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.
- package/bin/swagmanager-mcp.js +51 -0
- package/dist/cli/app.js +30 -2
- package/dist/cli/chat/ChatApp.d.ts +4 -4
- package/dist/cli/chat/ChatApp.js +114 -44
- package/dist/cli/chat/ChatInput.d.ts +13 -6
- package/dist/cli/chat/ChatInput.js +433 -89
- package/dist/cli/chat/MemoryManager.d.ts +15 -0
- package/dist/cli/chat/MemoryManager.js +61 -0
- package/dist/cli/chat/MessageList.d.ts +8 -0
- package/dist/cli/chat/MessageList.js +1 -1
- package/dist/cli/chat/NodeManager.d.ts +30 -0
- package/dist/cli/chat/NodeManager.js +89 -0
- package/dist/cli/chat/NodeSelector.d.ts +19 -0
- package/dist/cli/chat/NodeSelector.js +37 -0
- package/dist/cli/chat/PlanApproval.d.ts +17 -0
- package/dist/cli/chat/PlanApproval.js +82 -0
- package/dist/cli/chat/SessionManager.d.ts +16 -0
- package/dist/cli/chat/SessionManager.js +43 -0
- package/dist/cli/chat/SlashMenu.d.ts +38 -0
- package/dist/cli/chat/SlashMenu.js +208 -0
- package/dist/cli/chat/StatusBar.d.ts +16 -0
- package/dist/cli/chat/StatusBar.js +22 -0
- package/dist/cli/chat/ThemeSelector.d.ts +14 -0
- package/dist/cli/chat/ThemeSelector.js +29 -0
- package/dist/cli/chat/ToolIndicator.d.ts +8 -0
- package/dist/cli/chat/ToolIndicator.js +33 -9
- package/dist/cli/chat/hooks/useAgentLoop.d.ts +2 -1
- package/dist/cli/chat/hooks/useAgentLoop.js +22 -17
- package/dist/cli/chat/hooks/useSlashCommands.d.ts +19 -0
- package/dist/cli/chat/hooks/useSlashCommands.js +254 -15
- package/dist/cli/commands/config-cmd.js +4 -25
- package/dist/cli/commands/db.d.ts +13 -0
- package/dist/cli/commands/db.js +243 -0
- package/dist/cli/commands/doctor.js +6 -9
- package/dist/cli/commands/mcp.js +1 -20
- package/dist/cli/services/agent-events.d.ts +22 -1
- package/dist/cli/services/agent-events.js +9 -0
- package/dist/cli/services/agent-loop.js +65 -8
- package/dist/cli/services/agent-worker-base.js +21 -6
- package/dist/cli/services/api-retry.d.ts +25 -0
- package/dist/cli/services/api-retry.js +91 -0
- package/dist/cli/services/auth-service.d.ts +1 -1
- package/dist/cli/services/auth-service.js +40 -19
- package/dist/cli/services/background-processes.js +26 -2
- package/dist/cli/services/config-store.d.ts +13 -1
- package/dist/cli/services/config-store.js +116 -13
- package/dist/cli/services/format-server-response.js +12 -6
- package/dist/cli/services/ink-resize-fix.d.ts +18 -0
- package/dist/cli/services/ink-resize-fix.js +66 -0
- package/dist/cli/services/interactive-tools.d.ts +14 -0
- package/dist/cli/services/interactive-tools.js +47 -2
- package/dist/cli/services/keybinding-manager.js +1 -1
- package/dist/cli/services/local-tools.js +35 -2
- package/dist/cli/services/server-tools.js +175 -3
- package/dist/cli/services/subagent.js +7 -6
- package/dist/cli/services/system-prompt.js +5 -3
- package/dist/cli/services/task-decomposer.d.ts +35 -0
- package/dist/cli/services/task-decomposer.js +199 -0
- package/dist/cli/services/team-lead.d.ts +18 -0
- package/dist/cli/services/team-lead.js +80 -0
- package/dist/cli/services/teammate.js +5 -5
- package/dist/cli/services/telemetry.d.ts +8 -2
- package/dist/cli/services/telemetry.js +116 -92
- package/dist/cli/services/tools/agent-tools.d.ts +1 -0
- package/dist/cli/services/tools/agent-tools.js +50 -4
- package/dist/cli/services/tools/file-ops.d.ts +2 -0
- package/dist/cli/services/tools/file-ops.js +85 -19
- package/dist/cli/services/tools/shell-exec.js +22 -12
- package/dist/cli/shared/Theme.d.ts +1 -2
- package/dist/cli/shared/Theme.js +1 -1
- package/dist/cli/shared/WhaleBanner.d.ts +4 -1
- package/dist/cli/shared/WhaleBanner.js +12 -8
- package/dist/cli/shared/markdown.d.ts +5 -4
- package/dist/cli/shared/markdown.js +376 -334
- package/dist/cli/shared/theme-manager.d.ts +27 -0
- package/dist/cli/shared/theme-manager.js +178 -0
- package/dist/cli/shared/theme-presets.d.ts +16 -0
- package/dist/cli/shared/theme-presets.js +265 -0
- package/dist/index.js +0 -51
- package/dist/node/adapters/imessage.d.ts +10 -0
- package/dist/node/adapters/imessage.js +45 -6
- package/dist/node/cli.js +459 -8
- package/dist/node/config.d.ts +17 -0
- package/dist/node/gateway-client.d.ts +55 -0
- package/dist/node/gateway-client.js +201 -0
- package/dist/node/portal/clipboard.d.ts +28 -0
- package/dist/node/portal/clipboard.js +183 -0
- package/dist/node/portal/discovery.d.ts +29 -0
- package/dist/node/portal/discovery.js +61 -0
- package/dist/node/portal/forward.d.ts +30 -0
- package/dist/node/portal/forward.js +90 -0
- package/dist/node/portal/index.d.ts +47 -0
- package/dist/node/portal/index.js +250 -0
- package/dist/node/portal/multiplexer.d.ts +48 -0
- package/dist/node/portal/multiplexer.js +207 -0
- package/dist/node/portal/permissions.d.ts +36 -0
- package/dist/node/portal/permissions.js +131 -0
- package/dist/node/portal/protocol.d.ts +140 -0
- package/dist/node/portal/protocol.js +193 -0
- package/dist/node/portal/screen.d.ts +18 -0
- package/dist/node/portal/screen.js +93 -0
- package/dist/node/portal/session.d.ts +68 -0
- package/dist/node/portal/session.js +127 -0
- package/dist/node/portal/shell.d.ts +26 -0
- package/dist/node/portal/shell.js +142 -0
- package/dist/node/portal/stream.d.ts +43 -0
- package/dist/node/portal/stream.js +90 -0
- package/dist/node/portal/transfer.d.ts +33 -0
- package/dist/node/portal/transfer.js +231 -0
- package/dist/node/portal/ui.d.ts +16 -0
- package/dist/node/portal/ui.js +148 -0
- package/dist/node/remote-desktop/compile-helper.d.ts +13 -0
- package/dist/node/remote-desktop/compile-helper.js +73 -0
- package/dist/node/remote-desktop/index.d.ts +67 -0
- package/dist/node/remote-desktop/index.js +220 -0
- package/dist/node/remote-desktop/protocol.d.ts +96 -0
- package/dist/node/remote-desktop/protocol.js +67 -0
- package/dist/node/runtime.d.ts +8 -1
- package/dist/node/runtime.js +117 -9
- package/dist/server/handlers/__test-utils__/test-db.d.ts +25 -0
- package/dist/server/handlers/__test-utils__/test-db.js +128 -0
- package/dist/server/handlers/api-keys.js +26 -2
- package/dist/server/handlers/browser.d.ts +0 -4
- package/dist/server/handlers/browser.js +0 -46
- package/dist/server/handlers/catalog.js +37 -14
- package/dist/server/handlers/clickhouse.d.ts +10 -0
- package/dist/server/handlers/clickhouse.js +215 -0
- package/dist/server/handlers/comms.d.ts +308 -4
- package/dist/server/handlers/comms.js +444 -11
- package/dist/server/handlers/creations.js +1 -1
- package/dist/server/handlers/crm.d.ts +54 -8
- package/dist/server/handlers/crm.js +353 -68
- package/dist/server/handlers/embeddings.js +3 -3
- package/dist/server/handlers/enrichment.js +39 -55
- package/dist/server/handlers/inventory.js +1 -1
- package/dist/server/handlers/kali.d.ts +9 -1
- package/dist/server/handlers/kali.js +50 -1
- package/dist/server/handlers/media.d.ts +8 -0
- package/dist/server/handlers/media.js +902 -0
- package/dist/server/handlers/meta-ads.js +6 -3
- package/dist/server/handlers/nodes.d.ts +2 -0
- package/dist/server/handlers/nodes.js +331 -40
- package/dist/server/handlers/operations.d.ts +4 -6
- package/dist/server/handlers/operations.js +99 -38
- package/dist/server/handlers/platform.js +224 -107
- package/dist/server/handlers/remove-bg.d.ts +6 -0
- package/dist/server/handlers/remove-bg.js +96 -0
- package/dist/server/handlers/storefront.d.ts +6 -0
- package/dist/server/handlers/storefront.js +477 -0
- package/dist/server/handlers/supply-chain.js +21 -3
- package/dist/server/handlers/workflow-steps.js +87 -31
- package/dist/server/handlers/workflows.js +4 -1
- package/dist/server/index.js +334 -88
- package/dist/server/lib/clickhouse-buffer.d.ts +48 -0
- package/dist/server/lib/clickhouse-buffer.js +175 -0
- package/dist/server/lib/clickhouse-client.d.ts +112 -0
- package/dist/server/lib/clickhouse-client.js +141 -0
- package/dist/server/lib/coa-renderer.d.ts +91 -0
- package/dist/server/lib/coa-renderer.js +411 -0
- package/dist/server/lib/compaction-service.js +46 -1
- package/dist/server/lib/pdf-renderer.d.ts +143 -0
- package/dist/server/lib/pdf-renderer.js +867 -0
- package/dist/server/lib/react-pdf-layout.d.ts +40 -0
- package/dist/server/lib/react-pdf-layout.js +437 -0
- package/dist/server/lib/server-agent-loop.d.ts +2 -0
- package/dist/server/lib/server-agent-loop.js +36 -17
- package/dist/server/lib/server-subagent.d.ts +3 -0
- package/dist/server/lib/server-subagent.js +9 -6
- package/dist/server/lib/supabase-client.js +51 -3
- package/dist/server/lib/template-resolver.js +14 -4
- package/dist/server/lib/utils.js +15 -0
- package/dist/server/local-agent-gateway.d.ts +44 -0
- package/dist/server/local-agent-gateway.js +389 -49
- package/dist/server/providers/anthropic.js +12 -2
- package/dist/server/providers/gemini.js +17 -2
- package/dist/server/proxy-handlers.js +151 -0
- package/dist/server/tool-router.d.ts +2 -2
- package/dist/server/tool-router.js +25 -35
- package/dist/shared/agent-core.d.ts +25 -2
- package/dist/shared/agent-core.js +66 -5
- package/dist/shared/api-client.js +54 -3
- package/dist/shared/sse-parser.d.ts +1 -1
- package/dist/shared/sse-parser.js +5 -2
- package/dist/shared/tool-dispatch.js +15 -1
- package/package.json +16 -10
- package/dist/server/handlers/__test-utils__/mock-supabase.d.ts +0 -11
- 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,
|
|
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
|
-
|
|
26
|
-
|
|
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
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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
|
-
|
|
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
|
-
|
|
284
|
+
const ctxs = contextItemsRef.current;
|
|
285
|
+
if (!typed && !paste && imgs.length === 0 && fls.length === 0 && ctxs.length === 0)
|
|
216
286
|
return;
|
|
217
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
433
|
-
|
|
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
|
|
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
|
-
|
|
473
|
-
// Disabled — minimal divider
|
|
799
|
+
// Disabled — dimmed but visible input (accept typing to queue)
|
|
474
800
|
if (disabled) {
|
|
475
|
-
|
|
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
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
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: [
|
|
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;
|