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.
Files changed (132) hide show
  1. package/bin/swag-agent.js +9 -0
  2. package/bin/swagmanager-mcp.js +10 -0
  3. package/dist/cli/chat/ChatApp.js +72 -2
  4. package/dist/cli/chat/ChatInput.js +1 -0
  5. package/dist/cli/chat/MessageList.d.ts +4 -0
  6. package/dist/cli/chat/MessageList.js +11 -5
  7. package/dist/cli/chat/ModelSelector.js +7 -3
  8. package/dist/cli/chat/RewindViewer.d.ts +26 -0
  9. package/dist/cli/chat/RewindViewer.js +185 -0
  10. package/dist/cli/chat/ToolIndicator.js +33 -4
  11. package/dist/cli/chat/hooks/useAgentLoop.d.ts +3 -0
  12. package/dist/cli/chat/hooks/useAgentLoop.js +42 -3
  13. package/dist/cli/chat/hooks/useSlashCommands.d.ts +2 -0
  14. package/dist/cli/chat/hooks/useSlashCommands.js +11 -2
  15. package/dist/cli/print-mode.js +8 -2
  16. package/dist/cli/serve-mode.d.ts +3 -1
  17. package/dist/cli/serve-mode.js +54 -9
  18. package/dist/cli/services/agent-events.d.ts +5 -1
  19. package/dist/cli/services/agent-events.js +2 -2
  20. package/dist/cli/services/agent-loop.d.ts +3 -1
  21. package/dist/cli/services/agent-loop.js +287 -23
  22. package/dist/cli/services/agent-worker-base.js +16 -3
  23. package/dist/cli/services/auth-service.js +3 -2
  24. package/dist/cli/services/config-store.d.ts +1 -0
  25. package/dist/cli/services/config-store.js +4 -2
  26. package/dist/cli/services/error-logger.d.ts +58 -0
  27. package/dist/cli/services/error-logger.js +269 -0
  28. package/dist/cli/services/format-server-response.js +203 -36
  29. package/dist/cli/services/format-server-response.test.js +62 -36
  30. package/dist/cli/services/hooks.d.ts +71 -42
  31. package/dist/cli/services/hooks.js +229 -176
  32. package/dist/cli/services/hooks.test.d.ts +1 -0
  33. package/dist/cli/services/hooks.test.js +344 -0
  34. package/dist/cli/services/local-tools-files.test.d.ts +1 -0
  35. package/dist/cli/services/local-tools-files.test.js +231 -0
  36. package/dist/cli/services/local-tools.js +97 -2
  37. package/dist/cli/services/loop-detector.d.ts +64 -0
  38. package/dist/cli/services/loop-detector.js +167 -0
  39. package/dist/cli/services/model-manager.d.ts +3 -1
  40. package/dist/cli/services/model-manager.js +8 -2
  41. package/dist/cli/services/model-router.d.ts +26 -0
  42. package/dist/cli/services/model-router.js +149 -0
  43. package/dist/cli/services/model-router.test.d.ts +1 -0
  44. package/dist/cli/services/model-router.test.js +206 -0
  45. package/dist/cli/services/rewind.d.ts +84 -0
  46. package/dist/cli/services/rewind.js +194 -0
  47. package/dist/cli/services/rewind.test.d.ts +4 -0
  48. package/dist/cli/services/rewind.test.js +292 -0
  49. package/dist/cli/services/server-tools.d.ts +5 -1
  50. package/dist/cli/services/server-tools.js +313 -8
  51. package/dist/cli/services/slash-commands.d.ts +50 -0
  52. package/dist/cli/services/slash-commands.js +284 -0
  53. package/dist/cli/services/subagent-runner.d.ts +11 -0
  54. package/dist/cli/services/subagent-spawn.d.ts +32 -0
  55. package/dist/cli/services/subagent.js +18 -1
  56. package/dist/cli/services/system-prompt.js +34 -1
  57. package/dist/cli/services/teammate.js +10 -1
  58. package/dist/cli/services/telemetry.d.ts +1 -0
  59. package/dist/cli/services/telemetry.js +27 -6
  60. package/dist/cli/services/tools/agent-tools.js +1 -1
  61. package/dist/cli/services/tools/file-ops.js +28 -5
  62. package/dist/cli/services/tools/shell-exec.js +2 -2
  63. package/dist/cli/services/tools/web-tools.js +7 -1
  64. package/dist/index.js +169 -10
  65. package/dist/local-agent/connection.d.ts +48 -0
  66. package/dist/local-agent/connection.js +332 -0
  67. package/dist/local-agent/discovery.d.ts +18 -0
  68. package/dist/local-agent/discovery.js +146 -0
  69. package/dist/local-agent/executor.d.ts +34 -0
  70. package/dist/local-agent/executor.js +241 -0
  71. package/dist/local-agent/index.d.ts +14 -0
  72. package/dist/local-agent/index.js +198 -0
  73. package/dist/server/auth.d.ts +3 -0
  74. package/dist/server/auth.js +36 -0
  75. package/dist/server/handlers/api-keys.d.ts +6 -0
  76. package/dist/server/handlers/api-keys.js +221 -0
  77. package/dist/server/handlers/browser.js +155 -3
  78. package/dist/server/handlers/catalog.d.ts +22 -22
  79. package/dist/server/handlers/catalog.js +160 -6
  80. package/dist/server/handlers/creations.d.ts +6 -0
  81. package/dist/server/handlers/creations.js +479 -0
  82. package/dist/server/handlers/enrichment.d.ts +8 -0
  83. package/dist/server/handlers/enrichment.js +768 -0
  84. package/dist/server/handlers/inventory.d.ts +65 -15
  85. package/dist/server/handlers/inventory.js +127 -71
  86. package/dist/server/handlers/kali.d.ts +10 -0
  87. package/dist/server/handlers/kali.js +210 -0
  88. package/dist/server/handlers/llm-providers.js +7 -4
  89. package/dist/server/handlers/local-agent.d.ts +6 -0
  90. package/dist/server/handlers/local-agent.js +118 -0
  91. package/dist/server/handlers/meta-ads.d.ts +111 -0
  92. package/dist/server/handlers/meta-ads.js +2279 -0
  93. package/dist/server/handlers/supply-chain.d.ts +0 -8
  94. package/dist/server/handlers/supply-chain.js +174 -212
  95. package/dist/server/handlers/transcription.d.ts +17 -0
  96. package/dist/server/handlers/transcription.js +121 -0
  97. package/dist/server/handlers/voice.d.ts +4 -2
  98. package/dist/server/handlers/voice.js +1064 -58
  99. package/dist/server/handlers/workflows.js +23 -0
  100. package/dist/server/index.js +197 -56
  101. package/dist/server/lib/compaction-service.d.ts +20 -0
  102. package/dist/server/lib/compaction-service.js +99 -0
  103. package/dist/server/lib/server-agent-loop.d.ts +6 -3
  104. package/dist/server/lib/server-agent-loop.js +129 -83
  105. package/dist/server/lib/server-subagent.js +19 -12
  106. package/dist/server/lib/utils.d.ts +3 -1
  107. package/dist/server/lib/utils.js +8 -2
  108. package/dist/server/local-agent-gateway.d.ts +82 -0
  109. package/dist/server/local-agent-gateway.js +426 -0
  110. package/dist/server/proxy-handlers.js +27 -7
  111. package/dist/server/routes.d.ts +34 -0
  112. package/dist/server/routes.js +963 -0
  113. package/dist/server/tool-router.d.ts +37 -7
  114. package/dist/server/tool-router.js +182 -90
  115. package/dist/server/validation.js +23 -1
  116. package/dist/services/tool-registry.d.ts +37 -0
  117. package/dist/services/tool-registry.js +45 -0
  118. package/dist/shared/agent-core.d.ts +24 -4
  119. package/dist/shared/agent-core.js +148 -29
  120. package/dist/shared/agent-core.test.js +74 -11
  121. package/dist/shared/anthropic-types.d.ts +3 -0
  122. package/dist/shared/api-client.d.ts +30 -0
  123. package/dist/shared/api-client.js +138 -3
  124. package/dist/shared/api-client.test.js +135 -1
  125. package/dist/shared/constants.d.ts +5 -3
  126. package/dist/shared/constants.js +13 -4
  127. package/dist/shared/sse-parser.js +16 -2
  128. package/dist/shared/sse-parser.test.js +84 -0
  129. package/dist/shared/tool-dispatch.d.ts +5 -1
  130. package/dist/shared/tool-dispatch.js +50 -11
  131. package/dist/shared/tool-dispatch.test.js +60 -0
  132. 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
+ });
@@ -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.`);
@@ -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 Opus pricing as default since that's the primary model
22
- const opusPricing = MODEL_PRICING["claude-opus-4-6"] || { inputPer1M: 15, outputPer1M: 75 };
23
- const cost = (input * opusPricing.inputPer1M + output * opusPricing.outputPer1M) / 1_000_000;
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-20250514", provider: "Anthropic" },
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: "us.anthropic.claude-sonnet-4-20250514-v1:0", provider: "Bedrock" },
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, 3).join("\n") + "\n```" }) }))] }));
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>>;