features: /changes, /search, /fork, web_search, prompt memoization, per-turn spend cap, workspace refresh
ci / check (macos-latest) (push) Canceled after 0s
ci / check (ubuntu-latest) (push) Canceled after 0s
ci / check (windows-latest) (push) Canceled after 0s

- /changes diffs the last turn's file snapshot (added/modified/deleted), reusing undo infra via SnapshotStack.peek()
- system prompt memoized behind version counters (notebook/memory/skills/plugins/tools/workspace); hit-rate in /cost, foundation for provider caching
- web_search: DuckDuckGo Lite, keyless, 5 results, SSRF-filtered, in the net set with ask permission
- /search <query>: full-text grep over saved sessions incl. tool-input JSON
- workspace file list re-walks at a turn boundary after writes
- maxSpendPerTurn: per-turn cap stops a runaway step with a notice
- /fork: branch the session at the last turn boundary, original untouched

821 tests pass, typecheck clean, build green
This commit is contained in:
asepharyana committed 2026-09-09 18:06:31 +07:00
1 parent 0bdf642672
commit e709737df1
24 files changed
+720 -41

No files matched your search

+29
View File
@@ -72,6 +72,35 @@ export async function resolveId(prefix: string): Promise<string | undefined> {
return matches.length === 1 ? matches[0]!.id : undefined;
}
/**
* Full-text search over saved sessions, on the transcript text only.
*
* Plain substring match — the same deliberate choice as the workspace walker:
* no index to build or ship, and a coding session fits in a grep. Content parts
* that are objects (tool inputs/results) are stringified for the match, so a
* search for a path or command finds it even inside a tool call.
*/
export async function searchSessions(query: string, limit = 10): Promise<SessionRecord[]> {
const q = query.trim().toLowerCase();
if (!q) return [];
const found: SessionRecord[] = [];
for (const rec of await list(200)) {
const hay = [rec.title, ...rec.messages.map((m) => {
if (typeof m.content === 'string') return m.content;
try {
return JSON.stringify(m.content);
} catch {
return '';
}
})].join('\n').toLowerCase();
if (hay.includes(q)) {
found.push(rec);
if (found.length >= limit) break;
}
}
return found;
}
export function titleOf(messages: ModelMessage[]): string {
const first = messages.find((m) => m.role === 'user');
const text = typeof first?.content === 'string' ? first.content : '';