@cdoing/opentuicli 0.1.2 → 0.1.18
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 +91 -0
- package/dist/index.js +53 -38
- package/dist/index.js.map +4 -4
- package/package.json +5 -3
- package/src/app.tsx +260 -39
- package/src/components/dialog-command.tsx +110 -107
- package/src/components/dialog-help.tsx +48 -124
- package/src/components/dialog-model.tsx +98 -49
- package/src/components/dialog-status.tsx +46 -84
- package/src/components/dialog-theme.tsx +197 -171
- package/src/components/input-area.tsx +74 -12
- package/src/components/message-list.tsx +250 -42
- package/src/components/permission-prompt.tsx +2 -1
- package/src/components/session-browser.tsx +71 -60
- package/src/components/session-footer.tsx +2 -2
- package/src/components/session-header.tsx +1 -1
- package/src/components/setup-wizard.tsx +149 -70
- package/src/components/sidebar.tsx +66 -13
- package/src/components/status-bar.tsx +2 -2
- package/src/context/theme.tsx +109 -1
- package/src/lib/autocomplete.ts +5 -1
- package/src/routes/home.tsx +2 -2
- package/src/routes/session.tsx +141 -18
- package/src/store/settings.ts +107 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cdoing/opentuicli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.18",
|
|
4
4
|
"description": "OpenTUI-based terminal interface for cdoing agent (inspired by opencode's TUI)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -22,7 +22,8 @@
|
|
|
22
22
|
"@opentui/react": "0.1.87",
|
|
23
23
|
"chalk": "^4.1.2",
|
|
24
24
|
"commander": "^13.1.0",
|
|
25
|
-
"react": "^19.1.0"
|
|
25
|
+
"react": "^19.1.0",
|
|
26
|
+
"zustand": "^5.0.12"
|
|
26
27
|
},
|
|
27
28
|
"devDependencies": {
|
|
28
29
|
"@types/node": "^22.13.10",
|
|
@@ -30,5 +31,6 @@
|
|
|
30
31
|
"esbuild": "^0.25.0",
|
|
31
32
|
"ts-node": "^10.9.2",
|
|
32
33
|
"typescript": "^5.8.2"
|
|
33
|
-
}
|
|
34
|
+
},
|
|
35
|
+
"license": "Apache-2.0"
|
|
34
36
|
}
|
package/src/app.tsx
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* - Session browser overlay (Ctrl+S)
|
|
9
9
|
* - Setup wizard overlay (/setup)
|
|
10
10
|
* - Keyboard-driven navigation
|
|
11
|
-
* -
|
|
11
|
+
* - Command palette (Ctrl+P)
|
|
12
12
|
* - Theme support (dark/light/auto)
|
|
13
13
|
* - Status bar with token counts and context %
|
|
14
14
|
*/
|
|
@@ -17,13 +17,18 @@ import * as fs from "fs";
|
|
|
17
17
|
import * as path from "path";
|
|
18
18
|
import * as os from "os";
|
|
19
19
|
import { createRoot, useKeyboard, useTerminalDimensions } from "@opentui/react";
|
|
20
|
-
import { createCliRenderer, TextAttributes } from "@opentui/core";
|
|
20
|
+
import { createCliRenderer, TextAttributes, RGBA } from "@opentui/core";
|
|
21
21
|
import { useState, useRef, useCallback } from "react";
|
|
22
22
|
import {
|
|
23
23
|
ToolRegistry,
|
|
24
24
|
PermissionManager,
|
|
25
25
|
PermissionMode,
|
|
26
|
+
ProcessManager,
|
|
27
|
+
TodoStore,
|
|
28
|
+
MemoryStore,
|
|
26
29
|
registerAllTools,
|
|
30
|
+
resolveOAuthToken,
|
|
31
|
+
supportsOAuth,
|
|
27
32
|
} from "@cdoing/core";
|
|
28
33
|
import { AgentRunner, getDefaultModel, getApiKeyEnvVar } from "@cdoing/ai";
|
|
29
34
|
import type { ModelConfig } from "@cdoing/ai";
|
|
@@ -31,6 +36,7 @@ import type { ModelConfig } from "@cdoing/ai";
|
|
|
31
36
|
import { ThemeProvider, useTheme, detectTerminalTheme, restoreTerminalBackground, getThemeColors, setTerminalBackground } from "./context/theme";
|
|
32
37
|
import { SDKProvider } from "./context/sdk";
|
|
33
38
|
import { ToastProvider } from "./components/toast";
|
|
39
|
+
import { useSettingsStore } from "./store/settings";
|
|
34
40
|
import { Home } from "./routes/home";
|
|
35
41
|
import { SessionView } from "./routes/session";
|
|
36
42
|
import { StatusBar } from "./components/status-bar";
|
|
@@ -74,19 +80,32 @@ function AppShell(props: {
|
|
|
74
80
|
permissionManager: PermissionManager;
|
|
75
81
|
}) {
|
|
76
82
|
const dims = useTerminalDimensions();
|
|
77
|
-
const { theme, themeId, setMode, setThemeId } = useTheme();
|
|
83
|
+
const { theme, themeId, customBg, setMode, setThemeId } = useTheme();
|
|
78
84
|
const t = theme;
|
|
79
85
|
|
|
80
86
|
const [route, setRoute] = useState<Route>(props.options.prompt ? "session" : "home");
|
|
81
87
|
const [dialog, setDialog] = useState<Dialog>("none");
|
|
82
88
|
const [status, setStatus] = useState("Ready");
|
|
83
|
-
const [provider, setProvider] = useState(props.options.provider);
|
|
84
|
-
const [model, setModel] = useState(props.options.model || getDefaultModel(props.options.provider) || "default");
|
|
85
89
|
const [workingDir, setWorkingDir] = useState(props.options.workingDir);
|
|
86
90
|
const [tokens, setTokens] = useState<{ input: number; output: number } | undefined>();
|
|
87
91
|
const [contextPercent, setContextPercent] = useState(0);
|
|
88
92
|
const [activeTool, setActiveTool] = useState<string | undefined>();
|
|
89
|
-
|
|
93
|
+
|
|
94
|
+
// Persisted settings from Zustand store
|
|
95
|
+
const provider = useSettingsStore((s) => s.provider);
|
|
96
|
+
const model = useSettingsStore((s) => s.model);
|
|
97
|
+
const sidebarMode = useSettingsStore((s) => s.sidebarMode);
|
|
98
|
+
const setProvider = useSettingsStore((s) => s.setProvider);
|
|
99
|
+
const setModel = useSettingsStore((s) => s.setModel);
|
|
100
|
+
const setSidebarMode = useSettingsStore((s) => s.setSidebarMode);
|
|
101
|
+
|
|
102
|
+
// Auto-hide sidebar when terminal is too narrow (like opencode: > 120 cols)
|
|
103
|
+
const wide = dims.width > 120;
|
|
104
|
+
const showSidebar = sidebarMode === "show" || (sidebarMode === "auto" && wide);
|
|
105
|
+
|
|
106
|
+
const closeDialog = useCallback(() => {
|
|
107
|
+
setDialog("none");
|
|
108
|
+
}, []);
|
|
90
109
|
|
|
91
110
|
// Mutable refs for agent rebuild
|
|
92
111
|
const agentRef = useRef(props.agent);
|
|
@@ -119,11 +138,34 @@ function AppShell(props: {
|
|
|
119
138
|
});
|
|
120
139
|
|
|
121
140
|
// ── Agent Rebuild ──────────────────────────────────
|
|
122
|
-
const rebuildAgent = useCallback((newProvider: string, newModel: string, apiKey?: string) => {
|
|
123
|
-
// Resolve API key
|
|
141
|
+
const rebuildAgent = useCallback((newProvider: string, newModel: string, apiKey?: string, oauthToken?: string) => {
|
|
142
|
+
// Resolve API key or OAuth token
|
|
124
143
|
// If apiKey is explicitly "" (empty string), it means logout — skip all fallbacks
|
|
125
144
|
let resolvedKey = apiKey;
|
|
126
|
-
|
|
145
|
+
let resolvedOAuthToken = oauthToken;
|
|
146
|
+
|
|
147
|
+
if (!resolvedKey && !resolvedOAuthToken && apiKey !== "") {
|
|
148
|
+
// Try OAuth first for providers that support it
|
|
149
|
+
if (supportsOAuth(newProvider)) {
|
|
150
|
+
resolveOAuthToken(newProvider).then((token) => {
|
|
151
|
+
if (token) {
|
|
152
|
+
const modelConfig: Partial<ModelConfig> = {
|
|
153
|
+
provider: newProvider,
|
|
154
|
+
model: newModel,
|
|
155
|
+
oauthToken: token,
|
|
156
|
+
baseURL: props.options.baseUrl || undefined,
|
|
157
|
+
temperature: 0,
|
|
158
|
+
maxTokens: 8096,
|
|
159
|
+
};
|
|
160
|
+
const newAgent = new AgentRunner(modelConfig, registryRef.current, pmRef.current);
|
|
161
|
+
agentRef.current = newAgent;
|
|
162
|
+
setProvider(newProvider);
|
|
163
|
+
setModel(newModel);
|
|
164
|
+
}
|
|
165
|
+
}).catch(() => {});
|
|
166
|
+
// If OAuth token is being resolved async, still try API key fallback synchronously
|
|
167
|
+
}
|
|
168
|
+
|
|
127
169
|
const envVar = getApiKeyEnvVar(newProvider);
|
|
128
170
|
if (process.env[envVar]) {
|
|
129
171
|
resolvedKey = process.env[envVar];
|
|
@@ -142,6 +184,7 @@ function AppShell(props: {
|
|
|
142
184
|
provider: newProvider,
|
|
143
185
|
model: newModel,
|
|
144
186
|
apiKey: resolvedKey || undefined,
|
|
187
|
+
oauthToken: resolvedOAuthToken || undefined,
|
|
145
188
|
baseURL: props.options.baseUrl || undefined,
|
|
146
189
|
temperature: 0,
|
|
147
190
|
maxTokens: 8096,
|
|
@@ -161,8 +204,20 @@ function AppShell(props: {
|
|
|
161
204
|
// ── Global Keyboard ──────────────────────────────────
|
|
162
205
|
|
|
163
206
|
useKeyboard((key: any) => {
|
|
164
|
-
// Don't intercept keys when a dialog is open (
|
|
165
|
-
|
|
207
|
+
// Don't intercept keys when a dialog is open (let the dialog handle them)
|
|
208
|
+
// Exception: Ctrl+C and Escape should always work
|
|
209
|
+
if (dialog !== "none") {
|
|
210
|
+
if (key.ctrl && key.name === "c") {
|
|
211
|
+
const cleanup = (globalThis as any).__cdoingCleanup;
|
|
212
|
+
if (cleanup) cleanup();
|
|
213
|
+
process.exit(0);
|
|
214
|
+
}
|
|
215
|
+
if (key.name === "escape") {
|
|
216
|
+
setDialog("none");
|
|
217
|
+
setRoute("home");
|
|
218
|
+
}
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
166
221
|
|
|
167
222
|
// Ctrl+C — graceful quit
|
|
168
223
|
if (key.ctrl && key.name === "c") {
|
|
@@ -175,9 +230,9 @@ function AppShell(props: {
|
|
|
175
230
|
setRoute("session");
|
|
176
231
|
setStatus("Ready");
|
|
177
232
|
}
|
|
178
|
-
// Ctrl+P —
|
|
233
|
+
// Ctrl+P — command palette
|
|
179
234
|
if (key.ctrl && key.name === "p") {
|
|
180
|
-
setDialog((d) => (d === "
|
|
235
|
+
setDialog((d) => (d === "command" ? "none" : "command"));
|
|
181
236
|
}
|
|
182
237
|
// Ctrl+S — session browser
|
|
183
238
|
if (key.ctrl && key.name === "s") {
|
|
@@ -185,15 +240,15 @@ function AppShell(props: {
|
|
|
185
240
|
}
|
|
186
241
|
// Ctrl+B — toggle sidebar
|
|
187
242
|
if (key.ctrl && key.name === "b") {
|
|
188
|
-
|
|
243
|
+
setSidebarMode(sidebarMode === "hide" ? "show" : sidebarMode === "show" ? "hide" : showSidebar ? "hide" : "show");
|
|
189
244
|
}
|
|
190
245
|
// Ctrl+T — theme picker
|
|
191
246
|
if (key.ctrl && key.name === "t") {
|
|
192
247
|
setDialog((d) => (d === "theme" ? "none" : "theme"));
|
|
193
248
|
}
|
|
194
|
-
// Ctrl+
|
|
195
|
-
if (key.ctrl && key.name === "
|
|
196
|
-
setDialog((d) => (d === "
|
|
249
|
+
// Ctrl+O — model picker (Ctrl+M = Enter in terminals)
|
|
250
|
+
if (key.ctrl && key.name === "o") {
|
|
251
|
+
setDialog((d) => (d === "model" ? "none" : "model"));
|
|
197
252
|
}
|
|
198
253
|
// F1 — help dialog
|
|
199
254
|
if (key.name === "f1") {
|
|
@@ -214,9 +269,9 @@ function AppShell(props: {
|
|
|
214
269
|
}, []);
|
|
215
270
|
|
|
216
271
|
return (
|
|
217
|
-
<box width={dims.width} height={dims.height} flexDirection="column">
|
|
272
|
+
<box width={dims.width} height={dims.height} flexDirection="column" backgroundColor={customBg ? RGBA.fromHex(customBg) : t.bg}>
|
|
218
273
|
{/* Header bar */}
|
|
219
|
-
<box height={1} flexDirection="row" paddingX={1} flexShrink={0}>
|
|
274
|
+
<box height={1} flexDirection="row" paddingX={1} flexShrink={0} backgroundColor={t.bgSubtle}>
|
|
220
275
|
<text fg={t.primary} attributes={TextAttributes.BOLD}>{"cdoing"}</text>
|
|
221
276
|
<text fg={t.border}>{" │ "}</text>
|
|
222
277
|
<text fg={t.textMuted}>{model}</text>
|
|
@@ -267,7 +322,7 @@ function AppShell(props: {
|
|
|
267
322
|
) : dialog === "setup" ? (
|
|
268
323
|
<SetupWizard
|
|
269
324
|
onComplete={(config) => {
|
|
270
|
-
rebuildAgent(config.provider, config.model, config.apiKey);
|
|
325
|
+
rebuildAgent(config.provider, config.model, config.apiKey, config.oauthToken);
|
|
271
326
|
setDialog("none");
|
|
272
327
|
}}
|
|
273
328
|
onClose={() => setDialog("none")}
|
|
@@ -291,11 +346,19 @@ function AppShell(props: {
|
|
|
291
346
|
onContextPercent={setContextPercent}
|
|
292
347
|
onOpenDialog={(d) => setDialog(d as Dialog)}
|
|
293
348
|
initialMessage={initialMessageRef.current}
|
|
349
|
+
dialogOpen={dialog !== "none"}
|
|
294
350
|
/>
|
|
295
351
|
)}
|
|
296
352
|
</box>
|
|
297
353
|
</SDKProvider>
|
|
298
354
|
|
|
355
|
+
{/* Vertical border between content and sidebar */}
|
|
356
|
+
{showSidebar && (
|
|
357
|
+
<box width={1} flexShrink={0}>
|
|
358
|
+
<text fg={t.border}>{"│\n".repeat(Math.max(dims.height - 4, 1))}</text>
|
|
359
|
+
</box>
|
|
360
|
+
)}
|
|
361
|
+
|
|
299
362
|
{/* Sidebar (right panel) */}
|
|
300
363
|
{showSidebar && (
|
|
301
364
|
<Sidebar
|
|
@@ -368,6 +431,7 @@ function AppShell(props: {
|
|
|
368
431
|
break;
|
|
369
432
|
// Model
|
|
370
433
|
case "model:switch":
|
|
434
|
+
case "model:provider":
|
|
371
435
|
setDialog("model");
|
|
372
436
|
break;
|
|
373
437
|
// Theme
|
|
@@ -382,11 +446,13 @@ function AppShell(props: {
|
|
|
382
446
|
break;
|
|
383
447
|
// Display
|
|
384
448
|
case "display:sidebar":
|
|
385
|
-
|
|
449
|
+
setSidebarMode(sidebarMode === "hide" ? "show" : sidebarMode === "show" ? "hide" : showSidebar ? "hide" : "show");
|
|
386
450
|
break;
|
|
387
|
-
|
|
388
|
-
case "
|
|
389
|
-
|
|
451
|
+
// Tools (dispatch as slash commands into session)
|
|
452
|
+
case "tool:shell":
|
|
453
|
+
case "tool:search":
|
|
454
|
+
case "tool:tree":
|
|
455
|
+
setRoute("session");
|
|
390
456
|
break;
|
|
391
457
|
// System
|
|
392
458
|
case "system:status":
|
|
@@ -431,6 +497,96 @@ function AppShell(props: {
|
|
|
431
497
|
{dialog === "status" && (
|
|
432
498
|
<DialogStatus onClose={() => setDialog("none")} />
|
|
433
499
|
)}
|
|
500
|
+
|
|
501
|
+
</box>
|
|
502
|
+
);
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
// ── Error Boundary ───────────────────────────────────────
|
|
506
|
+
|
|
507
|
+
// Global error signal — set by uncaughtException/unhandledRejection handlers,
|
|
508
|
+
// read by the AppRoot wrapper to swap in the error screen.
|
|
509
|
+
let __fatalError: Error | null = null;
|
|
510
|
+
let __fatalErrorSetter: ((err: Error | null) => void) | null = null;
|
|
511
|
+
|
|
512
|
+
function AppRoot(props: {
|
|
513
|
+
children: any;
|
|
514
|
+
}) {
|
|
515
|
+
const [error, setError] = useState<Error | null>(__fatalError);
|
|
516
|
+
__fatalErrorSetter = setError;
|
|
517
|
+
|
|
518
|
+
if (error) {
|
|
519
|
+
return <ErrorScreen error={error} onReset={() => setError(null)} />;
|
|
520
|
+
}
|
|
521
|
+
return props.children;
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
function ErrorScreen(props: {
|
|
525
|
+
error: Error;
|
|
526
|
+
onReset: () => void;
|
|
527
|
+
}) {
|
|
528
|
+
const dims = useTerminalDimensions();
|
|
529
|
+
const maxW = Math.max(dims.width, 40);
|
|
530
|
+
|
|
531
|
+
const colors = {
|
|
532
|
+
bg: "#0a0a0a",
|
|
533
|
+
text: "#eeeeee",
|
|
534
|
+
muted: "#808080",
|
|
535
|
+
primary: "#fab283",
|
|
536
|
+
error: "#ff6b6b",
|
|
537
|
+
};
|
|
538
|
+
|
|
539
|
+
const issueURL = `https://github.com/AhmadMuj/cdoing-agent/issues/new?title=${encodeURIComponent(`tui: fatal: ${props.error.message}`)}&body=${encodeURIComponent("```\n" + (props.error.stack || props.error.message).substring(0, 4000) + "\n```")}`;
|
|
540
|
+
|
|
541
|
+
useKeyboard((key: any) => {
|
|
542
|
+
if (key.ctrl && key.name === "c") {
|
|
543
|
+
const cleanup = (globalThis as any).__cdoingCleanup;
|
|
544
|
+
if (cleanup) cleanup();
|
|
545
|
+
else process.exit(1);
|
|
546
|
+
}
|
|
547
|
+
if (key.name === "r") {
|
|
548
|
+
props.onReset();
|
|
549
|
+
}
|
|
550
|
+
if (key.name === "q" || key.name === "escape") {
|
|
551
|
+
const cleanup = (globalThis as any).__cdoingCleanup;
|
|
552
|
+
if (cleanup) cleanup();
|
|
553
|
+
else process.exit(0);
|
|
554
|
+
}
|
|
555
|
+
});
|
|
556
|
+
|
|
557
|
+
const stackLines = (props.error.stack || "").split("\n").slice(0, Math.max(5, dims.height - 12));
|
|
558
|
+
|
|
559
|
+
return (
|
|
560
|
+
<box
|
|
561
|
+
width={dims.width}
|
|
562
|
+
height={dims.height}
|
|
563
|
+
flexDirection="column"
|
|
564
|
+
backgroundColor={colors.bg}
|
|
565
|
+
paddingX={2}
|
|
566
|
+
paddingY={1}
|
|
567
|
+
>
|
|
568
|
+
<text fg={colors.error} attributes={TextAttributes.BOLD}>
|
|
569
|
+
{" A fatal error occurred!"}
|
|
570
|
+
</text>
|
|
571
|
+
<text>{""}</text>
|
|
572
|
+
<text fg={colors.text} attributes={TextAttributes.BOLD}>
|
|
573
|
+
{` ${props.error.message}`}
|
|
574
|
+
</text>
|
|
575
|
+
<text>{""}</text>
|
|
576
|
+
<text fg={colors.muted}>{" Stack trace:"}</text>
|
|
577
|
+
{stackLines.map((line, i) => (
|
|
578
|
+
<text key={i} fg={colors.muted}>
|
|
579
|
+
{` ${line}`}
|
|
580
|
+
</text>
|
|
581
|
+
))}
|
|
582
|
+
<text>{""}</text>
|
|
583
|
+
<text fg={colors.primary}>{" Report this issue:"}</text>
|
|
584
|
+
<text fg={colors.muted}>{` ${issueURL.length > maxW - 4 ? issueURL.substring(0, maxW - 7) + "..." : issueURL}`}</text>
|
|
585
|
+
<text>{""}</text>
|
|
586
|
+
<box height={1} flexShrink={0}>
|
|
587
|
+
<text fg={colors.muted}>{"─".repeat(maxW)}</text>
|
|
588
|
+
</box>
|
|
589
|
+
<text fg={colors.text}>{" r Reset TUI • q/Esc Exit • Ctrl+C Force quit"}</text>
|
|
434
590
|
</box>
|
|
435
591
|
);
|
|
436
592
|
}
|
|
@@ -451,13 +607,25 @@ export async function startTUI(options: TUIOptions): Promise<void> {
|
|
|
451
607
|
return "allow";
|
|
452
608
|
});
|
|
453
609
|
|
|
610
|
+
const processManager = new ProcessManager();
|
|
611
|
+
const todoStore = new TodoStore();
|
|
612
|
+
const memoryStore = new MemoryStore(options.workingDir);
|
|
454
613
|
await registerAllTools(registry, {
|
|
455
614
|
workingDir: options.workingDir,
|
|
456
615
|
permissionManager: pm,
|
|
616
|
+
processManager,
|
|
617
|
+
todoStore,
|
|
618
|
+
memoryStore,
|
|
619
|
+
planExitCallback: (summary: string) => {
|
|
620
|
+
// Signal that plan is ready — the session component handles the approval via /plan approve
|
|
621
|
+
console.log("\n 📋 Plan ready: " + summary);
|
|
622
|
+
console.log(" Use /plan approve, /plan reject, or /plan show\n");
|
|
623
|
+
},
|
|
457
624
|
});
|
|
458
625
|
|
|
459
|
-
// Resolve API key: flag → env var → stored config
|
|
626
|
+
// Resolve API key: flag → env var → stored config → OAuth token
|
|
460
627
|
let resolvedApiKey = options.apiKey;
|
|
628
|
+
let resolvedOAuthToken: string | undefined;
|
|
461
629
|
let resolvedProvider = options.provider;
|
|
462
630
|
let resolvedModel = options.model;
|
|
463
631
|
let resolvedBaseUrl = options.baseUrl;
|
|
@@ -492,13 +660,39 @@ export async function startTUI(options: TUIOptions): Promise<void> {
|
|
|
492
660
|
else if (storedConfig.apiKeys?.[resolvedProvider]) {
|
|
493
661
|
resolvedApiKey = storedConfig.apiKeys[resolvedProvider];
|
|
494
662
|
}
|
|
663
|
+
|
|
664
|
+
// If no API key found, try OAuth token
|
|
665
|
+
if (!resolvedApiKey && supportsOAuth(resolvedProvider)) {
|
|
666
|
+
try {
|
|
667
|
+
const token = await resolveOAuthToken(resolvedProvider);
|
|
668
|
+
if (token) resolvedOAuthToken = token;
|
|
669
|
+
} catch {}
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
// Hydrate settings store with resolved CLI values (overrides persisted defaults when flags are explicit)
|
|
674
|
+
const settingsStore = useSettingsStore.getState();
|
|
675
|
+
if (resolvedProvider && resolvedProvider !== "anthropic") {
|
|
676
|
+
settingsStore.setProvider(resolvedProvider);
|
|
677
|
+
} else if (!settingsStore.provider || settingsStore.provider === "anthropic") {
|
|
678
|
+
settingsStore.setProvider(resolvedProvider);
|
|
679
|
+
}
|
|
680
|
+
if (resolvedModel) {
|
|
681
|
+
settingsStore.setModel(resolvedModel);
|
|
682
|
+
} else if (!settingsStore.model) {
|
|
683
|
+
settingsStore.setModel(getDefaultModel(settingsStore.provider) || "default");
|
|
495
684
|
}
|
|
496
685
|
|
|
497
|
-
//
|
|
686
|
+
// Use persisted values as the effective config (store is now hydrated)
|
|
687
|
+
const effectiveProvider = useSettingsStore.getState().provider;
|
|
688
|
+
const effectiveModel = useSettingsStore.getState().model;
|
|
689
|
+
|
|
690
|
+
// Build model config (use effective values from persisted store)
|
|
498
691
|
const modelConfig: Partial<ModelConfig> = {
|
|
499
|
-
provider:
|
|
500
|
-
model:
|
|
692
|
+
provider: effectiveProvider,
|
|
693
|
+
model: effectiveModel || undefined,
|
|
501
694
|
apiKey: resolvedApiKey || undefined,
|
|
695
|
+
oauthToken: resolvedOAuthToken || undefined,
|
|
502
696
|
baseURL: resolvedBaseUrl || undefined,
|
|
503
697
|
temperature: 0,
|
|
504
698
|
maxTokens: 8096,
|
|
@@ -517,9 +711,24 @@ export async function startTUI(options: TUIOptions): Promise<void> {
|
|
|
517
711
|
const resolvedMode: "dark" | "light" = options.theme === "light" ? "light"
|
|
518
712
|
: options.theme === "auto" ? (detectedMode || "dark")
|
|
519
713
|
: "dark";
|
|
520
|
-
|
|
714
|
+
// Hydrate theme settings from store
|
|
715
|
+
const persistedThemeId = useSettingsStore.getState().themeId;
|
|
716
|
+
const persistedMode = useSettingsStore.getState().mode;
|
|
717
|
+
if (options.theme !== "light" && options.theme !== "dark") {
|
|
718
|
+
// "auto" mode — use persisted mode if available
|
|
719
|
+
if (persistedMode) settingsStore.setMode(persistedMode);
|
|
720
|
+
} else {
|
|
721
|
+
settingsStore.setMode(options.theme === "light" ? "light" : "dark");
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
const initialColors = getThemeColors(persistedThemeId || "default", resolvedMode);
|
|
521
725
|
setTerminalBackground(initialColors.bg);
|
|
522
726
|
|
|
727
|
+
// Reset terminal size to a good default (80x24 minimum)
|
|
728
|
+
const cols = Math.max(process.stdout.columns || 80, 80);
|
|
729
|
+
const rows = Math.max(process.stdout.rows || 24, 24);
|
|
730
|
+
process.stdout.write(`\x1b[8;${rows};${cols}t`);
|
|
731
|
+
|
|
523
732
|
console.clear();
|
|
524
733
|
|
|
525
734
|
// Set terminal title on mount
|
|
@@ -530,17 +739,29 @@ export async function startTUI(options: TUIOptions): Promise<void> {
|
|
|
530
739
|
exitOnCtrlC: false,
|
|
531
740
|
});
|
|
532
741
|
const root = createRoot(renderer);
|
|
742
|
+
// Install global error handlers to catch uncaught exceptions and show the error screen
|
|
743
|
+
const handleFatalError = (err: unknown) => {
|
|
744
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
745
|
+
process.stderr.write(`\n[cdoing] Fatal error: ${error.message}\n${error.stack || ""}\n`);
|
|
746
|
+
__fatalError = error;
|
|
747
|
+
if (__fatalErrorSetter) __fatalErrorSetter(error);
|
|
748
|
+
};
|
|
749
|
+
process.on("uncaughtException", handleFatalError);
|
|
750
|
+
process.on("unhandledRejection", handleFatalError);
|
|
751
|
+
|
|
533
752
|
root.render(
|
|
534
|
-
<
|
|
535
|
-
<
|
|
536
|
-
<
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
753
|
+
<AppRoot>
|
|
754
|
+
<ThemeProvider mode={options.theme} themeId={persistedThemeId} detectedMode={detectedMode} syncTerminalBg>
|
|
755
|
+
<ToastProvider>
|
|
756
|
+
<AppShell
|
|
757
|
+
options={{ ...options, provider: effectiveProvider, model: effectiveModel || undefined }}
|
|
758
|
+
agent={agent}
|
|
759
|
+
registry={registry}
|
|
760
|
+
permissionManager={pm}
|
|
761
|
+
/>
|
|
762
|
+
</ToastProvider>
|
|
763
|
+
</ThemeProvider>
|
|
764
|
+
</AppRoot>
|
|
544
765
|
);
|
|
545
766
|
|
|
546
767
|
// Graceful cleanup: unmount React, destroy renderer, restore terminal
|