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.
- package/.cursor/rules/development-workflow.mdc +66 -0
- package/.cursor/rules/project-overview.mdc +66 -0
- package/.cursor/rules/react-ink-components.mdc +45 -0
- package/.cursor/rules/tools-and-agent.mdc +62 -0
- package/.cursor/rules/typescript-conventions.mdc +54 -0
- package/.grok/settings.json +3 -0
- package/.husky/pre-commit +1 -0
- package/LICENSE +21 -0
- package/README.md +526 -0
- package/biome.json +51 -0
- package/dist/agent/agent.d.ts +62 -0
- package/dist/agent/agent.js +701 -0
- package/dist/agent/agent.js.map +1 -0
- package/dist/agent/delegations.d.ts +42 -0
- package/dist/agent/delegations.js +273 -0
- package/dist/agent/delegations.js.map +1 -0
- package/dist/grok/client.d.ts +12 -0
- package/dist/grok/client.js +37 -0
- package/dist/grok/client.js.map +1 -0
- package/dist/grok/models.d.ts +5 -0
- package/dist/grok/models.js +73 -0
- package/dist/grok/models.js.map +1 -0
- package/dist/grok/tools.d.ts +12 -0
- package/dist/grok/tools.js +230 -0
- package/dist/grok/tools.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +192 -0
- package/dist/index.js.map +1 -0
- package/dist/storage/db.d.ts +18 -0
- package/dist/storage/db.js +71 -0
- package/dist/storage/db.js.map +1 -0
- package/dist/storage/index.d.ts +4 -0
- package/dist/storage/index.js +5 -0
- package/dist/storage/index.js.map +1 -0
- package/dist/storage/migrations.d.ts +2 -0
- package/dist/storage/migrations.js +92 -0
- package/dist/storage/migrations.js.map +1 -0
- package/dist/storage/sessions.d.ts +15 -0
- package/dist/storage/sessions.js +135 -0
- package/dist/storage/sessions.js.map +1 -0
- package/dist/storage/transcript.d.ts +6 -0
- package/dist/storage/transcript.js +261 -0
- package/dist/storage/transcript.js.map +1 -0
- package/dist/storage/usage.d.ts +9 -0
- package/dist/storage/usage.js +58 -0
- package/dist/storage/usage.js.map +1 -0
- package/dist/storage/workspaces.d.ts +9 -0
- package/dist/storage/workspaces.js +60 -0
- package/dist/storage/workspaces.js.map +1 -0
- package/dist/telegram/bridge.d.ts +39 -0
- package/dist/telegram/bridge.js +164 -0
- package/dist/telegram/bridge.js.map +1 -0
- package/dist/telegram/index.d.ts +3 -0
- package/dist/telegram/index.js +4 -0
- package/dist/telegram/index.js.map +1 -0
- package/dist/telegram/limits.d.ts +3 -0
- package/dist/telegram/limits.js +12 -0
- package/dist/telegram/limits.js.map +1 -0
- package/dist/telegram/pairing.d.ts +9 -0
- package/dist/telegram/pairing.js +30 -0
- package/dist/telegram/pairing.js.map +1 -0
- package/dist/telegram/preview-stream.d.ts +22 -0
- package/dist/telegram/preview-stream.js +181 -0
- package/dist/telegram/preview-stream.js.map +1 -0
- package/dist/telegram/turn-coordinator.d.ts +7 -0
- package/dist/telegram/turn-coordinator.js +14 -0
- package/dist/telegram/turn-coordinator.js.map +1 -0
- package/dist/telegram/typing-refresh.d.ts +3 -0
- package/dist/telegram/typing-refresh.js +12 -0
- package/dist/telegram/typing-refresh.js.map +1 -0
- package/dist/tools/bash.d.ts +27 -0
- package/dist/tools/bash.js +261 -0
- package/dist/tools/bash.js.map +1 -0
- package/dist/tools/file.d.ts +15 -0
- package/dist/tools/file.js +94 -0
- package/dist/tools/file.js.map +1 -0
- package/dist/types/index.d.ts +151 -0
- package/dist/types/index.js +6 -0
- package/dist/types/index.js.map +1 -0
- package/dist/ui/app.d.ts +15 -0
- package/dist/ui/app.js +1720 -0
- package/dist/ui/app.js.map +1 -0
- package/dist/ui/markdown.d.ts +5 -0
- package/dist/ui/markdown.js +38 -0
- package/dist/ui/markdown.js.map +1 -0
- package/dist/ui/plan.d.ts +24 -0
- package/dist/ui/plan.js +122 -0
- package/dist/ui/plan.js.map +1 -0
- package/dist/ui/terminal-selection-text.d.ts +23 -0
- package/dist/ui/terminal-selection-text.js +59 -0
- package/dist/ui/terminal-selection-text.js.map +1 -0
- package/dist/ui/theme.d.ts +53 -0
- package/dist/ui/theme.js +53 -0
- package/dist/ui/theme.js.map +1 -0
- package/dist/utils/git-root.d.ts +1 -0
- package/dist/utils/git-root.js +16 -0
- package/dist/utils/git-root.js.map +1 -0
- package/dist/utils/host-clipboard.d.ts +4 -0
- package/dist/utils/host-clipboard.js +32 -0
- package/dist/utils/host-clipboard.js.map +1 -0
- package/dist/utils/instructions.d.ts +1 -0
- package/dist/utils/instructions.js +73 -0
- package/dist/utils/instructions.js.map +1 -0
- package/dist/utils/settings.d.ts +33 -0
- package/dist/utils/settings.js +88 -0
- package/dist/utils/settings.js.map +1 -0
- package/dist/utils/skills.d.ts +17 -0
- package/dist/utils/skills.js +161 -0
- package/dist/utils/skills.js.map +1 -0
- package/package.json +67 -0
- package/skills-lock.json +10 -0
- 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
|