Skip to content

Commit d70cf9f

Browse files
committed
fix: refine gemini cli tool result presents
1 parent e99fe13 commit d70cf9f

2 files changed

Lines changed: 233 additions & 28 deletions

File tree

server/gemini-cli.js

Lines changed: 61 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,65 @@ function extractTodosFromShellCommand(command) {
169169
return parseTodosFromMarkdown(heredocMatch[1]);
170170
}
171171

172+
async function enrichListDirectoryResult(toolContext, outputText, workingDir) {
173+
if (!toolContext) return outputText;
174+
const rawName = String(toolContext.rawToolName || '').toLowerCase();
175+
if (rawName !== 'list_directory') return outputText;
176+
177+
const dirInput = String(toolContext.toolInput?.dir_path || toolContext.toolInput?.path || '.');
178+
const resolvedDir = path.isAbsolute(dirInput)
179+
? path.resolve(dirInput)
180+
: path.resolve(workingDir, dirInput);
181+
const normalizedRoot = path.resolve(workingDir) + path.sep;
182+
if (!resolvedDir.startsWith(normalizedRoot) && resolvedDir !== path.resolve(workingDir)) {
183+
return outputText;
184+
}
185+
186+
try {
187+
const entries = await fs.readdir(resolvedDir, { withFileTypes: true });
188+
const sortedEntries = [...entries].sort((a, b) => {
189+
if (a.isDirectory() !== b.isDirectory()) {
190+
return a.isDirectory() ? -1 : 1;
191+
}
192+
return a.name.localeCompare(b.name);
193+
});
194+
const visibleEntries = sortedEntries
195+
.slice(0, 200)
196+
.map((entry) => ({
197+
name: entry.name,
198+
isDirectory: entry.isDirectory()
199+
}));
200+
201+
const baseRel = path.relative(workingDir, resolvedDir).replace(/\\/g, '/');
202+
const relDir = baseRel && baseRel !== '' ? baseRel : '.';
203+
const items = visibleEntries.map((entry) => {
204+
const relPath = relDir === '.'
205+
? entry.name
206+
: `${relDir}/${entry.name}`;
207+
return {
208+
name: entry.name,
209+
path: `${relPath}${entry.isDirectory ? '/' : ''}`,
210+
isDirectory: entry.isDirectory
211+
};
212+
});
213+
214+
return JSON.stringify(
215+
{
216+
summary: outputText,
217+
directory: relDir,
218+
files: items.map((item) => item.path),
219+
items,
220+
total: entries.length,
221+
truncated: entries.length > visibleEntries.length
222+
},
223+
null,
224+
2
225+
);
226+
} catch {
227+
return outputText;
228+
}
229+
}
230+
172231
/**
173232
* Ensures a session directory exists and creates a basic JSONL metadata file if it doesn't.
174233
* This helps VibeLab discover the session even if the CLI hasn't written to it yet.
@@ -668,10 +727,11 @@ export async function spawnGemini(command, options = {}, ws) {
668727
case 'tool_result':
669728
if (response.output || response.content) {
670729
const rawResult = response.output !== undefined ? response.output : response.content;
671-
const outputText = typeof rawResult === 'string' ? rawResult : JSON.stringify(rawResult, null, 2);
730+
const baseOutputText = typeof rawResult === 'string' ? rawResult : JSON.stringify(rawResult, null, 2);
672731
const resultToolCallId = response.id || response.tool_use_id;
673732
const ctx = resultToolCallId ? toolCallContext.get(resultToolCallId) : null;
674733
const isError = Boolean(response.is_error || response.error);
734+
const outputText = await enrichListDirectoryResult(ctx, baseOutputText, workingDir);
675735

676736
await appendToSessionFile(capturedSessionId || sessionId || initialKey, {
677737
type: 'tool_result',

src/components/chat/tools/configs/toolConfigs.ts

Lines changed: 172 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,127 @@ export interface ToolDisplayConfig {
4141
};
4242
}
4343

44+
function formatActivateSkillResult(content: unknown): string {
45+
const raw = String(content || '').trim();
46+
if (!raw) return 'Skill activated.';
47+
48+
const lines = raw.split('\n');
49+
const treeStart = lines.findIndex((line) => /^(\/|~\/|[A-Za-z]:[\\/])/.test(line.trim()));
50+
if (treeStart === -1) return raw;
51+
52+
const beforeTree = lines.slice(0, treeStart).join('\n').trim();
53+
const treeSection = lines.slice(treeStart).join('\n').trim();
54+
55+
return `${beforeTree}\n\n\`\`\`text\n${treeSection}\n\`\`\``.trim();
56+
}
57+
58+
function parseJsonSafe(input: unknown): any | null {
59+
if (typeof input !== 'string') return null;
60+
try {
61+
return JSON.parse(input);
62+
} catch {
63+
return null;
64+
}
65+
}
66+
67+
function parseLsResultPayload(result: any): {
68+
directory: string;
69+
total: number;
70+
truncated: boolean;
71+
summary: string;
72+
files: string[];
73+
items: Array<{ name: string; path: string; isDirectory: boolean }>;
74+
} | null {
75+
const raw = result?.content ?? result;
76+
const parsed = typeof raw === 'object' && raw !== null ? raw : parseJsonSafe(String(raw || ''));
77+
if (!parsed || typeof parsed !== 'object') return null;
78+
79+
const directory = typeof parsed.directory === 'string' ? parsed.directory : '.';
80+
const summary = typeof parsed.summary === 'string' ? parsed.summary : '';
81+
const truncated = Boolean(parsed.truncated);
82+
const total = typeof parsed.total === 'number' ? parsed.total : 0;
83+
const files = Array.isArray(parsed.files)
84+
? parsed.files.filter((f: unknown) => typeof f === 'string')
85+
: [];
86+
87+
let items: Array<{ name: string; path: string; isDirectory: boolean }> = [];
88+
if (Array.isArray(parsed.items)) {
89+
items = parsed.items
90+
.map((item: any) => ({
91+
name: typeof item?.name === 'string' ? item.name : '',
92+
path: typeof item?.path === 'string' ? item.path : '',
93+
isDirectory: Boolean(item?.isDirectory)
94+
}))
95+
.filter((item) => item.path);
96+
}
97+
98+
if (items.length === 0 && files.length > 0) {
99+
items = files.map((filePath: string) => {
100+
const normalized = filePath.replace(/\\/g, '/');
101+
const isDirectory = normalized.endsWith('/');
102+
const clean = isDirectory ? normalized.slice(0, -1) : normalized;
103+
const parts = clean.split('/');
104+
return {
105+
name: parts[parts.length - 1] || clean,
106+
path: normalized,
107+
isDirectory
108+
};
109+
});
110+
}
111+
112+
return { directory, total, truncated, summary, files, items };
113+
}
114+
115+
function formatLsResultAsMarkdown(result: any): string {
116+
const payload = parseLsResultPayload(result);
117+
if (!payload) {
118+
const raw = String(result?.content || result || '').trim();
119+
return raw || 'No directory entries returned.';
120+
}
121+
122+
const sortedItems = [...payload.items].sort((a, b) => {
123+
if (a.isDirectory !== b.isDirectory) {
124+
return a.isDirectory ? -1 : 1;
125+
}
126+
return a.name.localeCompare(b.name);
127+
});
128+
129+
const visibleCount = sortedItems.length;
130+
const dirCount = sortedItems.filter((item) => item.isDirectory).length;
131+
const fileCount = visibleCount - dirCount;
132+
const treeHeader = payload.directory === '.' ? './' : `${payload.directory.replace(/\/+$/, '')}/`;
133+
const treeLines = [treeHeader];
134+
135+
sortedItems.forEach((item, index) => {
136+
const prefix = index === visibleCount - 1 ? '└── ' : '├── ';
137+
treeLines.push(`${prefix}${item.name}${item.isDirectory ? '/' : ''}`);
138+
});
139+
140+
if (visibleCount === 0) {
141+
treeLines.push('└── (empty)');
142+
}
143+
144+
const lines = [
145+
`**Path:** \`${payload.directory}\``,
146+
`**Entries:** ${visibleCount}${payload.total > visibleCount ? ` (showing first ${visibleCount} of ${payload.total})` : ''}`,
147+
`**Breakdown:** ${dirCount} dirs, ${fileCount} files`,
148+
'',
149+
'```text',
150+
treeLines.join('\n'),
151+
'```'
152+
];
153+
154+
if (payload.truncated) {
155+
lines.push('', '_Results truncated to keep the panel responsive._');
156+
}
157+
158+
if (payload.summary && !/listed\s+\d+\s+items?/i.test(payload.summary)) {
159+
lines.push('', `**Tool output:** ${payload.summary.trim()}`);
160+
}
161+
162+
return lines.join('\n');
163+
}
164+
44165
export const TOOL_CONFIGS: Record<string, ToolDisplayConfig> = {
45166
// ============================================================================
46167
// COMMAND TOOLS
@@ -85,6 +206,29 @@ export const TOOL_CONFIGS: Record<string, ToolDisplayConfig> = {
85206
}
86207
},
87208

209+
activate_skill: {
210+
input: {
211+
type: 'one-line',
212+
label: 'Activate Skill',
213+
getValue: (input) => input.name || input.skill || 'skill',
214+
action: 'none',
215+
colorScheme: {
216+
primary: 'text-indigo-600 dark:text-indigo-300',
217+
border: 'border-indigo-400 dark:border-indigo-500',
218+
icon: 'text-indigo-500 dark:text-indigo-400'
219+
}
220+
},
221+
result: {
222+
type: 'collapsible',
223+
defaultOpen: true,
224+
title: 'Skill activation details',
225+
contentType: 'markdown',
226+
getContentProps: (result) => ({
227+
content: formatActivateSkillResult(result?.content || result)
228+
})
229+
}
230+
},
231+
88232
// ============================================================================
89233
// FILE OPERATION TOOLS
90234
// ============================================================================
@@ -246,6 +390,33 @@ export const TOOL_CONFIGS: Record<string, ToolDisplayConfig> = {
246390
}
247391
},
248392

393+
LS: {
394+
input: {
395+
type: 'one-line',
396+
label: 'LS',
397+
getValue: (input) => input.dir_path || input.path || '.',
398+
action: 'none',
399+
colorScheme: {
400+
primary: 'text-gray-700 dark:text-gray-300',
401+
border: 'border-gray-300 dark:border-gray-600',
402+
icon: 'text-gray-500 dark:text-gray-400'
403+
}
404+
},
405+
result: {
406+
type: 'collapsible',
407+
defaultOpen: true,
408+
title: (result) => {
409+
const payload = parseLsResultPayload(result);
410+
const count = payload?.items?.length ?? 0;
411+
return `Directory listing${count > 0 ? ` (${count})` : ''}`;
412+
},
413+
contentType: 'markdown',
414+
getContentProps: (result) => ({
415+
content: formatLsResultAsMarkdown(result)
416+
})
417+
}
418+
},
419+
249420
// ============================================================================
250421
// TODO TOOLS
251422
// ============================================================================
@@ -608,7 +779,7 @@ const GEMINI_TOOL_ALIASES: Record<string, string> = {
608779
google_web_search: 'WebSearch',
609780
web_fetch: 'WebFetch',
610781
complete_task: 'Default',
611-
activate_skill: 'Default',
782+
activate_skill: 'activate_skill',
612783
save_memory: 'Default',
613784
get_internal_docs: 'Default'
614785
};
@@ -619,32 +790,6 @@ for (const [alias, target] of Object.entries(GEMINI_TOOL_ALIASES)) {
619790
}
620791
}
621792

622-
if (!TOOL_CONFIGS.LS) {
623-
TOOL_CONFIGS.LS = {
624-
input: {
625-
type: 'one-line',
626-
label: 'LS',
627-
getValue: (input) => input.dir_path || input.path || '.',
628-
action: 'none',
629-
colorScheme: {
630-
primary: 'text-gray-700 dark:text-gray-300',
631-
border: 'border-gray-300 dark:border-gray-600',
632-
icon: 'text-gray-500 dark:text-gray-400'
633-
}
634-
},
635-
result: {
636-
type: 'collapsible',
637-
defaultOpen: false,
638-
title: 'Directory listing',
639-
contentType: 'text',
640-
getContentProps: (result) => ({
641-
content: String(result?.content || ''),
642-
format: 'plain'
643-
})
644-
}
645-
};
646-
}
647-
648793
/**
649794
* Get configuration for a tool, with fallback to default
650795
*/

0 commit comments

Comments
 (0)