@matterailab/orbcode 0.2.2 → 0.2.4

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,36 +1,61 @@
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";
19
22
  import { listSessions } from "../core/sessions.js";
20
- import { RowView, formatToolName } from "./components/rows.js";
23
+ import { RowView } 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) {
@@ -143,19 +161,23 @@ let rowCounter = 0;
143
161
  function rowId() {
144
162
  return `row-${rowCounter++}`;
145
163
  }
146
- export function App({ initialView, initialPrompt, initialSession, updateCheck, }) {
164
+ export function App({ initialView, initialPrompt, initialSession, systemPromptOverride, updateCheck, }) {
147
165
  const { exit } = useApp();
148
166
  const [settings, setSettings] = useState(() => loadSettings());
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");
156
179
  const [streamingReasoning, setStreamingReasoning] = useState("");
157
180
  const [streamingText, setStreamingText] = useState("");
158
- const [runningTool, setRunningTool] = useState(null);
159
181
  const [pendingApproval, setPendingApproval] = useState(null);
160
182
  const [pendingFollowup, setPendingFollowup] = useState(null);
161
183
  // Set when the current project defines hooks that haven't been trusted yet;
@@ -167,6 +189,12 @@ export function App({ initialView, initialPrompt, initialSession, updateCheck, }
167
189
  const [queuedMessages, setQueuedMessages] = useState([]);
168
190
  const [modelPickerOpen, setModelPickerOpen] = useState(false);
169
191
  const [resumableSessions, setResumableSessions] = useState(null);
192
+ // MCP manager (created once, shared across agents in this process). Null until
193
+ // the first agent is created so we don't spawn servers before login.
194
+ const mcpManagerRef = useRef(null);
195
+ const [mcpPickerOpen, setMcpPickerOpen] = useState(false);
196
+ // Project-scope MCP servers awaiting the user's approval at startup.
197
+ const [pendingMcpApproval, setPendingMcpApproval] = useState(null);
170
198
  const [staticKey, setStaticKey] = useState(0);
171
199
  const [tasks, setTasks] = useState("");
172
200
  const [contextTokens, setContextTokens] = useState(0);
@@ -184,7 +212,11 @@ export function App({ initialView, initialPrompt, initialSession, updateCheck, }
184
212
  if (!token)
185
213
  return;
186
214
  fetchProfile(token)
187
- .then((profile) => setUsage({ plan: profile.plan, usagePercentage: profile.usagePercentage, tieredUsage: profile.tieredUsage }))
215
+ .then((profile) => setUsage({
216
+ plan: profile.plan,
217
+ usagePercentage: profile.usagePercentage,
218
+ tieredUsage: profile.tieredUsage,
219
+ }))
188
220
  .catch(() => { });
189
221
  }, []);
190
222
  const agentRef = useRef(null);
@@ -201,6 +233,9 @@ export function App({ initialView, initialPrompt, initialSession, updateCheck, }
201
233
  const deferredPromptRef = useRef(null);
202
234
  // Guards endAndExit against double-invocation (Ctrl+D spam).
203
235
  const exitingRef = useRef(false);
236
+ // Mirror of the `-s` override so a fresh agent (created mid-session via
237
+ // /new or /resume) keeps the override instead of falling back to default.
238
+ const systemPromptOverrideRef = useRef(systemPromptOverride);
204
239
  const enqueueMessage = useCallback((text) => {
205
240
  queueRef.current = [...queueRef.current, text];
206
241
  setQueuedMessages(queueRef.current);
@@ -243,7 +278,12 @@ export function App({ initialView, initialPrompt, initialSession, updateCheck, }
243
278
  process.stdout.write("\x1b[2J\x1b[H");
244
279
  }
245
280
  setRows([
246
- { kind: "header", id: rowId(), cwd: process.cwd(), modelName: getModel(loadSettings().model).name },
281
+ {
282
+ kind: "header",
283
+ id: rowId(),
284
+ cwd: process.cwd(),
285
+ modelName: getModel(loadSettings().model).name,
286
+ },
247
287
  ]);
248
288
  setStaticKey((k) => k + 1);
249
289
  }, []);
@@ -263,6 +303,7 @@ export function App({ initialView, initialPrompt, initialSession, updateCheck, }
263
303
  });
264
304
  reasoningBufferRef.current = "";
265
305
  setStreamingReasoning("");
306
+ setBusyLabel("Working");
266
307
  break;
267
308
  case "text-delta":
268
309
  textBufferRef.current += event.text;
@@ -273,14 +314,13 @@ export function App({ initialView, initialPrompt, initialSession, updateCheck, }
273
314
  pushRow({ kind: "assistant", text: textBufferRef.current });
274
315
  textBufferRef.current = "";
275
316
  setStreamingText("");
317
+ setBusyLabel("Working");
276
318
  break;
277
319
  case "tool-start":
278
- setRunningTool({ id: event.id, name: event.name, summary: event.summary });
279
- setBusyLabel(`Running ${event.name}`);
320
+ setBusyLabel("Working");
280
321
  break;
281
322
  case "tool-end":
282
- setRunningTool(null);
283
- setBusyLabel("Thinking");
323
+ setBusyLabel("Working");
284
324
  pushRow({
285
325
  kind: "tool",
286
326
  name: event.name,
@@ -305,7 +345,10 @@ export function App({ initialView, initialPrompt, initialSession, updateCheck, }
305
345
  pushRow({ kind: "completion", text: event.result });
306
346
  break;
307
347
  case "system":
308
- pushRow({ kind: event.isError ? "error" : "info", text: event.message });
348
+ pushRow({
349
+ kind: event.isError ? "error" : "info",
350
+ text: event.message,
351
+ });
309
352
  break;
310
353
  case "error":
311
354
  pushRow({ kind: "error", text: event.message });
@@ -321,7 +364,6 @@ export function App({ initialView, initialPrompt, initialSession, updateCheck, }
321
364
  reasoningBufferRef.current = "";
322
365
  setStreamingReasoning("");
323
366
  }
324
- setRunningTool(null);
325
367
  setBusy(false);
326
368
  maybeFetchTitle();
327
369
  refreshUsage();
@@ -346,6 +388,13 @@ export function App({ initialView, initialPrompt, initialSession, updateCheck, }
346
388
  }, [pushRow, maybeFetchTitle, refreshUsage, drainQueue]);
347
389
  const createAgent = useCallback((resume) => {
348
390
  const current = loadSettings();
391
+ // Create the MCP manager once per process and reuse it across agents
392
+ // (so /new and /resume keep the same server connections). It reads
393
+ // the enabled/disabled lists from settings and connects to approved
394
+ // servers lazily on start().
395
+ if (!mcpManagerRef.current) {
396
+ mcpManagerRef.current = new McpManager(process.cwd(), current.disabledMcpServers ?? [], current.enabledMcpServers ?? []);
397
+ }
349
398
  return new Agent({
350
399
  cwd: process.cwd(),
351
400
  token: getAuthToken(current),
@@ -355,7 +404,11 @@ export function App({ initialView, initialPrompt, initialSession, updateCheck, }
355
404
  autoApproveEdits: current.autoApproveEdits,
356
405
  autoApproveSafeCommands: current.autoApproveSafeCommands,
357
406
  hooks: current.hooks,
407
+ mcp: mcpManagerRef.current,
358
408
  resume,
409
+ // The override (from `-s`) is captured in a ref so a /new or /resume
410
+ // mid-session still applies it to the new agent.
411
+ systemPromptOverride: systemPromptOverrideRef.current,
359
412
  callbacks: {
360
413
  onEvent: handleEvent,
361
414
  requestApproval: (request) => new Promise((resolve) => setPendingApproval({ request, resolve })),
@@ -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,54 @@ 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
- 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
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] }) })), 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 &&
929
+ !streamingReasoning && (_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
930
  }
775
931
  function tail(text, lines) {
776
932
  const all = text.split("\n").filter((l) => l.trim());
@@ -782,6 +938,6 @@ function truncateForQueue(text) {
782
938
  return text;
783
939
  return text.slice(0, QUEUE_PREVIEW_LIMIT - 1) + "…";
784
940
  }
785
- function LoginSection({ onLogin }) {
941
+ function LoginSection({ onLogin, }) {
786
942
  return _jsx(LoginView, { onLogin: onLogin });
787
943
  }
@@ -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
+ }