@kenkaiiii/ggcoder 4.3.206 → 4.3.208
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/cli.js +10 -7
- package/dist/cli.js.map +1 -1
- package/dist/core/goal-controller.js +13 -13
- package/dist/core/goal-controller.js.map +1 -1
- package/dist/core/goal-controller.test.js +54 -12
- package/dist/core/goal-controller.test.js.map +1 -1
- package/dist/core/goal-worker-dev-server-lifecycle.test.d.ts +2 -0
- package/dist/core/goal-worker-dev-server-lifecycle.test.d.ts.map +1 -0
- package/dist/core/goal-worker-dev-server-lifecycle.test.js +68 -0
- package/dist/core/goal-worker-dev-server-lifecycle.test.js.map +1 -0
- package/dist/core/goal-worker.d.ts +7 -3
- package/dist/core/goal-worker.d.ts.map +1 -1
- package/dist/core/goal-worker.js +15 -5
- package/dist/core/goal-worker.js.map +1 -1
- package/dist/core/goal-worker.test.js +19 -1
- package/dist/core/goal-worker.test.js.map +1 -1
- package/dist/core/model-registry.test.js +51 -1
- package/dist/core/model-registry.test.js.map +1 -1
- package/dist/core/process-manager-dev-server-repro.test.d.ts +2 -0
- package/dist/core/process-manager-dev-server-repro.test.d.ts.map +1 -0
- package/dist/core/process-manager-dev-server-repro.test.js +100 -0
- package/dist/core/process-manager-dev-server-repro.test.js.map +1 -0
- package/dist/core/process-manager.js +2 -2
- package/dist/core/process-manager.js.map +1 -1
- package/dist/core/prompt-commands.d.ts.map +1 -1
- package/dist/core/prompt-commands.js +2 -1
- package/dist/core/prompt-commands.js.map +1 -1
- package/dist/core/prompt-commands.test.js +2 -0
- package/dist/core/prompt-commands.test.js.map +1 -1
- package/dist/core/repomap.js +8 -1
- package/dist/core/repomap.js.map +1 -1
- package/dist/core/repomap.test.js +32 -0
- package/dist/core/repomap.test.js.map +1 -1
- package/dist/system-prompt.d.ts.map +1 -1
- package/dist/system-prompt.js +1 -0
- package/dist/system-prompt.js.map +1 -1
- package/dist/tools/edit.d.ts.map +1 -1
- package/dist/tools/edit.js +22 -12
- package/dist/tools/edit.js.map +1 -1
- package/dist/tools/edit.test.js +29 -6
- package/dist/tools/edit.test.js.map +1 -1
- package/dist/ui/App.d.ts +39 -4
- package/dist/ui/App.d.ts.map +1 -1
- package/dist/ui/App.js +216 -147
- package/dist/ui/App.js.map +1 -1
- package/dist/ui/app-state-persistence.test.js +80 -6
- package/dist/ui/app-state-persistence.test.js.map +1 -1
- package/dist/ui/components/GoalOverlay.d.ts +54 -1
- package/dist/ui/components/GoalOverlay.d.ts.map +1 -1
- package/dist/ui/components/GoalOverlay.js +392 -53
- package/dist/ui/components/GoalOverlay.js.map +1 -1
- package/dist/ui/components/InputArea.d.ts.map +1 -1
- package/dist/ui/components/InputArea.js +38 -1
- package/dist/ui/components/InputArea.js.map +1 -1
- package/dist/ui/components/InputArea.test.d.ts +2 -0
- package/dist/ui/components/InputArea.test.d.ts.map +1 -0
- package/dist/ui/components/InputArea.test.js +79 -0
- package/dist/ui/components/InputArea.test.js.map +1 -0
- package/dist/ui/components/ToolExecution.d.ts.map +1 -1
- package/dist/ui/components/ToolExecution.js +2 -2
- package/dist/ui/components/ToolExecution.js.map +1 -1
- package/dist/ui/goal-events.d.ts +88 -1
- package/dist/ui/goal-events.d.ts.map +1 -1
- package/dist/ui/goal-events.js +249 -28
- package/dist/ui/goal-events.js.map +1 -1
- package/dist/ui/goal-events.test.js +89 -4
- package/dist/ui/goal-events.test.js.map +1 -1
- package/dist/ui/goal-overlay.test.js +155 -1
- package/dist/ui/goal-overlay.test.js.map +1 -1
- package/dist/ui/render.d.ts +3 -1
- package/dist/ui/render.d.ts.map +1 -1
- package/dist/ui/render.js +2 -0
- package/dist/ui/render.js.map +1 -1
- package/dist/ui/scroll-stabilization.test.js +49 -2
- package/dist/ui/scroll-stabilization.test.js.map +1 -1
- package/dist/ui/slash-command-images.test.d.ts +2 -0
- package/dist/ui/slash-command-images.test.d.ts.map +1 -0
- package/dist/ui/slash-command-images.test.js +47 -0
- package/dist/ui/slash-command-images.test.js.map +1 -0
- package/package.json +5 -5
package/dist/ui/App.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
2
|
import React, { useState, useRef, useCallback, useEffect, useMemo } from "react";
|
|
3
|
-
import { Box, Text, Static
|
|
3
|
+
import { Box, Text, Static } from "ink";
|
|
4
4
|
import { useTerminalSize } from "./hooks/useTerminalSize.js";
|
|
5
5
|
import { useDoublePress } from "./hooks/useDoublePress.js";
|
|
6
6
|
import { useTaskBarStore, useTaskBarPolling, focusTaskBar, exitTaskBar, expandTaskBar, collapseTaskBar, navigateTaskBar, killTask, } from "./stores/taskbar-store.js";
|
|
@@ -96,6 +96,64 @@ function toErrorItem(err, id, contextPrefix) {
|
|
|
96
96
|
id,
|
|
97
97
|
};
|
|
98
98
|
}
|
|
99
|
+
export function routePromptCommandInput(input, promptCommands = PROMPT_COMMANDS, customCommands = []) {
|
|
100
|
+
const trimmed = input.trim();
|
|
101
|
+
if (!trimmed.startsWith("/"))
|
|
102
|
+
return null;
|
|
103
|
+
const parts = trimmed.slice(1).split(" ");
|
|
104
|
+
const cmdName = parts[0];
|
|
105
|
+
const cmdArgs = parts.slice(1).join(" ").trim();
|
|
106
|
+
const builtinCmd = promptCommands.find((c) => c.name === cmdName || c.aliases.includes(cmdName));
|
|
107
|
+
const customCmd = !builtinCmd ? customCommands.find((c) => c.name === cmdName) : undefined;
|
|
108
|
+
const promptText = builtinCmd?.prompt ?? customCmd?.prompt;
|
|
109
|
+
if (!promptText)
|
|
110
|
+
return null;
|
|
111
|
+
return {
|
|
112
|
+
cmdName,
|
|
113
|
+
cmdArgs,
|
|
114
|
+
promptText,
|
|
115
|
+
fullPrompt: cmdArgs ? `${promptText}\n\n## User Instructions\n\n${cmdArgs}` : promptText,
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
export function buildUserContentWithAttachments(text, inputImages, modelSupportsImages) {
|
|
119
|
+
if (inputImages.length === 0)
|
|
120
|
+
return text;
|
|
121
|
+
const parts = [];
|
|
122
|
+
if (text) {
|
|
123
|
+
parts.push({ type: "text", text });
|
|
124
|
+
}
|
|
125
|
+
for (const img of inputImages) {
|
|
126
|
+
if (img.kind === "text") {
|
|
127
|
+
parts.push({
|
|
128
|
+
type: "text",
|
|
129
|
+
text: `<file name="${img.fileName}">\n${img.data}\n</file>`,
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
else if (modelSupportsImages) {
|
|
133
|
+
parts.push({ type: "image", mediaType: img.mediaType, data: img.data });
|
|
134
|
+
}
|
|
135
|
+
else {
|
|
136
|
+
// GLM models: save image to temp file and instruct model to use vision MCP tool
|
|
137
|
+
const ext = img.mediaType.split("/")[1] ?? "png";
|
|
138
|
+
const tmpPath = `/tmp/ggcoder-img-${Date.now()}.${ext}`;
|
|
139
|
+
try {
|
|
140
|
+
writeFileSync(tmpPath, Buffer.from(img.data, "base64"));
|
|
141
|
+
parts.push({
|
|
142
|
+
type: "text",
|
|
143
|
+
text: `[User attached an image saved at: ${tmpPath} — use the image_analysis tool to view and analyze it]`,
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
catch {
|
|
147
|
+
parts.push({
|
|
148
|
+
type: "text",
|
|
149
|
+
text: `[User attached an image but it could not be saved for analysis]`,
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
// If only text parts remain after stripping images, simplify to plain string
|
|
155
|
+
return parts.length === 1 && parts[0].type === "text" ? parts[0].text : parts;
|
|
156
|
+
}
|
|
99
157
|
/** Tools that get aggregated into a single compact group when concurrent. */
|
|
100
158
|
const AGGREGATABLE_TOOLS = new Set(["read", "grep", "find", "ls"]);
|
|
101
159
|
const RUNNING_INDICATOR_ANIMATION_MS = 1_200;
|
|
@@ -135,6 +193,64 @@ function formatGoalWorkerFinishedTitle(taskTitle, status) {
|
|
|
135
193
|
? `Worker finished: ${taskTitle}. Reporting back.`
|
|
136
194
|
: `Worker failed: ${taskTitle}. Reporting back.`;
|
|
137
195
|
}
|
|
196
|
+
function countGoalTasksByStatus(tasks, status) {
|
|
197
|
+
return tasks.filter((task) => task.status === status).length;
|
|
198
|
+
}
|
|
199
|
+
function firstText(values) {
|
|
200
|
+
return values.find((value) => value !== undefined && value.trim().length > 0)?.trim();
|
|
201
|
+
}
|
|
202
|
+
function truncateGoalSummary(value, maxLength = 90) {
|
|
203
|
+
const normalized = value.replace(/\s+/g, " ").trim();
|
|
204
|
+
if (normalized.length <= maxLength)
|
|
205
|
+
return normalized;
|
|
206
|
+
return `${normalized.slice(0, maxLength - 1)}…`;
|
|
207
|
+
}
|
|
208
|
+
export function buildGoalSummaryRows(run) {
|
|
209
|
+
const rows = [];
|
|
210
|
+
const doneTasks = countGoalTasksByStatus(run.tasks, "done");
|
|
211
|
+
const failedTasks = countGoalTasksByStatus(run.tasks, "failed");
|
|
212
|
+
const blockedTasks = countGoalTasksByStatus(run.tasks, "blocked");
|
|
213
|
+
const taskSuffix = [
|
|
214
|
+
failedTasks > 0 ? `${failedTasks} failed` : undefined,
|
|
215
|
+
blockedTasks > 0 ? `${blockedTasks} blocked` : undefined,
|
|
216
|
+
].filter((item) => item !== undefined);
|
|
217
|
+
rows.push({
|
|
218
|
+
label: "Tasks",
|
|
219
|
+
value: run.tasks.length > 0 ? `${doneTasks}/${run.tasks.length} done` : "none",
|
|
220
|
+
...(taskSuffix.length > 0 ? { detail: taskSuffix.join(", ") } : {}),
|
|
221
|
+
});
|
|
222
|
+
const verifierResult = run.verifier?.lastResult;
|
|
223
|
+
const verifierDetail = firstText([verifierResult?.outputPath, run.verifier?.command]);
|
|
224
|
+
rows.push({
|
|
225
|
+
label: "Verifier",
|
|
226
|
+
value: verifierResult?.status ?? (run.verifier?.command ? "ready" : "missing"),
|
|
227
|
+
...(verifierDetail ? { detail: truncateGoalSummary(verifierDetail) } : {}),
|
|
228
|
+
});
|
|
229
|
+
const latestEvidence = run.evidence.at(-1);
|
|
230
|
+
rows.push({
|
|
231
|
+
label: "Evidence",
|
|
232
|
+
value: `${run.evidence.length} recorded`,
|
|
233
|
+
...(latestEvidence
|
|
234
|
+
? { detail: truncateGoalSummary(latestEvidence.path ?? latestEvidence.label) }
|
|
235
|
+
: {}),
|
|
236
|
+
});
|
|
237
|
+
if (run.status === "blocked" || run.status === "paused" || run.blockers.length > 0) {
|
|
238
|
+
rows.push({
|
|
239
|
+
label: run.status === "paused" ? "Paused on" : "Blocked on",
|
|
240
|
+
value: truncateGoalSummary(goalHasBlockingPrerequisites(run)
|
|
241
|
+
? formatGoalBlockingPrerequisites(run)
|
|
242
|
+
: (run.blockers[0] ?? "manual review"), 110),
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
else if (run.successCriteria.length > 0) {
|
|
246
|
+
rows.push({
|
|
247
|
+
label: "Criteria",
|
|
248
|
+
value: `${run.successCriteria.length} checked`,
|
|
249
|
+
detail: truncateGoalSummary(run.successCriteria[0] ?? "", 80),
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
return rows.slice(0, 4);
|
|
253
|
+
}
|
|
138
254
|
export function formatGoalTerminalProgress(run) {
|
|
139
255
|
switch (run.status) {
|
|
140
256
|
case "passed":
|
|
@@ -143,6 +259,7 @@ export function formatGoalTerminalProgress(run) {
|
|
|
143
259
|
phase: "terminal",
|
|
144
260
|
title: `Goal passed: ${run.title}`,
|
|
145
261
|
detail: "Verifier evidence is recorded; auto-continuation stopped.",
|
|
262
|
+
summaryRows: buildGoalSummaryRows(run),
|
|
146
263
|
status: run.status,
|
|
147
264
|
};
|
|
148
265
|
case "failed":
|
|
@@ -151,6 +268,7 @@ export function formatGoalTerminalProgress(run) {
|
|
|
151
268
|
phase: "terminal",
|
|
152
269
|
title: `Goal failed: ${run.title}`,
|
|
153
270
|
detail: "Auto-continuation stopped. Check Goal tasks for the failing step.",
|
|
271
|
+
summaryRows: buildGoalSummaryRows(run),
|
|
154
272
|
status: run.status,
|
|
155
273
|
};
|
|
156
274
|
case "blocked":
|
|
@@ -161,6 +279,7 @@ export function formatGoalTerminalProgress(run) {
|
|
|
161
279
|
detail: goalHasBlockingPrerequisites(run)
|
|
162
280
|
? formatGoalBlockingPrerequisites(run)
|
|
163
281
|
: (run.blockers[0] ?? "A prerequisite or missing verifier blocked progress."),
|
|
282
|
+
summaryRows: buildGoalSummaryRows(run),
|
|
164
283
|
status: run.status,
|
|
165
284
|
};
|
|
166
285
|
case "paused":
|
|
@@ -169,6 +288,7 @@ export function formatGoalTerminalProgress(run) {
|
|
|
169
288
|
phase: "terminal",
|
|
170
289
|
title: `Goal paused: ${run.title}`,
|
|
171
290
|
detail: run.blockers[0] ?? "Auto-continuation paused.",
|
|
291
|
+
summaryRows: buildGoalSummaryRows(run),
|
|
172
292
|
status: run.status,
|
|
173
293
|
};
|
|
174
294
|
case "draft":
|
|
@@ -178,19 +298,31 @@ export function formatGoalTerminalProgress(run) {
|
|
|
178
298
|
return null;
|
|
179
299
|
}
|
|
180
300
|
}
|
|
181
|
-
export function shouldHideHistoryForOverlayView(
|
|
182
|
-
//
|
|
183
|
-
//
|
|
184
|
-
//
|
|
185
|
-
|
|
186
|
-
|
|
301
|
+
export function shouldHideHistoryForOverlayView(_isOverlayView, _isAgentRunning) {
|
|
302
|
+
// Ink Static is append-only. Passing [] for overlay panes rewrites the Static
|
|
303
|
+
// accumulator and can destroy scrollback when the pane closes. Keep history
|
|
304
|
+
// mounted and let overlays render below it.
|
|
305
|
+
return false;
|
|
306
|
+
}
|
|
307
|
+
export function shouldStabilizeOverlayPaneRerender({ overlayPane, isAgentRunning, }) {
|
|
308
|
+
return isAgentRunning && (overlayPane === "goal" || overlayPane === "plan");
|
|
187
309
|
}
|
|
188
|
-
export function
|
|
310
|
+
export function shouldHideStaticItemsForOverlayView({ shouldHideHistoryForOverlay, stabilizeOverlayPaneRerender, }) {
|
|
311
|
+
return shouldHideHistoryForOverlay && !stabilizeOverlayPaneRerender;
|
|
312
|
+
}
|
|
313
|
+
export function getScrollStabilizationDecision({ isUserScrolled, hasNewOutput, hasTallLiveUserMessage = false, }) {
|
|
314
|
+
const shouldStabilize = isUserScrolled || hasTallLiveUserMessage;
|
|
189
315
|
return {
|
|
190
|
-
preserveStatic:
|
|
191
|
-
autoFollow: !
|
|
316
|
+
preserveStatic: shouldStabilize && hasNewOutput,
|
|
317
|
+
autoFollow: !shouldStabilize,
|
|
192
318
|
};
|
|
193
319
|
}
|
|
320
|
+
export function isTallLiveUserMessage(text, rows) {
|
|
321
|
+
return text.split("\n").length > Math.max(8, Math.floor(rows * 0.6));
|
|
322
|
+
}
|
|
323
|
+
export function getStaticHistoryKey({ resizeKey }) {
|
|
324
|
+
return `${resizeKey}`;
|
|
325
|
+
}
|
|
194
326
|
// flushOnTurnText, flushOnTurnEnd are imported from ./live-item-flush.ts
|
|
195
327
|
/** Check whether an item is still active (running spinner, pending result). */
|
|
196
328
|
function isActiveItem(item) {
|
|
@@ -352,7 +484,6 @@ function markTaskInProgress(cwd, taskId) {
|
|
|
352
484
|
export function App(props) {
|
|
353
485
|
const theme = useTheme();
|
|
354
486
|
const switchTheme = useSetTheme();
|
|
355
|
-
const { stdout } = useStdout();
|
|
356
487
|
const { columns, resizeKey } = useTerminalSize();
|
|
357
488
|
// Hoisted before terminal title hook so it can reference them
|
|
358
489
|
const [lastUserMessage, setLastUserMessage] = useState("");
|
|
@@ -414,8 +545,7 @@ export function App(props) {
|
|
|
414
545
|
const startPixelFixRef = useRef(() => { });
|
|
415
546
|
const cwdRef = useRef(props.cwd);
|
|
416
547
|
const [displayedCwd, setDisplayedCwd] = useState(props.cwd);
|
|
417
|
-
const [
|
|
418
|
-
const [doneStatus, setDoneStatus] = useState(null);
|
|
548
|
+
const [doneStatus, setDoneStatus] = useState(props.sessionStore?.doneStatus ?? null);
|
|
419
549
|
// Suppress "done" status when a plan overlay is about to open
|
|
420
550
|
const planOverlayPendingRef = useRef(false);
|
|
421
551
|
const [gitBranch, setGitBranch] = useState(null);
|
|
@@ -552,6 +682,10 @@ export function App(props) {
|
|
|
552
682
|
if (sessionStore)
|
|
553
683
|
sessionStore.liveItems = liveItems;
|
|
554
684
|
}, [liveItems, sessionStore]);
|
|
685
|
+
useEffect(() => {
|
|
686
|
+
if (sessionStore)
|
|
687
|
+
sessionStore.doneStatus = doneStatus;
|
|
688
|
+
}, [doneStatus, sessionStore]);
|
|
555
689
|
useEffect(() => {
|
|
556
690
|
if (sessionStore)
|
|
557
691
|
sessionStore.planSteps = planSteps;
|
|
@@ -769,14 +903,6 @@ export function App(props) {
|
|
|
769
903
|
// premature "done" status that fires when the agent loop finishes
|
|
770
904
|
planOverlayPendingRef.current = true;
|
|
771
905
|
setTimeout(() => {
|
|
772
|
-
// NOTE: this is the one open-overlay path that does NOT remount via
|
|
773
|
-
// resetUI. It runs while the agent is still mid-turn (after the
|
|
774
|
-
// exit_plan tool returned but before onDone fires), and unmounting
|
|
775
|
-
// here would kill the in-flight agent stream. Keep the bare ANSI
|
|
776
|
-
// clear; the drift bug is tolerable across just the agent's
|
|
777
|
-
// wrap-up turn, and onApprove/onReject both remount cleanly via
|
|
778
|
-
// resetUI when the user resolves the plan.
|
|
779
|
-
stdout?.write("\x1b[2J\x1b[3J\x1b[H");
|
|
780
906
|
setPlanAutoExpand(true);
|
|
781
907
|
setOverlay("plan");
|
|
782
908
|
// Don't clear planOverlayPendingRef here — keep it true until
|
|
@@ -791,7 +917,7 @@ export function App(props) {
|
|
|
791
917
|
planPath);
|
|
792
918
|
};
|
|
793
919
|
}
|
|
794
|
-
}, [props.onExitPlanRef, replaceSystemPrompt
|
|
920
|
+
}, [props.onExitPlanRef, replaceSystemPrompt]);
|
|
795
921
|
const appendMessagesToSession = useCallback(async (sessionPath, messages, startIndex) => {
|
|
796
922
|
const sm = sessionManagerRef.current;
|
|
797
923
|
if (!sm)
|
|
@@ -1908,14 +2034,10 @@ export function App(props) {
|
|
|
1908
2034
|
process.exit(0);
|
|
1909
2035
|
}
|
|
1910
2036
|
// Handle /clear — tear down the entire Ink instance and rebuild fresh.
|
|
1911
|
-
//
|
|
1912
|
-
//
|
|
1913
|
-
//
|
|
1914
|
-
//
|
|
1915
|
-
// terminal-state assumptions that ANSI clearing breaks. The reliable
|
|
1916
|
-
// fix is unmount + render again. Runtime state (model, provider,
|
|
1917
|
-
// thinking) survives via renderApp's closure-held `runtimeState`,
|
|
1918
|
-
// mirrored from React state via the useEffects above.
|
|
2037
|
+
// Avoid direct ANSI terminal clears here; they can erase scrollback.
|
|
2038
|
+
// Runtime state (model, provider, thinking) survives via renderApp's
|
|
2039
|
+
// closure-held `runtimeState`, mirrored from React state via the
|
|
2040
|
+
// useEffects above.
|
|
1919
2041
|
if (trimmed === "/clear") {
|
|
1920
2042
|
if (props.resetUI) {
|
|
1921
2043
|
void (async () => {
|
|
@@ -1928,8 +2050,7 @@ export function App(props) {
|
|
|
1928
2050
|
return;
|
|
1929
2051
|
}
|
|
1930
2052
|
// Fallback path (resetUI not wired — e.g. tests). Best-effort: clear
|
|
1931
|
-
// React state in place
|
|
1932
|
-
stdout?.write("\x1b[2J\x1b[3J\x1b[H");
|
|
2053
|
+
// React state in place without touching terminal scrollback.
|
|
1933
2054
|
pendingFlushRef.current = [];
|
|
1934
2055
|
setHistory([{ kind: "banner", id: "banner" }]);
|
|
1935
2056
|
setLiveItems([]);
|
|
@@ -1945,7 +2066,6 @@ export function App(props) {
|
|
|
1945
2066
|
agentLoop.reset();
|
|
1946
2067
|
setSessionTitle(undefined);
|
|
1947
2068
|
sessionTitleGeneratedRef.current = false;
|
|
1948
|
-
setStaticKey((k) => k + 1);
|
|
1949
2069
|
setLiveItems([{ kind: "info", text: "Session cleared.", id: getId() }]);
|
|
1950
2070
|
return;
|
|
1951
2071
|
}
|
|
@@ -2053,8 +2173,6 @@ export function App(props) {
|
|
|
2053
2173
|
props.resetUI();
|
|
2054
2174
|
}
|
|
2055
2175
|
else {
|
|
2056
|
-
stdout?.write("\x1b[2J\x1b[3J\x1b[H");
|
|
2057
|
-
setStaticKey((key) => key + 1);
|
|
2058
2176
|
if (props.sessionStore) {
|
|
2059
2177
|
props.sessionStore.overlay = "goal";
|
|
2060
2178
|
props.sessionStore.planAutoExpand = false;
|
|
@@ -2086,49 +2204,49 @@ export function App(props) {
|
|
|
2086
2204
|
return;
|
|
2087
2205
|
}
|
|
2088
2206
|
// Handle prompt-template commands (built-in + custom from .gg/commands/)
|
|
2089
|
-
|
|
2090
|
-
|
|
2091
|
-
const cmdName =
|
|
2092
|
-
|
|
2093
|
-
|
|
2094
|
-
|
|
2095
|
-
|
|
2096
|
-
|
|
2097
|
-
log("INFO", "command", `Prompt command: /${cmdName}${cmdArgs ? ` (args: ${cmdArgs})` : ""}`);
|
|
2098
|
-
// Move live items into history before starting
|
|
2099
|
-
setLiveItems((prev) => {
|
|
2100
|
-
if (prev.length > 0) {
|
|
2101
|
-
pendingFlushRef.current = [...pendingFlushRef.current, ...prev];
|
|
2102
|
-
}
|
|
2103
|
-
return [];
|
|
2104
|
-
});
|
|
2105
|
-
// Show the command name as the user message
|
|
2106
|
-
const userItem = { kind: "user", text: trimmed, id: getId() };
|
|
2107
|
-
setLastUserMessage(trimmed);
|
|
2108
|
-
setDoneStatus(null);
|
|
2109
|
-
setLiveItems([userItem]);
|
|
2110
|
-
// Send the full prompt to the agent, with user args appended if provided
|
|
2111
|
-
const fullPrompt = cmdArgs
|
|
2112
|
-
? `${promptText}\n\n## User Instructions\n\n${cmdArgs}`
|
|
2113
|
-
: promptText;
|
|
2114
|
-
try {
|
|
2115
|
-
await agentLoop.run(fullPrompt);
|
|
2116
|
-
}
|
|
2117
|
-
catch (err) {
|
|
2118
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
2119
|
-
log("ERROR", "error", msg);
|
|
2120
|
-
const isAbort = msg.includes("aborted") || msg.includes("abort");
|
|
2121
|
-
setLiveItems((prev) => [
|
|
2122
|
-
...prev,
|
|
2123
|
-
isAbort
|
|
2124
|
-
? { kind: "stopped", text: "Request was stopped.", id: getId() }
|
|
2125
|
-
: toErrorItem(err, getId()),
|
|
2126
|
-
]);
|
|
2207
|
+
const promptCommandRoute = routePromptCommandInput(trimmed, PROMPT_COMMANDS, customCommands);
|
|
2208
|
+
if (promptCommandRoute) {
|
|
2209
|
+
const { cmdName, cmdArgs, fullPrompt } = promptCommandRoute;
|
|
2210
|
+
log("INFO", "command", `Prompt command: /${cmdName}${cmdArgs ? ` (args: ${cmdArgs})` : ""}`);
|
|
2211
|
+
// Move live items into history before starting
|
|
2212
|
+
setLiveItems((prev) => {
|
|
2213
|
+
if (prev.length > 0) {
|
|
2214
|
+
pendingFlushRef.current = [...pendingFlushRef.current, ...prev];
|
|
2127
2215
|
}
|
|
2128
|
-
|
|
2129
|
-
|
|
2130
|
-
|
|
2216
|
+
return [];
|
|
2217
|
+
});
|
|
2218
|
+
const hasImages = inputImages.length > 0;
|
|
2219
|
+
const modelInfo = getModel(currentModel);
|
|
2220
|
+
const modelSupportsImages = modelInfo?.supportsImages ?? true;
|
|
2221
|
+
const userContent = buildUserContentWithAttachments(fullPrompt, inputImages, modelSupportsImages);
|
|
2222
|
+
// Show the typed command as the user message
|
|
2223
|
+
const userItem = {
|
|
2224
|
+
kind: "user",
|
|
2225
|
+
text: trimmed,
|
|
2226
|
+
imageCount: hasImages ? inputImages.length : undefined,
|
|
2227
|
+
id: getId(),
|
|
2228
|
+
};
|
|
2229
|
+
setLastUserMessage(trimmed);
|
|
2230
|
+
setDoneStatus(null);
|
|
2231
|
+
setLiveItems([userItem]);
|
|
2232
|
+
// Send the full prompt to the agent, with user args appended if provided
|
|
2233
|
+
try {
|
|
2234
|
+
await agentLoop.run(userContent);
|
|
2235
|
+
}
|
|
2236
|
+
catch (err) {
|
|
2237
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
2238
|
+
log("ERROR", "error", msg);
|
|
2239
|
+
const isAbort = msg.includes("aborted") || msg.includes("abort");
|
|
2240
|
+
setLiveItems((prev) => [
|
|
2241
|
+
...prev,
|
|
2242
|
+
isAbort
|
|
2243
|
+
? { kind: "stopped", text: "Request was stopped.", id: getId() }
|
|
2244
|
+
: toErrorItem(err, getId()),
|
|
2245
|
+
]);
|
|
2131
2246
|
}
|
|
2247
|
+
// Reload custom commands in case a setup command created new ones
|
|
2248
|
+
reloadCustomCommands();
|
|
2249
|
+
return;
|
|
2132
2250
|
}
|
|
2133
2251
|
// Check slash commands
|
|
2134
2252
|
if (props.onSlashCommand && input.startsWith("/")) {
|
|
@@ -2142,47 +2260,7 @@ export function App(props) {
|
|
|
2142
2260
|
const hasImages = inputImages.length > 0;
|
|
2143
2261
|
const modelInfo = getModel(currentModel);
|
|
2144
2262
|
const modelSupportsImages = modelInfo?.supportsImages ?? true;
|
|
2145
|
-
|
|
2146
|
-
if (hasImages) {
|
|
2147
|
-
const parts = [];
|
|
2148
|
-
if (trimmed) {
|
|
2149
|
-
parts.push({ type: "text", text: trimmed });
|
|
2150
|
-
}
|
|
2151
|
-
for (const img of inputImages) {
|
|
2152
|
-
if (img.kind === "text") {
|
|
2153
|
-
parts.push({
|
|
2154
|
-
type: "text",
|
|
2155
|
-
text: `<file name="${img.fileName}">\n${img.data}\n</file>`,
|
|
2156
|
-
});
|
|
2157
|
-
}
|
|
2158
|
-
else if (modelSupportsImages) {
|
|
2159
|
-
parts.push({ type: "image", mediaType: img.mediaType, data: img.data });
|
|
2160
|
-
}
|
|
2161
|
-
else {
|
|
2162
|
-
// GLM models: save image to temp file and instruct model to use vision MCP tool
|
|
2163
|
-
const ext = img.mediaType.split("/")[1] ?? "png";
|
|
2164
|
-
const tmpPath = `/tmp/ggcoder-img-${Date.now()}.${ext}`;
|
|
2165
|
-
try {
|
|
2166
|
-
writeFileSync(tmpPath, Buffer.from(img.data, "base64"));
|
|
2167
|
-
parts.push({
|
|
2168
|
-
type: "text",
|
|
2169
|
-
text: `[User attached an image saved at: ${tmpPath} — use the image_analysis tool to view and analyze it]`,
|
|
2170
|
-
});
|
|
2171
|
-
}
|
|
2172
|
-
catch {
|
|
2173
|
-
parts.push({
|
|
2174
|
-
type: "text",
|
|
2175
|
-
text: `[User attached an image but it could not be saved for analysis]`,
|
|
2176
|
-
});
|
|
2177
|
-
}
|
|
2178
|
-
}
|
|
2179
|
-
}
|
|
2180
|
-
// If only text parts remain after stripping images, simplify to plain string
|
|
2181
|
-
userContent = parts.length === 1 && parts[0].type === "text" ? parts[0].text : parts;
|
|
2182
|
-
}
|
|
2183
|
-
else {
|
|
2184
|
-
userContent = input;
|
|
2185
|
-
}
|
|
2263
|
+
const userContent = buildUserContentWithAttachments(input, inputImages, modelSupportsImages);
|
|
2186
2264
|
// ── Queue message if agent is already running ──
|
|
2187
2265
|
if (agentLoop.isRunning) {
|
|
2188
2266
|
log("INFO", "queue", `Queued message: ${trimmed.length > 80 ? trimmed.slice(0, 80) + "..." : trimmed}`);
|
|
@@ -2464,7 +2542,7 @@ export function App(props) {
|
|
|
2464
2542
|
? "◆ "
|
|
2465
2543
|
: "! "
|
|
2466
2544
|
: "↻ ";
|
|
2467
|
-
return (_jsxs(Box, { marginTop: 1, flexDirection: "column", flexShrink: 1, children: [_jsxs(Text, { wrap: "wrap", children: [_jsx(Text, { color: color, bold: true, children: glyph }), _jsx(Text, { color: color, bold: true, children: item.title }), item.workerId ? _jsxs(Text, { color: theme.textDim, children: [" \u00B7 worker ", item.workerId] }) : null] }), item.detail ? (_jsx(Text, { color: theme.textDim, wrap: "wrap", children: ` ${item.detail}` })) : null] }, item.id));
|
|
2545
|
+
return (_jsxs(Box, { marginTop: 1, flexDirection: "column", flexShrink: 1, children: [_jsxs(Text, { wrap: "wrap", children: [_jsx(Text, { color: color, bold: true, children: glyph }), _jsx(Text, { color: color, bold: true, children: item.title }), item.workerId ? _jsxs(Text, { color: theme.textDim, children: [" \u00B7 worker ", item.workerId] }) : null] }), item.detail ? (_jsx(Text, { color: theme.textDim, wrap: "wrap", children: ` ${item.detail}` })) : null, item.summaryRows && item.summaryRows.length > 0 ? (_jsx(Box, { flexDirection: "column", marginTop: 1, marginLeft: 2, flexShrink: 1, children: item.summaryRows.map((row) => (_jsxs(Text, { wrap: "truncate", children: [_jsx(Text, { color: theme.textDim, children: row.label.padEnd(10) }), _jsx(Text, { color: theme.text, children: row.value }), row.detail ? _jsxs(Text, { color: theme.textDim, children: [" \u00B7 ", row.detail] }) : null] }, row.label))) })) : null] }, item.id));
|
|
2468
2546
|
}
|
|
2469
2547
|
case "style_pack": {
|
|
2470
2548
|
const names = item.added.map((id) => LANGUAGE_DISPLAY_NAMES[id]);
|
|
@@ -2578,7 +2656,6 @@ export function App(props) {
|
|
|
2578
2656
|
return;
|
|
2579
2657
|
}
|
|
2580
2658
|
// Fallback path (resetUI not wired — tests).
|
|
2581
|
-
stdout?.write("\x1b[2J\x1b[3J\x1b[H");
|
|
2582
2659
|
setHistory([{ kind: "banner", id: "banner" }]);
|
|
2583
2660
|
setLiveItems([]);
|
|
2584
2661
|
messagesRef.current = messagesRef.current.slice(0, 1);
|
|
@@ -2612,15 +2689,7 @@ export function App(props) {
|
|
|
2612
2689
|
setRunAllTasks(false);
|
|
2613
2690
|
}
|
|
2614
2691
|
})();
|
|
2615
|
-
}, [
|
|
2616
|
-
props.cwd,
|
|
2617
|
-
props.resetUI,
|
|
2618
|
-
props.sessionStore,
|
|
2619
|
-
stdout,
|
|
2620
|
-
agentLoop,
|
|
2621
|
-
currentProvider,
|
|
2622
|
-
currentModel,
|
|
2623
|
-
]);
|
|
2692
|
+
}, [props.cwd, props.resetUI, props.sessionStore, agentLoop, currentProvider, currentModel]);
|
|
2624
2693
|
const openOverlay = useCallback((kind) => {
|
|
2625
2694
|
if (props.resetUI && props.sessionStore && !agentLoop.isRunning) {
|
|
2626
2695
|
props.sessionStore.overlay = kind;
|
|
@@ -2633,14 +2702,15 @@ export function App(props) {
|
|
|
2633
2702
|
props.sessionStore.overlay = kind;
|
|
2634
2703
|
if (kind !== "plan")
|
|
2635
2704
|
props.sessionStore.planAutoExpand = false;
|
|
2636
|
-
if (agentLoop.isRunning)
|
|
2705
|
+
if (agentLoop.isRunning && kind !== "goal" && kind !== "plan") {
|
|
2637
2706
|
props.sessionStore.pendingResetUI = true;
|
|
2707
|
+
}
|
|
2638
2708
|
}
|
|
2639
2709
|
if (kind !== "plan")
|
|
2640
2710
|
setPlanAutoExpand(false);
|
|
2641
2711
|
setOverlay(kind);
|
|
2642
2712
|
}
|
|
2643
|
-
}, [agentLoop.isRunning, props
|
|
2713
|
+
}, [agentLoop.isRunning, props]);
|
|
2644
2714
|
const closeOverlay = useCallback(() => {
|
|
2645
2715
|
if (props.resetUI && props.sessionStore && !agentLoop.isRunning) {
|
|
2646
2716
|
props.sessionStore.overlay = null;
|
|
@@ -2649,12 +2719,10 @@ export function App(props) {
|
|
|
2649
2719
|
else {
|
|
2650
2720
|
if (props.sessionStore) {
|
|
2651
2721
|
props.sessionStore.overlay = null;
|
|
2652
|
-
if (agentLoop.isRunning)
|
|
2653
|
-
props.sessionStore.pendingResetUI = true;
|
|
2654
2722
|
}
|
|
2655
2723
|
setOverlay(null);
|
|
2656
2724
|
}
|
|
2657
|
-
}, [agentLoop.isRunning, overlay, props
|
|
2725
|
+
}, [agentLoop.isRunning, overlay, props]);
|
|
2658
2726
|
const runGoalSyntheticEvent = useCallback((eventText) => {
|
|
2659
2727
|
const eventInfo = parseGoalSyntheticEvent(eventText);
|
|
2660
2728
|
const detail = eventInfo?.kind === "worker"
|
|
@@ -2954,6 +3022,7 @@ export function App(props) {
|
|
|
2954
3022
|
model: currentModel,
|
|
2955
3023
|
goalRunId: run.id,
|
|
2956
3024
|
goalTaskId: decision.task.id,
|
|
3025
|
+
taskTitle: decision.task.title,
|
|
2957
3026
|
prompt: decision.task.prompt,
|
|
2958
3027
|
});
|
|
2959
3028
|
await upsertGoalRun(props.cwd, {
|
|
@@ -3267,14 +3336,10 @@ export function App(props) {
|
|
|
3267
3336
|
activeLanguages: detectedForPixelFix,
|
|
3268
3337
|
tools: toolsForPixelFix,
|
|
3269
3338
|
});
|
|
3270
|
-
// Now that the cwd swap is committed, reset chat.
|
|
3271
|
-
//
|
|
3272
|
-
// staticKey would print a second banner with the new cwd — leaving
|
|
3273
|
-
// two banners stacked in the scrollback.
|
|
3274
|
-
stdout?.write("\x1b[2J\x1b[3J\x1b[H");
|
|
3339
|
+
// Now that the cwd swap is committed, reset chat. Do not clear the
|
|
3340
|
+
// terminal here; terminal clear sequences can erase saved scrollback.
|
|
3275
3341
|
setHistory([{ kind: "banner", id: "banner" }]);
|
|
3276
3342
|
setLiveItems([]);
|
|
3277
|
-
setStaticKey((k) => k + 1);
|
|
3278
3343
|
messagesRef.current = messagesRef.current.slice(0, 1);
|
|
3279
3344
|
agentLoop.reset();
|
|
3280
3345
|
persistedIndexRef.current = messagesRef.current.length;
|
|
@@ -3306,7 +3371,7 @@ export function App(props) {
|
|
|
3306
3371
|
setLiveItems((prev) => [...prev, toErrorItem(err, getId())]);
|
|
3307
3372
|
}
|
|
3308
3373
|
})();
|
|
3309
|
-
}, [props.cwd,
|
|
3374
|
+
}, [props.cwd, agentLoop, currentProvider, currentModel]);
|
|
3310
3375
|
startPixelFixRef.current = startPixelFix;
|
|
3311
3376
|
// Seed from sessionStore so "Fix All" chaining survives a deferred
|
|
3312
3377
|
// resetUI() if it fires between pixel fixes (e.g. user toggled a pane).
|
|
@@ -3330,7 +3395,17 @@ export function App(props) {
|
|
|
3330
3395
|
const isPixelView = overlay === "pixel";
|
|
3331
3396
|
const isOverlayView = isTaskView || isGoalView || isSkillsView || isPlanView || isEyesView || isPixelView;
|
|
3332
3397
|
const shouldHideHistoryForOverlay = shouldHideHistoryForOverlayView(isOverlayView, agentLoop.isRunning);
|
|
3333
|
-
|
|
3398
|
+
const stabilizeOverlayPaneRerender = shouldStabilizeOverlayPaneRerender({
|
|
3399
|
+
overlayPane: overlay,
|
|
3400
|
+
isAgentRunning: agentLoop.isRunning,
|
|
3401
|
+
});
|
|
3402
|
+
const staticItems = shouldHideStaticItemsForOverlayView({
|
|
3403
|
+
shouldHideHistoryForOverlay,
|
|
3404
|
+
stabilizeOverlayPaneRerender,
|
|
3405
|
+
})
|
|
3406
|
+
? []
|
|
3407
|
+
: history;
|
|
3408
|
+
return (_jsxs(Box, { flexDirection: "column", width: columns, children: [_jsx(Static, { items: staticItems, style: { width: "100%" }, children: (item) => (_jsx(Box, { flexDirection: "column", paddingRight: 1, children: renderItem(item) }, item.id)) }, getStaticHistoryKey({ resizeKey })), isTaskView ? (_jsx(TaskOverlay, { cwd: props.cwd, agentRunning: agentLoop.isRunning, onClose: () => {
|
|
3334
3409
|
if (props.resetUI && props.sessionStore && !agentLoop.isRunning) {
|
|
3335
3410
|
props.sessionStore.overlay = null;
|
|
3336
3411
|
props.resetUI();
|
|
@@ -3480,10 +3555,8 @@ export function App(props) {
|
|
|
3480
3555
|
approvedPlanPathRef.current = planPath;
|
|
3481
3556
|
planStepsRef.current = steps;
|
|
3482
3557
|
setPlanSteps(steps);
|
|
3483
|
-
stdout?.write("\x1b[2J\x1b[3J\x1b[H");
|
|
3484
3558
|
setHistory([{ kind: "banner", id: "banner" }]);
|
|
3485
3559
|
setLiveItems([]);
|
|
3486
|
-
setStaticKey((k) => k + 1);
|
|
3487
3560
|
setPlanAutoExpand(false);
|
|
3488
3561
|
setOverlay(null);
|
|
3489
3562
|
messagesRef.current = [{ role: "system", content: newPrompt }];
|
|
@@ -3524,8 +3597,6 @@ export function App(props) {
|
|
|
3524
3597
|
});
|
|
3525
3598
|
return;
|
|
3526
3599
|
}
|
|
3527
|
-
stdout?.write("\x1b[2J\x1b[3J\x1b[H");
|
|
3528
|
-
setStaticKey((k) => k + 1);
|
|
3529
3600
|
setPlanAutoExpand(false);
|
|
3530
3601
|
setOverlay(null);
|
|
3531
3602
|
setDoneStatus(null);
|
|
@@ -3540,8 +3611,6 @@ export function App(props) {
|
|
|
3540
3611
|
});
|
|
3541
3612
|
} })) : (_jsxs(_Fragment, { children: [_jsxs(Box, { flexDirection: "column", flexGrow: 1, paddingRight: 1, children: [liveItems.map((item) => renderItem(item)), _jsx(StreamingArea, { isRunning: agentLoop.isRunning, streamingText: agentLoop.streamingText, streamingThinking: agentLoop.streamingThinking, thinkingMs: agentLoop.thinkingMs, planMode: planMode })] }), agentLoop.isRunning && agentLoop.activityPhase !== "idle" ? (_jsx(Box, { marginTop: 1, borderStyle: "round", borderColor: agentLoop.activityPhase === "thinking" ? THINKING_BORDER_COLORS[0] : "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, thinkingEnabled: thinkingEnabled, 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, staticDisplay: true }) })) : 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 &&
|
|
3542
3613
|
!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: () => {
|
|
3543
|
-
// While the agent is running, skip the screen-clear + staticKey
|
|
3544
|
-
// bump that would otherwise wipe the chat history from scrollback.
|
|
3545
3614
|
// Just flip the overlay state — Ink's log-update handles the
|
|
3546
3615
|
// live-area transition (chat input → TaskOverlay) natively, and
|
|
3547
3616
|
// the chat history above stays in scrollback. When the overlay
|