pi-crew 0.11.0 → 0.11.2
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 +155 -9
- package/README.md +161 -1037
- package/agents/verifier.md +18 -7
- package/dist/index.mjs +744 -90644
- package/docs/README.md +57 -46
- package/docs/architecture.md +87 -33
- package/docs/commands-reference.md +9 -5
- package/docs/troubleshooting.md +3 -2
- package/package.json +1 -3
- package/schema.json +39 -0
- package/skills/real-test-pi-crew/REPORT-TEMPLATE.md +2 -0
- package/skills/real-test-pi-crew/SKILL.md +371 -34
- package/src/agents/agent-config.ts +1 -1
- package/src/agents/discover-agents.ts +1 -1
- package/src/config/config-validation.ts +15 -2
- package/src/config/config.ts +47 -13
- package/src/config/defaults.ts +0 -1
- package/src/config/env-vars.ts +35 -0
- package/src/config/types.ts +19 -5
- package/src/errors.ts +2 -2
- package/src/extension/async-notifier.ts +23 -0
- package/src/extension/crew-vibes/config.ts +0 -21
- package/src/extension/crew-vibes/index.ts +0 -2
- package/src/extension/crew-vibes/render.ts +1 -50
- package/src/extension/help.ts +21 -12
- package/src/extension/knowledge-injection.ts +2 -1
- package/src/extension/management.ts +8 -3
- package/src/extension/notification-sink.ts +17 -0
- package/src/extension/register.ts +7 -2
- package/src/extension/registration/command-utils.ts +28 -2
- package/src/extension/registration/commands/dashboard.ts +11 -1
- package/src/extension/registration/commands/manage.ts +36 -19
- package/src/extension/registration/commands/run.ts +24 -2
- package/src/extension/registration/commands/shared.ts +23 -1
- package/src/extension/registration/commands/status.ts +25 -2
- package/src/extension/registration/context-builder.ts +8 -2
- package/src/extension/registration/health-notify-policy.ts +100 -0
- package/src/extension/registration/lazy-configurers.ts +35 -0
- package/src/extension/registration/lifecycle-handlers.ts +91 -30
- package/src/extension/registration/lifecycle.ts +75 -10
- package/src/extension/registration/observability.ts +98 -35
- package/src/extension/registration/registration-types.ts +7 -5
- package/src/extension/registration/runtime-cleanup.ts +9 -3
- package/src/extension/registration/subagent-helpers.ts +38 -0
- package/src/extension/registration/subagent-tools.ts +16 -6
- package/src/extension/registration/team-tool.ts +10 -3
- package/src/extension/registration/terminal-status-wiring.ts +172 -0
- package/src/extension/registration/viewers.ts +6 -0
- package/src/extension/registration/wire-cross-extension.ts +28 -0
- package/src/extension/run-compare.ts +220 -0
- package/src/extension/run-export.ts +37 -5
- package/src/extension/run-maintenance.ts +155 -5
- package/src/extension/team-tool/dispatch/index.ts +3 -2
- package/src/extension/team-tool/dispatch/manage.ts +5 -2
- package/src/extension/team-tool/goal.ts +4 -1
- package/src/extension/team-tool/handle-settings.ts +33 -4
- package/src/extension/team-tool/health-monitor.ts +21 -7
- package/src/extension/team-tool/lifecycle-actions.ts +49 -1
- package/src/extension/team-tool/plan.ts +10 -0
- package/src/extension/team-tool/routing-hint.ts +63 -0
- package/src/extension/team-tool/status.ts +4 -0
- package/src/extension/team-tool.ts +52 -6
- package/src/extension/webhook-notify.ts +382 -0
- package/src/observability/metric-sink.ts +12 -2
- package/src/prompt/prompt-runtime.ts +82 -31
- package/src/prompt/worker-events-channel.ts +12 -0
- package/src/runtime/README.md +1 -1
- package/src/runtime/async-runner.ts +87 -1
- package/src/runtime/background-runner.ts +313 -234
- package/src/runtime/broker/crew-broker.ts +17 -11
- package/src/runtime/broker/delegate/shadow-lifecycle.ts +92 -0
- package/src/runtime/broker/wait-status-cache.ts +1 -1
- package/src/runtime/child-pi/child-pi-timers.ts +1 -1
- package/src/runtime/child-pi/mock-fixtures.ts +48 -0
- package/src/runtime/crew-agent-records.ts +337 -45
- package/src/runtime/deadletter.ts +43 -1
- package/src/runtime/delegate-spawn.ts +5 -1
- package/src/runtime/dispatch-batch.ts +72 -5
- package/src/runtime/goal-workflow/goal-loop-runner.ts +73 -4
- package/src/runtime/heartbeat/heartbeat-watcher.ts +7 -0
- package/src/runtime/model/model-fallback.ts +21 -1
- package/src/runtime/model/pi-args.ts +8 -10
- package/src/runtime/recovery/crash-recovery.ts +25 -1
- package/src/runtime/run-worker.ts +12 -1
- package/src/runtime/scheduling/global-worker-cap.ts +13 -6
- package/src/runtime/scheduling/run-coalesced-task-group.ts +27 -1
- package/src/runtime/scheduling/scheduler.ts +49 -13
- package/src/runtime/scheduling/semaphore.ts +148 -20
- package/src/runtime/scratchpad/README.md +1 -1
- package/src/runtime/scratchpad/protocol.ts +1 -1
- package/src/runtime/settings-store.ts +1 -1
- package/src/runtime/skill-instructions.ts +22 -0
- package/src/runtime/stale-reconciler.ts +85 -13
- package/src/runtime/task-display.ts +1 -1
- package/src/runtime/task-runner/pre-execution.ts +26 -2
- package/src/runtime/task-runner/prompt-builder.ts +142 -45
- package/src/runtime/task-runner.ts +21 -1
- package/src/runtime/team-runner.ts +38 -1
- package/src/runtime/workspace-lock.ts +4 -1
- package/src/schema/config-schema.ts +18 -0
- package/src/schema/team-tool-schema.ts +17 -0
- package/src/state/atomic-write.ts +53 -0
- package/src/state/contracts.ts +109 -0
- package/src/state/coordination/locks.ts +191 -33
- package/src/state/coordination/mailbox.ts +140 -15
- package/src/state/crew-init.ts +87 -12
- package/src/state/event-log/cursor.ts +37 -1
- package/src/state/event-log/event-log-rotation.ts +72 -7
- package/src/state/stores/active-run-registry.ts +13 -1
- package/src/state/stores/state-store.ts +112 -22
- package/src/state/types.ts +4 -0
- package/src/ui/adaptive-card.ts +65 -0
- package/src/ui/agents-jobs-browser.ts +70 -64
- package/src/ui/card-colors.ts +36 -7
- package/src/ui/dashboard-panes/agents-pane.ts +55 -14
- package/src/ui/dashboard-panes/cancellation-pane.ts +0 -42
- package/src/ui/dashboard-panes/health-pane.ts +7 -5
- package/src/ui/dashboard-panes/mailbox-pane.ts +22 -6
- package/src/ui/dashboard-panes/metrics-pane.ts +15 -7
- package/src/ui/dashboard-panes/pane-theme.ts +21 -0
- package/src/ui/dashboard-panes/plan-pane.ts +63 -30
- package/src/ui/dashboard-panes/progress-pane.ts +3 -2
- package/src/ui/dashboard-panes/schedules-pane.ts +44 -21
- package/src/ui/dashboard-panes/transcript-pane.ts +11 -5
- package/src/ui/dwf-phase-display.ts +3 -20
- package/src/ui/format-helpers.ts +22 -0
- package/src/ui/heartbeat-aggregator.ts +34 -0
- package/src/ui/inline-panel/crew-editor.ts +13 -3
- package/src/ui/inline-panel/index.ts +60 -4
- package/src/ui/keybinding-map.ts +251 -35
- package/src/ui/live-conversation-overlay.ts +180 -47
- package/src/ui/live-run-sidebar.ts +134 -55
- package/src/ui/mascot.ts +32 -16
- package/src/ui/overlays/agent-picker-overlay.ts +81 -26
- package/src/ui/overlays/confirm-overlay.ts +55 -29
- package/src/ui/overlays/help-overlay.ts +108 -53
- package/src/ui/overlays/mailbox-compose-overlay.ts +89 -50
- package/src/ui/overlays/mailbox-detail-overlay.ts +137 -57
- package/src/ui/powerbar-publisher.ts +0 -1
- package/src/ui/rail.ts +333 -0
- package/src/ui/run-dashboard.ts +193 -79
- package/src/ui/run-snapshot-cache.ts +18 -1
- package/src/ui/settings-overlay.ts +81 -39
- package/src/ui/spinner.ts +26 -2
- package/src/ui/terminal-status.ts +7 -1
- package/src/ui/theme-adapter.ts +0 -45
- package/src/ui/theme-discovery.ts +12 -6
- package/src/ui/tool-progress-formatter.ts +128 -9
- package/src/ui/tool-renderers/brief-mode.ts +10 -67
- package/src/ui/tool-renderers/index.ts +374 -523
- package/src/ui/transcript-viewer.ts +30 -12
- package/src/ui/widget/index.ts +32 -52
- package/src/ui/widget/task-list.ts +64 -32
- package/src/ui/widget/widget-formatters.ts +3 -402
- package/src/ui/widget/widget-model.ts +28 -7
- package/src/ui/widget/widget-renderer.ts +201 -128
- package/src/ui/widget/widget-types.ts +0 -2
- package/src/utils/incremental-reader.ts +11 -3
- package/src/utils/paths.ts +94 -12
- package/src/utils/project-markers.ts +40 -0
- package/src/utils/visual.ts +0 -4
- package/src/worktree/worktree-manager.ts +206 -26
- package/workflows/distill.workflow.md +3 -3
- package/workflows/fast-fix.workflow.md +1 -1
- package/workflows/plan-execute.workflow.md +1 -1
- package/workflows/review.workflow.md +1 -1
- package/workflows/strict-fast-fix.workflow.md +1 -1
- package/docs/migration-v0.4-v0.5.md +0 -208
- package/docs/runtime-flow.md +0 -148
- package/src/extension/crew-vibes/figures.ts +0 -22
- package/src/extension/crew-vibes/font-detect.ts +0 -71
- package/src/ui/dynamic-border.ts +0 -35
- package/src/ui/loaders.ts +0 -6
- package/src/ui/overlay-stack.ts +0 -148
|
@@ -5,8 +5,8 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import { truncateToWidth, visibleWidth } from "../utils/visual.ts";
|
|
8
|
-
import { DynamicCrewBorder } from "./dynamic-border.ts";
|
|
9
8
|
import { keyOf, matchesKey } from "./key-utils.ts";
|
|
9
|
+
import { CURSOR, canopyLine, formatHint, overflowHint, RAIL, railLine } from "./rail.ts";
|
|
10
10
|
import type { CrewTheme } from "./theme-adapter.ts";
|
|
11
11
|
import { discoverPiThemes, getActivePiTheme } from "./theme-discovery.ts";
|
|
12
12
|
|
|
@@ -509,19 +509,29 @@ class SelectSubmenu {
|
|
|
509
509
|
const end = Math.min(start + this.maxVisible, this.items.length);
|
|
510
510
|
const needsScroll = this.items.length > this.maxVisible;
|
|
511
511
|
if (needsScroll && start > 0) {
|
|
512
|
-
lines.push(
|
|
512
|
+
lines.push(` ${overflowHint(start, 0, this.theme)}`);
|
|
513
513
|
}
|
|
514
514
|
for (let i = start; i < end; i++) {
|
|
515
515
|
const isSelected = i === this.selectedIndex;
|
|
516
|
-
|
|
516
|
+
// `›` is the ONLY selection marker (RAIL): `→`-as-cursor is retired.
|
|
517
|
+
const prefix = isSelected ? ` ${CURSOR} ` : " ";
|
|
517
518
|
const line = `${prefix}${this.items[i]}`;
|
|
518
519
|
lines.push(isSelected ? (this.theme.inverse?.(line) ?? line) : line);
|
|
519
520
|
}
|
|
520
521
|
if (needsScroll && end < this.items.length) {
|
|
521
|
-
lines.push(
|
|
522
|
+
lines.push(` ${overflowHint(0, this.items.length - end, this.theme)}`);
|
|
522
523
|
}
|
|
523
524
|
lines.push("");
|
|
524
|
-
lines.push(
|
|
525
|
+
lines.push(
|
|
526
|
+
this.theme.fg(
|
|
527
|
+
"dim",
|
|
528
|
+
formatHint([
|
|
529
|
+
[["up", "down"], "navigate"],
|
|
530
|
+
["enter", "to select"],
|
|
531
|
+
["esc", "to go back"],
|
|
532
|
+
]),
|
|
533
|
+
),
|
|
534
|
+
);
|
|
525
535
|
return lines;
|
|
526
536
|
}
|
|
527
537
|
|
|
@@ -593,7 +603,16 @@ class TextinputSubmenu {
|
|
|
593
603
|
lines.push("");
|
|
594
604
|
lines.push(` ${this.buffer}█`);
|
|
595
605
|
lines.push("");
|
|
596
|
-
lines.push(
|
|
606
|
+
lines.push(
|
|
607
|
+
this.theme.fg(
|
|
608
|
+
"dim",
|
|
609
|
+
formatHint([
|
|
610
|
+
["enter", "to save"],
|
|
611
|
+
["esc", "to cancel"],
|
|
612
|
+
["clear", "to unset"],
|
|
613
|
+
]),
|
|
614
|
+
),
|
|
615
|
+
);
|
|
597
616
|
return lines;
|
|
598
617
|
}
|
|
599
618
|
|
|
@@ -682,7 +701,7 @@ class AgentOverridesSubmenu {
|
|
|
682
701
|
const thinkingPart = thinking ? `thinking=${thinking}` : "";
|
|
683
702
|
const valueParts = [modelPart, thinkingPart].filter(Boolean).join(", ");
|
|
684
703
|
const valueText = valueParts || this.theme.fg("dim", "(default)");
|
|
685
|
-
const prefix = isSelected ?
|
|
704
|
+
const prefix = isSelected ? ` ${CURSOR} ` : " ";
|
|
686
705
|
const line = `${prefix}${label} ${valueText}`;
|
|
687
706
|
lines.push(
|
|
688
707
|
isSelected
|
|
@@ -691,19 +710,38 @@ class AgentOverridesSubmenu {
|
|
|
691
710
|
);
|
|
692
711
|
}
|
|
693
712
|
lines.push("");
|
|
694
|
-
lines.push(
|
|
713
|
+
lines.push(
|
|
714
|
+
this.theme.fg(
|
|
715
|
+
"dim",
|
|
716
|
+
formatHint([
|
|
717
|
+
["enter", "to edit model"],
|
|
718
|
+
["e", "to edit thinking"],
|
|
719
|
+
["esc", "to go back"],
|
|
720
|
+
]),
|
|
721
|
+
),
|
|
722
|
+
);
|
|
695
723
|
return lines;
|
|
696
724
|
}
|
|
697
725
|
|
|
698
726
|
private renderEdit(width: number): string[] {
|
|
699
|
-
|
|
727
|
+
// GUARD (M4 §4): an out-of-range cursor used to print `Edit undefined model`.
|
|
728
|
+
const agent = this.agents[this.selectedIndex] ?? "?";
|
|
700
729
|
const field = this.editField === "model" ? "model" : "thinking";
|
|
701
730
|
const lines: string[] = [];
|
|
702
731
|
lines.push(this.theme.bold(this.theme.fg("accent", `Edit ${agent} ${field}`)));
|
|
703
732
|
lines.push("");
|
|
704
733
|
lines.push(` ${this.editBuffer}█`);
|
|
705
734
|
lines.push("");
|
|
706
|
-
lines.push(
|
|
735
|
+
lines.push(
|
|
736
|
+
this.theme.fg(
|
|
737
|
+
"dim",
|
|
738
|
+
formatHint([
|
|
739
|
+
["enter", "to save"],
|
|
740
|
+
["clear", "to unset"],
|
|
741
|
+
["esc", "to cancel"],
|
|
742
|
+
]),
|
|
743
|
+
),
|
|
744
|
+
);
|
|
707
745
|
return lines;
|
|
708
746
|
}
|
|
709
747
|
|
|
@@ -801,33 +839,30 @@ class SettingsOverlay {
|
|
|
801
839
|
}
|
|
802
840
|
|
|
803
841
|
render(width: number): string[] {
|
|
804
|
-
//
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
const
|
|
809
|
-
const
|
|
810
|
-
const row = (text: string) => `│ ${padToWidth(truncateToWidth(text, innerWidth - 1), innerWidth - 1)}│`;
|
|
842
|
+
// RAIL frame (M4, 2026-09-16) — the rounded box, the `├ ┤` rule and the
|
|
843
|
+
// `│` tab separator are retired: canopy `┏ SETTINGS ▸ pi-crew`, `┃` body
|
|
844
|
+
// rows (tab bar + content) and a `┗ <hint>` cap. railLine owns the glyph +
|
|
845
|
+
// space and the pad/truncate to `budget`, so no line can exceed `width`.
|
|
846
|
+
const budget = Math.max(0, width - 2);
|
|
847
|
+
const row = (text: string) => railLine(RAIL.body, "border", text, this.theme, budget);
|
|
811
848
|
|
|
812
849
|
const lines: string[] = [];
|
|
813
850
|
|
|
814
|
-
// ── Title bar ──
|
|
815
|
-
lines.push(
|
|
816
|
-
lines.push(row(`${fg("accent", "▐")} ${this.theme.bold("pi-crew Settings")}`));
|
|
851
|
+
// ── Title bar (canopy) ──
|
|
852
|
+
lines.push(canopyLine({ word: "SETTINGS", subject: "pi-crew", theme: this.theme, budget }));
|
|
817
853
|
|
|
818
|
-
// ── Tab bar ──
|
|
819
|
-
|
|
820
|
-
lines.push(row(tabLine));
|
|
821
|
-
lines.push(border("├", "┤"));
|
|
854
|
+
// ── Tab bar (a rail body row; tabs joined by ` · `, never `│`) ──
|
|
855
|
+
lines.push(row(this.renderTabBarContent(budget)));
|
|
822
856
|
|
|
823
857
|
// ── Content ──
|
|
824
|
-
const
|
|
858
|
+
const bodyBudget = Math.max(0, budget - 2);
|
|
859
|
+
const content = this.submenu ? this.renderSubmenuContent(bodyBudget) : this.renderSettingsContent(bodyBudget);
|
|
825
860
|
for (const line of content) {
|
|
826
|
-
lines.push(row(` ${truncateToWidth(line,
|
|
861
|
+
lines.push(row(` ${truncateToWidth(line, Math.max(0, budget - 1))}`));
|
|
827
862
|
}
|
|
828
863
|
|
|
829
|
-
// ──
|
|
830
|
-
lines.push(border("
|
|
864
|
+
// ── Cap (close action LAST) ──
|
|
865
|
+
lines.push(railLine(RAIL.close, "border", formatHint([["esc", "close"]]), this.theme, budget));
|
|
831
866
|
|
|
832
867
|
return lines;
|
|
833
868
|
}
|
|
@@ -839,7 +874,7 @@ class SettingsOverlay {
|
|
|
839
874
|
const text = `${tab.icon} ${tab.label}`;
|
|
840
875
|
parts.push(isActive ? this.theme.bold(this.theme.fg("accent", text)) : this.theme.fg("dim", text));
|
|
841
876
|
}
|
|
842
|
-
return parts.join(
|
|
877
|
+
return truncateToWidth(parts.join(` ${this.theme.fg("dim", "·")} `), innerWidth);
|
|
843
878
|
}
|
|
844
879
|
|
|
845
880
|
private renderSettingsContent(innerWidth: number): string[] {
|
|
@@ -864,7 +899,8 @@ class SettingsOverlay {
|
|
|
864
899
|
const valueStr = formatValue(effective, def.id);
|
|
865
900
|
const suffix = isDefault && (effective !== undefined || EFFECTIVE_DEFAULTS[def.id] !== undefined) ? " (default)" : "";
|
|
866
901
|
|
|
867
|
-
|
|
902
|
+
// Cursor: `›` is the ONLY selection marker (RAIL).
|
|
903
|
+
const prefix = isSelected ? ` ${CURSOR} ` : " ";
|
|
868
904
|
const labelPad = padToWidth(def.label, maxLabelWidth);
|
|
869
905
|
const valueMax = innerWidth - maxLabelWidth - 6 - prefix.length - suffix.length;
|
|
870
906
|
const valueText = truncateToWidth(valueStr, Math.max(10, valueMax));
|
|
@@ -877,15 +913,12 @@ class SettingsOverlay {
|
|
|
877
913
|
}
|
|
878
914
|
}
|
|
879
915
|
|
|
880
|
-
// Scroll indicator
|
|
916
|
+
// Scroll indicator — RAIL overflow dialect (`▲ n above · ▼ m below`).
|
|
881
917
|
if (startIdx > 0 || endIdx < settings.length) {
|
|
882
918
|
const remaining = settings.length - endIdx;
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
if (parts.length > 0) {
|
|
887
|
-
lines.push(this.theme.fg("dim", ` (${this.selectedIndex + 1}/${settings.length}) ${parts.join(" ")}`));
|
|
888
|
-
}
|
|
919
|
+
lines.push(
|
|
920
|
+
`${this.theme.fg("dim", ` (${this.selectedIndex + 1}/${settings.length})`)} ${overflowHint(startIdx, remaining, this.theme)}`,
|
|
921
|
+
);
|
|
889
922
|
}
|
|
890
923
|
|
|
891
924
|
// Description
|
|
@@ -895,9 +928,18 @@ class SettingsOverlay {
|
|
|
895
928
|
lines.push(this.theme.fg("muted", ` ${selectedDef.description}`));
|
|
896
929
|
}
|
|
897
930
|
|
|
898
|
-
// Hints
|
|
931
|
+
// Hints — one format (`formatHint`); the close action lives in the cap.
|
|
899
932
|
lines.push("");
|
|
900
|
-
lines.push(
|
|
933
|
+
lines.push(
|
|
934
|
+
this.theme.fg(
|
|
935
|
+
"dim",
|
|
936
|
+
` ${formatHint([
|
|
937
|
+
[["up", "down"], "Navigate"],
|
|
938
|
+
[["enter", "space"], "Change"],
|
|
939
|
+
["tab", "switch"],
|
|
940
|
+
])}`,
|
|
941
|
+
),
|
|
942
|
+
);
|
|
901
943
|
|
|
902
944
|
return lines;
|
|
903
945
|
}
|
package/src/ui/spinner.ts
CHANGED
|
@@ -1,7 +1,31 @@
|
|
|
1
1
|
export const SUBAGENT_SPINNER_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"] as const;
|
|
2
2
|
export const SUBAGENT_SPINNER_FRAME_MS = 160;
|
|
3
3
|
|
|
4
|
-
|
|
4
|
+
// Clock source indirection (2026-09-22): spinner phase is derived from wall
|
|
5
|
+
// time at ~30 call sites that don't thread an injectable `now` through their
|
|
6
|
+
// render signatures. Deterministic consumers (docs/ui-samples/capture.ts,
|
|
7
|
+
// tests) swap the source once at startup instead of refactoring every
|
|
8
|
+
// signature; production keeps the real clock. Same pattern as the `__test__`
|
|
9
|
+
// hooks in state-store.
|
|
10
|
+
let spinnerClockSource: () => number = Date.now;
|
|
11
|
+
|
|
12
|
+
/** @internal — swap the spinner clock source. Returns the previous source
|
|
13
|
+
* (pass it back to restore). Production default is Date.now. */
|
|
14
|
+
export function __setSpinnerClockSource(source: () => number): () => number {
|
|
15
|
+
const previous = spinnerClockSource;
|
|
16
|
+
spinnerClockSource = source;
|
|
17
|
+
return previous;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** The spinner clock as a shared read: elapsed-time fallbacks that have no
|
|
21
|
+
* injected `now` (tool-progress, record durations) read it here so deterministic
|
|
22
|
+
* captures pin BOTH spinner phase and elapsed text with one hook. Production
|
|
23
|
+
* default stays Date.now(). */
|
|
24
|
+
export function spinnerClockNow(): number {
|
|
25
|
+
return spinnerClockSource();
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function spinnerBucket(now = spinnerClockSource(), frameMs = SUBAGENT_SPINNER_FRAME_MS): number {
|
|
5
29
|
return Math.floor(now / Math.max(1, frameMs));
|
|
6
30
|
}
|
|
7
31
|
|
|
@@ -11,7 +35,7 @@ function hashKey(key: string): number {
|
|
|
11
35
|
return hash;
|
|
12
36
|
}
|
|
13
37
|
|
|
14
|
-
export function spinnerFrame(key = "", now =
|
|
38
|
+
export function spinnerFrame(key = "", now = spinnerClockSource()): string {
|
|
15
39
|
const offset = key ? hashKey(key) % SUBAGENT_SPINNER_FRAMES.length : 0;
|
|
16
40
|
return SUBAGENT_SPINNER_FRAMES[(spinnerBucket(now) + offset) % SUBAGENT_SPINNER_FRAMES.length] ?? SUBAGENT_SPINNER_FRAMES[0];
|
|
17
41
|
}
|
|
@@ -26,6 +26,12 @@
|
|
|
26
26
|
* - run just completed → Ghostty state 1/100 (green flash), schedule clear.
|
|
27
27
|
* - idle (no active runs) → restore pi's natural title, Ghostty state 0.
|
|
28
28
|
*
|
|
29
|
+
* M3-1 (2026-09-15): the wiring now exists — see
|
|
30
|
+
* `src/extension/registration/terminal-status-wiring.ts` (installed from
|
|
31
|
+
* `register.ts`). From v0.8.3 (819d71ad) until this change the controller was
|
|
32
|
+
* constructed only by its unit tests: `ctx.terminalStatus` was never assigned,
|
|
33
|
+
* so neither channel ever ran (UI-AUDIT P0-2/P1-1).
|
|
34
|
+
*
|
|
29
35
|
* @module terminal-status
|
|
30
36
|
*/
|
|
31
37
|
|
|
@@ -61,7 +67,7 @@ const MAX_TITLE_ROLES = 3;
|
|
|
61
67
|
const IDLE_REASSERT_START_MS = 200;
|
|
62
68
|
const IDLE_REASSERT_MAX_MS = 5000;
|
|
63
69
|
/** How long the green completion flash stays before clearing. */
|
|
64
|
-
const COMPLETE_FLASH_MS = 1500;
|
|
70
|
+
export const COMPLETE_FLASH_MS = 1500;
|
|
65
71
|
|
|
66
72
|
/** Injected for tests (defaults write to the real /dev/tty). */
|
|
67
73
|
export type GhosttyWriter = (seq: string) => void;
|
package/src/ui/theme-adapter.ts
CHANGED
|
@@ -49,51 +49,6 @@ export type CrewThemeColor =
|
|
|
49
49
|
|
|
50
50
|
export type CrewThemeBg = "selectedBg" | "userMessageBg" | "customMessageBg" | "toolPendingBg" | "toolSuccessBg" | "toolErrorBg";
|
|
51
51
|
|
|
52
|
-
/** ANSI fallback values for theme color slots when the active theme doesn't define them. */
|
|
53
|
-
export const THEME_COLOR_FALLBACKS: Record<CrewThemeColor, string> = {
|
|
54
|
-
accent: "\x1b[36m",
|
|
55
|
-
border: "\x1b[38;5;240m",
|
|
56
|
-
borderAccent: "\x1b[35m",
|
|
57
|
-
borderMuted: "\x1b[38;5;236m",
|
|
58
|
-
success: "\x1b[32m",
|
|
59
|
-
error: "\x1b[31m",
|
|
60
|
-
warning: "\x1b[33m",
|
|
61
|
-
muted: "\x1b[38;5;245m",
|
|
62
|
-
dim: "\x1b[38;5;240m",
|
|
63
|
-
text: "\x1b[39m",
|
|
64
|
-
thinkingText: "\x1b[38;5;245m",
|
|
65
|
-
toolTitle: "\x1b[36m",
|
|
66
|
-
toolOutput: "\x1b[38;5;245m",
|
|
67
|
-
toolDiffAdded: "\x1b[32m",
|
|
68
|
-
toolDiffRemoved: "\x1b[31m",
|
|
69
|
-
toolDiffContext: "\x1b[38;5;245m",
|
|
70
|
-
mdHeading: "\x1b[33m",
|
|
71
|
-
mdLink: "\x1b[35m",
|
|
72
|
-
mdCode: "\x1b[32m",
|
|
73
|
-
mdCodeBlock: "\x1b[39m",
|
|
74
|
-
mdQuote: "\x1b[38;5;245m",
|
|
75
|
-
mdHr: "\x1b[38;5;240m",
|
|
76
|
-
mdListBullet: "\x1b[36m",
|
|
77
|
-
syntaxKeyword: "\x1b[35m",
|
|
78
|
-
syntaxString: "\x1b[32m",
|
|
79
|
-
syntaxNumber: "\x1b[33m",
|
|
80
|
-
syntaxComment: "\x1b[38;5;245m",
|
|
81
|
-
syntaxFunction: "\x1b[36m",
|
|
82
|
-
syntaxVariable: "\x1b[39m",
|
|
83
|
-
syntaxType: "\x1b[35m",
|
|
84
|
-
syntaxOperator: "\x1b[35m",
|
|
85
|
-
syntaxPunctuation: "\x1b[35m",
|
|
86
|
-
userMessageText: "\x1b[39m",
|
|
87
|
-
customMessageLabel: "\x1b[35m",
|
|
88
|
-
thinkingOff: "\x1b[38;5;236m",
|
|
89
|
-
thinkingMinimal: "\x1b[38;5;245m",
|
|
90
|
-
thinkingLow: "\x1b[35m",
|
|
91
|
-
thinkingMedium: "\x1b[35m",
|
|
92
|
-
thinkingHigh: "\x1b[36m",
|
|
93
|
-
thinkingXhigh: "\x1b[35m",
|
|
94
|
-
bashMode: "\x1b[32m",
|
|
95
|
-
};
|
|
96
|
-
|
|
97
52
|
export interface CrewTheme {
|
|
98
53
|
fg(color: CrewThemeColor, text: string): string;
|
|
99
54
|
bg?(color: CrewThemeBg, text: string): string;
|
|
@@ -7,7 +7,19 @@
|
|
|
7
7
|
* - setPiTheme() to persist a choice in ~/.pi/agent/settings.json
|
|
8
8
|
*
|
|
9
9
|
* Wired into the `team-settings themes` / `theme` subcommands.
|
|
10
|
+
*
|
|
11
|
+
* P0-1 (2026-09-15): this module used bare `require("node:fs")` in three
|
|
12
|
+
* places. It is an ESM module, so `require` is undefined there and every
|
|
13
|
+
* filesystem probe threw a ReferenceError that the surrounding try/catch
|
|
14
|
+
* swallowed: `discoverPiThemes()` collapsed to the 2 builtins, and
|
|
15
|
+
* `getActivePiTheme()` always returned undefined (so no crew theme was
|
|
16
|
+
* selectable and the active theme was misreported). The fs access is now a
|
|
17
|
+
* static top-level import. `node:fs` is a builtin and this module is already
|
|
18
|
+
* eagerly reachable from handle-settings.ts / settings-overlay.ts, so this
|
|
19
|
+
* does not change lazy-load behaviour (check:lazy-imports only polices
|
|
20
|
+
* `await import(` without a `// LAZY:` marker).
|
|
10
21
|
*/
|
|
22
|
+
import * as fs from "node:fs";
|
|
11
23
|
|
|
12
24
|
export interface PiThemeInfo {
|
|
13
25
|
/** Theme name (filename stem or builtin id). */
|
|
@@ -72,8 +84,6 @@ export function discoverPiThemes(): PiThemeInfo[] {
|
|
|
72
84
|
// Custom themes from ~/.pi/agent/themes/
|
|
73
85
|
const dir = customThemesDir();
|
|
74
86
|
try {
|
|
75
|
-
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
76
|
-
const fs = require("node:fs");
|
|
77
87
|
if (dir && fs.existsSync(dir)) {
|
|
78
88
|
for (const file of fs.readdirSync(dir) as string[]) {
|
|
79
89
|
if (!file.endsWith(".json")) continue;
|
|
@@ -109,8 +119,6 @@ export function discoverPiThemes(): PiThemeInfo[] {
|
|
|
109
119
|
/** Read the currently active Pi theme from ~/.pi/agent/settings.json. */
|
|
110
120
|
export function getActivePiTheme(): string | undefined {
|
|
111
121
|
try {
|
|
112
|
-
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
113
|
-
const fs = require("node:fs");
|
|
114
122
|
const p = settingsPath();
|
|
115
123
|
if (!p || !fs.existsSync(p)) return undefined;
|
|
116
124
|
const json = JSON.parse(fs.readFileSync(p, "utf8"));
|
|
@@ -122,8 +130,6 @@ export function getActivePiTheme(): string | undefined {
|
|
|
122
130
|
|
|
123
131
|
/** Persist a Pi theme choice in ~/.pi/agent/settings.json. Returns the path or throws. */
|
|
124
132
|
export function setPiTheme(name: string): string {
|
|
125
|
-
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
126
|
-
const fs = require("node:fs");
|
|
127
133
|
const p = settingsPath();
|
|
128
134
|
if (!p) throw new Error("Could not determine settings path (no HOME).");
|
|
129
135
|
let settings: Record<string, unknown> = {};
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { CrewAgentRecord } from "../runtime/crew-agent-runtime.ts";
|
|
2
2
|
import type { TeamRunManifest, TeamTaskState } from "../state/types.ts";
|
|
3
|
+
import { spinnerClockNow } from "./spinner.ts";
|
|
3
4
|
|
|
4
5
|
export interface ToolProgressInput {
|
|
5
6
|
/** Subagent record id or synthetic agent label shown in the header. */
|
|
@@ -22,6 +23,119 @@ export interface ToolProgressInput {
|
|
|
22
23
|
|
|
23
24
|
const MAX_OUTPUT_LINE = 80;
|
|
24
25
|
|
|
26
|
+
// ── Producer↔consumer contract (audit P1-13) ───────────────────────────
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Every token that `formatCompactToolProgress` (producer) emits and that the
|
|
30
|
+
* `team`/`agent` tool renderers (consumer, `parseCompactToolProgress` below)
|
|
31
|
+
* look for. The two sides used to be coupled only by hand-written regexes, so a
|
|
32
|
+
* one-character format change made the streaming progress block silently empty.
|
|
33
|
+
* Both sides now read the tokens from here.
|
|
34
|
+
*/
|
|
35
|
+
export const PROGRESS_FORMAT = {
|
|
36
|
+
/** Header: `agent=<id> status=<status> elapsed=<n>s` (`agent` with no id). */
|
|
37
|
+
agentKey: "agent",
|
|
38
|
+
statusKey: "status",
|
|
39
|
+
elapsedKey: "elapsed",
|
|
40
|
+
/** Unit suffix for `elapsed=` (seconds). */
|
|
41
|
+
elapsedUnit: "s",
|
|
42
|
+
/** Task tally: `tasks <completed>/<total> done <status>=<n> …`. */
|
|
43
|
+
tasksKey: "tasks",
|
|
44
|
+
tallySeparator: "/",
|
|
45
|
+
doneWord: "done",
|
|
46
|
+
/** Active worker row: `<role>-><agent> turn=<n> tokens=<n>`. */
|
|
47
|
+
roleSeparator: "->",
|
|
48
|
+
turnKey: "turn",
|
|
49
|
+
tokensKey: "tokens",
|
|
50
|
+
/** Current tool row: `tool: <name> (#<n>)`. */
|
|
51
|
+
toolKey: "tool",
|
|
52
|
+
toolSeparator: ":",
|
|
53
|
+
/** Pending-run row: `run=<id> (starting)`. */
|
|
54
|
+
runKey: "run",
|
|
55
|
+
/** Failure row: `error: <text>`. */
|
|
56
|
+
errorKey: "error",
|
|
57
|
+
/** Fallback row before the run materializes. */
|
|
58
|
+
waitingLine: "waiting for run to start",
|
|
59
|
+
} as const;
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Structured image of a compact progress block. Consumed by the tool renderers;
|
|
63
|
+
* `null` from the parser means "nothing progress-like in this text".
|
|
64
|
+
*/
|
|
65
|
+
export interface ParsedToolProgress {
|
|
66
|
+
/** `elapsed=<n>s` in milliseconds (0 when absent). */
|
|
67
|
+
elapsedMs: number;
|
|
68
|
+
/** `status=<x>` value (note: the renderer only surfaces it when no task counts exist). */
|
|
69
|
+
status: string | null;
|
|
70
|
+
/** Completed task count from the tally row (null when absent). */
|
|
71
|
+
completed: number | null;
|
|
72
|
+
/** Total task count from the tally row (completed + remaining for the legacy bucket form). */
|
|
73
|
+
total: number | null;
|
|
74
|
+
/** `<role>/<agent>` of the active worker row, if present. */
|
|
75
|
+
activeAgent: string | null;
|
|
76
|
+
/** Current tool name from `tool: <name>`, if present. */
|
|
77
|
+
currentTool: string | null;
|
|
78
|
+
/** Turn count of the active worker row, if present. */
|
|
79
|
+
turns: number | null;
|
|
80
|
+
/** Token count of the active worker row, if present. */
|
|
81
|
+
tokens: number | null;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const ELAPSED_RE = new RegExp(`${PROGRESS_FORMAT.elapsedKey}=(\\d+)${PROGRESS_FORMAT.elapsedUnit}`);
|
|
85
|
+
const STATUS_RE = new RegExp(`${PROGRESS_FORMAT.statusKey}=(\\w+)`);
|
|
86
|
+
const TALLY_RE = new RegExp(
|
|
87
|
+
`${PROGRESS_FORMAT.tasksKey}\\s+(\\d+)${PROGRESS_FORMAT.tallySeparator}(\\d+)\\s+${PROGRESS_FORMAT.doneWord}\\b`,
|
|
88
|
+
);
|
|
89
|
+
// Legacy bucket form (`tasks completed=2 running=1`) — kept for older/foreign
|
|
90
|
+
// producers; the tally form above wins when both are present.
|
|
91
|
+
const BUCKETS_RE = new RegExp(
|
|
92
|
+
`${PROGRESS_FORMAT.tasksKey}[^\\n]*(?:completed|${PROGRESS_FORMAT.doneWord})=(\\d+)[^\\n]*(?:running|waiting|queued)=(\\d+)`,
|
|
93
|
+
);
|
|
94
|
+
const AGENT_ROW_RE = new RegExp(`\\s+(\\w+)${PROGRESS_FORMAT.roleSeparator}(\\w+)\\s+${PROGRESS_FORMAT.turnKey}=`);
|
|
95
|
+
const TURN_RE = new RegExp(`${PROGRESS_FORMAT.turnKey}=(\\d+)`);
|
|
96
|
+
const TOKENS_RE = new RegExp(`${PROGRESS_FORMAT.tokensKey}=(\\d+)`);
|
|
97
|
+
const TOOL_ROW_RE = new RegExp(`${PROGRESS_FORMAT.toolKey}${PROGRESS_FORMAT.toolSeparator}\\s+(\\S+)`);
|
|
98
|
+
|
|
99
|
+
/** Parse a compact progress block back into structured fields (see PROGRESS_FORMAT). */
|
|
100
|
+
export function parseCompactToolProgress(text: string): ParsedToolProgress | null {
|
|
101
|
+
if (!text) return null;
|
|
102
|
+
|
|
103
|
+
const elapsedMatch = ELAPSED_RE.exec(text);
|
|
104
|
+
const elapsedMs = elapsedMatch ? Number.parseInt(elapsedMatch[1]!, 10) * 1000 : 0;
|
|
105
|
+
|
|
106
|
+
let completed: number | null = null;
|
|
107
|
+
let total: number | null = null;
|
|
108
|
+
const tally = TALLY_RE.exec(text);
|
|
109
|
+
if (tally) {
|
|
110
|
+
completed = Number.parseInt(tally[1]!, 10);
|
|
111
|
+
total = Number.parseInt(tally[2]!, 10);
|
|
112
|
+
} else {
|
|
113
|
+
const buckets = BUCKETS_RE.exec(text);
|
|
114
|
+
if (buckets) {
|
|
115
|
+
completed = Number.parseInt(buckets[1]!, 10);
|
|
116
|
+
total = completed + Number.parseInt(buckets[2]!, 10);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Nothing progress-like: mirrors the historical "no elapsed/task signal → null".
|
|
121
|
+
if (elapsedMs === 0 && completed === null) return null;
|
|
122
|
+
|
|
123
|
+
const agentRow = AGENT_ROW_RE.exec(text);
|
|
124
|
+
const toolRow = TOOL_ROW_RE.exec(text);
|
|
125
|
+
const turns = TURN_RE.exec(text);
|
|
126
|
+
const tokens = TOKENS_RE.exec(text);
|
|
127
|
+
return {
|
|
128
|
+
elapsedMs,
|
|
129
|
+
status: STATUS_RE.exec(text)?.[1] ?? null,
|
|
130
|
+
completed,
|
|
131
|
+
total,
|
|
132
|
+
activeAgent: agentRow ? `${agentRow[1]}/${agentRow[2]}` : null,
|
|
133
|
+
currentTool: toolRow ? toolRow[1]! : null,
|
|
134
|
+
turns: turns ? Number.parseInt(turns[1]!, 10) : null,
|
|
135
|
+
tokens: tokens ? Number.parseInt(tokens[1]!, 10) : null,
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
|
|
25
139
|
function pickActiveAgent(agents: CrewAgentRecord[] | undefined): CrewAgentRecord | undefined {
|
|
26
140
|
if (!agents || agents.length === 0) return undefined;
|
|
27
141
|
return (
|
|
@@ -55,7 +169,7 @@ function taskCounts(tasks: TeamTaskState[] | undefined): string | undefined {
|
|
|
55
169
|
const buckets = new Map<string, number>();
|
|
56
170
|
for (const task of tasks) buckets.set(task.status, (buckets.get(task.status) ?? 0) + 1);
|
|
57
171
|
const summary = [...buckets.entries()].map(([status, count]) => `${status}=${count}`).join(" ");
|
|
58
|
-
return
|
|
172
|
+
return `${PROGRESS_FORMAT.tasksKey} ${completed}${PROGRESS_FORMAT.tallySeparator}${total} ${PROGRESS_FORMAT.doneWord} ${summary}`;
|
|
59
173
|
}
|
|
60
174
|
|
|
61
175
|
/**
|
|
@@ -64,9 +178,12 @@ function taskCounts(tasks: TeamTaskState[] | undefined): string | undefined {
|
|
|
64
178
|
* the chat widget overlay does not jitter.
|
|
65
179
|
*/
|
|
66
180
|
export function formatCompactToolProgress(input: ToolProgressInput): string {
|
|
67
|
-
const elapsedSec = Math.max(0, Math.round((
|
|
68
|
-
const head = input.agentId ?
|
|
69
|
-
|
|
181
|
+
const elapsedSec = Math.max(0, Math.round((spinnerClockNow() - input.startedAt) / 1000));
|
|
182
|
+
const head = input.agentId ? `${PROGRESS_FORMAT.agentKey}=${input.agentId}` : PROGRESS_FORMAT.agentKey;
|
|
183
|
+
// Emitted from PROGRESS_FORMAT so the parser can never drift from the wire format.
|
|
184
|
+
const lines: string[] = [
|
|
185
|
+
`${head} ${PROGRESS_FORMAT.statusKey}=${input.status} ${PROGRESS_FORMAT.elapsedKey}=${elapsedSec}${PROGRESS_FORMAT.elapsedUnit}`,
|
|
186
|
+
];
|
|
70
187
|
|
|
71
188
|
const counts = taskCounts(input.tasks);
|
|
72
189
|
if (counts) lines.push(` ${counts}`);
|
|
@@ -75,19 +192,21 @@ export function formatCompactToolProgress(input: ToolProgressInput): string {
|
|
|
75
192
|
if (active) {
|
|
76
193
|
const turns = active.progress?.turns ?? 0;
|
|
77
194
|
const tokens = totalTokens(active);
|
|
78
|
-
lines.push(
|
|
195
|
+
lines.push(
|
|
196
|
+
` ${active.role}${PROGRESS_FORMAT.roleSeparator}${active.agent} ${PROGRESS_FORMAT.turnKey}=${turns} ${PROGRESS_FORMAT.tokensKey}=${tokens}`,
|
|
197
|
+
);
|
|
79
198
|
if (active.progress?.currentTool) {
|
|
80
199
|
const count = active.progress.toolCount ? ` (#${active.progress.toolCount})` : "";
|
|
81
|
-
lines.push(`
|
|
200
|
+
lines.push(` ${PROGRESS_FORMAT.toolKey}${PROGRESS_FORMAT.toolSeparator} ${active.progress.currentTool}${count}`);
|
|
82
201
|
}
|
|
83
202
|
const recent = active.progress?.recentOutput?.at(-1);
|
|
84
203
|
if (recent?.trim()) lines.push(` ${trimLine(recent)}`);
|
|
85
204
|
} else if (input.runId && !counts) {
|
|
86
|
-
lines.push(`
|
|
205
|
+
lines.push(` ${PROGRESS_FORMAT.runKey}=${input.runId} (starting)`);
|
|
87
206
|
} else if (input.error) {
|
|
88
|
-
lines.push(`
|
|
207
|
+
lines.push(` ${PROGRESS_FORMAT.errorKey}: ${trimLine(input.error)}`);
|
|
89
208
|
} else if (!counts) {
|
|
90
|
-
lines.push(
|
|
209
|
+
lines.push(` ${PROGRESS_FORMAT.waitingLine}`);
|
|
91
210
|
}
|
|
92
211
|
return lines.join("\n");
|
|
93
212
|
}
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
|
|
9
9
|
import type { CrewAgentRecord } from "../../runtime/crew-agent-runtime.ts";
|
|
10
10
|
import { formatDuration, formatTokens, truncLine } from "../format-helpers.ts";
|
|
11
|
+
import { spinnerClockNow } from "../spinner.ts";
|
|
11
12
|
import type { CrewTheme } from "../theme-adapter.ts";
|
|
12
13
|
|
|
13
14
|
// ── State ──────────────────────────────────────────────────────────────
|
|
@@ -57,24 +58,17 @@ export { BRIEF_ENTRY_TYPE };
|
|
|
57
58
|
|
|
58
59
|
// ── Brief renderers ────────────────────────────────────────────────────
|
|
59
60
|
|
|
60
|
-
/** Brief summary for a single tool result.
|
|
61
|
+
/** Brief summary for a single tool result.
|
|
62
|
+
*
|
|
63
|
+
* SCOPE (audit P1-12): this is only ever called with the hard-coded names
|
|
64
|
+
* "team" (`tool-renderers/index.ts:303`) and "agent" (`:414`). The 7
|
|
65
|
+
* native-tool arms (read/bash/edit/write/find/grep/ls) were unreachable and
|
|
66
|
+
* were REMOVED in M3-3 — turning /crew-brief on cannot change native tool
|
|
67
|
+
* output; only team/agent results and the default fallback render brief.
|
|
68
|
+
*/
|
|
61
69
|
export function briefToolResult(toolName: string, result: { content?: unknown[] }, theme: CrewTheme): string {
|
|
62
70
|
const text = extractText(result?.content);
|
|
63
71
|
switch (toolName) {
|
|
64
|
-
case "read":
|
|
65
|
-
return briefRead(text, theme);
|
|
66
|
-
case "bash":
|
|
67
|
-
return briefBash(text, theme);
|
|
68
|
-
case "edit":
|
|
69
|
-
return briefEdit(text, theme);
|
|
70
|
-
case "write":
|
|
71
|
-
return briefWrite(text, theme);
|
|
72
|
-
case "find":
|
|
73
|
-
return briefFind(text, theme);
|
|
74
|
-
case "grep":
|
|
75
|
-
return briefGrep(text, theme);
|
|
76
|
-
case "ls":
|
|
77
|
-
return briefLs(text, theme);
|
|
78
72
|
case "team":
|
|
79
73
|
return briefTeam(result, theme);
|
|
80
74
|
case "agent":
|
|
@@ -84,57 +78,6 @@ export function briefToolResult(toolName: string, result: { content?: unknown[]
|
|
|
84
78
|
}
|
|
85
79
|
}
|
|
86
80
|
|
|
87
|
-
function briefRead(text: string, theme: CrewTheme): string {
|
|
88
|
-
if (!text) return theme.fg("dim", "→ empty");
|
|
89
|
-
const count = text.trim().split("\n").filter(Boolean).length;
|
|
90
|
-
return theme.fg("muted", `→ ${count} lines`);
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
function briefBash(text: string, theme: CrewTheme): string {
|
|
94
|
-
if (!text?.trim()) return theme.fg("dim", "→ done");
|
|
95
|
-
const lines = text.trim().split("\n");
|
|
96
|
-
if (lines.length === 1 && lines[0]!.length < 40) {
|
|
97
|
-
return theme.fg("muted", `→ ${lines[0]}`);
|
|
98
|
-
}
|
|
99
|
-
return theme.fg("muted", `→ ${lines.length} lines`);
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
function briefEdit(text: string, theme: CrewTheme): string {
|
|
103
|
-
if (!text) return theme.fg("dim", "→ edited");
|
|
104
|
-
if (text.includes("Error") || text.includes("error")) {
|
|
105
|
-
return theme.fg("error", "→ failed");
|
|
106
|
-
}
|
|
107
|
-
const added = (text.match(/^\+ /gm) ?? []).length;
|
|
108
|
-
const removed = (text.match(/^- /gm) ?? []).length;
|
|
109
|
-
if (added === 0 && removed === 0) {
|
|
110
|
-
return theme.fg("success", "→ edited");
|
|
111
|
-
}
|
|
112
|
-
return theme.fg("success", "→ edited ") + theme.fg("toolDiffAdded", `+${added} `) + theme.fg("toolDiffRemoved", `-${removed}`);
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
function briefWrite(text: string, theme: CrewTheme): string {
|
|
116
|
-
if (text) return theme.fg("error", `→ ${text}`);
|
|
117
|
-
return theme.fg("success", "→ written");
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
function briefFind(text: string, theme: CrewTheme): string {
|
|
121
|
-
if (!text) return theme.fg("dim", "→ none");
|
|
122
|
-
const count = text.trim().split("\n").filter(Boolean).length;
|
|
123
|
-
return theme.fg("muted", `→ ${count} files`);
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
function briefGrep(text: string, theme: CrewTheme): string {
|
|
127
|
-
if (!text) return theme.fg("dim", "→ none");
|
|
128
|
-
const count = text.trim().split("\n").filter(Boolean).length;
|
|
129
|
-
return theme.fg("muted", `→ ${count} matches`);
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
function briefLs(text: string, theme: CrewTheme): string {
|
|
133
|
-
if (!text) return theme.fg("dim", "→ empty");
|
|
134
|
-
const count = text.trim().split("\n").filter(Boolean).length;
|
|
135
|
-
return theme.fg("muted", `→ ${count} entries`);
|
|
136
|
-
}
|
|
137
|
-
|
|
138
81
|
function briefTeam(result: { content?: unknown[] }, theme: CrewTheme): string {
|
|
139
82
|
// Try to extract structured details
|
|
140
83
|
const details = (result as Record<string, unknown>).details ?? result;
|
|
@@ -207,7 +150,7 @@ function computeTotalDuration(records: CrewAgentRecord[]): number {
|
|
|
207
150
|
for (const r of records) {
|
|
208
151
|
if (r.startedAt) {
|
|
209
152
|
const start = new Date(r.startedAt).getTime();
|
|
210
|
-
const end = r.completedAt ? new Date(r.completedAt).getTime() :
|
|
153
|
+
const end = r.completedAt ? new Date(r.completedAt).getTime() : spinnerClockNow();
|
|
211
154
|
if (Number.isFinite(start) && Number.isFinite(end)) total += Math.max(0, end - start);
|
|
212
155
|
}
|
|
213
156
|
}
|