pi-crew 0.1.37 → 0.1.39
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/AGENTS.md +1 -1
- package/CHANGELOG.md +27 -0
- package/README.md +5 -0
- 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/refactor-tasks-phase3.md +394 -394
- package/docs/refactor-tasks-phase4.md +564 -564
- package/docs/refactor-tasks-phase5.md +402 -402
- package/docs/refactor-tasks-phase6.md +662 -662
- package/docs/research-extension-examples.md +297 -297
- package/docs/research-extension-system.md +324 -324
- package/docs/research-optimization-plan.md +548 -548
- package/docs/research-pi-coding-agent.md +357 -357
- package/docs/research-source-pi-crew-reference.md +174 -174
- package/docs/resource-formats.md +10 -8
- package/docs/runtime-flow.md +148 -148
- package/docs/source-runtime-refactor-map.md +83 -83
- package/docs/usage.md +6 -0
- package/index.ts +6 -6
- package/package.json +3 -3
- package/schema.json +2 -2
- package/src/agents/agent-serializer.ts +34 -34
- package/src/config/config.ts +8 -4
- package/src/extension/cross-extension-rpc.ts +82 -82
- package/src/extension/import-index.ts +18 -2
- package/src/extension/register.ts +11 -1
- package/src/extension/registration/compaction-guard.ts +125 -125
- package/src/extension/registration/subagent-helpers.ts +30 -6
- package/src/extension/registration/subagent-tools.ts +8 -3
- package/src/extension/result-watcher.ts +98 -98
- package/src/extension/run-import.ts +12 -2
- package/src/extension/run-index.ts +12 -2
- package/src/extension/run-maintenance.ts +24 -24
- package/src/extension/team-tool/api.ts +54 -14
- package/src/extension/team-tool/cancel.ts +31 -31
- package/src/extension/team-tool/doctor.ts +179 -179
- package/src/extension/team-tool/inspect.ts +41 -41
- package/src/extension/team-tool/lifecycle-actions.ts +79 -79
- package/src/extension/team-tool/plan.ts +19 -19
- package/src/extension/team-tool/status.ts +73 -73
- package/src/observability/correlation.ts +35 -35
- package/src/observability/event-to-metric.ts +54 -54
- package/src/observability/exporters/adapter.ts +24 -24
- package/src/observability/exporters/otlp-exporter.ts +65 -65
- package/src/observability/exporters/prometheus-exporter.ts +47 -47
- package/src/observability/metric-registry.ts +72 -72
- package/src/observability/metric-retention.ts +46 -46
- package/src/observability/metric-sink.ts +51 -51
- package/src/observability/metrics-primitives.ts +166 -166
- package/src/prompt/prompt-runtime.ts +68 -68
- package/src/runtime/agent-control.ts +64 -64
- package/src/runtime/agent-memory.ts +72 -72
- package/src/runtime/agent-observability.ts +114 -113
- package/src/runtime/async-marker.ts +26 -26
- package/src/runtime/background-runner.ts +53 -53
- package/src/runtime/crash-recovery.ts +56 -56
- package/src/runtime/crew-agent-records.ts +54 -9
- package/src/runtime/crew-agent-runtime.ts +58 -58
- package/src/runtime/deadletter.ts +36 -36
- package/src/runtime/direct-run.ts +35 -35
- package/src/runtime/foreground-control.ts +82 -82
- package/src/runtime/green-contract.ts +46 -46
- package/src/runtime/group-join.ts +88 -88
- package/src/runtime/heartbeat-gradient.ts +28 -28
- package/src/runtime/heartbeat-watcher.ts +80 -80
- package/src/runtime/live-agent-control.ts +87 -78
- package/src/runtime/live-agent-manager.ts +85 -85
- package/src/runtime/live-control-realtime.ts +36 -36
- package/src/runtime/live-session-runtime.ts +299 -299
- package/src/runtime/manifest-cache.ts +248 -212
- package/src/runtime/model-fallback.ts +261 -261
- package/src/runtime/parallel-research.ts +44 -44
- package/src/runtime/parallel-utils.ts +99 -99
- package/src/runtime/pi-json-output.ts +111 -111
- package/src/runtime/policy-engine.ts +78 -78
- package/src/runtime/post-exit-stdio-guard.ts +86 -86
- package/src/runtime/process-status.ts +56 -56
- package/src/runtime/progress-event-coalescer.ts +43 -43
- package/src/runtime/recovery-recipes.ts +74 -74
- package/src/runtime/retry-executor.ts +59 -59
- package/src/runtime/role-permission.ts +39 -39
- package/src/runtime/session-usage.ts +79 -79
- package/src/runtime/sidechain-output.ts +28 -28
- package/src/runtime/subagent-manager.ts +80 -12
- package/src/runtime/task-display.ts +38 -38
- package/src/runtime/task-output-context.ts +127 -106
- package/src/runtime/task-runner/live-executor.ts +98 -98
- package/src/runtime/task-runner/progress.ts +111 -111
- package/src/runtime/task-runner/result-utils.ts +14 -14
- package/src/runtime/task-runner/state-helpers.ts +22 -22
- package/src/runtime/team-runner.ts +1 -1
- package/src/runtime/worker-heartbeat.ts +21 -21
- package/src/runtime/worker-startup.ts +57 -57
- package/src/schema/config-schema.ts +21 -21
- package/src/schema/team-tool-schema.ts +100 -100
- package/src/state/artifact-store.ts +122 -108
- package/src/state/contracts.ts +105 -105
- package/src/state/jsonl-writer.ts +77 -77
- package/src/state/mailbox.ts +67 -22
- package/src/state/state-store.ts +36 -5
- package/src/state/task-claims.ts +42 -42
- package/src/state/usage.ts +29 -29
- package/src/subagents/async-entry.ts +1 -1
- package/src/subagents/index.ts +3 -3
- package/src/subagents/live/control.ts +1 -1
- package/src/subagents/live/manager.ts +1 -1
- package/src/subagents/live/realtime.ts +1 -1
- package/src/subagents/live/session-runtime.ts +1 -1
- package/src/subagents/manager.ts +1 -1
- package/src/subagents/spawn.ts +1 -1
- package/src/teams/discover-teams.ts +27 -5
- package/src/teams/team-serializer.ts +38 -36
- package/src/types/diff.d.ts +18 -18
- package/src/ui/crew-footer.ts +101 -101
- package/src/ui/crew-select-list.ts +111 -111
- package/src/ui/dashboard-panes/metrics-pane.ts +34 -34
- package/src/ui/dynamic-border.ts +25 -25
- package/src/ui/layout-primitives.ts +106 -106
- package/src/ui/loaders.ts +158 -158
- package/src/ui/mascot.ts +441 -441
- package/src/ui/render-diff.ts +119 -119
- package/src/ui/run-dashboard.ts +5 -2
- package/src/ui/run-snapshot-cache.ts +19 -8
- package/src/ui/spinner.ts +17 -17
- package/src/ui/status-colors.ts +54 -54
- package/src/ui/syntax-highlight.ts +116 -116
- package/src/ui/transcript-viewer.ts +15 -1
- package/src/utils/completion-dedupe.ts +63 -63
- package/src/utils/file-coalescer.ts +84 -84
- package/src/utils/frontmatter.ts +36 -36
- package/src/utils/fs-watch.ts +31 -31
- package/src/utils/git.ts +262 -262
- package/src/utils/ids.ts +12 -12
- package/src/utils/names.ts +26 -26
- package/src/utils/paths.ts +3 -2
- package/src/utils/safe-paths.ts +34 -0
- package/src/utils/sleep.ts +32 -32
- package/src/utils/timings.ts +31 -31
- package/src/utils/visual.ts +159 -159
- package/src/workflows/discover-workflows.ts +30 -3
- package/src/workflows/validate-workflow.ts +40 -40
- package/src/worktree/branch-freshness.ts +45 -45
- package/teams/default.team.md +12 -12
- package/teams/fast-fix.team.md +11 -11
- package/teams/implementation.team.md +18 -18
- package/teams/parallel-research.team.md +14 -14
- package/teams/research.team.md +11 -11
- package/teams/review.team.md +12 -12
- package/workflows/default.workflow.md +29 -29
- package/workflows/fast-fix.workflow.md +22 -22
- package/workflows/implementation.workflow.md +38 -38
- package/workflows/parallel-research.workflow.md +46 -46
- package/workflows/research.workflow.md +22 -22
- package/workflows/review.workflow.md +30 -30
|
@@ -1,36 +1,38 @@
|
|
|
1
|
-
import type { TeamConfig, TeamRole } from "./team-config.ts";
|
|
2
|
-
|
|
3
|
-
function line(key: string, value: string | string[] | undefined): string | undefined {
|
|
4
|
-
if (value === undefined) return undefined;
|
|
5
|
-
if (Array.isArray(value)) return `${key}: ${value.join(", ")}`;
|
|
6
|
-
return `${key}: ${value}`;
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
function serializeRole(role: TeamRole): string {
|
|
10
|
-
const parts = [`agent=${role.agent}`];
|
|
11
|
-
if (role.model) parts.push(`model=${role.model}`);
|
|
12
|
-
if (role.
|
|
13
|
-
if (role.
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
team.
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
line("
|
|
28
|
-
line("
|
|
29
|
-
line("
|
|
30
|
-
"
|
|
31
|
-
"",
|
|
32
|
-
|
|
33
|
-
"",
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
1
|
+
import type { TeamConfig, TeamRole } from "./team-config.ts";
|
|
2
|
+
|
|
3
|
+
function line(key: string, value: string | string[] | undefined): string | undefined {
|
|
4
|
+
if (value === undefined) return undefined;
|
|
5
|
+
if (Array.isArray(value)) return `${key}: ${value.join(", ")}`;
|
|
6
|
+
return `${key}: ${value}`;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function serializeRole(role: TeamRole): string {
|
|
10
|
+
const parts = [`agent=${role.agent}`];
|
|
11
|
+
if (role.model) parts.push(`model=${role.model}`);
|
|
12
|
+
if (role.skills === false) parts.push("skills=false");
|
|
13
|
+
else if (role.skills?.length) parts.push(`skills=${role.skills.join(",")}`);
|
|
14
|
+
if (role.maxConcurrency !== undefined) parts.push(`maxConcurrency=${role.maxConcurrency}`);
|
|
15
|
+
if (role.description) parts.push(role.description);
|
|
16
|
+
return `- ${role.name}: ${parts.join(" ")}`;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function serializeTeam(team: TeamConfig): string {
|
|
20
|
+
const lines = [
|
|
21
|
+
"---",
|
|
22
|
+
`name: ${team.name}`,
|
|
23
|
+
`description: ${team.description}`,
|
|
24
|
+
team.defaultWorkflow ? `defaultWorkflow: ${team.defaultWorkflow}` : undefined,
|
|
25
|
+
team.workspaceMode ? `workspaceMode: ${team.workspaceMode}` : undefined,
|
|
26
|
+
team.maxConcurrency !== undefined ? `maxConcurrency: ${team.maxConcurrency}` : undefined,
|
|
27
|
+
line("triggers", team.routing?.triggers),
|
|
28
|
+
line("useWhen", team.routing?.useWhen),
|
|
29
|
+
line("avoidWhen", team.routing?.avoidWhen),
|
|
30
|
+
line("cost", team.routing?.cost),
|
|
31
|
+
line("category", team.routing?.category),
|
|
32
|
+
"---",
|
|
33
|
+
"",
|
|
34
|
+
...team.roles.map(serializeRole),
|
|
35
|
+
"",
|
|
36
|
+
].filter((entry): entry is string => entry !== undefined);
|
|
37
|
+
return lines.join("\n");
|
|
38
|
+
}
|
package/src/types/diff.d.ts
CHANGED
|
@@ -1,18 +1,18 @@
|
|
|
1
|
-
declare module "diff" {
|
|
2
|
-
export interface Change {
|
|
3
|
-
value: string;
|
|
4
|
-
count?: number;
|
|
5
|
-
added?: boolean;
|
|
6
|
-
removed?: boolean;
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
export interface DiffOptions {
|
|
10
|
-
ignoreCase?: boolean;
|
|
11
|
-
newlineIsToken?: boolean;
|
|
12
|
-
ignoreWhitespace?: boolean;
|
|
13
|
-
stripTrailingCr?: boolean;
|
|
14
|
-
oneChangePerToken?: boolean;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
export function diffWords(oldStr: string, newStr: string, options?: DiffOptions): Change[];
|
|
18
|
-
}
|
|
1
|
+
declare module "diff" {
|
|
2
|
+
export interface Change {
|
|
3
|
+
value: string;
|
|
4
|
+
count?: number;
|
|
5
|
+
added?: boolean;
|
|
6
|
+
removed?: boolean;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface DiffOptions {
|
|
10
|
+
ignoreCase?: boolean;
|
|
11
|
+
newlineIsToken?: boolean;
|
|
12
|
+
ignoreWhitespace?: boolean;
|
|
13
|
+
stripTrailingCr?: boolean;
|
|
14
|
+
oneChangePerToken?: boolean;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function diffWords(oldStr: string, newStr: string, options?: DiffOptions): Change[];
|
|
18
|
+
}
|
package/src/ui/crew-footer.ts
CHANGED
|
@@ -1,101 +1,101 @@
|
|
|
1
|
-
import type { UsageState } from "../state/types.ts";
|
|
2
|
-
import { pad, truncate } from "../utils/visual.ts";
|
|
3
|
-
import type { RunStatus } from "./status-colors.ts";
|
|
4
|
-
import type { CrewTheme } from "./theme-adapter.ts";
|
|
5
|
-
|
|
6
|
-
export interface CrewFooterData {
|
|
7
|
-
pwd: string;
|
|
8
|
-
branch?: string;
|
|
9
|
-
runId?: string;
|
|
10
|
-
status?: RunStatus;
|
|
11
|
-
usage?: UsageState;
|
|
12
|
-
contextWindow?: number;
|
|
13
|
-
contextPercent?: number;
|
|
14
|
-
badges?: string[];
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
function formatCount(value: number | undefined): string {
|
|
18
|
-
if (value === undefined || !Number.isFinite(value)) return "?";
|
|
19
|
-
if (Math.abs(value) >= 1000) return `${(value / 1000).toFixed(Math.abs(value) >= 10_000 ? 0 : 1)}k`;
|
|
20
|
-
return `${value}`;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
function formatCost(value: number | undefined): string {
|
|
24
|
-
return value === undefined || !Number.isFinite(value) ? "$0.0000" : `$${value.toFixed(4)}`;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
function displayPwd(pwd: string): string {
|
|
28
|
-
const home = process.env.HOME || process.env.USERPROFILE;
|
|
29
|
-
if (home && pwd.startsWith(home)) return `~${pwd.slice(home.length) || "/"}`;
|
|
30
|
-
return pwd || ".";
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
function contextText(data: CrewFooterData): string {
|
|
34
|
-
const windowText = data.contextWindow && Number.isFinite(data.contextWindow) ? formatCount(data.contextWindow) : "window";
|
|
35
|
-
const percent = data.contextPercent;
|
|
36
|
-
if (percent === undefined || !Number.isFinite(percent)) return `?/${windowText}`;
|
|
37
|
-
return `${percent.toFixed(1)}%/${windowText}`;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
export class CrewFooter {
|
|
41
|
-
private data: CrewFooterData;
|
|
42
|
-
private readonly theme: CrewTheme;
|
|
43
|
-
private cacheKey = "";
|
|
44
|
-
private cacheWidth = 0;
|
|
45
|
-
private cacheLines: string[] = [];
|
|
46
|
-
|
|
47
|
-
constructor(data: CrewFooterData, theme: CrewTheme) {
|
|
48
|
-
this.data = data;
|
|
49
|
-
this.theme = theme;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
setData(data: CrewFooterData): void {
|
|
53
|
-
this.data = data;
|
|
54
|
-
this.invalidate();
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
invalidate(): void {
|
|
58
|
-
this.cacheKey = "";
|
|
59
|
-
this.cacheLines = [];
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
render(width: number): string[] {
|
|
63
|
-
const key = JSON.stringify(this.data);
|
|
64
|
-
if (this.cacheKey === key && this.cacheWidth === width && this.cacheLines.length) return this.cacheLines;
|
|
65
|
-
const lineWidth = Math.max(1, width);
|
|
66
|
-
const firstParts = [
|
|
67
|
-
displayPwd(this.data.pwd),
|
|
68
|
-
this.data.branch ? `(${this.data.branch})` : undefined,
|
|
69
|
-
this.data.runId,
|
|
70
|
-
this.data.status,
|
|
71
|
-
].filter((part): part is string => Boolean(part));
|
|
72
|
-
const usage = this.data.usage;
|
|
73
|
-
const context = contextText(this.data);
|
|
74
|
-
const contextPercent = this.data.contextPercent;
|
|
75
|
-
const contextColor = contextPercent !== undefined && Number.isFinite(contextPercent)
|
|
76
|
-
? contextPercent > 90
|
|
77
|
-
? "error"
|
|
78
|
-
: contextPercent > 70
|
|
79
|
-
? "warning"
|
|
80
|
-
: undefined
|
|
81
|
-
: undefined;
|
|
82
|
-
const contextRendered = contextColor ? this.theme.fg(contextColor, context) : context;
|
|
83
|
-
const usageLine = [
|
|
84
|
-
`↑${formatCount(usage?.input)}`,
|
|
85
|
-
`↓${formatCount(usage?.output)}`,
|
|
86
|
-
`R ${formatCount(usage?.cacheRead)} cache`,
|
|
87
|
-
`W ${formatCount(usage?.cacheWrite)} cache`,
|
|
88
|
-
formatCost(usage?.cost),
|
|
89
|
-
contextRendered,
|
|
90
|
-
].join(" • ");
|
|
91
|
-
const badges = this.data.badges?.length ? this.data.badges.map((badge) => `[${badge}]`).join(" ") : "";
|
|
92
|
-
this.cacheLines = [
|
|
93
|
-
this.theme.fg("dim", pad(truncate(firstParts.join(" • "), lineWidth, "..."), lineWidth)),
|
|
94
|
-
this.theme.fg("dim", pad(truncate(usageLine, lineWidth, "..."), lineWidth)),
|
|
95
|
-
this.theme.fg("dim", pad(truncate(badges, lineWidth, "..."), lineWidth)),
|
|
96
|
-
];
|
|
97
|
-
this.cacheKey = key;
|
|
98
|
-
this.cacheWidth = width;
|
|
99
|
-
return this.cacheLines;
|
|
100
|
-
}
|
|
101
|
-
}
|
|
1
|
+
import type { UsageState } from "../state/types.ts";
|
|
2
|
+
import { pad, truncate } from "../utils/visual.ts";
|
|
3
|
+
import type { RunStatus } from "./status-colors.ts";
|
|
4
|
+
import type { CrewTheme } from "./theme-adapter.ts";
|
|
5
|
+
|
|
6
|
+
export interface CrewFooterData {
|
|
7
|
+
pwd: string;
|
|
8
|
+
branch?: string;
|
|
9
|
+
runId?: string;
|
|
10
|
+
status?: RunStatus;
|
|
11
|
+
usage?: UsageState;
|
|
12
|
+
contextWindow?: number;
|
|
13
|
+
contextPercent?: number;
|
|
14
|
+
badges?: string[];
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function formatCount(value: number | undefined): string {
|
|
18
|
+
if (value === undefined || !Number.isFinite(value)) return "?";
|
|
19
|
+
if (Math.abs(value) >= 1000) return `${(value / 1000).toFixed(Math.abs(value) >= 10_000 ? 0 : 1)}k`;
|
|
20
|
+
return `${value}`;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function formatCost(value: number | undefined): string {
|
|
24
|
+
return value === undefined || !Number.isFinite(value) ? "$0.0000" : `$${value.toFixed(4)}`;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function displayPwd(pwd: string): string {
|
|
28
|
+
const home = process.env.HOME || process.env.USERPROFILE;
|
|
29
|
+
if (home && pwd.startsWith(home)) return `~${pwd.slice(home.length) || "/"}`;
|
|
30
|
+
return pwd || ".";
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function contextText(data: CrewFooterData): string {
|
|
34
|
+
const windowText = data.contextWindow && Number.isFinite(data.contextWindow) ? formatCount(data.contextWindow) : "window";
|
|
35
|
+
const percent = data.contextPercent;
|
|
36
|
+
if (percent === undefined || !Number.isFinite(percent)) return `?/${windowText}`;
|
|
37
|
+
return `${percent.toFixed(1)}%/${windowText}`;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export class CrewFooter {
|
|
41
|
+
private data: CrewFooterData;
|
|
42
|
+
private readonly theme: CrewTheme;
|
|
43
|
+
private cacheKey = "";
|
|
44
|
+
private cacheWidth = 0;
|
|
45
|
+
private cacheLines: string[] = [];
|
|
46
|
+
|
|
47
|
+
constructor(data: CrewFooterData, theme: CrewTheme) {
|
|
48
|
+
this.data = data;
|
|
49
|
+
this.theme = theme;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
setData(data: CrewFooterData): void {
|
|
53
|
+
this.data = data;
|
|
54
|
+
this.invalidate();
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
invalidate(): void {
|
|
58
|
+
this.cacheKey = "";
|
|
59
|
+
this.cacheLines = [];
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
render(width: number): string[] {
|
|
63
|
+
const key = JSON.stringify(this.data);
|
|
64
|
+
if (this.cacheKey === key && this.cacheWidth === width && this.cacheLines.length) return this.cacheLines;
|
|
65
|
+
const lineWidth = Math.max(1, width);
|
|
66
|
+
const firstParts = [
|
|
67
|
+
displayPwd(this.data.pwd),
|
|
68
|
+
this.data.branch ? `(${this.data.branch})` : undefined,
|
|
69
|
+
this.data.runId,
|
|
70
|
+
this.data.status,
|
|
71
|
+
].filter((part): part is string => Boolean(part));
|
|
72
|
+
const usage = this.data.usage;
|
|
73
|
+
const context = contextText(this.data);
|
|
74
|
+
const contextPercent = this.data.contextPercent;
|
|
75
|
+
const contextColor = contextPercent !== undefined && Number.isFinite(contextPercent)
|
|
76
|
+
? contextPercent > 90
|
|
77
|
+
? "error"
|
|
78
|
+
: contextPercent > 70
|
|
79
|
+
? "warning"
|
|
80
|
+
: undefined
|
|
81
|
+
: undefined;
|
|
82
|
+
const contextRendered = contextColor ? this.theme.fg(contextColor, context) : context;
|
|
83
|
+
const usageLine = [
|
|
84
|
+
`↑${formatCount(usage?.input)}`,
|
|
85
|
+
`↓${formatCount(usage?.output)}`,
|
|
86
|
+
`R ${formatCount(usage?.cacheRead)} cache`,
|
|
87
|
+
`W ${formatCount(usage?.cacheWrite)} cache`,
|
|
88
|
+
formatCost(usage?.cost),
|
|
89
|
+
contextRendered,
|
|
90
|
+
].join(" • ");
|
|
91
|
+
const badges = this.data.badges?.length ? this.data.badges.map((badge) => `[${badge}]`).join(" ") : "";
|
|
92
|
+
this.cacheLines = [
|
|
93
|
+
this.theme.fg("dim", pad(truncate(firstParts.join(" • "), lineWidth, "..."), lineWidth)),
|
|
94
|
+
this.theme.fg("dim", pad(truncate(usageLine, lineWidth, "..."), lineWidth)),
|
|
95
|
+
this.theme.fg("dim", pad(truncate(badges, lineWidth, "..."), lineWidth)),
|
|
96
|
+
];
|
|
97
|
+
this.cacheKey = key;
|
|
98
|
+
this.cacheWidth = width;
|
|
99
|
+
return this.cacheLines;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
@@ -1,111 +1,111 @@
|
|
|
1
|
-
import { pad, truncate } from "../utils/visual.ts";
|
|
2
|
-
import type { CrewTheme } from "./theme-adapter.ts";
|
|
3
|
-
|
|
4
|
-
export interface CrewSelectItem<T = string> {
|
|
5
|
-
value: T;
|
|
6
|
-
label: string;
|
|
7
|
-
description?: string;
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
export interface CrewSelectListOptions<T = string> {
|
|
11
|
-
onSelect: (item: CrewSelectItem<T>) => void;
|
|
12
|
-
onCancel: () => void;
|
|
13
|
-
onPreview?: (item: CrewSelectItem<T>) => void;
|
|
14
|
-
maxHeight?: number;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
export class CrewSelectList<T = string> {
|
|
18
|
-
private readonly items: CrewSelectItem<T>[];
|
|
19
|
-
private readonly theme: CrewTheme;
|
|
20
|
-
private readonly options: CrewSelectListOptions<T>;
|
|
21
|
-
private selectedIndex = 0;
|
|
22
|
-
private scrollOffset = 0;
|
|
23
|
-
|
|
24
|
-
constructor(items: CrewSelectItem<T>[], theme: CrewTheme, options: CrewSelectListOptions<T>) {
|
|
25
|
-
this.items = [...items];
|
|
26
|
-
this.theme = theme;
|
|
27
|
-
this.options = options;
|
|
28
|
-
this.selectedIndex = this.items.length ? 0 : -1;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
invalidate(): void {}
|
|
32
|
-
|
|
33
|
-
getSelected(): CrewSelectItem<T> | undefined {
|
|
34
|
-
return this.selectedIndex >= 0 ? this.items[this.selectedIndex] : undefined;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
setSelectedIndex(index: number): void {
|
|
38
|
-
if (!this.items.length) {
|
|
39
|
-
this.selectedIndex = -1;
|
|
40
|
-
this.scrollOffset = 0;
|
|
41
|
-
return;
|
|
42
|
-
}
|
|
43
|
-
const next = Math.min(this.items.length - 1, Math.max(0, index));
|
|
44
|
-
const changed = next !== this.selectedIndex;
|
|
45
|
-
this.selectedIndex = next;
|
|
46
|
-
this.ensureVisible();
|
|
47
|
-
if (changed) {
|
|
48
|
-
const selected = this.getSelected();
|
|
49
|
-
if (selected) this.options.onPreview?.(selected);
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
handleInput(data: string): void {
|
|
54
|
-
if (data === "q" || data === "\u001b") {
|
|
55
|
-
this.options.onCancel();
|
|
56
|
-
return;
|
|
57
|
-
}
|
|
58
|
-
if (data === "j" || data === "\u001b[B") {
|
|
59
|
-
this.setSelectedIndex(this.selectedIndex + 1);
|
|
60
|
-
return;
|
|
61
|
-
}
|
|
62
|
-
if (data === "k" || data === "\u001b[A") {
|
|
63
|
-
this.setSelectedIndex(this.selectedIndex - 1);
|
|
64
|
-
return;
|
|
65
|
-
}
|
|
66
|
-
if (data === "\r" || data === "\n") {
|
|
67
|
-
const selected = this.getSelected();
|
|
68
|
-
if (selected) this.options.onSelect(selected);
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
render(width: number): string[] {
|
|
73
|
-
if (!this.items.length) return [this.theme.fg("muted", "(no items)")];
|
|
74
|
-
const maxHeight = Math.max(1, Math.floor(this.options.maxHeight ?? this.items.length));
|
|
75
|
-
this.ensureVisible();
|
|
76
|
-
const hasTop = this.scrollOffset > 0;
|
|
77
|
-
const availableWithoutBottom = Math.max(1, maxHeight - (hasTop ? 1 : 0));
|
|
78
|
-
const hasBottom = this.scrollOffset + availableWithoutBottom < this.items.length;
|
|
79
|
-
const slots = this.visibleItemSlots(maxHeight, hasTop, hasBottom);
|
|
80
|
-
const visibleItems = this.items.slice(this.scrollOffset, this.scrollOffset + slots);
|
|
81
|
-
const lines: string[] = [];
|
|
82
|
-
if (hasTop) lines.push(this.theme.fg("muted", `↑ ${this.scrollOffset} more`));
|
|
83
|
-
for (const [offset, item] of visibleItems.entries()) {
|
|
84
|
-
const index = this.scrollOffset + offset;
|
|
85
|
-
const prefix = index === this.selectedIndex ? " → " : " ";
|
|
86
|
-
const suffix = item.description ? this.theme.fg("dim", ` — ${item.description}`) : "";
|
|
87
|
-
const raw = `${prefix}${item.label}${suffix}`;
|
|
88
|
-
const line = index === this.selectedIndex ? this.theme.inverse?.(raw) ?? raw : raw;
|
|
89
|
-
lines.push(pad(truncate(line, width, "..."), Math.max(1, width)));
|
|
90
|
-
}
|
|
91
|
-
if (hasBottom) lines.push(this.theme.fg("muted", `↓ ${this.items.length - (this.scrollOffset + slots)} more`));
|
|
92
|
-
return lines.slice(0, maxHeight);
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
private visibleItemSlots(maxHeight: number, hasTop: boolean, hasBottom: boolean): number {
|
|
96
|
-
return Math.max(1, maxHeight - (hasTop ? 1 : 0) - (hasBottom ? 1 : 0));
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
private ensureVisible(): void {
|
|
100
|
-
if (this.selectedIndex < 0) return;
|
|
101
|
-
const maxHeight = Math.max(1, Math.floor(this.options.maxHeight ?? this.items.length));
|
|
102
|
-
const reservedTop = this.scrollOffset > 0 ? 1 : 0;
|
|
103
|
-
const visibleSlots = Math.max(1, maxHeight - reservedTop - 1);
|
|
104
|
-
if (this.selectedIndex < this.scrollOffset) {
|
|
105
|
-
this.scrollOffset = this.selectedIndex;
|
|
106
|
-
} else if (this.selectedIndex >= this.scrollOffset + visibleSlots) {
|
|
107
|
-
this.scrollOffset = Math.max(0, this.selectedIndex - visibleSlots + 1);
|
|
108
|
-
}
|
|
109
|
-
this.scrollOffset = Math.min(this.scrollOffset, Math.max(0, this.items.length - 1));
|
|
110
|
-
}
|
|
111
|
-
}
|
|
1
|
+
import { pad, truncate } from "../utils/visual.ts";
|
|
2
|
+
import type { CrewTheme } from "./theme-adapter.ts";
|
|
3
|
+
|
|
4
|
+
export interface CrewSelectItem<T = string> {
|
|
5
|
+
value: T;
|
|
6
|
+
label: string;
|
|
7
|
+
description?: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface CrewSelectListOptions<T = string> {
|
|
11
|
+
onSelect: (item: CrewSelectItem<T>) => void;
|
|
12
|
+
onCancel: () => void;
|
|
13
|
+
onPreview?: (item: CrewSelectItem<T>) => void;
|
|
14
|
+
maxHeight?: number;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export class CrewSelectList<T = string> {
|
|
18
|
+
private readonly items: CrewSelectItem<T>[];
|
|
19
|
+
private readonly theme: CrewTheme;
|
|
20
|
+
private readonly options: CrewSelectListOptions<T>;
|
|
21
|
+
private selectedIndex = 0;
|
|
22
|
+
private scrollOffset = 0;
|
|
23
|
+
|
|
24
|
+
constructor(items: CrewSelectItem<T>[], theme: CrewTheme, options: CrewSelectListOptions<T>) {
|
|
25
|
+
this.items = [...items];
|
|
26
|
+
this.theme = theme;
|
|
27
|
+
this.options = options;
|
|
28
|
+
this.selectedIndex = this.items.length ? 0 : -1;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
invalidate(): void {}
|
|
32
|
+
|
|
33
|
+
getSelected(): CrewSelectItem<T> | undefined {
|
|
34
|
+
return this.selectedIndex >= 0 ? this.items[this.selectedIndex] : undefined;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
setSelectedIndex(index: number): void {
|
|
38
|
+
if (!this.items.length) {
|
|
39
|
+
this.selectedIndex = -1;
|
|
40
|
+
this.scrollOffset = 0;
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
const next = Math.min(this.items.length - 1, Math.max(0, index));
|
|
44
|
+
const changed = next !== this.selectedIndex;
|
|
45
|
+
this.selectedIndex = next;
|
|
46
|
+
this.ensureVisible();
|
|
47
|
+
if (changed) {
|
|
48
|
+
const selected = this.getSelected();
|
|
49
|
+
if (selected) this.options.onPreview?.(selected);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
handleInput(data: string): void {
|
|
54
|
+
if (data === "q" || data === "\u001b") {
|
|
55
|
+
this.options.onCancel();
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
if (data === "j" || data === "\u001b[B") {
|
|
59
|
+
this.setSelectedIndex(this.selectedIndex + 1);
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
if (data === "k" || data === "\u001b[A") {
|
|
63
|
+
this.setSelectedIndex(this.selectedIndex - 1);
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
if (data === "\r" || data === "\n") {
|
|
67
|
+
const selected = this.getSelected();
|
|
68
|
+
if (selected) this.options.onSelect(selected);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
render(width: number): string[] {
|
|
73
|
+
if (!this.items.length) return [this.theme.fg("muted", "(no items)")];
|
|
74
|
+
const maxHeight = Math.max(1, Math.floor(this.options.maxHeight ?? this.items.length));
|
|
75
|
+
this.ensureVisible();
|
|
76
|
+
const hasTop = this.scrollOffset > 0;
|
|
77
|
+
const availableWithoutBottom = Math.max(1, maxHeight - (hasTop ? 1 : 0));
|
|
78
|
+
const hasBottom = this.scrollOffset + availableWithoutBottom < this.items.length;
|
|
79
|
+
const slots = this.visibleItemSlots(maxHeight, hasTop, hasBottom);
|
|
80
|
+
const visibleItems = this.items.slice(this.scrollOffset, this.scrollOffset + slots);
|
|
81
|
+
const lines: string[] = [];
|
|
82
|
+
if (hasTop) lines.push(this.theme.fg("muted", `↑ ${this.scrollOffset} more`));
|
|
83
|
+
for (const [offset, item] of visibleItems.entries()) {
|
|
84
|
+
const index = this.scrollOffset + offset;
|
|
85
|
+
const prefix = index === this.selectedIndex ? " → " : " ";
|
|
86
|
+
const suffix = item.description ? this.theme.fg("dim", ` — ${item.description}`) : "";
|
|
87
|
+
const raw = `${prefix}${item.label}${suffix}`;
|
|
88
|
+
const line = index === this.selectedIndex ? this.theme.inverse?.(raw) ?? raw : raw;
|
|
89
|
+
lines.push(pad(truncate(line, width, "..."), Math.max(1, width)));
|
|
90
|
+
}
|
|
91
|
+
if (hasBottom) lines.push(this.theme.fg("muted", `↓ ${this.items.length - (this.scrollOffset + slots)} more`));
|
|
92
|
+
return lines.slice(0, maxHeight);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
private visibleItemSlots(maxHeight: number, hasTop: boolean, hasBottom: boolean): number {
|
|
96
|
+
return Math.max(1, maxHeight - (hasTop ? 1 : 0) - (hasBottom ? 1 : 0));
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
private ensureVisible(): void {
|
|
100
|
+
if (this.selectedIndex < 0) return;
|
|
101
|
+
const maxHeight = Math.max(1, Math.floor(this.options.maxHeight ?? this.items.length));
|
|
102
|
+
const reservedTop = this.scrollOffset > 0 ? 1 : 0;
|
|
103
|
+
const visibleSlots = Math.max(1, maxHeight - reservedTop - 1);
|
|
104
|
+
if (this.selectedIndex < this.scrollOffset) {
|
|
105
|
+
this.scrollOffset = this.selectedIndex;
|
|
106
|
+
} else if (this.selectedIndex >= this.scrollOffset + visibleSlots) {
|
|
107
|
+
this.scrollOffset = Math.max(0, this.selectedIndex - visibleSlots + 1);
|
|
108
|
+
}
|
|
109
|
+
this.scrollOffset = Math.min(this.scrollOffset, Math.max(0, this.items.length - 1));
|
|
110
|
+
}
|
|
111
|
+
}
|
|
@@ -1,34 +1,34 @@
|
|
|
1
|
-
import type { MetricRegistry } from "../../observability/metric-registry.ts";
|
|
2
|
-
import type { HistogramPoint, MetricLabels, MetricPoint } from "../../observability/metrics-primitives.ts";
|
|
3
|
-
import type { RunUiSnapshot } from "../snapshot-types.ts";
|
|
4
|
-
|
|
5
|
-
export interface MetricsPaneOptions {
|
|
6
|
-
registry?: MetricRegistry;
|
|
7
|
-
maxCounters?: number;
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
function labelsText(labels: MetricLabels): string {
|
|
11
|
-
const entries = Object.entries(labels);
|
|
12
|
-
return entries.length ? `{${entries.map(([key, value]) => `${key}=${value}`).join(",")}}` : "";
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
function isHistogramPoint(point: MetricPoint | HistogramPoint): point is HistogramPoint {
|
|
16
|
-
return "quantiles" in point;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
export function renderMetricsPane(_snapshot: RunUiSnapshot | undefined, opts: MetricsPaneOptions = {}): string[] {
|
|
20
|
-
if (!opts.registry) return ["Metrics pane: registry unavailable"];
|
|
21
|
-
const snapshots = opts.registry.snapshot();
|
|
22
|
-
if (!snapshots.length) return ["Metrics pane: no metrics recorded"];
|
|
23
|
-
const lines = ["Metrics pane: top metrics"];
|
|
24
|
-
for (const snapshot of snapshots.slice(0, opts.maxCounters ?? 10)) {
|
|
25
|
-
const first = snapshot.values[0];
|
|
26
|
-
if (!first) {
|
|
27
|
-
lines.push(`${snapshot.name}: empty`);
|
|
28
|
-
continue;
|
|
29
|
-
}
|
|
30
|
-
if (isHistogramPoint(first)) lines.push(`${snapshot.name}${labelsText(first.labels)} count=${first.count} p95=${Number.isFinite(first.quantiles.p95) ? Math.round(first.quantiles.p95) : "n/a"}`);
|
|
31
|
-
else lines.push(`${snapshot.name}${labelsText(first.labels)} ${first.value}`);
|
|
32
|
-
}
|
|
33
|
-
return lines;
|
|
34
|
-
}
|
|
1
|
+
import type { MetricRegistry } from "../../observability/metric-registry.ts";
|
|
2
|
+
import type { HistogramPoint, MetricLabels, MetricPoint } from "../../observability/metrics-primitives.ts";
|
|
3
|
+
import type { RunUiSnapshot } from "../snapshot-types.ts";
|
|
4
|
+
|
|
5
|
+
export interface MetricsPaneOptions {
|
|
6
|
+
registry?: MetricRegistry;
|
|
7
|
+
maxCounters?: number;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function labelsText(labels: MetricLabels): string {
|
|
11
|
+
const entries = Object.entries(labels);
|
|
12
|
+
return entries.length ? `{${entries.map(([key, value]) => `${key}=${value}`).join(",")}}` : "";
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function isHistogramPoint(point: MetricPoint | HistogramPoint): point is HistogramPoint {
|
|
16
|
+
return "quantiles" in point;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function renderMetricsPane(_snapshot: RunUiSnapshot | undefined, opts: MetricsPaneOptions = {}): string[] {
|
|
20
|
+
if (!opts.registry) return ["Metrics pane: registry unavailable"];
|
|
21
|
+
const snapshots = opts.registry.snapshot();
|
|
22
|
+
if (!snapshots.length) return ["Metrics pane: no metrics recorded"];
|
|
23
|
+
const lines = ["Metrics pane: top metrics"];
|
|
24
|
+
for (const snapshot of snapshots.slice(0, opts.maxCounters ?? 10)) {
|
|
25
|
+
const first = snapshot.values[0];
|
|
26
|
+
if (!first) {
|
|
27
|
+
lines.push(`${snapshot.name}: empty`);
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
30
|
+
if (isHistogramPoint(first)) lines.push(`${snapshot.name}${labelsText(first.labels)} count=${first.count} p95=${Number.isFinite(first.quantiles.p95) ? Math.round(first.quantiles.p95) : "n/a"}`);
|
|
31
|
+
else lines.push(`${snapshot.name}${labelsText(first.labels)} ${first.value}`);
|
|
32
|
+
}
|
|
33
|
+
return lines;
|
|
34
|
+
}
|
package/src/ui/dynamic-border.ts
CHANGED
|
@@ -1,25 +1,25 @@
|
|
|
1
|
-
import type { CrewTheme } from "./theme-adapter.ts";
|
|
2
|
-
|
|
3
|
-
export interface DynamicCrewBorderOptions {
|
|
4
|
-
color?: (value: string) => string;
|
|
5
|
-
char?: string;
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
export class DynamicCrewBorder {
|
|
9
|
-
private readonly theme: CrewTheme;
|
|
10
|
-
private readonly color?: (value: string) => string;
|
|
11
|
-
private readonly char: string;
|
|
12
|
-
|
|
13
|
-
constructor(theme: CrewTheme, options: DynamicCrewBorderOptions = {}) {
|
|
14
|
-
this.theme = theme;
|
|
15
|
-
this.color = options.color;
|
|
16
|
-
this.char = options.char && options.char.length > 0 ? options.char : "─";
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
render(width: number): string[] {
|
|
20
|
-
const line = this.char.repeat(Math.max(0, width));
|
|
21
|
-
return [this.color ? this.color(line) : this.theme.fg("border", line)];
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
invalidate(): void {}
|
|
25
|
-
}
|
|
1
|
+
import type { CrewTheme } from "./theme-adapter.ts";
|
|
2
|
+
|
|
3
|
+
export interface DynamicCrewBorderOptions {
|
|
4
|
+
color?: (value: string) => string;
|
|
5
|
+
char?: string;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export class DynamicCrewBorder {
|
|
9
|
+
private readonly theme: CrewTheme;
|
|
10
|
+
private readonly color?: (value: string) => string;
|
|
11
|
+
private readonly char: string;
|
|
12
|
+
|
|
13
|
+
constructor(theme: CrewTheme, options: DynamicCrewBorderOptions = {}) {
|
|
14
|
+
this.theme = theme;
|
|
15
|
+
this.color = options.color;
|
|
16
|
+
this.char = options.char && options.char.length > 0 ? options.char : "─";
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
render(width: number): string[] {
|
|
20
|
+
const line = this.char.repeat(Math.max(0, width));
|
|
21
|
+
return [this.color ? this.color(line) : this.theme.fg("border", line)];
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
invalidate(): void {}
|
|
25
|
+
}
|