@yagni-app/code-staging 0.3.0-staging.1085.1 → 0.3.0-staging.1088.1

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.
@@ -26,10 +26,12 @@
26
26
  * this module is the I/O.
27
27
  */
28
28
  import type { ExtensionAPI, ToolDefinition } from "@earendil-works/pi-coding-agent";
29
+ import { type Component } from "@earendil-works/pi-tui";
29
30
  import { Type } from "typebox";
30
31
  import { type AdvisorLimits, type AdvisorStateHandle } from "./advisor.js";
31
32
  import { runStage as defaultRunStage } from "./pipeline/runner.js";
32
33
  import type { PipelineStage } from "./pipeline/types.js";
34
+ import { type RenderTheme, type SubagentTaskProgress } from "./subagentRender.js";
33
35
  /**
34
36
  * Read-only recon plus grounding. Mirrors the `plan` stage's allowlist for the
35
37
  * reason documented there: enough tools to navigate instead of flailing on a
@@ -66,10 +68,18 @@ export declare function buildConsultBrief(params: {
66
68
  * persona, and that is the advisor's own.
67
69
  */
68
70
  export declare function advisorStage(): PipelineStage;
69
- export declare function makeAskAdvisorTool(opts: MakeAskAdvisorToolOptions): ToolDefinition<typeof parameters, {
71
+ /** The ask_advisor result details: consult accounting plus the live progress
72
+ * record the subagent renderers paint (one task, the consult itself). */
73
+ export interface AdvisorToolDetails {
70
74
  consults: number;
71
75
  cost: number;
72
- }>;
76
+ tasks?: SubagentTaskProgress[];
77
+ }
78
+ /** Title painted the moment the ask_advisor call streams in. */
79
+ export declare function renderAdvisorCall(args: {
80
+ question?: string;
81
+ } | undefined, theme: RenderTheme, _context: unknown): Component;
82
+ export declare function makeAskAdvisorTool(opts: MakeAskAdvisorToolOptions): ToolDefinition<typeof parameters, AdvisorToolDetails>;
73
83
  /**
74
84
  * `/advise <question>` — the manual escalation lever.
75
85
  *
@@ -77,6 +87,11 @@ export declare function makeAskAdvisorTool(opts: MakeAskAdvisorToolOptions): Too
77
87
  * consult draws on the same cap rather than opening a side channel around it.
78
88
  * The advice is sent into the conversation (like /go-compare's report) so the
79
89
  * driver sees it in context and can act on it.
90
+ *
91
+ * A command has no tool row for pi to render, so the live view is an
92
+ * aboveEditor widget painted from the tool's partial updates: the same
93
+ * two-line `agent — task` / `↳ current tool` status the subagent tool shows,
94
+ * driven by a spinner ticker so it visibly moves between child events.
80
95
  */
81
96
  export declare function registerAdviseCommand(pi: ExtensionAPI, tool: ReturnType<typeof makeAskAdvisorTool>): void;
82
97
  export {};
@@ -25,9 +25,12 @@
25
25
  * The gate, the cap and the spend ceiling live in `advisor.ts` and are pure;
26
26
  * this module is the I/O.
27
27
  */
28
+ import { Text } from "@earendil-works/pi-tui";
28
29
  import { Type } from "typebox";
29
30
  import { ADVISOR_MODEL_TIER, DEFAULT_ADVISOR_LIMITS, decideConsult, formatConsultCost, } from "./advisor.js";
31
+ import { SPINNER_FRAMES } from "./pipeline/activityFeed.js";
30
32
  import { runStage as defaultRunStage } from "./pipeline/runner.js";
33
+ import { applyChildEvent, finalizeTask, formatWorkingMessage, newTaskProgress, progressSummaryText, receiptLine, renderSubagentResult, runningLines, } from "./subagentRender.js";
31
34
  /**
32
35
  * Read-only recon plus grounding. Mirrors the `plan` stage's allowlist for the
33
36
  * reason documented there: enough tools to navigate instead of flailing on a
@@ -71,6 +74,24 @@ export function advisorStage() {
71
74
  taskTemplate: "{ticket}",
72
75
  };
73
76
  }
77
+ /** Spinner cadence for the advisor's live views; matches subagentRender. */
78
+ const SPINNER_TICK_MS = 120;
79
+ /** Question preview width on the call title line. */
80
+ const QUESTION_PREVIEW_MAX = 128;
81
+ function clip(text, max) {
82
+ const collapsed = text.replace(/\s+/g, " ").trim();
83
+ if (collapsed.length <= max)
84
+ return collapsed;
85
+ return `${collapsed.slice(0, max - 1)}…`;
86
+ }
87
+ /** Title painted the moment the ask_advisor call streams in. */
88
+ export function renderAdvisorCall(args, theme, _context) {
89
+ const title = theme.fg("toolTitle", theme.bold("advisor"));
90
+ let text = `${title} ${theme.fg("accent", "peak-tier consult")}`;
91
+ if (args?.question)
92
+ text += `\n ${theme.fg("dim", clip(args.question, QUESTION_PREVIEW_MAX))}`;
93
+ return new Text(text, 0, 0);
94
+ }
74
95
  export function makeAskAdvisorTool(opts) {
75
96
  const limits = opts.limits ?? DEFAULT_ADVISOR_LIMITS;
76
97
  const runStage = opts.runStage ?? defaultRunStage;
@@ -95,6 +116,8 @@ export function makeAskAdvisorTool(opts) {
95
116
  "The advice comes back as plain text: act on it, and call record_decision when it settles a product-intent call so the next agent inherits it.",
96
117
  ],
97
118
  parameters,
119
+ renderCall: renderAdvisorCall,
120
+ renderResult: renderSubagentResult,
98
121
  async execute(_toolCallId, params, signal, onUpdate, ctx) {
99
122
  // Read the LIVE session model: pi's picker can change it after this tool
100
123
  // was registered, in both directions.
@@ -109,19 +132,51 @@ export function makeAskAdvisorTool(opts) {
109
132
  details: { consults: opts.state.read().consults, cost: 0 },
110
133
  };
111
134
  }
112
- onUpdate?.({
113
- content: [{ type: "text", text: "Consulting the advisor…" }],
114
- details: { consults: opts.state.read().consults, cost: 0 },
115
- });
116
- const result = await runStage(advisorStage(), { ticket: buildConsultBrief(params) }, {
117
- cwd: ctx?.cwd ?? process.cwd(),
118
- ...(signal ? { signal } : {}),
119
- // YAG-471: attribute the consult's completions to the advisor, not
120
- // the "plan" stage id advisorStage() borrows (see its docblock).
121
- callerLabel: "advisor",
122
- });
135
+ // Live progress, exactly the subagent tool's shape: the folded record
136
+ // rides every partial update's `details.tasks` (painted by
137
+ // renderSubagentResult), the plain-text summary rides `content` for
138
+ // headless consumers, and the harness "Working…" line mirrors it.
139
+ const progress = newTaskProgress("advisor", params.question, Date.now());
140
+ const ui = ctx?.hasUI ? ctx.ui : undefined;
141
+ let lastWorking;
142
+ const emit = () => {
143
+ const now = Date.now();
144
+ onUpdate?.({
145
+ content: [{ type: "text", text: progressSummaryText([progress], now) }],
146
+ details: {
147
+ consults: opts.state.read().consults,
148
+ cost: progress.usage.cost,
149
+ tasks: [{ ...progress, actions: [...progress.actions], usage: { ...progress.usage } }],
150
+ },
151
+ });
152
+ const working = formatWorkingMessage([progress], now);
153
+ if (ui && working !== lastWorking) {
154
+ lastWorking = working;
155
+ ui.setWorkingMessage?.(working);
156
+ }
157
+ };
158
+ emit();
159
+ let result;
160
+ try {
161
+ result = await runStage(advisorStage(), { ticket: buildConsultBrief(params) }, {
162
+ cwd: ctx?.cwd ?? process.cwd(),
163
+ ...(signal ? { signal } : {}),
164
+ // YAG-471: attribute the consult's completions to the advisor, not
165
+ // the "plan" stage id advisorStage() borrows (see its docblock).
166
+ callerLabel: "advisor",
167
+ onEvent: (ev) => {
168
+ if (applyChildEvent(progress, ev))
169
+ emit();
170
+ },
171
+ });
172
+ }
173
+ finally {
174
+ // Restore the default "Working…" text whether we resolved or threw.
175
+ ui?.setWorkingMessage?.();
176
+ }
123
177
  const cost = result.usage?.cost ?? 0;
124
178
  const state = opts.state.record(cost);
179
+ finalizeTask(progress, result, Date.now());
125
180
  if (result.exitCode !== 0 && !result.finalOutput.trim()) {
126
181
  // Fail honestly rather than returning an empty recommendation. The
127
182
  // consult still counts: it spawned, and it may well have spent.
@@ -134,7 +189,7 @@ export function makeAskAdvisorTool(opts) {
134
189
  `Decide this one yourself.`,
135
190
  },
136
191
  ],
137
- details: { consults: state.consults, cost },
192
+ details: { consults: state.consults, cost, tasks: [progress] },
138
193
  };
139
194
  }
140
195
  return {
@@ -144,11 +199,13 @@ export function makeAskAdvisorTool(opts) {
144
199
  text: `${result.finalOutput.trim()}\n\n${formatConsultCost({ cost, consults: state.consults, limits })}`,
145
200
  },
146
201
  ],
147
- details: { consults: state.consults, cost },
202
+ details: { consults: state.consults, cost, tasks: [progress] },
148
203
  };
149
204
  },
150
205
  };
151
206
  }
207
+ /** The widget key the /advise live panel paints under. */
208
+ const ADVISE_WIDGET_KEY = "yagni-advise";
152
209
  /**
153
210
  * `/advise <question>` — the manual escalation lever.
154
211
  *
@@ -156,6 +213,11 @@ export function makeAskAdvisorTool(opts) {
156
213
  * consult draws on the same cap rather than opening a side channel around it.
157
214
  * The advice is sent into the conversation (like /go-compare's report) so the
158
215
  * driver sees it in context and can act on it.
216
+ *
217
+ * A command has no tool row for pi to render, so the live view is an
218
+ * aboveEditor widget painted from the tool's partial updates: the same
219
+ * two-line `agent — task` / `↳ current tool` status the subagent tool shows,
220
+ * driven by a spinner ticker so it visibly moves between child events.
159
221
  */
160
222
  export function registerAdviseCommand(pi, tool) {
161
223
  pi.registerCommand("advise", {
@@ -175,8 +237,41 @@ export function registerAdviseCommand(pi, tool) {
175
237
  return;
176
238
  }
177
239
  notify("Consulting the peak-tier advisor. This reads the repo, so it takes a moment.", "info");
240
+ // Live panel state: the newest folded progress record from the tool's
241
+ // partial updates, repainted on a spinner tick until the consult ends.
242
+ let progress;
243
+ let paintTimer;
244
+ const theme = ctx.hasUI ? ctx.ui.theme : undefined;
245
+ const paint = () => {
246
+ if (!ctx.hasUI || !theme || !progress)
247
+ return;
248
+ const now = Date.now();
249
+ const lines = progress.status === "running"
250
+ ? runningLines(progress, theme, now, SPINNER_FRAMES[Math.floor(now / SPINNER_TICK_MS) % SPINNER_FRAMES.length])
251
+ : [receiptLine(progress, theme)];
252
+ ctx.ui.setWidget?.(ADVISE_WIDGET_KEY, lines, { placement: "aboveEditor" });
253
+ };
254
+ const onUpdate = (update) => {
255
+ const task = update.details?.tasks?.[0];
256
+ if (!task)
257
+ return;
258
+ progress = task;
259
+ if (ctx.hasUI && theme && !paintTimer) {
260
+ paintTimer = setInterval(paint, SPINNER_TICK_MS);
261
+ paintTimer.unref?.();
262
+ }
263
+ paint();
264
+ };
265
+ const clearPanel = () => {
266
+ if (paintTimer) {
267
+ clearInterval(paintTimer);
268
+ paintTimer = undefined;
269
+ }
270
+ if (ctx.hasUI)
271
+ ctx.ui.setWidget?.(ADVISE_WIDGET_KEY, undefined);
272
+ };
178
273
  try {
179
- const out = await tool.execute("advise", { question }, ctx.signal, undefined, ctx);
274
+ const out = await tool.execute("advise", { question }, ctx.signal, onUpdate, ctx);
180
275
  const text = out.content
181
276
  .map((c) => c.text ?? "")
182
277
  .join("\n")
@@ -188,6 +283,9 @@ export function registerAdviseCommand(pi, tool) {
188
283
  notify(`/advise failed: ${message}`, "error");
189
284
  await pi.sendUserMessage(`/advise failed: ${message}`);
190
285
  }
286
+ finally {
287
+ clearPanel();
288
+ }
191
289
  },
192
290
  });
193
291
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yagni-app/code-staging",
3
- "version": "0.3.0-staging.1085.1",
3
+ "version": "0.3.0-staging.1088.1",
4
4
  "description": "YAGNI Code: a terminal coding agent that already knows your company. One YAGNI login routes the model and grounds the agent in your team's context.",
5
5
  "license": "SEE LICENSE IN LICENSE.md",
6
6
  "author": "YAGNI, Inc. <jack@yagni.app> (https://yagni.app)",
@@ -38,5 +38,5 @@
38
38
  "@earendil-works/pi-tui": "0.84.1",
39
39
  "typebox": "^1.3.11"
40
40
  },
41
- "yagniSourceSha": "2af1762b42b06fc1e55b7f85fbbed71ce3bd3272"
41
+ "yagniSourceSha": "eebceee1a0d1673dd4b2755942aa027a89f18611"
42
42
  }