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
@@ -16,35 +16,13 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
16
16
  * - Multi-line input with ⎸ continuation markers
17
17
  */
18
18
  import { useState, useEffect, useRef } from "react";
19
- import { Box, Text, useInput, useStdin } from "ink";
20
- import SelectInput from "ink-select-input";
19
+ import { Box, Text, useStdin } from "ink";
21
20
  import { readFileSync, existsSync, statSync } from "fs";
22
21
  import { basename, extname } from "path";
23
22
  import { colors } from "../shared/Theme.js";
24
23
  import { loadKeybindings, bindingToControlChar } from "../services/keybinding-manager.js";
25
- export const SLASH_COMMANDS = [
26
- { name: "/help", description: "Show available commands" },
27
- { name: "/tools", description: "List all tools" },
28
- { name: "/model", description: "Switch model (Anthropic/Bedrock/Gemini)" },
29
- { name: "/compact", description: "Compress conversation context" },
30
- { name: "/save", description: "Save session to disk" },
31
- { name: "/sessions", description: "List saved sessions" },
32
- { name: "/resume", description: "Resume a saved session" },
33
- { name: "/mcp", description: "Server connection status" },
34
- { name: "/store", description: "Switch active store" },
35
- { name: "/status", description: "Show session info" },
36
- { name: "/agents", description: "List available agent types" },
37
- { name: "/remember", description: "Remember a fact across sessions" },
38
- { name: "/forget", description: "Forget a remembered fact" },
39
- { name: "/memory", description: "List all remembered facts" },
40
- { name: "/mode", description: "Permission mode (default/plan/yolo)" },
41
- { name: "/thinking", description: "Toggle extended thinking" },
42
- { name: "/rewind", description: "Rewind conversation to earlier point" },
43
- { name: "/init", description: "Generate project config (.whale/CLAUDE.md)" },
44
- { name: "/update", description: "Check for updates & install" },
45
- { name: "/clear", description: "Clear conversation" },
46
- { name: "/exit", description: "Exit" },
47
- ];
24
+ import { SlashMenu, SLASH_COMMANDS as _SLASH_COMMANDS, getFilteredSelectableCount, getCommandAtIndex, getCategoryCount, getCategoryCommandCount, getCategoryCommand, getCategoryLabel, getCategoryColor } from "./SlashMenu.js";
25
+ export const SLASH_COMMANDS = _SLASH_COMMANDS;
48
26
  // ── Constants ──
49
27
  const IMAGE_EXTENSIONS = {
50
28
  ".png": "image/png",
@@ -56,9 +34,41 @@ const IMAGE_EXTENSIONS = {
56
34
  const MAX_IMAGE_SIZE = 20 * 1024 * 1024; // 20MB
57
35
  const MAX_DISPLAY_LINES = 8;
58
36
  // ── Helpers ──
59
- function dividerLine() {
60
- const w = (process.stdout.columns || 80) - 2;
61
- return "".repeat(Math.max(20, w));
37
+ const CONTEXT_TYPES = ["product", "customer", "order", "inventory", "media", "document", "location"];
38
+ const CONTEXT_COLORS = {
39
+ product: "blue", customer: "#a855f7", order: "green",
40
+ inventory: "#f97316", media: "#ec4899", document: "cyan", location: "yellow",
41
+ };
42
+ /** Detect if pasted text is a valid WhaleContextItem JSON object */
43
+ function parseWhaleContext(text) {
44
+ const trimmed = text.trim();
45
+ if (!trimmed.startsWith("{") || !trimmed.endsWith("}"))
46
+ return null;
47
+ try {
48
+ const obj = JSON.parse(trimmed);
49
+ if (obj && typeof obj.id === "string" && typeof obj.type === "string" &&
50
+ CONTEXT_TYPES.includes(obj.type) && typeof obj.title === "string" &&
51
+ typeof obj.sourceApp === "string") {
52
+ return obj;
53
+ }
54
+ }
55
+ catch { /* not JSON */ }
56
+ return null;
57
+ }
58
+ /** Format a context attachment into an XML context block for AI prompt */
59
+ function contextToBlock(ctx) {
60
+ const lines = [];
61
+ lines.push(`<context type="${ctx.type}" source="${ctx.sourceApp}">`);
62
+ lines.push(` ${ctx.title}`);
63
+ if (ctx.subtitle)
64
+ lines.push(` ${ctx.subtitle}`);
65
+ if (ctx.data) {
66
+ for (const [key, value] of Object.entries(ctx.data).sort(([a], [b]) => a.localeCompare(b))) {
67
+ lines.push(` ${key}: ${value}`);
68
+ }
69
+ }
70
+ lines.push("</context>");
71
+ return lines.join("\n");
62
72
  }
63
73
  function isImagePath(text) {
64
74
  const p = normalizePath(text);
@@ -124,19 +134,55 @@ function getCursorLineCol(text, cursor) {
124
134
  return { line: lines.length - 1, col: lines[lines.length - 1].length };
125
135
  }
126
136
  // ── Component ──
127
- export function ChatInput({ onSubmit, onCommand, disabled, agentName }) {
137
+ export function ChatInput({ onSubmit, onCommand, disabled, agentName, onInterrupt }) {
128
138
  const { stdin } = useStdin();
129
139
  // Input state — ref for synchronous handler access, state for render
130
140
  const inputRef = useRef({ value: "", cursor: 0 });
131
141
  const [displayValue, setDisplayValue] = useState("");
132
142
  const [displayCursor, setDisplayCursor] = useState(0);
143
+ // Stable refs for callbacks — prevents stale closures in stdin handler
144
+ const onSubmitRef = useRef(onSubmit);
145
+ const onCommandRef = useRef(onCommand);
146
+ const onInterruptRef = useRef(onInterrupt);
147
+ const disabledRef = useRef(disabled);
148
+ useEffect(() => { onSubmitRef.current = onSubmit; }, [onSubmit]);
149
+ useEffect(() => { onCommandRef.current = onCommand; }, [onCommand]);
150
+ useEffect(() => { onInterruptRef.current = onInterrupt; }, [onInterrupt]);
151
+ useEffect(() => { disabledRef.current = disabled; }, [disabled]);
152
+ // Queued message — typed during streaming, auto-submitted when streaming ends
153
+ const [queued, setQueued] = useState(false);
154
+ const queuedRef = useRef(false);
155
+ // Auto-submit queued input when streaming ends (disabled goes from true → false)
156
+ const prevDisabledRef = useRef(disabled);
157
+ useEffect(() => {
158
+ if (prevDisabledRef.current && !disabled && queuedRef.current) {
159
+ // Streaming just ended and we have queued input — submit it
160
+ queuedRef.current = false;
161
+ setQueued(false);
162
+ handleSubmit();
163
+ }
164
+ prevDisabledRef.current = disabled;
165
+ }, [disabled]);
133
166
  // Mode & attachments
134
167
  const [menuMode, setMenuMode] = useState(false);
168
+ const menuModeRef = useRef(false);
135
169
  const [menuFilter, setMenuFilter] = useState("");
170
+ const menuFilterRef = useRef("");
171
+ const [menuIndex, setMenuIndex] = useState(0);
172
+ const menuIndexRef = useRef(0);
136
173
  const [images, setImages] = useState([]);
137
174
  const imagesRef = useRef([]);
138
175
  const [files, setFiles] = useState([]);
139
176
  const filesRef = useRef([]);
177
+ const [contextItems, setContextItems] = useState([]);
178
+ const contextItemsRef = useRef([]);
179
+ // Two-level menu: null = Level 1 (categories), number = Level 2 (drilled into that category)
180
+ const [selectedCategory, setSelectedCategory] = useState(null);
181
+ const selectedCategoryRef = useRef(null);
182
+ function setSelectedCategorySync(val) {
183
+ selectedCategoryRef.current = val;
184
+ setSelectedCategory(val);
185
+ }
140
186
  // Input history (up/down arrow recall)
141
187
  const historyRef = useRef([]);
142
188
  const historyIndexRef = useRef(-1);
@@ -151,7 +197,21 @@ export function ChatInput({ onSubmit, onCommand, disabled, agentName }) {
151
197
  // Sync refs
152
198
  useEffect(() => { imagesRef.current = images; }, [images]);
153
199
  useEffect(() => { filesRef.current = files; }, [files]);
200
+ useEffect(() => { contextItemsRef.current = contextItems; }, [contextItems]);
154
201
  useEffect(() => { pastedTextRef.current = pastedText; }, [pastedText]);
202
+ // ── Sync helpers: update ref THEN state (fixes race conditions) ──
203
+ function setMenuModeSync(val) {
204
+ menuModeRef.current = val;
205
+ setMenuMode(val);
206
+ }
207
+ function setMenuFilterSync(val) {
208
+ menuFilterRef.current = val;
209
+ setMenuFilter(val);
210
+ }
211
+ function setMenuIndexSync(val) {
212
+ menuIndexRef.current = val;
213
+ setMenuIndex(val);
214
+ }
155
215
  // ── Enable bracketed paste mode ──
156
216
  useEffect(() => {
157
217
  process.stdout.write("\x1b[?2004h");
@@ -166,6 +226,15 @@ export function ChatInput({ onSubmit, onCommand, disabled, agentName }) {
166
226
  // ── Process paste content ──
167
227
  function processPaste(text) {
168
228
  const clean = text.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
229
+ // Check for WhaleContext JSON → attach as context chip
230
+ const ctx = parseWhaleContext(clean);
231
+ if (ctx) {
232
+ // Avoid duplicates
233
+ if (!contextItemsRef.current.some(c => c.id === ctx.id)) {
234
+ setContextItems(prev => [...prev, ctx]);
235
+ }
236
+ return;
237
+ }
169
238
  // Check if ALL non-empty lines are image paths → attach them
170
239
  const lines = clean.split("\n").map(l => l.trim()).filter(Boolean);
171
240
  if (lines.length > 0 && lines.every(l => isImagePath(l))) {
@@ -191,7 +260,7 @@ export function ChatInput({ onSubmit, onCommand, disabled, agentName }) {
191
260
  }
192
261
  // Slash command pasted directly (must look like /word, not an absolute path)
193
262
  if (clean.startsWith("/") && !clean.includes("\n") && !clean.includes("/", 1) && inputRef.current.value === "") {
194
- onCommand(clean.trim());
263
+ onCommandRef.current(clean.trim());
195
264
  update("", 0);
196
265
  return;
197
266
  }
@@ -212,22 +281,29 @@ export function ChatInput({ onSubmit, onCommand, disabled, agentName }) {
212
281
  const paste = pastedTextRef.current;
213
282
  const imgs = imagesRef.current;
214
283
  const fls = filesRef.current;
215
- if (!typed && !paste && imgs.length === 0 && fls.length === 0)
284
+ const ctxs = contextItemsRef.current;
285
+ if (!typed && !paste && imgs.length === 0 && fls.length === 0 && ctxs.length === 0)
216
286
  return;
217
- if (typed.startsWith("/") && !paste && imgs.length === 0 && fls.length === 0) {
287
+ // Slash command must start with / and NOT contain a second /
288
+ // (prevents file paths like /Users/whale/file.txt from being misidentified)
289
+ if (typed.startsWith("/") && !typed.includes("/", 1) && !paste && imgs.length === 0 && fls.length === 0 && ctxs.length === 0) {
218
290
  update("", 0);
219
- onCommand(typed);
291
+ onCommandRef.current(typed);
220
292
  return;
221
293
  }
294
+ // Build context blocks prefix
295
+ const contextPrefix = ctxs.length > 0
296
+ ? ctxs.map(c => contextToBlock(c)).join("\n\n") + "\n\n"
297
+ : "";
222
298
  // Build file context prefix
223
299
  const filePrefix = fls.length > 0
224
300
  ? fls.map(f => f.path).join("\n") + "\n\n"
225
301
  : "";
226
- // Combine file prefix + typed text + pasted content
302
+ // Combine context + file prefix + typed text + pasted content
227
303
  const body = paste
228
304
  ? (typed ? `${typed}\n\n${paste}` : paste)
229
- : typed;
230
- const fullText = filePrefix + body;
305
+ : (typed || (ctxs.length > 0 ? "Tell me about this" : ""));
306
+ const fullText = contextPrefix + filePrefix + body;
231
307
  // Record in history
232
308
  if (fullText.trim()) {
233
309
  historyRef.current.push(fullText.trim());
@@ -235,18 +311,294 @@ export function ChatInput({ onSubmit, onCommand, disabled, agentName }) {
235
311
  historyRef.current.shift();
236
312
  }
237
313
  historyIndexRef.current = -1;
238
- onSubmit(fullText, imgs.length > 0 ? imgs : undefined);
314
+ onSubmitRef.current(fullText, imgs.length > 0 ? imgs : undefined);
239
315
  update("", 0);
240
316
  setImages([]);
241
317
  setFiles([]);
318
+ setContextItems([]);
242
319
  setPastedText(null);
243
320
  }
244
- // ── Raw stdin handler ──
321
+ // ── Raw stdin handler (handles both normal input and menu mode) ──
245
322
  useEffect(() => {
246
- if (!stdin || disabled || menuMode)
323
+ if (!stdin)
247
324
  return;
248
325
  const onData = (data) => {
249
326
  const str = data.toString("utf-8");
327
+ // ══════════════════════════════════════════════
328
+ // DISABLED MODE — accept typing, block commands, handle Esc
329
+ // ══════════════════════════════════════════════
330
+ if (disabledRef.current) {
331
+ // Escape → interrupt streaming
332
+ if (str === "\x1b") {
333
+ onInterruptRef.current?.();
334
+ return;
335
+ }
336
+ // Control chars handled by ChatApp (exit, expand, thinking toggle)
337
+ const _kb = loadKeybindings();
338
+ const exitChar = bindingToControlChar(_kb.exit);
339
+ const expandChar = bindingToControlChar(_kb.toggle_expand);
340
+ const thinkingChar = bindingToControlChar(_kb.toggle_thinking);
341
+ if (str === exitChar || str === expandChar || str === thinkingChar)
342
+ return;
343
+ // Arrow keys — cursor movement within buffer while disabled
344
+ if (str.startsWith("\x1b[")) {
345
+ const { value: val, cursor: cur } = inputRef.current;
346
+ if (str === "\x1b[C") { // Right
347
+ update(val, Math.min(cur + 1, val.length));
348
+ }
349
+ else if (str === "\x1b[D") { // Left
350
+ update(val, Math.max(cur - 1, 0));
351
+ }
352
+ else if (str === "\x1b[A" || str === "\x1b[B") { // Up/Down
353
+ const lines = val.split("\n");
354
+ if (lines.length > 1) {
355
+ const { line: curLine, col: curCol } = getCursorLineCol(val, cur);
356
+ const targetLine = str === "\x1b[A"
357
+ ? Math.max(0, curLine - 1)
358
+ : Math.min(lines.length - 1, curLine + 1);
359
+ if (targetLine !== curLine) {
360
+ const targetCol = Math.min(curCol, lines[targetLine].length);
361
+ let newCursor = 0;
362
+ for (let i = 0; i < targetLine; i++)
363
+ newCursor += lines[i].length + 1;
364
+ newCursor += targetCol;
365
+ update(val, newCursor);
366
+ }
367
+ }
368
+ }
369
+ else if (str === "\x1b[H" || str === "\x1b[1~") { // Home
370
+ update(val, 0);
371
+ }
372
+ else if (str === "\x1b[F" || str === "\x1b[4~") { // End
373
+ update(val, val.length);
374
+ }
375
+ return;
376
+ }
377
+ // Bracketed paste while disabled — accept into buffer
378
+ if (str.includes("\x1b[200~")) {
379
+ isPasting.current = true;
380
+ let text = str.replace(/\x1b\[200~/g, "").replace(/\x1b\[201~/g, "");
381
+ pasteBuffer.current += text;
382
+ if (str.includes("\x1b[201~")) {
383
+ isPasting.current = false;
384
+ const paste = pasteBuffer.current;
385
+ pasteBuffer.current = "";
386
+ processPaste(paste);
387
+ }
388
+ return;
389
+ }
390
+ if (isPasting.current) {
391
+ if (str.includes("\x1b[201~")) {
392
+ isPasting.current = false;
393
+ pasteBuffer.current += str.replace(/\x1b\[201~/g, "");
394
+ const paste = pasteBuffer.current;
395
+ pasteBuffer.current = "";
396
+ processPaste(paste);
397
+ }
398
+ else {
399
+ pasteBuffer.current += str;
400
+ }
401
+ return;
402
+ }
403
+ // Enter → queue the current input for auto-submit
404
+ if (str === "\r" || str === "\n") {
405
+ const typed = inputRef.current.value.trim();
406
+ if (typed) {
407
+ queuedRef.current = true;
408
+ setQueued(true);
409
+ }
410
+ return;
411
+ }
412
+ // Backspace
413
+ if (str === "\x7f" || str === "\b") {
414
+ const { value: val, cursor: cur } = inputRef.current;
415
+ if (cur > 0) {
416
+ update(val.slice(0, cur - 1) + val.slice(cur), cur - 1);
417
+ // Un-queue if editing
418
+ if (queuedRef.current) {
419
+ queuedRef.current = false;
420
+ setQueued(false);
421
+ }
422
+ }
423
+ return;
424
+ }
425
+ // Slash — block menu mode while streaming
426
+ if (str === "/" && inputRef.current.value === "")
427
+ return;
428
+ // Printable char — type into buffer
429
+ if (str.length === 1 && str.charCodeAt(0) >= 0x20) {
430
+ const { value: val, cursor: cur } = inputRef.current;
431
+ update(val.slice(0, cur) + str + val.slice(cur), cur + 1);
432
+ // Un-queue if editing after queuing
433
+ if (queuedRef.current) {
434
+ queuedRef.current = false;
435
+ setQueued(false);
436
+ }
437
+ return;
438
+ }
439
+ // Multi-character non-escape (unbracketted paste)
440
+ if (str.length > 1 && !str.startsWith("\x1b")) {
441
+ const codePoints = [...str];
442
+ if (codePoints.length === 1) {
443
+ const { value: val, cursor: cur } = inputRef.current;
444
+ update(val.slice(0, cur) + str + val.slice(cur), cur + str.length);
445
+ }
446
+ else {
447
+ processPaste(str);
448
+ }
449
+ if (queuedRef.current) {
450
+ queuedRef.current = false;
451
+ setQueued(false);
452
+ }
453
+ return;
454
+ }
455
+ return;
456
+ }
457
+ // ══════════════════════════════════════════════
458
+ // MENU MODE — two-level navigation
459
+ // ══════════════════════════════════════════════
460
+ if (menuModeRef.current) {
461
+ const hasFilter = menuFilterRef.current.length > 0;
462
+ const inCategory = selectedCategoryRef.current !== null;
463
+ // Escape or Ctrl+C → dismiss menu entirely
464
+ if (str === "\x1b" || str === "\x03") {
465
+ setMenuModeSync(false);
466
+ setMenuFilterSync("");
467
+ setMenuIndexSync(0);
468
+ setSelectedCategorySync(null);
469
+ update("", 0);
470
+ return;
471
+ }
472
+ // Tab → no-op
473
+ if (str === "\t")
474
+ return;
475
+ // ── Filter mode (has filter text) ──
476
+ if (hasFilter) {
477
+ if (str === "\r" || str === "\n") {
478
+ const cmd = getCommandAtIndex(menuFilterRef.current, menuIndexRef.current);
479
+ if (cmd) {
480
+ setMenuModeSync(false);
481
+ setMenuFilterSync("");
482
+ setMenuIndexSync(0);
483
+ setSelectedCategorySync(null);
484
+ update("", 0);
485
+ onCommandRef.current(cmd.name);
486
+ }
487
+ return;
488
+ }
489
+ if (str === "\x1b[A") {
490
+ setMenuIndexSync(Math.max(0, menuIndexRef.current - 1));
491
+ return;
492
+ }
493
+ if (str === "\x1b[B") {
494
+ const count = getFilteredSelectableCount(menuFilterRef.current);
495
+ setMenuIndexSync(Math.min(count - 1, menuIndexRef.current + 1));
496
+ return;
497
+ }
498
+ if (str === "\x7f" || str === "\b") {
499
+ const f = menuFilterRef.current;
500
+ if (f.length > 1) {
501
+ setMenuFilterSync(f.slice(0, -1));
502
+ setMenuIndexSync(0);
503
+ }
504
+ else {
505
+ // Clearing filter → back to Level 1
506
+ setMenuFilterSync("");
507
+ setMenuIndexSync(0);
508
+ setSelectedCategorySync(null);
509
+ }
510
+ return;
511
+ }
512
+ if (str.startsWith("\x1b"))
513
+ return;
514
+ if (str.length === 1 && str.charCodeAt(0) >= 0x20) {
515
+ setMenuFilterSync(menuFilterRef.current + str);
516
+ setMenuIndexSync(0);
517
+ return;
518
+ }
519
+ return;
520
+ }
521
+ // ── Level 2: inside a category ──
522
+ if (inCategory) {
523
+ const catIdx = selectedCategoryRef.current;
524
+ if (str === "\r" || str === "\n") {
525
+ const cmd = getCategoryCommand(catIdx, menuIndexRef.current);
526
+ if (cmd) {
527
+ setMenuModeSync(false);
528
+ setMenuFilterSync("");
529
+ setMenuIndexSync(0);
530
+ setSelectedCategorySync(null);
531
+ update("", 0);
532
+ onCommandRef.current(cmd.name);
533
+ }
534
+ return;
535
+ }
536
+ if (str === "\x1b[A") {
537
+ setMenuIndexSync(Math.max(0, menuIndexRef.current - 1));
538
+ return;
539
+ }
540
+ if (str === "\x1b[B") {
541
+ const count = getCategoryCommandCount(catIdx);
542
+ setMenuIndexSync(Math.min(count - 1, menuIndexRef.current + 1));
543
+ return;
544
+ }
545
+ // Left arrow or Backspace → back to Level 1
546
+ if (str === "\x1b[D" || str === "\x7f" || str === "\b") {
547
+ setMenuIndexSync(catIdx); // restore highlight to the category we were in
548
+ setSelectedCategorySync(null);
549
+ return;
550
+ }
551
+ if (str.startsWith("\x1b"))
552
+ return;
553
+ // Printable → switch to filter mode
554
+ if (str.length === 1 && str.charCodeAt(0) >= 0x20) {
555
+ setSelectedCategorySync(null);
556
+ setMenuFilterSync(str);
557
+ setMenuIndexSync(0);
558
+ return;
559
+ }
560
+ return;
561
+ }
562
+ // ── Level 1: category list ──
563
+ if (str === "\r" || str === "\n" || str === "\x1b[C") {
564
+ // Enter or Right → drill into selected category
565
+ const catIdx = menuIndexRef.current;
566
+ if (catIdx >= 0 && catIdx < getCategoryCount()) {
567
+ setSelectedCategorySync(catIdx);
568
+ setMenuIndexSync(0);
569
+ }
570
+ return;
571
+ }
572
+ if (str === "\x1b[A") {
573
+ setMenuIndexSync(Math.max(0, menuIndexRef.current - 1));
574
+ return;
575
+ }
576
+ if (str === "\x1b[B") {
577
+ setMenuIndexSync(Math.min(getCategoryCount() - 1, menuIndexRef.current + 1));
578
+ return;
579
+ }
580
+ if (str === "\x7f" || str === "\b") {
581
+ // Backspace on Level 1 → dismiss menu
582
+ setMenuModeSync(false);
583
+ setMenuFilterSync("");
584
+ setMenuIndexSync(0);
585
+ setSelectedCategorySync(null);
586
+ update("", 0);
587
+ return;
588
+ }
589
+ if (str.startsWith("\x1b"))
590
+ return;
591
+ // Printable → switch to filter mode
592
+ if (str.length === 1 && str.charCodeAt(0) >= 0x20) {
593
+ setMenuFilterSync(str);
594
+ setMenuIndexSync(0);
595
+ return;
596
+ }
597
+ return;
598
+ }
599
+ // ══════════════════════════════════════════════
600
+ // NORMAL INPUT MODE
601
+ // ══════════════════════════════════════════════
250
602
  // ── Bracketed paste detection ──
251
603
  if (str.includes("\x1b[200~")) {
252
604
  isPasting.current = true;
@@ -295,10 +647,13 @@ export function ChatInput({ onSubmit, onCommand, disabled, agentName }) {
295
647
  update(val.slice(0, cur - 1) + val.slice(cur), cur - 1);
296
648
  }
297
649
  else if (val === "") {
298
- // Empty input + backspace → remove paste chip, then files, then images
650
+ // Empty input + backspace → remove paste chip, then context, then files, then images
299
651
  if (pastedTextRef.current) {
300
652
  setPastedText(null);
301
653
  }
654
+ else if (contextItemsRef.current.length > 0) {
655
+ setContextItems(prev => prev.slice(0, -1));
656
+ }
302
657
  else if (filesRef.current.length > 0) {
303
658
  setFiles(prev => prev.slice(0, -1));
304
659
  }
@@ -429,8 +784,9 @@ export function ChatInput({ onSubmit, onCommand, disabled, agentName }) {
429
784
  const { value: val, cursor: cur } = inputRef.current;
430
785
  // Slash menu trigger
431
786
  if (str === "/" && val === "") {
432
- setMenuMode(true);
433
- setMenuFilter("");
787
+ setMenuModeSync(true);
788
+ setMenuFilterSync("");
789
+ setMenuIndexSync(0);
434
790
  return;
435
791
  }
436
792
  update(val.slice(0, cur) + str + val.slice(cur), cur + 1);
@@ -438,56 +794,44 @@ export function ChatInput({ onSubmit, onCommand, disabled, agentName }) {
438
794
  };
439
795
  stdin.on("data", onData);
440
796
  return () => { stdin.off("data", onData); };
441
- }, [stdin, disabled, menuMode]); // eslint-disable-line react-hooks/exhaustive-deps
442
- // ── Menu input — filter + dismiss (uses Ink's useInput for SelectInput compatibility) ──
443
- useInput((input, key) => {
444
- if (!menuMode || disabled)
445
- return;
446
- if (key.escape) {
447
- setMenuMode(false);
448
- setMenuFilter("");
449
- return;
450
- }
451
- if (key.backspace || key.delete) {
452
- setMenuFilter(prev => {
453
- if (prev.length > 0)
454
- return prev.slice(0, -1);
455
- setMenuMode(false);
456
- return "";
457
- });
458
- return;
459
- }
460
- // Printable character → append to filter
461
- if (input && !key.ctrl && !key.meta && !key.return && !key.tab) {
462
- setMenuFilter(prev => prev + input);
463
- }
464
- }, { isActive: menuMode });
465
- const handleMenuSelect = (item) => {
466
- setMenuMode(false);
467
- setMenuFilter("");
468
- update("", 0);
469
- onCommand(item.value);
470
- };
797
+ }, [stdin]); // eslint-disable-line react-hooks/exhaustive-deps
471
798
  // ── Render ──
472
- const divider = dividerLine();
473
- // Disabled — minimal divider
799
+ // Disabled dimmed but visible input (accept typing to queue)
474
800
  if (disabled) {
475
- return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { children: " " }), _jsx(Text, { color: colors.separator, children: divider })] }));
801
+ const disabledLines = displayValue.split("\n");
802
+ const { line: dCursorLine, col: dCursorCol } = displayValue
803
+ ? getCursorLineCol(displayValue, displayCursor)
804
+ : { line: 0, col: 0 };
805
+ return (_jsx(Box, { flexDirection: "column", children: disabledLines.length <= 1 ? (_jsxs(Box, { children: [_jsx(Text, { color: colors.quaternary, bold: true, children: "❯ " }), !displayValue ? (_jsx(Text, { color: colors.quaternary, wrap: "truncate", children: queued ? "queued — will send when done" : "type to queue a message..." })) : (_jsxs(Box, { children: [_jsxs(Text, { color: colors.dim, children: [displayValue.slice(0, displayCursor), _jsx(Text, { inverse: true, children: displayCursor < displayValue.length ? displayValue[displayCursor] : " " }), displayCursor < displayValue.length ? displayValue.slice(displayCursor + 1) : ""] }), queued && _jsx(Text, { color: colors.warning, children: " [queued]" })] }))] })) : (
806
+ /* Multi-line disabled */
807
+ _jsxs(Box, { flexDirection: "column", children: [disabledLines.map((line, i) => {
808
+ const isCursorOnLine = i === dCursorLine;
809
+ return (_jsxs(Box, { children: [_jsx(Text, { color: colors.quaternary, bold: true, children: i === 0 ? "❯ " : "⎸ " }), isCursorOnLine ? (_jsxs(Text, { color: colors.dim, children: [line.slice(0, dCursorCol), _jsx(Text, { inverse: true, children: dCursorCol < line.length ? line[dCursorCol] : " " }), dCursorCol < line.length ? line.slice(dCursorCol + 1) : ""] })) : (_jsx(Text, { color: colors.dim, children: line }))] }, i));
810
+ }), queued && (_jsx(Box, { marginLeft: 2, children: _jsx(Text, { color: colors.warning, children: "[queued]" }) }))] })) }));
476
811
  }
477
- // Slash command menu
812
+ // Slash command menu — two-level categorized via SlashMenu component
478
813
  if (menuMode) {
479
- const q = menuFilter.toLowerCase();
480
- const filtered = q
481
- ? SLASH_COMMANDS.filter(c => c.name.includes(q) || c.description.toLowerCase().includes(q))
482
- : SLASH_COMMANDS;
483
- const items = filtered.map((c) => ({
484
- label: c.name,
485
- value: c.name,
486
- }));
487
- return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { children: " " }), _jsx(Text, { color: colors.separator, children: divider }), _jsxs(Box, { children: [_jsx(Text, { color: colors.brand, bold: true, children: "/ " }), menuFilter ? (_jsxs(Text, { children: [menuFilter, _jsx(Text, { inverse: true, children: " " })] })) : (_jsxs(Text, { children: [_jsx(Text, { inverse: true, children: " " }), _jsx(Text, { color: colors.dim, children: " type to filter" })] }))] }), items.length > 0 ? (_jsx(SelectInput, { items: items, onSelect: handleMenuSelect, indicatorComponent: ({ isSelected = false }) => (_jsxs(Text, { color: isSelected ? colors.brand : colors.quaternary, children: [isSelected ? "›" : " ", " "] })), itemComponent: ({ isSelected = false, label = "" }) => {
488
- const cmd = SLASH_COMMANDS.find((c) => c.name === label);
489
- return (_jsxs(Text, { children: [_jsx(Text, { color: isSelected ? colors.brand : colors.secondary, bold: isSelected, children: label }), _jsxs(Text, { color: colors.tertiary, children: [" ", cmd?.description] })] }));
490
- } })) : (_jsx(Text, { color: colors.tertiary, children: " no matching commands" })), _jsx(Text, { color: colors.quaternary, children: " esc to dismiss" })] }));
814
+ // Prompt line varies by level
815
+ let promptLine;
816
+ if (menuFilter) {
817
+ // Filter mode
818
+ promptLine = (_jsxs(Box, { children: [_jsx(Text, { color: colors.brand, bold: true, children: "/ " }), _jsxs(Text, { children: [menuFilter, _jsx(Text, { inverse: true, children: " " })] })] }));
819
+ }
820
+ else if (selectedCategory !== null) {
821
+ // Level 2: show category name
822
+ const catLabel = getCategoryLabel(selectedCategory);
823
+ const catColor = getCategoryColor(selectedCategory);
824
+ promptLine = (_jsxs(Box, { children: [_jsx(Text, { color: colors.brand, bold: true, children: "/ " }), _jsx(Text, { color: catColor, bold: true, children: catLabel }), _jsx(Text, { color: colors.tertiary, children: " > " }), _jsx(Text, { inverse: true, children: " " })] }));
825
+ }
826
+ else {
827
+ // Level 1
828
+ promptLine = (_jsxs(Box, { children: [_jsx(Text, { color: colors.brand, bold: true, children: "/ " }), _jsxs(Text, { children: [_jsx(Text, { inverse: true, children: " " }), _jsx(Text, { color: colors.dim, children: " type to filter" })] })] }));
829
+ }
830
+ // Hint line varies by level
831
+ const hint = selectedCategory !== null && !menuFilter
832
+ ? " \u2190 back esc dismiss"
833
+ : " esc to dismiss";
834
+ return (_jsxs(Box, { flexDirection: "column", children: [promptLine, _jsx(SlashMenu, { filter: menuFilter, selectedIndex: menuIndex, selectedCategory: selectedCategory }), _jsx(Text, { color: colors.quaternary, children: hint })] }));
491
835
  }
492
836
  // ── Normal input rendering ──
493
837
  const lines = displayValue.split("\n");
@@ -499,7 +843,7 @@ export function ChatInput({ onSubmit, onCommand, disabled, agentName }) {
499
843
  const visibleLines = isTruncated
500
844
  ? [...lines.slice(0, MAX_DISPLAY_LINES - 1), `… ${lines.length - MAX_DISPLAY_LINES + 1} more lines`]
501
845
  : lines;
502
- return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { children: " " }), (images.length > 0 || files.length > 0 || pastedText) && (_jsxs(Box, { children: [_jsx(Text, { children: " " }), images.map((img, i) => (_jsxs(Text, { children: [_jsx(Text, { color: colors.indigo, children: "[" }), _jsx(Text, { color: colors.secondary, children: img.name }), _jsx(Text, { color: colors.indigo, children: "]" }), _jsx(Text, { children: " " })] }, `img-${i}`))), files.map((f, i) => (_jsxs(Text, { children: [_jsx(Text, { color: colors.purple, children: "[" }), _jsx(Text, { color: colors.secondary, children: f.name }), _jsx(Text, { color: colors.purple, children: "]" }), _jsx(Text, { children: " " })] }, `file-${i}`))), pastedText && (_jsxs(Text, { children: [_jsx(Text, { color: colors.indigo, children: "[" }), _jsxs(Text, { color: colors.secondary, children: ["pasted ", pastedText.split("\n").length, " lines"] }), _jsx(Text, { color: colors.indigo, children: "]" })] }))] })), _jsx(Text, { color: colors.separator, children: divider }), _jsx(Text, { children: " " }), lines.length <= 1 ? (_jsxs(Box, { children: [_jsx(Text, { color: colors.brand, bold: true, children: "❯ " }), !displayValue ? (_jsxs(Text, { children: [_jsx(Text, { inverse: true, children: " " }), _jsx(Text, { color: colors.dim, children: `Message ${agentName || "whale"}, or type / for commands` })] })) : (_jsxs(Text, { children: [displayValue.slice(0, displayCursor), _jsx(Text, { inverse: true, children: displayCursor < displayValue.length ? displayValue[displayCursor] : " " }), displayCursor < displayValue.length ? displayValue.slice(displayCursor + 1) : ""] }))] })) : (
846
+ return (_jsxs(Box, { flexDirection: "column", children: [(images.length > 0 || files.length > 0 || contextItems.length > 0 || pastedText) && (_jsxs(Box, { children: [_jsx(Text, { children: " " }), contextItems.map((ctx, i) => (_jsxs(Text, { children: [_jsx(Text, { color: CONTEXT_COLORS[ctx.type] || "cyan", children: "[" }), _jsxs(Text, { color: colors.secondary, children: [ctx.type, ": ", ctx.title] }), _jsx(Text, { color: CONTEXT_COLORS[ctx.type] || "cyan", children: "]" }), _jsx(Text, { children: " " })] }, `ctx-${i}`))), images.map((img, i) => (_jsxs(Text, { children: [_jsx(Text, { color: colors.indigo, children: "[" }), _jsx(Text, { color: colors.secondary, children: img.name }), _jsx(Text, { color: colors.indigo, children: "]" }), _jsx(Text, { children: " " })] }, `img-${i}`))), files.map((f, i) => (_jsxs(Text, { children: [_jsx(Text, { color: colors.purple, children: "[" }), _jsx(Text, { color: colors.secondary, children: f.name }), _jsx(Text, { color: colors.purple, children: "]" }), _jsx(Text, { children: " " })] }, `file-${i}`))), pastedText && (_jsxs(Text, { children: [_jsx(Text, { color: colors.indigo, children: "[" }), _jsxs(Text, { color: colors.secondary, children: ["pasted ", pastedText.split("\n").length, " lines"] }), _jsx(Text, { color: colors.indigo, children: "]" })] }))] })), lines.length <= 1 ? (_jsxs(Box, { children: [_jsx(Text, { color: colors.brand, bold: true, children: "❯ " }), !displayValue ? (_jsxs(Text, { wrap: "truncate", children: [_jsx(Text, { inverse: true, children: " " }), _jsx(Text, { color: colors.dim, children: `Message ${agentName || "whale"}, or type / for commands` })] })) : (_jsxs(Text, { children: [displayValue.slice(0, displayCursor), _jsx(Text, { inverse: true, children: displayCursor < displayValue.length ? displayValue[displayCursor] : " " }), displayCursor < displayValue.length ? displayValue.slice(displayCursor + 1) : ""] }))] })) : (
503
847
  /* Multi-line input */
504
848
  _jsx(Box, { flexDirection: "column", children: visibleLines.map((line, i) => {
505
849
  const isRealLine = !isTruncated || i < MAX_DISPLAY_LINES - 1;