@matterailab/orbcode 0.3.1 → 0.3.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -1
- package/dist/api/client.js +26 -10
- package/dist/branding.js +5 -5
- package/dist/config/links.js +163 -0
- package/dist/core/agent.js +3 -1
- package/dist/memory/loader.js +5 -3
- package/dist/ui/App.js +45 -10
- package/dist/ui/components/LinkManager.js +65 -0
- 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/api/client.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import OpenAI from "openai";
|
|
2
|
-
import { API_GATEWAY_PATH
|
|
3
|
-
import { DEFAULT_HEADERS, X_AXONCODE_TASKID, X_AXON_REPO, X_ORGANIZATIONID } from "./headers.js";
|
|
2
|
+
import { API_GATEWAY_PATH } from "../auth/auth.js";
|
|
3
|
+
import { DEFAULT_HEADERS, X_AXONCODE_TASKID, X_AXON_REPO, X_ORGANIZATIONID, } from "./headers.js";
|
|
4
4
|
import { stripReasoningDetails } from "./llmClient.js";
|
|
5
5
|
import { getModel } from "./models.js";
|
|
6
6
|
export class AxonClient {
|
|
@@ -9,13 +9,20 @@ export class AxonClient {
|
|
|
9
9
|
constructor(options) {
|
|
10
10
|
this.options = options;
|
|
11
11
|
this.client = new OpenAI({
|
|
12
|
-
|
|
12
|
+
// Use the gateway URL directly. Do not rehost it through
|
|
13
|
+
// getUrlFromToken — that helper rewrites any api.matterai.so host
|
|
14
|
+
// onto the control plane, which would send inference to
|
|
15
|
+
// api.matterai.so instead of the gateway at api2.matterai.so.
|
|
16
|
+
// Per-model `baseUrl` overrides (e.g. local dev) still win.
|
|
17
|
+
baseURL: options.baseUrl || API_GATEWAY_PATH,
|
|
13
18
|
apiKey: options.token,
|
|
14
19
|
defaultHeaders: DEFAULT_HEADERS,
|
|
15
20
|
});
|
|
16
21
|
}
|
|
17
22
|
requestHeaders() {
|
|
18
|
-
const headers = {
|
|
23
|
+
const headers = {
|
|
24
|
+
[X_AXONCODE_TASKID]: this.options.taskId,
|
|
25
|
+
};
|
|
19
26
|
if (this.options.organizationId)
|
|
20
27
|
headers[X_ORGANIZATIONID] = this.options.organizationId;
|
|
21
28
|
if (this.options.repo)
|
|
@@ -27,7 +34,10 @@ export class AxonClient {
|
|
|
27
34
|
const requestOptions = {
|
|
28
35
|
model: model.id,
|
|
29
36
|
temperature: 0.2,
|
|
30
|
-
messages: [
|
|
37
|
+
messages: [
|
|
38
|
+
{ role: "system", content: systemPrompt },
|
|
39
|
+
...stripReasoningDetails(messages),
|
|
40
|
+
],
|
|
31
41
|
stream: true,
|
|
32
42
|
stream_options: { include_usage: true },
|
|
33
43
|
max_tokens: model.maxOutputTokens,
|
|
@@ -48,12 +58,14 @@ export class AxonClient {
|
|
|
48
58
|
for await (const chunk of stream) {
|
|
49
59
|
// The gateway may return an error object instead of throwing.
|
|
50
60
|
if ("error" in chunk) {
|
|
51
|
-
const error = chunk
|
|
61
|
+
const error = chunk
|
|
62
|
+
.error;
|
|
52
63
|
const err = new Error(`Axon API Error ${error?.code ?? ""}: ${error?.message ?? "unknown error"}`);
|
|
53
64
|
err.status = error?.code;
|
|
54
65
|
throw err;
|
|
55
66
|
}
|
|
56
|
-
if ("provider" in chunk &&
|
|
67
|
+
if ("provider" in chunk &&
|
|
68
|
+
typeof chunk.provider === "string") {
|
|
57
69
|
inferenceProvider = chunk.provider;
|
|
58
70
|
}
|
|
59
71
|
if (chunk.usage) {
|
|
@@ -74,7 +86,9 @@ export class AxonClient {
|
|
|
74
86
|
if (newText.includes("<think>")) {
|
|
75
87
|
isThinking = true;
|
|
76
88
|
}
|
|
77
|
-
if (newText.includes("<think>") ||
|
|
89
|
+
if (newText.includes("<think>") ||
|
|
90
|
+
newText.includes("</think>") ||
|
|
91
|
+
isThinking) {
|
|
78
92
|
if (newText.includes("</think>")) {
|
|
79
93
|
isThinking = false;
|
|
80
94
|
}
|
|
@@ -90,7 +104,8 @@ export class AxonClient {
|
|
|
90
104
|
if (typeof deltaRecord.reasoning === "string" && deltaRecord.reasoning) {
|
|
91
105
|
yield { type: "reasoning", text: deltaRecord.reasoning };
|
|
92
106
|
}
|
|
93
|
-
if (typeof deltaRecord.reasoning_content === "string" &&
|
|
107
|
+
if (typeof deltaRecord.reasoning_content === "string" &&
|
|
108
|
+
deltaRecord.reasoning_content) {
|
|
94
109
|
yield { type: "reasoning", text: deltaRecord.reasoning_content };
|
|
95
110
|
}
|
|
96
111
|
if (delta.tool_calls && delta.tool_calls.length > 0) {
|
|
@@ -101,7 +116,8 @@ export class AxonClient {
|
|
|
101
116
|
// index + argument fragments. Drop pure placeholders.
|
|
102
117
|
const hasValidId = tc.id !== null && tc.id !== undefined;
|
|
103
118
|
const hasValidName = !!tc.function.name;
|
|
104
|
-
const hasArguments = typeof tc.function.arguments === "string" &&
|
|
119
|
+
const hasArguments = typeof tc.function.arguments === "string" &&
|
|
120
|
+
tc.function.arguments.length > 0;
|
|
105
121
|
return hasValidId || hasValidName || hasArguments;
|
|
106
122
|
})
|
|
107
123
|
.map((tc) => ({
|
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,6 +11,7 @@ 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;
|
|
16
17
|
function detectRepo(cwd) {
|
|
@@ -219,6 +220,7 @@ export class Agent {
|
|
|
219
220
|
const timeZoneOffsetHours = Math.floor(Math.abs(timeZoneOffset));
|
|
220
221
|
const timeZoneOffsetMinutes = Math.abs(Math.round((Math.abs(timeZoneOffset) - timeZoneOffsetHours) * 60));
|
|
221
222
|
const timeZoneOffsetStr = `${timeZoneOffset >= 0 ? "+" : "-"}${timeZoneOffsetHours}:${timeZoneOffsetMinutes.toString().padStart(2, "0")}`;
|
|
223
|
+
const linkedRepos = renderLinkedReposSection(this.options.cwd);
|
|
222
224
|
return `# Environment Details
|
|
223
225
|
|
|
224
226
|
## Current Workspace Directory (${this.options.cwd}) Files
|
|
@@ -226,7 +228,7 @@ ${files.join("\n") || "(empty directory)"}
|
|
|
226
228
|
${files.length >= 200 ? "\n(File list truncated.)" : ""}
|
|
227
229
|
|
|
228
230
|
${git}
|
|
229
|
-
|
|
231
|
+
${linkedRepos ? `\n${linkedRepos}` : ""}
|
|
230
232
|
## Current Time
|
|
231
233
|
Current time in ISO 8601 UTC format: ${now.toISOString()}
|
|
232
234
|
User time zone: ${timeZone}, UTC${timeZoneOffsetStr}`;
|
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/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" },
|
|
@@ -46,6 +49,10 @@ const SLASH_COMMANDS = [
|
|
|
46
49
|
name: "/init",
|
|
47
50
|
description: "analyze this codebase and create an AGENTS.md",
|
|
48
51
|
},
|
|
52
|
+
{
|
|
53
|
+
name: "/link",
|
|
54
|
+
description: "link other repos so changes here are checked against them",
|
|
55
|
+
},
|
|
49
56
|
{
|
|
50
57
|
name: "/mcp",
|
|
51
58
|
description: "manage MCP servers — enable, disable, reconnect, view status",
|
|
@@ -117,13 +124,21 @@ function usageLines(profile) {
|
|
|
117
124
|
}
|
|
118
125
|
return lines;
|
|
119
126
|
}
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
127
|
+
function buildInitPrompt(agentsPath) {
|
|
128
|
+
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:
|
|
129
|
+
|
|
130
|
+
${agentsPath}
|
|
131
|
+
|
|
132
|
+
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:
|
|
133
|
+
1. What the project does (1-2 lines) and its main tech stack.
|
|
134
|
+
2. Project structure — the key directories/files and what each is responsible for.
|
|
135
|
+
3. Architecture — how the main pieces fit together (entry points, data/control flow).
|
|
136
|
+
4. Business-logic / domain mapping — where the core domain concepts live in the code.
|
|
137
|
+
5. Notable code patterns and conventions to follow (imports, naming, error handling, tests).
|
|
138
|
+
6. The common build, run, lint and test commands.
|
|
125
139
|
|
|
126
|
-
If an AGENTS.md already exists
|
|
140
|
+
Favor durable facts over volatile detail. If an AGENTS.md already exists at that path, refine it rather than rewriting from scratch.`;
|
|
141
|
+
}
|
|
127
142
|
// Ported from the Orbital extension's commit slash command (commitCommandResponse).
|
|
128
143
|
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
144
|
|
|
@@ -195,6 +210,9 @@ export function App({ initialView, initialPrompt, initialSession, systemPromptOv
|
|
|
195
210
|
const [queuedMessages, setQueuedMessages] = useState([]);
|
|
196
211
|
const [modelPickerOpen, setModelPickerOpen] = useState(false);
|
|
197
212
|
const [resumableSessions, setResumableSessions] = useState(null);
|
|
213
|
+
const [linkManagerOpen, setLinkManagerOpen] = useState(false);
|
|
214
|
+
const [links, setLinks] = useState([]);
|
|
215
|
+
const [linkStatus, setLinkStatus] = useState("");
|
|
198
216
|
// MCP manager (created once, shared across agents in this process). Null until
|
|
199
217
|
// the first agent is created so we don't spawn servers before login.
|
|
200
218
|
const mcpManagerRef = useRef(null);
|
|
@@ -633,7 +651,7 @@ export function App({ initialView, initialPrompt, initialSession, systemPromptOv
|
|
|
633
651
|
}
|
|
634
652
|
break;
|
|
635
653
|
}
|
|
636
|
-
case "/init":
|
|
654
|
+
case "/init": {
|
|
637
655
|
if (!getAuthToken(settings)) {
|
|
638
656
|
setView("login");
|
|
639
657
|
break;
|
|
@@ -641,7 +659,14 @@ export function App({ initialView, initialPrompt, initialSession, systemPromptOv
|
|
|
641
659
|
pushRow({ kind: "user", text: "/init" });
|
|
642
660
|
setBusy(true);
|
|
643
661
|
setBusyLabel("Thinking");
|
|
644
|
-
|
|
662
|
+
const agentsPath = path.join(resolveProjectDir(process.cwd()), "AGENTS.md");
|
|
663
|
+
void getAgent().runTurn(buildInitPrompt(agentsPath));
|
|
664
|
+
break;
|
|
665
|
+
}
|
|
666
|
+
case "/link":
|
|
667
|
+
setLinks(loadLinks(process.cwd()));
|
|
668
|
+
setLinkStatus("");
|
|
669
|
+
setLinkManagerOpen(true);
|
|
645
670
|
break;
|
|
646
671
|
case "/mcp": {
|
|
647
672
|
const manager = mcpManagerRef.current;
|
|
@@ -995,14 +1020,24 @@ export function App({ initialView, initialPrompt, initialSession, systemPromptOv
|
|
|
995
1020
|
!modelPickerOpen &&
|
|
996
1021
|
!mcpPickerOpen &&
|
|
997
1022
|
!mcpMigrationEntries &&
|
|
998
|
-
!resumableSessions
|
|
1023
|
+
!resumableSessions &&
|
|
1024
|
+
!linkManagerOpen;
|
|
999
1025
|
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
1026
|
.replace(/^[-*]\s*\[x\]/i, " ■")
|
|
1001
1027
|
.replace(/^[-*]\s*\[-\]/, " ◧")
|
|
1002
1028
|
.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
1029
|
setModelPickerOpen(false);
|
|
1004
1030
|
switchModel(modelId);
|
|
1005
|
-
}, onCancel: () => setModelPickerOpen(false) }) })), resumableSessions && (_jsx(Box, { marginTop: 1, children: _jsx(SessionPicker, { sessions: resumableSessions, onSelect: handleResume, onCancel: () => setResumableSessions(null) }) })),
|
|
1031
|
+
}, onCancel: () => setModelPickerOpen(false) }) })), resumableSessions && (_jsx(Box, { marginTop: 1, children: _jsx(SessionPicker, { sessions: resumableSessions, onSelect: handleResume, onCancel: () => setResumableSessions(null) }) })), linkManagerOpen && (_jsx(Box, { marginTop: 1, children: _jsx(LinkManager, { links: links, status: linkStatus, onAdd: (input) => {
|
|
1032
|
+
const result = addLink(process.cwd(), input);
|
|
1033
|
+
setLinkStatus(result.message);
|
|
1034
|
+
if (result.ok)
|
|
1035
|
+
setLinks(loadLinks(process.cwd()));
|
|
1036
|
+
}, onRemove: (link) => {
|
|
1037
|
+
removeLink(process.cwd(), link.path);
|
|
1038
|
+
setLinks(loadLinks(process.cwd()));
|
|
1039
|
+
setLinkStatus(`Unlinked ${path.basename(link.path)}`);
|
|
1040
|
+
}, 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
1041
|
const m = mcpManagerRef.current;
|
|
1007
1042
|
if (m)
|
|
1008
1043
|
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
|
+
}
|