@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/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, fetchBalance, fetchProfile, fetchTaskTitle } from "../auth/auth.js";
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
- { name: "/clear", description: "clear the screen — the conversation continues" },
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
- { name: "/compact", description: "summarize the conversation to free up context" },
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
- { name: "/status", description: "show session status (model, context, cost, account)" },
30
- { name: "/cost", description: "show session cost and balance" },
31
- { name: "/init", description: "analyze this codebase and create an AGENTS.md" },
32
- { name: "/commit", description: "check pending changes and create detailed commits" },
33
- { name: "/code-review", description: "expert review of pending changes: performance, security, bugs, tests" },
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 /cost usage block (extension profile data). */
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)} ${remaining.toFixed(1)}/${w.limit.toFixed(1)} left (${pct}% used) · Resets ${reset}`);
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
- { kind: "header", id: "header", cwd: process.cwd(), modelName: getModel(loadSettings().model).name },
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({ plan: profile.plan, usagePercentage: profile.usagePercentage, tieredUsage: profile.tieredUsage }))
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
- { kind: "header", id: rowId(), cwd: process.cwd(), modelName: getModel(loadSettings().model).name },
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({ id: event.id, name: event.name, summary: event.summary });
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({ kind: event.isError ? "error" : "info", text: event.message });
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" && typeof message.content === "string" && message.content.trim()) {
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({ kind: "info", text: `Resumed session: ${session.title || session.id}` });
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({ kind: "info", text: `Model switched to ${getModel(modelId).name}` });
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({ kind: "error", text: `Unknown model "${arg}". Available: ${pickerIds.join(", ")}` });
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({ kind: "info", text: "No previous sessions found for this directory." });
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 "/cost": {
591
- pushRow({ kind: "info", text: `Session cost: $${totalCost.toFixed(4)} (fetching balance)` });
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({ kind: "error", text: `Unknown command: ${name}. Try /help.` });
704
+ pushRow({
705
+ kind: "error",
706
+ text: `Unknown command: ${name}. Try /help.`,
707
+ });
629
708
  }
630
- }, [settings, tasks, contextTokens, totalCost, sessionTitle, endAndExit, pushRow, getAgent, switchModel, resetTranscript, clearQueue]);
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({ kind: "info", text: "Project hooks trusted — enabled for this workspace." });
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 = { ...loadSettings(), autoApproveEdits, autoApproveSafeCommands };
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) => (row.kind === "reasoning" ? { ...row, expanded } : 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({ plan: profile.plan, usagePercentage: profile.usagePercentage, tieredUsage: profile.tieredUsage });
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({ kind: "info", text: `Signed in${who ? ` as ${who}` : ""}. Ready when you are.` });
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.split("\n").map((l) => l.trim()).filter(Boolean), [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 }) })), pendingApproval && (_jsx(Box, { marginTop: 1, children: _jsx(ApprovalPrompt, { request: pendingApproval.request, onDecision: (decision) => {
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 && !pendingApproval && !pendingFollowup && !pendingHookTrust && !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 })] })] }))] }));
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
+ }