features: /changes, /search, /fork, web_search, prompt memoization, per-turn spend cap, workspace refresh
- /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:
1 parent
0bdf642672
commit
e709737df1
24 files changed
+720
-41
No files matched your search
+35
-1
@@ -33,7 +33,7 @@ import {
|
||||
type SubagentView,
|
||||
} from './Panels';
|
||||
import { CommandMenu, InstallConfirm, Picker } from './Pickers';
|
||||
import { contextPanel, costPanel, todosPanel, toolsPanel } from './panel-bodies';
|
||||
import { contextPanel, costPanel, todosPanel, toolsPanel, changesPanel } from './panel-bodies';
|
||||
import { PromptInput } from './PromptInput';
|
||||
import { accent, glyph } from './theme';
|
||||
import { nextKey, resultSummary, toolDetail, withResult, type Line, type NewLine } from './transcript';
|
||||
@@ -55,6 +55,10 @@ export type AppHooks = {
|
||||
applyProvider: (result: OnboardResult) => Promise<string>;
|
||||
listModels: () => Promise<{ models: string[]; warning?: string }>;
|
||||
listSessions: () => Promise<string>;
|
||||
/** Full-text search over saved sessions; returns a formatted panel body or ''. */
|
||||
searchSessions: (query: string) => Promise<string>;
|
||||
/** Fork the current session at the last turn boundary; returns a resume hint. */
|
||||
forkSession: () => Promise<string>;
|
||||
listSkills: () => string;
|
||||
listPlugins: () => string;
|
||||
listMemory: () => Promise<string>;
|
||||
@@ -705,6 +709,36 @@ export function App({
|
||||
setWorking(false);
|
||||
return;
|
||||
}
|
||||
case 'changes': {
|
||||
push({ kind: 'user', text: chosen.trim() });
|
||||
setPanel(changesPanel(session));
|
||||
return;
|
||||
}
|
||||
case 'search': {
|
||||
push({ kind: 'user', text: chosen.trim() });
|
||||
setWorking(true);
|
||||
try {
|
||||
const results = await hooks.searchSessions(action.query);
|
||||
if (results === '') {
|
||||
push({ kind: 'info', text: 'no sessions match that phrase' });
|
||||
} else {
|
||||
setPanel({ title: `sessions: ${action.query}`, body: results });
|
||||
}
|
||||
} catch (e) {
|
||||
push({ kind: 'error', text: e instanceof Error ? e.message : String(e) });
|
||||
}
|
||||
setWorking(false);
|
||||
return;
|
||||
}
|
||||
case 'fork': {
|
||||
push({ kind: 'user', text: chosen.trim() });
|
||||
try {
|
||||
push({ kind: 'info', text: await hooks.forkSession() });
|
||||
} catch (e) {
|
||||
push({ kind: 'error', text: e instanceof Error ? e.message : String(e) });
|
||||
}
|
||||
return;
|
||||
}
|
||||
case 'provider':
|
||||
push({ kind: 'user', text: chosen.trim() });
|
||||
setOnboarding(true);
|
||||
|
||||
@@ -57,6 +57,14 @@ export function costPanel(
|
||||
`- ceiling: ${ceiling.usd === undefined ? 'unpriced' : formatUsd(ceiling.usd)} of ${formatUsd(ceiling.ceiling)}`,
|
||||
);
|
||||
}
|
||||
const perTurn = session.maxSpendPerTurn();
|
||||
if (perTurn !== undefined) {
|
||||
lines.push(`- per-turn cap: ${formatUsd(perTurn)}`);
|
||||
}
|
||||
const cache = session.promptCacheStats();
|
||||
if (cache.hits + cache.misses > 0) {
|
||||
lines.push(`- prompt cache: ${cache.hits} hits / ${cache.misses} misses`);
|
||||
}
|
||||
|
||||
lines.push(`- context: ~${session.estimatedTokens()} tokens (est.)`, `- agent: \`${info.agent}\` thinking \`${info.thinking}\``);
|
||||
lines.push(`- pricing verified: ${PRICING_VERIFIED_AT} (est., verify before billing)`);
|
||||
@@ -77,3 +85,20 @@ export const todosPanel = (session: Session): Panel => ({
|
||||
title: 'task list',
|
||||
body: todoLines(session.notebook.state().todos),
|
||||
});
|
||||
|
||||
const renderChangeList = (kind: string, paths: string[]): string =>
|
||||
paths.length === 0 ? '' : `${kind}:\n${paths.map((p) => `- ${p}`).join('\n')}`;
|
||||
|
||||
/** What the last turn changed on disk — the file side of /undo, without undoing. */
|
||||
export function changesPanel(session: Session): Panel {
|
||||
const summary = session.lastTurnSummary();
|
||||
if (!summary) return { title: 'changes', body: 'the last turn changed no files (bash effects are not tracked)' };
|
||||
const body = [
|
||||
renderChangeList('added', summary.added),
|
||||
renderChangeList('modified', summary.modified),
|
||||
renderChangeList('deleted', summary.deleted),
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join('\n');
|
||||
return { title: 'changes', body };
|
||||
}
|
||||
Reference in new issue
Block a user