@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.
Files changed (68) hide show
  1. package/CHANGELOG.md +37 -0
  2. package/dist/cli.js +3211 -3231
  3. package/dist/types/cli/bench-cli.d.ts +6 -1
  4. package/dist/types/commands/bench.d.ts +8 -0
  5. package/dist/types/config/model-discovery.d.ts +6 -1
  6. package/dist/types/config/settings-schema.d.ts +1 -1
  7. package/dist/types/edit/index.d.ts +1 -0
  8. package/dist/types/edit/renderer.d.ts +4 -0
  9. package/dist/types/edit/snapshot-details.d.ts +33 -0
  10. package/dist/types/mcp/oauth-flow.d.ts +4 -6
  11. package/dist/types/modes/components/status-line/types.d.ts +10 -0
  12. package/dist/types/modes/controllers/tool-args-reveal.d.ts +9 -2
  13. package/dist/types/modes/theme/theme.d.ts +2 -1
  14. package/dist/types/session/agent-session.d.ts +11 -0
  15. package/dist/types/session/session-manager.d.ts +3 -4
  16. package/dist/types/task/provider-concurrency.d.ts +40 -0
  17. package/dist/types/utils/git.d.ts +11 -0
  18. package/dist/types/web/search/providers/duckduckgo.d.ts +2 -2
  19. package/dist/types/web/search/types.d.ts +1 -1
  20. package/package.json +12 -12
  21. package/src/cli/args.ts +32 -1
  22. package/src/cli/bench-cli.ts +89 -21
  23. package/src/cli/web-search-cli.ts +6 -1
  24. package/src/cli/worktree-cli.ts +8 -5
  25. package/src/commands/bench.ts +10 -1
  26. package/src/config/mcp-schema.json +1 -1
  27. package/src/config/model-discovery.ts +98 -20
  28. package/src/config/model-registry.ts +13 -6
  29. package/src/config/settings-schema.ts +5 -2
  30. package/src/edit/hashline/execute.ts +15 -9
  31. package/src/edit/index.ts +19 -6
  32. package/src/edit/modes/patch.ts +3 -2
  33. package/src/edit/modes/replace.ts +3 -2
  34. package/src/edit/renderer.ts +4 -0
  35. package/src/edit/snapshot-details.ts +77 -0
  36. package/src/extensibility/plugins/legacy-pi-compat.ts +2 -2
  37. package/src/internal-urls/docs-index.generated.txt +1 -1
  38. package/src/mcp/oauth-flow.ts +10 -8
  39. package/src/mcp/transports/stdio.ts +9 -17
  40. package/src/modes/components/status-line/component.ts +29 -2
  41. package/src/modes/components/status-line/segments.ts +22 -8
  42. package/src/modes/components/status-line/types.ts +7 -0
  43. package/src/modes/controllers/event-controller.ts +17 -8
  44. package/src/modes/controllers/input-controller.ts +1 -1
  45. package/src/modes/controllers/tool-args-reveal.ts +100 -22
  46. package/src/modes/theme/theme.ts +6 -0
  47. package/src/prompts/bench.md +4 -10
  48. package/src/prompts/tools/irc.md +19 -29
  49. package/src/prompts/tools/job.md +8 -14
  50. package/src/prompts/tools/lsp.md +19 -30
  51. package/src/prompts/tools/task.md +42 -62
  52. package/src/sdk.ts +14 -5
  53. package/src/session/agent-session.ts +107 -13
  54. package/src/session/session-listing.ts +9 -8
  55. package/src/session/session-loader.ts +98 -3
  56. package/src/session/session-manager.ts +34 -4
  57. package/src/subprocess/worker-client.ts +12 -4
  58. package/src/task/executor.ts +4 -62
  59. package/src/task/provider-concurrency.ts +100 -0
  60. package/src/task/worktree.ts +13 -4
  61. package/src/task/yield-assembly.ts +27 -39
  62. package/src/tools/path-utils.ts +4 -2
  63. package/src/tools/read.ts +11 -1
  64. package/src/utils/git.ts +13 -0
  65. package/src/web/search/index.ts +14 -8
  66. package/src/web/search/providers/duckduckgo.ts +136 -78
  67. package/src/web/search/providers/gemini.ts +268 -185
  68. package/src/web/search/types.ts +1 -1
@@ -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. Defaults to `"consent"`
229
- * so the provider always shows its authorize screen instead of silently
230
- * re-approving the browser's current session without it, reauthorizing to
231
- * switch accounts/workspaces is impossible once a session cookie exists
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: "Codex",
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
- const prog = extractWindowsNpmShimProg(content);
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 = extractWindowsNpmShimTarget(content);
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
- this.#activeRepoCache = { projectDir, activeRepo, effectiveGitCwd };
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 (opts.stripWorkPrefix !== false) {
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
- const maxLen = opts.maxLength ?? 40;
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 && opts.stripWorkPrefix !== false;
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
- content.id,
637
- partialJson,
638
- content.customWireName !== undefined,
639
- content.arguments,
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.getText().trim();
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
- /** Display args for a revealed raw-stream prefix. Function-tool prefixes are
36
- * re-parsed with the same streaming-tolerant parser providers use, so every
37
- * frame is a state the provider itself could have produced; custom tools
38
- * mirror the provider's `{ input }` shape. `__partialJson` carries the
39
- * matching raw prefix for renderers that read it directly (bash env preview,
40
- * edit strategies). */
41
- function buildDisplayArgs(prefix: string, rawInput: boolean): Record<string, unknown> {
42
- const base: Record<string, unknown> = rawInput ? { input: prefix } : parseStreamingJson(prefix);
43
- return { ...base, __partialJson: prefix };
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, re-parsing the revealed prefix per frame.
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
- id: string,
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 = { component: undefined, target: partialJson, revealed: 0, rawInput };
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 buildDisplayArgs(entry.target.slice(0, entry.revealed), entry.rawInput);
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(buildDisplayArgs(entry.target, entry.rawInput), id);
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
- entry.component.updateArgs(buildDisplayArgs(entry.target.slice(0, entry.revealed), entry.rawInput), id);
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();
@@ -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"],
@@ -1,12 +1,6 @@
1
- You are given a relational schema and a multi-way analytical query, and you must work out from first principles the execution plan a cost-based optimizer should choose. This is a hard estimation problem with a large search space, so think it all the way through before you settle on anything and reason your way to each number instead of answering from intuition. Do not recite how query optimization works in general — actually do the analysis for this query, deriving every estimate.
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 tables, no preamble.
9
- - Derive each estimate explicitly; state no conclusion you have not computed.
10
- - Do not wrap up early or summarize; keep reasoning until you are cut off.
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.
@@ -1,33 +1,23 @@
1
- Send/receive short text messages between agents in this process.
1
+ Send and receive short text messages between the agents running in this process.
2
2
 
3
- <instruction>
4
- - Main agent is `Main`; subagents reuse their task id (`AuthLoader`, `AuthLoader-2` on repeat).
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
- <when_to_use>
14
- Reach for `irc` when going alone is wasteful or wrong; when in doubt, message.
15
- - **Unexpected state** missing file, config contradicting the assignment, API/tool behaving differently than told. DM `Main` (or your spawner), don't guess.
16
- - **Blocked by another agent** — a peer holds the file/branch/resource/decision you need, or started your change. DM them (or broadcast to find who) before duplicating work.
17
- - **Decision outside your scope** a genuine fork the assignment didn't pre-decide. Ask the requester, don't pick unilaterally.
18
- - **Coordination** a peer's in-flight work overlaps yours (roster shows each peer's role + activity); message before editing a shared file or duplicating a sibling's change.
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
- NEVER for: routine progress updates, things a tool call can verify, questions your assignment/repo/docs already answer.
21
- </when_to_use>
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
- <etiquette>
24
- Applies to sending + replying.
25
- - **Plain prose only.** NEVER JSON status payloads like `{"type":"task_completed",…}` write a normal sentence.
26
- - **NEVER quote the message you answer.** Lead with the answer; set `replyTo`.
27
- - **Learn about peers via IRC** — NEVER grep artifacts, read other sessions' JSONL, or shell-poke. DM them.
28
- - **Send, then keep working.** `wait`/`await: true` only when you cannot proceed. NEVER "did you get my message?". A `failed` receipt = peer unreachable — move on; NEVER retry in a loop.
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).
@@ -1,17 +1,11 @@
1
- Inspects, waits, or cancels async jobs.
1
+ Manages async background tasks (e.g. bash scripts, subagents).
2
2
 
3
- Results arrive automatically on completion; reach for this tool only to intervene.
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
- # Operations
5
+ # Interventions
6
6
 
7
- ## `list: true`
8
- Inspect what's running.
9
-
10
- ## `poll: [id, …]`
11
- Block until specified jobs finish or the wait window elapses. Omit `poll` (no `list`/`cancel`) to wait on ALL running jobs NEVER enumerate ids you don't need to filter.
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.