whale-code 6.4.0 → 6.5.1

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 +51 -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 +65 -8
  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 +7 -6
  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 +85 -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 +46 -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 +36 -17
  167. package/dist/server/lib/server-subagent.d.ts +3 -0
  168. package/dist/server/lib/server-subagent.js +9 -6
  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 +25 -2
  180. package/dist/shared/agent-core.js +66 -5
  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 +15 -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
@@ -1,10 +1,12 @@
1
1
  /**
2
2
  * Config Store
3
3
  *
4
- * Persistent configuration at ~/.swagmanager/config.json
4
+ * Unified auth session at ~/.whaletools/session.json
5
+ * User preferences at ~/.whaletools/preferences.json
5
6
  *
6
7
  * v2.0: Raw Supabase/Anthropic keys (for MCP server env vars)
7
8
  * v2.1: Auth tokens from login flow (for CLI chat/status)
9
+ * v4.0: Shared auth with Swift apps via ~/.whaletools/session.json
8
10
  *
9
11
  * Environment variables always override file-based config for MCP server mode.
10
12
  */
@@ -14,32 +16,111 @@ import { join } from "path";
14
16
  // ============================================================================
15
17
  // PATHS
16
18
  // ============================================================================
17
- const CONFIG_DIR = join(homedir(), ".swagmanager");
18
- const CONFIG_PATH = join(CONFIG_DIR, "config.json");
19
+ const CONFIG_DIR = join(homedir(), ".whaletools");
20
+ const SESSION_PATH = join(CONFIG_DIR, "session.json");
21
+ const PREFS_PATH = join(CONFIG_DIR, "preferences.json");
22
+ // Legacy paths for migration
23
+ const LEGACY_CONFIG_DIR = join(homedir(), ".swagmanager");
24
+ const LEGACY_CONFIG_PATH = join(LEGACY_CONFIG_DIR, "config.json");
19
25
  // ============================================================================
20
- // READ / WRITE
26
+ // AUTO-MIGRATION from ~/.swagmanager to ~/.whaletools
27
+ // ============================================================================
28
+ let migrationChecked = false;
29
+ function ensureMigration() {
30
+ if (migrationChecked)
31
+ return;
32
+ migrationChecked = true;
33
+ // Skip if new session already exists
34
+ if (existsSync(SESSION_PATH))
35
+ return;
36
+ // Check for legacy config
37
+ if (!existsSync(LEGACY_CONFIG_PATH))
38
+ return;
39
+ try {
40
+ const legacy = JSON.parse(readFileSync(LEGACY_CONFIG_PATH, "utf-8"));
41
+ if (!existsSync(CONFIG_DIR)) {
42
+ mkdirSync(CONFIG_DIR, { recursive: true, mode: 0o700 });
43
+ }
44
+ // Split auth fields into session.json
45
+ if (legacy.access_token && legacy.refresh_token) {
46
+ const session = {
47
+ access_token: legacy.access_token,
48
+ refresh_token: legacy.refresh_token,
49
+ user_id: legacy.user_id,
50
+ email: legacy.email,
51
+ store_id: legacy.store_id,
52
+ store_name: legacy.store_name,
53
+ expires_at: legacy.expires_at,
54
+ };
55
+ writeFileSync(SESSION_PATH, JSON.stringify(session, null, 2) + "\n", {
56
+ encoding: "utf-8",
57
+ mode: 0o600,
58
+ });
59
+ }
60
+ // Split preference fields into preferences.json
61
+ const prefs = {};
62
+ if (legacy.default_model)
63
+ prefs.default_model = legacy.default_model;
64
+ if (legacy.thinking_enabled !== undefined)
65
+ prefs.thinking_enabled = legacy.thinking_enabled;
66
+ if (legacy.permission_mode)
67
+ prefs.permission_mode = legacy.permission_mode;
68
+ if (legacy.platform_url)
69
+ prefs.platform_url = legacy.platform_url;
70
+ if (Object.keys(prefs).length > 0) {
71
+ writeFileSync(PREFS_PATH, JSON.stringify(prefs, null, 2) + "\n", {
72
+ encoding: "utf-8",
73
+ mode: 0o600,
74
+ });
75
+ }
76
+ }
77
+ catch {
78
+ // Migration failed — not fatal, user can re-login
79
+ }
80
+ }
81
+ // ============================================================================
82
+ // READ / WRITE — session.json (auth tokens + store)
21
83
  // ============================================================================
22
84
  export function loadConfig() {
85
+ ensureMigration();
23
86
  try {
24
- if (existsSync(CONFIG_PATH)) {
25
- return JSON.parse(readFileSync(CONFIG_PATH, "utf-8"));
87
+ if (existsSync(SESSION_PATH)) {
88
+ const session = JSON.parse(readFileSync(SESSION_PATH, "utf-8"));
89
+ // Merge preferences so callers see a unified config
90
+ const prefs = loadPreferences();
91
+ return { ...prefs, ...session };
26
92
  }
27
93
  }
28
94
  catch (err) {
29
- // Log warning for corrupted config user may need to re-login
30
- console.error(`[config] Warning: Failed to parse ${CONFIG_PATH}: ${err instanceof Error ? err.message : err}`);
95
+ console.error(`[config] Warning: Failed to parse ${SESSION_PATH}: ${err instanceof Error ? err.message : err}`);
31
96
  console.error("[config] Using empty config. You may need to re-login with: whale login");
32
97
  }
33
- return {};
98
+ // Even if no session, return preferences
99
+ return { ...loadPreferences() };
34
100
  }
35
101
  export function saveConfig(config) {
36
102
  if (!existsSync(CONFIG_DIR)) {
37
103
  mkdirSync(CONFIG_DIR, { recursive: true, mode: 0o700 });
38
104
  }
39
- writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2) + "\n", {
105
+ // Split: auth fields go to session.json, prefs go to preferences.json
106
+ const { default_model, thinking_enabled, permission_mode, platform_url, ...authFields } = config;
107
+ writeFileSync(SESSION_PATH, JSON.stringify(authFields, null, 2) + "\n", {
40
108
  encoding: "utf-8",
41
109
  mode: 0o600,
42
110
  });
111
+ // Update preferences if any pref fields are present
112
+ const newPrefs = {};
113
+ if (default_model !== undefined)
114
+ newPrefs.default_model = default_model;
115
+ if (thinking_enabled !== undefined)
116
+ newPrefs.thinking_enabled = thinking_enabled;
117
+ if (permission_mode !== undefined)
118
+ newPrefs.permission_mode = permission_mode;
119
+ if (platform_url !== undefined)
120
+ newPrefs.platform_url = platform_url;
121
+ if (Object.keys(newPrefs).length > 0) {
122
+ savePreferences({ ...loadPreferences(), ...newPrefs });
123
+ }
43
124
  }
44
125
  export function updateConfig(partial) {
45
126
  const existing = loadConfig();
@@ -47,10 +128,32 @@ export function updateConfig(partial) {
47
128
  }
48
129
  export function clearConfig() {
49
130
  try {
50
- if (existsSync(CONFIG_PATH))
51
- unlinkSync(CONFIG_PATH);
131
+ if (existsSync(SESSION_PATH))
132
+ unlinkSync(SESSION_PATH);
133
+ }
134
+ catch { /* ignore */ }
135
+ // Preferences are preserved across sign-out
136
+ }
137
+ // ============================================================================
138
+ // READ / WRITE — preferences.json (survives sign-out)
139
+ // ============================================================================
140
+ export function loadPreferences() {
141
+ try {
142
+ if (existsSync(PREFS_PATH)) {
143
+ return JSON.parse(readFileSync(PREFS_PATH, "utf-8"));
144
+ }
52
145
  }
53
146
  catch { /* ignore */ }
147
+ return {};
148
+ }
149
+ export function savePreferences(prefs) {
150
+ if (!existsSync(CONFIG_DIR)) {
151
+ mkdirSync(CONFIG_DIR, { recursive: true, mode: 0o700 });
152
+ }
153
+ writeFileSync(PREFS_PATH, JSON.stringify(prefs, null, 2) + "\n", {
154
+ encoding: "utf-8",
155
+ mode: 0o600,
156
+ });
54
157
  }
55
158
  /** Default Fly.io agent server URL */
56
159
  export const WHALE_SERVER_URL = "https://whale-agent.fly.dev";
@@ -71,7 +174,7 @@ export function resolveConfig() {
71
174
  };
72
175
  }
73
176
  export function getConfigPath() {
74
- return CONFIG_PATH;
177
+ return SESSION_PATH;
75
178
  }
76
179
  /** Lazy proxy URL — avoids reading config at import time */
77
180
  export function getProxyUrl() {
@@ -18,6 +18,10 @@ const PRIORITY_COLUMNS = {
18
18
  supply_chain: ["name", "status", "quantity", "supplier", "location", "expected_delivery_date"],
19
19
  workflows: ["name", "status", "trigger_type", "created_at", "last_run"],
20
20
  email: ["subject", "to", "status", "sent_at", "category"],
21
+ locations: ["name", "city", "state", "type", "is_active", "address_line1"],
22
+ media: ["title", "file_name", "category", "tags", "file_type"],
23
+ creations: ["name", "creation_type", "status", "visibility", "slug"],
24
+ api_keys: ["name", "key_type", "is_active", "scopes"],
21
25
  };
22
26
  // Keys that are always deprioritized (shown last or hidden)
23
27
  const LOW_PRIORITY_KEYS = new Set([
@@ -27,9 +31,6 @@ const LOW_PRIORITY_KEYS = new Set([
27
31
  // ============================================================================
28
32
  // FORMATTERS
29
33
  // ============================================================================
30
- function isNarrow() {
31
- return (process.stdout.columns || 80) < 90;
32
- }
33
34
  function truncateUuid(val) {
34
35
  // UUID pattern: 8-4-4-4-12 hex chars
35
36
  if (/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(val)) {
@@ -149,7 +150,8 @@ function selectColumns(rows, toolName) {
149
150
  allKeys.add(key);
150
151
  }
151
152
  // Find matching priority list
152
- const maxCols = isNarrow() ? 4 : 6;
153
+ const termWidth = process.stdout.columns || 80;
154
+ const maxCols = termWidth < 90 ? 4 : termWidth < 120 ? 6 : termWidth < 160 ? 8 : 10;
153
155
  let priority = [];
154
156
  if (toolName) {
155
157
  // Match tool name to category
@@ -193,14 +195,18 @@ function buildTable(rows, columns) {
193
195
  if (columns.length === 0 || rows.length === 0)
194
196
  return "";
195
197
  const headers = columns.map(prettifyKey);
196
- // Calculate column widths
198
+ // Calculate column widths — dynamic cap based on terminal width and column count
199
+ const termW = process.stdout.columns || 80;
200
+ // Each column gets a fair share of available space (minus separators + borders)
201
+ const overhead = columns.length * 3 + 4; // " | " between cols + "| " and " |"
202
+ const cellCap = Math.min(Math.max(Math.floor((termW - overhead) / columns.length), 15), 50);
197
203
  const widths = columns.map((col, i) => {
198
204
  let max = headers[i].length;
199
205
  for (const row of rows) {
200
206
  const val = isMoneyKey(col) ? formatMoneyValue(row[col]) : formatValue(row[col], col);
201
207
  max = Math.max(max, val.length);
202
208
  }
203
- return Math.min(max, 30); // Cap individual column width
209
+ return Math.min(max, cellCap);
204
210
  });
205
211
  // Header row
206
212
  const headerRow = columns.map((_, i) => headers[i].padEnd(widths[i])).join(" | ");
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Ink Resize Ghost Fix
3
+ *
4
+ * Patches Ink 6's resize handler to prevent ghost artifacts from text reflow.
5
+ *
6
+ * Problem: Ink's resized() clears the dynamic area using eraseLines(N) where N
7
+ * is the string line count. But when the terminal shrinks, already-painted text
8
+ * reflows to more visual lines than N, so eraseLines(N) doesn't erase enough,
9
+ * leaving "ghost" lines above the new render.
10
+ *
11
+ * Fix: After Ink's standard clear, calculate the extra visual lines caused by
12
+ * reflow at the new width and erase them with cursor-up + erase-to-end-of-screen.
13
+ */
14
+ /**
15
+ * Patches the Ink instance's resize handler to account for visual line reflow.
16
+ * Call this AFTER render() — the Ink instance must exist in the registry.
17
+ */
18
+ export declare function patchInkResize(): Promise<void>;
@@ -0,0 +1,66 @@
1
+ /**
2
+ * Ink Resize Ghost Fix
3
+ *
4
+ * Patches Ink 6's resize handler to prevent ghost artifacts from text reflow.
5
+ *
6
+ * Problem: Ink's resized() clears the dynamic area using eraseLines(N) where N
7
+ * is the string line count. But when the terminal shrinks, already-painted text
8
+ * reflows to more visual lines than N, so eraseLines(N) doesn't erase enough,
9
+ * leaving "ghost" lines above the new render.
10
+ *
11
+ * Fix: After Ink's standard clear, calculate the extra visual lines caused by
12
+ * reflow at the new width and erase them with cursor-up + erase-to-end-of-screen.
13
+ */
14
+ import { pathToFileURL } from "url";
15
+ import { resolve, dirname } from "path";
16
+ import { fileURLToPath } from "url";
17
+ /**
18
+ * Patches the Ink instance's resize handler to account for visual line reflow.
19
+ * Call this AFTER render() — the Ink instance must exist in the registry.
20
+ */
21
+ export async function patchInkResize() {
22
+ try {
23
+ // Dynamically import Ink's internal instance registry and wrap-ansi
24
+ const __dirname = dirname(fileURLToPath(import.meta.url));
25
+ const instancesPath = pathToFileURL(resolve(__dirname, "..", "..", "..", "node_modules", "ink", "build", "instances.js")).href;
26
+ const [{ default: instances }, { default: wrapAnsi }] = await Promise.all([
27
+ import(/* @vite-ignore */ instancesPath),
28
+ import("wrap-ansi"),
29
+ ]);
30
+ const ink = instances.get(process.stdout);
31
+ if (!ink)
32
+ return;
33
+ // Replace Ink's resized handler with our visual-line-aware version
34
+ ink.resized = () => {
35
+ const currentWidth = ink.getTerminalWidth();
36
+ if (currentWidth < ink.lastTerminalWidth) {
37
+ // Calculate how many visual lines the old output occupies at the NEW width.
38
+ // wrapAnsi handles ANSI escape codes, wide chars (CJK), and emoji correctly.
39
+ const oldOutput = ink.lastOutput || "";
40
+ const stringLineCount = oldOutput === "" ? 0 : oldOutput.split("\n").length;
41
+ let visualLineCount = stringLineCount;
42
+ if (oldOutput && currentWidth > 0) {
43
+ const wrapped = wrapAnsi(oldOutput, currentWidth, { trim: false, hard: true });
44
+ visualLineCount = wrapped === "" ? 0 : wrapped.split("\n").length;
45
+ }
46
+ const extraLines = Math.max(0, visualLineCount - stringLineCount);
47
+ // Standard clear — erases stringLineCount lines, resets log's internal state
48
+ ink.log.clear();
49
+ // Erase additional ghost lines caused by text reflow.
50
+ // After log.clear(), cursor is stringLineCount lines above bottom.
51
+ // Ghost lines are above that — move up extraLines more and erase to end of screen.
52
+ if (extraLines > 0) {
53
+ ink.options.stdout.write(`\x1b[${extraLines}A\x1b[J`);
54
+ }
55
+ ink.lastOutput = "";
56
+ ink.lastOutputToRender = "";
57
+ }
58
+ ink.calculateLayout();
59
+ ink.onRender();
60
+ ink.lastTerminalWidth = currentWidth;
61
+ };
62
+ }
63
+ catch {
64
+ // Silent fail — resize fix is best-effort, app still works without it
65
+ }
66
+ }
@@ -27,6 +27,10 @@ export interface PlanModeState {
27
27
  planContent?: string;
28
28
  startedAt?: Date;
29
29
  }
30
+ export interface PlanApprovalDecision {
31
+ action: "execute" | "edit" | "cancel" | "feedback";
32
+ feedback?: string;
33
+ }
30
34
  export declare const interactiveEvents: EventEmitter<[never]>;
31
35
  export declare function createQuestionRequest(questions: Question[]): QuestionRequest;
32
36
  export declare function getPendingQuestion(): QuestionRequest | undefined;
@@ -40,8 +44,18 @@ export declare function exitPlanMode(): {
40
44
  success: boolean;
41
45
  message: string;
42
46
  };
47
+ /**
48
+ * Wait for user to approve/reject the plan via the UI overlay.
49
+ * Called by the agent loop after exitPlanMode() returns.
50
+ */
51
+ export declare function waitForPlanApproval(): Promise<PlanApprovalDecision>;
43
52
  export declare function isPlanMode(): boolean;
44
53
  export declare function getPlanModeState(): PlanModeState;
54
+ export declare function getPendingPlanApproval(): {
55
+ planContent: string;
56
+ planFile: string;
57
+ } | null;
58
+ export declare function resolvePlanApproval(decision: PlanApprovalDecision): boolean;
45
59
  export declare const INTERACTIVE_TOOL_DEFINITIONS: ({
46
60
  name: string;
47
61
  description: string;
@@ -14,6 +14,8 @@ import { resolve } from "path";
14
14
  const pendingQuestions = new Map();
15
15
  // Plan mode state
16
16
  let planModeState = { active: false };
17
+ // Plan approval state — blocks exit_plan_mode until UI resolves
18
+ let pendingPlanApproval = null;
17
19
  // Event emitter for UI coordination
18
20
  export const interactiveEvents = new EventEmitter();
19
21
  // ============================================================================
@@ -83,8 +85,7 @@ export function exitPlanMode() {
83
85
  }
84
86
  const planFile = planModeState.planFile || ".whale/plan.md";
85
87
  planModeState = { active: false };
86
- interactiveEvents.emit("planModeExited", { planFile });
87
- // Read the plan file to display its content
88
+ // Read the plan file content
88
89
  const fullPath = resolve(process.cwd(), planFile);
89
90
  let planContent = "";
90
91
  if (existsSync(fullPath)) {
@@ -93,6 +94,15 @@ export function exitPlanMode() {
93
94
  }
94
95
  catch { /* ignore read errors */ }
95
96
  }
97
+ // Emit event for UI to show approval overlay
98
+ interactiveEvents.emit("planModeExited", { planFile, planContent });
99
+ // Set up pending approval state so UI can resolve it via resolvePlanApproval()
100
+ pendingPlanApproval = {
101
+ resolve: () => { },
102
+ reject: () => { },
103
+ planContent,
104
+ planFile,
105
+ };
96
106
  if (planContent) {
97
107
  return {
98
108
  success: true,
@@ -104,6 +114,25 @@ export function exitPlanMode() {
104
114
  message: `Plan mode complete. Plan saved to ${planFile}.`,
105
115
  };
106
116
  }
117
+ /**
118
+ * Wait for user to approve/reject the plan via the UI overlay.
119
+ * Called by the agent loop after exitPlanMode() returns.
120
+ */
121
+ export async function waitForPlanApproval() {
122
+ if (!pendingPlanApproval) {
123
+ return { action: "execute" };
124
+ }
125
+ const { planContent, planFile } = pendingPlanApproval;
126
+ const decision = await new Promise((resolvePromise, rejectPromise) => {
127
+ pendingPlanApproval = {
128
+ resolve: resolvePromise,
129
+ reject: rejectPromise,
130
+ planContent,
131
+ planFile,
132
+ };
133
+ });
134
+ return decision;
135
+ }
107
136
  export function isPlanMode() {
108
137
  return planModeState.active;
109
138
  }
@@ -111,6 +140,22 @@ export function getPlanModeState() {
111
140
  return { ...planModeState };
112
141
  }
113
142
  // ============================================================================
143
+ // PLAN APPROVAL — blocking resolution from UI
144
+ // ============================================================================
145
+ export function getPendingPlanApproval() {
146
+ if (!pendingPlanApproval)
147
+ return null;
148
+ return { planContent: pendingPlanApproval.planContent, planFile: pendingPlanApproval.planFile };
149
+ }
150
+ export function resolvePlanApproval(decision) {
151
+ if (!pendingPlanApproval)
152
+ return false;
153
+ const { resolve } = pendingPlanApproval;
154
+ pendingPlanApproval = null;
155
+ resolve(decision);
156
+ return true;
157
+ }
158
+ // ============================================================================
114
159
  // TOOL DEFINITIONS
115
160
  // ============================================================================
116
161
  export const INTERACTIVE_TOOL_DEFINITIONS = [
@@ -12,7 +12,7 @@ import { homedir } from "os";
12
12
  // ============================================================================
13
13
  const DEFAULTS = {
14
14
  cancel_stream: "escape",
15
- toggle_expand: "ctrl+e",
15
+ toggle_expand: "ctrl+o",
16
16
  toggle_thinking: "ctrl+t",
17
17
  exit: "ctrl+c",
18
18
  clear_line: "ctrl+u",
@@ -14,7 +14,7 @@ import { globSearch, grepSearch } from "./tools/search-tools.js";
14
14
  import { runCommand, bashOutput, killShell, listShellsFn } from "./tools/shell-exec.js";
15
15
  import { webFetch, webSearch } from "./tools/web-tools.js";
16
16
  import { tasksTool } from "./tools/task-manager.js";
17
- import { taskTool, teamCreateTool, taskOutput, taskStop, configTool, askUser, lspTool, skillTool } from "./tools/agent-tools.js";
17
+ import { taskTool, teamCreateTool, teamAutoTool, taskOutput, taskStop, configTool, askUser, lspTool, skillTool } from "./tools/agent-tools.js";
18
18
  // ============================================================================
19
19
  // TOOL NAMES
20
20
  // ============================================================================
@@ -36,7 +36,8 @@ export const LOCAL_TOOL_NAMES = new Set([
36
36
  "tasks", // Replaces todo_write — action-based CRUD with IDs, deps
37
37
  "multi_edit", // Multi-edit tool
38
38
  "task", // Subagent tool
39
- "team_create", // Agent team tool
39
+ "team_create", // Agent team tool (explicit tasks)
40
+ "team_auto", // Auto-decompose + parallel team + review
40
41
  // Background process tools
41
42
  "bash_output",
42
43
  "kill_shell",
@@ -421,6 +422,37 @@ export const LOCAL_TOOL_DEFINITIONS = [
421
422
  required: ["name", "teammate_count", "tasks"],
422
423
  },
423
424
  },
425
+ {
426
+ name: "team_auto",
427
+ description: "Auto-decompose a task and run it as a parallel agent team. AI breaks the task into sub-tasks with file ownership, spawns teammates, executes in parallel, and reviews results. Use this for large tasks where you don't want to manually plan the decomposition.",
428
+ input_schema: {
429
+ type: "object",
430
+ properties: {
431
+ task: {
432
+ type: "string",
433
+ description: "The task to decompose and execute (e.g., 'Refactor all components to use TypeScript strict mode')",
434
+ },
435
+ max_teammates: {
436
+ type: "number",
437
+ description: "Maximum number of parallel teammates (default: 4, max: 6)",
438
+ },
439
+ model: {
440
+ type: "string",
441
+ enum: ["sonnet", "opus", "haiku"],
442
+ description: "Model for teammates (default: sonnet)",
443
+ },
444
+ working_directory: {
445
+ type: "string",
446
+ description: "Project directory to work in (default: cwd)",
447
+ },
448
+ review: {
449
+ type: "boolean",
450
+ description: "Run a review pass after completion (default: true)",
451
+ },
452
+ },
453
+ required: ["task"],
454
+ },
455
+ },
424
456
  // ------------------------------------------------------------------
425
457
  // BACKGROUND PROCESS TOOLS
426
458
  // ------------------------------------------------------------------
@@ -682,6 +714,7 @@ export async function executeLocalTool(name, input) {
682
714
  // Agent tools (tools/agent-tools.ts)
683
715
  case "task": return await taskTool(input);
684
716
  case "team_create": return await teamCreateTool(input);
717
+ case "team_auto": return await teamAutoTool(input);
685
718
  case "task_output": return await taskOutput(input);
686
719
  case "task_stop": return taskStop(input);
687
720
  case "config": return configTool(input);