@prestyj/cli 4.2.77 → 4.3.34
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/README.md +2 -2
- package/dist/cli.js +335 -85
- package/dist/cli.js.map +1 -1
- package/dist/config.d.ts +10 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +33 -0
- package/dist/config.js.map +1 -1
- package/dist/core/agent-session.d.ts.map +1 -1
- package/dist/core/agent-session.js +42 -26
- package/dist/core/agent-session.js.map +1 -1
- package/dist/core/auth-storage.d.ts.map +1 -1
- package/dist/core/auth-storage.js +17 -2
- package/dist/core/auth-storage.js.map +1 -1
- package/dist/core/auto-update.d.ts +16 -3
- package/dist/core/auto-update.d.ts.map +1 -1
- package/dist/core/auto-update.js +126 -56
- package/dist/core/auto-update.js.map +1 -1
- package/dist/core/auto-update.test.d.ts +2 -0
- package/dist/core/auto-update.test.d.ts.map +1 -0
- package/dist/core/auto-update.test.js +205 -0
- package/dist/core/auto-update.test.js.map +1 -0
- package/dist/core/compaction/compactor.d.ts +18 -1
- package/dist/core/compaction/compactor.d.ts.map +1 -1
- package/dist/core/compaction/compactor.js +48 -7
- package/dist/core/compaction/compactor.js.map +1 -1
- package/dist/core/compaction/compactor.test.js +39 -6
- package/dist/core/compaction/compactor.test.js.map +1 -1
- package/dist/core/compaction/token-estimator.d.ts.map +1 -1
- package/dist/core/compaction/token-estimator.js +4 -0
- package/dist/core/compaction/token-estimator.js.map +1 -1
- package/dist/core/compaction/token-estimator.test.js +1 -1
- package/dist/core/index.d.ts +1 -1
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/index.js +1 -1
- package/dist/core/index.js.map +1 -1
- package/dist/core/mcp/client.d.ts.map +1 -1
- package/dist/core/mcp/client.js +15 -2
- package/dist/core/mcp/client.js.map +1 -1
- package/dist/core/model-registry.d.ts.map +1 -1
- package/dist/core/model-registry.js +74 -5
- package/dist/core/model-registry.js.map +1 -1
- package/dist/core/oauth/types.d.ts +1 -0
- package/dist/core/oauth/types.d.ts.map +1 -1
- package/dist/core/prompt-commands.d.ts.map +1 -1
- package/dist/core/prompt-commands.js +134 -0
- package/dist/core/prompt-commands.js.map +1 -1
- package/dist/core/settings-manager.d.ts +5 -0
- package/dist/core/settings-manager.d.ts.map +1 -1
- package/dist/core/settings-manager.js +14 -2
- package/dist/core/settings-manager.js.map +1 -1
- package/dist/interactive.js +1 -1
- package/dist/interactive.js.map +1 -1
- package/dist/modes/agent-home-mode.d.ts +21 -0
- package/dist/modes/agent-home-mode.d.ts.map +1 -0
- package/dist/modes/agent-home-mode.js +553 -0
- package/dist/modes/agent-home-mode.js.map +1 -0
- package/dist/system-prompt.d.ts.map +1 -1
- package/dist/system-prompt.js +30 -14
- package/dist/system-prompt.js.map +1 -1
- package/dist/tools/bash.d.ts.map +1 -1
- package/dist/tools/bash.js +9 -2
- package/dist/tools/bash.js.map +1 -1
- package/dist/tools/edit.d.ts +4 -2
- package/dist/tools/edit.d.ts.map +1 -1
- package/dist/tools/edit.js +48 -33
- package/dist/tools/edit.js.map +1 -1
- package/dist/tools/edit.test.js +80 -20
- package/dist/tools/edit.test.js.map +1 -1
- package/dist/tools/grep.d.ts.map +1 -1
- package/dist/tools/grep.js +12 -0
- package/dist/tools/grep.js.map +1 -1
- package/dist/tools/index.d.ts +1 -0
- package/dist/tools/index.d.ts.map +1 -1
- package/dist/tools/index.js +6 -0
- package/dist/tools/index.js.map +1 -1
- package/dist/tools/overflow.js +1 -1
- package/dist/tools/overflow.js.map +1 -1
- package/dist/tools/plan-mode.test.js +2 -2
- package/dist/tools/plan-mode.test.js.map +1 -1
- package/dist/tools/read.d.ts.map +1 -1
- package/dist/tools/read.js +14 -1
- package/dist/tools/read.js.map +1 -1
- package/dist/tools/subagent.d.ts.map +1 -1
- package/dist/tools/subagent.js +20 -3
- package/dist/tools/subagent.js.map +1 -1
- package/dist/tools/web-search.d.ts +9 -0
- package/dist/tools/web-search.d.ts.map +1 -0
- package/dist/tools/web-search.js +241 -0
- package/dist/tools/web-search.js.map +1 -0
- package/dist/tools/write.js +2 -2
- package/dist/tools/write.js.map +1 -1
- package/dist/tools/write.test.js +7 -9
- package/dist/tools/write.test.js.map +1 -1
- package/dist/ui/App.d.ts +4 -0
- package/dist/ui/App.d.ts.map +1 -1
- package/dist/ui/App.js +324 -161
- package/dist/ui/App.js.map +1 -1
- package/dist/ui/activity-phrases.d.ts.map +1 -1
- package/dist/ui/activity-phrases.js +74 -129
- package/dist/ui/activity-phrases.js.map +1 -1
- package/dist/ui/components/ActivityIndicator.d.ts +3 -1
- package/dist/ui/components/ActivityIndicator.d.ts.map +1 -1
- package/dist/ui/components/ActivityIndicator.js +5 -2
- package/dist/ui/components/ActivityIndicator.js.map +1 -1
- package/dist/ui/components/AssistantMessage.d.ts +1 -0
- package/dist/ui/components/AssistantMessage.d.ts.map +1 -1
- package/dist/ui/components/AssistantMessage.js +2 -2
- package/dist/ui/components/AssistantMessage.js.map +1 -1
- package/dist/ui/components/DiffFrame.d.ts +10 -0
- package/dist/ui/components/DiffFrame.d.ts.map +1 -0
- package/dist/ui/components/DiffFrame.js +13 -0
- package/dist/ui/components/DiffFrame.js.map +1 -0
- package/dist/ui/components/Footer.d.ts +2 -3
- package/dist/ui/components/Footer.d.ts.map +1 -1
- package/dist/ui/components/Footer.js +6 -5
- package/dist/ui/components/Footer.js.map +1 -1
- package/dist/ui/components/InputArea.d.ts.map +1 -1
- package/dist/ui/components/InputArea.js +297 -8
- package/dist/ui/components/InputArea.js.map +1 -1
- package/dist/ui/components/Markdown.d.ts +1 -1
- package/dist/ui/components/Markdown.d.ts.map +1 -1
- package/dist/ui/components/Markdown.js +25 -213
- package/dist/ui/components/Markdown.js.map +1 -1
- package/dist/ui/components/ModelSelector.d.ts.map +1 -1
- package/dist/ui/components/ModelSelector.js +10 -2
- package/dist/ui/components/ModelSelector.js.map +1 -1
- package/dist/ui/components/Ratchet.d.ts.map +1 -1
- package/dist/ui/components/Ratchet.js +10 -3
- package/dist/ui/components/Ratchet.js.map +1 -1
- package/dist/ui/components/StreamingArea.d.ts.map +1 -1
- package/dist/ui/components/StreamingArea.js +2 -2
- package/dist/ui/components/StreamingArea.js.map +1 -1
- package/dist/ui/components/SubAgentPanel.d.ts.map +1 -1
- package/dist/ui/components/SubAgentPanel.js +5 -1
- package/dist/ui/components/SubAgentPanel.js.map +1 -1
- package/dist/ui/components/ThemeSelector.d.ts +9 -0
- package/dist/ui/components/ThemeSelector.d.ts.map +1 -0
- package/dist/ui/components/ThemeSelector.js +25 -0
- package/dist/ui/components/ThemeSelector.js.map +1 -0
- package/dist/ui/components/ToolExecution.d.ts +2 -0
- package/dist/ui/components/ToolExecution.d.ts.map +1 -1
- package/dist/ui/components/ToolExecution.js +95 -30
- package/dist/ui/components/ToolExecution.js.map +1 -1
- package/dist/ui/components/ToolGroupExecution.d.ts +3 -2
- package/dist/ui/components/ToolGroupExecution.d.ts.map +1 -1
- package/dist/ui/components/ToolGroupExecution.js +68 -51
- package/dist/ui/components/ToolGroupExecution.js.map +1 -1
- package/dist/ui/hooks/stall-retry-duplicate.test.d.ts +2 -0
- package/dist/ui/hooks/stall-retry-duplicate.test.d.ts.map +1 -0
- package/dist/ui/hooks/stall-retry-duplicate.test.js +94 -0
- package/dist/ui/hooks/stall-retry-duplicate.test.js.map +1 -0
- package/dist/ui/hooks/useAgentLoop.d.ts +10 -3
- package/dist/ui/hooks/useAgentLoop.d.ts.map +1 -1
- package/dist/ui/hooks/useAgentLoop.js +131 -16
- package/dist/ui/hooks/useAgentLoop.js.map +1 -1
- package/dist/ui/hooks/useDoublePress.d.ts +8 -0
- package/dist/ui/hooks/useDoublePress.d.ts.map +1 -0
- package/dist/ui/hooks/useDoublePress.js +44 -0
- package/dist/ui/hooks/useDoublePress.js.map +1 -0
- package/dist/ui/hooks/useTerminalProgress.d.ts +1 -1
- package/dist/ui/hooks/useTerminalProgress.d.ts.map +1 -1
- package/dist/ui/hooks/useTerminalProgress.js +18 -15
- package/dist/ui/hooks/useTerminalProgress.js.map +1 -1
- package/dist/ui/hooks/useTerminalTitle.d.ts +3 -6
- package/dist/ui/hooks/useTerminalTitle.d.ts.map +1 -1
- package/dist/ui/hooks/useTerminalTitle.js +10 -19
- package/dist/ui/hooks/useTerminalTitle.js.map +1 -1
- package/dist/ui/live-item-flush.d.ts +4 -0
- package/dist/ui/live-item-flush.d.ts.map +1 -1
- package/dist/ui/live-item-flush.js +36 -0
- package/dist/ui/live-item-flush.js.map +1 -1
- package/dist/ui/login.d.ts.map +1 -1
- package/dist/ui/login.js +3 -0
- package/dist/ui/login.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 +9 -3
- package/dist/ui/render.js.map +1 -1
- package/dist/ui/stores/create-store.d.ts +15 -0
- package/dist/ui/stores/create-store.d.ts.map +1 -0
- package/dist/ui/stores/create-store.js +30 -0
- package/dist/ui/stores/create-store.js.map +1 -0
- package/dist/ui/stores/index.d.ts +3 -0
- package/dist/ui/stores/index.d.ts.map +1 -0
- package/dist/ui/stores/index.js +3 -0
- package/dist/ui/stores/index.js.map +1 -0
- package/dist/ui/stores/taskbar-store.d.ts +19 -0
- package/dist/ui/stores/taskbar-store.d.ts.map +1 -0
- package/dist/ui/stores/taskbar-store.js +59 -0
- package/dist/ui/stores/taskbar-store.js.map +1 -0
- package/dist/ui/theme/dark-ansi.json +48 -0
- package/dist/ui/theme/dark-daltonized.json +48 -0
- package/dist/ui/theme/dark.json +22 -1
- package/dist/ui/theme/detect-theme.d.ts +12 -8
- package/dist/ui/theme/detect-theme.d.ts.map +1 -1
- package/dist/ui/theme/detect-theme.js +58 -39
- package/dist/ui/theme/detect-theme.js.map +1 -1
- package/dist/ui/theme/light-ansi.json +48 -0
- package/dist/ui/theme/light-daltonized.json +48 -0
- package/dist/ui/theme/light.json +44 -23
- package/dist/ui/theme/theme.d.ts +27 -1
- package/dist/ui/theme/theme.d.ts.map +1 -1
- package/dist/ui/theme/theme.js +24 -1
- package/dist/ui/theme/theme.js.map +1 -1
- package/dist/ui/utils/highlight.d.ts +1 -1
- package/dist/ui/utils/highlight.d.ts.map +1 -1
- package/dist/ui/utils/highlight.js +24 -4
- package/dist/ui/utils/highlight.js.map +1 -1
- package/dist/ui/utils/hyperlink.d.ts +11 -0
- package/dist/ui/utils/hyperlink.d.ts.map +1 -0
- package/dist/ui/utils/hyperlink.js +27 -0
- package/dist/ui/utils/hyperlink.js.map +1 -0
- package/dist/ui/utils/markdown-cache.d.ts +5 -6
- package/dist/ui/utils/markdown-cache.d.ts.map +1 -1
- package/dist/ui/utils/markdown-cache.js +5 -5
- package/dist/ui/utils/markdown-cache.js.map +1 -1
- package/dist/ui/utils/signal.d.ts +21 -0
- package/dist/ui/utils/signal.d.ts.map +1 -0
- package/dist/ui/utils/signal.js +30 -0
- package/dist/ui/utils/signal.js.map +1 -0
- package/dist/ui/utils/supports-hyperlinks.d.ts +6 -0
- package/dist/ui/utils/supports-hyperlinks.d.ts.map +1 -0
- package/dist/ui/utils/supports-hyperlinks.js +52 -0
- package/dist/ui/utils/supports-hyperlinks.js.map +1 -0
- package/dist/ui/utils/token-to-ansi.d.ts +9 -0
- package/dist/ui/utils/token-to-ansi.d.ts.map +1 -0
- package/dist/ui/utils/token-to-ansi.js +305 -0
- package/dist/ui/utils/token-to-ansi.js.map +1 -0
- package/dist/utils/session-title.d.ts +19 -0
- package/dist/utils/session-title.d.ts.map +1 -0
- package/dist/utils/session-title.js +82 -0
- package/dist/utils/session-title.js.map +1 -0
- package/package.json +7 -5
package/dist/ui/App.js
CHANGED
|
@@ -2,6 +2,8 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
|
|
|
2
2
|
import React, { useState, useRef, useCallback, useEffect, useMemo } from "react";
|
|
3
3
|
import { Box, Text, Static, useStdout } from "ink";
|
|
4
4
|
import { useTerminalSize } from "./hooks/useTerminalSize.js";
|
|
5
|
+
import { useDoublePress } from "./hooks/useDoublePress.js";
|
|
6
|
+
import { useTaskBarStore, useTaskBarPolling, focusTaskBar, exitTaskBar, expandTaskBar, collapseTaskBar, navigateTaskBar, killTask, } from "./stores/taskbar-store.js";
|
|
5
7
|
import crypto, { createHash } from "node:crypto";
|
|
6
8
|
import { readFileSync, writeFileSync } from "node:fs";
|
|
7
9
|
import { homedir } from "node:os";
|
|
@@ -16,6 +18,7 @@ import { ToolGroupExecution } from "./components/ToolGroupExecution.js";
|
|
|
16
18
|
import { ServerToolExecution } from "./components/ServerToolExecution.js";
|
|
17
19
|
import { SubAgentPanel } from "./components/SubAgentPanel.js";
|
|
18
20
|
import { CompactionSpinner, CompactionDone } from "./components/CompactionNotice.js";
|
|
21
|
+
import { createWebSearchTool } from "../tools/web-search.js";
|
|
19
22
|
import { StreamingArea } from "./components/StreamingArea.js";
|
|
20
23
|
import { ActivityIndicator } from "./components/ActivityIndicator.js";
|
|
21
24
|
import { InputArea } from "./components/InputArea.js";
|
|
@@ -25,8 +28,9 @@ import { PlanOverlay } from "./components/PlanOverlay.js";
|
|
|
25
28
|
import { ModelSelector } from "./components/ModelSelector.js";
|
|
26
29
|
import { TaskOverlay } from "./components/TaskOverlay.js";
|
|
27
30
|
import { SkillsOverlay } from "./components/SkillsOverlay.js";
|
|
31
|
+
import { ThemeSelector } from "./components/ThemeSelector.js";
|
|
28
32
|
import { BackgroundTasksBar } from "./components/BackgroundTasksBar.js";
|
|
29
|
-
import { useTheme } from "./theme/theme.js";
|
|
33
|
+
import { useTheme, useSetTheme } from "./theme/theme.js";
|
|
30
34
|
import { useAnimationTick, useAnimationActive, deriveFrame, } from "./components/AnimationContext.js";
|
|
31
35
|
import { useTerminalTitle } from "./hooks/useTerminalTitle.js";
|
|
32
36
|
import { useTerminalProgress } from "./hooks/useTerminalProgress.js";
|
|
@@ -34,6 +38,8 @@ import { getGitBranch } from "../utils/git.js";
|
|
|
34
38
|
import { getModel, getContextWindow } from "../core/model-registry.js";
|
|
35
39
|
import { SessionManager } from "../core/session-manager.js";
|
|
36
40
|
import { log } from "../core/logger.js";
|
|
41
|
+
import { startPeriodicUpdateCheck, stopPeriodicUpdateCheck } from "../core/auto-update.js";
|
|
42
|
+
import { generateSessionTitle } from "../utils/session-title.js";
|
|
37
43
|
import { SettingsManager } from "../core/settings-manager.js";
|
|
38
44
|
import { shouldCompact, compact } from "../core/compaction/compactor.js";
|
|
39
45
|
import { estimateConversationTokens } from "../core/compaction/token-estimator.js";
|
|
@@ -42,7 +48,7 @@ import { loadCustomCommands } from "../core/custom-commands.js";
|
|
|
42
48
|
import { buildSystemPrompt } from "../system-prompt.js";
|
|
43
49
|
import { extractPlanSteps, findCompletedMarkers, markStepsCompleted, stripDoneMarkers, } from "../utils/plan-steps.js";
|
|
44
50
|
import { getMCPServers } from "../core/mcp/index.js";
|
|
45
|
-
import { trimFlushedItems, flushOnTurnText, flushOnTurnEnd } from "./live-item-flush.js";
|
|
51
|
+
import { trimFlushedItems, flushOnTurnText, flushOnTurnEnd, flushOverflow, } from "./live-item-flush.js";
|
|
46
52
|
import { Buddy } from "./buddy/Buddy.js";
|
|
47
53
|
// ── Provider Error Hints ──────────────────────────────────
|
|
48
54
|
/** Detect provider-side errors and return a user-facing hint. */
|
|
@@ -264,22 +270,23 @@ function markTaskInProgress(cwd, taskId) {
|
|
|
264
270
|
// ── App Component ──────────────────────────────────────────
|
|
265
271
|
export function App(props) {
|
|
266
272
|
const theme = useTheme();
|
|
273
|
+
const switchTheme = useSetTheme();
|
|
267
274
|
const { stdout } = useStdout();
|
|
268
275
|
const { columns, resizeKey } = useTerminalSize();
|
|
269
276
|
// Hoisted before terminal title hook so it can reference them
|
|
270
277
|
const [lastUserMessage, setLastUserMessage] = useState("");
|
|
278
|
+
const [exitPending, setExitPending] = useState(false);
|
|
271
279
|
const [planMode, setPlanMode] = useState(false);
|
|
280
|
+
const planModeLocalRef = useRef(false);
|
|
281
|
+
planModeLocalRef.current = planMode;
|
|
272
282
|
// Terminal title — updated later after agentLoop is created
|
|
273
283
|
// (hoisted here so the hook is always called in the same order)
|
|
274
|
-
const [titlePhase, setTitlePhase] = useState("idle");
|
|
275
284
|
const [titleRunning, setTitleRunning] = useState(false);
|
|
276
|
-
const [
|
|
285
|
+
const [sessionTitle, setSessionTitle] = useState(undefined);
|
|
286
|
+
const sessionTitleGeneratedRef = useRef(false);
|
|
277
287
|
useTerminalTitle({
|
|
278
|
-
phase: titlePhase,
|
|
279
288
|
isRunning: titleRunning,
|
|
280
|
-
|
|
281
|
-
activeToolNames: titleToolNames,
|
|
282
|
-
planMode,
|
|
289
|
+
sessionTitle,
|
|
283
290
|
});
|
|
284
291
|
// Items scrolled into Static (history). For restored sessions, skip the
|
|
285
292
|
// banner and add restored items via useEffect so Ink's <Static> treats them
|
|
@@ -322,20 +329,38 @@ export function App(props) {
|
|
|
322
329
|
const persistedIndexRef = useRef(messagesRef.current.length);
|
|
323
330
|
/** Last actual API-reported input token count (from turn_end). */
|
|
324
331
|
const lastActualTokensRef = useRef(0);
|
|
325
|
-
/** Timestamp
|
|
332
|
+
/** Timestamp (ms) when lastActualTokensRef was last updated by turn_end. */
|
|
333
|
+
const lastActualTokensTimestampRef = useRef(0);
|
|
334
|
+
/** Timestamp of last compaction — used for time-based cooldown and staleness detection. */
|
|
326
335
|
const lastCompactionTimeRef = useRef(0);
|
|
327
336
|
const getId = () => String(nextIdRef.current++);
|
|
328
337
|
// Two-phase flush: items waiting to be moved to Static history after the
|
|
329
338
|
// live area has been cleared and Ink has committed the smaller output.
|
|
330
339
|
const pendingFlushRef = useRef([]);
|
|
340
|
+
const [flushGeneration, setFlushGeneration] = useState(0);
|
|
341
|
+
/** Queue items for two-phase flush and signal the drain effect. */
|
|
342
|
+
const queueFlush = useCallback((items) => {
|
|
343
|
+
if (items.length === 0)
|
|
344
|
+
return;
|
|
345
|
+
pendingFlushRef.current = [...pendingFlushRef.current, ...items];
|
|
346
|
+
setFlushGeneration((g) => g + 1);
|
|
347
|
+
}, []);
|
|
331
348
|
// Derive credentials for the current provider
|
|
332
349
|
const currentCreds = props.credentialsByProvider?.[currentProvider];
|
|
333
350
|
const activeApiKey = currentCreds?.accessToken ?? props.apiKey;
|
|
334
351
|
const activeAccountId = currentCreds?.accountId ?? props.accountId;
|
|
352
|
+
const activeBaseUrl = currentCreds?.baseUrl ?? props.baseUrl;
|
|
335
353
|
// Load git branch
|
|
336
354
|
useEffect(() => {
|
|
337
355
|
getGitBranch(props.cwd).then(setGitBranch);
|
|
338
356
|
}, [props.cwd]);
|
|
357
|
+
// Periodic update check during long sessions
|
|
358
|
+
useEffect(() => {
|
|
359
|
+
startPeriodicUpdateCheck(props.version, (msg) => {
|
|
360
|
+
setLiveItems((prev) => [...prev, { kind: "info", text: msg, id: getId() }]);
|
|
361
|
+
});
|
|
362
|
+
return () => stopPeriodicUpdateCheck();
|
|
363
|
+
}, [props.version]);
|
|
339
364
|
// Load custom commands from .ezcoder/commands/
|
|
340
365
|
const [customCommands, setCustomCommands] = useState([]);
|
|
341
366
|
const reloadCustomCommands = useCallback(() => {
|
|
@@ -392,7 +417,11 @@ export function App(props) {
|
|
|
392
417
|
stdout?.write("\x1b[2J\x1b[3J\x1b[H");
|
|
393
418
|
setPlanAutoExpand(true);
|
|
394
419
|
setOverlay("plan");
|
|
395
|
-
planOverlayPendingRef
|
|
420
|
+
// Don't clear planOverlayPendingRef here — keep it true until
|
|
421
|
+
// the user actually approves/rejects the plan. Clearing it on a
|
|
422
|
+
// timer causes a race where agent_done fires after the 300ms
|
|
423
|
+
// timeout but before the user interacts, triggering a premature
|
|
424
|
+
// completion sound.
|
|
396
425
|
}, 300);
|
|
397
426
|
return ("Plan submitted. Exiting plan mode.\n" +
|
|
398
427
|
"The plan pane is opening for user review.\n" +
|
|
@@ -461,17 +490,24 @@ export function App(props) {
|
|
|
461
490
|
signal: undefined,
|
|
462
491
|
approvedPlanPath: approvedPlanPathRef.current,
|
|
463
492
|
});
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
493
|
+
if (result.result.compacted) {
|
|
494
|
+
// Replace spinner with completed notice
|
|
495
|
+
setLiveItems((prev) => prev.map((item) => item.id === spinId
|
|
496
|
+
? {
|
|
497
|
+
kind: "compacted",
|
|
498
|
+
originalCount: result.result.originalCount,
|
|
499
|
+
newCount: result.result.newCount,
|
|
500
|
+
tokensBefore: result.result.tokensBeforeEstimate,
|
|
501
|
+
tokensAfter: result.result.tokensAfterEstimate,
|
|
502
|
+
id: spinId,
|
|
503
|
+
}
|
|
504
|
+
: item));
|
|
505
|
+
}
|
|
506
|
+
else {
|
|
507
|
+
// Nothing was actually compacted — remove spinner silently
|
|
508
|
+
log("INFO", "compaction", `Compaction skipped: ${result.result.reason ?? "unknown"}`);
|
|
509
|
+
setLiveItems((prev) => prev.filter((item) => item.id !== spinId));
|
|
510
|
+
}
|
|
475
511
|
return result.messages;
|
|
476
512
|
}
|
|
477
513
|
catch (err) {
|
|
@@ -502,7 +538,6 @@ export function App(props) {
|
|
|
502
538
|
if (options?.force) {
|
|
503
539
|
const result = await compactConversation(messages);
|
|
504
540
|
lastCompactionTimeRef.current = Date.now();
|
|
505
|
-
lastActualTokensRef.current = 0; // Reset stale pre-compaction count
|
|
506
541
|
return result;
|
|
507
542
|
}
|
|
508
543
|
if (!autoCompact)
|
|
@@ -513,73 +548,41 @@ export function App(props) {
|
|
|
513
548
|
return messages;
|
|
514
549
|
}
|
|
515
550
|
const contextWindow = getContextWindow(currentModel);
|
|
516
|
-
// Prefer actual API-reported tokens over char-based estimate
|
|
517
|
-
|
|
551
|
+
// Prefer actual API-reported tokens over char-based estimate, but only
|
|
552
|
+
// when the token count was recorded AFTER the most recent compaction.
|
|
553
|
+
// A count from before compaction is stale — it reflects the old context
|
|
554
|
+
// size and would trigger compaction again immediately for no reason.
|
|
555
|
+
const tokensFresh = lastActualTokensTimestampRef.current > lastCompactionTimeRef.current;
|
|
556
|
+
const actualTokens = lastActualTokensRef.current > 0 && tokensFresh ? lastActualTokensRef.current : undefined;
|
|
518
557
|
if (shouldCompact(messages, contextWindow, threshold, actualTokens)) {
|
|
558
|
+
const before = messages.length;
|
|
519
559
|
const result = await compactConversation(messages);
|
|
520
560
|
lastCompactionTimeRef.current = Date.now();
|
|
521
|
-
|
|
561
|
+
// If compaction was a no-op (e.g. too few middle messages to summarize),
|
|
562
|
+
// return the original reference so the agent loop doesn't replace messages.
|
|
563
|
+
if (result.length === before)
|
|
564
|
+
return messages;
|
|
522
565
|
return result;
|
|
523
566
|
}
|
|
524
567
|
return messages;
|
|
525
568
|
}, [currentModel, compactConversation]);
|
|
526
|
-
// ── Background task bar state
|
|
527
|
-
const
|
|
528
|
-
|
|
529
|
-
const
|
|
530
|
-
const
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
if (!props.processManager)
|
|
534
|
-
return;
|
|
535
|
-
const pm = props.processManager;
|
|
536
|
-
const poll = () => {
|
|
537
|
-
const running = pm.list().filter((p) => p.exitCode === null);
|
|
538
|
-
setBgTasks(running);
|
|
539
|
-
};
|
|
540
|
-
poll();
|
|
541
|
-
const interval = setInterval(poll, 2000);
|
|
542
|
-
return () => clearInterval(interval);
|
|
543
|
-
}, [props.processManager]);
|
|
544
|
-
// Auto-exit task panel when all tasks gone
|
|
545
|
-
useEffect(() => {
|
|
546
|
-
if (bgTasks.length === 0) {
|
|
547
|
-
setTaskBarFocused(false);
|
|
548
|
-
setTaskBarExpanded(false);
|
|
549
|
-
}
|
|
550
|
-
// Clamp selected index
|
|
551
|
-
const maxIdx = Math.min(bgTasks.length, 5) - 1;
|
|
552
|
-
if (selectedTaskIndex > maxIdx && maxIdx >= 0) {
|
|
553
|
-
setSelectedTaskIndex(maxIdx);
|
|
554
|
-
}
|
|
555
|
-
}, [bgTasks.length, selectedTaskIndex]);
|
|
556
|
-
const handleFocusTaskBar = useCallback(() => {
|
|
557
|
-
if (bgTasks.length > 0) {
|
|
558
|
-
setTaskBarFocused(true);
|
|
559
|
-
}
|
|
560
|
-
}, [bgTasks.length]);
|
|
561
|
-
const handleTaskBarExit = useCallback(() => {
|
|
562
|
-
setTaskBarFocused(false);
|
|
563
|
-
setTaskBarExpanded(false);
|
|
564
|
-
}, []);
|
|
565
|
-
const handleTaskBarExpand = useCallback(() => {
|
|
566
|
-
setTaskBarExpanded(true);
|
|
567
|
-
setSelectedTaskIndex(0);
|
|
568
|
-
}, []);
|
|
569
|
-
const handleTaskBarCollapse = useCallback(() => {
|
|
570
|
-
setTaskBarExpanded(false);
|
|
571
|
-
}, []);
|
|
569
|
+
// ── Background task bar state (external store) ──────────
|
|
570
|
+
const { bgTasks, focused: taskBarFocused, expanded: taskBarExpanded, selectedIndex: selectedTaskIndex, } = useTaskBarStore();
|
|
571
|
+
useTaskBarPolling(props.processManager);
|
|
572
|
+
const handleFocusTaskBar = useCallback(() => focusTaskBar(), []);
|
|
573
|
+
const handleTaskBarExit = useCallback(() => exitTaskBar(), []);
|
|
574
|
+
const handleTaskBarExpand = useCallback(() => expandTaskBar(), []);
|
|
575
|
+
const handleTaskBarCollapse = useCallback(() => collapseTaskBar(), []);
|
|
572
576
|
const handleTaskKill = useCallback((id) => {
|
|
573
|
-
props.processManager
|
|
577
|
+
if (props.processManager)
|
|
578
|
+
killTask(props.processManager, id);
|
|
574
579
|
}, [props.processManager]);
|
|
575
|
-
const handleTaskNavigate = useCallback((index) =>
|
|
576
|
-
setSelectedTaskIndex(index);
|
|
577
|
-
}, []);
|
|
580
|
+
const handleTaskNavigate = useCallback((index) => navigateTaskBar(index), []);
|
|
578
581
|
// Resolve fresh OAuth credentials before each agent loop run.
|
|
579
582
|
// Falls back to the static props when authStorage is not available.
|
|
580
|
-
const resolveCredentials = useCallback(async () => {
|
|
583
|
+
const resolveCredentials = useCallback(async (opts) => {
|
|
581
584
|
if (props.authStorage) {
|
|
582
|
-
const creds = await props.authStorage.resolveCredentials(currentProvider);
|
|
585
|
+
const creds = await props.authStorage.resolveCredentials(currentProvider, opts);
|
|
583
586
|
return { apiKey: creds.accessToken, accountId: creds.accountId };
|
|
584
587
|
}
|
|
585
588
|
return { apiKey: activeApiKey, accountId: activeAccountId };
|
|
@@ -592,7 +595,7 @@ export function App(props) {
|
|
|
592
595
|
maxTokens: props.maxTokens,
|
|
593
596
|
thinking: thinkingEnabled ? (props.thinking ?? "medium") : undefined,
|
|
594
597
|
apiKey: activeApiKey,
|
|
595
|
-
baseUrl:
|
|
598
|
+
baseUrl: activeBaseUrl,
|
|
596
599
|
accountId: activeAccountId,
|
|
597
600
|
resolveCredentials,
|
|
598
601
|
transformContext,
|
|
@@ -613,7 +616,57 @@ export function App(props) {
|
|
|
613
616
|
}
|
|
614
617
|
})();
|
|
615
618
|
}
|
|
616
|
-
|
|
619
|
+
// Generate session title after the first turn (background, best-effort)
|
|
620
|
+
if (!sessionTitleGeneratedRef.current) {
|
|
621
|
+
sessionTitleGeneratedRef.current = true;
|
|
622
|
+
const msgs = messagesRef.current;
|
|
623
|
+
// Find the first user message and first assistant text
|
|
624
|
+
const userMsg = msgs.find((m) => m.role === "user");
|
|
625
|
+
const assistantMsg = msgs.find((m) => m.role === "assistant");
|
|
626
|
+
const userText = typeof userMsg?.content === "string"
|
|
627
|
+
? userMsg.content
|
|
628
|
+
: Array.isArray(userMsg?.content)
|
|
629
|
+
? userMsg.content
|
|
630
|
+
.filter((c) => c.type === "text")
|
|
631
|
+
.map((c) => c.text)
|
|
632
|
+
.join(" ")
|
|
633
|
+
: "";
|
|
634
|
+
const assistantText = typeof assistantMsg?.content === "string"
|
|
635
|
+
? assistantMsg.content
|
|
636
|
+
: Array.isArray(assistantMsg?.content)
|
|
637
|
+
? assistantMsg.content
|
|
638
|
+
.filter((c) => c.type === "text")
|
|
639
|
+
.map((c) => c.text)
|
|
640
|
+
.join(" ")
|
|
641
|
+
: "";
|
|
642
|
+
if (userText) {
|
|
643
|
+
generateSessionTitle({
|
|
644
|
+
provider: currentProvider,
|
|
645
|
+
userMessage: userText,
|
|
646
|
+
assistantPreview: assistantText.slice(0, 200),
|
|
647
|
+
apiKey: activeApiKey,
|
|
648
|
+
baseUrl: activeBaseUrl,
|
|
649
|
+
accountId: activeAccountId,
|
|
650
|
+
resolveCredentials,
|
|
651
|
+
}).then((title) => {
|
|
652
|
+
setSessionTitle(title);
|
|
653
|
+
log("INFO", "title", `Session title generated: ${title}`);
|
|
654
|
+
}, () => {
|
|
655
|
+
// Best-effort — silently ignore failures
|
|
656
|
+
});
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
}, [
|
|
660
|
+
persistNewMessages,
|
|
661
|
+
planMode,
|
|
662
|
+
props.cwd,
|
|
663
|
+
props.skills,
|
|
664
|
+
currentProvider,
|
|
665
|
+
activeApiKey,
|
|
666
|
+
activeAccountId,
|
|
667
|
+
activeBaseUrl,
|
|
668
|
+
resolveCredentials,
|
|
669
|
+
]),
|
|
617
670
|
onTurnText: useCallback((text, thinking, thinkingMs) => {
|
|
618
671
|
// Track [DONE:n] markers for plan step progress
|
|
619
672
|
if (planStepsRef.current.length > 0) {
|
|
@@ -629,13 +682,28 @@ export function App(props) {
|
|
|
629
682
|
// Flush all completed items from the previous turn to Static history.
|
|
630
683
|
// This keeps liveItems bounded per-turn, preventing Ink's live area from
|
|
631
684
|
// growing unbounded, which makes Ink's live-area re-renders expensive.
|
|
685
|
+
//
|
|
686
|
+
// Items are queued in pendingFlushRef (not sent to setHistory directly)
|
|
687
|
+
// so the Static write happens in a SEPARATE render cycle from the
|
|
688
|
+
// live-area change — avoiding both Ink cursor-math clipping and the
|
|
689
|
+
// brief duplicate that occurred when setHistory was nested inside the
|
|
690
|
+
// setLiveItems updater.
|
|
632
691
|
setLiveItems((prev) => {
|
|
633
692
|
const flushed = flushOnTurnText(prev);
|
|
634
693
|
if (flushed.length > 0) {
|
|
635
|
-
|
|
694
|
+
queueFlush(flushed);
|
|
636
695
|
}
|
|
637
696
|
const displayText = planStepsRef.current.length > 0 ? stripDoneMarkers(text) : text;
|
|
638
|
-
return [
|
|
697
|
+
return [
|
|
698
|
+
{
|
|
699
|
+
kind: "assistant",
|
|
700
|
+
text: displayText,
|
|
701
|
+
thinking,
|
|
702
|
+
thinkingMs,
|
|
703
|
+
planMode: planModeLocalRef.current,
|
|
704
|
+
id: getId(),
|
|
705
|
+
},
|
|
706
|
+
];
|
|
639
707
|
});
|
|
640
708
|
}, []),
|
|
641
709
|
onToolStart: useCallback((toolCallId, name, args) => {
|
|
@@ -646,7 +714,7 @@ export function App(props) {
|
|
|
646
714
|
setLiveItems((prev) => {
|
|
647
715
|
const { flushed, remaining } = partitionCompleted(prev);
|
|
648
716
|
if (flushed.length > 0) {
|
|
649
|
-
|
|
717
|
+
queueFlush(flushed);
|
|
650
718
|
}
|
|
651
719
|
return remaining;
|
|
652
720
|
});
|
|
@@ -707,6 +775,24 @@ export function App(props) {
|
|
|
707
775
|
}
|
|
708
776
|
}, []),
|
|
709
777
|
onToolUpdate: useCallback((toolCallId, update) => {
|
|
778
|
+
const u = update;
|
|
779
|
+
// Bash progress streaming — append output to tool_start item
|
|
780
|
+
if (u.type === "bash_progress") {
|
|
781
|
+
setLiveItems((prev) => {
|
|
782
|
+
const idx = prev.findIndex((item) => item.kind === "tool_start" && item.toolCallId === toolCallId);
|
|
783
|
+
if (idx === -1)
|
|
784
|
+
return prev;
|
|
785
|
+
const item = prev[idx];
|
|
786
|
+
const next = [...prev];
|
|
787
|
+
next[idx] = {
|
|
788
|
+
...item,
|
|
789
|
+
progressOutput: (item.progressOutput ?? "") + String(u.output ?? ""),
|
|
790
|
+
};
|
|
791
|
+
return next;
|
|
792
|
+
});
|
|
793
|
+
return;
|
|
794
|
+
}
|
|
795
|
+
// Subagent updates
|
|
710
796
|
setLiveItems((prev) => {
|
|
711
797
|
const groupIdx = prev.findIndex((item) => item.kind === "subagent_group");
|
|
712
798
|
if (groupIdx === -1)
|
|
@@ -759,7 +845,7 @@ export function App(props) {
|
|
|
759
845
|
// Flush completed items to Static to keep the live area small
|
|
760
846
|
const { flushed, remaining } = partitionCompleted(next);
|
|
761
847
|
if (flushed.length > 0) {
|
|
762
|
-
|
|
848
|
+
queueFlush(flushed);
|
|
763
849
|
}
|
|
764
850
|
return remaining;
|
|
765
851
|
});
|
|
@@ -818,7 +904,14 @@ export function App(props) {
|
|
|
818
904
|
// Flush completed items to Static to keep the live area small
|
|
819
905
|
const { flushed, remaining } = partitionCompleted(updated);
|
|
820
906
|
if (flushed.length > 0) {
|
|
821
|
-
|
|
907
|
+
queueFlush(flushed);
|
|
908
|
+
return remaining;
|
|
909
|
+
}
|
|
910
|
+
// Overflow flush: if live area is still large, flush aggressively
|
|
911
|
+
const overflow = flushOverflow(updated);
|
|
912
|
+
if (overflow.flushed.length > 0) {
|
|
913
|
+
queueFlush(overflow.flushed);
|
|
914
|
+
return overflow.remaining;
|
|
822
915
|
}
|
|
823
916
|
return remaining;
|
|
824
917
|
});
|
|
@@ -831,7 +924,7 @@ export function App(props) {
|
|
|
831
924
|
setLiveItems((prev) => {
|
|
832
925
|
const { flushed, remaining } = partitionCompleted(prev);
|
|
833
926
|
if (flushed.length > 0) {
|
|
834
|
-
|
|
927
|
+
queueFlush(flushed);
|
|
835
928
|
}
|
|
836
929
|
return [
|
|
837
930
|
...remaining,
|
|
@@ -882,7 +975,7 @@ export function App(props) {
|
|
|
882
975
|
// Flush completed items to Static
|
|
883
976
|
const { flushed, remaining } = partitionCompleted(updated);
|
|
884
977
|
if (flushed.length > 0) {
|
|
885
|
-
|
|
978
|
+
queueFlush(flushed);
|
|
886
979
|
}
|
|
887
980
|
return remaining;
|
|
888
981
|
});
|
|
@@ -895,15 +988,19 @@ export function App(props) {
|
|
|
895
988
|
...(usage.cacheRead != null && { cacheRead: String(usage.cacheRead) }),
|
|
896
989
|
...(usage.cacheWrite != null && { cacheWrite: String(usage.cacheWrite) }),
|
|
897
990
|
});
|
|
898
|
-
// Track actual token count for compaction decisions
|
|
991
|
+
// Track actual token count for compaction decisions.
|
|
992
|
+
// Anthropic has separate input/output limits — only count input.
|
|
993
|
+
// All other providers share the context window — count both.
|
|
994
|
+
const inputContext = usage.inputTokens + (usage.cacheRead ?? 0) + (usage.cacheWrite ?? 0);
|
|
899
995
|
lastActualTokensRef.current =
|
|
900
|
-
|
|
996
|
+
currentProvider === "anthropic" ? inputContext : inputContext + usage.outputTokens;
|
|
997
|
+
lastActualTokensTimestampRef.current = Date.now();
|
|
901
998
|
// For tool-only turns (no text), flush completed items to Static so
|
|
902
999
|
// liveItems doesn't grow unbounded across consecutive tool-only turns.
|
|
903
1000
|
setLiveItems((prev) => {
|
|
904
1001
|
const { flushed, remaining } = flushOnTurnEnd(prev, stopReason);
|
|
905
1002
|
if (flushed.length > 0) {
|
|
906
|
-
|
|
1003
|
+
queueFlush(flushed);
|
|
907
1004
|
}
|
|
908
1005
|
return remaining;
|
|
909
1006
|
});
|
|
@@ -926,9 +1023,8 @@ export function App(props) {
|
|
|
926
1023
|
// separate render cycle so the Static write never coincides with
|
|
927
1024
|
// a live-area height change in the same frame.
|
|
928
1025
|
setLiveItems((prev) => {
|
|
929
|
-
if (prev.length > 0)
|
|
930
|
-
|
|
931
|
-
}
|
|
1026
|
+
if (prev.length > 0)
|
|
1027
|
+
queueFlush(prev);
|
|
932
1028
|
return [];
|
|
933
1029
|
});
|
|
934
1030
|
// Run-all: auto-start next pending task after a short delay
|
|
@@ -1007,9 +1103,8 @@ export function App(props) {
|
|
|
1007
1103
|
? undefined
|
|
1008
1104
|
: content.filter((c) => c.type === "image").length || undefined;
|
|
1009
1105
|
setLiveItems((prev) => {
|
|
1010
|
-
if (prev.length > 0)
|
|
1011
|
-
|
|
1012
|
-
}
|
|
1106
|
+
if (prev.length > 0)
|
|
1107
|
+
queueFlush(prev);
|
|
1013
1108
|
return [];
|
|
1014
1109
|
});
|
|
1015
1110
|
const userItem = {
|
|
@@ -1036,14 +1131,11 @@ export function App(props) {
|
|
|
1036
1131
|
pendingFlushRef.current = [];
|
|
1037
1132
|
setHistory((h) => compactHistory([...h, ...trimFlushedItems(items)]));
|
|
1038
1133
|
}
|
|
1039
|
-
});
|
|
1134
|
+
}, [flushGeneration]);
|
|
1040
1135
|
// Sync terminal title with agent loop state
|
|
1041
|
-
const activeToolNamesKey = agentLoop.activeToolCalls.map((tc) => tc.name).join(",");
|
|
1042
1136
|
useEffect(() => {
|
|
1043
|
-
setTitlePhase(agentLoop.activityPhase);
|
|
1044
1137
|
setTitleRunning(agentLoop.isRunning);
|
|
1045
|
-
|
|
1046
|
-
}, [agentLoop.activityPhase, agentLoop.isRunning, activeToolNamesKey]);
|
|
1138
|
+
}, [agentLoop.isRunning]);
|
|
1047
1139
|
// Terminal progress bar (OSC 9;4) — pulsing bar in supported terminals
|
|
1048
1140
|
useTerminalProgress(agentLoop.isRunning, agentLoop.activeToolCalls.length > 0);
|
|
1049
1141
|
// Animated thinking border — derived from global animation tick
|
|
@@ -1084,6 +1176,9 @@ export function App(props) {
|
|
|
1084
1176
|
// Clear terminal screen + scrollback — needed because Ink's <Static>
|
|
1085
1177
|
// writes directly to stdout and can't be removed by clearing React state
|
|
1086
1178
|
stdout?.write("\x1b[2J\x1b[3J\x1b[H");
|
|
1179
|
+
// Discard any items queued for two-phase flush so they don't leak
|
|
1180
|
+
// into the new session after the Static remount.
|
|
1181
|
+
pendingFlushRef.current = [];
|
|
1087
1182
|
setHistory([{ kind: "banner", id: "banner" }]);
|
|
1088
1183
|
setLiveItems([]);
|
|
1089
1184
|
setDoneStatus(null);
|
|
@@ -1094,11 +1189,22 @@ export function App(props) {
|
|
|
1094
1189
|
void (async () => {
|
|
1095
1190
|
const newPrompt = await buildSystemPrompt(props.cwd, props.skills, planMode, undefined);
|
|
1096
1191
|
messagesRef.current = [{ role: "system", content: newPrompt }];
|
|
1192
|
+
persistedIndexRef.current = messagesRef.current.length;
|
|
1097
1193
|
})();
|
|
1098
1194
|
agentLoop.reset();
|
|
1195
|
+
setSessionTitle(undefined);
|
|
1196
|
+
sessionTitleGeneratedRef.current = false;
|
|
1197
|
+
// Bump staticKey to force Ink's <Static> to remount, discarding its
|
|
1198
|
+
// internal record of previously rendered items so they don't reappear.
|
|
1199
|
+
setStaticKey((k) => k + 1);
|
|
1099
1200
|
setLiveItems([{ kind: "info", text: "Session cleared.", id: getId() }]);
|
|
1100
1201
|
return;
|
|
1101
1202
|
}
|
|
1203
|
+
// Handle /theme — open theme selector overlay
|
|
1204
|
+
if (trimmed === "/theme" || trimmed === "/t") {
|
|
1205
|
+
setOverlay("theme");
|
|
1206
|
+
return;
|
|
1207
|
+
}
|
|
1102
1208
|
// Handle /plan — toggle plan mode
|
|
1103
1209
|
if (trimmed === "/plan" || trimmed === "/plan on") {
|
|
1104
1210
|
setPlanMode(true);
|
|
@@ -1175,7 +1281,7 @@ export function App(props) {
|
|
|
1175
1281
|
// Move live items into history before starting
|
|
1176
1282
|
setLiveItems((prev) => {
|
|
1177
1283
|
if (prev.length > 0) {
|
|
1178
|
-
|
|
1284
|
+
pendingFlushRef.current = [...pendingFlushRef.current, ...prev];
|
|
1179
1285
|
}
|
|
1180
1286
|
return [];
|
|
1181
1287
|
});
|
|
@@ -1278,10 +1384,14 @@ export function App(props) {
|
|
|
1278
1384
|
setLiveItems((prev) => [...prev, queuedItem]);
|
|
1279
1385
|
return;
|
|
1280
1386
|
}
|
|
1281
|
-
// Move any remaining live items into history (Static) before starting
|
|
1387
|
+
// Move any remaining live items into history (Static) before starting a
|
|
1388
|
+
// new turn. Must go through queueFlush so flushGeneration bumps and the
|
|
1389
|
+
// drain effect actually runs — mutating pendingFlushRef directly here
|
|
1390
|
+
// stashed items that nothing was signalled to pick up, so they sat in
|
|
1391
|
+
// limbo until some unrelated later code path happened to call queueFlush.
|
|
1282
1392
|
setLiveItems((prev) => {
|
|
1283
1393
|
if (prev.length > 0) {
|
|
1284
|
-
|
|
1394
|
+
queueFlush(prev);
|
|
1285
1395
|
}
|
|
1286
1396
|
return [];
|
|
1287
1397
|
});
|
|
@@ -1323,15 +1433,16 @@ export function App(props) {
|
|
|
1323
1433
|
]);
|
|
1324
1434
|
}
|
|
1325
1435
|
}, [agentLoop, props.onSlashCommand, compactConversation]);
|
|
1436
|
+
const handleDoubleExit = useDoublePress(setExitPending, () => process.exit(0));
|
|
1326
1437
|
const handleAbort = useCallback(() => {
|
|
1327
1438
|
if (agentLoop.isRunning) {
|
|
1328
1439
|
agentLoop.clearQueue();
|
|
1329
1440
|
agentLoop.abort();
|
|
1330
1441
|
}
|
|
1331
1442
|
else {
|
|
1332
|
-
|
|
1443
|
+
handleDoubleExit();
|
|
1333
1444
|
}
|
|
1334
|
-
}, [agentLoop]);
|
|
1445
|
+
}, [agentLoop, handleDoubleExit]);
|
|
1335
1446
|
const handleToggleThinking = useCallback(() => {
|
|
1336
1447
|
setThinkingEnabled((prev) => {
|
|
1337
1448
|
const next = !prev;
|
|
@@ -1355,40 +1466,57 @@ export function App(props) {
|
|
|
1355
1466
|
const newProvider = value.slice(0, colonIdx);
|
|
1356
1467
|
const newModelId = value.slice(colonIdx + 1);
|
|
1357
1468
|
log("INFO", "model", `Model changed`, { provider: newProvider, model: newModelId });
|
|
1358
|
-
//
|
|
1469
|
+
// Handle provider-specific tool changes when provider changes
|
|
1359
1470
|
setCurrentProvider((prevProvider) => {
|
|
1360
|
-
if (newProvider !== prevProvider
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1471
|
+
if (newProvider !== prevProvider) {
|
|
1472
|
+
// Add/remove client-side web_search tool based on provider.
|
|
1473
|
+
// Anthropic has native server-side web search; all other providers need the client tool.
|
|
1474
|
+
setCurrentTools((prev) => {
|
|
1475
|
+
const hasWebSearch = prev.some((t) => t.name === "web_search");
|
|
1476
|
+
if (newProvider === "anthropic" && hasWebSearch) {
|
|
1477
|
+
// Switching TO anthropic — remove client-side web_search (server-side handles it)
|
|
1478
|
+
return prev.filter((t) => t.name !== "web_search");
|
|
1479
|
+
}
|
|
1480
|
+
else if (newProvider !== "anthropic" && !hasWebSearch) {
|
|
1481
|
+
// Switching FROM anthropic — add client-side web_search
|
|
1482
|
+
return [...prev, createWebSearchTool()];
|
|
1483
|
+
}
|
|
1484
|
+
return prev;
|
|
1485
|
+
});
|
|
1486
|
+
// Reconnect MCP servers
|
|
1487
|
+
if (props.mcpManager) {
|
|
1488
|
+
void (async () => {
|
|
1489
|
+
// Disconnect old MCP servers
|
|
1490
|
+
await props.mcpManager.dispose();
|
|
1491
|
+
// Remove old MCP tools, connect new ones
|
|
1492
|
+
let apiKey;
|
|
1493
|
+
if (newProvider === "glm" && props.authStorage) {
|
|
1494
|
+
try {
|
|
1495
|
+
const glmCreds = await props.authStorage.resolveCredentials("glm");
|
|
1496
|
+
apiKey = glmCreds.accessToken;
|
|
1497
|
+
}
|
|
1498
|
+
catch {
|
|
1499
|
+
// GLM not configured — skip Z.AI MCP servers
|
|
1500
|
+
}
|
|
1501
|
+
}
|
|
1502
|
+
else if (newProvider === "glm") {
|
|
1503
|
+
apiKey = props.credentialsByProvider?.["glm"]?.accessToken;
|
|
1504
|
+
}
|
|
1367
1505
|
try {
|
|
1368
|
-
const
|
|
1369
|
-
|
|
1506
|
+
const mcpTools = await props.mcpManager.connectAll(getMCPServers(newProvider, apiKey));
|
|
1507
|
+
setCurrentTools((prev) => [
|
|
1508
|
+
...prev.filter((t) => !t.name.startsWith("mcp__")),
|
|
1509
|
+
...mcpTools,
|
|
1510
|
+
]);
|
|
1511
|
+
log("INFO", "mcp", `MCP servers reconnected for provider ${newProvider}`);
|
|
1370
1512
|
}
|
|
1371
|
-
catch {
|
|
1372
|
-
|
|
1513
|
+
catch (err) {
|
|
1514
|
+
log("WARN", "mcp", `MCP reconnection failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
1515
|
+
// Still remove old MCP tools even if reconnection fails
|
|
1516
|
+
setCurrentTools((prev) => prev.filter((t) => !t.name.startsWith("mcp__")));
|
|
1373
1517
|
}
|
|
1374
|
-
}
|
|
1375
|
-
|
|
1376
|
-
apiKey = props.credentialsByProvider?.["glm"]?.accessToken;
|
|
1377
|
-
}
|
|
1378
|
-
try {
|
|
1379
|
-
const mcpTools = await props.mcpManager.connectAll(getMCPServers(newProvider, apiKey));
|
|
1380
|
-
setCurrentTools((prev) => [
|
|
1381
|
-
...prev.filter((t) => !t.name.startsWith("mcp__")),
|
|
1382
|
-
...mcpTools,
|
|
1383
|
-
]);
|
|
1384
|
-
log("INFO", "mcp", `MCP servers reconnected for provider ${newProvider}`);
|
|
1385
|
-
}
|
|
1386
|
-
catch (err) {
|
|
1387
|
-
log("WARN", "mcp", `MCP reconnection failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
1388
|
-
// Still remove old MCP tools even if reconnection fails
|
|
1389
|
-
setCurrentTools((prev) => prev.filter((t) => !t.name.startsWith("mcp__")));
|
|
1390
|
-
}
|
|
1391
|
-
})();
|
|
1518
|
+
})();
|
|
1519
|
+
}
|
|
1392
1520
|
}
|
|
1393
1521
|
return newProvider;
|
|
1394
1522
|
});
|
|
@@ -1408,12 +1536,28 @@ export function App(props) {
|
|
|
1408
1536
|
});
|
|
1409
1537
|
}
|
|
1410
1538
|
}, [props.settingsFile, props.mcpManager, props.credentialsByProvider, props.authStorage]);
|
|
1539
|
+
const handleThemeSelect = useCallback((name) => {
|
|
1540
|
+
setOverlay(null);
|
|
1541
|
+
if (switchTheme) {
|
|
1542
|
+
switchTheme(name);
|
|
1543
|
+
}
|
|
1544
|
+
// Persist to settings
|
|
1545
|
+
if (props.settingsFile) {
|
|
1546
|
+
const sm = new SettingsManager(props.settingsFile);
|
|
1547
|
+
sm.load().then(() => sm.set("theme", name));
|
|
1548
|
+
}
|
|
1549
|
+
setLiveItems((prev) => [
|
|
1550
|
+
...prev,
|
|
1551
|
+
{ kind: "info", text: `Theme switched to: ${name}`, id: getId() },
|
|
1552
|
+
]);
|
|
1553
|
+
}, [switchTheme, props.settingsFile]);
|
|
1411
1554
|
// All available slash commands for the command palette
|
|
1412
1555
|
const allCommands = useMemo(() => [
|
|
1413
1556
|
{ name: "model", aliases: ["m"], description: "Switch model" },
|
|
1414
1557
|
{ name: "compact", aliases: ["c"], description: "Compact conversation" },
|
|
1415
1558
|
{ name: "clear", aliases: [], description: "Clear session and terminal" },
|
|
1416
1559
|
{ name: "quit", aliases: ["q", "exit"], description: "Exit the agent" },
|
|
1560
|
+
{ name: "theme", aliases: ["t"], description: "Switch theme" },
|
|
1417
1561
|
{ name: "plan", aliases: [], description: "Toggle plan mode (on/off)" },
|
|
1418
1562
|
{ name: "plans", aliases: [], description: "Open plans pane" },
|
|
1419
1563
|
...PROMPT_COMMANDS.map((cmd) => ({
|
|
@@ -1432,15 +1576,15 @@ export function App(props) {
|
|
|
1432
1576
|
case "tombstone":
|
|
1433
1577
|
return null;
|
|
1434
1578
|
case "banner":
|
|
1435
|
-
return (_jsx(Banner, { version: props.version, model:
|
|
1579
|
+
return (_jsx(Banner, { version: props.version, model: currentModel, provider: currentProvider, cwd: props.cwd, taskCount: taskCount }, item.id));
|
|
1436
1580
|
case "user":
|
|
1437
1581
|
return (_jsx(UserMessage, { text: item.text, imageCount: item.imageCount, pasteInfo: item.pasteInfo }, item.id));
|
|
1438
1582
|
case "task":
|
|
1439
1583
|
return (_jsx(Box, { marginTop: 1, children: _jsxs(Text, { wrap: "wrap", children: [_jsx(Text, { color: theme.success, bold: true, children: "▶ " }), _jsx(Text, { color: theme.textDim, children: "Task: " }), _jsx(Text, { color: theme.success, children: item.title })] }) }, item.id));
|
|
1440
1584
|
case "assistant":
|
|
1441
|
-
return (_jsx(AssistantMessage, { text: item.text, thinking: item.thinking, thinkingMs: item.thinkingMs, showThinking: props.showThinking }, item.id));
|
|
1585
|
+
return (_jsx(AssistantMessage, { text: item.text, thinking: item.thinking, thinkingMs: item.thinkingMs, showThinking: props.showThinking, planMode: item.planMode }, item.id));
|
|
1442
1586
|
case "tool_start":
|
|
1443
|
-
return _jsx(ToolExecution, { status: "running", name: item.name, args: item.args }, item.id);
|
|
1587
|
+
return (_jsx(ToolExecution, { status: "running", name: item.name, args: item.args, progressOutput: item.progressOutput }, item.id));
|
|
1444
1588
|
case "tool_done":
|
|
1445
1589
|
return (_jsx(ToolExecution, { status: "done", name: item.name, args: item.args, result: item.result, isError: item.isError, details: item.details }, item.id));
|
|
1446
1590
|
case "tool_group":
|
|
@@ -1456,7 +1600,7 @@ export function App(props) {
|
|
|
1456
1600
|
case "info":
|
|
1457
1601
|
return (_jsx(Box, { marginTop: 1, flexShrink: 1, children: _jsx(Text, { color: theme.textDim, wrap: "wrap", children: item.text }) }, item.id));
|
|
1458
1602
|
case "plan_transition":
|
|
1459
|
-
return (_jsx(Box, { marginTop: 1, flexShrink: 1, children: _jsxs(Text, { color: theme.planPrimary, bold: true, wrap: "wrap", children: [item.active ? "
|
|
1603
|
+
return (_jsx(Box, { marginTop: 1, flexShrink: 1, children: _jsxs(Text, { color: theme.planPrimary, bold: true, wrap: "wrap", children: [item.active ? "● " : "● ", item.text] }) }, item.id));
|
|
1460
1604
|
case "queued":
|
|
1461
1605
|
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
|
|
1462
1606
|
? ` (+${item.imageCount} image${item.imageCount > 1 ? "s" : ""})`
|
|
@@ -1547,11 +1691,17 @@ export function App(props) {
|
|
|
1547
1691
|
setStaticKey((k) => k + 1);
|
|
1548
1692
|
setOverlay(null);
|
|
1549
1693
|
} })) : isPlanView ? (_jsx(PlanOverlay, { cwd: props.cwd, autoExpandNewest: planAutoExpand, onClose: () => {
|
|
1694
|
+
planOverlayPendingRef.current = false;
|
|
1550
1695
|
stdout?.write("\x1b[2J\x1b[3J\x1b[H");
|
|
1551
1696
|
setStaticKey((k) => k + 1);
|
|
1552
1697
|
setPlanAutoExpand(false);
|
|
1553
1698
|
setOverlay(null);
|
|
1554
1699
|
}, onApprove: (planPath) => {
|
|
1700
|
+
log("INFO", "plan", "Plan approved — transitioning to implementation", {
|
|
1701
|
+
planPath,
|
|
1702
|
+
});
|
|
1703
|
+
// Plan overlay dismissed — allow future onDone to fire normally
|
|
1704
|
+
planOverlayPendingRef.current = false;
|
|
1555
1705
|
// Store approved plan path — will be injected into the new system prompt
|
|
1556
1706
|
approvedPlanPathRef.current = planPath;
|
|
1557
1707
|
// Extract plan steps for progress tracking
|
|
@@ -1569,28 +1719,36 @@ export function App(props) {
|
|
|
1569
1719
|
setOverlay(null);
|
|
1570
1720
|
// Rebuild system prompt with the approved plan, then reset the session
|
|
1571
1721
|
void (async () => {
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1722
|
+
try {
|
|
1723
|
+
const newPrompt = await buildSystemPrompt(props.cwd, props.skills, false, planPath);
|
|
1724
|
+
messagesRef.current = [{ role: "system", content: newPrompt }];
|
|
1725
|
+
agentLoop.reset();
|
|
1726
|
+
persistedIndexRef.current = messagesRef.current.length;
|
|
1727
|
+
// Create a new session file
|
|
1728
|
+
const sm = sessionManagerRef.current;
|
|
1729
|
+
if (sm) {
|
|
1730
|
+
const s = await sm.create(props.cwd, currentProvider, currentModel);
|
|
1731
|
+
sessionPathRef.current = s.path;
|
|
1732
|
+
}
|
|
1733
|
+
// Start implementation with a clean context
|
|
1734
|
+
setLiveItems([
|
|
1735
|
+
{
|
|
1736
|
+
kind: "info",
|
|
1737
|
+
text: "Plan approved — starting fresh session for implementation",
|
|
1738
|
+
id: getId(),
|
|
1739
|
+
},
|
|
1740
|
+
]);
|
|
1741
|
+
setDoneStatus(null);
|
|
1742
|
+
await agentLoop.run("The plan has been approved. Implement it now, following each step in order.");
|
|
1743
|
+
}
|
|
1744
|
+
catch (err) {
|
|
1745
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
1746
|
+
log("ERROR", "error", errMsg);
|
|
1747
|
+
setLiveItems((prev) => [...prev, { kind: "error", message: errMsg, id: getId() }]);
|
|
1581
1748
|
}
|
|
1582
|
-
// Start implementation with a clean context
|
|
1583
|
-
setLiveItems([
|
|
1584
|
-
{
|
|
1585
|
-
kind: "info",
|
|
1586
|
-
text: "Plan approved — starting fresh session for implementation",
|
|
1587
|
-
id: getId(),
|
|
1588
|
-
},
|
|
1589
|
-
]);
|
|
1590
|
-
setDoneStatus(null);
|
|
1591
|
-
await agentLoop.run("The plan has been approved. Implement it now, following each step in order.");
|
|
1592
1749
|
})();
|
|
1593
1750
|
}, onReject: (planPath, feedback) => {
|
|
1751
|
+
planOverlayPendingRef.current = false;
|
|
1594
1752
|
stdout?.write("\x1b[2J\x1b[3J\x1b[H");
|
|
1595
1753
|
setStaticKey((k) => k + 1);
|
|
1596
1754
|
setPlanAutoExpand(false);
|
|
@@ -1603,10 +1761,15 @@ export function App(props) {
|
|
|
1603
1761
|
...prev,
|
|
1604
1762
|
{ kind: "info", text: `Plan rejected — "${feedback}"`, id: getId() },
|
|
1605
1763
|
]);
|
|
1606
|
-
void agentLoop.run(msg)
|
|
1764
|
+
void agentLoop.run(msg).catch((err) => {
|
|
1765
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
1766
|
+
log("ERROR", "error", errMsg);
|
|
1767
|
+
setLiveItems((prev) => [...prev, { kind: "error", message: errMsg, id: getId() }]);
|
|
1768
|
+
});
|
|
1607
1769
|
} })) : (_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, showThinking: props.showThinking, thinkingMs: agentLoop.thinkingMs, planMode: planMode })] }), agentLoop.isRunning && agentLoop.activityPhase !== "idle" ? (_jsx(Box, { marginTop: 1, borderStyle: "round", borderColor: agentLoop.activityPhase === "thinking"
|
|
1608
1770
|
? THINKING_BORDER_COLORS[thinkingBorderFrame]
|
|
1609
|
-
: "transparent", paddingLeft: 1, paddingRight: 1, width: columns, children: _jsx(ActivityIndicator, { phase: agentLoop.activityPhase, elapsedMs: agentLoop.elapsedMs, 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 }) })) :
|
|
1771
|
+
: "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 &&
|
|
1772
|
+
!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: () => {
|
|
1610
1773
|
stdout?.write("\x1b[2J\x1b[3J\x1b[H");
|
|
1611
1774
|
setOverlay("tasks");
|
|
1612
1775
|
}, onToggleSkills: () => {
|
|
@@ -1625,6 +1788,6 @@ export function App(props) {
|
|
|
1625
1788
|
id: getId(),
|
|
1626
1789
|
},
|
|
1627
1790
|
]);
|
|
1628
|
-
}, cwd: props.cwd, commands: allCommands }), overlay === "model" ? (_jsx(ModelSelector, { onSelect: handleModelSelect, onCancel: () => setOverlay(null), loggedInProviders: props.loggedInProviders ?? [currentProvider], currentModel: currentModel, currentProvider: currentProvider })) : (_jsx(
|
|
1791
|
+
}, cwd: props.cwd, commands: allCommands }), overlay === "model" ? (_jsx(ModelSelector, { onSelect: handleModelSelect, onCancel: () => setOverlay(null), loggedInProviders: props.loggedInProviders ?? [currentProvider], currentModel: currentModel, currentProvider: currentProvider })) : overlay === "theme" ? (_jsx(ThemeSelector, { onSelect: handleThemeSelect, onCancel: () => setOverlay(null), currentTheme: theme.name })) : (_jsx(Footer, { model: currentModel, tokensIn: agentLoop.contextUsed, cwd: props.cwd, gitBranch: gitBranch, thinkingEnabled: thinkingEnabled, planMode: planMode, exitPending: exitPending })), buddyEnabled && _jsx(Buddy, { phase: agentLoop.activityPhase }), bgTasks.length > 0 && (_jsx(BackgroundTasksBar, { tasks: bgTasks, focused: taskBarFocused, expanded: taskBarExpanded, selectedIndex: selectedTaskIndex, onExpand: handleTaskBarExpand, onCollapse: handleTaskBarCollapse, onKill: handleTaskKill, onExit: handleTaskBarExit, onNavigate: handleTaskNavigate }))] }))] }));
|
|
1629
1792
|
}
|
|
1630
1793
|
//# sourceMappingURL=App.js.map
|