pi-crew 0.6.3 → 0.7.1

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 (56) hide show
  1. package/CHANGELOG.md +84 -0
  2. package/README.md +80 -1
  3. package/docs/ui-optimization-plan.md +447 -0
  4. package/package.json +2 -1
  5. package/src/extension/knowledge-injection.ts +71 -0
  6. package/src/extension/pi-api.ts +47 -0
  7. package/src/extension/register.ts +32 -1
  8. package/src/extension/registration/commands.ts +65 -1
  9. package/src/extension/registration/compaction-guard.ts +154 -14
  10. package/src/extension/registration/subagent-tools.ts +8 -3
  11. package/src/extension/registration/team-tool.ts +18 -11
  12. package/src/extension/team-tool/handle-settings.ts +57 -0
  13. package/src/extension/team-tool/inspect.ts +4 -1
  14. package/src/extension/team-tool/plan.ts +8 -1
  15. package/src/extension/team-tool/run.ts +4 -3
  16. package/src/extension/team-tool-types.ts +2 -0
  17. package/src/runtime/intercom-bridge.ts +5 -1
  18. package/src/runtime/replace.ts +555 -0
  19. package/src/runtime/resilient-edit.ts +166 -0
  20. package/src/runtime/single-agent-compose.ts +87 -0
  21. package/src/runtime/team-runner.ts +23 -6
  22. package/src/schema/team-tool-schema.ts +6 -0
  23. package/src/state/session-state-map.ts +51 -0
  24. package/src/state/usage.ts +73 -0
  25. package/src/ui/card-colors.ts +126 -0
  26. package/src/ui/deploy-bundled-themes.ts +71 -0
  27. package/src/ui/powerbar-publisher.ts +1 -1
  28. package/src/ui/render-diff.ts +37 -3
  29. package/src/ui/settings-overlay.ts +70 -7
  30. package/src/ui/status-colors.ts +5 -1
  31. package/src/ui/syntax-highlight.ts +42 -23
  32. package/src/ui/theme-adapter.ts +80 -1
  33. package/src/ui/theme-discovery.ts +188 -0
  34. package/src/ui/tool-progress-formatter.ts +9 -5
  35. package/src/ui/tool-render.ts +4 -0
  36. package/src/ui/tool-renderers/brief-mode.ts +207 -0
  37. package/src/ui/tool-renderers/index.ts +640 -0
  38. package/src/ui/widget/index.ts +224 -0
  39. package/src/ui/widget/widget-formatters.ts +148 -0
  40. package/src/ui/widget/widget-model.ts +90 -0
  41. package/src/ui/widget/widget-renderer.ts +130 -0
  42. package/src/ui/widget/widget-types.ts +37 -0
  43. package/src/utils/guards.ts +110 -0
  44. package/themes/crew-catppuccin-latte.json +96 -0
  45. package/themes/crew-catppuccin-mocha.json +93 -0
  46. package/themes/crew-dark.json +90 -0
  47. package/themes/crew-dracula.json +83 -0
  48. package/themes/crew-gruvbox-dark.json +83 -0
  49. package/themes/crew-gruvbox-light.json +90 -0
  50. package/themes/crew-nord.json +85 -0
  51. package/themes/crew-one-dark.json +89 -0
  52. package/themes/crew-solarized-dark.json +90 -0
  53. package/themes/crew-solarized-light.json +92 -0
  54. package/themes/crew-tokyo-night.json +85 -0
  55. package/src/runtime/budget-tracker.ts +0 -354
  56. package/src/state/memory-store.ts +0 -244
@@ -4,6 +4,11 @@ import { configPatchFromConfig } from "../team-tool/config-patch.ts";
4
4
  import { result } from "../team-tool/context.ts";
5
5
  import type { PiTeamsToolResult } from "../tool-result.ts";
6
6
  import { suggestConfigKey } from "../../config/suggestions.ts";
7
+ import {
8
+ formatThemesListing,
9
+ discoverPiThemes,
10
+ setPiTheme,
11
+ } from "../../ui/theme-discovery.ts";
7
12
 
8
13
  // ---------------------------------------------------------------------------
9
14
  // Effective defaults — values used when config key is not set
@@ -244,6 +249,8 @@ const USAGE = [
244
249
  " json Show full effective config as JSON",
245
250
  " schema Show all known config keys (schema reference)",
246
251
  " paths Show config file paths (user + project)",
252
+ " themes Browse theme gallery (Pi UI themes)",
253
+ " theme <name> Switch the Pi UI theme (applies live, no restart)",
247
254
  " get <key> Get a specific config value",
248
255
  " set <key> <value> Set a config value",
249
256
  " unset <key> Remove a config value",
@@ -319,6 +326,56 @@ export function handleSettings(params: { config?: Record<string, unknown> }, ctx
319
326
  return result(lines.join("\n"), { ...OK, path: loaded.path } as never);
320
327
  }
321
328
 
329
+ // team-settings themes — browse the theme gallery
330
+ if (args === "themes" || args === "theme-gallery") {
331
+ return result(formatThemesListing(), { ...OK } as never);
332
+ }
333
+
334
+ // team-settings theme <name> — switch the Pi UI theme
335
+ if (args === "theme" || args.startsWith("theme ")) {
336
+ const name = args === "theme" ? "" : args.slice(6).trim();
337
+ if (!name) {
338
+ const available = discoverPiThemes().map((t) => t.name).join(", ");
339
+ return result(
340
+ `Usage: team-settings theme <name>\n\nAvailable Pi themes: ${available}\n\nBrowse all: team-settings themes`,
341
+ { ...ERR },
342
+ true,
343
+ );
344
+ }
345
+ const available = discoverPiThemes();
346
+ const exists = available.some((t) => t.name === name);
347
+ if (!exists) {
348
+ const hint = available.map((t) => t.name).join(", ");
349
+ return result(
350
+ `Unknown Pi theme: ${name}\n\nAvailable: ${hint}\n\nCustom themes live in ~/.pi/agent/themes/<name>.json`,
351
+ { ...ERR },
352
+ true,
353
+ );
354
+ }
355
+ try {
356
+ const savedTo = setPiTheme(name);
357
+ return result(
358
+ [
359
+ `✓ Pi theme set to '${name}'`,
360
+ ` Written to: ${savedTo}`,
361
+ ` Applied live — no restart needed.`,
362
+ ].join("\n"),
363
+ { ...OK, theme: name } as never,
364
+ );
365
+ } catch (error) {
366
+ return result(error instanceof Error ? error.message : String(error), { ...ERR }, true);
367
+ }
368
+ }
369
+
370
+ // team-settings shiki <name> — removed (Shiki highlighting dropped)
371
+ if (args === "shiki" || args === "shiki-theme" || args.startsWith("shiki ") || args.startsWith("shiki-theme ")) {
372
+ return result(
373
+ `Shiki syntax highlighting has been removed from pi-crew.\nUse 'team-settings theme <name>' to switch the Pi UI theme, which drives code-block colors.`,
374
+ { ...ERR },
375
+ true,
376
+ );
377
+ }
378
+
322
379
  // team-settings scope [user|project]
323
380
  if (args === "scope" || args.startsWith("scope ")) {
324
381
  const scopeArg = args === "scope" ? "" : args.slice(6).trim();
@@ -1,7 +1,7 @@
1
1
  import type { TeamToolParamsValue } from "../../schema/team-tool-schema.ts";
2
2
  import { readEvents } from "../../state/event-log.ts";
3
3
  import { loadRunManifestById } from "../../state/state-store.ts";
4
- import { aggregateUsage, formatUsage } from "../../state/usage.ts";
4
+ import { aggregateUsage, formatUsage, formatCostReport } from "../../state/usage.ts";
5
5
  import type { PiTeamsToolResult } from "../tool-result.ts";
6
6
  import { locateRunCwd } from "../team-tool.ts";
7
7
  import { result, type TeamContext } from "./context.ts";
@@ -41,6 +41,9 @@ export function handleSummary(params: TeamToolParamsValue, ctx: TeamContext): Pi
41
41
  `Workflow: ${loaded.manifest.workflow ?? "(none)"}`,
42
42
  `Goal: ${loaded.manifest.goal}`,
43
43
  `Usage: ${formatUsage(usage)}`,
44
+ "",
45
+ formatCostReport(loaded.tasks),
46
+ "",
44
47
  "Tasks:",
45
48
  ...loaded.tasks.map((task) => `- ${task.id}: ${task.status} (${task.role} -> ${task.agent})${task.error ? ` - ${task.error}` : ""}`),
46
49
  ];
@@ -4,6 +4,7 @@ import { validateWorkflowForTeam } from "../../workflows/validate-workflow.ts";
4
4
  import type { TeamToolParamsValue } from "../../schema/team-tool-schema.ts";
5
5
  import type { PiTeamsToolResult } from "../tool-result.ts";
6
6
  import { result, type TeamContext } from "./context.ts";
7
+ import { composeSingleAgentPrompt } from "../../runtime/single-agent-compose.ts";
7
8
 
8
9
  export function handlePlan(params: TeamToolParamsValue, ctx: TeamContext): PiTeamsToolResult {
9
10
  const teamName = params.team ?? "default";
@@ -14,6 +15,12 @@ export function handlePlan(params: TeamToolParamsValue, ctx: TeamContext): PiTea
14
15
  if (!workflow) return result(`Workflow '${workflowName}' not found.`, { action: "plan", status: "error" }, true);
15
16
  const errors = validateWorkflowForTeam(workflow, team);
16
17
  if (errors.length > 0) return result([`Workflow '${workflow.name}' is not valid for team '${team.name}':`, ...errors.map((error) => `- ${error}`)].join("\n"), { action: "plan", status: "error" }, true);
17
- const lines = [`Team plan: ${team.name}`, `Workflow: ${workflow.name}`, `Goal: ${params.goal ?? params.task ?? "(not provided)"}`, "", "Steps:", ...workflow.steps.map((step, index) => `${index + 1}. ${step.id} [${step.role}]${step.dependsOn?.length ? ` after ${step.dependsOn.join(", ")}` : ""}`)];
18
+ const goal = params.goal ?? params.task ?? "(not provided)";
19
+ // ROADMAP T2.2: single-agent composition mode (cliff hedge).
20
+ if (params.singleAgent) {
21
+ const composed = composeSingleAgentPrompt(workflow, goal);
22
+ return result([`Single-agent plan for ${team.name} / ${workflow.name} (${composed.stepCount} phases composed into one sequential prompt):`, "", composed.prompt, "", "This prompt can be handed to a single agent to execute the entire workflow sequentially — pi-crew's cliff-resilient mode (survives single-agent domination)."].join("\n"), { action: "plan", status: "ok" });
23
+ }
24
+ const lines = [`Team plan: ${team.name}`, `Workflow: ${workflow.name}`, `Goal: ${goal}`, "", "Steps:", ...workflow.steps.map((step, index) => `${index + 1}. ${step.id} [${step.role}]${step.dependsOn?.length ? ` after ${step.dependsOn.join(", ")}` : ""}`)];
18
25
  return result(lines.join("\n"), { action: "plan", status: "ok" });
19
26
  }
@@ -254,6 +254,7 @@ export async function handleRun(params: TeamToolParamsValue, ctx: TeamContext):
254
254
  const asyncManifest = { ...effectiveManifest, async: { pid: spawned.pid, logPath: spawned.logPath, spawnedAt: new Date().toISOString() } };
255
255
  atomicWriteJson(paths.manifestPath, asyncManifest);
256
256
  void appendEventAsync(effectiveManifest.eventsPath, { type: "async.spawned", runId: effectiveManifest.runId, data: { pid: spawned.pid, logPath: spawned.logPath } });
257
+ ctx.onRunStarted?.(effectiveManifest.runId);
257
258
  scheduleBackgroundEarlyExitGuard(resolvedCtx.cwd, effectiveManifest.runId, spawned.pid, spawned.logPath);
258
259
  // Wait for the async run to complete and return actual results.
259
260
  try {
@@ -341,7 +342,7 @@ export async function handleRun(params: TeamToolParamsValue, ctx: TeamContext):
341
342
  }
342
343
 
343
344
  const runFailed = completed.manifest.status === "failed" || completed.manifest.status === "blocked";
344
- return result(lines.join("\n"), { action: "run", status: runFailed ? "error" : "ok", runId: completed.manifest.runId, artifactsRoot: completed.manifest.artifactsRoot }, runFailed);
345
+ return result(lines.join("\n"), { action: "run", status: runFailed ? "error" : "ok", runId: completed.manifest.runId, artifactsRoot: completed.manifest.artifactsRoot, metrics }, runFailed);
345
346
  } catch (waitError: unknown) {
346
347
  const errorMessage = waitError instanceof Error ? waitError.message : String(waitError);
347
348
  return result(
@@ -474,7 +475,7 @@ export async function handleRun(params: TeamToolParamsValue, ctx: TeamContext):
474
475
  }
475
476
 
476
477
  const runFailed = completed.manifest.status === "failed" || completed.manifest.status === "blocked";
477
- return result(lines.join("\n"), { action: "run", status: runFailed ? "error" : "ok", runId: completed.manifest.runId, artifactsRoot: completed.manifest.artifactsRoot }, runFailed);
478
+ return result(lines.join("\n"), { action: "run", status: runFailed ? "error" : "ok", runId: completed.manifest.runId, artifactsRoot: completed.manifest.artifactsRoot, metrics }, runFailed);
478
479
  } catch (waitError: unknown) {
479
480
  const errorMessage = waitError instanceof Error ? waitError.message : String(waitError);
480
481
  return result(
@@ -514,5 +515,5 @@ export async function handleRun(params: TeamToolParamsValue, ctx: TeamContext):
514
515
  ? "Experimental live-session worker execution was enabled."
515
516
  : "Safe scaffold mode: child Pi workers were not launched because runtime.mode=scaffold or executeWorkers=false was configured.",
516
517
  ].join("\n");
517
- return result(text, { action: "run", status: executed.manifest.status === "failed" ? "error" : "ok", runId: executed.manifest.runId, artifactsRoot: executed.manifest.artifactsRoot }, executed.manifest.status === "failed");
518
+ return result(text, { action: "run", status: executed.manifest.status === "failed" ? "error" : "ok", runId: executed.manifest.runId, artifactsRoot: executed.manifest.artifactsRoot, metrics: collectRunMetrics(resolvedCtx.cwd, executed.manifest.runId) }, executed.manifest.status === "failed");
518
519
  }
@@ -10,6 +10,8 @@ export interface TeamToolDetails {
10
10
  resumedIds?: string[];
11
11
  retriedTaskIds?: string[];
12
12
  mailboxIds?: string[];
13
+ /** Run metrics for compact display in TUI tool result rendering. */
14
+ metrics?: { taskCount?: number; completedCount?: number; totalTokens?: number; totalCost?: number; durationMs?: number; consistencyScore?: number };
13
15
  /** Structured data for programmatic consumption (e.g. TUI widgets). */
14
16
  data?: Record<string, unknown>;
15
17
  }
@@ -51,6 +51,10 @@ const MAX_QUEUE_SIZE = 100;
51
51
  export class IntercomQueue {
52
52
  private pending = new Map<string, PendingMessage>();
53
53
  private queue: IntercomMessage[] = [];
54
+ /** Monotonic sequence counter — guarantees unique IDs even when many
55
+ * messages enqueue within the same millisecond (avoids Date.now()
56
+ * collision + weak Math.random birthday-paradox flakes under load). */
57
+ private seq = 0;
54
58
 
55
59
  /**
56
60
  * Enqueue a message and return a promise that resolves when the
@@ -63,7 +67,7 @@ export class IntercomQueue {
63
67
  if (firstKey) this.evict(firstKey, "queue_full");
64
68
  }
65
69
 
66
- const id = `icm-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 6)}`;
70
+ const id = `icm-${Date.now().toString(36)}-${(this.seq++).toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
67
71
 
68
72
  return new Promise<IntercomResponse>((resolve) => {
69
73
  const entry: PendingMessage = { message, id, resolve };