From 88e3a41fa150628f8a597ad376e8def48c0fd916 Mon Sep 17 00:00:00 2001 From: Guilhem Lemouel Date: Mon, 20 Jul 2026 14:52:07 +0200 Subject: [PATCH] feat(ai): show lint details on demand in the chat MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The lint tool call rendered as a bare summary line with nothing behind it. Mark it as having details so the individual problems can be opened, and pass the formatted output as the tool result — without it the details panel reads "No result yet". Details stay collapsed on success, since the header already carries the counts. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../copilot/chat/global/core.test.ts | 33 +++++++++++++++++++ .../components/copilot/chat/global/core.ts | 32 +++++++++++++----- 2 files changed, 57 insertions(+), 8 deletions(-) diff --git a/frontend/src/lib/components/copilot/chat/global/core.test.ts b/frontend/src/lib/components/copilot/chat/global/core.test.ts index 407870433d..eda40d0615 100644 --- a/frontend/src/lib/components/copilot/chat/global/core.test.ts +++ b/frontend/src/lib/components/copilot/chat/global/core.test.ts @@ -245,6 +245,19 @@ vi.mock('./rawAppBundlerBridge', () => ({ })) })) +// Monaco cannot load in the node test environment, and core.ts imports the lint +// service lazily precisely to keep it out of this import graph. +vi.mock('$lib/components/lint/headlessLint', () => ({ + canLintHeadless: (lang: string) => lang === 'bun', + lintCode: vi.fn(async () => ({ + errorCount: 1, + warningCount: 0, + errors: [{ startLineNumber: 2, message: "Type 'string' is not assignable to type 'number'." }], + warnings: [], + contentMismatch: false + })) +})) + vi.mock('$lib/infer', async () => ({ ...(await vi.importActual('$lib/infer')), // Avoid the wasm parser in unit tests: the script deploy path infers the arg @@ -433,6 +446,26 @@ describe('global AI tools', () => { ) }) + it('surfaces lint output as the tool result so the details panel can show it', async () => { + vi.mocked(ScriptService.getScriptByPath).mockResolvedValueOnce({ + path: 'u/admin/probe', + content: 'const a: number = "x"', + language: 'bun', + summary: '' + } as any) + + const result = await callGlobalTool('get_lint_errors', { + kind: 'script', + path: 'u/admin/probe' + }) + + expect(result).toContain("Type 'string' is not assignable to type 'number'.") + expect(toolCallbacks.setToolStatus).toHaveBeenCalledWith( + 'test-get_lint_errors', + expect.objectContaining({ result }) + ) + }) + it('defaults list_runs to 30 results when no limit is given', async () => { await callGlobalTool('list_runs', {}) expect(JobService.listJobs).toHaveBeenCalledWith( diff --git a/frontend/src/lib/components/copilot/chat/global/core.ts b/frontend/src/lib/components/copilot/chat/global/core.ts index bca93f83f7..6ffda6cbec 100644 --- a/frontend/src/lib/components/copilot/chat/global/core.ts +++ b/frontend/src/lib/components/copilot/chat/global/core.ts @@ -2751,6 +2751,9 @@ export const globalTools: Tool<{}>[] = [ 'get_lint_errors', 'Type-check a script, flow module, or raw app backend runnable and report its errors and warnings. Supports TypeScript, JavaScript, Python, Go, Deno and Bash.' ), + // The header carries the counts; the individual problems are worth a look but not + // worth the vertical space by default. + showDetails: true, fn: async (ctx) => { const parsed = getLintErrorsSchema.parse(ctx.args) return getLintErrors(parsed, ctx) @@ -3812,12 +3815,20 @@ async function checkAppFrontend(path: string, ctx: WriteDraftCtx): Promise 0 ? `${result.warningCount} warning(s)` : 'no issues' - toolCallbacks.setToolStatus(toolId, { content: `Linted ${target.label}: ${summary}` }) - let response = formatScriptLintResult(result) if (result.unavailableServers?.length) { response += `\n\nNote: the ${result.unavailableServers.join(' and ')} language server${result.unavailableServers.length > 1 ? 's did' : ' did'} not respond, so some problems may not be listed. Treat a clean result as inconclusive.` @@ -3864,6 +3875,11 @@ async function getLintErrors(args: LintTargetArgs, ctx: WriteDraftCtx): Promise< if (result.contentMismatch) { response += `\n\nNote: an editor is currently open on this code and its buffer differs from the draft. The results above are for what that editor shows.` } + // The result has to reach the tool display, or its details panel reads "No result yet". + toolCallbacks.setToolStatus(toolId, { + content: `Linted ${target.label}: ${summary}`, + result: response + }) return response }