pi-crew 0.1.44 → 0.1.45

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.
Files changed (142) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/agents/analyst.md +11 -11
  3. package/agents/critic.md +11 -11
  4. package/agents/executor.md +11 -11
  5. package/agents/explorer.md +11 -11
  6. package/agents/planner.md +11 -11
  7. package/agents/reviewer.md +11 -11
  8. package/agents/security-reviewer.md +11 -11
  9. package/agents/test-engineer.md +11 -11
  10. package/agents/verifier.md +11 -11
  11. package/agents/writer.md +11 -11
  12. package/docs/refactor-tasks-phase3.md +394 -394
  13. package/docs/refactor-tasks-phase4.md +564 -564
  14. package/docs/refactor-tasks-phase5.md +402 -402
  15. package/docs/refactor-tasks-phase6.md +662 -662
  16. package/docs/research-extension-examples.md +297 -297
  17. package/docs/research-extension-system.md +324 -324
  18. package/docs/research-optimization-plan.md +548 -548
  19. package/docs/research-phase10-distillation.md +198 -198
  20. package/docs/research-phase11-distillation.md +201 -201
  21. package/docs/research-pi-coding-agent.md +357 -357
  22. package/docs/research-source-pi-crew-reference.md +174 -174
  23. package/docs/runtime-flow.md +148 -148
  24. package/docs/source-runtime-refactor-map.md +83 -83
  25. package/index.ts +6 -6
  26. package/package.json +1 -1
  27. package/src/agents/agent-serializer.ts +34 -34
  28. package/src/extension/cross-extension-rpc.ts +82 -82
  29. package/src/extension/register.ts +8 -1
  30. package/src/extension/registration/commands.ts +18 -2
  31. package/src/extension/registration/compaction-guard.ts +125 -125
  32. package/src/extension/registration/subagent-tools.ts +148 -148
  33. package/src/extension/registration/team-tool.ts +26 -8
  34. package/src/extension/run-bundle-schema.ts +89 -89
  35. package/src/extension/run-maintenance.ts +43 -43
  36. package/src/extension/team-tool/cancel.ts +105 -102
  37. package/src/extension/team-tool/context.ts +1 -0
  38. package/src/extension/team-tool/handle-settings.ts +188 -188
  39. package/src/extension/team-tool/inspect.ts +41 -41
  40. package/src/extension/team-tool/lifecycle-actions.ts +79 -79
  41. package/src/extension/team-tool/plan.ts +19 -19
  42. package/src/extension/team-tool/respond.ts +83 -66
  43. package/src/extension/team-tool/run.ts +1 -0
  44. package/src/i18n.ts +184 -184
  45. package/src/observability/exporters/otlp-exporter.ts +77 -77
  46. package/src/prompt/prompt-runtime.ts +72 -72
  47. package/src/runtime/agent-control.ts +63 -63
  48. package/src/runtime/agent-memory.ts +72 -72
  49. package/src/runtime/agent-observability.ts +114 -114
  50. package/src/runtime/async-marker.ts +26 -26
  51. package/src/runtime/attention-events.ts +28 -28
  52. package/src/runtime/background-runner.ts +53 -53
  53. package/src/runtime/child-pi.ts +444 -444
  54. package/src/runtime/completion-guard.ts +190 -190
  55. package/src/runtime/crew-agent-records.ts +8 -0
  56. package/src/runtime/delivery-coordinator.ts +153 -142
  57. package/src/runtime/direct-run.ts +35 -35
  58. package/src/runtime/foreground-control.ts +82 -82
  59. package/src/runtime/green-contract.ts +46 -46
  60. package/src/runtime/group-join.ts +106 -106
  61. package/src/runtime/heartbeat-gradient.ts +28 -28
  62. package/src/runtime/heartbeat-watcher.ts +124 -124
  63. package/src/runtime/live-agent-control.ts +87 -87
  64. package/src/runtime/live-agent-manager.ts +85 -85
  65. package/src/runtime/live-control-realtime.ts +36 -36
  66. package/src/runtime/live-session-runtime.ts +305 -305
  67. package/src/runtime/overflow-recovery.ts +175 -156
  68. package/src/runtime/parallel-research.ts +44 -44
  69. package/src/runtime/pi-json-output.ts +111 -111
  70. package/src/runtime/policy-engine.ts +79 -79
  71. package/src/runtime/progress-event-coalescer.ts +43 -43
  72. package/src/runtime/recovery-recipes.ts +74 -74
  73. package/src/runtime/retry-executor.ts +64 -64
  74. package/src/runtime/role-permission.ts +39 -39
  75. package/src/runtime/session-resources.ts +25 -25
  76. package/src/runtime/session-snapshot.ts +59 -59
  77. package/src/runtime/session-usage.ts +79 -79
  78. package/src/runtime/sidechain-output.ts +29 -29
  79. package/src/runtime/stale-reconciler.ts +199 -179
  80. package/src/runtime/supervisor-contact.ts +59 -59
  81. package/src/runtime/task-display.ts +38 -38
  82. package/src/runtime/task-output-context.ts +127 -127
  83. package/src/runtime/task-runner/live-executor.ts +101 -101
  84. package/src/runtime/task-runner/progress.ts +119 -119
  85. package/src/runtime/task-runner/result-utils.ts +14 -14
  86. package/src/runtime/task-runner/state-helpers.ts +22 -22
  87. package/src/runtime/team-runner.ts +13 -4
  88. package/src/runtime/worker-heartbeat.ts +21 -21
  89. package/src/runtime/worker-startup.ts +57 -57
  90. package/src/state/state-store.ts +43 -0
  91. package/src/state/task-claims.ts +44 -44
  92. package/src/state/types.ts +2 -0
  93. package/src/state/usage.ts +29 -29
  94. package/src/subagents/async-entry.ts +1 -1
  95. package/src/subagents/index.ts +3 -3
  96. package/src/subagents/live/control.ts +1 -1
  97. package/src/subagents/live/manager.ts +1 -1
  98. package/src/subagents/live/realtime.ts +1 -1
  99. package/src/subagents/live/session-runtime.ts +1 -1
  100. package/src/subagents/manager.ts +1 -1
  101. package/src/subagents/spawn.ts +1 -1
  102. package/src/teams/team-serializer.ts +38 -38
  103. package/src/types/diff.d.ts +18 -18
  104. package/src/ui/crew-footer.ts +101 -101
  105. package/src/ui/crew-select-list.ts +111 -111
  106. package/src/ui/crew-widget.ts +5 -1
  107. package/src/ui/dashboard-panes/mailbox-pane.ts +2 -1
  108. package/src/ui/dashboard-panes/metrics-pane.ts +34 -34
  109. package/src/ui/dynamic-border.ts +25 -25
  110. package/src/ui/layout-primitives.ts +106 -106
  111. package/src/ui/loaders.ts +158 -158
  112. package/src/ui/powerbar-publisher.ts +1 -1
  113. package/src/ui/render-diff.ts +119 -119
  114. package/src/ui/render-scheduler.ts +143 -143
  115. package/src/ui/run-snapshot-cache.ts +56 -37
  116. package/src/ui/snapshot-types.ts +5 -0
  117. package/src/ui/spinner.ts +17 -17
  118. package/src/ui/status-colors.ts +58 -58
  119. package/src/ui/syntax-highlight.ts +116 -116
  120. package/src/utils/atomic-write.ts +33 -33
  121. package/src/utils/completion-dedupe.ts +63 -63
  122. package/src/utils/frontmatter.ts +68 -68
  123. package/src/utils/git.ts +262 -262
  124. package/src/utils/ids.ts +12 -12
  125. package/src/utils/names.ts +27 -27
  126. package/src/utils/redaction.ts +44 -44
  127. package/src/utils/safe-paths.ts +47 -47
  128. package/src/utils/sleep.ts +32 -32
  129. package/src/workflows/validate-workflow.ts +40 -40
  130. package/src/worktree/branch-freshness.ts +45 -45
  131. package/teams/default.team.md +12 -12
  132. package/teams/fast-fix.team.md +11 -11
  133. package/teams/implementation.team.md +18 -18
  134. package/teams/parallel-research.team.md +14 -14
  135. package/teams/research.team.md +11 -11
  136. package/teams/review.team.md +12 -12
  137. package/workflows/default.workflow.md +29 -29
  138. package/workflows/fast-fix.workflow.md +22 -22
  139. package/workflows/implementation.workflow.md +38 -38
  140. package/workflows/parallel-research.workflow.md +46 -46
  141. package/workflows/research.workflow.md +22 -22
  142. package/workflows/review.workflow.md +30 -30
@@ -1,44 +1,44 @@
1
- import { randomUUID } from "node:crypto";
2
- import type { TeamTaskState } from "./types.ts";
3
-
4
- export interface TaskClaimState {
5
- owner: string;
6
- token: string;
7
- leasedUntil: string;
8
- }
9
-
10
- export function createTaskClaim(owner: string, leaseMs = 5 * 60_000, now = new Date()): TaskClaimState {
11
- return { owner, token: randomUUID(), leasedUntil: new Date(now.getTime() + leaseMs).toISOString() };
12
- }
13
-
14
- export function isTaskClaimExpired(claim: TaskClaimState | undefined, now = new Date()): boolean {
15
- if (!claim) return false;
16
- const parsed = Date.parse(claim.leasedUntil);
17
- // Corrupt or invalid date strings produce NaN — treat as expired immediately.
18
- return Number.isFinite(parsed) ? parsed <= now.getTime() : true;
19
- }
20
-
21
- export function canUseTaskClaim(task: Pick<TeamTaskState, "claim">, owner: string, token: string, now = new Date()): boolean {
22
- return task.claim?.owner === owner && task.claim.token === token && !isTaskClaimExpired(task.claim, now);
23
- }
24
-
25
- export function claimTask<T extends TeamTaskState>(task: T, owner: string, leaseMs?: number, now = new Date()): T {
26
- if (task.claim && !isTaskClaimExpired(task.claim, now)) {
27
- throw new Error(`Task '${task.id}' is already claimed by '${task.claim.owner}'.`);
28
- }
29
- return { ...task, claim: createTaskClaim(owner, leaseMs, now) };
30
- }
31
-
32
- export function releaseTaskClaim<T extends TeamTaskState>(task: T, owner: string, token: string, now = new Date()): T {
33
- if (!canUseTaskClaim(task, owner, token, now)) {
34
- throw new Error(`Task '${task.id}' claim is not held by '${owner}' or has expired.`);
35
- }
36
- return { ...task, claim: undefined };
37
- }
38
-
39
- export function transitionClaimedTaskStatus<T extends TeamTaskState>(task: T, owner: string, token: string, status: T["status"], now = new Date()): T {
40
- if (!canUseTaskClaim(task, owner, token, now)) {
41
- throw new Error(`Task '${task.id}' claim is not held by '${owner}' or has expired.`);
42
- }
43
- return { ...task, status };
44
- }
1
+ import { randomUUID } from "node:crypto";
2
+ import type { TeamTaskState } from "./types.ts";
3
+
4
+ export interface TaskClaimState {
5
+ owner: string;
6
+ token: string;
7
+ leasedUntil: string;
8
+ }
9
+
10
+ export function createTaskClaim(owner: string, leaseMs = 5 * 60_000, now = new Date()): TaskClaimState {
11
+ return { owner, token: randomUUID(), leasedUntil: new Date(now.getTime() + leaseMs).toISOString() };
12
+ }
13
+
14
+ export function isTaskClaimExpired(claim: TaskClaimState | undefined, now = new Date()): boolean {
15
+ if (!claim) return false;
16
+ const parsed = Date.parse(claim.leasedUntil);
17
+ // Corrupt or invalid date strings produce NaN — treat as expired immediately.
18
+ return Number.isFinite(parsed) ? parsed <= now.getTime() : true;
19
+ }
20
+
21
+ export function canUseTaskClaim(task: Pick<TeamTaskState, "claim">, owner: string, token: string, now = new Date()): boolean {
22
+ return task.claim?.owner === owner && task.claim.token === token && !isTaskClaimExpired(task.claim, now);
23
+ }
24
+
25
+ export function claimTask<T extends TeamTaskState>(task: T, owner: string, leaseMs?: number, now = new Date()): T {
26
+ if (task.claim && !isTaskClaimExpired(task.claim, now)) {
27
+ throw new Error(`Task '${task.id}' is already claimed by '${task.claim.owner}'.`);
28
+ }
29
+ return { ...task, claim: createTaskClaim(owner, leaseMs, now) };
30
+ }
31
+
32
+ export function releaseTaskClaim<T extends TeamTaskState>(task: T, owner: string, token: string, now = new Date()): T {
33
+ if (!canUseTaskClaim(task, owner, token, now)) {
34
+ throw new Error(`Task '${task.id}' claim is not held by '${owner}' or has expired.`);
35
+ }
36
+ return { ...task, claim: undefined };
37
+ }
38
+
39
+ export function transitionClaimedTaskStatus<T extends TeamTaskState>(task: T, owner: string, token: string, status: T["status"], now = new Date()): T {
40
+ if (!canUseTaskClaim(task, owner, token, now)) {
41
+ throw new Error(`Task '${task.id}' claim is not held by '${owner}' or has expired.`);
42
+ }
43
+ return { ...task, status };
44
+ }
@@ -123,6 +123,8 @@ export interface TeamRunManifest {
123
123
  artifacts: ArtifactDescriptor[];
124
124
  async?: AsyncRunState;
125
125
  planApproval?: PlanApprovalState;
126
+ /** Pi session that created the run, when available. Used to prevent cross-session destructive actions. */
127
+ ownerSessionId?: string;
126
128
  summary?: string;
127
129
  policyDecisions?: PolicyDecision[];
128
130
  }
@@ -1,29 +1,29 @@
1
- import type { TeamTaskState, UsageState } from "./types.ts";
2
-
3
- export function aggregateUsage(tasks: TeamTaskState[]): UsageState | undefined {
4
- const total: UsageState = {};
5
- let found = false;
6
- for (const task of tasks) {
7
- if (!task.usage) continue;
8
- found = true;
9
- total.input = (total.input ?? 0) + (task.usage.input ?? 0);
10
- total.output = (total.output ?? 0) + (task.usage.output ?? 0);
11
- total.cacheRead = (total.cacheRead ?? 0) + (task.usage.cacheRead ?? 0);
12
- total.cacheWrite = (total.cacheWrite ?? 0) + (task.usage.cacheWrite ?? 0);
13
- total.cost = (total.cost ?? 0) + (task.usage.cost ?? 0);
14
- total.turns = (total.turns ?? 0) + (task.usage.turns ?? 0);
15
- }
16
- return found ? total : undefined;
17
- }
18
-
19
- export function formatUsage(usage: UsageState | undefined): string {
20
- if (!usage) return "(none)";
21
- const parts: string[] = [];
22
- if (usage.input !== undefined) parts.push(`input=${usage.input}`);
23
- if (usage.output !== undefined) parts.push(`output=${usage.output}`);
24
- if (usage.cacheRead !== undefined) parts.push(`cacheRead=${usage.cacheRead}`);
25
- if (usage.cacheWrite !== undefined) parts.push(`cacheWrite=${usage.cacheWrite}`);
26
- if (usage.cost !== undefined && Number.isFinite(usage.cost)) parts.push(`cost=${usage.cost.toFixed(6)}`);
27
- if (usage.turns !== undefined) parts.push(`turns=${usage.turns}`);
28
- return parts.join(", ") || "(none)";
29
- }
1
+ import type { TeamTaskState, UsageState } from "./types.ts";
2
+
3
+ export function aggregateUsage(tasks: TeamTaskState[]): UsageState | undefined {
4
+ const total: UsageState = {};
5
+ let found = false;
6
+ for (const task of tasks) {
7
+ if (!task.usage) continue;
8
+ found = true;
9
+ total.input = (total.input ?? 0) + (task.usage.input ?? 0);
10
+ total.output = (total.output ?? 0) + (task.usage.output ?? 0);
11
+ total.cacheRead = (total.cacheRead ?? 0) + (task.usage.cacheRead ?? 0);
12
+ total.cacheWrite = (total.cacheWrite ?? 0) + (task.usage.cacheWrite ?? 0);
13
+ total.cost = (total.cost ?? 0) + (task.usage.cost ?? 0);
14
+ total.turns = (total.turns ?? 0) + (task.usage.turns ?? 0);
15
+ }
16
+ return found ? total : undefined;
17
+ }
18
+
19
+ export function formatUsage(usage: UsageState | undefined): string {
20
+ if (!usage) return "(none)";
21
+ const parts: string[] = [];
22
+ if (usage.input !== undefined) parts.push(`input=${usage.input}`);
23
+ if (usage.output !== undefined) parts.push(`output=${usage.output}`);
24
+ if (usage.cacheRead !== undefined) parts.push(`cacheRead=${usage.cacheRead}`);
25
+ if (usage.cacheWrite !== undefined) parts.push(`cacheWrite=${usage.cacheWrite}`);
26
+ if (usage.cost !== undefined && Number.isFinite(usage.cost)) parts.push(`cost=${usage.cost.toFixed(6)}`);
27
+ if (usage.turns !== undefined) parts.push(`turns=${usage.turns}`);
28
+ return parts.join(", ") || "(none)";
29
+ }
@@ -1 +1 @@
1
- export * from "../runtime/async-runner.ts";
1
+ export * from "../runtime/async-runner.ts";
@@ -1,3 +1,3 @@
1
- export * from "./spawn.ts";
2
- export * from "./manager.ts";
3
- export * from "./async-entry.ts";
1
+ export * from "./spawn.ts";
2
+ export * from "./manager.ts";
3
+ export * from "./async-entry.ts";
@@ -1 +1 @@
1
- export * from "../../runtime/live-agent-control.ts";
1
+ export * from "../../runtime/live-agent-control.ts";
@@ -1 +1 @@
1
- export * from "../../runtime/live-agent-manager.ts";
1
+ export * from "../../runtime/live-agent-manager.ts";
@@ -1 +1 @@
1
- export * from "../../runtime/live-control-realtime.ts";
1
+ export * from "../../runtime/live-control-realtime.ts";
@@ -1 +1 @@
1
- export * from "../../runtime/live-session-runtime.ts";
1
+ export * from "../../runtime/live-session-runtime.ts";
@@ -1 +1 @@
1
- export * from "../runtime/subagent-manager.ts";
1
+ export * from "../runtime/subagent-manager.ts";
@@ -1 +1 @@
1
- export * from "../runtime/child-pi.ts";
1
+ export * from "../runtime/child-pi.ts";
@@ -1,38 +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.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
- }
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
+ }
@@ -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
+ }
@@ -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
+ }