@matterailab/orbcode 0.2.2 → 0.2.3
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 +351 -8
- package/dist/commands/mcp.js +266 -0
- package/dist/config/settings.js +27 -0
- package/dist/core/agent.js +28 -7
- package/dist/headless.js +18 -0
- package/dist/index.js +9 -0
- package/dist/mcp/auth.js +289 -0
- package/dist/mcp/client.js +132 -0
- package/dist/mcp/config.js +270 -0
- package/dist/mcp/manager.js +277 -0
- package/dist/mcp/types.js +8 -0
- package/dist/memory/loader.js +167 -0
- package/dist/memory/types.js +1 -0
- package/dist/prompts/system.js +26 -7
- package/dist/skills/loader.js +132 -0
- package/dist/skills/types.js +1 -0
- package/dist/tools/executors/skills.js +28 -0
- package/dist/tools/index.js +22 -3
- package/dist/tools/schemas/index.js +6 -3
- package/dist/tools/schemas/use_skill.js +1 -1
- package/dist/ui/App.js +205 -50
- package/dist/ui/components/McpApprovalPrompt.js +61 -0
- package/dist/ui/components/McpAuthScreen.js +55 -0
- package/dist/ui/components/McpPicker.js +262 -0
- package/dist/ui/components/StatusBar.js +28 -9
- package/dist/utils/clipboard.js +45 -0
- package/package.json +2 -1
package/dist/ui/App.js
CHANGED
|
@@ -1,18 +1,21 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
2
|
+
import { useCallback, useEffect, useMemo, useRef, useState, } from "react";
|
|
3
3
|
import { Box, Static, Text, useApp, useInput } from "ink";
|
|
4
4
|
import open from "open";
|
|
5
5
|
import { COLORS, VERSION } from "../branding.js";
|
|
6
|
-
import { BUILTIN_AXON_MODELS, getModel, isValidAxonModel } from "../api/models.js";
|
|
6
|
+
import { BUILTIN_AXON_MODELS, getModel, isValidAxonModel, } from "../api/models.js";
|
|
7
7
|
import { LoginView } from "./LoginView.js";
|
|
8
|
-
import { APP_URL,
|
|
9
|
-
import { getAuthToken, getPendingProjectHooks, loadSettings, saveSettings, trustProjectHooks, } from "../config/settings.js";
|
|
8
|
+
import { APP_URL, fetchProfile, fetchTaskTitle, } from "../auth/auth.js";
|
|
9
|
+
import { getAuthToken, getPendingProjectHooks, loadSettings, saveMcpApproval, saveSettings, trustProjectHooks, } from "../config/settings.js";
|
|
10
10
|
import { Agent } from "../core/agent.js";
|
|
11
|
+
import { McpManager } from "../mcp/manager.js";
|
|
11
12
|
import { Spinner } from "./components/Spinner.js";
|
|
12
13
|
import { InputBox } from "./components/InputBox.js";
|
|
13
14
|
import { ApprovalPrompt } from "./components/ApprovalPrompt.js";
|
|
14
15
|
import { FollowupPrompt } from "./components/FollowupPrompt.js";
|
|
15
16
|
import { HookTrustPrompt } from "./components/HookTrustPrompt.js";
|
|
17
|
+
import { McpApprovalPrompt } from "./components/McpApprovalPrompt.js";
|
|
18
|
+
import { McpPicker } from "./components/McpPicker.js";
|
|
16
19
|
import { StatusBar } from "./components/StatusBar.js";
|
|
17
20
|
import { ModelPicker } from "./components/ModelPicker.js";
|
|
18
21
|
import { SessionPicker } from "./components/SessionPicker.js";
|
|
@@ -21,16 +24,38 @@ import { RowView, formatToolName } from "./components/rows.js";
|
|
|
21
24
|
const SLASH_COMMANDS = [
|
|
22
25
|
{ name: "/help", description: "show available commands" },
|
|
23
26
|
{ name: "/model", description: "select the Axon model to use" },
|
|
24
|
-
{
|
|
27
|
+
{
|
|
28
|
+
name: "/clear",
|
|
29
|
+
description: "clear the screen — the conversation continues",
|
|
30
|
+
},
|
|
25
31
|
{ name: "/new", description: "start a new conversation with a clean slate" },
|
|
26
32
|
{ name: "/resume", description: "resume a previous session" },
|
|
27
|
-
{
|
|
33
|
+
{
|
|
34
|
+
name: "/compact",
|
|
35
|
+
description: "summarize the conversation to free up context",
|
|
36
|
+
},
|
|
28
37
|
{ name: "/tasks", description: "show the current task list" },
|
|
29
|
-
{
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
{ name: "/
|
|
38
|
+
{
|
|
39
|
+
name: "/status",
|
|
40
|
+
description: "show session status (model, context, cost, account)",
|
|
41
|
+
},
|
|
42
|
+
{ name: "/usage", description: "fetch plan usage" },
|
|
43
|
+
{
|
|
44
|
+
name: "/init",
|
|
45
|
+
description: "analyze this codebase and create an AGENTS.md",
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
name: "/mcp",
|
|
49
|
+
description: "manage MCP servers — enable, disable, reconnect, view status",
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
name: "/commit",
|
|
53
|
+
description: "check pending changes and create detailed commits",
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
name: "/code-review",
|
|
57
|
+
description: "expert review of pending changes: performance, security, bugs, tests",
|
|
58
|
+
},
|
|
34
59
|
{ name: "/analytics", description: "open your MatterAI analytics dashboard" },
|
|
35
60
|
{ name: "/login", description: "sign in to MatterAI" },
|
|
36
61
|
{ name: "/logout", description: "sign out and remove the saved token" },
|
|
@@ -64,17 +89,11 @@ function formatRelativeTime(isoStr) {
|
|
|
64
89
|
return `in ${min}m`;
|
|
65
90
|
return "soon";
|
|
66
91
|
}
|
|
67
|
-
/** Human lines for the /status and /
|
|
92
|
+
/** Human lines for the /status and /usage usage block (extension profile data). */
|
|
68
93
|
function usageLines(profile) {
|
|
69
94
|
const lines = [];
|
|
70
95
|
if (profile.plan)
|
|
71
|
-
lines.push(`Plan ${profile.plan}`);
|
|
72
|
-
if (typeof profile.usagePercentage === "number") {
|
|
73
|
-
lines.push(`Usage ${profile.usagePercentage}% used · ${Math.max(0, 100 - profile.usagePercentage)}% remaining`);
|
|
74
|
-
}
|
|
75
|
-
if (typeof profile.remainingReviews === "number") {
|
|
76
|
-
lines.push(`Reviews ${profile.remainingReviews} remaining`);
|
|
77
|
-
}
|
|
96
|
+
lines.push(`Plan ${profile.plan?.toUpperCase()}`);
|
|
78
97
|
if (profile.tieredUsage) {
|
|
79
98
|
const ws = [
|
|
80
99
|
["5hr", "5-Hour", profile.tieredUsage.fiveHour],
|
|
@@ -82,10 +101,9 @@ function usageLines(profile) {
|
|
|
82
101
|
["mo", "Monthly", profile.tieredUsage.monthly],
|
|
83
102
|
];
|
|
84
103
|
for (const [short, label, w] of ws) {
|
|
85
|
-
const remaining = Math.max(0, w.remaining || 0);
|
|
86
104
|
const pct = Math.max(0, Math.min(100, w.percentage || 0));
|
|
87
105
|
const reset = formatRelativeTime(w.resetsAt);
|
|
88
|
-
lines.push(`${label.padEnd(12)}
|
|
106
|
+
lines.push(`${label.padEnd(12)}${pct}% used · Resets ${reset}`);
|
|
89
107
|
}
|
|
90
108
|
}
|
|
91
109
|
else if (profile.creditsResetDate) {
|
|
@@ -149,7 +167,12 @@ export function App({ initialView, initialPrompt, initialSession, updateCheck, }
|
|
|
149
167
|
const [view, setView] = useState(initialView ?? (getAuthToken(settings) ? "chat" : "login"));
|
|
150
168
|
const [updateInfo, setUpdateInfo] = useState(null);
|
|
151
169
|
const [rows, setRows] = useState(() => [
|
|
152
|
-
{
|
|
170
|
+
{
|
|
171
|
+
kind: "header",
|
|
172
|
+
id: "header",
|
|
173
|
+
cwd: process.cwd(),
|
|
174
|
+
modelName: getModel(loadSettings().model).name,
|
|
175
|
+
},
|
|
153
176
|
]);
|
|
154
177
|
const [busy, setBusy] = useState(false);
|
|
155
178
|
const [busyLabel, setBusyLabel] = useState("Thinking");
|
|
@@ -167,6 +190,12 @@ export function App({ initialView, initialPrompt, initialSession, updateCheck, }
|
|
|
167
190
|
const [queuedMessages, setQueuedMessages] = useState([]);
|
|
168
191
|
const [modelPickerOpen, setModelPickerOpen] = useState(false);
|
|
169
192
|
const [resumableSessions, setResumableSessions] = useState(null);
|
|
193
|
+
// MCP manager (created once, shared across agents in this process). Null until
|
|
194
|
+
// the first agent is created so we don't spawn servers before login.
|
|
195
|
+
const mcpManagerRef = useRef(null);
|
|
196
|
+
const [mcpPickerOpen, setMcpPickerOpen] = useState(false);
|
|
197
|
+
// Project-scope MCP servers awaiting the user's approval at startup.
|
|
198
|
+
const [pendingMcpApproval, setPendingMcpApproval] = useState(null);
|
|
170
199
|
const [staticKey, setStaticKey] = useState(0);
|
|
171
200
|
const [tasks, setTasks] = useState("");
|
|
172
201
|
const [contextTokens, setContextTokens] = useState(0);
|
|
@@ -184,7 +213,11 @@ export function App({ initialView, initialPrompt, initialSession, updateCheck, }
|
|
|
184
213
|
if (!token)
|
|
185
214
|
return;
|
|
186
215
|
fetchProfile(token)
|
|
187
|
-
.then((profile) => setUsage({
|
|
216
|
+
.then((profile) => setUsage({
|
|
217
|
+
plan: profile.plan,
|
|
218
|
+
usagePercentage: profile.usagePercentage,
|
|
219
|
+
tieredUsage: profile.tieredUsage,
|
|
220
|
+
}))
|
|
188
221
|
.catch(() => { });
|
|
189
222
|
}, []);
|
|
190
223
|
const agentRef = useRef(null);
|
|
@@ -243,7 +276,12 @@ export function App({ initialView, initialPrompt, initialSession, updateCheck, }
|
|
|
243
276
|
process.stdout.write("\x1b[2J\x1b[H");
|
|
244
277
|
}
|
|
245
278
|
setRows([
|
|
246
|
-
{
|
|
279
|
+
{
|
|
280
|
+
kind: "header",
|
|
281
|
+
id: rowId(),
|
|
282
|
+
cwd: process.cwd(),
|
|
283
|
+
modelName: getModel(loadSettings().model).name,
|
|
284
|
+
},
|
|
247
285
|
]);
|
|
248
286
|
setStaticKey((k) => k + 1);
|
|
249
287
|
}, []);
|
|
@@ -275,7 +313,11 @@ export function App({ initialView, initialPrompt, initialSession, updateCheck, }
|
|
|
275
313
|
setStreamingText("");
|
|
276
314
|
break;
|
|
277
315
|
case "tool-start":
|
|
278
|
-
setRunningTool({
|
|
316
|
+
setRunningTool({
|
|
317
|
+
id: event.id,
|
|
318
|
+
name: event.name,
|
|
319
|
+
summary: event.summary,
|
|
320
|
+
});
|
|
279
321
|
setBusyLabel(`Running ${event.name}`);
|
|
280
322
|
break;
|
|
281
323
|
case "tool-end":
|
|
@@ -305,7 +347,10 @@ export function App({ initialView, initialPrompt, initialSession, updateCheck, }
|
|
|
305
347
|
pushRow({ kind: "completion", text: event.result });
|
|
306
348
|
break;
|
|
307
349
|
case "system":
|
|
308
|
-
pushRow({
|
|
350
|
+
pushRow({
|
|
351
|
+
kind: event.isError ? "error" : "info",
|
|
352
|
+
text: event.message,
|
|
353
|
+
});
|
|
309
354
|
break;
|
|
310
355
|
case "error":
|
|
311
356
|
pushRow({ kind: "error", text: event.message });
|
|
@@ -346,6 +391,13 @@ export function App({ initialView, initialPrompt, initialSession, updateCheck, }
|
|
|
346
391
|
}, [pushRow, maybeFetchTitle, refreshUsage, drainQueue]);
|
|
347
392
|
const createAgent = useCallback((resume) => {
|
|
348
393
|
const current = loadSettings();
|
|
394
|
+
// Create the MCP manager once per process and reuse it across agents
|
|
395
|
+
// (so /new and /resume keep the same server connections). It reads
|
|
396
|
+
// the enabled/disabled lists from settings and connects to approved
|
|
397
|
+
// servers lazily on start().
|
|
398
|
+
if (!mcpManagerRef.current) {
|
|
399
|
+
mcpManagerRef.current = new McpManager(process.cwd(), current.disabledMcpServers ?? [], current.enabledMcpServers ?? []);
|
|
400
|
+
}
|
|
349
401
|
return new Agent({
|
|
350
402
|
cwd: process.cwd(),
|
|
351
403
|
token: getAuthToken(current),
|
|
@@ -355,6 +407,7 @@ export function App({ initialView, initialPrompt, initialSession, updateCheck, }
|
|
|
355
407
|
autoApproveEdits: current.autoApproveEdits,
|
|
356
408
|
autoApproveSafeCommands: current.autoApproveSafeCommands,
|
|
357
409
|
hooks: current.hooks,
|
|
410
|
+
mcp: mcpManagerRef.current,
|
|
358
411
|
resume,
|
|
359
412
|
callbacks: {
|
|
360
413
|
onEvent: handleEvent,
|
|
@@ -393,23 +446,33 @@ export function App({ initialView, initialPrompt, initialSession, updateCheck, }
|
|
|
393
446
|
if (match)
|
|
394
447
|
pushRow({ kind: "user", text: match[1] });
|
|
395
448
|
}
|
|
396
|
-
else if (message.role === "assistant" &&
|
|
449
|
+
else if (message.role === "assistant" &&
|
|
450
|
+
typeof message.content === "string" &&
|
|
451
|
+
message.content.trim()) {
|
|
397
452
|
pushRow({ kind: "assistant", text: message.content });
|
|
398
453
|
}
|
|
399
454
|
}
|
|
400
|
-
pushRow({
|
|
455
|
+
pushRow({
|
|
456
|
+
kind: "info",
|
|
457
|
+
text: `Resumed session: ${session.title || session.id}`,
|
|
458
|
+
});
|
|
401
459
|
}, [createAgent, pushRow, resetTranscript]);
|
|
402
460
|
const switchModel = useCallback((modelId) => {
|
|
403
461
|
const updated = { ...loadSettings(), model: modelId };
|
|
404
462
|
setSettings(updated);
|
|
405
463
|
saveSettings(updated);
|
|
406
464
|
agentRef.current?.setModel(modelId);
|
|
407
|
-
pushRow({
|
|
465
|
+
pushRow({
|
|
466
|
+
kind: "info",
|
|
467
|
+
text: `Model switched to ${getModel(modelId).name}`,
|
|
468
|
+
});
|
|
408
469
|
}, [pushRow]);
|
|
409
470
|
// Fire SessionEnd hooks (best-effort, capped at 3s) before quitting.
|
|
410
471
|
const endAndExit = useCallback((reason) => {
|
|
411
472
|
const agent = agentRef.current;
|
|
412
473
|
if (!agent) {
|
|
474
|
+
// No agent yet, but the MCP manager may have started; tear it down.
|
|
475
|
+
void mcpManagerRef.current?.stop().catch(() => { });
|
|
413
476
|
exit();
|
|
414
477
|
return;
|
|
415
478
|
}
|
|
@@ -471,7 +534,10 @@ export function App({ initialView, initialPrompt, initialSession, updateCheck, }
|
|
|
471
534
|
switchModel(matches[0]);
|
|
472
535
|
}
|
|
473
536
|
else {
|
|
474
|
-
pushRow({
|
|
537
|
+
pushRow({
|
|
538
|
+
kind: "error",
|
|
539
|
+
text: `Unknown model "${arg}". Available: ${pickerIds.join(", ")}`,
|
|
540
|
+
});
|
|
475
541
|
}
|
|
476
542
|
}
|
|
477
543
|
else {
|
|
@@ -504,7 +570,10 @@ export function App({ initialView, initialPrompt, initialSession, updateCheck, }
|
|
|
504
570
|
case "/resume": {
|
|
505
571
|
const sessions = listSessions(process.cwd()).filter((s) => s.id !== agentRef.current?.taskId);
|
|
506
572
|
if (sessions.length === 0) {
|
|
507
|
-
pushRow({
|
|
573
|
+
pushRow({
|
|
574
|
+
kind: "info",
|
|
575
|
+
text: "No previous sessions found for this directory.",
|
|
576
|
+
});
|
|
508
577
|
break;
|
|
509
578
|
}
|
|
510
579
|
setResumableSessions(sessions);
|
|
@@ -564,6 +633,18 @@ export function App({ initialView, initialPrompt, initialSession, updateCheck, }
|
|
|
564
633
|
setBusyLabel("Thinking");
|
|
565
634
|
void getAgent().runTurn(INIT_PROMPT);
|
|
566
635
|
break;
|
|
636
|
+
case "/mcp": {
|
|
637
|
+
const manager = mcpManagerRef.current;
|
|
638
|
+
if (!manager) {
|
|
639
|
+
pushRow({
|
|
640
|
+
kind: "info",
|
|
641
|
+
text: "MCP not initialized yet — send a message first.",
|
|
642
|
+
});
|
|
643
|
+
break;
|
|
644
|
+
}
|
|
645
|
+
setMcpPickerOpen(true);
|
|
646
|
+
break;
|
|
647
|
+
}
|
|
567
648
|
case "/commit":
|
|
568
649
|
if (!getAuthToken(settings)) {
|
|
569
650
|
setView("login");
|
|
@@ -587,15 +668,10 @@ export function App({ initialView, initialPrompt, initialSession, updateCheck, }
|
|
|
587
668
|
case "/version":
|
|
588
669
|
pushRow({ kind: "info", text: `OrbCode CLI v${VERSION}` });
|
|
589
670
|
break;
|
|
590
|
-
case "/
|
|
591
|
-
pushRow({ kind: "info", text:
|
|
671
|
+
case "/usage": {
|
|
672
|
+
pushRow({ kind: "info", text: "Fetching plan usage…" });
|
|
592
673
|
const token = getAuthToken(settings);
|
|
593
674
|
if (token) {
|
|
594
|
-
fetchBalance(token, settings.organizationId).then((balance) => {
|
|
595
|
-
if (balance !== undefined) {
|
|
596
|
-
pushRow({ kind: "info", text: `Account balance: $${balance.toFixed(2)}` });
|
|
597
|
-
}
|
|
598
|
-
});
|
|
599
675
|
fetchProfile(token)
|
|
600
676
|
.then((profile) => {
|
|
601
677
|
const lines = usageLines(profile);
|
|
@@ -625,9 +701,24 @@ export function App({ initialView, initialPrompt, initialSession, updateCheck, }
|
|
|
625
701
|
endAndExit("prompt_input_exit");
|
|
626
702
|
break;
|
|
627
703
|
default:
|
|
628
|
-
pushRow({
|
|
704
|
+
pushRow({
|
|
705
|
+
kind: "error",
|
|
706
|
+
text: `Unknown command: ${name}. Try /help.`,
|
|
707
|
+
});
|
|
629
708
|
}
|
|
630
|
-
}, [
|
|
709
|
+
}, [
|
|
710
|
+
settings,
|
|
711
|
+
tasks,
|
|
712
|
+
contextTokens,
|
|
713
|
+
totalCost,
|
|
714
|
+
sessionTitle,
|
|
715
|
+
endAndExit,
|
|
716
|
+
pushRow,
|
|
717
|
+
getAgent,
|
|
718
|
+
switchModel,
|
|
719
|
+
resetTranscript,
|
|
720
|
+
clearQueue,
|
|
721
|
+
]);
|
|
631
722
|
const handleSubmit = useCallback((value) => {
|
|
632
723
|
if (value.startsWith("/")) {
|
|
633
724
|
handleCommand(value);
|
|
@@ -658,7 +749,10 @@ export function App({ initialView, initialPrompt, initialSession, updateCheck, }
|
|
|
658
749
|
const updated = loadSettings();
|
|
659
750
|
setSettings(updated);
|
|
660
751
|
agentRef.current?.setHooks(updated.hooks);
|
|
661
|
-
pushRow({
|
|
752
|
+
pushRow({
|
|
753
|
+
kind: "info",
|
|
754
|
+
text: "Project hooks trusted — enabled for this workspace.",
|
|
755
|
+
});
|
|
662
756
|
}
|
|
663
757
|
else {
|
|
664
758
|
pushRow({
|
|
@@ -671,6 +765,28 @@ export function App({ initialView, initialPrompt, initialSession, updateCheck, }
|
|
|
671
765
|
if (deferred)
|
|
672
766
|
handleSubmit(deferred);
|
|
673
767
|
}, [handleSubmit, pushRow]);
|
|
768
|
+
// Resolve the MCP server approval prompt: connect to the approved project
|
|
769
|
+
// servers, persist the decision, and proceed with any deferred startup prompt.
|
|
770
|
+
const resolveMcpApproval = useCallback(async (approved) => {
|
|
771
|
+
setPendingMcpApproval(null);
|
|
772
|
+
const manager = mcpManagerRef.current;
|
|
773
|
+
if (manager) {
|
|
774
|
+
// Enable each approved server (connects immediately). Disapproved
|
|
775
|
+
// ones stay disabled and won't prompt again (persisted below).
|
|
776
|
+
await Promise.all(approved.map((name) => manager.enableServer(name).catch(() => { })));
|
|
777
|
+
saveMcpApproval(process.cwd(), manager.getEnabled(), manager.getDisabled());
|
|
778
|
+
const snap = manager.snapshot();
|
|
779
|
+
const connected = snap.servers.filter((s) => s.status === "connected").length;
|
|
780
|
+
pushRow({
|
|
781
|
+
kind: "info",
|
|
782
|
+
text: `MCP: ${connected}/${snap.servers.length} server${snap.servers.length === 1 ? "" : "s"} connected${approved.length ? ` (approved: ${approved.join(", ")})` : ""}.`,
|
|
783
|
+
});
|
|
784
|
+
}
|
|
785
|
+
const deferred = deferredPromptRef.current;
|
|
786
|
+
deferredPromptRef.current = null;
|
|
787
|
+
if (deferred)
|
|
788
|
+
handleSubmit(deferred);
|
|
789
|
+
}, [handleSubmit, pushRow]);
|
|
674
790
|
// Apply --resume and an initial prompt (`orbcode "do something"`) on startup.
|
|
675
791
|
const bootedRef = useRef(false);
|
|
676
792
|
useEffect(() => {
|
|
@@ -689,6 +805,19 @@ export function App({ initialView, initialPrompt, initialSession, updateCheck, }
|
|
|
689
805
|
else if (initialPrompt) {
|
|
690
806
|
handleSubmit(initialPrompt);
|
|
691
807
|
}
|
|
808
|
+
// Start the MCP manager and surface any project-scope servers that need
|
|
809
|
+
// approval. The approval prompt is non-blocking: the agent can start
|
|
810
|
+
// working while the user decides, and approved servers connect live.
|
|
811
|
+
const current = loadSettings();
|
|
812
|
+
if (getAuthToken(current)) {
|
|
813
|
+
const manager = new McpManager(process.cwd(), current.disabledMcpServers ?? [], current.enabledMcpServers ?? []);
|
|
814
|
+
mcpManagerRef.current = manager;
|
|
815
|
+
void manager.start().then(() => {
|
|
816
|
+
const pendingMcp = manager.getPendingApproval();
|
|
817
|
+
if (pendingMcp.length > 0)
|
|
818
|
+
setPendingMcpApproval(pendingMcp);
|
|
819
|
+
});
|
|
820
|
+
}
|
|
692
821
|
refreshUsage();
|
|
693
822
|
}, [initialSession, initialPrompt, handleResume, handleSubmit, refreshUsage]);
|
|
694
823
|
// Resolve the npm version check after first paint so the TUI shows up
|
|
@@ -721,7 +850,11 @@ export function App({ initialView, initialPrompt, initialSession, updateCheck, }
|
|
|
721
850
|
const next = prev === "ask" ? "edits" : prev === "edits" ? "auto" : "ask";
|
|
722
851
|
const autoApproveEdits = next !== "ask";
|
|
723
852
|
const autoApproveSafeCommands = next === "auto";
|
|
724
|
-
const updated = {
|
|
853
|
+
const updated = {
|
|
854
|
+
...loadSettings(),
|
|
855
|
+
autoApproveEdits,
|
|
856
|
+
autoApproveSafeCommands,
|
|
857
|
+
};
|
|
725
858
|
setSettings(updated);
|
|
726
859
|
saveSettings(updated);
|
|
727
860
|
agentRef.current?.setApprovalMode(autoApproveEdits, autoApproveSafeCommands);
|
|
@@ -733,7 +866,7 @@ export function App({ initialView, initialPrompt, initialSession, updateCheck, }
|
|
|
733
866
|
expandReasoningRef.current = expanded;
|
|
734
867
|
// Re-render the whole transcript (including past thinking) with the
|
|
735
868
|
// new expansion state: clear the screen and remount <Static>.
|
|
736
|
-
setRows((prev) => prev.map((row) =>
|
|
869
|
+
setRows((prev) => prev.map((row) => row.kind === "reasoning" ? { ...row, expanded } : row));
|
|
737
870
|
if (process.stdout.isTTY) {
|
|
738
871
|
process.stdout.write("\x1b[2J\x1b[H");
|
|
739
872
|
}
|
|
@@ -746,31 +879,53 @@ export function App({ initialView, initialPrompt, initialSession, updateCheck, }
|
|
|
746
879
|
saveSettings(updated);
|
|
747
880
|
agentRef.current = null;
|
|
748
881
|
setView("chat");
|
|
749
|
-
setUsage({
|
|
882
|
+
setUsage({
|
|
883
|
+
plan: profile.plan,
|
|
884
|
+
usagePercentage: profile.usagePercentage,
|
|
885
|
+
tieredUsage: profile.tieredUsage,
|
|
886
|
+
});
|
|
750
887
|
const who = profile.user?.name || profile.user?.email;
|
|
751
|
-
pushRow({
|
|
888
|
+
pushRow({
|
|
889
|
+
kind: "info",
|
|
890
|
+
text: `Signed in${who ? ` as ${who}` : ""}. Ready when you are.`,
|
|
891
|
+
});
|
|
752
892
|
}, [pushRow]);
|
|
753
|
-
const taskLines = useMemo(() => tasks
|
|
893
|
+
const taskLines = useMemo(() => tasks
|
|
894
|
+
.split("\n")
|
|
895
|
+
.map((l) => l.trim())
|
|
896
|
+
.filter(Boolean), [tasks]);
|
|
754
897
|
const inputActive = view === "chat" &&
|
|
755
898
|
!pendingApproval &&
|
|
756
899
|
!pendingFollowup &&
|
|
757
900
|
!pendingHookTrust &&
|
|
901
|
+
!pendingMcpApproval &&
|
|
758
902
|
!modelPickerOpen &&
|
|
903
|
+
!mcpPickerOpen &&
|
|
759
904
|
!resumableSessions;
|
|
760
905
|
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Static, { items: rows, children: (row) => _jsx(RowView, { row: row }, row.id) }, staticKey), view === "login" ? (_jsx(LoginSection, { onLogin: handleLogin })) : (_jsxs(Box, { flexDirection: "column", children: [updateInfo?.updateAvailable && updateInfo.latest && (_jsxs(Box, { marginTop: 1, flexDirection: "column", borderStyle: "round", borderColor: COLORS.warning, paddingX: 2, alignSelf: "flex-start", children: [_jsxs(Text, { color: COLORS.warning, bold: true, children: ["\u2191 Update available: v", updateInfo.current, " \u2192 v", updateInfo.latest] }), _jsxs(Text, { children: ["Run ", _jsx(Text, { color: COLORS.accent, children: "orbcode update" }), " to install the latest version, then relaunch."] })] })), streamingReasoning && (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsx(Text, { color: COLORS.thinking, italic: true, children: "\u2726 Thinking\u2026" }), _jsx(Box, { paddingLeft: 2, children: _jsx(Text, { dimColor: true, italic: true, children: tail(streamingReasoning, expandReasoningRef.current ? 30 : 3) }) })] })), streamingText && (_jsx(Box, { marginTop: 1, children: _jsxs(Text, { children: [_jsx(Text, { color: COLORS.primary, children: "\u25CF " }), streamingText] }) })), runningTool && (_jsx(Box, { marginTop: 1, children: _jsxs(Text, { color: COLORS.warning, children: [formatToolName(runningTool.name), " ", _jsx(Text, { dimColor: true, children: runningTool.summary })] }) })), taskLines.length > 0 && (_jsxs(Box, { flexDirection: "column", marginTop: 1, paddingLeft: 1, children: [_jsx(Text, { dimColor: true, bold: true, children: "Tasks" }), taskLines.slice(0, 10).map((line, i) => (_jsx(Text, { dimColor: /^[-*]\s*\[x\]/i.test(line), children: line
|
|
761
906
|
.replace(/^[-*]\s*\[x\]/i, " ■")
|
|
762
907
|
.replace(/^[-*]\s*\[-\]/, " ◧")
|
|
763
|
-
.replace(/^[-*]\s*\[ \]/, " □") }, i))), taskLines.length > 10 && _jsxs(Text, { dimColor: true, children: [" \u2026 ", taskLines.length - 10, " more"] })] })), modelPickerOpen && (_jsx(Box, { marginTop: 1, children: _jsx(ModelPicker, { currentId: settings.model, onSelect: (modelId) => {
|
|
908
|
+
.replace(/^[-*]\s*\[ \]/, " □") }, i))), taskLines.length > 10 && (_jsxs(Text, { dimColor: true, children: [" \u2026 ", taskLines.length - 10, " more"] }))] })), modelPickerOpen && (_jsx(Box, { marginTop: 1, children: _jsx(ModelPicker, { currentId: settings.model, onSelect: (modelId) => {
|
|
764
909
|
setModelPickerOpen(false);
|
|
765
910
|
switchModel(modelId);
|
|
766
|
-
}, onCancel: () => setModelPickerOpen(false) }) })), resumableSessions && (_jsx(Box, { marginTop: 1, children: _jsx(SessionPicker, { sessions: resumableSessions, onSelect: handleResume, onCancel: () => setResumableSessions(null) }) })), pendingHookTrust && (_jsx(Box, { marginTop: 1, children: _jsx(HookTrustPrompt, { cwd: process.cwd(), commands: pendingHookTrust.commands, onDecision: resolveHookTrust }) })),
|
|
911
|
+
}, onCancel: () => setModelPickerOpen(false) }) })), resumableSessions && (_jsx(Box, { marginTop: 1, children: _jsx(SessionPicker, { sessions: resumableSessions, onSelect: handleResume, onCancel: () => setResumableSessions(null) }) })), pendingHookTrust && (_jsx(Box, { marginTop: 1, children: _jsx(HookTrustPrompt, { cwd: process.cwd(), commands: pendingHookTrust.commands, onDecision: resolveHookTrust }) })), pendingMcpApproval && (_jsx(Box, { marginTop: 1, children: _jsx(McpApprovalPrompt, { serverNames: pendingMcpApproval, onApprove: resolveMcpApproval }) })), mcpPickerOpen && mcpManagerRef.current && (_jsx(Box, { marginTop: 1, children: _jsx(McpPicker, { manager: mcpManagerRef.current, onChanged: () => {
|
|
912
|
+
const m = mcpManagerRef.current;
|
|
913
|
+
if (m)
|
|
914
|
+
saveMcpApproval(process.cwd(), m.getEnabled(), m.getDisabled());
|
|
915
|
+
}, onCancel: () => setMcpPickerOpen(false) }) })), pendingApproval && (_jsx(Box, { marginTop: 1, children: _jsx(ApprovalPrompt, { request: pendingApproval.request, onDecision: (decision) => {
|
|
767
916
|
pendingApproval.resolve(decision);
|
|
768
917
|
setPendingApproval(null);
|
|
769
918
|
} }) })), pendingFollowup && (_jsx(Box, { marginTop: 1, children: _jsx(FollowupPrompt, { question: pendingFollowup.question, suggestions: pendingFollowup.suggestions, onAnswer: (answer) => {
|
|
770
919
|
pushRow({ kind: "user", text: answer });
|
|
771
920
|
pendingFollowup.resolve(answer);
|
|
772
921
|
setPendingFollowup(null);
|
|
773
|
-
} }) })), busy &&
|
|
922
|
+
} }) })), busy &&
|
|
923
|
+
!pendingApproval &&
|
|
924
|
+
!pendingFollowup &&
|
|
925
|
+
!pendingHookTrust &&
|
|
926
|
+
!pendingMcpApproval &&
|
|
927
|
+
!mcpPickerOpen &&
|
|
928
|
+
!streamingText && (_jsx(Box, { marginTop: 1, children: _jsx(Spinner, { label: busyLabel }) })), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [queuedMessages.length > 0 && (_jsxs(Box, { flexDirection: "column", paddingLeft: 1, marginBottom: 1, children: [_jsxs(Text, { dimColor: true, bold: true, children: ["Queue (", queuedMessages.length, ")"] }), queuedMessages.slice(0, 5).map((msg, i) => (_jsxs(Text, { dimColor: true, children: [i + 1, ". ", truncateForQueue(msg).replace(/\n/g, "↵")] }, i))), queuedMessages.length > 5 && (_jsxs(Text, { dimColor: true, children: [" \u2026 ", queuedMessages.length - 5, " more"] }))] })), _jsx(InputBox, { active: inputActive, slashCommands: SLASH_COMMANDS, onSubmit: handleSubmit }), _jsx(StatusBar, { modelId: settings.model, contextTokens: contextTokens, totalCost: totalCost, state: busy ? busyLabel : "", approvalMode: approvalMode, busy: busy, title: sessionTitle, plan: usage?.plan, usagePercentage: usage?.usagePercentage, tieredUsage: usage?.tieredUsage })] })] }))] }));
|
|
774
929
|
}
|
|
775
930
|
function tail(text, lines) {
|
|
776
931
|
const all = text.split("\n").filter((l) => l.trim());
|
|
@@ -782,6 +937,6 @@ function truncateForQueue(text) {
|
|
|
782
937
|
return text;
|
|
783
938
|
return text.slice(0, QUEUE_PREVIEW_LIMIT - 1) + "…";
|
|
784
939
|
}
|
|
785
|
-
function LoginSection({ onLogin }) {
|
|
940
|
+
function LoginSection({ onLogin, }) {
|
|
786
941
|
return _jsx(LoginView, { onLogin: onLogin });
|
|
787
942
|
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { useState } from "react";
|
|
3
|
+
import { Box, Text, useInput } from "ink";
|
|
4
|
+
import { COLORS } from "../../branding.js";
|
|
5
|
+
const VISIBLE_ROWS = 8;
|
|
6
|
+
/**
|
|
7
|
+
* Shown at startup when the project's `.mcp.json` defines servers that haven't
|
|
8
|
+
* been approved yet. Project-scope servers ship in the repo and can spawn
|
|
9
|
+
* processes / open network connections, so they require explicit per-project
|
|
10
|
+
* approval — mirroring Claude Code's `.mcp.json` approval dialog.
|
|
11
|
+
*
|
|
12
|
+
* The user toggles servers with Space and confirms with Enter; Escape rejects
|
|
13
|
+
* all. Approved servers are persisted to `.orbcode/settings.json` so they
|
|
14
|
+
* auto-connect on future sessions in this project.
|
|
15
|
+
*/
|
|
16
|
+
export function McpApprovalPrompt({ serverNames, onApprove }) {
|
|
17
|
+
const [selected, setSelected] = useState(0);
|
|
18
|
+
const [checked, setChecked] = useState(new Set(serverNames));
|
|
19
|
+
useInput((input, key) => {
|
|
20
|
+
if (key.upArrow) {
|
|
21
|
+
setSelected((s) => (s - 1 + serverNames.length) % serverNames.length);
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
if (key.downArrow || key.tab) {
|
|
25
|
+
setSelected((s) => (s + 1) % serverNames.length);
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
if (key.escape) {
|
|
29
|
+
onApprove([]);
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
if (key.return) {
|
|
33
|
+
onApprove([...checked]);
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
if (input === " ") {
|
|
37
|
+
const name = serverNames[selected];
|
|
38
|
+
if (!name)
|
|
39
|
+
return;
|
|
40
|
+
setChecked((prev) => {
|
|
41
|
+
const next = new Set(prev);
|
|
42
|
+
if (next.has(name))
|
|
43
|
+
next.delete(name);
|
|
44
|
+
else
|
|
45
|
+
next.add(name);
|
|
46
|
+
return next;
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
if (serverNames.length === 0)
|
|
51
|
+
return null;
|
|
52
|
+
const count = serverNames.length;
|
|
53
|
+
const windowStart = Math.max(0, Math.min(selected - VISIBLE_ROWS + 1, count - VISIBLE_ROWS));
|
|
54
|
+
const visible = serverNames.slice(windowStart, windowStart + VISIBLE_ROWS);
|
|
55
|
+
return (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: COLORS.warning, paddingX: 1, children: [_jsxs(Text, { bold: true, color: COLORS.warning, children: [count, " MCP server", count === 1 ? "" : "s", " found in .mcp.json"] }), _jsx(Text, { dimColor: true, children: "Select any you wish to enable for this project." }), windowStart > 0 && _jsxs(Text, { dimColor: true, children: [" \u2191 ", windowStart, " more"] }), visible.map((name, i) => {
|
|
56
|
+
const index = windowStart + i;
|
|
57
|
+
const isSelected = index === selected;
|
|
58
|
+
const isChecked = checked.has(name);
|
|
59
|
+
return (_jsxs(Text, { color: isSelected ? COLORS.accent : undefined, children: [isSelected ? "❯ " : " ", isChecked ? "☑" : "☐", " ", name] }, name));
|
|
60
|
+
}), windowStart + VISIBLE_ROWS < count && (_jsxs(Text, { dimColor: true, children: [" \u2193 ", count - windowStart - VISIBLE_ROWS, " more"] })), _jsx(Text, { dimColor: true, children: "space toggle \u00B7 enter confirm \u00B7 esc reject all" })] }));
|
|
61
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { useState } from "react";
|
|
3
|
+
import { Box, Text, useInput } from "ink";
|
|
4
|
+
import { COLORS } from "../../branding.js";
|
|
5
|
+
import { copyToClipboard } from "../../utils/clipboard.js";
|
|
6
|
+
/**
|
|
7
|
+
* Shown when the user triggers OAuth authentication for an MCP server. Mirrors
|
|
8
|
+
* Claude Code's auth screen:
|
|
9
|
+
*
|
|
10
|
+
* - "Authenticating with <server>…"
|
|
11
|
+
* - "A browser window will open for authentication"
|
|
12
|
+
* - The authorization URL (with `c` to copy it)
|
|
13
|
+
* - A paste fallback for when the browser redirect fails
|
|
14
|
+
* - "Return here after authenticating. Press Esc to go back."
|
|
15
|
+
*
|
|
16
|
+
* The auth code can arrive two ways: the loopback callback server (automatic,
|
|
17
|
+
* handled by the manager) or a manual paste here. Both resolve the same
|
|
18
|
+
* promise, so whichever fires first wins.
|
|
19
|
+
*/
|
|
20
|
+
export function McpAuthScreen({ serverName, authUrl, onPasteCode, onCancel }) {
|
|
21
|
+
const [pasted, setPasted] = useState("");
|
|
22
|
+
const [copied, setCopied] = useState(false);
|
|
23
|
+
const [mode, setMode] = useState("waiting");
|
|
24
|
+
useInput((input, key) => {
|
|
25
|
+
if (key.escape) {
|
|
26
|
+
onCancel();
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
if (mode === "paste") {
|
|
30
|
+
if (key.return) {
|
|
31
|
+
if (pasted.trim())
|
|
32
|
+
onPasteCode(pasted.trim());
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
if (key.backspace || key.delete) {
|
|
36
|
+
setPasted((p) => p.slice(0, -1));
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
if (input && !key.ctrl && !key.meta) {
|
|
40
|
+
setPasted((p) => p + input);
|
|
41
|
+
}
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
// waiting mode
|
|
45
|
+
if (input.toLowerCase() === "c" && authUrl) {
|
|
46
|
+
const ok = copyToClipboard(authUrl);
|
|
47
|
+
setCopied(ok);
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
if (key.return) {
|
|
51
|
+
setMode("paste");
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
return (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: COLORS.warning, paddingX: 1, children: [_jsxs(Text, { bold: true, color: COLORS.warning, children: ["Authenticating with ", serverName, "\u2026"] }), _jsx(Text, { children: " " }), _jsxs(Text, { children: [_jsx(Text, { color: COLORS.thinking, children: "\u273D " }), _jsx(Text, { children: "A browser window will open for authentication" })] }), _jsx(Text, { children: " " }), authUrl ? (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { dimColor: true, children: "If your browser doesn't open automatically, copy this URL manually (c to copy)" }), _jsx(Box, { paddingLeft: 1, marginTop: 0, children: _jsx(Text, { wrap: "wrap", color: COLORS.accent, children: authUrl }) }), copied && _jsx(Text, { color: COLORS.success, children: "\u2713 Copied to clipboard" })] })) : (_jsx(Text, { dimColor: true, children: "Waiting for the authorization URL\u2026" })), _jsx(Text, { children: " " }), mode === "paste" ? (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { dimColor: true, children: "If the redirect page shows a connection error, paste the URL from your browser's address bar:" }), _jsxs(Text, { children: [_jsx(Text, { dimColor: true, children: "URL> " }), pasted, _jsx(Text, { inverse: true, children: " " })] }), _jsx(Text, { dimColor: true, children: "enter to submit \u00B7 esc to go back" })] })) : (_jsx(Text, { dimColor: true, children: "Press enter to paste a redirect URL manually \u00B7 esc to go back" })), _jsx(Text, { children: " " }), _jsx(Text, { dimColor: true, children: "Return here after authenticating in your browser. Press Esc to go back." })] }));
|
|
55
|
+
}
|