Merge remote-tracking branch 'refs/remotes/upstream/main'
ci / check (macos-latest) (push) Canceled after 0s
ci / check (ubuntu-latest) (push) Canceled after 0s
ci / check (windows-latest) (push) Canceled after 0s

# Conflicts:
#	ROADMAP.md
#	TODO.md
#	src/cli.tsx
#	src/config.ts
#	src/mcp.ts
#	src/permission.ts
#	src/prompt.ts
#	src/session.ts
#	src/snapshot.ts
#	src/subagent.ts
#	src/tools-extra.ts
#	src/tools.ts
#	src/ui/App.tsx
#	test/mcp.test.ts
#	test/prune.test.ts
#	test/session.test.ts
#	test/tools.test.ts
This commit is contained in:
asepharyana
2026-09-21 20:43:35 +07:00
59 changed files with 3481 additions and 463 deletions
+61 -35
View File
@@ -38,7 +38,7 @@ import { CommandMenu, InstallConfirm, Picker } from './Pickers';
import { contextPanel, costPanel, todosPanel, toolsPanel, changesPanel, diffPanel, diffReviewPanel, workflowPanel } from './panel-bodies';
import { PromptInput } from './PromptInput';
import { accent, glyph } from './theme';
import { nextKey, resultSummary, toolDetail, withResult, type Line, type NewLine } from './transcript';
import { historyFromMessages, nextKey, resultSummary, toolDetail, withResult, type Line, type NewLine } from './transcript';
export { createApprovalBridge, createNoticeBus, createSubagentBus, applySubagentEvent };
export type { ApprovalBridge, NoticeBus, SubagentBus };
@@ -144,7 +144,11 @@ export function App({
stdout.off('resize', onResize);
};
}, [stdout]);
const [history, setHistory] = useState<Line[]>([]);
// Seeded from the resumed history: a session loaded with -r/-c should show its
// saved conversation rather than a blank transcript. Only read once, at mount.
const [history, setHistory] = useState<Line[]>(() =>
session.messages.length === 0 ? [] : historyFromMessages(session.messages as { role?: string; content?: unknown }[]),
);
const [draft, setDraft] = useState('');
const [live, setLive] = useState('');
const [busy, setBusy] = useState(false);
@@ -196,17 +200,33 @@ export function App({
// The walk costs a full ignore-aware traversal, so it happens on the first `@`
// rather than at startup, and re-runs when files change (listPaths is cached
// in hook, but App keeps seq so a stale `paths` is dropped).
// in hook, but App keeps seq so a stale `paths` is dropped). A slow cooldown
// also re-walks so a file created after that first `@` shows up within a short
// window instead of staying hidden all session.
const pathsRef = useRef(paths);
useEffect(() => {
if (token === undefined || paths !== undefined) return;
let live = true;
void hooks.listPaths().then((all) => {
if (live) setPaths(all);
});
return () => {
live = false;
};
}, [hooks, paths, token]);
pathsRef.current = paths;
}, [paths]);
const didLoadRef = useRef(false);
useEffect(() => {
if (token === undefined) return;
if (!didLoadRef.current) {
didLoadRef.current = true;
let live = true;
void hooks.listPaths().then((all) => {
if (live) setPaths(all);
});
const refresh = setInterval(async () => {
if (!live) return;
const all = await hooks.listPaths();
if (live && JSON.stringify(all) !== JSON.stringify(pathsRef.current)) setPaths(all);
}, 10_000);
return () => {
live = false;
clearInterval(refresh);
};
}
}, [hooks, token]);
// A file mutated this turn: drop the cached walk so next `@` re-walks.
const seq = hooks.fileChangeSeq();
@@ -712,34 +732,18 @@ export function App({
push({ kind: 'user', text: chosen.trim() });
try {
const msg = await hooks.resumeSession(action.id);
setHistory([]);
// Reflect the freshly loaded history: session.messages now holds the
// restored wire messages, and the transcript must show them again.
setHistory(
session.messages.length === 0
? []
: historyFromMessages(session.messages as { role?: string; content?: unknown }[]),
);
push({ kind: 'info', text: msg });
} catch (e) {
push({ kind: 'error', text: e instanceof Error ? e.message : String(e) });
}
return;
case 'undo': {
push({ kind: 'user', text: chosen.trim() });
setWorking(true);
try {
push({ kind: 'info', text: await session.undo() });
} catch (e) {
push({ kind: 'error', text: e instanceof Error ? e.message : String(e) });
}
setWorking(false);
return;
}
case 'redo': {
push({ kind: 'user', text: chosen.trim() });
setWorking(true);
try {
push({ kind: 'info', text: await session.redo() });
} catch (e) {
push({ kind: 'error', text: e instanceof Error ? e.message : String(e) });
}
setWorking(false);
return;
}
case 'changes': {
push({ kind: 'user', text: chosen.trim() });
setPanel(changesPanel(session));
@@ -843,6 +847,28 @@ export function App({
setModelPicker(models);
return;
}
case 'undo': {
push({ kind: 'user', text: chosen.trim() });
setWorking(true);
try {
push({ kind: 'info', text: await session.undo() });
} catch (e) {
push({ kind: 'error', text: e instanceof Error ? e.message : String(e) });
}
setWorking(false);
return;
}
case 'redo': {
push({ kind: 'user', text: chosen.trim() });
setWorking(true);
try {
push({ kind: 'info', text: await session.redo() });
} catch (e) {
push({ kind: 'error', text: e instanceof Error ? e.message : String(e) });
}
setWorking(false);
return;
}
case 'compact': {
push({ kind: 'user', text: chosen.trim() });
setWorking(true);