pi-crew 0.9.41 → 0.9.44
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 +253 -0
- package/README.md +14 -0
- package/dist/build-meta.json +6450 -12419
- package/dist/index.mjs +57998 -68417
- package/dist/index.mjs.map +4 -4
- package/docs/migration/atomic-write-v2-migration.md +2 -0
- package/docs/superpowers/plans/2026-06-09-fallow-patterns-adoption.md +2 -0
- package/package.json +4 -2
- package/scripts/README.md +6 -0
- package/scripts/build-bundle.mjs +2 -1
- package/scripts/check-all-skills.ts +3 -3
- package/scripts/dev-runner.mjs +106 -0
- package/scripts/postinstall.mjs +6 -2
- package/scripts/verify-skill.ts +2 -2
- package/src/agents/agent-config.ts +1 -1
- package/src/config/config.ts +20 -1
- package/src/config/drift-detector.ts +97 -15
- package/src/config/resilient-parser.ts +4 -4
- package/src/config/types.ts +0 -2
- package/src/extension/crew-cleanup.ts +17 -3
- package/src/extension/crew-vibes/config.ts +1 -1
- package/src/extension/crew-vibes/font-detect.ts +16 -3
- package/src/extension/crew-vibes/index.ts +1 -1
- package/src/extension/cross-extension-rpc.ts +26 -9
- package/src/extension/management.ts +6 -5
- package/src/extension/register.ts +58 -1573
- package/src/extension/registration/command-registration.ts +57 -0
- package/src/extension/registration/commands.ts +7 -2
- package/src/extension/registration/context-builder.ts +131 -0
- package/src/extension/registration/crash-recovery-cache.ts +54 -0
- package/src/extension/registration/foreground-run-controller.ts +238 -0
- package/src/extension/registration/hook-registration.ts +119 -0
- package/src/extension/registration/lazy-configurers.ts +124 -0
- package/src/extension/registration/lifecycle-handlers.ts +731 -0
- package/src/extension/registration/observability.ts +33 -4
- package/src/extension/registration/registration-types.ts +149 -0
- package/src/extension/registration/runtime-cleanup.ts +230 -0
- package/src/extension/registration/subagent-manager-setup.ts +211 -0
- package/src/extension/registration/tool-registration.ts +50 -0
- package/src/extension/registration/wire-cross-extension.ts +37 -0
- package/src/extension/rpc-hmac.ts +21 -1
- package/src/extension/run-import.ts +3 -3
- package/src/extension/team-tool/auto-summarize.ts +4 -4
- package/src/extension/team-tool/cancel.ts +5 -5
- package/src/extension/team-tool/chain-executor.ts +5 -4
- package/src/extension/team-tool/context.ts +11 -1
- package/src/extension/team-tool/doctor.ts +2 -1
- package/src/extension/team-tool/goal-wrap.ts +2 -2
- package/src/extension/team-tool/goal.ts +1 -1
- package/src/extension/team-tool/handle-schedule.ts +25 -36
- package/src/extension/team-tool/orchestrate.ts +0 -1
- package/src/extension/team-tool/parallel-dispatch.ts +5 -0
- package/src/extension/team-tool/run.ts +220 -214
- package/src/extension/team-tool/workflow-manage.ts +1 -1
- package/src/extension/team-tool.ts +69 -16
- package/src/hooks/registry.ts +7 -2
- package/src/observability/correlation.ts +4 -3
- package/src/observability/event-bus.ts +10 -1
- package/src/observability/event-to-metric.ts +8 -7
- package/src/observability/exporters/adapter.ts +6 -9
- package/src/observability/exporters/otlp-exporter.ts +81 -0
- package/src/observability/metric-retention.ts +4 -0
- package/src/observability/metric-sink.ts +14 -4
- package/src/prompt/prompt-runtime.ts +15 -0
- package/src/runtime/adaptive-plan.ts +1 -1
- package/src/runtime/async-runner.ts +13 -4
- package/src/runtime/background-runner.ts +32 -23
- package/src/runtime/capability-inventory.ts +1 -1
- package/src/runtime/chain-runner.ts +5 -5
- package/src/runtime/checkpoint.ts +6 -16
- package/src/runtime/child-pi-constants.ts +42 -0
- package/src/runtime/child-pi-kill.ts +180 -0
- package/src/runtime/child-pi-spawn.ts +234 -0
- package/src/runtime/child-pi-steering.ts +128 -0
- package/src/runtime/child-pi-streams.ts +296 -0
- package/src/runtime/child-pi-transcript.ts +169 -0
- package/src/runtime/child-pi.ts +80 -822
- package/src/runtime/coalesce-tasks.ts +1 -1
- package/src/runtime/compact-stages/tail-capture-stage.ts +1 -1
- package/src/runtime/crash-recovery.ts +63 -47
- package/src/runtime/delivery-coordinator.ts +1 -1
- package/src/runtime/deterministic-ast.ts +2 -2
- package/src/runtime/diagnostic-export.ts +2 -1
- package/src/runtime/dwf-state-store.ts +5 -0
- package/src/runtime/dynamic-workflow-context.ts +78 -18
- package/src/runtime/dynamic-workflow-runner.ts +18 -2
- package/src/runtime/foreground-control.ts +3 -2
- package/src/runtime/goal-evaluator.ts +59 -27
- package/src/runtime/goal-loop-runner.ts +46 -13
- package/src/runtime/goal-state-store.ts +1 -1
- package/src/runtime/handoff-manager.ts +0 -2
- package/src/runtime/intercom-bridge.ts +0 -2
- package/src/runtime/live-agent-manager.ts +30 -5
- package/src/runtime/live-extension-bridge.ts +0 -2
- package/src/runtime/live-session-runtime.ts +65 -57
- package/src/runtime/manifest-cache.ts +59 -0
- package/src/runtime/mcp-proxy.ts +1 -1
- package/src/runtime/orphan-worker-registry.ts +3 -3
- package/src/runtime/output-validator.ts +1 -1
- package/src/runtime/path-overlap.ts +1 -1
- package/src/runtime/pi-args.ts +3 -3
- package/src/runtime/pi-json-output.ts +4 -3
- package/src/runtime/pi-spawn.ts +7 -1
- package/src/runtime/pipeline-runner.ts +1 -4
- package/src/runtime/post-checks.ts +0 -1
- package/src/runtime/retry-runner.ts +1 -1
- package/src/runtime/run-coalesced-task-group.ts +18 -23
- package/src/runtime/scheduler.ts +2 -0
- package/src/runtime/session-resources.ts +1 -1
- package/src/runtime/settings-store.ts +31 -1
- package/src/runtime/skill-effectiveness.ts +2 -2
- package/src/runtime/stale-reconciler.ts +23 -53
- package/src/runtime/subagent-manager.ts +19 -3
- package/src/runtime/task-output-context.ts +2 -1
- package/src/runtime/task-runner/prompt-builder.ts +3 -3
- package/src/runtime/task-runner/run-projection.ts +3 -1
- package/src/runtime/task-runner/state-helpers.ts +13 -1
- package/src/runtime/task-runner.ts +31 -6
- package/src/runtime/team-runner.ts +8 -2
- package/src/runtime/verification-gates.ts +54 -5
- package/src/schema/config-schema.ts +19 -1
- package/src/schema/team-tool-schema.ts +2 -0
- package/src/skills/discover-skills.ts +25 -15
- package/src/state/active-run-registry.ts +0 -1
- package/src/state/atomic-write.ts +37 -11
- package/src/state/blob-store.ts +3 -9
- package/src/state/contracts.ts +5 -1
- package/src/state/crew-init.ts +3 -2
- package/src/state/decision-ledger.ts +2 -2
- package/src/state/event-log-rotation.ts +1 -1
- package/src/state/event-log.ts +4 -3
- package/src/state/gitignore-manager.ts +2 -1
- package/src/state/health-store.ts +5 -2
- package/src/state/instinct-store.ts +11 -2
- package/src/state/locks.ts +132 -49
- package/src/state/mailbox.ts +3 -4
- package/src/state/observation-store.ts +1 -13
- package/src/state/run-cache.ts +12 -12
- package/src/state/run-graph.ts +2 -1
- package/src/state/run-metrics.ts +1 -1
- package/src/state/schedule.ts +7 -1
- package/src/state/state-store.ts +9 -3
- package/src/state/tiered-eval.ts +1 -1
- package/src/state/worker-atomic-writer.ts +1 -0
- package/src/ui/dynamic-border.ts +13 -3
- package/src/ui/live-conversation-overlay.ts +7 -27
- package/src/ui/live-duration.ts +2 -2
- package/src/ui/live-run-sidebar.ts +56 -32
- package/src/ui/mascot.ts +19 -13
- package/src/ui/powerbar-publisher.ts +244 -178
- package/src/ui/render-coalescer.ts +44 -3
- package/src/ui/render-diff.ts +2 -2
- package/src/ui/run-dashboard.ts +122 -21
- package/src/ui/run-event-bus.ts +66 -0
- package/src/ui/run-snapshot-cache.ts +84 -81
- package/src/ui/settings-overlay.ts +3 -3
- package/src/ui/status-colors.ts +24 -0
- package/src/ui/tool-progress-formatter.ts +1 -1
- package/src/ui/tool-render.ts +3 -3
- package/src/ui/transcript-cache.ts +5 -10
- package/src/ui/transcript-entries.ts +68 -72
- package/src/ui/transcript-viewer.ts +54 -14
- package/src/ui/widget/index.ts +26 -15
- package/src/ui/widget/widget-model.ts +18 -9
- package/src/ui/widget/widget-renderer.ts +2 -5
- package/src/utils/fingerprint.ts +1 -1
- package/src/utils/paths.ts +1 -1
- package/src/utils/project-detector.ts +1 -1
- package/src/worktree/worktree-manager.ts +47 -19
- package/src/state/atomic-write-v2.ts +0 -108
|
@@ -557,8 +557,8 @@ class TextinputSubmenu {
|
|
|
557
557
|
this.buffer = this.buffer.slice(0, -1);
|
|
558
558
|
return;
|
|
559
559
|
}
|
|
560
|
-
// Printable character
|
|
561
|
-
if (data.length === 1 && data >= " "
|
|
560
|
+
// Printable character (accept any non-control character, including Unicode)
|
|
561
|
+
if (data.length === 1 && data >= " ") {
|
|
562
562
|
this.buffer += data;
|
|
563
563
|
return;
|
|
564
564
|
}
|
|
@@ -704,7 +704,7 @@ class AgentOverridesSubmenu {
|
|
|
704
704
|
this.editBuffer = this.editBuffer.slice(0, -1);
|
|
705
705
|
return;
|
|
706
706
|
}
|
|
707
|
-
if (data.length === 1 && data >= " "
|
|
707
|
+
if (data.length === 1 && data >= " ") {
|
|
708
708
|
this.editBuffer += data;
|
|
709
709
|
}
|
|
710
710
|
}
|
package/src/ui/status-colors.ts
CHANGED
|
@@ -106,6 +106,11 @@ function glyphColor(glyph: string): CrewThemeColor {
|
|
|
106
106
|
}
|
|
107
107
|
}
|
|
108
108
|
|
|
109
|
+
/**
|
|
110
|
+
* Status glyph characters used in the fast-path check.
|
|
111
|
+
*/
|
|
112
|
+
const STATUS_GLYPH_CHARS = "\u2713\u2717\u25a0\u23f8\u25e6\u00b7\u25b6\u23f3\u26a0";
|
|
113
|
+
|
|
109
114
|
/**
|
|
110
115
|
* Colorize status glyphs embedded in a rendered line. Wraps every status glyph
|
|
111
116
|
* (✓ ✗ ■ ⏸ ⏳ ⚠ ◦ · ▶ and braille spinner frames ⠁–⣿) in the appropriate theme
|
|
@@ -115,5 +120,24 @@ function glyphColor(glyph: string): CrewThemeColor {
|
|
|
115
120
|
* and live-run-sidebar.ts with a single shared helper (F-1, F-2, V-3).
|
|
116
121
|
*/
|
|
117
122
|
export function colorizeStatusGlyphs(line: string, theme: CrewTheme): string {
|
|
123
|
+
// Fast path: skip regex if line contains no status glyphs at all.
|
|
124
|
+
let hasGlyph = false;
|
|
125
|
+
for (let i = 0; i < STATUS_GLYPH_CHARS.length; i++) {
|
|
126
|
+
if (line.indexOf(STATUS_GLYPH_CHARS[i]) !== -1) {
|
|
127
|
+
hasGlyph = true;
|
|
128
|
+
break;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
if (!hasGlyph) {
|
|
132
|
+
// Check for braille range (U+2800–U+28FF)
|
|
133
|
+
for (let i = 0; i < line.length; i++) {
|
|
134
|
+
const cp = line.codePointAt(i)!;
|
|
135
|
+
if (cp >= 0x2800 && cp <= 0x28ff) {
|
|
136
|
+
hasGlyph = true;
|
|
137
|
+
break;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
if (!hasGlyph) return line;
|
|
118
142
|
return line.replace(/[✓✗■⏸◦·▶⏳⚠]|[\u2800-\u28FF]/g, (glyph) => theme.fg(glyphColor(glyph), glyph));
|
|
119
143
|
}
|
|
@@ -81,7 +81,7 @@ export function formatCompactToolProgress(input: ToolProgressInput): string {
|
|
|
81
81
|
lines.push(` tool: ${active.progress.currentTool}${count}`);
|
|
82
82
|
}
|
|
83
83
|
const recent = active.progress?.recentOutput?.at(-1);
|
|
84
|
-
if (recent
|
|
84
|
+
if (recent?.trim()) lines.push(` ${trimLine(recent)}`);
|
|
85
85
|
} else if (input.runId && !counts) {
|
|
86
86
|
lines.push(` run=${input.runId} (starting)`);
|
|
87
87
|
} else if (input.error) {
|
package/src/ui/tool-render.ts
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* Ports logic from pi-subagent4 adapted for pi-crew's data model.
|
|
8
8
|
* Uses @earendil-works/pi-tui Components (Container, Text, Spacer) directly.
|
|
9
9
|
*/
|
|
10
|
-
import { Container, Spacer, Text
|
|
10
|
+
import { Container, Spacer, Text } from "@earendil-works/pi-tui";
|
|
11
11
|
import type { CrewAgentRecord } from "../runtime/crew-agent-runtime.ts";
|
|
12
12
|
import { truncateToWidth } from "../utils/visual.ts";
|
|
13
13
|
import { replaceTabs } from "./render-diff.ts";
|
|
@@ -444,10 +444,10 @@ function parseArgs(argsStr: string | undefined): Record<string, unknown> {
|
|
|
444
444
|
function computeDurationMs(startedAt: string, completedAt?: string): number {
|
|
445
445
|
if (!startedAt) return 0;
|
|
446
446
|
const start = new Date(startedAt).getTime();
|
|
447
|
-
if (isNaN(start)) return 0;
|
|
447
|
+
if (Number.isNaN(start)) return 0;
|
|
448
448
|
if (completedAt) {
|
|
449
449
|
const end = new Date(completedAt).getTime();
|
|
450
|
-
return isNaN(end) ? 0 : Math.max(0, end - start);
|
|
450
|
+
return Number.isNaN(end) ? 0 : Math.max(0, end - start);
|
|
451
451
|
}
|
|
452
452
|
return Math.max(0, Date.now() - start);
|
|
453
453
|
}
|
|
@@ -112,17 +112,12 @@ export function readTranscriptLinesCached(
|
|
|
112
112
|
truncated: read.truncated,
|
|
113
113
|
};
|
|
114
114
|
transcriptCache.set(key, entry);
|
|
115
|
-
//
|
|
115
|
+
// FIND-12: evict the oldest entry by Map insertion order. Map natively
|
|
116
|
+
// preserves insertion order, so the first key is the oldest — replaces
|
|
117
|
+
// the previous O(N) `parsedAt` min-scan that ran on every miss.
|
|
116
118
|
if (transcriptCache.size > MAX_CACHE_SIZE) {
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
for (const [k, v] of transcriptCache.entries()) {
|
|
120
|
-
if (v.parsedAt < oldestParsedAt) {
|
|
121
|
-
oldestParsedAt = v.parsedAt;
|
|
122
|
-
oldestKey = k;
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
if (oldestKey) transcriptCache.delete(oldestKey);
|
|
119
|
+
const oldestKey = transcriptCache.keys().next().value;
|
|
120
|
+
if (oldestKey !== undefined) transcriptCache.delete(oldestKey);
|
|
126
121
|
}
|
|
127
122
|
return lines;
|
|
128
123
|
} catch {
|
|
@@ -97,71 +97,61 @@ export function parseTranscriptEntries(lines: string[]): TranscriptEntry[] {
|
|
|
97
97
|
}
|
|
98
98
|
}
|
|
99
99
|
|
|
100
|
-
|
|
101
|
-
|
|
100
|
+
// FIND-18: single linear pass to match each tool_call with the next
|
|
101
|
+
// tool-flavored event after it. Replaces the previous O(n²) lookahead that
|
|
102
|
+
// re-scanned the suffix of the array for every tool_call. The pending slot
|
|
103
|
+
// holds the most recent tool_call awaiting its result. A new tool_call
|
|
104
|
+
// arriving before any result flushes the previous one as no-result (same
|
|
105
|
+
// semantic as the old lookahead hitting the next tool_call). Non-tool
|
|
106
|
+
// events are emitted in order and do not interrupt the pending slot,
|
|
107
|
+
// matching the old lookahead's behaviour of skipping over messages.
|
|
108
|
+
let pending: { obj: Record<string, unknown>; toolName: string; inputText: string; timestamp?: number } | null = null;
|
|
109
|
+
|
|
110
|
+
const flushPending = (isError: boolean, resultText: string, resultTimestamp: number | undefined): void => {
|
|
111
|
+
if (!pending) return;
|
|
112
|
+
const callSummary = summarize(
|
|
113
|
+
`🔧 ${pending.toolName}${pending.inputText ? `: ${summarize(pending.inputText, 60)}` : ""}`,
|
|
114
|
+
SUMMARY_MAX,
|
|
115
|
+
);
|
|
116
|
+
const callContent = [
|
|
117
|
+
`[Tool Call: ${pending.toolName}]`,
|
|
118
|
+
pending.inputText || "(no input)",
|
|
119
|
+
`[Result${isError ? " ✗" : " ✓"}]`,
|
|
120
|
+
resultText || "(no output)",
|
|
121
|
+
].join("\n");
|
|
122
|
+
entries.push({
|
|
123
|
+
id: id++,
|
|
124
|
+
type: "tool_call",
|
|
125
|
+
toolName: pending.toolName,
|
|
126
|
+
summary: callSummary,
|
|
127
|
+
content: callContent,
|
|
128
|
+
expanded: false,
|
|
129
|
+
timestamp: resultTimestamp ?? pending.timestamp,
|
|
130
|
+
});
|
|
131
|
+
pending = null;
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
for (let i = 0; i < parsed.length; i++) {
|
|
102
135
|
const { obj } = parsed[i]!;
|
|
103
136
|
const type = typeof obj.type === "string" ? obj.type : "";
|
|
104
137
|
const timestamp = typeof obj.timestamp === "number" ? obj.timestamp : undefined;
|
|
105
138
|
|
|
106
139
|
if (isToolCallType(type)) {
|
|
107
|
-
//
|
|
108
|
-
|
|
140
|
+
// A new tool_call interrupts any pending one (same as the old
|
|
141
|
+
// lookahead breaking on the next tool_call). Flush as no-result.
|
|
142
|
+
if (pending) flushPending(false, "", undefined);
|
|
109
143
|
const inputText = typeof obj.input === "string" ? obj.input : obj.input !== undefined ? JSON.stringify(obj.input) : "";
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
while (j < parsed.length) {
|
|
117
|
-
const nextTypeVal = parsed[j]!.obj.type;
|
|
118
|
-
const nextType = typeof nextTypeVal === "string" ? nextTypeVal : "";
|
|
119
|
-
if (isToolResultType(nextType) || /tool/i.test(nextType)) {
|
|
120
|
-
const robj = parsed[j]!.obj;
|
|
121
|
-
const rt =
|
|
122
|
-
typeof robj.text === "string"
|
|
123
|
-
? robj.text
|
|
124
|
-
: typeof robj.result === "string"
|
|
125
|
-
? robj.result
|
|
126
|
-
: robj.result !== undefined
|
|
127
|
-
? JSON.stringify(robj.result)
|
|
128
|
-
: "";
|
|
129
|
-
resultText = rt;
|
|
130
|
-
isError = robj.isError === true;
|
|
131
|
-
resultTimestamp = typeof robj.timestamp === "number" ? robj.timestamp : resultTimestamp;
|
|
132
|
-
j++;
|
|
133
|
-
break;
|
|
134
|
-
}
|
|
135
|
-
if (isToolCallType(nextType)) break; // next tool call, stop looking
|
|
136
|
-
j++;
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
const callSummary = summarize(`🔧 ${toolName}${inputText ? ": " + summarize(inputText, 60) : ""}`, SUMMARY_MAX);
|
|
140
|
-
const callContent = [
|
|
141
|
-
`[Tool Call: ${toolName}]`,
|
|
142
|
-
inputText || "(no input)",
|
|
143
|
-
`[Result${isError ? " ✗" : " ✓"}]`,
|
|
144
|
-
resultText || "(no output)",
|
|
145
|
-
].join("\n");
|
|
146
|
-
|
|
147
|
-
entries.push({
|
|
148
|
-
id: id++,
|
|
149
|
-
type: "tool_call",
|
|
150
|
-
toolName,
|
|
151
|
-
summary: callSummary,
|
|
152
|
-
content: callContent,
|
|
153
|
-
expanded: false,
|
|
154
|
-
timestamp: resultTimestamp ?? timestamp,
|
|
155
|
-
});
|
|
156
|
-
i = j;
|
|
144
|
+
pending = {
|
|
145
|
+
obj,
|
|
146
|
+
toolName: extractToolName(obj) ?? "unknown",
|
|
147
|
+
inputText,
|
|
148
|
+
timestamp,
|
|
149
|
+
};
|
|
157
150
|
continue;
|
|
158
151
|
}
|
|
159
152
|
|
|
160
153
|
if (isToolResultType(type) || /tool/i.test(type)) {
|
|
161
|
-
|
|
162
|
-
const toolName = extractToolName(obj) ?? "unknown";
|
|
163
|
-
const isError = obj.isError === true;
|
|
164
|
-
const text =
|
|
154
|
+
const rt =
|
|
165
155
|
typeof obj.text === "string"
|
|
166
156
|
? obj.text
|
|
167
157
|
: typeof obj.result === "string"
|
|
@@ -169,20 +159,25 @@ export function parseTranscriptEntries(lines: string[]): TranscriptEntry[] {
|
|
|
169
159
|
: obj.result !== undefined
|
|
170
160
|
? JSON.stringify(obj.result)
|
|
171
161
|
: "";
|
|
172
|
-
|
|
173
|
-
const
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
toolName,
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
162
|
+
const isError = obj.isError === true;
|
|
163
|
+
const resultTimestamp = typeof obj.timestamp === "number" ? obj.timestamp : undefined;
|
|
164
|
+
if (pending) {
|
|
165
|
+
flushPending(isError, rt, resultTimestamp);
|
|
166
|
+
} else {
|
|
167
|
+
// Standalone tool result (no preceding tool_call consumed it)
|
|
168
|
+
const toolName = extractToolName(obj) ?? "unknown";
|
|
169
|
+
const summary = summarize(`${isError ? "✗" : "✓"} ${toolName}${rt ? `: ${summarize(rt, 60)}` : ""}`, SUMMARY_MAX);
|
|
170
|
+
const content = [`[Tool Result: ${toolName}${isError ? " (error)" : ""}]`, rt || "(no output)"].join("\n");
|
|
171
|
+
entries.push({
|
|
172
|
+
id: id++,
|
|
173
|
+
type: "tool_result",
|
|
174
|
+
toolName,
|
|
175
|
+
summary,
|
|
176
|
+
content,
|
|
177
|
+
expanded: false,
|
|
178
|
+
timestamp,
|
|
179
|
+
});
|
|
180
|
+
}
|
|
186
181
|
continue;
|
|
187
182
|
}
|
|
188
183
|
|
|
@@ -194,7 +189,7 @@ export function parseTranscriptEntries(lines: string[]): TranscriptEntry[] {
|
|
|
194
189
|
const text = textFromContent(msg.content);
|
|
195
190
|
|
|
196
191
|
const label = role === "assistant" ? "🤖" : role === "user" ? "👤" : "💬";
|
|
197
|
-
const summary = summarize(`${label} ${role}${text ?
|
|
192
|
+
const summary = summarize(`${label} ${role}${text ? `: ${summarize(text, 80)}` : ""}`, SUMMARY_MAX);
|
|
198
193
|
const content = `[${role.charAt(0).toUpperCase()}${role.slice(1)}]:\n${text || "(empty)"}`;
|
|
199
194
|
|
|
200
195
|
entries.push({
|
|
@@ -206,7 +201,6 @@ export function parseTranscriptEntries(lines: string[]): TranscriptEntry[] {
|
|
|
206
201
|
expanded: false,
|
|
207
202
|
timestamp,
|
|
208
203
|
});
|
|
209
|
-
i++;
|
|
210
204
|
continue;
|
|
211
205
|
}
|
|
212
206
|
|
|
@@ -221,9 +215,11 @@ export function parseTranscriptEntries(lines: string[]): TranscriptEntry[] {
|
|
|
221
215
|
expanded: false,
|
|
222
216
|
timestamp,
|
|
223
217
|
});
|
|
224
|
-
i++;
|
|
225
218
|
}
|
|
226
219
|
|
|
220
|
+
// End of input: any pending tool_call never received a result.
|
|
221
|
+
if (pending) flushPending(false, "", undefined);
|
|
222
|
+
|
|
227
223
|
return entries;
|
|
228
224
|
}
|
|
229
225
|
|
|
@@ -18,6 +18,38 @@ type Component = {
|
|
|
18
18
|
|
|
19
19
|
type TranscriptTheme = CrewTheme;
|
|
20
20
|
|
|
21
|
+
interface ReadRunTranscriptResult {
|
|
22
|
+
title: string;
|
|
23
|
+
path: string;
|
|
24
|
+
lines: string[];
|
|
25
|
+
bytesRead: number;
|
|
26
|
+
size: number;
|
|
27
|
+
truncated: boolean;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// FIND-06: short-TTL cache keyed by (runId, taskId, readOptions) so the ~500ms
|
|
31
|
+
// render tick of `DurableTranscriptViewer` does not re-stat / re-read / re-parse
|
|
32
|
+
// the underlying transcript on every call. The deeper `transcriptCache` only
|
|
33
|
+
// reuses parsed lines when mtime/size are stable; during an actively-written
|
|
34
|
+
// run, the file's mtime changes on every tick and the inner cache is
|
|
35
|
+
// invalidated. This outer cache caps the per-tick re-parse cost at one parse
|
|
36
|
+
// per ~500ms even while the file is growing.
|
|
37
|
+
const READ_RUN_TRANSCRIPT_TTL_MS = 500;
|
|
38
|
+
const READ_RUN_TRANSCRIPT_CACHE_MAX = 32;
|
|
39
|
+
interface ReadRunTranscriptCacheEntry {
|
|
40
|
+
parsedAt: number;
|
|
41
|
+
result: ReadRunTranscriptResult;
|
|
42
|
+
}
|
|
43
|
+
const readRunTranscriptCache = new Map<string, ReadRunTranscriptCacheEntry>();
|
|
44
|
+
|
|
45
|
+
function readRunTranscriptCacheKey(
|
|
46
|
+
manifest: TeamRunManifest,
|
|
47
|
+
taskId: string | undefined,
|
|
48
|
+
readOptions: { full: boolean; maxTailBytes: number },
|
|
49
|
+
): string {
|
|
50
|
+
return `${manifest.runId}|${taskId ?? ""}|${readOptions.full ? "full" : "tail"}|${readOptions.maxTailBytes}`;
|
|
51
|
+
}
|
|
52
|
+
|
|
21
53
|
function asRecord(value: unknown): Record<string, unknown> | undefined {
|
|
22
54
|
return value && typeof value === "object" && !Array.isArray(value) ? (value as Record<string, unknown>) : undefined;
|
|
23
55
|
}
|
|
@@ -140,14 +172,17 @@ export function readRunTranscript(
|
|
|
140
172
|
manifest: TeamRunManifest,
|
|
141
173
|
taskId?: string,
|
|
142
174
|
options: { full?: boolean; maxTailBytes?: number } = {},
|
|
143
|
-
): {
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
175
|
+
): ReadRunTranscriptResult {
|
|
176
|
+
const now = Date.now();
|
|
177
|
+
const readOptions = {
|
|
178
|
+
full: options.full === true,
|
|
179
|
+
maxTailBytes: options.maxTailBytes ?? DEFAULT_TRANSCRIPT_TAIL_BYTES,
|
|
180
|
+
};
|
|
181
|
+
const cacheKey = readRunTranscriptCacheKey(manifest, taskId, readOptions);
|
|
182
|
+
const cached = readRunTranscriptCache.get(cacheKey);
|
|
183
|
+
if (cached && now - cached.parsedAt < READ_RUN_TRANSCRIPT_TTL_MS) {
|
|
184
|
+
return cached.result;
|
|
185
|
+
}
|
|
151
186
|
const agents = readCrewAgents(manifest);
|
|
152
187
|
const agent = taskId
|
|
153
188
|
? agents.find((item) => item.taskId === taskId || item.id === taskId)
|
|
@@ -172,13 +207,9 @@ export function readRunTranscript(
|
|
|
172
207
|
// Ignore untrusted transcript paths from mutable agent state and fall back to durable agent output.
|
|
173
208
|
}
|
|
174
209
|
}
|
|
175
|
-
const
|
|
176
|
-
full: options.full === true,
|
|
177
|
-
maxTailBytes: options.maxTailBytes ?? DEFAULT_TRANSCRIPT_TAIL_BYTES,
|
|
178
|
-
};
|
|
179
|
-
const lines = readTranscriptLinesCached(transcriptPath, (text) => formatTranscriptText(text), Date.now(), readOptions);
|
|
210
|
+
const lines = readTranscriptLinesCached(transcriptPath, (text) => formatTranscriptText(text), now, readOptions);
|
|
180
211
|
const entry = getTranscriptCacheEntry(transcriptPath, readOptions);
|
|
181
|
-
|
|
212
|
+
const result: ReadRunTranscriptResult = {
|
|
182
213
|
title: `${manifest.runId}:${selectedTaskId}`,
|
|
183
214
|
path: transcriptPath,
|
|
184
215
|
lines: lines.length ? lines : ["(no transcript content)"],
|
|
@@ -186,6 +217,15 @@ export function readRunTranscript(
|
|
|
186
217
|
size: entry?.size ?? 0,
|
|
187
218
|
truncated: entry?.truncated ?? false,
|
|
188
219
|
};
|
|
220
|
+
// Map preserves insertion order; the first key is the oldest entry. Evict
|
|
221
|
+
// before insert so the cache stays at most READ_RUN_TRANSCRIPT_CACHE_MAX.
|
|
222
|
+
while (readRunTranscriptCache.size >= READ_RUN_TRANSCRIPT_CACHE_MAX) {
|
|
223
|
+
const oldest = readRunTranscriptCache.keys().next().value;
|
|
224
|
+
if (oldest === undefined) break;
|
|
225
|
+
readRunTranscriptCache.delete(oldest);
|
|
226
|
+
}
|
|
227
|
+
readRunTranscriptCache.set(cacheKey, { parsedAt: now, result });
|
|
228
|
+
return result;
|
|
189
229
|
}
|
|
190
230
|
|
|
191
231
|
interface ViewerState {
|
package/src/ui/widget/index.ts
CHANGED
|
@@ -12,13 +12,14 @@ import type { ManifestCache } from "../../runtime/manifest-cache.ts";
|
|
|
12
12
|
import type { TeamRunManifest } from "../../state/types.ts";
|
|
13
13
|
import { truncate } from "../../utils/visual.ts";
|
|
14
14
|
import { requestRender, requestRenderTarget, setExtensionWidget } from "../pi-ui-compat.ts";
|
|
15
|
-
import {
|
|
16
|
-
import
|
|
15
|
+
import { RenderScheduler } from "../render-scheduler.ts";
|
|
16
|
+
import { runEventBusAsRenderScheduler } from "../run-event-bus.ts";
|
|
17
|
+
import type { RunSnapshotCache } from "../snapshot-types.ts";
|
|
17
18
|
import { spinnerBucket, spinnerFrame } from "../spinner.ts";
|
|
18
19
|
import type { CrewTheme } from "../theme-adapter.ts";
|
|
19
20
|
import { asCrewTheme, subscribeThemeChange } from "../theme-adapter.ts";
|
|
20
21
|
import { activeWidgetRuns, statusSummary } from "./widget-model.ts";
|
|
21
|
-
import { buildWidgetLines, colorWidgetLine, DEFAULT_WIDGET_WIDTH, renderLines
|
|
22
|
+
import { buildWidgetLines, colorWidgetLine, DEFAULT_WIDGET_WIDTH, renderLines } from "./widget-renderer.ts";
|
|
22
23
|
import type { CrewWidgetModel, CrewWidgetState, WidgetRun } from "./widget-types.ts";
|
|
23
24
|
|
|
24
25
|
export { activeWidgetRuns, statusSummary } from "./widget-model.ts";
|
|
@@ -109,7 +110,7 @@ class CrewWidgetComponent implements WidgetComponent {
|
|
|
109
110
|
private cachedTheme: CrewTheme;
|
|
110
111
|
private readonly tui: unknown;
|
|
111
112
|
private readonly unsubscribeTheme: () => void;
|
|
112
|
-
private readonly
|
|
113
|
+
private readonly renderScheduler: RenderScheduler;
|
|
113
114
|
|
|
114
115
|
constructor(model: CrewWidgetModel, themeLike: unknown, tui?: unknown) {
|
|
115
116
|
this.model = model;
|
|
@@ -123,16 +124,21 @@ class CrewWidgetComponent implements WidgetComponent {
|
|
|
123
124
|
activeResizeTarget = this;
|
|
124
125
|
installResizeListener();
|
|
125
126
|
this.unsubscribeTheme = subscribeThemeChange(themeLike, () => this.invalidate());
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
127
|
+
// 1.10 (UI-P1-1): route run:state / worker:lifecycle / ui:invalidate
|
|
128
|
+
// through a RenderScheduler (debounce + fallback) instead of three
|
|
129
|
+
// direct runEventBus.onChannel subscriptions. With 3 overlays
|
|
130
|
+
// subscribing independently a single event triggered up to 9 callbacks
|
|
131
|
+
// and ~150 invalidates/sec under load. The scheduler collapses bursts
|
|
132
|
+
// into one debounced invalidate.
|
|
133
|
+
this.renderScheduler = new RenderScheduler(
|
|
134
|
+
runEventBusAsRenderScheduler(["run:state", "worker:lifecycle", "ui:invalidate"]),
|
|
135
|
+
() => this.invalidate(),
|
|
136
|
+
{
|
|
137
|
+
debounceMs: 75,
|
|
138
|
+
fallbackMs: 750,
|
|
139
|
+
events: ["run:state", "worker:lifecycle", "ui:invalidate"],
|
|
140
|
+
},
|
|
141
|
+
);
|
|
136
142
|
}
|
|
137
143
|
|
|
138
144
|
private buildSignature(runs: WidgetRun[]): string {
|
|
@@ -193,7 +199,7 @@ class CrewWidgetComponent implements WidgetComponent {
|
|
|
193
199
|
|
|
194
200
|
dispose(): void {
|
|
195
201
|
this.unsubscribeTheme();
|
|
196
|
-
this.
|
|
202
|
+
this.renderScheduler.dispose();
|
|
197
203
|
if (activeResizeTarget === this) activeResizeTarget = undefined;
|
|
198
204
|
}
|
|
199
205
|
|
|
@@ -222,6 +228,11 @@ class CrewWidgetComponent implements WidgetComponent {
|
|
|
222
228
|
|
|
223
229
|
if (runs.length === 0) {
|
|
224
230
|
this.invalidate();
|
|
231
|
+
// P0-6: render from snapshots only — never read disk on every render tick.
|
|
232
|
+
// When the snapshot cache is provided but hasn't populated yet, paint a
|
|
233
|
+
// single "(loading…)" line so the pre-load frame is well-formed instead
|
|
234
|
+
// of an empty panel. Without a cache (legacy/tests) keep the empty result.
|
|
235
|
+
if (this.model.snapshotCache) return ["(loading…)"];
|
|
225
236
|
return [];
|
|
226
237
|
}
|
|
227
238
|
|
|
@@ -8,7 +8,7 @@ import { listRecentRuns } from "../../extension/run-index.ts";
|
|
|
8
8
|
import { reconcileAllStaleRuns } from "../../runtime/crash-recovery.ts";
|
|
9
9
|
import { readCrewAgents } from "../../runtime/crew-agent-records.ts";
|
|
10
10
|
import type { CrewAgentRecord } from "../../runtime/crew-agent-runtime.ts";
|
|
11
|
-
import { evictStaleLiveAgentHandles
|
|
11
|
+
import { evictStaleLiveAgentHandles } from "../../runtime/live-agent-manager.ts";
|
|
12
12
|
import type { ManifestCache } from "../../runtime/manifest-cache.ts";
|
|
13
13
|
import { isDisplayActiveRun } from "../../runtime/process-status.ts";
|
|
14
14
|
import type { TeamRunManifest } from "../../state/types.ts";
|
|
@@ -57,18 +57,27 @@ export function activeWidgetRuns(
|
|
|
57
57
|
.map((run) => {
|
|
58
58
|
try {
|
|
59
59
|
const snapshot = snapshotCache?.get(run.runId);
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
60
|
+
if (snapshot) {
|
|
61
|
+
return {
|
|
62
|
+
run: snapshot.manifest,
|
|
63
|
+
agents: snapshot.agents,
|
|
64
|
+
snapshot,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
// P0-6: render from snapshots only — never read disk on every
|
|
68
|
+
// render tick. When the snapshot cache is provided but hasn't
|
|
69
|
+
// populated yet (first frame after `updateCrewWidget`), drop the
|
|
70
|
+
// run from the result so the widget paints "(loading…)" instead
|
|
71
|
+
// of calling `agentsFor(run) → readCrewAgents`. Legacy callers
|
|
72
|
+
// (no snapshotCache) keep the disk-read fallback for tests/dev.
|
|
73
|
+
if (snapshotCache) return null;
|
|
74
|
+
return { run, agents: agentsFor(run) };
|
|
67
75
|
} catch {
|
|
76
|
+
if (snapshotCache) return null;
|
|
68
77
|
return { run, agents: agentsFor(run) };
|
|
69
78
|
}
|
|
70
79
|
})
|
|
71
|
-
.filter((item) => isDisplayActiveRun(item.run, item.agents));
|
|
80
|
+
.filter((item): item is WidgetRun => item !== null && isDisplayActiveRun(item.run, item.agents));
|
|
72
81
|
}
|
|
73
82
|
|
|
74
83
|
/**
|
|
@@ -5,16 +5,13 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import { listLiveAgents } from "../../runtime/live-agent-manager.ts";
|
|
8
|
-
import { computePhaseProgress, formatPhaseProgressLine } from "../../runtime/phase-progress.ts";
|
|
9
8
|
import { isFinishedRunStatus } from "../../runtime/process-status.ts";
|
|
10
|
-
import { getTaskUsage } from "../../runtime/usage-tracker.ts";
|
|
11
9
|
import { truncate } from "../../utils/visual.ts";
|
|
12
10
|
import { Box, Text } from "../layout-primitives.ts";
|
|
13
|
-
import { computeLiveDurationMs } from "../live-duration.ts";
|
|
14
11
|
import { spinnerFrame } from "../spinner.ts";
|
|
15
12
|
import { colorizeStatusGlyphs, iconForStatus } from "../status-colors.ts";
|
|
16
13
|
import type { CrewTheme } from "../theme-adapter.ts";
|
|
17
|
-
import { agentActivity, agentStats,
|
|
14
|
+
import { agentActivity, agentStats, notificationBadge } from "./widget-formatters.ts";
|
|
18
15
|
import { activeWidgetRuns, shortRunLabel } from "./widget-model.ts";
|
|
19
16
|
import type { WidgetRun } from "./widget-types.ts";
|
|
20
17
|
|
|
@@ -98,7 +95,7 @@ export function buildWidgetLines(
|
|
|
98
95
|
// reached its terminal status). The status label is also surfaced
|
|
99
96
|
// explicitly so the row cannot be misread as an active run.
|
|
100
97
|
const agentCountText = `${completed}/${agents.length} agents`;
|
|
101
|
-
const runEndMs = isTerminal ? new Date(run.updatedAt).getTime() :
|
|
98
|
+
const runEndMs = isTerminal ? new Date(run.updatedAt).getTime() : now;
|
|
102
99
|
const runElapsedMs = Math.max(0, Number.isFinite(runEndMs) ? runEndMs - new Date(run.createdAt).getTime() : 0);
|
|
103
100
|
const runElapsedText = `${Math.floor(runElapsedMs / 1000)}s`;
|
|
104
101
|
const statusLabel = isTerminal ? ` · ${run.status}` : "";
|
package/src/utils/fingerprint.ts
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* Only STRUCTURAL changes trigger re-analysis.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
import { createHash
|
|
10
|
+
import { createHash } from "node:crypto";
|
|
11
11
|
import { existsSync, readFileSync, statSync, writeFileSync } from "node:fs";
|
|
12
12
|
import { logInternalError } from "../utils/internal-error.ts";
|
|
13
13
|
|
package/src/utils/paths.ts
CHANGED
|
@@ -44,7 +44,7 @@ export function userPiRoot(): string {
|
|
|
44
44
|
// "undefined/.pi/agent" relative to cwd and silently create a junk "undefined/"
|
|
45
45
|
// tree. Treat the literal "undefined" (and empty) as unset and fall back to
|
|
46
46
|
// os.homedir().
|
|
47
|
-
const rawHome = process.env.PI_TEAMS_HOME?.trim();
|
|
47
|
+
const rawHome = (process.env.PI_TEAMS_HOME ?? process.env.PI_CREW_HOME)?.trim();
|
|
48
48
|
const home = rawHome && rawHome !== "undefined" ? rawHome : os.homedir();
|
|
49
49
|
const resolved = path.join(home, ".pi", "agent");
|
|
50
50
|
|
|
@@ -113,7 +113,7 @@ export function detectProjectId(cwd: string): ProjectInfo {
|
|
|
113
113
|
|
|
114
114
|
// Method 1: Check CLAUDE_PROJECT_DIR env var
|
|
115
115
|
const claudeProjectDir = process.env.CLAUDE_PROJECT_DIR;
|
|
116
|
-
if (claudeProjectDir
|
|
116
|
+
if (claudeProjectDir?.trim()) {
|
|
117
117
|
const resolvedPath = path.resolve(claudeProjectDir.trim());
|
|
118
118
|
const projectName = path.basename(resolvedPath);
|
|
119
119
|
const projectId = hashPath(resolvedPath);
|