@ian-pascoe/pi-minimal-subagents 0.1.0 → 0.1.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/README.md +19 -4
- package/package.json +6 -6
- package/src/minimal-subagents-capabilities.ts +1 -8
- package/src/minimal-subagents-config.ts +47 -15
- package/src/minimal-subagents-coordinator.ts +5 -2
- package/src/minimal-subagents-sessions.ts +10 -0
- package/src/minimal-subagents-tools.ts +5 -4
- package/src/minimal-subagents-types.ts +11 -7
- package/src/minimal-subagents-ui.ts +209 -21
package/README.md
CHANGED
|
@@ -45,15 +45,15 @@ Project values override global values. Run `/reload` after editing either file.
|
|
|
45
45
|
`minimalSubagents.modelRoles` gives the parent agent advisory names for
|
|
46
46
|
eligible models. The extension defines no roles itself, performs no task
|
|
47
47
|
classification, and does not route launches. The parent still passes the
|
|
48
|
-
ordinary `model`
|
|
48
|
+
ordinary `model` and `thinking_level` arguments separately.
|
|
49
49
|
|
|
50
50
|
```json
|
|
51
51
|
{
|
|
52
52
|
"minimalSubagents": {
|
|
53
53
|
"modelRoles": {
|
|
54
|
-
"budget": "opencode-go/glm-5.2",
|
|
54
|
+
"budget": "opencode-go/glm-5.2:low",
|
|
55
55
|
"design": {
|
|
56
|
-
"model": "opencode-go/kimi-k3",
|
|
56
|
+
"model": "opencode-go/kimi-k3:high",
|
|
57
57
|
"hint": "UI design, visual critique, and frontend polish"
|
|
58
58
|
}
|
|
59
59
|
}
|
|
@@ -61,10 +61,18 @@ ordinary `model` argument and chooses `thinking_level` independently.
|
|
|
61
61
|
}
|
|
62
62
|
```
|
|
63
63
|
|
|
64
|
+
A recognized final suffix (`off`, `minimal`, `low`, `medium`, `high`, `xhigh`,
|
|
65
|
+
or `max`) is a preferred `thinking_level`, not part of the canonical model
|
|
66
|
+
passed to `subagent`. Unsuffixed roles leave thinking selection independent.
|
|
64
67
|
Role names and hints are trimmed, single-line text. Names may be up to 64
|
|
65
68
|
characters and hints up to 500 characters. Models use canonical
|
|
66
69
|
`provider/model` IDs and must be available under the effective `enabledModels`
|
|
67
|
-
scope.
|
|
70
|
+
scope. The resolver matches the complete authored model ID first, so real
|
|
71
|
+
colon-bearing IDs—including IDs ending in `:high`—remain exact model IDs;
|
|
72
|
+
only an otherwise-unmatched recognized final suffix is treated as a thinking
|
|
73
|
+
preference. A thinking level pinned in `enabledModels` neither supplies nor
|
|
74
|
+
constrains a role preference, and normal spawn-time model-capability clamping
|
|
75
|
+
still applies.
|
|
68
76
|
|
|
69
77
|
Global and project roles merge by name in settings order. Expanded role
|
|
70
78
|
objects merge by field; a project string replaces the whole global entry. A
|
|
@@ -127,3 +135,10 @@ falls back to unlinking its session file. Each Child Agent has a persistent
|
|
|
127
135
|
JSONL session. Append-only Root Agent Registry entries retain hierarchy and
|
|
128
136
|
Delivery Evidence across reloads. Forking cancels and drains active work, then
|
|
129
137
|
clones child session leaves so the fork receives an independent hierarchy.
|
|
138
|
+
|
|
139
|
+
## Status and TUI
|
|
140
|
+
|
|
141
|
+
Visible Child Agent rows show the canonical `provider/model:thinking` Runtime
|
|
142
|
+
Profile. Status uses the live Runtime Profile while a runtime exists and falls
|
|
143
|
+
back to the immutable Launch Contract otherwise. Live changes are observational:
|
|
144
|
+
they do not rewrite persistence or change nested spawn defaults.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ian-pascoe/pi-minimal-subagents",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Persistent nested subagents with bounded delegation for Pi",
|
|
6
6
|
"keywords": [
|
|
@@ -29,10 +29,6 @@
|
|
|
29
29
|
"access": "public",
|
|
30
30
|
"provenance": true
|
|
31
31
|
},
|
|
32
|
-
"scripts": {
|
|
33
|
-
"test": "vitest run --config ../../vitest.config.ts --root .",
|
|
34
|
-
"typecheck": "tsc --noEmit -p tsconfig.json"
|
|
35
|
-
},
|
|
36
32
|
"peerDependencies": {
|
|
37
33
|
"@earendil-works/pi-agent-core": "*",
|
|
38
34
|
"@earendil-works/pi-ai": "*",
|
|
@@ -47,5 +43,9 @@
|
|
|
47
43
|
"extensions": [
|
|
48
44
|
"./src/index.ts"
|
|
49
45
|
]
|
|
46
|
+
},
|
|
47
|
+
"scripts": {
|
|
48
|
+
"test": "vitest run --config ../../vitest.config.ts --root .",
|
|
49
|
+
"typecheck": "tsc --noEmit -p tsconfig.json"
|
|
50
50
|
}
|
|
51
|
-
}
|
|
51
|
+
}
|
|
@@ -16,8 +16,6 @@ export const COORDINATOR_TOOL_NAMES = [
|
|
|
16
16
|
|
|
17
17
|
const READ_TOOL_BUNDLE = ["read", "grep", "find", "ls"];
|
|
18
18
|
const MODIFY_TOOL_BUNDLE = [...READ_TOOL_BUNDLE, "bash", "edit", "write"];
|
|
19
|
-
const THINKING_SUFFIX_PATTERN = /:(?:off|minimal|low|medium|high|xhigh|max)$/;
|
|
20
|
-
|
|
21
19
|
interface ModelReference {
|
|
22
20
|
provider: string;
|
|
23
21
|
id: string;
|
|
@@ -28,11 +26,6 @@ interface ScopedModelReference {
|
|
|
28
26
|
thinkingLevel?: string;
|
|
29
27
|
}
|
|
30
28
|
|
|
31
|
-
/** Remove a recognized Pi thinking suffix without changing model IDs containing other colons. */
|
|
32
|
-
export function stripThinkingSuffix(modelPattern: string): string {
|
|
33
|
-
return modelPattern.replace(THINKING_SUFFIX_PATTERN, "");
|
|
34
|
-
}
|
|
35
|
-
|
|
36
29
|
/** Build the authenticated runtime model enum from Pi's already-resolved model scope. */
|
|
37
30
|
export function buildEligibleModelIds(input: {
|
|
38
31
|
availableModels: readonly ModelReference[];
|
|
@@ -47,7 +40,7 @@ export function buildEligibleModelIds(input: {
|
|
|
47
40
|
const result: string[] = [];
|
|
48
41
|
|
|
49
42
|
for (const model of source) {
|
|
50
|
-
const canonicalId =
|
|
43
|
+
const canonicalId = `${model.provider}/${model.id}`;
|
|
51
44
|
if (!seen.has(canonicalId)) {
|
|
52
45
|
seen.add(canonicalId);
|
|
53
46
|
result.push(canonicalId);
|
|
@@ -1,15 +1,15 @@
|
|
|
1
|
-
import {
|
|
2
|
-
DEFAULT_MAX_SUBAGENT_DEPTH,
|
|
3
|
-
stripThinkingSuffix,
|
|
4
|
-
} from "./minimal-subagents-capabilities.js";
|
|
1
|
+
import { DEFAULT_MAX_SUBAGENT_DEPTH, THINKING_LEVELS } from "./minimal-subagents-capabilities.js";
|
|
5
2
|
|
|
6
3
|
const MODEL_ROLE_NAME_MAX_LENGTH = 64;
|
|
7
4
|
const MODEL_ROLE_HINT_MAX_LENGTH = 500;
|
|
8
5
|
|
|
6
|
+
type ModelRoleThinkingLevel = (typeof THINKING_LEVELS)[number];
|
|
7
|
+
|
|
9
8
|
/** Describes one user-authored advisory model role shown to subagent callers. */
|
|
10
9
|
export interface MinimalSubagentsModelRole {
|
|
11
10
|
name: string;
|
|
12
11
|
model: string;
|
|
12
|
+
thinkingLevel?: ModelRoleThinkingLevel;
|
|
13
13
|
hint?: string;
|
|
14
14
|
}
|
|
15
15
|
|
|
@@ -89,6 +89,39 @@ function mergeModelRoleEntries(
|
|
|
89
89
|
return entries;
|
|
90
90
|
}
|
|
91
91
|
|
|
92
|
+
interface ResolvedModelRoleReference {
|
|
93
|
+
model: string;
|
|
94
|
+
thinkingLevel?: ModelRoleThinkingLevel;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function resolveThinkingLevelSuffix(suffix: string): ModelRoleThinkingLevel | undefined {
|
|
98
|
+
return THINKING_LEVELS.find((thinkingLevel) => thinkingLevel === suffix);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function resolveModelRoleReference(
|
|
102
|
+
model: string,
|
|
103
|
+
eligibleModels: ReadonlySet<string>,
|
|
104
|
+
path: string,
|
|
105
|
+
warnings: string[],
|
|
106
|
+
): ResolvedModelRoleReference | undefined {
|
|
107
|
+
if (eligibleModels.has(model)) return { model };
|
|
108
|
+
|
|
109
|
+
const separatorIndex = model.lastIndexOf(":");
|
|
110
|
+
if (separatorIndex >= 0) {
|
|
111
|
+
const prefix = model.slice(0, separatorIndex);
|
|
112
|
+
const suffix = model.slice(separatorIndex + 1);
|
|
113
|
+
if (eligibleModels.has(prefix)) {
|
|
114
|
+
const thinkingLevel = resolveThinkingLevelSuffix(suffix);
|
|
115
|
+
if (thinkingLevel !== undefined) return { model: prefix, thinkingLevel };
|
|
116
|
+
warnings.push(`${path}: unknown thinking level suffix: ${suffix}`);
|
|
117
|
+
return undefined;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
warnings.push(`${path}: model is not eligible: ${model}`);
|
|
122
|
+
return undefined;
|
|
123
|
+
}
|
|
124
|
+
|
|
92
125
|
function parseModelRoles(
|
|
93
126
|
entries: ReadonlyMap<string, ScopedSettingValue>,
|
|
94
127
|
eligibleModelIds: readonly string[],
|
|
@@ -125,16 +158,8 @@ function parseModelRoles(
|
|
|
125
158
|
warnings.push(`${path}: model must be a non-empty trimmed string`);
|
|
126
159
|
continue;
|
|
127
160
|
}
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
`${path}: thinking level suffixes are not allowed; choose thinking_level per spawn`,
|
|
131
|
-
);
|
|
132
|
-
continue;
|
|
133
|
-
}
|
|
134
|
-
if (!eligibleModels.has(model)) {
|
|
135
|
-
warnings.push(`${path}: model is not eligible: ${model}`);
|
|
136
|
-
continue;
|
|
137
|
-
}
|
|
161
|
+
const resolvedModel = resolveModelRoleReference(model, eligibleModels, path, warnings);
|
|
162
|
+
if (resolvedModel === undefined) continue;
|
|
138
163
|
|
|
139
164
|
const hint = isRecord(value) ? value.hint : undefined;
|
|
140
165
|
if (
|
|
@@ -148,7 +173,14 @@ function parseModelRoles(
|
|
|
148
173
|
warnings.push(`${path}.hint: expected trimmed single-line text up to 500 characters`);
|
|
149
174
|
continue;
|
|
150
175
|
}
|
|
151
|
-
roles.push({
|
|
176
|
+
roles.push({
|
|
177
|
+
name,
|
|
178
|
+
model: resolvedModel.model,
|
|
179
|
+
...(resolvedModel.thinkingLevel === undefined
|
|
180
|
+
? {}
|
|
181
|
+
: { thinkingLevel: resolvedModel.thinkingLevel }),
|
|
182
|
+
...(hint === undefined ? {} : { hint }),
|
|
183
|
+
});
|
|
152
184
|
}
|
|
153
185
|
return roles;
|
|
154
186
|
}
|
|
@@ -993,6 +993,10 @@ export class MinimalSubagentsCoordinator {
|
|
|
993
993
|
const elapsed = agent.active_turn_started_at
|
|
994
994
|
? Math.max(0, this.now().getTime() - new Date(agent.active_turn_started_at).getTime())
|
|
995
995
|
: undefined;
|
|
996
|
+
const runtimeProfile = this.runtimes.get(agent.agent_id)?.getRuntimeProfile() ?? {
|
|
997
|
+
model: agent.launch_contract.model,
|
|
998
|
+
thinking_level: agent.launch_contract.thinking_level,
|
|
999
|
+
};
|
|
996
1000
|
return {
|
|
997
1001
|
agent_id: agent.agent_id,
|
|
998
1002
|
parent_id: agent.parent_id,
|
|
@@ -1002,8 +1006,7 @@ export class MinimalSubagentsCoordinator {
|
|
|
1002
1006
|
latest_turn: agent.latest_result
|
|
1003
1007
|
? { turn_id: agent.latest_result.turn_id, status: agent.latest_result.status }
|
|
1004
1008
|
: undefined,
|
|
1005
|
-
|
|
1006
|
-
thinking_level: agent.launch_contract.thinking_level,
|
|
1009
|
+
...runtimeProfile,
|
|
1007
1010
|
tools: [...agent.launch_contract.ordinary_tools],
|
|
1008
1011
|
elapsed_ms: elapsed ?? agent.latest_result?.elapsed_ms,
|
|
1009
1012
|
latest_activity_at: agent.latest_activity_at ?? agent.created_at,
|
|
@@ -37,6 +37,7 @@ import type {
|
|
|
37
37
|
PersistedSessionIdentity,
|
|
38
38
|
ProjectContextMode,
|
|
39
39
|
RuntimeCreationRequest,
|
|
40
|
+
RuntimeProfile,
|
|
40
41
|
RuntimeTurnOutcome,
|
|
41
42
|
} from "./minimal-subagents-types.js";
|
|
42
43
|
|
|
@@ -329,6 +330,15 @@ class PiChildAgentRuntime implements ChildAgentRuntime {
|
|
|
329
330
|
this.session.dispose();
|
|
330
331
|
}
|
|
331
332
|
|
|
333
|
+
getRuntimeProfile(): RuntimeProfile | undefined {
|
|
334
|
+
const model = this.session.model;
|
|
335
|
+
if (!model) return undefined;
|
|
336
|
+
return {
|
|
337
|
+
model: `${model.provider}/${model.id}`,
|
|
338
|
+
thinking_level: this.session.thinkingLevel,
|
|
339
|
+
};
|
|
340
|
+
}
|
|
341
|
+
|
|
332
342
|
snapshotCommittedMessages(): AgentMessage[] {
|
|
333
343
|
return snapshotCommittedContext(this.session.messages, this.session.isStreaming);
|
|
334
344
|
}
|
|
@@ -40,12 +40,13 @@ function buildModelRolePromptGuidelines(
|
|
|
40
40
|
modelRoles: readonly MinimalSubagentsModelRole[],
|
|
41
41
|
): string[] | undefined {
|
|
42
42
|
if (modelRoles.length === 0) return undefined;
|
|
43
|
-
const roleLines = modelRoles.map(
|
|
44
|
-
|
|
45
|
-
|
|
43
|
+
const roleLines = modelRoles.map((role) => {
|
|
44
|
+
const thinkingGuidance = role.thinkingLevel ? `, thinking_level=${role.thinkingLevel}` : "";
|
|
45
|
+
return ` - ${role.name} → model=${role.model}${thinkingGuidance}${role.hint ? ` — ${role.hint}` : ""}`;
|
|
46
|
+
});
|
|
46
47
|
return [
|
|
47
48
|
["Configured model roles are guidance, not constraints:", ...roleLines].join("\n"),
|
|
48
|
-
"Choose a model based on the task.
|
|
49
|
+
"Choose a model based on the task. A listed thinking_level is a preference, not a constraint. Callers choose thinking_level independently for roles without one.",
|
|
49
50
|
];
|
|
50
51
|
}
|
|
51
52
|
|
|
@@ -22,6 +22,12 @@ export type AgentAvailability = "available" | "unavailable";
|
|
|
22
22
|
/** Classifies active and terminal persistent subagent turn outcomes. */
|
|
23
23
|
export type TurnStatus = "running" | "completed" | "failed" | "cancelled" | "interrupted";
|
|
24
24
|
|
|
25
|
+
/** Describes the canonical model and resolved thinking level currently used by a Child Agent. */
|
|
26
|
+
export interface RuntimeProfile {
|
|
27
|
+
model: string;
|
|
28
|
+
thinking_level: ThinkingLevel;
|
|
29
|
+
}
|
|
30
|
+
|
|
25
31
|
/** Defines the validated launch contract accepted by the subagent tool. */
|
|
26
32
|
export interface SpawnParameters {
|
|
27
33
|
task: string;
|
|
@@ -59,16 +65,14 @@ export interface AgentMessageResult {
|
|
|
59
65
|
error?: string;
|
|
60
66
|
}
|
|
61
67
|
|
|
62
|
-
/** Provides bounded hierarchy and
|
|
63
|
-
export interface AgentSummary {
|
|
68
|
+
/** Provides bounded hierarchy, usage, and best-known Runtime Profile data for one persistent agent. */
|
|
69
|
+
export interface AgentSummary extends RuntimeProfile {
|
|
64
70
|
agent_id: string;
|
|
65
71
|
parent_id: string;
|
|
66
72
|
state: AgentState;
|
|
67
73
|
availability: AgentAvailability;
|
|
68
74
|
active_turn_id?: string;
|
|
69
75
|
latest_turn?: Pick<TurnResult, "turn_id" | "status">;
|
|
70
|
-
model: string;
|
|
71
|
-
thinking_level: ThinkingLevel;
|
|
72
76
|
tools: string[];
|
|
73
77
|
elapsed_ms?: number;
|
|
74
78
|
latest_activity?: string;
|
|
@@ -125,11 +129,9 @@ export interface DeleteResult {
|
|
|
125
129
|
}
|
|
126
130
|
|
|
127
131
|
/** Persists immutable context, model, thinking, and ordinary-tool launch choices. */
|
|
128
|
-
export interface LaunchContract {
|
|
132
|
+
export interface LaunchContract extends RuntimeProfile {
|
|
129
133
|
session_context: SessionContextMode;
|
|
130
134
|
project_context: ProjectContextMode;
|
|
131
|
-
model: string;
|
|
132
|
-
thinking_level: ThinkingLevel;
|
|
133
135
|
tools: ToolSelection | undefined;
|
|
134
136
|
ordinary_tools: string[];
|
|
135
137
|
delegation?: DelegationMode;
|
|
@@ -184,6 +186,8 @@ export interface ChildAgentRuntime {
|
|
|
184
186
|
steerCoordinatorMessage(message: CoordinatorMessage): Promise<void>;
|
|
185
187
|
abort(): Promise<void>;
|
|
186
188
|
dispose(): void;
|
|
189
|
+
/** Return the live Runtime Profile, or undefined when the SDK session has no model. */
|
|
190
|
+
getRuntimeProfile(): RuntimeProfile | undefined;
|
|
187
191
|
snapshotCommittedMessages(): AgentMessage[];
|
|
188
192
|
hasDeliveryEvidence(sourceAgentId: string, sourceTurnId: string): boolean;
|
|
189
193
|
getUsage(): Usage | undefined;
|
|
@@ -1,13 +1,24 @@
|
|
|
1
1
|
import type { Theme } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
sliceByColumn,
|
|
5
|
+
truncateToWidth,
|
|
6
|
+
visibleWidth,
|
|
7
|
+
type Component,
|
|
8
|
+
type TUI,
|
|
9
|
+
} from "@earendil-works/pi-tui";
|
|
4
10
|
import type { MinimalSubagentsCoordinator } from "./minimal-subagents-coordinator.js";
|
|
5
11
|
import {
|
|
6
12
|
formatSubagentDuration,
|
|
7
13
|
renderSubagentStatusLabel,
|
|
8
14
|
renderSubagentStatusSymbol,
|
|
9
15
|
} from "./minimal-subagents-rendering.js";
|
|
10
|
-
import type {
|
|
16
|
+
import type {
|
|
17
|
+
AgentSummary,
|
|
18
|
+
HierarchyStatusResult,
|
|
19
|
+
RuntimeProfile,
|
|
20
|
+
TurnStatus,
|
|
21
|
+
} from "./minimal-subagents-types.js";
|
|
11
22
|
|
|
12
23
|
const MINIMAL_SUBAGENTS_UI_KEY = "minimal-subagents";
|
|
13
24
|
const MINIMAL_SUBAGENTS_RECENT_LIMIT = 3;
|
|
@@ -20,6 +31,7 @@ export interface MinimalSubagentsWidgetRow {
|
|
|
20
31
|
depth: number;
|
|
21
32
|
status: TurnStatus | "idle" | "unavailable";
|
|
22
33
|
elapsedMs?: number;
|
|
34
|
+
runtimeProfile: RuntimeProfile;
|
|
23
35
|
task?: string;
|
|
24
36
|
structural: boolean;
|
|
25
37
|
}
|
|
@@ -122,6 +134,10 @@ export function buildMinimalSubagentsWidgetView(
|
|
|
122
134
|
depth: item.depth,
|
|
123
135
|
status: agentTerminalStatus(item.agent),
|
|
124
136
|
elapsedMs: item.agent.elapsed_ms,
|
|
137
|
+
runtimeProfile: {
|
|
138
|
+
model: item.agent.model,
|
|
139
|
+
thinking_level: item.agent.thinking_level,
|
|
140
|
+
},
|
|
125
141
|
task: structural ? undefined : item.agent.task,
|
|
126
142
|
structural,
|
|
127
143
|
};
|
|
@@ -135,6 +151,187 @@ export function buildMinimalSubagentsWidgetView(
|
|
|
135
151
|
};
|
|
136
152
|
}
|
|
137
153
|
|
|
154
|
+
const MINIMAL_SUBAGENTS_WIDGET_SEPARATOR_TEXT = " · ";
|
|
155
|
+
const MINIMAL_SUBAGENTS_WIDGET_ELLIPSIS = "…";
|
|
156
|
+
|
|
157
|
+
interface MinimalSubagentsWidgetRowParts {
|
|
158
|
+
identity: string;
|
|
159
|
+
status: string;
|
|
160
|
+
profile: string;
|
|
161
|
+
task?: string;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function joinMinimalSubagentsWidgetRow(
|
|
165
|
+
parts: MinimalSubagentsWidgetRowParts,
|
|
166
|
+
separator: string,
|
|
167
|
+
): string {
|
|
168
|
+
return [parts.identity, parts.status, parts.profile, parts.task]
|
|
169
|
+
.filter((part): part is string => part !== undefined)
|
|
170
|
+
.join(separator);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function formatMinimalSubagentsRuntimeProfile(
|
|
174
|
+
profile: RuntimeProfile,
|
|
175
|
+
maxWidth?: number,
|
|
176
|
+
): string | undefined {
|
|
177
|
+
const suffix = `:${profile.thinking_level}`;
|
|
178
|
+
const fullProfile = `${profile.model}${suffix}`;
|
|
179
|
+
if (maxWidth === undefined || visibleWidth(fullProfile) <= maxWidth) return fullProfile;
|
|
180
|
+
|
|
181
|
+
const shortestProfile = `${MINIMAL_SUBAGENTS_WIDGET_ELLIPSIS}${suffix}`;
|
|
182
|
+
if (maxWidth < visibleWidth(shortestProfile)) return undefined;
|
|
183
|
+
const modelWidth = maxWidth - visibleWidth(suffix);
|
|
184
|
+
const prefix = sliceByColumn(
|
|
185
|
+
profile.model,
|
|
186
|
+
0,
|
|
187
|
+
Math.max(0, modelWidth - visibleWidth(MINIMAL_SUBAGENTS_WIDGET_ELLIPSIS)),
|
|
188
|
+
true,
|
|
189
|
+
);
|
|
190
|
+
return `${prefix}${MINIMAL_SUBAGENTS_WIDGET_ELLIPSIS}${suffix}`;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function renderMinimalSubagentsWidgetRowParts(
|
|
194
|
+
row: MinimalSubagentsWidgetRow,
|
|
195
|
+
task: string | undefined,
|
|
196
|
+
duration: string | undefined,
|
|
197
|
+
profile: string,
|
|
198
|
+
theme: Theme,
|
|
199
|
+
): MinimalSubagentsWidgetRowParts {
|
|
200
|
+
const branch = row.depth > 0 ? `${" ".repeat(row.depth)}╰─ ` : " ";
|
|
201
|
+
const styledBranch = theme.fg("borderMuted", branch);
|
|
202
|
+
const agentId = row.structural ? theme.fg("muted", row.agentId) : theme.bold(row.agentId);
|
|
203
|
+
const identity = `${styledBranch}${renderSubagentStatusSymbol(theme, row.status)} ${agentId}`;
|
|
204
|
+
const status = `${renderSubagentStatusLabel(theme, row.status)}${
|
|
205
|
+
duration ? ` ${theme.fg("muted", duration)}` : ""
|
|
206
|
+
}`;
|
|
207
|
+
return {
|
|
208
|
+
identity,
|
|
209
|
+
status,
|
|
210
|
+
profile: theme.fg("muted", profile),
|
|
211
|
+
task: task ? theme.fg("muted", task) : undefined,
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function minimalSubagentsWidgetRowFits(
|
|
216
|
+
parts: MinimalSubagentsWidgetRowParts,
|
|
217
|
+
separator: string,
|
|
218
|
+
width: number,
|
|
219
|
+
): boolean {
|
|
220
|
+
return visibleWidth(joinMinimalSubagentsWidgetRow(parts, separator)) <= width;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function minimalSubagentsWidgetProfileBudget(
|
|
224
|
+
row: MinimalSubagentsWidgetRow,
|
|
225
|
+
task: string | undefined,
|
|
226
|
+
duration: string | undefined,
|
|
227
|
+
separator: string,
|
|
228
|
+
theme: Theme,
|
|
229
|
+
width: number,
|
|
230
|
+
): number {
|
|
231
|
+
const fixedParts = renderMinimalSubagentsWidgetRowParts(row, task, duration, "", theme);
|
|
232
|
+
return width - visibleWidth(joinMinimalSubagentsWidgetRow(fixedParts, separator));
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function renderMinimalSubagentsWidgetRow(
|
|
236
|
+
row: MinimalSubagentsWidgetRow,
|
|
237
|
+
width: number,
|
|
238
|
+
theme: Theme,
|
|
239
|
+
): string {
|
|
240
|
+
const separator = theme.fg("dim", MINIMAL_SUBAGENTS_WIDGET_SEPARATOR_TEXT);
|
|
241
|
+
const task = row.task?.replace(/\s+/g, " ").trim() || undefined;
|
|
242
|
+
const duration = row.status === "unavailable" ? undefined : formatSubagentDuration(row.elapsedMs);
|
|
243
|
+
const fullProfile = formatMinimalSubagentsRuntimeProfile(row.runtimeProfile);
|
|
244
|
+
if (!fullProfile) return "";
|
|
245
|
+
|
|
246
|
+
const completeParts = renderMinimalSubagentsWidgetRowParts(
|
|
247
|
+
row,
|
|
248
|
+
task,
|
|
249
|
+
duration,
|
|
250
|
+
fullProfile,
|
|
251
|
+
theme,
|
|
252
|
+
);
|
|
253
|
+
if (minimalSubagentsWidgetRowFits(completeParts, separator, width)) {
|
|
254
|
+
return joinMinimalSubagentsWidgetRow(completeParts, separator);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
const partsWithoutTask = renderMinimalSubagentsWidgetRowParts(
|
|
258
|
+
row,
|
|
259
|
+
undefined,
|
|
260
|
+
duration,
|
|
261
|
+
fullProfile,
|
|
262
|
+
theme,
|
|
263
|
+
);
|
|
264
|
+
if (minimalSubagentsWidgetRowFits(partsWithoutTask, separator, width)) {
|
|
265
|
+
return joinMinimalSubagentsWidgetRow(partsWithoutTask, separator);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
const profileBudget = minimalSubagentsWidgetProfileBudget(
|
|
269
|
+
row,
|
|
270
|
+
undefined,
|
|
271
|
+
duration,
|
|
272
|
+
separator,
|
|
273
|
+
theme,
|
|
274
|
+
width,
|
|
275
|
+
);
|
|
276
|
+
const shortenedProfile = formatMinimalSubagentsRuntimeProfile(row.runtimeProfile, profileBudget);
|
|
277
|
+
if (shortenedProfile) {
|
|
278
|
+
const shortenedParts = renderMinimalSubagentsWidgetRowParts(
|
|
279
|
+
row,
|
|
280
|
+
undefined,
|
|
281
|
+
duration,
|
|
282
|
+
shortenedProfile,
|
|
283
|
+
theme,
|
|
284
|
+
);
|
|
285
|
+
if (minimalSubagentsWidgetRowFits(shortenedParts, separator, width)) {
|
|
286
|
+
return joinMinimalSubagentsWidgetRow(shortenedParts, separator);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
if (duration) {
|
|
291
|
+
const noDurationBudget = minimalSubagentsWidgetProfileBudget(
|
|
292
|
+
row,
|
|
293
|
+
undefined,
|
|
294
|
+
undefined,
|
|
295
|
+
separator,
|
|
296
|
+
theme,
|
|
297
|
+
width,
|
|
298
|
+
);
|
|
299
|
+
const noDurationProfile = formatMinimalSubagentsRuntimeProfile(
|
|
300
|
+
row.runtimeProfile,
|
|
301
|
+
noDurationBudget,
|
|
302
|
+
);
|
|
303
|
+
if (noDurationProfile) {
|
|
304
|
+
const noDurationParts = renderMinimalSubagentsWidgetRowParts(
|
|
305
|
+
row,
|
|
306
|
+
undefined,
|
|
307
|
+
undefined,
|
|
308
|
+
noDurationProfile,
|
|
309
|
+
theme,
|
|
310
|
+
);
|
|
311
|
+
if (minimalSubagentsWidgetRowFits(noDurationParts, separator, width)) {
|
|
312
|
+
return joinMinimalSubagentsWidgetRow(noDurationParts, separator);
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
const shortestProfile = formatMinimalSubagentsRuntimeProfile(
|
|
318
|
+
row.runtimeProfile,
|
|
319
|
+
visibleWidth(`${MINIMAL_SUBAGENTS_WIDGET_ELLIPSIS}:${row.runtimeProfile.thinking_level}`),
|
|
320
|
+
)!;
|
|
321
|
+
const lastResortParts = renderMinimalSubagentsWidgetRowParts(
|
|
322
|
+
row,
|
|
323
|
+
undefined,
|
|
324
|
+
undefined,
|
|
325
|
+
shortestProfile,
|
|
326
|
+
theme,
|
|
327
|
+
);
|
|
328
|
+
return truncateToWidth(
|
|
329
|
+
joinMinimalSubagentsWidgetRow(lastResortParts, separator),
|
|
330
|
+
width,
|
|
331
|
+
MINIMAL_SUBAGENTS_WIDGET_ELLIPSIS,
|
|
332
|
+
);
|
|
333
|
+
}
|
|
334
|
+
|
|
138
335
|
/** Render a responsive widget snapshot with ANSI-safe terminal-width truncation. */
|
|
139
336
|
export function renderMinimalSubagentsWidgetLines(
|
|
140
337
|
view: MinimalSubagentsWidgetView,
|
|
@@ -142,7 +339,7 @@ export function renderMinimalSubagentsWidgetLines(
|
|
|
142
339
|
theme: Theme,
|
|
143
340
|
): string[] {
|
|
144
341
|
if (width <= 0) return [];
|
|
145
|
-
const separator = theme.fg("dim",
|
|
342
|
+
const separator = theme.fg("dim", MINIMAL_SUBAGENTS_WIDGET_SEPARATOR_TEXT);
|
|
146
343
|
const activity =
|
|
147
344
|
view.runningCount > 0
|
|
148
345
|
? theme.fg("accent", `${view.runningCount} running`)
|
|
@@ -159,27 +356,18 @@ export function renderMinimalSubagentsWidgetLines(
|
|
|
159
356
|
.filter((part): part is string => Boolean(part))
|
|
160
357
|
.join(separator),
|
|
161
358
|
width,
|
|
162
|
-
|
|
359
|
+
MINIMAL_SUBAGENTS_WIDGET_ELLIPSIS,
|
|
163
360
|
),
|
|
164
361
|
];
|
|
165
|
-
for (const row of view.rows)
|
|
166
|
-
const duration = formatSubagentDuration(row.elapsedMs);
|
|
167
|
-
const task = row.task?.replace(/\s+/g, " ").trim();
|
|
168
|
-
const branch =
|
|
169
|
-
row.depth > 0
|
|
170
|
-
? theme.fg("borderMuted", `${" ".repeat(row.depth)}╰─ `)
|
|
171
|
-
: theme.fg("borderMuted", " ");
|
|
172
|
-
const agentId = row.structural ? theme.fg("muted", row.agentId) : theme.bold(row.agentId);
|
|
173
|
-
const parts = [
|
|
174
|
-
`${branch}${renderSubagentStatusSymbol(theme, row.status)} ${agentId}`,
|
|
175
|
-
row.structural ? undefined : renderSubagentStatusLabel(theme, row.status),
|
|
176
|
-
task ? theme.fg("muted", task) : undefined,
|
|
177
|
-
duration ? theme.fg("muted", duration) : undefined,
|
|
178
|
-
].filter((part): part is string => Boolean(part));
|
|
179
|
-
lines.push(truncateToWidth(parts.join(separator), width, "…"));
|
|
180
|
-
}
|
|
362
|
+
for (const row of view.rows) lines.push(renderMinimalSubagentsWidgetRow(row, width, theme));
|
|
181
363
|
if (view.overflowCount > 0) {
|
|
182
|
-
lines.push(
|
|
364
|
+
lines.push(
|
|
365
|
+
truncateToWidth(
|
|
366
|
+
theme.fg("dim", ` … +${view.overflowCount} more`),
|
|
367
|
+
width,
|
|
368
|
+
MINIMAL_SUBAGENTS_WIDGET_ELLIPSIS,
|
|
369
|
+
),
|
|
370
|
+
);
|
|
183
371
|
}
|
|
184
372
|
return lines;
|
|
185
373
|
}
|