takomi 2.1.44 → 2.1.45

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 (39) hide show
  1. package/.pi/README.md +4 -3
  2. package/.pi/extensions/oauth-router/commands.ts +66 -35
  3. package/.pi/extensions/oauth-router/index.ts +51 -7
  4. package/.pi/extensions/oauth-router/report-ui.ts +205 -0
  5. package/.pi/extensions/takomi-context-manager/diagnostics-tools.ts +41 -3
  6. package/.pi/extensions/takomi-context-manager/diagnostics.ts +26 -0
  7. package/.pi/extensions/takomi-context-manager/index.ts +3 -2
  8. package/.pi/extensions/takomi-context-manager/model-policy-gate.ts +36 -15
  9. package/.pi/extensions/takomi-context-manager/policy-tools.ts +93 -42
  10. package/.pi/extensions/takomi-context-manager/skill-categories.d.ts +11 -0
  11. package/.pi/extensions/takomi-context-manager/skill-categories.js +212 -0
  12. package/.pi/extensions/takomi-context-manager/skill-registry.ts +179 -8
  13. package/.pi/extensions/takomi-context-manager/skill-tools.ts +208 -103
  14. package/.pi/extensions/takomi-context-manager/tool-renderers.ts +112 -0
  15. package/.pi/extensions/takomi-context-manager/types.ts +6 -0
  16. package/.pi/extensions/takomi-runtime/commands.ts +45 -12
  17. package/.pi/extensions/takomi-runtime/gate-provenance.ts +24 -0
  18. package/.pi/extensions/takomi-runtime/index.ts +111 -42
  19. package/.pi/extensions/takomi-runtime/routing-policy.ts +14 -2
  20. package/.pi/extensions/takomi-runtime/takomi-stats.js +46 -26
  21. package/.pi/extensions/takomi-runtime/tool-renderers.ts +234 -0
  22. package/.pi/extensions/takomi-runtime/workflow-catalog.ts +54 -0
  23. package/.pi/extensions/takomi-subagents/agents.ts +4 -0
  24. package/.pi/extensions/takomi-subagents/async-lifecycle.ts +401 -0
  25. package/.pi/extensions/takomi-subagents/detached-results.ts +940 -0
  26. package/.pi/extensions/takomi-subagents/index.ts +46 -1
  27. package/.pi/extensions/takomi-subagents/native-render.ts +250 -188
  28. package/.pi/extensions/takomi-subagents/pi-subagents-engine.ts +55 -46
  29. package/.pi/extensions/takomi-subagents/pi-subagents-internal.ts +11 -5
  30. package/.pi/extensions/takomi-subagents/result-heartbeat.ts +55 -0
  31. package/.pi/extensions/takomi-subagents/subagent-ux.ts +162 -0
  32. package/.pi/extensions/takomi-subagents/tool-runner.ts +158 -28
  33. package/.pi/takomi/model-routing.md +282 -3
  34. package/assets/.agent/skills/shared-resend-portfolio/SKILL.md +124 -0
  35. package/package.json +4 -3
  36. package/src/pi-takomi-core/types.ts +10 -0
  37. package/src/pi-takomi-core/workflows.ts +86 -45
  38. package/src/skills-catalog.js +2 -202
  39. package/src/skills-installer.js +4 -1
@@ -1,14 +1,30 @@
1
1
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
2
  import { Type } from "typebox";
3
+ import { initializeTakomiAsyncLifecycle, resetTakomiAsyncLifecycle } from "./async-lifecycle";
4
+ import {
5
+ clearDetachedResults,
6
+ initializeDetachedSession,
7
+ registerDetachedCompletionNotifications,
8
+ } from "./detached-results";
3
9
  import { renderTakomiSubagentCall, renderTakomiSubagentResult } from "./native-render";
4
10
  import { loadPiSubagentsInternals } from "./pi-subagents-internal";
5
- import { executeTakomiSubagentTool } from "./tool-runner";
11
+ import { clearAllTakomiSubagentResultHeartbeats } from "./result-heartbeat";
12
+ import { executeTakomiSubagentTool, invalidateTakomiPiSubagentsEngine, type TakomiAcceptanceInput } from "./tool-runner";
6
13
 
7
14
  const ChecklistItemSchema = Type.Object({
8
15
  text: Type.String(),
9
16
  done: Type.Optional(Type.Boolean()),
10
17
  });
11
18
 
19
+ const AcceptanceSchema = Type.Unsafe<TakomiAcceptanceInput>({
20
+ anyOf: [
21
+ { type: "string", enum: ["auto", "none", "attested", "checked", "verified", "reviewed"] },
22
+ { type: "boolean", enum: [false] },
23
+ { type: "object", additionalProperties: true },
24
+ ],
25
+ description: "Optional explicit acceptance policy. Omitted Takomi tasks do not enforce acceptance; explicit contracts are forwarded to pi-subagents.",
26
+ });
27
+
12
28
  const ThinkingSchema = Type.Union([
13
29
  Type.Literal("off"),
14
30
  Type.Literal("minimal"),
@@ -29,6 +45,7 @@ const TaskSchema = Type.Object({
29
45
  conversationId: Type.Optional(Type.String()),
30
46
  cwd: Type.Optional(Type.String()),
31
47
  checklist: Type.Optional(Type.Array(Type.Union([Type.String(), ChecklistItemSchema]))),
48
+ acceptance: Type.Optional(AcceptanceSchema),
32
49
  });
33
50
 
34
51
  const ContextSchema = Type.Union([
@@ -58,6 +75,7 @@ const SubagentParameters = Type.Object({
58
75
  conversationId: Type.Optional(Type.String({ description: "Persistent conversation id to resume the same subagent session" })),
59
76
  cwd: Type.Optional(Type.String({ description: "Working directory override" })),
60
77
  checklist: Type.Optional(Type.Array(Type.Union([Type.String(), ChecklistItemSchema]), { description: "Optional checklist for the subagent" })),
78
+ acceptance: Type.Optional(AcceptanceSchema),
61
79
  tasks: Type.Optional(Type.Array(TaskSchema, { description: "Parallel subagent tasks" })),
62
80
  confirmLaunch: Type.Optional(Type.Boolean({ description: "Required to launch immediately in manual Takomi launch mode" })),
63
81
  previewOnly: Type.Optional(Type.Boolean({ description: "Return the delegation plan without launching" })),
@@ -108,5 +126,32 @@ export default async function takomiSubagents(pi: ExtensionAPI) {
108
126
  // a completed takomi_subagent result can fall back to Takomi's plain text
109
127
  // renderer and lose the native compact/expanded details shown by `subagent`.
110
128
  await loadPiSubagentsInternals();
129
+ // A same-process Takomi reload reuses the ExtensionAPI identity, so explicitly
130
+ // discard the cached engine before replacing its lifecycle generation.
131
+ invalidateTakomiPiSubagentsEngine(pi);
132
+ const cleanupAsyncLifecycle = await initializeTakomiAsyncLifecycle(pi);
111
133
  registerSubagentTool(pi);
134
+
135
+ // Replace native completion notification through pi-subagents' own reload-safe
136
+ // registration slot. The shared renderer and dedupe key remain authoritative;
137
+ // Takomi enriches the single native notice without adding polling or timers.
138
+ const unregisterCompletionNotifications = registerDetachedCompletionNotifications(pi);
139
+
140
+ // Tool rows normally settle and clear their own heartbeat. A turn can also end
141
+ // without Pi rendering a final result (for example, after an abnormal tool
142
+ // interruption), so clear abandoned rows and rehydrate only authenticated
143
+ // launch provenance from the replacement session's own custom entries.
144
+ pi.on("agent_end", () => clearAllTakomiSubagentResultHeartbeats());
145
+ pi.on("session_start", async (_event, ctx) => {
146
+ clearAllTakomiSubagentResultHeartbeats();
147
+ await resetTakomiAsyncLifecycle(pi, ctx);
148
+ await initializeDetachedSession(pi, ctx);
149
+ });
150
+ pi.on("session_shutdown", () => {
151
+ clearAllTakomiSubagentResultHeartbeats();
152
+ clearDetachedResults(pi);
153
+ invalidateTakomiPiSubagentsEngine(pi);
154
+ cleanupAsyncLifecycle();
155
+ unregisterCompletionNotifications();
156
+ });
112
157
  }
@@ -1,188 +1,250 @@
1
- import type { AgentToolResult } from "@earendil-works/pi-agent-core";
2
- import type { Theme } from "@earendil-works/pi-coding-agent";
3
- import { Text } from "@earendil-works/pi-tui";
4
- import type { TakomiSubagentToolParams } from "./tool-runner";
5
- import { renderNativeSubagentResult, type Details } from "./pi-subagents-internal";
6
-
7
- type ToolResult = AgentToolResult<Details>;
8
-
9
- function taskList(params: TakomiSubagentToolParams): Array<{ agent: string; task: string }> {
10
- if (params.chain?.length) return params.chain;
11
- if (params.tasks?.length) return params.tasks;
12
- if (params.agent || params.task) return [{ agent: params.agent ?? "...", task: params.task ?? "..." }];
13
- return [];
14
- }
15
-
16
- export function renderTakomiSubagentCall(params: TakomiSubagentToolParams, theme: Theme) {
17
- const tasks = taskList(params);
18
- const mode = params.chain?.length ? "chain" : params.tasks?.length ? "parallel" : "single";
19
- if (tasks.length === 1) {
20
- return new Text(
21
- `${theme.fg("toolTitle", theme.bold("takomi_subagent "))}${theme.fg("accent", tasks[0]?.agent || "?")}`,
22
- 0,
23
- 0,
24
- );
25
- }
26
- return new Text(
27
- `${theme.fg("toolTitle", theme.bold("takomi_subagent "))}${mode} (${tasks.length})`,
28
- 0,
29
- 0,
30
- );
31
- }
32
-
33
- function resultText(result: ToolResult): string {
34
- return typeof (result as any)?.content === "string"
35
- ? (result as any).content
36
- : Array.isArray((result as any)?.content)
37
- ? (result as any).content.map((part: any) => part?.text ?? "").filter(Boolean).join("\n")
38
- : JSON.stringify((result as any)?.details ?? {}, null, 2);
39
- }
40
-
41
- function extractPolicyNames(text: string): string[] {
42
- const policyMatch = text.match(/Required policies:\n((?:- .+\n?)+)/);
43
- return policyMatch?.[1]
44
- ?.split("\n")
45
- .map((line) => line.replace(/^[-\s]+/, "").trim())
46
- .filter(Boolean) ?? [];
47
- }
48
-
49
- function summarizeCollapsedResult(text: string, status: string, theme: Theme): string {
50
- const policies = extractPolicyNames(text);
51
- const lineCount = text ? text.split(/\r?\n/).length : 0;
52
- const label = policies.length > 0
53
- ? `policy context loaded: ${policies.join(", ")}`
54
- : `${lineCount} result line${lineCount === 1 ? "" : "s"} hidden`;
55
- const icon = status === "failed" ? "⚠" : status === "running" ? "…" : "✓";
56
- const color = status === "failed" ? "warning" : status === "running" ? "accent" : "success";
57
- return `${theme.fg(color, `${icon} takomi_subagent ${status}`)}${theme.fg("dim", ` · ${label} (ctrl+o to expand)`)}`;
58
- }
59
-
60
- function isPolicyGateBlock(text: string): boolean {
61
- return /^Blocked\s+takomi_subagent:\s+required policy context had not been loaded yet\./m.test(text)
62
- && /\nRequired policies:\n/.test(text)
63
- && /\nLoaded policy context:\n/.test(text);
64
- }
65
-
66
- function renderPolicyGateBlock(text: string, expanded: boolean | undefined, theme: Theme): string {
67
- const policies = extractPolicyNames(text);
68
- const lineCount = text ? text.split(/\r?\n/).length : 0;
69
- const policyLabel = policies.length ? policies.join(", ") : "required policy";
70
- if (!expanded) {
71
- return [
72
- theme.fg("warning", "⚠ takomi_subagent blocked"),
73
- theme.fg("dim", `Required policy context loaded for this session: ${policyLabel}.`),
74
- theme.fg("dim", `Retry the original tool call. ${lineCount} policy lines hidden (ctrl+o to expand).`),
75
- ].join("\n");
76
- }
77
- return [
78
- theme.fg("warning", " takomi_subagent blocked"),
79
- theme.fg("dim", "Policy context was loaded and passed back to the model; retry the original call."),
80
- theme.fg("dim", "Press ctrl+o again to collapse."),
81
- "",
82
- text,
83
- ].join("\n");
84
- }
85
-
86
- function recentOutputLines(value: unknown): string[] {
87
- if (Array.isArray(value)) return value.map(String).filter(Boolean).slice(-3);
88
- if (typeof value === "string") return value.split(/\r?\n/).map((line) => line.trim()).filter(Boolean).slice(-3);
89
- return [];
90
- }
91
-
92
- function normalizeLiveStatus(value: string | undefined): string | undefined {
93
- if (!value) return undefined;
94
- if (value === "complete") return "completed";
95
- if (value === "in-progress") return "running";
96
- if (value === "timed-out") return "failed";
97
- return value;
98
- }
99
-
100
- function rawLiveStatus(row: any): string {
101
- const explicit = normalizeLiveStatus(
102
- typeof row?.progress?.status === "string" ? row.progress.status
103
- : typeof row?.status === "string" ? row.status
104
- : undefined,
105
- );
106
- if (explicit) return explicit;
107
-
108
- const exitCode = typeof row?.exitCode === "number" ? row.exitCode
109
- : typeof row?.code === "number" ? row.code
110
- : undefined;
111
- if (exitCode === 0) return "completed";
112
- if (exitCode === undefined || exitCode === null || exitCode === -1) return "running";
113
- return "failed";
114
- }
115
-
116
- function isLiveTerminalStatus(status: string): boolean {
117
- return ["completed", "failed", "blocked", "detached", "paused"].includes(status);
118
- }
119
-
120
- function displayLiveStatus(status: string, allRowsTerminal: boolean): string {
121
- // Native pi-subagents can emit one last partial update after the child process
122
- // reaches exitCode 0 but before the takomi_subagent tool result has actually
123
- // settled. In that window the tool card is still partial/running, so showing a
124
- // child as "completed" is misleading. Call it finalizing until Pi renders the
125
- // real final result.
126
- if (allRowsTerminal && status === "completed") return "finalizing";
127
- return status;
128
- }
129
-
130
- function livePartialText(result: ToolResult, theme: Theme): string {
131
- const details = (result as any)?.details ?? {};
132
- const results = Array.isArray(details.results) ? details.results : [];
133
- const progress = Array.isArray(details.progress) ? details.progress : [];
134
- const rows = results.length ? results : progress;
135
- const rawStatuses = rows.map(rawLiveStatus);
136
- const allRowsTerminal = rows.length > 0 && rawStatuses.every(isLiveTerminalStatus);
137
- const titleStatus = allRowsTerminal ? "finalizing" : "running";
138
- const lines = [
139
- theme.fg("toolTitle", theme.bold(`takomi_subagent ${titleStatus}`)),
140
- theme.fg("dim", "Live detail is bounded while streaming so manual scroll/ctrl+o does not jump on every token."),
141
- ];
142
-
143
- if (!rows.length) {
144
- const text = resultText(result).split(/\r?\n/).find((line) => line.trim())?.trim();
145
- lines.push(theme.fg("dim", text || "Waiting for subagent progress…"));
146
- return lines.join("\n");
147
- }
148
-
149
- rows.slice(0, 6).forEach((row: any, index: number) => {
150
- const status = displayLiveStatus(rawStatuses[index] ?? rawLiveStatus(row), allRowsTerminal);
151
- const agent = row.agent ?? `task ${index + 1}`;
152
- const task = String(row.task ?? "").replace(/\s+/g, " ").trim();
153
- const currentTool = row.currentTool ?? row.progress?.currentTool;
154
- const tokens = row.tokens ?? row.progress?.tokens ?? row.usage?.output;
155
- const tail = recentOutputLines(row.recentOutput ?? row.progress?.recentOutput ?? row.finalOutput ?? row.output);
156
- lines.push(theme.fg("accent", `${index + 1}. ${agent} ${theme.fg("dim", `[${status}]`)}`));
157
- if (task) lines.push(theme.fg("dim", ` ${task.slice(0, 140)}${task.length > 140 ? "…" : ""}`));
158
- if (currentTool || tokens) lines.push(theme.fg("muted", ` ${[currentTool ? `tool:${currentTool}` : "", tokens ? `tokens:${tokens}` : ""].filter(Boolean).join(" | ")}`));
159
- for (const line of tail) lines.push(theme.fg("dim", ` › ${line.slice(0, 160)}${line.length > 160 ? "…" : ""}`));
160
- });
161
- if (rows.length > 6) lines.push(theme.fg("muted", `… ${rows.length - 6} more running item${rows.length - 6 === 1 ? "" : "s"}`));
162
- lines.push(theme.fg("muted", "Final output will use the normal expandable native subagent renderer."));
163
- return lines.join("\n");
164
- }
165
-
166
- export function renderTakomiSubagentResult(result: ToolResult, options: { expanded?: boolean; isPartial?: boolean }, theme: Theme, context: any): any {
167
- const status = ((result as any)?.isError || context?.isError) ? "failed" : options.isPartial ? "running" : "completed";
168
- const text = resultText(result);
169
-
170
- if (isPolicyGateBlock(text)) {
171
- return new Text(renderPolicyGateBlock(text, options.expanded, theme), 0, 0);
172
- }
173
-
174
- const native = renderNativeSubagentResult(result, options, theme, context);
175
- if (options.isPartial && native) return native;
176
-
177
- if (options.isPartial) {
178
- if (options.expanded) return new Text(livePartialText(result, theme), 0, 0);
179
- return new Text(summarizeCollapsedResult(text, status, theme), 0, 0);
180
- }
181
-
182
- if (native) return native;
183
-
184
- if (!options.expanded) {
185
- return new Text(summarizeCollapsedResult(text, status, theme), 0, 0);
186
- }
187
- return new Text(`${theme.fg("toolTitle", theme.bold(`takomi_subagent ${status}`))}\n${text || "No result content."}`, 0, 0);
188
- }
1
+ import type { AgentToolResult } from "@earendil-works/pi-agent-core";
2
+ import type { Theme } from "@earendil-works/pi-coding-agent";
3
+ import { Container, Spacer, Text, type Component } from "@earendil-works/pi-tui";
4
+ import type { TakomiSubagentToolParams } from "./tool-runner";
5
+ import { renderNativeSubagentResult, type Details } from "./pi-subagents-internal";
6
+ import {
7
+ boundNarrative,
8
+ explicitAssistantTexts,
9
+ finalAnswer,
10
+ resolvedChecklist,
11
+ sanitizeUntrustedText,
12
+ sanitizeUntrustedValue,
13
+ type TakomiUxTask,
14
+ } from "./subagent-ux";
15
+ import {
16
+ clearTakomiSubagentResultHeartbeat,
17
+ ensureTakomiSubagentResultHeartbeat,
18
+ getTakomiSubagentHeartbeatFrame,
19
+ type TakomiSubagentRenderContext,
20
+ } from "./result-heartbeat";
21
+
22
+ type ToolResult = AgentToolResult<Details>;
23
+ const FALLBACK_RENDER_WIDTH = 80;
24
+ const COMPACT_CUSTOM_LINE_BUDGET = 3;
25
+ const EXPANDED_CUSTOM_LINE_BUDGET = 2;
26
+ const COMPACT_NARRATIVE_LINE_BUDGET = 2;
27
+ const NARRATIVE_PREFIX = " ";
28
+
29
+ class WidthAwareLines implements Component {
30
+ constructor(private readonly buildLines: (width: number) => string[]) {}
31
+ render(width: number): string[] {
32
+ const available = Number.isFinite(width) && width > 0 ? Math.floor(width) : FALLBACK_RENDER_WIDTH;
33
+ return this.buildLines(available);
34
+ }
35
+ invalidate(): void {}
36
+ }
37
+
38
+ function taskList(params: TakomiSubagentToolParams): Array<{ agent: string; task: string }> {
39
+ if (params.chain?.length) return params.chain;
40
+ if (params.tasks?.length) return params.tasks;
41
+ if (params.agent || params.task) return [{ agent: params.agent ?? "...", task: params.task ?? "..." }];
42
+ return [];
43
+ }
44
+
45
+ export function renderTakomiSubagentCall(params: TakomiSubagentToolParams, theme: Theme) {
46
+ const safeParams = sanitizeUntrustedValue(params);
47
+ const tasks = taskList(safeParams);
48
+ const mode = safeParams.chain?.length ? "chain" : safeParams.tasks?.length ? "parallel" : "single";
49
+ if (tasks.length === 1) {
50
+ return new Text(`${theme.fg("toolTitle", theme.bold("takomi_subagent "))}${theme.fg("accent", tasks[0]?.agent || "?")}`, 0, 0);
51
+ }
52
+ return new Text(`${theme.fg("toolTitle", theme.bold("takomi_subagent "))}${mode} (${tasks.length})`, 0, 0);
53
+ }
54
+
55
+ function resultText(result: ToolResult): string {
56
+ const text = typeof (result as any)?.content === "string"
57
+ ? (result as any).content
58
+ : Array.isArray((result as any)?.content)
59
+ ? (result as any).content.map((part: any) => part?.text ?? "").filter(Boolean).join("\n")
60
+ : JSON.stringify((result as any)?.details ?? {}, null, 2);
61
+ return sanitizeUntrustedText(text);
62
+ }
63
+
64
+ function extractPolicyNames(text: string): string[] {
65
+ const policyMatch = text.match(/Required policies:\n((?:- .+\n?)+)/);
66
+ return policyMatch?.[1]?.split("\n").map((line) => line.replace(/^[-\s]+/, "").trim()).filter(Boolean) ?? [];
67
+ }
68
+
69
+ function isPolicyGateBlock(text: string): boolean {
70
+ return /^Blocked\s+takomi_subagent:\s+required policy context had not been loaded yet\./m.test(text)
71
+ && /\nRequired policies:\n/.test(text)
72
+ && /\nLoaded policy context:\n/.test(text);
73
+ }
74
+
75
+ function renderPolicyGateBlock(text: string, expanded: boolean | undefined, theme: Theme): string {
76
+ const safeText = sanitizeUntrustedText(text);
77
+ const policies = extractPolicyNames(safeText);
78
+ const policyLabel = policies.length ? policies.join(", ") : "required policy";
79
+ if (!expanded) {
80
+ return [
81
+ theme.fg("warning", "⚠ takomi_subagent blocked"),
82
+ theme.fg("dim", `Required policy context loaded for this session: ${policyLabel}.`),
83
+ theme.fg("dim", "Retry the original tool call. Ctrl+O shows the complete policy detail."),
84
+ ].join("\n");
85
+ }
86
+ return [
87
+ theme.fg("warning", "⚠ takomi_subagent blocked"),
88
+ theme.fg("dim", "Policy context was loaded and passed back to the model; retry the original call."),
89
+ "",
90
+ safeText,
91
+ ].join("\n");
92
+ }
93
+
94
+ function uxTasks(details: any): TakomiUxTask[] {
95
+ return Array.isArray(details?.takomiUx?.tasks) ? details.takomiUx.tasks : [];
96
+ }
97
+
98
+ function taskFor(tasks: TakomiUxTask[], row: any, index: number): TakomiUxTask {
99
+ return tasks[index] ?? { agent: row?.agent ?? `task ${index + 1}`, task: row?.task ?? "", checklist: [] };
100
+ }
101
+
102
+ function checklistFor(task: TakomiUxTask, row: any): ReturnType<typeof resolvedChecklist> {
103
+ const texts = explicitAssistantTexts(row);
104
+ if (typeof row?.finalOutput === "string") texts.push(row.finalOutput);
105
+ return resolvedChecklist(task.checklist, texts);
106
+ }
107
+
108
+ function normalizedLine(value: string): string {
109
+ return sanitizeUntrustedText(value).replace(/\s+/g, " ").trim();
110
+ }
111
+
112
+ function nativePreview(row: any): string {
113
+ const output = typeof row?.truncation?.text === "string" && row.truncation.text.trim()
114
+ ? row.truncation.text
115
+ : finalAnswer(row);
116
+ return output.split(/\r?\n/).find((line: string) => line.trim())?.trim() ?? "";
117
+ }
118
+
119
+ function compactNarratives(rows: any[], isPartial: boolean): string[] {
120
+ const narratives: string[] = [];
121
+ for (const row of rows) {
122
+ const source = isPartial ? explicitAssistantTexts(row).at(-1) ?? "" : finalAnswer(row);
123
+ const lines = source.split(/\r?\n/)
124
+ .filter((line) => !/^\s*[-*+]\s+\[[ xX]\]\s+/.test(line))
125
+ .map(normalizedLine)
126
+ .filter(Boolean);
127
+ // Native compact only previews successful single results. Acceptance rejection
128
+ // changes exitCode to non-zero, so keep the first final line in that case.
129
+ if (!isPartial && rows.length === 1 && row?.exitCode === 0 && lines.length && normalizedLine(nativePreview(row)) === lines[0]) lines.shift();
130
+ for (const line of lines) {
131
+ if (narratives.some((existing) => normalizedLine(existing) === line)) continue;
132
+ narratives.push(line);
133
+ if (narratives.length >= COMPACT_NARRATIVE_LINE_BUDGET) return narratives;
134
+ }
135
+ }
136
+ return narratives;
137
+ }
138
+
139
+ function checklistSummary(rows: any[], tasks: TakomiUxTask[]): { summary: string; expanded: string } | undefined {
140
+ const checklists = rows.map((row, index) => checklistFor(taskFor(tasks, row, index), row));
141
+ const items = checklists.flat();
142
+ if (!items.length) return undefined;
143
+ const done = items.filter((item) => item.done).length;
144
+ const reported = items.filter((item) => item.done && item.stateSource === "agent-reported").length;
145
+ const summary = `checklist ${done}/${items.length} complete${reported ? ` · ${reported} agent-reported` : ""}`;
146
+ if (rows.length !== 1) return { summary, expanded: `Checklist: ${done}/${items.length} complete${reported ? ` (${reported} agent-reported)` : ""}` };
147
+ const labels = items.map((item) => `[${item.done ? "x" : " "}] ${item.text}`);
148
+ return { summary, expanded: `Checklist: ${labels.join(" · ")}` };
149
+ }
150
+
151
+ function fallbackSummary(rows: any[]): string | undefined {
152
+ const states = rows.map((row) => row?.takomiDetachedOutput)
153
+ .filter((value) => value && value.fallbackState !== "not-needed");
154
+ if (!states.length) return undefined;
155
+ const first = states[0];
156
+ return `output ${first.source} fallback: ${first.fallbackState}`;
157
+ }
158
+
159
+ function compactAddition(details: any, isPartial: boolean, theme: Theme): Component | undefined {
160
+ const rows = Array.isArray(details?.results) ? details.results : [];
161
+ if (!rows.length) return undefined;
162
+ const tasks = uxTasks(details);
163
+ const narratives = compactNarratives(rows, isPartial);
164
+ const checklist = checklistSummary(rows, tasks);
165
+ const checklistProvenance = details?.takomiDetached?.checklistProvenance === "unavailable-after-restart"
166
+ ? "checklist provenance unavailable after restart"
167
+ : checklist?.summary;
168
+ const provenance = [checklistProvenance, fallbackSummary(rows)].filter((value): value is string => Boolean(value));
169
+ if (!narratives.length && !provenance.length) return undefined;
170
+
171
+ return new WidthAwareLines((width) => {
172
+ const lines: string[] = [];
173
+ const narrativeWidth = Math.max(1, width - NARRATIVE_PREFIX.length);
174
+ for (const narrative of narratives) {
175
+ const bounded = boundNarrative(narrative, { maxLines: 1, maxColumns: narrativeWidth, from: "start" });
176
+ if (bounded.lines[0]) lines.push(theme.fg("dim", `${NARRATIVE_PREFIX}${bounded.lines[0]}`));
177
+ }
178
+ for (const item of provenance) {
179
+ if (lines.length >= COMPACT_CUSTOM_LINE_BUDGET) break;
180
+ const bounded = boundNarrative(item, { maxLines: 1, maxColumns: Math.max(1, width - 2), from: "start" });
181
+ if (bounded.lines[0]) lines.push(theme.fg("muted", ` ${bounded.lines[0]}`));
182
+ }
183
+ return lines.slice(0, COMPACT_CUSTOM_LINE_BUDGET);
184
+ });
185
+ }
186
+
187
+ function expandedAddition(details: any, theme: Theme): Component | undefined {
188
+ const rows = Array.isArray(details?.results) ? details.results : [];
189
+ if (!rows.length) return undefined;
190
+ const checklist = checklistSummary(rows, uxTasks(details));
191
+ const checklistProvenance = details?.takomiDetached?.checklistProvenance === "unavailable-after-restart"
192
+ ? "Checklist provenance unavailable after restart"
193
+ : checklist ? checklist.summary.replace(/^checklist/, "Checklist provenance:") : undefined;
194
+ const provenance = [checklistProvenance, fallbackSummary(rows)]
195
+ .filter(Boolean)
196
+ .join("\n");
197
+ if (!provenance) return undefined;
198
+
199
+ return new WidthAwareLines((width) => boundNarrative(provenance, {
200
+ maxLines: EXPANDED_CUSTOM_LINE_BUDGET,
201
+ maxColumns: width,
202
+ from: "start",
203
+ }).lines.slice(0, EXPANDED_CUSTOM_LINE_BUDGET).map((line) => theme.fg("dim", line)));
204
+ }
205
+
206
+ function compose(
207
+ native: Component | undefined,
208
+ addition: Component | undefined,
209
+ fallback: Component,
210
+ errorNotice?: Component,
211
+ ): Component {
212
+ if (!native && !addition && !errorNotice) return fallback;
213
+ const container = new Container();
214
+ if (errorNotice) container.addChild(errorNotice);
215
+ if (errorNotice && (native || addition)) container.addChild(new Spacer(1));
216
+ if (native) container.addChild(native);
217
+ if (native && addition) container.addChild(new Spacer(1));
218
+ if (addition) container.addChild(addition);
219
+ return container;
220
+ }
221
+
222
+ export function renderTakomiSubagentResult(
223
+ result: ToolResult,
224
+ options: { expanded?: boolean; isPartial?: boolean },
225
+ theme: Theme,
226
+ context: TakomiSubagentRenderContext & { isError?: boolean },
227
+ ): any {
228
+ if (options.isPartial) ensureTakomiSubagentResultHeartbeat(context);
229
+ else clearTakomiSubagentResultHeartbeat(context);
230
+
231
+ const safeResult = sanitizeUntrustedValue(result);
232
+ const text = resultText(safeResult);
233
+ if (isPolicyGateBlock(text)) return new Text(renderPolicyGateBlock(text, options.expanded, theme), 0, 0);
234
+
235
+ const frame = getTakomiSubagentHeartbeatFrame(context);
236
+ const native = renderNativeSubagentResult(
237
+ safeResult,
238
+ { expanded: options.expanded === true, isPartial: options.isPartial === true },
239
+ theme,
240
+ frame,
241
+ ) as Component | undefined;
242
+ const details: any = (safeResult as any)?.details ?? {};
243
+ const addition = options.expanded
244
+ ? expandedAddition(details, theme)
245
+ : compactAddition(details, options.isPartial === true, theme);
246
+ const isError = Boolean((safeResult as any)?.isError || context?.isError);
247
+ const errorNotice = isError ? new Text(theme.fg("error", "takomi_subagent failed"), 0, 0) : undefined;
248
+ const fallback = new Text(`${isError ? `${theme.fg("error", "failed")}\n` : ""}${text || "No result content."}`, 0, 0);
249
+ return compose(native, addition, fallback, errorNotice);
250
+ }