whale-code 6.4.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.
- package/README.md +95 -0
- package/bin/swag-agent.js +9 -0
- package/bin/swagmanager-mcp.js +321 -0
- package/dist/cli/app.d.ts +26 -0
- package/dist/cli/app.js +64 -0
- package/dist/cli/chat/AgentSelector.d.ts +14 -0
- package/dist/cli/chat/AgentSelector.js +14 -0
- package/dist/cli/chat/ChatApp.d.ts +9 -0
- package/dist/cli/chat/ChatApp.js +267 -0
- package/dist/cli/chat/ChatInput.d.ts +39 -0
- package/dist/cli/chat/ChatInput.js +509 -0
- package/dist/cli/chat/MarkdownText.d.ts +10 -0
- package/dist/cli/chat/MarkdownText.js +20 -0
- package/dist/cli/chat/MessageList.d.ts +37 -0
- package/dist/cli/chat/MessageList.js +80 -0
- package/dist/cli/chat/ModelSelector.d.ts +20 -0
- package/dist/cli/chat/ModelSelector.js +73 -0
- package/dist/cli/chat/RewindViewer.d.ts +26 -0
- package/dist/cli/chat/RewindViewer.js +185 -0
- package/dist/cli/chat/StoreSelector.d.ts +14 -0
- package/dist/cli/chat/StoreSelector.js +24 -0
- package/dist/cli/chat/StreamingText.d.ts +12 -0
- package/dist/cli/chat/StreamingText.js +12 -0
- package/dist/cli/chat/SubagentPanel.d.ts +45 -0
- package/dist/cli/chat/SubagentPanel.js +110 -0
- package/dist/cli/chat/TeamPanel.d.ts +21 -0
- package/dist/cli/chat/TeamPanel.js +42 -0
- package/dist/cli/chat/ToolIndicator.d.ts +25 -0
- package/dist/cli/chat/ToolIndicator.js +436 -0
- package/dist/cli/chat/hooks/useAgentLoop.d.ts +39 -0
- package/dist/cli/chat/hooks/useAgentLoop.js +382 -0
- package/dist/cli/chat/hooks/useSlashCommands.d.ts +37 -0
- package/dist/cli/chat/hooks/useSlashCommands.js +387 -0
- package/dist/cli/commands/config-cmd.d.ts +10 -0
- package/dist/cli/commands/config-cmd.js +99 -0
- package/dist/cli/commands/doctor.d.ts +14 -0
- package/dist/cli/commands/doctor.js +172 -0
- package/dist/cli/commands/init.d.ts +16 -0
- package/dist/cli/commands/init.js +278 -0
- package/dist/cli/commands/mcp.d.ts +12 -0
- package/dist/cli/commands/mcp.js +162 -0
- package/dist/cli/login/LoginApp.d.ts +7 -0
- package/dist/cli/login/LoginApp.js +157 -0
- package/dist/cli/print-mode.d.ts +31 -0
- package/dist/cli/print-mode.js +202 -0
- package/dist/cli/serve-mode.d.ts +37 -0
- package/dist/cli/serve-mode.js +636 -0
- package/dist/cli/services/agent-definitions.d.ts +25 -0
- package/dist/cli/services/agent-definitions.js +91 -0
- package/dist/cli/services/agent-events.d.ts +178 -0
- package/dist/cli/services/agent-events.js +175 -0
- package/dist/cli/services/agent-loop.d.ts +90 -0
- package/dist/cli/services/agent-loop.js +762 -0
- package/dist/cli/services/agent-worker-base.d.ts +97 -0
- package/dist/cli/services/agent-worker-base.js +220 -0
- package/dist/cli/services/auth-service.d.ts +30 -0
- package/dist/cli/services/auth-service.js +160 -0
- package/dist/cli/services/background-processes.d.ts +126 -0
- package/dist/cli/services/background-processes.js +318 -0
- package/dist/cli/services/browser-auth.d.ts +24 -0
- package/dist/cli/services/browser-auth.js +180 -0
- package/dist/cli/services/claude-md-loader.d.ts +16 -0
- package/dist/cli/services/claude-md-loader.js +58 -0
- package/dist/cli/services/config-store.d.ts +47 -0
- package/dist/cli/services/config-store.js +79 -0
- package/dist/cli/services/debug-log.d.ts +10 -0
- package/dist/cli/services/debug-log.js +52 -0
- package/dist/cli/services/error-logger.d.ts +58 -0
- package/dist/cli/services/error-logger.js +269 -0
- package/dist/cli/services/file-history.d.ts +21 -0
- package/dist/cli/services/file-history.js +83 -0
- package/dist/cli/services/format-server-response.d.ts +16 -0
- package/dist/cli/services/format-server-response.js +440 -0
- package/dist/cli/services/git-context.d.ts +11 -0
- package/dist/cli/services/git-context.js +66 -0
- package/dist/cli/services/hooks.d.ts +85 -0
- package/dist/cli/services/hooks.js +258 -0
- package/dist/cli/services/interactive-tools.d.ts +125 -0
- package/dist/cli/services/interactive-tools.js +260 -0
- package/dist/cli/services/keybinding-manager.d.ts +52 -0
- package/dist/cli/services/keybinding-manager.js +115 -0
- package/dist/cli/services/local-tools.d.ts +22 -0
- package/dist/cli/services/local-tools.js +697 -0
- package/dist/cli/services/lsp-manager.d.ts +18 -0
- package/dist/cli/services/lsp-manager.js +717 -0
- package/dist/cli/services/mcp-client.d.ts +48 -0
- package/dist/cli/services/mcp-client.js +157 -0
- package/dist/cli/services/memory-manager.d.ts +16 -0
- package/dist/cli/services/memory-manager.js +57 -0
- package/dist/cli/services/model-manager.d.ts +18 -0
- package/dist/cli/services/model-manager.js +71 -0
- package/dist/cli/services/model-router.d.ts +26 -0
- package/dist/cli/services/model-router.js +149 -0
- package/dist/cli/services/permission-modes.d.ts +13 -0
- package/dist/cli/services/permission-modes.js +43 -0
- package/dist/cli/services/rewind.d.ts +84 -0
- package/dist/cli/services/rewind.js +194 -0
- package/dist/cli/services/ripgrep.d.ts +28 -0
- package/dist/cli/services/ripgrep.js +138 -0
- package/dist/cli/services/sandbox.d.ts +29 -0
- package/dist/cli/services/sandbox.js +97 -0
- package/dist/cli/services/server-tools.d.ts +61 -0
- package/dist/cli/services/server-tools.js +543 -0
- package/dist/cli/services/session-persistence.d.ts +23 -0
- package/dist/cli/services/session-persistence.js +99 -0
- package/dist/cli/services/subagent-worker.d.ts +19 -0
- package/dist/cli/services/subagent-worker.js +41 -0
- package/dist/cli/services/subagent.d.ts +47 -0
- package/dist/cli/services/subagent.js +647 -0
- package/dist/cli/services/system-prompt.d.ts +7 -0
- package/dist/cli/services/system-prompt.js +198 -0
- package/dist/cli/services/team-lead.d.ts +73 -0
- package/dist/cli/services/team-lead.js +512 -0
- package/dist/cli/services/team-state.d.ts +77 -0
- package/dist/cli/services/team-state.js +398 -0
- package/dist/cli/services/teammate.d.ts +31 -0
- package/dist/cli/services/teammate.js +689 -0
- package/dist/cli/services/telemetry.d.ts +61 -0
- package/dist/cli/services/telemetry.js +209 -0
- package/dist/cli/services/tools/agent-tools.d.ts +14 -0
- package/dist/cli/services/tools/agent-tools.js +347 -0
- package/dist/cli/services/tools/file-ops.d.ts +15 -0
- package/dist/cli/services/tools/file-ops.js +487 -0
- package/dist/cli/services/tools/search-tools.d.ts +8 -0
- package/dist/cli/services/tools/search-tools.js +186 -0
- package/dist/cli/services/tools/shell-exec.d.ts +10 -0
- package/dist/cli/services/tools/shell-exec.js +168 -0
- package/dist/cli/services/tools/task-manager.d.ts +28 -0
- package/dist/cli/services/tools/task-manager.js +209 -0
- package/dist/cli/services/tools/web-tools.d.ts +11 -0
- package/dist/cli/services/tools/web-tools.js +395 -0
- package/dist/cli/setup/SetupApp.d.ts +9 -0
- package/dist/cli/setup/SetupApp.js +191 -0
- package/dist/cli/shared/MatrixIntro.d.ts +4 -0
- package/dist/cli/shared/MatrixIntro.js +83 -0
- package/dist/cli/shared/Theme.d.ts +74 -0
- package/dist/cli/shared/Theme.js +127 -0
- package/dist/cli/shared/WhaleBanner.d.ts +10 -0
- package/dist/cli/shared/WhaleBanner.js +12 -0
- package/dist/cli/shared/markdown.d.ts +21 -0
- package/dist/cli/shared/markdown.js +756 -0
- package/dist/cli/status/StatusApp.d.ts +4 -0
- package/dist/cli/status/StatusApp.js +105 -0
- package/dist/cli/stores/StoreApp.d.ts +7 -0
- package/dist/cli/stores/StoreApp.js +81 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.js +538 -0
- package/dist/local-agent/connection.d.ts +48 -0
- package/dist/local-agent/connection.js +332 -0
- package/dist/local-agent/discovery.d.ts +18 -0
- package/dist/local-agent/discovery.js +146 -0
- package/dist/local-agent/executor.d.ts +34 -0
- package/dist/local-agent/executor.js +241 -0
- package/dist/local-agent/index.d.ts +14 -0
- package/dist/local-agent/index.js +198 -0
- package/dist/node/adapters/base.d.ts +35 -0
- package/dist/node/adapters/base.js +10 -0
- package/dist/node/adapters/discord.d.ts +29 -0
- package/dist/node/adapters/discord.js +299 -0
- package/dist/node/adapters/email.d.ts +23 -0
- package/dist/node/adapters/email.js +218 -0
- package/dist/node/adapters/imessage.d.ts +17 -0
- package/dist/node/adapters/imessage.js +118 -0
- package/dist/node/adapters/slack.d.ts +26 -0
- package/dist/node/adapters/slack.js +259 -0
- package/dist/node/adapters/sms.d.ts +23 -0
- package/dist/node/adapters/sms.js +161 -0
- package/dist/node/adapters/telegram.d.ts +17 -0
- package/dist/node/adapters/telegram.js +101 -0
- package/dist/node/adapters/webchat.d.ts +27 -0
- package/dist/node/adapters/webchat.js +160 -0
- package/dist/node/adapters/whatsapp.d.ts +28 -0
- package/dist/node/adapters/whatsapp.js +230 -0
- package/dist/node/cli.d.ts +2 -0
- package/dist/node/cli.js +325 -0
- package/dist/node/config.d.ts +17 -0
- package/dist/node/config.js +31 -0
- package/dist/node/runtime.d.ts +50 -0
- package/dist/node/runtime.js +351 -0
- package/dist/server/handlers/__test-utils__/mock-supabase.d.ts +11 -0
- package/dist/server/handlers/__test-utils__/mock-supabase.js +393 -0
- package/dist/server/handlers/analytics.d.ts +17 -0
- package/dist/server/handlers/analytics.js +266 -0
- package/dist/server/handlers/api-keys.d.ts +6 -0
- package/dist/server/handlers/api-keys.js +221 -0
- package/dist/server/handlers/billing.d.ts +33 -0
- package/dist/server/handlers/billing.js +272 -0
- package/dist/server/handlers/browser.d.ts +10 -0
- package/dist/server/handlers/browser.js +517 -0
- package/dist/server/handlers/catalog.d.ts +99 -0
- package/dist/server/handlers/catalog.js +976 -0
- package/dist/server/handlers/comms.d.ts +254 -0
- package/dist/server/handlers/comms.js +588 -0
- package/dist/server/handlers/creations.d.ts +6 -0
- package/dist/server/handlers/creations.js +479 -0
- package/dist/server/handlers/crm.d.ts +89 -0
- package/dist/server/handlers/crm.js +538 -0
- package/dist/server/handlers/discovery.d.ts +6 -0
- package/dist/server/handlers/discovery.js +288 -0
- package/dist/server/handlers/embeddings.d.ts +92 -0
- package/dist/server/handlers/embeddings.js +197 -0
- package/dist/server/handlers/enrichment.d.ts +8 -0
- package/dist/server/handlers/enrichment.js +768 -0
- package/dist/server/handlers/image-gen.d.ts +6 -0
- package/dist/server/handlers/image-gen.js +409 -0
- package/dist/server/handlers/inventory.d.ts +319 -0
- package/dist/server/handlers/inventory.js +447 -0
- package/dist/server/handlers/kali.d.ts +10 -0
- package/dist/server/handlers/kali.js +210 -0
- package/dist/server/handlers/llm-providers.d.ts +6 -0
- package/dist/server/handlers/llm-providers.js +673 -0
- package/dist/server/handlers/local-agent.d.ts +6 -0
- package/dist/server/handlers/local-agent.js +118 -0
- package/dist/server/handlers/meta-ads.d.ts +111 -0
- package/dist/server/handlers/meta-ads.js +2279 -0
- package/dist/server/handlers/nodes.d.ts +33 -0
- package/dist/server/handlers/nodes.js +699 -0
- package/dist/server/handlers/operations.d.ts +138 -0
- package/dist/server/handlers/operations.js +131 -0
- package/dist/server/handlers/platform.d.ts +23 -0
- package/dist/server/handlers/platform.js +227 -0
- package/dist/server/handlers/supply-chain.d.ts +19 -0
- package/dist/server/handlers/supply-chain.js +327 -0
- package/dist/server/handlers/transcription.d.ts +17 -0
- package/dist/server/handlers/transcription.js +121 -0
- package/dist/server/handlers/video-gen.d.ts +6 -0
- package/dist/server/handlers/video-gen.js +466 -0
- package/dist/server/handlers/voice.d.ts +8 -0
- package/dist/server/handlers/voice.js +1146 -0
- package/dist/server/handlers/workflow-steps.d.ts +86 -0
- package/dist/server/handlers/workflow-steps.js +2349 -0
- package/dist/server/handlers/workflows.d.ts +7 -0
- package/dist/server/handlers/workflows.js +989 -0
- package/dist/server/index.d.ts +1 -0
- package/dist/server/index.js +2427 -0
- package/dist/server/lib/batch-client.d.ts +80 -0
- package/dist/server/lib/batch-client.js +467 -0
- package/dist/server/lib/code-worker-pool.d.ts +31 -0
- package/dist/server/lib/code-worker-pool.js +224 -0
- package/dist/server/lib/code-worker.d.ts +1 -0
- package/dist/server/lib/code-worker.js +188 -0
- package/dist/server/lib/compaction-service.d.ts +32 -0
- package/dist/server/lib/compaction-service.js +162 -0
- package/dist/server/lib/logger.d.ts +19 -0
- package/dist/server/lib/logger.js +46 -0
- package/dist/server/lib/otel.d.ts +38 -0
- package/dist/server/lib/otel.js +126 -0
- package/dist/server/lib/pg-rate-limiter.d.ts +21 -0
- package/dist/server/lib/pg-rate-limiter.js +86 -0
- package/dist/server/lib/prompt-sanitizer.d.ts +37 -0
- package/dist/server/lib/prompt-sanitizer.js +177 -0
- package/dist/server/lib/provider-capabilities.d.ts +85 -0
- package/dist/server/lib/provider-capabilities.js +190 -0
- package/dist/server/lib/provider-failover.d.ts +74 -0
- package/dist/server/lib/provider-failover.js +210 -0
- package/dist/server/lib/rate-limiter.d.ts +39 -0
- package/dist/server/lib/rate-limiter.js +147 -0
- package/dist/server/lib/server-agent-loop.d.ts +107 -0
- package/dist/server/lib/server-agent-loop.js +667 -0
- package/dist/server/lib/server-subagent.d.ts +78 -0
- package/dist/server/lib/server-subagent.js +203 -0
- package/dist/server/lib/session-checkpoint.d.ts +51 -0
- package/dist/server/lib/session-checkpoint.js +145 -0
- package/dist/server/lib/ssrf-guard.d.ts +13 -0
- package/dist/server/lib/ssrf-guard.js +240 -0
- package/dist/server/lib/supabase-client.d.ts +7 -0
- package/dist/server/lib/supabase-client.js +78 -0
- package/dist/server/lib/template-resolver.d.ts +31 -0
- package/dist/server/lib/template-resolver.js +215 -0
- package/dist/server/lib/utils.d.ts +16 -0
- package/dist/server/lib/utils.js +147 -0
- package/dist/server/local-agent-gateway.d.ts +82 -0
- package/dist/server/local-agent-gateway.js +426 -0
- package/dist/server/providers/anthropic.d.ts +20 -0
- package/dist/server/providers/anthropic.js +199 -0
- package/dist/server/providers/bedrock.d.ts +20 -0
- package/dist/server/providers/bedrock.js +194 -0
- package/dist/server/providers/gemini.d.ts +24 -0
- package/dist/server/providers/gemini.js +486 -0
- package/dist/server/providers/openai.d.ts +24 -0
- package/dist/server/providers/openai.js +522 -0
- package/dist/server/providers/registry.d.ts +32 -0
- package/dist/server/providers/registry.js +58 -0
- package/dist/server/providers/shared.d.ts +32 -0
- package/dist/server/providers/shared.js +124 -0
- package/dist/server/providers/types.d.ts +92 -0
- package/dist/server/providers/types.js +12 -0
- package/dist/server/proxy-handlers.d.ts +6 -0
- package/dist/server/proxy-handlers.js +89 -0
- package/dist/server/tool-router.d.ts +149 -0
- package/dist/server/tool-router.js +803 -0
- package/dist/server/validation.d.ts +24 -0
- package/dist/server/validation.js +301 -0
- package/dist/server/worker.d.ts +19 -0
- package/dist/server/worker.js +201 -0
- package/dist/setup.d.ts +8 -0
- package/dist/setup.js +181 -0
- package/dist/shared/agent-core.d.ts +157 -0
- package/dist/shared/agent-core.js +534 -0
- package/dist/shared/anthropic-types.d.ts +105 -0
- package/dist/shared/anthropic-types.js +7 -0
- package/dist/shared/api-client.d.ts +90 -0
- package/dist/shared/api-client.js +379 -0
- package/dist/shared/constants.d.ts +33 -0
- package/dist/shared/constants.js +80 -0
- package/dist/shared/sse-parser.d.ts +26 -0
- package/dist/shared/sse-parser.js +259 -0
- package/dist/shared/tool-dispatch.d.ts +52 -0
- package/dist/shared/tool-dispatch.js +191 -0
- package/dist/shared/types.d.ts +72 -0
- package/dist/shared/types.js +7 -0
- package/dist/updater.d.ts +25 -0
- package/dist/updater.js +140 -0
- package/dist/webchat/widget.d.ts +0 -0
- package/dist/webchat/widget.js +397 -0
- package/package.json +95 -0
- package/src/cli/services/builtin-skills/commit.md +19 -0
- package/src/cli/services/builtin-skills/review-pr.md +21 -0
- package/src/cli/services/builtin-skills/review.md +18 -0
|
@@ -0,0 +1,509 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* ChatInput — Custom input with bracketed paste + image attachments
|
|
4
|
+
*
|
|
5
|
+
* Replaces ink-text-input with raw stdin handling to fix:
|
|
6
|
+
* - Paste losing content on enter (newlines in paste triggered submit)
|
|
7
|
+
* - Multi-line paste mangled/unformatted
|
|
8
|
+
* - No drag-drop image support
|
|
9
|
+
*
|
|
10
|
+
* Features:
|
|
11
|
+
* - Bracketed paste mode — clean multi-line paste
|
|
12
|
+
* - Drag-drop images — detects image file paths, attaches as chips
|
|
13
|
+
* - Image chips above input like Claude Code: [image1.png] [image2.jpg]
|
|
14
|
+
* - Backspace on empty input removes last image
|
|
15
|
+
* - Slash command menu preserved
|
|
16
|
+
* - Multi-line input with ⎸ continuation markers
|
|
17
|
+
*/
|
|
18
|
+
import { useState, useEffect, useRef } from "react";
|
|
19
|
+
import { Box, Text, useInput, useStdin } from "ink";
|
|
20
|
+
import SelectInput from "ink-select-input";
|
|
21
|
+
import { readFileSync, existsSync, statSync } from "fs";
|
|
22
|
+
import { basename, extname } from "path";
|
|
23
|
+
import { colors } from "../shared/Theme.js";
|
|
24
|
+
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
|
+
];
|
|
48
|
+
// ── Constants ──
|
|
49
|
+
const IMAGE_EXTENSIONS = {
|
|
50
|
+
".png": "image/png",
|
|
51
|
+
".jpg": "image/jpeg",
|
|
52
|
+
".jpeg": "image/jpeg",
|
|
53
|
+
".gif": "image/gif",
|
|
54
|
+
".webp": "image/webp",
|
|
55
|
+
};
|
|
56
|
+
const MAX_IMAGE_SIZE = 20 * 1024 * 1024; // 20MB
|
|
57
|
+
const MAX_DISPLAY_LINES = 8;
|
|
58
|
+
// ── Helpers ──
|
|
59
|
+
function dividerLine() {
|
|
60
|
+
const w = (process.stdout.columns || 80) - 2;
|
|
61
|
+
return "─".repeat(Math.max(20, w));
|
|
62
|
+
}
|
|
63
|
+
function isImagePath(text) {
|
|
64
|
+
const p = normalizePath(text);
|
|
65
|
+
if (!p || p.includes("\n"))
|
|
66
|
+
return false;
|
|
67
|
+
const ext = extname(p).toLowerCase();
|
|
68
|
+
return ext in IMAGE_EXTENSIONS && existsSync(p);
|
|
69
|
+
}
|
|
70
|
+
/** Normalize a terminal-pasted path (handles escapes, file:// URLs, quotes) */
|
|
71
|
+
function normalizePath(raw) {
|
|
72
|
+
let p = raw.trim();
|
|
73
|
+
// Strip file:// URL prefix + percent-decode
|
|
74
|
+
if (p.startsWith("file://")) {
|
|
75
|
+
p = decodeURIComponent(p.replace(/^file:\/\//, ""));
|
|
76
|
+
}
|
|
77
|
+
// Strip surrounding quotes
|
|
78
|
+
p = p.replace(/^['"]|['"]$/g, "");
|
|
79
|
+
// Unescape backslash-escaped characters (spaces, parens, etc.)
|
|
80
|
+
p = p.replace(/\\(.)/g, "$1");
|
|
81
|
+
return p;
|
|
82
|
+
}
|
|
83
|
+
function isFilePath(text) {
|
|
84
|
+
const p = normalizePath(text);
|
|
85
|
+
if (!p || p.includes("\n"))
|
|
86
|
+
return false;
|
|
87
|
+
// Skip image paths — handled separately
|
|
88
|
+
const ext = extname(p).toLowerCase();
|
|
89
|
+
if (ext in IMAGE_EXTENSIONS)
|
|
90
|
+
return false;
|
|
91
|
+
try {
|
|
92
|
+
return existsSync(p) && statSync(p).isFile();
|
|
93
|
+
}
|
|
94
|
+
catch {
|
|
95
|
+
return false;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
function loadImage(filePath) {
|
|
99
|
+
try {
|
|
100
|
+
const p = normalizePath(filePath);
|
|
101
|
+
const ext = extname(p).toLowerCase();
|
|
102
|
+
const mediaType = IMAGE_EXTENSIONS[ext];
|
|
103
|
+
if (!mediaType)
|
|
104
|
+
return null;
|
|
105
|
+
const stat = statSync(p);
|
|
106
|
+
if (stat.size > MAX_IMAGE_SIZE)
|
|
107
|
+
return null;
|
|
108
|
+
const data = readFileSync(p);
|
|
109
|
+
return { path: p, name: basename(p), base64: data.toString("base64"), mediaType };
|
|
110
|
+
}
|
|
111
|
+
catch {
|
|
112
|
+
return null;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
function getCursorLineCol(text, cursor) {
|
|
116
|
+
let pos = 0;
|
|
117
|
+
const lines = text.split("\n");
|
|
118
|
+
for (let i = 0; i < lines.length; i++) {
|
|
119
|
+
if (cursor <= pos + lines[i].length) {
|
|
120
|
+
return { line: i, col: cursor - pos };
|
|
121
|
+
}
|
|
122
|
+
pos += lines[i].length + 1;
|
|
123
|
+
}
|
|
124
|
+
return { line: lines.length - 1, col: lines[lines.length - 1].length };
|
|
125
|
+
}
|
|
126
|
+
// ── Component ──
|
|
127
|
+
export function ChatInput({ onSubmit, onCommand, disabled, agentName }) {
|
|
128
|
+
const { stdin } = useStdin();
|
|
129
|
+
// Input state — ref for synchronous handler access, state for render
|
|
130
|
+
const inputRef = useRef({ value: "", cursor: 0 });
|
|
131
|
+
const [displayValue, setDisplayValue] = useState("");
|
|
132
|
+
const [displayCursor, setDisplayCursor] = useState(0);
|
|
133
|
+
// Mode & attachments
|
|
134
|
+
const [menuMode, setMenuMode] = useState(false);
|
|
135
|
+
const [menuFilter, setMenuFilter] = useState("");
|
|
136
|
+
const [images, setImages] = useState([]);
|
|
137
|
+
const imagesRef = useRef([]);
|
|
138
|
+
const [files, setFiles] = useState([]);
|
|
139
|
+
const filesRef = useRef([]);
|
|
140
|
+
// Input history (up/down arrow recall)
|
|
141
|
+
const historyRef = useRef([]);
|
|
142
|
+
const historyIndexRef = useRef(-1);
|
|
143
|
+
const savedInputRef = useRef("");
|
|
144
|
+
const MAX_HISTORY = 50;
|
|
145
|
+
// Paste tracking
|
|
146
|
+
const isPasting = useRef(false);
|
|
147
|
+
const pasteBuffer = useRef("");
|
|
148
|
+
// Multi-line paste — stored separately, displayed as chip
|
|
149
|
+
const [pastedText, setPastedText] = useState(null);
|
|
150
|
+
const pastedTextRef = useRef(null);
|
|
151
|
+
// Sync refs
|
|
152
|
+
useEffect(() => { imagesRef.current = images; }, [images]);
|
|
153
|
+
useEffect(() => { filesRef.current = files; }, [files]);
|
|
154
|
+
useEffect(() => { pastedTextRef.current = pastedText; }, [pastedText]);
|
|
155
|
+
// ── Enable bracketed paste mode ──
|
|
156
|
+
useEffect(() => {
|
|
157
|
+
process.stdout.write("\x1b[?2004h");
|
|
158
|
+
return () => { process.stdout.write("\x1b[?2004l"); };
|
|
159
|
+
}, []);
|
|
160
|
+
// ── Update helper ──
|
|
161
|
+
function update(value, cursor) {
|
|
162
|
+
inputRef.current = { value, cursor };
|
|
163
|
+
setDisplayValue(value);
|
|
164
|
+
setDisplayCursor(cursor);
|
|
165
|
+
}
|
|
166
|
+
// ── Process paste content ──
|
|
167
|
+
function processPaste(text) {
|
|
168
|
+
const clean = text.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
|
|
169
|
+
// Check if ALL non-empty lines are image paths → attach them
|
|
170
|
+
const lines = clean.split("\n").map(l => l.trim()).filter(Boolean);
|
|
171
|
+
if (lines.length > 0 && lines.every(l => isImagePath(l))) {
|
|
172
|
+
const newImages = [];
|
|
173
|
+
for (const line of lines) {
|
|
174
|
+
const img = loadImage(line);
|
|
175
|
+
if (img)
|
|
176
|
+
newImages.push(img);
|
|
177
|
+
}
|
|
178
|
+
if (newImages.length > 0) {
|
|
179
|
+
setImages(prev => [...prev, ...newImages]);
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
// Check for non-image file paths → attach as file chips
|
|
184
|
+
if (lines.length > 0 && lines.length <= 4 && lines.every(l => isFilePath(l))) {
|
|
185
|
+
const newFiles = lines.map(l => {
|
|
186
|
+
const p = normalizePath(l);
|
|
187
|
+
return { path: p, name: basename(p) };
|
|
188
|
+
});
|
|
189
|
+
setFiles(prev => [...prev, ...newFiles]);
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
// Slash command pasted directly (must look like /word, not an absolute path)
|
|
193
|
+
if (clean.startsWith("/") && !clean.includes("\n") && !clean.includes("/", 1) && inputRef.current.value === "") {
|
|
194
|
+
onCommand(clean.trim());
|
|
195
|
+
update("", 0);
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
// Multi-line paste → store as chip, don't spam input
|
|
199
|
+
const pasteLines = clean.split("\n");
|
|
200
|
+
if (pasteLines.length >= 4) {
|
|
201
|
+
setPastedText(clean);
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
// Short paste — insert inline as before
|
|
205
|
+
const { value: val, cursor: cur } = inputRef.current;
|
|
206
|
+
const newValue = val.slice(0, cur) + clean + val.slice(cur);
|
|
207
|
+
update(newValue, cur + clean.length);
|
|
208
|
+
}
|
|
209
|
+
// ── Handle submit ──
|
|
210
|
+
function handleSubmit() {
|
|
211
|
+
const typed = inputRef.current.value.trim();
|
|
212
|
+
const paste = pastedTextRef.current;
|
|
213
|
+
const imgs = imagesRef.current;
|
|
214
|
+
const fls = filesRef.current;
|
|
215
|
+
if (!typed && !paste && imgs.length === 0 && fls.length === 0)
|
|
216
|
+
return;
|
|
217
|
+
if (typed.startsWith("/") && !paste && imgs.length === 0 && fls.length === 0) {
|
|
218
|
+
update("", 0);
|
|
219
|
+
onCommand(typed);
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
// Build file context prefix
|
|
223
|
+
const filePrefix = fls.length > 0
|
|
224
|
+
? fls.map(f => f.path).join("\n") + "\n\n"
|
|
225
|
+
: "";
|
|
226
|
+
// Combine file prefix + typed text + pasted content
|
|
227
|
+
const body = paste
|
|
228
|
+
? (typed ? `${typed}\n\n${paste}` : paste)
|
|
229
|
+
: typed;
|
|
230
|
+
const fullText = filePrefix + body;
|
|
231
|
+
// Record in history
|
|
232
|
+
if (fullText.trim()) {
|
|
233
|
+
historyRef.current.push(fullText.trim());
|
|
234
|
+
if (historyRef.current.length > MAX_HISTORY)
|
|
235
|
+
historyRef.current.shift();
|
|
236
|
+
}
|
|
237
|
+
historyIndexRef.current = -1;
|
|
238
|
+
onSubmit(fullText, imgs.length > 0 ? imgs : undefined);
|
|
239
|
+
update("", 0);
|
|
240
|
+
setImages([]);
|
|
241
|
+
setFiles([]);
|
|
242
|
+
setPastedText(null);
|
|
243
|
+
}
|
|
244
|
+
// ── Raw stdin handler ──
|
|
245
|
+
useEffect(() => {
|
|
246
|
+
if (!stdin || disabled || menuMode)
|
|
247
|
+
return;
|
|
248
|
+
const onData = (data) => {
|
|
249
|
+
const str = data.toString("utf-8");
|
|
250
|
+
// ── Bracketed paste detection ──
|
|
251
|
+
if (str.includes("\x1b[200~")) {
|
|
252
|
+
isPasting.current = true;
|
|
253
|
+
let text = str.replace(/\x1b\[200~/g, "").replace(/\x1b\[201~/g, "");
|
|
254
|
+
pasteBuffer.current += text;
|
|
255
|
+
if (str.includes("\x1b[201~")) {
|
|
256
|
+
isPasting.current = false;
|
|
257
|
+
const paste = pasteBuffer.current;
|
|
258
|
+
pasteBuffer.current = "";
|
|
259
|
+
processPaste(paste);
|
|
260
|
+
}
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
263
|
+
if (isPasting.current) {
|
|
264
|
+
if (str.includes("\x1b[201~")) {
|
|
265
|
+
isPasting.current = false;
|
|
266
|
+
pasteBuffer.current += str.replace(/\x1b\[201~/g, "");
|
|
267
|
+
const paste = pasteBuffer.current;
|
|
268
|
+
pasteBuffer.current = "";
|
|
269
|
+
processPaste(paste);
|
|
270
|
+
}
|
|
271
|
+
else {
|
|
272
|
+
pasteBuffer.current += str;
|
|
273
|
+
}
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
276
|
+
// ── Control chars handled by ChatApp (keybinding-aware) ──
|
|
277
|
+
const _kb = loadKeybindings();
|
|
278
|
+
const exitChar = bindingToControlChar(_kb.exit);
|
|
279
|
+
const expandChar = bindingToControlChar(_kb.toggle_expand);
|
|
280
|
+
const thinkingChar = bindingToControlChar(_kb.toggle_thinking);
|
|
281
|
+
if (str === exitChar || str === expandChar || str === thinkingChar)
|
|
282
|
+
return;
|
|
283
|
+
// ── Enter ──
|
|
284
|
+
if (str === "\r" || str === "\n") {
|
|
285
|
+
handleSubmit();
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
// ── Tab ── (no-op in input)
|
|
289
|
+
if (str === "\t")
|
|
290
|
+
return;
|
|
291
|
+
// ── Backspace ──
|
|
292
|
+
if (str === "\x7f" || str === "\b") {
|
|
293
|
+
const { value: val, cursor: cur } = inputRef.current;
|
|
294
|
+
if (cur > 0) {
|
|
295
|
+
update(val.slice(0, cur - 1) + val.slice(cur), cur - 1);
|
|
296
|
+
}
|
|
297
|
+
else if (val === "") {
|
|
298
|
+
// Empty input + backspace → remove paste chip, then files, then images
|
|
299
|
+
if (pastedTextRef.current) {
|
|
300
|
+
setPastedText(null);
|
|
301
|
+
}
|
|
302
|
+
else if (filesRef.current.length > 0) {
|
|
303
|
+
setFiles(prev => prev.slice(0, -1));
|
|
304
|
+
}
|
|
305
|
+
else if (imagesRef.current.length > 0) {
|
|
306
|
+
setImages(prev => prev.slice(0, -1));
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
311
|
+
// ── Clear line (default: Ctrl+U) ──
|
|
312
|
+
if (str === bindingToControlChar(_kb.clear_line)) {
|
|
313
|
+
update("", 0);
|
|
314
|
+
setImages([]);
|
|
315
|
+
setFiles([]);
|
|
316
|
+
setPastedText(null);
|
|
317
|
+
return;
|
|
318
|
+
}
|
|
319
|
+
// ── Delete word (default: Ctrl+W) ──
|
|
320
|
+
if (str === bindingToControlChar(_kb.delete_word)) {
|
|
321
|
+
const { value: val, cursor: cur } = inputRef.current;
|
|
322
|
+
const before = val.slice(0, cur);
|
|
323
|
+
const match = before.match(/\S+\s*$/);
|
|
324
|
+
if (match) {
|
|
325
|
+
const len = match[0].length;
|
|
326
|
+
update(val.slice(0, cur - len) + val.slice(cur), cur - len);
|
|
327
|
+
}
|
|
328
|
+
return;
|
|
329
|
+
}
|
|
330
|
+
// ── Home (default: Ctrl+A) ──
|
|
331
|
+
if (str === bindingToControlChar(_kb.home)) {
|
|
332
|
+
update(inputRef.current.value, 0);
|
|
333
|
+
return;
|
|
334
|
+
}
|
|
335
|
+
// ── Escape sequences ──
|
|
336
|
+
if (str.startsWith("\x1b[")) {
|
|
337
|
+
const { value: val, cursor: cur } = inputRef.current;
|
|
338
|
+
if (str === "\x1b[C") { // Right
|
|
339
|
+
update(val, Math.min(cur + 1, val.length));
|
|
340
|
+
}
|
|
341
|
+
else if (str === "\x1b[D") { // Left
|
|
342
|
+
update(val, Math.max(cur - 1, 0));
|
|
343
|
+
}
|
|
344
|
+
else if (str === "\x1b[H" || str === "\x1b[1~") { // Home
|
|
345
|
+
update(val, 0);
|
|
346
|
+
}
|
|
347
|
+
else if (str === "\x1b[F" || str === "\x1b[4~") { // End
|
|
348
|
+
update(val, val.length);
|
|
349
|
+
}
|
|
350
|
+
else if (str === "\x1b[3~") { // Delete
|
|
351
|
+
if (cur < val.length) {
|
|
352
|
+
update(val.slice(0, cur) + val.slice(cur + 1), cur);
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
else if (str === "\x1b[A" || str === "\x1b[B") { // Up/Down
|
|
356
|
+
const lines = val.split("\n");
|
|
357
|
+
if (lines.length > 1) {
|
|
358
|
+
// Multi-line: navigate lines
|
|
359
|
+
const { line: curLine, col: curCol } = getCursorLineCol(val, cur);
|
|
360
|
+
const targetLine = str === "\x1b[A"
|
|
361
|
+
? Math.max(0, curLine - 1)
|
|
362
|
+
: Math.min(lines.length - 1, curLine + 1);
|
|
363
|
+
if (targetLine !== curLine) {
|
|
364
|
+
const targetCol = Math.min(curCol, lines[targetLine].length);
|
|
365
|
+
let newCursor = 0;
|
|
366
|
+
for (let i = 0; i < targetLine; i++)
|
|
367
|
+
newCursor += lines[i].length + 1;
|
|
368
|
+
newCursor += targetCol;
|
|
369
|
+
update(val, newCursor);
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
else if (str === "\x1b[A") {
|
|
373
|
+
// Up arrow — history recall
|
|
374
|
+
if (historyRef.current.length === 0) { /* no history */ }
|
|
375
|
+
else {
|
|
376
|
+
if (historyIndexRef.current === -1)
|
|
377
|
+
savedInputRef.current = val;
|
|
378
|
+
const newIdx = Math.min(historyIndexRef.current + 1, historyRef.current.length - 1);
|
|
379
|
+
historyIndexRef.current = newIdx;
|
|
380
|
+
const hist = historyRef.current[historyRef.current.length - 1 - newIdx];
|
|
381
|
+
update(hist, hist.length);
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
else {
|
|
385
|
+
// Down arrow — forward in history
|
|
386
|
+
if (historyIndexRef.current <= -1) { /* already at current */ }
|
|
387
|
+
else {
|
|
388
|
+
const newIdx = historyIndexRef.current - 1;
|
|
389
|
+
historyIndexRef.current = newIdx;
|
|
390
|
+
if (newIdx < 0) {
|
|
391
|
+
update(savedInputRef.current, savedInputRef.current.length);
|
|
392
|
+
}
|
|
393
|
+
else {
|
|
394
|
+
const hist = historyRef.current[historyRef.current.length - 1 - newIdx];
|
|
395
|
+
update(hist, hist.length);
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
// Ignore unrecognized escape sequences
|
|
401
|
+
return;
|
|
402
|
+
}
|
|
403
|
+
// ── Standalone escape — clear input if non-empty ──
|
|
404
|
+
if (str === "\x1b") {
|
|
405
|
+
const { value } = inputRef.current;
|
|
406
|
+
if (value || filesRef.current.length > 0) {
|
|
407
|
+
update("", 0);
|
|
408
|
+
setImages([]);
|
|
409
|
+
setFiles([]);
|
|
410
|
+
setPastedText(null);
|
|
411
|
+
}
|
|
412
|
+
return;
|
|
413
|
+
}
|
|
414
|
+
// ── Multi-character non-escape = paste without brackets ──
|
|
415
|
+
if (str.length > 1 && !str.startsWith("\x1b")) {
|
|
416
|
+
const codePoints = [...str];
|
|
417
|
+
if (codePoints.length === 1) {
|
|
418
|
+
// Single code point (emoji etc.)
|
|
419
|
+
const { value: val, cursor: cur } = inputRef.current;
|
|
420
|
+
update(val.slice(0, cur) + str + val.slice(cur), cur + str.length);
|
|
421
|
+
}
|
|
422
|
+
else {
|
|
423
|
+
processPaste(str);
|
|
424
|
+
}
|
|
425
|
+
return;
|
|
426
|
+
}
|
|
427
|
+
// ── Single printable character ──
|
|
428
|
+
if (str.length === 1 && str.charCodeAt(0) >= 0x20) {
|
|
429
|
+
const { value: val, cursor: cur } = inputRef.current;
|
|
430
|
+
// Slash menu trigger
|
|
431
|
+
if (str === "/" && val === "") {
|
|
432
|
+
setMenuMode(true);
|
|
433
|
+
setMenuFilter("");
|
|
434
|
+
return;
|
|
435
|
+
}
|
|
436
|
+
update(val.slice(0, cur) + str + val.slice(cur), cur + 1);
|
|
437
|
+
}
|
|
438
|
+
};
|
|
439
|
+
stdin.on("data", onData);
|
|
440
|
+
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
|
+
};
|
|
471
|
+
// ── Render ──
|
|
472
|
+
const divider = dividerLine();
|
|
473
|
+
// Disabled — minimal divider
|
|
474
|
+
if (disabled) {
|
|
475
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { children: " " }), _jsx(Text, { color: colors.separator, children: divider })] }));
|
|
476
|
+
}
|
|
477
|
+
// Slash command menu
|
|
478
|
+
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" })] }));
|
|
491
|
+
}
|
|
492
|
+
// ── Normal input rendering ──
|
|
493
|
+
const lines = displayValue.split("\n");
|
|
494
|
+
const { line: cursorLine, col: cursorCol } = displayValue
|
|
495
|
+
? getCursorLineCol(displayValue, displayCursor)
|
|
496
|
+
: { line: 0, col: 0 };
|
|
497
|
+
// Truncate display for very long pastes
|
|
498
|
+
const isTruncated = lines.length > MAX_DISPLAY_LINES;
|
|
499
|
+
const visibleLines = isTruncated
|
|
500
|
+
? [...lines.slice(0, MAX_DISPLAY_LINES - 1), `… ${lines.length - MAX_DISPLAY_LINES + 1} more lines`]
|
|
501
|
+
: 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) : ""] }))] })) : (
|
|
503
|
+
/* Multi-line input */
|
|
504
|
+
_jsx(Box, { flexDirection: "column", children: visibleLines.map((line, i) => {
|
|
505
|
+
const isRealLine = !isTruncated || i < MAX_DISPLAY_LINES - 1;
|
|
506
|
+
const isCursorOnLine = isRealLine && i === cursorLine;
|
|
507
|
+
return (_jsxs(Box, { children: [_jsx(Text, { color: colors.brand, bold: true, children: i === 0 ? "❯ " : "⎸ " }), isCursorOnLine ? (_jsxs(Text, { children: [line.slice(0, cursorCol), _jsx(Text, { inverse: true, children: cursorCol < line.length ? line[cursorCol] : " " }), cursorCol < line.length ? line.slice(cursorCol + 1) : ""] })) : (_jsx(Text, { color: isRealLine ? undefined : colors.tertiary, children: line }))] }, i));
|
|
508
|
+
}) }))] }));
|
|
509
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MarkdownText — renders markdown as ANSI-styled terminal output
|
|
3
|
+
*/
|
|
4
|
+
interface MarkdownTextProps {
|
|
5
|
+
text: string;
|
|
6
|
+
/** When true, closes incomplete fences for safe mid-stream display. Default: false. */
|
|
7
|
+
streaming?: boolean;
|
|
8
|
+
}
|
|
9
|
+
export declare function MarkdownText({ text, streaming }: MarkdownTextProps): import("react/jsx-runtime").JSX.Element;
|
|
10
|
+
export {};
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* MarkdownText — renders markdown as ANSI-styled terminal output
|
|
4
|
+
*/
|
|
5
|
+
import { useMemo } from "react";
|
|
6
|
+
import { Text } from "ink";
|
|
7
|
+
import { renderMarkdown } from "../shared/markdown.js";
|
|
8
|
+
export function MarkdownText({ text, streaming = false }) {
|
|
9
|
+
const rendered = useMemo(() => {
|
|
10
|
+
if (!text)
|
|
11
|
+
return "";
|
|
12
|
+
try {
|
|
13
|
+
return renderMarkdown(text, streaming);
|
|
14
|
+
}
|
|
15
|
+
catch {
|
|
16
|
+
return text;
|
|
17
|
+
}
|
|
18
|
+
}, [text, streaming]);
|
|
19
|
+
return _jsx(Text, { children: rendered });
|
|
20
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MessageList — types and CompletedMessage component
|
|
3
|
+
*
|
|
4
|
+
* CompletedMessage is React.memo'd to prevent re-renders during streaming.
|
|
5
|
+
* Telemetry footer: tokens, cost estimate, tool count.
|
|
6
|
+
* Minimal Box usage — Text-first for reliable rendering.
|
|
7
|
+
*/
|
|
8
|
+
import React from "react";
|
|
9
|
+
import { type CompletedSubagentInfo } from "./SubagentPanel.js";
|
|
10
|
+
export interface ToolCall {
|
|
11
|
+
name: string;
|
|
12
|
+
status: "running" | "success" | "error";
|
|
13
|
+
result?: string;
|
|
14
|
+
input?: Record<string, unknown>;
|
|
15
|
+
durationMs?: number;
|
|
16
|
+
}
|
|
17
|
+
export interface ChatMessage {
|
|
18
|
+
role: "user" | "assistant";
|
|
19
|
+
text: string;
|
|
20
|
+
images?: string[];
|
|
21
|
+
toolCalls?: ToolCall[];
|
|
22
|
+
completedSubagents?: CompletedSubagentInfo[];
|
|
23
|
+
usage?: {
|
|
24
|
+
input_tokens: number;
|
|
25
|
+
output_tokens: number;
|
|
26
|
+
thinking_tokens?: number;
|
|
27
|
+
model?: string;
|
|
28
|
+
costUsd?: number;
|
|
29
|
+
cache_read_tokens?: number;
|
|
30
|
+
cache_creation_tokens?: number;
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
export declare const CompletedMessage: React.NamedExoticComponent<{
|
|
34
|
+
msg: ChatMessage;
|
|
35
|
+
index: number;
|
|
36
|
+
toolsExpanded: boolean;
|
|
37
|
+
}>;
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* MessageList — types and CompletedMessage component
|
|
4
|
+
*
|
|
5
|
+
* CompletedMessage is React.memo'd to prevent re-renders during streaming.
|
|
6
|
+
* Telemetry footer: tokens, cost estimate, tool count.
|
|
7
|
+
* Minimal Box usage — Text-first for reliable rendering.
|
|
8
|
+
*/
|
|
9
|
+
import React, { useMemo } from "react";
|
|
10
|
+
import { Box, Text } from "ink";
|
|
11
|
+
import { ToolIndicator } from "./ToolIndicator.js";
|
|
12
|
+
import { MarkdownText } from "./MarkdownText.js";
|
|
13
|
+
import { CompletedSubagentTree } from "./SubagentPanel.js";
|
|
14
|
+
import { colors } from "../shared/Theme.js";
|
|
15
|
+
import { contentWidth } from "../shared/markdown.js";
|
|
16
|
+
import { MODEL_PRICING } from "../../shared/agent-core.js";
|
|
17
|
+
// ============================================================================
|
|
18
|
+
// HELPERS
|
|
19
|
+
// ============================================================================
|
|
20
|
+
function estimateCost(input, output, model, precomputedCost) {
|
|
21
|
+
// Use precomputed cost if available (accurate for all providers)
|
|
22
|
+
let cost = precomputedCost;
|
|
23
|
+
if (cost == null) {
|
|
24
|
+
// Fall back to model-specific pricing, then Sonnet as default
|
|
25
|
+
const pricing = (model && MODEL_PRICING[model])
|
|
26
|
+
|| MODEL_PRICING[Object.keys(MODEL_PRICING).find(k => model?.startsWith(k)) ?? ""]
|
|
27
|
+
|| MODEL_PRICING["claude-sonnet-4-6"];
|
|
28
|
+
cost = (input * pricing.inputPer1M + output * pricing.outputPer1M) / 1_000_000;
|
|
29
|
+
}
|
|
30
|
+
if (cost < 0.001)
|
|
31
|
+
return "<$0.001";
|
|
32
|
+
if (cost < 0.01)
|
|
33
|
+
return `$${cost.toFixed(4)}`;
|
|
34
|
+
return `$${cost.toFixed(3)}`;
|
|
35
|
+
}
|
|
36
|
+
function formatTokens(n) {
|
|
37
|
+
if (n < 1000)
|
|
38
|
+
return String(n);
|
|
39
|
+
if (n < 10000)
|
|
40
|
+
return `${(n / 1000).toFixed(1)}k`;
|
|
41
|
+
return `${Math.round(n / 1000)}k`;
|
|
42
|
+
}
|
|
43
|
+
function totalToolDuration(toolCalls) {
|
|
44
|
+
return toolCalls.reduce((sum, tc) => sum + (tc.durationMs || 0), 0);
|
|
45
|
+
}
|
|
46
|
+
function formatMs(ms) {
|
|
47
|
+
if (ms < 1000)
|
|
48
|
+
return `${ms}ms`;
|
|
49
|
+
if (ms < 10000)
|
|
50
|
+
return `${(ms / 1000).toFixed(1)}s`;
|
|
51
|
+
return `${Math.round(ms / 1000)}s`;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Group consecutive tool calls with the same name and input into groups.
|
|
55
|
+
* Identical consecutive calls collapse into one entry with count > 1.
|
|
56
|
+
* Caches previous key to avoid redundant JSON.stringify calls.
|
|
57
|
+
*/
|
|
58
|
+
function groupConsecutiveTools(toolCalls) {
|
|
59
|
+
const groups = [];
|
|
60
|
+
let prevKey = "";
|
|
61
|
+
for (const tc of toolCalls) {
|
|
62
|
+
const key = tc.name + "|" + tc.status + "|" + JSON.stringify(tc.input || {});
|
|
63
|
+
if (key === prevKey && groups.length > 0) {
|
|
64
|
+
groups[groups.length - 1].count++;
|
|
65
|
+
}
|
|
66
|
+
else {
|
|
67
|
+
groups.push({ tool: tc, count: 1 });
|
|
68
|
+
prevKey = key;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return groups;
|
|
72
|
+
}
|
|
73
|
+
// ============================================================================
|
|
74
|
+
// COMPLETED MESSAGE — memoized, never re-renders during streaming
|
|
75
|
+
// ============================================================================
|
|
76
|
+
export const CompletedMessage = React.memo(function CompletedMessage({ msg, index, toolsExpanded }) {
|
|
77
|
+
const cw = Math.max(20, contentWidth());
|
|
78
|
+
const toolGroups = useMemo(() => msg.toolCalls ? groupConsecutiveTools(msg.toolCalls) : [], [msg.toolCalls]);
|
|
79
|
+
return (_jsxs(Box, { flexDirection: "column", children: [msg.role === "user" && index > 0 && (_jsxs(_Fragment, { children: [_jsx(Text, { children: " " }), _jsx(Text, { color: colors.separator, children: "─".repeat(cw) })] })), msg.role === "user" ? (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { children: " " }), msg.images && msg.images.length > 0 && (_jsx(Box, { marginLeft: 2, children: msg.images.map((name, i) => (_jsxs(Text, { children: [_jsx(Text, { color: colors.indigo, children: "[" }), _jsx(Text, { color: colors.secondary, children: name }), _jsx(Text, { color: colors.indigo, children: "]" }), _jsx(Text, { children: " " })] }, i))) })), _jsxs(Text, { children: [_jsx(Text, { color: colors.brand, bold: true, children: "❯ " }), _jsx(Text, { color: colors.user, children: msg.text })] })] })) : (_jsxs(Box, { flexDirection: "column", children: [toolGroups.length > 0 && (_jsx(Box, { flexDirection: "column", marginLeft: 2, marginTop: 1, children: toolGroups.map((group, j) => (_jsx(ToolIndicator, { id: `done-${index}-${j}`, name: group.tool.name, status: group.tool.status, result: group.tool.result, input: group.tool.input, durationMs: group.tool.durationMs, expanded: toolsExpanded, count: group.count }, j))) })), msg.completedSubagents && msg.completedSubagents.length > 0 && (_jsx(CompletedSubagentTree, { agents: msg.completedSubagents })), msg.text && (_jsxs(Box, { flexDirection: "column", marginLeft: 2, children: [_jsx(Text, { children: " " }), _jsx(MarkdownText, { text: msg.text })] })), msg.usage && (_jsxs(_Fragment, { children: [_jsx(Text, { children: " " }), _jsxs(Text, { children: [" ", _jsxs(Text, { color: colors.quaternary, children: [formatTokens(msg.usage.input_tokens), _jsx(Text, { color: colors.indigo, children: "\u2191" }), " ", formatTokens(msg.usage.output_tokens), _jsx(Text, { color: colors.purple, children: "\u2193" }), msg.usage.thinking_tokens ? (_jsxs(_Fragment, { children: [" ", formatTokens(msg.usage.thinking_tokens), _jsx(Text, { color: colors.warning, children: "T" })] })) : null, msg.usage.cache_read_tokens ? (_jsxs(_Fragment, { children: [" ", formatTokens(msg.usage.cache_read_tokens), _jsx(Text, { color: colors.success, children: "C" })] })) : null] }), _jsxs(Text, { color: colors.quaternary, children: [" ", estimateCost(msg.usage.input_tokens, msg.usage.output_tokens, msg.usage.model, msg.usage.costUsd)] }), msg.toolCalls && msg.toolCalls.length > 0 ? (_jsxs(Text, { color: colors.quaternary, children: [" ", msg.toolCalls.length, " tool", msg.toolCalls.length !== 1 ? "s" : "", " ", formatMs(totalToolDuration(msg.toolCalls))] })) : null] })] }))] }))] }));
|
|
80
|
+
});
|