@matterailab/orbcode 0.3.3 → 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/dist/core/agent.js +125 -8
- package/dist/prompts/system.js +41 -168
- package/dist/ui/App.js +82 -1
- package/dist/ui/components/SessionPicker.js +2 -2
- package/package.json +1 -1
package/dist/core/agent.js
CHANGED
|
@@ -14,6 +14,42 @@ import { loadSkills } from "../skills/loader.js";
|
|
|
14
14
|
import { renderLinkedReposSection } from "../config/links.js";
|
|
15
15
|
const MAX_STEPS_PER_TURN = 50;
|
|
16
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
|
+
}
|
|
17
53
|
function detectRepo(cwd) {
|
|
18
54
|
try {
|
|
19
55
|
const remote = execSync("git config --get remote.origin.url", {
|
|
@@ -422,7 +458,13 @@ User time zone: ${timeZone}, UTC${timeZoneOffsetStr}`;
|
|
|
422
458
|
},
|
|
423
459
|
];
|
|
424
460
|
let summary = "";
|
|
425
|
-
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
|
+
})) {
|
|
426
468
|
if (signal.aborted)
|
|
427
469
|
throw new DOMException("aborted", "AbortError");
|
|
428
470
|
if (chunk.type === "text") {
|
|
@@ -468,28 +510,102 @@ User time zone: ${timeZone}, UTC${timeZoneOffsetStr}`;
|
|
|
468
510
|
onEvent({ type: "turn-end" });
|
|
469
511
|
}
|
|
470
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
|
+
}
|
|
471
554
|
/** Run one model request + tool execution round. Returns true when the turn is over. */
|
|
472
555
|
async runStep() {
|
|
473
556
|
const { onEvent } = this.options.callbacks;
|
|
474
557
|
const signal = this.abortController.signal;
|
|
475
558
|
let assistantText = "";
|
|
476
|
-
|
|
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;
|
|
477
565
|
let reasoningStart = 0;
|
|
478
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
|
+
};
|
|
479
577
|
const toolCallsByIndex = new Map();
|
|
480
578
|
let nextSyntheticIndex = 10000;
|
|
481
|
-
|
|
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);
|
|
482
596
|
for await (const chunk of stream) {
|
|
483
597
|
if (signal.aborted)
|
|
484
598
|
throw new DOMException("aborted", "AbortError");
|
|
485
599
|
switch (chunk.type) {
|
|
486
600
|
case "text":
|
|
601
|
+
// Visible content begins — the reasoning phase (if any) is over.
|
|
602
|
+
finalizeReasoning();
|
|
487
603
|
assistantText += chunk.text;
|
|
488
604
|
onEvent({ type: "text-delta", text: chunk.text });
|
|
489
605
|
break;
|
|
490
606
|
case "reasoning":
|
|
491
|
-
if (!
|
|
492
|
-
|
|
607
|
+
if (!reasoningOpen) {
|
|
608
|
+
reasoningOpen = true;
|
|
493
609
|
reasoningStart = Date.now();
|
|
494
610
|
}
|
|
495
611
|
onEvent({ type: "reasoning-delta", text: chunk.text });
|
|
@@ -499,6 +615,8 @@ User time zone: ${timeZone}, UTC${timeZoneOffsetStr}`;
|
|
|
499
615
|
reasoningDetails = chunk.details;
|
|
500
616
|
break;
|
|
501
617
|
case "native_tool_calls":
|
|
618
|
+
// A tool call also ends the reasoning phase.
|
|
619
|
+
finalizeReasoning();
|
|
502
620
|
for (const tc of chunk.toolCalls) {
|
|
503
621
|
const index = tc.index ?? nextSyntheticIndex++;
|
|
504
622
|
let pending = toolCallsByIndex.get(index);
|
|
@@ -527,9 +645,8 @@ User time zone: ${timeZone}, UTC${timeZoneOffsetStr}`;
|
|
|
527
645
|
break;
|
|
528
646
|
}
|
|
529
647
|
}
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
}
|
|
648
|
+
// A reasoning-only turn (no following text/tool content) still needs closing.
|
|
649
|
+
finalizeReasoning();
|
|
533
650
|
if (assistantText) {
|
|
534
651
|
onEvent({ type: "text-done" });
|
|
535
652
|
}
|
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
|
@@ -40,6 +40,10 @@ const SLASH_COMMANDS = [
|
|
|
40
40
|
description: "summarize the conversation to free up context",
|
|
41
41
|
},
|
|
42
42
|
{ name: "/tasks", description: "show the current task list" },
|
|
43
|
+
{
|
|
44
|
+
name: "/task",
|
|
45
|
+
description: "reference a previous task in the current conversation",
|
|
46
|
+
},
|
|
43
47
|
{
|
|
44
48
|
name: "/status",
|
|
45
49
|
description: "show session status (model, context, cost, account)",
|
|
@@ -178,6 +182,44 @@ Instructions:
|
|
|
178
182
|
5. Finish with a short summary: all findings ordered by severity, and an overall verdict on whether the changes are safe to merge.
|
|
179
183
|
|
|
180
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
|
+
}
|
|
181
223
|
let rowCounter = 0;
|
|
182
224
|
function rowId() {
|
|
183
225
|
return `row-${rowCounter++}`;
|
|
@@ -210,6 +252,7 @@ export function App({ initialView, initialPrompt, initialSession, systemPromptOv
|
|
|
210
252
|
const [queuedMessages, setQueuedMessages] = useState([]);
|
|
211
253
|
const [modelPickerOpen, setModelPickerOpen] = useState(false);
|
|
212
254
|
const [resumableSessions, setResumableSessions] = useState(null);
|
|
255
|
+
const [taskPickerSessions, setTaskPickerSessions] = useState(null);
|
|
213
256
|
const [linkManagerOpen, setLinkManagerOpen] = useState(false);
|
|
214
257
|
const [links, setLinks] = useState([]);
|
|
215
258
|
const [linkStatus, setLinkStatus] = useState("");
|
|
@@ -344,6 +387,17 @@ export function App({ initialView, initialPrompt, initialSession, systemPromptOv
|
|
|
344
387
|
setStreamingText("");
|
|
345
388
|
setBusyLabel("Working");
|
|
346
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;
|
|
347
401
|
case "tool-start":
|
|
348
402
|
setBusyLabel("Working");
|
|
349
403
|
break;
|
|
@@ -485,6 +539,16 @@ export function App({ initialView, initialPrompt, initialSession, systemPromptOv
|
|
|
485
539
|
text: `Resumed session: ${session.title || session.id}`,
|
|
486
540
|
});
|
|
487
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]);
|
|
488
552
|
const switchModel = useCallback((modelId) => {
|
|
489
553
|
const updated = { ...loadSettings(), model: modelId };
|
|
490
554
|
setSettings(updated);
|
|
@@ -607,6 +671,22 @@ export function App({ initialView, initialPrompt, initialSession, systemPromptOv
|
|
|
607
671
|
setResumableSessions(sessions);
|
|
608
672
|
break;
|
|
609
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
|
+
}
|
|
610
690
|
case "/compact":
|
|
611
691
|
if (!getAuthToken(settings)) {
|
|
612
692
|
setView("login");
|
|
@@ -1021,6 +1101,7 @@ export function App({ initialView, initialPrompt, initialSession, systemPromptOv
|
|
|
1021
1101
|
!mcpPickerOpen &&
|
|
1022
1102
|
!mcpMigrationEntries &&
|
|
1023
1103
|
!resumableSessions &&
|
|
1104
|
+
!taskPickerSessions &&
|
|
1024
1105
|
!linkManagerOpen;
|
|
1025
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
|
|
1026
1107
|
.replace(/^[-*]\s*\[x\]/i, " ■")
|
|
@@ -1028,7 +1109,7 @@ export function App({ initialView, initialPrompt, initialSession, systemPromptOv
|
|
|
1028
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) => {
|
|
1029
1110
|
setModelPickerOpen(false);
|
|
1030
1111
|
switchModel(modelId);
|
|
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) => {
|
|
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) => {
|
|
1032
1113
|
const result = addLink(process.cwd(), input);
|
|
1033
1114
|
setLinkStatus(result.message);
|
|
1034
1115
|
if (result.ok)
|
|
@@ -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;
|