@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 +2 -1
- package/dist/branding.js +5 -5
- package/dist/config/links.js +163 -0
- package/dist/core/agent.js +128 -9
- package/dist/memory/loader.js +5 -3
- package/dist/prompts/system.js +41 -168
- package/dist/ui/App.js +126 -10
- package/dist/ui/components/LinkManager.js +65 -0
- package/dist/ui/components/SessionPicker.js +2 -2
- package/package.json +1 -1
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
|
+
}
|
package/dist/core/agent.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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 (!
|
|
490
|
-
|
|
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
|
-
|
|
529
|
-
|
|
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
|
}
|
package/dist/memory/loader.js
CHANGED
|
@@ -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 .
|
|
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 + .
|
|
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
|
package/dist/prompts/system.js
CHANGED
|
@@ -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
|
-
#
|
|
35
|
+
# Gather Enough Context, Then Act
|
|
36
36
|
|
|
37
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
###
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
###
|
|
156
|
+
### Reading Strategy
|
|
209
157
|
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
###
|
|
216
|
+
### Search Hygiene
|
|
314
217
|
|
|
315
|
-
|
|
316
|
-
- \`
|
|
317
|
-
-
|
|
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
|
-
##
|
|
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
|
-
|
|
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
|
-
-
|
|
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
|
-
-
|
|
370
|
-
-
|
|
371
|
-
-
|
|
372
|
-
-
|
|
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
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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) }) })),
|
|
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:
|
|
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;
|