@danypops/pi-papyrus 0.43.0 → 0.43.3

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 (37) hide show
  1. package/extension/src/artifact-browser.ts +54 -31
  2. package/extension/src/artifact-detail-view.ts +52 -42
  3. package/extension/src/artifact-format.ts +11 -5
  4. package/extension/src/artifact-relationship-lines.ts +4 -5
  5. package/extension/src/beautiful-mermaid-renderer.ts +3 -5
  6. package/extension/src/context-budget.ts +8 -5
  7. package/extension/src/context-hub-contribution.ts +6 -2
  8. package/extension/src/context-injection-telemetry.ts +2 -2
  9. package/extension/src/discuss-ask-layout.ts +3 -1
  10. package/extension/src/discuss-ask-view.ts +437 -111
  11. package/extension/src/discuss.ts +64 -15
  12. package/extension/src/discussion-detail-view.ts +44 -22
  13. package/extension/src/docs.ts +3 -2
  14. package/extension/src/domain-tools.ts +79 -34
  15. package/extension/src/index.ts +170 -66
  16. package/extension/src/markdown.ts +3 -7
  17. package/extension/src/note-widget.ts +1 -1
  18. package/extension/src/notes.ts +3 -8
  19. package/extension/src/playbook-bridge.ts +17 -6
  20. package/extension/src/playbooks.ts +17 -7
  21. package/extension/src/rules.ts +5 -5
  22. package/extension/src/service-client.ts +23 -8
  23. package/extension/src/skill-catalog-footprint.ts +1 -1
  24. package/extension/src/task-detail-format.ts +9 -9
  25. package/extension/src/task-detail-view.ts +36 -29
  26. package/extension/src/task-focus-events.ts +3 -2
  27. package/extension/src/task-graph.ts +16 -12
  28. package/extension/src/task-presentation.ts +2 -6
  29. package/extension/src/task-widget.ts +12 -8
  30. package/extension/src/tasks.ts +148 -57
  31. package/extension/src/tool-rendering/artifact-card.ts +1 -4
  32. package/extension/src/tool-rendering/artifact-list.ts +23 -24
  33. package/extension/src/tool-rendering/index.ts +2 -6
  34. package/extension/src/tool-rendering/render-model.ts +69 -55
  35. package/extension/src/vehicle-artifact-renderers.ts +58 -0
  36. package/extension/src/vehicle-notes-client.ts +35 -7
  37. package/package.json +5 -5
@@ -5,19 +5,20 @@
5
5
  */
6
6
  import type { ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
7
7
  import { DynamicBorder, rawKeyHint } from "@earendil-works/pi-coding-agent";
8
- import { Container, Input, Spacer, matchesKey, truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
8
+ import { Container, Input, matchesKey, Spacer, truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
9
9
  import { callService } from "./service-client.ts";
10
- import { emitTaskFocusEvent } from "./task-focus-events.ts";
11
10
  import { sessionSecretField } from "./session-identity.ts";
12
11
  import { showTaskDetails } from "./task-detail-view.ts";
12
+ import { emitTaskFocusEvent } from "./task-focus-events.ts";
13
13
  import { showTaskGraph } from "./task-graph.ts";
14
14
 
15
15
  export { taskDetailsText } from "./task-detail-format.ts";
16
16
  export { showTaskDetails } from "./task-detail-view.ts";
17
+
17
18
  import {
18
- projectTaskExecution,
19
19
  type Artifact,
20
20
  type GateResult,
21
+ projectTaskExecution,
21
22
  type TaskCompletion,
22
23
  type TaskGraph,
23
24
  type TaskHistoryPage,
@@ -39,7 +40,7 @@ type TaskRow = Artifact;
39
40
  function taskChoiceLabels(tasks: readonly Artifact[]): string[] {
40
41
  const titleCounts = new Map<string, number>();
41
42
  for (const task of tasks) titleCounts.set(task.title, (titleCounts.get(task.title) ?? 0) + 1);
42
- return tasks.map((task) => titleCounts.get(task.title)! > 1 ? `${task.title} (${task.id})` : task.title);
43
+ return tasks.map((task) => (titleCounts.get(task.title)! > 1 ? `${task.title} (${task.id})` : task.title));
43
44
  }
44
45
 
45
46
  export interface TaskHierarchyRow {
@@ -60,7 +61,13 @@ export function buildTaskHierarchy(graph: TaskGraph): TaskHierarchyRow[] {
60
61
  if (!node) return;
61
62
  visited.add(id);
62
63
  const children = node.childIds.filter((childId) => byId.has(childId));
63
- result.push({ task: node.task, depth, childCount: children.length, dependencies: [...node.dependencyIds], active: node.active === true });
64
+ result.push({
65
+ task: node.task,
66
+ depth,
67
+ childCount: children.length,
68
+ dependencies: [...node.dependencyIds],
69
+ active: node.active === true,
70
+ });
64
71
  for (const childId of children) visit(childId, depth + 1);
65
72
  };
66
73
  for (const rootId of graph.rootIds) visit(rootId, 0);
@@ -68,7 +75,12 @@ export function buildTaskHierarchy(graph: TaskGraph): TaskHierarchyRow[] {
68
75
  return result;
69
76
  }
70
77
 
71
- async function loadTaskGraph(projectRoot: string, sessionId: string, scope?: "project" | "graph" | "all", rootTaskId?: string): Promise<TaskGraph> {
78
+ async function loadTaskGraph(
79
+ projectRoot: string,
80
+ sessionId: string,
81
+ scope?: "project" | "graph" | "all",
82
+ rootTaskId?: string,
83
+ ): Promise<TaskGraph> {
72
84
  return callService<Record<string, unknown>, TaskGraph>("tasks.graph", {
73
85
  limit: 200,
74
86
  project_root: projectRoot,
@@ -102,7 +114,10 @@ export async function showTasks(ctx: ExtensionCommandContext): Promise<void> {
102
114
  for (;;) {
103
115
  const action = await renderPanel(ctx, graph);
104
116
  if (!action) return;
105
- if (action.type === "refresh") { graph = await loadTaskGraph(ctx.cwd, sessionId); continue; }
117
+ if (action.type === "refresh") {
118
+ graph = await loadTaskGraph(ctx.cwd, sessionId);
119
+ continue;
120
+ }
106
121
  if (action.type === "scope") {
107
122
  const choice = await ctx.ui.select("Task scope", ["Current project", "Focused graph", "All projects"]);
108
123
  if (!choice) continue;
@@ -110,7 +125,9 @@ export async function showTasks(ctx: ExtensionCommandContext): Promise<void> {
110
125
  let rootTaskId: string | undefined;
111
126
  if (scope === "graph") {
112
127
  const projectGraph = await loadTaskGraph(ctx.cwd, sessionId, "project");
113
- const roots = projectGraph.rootIds.map((id) => projectGraph.nodes.find((node) => node.task.id === id)?.task).filter((task): task is Artifact => task !== undefined);
128
+ const roots = projectGraph.rootIds
129
+ .map((id) => projectGraph.nodes.find((node) => node.task.id === id)?.task)
130
+ .filter((task): task is Artifact => task !== undefined);
114
131
  const rootLabels = taskChoiceLabels(roots);
115
132
  const selected = await ctx.ui.select("Focused root or epic", rootLabels);
116
133
  if (!selected) continue;
@@ -121,7 +138,10 @@ export async function showTasks(ctx: ExtensionCommandContext): Promise<void> {
121
138
  graph = await loadTaskGraph(ctx.cwd, sessionId);
122
139
  continue;
123
140
  }
124
- if (action.type === "graph") { await showTaskGraph(ctx, graph); continue; }
141
+ if (action.type === "graph") {
142
+ await showTaskGraph(ctx, graph);
143
+ continue;
144
+ }
125
145
  if (action.type !== "action" || !action.row) continue;
126
146
 
127
147
  const rowId = action.row.id;
@@ -143,19 +163,36 @@ export async function showTasks(ctx: ExtensionCommandContext): Promise<void> {
143
163
 
144
164
  if ((choice === "Remove dependency" || choice === "Remove from parent") && node) {
145
165
  const relatedIds = choice === "Remove dependency" ? node.dependencyIds : node.parentIds;
146
- const relatedTasks = relatedIds.map((relatedId) => graph.nodes.find((entry) => entry.task.id === relatedId)?.task).filter((task): task is Artifact => task !== undefined);
166
+ const relatedTasks = relatedIds
167
+ .map((relatedId) => graph.nodes.find((entry) => entry.task.id === relatedId)?.task)
168
+ .filter((task): task is Artifact => task !== undefined);
147
169
  const relatedTitles = taskChoiceLabels(relatedTasks);
148
- const selected = await ctx.ui.select(choice === "Remove dependency" ? "Remove which dependency?" : "Remove from which parent?", relatedTitles);
170
+ const selected = await ctx.ui.select(
171
+ choice === "Remove dependency" ? "Remove which dependency?" : "Remove from which parent?",
172
+ relatedTitles,
173
+ );
149
174
  if (!selected) continue;
150
175
  const relatedTask = relatedTasks[relatedTitles.indexOf(selected)];
151
176
  if (!relatedTask) continue;
152
177
  const relatedId = relatedTask.id;
153
178
  try {
154
179
  if (choice === "Remove dependency") {
155
- await callService("tasks.undepend", { id: action.row.id, dependency_id: relatedId, actor: "user", source: "tasks-tui", session_id: sessionId });
180
+ await callService("tasks.undepend", {
181
+ id: action.row.id,
182
+ dependency_id: relatedId,
183
+ actor: "user",
184
+ source: "tasks-tui",
185
+ session_id: sessionId,
186
+ });
156
187
  ctx.ui.notify(`Removed dependency on ${relatedTask.title}`, "info");
157
188
  } else {
158
- await callService("tasks.uncontain", { parent_id: relatedId, child_id: action.row.id, actor: "user", source: "tasks-tui", session_id: sessionId });
189
+ await callService("tasks.uncontain", {
190
+ parent_id: relatedId,
191
+ child_id: action.row.id,
192
+ actor: "user",
193
+ source: "tasks-tui",
194
+ session_id: sessionId,
195
+ });
159
196
  ctx.ui.notify(`Removed from parent ${relatedTask.title}`, "info");
160
197
  }
161
198
  } catch (error) {
@@ -167,7 +204,10 @@ export async function showTasks(ctx: ExtensionCommandContext): Promise<void> {
167
204
 
168
205
  if (choice === "Show details") {
169
206
  const art = await callService<Record<string, unknown>, Artifact | null>("tasks.show", { id: action.row.id });
170
- if (!art) { ctx.ui.notify("Not found", "error"); continue; }
207
+ if (!art) {
208
+ ctx.ui.notify("Not found", "error");
209
+ continue;
210
+ }
171
211
  const history = await callService<Record<string, unknown>, TaskHistoryPage>("tasks.history", { id: art.id, direction: "desc" });
172
212
  await showTaskDetails(ctx, art, graph, undefined, [...history.events].reverse());
173
213
  } else if (choice === "Edit task") {
@@ -191,7 +231,13 @@ export async function showTasks(ctx: ExtensionCommandContext): Promise<void> {
191
231
  }
192
232
  } else if (choice === "Make active") {
193
233
  try {
194
- const focused = await callService<Record<string, unknown>, Artifact>("tasks.focus", { id: action.row.id, actor: "user", source: "tasks-tui", session_id: sessionId, ...sessionSecretField(sessionId) });
234
+ const focused = await callService<Record<string, unknown>, Artifact>("tasks.focus", {
235
+ id: action.row.id,
236
+ actor: "user",
237
+ source: "tasks-tui",
238
+ session_id: sessionId,
239
+ ...sessionSecretField(sessionId),
240
+ });
195
241
  emitTaskFocusEvent({ taskId: focused.id, sessionId, status: "focused" });
196
242
  ctx.ui.notify(`Active: ${action.row.title}`, "info");
197
243
  } catch (error) {
@@ -200,11 +246,21 @@ export async function showTasks(ctx: ExtensionCommandContext): Promise<void> {
200
246
  } else if (choice === "Pause focus" || choice === "Resume focus" || choice === "Clear focus") {
201
247
  try {
202
248
  if (choice === "Clear focus") {
203
- await callService("tasks.clear_focus", { actor: "user", source: "tasks-tui", session_id: sessionId, ...sessionSecretField(sessionId) });
249
+ await callService("tasks.clear_focus", {
250
+ actor: "user",
251
+ source: "tasks-tui",
252
+ session_id: sessionId,
253
+ ...sessionSecretField(sessionId),
254
+ });
204
255
  emitTaskFocusEvent({ taskId: null, sessionId, status: "cleared" });
205
256
  } else {
206
257
  const operation = choice === "Pause focus" ? "tasks.pause" : "tasks.unpause";
207
- const result = await callService<Record<string, unknown>, { artifact: Artifact; status: string }>(operation, { actor: "user", source: "tasks-tui", session_id: sessionId, ...sessionSecretField(sessionId) });
258
+ const result = await callService<Record<string, unknown>, { artifact: Artifact; status: string }>(operation, {
259
+ actor: "user",
260
+ source: "tasks-tui",
261
+ session_id: sessionId,
262
+ ...sessionSecretField(sessionId),
263
+ });
208
264
  emitTaskFocusEvent({ taskId: result.artifact.id, sessionId, status: choice === "Pause focus" ? "paused" : "unpaused" });
209
265
  }
210
266
  ctx.ui.notify(choice === "Clear focus" ? "Task focus cleared" : choice, "info");
@@ -213,34 +269,48 @@ export async function showTasks(ctx: ExtensionCommandContext): Promise<void> {
213
269
  }
214
270
  } else if (choice === "Run gates") {
215
271
  try {
216
- const results = await callService<Record<string, unknown>, GateResult[]>("tasks.run_gates", { id: action.row.id, actor: "user", source: "tasks-tui" });
217
- ctx.ui.notify(`Gates:\n${results.map((gate) => `${gate.passed ? "✓" : "✗"} ${gate.gate.type}: ${gate.gate.target} — ${gate.output}`).join("\n")}`, "info");
272
+ const results = await callService<Record<string, unknown>, GateResult[]>("tasks.run_gates", {
273
+ id: action.row.id,
274
+ actor: "user",
275
+ source: "tasks-tui",
276
+ });
277
+ ctx.ui.notify(
278
+ `Gates:\n${results.map((gate) => `${gate.passed ? "✓" : "✗"} ${gate.gate.type}: ${gate.gate.target} — ${gate.output}`).join("\n")}`,
279
+ "info",
280
+ );
218
281
  } catch (error) {
219
282
  ctx.ui.notify(`Gates failed: ${error instanceof Error ? error.message : error}`, "error");
220
283
  }
221
284
  } else {
222
285
  try {
223
- const operation = choice === "Start"
224
- ? "tasks.start"
225
- : choice === "Submit for review"
226
- ? "tasks.submit"
227
- : choice === "Reject"
228
- ? "tasks.reject"
229
- : choice === "Retry"
230
- ? "tasks.retry"
231
- : choice === "Cancel"
232
- ? "tasks.cancel"
233
- : "tasks.complete";
286
+ const operation =
287
+ choice === "Start"
288
+ ? "tasks.start"
289
+ : choice === "Submit for review"
290
+ ? "tasks.submit"
291
+ : choice === "Reject"
292
+ ? "tasks.reject"
293
+ : choice === "Retry"
294
+ ? "tasks.retry"
295
+ : choice === "Cancel"
296
+ ? "tasks.cancel"
297
+ : "tasks.complete";
234
298
  if (operation === "tasks.complete") {
235
- const result = await callService<Record<string, unknown>, TaskCompletion>(operation, { id: action.row.id, actor: "user", source: "tasks-tui", session_id: sessionId });
299
+ const result = await callService<Record<string, unknown>, TaskCompletion>(operation, {
300
+ id: action.row.id,
301
+ actor: "user",
302
+ source: "tasks-tui",
303
+ session_id: sessionId,
304
+ });
236
305
  action.row.status = result.artifact.status;
237
306
  const gates = result.gates.map((gate) => `${gate.passed ? "✓" : "✗"} ${gate.gate.type}: ${gate.gate.target}`).join("\n");
238
307
  const checklist = result.checklist.map((item) => `${item.accepted ? "✓" : "✗"} proof: ${item.item}`).join("\n");
239
308
  const focused = result.focused ? `\nActive: ${result.focused.title}` : "";
240
309
  const taskById = new Map(graph.nodes.map((entry) => [entry.task.id, entry.task]));
241
- const blocked = result.blocked.length > 0
242
- ? `\nWaiting: ${result.blocked.map((entry) => `${entry.artifact.title} needs ${entry.dependencyIds.map((id) => taskById.get(id)?.title ?? "unknown task").join(", ")}`).join("; ")}`
243
- : "";
310
+ const blocked =
311
+ result.blocked.length > 0
312
+ ? `\nWaiting: ${result.blocked.map((entry) => `${entry.artifact.title} needs ${entry.dependencyIds.map((id) => taskById.get(id)?.title ?? "unknown task").join(", ")}`).join("; ")}`
313
+ : "";
244
314
  ctx.ui.notify(
245
315
  result.completed
246
316
  ? `Completed ${result.artifact.title}${focused}${blocked}${checklist ? `\n${checklist}` : ""}${gates ? `\n${gates}` : ""}`
@@ -248,7 +318,12 @@ export async function showTasks(ctx: ExtensionCommandContext): Promise<void> {
248
318
  result.completed ? "info" : "warning",
249
319
  );
250
320
  } else {
251
- const updated = await callService<Record<string, unknown>, Artifact>(operation, { id: action.row.id, actor: "user", source: "tasks-tui", session_id: sessionId });
321
+ const updated = await callService<Record<string, unknown>, Artifact>(operation, {
322
+ id: action.row.id,
323
+ actor: "user",
324
+ source: "tasks-tui",
325
+ session_id: sessionId,
326
+ });
252
327
  action.row.status = updated.status;
253
328
  ctx.ui.notify(`${updated.title} → [${updated.status}]`, "info");
254
329
  }
@@ -279,9 +354,9 @@ function renderPanel(ctx: ExtensionCommandContext, graph: TaskGraph): Promise<Pa
279
354
 
280
355
  function applyFilter(): void {
281
356
  const q = searchInput.getValue().trim().toLowerCase();
282
- filtered = q ? hierarchy.filter(({ task }) =>
283
- task.title.toLowerCase().includes(q) || task.id.toLowerCase().includes(q)
284
- ) : [...hierarchy];
357
+ filtered = q
358
+ ? hierarchy.filter(({ task }) => task.title.toLowerCase().includes(q) || task.id.toLowerCase().includes(q))
359
+ : [...hierarchy];
285
360
  selectedIndex = 0;
286
361
  }
287
362
 
@@ -345,26 +420,28 @@ function renderPanel(ctx: ExtensionCommandContext, graph: TaskGraph): Promise<Pa
345
420
  const execution = executionById.get(row.id);
346
421
  const state = execution?.state ?? row.status;
347
422
  const presentation = TASK_STATUS_PRESENTATION[row.status as TaskStatus];
348
- const glyphStyled = state === "invalid"
349
- ? theme.fg("error", "!")
350
- : presentation
351
- ? theme.fg(presentation.color, presentation.glyph)
352
- : theme.fg("muted", "?");
423
+ const glyphStyled =
424
+ state === "invalid"
425
+ ? theme.fg("error", "!")
426
+ : presentation
427
+ ? theme.fg(presentation.color, presentation.glyph)
428
+ : theme.fg("muted", "?");
353
429
  const title = selected ? theme.bold(row.title) : row.title;
354
430
  let laterSibling = false;
355
431
  for (let candidate = i + 1; candidate < filtered.length; candidate++) {
356
432
  if (filtered[candidate]!.depth < entry.depth) break;
357
- if (filtered[candidate]!.depth === entry.depth) { laterSibling = true; break; }
433
+ if (filtered[candidate]!.depth === entry.depth) {
434
+ laterSibling = true;
435
+ break;
436
+ }
358
437
  }
359
438
  const connector = taskTreeConnector({
360
439
  depth: entry.depth,
361
440
  hasChildren: entry.childCount > 0,
362
441
  hasLaterSibling: laterSibling,
363
442
  });
364
- const node = entry.depth === 0 && entry.childCount > 0
365
- ? theme.fg("accent", connector)
366
- : theme.fg("dim", connector);
367
- const gates = (row.extra?.["gates"] as any[])?.length;
443
+ const node = entry.depth === 0 && entry.childCount > 0 ? theme.fg("accent", connector) : theme.fg("dim", connector);
444
+ const gates = (row.extra?.gates as any[])?.length;
368
445
  const relationParts: string[] = [];
369
446
  if (execution) relationParts.push(execution.layer === null ? state : `layer ${execution.layer + 1} · ${state}`);
370
447
  if (entry.childCount > 0) relationParts.push(`${entry.childCount} subtask${entry.childCount === 1 ? "" : "s"}`);
@@ -397,24 +474,38 @@ function renderPanel(ctx: ExtensionCommandContext, graph: TaskGraph): Promise<Pa
397
474
  invalidate: () => container.invalidate(),
398
475
  handleInput(data: string) {
399
476
  if (searchActive) {
400
- if (matchesKey(data, "escape")) { searchActive = false; applyFilter(); }
401
- else if (matchesKey(data, "enter")) { searchActive = false; }
402
- else { searchInput.handleInput(data); applyFilter(); }
477
+ if (matchesKey(data, "escape")) {
478
+ searchActive = false;
479
+ applyFilter();
480
+ } else if (matchesKey(data, "enter")) {
481
+ searchActive = false;
482
+ } else {
483
+ searchInput.handleInput(data);
484
+ applyFilter();
485
+ }
403
486
  tui.requestRender();
404
487
  return;
405
488
  }
406
489
  if (matchesKey(data, "up")) selectedIndex = (selectedIndex - 1 + filtered.length) % Math.max(filtered.length, 1);
407
490
  else if (matchesKey(data, "down")) selectedIndex = (selectedIndex + 1) % Math.max(filtered.length, 1);
408
491
  else if (data === "/") searchActive = true;
409
- else if (data === "g") { done({ type: "graph" }); return; }
410
- else if (data === "s") { done({ type: "scope" }); return; }
411
- else if (data === "r") { done({ type: "refresh" }); return; }
412
- else if (matchesKey(data, "enter")) {
492
+ else if (data === "g") {
493
+ done({ type: "graph" });
494
+ return;
495
+ } else if (data === "s") {
496
+ done({ type: "scope" });
497
+ return;
498
+ } else if (data === "r") {
499
+ done({ type: "refresh" });
500
+ return;
501
+ } else if (matchesKey(data, "enter")) {
413
502
  const entry = filtered[selectedIndex];
414
503
  if (entry) done({ type: "action", row: entry.task });
415
504
  return;
416
- } else if (matchesKey(data, "escape")) { done(undefined); return; }
417
- else return;
505
+ } else if (matchesKey(data, "escape")) {
506
+ done(undefined);
507
+ return;
508
+ } else return;
418
509
  tui.requestRender();
419
510
  },
420
511
  };
@@ -96,10 +96,7 @@ export class ArtifactCard implements Component {
96
96
  if (metadata) lines.push(truncateToWidth(this.theme.fg("muted", metadata), safeWidth));
97
97
  if (artifact.body) lines.push(...wrapTextWithAnsi(artifact.body, safeWidth));
98
98
  if (this.details.completeness.truncated) {
99
- lines.push(truncateToWidth(
100
- this.theme.fg("warning", `[truncated ${this.details.completeness.omitted} characters]`),
101
- safeWidth,
102
- ));
99
+ lines.push(truncateToWidth(this.theme.fg("warning", `[truncated ${this.details.completeness.omitted} characters]`), safeWidth));
103
100
  }
104
101
  } else if (artifact.body || artifact.labels.length > 0) {
105
102
  lines.push(truncateToWidth(this.theme.fg("dim", expandHint()), safeWidth));
@@ -1,12 +1,8 @@
1
+ import { TOOL_COLLAPSED_ROW_LIMIT } from "@danypops/papyrus";
1
2
  import type { Theme } from "@earendil-works/pi-coding-agent";
2
3
  import { type Component, truncateToWidth } from "@earendil-works/pi-tui";
3
- import { TOOL_COLLAPSED_ROW_LIMIT } from "@danypops/papyrus";
4
4
  import { countSummary, expandHint, kindGlyph, statusGlyph, treeConnector } from "./artifact-card.ts";
5
- import type {
6
- ArtifactListToolDetails,
7
- GraphToolDetails,
8
- ToolArtifactSummary,
9
- } from "./render-model.ts";
5
+ import type { ArtifactListToolDetails, GraphToolDetails, ToolArtifactSummary } from "./render-model.ts";
10
6
 
11
7
  function pluralKind(rows: readonly ToolArtifactSummary[]): string {
12
8
  const kind = rows[0]?.kind ?? "artifact";
@@ -25,11 +21,9 @@ function statusSummary(rows: readonly ToolArtifactSummary[]): string {
25
21
 
26
22
  function rowLine(row: ToolArtifactSummary, expanded: boolean, theme: Theme): string {
27
23
  const identity = expanded ? `${row.id} ` : "";
28
- return [
29
- theme.fg("muted", `${statusGlyph(row.status)} ${row.status}`),
30
- theme.fg("accent", identity),
31
- theme.fg("text", row.title),
32
- ].join(" ");
24
+ return [theme.fg("muted", `${statusGlyph(row.status)} ${row.status}`), theme.fg("accent", identity), theme.fg("text", row.title)].join(
25
+ " ",
26
+ );
33
27
  }
34
28
 
35
29
  function rowMetadata(row: ToolArtifactSummary): string {
@@ -62,10 +56,9 @@ export class ArtifactListCard implements Component {
62
56
  if (this.cachedLines && this.cachedWidth === safeWidth) return this.cachedLines;
63
57
  const rows = this.details.rows;
64
58
  const noun = pluralKind(rows);
65
- const lines = [truncateToWidth(
66
- this.theme.fg("toolTitle", this.theme.bold(`${countSummary(rows.length, this.details.total)} ${noun}`)),
67
- safeWidth,
68
- )];
59
+ const lines = [
60
+ truncateToWidth(this.theme.fg("toolTitle", this.theme.bold(`${countSummary(rows.length, this.details.total)} ${noun}`)), safeWidth),
61
+ ];
69
62
  if (rows.length === 0) {
70
63
  lines.push(truncateToWidth(this.theme.fg("dim", `No ${noun}.`), safeWidth));
71
64
  } else {
@@ -116,7 +109,9 @@ function hierarchyRows(details: GraphToolDetails): HierarchyRow[] {
116
109
  if (visited.has(node.id)) return;
117
110
  visited.add(node.id);
118
111
  rows.push({ node, prefix, connector });
119
- const children = (childIds.get(node.id) ?? []).map((id) => byId.get(id)).filter((child): child is ToolArtifactSummary => child !== undefined);
112
+ const children = (childIds.get(node.id) ?? [])
113
+ .map((id) => byId.get(id))
114
+ .filter((child): child is ToolArtifactSummary => child !== undefined);
120
115
  children.forEach((child, index) => {
121
116
  const last = index === children.length - 1;
122
117
  visit(child, `${prefix}${connector ? (connector === "└─" ? " " : "│ ") : ""}`, treeConnector(last));
@@ -152,16 +147,20 @@ export class TaskHierarchyPreview implements Component {
152
147
  const safeWidth = Math.max(1, width);
153
148
  if (this.cachedLines && this.cachedWidth === safeWidth) return this.cachedLines;
154
149
  const rows = hierarchyRows(this.details);
155
- const lines = [truncateToWidth(
156
- this.theme.fg("toolTitle", this.theme.bold(`${this.details.nodes.length} tasks · ${this.details.edges.length} edges`)),
157
- safeWidth,
158
- )];
150
+ const lines = [
151
+ truncateToWidth(
152
+ this.theme.fg("toolTitle", this.theme.bold(`${this.details.nodes.length} tasks · ${this.details.edges.length} edges`)),
153
+ safeWidth,
154
+ ),
155
+ ];
159
156
  for (const row of rows) {
160
157
  const identity = this.expanded ? `${row.node.id} ` : "";
161
- lines.push(truncateToWidth(
162
- `${row.prefix}${row.connector}${row.connector ? " " : ""}${this.theme.fg("accent", kindGlyph(row.node.kind))} ${this.theme.fg("muted", statusGlyph(row.node.status))} ${this.theme.fg("accent", identity)}${this.theme.fg("text", row.node.title)}`,
163
- safeWidth,
164
- ));
158
+ lines.push(
159
+ truncateToWidth(
160
+ `${row.prefix}${row.connector}${row.connector ? " " : ""}${this.theme.fg("accent", kindGlyph(row.node.kind))} ${this.theme.fg("muted", statusGlyph(row.node.status))} ${this.theme.fg("accent", identity)}${this.theme.fg("text", row.node.title)}`,
161
+ safeWidth,
162
+ ),
163
+ );
165
164
  if (this.expanded) {
166
165
  const metadata = rowMetadata(row.node);
167
166
  if (metadata) lines.push(truncateToWidth(this.theme.fg("dim", `${row.prefix} ${metadata}`), safeWidth));
@@ -1,12 +1,8 @@
1
- import type {
2
- AgentToolResult,
3
- Theme,
4
- ToolRenderResultOptions,
5
- } from "@earendil-works/pi-coding-agent";
1
+ import type { AgentToolResult, Theme, ToolRenderResultOptions } from "@earendil-works/pi-coding-agent";
6
2
  import { type Component, Text } from "@earendil-works/pi-tui";
7
3
  import { ArtifactCard } from "./artifact-card.ts";
8
4
  import { ArtifactListCard, TaskHierarchyPreview } from "./artifact-list.ts";
9
- import { parsePapyrusToolDetails, type PapyrusToolDetails } from "./render-model.ts";
5
+ import { type PapyrusToolDetails, parsePapyrusToolDetails } from "./render-model.ts";
10
6
 
11
7
  const CALL_VALUE_MAX_CHARACTERS = 80;
12
8