fix(ai-chat): stop echoing app draft value in global chat write tool results (#9658)

finishAppDraftWrite returned `item: result.item`, whose `value` is the entire
app draft (every frontend file body and inline runnable). Each write_app_file /
patch_app_file / write_app_runnable therefore re-sent the whole app back to the
model; on a large app a few edits overflow the 200k context window.

This restores #9530 (which removed the echo) — the DB-backed-draft refactor
(#9601) reintroduced it by routing all app writes through this shared helper
with `item:` re-added. Write results now return only `{ success, message }`,
matching the flow write tools. Adds a regression test asserting the value is
not echoed.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
centdix
2026-06-18 23:21:49 +02:00
committed by GitHub
co-authored by Claude Opus 4.8
parent 3f5f211a22
commit 2fed808b9e
2 changed files with 37 additions and 3 deletions
@@ -783,6 +783,40 @@ describe('global AI tools', () => {
expect(getBackendDraft('raw_app', 'u/admin/live_app', { workspace: WORKSPACE })).toBeUndefined()
})
it('does not echo the app value back to the model on write', async () => {
const sentinel = 'SENTINEL_DO_NOT_ECHO_DEADBEEF'
seedBackendDraft(
'raw_app',
'',
{
summary: 'Echo check',
files: { '/src/App.tsx': `export default function App() { return '${sentinel}' }` },
runnables: {},
data: { tables: [] }
},
{ workspace: WORKSPACE }
)
UserDraft.setLiveEditorDraft({
workspace: WORKSPACE,
itemKind: 'raw_app',
storagePath: '',
effectivePath: 'u/admin/echo_app'
})
const raw = await callGlobalTool('write_app_file', {
path: 'u/admin/echo_app',
file_path: '/src/New.tsx',
content: 'export default function New() { return null }'
})
const parsed = JSON.parse(raw)
expect(parsed.success).toBe(true)
expect(parsed.item).toBeUndefined()
// Neither the pre-existing file body nor the just-written one is resent.
expect(raw).not.toContain(sentinel)
expect(raw).not.toContain('function New')
})
it('discards a draft without deleting the workspace item', async () => {
await callGlobalTool('write_script', {
path: 'f/scripts/discard-me',
@@ -2399,13 +2399,13 @@ function draftWriteFailure(result: DraftPersistResult, ctx: WriteDraftCtx): stri
function finishAppDraftWrite(
result: DraftPersistResult,
ctx: WriteDraftCtx,
onSaved: (item: WorkspaceItem) => { content: string; message: string }
onSaved: () => { content: string; message: string }
): string {
const failure = draftWriteFailure(result, ctx)
if (failure) return failure
const { content, message } = onSaved(result.item)
const { content, message } = onSaved()
ctx.toolCallbacks.setToolStatus(ctx.toolId, { content, result: 'Saved as draft' })
return JSON.stringify({ success: true, message, item: result.item }, null, 2)
return JSON.stringify({ success: true, message }, null, 2)
}
function finishDraftWrite(