swagmanager-mcp 5.0.0 → 6.3.0
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/bin/swag-agent.js +9 -0
- package/bin/swagmanager-mcp.js +10 -0
- package/dist/cli/chat/ChatApp.js +72 -2
- package/dist/cli/chat/ChatInput.js +1 -0
- package/dist/cli/chat/MessageList.d.ts +4 -0
- package/dist/cli/chat/MessageList.js +11 -5
- package/dist/cli/chat/ModelSelector.js +7 -3
- package/dist/cli/chat/RewindViewer.d.ts +26 -0
- package/dist/cli/chat/RewindViewer.js +185 -0
- package/dist/cli/chat/ToolIndicator.js +33 -4
- package/dist/cli/chat/hooks/useAgentLoop.d.ts +3 -0
- package/dist/cli/chat/hooks/useAgentLoop.js +42 -3
- package/dist/cli/chat/hooks/useSlashCommands.d.ts +2 -0
- package/dist/cli/chat/hooks/useSlashCommands.js +11 -2
- package/dist/cli/print-mode.js +8 -2
- package/dist/cli/serve-mode.d.ts +3 -1
- package/dist/cli/serve-mode.js +54 -9
- package/dist/cli/services/agent-events.d.ts +5 -1
- package/dist/cli/services/agent-events.js +2 -2
- package/dist/cli/services/agent-loop.d.ts +3 -1
- package/dist/cli/services/agent-loop.js +287 -23
- package/dist/cli/services/agent-worker-base.js +16 -3
- package/dist/cli/services/auth-service.js +3 -2
- package/dist/cli/services/config-store.d.ts +1 -0
- package/dist/cli/services/config-store.js +4 -2
- package/dist/cli/services/error-logger.d.ts +58 -0
- package/dist/cli/services/error-logger.js +269 -0
- package/dist/cli/services/format-server-response.js +203 -36
- package/dist/cli/services/format-server-response.test.js +62 -36
- package/dist/cli/services/hooks.d.ts +71 -42
- package/dist/cli/services/hooks.js +229 -176
- package/dist/cli/services/hooks.test.d.ts +1 -0
- package/dist/cli/services/hooks.test.js +344 -0
- package/dist/cli/services/local-tools-files.test.d.ts +1 -0
- package/dist/cli/services/local-tools-files.test.js +231 -0
- package/dist/cli/services/local-tools.js +97 -2
- package/dist/cli/services/loop-detector.d.ts +64 -0
- package/dist/cli/services/loop-detector.js +167 -0
- package/dist/cli/services/model-manager.d.ts +3 -1
- package/dist/cli/services/model-manager.js +8 -2
- package/dist/cli/services/model-router.d.ts +26 -0
- package/dist/cli/services/model-router.js +149 -0
- package/dist/cli/services/model-router.test.d.ts +1 -0
- package/dist/cli/services/model-router.test.js +206 -0
- package/dist/cli/services/rewind.d.ts +84 -0
- package/dist/cli/services/rewind.js +194 -0
- package/dist/cli/services/rewind.test.d.ts +4 -0
- package/dist/cli/services/rewind.test.js +292 -0
- package/dist/cli/services/server-tools.d.ts +5 -1
- package/dist/cli/services/server-tools.js +313 -8
- package/dist/cli/services/slash-commands.d.ts +50 -0
- package/dist/cli/services/slash-commands.js +284 -0
- package/dist/cli/services/subagent-runner.d.ts +11 -0
- package/dist/cli/services/subagent-spawn.d.ts +32 -0
- package/dist/cli/services/subagent.js +18 -1
- package/dist/cli/services/system-prompt.js +34 -1
- package/dist/cli/services/teammate.js +10 -1
- package/dist/cli/services/telemetry.d.ts +1 -0
- package/dist/cli/services/telemetry.js +27 -6
- package/dist/cli/services/tools/agent-tools.js +1 -1
- package/dist/cli/services/tools/file-ops.js +28 -5
- package/dist/cli/services/tools/shell-exec.js +2 -2
- package/dist/cli/services/tools/web-tools.js +7 -1
- package/dist/index.js +169 -10
- package/dist/local-agent/connection.d.ts +48 -0
- package/dist/local-agent/connection.js +332 -0
- package/dist/local-agent/discovery.d.ts +18 -0
- package/dist/local-agent/discovery.js +146 -0
- package/dist/local-agent/executor.d.ts +34 -0
- package/dist/local-agent/executor.js +241 -0
- package/dist/local-agent/index.d.ts +14 -0
- package/dist/local-agent/index.js +198 -0
- package/dist/server/auth.d.ts +3 -0
- package/dist/server/auth.js +36 -0
- package/dist/server/handlers/api-keys.d.ts +6 -0
- package/dist/server/handlers/api-keys.js +221 -0
- package/dist/server/handlers/browser.js +155 -3
- package/dist/server/handlers/catalog.d.ts +22 -22
- package/dist/server/handlers/catalog.js +160 -6
- package/dist/server/handlers/creations.d.ts +6 -0
- package/dist/server/handlers/creations.js +479 -0
- package/dist/server/handlers/enrichment.d.ts +8 -0
- package/dist/server/handlers/enrichment.js +768 -0
- package/dist/server/handlers/inventory.d.ts +65 -15
- package/dist/server/handlers/inventory.js +127 -71
- package/dist/server/handlers/kali.d.ts +10 -0
- package/dist/server/handlers/kali.js +210 -0
- package/dist/server/handlers/llm-providers.js +7 -4
- package/dist/server/handlers/local-agent.d.ts +6 -0
- package/dist/server/handlers/local-agent.js +118 -0
- package/dist/server/handlers/meta-ads.d.ts +111 -0
- package/dist/server/handlers/meta-ads.js +2279 -0
- package/dist/server/handlers/supply-chain.d.ts +0 -8
- package/dist/server/handlers/supply-chain.js +174 -212
- package/dist/server/handlers/transcription.d.ts +17 -0
- package/dist/server/handlers/transcription.js +121 -0
- package/dist/server/handlers/voice.d.ts +4 -2
- package/dist/server/handlers/voice.js +1064 -58
- package/dist/server/handlers/workflows.js +23 -0
- package/dist/server/index.js +197 -56
- package/dist/server/lib/compaction-service.d.ts +20 -0
- package/dist/server/lib/compaction-service.js +99 -0
- package/dist/server/lib/server-agent-loop.d.ts +6 -3
- package/dist/server/lib/server-agent-loop.js +129 -83
- package/dist/server/lib/server-subagent.js +19 -12
- package/dist/server/lib/utils.d.ts +3 -1
- package/dist/server/lib/utils.js +8 -2
- package/dist/server/local-agent-gateway.d.ts +82 -0
- package/dist/server/local-agent-gateway.js +426 -0
- package/dist/server/proxy-handlers.js +27 -7
- package/dist/server/routes.d.ts +34 -0
- package/dist/server/routes.js +963 -0
- package/dist/server/tool-router.d.ts +37 -7
- package/dist/server/tool-router.js +182 -90
- package/dist/server/validation.js +23 -1
- package/dist/services/tool-registry.d.ts +37 -0
- package/dist/services/tool-registry.js +45 -0
- package/dist/shared/agent-core.d.ts +24 -4
- package/dist/shared/agent-core.js +148 -29
- package/dist/shared/agent-core.test.js +74 -11
- package/dist/shared/anthropic-types.d.ts +3 -0
- package/dist/shared/api-client.d.ts +30 -0
- package/dist/shared/api-client.js +138 -3
- package/dist/shared/api-client.test.js +135 -1
- package/dist/shared/constants.d.ts +5 -3
- package/dist/shared/constants.js +13 -4
- package/dist/shared/sse-parser.js +16 -2
- package/dist/shared/sse-parser.test.js +84 -0
- package/dist/shared/tool-dispatch.d.ts +5 -1
- package/dist/shared/tool-dispatch.js +50 -11
- package/dist/shared/tool-dispatch.test.js +60 -0
- package/package.json +4 -3
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// SwagManager Local Agent — CLI entry point
|
|
3
|
+
// Usage: swag-agent start --key YOUR_API_KEY
|
|
4
|
+
|
|
5
|
+
import("../dist/local-agent/index.js").catch((err) => {
|
|
6
|
+
console.error("Failed to start swag-agent:", err.message);
|
|
7
|
+
console.error("Run 'npm run build' first if you see module not found errors.");
|
|
8
|
+
process.exit(1);
|
|
9
|
+
});
|
package/bin/swagmanager-mcp.js
CHANGED
|
@@ -112,6 +112,7 @@ function showHelp() {
|
|
|
112
112
|
console.log(` whale init${d} Generate .whale/CLAUDE.md for project${r}`);
|
|
113
113
|
console.log(` whale config${d} View/set configuration${r}`);
|
|
114
114
|
console.log(` whale serve${d} Local agent WebSocket server${r}`);
|
|
115
|
+
console.log(` whale agent${d} Start local security agent${r}`);
|
|
115
116
|
console.log();
|
|
116
117
|
console.log(` ${B}Print Mode (non-interactive):${r}`);
|
|
117
118
|
console.log(` whale -p "prompt"${d} Run single prompt, output to stdout${r}`);
|
|
@@ -304,6 +305,15 @@ switch (command) {
|
|
|
304
305
|
break;
|
|
305
306
|
}
|
|
306
307
|
|
|
308
|
+
case "agent": {
|
|
309
|
+
// Forward remaining args to local-agent CLI
|
|
310
|
+
// Rebuild process.argv so the agent sees: [node, script, subcommand, ...flags]
|
|
311
|
+
const agentArgs = process.argv.slice(2).filter(a => a !== "agent");
|
|
312
|
+
process.argv = [process.argv[0], process.argv[1], ...agentArgs];
|
|
313
|
+
await import(join(distDir, "local-agent", "index.js"));
|
|
314
|
+
break;
|
|
315
|
+
}
|
|
316
|
+
|
|
307
317
|
default:
|
|
308
318
|
console.error(`Unknown command: ${command}`);
|
|
309
319
|
console.error(`Run 'whale help' for usage.`);
|
package/dist/cli/chat/ChatApp.js
CHANGED
|
@@ -7,7 +7,7 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
|
|
|
7
7
|
* is managed by Ink's render loop. This prevents scroll bounce when
|
|
8
8
|
* content exceeds the terminal height.
|
|
9
9
|
*/
|
|
10
|
-
import { useState, useEffect, useRef, useMemo } from "react";
|
|
10
|
+
import { useState, useEffect, useRef, useMemo, useCallback } from "react";
|
|
11
11
|
import { Box, Text, Static, useApp, useInput } from "ink";
|
|
12
12
|
import Spinner from "ink-spinner";
|
|
13
13
|
import { canUseAgent, getServerToolCount, getModelShortName, setModel, getPermissionMode, mcpClientManager, } from "../services/agent-loop.js";
|
|
@@ -19,6 +19,8 @@ import { StreamingText } from "./StreamingText.js";
|
|
|
19
19
|
import { ChatInput } from "./ChatInput.js";
|
|
20
20
|
import { StoreSelector } from "./StoreSelector.js";
|
|
21
21
|
import { ModelSelector } from "./ModelSelector.js";
|
|
22
|
+
import { RewindViewer, RewindOutcome } from "./RewindViewer.js";
|
|
23
|
+
import { RewindManager } from "../services/rewind.js";
|
|
22
24
|
import { colors, symbols } from "../shared/Theme.js";
|
|
23
25
|
import { loadKeybindings, matchesBinding } from "../services/keybinding-manager.js";
|
|
24
26
|
import { loadConfig, updateConfig } from "../services/config-store.js";
|
|
@@ -78,6 +80,10 @@ export function ChatApp() {
|
|
|
78
80
|
return true;
|
|
79
81
|
}
|
|
80
82
|
});
|
|
83
|
+
// Rewind state
|
|
84
|
+
const [showRewind, setShowRewind] = useState(false);
|
|
85
|
+
const rewindManagerRef = useRef(new RewindManager());
|
|
86
|
+
const turnIndexRef = useRef(0);
|
|
81
87
|
// Refs
|
|
82
88
|
const conversationRef = useRef([]);
|
|
83
89
|
const abortRef = useRef(null);
|
|
@@ -150,14 +156,78 @@ export function ChatApp() {
|
|
|
150
156
|
setMessages, setStreamingText, setActiveTools, setTeamState,
|
|
151
157
|
setStoreList, setStoreSelectMode, setModelSelectMode, setCurrentModel,
|
|
152
158
|
setSessionId, setThinkingEnabled, setUserLabel, setServerToolsAvailable,
|
|
159
|
+
setShowRewind,
|
|
160
|
+
rewindCheckpointCount: rewindManagerRef.current.getCheckpointCount(),
|
|
153
161
|
PKG_NAME, PKG_VERSION,
|
|
154
162
|
});
|
|
155
163
|
const { handleSend } = useAgentLoop({
|
|
156
164
|
isStreaming, thinkingEnabled, conversationRef, abortRef,
|
|
157
165
|
accTextRef, textTimerRef, teamTimerRef, toolOutputTimerRef, thinkingVerbRef,
|
|
166
|
+
rewindManagerRef, turnIndexRef,
|
|
158
167
|
setMessages, setStreamingText, setIsStreaming, setActiveTools,
|
|
159
168
|
setSubagentActivity, setCompletedSubagents, setTeamState,
|
|
160
169
|
});
|
|
170
|
+
// ── Rewind handler ──
|
|
171
|
+
const handleRewind = useCallback((checkpointIndex, outcome) => {
|
|
172
|
+
const rm = rewindManagerRef.current;
|
|
173
|
+
if (outcome === RewindOutcome.Cancel) {
|
|
174
|
+
setShowRewind(false);
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
if (outcome === RewindOutcome.RevertOnly) {
|
|
178
|
+
// Revert files only, keep conversation
|
|
179
|
+
const result = rm.revertFilesFrom(checkpointIndex);
|
|
180
|
+
setShowRewind(false);
|
|
181
|
+
const total = result.filesReverted.length + result.filesDeleted.length;
|
|
182
|
+
const parts = [];
|
|
183
|
+
if (result.filesReverted.length > 0)
|
|
184
|
+
parts.push(`${result.filesReverted.length} reverted`);
|
|
185
|
+
if (result.filesDeleted.length > 0)
|
|
186
|
+
parts.push(`${result.filesDeleted.length} deleted`);
|
|
187
|
+
if (result.errors.length > 0)
|
|
188
|
+
parts.push(`${result.errors.length} errors`);
|
|
189
|
+
setMessages(prev => [...prev, {
|
|
190
|
+
role: "assistant",
|
|
191
|
+
text: total > 0
|
|
192
|
+
? ` ${symbols.check} File changes reverted (${parts.join(", ")})`
|
|
193
|
+
: ` ${symbols.check} No file changes to revert.`,
|
|
194
|
+
}]);
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
// RewindOnly or RewindAndRevert
|
|
198
|
+
const shouldRevertFiles = outcome === RewindOutcome.RewindAndRevert;
|
|
199
|
+
const result = rm.rewindTo(checkpointIndex);
|
|
200
|
+
// Truncate messages
|
|
201
|
+
setMessages(prev => prev.slice(0, result.messageCount));
|
|
202
|
+
// Truncate conversation history to match
|
|
203
|
+
// We need to figure out how many Anthropic messages correspond to the checkpoint.
|
|
204
|
+
// The checkpoint stores messageCount which is the UI messages count at that point.
|
|
205
|
+
// Rebuild conversation from remaining messages by keeping only the conversation
|
|
206
|
+
// entries up to the corresponding turn.
|
|
207
|
+
const targetTurnIndex = rm.getCheckpoints()[checkpointIndex]?.turnIndex ?? 0;
|
|
208
|
+
// Each user/assistant exchange is roughly 2 conversation entries (user + assistant).
|
|
209
|
+
// But the actual conversation is maintained independently.
|
|
210
|
+
// We slice it to 2*(turnIndex+1) entries (user+assistant pairs)
|
|
211
|
+
const conversationSlicePoint = Math.min((targetTurnIndex + 1) * 2, conversationRef.current.length);
|
|
212
|
+
conversationRef.current = conversationRef.current.slice(0, conversationSlicePoint);
|
|
213
|
+
// Reset turn index to match
|
|
214
|
+
turnIndexRef.current = targetTurnIndex + 1;
|
|
215
|
+
setShowRewind(false);
|
|
216
|
+
const parts = [];
|
|
217
|
+
if (shouldRevertFiles) {
|
|
218
|
+
if (result.filesReverted.length > 0)
|
|
219
|
+
parts.push(`${result.filesReverted.length} files reverted`);
|
|
220
|
+
if (result.filesDeleted.length > 0)
|
|
221
|
+
parts.push(`${result.filesDeleted.length} files deleted`);
|
|
222
|
+
}
|
|
223
|
+
if (result.errors.length > 0)
|
|
224
|
+
parts.push(`${result.errors.length} revert errors`);
|
|
225
|
+
const detail = parts.length > 0 ? ` (${parts.join(", ")})` : "";
|
|
226
|
+
setMessages(prev => [...prev, {
|
|
227
|
+
role: "assistant",
|
|
228
|
+
text: ` ${symbols.check} Rewound to turn ${targetTurnIndex + 1}${detail}`,
|
|
229
|
+
}]);
|
|
230
|
+
}, []);
|
|
161
231
|
// ── Render ──
|
|
162
232
|
const termWidth = process.stdout.columns || 80;
|
|
163
233
|
const contentWidth = Math.max(20, termWidth - 2);
|
|
@@ -181,7 +251,7 @@ export function ChatApp() {
|
|
|
181
251
|
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { children: " " }), _jsxs(Text, { children: [_jsx(Text, { color: colors.brand, bold: true, children: "\u25C6 whale code" }), userLabel ? _jsxs(Text, { color: colors.dim, children: [" ", userLabel] }) : null, _jsxs(Text, { color: colors.dim, children: [" ", currentModel] }), thinkingEnabled ? _jsx(Text, { color: colors.warning, children: " thinking" }) : null, getPermissionMode() !== "default" && (_jsxs(Text, { color: getPermissionMode() === "yolo" ? colors.error : colors.info, children: [" ", getPermissionMode()] })), serverToolsAvailable > 0 ? (_jsxs(Text, { color: colors.tertiary, children: [" ", symbols.dot, " ", serverToolsAvailable, " server tools"] })) : null] }), _jsx(Text, { color: colors.separator, children: "─".repeat(contentWidth) })] }, item.id));
|
|
182
252
|
}
|
|
183
253
|
return _jsx(CompletedMessage, { msg: item.msg, index: item.index, toolsExpanded: toolsExpanded }, item.id);
|
|
184
|
-
} }), dynamicMessages.map((msg) => (_jsx(CompletedMessage, { msg: msg, index: messages.length - 1, toolsExpanded: toolsExpanded }, `dynamic-${messages.length - 1}`))), teamState ? (_jsx(TeamPanel, { team: teamState })) : (_jsxs(_Fragment, { children: [isStreaming && !streamingText && activeTools.length === 0 && (_jsxs(Box, { marginLeft: 2, marginY: 1, children: [_jsx(Text, { color: colors.brand, children: _jsx(Spinner, { type: "dots" }) }), _jsxs(Text, { color: colors.dim, children: [" ", thinkingVerbRef.current] })] })), activeTools.length > 0 && (_jsx(Box, { flexDirection: "column", marginLeft: 2, children: activeTools.map((tc, i) => (_jsxs(Box, { flexDirection: "column", children: [_jsx(ToolIndicator, { id: `live-${tc.name}-${i}`, name: tc.name, status: tc.status, input: tc.input, expanded: toolsExpanded }), tc.name === "task" && tc.status === "running" && (subagentActivity.size > 0 || completedSubagents.length > 0) && (_jsx(SubagentPanel, { running: subagentActivity, completed: completedSubagents }))] }, `live-${tc.name}-${i}`))) })), streamingText && (_jsx(Box, { marginLeft: 2, children: _jsx(StreamingText, { text: streamingText }) }))] })), storeSelectMode ? (_jsx(StoreSelector, { stores: storeList, currentStoreId: loadConfig().store_id || "", onSelect: handleStoreSelect, onCancel: handleStoreCancel })) : modelSelectMode ? (_jsx(ModelSelector, { currentModel: currentModel, onSelect: (model) => {
|
|
254
|
+
} }), dynamicMessages.map((msg) => (_jsx(CompletedMessage, { msg: msg, index: messages.length - 1, toolsExpanded: toolsExpanded }, `dynamic-${messages.length - 1}`))), teamState ? (_jsx(TeamPanel, { team: teamState })) : (_jsxs(_Fragment, { children: [isStreaming && !streamingText && activeTools.length === 0 && (_jsxs(Box, { marginLeft: 2, marginY: 1, children: [_jsx(Text, { color: colors.brand, children: _jsx(Spinner, { type: "dots" }) }), _jsxs(Text, { color: colors.dim, children: [" ", thinkingVerbRef.current] })] })), activeTools.length > 0 && (_jsx(Box, { flexDirection: "column", marginLeft: 2, children: activeTools.map((tc, i) => (_jsxs(Box, { flexDirection: "column", children: [_jsx(ToolIndicator, { id: `live-${tc.name}-${i}`, name: tc.name, status: tc.status, input: tc.input, expanded: toolsExpanded }), tc.name === "task" && tc.status === "running" && (subagentActivity.size > 0 || completedSubagents.length > 0) && (_jsx(SubagentPanel, { running: subagentActivity, completed: completedSubagents }))] }, `live-${tc.name}-${i}`))) })), streamingText && (_jsx(Box, { marginLeft: 2, children: _jsx(StreamingText, { text: streamingText }) }))] })), showRewind ? (_jsx(RewindViewer, { checkpoints: rewindManagerRef.current.getCheckpoints(), onRewind: handleRewind, onCancel: () => setShowRewind(false) })) : storeSelectMode ? (_jsx(StoreSelector, { stores: storeList, currentStoreId: loadConfig().store_id || "", onSelect: handleStoreSelect, onCancel: handleStoreCancel })) : modelSelectMode ? (_jsx(ModelSelector, { currentModel: currentModel, onSelect: (model) => {
|
|
185
255
|
setModelSelectMode(false);
|
|
186
256
|
setModel(model.value);
|
|
187
257
|
setCurrentModel(model.value);
|
|
@@ -39,6 +39,7 @@ export const SLASH_COMMANDS = [
|
|
|
39
39
|
{ name: "/memory", description: "List all remembered facts" },
|
|
40
40
|
{ name: "/mode", description: "Permission mode (default/plan/yolo)" },
|
|
41
41
|
{ name: "/thinking", description: "Toggle extended thinking" },
|
|
42
|
+
{ name: "/rewind", description: "Rewind conversation to earlier point" },
|
|
42
43
|
{ name: "/init", description: "Generate project config (.whale/CLAUDE.md)" },
|
|
43
44
|
{ name: "/update", description: "Check for updates & install" },
|
|
44
45
|
{ name: "/clear", description: "Clear conversation" },
|
|
@@ -24,6 +24,10 @@ export interface ChatMessage {
|
|
|
24
24
|
input_tokens: number;
|
|
25
25
|
output_tokens: number;
|
|
26
26
|
thinking_tokens?: number;
|
|
27
|
+
model?: string;
|
|
28
|
+
costUsd?: number;
|
|
29
|
+
cache_read_tokens?: number;
|
|
30
|
+
cache_creation_tokens?: number;
|
|
27
31
|
};
|
|
28
32
|
}
|
|
29
33
|
export declare const CompletedMessage: React.NamedExoticComponent<{
|
|
@@ -17,10 +17,16 @@ import { MODEL_PRICING } from "../../shared/agent-core.js";
|
|
|
17
17
|
// ============================================================================
|
|
18
18
|
// HELPERS
|
|
19
19
|
// ============================================================================
|
|
20
|
-
function estimateCost(input, output) {
|
|
21
|
-
// Use
|
|
22
|
-
|
|
23
|
-
|
|
20
|
+
function estimateCost(input, output, model, precomputedCost) {
|
|
21
|
+
// Use precomputed cost if available (accurate for all providers)
|
|
22
|
+
let cost = precomputedCost;
|
|
23
|
+
if (cost == null) {
|
|
24
|
+
// Fall back to model-specific pricing, then Sonnet as default
|
|
25
|
+
const pricing = (model && MODEL_PRICING[model])
|
|
26
|
+
|| MODEL_PRICING[Object.keys(MODEL_PRICING).find(k => model?.startsWith(k)) ?? ""]
|
|
27
|
+
|| MODEL_PRICING["claude-sonnet-4-6"];
|
|
28
|
+
cost = (input * pricing.inputPer1M + output * pricing.outputPer1M) / 1_000_000;
|
|
29
|
+
}
|
|
24
30
|
if (cost < 0.001)
|
|
25
31
|
return "<$0.001";
|
|
26
32
|
if (cost < 0.01)
|
|
@@ -70,5 +76,5 @@ function groupConsecutiveTools(toolCalls) {
|
|
|
70
76
|
export const CompletedMessage = React.memo(function CompletedMessage({ msg, index, toolsExpanded }) {
|
|
71
77
|
const cw = Math.max(20, contentWidth());
|
|
72
78
|
const toolGroups = useMemo(() => msg.toolCalls ? groupConsecutiveTools(msg.toolCalls) : [], [msg.toolCalls]);
|
|
73
|
-
return (_jsxs(Box, { flexDirection: "column", children: [msg.role === "user" && index > 0 && (_jsxs(_Fragment, { children: [_jsx(Text, { children: " " }), _jsx(Text, { color: colors.separator, children: "─".repeat(cw) })] })), msg.role === "user" ? (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { children: " " }), msg.images && msg.images.length > 0 && (_jsx(Box, { marginLeft: 2, children: msg.images.map((name, i) => (_jsxs(Text, { children: [_jsx(Text, { color: colors.indigo, children: "[" }), _jsx(Text, { color: colors.secondary, children: name }), _jsx(Text, { color: colors.indigo, children: "]" }), _jsx(Text, { children: " " })] }, i))) })), _jsxs(Text, { children: [_jsx(Text, { color: colors.brand, bold: true, children: "❯ " }), _jsx(Text, { color: colors.user, children: msg.text })] })] })) : (_jsxs(Box, { flexDirection: "column", children: [toolGroups.length > 0 && (_jsx(Box, { flexDirection: "column", marginLeft: 2, marginTop: 1, children: toolGroups.map((group, j) => (_jsx(ToolIndicator, { id: `done-${index}-${j}`, name: group.tool.name, status: group.tool.status, result: group.tool.result, input: group.tool.input, durationMs: group.tool.durationMs, expanded: toolsExpanded, count: group.count }, j))) })), msg.completedSubagents && msg.completedSubagents.length > 0 && (_jsx(CompletedSubagentTree, { agents: msg.completedSubagents })), msg.text && (_jsxs(Box, { flexDirection: "column", marginLeft: 2, children: [_jsx(Text, { children: " " }), _jsx(MarkdownText, { text: msg.text })] })), msg.usage && (_jsxs(_Fragment, { children: [_jsx(Text, { children: " " }), _jsxs(Text, { children: [" ", _jsxs(Text, { color: colors.quaternary, children: [formatTokens(msg.usage.input_tokens), _jsx(Text, { color: colors.indigo, children: "\u2191" }), " ", formatTokens(msg.usage.output_tokens), _jsx(Text, { color: colors.purple, children: "\u2193" }), msg.usage.thinking_tokens ? (_jsxs(_Fragment, { children: [" ", formatTokens(msg.usage.thinking_tokens), _jsx(Text, { color: colors.warning, children: "T" })] })) : null] }), _jsxs(Text, { color: colors.quaternary, children: [" ", estimateCost(msg.usage.input_tokens, msg.usage.output_tokens)] }), msg.toolCalls && msg.toolCalls.length > 0 ? (_jsxs(Text, { color: colors.quaternary, children: [" ", msg.toolCalls.length, " tool", msg.toolCalls.length !== 1 ? "s" : "", " ", formatMs(totalToolDuration(msg.toolCalls))] })) : null] })] }))] }))] }));
|
|
79
|
+
return (_jsxs(Box, { flexDirection: "column", children: [msg.role === "user" && index > 0 && (_jsxs(_Fragment, { children: [_jsx(Text, { children: " " }), _jsx(Text, { color: colors.separator, children: "─".repeat(cw) })] })), msg.role === "user" ? (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { children: " " }), msg.images && msg.images.length > 0 && (_jsx(Box, { marginLeft: 2, children: msg.images.map((name, i) => (_jsxs(Text, { children: [_jsx(Text, { color: colors.indigo, children: "[" }), _jsx(Text, { color: colors.secondary, children: name }), _jsx(Text, { color: colors.indigo, children: "]" }), _jsx(Text, { children: " " })] }, i))) })), _jsxs(Text, { children: [_jsx(Text, { color: colors.brand, bold: true, children: "❯ " }), _jsx(Text, { color: colors.user, children: msg.text })] })] })) : (_jsxs(Box, { flexDirection: "column", children: [toolGroups.length > 0 && (_jsx(Box, { flexDirection: "column", marginLeft: 2, marginTop: 1, children: toolGroups.map((group, j) => (_jsx(ToolIndicator, { id: `done-${index}-${j}`, name: group.tool.name, status: group.tool.status, result: group.tool.result, input: group.tool.input, durationMs: group.tool.durationMs, expanded: toolsExpanded, count: group.count }, j))) })), msg.completedSubagents && msg.completedSubagents.length > 0 && (_jsx(CompletedSubagentTree, { agents: msg.completedSubagents })), msg.text && (_jsxs(Box, { flexDirection: "column", marginLeft: 2, children: [_jsx(Text, { children: " " }), _jsx(MarkdownText, { text: msg.text })] })), msg.usage && (_jsxs(_Fragment, { children: [_jsx(Text, { children: " " }), _jsxs(Text, { children: [" ", _jsxs(Text, { color: colors.quaternary, children: [formatTokens(msg.usage.input_tokens), _jsx(Text, { color: colors.indigo, children: "\u2191" }), " ", formatTokens(msg.usage.output_tokens), _jsx(Text, { color: colors.purple, children: "\u2193" }), msg.usage.thinking_tokens ? (_jsxs(_Fragment, { children: [" ", formatTokens(msg.usage.thinking_tokens), _jsx(Text, { color: colors.warning, children: "T" })] })) : null, msg.usage.cache_read_tokens ? (_jsxs(_Fragment, { children: [" ", formatTokens(msg.usage.cache_read_tokens), _jsx(Text, { color: colors.success, children: "C" })] })) : null] }), _jsxs(Text, { color: colors.quaternary, children: [" ", estimateCost(msg.usage.input_tokens, msg.usage.output_tokens, msg.usage.model, msg.usage.costUsd)] }), msg.toolCalls && msg.toolCalls.length > 0 ? (_jsxs(Text, { color: colors.quaternary, children: [" ", msg.toolCalls.length, " tool", msg.toolCalls.length !== 1 ? "s" : "", " ", formatMs(totalToolDuration(msg.toolCalls))] })) : null] })] }))] }))] }));
|
|
74
80
|
});
|
|
@@ -3,12 +3,16 @@ import { Box, Text, useInput } from "ink";
|
|
|
3
3
|
import SelectInput from "ink-select-input";
|
|
4
4
|
import { colors, symbols } from "../shared/Theme.js";
|
|
5
5
|
export const MODEL_OPTIONS = [
|
|
6
|
+
// Auto
|
|
7
|
+
{ label: "Auto (smart routing)", value: "auto", modelId: "auto", provider: "Auto" },
|
|
6
8
|
// Anthropic
|
|
7
9
|
{ label: "Opus 4.6", value: "opus", modelId: "claude-opus-4-6", provider: "Anthropic" },
|
|
8
|
-
{ label: "Sonnet 4", value: "sonnet", modelId: "claude-sonnet-4-
|
|
10
|
+
{ label: "Sonnet 4.6", value: "sonnet", modelId: "claude-sonnet-4-6", provider: "Anthropic" },
|
|
11
|
+
{ label: "Sonnet 4", value: "sonnet-4", modelId: "claude-sonnet-4-20250514", provider: "Anthropic" },
|
|
9
12
|
{ label: "Haiku 4.5", value: "haiku", modelId: "claude-haiku-4-5-20251001", provider: "Anthropic" },
|
|
10
13
|
// Bedrock
|
|
11
|
-
{ label: "Sonnet 4", value: "bedrock-sonnet", modelId: "
|
|
14
|
+
{ label: "Sonnet 4.6", value: "bedrock-sonnet", modelId: "anthropic.claude-sonnet-4-6", provider: "Bedrock" },
|
|
15
|
+
{ label: "Sonnet 4", value: "bedrock-sonnet-4", modelId: "us.anthropic.claude-sonnet-4-20250514-v1:0", provider: "Bedrock" },
|
|
12
16
|
{ label: "Sonnet 4.5", value: "bedrock-sonnet-4.5", modelId: "us.anthropic.claude-sonnet-4-5-20250929-v1:0", provider: "Bedrock" },
|
|
13
17
|
{ label: "Haiku 4.5", value: "bedrock-haiku", modelId: "us.anthropic.claude-haiku-4-5-20251001-v1:0", provider: "Bedrock" },
|
|
14
18
|
// Gemini
|
|
@@ -26,7 +30,7 @@ export const MODEL_OPTIONS = [
|
|
|
26
30
|
{ label: "GPT-4o", value: "gpt-4o", modelId: "gpt-4o", provider: "OpenAI" },
|
|
27
31
|
];
|
|
28
32
|
// Group labels to render section headers
|
|
29
|
-
const PROVIDER_ORDER = ["Anthropic", "Bedrock", "Gemini", "OpenAI"];
|
|
33
|
+
const PROVIDER_ORDER = ["Auto", "Anthropic", "Bedrock", "Gemini", "OpenAI"];
|
|
30
34
|
export function ModelSelector({ currentModel, onSelect, onCancel }) {
|
|
31
35
|
useInput((_input, key) => {
|
|
32
36
|
if (key.escape)
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RewindViewer — conversation timeline for rewind selection
|
|
3
|
+
*
|
|
4
|
+
* Displays checkpoints as a navigable list. Users select a point to
|
|
5
|
+
* rewind to with arrow keys and Enter. Esc cancels.
|
|
6
|
+
*
|
|
7
|
+
* UX modeled after Gemini CLI's rewind viewer:
|
|
8
|
+
* - Shows each turn with a summary of what happened
|
|
9
|
+
* - Lists file changes for each turn
|
|
10
|
+
* - Highlight selected checkpoint
|
|
11
|
+
* - Confirmation step with rewind options
|
|
12
|
+
*/
|
|
13
|
+
import type { RewindCheckpoint } from "../services/rewind.js";
|
|
14
|
+
export declare enum RewindOutcome {
|
|
15
|
+
RewindAndRevert = "rewind_and_revert",
|
|
16
|
+
RewindOnly = "rewind_only",
|
|
17
|
+
RevertOnly = "revert_only",
|
|
18
|
+
Cancel = "cancel"
|
|
19
|
+
}
|
|
20
|
+
interface RewindViewerProps {
|
|
21
|
+
checkpoints: RewindCheckpoint[];
|
|
22
|
+
onRewind: (checkpointIndex: number, outcome: RewindOutcome) => void;
|
|
23
|
+
onCancel: () => void;
|
|
24
|
+
}
|
|
25
|
+
export declare function RewindViewer({ checkpoints, onRewind, onCancel }: RewindViewerProps): import("react/jsx-runtime").JSX.Element;
|
|
26
|
+
export {};
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* RewindViewer — conversation timeline for rewind selection
|
|
4
|
+
*
|
|
5
|
+
* Displays checkpoints as a navigable list. Users select a point to
|
|
6
|
+
* rewind to with arrow keys and Enter. Esc cancels.
|
|
7
|
+
*
|
|
8
|
+
* UX modeled after Gemini CLI's rewind viewer:
|
|
9
|
+
* - Shows each turn with a summary of what happened
|
|
10
|
+
* - Lists file changes for each turn
|
|
11
|
+
* - Highlight selected checkpoint
|
|
12
|
+
* - Confirmation step with rewind options
|
|
13
|
+
*/
|
|
14
|
+
import { useState, useMemo } from "react";
|
|
15
|
+
import { Box, Text, useInput } from "ink";
|
|
16
|
+
import { colors, symbols } from "../shared/Theme.js";
|
|
17
|
+
// ============================================================================
|
|
18
|
+
// TYPES
|
|
19
|
+
// ============================================================================
|
|
20
|
+
export var RewindOutcome;
|
|
21
|
+
(function (RewindOutcome) {
|
|
22
|
+
RewindOutcome["RewindAndRevert"] = "rewind_and_revert";
|
|
23
|
+
RewindOutcome["RewindOnly"] = "rewind_only";
|
|
24
|
+
RewindOutcome["RevertOnly"] = "revert_only";
|
|
25
|
+
RewindOutcome["Cancel"] = "cancel";
|
|
26
|
+
})(RewindOutcome || (RewindOutcome = {}));
|
|
27
|
+
// ============================================================================
|
|
28
|
+
// HELPERS
|
|
29
|
+
// ============================================================================
|
|
30
|
+
function truncateSummary(text, maxLen) {
|
|
31
|
+
if (!text)
|
|
32
|
+
return "(no text)";
|
|
33
|
+
const cleaned = text.replace(/\n/g, " ").trim();
|
|
34
|
+
if (cleaned.length <= maxLen)
|
|
35
|
+
return cleaned;
|
|
36
|
+
return cleaned.slice(0, maxLen - 1) + "\u2026";
|
|
37
|
+
}
|
|
38
|
+
function formatFileOp(op, isNew) {
|
|
39
|
+
if (isNew)
|
|
40
|
+
return "created";
|
|
41
|
+
switch (op) {
|
|
42
|
+
case "write": return "written";
|
|
43
|
+
case "edit": return "edited";
|
|
44
|
+
case "multi_edit": return "edited";
|
|
45
|
+
default: return "modified";
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
function formatTimeAgo(timestamp) {
|
|
49
|
+
const diff = Date.now() - timestamp;
|
|
50
|
+
const seconds = Math.floor(diff / 1000);
|
|
51
|
+
if (seconds < 60)
|
|
52
|
+
return "just now";
|
|
53
|
+
const minutes = Math.floor(seconds / 60);
|
|
54
|
+
if (minutes < 60)
|
|
55
|
+
return `${minutes}m ago`;
|
|
56
|
+
const hours = Math.floor(minutes / 60);
|
|
57
|
+
return `${hours}h ago`;
|
|
58
|
+
}
|
|
59
|
+
function basename(filePath) {
|
|
60
|
+
const parts = filePath.split("/");
|
|
61
|
+
return parts[parts.length - 1] || filePath;
|
|
62
|
+
}
|
|
63
|
+
// ============================================================================
|
|
64
|
+
// CONFIRMATION COMPONENT
|
|
65
|
+
// ============================================================================
|
|
66
|
+
function RewindConfirmation({ checkpoint, fileChangeCount, onConfirm, onCancel, }) {
|
|
67
|
+
const hasFileChanges = fileChangeCount > 0;
|
|
68
|
+
const options = useMemo(() => {
|
|
69
|
+
const opts = [];
|
|
70
|
+
if (hasFileChanges) {
|
|
71
|
+
opts.push({
|
|
72
|
+
label: "Rewind conversation and revert file changes",
|
|
73
|
+
value: RewindOutcome.RewindAndRevert,
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
opts.push({
|
|
77
|
+
label: "Rewind conversation only",
|
|
78
|
+
value: RewindOutcome.RewindOnly,
|
|
79
|
+
});
|
|
80
|
+
if (hasFileChanges) {
|
|
81
|
+
opts.push({
|
|
82
|
+
label: "Revert file changes only",
|
|
83
|
+
value: RewindOutcome.RevertOnly,
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
opts.push({
|
|
87
|
+
label: "Cancel (Esc)",
|
|
88
|
+
value: RewindOutcome.Cancel,
|
|
89
|
+
});
|
|
90
|
+
return opts;
|
|
91
|
+
}, [hasFileChanges]);
|
|
92
|
+
const [selectedIndex, setSelectedIndex] = useState(0);
|
|
93
|
+
useInput((input, key) => {
|
|
94
|
+
if (key.escape) {
|
|
95
|
+
onCancel();
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
if (key.upArrow) {
|
|
99
|
+
setSelectedIndex(prev => Math.max(0, prev - 1));
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
if (key.downArrow) {
|
|
103
|
+
setSelectedIndex(prev => Math.min(options.length - 1, prev + 1));
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
if (key.return) {
|
|
107
|
+
onConfirm(options[selectedIndex].value);
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
});
|
|
111
|
+
const termWidth = process.stdout.columns || 80;
|
|
112
|
+
const boxWidth = Math.min(termWidth - 2, 60);
|
|
113
|
+
return (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: colors.border, paddingX: 1, paddingY: 1, width: boxWidth, children: [_jsx(Box, { marginBottom: 1, children: _jsx(Text, { bold: true, color: colors.brand, children: "Confirm Rewind" }) }), _jsxs(Box, { marginBottom: 1, flexDirection: "column", children: [_jsxs(Text, { dimColor: true, children: ["Rewind to turn ", checkpoint.turnIndex + 1, ": ", truncateSummary(checkpoint.summary, 40)] }), hasFileChanges && (_jsxs(Text, { dimColor: true, children: [fileChangeCount, " file change", fileChangeCount !== 1 ? "s" : "", " will be affected"] })), !hasFileChanges && (_jsx(Text, { dimColor: true, children: "No file changes to revert." }))] }), _jsx(Box, { marginBottom: 1, children: _jsx(Text, { children: "Select an action:" }) }), _jsx(Box, { flexDirection: "column", children: options.map((opt, i) => (_jsxs(Box, { children: [_jsxs(Text, { color: i === selectedIndex ? colors.brand : colors.quaternary, children: [i === selectedIndex ? symbols.arrowRight : " ", " "] }), _jsx(Text, { color: i === selectedIndex ? colors.text : colors.secondary, bold: i === selectedIndex, children: opt.label })] }, opt.value))) })] }));
|
|
114
|
+
}
|
|
115
|
+
// ============================================================================
|
|
116
|
+
// MAIN VIEWER COMPONENT
|
|
117
|
+
// ============================================================================
|
|
118
|
+
export function RewindViewer({ checkpoints, onRewind, onCancel }) {
|
|
119
|
+
// Start selection at the last checkpoint (current position)
|
|
120
|
+
const [selectedIndex, setSelectedIndex] = useState(checkpoints.length - 1);
|
|
121
|
+
const [confirmIndex, setConfirmIndex] = useState(null);
|
|
122
|
+
// Calculate visible window (scrolling for long lists)
|
|
123
|
+
const termHeight = process.stdout.rows || 24;
|
|
124
|
+
const maxVisible = Math.max(3, Math.min(checkpoints.length, termHeight - 8));
|
|
125
|
+
const scrollOffset = useMemo(() => {
|
|
126
|
+
if (checkpoints.length <= maxVisible)
|
|
127
|
+
return 0;
|
|
128
|
+
// Keep selected item roughly centered
|
|
129
|
+
const half = Math.floor(maxVisible / 2);
|
|
130
|
+
const start = Math.max(0, Math.min(selectedIndex - half, checkpoints.length - maxVisible));
|
|
131
|
+
return start;
|
|
132
|
+
}, [selectedIndex, maxVisible, checkpoints.length]);
|
|
133
|
+
const visibleCheckpoints = checkpoints.slice(scrollOffset, scrollOffset + maxVisible);
|
|
134
|
+
useInput((input, key) => {
|
|
135
|
+
if (confirmIndex !== null)
|
|
136
|
+
return; // Confirmation handles its own input
|
|
137
|
+
if (key.escape) {
|
|
138
|
+
onCancel();
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
if (key.upArrow) {
|
|
142
|
+
setSelectedIndex(prev => Math.max(0, prev - 1));
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
if (key.downArrow) {
|
|
146
|
+
setSelectedIndex(prev => Math.min(checkpoints.length - 1, prev + 1));
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
if (key.return) {
|
|
150
|
+
// Selecting the last checkpoint (current) = cancel
|
|
151
|
+
if (selectedIndex === checkpoints.length - 1) {
|
|
152
|
+
onCancel();
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
setConfirmIndex(selectedIndex);
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
});
|
|
159
|
+
const termWidth = process.stdout.columns || 80;
|
|
160
|
+
const boxWidth = Math.min(termWidth - 2, 70);
|
|
161
|
+
// Show confirmation dialog
|
|
162
|
+
if (confirmIndex !== null) {
|
|
163
|
+
const checkpoint = checkpoints[confirmIndex];
|
|
164
|
+
const fileChangeCount = checkpoints
|
|
165
|
+
.slice(confirmIndex + 1)
|
|
166
|
+
.reduce((sum, cp) => sum + cp.fileChanges.length, 0);
|
|
167
|
+
return (_jsx(RewindConfirmation, { checkpoint: checkpoint, fileChangeCount: fileChangeCount, onConfirm: (outcome) => {
|
|
168
|
+
if (outcome === RewindOutcome.Cancel) {
|
|
169
|
+
setConfirmIndex(null);
|
|
170
|
+
}
|
|
171
|
+
else {
|
|
172
|
+
onRewind(confirmIndex, outcome);
|
|
173
|
+
}
|
|
174
|
+
}, onCancel: () => setConfirmIndex(null) }));
|
|
175
|
+
}
|
|
176
|
+
if (checkpoints.length === 0) {
|
|
177
|
+
return (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: colors.border, paddingX: 1, paddingY: 1, width: boxWidth, children: [_jsx(Text, { bold: true, color: colors.brand, children: "Rewind" }), _jsx(Text, { children: " " }), _jsx(Text, { dimColor: true, children: "Nothing to rewind to." }), _jsx(Text, { children: " " }), _jsx(Text, { dimColor: true, children: "Press Esc to close." })] }));
|
|
178
|
+
}
|
|
179
|
+
return (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: colors.border, paddingX: 1, paddingY: 1, width: boxWidth, children: [_jsx(Box, { marginBottom: 1, children: _jsxs(Text, { bold: true, color: colors.brand, children: [symbols.arrowRight, " Rewind"] }) }), scrollOffset > 0 && (_jsxs(Text, { dimColor: true, children: [" ", symbols.dot, symbols.dot, symbols.dot, " ", scrollOffset, " more above"] })), _jsx(Box, { flexDirection: "column", children: visibleCheckpoints.map((cp, visIdx) => {
|
|
180
|
+
const realIdx = scrollOffset + visIdx;
|
|
181
|
+
const isSelected = realIdx === selectedIndex;
|
|
182
|
+
const isCurrent = realIdx === checkpoints.length - 1;
|
|
183
|
+
return (_jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [_jsxs(Box, { children: [_jsxs(Text, { color: isSelected ? colors.brand : colors.quaternary, children: [isSelected ? symbols.arrowRight : " ", " "] }), _jsx(Text, { color: isSelected ? colors.text : (isCurrent ? colors.secondary : colors.text), children: isCurrent ? symbols.dot : symbols.bullet }), _jsxs(Text, { color: isSelected ? colors.brand : colors.secondary, bold: isSelected, children: [" ", "Turn ", cp.turnIndex + 1] }), _jsxs(Text, { dimColor: true, children: [" ", symbols.divider, " "] }), _jsx(Text, { color: isSelected ? colors.text : colors.secondary, bold: isSelected, children: truncateSummary(cp.summary, 45) }), isCurrent && (_jsx(Text, { dimColor: true, children: " (current)" }))] }), cp.fileChanges.length > 0 && (_jsx(Box, { flexDirection: "column", marginLeft: 4, children: cp.fileChanges.map((fc, fi) => (_jsxs(Text, { dimColor: true, children: [basename(fc.filePath), " (", formatFileOp(fc.operation, fc.isNewFile), ")"] }, fi))) })), isSelected && (_jsx(Box, { marginLeft: 4, children: _jsx(Text, { dimColor: true, children: formatTimeAgo(cp.timestamp) }) }))] }, realIdx));
|
|
184
|
+
}) }), scrollOffset + maxVisible < checkpoints.length && (_jsxs(Text, { dimColor: true, children: [" ", symbols.dot, symbols.dot, symbols.dot, " ", checkpoints.length - scrollOffset - maxVisible, " more below"] })), _jsx(Box, { marginTop: 1, children: _jsxs(Text, { dimColor: true, children: [symbols.arrowRight, " Use arrow keys to select, Enter to rewind, Esc to cancel"] }) })] }));
|
|
185
|
+
}
|
|
@@ -7,7 +7,7 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
|
|
|
7
7
|
* Params: purple keys, typed values (blue dates, green money, red negatives).
|
|
8
8
|
* Duration badge, tool type glyph.
|
|
9
9
|
*/
|
|
10
|
-
import React, { useMemo } from "react";
|
|
10
|
+
import React, { useMemo, useState, useEffect } from "react";
|
|
11
11
|
import { Box, Text } from "ink";
|
|
12
12
|
import Spinner from "ink-spinner";
|
|
13
13
|
import { MarkdownText } from "./MarkdownText.js";
|
|
@@ -102,11 +102,21 @@ function formatContext(name, input) {
|
|
|
102
102
|
const ln = input.line ? `:${input.line}` : "";
|
|
103
103
|
return `${op} ${fp}${ln}`;
|
|
104
104
|
}
|
|
105
|
+
// Kali — show the command being executed (most useful context)
|
|
106
|
+
if (name === "kali" && input.command) {
|
|
107
|
+
let cmd = String(input.command);
|
|
108
|
+
const maxCtx = Math.min(60, Math.max(20, contentWidth() - 40));
|
|
109
|
+
cmd = cmd.length > maxCtx ? cmd.slice(0, maxCtx - 1) + "…" : cmd;
|
|
110
|
+
const action = input.action ? String(input.action) + " " : "";
|
|
111
|
+
return action + cmd;
|
|
112
|
+
}
|
|
105
113
|
// Server tools → action + key param
|
|
106
114
|
if (input.action) {
|
|
107
115
|
const parts = [String(input.action)];
|
|
108
116
|
if (input.query)
|
|
109
117
|
parts.push(String(input.query).slice(0, 25));
|
|
118
|
+
else if (input.command)
|
|
119
|
+
parts.push(String(input.command).slice(0, 40));
|
|
110
120
|
else if (input.name)
|
|
111
121
|
parts.push(String(input.name));
|
|
112
122
|
else if (input.period)
|
|
@@ -274,6 +284,15 @@ function wrapInFence(content, lang, subtitle) {
|
|
|
274
284
|
export const ToolIndicator = React.memo(function ToolIndicator({ id: _id, name, status, result, input, durationMs, expanded = false, count }) {
|
|
275
285
|
const context = useMemo(() => formatContext(name, input), [name, input]);
|
|
276
286
|
const lineCount = useMemo(() => result ? result.split("\n").length : 0, [result]);
|
|
287
|
+
// Elapsed time counter for running tools
|
|
288
|
+
const [elapsed, setElapsed] = useState(0);
|
|
289
|
+
useEffect(() => {
|
|
290
|
+
if (status !== "running")
|
|
291
|
+
return;
|
|
292
|
+
setElapsed(0);
|
|
293
|
+
const t = setInterval(() => setElapsed(e => e + 1), 1000);
|
|
294
|
+
return () => clearInterval(t);
|
|
295
|
+
}, [status]);
|
|
277
296
|
// Detect lang — writes with diffs get "diff" treatment
|
|
278
297
|
const lang = useMemo(() => {
|
|
279
298
|
const base = detectLang(name, input);
|
|
@@ -370,11 +389,11 @@ export const ToolIndicator = React.memo(function ToolIndicator({ id: _id, name,
|
|
|
370
389
|
}, [status, result]);
|
|
371
390
|
// ── RUNNING ──
|
|
372
391
|
if (status === "running") {
|
|
373
|
-
return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { children: [_jsx(Text, { color: catStyle.color, children: _jsx(Spinner, { type: "dots" }) }), _jsxs(Text, { color: catStyle.color, children: [" ", catStyle.icon] }), _jsxs(Text, { color: catStyle.color, bold: true, children: [" ", getDisplayName(name)] }), context ? _jsxs(Text, { color: colors.dim, children: [" ", context] }) : null] }), liveLines.length > 0 && (_jsx(Box, { flexDirection: "column", marginLeft: 4, children: liveLines.map((line, i) => (_jsx(Text, { color: colors.tertiary, wrap: "truncate", children: line }, i))) }))] }));
|
|
392
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { children: [_jsx(Text, { color: catStyle.color, children: _jsx(Spinner, { type: "dots" }) }), _jsxs(Text, { color: catStyle.color, children: [" ", catStyle.icon] }), _jsxs(Text, { color: catStyle.color, bold: true, children: [" ", getDisplayName(name)] }), context ? _jsxs(Text, { color: colors.dim, children: [" ", context] }) : null, elapsed >= 2 && _jsxs(Text, { color: elapsed >= 30 ? colors.warning : colors.dim, children: [" ", formatDuration(elapsed * 1000)] })] }), liveLines.length > 0 && (_jsx(Box, { flexDirection: "column", marginLeft: 4, children: liveLines.map((line, i) => (_jsx(Text, { color: colors.tertiary, wrap: "truncate", children: line }, i))) }))] }));
|
|
374
393
|
}
|
|
375
394
|
// ── ERROR ──
|
|
376
395
|
if (status === "error") {
|
|
377
|
-
return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { children: [_jsx(Text, { color: colors.error, bold: true, children: "\u2715" }), _jsxs(Text, { color: catStyle.color, children: [" ", catStyle.icon] }), _jsxs(Text, { color: catStyle.color, bold: true, children: [" ", getDisplayName(name)] }), context ? _jsxs(Text, { color: colors.dim, children: [" ", context] }) : null, durationMs !== undefined && _jsxs(Text, { color: colors.dim, children: [" ", formatDuration(durationMs)] })] }), result && (_jsx(Box, { marginLeft: 2, children: _jsx(MarkdownText, { text: "```\n" + result.split("\n").slice(0,
|
|
396
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { children: [_jsx(Text, { color: colors.error, bold: true, children: "\u2715" }), _jsxs(Text, { color: catStyle.color, children: [" ", catStyle.icon] }), _jsxs(Text, { color: catStyle.color, bold: true, children: [" ", getDisplayName(name)] }), context ? _jsxs(Text, { color: colors.dim, children: [" ", context] }) : null, durationMs !== undefined && _jsxs(Text, { color: colors.dim, children: [" ", formatDuration(durationMs)] })] }), result && (_jsx(Box, { marginLeft: 2, children: _jsx(MarkdownText, { text: "```\n" + result.split("\n").slice(0, PREVIEW_LINES).join("\n") + "\n```" }) }))] }));
|
|
378
397
|
}
|
|
379
398
|
// ── SUCCESS ──
|
|
380
399
|
const hasResult = !!(result && result.trim());
|
|
@@ -388,9 +407,19 @@ export const ToolIndicator = React.memo(function ToolIndicator({ id: _id, name,
|
|
|
388
407
|
const alwaysExpand = category === "interactive";
|
|
389
408
|
const isGrouped = (count ?? 0) > 1;
|
|
390
409
|
const showFull = hasResult && !isGrouped && (expanded || alwaysExpand || (isShort && !collapseByDefault));
|
|
410
|
+
// For grouped tools, extract a one-line summary so they aren't completely invisible
|
|
411
|
+
const groupSummaryLine = useMemo(() => {
|
|
412
|
+
if (!isGrouped || !result)
|
|
413
|
+
return null;
|
|
414
|
+
const firstLine = result.split("\n").find(l => l.trim() && !l.startsWith("**Success**") && !l.startsWith("**Killed**"));
|
|
415
|
+
if (!firstLine)
|
|
416
|
+
return null;
|
|
417
|
+
const trimmed = firstLine.replace(/^\*\*\w+\*\*:\s*/, "").trim();
|
|
418
|
+
return trimmed.length > 80 ? trimmed.slice(0, 77) + "…" : trimmed;
|
|
419
|
+
}, [isGrouped, result]);
|
|
391
420
|
return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { children: [_jsx(Text, { color: colors.success, children: "\u2713" }), _jsxs(Text, { color: catStyle.color, children: [" ", catStyle.icon] }), _jsxs(Text, { color: catStyle.color, bold: true, children: [" ", getDisplayName(name)] }), context ? _jsxs(Text, { color: colors.dim, children: [" ", context] }) : null, durationMs !== undefined && (durationMs > 3000
|
|
392
421
|
? _jsxs(Text, { color: colors.warning, children: [" ", formatDuration(durationMs)] })
|
|
393
|
-
: _jsxs(Text, { color: colors.dim, children: [" ", formatDuration(durationMs)] })), summary?.type === "edit" && category === "edit" && (_jsxs(_Fragment, { children: [_jsxs(Text, { color: colors.success, children: [" +", summary.added] }), _jsxs(Text, { color: colors.error, children: [" -", summary.removed] })] })), summary?.type === "search" && (_jsxs(Text, { color: colors.dim, children: [" ", summary.matches, " match", summary.matches !== 1 ? "es" : "", summary.files > 0 ? ` in ${summary.files} file${summary.files !== 1 ? "s" : ""}` : ""] })), summary && "label" in summary && summary.type !== "server" && (_jsxs(Text, { color: colors.dim, children: [" ", summary.label] })), summary?.type === "server" && (_jsxs(Text, { color: colors.info, children: [" ", summary.label] })), !summary && hasResult && !showFull && _jsxs(Text, { color: colors.dim, children: [" ", lineCount, " lines"] }), (count ?? 0) > 1 && _jsxs(Text, { color: colors.dim, dimColor: true, children: [" \u00D7 ", count] })] }), category === "write" && hasDiff && summary?.type === "edit" && (_jsxs(Box, { marginLeft: 2, children: [_jsx(Text, { color: colors.tertiary, children: "\u2514 Added " }), _jsx(Text, { color: colors.success, children: summary.added }), _jsx(Text, { color: colors.tertiary, children: " lines, removed " }), _jsx(Text, { color: colors.error, children: summary.removed }), _jsx(Text, { color: colors.tertiary, children: " lines" })] })), showFull && (_jsx(Box, { marginLeft: 2, flexDirection: "column", children: _jsx(MarkdownText, { text: category === "agent"
|
|
422
|
+
: _jsxs(Text, { color: colors.dim, children: [" ", formatDuration(durationMs)] })), summary?.type === "edit" && category === "edit" && (_jsxs(_Fragment, { children: [_jsxs(Text, { color: colors.success, children: [" +", summary.added] }), _jsxs(Text, { color: colors.error, children: [" -", summary.removed] })] })), summary?.type === "search" && (_jsxs(Text, { color: colors.dim, children: [" ", summary.matches, " match", summary.matches !== 1 ? "es" : "", summary.files > 0 ? ` in ${summary.files} file${summary.files !== 1 ? "s" : ""}` : ""] })), summary && "label" in summary && summary.type !== "server" && (_jsxs(Text, { color: colors.dim, children: [" ", summary.label] })), summary?.type === "server" && (_jsxs(Text, { color: colors.info, children: [" ", summary.label] })), !summary && hasResult && !showFull && _jsxs(Text, { color: colors.dim, children: [" ", lineCount, " lines"] }), (count ?? 0) > 1 && _jsxs(Text, { color: colors.dim, dimColor: true, children: [" \u00D7 ", count] })] }), isGrouped && groupSummaryLine && (_jsx(Box, { marginLeft: 4, children: _jsx(Text, { color: colors.tertiary, wrap: "truncate", children: groupSummaryLine }) })), category === "write" && hasDiff && summary?.type === "edit" && (_jsxs(Box, { marginLeft: 2, children: [_jsx(Text, { color: colors.tertiary, children: "\u2514 Added " }), _jsx(Text, { color: colors.success, children: summary.added }), _jsx(Text, { color: colors.tertiary, children: " lines, removed " }), _jsx(Text, { color: colors.error, children: summary.removed }), _jsx(Text, { color: colors.tertiary, children: " lines" })] })), showFull && (_jsx(Box, { marginLeft: 2, flexDirection: "column", children: _jsx(MarkdownText, { text: category === "agent"
|
|
394
423
|
? result
|
|
395
424
|
: wrapInFence(category === "search" ? formatSearchResult(result) : result, lang, filePath) }) })), hasResult && !showFull && !collapseByDefault && !isGrouped && (_jsxs(Box, { flexDirection: "column", marginLeft: 2, children: [_jsx(MarkdownText, { text: category === "agent"
|
|
396
425
|
? result.split("\n").slice(0, PREVIEW_LINES).join("\n")
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
* All consumers should import from ChatApp (re-export facade).
|
|
5
5
|
*/
|
|
6
6
|
import type Anthropic from "@anthropic-ai/sdk";
|
|
7
|
+
import { RewindManager } from "../../services/rewind.js";
|
|
7
8
|
import type { ChatMessage, ToolCall } from "../MessageList.js";
|
|
8
9
|
import type { SubagentActivityState, CompletedSubagentInfo } from "../SubagentPanel.js";
|
|
9
10
|
import type { ImageAttachment } from "../ChatInput.js";
|
|
@@ -17,6 +18,8 @@ export interface AgentLoopDeps {
|
|
|
17
18
|
teamTimerRef: React.MutableRefObject<NodeJS.Timeout | null>;
|
|
18
19
|
toolOutputTimerRef: React.MutableRefObject<NodeJS.Timeout | null>;
|
|
19
20
|
thinkingVerbRef: React.MutableRefObject<string>;
|
|
21
|
+
rewindManagerRef: React.MutableRefObject<RewindManager>;
|
|
22
|
+
turnIndexRef: React.MutableRefObject<number>;
|
|
20
23
|
setMessages: React.Dispatch<React.SetStateAction<ChatMessage[]>>;
|
|
21
24
|
setStreamingText: React.Dispatch<React.SetStateAction<string>>;
|
|
22
25
|
setIsStreaming: React.Dispatch<React.SetStateAction<boolean>>;
|