pi-subagents 0.22.0 → 0.23.0
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/CHANGELOG.md +12 -0
- package/README.md +9 -7
- package/package.json +1 -1
- package/skills/pi-subagents/SKILL.md +6 -5
- package/src/agents/agent-management.ts +3 -1
- package/src/agents/agents.ts +22 -4
- package/src/extension/doctor.ts +1 -0
- package/src/intercom/intercom-bridge.ts +137 -11
- package/src/manager-ui/agent-manager.ts +6 -5
- package/src/runs/background/async-execution.ts +12 -2
- package/src/runs/background/async-job-tracker.ts +2 -2
- package/src/runs/background/async-status.ts +7 -1
- package/src/runs/background/subagent-runner.ts +31 -0
- package/src/runs/foreground/chain-clarify.ts +2 -3
- package/src/runs/foreground/execution.ts +3 -2
- package/src/runs/foreground/subagent-executor.ts +5 -3
- package/src/shared/types.ts +8 -1
- package/src/tui/render.ts +200 -16
- package/src/tui/subagents-status.ts +18 -3
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,18 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [0.23.0] - 2026-05-02
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
- Detect `pi-intercom` when installed through the documented `pi install npm:pi-intercom` package flow, instead of only checking the legacy local extension path.
|
|
9
|
+
|
|
10
|
+
### Changed
|
|
11
|
+
- Store and discover saved chain workflows from dedicated chain directories: user chains in `~/.pi/agent/chains/**/*.chain.md` and project chains in `.pi/chains/**/*.chain.md`.
|
|
12
|
+
- Retry foreground subagent fallback models when Pi reports a retryable provider error, such as 429/quota, even if the child process exits successfully.
|
|
13
|
+
- Align single-run async subagent widgets and `/subagents-status` rendering with foreground subagent result styling for parallel, chain, and grouped chain runs, including inline live detail when tool output expansion is enabled, while keeping multi-job async widgets compact.
|
|
14
|
+
- Render async subagent widgets through an adaptive component so active parallel agent rows fit without Pi's fixed string-widget truncation marker.
|
|
15
|
+
- Tell parent agents that async runs are detached and they should end the turn instead of running sleep/poll loops when no independent work remains.
|
|
16
|
+
|
|
5
17
|
## [0.22.0] - 2026-05-02
|
|
6
18
|
|
|
7
19
|
### Added
|
package/README.md
CHANGED
|
@@ -211,7 +211,7 @@ The package includes reusable prompt templates for common workflows. You do not
|
|
|
211
211
|
pi install npm:pi-intercom
|
|
212
212
|
```
|
|
213
213
|
|
|
214
|
-
Most users do not call `intercom` directly. After `pi-intercom` is installed, `pi-subagents` can automatically give child agents a private coordination channel back to the parent session.
|
|
214
|
+
Most users do not call `intercom` directly. After `pi-intercom` is installed, `pi-subagents` can automatically give child agents a private coordination channel back to the parent session. The bridge recognizes the normal `pi install npm:pi-intercom` package install as well as legacy local extension checkouts.
|
|
215
215
|
|
|
216
216
|
Use it for work where the child might need a decision instead of guessing:
|
|
217
217
|
|
|
@@ -332,6 +332,8 @@ You can combine them in either order:
|
|
|
332
332
|
/run reviewer "review this diff" --bg --fork
|
|
333
333
|
```
|
|
334
334
|
|
|
335
|
+
Background runs are detached. If the parent agent has other independent work, it should keep working. If it has nothing useful to do until the background result arrives, it should end the turn instead of running sleep or status-polling loops. Pi will deliver the completion when the run finishes.
|
|
336
|
+
|
|
335
337
|
The `oracle` and `worker` builtins are designed for an explicit decision loop. A typical pattern is to ask `oracle` for diagnosis and a recommended execution prompt, then only run `worker` after the main agent approves that direction.
|
|
336
338
|
|
|
337
339
|
## Clarify and launch UI
|
|
@@ -401,7 +403,7 @@ Agent locations, lowest to highest priority:
|
|
|
401
403
|
| User | `~/.pi/agent/agents/**/*.md` |
|
|
402
404
|
| Project | `.pi/agents/**/*.md` |
|
|
403
405
|
|
|
404
|
-
Project discovery also reads legacy `.agents/**/*.md` files. Nested subdirectories are discovered recursively. `.chain.md` files
|
|
406
|
+
Project discovery also reads legacy `.agents/**/*.md` files. Nested subdirectories are discovered recursively. `.chain.md` files do not define agents. If both `.agents/` and `.pi/agents/` define the same parsed runtime agent name, `.pi/agents/` wins. Use `agentScope: "user" | "project" | "both"` to control discovery; `both` is the default and project definitions win runtime-name collisions.
|
|
405
407
|
|
|
406
408
|
Builtin agents load at the lowest priority, so a user or project agent with the same name overrides them. They do not pin a provider model; they inherit your current Pi default model unless you set `subagents.agentOverrides.<name>.model`. `oracle` is an advisory reviewer that critiques direction and proposes an execution prompt without editing files. `worker` is the implementation agent for normal tasks and approved oracle handoffs.
|
|
407
409
|
|
|
@@ -531,14 +533,14 @@ When `extensions` is present, it takes precedence over extension paths implied b
|
|
|
531
533
|
|
|
532
534
|
## Chain files
|
|
533
535
|
|
|
534
|
-
Chains are reusable `.chain.md` workflows stored
|
|
536
|
+
Chains are reusable `.chain.md` workflows stored separately from agent files.
|
|
535
537
|
|
|
536
538
|
| Scope | Path |
|
|
537
539
|
|-------|------|
|
|
538
|
-
| User | `~/.pi/agent/
|
|
539
|
-
| Project | `.pi/
|
|
540
|
+
| User | `~/.pi/agent/chains/**/*.chain.md` |
|
|
541
|
+
| Project | `.pi/chains/**/*.chain.md` |
|
|
540
542
|
|
|
541
|
-
|
|
543
|
+
Nested subdirectories are discovered recursively. If user and project scopes define the same parsed runtime chain name, the project chain wins. Chains support the same optional `package` frontmatter as agents; `name: review-flow` plus `package: code-analysis` runs as `code-analysis.review-flow`.
|
|
542
544
|
|
|
543
545
|
Example:
|
|
544
546
|
|
|
@@ -893,7 +895,7 @@ Fields:
|
|
|
893
895
|
- `mode`: default `always`; use `fork-only` to inject only for forked runs, or `off` to disable the bridge.
|
|
894
896
|
- `instructionFile`: optional Markdown template replacing the default bridge instructions. `{orchestratorTarget}` is interpolated. Relative paths resolve from `~/.pi/agent/extensions/subagent/`.
|
|
895
897
|
|
|
896
|
-
Bridge activation also requires `pi-intercom` to be installed and enabled, a targetable current session name or fallback alias, and `pi-intercom` in any explicit agent `extensions` allowlist.
|
|
898
|
+
Bridge activation also requires `pi-intercom` to be installed and enabled through `pi install npm:pi-intercom` or a legacy local extension checkout, a targetable current session name or fallback alias, and `pi-intercom` in any explicit agent `extensions` allowlist.
|
|
897
899
|
|
|
898
900
|
The default injected guidance tells children to use `contact_supervisor` with `reason: "need_decision"` when blocked or needing a decision, `reason: "progress_update"` only for meaningful blocked/progress updates, generic `intercom` as fallback plumbing, and avoid routine completion handoffs.
|
|
899
901
|
|
package/package.json
CHANGED
|
@@ -183,11 +183,10 @@ Agent files can live in:
|
|
|
183
183
|
- legacy `.agents/**/*.md` — still read for compatibility, but `.pi/agents/` wins on conflicts
|
|
184
184
|
|
|
185
185
|
Chains live in:
|
|
186
|
-
- `~/.pi/agent/
|
|
187
|
-
- `.pi/
|
|
188
|
-
- legacy `.agents/**/*.chain.md`
|
|
186
|
+
- `~/.pi/agent/chains/**/*.chain.md` — user scope
|
|
187
|
+
- `.pi/chains/**/*.chain.md` — project scope
|
|
189
188
|
|
|
190
|
-
Discovery is recursive. `.chain.md` files
|
|
189
|
+
Discovery is recursive. `.chain.md` files do not define agents. Agents and chains can set optional frontmatter `package: code-analysis`; `name: scout` plus `package: code-analysis` registers as runtime name `code-analysis.scout` while serialization keeps `name` and `package` separate.
|
|
191
190
|
|
|
192
191
|
Precedence is by parsed runtime name:
|
|
193
192
|
1. project scope
|
|
@@ -263,7 +262,9 @@ without forcing each step to rediscover everything.
|
|
|
263
262
|
|
|
264
263
|
### Async/background
|
|
265
264
|
|
|
266
|
-
Use async mode whenever the parent agent should keep working while a child runs. A normal foreground `subagent(...)` call blocks the parent until the child completes; it is appropriate when the next parent step depends on the child result. If you say you will "ask a reviewer while I continue auditing" or otherwise run local work in parallel with a child, launch with `async: true`.
|
|
265
|
+
Use async mode whenever the parent agent should keep working while a child runs. A normal foreground `subagent(...)` call blocks the parent until the child completes; it is appropriate when the next parent step depends on the child result. If you say you will "ask a reviewer while I continue auditing" or otherwise run local work in parallel with a child, launch with `async: true`.
|
|
266
|
+
|
|
267
|
+
Do not end your turn immediately after launching an async child if you promised to keep working. Continue the local inspection or other independent work, then check the async run when its result is needed. If there is no independent work left and you would only be running `sleep` or status polling commands to wait, end your turn instead. Pi will deliver the async completion when it arrives.
|
|
267
268
|
|
|
268
269
|
```typescript
|
|
269
270
|
subagent({
|
|
@@ -459,7 +459,9 @@ export function handleCreate(params: ManagementParams, ctx: ManagementContext):
|
|
|
459
459
|
const scope = scopeRaw as ManagementScope;
|
|
460
460
|
const isChain = hasKey(cfg, "steps");
|
|
461
461
|
const d = discoverAgentsAll(ctx.cwd);
|
|
462
|
-
const targetDir =
|
|
462
|
+
const targetDir = isChain
|
|
463
|
+
? scope === "user" ? d.userChainDir : d.projectChainDir ?? path.join(ctx.cwd, ".pi", "chains")
|
|
464
|
+
: scope === "user" ? d.userDir : d.projectDir ?? path.join(ctx.cwd, ".pi", "agents");
|
|
463
465
|
fs.mkdirSync(targetDir, { recursive: true });
|
|
464
466
|
if (nameExistsInScope(ctx.cwd, scope, runtimeName)) return result(`Name '${runtimeName}' already exists in ${scope} scope. Use update instead.`, true);
|
|
465
467
|
const targetPath = path.join(targetDir, isChain ? `${runtimeName}.chain.md` : `${runtimeName}.md`);
|
package/src/agents/agents.ts
CHANGED
|
@@ -130,6 +130,10 @@ interface AgentDiscoveryResult {
|
|
|
130
130
|
projectAgentsDir: string | null;
|
|
131
131
|
}
|
|
132
132
|
|
|
133
|
+
export function getUserChainDir(): string {
|
|
134
|
+
return path.join(os.homedir(), ".pi", "agent", "chains");
|
|
135
|
+
}
|
|
136
|
+
|
|
133
137
|
function splitToolList(rawTools: string[] | undefined): { tools?: string[]; mcpDirectTools?: string[] } {
|
|
134
138
|
const mcpDirectTools: string[] = [];
|
|
135
139
|
const tools: string[] = [];
|
|
@@ -705,6 +709,17 @@ function resolveNearestProjectAgentDirs(cwd: string): { readDirs: string[]; pref
|
|
|
705
709
|
preferredDir,
|
|
706
710
|
};
|
|
707
711
|
}
|
|
712
|
+
|
|
713
|
+
function resolveNearestProjectChainDirs(cwd: string): { readDirs: string[]; preferredDir: string | null } {
|
|
714
|
+
const projectRoot = findNearestProjectRoot(cwd);
|
|
715
|
+
if (!projectRoot) return { readDirs: [], preferredDir: null };
|
|
716
|
+
|
|
717
|
+
const preferredDir = path.join(projectRoot, ".pi", "chains");
|
|
718
|
+
return {
|
|
719
|
+
readDirs: isDirectory(preferredDir) ? [preferredDir] : [],
|
|
720
|
+
preferredDir,
|
|
721
|
+
};
|
|
722
|
+
}
|
|
708
723
|
const BUILTIN_AGENTS_DIR = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..", "..", "agents");
|
|
709
724
|
|
|
710
725
|
export function discoverAgents(cwd: string, scope: AgentScope): AgentDiscoveryResult {
|
|
@@ -742,12 +757,16 @@ export function discoverAgentsAll(cwd: string): {
|
|
|
742
757
|
chains: ChainConfig[];
|
|
743
758
|
userDir: string;
|
|
744
759
|
projectDir: string | null;
|
|
760
|
+
userChainDir: string;
|
|
761
|
+
projectChainDir: string | null;
|
|
745
762
|
userSettingsPath: string;
|
|
746
763
|
projectSettingsPath: string | null;
|
|
747
764
|
} {
|
|
748
765
|
const userDirOld = path.join(os.homedir(), ".pi", "agent", "agents");
|
|
749
766
|
const userDirNew = path.join(os.homedir(), ".agents");
|
|
767
|
+
const userChainDir = getUserChainDir();
|
|
750
768
|
const { readDirs: projectDirs, preferredDir: projectDir } = resolveNearestProjectAgentDirs(cwd);
|
|
769
|
+
const { readDirs: projectChainDirs, preferredDir: projectChainDir } = resolveNearestProjectChainDirs(cwd);
|
|
751
770
|
const userSettingsPath = getUserAgentSettingsPath();
|
|
752
771
|
const projectSettingsPath = getProjectAgentSettingsPath(cwd);
|
|
753
772
|
const userSettings = readSubagentSettings(userSettingsPath);
|
|
@@ -773,18 +792,17 @@ export function discoverAgentsAll(cwd: string): {
|
|
|
773
792
|
const project = Array.from(projectMap.values());
|
|
774
793
|
|
|
775
794
|
const chainMap = new Map<string, ChainConfig>();
|
|
776
|
-
for (const dir of
|
|
795
|
+
for (const dir of projectChainDirs) {
|
|
777
796
|
for (const chain of loadChainsFromDir(dir, "project")) {
|
|
778
797
|
chainMap.set(chain.name, chain);
|
|
779
798
|
}
|
|
780
799
|
}
|
|
781
800
|
const chains = [
|
|
782
|
-
...loadChainsFromDir(
|
|
783
|
-
...loadChainsFromDir(userDirNew, "user"),
|
|
801
|
+
...loadChainsFromDir(userChainDir, "user"),
|
|
784
802
|
...Array.from(chainMap.values()),
|
|
785
803
|
];
|
|
786
804
|
|
|
787
805
|
const userDir = fs.existsSync(userDirNew) ? userDirNew : userDirOld;
|
|
788
806
|
|
|
789
|
-
return { builtin, user, project, chains, userDir, projectDir, userSettingsPath, projectSettingsPath };
|
|
807
|
+
return { builtin, user, project, chains, userDir, projectDir, userChainDir, projectChainDir, userSettingsPath, projectSettingsPath };
|
|
790
808
|
}
|
package/src/extension/doctor.ts
CHANGED
|
@@ -192,6 +192,7 @@ export function buildDoctorReport(input: DoctorReportInput): string {
|
|
|
192
192
|
config: input.config.intercomBridge,
|
|
193
193
|
context: input.context,
|
|
194
194
|
orchestratorTarget: input.orchestratorTarget,
|
|
195
|
+
cwd: input.cwd,
|
|
195
196
|
}), input.context).join("\n")).split("\n"),
|
|
196
197
|
];
|
|
197
198
|
return lines.join("\n");
|
|
@@ -1,19 +1,27 @@
|
|
|
1
|
+
import { execSync } from "node:child_process";
|
|
1
2
|
import * as fs from "node:fs";
|
|
2
3
|
import * as os from "node:os";
|
|
3
4
|
import * as path from "node:path";
|
|
4
5
|
import type { AgentConfig } from "../agents/agents.ts";
|
|
5
6
|
import type { ExtensionConfig, IntercomBridgeConfig, IntercomBridgeMode } from "../shared/types.ts";
|
|
6
7
|
|
|
7
|
-
|
|
8
|
-
|
|
8
|
+
const PI_INTERCOM_PACKAGE_NAME = "pi-intercom";
|
|
9
|
+
const CONFIG_DIR = ".pi";
|
|
10
|
+
|
|
11
|
+
function defaultAgentDir(): string {
|
|
12
|
+
return path.join(os.homedir(), ".pi", "agent");
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function defaultIntercomExtensionDir(agentDir = defaultAgentDir()): string {
|
|
16
|
+
return path.join(agentDir, "extensions", PI_INTERCOM_PACKAGE_NAME);
|
|
9
17
|
}
|
|
10
18
|
|
|
11
|
-
function defaultIntercomConfigPath(): string {
|
|
12
|
-
return path.join(
|
|
19
|
+
function defaultIntercomConfigPath(agentDir = defaultAgentDir()): string {
|
|
20
|
+
return path.join(agentDir, "intercom", "config.json");
|
|
13
21
|
}
|
|
14
22
|
|
|
15
|
-
function defaultSubagentConfigDir(): string {
|
|
16
|
-
return path.join(
|
|
23
|
+
function defaultSubagentConfigDir(agentDir = defaultAgentDir()): string {
|
|
24
|
+
return path.join(agentDir, "extensions", "subagent");
|
|
17
25
|
}
|
|
18
26
|
|
|
19
27
|
const DEFAULT_INTERCOM_TARGET_PREFIX = "subagent-chat";
|
|
@@ -56,6 +64,9 @@ interface ResolveIntercomBridgeInput {
|
|
|
56
64
|
extensionDir?: string;
|
|
57
65
|
configPath?: string;
|
|
58
66
|
settingsDir?: string;
|
|
67
|
+
cwd?: string;
|
|
68
|
+
agentDir?: string;
|
|
69
|
+
globalNpmRoot?: string | null;
|
|
59
70
|
}
|
|
60
71
|
|
|
61
72
|
export function resolveIntercomSessionTarget(sessionName: string | undefined, sessionId: string): string {
|
|
@@ -102,6 +113,119 @@ function intercomConfigStatus(configPath: string): { enabled: boolean; error?: u
|
|
|
102
113
|
}
|
|
103
114
|
}
|
|
104
115
|
|
|
116
|
+
function readJsonBestEffort(filePath: string): unknown {
|
|
117
|
+
try {
|
|
118
|
+
return JSON.parse(fs.readFileSync(filePath, "utf-8"));
|
|
119
|
+
} catch {
|
|
120
|
+
return null;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function packageHasPiExtension(packageRoot: string): boolean {
|
|
125
|
+
if (!fs.existsSync(packageRoot)) return false;
|
|
126
|
+
const pkg = readJsonBestEffort(path.join(packageRoot, "package.json"));
|
|
127
|
+
if (pkg && typeof pkg === "object" && !Array.isArray(pkg)) {
|
|
128
|
+
const pi = (pkg as { pi?: unknown }).pi;
|
|
129
|
+
if (pi && typeof pi === "object" && !Array.isArray(pi)) {
|
|
130
|
+
const extensions = (pi as { extensions?: unknown }).extensions;
|
|
131
|
+
return Array.isArray(extensions) && extensions.some((entry) => typeof entry === "string" && entry.trim() !== "");
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
return fs.existsSync(path.join(packageRoot, "extensions"));
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function isSafePackagePath(value: string): boolean {
|
|
138
|
+
return value.length > 0
|
|
139
|
+
&& !path.isAbsolute(value)
|
|
140
|
+
&& value.split(/[\\/]/).every((part) => part.length > 0 && part !== "." && part !== "..");
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function parseNpmPackageName(source: string): string | undefined {
|
|
144
|
+
const spec = source.slice(4).trim();
|
|
145
|
+
if (!spec) return undefined;
|
|
146
|
+
const match = spec.match(/^(@?[^@]+(?:\/[^@]+)?)(?:@(.+))?$/);
|
|
147
|
+
const packageName = match?.[1] ?? spec;
|
|
148
|
+
return isSafePackagePath(packageName) ? packageName : undefined;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function packageEntrySource(entry: unknown): string | undefined {
|
|
152
|
+
if (typeof entry === "string") return entry;
|
|
153
|
+
if (entry && typeof entry === "object" && !Array.isArray(entry) && typeof (entry as { source?: unknown }).source === "string") {
|
|
154
|
+
return (entry as { source: string }).source;
|
|
155
|
+
}
|
|
156
|
+
return undefined;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function packageEntryAllowsExtensions(entry: unknown): boolean {
|
|
160
|
+
if (!entry || typeof entry !== "object" || Array.isArray(entry)) return true;
|
|
161
|
+
const extensions = (entry as { extensions?: unknown }).extensions;
|
|
162
|
+
return !Array.isArray(extensions) || extensions.length > 0;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function findNearestProjectConfigDir(cwd: string): string | undefined {
|
|
166
|
+
let current = path.resolve(cwd);
|
|
167
|
+
while (true) {
|
|
168
|
+
const configDir = path.join(current, CONFIG_DIR);
|
|
169
|
+
if (fs.existsSync(path.join(configDir, "settings.json"))) return configDir;
|
|
170
|
+
const parent = path.dirname(current);
|
|
171
|
+
if (parent === current) return undefined;
|
|
172
|
+
current = parent;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
let cachedGlobalNpmRoot: string | null | undefined;
|
|
177
|
+
|
|
178
|
+
function getGlobalNpmRoot(): string | null {
|
|
179
|
+
if (cachedGlobalNpmRoot !== undefined) return cachedGlobalNpmRoot;
|
|
180
|
+
try {
|
|
181
|
+
cachedGlobalNpmRoot = execSync("npm root -g", { encoding: "utf-8", timeout: 5000 }).trim();
|
|
182
|
+
return cachedGlobalNpmRoot;
|
|
183
|
+
} catch {
|
|
184
|
+
cachedGlobalNpmRoot = null;
|
|
185
|
+
return null;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function configuredPiIntercomPackageDir(input: ResolveIntercomBridgeInput, agentDir: string): string | undefined {
|
|
190
|
+
const cwd = path.resolve(input.cwd ?? process.cwd());
|
|
191
|
+
const projectConfigDir = findNearestProjectConfigDir(cwd);
|
|
192
|
+
const settingsFiles = [
|
|
193
|
+
...(projectConfigDir ? [{ file: path.join(projectConfigDir, "settings.json"), configDir: projectConfigDir, scope: "project" as const }] : []),
|
|
194
|
+
{ file: path.join(agentDir, "settings.json"), configDir: agentDir, scope: "user" as const },
|
|
195
|
+
];
|
|
196
|
+
const globalNpmRoot = input.globalNpmRoot === undefined ? getGlobalNpmRoot() : input.globalNpmRoot;
|
|
197
|
+
|
|
198
|
+
for (const { file, configDir, scope } of settingsFiles) {
|
|
199
|
+
const settings = readJsonBestEffort(file);
|
|
200
|
+
if (!settings || typeof settings !== "object" || Array.isArray(settings)) continue;
|
|
201
|
+
const packages = (settings as { packages?: unknown }).packages;
|
|
202
|
+
if (!Array.isArray(packages)) continue;
|
|
203
|
+
|
|
204
|
+
for (const entry of packages) {
|
|
205
|
+
if (!packageEntryAllowsExtensions(entry)) continue;
|
|
206
|
+
const source = packageEntrySource(entry)?.trim();
|
|
207
|
+
if (!source?.startsWith("npm:")) continue;
|
|
208
|
+
const packageName = parseNpmPackageName(source);
|
|
209
|
+
if (packageName !== PI_INTERCOM_PACKAGE_NAME) continue;
|
|
210
|
+
const candidates = scope === "project"
|
|
211
|
+
? [path.join(configDir, "npm", "node_modules", packageName)]
|
|
212
|
+
: [
|
|
213
|
+
...(globalNpmRoot ? [path.join(globalNpmRoot, packageName)] : []),
|
|
214
|
+
path.join(agentDir, "npm", "node_modules", packageName),
|
|
215
|
+
];
|
|
216
|
+
const packageRoot = candidates.find(packageHasPiExtension);
|
|
217
|
+
if (packageRoot) return path.resolve(packageRoot);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
return undefined;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function resolveIntercomExtensionDir(input: ResolveIntercomBridgeInput, agentDir: string): string {
|
|
224
|
+
const legacyDir = path.resolve(input.extensionDir ?? defaultIntercomExtensionDir(agentDir));
|
|
225
|
+
if (fs.existsSync(legacyDir)) return legacyDir;
|
|
226
|
+
return configuredPiIntercomPackageDir(input, agentDir) ?? legacyDir;
|
|
227
|
+
}
|
|
228
|
+
|
|
105
229
|
function extensionSandboxAllowsIntercom(extensions: string[] | undefined, extensionDir: string): boolean {
|
|
106
230
|
if (extensions === undefined) return true;
|
|
107
231
|
|
|
@@ -145,9 +269,10 @@ ${instruction}`;
|
|
|
145
269
|
export function diagnoseIntercomBridge(input: ResolveIntercomBridgeInput): IntercomBridgeDiagnostic {
|
|
146
270
|
const config = resolveIntercomBridgeConfig(input.config);
|
|
147
271
|
const mode = config.mode;
|
|
148
|
-
const
|
|
272
|
+
const agentDir = path.resolve(input.agentDir ?? defaultAgentDir());
|
|
273
|
+
const extensionDir = resolveIntercomExtensionDir(input, agentDir);
|
|
149
274
|
const orchestratorTarget = input.orchestratorTarget?.trim();
|
|
150
|
-
const configPath = path.resolve(input.configPath ?? defaultIntercomConfigPath());
|
|
275
|
+
const configPath = path.resolve(input.configPath ?? defaultIntercomConfigPath(agentDir));
|
|
151
276
|
const wantsIntercom = mode !== "off" && !(mode === "fork-only" && input.context !== "fork");
|
|
152
277
|
const piIntercomAvailable = fs.existsSync(extensionDir);
|
|
153
278
|
let configStatus: ReturnType<typeof intercomConfigStatus> | undefined;
|
|
@@ -183,9 +308,10 @@ export function diagnoseIntercomBridge(input: ResolveIntercomBridgeInput): Inter
|
|
|
183
308
|
export function resolveIntercomBridge(input: ResolveIntercomBridgeInput): IntercomBridgeState {
|
|
184
309
|
const config = resolveIntercomBridgeConfig(input.config);
|
|
185
310
|
const mode = config.mode;
|
|
186
|
-
const
|
|
311
|
+
const agentDir = path.resolve(input.agentDir ?? defaultAgentDir());
|
|
312
|
+
const extensionDir = resolveIntercomExtensionDir(input, agentDir);
|
|
187
313
|
const orchestratorTarget = input.orchestratorTarget?.trim();
|
|
188
|
-
const settingsDir = path.resolve(input.settingsDir ?? defaultSubagentConfigDir());
|
|
314
|
+
const settingsDir = path.resolve(input.settingsDir ?? defaultSubagentConfigDir(agentDir));
|
|
189
315
|
const defaultInstruction = buildIntercomBridgeInstruction(
|
|
190
316
|
orchestratorTarget || "{orchestratorTarget}",
|
|
191
317
|
DEFAULT_INTERCOM_BRIDGE_TEMPLATE,
|
|
@@ -204,7 +330,7 @@ export function resolveIntercomBridge(input: ResolveIntercomBridgeInput): Interc
|
|
|
204
330
|
return { active: false, mode, extensionDir, instruction: defaultInstruction };
|
|
205
331
|
}
|
|
206
332
|
|
|
207
|
-
const configPath = path.resolve(input.configPath ?? defaultIntercomConfigPath());
|
|
333
|
+
const configPath = path.resolve(input.configPath ?? defaultIntercomConfigPath(agentDir));
|
|
208
334
|
const intercomStatus = intercomConfigStatus(configPath);
|
|
209
335
|
if (intercomStatus.error) console.warn(`Failed to parse intercom config at '${configPath}'. Assuming enabled.`, intercomStatus.error);
|
|
210
336
|
if (!intercomStatus.enabled) {
|
|
@@ -38,7 +38,7 @@ export type ManagerResult =
|
|
|
38
38
|
| { action: "launch-chain"; chain: ChainConfig; task: string; skipClarify?: boolean; fork?: boolean; background?: boolean; worktree?: boolean }
|
|
39
39
|
| undefined;
|
|
40
40
|
|
|
41
|
-
export interface AgentData { builtin: AgentConfig[]; user: AgentConfig[]; project: AgentConfig[]; chains: ChainConfig[]; userDir: string; projectDir: string | null; userSettingsPath: string; projectSettingsPath: string | null; cwd: string; }
|
|
41
|
+
export interface AgentData { builtin: AgentConfig[]; user: AgentConfig[]; project: AgentConfig[]; chains: ChainConfig[]; userDir: string; projectDir: string | null; userChainDir: string; projectChainDir: string | null; userSettingsPath: string; projectSettingsPath: string | null; cwd: string; }
|
|
42
42
|
type ManagerScreen = "list" | "detail" | "chain-detail" | "edit" | "edit-field" | "edit-prompt" | "task-input" | "confirm-delete" | "name-input" | "chain-edit" | "template-select" | "parallel-builder" | "override-scope";
|
|
43
43
|
interface AgentEntry { id: string; kind: "agent"; config: AgentConfig; isNew: boolean; }
|
|
44
44
|
interface ChainEntry { id: string; kind: "chain"; config: ChainConfig; }
|
|
@@ -253,7 +253,8 @@ export class AgentManagerComponent implements Component {
|
|
|
253
253
|
}
|
|
254
254
|
|
|
255
255
|
private enterNameInput(mode: NameInputState["mode"], sourceId?: string, template?: AgentTemplate): void {
|
|
256
|
-
const
|
|
256
|
+
const isChain = mode === "new-chain" || mode === "clone-chain";
|
|
257
|
+
const allowProject = Boolean(isChain ? this.agentData.projectChainDir : this.agentData.projectDir); let initial = ""; let scope: "user" | "project" = "user";
|
|
257
258
|
if (mode === "clone-agent" && sourceId) { const entry = this.getAgentEntry(sourceId); if (entry) { initial = `${frontmatterNameForConfig(entry.config)}-copy`; scope = entry.config.source === "project" ? "project" : "user"; } }
|
|
258
259
|
if (mode === "clone-chain" && sourceId) { const entry = this.getChainEntry(sourceId); if (entry) { initial = `${frontmatterNameForConfig(entry.config)}-copy`; scope = entry.config.source === "project" ? "project" : "user"; } }
|
|
259
260
|
if (mode === "new-agent" && template && template.name !== "Blank") initial = slugTemplateName(template.name);
|
|
@@ -425,14 +426,14 @@ export class AgentManagerComponent implements Component {
|
|
|
425
426
|
|
|
426
427
|
if (state.mode === "clone-chain" && state.sourceId) {
|
|
427
428
|
const sourceEntry = this.getChainEntry(state.sourceId); if (!sourceEntry) { this.screen = "list"; this.tui.requestRender(); return; }
|
|
428
|
-
const dir = state.scope === "project" ? this.agentData.
|
|
429
|
-
if (!dir) { state.error = "Project
|
|
429
|
+
const dir = state.scope === "project" ? this.agentData.projectChainDir : this.agentData.userChainDir;
|
|
430
|
+
if (!dir) { state.error = "Project chains directory not found."; this.tui.requestRender(); return; }
|
|
430
431
|
const filePath = path.join(dir, `${name}.chain.md`); if (fs.existsSync(filePath) || this.runtimeNameExistsInScope("chain", state.scope, name)) { state.error = "A chain with that name already exists."; this.tui.requestRender(); return; }
|
|
431
432
|
try { const cloned = cloneChainConfig({ ...sourceEntry.config, name, localName: name, packageName: undefined, source: state.scope, filePath }); fs.mkdirSync(dir, { recursive: true }); fs.writeFileSync(filePath, serializeChain(cloned), "utf-8"); const added: ChainEntry = { id: `c${this.nextId++}`, kind: "chain", config: cloned }; this.chains.push(added); this.nameInputState = null; this.enterChainDetail(added); this.tui.requestRender(); return; }
|
|
432
433
|
catch (err) { state.error = err instanceof Error ? err.message : "Failed to clone chain."; this.tui.requestRender(); return; }
|
|
433
434
|
}
|
|
434
435
|
if (state.mode === "new-chain") {
|
|
435
|
-
const dir = state.scope === "project" ? this.agentData.
|
|
436
|
+
const dir = state.scope === "project" ? this.agentData.projectChainDir : this.agentData.userChainDir;
|
|
436
437
|
if (!dir) { state.error = "Directory not found."; this.tui.requestRender(); return; }
|
|
437
438
|
const filePath = path.join(dir, `${name}.chain.md`); if (fs.existsSync(filePath) || this.runtimeNameExistsInScope("chain", state.scope, name)) { state.error = "A chain with that name already exists."; this.tui.requestRender(); return; }
|
|
438
439
|
const config: ChainConfig = { name, localName: name, description: "Describe this chain", source: state.scope, filePath, steps: [{ agent: "agent-name", task: "{task}" }] };
|
|
@@ -116,6 +116,16 @@ interface AsyncExecutionResult {
|
|
|
116
116
|
isError?: boolean;
|
|
117
117
|
}
|
|
118
118
|
|
|
119
|
+
export function formatAsyncStartedMessage(headline: string): string {
|
|
120
|
+
return [
|
|
121
|
+
headline,
|
|
122
|
+
"",
|
|
123
|
+
"The async run is detached. Do not run sleep timers or polling loops just to wait for it.",
|
|
124
|
+
"If you have independent work, continue that work. If you have nothing else to do until the async result arrives, end your turn now; Pi will deliver the completion when the run finishes.",
|
|
125
|
+
"Use subagent({ action: \"status\", id: \"...\" }) when you need the current status/result, or to inspect a blocked/stale run. Do not poll just to wait.",
|
|
126
|
+
].join("\n");
|
|
127
|
+
}
|
|
128
|
+
|
|
119
129
|
/**
|
|
120
130
|
* Check if jiti is available for async execution
|
|
121
131
|
*/
|
|
@@ -425,7 +435,7 @@ export function executeAsyncChain(
|
|
|
425
435
|
.join(" -> ");
|
|
426
436
|
|
|
427
437
|
return {
|
|
428
|
-
content: [{ type: "text", text: `Async ${resultMode}: ${chainDesc} [${id}]` }],
|
|
438
|
+
content: [{ type: "text", text: formatAsyncStartedMessage(`Async ${resultMode}: ${chainDesc} [${id}]`) }],
|
|
429
439
|
details: { mode: resultMode, results: [], asyncId: id, asyncDir },
|
|
430
440
|
};
|
|
431
441
|
}
|
|
@@ -557,7 +567,7 @@ export function executeAsyncSingle(
|
|
|
557
567
|
}
|
|
558
568
|
|
|
559
569
|
return {
|
|
560
|
-
content: [{ type: "text", text: `Async: ${agent} [${id}]` }],
|
|
570
|
+
content: [{ type: "text", text: formatAsyncStartedMessage(`Async: ${agent} [${id}]`) }],
|
|
561
571
|
details: { mode: "single", results: [], asyncId: id, asyncDir },
|
|
562
572
|
};
|
|
563
573
|
}
|
|
@@ -162,8 +162,8 @@ export function createAsyncJobTracker(pi: Pick<ExtensionAPI, "events">, state: S
|
|
|
162
162
|
? groups.find((group) => status.currentStep! >= group.start && status.currentStep! < group.start + group.count)
|
|
163
163
|
: undefined;
|
|
164
164
|
const visibleSteps = activeGroup
|
|
165
|
-
? status.steps.slice(activeGroup.start, activeGroup.start + activeGroup.count)
|
|
166
|
-
: status.steps;
|
|
165
|
+
? status.steps.slice(activeGroup.start, activeGroup.start + activeGroup.count).map((step, index) => ({ ...step, index: activeGroup.start + index }))
|
|
166
|
+
: status.steps.map((step, index) => ({ ...step, index }));
|
|
167
167
|
job.activeParallelGroup = Boolean(activeGroup);
|
|
168
168
|
job.agents = visibleSteps.map((step) => step.agent);
|
|
169
169
|
job.steps = visibleSteps;
|
|
@@ -13,8 +13,11 @@ interface AsyncRunStepSummary {
|
|
|
13
13
|
activityState?: ActivityState;
|
|
14
14
|
lastActivityAt?: number;
|
|
15
15
|
currentTool?: string;
|
|
16
|
+
currentToolArgs?: string;
|
|
16
17
|
currentToolStartedAt?: number;
|
|
17
18
|
currentPath?: string;
|
|
19
|
+
recentTools?: Array<{ tool: string; args: string; endMs: number }>;
|
|
20
|
+
recentOutput?: string[];
|
|
18
21
|
turnCount?: number;
|
|
19
22
|
toolCount?: number;
|
|
20
23
|
durationMs?: number;
|
|
@@ -155,8 +158,11 @@ function statusToSummary(asyncDir: string, status: AsyncStatus & { cwd?: string
|
|
|
155
158
|
...(stepActivityState ? { activityState: stepActivityState } : {}),
|
|
156
159
|
...(stepLastActivityAt ? { lastActivityAt: stepLastActivityAt } : {}),
|
|
157
160
|
...(step.currentTool ? { currentTool: step.currentTool } : {}),
|
|
161
|
+
...(step.currentToolArgs ? { currentToolArgs: step.currentToolArgs } : {}),
|
|
158
162
|
...(step.currentToolStartedAt ? { currentToolStartedAt: step.currentToolStartedAt } : {}),
|
|
159
163
|
...(step.currentPath ? { currentPath: step.currentPath } : {}),
|
|
164
|
+
...(step.recentTools ? { recentTools: step.recentTools.map((tool) => ({ ...tool })) } : {}),
|
|
165
|
+
...(step.recentOutput ? { recentOutput: [...step.recentOutput] } : {}),
|
|
160
166
|
...(step.turnCount !== undefined ? { turnCount: step.turnCount } : {}),
|
|
161
167
|
...(step.toolCount !== undefined ? { toolCount: step.toolCount } : {}),
|
|
162
168
|
...(step.durationMs !== undefined ? { durationMs: step.durationMs } : {}),
|
|
@@ -270,7 +276,7 @@ function formatParallelProgress(steps: Pick<AsyncRunStepSummary, "status">[], to
|
|
|
270
276
|
const failed = steps.filter((step) => step.status === "failed").length;
|
|
271
277
|
const paused = steps.filter((step) => step.status === "paused").length;
|
|
272
278
|
const parts = [`${done}/${total} done`];
|
|
273
|
-
if (showRunning) parts.unshift(running === 1 ? "1 agent running" : `${running} agents running`);
|
|
279
|
+
if (showRunning && running > 0) parts.unshift(running === 1 ? "1 agent running" : `${running} agents running`);
|
|
274
280
|
if (failed > 0) parts.push(`${failed} failed`);
|
|
275
281
|
if (paused > 0) parts.push(`${paused} paused`);
|
|
276
282
|
return parts.join(" · ");
|
|
@@ -142,6 +142,25 @@ function tokenUsageFromAttempts(attempts: ModelAttempt[] | undefined): TokenUsag
|
|
|
142
142
|
return total > 0 ? { input, output, total } : null;
|
|
143
143
|
}
|
|
144
144
|
|
|
145
|
+
function appendRecentStepOutput(step: RunnerStatusStep, lines: string[]): void {
|
|
146
|
+
const nonEmpty = lines.filter((line) => line.trim());
|
|
147
|
+
if (nonEmpty.length === 0) return;
|
|
148
|
+
step.recentOutput ??= [];
|
|
149
|
+
step.recentOutput.push(...nonEmpty);
|
|
150
|
+
if (step.recentOutput.length > 50) {
|
|
151
|
+
step.recentOutput.splice(0, step.recentOutput.length - 50);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function resetStepLiveDetail(step: RunnerStatusStep): void {
|
|
156
|
+
step.currentTool = undefined;
|
|
157
|
+
step.currentToolArgs = undefined;
|
|
158
|
+
step.currentToolStartedAt = undefined;
|
|
159
|
+
step.currentPath = undefined;
|
|
160
|
+
step.recentTools = [];
|
|
161
|
+
step.recentOutput = [];
|
|
162
|
+
}
|
|
163
|
+
|
|
145
164
|
interface ChildEventContext {
|
|
146
165
|
eventsPath: string;
|
|
147
166
|
runId: string;
|
|
@@ -900,6 +919,8 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
|
|
|
900
919
|
skills: step.skills,
|
|
901
920
|
model: step.model,
|
|
902
921
|
attemptedModels: step.modelCandidates && step.modelCandidates.length > 0 ? step.modelCandidates : step.model ? [step.model] : undefined,
|
|
922
|
+
recentTools: [],
|
|
923
|
+
recentOutput: [],
|
|
903
924
|
})),
|
|
904
925
|
artifactsDir,
|
|
905
926
|
sessionDir: config.sessionDir,
|
|
@@ -1002,13 +1023,19 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
|
|
|
1002
1023
|
const currentPath = resolveCurrentPath(event.toolName, event.args);
|
|
1003
1024
|
step.toolCount = (step.toolCount ?? 0) + 1;
|
|
1004
1025
|
step.currentTool = event.toolName;
|
|
1026
|
+
step.currentToolArgs = extractToolArgsPreview(event.args ?? {});
|
|
1005
1027
|
step.currentToolStartedAt = now;
|
|
1006
1028
|
step.currentPath = currentPath;
|
|
1007
1029
|
pendingToolResults[flatIndex] = { tool: event.toolName, path: currentPath, mutates, startedAt: now };
|
|
1008
1030
|
statusPayload.toolCount = (statusPayload.toolCount ?? 0) + 1;
|
|
1009
1031
|
syncTopLevelCurrentTool();
|
|
1010
1032
|
} else if (event.type === "tool_execution_end") {
|
|
1033
|
+
if (step.currentTool) {
|
|
1034
|
+
step.recentTools ??= [];
|
|
1035
|
+
step.recentTools.push({ tool: step.currentTool, args: step.currentToolArgs || "", endMs: now });
|
|
1036
|
+
}
|
|
1011
1037
|
step.currentTool = undefined;
|
|
1038
|
+
step.currentToolArgs = undefined;
|
|
1012
1039
|
step.currentToolStartedAt = undefined;
|
|
1013
1040
|
step.currentPath = undefined;
|
|
1014
1041
|
syncTopLevelCurrentTool();
|
|
@@ -1016,6 +1043,7 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
|
|
|
1016
1043
|
const toolSnapshot = pendingToolResults[flatIndex];
|
|
1017
1044
|
pendingToolResults[flatIndex] = undefined;
|
|
1018
1045
|
const resultText = extractTextFromContent(event.message.content);
|
|
1046
|
+
appendRecentStepOutput(step, resultText.split("\n").slice(-10));
|
|
1019
1047
|
if (toolSnapshot?.mutates && didMutatingToolFail(resultText)) {
|
|
1020
1048
|
const state = mutatingFailureStates[flatIndex]!;
|
|
1021
1049
|
recordMutatingFailure(state, {
|
|
@@ -1051,6 +1079,7 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
|
|
|
1051
1079
|
resetMutatingFailureState(mutatingFailureStates[flatIndex]!);
|
|
1052
1080
|
}
|
|
1053
1081
|
} else if (event.type === "message_end" && event.message?.role === "assistant") {
|
|
1082
|
+
appendRecentStepOutput(step, extractTextFromContent(event.message.content).split("\n").slice(-10));
|
|
1054
1083
|
step.turnCount = (step.turnCount ?? 0) + 1;
|
|
1055
1084
|
const usage = event.message.usage;
|
|
1056
1085
|
if (usage) {
|
|
@@ -1277,6 +1306,7 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
|
|
|
1277
1306
|
statusPayload.steps[fi].status = "running";
|
|
1278
1307
|
statusPayload.steps[fi].error = undefined;
|
|
1279
1308
|
statusPayload.steps[fi].activityState = undefined;
|
|
1309
|
+
resetStepLiveDetail(statusPayload.steps[fi]);
|
|
1280
1310
|
statusPayload.steps[fi].startedAt = taskStartTime;
|
|
1281
1311
|
statusPayload.steps[fi].endedAt = undefined;
|
|
1282
1312
|
statusPayload.steps[fi].durationMs = undefined;
|
|
@@ -1420,6 +1450,7 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
|
|
|
1420
1450
|
statusPayload.steps[flatIndex].status = "running";
|
|
1421
1451
|
statusPayload.steps[flatIndex].activityState = undefined;
|
|
1422
1452
|
statusPayload.activityState = undefined;
|
|
1453
|
+
resetStepLiveDetail(statusPayload.steps[flatIndex]);
|
|
1423
1454
|
statusPayload.steps[flatIndex].skills = seqStep.skills;
|
|
1424
1455
|
statusPayload.steps[flatIndex].startedAt = stepStartTime;
|
|
1425
1456
|
statusPayload.steps[flatIndex].lastActivityAt = stepStartTime;
|
|
@@ -9,9 +9,8 @@ import type { Theme } from "@mariozechner/pi-coding-agent";
|
|
|
9
9
|
import type { Component, TUI } from "@mariozechner/pi-tui";
|
|
10
10
|
import { matchesKey, visibleWidth, truncateToWidth } from "@mariozechner/pi-tui";
|
|
11
11
|
import * as fs from "node:fs";
|
|
12
|
-
import * as os from "node:os";
|
|
13
12
|
import * as path from "node:path";
|
|
14
|
-
import type
|
|
13
|
+
import { getUserChainDir, type AgentConfig, type ChainConfig, type ChainStepConfig } from "../../agents/agents.ts";
|
|
15
14
|
import type { ResolvedStepBehavior } from "../../shared/settings.ts";
|
|
16
15
|
import type { TextEditorState } from "../../tui/text-editor.ts";
|
|
17
16
|
import { createEditorState, ensureCursorVisible, getCursorDisplayPos, handleEditorInput, renderEditor, wrapText } from "../../tui/text-editor.ts";
|
|
@@ -317,7 +316,7 @@ export class ChainClarifyComponent implements Component {
|
|
|
317
316
|
return;
|
|
318
317
|
}
|
|
319
318
|
try {
|
|
320
|
-
const dir =
|
|
319
|
+
const dir = getUserChainDir();
|
|
321
320
|
fs.mkdirSync(dir, { recursive: true });
|
|
322
321
|
const filePath = path.join(dir, `${name}.chain.md`);
|
|
323
322
|
const config = this.buildChainConfig(name);
|
|
@@ -811,15 +811,16 @@ export async function runSync(
|
|
|
811
811
|
sumUsage(aggregateUsage, result.usage);
|
|
812
812
|
totalToolCount += result.progressSummary?.toolCount ?? 0;
|
|
813
813
|
totalDurationMs += result.progressSummary?.durationMs ?? 0;
|
|
814
|
+
const attemptSucceeded = result.exitCode === 0 && !result.error;
|
|
814
815
|
const attempt: ModelAttempt = {
|
|
815
816
|
model: candidate ?? result.model ?? agent.model ?? "default",
|
|
816
|
-
success:
|
|
817
|
+
success: attemptSucceeded,
|
|
817
818
|
exitCode: result.exitCode,
|
|
818
819
|
error: result.error,
|
|
819
820
|
usage: { ...result.usage },
|
|
820
821
|
};
|
|
821
822
|
modelAttempts.push(attempt);
|
|
822
|
-
if (
|
|
823
|
+
if (attemptSucceeded) {
|
|
823
824
|
break;
|
|
824
825
|
}
|
|
825
826
|
if (!isRetryableModelFailure(result.error) || i === modelsToTry.length - 1) {
|
|
@@ -28,7 +28,7 @@ import {
|
|
|
28
28
|
type StepOverrides,
|
|
29
29
|
} from "../../shared/settings.ts";
|
|
30
30
|
import { discoverAvailableSkills, normalizeSkillInput } from "../../agents/skills.ts";
|
|
31
|
-
import { executeAsyncChain, executeAsyncSingle, isAsyncAvailable } from "../background/async-execution.ts";
|
|
31
|
+
import { executeAsyncChain, executeAsyncSingle, formatAsyncStartedMessage, isAsyncAvailable } from "../background/async-execution.ts";
|
|
32
32
|
import { createForkContextResolver } from "../../shared/fork-context.ts";
|
|
33
33
|
import { resolveCurrentSessionId } from "../../shared/session-identity.ts";
|
|
34
34
|
import { applyIntercomBridgeToAgent, INTERCOM_BRIDGE_MARKER, resolveIntercomBridge, resolveIntercomSessionTarget, resolveSubagentIntercomTarget, type IntercomBridgeState } from "../../intercom/intercom-bridge.ts";
|
|
@@ -344,6 +344,7 @@ async function resumeAsyncRun(input: {
|
|
|
344
344
|
config: input.deps.config.intercomBridge,
|
|
345
345
|
context: input.params.context,
|
|
346
346
|
orchestratorTarget: sessionName,
|
|
347
|
+
cwd: effectiveCwd,
|
|
347
348
|
});
|
|
348
349
|
const agents = intercomBridge.active
|
|
349
350
|
? discoveredAgents.map((agent) => applyIntercomBridgeToAgent(agent, intercomBridge))
|
|
@@ -396,9 +397,9 @@ async function resumeAsyncRun(input: {
|
|
|
396
397
|
`Session: ${target.sessionFile}`,
|
|
397
398
|
result.details.asyncDir ? `Async dir: ${result.details.asyncDir}` : undefined,
|
|
398
399
|
revivedTarget ? `Intercom target: ${revivedTarget} (if registered)` : undefined,
|
|
399
|
-
`
|
|
400
|
+
`Status if needed: subagent({ action: "status", id: "${revivedId}" })`,
|
|
400
401
|
].filter((line): line is string => Boolean(line));
|
|
401
|
-
return { content: [{ type: "text", text: lines.join("\n") }], details: result.details };
|
|
402
|
+
return { content: [{ type: "text", text: formatAsyncStartedMessage(lines.join("\n")) }], details: result.details };
|
|
402
403
|
}
|
|
403
404
|
|
|
404
405
|
function resultSummaryForIntercom(result: SingleResult): string {
|
|
@@ -1962,6 +1963,7 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
|
|
|
1962
1963
|
config: deps.config.intercomBridge,
|
|
1963
1964
|
context: effectiveParams.context,
|
|
1964
1965
|
orchestratorTarget: sessionName,
|
|
1966
|
+
cwd: effectiveCwd,
|
|
1965
1967
|
});
|
|
1966
1968
|
const agents = intercomBridge.active
|
|
1967
1969
|
? discoveredAgents.map((agent) => applyIntercomBridgeToAgent(agent, intercomBridge))
|
package/src/shared/types.ts
CHANGED
|
@@ -299,8 +299,11 @@ export interface AsyncStatus {
|
|
|
299
299
|
activityState?: ActivityState;
|
|
300
300
|
lastActivityAt?: number;
|
|
301
301
|
currentTool?: string;
|
|
302
|
+
currentToolArgs?: string;
|
|
302
303
|
currentToolStartedAt?: number;
|
|
303
304
|
currentPath?: string;
|
|
305
|
+
recentTools?: Array<{ tool: string; args: string; endMs: number }>;
|
|
306
|
+
recentOutput?: string[];
|
|
304
307
|
turnCount?: number;
|
|
305
308
|
toolCount?: number;
|
|
306
309
|
startedAt?: number;
|
|
@@ -320,6 +323,10 @@ export interface AsyncStatus {
|
|
|
320
323
|
sessionFile?: string;
|
|
321
324
|
}
|
|
322
325
|
|
|
326
|
+
export type AsyncJobStep = NonNullable<AsyncStatus["steps"]>[number] & {
|
|
327
|
+
index?: number;
|
|
328
|
+
};
|
|
329
|
+
|
|
323
330
|
export interface AsyncJobState {
|
|
324
331
|
asyncId: string;
|
|
325
332
|
asyncDir: string;
|
|
@@ -338,7 +345,7 @@ export interface AsyncJobState {
|
|
|
338
345
|
currentStep?: number;
|
|
339
346
|
chainStepCount?: number;
|
|
340
347
|
parallelGroups?: AsyncParallelGroupStatus[];
|
|
341
|
-
steps?:
|
|
348
|
+
steps?: AsyncJobStep[];
|
|
342
349
|
stepsTotal?: number;
|
|
343
350
|
runningSteps?: number;
|
|
344
351
|
completedSteps?: number;
|
package/src/tui/render.ts
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
* Rendering functions for subagent results
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
+
import * as path from "node:path";
|
|
5
6
|
import type { AgentToolResult } from "@mariozechner/pi-agent-core";
|
|
6
7
|
import { getMarkdownTheme, type ExtensionContext } from "@mariozechner/pi-coding-agent";
|
|
7
8
|
import { Container, Markdown, Spacer, Text, visibleWidth, type Component } from "@mariozechner/pi-tui";
|
|
@@ -14,6 +15,7 @@ import {
|
|
|
14
15
|
} from "../shared/types.ts";
|
|
15
16
|
import { formatTokens, formatUsage, formatDuration, formatToolCall, shortenPath } from "../shared/formatters.ts";
|
|
16
17
|
import { getDisplayItems, getLastActivity, getSingleResultOutput } from "../shared/utils.ts";
|
|
18
|
+
import { flatToLogicalStepIndex } from "../runs/background/parallel-groups.ts";
|
|
17
19
|
|
|
18
20
|
type Theme = ExtensionContext["ui"]["theme"];
|
|
19
21
|
|
|
@@ -264,10 +266,10 @@ function formatWidgetAgents(agents: string[]): string {
|
|
|
264
266
|
}
|
|
265
267
|
|
|
266
268
|
function widgetJobName(job: AsyncJobState): string {
|
|
267
|
-
|
|
268
|
-
if (job.mode === "
|
|
269
|
-
if (job.
|
|
270
|
-
if (job.agents?.length) return job.agents
|
|
269
|
+
if (job.mode === "parallel") return "parallel";
|
|
270
|
+
if (job.mode === "chain") return "chain";
|
|
271
|
+
if (job.mode === "single" && job.agents?.length === 1) return job.agents[0]!;
|
|
272
|
+
if (job.agents?.length) return formatWidgetAgents(job.agents);
|
|
271
273
|
return job.mode ?? "subagent";
|
|
272
274
|
}
|
|
273
275
|
|
|
@@ -293,6 +295,7 @@ function widgetActivity(job: AsyncJobState): string {
|
|
|
293
295
|
if (activity && facts.length) return `${activity} · ${facts.join(" · ")}`;
|
|
294
296
|
if (activity) return activity;
|
|
295
297
|
if (facts.length) return facts.join(" · ");
|
|
298
|
+
if (job.status === "running") return "thinking…";
|
|
296
299
|
if (job.status === "queued") return "queued…";
|
|
297
300
|
if (job.status === "paused") return "Paused";
|
|
298
301
|
if (job.status === "failed") return "Failed";
|
|
@@ -308,7 +311,7 @@ function widgetStatusGlyph(job: AsyncJobState, theme: Theme): string {
|
|
|
308
311
|
}
|
|
309
312
|
|
|
310
313
|
function widgetStepGlyph(status: string, theme: Theme): string {
|
|
311
|
-
if (status === "running") return theme.fg("accent",
|
|
314
|
+
if (status === "running") return theme.fg("accent", spinnerFrame());
|
|
312
315
|
if (status === "complete" || status === "completed") return theme.fg("success", "✓");
|
|
313
316
|
if (status === "failed") return theme.fg("error", "✗");
|
|
314
317
|
if (status === "paused") return theme.fg("warning", "■");
|
|
@@ -337,14 +340,62 @@ function widgetStepActivity(step: NonNullable<AsyncJobState["steps"]>[number]):
|
|
|
337
340
|
return facts.join(" · ");
|
|
338
341
|
}
|
|
339
342
|
|
|
343
|
+
function widgetAggregateStepStatus(steps: NonNullable<AsyncJobState["steps"]>): string {
|
|
344
|
+
if (steps.some((step) => step.status === "running")) return "running";
|
|
345
|
+
if (steps.some((step) => step.status === "failed")) return "failed";
|
|
346
|
+
if (steps.some((step) => step.status === "paused")) return "paused";
|
|
347
|
+
if (steps.length > 0 && steps.every((step) => step.status === "complete" || step.status === "completed")) return "complete";
|
|
348
|
+
return "pending";
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
function widgetParallelOutcome(steps: NonNullable<AsyncJobState["steps"]>, total: number): string {
|
|
352
|
+
const running = steps.filter((step) => step.status === "running").length;
|
|
353
|
+
const done = steps.filter((step) => step.status === "complete" || step.status === "completed").length;
|
|
354
|
+
const failed = steps.filter((step) => step.status === "failed").length;
|
|
355
|
+
const paused = steps.filter((step) => step.status === "paused").length;
|
|
356
|
+
const parts = [`${done}/${total} done`];
|
|
357
|
+
if (running > 0) parts.unshift(formatAgentRunningLabel(running));
|
|
358
|
+
if (failed > 0) parts.push(`${failed} failed`);
|
|
359
|
+
if (paused > 0) parts.push(`${paused} paused`);
|
|
360
|
+
return parts.join(" · ");
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
function widgetChainDetails(job: AsyncJobState, theme: Theme, expanded = false, width = getTermWidth()): string[] {
|
|
364
|
+
if (!job.steps?.length) return [];
|
|
365
|
+
const total = job.chainStepCount ?? job.steps.length;
|
|
366
|
+
const groups = job.parallelGroups ?? [];
|
|
367
|
+
const lines: string[] = [];
|
|
368
|
+
let flatIndex = 0;
|
|
369
|
+
for (let stepIndex = 0; stepIndex < total; stepIndex++) {
|
|
370
|
+
const group = groups.find((candidate) => candidate.stepIndex === stepIndex);
|
|
371
|
+
if (group) {
|
|
372
|
+
const steps = job.steps.slice(group.start, group.start + group.count);
|
|
373
|
+
const status = widgetAggregateStepStatus(steps);
|
|
374
|
+
lines.push(` ${widgetStepGlyph(status, theme)} Step ${stepIndex + 1}/${total}: ${themeBold(theme, "parallel group")} ${theme.fg("dim", "·")} ${theme.fg("dim", widgetParallelOutcome(steps, group.count))}`);
|
|
375
|
+
flatIndex = Math.max(flatIndex, group.start + group.count);
|
|
376
|
+
continue;
|
|
377
|
+
}
|
|
378
|
+
const step = job.steps[flatIndex];
|
|
379
|
+
if (!step) {
|
|
380
|
+
lines.push(` ${theme.fg("dim", `◦ Step ${stepIndex + 1}/${total}: pending`)}`);
|
|
381
|
+
continue;
|
|
382
|
+
}
|
|
383
|
+
lines.push(...foregroundStyleWidgetStepLines(job, theme, step, "Step", stepIndex + 1, total, expanded, width));
|
|
384
|
+
flatIndex++;
|
|
385
|
+
}
|
|
386
|
+
return lines;
|
|
387
|
+
}
|
|
388
|
+
|
|
340
389
|
function widgetParallelAgentDetails(job: AsyncJobState, theme: Theme): string[] {
|
|
341
|
-
if (!job.
|
|
390
|
+
if (!job.steps?.length) return [];
|
|
342
391
|
if (job.mode !== "parallel" && job.mode !== "chain") return [];
|
|
392
|
+
if (job.mode === "chain" && !job.activeParallelGroup && job.parallelGroups?.length) return widgetChainDetails(job, theme);
|
|
343
393
|
const total = job.stepsTotal ?? job.steps.length;
|
|
344
394
|
return job.steps.map((step, index) => {
|
|
345
395
|
const marker = index === job.steps!.length - 1 ? "└" : "├";
|
|
346
396
|
const activity = widgetStepActivity(step);
|
|
347
|
-
|
|
397
|
+
const itemTitle = job.mode === "parallel" || job.activeParallelGroup ? "Agent" : "Step";
|
|
398
|
+
return ` ${theme.fg("dim", `${marker} ${widgetStepGlyph(step.status, theme)} ${itemTitle} ${index + 1}/${total}: ${step.agent} · ${widgetStepStatus(step.status, theme)}${activity ? ` · ${activity}` : ""}`)}`;
|
|
348
399
|
});
|
|
349
400
|
}
|
|
350
401
|
|
|
@@ -513,7 +564,7 @@ function widgetStats(job: AsyncJobState, theme: Theme): string {
|
|
|
513
564
|
const running = job.runningSteps ?? (job.status === "running" ? 1 : 0);
|
|
514
565
|
const done = job.completedSteps ?? (job.status === "complete" ? stepsTotal : 0);
|
|
515
566
|
if (job.mode === "parallel") {
|
|
516
|
-
if (job.status === "running") parts.push(formatAgentRunningLabel(running));
|
|
567
|
+
if (job.status === "running" && running > 0) parts.push(formatAgentRunningLabel(running));
|
|
517
568
|
if (stepsTotal > 0) parts.push(`${done}/${stepsTotal} done`);
|
|
518
569
|
} else {
|
|
519
570
|
const activeGroup = job.currentStep !== undefined
|
|
@@ -521,24 +572,157 @@ function widgetStats(job: AsyncJobState, theme: Theme): string {
|
|
|
521
572
|
: job.parallelGroups?.find((group) => group.start === 0);
|
|
522
573
|
const logicalStep = activeGroup?.stepIndex ?? job.currentStep ?? 0;
|
|
523
574
|
const total = job.chainStepCount ?? stepsTotal;
|
|
524
|
-
const
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
parts.push(`step ${logicalStep + 1}/${total} · parallel group: ${groupProgress}`);
|
|
575
|
+
const groupParts = [`${done}/${stepsTotal} done`];
|
|
576
|
+
if (job.status === "running" && running > 0) groupParts.unshift(formatAgentRunningLabel(running));
|
|
577
|
+
parts.push(`step ${logicalStep + 1}/${total} · parallel group: ${groupParts.join(" · ")}`);
|
|
528
578
|
}
|
|
529
579
|
} else if (job.currentStep !== undefined) {
|
|
530
|
-
|
|
580
|
+
if (job.mode === "chain" && job.parallelGroups?.length) {
|
|
581
|
+
const total = job.chainStepCount ?? stepsTotal;
|
|
582
|
+
parts.push(`step ${flatToLogicalStepIndex(job.currentStep, total, job.parallelGroups) + 1}/${total}`);
|
|
583
|
+
} else {
|
|
584
|
+
parts.push(`step ${job.currentStep + 1}/${stepsTotal}`);
|
|
585
|
+
}
|
|
531
586
|
} else if (stepsTotal > 1) {
|
|
532
587
|
parts.push(`steps ${stepsTotal}`);
|
|
533
588
|
}
|
|
589
|
+
if (job.toolCount !== undefined) parts.push(formatToolUseStat(job.toolCount));
|
|
534
590
|
if (job.totalTokens?.total) parts.push(formatTokenStat(job.totalTokens.total));
|
|
535
591
|
const endTime = job.status === "complete" || job.status === "failed" || job.status === "paused" ? (job.updatedAt ?? Date.now()) : Date.now();
|
|
536
592
|
if (job.startedAt) parts.push(formatDuration(Math.max(0, endTime - job.startedAt)));
|
|
537
593
|
return statJoin(theme, parts);
|
|
538
594
|
}
|
|
539
595
|
|
|
540
|
-
|
|
596
|
+
function widgetStepStats(theme: Theme, step: NonNullable<AsyncJobState["steps"]>[number]): string {
|
|
597
|
+
return statJoin(theme, [
|
|
598
|
+
step.turnCount !== undefined ? `${step.turnCount} turns` : "",
|
|
599
|
+
step.toolCount !== undefined ? formatToolUseStat(step.toolCount) : "",
|
|
600
|
+
step.tokens?.total ? formatTokenStat(step.tokens.total) : "",
|
|
601
|
+
step.durationMs !== undefined ? formatDuration(step.durationMs) : "",
|
|
602
|
+
]);
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
function widgetStepActivityLine(step: NonNullable<AsyncJobState["steps"]>[number], width: number, expanded: boolean): string {
|
|
606
|
+
const toolLine = formatCurrentToolLine(step, width, expanded);
|
|
607
|
+
if (toolLine) return toolLine;
|
|
608
|
+
const activity = formatActivityLabel(step.lastActivityAt, step.activityState);
|
|
609
|
+
if (activity) return activity;
|
|
610
|
+
if (step.status === "running") return "thinking…";
|
|
611
|
+
return "";
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
function widgetOutputPath(job: AsyncJobState, step: NonNullable<AsyncJobState["steps"]>[number]): string | undefined {
|
|
615
|
+
if (typeof step.index !== "number") return undefined;
|
|
616
|
+
return path.join(job.asyncDir, `output-${step.index}.log`);
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
function foregroundStyleWidgetStepLines(
|
|
620
|
+
job: AsyncJobState,
|
|
621
|
+
theme: Theme,
|
|
622
|
+
step: NonNullable<AsyncJobState["steps"]>[number],
|
|
623
|
+
itemTitle: "Agent" | "Step",
|
|
624
|
+
index: number,
|
|
625
|
+
total: number,
|
|
626
|
+
expanded: boolean,
|
|
627
|
+
width: number,
|
|
628
|
+
): string[] {
|
|
629
|
+
const stats = widgetStepStats(theme, step);
|
|
630
|
+
const lines = [` ${widgetStepGlyph(step.status, theme)} ${itemTitle} ${index}/${total}: ${themeBold(theme, step.agent)}${stats ? ` ${theme.fg("dim", "·")} ${stats}` : ""}`];
|
|
631
|
+
const activity = widgetStepActivityLine(step, width, expanded);
|
|
632
|
+
if (activity) lines.push(` ${theme.fg("dim", `⎿ ${activity}`)}`);
|
|
633
|
+
if (step.status === "running") {
|
|
634
|
+
if (!expanded) lines.push(` ${theme.fg("accent", "Press Ctrl+O for live detail")}`);
|
|
635
|
+
const output = widgetOutputPath(job, step);
|
|
636
|
+
if (output) lines.push(` ${theme.fg("dim", `output: ${shortenPath(output)}`)}`);
|
|
637
|
+
if (expanded) {
|
|
638
|
+
const liveStatus = buildLiveStatusLine(step);
|
|
639
|
+
if (liveStatus && liveStatus !== activity) lines.push(` ${theme.fg("accent", liveStatus)}`);
|
|
640
|
+
for (const tool of step.recentTools?.slice(-3) ?? []) {
|
|
641
|
+
const maxArgsLen = Math.max(40, width - 30);
|
|
642
|
+
const argsPreview = tool.args.length <= maxArgsLen ? tool.args : `${tool.args.slice(0, maxArgsLen)}...`;
|
|
643
|
+
lines.push(` ${theme.fg("dim", `${tool.tool}${argsPreview ? `: ${argsPreview}` : ""}`)}`);
|
|
644
|
+
}
|
|
645
|
+
for (const line of step.recentOutput?.slice(-5) ?? []) {
|
|
646
|
+
lines.push(` ${theme.fg("dim", line)}`);
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
return lines;
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
function foregroundStyleWidgetDetails(job: AsyncJobState, theme: Theme, expanded: boolean, width: number): string[] {
|
|
654
|
+
if (!job.steps?.length) return [` ${theme.fg("dim", `⎿ ${widgetActivity(job)}`)}`];
|
|
655
|
+
if (job.mode !== "parallel" && job.mode !== "chain") return [` ${theme.fg("dim", `⎿ ${widgetActivity(job)}`)}`];
|
|
656
|
+
if (job.mode === "chain" && !job.activeParallelGroup && job.parallelGroups?.length) return widgetChainDetails(job, theme, expanded, width);
|
|
657
|
+
const total = job.stepsTotal ?? job.steps.length;
|
|
658
|
+
const itemTitle = job.mode === "parallel" || job.activeParallelGroup ? "Agent" : "Step";
|
|
659
|
+
const lines: string[] = [];
|
|
660
|
+
for (const [index, step] of job.steps.entries()) {
|
|
661
|
+
lines.push(...foregroundStyleWidgetStepLines(job, theme, step, itemTitle, index + 1, total, expanded, width));
|
|
662
|
+
}
|
|
663
|
+
return lines;
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
function buildSingleWidgetLines(job: AsyncJobState, theme: Theme, width: number, expanded: boolean): string[] {
|
|
667
|
+
const stats = widgetStats(job, theme);
|
|
668
|
+
const count = job.mode === "chain" ? job.chainStepCount : job.stepsTotal ?? job.agents?.length ?? job.steps?.length;
|
|
669
|
+
const mode = widgetJobName(job);
|
|
670
|
+
const title = `async subagent ${mode}${count && count > 1 ? ` (${count})` : ""}`;
|
|
671
|
+
return [
|
|
672
|
+
`${theme.fg("toolTitle", themeBold(theme, title))} ${theme.fg("dim", "· background · /subagents-status")}`,
|
|
673
|
+
`${widgetStatusGlyph(job, theme)} ${themeBold(theme, mode)}${stats ? ` ${theme.fg("dim", "·")} ${stats}` : ""}`,
|
|
674
|
+
...foregroundStyleWidgetDetails(job, theme, expanded, width),
|
|
675
|
+
].map((line) => truncLine(line, width));
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
function compactSingleWidgetLines(job: AsyncJobState, theme: Theme, width: number): string[] {
|
|
679
|
+
const fullLines = buildSingleWidgetLines(job, theme, width, false);
|
|
680
|
+
if (fullLines.length <= 10 || !job.steps?.length || (job.mode !== "parallel" && !job.activeParallelGroup)) return fullLines;
|
|
681
|
+
|
|
682
|
+
const total = job.stepsTotal ?? job.steps.length;
|
|
683
|
+
const itemTitle = job.mode === "parallel" || job.activeParallelGroup ? "Agent" : "Step";
|
|
684
|
+
const lines = fullLines.slice(0, 2);
|
|
685
|
+
for (const [index, step] of job.steps.entries()) {
|
|
686
|
+
const stepStats = widgetStepStats(theme, step);
|
|
687
|
+
const activity = widgetStepActivityLine(step, width, false);
|
|
688
|
+
const activitySuffix = activity ? ` ${theme.fg("dim", "·")} ${theme.fg("dim", activity)}` : "";
|
|
689
|
+
lines.push(` ${widgetStepGlyph(step.status, theme)} ${itemTitle} ${index + 1}/${total}: ${themeBold(theme, step.agent)}${stepStats ? ` ${theme.fg("dim", "·")} ${stepStats}` : ""}${activitySuffix}`);
|
|
690
|
+
}
|
|
691
|
+
if (job.steps.some((step) => step.status === "running")) lines.push(theme.fg("accent", " Press Ctrl+O for live detail · /subagents-status for output paths"));
|
|
692
|
+
return lines.map((line) => truncLine(line, width));
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
function fitWidgetLineBudget(lines: string[], theme: Theme, width: number, expanded: boolean): string[] {
|
|
696
|
+
const rows = process.stdout.rows || 30;
|
|
697
|
+
const budget = expanded
|
|
698
|
+
? Math.max(12, Math.min(24, Math.floor(rows * 0.55)))
|
|
699
|
+
: Math.max(10, Math.min(14, Math.floor(rows * 0.35)));
|
|
700
|
+
if (lines.length <= budget) return lines;
|
|
701
|
+
const visibleLines = Math.max(1, budget - 1);
|
|
702
|
+
const hiddenCount = lines.length - visibleLines;
|
|
703
|
+
const hint = expanded
|
|
704
|
+
? `… ${hiddenCount} live-detail lines hidden · /subagents-status for full detail`
|
|
705
|
+
: `… ${hiddenCount} lines hidden · Ctrl+O expands · /subagents-status for full detail`;
|
|
706
|
+
return [...lines.slice(0, visibleLines), truncLine(theme.fg("dim", hint), width)];
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
function buildWidgetComponent(jobs: AsyncJobState[], expanded: boolean): (_tui: unknown, theme: Theme) => Component {
|
|
710
|
+
return (_tui, theme) => {
|
|
711
|
+
const width = getTermWidth();
|
|
712
|
+
const lines = expanded
|
|
713
|
+
? buildWidgetLines(jobs, theme, width, true)
|
|
714
|
+
: jobs.length === 1
|
|
715
|
+
? compactSingleWidgetLines(jobs[0]!, theme, width)
|
|
716
|
+
: buildWidgetLines(jobs, theme, width, false);
|
|
717
|
+
const container = new Container();
|
|
718
|
+
for (const line of fitWidgetLineBudget(lines, theme, width, expanded)) container.addChild(new Text(line, 1, 0));
|
|
719
|
+
return container;
|
|
720
|
+
};
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
export function buildWidgetLines(jobs: AsyncJobState[], theme: Theme, width = getTermWidth(), expanded = false): string[] {
|
|
541
724
|
if (jobs.length === 0) return [];
|
|
725
|
+
if (jobs.length === 1) return buildSingleWidgetLines(jobs[0]!, theme, width, expanded);
|
|
542
726
|
const running = jobs.filter((job) => job.status === "running");
|
|
543
727
|
const queued = jobs.filter((job) => job.status === "queued");
|
|
544
728
|
const finished = jobs.filter((job) => job.status !== "running" && job.status !== "queued");
|
|
@@ -608,7 +792,7 @@ export function buildWidgetLines(jobs: AsyncJobState[], theme: Theme, width = ge
|
|
|
608
792
|
function refreshAnimatedWidget(): void {
|
|
609
793
|
try {
|
|
610
794
|
if (!latestWidgetCtx?.hasUI || latestWidgetJobs.length === 0) return;
|
|
611
|
-
latestWidgetCtx.ui.setWidget(WIDGET_KEY,
|
|
795
|
+
latestWidgetCtx.ui.setWidget(WIDGET_KEY, buildWidgetComponent(latestWidgetJobs, latestWidgetCtx.ui.getToolsExpanded?.() ?? false));
|
|
612
796
|
latestWidgetCtx.ui.requestRender?.();
|
|
613
797
|
} catch (error) {
|
|
614
798
|
if (!isStaleExtensionContextError(error)) throw error;
|
|
@@ -662,7 +846,7 @@ export function renderWidget(ctx: ExtensionContext, jobs: AsyncJobState[]): void
|
|
|
662
846
|
latestWidgetCtx = ctx;
|
|
663
847
|
latestWidgetJobs = [...jobs];
|
|
664
848
|
|
|
665
|
-
ctx.ui.setWidget(WIDGET_KEY,
|
|
849
|
+
ctx.ui.setWidget(WIDGET_KEY, buildWidgetComponent(jobs, ctx.ui.getToolsExpanded?.() ?? false));
|
|
666
850
|
if (hasAnimatedWidgetJobs(jobs)) ensureWidgetAnimation();
|
|
667
851
|
else stopWidgetAnimation();
|
|
668
852
|
}
|
|
@@ -62,11 +62,20 @@ function stepGlyph(theme: Theme, status: string): string {
|
|
|
62
62
|
return theme.fg("dim", "◦");
|
|
63
63
|
}
|
|
64
64
|
|
|
65
|
+
function runGlyph(theme: Theme, status: AsyncRunSummary["state"]): string {
|
|
66
|
+
if (status === "running") return theme.fg("accent", "▶");
|
|
67
|
+
if (status === "queued") return theme.fg("dim", "◦");
|
|
68
|
+
if (status === "complete") return theme.fg("success", "✓");
|
|
69
|
+
if (status === "paused") return theme.fg("warning", "■");
|
|
70
|
+
return theme.fg("error", "✗");
|
|
71
|
+
}
|
|
72
|
+
|
|
65
73
|
function runLabel(theme: Theme, run: AsyncRunSummary, selected: boolean): string {
|
|
66
74
|
const prefix = selected ? theme.fg("accent", ">") : " ";
|
|
67
75
|
const stepLabel = formatAsyncRunProgressLabel(run);
|
|
68
76
|
const cwd = shortenPath(run.cwd ?? run.asyncDir);
|
|
69
|
-
|
|
77
|
+
const mode = ((theme as { bold?: (value: string) => string }).bold?.(run.mode)) ?? run.mode;
|
|
78
|
+
return `${prefix} ${runGlyph(theme, run.state)} ${mode} · ${stepLabel} · ${run.id.slice(0, 8)} · ${cwd}`;
|
|
70
79
|
}
|
|
71
80
|
|
|
72
81
|
function selectedIndex(rows: StatusRow[], cursor: number): number {
|
|
@@ -332,7 +341,13 @@ export class SubagentsStatusComponent implements Component {
|
|
|
332
341
|
if (run.sessionFile) {
|
|
333
342
|
lines.push(row(`session: ${truncateToWidth(shortenPath(run.sessionFile), innerW - 9)}`, width, this.theme));
|
|
334
343
|
}
|
|
335
|
-
|
|
344
|
+
if (run.mode === "chain" && (run.chainStepCount !== undefined || run.parallelGroups?.length)) {
|
|
345
|
+
lines.push(...this.renderChainProgressRows(run, width, innerW));
|
|
346
|
+
} else if (run.mode === "parallel") {
|
|
347
|
+
lines.push(...this.renderAgentRows(run, width, innerW));
|
|
348
|
+
} else {
|
|
349
|
+
lines.push(...this.renderStepRows(run, width, innerW));
|
|
350
|
+
}
|
|
336
351
|
return lines;
|
|
337
352
|
}
|
|
338
353
|
|
|
@@ -438,7 +453,7 @@ export class SubagentsStatusComponent implements Component {
|
|
|
438
453
|
: undefined;
|
|
439
454
|
|
|
440
455
|
const body: string[] = [];
|
|
441
|
-
body.push(...detailRows(`${
|
|
456
|
+
body.push(...detailRows(`${runGlyph(this.theme, run.state)} ${run.mode} · ${stepLabel} · ${statusColor(this.theme, run.state)} · ${duration}`, width, innerW, this.theme));
|
|
442
457
|
if (activity) body.push(...detailRows(activity, width, innerW, this.theme));
|
|
443
458
|
body.push(row("", width, this.theme));
|
|
444
459
|
if (run.mode === "chain" && (run.chainStepCount !== undefined || run.parallelGroups?.length)) {
|