@oh-my-pi/pi-coding-agent 16.2.4 → 16.2.6
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/CHANGELOG.md +37 -0
- package/dist/cli.js +3211 -3231
- package/dist/types/cli/bench-cli.d.ts +6 -1
- package/dist/types/commands/bench.d.ts +8 -0
- package/dist/types/config/model-discovery.d.ts +6 -1
- package/dist/types/config/settings-schema.d.ts +1 -1
- package/dist/types/edit/index.d.ts +1 -0
- package/dist/types/edit/renderer.d.ts +4 -0
- package/dist/types/edit/snapshot-details.d.ts +33 -0
- package/dist/types/mcp/oauth-flow.d.ts +4 -6
- package/dist/types/modes/components/status-line/types.d.ts +10 -0
- package/dist/types/modes/controllers/tool-args-reveal.d.ts +9 -2
- package/dist/types/modes/theme/theme.d.ts +2 -1
- package/dist/types/session/agent-session.d.ts +11 -0
- package/dist/types/session/session-manager.d.ts +3 -4
- package/dist/types/task/provider-concurrency.d.ts +40 -0
- package/dist/types/utils/git.d.ts +11 -0
- package/dist/types/web/search/providers/duckduckgo.d.ts +2 -2
- package/dist/types/web/search/types.d.ts +1 -1
- package/package.json +12 -12
- package/src/cli/args.ts +32 -1
- package/src/cli/bench-cli.ts +89 -21
- package/src/cli/web-search-cli.ts +6 -1
- package/src/cli/worktree-cli.ts +8 -5
- package/src/commands/bench.ts +10 -1
- package/src/config/mcp-schema.json +1 -1
- package/src/config/model-discovery.ts +98 -20
- package/src/config/model-registry.ts +13 -6
- package/src/config/settings-schema.ts +5 -2
- package/src/edit/hashline/execute.ts +15 -9
- package/src/edit/index.ts +19 -6
- package/src/edit/modes/patch.ts +3 -2
- package/src/edit/modes/replace.ts +3 -2
- package/src/edit/renderer.ts +4 -0
- package/src/edit/snapshot-details.ts +77 -0
- package/src/extensibility/plugins/legacy-pi-compat.ts +2 -2
- package/src/internal-urls/docs-index.generated.txt +1 -1
- package/src/mcp/oauth-flow.ts +10 -8
- package/src/mcp/transports/stdio.ts +9 -17
- package/src/modes/components/status-line/component.ts +29 -2
- package/src/modes/components/status-line/segments.ts +22 -8
- package/src/modes/components/status-line/types.ts +7 -0
- package/src/modes/controllers/event-controller.ts +17 -8
- package/src/modes/controllers/input-controller.ts +1 -1
- package/src/modes/controllers/tool-args-reveal.ts +100 -22
- package/src/modes/theme/theme.ts +6 -0
- package/src/prompts/bench.md +4 -10
- package/src/prompts/tools/irc.md +19 -29
- package/src/prompts/tools/job.md +8 -14
- package/src/prompts/tools/lsp.md +19 -30
- package/src/prompts/tools/task.md +42 -62
- package/src/sdk.ts +14 -5
- package/src/session/agent-session.ts +107 -13
- package/src/session/session-listing.ts +9 -8
- package/src/session/session-loader.ts +98 -3
- package/src/session/session-manager.ts +34 -4
- package/src/subprocess/worker-client.ts +12 -4
- package/src/task/executor.ts +4 -62
- package/src/task/provider-concurrency.ts +100 -0
- package/src/task/worktree.ts +13 -4
- package/src/task/yield-assembly.ts +27 -39
- package/src/tools/path-utils.ts +4 -2
- package/src/tools/read.ts +11 -1
- package/src/utils/git.ts +13 -0
- package/src/web/search/index.ts +14 -8
- package/src/web/search/providers/duckduckgo.ts +136 -78
- package/src/web/search/providers/gemini.ts +268 -185
- package/src/web/search/types.ts +1 -1
package/src/mcp/oauth-flow.ts
CHANGED
|
@@ -75,6 +75,10 @@ export interface MCPStoredOAuthCredential extends OAuthCredential {
|
|
|
75
75
|
const DEFAULT_PORT = 3000;
|
|
76
76
|
const CALLBACK_PATH = "/callback";
|
|
77
77
|
|
|
78
|
+
function hasOAuthScope(scopes: string | null | undefined, scope: string): boolean {
|
|
79
|
+
return !!scopes && scopes.split(/\s+/).includes(scope);
|
|
80
|
+
}
|
|
81
|
+
|
|
78
82
|
function isLoopbackHostname(hostname: string): boolean {
|
|
79
83
|
return hostname === "localhost" || hostname === "127.0.0.1";
|
|
80
84
|
}
|
|
@@ -225,12 +229,10 @@ export interface MCPOAuthConfig {
|
|
|
225
229
|
/** OAuth scopes (space-separated) */
|
|
226
230
|
scopes?: string;
|
|
227
231
|
/**
|
|
228
|
-
* `prompt` parameter for the authorization request.
|
|
229
|
-
*
|
|
230
|
-
*
|
|
231
|
-
*
|
|
232
|
-
* (RFC 6749 §3.1 requires servers to ignore the param when unsupported).
|
|
233
|
-
* Set to `""` to omit the parameter entirely.
|
|
232
|
+
* `prompt` parameter for the authorization request. By default the parameter
|
|
233
|
+
* is omitted, matching the reference MCP SDK, except for `offline_access`
|
|
234
|
+
* requests where OIDC Core requires `prompt=consent` to issue refresh-token
|
|
235
|
+
* access. Set to `""` to omit the parameter entirely.
|
|
234
236
|
*/
|
|
235
237
|
prompt?: string;
|
|
236
238
|
/** Exact redirect URI to advertise to the provider */
|
|
@@ -325,7 +327,7 @@ export class MCPOAuthFlow extends OAuthCallbackFlow {
|
|
|
325
327
|
if (this.config.scopes && !params.get("scope")) {
|
|
326
328
|
params.set("scope", this.config.scopes);
|
|
327
329
|
}
|
|
328
|
-
const prompt = this.config.prompt ?? "consent";
|
|
330
|
+
const prompt = this.config.prompt ?? (hasOAuthScope(params.get("scope"), "offline_access") ? "consent" : "");
|
|
329
331
|
if (prompt && !params.get("prompt")) {
|
|
330
332
|
params.set("prompt", prompt);
|
|
331
333
|
}
|
|
@@ -494,7 +496,7 @@ export class MCPOAuthFlow extends OAuthCallbackFlow {
|
|
|
494
496
|
Accept: "application/json",
|
|
495
497
|
},
|
|
496
498
|
body: JSON.stringify({
|
|
497
|
-
client_name: "
|
|
499
|
+
client_name: "oh-my-pi",
|
|
498
500
|
redirect_uris: [redirectUri],
|
|
499
501
|
grant_types: ["authorization_code", "refresh_token"],
|
|
500
502
|
response_types: ["code"],
|
|
@@ -147,21 +147,6 @@ function resolveWindowsShimPath(value: string, shimDir: string): string | null {
|
|
|
147
147
|
return path.join(shimDir, ...suffix.split(/[\\/]+/).filter(Boolean));
|
|
148
148
|
}
|
|
149
149
|
|
|
150
|
-
function extractWindowsNpmShimTarget(content: string): string | null {
|
|
151
|
-
const match = /"%_prog%"\s+"([^"]+)"\s+%\*/i.exec(content);
|
|
152
|
-
return match?.[1] ?? null;
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
/**
|
|
156
|
-
* Extract the shim's PATH-fallback interpreter (`SET "_prog=node"`). The
|
|
157
|
-
* `IF EXIST` branch assigns a `%dp0%`-prefixed value, so requiring a
|
|
158
|
-
* non-`%`-leading value picks the bare program name.
|
|
159
|
-
*/
|
|
160
|
-
function extractWindowsNpmShimProg(content: string): string | null {
|
|
161
|
-
const match = /SET\s+"_prog=([^%"][^"]*)"/i.exec(content);
|
|
162
|
-
return match?.[1] ?? null;
|
|
163
|
-
}
|
|
164
|
-
|
|
165
150
|
async function resolveWindowsNpmShimCommand(
|
|
166
151
|
command: string,
|
|
167
152
|
args: readonly string[],
|
|
@@ -171,6 +156,11 @@ async function resolveWindowsNpmShimCommand(
|
|
|
171
156
|
if (!isWindowsBatchCommand(command)) return null;
|
|
172
157
|
if (!hasPathSegment(command)) return null;
|
|
173
158
|
const commandPath = path.resolve(cwd, command);
|
|
159
|
+
const commandName = path
|
|
160
|
+
.basename(commandPath)
|
|
161
|
+
.replace(/\.cmd$/i, "")
|
|
162
|
+
.toLowerCase();
|
|
163
|
+
if (commandName === "npx") return null;
|
|
174
164
|
|
|
175
165
|
let content: string;
|
|
176
166
|
try {
|
|
@@ -181,7 +171,9 @@ async function resolveWindowsNpmShimCommand(
|
|
|
181
171
|
|
|
182
172
|
// cmd-shim emits the same invocation line for every interpreter; only
|
|
183
173
|
// bypass cmd.exe when the shim's fallback interpreter is actually node.
|
|
184
|
-
|
|
174
|
+
// The IF EXIST branch assigns a %dp0%-prefixed value, so requiring a
|
|
175
|
+
// non-%-leading SET value picks the bare PATH-fallback program name.
|
|
176
|
+
const prog = /SET\s+"_prog=([^%"][^"]*)"/i.exec(content)?.[1];
|
|
185
177
|
if (
|
|
186
178
|
!prog ||
|
|
187
179
|
path
|
|
@@ -191,7 +183,7 @@ async function resolveWindowsNpmShimCommand(
|
|
|
191
183
|
)
|
|
192
184
|
return null;
|
|
193
185
|
|
|
194
|
-
const rawTarget =
|
|
186
|
+
const rawTarget = /"%_prog%"\s+"([^"]+)"\s+%\*/i.exec(content)?.[1];
|
|
195
187
|
if (!rawTarget) return null;
|
|
196
188
|
|
|
197
189
|
const target = resolveWindowsShimPath(rawTarget, path.dirname(commandPath));
|
|
@@ -160,6 +160,29 @@ interface ActiveRepoCache {
|
|
|
160
160
|
projectDir: string;
|
|
161
161
|
activeRepo: ActiveRepoContext | null;
|
|
162
162
|
effectiveGitCwd: string;
|
|
163
|
+
/** Project + worktree dir name when `projectDir` is a linked worktree, else null. */
|
|
164
|
+
worktree: WorktreeContext | null;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
interface WorktreeContext {
|
|
168
|
+
/** Primary-checkout (project) name shown by the path segment. */
|
|
169
|
+
projectName: string;
|
|
170
|
+
/** Worktree directory name — suppressed from the path when it equals the branch. */
|
|
171
|
+
worktreeName: string;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Project + worktree-dir names when `cwd` is a linked git worktree, else null.
|
|
176
|
+
* The project name comes from the shared primary checkout; bare-repo worktrees
|
|
177
|
+
* resolve to the shared `foo.git` dir, so a trailing `.git` is stripped.
|
|
178
|
+
*/
|
|
179
|
+
function resolveWorktreeContext(cwd: string): WorktreeContext | null {
|
|
180
|
+
const worktree = git.repo.linkedWorktreeSync(cwd);
|
|
181
|
+
if (!worktree) return null;
|
|
182
|
+
const base = path.basename(worktree.primaryRoot);
|
|
183
|
+
const projectName = base.endsWith(".git") ? base.slice(0, -4) : base;
|
|
184
|
+
if (!projectName) return null;
|
|
185
|
+
return { projectName, worktreeName: path.basename(worktree.root) };
|
|
163
186
|
}
|
|
164
187
|
|
|
165
188
|
/**
|
|
@@ -312,7 +335,10 @@ export class StatusLineComponent implements Component {
|
|
|
312
335
|
|
|
313
336
|
const activeRepo = resolveActiveRepoContextSync(projectDir);
|
|
314
337
|
const effectiveGitCwd = activeRepo?.repoRoot ?? projectDir;
|
|
315
|
-
|
|
338
|
+
// Only collapse the bare-cwd case: a single-direct-child-repo context
|
|
339
|
+
// (activeRepo set) renders `<parent> ↳ <child>`, which we leave intact.
|
|
340
|
+
const worktree = activeRepo ? null : resolveWorktreeContext(effectiveGitCwd);
|
|
341
|
+
this.#activeRepoCache = { projectDir, activeRepo, effectiveGitCwd, worktree };
|
|
316
342
|
return this.#activeRepoCache;
|
|
317
343
|
}
|
|
318
344
|
|
|
@@ -988,7 +1014,7 @@ export class StatusLineComponent implements Component {
|
|
|
988
1014
|
const projectDir = getProjectDir();
|
|
989
1015
|
const activeRepoCache = shouldResolveActiveRepo
|
|
990
1016
|
? this.#resolveActiveRepoCache()
|
|
991
|
-
: { projectDir, activeRepo: null, effectiveGitCwd: projectDir };
|
|
1017
|
+
: { projectDir, activeRepo: null, effectiveGitCwd: projectDir, worktree: null };
|
|
992
1018
|
const gitBranch = includeGit || includePr ? this.#getCurrentBranch(activeRepoCache.effectiveGitCwd) : null;
|
|
993
1019
|
const gitStatus = includeGit ? this.#getGitStatus(activeRepoCache.effectiveGitCwd) : null;
|
|
994
1020
|
const gitPr = includePr ? this.#lookupPr(activeRepoCache.effectiveGitCwd) : null;
|
|
@@ -1015,6 +1041,7 @@ export class StatusLineComponent implements Component {
|
|
|
1015
1041
|
status: gitStatus,
|
|
1016
1042
|
pr: gitPr,
|
|
1017
1043
|
},
|
|
1044
|
+
worktree: activeRepoCache.worktree,
|
|
1018
1045
|
usage: this.#cachedUsage,
|
|
1019
1046
|
};
|
|
1020
1047
|
}
|
|
@@ -20,6 +20,13 @@ function withIcon(icon: string, text: string): string {
|
|
|
20
20
|
return icon ? `${icon} ${text}` : text;
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
+
/** Left-truncate a path/label to `maxLen`, prefixing an ellipsis when clipped. */
|
|
24
|
+
function clampPathLength(pwd: string, maxLen: number): string {
|
|
25
|
+
if (pwd.length <= maxLen) return pwd;
|
|
26
|
+
const ellipsis = "…";
|
|
27
|
+
return `${ellipsis}${pwd.slice(-Math.max(0, maxLen - ellipsis.length))}`;
|
|
28
|
+
}
|
|
29
|
+
|
|
23
30
|
/**
|
|
24
31
|
* Leading glyph of a thinking-level display string (e.g. "◉ xhigh" → "◉").
|
|
25
32
|
* Compact mode promotes this glyph to the model-segment icon so the level
|
|
@@ -220,12 +227,24 @@ const pathSegment: StatusLineSegment = {
|
|
|
220
227
|
id: "path",
|
|
221
228
|
render(ctx) {
|
|
222
229
|
const opts = ctx.options.path ?? {};
|
|
230
|
+
const stripPrefix = opts.stripWorkPrefix !== false;
|
|
231
|
+
|
|
232
|
+
// Linked git worktree: the on-disk path nests the worktree base, the
|
|
233
|
+
// project, and a worktree dir that usually duplicates the branch (already
|
|
234
|
+
// shown by the git segment). Collapse to the project name, appending the
|
|
235
|
+
// worktree dir only when it diverges from the branch.
|
|
236
|
+
if (stripPrefix && ctx.worktree) {
|
|
237
|
+
const { projectName, worktreeName } = ctx.worktree;
|
|
238
|
+
const label = ctx.git.branch === worktreeName ? projectName : `${projectName}/${worktreeName}`;
|
|
239
|
+
const content = withIcon(theme.icon.worktree, clampPathLength(label, opts.maxLength ?? 40));
|
|
240
|
+
return { content: theme.fg("statusLinePath", content), visible: true };
|
|
241
|
+
}
|
|
223
242
|
|
|
224
243
|
const projectDir = ctx.activeRepo?.cwd ?? getProjectDir();
|
|
225
244
|
const { scratch, relative } = classifyProjectDir(projectDir);
|
|
226
245
|
let pwd = projectDir;
|
|
227
246
|
|
|
228
|
-
if (
|
|
247
|
+
if (stripPrefix) {
|
|
229
248
|
if (scratch) {
|
|
230
249
|
if (relative) pwd = relative;
|
|
231
250
|
} else {
|
|
@@ -237,17 +256,12 @@ const pathSegment: StatusLineSegment = {
|
|
|
237
256
|
pwd = shortenPath(pwd);
|
|
238
257
|
}
|
|
239
258
|
|
|
240
|
-
|
|
241
|
-
if (pwd.length > maxLen) {
|
|
242
|
-
const ellipsis = "…";
|
|
243
|
-
const sliceLen = Math.max(0, maxLen - ellipsis.length);
|
|
244
|
-
pwd = `${ellipsis}${pwd.slice(-sliceLen)}`;
|
|
245
|
-
}
|
|
259
|
+
pwd = clampPathLength(pwd, opts.maxLength ?? 40);
|
|
246
260
|
if (repoSuffix) {
|
|
247
261
|
pwd = `${pwd}${repoSuffix}`;
|
|
248
262
|
}
|
|
249
263
|
|
|
250
|
-
const showScratchIcon = scratch &&
|
|
264
|
+
const showScratchIcon = scratch && stripPrefix;
|
|
251
265
|
const icon = showScratchIcon ? theme.icon.scratchFolder : theme.icon.folder;
|
|
252
266
|
const content = withIcon(icon, pwd);
|
|
253
267
|
return { content: theme.fg("statusLinePath", content), visible: true };
|
|
@@ -97,6 +97,13 @@ export interface SegmentContext {
|
|
|
97
97
|
status: { staged: number; unstaged: number; untracked: number } | null;
|
|
98
98
|
pr: { number: number; url: string } | null;
|
|
99
99
|
};
|
|
100
|
+
/**
|
|
101
|
+
* Set when the path cwd is a *linked* git worktree, naming the shared
|
|
102
|
+
* primary checkout (the project). Lets the path segment collapse the
|
|
103
|
+
* base-prefixed `<base>/<project>/<worktree>` path to the project name —
|
|
104
|
+
* the worktree/branch is already shown by the git segment.
|
|
105
|
+
*/
|
|
106
|
+
worktree: { projectName: string; worktreeName: string } | null;
|
|
100
107
|
usage: {
|
|
101
108
|
tier?: string;
|
|
102
109
|
fiveHour?: { percent: number; resetMinutes?: number };
|
|
@@ -50,6 +50,15 @@ const MAX_LIVE_IRC_CARDS = 4;
|
|
|
50
50
|
const IDLE_RECAP_MIN_SECONDS = 1;
|
|
51
51
|
const IDLE_RECAP_MAX_SECONDS = 3600;
|
|
52
52
|
|
|
53
|
+
const RAW_PARTIAL_JSON_RENDERERS: Record<string, true> = { bash: true, edit: true, apply_patch: true };
|
|
54
|
+
|
|
55
|
+
function exposesRawPartialJson(toolName: string, rawInput: boolean, tool: unknown): boolean {
|
|
56
|
+
if (rawInput) return true;
|
|
57
|
+
if (RAW_PARTIAL_JSON_RENDERERS[toolName]) return true;
|
|
58
|
+
if (tool === null || typeof tool !== "object" || !("renderCall" in tool)) return false;
|
|
59
|
+
return typeof tool.renderCall === "function";
|
|
60
|
+
}
|
|
61
|
+
|
|
53
62
|
type AgentSessionEventHandlers = {
|
|
54
63
|
[E in AgentSessionEventKind]: (event: Extract<AgentSessionEvent, { type: E }>) => Promise<void>;
|
|
55
64
|
};
|
|
@@ -623,7 +632,7 @@ export class EventController {
|
|
|
623
632
|
// Internal URL read falls through to ToolExecutionComponent below.
|
|
624
633
|
}
|
|
625
634
|
|
|
626
|
-
// Preserve the raw partial JSON for renderers that need to surface fields before the JSON object closes.
|
|
635
|
+
// Preserve the raw partial JSON only for renderers that need to surface fields before the JSON object closes.
|
|
627
636
|
// Bash uses this to show inline env assignments during streaming instead of popping them in at completion.
|
|
628
637
|
// While the JSON is still open, ToolArgsRevealController paces the
|
|
629
638
|
// reveal (write/edit/bash previews grow smoothly when a slow provider
|
|
@@ -631,13 +640,14 @@ export class EventController {
|
|
|
631
640
|
// as-is — mirroring how assistant text snaps at message_end.
|
|
632
641
|
let renderArgs: Record<string, unknown>;
|
|
633
642
|
const partialJson = getStreamingPartialJson(content);
|
|
643
|
+
const rawInput = content.customWireName !== undefined;
|
|
644
|
+
const tool = this.ctx.viewSession.getToolByName(content.name);
|
|
634
645
|
if (partialJson) {
|
|
635
|
-
renderArgs = this.#toolArgsReveal.setTarget(
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
content.
|
|
639
|
-
|
|
640
|
-
);
|
|
646
|
+
renderArgs = this.#toolArgsReveal.setTarget(content.id, partialJson, {
|
|
647
|
+
rawInput,
|
|
648
|
+
exposeRawPartialJson: exposesRawPartialJson(content.name, rawInput, tool),
|
|
649
|
+
fullArgs: content.arguments,
|
|
650
|
+
});
|
|
641
651
|
} else {
|
|
642
652
|
this.#toolArgsReveal.finish(content.id);
|
|
643
653
|
renderArgs = content.arguments;
|
|
@@ -645,7 +655,6 @@ export class EventController {
|
|
|
645
655
|
if (!this.ctx.pendingTools.has(content.id)) {
|
|
646
656
|
this.#resolveDisplaceablePoll(content.name);
|
|
647
657
|
this.#resetReadGroup();
|
|
648
|
-
const tool = this.ctx.viewSession.getToolByName(content.name);
|
|
649
658
|
const component = new ToolExecutionComponent(
|
|
650
659
|
content.name,
|
|
651
660
|
renderArgs,
|
|
@@ -1145,7 +1145,7 @@ export class InputController {
|
|
|
1145
1145
|
|
|
1146
1146
|
/** Send editor text as a follow-up message (queued behind current stream). */
|
|
1147
1147
|
async handleFollowUp(): Promise<void> {
|
|
1148
|
-
let text = this.ctx.editor.
|
|
1148
|
+
let text = this.ctx.editor.getExpandedText().trim();
|
|
1149
1149
|
const images = this.ctx.editor.pendingImages.length > 0 ? [...this.ctx.editor.pendingImages] : undefined;
|
|
1150
1150
|
const imageLinks =
|
|
1151
1151
|
images && this.ctx.editor.pendingImageLinks.length > 0 ? [...this.ctx.editor.pendingImageLinks] : undefined;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { parseStreamingJson } from "@oh-my-pi/pi-utils";
|
|
1
|
+
import { parseStreamingJson, parseStreamingJsonThrottled, STREAMING_JSON_PARSE_MIN_GROWTH } from "@oh-my-pi/pi-utils";
|
|
2
2
|
import { nextStep, STREAMING_REVEAL_FRAME_MS } from "./streaming-reveal";
|
|
3
3
|
|
|
4
4
|
/** Minimal component surface the reveal pushes frames into. */
|
|
@@ -19,6 +19,16 @@ type RevealEntry = {
|
|
|
19
19
|
revealed: number;
|
|
20
20
|
/** Custom-tool raw input: display args are `{ input: prefix }`, never parsed as JSON. */
|
|
21
21
|
rawInput: boolean;
|
|
22
|
+
/** Whether the renderer observes fresh raw JSON prefixes directly. */
|
|
23
|
+
exposeRawPartialJson: boolean;
|
|
24
|
+
/** Last parsed JSON args from the revealed prefix. */
|
|
25
|
+
parsedArgs: Record<string, unknown>;
|
|
26
|
+
/** Prefix length covered by `parsedArgs`. */
|
|
27
|
+
parsedLen: number;
|
|
28
|
+
/** Last object handed to a component; reused when visible args have not changed. */
|
|
29
|
+
displayArgs: Record<string, unknown>;
|
|
30
|
+
/** Raw prefix carried by `displayArgs.__partialJson`. */
|
|
31
|
+
displayPrefix: string;
|
|
22
32
|
};
|
|
23
33
|
|
|
24
34
|
/** Clamp a slice end into `text`, never splitting a surrogate pair: a prefix
|
|
@@ -32,15 +42,64 @@ function clampSliceEnd(text: string, end: number): number {
|
|
|
32
42
|
return code >= 0xd800 && code <= 0xdbff ? end + 1 : end;
|
|
33
43
|
}
|
|
34
44
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
45
|
+
type ToolArgsRevealTarget = {
|
|
46
|
+
rawInput: boolean;
|
|
47
|
+
exposeRawPartialJson: boolean;
|
|
48
|
+
fullArgs: Record<string, unknown>;
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
type DisplayArgsStep = {
|
|
52
|
+
args: Record<string, unknown>;
|
|
53
|
+
changed: boolean;
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
function initialDisplayArgs(): Record<string, unknown> {
|
|
57
|
+
return { __partialJson: "" };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function resetDisplayState(entry: RevealEntry): void {
|
|
61
|
+
entry.parsedArgs = {};
|
|
62
|
+
entry.parsedLen = 0;
|
|
63
|
+
entry.displayArgs = initialDisplayArgs();
|
|
64
|
+
entry.displayPrefix = "";
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** Display args for a revealed prefix. Function-tool JSON is parsed at the same
|
|
68
|
+
* growth-throttled cadence providers use, so a long `write` payload cannot make
|
|
69
|
+
* the reveal loop re-parse the whole growing buffer every frame. Renderers that
|
|
70
|
+
* read raw JSON directly still receive fresh `__partialJson` prefixes; other
|
|
71
|
+
* renderers get a stable object reference while parsed fields are unchanged. */
|
|
72
|
+
function displayArgsForPrefix(entry: RevealEntry, prefix: string, forceParse = false): DisplayArgsStep {
|
|
73
|
+
if (entry.rawInput) {
|
|
74
|
+
if (prefix === entry.displayPrefix) return { args: entry.displayArgs, changed: false };
|
|
75
|
+
const args = { input: prefix, __partialJson: prefix };
|
|
76
|
+
entry.displayArgs = args;
|
|
77
|
+
entry.displayPrefix = prefix;
|
|
78
|
+
return { args, changed: true };
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
let parsedChanged = false;
|
|
82
|
+
if (forceParse || (prefix.length > 0 && prefix.length < STREAMING_JSON_PARSE_MIN_GROWTH)) {
|
|
83
|
+
entry.parsedArgs = parseStreamingJson<Record<string, unknown>>(prefix);
|
|
84
|
+
entry.parsedLen = prefix.length;
|
|
85
|
+
parsedChanged = true;
|
|
86
|
+
} else {
|
|
87
|
+
const throttled = parseStreamingJsonThrottled<Record<string, unknown>>(prefix, entry.parsedLen);
|
|
88
|
+
if (throttled) {
|
|
89
|
+
entry.parsedArgs = throttled.value;
|
|
90
|
+
entry.parsedLen = throttled.parsedLen;
|
|
91
|
+
parsedChanged = true;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const rawPrefixChanged = entry.exposeRawPartialJson && prefix !== entry.displayPrefix;
|
|
96
|
+
if (!parsedChanged && !rawPrefixChanged) return { args: entry.displayArgs, changed: false };
|
|
97
|
+
|
|
98
|
+
const displayPrefix = entry.exposeRawPartialJson || parsedChanged ? prefix : entry.displayPrefix;
|
|
99
|
+
const args = { ...entry.parsedArgs, __partialJson: displayPrefix };
|
|
100
|
+
entry.displayArgs = args;
|
|
101
|
+
entry.displayPrefix = displayPrefix;
|
|
102
|
+
return { args, changed: true };
|
|
44
103
|
}
|
|
45
104
|
|
|
46
105
|
/**
|
|
@@ -49,7 +108,9 @@ function buildDisplayArgs(prefix: string, rawInput: boolean): Record<string, unk
|
|
|
49
108
|
* (or throttle their partial parses) would otherwise make write/edit/bash
|
|
50
109
|
* streaming previews jump in chunks. Each pending tool call reveals its raw
|
|
51
110
|
* argument stream at the shared 30fps cadence with the same adaptive
|
|
52
|
-
* catch-up step
|
|
111
|
+
* catch-up step. JSON prefixes are parsed only when enough new bytes arrive to
|
|
112
|
+
* change renderer-visible fields, while raw-prefix consumers still receive
|
|
113
|
+
* fresh `__partialJson` on every reveal frame.
|
|
53
114
|
*
|
|
54
115
|
* Reveal units are UTF-16 code units of the raw stream, not graphemes —
|
|
55
116
|
* the prefix goes through a JSON parser rather than straight to the screen,
|
|
@@ -71,12 +132,8 @@ export class ToolArgsRevealController {
|
|
|
71
132
|
* args to render right now. With smoothing disabled the full target passes
|
|
72
133
|
* through in the caller's legacy shape (`{ ...args, __partialJson }`).
|
|
73
134
|
*/
|
|
74
|
-
setTarget(
|
|
75
|
-
|
|
76
|
-
partialJson: string,
|
|
77
|
-
rawInput: boolean,
|
|
78
|
-
fullArgs: Record<string, unknown>,
|
|
79
|
-
): Record<string, unknown> {
|
|
135
|
+
setTarget(id: string, partialJson: string, target: ToolArgsRevealTarget): Record<string, unknown> {
|
|
136
|
+
const { rawInput, exposeRawPartialJson, fullArgs } = target;
|
|
80
137
|
if (!this.#getSmoothStreaming()) {
|
|
81
138
|
// Toggle may flip mid-call: drop any live entry so ticks stop.
|
|
82
139
|
this.#entries.delete(id);
|
|
@@ -84,18 +141,34 @@ export class ToolArgsRevealController {
|
|
|
84
141
|
}
|
|
85
142
|
let entry = this.#entries.get(id);
|
|
86
143
|
if (!entry) {
|
|
87
|
-
entry = {
|
|
144
|
+
entry = {
|
|
145
|
+
component: undefined,
|
|
146
|
+
target: partialJson,
|
|
147
|
+
revealed: 0,
|
|
148
|
+
rawInput,
|
|
149
|
+
exposeRawPartialJson,
|
|
150
|
+
parsedArgs: {},
|
|
151
|
+
parsedLen: 0,
|
|
152
|
+
displayArgs: initialDisplayArgs(),
|
|
153
|
+
displayPrefix: "",
|
|
154
|
+
};
|
|
88
155
|
this.#entries.set(id, entry);
|
|
89
156
|
} else {
|
|
157
|
+
if (entry.rawInput !== rawInput || entry.exposeRawPartialJson !== exposeRawPartialJson) {
|
|
158
|
+
entry.rawInput = rawInput;
|
|
159
|
+
entry.exposeRawPartialJson = exposeRawPartialJson;
|
|
160
|
+
resetDisplayState(entry);
|
|
161
|
+
}
|
|
90
162
|
// Streams only append; a non-prefix target means a rewind — snap into range.
|
|
91
163
|
if (!partialJson.startsWith(entry.target)) {
|
|
92
164
|
entry.revealed = Math.min(entry.revealed, partialJson.length);
|
|
165
|
+
resetDisplayState(entry);
|
|
93
166
|
}
|
|
94
167
|
entry.target = partialJson;
|
|
95
168
|
}
|
|
96
169
|
entry.revealed = clampSliceEnd(entry.target, entry.revealed);
|
|
97
170
|
this.#syncTimer();
|
|
98
|
-
return
|
|
171
|
+
return displayArgsForPrefix(entry, entry.target.slice(0, entry.revealed)).args;
|
|
99
172
|
}
|
|
100
173
|
|
|
101
174
|
/** Attach the component future ticks push frames into. */
|
|
@@ -118,7 +191,7 @@ export class ToolArgsRevealController {
|
|
|
118
191
|
flushAll(): void {
|
|
119
192
|
for (const [id, entry] of this.#entries) {
|
|
120
193
|
if (entry.component && entry.revealed < entry.target.length) {
|
|
121
|
-
entry.component.updateArgs(
|
|
194
|
+
entry.component.updateArgs(displayArgsForPrefix(entry, entry.target, true).args, id);
|
|
122
195
|
}
|
|
123
196
|
}
|
|
124
197
|
this.#entries.clear();
|
|
@@ -157,15 +230,20 @@ export class ToolArgsRevealController {
|
|
|
157
230
|
|
|
158
231
|
#tick(): void {
|
|
159
232
|
let advanced = false;
|
|
233
|
+
let rendered = false;
|
|
160
234
|
for (const [id, entry] of this.#entries) {
|
|
161
235
|
const backlog = entry.target.length - entry.revealed;
|
|
162
236
|
if (backlog <= 0 || !entry.component) continue;
|
|
163
237
|
entry.revealed = clampSliceEnd(entry.target, entry.revealed + nextStep(backlog));
|
|
164
|
-
|
|
238
|
+
const display = displayArgsForPrefix(entry, entry.target.slice(0, entry.revealed));
|
|
239
|
+
if (display.changed) {
|
|
240
|
+
entry.component.updateArgs(display.args, id);
|
|
241
|
+
rendered = true;
|
|
242
|
+
}
|
|
165
243
|
advanced = true;
|
|
166
244
|
}
|
|
167
245
|
if (advanced) {
|
|
168
|
-
this.#requestRender();
|
|
246
|
+
if (rendered) this.#requestRender();
|
|
169
247
|
} else {
|
|
170
248
|
// Every entry caught up (or unbound); setTarget restarts on growth.
|
|
171
249
|
this.#stopTimer();
|
package/src/modes/theme/theme.ts
CHANGED
|
@@ -96,6 +96,7 @@ export type SymbolKey =
|
|
|
96
96
|
| "icon.pause"
|
|
97
97
|
| "icon.loop"
|
|
98
98
|
| "icon.folder"
|
|
99
|
+
| "icon.worktree"
|
|
99
100
|
| "icon.search"
|
|
100
101
|
| "icon.scratchFolder"
|
|
101
102
|
| "icon.file"
|
|
@@ -299,6 +300,7 @@ const UNICODE_SYMBOLS: SymbolMap = {
|
|
|
299
300
|
"icon.pause": "⏸",
|
|
300
301
|
"icon.loop": "↻",
|
|
301
302
|
"icon.folder": "📁",
|
|
303
|
+
"icon.worktree": "🌳",
|
|
302
304
|
"icon.search": "🔍",
|
|
303
305
|
"icon.scratchFolder": "🗑",
|
|
304
306
|
"icon.file": "📄",
|
|
@@ -561,6 +563,8 @@ const NERD_SYMBOLS: SymbolMap = {
|
|
|
561
563
|
"icon.search": "\uf002",
|
|
562
564
|
// pick: | alt:
|
|
563
565
|
"icon.scratchFolder": "\uf014",
|
|
566
|
+
// pick: nf-fa-sitemap | alt: nf-cod-list_tree
|
|
567
|
+
"icon.worktree": "\uf0e8",
|
|
564
568
|
// pick: | alt:
|
|
565
569
|
"icon.file": "\uf15b",
|
|
566
570
|
// pick: | alt: ⎇
|
|
@@ -808,6 +812,7 @@ const ASCII_SYMBOLS: SymbolMap = {
|
|
|
808
812
|
"icon.pause": "||",
|
|
809
813
|
"icon.loop": "loop",
|
|
810
814
|
"icon.folder": "[D]",
|
|
815
|
+
"icon.worktree": "[wt]",
|
|
811
816
|
"icon.search": "[/]",
|
|
812
817
|
"icon.scratchFolder": "[T]",
|
|
813
818
|
"icon.file": "[F]",
|
|
@@ -1797,6 +1802,7 @@ export class Theme {
|
|
|
1797
1802
|
pause: this.#symbols["icon.pause"],
|
|
1798
1803
|
loop: this.#symbols["icon.loop"],
|
|
1799
1804
|
folder: this.#symbols["icon.folder"],
|
|
1805
|
+
worktree: this.#symbols["icon.worktree"],
|
|
1800
1806
|
scratchFolder: this.#symbols["icon.scratchFolder"],
|
|
1801
1807
|
file: this.#symbols["icon.file"],
|
|
1802
1808
|
git: this.#symbols["icon.git"],
|
package/src/prompts/bench.md
CHANGED
|
@@ -1,12 +1,6 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
Schema and statistics: orders(id, customer_id, status, total) holds 50,000,000 rows with 5 distinct status values; customers(id, country, segment) holds 4,000,000 rows across 200 countries; line_items(order_id, product_id, qty) holds 300,000,000 rows; products(id, category, price) holds 80,000 rows across 600 categories. The query reports total revenue per product category for shipped orders placed by customers in one given country.
|
|
4
|
-
|
|
5
|
-
Reason step by step and keep going: estimate the selectivity and output cardinality of each predicate and each join, then enumerate every join order over the four tables and derive the cost of each under both nested-loop and hash-join operators, weigh index access against full scans for each table, decide where the aggregation belongs and whether a partial pre-aggregation or a semi-join reduction earns its keep, and account for a memory limit that forces a hash build side to spill to disk. Compute the number behind every decision before you commit to it; when you finish one candidate plan, move on to the next and derive its cost too, and choose a winner only after you have costed the whole field. Never assert a choice you have not justified with an estimate.
|
|
1
|
+
Write a detailed, four-paragraph explanation of how a web browser renders a webpage. Cover the process from receiving the initial HTML payload to painting pixels on the screen. Include the construction of the DOM and CSSOM, the render tree, layout, and painting.
|
|
6
2
|
|
|
7
3
|
Form:
|
|
8
|
-
- Plain paragraphs only: no headings, no lists, no code fences, no
|
|
9
|
-
-
|
|
10
|
-
-
|
|
11
|
-
|
|
12
|
-
Output only the analysis.
|
|
4
|
+
- Plain paragraphs only: no headings, no lists, no code fences, no preamble.
|
|
5
|
+
- Do not summarize early; keep explaining until you reach the token limit.
|
|
6
|
+
- Output only the explanation.
|
package/src/prompts/tools/irc.md
CHANGED
|
@@ -1,33 +1,23 @@
|
|
|
1
|
-
Send
|
|
1
|
+
Send and receive short text messages between the agents running in this process.
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
- `op: "list"` — peers with status (`running` | `idle` | `parked`), unread count, parent, last activity. Use when unsure who exists.
|
|
6
|
-
- `op: "send"` — fire-and-forget; returns per-recipient receipts immediately, NEVER waits for the recipient to act. Outcomes: delivered, or `failed` (unreachable). `to: "all"` broadcasts to live peers.
|
|
7
|
-
- Messaging an `idle`/`parked` peer wakes it — no separate revive call.
|
|
8
|
-
- `op: "wait"` — block for a message (optionally only `from` one peer); consumes + returns it. Timeout = clean "no message", not an error.
|
|
9
|
-
- `op: "inbox"` — drain pending messages without blocking.
|
|
10
|
-
- Replies arrive only when the recipient sends one; don't interrogate a peer for status.
|
|
11
|
-
</instruction>
|
|
3
|
+
# Addressing and Discovery
|
|
4
|
+
The main agent is always `Main`. Subagents inherit their task ID (e.g., `AuthLoader`). If you don't know who is currently running, use `op: "list"` to view all peers alongside their status, unread message count, and recent activity. Address peers by their exact ID from the roster; NEVER invent names.
|
|
12
5
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
- **
|
|
16
|
-
- **
|
|
17
|
-
- **
|
|
18
|
-
- **
|
|
6
|
+
# Messaging Rules
|
|
7
|
+
Use `op: "send"` to deliver a message to a specific peer or broadcast to `"all"`.
|
|
8
|
+
- **Fire and forget:** Sending NEVER blocks. You get delivery receipts immediately (`delivered` or `failed`). Do not wait around—send your message and keep working. If a receipt says `failed`, the peer is gone; do not retry.
|
|
9
|
+
- **Waking peers:** Sending a message to an `idle` or `parked` agent automatically wakes them up.
|
|
10
|
+
- **Answering:** When replying to a question, use `op: "send"`, lead directly with your answer (NEVER quote the original message), and set `replyTo` so the recipient can correlate it.
|
|
11
|
+
- **Format:** Messages MUST be plain prose. NEVER send JSON status objects. Keep it terse and share paths via `local://` or `artifact://` URLs, not pasted blobs.
|
|
19
12
|
|
|
20
|
-
|
|
21
|
-
|
|
13
|
+
# Waiting and Inboxes
|
|
14
|
+
Messages only arrive when the peer actively sends one—do not interrogate a peer for status.
|
|
15
|
+
- If you are completely blocked and MUST wait for an answer, use `op: "wait"` (or `await: true` on a send). This blocks your turn until a message arrives. If it times out, that just means "no message arrived", not a failure.
|
|
16
|
+
- To check for messages without blocking, use `op: "inbox"` to drain your queue.
|
|
22
17
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
-
|
|
26
|
-
-
|
|
27
|
-
-
|
|
28
|
-
-
|
|
29
|
-
- **Answer expected questions** via `irc send` to the sender (finish your current step first).
|
|
30
|
-
- **Stay terse.** One question per send; share files via `local://`/`memory://`/`artifact://` URLs, never pasted blobs.
|
|
31
|
-
- **Address peers by exact id** from `op: "list"` (e.g. `AuthLoader`, `Main`). NEVER invent friendly names.
|
|
32
|
-
- **NEVER IRC what a tool answers.** A `read`, grep, or build resolves it? Do that first.
|
|
33
|
-
</etiquette>
|
|
18
|
+
# When to Coordinate
|
|
19
|
+
Message peers instead of guessing, duplicating work, or spying.
|
|
20
|
+
- Use IRC when you hit an unexpected state (e.g., missing files) or an out-of-scope decision. DM `Main` or your spawner for guidance.
|
|
21
|
+
- If you overlap with another agent's work or need a file they are touching, DM them before editing.
|
|
22
|
+
- NEVER use shell tools, grep, or read other sessions' files to figure out what a peer is doing. Message them directly.
|
|
23
|
+
- NEVER use IRC for something a tool can answer (e.g., grepping codebase, running a build).
|
package/src/prompts/tools/job.md
CHANGED
|
@@ -1,17 +1,11 @@
|
|
|
1
|
-
|
|
1
|
+
Manages async background tasks (e.g. bash scripts, subagents).
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Background tasks deliver their results automatically the moment they finish. You NEVER need to poll to retrieve output. Only use this tool if you need to intervene in the lifecycle of a task.
|
|
4
4
|
|
|
5
|
-
#
|
|
5
|
+
# Interventions
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
- Use only when genuinely blocked with no other work.
|
|
13
|
-
- Completed jobs include final output.
|
|
14
|
-
|
|
15
|
-
## `cancel: [id, …]`
|
|
16
|
-
Stop running jobs.
|
|
17
|
-
- Use when a job is stalled, hung, or no longer needed.
|
|
7
|
+
- **Block and wait:** Pass `poll` with specific job IDs when you are completely blocked and cannot do any other work. The call returns as soon as one watched job finishes or the wait window elapses — NOT when all of them finish; re-issue to keep waiting.
|
|
8
|
+
- To watch EVERY running job, issue a call with NO fields at all (no `poll`, no `cancel`, no `list`). NEVER pass an array of every running ID.
|
|
9
|
+
- A finished job's output is included in the wait result.
|
|
10
|
+
- **Stop execution:** Pass `cancel` with job IDs to kill jobs that have hung, stalled, or are no longer needed. A cancel-only call returns immediately.
|
|
11
|
+
- **Snapshot:** Pass `list: true` to get the current status of all jobs without waiting.
|