@matterailab/orbcode 0.3.2 → 0.3.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/README.md CHANGED
@@ -297,7 +297,8 @@ MatterAI gateway untouched.
297
297
  | `/tasks` | print the current task list |
298
298
  | `/status` | version, model, account, gateway, context usage, cost, approval modes |
299
299
  | `/usage` | fetch plan usage |
300
- | `/init` | analyze the codebase and create/improve `AGENTS.md` |
300
+ | `/init` | analyze the codebase and create/improve `AGENTS.md` in the repo's `.orb/` directory |
301
+ | `/link` | link other repos on your machine so changes here are checked against them (enter a folder path) |
301
302
  | `/mcp` | manage MCP servers — enable, disable, reconnect, view status & tool counts |
302
303
  | `/login` | start the browser sign-in flow |
303
304
  | `/logout` | remove the saved token |
package/dist/branding.js CHANGED
@@ -23,9 +23,9 @@ export const COLORS = {
23
23
  user: "#569CD6",
24
24
  };
25
25
  export const LOGO = `
26
- ___ _ ____ _
27
- / _ \\ _ __ | |__ / ___| ___ __| | ___
28
- | | | || '__|| '_ \\ | | / _ \\ / _\` | / _ \\
29
- | |_| || | | |_) || |___ | (_) || (_| || __/
30
- \\___/ |_| |_.__/ \\____| \\___/ \\__,_| \\___|
26
+ ___ _ _
27
+ / _ \\ _ __| |__ ___ ___ __| | ___
28
+ | | | | '__| '_ \\ / __/ _ \\ / _\` |/ _ \\
29
+ | |_| | | | |_) | (_| (_) | (_| | __/
30
+ \\___/|_| |_.__/ \\___\\___/ \\__,_|\\___|
31
31
  `;
@@ -0,0 +1,163 @@
1
+ import * as fs from "node:fs";
2
+ import * as os from "node:os";
3
+ import * as path from "node:path";
4
+ const MAX_AGENTS_CHARS = 4000;
5
+ const MAX_LINKED_REPOS = 8;
6
+ /** Where a linked repo's AGENTS.md might live, in precedence order. `.orbital`
7
+ * (the IDE extension's dir) and `.orbcode` are legacy locations, still read for
8
+ * backward compatibility and cross-tool linking. */
9
+ const AGENTS_LOCATIONS = [
10
+ path.join(".orb", "AGENTS.md"),
11
+ path.join(".orbital", "AGENTS.md"),
12
+ path.join(".orbcode", "AGENTS.md"),
13
+ "AGENTS.md",
14
+ ];
15
+ function isDir(p) {
16
+ try {
17
+ return fs.statSync(p).isDirectory();
18
+ }
19
+ catch {
20
+ return false;
21
+ }
22
+ }
23
+ /**
24
+ * The repo-level OrbCode directory for shared, tool-neutral data like AGENTS.md
25
+ * and links.json — always `.orb`. This is the folder the IDE and the CLI both
26
+ * read/write, so they stay in sync.
27
+ *
28
+ * Note: this is deliberately NOT where machine settings live. Those stay put
29
+ * (`~/.orbcode` and `<repo>/.orbcode/settings.json`); only the repo-level
30
+ * AGENTS.md/links folder moved to `.orb`. Legacy `.orbcode/AGENTS.md` files are
31
+ * still *read* (see the memory loader and AGENTS_LOCATIONS), but new files are
32
+ * written here.
33
+ */
34
+ export function resolveProjectDir(cwd) {
35
+ return path.join(cwd, ".orb");
36
+ }
37
+ function linksFilePath(cwd) {
38
+ return path.join(resolveProjectDir(cwd), "links.json");
39
+ }
40
+ /**
41
+ * Read the linked repos for a project (empty array if none / unreadable).
42
+ *
43
+ * The schema is tolerant so links written by either tool work: each entry needs
44
+ * only an `input` (the IDE extension may omit the resolved `path`); we fill the
45
+ * `path` by resolving the input when it's missing.
46
+ */
47
+ export function loadLinks(cwd = process.cwd()) {
48
+ try {
49
+ const parsed = JSON.parse(fs.readFileSync(linksFilePath(cwd), "utf8"));
50
+ if (!Array.isArray(parsed.links))
51
+ return [];
52
+ const out = [];
53
+ for (const raw of parsed.links) {
54
+ if (!raw)
55
+ continue;
56
+ const input = typeof raw.input === "string" ? raw.input : typeof raw.path === "string" ? raw.path : undefined;
57
+ if (!input)
58
+ continue;
59
+ const resolved = typeof raw.path === "string" ? raw.path : resolveLinkTarget(input);
60
+ if (!resolved)
61
+ continue;
62
+ out.push({ input, path: resolved });
63
+ }
64
+ return out;
65
+ }
66
+ catch {
67
+ return [];
68
+ }
69
+ }
70
+ function saveLinks(cwd, links) {
71
+ const file = linksFilePath(cwd);
72
+ fs.mkdirSync(path.dirname(file), { recursive: true });
73
+ fs.writeFileSync(file, JSON.stringify({ links }, null, "\t") + "\n");
74
+ }
75
+ /**
76
+ * Turn the folder path the user entered — absolute, `~/path`, or relative to
77
+ * cwd — into an absolute filesystem path. Returns undefined for empty input.
78
+ */
79
+ export function resolveLinkTarget(input) {
80
+ let value = input.trim();
81
+ if (!value)
82
+ return undefined;
83
+ if (value.startsWith("~/"))
84
+ value = path.join(os.homedir(), value.slice(2));
85
+ return path.resolve(value);
86
+ }
87
+ /** Resolve, validate and persist a new link. Idempotent on the resolved path. */
88
+ export function addLink(cwd, input) {
89
+ const resolved = resolveLinkTarget(input);
90
+ if (!resolved)
91
+ return { ok: false, message: "Enter a folder path." };
92
+ if (path.resolve(cwd) === resolved)
93
+ return { ok: false, message: "Can't link a repo to itself." };
94
+ if (!isDir(resolved))
95
+ return { ok: false, message: `Not a directory: ${resolved}` };
96
+ const links = loadLinks(cwd);
97
+ if (links.some((l) => l.path === resolved))
98
+ return { ok: false, message: "Already linked." };
99
+ if (links.length >= MAX_LINKED_REPOS) {
100
+ return { ok: false, message: `At most ${MAX_LINKED_REPOS} linked repos.` };
101
+ }
102
+ links.push({ input: input.trim(), path: resolved });
103
+ saveLinks(cwd, links);
104
+ return { ok: true, message: `Linked ${resolved}` };
105
+ }
106
+ /** Remove a link by its resolved path. */
107
+ export function removeLink(cwd, targetPath) {
108
+ const links = loadLinks(cwd).filter((l) => l.path !== targetPath);
109
+ saveLinks(cwd, links);
110
+ }
111
+ /** First AGENTS.md found inside a linked repo, or undefined. */
112
+ function readLinkedAgents(repo) {
113
+ for (const rel of AGENTS_LOCATIONS) {
114
+ try {
115
+ const text = fs.readFileSync(path.join(repo, rel), "utf8");
116
+ if (text.trim())
117
+ return text;
118
+ }
119
+ catch {
120
+ // try the next location
121
+ }
122
+ }
123
+ return undefined;
124
+ }
125
+ function truncate(text, max) {
126
+ return text.length <= max ? text : text.slice(0, max) + "\n… (truncated)";
127
+ }
128
+ /**
129
+ * Render the linked-repos block for the agent's environment details. Returns ""
130
+ * when nothing is linked. Each repo's AGENTS.md is pulled in (when present) so
131
+ * the model knows the linked codebase without exploring it first.
132
+ */
133
+ export function renderLinkedReposSection(cwd) {
134
+ const links = loadLinks(cwd).slice(0, MAX_LINKED_REPOS);
135
+ if (links.length === 0)
136
+ return "";
137
+ const parts = [
138
+ "## Linked Repositories",
139
+ "",
140
+ "This repository is linked to the repositories below — separate codebases on disk that are coupled to this one. When you change this repo, consider whether the change ripples into a linked repo: inspect the linked code for impact and, when relevant, propose (or make, if the user asks) the matching changes there. You can read and edit files in these repos directly by their absolute paths.",
141
+ "",
142
+ ];
143
+ for (const link of links) {
144
+ const exists = isDir(link.path);
145
+ parts.push(`### ${path.basename(link.path)} — \`${link.path}\`${exists ? "" : " (path not found)"}`);
146
+ if (!exists) {
147
+ parts.push("");
148
+ continue;
149
+ }
150
+ const agents = readLinkedAgents(link.path);
151
+ parts.push("");
152
+ if (agents) {
153
+ parts.push("Its AGENTS.md:");
154
+ parts.push("");
155
+ parts.push(truncate(agents.trim(), MAX_AGENTS_CHARS));
156
+ }
157
+ else {
158
+ parts.push("(no AGENTS.md found — explore the repo directly if you need its structure)");
159
+ }
160
+ parts.push("");
161
+ }
162
+ return parts.join("\n");
163
+ }
@@ -11,8 +11,45 @@ import { getSessionFilePath, saveSession } from "./sessions.js";
11
11
  import { HookRunner } from "./hooks.js";
12
12
  import { loadMemoryFiles } from "../memory/loader.js";
13
13
  import { loadSkills } from "../skills/loader.js";
14
+ import { renderLinkedReposSection } from "../config/links.js";
14
15
  const MAX_STEPS_PER_TURN = 50;
15
16
  const RESULT_PREVIEW_LINES = 6;
17
+ /** How many times to automatically re-establish a model request that fails
18
+ * before producing any output (transient/connection errors). */
19
+ const MAX_STREAM_RETRIES = 3;
20
+ /** Transient failures worth auto-retrying: any transport/connection error (no
21
+ * usable HTTP status — socket reset, DNS, timeout, TLS drop) plus 5xx/408/429
22
+ * server responses. Real 4xx client errors (auth, bad request) are not retried. */
23
+ function isRetryableStreamError(error) {
24
+ const err = error;
25
+ const status = Number(err?.status ?? err?.code);
26
+ if (Number.isFinite(status) && status !== 0) {
27
+ return status >= 500 || status === 408 || status === 429;
28
+ }
29
+ return true;
30
+ }
31
+ function retryBackoffMs(attempt) {
32
+ return Math.min(500 * 2 ** attempt, 8000);
33
+ }
34
+ /** Sleep that settles early (rejecting with AbortError) if the signal fires, so
35
+ * a user interrupt isn't stuck waiting out a retry backoff. */
36
+ function interruptibleDelay(ms, signal) {
37
+ return new Promise((resolve, reject) => {
38
+ if (signal.aborted) {
39
+ reject(new DOMException("aborted", "AbortError"));
40
+ return;
41
+ }
42
+ const onAbort = () => {
43
+ clearTimeout(timer);
44
+ reject(new DOMException("aborted", "AbortError"));
45
+ };
46
+ const timer = setTimeout(() => {
47
+ signal.removeEventListener("abort", onAbort);
48
+ resolve();
49
+ }, ms);
50
+ signal.addEventListener("abort", onAbort, { once: true });
51
+ });
52
+ }
16
53
  function detectRepo(cwd) {
17
54
  try {
18
55
  const remote = execSync("git config --get remote.origin.url", {
@@ -219,6 +256,7 @@ export class Agent {
219
256
  const timeZoneOffsetHours = Math.floor(Math.abs(timeZoneOffset));
220
257
  const timeZoneOffsetMinutes = Math.abs(Math.round((Math.abs(timeZoneOffset) - timeZoneOffsetHours) * 60));
221
258
  const timeZoneOffsetStr = `${timeZoneOffset >= 0 ? "+" : "-"}${timeZoneOffsetHours}:${timeZoneOffsetMinutes.toString().padStart(2, "0")}`;
259
+ const linkedRepos = renderLinkedReposSection(this.options.cwd);
222
260
  return `# Environment Details
223
261
 
224
262
  ## Current Workspace Directory (${this.options.cwd}) Files
@@ -226,7 +264,7 @@ ${files.join("\n") || "(empty directory)"}
226
264
  ${files.length >= 200 ? "\n(File list truncated.)" : ""}
227
265
 
228
266
  ${git}
229
-
267
+ ${linkedRepos ? `\n${linkedRepos}` : ""}
230
268
  ## Current Time
231
269
  Current time in ISO 8601 UTC format: ${now.toISOString()}
232
270
  User time zone: ${timeZone}, UTC${timeZoneOffsetStr}`;
@@ -420,7 +458,13 @@ User time zone: ${timeZone}, UTC${timeZoneOffsetStr}`;
420
458
  },
421
459
  ];
422
460
  let summary = "";
423
- for await (const chunk of this.client.createMessage(this.systemPrompt, request, [], signal)) {
461
+ for await (const chunk of this.streamWithRetry(() => this.client.createMessage(this.systemPrompt, request, [], signal), signal, () => {
462
+ // Compaction only streams text (committed once at the end), so a
463
+ // mid-stream retry just discards the partial summary.
464
+ summary = "";
465
+ onEvent({ type: "stream-reset" });
466
+ return true;
467
+ })) {
424
468
  if (signal.aborted)
425
469
  throw new DOMException("aborted", "AbortError");
426
470
  if (chunk.type === "text") {
@@ -466,28 +510,102 @@ User time zone: ${timeZone}, UTC${timeZoneOffsetStr}`;
466
510
  onEvent({ type: "turn-end" });
467
511
  }
468
512
  }
513
+ /**
514
+ * Consume a model stream, automatically re-establishing the request up to
515
+ * MAX_STREAM_RETRIES times on a transient/connection failure. A user abort is
516
+ * never retried.
517
+ *
518
+ * Before the first chunk of an attempt nothing has streamed, so the retry is
519
+ * always clean. Once chunks have streamed, retrying would duplicate on-screen
520
+ * output — so we only retry mid-stream when the caller supplies `onRestart` and
521
+ * it returns true, meaning it rolled the partial output back (cleared buffers,
522
+ * reset accumulators). If it can't (e.g. a row was already committed), the error
523
+ * propagates.
524
+ */
525
+ async *streamWithRetry(makeStream, signal, onRestart) {
526
+ for (let attempt = 0;; attempt++) {
527
+ let produced = false;
528
+ try {
529
+ for await (const chunk of makeStream()) {
530
+ produced = true;
531
+ yield chunk;
532
+ }
533
+ return;
534
+ }
535
+ catch (error) {
536
+ if (signal.aborted || error.name === "AbortError")
537
+ throw error;
538
+ if (attempt >= MAX_STREAM_RETRIES || !isRetryableStreamError(error))
539
+ throw error;
540
+ // Output already streamed this attempt: only retry if the caller can
541
+ // cleanly roll it back, otherwise a restart would duplicate it.
542
+ if (produced && !(onRestart?.() ?? false))
543
+ throw error;
544
+ const delayMs = retryBackoffMs(attempt);
545
+ this.options.callbacks.onEvent({
546
+ type: "system",
547
+ message: `Connection to the model failed (${error.message}). Retrying ${attempt + 1}/${MAX_STREAM_RETRIES} in ${Math.ceil(delayMs / 1000)}s…`,
548
+ isError: false,
549
+ });
550
+ await interruptibleDelay(delayMs, signal);
551
+ }
552
+ }
553
+ }
469
554
  /** Run one model request + tool execution round. Returns true when the turn is over. */
470
555
  async runStep() {
471
556
  const { onEvent } = this.options.callbacks;
472
557
  const signal = this.abortController.signal;
473
558
  let assistantText = "";
474
- let hadReasoning = false;
559
+ // A reasoning segment is "open" from its first delta until visible content
560
+ // (text or a tool call) begins. We emit reasoning-done at that transition so
561
+ // "Thought for Ns" reflects only the thinking time — not the answer that
562
+ // follows — and the live "Thinking" block stops before the answer streams.
563
+ // A fresh segment can re-open if the model interleaves reasoning with content.
564
+ let reasoningOpen = false;
475
565
  let reasoningStart = 0;
476
566
  let reasoningDetails;
567
+ // Once a reasoning-done row is committed to the transcript we can't roll it
568
+ // back, so a mid-stream retry after that point isn't clean.
569
+ let reasoningRowCommitted = false;
570
+ const finalizeReasoning = () => {
571
+ if (reasoningOpen) {
572
+ reasoningOpen = false;
573
+ reasoningRowCommitted = true;
574
+ onEvent({ type: "reasoning-done", durationMs: Date.now() - reasoningStart });
575
+ }
576
+ };
477
577
  const toolCallsByIndex = new Map();
478
578
  let nextSyntheticIndex = 10000;
479
- const stream = this.client.createMessage(this.systemPrompt, this.outgoingMessages(), getActiveTools(this.mcp), signal);
579
+ // Roll back this step's partial output so streamWithRetry can restart a
580
+ // dropped stream mid-flight. Tools only run after the stream completes, so
581
+ // nothing irreversible has happened yet; the one thing we can't undo is an
582
+ // already-committed reasoning row, so we decline the restart in that case.
583
+ const rollbackForRetry = () => {
584
+ if (reasoningRowCommitted)
585
+ return false;
586
+ assistantText = "";
587
+ reasoningOpen = false;
588
+ reasoningStart = 0;
589
+ reasoningDetails = undefined;
590
+ toolCallsByIndex.clear();
591
+ nextSyntheticIndex = 10000;
592
+ onEvent({ type: "stream-reset" });
593
+ return true;
594
+ };
595
+ const stream = this.streamWithRetry(() => this.client.createMessage(this.systemPrompt, this.outgoingMessages(), getActiveTools(this.mcp), signal), signal, rollbackForRetry);
480
596
  for await (const chunk of stream) {
481
597
  if (signal.aborted)
482
598
  throw new DOMException("aborted", "AbortError");
483
599
  switch (chunk.type) {
484
600
  case "text":
601
+ // Visible content begins — the reasoning phase (if any) is over.
602
+ finalizeReasoning();
485
603
  assistantText += chunk.text;
486
604
  onEvent({ type: "text-delta", text: chunk.text });
487
605
  break;
488
606
  case "reasoning":
489
- if (!hadReasoning) {
490
- hadReasoning = true;
607
+ if (!reasoningOpen) {
608
+ reasoningOpen = true;
491
609
  reasoningStart = Date.now();
492
610
  }
493
611
  onEvent({ type: "reasoning-delta", text: chunk.text });
@@ -497,6 +615,8 @@ User time zone: ${timeZone}, UTC${timeZoneOffsetStr}`;
497
615
  reasoningDetails = chunk.details;
498
616
  break;
499
617
  case "native_tool_calls":
618
+ // A tool call also ends the reasoning phase.
619
+ finalizeReasoning();
500
620
  for (const tc of chunk.toolCalls) {
501
621
  const index = tc.index ?? nextSyntheticIndex++;
502
622
  let pending = toolCallsByIndex.get(index);
@@ -525,9 +645,8 @@ User time zone: ${timeZone}, UTC${timeZoneOffsetStr}`;
525
645
  break;
526
646
  }
527
647
  }
528
- if (hadReasoning) {
529
- onEvent({ type: "reasoning-done", durationMs: Date.now() - reasoningStart });
530
- }
648
+ // A reasoning-only turn (no following text/tool content) still needs closing.
649
+ finalizeReasoning();
531
650
  if (assistantText) {
532
651
  onEvent({ type: "text-done" });
533
652
  }
@@ -11,8 +11,8 @@ import { getConfigDir } from "../config/settings.js";
11
11
  * first), with closer-to-cwd and higher-precedence types winning:
12
12
  *
13
13
  * 1. User memory: ~/.orbcode/AGENTS.md
14
- * 2. Project memory: AGENTS.md and .orbcode/AGENTS.md in cwd and every
15
- * parent directory (closer-to-cwd wins)
14
+ * 2. Project memory: AGENTS.md and .orb/AGENTS.md (and the legacy
15
+ * .orbcode/AGENTS.md) in cwd and every parent directory (closer-to-cwd wins)
16
16
  * 3. Local memory: AGENTS.local.md in cwd and parents (highest precedence)
17
17
  *
18
18
  * Memory files support `@path` include directives (relative, `~/`, or
@@ -118,9 +118,11 @@ export function loadMemoryFiles(cwd = process.cwd()) {
118
118
  const files = [];
119
119
  // 1. User memory (~/.orbcode/AGENTS.md)
120
120
  files.push(...loadWithIncludes(path.join(getConfigDir(), "AGENTS.md"), "user", processed, 0));
121
- // 2. Project memory (AGENTS.md + .orbcode/AGENTS.md), root -> cwd
121
+ // 2. Project memory (AGENTS.md + .orb/AGENTS.md), root -> cwd. The legacy
122
+ // .orbcode/AGENTS.md location is still read for backward compatibility.
122
123
  for (const dir of ancestorDirs(cwd).reverse()) {
123
124
  files.push(...loadWithIncludes(path.join(dir, "AGENTS.md"), "project", processed, 0));
125
+ files.push(...loadWithIncludes(path.join(dir, ".orb", "AGENTS.md"), "project", processed, 0));
124
126
  files.push(...loadWithIncludes(path.join(dir, ".orbcode", "AGENTS.md"), "project", processed, 0));
125
127
  }
126
128
  // 3. Local memory (AGENTS.local.md), root -> cwd — highest precedence
@@ -32,13 +32,13 @@ You have tools at your disposal to solve the coding task. Follow these rules reg
32
32
 
33
33
  If you intend to call multiple tools and there are no dependencies between the tool calls, make all of the independent tool calls in parallel. Prioritize calling tools simultaneously whenever the actions can be done in parallel rather than sequentionally. For example, when reading 3 files, run 3 tool calls in parallel to read all 3 files into context at the same time. Maximize use of parallel tool calls where possible to increase speed and efficiency. However, if some tool calls depend on previous calls to inform dependent values like the parameters, do NOT call these tools in parallel and instead call them sequentially. Never use placeholders or guess missing parameters in tool calls.
34
34
 
35
- # Maximize Context Understanding
35
+ # Gather Enough Context, Then Act
36
36
 
37
- Be THOROUGH when gathering information. Make sure you have the FULL picture before replying. Use additional tool calls or clarifying questions as needed.
38
- TRACE every symbol back to its definitions and usages so you fully understand it.
39
- Look past the first seemingly relevant result. EXPLORE alternative implementations, edge cases, and varied search terms until you have COMPREHENSIVE coverage of the topic.
37
+ Speed matters: your goal is the correct change in the fewest tool calls, not exhaustive coverage. Scale exploration to the task. A small, localized change typically needs about 3-6 calls locate the code, read the region and its immediate callers, check conventions — while only wide refactors justify long exploration.
40
38
 
41
- If you've performed an edit that may partially fulfill the USER's query, but you're not confident, gather more information or use more tools before ending your turn.
39
+ You have enough context when you know exactly which files and lines to change, you have seen the surrounding code's conventions, and you know how the code you are touching is used. From that point, every further search or read is waste: stop exploring and make the edit. Before each additional call, ask whether its result could change your edit; if not, skip it. Trace only the symbols your change actually depends on, never re-read regions you have already seen, and never re-verify facts you have already established.
40
+
41
+ Never edit code you have not read. If after an edit you are genuinely unsure it fulfills the USER's request, verify that specific doubt with one targeted check — do not relaunch broad exploration.
42
42
 
43
43
  Bias towards not asking the user for help if you can find the answer yourself.
44
44
 
@@ -65,7 +65,7 @@ Your system prompt may include an "Available Skills" section listing skills by n
65
65
 
66
66
  Tools whose names start with \`mcp__\` are provided by external MCP servers the user has configured. They work exactly like native tools — call them with the standard tool call format when the task requires their capabilities. Their descriptions and parameter schemas come from the MCP servers.
67
67
 
68
- CRITICAL: For any task, small or big, you will always and always use the update_todo_list tool to create the TODO list, always keep is upto date with updates to the status and updating/editing the list as needed.`;
68
+ Use the update_todo_list tool to create and maintain a TODO list for any multi-step task (3 or more steps), keeping statuses up to date as you work. For trivial tasks that need only one or two steps, skip the todo list and just do the work.`;
69
69
  const toolGuide = `
70
70
  Common tool calls and explanations
71
71
 
@@ -117,20 +117,15 @@ Common tool calls and explanations
117
117
  }
118
118
  \`\`\`
119
119
 
120
- **Example** (editing across multiple files):
121
- \`\`\`json
122
- {
123
- "edits": [
124
- {"file_path": "/path/to/api.ts", "old_string": "v1", "new_string": "v2"},
125
- {"file_path": "/path/to/config.ts", "old_string": "version: 1", "new_string": "version: 2"}
126
- ]
127
- }
128
- \`\`\`
129
-
130
120
  **Guidance for choosing between file_edit and multi_file_edit**:
131
121
  - 1 edit → \`file_edit\`
132
122
  - 2+ edits → \`multi_file_edit\` (always)
133
123
 
124
+ **Editing discipline (CRITICAL)**:
125
+ - ALWAYS copy \`old_string\` verbatim from a read_file result obtained in the same turn. NEVER reconstruct indentation or whitespace from memory — this is especially important in tab-indented files, where a reconstructed \`old_string\` will silently mismatch.
126
+ - After any successful edit, treat all earlier reads of that file as stale. Re-read the region with read_file before editing the same area of the file again.
127
+ - If one edit in a \`multi_file_edit\` batch fails with a string mismatch, STOP and re-read the file before retrying that edit. Do not guess at a corrected \`old_string\` — guessed corrections compound the mismatch.
128
+
134
129
  ## read_file Tool Usage
135
130
 
136
131
  The \`read_file\` tool reads file contents with optional offset and limit. Use it to examine code before making changes or to discuss specific sections.
@@ -141,33 +136,9 @@ The \`read_file\` tool reads file contents with optional offset and limit. Use i
141
136
  - \`offset\` (optional): Starting line number (1-indexed). Defaults to 1.
142
137
  - \`limit\` (optional): Maximum number of lines to read. If not specified, reads the complete file. Default and maximum limit is 1000 lines.
143
138
 
144
- ### Parameters Schema
145
- \`\`\`typescript
146
- {
147
- file_path: string, // Absolute path to file (required)
148
- offset?: number, // Starting line (1-indexed), defaults to 1
149
- limit?: number // Max lines to read, omit to read entire file
150
- }
151
- \`\`\`
152
-
153
- ### Examples
154
-
155
- **Read entire file:**
156
- \`\`\`json
157
- {
158
- "file_path": "/Users/username/project/src/App.tsx"
159
- }
160
- \`\`\`
161
-
162
- **Read first 50 lines:**
163
- \`\`\`json
164
- {
165
- "file_path": "/Users/username/project/src/App.tsx",
166
- "limit": 50
167
- }
168
- \`\`\`
139
+ ### Example
169
140
 
170
- **Read lines 100-150 (50 lines starting at line 100):**
141
+ **Read lines 100-150:**
171
142
  \`\`\`json
172
143
  {
173
144
  "file_path": "/Users/username/project/src/App.tsx",
@@ -176,43 +147,17 @@ The \`read_file\` tool reads file contents with optional offset and limit. Use i
176
147
  }
177
148
  \`\`\`
178
149
 
179
- ### Workflow: When You Don't Know Line Numbers
180
-
181
- **Step 1:** Use \`search_files\` to find the code:
182
- \`\`\`json
183
- {
184
- "path": "src",
185
- "regex": "function handleSubmit",
186
- "file_pattern": "*.ts"
187
- }
188
- \`\`\`
189
-
190
- **Step 2:** Note the line number from search results (e.g., line 45)
191
-
192
- **Step 3:** Read that section with \`read_file\`:
193
- \`\`\`json
194
- {
195
- "file_path": "/Users/username/project/src/Form.tsx",
196
- "offset": 40,
197
- "limit": 50
198
- }
199
- \`\`\`
150
+ Parameter rules: \`file_path\` must be an absolute path; \`offset\` and \`limit\` must be >= 1 if specified; omit \`limit\` to read from \`offset\` to the end. Call the tool multiple times to read multiple files.
200
151
 
201
- ### Parameter Rules
152
+ CRITICAL: \`offset\` is what targets a region — \`limit\` alone reads the TOP of the file. To inspect line N (e.g. from search results), you MUST pass \`offset\` ≈ N-20 together with \`limit\`. Before sending the call, confirm \`offset\` is present whenever you are aiming at a specific line.
202
153
 
203
- 1. \`file_path\` must be an absolute path
204
- 2. \`offset\` must be >= 1 if specified
205
- 3. \`limit\` must be >= 1 if specified
206
- 4. If \`limit\` is omitted, the entire file is read from \`offset\`
154
+ When you don't know line numbers: use \`search_files\` to locate the code, note the line number from the results, then \`read_file\` that region with surrounding context.
207
155
 
208
- ### Common Patterns
156
+ ### Reading Strategy
209
157
 
210
- | Use Case | Parameters |
211
- |----------|-----------|
212
- | Read entire file | \`file_path\` only |
213
- | Read from start | \`limit: 50\` |
214
- | Read middle section | \`offset: 100, limit: 50\` |
215
- | Read from a specific line to end | \`offset: 200\` |
158
+ - When investigating a bug, read whole functions or logical regions in ONE call rather than small slivers. Prefer one 150-line read over five 30-line reads — fragmented reads lose context and waste calls.
159
+ - Budget your re-reads: if you have already read a region and have not edited it since, work from what you have instead of fetching it again. Re-read only when the file has changed or you genuinely lack the detail.
160
+ - After every read, verify the output matches the parameters you sent. If you meant to read around line N but the result starts at line 1, you omitted \`offset\` re-issue the call with \`offset\` set. NEVER re-read the top of the file expecting a different result.
216
161
 
217
162
 
218
163
  # execute_command
@@ -226,7 +171,9 @@ The tool accepts these parameters:
226
171
  - \`command\` (required): The CLI command to execute. Must be valid for the user's operating system.
227
172
  - \`cwd\` (optional): The working directory to execute the command in. If not provided, the current working directory is used. Ensure this is always an absolute path (starting with \`/\`, or a drive letter like \`C:\\\` on Windows). If you are running the command in the root directly, skip this parameter. The command executor is defaulted to run in the root directory. You already have the Current Workspace Directory in the Environment Details section.
228
173
 
229
- CRITICAL: If the command is a very long running process, prefer to let the user to that they can run it manually in thier terminal. If the user specifically requests to run a long running command, you may proceed.
174
+ CRITICAL: If the command is a very long running process, prefer to let the user know so they can run it manually in their terminal. If the user specifically requests to run a long running command, you may proceed.
175
+
176
+ Command validity rules: a command is never empty, never just \`:\`, never a bare single word with no arguments, and never contains tool-call markup tokens or angle-bracket tags of any kind. Commands must be valid for the user's operating system, shell, and current working directory.
230
177
 
231
178
  ## search_files
232
179
 
@@ -262,90 +209,29 @@ The \`search_files\` tool allows you to search for patterns across files in a di
262
209
  "file_pattern": null
263
210
  }
264
211
 
265
- // Search in JSX/TSX files only
266
- {
267
- "path": "src/components",
268
- "regex": "useState",
269
- "file_pattern": "*.{jsx,tsx}"
270
- }
271
-
272
- // Search in nested directories
273
- {
274
- "path": ".",
275
- "regex": "API_KEY",
276
- "file_pattern": "**/*.env*"
277
- }
278
- \`\`\`
279
-
280
- ### ❌ INCORRECT Examples
281
- \`\`\`json
282
- // WRONG - Unquoted file_pattern (will cause JSON error)
283
- {
284
- "path": "src",
285
- "regex": "import",
286
- "file_pattern": *.js
287
- }
288
-
289
- // WRONG - Missing file_pattern entirely
290
- {
291
- "path": "src",
292
- "regex": "import"
293
- }
294
-
295
- // WRONG - Empty string instead of null
296
- {
297
- "path": "src",
298
- "regex": "import",
299
- "file_pattern": ""
300
- }
301
212
  \`\`\`
302
213
 
303
- ### Regex Pattern Tips
304
-
305
- - Use Rust regex syntax (similar to PCRE)
306
- - Escape special characters: \`\\.\`, \`\\(\`, \`\\[\`, etc.
307
- - Common patterns:
308
- - \`"word"\` - literal match
309
- - \`"\\bword\\b"\` - word boundary match
310
- - \`"function\\s+\\w+"\` - function declarations
311
- - \`"import.*from\\s+['\\"].*['\\"]"\` - import statements
214
+ The regex uses Rust syntax (similar to PCRE); escape special characters like \`\\.\` and \`\\(\`. \`file_pattern\` uses glob syntax: \`"*.ts"\`, \`"*.{jsx,tsx}"\`, \`"**/*.json"\`. When in doubt, use \`null\` to search all files.
312
215
 
313
- ### File Pattern Glob Syntax
216
+ ### Search Hygiene
314
217
 
315
- When using a string value for \`file_pattern\`:
316
- - \`"*.js"\` - All .js files in directory
317
- - \`"*.{js,ts}"\` - All .js and .ts files
318
- - \`"**/*.json"\` - All .json files recursively
319
- - \`"test_*.py"\` - Files starting with test_
320
- - \`"src/**/*.tsx"\` - All .tsx files under src/
321
-
322
- **When in doubt, use \`null\` to search all files.**
323
-
324
- ### Parameter Validation Checklist
325
-
326
- Before submitting, verify:
327
- - ✅ \`path\` is a string (directory path)
328
- - ✅ \`regex\` is a string (valid Rust regex)
329
- - ✅ \`file_pattern\` is EITHER a quoted string OR null
330
- - ✅ All three parameters are present
331
- - ✅ No unquoted glob patterns like \`*.js\`
218
+ - Exclude test, spec, and mock paths from discovery searches by default (\`__tests__\`, \`*.spec.*\`, \`*.test.*\`, \`__mocks__\`) unless the task itself is about tests. They pollute results and bury the implementation you are looking for.
219
+ - Scope \`path\` to the narrowest plausible directory instead of searching from the repository root.
220
+ - If a search returns hundreds of hits, tighten the regex or \`file_pattern\` and search again. Do not scan through the dump.
332
221
 
333
222
  ### Remember
334
223
 
335
224
  **Always quote the file_pattern value or use null. Never use bare/unquoted glob patterns.**
336
225
 
337
- ## execute_command
226
+ ## Verifying tool results and avoiding loops
227
+
228
+ - After EVERY tool call, verify the output actually matches the parameters you sent (correct file, correct line range, correct directory). A result that does not reflect your parameters means the call was malformed — fix the call, do not reason from the bad output.
229
+ - If two consecutive identical tool calls produce identical results, you are in a loop. Change the call or change the strategy. NEVER repeat the same call a third time.
338
230
 
339
- CRITICAL:
340
- 1. A command never starts with \`:\`
341
- 2. A command never contains tool-call markup tokens or angle-bracket tags of any kind
342
- 3. A command is never empty or \`:\`
343
- 4. A command is never a single word or a single word with a space
344
- 5. Commands are always valid for the user's operating system
345
- 6. Commands are always valid for the user's shell
346
- 7. Commands are always valid with executable permissions
347
- 8. Commands are always valid with the user's current working directory
231
+ ## Plan before editing
348
232
 
233
+ - Investigate first, edit second. Once the root cause is confirmed, write out the full change plan — which files, the exact locations, and the edit order — BEFORE touching anything.
234
+ - Then execute the edits in one pass (batched via \`multi_file_edit\`) and verify with a single typecheck/build at the end, rather than alternating between editing and checking.
349
235
 
350
236
  ## update_todo_list
351
237
 
@@ -353,27 +239,14 @@ CRITICAL:
353
239
  Replace the entire TODO list with an updated checklist reflecting the current state. Always provide the full list; the system will overwrite the previous one. This tool is designed for step-by-step task tracking, allowing you to confirm completion of each step before updating, update multiple task statuses at once (e.g., mark one as completed and start the next), and dynamically add new todos discovered during long or complex tasks.
354
240
 
355
241
  **Checklist Format:**
356
- - Use a single-level markdown checklist (no nesting or subtasks).
357
- - List todos in the intended execution order.
358
- - Status options:
359
- - [ ] Task description (pending)
360
- - [x] Task description (completed)
361
- - [-] Task description (in progress)
362
-
363
- **Status Rules:**
364
- - [ ] = pending (not started)
365
- - [x] = completed (fully finished, no unresolved issues)
366
- - [-] = in_progress (currently being worked on)
242
+ - Use a single-level markdown checklist (no nesting or subtasks), in intended execution order.
243
+ - Statuses: \`[ ]\` pending, \`[x]\` completed (fully finished, no unresolved issues), \`[-]\` in progress.
367
244
 
368
245
  **Core Principles:**
369
- - Before updating, always confirm which todos have been completed since the last update.
370
- - You may update multiple statuses in a single update (e.g., mark the previous as completed and the next as in progress).
371
- - When a new actionable item is discovered during a long or complex task, add it to the todo list immediately.
372
- - Do not remove any unfinished todos unless explicitly instructed.
373
- - Always retain all unfinished tasks, updating their status as needed.
374
- - Only mark a task as completed when it is fully accomplished (no partials, no unresolved dependencies).
375
- - If a task is blocked, keep it as in_progress and add a new todo describing what needs to be resolved.
376
- - Remove tasks only if they are no longer relevant or if the user requests deletion.
246
+ - Update multiple statuses in a single call (e.g., mark the previous task completed and the next in progress).
247
+ - Add newly discovered actionable items immediately. Retain all unfinished tasks; remove one only if it is no longer relevant or the user asks.
248
+ - Mark a task completed only when fully accomplished. If blocked, keep it in_progress and add a todo describing what must be resolved.
249
+ - Keep the todo list AHEAD of the work, not behind it: it is a steering tool, not a changelog. Lay out upcoming steps before you start them instead of only recording steps after they are finished.
377
250
 
378
251
  IMPORTANT: Use attempt_completion tool when you have completed the task. This signals that you are done.
379
252
  `;
package/dist/ui/App.js CHANGED
@@ -2,6 +2,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
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
+ import * as path from "node:path";
5
6
  import { COLORS, VERSION } from "../branding.js";
6
7
  import { BUILTIN_AXON_MODELS, getModel, isValidAxonModel, } from "../api/models.js";
7
8
  import { LoginView } from "./LoginView.js";
@@ -23,6 +24,8 @@ import { ModelPicker } from "./components/ModelPicker.js";
23
24
  import { SessionPicker } from "./components/SessionPicker.js";
24
25
  import { listSessions } from "../core/sessions.js";
25
26
  import { RowView } from "./components/rows.js";
27
+ import { LinkManager } from "./components/LinkManager.js";
28
+ import { addLink, loadLinks, removeLink, resolveProjectDir, } from "../config/links.js";
26
29
  const SLASH_COMMANDS = [
27
30
  { name: "/help", description: "show available commands" },
28
31
  { name: "/model", description: "select the Axon model to use" },
@@ -37,6 +40,10 @@ const SLASH_COMMANDS = [
37
40
  description: "summarize the conversation to free up context",
38
41
  },
39
42
  { name: "/tasks", description: "show the current task list" },
43
+ {
44
+ name: "/task",
45
+ description: "reference a previous task in the current conversation",
46
+ },
40
47
  {
41
48
  name: "/status",
42
49
  description: "show session status (model, context, cost, account)",
@@ -46,6 +53,10 @@ const SLASH_COMMANDS = [
46
53
  name: "/init",
47
54
  description: "analyze this codebase and create an AGENTS.md",
48
55
  },
56
+ {
57
+ name: "/link",
58
+ description: "link other repos so changes here are checked against them",
59
+ },
49
60
  {
50
61
  name: "/mcp",
51
62
  description: "manage MCP servers — enable, disable, reconnect, view status",
@@ -117,13 +128,21 @@ function usageLines(profile) {
117
128
  }
118
129
  return lines;
119
130
  }
120
- const INIT_PROMPT = `Analyze this codebase and create an AGENTS.md file containing:
121
- 1. A short overview of what the project does
122
- 2. Build, run, lint and test commands
123
- 3. Architecture and code structure (key directories and what lives in them)
124
- 4. Code style conventions used in this repo (imports, formatting, naming, error handling)
131
+ function buildInitPrompt(agentsPath) {
132
+ return `Analyze this codebase and write a concise AGENTS.md that reduces cold-start for future coding sessions. Create or update the file at exactly this path:
133
+
134
+ ${agentsPath}
125
135
 
126
- If an AGENTS.md already exists, improve it. Keep it under ~60 lines so it is cheap to include in future prompts.`;
136
+ Investigate first read the directory layout, key config files, and a few representative source files — then write. Keep it under ~150 lines so it stays cheap to include in every future prompt. Cover, briefly:
137
+ 1. What the project does (1-2 lines) and its main tech stack.
138
+ 2. Project structure — the key directories/files and what each is responsible for.
139
+ 3. Architecture — how the main pieces fit together (entry points, data/control flow).
140
+ 4. Business-logic / domain mapping — where the core domain concepts live in the code.
141
+ 5. Notable code patterns and conventions to follow (imports, naming, error handling, tests).
142
+ 6. The common build, run, lint and test commands.
143
+
144
+ Favor durable facts over volatile detail. If an AGENTS.md already exists at that path, refine it rather than rewriting from scratch.`;
145
+ }
127
146
  // Ported from the Orbital extension's commit slash command (commitCommandResponse).
128
147
  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.
129
148
 
@@ -163,6 +182,44 @@ Instructions:
163
182
  5. Finish with a short summary: all findings ordered by severity, and an overall verdict on whether the changes are safe to merge.
164
183
 
165
184
  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}` : ""}`;
185
+ const MAX_TASK_CONVERSATION_CHARS = 8000;
186
+ function extractConversation(session) {
187
+ const lines = [];
188
+ for (const message of session.messages) {
189
+ if (message.role === "user" && typeof message.content === "string") {
190
+ const match = /<user_query>\n?([\s\S]*?)\n?<\/user_query>/.exec(message.content);
191
+ if (match)
192
+ lines.push(`User: ${match[1]}`);
193
+ }
194
+ else if (message.role === "assistant" &&
195
+ typeof message.content === "string" &&
196
+ message.content.trim()) {
197
+ lines.push(`Assistant: ${message.content}`);
198
+ }
199
+ }
200
+ return lines.join("\n\n");
201
+ }
202
+ function buildTaskReferencePrompt(session) {
203
+ const conversation = extractConversation(session);
204
+ const truncated = conversation.length > MAX_TASK_CONVERSATION_CHARS
205
+ ? conversation.slice(0, MAX_TASK_CONVERSATION_CHARS) +
206
+ "\n\n[... conversation truncated ...]"
207
+ : conversation;
208
+ return `The user has referenced a previous task. Here is the conversation from that task:
209
+
210
+ <previous_task title="${session.title || session.id}">
211
+ ${truncated}
212
+ </previous_task>
213
+
214
+ Please summarize this previous task and add it as a reference for the current task. The summary should capture:
215
+ - What the task was about
216
+ - What was accomplished
217
+ - Key decisions made
218
+ - Files that were created or modified
219
+ - Any remaining work
220
+
221
+ Present the summary in a clear, organized format. This summary will serve as context for the current task.`;
222
+ }
166
223
  let rowCounter = 0;
167
224
  function rowId() {
168
225
  return `row-${rowCounter++}`;
@@ -195,6 +252,10 @@ export function App({ initialView, initialPrompt, initialSession, systemPromptOv
195
252
  const [queuedMessages, setQueuedMessages] = useState([]);
196
253
  const [modelPickerOpen, setModelPickerOpen] = useState(false);
197
254
  const [resumableSessions, setResumableSessions] = useState(null);
255
+ const [taskPickerSessions, setTaskPickerSessions] = useState(null);
256
+ const [linkManagerOpen, setLinkManagerOpen] = useState(false);
257
+ const [links, setLinks] = useState([]);
258
+ const [linkStatus, setLinkStatus] = useState("");
198
259
  // MCP manager (created once, shared across agents in this process). Null until
199
260
  // the first agent is created so we don't spawn servers before login.
200
261
  const mcpManagerRef = useRef(null);
@@ -326,6 +387,17 @@ export function App({ initialView, initialPrompt, initialSession, systemPromptOv
326
387
  setStreamingText("");
327
388
  setBusyLabel("Working");
328
389
  break;
390
+ case "stream-reset":
391
+ // An auto-retry is re-streaming this step from scratch — drop the
392
+ // partial text/reasoning shown for the failed attempt so it doesn't
393
+ // duplicate. (Committed rows are untouched; the agent only resets when
394
+ // nothing has been committed yet.)
395
+ textBufferRef.current = "";
396
+ setStreamingText("");
397
+ reasoningBufferRef.current = "";
398
+ setStreamingReasoning("");
399
+ setBusyLabel("Working");
400
+ break;
329
401
  case "tool-start":
330
402
  setBusyLabel("Working");
331
403
  break;
@@ -467,6 +539,16 @@ export function App({ initialView, initialPrompt, initialSession, systemPromptOv
467
539
  text: `Resumed session: ${session.title || session.id}`,
468
540
  });
469
541
  }, [createAgent, pushRow, resetTranscript]);
542
+ const handleTaskSelect = useCallback((session) => {
543
+ setTaskPickerSessions(null);
544
+ pushRow({
545
+ kind: "user",
546
+ text: `/task (referencing: ${session.title || session.id})`,
547
+ });
548
+ setBusy(true);
549
+ setBusyLabel("Thinking");
550
+ void getAgent().runTurn(buildTaskReferencePrompt(session));
551
+ }, [getAgent, pushRow]);
470
552
  const switchModel = useCallback((modelId) => {
471
553
  const updated = { ...loadSettings(), model: modelId };
472
554
  setSettings(updated);
@@ -589,6 +671,22 @@ export function App({ initialView, initialPrompt, initialSession, systemPromptOv
589
671
  setResumableSessions(sessions);
590
672
  break;
591
673
  }
674
+ case "/task": {
675
+ if (!getAuthToken(settings)) {
676
+ setView("login");
677
+ break;
678
+ }
679
+ const taskSessions = listSessions(process.cwd()).filter((s) => s.id !== agentRef.current?.taskId);
680
+ if (taskSessions.length === 0) {
681
+ pushRow({
682
+ kind: "info",
683
+ text: "No previous tasks found for this directory.",
684
+ });
685
+ break;
686
+ }
687
+ setTaskPickerSessions(taskSessions);
688
+ break;
689
+ }
592
690
  case "/compact":
593
691
  if (!getAuthToken(settings)) {
594
692
  setView("login");
@@ -633,7 +731,7 @@ export function App({ initialView, initialPrompt, initialSession, systemPromptOv
633
731
  }
634
732
  break;
635
733
  }
636
- case "/init":
734
+ case "/init": {
637
735
  if (!getAuthToken(settings)) {
638
736
  setView("login");
639
737
  break;
@@ -641,7 +739,14 @@ export function App({ initialView, initialPrompt, initialSession, systemPromptOv
641
739
  pushRow({ kind: "user", text: "/init" });
642
740
  setBusy(true);
643
741
  setBusyLabel("Thinking");
644
- void getAgent().runTurn(INIT_PROMPT);
742
+ const agentsPath = path.join(resolveProjectDir(process.cwd()), "AGENTS.md");
743
+ void getAgent().runTurn(buildInitPrompt(agentsPath));
744
+ break;
745
+ }
746
+ case "/link":
747
+ setLinks(loadLinks(process.cwd()));
748
+ setLinkStatus("");
749
+ setLinkManagerOpen(true);
645
750
  break;
646
751
  case "/mcp": {
647
752
  const manager = mcpManagerRef.current;
@@ -995,14 +1100,25 @@ export function App({ initialView, initialPrompt, initialSession, systemPromptOv
995
1100
  !modelPickerOpen &&
996
1101
  !mcpPickerOpen &&
997
1102
  !mcpMigrationEntries &&
998
- !resumableSessions;
1103
+ !resumableSessions &&
1104
+ !taskPickerSessions &&
1105
+ !linkManagerOpen;
999
1106
  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
1000
1107
  .replace(/^[-*]\s*\[x\]/i, " ■")
1001
1108
  .replace(/^[-*]\s*\[-\]/, " ◧")
1002
1109
  .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) => {
1003
1110
  setModelPickerOpen(false);
1004
1111
  switchModel(modelId);
1005
- }, 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: () => {
1112
+ }, onCancel: () => setModelPickerOpen(false) }) })), resumableSessions && (_jsx(Box, { marginTop: 1, children: _jsx(SessionPicker, { sessions: resumableSessions, onSelect: handleResume, onCancel: () => setResumableSessions(null) }) })), taskPickerSessions && (_jsx(Box, { marginTop: 1, children: _jsx(SessionPicker, { sessions: taskPickerSessions, title: "Reference a previous task", onSelect: handleTaskSelect, onCancel: () => setTaskPickerSessions(null) }) })), linkManagerOpen && (_jsx(Box, { marginTop: 1, children: _jsx(LinkManager, { links: links, status: linkStatus, onAdd: (input) => {
1113
+ const result = addLink(process.cwd(), input);
1114
+ setLinkStatus(result.message);
1115
+ if (result.ok)
1116
+ setLinks(loadLinks(process.cwd()));
1117
+ }, onRemove: (link) => {
1118
+ removeLink(process.cwd(), link.path);
1119
+ setLinks(loadLinks(process.cwd()));
1120
+ setLinkStatus(`Unlinked ${path.basename(link.path)}`);
1121
+ }, onClose: () => setLinkManagerOpen(false) }) })), 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: () => {
1006
1122
  const m = mcpManagerRef.current;
1007
1123
  if (m)
1008
1124
  saveMcpApproval(process.cwd(), m.getEnabled(), m.getDisabled());
@@ -0,0 +1,65 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useState } from "react";
3
+ import { Box, Text, useInput } from "ink";
4
+ import { COLORS } from "../../branding.js";
5
+ /**
6
+ * Interactive manager for the `/link` command. The last row is a free-text
7
+ * input where the user types a folder path to add; existing links above it can
8
+ * be selected and removed. Mirrors FollowupPrompt's "input is the final virtual
9
+ * row" pattern.
10
+ */
11
+ export function LinkManager({ links, status, onAdd, onRemove, onClose }) {
12
+ const [selected, setSelected] = useState(links.length);
13
+ const [draft, setDraft] = useState("");
14
+ const inputRow = links.length; // the free-text row sits after every link
15
+ const rowCount = links.length + 1;
16
+ const sel = Math.min(selected, inputRow); // clamp: list shrinks on removal
17
+ const isInput = sel === inputRow;
18
+ useInput((input, key) => {
19
+ if (key.escape) {
20
+ onClose();
21
+ return;
22
+ }
23
+ if (key.upArrow) {
24
+ setSelected((s) => (Math.min(s, inputRow) - 1 + rowCount) % rowCount);
25
+ return;
26
+ }
27
+ if (key.downArrow) {
28
+ setSelected((s) => (Math.min(s, inputRow) + 1) % rowCount);
29
+ return;
30
+ }
31
+ if (key.return) {
32
+ if (isInput) {
33
+ if (draft.trim()) {
34
+ onAdd(draft.trim());
35
+ setDraft("");
36
+ // Stay on the input row so several repos can be added in a row.
37
+ // rowCount == the new input-row index once a link is appended.
38
+ setSelected(rowCount);
39
+ }
40
+ }
41
+ else {
42
+ onRemove(links[sel]);
43
+ setSelected(inputRow); // jump back to the input row after removing
44
+ }
45
+ return;
46
+ }
47
+ if (isInput) {
48
+ if (key.backspace || key.delete)
49
+ setDraft((d) => d.slice(0, -1));
50
+ else if (input && !key.ctrl && !key.meta)
51
+ setDraft((d) => d + input);
52
+ }
53
+ else if (input === "d" || key.backspace || key.delete) {
54
+ onRemove(links[sel]);
55
+ setSelected(inputRow);
56
+ }
57
+ });
58
+ return (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: COLORS.primary, paddingX: 1, children: [_jsx(Text, { bold: true, color: COLORS.primary, children: "Linked repositories" }), _jsx(Text, { dimColor: true, children: "Repos linked here are shared with the agent so changes can be checked across them." }), links.length === 0 && _jsx(Text, { dimColor: true, children: " (none yet)" }), links.map((link, index) => (
59
+ // truncate-start keeps the meaningful tail of long paths visible
60
+ // instead of wrapping them onto the next line.
61
+ _jsxs(Text, { color: sel === index ? COLORS.accent : undefined, wrap: "truncate-start", children: [sel === index ? "❯ " : " ", index + 1, ". ", link.path] }, link.path))), _jsxs(Text, { color: isInput ? COLORS.accent : undefined, children: [isInput ? "❯ " : " ", "add a repo (folder path):"] }), isInput && (
62
+ // The typed/pasted path lives on its own line and truncates from
63
+ // the start, so a long path shows its tail + cursor without wrapping.
64
+ _jsxs(Text, { wrap: "truncate-start", children: [" ", draft, _jsx(Text, { inverse: true, children: " " })] })), status && _jsx(Text, { dimColor: true, children: status }), _jsx(Text, { dimColor: true, children: "\u2191/\u2193 select \u00B7 enter add/remove \u00B7 d remove \u00B7 esc done" })] }));
65
+ }
@@ -16,7 +16,7 @@ function relativeTime(iso) {
16
16
  const days = Math.round(hours / 24);
17
17
  return `${days}d ago`;
18
18
  }
19
- export function SessionPicker({ sessions, onSelect, onCancel }) {
19
+ export function SessionPicker({ sessions, onSelect, onCancel, title = "Resume a previous session" }) {
20
20
  const [selected, setSelected] = useState(0);
21
21
  useInput((input, key) => {
22
22
  if (key.upArrow) {
@@ -43,7 +43,7 @@ export function SessionPicker({ sessions, onSelect, onCancel }) {
43
43
  });
44
44
  const windowStart = Math.max(0, Math.min(selected - VISIBLE_ROWS + 1, sessions.length - VISIBLE_ROWS));
45
45
  const visible = sessions.slice(windowStart, windowStart + VISIBLE_ROWS);
46
- return (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: COLORS.primary, paddingX: 1, children: [_jsx(Text, { bold: true, color: COLORS.primary, children: "Resume a previous session" }), windowStart > 0 && _jsxs(Text, { dimColor: true, children: [" \u2191 ", windowStart, " more"] }), visible.map((session, i) => {
46
+ return (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: COLORS.primary, paddingX: 1, children: [_jsx(Text, { bold: true, color: COLORS.primary, children: title }), windowStart > 0 && _jsxs(Text, { dimColor: true, children: [" \u2191 ", windowStart, " more"] }), visible.map((session, i) => {
47
47
  const index = windowStart + i;
48
48
  const isSelected = index === selected;
49
49
  const userTurns = session.messages.filter((m) => m.role === "user").length;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@matterailab/orbcode",
3
- "version": "0.3.2",
3
+ "version": "0.3.4",
4
4
  "description": "OrbCode CLI — agentic coding in your terminal, powered by Axon models by MatterAI",
5
5
  "type": "module",
6
6
  "bin": {