From ffa79d9cbb7f3ca9589601f1657803bae6847722 Mon Sep 17 00:00:00 2001 From: JATMN Date: Tue, 27 Oct 2026 15:36:29 -0700 Subject: [PATCH] fix: repair provider arguments, deep search, and chat sessions (#2459) * fix: address five AI quality findings * fix: preserve search and session boundaries during reset * fix: reset chat interaction and invalidate filtered search results * fix: keep deep search diagnostics free of conversation content * fix: preserve resume readiness and deep search result boundaries * fix: show retryable search failures without logging model output * fix: describe the background search model accurately * docs: clarify deep search excerpt limits and data scope * fix: keep search hydration diagnostics private --- README.md | 13 ++ docs/advanced-setup.md | 8 +- docs/images/resume-deep-search-error.png | Bin 0 -> 32232 bytes docs/images/resume-deep-search.png | Bin 0 -> 27291 bytes scripts/provider-launch.test.ts | 43 +++++ scripts/provider-launch.ts | 15 +- .../LogSelector.resumeBranches.test.ts | 101 ++++++++++++ src/components/LogSelector.tsx | 50 ++++-- src/utils/agenticSessionSearch.test.ts | 154 ++++++++++++++++++ src/utils/agenticSessionSearch.ts | 47 +++--- src/utils/promptShellExecution.test.ts | 60 ++++++- src/utils/promptShellExecution.ts | 6 +- src/utils/sessionStorage.ts | 18 +- vscode-extension/openclaude-vscode/README.md | 5 + .../src/chat/chatProvider.js | 38 ++++- .../src/chat/chatRenderer.js | 2 + .../src/chat/chatRenderer.test.js | 46 ++++++ .../openclaude-vscode/src/chat/resume.test.js | 147 ++++++++++++++++- .../src/chat/sessionManager.js | 5 + .../src/chat/sessionManager.test.js | 17 ++ .../openclaude-vscode/src/extension.js | 3 +- 21 files changed, 703 insertions(+), 75 deletions(-) create mode 100644 docs/images/resume-deep-search-error.png create mode 100644 docs/images/resume-deep-search.png create mode 100644 src/utils/agenticSessionSearch.test.ts create mode 100644 vscode-extension/openclaude-vscode/src/chat/chatRenderer.test.js diff --git a/README.md b/README.md index 56aa6de51..5f4a115e3 100644 --- a/README.md +++ b/README.md @@ -223,6 +223,19 @@ then `d` again to delete it; any other key in between cancels. Deleting removes the transcript and its sidecar files from disk and cannot be undone. The session you are currently in is not listed and cannot be deleted. +In `/resume`, type a search query, then press Down and Enter on **Search deeply +using a background model** to search conversation contents with the background +search model, which can differ from the active chat model. This explicit action +sends transcript excerpts and session metadata to the model provider, including +from loaded sessions that do not match the query locally. Ordinary filtering +stays local. Deep search considers up to 100 loaded sessions within the active +filters, so older history may not be included. Transcript extraction uses up to +100 messages (the first 50 and last 50 for longer sessions), then keeps only the +first 2,000 extracted characters. Matches outside that excerpt may be missed. +Results remain available as more history loads; changing the query or active +filters clears the previous search. If a search fails, the picker shows an error +and lets you retry the same query. + ### Background sessions Run long non-interactive prompts detached from the current terminal: diff --git a/docs/advanced-setup.md b/docs/advanced-setup.md index 99154a6d4..905ed9462 100644 --- a/docs/advanced-setup.md +++ b/docs/advanced-setup.md @@ -829,7 +829,13 @@ Notes: ## Provider Launch Profiles -Use profile launchers to avoid repeated environment setup: +Use profile launchers to avoid repeated environment setup. The profile defaults +to `auto` when omitted. Put launcher options (`--fast` and `--goal`) before CLI +arguments; the first CLI argument starts passthrough, so prompts and option values +are forwarded unchanged. A leading provider name such as `ollama` still selects +that profile. To use it as a prompt, pass it as a CLI argument, for example +`bun run scripts/provider-launch.ts -p ollama`, or pass a `--` separator through +to the launcher before the prompt (some Bun invocations consume that separator): ```bash # one-time profile bootstrap (prefer viable local Ollama, otherwise OpenAI) diff --git a/docs/images/resume-deep-search-error.png b/docs/images/resume-deep-search-error.png new file mode 100644 index 0000000000000000000000000000000000000000..f0893ab6e592bea1646ba9f6bef991f948462f38 GIT binary patch literal 32232 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 literal 0 HcmV?d00001 diff --git a/scripts/provider-launch.test.ts b/scripts/provider-launch.test.ts index 67dd12db7..32bf76836 100644 --- a/scripts/provider-launch.test.ts +++ b/scripts/provider-launch.test.ts @@ -1,3 +1,7 @@ +import { clearManagedProfileEnv } from '../src/utils/providerProfile.ts' +import { mkdtempSync, writeFileSync, rmSync } from 'node:fs' +import { tmpdir } from 'node:os' +import { join } from 'node:path' import { captureSync } from "./subprocess.ts" import { describe, expect, test } from 'bun:test' import { @@ -123,3 +127,42 @@ describe('development process launches', () => { expect(result.stderr).toBe('diagnostic') }, 5000) }) + +// Exercise main and profile resolution; simulate only the external doctor/build/CLI. +test('auto-profile launches preserve CLI arguments after the passthrough boundary', () => { + const directory = mkdtempSync(join(tmpdir(), 'provider-launch-')) + try { + const childEnv = { ...process.env } + clearManagedProfileEnv(childEnv) + writeFileSync(join(directory, '.openclaude-profile.json'), JSON.stringify({ + profile: 'openai', env: { OPENAI_API_KEY: 'test-launch-key' }, + })) + const preload = join(directory, 'capture.ts') + writeFileSync(preload, `process.argv = [process.execPath, 'provider-launch.ts', ...JSON.parse(process.env.LAUNCH_ARGS)] + Bun.spawn = (argv) => { + if (argv[0] === 'node') console.log('CLI_ARGS=' + JSON.stringify(argv.slice(2))) + return { exited: Promise.resolve(0), exitCode: 0, signalCode: null } + }`) + for (const [args, expected] of [ + [['--version'], ['--version']], + [['fix tests', '--fast', 'ollama'], ['fix tests', '--fast', 'ollama']], + [['--model', 'ollama', '--goal', 'coding'], ['--model', 'ollama', '--goal', 'coding']], + [['-p', 'fix tests'], ['-p', 'fix tests']], + [['--', 'openai', '--fast'], ['openai', '--fast']], + [['openai', '--fast', '--goal', 'coding', '-p', 'fix tests'], ['-p', 'fix tests']], + ]) { + const result = captureSync(process.execPath, ['--preload', preload, + join(import.meta.dir, 'provider-launch.ts'), ...args!], { + cwd: directory, + env: { ...childEnv, OPENCLAUDE_CONFIG_DIR: directory, LAUNCH_ARGS: JSON.stringify(args) }, + timeout: 5000, + }) + expect(result.error).toBeUndefined() + expect(result.status).toBe(0) + expect(result.stdout).toContain('Launching profile: openai') + expect(result.stdout).toContain('CLI_ARGS=' + JSON.stringify(expected)) + } + } finally { + rmSync(directory, { recursive: true, force: true }) + } +}, 35000) // Six children have five-second deadlines plus cleanup time. diff --git a/scripts/provider-launch.ts b/scripts/provider-launch.ts index ef0baa49e..876bfce20 100644 --- a/scripts/provider-launch.ts +++ b/scripts/provider-launch.ts @@ -59,17 +59,10 @@ function parseLaunchOptions(argv: string[]): LaunchOptions { continue } - if (arg.startsWith('--')) { - passthroughArgs.push(arg) - continue - } - - if (requestedProfile === 'auto') { - requestedProfile = null - break - } - - passthroughArgs.push(arg) + // The first CLI argument ends launcher-option parsing. In particular, + // option values and prompts may themselves look like provider names. + passthroughArgs.push(...argv.slice(arg === '--' ? i + 1 : i)) + break } return { diff --git a/src/components/LogSelector.resumeBranches.test.ts b/src/components/LogSelector.resumeBranches.test.ts index 9a9170095..2a6fb0543 100644 --- a/src/components/LogSelector.resumeBranches.test.ts +++ b/src/components/LogSelector.resumeBranches.test.ts @@ -423,3 +423,104 @@ test('rendered picker expands branch groups and selects child branch logs', asyn releaseSharedMutationLock() } }) + +test('deep search is offered only with a callback and runs from the keyboard', async () => { + await acquireSharedMutationLock('components/LogSelector.resumeBranches.test.tsx') + let renderer: Awaited> | null = null + const { stdout, stdin, getOutput } = createTestStreams() + const logs = [ + log(id(60), 'Original history', 10, { projectPath: getOriginalCwd(), tag: 'included' }), + log(id(62), 'Excluded history', 5, { projectPath: getOriginalCwd(), tag: 'other' }), + ] + const recovered = log(id(61), 'Recovered session', 20, { projectPath: getOriginalCwd() }) + const queries: string[] = [] + const selected: LogOption[] = [] + let selectionComplete: () => void = () => {} + const selection = new Promise(resolve => { selectionComplete = resolve }) + let finishSearch: (results: LogOption[]) => void = () => {} + let failSearch: (error: Error) => void = () => {} + let searchSignal: AbortSignal | undefined + let pageRequests = 0 + const onLoadMore = () => { pageRequests++ } + let deliverPage: (n: number) => Promise = async () => {} + let pageCommitted: () => void = () => {} + const onAgenticSearch = async (query: string, candidates: LogOption[], signal?: AbortSignal) => { + queries.push(query) + expect(candidates).toEqual([logs[0]]) + expect(signal?.aborted).toBe(false) + searchSignal = signal + return new Promise((resolve, reject) => { finishSearch = resolve; failSearch = reject }) + } + function Picker({ enabled }: { enabled: boolean }) { + const [loaded, setLoaded] = React.useState(logs) + React.useEffect(() => { pageCommitted() }, [loaded]) + deliverPage = n => new Promise(resolve => { + pageCommitted = resolve + setLoaded(previous => previous.concat(log(id(n), 'Paged history', n, { + projectPath: getOriginalCwd(), tag: n === 63 ? 'aaa-new-tag' : 'zzz-new-tag', + }))) + }) + return React.createElement(LogSelector, { + logs: loaded, maxHeight: 30, forceWidth: 100, + onSelect: item => { selected.push(item); selectionComplete() }, onLoadMore, + onAgenticSearch: enabled ? onAgenticSearch : undefined, + }) + } + const renderPicker = (enabled: boolean) => renderer!.render( + React.createElement(AppStateProvider, null, + React.createElement(KeybindingSetup, null, React.createElement(Picker, { enabled }))), + ) + try { + renderer = await createRoot({ + stdout: stdout as unknown as NodeJS.WriteStream, + stdin: stdin as unknown as NodeJS.ReadStream, + patchConsole: false, + }) + renderPicker(false) + await waitForFrame(getOutput, frame => frame.includes('Original history')) + stdin.write('\t') + await waitForFrame(getOutput, frame => frame.includes('Original history') && !frame.includes('Excluded history')) + stdin.write('Original') + const noCallback = await waitForFrame(getOutput, frame => frame.includes('⌕ Original')) + expect(noCallback).not.toContain('Search deeply using a background model') + renderPicker(true) + await waitForFrame(getOutput, frame => frame.includes('Search deeply using a background model')) + stdin.write('\r') + await waitForFrame(getOutput, frame => frame.includes('Type to search')) + stdin.write('\r') + await selection + expect(selected).toEqual([logs[0]]) + expect(queries).toEqual([]) + stdin.write('z') + await waitForFrame(getOutput, frame => frame.includes('⌕ z')) + stdin.write('\x1B[B') + await waitForFrame(getOutput, frame => frame.includes('❯ Search deeply using a background model')) + stdin.write('\r') + await waitForFrame(getOutput, frame => frame.includes('Searching with a background model')) + failSearch(new Error('private failure details')) + const failedFrame = await waitForFrame(getOutput, frame => frame.includes('Search failed. Please try again.')) + expect(failedFrame).not.toContain('private failure details') + expect(failedFrame).not.toContain('No matching sessions found.') + expect(failedFrame).toContain('Search deeply using a background model') + stdin.write('\r') + await waitForFrame(getOutput, frame => frame.includes('Searching with a background model')) + expect(pageRequests).toBeGreaterThan(0) + await deliverPage(63) + expect(searchSignal?.aborted).toBe(false) + finishSearch([recovered]) + await waitForFrame(getOutput, frame => frame.includes('Recovered session')) + await deliverPage(64) + const recoveredSelected = new Promise(resolve => { selectionComplete = resolve }) + stdin.write('\r') + await recoveredSelected + expect(selected.at(-1)).toEqual(recovered) + expect(queries).toEqual(['z', 'z']) + stdin.write('\t') + await waitForFrame(getOutput, frame => !frame.includes('Recovered session') && + frame.includes('Searches a limited set of loaded sessions.')) + } finally { + renderer?.unmount() + stdin.end() + releaseSharedMutationLock() + } +}) diff --git a/src/components/LogSelector.tsx b/src/components/LogSelector.tsx index 3e6e68f6e..3e15975ee 100644 --- a/src/components/LogSelector.tsx +++ b/src/components/LogSelector.tsx @@ -272,7 +272,7 @@ export function LogSelector(t0: LogSelectorProps) { t5 = $[4]; } const highlightColor = t5; - const isAgenticSearchEnabled = false; + const isAgenticSearchEnabled = typeof onAgenticSearch === "function"; const [currentBranch, setCurrentBranch] = React.useState(null); const [branchFilterEnabled, setBranchFilterEnabled] = React.useState(false); const [showAllWorktrees, setShowAllWorktrees] = React.useState(false); @@ -300,7 +300,7 @@ export function LogSelector(t0: LogSelectorProps) { const [viewMode, setViewMode] = React.useState("list"); const [previewLog, setPreviewLog] = React.useState(null); const prevFocusedIdRef = React.useRef(null); - const [selectedTagIndex, setSelectedTagIndex] = React.useState(0); + const [selectedTag, setSelectedTag] = React.useState("All"); let t8: AgenticSearchState; if ($[7] === Symbol.for("react.memo_cache_sentinel")) { t8 = { @@ -425,7 +425,7 @@ export function LogSelector(t0: LogSelectorProps) { t21 = $[23]; } const tagTabs = t21; - const effectiveTagIndex = tagTabs.length > 0 && selectedTagIndex < tagTabs.length ? selectedTagIndex : 0; + const effectiveTagIndex = Math.max(0, tagTabs.indexOf(selectedTag)); const selectedTab = tagTabs[effectiveTagIndex]; const tagFilter = selectedTab === "All" ? undefined : selectedTab; const tagTabsLines = hasTags ? 1 : 0; @@ -500,6 +500,23 @@ export function LogSelector(t0: LogSelectorProps) { filtered = t22; } const baseFilteredLogs = filtered; + const branchScope = branchFilterEnabled ? currentBranch : null; + const currentWorktreeOnly = hasMultipleWorktrees && !showAllWorktrees; + React.useEffect(() => { + agenticSearchAbortRef.current?.abort(); + setAgenticSearchState({ status: "idle" }); + }, [tagFilter, branchScope, currentWorktreeOnly, showAllProjects, searchQuery]); + const previousLogsRef = React.useRef(logs); + React.useEffect(() => { + const previousLogs = previousLogsRef.current; + previousLogsRef.current = logs; + // Progressive loading appends candidates without changing the search snapshot. + if (logs.length >= previousLogs.length && previousLogs.every((log, index) => logs[index] === log)) { + return; + } + agenticSearchAbortRef.current?.abort(); + setAgenticSearchState({ status: "idle" }); + }, [logs]); let t22; bb0: { if (!searchQuery) { @@ -834,7 +851,7 @@ export function LogSelector(t0: LogSelectorProps) { } const enterSearchMode = t34; let t35; - if ($[94] !== logs || $[95] !== onAgenticSearch || $[96] !== searchQuery) { + if ($[94] !== baseFilteredLogs || $[95] !== onAgenticSearch || $[96] !== searchQuery) { t35 = async () => { const runAgenticSearch = onAgenticSearch; if (!searchQuery.trim() || !runAgenticSearch || !isAgenticSearchEnabled) { @@ -851,7 +868,7 @@ export function LogSelector(t0: LogSelectorProps) { }); ; try { - const results_0 = await runAgenticSearch(searchQuery, logs, abortController.signal); + const results_0 = await runAgenticSearch(searchQuery, baseFilteredLogs, abortController.signal); if (abortController.signal.aborted) { return; } @@ -873,12 +890,13 @@ export function LogSelector(t0: LogSelectorProps) { status: "error", message }); + setIsAgenticSearchOptionFocused(true); logEvent("tengu_agentic_search_error", { query_length: searchQuery.length }); } }; - $[94] = logs; + $[94] = baseFilteredLogs; $[95] = onAgenticSearch; $[96] = searchQuery; $[97] = t35; @@ -890,7 +908,7 @@ export function LogSelector(t0: LogSelectorProps) { if ($[98] !== agenticResultsQuery || $[99] !== agenticSearchState.status || $[100] !== searchQuery) { t36 = () => { if (agenticSearchState.status !== "idle" && agenticSearchState.status !== "searching") { - if (agenticSearchState.status === "results" && agenticSearchState.query !== searchQuery || agenticSearchState.status === "error") { + if (agenticSearchState.status === "results" && agenticSearchState.query !== searchQuery) { setAgenticSearchState({ status: "idle" }); @@ -1097,8 +1115,8 @@ export function LogSelector(t0: LogSelectorProps) { if (input.toLowerCase() === "n" && key.ctrl) { exitSearchMode(); } else { - if (key.return || key.downArrow) { - if (searchQuery.trim() && onAgenticSearch && false && agenticSearchState.status !== "results") { + if (key.downArrow) { + if (searchQuery.trim() && onAgenticSearch && isAgenticSearchEnabled && agenticSearchState.status !== "results") { setIsAgenticSearchOptionFocused(true); } } @@ -1124,15 +1142,15 @@ export function LogSelector(t0: LogSelectorProps) { } if (hasTags && key.tab) { const offset = key.shift ? -1 : 1; - setSelectedTagIndex(prev => { - const current = prev < tagTabs.length ? prev : 0; + setSelectedTag(prev => { + const current = Math.max(0, tagTabs.indexOf(prev)); const newIndex = (current + tagTabs.length + offset) % tagTabs.length; const newTab = tagTabs[newIndex]; logEvent("tengu_session_tag_filter_changed", { is_all: newTab === "All", tag_count: uniqueTags.length }); - return newIndex; + return newTab!; }); return; } @@ -1353,7 +1371,7 @@ export function LogSelector(t0: LogSelectorProps) { } let t66; if ($[187] !== agenticResults || $[188] !== agenticSearchState.status) { - t66 = agenticSearchState.status === "results" && agenticResults.length > 0 && Claude found these results:; + t66 = agenticSearchState.status === "results" && agenticResults.length > 0 && Matching sessions:; $[187] = agenticResults; $[188] = agenticSearchState.status; $[189] = t66; @@ -1372,7 +1390,7 @@ export function LogSelector(t0: LogSelectorProps) { } let t68; if ($[194] !== agenticSearchState.status || $[195] !== filteredLogs) { - t68 = agenticSearchState.status === "error" && filteredLogs.length === 0 && No matching sessions found.; + t68 = agenticSearchState.status === "error" && Search failed. Please try again.; $[194] = agenticSearchState.status; $[195] = filteredLogs; $[196] = t68; @@ -1381,7 +1399,7 @@ export function LogSelector(t0: LogSelectorProps) { } let t69; if ($[197] !== agenticSearchState.status || $[198] !== isAgenticSearchOptionFocused || $[199] !== onAgenticSearch || $[200] !== searchQuery) { - t69 = Boolean(searchQuery.trim()) && onAgenticSearch && false && agenticSearchState.status !== "searching" && agenticSearchState.status !== "results" && agenticSearchState.status !== "error" && {isAgenticSearchOptionFocused ? figures.pointer : " "}Search deeply using Claude →; + t69 = Boolean(searchQuery.trim()) && onAgenticSearch && isAgenticSearchEnabled && agenticSearchState.status !== "searching" && agenticSearchState.status !== "results" && {isAgenticSearchOptionFocused ? figures.pointer : " "}Search deeply using a background model →Searches a limited set of loaded sessions.; $[197] = agenticSearchState.status; $[198] = isAgenticSearchOptionFocused; $[199] = onAgenticSearch; @@ -1447,7 +1465,7 @@ export function LogSelector(t0: LogSelectorProps) { } let t71; if ($[222] !== agenticSearchState.status || $[223] !== currentBranch || $[224] !== exitState.keyName || $[225] !== exitState.pending || $[226] !== getExpandCollapseHint || $[227] !== hasMultipleWorktrees || $[228] !== isAgenticSearchOptionFocused || $[229] !== isSearching || $[230] !== onToggleAllProjects || $[231] !== showAllProjects || $[232] !== showAllWorktrees || $[233] !== viewMode) { - t71 = {exitState.pending ? Press {exitState.keyName} again to exit : viewMode === "rename" ? : agenticSearchState.status === "searching" ? Searching with Claude… : isAgenticSearchOptionFocused ? : viewMode === "search" ? {isSearching && false ? "Searching\u2026" : "Type to Search"} : {onToggleAllProjects && }{currentBranch && }{hasMultipleWorktrees && }Type to search{getExpandCollapseHint() && {getExpandCollapseHint()}}}; + t71 = {exitState.pending ? Press {exitState.keyName} again to exit : viewMode === "rename" ? : agenticSearchState.status === "searching" ? Searching with a background model… : isAgenticSearchOptionFocused ? : viewMode === "search" ? {isSearching && false ? "Searching\u2026" : "Type to Search"} : {onToggleAllProjects && }{currentBranch && }{hasMultipleWorktrees && }Type to search{getExpandCollapseHint() && {getExpandCollapseHint()}}}; $[222] = agenticSearchState.status; $[223] = currentBranch; $[224] = exitState.keyName; diff --git a/src/utils/agenticSessionSearch.test.ts b/src/utils/agenticSessionSearch.test.ts new file mode 100644 index 000000000..e1002f7f3 --- /dev/null +++ b/src/utils/agenticSessionSearch.test.ts @@ -0,0 +1,154 @@ +import { expect, test } from 'bun:test' +import { mkdtempSync, rmSync } from 'node:fs' +import { tmpdir } from 'node:os' +import { join } from 'node:path' +import { fileURLToPath } from 'node:url' + +test.each(['indices', 'diagnostics'] as const)('deep search validates %s at the model boundary', mode => { + // Isolate module mocks from the shared test process. + const child = Bun.spawnSync([process.execPath, '--feature=SLOW_OPERATION_LOGGING', '--eval', ` + import { mock } from 'bun:test' + import assert from 'node:assert/strict' + const debug = [], errors = [], slow = [] + process.env.CLAUDE_CODE_SLOW_OPERATION_THRESHOLD_MS = '0' + let clock = 0 + Object.defineProperty(performance, 'now', { value: () => clock += 10 }) + mock.module('../bootstrap/state.js', () => ({ addSlowOperation: text => slow.push(text) })) + let responseText = 'private-response {"relevant_indices":[0]}' + let providerFails = false + let lite = false + let prompt = '' + mock.module('./debug.js', () => ({ logForDebugging: text => debug.push(text) })) + mock.module('./log.js', () => ({ getLogDisplayTitle: log => log.firstPrompt, logError: error => errors.push(String(error)) })) + mock.module('./model/model.js', () => ({ getSmallFastModel: () => 'configured-test-model' })) + mock.module('./sessionStorage.js', () => ({ isLiteLog: () => lite, loadFullLog() { throw new Error('private-transcript') } })) + mock.module('./sideQuery.js', () => ({ sideQuery: async options => { + prompt = options.messages[0].content + if (providerFails) throw new Error('private-response') + return { content: [{ type: 'text', text: responseText }] } + } })) + const { jsonParse } = await import('./slowOperations.ts') + jsonParse('{}') + assert.ok(slow.length > 0 && debug.length > 0, 'slow-operation logging is active') + debug.length = slow.length = 0 + const { agenticSessionSearch } = await import('./agenticSessionSearch.ts') + const log = { + firstPrompt: 'private-first-message', + messages: [{ type: 'user', message: { content: 'private-transcript' } }], + } + const result = await agenticSessionSearch('private-query', [log]) + assert.equal(result[0], log) + for (const text of ['private-query', 'private-first-message', 'private-transcript']) { + assert.ok(prompt.includes(text), 'search input must retain ' + text) + } + if (${JSON.stringify(mode)} === 'indices') { + for (const field of [[], [0], [0, 0.5, null, false, true, '0', '0.0', [], [0], {}, -1, 1], null, 0, '0', {}, undefined]) { + responseText = JSON.stringify({ relevant_indices: field }) + const result = await agenticSessionSearch('private-query', [log]) + assert.deepEqual(result, Array.isArray(field) && field.includes(0) ? [log] : []) + } + } else { + responseText = '{"relevant_indices":[private-transcript]}' + await assert.rejects(agenticSessionSearch('private-query', [log]), /^Error: Agentic session search failed$/) + providerFails = true + await assert.rejects(agenticSessionSearch('private-query', [log]), /^Error: Agentic session search failed$/) + providerFails = false + lite = true + responseText = '{"relevant_indices":[0]}' + assert.deepEqual(await agenticSessionSearch('private-query', [log]), [log]) + assert.ok(errors.length > 0, 'failure diagnostics remain available') + } + assert.ok(debug.length > 0, 'diagnostics remain available') + for (const text of ['private-query', 'private-first-message', 'private-transcript', 'private-response']) { + assert.ok(![...debug, ...errors, ...slow].join('\\n').includes(text), 'diagnostics exposed ' + text) + } + `], { cwd: fileURLToPath(new URL('./', import.meta.url)), timeout: 10_000 }) + expect(child.stderr.toString()).toBe('') + expect(child.exitCode).toBe(0) +}, 15_000) + +test.each([false, true])('deep search keeps real hydration diagnostics private (instrumentation: %s)', loggingEnabled => { + const directory = mkdtempSync(join(tmpdir(), 'session-search-')) + try { + const child = Bun.spawnSync([process.execPath, + ...(loggingEnabled ? ['--feature=SLOW_OPERATION_LOGGING'] : []), '--eval', String.raw` + import assert from 'node:assert/strict' + import { writeFileSync } from 'node:fs' + import { join } from 'node:path' + import { mock, spyOn } from 'bun:test' + process.env.CLAUDE_CODE_SLOW_OPERATION_THRESHOLD_MS = '0' + delete process.env.CLAUDE_CODE_DISABLE_PRECOMPACT_SKIP + let clock = 0 + Object.defineProperty(performance, 'now', { value: () => clock += 10 }) + const debug = await import('./debug.ts') + const state = await import('../bootstrap/state.ts') + const diagnostics = [] + spyOn(debug, 'logForDebugging').mockImplementation(text => diagnostics.push(String(text))) + spyOn(state, 'addSlowOperation').mockImplementation(text => diagnostics.push(String(text))) + const model = await import('./model/model.ts') + mock.module('./model/model.js', () => ({ ...model, getSmallFastModel: () => 'test-model' })) + let prompt = '' + mock.module('./sideQuery.js', () => ({ sideQuery: async options => { + prompt = options.messages[0].content + return { content: [{ type: 'text', text: '{"relevant_indices":[0]}' }] } + } })) + const { jsonParse } = await import('./slowOperations.ts') + jsonParse('{}') + const enabled = process.env.SEARCH_TEST_INSTRUMENTATION === 'true' + assert.equal(diagnostics.length > 0, enabled, 'instrumentation control') + const { agenticSessionSearch } = await import('./agenticSessionSearch.ts') + // Keep the actual storage reader and both JSONL parsing passes. + const { loadFullLog } = await import('./sessionStorage.ts') + const { SKIP_PRECOMPACT_THRESHOLD } = await import('./sessionStoragePortable.ts') + const sessionId = '38f67e50-946e-4d74-b0b6-c19759b99623' + const oldId = '00000000-0000-4000-8000-000000000001' + const boundaryId = '00000000-0000-4000-8000-000000000002' + const currentId = '00000000-0000-4000-8000-000000000003' + const user = (uuid, parentUuid, content) => ({ + type: 'user', message: { role: 'user', content }, uuid, parentUuid, + sessionId, timestamp: '2026-10-27T00:00:00.000Z', isSidechain: false, + }) + for (const compacted of [false, true]) { + const file = join(process.env.OPENCLAUDE_CONFIG_DIR, compacted ? 'large.jsonl' : 'small.jsonl') + const entries = [ + { type: 'custom-title', customTitle: 'private-title', sessionId }, + { type: 'tag', tag: 'private-tag', sessionId }, + ] + if (compacted) { + entries.push(user(oldId, null, 'discarded-history-marker' + 'x'.repeat(SKIP_PRECOMPACT_THRESHOLD))) + entries.push({ type: 'system', subtype: 'compact_boundary', uuid: boundaryId, + parentUuid: null, sessionId, timestamp: '2026-10-27T00:00:00.000Z', + content: 'Conversation compacted', compactMetadata: { trigger: 'manual', preTokens: 123 } }) + } + entries.push(user(currentId, compacted ? boundaryId : null, 'private-transcript')) + writeFileSync(file, entries.map(entry => JSON.stringify(entry)).join('\n') + '\n') + const lite = { fullPath: file, messages: [], sessionId, firstPrompt: 'public', + modified: new Date(), created: new Date(), date: '', value: 0, + messageCount: 1, isSidechain: false } + diagnostics.length = 0 + const result = await agenticSessionSearch('private-query', [lite]) + assert.equal(result.length, 1) + assert.equal(result[0].customTitle, 'private-title', 'pre-boundary title restored') + assert.equal(result[0].tag, 'private-tag', 'pre-boundary tag restored') + assert.ok(prompt.includes('private-transcript'), 'model retains transcript content') + if (compacted) assert.ok(!prompt.includes('discarded-history-marker'), 'compaction preserved') + for (const text of ['private-title', 'private-tag', 'private-transcript', 'private-query']) { + assert.ok(!diagnostics.join('\n').includes(text), 'hydration diagnostics exposed ' + text) + } + diagnostics.length = 0 + // A quiet search must not suppress diagnostics for other loader callers. + assert.deepEqual(result, [await loadFullLog(lite)]) + assert.equal(diagnostics.some(text => text.includes('private-title')), enabled, + 'ordinary loading retains its existing diagnostic behavior') + } + `], { + cwd: fileURLToPath(new URL('./', import.meta.url)), timeout: 30_000, + env: { ...process.env, OPENCLAUDE_CONFIG_DIR: directory, + SEARCH_TEST_INSTRUMENTATION: String(loggingEnabled) }, + }) + expect(child.stderr.toString()).toBe('') + expect(child.exitCode).toBe(0) + } finally { + rmSync(directory, { recursive: true, force: true }) + } +}, 35_000) diff --git a/src/utils/agenticSessionSearch.ts b/src/utils/agenticSessionSearch.ts index b54dab914..a7b409443 100644 --- a/src/utils/agenticSessionSearch.ts +++ b/src/utils/agenticSessionSearch.ts @@ -5,7 +5,6 @@ import { getLogDisplayTitle, logError } from './log.js' import { getSmallFastModel } from './model/model.js' import { isLiteLog, loadFullLog } from './sessionStorage.js' import { sideQuery } from './sideQuery.js' -import { jsonParse } from './slowOperations.js' // Limits for transcript extraction const MAX_TRANSCRIPT_CHARS = 2000 // Max chars of transcript per session @@ -47,10 +46,6 @@ Return sessions ordered by relevance (most relevant first). If truly no sessions Respond with ONLY the JSON object, no markdown formatting: {"relevant_indices": [2, 5, 0]}` -type AgenticSearchResult = { - relevant_indices: number[] -} - /** * Extracts searchable text content from a message. */ @@ -140,7 +135,7 @@ function logContainsQuery(log: LogOption, queryLower: string): boolean { } /** - * Performs an agentic search using Claude to find relevant sessions + * Performs an agentic search using the background search model to find relevant sessions * based on semantic understanding of the query. */ export async function agenticSessionSearch( @@ -176,7 +171,7 @@ export async function agenticSessionSearch( // Debug: log what data we have logForDebugging( - `Agentic search: ${logsToSearch.length}/${logs.length} logs, query="${query}", ` + + `Agentic search: ${logsToSearch.length}/${logs.length} logs, ` + `matching: ${matchingLogs.length}, with messages: ${count(logsToSearch, l => l.messages?.length > 0)}`, ) @@ -184,9 +179,9 @@ export async function agenticSessionSearch( const logsWithTranscriptsPromises = logsToSearch.map(async log => { if (isLiteLog(log)) { try { - return await loadFullLog(log) - } catch (error) { - logError(error as Error) + return await loadFullLog(log, { logParseContent: false }) + } catch { + logError(new Error('Agentic session transcript loading failed')) // If loading fails, use the lite log (no transcript) return log } @@ -252,11 +247,6 @@ Search query: "${query}" Find the sessions that are most relevant to this query.` - // Debug: log first part of the session list - logForDebugging( - `Agentic search prompt (first 500 chars): ${userMessage.slice(0, 500)}...`, - ) - try { const model = getSmallFastModel() logForDebugging(`Agentic search using model: ${model}`) @@ -276,9 +266,6 @@ Find the sessions that are most relevant to this query.` return [] } - // Debug: log the response - logForDebugging(`Agentic search response: ${textContent.text}`) - // Parse the JSON response const jsonMatch = textContent.text.match(/\{[\s\S]*\}/) if (!jsonMatch) { @@ -286,22 +273,30 @@ Find the sessions that are most relevant to this query.` return [] } - const result: AgenticSearchResult = jsonParse(jsonMatch[0]) - const relevantIndices = result.relevant_indices || [] + // The instrumented parser can log input; model output may contain private text. + const result: unknown = JSON.parse(jsonMatch[0]) + const relevantIndices = result && typeof result === 'object' && + 'relevant_indices' in result && Array.isArray(result.relevant_indices) + ? result.relevant_indices : [] // Map indices back to logs (indices are relative to logsWithTranscripts) const relevantLogs = relevantIndices - .filter(index => index >= 0 && index < logsWithTranscripts.length) - .map(index => logsWithTranscripts[index]!) + .filter((index): index is number => typeof index === 'number' && + Number.isInteger(index) && index >= 0 && index < logsWithTranscripts.length) + .flatMap(index => { + const log = logsWithTranscripts[index] + return log ? [log] : [] + }) logForDebugging( `Agentic search found ${relevantLogs.length} relevant sessions`, ) return relevantLogs - } catch (error) { - logError(error as Error) - logForDebugging(`Agentic search error: ${error}`) - return [] + } catch { + // Parser/provider errors may contain excerpts of private conversation content. + logError(new Error('Agentic session search failed')) + logForDebugging('Agentic session search failed') + throw new Error('Agentic session search failed') } } diff --git a/src/utils/promptShellExecution.test.ts b/src/utils/promptShellExecution.test.ts index b133d4ab8..afe981255 100644 --- a/src/utils/promptShellExecution.test.ts +++ b/src/utils/promptShellExecution.test.ts @@ -89,7 +89,7 @@ test('executeShellCommandsInPrompt normalizes null shell output', async () => { }) }) -test('executeShellCommandsInPrompt applies per-prefix line limits', async () => { +test.each([0, 3])('executeShellCommandsInPrompt applies a %s-line cap', async limit => { BashTool.call = (async () => ({ data: { stdout: 'line1\nline2\nline3\nline4\nline5\n', @@ -137,10 +137,10 @@ test('executeShellCommandsInPrompt applies per-prefix line limits', async () => } as never, 'bughunter', undefined, - { lineLimits: { 'git diff HEAD -- .': 3 } }, + { lineLimits: { 'git diff HEAD -- .': limit } }, ) - expect(result).toBe('line1\nline2\nline3') + expect(result).toBe(limit === 0 ? '(Bash completed with no output)' : 'line1\nline2\nline3') }) test('executeShellCommandsInPrompt does not truncate below the cap', async () => { @@ -318,3 +318,57 @@ test('granularFallback still surfaces permission denials as MalformedCommandErro ), ).rejects.toBeInstanceOf(MalformedCommandError) }) + +test.each([-1, NaN, Infinity, -Infinity, 1.5])('ignores invalid line limit %s and uses the longest valid prefix', async invalid => { + BashTool.call = (async () => ({ + data: { + stdout: 'line1\nline2\nline3\nline4\nline5\n', + stderr: '', + interrupted: false, + }, + })) as unknown as typeof BashTool.call + + BashTool.mapToolResultToToolResultBlockParam = (result, toolUseID) => + originalMapToolResultToToolResultBlockParam( + result as never, + toolUseID, + ) + + const result = await executeShellCommandsInPrompt( + '```!\ngit diff HEAD -- .\n```', + { + abortController: new AbortController(), + options: { + commands: [], + debug: false, + mainLoopModel: 'sonnet', + tools: new Map(), + verbose: false, + thinkingConfig: { type: 'disabled' }, + mcpClients: [], + mcpResources: {}, + isNonInteractiveSession: false, + agentDefinitions: { + systemDefinitions: [], + projectDefinitions: [], + userDefinitions: [], + }, + }, + readFileState: new Map(), + getAppState() { + return { + toolPermissionContext: { + ...getEmptyToolPermissionContext(), + alwaysAllowRules: { command: ['Bash(*)'] }, + }, + } + }, + setAppState() {}, + } as never, + 'bughunter', + undefined, + { lineLimits: { 'git diff': 3, 'git diff HEAD -- .': invalid } }, + ) + + expect(result).toBe('line1\nline2\nline3') +}) diff --git a/src/utils/promptShellExecution.ts b/src/utils/promptShellExecution.ts index 0bb7d4603..514aade11 100644 --- a/src/utils/promptShellExecution.ts +++ b/src/utils/promptShellExecution.ts @@ -72,7 +72,8 @@ const INLINE_PATTERN = /(?<=^|\s)!`([^`]+)`/gm * @param options.lineLimits - Map of command-prefix → max output lines. * When a snippet's executed command (trimmed) starts with one of the * prefixes, the output is sliced to that many lines before being - * substituted. Use to bound diffs or other potentially-large outputs + * substituted. Invalid limits (nonfinite, fractional, or negative) are ignored. + * Use to bound diffs or other potentially-large outputs * without widening the Bash allowlist (avoids `| head -N` in commands, * which the permission parser treats as a compound and may reject). */ @@ -256,6 +257,9 @@ function applyLineLimit( let bestPrefix = '' let bestLimit = Infinity for (const [prefix, limit] of Object.entries(limits)) { + if (!Number.isFinite(limit) || !Number.isInteger(limit) || limit < 0) { + continue + } if (trimmed === prefix || trimmed.startsWith(prefix + ' ')) { if (prefix.length > bestPrefix.length) { bestPrefix = prefix diff --git a/src/utils/sessionStorage.ts b/src/utils/sessionStorage.ts index debd900ad..8e7dc189b 100644 --- a/src/utils/sessionStorage.ts +++ b/src/utils/sessionStorage.ts @@ -3093,7 +3093,9 @@ export function stripPersistedToolUseResultsFromJSONLBuffer(buf: Buffer): Buffer function forEachParsedJSONLBufferEntry( buf: Buffer, visit: (entry: T) => void, + logParseContent = true, ): void { + const parse = logParseContent ? jsonParse : JSON.parse const bufLen = buf.length let start = 0 @@ -3110,7 +3112,7 @@ function forEachParsedJSONLBufferEntry( if (!line) continue try { - visit(jsonParse(line) as T) + visit(parse(line) as T) } catch { // Skip malformed lines } @@ -4126,8 +4128,12 @@ export function isLiteLog(log: LogOption): boolean { * Returns a new LogOption with populated messages array. * Explicit paths select the latest non-sidechain leaf and retain its session ID. * If the log is already full or loading fails, returns the original/empty log. + * Set logParseContent to false to keep transcript bytes out of slow diagnostics. */ -export async function loadFullLog(source: LogOption | string): Promise { +export async function loadFullLog( + source: LogOption | string, + opts?: { logParseContent?: boolean }, +): Promise { // Explicit file resumes use the same metadata restoration as the picker. // The leaf supplies their identity; filenames may have been renamed. const fromPath = typeof source === 'string' @@ -4177,7 +4183,7 @@ export async function loadFullLog(source: LogOption | string): Promise summaries: Map @@ -4828,7 +4834,7 @@ export async function loadTranscriptFile( } else if (entry.type === 'session-branch' && entry.sessionId) { sessionBranches.set(entry.sessionId, entry) } - }) + }, opts?.logParseContent) } // Bridge map for legacy progress entries: progress_uuid → progress_parent_uuid. @@ -4916,7 +4922,7 @@ export async function loadTranscriptFile( } else if (entry.type === 'marble-origami-snapshot') { contextCollapseSnapshot = entry } - }) + }, opts?.logParseContent) } catch { // File doesn't exist or can't be read } diff --git a/vscode-extension/openclaude-vscode/README.md b/vscode-extension/openclaude-vscode/README.md index 434c568ee..e205573e6 100644 --- a/vscode-extension/openclaude-vscode/README.md +++ b/vscode-extension/openclaude-vscode/README.md @@ -39,6 +39,11 @@ from `~/.openclaude/projects/`, including long workspace paths. When a session l CLI so both read the same history. Claude Code's `CLAUDE_CONFIG_DIR` and `~/.claude/` are separate and are not used for OpenClaude session discovery. +The chat's **New Chat** button and **OpenClaude: New Chat** command clear the +active session and displayed history. The next message starts a fresh conversation; +saved transcripts remain available through Resume Session. Switching to a saved +conversation during a response keeps Send disabled until that session starts. + ## Commands - `OpenClaude: Open Control Center` diff --git a/vscode-extension/openclaude-vscode/src/chat/chatProvider.js b/vscode-extension/openclaude-vscode/src/chat/chatProvider.js index 7003783f6..b2025b829 100644 --- a/vscode-extension/openclaude-vscode/src/chat/chatProvider.js +++ b/vscode-extension/openclaude-vscode/src/chat/chatProvider.js @@ -41,6 +41,7 @@ class ChatController { this._sessionManager = sessionManager; this._process = null; this._startGeneration = 0; + this._resetGeneration = 0; this._pendingStart = null; this._webviews = new Set(); this._accumulatedText = ''; @@ -167,6 +168,14 @@ class ChatController { this._pendingPermissions.clear(); } + resetForNewSession() { + this._resetGeneration++; + this.stopSession(); + this._currentSessionId = null; + this._messages = []; + this.broadcast({ type: 'session_cleared', resetStreaming: true }); + } + async sendMessage(text) { // Reuse a pending restart so simultaneous sends retain both messages. // A stop or a new selection cancels sends waiting on the old startup. @@ -181,11 +190,14 @@ class ChatController { async _doSend(text) { if (!this._process) return; + const process = this._process; + const generation = this._startGeneration; // On first message after process start, wait for CLI to be ready. // On subsequent messages, the process is already running and accepting input. if (this._readyPromise) { const grace = new Promise(resolve => setTimeout(resolve, 8000)); await Promise.race([this._readyPromise, grace]); + if (generation !== this._startGeneration || process !== this._process) return; this._readyPromise = null; } this._accumulatedText = ''; @@ -527,14 +539,17 @@ class OpenClaudeChatViewProvider { this._chatController.abort(); break; case 'new_session': - this._chatController.stopSession(); - webview.postMessage({ type: 'session_cleared' }); + this._chatController.resetForNewSession(); break; case 'resume_session': this._chatController.stopSession(); webview.postMessage({ type: 'session_cleared' }); - await this._loadAndDisplaySession(webview, msg.sessionId); - await this._chatController.startSession({ sessionId: msg.sessionId }); + { + const generation = this._chatController._resetGeneration; + await this._loadAndDisplaySession(webview, msg.sessionId); + if (generation !== this._chatController._resetGeneration) break; + await this._chatController.startSession({ sessionId: msg.sessionId }); + } break; case 'permission_response': this._chatController.sendPermissionResponse(msg.requestId, msg.action, msg.toolUseId); @@ -577,7 +592,9 @@ class OpenClaudeChatViewProvider { async _loadAndDisplaySession(webview, sessionId) { if (!this._chatController.sessionManager) return; try { + const generation = this._chatController._resetGeneration; const messages = await this._chatController.sessionManager.loadSession(sessionId); + if (generation !== this._chatController._resetGeneration) return; if (messages && messages.length > 0) { this._chatController._messages = messages; webview.postMessage({ type: 'restore_messages', messages }); @@ -636,14 +653,17 @@ class OpenClaudeChatPanelManager { this._chatController.abort(); break; case 'new_session': - this._chatController.stopSession(); - webview.postMessage({ type: 'session_cleared' }); + this._chatController.resetForNewSession(); break; case 'resume_session': this._chatController.stopSession(); webview.postMessage({ type: 'session_cleared' }); - await this._loadAndDisplaySession(webview, msg.sessionId); - await this._chatController.startSession({ sessionId: msg.sessionId }); + { + const generation = this._chatController._resetGeneration; + await this._loadAndDisplaySession(webview, msg.sessionId); + if (generation !== this._chatController._resetGeneration) break; + await this._chatController.startSession({ sessionId: msg.sessionId }); + } break; case 'permission_response': this._chatController.sendPermissionResponse(msg.requestId, msg.action, msg.toolUseId); @@ -686,7 +706,9 @@ class OpenClaudeChatPanelManager { async _loadAndDisplaySession(webview, sessionId) { if (!this._chatController.sessionManager) return; try { + const generation = this._chatController._resetGeneration; const messages = await this._chatController.sessionManager.loadSession(sessionId); + if (generation !== this._chatController._resetGeneration) return; if (messages && messages.length > 0) { this._chatController._messages = messages; webview.postMessage({ type: 'restore_messages', messages }); diff --git a/vscode-extension/openclaude-vscode/src/chat/chatRenderer.js b/vscode-extension/openclaude-vscode/src/chat/chatRenderer.js index df9c023a0..b270e1c82 100644 --- a/vscode-extension/openclaude-vscode/src/chat/chatRenderer.js +++ b/vscode-extension/openclaude-vscode/src/chat/chatRenderer.js @@ -1263,6 +1263,8 @@ function renderChatHtml({ nonce, platform }) { break; case 'session_cleared': + // Resume also clears the view before the selected session is ready. + if (msg.resetStreaming) setStreaming(false); messagesEl.innerHTML = ''; if (welcomeEl) { messagesEl.appendChild(welcomeEl); diff --git a/vscode-extension/openclaude-vscode/src/chat/chatRenderer.test.js b/vscode-extension/openclaude-vscode/src/chat/chatRenderer.test.js new file mode 100644 index 000000000..b5f87204b --- /dev/null +++ b/vscode-extension/openclaude-vscode/src/chat/chatRenderer.test.js @@ -0,0 +1,46 @@ +const test = require('node:test'); +const assert = require('node:assert/strict'); +const vm = require('node:vm'); +const { renderChatHtml } = require('./chatRenderer'); + +test('New Chat during streaming enables sending the next prompt', () => { + // Execute the production webview script against the DOM operations used here. + const element = () => { + const classes = new Set(); + return { + style: {}, listeners: {}, value: '', scrollHeight: 20, + classList: { toggle(name, enabled) { if (enabled) classes.add(name); else classes.delete(name); } }, + addEventListener(name, listener) { this.listeners[name] = listener; }, + appendChild() {}, + }; + }; + const elements = new Map(); + const getElement = id => { + if (!elements.has(id)) elements.set(id, element()); + return elements.get(id); + }; + const sent = []; + let receive; + const html = renderChatHtml({ nonce: 'test', platform: 'linux' }); + const script = html.match(/