@cdoing/opentuicli 0.1.21 → 0.1.26
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +22 -13
- package/dist/cdoing-tui-darwin-arm64/bin/cdoing-tui +0 -0
- package/package.json +12 -9
- package/dist/index.js +0 -64
- package/dist/index.js.map +0 -7
- package/esbuild.config.cjs +0 -45
- package/src/app.tsx +0 -787
- package/src/components/dialog-command.tsx +0 -207
- package/src/components/dialog-help.tsx +0 -151
- package/src/components/dialog-model.tsx +0 -142
- package/src/components/dialog-status.tsx +0 -84
- package/src/components/dialog-theme.tsx +0 -318
- package/src/components/input-area.tsx +0 -380
- package/src/components/loading-spinner.tsx +0 -28
- package/src/components/message-list.tsx +0 -546
- package/src/components/permission-prompt.tsx +0 -72
- package/src/components/session-browser.tsx +0 -231
- package/src/components/session-footer.tsx +0 -30
- package/src/components/session-header.tsx +0 -39
- package/src/components/setup-wizard.tsx +0 -542
- package/src/components/sidebar.tsx +0 -183
- package/src/components/status-bar.tsx +0 -76
- package/src/components/toast.tsx +0 -139
- package/src/context/sdk.tsx +0 -40
- package/src/context/theme.tsx +0 -640
- package/src/index.ts +0 -50
- package/src/lib/autocomplete.ts +0 -262
- package/src/lib/context-providers.ts +0 -98
- package/src/lib/history.ts +0 -164
- package/src/lib/terminal-title.ts +0 -15
- package/src/routes/home.tsx +0 -148
- package/src/routes/session.tsx +0 -1309
- package/src/store/settings.ts +0 -107
- package/tsconfig.json +0 -23
package/src/routes/session.tsx
DELETED
|
@@ -1,1309 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Session Route — active chat session with full agent integration
|
|
3
|
-
*
|
|
4
|
-
* Wires up the AgentRunner with streaming callbacks to display:
|
|
5
|
-
* - Token-by-token streaming with cursor
|
|
6
|
-
* - Tool call display with status icons
|
|
7
|
-
* - Permission prompts (via SDK context)
|
|
8
|
-
* - Token usage tracking
|
|
9
|
-
* - Full slash command handling
|
|
10
|
-
* - Session persistence (save/resume/fork/delete)
|
|
11
|
-
* - @mention context expansion
|
|
12
|
-
* - Background jobs (/bg, /jobs)
|
|
13
|
-
* - One-shot questions (/btw)
|
|
14
|
-
* - Shell command auto-detection
|
|
15
|
-
* - OAuth status (/auth-status)
|
|
16
|
-
* - /config set support
|
|
17
|
-
*/
|
|
18
|
-
|
|
19
|
-
import { useState, useRef, useEffect } from "react";
|
|
20
|
-
import { TextAttributes } from "@opentui/core";
|
|
21
|
-
import { useKeyboard } from "@opentui/react";
|
|
22
|
-
import * as fs from "fs";
|
|
23
|
-
import * as path from "path";
|
|
24
|
-
import * as os from "os";
|
|
25
|
-
import { MessageList, type Message } from "../components/message-list";
|
|
26
|
-
import { InputArea, type AgentMode } from "../components/input-area";
|
|
27
|
-
import { PermissionPrompt } from "../components/permission-prompt";
|
|
28
|
-
import { LoadingSpinner } from "../components/loading-spinner";
|
|
29
|
-
import { useSDK } from "../context/sdk";
|
|
30
|
-
import { useTheme } from "../context/theme";
|
|
31
|
-
import { useToast } from "../components/toast";
|
|
32
|
-
import { setTerminalTitle } from "../lib/terminal-title";
|
|
33
|
-
import { execSync } from "child_process";
|
|
34
|
-
import type { AgentCallbacks, ImageAttachment } from "@cdoing/ai";
|
|
35
|
-
import {
|
|
36
|
-
createConversation,
|
|
37
|
-
addMessage as addHistoryMessage,
|
|
38
|
-
listConversations,
|
|
39
|
-
loadConversation,
|
|
40
|
-
deleteConversation,
|
|
41
|
-
forkConversation,
|
|
42
|
-
formatRelativeDate,
|
|
43
|
-
type Conversation,
|
|
44
|
-
} from "../lib/history";
|
|
45
|
-
import {
|
|
46
|
-
resolveContextProviders,
|
|
47
|
-
hasContextMentions,
|
|
48
|
-
pushTerminalOutput,
|
|
49
|
-
} from "../lib/context-providers";
|
|
50
|
-
|
|
51
|
-
// ── Shell Command Detection ──────────────────────────
|
|
52
|
-
|
|
53
|
-
const SHELL_COMMANDS = new Set([
|
|
54
|
-
"ls", "ll", "la", "pwd", "cd", "mkdir", "rmdir", "rm", "cp", "mv",
|
|
55
|
-
"cat", "head", "tail", "touch", "echo", "env",
|
|
56
|
-
"git", "npm", "yarn", "pnpm", "npx", "node", "ts-node",
|
|
57
|
-
"python", "python3", "pip", "pip3",
|
|
58
|
-
"docker", "docker-compose",
|
|
59
|
-
"grep", "find", "which", "whereis",
|
|
60
|
-
"curl", "wget",
|
|
61
|
-
"chmod", "chown", "ln",
|
|
62
|
-
"ps", "kill", "df", "du",
|
|
63
|
-
"open", "code",
|
|
64
|
-
"vim", "vi", "nano", "less", "more", "man", "top", "htop",
|
|
65
|
-
]);
|
|
66
|
-
|
|
67
|
-
function detectShellCommand(input: string): string | null {
|
|
68
|
-
const trimmed = input.trim();
|
|
69
|
-
const firstWord = trimmed.split(/\s+/)[0].toLowerCase();
|
|
70
|
-
return SHELL_COMMANDS.has(firstWord) ? trimmed : null;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
// ── Background Job ──────────────────────────────────
|
|
74
|
-
|
|
75
|
-
interface BackgroundJob {
|
|
76
|
-
id: string;
|
|
77
|
-
prompt: string;
|
|
78
|
-
status: "running" | "done" | "error";
|
|
79
|
-
result?: string;
|
|
80
|
-
error?: string;
|
|
81
|
-
startedAt: number;
|
|
82
|
-
completedAt?: number;
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
// ── Interrupt/Queue Prompt ────────────────────────────
|
|
86
|
-
|
|
87
|
-
function InterruptPrompt(props: {
|
|
88
|
-
message: string;
|
|
89
|
-
onInterrupt: () => void;
|
|
90
|
-
onQueue: () => void;
|
|
91
|
-
onCancel: () => void;
|
|
92
|
-
}) {
|
|
93
|
-
const { theme } = useTheme();
|
|
94
|
-
const t = theme;
|
|
95
|
-
const [selected, setSelected] = useState(0);
|
|
96
|
-
const options = [
|
|
97
|
-
{ label: "Interrupt — stop current response and send new message", action: props.onInterrupt },
|
|
98
|
-
{ label: "Queue — wait for current response, then send", action: props.onQueue },
|
|
99
|
-
{ label: "Cancel — discard new message", action: props.onCancel },
|
|
100
|
-
];
|
|
101
|
-
|
|
102
|
-
useKeyboard((key: any) => {
|
|
103
|
-
if (key.name === "escape") { props.onCancel(); return; }
|
|
104
|
-
if (key.name === "up" || key.name === "k") { setSelected((s) => Math.max(0, s - 1)); return; }
|
|
105
|
-
if (key.name === "down" || key.name === "j") { setSelected((s) => Math.min(options.length - 1, s + 1)); return; }
|
|
106
|
-
if (key.name === "return") { options[selected].action(); return; }
|
|
107
|
-
// Quick keys
|
|
108
|
-
if (key.sequence === "1" || key.sequence === "i") { props.onInterrupt(); return; }
|
|
109
|
-
if (key.sequence === "2" || key.sequence === "q") { props.onQueue(); return; }
|
|
110
|
-
if (key.sequence === "3") { props.onCancel(); return; }
|
|
111
|
-
});
|
|
112
|
-
|
|
113
|
-
const preview = props.message.length > 50 ? props.message.slice(0, 47) + "..." : props.message;
|
|
114
|
-
|
|
115
|
-
return (
|
|
116
|
-
<box flexDirection="column" flexShrink={0} paddingX={1}>
|
|
117
|
-
<text fg={t.warning} attributes={TextAttributes.BOLD}>
|
|
118
|
-
{" Agent is streaming. What to do with your message?"}
|
|
119
|
-
</text>
|
|
120
|
-
<text fg={t.textDim}>{` "${preview}"`}</text>
|
|
121
|
-
<text>{""}</text>
|
|
122
|
-
{options.map((opt, i) => (
|
|
123
|
-
<text key={i} fg={i === selected ? t.primary : t.textMuted} attributes={i === selected ? TextAttributes.BOLD : undefined}>
|
|
124
|
-
{` ${i === selected ? "❯" : " "} ${i + 1}. ${opt.label}`}
|
|
125
|
-
</text>
|
|
126
|
-
))}
|
|
127
|
-
<text fg={t.textDim}>{" ↑↓ Navigate Enter Select i/q/Esc Quick keys"}</text>
|
|
128
|
-
</box>
|
|
129
|
-
);
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
// ── Session View ────────────────────────────────────
|
|
133
|
-
|
|
134
|
-
export function SessionView(props: {
|
|
135
|
-
onStatus: (s: string) => void;
|
|
136
|
-
onTokens: (input: number, output: number) => void;
|
|
137
|
-
onActiveTool: (tool: string | undefined) => void;
|
|
138
|
-
onContextPercent: (pct: number) => void;
|
|
139
|
-
onOpenDialog?: (dialog: string) => void;
|
|
140
|
-
initialMessage?: { text: string; images?: ImageAttachment[] } | null;
|
|
141
|
-
dialogOpen?: boolean;
|
|
142
|
-
}) {
|
|
143
|
-
const sdk = useSDK();
|
|
144
|
-
const { setMode } = useTheme();
|
|
145
|
-
const { toast } = useToast();
|
|
146
|
-
|
|
147
|
-
// Set terminal title to indicate active session
|
|
148
|
-
setTerminalTitle("cdoing - session");
|
|
149
|
-
|
|
150
|
-
const [messages, setMessages] = useState<Message[]>([]);
|
|
151
|
-
const [streamingText, setStreamingText] = useState("");
|
|
152
|
-
const streamingTextRef = useRef(streamingText);
|
|
153
|
-
streamingTextRef.current = streamingText;
|
|
154
|
-
const [isStreaming, setIsStreaming] = useState(false);
|
|
155
|
-
const [agentMode, setAgentMode] = useState<AgentMode>("build");
|
|
156
|
-
|
|
157
|
-
// Wire mode change to permission manager's agentType
|
|
158
|
-
const handleModeChange = (mode: AgentMode) => {
|
|
159
|
-
setAgentMode(mode);
|
|
160
|
-
sdk.permissionManager.setAgentType(mode);
|
|
161
|
-
};
|
|
162
|
-
const [activeTool, setActiveTool] = useState<string | undefined>();
|
|
163
|
-
const [pendingPermission, setPendingPermission] = useState<{
|
|
164
|
-
toolName: string;
|
|
165
|
-
message: string;
|
|
166
|
-
resolve: (decision: "allow" | "always" | "deny") => void;
|
|
167
|
-
} | null>(null);
|
|
168
|
-
|
|
169
|
-
// Interrupt/queue prompt state
|
|
170
|
-
const [pendingInterrupt, setPendingInterrupt] = useState<{
|
|
171
|
-
text: string;
|
|
172
|
-
images?: ImageAttachment[];
|
|
173
|
-
} | null>(null);
|
|
174
|
-
const queuedMessagesRef = useRef<string[]>([]);
|
|
175
|
-
|
|
176
|
-
const totalInputRef = useRef(0);
|
|
177
|
-
const totalOutputRef = useRef(0);
|
|
178
|
-
const msgIdCounterRef = useRef(0);
|
|
179
|
-
const conversationRef = useRef<Conversation | null>(null);
|
|
180
|
-
const backgroundJobsRef = useRef<BackgroundJob[]>([]);
|
|
181
|
-
const bgIdCounterRef = useRef(0);
|
|
182
|
-
const planPendingRef = useRef(false);
|
|
183
|
-
const planSummaryRef = useRef("");
|
|
184
|
-
|
|
185
|
-
// Initialize conversation on first render
|
|
186
|
-
if (!conversationRef.current) {
|
|
187
|
-
conversationRef.current = createConversation(sdk.provider, sdk.model);
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
const addMessage = (role: Message["role"], content: string, extra?: Partial<Message>) => {
|
|
191
|
-
const msg: Message = {
|
|
192
|
-
id: `msg-${++msgIdCounterRef.current}`,
|
|
193
|
-
role,
|
|
194
|
-
content,
|
|
195
|
-
timestamp: Date.now(),
|
|
196
|
-
...extra,
|
|
197
|
-
};
|
|
198
|
-
setMessages((prev) => [...prev, msg]);
|
|
199
|
-
|
|
200
|
-
// Persist to conversation history
|
|
201
|
-
if (conversationRef.current && (role === "user" || role === "assistant")) {
|
|
202
|
-
addHistoryMessage(conversationRef.current, role, content);
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
return msg.id;
|
|
206
|
-
};
|
|
207
|
-
|
|
208
|
-
const updateMessage = (id: string, updates: Partial<Message>) => {
|
|
209
|
-
setMessages((prev) =>
|
|
210
|
-
prev.map((m) => (m.id === id ? { ...m, ...updates } : m))
|
|
211
|
-
);
|
|
212
|
-
};
|
|
213
|
-
|
|
214
|
-
// ── Helpers ────────────────────────────────────────
|
|
215
|
-
|
|
216
|
-
const loadStoredConfig = (): Record<string, any> => {
|
|
217
|
-
try {
|
|
218
|
-
const configPath = path.join(os.homedir(), ".cdoing", "config.json");
|
|
219
|
-
if (fs.existsSync(configPath)) {
|
|
220
|
-
return JSON.parse(fs.readFileSync(configPath, "utf-8"));
|
|
221
|
-
}
|
|
222
|
-
} catch {}
|
|
223
|
-
return {};
|
|
224
|
-
};
|
|
225
|
-
|
|
226
|
-
const saveConfigKey = (key: string, value: any): void => {
|
|
227
|
-
const configDir = path.join(os.homedir(), ".cdoing");
|
|
228
|
-
const configPath = path.join(configDir, "config.json");
|
|
229
|
-
let config: Record<string, any> = {};
|
|
230
|
-
try {
|
|
231
|
-
if (fs.existsSync(configPath)) {
|
|
232
|
-
config = JSON.parse(fs.readFileSync(configPath, "utf-8"));
|
|
233
|
-
}
|
|
234
|
-
} catch {}
|
|
235
|
-
config[key] = value;
|
|
236
|
-
if (!fs.existsSync(configDir)) fs.mkdirSync(configDir, { recursive: true });
|
|
237
|
-
fs.writeFileSync(configPath, JSON.stringify(config, null, 2), "utf-8");
|
|
238
|
-
};
|
|
239
|
-
|
|
240
|
-
const loadProjectSettings = (): Record<string, any> => {
|
|
241
|
-
try {
|
|
242
|
-
const settingsPath = path.join(sdk.workingDir, ".cdoing", "settings.json");
|
|
243
|
-
if (fs.existsSync(settingsPath)) {
|
|
244
|
-
return JSON.parse(fs.readFileSync(settingsPath, "utf-8"));
|
|
245
|
-
}
|
|
246
|
-
const claudePath = path.join(sdk.workingDir, ".claude", "settings.json");
|
|
247
|
-
if (fs.existsSync(claudePath)) {
|
|
248
|
-
return JSON.parse(fs.readFileSync(claudePath, "utf-8"));
|
|
249
|
-
}
|
|
250
|
-
} catch {}
|
|
251
|
-
return {};
|
|
252
|
-
};
|
|
253
|
-
|
|
254
|
-
// ── Background Jobs ──────────────────────────────
|
|
255
|
-
|
|
256
|
-
const runBackgroundJob = (prompt: string) => {
|
|
257
|
-
const job: BackgroundJob = {
|
|
258
|
-
id: `bg-${++bgIdCounterRef.current}`,
|
|
259
|
-
prompt,
|
|
260
|
-
status: "running",
|
|
261
|
-
startedAt: Date.now(),
|
|
262
|
-
};
|
|
263
|
-
backgroundJobsRef.current = [...backgroundJobsRef.current, job];
|
|
264
|
-
addMessage("system", `Background job ${job.id} started: ${prompt.substring(0, 60)}${prompt.length > 60 ? "..." : ""}`);
|
|
265
|
-
|
|
266
|
-
// Run in background with a separate callback set
|
|
267
|
-
let bgResult = "";
|
|
268
|
-
const bgCallbacks: AgentCallbacks = {
|
|
269
|
-
onToken: (token) => { bgResult += token; },
|
|
270
|
-
onToolCall: () => {},
|
|
271
|
-
onToolResult: () => {},
|
|
272
|
-
onComplete: () => {
|
|
273
|
-
job.status = "done";
|
|
274
|
-
job.result = bgResult.trim();
|
|
275
|
-
job.completedAt = Date.now();
|
|
276
|
-
backgroundJobsRef.current = [...backgroundJobsRef.current];
|
|
277
|
-
addMessage("system", `Background job ${job.id} completed. Use /jobs ${job.id} to see results.`);
|
|
278
|
-
},
|
|
279
|
-
onError: (error) => {
|
|
280
|
-
job.status = "error";
|
|
281
|
-
job.error = error.message;
|
|
282
|
-
job.completedAt = Date.now();
|
|
283
|
-
backgroundJobsRef.current = [...backgroundJobsRef.current];
|
|
284
|
-
addMessage("system", `Background job ${job.id} failed: ${error.message}`);
|
|
285
|
-
},
|
|
286
|
-
};
|
|
287
|
-
|
|
288
|
-
// Fire and forget
|
|
289
|
-
sdk.agent.run(prompt, bgCallbacks).catch((err) => {
|
|
290
|
-
job.status = "error";
|
|
291
|
-
job.error = err instanceof Error ? err.message : String(err);
|
|
292
|
-
job.completedAt = Date.now();
|
|
293
|
-
});
|
|
294
|
-
};
|
|
295
|
-
|
|
296
|
-
// ── Slash Commands ──────────────────────────────────
|
|
297
|
-
|
|
298
|
-
const handleSlashCommand = (cmd: string) => {
|
|
299
|
-
const [command, ...args] = cmd.split(" ");
|
|
300
|
-
const arg = args.join(" ").trim();
|
|
301
|
-
|
|
302
|
-
switch (command) {
|
|
303
|
-
case "/clear":
|
|
304
|
-
setMessages([]);
|
|
305
|
-
sdk.agent.clearHistory();
|
|
306
|
-
addMessage("system", "Chat cleared.");
|
|
307
|
-
toast("success", "Chat cleared");
|
|
308
|
-
break;
|
|
309
|
-
|
|
310
|
-
case "/new":
|
|
311
|
-
setMessages([]);
|
|
312
|
-
sdk.agent.clearHistory();
|
|
313
|
-
totalInputRef.current = 0;
|
|
314
|
-
totalOutputRef.current = 0;
|
|
315
|
-
props.onTokens(0, 0);
|
|
316
|
-
props.onContextPercent(0);
|
|
317
|
-
conversationRef.current = createConversation(sdk.provider, sdk.model);
|
|
318
|
-
addMessage("system", "New conversation started.");
|
|
319
|
-
toast("success", "New conversation started");
|
|
320
|
-
break;
|
|
321
|
-
|
|
322
|
-
case "/model": {
|
|
323
|
-
if (arg && sdk.rebuildAgent) {
|
|
324
|
-
sdk.rebuildAgent(sdk.provider, arg);
|
|
325
|
-
addMessage("system", `Model switched to: ${arg}`);
|
|
326
|
-
toast("info", `Model: ${arg}`);
|
|
327
|
-
} else if (arg) {
|
|
328
|
-
addMessage("system", "Model switching not available.");
|
|
329
|
-
toast("warning", "Model switching not available");
|
|
330
|
-
} else {
|
|
331
|
-
addMessage("system", `Current model: ${sdk.model}`);
|
|
332
|
-
}
|
|
333
|
-
break;
|
|
334
|
-
}
|
|
335
|
-
|
|
336
|
-
case "/provider": {
|
|
337
|
-
if (arg && sdk.rebuildAgent) {
|
|
338
|
-
const { getDefaultModel } = require("@cdoing/ai");
|
|
339
|
-
const defaultModel = getDefaultModel(arg) || sdk.model;
|
|
340
|
-
sdk.rebuildAgent(arg, defaultModel);
|
|
341
|
-
addMessage("system", `Provider switched to: ${arg} (model: ${defaultModel})`);
|
|
342
|
-
toast("info", `Provider: ${arg} (${defaultModel})`);
|
|
343
|
-
} else if (arg) {
|
|
344
|
-
addMessage("system", "Provider switching not available.");
|
|
345
|
-
toast("warning", "Provider switching not available");
|
|
346
|
-
} else {
|
|
347
|
-
addMessage("system", `Current provider: ${sdk.provider}`);
|
|
348
|
-
}
|
|
349
|
-
break;
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
case "/mode": {
|
|
353
|
-
const currentMode = (sdk.permissionManager as any)?.mode || "ask";
|
|
354
|
-
addMessage("system", `Permission mode: ${currentMode}\nAvailable: ask, auto-edit, auto`);
|
|
355
|
-
break;
|
|
356
|
-
}
|
|
357
|
-
|
|
358
|
-
case "/dir": {
|
|
359
|
-
if (arg) {
|
|
360
|
-
const resolved = path.resolve(sdk.workingDir, arg);
|
|
361
|
-
if (fs.existsSync(resolved) && fs.statSync(resolved).isDirectory()) {
|
|
362
|
-
if (sdk.setWorkingDir) sdk.setWorkingDir(resolved);
|
|
363
|
-
addMessage("system", `Working directory changed to: ${resolved}`);
|
|
364
|
-
} else {
|
|
365
|
-
addMessage("system", `Directory not found: ${resolved}`);
|
|
366
|
-
}
|
|
367
|
-
} else {
|
|
368
|
-
addMessage("system", `Working directory: ${sdk.workingDir}`);
|
|
369
|
-
}
|
|
370
|
-
break;
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
case "/config": {
|
|
374
|
-
if (arg.startsWith("set ")) {
|
|
375
|
-
const setParts = arg.substring(4).trim().split(/\s+/);
|
|
376
|
-
if (setParts.length >= 2) {
|
|
377
|
-
const [key, ...valParts] = setParts;
|
|
378
|
-
const value = valParts.join(" ");
|
|
379
|
-
if (key === "api-key") {
|
|
380
|
-
const config = loadStoredConfig();
|
|
381
|
-
if (!config.apiKeys) config.apiKeys = {};
|
|
382
|
-
config.apiKeys[sdk.provider] = value;
|
|
383
|
-
saveConfigKey("apiKeys", config.apiKeys);
|
|
384
|
-
addMessage("system", `API key saved for ${sdk.provider}.`);
|
|
385
|
-
} else if (key === "api-key-helper") {
|
|
386
|
-
saveConfigKey("apiKeyHelper", value);
|
|
387
|
-
addMessage("system", `API key helper set to: ${value}`);
|
|
388
|
-
} else {
|
|
389
|
-
saveConfigKey(key, value);
|
|
390
|
-
addMessage("system", `Config ${key} set to: ${value}`);
|
|
391
|
-
}
|
|
392
|
-
} else {
|
|
393
|
-
addMessage("system", "Usage: /config set <key> <value>");
|
|
394
|
-
}
|
|
395
|
-
} else if (arg === "show" || !arg) {
|
|
396
|
-
const config = loadStoredConfig();
|
|
397
|
-
const lines = Object.entries(config)
|
|
398
|
-
.filter(([k]) => k !== "apiKeys")
|
|
399
|
-
.map(([k, v]) => ` ${k}: ${typeof v === "object" ? JSON.stringify(v) : v}`);
|
|
400
|
-
addMessage("system", lines.length > 0 ? "Configuration:\n" + lines.join("\n") : "No configuration found. Run /setup to configure.");
|
|
401
|
-
} else {
|
|
402
|
-
addMessage("system", "Usage: /config [show | set <key> <value>]");
|
|
403
|
-
}
|
|
404
|
-
break;
|
|
405
|
-
}
|
|
406
|
-
|
|
407
|
-
case "/theme": {
|
|
408
|
-
const validModes = ["dark", "light"] as const;
|
|
409
|
-
if (arg && (validModes as readonly string[]).includes(arg)) {
|
|
410
|
-
setMode(arg as "dark" | "light");
|
|
411
|
-
addMessage("system", `Theme mode switched to: ${arg}`);
|
|
412
|
-
toast("success", `Mode: ${arg}`);
|
|
413
|
-
} else {
|
|
414
|
-
addMessage("system", "Usage: /theme dark | light (or Ctrl+T for theme picker)");
|
|
415
|
-
toast("warning", "Usage: /theme dark | light");
|
|
416
|
-
}
|
|
417
|
-
break;
|
|
418
|
-
}
|
|
419
|
-
|
|
420
|
-
case "/compact":
|
|
421
|
-
if ((sdk.agent as any).compressContext) {
|
|
422
|
-
(sdk.agent as any).compressContext();
|
|
423
|
-
addMessage("system", "Context compressed.");
|
|
424
|
-
toast("success", "Context compressed");
|
|
425
|
-
} else {
|
|
426
|
-
addMessage("system", "Context compression not available.");
|
|
427
|
-
toast("warning", "Context compression not available");
|
|
428
|
-
}
|
|
429
|
-
break;
|
|
430
|
-
|
|
431
|
-
case "/effort": {
|
|
432
|
-
const levels = ["low", "medium", "high", "max"];
|
|
433
|
-
if (arg && levels.includes(arg)) {
|
|
434
|
-
addMessage("system", `Effort level set to: ${arg}`);
|
|
435
|
-
toast("info", `Effort: ${arg}`);
|
|
436
|
-
} else {
|
|
437
|
-
addMessage("system", `Usage: /effort <${levels.join("|")}>`);
|
|
438
|
-
}
|
|
439
|
-
break;
|
|
440
|
-
}
|
|
441
|
-
|
|
442
|
-
case "/plan": {
|
|
443
|
-
// Helper: rebuild agent with fresh system prompt, preserving history
|
|
444
|
-
const rebuildWithHistory = () => {
|
|
445
|
-
if (sdk.rebuildAgent) {
|
|
446
|
-
const history = sdk.agent.getHistory();
|
|
447
|
-
sdk.rebuildAgent(sdk.provider, sdk.model);
|
|
448
|
-
if (history.length > 0) {
|
|
449
|
-
sdk.agent.setHistory(history);
|
|
450
|
-
}
|
|
451
|
-
}
|
|
452
|
-
};
|
|
453
|
-
|
|
454
|
-
if (arg === "off" || arg === "cancel") {
|
|
455
|
-
planPendingRef.current = false;
|
|
456
|
-
sdk.permissionManager.setMode("default" as any);
|
|
457
|
-
rebuildWithHistory();
|
|
458
|
-
addMessage("system", "Plan mode cancelled. Switched to build mode.");
|
|
459
|
-
} else if (arg === "show") {
|
|
460
|
-
addMessage("system", planSummaryRef.current || "No active plan.");
|
|
461
|
-
} else if (arg === "approve" || arg === "yes") {
|
|
462
|
-
if (!planPendingRef.current) {
|
|
463
|
-
addMessage("system", "No plan to approve. Use /plan <request> to create one.");
|
|
464
|
-
break;
|
|
465
|
-
}
|
|
466
|
-
planPendingRef.current = false;
|
|
467
|
-
sdk.permissionManager.setMode("default" as any);
|
|
468
|
-
rebuildWithHistory();
|
|
469
|
-
addMessage("system", "Plan approved! Switched to build mode. Executing...");
|
|
470
|
-
const buildPrompt = [
|
|
471
|
-
"[MODE SWITCH: Plan → Build]",
|
|
472
|
-
"Your operational mode has changed from plan to build.",
|
|
473
|
-
"You now have full access to write files, run commands, and execute tools.",
|
|
474
|
-
"",
|
|
475
|
-
"## Approved Plan",
|
|
476
|
-
planSummaryRef.current || "Execute the plan you created.",
|
|
477
|
-
"",
|
|
478
|
-
"## Instructions",
|
|
479
|
-
"Execute the plan step by step. If a step fails, explain why and suggest alternatives.",
|
|
480
|
-
].join("\n");
|
|
481
|
-
sendMessage(buildPrompt);
|
|
482
|
-
} else if (arg === "reject" || arg === "no") {
|
|
483
|
-
planPendingRef.current = false;
|
|
484
|
-
sdk.permissionManager.setMode("default" as any);
|
|
485
|
-
rebuildWithHistory();
|
|
486
|
-
addMessage("system", "Plan rejected. Switched to build mode.");
|
|
487
|
-
} else if (!arg) {
|
|
488
|
-
const isActive = planPendingRef.current;
|
|
489
|
-
if (isActive) {
|
|
490
|
-
planPendingRef.current = false;
|
|
491
|
-
sdk.permissionManager.setMode("default" as any);
|
|
492
|
-
rebuildWithHistory();
|
|
493
|
-
addMessage("system", "Plan mode OFF. Switched to build mode.");
|
|
494
|
-
} else {
|
|
495
|
-
sdk.permissionManager.setMode("plan" as any);
|
|
496
|
-
rebuildWithHistory();
|
|
497
|
-
addMessage("system", "Plan mode ON (read-only). Send a message to start planning.\nUse /plan approve to execute, /plan reject to cancel.");
|
|
498
|
-
}
|
|
499
|
-
} else {
|
|
500
|
-
// /plan <request>
|
|
501
|
-
sdk.permissionManager.setMode("plan" as any);
|
|
502
|
-
rebuildWithHistory();
|
|
503
|
-
planPendingRef.current = true;
|
|
504
|
-
addMessage("system", "Plan mode ON (read-only). Generating plan...\nUse /plan approve when ready, /plan reject to cancel.");
|
|
505
|
-
const planPrompt = [
|
|
506
|
-
"[PLAN MODE — Read-only]",
|
|
507
|
-
"Analyze this request and create a detailed step-by-step implementation plan.",
|
|
508
|
-
"You are in read-only mode — you can read files, search code, and explore, but CANNOT write or execute.",
|
|
509
|
-
"When your plan is complete, call plan_exit with a summary.",
|
|
510
|
-
"",
|
|
511
|
-
`Request: ${arg}`,
|
|
512
|
-
].join("\n");
|
|
513
|
-
sendMessage(planPrompt);
|
|
514
|
-
}
|
|
515
|
-
break;
|
|
516
|
-
}
|
|
517
|
-
|
|
518
|
-
case "/permissions": {
|
|
519
|
-
const settings = loadProjectSettings();
|
|
520
|
-
const allow = settings?.allow || [];
|
|
521
|
-
const deny = settings?.deny || [];
|
|
522
|
-
const lines: string[] = ["Permission rules:"];
|
|
523
|
-
if (deny.length > 0) lines.push(" Deny: " + deny.join(", "));
|
|
524
|
-
if (allow.length > 0) lines.push(" Allow: " + allow.join(", "));
|
|
525
|
-
if (deny.length === 0 && allow.length === 0) lines.push(" No custom rules configured.");
|
|
526
|
-
addMessage("system", lines.join("\n"));
|
|
527
|
-
break;
|
|
528
|
-
}
|
|
529
|
-
|
|
530
|
-
case "/hooks": {
|
|
531
|
-
try {
|
|
532
|
-
const hooksPath = path.join(sdk.workingDir, ".cdoing", "hooks.json");
|
|
533
|
-
if (fs.existsSync(hooksPath)) {
|
|
534
|
-
const hooks = JSON.parse(fs.readFileSync(hooksPath, "utf-8"));
|
|
535
|
-
const keys = Object.keys(hooks);
|
|
536
|
-
addMessage("system", keys.length > 0
|
|
537
|
-
? "Configured hooks:\n" + keys.map((k) => ` ${k}: ${JSON.stringify(hooks[k])}`).join("\n")
|
|
538
|
-
: "No hooks configured.");
|
|
539
|
-
} else {
|
|
540
|
-
addMessage("system", "No hooks file found (.cdoing/hooks.json).");
|
|
541
|
-
}
|
|
542
|
-
} catch {
|
|
543
|
-
addMessage("system", "No hooks configured.");
|
|
544
|
-
}
|
|
545
|
-
break;
|
|
546
|
-
}
|
|
547
|
-
|
|
548
|
-
case "/rules": {
|
|
549
|
-
try {
|
|
550
|
-
const rulesDir = path.join(sdk.workingDir, ".cdoing", "rules");
|
|
551
|
-
if (fs.existsSync(rulesDir)) {
|
|
552
|
-
const files = fs.readdirSync(rulesDir).filter((f: string) => f.endsWith(".md"));
|
|
553
|
-
addMessage("system", files.length > 0
|
|
554
|
-
? "Project rules:\n" + files.map((f: string) => ` ${f}`).join("\n")
|
|
555
|
-
: "No rules found in .cdoing/rules/.");
|
|
556
|
-
} else {
|
|
557
|
-
addMessage("system", "No rules directory found (.cdoing/rules/).");
|
|
558
|
-
}
|
|
559
|
-
} catch {
|
|
560
|
-
addMessage("system", "No rules configured.");
|
|
561
|
-
}
|
|
562
|
-
break;
|
|
563
|
-
}
|
|
564
|
-
|
|
565
|
-
case "/memory":
|
|
566
|
-
addMessage("system", "Memory store: not yet implemented in TUI. Coming soon.");
|
|
567
|
-
break;
|
|
568
|
-
|
|
569
|
-
case "/tasks":
|
|
570
|
-
addMessage("system", "Task list: not yet implemented in TUI. Coming soon.");
|
|
571
|
-
break;
|
|
572
|
-
|
|
573
|
-
case "/context":
|
|
574
|
-
addMessage("system", [
|
|
575
|
-
"Context providers (use @ to invoke):",
|
|
576
|
-
" @terminal — Recent terminal output",
|
|
577
|
-
" @url — Fetch URL content",
|
|
578
|
-
" @tree — Project file tree",
|
|
579
|
-
" @codebase — Full codebase context",
|
|
580
|
-
" @clip — Clipboard content",
|
|
581
|
-
" @file — Include a file",
|
|
582
|
-
].join("\n"));
|
|
583
|
-
break;
|
|
584
|
-
|
|
585
|
-
case "/mcp":
|
|
586
|
-
addMessage("system", "MCP server management: not yet implemented in TUI. Coming soon.");
|
|
587
|
-
break;
|
|
588
|
-
|
|
589
|
-
case "/history":
|
|
590
|
-
case "/ls": {
|
|
591
|
-
const convs = listConversations().slice(0, 20);
|
|
592
|
-
if (convs.length > 0) {
|
|
593
|
-
const lines = convs.map((c) => {
|
|
594
|
-
const id = c.id.substring(0, 12);
|
|
595
|
-
const date = formatRelativeDate(c.updatedAt);
|
|
596
|
-
const msgCount = c.messages.filter((m) => m.role === "user").length;
|
|
597
|
-
return ` ${id} ${date.padEnd(10)} (${msgCount} msgs) ${c.title}`;
|
|
598
|
-
});
|
|
599
|
-
addMessage("system", "Conversations:\n" + lines.join("\n") + "\n\nUse /resume <id> to continue. Ctrl+S for interactive browser.");
|
|
600
|
-
} else {
|
|
601
|
-
addMessage("system", "No saved conversations found.");
|
|
602
|
-
}
|
|
603
|
-
break;
|
|
604
|
-
}
|
|
605
|
-
|
|
606
|
-
case "/resume": {
|
|
607
|
-
if (!arg) {
|
|
608
|
-
addMessage("system", "Usage: /resume <conversation-id>");
|
|
609
|
-
break;
|
|
610
|
-
}
|
|
611
|
-
const conv = loadConversation(arg);
|
|
612
|
-
if (!conv) {
|
|
613
|
-
addMessage("system", `Conversation not found: ${arg}`);
|
|
614
|
-
break;
|
|
615
|
-
}
|
|
616
|
-
// Restore conversation
|
|
617
|
-
conversationRef.current = conv;
|
|
618
|
-
setMessages([]);
|
|
619
|
-
sdk.agent.clearHistory();
|
|
620
|
-
totalInputRef.current = 0;
|
|
621
|
-
totalOutputRef.current = 0;
|
|
622
|
-
// Replay messages into UI
|
|
623
|
-
for (const m of conv.messages) {
|
|
624
|
-
if (m.role === "user" || m.role === "assistant") {
|
|
625
|
-
const msg: Message = {
|
|
626
|
-
id: `msg-${++msgIdCounterRef.current}`,
|
|
627
|
-
role: m.role,
|
|
628
|
-
content: m.content,
|
|
629
|
-
timestamp: m.timestamp,
|
|
630
|
-
};
|
|
631
|
-
setMessages((prev) => [...prev, msg]);
|
|
632
|
-
}
|
|
633
|
-
}
|
|
634
|
-
addMessage("system", `Resumed conversation: ${conv.title}`);
|
|
635
|
-
break;
|
|
636
|
-
}
|
|
637
|
-
|
|
638
|
-
case "/view": {
|
|
639
|
-
if (!arg) {
|
|
640
|
-
addMessage("system", "Usage: /view <conversation-id>");
|
|
641
|
-
break;
|
|
642
|
-
}
|
|
643
|
-
const conv = loadConversation(arg);
|
|
644
|
-
if (!conv) {
|
|
645
|
-
addMessage("system", `Conversation not found: ${arg}`);
|
|
646
|
-
break;
|
|
647
|
-
}
|
|
648
|
-
const viewMsgs = conv.messages
|
|
649
|
-
.filter((m) => m.role !== "tool")
|
|
650
|
-
.slice(-20)
|
|
651
|
-
.map((m) => {
|
|
652
|
-
const prefix = m.role === "user" ? "❯" : "◆";
|
|
653
|
-
const content = m.content.length > 100 ? m.content.substring(0, 97) + "..." : m.content;
|
|
654
|
-
return ` ${prefix} ${content.replace(/\n/g, " ")}`;
|
|
655
|
-
});
|
|
656
|
-
addMessage("system", `Conversation: ${conv.title}\n\n${viewMsgs.join("\n")}`);
|
|
657
|
-
break;
|
|
658
|
-
}
|
|
659
|
-
|
|
660
|
-
case "/fork": {
|
|
661
|
-
const sourceConv = arg ? loadConversation(arg) : conversationRef.current;
|
|
662
|
-
if (!sourceConv) {
|
|
663
|
-
addMessage("system", arg ? `Conversation not found: ${arg}` : "No active conversation to fork.");
|
|
664
|
-
break;
|
|
665
|
-
}
|
|
666
|
-
const forked = forkConversation(sourceConv);
|
|
667
|
-
if (forked) {
|
|
668
|
-
addMessage("system", `Forked conversation: ${forked.id.substring(0, 12)} — "${forked.title}"`);
|
|
669
|
-
} else {
|
|
670
|
-
addMessage("system", "Failed to fork conversation.");
|
|
671
|
-
}
|
|
672
|
-
break;
|
|
673
|
-
}
|
|
674
|
-
|
|
675
|
-
case "/delete": {
|
|
676
|
-
if (!arg) {
|
|
677
|
-
addMessage("system", "Usage: /delete <conversation-id>");
|
|
678
|
-
break;
|
|
679
|
-
}
|
|
680
|
-
if (deleteConversation(arg)) {
|
|
681
|
-
addMessage("system", `Conversation deleted: ${arg}`);
|
|
682
|
-
} else {
|
|
683
|
-
addMessage("system", `Conversation not found: ${arg}`);
|
|
684
|
-
}
|
|
685
|
-
break;
|
|
686
|
-
}
|
|
687
|
-
|
|
688
|
-
case "/bg": {
|
|
689
|
-
if (!arg) {
|
|
690
|
-
addMessage("system", "Usage: /bg <prompt>");
|
|
691
|
-
break;
|
|
692
|
-
}
|
|
693
|
-
runBackgroundJob(arg);
|
|
694
|
-
break;
|
|
695
|
-
}
|
|
696
|
-
|
|
697
|
-
case "/jobs": {
|
|
698
|
-
const jobs = backgroundJobsRef.current;
|
|
699
|
-
if (arg) {
|
|
700
|
-
// Show specific job
|
|
701
|
-
const job = jobs.find((j) => j.id === arg);
|
|
702
|
-
if (job) {
|
|
703
|
-
const elapsed = job.completedAt
|
|
704
|
-
? `${((job.completedAt - job.startedAt) / 1000).toFixed(1)}s`
|
|
705
|
-
: `${((Date.now() - job.startedAt) / 1000).toFixed(1)}s (running)`;
|
|
706
|
-
const result = job.result
|
|
707
|
-
? job.result.length > 500 ? job.result.substring(0, 497) + "..." : job.result
|
|
708
|
-
: job.error || "(no output)";
|
|
709
|
-
addMessage("system", `Job ${job.id} [${job.status}] (${elapsed}):\n Prompt: ${job.prompt}\n Result: ${result}`);
|
|
710
|
-
} else {
|
|
711
|
-
addMessage("system", `Job not found: ${arg}`);
|
|
712
|
-
}
|
|
713
|
-
} else if (jobs.length > 0) {
|
|
714
|
-
const lines = jobs.map((j) => {
|
|
715
|
-
const icon = j.status === "running" ? "⏳" : j.status === "done" ? "✓" : "✗";
|
|
716
|
-
return ` ${icon} ${j.id} ${j.status} ${j.prompt.substring(0, 50)}${j.prompt.length > 50 ? "..." : ""}`;
|
|
717
|
-
});
|
|
718
|
-
addMessage("system", "Background jobs:\n" + lines.join("\n"));
|
|
719
|
-
} else {
|
|
720
|
-
addMessage("system", "No background jobs.");
|
|
721
|
-
}
|
|
722
|
-
break;
|
|
723
|
-
}
|
|
724
|
-
|
|
725
|
-
case "/btw": {
|
|
726
|
-
if (!arg) {
|
|
727
|
-
addMessage("system", "Usage: /btw <question>");
|
|
728
|
-
break;
|
|
729
|
-
}
|
|
730
|
-
// Ephemeral question — don't add to conversation history
|
|
731
|
-
const btwMsg: Message = {
|
|
732
|
-
id: `msg-${++msgIdCounterRef.current}`,
|
|
733
|
-
role: "user",
|
|
734
|
-
content: `(btw) ${arg}`,
|
|
735
|
-
timestamp: Date.now(),
|
|
736
|
-
};
|
|
737
|
-
setMessages((prev) => [...prev, btwMsg]);
|
|
738
|
-
setIsStreaming(true);
|
|
739
|
-
setStreamingText("");
|
|
740
|
-
props.onStatus("Processing...");
|
|
741
|
-
|
|
742
|
-
let btwResult = "";
|
|
743
|
-
const btwCallbacks: AgentCallbacks = {
|
|
744
|
-
onToken: (token) => {
|
|
745
|
-
btwResult += token;
|
|
746
|
-
setStreamingText((prev) => prev + token);
|
|
747
|
-
},
|
|
748
|
-
onToolCall: () => {},
|
|
749
|
-
onToolResult: () => {},
|
|
750
|
-
onComplete: () => {
|
|
751
|
-
if (btwResult.trim()) {
|
|
752
|
-
const msg: Message = {
|
|
753
|
-
id: `msg-${++msgIdCounterRef.current}`,
|
|
754
|
-
role: "assistant",
|
|
755
|
-
content: btwResult.trim(),
|
|
756
|
-
timestamp: Date.now(),
|
|
757
|
-
};
|
|
758
|
-
setMessages((prev) => [...prev, msg]);
|
|
759
|
-
setStreamingText("");
|
|
760
|
-
}
|
|
761
|
-
setIsStreaming(false);
|
|
762
|
-
props.onStatus("Ready");
|
|
763
|
-
},
|
|
764
|
-
onError: (error) => {
|
|
765
|
-
addMessage("system", `Error: ${error.message}`);
|
|
766
|
-
setIsStreaming(false);
|
|
767
|
-
props.onStatus("Error");
|
|
768
|
-
},
|
|
769
|
-
};
|
|
770
|
-
|
|
771
|
-
sdk.agent.run(arg, btwCallbacks).catch((err) => {
|
|
772
|
-
addMessage("system", `Error: ${err instanceof Error ? err.message : String(err)}`);
|
|
773
|
-
setIsStreaming(false);
|
|
774
|
-
props.onStatus("Error");
|
|
775
|
-
});
|
|
776
|
-
break;
|
|
777
|
-
}
|
|
778
|
-
|
|
779
|
-
case "/login":
|
|
780
|
-
case "/setup":
|
|
781
|
-
if (props.onOpenDialog) {
|
|
782
|
-
props.onOpenDialog("setup");
|
|
783
|
-
} else {
|
|
784
|
-
addMessage("system", "Setup wizard: configure via ~/.cdoing/config.json or run the base CLI with --login.");
|
|
785
|
-
}
|
|
786
|
-
break;
|
|
787
|
-
|
|
788
|
-
case "/logout": {
|
|
789
|
-
try {
|
|
790
|
-
const { fullLogout } = require("@cdoing/core");
|
|
791
|
-
const msg = fullLogout(sdk.provider);
|
|
792
|
-
|
|
793
|
-
// Invalidate the in-memory agent so it can't make further API calls
|
|
794
|
-
sdk.agent.invalidate();
|
|
795
|
-
|
|
796
|
-
addMessage("system", msg);
|
|
797
|
-
} catch {
|
|
798
|
-
addMessage("system", "OAuth logout not available.");
|
|
799
|
-
}
|
|
800
|
-
break;
|
|
801
|
-
}
|
|
802
|
-
|
|
803
|
-
case "/auth-status": {
|
|
804
|
-
const config = loadStoredConfig();
|
|
805
|
-
const lines: string[] = ["Authentication Status:", ""];
|
|
806
|
-
|
|
807
|
-
// OAuth status
|
|
808
|
-
try {
|
|
809
|
-
const { getAllOAuthStatuses } = require("@cdoing/core");
|
|
810
|
-
const oauthStatuses = getAllOAuthStatuses();
|
|
811
|
-
lines.push("OAuth:");
|
|
812
|
-
let hasOAuth = false;
|
|
813
|
-
for (const s of oauthStatuses) {
|
|
814
|
-
if (s.status === "none") continue;
|
|
815
|
-
hasOAuth = true;
|
|
816
|
-
const icon = s.status === "active" ? "✓" : "✗";
|
|
817
|
-
const label = s.status === "active" ? "active" : "expired";
|
|
818
|
-
const expires = s.expiresAt ? new Date(s.expiresAt).toLocaleString() : "unknown";
|
|
819
|
-
lines.push(` ${icon} ${s.name}: ${label}`);
|
|
820
|
-
if (s.expiresAt) lines.push(` Expires: ${expires}`);
|
|
821
|
-
}
|
|
822
|
-
if (!hasOAuth) lines.push(" None");
|
|
823
|
-
} catch {
|
|
824
|
-
lines.push("OAuth: unavailable");
|
|
825
|
-
}
|
|
826
|
-
|
|
827
|
-
// API keys
|
|
828
|
-
lines.push("");
|
|
829
|
-
lines.push("Stored API keys:");
|
|
830
|
-
if (config.apiKeys && Object.keys(config.apiKeys).length > 0) {
|
|
831
|
-
for (const [prov, key] of Object.entries(config.apiKeys)) {
|
|
832
|
-
const k = String(key);
|
|
833
|
-
const masked = k.slice(0, 8) + "..." + k.slice(-4);
|
|
834
|
-
lines.push(` ✓ ${prov}: ${masked}`);
|
|
835
|
-
}
|
|
836
|
-
} else {
|
|
837
|
-
lines.push(" None");
|
|
838
|
-
}
|
|
839
|
-
|
|
840
|
-
lines.push("");
|
|
841
|
-
lines.push("Environment variables:");
|
|
842
|
-
const envVars: [string, string | undefined][] = [
|
|
843
|
-
["ANTHROPIC_API_KEY", process.env.ANTHROPIC_API_KEY],
|
|
844
|
-
["OPENAI_API_KEY", process.env.OPENAI_API_KEY],
|
|
845
|
-
["GOOGLE_API_KEY", process.env.GOOGLE_API_KEY],
|
|
846
|
-
];
|
|
847
|
-
let hasEnvKey = false;
|
|
848
|
-
for (const [name, value] of envVars) {
|
|
849
|
-
if (value) {
|
|
850
|
-
hasEnvKey = true;
|
|
851
|
-
const masked = value.slice(0, 8) + "..." + value.slice(-4);
|
|
852
|
-
lines.push(` ✓ ${name}: ${masked}`);
|
|
853
|
-
}
|
|
854
|
-
}
|
|
855
|
-
if (!hasEnvKey) lines.push(" None");
|
|
856
|
-
|
|
857
|
-
addMessage("system", lines.join("\n"));
|
|
858
|
-
break;
|
|
859
|
-
}
|
|
860
|
-
|
|
861
|
-
case "/doctor": {
|
|
862
|
-
const checks: string[] = ["System health check:"];
|
|
863
|
-
const config = loadStoredConfig();
|
|
864
|
-
const envKey = process.env[`${sdk.provider.toUpperCase()}_API_KEY`];
|
|
865
|
-
let hasAuth = !!(config.apiKeys?.[sdk.provider] || envKey);
|
|
866
|
-
if (!hasAuth) {
|
|
867
|
-
try {
|
|
868
|
-
const { getOAuthStatus } = require("@cdoing/core");
|
|
869
|
-
hasAuth = getOAuthStatus(sdk.provider).status === "active";
|
|
870
|
-
} catch {}
|
|
871
|
-
}
|
|
872
|
-
checks.push(` Provider: ${sdk.provider} ${hasAuth ? "✓ Authenticated" : "✗ No API key or OAuth token"}`);
|
|
873
|
-
checks.push(` Model: ${sdk.model}`);
|
|
874
|
-
checks.push(` Working dir: ${sdk.workingDir} ${fs.existsSync(sdk.workingDir) ? "✓" : "✗"}`);
|
|
875
|
-
const hasCdoing = fs.existsSync(path.join(sdk.workingDir, ".cdoing"));
|
|
876
|
-
const hasClaude = fs.existsSync(path.join(sdk.workingDir, ".claude"));
|
|
877
|
-
checks.push(` Project config: ${hasCdoing ? ".cdoing/ ✓" : hasClaude ? ".claude/ ✓" : "✗ none"}`);
|
|
878
|
-
checks.push(` Node: ${process.version}`);
|
|
879
|
-
checks.push(` Platform: ${process.platform} ${process.arch}`);
|
|
880
|
-
|
|
881
|
-
// Check conversation history
|
|
882
|
-
const convs = listConversations();
|
|
883
|
-
checks.push(` Conversations: ${convs.length} saved`);
|
|
884
|
-
|
|
885
|
-
// Check background jobs
|
|
886
|
-
const runningJobs = backgroundJobsRef.current.filter((j) => j.status === "running");
|
|
887
|
-
if (runningJobs.length > 0) {
|
|
888
|
-
checks.push(` Background jobs: ${runningJobs.length} running`);
|
|
889
|
-
}
|
|
890
|
-
|
|
891
|
-
addMessage("system", checks.join("\n"));
|
|
892
|
-
break;
|
|
893
|
-
}
|
|
894
|
-
|
|
895
|
-
case "/init": {
|
|
896
|
-
const cdoingDir = path.join(sdk.workingDir, ".cdoing");
|
|
897
|
-
if (fs.existsSync(cdoingDir)) {
|
|
898
|
-
addMessage("system", "Project already initialized (.cdoing/ exists).");
|
|
899
|
-
} else {
|
|
900
|
-
try {
|
|
901
|
-
fs.mkdirSync(cdoingDir, { recursive: true });
|
|
902
|
-
fs.mkdirSync(path.join(cdoingDir, "rules"), { recursive: true });
|
|
903
|
-
fs.writeFileSync(
|
|
904
|
-
path.join(cdoingDir, "config.md"),
|
|
905
|
-
"# Project Configuration\n\nDescribe your project here for the AI assistant.\n",
|
|
906
|
-
"utf-8"
|
|
907
|
-
);
|
|
908
|
-
addMessage("system", "Project initialized. Created .cdoing/ with config.md and rules/.");
|
|
909
|
-
} catch (err) {
|
|
910
|
-
addMessage("system", `Failed to initialize: ${err instanceof Error ? err.message : String(err)}`);
|
|
911
|
-
}
|
|
912
|
-
}
|
|
913
|
-
break;
|
|
914
|
-
}
|
|
915
|
-
|
|
916
|
-
case "/queue":
|
|
917
|
-
addMessage("system", "Message queue is empty.");
|
|
918
|
-
break;
|
|
919
|
-
|
|
920
|
-
case "/help":
|
|
921
|
-
addMessage("system", [
|
|
922
|
-
"Available commands:",
|
|
923
|
-
"",
|
|
924
|
-
" Session",
|
|
925
|
-
" /clear Clear chat history",
|
|
926
|
-
" /new Start new conversation",
|
|
927
|
-
" /compact Compress context window",
|
|
928
|
-
" /btw <question> Ask without adding to history",
|
|
929
|
-
"",
|
|
930
|
-
" Configuration",
|
|
931
|
-
" /model [name] Show/change model",
|
|
932
|
-
" /provider [name] Show/change provider",
|
|
933
|
-
" /mode Show permission mode",
|
|
934
|
-
" /dir [path] Show/change working directory",
|
|
935
|
-
" /config Show configuration",
|
|
936
|
-
" /config set k v Set a config value",
|
|
937
|
-
" /theme <mode> Switch theme (dark/light/auto)",
|
|
938
|
-
" /effort <level> Set effort (low/medium/high/max)",
|
|
939
|
-
" /plan <on|off> Toggle plan mode",
|
|
940
|
-
"",
|
|
941
|
-
" History",
|
|
942
|
-
" /history, /ls List saved conversations",
|
|
943
|
-
" /resume <id> Resume conversation",
|
|
944
|
-
" /view <id> View conversation messages",
|
|
945
|
-
" /fork [id] Fork current/specified conversation",
|
|
946
|
-
" /delete <id> Delete conversation",
|
|
947
|
-
"",
|
|
948
|
-
" Background",
|
|
949
|
-
" /bg <prompt> Run prompt in background",
|
|
950
|
-
" /jobs [id] List/inspect background jobs",
|
|
951
|
-
"",
|
|
952
|
-
" System",
|
|
953
|
-
" /permissions Show permission rules",
|
|
954
|
-
" /hooks Show configured hooks",
|
|
955
|
-
" /rules Show project rules",
|
|
956
|
-
" /context Show context providers",
|
|
957
|
-
" /mcp MCP server management",
|
|
958
|
-
" /doctor System health check",
|
|
959
|
-
" /usage Show token usage",
|
|
960
|
-
" /auth-status Show authentication status",
|
|
961
|
-
"",
|
|
962
|
-
" /setup Run setup wizard",
|
|
963
|
-
" /login Open setup wizard",
|
|
964
|
-
" /logout Clear OAuth tokens",
|
|
965
|
-
" /init Initialize project config",
|
|
966
|
-
" /exit Quit",
|
|
967
|
-
"",
|
|
968
|
-
"Keyboard shortcuts:",
|
|
969
|
-
" Ctrl+V Paste text or image",
|
|
970
|
-
" Ctrl+U Clear input line",
|
|
971
|
-
" Ctrl+W Delete last word",
|
|
972
|
-
" Ctrl+P Command palette",
|
|
973
|
-
" Ctrl+O Model picker",
|
|
974
|
-
" Ctrl+N New session",
|
|
975
|
-
" Ctrl+S Session browser",
|
|
976
|
-
" Tab Switch mode (Build/Plan)",
|
|
977
|
-
" → Accept autocomplete",
|
|
978
|
-
" ↑/↓ Navigate suggestions",
|
|
979
|
-
" Escape Close dropdown",
|
|
980
|
-
"",
|
|
981
|
-
"Shell: prefix with ! or type commands directly (ls, git, npm, etc.)",
|
|
982
|
-
"Context: use @terminal, @url, @tree, @codebase, @clip, @file in messages",
|
|
983
|
-
].join("\n"));
|
|
984
|
-
break;
|
|
985
|
-
|
|
986
|
-
case "/usage":
|
|
987
|
-
addMessage("system", `Tokens: ${totalInputRef.current.toLocaleString()}→${totalOutputRef.current.toLocaleString()} (${(totalInputRef.current + totalOutputRef.current).toLocaleString()} total)`);
|
|
988
|
-
break;
|
|
989
|
-
|
|
990
|
-
case "/exit":
|
|
991
|
-
case "/quit":
|
|
992
|
-
process.exit(0);
|
|
993
|
-
break;
|
|
994
|
-
|
|
995
|
-
default:
|
|
996
|
-
addMessage("system", `Unknown command: ${command}. Type /help for available commands.`);
|
|
997
|
-
toast("error", `Unknown command: ${command}`);
|
|
998
|
-
}
|
|
999
|
-
};
|
|
1000
|
-
|
|
1001
|
-
// ── Shell Commands ────────────────────────────────
|
|
1002
|
-
|
|
1003
|
-
const runShellCommand = (shellCmd: string) => {
|
|
1004
|
-
addMessage("user", `!${shellCmd}`);
|
|
1005
|
-
let output = "";
|
|
1006
|
-
let errorMsg = "";
|
|
1007
|
-
try {
|
|
1008
|
-
output = execSync(shellCmd, {
|
|
1009
|
-
cwd: sdk.workingDir,
|
|
1010
|
-
env: { ...process.env },
|
|
1011
|
-
encoding: "utf-8",
|
|
1012
|
-
timeout: 120000,
|
|
1013
|
-
maxBuffer: 10 * 1024 * 1024,
|
|
1014
|
-
});
|
|
1015
|
-
} catch (err: any) {
|
|
1016
|
-
if (err.stdout) output = String(err.stdout);
|
|
1017
|
-
if (err.stderr) errorMsg = String(err.stderr);
|
|
1018
|
-
if (err.status !== undefined && err.status !== 0) {
|
|
1019
|
-
errorMsg += `\n[exited with code ${err.status}]`;
|
|
1020
|
-
} else if (!err.stdout && !err.stderr && err.message) {
|
|
1021
|
-
errorMsg = err.message;
|
|
1022
|
-
}
|
|
1023
|
-
}
|
|
1024
|
-
const result = (output + (errorMsg ? `\n${errorMsg}` : "")).trim();
|
|
1025
|
-
addMessage("system", `$ ${shellCmd}\n${result || "(no output)"}`);
|
|
1026
|
-
|
|
1027
|
-
// Push to terminal context provider
|
|
1028
|
-
pushTerminalOutput(`$ ${shellCmd}\n${result}`);
|
|
1029
|
-
};
|
|
1030
|
-
|
|
1031
|
-
// ── Interrupt: stop streaming, flush partial, send new message with context ──
|
|
1032
|
-
|
|
1033
|
-
const handleInterrupt = (text: string, images?: ImageAttachment[]) => {
|
|
1034
|
-
// Capture partial response and interrupt — adds partial to agent history for context
|
|
1035
|
-
const partialResponse = streamingTextRef.current.trim();
|
|
1036
|
-
sdk.agent.interrupt(partialResponse);
|
|
1037
|
-
|
|
1038
|
-
// Flush partial streaming text as a message
|
|
1039
|
-
if (partialResponse) {
|
|
1040
|
-
addMessage("assistant", partialResponse + "\n\n*(interrupted)*");
|
|
1041
|
-
setStreamingText("");
|
|
1042
|
-
}
|
|
1043
|
-
|
|
1044
|
-
setIsStreaming(false);
|
|
1045
|
-
props.onStatus("Ready");
|
|
1046
|
-
setActiveTool(undefined);
|
|
1047
|
-
props.onActiveTool(undefined);
|
|
1048
|
-
setPendingInterrupt(null);
|
|
1049
|
-
|
|
1050
|
-
// Send the new message — agent.cancel() calls onComplete which resets state,
|
|
1051
|
-
// so we use a small delay to let that settle
|
|
1052
|
-
setTimeout(() => {
|
|
1053
|
-
doSendMessage(text, images);
|
|
1054
|
-
}, 100);
|
|
1055
|
-
};
|
|
1056
|
-
|
|
1057
|
-
// ── Queue: add to queue, process after current stream finishes ──
|
|
1058
|
-
|
|
1059
|
-
const handleQueue = (text: string) => {
|
|
1060
|
-
queuedMessagesRef.current.push(text);
|
|
1061
|
-
addMessage("system", `📬 Queued message (${queuedMessagesRef.current.length} in queue)`);
|
|
1062
|
-
setPendingInterrupt(null);
|
|
1063
|
-
};
|
|
1064
|
-
|
|
1065
|
-
// ── Process queued messages after streaming completes ──
|
|
1066
|
-
|
|
1067
|
-
const processQueue = () => {
|
|
1068
|
-
if (queuedMessagesRef.current.length > 0) {
|
|
1069
|
-
const next = queuedMessagesRef.current.shift()!;
|
|
1070
|
-
setTimeout(() => doSendMessage(next), 100);
|
|
1071
|
-
}
|
|
1072
|
-
};
|
|
1073
|
-
|
|
1074
|
-
// ── Send Message ────────────────────────────────────
|
|
1075
|
-
|
|
1076
|
-
const sendMessage = async (text: string, images?: ImageAttachment[]) => {
|
|
1077
|
-
if (text.startsWith("/")) {
|
|
1078
|
-
handleSlashCommand(text);
|
|
1079
|
-
return;
|
|
1080
|
-
}
|
|
1081
|
-
|
|
1082
|
-
// Shell commands always run immediately
|
|
1083
|
-
const shellCmd = text.startsWith("!")
|
|
1084
|
-
? text.slice(1).trim()
|
|
1085
|
-
: detectShellCommand(text);
|
|
1086
|
-
|
|
1087
|
-
if (shellCmd) {
|
|
1088
|
-
// Intercept "cd" — execSync runs in a child process, so cd has no effect there
|
|
1089
|
-
const shellParts = shellCmd.trim().split(/\s+/);
|
|
1090
|
-
if (shellParts[0] === "cd") {
|
|
1091
|
-
const target = shellParts.slice(1).join(" ") || process.env.HOME || "/";
|
|
1092
|
-
const resolved = path.resolve(sdk.workingDir, target);
|
|
1093
|
-
if (fs.existsSync(resolved) && fs.statSync(resolved).isDirectory()) {
|
|
1094
|
-
if (sdk.setWorkingDir) sdk.setWorkingDir(resolved);
|
|
1095
|
-
addMessage("system", `Working directory changed to: ${resolved}`);
|
|
1096
|
-
} else {
|
|
1097
|
-
addMessage("system", `cd: no such directory: ${resolved}`);
|
|
1098
|
-
}
|
|
1099
|
-
return;
|
|
1100
|
-
}
|
|
1101
|
-
runShellCommand(shellCmd);
|
|
1102
|
-
return;
|
|
1103
|
-
}
|
|
1104
|
-
|
|
1105
|
-
// If currently streaming, show interrupt/queue prompt
|
|
1106
|
-
if (isStreaming) {
|
|
1107
|
-
setPendingInterrupt({ text, images });
|
|
1108
|
-
return;
|
|
1109
|
-
}
|
|
1110
|
-
|
|
1111
|
-
await doSendMessage(text, images);
|
|
1112
|
-
};
|
|
1113
|
-
|
|
1114
|
-
// Auto-send initial message from home screen input
|
|
1115
|
-
const initialMessageSentRef = useRef(false);
|
|
1116
|
-
useEffect(() => {
|
|
1117
|
-
if (props.initialMessage && !initialMessageSentRef.current) {
|
|
1118
|
-
initialMessageSentRef.current = true;
|
|
1119
|
-
sendMessage(props.initialMessage.text, props.initialMessage.images);
|
|
1120
|
-
}
|
|
1121
|
-
}, []); // eslint-disable-line react-hooks/exhaustive-deps
|
|
1122
|
-
|
|
1123
|
-
const doSendMessage = async (text: string, images?: ImageAttachment[]) => {
|
|
1124
|
-
// Inject plan mode context if active
|
|
1125
|
-
let messageText = text;
|
|
1126
|
-
if (planPendingRef.current || sdk.permissionManager.getMode() === ("plan" as any)) {
|
|
1127
|
-
messageText = `[PLAN MODE — Read-only] You are in plan mode. Do NOT write files, run commands, or modify anything. Only read, search, analyze, and create a plan using the todo tool. When your plan is ready, call plan_exit.\n\n${text}`;
|
|
1128
|
-
}
|
|
1129
|
-
|
|
1130
|
-
// Resolve @mention context providers
|
|
1131
|
-
let expandedText = messageText;
|
|
1132
|
-
if (hasContextMentions(messageText)) {
|
|
1133
|
-
try {
|
|
1134
|
-
expandedText = await resolveContextProviders(text, sdk.workingDir);
|
|
1135
|
-
} catch {
|
|
1136
|
-
// If expansion fails, send original text
|
|
1137
|
-
}
|
|
1138
|
-
}
|
|
1139
|
-
|
|
1140
|
-
addMessage("user", text + (images && images.length > 0 ? ` [${images.length} image${images.length > 1 ? "s" : ""}]` : ""));
|
|
1141
|
-
setIsStreaming(true);
|
|
1142
|
-
setStreamingText("");
|
|
1143
|
-
props.onStatus("Processing...");
|
|
1144
|
-
|
|
1145
|
-
let currentToolId: string | undefined;
|
|
1146
|
-
let turnInput = 0;
|
|
1147
|
-
|
|
1148
|
-
const callbacks: AgentCallbacks = {
|
|
1149
|
-
onToken: (token) => {
|
|
1150
|
-
setStreamingText((prev) => prev + token);
|
|
1151
|
-
},
|
|
1152
|
-
|
|
1153
|
-
onToolCall: (name, input) => {
|
|
1154
|
-
// Flush streaming text to a message
|
|
1155
|
-
const current = streamingTextRef.current.trim();
|
|
1156
|
-
if (current) {
|
|
1157
|
-
addMessage("assistant", current);
|
|
1158
|
-
setStreamingText("");
|
|
1159
|
-
}
|
|
1160
|
-
|
|
1161
|
-
const toolInput = (typeof input === "object" && input) ? input as Record<string, any> : {};
|
|
1162
|
-
const description = toolInput.description || "";
|
|
1163
|
-
currentToolId = addMessage("tool", description, {
|
|
1164
|
-
toolName: name,
|
|
1165
|
-
toolStatus: "running",
|
|
1166
|
-
toolInput,
|
|
1167
|
-
});
|
|
1168
|
-
setActiveTool(name);
|
|
1169
|
-
props.onActiveTool(name);
|
|
1170
|
-
},
|
|
1171
|
-
|
|
1172
|
-
onToolResult: (_name, result, isError) => {
|
|
1173
|
-
if (currentToolId) {
|
|
1174
|
-
const summary = result.length > 80 ? result.substring(0, 77) + "..." : result;
|
|
1175
|
-
updateMessage(currentToolId, {
|
|
1176
|
-
content: summary,
|
|
1177
|
-
toolStatus: isError ? "error" : "done",
|
|
1178
|
-
isError,
|
|
1179
|
-
});
|
|
1180
|
-
currentToolId = undefined;
|
|
1181
|
-
}
|
|
1182
|
-
setActiveTool(undefined);
|
|
1183
|
-
props.onActiveTool(undefined);
|
|
1184
|
-
},
|
|
1185
|
-
|
|
1186
|
-
onCompactStart: (contextPercent) => {
|
|
1187
|
-
try {
|
|
1188
|
-
addMessage("system", `⟳ Compacting context (${contextPercent}% used)...`);
|
|
1189
|
-
} catch {}
|
|
1190
|
-
},
|
|
1191
|
-
|
|
1192
|
-
onCompactEnd: (savedTokens, newPercent) => {
|
|
1193
|
-
try {
|
|
1194
|
-
addMessage("system", `✓ Context compacted — saved ${savedTokens.toLocaleString()} tokens (now ${newPercent}%)`);
|
|
1195
|
-
} catch {}
|
|
1196
|
-
},
|
|
1197
|
-
|
|
1198
|
-
onComplete: () => {
|
|
1199
|
-
const current = streamingTextRef.current.trim();
|
|
1200
|
-
if (current) {
|
|
1201
|
-
addMessage("assistant", current);
|
|
1202
|
-
setStreamingText("");
|
|
1203
|
-
}
|
|
1204
|
-
|
|
1205
|
-
setIsStreaming(false);
|
|
1206
|
-
props.onStatus("Ready");
|
|
1207
|
-
setActiveTool(undefined);
|
|
1208
|
-
props.onActiveTool(undefined);
|
|
1209
|
-
props.onTokens(totalInputRef.current, totalOutputRef.current);
|
|
1210
|
-
// Process queued messages
|
|
1211
|
-
processQueue();
|
|
1212
|
-
},
|
|
1213
|
-
|
|
1214
|
-
onError: (error) => {
|
|
1215
|
-
const current = streamingTextRef.current.trim();
|
|
1216
|
-
if (current) {
|
|
1217
|
-
addMessage("assistant", current);
|
|
1218
|
-
setStreamingText("");
|
|
1219
|
-
}
|
|
1220
|
-
addMessage("system", `Error: ${error.message}`);
|
|
1221
|
-
toast("error", error.message);
|
|
1222
|
-
setIsStreaming(false);
|
|
1223
|
-
props.onStatus("Error");
|
|
1224
|
-
setActiveTool(undefined);
|
|
1225
|
-
props.onActiveTool(undefined);
|
|
1226
|
-
},
|
|
1227
|
-
|
|
1228
|
-
onUsage: (usage) => {
|
|
1229
|
-
turnInput += usage.inputTokens;
|
|
1230
|
-
totalInputRef.current += usage.inputTokens;
|
|
1231
|
-
totalOutputRef.current += usage.outputTokens;
|
|
1232
|
-
props.onTokens(totalInputRef.current, totalOutputRef.current);
|
|
1233
|
-
// Estimate context usage (rough: typical 200k window)
|
|
1234
|
-
const pct = Math.min(100, (turnInput / 200000) * 100);
|
|
1235
|
-
props.onContextPercent(pct);
|
|
1236
|
-
},
|
|
1237
|
-
};
|
|
1238
|
-
|
|
1239
|
-
try {
|
|
1240
|
-
await sdk.agent.run(expandedText, callbacks, images);
|
|
1241
|
-
} catch (err) {
|
|
1242
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
1243
|
-
addMessage("system", `Error: ${msg}`);
|
|
1244
|
-
toast("error", msg);
|
|
1245
|
-
setIsStreaming(false);
|
|
1246
|
-
props.onStatus("Error");
|
|
1247
|
-
}
|
|
1248
|
-
};
|
|
1249
|
-
|
|
1250
|
-
return (
|
|
1251
|
-
<box flexDirection="column" flexGrow={1} paddingLeft={2} paddingRight={2} paddingTop={1} paddingBottom={1} gap={1}>
|
|
1252
|
-
{/* Scrollable message area — directly in layout for proper flex height (like OpenCode) */}
|
|
1253
|
-
<scrollbox
|
|
1254
|
-
stickyScroll={true}
|
|
1255
|
-
stickyStart="bottom"
|
|
1256
|
-
flexGrow={1}
|
|
1257
|
-
scrollY={true}
|
|
1258
|
-
>
|
|
1259
|
-
<MessageList
|
|
1260
|
-
messages={messages}
|
|
1261
|
-
streamingText={streamingText}
|
|
1262
|
-
isStreaming={isStreaming}
|
|
1263
|
-
/>
|
|
1264
|
-
</scrollbox>
|
|
1265
|
-
|
|
1266
|
-
{/* Fixed bottom area — never pushed off screen */}
|
|
1267
|
-
<box flexDirection="column" flexShrink={0}>
|
|
1268
|
-
{/* Permission prompt overlay */}
|
|
1269
|
-
{pendingPermission && (
|
|
1270
|
-
<PermissionPrompt
|
|
1271
|
-
toolName={pendingPermission.toolName}
|
|
1272
|
-
message={pendingPermission.message}
|
|
1273
|
-
onDecision={(decision) => {
|
|
1274
|
-
pendingPermission.resolve(decision);
|
|
1275
|
-
setPendingPermission(null);
|
|
1276
|
-
}}
|
|
1277
|
-
/>
|
|
1278
|
-
)}
|
|
1279
|
-
|
|
1280
|
-
{/* Interrupt/Queue prompt */}
|
|
1281
|
-
{pendingInterrupt && (
|
|
1282
|
-
<InterruptPrompt
|
|
1283
|
-
message={pendingInterrupt.text}
|
|
1284
|
-
onInterrupt={() => handleInterrupt(pendingInterrupt.text, pendingInterrupt.images)}
|
|
1285
|
-
onQueue={() => handleQueue(pendingInterrupt.text)}
|
|
1286
|
-
onCancel={() => setPendingInterrupt(null)}
|
|
1287
|
-
/>
|
|
1288
|
-
)}
|
|
1289
|
-
|
|
1290
|
-
{/* Loading spinner */}
|
|
1291
|
-
{isStreaming && !streamingText && !pendingInterrupt && (
|
|
1292
|
-
<LoadingSpinner label={activeTool || "Thinking..."} />
|
|
1293
|
-
)}
|
|
1294
|
-
|
|
1295
|
-
{/* Input area */}
|
|
1296
|
-
<InputArea
|
|
1297
|
-
onSubmit={sendMessage}
|
|
1298
|
-
disabled={false}
|
|
1299
|
-
suppressInput={props.dialogOpen}
|
|
1300
|
-
placeholder={isStreaming ? "Type to interrupt or queue..." : undefined}
|
|
1301
|
-
workingDir={sdk.workingDir}
|
|
1302
|
-
mode={agentMode}
|
|
1303
|
-
onModeChange={handleModeChange}
|
|
1304
|
-
modelLabel={sdk.model}
|
|
1305
|
-
/>
|
|
1306
|
-
</box>
|
|
1307
|
-
</box>
|
|
1308
|
-
);
|
|
1309
|
-
}
|