@yagni-app/code 1.0.0 → 1.0.2

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 (66) hide show
  1. package/README.md +42 -0
  2. package/dist/cli.js +231 -6
  3. package/dist/crashReport.d.ts +8 -0
  4. package/dist/crashReport.js +13 -1
  5. package/dist/doctor.d.ts +7 -0
  6. package/dist/doctor.js +33 -0
  7. package/dist/extension/askAdvisorTool.d.ts +14 -0
  8. package/dist/extension/askAdvisorTool.js +16 -4
  9. package/dist/extension/askYagniTool.d.ts +1 -1
  10. package/dist/extension/askYagniTool.js +21 -0
  11. package/dist/extension/branding.d.ts +15 -0
  12. package/dist/extension/branding.js +76 -0
  13. package/dist/extension/childUsage.d.ts +40 -0
  14. package/dist/extension/childUsage.js +43 -0
  15. package/dist/extension/chipEditor.d.ts +22 -1
  16. package/dist/extension/chipEditor.js +58 -5
  17. package/dist/extension/condensedTools.d.ts +97 -0
  18. package/dist/extension/condensedTools.js +396 -0
  19. package/dist/extension/diffStat.d.ts +62 -0
  20. package/dist/extension/diffStat.js +158 -0
  21. package/dist/extension/footer.d.ts +15 -1
  22. package/dist/extension/footer.js +35 -15
  23. package/dist/extension/index.d.ts +6 -0
  24. package/dist/extension/index.js +126 -6
  25. package/dist/extension/permission/execPolicy.js +47 -0
  26. package/dist/extension/permission/gate.d.ts +6 -0
  27. package/dist/extension/permission/gate.js +11 -2
  28. package/dist/extension/permission/guardian.d.ts +20 -0
  29. package/dist/extension/permission/guardian.js +16 -1
  30. package/dist/extension/pipeline/goCommand.d.ts +8 -0
  31. package/dist/extension/pipeline/goCommand.js +8 -0
  32. package/dist/extension/pipeline/invocation.d.ts +7 -0
  33. package/dist/extension/pipeline/invocation.js +7 -0
  34. package/dist/extension/pipeline/personas.js +4 -4
  35. package/dist/extension/pipeline/runner.d.ts +1 -0
  36. package/dist/extension/pipeline/runner.js +15 -3
  37. package/dist/extension/pipeline/sessionWorktree.d.ts +64 -0
  38. package/dist/extension/pipeline/sessionWorktree.js +225 -0
  39. package/dist/extension/scratchpad.d.ts +66 -0
  40. package/dist/extension/scratchpad.js +93 -0
  41. package/dist/extension/slashCommandFilter.d.ts +30 -0
  42. package/dist/extension/slashCommandFilter.js +89 -0
  43. package/dist/extension/subagents.d.ts +21 -1
  44. package/dist/extension/subagents.js +34 -5
  45. package/dist/extension/todos.d.ts +1 -0
  46. package/dist/extension/todos.js +15 -0
  47. package/dist/extension/toolRuns.d.ts +92 -0
  48. package/dist/extension/toolRuns.js +201 -0
  49. package/dist/extension/webFetchTool.js +2 -0
  50. package/dist/extension/workingLine.d.ts +49 -0
  51. package/dist/extension/workingLine.js +116 -0
  52. package/dist/feedback.d.ts +77 -0
  53. package/dist/feedback.js +500 -0
  54. package/dist/goHeadless.d.ts +3 -0
  55. package/dist/goHeadless.js +13 -0
  56. package/dist/launch.d.ts +8 -0
  57. package/dist/launch.js +6 -0
  58. package/dist/otel.d.ts +150 -0
  59. package/dist/otel.js +291 -0
  60. package/dist/outputFormat.d.ts +83 -0
  61. package/dist/outputFormat.js +207 -0
  62. package/dist/paths.d.ts +10 -0
  63. package/dist/paths.js +13 -0
  64. package/dist/worktreeArgs.d.ts +43 -0
  65. package/dist/worktreeArgs.js +96 -0
  66. package/package.json +3 -2
@@ -19,6 +19,8 @@
19
19
  */
20
20
  import { type ExtensionAPI, type ExtensionContext } from "@earendil-works/pi-coding-agent";
21
21
  import { Type } from "typebox";
22
+ import type { ChildUsageHandle } from "./childUsage.js";
23
+ import type { WorkingLineHandle } from "./workingLine.js";
22
24
  import { runStage } from "./pipeline/runner.js";
23
25
  import { type ModelTier, type PipelineStage } from "./pipeline/types.js";
24
26
  import { renderSubagentCall, renderSubagentResult } from "./subagentRender.js";
@@ -96,6 +98,19 @@ export interface MakeSubagentToolDeps {
96
98
  homeDir?: string;
97
99
  /** Live ultra-mode probe (/ultra): widens the per-call fan-out ceiling. */
98
100
  isUltra?: () => boolean;
101
+ /**
102
+ * The session working-line manager (workingLine.ts). When present, live
103
+ * progress goes through it (so the elapsed/token suffix survives); absent,
104
+ * the tool falls back to setting ui.setWorkingMessage directly.
105
+ */
106
+ workingLine?: WorkingLineHandle;
107
+ /**
108
+ * Session child-usage accumulator (childUsage.ts). Each completed task's
109
+ * folded usage is recorded so the footer's session totals include the
110
+ * child spend the per-task receipt already prints. Absent = not wired
111
+ * (headless/eval harnesses): no recording, nothing else changes.
112
+ */
113
+ childUsage?: ChildUsageHandle;
99
114
  }
100
115
  export declare function makeSubagentTool(deps?: MakeSubagentToolDeps): {
101
116
  name: string;
@@ -110,9 +125,10 @@ export declare function makeSubagentTool(deps?: MakeSubagentToolDeps): {
110
125
  agent: Type.TOptional<Type.TString>;
111
126
  }>>>;
112
127
  }>;
128
+ renderShell: "self";
113
129
  renderCall: typeof renderSubagentCall;
114
130
  renderResult: typeof renderSubagentResult;
115
- execute(_toolCallId: string, params: SubagentParams, signal?: AbortSignal, onUpdate?: (update: {
131
+ execute(toolCallId: string, params: SubagentParams, signal?: AbortSignal, onUpdate?: (update: {
116
132
  content: Array<{
117
133
  type: "text";
118
134
  text: string;
@@ -141,6 +157,10 @@ export interface RegisterSubagentsDeps {
141
157
  homeDir?: string;
142
158
  /** Live ultra-mode probe (/ultra): widens the per-call fan-out ceiling. */
143
159
  isUltra?: () => boolean;
160
+ /** Session working-line manager; see MakeSubagentToolDeps.workingLine. */
161
+ workingLine?: WorkingLineHandle;
162
+ /** Session child-usage accumulator; see MakeSubagentToolDeps.childUsage. */
163
+ childUsage?: ChildUsageHandle;
144
164
  }
145
165
  /** Wire the subagent tool and the /agents listing command. */
146
166
  export declare function registerSubagents(pi: ExtensionAPI, deps?: RegisterSubagentsDeps): void;
@@ -33,6 +33,17 @@ import { applyChildEvent, finalizeTask, formatWorkingMessage, newTaskProgress, p
33
33
  * within the model proxy's 64-char caller-label limit.
34
34
  */
35
35
  const SUBAGENT_CALLER_PREFIX = "subagent:";
36
+ /**
37
+ * Dedupe key for one task's childUsage record. Neither `startedAt` (parallel
38
+ * tasks on the same agent are minted in the same `resolved.map` pass and can
39
+ * share a millisecond) nor the per-invocation index (every invocation starts
40
+ * at 0) is unique alone; the pair is, because one tool call's task set is
41
+ * minted exactly once. A re-fired finalize for the same record lands on the
42
+ * same key, so the accumulator's (source, key) dedupe makes it a no-op.
43
+ */
44
+ function progressKey(toolCallId, p, index) {
45
+ return `${toolCallId}/${index}/${p.agent}@${p.startedAt}`;
46
+ }
36
47
  export const SUBAGENT_TOOL_NAME = "subagent";
37
48
  export const GENERAL_AGENT_NAME = "general";
38
49
  export const MAX_PARALLEL_SUBAGENTS = 4;
@@ -74,7 +85,7 @@ You are grounded in how THIS company works: call ask_yagni before inferring a co
74
85
 
75
86
  Never fabricate file paths, contents, or findings. If you cannot find something, say so.
76
87
 
77
- Your final message is your report back to the driving agent, which has NOT seen what you read or did. Make it compressed and complete: what you did, what you found, exact file paths and key excerpts, and anything the driver must know before continuing.`;
88
+ Complete the task fully — do not gold-plate, but do not leave it half-done. Your final message is your report back to the driving agent, which has NOT seen what you read or did: make it a concise report of what was done and the key findings, since the caller relays it to the user and it only needs the essentials. Cover what you did, what you found, exact file paths and key excerpts, and anything the driver must know before continuing.`;
78
89
  const GENERAL_AGENT = {
79
90
  name: GENERAL_AGENT_NAME,
80
91
  description: "General-purpose agent for research, multi-file changes, and self-contained tasks.",
@@ -94,6 +105,8 @@ one you actually read with a tool. If you cannot find something, say "not
94
105
  found" — a plausible-sounding invention is worse than no answer because the
95
106
  driving agent trusts your report.
96
107
 
108
+ Complete the task fully — do not gold-plate, but do not leave it half-done.
109
+
97
110
  Your final message is your report back to the driving agent, which has NOT
98
111
  seen what you read. Make it compressed and complete: exact file paths, the
99
112
  key excerpts, and a one-paragraph map of how the pieces relate. Say what you
@@ -122,6 +135,8 @@ convention, an ownership rule, or anything organization-specific.
122
135
  Never fabricate file paths or results. Report what you actually did and what
123
136
  you actually found.
124
137
 
138
+ Complete the task fully — do not gold-plate, but do not leave it half-done.
139
+
125
140
  Your final message is your report back to the driving agent, which has NOT
126
141
  seen what you did. List every file you touched, what changed in each, the
127
142
  commands you ran with their outcomes, and anything you deliberately left
@@ -154,6 +169,8 @@ a convention, an ownership rule, or anything organization-specific.
154
169
  Never fabricate file paths or findings. If you could not verify something,
155
170
  say exactly what you tried and why you could not.
156
171
 
172
+ Complete the task fully — do not gold-plate, but do not leave it half-done.
173
+
157
174
  Your final message is your verdict back to the driving agent, which has NOT
158
175
  seen what you read. Format:
159
176
  ## Verdict
@@ -344,9 +361,11 @@ export function makeSubagentTool(deps = {}) {
344
361
  "(list them with /agents); omit `agent` for the general-purpose one.",
345
362
  promptSnippet: "subagent: delegate a self-contained task (or parallel tasks) to a fresh-context agent; returns its report.",
346
363
  parameters,
364
+ // Self-framed: the condensed transcript look has no tinted tool boxes.
365
+ renderShell: "self",
347
366
  renderCall: renderSubagentCall,
348
367
  renderResult: renderSubagentResult,
349
- async execute(_toolCallId, params, signal, onUpdate, ctx) {
368
+ async execute(toolCallId, params, signal, onUpdate, ctx) {
350
369
  const fail = (text) => ({
351
370
  content: [{ type: "text", text }],
352
371
  details: {},
@@ -391,9 +410,12 @@ export function makeSubagentTool(deps = {}) {
391
410
  },
392
411
  });
393
412
  const working = formatWorkingMessage(progresses, now);
394
- if (ui && working !== lastWorking) {
413
+ if (working !== lastWorking) {
395
414
  lastWorking = working;
396
- ui.setWorkingMessage?.(working);
415
+ if (deps.workingLine)
416
+ deps.workingLine.setActivity(working);
417
+ else
418
+ ui?.setWorkingMessage?.(working);
397
419
  }
398
420
  };
399
421
  emit();
@@ -417,13 +439,20 @@ export function makeSubagentTool(deps = {}) {
417
439
  },
418
440
  });
419
441
  finalizeTask(progress, result, Date.now());
442
+ // Record the child's spend into the session accumulator so the
443
+ // footer totals include it (dedupe key: this progress record's
444
+ // identity — per invocation, so parallel tasks never collide).
445
+ deps.childUsage?.record("subagent", progressKey(toolCallId, progress, index), result.usage);
420
446
  emit();
421
447
  return { agent: def.name, task, result };
422
448
  }));
423
449
  }
424
450
  finally {
425
451
  // Restore the default "Working…" text whether we resolved or threw.
426
- ui?.setWorkingMessage?.();
452
+ if (deps.workingLine)
453
+ deps.workingLine.setActivity(undefined);
454
+ else
455
+ ui?.setWorkingMessage?.();
427
456
  }
428
457
  const allFailed = outcomes.every((o) => o.result.exitCode !== 0);
429
458
  const sections = outcomes.map((o) => {
@@ -105,6 +105,7 @@ export declare function makeTodoTool(get: () => TodoItem[], set: (todos: TodoIte
105
105
  label: string;
106
106
  description: string;
107
107
  promptSnippet: string;
108
+ promptGuidelines: string[];
108
109
  parameters: Type.TObject<{
109
110
  todos: Type.TArray<Type.TObject<{
110
111
  text: Type.TString;
@@ -204,6 +204,21 @@ export function makeTodoTool(get, set) {
204
204
  "in_progress at a time, mark items completed the moment they are done, and add newly " +
205
205
  "discovered steps as pending. Use it for any task with three or more steps, updating as you go.",
206
206
  promptSnippet: "todo_write: keep a user-visible checklist for multi-step work (full-list replacement).",
207
+ promptGuidelines: [
208
+ "Use todo_write proactively when a task needs 3 or more distinct steps, requires careful " +
209
+ "planning, or the user gives you a list of things (numbered or comma-separated).",
210
+ "Capture new instructions as todos the moment you receive them, and mark a step in_progress " +
211
+ "BEFORE you start working on it.",
212
+ "When in doubt, use it — a visible checklist answers \"is it stuck?\" without the user having " +
213
+ "to interrupt.",
214
+ "Skip it when there is only one straightforward task, the work is trivial, or the request is " +
215
+ "purely conversational or informational — in those cases just do the task directly.",
216
+ "Pass the FULL list every call; it replaces the previous one. Keep exactly ONE item in_progress " +
217
+ "at a time.",
218
+ "Mark a step completed the moment it is done (do not batch completions), and add newly " +
219
+ "discovered steps as pending. Only mark a step completed when it is fully done — if tests " +
220
+ "fail or work is partial, leave it in_progress and add a new step for the blocker.",
221
+ ],
207
222
  parameters,
208
223
  async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
209
224
  const normalized = normalizeTodos(params.todos);
@@ -0,0 +1,92 @@
1
+ /**
2
+ * Run model for the condensed tool transcript (Claude Code-style).
3
+ *
4
+ * A "run" is a maximal stretch of consecutive QUIET tool rows (reads, searches,
5
+ * listings, successful shell commands, scratchpad edits) uninterrupted by an
6
+ * assistant/user message or a visible row (errors, project writes/edits, image
7
+ * reads). Collapsed, every row in a completed run renders zero lines except the
8
+ * run's tail, which paints one summary line: "Read 2 files, ran 2 shell
9
+ * commands". Expanded (ctrl+o) bypasses this model entirely.
10
+ *
11
+ * The tracker is built purely from render calls (idempotent upserts keyed by
12
+ * toolCallId, in first-render order, which matches display order both live and
13
+ * on session replay). Message boundaries arrive via {@link ToolRunTracker.markBreak}
14
+ * from live `message_start` events only — after a resume, runs that were
15
+ * separated by prose may merge into one summary. That is a deliberate trade:
16
+ * render-derived state is the only state that survives replay.
17
+ *
18
+ * Everything here is PURE (no pi imports) so tests run against plain objects;
19
+ * the pi wiring lives in condensedTools.ts.
20
+ */
21
+ /** Aggregation category of one tool row. */
22
+ export type RowKind = "read" | "shell" | "search" | "list" | "write" | "edit";
23
+ /** Facts about one tool row; `quiet` is derived, never stored. */
24
+ export interface ToolRow {
25
+ id: string;
26
+ kind: RowKind;
27
+ /** The row's target path lives under the session scratchpad dir. */
28
+ scratchpad: boolean;
29
+ /** The tool call errored (visible regardless of kind). */
30
+ error: boolean;
31
+ /** The result carries inline images (a read screenshot must stay visible). */
32
+ images: boolean;
33
+ /** The result is final (not partial/streaming). */
34
+ final: boolean;
35
+ /** Added lines (scratchpad edits surface as "+N" in the summary). */
36
+ added: number;
37
+ /** An assistant/user message landed between the previous row and this one. */
38
+ breakBefore: boolean;
39
+ /** Repaint hook for this row's component (captured from the render context). */
40
+ invalidate?: () => void;
41
+ /** A deferred repaint is already queued for this row. */
42
+ invalidatePending?: boolean;
43
+ }
44
+ export type ToolRowPatch = Partial<Pick<ToolRow, "kind" | "scratchpad" | "error" | "images" | "final" | "added" | "invalidate">>;
45
+ /** Aggregation kind for a built-in tool name (undefined for non-built-ins). */
46
+ export declare function kindForTool(toolName: string): RowKind | undefined;
47
+ /**
48
+ * Whether a row folds into a run summary. Reads, searches, listings, and
49
+ * successful shell commands always do; writes/edits only when they target the
50
+ * scratchpad (project mutations must stay visible). Errors and image-bearing
51
+ * results are always visible.
52
+ */
53
+ export declare function isQuiet(row: Pick<ToolRow, "kind" | "scratchpad" | "error" | "images">): boolean;
54
+ /**
55
+ * One summary line for a completed run, phrases in first-occurrence order:
56
+ * "Read 2 files, ran 2 shell commands, made 1 scratchpad edit +20".
57
+ */
58
+ export declare function summarizeRun(rows: readonly Pick<ToolRow, "kind" | "scratchpad" | "added">[]): string;
59
+ /**
60
+ * Ordered row registry. Upserts are idempotent and diff-aware: only a change
61
+ * invalidates the affected run's tail (the one component whose output depends
62
+ * on neighbors), so repaints converge instead of looping.
63
+ */
64
+ export declare class ToolRunTracker {
65
+ private rows;
66
+ private indexById;
67
+ private breakPending;
68
+ /** Record an assistant/user message boundary; the next new row starts a fresh run. */
69
+ markBreak(): void;
70
+ get(id: string): ToolRow | undefined;
71
+ upsert(id: string, patch: ToolRowPatch): ToolRow;
72
+ /**
73
+ * The one summary line for `id`, present only when `id` is the tail of a
74
+ * fully-final quiet run. Every other member of the run gets undefined.
75
+ */
76
+ summaryFor(id: string): string | undefined;
77
+ /** The contiguous quiet run containing `row` (just `[row]` when visible). */
78
+ private runOf;
79
+ private invalidateTailOf;
80
+ /**
81
+ * Defer a row's repaint to a microtask, deduped per row. Upserts run INSIDE
82
+ * pi's synchronous `updateDisplay` pass (renderers call them), and the
83
+ * component's `invalidate()` re-enters `updateDisplay` immediately — a
84
+ * synchronous call from a renderer would rebuild the container while the
85
+ * outer frame is still appending to it, stacking duplicate children.
86
+ * Deferring means every repaint runs as its own clean top-level pass; it
87
+ * still cannot loop, because the diff-aware upsert only schedules on an
88
+ * actual change.
89
+ */
90
+ private scheduleInvalidate;
91
+ }
92
+ //# sourceMappingURL=toolRuns.d.ts.map
@@ -0,0 +1,201 @@
1
+ /**
2
+ * Run model for the condensed tool transcript (Claude Code-style).
3
+ *
4
+ * A "run" is a maximal stretch of consecutive QUIET tool rows (reads, searches,
5
+ * listings, successful shell commands, scratchpad edits) uninterrupted by an
6
+ * assistant/user message or a visible row (errors, project writes/edits, image
7
+ * reads). Collapsed, every row in a completed run renders zero lines except the
8
+ * run's tail, which paints one summary line: "Read 2 files, ran 2 shell
9
+ * commands". Expanded (ctrl+o) bypasses this model entirely.
10
+ *
11
+ * The tracker is built purely from render calls (idempotent upserts keyed by
12
+ * toolCallId, in first-render order, which matches display order both live and
13
+ * on session replay). Message boundaries arrive via {@link ToolRunTracker.markBreak}
14
+ * from live `message_start` events only — after a resume, runs that were
15
+ * separated by prose may merge into one summary. That is a deliberate trade:
16
+ * render-derived state is the only state that survives replay.
17
+ *
18
+ * Everything here is PURE (no pi imports) so tests run against plain objects;
19
+ * the pi wiring lives in condensedTools.ts.
20
+ */
21
+ const KIND_BY_TOOL = {
22
+ read: "read",
23
+ bash: "shell",
24
+ grep: "search",
25
+ find: "list",
26
+ ls: "list",
27
+ write: "write",
28
+ edit: "edit",
29
+ };
30
+ /** Aggregation kind for a built-in tool name (undefined for non-built-ins). */
31
+ export function kindForTool(toolName) {
32
+ return KIND_BY_TOOL[toolName];
33
+ }
34
+ /**
35
+ * Whether a row folds into a run summary. Reads, searches, listings, and
36
+ * successful shell commands always do; writes/edits only when they target the
37
+ * scratchpad (project mutations must stay visible). Errors and image-bearing
38
+ * results are always visible.
39
+ */
40
+ export function isQuiet(row) {
41
+ if (row.error || row.images)
42
+ return false;
43
+ if (row.kind === "write" || row.kind === "edit")
44
+ return row.scratchpad;
45
+ return true;
46
+ }
47
+ function plural(n, singular, pluralForm = `${singular}s`) {
48
+ return n === 1 ? singular : pluralForm;
49
+ }
50
+ /**
51
+ * One summary line for a completed run, phrases in first-occurrence order:
52
+ * "Read 2 files, ran 2 shell commands, made 1 scratchpad edit +20".
53
+ */
54
+ export function summarizeRun(rows) {
55
+ const order = [];
56
+ const counts = new Map();
57
+ for (const row of rows) {
58
+ const category = row.kind === "write" || row.kind === "edit" ? "scratch" : row.kind;
59
+ let entry = counts.get(category);
60
+ if (!entry) {
61
+ entry = { n: 0, added: 0 };
62
+ counts.set(category, entry);
63
+ order.push(category);
64
+ }
65
+ entry.n += 1;
66
+ entry.added += row.added;
67
+ }
68
+ const phrases = order.map((category) => {
69
+ const { n, added } = counts.get(category);
70
+ switch (category) {
71
+ case "read":
72
+ return `read ${n} ${plural(n, "file")}`;
73
+ case "shell":
74
+ return `ran ${n} shell ${plural(n, "command")}`;
75
+ case "search":
76
+ return `searched for ${n} ${plural(n, "pattern")}`;
77
+ case "list":
78
+ return `listed ${n} ${plural(n, "path")}`;
79
+ default:
80
+ return `made ${n} scratchpad ${plural(n, "edit")}${added > 0 ? ` +${added}` : ""}`;
81
+ }
82
+ });
83
+ const line = phrases.join(", ");
84
+ return line.charAt(0).toUpperCase() + line.slice(1);
85
+ }
86
+ /**
87
+ * Ordered row registry. Upserts are idempotent and diff-aware: only a change
88
+ * invalidates the affected run's tail (the one component whose output depends
89
+ * on neighbors), so repaints converge instead of looping.
90
+ */
91
+ export class ToolRunTracker {
92
+ rows = [];
93
+ indexById = new Map();
94
+ breakPending = true;
95
+ /** Record an assistant/user message boundary; the next new row starts a fresh run. */
96
+ markBreak() {
97
+ this.breakPending = true;
98
+ }
99
+ get(id) {
100
+ const i = this.indexById.get(id);
101
+ return i === undefined ? undefined : this.rows[i];
102
+ }
103
+ upsert(id, patch) {
104
+ const existingIndex = this.indexById.get(id);
105
+ if (existingIndex === undefined) {
106
+ const row = {
107
+ id,
108
+ kind: patch.kind ?? "shell",
109
+ scratchpad: patch.scratchpad ?? false,
110
+ error: patch.error ?? false,
111
+ images: patch.images ?? false,
112
+ final: patch.final ?? false,
113
+ added: patch.added ?? 0,
114
+ breakBefore: this.breakPending,
115
+ ...(patch.invalidate ? { invalidate: patch.invalidate } : {}),
116
+ };
117
+ this.breakPending = false;
118
+ this.indexById.set(id, this.rows.length);
119
+ this.rows.push(row);
120
+ // The previous row may have been its run's tail (painting a summary);
121
+ // now that the run extends past it, repaint it as a hidden member.
122
+ // (invalidateTailOf(prev) would be wrong here: prev's run now includes
123
+ // this new row, so its tail is the row currently painting, not prev.)
124
+ const prev = this.rows[this.rows.length - 2];
125
+ if (prev && isQuiet(prev) && !row.breakBefore)
126
+ this.scheduleInvalidate(prev);
127
+ return row;
128
+ }
129
+ const row = this.rows[existingIndex];
130
+ if (patch.invalidate)
131
+ row.invalidate = patch.invalidate;
132
+ let changed = false;
133
+ for (const key of ["kind", "scratchpad", "error", "images", "final", "added"]) {
134
+ const next = patch[key];
135
+ if (next !== undefined && row[key] !== next) {
136
+ row[key] = next;
137
+ changed = true;
138
+ }
139
+ }
140
+ if (changed)
141
+ this.invalidateTailOf(row);
142
+ return row;
143
+ }
144
+ /**
145
+ * The one summary line for `id`, present only when `id` is the tail of a
146
+ * fully-final quiet run. Every other member of the run gets undefined.
147
+ */
148
+ summaryFor(id) {
149
+ const row = this.get(id);
150
+ if (!row || !isQuiet(row) || !row.final)
151
+ return undefined;
152
+ const run = this.runOf(row);
153
+ if (run[run.length - 1] !== row)
154
+ return undefined;
155
+ if (run.some((r) => !r.final))
156
+ return undefined;
157
+ return summarizeRun(run);
158
+ }
159
+ /** The contiguous quiet run containing `row` (just `[row]` when visible). */
160
+ runOf(row) {
161
+ if (!isQuiet(row))
162
+ return [row];
163
+ const i = this.indexById.get(row.id);
164
+ let start = i;
165
+ while (start > 0 && !this.rows[start].breakBefore && isQuiet(this.rows[start - 1]))
166
+ start--;
167
+ let end = i;
168
+ while (end < this.rows.length - 1 &&
169
+ !this.rows[end + 1].breakBefore &&
170
+ isQuiet(this.rows[end + 1])) {
171
+ end++;
172
+ }
173
+ return this.rows.slice(start, end + 1);
174
+ }
175
+ invalidateTailOf(row) {
176
+ const run = this.runOf(row);
177
+ const tail = run[run.length - 1];
178
+ if (tail)
179
+ this.scheduleInvalidate(tail);
180
+ }
181
+ /**
182
+ * Defer a row's repaint to a microtask, deduped per row. Upserts run INSIDE
183
+ * pi's synchronous `updateDisplay` pass (renderers call them), and the
184
+ * component's `invalidate()` re-enters `updateDisplay` immediately — a
185
+ * synchronous call from a renderer would rebuild the container while the
186
+ * outer frame is still appending to it, stacking duplicate children.
187
+ * Deferring means every repaint runs as its own clean top-level pass; it
188
+ * still cannot loop, because the diff-aware upsert only schedules on an
189
+ * actual change.
190
+ */
191
+ scheduleInvalidate(row) {
192
+ if (row.invalidatePending)
193
+ return;
194
+ row.invalidatePending = true;
195
+ queueMicrotask(() => {
196
+ row.invalidatePending = false;
197
+ row.invalidate?.();
198
+ });
199
+ }
200
+ }
201
+ //# sourceMappingURL=toolRuns.js.map
@@ -44,6 +44,8 @@ export function makeWebFetchTool(opts) {
44
44
  "Use web_fetch to read a URL's content instead of bash + curl when you need a page or a summary of it.",
45
45
  ],
46
46
  parameters,
47
+ // Self-framed: the condensed transcript look has no tinted tool boxes.
48
+ renderShell: "self",
47
49
  renderCall(args, theme) {
48
50
  const t = theme;
49
51
  const url = clipLine(args?.url ?? "…", 80);
@@ -0,0 +1,49 @@
1
+ /**
2
+ * The Claude Code-style streaming status line: `Shaping… (12m 54s · ↓ 47.5k
3
+ * tokens)` in place of pi's static "Working...".
4
+ *
5
+ * One manager owns `ctx.ui.setWorkingMessage` for the whole session so the
6
+ * verb, the elapsed clock, and the token counter never fight the subagent /
7
+ * advisor progress text: those tools publish their activity line through
8
+ * {@link WorkingLineHandle.setActivity} (instead of calling setWorkingMessage
9
+ * directly), and the manager splices it in as the head of the same composed
10
+ * message. Elapsed time spans the whole agent loop (agent_start → agent_end);
11
+ * the token counter accumulates assistant output tokens across the loop's
12
+ * messages (message_end), which is when pi learns usage — subagent tokens live
13
+ * in the subagent's own activity text, not this counter.
14
+ *
15
+ * TUI-only by the agent_start guard; a headless /go child or desktop surface
16
+ * never gets a working line. Everything is fail-soft: a status line must never
17
+ * break a turn.
18
+ */
19
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
20
+ /** What subagents/advisor publish instead of calling setWorkingMessage. */
21
+ export interface WorkingLineHandle {
22
+ setActivity(text?: string): void;
23
+ }
24
+ /** A no-op handle for callers wired without a manager (tests, children). */
25
+ export declare const NULL_WORKING_LINE: WorkingLineHandle;
26
+ /**
27
+ * The verb pool. Neutral gerunds — one is picked per agent loop, so long
28
+ * sessions read as a person at work rather than a stuck spinner.
29
+ */
30
+ export declare const WORKING_VERBS: readonly string[];
31
+ /** Pulse frames for the streaming indicator (pi renders them verbatim). */
32
+ export declare const WORKING_INDICATOR_FRAMES: string[];
33
+ export interface ComposeWorkingOpts {
34
+ /** Override head from a running subagent/advisor (verb used when absent). */
35
+ activity?: string | undefined;
36
+ verb: string;
37
+ elapsedMs: number;
38
+ outputTokens: number;
39
+ }
40
+ /** `Shaping… (12m 54s · ↓ 47.5k tokens)` — pure, exported for tests. */
41
+ export declare function composeWorkingMessage(opts: ComposeWorkingOpts): string;
42
+ export interface RegisterWorkingLineDeps {
43
+ now?: () => number;
44
+ pickVerb?: (verbs: readonly string[]) => string;
45
+ /** Refresh cadence for the elapsed clock. */
46
+ tickMs?: number;
47
+ }
48
+ export declare function registerWorkingLine(pi: ExtensionAPI, deps?: RegisterWorkingLineDeps): WorkingLineHandle;
49
+ //# sourceMappingURL=workingLine.d.ts.map
@@ -0,0 +1,116 @@
1
+ /**
2
+ * The Claude Code-style streaming status line: `Shaping… (12m 54s · ↓ 47.5k
3
+ * tokens)` in place of pi's static "Working...".
4
+ *
5
+ * One manager owns `ctx.ui.setWorkingMessage` for the whole session so the
6
+ * verb, the elapsed clock, and the token counter never fight the subagent /
7
+ * advisor progress text: those tools publish their activity line through
8
+ * {@link WorkingLineHandle.setActivity} (instead of calling setWorkingMessage
9
+ * directly), and the manager splices it in as the head of the same composed
10
+ * message. Elapsed time spans the whole agent loop (agent_start → agent_end);
11
+ * the token counter accumulates assistant output tokens across the loop's
12
+ * messages (message_end), which is when pi learns usage — subagent tokens live
13
+ * in the subagent's own activity text, not this counter.
14
+ *
15
+ * TUI-only by the agent_start guard; a headless /go child or desktop surface
16
+ * never gets a working line. Everything is fail-soft: a status line must never
17
+ * break a turn.
18
+ */
19
+ import { usageFromMessage } from "./costHud.js";
20
+ import { formatDuration, formatTokens } from "./subagentRender.js";
21
+ /** A no-op handle for callers wired without a manager (tests, children). */
22
+ export const NULL_WORKING_LINE = { setActivity: () => { } };
23
+ /**
24
+ * The verb pool. Neutral gerunds — one is picked per agent loop, so long
25
+ * sessions read as a person at work rather than a stuck spinner.
26
+ */
27
+ export const WORKING_VERBS = [
28
+ "Working",
29
+ "Thinking",
30
+ "Exploring",
31
+ "Tracing",
32
+ "Shaping",
33
+ "Wiring",
34
+ "Weighing",
35
+ "Sketching",
36
+ "Assembling",
37
+ "Distilling",
38
+ "Untangling",
39
+ "Polishing",
40
+ ];
41
+ /** Pulse frames for the streaming indicator (pi renders them verbatim). */
42
+ export const WORKING_INDICATOR_FRAMES = ["·", "✢", "✳", "✶", "✳", "✢"];
43
+ /** `Shaping… (12m 54s · ↓ 47.5k tokens)` — pure, exported for tests. */
44
+ export function composeWorkingMessage(opts) {
45
+ const head = opts.activity ?? `${opts.verb}…`;
46
+ const stats = [
47
+ formatDuration(opts.elapsedMs),
48
+ opts.outputTokens > 0 ? `↓ ${formatTokens(opts.outputTokens)} tokens` : undefined,
49
+ ]
50
+ .filter(Boolean)
51
+ .join(" · ");
52
+ return `${head} (${stats})`;
53
+ }
54
+ export function registerWorkingLine(pi, deps = {}) {
55
+ const now = deps.now ?? (() => Date.now());
56
+ const pickVerb = deps.pickVerb ?? ((verbs) => verbs[Math.floor(Math.random() * verbs.length)]);
57
+ const tickMs = deps.tickMs ?? 1_000;
58
+ let ui;
59
+ let timer;
60
+ let startedAt;
61
+ let outputTokens = 0;
62
+ let verb = WORKING_VERBS[0];
63
+ let activity;
64
+ const refresh = () => {
65
+ if (!ui || startedAt === undefined)
66
+ return;
67
+ try {
68
+ ui.setWorkingMessage?.(composeWorkingMessage({ activity, verb, elapsedMs: now() - startedAt, outputTokens }));
69
+ }
70
+ catch {
71
+ // The status line must never break a turn.
72
+ }
73
+ };
74
+ const stop = () => {
75
+ if (timer)
76
+ clearInterval(timer);
77
+ timer = undefined;
78
+ startedAt = undefined;
79
+ activity = undefined;
80
+ try {
81
+ ui?.setWorkingMessage?.();
82
+ }
83
+ catch {
84
+ // Restoring the default label is best-effort.
85
+ }
86
+ };
87
+ pi.on("agent_start", (_event, ctx) => {
88
+ if (ctx.mode !== "tui" || !ctx.hasUI)
89
+ return;
90
+ ui = ctx.ui;
91
+ startedAt = now();
92
+ outputTokens = 0;
93
+ activity = undefined;
94
+ verb = pickVerb(WORKING_VERBS);
95
+ refresh();
96
+ if (timer)
97
+ clearInterval(timer);
98
+ timer = setInterval(refresh, tickMs);
99
+ timer.unref?.();
100
+ });
101
+ pi.on("agent_end", () => stop());
102
+ pi.on("message_end", (event) => {
103
+ const message = event.message;
104
+ if (message?.role !== "assistant")
105
+ return;
106
+ outputTokens += usageFromMessage(message).output;
107
+ refresh();
108
+ });
109
+ return {
110
+ setActivity(text) {
111
+ activity = text;
112
+ refresh();
113
+ },
114
+ };
115
+ }
116
+ //# sourceMappingURL=workingLine.js.map