@kenkaiiii/ggcoder 4.3.149 → 4.3.151
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/ui/App.d.ts +64 -1
- package/dist/ui/App.d.ts.map +1 -1
- package/dist/ui/App.js +344 -88
- package/dist/ui/App.js.map +1 -1
- package/dist/ui/components/AssistantMessage.d.ts +1 -1
- package/dist/ui/components/AssistantMessage.d.ts.map +1 -1
- package/dist/ui/components/AssistantMessage.js +9 -1
- package/dist/ui/components/AssistantMessage.js.map +1 -1
- package/dist/ui/components/StreamingArea.d.ts.map +1 -1
- package/dist/ui/components/StreamingArea.js +7 -3
- package/dist/ui/components/StreamingArea.js.map +1 -1
- package/dist/ui/render.d.ts +57 -0
- package/dist/ui/render.d.ts.map +1 -1
- package/dist/ui/render.js +103 -25
- package/dist/ui/render.js.map +1 -1
- package/dist/utils/plan-steps.d.ts +22 -0
- package/dist/utils/plan-steps.d.ts.map +1 -1
- package/dist/utils/plan-steps.js +32 -0
- package/dist/utils/plan-steps.js.map +1 -1
- package/package.json +4 -4
package/dist/ui/App.js
CHANGED
|
@@ -48,7 +48,7 @@ import { estimateConversationTokens } from "../core/compaction/token-estimator.j
|
|
|
48
48
|
import { PROMPT_COMMANDS, getPromptCommand } from "../core/prompt-commands.js";
|
|
49
49
|
import { loadCustomCommands } from "../core/custom-commands.js";
|
|
50
50
|
import { buildSystemPrompt } from "../system-prompt.js";
|
|
51
|
-
import { extractPlanSteps, findCompletedMarkers, markStepsCompleted, stripDoneMarkers, } from "../utils/plan-steps.js";
|
|
51
|
+
import { extractPlanSteps, findCompletedMarkers, markStepsCompleted, segmentDisplayText, stripDoneMarkers, } from "../utils/plan-steps.js";
|
|
52
52
|
import { getMCPServers } from "../core/mcp/index.js";
|
|
53
53
|
import { trimFlushedItems, flushOnTurnText, flushOnTurnEnd, flushOverflow, } from "./live-item-flush.js";
|
|
54
54
|
import { Buddy } from "./buddy/Buddy.js";
|
|
@@ -278,14 +278,17 @@ export function App(props) {
|
|
|
278
278
|
// Hoisted before terminal title hook so it can reference them
|
|
279
279
|
const [lastUserMessage, setLastUserMessage] = useState("");
|
|
280
280
|
const [exitPending, setExitPending] = useState(false);
|
|
281
|
-
|
|
281
|
+
// Initialize from planModeRef (lives outside React in cli.ts) so plan
|
|
282
|
+
// mode survives /clear's unmount/remount, matching the prior behavior
|
|
283
|
+
// where /clear didn't toggle plan mode off.
|
|
284
|
+
const [planMode, setPlanMode] = useState(props.planModeRef?.current ?? false);
|
|
282
285
|
const planModeLocalRef = useRef(false);
|
|
283
286
|
planModeLocalRef.current = planMode;
|
|
284
287
|
// Terminal title — updated later after agentLoop is created
|
|
285
288
|
// (hoisted here so the hook is always called in the same order)
|
|
286
289
|
const [titleRunning, setTitleRunning] = useState(false);
|
|
287
|
-
const [sessionTitle, setSessionTitle] = useState(
|
|
288
|
-
const sessionTitleGeneratedRef = useRef(false);
|
|
290
|
+
const [sessionTitle, setSessionTitle] = useState(() => props.sessionStore?.sessionTitle);
|
|
291
|
+
const sessionTitleGeneratedRef = useRef(props.sessionStore?.sessionTitleGenerated ?? false);
|
|
289
292
|
useTerminalTitle({
|
|
290
293
|
isRunning: titleRunning,
|
|
291
294
|
sessionTitle,
|
|
@@ -296,6 +299,11 @@ export function App(props) {
|
|
|
296
299
|
// gatsby). Ink's Static (build/components/Static.js) starts with index=0
|
|
297
300
|
// so slice(0) returns the full array regardless of length.
|
|
298
301
|
const [history, setHistory] = useState(() => {
|
|
302
|
+
// sessionStore wins (lives across remount). Falls back to initialHistory
|
|
303
|
+
// (loaded from a session file at startup), then a fresh banner-only list.
|
|
304
|
+
const stored = props.sessionStore?.history;
|
|
305
|
+
if (stored && stored.length > 0)
|
|
306
|
+
return stored;
|
|
299
307
|
if (props.initialHistory && props.initialHistory.length > 0) {
|
|
300
308
|
return compactHistory(trimFlushedItems(props.initialHistory));
|
|
301
309
|
}
|
|
@@ -303,7 +311,9 @@ export function App(props) {
|
|
|
303
311
|
});
|
|
304
312
|
// Items from the current/last turn — rendered in the live area so they stay visible
|
|
305
313
|
const [liveItems, setLiveItems] = useState([]);
|
|
306
|
-
|
|
314
|
+
// overlay seeded from sessionStore (lives across remount). Falls back to
|
|
315
|
+
// props.initialOverlay (CLI launched with one), then null.
|
|
316
|
+
const [overlay, setOverlay] = useState(props.sessionStore?.overlay ?? props.initialOverlay ?? null);
|
|
307
317
|
const [taskCount, setTaskCount] = useState(() => getTaskCount(props.cwd));
|
|
308
318
|
const [eyesCount, setEyesCount] = useState(() => isEyesActive(props.cwd) ? journalCount({ status: "open" }, props.cwd) : undefined);
|
|
309
319
|
const [updatePending, setUpdatePending] = useState(() => getPendingUpdate(props.version) !== null);
|
|
@@ -324,14 +334,14 @@ export function App(props) {
|
|
|
324
334
|
const [currentProvider, setCurrentProvider] = useState(props.provider);
|
|
325
335
|
const [currentTools, setCurrentTools] = useState(props.tools);
|
|
326
336
|
const [thinkingEnabled, setThinkingEnabled] = useState(!!props.thinking);
|
|
327
|
-
const messagesRef = useRef(props.messages);
|
|
328
|
-
const [planAutoExpand, setPlanAutoExpand] = useState(false);
|
|
329
|
-
const approvedPlanPathRef = useRef(
|
|
330
|
-
const planStepsRef = useRef([]);
|
|
331
|
-
const [planSteps, setPlanSteps] = useState([]);
|
|
337
|
+
const messagesRef = useRef(props.sessionStore?.messages ?? props.messages);
|
|
338
|
+
const [planAutoExpand, setPlanAutoExpand] = useState(props.sessionStore?.planAutoExpand ?? false);
|
|
339
|
+
const approvedPlanPathRef = useRef(props.sessionStore?.approvedPlanPath);
|
|
340
|
+
const planStepsRef = useRef(props.sessionStore?.planSteps ?? []);
|
|
341
|
+
const [planSteps, setPlanSteps] = useState(props.sessionStore?.planSteps ?? []);
|
|
332
342
|
const nextIdRef = useRef(0);
|
|
333
343
|
const sessionManagerRef = useRef(props.sessionsDir ? new SessionManager(props.sessionsDir) : null);
|
|
334
|
-
const sessionPathRef = useRef(props.sessionPath);
|
|
344
|
+
const sessionPathRef = useRef(props.sessionStore?.sessionPath ?? props.sessionPath);
|
|
335
345
|
const persistedIndexRef = useRef(messagesRef.current.length);
|
|
336
346
|
/** Last actual API-reported input token count (from turn_end). */
|
|
337
347
|
const lastActualTokensRef = useRef(0);
|
|
@@ -351,6 +361,44 @@ export function App(props) {
|
|
|
351
361
|
pendingFlushRef.current = [...pendingFlushRef.current, ...items];
|
|
352
362
|
setFlushGeneration((g) => g + 1);
|
|
353
363
|
}, []);
|
|
364
|
+
// Mirror runtime state choices (model/provider/thinking) into renderApp's
|
|
365
|
+
// closure so unmount/remount preserves them.
|
|
366
|
+
const onRuntimeStateChange = props.onRuntimeStateChange;
|
|
367
|
+
useEffect(() => {
|
|
368
|
+
onRuntimeStateChange?.({ model: currentModel });
|
|
369
|
+
}, [currentModel, onRuntimeStateChange]);
|
|
370
|
+
useEffect(() => {
|
|
371
|
+
onRuntimeStateChange?.({ provider: currentProvider });
|
|
372
|
+
}, [currentProvider, onRuntimeStateChange]);
|
|
373
|
+
useEffect(() => {
|
|
374
|
+
onRuntimeStateChange?.({
|
|
375
|
+
thinking: thinkingEnabled ? (props.thinking ?? "medium") : undefined,
|
|
376
|
+
});
|
|
377
|
+
}, [thinkingEnabled, props.thinking, onRuntimeStateChange]);
|
|
378
|
+
// Mirror session state into renderApp's closure so resetUI() can re-seed
|
|
379
|
+
// the conversation on remount. Each panel that previously did a bare ANSI
|
|
380
|
+
// screen clear (overlay open/close, plan accept/reject, /clear, startTask)
|
|
381
|
+
// now goes through resetUI; without these mirrors, the chat would vanish.
|
|
382
|
+
const sessionStore = props.sessionStore;
|
|
383
|
+
useEffect(() => {
|
|
384
|
+
if (sessionStore)
|
|
385
|
+
sessionStore.history = history;
|
|
386
|
+
}, [history, sessionStore]);
|
|
387
|
+
useEffect(() => {
|
|
388
|
+
if (sessionStore)
|
|
389
|
+
sessionStore.planSteps = planSteps;
|
|
390
|
+
}, [planSteps, sessionStore]);
|
|
391
|
+
useEffect(() => {
|
|
392
|
+
if (sessionStore)
|
|
393
|
+
sessionStore.sessionTitle = sessionTitle;
|
|
394
|
+
}, [sessionTitle, sessionStore]);
|
|
395
|
+
useEffect(() => {
|
|
396
|
+
if (sessionStore)
|
|
397
|
+
sessionStore.overlay = overlay;
|
|
398
|
+
}, [overlay, sessionStore]);
|
|
399
|
+
// pendingAction is consumed via a useEffect AFTER agentLoop is created
|
|
400
|
+
// — see below where useAgentLoop is set up.
|
|
401
|
+
const pendingActionConsumedRef = useRef(false);
|
|
354
402
|
// Derive credentials for the current provider
|
|
355
403
|
const currentCreds = props.credentialsByProvider?.[currentProvider];
|
|
356
404
|
const activeApiKey = currentCreds?.accessToken ?? props.apiKey;
|
|
@@ -422,6 +470,13 @@ export function App(props) {
|
|
|
422
470
|
// premature "done" status that fires when the agent loop finishes
|
|
423
471
|
planOverlayPendingRef.current = true;
|
|
424
472
|
setTimeout(() => {
|
|
473
|
+
// NOTE: this is the one open-overlay path that does NOT remount via
|
|
474
|
+
// resetUI. It runs while the agent is still mid-turn (after the
|
|
475
|
+
// exit_plan tool returned but before onDone fires), and unmounting
|
|
476
|
+
// here would kill the in-flight agent stream. Keep the bare ANSI
|
|
477
|
+
// clear; the drift bug is tolerable across just the agent's
|
|
478
|
+
// wrap-up turn, and onApprove/onReject both remount cleanly via
|
|
479
|
+
// resetUI when the user resolves the plan.
|
|
425
480
|
stdout?.write("\x1b[2J\x1b[3J\x1b[H");
|
|
426
481
|
setPlanAutoExpand(true);
|
|
427
482
|
setOverlay("plan");
|
|
@@ -708,17 +763,53 @@ export function App(props) {
|
|
|
708
763
|
if (flushed.length > 0) {
|
|
709
764
|
queueFlush(flushed);
|
|
710
765
|
}
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
766
|
+
// Split text on [DONE:N] markers so each marker renders inline as
|
|
767
|
+
// a styled "✓ Step N: <description>" item at the position the
|
|
768
|
+
// agent emitted it, instead of vanishing into stripped whitespace.
|
|
769
|
+
// Falls back to a single assistant item containing the
|
|
770
|
+
// marker-stripped text when there are no markers (keeps the
|
|
771
|
+
// common case zero-cost).
|
|
772
|
+
const segments = segmentDisplayText(text, planStepsRef.current);
|
|
773
|
+
const items = [];
|
|
774
|
+
let thinkingAttached = false;
|
|
775
|
+
for (const seg of segments) {
|
|
776
|
+
if (seg.kind === "text") {
|
|
777
|
+
items.push({
|
|
778
|
+
kind: "assistant",
|
|
779
|
+
text: stripDoneMarkers(seg.text),
|
|
780
|
+
// Attach thinking only to the first text segment so we
|
|
781
|
+
// don't render duplicate ThinkingBlocks when a turn
|
|
782
|
+
// contains multiple text chunks split by markers.
|
|
783
|
+
thinking: thinkingAttached ? undefined : thinking,
|
|
784
|
+
thinkingMs: thinkingAttached ? undefined : thinkingMs,
|
|
785
|
+
planMode: planModeLocalRef.current,
|
|
786
|
+
id: getId(),
|
|
787
|
+
});
|
|
788
|
+
thinkingAttached = true;
|
|
789
|
+
}
|
|
790
|
+
else {
|
|
791
|
+
items.push({
|
|
792
|
+
kind: "step_done",
|
|
793
|
+
stepNum: seg.stepNum,
|
|
794
|
+
description: seg.description,
|
|
795
|
+
id: getId(),
|
|
796
|
+
});
|
|
797
|
+
}
|
|
798
|
+
}
|
|
799
|
+
// No segments at all (text was empty/whitespace, no markers).
|
|
800
|
+
// Still emit an assistant item so a thinking block renders if
|
|
801
|
+
// there was thinking content for this turn.
|
|
802
|
+
if (items.length === 0) {
|
|
803
|
+
items.push({
|
|
714
804
|
kind: "assistant",
|
|
715
|
-
text:
|
|
805
|
+
text: "",
|
|
716
806
|
thinking,
|
|
717
807
|
thinkingMs,
|
|
718
808
|
planMode: planModeLocalRef.current,
|
|
719
809
|
id: getId(),
|
|
720
|
-
}
|
|
721
|
-
|
|
810
|
+
});
|
|
811
|
+
}
|
|
812
|
+
return items;
|
|
722
813
|
});
|
|
723
814
|
}, []),
|
|
724
815
|
onToolStart: useCallback((toolCallId, name, args) => {
|
|
@@ -1188,6 +1279,33 @@ export function App(props) {
|
|
|
1188
1279
|
useEffect(() => {
|
|
1189
1280
|
setTitleRunning(agentLoop.isRunning);
|
|
1190
1281
|
}, [agentLoop.isRunning]);
|
|
1282
|
+
// Consume sessionStore.pendingAction once on mount. Set by resetUI options
|
|
1283
|
+
// for paths that remount AND immediately drive the agent (plan accept,
|
|
1284
|
+
// plan reject, startTask, pixel fix). The action survives the unmount
|
|
1285
|
+
// because it lives in renderApp's closure (sessionStore), not React state.
|
|
1286
|
+
useEffect(() => {
|
|
1287
|
+
if (pendingActionConsumedRef.current)
|
|
1288
|
+
return;
|
|
1289
|
+
const action = sessionStore?.pendingAction;
|
|
1290
|
+
if (!action)
|
|
1291
|
+
return;
|
|
1292
|
+
pendingActionConsumedRef.current = true;
|
|
1293
|
+
if (sessionStore)
|
|
1294
|
+
sessionStore.pendingAction = undefined;
|
|
1295
|
+
if (action.infoText) {
|
|
1296
|
+
setLiveItems((prev) => [
|
|
1297
|
+
...prev,
|
|
1298
|
+
{ kind: "info", text: action.infoText, id: getId() },
|
|
1299
|
+
]);
|
|
1300
|
+
}
|
|
1301
|
+
setDoneStatus(null);
|
|
1302
|
+
void agentLoop.run(action.prompt).catch((err) => {
|
|
1303
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
1304
|
+
log("ERROR", "error", errMsg);
|
|
1305
|
+
setLiveItems((prev) => [...prev, { kind: "error", message: errMsg, id: getId() }]);
|
|
1306
|
+
});
|
|
1307
|
+
// Intentional one-shot: run once on mount, never re-fire on re-render.
|
|
1308
|
+
}, []);
|
|
1191
1309
|
// Refresh eyes badge count when the agent settles (end of a turn) — a turn
|
|
1192
1310
|
// may have logged new rough/wish/blocked signals. Also covers the case where
|
|
1193
1311
|
// /eyes was run for the first time (manifest now exists).
|
|
@@ -1229,13 +1347,29 @@ export function App(props) {
|
|
|
1229
1347
|
if (trimmed === "/quit" || trimmed === "/q" || trimmed === "/exit") {
|
|
1230
1348
|
process.exit(0);
|
|
1231
1349
|
}
|
|
1232
|
-
// Handle /clear —
|
|
1350
|
+
// Handle /clear — tear down the entire Ink instance and rebuild fresh.
|
|
1351
|
+
// Patching Ink's internal frame tracking in place (log-update reset,
|
|
1352
|
+
// lastOutput cleared, fullStaticOutput dropped, staticKey bump) all
|
|
1353
|
+
// looked correct for one frame but left the live area drifting on
|
|
1354
|
+
// subsequent streaming responses — Ink's cursor math depends on
|
|
1355
|
+
// terminal-state assumptions that ANSI clearing breaks. The reliable
|
|
1356
|
+
// fix is unmount + render again. Runtime state (model, provider,
|
|
1357
|
+
// thinking) survives via renderApp's closure-held `runtimeState`,
|
|
1358
|
+
// mirrored from React state via the useEffects above.
|
|
1233
1359
|
if (trimmed === "/clear") {
|
|
1234
|
-
|
|
1235
|
-
|
|
1360
|
+
if (props.resetUI) {
|
|
1361
|
+
void (async () => {
|
|
1362
|
+
const newPrompt = await buildSystemPrompt(props.cwd, props.skills, planMode, undefined);
|
|
1363
|
+
props.resetUI?.({
|
|
1364
|
+
wipeSession: true,
|
|
1365
|
+
messages: [{ role: "system", content: newPrompt }],
|
|
1366
|
+
});
|
|
1367
|
+
})();
|
|
1368
|
+
return;
|
|
1369
|
+
}
|
|
1370
|
+
// Fallback path (resetUI not wired — e.g. tests). Best-effort: clear
|
|
1371
|
+
// React state in place. The Ink-internal drift bug remains here.
|
|
1236
1372
|
stdout?.write("\x1b[2J\x1b[3J\x1b[H");
|
|
1237
|
-
// Discard any items queued for two-phase flush so they don't leak
|
|
1238
|
-
// into the new session after the Static remount.
|
|
1239
1373
|
pendingFlushRef.current = [];
|
|
1240
1374
|
setHistory([{ kind: "banner", id: "banner" }]);
|
|
1241
1375
|
setLiveItems([]);
|
|
@@ -1243,7 +1377,6 @@ export function App(props) {
|
|
|
1243
1377
|
approvedPlanPathRef.current = undefined;
|
|
1244
1378
|
planStepsRef.current = [];
|
|
1245
1379
|
setPlanSteps([]);
|
|
1246
|
-
// Rebuild system prompt without the approved plan
|
|
1247
1380
|
void (async () => {
|
|
1248
1381
|
const newPrompt = await buildSystemPrompt(props.cwd, props.skills, planMode, undefined);
|
|
1249
1382
|
messagesRef.current = [{ role: "system", content: newPrompt }];
|
|
@@ -1252,8 +1385,6 @@ export function App(props) {
|
|
|
1252
1385
|
agentLoop.reset();
|
|
1253
1386
|
setSessionTitle(undefined);
|
|
1254
1387
|
sessionTitleGeneratedRef.current = false;
|
|
1255
|
-
// Bump staticKey to force Ink's <Static> to remount, discarding its
|
|
1256
|
-
// internal record of previously rendered items so they don't reappear.
|
|
1257
1388
|
setStaticKey((k) => k + 1);
|
|
1258
1389
|
setLiveItems([{ kind: "info", text: "Session cleared.", id: getId() }]);
|
|
1259
1390
|
return;
|
|
@@ -1339,9 +1470,16 @@ export function App(props) {
|
|
|
1339
1470
|
}
|
|
1340
1471
|
// Handle /plans — open plan pane
|
|
1341
1472
|
if (trimmed === "/plans") {
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1473
|
+
if (props.resetUI && props.sessionStore) {
|
|
1474
|
+
props.sessionStore.overlay = "plan";
|
|
1475
|
+
props.sessionStore.planAutoExpand = false;
|
|
1476
|
+
props.resetUI();
|
|
1477
|
+
}
|
|
1478
|
+
else {
|
|
1479
|
+
stdout?.write("\x1b[2J\x1b[3J\x1b[H");
|
|
1480
|
+
setPlanAutoExpand(false);
|
|
1481
|
+
setOverlay("plan");
|
|
1482
|
+
}
|
|
1345
1483
|
return;
|
|
1346
1484
|
}
|
|
1347
1485
|
// Handle prompt-template commands (built-in + custom from .gg/commands/)
|
|
@@ -1707,6 +1845,8 @@ export function App(props) {
|
|
|
1707
1845
|
return (_jsx(Box, { marginTop: 1, flexShrink: 1, borderStyle: "round", borderColor: theme.success, paddingX: 1, children: _jsxs(Text, { color: theme.success, bold: true, wrap: "wrap", children: ["✨ ", item.text] }) }, item.id));
|
|
1708
1846
|
case "plan_transition":
|
|
1709
1847
|
return (_jsx(Box, { marginTop: 1, flexShrink: 1, children: _jsxs(Text, { color: theme.planPrimary, bold: true, wrap: "wrap", children: [item.active ? "● " : "● ", item.text] }) }, item.id));
|
|
1848
|
+
case "step_done":
|
|
1849
|
+
return (_jsx(Box, { marginTop: 1, flexShrink: 1, children: _jsxs(Text, { wrap: "wrap", children: [_jsx(Text, { color: theme.success, bold: true, children: "✓ " }), _jsx(Text, { color: theme.success, bold: true, children: `Step ${item.stepNum} done` }), item.description ? (_jsx(Text, { color: theme.textDim, children: ` — ${item.description}` })) : null] }) }, item.id));
|
|
1710
1850
|
case "queued":
|
|
1711
1851
|
return (_jsxs(Box, { marginTop: 1, children: [_jsx(Text, { color: theme.accent, bold: true, children: "⏳ Queued: " }), _jsxs(Text, { color: theme.text, wrap: "wrap", children: [item.text, item.imageCount
|
|
1712
1852
|
? ` (+${item.imageCount} image${item.imageCount > 1 ? "s" : ""})`
|
|
@@ -1724,7 +1864,42 @@ export function App(props) {
|
|
|
1724
1864
|
// ── Start a task (shared by manual "work on it" and run-all) ──
|
|
1725
1865
|
const startTask = useCallback((title, prompt, taskId) => {
|
|
1726
1866
|
setTaskCount(getTaskCount(props.cwd));
|
|
1727
|
-
|
|
1867
|
+
const shortId = taskId.slice(0, 8);
|
|
1868
|
+
const completionHint = `\n\n---\nWhen you have fully completed this task, call the tasks tool to mark it done:\n` +
|
|
1869
|
+
`tasks({ action: "done", id: "${shortId}" })`;
|
|
1870
|
+
const fullPrompt = prompt + completionHint;
|
|
1871
|
+
if (props.resetUI && props.sessionStore) {
|
|
1872
|
+
// Preserve the current system prompt (may differ from the launch
|
|
1873
|
+
// config — e.g. plan mode toggled or skills changed).
|
|
1874
|
+
const sysMsg = messagesRef.current[0];
|
|
1875
|
+
const newMessages = sysMsg && sysMsg.role === "system" ? [sysMsg] : messagesRef.current.slice(0, 1);
|
|
1876
|
+
const taskItem = { kind: "task", title, id: String(nextIdRef.current++) };
|
|
1877
|
+
const sm = sessionManagerRef.current;
|
|
1878
|
+
void (async () => {
|
|
1879
|
+
let newSessionPath;
|
|
1880
|
+
if (sm) {
|
|
1881
|
+
try {
|
|
1882
|
+
const s = await sm.create(props.cwd, currentProvider, currentModel);
|
|
1883
|
+
newSessionPath = s.path;
|
|
1884
|
+
log("INFO", "tasks", "New session for task", { path: s.path });
|
|
1885
|
+
}
|
|
1886
|
+
catch {
|
|
1887
|
+
// session creation is best-effort
|
|
1888
|
+
}
|
|
1889
|
+
}
|
|
1890
|
+
if (props.sessionStore)
|
|
1891
|
+
props.sessionStore.overlay = null;
|
|
1892
|
+
props.resetUI?.({
|
|
1893
|
+
wipeSession: true,
|
|
1894
|
+
messages: newMessages,
|
|
1895
|
+
history: [{ kind: "banner", id: "banner" }, taskItem],
|
|
1896
|
+
sessionPath: newSessionPath,
|
|
1897
|
+
pendingAction: { prompt: fullPrompt },
|
|
1898
|
+
});
|
|
1899
|
+
})();
|
|
1900
|
+
return;
|
|
1901
|
+
}
|
|
1902
|
+
// Fallback path (resetUI not wired — tests).
|
|
1728
1903
|
stdout?.write("\x1b[2J\x1b[3J\x1b[H");
|
|
1729
1904
|
setHistory([{ kind: "banner", id: "banner" }]);
|
|
1730
1905
|
setLiveItems([]);
|
|
@@ -1738,12 +1913,6 @@ export function App(props) {
|
|
|
1738
1913
|
log("INFO", "tasks", "New session for task", { path: s.path });
|
|
1739
1914
|
});
|
|
1740
1915
|
}
|
|
1741
|
-
// Inject completion instruction so the agent marks the task done
|
|
1742
|
-
const shortId = taskId.slice(0, 8);
|
|
1743
|
-
const completionHint = `\n\n---\nWhen you have fully completed this task, call the tasks tool to mark it done:\n` +
|
|
1744
|
-
`tasks({ action: "done", id: "${shortId}" })`;
|
|
1745
|
-
const fullPrompt = prompt + completionHint;
|
|
1746
|
-
// Show the short title in the TUI, but send the full prompt to the agent
|
|
1747
1916
|
const taskItem = { kind: "task", title, id: getId() };
|
|
1748
1917
|
setLastUserMessage(title);
|
|
1749
1918
|
setDoneStatus(null);
|
|
@@ -1762,11 +1931,18 @@ export function App(props) {
|
|
|
1762
1931
|
? { kind: "info", text: "Request was stopped.", id: getId() }
|
|
1763
1932
|
: { kind: "error", message: msg, id: getId() },
|
|
1764
1933
|
]);
|
|
1765
|
-
// Stop run-all if a task errors
|
|
1766
1934
|
setRunAllTasks(false);
|
|
1767
1935
|
}
|
|
1768
1936
|
})();
|
|
1769
|
-
}, [
|
|
1937
|
+
}, [
|
|
1938
|
+
props.cwd,
|
|
1939
|
+
props.resetUI,
|
|
1940
|
+
props.sessionStore,
|
|
1941
|
+
stdout,
|
|
1942
|
+
agentLoop,
|
|
1943
|
+
currentProvider,
|
|
1944
|
+
currentModel,
|
|
1945
|
+
]);
|
|
1770
1946
|
// Keep refs in sync for access from stale closures (onDone)
|
|
1771
1947
|
startTaskRef.current = startTask;
|
|
1772
1948
|
useEffect(() => {
|
|
@@ -1850,10 +2026,16 @@ export function App(props) {
|
|
|
1850
2026
|
const isPixelView = overlay === "pixel";
|
|
1851
2027
|
const isOverlayView = isTaskView || isSkillsView || isPlanView || isEyesView || isPixelView;
|
|
1852
2028
|
return (_jsxs(Box, { flexDirection: "column", width: columns, children: [_jsx(Static, { items: isOverlayView ? [] : history, style: { width: "100%" }, children: (item) => (_jsx(Box, { flexDirection: "column", paddingRight: 1, children: renderItem(item) }, item.id)) }, `${resizeKey}-${staticKey}`), isTaskView ? (_jsx(TaskOverlay, { cwd: props.cwd, agentRunning: agentLoop.isRunning, onClose: () => {
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
|
|
2029
|
+
if (props.resetUI && props.sessionStore) {
|
|
2030
|
+
props.sessionStore.overlay = null;
|
|
2031
|
+
props.resetUI();
|
|
2032
|
+
}
|
|
2033
|
+
else {
|
|
2034
|
+
stdout?.write("\x1b[2J\x1b[3J\x1b[H");
|
|
2035
|
+
setTaskCount(getTaskCount(props.cwd));
|
|
2036
|
+
setStaticKey((k) => k + 1);
|
|
2037
|
+
setOverlay(null);
|
|
2038
|
+
}
|
|
1857
2039
|
}, onWorkOnTask: (title, prompt, taskId) => {
|
|
1858
2040
|
setOverlay(null);
|
|
1859
2041
|
startTask(title, prompt, taskId);
|
|
@@ -1866,9 +2048,15 @@ export function App(props) {
|
|
|
1866
2048
|
startTask(next.title, next.prompt, next.id);
|
|
1867
2049
|
}
|
|
1868
2050
|
} })) : isPixelView ? (_jsx(PixelOverlay, { version: props.version, agentRunning: agentLoop.isRunning, onClose: () => {
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
|
|
2051
|
+
if (props.resetUI && props.sessionStore) {
|
|
2052
|
+
props.sessionStore.overlay = null;
|
|
2053
|
+
props.resetUI();
|
|
2054
|
+
}
|
|
2055
|
+
else {
|
|
2056
|
+
stdout?.write("\x1b[2J\x1b[3J\x1b[H");
|
|
2057
|
+
setStaticKey((k) => k + 1);
|
|
2058
|
+
setOverlay(null);
|
|
2059
|
+
}
|
|
1872
2060
|
}, onFixOne: (entry) => {
|
|
1873
2061
|
setOverlay(null);
|
|
1874
2062
|
startPixelFix(entry.errorId);
|
|
@@ -1880,57 +2068,95 @@ export function App(props) {
|
|
|
1880
2068
|
setRunAllPixel(true);
|
|
1881
2069
|
startPixelFix(first.errorId);
|
|
1882
2070
|
} })) : isSkillsView ? (_jsx(SkillsOverlay, { cwd: props.cwd, onClose: () => {
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
|
|
2071
|
+
if (props.resetUI && props.sessionStore) {
|
|
2072
|
+
props.sessionStore.overlay = null;
|
|
2073
|
+
props.resetUI();
|
|
2074
|
+
}
|
|
2075
|
+
else {
|
|
2076
|
+
stdout?.write("\x1b[2J\x1b[3J\x1b[H");
|
|
2077
|
+
setStaticKey((k) => k + 1);
|
|
2078
|
+
setOverlay(null);
|
|
2079
|
+
}
|
|
1886
2080
|
} })) : isEyesView ? (_jsx(EyesOverlay, { cwd: props.cwd, onClose: () => {
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
|
|
2081
|
+
if (props.resetUI && props.sessionStore) {
|
|
2082
|
+
props.sessionStore.overlay = null;
|
|
2083
|
+
props.resetUI();
|
|
2084
|
+
}
|
|
2085
|
+
else {
|
|
2086
|
+
stdout?.write("\x1b[2J\x1b[3J\x1b[H");
|
|
2087
|
+
setEyesCount(isEyesActive(props.cwd) ? journalCount({ status: "open" }, props.cwd) : undefined);
|
|
2088
|
+
setStaticKey((k) => k + 1);
|
|
2089
|
+
setOverlay(null);
|
|
2090
|
+
}
|
|
1891
2091
|
}, onQueueMessage: (msg) => {
|
|
1892
2092
|
agentLoop.queueMessage(msg);
|
|
1893
2093
|
} })) : isPlanView ? (_jsx(PlanOverlay, { cwd: props.cwd, autoExpandNewest: planAutoExpand, onClose: () => {
|
|
1894
2094
|
planOverlayPendingRef.current = false;
|
|
1895
|
-
|
|
1896
|
-
|
|
1897
|
-
|
|
1898
|
-
|
|
2095
|
+
if (props.resetUI && props.sessionStore) {
|
|
2096
|
+
props.sessionStore.overlay = null;
|
|
2097
|
+
props.sessionStore.planAutoExpand = false;
|
|
2098
|
+
props.resetUI();
|
|
2099
|
+
}
|
|
2100
|
+
else {
|
|
2101
|
+
stdout?.write("\x1b[2J\x1b[3J\x1b[H");
|
|
2102
|
+
setStaticKey((k) => k + 1);
|
|
2103
|
+
setPlanAutoExpand(false);
|
|
2104
|
+
setOverlay(null);
|
|
2105
|
+
}
|
|
1899
2106
|
}, onApprove: (planPath) => {
|
|
1900
2107
|
log("INFO", "plan", "Plan approved — transitioning to implementation", {
|
|
1901
2108
|
planPath,
|
|
1902
2109
|
});
|
|
1903
|
-
// Plan overlay dismissed — allow future onDone to fire normally
|
|
1904
2110
|
planOverlayPendingRef.current = false;
|
|
1905
|
-
// Store approved plan path — will be injected into the new system prompt
|
|
1906
|
-
approvedPlanPathRef.current = planPath;
|
|
1907
|
-
// Extract plan steps for progress tracking
|
|
1908
|
-
void import("node:fs/promises").then(({ readFile }) => readFile(planPath, "utf-8").then((content) => {
|
|
1909
|
-
const steps = extractPlanSteps(content);
|
|
1910
|
-
planStepsRef.current = steps;
|
|
1911
|
-
setPlanSteps(steps);
|
|
1912
|
-
}));
|
|
1913
|
-
// Clear session for a fresh context focused on the plan
|
|
1914
|
-
stdout?.write("\x1b[2J\x1b[3J\x1b[H");
|
|
1915
|
-
setHistory([{ kind: "banner", id: "banner" }]);
|
|
1916
|
-
setLiveItems([]);
|
|
1917
|
-
setStaticKey((k) => k + 1);
|
|
1918
|
-
setPlanAutoExpand(false);
|
|
1919
|
-
setOverlay(null);
|
|
1920
|
-
// Rebuild system prompt with the approved plan, then reset the session
|
|
1921
2111
|
void (async () => {
|
|
1922
2112
|
try {
|
|
2113
|
+
// Read plan steps for progress tracking — handed to the new
|
|
2114
|
+
// mount via sessionStore.planSteps below.
|
|
2115
|
+
const planContent = await import("node:fs/promises").then(({ readFile }) => readFile(planPath, "utf-8"));
|
|
2116
|
+
const steps = extractPlanSteps(planContent);
|
|
2117
|
+
// Build the new system prompt with the approved plan baked in.
|
|
1923
2118
|
const newPrompt = await buildSystemPrompt(props.cwd, props.skills, false, planPath);
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
// Create a new session file
|
|
2119
|
+
// Create a new session file BEFORE remount so the new tree
|
|
2120
|
+
// picks it up via sessionStore.sessionPath.
|
|
2121
|
+
let newSessionPath;
|
|
1928
2122
|
const sm = sessionManagerRef.current;
|
|
1929
2123
|
if (sm) {
|
|
1930
2124
|
const s = await sm.create(props.cwd, currentProvider, currentModel);
|
|
1931
|
-
|
|
2125
|
+
newSessionPath = s.path;
|
|
2126
|
+
}
|
|
2127
|
+
if (props.resetUI && props.sessionStore) {
|
|
2128
|
+
// Clear the overlay so the new mount lands on the chat,
|
|
2129
|
+
// not back inside the plan pane.
|
|
2130
|
+
props.sessionStore.overlay = null;
|
|
2131
|
+
props.sessionStore.planAutoExpand = false;
|
|
2132
|
+
props.resetUI({
|
|
2133
|
+
wipeSession: true,
|
|
2134
|
+
messages: [{ role: "system", content: newPrompt }],
|
|
2135
|
+
approvedPlanPath: planPath,
|
|
2136
|
+
planSteps: steps,
|
|
2137
|
+
sessionPath: newSessionPath,
|
|
2138
|
+
pendingAction: {
|
|
2139
|
+
prompt: "The plan has been approved. Implement it now, following each step in order.",
|
|
2140
|
+
infoText: "Plan approved — starting fresh session for implementation",
|
|
2141
|
+
},
|
|
2142
|
+
});
|
|
2143
|
+
return;
|
|
1932
2144
|
}
|
|
1933
|
-
//
|
|
2145
|
+
// Fallback path (resetUI not wired — tests). Mutate in place.
|
|
2146
|
+
approvedPlanPathRef.current = planPath;
|
|
2147
|
+
planStepsRef.current = steps;
|
|
2148
|
+
setPlanSteps(steps);
|
|
2149
|
+
stdout?.write("\x1b[2J\x1b[3J\x1b[H");
|
|
2150
|
+
setHistory([{ kind: "banner", id: "banner" }]);
|
|
2151
|
+
setLiveItems([]);
|
|
2152
|
+
setStaticKey((k) => k + 1);
|
|
2153
|
+
setPlanAutoExpand(false);
|
|
2154
|
+
setOverlay(null);
|
|
2155
|
+
messagesRef.current = [{ role: "system", content: newPrompt }];
|
|
2156
|
+
agentLoop.reset();
|
|
2157
|
+
persistedIndexRef.current = messagesRef.current.length;
|
|
2158
|
+
if (newSessionPath)
|
|
2159
|
+
sessionPathRef.current = newSessionPath;
|
|
1934
2160
|
setLiveItems([
|
|
1935
2161
|
{
|
|
1936
2162
|
kind: "info",
|
|
@@ -1949,19 +2175,31 @@ export function App(props) {
|
|
|
1949
2175
|
})();
|
|
1950
2176
|
}, onReject: (planPath, feedback) => {
|
|
1951
2177
|
planOverlayPendingRef.current = false;
|
|
2178
|
+
const rejectionMsg = `The plan at ${planPath} was rejected.\n\nFeedback: ${feedback}\n\n` +
|
|
2179
|
+
`Please revise the plan based on this feedback.`;
|
|
2180
|
+
if (props.resetUI && props.sessionStore) {
|
|
2181
|
+
props.sessionStore.overlay = null;
|
|
2182
|
+
props.sessionStore.planAutoExpand = false;
|
|
2183
|
+
// No wipeSession — keep history, messages, plan mode etc. The
|
|
2184
|
+
// agent picks up the rejection mid-conversation.
|
|
2185
|
+
props.resetUI({
|
|
2186
|
+
pendingAction: {
|
|
2187
|
+
prompt: rejectionMsg,
|
|
2188
|
+
infoText: `Plan rejected — "${feedback}"`,
|
|
2189
|
+
},
|
|
2190
|
+
});
|
|
2191
|
+
return;
|
|
2192
|
+
}
|
|
1952
2193
|
stdout?.write("\x1b[2J\x1b[3J\x1b[H");
|
|
1953
2194
|
setStaticKey((k) => k + 1);
|
|
1954
2195
|
setPlanAutoExpand(false);
|
|
1955
2196
|
setOverlay(null);
|
|
1956
2197
|
setDoneStatus(null);
|
|
1957
|
-
// Send rejection + feedback to the agent
|
|
1958
|
-
const msg = `The plan at ${planPath} was rejected.\n\nFeedback: ${feedback}\n\n` +
|
|
1959
|
-
`Please revise the plan based on this feedback.`;
|
|
1960
2198
|
setLiveItems((prev) => [
|
|
1961
2199
|
...prev,
|
|
1962
2200
|
{ kind: "info", text: `Plan rejected — "${feedback}"`, id: getId() },
|
|
1963
2201
|
]);
|
|
1964
|
-
void agentLoop.run(
|
|
2202
|
+
void agentLoop.run(rejectionMsg).catch((err) => {
|
|
1965
2203
|
const errMsg = err instanceof Error ? err.message : String(err);
|
|
1966
2204
|
log("ERROR", "error", errMsg);
|
|
1967
2205
|
setLiveItems((prev) => [...prev, { kind: "error", message: errMsg, id: getId() }]);
|
|
@@ -1970,14 +2208,32 @@ export function App(props) {
|
|
|
1970
2208
|
? THINKING_BORDER_COLORS[thinkingBorderFrame]
|
|
1971
2209
|
: "transparent", paddingLeft: 1, paddingRight: 1, width: columns, children: _jsx(ActivityIndicator, { phase: agentLoop.activityPhase, elapsedMs: agentLoop.elapsedMs, runStartRef: agentLoop.runStartRef, thinkingMs: agentLoop.thinkingMs, isThinking: agentLoop.isThinking, tokenEstimate: agentLoop.streamedTokenEstimate, charCountRef: agentLoop.charCountRef, realTokensAccumRef: agentLoop.realTokensAccumRef, userMessage: lastUserMessage, activeToolNames: agentLoop.activeToolCalls.map((tc) => tc.name), planMode: planMode, retryInfo: agentLoop.retryInfo, planDone: planSteps.filter((s) => s.completed).length, planTotal: planSteps.length }) })) : agentLoop.stallError ? (_jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(Text, { color: theme.warning, children: "⚠ API provider stream interrupted — retries exhausted." }), _jsx(Text, { color: theme.textDim, children: " Your conversation is preserved. Send a message to continue." })] })) : (doneStatus &&
|
|
1972
2210
|
!agentLoop.isRunning && (_jsx(Box, { marginTop: 1, children: _jsxs(Text, { color: theme.success, children: ["✻ ", doneStatus.verb, " ", formatDuration(doneStatus.durationMs)] }) }))), agentLoop.queuedCount > 0 && (_jsx(Box, { marginTop: 1, children: _jsxs(Text, { color: theme.accent, children: ["⏳ ", agentLoop.queuedCount, " message", agentLoop.queuedCount > 1 ? "s" : "", " queued"] }) })), _jsx(InputArea, { onSubmit: handleSubmit, onAbort: handleAbort, disabled: agentLoop.isRunning, isActive: !taskBarFocused && !overlay, onDownAtEnd: handleFocusTaskBar, onShiftTab: handleToggleThinking, onToggleTasks: () => {
|
|
1973
|
-
|
|
1974
|
-
|
|
2211
|
+
if (props.resetUI && props.sessionStore) {
|
|
2212
|
+
props.sessionStore.overlay = "tasks";
|
|
2213
|
+
props.resetUI();
|
|
2214
|
+
}
|
|
2215
|
+
else {
|
|
2216
|
+
stdout?.write("\x1b[2J\x1b[3J\x1b[H");
|
|
2217
|
+
setOverlay("tasks");
|
|
2218
|
+
}
|
|
1975
2219
|
}, onToggleSkills: () => {
|
|
1976
|
-
|
|
1977
|
-
|
|
2220
|
+
if (props.resetUI && props.sessionStore) {
|
|
2221
|
+
props.sessionStore.overlay = "skills";
|
|
2222
|
+
props.resetUI();
|
|
2223
|
+
}
|
|
2224
|
+
else {
|
|
2225
|
+
stdout?.write("\x1b[2J\x1b[3J\x1b[H");
|
|
2226
|
+
setOverlay("skills");
|
|
2227
|
+
}
|
|
1978
2228
|
}, onTogglePixel: () => {
|
|
1979
|
-
|
|
1980
|
-
|
|
2229
|
+
if (props.resetUI && props.sessionStore) {
|
|
2230
|
+
props.sessionStore.overlay = "pixel";
|
|
2231
|
+
props.resetUI();
|
|
2232
|
+
}
|
|
2233
|
+
else {
|
|
2234
|
+
stdout?.write("\x1b[2J\x1b[3J\x1b[H");
|
|
2235
|
+
setOverlay("pixel");
|
|
2236
|
+
}
|
|
1981
2237
|
}, onTogglePlanMode: () => {
|
|
1982
2238
|
const next = !planMode;
|
|
1983
2239
|
setPlanMode(next);
|