feat(ai-chat): show per-file diffs for raw app edits (#11521)

* feat(ai-chat): show per-file diffs for raw app edits

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix: keep reference runnable writes on the generic tool card

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix: extend diff row highlights across horizontal overflow

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Guilhem
2026-10-05 14:21:59 +00:00
committed by GitHub
co-authored by Claude Opus 5.5
parent 4f30982ce3
commit ec0883a627
9 changed files with 371 additions and 103 deletions
@@ -24,6 +24,7 @@
const diffRows = $derived(diffLines ?? toolDiffLines(diff, streaming))
const visibleRows = $derived(visibleToolDiffRows(diffRows, expansions))
const language = $derived(toolCodeDiffLanguage(diff.lang))
const beforeLanguage = $derived(toolCodeDiffLanguage(diff.beforeLang ?? diff.lang))
const lastLines = $derived.by(() => {
let old = 0
let current = 0
@@ -38,10 +39,12 @@
`max(0.875rem, ${String(Math.max(lastLines.old, lastLines.current)).length}ch)`
)
const highlighted = $derived.by(() => {
if (!hljs.getLanguage(language.name)) hljs.registerLanguage(language.name, language.register)
for (const { name, register } of [language, beforeLanguage]) {
if (!hljs.getLanguage(name)) hljs.registerLanguage(name, register)
}
return {
original: highlightedSourceLines(diff.before, language.name, lastLines.old),
original: highlightedSourceLines(diff.before, beforeLanguage.name, lastLines.old),
modified: highlightedSourceLines(diff.after, language.name, lastLines.current)
}
})
@@ -82,50 +85,54 @@
class="tool-code-diff max-h-[400px] overflow-auto bg-surface-tertiary text-xs leading-[18px] text-primary"
style="--line-number: {lineNumberWidth}"
>
{#each visibleRows as row, index ('key' in row ? `${row.kind}:${row.key}` : `line:${row.oldLine}:${row.newLine}:${index}`)}
{#if row.kind === 'collapsed' || row.kind === 'omitted'}
<Button
unifiedSize="2xs"
variant="subtle"
btnClasses="grid min-h-7 w-full grid-cols-[var(--line-number)_calc(2.0625rem_+_var(--line-number))_minmax(0,1fr)] items-center gap-0 rounded-none bg-surface-secondary px-0 py-1 text-left text-xs font-normal leading-[18px] text-hint hover:bg-surface-hover"
onclick={() => expand(row.key)}
>
<span class="text-right">...</span>
<span class="grid grid-cols-[1.125rem_var(--line-number)_0.9375rem]">
<span></span><span class="text-right">...</span><span>...</span>
</span>
<span>{row.count} {row.kind === 'collapsed' ? 'unchanged lines' : 'more lines'}</span>
</Button>
{:else}
<div
class="diff-line-{row.kind} grid grid-cols-[var(--line-number)_calc(2.0625rem_+_var(--line-number))_minmax(0,1fr)]"
>
<span class="text-right text-hint">{row.oldLine ?? ''}</span>
<span class="grid grid-cols-[1.125rem_var(--line-number)_0.9375rem] text-hint">
<span></span><span class="text-right">{row.newLine ?? ''}</span><span
>{row.kind === 'added' ? '+' : row.kind === 'removed' ? '-' : ''}</span
>
</span>
<span
class="relative min-w-0 whitespace-pre {row.kind === 'added'
? 'bg-green-500/20'
: row.kind === 'removed'
? 'bg-red-500/20'
: ''}"
<!-- Rows take this wrapper's width, so the longest line sets it: sized to the scroller
instead, a row's background stops at the visible edge when a line overflows. -->
<div class="w-max min-w-full">
{#each visibleRows as row, index ('key' in row ? `${row.kind}:${row.key}` : `line:${row.oldLine}:${row.newLine}:${index}`)}
{#if row.kind === 'collapsed' || row.kind === 'omitted'}
<Button
unifiedSize="2xs"
variant="subtle"
btnClasses="grid min-h-7 w-full grid-cols-[var(--line-number)_calc(2.0625rem_+_var(--line-number))_minmax(0,1fr)] items-center gap-0 rounded-none bg-surface-secondary px-0 py-1 text-left text-xs font-normal leading-[18px] text-hint hover:bg-surface-hover"
onclick={() => expand(row.key)}
>
{#each row.changedRanges ?? [] as range}
<span
class="pointer-events-none absolute top-0 z-0 h-[18px] {row.kind === 'added'
? 'bg-green-500/25'
: 'bg-red-500/20'}"
style={rangeStyle(row.content, range)}
></span>
{/each}
<span class="relative z-[1]">{@html highlightedLine(row) || '&nbsp;'}</span>
</span>
</div>
{/if}
{/each}
<span class="text-right">...</span>
<span class="grid grid-cols-[1.125rem_var(--line-number)_0.9375rem]">
<span></span><span class="text-right">...</span><span>...</span>
</span>
<span>{row.count} {row.kind === 'collapsed' ? 'unchanged lines' : 'more lines'}</span>
</Button>
{:else}
<div
class="diff-line-{row.kind} grid grid-cols-[var(--line-number)_calc(2.0625rem_+_var(--line-number))_minmax(0,1fr)]"
>
<span class="text-right text-hint">{row.oldLine ?? ''}</span>
<span class="grid grid-cols-[1.125rem_var(--line-number)_0.9375rem] text-hint">
<span></span><span class="text-right">{row.newLine ?? ''}</span><span
>{row.kind === 'added' ? '+' : row.kind === 'removed' ? '-' : ''}</span
>
</span>
<span
class="relative min-w-0 whitespace-pre {row.kind === 'added'
? 'bg-green-500/20'
: row.kind === 'removed'
? 'bg-red-500/20'
: ''}"
>
{#each row.changedRanges ?? [] as range}
<span
class="pointer-events-none absolute top-0 z-0 h-[18px] {row.kind === 'added'
? 'bg-green-500/25'
: 'bg-red-500/20'}"
style={rangeStyle(row.content, range)}
></span>
{/each}
<span class="relative z-[1]">{@html highlightedLine(row) || '&nbsp;'}</span>
</span>
</div>
{/if}
{/each}
</div>
</div>
<style>
@@ -4,7 +4,7 @@
import ToolCodeDiffView from './ToolCodeDiffView.svelte'
import ToolPreviewCard from './ToolPreviewCard.svelte'
import {
argumentsCarryWholeFile,
diffIsWholeFile,
diffLineCounts,
toolCodeDiff,
toolDiffLineCounts,
@@ -25,16 +25,17 @@
// Keyed by call id: a bare flag would carry the expansion onto the next message that
// reuses this instance. An active or failed call opens by itself, and remains open
// when it settles so the new diff does not disappear under the user. A call whose arguments
// are a whole file (`write_script`, full-code `edit_code`) opens only on failure. The key
// also changes on failure, so a collapse made while running cannot hide the error.
// when it settles so the new diff does not disappear under the user. A call whose diff is a
// whole file (`write_script`, full-code `edit_code`, raw app writes and deletes) opens only
// on failure. The key also changes on failure, so a collapse made while running cannot hide
// the error.
const toggleKey = $derived(`${message.tool_call_id}:${message.error ? 'error' : ''}`)
let toggled = $state<{ id: string; open: boolean } | undefined>(undefined)
const opensByDefault = $derived(
Boolean(
message.error ||
(hasDiff &&
!argumentsCarryWholeFile(message) &&
!diffIsWholeFile(message) &&
(message.isLoading || message.isQueued || message.isStreamingArguments))
)
)
@@ -43,9 +44,7 @@
toggled = { id: toggleKey, open: true }
}
})
const expanded = $derived(
hasBody && (toggled?.id === toggleKey ? toggled.open : opensByDefault)
)
const expanded = $derived(hasBody && (toggled?.id === toggleKey ? toggled.open : opensByDefault))
const lines = $derived(diff && expanded ? toolDiffLines(diff, isStreaming) : undefined)
const counts = $derived(
diff && expanded
@@ -141,7 +141,7 @@
// A call that inspected a run rather than starting one gets the same card, bound to
// the job it named — what happened in a run reads the same either way.
const isRunCard = $derived(Boolean(message.runForm || message.inspectedRun))
const isDiffCard = $derived(Boolean(message.codeDiff) || hasToolCodeDiff(message.toolName))
const isDiffCard = $derived(hasToolCodeDiff(message))
// The preview chip sits on the header row (to the right of the tool-call text);
// shown once the tool settled, never while loading/erroring/awaiting confirmation.
@@ -4255,6 +4255,86 @@ describe('global AI tools', () => {
expect(getBackendDraft('raw_app', 'f/apps/report', { workspace: WORKSPACE })).toBeUndefined()
})
it('records the diff of the one raw app file a call changed', async () => {
seedBackendDraft(
'raw_app',
'u/admin/diffed_app',
{
summary: 'Diffed',
files: { '/index.tsx': 'untouched', '/styles.css': 'a { color: red; }\n' },
runnables: {
go: {
name: 'Go',
type: 'inline',
inlineScript: { language: 'python3', content: 'x = 1' }
},
greet: {
name: 'Greet',
type: 'inline',
inlineScript: { language: 'bun', content: 'export const main = () => 1' }
}
},
data: { tables: [] }
},
{ workspace: WORKSPACE }
)
const statuses: any[] = []
const callbacks = {
...toolCallbacks,
setToolStatus: (_toolId: string, status: any) => statuses.push(status)
}
await callGlobalTool(
'patch_app_file',
{
path: 'u/admin/diffed_app',
file_path: '/styles.css',
old_string: 'red',
new_string: 'blue',
replace_all: false
},
callbacks
)
await callGlobalTool(
'delete_app_runnable',
{ path: 'u/admin/diffed_app', key: 'go' },
callbacks
)
await callGlobalTool(
'write_app_runnable',
{
path: 'u/admin/diffed_app',
key: 'ref',
runnable: { name: 'Ref', type: 'flow', path: 'u/admin/hello_flow' }
},
callbacks
)
await callGlobalTool(
'write_app_runnable',
{
path: 'u/admin/diffed_app',
key: 'greet',
runnable: {
name: 'Greet',
type: 'inline',
inlineScript: { language: 'python3', content: 'def main(): return 1' }
}
},
callbacks
)
expect(statuses.filter((status) => status.codeDiff).map((status) => status.codeDiff)).toEqual([
{ before: 'a { color: red; }\n', after: 'a { color: blue; }\n', lang: 'css' },
{ before: 'x = 1', after: '', lang: 'python' },
{
before: 'export const main = () => 1',
after: 'def main(): return 1',
lang: 'python',
beforeLang: 'typescript'
}
])
})
it('does not persist a raw app draft when delete_app_file validation fails', async () => {
vi.mocked(AppService.getAppByPath).mockResolvedValueOnce({
path: 'f/apps/report',
@@ -149,6 +149,7 @@ import {
type ToolDisplayAction
} from '../shared'
import { scriptLangToEditorLang } from '$lib/scripts'
import { appFileEditorLang } from '../toolCodeDiff'
import { searchDocsTool, readDocsPageTool } from '../docs/core'
import { createDbSchemaTool } from '../script/core'
import type { ContextElement } from '../context'
@@ -5300,10 +5301,12 @@ function maybeAttachPreviewCard(
function finishAppDraftWrite(
result: DraftPersistResult,
ctx: WriteDraftCtx,
onSaved: () => { content: string; message: string; warning?: string }
onSaved: () => { content: string; message: string; warning?: string },
codeDiff?: ToolCodeDiff
): string {
const failure = draftWriteFailure(result, ctx)
if (failure) return failure
if (codeDiff) ctx.toolCallbacks.setToolStatus(ctx.toolId, { codeDiff })
ctx.toolCallbacks.onItemModified?.(result.itemKind, result.storagePath)
maybeAttachPreviewCard(ctx, result.itemKind, result.item.path)
const { content, message, warning } = onSaved()
@@ -6842,12 +6845,18 @@ async function writeAppFile(
})
const { value } = await loadAppDraftValue(args.path, workspace)
const before = value.files[target.filePath] ?? ''
value.files = { ...value.files, [target.filePath]: args.content }
const result = await saveAppDraft(workspace, args.path, value)
return finishAppDraftWrite(result, ctx, () => ({
content: `Updated ${target.filePath} in app "${args.path}"`,
message: `Updated draft app "${args.path}" with frontend file "${target.filePath}".`
}))
return finishAppDraftWrite(
result,
ctx,
() => ({
content: `Updated ${target.filePath} in app "${args.path}"`,
message: `Updated draft app "${args.path}" with frontend file "${target.filePath}".`
}),
{ before, after: args.content, lang: appFileEditorLang(target.filePath) }
)
}
async function deleteAppFile(
@@ -6871,13 +6880,18 @@ async function deleteAppFile(
if (!(target.filePath in value.files)) {
throw new Error(`Frontend file "${target.filePath}" not found in app "${args.path}".`)
}
const { [target.filePath]: _removed, ...remaining } = value.files
const { [target.filePath]: removed, ...remaining } = value.files
value.files = remaining
const result = await saveAppDraft(workspace, args.path, value)
return finishAppDraftWrite(result, ctx, () => ({
content: `Removed ${target.filePath} from app "${args.path}"`,
message: `Removed "${target.filePath}" from draft app "${args.path}".`
}))
return finishAppDraftWrite(
result,
ctx,
() => ({
content: `Removed ${target.filePath} from app "${args.path}"`,
message: `Removed "${target.filePath}" from draft app "${args.path}".`
}),
{ before: removed, after: '', lang: appFileEditorLang(target.filePath) }
)
}
async function patchAppFile(
@@ -6948,10 +6962,15 @@ async function patchAppFile(
}
const result = await saveAppDraft(workspace, path, value)
return finishAppDraftWrite(result, ctx, () => ({
content: `Patched ${target.filePath} in app "${path}"`,
message: `Patched "${target.filePath}" in draft app "${path}".`
}))
return finishAppDraftWrite(
result,
ctx,
() => ({
content: `Patched ${target.filePath} in app "${path}"`,
message: `Patched "${target.filePath}" in draft app "${path}".`
}),
{ before: currentContent, after: updated, lang: appFileEditorLang(target.filePath) }
)
}
async function recomputeAppPolicy(value: AppDraftValue): Promise<void> {
@@ -6982,15 +7001,48 @@ async function writeAppRunnable(
await recomputeAppPolicy(value)
const undeployed = await undeployedRunnableTargets(workspace, { [key]: persisted })
const result = await saveAppDraft(workspace, path, value)
return finishAppDraftWrite(result, ctx, () => ({
content: `Updated runnable "${key}" in app "${path}"`,
message: `Updated draft app "${path}" with runnable "${key}".`,
warning: undeployed.length
? `This runnable points at an item that is NOT deployed (${undeployed[0]}), so it fails at runtime — ` +
`a path runnable runs the deployed item, never a draft. Offer to deploy just that item with ` +
`deploy_workspace_item; the app itself does not need deploying, since the preview runs its draft.`
: undefined
}))
return finishAppDraftWrite(
result,
ctx,
() => ({
content: `Updated runnable "${key}" in app "${path}"`,
message: `Updated draft app "${path}" with runnable "${key}".`,
warning: undeployed.length
? `This runnable points at an item that is NOT deployed (${undeployed[0]}), so it fails at runtime — ` +
`a path runnable runs the deployed item, never a draft. Offer to deploy just that item with ` +
`deploy_workspace_item; the app itself does not need deploying, since the preview runs its draft.`
: undefined
}),
inlineRunnableDiff(key, existing, persisted)
)
}
function inlineRunnableCode(runnable: PersistedRunnable | undefined): string | undefined {
return runnable?.type === 'inline' || runnable?.type === 'runnableByName'
? (runnable.inlineScript?.content ?? '')
: undefined
}
// A runnable that references a workspace or hub item has no code of its own to diff.
function inlineRunnableDiff(
key: string,
before: PersistedRunnable | undefined,
after: PersistedRunnable | undefined
): ToolCodeDiff | undefined {
const beforeCode = inlineRunnableCode(before)
const afterCode = inlineRunnableCode(after)
if (beforeCode === undefined && afterCode === undefined) return undefined
if (after !== undefined && afterCode === undefined) return undefined
const lang = (runnable: PersistedRunnable | undefined) =>
appFileEditorLang(`backend/${key}/main.${getInlineScriptExtension(runnable)}`)
const afterLang = lang(after ?? before)
const beforeLang = beforeCode === undefined ? afterLang : lang(before)
return {
before: beforeCode ?? '',
after: afterCode ?? '',
lang: afterLang,
...(beforeLang !== afterLang ? { beforeLang } : {})
}
}
/**
@@ -7090,14 +7142,19 @@ async function deleteAppRunnable(
if (!(key in value.runnables)) {
throw new Error(`Backend runnable "${key}" not found in app "${path}".`)
}
const { [key]: _removed, ...remaining } = value.runnables
const { [key]: removed, ...remaining } = value.runnables
value.runnables = remaining
await recomputeAppPolicy(value)
const result = await saveAppDraft(workspace, path, value)
return finishAppDraftWrite(result, ctx, () => ({
content: `Removed runnable "${key}" from app "${path}"`,
message: `Removed runnable "${key}" from draft app "${path}".`
}))
return finishAppDraftWrite(
result,
ctx,
() => ({
content: `Removed runnable "${key}" from app "${path}"`,
message: `Removed runnable "${key}" from draft app "${path}".`
}),
inlineRunnableDiff(key, removed as PersistedRunnable, undefined)
)
}
const triggerLabels: Record<TriggerKind, string> = {
@@ -622,6 +622,9 @@ export type ToolCodeDiff = {
after: string
/** Monaco language id. */
lang: string
/** Language of the before side, when the edit changed it (an inline runnable rewritten from
* TypeScript to Python). */
beforeLang?: string
}
export type ToolDisplayMessage = {
@@ -1,7 +1,7 @@
import { describe, expect, it } from 'vitest'
import hljs from 'highlight.js/lib/core'
import {
argumentsCarryWholeFile,
diffIsWholeFile,
diffLineCounts,
hasToolCodeDiff,
toolCodeDiff,
@@ -113,12 +113,31 @@ describe('toolCodeDiff', () => {
})
it('only marks tools with a defined argument diff as diff-capable', () => {
expect(hasToolCodeDiff('edit_script')).toBe(true)
expect(hasToolCodeDiff('edit_code')).toBe(true)
expect(hasToolCodeDiff('write_script')).toBe(true)
expect(hasToolCodeDiff('__proto__')).toBe(false)
expect(hasToolCodeDiff('toString')).toBe(false)
expect(hasToolCodeDiff(undefined)).toBe(false)
const call = (toolName: string | undefined, parameters?: unknown) =>
hasToolCodeDiff(message({ toolName, parameters }))
expect(call('edit_script')).toBe(true)
expect(call('edit_code')).toBe(true)
expect(call('write_script')).toBe(true)
expect(call('__proto__')).toBe(false)
expect(call('toString')).toBe(false)
expect(call(undefined)).toBe(false)
expect(
hasToolCodeDiff(
message({ toolName: 'delete_app_file', codeDiff: { before: 'a', after: '', lang: 'css' } })
)
).toBe(true)
})
it('keeps a reference runnable write on the generic card', () => {
const call = (parameters: unknown) =>
hasToolCodeDiff(message({ toolName: 'write_app_runnable', parameters }))
expect(call('{"path":"f/a","key":"go","runnable":{"name":"Go","type":"in')).toBe(true)
expect(call('{"path":"f/a","key":"go","runnable":{"name":"Go"')).toBe(true)
expect(call('{"path":"f/a","key":"go","runnable":{"name":"Go","type":"flow"')).toBe(false)
expect(call({ path: 'f/a', key: 'go', runnable: { type: 'script', path: 'f/s' } })).toBe(false)
expect(call({ path: 'f/a', key: 'go', runnable: { type: 'inline' } })).toBe(true)
})
it('ignores tool names inherited from Object.prototype', () => {
@@ -302,14 +321,38 @@ describe('toolCodeDiff', () => {
})
})
it('highlights streamed raw app edits by file', () => {
expect(
toolCodeDiff(
message({
toolName: 'patch_app_file',
parameters: '{"path":"f/a","file_path":"/src/App.tsx","old_string":"a","new_string":"b'
})
)
).toEqual({ before: 'a', after: 'b', lang: 'typescript' })
expect(
toolCodeDiff(
message({
toolName: 'write_app_runnable',
parameters:
'{"path":"f/a","key":"go","runnable":{"name":"Go","type":"inline","inlineScript":{"language":"python3","content":"def main'
})
)
).toEqual({ before: '', after: 'def main', lang: 'python' })
})
it('recognizes calls whose arguments are a whole file', () => {
const call = (
toolName: string,
parameters: unknown,
codeDiff?: ToolDisplayMessage['codeDiff']
) => argumentsCarryWholeFile(message({ toolName, parameters, codeDiff }))
) => diffIsWholeFile(message({ toolName, parameters, codeDiff }))
expect(call('write_script', { path: 'f/a', content: '' })).toBe(true)
expect(call('delete_app_file', { path: 'f/a', file_path: '/a.css' })).toBe(true)
expect(call('patch_app_file', { file_path: '/a.css', old_string: 'a', new_string: 'b' })).toBe(
false
)
expect(call('edit_code', '{"code":"x')).toBe(true)
expect(
call('edit_code', { code: 'x' }, { before: 'old', after: 'x', lang: 'typescript' })
@@ -24,17 +24,63 @@ const ARGS_DIFF_BY_TOOL: Record<string, (params: unknown) => ToolCodeDiff | unde
)
}
return argumentDiff(streamingEditArguments(params)) ?? fullContentArgumentDiff(params, 'code')
},
patch_app_file: (params) => {
const diff = argumentDiff(streamingEditArguments(params))
return diff && { ...diff, lang: appFileEditorLang(argumentString(params, 'file_path')) }
},
write_app_file: (params) => {
const diff = fullContentArgumentDiff(params, 'content')
return diff && { ...diff, lang: appFileEditorLang(argumentString(params, 'file_path')) }
},
write_app_runnable: (params) => {
const inlineScript =
params && typeof params === 'object'
? (params as { runnable?: { inlineScript?: unknown } }).runnable?.inlineScript
: params
const diff = fullContentArgumentDiff(inlineScript, 'content')
const language = argumentString(inlineScript, 'language')
return (
diff && { ...diff, lang: appFileEditorLang(language === 'python3' ? 'main.py' : 'main.ts') }
)
}
}
const LANGUAGE_BY_APP_FILE_EXTENSION: Record<string, string> = {
ts: 'typescript',
tsx: 'typescript',
mts: 'typescript',
js: 'javascript',
jsx: 'javascript',
mjs: 'javascript',
py: 'python',
css: 'css',
html: 'xml',
svelte: 'xml',
vue: 'xml',
json: 'json',
yaml: 'yaml',
yml: 'yaml'
}
// A raw app's backend runnables are named `backend/<key>/main.ts|py`, so their extension
// picks the language like any frontend file's.
export function appFileEditorLang(filePath: string | undefined): string {
const extension = filePath?.split('/').pop()?.split('.').slice(1).pop()?.toLowerCase()
return (extension && LANGUAGE_BY_APP_FILE_EXTENSION[extension]) ?? 'plaintext'
}
function argumentString(params: unknown, key: string): string | undefined {
if (params && typeof params === 'object') {
const value = (params as Record<string, unknown>)[key]
return typeof value === 'string' ? value : undefined
}
return typeof params === 'string' ? partialJsonString(params, key) : undefined
}
function fullContentArgumentDiff(params: unknown, key: string): ToolCodeDiff | undefined {
const content =
params && typeof params === 'object'
? (params as Record<string, unknown>)[key]
: typeof params === 'string'
? partialJsonString(params, key)
: undefined
return typeof content === 'string' ? { before: '', after: content, lang: 'plaintext' } : undefined
const content = argumentString(params, key)
return content !== undefined ? { before: '', after: content, lang: 'plaintext' } : undefined
}
function argumentDiff(
@@ -104,8 +150,22 @@ function partialJsonStrings(partialJson: string, key: string): string[] {
return result
}
export function hasToolCodeDiff(toolName: string | undefined): boolean {
return toolName !== undefined && Object.hasOwn(ARGS_DIFF_BY_TOOL, toolName)
export function hasToolCodeDiff(message: ToolDisplayMessage): boolean {
if (message.codeDiff) return true
if (!message.toolName || !Object.hasOwn(ARGS_DIFF_BY_TOOL, message.toolName)) return false
// A runnable that references a workspace or hub item has no code to diff, so it keeps the
// generic card, whose details show the target and static inputs. Until `type` has streamed
// past a prefix of "inline", the call is assumed inline so an inline runnable does not
// switch cards mid-stream.
if (message.toolName === 'write_app_runnable') {
const params = message.parameters
const type =
params && typeof params === 'object'
? (params as { runnable?: { type?: unknown } }).runnable?.type
: argumentString(params, 'type')
return type === undefined || (typeof type === 'string' && 'inline'.startsWith(type))
}
return true
}
export function toolCodeDiff(message: ToolDisplayMessage): ToolCodeDiff | undefined {
@@ -116,11 +176,19 @@ export function toolCodeDiff(message: ToolDisplayMessage): ToolCodeDiff | undefi
: undefined
}
const WHOLE_FILE_TOOLS = new Set([
'write_script',
'write_app_file',
'write_app_runnable',
'delete_app_file',
'delete_app_runnable'
])
// Decided by tool and argument shape, never by the saved `codeDiff`: an overwrite's saved diff
// has a before side, and clearing a script leaves both argument sides empty, yet both calls sent
// a whole file.
export function argumentsCarryWholeFile(message: ToolDisplayMessage): boolean {
if (message.toolName === 'write_script') return true
// a whole file. A delete's diff is the whole removed file, saved while the call still runs.
export function diffIsWholeFile(message: ToolDisplayMessage): boolean {
if (message.toolName && WHOLE_FILE_TOOLS.has(message.toolName)) return true
if (message.toolName !== 'edit_code') return false
const params = message.parameters
if (typeof params === 'string') return /"code"\s*:/.test(params)
@@ -1,4 +1,13 @@
import { csharp, go, java, json, plaintext, type LanguageType } from 'svelte-highlight/languages'
import {
csharp,
css,
go,
java,
json,
plaintext,
xml,
type LanguageType
} from 'svelte-highlight/languages'
import graphql from 'svelte-highlight/languages/graphql'
import javascript from 'svelte-highlight/languages/javascript'
import php from 'svelte-highlight/languages/php'
@@ -32,6 +41,8 @@ const LANGUAGE_BY_MONACO: Record<string, LanguageType<string>> = {
r,
go,
ruby,
css,
xml,
text: plaintext,
plaintext
}