whale-code 6.4.0 → 6.5.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 (187) hide show
  1. package/bin/swagmanager-mcp.js +7 -0
  2. package/dist/cli/app.js +30 -2
  3. package/dist/cli/chat/ChatApp.d.ts +4 -4
  4. package/dist/cli/chat/ChatApp.js +114 -44
  5. package/dist/cli/chat/ChatInput.d.ts +13 -6
  6. package/dist/cli/chat/ChatInput.js +433 -89
  7. package/dist/cli/chat/MemoryManager.d.ts +15 -0
  8. package/dist/cli/chat/MemoryManager.js +61 -0
  9. package/dist/cli/chat/MessageList.d.ts +8 -0
  10. package/dist/cli/chat/MessageList.js +1 -1
  11. package/dist/cli/chat/NodeManager.d.ts +30 -0
  12. package/dist/cli/chat/NodeManager.js +89 -0
  13. package/dist/cli/chat/NodeSelector.d.ts +19 -0
  14. package/dist/cli/chat/NodeSelector.js +37 -0
  15. package/dist/cli/chat/PlanApproval.d.ts +17 -0
  16. package/dist/cli/chat/PlanApproval.js +82 -0
  17. package/dist/cli/chat/SessionManager.d.ts +16 -0
  18. package/dist/cli/chat/SessionManager.js +43 -0
  19. package/dist/cli/chat/SlashMenu.d.ts +38 -0
  20. package/dist/cli/chat/SlashMenu.js +208 -0
  21. package/dist/cli/chat/StatusBar.d.ts +16 -0
  22. package/dist/cli/chat/StatusBar.js +22 -0
  23. package/dist/cli/chat/ThemeSelector.d.ts +14 -0
  24. package/dist/cli/chat/ThemeSelector.js +29 -0
  25. package/dist/cli/chat/ToolIndicator.d.ts +8 -0
  26. package/dist/cli/chat/ToolIndicator.js +33 -9
  27. package/dist/cli/chat/hooks/useAgentLoop.d.ts +2 -1
  28. package/dist/cli/chat/hooks/useAgentLoop.js +22 -17
  29. package/dist/cli/chat/hooks/useSlashCommands.d.ts +19 -0
  30. package/dist/cli/chat/hooks/useSlashCommands.js +254 -15
  31. package/dist/cli/commands/config-cmd.js +4 -25
  32. package/dist/cli/commands/db.d.ts +13 -0
  33. package/dist/cli/commands/db.js +243 -0
  34. package/dist/cli/commands/doctor.js +6 -9
  35. package/dist/cli/commands/mcp.js +1 -20
  36. package/dist/cli/services/agent-events.d.ts +22 -1
  37. package/dist/cli/services/agent-events.js +9 -0
  38. package/dist/cli/services/agent-loop.js +66 -2
  39. package/dist/cli/services/agent-worker-base.js +21 -6
  40. package/dist/cli/services/api-retry.d.ts +25 -0
  41. package/dist/cli/services/api-retry.js +91 -0
  42. package/dist/cli/services/auth-service.d.ts +1 -1
  43. package/dist/cli/services/auth-service.js +40 -19
  44. package/dist/cli/services/background-processes.js +26 -2
  45. package/dist/cli/services/config-store.d.ts +13 -1
  46. package/dist/cli/services/config-store.js +116 -13
  47. package/dist/cli/services/format-server-response.js +12 -6
  48. package/dist/cli/services/ink-resize-fix.d.ts +18 -0
  49. package/dist/cli/services/ink-resize-fix.js +66 -0
  50. package/dist/cli/services/interactive-tools.d.ts +14 -0
  51. package/dist/cli/services/interactive-tools.js +47 -2
  52. package/dist/cli/services/keybinding-manager.js +1 -1
  53. package/dist/cli/services/local-tools.js +35 -2
  54. package/dist/cli/services/server-tools.js +175 -3
  55. package/dist/cli/services/subagent.js +15 -3
  56. package/dist/cli/services/system-prompt.js +5 -3
  57. package/dist/cli/services/task-decomposer.d.ts +35 -0
  58. package/dist/cli/services/task-decomposer.js +199 -0
  59. package/dist/cli/services/team-lead.d.ts +18 -0
  60. package/dist/cli/services/team-lead.js +80 -0
  61. package/dist/cli/services/teammate.js +5 -5
  62. package/dist/cli/services/telemetry.d.ts +8 -2
  63. package/dist/cli/services/telemetry.js +116 -92
  64. package/dist/cli/services/tools/agent-tools.d.ts +1 -0
  65. package/dist/cli/services/tools/agent-tools.js +50 -4
  66. package/dist/cli/services/tools/file-ops.d.ts +2 -0
  67. package/dist/cli/services/tools/file-ops.js +71 -19
  68. package/dist/cli/services/tools/shell-exec.js +22 -12
  69. package/dist/cli/shared/Theme.d.ts +1 -2
  70. package/dist/cli/shared/Theme.js +1 -1
  71. package/dist/cli/shared/WhaleBanner.d.ts +4 -1
  72. package/dist/cli/shared/WhaleBanner.js +12 -8
  73. package/dist/cli/shared/markdown.d.ts +5 -4
  74. package/dist/cli/shared/markdown.js +376 -334
  75. package/dist/cli/shared/theme-manager.d.ts +27 -0
  76. package/dist/cli/shared/theme-manager.js +178 -0
  77. package/dist/cli/shared/theme-presets.d.ts +16 -0
  78. package/dist/cli/shared/theme-presets.js +265 -0
  79. package/dist/index.js +0 -51
  80. package/dist/node/adapters/imessage.d.ts +10 -0
  81. package/dist/node/adapters/imessage.js +45 -6
  82. package/dist/node/cli.js +459 -8
  83. package/dist/node/config.d.ts +17 -0
  84. package/dist/node/gateway-client.d.ts +55 -0
  85. package/dist/node/gateway-client.js +201 -0
  86. package/dist/node/portal/clipboard.d.ts +28 -0
  87. package/dist/node/portal/clipboard.js +183 -0
  88. package/dist/node/portal/discovery.d.ts +29 -0
  89. package/dist/node/portal/discovery.js +61 -0
  90. package/dist/node/portal/forward.d.ts +30 -0
  91. package/dist/node/portal/forward.js +90 -0
  92. package/dist/node/portal/index.d.ts +47 -0
  93. package/dist/node/portal/index.js +250 -0
  94. package/dist/node/portal/multiplexer.d.ts +48 -0
  95. package/dist/node/portal/multiplexer.js +207 -0
  96. package/dist/node/portal/permissions.d.ts +36 -0
  97. package/dist/node/portal/permissions.js +131 -0
  98. package/dist/node/portal/protocol.d.ts +140 -0
  99. package/dist/node/portal/protocol.js +193 -0
  100. package/dist/node/portal/screen.d.ts +18 -0
  101. package/dist/node/portal/screen.js +93 -0
  102. package/dist/node/portal/session.d.ts +68 -0
  103. package/dist/node/portal/session.js +127 -0
  104. package/dist/node/portal/shell.d.ts +26 -0
  105. package/dist/node/portal/shell.js +142 -0
  106. package/dist/node/portal/stream.d.ts +43 -0
  107. package/dist/node/portal/stream.js +90 -0
  108. package/dist/node/portal/transfer.d.ts +33 -0
  109. package/dist/node/portal/transfer.js +231 -0
  110. package/dist/node/portal/ui.d.ts +16 -0
  111. package/dist/node/portal/ui.js +148 -0
  112. package/dist/node/remote-desktop/compile-helper.d.ts +13 -0
  113. package/dist/node/remote-desktop/compile-helper.js +73 -0
  114. package/dist/node/remote-desktop/index.d.ts +67 -0
  115. package/dist/node/remote-desktop/index.js +220 -0
  116. package/dist/node/remote-desktop/protocol.d.ts +96 -0
  117. package/dist/node/remote-desktop/protocol.js +67 -0
  118. package/dist/node/runtime.d.ts +8 -1
  119. package/dist/node/runtime.js +117 -9
  120. package/dist/server/handlers/__test-utils__/test-db.d.ts +25 -0
  121. package/dist/server/handlers/__test-utils__/test-db.js +128 -0
  122. package/dist/server/handlers/api-keys.js +26 -2
  123. package/dist/server/handlers/browser.d.ts +0 -4
  124. package/dist/server/handlers/browser.js +0 -46
  125. package/dist/server/handlers/catalog.js +37 -14
  126. package/dist/server/handlers/clickhouse.d.ts +10 -0
  127. package/dist/server/handlers/clickhouse.js +215 -0
  128. package/dist/server/handlers/comms.d.ts +308 -4
  129. package/dist/server/handlers/comms.js +444 -11
  130. package/dist/server/handlers/creations.js +1 -1
  131. package/dist/server/handlers/crm.d.ts +54 -8
  132. package/dist/server/handlers/crm.js +353 -68
  133. package/dist/server/handlers/embeddings.js +3 -3
  134. package/dist/server/handlers/enrichment.js +39 -55
  135. package/dist/server/handlers/inventory.js +1 -1
  136. package/dist/server/handlers/kali.d.ts +9 -1
  137. package/dist/server/handlers/kali.js +50 -1
  138. package/dist/server/handlers/media.d.ts +8 -0
  139. package/dist/server/handlers/media.js +902 -0
  140. package/dist/server/handlers/meta-ads.js +6 -3
  141. package/dist/server/handlers/nodes.d.ts +2 -0
  142. package/dist/server/handlers/nodes.js +331 -40
  143. package/dist/server/handlers/operations.d.ts +4 -6
  144. package/dist/server/handlers/operations.js +99 -38
  145. package/dist/server/handlers/platform.js +224 -107
  146. package/dist/server/handlers/remove-bg.d.ts +6 -0
  147. package/dist/server/handlers/remove-bg.js +96 -0
  148. package/dist/server/handlers/storefront.d.ts +6 -0
  149. package/dist/server/handlers/storefront.js +477 -0
  150. package/dist/server/handlers/supply-chain.js +21 -3
  151. package/dist/server/handlers/workflow-steps.js +87 -31
  152. package/dist/server/handlers/workflows.js +4 -1
  153. package/dist/server/index.js +334 -88
  154. package/dist/server/lib/clickhouse-buffer.d.ts +48 -0
  155. package/dist/server/lib/clickhouse-buffer.js +175 -0
  156. package/dist/server/lib/clickhouse-client.d.ts +112 -0
  157. package/dist/server/lib/clickhouse-client.js +141 -0
  158. package/dist/server/lib/coa-renderer.d.ts +91 -0
  159. package/dist/server/lib/coa-renderer.js +411 -0
  160. package/dist/server/lib/compaction-service.js +45 -1
  161. package/dist/server/lib/pdf-renderer.d.ts +143 -0
  162. package/dist/server/lib/pdf-renderer.js +867 -0
  163. package/dist/server/lib/react-pdf-layout.d.ts +40 -0
  164. package/dist/server/lib/react-pdf-layout.js +437 -0
  165. package/dist/server/lib/server-agent-loop.d.ts +2 -0
  166. package/dist/server/lib/server-agent-loop.js +61 -15
  167. package/dist/server/lib/server-subagent.d.ts +3 -0
  168. package/dist/server/lib/server-subagent.js +7 -4
  169. package/dist/server/lib/supabase-client.js +51 -3
  170. package/dist/server/lib/template-resolver.js +14 -4
  171. package/dist/server/lib/utils.js +15 -0
  172. package/dist/server/local-agent-gateway.d.ts +44 -0
  173. package/dist/server/local-agent-gateway.js +389 -49
  174. package/dist/server/providers/anthropic.js +12 -2
  175. package/dist/server/providers/gemini.js +17 -2
  176. package/dist/server/proxy-handlers.js +151 -0
  177. package/dist/server/tool-router.d.ts +2 -2
  178. package/dist/server/tool-router.js +25 -35
  179. package/dist/shared/agent-core.d.ts +5 -2
  180. package/dist/shared/agent-core.js +30 -4
  181. package/dist/shared/api-client.js +54 -3
  182. package/dist/shared/sse-parser.d.ts +1 -1
  183. package/dist/shared/sse-parser.js +5 -2
  184. package/dist/shared/tool-dispatch.js +1 -1
  185. package/package.json +16 -10
  186. package/dist/server/handlers/__test-utils__/mock-supabase.d.ts +0 -11
  187. package/dist/server/handlers/__test-utils__/mock-supabase.js +0 -393
@@ -111,6 +111,7 @@ function showHelp() {
111
111
  console.log(` whale doctor${d} Run diagnostics${r}`);
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
+ console.log(` whale db${d} Local Supabase management${r}`);
114
115
  console.log(` whale serve${d} Local agent WebSocket server${r}`);
115
116
  console.log(` whale agent${d} Start local security agent${r}`);
116
117
  console.log();
@@ -305,6 +306,12 @@ switch (command) {
305
306
  break;
306
307
  }
307
308
 
309
+ case "db": {
310
+ const { runDbCommand } = await import(join(distDir, "cli", "commands", "db.js"));
311
+ await runDbCommand(positionals.slice(1));
312
+ break;
313
+ }
314
+
308
315
  case "agent": {
309
316
  // Forward remaining args to local-agent CLI
310
317
  // Rebuild process.argv so the agent sees: [node, script, subcommand, ...flags]
package/dist/cli/app.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import { render } from "ink";
3
+ import chalk from "chalk";
3
4
  import { initErrorLogger, setErrorLoggerUser, captureError } from "./services/error-logger.js";
4
5
  import { loadConfig } from "./services/config-store.js";
5
6
  export async function renderLogin() {
@@ -32,8 +33,35 @@ export async function renderChat(options) {
32
33
  const err = reason instanceof Error ? reason : new Error(String(reason));
33
34
  captureError({ error: err, severity: "error", tags: { source: "unhandledRejection" } });
34
35
  });
35
- const { matrixIntro } = await import("./shared/MatrixIntro.js");
36
- await matrixIntro();
36
+ // Auto-login if not authenticated seamless browser OAuth
37
+ const { isLoggedIn } = await import("./services/auth-service.js");
38
+ if (!isLoggedIn()) {
39
+ const { signInWithBrowser } = await import("./services/browser-auth.js");
40
+ const brand = chalk.hex("#6366F1");
41
+ const dim = chalk.hex("#64748B");
42
+ const green = chalk.hex("#30D158");
43
+ console.log();
44
+ console.log(` ${brand.bold("◆")} Opening browser to sign in...`);
45
+ const result = await signInWithBrowser(undefined, {
46
+ onBrowserOpening: (url) => {
47
+ console.log(` ${dim(url)}`);
48
+ console.log();
49
+ console.log(` ${dim("Waiting for authentication...")}`);
50
+ },
51
+ });
52
+ if (!result.success) {
53
+ console.error(` ${chalk.red("✗")} ${result.error}`);
54
+ console.error(` ${dim("Run")} whale login ${dim("to try again.")}`);
55
+ process.exit(1);
56
+ }
57
+ console.log(` ${green("✓")} Signed in as ${result.config?.email || "unknown"}`);
58
+ if (result.config?.store_name) {
59
+ console.log(` ${dim("Store:")} ${result.config.store_name}`);
60
+ }
61
+ console.log();
62
+ }
63
+ // Clear screen — fresh start
64
+ process.stdout.write("\x1b[2J\x1b[3J\x1b[H");
37
65
  // Apply options before starting chat
38
66
  if (options?.model || options?.permissionMode) {
39
67
  const agentLoop = await import("./services/agent-loop.js");
@@ -1,9 +1,9 @@
1
1
  /**
2
2
  * ChatApp — whale code CLI
3
3
  *
4
- * Uses Ink's <Static> for completed messages — written to stdout once,
5
- * never re-rendered. Only the active area (streaming, tools, input)
6
- * is managed by Ink's render loop. This prevents scroll bounce when
7
- * content exceeds the terminal height.
4
+ * Uses Ink's <Static> for ALL completed messages — written to stdout once,
5
+ * never re-rendered. Only the active area (running tools, streaming text,
6
+ * thinking spinner, input) is in Ink's render loop.
7
+ * This prevents screen overwriting and allows scrolling during generation.
8
8
  */
9
9
  export declare function ChatApp(): import("react/jsx-runtime").JSX.Element;
@@ -2,10 +2,10 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
2
2
  /**
3
3
  * ChatApp — whale code CLI
4
4
  *
5
- * Uses Ink's <Static> for completed messages — written to stdout once,
6
- * never re-rendered. Only the active area (streaming, tools, input)
7
- * is managed by Ink's render loop. This prevents scroll bounce when
8
- * content exceeds the terminal height.
5
+ * Uses Ink's <Static> for ALL completed messages — written to stdout once,
6
+ * never re-rendered. Only the active area (running tools, streaming text,
7
+ * thinking spinner, input) is in Ink's render loop.
8
+ * This prevents screen overwriting and allows scrolling during generation.
9
9
  */
10
10
  import { useState, useEffect, useRef, useMemo, useCallback } from "react";
11
11
  import { Box, Text, Static, useApp, useInput } from "ink";
@@ -16,12 +16,23 @@ import { ToolIndicator } from "./ToolIndicator.js";
16
16
  import { SubagentPanel } from "./SubagentPanel.js";
17
17
  import { TeamPanel } from "./TeamPanel.js";
18
18
  import { StreamingText } from "./StreamingText.js";
19
+ import { MarkdownText } from "./MarkdownText.js";
19
20
  import { ChatInput } from "./ChatInput.js";
21
+ import { StatusBar } from "./StatusBar.js";
20
22
  import { StoreSelector } from "./StoreSelector.js";
23
+ import { NodeSelector } from "./NodeSelector.js";
24
+ import { SessionManager } from "./SessionManager.js";
25
+ import { MemoryManager } from "./MemoryManager.js";
26
+ import { NodeManager } from "./NodeManager.js";
21
27
  import { ModelSelector } from "./ModelSelector.js";
28
+ import { ThemeSelector } from "./ThemeSelector.js";
22
29
  import { RewindViewer, RewindOutcome } from "./RewindViewer.js";
30
+ import { PlanApproval } from "./PlanApproval.js";
23
31
  import { RewindManager } from "../services/rewind.js";
32
+ import { interactiveEvents, resolvePlanApproval } from "../services/interactive-tools.js";
24
33
  import { colors, symbols } from "../shared/Theme.js";
34
+ import { initTheme, switchTheme } from "../shared/theme-manager.js";
35
+ import { WhaleBanner } from "../shared/WhaleBanner.js";
25
36
  import { loadKeybindings, matchesBinding } from "../services/keybinding-manager.js";
26
37
  import { loadConfig, updateConfig } from "../services/config-store.js";
27
38
  import { createRequire } from "module";
@@ -30,22 +41,6 @@ import { dirname, join } from "path";
30
41
  // Extracted hooks
31
42
  import { useSlashCommands } from "./hooks/useSlashCommands.js";
32
43
  import { useAgentLoop } from "./hooks/useAgentLoop.js";
33
- // Thinking verbs — rotate randomly each render (Claude Code parity)
34
- const THINKING_VERBS = [
35
- "thinking…",
36
- "reasoning…",
37
- "considering…",
38
- "analyzing…",
39
- "evaluating…",
40
- "pondering…",
41
- "processing…",
42
- "reflecting…",
43
- "examining…",
44
- "working…",
45
- ];
46
- function randomVerb() {
47
- return THINKING_VERBS[Math.floor(Math.random() * THINKING_VERBS.length)];
48
- }
49
44
  const PKG_NAME = "whale-code";
50
45
  const __filename = fileURLToPath(import.meta.url);
51
46
  const __dirname = dirname(__filename);
@@ -69,7 +64,16 @@ export function ChatApp() {
69
64
  const [serverToolsAvailable, setServerToolsAvailable] = useState(0);
70
65
  const [storeSelectMode, setStoreSelectMode] = useState(false);
71
66
  const [storeList, setStoreList] = useState([]);
67
+ const [nodeSelectMode, setNodeSelectMode] = useState(false);
68
+ const [nodeList, setNodeList] = useState([]);
69
+ const [sessionManagerMode, setSessionManagerMode] = useState(false);
70
+ const [sessionList, setSessionList] = useState([]);
71
+ const [memoryManagerMode, setMemoryManagerMode] = useState(false);
72
+ const [memoryList, setMemoryList] = useState([]);
73
+ const [nodeManagerMode, setNodeManagerMode] = useState(false);
74
+ const [nodeManagerStatus, setNodeManagerStatus] = useState({ registered: false, running: false });
72
75
  const [modelSelectMode, setModelSelectMode] = useState(false);
76
+ const [themeSelectMode, setThemeSelectMode] = useState(false);
73
77
  const [currentModel, setCurrentModel] = useState(getModelShortName());
74
78
  const [sessionId, setSessionId] = useState(null);
75
79
  const [thinkingEnabled, setThinkingEnabled] = useState(() => {
@@ -80,34 +84,57 @@ export function ChatApp() {
80
84
  return true;
81
85
  }
82
86
  });
83
- // Stream activity indicator — show spinner when no new content for 800ms+
84
- const [streamStale, setStreamStale] = useState(false);
85
87
  const lastContentUpdateRef = useRef(Date.now());
86
- // Poll-based staleness checkavoids re-renders on every content change
88
+ // Unified streaming activityverb cycling + elapsed timer (replaces separate thinking timer + StatusBar verb)
89
+ const FUN_VERBS = useMemo(() => [
90
+ "thinking", "pondering", "brewing", "cooking", "rowing", "juggling",
91
+ "weaving", "crafting", "forging", "sketching", "conjuring", "assembling",
92
+ "sculpting", "composing", "mixing", "polishing", "building", "wiring",
93
+ "spinning", "mapping", "plotting", "dreaming", "hurdling", "launching",
94
+ "surfing", "flying", "grabbing", "lifting", "twisting", "rolling",
95
+ ], []);
96
+ const [streamElapsed, setStreamElapsed] = useState(0);
97
+ const [verbIndex, setVerbIndex] = useState(0);
98
+ const streamStartRef = useRef(null);
87
99
  useEffect(() => {
88
- if (!isStreaming) {
89
- setStreamStale(false);
90
- return;
100
+ if (isStreaming) {
101
+ if (!streamStartRef.current)
102
+ streamStartRef.current = Date.now();
103
+ setStreamElapsed(0);
104
+ setVerbIndex(0);
105
+ const elapsedInterval = setInterval(() => {
106
+ if (streamStartRef.current) {
107
+ setStreamElapsed(Math.floor((Date.now() - streamStartRef.current) / 1000));
108
+ }
109
+ }, 1000);
110
+ const verbInterval = setInterval(() => {
111
+ setVerbIndex(i => (i + 1) % FUN_VERBS.length);
112
+ }, 4000);
113
+ return () => { clearInterval(elapsedInterval); clearInterval(verbInterval); };
91
114
  }
92
- lastContentUpdateRef.current = Date.now();
93
- const interval = setInterval(() => {
94
- setStreamStale(Date.now() - lastContentUpdateRef.current > 800);
95
- }, 400);
96
- return () => clearInterval(interval);
97
- }, [isStreaming]);
115
+ else {
116
+ streamStartRef.current = null;
117
+ setStreamElapsed(0);
118
+ setVerbIndex(0);
119
+ }
120
+ }, [isStreaming, FUN_VERBS]);
98
121
  // Rewind state
99
122
  const [showRewind, setShowRewind] = useState(false);
123
+ // Plan approval state
124
+ const [planApproval, setPlanApproval] = useState(null);
100
125
  const rewindManagerRef = useRef(new RewindManager());
101
126
  const turnIndexRef = useRef(0);
102
127
  // Refs
103
128
  const conversationRef = useRef([]);
104
129
  const abortRef = useRef(null);
105
130
  const accTextRef = useRef("");
106
- const thinkingVerbRef = useRef(randomVerb());
131
+ const thinkingChunksRef = useRef(0);
132
+ const thinkingDoneRef = useRef(false);
107
133
  // No timer cleanup needed — unified flush timer is managed inside useAgentLoop,
108
134
  // stale polling interval is cleaned up by its own effect.
109
135
  // ── Init ──
110
136
  useEffect(() => {
137
+ initTheme();
111
138
  const check = canUseAgent();
112
139
  if (!check.ready) {
113
140
  setError(check.reason || "Run 'whale login' to authenticate.");
@@ -120,8 +147,20 @@ export function ChatApp() {
120
147
  getServerToolCount().then((count) => setServerToolsAvailable(count));
121
148
  mcpClientManager.connectAll().catch(() => { });
122
149
  }
150
+ // Listen for plan approval requests — push plan content to Static for natural scrolling
151
+ const handlePlanExited = (data) => {
152
+ // Push plan content into messages so it renders in the <Static> area
153
+ const content = data.planContent || "(empty plan)";
154
+ setMessages(prev => [...prev, {
155
+ role: "assistant",
156
+ text: ` ${symbols.arrowRight} Plan Review — ${data.planFile}\n\n${content}`,
157
+ }]);
158
+ setPlanApproval({ planContent: data.planContent || "", planFile: data.planFile });
159
+ };
160
+ interactiveEvents.on("planModeExited", handlePlanExited);
123
161
  return () => {
124
162
  mcpClientManager.disconnectAll().catch(() => { });
163
+ interactiveEvents.off("planModeExited", handlePlanExited);
125
164
  };
126
165
  }, []);
127
166
  // ── Keys (configurable via ~/.swagmanager/keybindings.json) ──
@@ -153,24 +192,36 @@ export function ChatApp() {
153
192
  });
154
193
  }
155
194
  });
195
+ // ESC abort is now handled by ChatInput's onInterrupt prop — stdin stays active
196
+ // even when disabled, so ESC always reaches the handler.
156
197
  // ── Extracted hooks ──
157
- const { handleCommand, handleStoreSelect, handleStoreCancel } = useSlashCommands({
198
+ const { handleCommand, handleStoreSelect, handleStoreCancel, handleNodeSelect, handleNodeCancel, handleSessionSelect, handleMemoryDelete, handleNodeToggle, handleNodeRegister, handleNodeClose, } = useSlashCommands({
158
199
  exit, toolsExpanded, serverToolsAvailable, sessionId, thinkingEnabled,
159
200
  conversationRef,
160
201
  setMessages, setStreamingText, setActiveTools, setTeamState,
161
202
  setStoreList, setStoreSelectMode, setModelSelectMode, setCurrentModel,
162
203
  setSessionId, setThinkingEnabled, setUserLabel, setServerToolsAvailable,
163
204
  setShowRewind,
205
+ setNodeSelectMode, setNodeList,
206
+ setSessionManagerMode, setSessionList,
207
+ setMemoryManagerMode, setMemoryList,
208
+ setThemeSelectMode,
209
+ setNodeManagerMode, setNodeManagerStatus,
164
210
  rewindCheckpointCount: rewindManagerRef.current.getCheckpointCount(),
165
211
  PKG_NAME, PKG_VERSION,
166
212
  });
167
213
  const { handleSend } = useAgentLoop({
168
214
  isStreaming, thinkingEnabled, conversationRef, abortRef,
169
- accTextRef, lastContentUpdateRef, thinkingVerbRef,
215
+ accTextRef, lastContentUpdateRef, thinkingChunksRef, thinkingDoneRef,
170
216
  rewindManagerRef, turnIndexRef,
171
217
  setMessages, setStreamingText, setIsStreaming, setActiveTools,
172
218
  setSubagentActivity, setCompletedSubagents, setTeamState,
173
219
  });
220
+ // ── Plan approval handler ──
221
+ const handlePlanDecision = useCallback((decision) => {
222
+ setPlanApproval(null);
223
+ resolvePlanApproval(decision);
224
+ }, []);
174
225
  // ── Rewind handler ──
175
226
  const handleRewind = useCallback((checkpointIndex, outcome) => {
176
227
  const rm = rewindManagerRef.current;
@@ -235,27 +286,39 @@ export function ChatApp() {
235
286
  // ── Render ──
236
287
  const termWidth = process.stdout.columns || 80;
237
288
  const contentWidth = Math.max(20, termWidth - 2);
238
- const { staticItems, dynamicMessages } = useMemo(() => {
289
+ // ALL completed messages go to Static written to stdout once, never re-rendered.
290
+ // This keeps the dynamic area small (only active tools + streaming + input),
291
+ // preventing the constant screen overwriting that blocks scrolling.
292
+ const staticItems = useMemo(() => {
239
293
  const items = [{ id: "header", type: "header" }];
240
- const cutoff = Math.max(0, messages.length - 1);
241
- for (let i = 0; i < cutoff; i++) {
294
+ for (let i = 0; i < messages.length; i++) {
242
295
  items.push({ id: `msg-${i}`, type: "message", msg: messages[i], index: i });
243
296
  }
244
- const tail = messages.length > 0 ? [messages[messages.length - 1]] : [];
245
- return { staticItems: items, dynamicMessages: tail };
297
+ return items;
246
298
  }, [messages]);
247
299
  if (error) {
248
- return (_jsxs(Box, { flexDirection: "column", paddingLeft: 1, paddingRight: 1, children: [_jsx(Text, { children: " " }), _jsx(Text, { color: colors.brand, bold: true, children: "\u25C6 whale code" }), _jsx(Text, { children: " " }), _jsx(Text, { color: colors.error, children: error })] }));
300
+ return (_jsxs(Box, { flexDirection: "column", paddingLeft: 1, paddingRight: 1, children: [_jsx(WhaleBanner, {}), _jsx(Text, { children: " " }), _jsxs(Text, { color: colors.error, children: [" ", error] })] }));
249
301
  }
250
302
  if (!ready) {
251
303
  return (_jsx(Box, { flexDirection: "column", paddingLeft: 1, paddingRight: 1, children: _jsx(Text, { color: colors.tertiary, children: "loading..." }) }));
252
304
  }
305
+ // Plan approval no longer full-screen — plan content is pushed to Static (scrollable),
306
+ // PlanApproval renders as an overlay above the persistent input.
253
307
  return (_jsxs(Box, { flexDirection: "column", paddingLeft: 1, paddingRight: 1, children: [_jsx(Static, { items: staticItems, children: (item) => {
254
308
  if (item.type === "header") {
255
- 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));
309
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsx(WhaleBanner, {}), _jsx(Text, { children: " " }), _jsxs(Text, { children: [" ", userLabel ? _jsxs(Text, { color: colors.dim, children: [userLabel, " "] }) : null, _jsx(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, " tools"] })) : null] }), _jsx(Text, { children: " " })] }, item.id));
256
310
  }
257
311
  return _jsx(CompletedMessage, { msg: item.msg, index: item.index, toolsExpanded: toolsExpanded }, item.id);
258
- } }), 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 && (_jsxs(Box, { marginLeft: 2, flexDirection: "column", children: [_jsx(StreamingText, { text: streamingText }), streamStale && (_jsx(Box, { marginTop: 0, children: _jsx(Text, { color: colors.brand, children: _jsx(Spinner, { type: "dots" }) }) }))] }))] })), 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) => {
312
+ } }), teamState ? (_jsx(TeamPanel, { team: teamState })) : (_jsxs(_Fragment, { children: [isStreaming && (_jsxs(Box, { marginLeft: 2, marginTop: 1, marginBottom: 1, children: [_jsx(Text, { color: colors.brand, children: _jsx(Spinner, { type: "dots" }) }), _jsxs(Text, { color: colors.dim, children: [" ", FUN_VERBS[verbIndex]] }), streamElapsed > 0 && (_jsxs(Text, { color: colors.quaternary, children: [" ", streamElapsed < 60 ? `${streamElapsed}s` : `${Math.floor(streamElapsed / 60)}:${(streamElapsed % 60).toString().padStart(2, "0")}`] }))] })), 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, result: tc.result, input: tc.input, expanded: toolsExpanded, progress: tc.progress }), tc.name === "task" && tc.status === "running" && (subagentActivity.size > 0 || completedSubagents.length > 0) && (_jsx(SubagentPanel, { running: subagentActivity, completed: completedSubagents }))] }, `live-${tc.name}-${i}`))) })), toolsExpanded && !isStreaming && (() => {
313
+ // Find collapsed tool results from the last assistant turn
314
+ const lastAssistant = [...messages].reverse().find(m => m.role === "assistant" && m.toolCalls && m.toolCalls.length > 0);
315
+ if (!lastAssistant?.toolCalls)
316
+ return null;
317
+ const collapsed = lastAssistant.toolCalls.filter(tc => tc.result && tc.status === "success" && tc.result.split("\n").length > 12);
318
+ if (collapsed.length === 0)
319
+ return null;
320
+ return (_jsxs(Box, { flexDirection: "column", marginLeft: 2, marginBottom: 1, children: [_jsxs(Text, { color: colors.tertiary, children: ["─".repeat(Math.min(60, (process.stdout.columns || 80) - 6)), " ^O close"] }), collapsed.map((tc, i) => (_jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [_jsx(Text, { color: colors.info, bold: true, children: tc.name }), _jsx(MarkdownText, { text: tc.result })] }, i))), _jsx(Text, { color: colors.tertiary, children: "─".repeat(Math.min(60, (process.stdout.columns || 80) - 6)) })] }));
321
+ })(), streamingText && (_jsx(Box, { marginLeft: 2, flexDirection: "column", children: _jsx(StreamingText, { text: streamingText }) }))] })), showRewind && (_jsx(RewindViewer, { checkpoints: rewindManagerRef.current.getCheckpoints(), onRewind: handleRewind, onCancel: () => setShowRewind(false) })), sessionManagerMode && (_jsx(SessionManager, { sessions: sessionList, onSelect: handleSessionSelect, onCancel: () => setSessionManagerMode(false) })), memoryManagerMode && (_jsx(MemoryManager, { memories: memoryList, onDelete: handleMemoryDelete, onCancel: () => setMemoryManagerMode(false) })), nodeManagerMode && (_jsx(NodeManager, { initialStatus: nodeManagerStatus, onToggle: handleNodeToggle, onRegister: handleNodeRegister, onClose: handleNodeClose })), storeSelectMode && (_jsx(StoreSelector, { stores: storeList, currentStoreId: loadConfig().store_id || "", onSelect: handleStoreSelect, onCancel: handleStoreCancel })), nodeSelectMode && (_jsx(NodeSelector, { nodes: nodeList, onSelect: handleNodeSelect, onCancel: handleNodeCancel })), modelSelectMode && (_jsx(ModelSelector, { currentModel: currentModel, onSelect: (model) => {
259
322
  setModelSelectMode(false);
260
323
  setModel(model.value);
261
324
  setCurrentModel(model.value);
@@ -263,5 +326,12 @@ export function ChatApp() {
263
326
  role: "assistant",
264
327
  text: ` ${symbols.check} Model: ${model.label} (${model.modelId})`,
265
328
  }]);
266
- }, onCancel: () => setModelSelectMode(false) })) : (_jsx(ChatInput, { onSubmit: handleSend, onCommand: handleCommand, disabled: isStreaming, agentName: "whale code" }))] }));
329
+ }, onCancel: () => setModelSelectMode(false) })), themeSelectMode && (_jsx(ThemeSelector, { onSelect: (preset) => {
330
+ setThemeSelectMode(false);
331
+ switchTheme(preset.id);
332
+ setMessages((prev) => [...prev, {
333
+ role: "assistant",
334
+ text: ` ${symbols.check} Theme: ${preset.label}`,
335
+ }]);
336
+ }, onCancel: () => setThemeSelectMode(false) })), planApproval && (_jsx(PlanApproval, { planFile: planApproval.planFile, onDecision: handlePlanDecision })), _jsx(ChatInput, { onSubmit: handleSend, onCommand: handleCommand, disabled: isStreaming || !!planApproval, agentName: "whale code", onInterrupt: () => abortRef.current?.abort() }), _jsx(StatusBar, { permissionMode: getPermissionMode(), model: currentModel, thinkingEnabled: thinkingEnabled, serverTools: serverToolsAvailable, isStreaming: isStreaming, toolsExpanded: toolsExpanded })] }));
267
337
  }
@@ -24,16 +24,23 @@ export interface FileAttachment {
24
24
  path: string;
25
25
  name: string;
26
26
  }
27
- export interface SlashCommand {
28
- name: string;
29
- description: string;
27
+ export interface ContextAttachment {
28
+ id: string;
29
+ type: string;
30
+ title: string;
31
+ subtitle?: string;
32
+ icon: string;
33
+ sourceApp: string;
34
+ data: Record<string, unknown>;
35
+ timestamp: string;
30
36
  }
31
- export declare const SLASH_COMMANDS: SlashCommand[];
37
+ export type { SlashCommand } from "./SlashMenu.js";
38
+ export declare const SLASH_COMMANDS: import("./SlashMenu.js").SlashCommand[];
32
39
  interface ChatInputProps {
33
40
  onSubmit: (message: string, images?: ImageAttachment[]) => void;
34
41
  onCommand: (command: string) => void;
35
42
  disabled: boolean;
36
43
  agentName?: string;
44
+ onInterrupt?: () => void;
37
45
  }
38
- export declare function ChatInput({ onSubmit, onCommand, disabled, agentName }: ChatInputProps): import("react/jsx-runtime").JSX.Element;
39
- export {};
46
+ export declare function ChatInput({ onSubmit, onCommand, disabled, agentName, onInterrupt }: ChatInputProps): import("react/jsx-runtime").JSX.Element;