@prestyj/cli 4.3.164 → 4.3.201
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +106 -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/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 +480 -100
- 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/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.map +1 -1
- package/dist/core/style-packs/index.js +8 -9
- package/dist/core/style-packs/index.js.map +1 -1
- 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.map +1 -1
- package/dist/system-prompt.js +105 -108
- 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 +54 -5
- package/dist/ui/App.d.ts.map +1 -1
- package/dist/ui/App.js +533 -241
- 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/render.d.ts +44 -5
- package/dist/ui/render.d.ts.map +1 -1
- package/dist/ui/render.js +56 -8
- package/dist/ui/render.js.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 +8 -5
- 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,16 +35,16 @@ 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";
|
|
49
50
|
import { isFirstTimeSetup, markSetupAudited, getAnnouncedLanguages, markLanguagesAnnounced, } from "../core/setup-history.js";
|
|
@@ -51,49 +52,47 @@ import { loadCustomCommands } from "../core/custom-commands.js";
|
|
|
51
52
|
import { buildSystemPrompt } from "../system-prompt.js";
|
|
52
53
|
import { detectLanguages, LANGUAGE_DISPLAY_NAMES, } from "../core/language-detector.js";
|
|
53
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";
|
|
54
57
|
import { extractPlanSteps, findCompletedMarkers, markStepsCompleted, segmentDisplayText, stripDoneMarkers, } from "../utils/plan-steps.js";
|
|
55
58
|
import { getMCPServers } from "../core/mcp/index.js";
|
|
56
59
|
import { trimFlushedItems, flushOnTurnText, flushOnTurnEnd, flushOverflow, } from "./live-item-flush.js";
|
|
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
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
(lower.includes("model") &&
|
|
90
|
-
(lower.includes("not exist") || lower.includes("not found") || lower.includes("no access")))) {
|
|
91
|
-
return "Use /model to switch to a different model, or check that your account has access to the requested model.";
|
|
92
|
-
}
|
|
93
|
-
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
|
+
};
|
|
94
92
|
}
|
|
95
93
|
/** Tools that get aggregated into a single compact group when concurrent. */
|
|
96
94
|
const AGGREGATABLE_TOOLS = new Set(["read", "grep", "find", "ls"]);
|
|
95
|
+
const RUNNING_INDICATOR_ANIMATION_MS = 1_200;
|
|
97
96
|
/**
|
|
98
97
|
* Cap memory by replacing old items with tiny tombstones. Ink's <Static>
|
|
99
98
|
* tracks rendered items by array length — the array must never shrink, but
|
|
@@ -315,20 +314,22 @@ export function App(props) {
|
|
|
315
314
|
// Items from the current/last turn — rendered in the live area so they stay visible
|
|
316
315
|
const [liveItems, setLiveItems] = useState([]);
|
|
317
316
|
// overlay seeded from sessionStore (lives across remount). Falls back to
|
|
318
|
-
// props.initialOverlay (CLI launched with one)
|
|
319
|
-
|
|
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));
|
|
320
324
|
const [taskCount, setTaskCount] = useState(() => getTaskCount(props.cwd));
|
|
321
325
|
const [eyesCount, setEyesCount] = useState(() => isEyesActive(props.cwd) ? journalCount({ status: "open" }, props.cwd) : undefined);
|
|
322
326
|
const [updatePending, setUpdatePending] = useState(() => getPendingUpdate(props.version) !== null);
|
|
323
|
-
// Seed
|
|
324
|
-
//
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
// remount across all panes.)
|
|
328
|
-
const [runAllTasks, setRunAllTasks] = useState(() => props.sessionStore?.runAllTasks ?? false);
|
|
329
|
-
const runAllTasksRef = useRef(runAllTasks);
|
|
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);
|
|
330
331
|
const startTaskRef = useRef(() => { });
|
|
331
|
-
const runAllPixelRef = useRef(false);
|
|
332
|
+
const runAllPixelRef = useRef(props.sessionStore?.runAllPixel ?? false);
|
|
332
333
|
const currentPixelFixRef = useRef(null);
|
|
333
334
|
const startPixelFixRef = useRef(() => { });
|
|
334
335
|
const cwdRef = useRef(props.cwd);
|
|
@@ -341,13 +342,41 @@ export function App(props) {
|
|
|
341
342
|
const [currentModel, setCurrentModel] = useState(props.model);
|
|
342
343
|
const [currentProvider, setCurrentProvider] = useState(props.provider);
|
|
343
344
|
const [currentTools, setCurrentTools] = useState(props.tools);
|
|
345
|
+
const currentToolsRef = useRef(props.tools);
|
|
344
346
|
const [thinkingEnabled, setThinkingEnabled] = useState(!!props.thinking);
|
|
345
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());
|
|
346
354
|
const [planAutoExpand, setPlanAutoExpand] = useState(props.sessionStore?.planAutoExpand ?? false);
|
|
347
355
|
const approvedPlanPathRef = useRef(props.sessionStore?.approvedPlanPath);
|
|
348
356
|
const planStepsRef = useRef(props.sessionStore?.planSteps ?? []);
|
|
349
357
|
const [planSteps, setPlanSteps] = useState(props.sessionStore?.planSteps ?? []);
|
|
350
|
-
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
|
+
})());
|
|
351
380
|
const sessionManagerRef = useRef(props.sessionsDir ? new SessionManager(props.sessionsDir) : null);
|
|
352
381
|
const sessionPathRef = useRef(props.sessionStore?.sessionPath ?? props.sessionPath);
|
|
353
382
|
const persistedIndexRef = useRef(messagesRef.current.length);
|
|
@@ -400,9 +429,9 @@ export function App(props) {
|
|
|
400
429
|
}, [currentProvider, onRuntimeStateChange]);
|
|
401
430
|
useEffect(() => {
|
|
402
431
|
onRuntimeStateChange?.({
|
|
403
|
-
thinking: thinkingEnabled ? (
|
|
432
|
+
thinking: thinkingEnabled ? getMaxThinkingLevel(currentModel) : undefined,
|
|
404
433
|
});
|
|
405
|
-
}, [thinkingEnabled,
|
|
434
|
+
}, [thinkingEnabled, currentModel, onRuntimeStateChange]);
|
|
406
435
|
// Mirror session state into renderApp's closure so resetUI() can re-seed
|
|
407
436
|
// the conversation on remount. Each panel that previously did a bare ANSI
|
|
408
437
|
// screen clear (overlay open/close, plan accept/reject, /clear, startTask)
|
|
@@ -432,6 +461,7 @@ export function App(props) {
|
|
|
432
461
|
const activeApiKey = currentCreds?.accessToken ?? props.apiKey;
|
|
433
462
|
const activeAccountId = currentCreds?.accountId ?? props.accountId;
|
|
434
463
|
const activeBaseUrl = currentCreds?.baseUrl ?? props.baseUrl;
|
|
464
|
+
const contextWindowOptions = useMemo(() => ({ provider: currentProvider, accountId: activeAccountId }), [currentProvider, activeAccountId]);
|
|
435
465
|
// Load git branch — re-runs whenever the displayed cwd changes (e.g. when
|
|
436
466
|
// a pixel fix moves the agent into a different project root).
|
|
437
467
|
useEffect(() => {
|
|
@@ -453,13 +483,30 @@ export function App(props) {
|
|
|
453
483
|
useEffect(() => {
|
|
454
484
|
reloadCustomCommands();
|
|
455
485
|
}, [reloadCustomCommands]);
|
|
486
|
+
useEffect(() => {
|
|
487
|
+
currentToolsRef.current = currentTools;
|
|
488
|
+
}, [currentTools]);
|
|
456
489
|
// ── Plan mode wiring ─────────────────────────────────────
|
|
457
490
|
// Sync planModeRef with React state
|
|
458
491
|
useEffect(() => {
|
|
492
|
+
planModeStateRef.current = planMode;
|
|
459
493
|
if (props.planModeRef) {
|
|
460
494
|
props.planModeRef.current = planMode;
|
|
461
495
|
}
|
|
462
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]);
|
|
463
510
|
/**
|
|
464
511
|
* Unified "apply detection result" pipeline. Called from three sites:
|
|
465
512
|
* 1. Initial mount (existing project at startup).
|
|
@@ -502,10 +549,7 @@ export function App(props) {
|
|
|
502
549
|
}
|
|
503
550
|
injectedLanguagesRef.current = detected;
|
|
504
551
|
try {
|
|
505
|
-
|
|
506
|
-
if (messagesRef.current[0]?.role === "system") {
|
|
507
|
-
messagesRef.current[0] = { role: "system", content: newPrompt };
|
|
508
|
-
}
|
|
552
|
+
await replaceSystemPrompt({ cwd, activeLanguages: detected });
|
|
509
553
|
const verifyCmds = detectVerifyCommands(cwd, detected);
|
|
510
554
|
const tag = source === "initial" ? "Initial style packs" : "Style pack(s) loaded";
|
|
511
555
|
log("INFO", "language", `${tag}: ${added.join(", ")}`, {
|
|
@@ -549,16 +593,8 @@ export function App(props) {
|
|
|
549
593
|
}, []);
|
|
550
594
|
// Rebuild system prompt when plan mode changes
|
|
551
595
|
useEffect(() => {
|
|
552
|
-
void (
|
|
553
|
-
|
|
554
|
-
if (messagesRef.current[0]?.role === "system") {
|
|
555
|
-
messagesRef.current[0] = {
|
|
556
|
-
role: "system",
|
|
557
|
-
content: newPrompt,
|
|
558
|
-
};
|
|
559
|
-
}
|
|
560
|
-
})();
|
|
561
|
-
}, [planMode, props.cwd, props.skills]);
|
|
596
|
+
void replaceSystemPrompt({ planMode });
|
|
597
|
+
}, [planMode, replaceSystemPrompt]);
|
|
562
598
|
// Wire onEnterPlan callback ref
|
|
563
599
|
useEffect(() => {
|
|
564
600
|
if (props.onEnterPlanRef) {
|
|
@@ -577,8 +613,10 @@ export function App(props) {
|
|
|
577
613
|
if (props.onExitPlanRef) {
|
|
578
614
|
props.onExitPlanRef.current = async (planPath) => {
|
|
579
615
|
// Deactivate plan mode, store approved plan path, open pane
|
|
616
|
+
planModeStateRef.current = false;
|
|
580
617
|
setPlanMode(false);
|
|
581
618
|
approvedPlanPathRef.current = planPath;
|
|
619
|
+
await replaceSystemPrompt({ planMode: false, approvedPlanPath: planPath });
|
|
582
620
|
// Use setTimeout to open pane after the current tool execution completes,
|
|
583
621
|
// so the turn can finish and the UI transitions cleanly
|
|
584
622
|
// Flag that the plan overlay is about to open — suppresses the
|
|
@@ -607,28 +645,43 @@ export function App(props) {
|
|
|
607
645
|
planPath);
|
|
608
646
|
};
|
|
609
647
|
}
|
|
610
|
-
}, [props.onExitPlanRef, stdout]);
|
|
611
|
-
const
|
|
648
|
+
}, [props.onExitPlanRef, replaceSystemPrompt, stdout]);
|
|
649
|
+
const appendMessagesToSession = useCallback(async (sessionPath, messages, startIndex) => {
|
|
612
650
|
const sm = sessionManagerRef.current;
|
|
651
|
+
if (!sm)
|
|
652
|
+
return;
|
|
653
|
+
await appendSessionMessages(sm, sessionPath, messages, startIndex);
|
|
654
|
+
}, []);
|
|
655
|
+
const persistCompactedSession = useCallback(async (compactedMessages) => {
|
|
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 () => {
|
|
613
674
|
const sp = sessionPathRef.current;
|
|
614
|
-
if (!
|
|
675
|
+
if (!sp)
|
|
615
676
|
return;
|
|
616
677
|
const allMsgs = messagesRef.current;
|
|
617
|
-
|
|
618
|
-
const msg = allMsgs[i];
|
|
619
|
-
if (msg.role === "system")
|
|
620
|
-
continue;
|
|
621
|
-
const entry = {
|
|
622
|
-
type: "message",
|
|
623
|
-
id: crypto.randomUUID(),
|
|
624
|
-
parentId: null,
|
|
625
|
-
timestamp: new Date().toISOString(),
|
|
626
|
-
message: msg,
|
|
627
|
-
};
|
|
628
|
-
await sm.appendEntry(sp, entry);
|
|
629
|
-
}
|
|
678
|
+
await appendMessagesToSession(sp, allMsgs, persistedIndexRef.current);
|
|
630
679
|
persistedIndexRef.current = allMsgs.length;
|
|
631
|
-
|
|
680
|
+
if (sessionStore) {
|
|
681
|
+
sessionStore.messages = [...allMsgs];
|
|
682
|
+
sessionStore.sessionPath = sp;
|
|
683
|
+
}
|
|
684
|
+
}, [appendMessagesToSession, sessionStore]);
|
|
632
685
|
/**
|
|
633
686
|
* Run the language detector against the current cwd. If the detected set is a
|
|
634
687
|
* strict superset of what's already injected, rebuild the system prompt with
|
|
@@ -653,20 +706,18 @@ export function App(props) {
|
|
|
653
706
|
await applyLanguageDetectionRef.current("tool");
|
|
654
707
|
};
|
|
655
708
|
// ── Compaction ─────────────────────────────────────────
|
|
656
|
-
// Load settings for auto-compaction
|
|
709
|
+
// Load settings for auto-compaction
|
|
657
710
|
const settingsRef = useRef(null);
|
|
658
|
-
const [buddyEnabled, setBuddyEnabled] = useState(false);
|
|
659
711
|
useEffect(() => {
|
|
660
712
|
if (props.settingsFile) {
|
|
661
713
|
const sm = new SettingsManager(props.settingsFile);
|
|
662
714
|
sm.load().then(() => {
|
|
663
715
|
settingsRef.current = sm;
|
|
664
|
-
setBuddyEnabled(sm.get("buddyEnabled") ?? false);
|
|
665
716
|
});
|
|
666
717
|
}
|
|
667
718
|
}, [props.settingsFile]);
|
|
668
719
|
const compactConversation = useCallback(async (messages) => {
|
|
669
|
-
const contextWindow = getContextWindow(currentModel);
|
|
720
|
+
const contextWindow = getContextWindow(currentModel, contextWindowOptions);
|
|
670
721
|
const tokensBefore = estimateConversationTokens(messages);
|
|
671
722
|
const spinId = getId();
|
|
672
723
|
log("INFO", "compaction", `Running compaction`, {
|
|
@@ -679,14 +730,20 @@ export function App(props) {
|
|
|
679
730
|
try {
|
|
680
731
|
// Resolve fresh credentials for compaction too
|
|
681
732
|
let compactApiKey = activeApiKey;
|
|
733
|
+
let compactAccountId = activeAccountId;
|
|
734
|
+
let compactBaseUrl = activeBaseUrl;
|
|
682
735
|
if (props.authStorage) {
|
|
683
736
|
const creds = await props.authStorage.resolveCredentials(currentProvider);
|
|
684
737
|
compactApiKey = creds.accessToken;
|
|
738
|
+
compactAccountId = creds.accountId;
|
|
739
|
+
compactBaseUrl = creds.baseUrl ?? compactBaseUrl;
|
|
685
740
|
}
|
|
686
741
|
const result = await compact(messages, {
|
|
687
742
|
provider: currentProvider,
|
|
688
743
|
model: currentModel,
|
|
689
744
|
apiKey: compactApiKey,
|
|
745
|
+
accountId: compactAccountId,
|
|
746
|
+
baseUrl: compactBaseUrl,
|
|
690
747
|
contextWindow,
|
|
691
748
|
signal: undefined,
|
|
692
749
|
approvedPlanPath: approvedPlanPathRef.current,
|
|
@@ -715,65 +772,118 @@ export function App(props) {
|
|
|
715
772
|
const msg = err instanceof Error ? err.message : String(err);
|
|
716
773
|
log("ERROR", "compaction", `Compaction failed: ${msg}`);
|
|
717
774
|
// Replace spinner with error
|
|
718
|
-
setLiveItems((prev) => prev.map((item) => item.id === spinId
|
|
719
|
-
? { kind: "error", message: `Compaction failed: ${msg}`, id: spinId }
|
|
720
|
-
: item));
|
|
775
|
+
setLiveItems((prev) => prev.map((item) => item.id === spinId ? toErrorItem(err, spinId, "Compaction failed") : item));
|
|
721
776
|
return messages; // Return unchanged on failure
|
|
722
777
|
}
|
|
723
|
-
}, [
|
|
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]);
|
|
724
838
|
/**
|
|
725
839
|
* transformContext callback for the agent loop.
|
|
726
840
|
* Called before each LLM call and on context overflow.
|
|
727
|
-
*
|
|
728
|
-
*
|
|
729
|
-
* Uses actual API-reported token counts (from previous turn_end) when
|
|
730
|
-
* available, falling back to the character-based estimate. A 30-second
|
|
731
|
-
* cooldown prevents repeated compaction — matching the pattern used by
|
|
732
|
-
* Mysti, openclaw, and other real-world agent frameworks.
|
|
841
|
+
* Compacts persistent chat only, then injects the dynamic repo map transiently.
|
|
733
842
|
*/
|
|
734
843
|
const transformContext = useCallback(async (messages, options) => {
|
|
844
|
+
const stripped = stripRepoMapMessages(messages);
|
|
735
845
|
const settings = settingsRef.current;
|
|
736
846
|
const autoCompact = settings?.get("autoCompact") ?? true;
|
|
737
847
|
const threshold = settings?.get("compactThreshold") ?? 0.8;
|
|
738
848
|
// Force-compact on context overflow regardless of settings
|
|
739
849
|
if (options?.force) {
|
|
740
|
-
const result = await compactConversation(
|
|
850
|
+
const result = await compactConversation(stripped);
|
|
851
|
+
if (result !== stripped) {
|
|
852
|
+
messagesRef.current = result;
|
|
853
|
+
await persistCompactedSession(result);
|
|
854
|
+
}
|
|
741
855
|
lastCompactionTimeRef.current = Date.now();
|
|
742
|
-
return result;
|
|
856
|
+
return injectRepoMapContext(result);
|
|
743
857
|
}
|
|
744
858
|
if (!autoCompact)
|
|
745
|
-
return
|
|
859
|
+
return injectRepoMapContext(stripped);
|
|
746
860
|
// Time-based cooldown: skip if compaction ran within the last 30 seconds
|
|
747
861
|
if (Date.now() - lastCompactionTimeRef.current < 30_000) {
|
|
748
862
|
log("INFO", "compaction", `Skipping compaction — cooldown active`);
|
|
749
|
-
return
|
|
863
|
+
return injectRepoMapContext(stripped);
|
|
750
864
|
}
|
|
751
|
-
const contextWindow = getContextWindow(currentModel);
|
|
752
|
-
|
|
753
|
-
// schemas. Otherwise the API rejects requests where the prompt fits the
|
|
754
|
-
// window but leaves no room for the response (e.g. Codex Mini at 200K
|
|
755
|
-
// ctx / 100K out — pre-turn estimate may say 160K but a 100K reasoning
|
|
756
|
-
// response then overflows).
|
|
757
|
-
const modelInfo = getModel(currentModel);
|
|
758
|
-
const reserveTokens = (modelInfo?.maxOutputTokens ?? 0) + 5_000;
|
|
759
|
-
// Prefer actual API-reported tokens over char-based estimate, but only
|
|
760
|
-
// when the token count was recorded AFTER the most recent compaction.
|
|
761
|
-
// A count from before compaction is stale — it reflects the old context
|
|
762
|
-
// size and would trigger compaction again immediately for no reason.
|
|
865
|
+
const contextWindow = getContextWindow(currentModel, contextWindowOptions);
|
|
866
|
+
const reserveTokens = getCompactionReserveTokens(props.maxTokens);
|
|
763
867
|
const tokensFresh = lastActualTokensTimestampRef.current > lastCompactionTimeRef.current;
|
|
764
868
|
const actualTokens = lastActualTokensRef.current > 0 && tokensFresh ? lastActualTokensRef.current : undefined;
|
|
765
|
-
if (shouldCompact(
|
|
766
|
-
const
|
|
767
|
-
|
|
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
|
+
}
|
|
768
875
|
lastCompactionTimeRef.current = Date.now();
|
|
769
|
-
|
|
770
|
-
// return the original reference so the agent loop doesn't replace messages.
|
|
771
|
-
if (result.length === before)
|
|
772
|
-
return messages;
|
|
773
|
-
return result;
|
|
876
|
+
return injectRepoMapContext(result);
|
|
774
877
|
}
|
|
775
|
-
return
|
|
776
|
-
}, [
|
|
878
|
+
return injectRepoMapContext(stripped);
|
|
879
|
+
}, [
|
|
880
|
+
currentModel,
|
|
881
|
+
compactConversation,
|
|
882
|
+
contextWindowOptions,
|
|
883
|
+
injectRepoMapContext,
|
|
884
|
+
persistCompactedSession,
|
|
885
|
+
stripRepoMapMessages,
|
|
886
|
+
]);
|
|
777
887
|
// ── Background task bar state (external store) ──────────
|
|
778
888
|
const { bgTasks, focused: taskBarFocused, expanded: taskBarExpanded, selectedIndex: selectedTaskIndex, } = useTaskBarStore();
|
|
779
889
|
useTaskBarPolling(props.processManager);
|
|
@@ -801,7 +911,7 @@ export function App(props) {
|
|
|
801
911
|
tools: currentTools,
|
|
802
912
|
webSearch: props.webSearch,
|
|
803
913
|
maxTokens: props.maxTokens,
|
|
804
|
-
thinking: thinkingEnabled ? (
|
|
914
|
+
thinking: thinkingEnabled ? getMaxThinkingLevel(currentModel) : undefined,
|
|
805
915
|
apiKey: activeApiKey,
|
|
806
916
|
baseUrl: activeBaseUrl,
|
|
807
917
|
accountId: activeAccountId,
|
|
@@ -809,6 +919,7 @@ export function App(props) {
|
|
|
809
919
|
transformContext,
|
|
810
920
|
}, {
|
|
811
921
|
onComplete: useCallback(() => {
|
|
922
|
+
messagesRef.current = stripRepoMapMessages(messagesRef.current);
|
|
812
923
|
persistNewMessages();
|
|
813
924
|
// Auto-clear plan progress and approved plan when all steps are completed
|
|
814
925
|
const steps = planStepsRef.current;
|
|
@@ -817,12 +928,7 @@ export function App(props) {
|
|
|
817
928
|
setPlanSteps([]);
|
|
818
929
|
approvedPlanPathRef.current = undefined;
|
|
819
930
|
// Rebuild system prompt to remove the completed plan from context
|
|
820
|
-
void (
|
|
821
|
-
const newPrompt = await buildSystemPrompt(props.cwd, props.skills, planMode, undefined, undefined, injectedLanguagesRef.current);
|
|
822
|
-
if (messagesRef.current[0]?.role === "system") {
|
|
823
|
-
messagesRef.current[0] = { role: "system", content: newPrompt };
|
|
824
|
-
}
|
|
825
|
-
})();
|
|
931
|
+
void replaceSystemPrompt({ clearApprovedPlan: true });
|
|
826
932
|
}
|
|
827
933
|
// Generate session title after the first turn (background, best-effort)
|
|
828
934
|
if (!sessionTitleGeneratedRef.current) {
|
|
@@ -866,6 +972,7 @@ export function App(props) {
|
|
|
866
972
|
}
|
|
867
973
|
}, [
|
|
868
974
|
persistNewMessages,
|
|
975
|
+
stripRepoMapMessages,
|
|
869
976
|
planMode,
|
|
870
977
|
props.cwd,
|
|
871
978
|
props.skills,
|
|
@@ -885,6 +992,9 @@ export function App(props) {
|
|
|
885
992
|
planStepsRef.current = updated;
|
|
886
993
|
setPlanSteps(updated);
|
|
887
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 };
|
|
888
998
|
}
|
|
889
999
|
}
|
|
890
1000
|
// Flush all completed items from the previous turn to Static history.
|
|
@@ -952,6 +1062,8 @@ export function App(props) {
|
|
|
952
1062
|
}, []),
|
|
953
1063
|
onToolStart: useCallback((toolCallId, name, args) => {
|
|
954
1064
|
log("INFO", "tool", `Tool call started: ${name}`, { id: toolCallId });
|
|
1065
|
+
const startedAt = Date.now();
|
|
1066
|
+
const animateUntil = startedAt + RUNNING_INDICATOR_ANIMATION_MS;
|
|
955
1067
|
// Flush completed items (assistant text, finished tools) to Static
|
|
956
1068
|
// before adding tool UI. Keeping both in the live area makes it tall
|
|
957
1069
|
// and causes Ink's cursor math to clip the top.
|
|
@@ -997,7 +1109,10 @@ export function App(props) {
|
|
|
997
1109
|
const next = [...prev];
|
|
998
1110
|
next[groupIdx] = {
|
|
999
1111
|
...group,
|
|
1000
|
-
tools: [
|
|
1112
|
+
tools: [
|
|
1113
|
+
...group.tools,
|
|
1114
|
+
{ toolCallId, name, args, status: "running", animateUntil },
|
|
1115
|
+
],
|
|
1001
1116
|
};
|
|
1002
1117
|
return next;
|
|
1003
1118
|
}
|
|
@@ -1005,7 +1120,7 @@ export function App(props) {
|
|
|
1005
1120
|
...prev,
|
|
1006
1121
|
{
|
|
1007
1122
|
kind: "tool_group",
|
|
1008
|
-
tools: [{ toolCallId, name, args, status: "running" }],
|
|
1123
|
+
tools: [{ toolCallId, name, args, status: "running", animateUntil }],
|
|
1009
1124
|
id: getId(),
|
|
1010
1125
|
},
|
|
1011
1126
|
];
|
|
@@ -1014,7 +1129,7 @@ export function App(props) {
|
|
|
1014
1129
|
else {
|
|
1015
1130
|
setLiveItems((prev) => [
|
|
1016
1131
|
...prev,
|
|
1017
|
-
{ kind: "tool_start", toolCallId, name, args, id: getId() },
|
|
1132
|
+
{ kind: "tool_start", toolCallId, name, args, id: getId(), startedAt, animateUntil },
|
|
1018
1133
|
]);
|
|
1019
1134
|
}
|
|
1020
1135
|
}, []),
|
|
@@ -1167,6 +1282,8 @@ export function App(props) {
|
|
|
1167
1282
|
}, []),
|
|
1168
1283
|
onServerToolCall: useCallback((id, name, input) => {
|
|
1169
1284
|
log("INFO", "server_tool", `Server tool call: ${name}`, { id });
|
|
1285
|
+
const startedAt = Date.now();
|
|
1286
|
+
const animateUntil = startedAt + RUNNING_INDICATOR_ANIMATION_MS;
|
|
1170
1287
|
// Flush completed items (including assistant text) to Static before
|
|
1171
1288
|
// adding server tool UI — same rationale as onToolStart.
|
|
1172
1289
|
setLiveItems((prev) => {
|
|
@@ -1181,7 +1298,8 @@ export function App(props) {
|
|
|
1181
1298
|
serverToolCallId: id,
|
|
1182
1299
|
name,
|
|
1183
1300
|
input,
|
|
1184
|
-
startedAt
|
|
1301
|
+
startedAt,
|
|
1302
|
+
animateUntil,
|
|
1185
1303
|
id: getId(),
|
|
1186
1304
|
},
|
|
1187
1305
|
];
|
|
@@ -1331,12 +1449,6 @@ export function App(props) {
|
|
|
1331
1449
|
log("WARN", "agent", "Agent run aborted by user");
|
|
1332
1450
|
setRunAllTasks(false);
|
|
1333
1451
|
setRunAllPixel(false);
|
|
1334
|
-
// Mirror immediately — the useEffect that syncs sessionStore
|
|
1335
|
-
// won't run if we unmount before the next render commit.
|
|
1336
|
-
if (props.sessionStore) {
|
|
1337
|
-
props.sessionStore.runAllTasks = false;
|
|
1338
|
-
props.sessionStore.runAllPixel = false;
|
|
1339
|
-
}
|
|
1340
1452
|
currentPixelFixRef.current = null;
|
|
1341
1453
|
setDoneStatus(null);
|
|
1342
1454
|
setLiveItems((prev) => {
|
|
@@ -1378,7 +1490,7 @@ export function App(props) {
|
|
|
1378
1490
|
}
|
|
1379
1491
|
return item;
|
|
1380
1492
|
});
|
|
1381
|
-
return [...next, { kind: "
|
|
1493
|
+
return [...next, { kind: "stopped", text: "Request was stopped.", id: getId() }];
|
|
1382
1494
|
});
|
|
1383
1495
|
}, []),
|
|
1384
1496
|
onQueuedStart: useCallback((content) => {
|
|
@@ -1408,6 +1520,36 @@ export function App(props) {
|
|
|
1408
1520
|
setDoneStatus(null);
|
|
1409
1521
|
setLiveItems([userItem]);
|
|
1410
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
|
+
}, []),
|
|
1411
1553
|
});
|
|
1412
1554
|
// First-time-per-project auto-run of /setup. Bound after `agentLoop` is in
|
|
1413
1555
|
// scope so the ref closure can dispatch to it. Called from the initial
|
|
@@ -1442,8 +1584,8 @@ export function App(props) {
|
|
|
1442
1584
|
setLiveItems((prev) => [
|
|
1443
1585
|
...prev,
|
|
1444
1586
|
isAbort
|
|
1445
|
-
? { kind: "
|
|
1446
|
-
:
|
|
1587
|
+
? { kind: "stopped", text: "Auto-setup cancelled.", id: getId() }
|
|
1588
|
+
: toErrorItem(err, getId()),
|
|
1447
1589
|
]);
|
|
1448
1590
|
}
|
|
1449
1591
|
};
|
|
@@ -1465,6 +1607,29 @@ export function App(props) {
|
|
|
1465
1607
|
useEffect(() => {
|
|
1466
1608
|
setTitleRunning(agentLoop.isRunning);
|
|
1467
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]);
|
|
1468
1633
|
// Consume sessionStore.pendingAction once on mount. Set by resetUI options
|
|
1469
1634
|
// for paths that remount AND immediately drive the agent (plan accept,
|
|
1470
1635
|
// plan reject, startTask, pixel fix). The action survives the unmount
|
|
@@ -1478,7 +1643,14 @@ export function App(props) {
|
|
|
1478
1643
|
pendingActionConsumedRef.current = true;
|
|
1479
1644
|
if (sessionStore)
|
|
1480
1645
|
sessionStore.pendingAction = undefined;
|
|
1481
|
-
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) {
|
|
1482
1654
|
setLiveItems((prev) => [
|
|
1483
1655
|
...prev,
|
|
1484
1656
|
{ kind: "info", text: action.infoText, id: getId() },
|
|
@@ -1488,7 +1660,7 @@ export function App(props) {
|
|
|
1488
1660
|
void agentLoop.run(action.prompt).catch((err) => {
|
|
1489
1661
|
const errMsg = err instanceof Error ? err.message : String(err);
|
|
1490
1662
|
log("ERROR", "error", errMsg);
|
|
1491
|
-
setLiveItems((prev) => [...prev,
|
|
1663
|
+
setLiveItems((prev) => [...prev, toErrorItem(err, getId())]);
|
|
1492
1664
|
});
|
|
1493
1665
|
// Intentional one-shot: run once on mount, never re-fire on re-render.
|
|
1494
1666
|
}, []);
|
|
@@ -1500,12 +1672,6 @@ export function App(props) {
|
|
|
1500
1672
|
setEyesCount(isEyesActive(props.cwd) ? journalCount({ status: "open" }, props.cwd) : undefined);
|
|
1501
1673
|
}
|
|
1502
1674
|
}, [agentLoop.isRunning, props.cwd]);
|
|
1503
|
-
// Animated thinking border — derived from global animation tick
|
|
1504
|
-
useAnimationActive();
|
|
1505
|
-
const animTick = useAnimationTick();
|
|
1506
|
-
const thinkingBorderFrame = agentLoop.activityPhase === "thinking"
|
|
1507
|
-
? deriveFrame(animTick, 1000, THINKING_BORDER_COLORS.length)
|
|
1508
|
-
: 0;
|
|
1509
1675
|
const handleSubmit = useCallback(async (input, inputImages = [], pasteInfo) => {
|
|
1510
1676
|
const trimmed = input.trim();
|
|
1511
1677
|
if (trimmed.startsWith("/")) {
|
|
@@ -1518,7 +1684,7 @@ export function App(props) {
|
|
|
1518
1684
|
// external changes between turns and ensures non-writing prompts still
|
|
1519
1685
|
// surface the badge when packs are newly applicable. No-op if the set
|
|
1520
1686
|
// has not grown.
|
|
1521
|
-
|
|
1687
|
+
await applyLanguageDetectionRef.current("input");
|
|
1522
1688
|
}
|
|
1523
1689
|
// Handle /model directly — open inline selector
|
|
1524
1690
|
if (trimmed === "/model" || trimmed === "/m") {
|
|
@@ -1530,7 +1696,7 @@ export function App(props) {
|
|
|
1530
1696
|
const compacted = await compactConversation(messagesRef.current);
|
|
1531
1697
|
if (compacted !== messagesRef.current) {
|
|
1532
1698
|
messagesRef.current = compacted;
|
|
1533
|
-
|
|
1699
|
+
await persistCompactedSession(compacted);
|
|
1534
1700
|
}
|
|
1535
1701
|
return;
|
|
1536
1702
|
}
|
|
@@ -1550,7 +1716,7 @@ export function App(props) {
|
|
|
1550
1716
|
if (trimmed === "/clear") {
|
|
1551
1717
|
if (props.resetUI) {
|
|
1552
1718
|
void (async () => {
|
|
1553
|
-
const newPrompt = await
|
|
1719
|
+
const newPrompt = await rebuildSystemPrompt({ clearApprovedPlan: true });
|
|
1554
1720
|
props.resetUI?.({
|
|
1555
1721
|
wipeSession: true,
|
|
1556
1722
|
messages: [{ role: "system", content: newPrompt }],
|
|
@@ -1569,7 +1735,7 @@ export function App(props) {
|
|
|
1569
1735
|
planStepsRef.current = [];
|
|
1570
1736
|
setPlanSteps([]);
|
|
1571
1737
|
void (async () => {
|
|
1572
|
-
const newPrompt = await
|
|
1738
|
+
const newPrompt = await rebuildSystemPrompt({ clearApprovedPlan: true });
|
|
1573
1739
|
messagesRef.current = [{ role: "system", content: newPrompt }];
|
|
1574
1740
|
persistedIndexRef.current = messagesRef.current.length;
|
|
1575
1741
|
})();
|
|
@@ -1631,29 +1797,46 @@ export function App(props) {
|
|
|
1631
1797
|
planStepsRef.current = [];
|
|
1632
1798
|
setPlanSteps([]);
|
|
1633
1799
|
// Rebuild system prompt without the plan
|
|
1634
|
-
void (
|
|
1635
|
-
|
|
1636
|
-
if (messagesRef.current[0]?.role === "system") {
|
|
1637
|
-
messagesRef.current[0] = { role: "system", content: newPrompt };
|
|
1638
|
-
}
|
|
1639
|
-
})();
|
|
1640
|
-
setLiveItems([{ kind: "info", text: "Approved plan dismissed.", id: getId() }]);
|
|
1800
|
+
void replaceSystemPrompt({ clearApprovedPlan: true });
|
|
1801
|
+
setLiveItems([{ kind: "plan_event", event: "dismissed", id: getId() }]);
|
|
1641
1802
|
return;
|
|
1642
1803
|
}
|
|
1643
|
-
// Handle /
|
|
1644
|
-
if (trimmed === "/
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
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;
|
|
1649
1818
|
}
|
|
1650
|
-
|
|
1651
|
-
|
|
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,
|
|
1652
1837
|
{
|
|
1653
1838
|
kind: "info",
|
|
1654
|
-
text:
|
|
1655
|
-
? "Buddy enabled! Your companion will appear near the prompt."
|
|
1656
|
-
: "Buddy disabled.",
|
|
1839
|
+
text: formatRepoMapCommandOutput(repoMapInjectionEnabledRef.current, markdown, action === "refresh"),
|
|
1657
1840
|
id: getId(),
|
|
1658
1841
|
},
|
|
1659
1842
|
]);
|
|
@@ -1661,13 +1844,18 @@ export function App(props) {
|
|
|
1661
1844
|
}
|
|
1662
1845
|
// Handle /plans — open plan pane
|
|
1663
1846
|
if (trimmed === "/plans") {
|
|
1664
|
-
if (props.resetUI && props.sessionStore) {
|
|
1847
|
+
if (props.resetUI && props.sessionStore && !agentLoop.isRunning) {
|
|
1665
1848
|
props.sessionStore.overlay = "plan";
|
|
1666
1849
|
props.sessionStore.planAutoExpand = false;
|
|
1667
1850
|
props.resetUI();
|
|
1668
1851
|
}
|
|
1669
1852
|
else {
|
|
1670
|
-
|
|
1853
|
+
if (props.sessionStore) {
|
|
1854
|
+
props.sessionStore.overlay = "plan";
|
|
1855
|
+
props.sessionStore.planAutoExpand = false;
|
|
1856
|
+
if (agentLoop.isRunning)
|
|
1857
|
+
props.sessionStore.pendingResetUI = true;
|
|
1858
|
+
}
|
|
1671
1859
|
setPlanAutoExpand(false);
|
|
1672
1860
|
setOverlay("plan");
|
|
1673
1861
|
}
|
|
@@ -1709,8 +1897,8 @@ export function App(props) {
|
|
|
1709
1897
|
setLiveItems((prev) => [
|
|
1710
1898
|
...prev,
|
|
1711
1899
|
isAbort
|
|
1712
|
-
? { kind: "
|
|
1713
|
-
:
|
|
1900
|
+
? { kind: "stopped", text: "Request was stopped.", id: getId() }
|
|
1901
|
+
: toErrorItem(err, getId()),
|
|
1714
1902
|
]);
|
|
1715
1903
|
}
|
|
1716
1904
|
// Reload custom commands in case a setup command created new ones
|
|
@@ -1833,11 +2021,19 @@ export function App(props) {
|
|
|
1833
2021
|
setLiveItems((prev) => [
|
|
1834
2022
|
...prev,
|
|
1835
2023
|
isAbort
|
|
1836
|
-
? { kind: "
|
|
1837
|
-
:
|
|
2024
|
+
? { kind: "stopped", text: "Request was stopped.", id: getId() }
|
|
2025
|
+
: toErrorItem(err, getId()),
|
|
1838
2026
|
]);
|
|
1839
2027
|
}
|
|
1840
|
-
}, [
|
|
2028
|
+
}, [
|
|
2029
|
+
agentLoop,
|
|
2030
|
+
props.onSlashCommand,
|
|
2031
|
+
compactConversation,
|
|
2032
|
+
rebuildSystemPrompt,
|
|
2033
|
+
replaceSystemPrompt,
|
|
2034
|
+
refreshRepoMap,
|
|
2035
|
+
stripRepoMapMessages,
|
|
2036
|
+
]);
|
|
1841
2037
|
const handleDoubleExit = useDoublePress(setExitPending, () => process.exit(0));
|
|
1842
2038
|
const handleAbort = useCallback(() => {
|
|
1843
2039
|
if (agentLoop.isRunning) {
|
|
@@ -1854,7 +2050,7 @@ export function App(props) {
|
|
|
1854
2050
|
log("INFO", "thinking", `Thinking ${next ? "enabled" : "disabled"}`);
|
|
1855
2051
|
setLiveItems((items) => [
|
|
1856
2052
|
...items,
|
|
1857
|
-
{ kind: "
|
|
2053
|
+
{ kind: "thinking_transition", active: next, id: getId() },
|
|
1858
2054
|
]);
|
|
1859
2055
|
if (props.settingsFile) {
|
|
1860
2056
|
const sm = new SettingsManager(props.settingsFile);
|
|
@@ -1871,6 +2067,10 @@ export function App(props) {
|
|
|
1871
2067
|
const newProvider = value.slice(0, colonIdx);
|
|
1872
2068
|
const newModelId = value.slice(colonIdx + 1);
|
|
1873
2069
|
log("INFO", "model", `Model changed`, { provider: newProvider, model: newModelId });
|
|
2070
|
+
const rebuildPromptWithTools = (tools) => {
|
|
2071
|
+
currentToolsRef.current = tools;
|
|
2072
|
+
void replaceSystemPrompt({ tools });
|
|
2073
|
+
};
|
|
1874
2074
|
// Handle provider-specific tool changes when provider changes
|
|
1875
2075
|
setCurrentProvider((prevProvider) => {
|
|
1876
2076
|
if (newProvider !== prevProvider) {
|
|
@@ -1878,15 +2078,17 @@ export function App(props) {
|
|
|
1878
2078
|
// Anthropic has native server-side web search; all other providers need the client tool.
|
|
1879
2079
|
setCurrentTools((prev) => {
|
|
1880
2080
|
const hasWebSearch = prev.some((t) => t.name === "web_search");
|
|
2081
|
+
let next = prev;
|
|
1881
2082
|
if (newProvider === "anthropic" && hasWebSearch) {
|
|
1882
2083
|
// Switching TO anthropic — remove client-side web_search (server-side handles it)
|
|
1883
|
-
|
|
2084
|
+
next = prev.filter((t) => t.name !== "web_search");
|
|
1884
2085
|
}
|
|
1885
2086
|
else if (newProvider !== "anthropic" && !hasWebSearch) {
|
|
1886
2087
|
// Switching FROM anthropic — add client-side web_search
|
|
1887
|
-
|
|
2088
|
+
next = [...prev, createWebSearchTool()];
|
|
1888
2089
|
}
|
|
1889
|
-
|
|
2090
|
+
rebuildPromptWithTools(next);
|
|
2091
|
+
return next;
|
|
1890
2092
|
});
|
|
1891
2093
|
// Reconnect MCP servers
|
|
1892
2094
|
if (props.mcpManager) {
|
|
@@ -1909,16 +2111,21 @@ export function App(props) {
|
|
|
1909
2111
|
}
|
|
1910
2112
|
try {
|
|
1911
2113
|
const mcpTools = await props.mcpManager.connectAll(getMCPServers(newProvider, apiKey));
|
|
1912
|
-
setCurrentTools((prev) =>
|
|
1913
|
-
...prev.filter((t) => !t.name.startsWith("mcp__")),
|
|
1914
|
-
|
|
1915
|
-
|
|
2114
|
+
setCurrentTools((prev) => {
|
|
2115
|
+
const next = [...prev.filter((t) => !t.name.startsWith("mcp__")), ...mcpTools];
|
|
2116
|
+
rebuildPromptWithTools(next);
|
|
2117
|
+
return next;
|
|
2118
|
+
});
|
|
1916
2119
|
log("INFO", "mcp", `MCP servers reconnected for provider ${newProvider}`);
|
|
1917
2120
|
}
|
|
1918
2121
|
catch (err) {
|
|
1919
2122
|
log("WARN", "mcp", `MCP reconnection failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
1920
2123
|
// Still remove old MCP tools even if reconnection fails
|
|
1921
|
-
setCurrentTools((prev) =>
|
|
2124
|
+
setCurrentTools((prev) => {
|
|
2125
|
+
const next = prev.filter((t) => !t.name.startsWith("mcp__"));
|
|
2126
|
+
rebuildPromptWithTools(next);
|
|
2127
|
+
return next;
|
|
2128
|
+
});
|
|
1922
2129
|
}
|
|
1923
2130
|
})();
|
|
1924
2131
|
}
|
|
@@ -1930,7 +2137,7 @@ export function App(props) {
|
|
|
1930
2137
|
const displayName = modelInfo?.name ?? newModelId;
|
|
1931
2138
|
setLiveItems((prev) => [
|
|
1932
2139
|
...prev,
|
|
1933
|
-
{ kind: "
|
|
2140
|
+
{ kind: "model_transition", modelName: displayName, id: getId() },
|
|
1934
2141
|
]);
|
|
1935
2142
|
// Persist model selection for next CLI launch
|
|
1936
2143
|
if (props.settingsFile) {
|
|
@@ -1951,10 +2158,7 @@ export function App(props) {
|
|
|
1951
2158
|
const sm = new SettingsManager(props.settingsFile);
|
|
1952
2159
|
sm.load().then(() => sm.set("theme", name));
|
|
1953
2160
|
}
|
|
1954
|
-
setLiveItems((prev) => [
|
|
1955
|
-
...prev,
|
|
1956
|
-
{ kind: "info", text: `Theme switched to: ${name}`, id: getId() },
|
|
1957
|
-
]);
|
|
2161
|
+
setLiveItems((prev) => [...prev, { kind: "theme_transition", themeName: name, id: getId() }]);
|
|
1958
2162
|
}, [switchTheme, props.settingsFile]);
|
|
1959
2163
|
// All available slash commands for the command palette — ordered by how
|
|
1960
2164
|
// commonly they're used and grouped by purpose; /quit stays dead last.
|
|
@@ -1970,6 +2174,8 @@ export function App(props) {
|
|
|
1970
2174
|
"research",
|
|
1971
2175
|
"scan",
|
|
1972
2176
|
"verify",
|
|
2177
|
+
"expand",
|
|
2178
|
+
"bullet-proof",
|
|
1973
2179
|
"simplify",
|
|
1974
2180
|
"compare",
|
|
1975
2181
|
"batch",
|
|
@@ -2022,20 +2228,20 @@ export function App(props) {
|
|
|
2022
2228
|
case "setup_hint":
|
|
2023
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));
|
|
2024
2230
|
case "assistant":
|
|
2025
|
-
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));
|
|
2026
2232
|
case "tool_start":
|
|
2027
|
-
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));
|
|
2028
2234
|
case "tool_done":
|
|
2029
2235
|
return (_jsx(ToolExecution, { status: "done", name: item.name, args: item.args, result: item.result, isError: item.isError, details: item.details }, item.id));
|
|
2030
2236
|
case "tool_group":
|
|
2031
2237
|
return _jsx(ToolGroupExecution, { tools: item.tools }, item.id);
|
|
2032
2238
|
case "server_tool_start":
|
|
2033
|
-
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));
|
|
2034
2240
|
case "server_tool_done":
|
|
2035
2241
|
return (_jsx(ServerToolExecution, { status: "done", name: item.name, input: item.input, durationMs: item.durationMs, resultType: item.resultType }, item.id));
|
|
2036
2242
|
case "error": {
|
|
2037
|
-
const
|
|
2038
|
-
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));
|
|
2039
2245
|
}
|
|
2040
2246
|
case "info":
|
|
2041
2247
|
return (_jsx(Box, { marginTop: 1, flexShrink: 1, children: _jsx(Text, { color: theme.textDim, wrap: "wrap", children: item.text }) }, item.id));
|
|
@@ -2043,6 +2249,34 @@ export function App(props) {
|
|
|
2043
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));
|
|
2044
2250
|
case "plan_transition":
|
|
2045
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));
|
|
2046
2280
|
case "step_done":
|
|
2047
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));
|
|
2048
2282
|
case "queued":
|
|
@@ -2050,7 +2284,7 @@ export function App(props) {
|
|
|
2050
2284
|
? ` (+${item.imageCount} image${item.imageCount > 1 ? "s" : ""})`
|
|
2051
2285
|
: ""] })] }, item.id));
|
|
2052
2286
|
case "compacting":
|
|
2053
|
-
return _jsx(CompactionSpinner, {}, item.id);
|
|
2287
|
+
return _jsx(CompactionSpinner, { staticDisplay: true }, item.id);
|
|
2054
2288
|
case "compacted":
|
|
2055
2289
|
return (_jsx(CompactionDone, { originalCount: item.originalCount, newCount: item.newCount, tokensBefore: item.tokensBefore, tokensAfter: item.tokensAfter }, item.id));
|
|
2056
2290
|
case "duration":
|
|
@@ -2126,8 +2360,8 @@ export function App(props) {
|
|
|
2126
2360
|
setLiveItems((prev) => [
|
|
2127
2361
|
...prev,
|
|
2128
2362
|
isAbort
|
|
2129
|
-
? { kind: "
|
|
2130
|
-
:
|
|
2363
|
+
? { kind: "stopped", text: "Request was stopped.", id: getId() }
|
|
2364
|
+
: toErrorItem(err, getId()),
|
|
2131
2365
|
]);
|
|
2132
2366
|
setRunAllTasks(false);
|
|
2133
2367
|
}
|
|
@@ -2145,8 +2379,6 @@ export function App(props) {
|
|
|
2145
2379
|
startTaskRef.current = startTask;
|
|
2146
2380
|
useEffect(() => {
|
|
2147
2381
|
runAllTasksRef.current = runAllTasks;
|
|
2148
|
-
// Mirror into sessionStore so the flag survives unmount/remount
|
|
2149
|
-
// when startTask triggers resetUI for the NEXT task in the chain.
|
|
2150
2382
|
if (props.sessionStore)
|
|
2151
2383
|
props.sessionStore.runAllTasks = runAllTasks;
|
|
2152
2384
|
}, [runAllTasks, props.sessionStore]);
|
|
@@ -2171,9 +2403,19 @@ export function App(props) {
|
|
|
2171
2403
|
log("WARN", "pixel", `chdir failed: ${err.message}`);
|
|
2172
2404
|
}
|
|
2173
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();
|
|
2174
2413
|
setDisplayedCwd(prep.projectPath);
|
|
2414
|
+
let toolsForPixelFix = currentToolsRef.current;
|
|
2175
2415
|
if (props.rebuildToolsForCwd) {
|
|
2176
|
-
|
|
2416
|
+
toolsForPixelFix = props.rebuildToolsForCwd(prep.projectPath);
|
|
2417
|
+
currentToolsRef.current = toolsForPixelFix;
|
|
2418
|
+
setCurrentTools(toolsForPixelFix);
|
|
2177
2419
|
}
|
|
2178
2420
|
// Pixel-fix swaps the project root — reset injected packs so the
|
|
2179
2421
|
// new project re-detects from scratch on the next tool call. Also
|
|
@@ -2183,7 +2425,13 @@ export function App(props) {
|
|
|
2183
2425
|
setupHintShownRef.current = false;
|
|
2184
2426
|
const detectedForPixelFix = detectLanguages(prep.projectPath);
|
|
2185
2427
|
injectedLanguagesRef.current = detectedForPixelFix;
|
|
2186
|
-
const newSystemPrompt = await
|
|
2428
|
+
const newSystemPrompt = await rebuildSystemPrompt({
|
|
2429
|
+
cwd: prep.projectPath,
|
|
2430
|
+
planMode: false,
|
|
2431
|
+
clearApprovedPlan: true,
|
|
2432
|
+
activeLanguages: detectedForPixelFix,
|
|
2433
|
+
tools: toolsForPixelFix,
|
|
2434
|
+
});
|
|
2187
2435
|
// Now that the cwd swap is committed, reset chat. Doing this BEFORE
|
|
2188
2436
|
// the chdir would print a banner with the old cwd, then bumping
|
|
2189
2437
|
// staticKey would print a second banner with the new cwd — leaving
|
|
@@ -2220,12 +2468,14 @@ export function App(props) {
|
|
|
2220
2468
|
log("ERROR", "pixel", msg);
|
|
2221
2469
|
currentPixelFixRef.current = null;
|
|
2222
2470
|
setRunAllPixel(false);
|
|
2223
|
-
setLiveItems((prev) => [...prev,
|
|
2471
|
+
setLiveItems((prev) => [...prev, toErrorItem(err, getId())]);
|
|
2224
2472
|
}
|
|
2225
2473
|
})();
|
|
2226
2474
|
}, [props.cwd, stdout, agentLoop, currentProvider, currentModel]);
|
|
2227
2475
|
startPixelFixRef.current = startPixelFix;
|
|
2228
|
-
|
|
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);
|
|
2229
2479
|
useEffect(() => {
|
|
2230
2480
|
runAllPixelRef.current = runAllPixel;
|
|
2231
2481
|
if (props.sessionStore)
|
|
@@ -2237,15 +2487,18 @@ export function App(props) {
|
|
|
2237
2487
|
const isEyesView = overlay === "eyes";
|
|
2238
2488
|
const isPixelView = overlay === "pixel";
|
|
2239
2489
|
const isOverlayView = isTaskView || isSkillsView || isPlanView || isEyesView || isPixelView;
|
|
2240
|
-
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: () => {
|
|
2241
|
-
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) {
|
|
2242
2492
|
props.sessionStore.overlay = null;
|
|
2243
2493
|
props.resetUI();
|
|
2244
2494
|
}
|
|
2245
2495
|
else {
|
|
2246
|
-
|
|
2496
|
+
if (props.sessionStore) {
|
|
2497
|
+
props.sessionStore.overlay = null;
|
|
2498
|
+
if (agentLoop.isRunning)
|
|
2499
|
+
props.sessionStore.pendingResetUI = true;
|
|
2500
|
+
}
|
|
2247
2501
|
setTaskCount(getTaskCount(props.cwd));
|
|
2248
|
-
setStaticKey((k) => k + 1);
|
|
2249
2502
|
setOverlay(null);
|
|
2250
2503
|
}
|
|
2251
2504
|
}, onWorkOnTask: (title, prompt, taskId) => {
|
|
@@ -2260,13 +2513,16 @@ export function App(props) {
|
|
|
2260
2513
|
startTask(next.title, next.prompt, next.id);
|
|
2261
2514
|
}
|
|
2262
2515
|
} })) : isPixelView ? (_jsx(PixelOverlay, { version: props.version, agentRunning: agentLoop.isRunning, onClose: () => {
|
|
2263
|
-
if (props.resetUI && props.sessionStore) {
|
|
2516
|
+
if (props.resetUI && props.sessionStore && !agentLoop.isRunning) {
|
|
2264
2517
|
props.sessionStore.overlay = null;
|
|
2265
2518
|
props.resetUI();
|
|
2266
2519
|
}
|
|
2267
2520
|
else {
|
|
2268
|
-
|
|
2269
|
-
|
|
2521
|
+
if (props.sessionStore) {
|
|
2522
|
+
props.sessionStore.overlay = null;
|
|
2523
|
+
if (agentLoop.isRunning)
|
|
2524
|
+
props.sessionStore.pendingResetUI = true;
|
|
2525
|
+
}
|
|
2270
2526
|
setOverlay(null);
|
|
2271
2527
|
}
|
|
2272
2528
|
}, onFixOne: (entry) => {
|
|
@@ -2280,38 +2536,48 @@ export function App(props) {
|
|
|
2280
2536
|
setRunAllPixel(true);
|
|
2281
2537
|
startPixelFix(first.errorId);
|
|
2282
2538
|
} })) : isSkillsView ? (_jsx(SkillsOverlay, { cwd: props.cwd, onClose: () => {
|
|
2283
|
-
if (props.resetUI && props.sessionStore) {
|
|
2539
|
+
if (props.resetUI && props.sessionStore && !agentLoop.isRunning) {
|
|
2284
2540
|
props.sessionStore.overlay = null;
|
|
2285
2541
|
props.resetUI();
|
|
2286
2542
|
}
|
|
2287
2543
|
else {
|
|
2288
|
-
|
|
2289
|
-
|
|
2544
|
+
if (props.sessionStore) {
|
|
2545
|
+
props.sessionStore.overlay = null;
|
|
2546
|
+
if (agentLoop.isRunning)
|
|
2547
|
+
props.sessionStore.pendingResetUI = true;
|
|
2548
|
+
}
|
|
2290
2549
|
setOverlay(null);
|
|
2291
2550
|
}
|
|
2292
2551
|
} })) : isEyesView ? (_jsx(EyesOverlay, { cwd: props.cwd, onClose: () => {
|
|
2293
|
-
if (props.resetUI && props.sessionStore) {
|
|
2552
|
+
if (props.resetUI && props.sessionStore && !agentLoop.isRunning) {
|
|
2294
2553
|
props.sessionStore.overlay = null;
|
|
2295
2554
|
props.resetUI();
|
|
2296
2555
|
}
|
|
2297
2556
|
else {
|
|
2298
|
-
|
|
2557
|
+
if (props.sessionStore) {
|
|
2558
|
+
props.sessionStore.overlay = null;
|
|
2559
|
+
if (agentLoop.isRunning)
|
|
2560
|
+
props.sessionStore.pendingResetUI = true;
|
|
2561
|
+
}
|
|
2299
2562
|
setEyesCount(isEyesActive(props.cwd) ? journalCount({ status: "open" }, props.cwd) : undefined);
|
|
2300
|
-
setStaticKey((k) => k + 1);
|
|
2301
2563
|
setOverlay(null);
|
|
2302
2564
|
}
|
|
2303
2565
|
}, onQueueMessage: (msg) => {
|
|
2304
2566
|
agentLoop.queueMessage(msg);
|
|
2305
2567
|
} })) : isPlanView ? (_jsx(PlanOverlay, { cwd: props.cwd, autoExpandNewest: planAutoExpand, onClose: () => {
|
|
2306
2568
|
planOverlayPendingRef.current = false;
|
|
2307
|
-
if (props.resetUI && props.sessionStore) {
|
|
2569
|
+
if (props.resetUI && props.sessionStore && !agentLoop.isRunning) {
|
|
2308
2570
|
props.sessionStore.overlay = null;
|
|
2309
2571
|
props.sessionStore.planAutoExpand = false;
|
|
2310
2572
|
props.resetUI();
|
|
2311
2573
|
}
|
|
2312
2574
|
else {
|
|
2313
|
-
|
|
2314
|
-
|
|
2575
|
+
if (props.sessionStore) {
|
|
2576
|
+
props.sessionStore.overlay = null;
|
|
2577
|
+
props.sessionStore.planAutoExpand = false;
|
|
2578
|
+
if (agentLoop.isRunning)
|
|
2579
|
+
props.sessionStore.pendingResetUI = true;
|
|
2580
|
+
}
|
|
2315
2581
|
setPlanAutoExpand(false);
|
|
2316
2582
|
setOverlay(null);
|
|
2317
2583
|
}
|
|
@@ -2327,7 +2593,10 @@ export function App(props) {
|
|
|
2327
2593
|
const planContent = await import("node:fs/promises").then(({ readFile }) => readFile(planPath, "utf-8"));
|
|
2328
2594
|
const steps = extractPlanSteps(planContent);
|
|
2329
2595
|
// Build the new system prompt with the approved plan baked in.
|
|
2330
|
-
const newPrompt = await
|
|
2596
|
+
const newPrompt = await rebuildSystemPrompt({
|
|
2597
|
+
planMode: false,
|
|
2598
|
+
approvedPlanPath: planPath,
|
|
2599
|
+
});
|
|
2331
2600
|
// Create a new session file BEFORE remount so the new tree
|
|
2332
2601
|
// picks it up via sessionStore.sessionPath.
|
|
2333
2602
|
let newSessionPath;
|
|
@@ -2349,7 +2618,7 @@ export function App(props) {
|
|
|
2349
2618
|
sessionPath: newSessionPath,
|
|
2350
2619
|
pendingAction: {
|
|
2351
2620
|
prompt: "The plan has been approved. Implement it now, following each step in order.",
|
|
2352
|
-
|
|
2621
|
+
planEvent: { event: "approved" },
|
|
2353
2622
|
},
|
|
2354
2623
|
});
|
|
2355
2624
|
return;
|
|
@@ -2382,7 +2651,7 @@ export function App(props) {
|
|
|
2382
2651
|
catch (err) {
|
|
2383
2652
|
const errMsg = err instanceof Error ? err.message : String(err);
|
|
2384
2653
|
log("ERROR", "error", errMsg);
|
|
2385
|
-
setLiveItems((prev) => [...prev,
|
|
2654
|
+
setLiveItems((prev) => [...prev, toErrorItem(err, getId())]);
|
|
2386
2655
|
}
|
|
2387
2656
|
})();
|
|
2388
2657
|
}, onReject: (planPath, feedback) => {
|
|
@@ -2397,7 +2666,7 @@ export function App(props) {
|
|
|
2397
2666
|
props.resetUI({
|
|
2398
2667
|
pendingAction: {
|
|
2399
2668
|
prompt: rejectionMsg,
|
|
2400
|
-
|
|
2669
|
+
planEvent: { event: "rejected", detail: feedback },
|
|
2401
2670
|
},
|
|
2402
2671
|
});
|
|
2403
2672
|
return;
|
|
@@ -2414,36 +2683,52 @@ export function App(props) {
|
|
|
2414
2683
|
void agentLoop.run(rejectionMsg).catch((err) => {
|
|
2415
2684
|
const errMsg = err instanceof Error ? err.message : String(err);
|
|
2416
2685
|
log("ERROR", "error", errMsg);
|
|
2417
|
-
setLiveItems((prev) => [...prev,
|
|
2686
|
+
setLiveItems((prev) => [...prev, toErrorItem(err, getId())]);
|
|
2418
2687
|
});
|
|
2419
|
-
} })) : (_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,
|
|
2420
|
-
? THINKING_BORDER_COLORS[thinkingBorderFrame]
|
|
2421
|
-
: "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 &&
|
|
2422
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: () => {
|
|
2423
|
-
|
|
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) {
|
|
2424
2697
|
props.sessionStore.overlay = "tasks";
|
|
2425
2698
|
props.resetUI();
|
|
2426
2699
|
}
|
|
2427
2700
|
else {
|
|
2428
|
-
|
|
2701
|
+
if (props.sessionStore) {
|
|
2702
|
+
props.sessionStore.overlay = "tasks";
|
|
2703
|
+
if (agentLoop.isRunning)
|
|
2704
|
+
props.sessionStore.pendingResetUI = true;
|
|
2705
|
+
}
|
|
2429
2706
|
setOverlay("tasks");
|
|
2430
2707
|
}
|
|
2431
2708
|
}, onToggleSkills: () => {
|
|
2432
|
-
if (props.resetUI && props.sessionStore) {
|
|
2709
|
+
if (props.resetUI && props.sessionStore && !agentLoop.isRunning) {
|
|
2433
2710
|
props.sessionStore.overlay = "skills";
|
|
2434
2711
|
props.resetUI();
|
|
2435
2712
|
}
|
|
2436
2713
|
else {
|
|
2437
|
-
|
|
2714
|
+
if (props.sessionStore) {
|
|
2715
|
+
props.sessionStore.overlay = "skills";
|
|
2716
|
+
if (agentLoop.isRunning)
|
|
2717
|
+
props.sessionStore.pendingResetUI = true;
|
|
2718
|
+
}
|
|
2438
2719
|
setOverlay("skills");
|
|
2439
2720
|
}
|
|
2440
2721
|
}, onTogglePixel: () => {
|
|
2441
|
-
if (props.resetUI && props.sessionStore) {
|
|
2722
|
+
if (props.resetUI && props.sessionStore && !agentLoop.isRunning) {
|
|
2442
2723
|
props.sessionStore.overlay = "pixel";
|
|
2443
2724
|
props.resetUI();
|
|
2444
2725
|
}
|
|
2445
2726
|
else {
|
|
2446
|
-
|
|
2727
|
+
if (props.sessionStore) {
|
|
2728
|
+
props.sessionStore.overlay = "pixel";
|
|
2729
|
+
if (agentLoop.isRunning)
|
|
2730
|
+
props.sessionStore.pendingResetUI = true;
|
|
2731
|
+
}
|
|
2447
2732
|
setOverlay("pixel");
|
|
2448
2733
|
}
|
|
2449
2734
|
}, onTogglePlanMode: () => {
|
|
@@ -2459,6 +2744,13 @@ export function App(props) {
|
|
|
2459
2744
|
id: getId(),
|
|
2460
2745
|
},
|
|
2461
2746
|
]);
|
|
2462
|
-
}, 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}`;
|
|
2463
2755
|
}
|
|
2464
2756
|
//# sourceMappingURL=App.js.map
|