pi-crew 0.1.45 → 0.1.49
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 +97 -0
- package/README.md +5 -5
- package/agents/analyst.md +11 -11
- package/agents/critic.md +11 -11
- package/agents/executor.md +11 -11
- package/agents/explorer.md +11 -11
- package/agents/planner.md +11 -11
- package/agents/reviewer.md +11 -11
- package/agents/security-reviewer.md +11 -11
- package/agents/test-engineer.md +11 -11
- package/agents/verifier.md +11 -11
- package/agents/writer.md +11 -11
- package/docs/next-upgrade-roadmap.md +808 -0
- package/docs/research/AGENT-EXECUTION-ARCHITECTURE.md +261 -0
- package/docs/research/AGENT-LIFECYCLE-COMPARISON.md +111 -0
- package/docs/research/AUDIT_OH_MY_PI.md +261 -0
- package/docs/research/AUDIT_PI_CREW.md +457 -0
- package/docs/research/CAVEMAN-DEEP-RESEARCH.md +281 -0
- package/docs/research/COMPARISON_OH_MY_PI_VS_PI_CREW.md +264 -0
- package/docs/research/DEEP-RESEARCH-PI-POWERBAR.md +343 -0
- package/docs/research/DEEP_RESEARCH_SUBAGENT_ARCHITECTURE.md +480 -0
- package/docs/research/GAP_CLOSURE_IMPLEMENTATION_PLAN.md +354 -0
- package/docs/research/IMPLEMENTATION_PLAN.md +385 -0
- package/docs/research/LIVE-SESSION-PRODUCTION-READY-PLAN.md +502 -0
- package/docs/research/OH-MY-PI-DEEP-RESEARCH-v14.7.6.md +266 -0
- package/docs/research/REMAINING-GAPS-PLAN.md +363 -0
- package/docs/research/SESSION-SUMMARY-2026-05-08.md +146 -0
- package/docs/research/UI-RESPONSIVENESS-AUDIT.md +173 -0
- package/docs/research-awesome-agent-skills-distillation.md +100 -0
- package/docs/research-oh-my-pi-distillation.md +369 -0
- package/docs/source-runtime-refactor-map.md +24 -0
- package/docs/usage.md +3 -3
- package/install.mjs +52 -8
- package/package.json +99 -98
- package/schema.json +10 -1
- package/skills/async-worker-recovery/SKILL.md +42 -0
- package/skills/context-artifact-hygiene/SKILL.md +52 -0
- package/skills/delegation-patterns/SKILL.md +54 -0
- package/skills/mailbox-interactive/SKILL.md +40 -0
- package/skills/model-routing-context/SKILL.md +39 -0
- package/skills/multi-perspective-review/SKILL.md +58 -0
- package/skills/observability-reliability/SKILL.md +41 -0
- package/skills/orchestration/SKILL.md +157 -0
- package/skills/ownership-session-security/SKILL.md +41 -0
- package/skills/pi-extension-lifecycle/SKILL.md +39 -0
- package/skills/requirements-to-task-packet/SKILL.md +63 -0
- package/skills/resource-discovery-config/SKILL.md +41 -0
- package/skills/runtime-state-reader/SKILL.md +44 -0
- package/skills/secure-agent-orchestration-review/SKILL.md +45 -0
- package/skills/state-mutation-locking/SKILL.md +42 -0
- package/skills/systematic-debugging/SKILL.md +67 -0
- package/skills/ui-render-performance/SKILL.md +39 -0
- package/skills/verification-before-done/SKILL.md +57 -0
- package/skills/worktree-isolation/SKILL.md +39 -0
- package/src/agents/agent-config.ts +6 -0
- package/src/agents/agent-search.ts +98 -0
- package/src/agents/agent-serializer.ts +38 -34
- package/src/agents/discover-agents.ts +29 -15
- package/src/config/config.ts +72 -24
- package/src/config/defaults.ts +25 -0
- package/src/extension/autonomous-policy.ts +26 -33
- package/src/extension/help.ts +1 -0
- package/src/extension/management.ts +5 -0
- package/src/extension/project-init.ts +62 -2
- package/src/extension/register.ts +69 -22
- package/src/extension/registration/commands.ts +64 -25
- package/src/extension/registration/compaction-guard.ts +1 -1
- package/src/extension/registration/subagent-helpers.ts +8 -0
- package/src/extension/registration/subagent-tools.ts +149 -148
- package/src/extension/registration/team-tool.ts +14 -10
- package/src/extension/run-index.ts +35 -21
- package/src/extension/run-maintenance.ts +30 -5
- package/src/extension/team-tool/api.ts +47 -9
- package/src/extension/team-tool/cancel.ts +109 -5
- package/src/extension/team-tool/context.ts +8 -0
- package/src/extension/team-tool/intent-policy.ts +42 -0
- package/src/extension/team-tool/lifecycle-actions.ts +120 -79
- package/src/extension/team-tool/parallel-dispatch.ts +156 -0
- package/src/extension/team-tool/respond.ts +46 -18
- package/src/extension/team-tool/run.ts +55 -12
- package/src/extension/team-tool/status.ts +13 -2
- package/src/extension/team-tool-types.ts +3 -0
- package/src/extension/team-tool.ts +45 -14
- package/src/hooks/registry.ts +61 -0
- package/src/hooks/types.ts +41 -0
- package/src/observability/event-to-metric.ts +8 -1
- package/src/runtime/agent-control.ts +169 -63
- package/src/runtime/async-runner.ts +3 -1
- package/src/runtime/background-runner.ts +78 -53
- package/src/runtime/cancellation-token.ts +89 -0
- package/src/runtime/cancellation.ts +61 -0
- package/src/runtime/capability-inventory.ts +116 -0
- package/src/runtime/child-pi.ts +458 -444
- package/src/runtime/code-summary.ts +247 -0
- package/src/runtime/crash-recovery.ts +182 -0
- package/src/runtime/crew-agent-records.ts +70 -10
- package/src/runtime/crew-agent-runtime.ts +1 -0
- package/src/runtime/custom-tools/irc-tool.ts +201 -0
- package/src/runtime/custom-tools/submit-result-tool.ts +90 -0
- package/src/runtime/deadletter.ts +1 -0
- package/src/runtime/delivery-coordinator.ts +48 -25
- package/src/runtime/effectiveness.ts +81 -0
- package/src/runtime/event-stream-bridge.ts +90 -0
- package/src/runtime/live-agent-control.ts +2 -1
- package/src/runtime/live-agent-manager.ts +179 -85
- package/src/runtime/live-control-realtime.ts +1 -1
- package/src/runtime/live-extension-bridge.ts +150 -0
- package/src/runtime/live-irc.ts +92 -0
- package/src/runtime/live-session-health.ts +100 -0
- package/src/runtime/live-session-runtime.ts +599 -305
- package/src/runtime/manifest-cache.ts +17 -2
- package/src/runtime/mcp-proxy.ts +113 -0
- package/src/runtime/model-fallback.ts +6 -4
- package/src/runtime/notebook-helpers.ts +90 -0
- package/src/runtime/orphan-sentinel.ts +7 -0
- package/src/runtime/output-validator.ts +187 -0
- package/src/runtime/parallel-utils.ts +57 -0
- package/src/runtime/parent-guard.ts +80 -0
- package/src/runtime/pi-args.ts +18 -3
- package/src/runtime/process-status.ts +5 -1
- package/src/runtime/prose-compressor.ts +164 -0
- package/src/runtime/result-extractor.ts +121 -0
- package/src/runtime/retry-executor.ts +81 -64
- package/src/runtime/runtime-resolver.ts +23 -10
- package/src/runtime/semaphore.ts +131 -0
- package/src/runtime/sensitive-paths.ts +92 -0
- package/src/runtime/skill-instructions.ts +222 -0
- package/src/runtime/stale-reconciler.ts +4 -14
- package/src/runtime/stream-preview.ts +177 -0
- package/src/runtime/subagent-manager.ts +6 -2
- package/src/runtime/subprocess-tool-registry.ts +67 -0
- package/src/runtime/task-output-context.ts +177 -127
- package/src/runtime/task-runner/capabilities.ts +78 -0
- package/src/runtime/task-runner/live-executor.ts +107 -101
- package/src/runtime/task-runner/prompt-builder.ts +72 -8
- package/src/runtime/task-runner/prompt-pipeline.ts +64 -0
- package/src/runtime/task-runner/run-projection.ts +104 -0
- package/src/runtime/task-runner.ts +115 -5
- package/src/runtime/team-runner.ts +134 -19
- package/src/runtime/workspace-tree.ts +298 -0
- package/src/runtime/yield-handler.ts +189 -0
- package/src/schema/config-schema.ts +7 -0
- package/src/schema/team-tool-schema.ts +14 -4
- package/src/skills/discover-skills.ts +67 -0
- package/src/state/active-run-registry.ts +167 -0
- package/src/state/artifact-store.ts +4 -1
- package/src/state/atomic-write.ts +50 -1
- package/src/state/blob-store.ts +117 -0
- package/src/state/contracts.ts +2 -1
- package/src/state/event-log-rotation.ts +158 -0
- package/src/state/event-log.ts +52 -2
- package/src/state/mailbox.ts +129 -9
- package/src/state/state-store.ts +32 -5
- package/src/state/types.ts +64 -2
- package/src/teams/team-config.ts +1 -0
- package/src/ui/agent-management-overlay.ts +144 -0
- package/src/ui/crew-widget.ts +15 -5
- package/src/ui/dashboard-panes/cancellation-pane.ts +43 -0
- package/src/ui/dashboard-panes/capability-pane.ts +60 -0
- package/src/ui/dashboard-panes/mailbox-pane.ts +35 -11
- package/src/ui/dashboard-panes/progress-pane.ts +2 -0
- package/src/ui/live-run-sidebar.ts +4 -0
- package/src/ui/powerbar-publisher.ts +77 -15
- package/src/ui/render-coalescer.ts +51 -0
- package/src/ui/run-dashboard.ts +4 -0
- package/src/ui/run-event-bus.ts +209 -0
- package/src/ui/run-snapshot-cache.ts +78 -18
- package/src/ui/snapshot-types.ts +10 -0
- package/src/ui/transcript-entries.ts +258 -0
- package/src/utils/ids.ts +5 -0
- package/src/utils/incremental-reader.ts +104 -0
- package/src/utils/paths.ts +4 -2
- package/src/utils/scan-cache.ts +137 -0
- package/src/utils/sse-parser.ts +134 -0
- package/src/utils/task-name-generator.ts +337 -0
- package/src/utils/visual.ts +33 -2
- package/src/workflows/workflow-config.ts +1 -0
- package/src/worktree/cleanup.ts +2 -1
package/src/state/state-store.ts
CHANGED
|
@@ -27,12 +27,15 @@ interface ManifestCacheEntry {
|
|
|
27
27
|
manifestSize: number;
|
|
28
28
|
tasksMtimeMs: number;
|
|
29
29
|
tasksSize: number;
|
|
30
|
+
cachedAt?: number;
|
|
30
31
|
}
|
|
31
32
|
|
|
33
|
+
const MANIFEST_CACHE_TTL_MS = 5 * 60 * 1000; // 5 minutes
|
|
32
34
|
const manifestCache = new Map<string, ManifestCacheEntry>();
|
|
33
35
|
|
|
34
36
|
function setManifestCache(stateRoot: string, entry: ManifestCacheEntry): void {
|
|
35
37
|
if (manifestCache.has(stateRoot)) manifestCache.delete(stateRoot);
|
|
38
|
+
entry.cachedAt = Date.now();
|
|
36
39
|
manifestCache.set(stateRoot, entry);
|
|
37
40
|
while (manifestCache.size > DEFAULT_CACHE.manifestMaxEntries) {
|
|
38
41
|
const oldest = manifestCache.keys().next().value;
|
|
@@ -196,7 +199,21 @@ export async function saveRunTasksAsync(manifest: TeamRunManifest, tasks: TeamTa
|
|
|
196
199
|
invalidateRunCache(manifest.stateRoot);
|
|
197
200
|
}
|
|
198
201
|
|
|
199
|
-
|
|
202
|
+
/** M8: Atomically save manifest + tasks and invalidate cache once to prevent stale reads between saves */
|
|
203
|
+
export async function saveManifestAndTasksAtomic(manifest: TeamRunManifest, tasks: TeamTaskState[]): Promise<void> {
|
|
204
|
+
await Promise.all([
|
|
205
|
+
atomicWriteJsonAsync(path.join(manifest.stateRoot, "manifest.json"), manifest),
|
|
206
|
+
atomicWriteJsonAsync(manifest.tasksPath, tasks),
|
|
207
|
+
]);
|
|
208
|
+
invalidateRunCache(manifest.stateRoot);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
export interface UpdateRunStatusOptions {
|
|
212
|
+
data?: Record<string, unknown>;
|
|
213
|
+
metadata?: Parameters<typeof appendEvent>[1]["metadata"];
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
export function updateRunStatus(manifest: TeamRunManifest, status: TeamRunManifest["status"], summary?: string, options: UpdateRunStatusOptions = {}): TeamRunManifest {
|
|
200
217
|
if (!canTransitionRunStatus(manifest.status, status)) {
|
|
201
218
|
throw new Error(`Invalid run status transition: ${manifest.status} -> ${status}`);
|
|
202
219
|
}
|
|
@@ -206,11 +223,13 @@ export function updateRunStatus(manifest: TeamRunManifest, status: TeamRunManife
|
|
|
206
223
|
type: `run.${status}`,
|
|
207
224
|
runId: updated.runId,
|
|
208
225
|
message: summary,
|
|
226
|
+
...(options.data ? { data: options.data } : {}),
|
|
209
227
|
metadata: {
|
|
210
228
|
provenance: "team_runner",
|
|
211
229
|
sessionIdentity: { title: updated.team, workspace: updated.cwd, purpose: updated.goal },
|
|
212
230
|
ownership: { owner: updated.team, workflowScope: updated.workflow ?? "manual", watcherAction: "act" },
|
|
213
231
|
confidence: "high",
|
|
232
|
+
...options.metadata,
|
|
214
233
|
},
|
|
215
234
|
});
|
|
216
235
|
return updated;
|
|
@@ -259,11 +278,15 @@ export function loadRunManifestById(cwd: string, runId: string): { manifest: Tea
|
|
|
259
278
|
&& cached.tasksMtimeMs === tasksMtimeMs
|
|
260
279
|
&& cached.tasksSize === (tasksStat?.size ?? 0)
|
|
261
280
|
) {
|
|
262
|
-
|
|
281
|
+
// TTL eviction: expire stale entries even if mtime matches
|
|
282
|
+
if (cached.cachedAt && Date.now() - cached.cachedAt > MANIFEST_CACHE_TTL_MS) {
|
|
283
|
+
manifestCache.delete(stateRoot);
|
|
284
|
+
} else if (!validateRunManifestPaths(cwd, runId, cached.manifest, stateRoot, tasksPath)) {
|
|
263
285
|
manifestCache.delete(stateRoot);
|
|
264
286
|
return undefined;
|
|
287
|
+
} else {
|
|
288
|
+
return { manifest: cached.manifest, tasks: cached.tasks };
|
|
265
289
|
}
|
|
266
|
-
return { manifest: cached.manifest, tasks: cached.tasks };
|
|
267
290
|
}
|
|
268
291
|
|
|
269
292
|
const manifest = readJsonFile<TeamRunManifest>(manifestPath);
|
|
@@ -300,11 +323,15 @@ export async function loadRunManifestByIdAsync(cwd: string, runId: string): Prom
|
|
|
300
323
|
}
|
|
301
324
|
const tasksMtimeMs = tasksStat?.mtimeMs ?? 0;
|
|
302
325
|
if (cached && cached.manifestMtimeMs === manifestStat.mtimeMs && cached.manifestSize === manifestStat.size && cached.tasksMtimeMs === tasksMtimeMs && cached.tasksSize === (tasksStat?.size ?? 0)) {
|
|
303
|
-
|
|
326
|
+
// TTL eviction: expire stale entries even if mtime matches
|
|
327
|
+
if (cached.cachedAt && Date.now() - cached.cachedAt > MANIFEST_CACHE_TTL_MS) {
|
|
328
|
+
manifestCache.delete(stateRoot);
|
|
329
|
+
} else if (!validateRunManifestPaths(cwd, runId, cached.manifest, stateRoot, tasksPath)) {
|
|
304
330
|
manifestCache.delete(stateRoot);
|
|
305
331
|
return undefined;
|
|
332
|
+
} else {
|
|
333
|
+
return { manifest: cached.manifest, tasks: cached.tasks };
|
|
306
334
|
}
|
|
307
|
-
return { manifest: cached.manifest, tasks: cached.tasks };
|
|
308
335
|
}
|
|
309
336
|
const manifest = await readJsonFileAsync<TeamRunManifest>(manifestPath);
|
|
310
337
|
if (!manifest || !validateRunManifestPaths(cwd, runId, manifest, stateRoot, tasksPath)) return undefined;
|
package/src/state/types.ts
CHANGED
|
@@ -39,6 +39,17 @@ export interface VerificationEvidence {
|
|
|
39
39
|
notes?: string;
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
+
export interface TaskOutputSchema {
|
|
43
|
+
/** Output format expected from the worker */
|
|
44
|
+
format: "json" | "markdown" | "text";
|
|
45
|
+
/** JTD or JSON Schema for validating JSON output (only when format="json") */
|
|
46
|
+
schema?: Record<string, unknown>;
|
|
47
|
+
/** Human-readable description of expected output */
|
|
48
|
+
description?: string;
|
|
49
|
+
/** Example of valid output (for prompt guidance) */
|
|
50
|
+
example?: string;
|
|
51
|
+
}
|
|
52
|
+
|
|
42
53
|
export interface TaskPacket {
|
|
43
54
|
objective: string;
|
|
44
55
|
scope: TaskScope;
|
|
@@ -53,10 +64,11 @@ export interface TaskPacket {
|
|
|
53
64
|
constraints: string[];
|
|
54
65
|
expectedArtifacts: string[];
|
|
55
66
|
verification: VerificationContract;
|
|
67
|
+
outputSchema?: TaskOutputSchema;
|
|
56
68
|
}
|
|
57
69
|
|
|
58
|
-
export type PolicyDecisionAction = "retry" | "reassign" | "escalate" | "block" | "notify" | "cleanup" | "closeout";
|
|
59
|
-
export type PolicyDecisionReason = "task_failed" | "worker_stale" | "green_unsatisfied" | "limit_exceeded" | "run_complete" | "mailbox_timeout" | "review_rejected" | "branch_stale" | "scope_mismatch";
|
|
70
|
+
export type PolicyDecisionAction = "retry" | "reassign" | "escalate" | "block" | "notify" | "cleanup" | "closeout" | "fail";
|
|
71
|
+
export type PolicyDecisionReason = "task_failed" | "worker_stale" | "green_unsatisfied" | "limit_exceeded" | "run_complete" | "mailbox_timeout" | "review_rejected" | "branch_stale" | "scope_mismatch" | "ineffective_worker";
|
|
60
72
|
|
|
61
73
|
export interface PolicyDecision {
|
|
62
74
|
action: PolicyDecisionAction;
|
|
@@ -81,6 +93,39 @@ export interface AsyncRunState {
|
|
|
81
93
|
spawnedAt: string;
|
|
82
94
|
}
|
|
83
95
|
|
|
96
|
+
export interface RuntimeResolutionState {
|
|
97
|
+
kind: "scaffold" | "child-process" | "live-session";
|
|
98
|
+
requestedMode: "auto" | "scaffold" | "child-process" | "live-session";
|
|
99
|
+
safety: "trusted" | "explicit_dry_run" | "blocked";
|
|
100
|
+
available: boolean;
|
|
101
|
+
fallback?: "scaffold" | "child-process" | "live-session";
|
|
102
|
+
reason?: string;
|
|
103
|
+
resolvedAt: string;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export interface WorkerExitStatus {
|
|
107
|
+
exitCode: number | null;
|
|
108
|
+
cancelled: boolean;
|
|
109
|
+
timedOut: boolean;
|
|
110
|
+
killed: boolean;
|
|
111
|
+
signal?: string;
|
|
112
|
+
cleanupErrors: string[];
|
|
113
|
+
finalDrainMs: number;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export interface OperationTerminalEvidence {
|
|
117
|
+
operation: "worker" | "tool" | "model";
|
|
118
|
+
status: "cancelled" | "failed" | "completed";
|
|
119
|
+
startedAt?: string;
|
|
120
|
+
finishedAt: string;
|
|
121
|
+
attemptId?: string;
|
|
122
|
+
reason?: {
|
|
123
|
+
code: string;
|
|
124
|
+
message: string;
|
|
125
|
+
};
|
|
126
|
+
exitStatus?: WorkerExitStatus;
|
|
127
|
+
}
|
|
128
|
+
|
|
84
129
|
export interface PlanApprovalState {
|
|
85
130
|
required: boolean;
|
|
86
131
|
status: "pending" | "approved" | "cancelled";
|
|
@@ -125,6 +170,12 @@ export interface TeamRunManifest {
|
|
|
125
170
|
planApproval?: PlanApprovalState;
|
|
126
171
|
/** Pi session that created the run, when available. Used to prevent cross-session destructive actions. */
|
|
127
172
|
ownerSessionId?: string;
|
|
173
|
+
/** pi-crew skill override selected when the run was created. false disables injected skill instructions. */
|
|
174
|
+
skillOverride?: string[] | false;
|
|
175
|
+
/** Resolved runtime/safety mode used for execution. Optional for backward compatibility with older manifests. */
|
|
176
|
+
runtimeResolution?: RuntimeResolutionState;
|
|
177
|
+
/** Effective run config snapshot used by async background workers. Optional for backward compatibility. */
|
|
178
|
+
runConfig?: unknown;
|
|
128
179
|
summary?: string;
|
|
129
180
|
policyDecisions?: PolicyDecision[];
|
|
130
181
|
}
|
|
@@ -166,6 +217,7 @@ export interface TaskCheckpointState {
|
|
|
166
217
|
}
|
|
167
218
|
|
|
168
219
|
export interface TaskAttemptState {
|
|
220
|
+
attemptId?: string;
|
|
169
221
|
startedAt: string;
|
|
170
222
|
endedAt?: string;
|
|
171
223
|
error?: string;
|
|
@@ -178,6 +230,7 @@ export interface TeamTaskState {
|
|
|
178
230
|
role: string;
|
|
179
231
|
agent: string;
|
|
180
232
|
title: string;
|
|
233
|
+
displayName?: string;
|
|
181
234
|
status: TeamTaskStatus;
|
|
182
235
|
dependsOn: string[];
|
|
183
236
|
cwd: string;
|
|
@@ -200,6 +253,8 @@ export interface TeamTaskState {
|
|
|
200
253
|
heartbeat?: WorkerHeartbeatState;
|
|
201
254
|
checkpoint?: TaskCheckpointState;
|
|
202
255
|
attempts?: TaskAttemptState[];
|
|
256
|
+
workerExitStatus?: WorkerExitStatus;
|
|
257
|
+
terminalEvidence?: OperationTerminalEvidence[];
|
|
203
258
|
taskPacket?: TaskPacket;
|
|
204
259
|
verification?: VerificationEvidence;
|
|
205
260
|
graph?: TaskGraphNode;
|
|
@@ -211,4 +266,11 @@ export interface TeamTaskState {
|
|
|
211
266
|
retryCount?: number;
|
|
212
267
|
lastDecision?: PolicyDecision;
|
|
213
268
|
};
|
|
269
|
+
controlReservation?: ControlReservation;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
export interface ControlReservation {
|
|
273
|
+
reservedAt: string;
|
|
274
|
+
controllerId: string;
|
|
275
|
+
acceptsControlEvents: boolean;
|
|
214
276
|
}
|
package/src/teams/team-config.ts
CHANGED
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Agent Management Overlay — displays discovered agents with their configuration.
|
|
3
|
+
* Read-only view of agent definitions from builtin/user/project sources.
|
|
4
|
+
* Future: enable/disable toggle, model override editing.
|
|
5
|
+
*/
|
|
6
|
+
import type { AgentConfig, ResourceSource } from "../agents/agent-config.ts";
|
|
7
|
+
import { truncate } from "../utils/visual.ts";
|
|
8
|
+
|
|
9
|
+
export interface AgentEntry {
|
|
10
|
+
name: string;
|
|
11
|
+
description: string;
|
|
12
|
+
source: ResourceSource;
|
|
13
|
+
model?: string;
|
|
14
|
+
thinking?: string;
|
|
15
|
+
loadMode?: string;
|
|
16
|
+
contextMode?: string;
|
|
17
|
+
disabled?: boolean;
|
|
18
|
+
filePath: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function agentToEntry(agent: AgentConfig): AgentEntry {
|
|
22
|
+
return {
|
|
23
|
+
name: agent.name,
|
|
24
|
+
description: agent.description,
|
|
25
|
+
source: agent.source,
|
|
26
|
+
model: agent.model,
|
|
27
|
+
thinking: agent.thinking,
|
|
28
|
+
loadMode: agent.loadMode,
|
|
29
|
+
contextMode: agent.contextMode,
|
|
30
|
+
disabled: agent.disabled,
|
|
31
|
+
filePath: agent.filePath,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function sourceIcon(source: ResourceSource): string {
|
|
36
|
+
switch (source) {
|
|
37
|
+
case "builtin": return "📦";
|
|
38
|
+
case "user": return "👤";
|
|
39
|
+
case "project": return "📂";
|
|
40
|
+
case "git": return "🔗";
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function sourceLabel(source: ResourceSource): string {
|
|
45
|
+
switch (source) {
|
|
46
|
+
case "builtin": return "builtin";
|
|
47
|
+
case "user": return "user";
|
|
48
|
+
case "project": return "project";
|
|
49
|
+
case "git": return "git";
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export interface AgentOverlayState {
|
|
54
|
+
entries: AgentEntry[];
|
|
55
|
+
selectedIndex: number;
|
|
56
|
+
scrollOffset: number;
|
|
57
|
+
expanded: Set<number>;
|
|
58
|
+
maxVisible: number;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function createAgentOverlayState(entries: AgentEntry[], maxVisible = 20): AgentOverlayState {
|
|
62
|
+
return {
|
|
63
|
+
entries: entries.sort((a, b) => {
|
|
64
|
+
const order: Record<ResourceSource, number> = { project: 0, user: 1, git: 2, builtin: 3 };
|
|
65
|
+
const diff = (order[a.source] ?? 4) - (order[b.source] ?? 4);
|
|
66
|
+
return diff !== 0 ? diff : a.name.localeCompare(b.name);
|
|
67
|
+
}),
|
|
68
|
+
selectedIndex: 0,
|
|
69
|
+
scrollOffset: 0,
|
|
70
|
+
expanded: new Set(),
|
|
71
|
+
maxVisible,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function moveSelection(state: AgentOverlayState, direction: -1 | 1): AgentOverlayState {
|
|
76
|
+
const next = Math.max(0, Math.min(state.entries.length - 1, state.selectedIndex + direction));
|
|
77
|
+
const visibleStart = state.scrollOffset;
|
|
78
|
+
const visibleEnd = state.scrollOffset + state.maxVisible;
|
|
79
|
+
const newScroll = next < visibleStart
|
|
80
|
+
? next
|
|
81
|
+
: next >= visibleEnd
|
|
82
|
+
? Math.max(0, next - state.maxVisible + 1)
|
|
83
|
+
: state.scrollOffset;
|
|
84
|
+
return { ...state, selectedIndex: next, scrollOffset: newScroll };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function toggleExpand(state: AgentOverlayState): AgentOverlayState {
|
|
88
|
+
const expanded = new Set(state.expanded);
|
|
89
|
+
if (expanded.has(state.selectedIndex)) {
|
|
90
|
+
expanded.delete(state.selectedIndex);
|
|
91
|
+
} else {
|
|
92
|
+
expanded.add(state.selectedIndex);
|
|
93
|
+
}
|
|
94
|
+
return { ...state, expanded };
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function renderAgentOverlay(state: AgentOverlayState, width: number): string[] {
|
|
98
|
+
const lines: string[] = [];
|
|
99
|
+
const header = ` Agent Configuration (${state.entries.length} agents)`;
|
|
100
|
+
lines.push(truncate(header, width));
|
|
101
|
+
lines.push(truncate("─".repeat(Math.min(width, 60)), width));
|
|
102
|
+
|
|
103
|
+
if (state.entries.length === 0) {
|
|
104
|
+
lines.push(truncate(" No agents discovered.", width));
|
|
105
|
+
return lines;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const visible = state.entries.slice(
|
|
109
|
+
state.scrollOffset,
|
|
110
|
+
state.scrollOffset + state.maxVisible,
|
|
111
|
+
);
|
|
112
|
+
|
|
113
|
+
for (const [i, entry] of visible.entries()) {
|
|
114
|
+
const globalIndex = state.scrollOffset + i;
|
|
115
|
+
const isSelected = globalIndex === state.selectedIndex;
|
|
116
|
+
const isExpanded = state.expanded.has(globalIndex);
|
|
117
|
+
const cursor = isSelected ? "▸" : " ";
|
|
118
|
+
const disabled = entry.disabled ? " [disabled]" : "";
|
|
119
|
+
const model = entry.model ? ` (${entry.model})` : "";
|
|
120
|
+
|
|
121
|
+
const summary = `${cursor} ${sourceIcon(entry.source)} ${entry.name}${model}${disabled}`;
|
|
122
|
+
lines.push(truncate(summary, width));
|
|
123
|
+
|
|
124
|
+
if (isExpanded) {
|
|
125
|
+
const desc = ` ${entry.description}`;
|
|
126
|
+
lines.push(truncate(desc, width));
|
|
127
|
+
const meta: string[] = [` source: ${sourceLabel(entry.source)}`];
|
|
128
|
+
if (entry.model) meta.push(`model: ${entry.model}`);
|
|
129
|
+
if (entry.thinking) meta.push(`thinking: ${entry.thinking}`);
|
|
130
|
+
if (entry.loadMode) meta.push(`loadMode: ${entry.loadMode}`);
|
|
131
|
+
if (entry.contextMode) meta.push(`context: ${entry.contextMode}`);
|
|
132
|
+
meta.push(`file: ${entry.filePath}`);
|
|
133
|
+
lines.push(truncate(meta.join(" · "), width));
|
|
134
|
+
lines.push(truncate("─".repeat(Math.min(width - 4, 50)), width));
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (state.scrollOffset + state.maxVisible < state.entries.length) {
|
|
139
|
+
const remaining = state.entries.length - state.scrollOffset - state.maxVisible;
|
|
140
|
+
lines.push(truncate(` … +${remaining} more`, width));
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return lines;
|
|
144
|
+
}
|
package/src/ui/crew-widget.ts
CHANGED
|
@@ -13,6 +13,8 @@ import { asCrewTheme, subscribeThemeChange } from "./theme-adapter.ts";
|
|
|
13
13
|
import { Box, Text } from "./layout-primitives.ts";
|
|
14
14
|
import { requestRender, setExtensionWidget } from "./pi-ui-compat.ts";
|
|
15
15
|
import type { RunSnapshotCache, RunUiSnapshot } from "./snapshot-types.ts";
|
|
16
|
+
import { runEventBus } from "./run-event-bus.ts";
|
|
17
|
+
import { DEFAULT_UI } from "../config/defaults.ts";
|
|
16
18
|
|
|
17
19
|
const SPINNER = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
|
|
18
20
|
const TOOL_LABELS: Record<string, string> = {
|
|
@@ -28,7 +30,7 @@ const LEGACY_WIDGET_KEY = "pi-crew";
|
|
|
28
30
|
const WIDGET_KEY = "pi-crew-active";
|
|
29
31
|
const STATUS_KEY = "pi-crew";
|
|
30
32
|
|
|
31
|
-
const MAX_LINES_DEFAULT =
|
|
33
|
+
const MAX_LINES_DEFAULT = DEFAULT_UI.widgetMaxLines;
|
|
32
34
|
const MAX_AGENTS_DISPLAY = 3;
|
|
33
35
|
|
|
34
36
|
type WidgetComponent = { render(width: number): string[]; invalidate(): void };
|
|
@@ -77,7 +79,11 @@ function agentActivity(agent: CrewAgentRecord): string {
|
|
|
77
79
|
if (recent) return recent.replace(/\s+/g, " ").trim();
|
|
78
80
|
if (agent.progress?.activityState === "needs_attention") return "needs attention";
|
|
79
81
|
if (agent.status === "queued") return "queued";
|
|
80
|
-
if (agent.status === "running")
|
|
82
|
+
if (agent.status === "running") {
|
|
83
|
+
const age = agent.startedAt ? Date.now() - new Date(agent.startedAt).getTime() : Infinity;
|
|
84
|
+
if (age < 5000 && !agent.progress?.currentTool) return "spawning…";
|
|
85
|
+
return "thinking…";
|
|
86
|
+
}
|
|
81
87
|
if (agent.status === "failed") return agent.error ?? "failed";
|
|
82
88
|
return "done";
|
|
83
89
|
}
|
|
@@ -218,6 +224,7 @@ class CrewWidgetComponent implements WidgetComponent {
|
|
|
218
224
|
private cachedBaseLines: string[] = [];
|
|
219
225
|
private cachedTheme: CrewTheme;
|
|
220
226
|
private readonly unsubscribeTheme: () => void;
|
|
227
|
+
private readonly unsubscribeEventBus: () => void;
|
|
221
228
|
|
|
222
229
|
constructor(model: CrewWidgetModel, themeLike: unknown) {
|
|
223
230
|
this.model = model;
|
|
@@ -225,6 +232,7 @@ class CrewWidgetComponent implements WidgetComponent {
|
|
|
225
232
|
this.cachedTheme = this.theme;
|
|
226
233
|
this.cacheSignature = "";
|
|
227
234
|
this.unsubscribeTheme = subscribeThemeChange(themeLike, () => this.invalidate());
|
|
235
|
+
this.unsubscribeEventBus = runEventBus.onAny(() => this.invalidate());
|
|
228
236
|
}
|
|
229
237
|
|
|
230
238
|
private buildSignature(runs: WidgetRun[]): string {
|
|
@@ -249,6 +257,7 @@ class CrewWidgetComponent implements WidgetComponent {
|
|
|
249
257
|
|
|
250
258
|
dispose(): void {
|
|
251
259
|
this.unsubscribeTheme();
|
|
260
|
+
this.unsubscribeEventBus();
|
|
252
261
|
}
|
|
253
262
|
|
|
254
263
|
render(width: number): string[] {
|
|
@@ -273,7 +282,8 @@ class CrewWidgetComponent implements WidgetComponent {
|
|
|
273
282
|
// Update only spinner and command icon on header line to avoid full re-color for every frame.
|
|
274
283
|
const updatedHeader = `${runningGlyph}${this.cachedBaseLines[0]?.slice(1) ?? ""}`;
|
|
275
284
|
this.cachedLines[0] = truncate(colorWidgetLine(updatedHeader, 0, this.theme), width);
|
|
276
|
-
|
|
285
|
+
// Safety: ensure all lines fit within terminal width (handles emoji/CJK width mismatch)
|
|
286
|
+
return this.cachedLines.map((line) => truncate(line, width));
|
|
277
287
|
}
|
|
278
288
|
}
|
|
279
289
|
|
|
@@ -290,7 +300,7 @@ export function updateCrewWidget(
|
|
|
290
300
|
const maxLines = config?.widgetMaxLines ?? MAX_LINES_DEFAULT;
|
|
291
301
|
const runs = activeWidgetRuns(ctx.cwd, manifestCache, snapshotCache, preloadedManifests);
|
|
292
302
|
const lines = buildCrewWidgetLines(ctx.cwd, state.frame, maxLines, runs, state.notificationCount ?? 0);
|
|
293
|
-
const placement = config?.widgetPlacement ??
|
|
303
|
+
const placement = config?.widgetPlacement ?? DEFAULT_UI.widgetPlacement;
|
|
294
304
|
ctx.ui.setStatus(STATUS_KEY, lines.length ? statusSummary(runs) : undefined);
|
|
295
305
|
const shouldClearLegacy = state.legacyCleared !== true || state.lastPlacement !== placement;
|
|
296
306
|
if (shouldClearLegacy) {
|
|
@@ -341,7 +351,7 @@ export function stopCrewWidget(ctx: Pick<ExtensionContext, "hasUI" | "ui"> | und
|
|
|
341
351
|
if (state.interval) clearInterval(state.interval);
|
|
342
352
|
state.interval = undefined;
|
|
343
353
|
if (ctx?.hasUI) {
|
|
344
|
-
const placement = config?.widgetPlacement ??
|
|
354
|
+
const placement = config?.widgetPlacement ?? DEFAULT_UI.widgetPlacement;
|
|
345
355
|
ctx.ui.setStatus(STATUS_KEY, undefined);
|
|
346
356
|
setExtensionWidget(ctx, LEGACY_WIDGET_KEY, undefined, { placement });
|
|
347
357
|
setExtensionWidget(ctx, WIDGET_KEY, undefined, { placement });
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type { TeamRunManifest, TeamTaskState } from "../../state/types.ts";
|
|
2
|
+
|
|
3
|
+
export interface CancellationPaneOptions {
|
|
4
|
+
maxReasons?: number;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function renderCancellationPane(manifest: TeamRunManifest, tasks: TeamTaskState[], opts: CancellationPaneOptions = {}): string[] {
|
|
8
|
+
const maxReasons = opts.maxReasons ?? 5;
|
|
9
|
+
if (manifest.status !== "cancelled" && manifest.status !== "blocked") {
|
|
10
|
+
const cancellingTasks = tasks.filter((t) => t.status === "cancelled");
|
|
11
|
+
if (cancellingTasks.length === 0) return ["Cancellation pane: no active cancellations"];
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const lines: string[] = ["Cancellation pane"];
|
|
15
|
+
|
|
16
|
+
if (manifest.status === "cancelled") {
|
|
17
|
+
lines.push(` Run status: cancelled`);
|
|
18
|
+
} else if (manifest.status === "blocked") {
|
|
19
|
+
lines.push(` Run status: blocked`);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const cancelledTasks = tasks.filter((t) => t.status === "cancelled");
|
|
23
|
+
if (cancelledTasks.length > 0) {
|
|
24
|
+
lines.push(` Cancelled tasks (${cancelledTasks.length}):`);
|
|
25
|
+
for (const task of cancelledTasks.slice(0, maxReasons)) {
|
|
26
|
+
const reason = task.error ?? "unknown";
|
|
27
|
+
lines.push(` ✗ ${task.id}: ${reason}`);
|
|
28
|
+
}
|
|
29
|
+
if (cancelledTasks.length > maxReasons) {
|
|
30
|
+
lines.push(` ... and ${cancelledTasks.length - maxReasons} more`);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (manifest.policyDecisions?.length) {
|
|
35
|
+
const decisions = manifest.policyDecisions.slice(0, maxReasons);
|
|
36
|
+
lines.push(` Policy decisions (${manifest.policyDecisions.length}):`);
|
|
37
|
+
for (const d of decisions) {
|
|
38
|
+
lines.push(` ${d.action}: ${d.message}`);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return lines;
|
|
43
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { buildCapabilityInventory } from "../../runtime/capability-inventory.ts";
|
|
2
|
+
import type { PiTeamsConfig } from "../../config/config.ts";
|
|
3
|
+
import type { CapabilityItem } from "../../runtime/capability-inventory.ts";
|
|
4
|
+
|
|
5
|
+
export interface CapabilityPaneOptions {
|
|
6
|
+
config?: PiTeamsConfig;
|
|
7
|
+
filter?: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function kindIcon(kind: string): string {
|
|
11
|
+
switch (kind) {
|
|
12
|
+
case "team": return "👥";
|
|
13
|
+
case "workflow": return "📋";
|
|
14
|
+
case "agent": return "🤖";
|
|
15
|
+
case "skill": return "🔧";
|
|
16
|
+
case "tool": return "🛠";
|
|
17
|
+
case "runtime": return "⚙";
|
|
18
|
+
default: return "•";
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function stateLabel(state: string): string {
|
|
23
|
+
switch (state) {
|
|
24
|
+
case "active": return "";
|
|
25
|
+
case "disabled": return " [DISABLED]";
|
|
26
|
+
case "shadowed": return " [SHADOWED]";
|
|
27
|
+
case "missing": return " [MISSING]";
|
|
28
|
+
default: return "";
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function renderCapabilityPane(cwd: string, opts: CapabilityPaneOptions = {}): string[] {
|
|
33
|
+
const inventory = buildCapabilityInventory(cwd, opts.config);
|
|
34
|
+
const filtered = opts.filter
|
|
35
|
+
? inventory.filter((item) => item.kind.includes(opts.filter!.toLowerCase()) || item.name.toLowerCase().includes(opts.filter!.toLowerCase()) || item.id.toLowerCase().includes(opts.filter!.toLowerCase()))
|
|
36
|
+
: inventory;
|
|
37
|
+
|
|
38
|
+
if (filtered.length === 0) return ["Capability pane: no items found"];
|
|
39
|
+
|
|
40
|
+
const byKind = new Map<string, CapabilityItem[]>();
|
|
41
|
+
for (const item of filtered) {
|
|
42
|
+
const group = byKind.get(item.kind) ?? [];
|
|
43
|
+
group.push(item);
|
|
44
|
+
byKind.set(item.kind, group);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const lines = [`Capability pane: ${filtered.length} item(s) (filter: ${opts.filter ?? "none"})`];
|
|
48
|
+
for (const [kind, items] of byKind) {
|
|
49
|
+
lines.push(` ${kindIcon(kind)} ${kind} (${items.length}):`);
|
|
50
|
+
for (const item of items.slice(0, 10)) {
|
|
51
|
+
const icon = item.state === "active" ? "✓" : "✗";
|
|
52
|
+
lines.push(` ${icon} ${item.name}${stateLabel(item.state)} [${item.source}]`);
|
|
53
|
+
}
|
|
54
|
+
if (items.length > 10) lines.push(` ... and ${items.length - 10} more`);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const disabled = filtered.filter((i) => i.state === "disabled").length;
|
|
58
|
+
if (disabled > 0) lines.push(` Disabled: ${disabled}`);
|
|
59
|
+
return lines;
|
|
60
|
+
}
|
|
@@ -1,11 +1,35 @@
|
|
|
1
|
-
import type { RunUiSnapshot } from "../snapshot-types.ts";
|
|
2
|
-
|
|
3
|
-
export function renderMailboxPane(snapshot: RunUiSnapshot | undefined): string[] {
|
|
4
|
-
if (!snapshot) return ["Mailbox pane: snapshot unavailable"];
|
|
5
|
-
const mailbox = snapshot.mailbox;
|
|
6
|
-
const approx = mailbox.approximate ? " · approximate (tail)" : "";
|
|
7
|
-
|
|
8
|
-
`Mailbox pane: inbox unread=${mailbox.inboxUnread} · outbox pending=${mailbox.outboxPending} · attention=${mailbox.needsAttention}${approx}`,
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
1
|
+
import type { RunUiSnapshot } from "../snapshot-types.ts";
|
|
2
|
+
|
|
3
|
+
export function renderMailboxPane(snapshot: RunUiSnapshot | undefined): string[] {
|
|
4
|
+
if (!snapshot) return ["Mailbox pane: snapshot unavailable"];
|
|
5
|
+
const mailbox = snapshot.mailbox;
|
|
6
|
+
const approx = mailbox.approximate ? " · approximate (tail)" : "";
|
|
7
|
+
const lines: string[] = [
|
|
8
|
+
`Mailbox pane: inbox unread=${mailbox.inboxUnread} · outbox pending=${mailbox.outboxPending} · attention=${mailbox.needsAttention}${approx}`,
|
|
9
|
+
];
|
|
10
|
+
// Kind-separated breakdown
|
|
11
|
+
const kindParts: string[] = [];
|
|
12
|
+
const steer = mailbox.steerUnread ?? 0;
|
|
13
|
+
const followUp = mailbox.followUpUnread ?? 0;
|
|
14
|
+
const response = mailbox.responseUnread ?? 0;
|
|
15
|
+
const message = mailbox.messageUnread ?? 0;
|
|
16
|
+
if (steer > 0) kindParts.push(`steer=${steer}`);
|
|
17
|
+
if (followUp > 0) kindParts.push(`follow-up=${followUp}`);
|
|
18
|
+
if (response > 0) kindParts.push(`response=${response}`);
|
|
19
|
+
if (message > 0) kindParts.push(`message=${message}`);
|
|
20
|
+
if (kindParts.length > 0) {
|
|
21
|
+
lines.push(` Breakdown: ${kindParts.join(" · ")}`);
|
|
22
|
+
if (steer > 0) {
|
|
23
|
+
lines.push(" ⚠ Urgent: steering messages require immediate attention.");
|
|
24
|
+
}
|
|
25
|
+
if (followUp > 0) {
|
|
26
|
+
lines.push(` 📋 ${followUp} follow-up(s) pending review.`);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
if (mailbox.needsAttention > 0) {
|
|
30
|
+
lines.push("Needs attention: press Enter for detail · A ack · N nudge · C compose · X ack all.");
|
|
31
|
+
} else {
|
|
32
|
+
lines.push("No mailbox items need attention. Press Enter for detail or C compose.");
|
|
33
|
+
}
|
|
34
|
+
return lines;
|
|
35
|
+
}
|
|
@@ -5,8 +5,10 @@ export function renderProgressPane(snapshot: RunUiSnapshot | undefined): string[
|
|
|
5
5
|
const progress = snapshot.progress;
|
|
6
6
|
const groupJoins = snapshot.groupJoins ?? [];
|
|
7
7
|
const groupJoinLines = groupJoins.length ? groupJoins.map((item) => `group join ${item.partial ? "partial" : "completed"}: ${item.requestId} ack=${item.ack}`) : ["group joins: none"];
|
|
8
|
+
const cancellationLine = snapshot.cancellationReason ? [`cancelled: reason=${snapshot.cancellationReason}`] : [];
|
|
8
9
|
return [
|
|
9
10
|
`Progress pane: ${progress.completed}/${progress.total} completed · running=${progress.running} queued=${progress.queued} failed=${progress.failed}`,
|
|
11
|
+
...cancellationLine,
|
|
10
12
|
...groupJoinLines,
|
|
11
13
|
...snapshot.recentEvents.slice(-10).map((event) => {
|
|
12
14
|
const seq = event.metadata?.seq !== undefined ? `#${event.metadata.seq}` : "#?";
|
|
@@ -14,6 +14,7 @@ import { asCrewTheme, subscribeThemeChange } from "./theme-adapter.ts";
|
|
|
14
14
|
import { Box, Text } from "./layout-primitives.ts";
|
|
15
15
|
import type { RunSnapshotCache, RunUiSnapshot } from "./snapshot-types.ts";
|
|
16
16
|
import { spinnerBucket, spinnerFrame } from "./spinner.ts";
|
|
17
|
+
import { runEventBus } from "./run-event-bus.ts";
|
|
17
18
|
|
|
18
19
|
const TASK_READ_TTL_MS = 200;
|
|
19
20
|
|
|
@@ -59,6 +60,7 @@ export class LiveRunSidebar {
|
|
|
59
60
|
private readonly theme: CrewTheme;
|
|
60
61
|
private readonly config: CrewUiConfig;
|
|
61
62
|
private readonly unsubscribeTheme: () => void;
|
|
63
|
+
private readonly unsubscribeEventBus: () => void;
|
|
62
64
|
private readonly snapshotCache?: RunSnapshotCache;
|
|
63
65
|
private cachedLines: string[] = [];
|
|
64
66
|
private cachedWidth = 0;
|
|
@@ -72,6 +74,7 @@ export class LiveRunSidebar {
|
|
|
72
74
|
this.config = input.config ?? {};
|
|
73
75
|
this.snapshotCache = input.snapshotCache;
|
|
74
76
|
this.unsubscribeTheme = subscribeThemeChange(input.theme, () => this.invalidate());
|
|
77
|
+
this.unsubscribeEventBus = runEventBus.onAny(() => this.invalidate());
|
|
75
78
|
}
|
|
76
79
|
|
|
77
80
|
private buildSignature(manifestStatus: string, tasks: TeamTaskState[], agents: ReturnType<typeof readCrewAgents>, waitingCount: number, snapshot?: RunUiSnapshot): string {
|
|
@@ -99,6 +102,7 @@ export class LiveRunSidebar {
|
|
|
99
102
|
|
|
100
103
|
dispose(): void {
|
|
101
104
|
this.unsubscribeTheme();
|
|
105
|
+
this.unsubscribeEventBus();
|
|
102
106
|
}
|
|
103
107
|
|
|
104
108
|
render(width: number): string[] {
|