pi-crew 0.9.36 → 0.9.38
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 +41 -0
- package/dist/build-meta.json +29 -19
- package/dist/index.mjs +90 -27
- package/dist/index.mjs.map +3 -3
- package/package.json +1 -1
- package/src/config/config.ts +2 -2
- package/src/extension/crew-vibes/figures.ts +13 -6
- package/src/extension/crew-vibes/index.ts +36 -13
- package/src/extension/team-tool/context.ts +61 -4
- package/src/extension/team-tool/handle-settings.ts +1 -1
- package/src/extension/team-tool/run.ts +1 -0
- package/src/ui/settings-overlay.ts +1 -1
- package/src/ui/tool-renderers/index.ts +20 -3
- package/src/ui/widget/widget-formatters.ts +10 -0
package/package.json
CHANGED
package/src/config/config.ts
CHANGED
|
@@ -694,7 +694,7 @@ function parseIsolationPolicy(value: unknown): CrewRuntimeConfig["isolationPolic
|
|
|
694
694
|
|
|
695
695
|
function parseRuntimeConfig(value: unknown): CrewRuntimeConfig | undefined {
|
|
696
696
|
const obj = asRecord(value);
|
|
697
|
-
if (!obj) return
|
|
697
|
+
if (!obj) return { inheritContext: true } as CrewRuntimeConfig;
|
|
698
698
|
const runtime: CrewRuntimeConfig = {
|
|
699
699
|
mode: parseWithSchema(
|
|
700
700
|
Type.Union([Type.Literal("auto"), Type.Literal("scaffold"), Type.Literal("child-process"), Type.Literal("live-session")]),
|
|
@@ -704,7 +704,7 @@ function parseRuntimeConfig(value: unknown): CrewRuntimeConfig | undefined {
|
|
|
704
704
|
allowChildProcessFallback: parseWithSchema(Type.Boolean(), obj.allowChildProcessFallback),
|
|
705
705
|
maxTurns: parsePositiveInteger(obj.maxTurns, LIMIT_CEILINGS.runtimeMaxTurns),
|
|
706
706
|
graceTurns: parsePositiveInteger(obj.graceTurns, LIMIT_CEILINGS.runtimeGraceTurns),
|
|
707
|
-
inheritContext: parseWithSchema(Type.Boolean(), obj.inheritContext),
|
|
707
|
+
inheritContext: parseWithSchema(Type.Boolean(), obj.inheritContext) ?? true,
|
|
708
708
|
promptMode: parseWithSchema(Type.Union([Type.Literal("replace"), Type.Literal("append")]), obj.promptMode),
|
|
709
709
|
groupJoin: parseWithSchema(Type.Union([Type.Literal("off"), Type.Literal("group"), Type.Literal("smart")]), obj.groupJoin),
|
|
710
710
|
groupJoinAckTimeoutMs: parsePositiveInteger(obj.groupJoinAckTimeoutMs, 86_400_000),
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { SpeedConfig } from "./config.ts";
|
|
2
|
-
import { isWebTerminal } from "./font-detect.ts";
|
|
2
|
+
import { hasCrewFontFile, isWebTerminal } from "./font-detect.ts";
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* Crew figures replacing the original cat glyphs from pi-speeed / pi-chonk.
|
|
@@ -57,12 +57,19 @@ const PUA_CREW_FRAMES: readonly string[] = [
|
|
|
57
57
|
/**
|
|
58
58
|
* Return the indicator frames for the given style.
|
|
59
59
|
*
|
|
60
|
-
*
|
|
61
|
-
*
|
|
60
|
+
* Default is `"braille"` — standard Unicode braille spinner (⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏)
|
|
61
|
+
* that renders correctly on every terminal, every platform, with zero
|
|
62
|
+
* font dependency. PUA runner-glyphs require crew-vibes.ttf installed AND
|
|
63
|
+
* a terminal that does font fallback for Private Use Area codepoints —
|
|
64
|
+
* Windows Terminal does NOT do this reliably, showing garbage chars even
|
|
65
|
+
* with the font installed.
|
|
66
|
+
*
|
|
67
|
+
* To opt into the PUA runner look, pass `style: "pua"` explicitly.
|
|
62
68
|
*/
|
|
63
|
-
export function crewFrames(style: "braille" | "pua" = "
|
|
64
|
-
//
|
|
65
|
-
|
|
69
|
+
export function crewFrames(style: "braille" | "pua" = "braille"): readonly string[] {
|
|
70
|
+
// Even when explicitly requested, fall back to braille if the font is
|
|
71
|
+
// missing or we're on a web terminal (which can't render PUA at all).
|
|
72
|
+
if (style === "pua" && (isWebTerminal() || !hasCrewFontFile())) return BRAILLE_FRAMES;
|
|
66
73
|
return style === "pua" ? PUA_CREW_FRAMES : BRAILLE_FRAMES;
|
|
67
74
|
}
|
|
68
75
|
|
|
@@ -1,5 +1,24 @@
|
|
|
1
1
|
import type { ExtensionAPI, ExtensionCommandContext, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import { requestRender, setFooter } from "../../ui/pi-ui-compat.ts";
|
|
3
|
+
import { logInternalError } from "../../utils/internal-error.ts";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Wrap a pi UI call in try/catch. If the call throws (e.g. terminal doesn't
|
|
7
|
+
* support PUA glyphs, theme helper returns undefined, or pi's API signature
|
|
8
|
+
* changed), log it and silently fall back to pi's default behavior — which
|
|
9
|
+
* is simply "do nothing", letting pi show its own built-in indicator.
|
|
10
|
+
*
|
|
11
|
+
* Crew-vibes must NEVER break the user's session. A broken spinner is not
|
|
12
|
+
* worth a crashed pi.
|
|
13
|
+
*/
|
|
14
|
+
function safeUiCall(scope: string, fn: () => void): void {
|
|
15
|
+
try {
|
|
16
|
+
fn();
|
|
17
|
+
} catch (error) {
|
|
18
|
+
logInternalError(`crew-vibes.${scope}`, error, undefined, "warn");
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
3
22
|
import { type CrewVibesConfig, loadConfig, PROVIDER_STATUS_ID, saveConfig } from "./config.ts";
|
|
4
23
|
import { intervalForSpeed } from "./figures.ts";
|
|
5
24
|
import { type CrewVibesFooterSource, createCrewVibesFooter } from "./footer.ts";
|
|
@@ -72,44 +91,48 @@ export function registerCrewVibes(pi: ExtensionAPI): void {
|
|
|
72
91
|
function installFooter(ctx: ExtensionContext): void {
|
|
73
92
|
if (!ctx?.hasUI) return;
|
|
74
93
|
if (!metersActive()) {
|
|
75
|
-
setFooter(ctx, undefined);
|
|
94
|
+
safeUiCall("clear-footer", () => setFooter(ctx, undefined));
|
|
76
95
|
return;
|
|
77
96
|
}
|
|
78
|
-
|
|
79
|
-
|
|
97
|
+
safeUiCall("install-footer", () => {
|
|
98
|
+
setFooter(ctx, (tui, theme, footerData) => createCrewVibesFooter({ tui, theme, footerData, ctx, source: footerSource }));
|
|
99
|
+
requestRender(ctx);
|
|
100
|
+
});
|
|
80
101
|
}
|
|
81
102
|
|
|
82
103
|
/** Trigger a footer repaint; the footer recomputes capacity/quota on render. */
|
|
83
104
|
function refreshFooter(ctx: ExtensionContext): void {
|
|
84
|
-
if (ctx?.hasUI) requestRender(ctx);
|
|
105
|
+
if (ctx?.hasUI) safeUiCall("refresh-footer", () => requestRender(ctx));
|
|
85
106
|
}
|
|
86
107
|
|
|
87
108
|
function publishSpeedFooter(ctx: ExtensionContext, speed = footerAnimator.value()): void {
|
|
88
109
|
if (!config.enabled || !config.speed.enabled || !config.speed.footer) {
|
|
89
|
-
setSpeedStatus(ctx, config, undefined);
|
|
110
|
+
safeUiCall("clear-speed-status", () => setSpeedStatus(ctx, config, undefined));
|
|
90
111
|
return;
|
|
91
112
|
}
|
|
92
|
-
setSpeedStatus(ctx, config, renderSpeedFooter(themeOf(ctx), config.speed, speed));
|
|
113
|
+
safeUiCall("publish-speed-status", () => setSpeedStatus(ctx, config, renderSpeedFooter(themeOf(ctx), config.speed, speed)));
|
|
93
114
|
}
|
|
94
115
|
|
|
95
116
|
function applyIndicator(ctx: ExtensionContext, speed: number | null, force = false): void {
|
|
96
117
|
if (!ctx.hasUI || !ctx.ui.setWorkingIndicator) return;
|
|
97
118
|
if (!config.enabled || !config.speed.enabled || !config.speed.indicator) {
|
|
98
|
-
ctx.ui.setWorkingIndicator();
|
|
119
|
+
safeUiCall("reset-indicator", () => ctx.ui.setWorkingIndicator?.());
|
|
99
120
|
return;
|
|
100
121
|
}
|
|
101
122
|
const next = intervalForSpeed(config.speed, speed);
|
|
102
123
|
if (!force && Math.abs(next - currentIntervalMs) < 10) return;
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
124
|
+
safeUiCall("set-indicator", () =>
|
|
125
|
+
ctx.ui.setWorkingIndicator?.({
|
|
126
|
+
frames: crewIndicatorFrames(themeOf(ctx)),
|
|
127
|
+
intervalMs: next,
|
|
128
|
+
}),
|
|
129
|
+
);
|
|
107
130
|
currentIntervalMs = next;
|
|
108
131
|
}
|
|
109
132
|
|
|
110
133
|
function renderWorking(ctx: ExtensionContext, speed: number | null): void {
|
|
111
134
|
if (!config.enabled || !config.speed.enabled || !ctx.hasUI) return;
|
|
112
|
-
ctx.ui.setWorkingMessage(renderWorkingMessage(themeOf(ctx), config.speed, speed));
|
|
135
|
+
safeUiCall("set-working-message", () => ctx.ui.setWorkingMessage?.(renderWorkingMessage(themeOf(ctx), config.speed, speed)));
|
|
113
136
|
}
|
|
114
137
|
|
|
115
138
|
function stopLiveTimer(): void {
|
|
@@ -315,7 +338,7 @@ export function registerCrewVibes(pi: ExtensionAPI): void {
|
|
|
315
338
|
stopLiveTimer();
|
|
316
339
|
if (ctx && config.enabled && ctx.hasUI) {
|
|
317
340
|
applyIndicator(ctx, speedTracker.lastTokS);
|
|
318
|
-
ctx.ui.setWorkingMessage();
|
|
341
|
+
safeUiCall("clear-working-message", () => ctx.ui.setWorkingMessage?.());
|
|
319
342
|
}
|
|
320
343
|
});
|
|
321
344
|
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
|
|
2
3
|
import type { PiTeamsConfig } from "../../config/config.ts";
|
|
3
4
|
import type { MetricRegistry } from "../../observability/metric-registry.ts";
|
|
4
5
|
import type { RunSnapshotCache } from "../../ui/run-snapshot-cache.ts";
|
|
@@ -47,6 +48,40 @@ function extractTextContent(content: unknown): string {
|
|
|
47
48
|
.join("\n");
|
|
48
49
|
}
|
|
49
50
|
|
|
51
|
+
/**
|
|
52
|
+
* Maximum character budget for inherited parent context (~3K tokens).
|
|
53
|
+
* When exceeded, oldest messages are dropped first (most-recent-first retention).
|
|
54
|
+
*/
|
|
55
|
+
export const MAX_PARENT_CONTEXT_CHARS = 12_000;
|
|
56
|
+
|
|
57
|
+
/** Maximum chars for a single assistant message before truncation. */
|
|
58
|
+
const MAX_ASSISTANT_MSG_CHARS = 500;
|
|
59
|
+
|
|
60
|
+
/** Truncated length for oversized assistant messages. */
|
|
61
|
+
const TRUNCATED_ASSISTANT_CHARS = 200;
|
|
62
|
+
|
|
63
|
+
/** Messages starting with these patterns and exceeding this size are likely
|
|
64
|
+
* file dumps / bash output — skip them to keep context relevant and compact. */
|
|
65
|
+
const NOISY_THRESHOLD = 1_000;
|
|
66
|
+
const NOISY_PREFIXES = ["```", "total ", "drwx", "-rw", "import ", "export "];
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Check if a message looks like noisy file/tool output (code dumps, ls output,
|
|
70
|
+
* import lists) that bloats context without helping the subagent.
|
|
71
|
+
*/
|
|
72
|
+
function isNoisyContent(text: string): boolean {
|
|
73
|
+
if (text.length < NOISY_THRESHOLD) return false;
|
|
74
|
+
return NOISY_PREFIXES.some((prefix) => text.startsWith(prefix));
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Build a compact parent conversation context for subagent inheritance.
|
|
79
|
+
*
|
|
80
|
+
* Extracts recent user messages, assistant reasoning, and compaction summaries
|
|
81
|
+
* from the parent session. Applies a character budget (most-recent-first
|
|
82
|
+
* retention) and filters noisy content (file dumps, long bash output) to keep
|
|
83
|
+
* the inherited context relevant without bloating the subagent's token budget.
|
|
84
|
+
*/
|
|
50
85
|
export function buildParentContext(ctx: TeamContext): string | undefined {
|
|
51
86
|
const branch = ctx.sessionManager?.getBranch?.();
|
|
52
87
|
if (!Array.isArray(branch) || branch.length === 0) return undefined;
|
|
@@ -58,21 +93,43 @@ export function buildParentContext(ctx: TeamContext): string | undefined {
|
|
|
58
93
|
message?: unknown;
|
|
59
94
|
summary?: unknown;
|
|
60
95
|
};
|
|
61
|
-
|
|
96
|
+
// Compaction summaries are always valuable — keep in full.
|
|
97
|
+
if (record.type === "compaction" && typeof record.summary === "string") {
|
|
98
|
+
parts.push(`[Summary]: ${record.summary}`);
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
62
101
|
const message =
|
|
63
102
|
record.message && typeof record.message === "object" && !Array.isArray(record.message)
|
|
64
103
|
? (record.message as { role?: unknown; content?: unknown })
|
|
65
104
|
: undefined;
|
|
66
105
|
if (!message || (message.role !== "user" && message.role !== "assistant")) continue;
|
|
67
|
-
|
|
68
|
-
if (text)
|
|
106
|
+
let text = extractTextContent(message.content).trim();
|
|
107
|
+
if (!text) continue;
|
|
108
|
+
// Filter: skip noisy content (file dumps, long bash output).
|
|
109
|
+
if (isNoisyContent(text)) continue;
|
|
110
|
+
// Truncate: long assistant messages get shortened to key points.
|
|
111
|
+
if (message.role === "assistant" && text.length > MAX_ASSISTANT_MSG_CHARS) {
|
|
112
|
+
text = `${text.slice(0, TRUNCATED_ASSISTANT_CHARS)}…`;
|
|
113
|
+
}
|
|
114
|
+
parts.push(`[${message.role === "user" ? "User" : "Assistant"}]: ${text}`);
|
|
69
115
|
}
|
|
70
116
|
if (!parts.length) return undefined;
|
|
117
|
+
|
|
118
|
+
// Budget: keep most-recent messages first, drop oldest when over budget.
|
|
119
|
+
let totalChars = 0;
|
|
120
|
+
const budgeted: string[] = [];
|
|
121
|
+
for (const part of [...parts].reverse()) {
|
|
122
|
+
if (totalChars + part.length > MAX_PARENT_CONTEXT_CHARS) break;
|
|
123
|
+
budgeted.unshift(part);
|
|
124
|
+
totalChars += part.length;
|
|
125
|
+
}
|
|
126
|
+
if (!budgeted.length) return undefined;
|
|
127
|
+
|
|
71
128
|
return [
|
|
72
129
|
`# Parent Conversation Context`,
|
|
73
130
|
"The following context was inherited from the parent Pi session. Treat it as reference-only.",
|
|
74
131
|
"",
|
|
75
|
-
|
|
132
|
+
budgeted.join("\n\n"),
|
|
76
133
|
].join("\n");
|
|
77
134
|
}
|
|
78
135
|
|
|
@@ -14,7 +14,7 @@ const EFFECTIVE_DEFAULTS: Record<string, unknown> = {
|
|
|
14
14
|
"runtime.mode": "auto",
|
|
15
15
|
"runtime.maxTurns": 10000,
|
|
16
16
|
"runtime.graceTurns": 5,
|
|
17
|
-
"runtime.inheritContext":
|
|
17
|
+
"runtime.inheritContext": true,
|
|
18
18
|
"runtime.promptMode": "replace",
|
|
19
19
|
"runtime.completionMutationGuard": "warn",
|
|
20
20
|
"runtime.isolationPolicy": undefined,
|
|
@@ -832,6 +832,7 @@ export async function handleRun(params: TeamToolParamsValue, ctx: TeamContext):
|
|
|
832
832
|
runtime,
|
|
833
833
|
runtimeConfig: executedConfig.runtime,
|
|
834
834
|
parentContext: buildParentContext(ctx),
|
|
835
|
+
|
|
835
836
|
parentModel: ctx.model,
|
|
836
837
|
modelRegistry: ctx.modelRegistry,
|
|
837
838
|
modelOverride: params.model,
|
|
@@ -327,7 +327,7 @@ const EFFECTIVE_DEFAULTS: Record<string, unknown> = {
|
|
|
327
327
|
"runtime.mode": "auto",
|
|
328
328
|
"runtime.maxTurns": 10000,
|
|
329
329
|
"runtime.graceTurns": 5,
|
|
330
|
-
"runtime.inheritContext":
|
|
330
|
+
"runtime.inheritContext": true,
|
|
331
331
|
"runtime.promptMode": "replace",
|
|
332
332
|
"runtime.completionMutationGuard": "warn",
|
|
333
333
|
"runtime.isolationPolicy": undefined,
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
import { type Container, Text, visibleWidth } from "@earendil-works/pi-tui";
|
|
10
10
|
import type { CrewAgentRecord } from "../../runtime/crew-agent-runtime.ts";
|
|
11
11
|
import { truncateToWidth } from "../../utils/visual.ts";
|
|
12
|
+
import { spinnerFrame } from "../spinner.ts";
|
|
12
13
|
import type { CrewTheme } from "../theme-adapter.ts";
|
|
13
14
|
import { formatDuration, formatTokens, truncLine } from "../tool-render.ts";
|
|
14
15
|
import { briefToolResult, isBrief } from "./brief-mode.ts";
|
|
@@ -373,9 +374,25 @@ function renderAgentResult(result: Record<string, unknown>, options: unknown, th
|
|
|
373
374
|
// isPartial = agent still running
|
|
374
375
|
const isPartial = (options as Record<string, unknown>)?.isPartial === true;
|
|
375
376
|
if (isPartial && !ctx.expanded) {
|
|
376
|
-
const spinner = theme.fg("
|
|
377
|
-
const
|
|
378
|
-
|
|
377
|
+
const spinner = theme.fg("accent", spinnerFrame(d.agentId as string));
|
|
378
|
+
const parts: string[] = [theme.fg("muted", "working")];
|
|
379
|
+
|
|
380
|
+
// Extract progress from onUpdate content (formatCompactToolProgress output)
|
|
381
|
+
const progressText = extractContentText(result?.content);
|
|
382
|
+
if (progressText) {
|
|
383
|
+
const elapsedMatch = progressText.match(/elapsed=(\d+)s/);
|
|
384
|
+
const tokenMatch = progressText.match(/tokens=(\d+)/);
|
|
385
|
+
const toolMatch = progressText.match(/tool:\s+(\S+)/);
|
|
386
|
+
const elapsedSec = elapsedMatch ? parseInt(elapsedMatch[1]!, 10) : 0;
|
|
387
|
+
const tokens = tokenMatch ? parseInt(tokenMatch[1]!, 10) : 0;
|
|
388
|
+
const tps = elapsedSec > 0 ? Math.round(tokens / elapsedSec) : 0;
|
|
389
|
+
|
|
390
|
+
if (elapsedSec > 0) parts.push(theme.fg("dim", formatDuration(elapsedSec * 1000)));
|
|
391
|
+
if (tps > 0) parts.push(theme.fg("dim", `${formatTokens(tps)} tok/s`));
|
|
392
|
+
if (toolMatch) parts.push(theme.fg("dim", `· ${toolMatch[1]}`));
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
contentLines.push(padVisual(` ${spinner} ${parts.join(" ")}`, innerW));
|
|
379
396
|
return buildFrameBottom(contentLines, w, theme, "borderAccent", "borderAccent");
|
|
380
397
|
}
|
|
381
398
|
|
|
@@ -18,6 +18,7 @@ import { computeLiveDurationMs } from "../live-duration.ts";
|
|
|
18
18
|
// verbatim (no truncation/crash) — these are rare one-off states, not jitter.
|
|
19
19
|
const TOOLS_METRIC_WIDTH = 8; // "127 tools"
|
|
20
20
|
const TOKENS_METRIC_WIDTH = 10; // "1.2k tok", "12.3M tok"
|
|
21
|
+
const TPS_METRIC_WIDTH = 9; // "411 tok/s"
|
|
21
22
|
const CTX_METRIC_WIDTH = 7; // "100% ctx"
|
|
22
23
|
const DURATION_METRIC_WIDTH = 6; // "120.0s"
|
|
23
24
|
|
|
@@ -139,10 +140,19 @@ export function agentStats(agent: CrewAgentRecord, liveHandle?: LiveAgentHandle)
|
|
|
139
140
|
/* ignore */
|
|
140
141
|
}
|
|
141
142
|
const ms = computeLiveDurationMs(act);
|
|
143
|
+
if (total > 0 && ms > 1000) {
|
|
144
|
+
const tps = Math.round(total / (ms / 1000));
|
|
145
|
+
if (tps > 0) parts.push(alignMetric(`${formatTokensCompact(tps)}/s`, TPS_METRIC_WIDTH));
|
|
146
|
+
}
|
|
142
147
|
parts.push(alignMetric(`${(ms / 1000).toFixed(1)}s`, DURATION_METRIC_WIDTH));
|
|
143
148
|
} else {
|
|
144
149
|
if (agent.toolUses) parts.push(alignMetric(`${agent.toolUses} tools`, TOOLS_METRIC_WIDTH));
|
|
145
150
|
if (agent.progress?.tokens) parts.push(alignMetric(formatTokensCompact(agent.progress.tokens), TOKENS_METRIC_WIDTH));
|
|
151
|
+
const ageMs = agent.startedAt ? Math.max(0, Date.now() - new Date(agent.startedAt).getTime()) : 0;
|
|
152
|
+
if (agent.progress?.tokens && ageMs > 1000) {
|
|
153
|
+
const tps = Math.round(agent.progress.tokens / (ageMs / 1000));
|
|
154
|
+
if (tps > 0) parts.push(alignMetric(`${formatTokensCompact(tps)}/s`, TPS_METRIC_WIDTH));
|
|
155
|
+
}
|
|
146
156
|
const age = elapsed(agent.completedAt ?? agent.startedAt);
|
|
147
157
|
if (age) parts.push(alignMetric(age, DURATION_METRIC_WIDTH));
|
|
148
158
|
}
|