grok-dev 1.0.0-rc1

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 (112) hide show
  1. package/.cursor/rules/development-workflow.mdc +66 -0
  2. package/.cursor/rules/project-overview.mdc +66 -0
  3. package/.cursor/rules/react-ink-components.mdc +45 -0
  4. package/.cursor/rules/tools-and-agent.mdc +62 -0
  5. package/.cursor/rules/typescript-conventions.mdc +54 -0
  6. package/.grok/settings.json +3 -0
  7. package/.husky/pre-commit +1 -0
  8. package/LICENSE +21 -0
  9. package/README.md +526 -0
  10. package/biome.json +51 -0
  11. package/dist/agent/agent.d.ts +62 -0
  12. package/dist/agent/agent.js +701 -0
  13. package/dist/agent/agent.js.map +1 -0
  14. package/dist/agent/delegations.d.ts +42 -0
  15. package/dist/agent/delegations.js +273 -0
  16. package/dist/agent/delegations.js.map +1 -0
  17. package/dist/grok/client.d.ts +12 -0
  18. package/dist/grok/client.js +37 -0
  19. package/dist/grok/client.js.map +1 -0
  20. package/dist/grok/models.d.ts +5 -0
  21. package/dist/grok/models.js +73 -0
  22. package/dist/grok/models.js.map +1 -0
  23. package/dist/grok/tools.d.ts +12 -0
  24. package/dist/grok/tools.js +230 -0
  25. package/dist/grok/tools.js.map +1 -0
  26. package/dist/index.d.ts +2 -0
  27. package/dist/index.js +192 -0
  28. package/dist/index.js.map +1 -0
  29. package/dist/storage/db.d.ts +18 -0
  30. package/dist/storage/db.js +71 -0
  31. package/dist/storage/db.js.map +1 -0
  32. package/dist/storage/index.d.ts +4 -0
  33. package/dist/storage/index.js +5 -0
  34. package/dist/storage/index.js.map +1 -0
  35. package/dist/storage/migrations.d.ts +2 -0
  36. package/dist/storage/migrations.js +92 -0
  37. package/dist/storage/migrations.js.map +1 -0
  38. package/dist/storage/sessions.d.ts +15 -0
  39. package/dist/storage/sessions.js +135 -0
  40. package/dist/storage/sessions.js.map +1 -0
  41. package/dist/storage/transcript.d.ts +6 -0
  42. package/dist/storage/transcript.js +261 -0
  43. package/dist/storage/transcript.js.map +1 -0
  44. package/dist/storage/usage.d.ts +9 -0
  45. package/dist/storage/usage.js +58 -0
  46. package/dist/storage/usage.js.map +1 -0
  47. package/dist/storage/workspaces.d.ts +9 -0
  48. package/dist/storage/workspaces.js +60 -0
  49. package/dist/storage/workspaces.js.map +1 -0
  50. package/dist/telegram/bridge.d.ts +39 -0
  51. package/dist/telegram/bridge.js +164 -0
  52. package/dist/telegram/bridge.js.map +1 -0
  53. package/dist/telegram/index.d.ts +3 -0
  54. package/dist/telegram/index.js +4 -0
  55. package/dist/telegram/index.js.map +1 -0
  56. package/dist/telegram/limits.d.ts +3 -0
  57. package/dist/telegram/limits.js +12 -0
  58. package/dist/telegram/limits.js.map +1 -0
  59. package/dist/telegram/pairing.d.ts +9 -0
  60. package/dist/telegram/pairing.js +30 -0
  61. package/dist/telegram/pairing.js.map +1 -0
  62. package/dist/telegram/preview-stream.d.ts +22 -0
  63. package/dist/telegram/preview-stream.js +181 -0
  64. package/dist/telegram/preview-stream.js.map +1 -0
  65. package/dist/telegram/turn-coordinator.d.ts +7 -0
  66. package/dist/telegram/turn-coordinator.js +14 -0
  67. package/dist/telegram/turn-coordinator.js.map +1 -0
  68. package/dist/telegram/typing-refresh.d.ts +3 -0
  69. package/dist/telegram/typing-refresh.js +12 -0
  70. package/dist/telegram/typing-refresh.js.map +1 -0
  71. package/dist/tools/bash.d.ts +27 -0
  72. package/dist/tools/bash.js +261 -0
  73. package/dist/tools/bash.js.map +1 -0
  74. package/dist/tools/file.d.ts +15 -0
  75. package/dist/tools/file.js +94 -0
  76. package/dist/tools/file.js.map +1 -0
  77. package/dist/types/index.d.ts +151 -0
  78. package/dist/types/index.js +6 -0
  79. package/dist/types/index.js.map +1 -0
  80. package/dist/ui/app.d.ts +15 -0
  81. package/dist/ui/app.js +1720 -0
  82. package/dist/ui/app.js.map +1 -0
  83. package/dist/ui/markdown.d.ts +5 -0
  84. package/dist/ui/markdown.js +38 -0
  85. package/dist/ui/markdown.js.map +1 -0
  86. package/dist/ui/plan.d.ts +24 -0
  87. package/dist/ui/plan.js +122 -0
  88. package/dist/ui/plan.js.map +1 -0
  89. package/dist/ui/terminal-selection-text.d.ts +23 -0
  90. package/dist/ui/terminal-selection-text.js +59 -0
  91. package/dist/ui/terminal-selection-text.js.map +1 -0
  92. package/dist/ui/theme.d.ts +53 -0
  93. package/dist/ui/theme.js +53 -0
  94. package/dist/ui/theme.js.map +1 -0
  95. package/dist/utils/git-root.d.ts +1 -0
  96. package/dist/utils/git-root.js +16 -0
  97. package/dist/utils/git-root.js.map +1 -0
  98. package/dist/utils/host-clipboard.d.ts +4 -0
  99. package/dist/utils/host-clipboard.js +32 -0
  100. package/dist/utils/host-clipboard.js.map +1 -0
  101. package/dist/utils/instructions.d.ts +1 -0
  102. package/dist/utils/instructions.js +73 -0
  103. package/dist/utils/instructions.js.map +1 -0
  104. package/dist/utils/settings.d.ts +33 -0
  105. package/dist/utils/settings.js +88 -0
  106. package/dist/utils/settings.js.map +1 -0
  107. package/dist/utils/skills.d.ts +17 -0
  108. package/dist/utils/skills.js +161 -0
  109. package/dist/utils/skills.js.map +1 -0
  110. package/package.json +67 -0
  111. package/skills-lock.json +10 -0
  112. package/tmp/large_class.py +633 -0
package/dist/ui/app.js ADDED
@@ -0,0 +1,1720 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "@opentui/react/jsx-runtime";
2
+ import { decodePasteBytes } from "@opentui/core";
3
+ import { useKeyboard, useRenderer, useTerminalDimensions } from "@opentui/react";
4
+ import os from "os";
5
+ import { useCallback, useEffect, useRef, useState } from "react";
6
+ import { Agent } from "../agent/agent";
7
+ import { getModelInfo, MODELS } from "../grok/models";
8
+ import { createTelegramBridge } from "../telegram/bridge";
9
+ import { approvePairingCode } from "../telegram/pairing";
10
+ import { createTurnCoordinator } from "../telegram/turn-coordinator";
11
+ import { MODES } from "../types/index";
12
+ import { copyTextToHostClipboard } from "../utils/host-clipboard";
13
+ import { getApiKey, getTelegramBotToken, loadUserSettings, saveProjectSettings, saveUserSettings, } from "../utils/settings";
14
+ import { discoverSkills, formatSkillsForChat } from "../utils/skills";
15
+ import { Markdown } from "./markdown";
16
+ import { formatPlanAnswers, initialPlanQuestionsState, PlanQuestionsPanel, PlanView, } from "./plan";
17
+ import { getCompactTuiSelectionText } from "./terminal-selection-text";
18
+ import { dark } from "./theme";
19
+ const STAR_PALETTE = ["#777777", "#666666", "#4a4a4a", "#333333", "#222222"];
20
+ const LOADING_SPINNER_FRAMES = ["⬒", "⬔", "⬓", "⬕"];
21
+ const HERO_ROWS = [
22
+ {
23
+ stars: [
24
+ { col: 0, ch: "·" },
25
+ { col: 13, ch: "*" },
26
+ { col: 21, ch: "·" },
27
+ { col: 34, ch: "·" },
28
+ ],
29
+ },
30
+ {
31
+ stars: [
32
+ { col: 3, ch: "*" },
33
+ { col: 11, ch: "·" },
34
+ { col: 17, ch: "·" },
35
+ { col: 25, ch: "*" },
36
+ ],
37
+ },
38
+ {
39
+ stars: [
40
+ { col: 6, ch: "·" },
41
+ { col: 12, ch: "·" },
42
+ { col: 15, ch: "·" },
43
+ { col: 18, ch: "·" },
44
+ { col: 24, ch: "·" },
45
+ ],
46
+ },
47
+ {
48
+ stars: [
49
+ { col: 2, ch: "·" },
50
+ { col: 10, ch: "·" },
51
+ { col: 19, ch: "·" },
52
+ { col: 27, ch: "·" },
53
+ ],
54
+ grok: 13,
55
+ },
56
+ {
57
+ stars: [
58
+ { col: 6, ch: "·" },
59
+ { col: 12, ch: "·" },
60
+ { col: 15, ch: "·" },
61
+ { col: 18, ch: "·" },
62
+ { col: 24, ch: "·" },
63
+ ],
64
+ },
65
+ {
66
+ stars: [
67
+ { col: 3, ch: "·" },
68
+ { col: 11, ch: "*" },
69
+ { col: 17, ch: "·" },
70
+ { col: 25, ch: "·" },
71
+ ],
72
+ },
73
+ {
74
+ stars: [
75
+ { col: 0, ch: "*" },
76
+ { col: 13, ch: "·" },
77
+ { col: 21, ch: "*" },
78
+ { col: 34, ch: "·" },
79
+ ],
80
+ },
81
+ ];
82
+ function HeroLogo({ t }) {
83
+ const [tick, setTick] = useState(0);
84
+ const starIdx = useRef(0);
85
+ useEffect(() => {
86
+ const id = setInterval(() => setTick((n) => n + 1), 900);
87
+ return () => clearInterval(id);
88
+ }, []);
89
+ starIdx.current = 0;
90
+ const nextColor = () => {
91
+ const i = starIdx.current++;
92
+ return STAR_PALETTE[(i * 7 + tick * 3 + i * tick) % STAR_PALETTE.length];
93
+ };
94
+ return (_jsx("box", { flexDirection: "column", alignItems: "center", children: HERO_ROWS.map((row, r) => {
95
+ const els = [];
96
+ let cursor = 0;
97
+ for (const star of row.stars) {
98
+ if (row.grok !== undefined && cursor <= row.grok && star.col > row.grok) {
99
+ els.push(" ".repeat(row.grok - cursor));
100
+ els.push(_jsx("span", { style: { fg: t.primary }, children: "Grok" }, "grok"));
101
+ cursor = row.grok + 4;
102
+ }
103
+ const gap = star.col - cursor;
104
+ if (gap > 0)
105
+ els.push(" ".repeat(gap));
106
+ els.push(_jsx("span", { style: { fg: nextColor() }, children: star.ch }, `s-${star.col}`));
107
+ cursor = star.col + 1;
108
+ }
109
+ if (row.grok !== undefined && cursor <= row.grok) {
110
+ els.push(" ".repeat(row.grok - cursor));
111
+ els.push(_jsx("span", { style: { fg: t.primary }, children: "Grok" }, "grok"));
112
+ cursor = row.grok + 4;
113
+ }
114
+ els.push(" ".repeat(Math.max(0, 35 - cursor)));
115
+ // biome-ignore lint/suspicious/noArrayIndexKey: static constant array that never reorders
116
+ return _jsx("text", { children: els }, r);
117
+ }) }));
118
+ }
119
+ const SPLIT = {
120
+ topLeft: "",
121
+ bottomLeft: "",
122
+ vertical: "┃",
123
+ topRight: "",
124
+ bottomRight: "",
125
+ horizontal: " ",
126
+ bottomT: "",
127
+ topT: "",
128
+ cross: "",
129
+ leftT: "",
130
+ rightT: "",
131
+ };
132
+ const _SPLIT_END = { ...SPLIT, bottomLeft: "╹" };
133
+ const _EMPTY = {
134
+ topLeft: "",
135
+ bottomLeft: "",
136
+ vertical: "",
137
+ topRight: "",
138
+ bottomRight: "",
139
+ horizontal: " ",
140
+ bottomT: "",
141
+ topT: "",
142
+ cross: "",
143
+ leftT: "",
144
+ rightT: "",
145
+ };
146
+ const _LINE = {
147
+ topLeft: "━",
148
+ bottomLeft: "━",
149
+ vertical: "",
150
+ topRight: "━",
151
+ bottomRight: "━",
152
+ horizontal: "━",
153
+ bottomT: "━",
154
+ topT: "━",
155
+ cross: "━",
156
+ leftT: "━",
157
+ rightT: "━",
158
+ };
159
+ const SLASH_MENU_ITEMS = [
160
+ { id: "exit", label: "exit", description: "Quit the CLI" },
161
+ { id: "help", label: "help", description: "Show available commands" },
162
+ { id: "remote-control", label: "remote-control", description: "Remote control" },
163
+ { id: "mcps", label: "mcps", description: "Manage MCP servers" },
164
+ { id: "models", label: "models", description: "Select a model" },
165
+ { id: "new", label: "new session", description: "Start a new session" },
166
+ { id: "review", label: "review", description: "Review recent changes" },
167
+ { id: "skills", label: "skills", description: "Manage skills" },
168
+ ];
169
+ const CONNECT_CHANNELS = [
170
+ { id: "telegram", label: "Telegram", description: "Chat with Grok from Telegram" },
171
+ ];
172
+ export function App({ agent, startupConfig, initialMessage, onExit }) {
173
+ const t = dark;
174
+ const renderer = useRenderer();
175
+ const initialHasApiKey = agent.hasApiKey();
176
+ const [hasApiKey, setHasApiKey] = useState(initialHasApiKey);
177
+ const [messages, setMessages] = useState(() => agent.getChatEntries());
178
+ const [streamContent, setStreamContent] = useState("");
179
+ const [_streamReasoning, setStreamReasoning] = useState("");
180
+ const [isProcessing, setIsProcessing] = useState(false);
181
+ const [model, setModel] = useState(agent.getModel());
182
+ const [mode, setModeState] = useState(agent.getMode());
183
+ const [showModelPicker, setShowModelPicker] = useState(false);
184
+ const [modelPickerIndex, setModelPickerIndex] = useState(0);
185
+ const [modelSearchQuery, setModelSearchQuery] = useState("");
186
+ const [activeToolCalls, setActiveToolCalls] = useState([]);
187
+ const [sessionTitle, setSessionTitle] = useState(() => agent.getSessionTitle());
188
+ const [sessionId, setSessionId] = useState(() => agent.getSessionId());
189
+ const [showApiKeyModal, setShowApiKeyModal] = useState(() => !initialHasApiKey);
190
+ const [apiKeyError, setApiKeyError] = useState(null);
191
+ const [showSlashMenu, setShowSlashMenu] = useState(false);
192
+ const [slashMenuIndex, setSlashMenuIndex] = useState(0);
193
+ const [slashSearchQuery, setSlashSearchQuery] = useState("");
194
+ const [pasteBlocks, setPasteBlocks] = useState([]);
195
+ const [activePlan, setActivePlan] = useState(null);
196
+ /** Incremented on each successful TUI copy; drives a brief "Copied" banner. */
197
+ const [copyFlashId, setCopyFlashId] = useState(0);
198
+ const [activeSubagent, setActiveSubagent] = useState(null);
199
+ const [pqs, setPqs] = useState(initialPlanQuestionsState());
200
+ const imageCounterRef = useRef(0);
201
+ const pasteCounterRef = useRef(0);
202
+ const apiKeyInputRef = useRef(null);
203
+ const inputRef = useRef(null);
204
+ const scrollRef = useRef(null);
205
+ const { width, height } = useTerminalDimensions();
206
+ const processedInitial = useRef(false);
207
+ const contentAccRef = useRef("");
208
+ const startTimeRef = useRef(0);
209
+ const isProcessingRef = useRef(false);
210
+ const hasApiKeyRef = useRef(initialHasApiKey);
211
+ const showApiKeyModalRef = useRef(!initialHasApiKey);
212
+ const queuedMessagesRef = useRef([]);
213
+ const [queuedMessages, setQueuedMessages] = useState([]);
214
+ const modeInfoRef = useRef(MODES[0]);
215
+ const activeRunIdRef = useRef(0);
216
+ const interruptedRunIdRef = useRef(null);
217
+ const coordinatorRef = useRef(createTurnCoordinator());
218
+ const bridgeRef = useRef(null);
219
+ const telegramAgentsRef = useRef(new Map());
220
+ const [showConnectModal, setShowConnectModal] = useState(false);
221
+ const [showTelegramTokenModal, setShowTelegramTokenModal] = useState(false);
222
+ const [showTelegramPairModal, setShowTelegramPairModal] = useState(false);
223
+ const [telegramTokenError, setTelegramTokenError] = useState(null);
224
+ const [telegramPairError, setTelegramPairError] = useState(null);
225
+ const [connectModalIndex, setConnectModalIndex] = useState(0);
226
+ const telegramTokenInputRef = useRef(null);
227
+ const telegramPairInputRef = useRef(null);
228
+ const showConnectModalRef = useRef(false);
229
+ const showTelegramTokenModalRef = useRef(false);
230
+ const showTelegramPairModalRef = useRef(false);
231
+ const setMode = useCallback((m) => {
232
+ if (m === "agent" && mode === "plan" && activePlan) {
233
+ const planText = [
234
+ `# ${activePlan.title}`,
235
+ activePlan.summary,
236
+ "",
237
+ ...activePlan.steps.map((s, i) => `${i + 1}. ${s.title}: ${s.description}${s.filePaths?.length ? ` (${s.filePaths.join(", ")})` : ""}`),
238
+ ].join("\n");
239
+ agent.setPlanContext(planText);
240
+ }
241
+ agent.setMode(m);
242
+ setModeState(m);
243
+ }, [agent, mode, activePlan]);
244
+ const cycleMode = useCallback(() => {
245
+ const idx = MODES.findIndex((m) => m.id === mode);
246
+ setMode(MODES[(idx + 1) % MODES.length].id);
247
+ }, [mode, setMode]);
248
+ const modeInfo = MODES.find((m) => m.id === mode);
249
+ modeInfoRef.current = modeInfo;
250
+ const modelInfo = getModelInfo(model);
251
+ const contextStats = modelInfo ? agent.getContextStats(modelInfo.contextWindow, streamContent) : null;
252
+ const _flatModels = MODELS.map((m) => m.id);
253
+ const filteredModels = modelSearchQuery
254
+ ? MODELS.filter((m) => m.name.toLowerCase().includes(modelSearchQuery.toLowerCase()) ||
255
+ m.id.toLowerCase().includes(modelSearchQuery.toLowerCase()))
256
+ : MODELS;
257
+ const filteredModelIds = filteredModels.map((m) => m.id);
258
+ const filteredSlashItems = slashSearchQuery
259
+ ? SLASH_MENU_ITEMS.filter((item) => item.label.toLowerCase().includes(slashSearchQuery.toLowerCase()) ||
260
+ item.description.toLowerCase().includes(slashSearchQuery.toLowerCase()))
261
+ : SLASH_MENU_ITEMS;
262
+ const scrollToBottom = useCallback(() => {
263
+ try {
264
+ scrollRef.current?.scrollTo(scrollRef.current?.scrollHeight ?? 99999);
265
+ }
266
+ catch {
267
+ /* */
268
+ }
269
+ }, []);
270
+ const getTelegramAgent = useCallback((userId) => {
271
+ const map = telegramAgentsRef.current;
272
+ const existing = map.get(userId);
273
+ if (existing)
274
+ return existing;
275
+ const apiKey = getApiKey();
276
+ if (!apiKey) {
277
+ throw new Error("Grok API key required. Add it in the CLI or set GROK_API_KEY.");
278
+ }
279
+ const u = loadUserSettings();
280
+ const sid = u.telegram?.sessionsByUserId?.[String(userId)];
281
+ const a = new Agent(apiKey, startupConfig.baseURL, startupConfig.model, startupConfig.maxToolRounds, {
282
+ session: sid,
283
+ });
284
+ if (!sid && a.getSessionId()) {
285
+ saveUserSettings({
286
+ telegram: {
287
+ ...u.telegram,
288
+ sessionsByUserId: {
289
+ ...u.telegram?.sessionsByUserId,
290
+ [String(userId)]: a.getSessionId(),
291
+ },
292
+ },
293
+ });
294
+ }
295
+ map.set(userId, a);
296
+ return a;
297
+ }, [startupConfig]);
298
+ const appendTelegramUserMessage = useCallback((event) => {
299
+ setMessages((prev) => [
300
+ ...prev,
301
+ {
302
+ type: "user",
303
+ content: event.content,
304
+ timestamp: new Date(),
305
+ sourceLabel: `Telegram user ${event.userId}`,
306
+ },
307
+ ]);
308
+ setTimeout(scrollToBottom, 10);
309
+ }, [scrollToBottom]);
310
+ const upsertTelegramAssistantMessage = useCallback((event) => {
311
+ const content = sanitizeContent(event.content);
312
+ if (!content && !event.done)
313
+ return;
314
+ setMessages((prev) => {
315
+ const next = [...prev];
316
+ const idx = next.findIndex((entry) => entry.type === "assistant" && entry.remoteKey === event.turnKey);
317
+ const entry = {
318
+ type: "assistant",
319
+ content: content || "(no text output)",
320
+ timestamp: new Date(),
321
+ remoteKey: event.turnKey,
322
+ sourceLabel: `Telegram Grok • user ${event.userId}`,
323
+ };
324
+ if (idx === -1) {
325
+ next.push(entry);
326
+ }
327
+ else {
328
+ next[idx] = {
329
+ ...next[idx],
330
+ content: entry.content,
331
+ timestamp: entry.timestamp,
332
+ sourceLabel: entry.sourceLabel,
333
+ };
334
+ }
335
+ return next;
336
+ });
337
+ if (event.done) {
338
+ setActiveToolCalls([]);
339
+ }
340
+ setTimeout(scrollToBottom, 10);
341
+ }, [scrollToBottom]);
342
+ const showTelegramToolCalls = useCallback((event) => {
343
+ setActiveToolCalls(event.toolCalls);
344
+ setTimeout(scrollToBottom, 10);
345
+ }, [scrollToBottom]);
346
+ const appendTelegramToolResult = useCallback((event) => {
347
+ setMessages((prev) => [
348
+ ...prev,
349
+ {
350
+ type: "tool_result",
351
+ content: event.toolResult.success ? event.toolResult.output || "Success" : event.toolResult.error || "Error",
352
+ timestamp: new Date(),
353
+ toolCall: event.toolCall,
354
+ toolResult: event.toolResult,
355
+ },
356
+ ]);
357
+ if (event.toolResult.plan?.questions?.length) {
358
+ setActivePlan(event.toolResult.plan);
359
+ setPqs(initialPlanQuestionsState());
360
+ }
361
+ setActiveToolCalls([]);
362
+ setTimeout(scrollToBottom, 10);
363
+ }, [scrollToBottom]);
364
+ const startTelegramBridge = useCallback(() => {
365
+ const token = getTelegramBotToken();
366
+ if (!token || !getApiKey())
367
+ return;
368
+ if (bridgeRef.current)
369
+ return;
370
+ const bridge = createTelegramBridge({
371
+ token,
372
+ getApprovedUserIds: () => loadUserSettings().telegram?.approvedUserIds ?? [],
373
+ coordinator: coordinatorRef.current,
374
+ getTelegramAgent,
375
+ onUserMessage: appendTelegramUserMessage,
376
+ onAssistantMessage: upsertTelegramAssistantMessage,
377
+ onToolCalls: showTelegramToolCalls,
378
+ onToolResult: appendTelegramToolResult,
379
+ onError: (msg) => {
380
+ setMessages((p) => [...p, { type: "assistant", content: `Telegram: ${msg}`, timestamp: new Date() }]);
381
+ },
382
+ });
383
+ bridgeRef.current = bridge;
384
+ bridge.start();
385
+ }, [
386
+ appendTelegramToolResult,
387
+ appendTelegramUserMessage,
388
+ getTelegramAgent,
389
+ showTelegramToolCalls,
390
+ upsertTelegramAssistantMessage,
391
+ ]);
392
+ /** Start long polling when a bot token is already saved (pairing UI is optional if already approved). */
393
+ useEffect(() => {
394
+ if (!hasApiKey)
395
+ return;
396
+ if (!getTelegramBotToken())
397
+ return;
398
+ startTelegramBridge();
399
+ }, [hasApiKey, startTelegramBridge]);
400
+ const handleExit = useCallback(() => {
401
+ void bridgeRef.current?.stop();
402
+ bridgeRef.current = null;
403
+ onExit?.();
404
+ }, [onExit]);
405
+ const showCopyBanner = useCallback(() => {
406
+ setCopyFlashId((n) => n + 1);
407
+ }, []);
408
+ /** Match OpenCode: OSC 52 + real OS clipboard; used from keyboard and root onMouseUp. */
409
+ const copyTuiSelectionToHost = useCallback(() => {
410
+ if (!renderer.hasSelection)
411
+ return false;
412
+ const sel = renderer.getSelection();
413
+ const text = sel ? getCompactTuiSelectionText(sel) : "";
414
+ if (!text)
415
+ return false;
416
+ renderer.copyToClipboardOSC52(text);
417
+ copyTextToHostClipboard(text);
418
+ renderer.clearSelection();
419
+ showCopyBanner();
420
+ return true;
421
+ }, [renderer, showCopyBanner]);
422
+ const handleRootMouseUp = useCallback(() => {
423
+ copyTuiSelectionToHost();
424
+ }, [copyTuiSelectionToHost]);
425
+ useEffect(() => {
426
+ if (copyFlashId === 0)
427
+ return;
428
+ const id = setTimeout(() => setCopyFlashId(0), 2000);
429
+ return () => clearTimeout(id);
430
+ }, [copyFlashId]);
431
+ const openApiKeyModal = useCallback(() => {
432
+ showApiKeyModalRef.current = true;
433
+ setApiKeyError(null);
434
+ setShowApiKeyModal(true);
435
+ }, []);
436
+ const closeApiKeyModal = useCallback(() => {
437
+ showApiKeyModalRef.current = false;
438
+ setApiKeyError(null);
439
+ setShowApiKeyModal(false);
440
+ }, []);
441
+ const submitApiKey = useCallback(() => {
442
+ const apiKey = (apiKeyInputRef.current?.plainText || "").trim();
443
+ if (!apiKey) {
444
+ setApiKeyError("Enter an API key to continue.");
445
+ return;
446
+ }
447
+ if (!apiKey.startsWith("xai-")) {
448
+ setApiKeyError("API keys should start with xai-.");
449
+ return;
450
+ }
451
+ saveUserSettings({ apiKey });
452
+ agent.setApiKey(apiKey);
453
+ hasApiKeyRef.current = true;
454
+ showApiKeyModalRef.current = false;
455
+ setHasApiKey(true);
456
+ setApiKeyError(null);
457
+ setShowApiKeyModal(false);
458
+ apiKeyInputRef.current?.clear();
459
+ if (getTelegramBotToken()) {
460
+ startTelegramBridge();
461
+ }
462
+ }, [agent, startTelegramBridge]);
463
+ useEffect(() => {
464
+ hasApiKeyRef.current = hasApiKey;
465
+ }, [hasApiKey]);
466
+ useEffect(() => {
467
+ showApiKeyModalRef.current = showApiKeyModal;
468
+ }, [showApiKeyModal]);
469
+ useEffect(() => {
470
+ showConnectModalRef.current = showConnectModal;
471
+ }, [showConnectModal]);
472
+ useEffect(() => {
473
+ showTelegramTokenModalRef.current = showTelegramTokenModal;
474
+ }, [showTelegramTokenModal]);
475
+ useEffect(() => {
476
+ showTelegramPairModalRef.current = showTelegramPairModal;
477
+ }, [showTelegramPairModal]);
478
+ useEffect(() => {
479
+ return () => {
480
+ void bridgeRef.current?.stop();
481
+ bridgeRef.current = null;
482
+ };
483
+ }, []);
484
+ const submitTelegramToken = useCallback(() => {
485
+ const token = (telegramTokenInputRef.current?.plainText || "").trim();
486
+ if (!token) {
487
+ setTelegramTokenError("Paste your bot token from @BotFather.");
488
+ return;
489
+ }
490
+ if (!getApiKey()) {
491
+ setTelegramTokenError("Add a Grok API key first.");
492
+ return;
493
+ }
494
+ const u = loadUserSettings();
495
+ saveUserSettings({ telegram: { ...u.telegram, botToken: token } });
496
+ telegramTokenInputRef.current?.clear();
497
+ setShowTelegramTokenModal(false);
498
+ setTelegramTokenError(null);
499
+ startTelegramBridge();
500
+ setShowTelegramPairModal(true);
501
+ setTelegramPairError(null);
502
+ setMessages((p) => [
503
+ ...p,
504
+ {
505
+ type: "assistant",
506
+ content: "Telegram polling started. In Telegram, DM your bot and send /pair. Copy the code, then enter it below.",
507
+ timestamp: new Date(),
508
+ },
509
+ ]);
510
+ }, [startTelegramBridge]);
511
+ const submitTelegramPair = useCallback(async () => {
512
+ const code = (telegramPairInputRef.current?.plainText || "").trim();
513
+ if (!code) {
514
+ setTelegramPairError("Enter the pairing code.");
515
+ return;
516
+ }
517
+ const result = approvePairingCode(code);
518
+ if (!result.ok) {
519
+ setTelegramPairError(result.error);
520
+ return;
521
+ }
522
+ const u = loadUserSettings();
523
+ const ids = new Set(u.telegram?.approvedUserIds ?? []);
524
+ ids.add(result.userId);
525
+ saveUserSettings({ telegram: { ...u.telegram, approvedUserIds: [...ids] } });
526
+ telegramPairInputRef.current?.clear();
527
+ setShowTelegramPairModal(false);
528
+ setTelegramPairError(null);
529
+ setMessages((p) => [
530
+ ...p,
531
+ {
532
+ type: "assistant",
533
+ content: `Telegram user ${result.userId} paired. Keep this CLI open while you use the bot.`,
534
+ timestamp: new Date(),
535
+ },
536
+ ]);
537
+ try {
538
+ await bridgeRef.current?.sendDm(result.userId, "Pairing approved. You can message Grok here.");
539
+ }
540
+ catch {
541
+ /* optional DM */
542
+ }
543
+ }, []);
544
+ const beginTelegramFromConnect = useCallback(() => {
545
+ setShowConnectModal(false);
546
+ if (!getApiKey()) {
547
+ setMessages((p) => [...p, { type: "assistant", content: "Add a Grok API key first.", timestamp: new Date() }]);
548
+ openApiKeyModal();
549
+ return;
550
+ }
551
+ if (!getTelegramBotToken()) {
552
+ setShowTelegramTokenModal(true);
553
+ setTelegramTokenError(null);
554
+ return;
555
+ }
556
+ startTelegramBridge();
557
+ const alreadyPaired = (loadUserSettings().telegram?.approvedUserIds?.length ?? 0) > 0;
558
+ if (!alreadyPaired) {
559
+ setShowTelegramPairModal(true);
560
+ setTelegramPairError(null);
561
+ setMessages((p) => [
562
+ ...p,
563
+ {
564
+ type: "assistant",
565
+ content: "Telegram polling started. In Telegram, DM your bot and send /pair. Copy the code, then enter it below.",
566
+ timestamp: new Date(),
567
+ },
568
+ ]);
569
+ }
570
+ else {
571
+ setMessages((p) => [
572
+ ...p,
573
+ {
574
+ type: "assistant",
575
+ content: "Telegram polling is running. Your chat is already paired.",
576
+ timestamp: new Date(),
577
+ },
578
+ ]);
579
+ }
580
+ }, [openApiKeyModal, startTelegramBridge]);
581
+ const invalidateActiveRun = useCallback(() => {
582
+ activeRunIdRef.current += 1;
583
+ setActiveToolCalls([]);
584
+ setActiveSubagent(null);
585
+ setStreamContent("");
586
+ setStreamReasoning("");
587
+ }, []);
588
+ const resetToNewSession = useCallback(() => {
589
+ const snapshot = agent.startNewSession();
590
+ setMessages(snapshot?.entries ?? []);
591
+ setStreamContent("");
592
+ setStreamReasoning("");
593
+ setSessionTitle(snapshot?.session.title ?? null);
594
+ setSessionId(snapshot?.session.id ?? agent.getSessionId());
595
+ setActiveToolCalls([]);
596
+ setActiveSubagent(null);
597
+ setActivePlan(null);
598
+ setPqs(initialPlanQuestionsState());
599
+ setPasteBlocks([]);
600
+ queuedMessagesRef.current = [];
601
+ setQueuedMessages([]);
602
+ imageCounterRef.current = 0;
603
+ }, [agent]);
604
+ const processMessage = useCallback(async (text) => {
605
+ if (!text.trim() || isProcessingRef.current)
606
+ return;
607
+ const runId = ++activeRunIdRef.current;
608
+ const isStale = () => activeRunIdRef.current !== runId;
609
+ isProcessingRef.current = true;
610
+ setIsProcessing(true);
611
+ if (!sessionTitle)
612
+ agent
613
+ .generateTitle(text.trim())
614
+ .then(setSessionTitle)
615
+ .catch(() => { });
616
+ await coordinatorRef.current.run(async () => {
617
+ setStreamContent("");
618
+ setStreamReasoning("");
619
+ setActiveToolCalls([]);
620
+ setActiveSubagent(null);
621
+ contentAccRef.current = "";
622
+ startTimeRef.current = Date.now();
623
+ const color = modeInfoRef.current.color;
624
+ setMessages((prev) => [
625
+ ...prev,
626
+ { type: "user", content: text.trim(), timestamp: new Date(), modeColor: color },
627
+ ]);
628
+ setTimeout(scrollToBottom, 50);
629
+ try {
630
+ for await (const chunk of agent.processMessage(text.trim())) {
631
+ if (isStale()) {
632
+ break;
633
+ }
634
+ switch (chunk.type) {
635
+ case "content":
636
+ contentAccRef.current += chunk.content || "";
637
+ setStreamContent(sanitizeContent(contentAccRef.current));
638
+ setTimeout(scrollToBottom, 10);
639
+ break;
640
+ case "reasoning":
641
+ setStreamReasoning((p) => p + (chunk.content || ""));
642
+ break;
643
+ case "tool_calls":
644
+ if (chunk.toolCalls) {
645
+ const cleaned = sanitizeContent(contentAccRef.current);
646
+ if (cleaned) {
647
+ setMessages((p) => [
648
+ ...p,
649
+ {
650
+ type: "assistant",
651
+ content: cleaned,
652
+ timestamp: new Date(),
653
+ modeColor: modeInfoRef.current.color,
654
+ },
655
+ ]);
656
+ }
657
+ contentAccRef.current = "";
658
+ setStreamContent("");
659
+ setActiveToolCalls(chunk.toolCalls);
660
+ }
661
+ break;
662
+ case "tool_result":
663
+ if (chunk.toolCall && chunk.toolResult) {
664
+ setMessages((p) => [
665
+ ...p,
666
+ {
667
+ type: "tool_result",
668
+ content: chunk.toolResult.success
669
+ ? chunk.toolResult.output || "Success"
670
+ : chunk.toolResult.error || "Error",
671
+ timestamp: new Date(),
672
+ modeColor: modeInfoRef.current.color,
673
+ toolCall: chunk.toolCall,
674
+ toolResult: chunk.toolResult,
675
+ },
676
+ ]);
677
+ if (chunk.toolResult.plan?.questions?.length) {
678
+ setActivePlan(chunk.toolResult.plan);
679
+ setPqs(initialPlanQuestionsState());
680
+ }
681
+ setActiveToolCalls([]);
682
+ setTimeout(scrollToBottom, 10);
683
+ }
684
+ break;
685
+ case "error":
686
+ contentAccRef.current += `\n${chunk.content || "Unknown error"}`;
687
+ setStreamContent(contentAccRef.current);
688
+ break;
689
+ case "done":
690
+ break;
691
+ }
692
+ }
693
+ }
694
+ catch {
695
+ if (!isStale()) {
696
+ contentAccRef.current += "\nAn unexpected error occurred.";
697
+ setStreamContent(contentAccRef.current);
698
+ }
699
+ }
700
+ const wasInterrupted = interruptedRunIdRef.current === runId;
701
+ const finalContent = sanitizeContent(contentAccRef.current);
702
+ if (isStale()) {
703
+ contentAccRef.current = "";
704
+ return;
705
+ }
706
+ if (!wasInterrupted && finalContent) {
707
+ setMessages((p) => [
708
+ ...p,
709
+ { type: "assistant", content: finalContent, timestamp: new Date(), modeColor: modeInfoRef.current.color },
710
+ ]);
711
+ }
712
+ contentAccRef.current = "";
713
+ if (!isStale()) {
714
+ setStreamContent("");
715
+ setStreamReasoning("");
716
+ setActiveToolCalls([]);
717
+ setActiveSubagent(null);
718
+ }
719
+ if (wasInterrupted) {
720
+ interruptedRunIdRef.current = null;
721
+ }
722
+ const nextQueued = queuedMessagesRef.current.shift();
723
+ if (nextQueued) {
724
+ setQueuedMessages([...queuedMessagesRef.current]);
725
+ isProcessingRef.current = false;
726
+ processMessage(nextQueued);
727
+ }
728
+ else {
729
+ isProcessingRef.current = false;
730
+ if (!isStale()) {
731
+ setIsProcessing(false);
732
+ }
733
+ }
734
+ setTimeout(scrollToBottom, 50);
735
+ });
736
+ }, [agent, scrollToBottom, sessionTitle]);
737
+ useEffect(() => {
738
+ if (initialMessage && hasApiKey && !processedInitial.current) {
739
+ processedInitial.current = true;
740
+ processMessage(initialMessage);
741
+ }
742
+ }, [hasApiKey, initialMessage, processMessage]);
743
+ useEffect(() => agent.onSubagentStatus(setActiveSubagent), [agent]);
744
+ useEffect(() => {
745
+ let active = true;
746
+ const id = setInterval(() => {
747
+ agent
748
+ .consumeBackgroundNotifications()
749
+ .then((notifications) => {
750
+ if (!active || notifications.length === 0)
751
+ return;
752
+ setMessages((prev) => [
753
+ ...prev,
754
+ ...notifications.map((message) => ({
755
+ type: "assistant",
756
+ content: message,
757
+ timestamp: new Date(),
758
+ })),
759
+ ]);
760
+ setTimeout(scrollToBottom, 10);
761
+ })
762
+ .catch(() => { });
763
+ }, 2000);
764
+ return () => {
765
+ active = false;
766
+ clearInterval(id);
767
+ };
768
+ }, [agent, scrollToBottom]);
769
+ const handleCommand = useCallback((cmd) => {
770
+ const c = cmd.trim().toLowerCase();
771
+ if (c === "/clear") {
772
+ resetToNewSession();
773
+ return true;
774
+ }
775
+ if (c === "/model" || c === "/models") {
776
+ setShowModelPicker(true);
777
+ setModelPickerIndex(0);
778
+ setModelSearchQuery("");
779
+ return true;
780
+ }
781
+ if (c === "/remote-control") {
782
+ setConnectModalIndex(0);
783
+ setShowConnectModal(true);
784
+ return true;
785
+ }
786
+ if (c === "/quit" || c === "/exit" || c === "/q") {
787
+ handleExit();
788
+ return true;
789
+ }
790
+ return false;
791
+ }, [handleExit, resetToNewSession]);
792
+ const handleSlashMenuSelect = useCallback((item) => {
793
+ setShowSlashMenu(false);
794
+ inputRef.current?.clear();
795
+ switch (item.id) {
796
+ case "new":
797
+ resetToNewSession();
798
+ break;
799
+ case "models":
800
+ setShowModelPicker(true);
801
+ setModelPickerIndex(0);
802
+ setModelSearchQuery("");
803
+ break;
804
+ case "remote-control":
805
+ setConnectModalIndex(0);
806
+ setShowConnectModal(true);
807
+ break;
808
+ case "exit":
809
+ handleExit();
810
+ break;
811
+ case "help":
812
+ setMessages((p) => [
813
+ ...p,
814
+ {
815
+ type: "assistant",
816
+ content: SLASH_MENU_ITEMS.map((i) => `/${i.label} — ${i.description}`).join("\n"),
817
+ timestamp: new Date(),
818
+ },
819
+ ]);
820
+ break;
821
+ case "skills":
822
+ setMessages((p) => [
823
+ ...p,
824
+ {
825
+ type: "assistant",
826
+ content: formatSkillsForChat(discoverSkills(agent.getCwd()), agent.getCwd()),
827
+ timestamp: new Date(),
828
+ },
829
+ ]);
830
+ break;
831
+ case "mcps":
832
+ setMessages((p) => [
833
+ ...p,
834
+ { type: "assistant", content: "MCP server management coming soon.", timestamp: new Date() },
835
+ ]);
836
+ break;
837
+ case "review":
838
+ setMessages((p) => [
839
+ ...p,
840
+ { type: "assistant", content: "Review feature coming soon.", timestamp: new Date() },
841
+ ]);
842
+ break;
843
+ }
844
+ }, [agent, handleExit, resetToNewSession]);
845
+ const blockPrompt = showConnectModal || showTelegramTokenModal || showTelegramPairModal;
846
+ const showPlanPanel = !!activePlan?.questions?.length;
847
+ const planQuestions = activePlan?.questions ?? [];
848
+ const isSinglePlan = planQuestions.length === 1 && planQuestions[0]?.type !== "multiselect";
849
+ const planTabCount = isSinglePlan ? 1 : planQuestions.length + 1;
850
+ const isPlanConfirmTab = !isSinglePlan && pqs.tab === planQuestions.length;
851
+ const dismissPlan = useCallback(() => {
852
+ setActivePlan(null);
853
+ setPqs(initialPlanQuestionsState());
854
+ }, []);
855
+ const submitPlanAnswers = useCallback(() => {
856
+ if (!activePlan?.questions?.length)
857
+ return;
858
+ const text = formatPlanAnswers(activePlan.questions, pqs.answers);
859
+ setActivePlan(null);
860
+ setPqs(initialPlanQuestionsState());
861
+ processMessage(text);
862
+ }, [activePlan, pqs.answers, processMessage]);
863
+ const handlePlanSelect = useCallback((q, idx, options, showCustom) => {
864
+ const isCustom = showCustom && idx === options.length;
865
+ if (isCustom) {
866
+ if (q.type === "multiselect") {
867
+ const customVal = pqs.customInputs[q.id] ?? "";
868
+ if (customVal) {
869
+ const existing = pqs.answers[q.id] ?? [];
870
+ if (existing.includes(customVal)) {
871
+ setPqs((s) => ({ ...s, answers: { ...s.answers, [q.id]: existing.filter((x) => x !== customVal) } }));
872
+ }
873
+ else {
874
+ setPqs((s) => ({ ...s, editing: true }));
875
+ }
876
+ }
877
+ else {
878
+ setPqs((s) => ({ ...s, editing: true }));
879
+ }
880
+ }
881
+ else {
882
+ setPqs((s) => ({ ...s, editing: true }));
883
+ }
884
+ return;
885
+ }
886
+ const opt = options[idx];
887
+ if (!opt)
888
+ return;
889
+ if (q.type === "multiselect") {
890
+ setPqs((s) => {
891
+ const existing = s.answers[q.id] ?? [];
892
+ const next = existing.includes(opt.id) ? existing.filter((x) => x !== opt.id) : [...existing, opt.id];
893
+ return { ...s, answers: { ...s.answers, [q.id]: next } };
894
+ });
895
+ }
896
+ else {
897
+ setPqs((s) => ({ ...s, answers: { ...s.answers, [q.id]: opt.id } }));
898
+ if (isSinglePlan) {
899
+ submitPlanAnswers();
900
+ return;
901
+ }
902
+ setPqs((s) => ({ ...s, tab: s.tab + 1, selected: 0 }));
903
+ }
904
+ }, [pqs, isSinglePlan, submitPlanAnswers]);
905
+ const handleKey = useCallback((key) => {
906
+ if (showPlanPanel) {
907
+ const q = planQuestions[pqs.tab];
908
+ // Escape always dismisses
909
+ if (key.name === "escape") {
910
+ dismissPlan();
911
+ return;
912
+ }
913
+ // When editing custom text input
914
+ if (pqs.editing && !isPlanConfirmTab) {
915
+ if (key.name === "return") {
916
+ const qId = q?.id;
917
+ if (qId) {
918
+ const text = (pqs.customInputs[qId] ?? "").trim();
919
+ if (text) {
920
+ if (q.type === "multiselect") {
921
+ const existing = pqs.answers[qId] ?? [];
922
+ const next = existing.includes(text) ? existing : [...existing, text];
923
+ setPqs((s) => ({ ...s, editing: false, answers: { ...s.answers, [qId]: next } }));
924
+ }
925
+ else if (q.type === "text") {
926
+ setPqs((s) => ({ ...s, editing: false, answers: { ...s.answers, [qId]: text } }));
927
+ if (isSinglePlan) {
928
+ submitPlanAnswers();
929
+ return;
930
+ }
931
+ setPqs((s) => ({ ...s, tab: s.tab + 1, selected: 0 }));
932
+ }
933
+ else {
934
+ setPqs((s) => ({ ...s, editing: false, answers: { ...s.answers, [qId]: text } }));
935
+ if (isSinglePlan) {
936
+ submitPlanAnswers();
937
+ return;
938
+ }
939
+ setPqs((s) => ({ ...s, tab: s.tab + 1, selected: 0 }));
940
+ }
941
+ }
942
+ else {
943
+ setPqs((s) => ({ ...s, editing: false }));
944
+ }
945
+ }
946
+ return;
947
+ }
948
+ if (key.name === "backspace") {
949
+ const qId = q?.id;
950
+ if (qId)
951
+ setPqs((s) => ({
952
+ ...s,
953
+ customInputs: { ...s.customInputs, [qId]: (s.customInputs[qId] ?? "").slice(0, -1) },
954
+ }));
955
+ return;
956
+ }
957
+ if (key.sequence && key.sequence.length === 1 && !key.ctrl && !key.meta) {
958
+ const qId = q?.id;
959
+ if (qId)
960
+ setPqs((s) => ({
961
+ ...s,
962
+ customInputs: { ...s.customInputs, [qId]: (s.customInputs[qId] ?? "") + key.sequence },
963
+ }));
964
+ return;
965
+ }
966
+ return;
967
+ }
968
+ // Tab / left / right — switch between question tabs
969
+ if (key.name === "tab") {
970
+ const dir = key.shift ? -1 : 1;
971
+ setPqs((s) => ({ ...s, tab: (s.tab + dir + planTabCount) % planTabCount, selected: 0 }));
972
+ return;
973
+ }
974
+ if (key.name === "left" || key.name === "h") {
975
+ setPqs((s) => ({ ...s, tab: (s.tab - 1 + planTabCount) % planTabCount, selected: 0 }));
976
+ return;
977
+ }
978
+ if (key.name === "right" || key.name === "l") {
979
+ setPqs((s) => ({ ...s, tab: (s.tab + 1) % planTabCount, selected: 0 }));
980
+ return;
981
+ }
982
+ // Confirm tab
983
+ if (isPlanConfirmTab) {
984
+ if (key.name === "return") {
985
+ submitPlanAnswers();
986
+ return;
987
+ }
988
+ return;
989
+ }
990
+ if (!q)
991
+ return;
992
+ // Text-only question (no options)
993
+ if (q.type === "text") {
994
+ setPqs((s) => ({ ...s, editing: true }));
995
+ return;
996
+ }
997
+ // Up/down — navigate options
998
+ const options = q.options ?? [];
999
+ const showCustom = true;
1000
+ const totalItems = options.length + 1;
1001
+ if (key.name === "up" || key.name === "k") {
1002
+ setPqs((s) => ({ ...s, selected: (s.selected - 1 + totalItems) % totalItems }));
1003
+ return;
1004
+ }
1005
+ if (key.name === "down" || key.name === "j") {
1006
+ setPqs((s) => ({ ...s, selected: (s.selected + 1) % totalItems }));
1007
+ return;
1008
+ }
1009
+ // Number keys 1-9 for quick selection
1010
+ const digit = Number(key.name);
1011
+ if (!Number.isNaN(digit) && digit >= 1 && digit <= Math.min(totalItems, 9)) {
1012
+ const idx = digit - 1;
1013
+ setPqs((s) => ({ ...s, selected: idx }));
1014
+ handlePlanSelect(q, idx, options, showCustom);
1015
+ return;
1016
+ }
1017
+ // Enter — select current option
1018
+ if (key.name === "return") {
1019
+ handlePlanSelect(q, pqs.selected, options, showCustom);
1020
+ return;
1021
+ }
1022
+ return;
1023
+ }
1024
+ if (showTelegramTokenModalRef.current) {
1025
+ if (key.name === "escape") {
1026
+ setShowTelegramTokenModal(false);
1027
+ setTelegramTokenError(null);
1028
+ return;
1029
+ }
1030
+ if (key.name === "return") {
1031
+ submitTelegramToken();
1032
+ }
1033
+ return;
1034
+ }
1035
+ if (showTelegramPairModalRef.current) {
1036
+ if (key.name === "escape") {
1037
+ setShowTelegramPairModal(false);
1038
+ setTelegramPairError(null);
1039
+ return;
1040
+ }
1041
+ if (key.name === "return") {
1042
+ void submitTelegramPair();
1043
+ }
1044
+ return;
1045
+ }
1046
+ if (showConnectModalRef.current) {
1047
+ if (key.name === "escape") {
1048
+ setShowConnectModal(false);
1049
+ return;
1050
+ }
1051
+ if (key.name === "up") {
1052
+ setConnectModalIndex((i) => Math.max(0, i - 1));
1053
+ return;
1054
+ }
1055
+ if (key.name === "down") {
1056
+ setConnectModalIndex((i) => Math.min(CONNECT_CHANNELS.length - 1, i + 1));
1057
+ return;
1058
+ }
1059
+ if (key.name === "return") {
1060
+ const ch = CONNECT_CHANNELS[connectModalIndex];
1061
+ if (ch?.id === "telegram")
1062
+ beginTelegramFromConnect();
1063
+ return;
1064
+ }
1065
+ return;
1066
+ }
1067
+ if (showApiKeyModalRef.current) {
1068
+ if (key.name === "escape") {
1069
+ closeApiKeyModal();
1070
+ return;
1071
+ }
1072
+ if (key.name === "return") {
1073
+ submitApiKey();
1074
+ }
1075
+ return;
1076
+ }
1077
+ if (showSlashMenu) {
1078
+ if (key.name === "escape") {
1079
+ setShowSlashMenu(false);
1080
+ setSlashSearchQuery("");
1081
+ inputRef.current?.clear();
1082
+ return;
1083
+ }
1084
+ if (key.name === "up") {
1085
+ setSlashMenuIndex((i) => Math.max(0, i - 1));
1086
+ return;
1087
+ }
1088
+ if (key.name === "down") {
1089
+ setSlashMenuIndex((i) => Math.min(filteredSlashItems.length - 1, i + 1));
1090
+ return;
1091
+ }
1092
+ if (key.name === "return") {
1093
+ const item = filteredSlashItems[slashMenuIndex];
1094
+ if (item)
1095
+ handleSlashMenuSelect(item);
1096
+ setSlashSearchQuery("");
1097
+ return;
1098
+ }
1099
+ if (key.name === "backspace") {
1100
+ setSlashSearchQuery((q) => q.slice(0, -1));
1101
+ setSlashMenuIndex(0);
1102
+ return;
1103
+ }
1104
+ if (key.sequence && key.sequence.length === 1 && !key.ctrl && !key.meta) {
1105
+ setSlashSearchQuery((q) => q + key.sequence);
1106
+ setSlashMenuIndex(0);
1107
+ return;
1108
+ }
1109
+ return;
1110
+ }
1111
+ if (showModelPicker) {
1112
+ if (key.name === "escape") {
1113
+ setShowModelPicker(false);
1114
+ setModelSearchQuery("");
1115
+ return;
1116
+ }
1117
+ if (key.name === "up") {
1118
+ setModelPickerIndex((i) => Math.max(0, i - 1));
1119
+ return;
1120
+ }
1121
+ if (key.name === "down") {
1122
+ setModelPickerIndex((i) => Math.min(filteredModelIds.length - 1, i + 1));
1123
+ return;
1124
+ }
1125
+ if (key.name === "return") {
1126
+ const sel = filteredModelIds[modelPickerIndex];
1127
+ if (sel) {
1128
+ agent.setModel(sel);
1129
+ setModel(sel);
1130
+ saveProjectSettings({ model: sel });
1131
+ saveUserSettings({ defaultModel: sel });
1132
+ }
1133
+ setShowModelPicker(false);
1134
+ setModelSearchQuery("");
1135
+ return;
1136
+ }
1137
+ if (key.name === "backspace") {
1138
+ setModelSearchQuery((q) => q.slice(0, -1));
1139
+ setModelPickerIndex(0);
1140
+ return;
1141
+ }
1142
+ if (key.sequence && key.sequence.length === 1 && !key.ctrl && !key.meta) {
1143
+ setModelSearchQuery((q) => q + key.sequence);
1144
+ setModelPickerIndex(0);
1145
+ return;
1146
+ }
1147
+ return;
1148
+ }
1149
+ if (!hasApiKeyRef.current && shouldOpenApiKeyModalForKey(key)) {
1150
+ openApiKeyModal();
1151
+ return;
1152
+ }
1153
+ if (isProcessing && key.name === "escape") {
1154
+ invalidateActiveRun();
1155
+ if (queuedMessagesRef.current.length > 0) {
1156
+ queuedMessagesRef.current = [];
1157
+ setQueuedMessages([]);
1158
+ }
1159
+ else {
1160
+ agent.abort();
1161
+ }
1162
+ return;
1163
+ }
1164
+ if (key.sequence === "/" && !isProcessing) {
1165
+ const text = inputRef.current?.plainText || "";
1166
+ if (!text.trim()) {
1167
+ setShowSlashMenu(true);
1168
+ setSlashMenuIndex(0);
1169
+ setSlashSearchQuery("");
1170
+ return;
1171
+ }
1172
+ }
1173
+ if (key.name === "c" && key.ctrl && key.shift) {
1174
+ if (copyTuiSelectionToHost()) {
1175
+ key.preventDefault();
1176
+ key.stopPropagation();
1177
+ }
1178
+ return;
1179
+ }
1180
+ if (key.name === "y" && key.ctrl && copyTuiSelectionToHost()) {
1181
+ key.preventDefault();
1182
+ key.stopPropagation();
1183
+ return;
1184
+ }
1185
+ // ⌘C: Kitty / iTerm report Command as `super`; some setups use `meta` instead.
1186
+ if (key.name === "c" && !key.ctrl && (key.meta || key.super)) {
1187
+ if (copyTuiSelectionToHost()) {
1188
+ key.preventDefault();
1189
+ key.stopPropagation();
1190
+ return;
1191
+ }
1192
+ }
1193
+ if (key.name === "c" && key.ctrl) {
1194
+ if (copyTuiSelectionToHost()) {
1195
+ key.preventDefault();
1196
+ key.stopPropagation();
1197
+ return;
1198
+ }
1199
+ const text = inputRef.current?.plainText || "";
1200
+ if (text.trim()) {
1201
+ inputRef.current?.clear();
1202
+ setPasteBlocks([]);
1203
+ }
1204
+ else {
1205
+ handleExit();
1206
+ }
1207
+ return;
1208
+ }
1209
+ if (key.name === "tab" && !isProcessing) {
1210
+ cycleMode();
1211
+ return;
1212
+ }
1213
+ }, [
1214
+ agent,
1215
+ beginTelegramFromConnect,
1216
+ closeApiKeyModal,
1217
+ connectModalIndex,
1218
+ cycleMode,
1219
+ dismissPlan,
1220
+ filteredModelIds,
1221
+ filteredSlashItems,
1222
+ handleExit,
1223
+ handlePlanSelect,
1224
+ handleSlashMenuSelect,
1225
+ invalidateActiveRun,
1226
+ isPlanConfirmTab,
1227
+ isProcessing,
1228
+ isSinglePlan,
1229
+ modelPickerIndex,
1230
+ openApiKeyModal,
1231
+ submitTelegramPair,
1232
+ submitTelegramToken,
1233
+ planQuestions,
1234
+ planTabCount,
1235
+ pqs,
1236
+ showModelPicker,
1237
+ showPlanPanel,
1238
+ showSlashMenu,
1239
+ slashMenuIndex,
1240
+ submitApiKey,
1241
+ submitPlanAnswers,
1242
+ copyTuiSelectionToHost,
1243
+ ]);
1244
+ useKeyboard(handleKey);
1245
+ const handlePaste = useCallback((event) => {
1246
+ if (!hasApiKeyRef.current) {
1247
+ event.preventDefault();
1248
+ openApiKeyModal();
1249
+ return;
1250
+ }
1251
+ const text = decodePasteBytes(event.bytes);
1252
+ const trimmed = text.trim();
1253
+ const imageExts = /\.(png|jpe?g|gif|webp|svg|bmp|ico|tiff?)$/i;
1254
+ if (imageExts.test(trimmed) && !trimmed.includes("\n")) {
1255
+ event.preventDefault();
1256
+ const id = ++pasteCounterRef.current;
1257
+ const imgNum = ++imageCounterRef.current;
1258
+ setPasteBlocks((prev) => [...prev, { id, content: trimmed, lines: 1, isImage: true }]);
1259
+ inputRef.current?.insertText(`[Image ${imgNum}]`);
1260
+ return;
1261
+ }
1262
+ const lineCount = text.split("\n").length;
1263
+ if (lineCount < 2)
1264
+ return;
1265
+ event.preventDefault();
1266
+ const id = ++pasteCounterRef.current;
1267
+ setPasteBlocks((prev) => [...prev, { id, content: text, lines: lineCount }]);
1268
+ inputRef.current?.insertText(`[Pasted ~${lineCount} lines]`);
1269
+ }, [openApiKeyModal]);
1270
+ const handleSubmit = useCallback(() => {
1271
+ const raw = inputRef.current?.plainText || "";
1272
+ if (!raw.trim() && pasteBlocks.length === 0) {
1273
+ if (queuedMessagesRef.current.length > 0 && isProcessingRef.current) {
1274
+ interruptedRunIdRef.current = activeRunIdRef.current;
1275
+ setStreamContent("");
1276
+ setStreamReasoning("");
1277
+ setActiveToolCalls([]);
1278
+ setActiveSubagent(null);
1279
+ agent.abort();
1280
+ }
1281
+ return;
1282
+ }
1283
+ inputRef.current?.clear();
1284
+ let message = raw;
1285
+ const blocks = [...pasteBlocks];
1286
+ let imgIdx = 0;
1287
+ setPasteBlocks([]);
1288
+ for (const block of blocks) {
1289
+ if (block.isImage) {
1290
+ imgIdx++;
1291
+ message = message.replace(`[Image ${imgIdx}]`, block.content);
1292
+ }
1293
+ else {
1294
+ message = message.replace(`[Pasted ~${block.lines} lines]`, block.content);
1295
+ }
1296
+ }
1297
+ if (!message.trim())
1298
+ return;
1299
+ if (!hasApiKeyRef.current) {
1300
+ openApiKeyModal();
1301
+ return;
1302
+ }
1303
+ if (handleCommand(message))
1304
+ return;
1305
+ if (isProcessingRef.current) {
1306
+ queuedMessagesRef.current.push(message.trim());
1307
+ setQueuedMessages([...queuedMessagesRef.current]);
1308
+ setTimeout(scrollToBottom, 10);
1309
+ return;
1310
+ }
1311
+ processMessage(message);
1312
+ }, [agent, handleCommand, openApiKeyModal, processMessage, pasteBlocks, scrollToBottom]);
1313
+ const hasMessages = messages.length > 0 || streamContent || isProcessing;
1314
+ return (
1315
+ // biome-ignore lint/a11y/noStaticElementInteractions: OpenCode-style copy-on-mouse-up on root surface
1316
+ _jsxs("box", { width: width, height: height, backgroundColor: t.background, flexDirection: "column", onMouseUp: handleRootMouseUp, children: [copyFlashId > 0 ? _jsx(CopyFlashBanner, { t: t, width: width }) : null, hasMessages ? (_jsxs("box", { flexGrow: 1, paddingBottom: 1, paddingTop: 1, paddingLeft: 2, paddingRight: 2, gap: 1, children: [_jsx(SessionHeader, { t: t, modeInfo: modeInfo, sessionTitle: sessionTitle, sessionId: sessionId }), _jsxs("scrollbox", { ref: scrollRef, flexGrow: 1, stickyScroll: true, stickyStart: "bottom", children: [messages.map((msg, i) => (
1317
+ // biome-ignore lint/suspicious/noArrayIndexKey: append-only message list without stable IDs
1318
+ _jsx(MessageView, { entry: msg, index: i, t: t, modeColor: modeInfo.color }, i))), activeToolCalls.map((tc) => tc.function.name === "task" ? (_jsx(SubagentTaskLine, { t: t, label: toolArgs(tc) || "Working", pending: true }, tc.id)) : tc.function.name === "delegate" ? (_jsx(DelegationTaskLine, { t: t, label: toolArgs(tc) || "Background research", pending: true, id: undefined }, tc.id)) : (_jsx(InlineTool, { t: t, pending: true, children: toolLabel(tc) }, tc.id))), activeSubagent && _jsx(SubagentActivity, { t: t, status: activeSubagent }), streamContent && (_jsx("box", { paddingLeft: 3, marginTop: 1, flexShrink: 0, children: _jsx(Markdown, { content: streamContent, t: t }) })), isProcessing && !streamContent && activeToolCalls.length === 0 && (_jsx(ShimmerText, { t: t, text: "Planning next moves" })), showPlanPanel && _jsx(PlanQuestionsPanel, { t: t, questions: planQuestions, state: pqs })] }), _jsx("box", { flexShrink: 0, children: _jsx(PromptBox, { t: t, inputRef: inputRef, isProcessing: isProcessing, showModelPicker: showModelPicker, showSlashMenu: showSlashMenu, showPlanQuestions: showPlanPanel, showApiKeyModal: showApiKeyModal, blockPrompt: blockPrompt, onSubmit: handleSubmit, onPaste: handlePaste, pasteBlocks: pasteBlocks, modeInfo: modeInfo, model: model, modelInfo: modelInfo, contextStats: contextStats, queuedCount: queuedMessages.length, queuedMessages: queuedMessages }) })] })) : (
1319
+ /* ── Home ───────────────────────────────────────── */
1320
+ _jsxs(_Fragment, { children: [_jsxs("box", { flexGrow: 1, alignItems: "center", paddingLeft: 2, paddingRight: 2, children: [_jsx("box", { flexGrow: 1, minHeight: 0 }), _jsx("box", { flexShrink: 0, alignItems: "center", children: _jsx(HeroLogo, { t: t }) }), _jsx("box", { height: 1, minHeight: 0, flexShrink: 1 }), _jsx("box", { width: "100%", maxWidth: 75, flexShrink: 0, children: _jsx(PromptBox, { t: t, inputRef: inputRef, isProcessing: isProcessing, showModelPicker: showModelPicker, showSlashMenu: showSlashMenu, showPlanQuestions: showPlanPanel, showApiKeyModal: showApiKeyModal, blockPrompt: blockPrompt, onSubmit: handleSubmit, onPaste: handlePaste, pasteBlocks: pasteBlocks, modeInfo: modeInfo, model: model, modelInfo: modelInfo, contextStats: contextStats, placeholder: "What are we building?" }) }), _jsx("box", { height: 2, minHeight: 0, flexShrink: 1 }), _jsx("box", { flexGrow: 1, minHeight: 0 })] }), _jsxs("box", { paddingLeft: 2, paddingRight: 2, paddingBottom: 1, flexDirection: "row", flexShrink: 0, children: [_jsx("text", { fg: t.textDim, children: agent.getCwd().replace(os.homedir(), "~") }), _jsx("box", { flexGrow: 1 }), _jsx("text", { fg: t.textDim, children: "v1.0.0" })] })] })), showApiKeyModal && (_jsx(ApiKeyModal, { t: t, width: width, height: height, inputRef: apiKeyInputRef, error: apiKeyError, onSubmit: submitApiKey })), showSlashMenu && (_jsx(SlashMenuModal, { t: t, selectedIndex: slashMenuIndex, width: width, height: height, searchQuery: slashSearchQuery, filteredItems: filteredSlashItems })), showModelPicker && (_jsx(ModelPickerModal, { t: t, currentModel: model, selectedIndex: modelPickerIndex, width: width, height: height, searchQuery: modelSearchQuery, filteredModels: filteredModels })), showConnectModal && (_jsx(ConnectModal, { t: t, width: width, height: height, selectedIndex: connectModalIndex, channels: CONNECT_CHANNELS })), showTelegramTokenModal && (_jsx(TelegramTokenModal, { t: t, width: width, height: height, inputRef: telegramTokenInputRef, error: telegramTokenError, onSubmit: submitTelegramToken })), showTelegramPairModal && (_jsx(TelegramPairModal, { t: t, width: width, height: height, inputRef: telegramPairInputRef, error: telegramPairError, onSubmit: () => void submitTelegramPair() }))] }));
1321
+ }
1322
+ /* ── Session Header ──────────────────────────────────────────── */
1323
+ function SessionHeader({ t, modeInfo, sessionTitle, sessionId, }) {
1324
+ return (_jsx("box", { flexShrink: 0, children: _jsx("box", { paddingTop: 1, paddingBottom: 1, paddingLeft: 2, paddingRight: 1, border: ["left"], customBorderChars: SPLIT, borderColor: t.border, backgroundColor: t.backgroundPanel, children: _jsxs("box", { flexDirection: "row", width: "100%", children: [_jsxs("text", { children: [_jsx("span", { style: { fg: modeInfo.color }, children: _jsx("b", { children: modeInfo.label }) }), sessionTitle ? (_jsx("span", { style: { fg: t.text }, children: _jsxs("b", { children: [": ", sessionTitle] }) })) : null] }), _jsx("box", { flexGrow: 1 }), sessionId ? _jsx("text", { fg: t.textDim, children: sessionId }) : null] }) }) }));
1325
+ }
1326
+ /* ── Prompt Box ──────────────────────────────────────────────── */
1327
+ const TEXTAREA_KEYBINDINGS = [
1328
+ { name: "return", action: "submit" },
1329
+ { name: "return", shift: true, action: "newline" },
1330
+ ];
1331
+ function formatTokenCount(tokens) {
1332
+ if (tokens >= 1_000_000)
1333
+ return `${(tokens / 1_000_000).toFixed(1).replace(/\.0$/, "")}M`;
1334
+ if (tokens >= 1_000)
1335
+ return `${Math.round(tokens / 1_000)}K`;
1336
+ return String(tokens);
1337
+ }
1338
+ function ContextMeter({ t, stats }) {
1339
+ return (_jsxs("text", { children: [_jsx("span", { style: { fg: t.textMuted }, children: `${Math.round(stats.ratioRemaining * 100)}%` }), _jsx("span", { style: { fg: t.textDim }, children: ` ${formatTokenCount(stats.remainingTokens)}` })] }));
1340
+ }
1341
+ function PromptBox({ t, inputRef, isProcessing, showModelPicker, showSlashMenu, showPlanQuestions, showApiKeyModal, blockPrompt, onSubmit, onPaste, pasteBlocks: _pasteBlocks, modeInfo, model, modelInfo, contextStats, placeholder, queuedCount, queuedMessages, }) {
1342
+ const hasQueue = (queuedMessages?.length ?? 0) > 0;
1343
+ return (_jsxs("box", { backgroundColor: t.backgroundPanel, children: [_jsxs("box", { children: [hasQueue && (_jsxs("box", { paddingLeft: 2, paddingRight: 2, paddingTop: 1, paddingBottom: 1, backgroundColor: t.queueBg, flexShrink: 0, children: [queuedMessages.map((msg, i) => (
1344
+ // biome-ignore lint/suspicious/noArrayIndexKey: append-only queue of plain strings
1345
+ _jsxs("text", { fg: t.text, children: ["→ ", msg] }, i))), _jsx("box", { height: 1 }), _jsxs("text", { children: [_jsx("span", { style: { fg: t.primary }, children: "enter " }), _jsx("span", { style: { fg: t.textMuted }, children: "send now" }), _jsx("span", { style: { fg: t.textDim }, children: " · " }), _jsx("span", { style: { fg: t.primary }, children: "↑ " }), _jsx("span", { style: { fg: t.textMuted }, children: "edit" }), _jsx("span", { style: { fg: t.textDim }, children: " · " }), _jsx("span", { style: { fg: t.primary }, children: "esc " }), _jsx("span", { style: { fg: t.textMuted }, children: "cancel" })] })] })), _jsxs("box", { paddingLeft: 2, paddingRight: 2, paddingTop: 1, paddingBottom: 1, backgroundColor: t.backgroundElement, flexDirection: "row", gap: 2, alignItems: "flex-start", flexShrink: 0, children: [_jsx("text", { fg: modeInfo.color, children: _jsx("b", { children: modeInfo.label }) }), _jsx("box", { flexGrow: 1, children: _jsx("textarea", { ref: inputRef, focused: !showModelPicker && !showSlashMenu && !showPlanQuestions && !showApiKeyModal && !blockPrompt, placeholder: isProcessing ? "Queue a follow-up... (esc to interrupt)" : placeholder || "Message Grok...", textColor: t.text, backgroundColor: t.backgroundElement, placeholderColor: t.textMuted, minHeight: 1, maxHeight: 10, wrapMode: "word", keyBindings: TEXTAREA_KEYBINDINGS, onSubmit: onSubmit, onPaste: onPaste }) })] })] }), _jsxs("box", { flexDirection: "row", justifyContent: "space-between", alignItems: "center", paddingLeft: 2, paddingRight: 2, height: 1, flexShrink: 0, children: [_jsxs("box", { flexDirection: "row", gap: 1, alignItems: "center", height: 1, children: [_jsx("text", { fg: t.text, children: modelInfo?.name || model }), contextStats ? _jsx(ContextMeter, { t: t, stats: contextStats }) : null] }), _jsx("box", { flexDirection: "row", gap: 3, alignItems: "center", height: 1, children: isProcessing ? (_jsxs("box", { flexDirection: "row", gap: 3, children: [_jsxs("text", { fg: t.text, children: ["enter ", _jsx("span", { style: { fg: t.textMuted }, children: "queue" })] }), _jsxs("text", { fg: t.text, children: ["esc ", _jsx("span", { style: { fg: t.textMuted }, children: (queuedCount ?? 0) > 0 ? "clear queue" : "interrupt" })] })] })) : (_jsxs(_Fragment, { children: [_jsxs("text", { fg: t.text, children: ["shift+enter ", _jsx("span", { style: { fg: t.textMuted }, children: "new line" })] }), _jsxs("text", { fg: t.text, children: ["tab ", _jsx("span", { style: { fg: t.textMuted }, children: "modes" })] })] })) })] })] }));
1346
+ }
1347
+ function CopyFlashBanner({ t, width }) {
1348
+ return (_jsx("box", { position: "absolute", left: 0, top: 1, width: width, zIndex: 500, alignItems: "center", flexShrink: 0, backgroundColor: t.background, shouldFill: false, children: _jsx("box", { height: 3, paddingLeft: 2, paddingRight: 2, backgroundColor: t.queueBg, justifyContent: "center", alignItems: "center", children: _jsxs("text", { children: [_jsx("span", { style: { fg: t.accent }, children: "✓ " }), _jsx("span", { style: { fg: t.text }, children: "Copied to clipboard" })] }) }) }));
1349
+ }
1350
+ function ApiKeyModal({ t, width, height, inputRef, error, onSubmit, }) {
1351
+ const overlayBg = "#000000cc";
1352
+ const panelWidth = Math.min(68, width - 6);
1353
+ const panelHeight = 13;
1354
+ const top = bottomAlignedModalTop(height, panelHeight);
1355
+ return (_jsx("box", { position: "absolute", left: 0, top: 0, width: width, height: height, alignItems: "center", paddingTop: top, backgroundColor: overlayBg, children: _jsxs("box", { width: panelWidth, height: panelHeight, backgroundColor: t.backgroundPanel, paddingTop: 1, paddingBottom: 1, flexDirection: "column", children: [_jsxs("box", { flexShrink: 0, flexDirection: "row", justifyContent: "space-between", paddingLeft: 2, paddingRight: 2, children: [_jsx("text", { fg: t.primary, children: _jsx("b", { children: "Add API key" }) }), _jsx("text", { fg: t.textMuted, children: "esc" })] }), _jsx("box", { paddingLeft: 2, paddingRight: 2, paddingTop: 1, children: _jsx("text", { fg: t.text, children: "Paste your xAI API key to unlock chat. You can hide this prompt with esc." }) }), _jsx("box", { paddingLeft: 2, paddingRight: 2, paddingTop: 1, children: _jsx("box", { backgroundColor: t.backgroundElement, paddingLeft: 1, paddingRight: 1, width: "100%", children: _jsx("textarea", { ref: inputRef, focused: true, placeholder: "xai-...", textColor: t.text, backgroundColor: t.backgroundElement, placeholderColor: t.textMuted, minHeight: 1, maxHeight: 3, wrapMode: "word", keyBindings: TEXTAREA_KEYBINDINGS, onSubmit: onSubmit }) }) }), _jsx("box", { flexGrow: 1, minHeight: 0 }), _jsx("box", { paddingLeft: 2, paddingRight: 2, paddingTop: 2, paddingBottom: 1, children: error ? (_jsx("text", { fg: t.diffRemovedFg, children: error })) : (_jsxs("text", { children: [_jsx("span", { style: { fg: t.primary }, children: "enter " }), _jsx("span", { style: { fg: t.textMuted }, children: "save key · " }), _jsx("span", { style: { fg: t.primary }, children: "esc " }), _jsx("span", { style: { fg: t.textMuted }, children: "hide" })] })) })] }) }));
1356
+ }
1357
+ /* ── Messages ────────────────────────────────────────────────── */
1358
+ function MessageView({ entry, index, t, modeColor }) {
1359
+ switch (entry.type) {
1360
+ case "user":
1361
+ return (_jsx("box", { border: ["left"], customBorderChars: SPLIT, borderColor: entry.modeColor || modeColor, marginTop: index === 0 ? 0 : 1, marginBottom: 1, children: _jsxs("box", { paddingTop: 1, paddingBottom: 1, paddingLeft: 2, backgroundColor: t.backgroundPanel, flexShrink: 0, flexDirection: "column", children: [entry.sourceLabel ? _jsx("text", { fg: t.textMuted, children: entry.sourceLabel }) : null, _jsx("text", { fg: t.text, children: entry.content })] }) }));
1362
+ case "assistant":
1363
+ return (_jsxs("box", { paddingLeft: 3, marginTop: 1, flexShrink: 0, flexDirection: "column", children: [entry.sourceLabel ? _jsx("text", { fg: t.textMuted, children: entry.sourceLabel }) : null, _jsx(Markdown, { content: entry.content, t: t })] }));
1364
+ case "tool_call":
1365
+ return (_jsx("box", { paddingLeft: 3, marginTop: 1, children: _jsxs("text", { children: [_jsx("span", { style: { fg: entry.modeColor || modeColor }, children: "▣ " }), _jsx("span", { style: { fg: t.textMuted }, children: entry.content.replace("▣ ", "") })] }) }));
1366
+ case "tool_result": {
1367
+ const name = entry.toolCall?.function.name || "tool";
1368
+ const args = toolArgs(entry.toolCall);
1369
+ const diff = entry.toolResult?.diff;
1370
+ const plan = entry.toolResult?.plan;
1371
+ if (name === "generate_plan" && plan) {
1372
+ return _jsx(PlanView, { plan: plan, t: t });
1373
+ }
1374
+ if (name === "task" && entry.toolResult?.task) {
1375
+ return _jsx(TaskResultView, { t: t, entry: entry });
1376
+ }
1377
+ if (name === "delegate" && entry.toolResult?.delegation) {
1378
+ return _jsx(DelegationResultView, { t: t, entry: entry });
1379
+ }
1380
+ if (name === "delegation_list") {
1381
+ return _jsx(DelegationListView, { t: t, content: entry.content });
1382
+ }
1383
+ if (name === "delegation_read") {
1384
+ return _jsx(ToolTextOutputView, { t: t, label: toolLabel(entry.toolCall), content: entry.content });
1385
+ }
1386
+ if (name === "write_file" || name === "edit_file") {
1387
+ const filePath = diff?.filePath || tryParseArg(entry.toolCall, "path") || args;
1388
+ const label = name === "write_file" ? `Write ${filePath}` : `Edit ${filePath}`;
1389
+ return (_jsxs("box", { gap: 0, children: [_jsx(InlineTool, { t: t, pending: false, children: label }), diff && _jsx(DiffView, { t: t, diff: diff })] }));
1390
+ }
1391
+ if (name === "bash" && entry.toolResult?.backgroundProcess) {
1392
+ const bp = entry.toolResult.backgroundProcess;
1393
+ return _jsx(BackgroundProcessLine, { t: t, id: bp.id, pid: bp.pid, command: bp.command });
1394
+ }
1395
+ if (name === "process_logs") {
1396
+ return _jsx(ProcessLogsView, { t: t, content: entry.content });
1397
+ }
1398
+ if (name === "process_stop" || name === "process_list") {
1399
+ return (_jsx(InlineTool, { t: t, pending: false, children: entry.content }));
1400
+ }
1401
+ if (name === "read_file")
1402
+ return (_jsx(InlineTool, { t: t, pending: false, children: `Read ${trunc(tryParseArg(entry.toolCall, "path") || args, 60)}` }));
1403
+ if (name === "search_web" || name === "search_x")
1404
+ return (_jsxs(InlineTool, { t: t, pending: false, children: [name === "search_web" ? "Web" : "X", ` Search "${trunc(args, 60)}"`] }));
1405
+ return (_jsx(InlineTool, { t: t, pending: false, children: trunc(name === "bash" ? args : `${name} ${args}`, 80) }));
1406
+ }
1407
+ default:
1408
+ return _jsx("text", { fg: t.textMuted, children: entry.content });
1409
+ }
1410
+ }
1411
+ const MAX_DIFF_ROWS = 20;
1412
+ const LINE_NUM_WIDTH = 4;
1413
+ function parsePatch(patch) {
1414
+ const lines = patch.split("\n");
1415
+ const rows = [];
1416
+ let oldLine = 0;
1417
+ let newLine = 0;
1418
+ let prevOldEnd = 0;
1419
+ for (const line of lines) {
1420
+ const hunkMatch = line.match(/^@@ -(\d+)(?:,\d+)? \+(\d+)(?:,\d+)? @@/);
1421
+ if (hunkMatch) {
1422
+ oldLine = parseInt(hunkMatch[1], 10);
1423
+ newLine = parseInt(hunkMatch[2], 10);
1424
+ const skipped = oldLine - prevOldEnd - 1;
1425
+ if (skipped > 0) {
1426
+ rows.push({ kind: "separator", count: skipped });
1427
+ }
1428
+ continue;
1429
+ }
1430
+ if (line.startsWith("---") || line.startsWith("+++") || line.startsWith("\\"))
1431
+ continue;
1432
+ if (line.startsWith("Index:") || line.startsWith("===="))
1433
+ continue;
1434
+ if (line.startsWith("-")) {
1435
+ rows.push({ kind: "removed", oldNum: oldLine, text: line.slice(1) });
1436
+ oldLine++;
1437
+ prevOldEnd = oldLine - 1;
1438
+ }
1439
+ else if (line.startsWith("+")) {
1440
+ rows.push({ kind: "added", newNum: newLine, text: line.slice(1) });
1441
+ newLine++;
1442
+ }
1443
+ else if (line.length > 0 || (oldLine > 0 && newLine > 0)) {
1444
+ const content = line.startsWith(" ") ? line.slice(1) : line;
1445
+ rows.push({ kind: "context", oldNum: oldLine, newNum: newLine, text: content });
1446
+ oldLine++;
1447
+ newLine++;
1448
+ prevOldEnd = oldLine - 1;
1449
+ }
1450
+ }
1451
+ return rows;
1452
+ }
1453
+ function DiffView({ t, diff }) {
1454
+ const rows = parsePatch(diff.patch);
1455
+ if (rows.length === 0)
1456
+ return null;
1457
+ const truncated = rows.length > MAX_DIFF_ROWS;
1458
+ const visible = truncated ? rows.slice(0, MAX_DIFF_ROWS) : rows;
1459
+ const pad = (n) => n !== undefined ? String(n).padStart(LINE_NUM_WIDTH) : " ".repeat(LINE_NUM_WIDTH);
1460
+ return (_jsx("box", { paddingLeft: 5, marginTop: 0, flexShrink: 0, children: _jsxs("box", { flexDirection: "column", children: [_jsx("box", { backgroundColor: t.diffHeader, paddingLeft: 1, paddingRight: 1, children: _jsxs("text", { children: [_jsx("span", { style: { fg: t.diffHeaderFg }, children: diff.filePath }), _jsx("span", { style: { fg: t.textDim }, children: " " }), _jsx("span", { style: { fg: t.diffRemovedFg }, children: `-${diff.removals}` }), _jsx("span", { style: { fg: t.textDim }, children: " " }), _jsx("span", { style: { fg: t.diffAddedFg }, children: `+${diff.additions}` })] }) }), visible.map((row, i) => {
1461
+ if (row.kind === "separator") {
1462
+ return (
1463
+ // biome-ignore lint/suspicious/noArrayIndexKey: separator rows lack unique identifiers
1464
+ _jsx("box", { backgroundColor: t.diffSeparator, paddingLeft: 1, children: _jsxs("text", { fg: t.diffSeparatorFg, children: ["⌃ ", row.count, " unmodified lines"] }) }, `sep-${i}`));
1465
+ }
1466
+ if (row.kind === "removed") {
1467
+ return (_jsxs("box", { backgroundColor: t.diffRemoved, flexDirection: "row", children: [_jsx("text", { fg: t.diffRemovedLineNum, children: pad(row.oldNum) }), _jsx("text", { fg: t.diffRemovedFg, children: ` ${row.text}` })] }, `rm-${row.oldNum}`));
1468
+ }
1469
+ if (row.kind === "added") {
1470
+ return (_jsxs("box", { backgroundColor: t.diffAdded, flexDirection: "row", children: [_jsx("text", { fg: t.diffAddedLineNum, children: pad(row.newNum) }), _jsx("text", { fg: t.diffAddedFg, children: ` ${row.text}` })] }, `add-${row.newNum}`));
1471
+ }
1472
+ return (_jsxs("box", { backgroundColor: t.diffContext, flexDirection: "row", children: [_jsx("text", { fg: t.diffLineNumber, children: pad(row.oldNum) }), _jsx("text", { fg: t.diffContextFg, children: ` ${row.text}` })] }, `ctx-${row.oldNum}`));
1473
+ }), truncated && (_jsx("box", { backgroundColor: t.diffSeparator, paddingLeft: 1, children: _jsxs("text", { fg: t.diffSeparatorFg, children: ["⌃ ", rows.length - MAX_DIFF_ROWS, " more lines"] }) }))] }) }));
1474
+ }
1475
+ function ShimmerText({ t, text }) {
1476
+ return (_jsx("box", { paddingLeft: 3, children: _jsxs("text", { children: [_jsx("span", { style: { fg: t.textMuted }, children: _jsx(LoadingSpinner, {}) }), _jsxs("span", { style: { fg: t.textMuted }, children: [" ", text] })] }) }));
1477
+ }
1478
+ function InlineTool({ t, pending: _pending, children }) {
1479
+ return (_jsx("box", { paddingLeft: 3, children: _jsxs("text", { fg: t.textMuted, children: ["→ ", children] }) }));
1480
+ }
1481
+ function SubagentTaskLine({ t, label, pending }) {
1482
+ const displayLabel = compactTaskLabel(label);
1483
+ return (_jsx("box", { paddingLeft: 3, children: _jsxs("text", { children: [pending ? (_jsx("span", { style: { fg: t.subagentAccent }, children: _jsx(LoadingSpinner, {}) })) : null, pending ? " " : "", _jsx("span", { style: { fg: t.subagentAccent }, children: _jsx("b", { children: `Sub-agent: ${displayLabel}` }) })] }) }));
1484
+ }
1485
+ function DelegationTaskLine({ t, label, pending, id }) {
1486
+ const displayLabel = compactTaskLabel(label);
1487
+ return (_jsx("box", { paddingLeft: 3, children: _jsxs("text", { children: [pending ? (_jsx("span", { style: { fg: t.subagentAccent }, children: _jsx(LoadingSpinner, {}) })) : (_jsx("span", { style: { fg: t.subagentAccent }, children: "◆" })), " ", _jsx("span", { style: { fg: t.subagentAccent }, children: _jsx("b", { children: "Background" }) }), _jsxs("span", { style: { fg: t.textMuted }, children: [" — ", displayLabel] }), id ? _jsx("span", { style: { fg: t.textDim }, children: ` (${id})` }) : null] }) }));
1488
+ }
1489
+ function LoadingSpinner() {
1490
+ const [frame, setFrame] = useState(0);
1491
+ useEffect(() => {
1492
+ const id = setInterval(() => setFrame((n) => (n + 1) % LOADING_SPINNER_FRAMES.length), 120);
1493
+ return () => clearInterval(id);
1494
+ }, []);
1495
+ return _jsx(_Fragment, { children: LOADING_SPINNER_FRAMES[frame] });
1496
+ }
1497
+ function SubagentActivity({ t, status }) {
1498
+ return (_jsx("box", { paddingLeft: 5, children: _jsxs("text", { fg: t.textMuted, children: ["→ ", truncateLine(status.detail, 100)] }) }));
1499
+ }
1500
+ function TaskResultView({ t, entry }) {
1501
+ const task = entry.toolResult?.task;
1502
+ if (!task)
1503
+ return null;
1504
+ return (_jsxs("box", { gap: 0, children: [_jsx(SubagentTaskLine, { t: t, label: task.description, pending: false }), _jsx("box", { paddingLeft: 5, children: _jsxs("text", { fg: t.text, children: [task.agent, ": ", truncateLine(task.summary, 90)] }) })] }));
1505
+ }
1506
+ function DelegationResultView({ t, entry }) {
1507
+ const delegation = entry.toolResult?.delegation;
1508
+ if (!delegation)
1509
+ return null;
1510
+ return _jsx(DelegationTaskLine, { t: t, label: delegation.description, pending: false, id: delegation.id });
1511
+ }
1512
+ function DelegationListView({ t, content }) {
1513
+ const items = parseDelegationList(content);
1514
+ if (items.length === 0) {
1515
+ return (_jsx(InlineTool, { t: t, pending: false, children: "No background delegations" }));
1516
+ }
1517
+ return (_jsx("box", { paddingLeft: 3, gap: 0, children: items.map((item) => {
1518
+ const statusColor = item.status === "complete"
1519
+ ? "#8adf8a"
1520
+ : item.status === "running"
1521
+ ? t.subagentAccent
1522
+ : item.status === "error"
1523
+ ? "#df8a8a"
1524
+ : t.textMuted;
1525
+ return (_jsx("box", { children: _jsxs("text", { children: [_jsx("span", { style: { fg: statusColor }, children: "◆ " }), _jsx("span", { style: { fg: t.text }, children: item.id }), _jsx("span", { style: { fg: statusColor }, children: ` ${item.status}` }), _jsxs("span", { style: { fg: t.textMuted }, children: [" — ", truncateLine(item.label, 60)] })] }) }, item.id));
1526
+ }) }));
1527
+ }
1528
+ function parseDelegationList(content) {
1529
+ const items = [];
1530
+ for (const line of content.split("\n")) {
1531
+ const match = line.match(/`([^`]+)`\s+\[(\w+)]\s+(.*)/);
1532
+ if (match) {
1533
+ items.push({ id: match[1], status: match[2], label: match[3].trim() });
1534
+ }
1535
+ }
1536
+ return items;
1537
+ }
1538
+ function BackgroundProcessLine({ t, id, pid, command }) {
1539
+ return (_jsx("box", { paddingLeft: 3, children: _jsxs("text", { children: [_jsx("span", { style: { fg: t.subagentAccent }, children: "◆ " }), _jsx("span", { style: { fg: t.subagentAccent }, children: _jsx("b", { children: "Background process" }) }), _jsx("span", { style: { fg: t.textMuted }, children: ` id:${id} pid:${pid}` }), _jsxs("span", { style: { fg: t.textDim }, children: [" — ", truncateLine(command, 60)] })] }) }));
1540
+ }
1541
+ function ProcessLogsView({ t, content }) {
1542
+ const lines = content.split("\n");
1543
+ const header = lines[0] || "";
1544
+ const body = lines.slice(1).join("\n").trim();
1545
+ return (_jsxs("box", { paddingLeft: 3, gap: 0, children: [_jsxs("text", { fg: t.textMuted, children: ["→ ", header] }), body ? (_jsx("box", { paddingLeft: 2, marginTop: 0, children: _jsx("box", { backgroundColor: t.mdCodeBlockBg, paddingLeft: 1, paddingRight: 1, children: _jsx("text", { fg: t.mdCodeBlockFg, children: truncateBlock(body, 15) }) }) })) : null] }));
1546
+ }
1547
+ function truncateBlock(text, maxLines) {
1548
+ const lines = text.split("\n");
1549
+ if (lines.length <= maxLines)
1550
+ return text;
1551
+ return [...lines.slice(0, maxLines), `… ${lines.length - maxLines} more lines`].join("\n");
1552
+ }
1553
+ function ToolTextOutputView({ t, label, content }) {
1554
+ return (_jsxs("box", { gap: 0, children: [_jsx(InlineTool, { t: t, pending: false, children: label }), _jsx("box", { paddingLeft: 5, marginTop: 1, flexShrink: 0, children: _jsx(Markdown, { content: content, t: t }) })] }));
1555
+ }
1556
+ /* ── Slash Menu ──────────────────────────────────────────────── */
1557
+ function bottomAlignedModalTop(height, panelHeight) {
1558
+ return Math.max(2, Math.floor((height - panelHeight) / 2));
1559
+ }
1560
+ function SlashMenuModal({ t, selectedIndex, width, height, searchQuery, filteredItems, }) {
1561
+ const listRef = useRef(null);
1562
+ useEffect(() => {
1563
+ const item = filteredItems[selectedIndex];
1564
+ if (item)
1565
+ listRef.current?.scrollChildIntoView(`slash-${item.id}`);
1566
+ }, [selectedIndex, filteredItems]);
1567
+ const itemCount = Math.max(filteredItems.length, 1);
1568
+ const contentHeight = itemCount + 5;
1569
+ const maxH = Math.floor(height * 0.6);
1570
+ const panelHeight = Math.min(contentHeight, maxH);
1571
+ const top = bottomAlignedModalTop(height, panelHeight);
1572
+ const overlayBg = "#000000cc";
1573
+ return (_jsx("box", { position: "absolute", left: 0, top: 0, width: width, height: height, alignItems: "center", paddingTop: top, backgroundColor: overlayBg, children: _jsxs("box", { width: Math.min(50, width - 6), height: panelHeight, backgroundColor: t.backgroundPanel, paddingTop: 1, paddingBottom: 1, flexDirection: "column", children: [_jsxs("box", { flexShrink: 0, flexDirection: "row", justifyContent: "space-between", paddingLeft: 2, paddingRight: 2, children: [_jsx("text", { fg: t.primary, children: _jsx("b", { children: "Commands" }) }), _jsx("text", { fg: t.textMuted, children: "esc" })] }), _jsx("box", { flexShrink: 0, paddingLeft: 2, paddingRight: 2, paddingTop: 1, paddingBottom: 1, children: _jsx("text", { fg: t.text, children: searchQuery || _jsx("span", { style: { fg: t.textMuted }, children: "Search..." }) }) }), _jsxs("scrollbox", { ref: listRef, flexGrow: 1, minHeight: 0, children: [filteredItems.map((item, idx) => (_jsx("box", { id: `slash-${item.id}`, backgroundColor: idx === selectedIndex ? t.selectedBg : undefined, paddingLeft: 2, paddingRight: 2, children: _jsxs("box", { flexDirection: "row", justifyContent: "space-between", children: [_jsxs("text", { fg: idx === selectedIndex ? t.selected : t.text, children: ["/", item.label] }), _jsx("text", { fg: t.textMuted, children: item.description })] }) }, item.id))), filteredItems.length === 0 && (_jsx("box", { paddingLeft: 2, children: _jsx("text", { fg: t.textMuted, children: "No commands match your search" }) }))] })] }) }));
1574
+ }
1575
+ function ConnectModal({ t, width, height, selectedIndex, channels, }) {
1576
+ const listRef = useRef(null);
1577
+ useEffect(() => {
1578
+ const ch = channels[selectedIndex];
1579
+ if (ch)
1580
+ listRef.current?.scrollChildIntoView(`connect-${ch.id}`);
1581
+ }, [selectedIndex, channels]);
1582
+ const panelHeight = Math.min(channels.length + 9, Math.floor(height * 0.5));
1583
+ const top = bottomAlignedModalTop(height, panelHeight);
1584
+ const overlayBg = "#000000cc";
1585
+ return (_jsx("box", { position: "absolute", left: 0, top: 0, width: width, height: height, alignItems: "center", paddingTop: top, backgroundColor: overlayBg, children: _jsxs("box", { width: Math.min(56, width - 6), height: panelHeight, backgroundColor: t.backgroundPanel, paddingTop: 1, paddingBottom: 1, flexDirection: "column", children: [_jsxs("box", { flexShrink: 0, flexDirection: "row", justifyContent: "space-between", paddingLeft: 2, paddingRight: 2, children: [_jsx("text", { fg: t.primary, children: _jsx("b", { children: "Connect" }) }), _jsx("text", { fg: t.textMuted, children: "esc" })] }), _jsx("box", { flexShrink: 0, paddingLeft: 2, paddingRight: 2, paddingTop: 1, paddingBottom: 1, children: _jsx("text", { fg: t.textMuted, children: "Choose a channel" }) }), _jsx("scrollbox", { ref: listRef, flexGrow: 1, minHeight: 0, children: channels.map((ch, idx) => (_jsx("box", { id: `connect-${ch.id}`, backgroundColor: idx === selectedIndex ? t.selectedBg : undefined, paddingLeft: 2, paddingRight: 2, children: _jsxs("box", { flexDirection: "row", justifyContent: "space-between", children: [_jsx("text", { fg: idx === selectedIndex ? t.selected : t.text, children: ch.label }), _jsx("text", { fg: t.textMuted, children: ch.description })] }) }, ch.id))) }), _jsx("box", { flexShrink: 0, paddingLeft: 2, paddingRight: 2, paddingTop: 2, paddingBottom: 1, children: _jsxs("text", { children: [_jsx("span", { style: { fg: t.primary }, children: "enter " }), _jsx("span", { style: { fg: t.textMuted }, children: "select · " }), _jsx("span", { style: { fg: t.primary }, children: "↑↓ " }), _jsx("span", { style: { fg: t.textMuted }, children: "navigate · " }), _jsx("span", { style: { fg: t.primary }, children: "esc " }), _jsx("span", { style: { fg: t.textMuted }, children: "close" })] }) })] }) }));
1586
+ }
1587
+ function TelegramTokenModal({ t, width, height, inputRef, error, onSubmit, }) {
1588
+ const overlayBg = "#000000cc";
1589
+ const panelWidth = Math.min(68, width - 6);
1590
+ const panelHeight = 14;
1591
+ const top = bottomAlignedModalTop(height, panelHeight);
1592
+ return (_jsx("box", { position: "absolute", left: 0, top: 0, width: width, height: height, alignItems: "center", paddingTop: top, backgroundColor: overlayBg, children: _jsxs("box", { width: panelWidth, height: panelHeight, backgroundColor: t.backgroundPanel, paddingTop: 1, paddingBottom: 1, flexDirection: "column", children: [_jsxs("box", { flexShrink: 0, flexDirection: "row", justifyContent: "space-between", paddingLeft: 2, paddingRight: 2, children: [_jsx("text", { fg: t.primary, children: _jsx("b", { children: "Telegram bot token" }) }), _jsx("text", { fg: t.textMuted, children: "esc" })] }), _jsx("box", { paddingLeft: 2, paddingRight: 2, paddingTop: 1, children: _jsx("text", { fg: t.text, children: "From @BotFather: /newbot, then paste the token here. Stored in ~/.grok/user-settings.json." }) }), _jsx("box", { paddingLeft: 2, paddingRight: 2, paddingTop: 1, children: _jsx("box", { backgroundColor: t.backgroundElement, paddingLeft: 1, paddingRight: 1, width: "100%", children: _jsx("textarea", { ref: inputRef, focused: true, placeholder: "123456:ABC...", textColor: t.text, backgroundColor: t.backgroundElement, placeholderColor: t.textMuted, minHeight: 1, maxHeight: 3, wrapMode: "word", keyBindings: TEXTAREA_KEYBINDINGS, onSubmit: onSubmit }) }) }), _jsx("box", { flexGrow: 1, minHeight: 0 }), _jsx("box", { paddingLeft: 2, paddingRight: 2, paddingTop: 2, paddingBottom: 1, children: error ? (_jsx("text", { fg: t.diffRemovedFg, children: error })) : (_jsxs("text", { children: [_jsx("span", { style: { fg: t.primary }, children: "enter " }), _jsx("span", { style: { fg: t.textMuted }, children: "save token · " }), _jsx("span", { style: { fg: t.primary }, children: "esc " }), _jsx("span", { style: { fg: t.textMuted }, children: "close" })] })) })] }) }));
1593
+ }
1594
+ function TelegramPairModal({ t, width, height, inputRef, error, onSubmit, }) {
1595
+ const overlayBg = "#000000cc";
1596
+ const panelWidth = Math.min(68, width - 6);
1597
+ const panelHeight = 13;
1598
+ const top = bottomAlignedModalTop(height, panelHeight);
1599
+ return (_jsx("box", { position: "absolute", left: 0, top: 0, width: width, height: height, alignItems: "center", paddingTop: top, backgroundColor: overlayBg, children: _jsxs("box", { width: panelWidth, height: panelHeight, backgroundColor: t.backgroundPanel, paddingTop: 1, paddingBottom: 1, flexDirection: "column", children: [_jsxs("box", { flexShrink: 0, flexDirection: "row", justifyContent: "space-between", paddingLeft: 2, paddingRight: 2, children: [_jsx("text", { fg: t.primary, children: _jsx("b", { children: "Pairing code" }) }), _jsx("text", { fg: t.textMuted, children: "esc" })] }), _jsx("box", { paddingLeft: 2, paddingRight: 2, paddingTop: 1, children: _jsx("text", { fg: t.text, children: "DM your bot with /pair, then paste the 6-character code." }) }), _jsx("box", { paddingLeft: 2, paddingRight: 2, paddingTop: 1, children: _jsx("box", { backgroundColor: t.backgroundElement, paddingLeft: 1, paddingRight: 1, width: "100%", children: _jsx("textarea", { ref: inputRef, focused: true, placeholder: "ABC123", textColor: t.text, backgroundColor: t.backgroundElement, placeholderColor: t.textMuted, minHeight: 1, maxHeight: 2, wrapMode: "word", keyBindings: TEXTAREA_KEYBINDINGS, onSubmit: onSubmit }) }) }), _jsx("box", { flexGrow: 1, minHeight: 0 }), _jsx("box", { paddingLeft: 2, paddingRight: 2, paddingTop: 2, paddingBottom: 1, children: error ? (_jsx("text", { fg: t.diffRemovedFg, children: error })) : (_jsxs("text", { children: [_jsx("span", { style: { fg: t.primary }, children: "enter " }), _jsx("span", { style: { fg: t.textMuted }, children: "approve pairing · " }), _jsx("span", { style: { fg: t.primary }, children: "esc " }), _jsx("span", { style: { fg: t.textMuted }, children: "close" })] })) })] }) }));
1600
+ }
1601
+ /* ── Model Picker ────────────────────────────────────────────── */
1602
+ function ModelPickerModal({ t, currentModel, selectedIndex, width, height, searchQuery, filteredModels, }) {
1603
+ const listRef = useRef(null);
1604
+ useEffect(() => {
1605
+ const m = filteredModels[selectedIndex];
1606
+ if (m)
1607
+ listRef.current?.scrollChildIntoView(`model-${m.id}`);
1608
+ }, [selectedIndex, filteredModels]);
1609
+ const itemCount = Math.max(filteredModels.length, 1);
1610
+ const contentHeight = itemCount + 5;
1611
+ const maxH = Math.floor(height * 0.6);
1612
+ const panelHeight = Math.min(contentHeight, maxH);
1613
+ const top = bottomAlignedModalTop(height, panelHeight);
1614
+ const overlayBg = "#000000cc";
1615
+ return (_jsx("box", { position: "absolute", left: 0, top: 0, width: width, height: height, alignItems: "center", paddingTop: top, backgroundColor: overlayBg, children: _jsxs("box", { width: Math.min(60, width - 6), height: panelHeight, backgroundColor: t.backgroundPanel, paddingTop: 1, paddingBottom: 1, flexDirection: "column", children: [_jsxs("box", { flexShrink: 0, flexDirection: "row", justifyContent: "space-between", paddingLeft: 2, paddingRight: 2, children: [_jsx("text", { fg: t.primary, children: _jsx("b", { children: "Select model" }) }), _jsx("text", { fg: t.textMuted, children: "esc" })] }), _jsx("box", { flexShrink: 0, paddingLeft: 2, paddingRight: 2, paddingTop: 1, paddingBottom: 1, children: _jsx("text", { fg: t.text, children: searchQuery || _jsx("span", { style: { fg: t.textMuted }, children: "Search..." }) }) }), _jsxs("scrollbox", { ref: listRef, flexGrow: 1, minHeight: 0, children: [filteredModels.map((m, idx) => {
1616
+ const selected = idx === selectedIndex;
1617
+ const current = m.id === currentModel;
1618
+ return (_jsx("box", { id: `model-${m.id}`, backgroundColor: selected ? t.selectedBg : undefined, paddingLeft: 2, paddingRight: 2, children: _jsx("text", { fg: current ? t.accent : selected ? t.selected : t.text, children: m.name }) }, m.id));
1619
+ }), filteredModels.length === 0 && (_jsx("box", { paddingLeft: 2, children: _jsx("text", { fg: t.textMuted, children: "No models match your search" }) }))] })] }) }));
1620
+ }
1621
+ /* ── Helpers ──────────────────────────────────────────────────── */
1622
+ function toolArgs(tc) {
1623
+ if (!tc)
1624
+ return "";
1625
+ try {
1626
+ const a = JSON.parse(tc.function.arguments);
1627
+ if (tc.function.name === "bash")
1628
+ return a.command || "";
1629
+ if (tc.function.name === "read_file" || tc.function.name === "write_file" || tc.function.name === "edit_file")
1630
+ return a.path || "";
1631
+ if (tc.function.name === "task")
1632
+ return a.description || "";
1633
+ if (tc.function.name === "delegate")
1634
+ return a.description || "";
1635
+ if (tc.function.name === "delegation_read")
1636
+ return a.id || "";
1637
+ if (tc.function.name === "process_logs" || tc.function.name === "process_stop")
1638
+ return a.id != null ? String(a.id) : "";
1639
+ return a.query || "";
1640
+ }
1641
+ catch {
1642
+ return "";
1643
+ }
1644
+ }
1645
+ function tryParseArg(tc, key) {
1646
+ if (!tc)
1647
+ return "";
1648
+ try {
1649
+ return JSON.parse(tc.function.arguments)[key] || "";
1650
+ }
1651
+ catch {
1652
+ return "";
1653
+ }
1654
+ }
1655
+ function toolLabel(tc) {
1656
+ const args = toolArgs(tc);
1657
+ if (tc.function.name === "bash") {
1658
+ try {
1659
+ const parsed = JSON.parse(tc.function.arguments);
1660
+ if (parsed.background)
1661
+ return `Background: ${trunc(args || "Starting process...", 70)}`;
1662
+ }
1663
+ catch {
1664
+ /* */
1665
+ }
1666
+ return trunc(args || "Running command...", 80);
1667
+ }
1668
+ if (tc.function.name === "read_file")
1669
+ return `Read ${trunc(args, 60)}`;
1670
+ if (tc.function.name === "write_file")
1671
+ return `Write ${trunc(args, 60)}`;
1672
+ if (tc.function.name === "edit_file")
1673
+ return `Edit ${trunc(args, 60)}`;
1674
+ if (tc.function.name === "search_web")
1675
+ return `Web Search "${trunc(args, 60)}"`;
1676
+ if (tc.function.name === "search_x")
1677
+ return `X Search "${trunc(args, 60)}"`;
1678
+ if (tc.function.name === "task")
1679
+ return `Task ${trunc(args, 60)}`;
1680
+ if (tc.function.name === "delegate")
1681
+ return `Background ${trunc(args, 60)}`;
1682
+ if (tc.function.name === "delegation_read")
1683
+ return `Read delegation ${trunc(args, 60)}`;
1684
+ if (tc.function.name === "delegation_list")
1685
+ return "List delegations";
1686
+ if (tc.function.name === "process_logs")
1687
+ return `Logs for process ${args}`;
1688
+ if (tc.function.name === "process_stop")
1689
+ return `Stop process ${args}`;
1690
+ if (tc.function.name === "process_list")
1691
+ return "List processes";
1692
+ if (tc.function.name === "generate_plan")
1693
+ return "Generating plan...";
1694
+ return trunc(`${tc.function.name} ${args}`, 80);
1695
+ }
1696
+ function sanitizeContent(raw) {
1697
+ let s = raw.replace(/^[\s\n]*assistant:\s*/gi, "");
1698
+ s = s.replace(/\{"success"\s*:\s*(true|false)\s*,\s*"output"\s*:\s*"[\s\S]*$/m, "");
1699
+ return s.trim();
1700
+ }
1701
+ function shouldOpenApiKeyModalForKey(key) {
1702
+ if (key.ctrl || key.meta)
1703
+ return false;
1704
+ if (key.name === "return" || key.name === "backspace")
1705
+ return true;
1706
+ return !!(key.sequence && key.sequence.length === 1);
1707
+ }
1708
+ function compactTaskLabel(label) {
1709
+ const words = label.trim().split(/\s+/).filter(Boolean);
1710
+ if (words.length <= 3)
1711
+ return label.trim() || "Working";
1712
+ return `${words.slice(0, 3).join(" ")}...`;
1713
+ }
1714
+ function trunc(s, n) {
1715
+ return s.length <= n ? s : `${s.slice(0, n)}…`;
1716
+ }
1717
+ function truncateLine(s, n) {
1718
+ return trunc(s.replace(/\s+/g, " ").trim(), n);
1719
+ }
1720
+ //# sourceMappingURL=app.js.map