@matterailab/orbcode 0.1.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.
Files changed (63) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +471 -0
  3. package/bin/orbcode.js +2 -0
  4. package/dist/api/client.js +141 -0
  5. package/dist/api/headers.js +14 -0
  6. package/dist/api/models.js +49 -0
  7. package/dist/api/stream.js +1 -0
  8. package/dist/auth/auth.js +172 -0
  9. package/dist/branding.js +31 -0
  10. package/dist/config/promptHistory.js +33 -0
  11. package/dist/config/settings.js +112 -0
  12. package/dist/core/agent.js +459 -0
  13. package/dist/core/events.js +1 -0
  14. package/dist/core/sessions.js +44 -0
  15. package/dist/headless.js +64 -0
  16. package/dist/index.js +84 -0
  17. package/dist/prompts/system.js +379 -0
  18. package/dist/tools/executors/executeCommand.js +58 -0
  19. package/dist/tools/executors/files.js +197 -0
  20. package/dist/tools/executors/listFiles.js +65 -0
  21. package/dist/tools/executors/searchFiles.js +104 -0
  22. package/dist/tools/executors/web.js +72 -0
  23. package/dist/tools/index.js +85 -0
  24. package/dist/tools/schemas/ask_followup_question.js +40 -0
  25. package/dist/tools/schemas/attempt_completion.js +19 -0
  26. package/dist/tools/schemas/browser_action.js +60 -0
  27. package/dist/tools/schemas/check_past_chat_memories.js +23 -0
  28. package/dist/tools/schemas/codebase_search.js +23 -0
  29. package/dist/tools/schemas/execute_command.js +31 -0
  30. package/dist/tools/schemas/fetch_instructions.js +20 -0
  31. package/dist/tools/schemas/file_edit.js +31 -0
  32. package/dist/tools/schemas/file_write.js +27 -0
  33. package/dist/tools/schemas/generate_image.js +27 -0
  34. package/dist/tools/schemas/index.js +29 -0
  35. package/dist/tools/schemas/list_code_definition_names.js +19 -0
  36. package/dist/tools/schemas/list_files.js +23 -0
  37. package/dist/tools/schemas/lsp.js +46 -0
  38. package/dist/tools/schemas/multi_file_edit.js +42 -0
  39. package/dist/tools/schemas/new_task.js +27 -0
  40. package/dist/tools/schemas/read_file.js +27 -0
  41. package/dist/tools/schemas/run_slash_command.js +23 -0
  42. package/dist/tools/schemas/search_files.js +27 -0
  43. package/dist/tools/schemas/switch_mode.js +23 -0
  44. package/dist/tools/schemas/update_todo_list.js +19 -0
  45. package/dist/tools/schemas/use_skill.js +19 -0
  46. package/dist/tools/schemas/web_fetch.js +19 -0
  47. package/dist/tools/schemas/web_search.js +19 -0
  48. package/dist/tools/types.js +7 -0
  49. package/dist/ui/App.js +569 -0
  50. package/dist/ui/LoginView.js +82 -0
  51. package/dist/ui/components/ApprovalPrompt.js +21 -0
  52. package/dist/ui/components/FollowupPrompt.js +45 -0
  53. package/dist/ui/components/Header.js +12 -0
  54. package/dist/ui/components/InputBox.js +220 -0
  55. package/dist/ui/components/ModelPicker.js +51 -0
  56. package/dist/ui/components/SessionPicker.js +52 -0
  57. package/dist/ui/components/Spinner.js +19 -0
  58. package/dist/ui/components/StatusBar.js +18 -0
  59. package/dist/ui/components/rows.js +106 -0
  60. package/dist/ui/markdown.js +64 -0
  61. package/dist/utils/diff.js +144 -0
  62. package/dist/utils/shell.js +19 -0
  63. package/package.json +62 -0
package/dist/ui/App.js ADDED
@@ -0,0 +1,569 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
3
+ import { Box, Static, Text, useApp, useInput } from "ink";
4
+ import open from "open";
5
+ import { COLORS, VERSION } from "../branding.js";
6
+ import { AXON_MODELS, getModel, isValidAxonModel } from "../api/models.js";
7
+ import { LoginView } from "./LoginView.js";
8
+ import { APP_URL, fetchBalance, fetchProfile, fetchTaskTitle } from "../auth/auth.js";
9
+ import { getAuthToken, loadSettings, saveSettings } from "../config/settings.js";
10
+ import { Agent } from "../core/agent.js";
11
+ import { Spinner } from "./components/Spinner.js";
12
+ import { InputBox } from "./components/InputBox.js";
13
+ import { ApprovalPrompt } from "./components/ApprovalPrompt.js";
14
+ import { FollowupPrompt } from "./components/FollowupPrompt.js";
15
+ import { StatusBar } from "./components/StatusBar.js";
16
+ import { ModelPicker } from "./components/ModelPicker.js";
17
+ import { SessionPicker } from "./components/SessionPicker.js";
18
+ import { listSessions } from "../core/sessions.js";
19
+ import { RowView, formatToolName } from "./components/rows.js";
20
+ const SLASH_COMMANDS = [
21
+ { name: "/help", description: "show available commands" },
22
+ { name: "/model", description: "select the Axon model to use" },
23
+ { name: "/clear", description: "clear the screen — the conversation continues" },
24
+ { name: "/new", description: "start a new conversation with a clean slate" },
25
+ { name: "/resume", description: "resume a previous session" },
26
+ { name: "/compact", description: "summarize the conversation to free up context" },
27
+ { name: "/tasks", description: "show the current task list" },
28
+ { name: "/status", description: "show session status (model, context, cost, account)" },
29
+ { name: "/cost", description: "show session cost and balance" },
30
+ { name: "/init", description: "analyze this codebase and create an AGENTS.md" },
31
+ { name: "/commit", description: "check pending changes and create detailed commits" },
32
+ { name: "/code-review", description: "expert review of pending changes: performance, security, bugs, tests" },
33
+ { name: "/analytics", description: "open your MatterAI analytics dashboard" },
34
+ { name: "/login", description: "sign in to MatterAI" },
35
+ { name: "/logout", description: "sign out and remove the saved token" },
36
+ { name: "/version", description: "show the OrbCode CLI version" },
37
+ { name: "/exit", description: "quit OrbCode CLI" },
38
+ ];
39
+ function setTerminalTitle(title) {
40
+ if (process.stdout.isTTY) {
41
+ process.stdout.write(`\x1b]0;${title}\x07`);
42
+ }
43
+ }
44
+ /** Human lines for the /status and /cost usage block (extension profile data). */
45
+ function usageLines(profile) {
46
+ const lines = [];
47
+ if (profile.plan)
48
+ lines.push(`Plan ${profile.plan}`);
49
+ if (typeof profile.usagePercentage === "number") {
50
+ lines.push(`Usage ${profile.usagePercentage}% used · ${Math.max(0, 100 - profile.usagePercentage)}% remaining`);
51
+ }
52
+ if (typeof profile.remainingReviews === "number") {
53
+ lines.push(`Reviews ${profile.remainingReviews} remaining`);
54
+ }
55
+ if (profile.creditsResetDate)
56
+ lines.push(`Resets ${profile.creditsResetDate}`);
57
+ return lines;
58
+ }
59
+ const INIT_PROMPT = `Analyze this codebase and create an AGENTS.md file containing:
60
+ 1. A short overview of what the project does
61
+ 2. Build, run, lint and test commands
62
+ 3. Architecture and code structure (key directories and what lives in them)
63
+ 4. Code style conventions used in this repo (imports, formatting, naming, error handling)
64
+
65
+ If an AGENTS.md already exists, improve it. Keep it under ~60 lines so it is cheap to include in future prompts.`;
66
+ // Ported from the Orbital extension's commit slash command (commitCommandResponse).
67
+ const buildCommitPrompt = (userInput) => `The user has explicitly asked you to check pending changes and generate detailed commit messages. You MUST now help them with this.
68
+
69
+ Please check all the pending changes in the git repository and generate detailed commit messages. If needed, you can split into multiple commits also.
70
+
71
+ Instructions:
72
+ 1. First, check all pending changes using git status and git diff
73
+ 2. Group related changes together logically (e.g., a feature implementation, a bug fix, refactoring, etc.)
74
+ 3. For each logical group, generate a detailed, conventional commit message following the format:
75
+ type(scope): short description
76
+
77
+ Detailed explanation of what changed and why
78
+ 4. Create separate commits for each logical group using git add and git commit
79
+ 5. For GitHub repositories only, attribute the commit author as: matterai-app[bot]
80
+
81
+ To detect if the repository is hosted on GitHub, check the remote URL using:
82
+ git remote get-url origin
83
+
84
+ If the remote URL contains "github.com", use the author flag:
85
+ git commit --author="matterai-app[bot] <matterai-app[bot]@users.noreply.github.com>"
86
+
87
+ Before committing, present the commit messages to the user for review and ask them to confirm before executing.${userInput ? `\n\nThe user provided the following input with the commit command:\n${userInput}` : ""}`;
88
+ const buildCodeReviewPrompt = (userInput) => `The user has explicitly asked you to perform a thorough code review of the pending changes. You MUST now help them with this.
89
+
90
+ Review the code as a panel of four experts. Adopt each expert persona fully, one at a time, and review the complete change set from that specialty before moving on to the next:
91
+
92
+ 1. Performance Expert — algorithmic complexity, redundant computation or I/O, N+1 queries, unnecessary allocations, blocking calls on hot paths, missed caching or batching opportunities, and memory leaks.
93
+ 2. Security Expert — injection (SQL/command/path), unsafe deserialization, missing input validation or sanitization, secrets or credentials in code, authentication/authorization gaps, unsafe defaults, and risky dependency usage.
94
+ 3. Bug Hunter — logic errors, off-by-one mistakes, null/undefined handling, unhandled errors and rejected promises, race conditions, incorrect edge-case behavior, type coercion pitfalls, and broken assumptions between callers and callees.
95
+ 4. Test Expert — missing or inadequate test coverage for the changed behavior, untested edge cases and error paths, assertions that don't verify the actual behavior, and brittle or flaky test patterns; propose specific test cases worth adding.
96
+
97
+ Instructions:
98
+ 1. First, gather the changes to review: use git status and git diff (including staged changes). If the working tree is clean, review the most recent commit instead.
99
+ 2. Read the surrounding code of the changed files whenever you need more context for a finding — do not judge a diff hunk in isolation.
100
+ 3. Report findings grouped per expert. For each finding include: severity (critical / major / minor), the file and line, what is wrong, why it matters, and a concrete suggested fix.
101
+ 4. Only report real findings. If an expert finds nothing significant, state that explicitly — do not invent issues to fill space.
102
+ 5. Finish with a short summary: all findings ordered by severity, and an overall verdict on whether the changes are safe to merge.
103
+
104
+ This is a review only — do NOT modify any files. Present the findings to the user.${userInput ? `\n\nThe user provided the following input with the code-review command:\n${userInput}` : ""}`;
105
+ let rowCounter = 0;
106
+ function rowId() {
107
+ return `row-${rowCounter++}`;
108
+ }
109
+ export function App({ initialView, initialPrompt, initialSession, }) {
110
+ const { exit } = useApp();
111
+ const [settings, setSettings] = useState(() => loadSettings());
112
+ const [view, setView] = useState(initialView ?? (getAuthToken(settings) ? "chat" : "login"));
113
+ const [rows, setRows] = useState(() => [
114
+ { kind: "header", id: "header", cwd: process.cwd(), modelName: getModel(loadSettings().model).name },
115
+ ]);
116
+ const [busy, setBusy] = useState(false);
117
+ const [busyLabel, setBusyLabel] = useState("Thinking");
118
+ const [streamingReasoning, setStreamingReasoning] = useState("");
119
+ const [streamingText, setStreamingText] = useState("");
120
+ const [runningTool, setRunningTool] = useState(null);
121
+ const [pendingApproval, setPendingApproval] = useState(null);
122
+ const [pendingFollowup, setPendingFollowup] = useState(null);
123
+ const [modelPickerOpen, setModelPickerOpen] = useState(false);
124
+ const [resumableSessions, setResumableSessions] = useState(null);
125
+ const [staticKey, setStaticKey] = useState(0);
126
+ const [tasks, setTasks] = useState("");
127
+ const [contextTokens, setContextTokens] = useState(0);
128
+ const [totalCost, setTotalCost] = useState(0);
129
+ const [approvalMode, setApprovalMode] = useState(() => settings.autoApproveEdits && settings.autoApproveSafeCommands
130
+ ? "auto"
131
+ : settings.autoApproveEdits
132
+ ? "edits"
133
+ : "ask");
134
+ const [sessionTitle, setSessionTitle] = useState("");
135
+ const [usage, setUsage] = useState(null);
136
+ // Refresh plan/usage from /axoncode/profile (shown below the chat box).
137
+ const refreshUsage = useCallback(() => {
138
+ const token = getAuthToken(loadSettings());
139
+ if (!token)
140
+ return;
141
+ fetchProfile(token)
142
+ .then((profile) => setUsage({ plan: profile.plan, usagePercentage: profile.usagePercentage }))
143
+ .catch(() => { });
144
+ }, []);
145
+ const agentRef = useRef(null);
146
+ const expandReasoningRef = useRef(false);
147
+ const reasoningBufferRef = useRef("");
148
+ const textBufferRef = useRef("");
149
+ // taskId for which a title fetch has already been started (once per task).
150
+ const titleTaskRef = useRef(null);
151
+ const maybeFetchTitle = useCallback(() => {
152
+ const agent = agentRef.current;
153
+ if (!agent || titleTaskRef.current === agent.taskId)
154
+ return;
155
+ titleTaskRef.current = agent.taskId;
156
+ const token = getAuthToken(loadSettings());
157
+ if (!token)
158
+ return;
159
+ void fetchTaskTitle(agent.taskId, token).then((title) => {
160
+ if (title && agentRef.current?.taskId === agent.taskId) {
161
+ setSessionTitle(title);
162
+ setTerminalTitle(`${title} (orbcode)`);
163
+ agent.setTitle(title);
164
+ }
165
+ });
166
+ }, []);
167
+ const pushRow = useCallback((row) => {
168
+ setRows((prev) => [...prev, { ...row, id: rowId() }]);
169
+ }, []);
170
+ // Wipe the visible transcript and remount <Static> with just the header —
171
+ // a clean slate for /clear, /new and /resume.
172
+ const resetTranscript = useCallback(() => {
173
+ if (process.stdout.isTTY) {
174
+ process.stdout.write("\x1b[2J\x1b[H");
175
+ }
176
+ setRows([
177
+ { kind: "header", id: rowId(), cwd: process.cwd(), modelName: getModel(loadSettings().model).name },
178
+ ]);
179
+ setStaticKey((k) => k + 1);
180
+ }, []);
181
+ const handleEvent = useCallback((event) => {
182
+ switch (event.type) {
183
+ case "reasoning-delta":
184
+ reasoningBufferRef.current += event.text;
185
+ setStreamingReasoning(reasoningBufferRef.current);
186
+ setBusyLabel("Thinking");
187
+ break;
188
+ case "reasoning-done":
189
+ pushRow({
190
+ kind: "reasoning",
191
+ text: reasoningBufferRef.current,
192
+ durationMs: event.durationMs,
193
+ expanded: expandReasoningRef.current,
194
+ });
195
+ reasoningBufferRef.current = "";
196
+ setStreamingReasoning("");
197
+ break;
198
+ case "text-delta":
199
+ textBufferRef.current += event.text;
200
+ setStreamingText(textBufferRef.current);
201
+ setBusyLabel("Responding");
202
+ break;
203
+ case "text-done":
204
+ pushRow({ kind: "assistant", text: textBufferRef.current });
205
+ textBufferRef.current = "";
206
+ setStreamingText("");
207
+ break;
208
+ case "tool-start":
209
+ setRunningTool({ id: event.id, name: event.name, summary: event.summary });
210
+ setBusyLabel(`Running ${event.name}`);
211
+ break;
212
+ case "tool-end":
213
+ setRunningTool(null);
214
+ setBusyLabel("Thinking");
215
+ pushRow({
216
+ kind: "tool",
217
+ name: event.name,
218
+ summary: event.summary,
219
+ resultPreview: event.resultPreview,
220
+ isError: event.isError,
221
+ diff: event.diff,
222
+ });
223
+ break;
224
+ case "todos":
225
+ setTasks(event.todos);
226
+ break;
227
+ case "usage":
228
+ setContextTokens(event.inputTokens + event.outputTokens);
229
+ setTotalCost(event.totalCost);
230
+ break;
231
+ case "completion":
232
+ pushRow({ kind: "completion", text: event.result });
233
+ break;
234
+ case "error":
235
+ pushRow({ kind: "error", text: event.message });
236
+ break;
237
+ case "turn-end":
238
+ // Flush anything still streaming (e.g. on interrupt).
239
+ if (textBufferRef.current) {
240
+ pushRow({ kind: "assistant", text: textBufferRef.current });
241
+ textBufferRef.current = "";
242
+ setStreamingText("");
243
+ }
244
+ if (reasoningBufferRef.current) {
245
+ reasoningBufferRef.current = "";
246
+ setStreamingReasoning("");
247
+ }
248
+ setRunningTool(null);
249
+ setBusy(false);
250
+ maybeFetchTitle();
251
+ refreshUsage();
252
+ break;
253
+ }
254
+ }, [pushRow, maybeFetchTitle, refreshUsage]);
255
+ const createAgent = useCallback((resume) => {
256
+ const current = loadSettings();
257
+ return new Agent({
258
+ cwd: process.cwd(),
259
+ token: getAuthToken(current),
260
+ modelId: current.model,
261
+ organizationId: current.organizationId,
262
+ baseUrl: current.baseUrl,
263
+ autoApproveEdits: current.autoApproveEdits,
264
+ autoApproveSafeCommands: current.autoApproveSafeCommands,
265
+ resume,
266
+ callbacks: {
267
+ onEvent: handleEvent,
268
+ requestApproval: (request) => new Promise((resolve) => setPendingApproval({ request, resolve })),
269
+ requestFollowup: (question, suggestions) => new Promise((resolve) => setPendingFollowup({ question, suggestions, resolve })),
270
+ },
271
+ });
272
+ }, [handleEvent]);
273
+ const getAgent = useCallback(() => {
274
+ if (!agentRef.current) {
275
+ agentRef.current = createAgent();
276
+ }
277
+ return agentRef.current;
278
+ }, [createAgent]);
279
+ const handleResume = useCallback((session) => {
280
+ setResumableSessions(null);
281
+ agentRef.current = createAgent(session);
282
+ resetTranscript();
283
+ setTasks(session.todos ?? "");
284
+ setTotalCost(session.totalCost ?? 0);
285
+ if (session.title) {
286
+ // The stored title is already the backend one (or the prompt
287
+ // fallback); don't re-fetch for this task.
288
+ titleTaskRef.current = session.id;
289
+ setSessionTitle(session.title);
290
+ setTerminalTitle(`${session.title} (orbcode)`);
291
+ }
292
+ // Replay the conversation into the transcript.
293
+ for (const message of session.messages) {
294
+ if (message.role === "user" && typeof message.content === "string") {
295
+ const match = /<user_query>\n?([\s\S]*?)\n?<\/user_query>/.exec(message.content);
296
+ if (match)
297
+ pushRow({ kind: "user", text: match[1] });
298
+ }
299
+ else if (message.role === "assistant" && typeof message.content === "string" && message.content.trim()) {
300
+ pushRow({ kind: "assistant", text: message.content });
301
+ }
302
+ }
303
+ pushRow({ kind: "info", text: `Resumed session: ${session.title || session.id}` });
304
+ }, [createAgent, pushRow, resetTranscript]);
305
+ const switchModel = useCallback((modelId) => {
306
+ const updated = { ...loadSettings(), model: modelId };
307
+ setSettings(updated);
308
+ saveSettings(updated);
309
+ agentRef.current?.setModel(modelId);
310
+ pushRow({ kind: "info", text: `Model switched to ${getModel(modelId).name}` });
311
+ }, [pushRow]);
312
+ const handleCommand = useCallback((command) => {
313
+ const [name, ...rest] = command.split(/\s+/);
314
+ const arg = rest.join(" ");
315
+ switch (name) {
316
+ case "/help":
317
+ pushRow({
318
+ kind: "info",
319
+ text: SLASH_COMMANDS.map((c) => `${c.name.padEnd(12)} ${c.description}`).join("\n"),
320
+ });
321
+ break;
322
+ case "/model": {
323
+ const ids = Object.keys(AXON_MODELS);
324
+ if (arg && isValidAxonModel(arg)) {
325
+ switchModel(arg);
326
+ }
327
+ else if (arg && isValidAxonModel(`axon-code-2-5-${arg}`)) {
328
+ switchModel(`axon-code-2-5-${arg}`);
329
+ }
330
+ else if (arg) {
331
+ pushRow({ kind: "error", text: `Unknown model "${arg}". Available: ${ids.join(", ")}` });
332
+ }
333
+ else {
334
+ setModelPickerOpen(true);
335
+ }
336
+ break;
337
+ }
338
+ case "/clear":
339
+ // Like the terminal's `clear`: wipe the view only. The
340
+ // conversation, session and context all continue.
341
+ resetTranscript();
342
+ break;
343
+ case "/new":
344
+ // Drop the agent entirely so the next message starts a fresh session.
345
+ agentRef.current = null;
346
+ titleTaskRef.current = null;
347
+ setSessionTitle("");
348
+ setTerminalTitle("orbcode");
349
+ setTasks("");
350
+ setContextTokens(0);
351
+ resetTranscript();
352
+ break;
353
+ case "/analytics": {
354
+ const url = `${APP_URL}/orbital`;
355
+ pushRow({ kind: "info", text: `Opening analytics: ${url}` });
356
+ void open(url).catch(() => { });
357
+ break;
358
+ }
359
+ case "/resume": {
360
+ const sessions = listSessions(process.cwd()).filter((s) => s.id !== agentRef.current?.taskId);
361
+ if (sessions.length === 0) {
362
+ pushRow({ kind: "info", text: "No previous sessions found for this directory." });
363
+ break;
364
+ }
365
+ setResumableSessions(sessions);
366
+ break;
367
+ }
368
+ case "/compact":
369
+ if (!getAuthToken(settings)) {
370
+ setView("login");
371
+ break;
372
+ }
373
+ setBusy(true);
374
+ setBusyLabel("Compacting");
375
+ void getAgent().compact();
376
+ break;
377
+ case "/tasks":
378
+ pushRow({
379
+ kind: "info",
380
+ text: tasks.trim() ? `Tasks\n${tasks}` : "No tasks yet.",
381
+ });
382
+ break;
383
+ case "/status": {
384
+ const model = getModel(settings.model);
385
+ const contextPct = Math.min(100, Math.round((contextTokens / model.contextWindow) * 100));
386
+ pushRow({
387
+ kind: "info",
388
+ text: [
389
+ `Version ${VERSION}`,
390
+ `Model ${model.name} (${model.id})`,
391
+ `Directory ${process.cwd()}`,
392
+ `Account ${getAuthToken(settings) ? (settings.apiKey || process.env.ORBCODE_TOKEN ? "API key" : "signed in") : "signed out"}${settings.organizationId ? ` · org ${settings.organizationId}` : ""}`,
393
+ `Gateway ${settings.baseUrl ?? "MatterAI (default)"}`,
394
+ `Context ${contextTokens.toLocaleString()} / ${model.contextWindow.toLocaleString()} tokens (${contextPct}%)`,
395
+ `Cost $${totalCost.toFixed(4)} this session`,
396
+ `Approvals edits ${settings.autoApproveEdits ? "auto" : "ask"} · safe commands ${settings.autoApproveSafeCommands ? "auto" : "ask"}`,
397
+ ...(sessionTitle ? [`Task ${sessionTitle}`] : []),
398
+ ].join("\n"),
399
+ });
400
+ const statusToken = getAuthToken(settings);
401
+ if (statusToken) {
402
+ fetchProfile(statusToken)
403
+ .then((profile) => {
404
+ const lines = usageLines(profile);
405
+ if (lines.length > 0)
406
+ pushRow({ kind: "info", text: lines.join("\n") });
407
+ })
408
+ .catch(() => { });
409
+ }
410
+ break;
411
+ }
412
+ case "/init":
413
+ if (!getAuthToken(settings)) {
414
+ setView("login");
415
+ break;
416
+ }
417
+ pushRow({ kind: "user", text: "/init" });
418
+ setBusy(true);
419
+ setBusyLabel("Thinking");
420
+ void getAgent().runTurn(INIT_PROMPT);
421
+ break;
422
+ case "/commit":
423
+ if (!getAuthToken(settings)) {
424
+ setView("login");
425
+ break;
426
+ }
427
+ pushRow({ kind: "user", text: command });
428
+ setBusy(true);
429
+ setBusyLabel("Thinking");
430
+ void getAgent().runTurn(buildCommitPrompt(arg));
431
+ break;
432
+ case "/code-review":
433
+ if (!getAuthToken(settings)) {
434
+ setView("login");
435
+ break;
436
+ }
437
+ pushRow({ kind: "user", text: command });
438
+ setBusy(true);
439
+ setBusyLabel("Reviewing");
440
+ void getAgent().runTurn(buildCodeReviewPrompt(arg));
441
+ break;
442
+ case "/version":
443
+ pushRow({ kind: "info", text: `OrbCode CLI v${VERSION}` });
444
+ break;
445
+ case "/cost": {
446
+ pushRow({ kind: "info", text: `Session cost: $${totalCost.toFixed(4)} (fetching balance…)` });
447
+ const token = getAuthToken(settings);
448
+ if (token) {
449
+ fetchBalance(token, settings.organizationId).then((balance) => {
450
+ if (balance !== undefined) {
451
+ pushRow({ kind: "info", text: `Account balance: $${balance.toFixed(2)}` });
452
+ }
453
+ });
454
+ fetchProfile(token)
455
+ .then((profile) => {
456
+ const lines = usageLines(profile);
457
+ if (lines.length > 0)
458
+ pushRow({ kind: "info", text: lines.join("\n") });
459
+ })
460
+ .catch(() => { });
461
+ }
462
+ break;
463
+ }
464
+ case "/login":
465
+ setView("login");
466
+ break;
467
+ case "/logout": {
468
+ const updated = { ...settings, token: undefined };
469
+ setSettings(updated);
470
+ saveSettings(updated);
471
+ agentRef.current = null;
472
+ setView("login");
473
+ break;
474
+ }
475
+ case "/exit":
476
+ exit();
477
+ break;
478
+ default:
479
+ pushRow({ kind: "error", text: `Unknown command: ${name}. Try /help.` });
480
+ }
481
+ }, [settings, tasks, contextTokens, totalCost, sessionTitle, exit, pushRow, getAgent, switchModel, resetTranscript]);
482
+ const handleSubmit = useCallback((value) => {
483
+ if (value.startsWith("/")) {
484
+ handleCommand(value);
485
+ return;
486
+ }
487
+ if (!getAuthToken(settings)) {
488
+ setView("login");
489
+ return;
490
+ }
491
+ pushRow({ kind: "user", text: value });
492
+ setBusy(true);
493
+ setBusyLabel("Thinking");
494
+ void getAgent().runTurn(value);
495
+ }, [settings.token, handleCommand, getAgent, pushRow]);
496
+ // Apply --resume and an initial prompt (`orbcode "do something"`) on startup.
497
+ const bootedRef = useRef(false);
498
+ useEffect(() => {
499
+ if (bootedRef.current)
500
+ return;
501
+ bootedRef.current = true;
502
+ if (initialSession)
503
+ handleResume(initialSession);
504
+ if (initialPrompt)
505
+ handleSubmit(initialPrompt);
506
+ refreshUsage();
507
+ }, [initialSession, initialPrompt, handleResume, handleSubmit, refreshUsage]);
508
+ useInput((input, key) => {
509
+ if (key.escape && busy && !pendingApproval && !pendingFollowup) {
510
+ agentRef.current?.abort();
511
+ }
512
+ if (key.tab && key.shift) {
513
+ setApprovalMode((prev) => {
514
+ const next = prev === "ask" ? "edits" : prev === "edits" ? "auto" : "ask";
515
+ const autoApproveEdits = next !== "ask";
516
+ const autoApproveSafeCommands = next === "auto";
517
+ const updated = { ...loadSettings(), autoApproveEdits, autoApproveSafeCommands };
518
+ setSettings(updated);
519
+ saveSettings(updated);
520
+ agentRef.current?.setApprovalMode(autoApproveEdits, autoApproveSafeCommands);
521
+ return next;
522
+ });
523
+ }
524
+ if (key.ctrl && input === "o") {
525
+ const expanded = !expandReasoningRef.current;
526
+ expandReasoningRef.current = expanded;
527
+ // Re-render the whole transcript (including past thinking) with the
528
+ // new expansion state: clear the screen and remount <Static>.
529
+ setRows((prev) => prev.map((row) => (row.kind === "reasoning" ? { ...row, expanded } : row)));
530
+ if (process.stdout.isTTY) {
531
+ process.stdout.write("\x1b[2J\x1b[H");
532
+ }
533
+ setStaticKey((k) => k + 1);
534
+ }
535
+ });
536
+ const handleLogin = useCallback((token, profile) => {
537
+ const updated = { ...loadSettings(), token };
538
+ setSettings(updated);
539
+ saveSettings(updated);
540
+ agentRef.current = null;
541
+ setView("chat");
542
+ setUsage({ plan: profile.plan, usagePercentage: profile.usagePercentage });
543
+ const who = profile.user?.name || profile.user?.email;
544
+ pushRow({ kind: "info", text: `Signed in${who ? ` as ${who}` : ""}. Ready when you are.` });
545
+ }, [pushRow]);
546
+ const taskLines = useMemo(() => tasks.split("\n").map((l) => l.trim()).filter(Boolean), [tasks]);
547
+ const inputActive = view === "chat" && !busy && !pendingApproval && !pendingFollowup && !modelPickerOpen && !resumableSessions;
548
+ 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: [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
549
+ .replace(/^[-*]\s*\[x\]/i, " ■")
550
+ .replace(/^[-*]\s*\[-\]/, " ◧")
551
+ .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) => {
552
+ setModelPickerOpen(false);
553
+ switchModel(modelId);
554
+ }, onCancel: () => setModelPickerOpen(false) }) })), resumableSessions && (_jsx(Box, { marginTop: 1, children: _jsx(SessionPicker, { sessions: resumableSessions, onSelect: handleResume, onCancel: () => setResumableSessions(null) }) })), pendingApproval && (_jsx(Box, { marginTop: 1, children: _jsx(ApprovalPrompt, { request: pendingApproval.request, onDecision: (decision) => {
555
+ pendingApproval.resolve(decision);
556
+ setPendingApproval(null);
557
+ } }) })), pendingFollowup && (_jsx(Box, { marginTop: 1, children: _jsx(FollowupPrompt, { question: pendingFollowup.question, suggestions: pendingFollowup.suggestions, onAnswer: (answer) => {
558
+ pushRow({ kind: "user", text: answer });
559
+ pendingFollowup.resolve(answer);
560
+ setPendingFollowup(null);
561
+ } }) })), busy && !pendingApproval && !pendingFollowup && !streamingText && (_jsx(Box, { marginTop: 1, children: _jsx(Spinner, { label: busyLabel }) })), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_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 })] })] }))] }));
562
+ }
563
+ function tail(text, lines) {
564
+ const all = text.split("\n").filter((l) => l.trim());
565
+ return all.slice(-lines).join("\n");
566
+ }
567
+ function LoginSection({ onLogin }) {
568
+ return _jsx(LoginView, { onLogin: onLogin });
569
+ }
@@ -0,0 +1,82 @@
1
+ import { jsxs as _jsxs, jsx as _jsx, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { useEffect, useRef, useState } from "react";
3
+ import { Box, Text, useInput } from "ink";
4
+ import open from "open";
5
+ import { COLORS, PRODUCT_NAME } from "../branding.js";
6
+ import { getAuthorizeUrl, pollDeviceAuth, startDeviceAuth, verifyToken, } from "../auth/auth.js";
7
+ import { Spinner } from "./components/Spinner.js";
8
+ export function LoginView({ onLogin }) {
9
+ const [phase, setPhase] = useState("idle");
10
+ const [error, setError] = useState(null);
11
+ const [authorizeUrl, setAuthorizeUrl] = useState("");
12
+ const cancelledRef = useRef(false);
13
+ const pollGeneration = useRef(0);
14
+ useEffect(() => () => {
15
+ cancelledRef.current = true;
16
+ }, []);
17
+ const finishWithToken = async (token) => {
18
+ setPhase("verifying");
19
+ setError(null);
20
+ try {
21
+ const profile = await verifyToken(token);
22
+ if (!cancelledRef.current)
23
+ onLogin(token, profile);
24
+ }
25
+ catch (err) {
26
+ setPhase("idle");
27
+ setError(err.message);
28
+ }
29
+ };
30
+ const beginBrowserLogin = async () => {
31
+ setPhase("starting");
32
+ setError(null);
33
+ const generation = ++pollGeneration.current;
34
+ try {
35
+ const { devicecode, expiresIn, interval } = await startDeviceAuth();
36
+ const url = getAuthorizeUrl(devicecode);
37
+ setAuthorizeUrl(url);
38
+ open(url).catch(() => { });
39
+ setPhase("waiting");
40
+ const deadline = Date.now() + expiresIn * 1000;
41
+ while (Date.now() < deadline) {
42
+ if (cancelledRef.current || pollGeneration.current !== generation)
43
+ return;
44
+ await new Promise((resolve) => setTimeout(resolve, interval * 1000));
45
+ if (cancelledRef.current || pollGeneration.current !== generation)
46
+ return;
47
+ const result = await pollDeviceAuth(devicecode);
48
+ if (result.status === "authorized") {
49
+ await finishWithToken(result.token);
50
+ return;
51
+ }
52
+ if (result.status === "expired")
53
+ break;
54
+ }
55
+ if (pollGeneration.current === generation && !cancelledRef.current) {
56
+ setPhase("idle");
57
+ setError("Sign-in timed out. Press Enter to try again.");
58
+ }
59
+ }
60
+ catch (err) {
61
+ if (pollGeneration.current === generation && !cancelledRef.current) {
62
+ setPhase("idle");
63
+ setError(err.message);
64
+ }
65
+ }
66
+ };
67
+ useInput((input, key) => {
68
+ if (phase === "verifying" || phase === "starting")
69
+ return;
70
+ if (key.escape && phase === "waiting") {
71
+ // Cancel polling and go back to idle so the user can retry.
72
+ pollGeneration.current++;
73
+ setPhase("idle");
74
+ setError(null);
75
+ return;
76
+ }
77
+ if (key.return && phase === "idle") {
78
+ void beginBrowserLogin();
79
+ }
80
+ });
81
+ return (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: COLORS.primary, paddingX: 1, children: [_jsxs(Text, { bold: true, color: COLORS.primary, children: ["Sign in to ", PRODUCT_NAME] }), phase === "waiting" ? (_jsxs(_Fragment, { children: [_jsx(Box, { marginTop: 1, children: _jsx(Spinner, { label: "Waiting for authorization in your browser" }) }), _jsx(Text, { dimColor: true, children: "Approve the \"Authorize OrbCode CLI\" dialog at:" }), _jsxs(Text, { dimColor: true, children: [" ", authorizeUrl] }), _jsx(Text, { dimColor: true, children: "Esc to cancel" })] })) : (_jsxs(Text, { children: ["Press ", _jsx(Text, { bold: true, children: "Enter" }), " to open MatterAI in your browser and authorize OrbCode CLI."] })), phase === "starting" && _jsx(Text, { color: COLORS.thinking, children: "Contacting sign-in service\u2026" }), phase === "verifying" && _jsx(Text, { color: COLORS.thinking, children: "Verifying\u2026" }), error && _jsxs(Text, { color: COLORS.error, children: ["\u2717 ", error] }), _jsx(Text, { dimColor: true, children: "To use a token instead, set apiKey in ~/.orbcode/settings.json or the ORBCODE_TOKEN env var." })] }));
82
+ }