@buyi1net/pi-toolkit 0.0.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.
- package/LICENSE +21 -0
- package/README.md +45 -0
- package/assembler.ts +127 -0
- package/config.ts +201 -0
- package/i18n.ts +693 -0
- package/index.ts +99 -0
- package/menu/items.ts +264 -0
- package/menu/panels.ts +91 -0
- package/menu/settings-list.ts +84 -0
- package/menu/theme.ts +31 -0
- package/menu/toolkit-menu.ts +139 -0
- package/module.ts +156 -0
- package/modules/eyes/chain.ts +173 -0
- package/modules/eyes/config.ts +319 -0
- package/modules/eyes/index.ts +161 -0
- package/modules/eyes/menu.ts +828 -0
- package/modules/eyes/pi-model-backend.ts +245 -0
- package/modules/eyes/resilience.ts +161 -0
- package/modules/eyes/vision-bridge.ts +232 -0
- package/modules/eyes/vision-cache.ts +86 -0
- package/modules/eyes/vision-json.ts +19 -0
- package/modules/eyes/vision-preprocess.ts +192 -0
- package/modules/eyes/vision-probe.ts +21 -0
- package/modules/eyes/vision-prompt.ts +90 -0
- package/modules/eyes/vision-tool.ts +59 -0
- package/modules/eyes/vision-types.ts +33 -0
- package/modules/index.ts +10 -0
- package/modules/subagents/agents/researcher.md +51 -0
- package/modules/subagents/agents/scout.md +40 -0
- package/modules/subagents/agents/worker.md +79 -0
- package/modules/subagents/config.json.example +6 -0
- package/modules/subagents/config.ts +144 -0
- package/modules/subagents/index.ts +91 -0
- package/modules/subagents/menu.ts +388 -0
- package/modules/subagents/src/activity.ts +511 -0
- package/modules/subagents/src/agents.ts +126 -0
- package/modules/subagents/src/command.ts +37 -0
- package/modules/subagents/src/dependencies.ts +246 -0
- package/modules/subagents/src/diagnostics.ts +13 -0
- package/modules/subagents/src/display.ts +94 -0
- package/modules/subagents/src/headless.ts +342 -0
- package/modules/subagents/src/herdr.ts +203 -0
- package/modules/subagents/src/index.ts +2798 -0
- package/modules/subagents/src/inspect-tool.ts +839 -0
- package/modules/subagents/src/launch-config.ts +196 -0
- package/modules/subagents/src/layout-budget.ts +26 -0
- package/modules/subagents/src/list-tool.ts +292 -0
- package/modules/subagents/src/message-tool.ts +808 -0
- package/modules/subagents/src/names.ts +17 -0
- package/modules/subagents/src/pane-layout.ts +49 -0
- package/modules/subagents/src/params.ts +145 -0
- package/modules/subagents/src/renderers.ts +181 -0
- package/modules/subagents/src/result.ts +43 -0
- package/modules/subagents/src/retention.ts +114 -0
- package/modules/subagents/src/route-error.ts +212 -0
- package/modules/subagents/src/routing.ts +235 -0
- package/modules/subagents/src/runtime-registry.ts +159 -0
- package/modules/subagents/src/session.ts +801 -0
- package/modules/subagents/src/status.ts +513 -0
- package/modules/subagents/src/stop-tool.ts +235 -0
- package/modules/subagents/src/subagent-done.ts +590 -0
- package/modules/subagents/src/subagent-tool.ts +1155 -0
- package/modules/subagents/src/surface.ts +355 -0
- package/modules/subagents/src/team-dispatch-tool.ts +219 -0
- package/modules/subagents/src/team.ts +232 -0
- package/modules/subagents/src/tmux.ts +210 -0
- package/modules/subagents/src/tools/safe-bash.ts +72 -0
- package/modules/subagents/src/types.ts +131 -0
- package/modules/tui/adapter/provider-usage.ts +143 -0
- package/modules/tui/config.ts +50 -0
- package/modules/tui/index.ts +120 -0
- package/modules/tui/kernel/pkg/shared/grok-subscription.ts +169 -0
- package/modules/tui/kernel/pkg/shared/official-subscription.ts +237 -0
- package/modules/tui/kernel/pkg/shared/provider-catalog.ts +367 -0
- package/modules/tui/kernel/pkg/shared/provider-contracts.ts +150 -0
- package/modules/tui/kernel/pkg/shared/provider-display.ts +55 -0
- package/modules/tui/kernel/pkg/shared/provider-parsers.ts +171 -0
- package/modules/tui/kernel/pkg/shared/volcengine.ts +191 -0
- package/modules/tui/kernel/pkg/shared/zhipu.ts +149 -0
- package/modules/tui/kernel/pkg/usage-core/index.ts +335 -0
- package/modules/tui/kernel/pkg/usage-core/provider-routes.ts +187 -0
- package/modules/tui/kernel/pkg/usage-node/index.ts +635 -0
- package/modules/tui/kernel/pkg/usage-node/provider-usage.ts +388 -0
- package/modules/tui/kernel/usage-core.ts +2 -0
- package/modules/tui/kernel/usage-node.ts +2 -0
- package/modules/tui/menu.ts +418 -0
- package/modules/tui/plugin/editor.ts +396 -0
- package/modules/tui/plugin/footer.ts +161 -0
- package/modules/tui/plugin/index.ts +30 -0
- package/modules/tui/plugin/lifecycle.ts +637 -0
- package/modules/tui/plugin/package-order.ts +169 -0
- package/modules/tui/plugin/screen-transition.ts +203 -0
- package/modules/tui/plugin/settings-config.ts +297 -0
- package/modules/tui/plugin/status-sources.ts +49 -0
- package/modules/tui/plugin/transition-gate.ts +261 -0
- package/modules/tui/renderer/custom-header.ts +157 -0
- package/modules/tui/renderer/editor.ts +70 -0
- package/modules/tui/renderer/header.ts +72 -0
- package/modules/tui/renderer/icons.ts +149 -0
- package/modules/tui/renderer/pi-installer-logo.ts +194 -0
- package/modules/tui/status/auto-compaction.ts +67 -0
- package/modules/tui/status/project-status.ts +655 -0
- package/modules/tui/status/provider-status.ts +120 -0
- package/modules/tui/status/runtime-status.ts +307 -0
- package/modules/tui/status/session-status.ts +325 -0
- package/modules/tui/status/status-config.ts +95 -0
- package/modules/tui/status/status-segments.ts +263 -0
- package/modules/tui/status/turn-telemetry.ts +466 -0
- package/modules/tui/themes/LICENSE.pi-themes-bundle +21 -0
- package/modules/tui/themes/UPSTREAM.md +7 -0
- package/modules/tui/themes/catppuccin-latte.json +80 -0
- package/modules/tui/themes/catppuccin-mocha.json +79 -0
- package/modules/tui/themes/crimson-noir.json +85 -0
- package/modules/tui/themes/dracula.json +79 -0
- package/modules/tui/themes/everforest-dark.json +85 -0
- package/modules/tui/themes/gruvbox-dark.json +85 -0
- package/modules/tui/themes/gruvbox-light.json +85 -0
- package/modules/tui/themes/matrix.json +85 -0
- package/modules/tui/themes/nord.json +85 -0
- package/modules/tui/themes/one-dark.json +85 -0
- package/modules/tui/themes/rose-pine-dawn.json +85 -0
- package/modules/tui/themes/rose-pine.json +85 -0
- package/modules/tui/themes/solarized-dark.json +85 -0
- package/modules/tui/themes/solarized-light.json +85 -0
- package/modules/tui/themes/tokyo-night-storm.json +79 -0
- package/modules/tui/themes/tokyo-night.json +79 -0
- package/package.json +28 -0
- package/services.ts +38 -0
- package/toolkit.ts +147 -0
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
const MAX_NAME_LENGTH = 72;
|
|
2
|
+
|
|
3
|
+
/** 统一 pane、widget 和 registry 使用的显示名,避免不可寻址的空白字符。 */
|
|
4
|
+
export function normalizeSubagentName(name: string, fallback = "subagent"): string {
|
|
5
|
+
const normalized = name.replace(/\s+/g, " ").trim() || fallback;
|
|
6
|
+
return normalized.slice(0, MAX_NAME_LENGTH);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function sanitizeSubagentFileName(name: string, fallback = "subagent"): string {
|
|
10
|
+
const sanitized = name
|
|
11
|
+
.toLowerCase()
|
|
12
|
+
.replace(/[^a-z0-9\s-]/g, "")
|
|
13
|
+
.replace(/\s+/g, "-")
|
|
14
|
+
.replace(/-+/g, "-")
|
|
15
|
+
.replace(/^-|-$/g, "");
|
|
16
|
+
return sanitized || fallback;
|
|
17
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import type { PaneRect } from "./layout-budget.ts";
|
|
2
|
+
|
|
3
|
+
export type SplitDirection = "right" | "down";
|
|
4
|
+
|
|
5
|
+
export interface LayoutPane {
|
|
6
|
+
pane_id: string;
|
|
7
|
+
rect: PaneRect;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* 选择下一次分屏的目标 pane。
|
|
12
|
+
*
|
|
13
|
+
* 布局契约:主 pane(parentId)稳定占据一侧。已有本扩展管理的 pane 时,
|
|
14
|
+
* 只在 managed 区域内选面积最大的继续切分——无论 parent 面积多大都不再切
|
|
15
|
+
* parent,避免主 pane 被后续分屏挤成小窗;当前布局中已无任何 managed pane
|
|
16
|
+
* (尚未创建过,或均已被外部关闭)时,才首次/重新从 parent 切出。
|
|
17
|
+
* managedPaneIds 里已失效的 id(pane 已不存在)会被当前布局自然过滤,
|
|
18
|
+
* 不影响上述判断。
|
|
19
|
+
*/
|
|
20
|
+
export function chooseSplitTarget(
|
|
21
|
+
layout: LayoutPane[] | null,
|
|
22
|
+
parentId: string,
|
|
23
|
+
managedPaneIds: Set<string>,
|
|
24
|
+
): LayoutPane | null {
|
|
25
|
+
if (!layout) return null;
|
|
26
|
+
const managed = layout.filter((pane) => managedPaneIds.has(pane.pane_id));
|
|
27
|
+
if (managed.length === 0) {
|
|
28
|
+
return layout.find((pane) => pane.pane_id === parentId) ?? null;
|
|
29
|
+
}
|
|
30
|
+
return managed.reduce<LayoutPane | null>(
|
|
31
|
+
(largest, pane) =>
|
|
32
|
+
!largest || pane.rect.width * pane.rect.height > largest.rect.width * largest.rect.height ? pane : largest,
|
|
33
|
+
null,
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** 优先沿仍能满足最小尺寸的方向切分;两边都可用时沿较长轴切分。 */
|
|
38
|
+
export function chooseSplitDirection(
|
|
39
|
+
rect: PaneRect,
|
|
40
|
+
minCols: number,
|
|
41
|
+
minRows: number,
|
|
42
|
+
): SplitDirection | null {
|
|
43
|
+
const canSplitRight = rect.width >= minCols * 2;
|
|
44
|
+
const canSplitDown = rect.height >= minRows * 2;
|
|
45
|
+
if (canSplitRight && canSplitDown) return rect.width >= rect.height ? "right" : "down";
|
|
46
|
+
if (canSplitRight) return "right";
|
|
47
|
+
if (canSplitDown) return "down";
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import { Type, type Static } from "@sinclair/typebox";
|
|
2
|
+
|
|
3
|
+
export const MAX_COHORT_ID_LENGTH = 128;
|
|
4
|
+
|
|
5
|
+
export function normalizeCohortId(value: unknown): string | undefined {
|
|
6
|
+
if (typeof value !== "string") return undefined;
|
|
7
|
+
const normalized = value.trim();
|
|
8
|
+
return normalized || undefined;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function validateCohortId(value: unknown): string | null {
|
|
12
|
+
if (value == null) return null;
|
|
13
|
+
if (typeof value !== "string") {
|
|
14
|
+
return `Invalid cohortId: must be a string when provided; got ${String(value)}.`;
|
|
15
|
+
}
|
|
16
|
+
const normalized = value.trim();
|
|
17
|
+
if (!normalized) return "Invalid cohortId: must be non-empty after trimming.";
|
|
18
|
+
if (/[\u0000-\u001f\u007f]/.test(normalized)) {
|
|
19
|
+
return "Invalid cohortId: must not contain control characters.";
|
|
20
|
+
}
|
|
21
|
+
if (normalized.length > MAX_COHORT_ID_LENGTH) {
|
|
22
|
+
return `Invalid cohortId: must be at most ${MAX_COHORT_ID_LENGTH} characters.`;
|
|
23
|
+
}
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export const SubagentParams = Type.Object({
|
|
28
|
+
agent: Type.String({
|
|
29
|
+
description:
|
|
30
|
+
"Which agent to spawn (e.g. 'worker', 'scout', 'researcher'). This loads the agent's " +
|
|
31
|
+
"fixed profile — its model, tool loadout, and system prompt. Must be one of the available agents.",
|
|
32
|
+
}),
|
|
33
|
+
task: Type.String({ description: "Task/prompt for the sub-agent" }),
|
|
34
|
+
name: Type.Optional(
|
|
35
|
+
Type.String({
|
|
36
|
+
description:
|
|
37
|
+
"Optional cosmetic label for the subagent's pane and widget row. Defaults to the agent name. " +
|
|
38
|
+
"Has no effect on which agent runs — use `agent` for that.",
|
|
39
|
+
}),
|
|
40
|
+
),
|
|
41
|
+
cohortId: Type.Optional(
|
|
42
|
+
Type.String({
|
|
43
|
+
minLength: 1,
|
|
44
|
+
maxLength: MAX_COHORT_ID_LENGTH,
|
|
45
|
+
pattern: "^[^\\u0000-\\u001F\\u007F]+$",
|
|
46
|
+
description:
|
|
47
|
+
"Optional opaque group label for displaying and aggregating results from parallel subagents. " +
|
|
48
|
+
"It has no scheduling, dependency, ownership, retention, or cleanup semantics; resume keeps the original label.",
|
|
49
|
+
}),
|
|
50
|
+
),
|
|
51
|
+
model: Type.Optional(Type.String({ description: "Model override (overrides agent default and takes precedence over `tier`). Supports 'provider/id' and optional ':<thinking>' suffix" })),
|
|
52
|
+
tier: Type.Optional(
|
|
53
|
+
Type.String({
|
|
54
|
+
description:
|
|
55
|
+
"Model tier preset: 'fast', 'balanced', or 'deep' (aliases quick/balance|standard/strong accepted). " +
|
|
56
|
+
"Resolved from the pi-subagents config (models.fast/balanced/deep). An explicit `model` always wins over `tier`. " +
|
|
57
|
+
"A tier without a configured model fails with a clear error — models are never silently swapped.",
|
|
58
|
+
}),
|
|
59
|
+
),
|
|
60
|
+
thinking: Type.Optional(
|
|
61
|
+
Type.String({
|
|
62
|
+
description:
|
|
63
|
+
"Thinking level override for this spawn: off, minimal, low, medium, high, xhigh, or max. " +
|
|
64
|
+
"Takes precedence over the agent's frontmatter thinking and over any ':<thinking>' suffix on the model parameter.",
|
|
65
|
+
}),
|
|
66
|
+
),
|
|
67
|
+
cwd: Type.Optional(
|
|
68
|
+
Type.String({
|
|
69
|
+
description:
|
|
70
|
+
"Working directory for the sub-agent. The agent starts in this folder and picks up its local .pi/ config, CLAUDE.md, skills, and extensions. Use for role-specific subfolders.",
|
|
71
|
+
}),
|
|
72
|
+
),
|
|
73
|
+
dependsOn: Type.Optional(
|
|
74
|
+
Type.Array(Type.String(), {
|
|
75
|
+
description:
|
|
76
|
+
"Names of same-session subagents that MUST reach a terminal state BEFORE this call launches. " +
|
|
77
|
+
"Also works for sibling subagent calls issued in the SAME assistant message: the dependent call waits on the upstream completion promise instead of launching in parallel; independent siblings still run in parallel. " +
|
|
78
|
+
"When every dependency completed, a short upstream-results context block is appended to `task` (real extracted summaries, never fabricated). " +
|
|
79
|
+
"If any dependency failed or was cancelled, this call does NOT launch and returns a structured `dependencyException` in the tool result details — no pane/process is created. " +
|
|
80
|
+
"Rejected without launching: depending on this call's own name, on unknown names (not running, not announced in this message, and not in the session registry), and on interactive demo-pane subagents (they have no waitable terminal state). " +
|
|
81
|
+
"Dependencies that already finished in an earlier turn are resolved from the session's name registry / session files.",
|
|
82
|
+
}),
|
|
83
|
+
),
|
|
84
|
+
surface: Type.Optional(
|
|
85
|
+
Type.Union(
|
|
86
|
+
[
|
|
87
|
+
Type.Literal("auto"),
|
|
88
|
+
Type.Literal("background"),
|
|
89
|
+
Type.Literal("pane"),
|
|
90
|
+
],
|
|
91
|
+
{
|
|
92
|
+
description:
|
|
93
|
+
"Where the sub-agent runs. 'auto' (default): autonomous agents run headless as an independent background pi process " +
|
|
94
|
+
'(no pane/tab is created); demo/interactive agents run in a visible pane the user can operate. ' +
|
|
95
|
+
"'background': force headless (refused for interactive agents — they need a visible pane). 'pane': force a visible pane. " +
|
|
96
|
+
"Headless sub-agents are NOT visible and cannot be operated by the user.",
|
|
97
|
+
},
|
|
98
|
+
),
|
|
99
|
+
),
|
|
100
|
+
retention: Type.Optional(
|
|
101
|
+
Type.Union(
|
|
102
|
+
[
|
|
103
|
+
Type.Literal("auto"),
|
|
104
|
+
Type.Literal("preserve"),
|
|
105
|
+
Type.Literal("discard"),
|
|
106
|
+
],
|
|
107
|
+
{
|
|
108
|
+
description:
|
|
109
|
+
"Session retention for headless autonomous sub-agents after this call settles. 'auto' (default): delete this sub-agent's " +
|
|
110
|
+
"session JSONL, .loadout.json, context task/system-prompt files and activity/runtime registration after a SUCCESSFUL terminal state; " +
|
|
111
|
+
"failed, cancelled or handed-off runs are preserved so you can resume them with subagent_message. 'preserve': always keep everything. " +
|
|
112
|
+
"'discard': always delete after a terminal state (including failure/cancellation) — use when you know you will never resume. " +
|
|
113
|
+
"Visible pane / demo sub-agents are ALWAYS preserved regardless of this parameter. Deletion happens only after the result has been " +
|
|
114
|
+
"extracted and returned — it never affects the tool result.",
|
|
115
|
+
},
|
|
116
|
+
),
|
|
117
|
+
),
|
|
118
|
+
timeoutMs: Type.Optional(
|
|
119
|
+
Type.Integer({
|
|
120
|
+
minimum: 1000,
|
|
121
|
+
description:
|
|
122
|
+
"Optional wait timeout in milliseconds (>= 1000) for THIS tool call's hard-barrier wait on an AUTONOMOUS sub-agent. " +
|
|
123
|
+
"Omit it to wait indefinitely (existing behavior). When the timeout fires the call returns a structured non-terminal `timed-out` result: " +
|
|
124
|
+
"the sub-agent itself is NOT killed, NOT cancelled, nothing is cleaned up and no summary is fabricated — it keeps running exactly like an " +
|
|
125
|
+
"Escape-detached wait, and its real result is delivered later as a single steer message (lateDelivery). DependsOn consumers keep waiting " +
|
|
126
|
+
"for the real terminal state. The timeout covers the post-launch run wait only; it does not shorten the pre-launch dependsOn wait. " +
|
|
127
|
+
"Refused for interactive (demo) agents — their spawn returns immediately and has no wait to bound. " +
|
|
128
|
+
"Siblings in the same message return their own tool results independently; a timed-out call is not a batch summary.",
|
|
129
|
+
}),
|
|
130
|
+
),
|
|
131
|
+
member: Type.Optional(
|
|
132
|
+
Type.Boolean({
|
|
133
|
+
description:
|
|
134
|
+
"Spawn this sub-agent as a persistent TEAM MEMBER instead of a one-shot task. The call returns an immediate ack (no hard barrier): " +
|
|
135
|
+
"the member runs as a long-lived headless process, stays alive after each round, and receives follow-up rounds via team_dispatch. " +
|
|
136
|
+
"Each round's result is delivered once as a steer message when it ends. Members may message each other (fire-and-forget team_send) only " +
|
|
137
|
+
"along ACL edges declared in the agent profile's `peer-send`. Constraints (violations are refused): headless only (surface may not be " +
|
|
138
|
+
"'pane'), retention is forced to 'preserve' (may not be 'discard'), no timeoutMs, no dependsOn, and interactive (demo) agents cannot be " +
|
|
139
|
+
"members. Members cannot be dependsOn targets (no process-level terminal state). Stopping a member requires subagent_stop; host session " +
|
|
140
|
+
"shutdown or /reload terminates members and marks them offline (session preserved for explicit resume).",
|
|
141
|
+
}),
|
|
142
|
+
),
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
export type SubagentParams = Static<typeof SubagentParams>;
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { keyHint } from "@earendil-works/pi-coding-agent";
|
|
3
|
+
import { Box, Text, truncateToWidth } from "@earendil-works/pi-tui";
|
|
4
|
+
import type { SessionStats } from "./session.ts";
|
|
5
|
+
import { contextWindowFor, formatContextUsage, formatElapsed, formatUsageSegments } from "./display.ts";
|
|
6
|
+
|
|
7
|
+
export function registerSubagentRenderers(pi: ExtensionAPI): void {
|
|
8
|
+
pi.registerMessageRenderer("subagent_result", (message, options, theme) => {
|
|
9
|
+
const details = message.details as any;
|
|
10
|
+
if (!details) return undefined;
|
|
11
|
+
|
|
12
|
+
return {
|
|
13
|
+
invalidate() {},
|
|
14
|
+
render(width: number): string[] {
|
|
15
|
+
const name = details.name ?? "subagent";
|
|
16
|
+
const exitCode = details.exitCode ?? 0;
|
|
17
|
+
const errorMessage = typeof details.errorMessage === "string" ? details.errorMessage : "";
|
|
18
|
+
const userClosed = details.userClosed === true;
|
|
19
|
+
const late = details.lateDelivery === "detached";
|
|
20
|
+
const failed = !userClosed && (exitCode !== 0 || !!errorMessage);
|
|
21
|
+
const elapsed = details.elapsed != null ? formatElapsed(details.elapsed) : "?";
|
|
22
|
+
const bgFn = userClosed
|
|
23
|
+
? (text: string) => theme.bg("customMessageBg", text)
|
|
24
|
+
: failed
|
|
25
|
+
? (text: string) => theme.bg("toolErrorBg", text)
|
|
26
|
+
: (text: string) => theme.bg("toolSuccessBg", text);
|
|
27
|
+
const stats = (details.stats ?? null) as SessionStats | null;
|
|
28
|
+
const icon = userClosed ? theme.fg("warning", "✕") : failed ? theme.fg("error", "✗") : theme.fg("success", "✓");
|
|
29
|
+
const agentTag = details.agent ? theme.fg("dim", ` (${details.agent})`) : "";
|
|
30
|
+
const modelTag = stats?.model ? theme.fg("dim", ` (${stats.model})`) : "";
|
|
31
|
+
const lateTag = late ? theme.fg("accent", " (late delivery after detached wait)") : "";
|
|
32
|
+
const title = `${icon} ${theme.fg("toolTitle", theme.bold(name))}${agentTag}${modelTag} ${theme.fg("dim", "—")} `;
|
|
33
|
+
|
|
34
|
+
let header: string;
|
|
35
|
+
if (userClosed) {
|
|
36
|
+
// 用户直接关闭 pane:稳定分类,不是 provider/agent 错误。
|
|
37
|
+
header = `${title}${theme.fg("warning", "closed by user")} ${theme.fg("dim", `· ${elapsed}`)}${lateTag}`;
|
|
38
|
+
} else if (failed) {
|
|
39
|
+
const reason = errorMessage ? "failed (provider/agent error)" : `failed (exit ${exitCode})`;
|
|
40
|
+
header = `${title}${theme.fg("error", reason)} ${theme.fg("dim", `· ${elapsed}`)}${lateTag}`;
|
|
41
|
+
} else {
|
|
42
|
+
const toolPart = stats ? `${stats.toolCount} tools · ${elapsed}` : elapsed;
|
|
43
|
+
header = `${title}${theme.fg("dim", toolPart)}${lateTag}`;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
let usageLine: string | null = null;
|
|
47
|
+
if (stats) {
|
|
48
|
+
const segments = formatUsageSegments(stats).map((segment) => theme.fg("dim", segment));
|
|
49
|
+
if (stats.contextTokens > 0) {
|
|
50
|
+
const window = contextWindowFor(stats.model);
|
|
51
|
+
const context = formatContextUsage(stats.contextTokens, window);
|
|
52
|
+
const percent = window ? (stats.contextTokens / window) * 100 : 0;
|
|
53
|
+
segments.push(percent > 90 ? theme.fg("error", context) : percent > 70 ? theme.fg("warning", context) : theme.fg("dim", context));
|
|
54
|
+
}
|
|
55
|
+
if (segments.length > 0) usageLine = segments.join(theme.fg("dim", " "));
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const rawContent = typeof message.content === "string" ? message.content : "";
|
|
59
|
+
const summary = rawContent
|
|
60
|
+
.replace(/\n\nFollow up with subagent_message[\s\S]+$/, "")
|
|
61
|
+
.replace(`Sub-agent "${name}" completed (${elapsed}).\n\n`, "")
|
|
62
|
+
.replace(`Sub-agent "${name}" failed (exit code ${exitCode}).\n\n`, "")
|
|
63
|
+
.replace(new RegExp(`^Sub-agent "${name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}" failed after ${elapsed} \\(provider/agent error — auto-retry exhausted\\)\\.\\n\\n`), "");
|
|
64
|
+
|
|
65
|
+
const contentLines = [header];
|
|
66
|
+
if (usageLine) contentLines.push(usageLine);
|
|
67
|
+
if (options.expanded) {
|
|
68
|
+
if (summary) contentLines.push(...summary.split("\n").map((line) => truncateToWidth(line, Math.max(0, width - 6))));
|
|
69
|
+
if (details.name || details.sessionFile) {
|
|
70
|
+
contentLines.push("");
|
|
71
|
+
if (details.name) contentLines.push(theme.fg("dim", `Session preserved: message only for NEW instructions → subagent_message({ name: "${details.name}" })`));
|
|
72
|
+
if (details.sessionFile) contentLines.push(theme.fg("muted", `Session file: ${details.sessionFile}`));
|
|
73
|
+
}
|
|
74
|
+
} else {
|
|
75
|
+
if (summary) {
|
|
76
|
+
const previewLines = summary.split("\n").slice(0, 5);
|
|
77
|
+
contentLines.push(...previewLines.map((line) => theme.fg("dim", truncateToWidth(line, Math.max(0, width - 6)))));
|
|
78
|
+
if (summary.split("\n").length > 5) contentLines.push(theme.fg("muted", `… ${summary.split("\n").length - 5} more lines`));
|
|
79
|
+
}
|
|
80
|
+
contentLines.push(theme.fg("muted", keyHint("app.tools.expand", "to expand")));
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const box = new Box(1, 1, bgFn);
|
|
84
|
+
box.addChild(new Text(contentLines.join("\n"), 0, 0));
|
|
85
|
+
return ["", ...box.render(width)];
|
|
86
|
+
},
|
|
87
|
+
};
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
// 历史会话兼容:旧版本曾以 subagent_batch_result 消息回注批次结果,
|
|
91
|
+
// 生成逻辑已随硬屏障移除,但渲染器保留,保证 /reload 或恢复会话时旧消息仍可显示。
|
|
92
|
+
pi.registerMessageRenderer("subagent_batch_result", (message, options, theme) => {
|
|
93
|
+
const details = message.details as any;
|
|
94
|
+
const results = Array.isArray(details?.results) ? details.results : [];
|
|
95
|
+
if (results.length === 0) return undefined;
|
|
96
|
+
|
|
97
|
+
return {
|
|
98
|
+
invalidate() {},
|
|
99
|
+
render(width: number): string[] {
|
|
100
|
+
const lineWidth = Math.max(0, width - 6);
|
|
101
|
+
const contentLines = [
|
|
102
|
+
`${theme.fg("accent", "◆")} ${theme.fg("toolTitle", theme.bold(`Sub-agent batch · ${results.length} results`))}`,
|
|
103
|
+
];
|
|
104
|
+
for (const entry of results) {
|
|
105
|
+
const failed = (entry.exitCode ?? 0) !== 0 || !!entry.errorMessage;
|
|
106
|
+
const icon = failed ? theme.fg("error", "✗") : theme.fg("success", "✓");
|
|
107
|
+
const elapsed = entry.elapsed != null ? formatElapsed(entry.elapsed) : "?";
|
|
108
|
+
const agentTag = entry.agent ? theme.fg("dim", ` (${entry.agent})`) : "";
|
|
109
|
+
const reason = failed
|
|
110
|
+
? (entry.errorMessage ? "failed (provider/agent error)" : `failed (exit ${entry.exitCode})`)
|
|
111
|
+
: "completed";
|
|
112
|
+
contentLines.push(
|
|
113
|
+
`${icon} ${theme.fg("toolTitle", theme.bold(entry.name ?? "subagent"))}${agentTag}` +
|
|
114
|
+
theme.fg(failed ? "error" : "dim", ` — ${reason}`) +
|
|
115
|
+
theme.fg("dim", ` · ${elapsed}`),
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
if (options.expanded) {
|
|
119
|
+
for (const entry of results) {
|
|
120
|
+
contentLines.push("", theme.fg("toolTitle", theme.bold(entry.name ?? "subagent")));
|
|
121
|
+
const summary = typeof entry.summary === "string" ? entry.summary : "";
|
|
122
|
+
if (summary) {
|
|
123
|
+
contentLines.push(...summary.split("\n").map((line: string) => theme.fg("dim", truncateToWidth(line, lineWidth))));
|
|
124
|
+
}
|
|
125
|
+
if (entry.sessionFile) contentLines.push(theme.fg("muted", `Session file: ${entry.sessionFile}`));
|
|
126
|
+
}
|
|
127
|
+
} else {
|
|
128
|
+
contentLines.push(theme.fg("muted", keyHint("app.tools.expand", "to expand")));
|
|
129
|
+
}
|
|
130
|
+
const box = new Box(1, 1, (text: string) => theme.bg("customMessageBg", text));
|
|
131
|
+
box.addChild(new Text(contentLines.join("\n"), 0, 0));
|
|
132
|
+
return ["", ...box.render(width)];
|
|
133
|
+
},
|
|
134
|
+
};
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
pi.registerMessageRenderer("subagent_status", (message, options, theme) => {
|
|
138
|
+
const details = message.details as any;
|
|
139
|
+
const lines = Array.isArray(details?.lines) ? details.lines : [];
|
|
140
|
+
const overflow = typeof details?.overflow === "number" ? details.overflow : 0;
|
|
141
|
+
if (lines.length === 0 && overflow === 0) return undefined;
|
|
142
|
+
|
|
143
|
+
return {
|
|
144
|
+
invalidate() {},
|
|
145
|
+
render(width: number): string[] {
|
|
146
|
+
const lineWidth = Math.max(0, width - 6);
|
|
147
|
+
const contentLines = [
|
|
148
|
+
`${theme.fg("accent", "•")} ${theme.fg("toolTitle", theme.bold("Subagent status"))}`,
|
|
149
|
+
...lines.map((line: string) => theme.fg("dim", truncateToWidth(line, lineWidth))),
|
|
150
|
+
];
|
|
151
|
+
if (overflow > 0) contentLines.push(theme.fg("muted", `+${overflow} more running.`));
|
|
152
|
+
if (!options.expanded) contentLines.push(theme.fg("muted", keyHint("app.tools.expand", "to expand")));
|
|
153
|
+
const box = new Box(1, 1, (text: string) => theme.bg("customMessageBg", text));
|
|
154
|
+
box.addChild(new Text(contentLines.join("\n"), 0, 0));
|
|
155
|
+
return ["", ...box.render(width)];
|
|
156
|
+
},
|
|
157
|
+
};
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
pi.registerMessageRenderer("subagent_question", (message, options, theme) => {
|
|
161
|
+
const details = message.details as any;
|
|
162
|
+
if (!details) return undefined;
|
|
163
|
+
return {
|
|
164
|
+
invalidate() {},
|
|
165
|
+
render(width: number): string[] {
|
|
166
|
+
const name = details.name ?? "subagent";
|
|
167
|
+
const agentTag = details.agent ? theme.fg("dim", ` (${details.agent})`) : "";
|
|
168
|
+
const header = `${theme.fg("accent", "?")} ${theme.fg("toolTitle", theme.bold(name))}${agentTag} ${theme.fg("dim", "— asks a question")}`;
|
|
169
|
+
const contentLines = [header];
|
|
170
|
+
if (options.expanded) {
|
|
171
|
+
contentLines.push("", details.question ?? "", "", theme.fg("dim", `Reply: subagent_message({ name: "${name}", message: "…" })`));
|
|
172
|
+
} else {
|
|
173
|
+
contentLines.push(theme.fg("dim", (details.question ?? "").split("\n")[0].slice(0, Math.max(0, width - 10))), theme.fg("muted", keyHint("app.tools.expand", "to expand")));
|
|
174
|
+
}
|
|
175
|
+
const box = new Box(1, 1, (text: string) => theme.bg("toolSuccessBg", text));
|
|
176
|
+
box.addChild(new Text(contentLines.join("\n"), 0, 0));
|
|
177
|
+
return ["", ...box.render(width)];
|
|
178
|
+
},
|
|
179
|
+
};
|
|
180
|
+
});
|
|
181
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import {
|
|
3
|
+
findLastAssistantMessage,
|
|
4
|
+
getNewEntriesAsync,
|
|
5
|
+
getSessionId,
|
|
6
|
+
summarizeSessionStatsAsync,
|
|
7
|
+
type SessionStats,
|
|
8
|
+
} from "./session.ts";
|
|
9
|
+
|
|
10
|
+
export interface ResultExit {
|
|
11
|
+
exitCode: number;
|
|
12
|
+
errorMessage?: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface ExtractedSubagentResult {
|
|
16
|
+
summary: string;
|
|
17
|
+
sessionId: string | null;
|
|
18
|
+
stats: SessionStats | null;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export async function extractSubagentResult(
|
|
22
|
+
sessionFile: string,
|
|
23
|
+
exit: ResultExit,
|
|
24
|
+
afterLine = 0,
|
|
25
|
+
fallbackPrefix = "Sub-agent",
|
|
26
|
+
): Promise<ExtractedSubagentResult> {
|
|
27
|
+
const fallback = exit.errorMessage
|
|
28
|
+
? `Subagent error: ${exit.errorMessage}`
|
|
29
|
+
: exit.exitCode !== 0
|
|
30
|
+
? `${fallbackPrefix} exited with code ${exit.exitCode}`
|
|
31
|
+
: `${fallbackPrefix} exited without output`;
|
|
32
|
+
|
|
33
|
+
if (!existsSync(sessionFile)) {
|
|
34
|
+
return { summary: fallback, sessionId: null, stats: null };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const entries = await getNewEntriesAsync(sessionFile, afterLine);
|
|
38
|
+
return {
|
|
39
|
+
summary: findLastAssistantMessage(entries) ?? fallback,
|
|
40
|
+
sessionId: getSessionId(sessionFile),
|
|
41
|
+
stats: await summarizeSessionStatsAsync(sessionFile),
|
|
42
|
+
};
|
|
43
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { existsSync, rmSync } from "node:fs";
|
|
2
|
+
import { debugLog } from "./diagnostics.ts";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* 会话保留策略(spawn 参数 retention):
|
|
6
|
+
* - "auto"(默认):headless 自动子代理成功终态后清理本次会话工件;失败/
|
|
7
|
+
* 取消/handed-off 保留,主代理可用 subagent_message resume;
|
|
8
|
+
* - "preserve":永不清理(pane/演示子代理无论参数如何恒为 preserve);
|
|
9
|
+
* - "discard":headless 自动子代理到达非 handed-off 终态后一律清理
|
|
10
|
+
* (含失败/取消),显式声明"不需要 resume"。
|
|
11
|
+
*/
|
|
12
|
+
export type RetentionPolicy = "auto" | "preserve" | "discard";
|
|
13
|
+
|
|
14
|
+
export interface RetentionInput {
|
|
15
|
+
policy: RetentionPolicy;
|
|
16
|
+
/** 交互式演示子代理(可见 pane 归用户操作)。 */
|
|
17
|
+
interactive: boolean;
|
|
18
|
+
/** 运行载体;缺省(旧 mock/记录)按可见 pane 对待:未显式声明 headless 就不删。 */
|
|
19
|
+
kind?: "pane" | "headless";
|
|
20
|
+
cancelled: boolean;
|
|
21
|
+
failed: boolean;
|
|
22
|
+
handedOff: boolean;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export type RetentionDecision =
|
|
26
|
+
| { action: "preserve"; reason: string }
|
|
27
|
+
| { action: "delete"; reason: string };
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* 纯决策函数:给定策略与终态,判定是否清理会话工件。
|
|
31
|
+
* 恒 preserve 的边界(优先级从高到低):handed-off(进程仍在跑,交给恢复
|
|
32
|
+
* 路径)、pane/演示/未声明 headless 的运行(可见会话归用户;载体未知时
|
|
33
|
+
* 保守不删)。discard 只跳过这几类,失败/取消也清理——显式声明不需要
|
|
34
|
+
* resume。auto 在失败/取消时保留以便 resume。
|
|
35
|
+
*/
|
|
36
|
+
export function resolveRetentionDecision(input: RetentionInput): RetentionDecision {
|
|
37
|
+
if (input.handedOff) return { action: "preserve", reason: "handed-off" };
|
|
38
|
+
if (input.interactive || input.kind !== "headless") {
|
|
39
|
+
return { action: "preserve", reason: "pane" };
|
|
40
|
+
}
|
|
41
|
+
if (input.policy === "preserve") return { action: "preserve", reason: "policy=preserve" };
|
|
42
|
+
if (input.policy === "discard") return { action: "delete", reason: "policy=discard" };
|
|
43
|
+
if (input.cancelled) return { action: "preserve", reason: "cancelled" };
|
|
44
|
+
if (input.failed) return { action: "preserve", reason: "failed" };
|
|
45
|
+
return { action: "delete", reason: "auto-success" };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** 待处理 ask_question 的 sidecar;存在即表示还有未投递的问题。 */
|
|
49
|
+
export function pendingAskSidecarPath(sessionFile: string): string {
|
|
50
|
+
return `${sessionFile}.ask`;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** subagent-done 错误路径写入的退出 sidecar。 */
|
|
54
|
+
export function exitSidecarPath(sessionFile: string): string {
|
|
55
|
+
return `${sessionFile}.exit`;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export interface RetentionCleanupTarget {
|
|
59
|
+
/** 子代理会话 JSONL(清理主体)。 */
|
|
60
|
+
sessionFile: string;
|
|
61
|
+
/** 活动快照文件(artifactDir/subagent-activity/<id>.json)。 */
|
|
62
|
+
activityFile?: string;
|
|
63
|
+
/** 本次 spawn 写入父会话 artifactDir/context/ 的任务与系统提示文件。 */
|
|
64
|
+
contextFiles?: string[];
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export interface RetentionCleanupResult {
|
|
68
|
+
/** 实际删除的文件路径。 */
|
|
69
|
+
removed: string[];
|
|
70
|
+
/** 因待处理 .ask 而保留的文件路径(此时 removed 必为空)。 */
|
|
71
|
+
skipped: string[];
|
|
72
|
+
/** 删除失败的文件路径与原因;只能 debug,不覆盖子代理结果。 */
|
|
73
|
+
errors: Array<{ path: string; error: string }>;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* 删除一次 headless 自动子代理的全部会话工件:会话 JSONL、.loadout.json、
|
|
78
|
+
* .exit sidecar、activity 快照与 context 任务/系统提示文件。
|
|
79
|
+
* 存在待处理 .ask(未投递的问题)时整体跳过——不能删除运行中/待处理的
|
|
80
|
+
* 提问信号。单个文件删除失败只收集不抛出,由调用方 debug 上报。
|
|
81
|
+
*/
|
|
82
|
+
export function cleanupSubagentArtifacts(target: RetentionCleanupTarget): RetentionCleanupResult {
|
|
83
|
+
const askPath = pendingAskSidecarPath(target.sessionFile);
|
|
84
|
+
if (existsSync(askPath)) {
|
|
85
|
+
debugLog(`Subagent artifact cleanup skipped (pending ask sidecar): ${askPath}`);
|
|
86
|
+
return { removed: [], skipped: [askPath], errors: [] };
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const removed: string[] = [];
|
|
90
|
+
const errors: RetentionCleanupResult["errors"] = [];
|
|
91
|
+
const candidates = [
|
|
92
|
+
target.sessionFile,
|
|
93
|
+
`${target.sessionFile}.loadout.json`,
|
|
94
|
+
exitSidecarPath(target.sessionFile),
|
|
95
|
+
...(target.activityFile ? [target.activityFile] : []),
|
|
96
|
+
...(target.contextFiles ?? []),
|
|
97
|
+
];
|
|
98
|
+
for (const path of candidates) {
|
|
99
|
+
if (!existsSync(path)) continue;
|
|
100
|
+
try {
|
|
101
|
+
rmSync(path, { force: true });
|
|
102
|
+
removed.push(path);
|
|
103
|
+
} catch (error: any) {
|
|
104
|
+
errors.push({ path, error: error?.message ?? String(error) });
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
if (errors.length > 0) {
|
|
108
|
+
debugLog(
|
|
109
|
+
`Subagent artifact cleanup partially failed for ${target.sessionFile}`,
|
|
110
|
+
errors.map((item) => `${item.path}: ${item.error}`).join("; "),
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
return { removed, skipped: [], errors };
|
|
114
|
+
}
|