Skip to content

Commit 4eea753

Browse files
committed
fix: preserve conversation context in /clear-screen
The previous implementation replaced the messages array with a single empty-content assistant message to preserve prompt cache. This caused two problems: 1. Claude lost all conversation context and treated the next message as the start of a new session ("This is the first message...") 2. The prompt cache was not actually preserved — cache breakpoints are set via cache_control in the API request, and the usage field in the saved assistant message is a response from the previous call, not an instruction for the next one. The new implementation hides messages from the UI without removing them from the messages array: - On /clear-screen, all current message UUIDs (first 24 chars) are collected into a globalThis.__tweakccHiddenUUIDs Set. - The render filter function (g97) is patched to check this Set and skip messages whose UUID prefix matches. - Messages remain in the array and are sent to the API as usual, preserving full conversation context. UUID prefix (24 chars) is used because the render pipeline function lP() splits multi-content assistant messages into separate objects with modified UUIDs (original.slice(0,24) + hex index). Matching on the prefix ensures both original and split messages are hidden. The approach with a per-message flag (__tweakccHiddenFromUI) was tried first but failed: lP() creates new objects for assistant messages with a fixed set of fields, dropping custom properties. User messages worked because lP() uses object spread for string content, preserving extra fields — but assistant messages did not. After session resume, globalThis is empty, so previously hidden messages become visible again (acceptable trade-off).
1 parent 683c0af commit 4eea753

2 files changed

Lines changed: 114 additions & 13 deletions

File tree

src/patches/clearScreen.test.ts

Lines changed: 69 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,16 @@
11
import { describe, expect, it } from 'vitest';
22

3-
import { writeClearScreen } from './clearScreen';
3+
import { writeClearScreen, patchRenderFilter } from './clearScreen';
44

55
const cmds = Array.from({ length: 31 }, (_, i) => `c${i}`).join(',');
66
const slashCommandArray = `=>[${cmds}]`;
77

8+
const renderFilter =
9+
'function g97(H,$){if(H.type!=="user")return!0;if(H.isMeta){if(H.origin?.kind==="channel")return!0;return!1}if(H.isVisibleInTranscriptOnly&&!$)return!1;return!0}';
10+
811
const makeInput = (delimiter = ';') =>
912
'const x=1' +
13+
renderFilter +
1014
slashCommandArray +
1115
`${delimiter}let Z=G_H.useCallback(()=>{Nw.get(process.stdout)?.forceRedraw()})`;
1216

@@ -19,10 +23,30 @@ describe('clearScreen', () => {
1923
'globalThis.__tweakccForceRedraw=()=>Nw.get(process.stdout)?.forceRedraw()'
2024
);
2125
expect(result).toContain('name:"clear-screen"');
22-
expect(result).toContain('$.setMessages(');
26+
expect(result).toContain('__tweakccHiddenUUIDs');
2327
expect(result).toContain('globalThis.__tweakccForceRedraw?.()');
2428
});
2529

30+
it('preserves all messages for API context (hides via UUID set, does not remove)', () => {
31+
const result = writeClearScreen(makeInput());
32+
33+
expect(result).not.toBeNull();
34+
expect(result).toContain('__tweakccHiddenUUIDs=new Set(');
35+
expect(result).toContain('return[...m]');
36+
expect(result).not.toContain('content:[]');
37+
expect(result).not.toContain('return k?[');
38+
expect(result).not.toContain('return[]');
39+
});
40+
41+
it('patches render filter to check __tweakccHiddenUUIDs', () => {
42+
const result = writeClearScreen(makeInput());
43+
44+
expect(result).not.toBeNull();
45+
expect(result).toContain(
46+
'globalThis.__tweakccHiddenUUIDs?.has(H.uuid?.slice(0,24)))return!1;if(H.type!=="user")'
47+
);
48+
});
49+
2650
it('preserves original app:redraw callback', () => {
2751
const result = writeClearScreen(makeInput());
2852

@@ -45,11 +69,14 @@ describe('clearScreen', () => {
4569
expect(result).toBeNull();
4670
});
4771

48-
it('preserves fallback for assistant messages without usage', () => {
49-
const result = writeClearScreen(makeInput());
72+
it('returns null when render filter not found', () => {
73+
const input =
74+
'const x=1' +
75+
slashCommandArray +
76+
';let Z=G_H.useCallback(()=>{Nw.get(process.stdout)?.forceRedraw()})';
77+
const result = writeClearScreen(input);
5078

51-
expect(result).not.toBeNull();
52-
expect(result).toContain('if(!k&&m[i]?.type==="assistant")k=m[i]');
79+
expect(result).toBeNull();
5380
});
5481

5582
it('works with different delimiters before useCallback', () => {
@@ -60,3 +87,39 @@ describe('clearScreen', () => {
6087
}
6188
});
6289
});
90+
91+
describe('patchRenderFilter', () => {
92+
it('adds __tweakccHiddenUUIDs check at the start of the function', () => {
93+
const result = patchRenderFilter(renderFilter);
94+
95+
expect(result).not.toBeNull();
96+
expect(result).toContain(
97+
'function g97(H,$){if(globalThis.__tweakccHiddenUUIDs?.has(H.uuid?.slice(0,24)))return!1;if(H.type!=="user")'
98+
);
99+
});
100+
101+
it('preserves the rest of the function', () => {
102+
const result = patchRenderFilter(renderFilter);
103+
104+
expect(result).not.toBeNull();
105+
expect(result).toContain('if(H.isMeta)');
106+
expect(result).toContain('if(H.isVisibleInTranscriptOnly&&!$)return!1');
107+
});
108+
109+
it('returns null when pattern not found', () => {
110+
const result = patchRenderFilter('const x=1;');
111+
112+
expect(result).toBeNull();
113+
});
114+
115+
it('works with different function and argument names', () => {
116+
const input =
117+
'function abc(X$,Y$){if(X$.type!=="user")return!0;if(X$.isMeta){if(X$.origin?.kind==="channel")return!0;return!1}return!0}';
118+
const result = patchRenderFilter(input);
119+
120+
expect(result).not.toBeNull();
121+
expect(result).toContain(
122+
'if(globalThis.__tweakccHiddenUUIDs?.has(X$.uuid?.slice(0,24)))return!1;'
123+
);
124+
});
125+
});

src/patches/clearScreen.ts

Lines changed: 45 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ export const writeClearScreen = (oldFile: string): string | null => {
2424
`${delimiter}globalThis.__tweakccForceRedraw=()=>${mapVar}.get(process.stdout)?.forceRedraw();` +
2525
redrawMatch[0].slice(1);
2626

27-
const file =
27+
let file =
2828
oldFile.slice(0, redrawMatch.index) +
2929
redrawReplacement +
3030
oldFile.slice(redrawMatch.index + redrawMatch[0].length);
@@ -37,18 +37,22 @@ export const writeClearScreen = (oldFile: string): string | null => {
3737
redrawMatch.index + redrawMatch[0].length
3838
);
3939

40+
const renderFilterResult = patchRenderFilter(file);
41+
if (!renderFilterResult) {
42+
debug('patch: clearScreen: failed to patch render filter g97');
43+
return null;
44+
}
45+
file = renderFilterResult;
46+
4047
const commandDef =
4148
',{type:"local",name:"clear-screen",' +
4249
'description:"Clear screen without resetting conversation context",' +
4350
'supportsNonInteractive:!1,' +
4451
'load:()=>Promise.resolve().then(()=>({call:(H,$)=>{' +
4552
'$.setMessages(m=>{' +
46-
'let k=null;' +
47-
'for(let i=m.length-1;i>=0;i--){' +
48-
'if(m[i]?.type==="assistant"&&m[i].message?.usage){k=m[i];break}' +
49-
'if(!k&&m[i]?.type==="assistant")k=m[i]}' +
50-
'return k?[{...k,message:{...k.message,content:[]}}]:[]});' +
51-
'process.stdout.write("\\x1b[3J");' +
53+
'globalThis.__tweakccHiddenUUIDs=new Set(m.map(x=>x.uuid?.slice(0,24)).filter(Boolean));' +
54+
'return[...m]});' +
55+
'process.stdout.write("\\x1b[2J\\x1b[H\\x1b[3J");' +
5256
'globalThis.__tweakccForceRedraw?.();' +
5357
'return{type:"skip"}}}))}';
5458

@@ -60,3 +64,37 @@ export const writeClearScreen = (oldFile: string): string | null => {
6064

6165
return result;
6266
};
67+
68+
export const patchRenderFilter = (oldFile: string): string | null => {
69+
const pattern =
70+
/(function [$\w]+\([$\w]+,[$\w]+\)\{)if\([$\w]+\.type!=="user"\)return!0;if\([$\w]+\.isMeta\)/;
71+
const match = oldFile.match(pattern);
72+
if (!match || match.index === undefined) {
73+
return null;
74+
}
75+
76+
const funcPrefix = match[1];
77+
const firstArg = match[0].match(/function ([$\w]+)\(([$\w]+),/)?.[2];
78+
if (!firstArg) {
79+
return null;
80+
}
81+
82+
const replacement =
83+
`${funcPrefix}if(globalThis.__tweakccHiddenUUIDs?.has(${firstArg}.uuid?.slice(0,24)))return!1;` +
84+
match[0].slice(funcPrefix.length);
85+
86+
const result =
87+
oldFile.slice(0, match.index) +
88+
replacement +
89+
oldFile.slice(match.index + match[0].length);
90+
91+
showDiff(
92+
oldFile,
93+
result,
94+
replacement,
95+
match.index,
96+
match.index + match[0].length
97+
);
98+
99+
return result;
100+
};

0 commit comments

Comments
 (0)