@prestyj/cli 4.3.161 → 4.3.200
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.d.ts +1 -1
- package/dist/cli.js +112 -27
- package/dist/cli.js.map +1 -1
- package/dist/core/agent-session.d.ts +50 -1
- package/dist/core/agent-session.d.ts.map +1 -1
- package/dist/core/agent-session.js +175 -20
- package/dist/core/agent-session.js.map +1 -1
- package/dist/core/agents.d.ts +6 -1
- package/dist/core/agents.d.ts.map +1 -1
- package/dist/core/agents.js +112 -3
- package/dist/core/agents.js.map +1 -1
- package/dist/core/auth-storage.d.ts.map +1 -1
- package/dist/core/auth-storage.js +20 -1
- package/dist/core/auth-storage.js.map +1 -1
- package/dist/core/claude-code-version.d.ts +10 -0
- package/dist/core/claude-code-version.d.ts.map +1 -0
- package/dist/core/claude-code-version.js +106 -0
- package/dist/core/claude-code-version.js.map +1 -0
- package/dist/core/compaction/compactor.d.ts +13 -0
- package/dist/core/compaction/compactor.d.ts.map +1 -1
- package/dist/core/compaction/compactor.js +20 -1
- package/dist/core/compaction/compactor.js.map +1 -1
- package/dist/core/compaction/compactor.test.js +12 -1
- package/dist/core/compaction/compactor.test.js.map +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/language-detector.d.ts +32 -0
- package/dist/core/language-detector.d.ts.map +1 -0
- package/dist/core/language-detector.js +191 -0
- package/dist/core/language-detector.js.map +1 -0
- package/dist/core/language-detector.test.d.ts +2 -0
- package/dist/core/language-detector.test.d.ts.map +1 -0
- package/dist/core/language-detector.test.js +109 -0
- package/dist/core/language-detector.test.js.map +1 -0
- package/dist/core/model-registry.d.ts +30 -2
- package/dist/core/model-registry.d.ts.map +1 -1
- package/dist/core/model-registry.js +41 -26
- package/dist/core/model-registry.js.map +1 -1
- package/dist/core/model-registry.test.d.ts +2 -0
- package/dist/core/model-registry.test.d.ts.map +1 -0
- package/dist/core/model-registry.test.js +19 -0
- package/dist/core/model-registry.test.js.map +1 -0
- package/dist/core/oauth/anthropic.d.ts.map +1 -1
- package/dist/core/oauth/anthropic.js +64 -42
- package/dist/core/oauth/anthropic.js.map +1 -1
- package/dist/core/oauth/openai.js +1 -1
- package/dist/core/oauth/openai.js.map +1 -1
- package/dist/core/prompt-commands.d.ts.map +1 -1
- package/dist/core/prompt-commands.js +569 -96
- package/dist/core/prompt-commands.js.map +1 -1
- package/dist/core/prompt-commands.test.d.ts +2 -0
- package/dist/core/prompt-commands.test.d.ts.map +1 -0
- package/dist/core/prompt-commands.test.js +33 -0
- package/dist/core/prompt-commands.test.js.map +1 -0
- package/dist/core/repomap-context.d.ts +11 -0
- package/dist/core/repomap-context.d.ts.map +1 -0
- package/dist/core/repomap-context.js +68 -0
- package/dist/core/repomap-context.js.map +1 -0
- package/dist/core/repomap-context.test.d.ts +2 -0
- package/dist/core/repomap-context.test.d.ts.map +1 -0
- package/dist/core/repomap-context.test.js +47 -0
- package/dist/core/repomap-context.test.js.map +1 -0
- package/dist/core/repomap.d.ts +74 -0
- package/dist/core/repomap.d.ts.map +1 -0
- package/dist/core/repomap.js +897 -0
- package/dist/core/repomap.js.map +1 -0
- package/dist/core/repomap.test.d.ts +2 -0
- package/dist/core/repomap.test.d.ts.map +1 -0
- package/dist/core/repomap.test.js +444 -0
- package/dist/core/repomap.test.js.map +1 -0
- package/dist/core/session-compaction.d.ts +15 -0
- package/dist/core/session-compaction.d.ts.map +1 -0
- package/dist/core/session-compaction.js +31 -0
- package/dist/core/session-compaction.js.map +1 -0
- package/dist/core/session-compaction.test.d.ts +2 -0
- package/dist/core/session-compaction.test.d.ts.map +1 -0
- package/dist/core/session-compaction.test.js +70 -0
- package/dist/core/session-compaction.test.js.map +1 -0
- package/dist/core/settings-manager.d.ts +3 -5
- package/dist/core/settings-manager.d.ts.map +1 -1
- package/dist/core/settings-manager.js +6 -5
- package/dist/core/settings-manager.js.map +1 -1
- package/dist/core/setup-history.d.ts +13 -0
- package/dist/core/setup-history.d.ts.map +1 -0
- package/dist/core/setup-history.js +71 -0
- package/dist/core/setup-history.js.map +1 -0
- package/dist/core/setup-history.test.d.ts +2 -0
- package/dist/core/setup-history.test.d.ts.map +1 -0
- package/dist/core/setup-history.test.js +74 -0
- package/dist/core/setup-history.test.js.map +1 -0
- package/dist/core/slash-commands.d.ts +2 -0
- package/dist/core/slash-commands.d.ts.map +1 -1
- package/dist/core/slash-commands.js +15 -14
- package/dist/core/slash-commands.js.map +1 -1
- package/dist/core/style-packs/index.d.ts +19 -0
- package/dist/core/style-packs/index.d.ts.map +1 -0
- package/dist/core/style-packs/index.js +69 -0
- package/dist/core/style-packs/index.js.map +1 -0
- package/dist/core/style-packs/packs.d.ts +18 -0
- package/dist/core/style-packs/packs.d.ts.map +1 -0
- package/dist/core/style-packs/packs.js +229 -0
- package/dist/core/style-packs/packs.js.map +1 -0
- package/dist/core/verify-commands.d.ts +35 -0
- package/dist/core/verify-commands.d.ts.map +1 -0
- package/dist/core/verify-commands.js +209 -0
- package/dist/core/verify-commands.js.map +1 -0
- package/dist/core/verify-commands.test.d.ts +2 -0
- package/dist/core/verify-commands.test.d.ts.map +1 -0
- package/dist/core/verify-commands.test.js +85 -0
- package/dist/core/verify-commands.test.js.map +1 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/interactive.d.ts.map +1 -1
- package/dist/interactive.js +12 -6
- package/dist/interactive.js.map +1 -1
- package/dist/modes/json-mode.d.ts +7 -0
- package/dist/modes/json-mode.d.ts.map +1 -1
- package/dist/modes/json-mode.js +9 -1
- package/dist/modes/json-mode.js.map +1 -1
- package/dist/system-prompt.d.ts +2 -1
- package/dist/system-prompt.d.ts.map +1 -1
- package/dist/system-prompt.js +116 -92
- package/dist/system-prompt.js.map +1 -1
- package/dist/system-prompt.test.d.ts +2 -0
- package/dist/system-prompt.test.d.ts.map +1 -0
- package/dist/system-prompt.test.js +107 -0
- package/dist/system-prompt.test.js.map +1 -0
- package/dist/tools/bash.d.ts.map +1 -1
- package/dist/tools/bash.js +1 -0
- package/dist/tools/bash.js.map +1 -1
- package/dist/tools/edit-diff.d.ts +66 -1
- package/dist/tools/edit-diff.d.ts.map +1 -1
- package/dist/tools/edit-diff.js +208 -10
- package/dist/tools/edit-diff.js.map +1 -1
- package/dist/tools/edit-diff.test.js +175 -15
- package/dist/tools/edit-diff.test.js.map +1 -1
- package/dist/tools/edit.d.ts +3 -1
- package/dist/tools/edit.d.ts.map +1 -1
- package/dist/tools/edit.js +190 -44
- package/dist/tools/edit.js.map +1 -1
- package/dist/tools/edit.test.js +392 -3
- package/dist/tools/edit.test.js.map +1 -1
- package/dist/tools/enter-plan.d.ts.map +1 -1
- package/dist/tools/enter-plan.js +1 -0
- package/dist/tools/enter-plan.js.map +1 -1
- package/dist/tools/exit-plan.d.ts.map +1 -1
- package/dist/tools/exit-plan.js +1 -0
- package/dist/tools/exit-plan.js.map +1 -1
- package/dist/tools/index.d.ts +13 -0
- package/dist/tools/index.d.ts.map +1 -1
- package/dist/tools/index.js +4 -4
- package/dist/tools/index.js.map +1 -1
- package/dist/tools/prompt-hints.d.ts.map +1 -1
- package/dist/tools/prompt-hints.js +6 -4
- package/dist/tools/prompt-hints.js.map +1 -1
- package/dist/tools/read.d.ts +1 -1
- package/dist/tools/read.d.ts.map +1 -1
- package/dist/tools/read.js +2 -1
- package/dist/tools/read.js.map +1 -1
- package/dist/tools/subagent.d.ts +1 -1
- package/dist/tools/subagent.d.ts.map +1 -1
- package/dist/tools/subagent.js +55 -2
- package/dist/tools/subagent.js.map +1 -1
- package/dist/tools/tasks.d.ts.map +1 -1
- package/dist/tools/tasks.js +1 -0
- package/dist/tools/tasks.js.map +1 -1
- package/dist/tools/web-fetch.d.ts +1 -0
- package/dist/tools/web-fetch.d.ts.map +1 -1
- package/dist/tools/web-fetch.js +96 -6
- package/dist/tools/web-fetch.js.map +1 -1
- package/dist/tools/web-fetch.test.d.ts +2 -0
- package/dist/tools/web-fetch.test.d.ts.map +1 -0
- package/dist/tools/web-fetch.test.js +90 -0
- package/dist/tools/web-fetch.test.js.map +1 -0
- package/dist/tools/web-search.d.ts +1 -0
- package/dist/tools/web-search.d.ts.map +1 -1
- package/dist/tools/web-search.js +167 -27
- package/dist/tools/web-search.js.map +1 -1
- package/dist/tools/web-search.test.d.ts +2 -0
- package/dist/tools/web-search.test.d.ts.map +1 -0
- package/dist/tools/web-search.test.js +119 -0
- package/dist/tools/web-search.test.js.map +1 -0
- package/dist/tools/write.d.ts +1 -1
- package/dist/tools/write.d.ts.map +1 -1
- package/dist/tools/write.js +3 -1
- package/dist/tools/write.js.map +1 -1
- package/dist/tools/write.test.js +23 -0
- package/dist/tools/write.test.js.map +1 -1
- package/dist/ui/App.d.ts +74 -3
- package/dist/ui/App.d.ts.map +1 -1
- package/dist/ui/App.js +727 -223
- package/dist/ui/App.js.map +1 -1
- package/dist/ui/activity-phrases.d.ts +2 -1
- package/dist/ui/activity-phrases.d.ts.map +1 -1
- package/dist/ui/activity-phrases.js +11 -1
- package/dist/ui/activity-phrases.js.map +1 -1
- package/dist/ui/components/ActivityIndicator.d.ts +15 -1
- package/dist/ui/components/ActivityIndicator.d.ts.map +1 -1
- package/dist/ui/components/ActivityIndicator.js +143 -44
- package/dist/ui/components/ActivityIndicator.js.map +1 -1
- package/dist/ui/components/ActivityIndicator.test.d.ts +2 -0
- package/dist/ui/components/ActivityIndicator.test.d.ts.map +1 -0
- package/dist/ui/components/ActivityIndicator.test.js +40 -0
- package/dist/ui/components/ActivityIndicator.test.js.map +1 -0
- package/dist/ui/components/AnimationContext.d.ts +8 -2
- package/dist/ui/components/AnimationContext.d.ts.map +1 -1
- package/dist/ui/components/AnimationContext.js +94 -16
- package/dist/ui/components/AnimationContext.js.map +1 -1
- package/dist/ui/components/AssistantMessage.js +1 -1
- package/dist/ui/components/AssistantMessage.js.map +1 -1
- package/dist/ui/components/CompactionNotice.d.ts +5 -1
- package/dist/ui/components/CompactionNotice.d.ts.map +1 -1
- package/dist/ui/components/CompactionNotice.js +4 -6
- package/dist/ui/components/CompactionNotice.js.map +1 -1
- package/dist/ui/components/Footer.d.ts +10 -2
- package/dist/ui/components/Footer.d.ts.map +1 -1
- package/dist/ui/components/Footer.js +45 -8
- 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 +76 -28
- package/dist/ui/components/InputArea.js.map +1 -1
- package/dist/ui/components/ServerToolExecution.d.ts +1 -0
- package/dist/ui/components/ServerToolExecution.d.ts.map +1 -1
- package/dist/ui/components/ServerToolExecution.js +21 -2
- package/dist/ui/components/ServerToolExecution.js.map +1 -1
- package/dist/ui/components/Spinner.d.ts +3 -1
- package/dist/ui/components/Spinner.d.ts.map +1 -1
- package/dist/ui/components/Spinner.js +10 -4
- package/dist/ui/components/Spinner.js.map +1 -1
- package/dist/ui/components/StreamingArea.js +1 -1
- 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 +3 -4
- package/dist/ui/components/SubAgentPanel.js.map +1 -1
- 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 +24 -5
- package/dist/ui/components/ToolExecution.js.map +1 -1
- package/dist/ui/components/ToolGroupExecution.d.ts.map +1 -1
- package/dist/ui/components/ToolGroupExecution.js +23 -2
- package/dist/ui/components/ToolGroupExecution.js.map +1 -1
- package/dist/ui/components/ToolUseLoader.d.ts +4 -2
- package/dist/ui/components/ToolUseLoader.d.ts.map +1 -1
- package/dist/ui/components/ToolUseLoader.js +3 -3
- package/dist/ui/components/ToolUseLoader.js.map +1 -1
- package/dist/ui/components/index.d.ts +1 -2
- package/dist/ui/components/index.d.ts.map +1 -1
- package/dist/ui/components/index.js +1 -2
- package/dist/ui/components/index.js.map +1 -1
- package/dist/ui/hooks/useAgentLoop.d.ts +4 -1
- package/dist/ui/hooks/useAgentLoop.d.ts.map +1 -1
- package/dist/ui/hooks/useAgentLoop.js +110 -13
- package/dist/ui/hooks/useAgentLoop.js.map +1 -1
- package/dist/ui/hooks/useBlink.d.ts.map +1 -1
- package/dist/ui/hooks/useBlink.js +3 -4
- package/dist/ui/hooks/useBlink.js.map +1 -1
- package/dist/ui/hooks/useTerminalSize.d.ts +12 -3
- package/dist/ui/hooks/useTerminalSize.d.ts.map +1 -1
- package/dist/ui/hooks/useTerminalSize.js +16 -1
- package/dist/ui/hooks/useTerminalSize.js.map +1 -1
- package/dist/ui/pixel.js +1 -1
- package/dist/ui/pixel.test.js +1 -1
- package/dist/ui/pixel.test.js.map +1 -1
- package/dist/ui/render.d.ts +48 -1
- package/dist/ui/render.d.ts.map +1 -1
- package/dist/ui/render.js +56 -6
- package/dist/ui/render.js.map +1 -1
- package/dist/ui/theme/dark-ansi.json +2 -1
- package/dist/ui/theme/dark-daltonized.json +2 -1
- package/dist/ui/theme/dark.json +2 -1
- package/dist/ui/theme/light-ansi.json +2 -1
- package/dist/ui/theme/light-daltonized.json +2 -1
- package/dist/ui/theme/light.json +2 -1
- package/dist/ui/theme/theme.d.ts +1 -0
- package/dist/ui/theme/theme.d.ts.map +1 -1
- package/dist/utils/error-handler.d.ts.map +1 -1
- package/dist/utils/error-handler.js +2 -0
- package/dist/utils/error-handler.js.map +1 -1
- package/package.json +9 -6
- package/dist/ui/buddy/Buddy.d.ts +0 -15
- package/dist/ui/buddy/Buddy.d.ts.map +0 -1
- package/dist/ui/buddy/Buddy.js +0 -118
- package/dist/ui/buddy/Buddy.js.map +0 -1
- package/dist/ui/buddy/gacha.d.ts +0 -8
- package/dist/ui/buddy/gacha.d.ts.map +0 -1
- package/dist/ui/buddy/gacha.js +0 -94
- package/dist/ui/buddy/gacha.js.map +0 -1
- package/dist/ui/buddy/species.d.ts +0 -52
- package/dist/ui/buddy/species.d.ts.map +0 -1
- package/dist/ui/buddy/species.js +0 -257
- package/dist/ui/buddy/species.js.map +0 -1
- package/dist/ui/components/ThinkingIndicator.d.ts +0 -6
- package/dist/ui/components/ThinkingIndicator.d.ts.map +0 -1
- package/dist/ui/components/ThinkingIndicator.js +0 -145
- package/dist/ui/components/ThinkingIndicator.js.map +0 -1
package/dist/ui/App.js
CHANGED
|
@@ -4,11 +4,12 @@ import { Box, Text, Static, useStdout } 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";
|
|
7
|
-
import
|
|
7
|
+
import { createHash } from "node:crypto";
|
|
8
8
|
import { readFileSync, writeFileSync } from "node:fs";
|
|
9
9
|
import { homedir } from "node:os";
|
|
10
10
|
import { join } from "node:path";
|
|
11
11
|
import { playNotificationSound } from "../utils/sound.js";
|
|
12
|
+
import { formatError, } from "@prestyj/ai";
|
|
12
13
|
import { extractImagePaths } from "../utils/image.js";
|
|
13
14
|
import { useAgentLoop } from "./hooks/useAgentLoop.js";
|
|
14
15
|
import { isEyesActive, journalCount } from "@prestyj/eyes";
|
|
@@ -34,63 +35,64 @@ import { EyesOverlay } from "./components/EyesOverlay.js";
|
|
|
34
35
|
import { ThemeSelector } from "./components/ThemeSelector.js";
|
|
35
36
|
import { BackgroundTasksBar } from "./components/BackgroundTasksBar.js";
|
|
36
37
|
import { useTheme, useSetTheme } from "./theme/theme.js";
|
|
37
|
-
import { useAnimationTick, useAnimationActive, deriveFrame, } from "./components/AnimationContext.js";
|
|
38
38
|
import { useTerminalTitle } from "./hooks/useTerminalTitle.js";
|
|
39
39
|
import { getGitBranch } from "../utils/git.js";
|
|
40
|
-
import { getModel, getContextWindow } from "../core/model-registry.js";
|
|
40
|
+
import { getModel, getContextWindow, getMaxThinkingLevel } from "../core/model-registry.js";
|
|
41
41
|
import { SessionManager } from "../core/session-manager.js";
|
|
42
|
+
import { appendMessagesToSession as appendSessionMessages, createCompactedSessionCheckpoint, } from "../core/session-compaction.js";
|
|
42
43
|
import { log } from "../core/logger.js";
|
|
43
44
|
import { getPendingUpdate, startPeriodicUpdateCheck, stopPeriodicUpdateCheck, } from "../core/auto-update.js";
|
|
44
45
|
import { generateSessionTitle } from "../utils/session-title.js";
|
|
45
46
|
import { SettingsManager } from "../core/settings-manager.js";
|
|
46
|
-
import { shouldCompact, compact } from "../core/compaction/compactor.js";
|
|
47
|
+
import { shouldCompact, compact, getCompactionReserveTokens, } from "../core/compaction/compactor.js";
|
|
47
48
|
import { estimateConversationTokens } from "../core/compaction/token-estimator.js";
|
|
48
49
|
import { PROMPT_COMMANDS, getPromptCommand } from "../core/prompt-commands.js";
|
|
50
|
+
import { isFirstTimeSetup, markSetupAudited, getAnnouncedLanguages, markLanguagesAnnounced, } from "../core/setup-history.js";
|
|
49
51
|
import { loadCustomCommands } from "../core/custom-commands.js";
|
|
50
52
|
import { buildSystemPrompt } from "../system-prompt.js";
|
|
53
|
+
import { detectLanguages, LANGUAGE_DISPLAY_NAMES, } from "../core/language-detector.js";
|
|
54
|
+
import { detectVerifyCommands } from "../core/verify-commands.js";
|
|
55
|
+
import { FOCUSED_REPO_MAP_MAX_CHARS, FIRST_TURN_REPO_MAP_MAX_CHARS, buildRepoMap, createRepoMapCache, } from "../core/repomap.js";
|
|
56
|
+
import { getLatestUserText, injectRepoMapContextMessages, stripRepoMapContextMessages, } from "../core/repomap-context.js";
|
|
51
57
|
import { extractPlanSteps, findCompletedMarkers, markStepsCompleted, segmentDisplayText, stripDoneMarkers, } from "../utils/plan-steps.js";
|
|
52
58
|
import { getMCPServers } from "../core/mcp/index.js";
|
|
53
59
|
import { trimFlushedItems, flushOnTurnText, flushOnTurnEnd, flushOverflow, } from "./live-item-flush.js";
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
/**
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
(lower.includes("model") &&
|
|
87
|
-
(lower.includes("not exist") || lower.includes("not found") || lower.includes("no access")))) {
|
|
88
|
-
return "Use /model to switch to a different model, or check that your account has access to the requested model.";
|
|
89
|
-
}
|
|
90
|
-
return null;
|
|
60
|
+
/** Where ezcoder bugs should be reported. Surfaced in the guidance line. */
|
|
61
|
+
const GGCODER_BUG_REPORT_URL = "github.com/Gahroot/ezcoder/issues";
|
|
62
|
+
/**
|
|
63
|
+
* Build an ErrorItem from any thrown value. Centralises headline / message /
|
|
64
|
+
* guidance extraction so every error answers the same question for the user:
|
|
65
|
+
* "Should I retry, or is this a ezcoder bug to report?"
|
|
66
|
+
*/
|
|
67
|
+
function toErrorItem(err, id, contextPrefix) {
|
|
68
|
+
const f = formatError(err);
|
|
69
|
+
const headline = contextPrefix ? `${contextPrefix} — ${f.headline}` : f.headline;
|
|
70
|
+
// For ezcoder bugs, swap the generic "see /help" guidance for an actual URL
|
|
71
|
+
// so users have a clear place to send the report.
|
|
72
|
+
const guidance = f.source === "ezcoder"
|
|
73
|
+
? `This looks like a ezcoder bug — please send it to the dev at ${GGCODER_BUG_REPORT_URL}.`
|
|
74
|
+
: f.guidance;
|
|
75
|
+
// Mirror every user-visible error into ~/.ezcoder/debug.log so reports can be
|
|
76
|
+
// diagnosed even after the terminal scrollback is gone.
|
|
77
|
+
log("ERROR", "ui-error", headline, {
|
|
78
|
+
source: f.source,
|
|
79
|
+
message: f.message,
|
|
80
|
+
...(f.provider ? { provider: f.provider } : {}),
|
|
81
|
+
...(f.statusCode != null ? { statusCode: String(f.statusCode) } : {}),
|
|
82
|
+
...(f.requestId ? { requestId: f.requestId } : {}),
|
|
83
|
+
...(err instanceof Error && err.stack ? { stack: err.stack } : {}),
|
|
84
|
+
});
|
|
85
|
+
return {
|
|
86
|
+
kind: "error",
|
|
87
|
+
headline,
|
|
88
|
+
message: f.message,
|
|
89
|
+
guidance,
|
|
90
|
+
id,
|
|
91
|
+
};
|
|
91
92
|
}
|
|
92
93
|
/** Tools that get aggregated into a single compact group when concurrent. */
|
|
93
94
|
const AGGREGATABLE_TOOLS = new Set(["read", "grep", "find", "ls"]);
|
|
95
|
+
const RUNNING_INDICATOR_ANIMATION_MS = 1_200;
|
|
94
96
|
/**
|
|
95
97
|
* Cap memory by replacing old items with tiny tombstones. Ink's <Static>
|
|
96
98
|
* tracks rendered items by array length — the array must never shrink, but
|
|
@@ -312,15 +314,22 @@ export function App(props) {
|
|
|
312
314
|
// Items from the current/last turn — rendered in the live area so they stay visible
|
|
313
315
|
const [liveItems, setLiveItems] = useState([]);
|
|
314
316
|
// overlay seeded from sessionStore (lives across remount). Falls back to
|
|
315
|
-
// props.initialOverlay (CLI launched with one)
|
|
316
|
-
|
|
317
|
+
// props.initialOverlay (CLI launched with one) ONLY when there's no
|
|
318
|
+
// sessionStore — otherwise sessionStore is authoritative (its initial
|
|
319
|
+
// value already incorporated initialOverlay at session creation). Without
|
|
320
|
+
// this distinction, closing an overlay that was opened via initialOverlay
|
|
321
|
+
// (e.g. `ezcoder pixel`) re-asserts the overlay on resetUI remount because
|
|
322
|
+
// `null ?? "pixel"` is `"pixel"`.
|
|
323
|
+
const [overlay, setOverlay] = useState(props.sessionStore ? (props.sessionStore.overlay ?? null) : (props.initialOverlay ?? null));
|
|
317
324
|
const [taskCount, setTaskCount] = useState(() => getTaskCount(props.cwd));
|
|
318
325
|
const [eyesCount, setEyesCount] = useState(() => isEyesActive(props.cwd) ? journalCount({ status: "open" }, props.cwd) : undefined);
|
|
319
326
|
const [updatePending, setUpdatePending] = useState(() => getPendingUpdate(props.version) !== null);
|
|
320
|
-
|
|
321
|
-
|
|
327
|
+
// Seed from sessionStore so "Run All" chaining survives the resetUI()
|
|
328
|
+
// remount that startTask() triggers between tasks.
|
|
329
|
+
const [runAllTasks, setRunAllTasks] = useState(props.sessionStore?.runAllTasks ?? false);
|
|
330
|
+
const runAllTasksRef = useRef(props.sessionStore?.runAllTasks ?? false);
|
|
322
331
|
const startTaskRef = useRef(() => { });
|
|
323
|
-
const runAllPixelRef = useRef(false);
|
|
332
|
+
const runAllPixelRef = useRef(props.sessionStore?.runAllPixel ?? false);
|
|
324
333
|
const currentPixelFixRef = useRef(null);
|
|
325
334
|
const startPixelFixRef = useRef(() => { });
|
|
326
335
|
const cwdRef = useRef(props.cwd);
|
|
@@ -333,13 +342,41 @@ export function App(props) {
|
|
|
333
342
|
const [currentModel, setCurrentModel] = useState(props.model);
|
|
334
343
|
const [currentProvider, setCurrentProvider] = useState(props.provider);
|
|
335
344
|
const [currentTools, setCurrentTools] = useState(props.tools);
|
|
345
|
+
const currentToolsRef = useRef(props.tools);
|
|
336
346
|
const [thinkingEnabled, setThinkingEnabled] = useState(!!props.thinking);
|
|
337
347
|
const messagesRef = useRef(props.sessionStore?.messages ?? props.messages);
|
|
348
|
+
const repoMapInjectionEnabledRef = useRef(true);
|
|
349
|
+
const repoMapDirtyRef = useRef(true);
|
|
350
|
+
const repoMapMarkdownRef = useRef("");
|
|
351
|
+
const repoMapSnapshotRef = useRef(undefined);
|
|
352
|
+
const repoMapChangedCountRef = useRef(0);
|
|
353
|
+
const repoMapCacheRef = useRef(createRepoMapCache());
|
|
338
354
|
const [planAutoExpand, setPlanAutoExpand] = useState(props.sessionStore?.planAutoExpand ?? false);
|
|
339
355
|
const approvedPlanPathRef = useRef(props.sessionStore?.approvedPlanPath);
|
|
340
356
|
const planStepsRef = useRef(props.sessionStore?.planSteps ?? []);
|
|
341
357
|
const [planSteps, setPlanSteps] = useState(props.sessionStore?.planSteps ?? []);
|
|
342
|
-
const
|
|
358
|
+
const planModeStateRef = useRef(planMode);
|
|
359
|
+
// Stuck-guard for the plan-continuation follow-up nudge. Tracks how many
|
|
360
|
+
// times we've nudged the agent to continue the same step. Reset whenever a
|
|
361
|
+
// new [DONE:n] marker advances progress (see onTurnText). Caps at 2 nudges
|
|
362
|
+
// so a genuinely stuck agent surfaces instead of looping forever.
|
|
363
|
+
const followUpNudgesRef = useRef({ step: 0, count: 0 });
|
|
364
|
+
// Seed the per-item ID counter so it doesn't collide with IDs already in
|
|
365
|
+
// sessionStore.history (which survives remount). Without this, a remount
|
|
366
|
+
// (resize, overlay toggle, etc.) starts the counter at 0 and new items
|
|
367
|
+
// generate ids "0", "1", "2"… that collide with the same ids from the
|
|
368
|
+
// previous mount, triggering React's duplicate-key warning and causing
|
|
369
|
+
// duplicate/omitted renders.
|
|
370
|
+
const nextIdRef = useRef((() => {
|
|
371
|
+
const hist = props.sessionStore?.history ?? props.initialHistory ?? [];
|
|
372
|
+
let max = -1;
|
|
373
|
+
for (const item of hist) {
|
|
374
|
+
const n = Number(item.id);
|
|
375
|
+
if (Number.isFinite(n) && n > max)
|
|
376
|
+
max = n;
|
|
377
|
+
}
|
|
378
|
+
return max + 1;
|
|
379
|
+
})());
|
|
343
380
|
const sessionManagerRef = useRef(props.sessionsDir ? new SessionManager(props.sessionsDir) : null);
|
|
344
381
|
const sessionPathRef = useRef(props.sessionStore?.sessionPath ?? props.sessionPath);
|
|
345
382
|
const persistedIndexRef = useRef(messagesRef.current.length);
|
|
@@ -349,6 +386,26 @@ export function App(props) {
|
|
|
349
386
|
const lastActualTokensTimestampRef = useRef(0);
|
|
350
387
|
/** Timestamp of last compaction — used for time-based cooldown and staleness detection. */
|
|
351
388
|
const lastCompactionTimeRef = useRef(0);
|
|
389
|
+
/**
|
|
390
|
+
* Languages whose style packs are currently injected into the system prompt.
|
|
391
|
+
* Grown by `maybeInjectLanguagePacks` after `write`/`bash` tool results when
|
|
392
|
+
* the language detector sees new marker files. Reset on `chdir` (pixel-fix).
|
|
393
|
+
* Only grows within a session; we never strip packs once injected (cheaper
|
|
394
|
+
* than invalidating prompt caching, and stale guidance is harmless).
|
|
395
|
+
*/
|
|
396
|
+
const injectedLanguagesRef = useRef(new Set());
|
|
397
|
+
/**
|
|
398
|
+
* True until the first style-pack badge is pushed. Used to gate the
|
|
399
|
+
* one-time "/setup" hint so users learn the slash command without being
|
|
400
|
+
* spammed on every subsequent pack swap.
|
|
401
|
+
*/
|
|
402
|
+
const setupHintShownRef = useRef(false);
|
|
403
|
+
/**
|
|
404
|
+
* Callback that fires `/setup` programmatically. Assigned later in the
|
|
405
|
+
* component once `agentLoop` is in scope. Called from the initial
|
|
406
|
+
* language-detection path when this cwd has never been audited before.
|
|
407
|
+
*/
|
|
408
|
+
const triggerAutoSetupRef = useRef(async () => { });
|
|
352
409
|
const getId = () => String(nextIdRef.current++);
|
|
353
410
|
// Two-phase flush: items waiting to be moved to Static history after the
|
|
354
411
|
// live area has been cleared and Ink has committed the smaller output.
|
|
@@ -372,9 +429,9 @@ export function App(props) {
|
|
|
372
429
|
}, [currentProvider, onRuntimeStateChange]);
|
|
373
430
|
useEffect(() => {
|
|
374
431
|
onRuntimeStateChange?.({
|
|
375
|
-
thinking: thinkingEnabled ? (
|
|
432
|
+
thinking: thinkingEnabled ? getMaxThinkingLevel(currentModel) : undefined,
|
|
376
433
|
});
|
|
377
|
-
}, [thinkingEnabled,
|
|
434
|
+
}, [thinkingEnabled, currentModel, onRuntimeStateChange]);
|
|
378
435
|
// Mirror session state into renderApp's closure so resetUI() can re-seed
|
|
379
436
|
// the conversation on remount. Each panel that previously did a bare ANSI
|
|
380
437
|
// screen clear (overlay open/close, plan accept/reject, /clear, startTask)
|
|
@@ -404,6 +461,7 @@ export function App(props) {
|
|
|
404
461
|
const activeApiKey = currentCreds?.accessToken ?? props.apiKey;
|
|
405
462
|
const activeAccountId = currentCreds?.accountId ?? props.accountId;
|
|
406
463
|
const activeBaseUrl = currentCreds?.baseUrl ?? props.baseUrl;
|
|
464
|
+
const contextWindowOptions = useMemo(() => ({ provider: currentProvider, accountId: activeAccountId }), [currentProvider, activeAccountId]);
|
|
407
465
|
// Load git branch — re-runs whenever the displayed cwd changes (e.g. when
|
|
408
466
|
// a pixel fix moves the agent into a different project root).
|
|
409
467
|
useEffect(() => {
|
|
@@ -425,25 +483,118 @@ export function App(props) {
|
|
|
425
483
|
useEffect(() => {
|
|
426
484
|
reloadCustomCommands();
|
|
427
485
|
}, [reloadCustomCommands]);
|
|
486
|
+
useEffect(() => {
|
|
487
|
+
currentToolsRef.current = currentTools;
|
|
488
|
+
}, [currentTools]);
|
|
428
489
|
// ── Plan mode wiring ─────────────────────────────────────
|
|
429
490
|
// Sync planModeRef with React state
|
|
430
491
|
useEffect(() => {
|
|
492
|
+
planModeStateRef.current = planMode;
|
|
431
493
|
if (props.planModeRef) {
|
|
432
494
|
props.planModeRef.current = planMode;
|
|
433
495
|
}
|
|
434
496
|
}, [planMode, props.planModeRef]);
|
|
497
|
+
const rebuildSystemPrompt = useCallback(async (options) => {
|
|
498
|
+
const approvedPlanPath = options?.clearApprovedPlan
|
|
499
|
+
? undefined
|
|
500
|
+
: (options?.approvedPlanPath ?? approvedPlanPathRef.current);
|
|
501
|
+
return buildSystemPrompt(options?.cwd ?? cwdRef.current, props.skills, options?.planMode ?? planModeStateRef.current, approvedPlanPath, (options?.tools ?? currentToolsRef.current).map((tool) => tool.name), options?.activeLanguages ?? injectedLanguagesRef.current);
|
|
502
|
+
}, [props.skills]);
|
|
503
|
+
const replaceSystemPrompt = useCallback(async (options) => {
|
|
504
|
+
const newPrompt = await rebuildSystemPrompt(options);
|
|
505
|
+
if (messagesRef.current[0]?.role === "system") {
|
|
506
|
+
messagesRef.current[0] = { role: "system", content: newPrompt };
|
|
507
|
+
}
|
|
508
|
+
return newPrompt;
|
|
509
|
+
}, [rebuildSystemPrompt]);
|
|
510
|
+
/**
|
|
511
|
+
* Unified "apply detection result" pipeline. Called from three sites:
|
|
512
|
+
* 1. Initial mount (existing project at startup).
|
|
513
|
+
* 2. After every `write`/`bash` tool result (reactive to new manifests).
|
|
514
|
+
* 3. Before every user submit (catches external changes between turns,
|
|
515
|
+
* and ensures non-writing prompts still surface the badge).
|
|
516
|
+
*
|
|
517
|
+
* No-op when no new languages were added vs `injectedLanguagesRef.current`.
|
|
518
|
+
* The set-growth gate keeps this safe to call from every hot path.
|
|
519
|
+
*/
|
|
520
|
+
const applyLanguageDetectionRef = useRef(async () => { });
|
|
521
|
+
applyLanguageDetectionRef.current = async (source) => {
|
|
522
|
+
const cwd = cwdRef.current;
|
|
523
|
+
const detected = detectLanguages(cwd);
|
|
524
|
+
const added = [];
|
|
525
|
+
for (const id of detected) {
|
|
526
|
+
if (!injectedLanguagesRef.current.has(id))
|
|
527
|
+
added.push(id);
|
|
528
|
+
}
|
|
529
|
+
if (added.length === 0) {
|
|
530
|
+
// No new packs to inject. The empty-detection hint + auto-run are
|
|
531
|
+
// first-time-per-cwd only — once the user has been shown the box and
|
|
532
|
+
// /setup has had a chance to run, re-showing on every session is noise.
|
|
533
|
+
// The with-packs path below is gated the same way via
|
|
534
|
+
// getAnnouncedLanguages / markLanguagesAnnounced: badge fires once per
|
|
535
|
+
// (cwd, language) and stays silent on subsequent sessions / /clear.
|
|
536
|
+
if (source === "initial" &&
|
|
537
|
+
!setupHintShownRef.current &&
|
|
538
|
+
injectedLanguagesRef.current.size === 0 &&
|
|
539
|
+
isFirstTimeSetup(cwd)) {
|
|
540
|
+
setupHintShownRef.current = true;
|
|
541
|
+
markSetupAudited(cwd);
|
|
542
|
+
log("INFO", "language", `No style packs detected for ${cwd}`, { source });
|
|
543
|
+
setLiveItems((prev) => [...prev, { kind: "setup_hint", id: getId() }]);
|
|
544
|
+
// /setup handles the empty / parent-folder / scratch-dir case via
|
|
545
|
+
// its brand-new-empty-project branch in the prompt template.
|
|
546
|
+
void triggerAutoSetupRef.current();
|
|
547
|
+
}
|
|
548
|
+
return;
|
|
549
|
+
}
|
|
550
|
+
injectedLanguagesRef.current = detected;
|
|
551
|
+
try {
|
|
552
|
+
await replaceSystemPrompt({ cwd, activeLanguages: detected });
|
|
553
|
+
const verifyCmds = detectVerifyCommands(cwd, detected);
|
|
554
|
+
const tag = source === "initial" ? "Initial style packs" : "Style pack(s) loaded";
|
|
555
|
+
log("INFO", "language", `${tag}: ${added.join(", ")}`, {
|
|
556
|
+
source,
|
|
557
|
+
active: [...detected].join(","),
|
|
558
|
+
verify_count: String(verifyCmds.length),
|
|
559
|
+
verify: verifyCmds.map((c) => `${c.language}:${c.label}=${c.command}`).join(" | "),
|
|
560
|
+
});
|
|
561
|
+
// The badge is purely user-facing notification ("hey, this pack just
|
|
562
|
+
// turned on"). The system prompt is already updated above — that's the
|
|
563
|
+
// load-bearing part. We persist the announced set per-cwd so /clear,
|
|
564
|
+
// restart, and new sessions stay quiet for packs the user has seen.
|
|
565
|
+
const alreadyAnnounced = new Set(getAnnouncedLanguages(cwd));
|
|
566
|
+
const toAnnounce = added.filter((id) => !alreadyAnnounced.has(id));
|
|
567
|
+
if (toAnnounce.length > 0) {
|
|
568
|
+
markLanguagesAnnounced(cwd, toAnnounce);
|
|
569
|
+
const showSetupHint = !setupHintShownRef.current;
|
|
570
|
+
setupHintShownRef.current = true;
|
|
571
|
+
setLiveItems((prev) => [
|
|
572
|
+
...prev,
|
|
573
|
+
{ kind: "style_pack", added: toAnnounce, showSetupHint, id: getId() },
|
|
574
|
+
]);
|
|
575
|
+
}
|
|
576
|
+
// First-time-per-project auto-run. Fires only on the initial mount
|
|
577
|
+
// detection path — not on tool/input triggers — so we don't surprise
|
|
578
|
+
// users mid-session. Persisted across sessions via setup-history.json.
|
|
579
|
+
if (source === "initial" && isFirstTimeSetup(cwd)) {
|
|
580
|
+
markSetupAudited(cwd);
|
|
581
|
+
void triggerAutoSetupRef.current();
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
catch (err) {
|
|
585
|
+
log("WARN", "language", `Detection apply failed (${source}): ${err.message}`);
|
|
586
|
+
}
|
|
587
|
+
};
|
|
588
|
+
// Initial language detection — runs once on mount so existing projects with
|
|
589
|
+
// marker files (package.json, Cargo.toml, etc.) get their style packs from
|
|
590
|
+
// turn 1, with a visible badge.
|
|
591
|
+
useEffect(() => {
|
|
592
|
+
void applyLanguageDetectionRef.current("initial");
|
|
593
|
+
}, []);
|
|
435
594
|
// Rebuild system prompt when plan mode changes
|
|
436
595
|
useEffect(() => {
|
|
437
|
-
void (
|
|
438
|
-
|
|
439
|
-
if (messagesRef.current[0]?.role === "system") {
|
|
440
|
-
messagesRef.current[0] = {
|
|
441
|
-
role: "system",
|
|
442
|
-
content: newPrompt,
|
|
443
|
-
};
|
|
444
|
-
}
|
|
445
|
-
})();
|
|
446
|
-
}, [planMode, props.cwd, props.skills]);
|
|
596
|
+
void replaceSystemPrompt({ planMode });
|
|
597
|
+
}, [planMode, replaceSystemPrompt]);
|
|
447
598
|
// Wire onEnterPlan callback ref
|
|
448
599
|
useEffect(() => {
|
|
449
600
|
if (props.onEnterPlanRef) {
|
|
@@ -462,8 +613,10 @@ export function App(props) {
|
|
|
462
613
|
if (props.onExitPlanRef) {
|
|
463
614
|
props.onExitPlanRef.current = async (planPath) => {
|
|
464
615
|
// Deactivate plan mode, store approved plan path, open pane
|
|
616
|
+
planModeStateRef.current = false;
|
|
465
617
|
setPlanMode(false);
|
|
466
618
|
approvedPlanPathRef.current = planPath;
|
|
619
|
+
await replaceSystemPrompt({ planMode: false, approvedPlanPath: planPath });
|
|
467
620
|
// Use setTimeout to open pane after the current tool execution completes,
|
|
468
621
|
// so the turn can finish and the UI transitions cleanly
|
|
469
622
|
// Flag that the plan overlay is about to open — suppresses the
|
|
@@ -492,43 +645,79 @@ export function App(props) {
|
|
|
492
645
|
planPath);
|
|
493
646
|
};
|
|
494
647
|
}
|
|
495
|
-
}, [props.onExitPlanRef, stdout]);
|
|
496
|
-
const
|
|
648
|
+
}, [props.onExitPlanRef, replaceSystemPrompt, stdout]);
|
|
649
|
+
const appendMessagesToSession = useCallback(async (sessionPath, messages, startIndex) => {
|
|
650
|
+
const sm = sessionManagerRef.current;
|
|
651
|
+
if (!sm)
|
|
652
|
+
return;
|
|
653
|
+
await appendSessionMessages(sm, sessionPath, messages, startIndex);
|
|
654
|
+
}, []);
|
|
655
|
+
const persistCompactedSession = useCallback(async (compactedMessages) => {
|
|
497
656
|
const sm = sessionManagerRef.current;
|
|
657
|
+
if (!sm)
|
|
658
|
+
return;
|
|
659
|
+
const session = await createCompactedSessionCheckpoint(sm, {
|
|
660
|
+
cwd: cwdRef.current,
|
|
661
|
+
provider: currentProvider,
|
|
662
|
+
model: currentModel,
|
|
663
|
+
messages: compactedMessages,
|
|
664
|
+
});
|
|
665
|
+
sessionPathRef.current = session.path;
|
|
666
|
+
persistedIndexRef.current = compactedMessages.length;
|
|
667
|
+
if (sessionStore) {
|
|
668
|
+
sessionStore.sessionPath = session.path;
|
|
669
|
+
sessionStore.messages = [...compactedMessages];
|
|
670
|
+
}
|
|
671
|
+
log("INFO", "compaction", "Persisted compacted session checkpoint", { path: session.path });
|
|
672
|
+
}, [currentModel, currentProvider, sessionStore]);
|
|
673
|
+
const persistNewMessages = useCallback(async () => {
|
|
498
674
|
const sp = sessionPathRef.current;
|
|
499
|
-
if (!
|
|
675
|
+
if (!sp)
|
|
500
676
|
return;
|
|
501
677
|
const allMsgs = messagesRef.current;
|
|
502
|
-
|
|
503
|
-
const msg = allMsgs[i];
|
|
504
|
-
if (msg.role === "system")
|
|
505
|
-
continue;
|
|
506
|
-
const entry = {
|
|
507
|
-
type: "message",
|
|
508
|
-
id: crypto.randomUUID(),
|
|
509
|
-
parentId: null,
|
|
510
|
-
timestamp: new Date().toISOString(),
|
|
511
|
-
message: msg,
|
|
512
|
-
};
|
|
513
|
-
await sm.appendEntry(sp, entry);
|
|
514
|
-
}
|
|
678
|
+
await appendMessagesToSession(sp, allMsgs, persistedIndexRef.current);
|
|
515
679
|
persistedIndexRef.current = allMsgs.length;
|
|
516
|
-
|
|
680
|
+
if (sessionStore) {
|
|
681
|
+
sessionStore.messages = [...allMsgs];
|
|
682
|
+
sessionStore.sessionPath = sp;
|
|
683
|
+
}
|
|
684
|
+
}, [appendMessagesToSession, sessionStore]);
|
|
685
|
+
/**
|
|
686
|
+
* Run the language detector against the current cwd. If the detected set is a
|
|
687
|
+
* strict superset of what's already injected, rebuild the system prompt with
|
|
688
|
+
* the expanded set and swap `messagesRef.current[0]`.
|
|
689
|
+
*
|
|
690
|
+
* Called from `onToolEnd` after `write`/`bash` succeeds — these are the only
|
|
691
|
+
* tools that can introduce new marker files (package.json, Cargo.toml, etc.).
|
|
692
|
+
* Other tool kinds skip detection entirely to avoid wasted filesystem stats.
|
|
693
|
+
*
|
|
694
|
+
* No restart required: the system prompt is mutated in place, same mechanism
|
|
695
|
+
* already used for plan mode + pixel-fix chdir.
|
|
696
|
+
*
|
|
697
|
+
* Stored in a ref so `onToolEnd` (whose useCallback dep array is intentionally
|
|
698
|
+
* empty to keep agent-loop options stable) can call the freshest version.
|
|
699
|
+
*/
|
|
700
|
+
const maybeInjectLanguagePacksRef = useRef(async () => { });
|
|
701
|
+
maybeInjectLanguagePacksRef.current = async (toolName, isError) => {
|
|
702
|
+
if (isError)
|
|
703
|
+
return;
|
|
704
|
+
if (toolName !== "write" && toolName !== "bash")
|
|
705
|
+
return;
|
|
706
|
+
await applyLanguageDetectionRef.current("tool");
|
|
707
|
+
};
|
|
517
708
|
// ── Compaction ─────────────────────────────────────────
|
|
518
|
-
// Load settings for auto-compaction
|
|
709
|
+
// Load settings for auto-compaction
|
|
519
710
|
const settingsRef = useRef(null);
|
|
520
|
-
const [buddyEnabled, setBuddyEnabled] = useState(false);
|
|
521
711
|
useEffect(() => {
|
|
522
712
|
if (props.settingsFile) {
|
|
523
713
|
const sm = new SettingsManager(props.settingsFile);
|
|
524
714
|
sm.load().then(() => {
|
|
525
715
|
settingsRef.current = sm;
|
|
526
|
-
setBuddyEnabled(sm.get("buddyEnabled") ?? false);
|
|
527
716
|
});
|
|
528
717
|
}
|
|
529
718
|
}, [props.settingsFile]);
|
|
530
719
|
const compactConversation = useCallback(async (messages) => {
|
|
531
|
-
const contextWindow = getContextWindow(currentModel);
|
|
720
|
+
const contextWindow = getContextWindow(currentModel, contextWindowOptions);
|
|
532
721
|
const tokensBefore = estimateConversationTokens(messages);
|
|
533
722
|
const spinId = getId();
|
|
534
723
|
log("INFO", "compaction", `Running compaction`, {
|
|
@@ -541,14 +730,20 @@ export function App(props) {
|
|
|
541
730
|
try {
|
|
542
731
|
// Resolve fresh credentials for compaction too
|
|
543
732
|
let compactApiKey = activeApiKey;
|
|
733
|
+
let compactAccountId = activeAccountId;
|
|
734
|
+
let compactBaseUrl = activeBaseUrl;
|
|
544
735
|
if (props.authStorage) {
|
|
545
736
|
const creds = await props.authStorage.resolveCredentials(currentProvider);
|
|
546
737
|
compactApiKey = creds.accessToken;
|
|
738
|
+
compactAccountId = creds.accountId;
|
|
739
|
+
compactBaseUrl = creds.baseUrl ?? compactBaseUrl;
|
|
547
740
|
}
|
|
548
741
|
const result = await compact(messages, {
|
|
549
742
|
provider: currentProvider,
|
|
550
743
|
model: currentModel,
|
|
551
744
|
apiKey: compactApiKey,
|
|
745
|
+
accountId: compactAccountId,
|
|
746
|
+
baseUrl: compactBaseUrl,
|
|
552
747
|
contextWindow,
|
|
553
748
|
signal: undefined,
|
|
554
749
|
approvedPlanPath: approvedPlanPathRef.current,
|
|
@@ -577,65 +772,118 @@ export function App(props) {
|
|
|
577
772
|
const msg = err instanceof Error ? err.message : String(err);
|
|
578
773
|
log("ERROR", "compaction", `Compaction failed: ${msg}`);
|
|
579
774
|
// Replace spinner with error
|
|
580
|
-
setLiveItems((prev) => prev.map((item) => item.id === spinId
|
|
581
|
-
? { kind: "error", message: `Compaction failed: ${msg}`, id: spinId }
|
|
582
|
-
: item));
|
|
775
|
+
setLiveItems((prev) => prev.map((item) => item.id === spinId ? toErrorItem(err, spinId, "Compaction failed") : item));
|
|
583
776
|
return messages; // Return unchanged on failure
|
|
584
777
|
}
|
|
585
|
-
}, [
|
|
778
|
+
}, [
|
|
779
|
+
currentModel,
|
|
780
|
+
currentProvider,
|
|
781
|
+
activeApiKey,
|
|
782
|
+
activeAccountId,
|
|
783
|
+
activeBaseUrl,
|
|
784
|
+
contextWindowOptions,
|
|
785
|
+
props.authStorage,
|
|
786
|
+
]);
|
|
787
|
+
const getRepoMapSignalCount = useCallback(() => {
|
|
788
|
+
return ((props.repoMapChangedFilesRef?.current.size ?? 0) +
|
|
789
|
+
(props.repoMapReadFilesRef?.current.size ?? 0));
|
|
790
|
+
}, [props.repoMapChangedFilesRef, props.repoMapReadFilesRef]);
|
|
791
|
+
const getRepoMapBudget = useCallback(() => {
|
|
792
|
+
const userTurns = messagesRef.current.filter((message) => message.role === "user").length;
|
|
793
|
+
const readCount = props.repoMapReadFilesRef?.current.size ?? 0;
|
|
794
|
+
if (userTurns <= 1 && readCount === 0)
|
|
795
|
+
return FIRST_TURN_REPO_MAP_MAX_CHARS;
|
|
796
|
+
if (readCount > 0)
|
|
797
|
+
return FOCUSED_REPO_MAP_MAX_CHARS;
|
|
798
|
+
return FOCUSED_REPO_MAP_MAX_CHARS + 1000;
|
|
799
|
+
}, [props.repoMapReadFilesRef]);
|
|
800
|
+
const refreshRepoMap = useCallback(async (latestUserPrompt) => {
|
|
801
|
+
const rendered = await buildRepoMap({
|
|
802
|
+
cwd: cwdRef.current,
|
|
803
|
+
maxChars: getRepoMapBudget(),
|
|
804
|
+
changedFiles: [...(props.repoMapChangedFilesRef?.current ?? new Set())],
|
|
805
|
+
readFiles: [...(props.repoMapReadFilesRef?.current ?? new Set())],
|
|
806
|
+
focusTerms: latestUserPrompt ? [latestUserPrompt] : [],
|
|
807
|
+
cache: repoMapCacheRef.current,
|
|
808
|
+
});
|
|
809
|
+
repoMapMarkdownRef.current = rendered.markdown;
|
|
810
|
+
repoMapSnapshotRef.current = rendered.snapshot;
|
|
811
|
+
repoMapChangedCountRef.current = getRepoMapSignalCount();
|
|
812
|
+
repoMapDirtyRef.current = false;
|
|
813
|
+
return rendered.markdown;
|
|
814
|
+
}, [
|
|
815
|
+
getRepoMapBudget,
|
|
816
|
+
getRepoMapSignalCount,
|
|
817
|
+
props.repoMapChangedFilesRef,
|
|
818
|
+
props.repoMapReadFilesRef,
|
|
819
|
+
]);
|
|
820
|
+
const stripRepoMapMessages = useCallback((messages) => {
|
|
821
|
+
return stripRepoMapContextMessages(messages);
|
|
822
|
+
}, []);
|
|
823
|
+
const injectRepoMapContext = useCallback(async (messages) => {
|
|
824
|
+
if (!repoMapInjectionEnabledRef.current)
|
|
825
|
+
return stripRepoMapMessages(messages);
|
|
826
|
+
const stripped = stripRepoMapMessages(messages);
|
|
827
|
+
const latestUserPrompt = getLatestUserText(stripped);
|
|
828
|
+
const signalCount = getRepoMapSignalCount();
|
|
829
|
+
if (signalCount !== repoMapChangedCountRef.current)
|
|
830
|
+
repoMapDirtyRef.current = true;
|
|
831
|
+
if (repoMapDirtyRef.current || !repoMapMarkdownRef.current) {
|
|
832
|
+
await refreshRepoMap(latestUserPrompt);
|
|
833
|
+
}
|
|
834
|
+
if (!repoMapMarkdownRef.current)
|
|
835
|
+
return stripped;
|
|
836
|
+
return injectRepoMapContextMessages(stripped, repoMapMarkdownRef.current);
|
|
837
|
+
}, [props.repoMapChangedFilesRef, props.repoMapReadFilesRef, refreshRepoMap, stripRepoMapMessages]);
|
|
586
838
|
/**
|
|
587
839
|
* transformContext callback for the agent loop.
|
|
588
840
|
* Called before each LLM call and on context overflow.
|
|
589
|
-
*
|
|
590
|
-
*
|
|
591
|
-
* Uses actual API-reported token counts (from previous turn_end) when
|
|
592
|
-
* available, falling back to the character-based estimate. A 30-second
|
|
593
|
-
* cooldown prevents repeated compaction — matching the pattern used by
|
|
594
|
-
* Mysti, openclaw, and other real-world agent frameworks.
|
|
841
|
+
* Compacts persistent chat only, then injects the dynamic repo map transiently.
|
|
595
842
|
*/
|
|
596
843
|
const transformContext = useCallback(async (messages, options) => {
|
|
844
|
+
const stripped = stripRepoMapMessages(messages);
|
|
597
845
|
const settings = settingsRef.current;
|
|
598
846
|
const autoCompact = settings?.get("autoCompact") ?? true;
|
|
599
847
|
const threshold = settings?.get("compactThreshold") ?? 0.8;
|
|
600
848
|
// Force-compact on context overflow regardless of settings
|
|
601
849
|
if (options?.force) {
|
|
602
|
-
const result = await compactConversation(
|
|
850
|
+
const result = await compactConversation(stripped);
|
|
851
|
+
if (result !== stripped) {
|
|
852
|
+
messagesRef.current = result;
|
|
853
|
+
await persistCompactedSession(result);
|
|
854
|
+
}
|
|
603
855
|
lastCompactionTimeRef.current = Date.now();
|
|
604
|
-
return result;
|
|
856
|
+
return injectRepoMapContext(result);
|
|
605
857
|
}
|
|
606
858
|
if (!autoCompact)
|
|
607
|
-
return
|
|
859
|
+
return injectRepoMapContext(stripped);
|
|
608
860
|
// Time-based cooldown: skip if compaction ran within the last 30 seconds
|
|
609
861
|
if (Date.now() - lastCompactionTimeRef.current < 30_000) {
|
|
610
862
|
log("INFO", "compaction", `Skipping compaction — cooldown active`);
|
|
611
|
-
return
|
|
863
|
+
return injectRepoMapContext(stripped);
|
|
612
864
|
}
|
|
613
|
-
const contextWindow = getContextWindow(currentModel);
|
|
614
|
-
|
|
615
|
-
// schemas. Otherwise the API rejects requests where the prompt fits the
|
|
616
|
-
// window but leaves no room for the response (e.g. Codex Mini at 200K
|
|
617
|
-
// ctx / 100K out — pre-turn estimate may say 160K but a 100K reasoning
|
|
618
|
-
// response then overflows).
|
|
619
|
-
const modelInfo = getModel(currentModel);
|
|
620
|
-
const reserveTokens = (modelInfo?.maxOutputTokens ?? 0) + 5_000;
|
|
621
|
-
// Prefer actual API-reported tokens over char-based estimate, but only
|
|
622
|
-
// when the token count was recorded AFTER the most recent compaction.
|
|
623
|
-
// A count from before compaction is stale — it reflects the old context
|
|
624
|
-
// size and would trigger compaction again immediately for no reason.
|
|
865
|
+
const contextWindow = getContextWindow(currentModel, contextWindowOptions);
|
|
866
|
+
const reserveTokens = getCompactionReserveTokens(props.maxTokens);
|
|
625
867
|
const tokensFresh = lastActualTokensTimestampRef.current > lastCompactionTimeRef.current;
|
|
626
868
|
const actualTokens = lastActualTokensRef.current > 0 && tokensFresh ? lastActualTokensRef.current : undefined;
|
|
627
|
-
if (shouldCompact(
|
|
628
|
-
const
|
|
629
|
-
|
|
869
|
+
if (shouldCompact(stripped, contextWindow, threshold, actualTokens, reserveTokens)) {
|
|
870
|
+
const result = await compactConversation(stripped);
|
|
871
|
+
if (result !== stripped) {
|
|
872
|
+
messagesRef.current = result;
|
|
873
|
+
await persistCompactedSession(result);
|
|
874
|
+
}
|
|
630
875
|
lastCompactionTimeRef.current = Date.now();
|
|
631
|
-
|
|
632
|
-
// return the original reference so the agent loop doesn't replace messages.
|
|
633
|
-
if (result.length === before)
|
|
634
|
-
return messages;
|
|
635
|
-
return result;
|
|
876
|
+
return injectRepoMapContext(result);
|
|
636
877
|
}
|
|
637
|
-
return
|
|
638
|
-
}, [
|
|
878
|
+
return injectRepoMapContext(stripped);
|
|
879
|
+
}, [
|
|
880
|
+
currentModel,
|
|
881
|
+
compactConversation,
|
|
882
|
+
contextWindowOptions,
|
|
883
|
+
injectRepoMapContext,
|
|
884
|
+
persistCompactedSession,
|
|
885
|
+
stripRepoMapMessages,
|
|
886
|
+
]);
|
|
639
887
|
// ── Background task bar state (external store) ──────────
|
|
640
888
|
const { bgTasks, focused: taskBarFocused, expanded: taskBarExpanded, selectedIndex: selectedTaskIndex, } = useTaskBarStore();
|
|
641
889
|
useTaskBarPolling(props.processManager);
|
|
@@ -663,7 +911,7 @@ export function App(props) {
|
|
|
663
911
|
tools: currentTools,
|
|
664
912
|
webSearch: props.webSearch,
|
|
665
913
|
maxTokens: props.maxTokens,
|
|
666
|
-
thinking: thinkingEnabled ? (
|
|
914
|
+
thinking: thinkingEnabled ? getMaxThinkingLevel(currentModel) : undefined,
|
|
667
915
|
apiKey: activeApiKey,
|
|
668
916
|
baseUrl: activeBaseUrl,
|
|
669
917
|
accountId: activeAccountId,
|
|
@@ -671,6 +919,7 @@ export function App(props) {
|
|
|
671
919
|
transformContext,
|
|
672
920
|
}, {
|
|
673
921
|
onComplete: useCallback(() => {
|
|
922
|
+
messagesRef.current = stripRepoMapMessages(messagesRef.current);
|
|
674
923
|
persistNewMessages();
|
|
675
924
|
// Auto-clear plan progress and approved plan when all steps are completed
|
|
676
925
|
const steps = planStepsRef.current;
|
|
@@ -679,12 +928,7 @@ export function App(props) {
|
|
|
679
928
|
setPlanSteps([]);
|
|
680
929
|
approvedPlanPathRef.current = undefined;
|
|
681
930
|
// Rebuild system prompt to remove the completed plan from context
|
|
682
|
-
void (
|
|
683
|
-
const newPrompt = await buildSystemPrompt(props.cwd, props.skills, planMode, undefined);
|
|
684
|
-
if (messagesRef.current[0]?.role === "system") {
|
|
685
|
-
messagesRef.current[0] = { role: "system", content: newPrompt };
|
|
686
|
-
}
|
|
687
|
-
})();
|
|
931
|
+
void replaceSystemPrompt({ clearApprovedPlan: true });
|
|
688
932
|
}
|
|
689
933
|
// Generate session title after the first turn (background, best-effort)
|
|
690
934
|
if (!sessionTitleGeneratedRef.current) {
|
|
@@ -728,6 +972,7 @@ export function App(props) {
|
|
|
728
972
|
}
|
|
729
973
|
}, [
|
|
730
974
|
persistNewMessages,
|
|
975
|
+
stripRepoMapMessages,
|
|
731
976
|
planMode,
|
|
732
977
|
props.cwd,
|
|
733
978
|
props.skills,
|
|
@@ -747,6 +992,9 @@ export function App(props) {
|
|
|
747
992
|
planStepsRef.current = updated;
|
|
748
993
|
setPlanSteps(updated);
|
|
749
994
|
}
|
|
995
|
+
// Real progress happened — reset the stuck-guard so the next
|
|
996
|
+
// step gets its own fresh nudge budget.
|
|
997
|
+
followUpNudgesRef.current = { step: 0, count: 0 };
|
|
750
998
|
}
|
|
751
999
|
}
|
|
752
1000
|
// Flush all completed items from the previous turn to Static history.
|
|
@@ -814,6 +1062,8 @@ export function App(props) {
|
|
|
814
1062
|
}, []),
|
|
815
1063
|
onToolStart: useCallback((toolCallId, name, args) => {
|
|
816
1064
|
log("INFO", "tool", `Tool call started: ${name}`, { id: toolCallId });
|
|
1065
|
+
const startedAt = Date.now();
|
|
1066
|
+
const animateUntil = startedAt + RUNNING_INDICATOR_ANIMATION_MS;
|
|
817
1067
|
// Flush completed items (assistant text, finished tools) to Static
|
|
818
1068
|
// before adding tool UI. Keeping both in the live area makes it tall
|
|
819
1069
|
// and causes Ink's cursor math to clip the top.
|
|
@@ -859,7 +1109,10 @@ export function App(props) {
|
|
|
859
1109
|
const next = [...prev];
|
|
860
1110
|
next[groupIdx] = {
|
|
861
1111
|
...group,
|
|
862
|
-
tools: [
|
|
1112
|
+
tools: [
|
|
1113
|
+
...group.tools,
|
|
1114
|
+
{ toolCallId, name, args, status: "running", animateUntil },
|
|
1115
|
+
],
|
|
863
1116
|
};
|
|
864
1117
|
return next;
|
|
865
1118
|
}
|
|
@@ -867,7 +1120,7 @@ export function App(props) {
|
|
|
867
1120
|
...prev,
|
|
868
1121
|
{
|
|
869
1122
|
kind: "tool_group",
|
|
870
|
-
tools: [{ toolCallId, name, args, status: "running" }],
|
|
1123
|
+
tools: [{ toolCallId, name, args, status: "running", animateUntil }],
|
|
871
1124
|
id: getId(),
|
|
872
1125
|
},
|
|
873
1126
|
];
|
|
@@ -876,7 +1129,7 @@ export function App(props) {
|
|
|
876
1129
|
else {
|
|
877
1130
|
setLiveItems((prev) => [
|
|
878
1131
|
...prev,
|
|
879
|
-
{ kind: "tool_start", toolCallId, name, args, id: getId() },
|
|
1132
|
+
{ kind: "tool_start", toolCallId, name, args, id: getId(), startedAt, animateUntil },
|
|
880
1133
|
]);
|
|
881
1134
|
}
|
|
882
1135
|
}, []),
|
|
@@ -921,6 +1174,10 @@ export function App(props) {
|
|
|
921
1174
|
});
|
|
922
1175
|
}, []),
|
|
923
1176
|
onToolEnd: useCallback((toolCallId, name, result, isError, durationMs, details) => {
|
|
1177
|
+
// Language-pack detection — gated on `write`/`bash` inside the
|
|
1178
|
+
// helper; cheap to call unconditionally. Fire-and-forget; the next
|
|
1179
|
+
// LLM turn picks up the swapped system prompt automatically.
|
|
1180
|
+
void maybeInjectLanguagePacksRef.current(name, isError);
|
|
924
1181
|
const level = isError ? "ERROR" : "INFO";
|
|
925
1182
|
log(level, "tool", `Tool call ended: ${name}`, {
|
|
926
1183
|
id: toolCallId,
|
|
@@ -1025,6 +1282,8 @@ export function App(props) {
|
|
|
1025
1282
|
}, []),
|
|
1026
1283
|
onServerToolCall: useCallback((id, name, input) => {
|
|
1027
1284
|
log("INFO", "server_tool", `Server tool call: ${name}`, { id });
|
|
1285
|
+
const startedAt = Date.now();
|
|
1286
|
+
const animateUntil = startedAt + RUNNING_INDICATOR_ANIMATION_MS;
|
|
1028
1287
|
// Flush completed items (including assistant text) to Static before
|
|
1029
1288
|
// adding server tool UI — same rationale as onToolStart.
|
|
1030
1289
|
setLiveItems((prev) => {
|
|
@@ -1039,7 +1298,8 @@ export function App(props) {
|
|
|
1039
1298
|
serverToolCallId: id,
|
|
1040
1299
|
name,
|
|
1041
1300
|
input,
|
|
1042
|
-
startedAt
|
|
1301
|
+
startedAt,
|
|
1302
|
+
animateUntil,
|
|
1043
1303
|
id: getId(),
|
|
1044
1304
|
},
|
|
1045
1305
|
];
|
|
@@ -1230,7 +1490,7 @@ export function App(props) {
|
|
|
1230
1490
|
}
|
|
1231
1491
|
return item;
|
|
1232
1492
|
});
|
|
1233
|
-
return [...next, { kind: "
|
|
1493
|
+
return [...next, { kind: "stopped", text: "Request was stopped.", id: getId() }];
|
|
1234
1494
|
});
|
|
1235
1495
|
}, []),
|
|
1236
1496
|
onQueuedStart: useCallback((content) => {
|
|
@@ -1260,7 +1520,75 @@ export function App(props) {
|
|
|
1260
1520
|
setDoneStatus(null);
|
|
1261
1521
|
setLiveItems([userItem]);
|
|
1262
1522
|
}, []),
|
|
1523
|
+
// Inject a "continue with the next step" follow-up when the agent
|
|
1524
|
+
// would otherwise stop mid-plan. The prompt-only instruction wasn't
|
|
1525
|
+
// enough — some models (notably Opus) treat each [DONE:n] as a
|
|
1526
|
+
// natural completion boundary regardless. The stuck-guard caps
|
|
1527
|
+
// nudges per step so a genuinely blocked agent surfaces.
|
|
1528
|
+
getFollowUpMessages: useCallback(() => {
|
|
1529
|
+
const steps = planStepsRef.current;
|
|
1530
|
+
if (steps.length === 0 || !approvedPlanPathRef.current)
|
|
1531
|
+
return null;
|
|
1532
|
+
const next = steps.find((s) => !s.completed);
|
|
1533
|
+
if (!next)
|
|
1534
|
+
return null;
|
|
1535
|
+
const r = followUpNudgesRef.current;
|
|
1536
|
+
if (r.step !== next.step) {
|
|
1537
|
+
r.step = next.step;
|
|
1538
|
+
r.count = 0;
|
|
1539
|
+
}
|
|
1540
|
+
if (r.count >= 2)
|
|
1541
|
+
return null;
|
|
1542
|
+
r.count++;
|
|
1543
|
+
return [
|
|
1544
|
+
{
|
|
1545
|
+
role: "user",
|
|
1546
|
+
content: `Continue with step ${next.step}: ${next.text}. ` +
|
|
1547
|
+
`Emit [DONE:${next.step}] when done, then proceed to step ${next.step + 1} ` +
|
|
1548
|
+
`in the same turn. Only stop when every step in \`## Steps\` is complete ` +
|
|
1549
|
+
`or you genuinely need user input.`,
|
|
1550
|
+
},
|
|
1551
|
+
];
|
|
1552
|
+
}, []),
|
|
1263
1553
|
});
|
|
1554
|
+
// First-time-per-project auto-run of /setup. Bound after `agentLoop` is in
|
|
1555
|
+
// scope so the ref closure can dispatch to it. Called from the initial
|
|
1556
|
+
// language-detection path when `isFirstTimeSetup(cwd)` is true. Pushes a
|
|
1557
|
+
// notice item explaining what's happening, then runs the audit prompt.
|
|
1558
|
+
triggerAutoSetupRef.current = async () => {
|
|
1559
|
+
const setupCmd = getPromptCommand("setup");
|
|
1560
|
+
if (!setupCmd) {
|
|
1561
|
+
log("WARN", "setup", "Auto-setup skipped — /setup command not found in registry.");
|
|
1562
|
+
return;
|
|
1563
|
+
}
|
|
1564
|
+
log("INFO", "setup", `Auto-running /setup (first session for ${cwdRef.current})`);
|
|
1565
|
+
setLiveItems((prev) => [
|
|
1566
|
+
...prev,
|
|
1567
|
+
{
|
|
1568
|
+
kind: "info",
|
|
1569
|
+
text: "First time in this project — auto-running /setup to audit hygiene, tooling, and style-pack alignment. " +
|
|
1570
|
+
"Press Esc to cancel.",
|
|
1571
|
+
id: getId(),
|
|
1572
|
+
},
|
|
1573
|
+
{ kind: "user", text: "/setup", id: getId() },
|
|
1574
|
+
]);
|
|
1575
|
+
setLastUserMessage("/setup");
|
|
1576
|
+
setDoneStatus(null);
|
|
1577
|
+
try {
|
|
1578
|
+
await agentLoop.run(setupCmd.prompt);
|
|
1579
|
+
}
|
|
1580
|
+
catch (err) {
|
|
1581
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1582
|
+
const isAbort = msg.includes("aborted") || msg.includes("abort");
|
|
1583
|
+
log(isAbort ? "INFO" : "ERROR", "setup", `Auto-setup ended: ${msg}`);
|
|
1584
|
+
setLiveItems((prev) => [
|
|
1585
|
+
...prev,
|
|
1586
|
+
isAbort
|
|
1587
|
+
? { kind: "stopped", text: "Auto-setup cancelled.", id: getId() }
|
|
1588
|
+
: toErrorItem(err, getId()),
|
|
1589
|
+
]);
|
|
1590
|
+
}
|
|
1591
|
+
};
|
|
1264
1592
|
// Phase 2 of the two-phase flush: after onDone clears liveItems (phase 1)
|
|
1265
1593
|
// and Ink renders the smaller live area (updating its internal line
|
|
1266
1594
|
// counter), this effect pushes the stashed items into Static history.
|
|
@@ -1279,6 +1607,29 @@ export function App(props) {
|
|
|
1279
1607
|
useEffect(() => {
|
|
1280
1608
|
setTitleRunning(agentLoop.isRunning);
|
|
1281
1609
|
}, [agentLoop.isRunning]);
|
|
1610
|
+
// Mirror agent running state into sessionStore so renderApp's resize
|
|
1611
|
+
// handler and overlay toggles can skip their unmount/remount while the
|
|
1612
|
+
// agent is in flight (unmounting fires useAgentLoop's cleanup which
|
|
1613
|
+
// aborts the in-flight request). On the running→idle transition,
|
|
1614
|
+
// consume any pendingResetUI flag set during the run by scheduling a
|
|
1615
|
+
// deferred resetUI to clean up accumulated log-update drift. The 100ms
|
|
1616
|
+
// setTimeout lets onDone's two-phase flush commit to sessionStore.history
|
|
1617
|
+
// first, so the chat isn't lost. The cleanup also bails if the user
|
|
1618
|
+
// started a new run before the timer fires, to avoid aborting it.
|
|
1619
|
+
useEffect(() => {
|
|
1620
|
+
if (!sessionStore)
|
|
1621
|
+
return;
|
|
1622
|
+
sessionStore.isAgentRunning = agentLoop.isRunning;
|
|
1623
|
+
if (!agentLoop.isRunning && sessionStore.pendingResetUI) {
|
|
1624
|
+
sessionStore.pendingResetUI = false;
|
|
1625
|
+
const timer = setTimeout(() => {
|
|
1626
|
+
if (sessionStore.isAgentRunning)
|
|
1627
|
+
return;
|
|
1628
|
+
props.resetUI?.();
|
|
1629
|
+
}, 100);
|
|
1630
|
+
return () => clearTimeout(timer);
|
|
1631
|
+
}
|
|
1632
|
+
}, [agentLoop.isRunning, sessionStore, props.resetUI]);
|
|
1282
1633
|
// Consume sessionStore.pendingAction once on mount. Set by resetUI options
|
|
1283
1634
|
// for paths that remount AND immediately drive the agent (plan accept,
|
|
1284
1635
|
// plan reject, startTask, pixel fix). The action survives the unmount
|
|
@@ -1292,7 +1643,14 @@ export function App(props) {
|
|
|
1292
1643
|
pendingActionConsumedRef.current = true;
|
|
1293
1644
|
if (sessionStore)
|
|
1294
1645
|
sessionStore.pendingAction = undefined;
|
|
1295
|
-
if (action.
|
|
1646
|
+
if (action.planEvent) {
|
|
1647
|
+
const ev = action.planEvent;
|
|
1648
|
+
setLiveItems((prev) => [
|
|
1649
|
+
...prev,
|
|
1650
|
+
{ kind: "plan_event", event: ev.event, detail: ev.detail, id: getId() },
|
|
1651
|
+
]);
|
|
1652
|
+
}
|
|
1653
|
+
else if (action.infoText) {
|
|
1296
1654
|
setLiveItems((prev) => [
|
|
1297
1655
|
...prev,
|
|
1298
1656
|
{ kind: "info", text: action.infoText, id: getId() },
|
|
@@ -1302,7 +1660,7 @@ export function App(props) {
|
|
|
1302
1660
|
void agentLoop.run(action.prompt).catch((err) => {
|
|
1303
1661
|
const errMsg = err instanceof Error ? err.message : String(err);
|
|
1304
1662
|
log("ERROR", "error", errMsg);
|
|
1305
|
-
setLiveItems((prev) => [...prev,
|
|
1663
|
+
setLiveItems((prev) => [...prev, toErrorItem(err, getId())]);
|
|
1306
1664
|
});
|
|
1307
1665
|
// Intentional one-shot: run once on mount, never re-fire on re-render.
|
|
1308
1666
|
}, []);
|
|
@@ -1314,12 +1672,6 @@ export function App(props) {
|
|
|
1314
1672
|
setEyesCount(isEyesActive(props.cwd) ? journalCount({ status: "open" }, props.cwd) : undefined);
|
|
1315
1673
|
}
|
|
1316
1674
|
}, [agentLoop.isRunning, props.cwd]);
|
|
1317
|
-
// Animated thinking border — derived from global animation tick
|
|
1318
|
-
useAnimationActive();
|
|
1319
|
-
const animTick = useAnimationTick();
|
|
1320
|
-
const thinkingBorderFrame = agentLoop.activityPhase === "thinking"
|
|
1321
|
-
? deriveFrame(animTick, 1000, THINKING_BORDER_COLORS.length)
|
|
1322
|
-
: 0;
|
|
1323
1675
|
const handleSubmit = useCallback(async (input, inputImages = [], pasteInfo) => {
|
|
1324
1676
|
const trimmed = input.trim();
|
|
1325
1677
|
if (trimmed.startsWith("/")) {
|
|
@@ -1328,6 +1680,11 @@ export function App(props) {
|
|
|
1328
1680
|
else {
|
|
1329
1681
|
const truncated = trimmed.length > 100 ? trimmed.slice(0, 100) + "..." : trimmed;
|
|
1330
1682
|
log("INFO", "input", `User input: ${truncated}${inputImages.length > 0 ? ` (+${inputImages.length} image${inputImages.length > 1 ? "s" : ""})` : ""}`);
|
|
1683
|
+
// Re-detect on every user submit — cheap (fs stats only). Catches
|
|
1684
|
+
// external changes between turns and ensures non-writing prompts still
|
|
1685
|
+
// surface the badge when packs are newly applicable. No-op if the set
|
|
1686
|
+
// has not grown.
|
|
1687
|
+
await applyLanguageDetectionRef.current("input");
|
|
1331
1688
|
}
|
|
1332
1689
|
// Handle /model directly — open inline selector
|
|
1333
1690
|
if (trimmed === "/model" || trimmed === "/m") {
|
|
@@ -1339,7 +1696,7 @@ export function App(props) {
|
|
|
1339
1696
|
const compacted = await compactConversation(messagesRef.current);
|
|
1340
1697
|
if (compacted !== messagesRef.current) {
|
|
1341
1698
|
messagesRef.current = compacted;
|
|
1342
|
-
|
|
1699
|
+
await persistCompactedSession(compacted);
|
|
1343
1700
|
}
|
|
1344
1701
|
return;
|
|
1345
1702
|
}
|
|
@@ -1359,7 +1716,7 @@ export function App(props) {
|
|
|
1359
1716
|
if (trimmed === "/clear") {
|
|
1360
1717
|
if (props.resetUI) {
|
|
1361
1718
|
void (async () => {
|
|
1362
|
-
const newPrompt = await
|
|
1719
|
+
const newPrompt = await rebuildSystemPrompt({ clearApprovedPlan: true });
|
|
1363
1720
|
props.resetUI?.({
|
|
1364
1721
|
wipeSession: true,
|
|
1365
1722
|
messages: [{ role: "system", content: newPrompt }],
|
|
@@ -1378,7 +1735,7 @@ export function App(props) {
|
|
|
1378
1735
|
planStepsRef.current = [];
|
|
1379
1736
|
setPlanSteps([]);
|
|
1380
1737
|
void (async () => {
|
|
1381
|
-
const newPrompt = await
|
|
1738
|
+
const newPrompt = await rebuildSystemPrompt({ clearApprovedPlan: true });
|
|
1382
1739
|
messagesRef.current = [{ role: "system", content: newPrompt }];
|
|
1383
1740
|
persistedIndexRef.current = messagesRef.current.length;
|
|
1384
1741
|
})();
|
|
@@ -1440,29 +1797,46 @@ export function App(props) {
|
|
|
1440
1797
|
planStepsRef.current = [];
|
|
1441
1798
|
setPlanSteps([]);
|
|
1442
1799
|
// Rebuild system prompt without the plan
|
|
1443
|
-
void (
|
|
1444
|
-
|
|
1445
|
-
if (messagesRef.current[0]?.role === "system") {
|
|
1446
|
-
messagesRef.current[0] = { role: "system", content: newPrompt };
|
|
1447
|
-
}
|
|
1448
|
-
})();
|
|
1449
|
-
setLiveItems([{ kind: "info", text: "Approved plan dismissed.", id: getId() }]);
|
|
1800
|
+
void replaceSystemPrompt({ clearApprovedPlan: true });
|
|
1801
|
+
setLiveItems([{ kind: "plan_event", event: "dismissed", id: getId() }]);
|
|
1450
1802
|
return;
|
|
1451
1803
|
}
|
|
1452
|
-
// Handle /
|
|
1453
|
-
if (trimmed === "/
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1804
|
+
// Handle /map — show, refresh, or toggle dynamic repo map injection
|
|
1805
|
+
if (trimmed === "/map" ||
|
|
1806
|
+
trimmed === "/map refresh" ||
|
|
1807
|
+
trimmed === "/map on" ||
|
|
1808
|
+
trimmed === "/map off") {
|
|
1809
|
+
const action = trimmed.slice("/map".length).trim();
|
|
1810
|
+
if (action === "on") {
|
|
1811
|
+
repoMapInjectionEnabledRef.current = true;
|
|
1812
|
+
repoMapDirtyRef.current = true;
|
|
1813
|
+
setLiveItems((prev) => [
|
|
1814
|
+
...prev,
|
|
1815
|
+
{ kind: "info", text: "Dynamic repo map injection is on.", id: getId() },
|
|
1816
|
+
]);
|
|
1817
|
+
return;
|
|
1458
1818
|
}
|
|
1459
|
-
|
|
1460
|
-
|
|
1819
|
+
if (action === "off") {
|
|
1820
|
+
repoMapInjectionEnabledRef.current = false;
|
|
1821
|
+
messagesRef.current = stripRepoMapMessages(messagesRef.current);
|
|
1822
|
+
setLiveItems((prev) => [
|
|
1823
|
+
...prev,
|
|
1824
|
+
{
|
|
1825
|
+
kind: "info",
|
|
1826
|
+
text: "Dynamic repo map injection is off for this session.",
|
|
1827
|
+
id: getId(),
|
|
1828
|
+
},
|
|
1829
|
+
]);
|
|
1830
|
+
return;
|
|
1831
|
+
}
|
|
1832
|
+
if (action === "refresh")
|
|
1833
|
+
repoMapDirtyRef.current = true;
|
|
1834
|
+
const markdown = await refreshRepoMap(getLatestUserText(messagesRef.current));
|
|
1835
|
+
setLiveItems((prev) => [
|
|
1836
|
+
...prev,
|
|
1461
1837
|
{
|
|
1462
1838
|
kind: "info",
|
|
1463
|
-
text:
|
|
1464
|
-
? "Buddy enabled! Your companion will appear near the prompt."
|
|
1465
|
-
: "Buddy disabled.",
|
|
1839
|
+
text: formatRepoMapCommandOutput(repoMapInjectionEnabledRef.current, markdown, action === "refresh"),
|
|
1466
1840
|
id: getId(),
|
|
1467
1841
|
},
|
|
1468
1842
|
]);
|
|
@@ -1470,13 +1844,18 @@ export function App(props) {
|
|
|
1470
1844
|
}
|
|
1471
1845
|
// Handle /plans — open plan pane
|
|
1472
1846
|
if (trimmed === "/plans") {
|
|
1473
|
-
if (props.resetUI && props.sessionStore) {
|
|
1847
|
+
if (props.resetUI && props.sessionStore && !agentLoop.isRunning) {
|
|
1474
1848
|
props.sessionStore.overlay = "plan";
|
|
1475
1849
|
props.sessionStore.planAutoExpand = false;
|
|
1476
1850
|
props.resetUI();
|
|
1477
1851
|
}
|
|
1478
1852
|
else {
|
|
1479
|
-
|
|
1853
|
+
if (props.sessionStore) {
|
|
1854
|
+
props.sessionStore.overlay = "plan";
|
|
1855
|
+
props.sessionStore.planAutoExpand = false;
|
|
1856
|
+
if (agentLoop.isRunning)
|
|
1857
|
+
props.sessionStore.pendingResetUI = true;
|
|
1858
|
+
}
|
|
1480
1859
|
setPlanAutoExpand(false);
|
|
1481
1860
|
setOverlay("plan");
|
|
1482
1861
|
}
|
|
@@ -1518,8 +1897,8 @@ export function App(props) {
|
|
|
1518
1897
|
setLiveItems((prev) => [
|
|
1519
1898
|
...prev,
|
|
1520
1899
|
isAbort
|
|
1521
|
-
? { kind: "
|
|
1522
|
-
:
|
|
1900
|
+
? { kind: "stopped", text: "Request was stopped.", id: getId() }
|
|
1901
|
+
: toErrorItem(err, getId()),
|
|
1523
1902
|
]);
|
|
1524
1903
|
}
|
|
1525
1904
|
// Reload custom commands in case a setup command created new ones
|
|
@@ -1642,11 +2021,19 @@ export function App(props) {
|
|
|
1642
2021
|
setLiveItems((prev) => [
|
|
1643
2022
|
...prev,
|
|
1644
2023
|
isAbort
|
|
1645
|
-
? { kind: "
|
|
1646
|
-
:
|
|
2024
|
+
? { kind: "stopped", text: "Request was stopped.", id: getId() }
|
|
2025
|
+
: toErrorItem(err, getId()),
|
|
1647
2026
|
]);
|
|
1648
2027
|
}
|
|
1649
|
-
}, [
|
|
2028
|
+
}, [
|
|
2029
|
+
agentLoop,
|
|
2030
|
+
props.onSlashCommand,
|
|
2031
|
+
compactConversation,
|
|
2032
|
+
rebuildSystemPrompt,
|
|
2033
|
+
replaceSystemPrompt,
|
|
2034
|
+
refreshRepoMap,
|
|
2035
|
+
stripRepoMapMessages,
|
|
2036
|
+
]);
|
|
1650
2037
|
const handleDoubleExit = useDoublePress(setExitPending, () => process.exit(0));
|
|
1651
2038
|
const handleAbort = useCallback(() => {
|
|
1652
2039
|
if (agentLoop.isRunning) {
|
|
@@ -1663,7 +2050,7 @@ export function App(props) {
|
|
|
1663
2050
|
log("INFO", "thinking", `Thinking ${next ? "enabled" : "disabled"}`);
|
|
1664
2051
|
setLiveItems((items) => [
|
|
1665
2052
|
...items,
|
|
1666
|
-
{ kind: "
|
|
2053
|
+
{ kind: "thinking_transition", active: next, id: getId() },
|
|
1667
2054
|
]);
|
|
1668
2055
|
if (props.settingsFile) {
|
|
1669
2056
|
const sm = new SettingsManager(props.settingsFile);
|
|
@@ -1680,6 +2067,10 @@ export function App(props) {
|
|
|
1680
2067
|
const newProvider = value.slice(0, colonIdx);
|
|
1681
2068
|
const newModelId = value.slice(colonIdx + 1);
|
|
1682
2069
|
log("INFO", "model", `Model changed`, { provider: newProvider, model: newModelId });
|
|
2070
|
+
const rebuildPromptWithTools = (tools) => {
|
|
2071
|
+
currentToolsRef.current = tools;
|
|
2072
|
+
void replaceSystemPrompt({ tools });
|
|
2073
|
+
};
|
|
1683
2074
|
// Handle provider-specific tool changes when provider changes
|
|
1684
2075
|
setCurrentProvider((prevProvider) => {
|
|
1685
2076
|
if (newProvider !== prevProvider) {
|
|
@@ -1687,15 +2078,17 @@ export function App(props) {
|
|
|
1687
2078
|
// Anthropic has native server-side web search; all other providers need the client tool.
|
|
1688
2079
|
setCurrentTools((prev) => {
|
|
1689
2080
|
const hasWebSearch = prev.some((t) => t.name === "web_search");
|
|
2081
|
+
let next = prev;
|
|
1690
2082
|
if (newProvider === "anthropic" && hasWebSearch) {
|
|
1691
2083
|
// Switching TO anthropic — remove client-side web_search (server-side handles it)
|
|
1692
|
-
|
|
2084
|
+
next = prev.filter((t) => t.name !== "web_search");
|
|
1693
2085
|
}
|
|
1694
2086
|
else if (newProvider !== "anthropic" && !hasWebSearch) {
|
|
1695
2087
|
// Switching FROM anthropic — add client-side web_search
|
|
1696
|
-
|
|
2088
|
+
next = [...prev, createWebSearchTool()];
|
|
1697
2089
|
}
|
|
1698
|
-
|
|
2090
|
+
rebuildPromptWithTools(next);
|
|
2091
|
+
return next;
|
|
1699
2092
|
});
|
|
1700
2093
|
// Reconnect MCP servers
|
|
1701
2094
|
if (props.mcpManager) {
|
|
@@ -1718,16 +2111,21 @@ export function App(props) {
|
|
|
1718
2111
|
}
|
|
1719
2112
|
try {
|
|
1720
2113
|
const mcpTools = await props.mcpManager.connectAll(getMCPServers(newProvider, apiKey));
|
|
1721
|
-
setCurrentTools((prev) =>
|
|
1722
|
-
...prev.filter((t) => !t.name.startsWith("mcp__")),
|
|
1723
|
-
|
|
1724
|
-
|
|
2114
|
+
setCurrentTools((prev) => {
|
|
2115
|
+
const next = [...prev.filter((t) => !t.name.startsWith("mcp__")), ...mcpTools];
|
|
2116
|
+
rebuildPromptWithTools(next);
|
|
2117
|
+
return next;
|
|
2118
|
+
});
|
|
1725
2119
|
log("INFO", "mcp", `MCP servers reconnected for provider ${newProvider}`);
|
|
1726
2120
|
}
|
|
1727
2121
|
catch (err) {
|
|
1728
2122
|
log("WARN", "mcp", `MCP reconnection failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
1729
2123
|
// Still remove old MCP tools even if reconnection fails
|
|
1730
|
-
setCurrentTools((prev) =>
|
|
2124
|
+
setCurrentTools((prev) => {
|
|
2125
|
+
const next = prev.filter((t) => !t.name.startsWith("mcp__"));
|
|
2126
|
+
rebuildPromptWithTools(next);
|
|
2127
|
+
return next;
|
|
2128
|
+
});
|
|
1731
2129
|
}
|
|
1732
2130
|
})();
|
|
1733
2131
|
}
|
|
@@ -1739,7 +2137,7 @@ export function App(props) {
|
|
|
1739
2137
|
const displayName = modelInfo?.name ?? newModelId;
|
|
1740
2138
|
setLiveItems((prev) => [
|
|
1741
2139
|
...prev,
|
|
1742
|
-
{ kind: "
|
|
2140
|
+
{ kind: "model_transition", modelName: displayName, id: getId() },
|
|
1743
2141
|
]);
|
|
1744
2142
|
// Persist model selection for next CLI launch
|
|
1745
2143
|
if (props.settingsFile) {
|
|
@@ -1760,10 +2158,7 @@ export function App(props) {
|
|
|
1760
2158
|
const sm = new SettingsManager(props.settingsFile);
|
|
1761
2159
|
sm.load().then(() => sm.set("theme", name));
|
|
1762
2160
|
}
|
|
1763
|
-
setLiveItems((prev) => [
|
|
1764
|
-
...prev,
|
|
1765
|
-
{ kind: "info", text: `Theme switched to: ${name}`, id: getId() },
|
|
1766
|
-
]);
|
|
2161
|
+
setLiveItems((prev) => [...prev, { kind: "theme_transition", themeName: name, id: getId() }]);
|
|
1767
2162
|
}, [switchTheme, props.settingsFile]);
|
|
1768
2163
|
// All available slash commands for the command palette — ordered by how
|
|
1769
2164
|
// commonly they're used and grouped by purpose; /quit stays dead last.
|
|
@@ -1779,6 +2174,8 @@ export function App(props) {
|
|
|
1779
2174
|
"research",
|
|
1780
2175
|
"scan",
|
|
1781
2176
|
"verify",
|
|
2177
|
+
"expand",
|
|
2178
|
+
"bullet-proof",
|
|
1782
2179
|
"simplify",
|
|
1783
2180
|
"compare",
|
|
1784
2181
|
"batch",
|
|
@@ -1823,21 +2220,28 @@ export function App(props) {
|
|
|
1823
2220
|
return (_jsx(UserMessage, { text: item.text, imageCount: item.imageCount, pasteInfo: item.pasteInfo }, item.id));
|
|
1824
2221
|
case "task":
|
|
1825
2222
|
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));
|
|
2223
|
+
case "style_pack": {
|
|
2224
|
+
const names = item.added.map((id) => LANGUAGE_DISPLAY_NAMES[id]);
|
|
2225
|
+
const headerLabel = item.added.length > 1 ? "STYLE PACKS ACTIVE" : "STYLE PACK ACTIVE";
|
|
2226
|
+
return (_jsxs(Box, { marginTop: 1, flexShrink: 1, flexDirection: "column", borderStyle: "round", borderColor: theme.language, paddingX: 1, children: [_jsxs(Text, { wrap: "wrap", children: [_jsx(Text, { color: theme.language, bold: true, children: "◆ " }), _jsx(Text, { color: theme.language, bold: true, children: headerLabel })] }), _jsx(Text, { color: theme.text, bold: true, wrap: "wrap", children: names.join(", ") }), item.showSetupHint && (_jsx(Box, { marginTop: 1, children: _jsxs(Text, { wrap: "wrap", children: [_jsx(Text, { color: theme.textMuted, children: "Tip: run " }), _jsx(Text, { color: theme.language, bold: true, children: "/setup" }), _jsx(Text, { color: theme.textMuted, children: " to audit this project against the active pack(s)" })] }) }))] }, item.id));
|
|
2227
|
+
}
|
|
2228
|
+
case "setup_hint":
|
|
2229
|
+
return (_jsxs(Box, { marginTop: 1, flexShrink: 1, flexDirection: "column", borderStyle: "round", borderColor: theme.language, paddingX: 1, children: [_jsxs(Text, { wrap: "wrap", children: [_jsx(Text, { color: theme.language, bold: true, children: "◆ " }), _jsx(Text, { color: theme.language, bold: true, children: "NO STYLE PACKS DETECTED" })] }), _jsx(Text, { color: theme.textMuted, wrap: "wrap", children: "This directory has no recognized language manifest at its root." }), _jsx(Box, { marginTop: 1, children: _jsxs(Text, { wrap: "wrap", children: [_jsx(Text, { color: theme.textMuted, children: "Tip: run " }), _jsx(Text, { color: theme.language, bold: true, children: "/setup" }), _jsx(Text, { color: theme.textMuted, children: " to audit project hygiene or bootstrap a new project from scratch" })] }) })] }, item.id));
|
|
1826
2230
|
case "assistant":
|
|
1827
|
-
return (_jsx(AssistantMessage, { text: item.text, thinking: item.thinking, thinkingMs: item.thinkingMs,
|
|
2231
|
+
return (_jsx(AssistantMessage, { text: item.text, thinking: item.thinking, thinkingMs: item.thinkingMs, planMode: item.planMode }, item.id));
|
|
1828
2232
|
case "tool_start":
|
|
1829
|
-
return (_jsx(ToolExecution, { status: "running", name: item.name, args: item.args, progressOutput: item.progressOutput }, item.id));
|
|
2233
|
+
return (_jsx(ToolExecution, { status: "running", name: item.name, args: item.args, progressOutput: item.progressOutput, animateUntil: item.animateUntil }, item.id));
|
|
1830
2234
|
case "tool_done":
|
|
1831
2235
|
return (_jsx(ToolExecution, { status: "done", name: item.name, args: item.args, result: item.result, isError: item.isError, details: item.details }, item.id));
|
|
1832
2236
|
case "tool_group":
|
|
1833
2237
|
return _jsx(ToolGroupExecution, { tools: item.tools }, item.id);
|
|
1834
2238
|
case "server_tool_start":
|
|
1835
|
-
return (_jsx(ServerToolExecution, { status: "running", name: item.name, input: item.input, startedAt: item.startedAt }, item.id));
|
|
2239
|
+
return (_jsx(ServerToolExecution, { status: "running", name: item.name, input: item.input, startedAt: item.startedAt, animateUntil: item.animateUntil }, item.id));
|
|
1836
2240
|
case "server_tool_done":
|
|
1837
2241
|
return (_jsx(ServerToolExecution, { status: "done", name: item.name, input: item.input, durationMs: item.durationMs, resultType: item.resultType }, item.id));
|
|
1838
2242
|
case "error": {
|
|
1839
|
-
const
|
|
1840
|
-
return (_jsxs(Box, { marginTop: 1, flexDirection: "column", flexShrink: 1, children: [_jsxs(Text, { color: theme.error, wrap: "wrap", children: ["✗ ", item.
|
|
2243
|
+
const showMessage = item.message && item.message !== item.headline;
|
|
2244
|
+
return (_jsxs(Box, { marginTop: 1, flexDirection: "column", flexShrink: 1, children: [_jsxs(Text, { color: theme.error, wrap: "wrap", children: ["✗ ", item.headline] }), showMessage && (_jsx(Text, { color: theme.textDim, wrap: "wrap", children: ` ${item.message}` })), _jsx(Text, { color: theme.textDim, wrap: "wrap", children: ` → ${item.guidance}` })] }, item.id));
|
|
1841
2245
|
}
|
|
1842
2246
|
case "info":
|
|
1843
2247
|
return (_jsx(Box, { marginTop: 1, flexShrink: 1, children: _jsx(Text, { color: theme.textDim, wrap: "wrap", children: item.text }) }, item.id));
|
|
@@ -1845,6 +2249,34 @@ export function App(props) {
|
|
|
1845
2249
|
return (_jsx(Box, { marginTop: 1, flexShrink: 1, borderStyle: "round", borderColor: theme.success, paddingX: 1, children: _jsxs(Text, { color: theme.success, bold: true, wrap: "wrap", children: ["✨ ", item.text] }) }, item.id));
|
|
1846
2250
|
case "plan_transition":
|
|
1847
2251
|
return (_jsx(Box, { marginTop: 1, flexShrink: 1, children: _jsxs(Text, { color: theme.planPrimary, bold: true, wrap: "wrap", children: [item.active ? "● " : "● ", item.text] }) }, item.id));
|
|
2252
|
+
case "thinking_transition": {
|
|
2253
|
+
const glyphColor = item.active ? THINKING_BORDER_COLORS[0] : theme.textDim;
|
|
2254
|
+
return (_jsxs(Box, { marginTop: 1, flexShrink: 1, children: [_jsx(Text, { color: glyphColor, bold: true, children: "✻ " }), _jsx(Text, { color: item.active ? theme.accent : theme.textDim, bold: true, children: item.active ? "Thinking ON" : "Thinking OFF" })] }, item.id));
|
|
2255
|
+
}
|
|
2256
|
+
case "model_transition": {
|
|
2257
|
+
const glyphColor = THINKING_BORDER_COLORS[0];
|
|
2258
|
+
return (_jsxs(Box, { marginTop: 1, flexShrink: 1, children: [_jsx(Text, { color: glyphColor, bold: true, children: "▸ " }), _jsx(Text, { color: theme.textDim, children: "Switched to " }), _jsx(Text, { color: theme.primary, bold: true, children: item.modelName })] }, item.id));
|
|
2259
|
+
}
|
|
2260
|
+
case "theme_transition": {
|
|
2261
|
+
const glyphColor = THINKING_BORDER_COLORS[0];
|
|
2262
|
+
return (_jsxs(Box, { marginTop: 1, flexShrink: 1, children: [_jsx(Text, { color: glyphColor, bold: true, children: "◐ " }), _jsx(Text, { color: theme.textDim, children: "Theme switched to " }), _jsx(Text, { color: theme.primary, bold: true, children: item.themeName })] }, item.id));
|
|
2263
|
+
}
|
|
2264
|
+
case "plan_event": {
|
|
2265
|
+
// Plan-domain status changes (approve / reject / dismiss). Uses
|
|
2266
|
+
// theme.planPrimary to match the existing plan_transition family,
|
|
2267
|
+
// distinct from the model/thinking gradient.
|
|
2268
|
+
const label = item.event === "approved"
|
|
2269
|
+
? "Plan approved"
|
|
2270
|
+
: item.event === "rejected"
|
|
2271
|
+
? "Plan rejected"
|
|
2272
|
+
: "Plan dismissed";
|
|
2273
|
+
return (_jsxs(Box, { marginTop: 1, flexShrink: 1, children: [_jsxs(Text, { color: theme.planPrimary, bold: true, children: ["○ ", label] }), item.detail ? _jsx(Text, { color: theme.textDim, children: ` — "${item.detail}"` }) : null] }, item.id));
|
|
2274
|
+
}
|
|
2275
|
+
case "stopped":
|
|
2276
|
+
// Cancellation / abort acknowledgement (ESC, auto-setup cancel, etc.).
|
|
2277
|
+
// Muted dim treatment — this is an ack, not a state change worth a
|
|
2278
|
+
// gradient. Glyph `⊘` reads as "stop" without being alarming.
|
|
2279
|
+
return (_jsx(Box, { marginTop: 1, flexShrink: 1, children: _jsxs(Text, { color: theme.textDim, bold: true, children: ["⊘ ", item.text] }) }, item.id));
|
|
1848
2280
|
case "step_done":
|
|
1849
2281
|
return (_jsx(Box, { marginTop: 1, flexShrink: 1, children: _jsxs(Text, { wrap: "wrap", children: [_jsx(Text, { color: theme.success, bold: true, children: "✓ " }), _jsx(Text, { color: theme.success, bold: true, children: `Step ${item.stepNum} done` }), item.description ? (_jsx(Text, { color: theme.textDim, children: ` — ${item.description}` })) : null] }) }, item.id));
|
|
1850
2282
|
case "queued":
|
|
@@ -1852,7 +2284,7 @@ export function App(props) {
|
|
|
1852
2284
|
? ` (+${item.imageCount} image${item.imageCount > 1 ? "s" : ""})`
|
|
1853
2285
|
: ""] })] }, item.id));
|
|
1854
2286
|
case "compacting":
|
|
1855
|
-
return _jsx(CompactionSpinner, {}, item.id);
|
|
2287
|
+
return _jsx(CompactionSpinner, { staticDisplay: true }, item.id);
|
|
1856
2288
|
case "compacted":
|
|
1857
2289
|
return (_jsx(CompactionDone, { originalCount: item.originalCount, newCount: item.newCount, tokensBefore: item.tokensBefore, tokensAfter: item.tokensAfter }, item.id));
|
|
1858
2290
|
case "duration":
|
|
@@ -1928,8 +2360,8 @@ export function App(props) {
|
|
|
1928
2360
|
setLiveItems((prev) => [
|
|
1929
2361
|
...prev,
|
|
1930
2362
|
isAbort
|
|
1931
|
-
? { kind: "
|
|
1932
|
-
:
|
|
2363
|
+
? { kind: "stopped", text: "Request was stopped.", id: getId() }
|
|
2364
|
+
: toErrorItem(err, getId()),
|
|
1933
2365
|
]);
|
|
1934
2366
|
setRunAllTasks(false);
|
|
1935
2367
|
}
|
|
@@ -1947,7 +2379,9 @@ export function App(props) {
|
|
|
1947
2379
|
startTaskRef.current = startTask;
|
|
1948
2380
|
useEffect(() => {
|
|
1949
2381
|
runAllTasksRef.current = runAllTasks;
|
|
1950
|
-
|
|
2382
|
+
if (props.sessionStore)
|
|
2383
|
+
props.sessionStore.runAllTasks = runAllTasks;
|
|
2384
|
+
}, [runAllTasks, props.sessionStore]);
|
|
1951
2385
|
const startPixelFix = useCallback((errorId) => {
|
|
1952
2386
|
void (async () => {
|
|
1953
2387
|
try {
|
|
@@ -1969,11 +2403,35 @@ export function App(props) {
|
|
|
1969
2403
|
log("WARN", "pixel", `chdir failed: ${err.message}`);
|
|
1970
2404
|
}
|
|
1971
2405
|
cwdRef.current = prep.projectPath;
|
|
2406
|
+
repoMapDirtyRef.current = true;
|
|
2407
|
+
repoMapMarkdownRef.current = "";
|
|
2408
|
+
repoMapSnapshotRef.current = undefined;
|
|
2409
|
+
repoMapChangedCountRef.current = 0;
|
|
2410
|
+
repoMapCacheRef.current = createRepoMapCache();
|
|
2411
|
+
props.repoMapChangedFilesRef?.current.clear();
|
|
2412
|
+
props.repoMapReadFilesRef?.current.clear();
|
|
1972
2413
|
setDisplayedCwd(prep.projectPath);
|
|
2414
|
+
let toolsForPixelFix = currentToolsRef.current;
|
|
1973
2415
|
if (props.rebuildToolsForCwd) {
|
|
1974
|
-
|
|
2416
|
+
toolsForPixelFix = props.rebuildToolsForCwd(prep.projectPath);
|
|
2417
|
+
currentToolsRef.current = toolsForPixelFix;
|
|
2418
|
+
setCurrentTools(toolsForPixelFix);
|
|
1975
2419
|
}
|
|
1976
|
-
|
|
2420
|
+
// Pixel-fix swaps the project root — reset injected packs so the
|
|
2421
|
+
// new project re-detects from scratch on the next tool call. Also
|
|
2422
|
+
// reset the setup-hint flag so the new project's first badge re-
|
|
2423
|
+
// surfaces the tip (different project, may need the reminder).
|
|
2424
|
+
injectedLanguagesRef.current = new Set();
|
|
2425
|
+
setupHintShownRef.current = false;
|
|
2426
|
+
const detectedForPixelFix = detectLanguages(prep.projectPath);
|
|
2427
|
+
injectedLanguagesRef.current = detectedForPixelFix;
|
|
2428
|
+
const newSystemPrompt = await rebuildSystemPrompt({
|
|
2429
|
+
cwd: prep.projectPath,
|
|
2430
|
+
planMode: false,
|
|
2431
|
+
clearApprovedPlan: true,
|
|
2432
|
+
activeLanguages: detectedForPixelFix,
|
|
2433
|
+
tools: toolsForPixelFix,
|
|
2434
|
+
});
|
|
1977
2435
|
// Now that the cwd swap is committed, reset chat. Doing this BEFORE
|
|
1978
2436
|
// the chdir would print a banner with the old cwd, then bumping
|
|
1979
2437
|
// staticKey would print a second banner with the new cwd — leaving
|
|
@@ -2010,30 +2468,37 @@ export function App(props) {
|
|
|
2010
2468
|
log("ERROR", "pixel", msg);
|
|
2011
2469
|
currentPixelFixRef.current = null;
|
|
2012
2470
|
setRunAllPixel(false);
|
|
2013
|
-
setLiveItems((prev) => [...prev,
|
|
2471
|
+
setLiveItems((prev) => [...prev, toErrorItem(err, getId())]);
|
|
2014
2472
|
}
|
|
2015
2473
|
})();
|
|
2016
2474
|
}, [props.cwd, stdout, agentLoop, currentProvider, currentModel]);
|
|
2017
2475
|
startPixelFixRef.current = startPixelFix;
|
|
2018
|
-
|
|
2476
|
+
// Seed from sessionStore so "Fix All" chaining survives a deferred
|
|
2477
|
+
// resetUI() if it fires between pixel fixes (e.g. user toggled a pane).
|
|
2478
|
+
const [runAllPixel, setRunAllPixel] = useState(props.sessionStore?.runAllPixel ?? false);
|
|
2019
2479
|
useEffect(() => {
|
|
2020
2480
|
runAllPixelRef.current = runAllPixel;
|
|
2021
|
-
|
|
2481
|
+
if (props.sessionStore)
|
|
2482
|
+
props.sessionStore.runAllPixel = runAllPixel;
|
|
2483
|
+
}, [runAllPixel, props.sessionStore]);
|
|
2022
2484
|
const isTaskView = overlay === "tasks";
|
|
2023
2485
|
const isSkillsView = overlay === "skills";
|
|
2024
2486
|
const isPlanView = overlay === "plan";
|
|
2025
2487
|
const isEyesView = overlay === "eyes";
|
|
2026
2488
|
const isPixelView = overlay === "pixel";
|
|
2027
2489
|
const isOverlayView = isTaskView || isSkillsView || isPlanView || isEyesView || isPixelView;
|
|
2028
|
-
return (_jsxs(Box, { flexDirection: "column", width: columns, children: [_jsx(Static, { items: isOverlayView ? [] : history, style: { width: "100%" }, children: (item) => (_jsx(Box, { flexDirection: "column", paddingRight: 1, children: renderItem(item) }, item.id)) }, `${resizeKey}-${staticKey}`), isTaskView ? (_jsx(TaskOverlay, { cwd: props.cwd, agentRunning: agentLoop.isRunning, onClose: () => {
|
|
2029
|
-
if (props.resetUI && props.sessionStore) {
|
|
2490
|
+
return (_jsxs(Box, { flexDirection: "column", width: columns, children: [_jsx(Static, { items: isOverlayView && !agentLoop.isRunning ? [] : history, style: { width: "100%" }, children: (item) => (_jsx(Box, { flexDirection: "column", paddingRight: 1, children: renderItem(item) }, item.id)) }, `${resizeKey}-${staticKey}`), isTaskView ? (_jsx(TaskOverlay, { cwd: props.cwd, agentRunning: agentLoop.isRunning, onClose: () => {
|
|
2491
|
+
if (props.resetUI && props.sessionStore && !agentLoop.isRunning) {
|
|
2030
2492
|
props.sessionStore.overlay = null;
|
|
2031
2493
|
props.resetUI();
|
|
2032
2494
|
}
|
|
2033
2495
|
else {
|
|
2034
|
-
|
|
2496
|
+
if (props.sessionStore) {
|
|
2497
|
+
props.sessionStore.overlay = null;
|
|
2498
|
+
if (agentLoop.isRunning)
|
|
2499
|
+
props.sessionStore.pendingResetUI = true;
|
|
2500
|
+
}
|
|
2035
2501
|
setTaskCount(getTaskCount(props.cwd));
|
|
2036
|
-
setStaticKey((k) => k + 1);
|
|
2037
2502
|
setOverlay(null);
|
|
2038
2503
|
}
|
|
2039
2504
|
}, onWorkOnTask: (title, prompt, taskId) => {
|
|
@@ -2048,13 +2513,16 @@ export function App(props) {
|
|
|
2048
2513
|
startTask(next.title, next.prompt, next.id);
|
|
2049
2514
|
}
|
|
2050
2515
|
} })) : isPixelView ? (_jsx(PixelOverlay, { version: props.version, agentRunning: agentLoop.isRunning, onClose: () => {
|
|
2051
|
-
if (props.resetUI && props.sessionStore) {
|
|
2516
|
+
if (props.resetUI && props.sessionStore && !agentLoop.isRunning) {
|
|
2052
2517
|
props.sessionStore.overlay = null;
|
|
2053
2518
|
props.resetUI();
|
|
2054
2519
|
}
|
|
2055
2520
|
else {
|
|
2056
|
-
|
|
2057
|
-
|
|
2521
|
+
if (props.sessionStore) {
|
|
2522
|
+
props.sessionStore.overlay = null;
|
|
2523
|
+
if (agentLoop.isRunning)
|
|
2524
|
+
props.sessionStore.pendingResetUI = true;
|
|
2525
|
+
}
|
|
2058
2526
|
setOverlay(null);
|
|
2059
2527
|
}
|
|
2060
2528
|
}, onFixOne: (entry) => {
|
|
@@ -2068,38 +2536,48 @@ export function App(props) {
|
|
|
2068
2536
|
setRunAllPixel(true);
|
|
2069
2537
|
startPixelFix(first.errorId);
|
|
2070
2538
|
} })) : isSkillsView ? (_jsx(SkillsOverlay, { cwd: props.cwd, onClose: () => {
|
|
2071
|
-
if (props.resetUI && props.sessionStore) {
|
|
2539
|
+
if (props.resetUI && props.sessionStore && !agentLoop.isRunning) {
|
|
2072
2540
|
props.sessionStore.overlay = null;
|
|
2073
2541
|
props.resetUI();
|
|
2074
2542
|
}
|
|
2075
2543
|
else {
|
|
2076
|
-
|
|
2077
|
-
|
|
2544
|
+
if (props.sessionStore) {
|
|
2545
|
+
props.sessionStore.overlay = null;
|
|
2546
|
+
if (agentLoop.isRunning)
|
|
2547
|
+
props.sessionStore.pendingResetUI = true;
|
|
2548
|
+
}
|
|
2078
2549
|
setOverlay(null);
|
|
2079
2550
|
}
|
|
2080
2551
|
} })) : isEyesView ? (_jsx(EyesOverlay, { cwd: props.cwd, onClose: () => {
|
|
2081
|
-
if (props.resetUI && props.sessionStore) {
|
|
2552
|
+
if (props.resetUI && props.sessionStore && !agentLoop.isRunning) {
|
|
2082
2553
|
props.sessionStore.overlay = null;
|
|
2083
2554
|
props.resetUI();
|
|
2084
2555
|
}
|
|
2085
2556
|
else {
|
|
2086
|
-
|
|
2557
|
+
if (props.sessionStore) {
|
|
2558
|
+
props.sessionStore.overlay = null;
|
|
2559
|
+
if (agentLoop.isRunning)
|
|
2560
|
+
props.sessionStore.pendingResetUI = true;
|
|
2561
|
+
}
|
|
2087
2562
|
setEyesCount(isEyesActive(props.cwd) ? journalCount({ status: "open" }, props.cwd) : undefined);
|
|
2088
|
-
setStaticKey((k) => k + 1);
|
|
2089
2563
|
setOverlay(null);
|
|
2090
2564
|
}
|
|
2091
2565
|
}, onQueueMessage: (msg) => {
|
|
2092
2566
|
agentLoop.queueMessage(msg);
|
|
2093
2567
|
} })) : isPlanView ? (_jsx(PlanOverlay, { cwd: props.cwd, autoExpandNewest: planAutoExpand, onClose: () => {
|
|
2094
2568
|
planOverlayPendingRef.current = false;
|
|
2095
|
-
if (props.resetUI && props.sessionStore) {
|
|
2569
|
+
if (props.resetUI && props.sessionStore && !agentLoop.isRunning) {
|
|
2096
2570
|
props.sessionStore.overlay = null;
|
|
2097
2571
|
props.sessionStore.planAutoExpand = false;
|
|
2098
2572
|
props.resetUI();
|
|
2099
2573
|
}
|
|
2100
2574
|
else {
|
|
2101
|
-
|
|
2102
|
-
|
|
2575
|
+
if (props.sessionStore) {
|
|
2576
|
+
props.sessionStore.overlay = null;
|
|
2577
|
+
props.sessionStore.planAutoExpand = false;
|
|
2578
|
+
if (agentLoop.isRunning)
|
|
2579
|
+
props.sessionStore.pendingResetUI = true;
|
|
2580
|
+
}
|
|
2103
2581
|
setPlanAutoExpand(false);
|
|
2104
2582
|
setOverlay(null);
|
|
2105
2583
|
}
|
|
@@ -2115,7 +2593,10 @@ export function App(props) {
|
|
|
2115
2593
|
const planContent = await import("node:fs/promises").then(({ readFile }) => readFile(planPath, "utf-8"));
|
|
2116
2594
|
const steps = extractPlanSteps(planContent);
|
|
2117
2595
|
// Build the new system prompt with the approved plan baked in.
|
|
2118
|
-
const newPrompt = await
|
|
2596
|
+
const newPrompt = await rebuildSystemPrompt({
|
|
2597
|
+
planMode: false,
|
|
2598
|
+
approvedPlanPath: planPath,
|
|
2599
|
+
});
|
|
2119
2600
|
// Create a new session file BEFORE remount so the new tree
|
|
2120
2601
|
// picks it up via sessionStore.sessionPath.
|
|
2121
2602
|
let newSessionPath;
|
|
@@ -2137,7 +2618,7 @@ export function App(props) {
|
|
|
2137
2618
|
sessionPath: newSessionPath,
|
|
2138
2619
|
pendingAction: {
|
|
2139
2620
|
prompt: "The plan has been approved. Implement it now, following each step in order.",
|
|
2140
|
-
|
|
2621
|
+
planEvent: { event: "approved" },
|
|
2141
2622
|
},
|
|
2142
2623
|
});
|
|
2143
2624
|
return;
|
|
@@ -2170,7 +2651,7 @@ export function App(props) {
|
|
|
2170
2651
|
catch (err) {
|
|
2171
2652
|
const errMsg = err instanceof Error ? err.message : String(err);
|
|
2172
2653
|
log("ERROR", "error", errMsg);
|
|
2173
|
-
setLiveItems((prev) => [...prev,
|
|
2654
|
+
setLiveItems((prev) => [...prev, toErrorItem(err, getId())]);
|
|
2174
2655
|
}
|
|
2175
2656
|
})();
|
|
2176
2657
|
}, onReject: (planPath, feedback) => {
|
|
@@ -2185,7 +2666,7 @@ export function App(props) {
|
|
|
2185
2666
|
props.resetUI({
|
|
2186
2667
|
pendingAction: {
|
|
2187
2668
|
prompt: rejectionMsg,
|
|
2188
|
-
|
|
2669
|
+
planEvent: { event: "rejected", detail: feedback },
|
|
2189
2670
|
},
|
|
2190
2671
|
});
|
|
2191
2672
|
return;
|
|
@@ -2202,36 +2683,52 @@ export function App(props) {
|
|
|
2202
2683
|
void agentLoop.run(rejectionMsg).catch((err) => {
|
|
2203
2684
|
const errMsg = err instanceof Error ? err.message : String(err);
|
|
2204
2685
|
log("ERROR", "error", errMsg);
|
|
2205
|
-
setLiveItems((prev) => [...prev,
|
|
2686
|
+
setLiveItems((prev) => [...prev, toErrorItem(err, getId())]);
|
|
2206
2687
|
});
|
|
2207
|
-
} })) : (_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,
|
|
2208
|
-
? THINKING_BORDER_COLORS[thinkingBorderFrame]
|
|
2209
|
-
: "transparent", paddingLeft: 1, paddingRight: 1, width: columns, children: _jsx(ActivityIndicator, { phase: agentLoop.activityPhase, elapsedMs: agentLoop.elapsedMs, runStartRef: agentLoop.runStartRef, thinkingMs: agentLoop.thinkingMs, isThinking: agentLoop.isThinking, tokenEstimate: agentLoop.streamedTokenEstimate, charCountRef: agentLoop.charCountRef, realTokensAccumRef: agentLoop.realTokensAccumRef, userMessage: lastUserMessage, activeToolNames: agentLoop.activeToolCalls.map((tc) => tc.name), planMode: planMode, retryInfo: agentLoop.retryInfo, planDone: planSteps.filter((s) => s.completed).length, planTotal: planSteps.length }) })) : agentLoop.stallError ? (_jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(Text, { color: theme.warning, children: "⚠ API provider stream interrupted — retries exhausted." }), _jsx(Text, { color: theme.textDim, children: " Your conversation is preserved. Send a message to continue." })] })) : (doneStatus &&
|
|
2688
|
+
} })) : (_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 &&
|
|
2210
2689
|
!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: () => {
|
|
2211
|
-
|
|
2690
|
+
// While the agent is running, skip the screen-clear + staticKey
|
|
2691
|
+
// bump that would otherwise wipe the chat history from scrollback.
|
|
2692
|
+
// Just flip the overlay state — Ink's log-update handles the
|
|
2693
|
+
// live-area transition (chat input → TaskOverlay) natively, and
|
|
2694
|
+
// the chat history above stays in scrollback. When the overlay
|
|
2695
|
+
// closes, the history is still there (banner included).
|
|
2696
|
+
if (props.resetUI && props.sessionStore && !agentLoop.isRunning) {
|
|
2212
2697
|
props.sessionStore.overlay = "tasks";
|
|
2213
2698
|
props.resetUI();
|
|
2214
2699
|
}
|
|
2215
2700
|
else {
|
|
2216
|
-
|
|
2701
|
+
if (props.sessionStore) {
|
|
2702
|
+
props.sessionStore.overlay = "tasks";
|
|
2703
|
+
if (agentLoop.isRunning)
|
|
2704
|
+
props.sessionStore.pendingResetUI = true;
|
|
2705
|
+
}
|
|
2217
2706
|
setOverlay("tasks");
|
|
2218
2707
|
}
|
|
2219
2708
|
}, onToggleSkills: () => {
|
|
2220
|
-
if (props.resetUI && props.sessionStore) {
|
|
2709
|
+
if (props.resetUI && props.sessionStore && !agentLoop.isRunning) {
|
|
2221
2710
|
props.sessionStore.overlay = "skills";
|
|
2222
2711
|
props.resetUI();
|
|
2223
2712
|
}
|
|
2224
2713
|
else {
|
|
2225
|
-
|
|
2714
|
+
if (props.sessionStore) {
|
|
2715
|
+
props.sessionStore.overlay = "skills";
|
|
2716
|
+
if (agentLoop.isRunning)
|
|
2717
|
+
props.sessionStore.pendingResetUI = true;
|
|
2718
|
+
}
|
|
2226
2719
|
setOverlay("skills");
|
|
2227
2720
|
}
|
|
2228
2721
|
}, onTogglePixel: () => {
|
|
2229
|
-
if (props.resetUI && props.sessionStore) {
|
|
2722
|
+
if (props.resetUI && props.sessionStore && !agentLoop.isRunning) {
|
|
2230
2723
|
props.sessionStore.overlay = "pixel";
|
|
2231
2724
|
props.resetUI();
|
|
2232
2725
|
}
|
|
2233
2726
|
else {
|
|
2234
|
-
|
|
2727
|
+
if (props.sessionStore) {
|
|
2728
|
+
props.sessionStore.overlay = "pixel";
|
|
2729
|
+
if (agentLoop.isRunning)
|
|
2730
|
+
props.sessionStore.pendingResetUI = true;
|
|
2731
|
+
}
|
|
2235
2732
|
setOverlay("pixel");
|
|
2236
2733
|
}
|
|
2237
2734
|
}, onTogglePlanMode: () => {
|
|
@@ -2247,6 +2744,13 @@ export function App(props) {
|
|
|
2247
2744
|
id: getId(),
|
|
2248
2745
|
},
|
|
2249
2746
|
]);
|
|
2250
|
-
}, cwd: props.cwd, commands: allCommands, eyesCount: eyesCount }), 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: displayedCwd, gitBranch: gitBranch, thinkingEnabled:
|
|
2747
|
+
}, cwd: props.cwd, commands: allCommands, eyesCount: eyesCount }), 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, contextWindowOptions: contextWindowOptions, cwd: displayedCwd, gitBranch: gitBranch, thinkingLevel: thinkingEnabled ? getMaxThinkingLevel(currentModel) : undefined, planMode: planMode, exitPending: exitPending })), (bgTasks.length > 0 || (eyesCount !== undefined && eyesCount > 0) || updatePending) && (_jsxs(Box, { children: [bgTasks.length > 0 && (_jsx(BackgroundTasksBar, { tasks: bgTasks, focused: taskBarFocused, expanded: taskBarExpanded, selectedIndex: selectedTaskIndex, onExpand: handleTaskBarExpand, onCollapse: handleTaskBarCollapse, onKill: handleTaskKill, onExit: handleTaskBarExit, onNavigate: handleTaskNavigate })), eyesCount !== undefined && eyesCount > 0 && (_jsx(Box, { paddingLeft: bgTasks.length > 0 ? 2 : 1, paddingRight: 1, children: _jsx(Text, { color: theme.accent, bold: true, children: `${eyesCount} eyes signal${eyesCount === 1 ? "" : "s"} · Run /eyes-improve to enhance EZ Coder` }) })), updatePending && (_jsx(Box, { paddingLeft: bgTasks.length > 0 || (eyesCount !== undefined && eyesCount > 0) ? 2 : 1, paddingRight: 1, children: _jsx(Text, { color: theme.success, bold: true, children: "\u2728 Update ready \u00B7 restart to apply" }) }))] }))] }))] }));
|
|
2748
|
+
}
|
|
2749
|
+
function formatRepoMapCommandOutput(enabled, markdown, refreshed) {
|
|
2750
|
+
const status = enabled ? "on" : "off";
|
|
2751
|
+
const prefix = refreshed
|
|
2752
|
+
? `Dynamic repo map refreshed · injection: ${status}`
|
|
2753
|
+
: `Dynamic repo map · injection: ${status}`;
|
|
2754
|
+
return `${prefix}\n\n${markdown}`;
|
|
2251
2755
|
}
|
|
2252
2756
|
//# sourceMappingURL=App.js.map
|