pi-crew 0.10.6 → 0.11.0
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 +99 -0
- package/dist/index.mjs +34850 -34889
- package/package.json +170 -169
- package/scripts/watch-agent-transcript.mjs +120 -0
- package/src/extension/crew-input-router.ts +2 -0
- package/src/extension/crew-vibes/config.ts +1 -56
- package/src/extension/crew-vibes/figures.ts +5 -84
- package/src/extension/crew-vibes/font-detect.ts +1 -1
- package/src/extension/crew-vibes/index.ts +36 -328
- package/src/extension/crew-vibes/render.ts +2 -58
- package/src/extension/help.ts +1 -0
- package/src/extension/registration/command-registration.ts +5 -1
- package/src/extension/registration/commands/schedules.ts +212 -0
- package/src/extension/registration/commands/shared.ts +56 -1
- package/src/extension/registration/lifecycle-handlers.ts +16 -2
- package/src/extension/registration/schedule-toast-bridge.ts +109 -0
- package/src/extension/registration/viewers.ts +66 -1
- package/src/extension/team-tool/handle-schedule.ts +120 -1
- package/src/runtime/scheduling/scheduler.ts +120 -6
- package/src/runtime/settings-store.ts +28 -2
- package/src/state/coordination/locks.ts +17 -2
- package/src/ui/agents-jobs-browser.ts +785 -0
- package/src/ui/dashboard-panes/agents-pane.ts +7 -4
- package/src/ui/dashboard-panes/schedules-pane.ts +154 -0
- package/src/ui/inline-panel/crew-editor.ts +19 -6
- package/src/ui/inline-panel/index.ts +6 -0
- package/src/ui/key-utils.ts +3 -0
- package/src/ui/keybinding-map.ts +56 -2
- package/src/ui/mascot.ts +2 -1
- package/src/ui/overlays/agent-picker-overlay.ts +3 -3
- package/src/ui/overlays/confirm-overlay.ts +3 -2
- package/src/ui/overlays/mailbox-compose-overlay.ts +4 -3
- package/src/ui/overlays/mailbox-detail-overlay.ts +3 -3
- package/src/ui/run-dashboard.ts +257 -6
- package/src/ui/settings-overlay.ts +3 -3
- package/src/ui/transcript-viewer.ts +3 -2
- package/src/ui/widget/index.ts +61 -88
- package/src/ui/widget/widget-formatters.ts +7 -4
- package/src/ui/widget/widget-renderer.ts +134 -174
- package/src/ui/widget/widget-types.ts +0 -7
- package/src/utils/relative-time.ts +33 -0
- package/src/extension/crew-vibes/footer.ts +0 -313
- package/src/extension/crew-vibes/speed.ts +0 -286
- package/src/ui/dock-footer.ts +0 -49
package/src/ui/run-dashboard.ts
CHANGED
|
@@ -1,10 +1,17 @@
|
|
|
1
1
|
import * as fs from "node:fs";
|
|
2
|
+
// Tier A (schedules pane): job reads go through the extension-layer provider
|
|
3
|
+
// (G17 single source of truth) — this module NEVER touches the scheduler or
|
|
4
|
+
// the scheduledJobs settings key directly. handle-schedule.ts is a light
|
|
5
|
+
// module (crypto + scheduler + settings-store); the heavy team-tool.ts chain
|
|
6
|
+
// is NOT pulled in.
|
|
7
|
+
import { getScheduledJobs, getScheduledJobsHiddenCountView } from "../extension/team-tool/handle-schedule.ts";
|
|
2
8
|
import type { MetricRegistry } from "../observability/metric-registry.ts";
|
|
3
9
|
import { readCrewAgents } from "../runtime/crew-agent-records.ts";
|
|
4
10
|
import type { CrewAgentRecord } from "../runtime/crew-agent-runtime.ts";
|
|
5
11
|
import { getLiveAgentContextPercent } from "../runtime/live-session/live-agent-manager.ts";
|
|
6
12
|
import { isPlanApprovalPending } from "../runtime/plan-approval.ts";
|
|
7
13
|
import { isDisplayActiveRun, isLikelyOrphanedActiveRun } from "../runtime/process-status.ts";
|
|
14
|
+
import type { ScheduledJob } from "../runtime/scheduling/scheduler.ts";
|
|
8
15
|
import type { TeamRunManifest, TeamTaskState, UsageState } from "../state/types.ts";
|
|
9
16
|
import { aggregateUsage } from "../state/usage.ts";
|
|
10
17
|
import { readJsonFileCoalesced } from "../utils/file-coalescer.ts";
|
|
@@ -19,9 +26,10 @@ import { renderMailboxPane } from "./dashboard-panes/mailbox-pane.ts";
|
|
|
19
26
|
import { renderMetricsPane } from "./dashboard-panes/metrics-pane.ts";
|
|
20
27
|
import { renderPlanPane } from "./dashboard-panes/plan-pane.ts";
|
|
21
28
|
import { renderProgressPane } from "./dashboard-panes/progress-pane.ts";
|
|
29
|
+
import { renderScheduleDetails, renderSchedulesPane } from "./dashboard-panes/schedules-pane.ts";
|
|
22
30
|
import { renderTranscriptPane } from "./dashboard-panes/transcript-pane.ts";
|
|
23
31
|
import { DynamicCrewBorder } from "./dynamic-border.ts";
|
|
24
|
-
import { dashboardActionForKey } from "./keybinding-map.ts";
|
|
32
|
+
import { type DashboardKeyAction, dashboardActionForKey } from "./keybinding-map.ts";
|
|
25
33
|
import { HelpOverlay } from "./overlays/help-overlay.ts";
|
|
26
34
|
import type { OverlaySchedulerHandle } from "./shared-overlay-scheduler.ts";
|
|
27
35
|
import { registerOverlayScheduler } from "./shared-overlay-scheduler.ts";
|
|
@@ -54,6 +62,8 @@ export interface RunDashboardOptions {
|
|
|
54
62
|
showTools?: boolean;
|
|
55
63
|
snapshotCache?: RunSnapshotCache;
|
|
56
64
|
runProvider?: () => TeamRunManifest[];
|
|
65
|
+
/** Pinned render clock (D6-T4) for pane renderers (agents pane durations). */
|
|
66
|
+
nowMs?: number;
|
|
57
67
|
registry?: MetricRegistry;
|
|
58
68
|
/**
|
|
59
69
|
* Workspace/session ID for filtering runs and live agents. When provided,
|
|
@@ -61,6 +71,11 @@ export interface RunDashboardOptions {
|
|
|
61
71
|
* workspaceId are shown. This ensures session isolation in the UI.
|
|
62
72
|
*/
|
|
63
73
|
workspaceId?: string;
|
|
74
|
+
/** Tier A (schedules pane): injectable clock source (D6-T4). Render paths
|
|
75
|
+
* receive `now: Date` as a parameter and never read the wall clock
|
|
76
|
+
* themselves; tests inject a fixed Date. When omitted the clock is read
|
|
77
|
+
* ONCE per schedules-pane render pass, at this injection point only. */
|
|
78
|
+
now?: () => Date;
|
|
64
79
|
/**
|
|
65
80
|
* Poke the host TUI to repaint after a state change. Must be wired from
|
|
66
81
|
* `commands.ts` (`() => requestRenderTarget(tui)`) so keypresses and event-bus
|
|
@@ -77,7 +92,7 @@ export interface RunDashboardOptions {
|
|
|
77
92
|
* looking at. Resetting to "agents" on every `new RunDashboard(...)` was a
|
|
78
93
|
* UX regression.
|
|
79
94
|
*/
|
|
80
|
-
let lastActivePane: "agents" | "progress" | "mailbox" | "output" | "health" | "metrics" | "plan" = "agents";
|
|
95
|
+
let lastActivePane: "agents" | "progress" | "mailbox" | "output" | "health" | "metrics" | "plan" | "schedules" = "agents";
|
|
81
96
|
|
|
82
97
|
export type RunDashboardAction =
|
|
83
98
|
| "status"
|
|
@@ -90,6 +105,7 @@ export type RunDashboardAction =
|
|
|
90
105
|
| "agent-output"
|
|
91
106
|
| "agent-transcript"
|
|
92
107
|
| "agent-live"
|
|
108
|
+
| "browser-open"
|
|
93
109
|
| "mailbox"
|
|
94
110
|
| "reload"
|
|
95
111
|
| "mailbox-detail"
|
|
@@ -98,14 +114,57 @@ export type RunDashboardAction =
|
|
|
98
114
|
| "health-diagnostic-export"
|
|
99
115
|
| "plan-approve"
|
|
100
116
|
| "plan-deny"
|
|
117
|
+
| "schedule-enable"
|
|
118
|
+
| "schedule-disable"
|
|
119
|
+
| "schedule-run-now"
|
|
120
|
+
| "schedule-remove"
|
|
101
121
|
| "notifications-dismiss";
|
|
102
122
|
export interface RunDashboardSelection {
|
|
103
123
|
runId: string;
|
|
104
124
|
action: RunDashboardAction;
|
|
125
|
+
/** Tier A: target scheduled-job id. Present only for schedule-* actions
|
|
126
|
+
* (the extension layer routes them to handle-schedule.ts subActions). */
|
|
127
|
+
jobId?: string;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Tier A: map dashboard schedule actions onto handle-schedule.ts subActions —
|
|
132
|
+
* the ONLY mutation channel (the UI never calls the scheduler directly).
|
|
133
|
+
* The extension layer consumes `done()` selections carrying schedule-*
|
|
134
|
+
* actions and dispatches them as
|
|
135
|
+
* `handleSchedule({ action: 'schedule', subAction, jobId })`.
|
|
136
|
+
* Kept next to the action union so the two can never drift.
|
|
137
|
+
*/
|
|
138
|
+
export function scheduleDashboardActionToSubAction(action: RunDashboardAction): "enable" | "disable" | "run-now" | "remove" | undefined {
|
|
139
|
+
switch (action) {
|
|
140
|
+
case "schedule-enable":
|
|
141
|
+
return "enable";
|
|
142
|
+
case "schedule-disable":
|
|
143
|
+
return "disable";
|
|
144
|
+
case "schedule-run-now":
|
|
145
|
+
return "run-now";
|
|
146
|
+
case "schedule-remove":
|
|
147
|
+
return "remove";
|
|
148
|
+
default:
|
|
149
|
+
return undefined;
|
|
150
|
+
}
|
|
105
151
|
}
|
|
106
152
|
|
|
107
153
|
const TASK_READ_TTL_MS = 1000;
|
|
108
154
|
|
|
155
|
+
/** Tier A: short TTL for the scheduled-jobs provider cache. getScheduledJobs()
|
|
156
|
+
* is cheap when the scheduler singleton is registered (in-memory list) but
|
|
157
|
+
* falls back to a settings-store disk read otherwise — render ticks must not
|
|
158
|
+
* hit disk per frame (P0-6 discipline). Key `R` (schedule-refresh) drops the
|
|
159
|
+
* cache for an immediate re-read. */
|
|
160
|
+
const SCHEDULE_JOBS_TTL_MS = 1000;
|
|
161
|
+
|
|
162
|
+
/** Tier A: max schedules-pane body lines per render. Existing run-scoped
|
|
163
|
+
* panes slice to 8; the schedules table spends 2 lines per job (main + sub),
|
|
164
|
+
* so pane 8 gets a taller budget (6 jobs + header + key hint) — still well
|
|
165
|
+
* inside the stable targetHeight clamp (12–36). */
|
|
166
|
+
const SCHEDULES_PANE_MAX_LINES = 14;
|
|
167
|
+
|
|
109
168
|
/** Max run rows rendered in the dashboard run-list block (L-1 window budget). */
|
|
110
169
|
const RUN_LIST_MAX = 8;
|
|
111
170
|
|
|
@@ -435,9 +494,15 @@ export class RunDashboard implements DashboardComponent {
|
|
|
435
494
|
private runScrollOffset = 0;
|
|
436
495
|
private showFullProgress = false;
|
|
437
496
|
private showHelp = false;
|
|
438
|
-
private activePane: "agents" | "progress" | "mailbox" | "output" | "health" | "metrics" | "plan" = lastActivePane;
|
|
497
|
+
private activePane: "agents" | "progress" | "mailbox" | "output" | "health" | "metrics" | "plan" | "schedules" = lastActivePane;
|
|
439
498
|
/** WP-7 (R7): pane-scoped revision-diff toggle (X). */
|
|
440
499
|
private planDiff = false;
|
|
500
|
+
/** Tier A (schedules pane): job-cursor, details toggle, 2-step delete
|
|
501
|
+
* confirm-gate state, and the TTL'd provider cache (G17 reads only). */
|
|
502
|
+
private scheduleSelected = 0;
|
|
503
|
+
private scheduleDetails = false;
|
|
504
|
+
private scheduleDeleteArmed = false;
|
|
505
|
+
private scheduleJobsCache: { at: number; jobs: ScheduledJob[]; hiddenCount: number } | undefined;
|
|
441
506
|
private runs: TeamRunManifest[];
|
|
442
507
|
private readonly done: (selection: RunDashboardSelection | undefined) => void;
|
|
443
508
|
private readonly theme: CrewTheme;
|
|
@@ -617,7 +682,8 @@ export class RunDashboard implements DashboardComponent {
|
|
|
617
682
|
.join("|");
|
|
618
683
|
const metricsSig =
|
|
619
684
|
this.activePane === "metrics" ? `:metrics=${this.options.registry?.snapshot().length ?? 0}:${spinnerBucket()}` : "";
|
|
620
|
-
const
|
|
685
|
+
const schedulesSig = this.activePane === "schedules" ? `:sched=${this.schedulesSignatureFragment()}` : "";
|
|
686
|
+
const sig = `${this.selected}:${this.showHelp ? 1 : 0}:${this.showFullProgress ? 1 : 0}:${this.activePane}:${statuses}${hasRunning ? `:spin=${spinnerBucket()}` : ""}${metricsSig}${schedulesSig}`;
|
|
621
687
|
this.cachedSignature = sig;
|
|
622
688
|
this.cachedSignatureAt = now;
|
|
623
689
|
return sig;
|
|
@@ -642,6 +708,78 @@ export class RunDashboard implements DashboardComponent {
|
|
|
642
708
|
return selectedRunFromGrouped(this.runs, this.selected, this.options.snapshotCache, resolve)?.runId;
|
|
643
709
|
}
|
|
644
710
|
|
|
711
|
+
/** Tier A: scheduled jobs via the SINGLE source of truth (G17) — the
|
|
712
|
+
* extension-layer provider, TTL-cached so render ticks never hit disk
|
|
713
|
+
* per frame. `R` (schedule-refresh) drops the cache. The TTL timestamp
|
|
714
|
+
* reads the SAME injected clock as the pane render (D6-T4 — no direct
|
|
715
|
+
* Date.now() anywhere on the render path; review round 1 minor-1).
|
|
716
|
+
* P2-1: the hidden project-tier count rides the SAME cached provider
|
|
717
|
+
* read (jobs + count populate ONE cache entry) — no second uncached
|
|
718
|
+
* disk-read path exists on render ticks. */
|
|
719
|
+
private scheduleData(): { jobs: ScheduledJob[]; hiddenCount: number } {
|
|
720
|
+
const at = this.scheduleNow().getTime();
|
|
721
|
+
if (this.scheduleJobsCache && at - this.scheduleJobsCache.at < SCHEDULE_JOBS_TTL_MS) {
|
|
722
|
+
return this.scheduleJobsCache;
|
|
723
|
+
}
|
|
724
|
+
let jobs: ScheduledJob[] = [];
|
|
725
|
+
let hiddenCount = 0;
|
|
726
|
+
try {
|
|
727
|
+
jobs = getScheduledJobs();
|
|
728
|
+
} catch {
|
|
729
|
+
jobs = [];
|
|
730
|
+
}
|
|
731
|
+
try {
|
|
732
|
+
// Stash-first: in-memory while the scheduler singleton is registered;
|
|
733
|
+
// a tiers read only in the no-scheduler fallback (TTL-bounded here).
|
|
734
|
+
hiddenCount = getScheduledJobsHiddenCountView();
|
|
735
|
+
} catch {
|
|
736
|
+
hiddenCount = 0;
|
|
737
|
+
}
|
|
738
|
+
this.scheduleJobsCache = { at, jobs, hiddenCount };
|
|
739
|
+
if (this.scheduleSelected > jobs.length - 1) this.scheduleSelected = Math.max(0, jobs.length - 1);
|
|
740
|
+
return this.scheduleJobsCache;
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
private scheduleJobs(): ScheduledJob[] {
|
|
744
|
+
return this.scheduleData().jobs;
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
/** P2-1: hidden project-tier jobs count — same cache entry as the jobs
|
|
748
|
+
* read above (single provider read per TTL window). */
|
|
749
|
+
private scheduleHiddenCount(): number {
|
|
750
|
+
return this.scheduleData().hiddenCount;
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
/** Job under the pane-8 cursor (up/down move it); undefined when the list
|
|
754
|
+
* is empty — schedule actions are silent no-ops then (plan-approve
|
|
755
|
+
* precedent for a gated action). */
|
|
756
|
+
private selectedScheduleJob(): ScheduledJob | undefined {
|
|
757
|
+
const jobs = this.scheduleJobs();
|
|
758
|
+
return jobs[Math.min(this.scheduleSelected, jobs.length - 1)];
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
/** Clock for the schedules pane (D6-T4): the render path receives the
|
|
762
|
+
* resulting `now: Date` as a parameter and never reads the clock itself.
|
|
763
|
+
* The SINGLE clock read point for everything pane-8: render, signature
|
|
764
|
+
* pass, and the provider TTL timestamp (scheduleJobs) — so no second
|
|
765
|
+
* wall-clock read exists on the render path. */
|
|
766
|
+
private scheduleNow(): Date {
|
|
767
|
+
return this.options.now ? this.options.now() : new Date();
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
/** Signature fragment for the schedules pane so job data, cursor, details,
|
|
771
|
+
* confirm-gate state, and relative-time minute buckets invalidate the
|
|
772
|
+
* layout cache (mirrors metricsSig). P2-1: the hidden count is part of the
|
|
773
|
+
* fragment so a changed stash repaints the pane after the TTL. */
|
|
774
|
+
private schedulesSignatureFragment(): string {
|
|
775
|
+
const { jobs, hiddenCount } = this.scheduleData();
|
|
776
|
+
const minuteBucket = Math.floor(this.scheduleNow().getTime() / 60_000);
|
|
777
|
+
const jobSig = jobs
|
|
778
|
+
.map((job) => `${job.enabled ? 1 : 0}:${job.runCount}:${job.nextRun ?? ""}:${job.lastStatus ?? ""}:${job.lastRun ?? ""}`)
|
|
779
|
+
.join(",");
|
|
780
|
+
return `${jobs.length}:${jobSig}:${hiddenCount}:${this.scheduleSelected}:${this.scheduleDetails ? 1 : 0}:${this.scheduleDeleteArmed ? 1 : 0}:${minuteBucket}`;
|
|
781
|
+
}
|
|
782
|
+
|
|
645
783
|
render(width: number): string[] {
|
|
646
784
|
try {
|
|
647
785
|
return this.renderUnsafe(width);
|
|
@@ -683,7 +821,7 @@ export class RunDashboard implements DashboardComponent {
|
|
|
683
821
|
lines.push(
|
|
684
822
|
border("╭", "╮"),
|
|
685
823
|
row(
|
|
686
|
-
`${fg("accent", "▐")} ${this.theme.bold("pi-crew")} · ${this.runs.length} runs ${fg("dim", "1-
|
|
824
|
+
`${fg("accent", "▐")} ${this.theme.bold("pi-crew")} · ${this.runs.length} runs ${fg("dim", "1-8 pane · ↑↓ · Enter · ? help · Esc")}`,
|
|
687
825
|
),
|
|
688
826
|
sep(),
|
|
689
827
|
);
|
|
@@ -736,8 +874,10 @@ export class RunDashboard implements DashboardComponent {
|
|
|
736
874
|
// `selectable` rows already derived from the single groupedRuns()
|
|
737
875
|
// computation above instead of recomputing grouping a third time
|
|
738
876
|
// (`selectedRunFromGrouped` is exactly `selectable[selected].run`).
|
|
877
|
+
// Tier A: the schedules pane REPLACES this run-scoped detail block —
|
|
878
|
+
// it renders as a root-level section below (non-run-scoped).
|
|
739
879
|
const selectedRun = selectable[Math.min(this.selected, selectable.length - 1)]?.run;
|
|
740
|
-
if (selectedRun) {
|
|
880
|
+
if (selectedRun && this.activePane !== "schedules") {
|
|
741
881
|
const snap = snapshotOnce(selectedRun);
|
|
742
882
|
const r = snap?.manifest ?? selectedRun;
|
|
743
883
|
const agents = snap?.agents ?? agentsFor(selectedRun, this.options.snapshotCache, snapshotOnce);
|
|
@@ -828,6 +968,33 @@ export class RunDashboard implements DashboardComponent {
|
|
|
828
968
|
if (footerFields.length) lines.push(row(fg("dim", footerFields.join(" · "))));
|
|
829
969
|
}
|
|
830
970
|
}
|
|
971
|
+
|
|
972
|
+
// Tier A (schedules pane, key 8) — NON-RUN-SCOPED section: renders
|
|
973
|
+
// with zero runs (below the empty-runs state) AND alongside the
|
|
974
|
+
// run list, replacing the selected-run detail block (guard above).
|
|
975
|
+
// Data via the G17 provider only; clock injected per D6-T4.
|
|
976
|
+
if (this.activePane === "schedules") {
|
|
977
|
+
const jobs = this.scheduleJobs();
|
|
978
|
+
const now = this.scheduleNow();
|
|
979
|
+
lines.push(sep());
|
|
980
|
+
lines.push(row(fg("dim", "── schedules ──")));
|
|
981
|
+
const paneLines = safeRenderPane("schedules", () =>
|
|
982
|
+
this.scheduleDetails && jobs.length > 0
|
|
983
|
+
? renderScheduleDetails(jobs[Math.min(this.scheduleSelected, jobs.length - 1)], now)
|
|
984
|
+
: renderSchedulesPane(jobs, now, {
|
|
985
|
+
selectedIndex: this.scheduleSelected,
|
|
986
|
+
hiddenCount: this.scheduleHiddenCount(),
|
|
987
|
+
}),
|
|
988
|
+
);
|
|
989
|
+
for (const line of paneLines.filter((l) => l && l.trim() !== "").slice(0, SCHEDULES_PANE_MAX_LINES)) {
|
|
990
|
+
lines.push(colorizeStatusGlyphs(row(truncate(sanitizeLine(line), innerWidth - 2)), this.theme));
|
|
991
|
+
}
|
|
992
|
+
if (this.scheduleDeleteArmed) {
|
|
993
|
+
const victim = jobs[Math.min(this.scheduleSelected, jobs.length - 1)];
|
|
994
|
+
const name = victim ? truncate(sanitizeLine(victim.name), 24) : "?";
|
|
995
|
+
lines.push(row(fg("warning", `⚠ X again to DELETE '${name}' · any other key cancels`)));
|
|
996
|
+
}
|
|
997
|
+
}
|
|
831
998
|
lines.push(border("╰", "╯"));
|
|
832
999
|
}
|
|
833
1000
|
|
|
@@ -876,6 +1043,13 @@ export class RunDashboard implements DashboardComponent {
|
|
|
876
1043
|
this.scheduleRender();
|
|
877
1044
|
return;
|
|
878
1045
|
}
|
|
1046
|
+
// Tier A (schedules pane): 2-step confirm-gate — once X has armed a
|
|
1047
|
+
// pending delete, ANY other key (bound or not) disarms it before that
|
|
1048
|
+
// key's own processing continues.
|
|
1049
|
+
if (this.activePane === "schedules" && this.scheduleDeleteArmed && action !== "schedule-delete") {
|
|
1050
|
+
this.scheduleDeleteArmed = false;
|
|
1051
|
+
this.invalidate();
|
|
1052
|
+
}
|
|
879
1053
|
const selectedRunId = this.selectedRunId();
|
|
880
1054
|
if (action === "close") {
|
|
881
1055
|
this.done(undefined);
|
|
@@ -898,6 +1072,32 @@ export class RunDashboard implements DashboardComponent {
|
|
|
898
1072
|
}
|
|
899
1073
|
return;
|
|
900
1074
|
}
|
|
1075
|
+
// Tier A: schedules-pane actions (T/N/V/X/R — pane-scoped bindings) and
|
|
1076
|
+
// job-cursor navigation. Deliberately a DEDICATED branch (plan-approve
|
|
1077
|
+
// precedent): with no job under the cursor the mutation keys are silent
|
|
1078
|
+
// no-ops (dashboard stays open) instead of closing it via done(undefined).
|
|
1079
|
+
// Mutations ONLY ever leave via done() selections carrying schedule-*
|
|
1080
|
+
// actions — the extension layer routes them through handle-schedule.ts
|
|
1081
|
+
// subActions (see scheduleDashboardActionToSubAction); the UI never
|
|
1082
|
+
// touches the scheduler directly.
|
|
1083
|
+
if (
|
|
1084
|
+
action === "schedule-toggle" ||
|
|
1085
|
+
action === "schedule-run-now" ||
|
|
1086
|
+
action === "schedule-delete" ||
|
|
1087
|
+
action === "schedule-details" ||
|
|
1088
|
+
action === "schedule-refresh"
|
|
1089
|
+
) {
|
|
1090
|
+
this.handleSchedulePaneAction(action);
|
|
1091
|
+
return;
|
|
1092
|
+
}
|
|
1093
|
+
if (this.activePane === "schedules" && (action === "up" || action === "down")) {
|
|
1094
|
+
// Job cursor moves instead of the run-list selection while pane 8 owns input.
|
|
1095
|
+
const count = this.scheduleJobs().length;
|
|
1096
|
+
this.scheduleSelected =
|
|
1097
|
+
action === "up" ? Math.max(0, this.scheduleSelected - 1) : Math.min(Math.max(0, count - 1), this.scheduleSelected + 1);
|
|
1098
|
+
this.invalidate();
|
|
1099
|
+
return;
|
|
1100
|
+
}
|
|
901
1101
|
if (
|
|
902
1102
|
action === "summary" ||
|
|
903
1103
|
action === "artifacts" ||
|
|
@@ -930,6 +1130,15 @@ export class RunDashboard implements DashboardComponent {
|
|
|
930
1130
|
this.done(selectedRunId ? { runId: selectedRunId, action: "agent-live" } : undefined);
|
|
931
1131
|
return;
|
|
932
1132
|
}
|
|
1133
|
+
// Agents & Jobs browser (feat/agents-browser): one-keypress overlay,
|
|
1134
|
+
// mirrors the live-conversation branch. Unlike it, a missing run
|
|
1135
|
+
// selection does NOT dead-end — the browser lists live agents and
|
|
1136
|
+
// scheduled jobs independent of any single run, so runId "" keeps it
|
|
1137
|
+
// reachable with zero runs (the reload-action precedent).
|
|
1138
|
+
if (action === "browser") {
|
|
1139
|
+
this.done(selectedRunId ? { runId: selectedRunId, action: "browser-open" } : { runId: "", action: "browser-open" });
|
|
1140
|
+
return;
|
|
1141
|
+
}
|
|
933
1142
|
if (action === "progressToggle") {
|
|
934
1143
|
this.showFullProgress = !this.showFullProgress;
|
|
935
1144
|
this.invalidate();
|
|
@@ -942,6 +1151,7 @@ export class RunDashboard implements DashboardComponent {
|
|
|
942
1151
|
else if (action === "pane-health") this.activePane = "health";
|
|
943
1152
|
else if (action === "pane-metrics") this.activePane = "metrics";
|
|
944
1153
|
else if (action === "pane-plan") this.activePane = "plan";
|
|
1154
|
+
else if (action === "pane-schedules") this.activePane = "schedules";
|
|
945
1155
|
else if (action === "plan-diff") {
|
|
946
1156
|
this.planDiff = !this.planDiff;
|
|
947
1157
|
this.invalidate();
|
|
@@ -956,4 +1166,45 @@ export class RunDashboard implements DashboardComponent {
|
|
|
956
1166
|
this.scheduleRender();
|
|
957
1167
|
}
|
|
958
1168
|
}
|
|
1169
|
+
|
|
1170
|
+
/** Tier A: dispatch a pane-8 action. Local state toggles (V details, R
|
|
1171
|
+
* refresh) mutate in place; mutations (T toggle, N run-now, X delete)
|
|
1172
|
+
* leave the component ONLY as done() selections carrying schedule-*
|
|
1173
|
+
* actions + jobId for the extension layer to route through
|
|
1174
|
+
* handle-schedule.ts subActions — the UI layer never mutates jobs itself. */
|
|
1175
|
+
private handleSchedulePaneAction(action: DashboardKeyAction): void {
|
|
1176
|
+
// Pane-local state toggles follow the progressToggle/plan-diff precedent:
|
|
1177
|
+
// invalidate() so the next render (sync in tests, host-tick in the TUI)
|
|
1178
|
+
// recomputes immediately.
|
|
1179
|
+
if (action === "schedule-details") {
|
|
1180
|
+
this.scheduleDetails = !this.scheduleDetails;
|
|
1181
|
+
this.invalidate();
|
|
1182
|
+
return;
|
|
1183
|
+
}
|
|
1184
|
+
if (action === "schedule-refresh") {
|
|
1185
|
+
this.scheduleJobsCache = undefined;
|
|
1186
|
+
this.invalidate();
|
|
1187
|
+
return;
|
|
1188
|
+
}
|
|
1189
|
+
const job = this.selectedScheduleJob();
|
|
1190
|
+
if (!job) return; // silent no-op — plan-approve precedent for gated actions
|
|
1191
|
+
if (action === "schedule-delete") {
|
|
1192
|
+
// 2-step destructive confirm-gate: first X arms (render shows the
|
|
1193
|
+
// warning line; any other key disarms — see handleInput), second X
|
|
1194
|
+
// confirms and emits the remove selection.
|
|
1195
|
+
if (this.scheduleDeleteArmed) {
|
|
1196
|
+
this.scheduleDeleteArmed = false;
|
|
1197
|
+
this.done({ runId: "", action: "schedule-remove", jobId: job.id });
|
|
1198
|
+
} else {
|
|
1199
|
+
this.scheduleDeleteArmed = true;
|
|
1200
|
+
this.invalidate();
|
|
1201
|
+
}
|
|
1202
|
+
return;
|
|
1203
|
+
}
|
|
1204
|
+
this.done({
|
|
1205
|
+
runId: "",
|
|
1206
|
+
action: action === "schedule-toggle" ? (job.enabled ? "schedule-disable" : "schedule-enable") : "schedule-run-now",
|
|
1207
|
+
jobId: job.id,
|
|
1208
|
+
});
|
|
1209
|
+
}
|
|
959
1210
|
}
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
import { truncateToWidth, visibleWidth } from "../utils/visual.ts";
|
|
8
8
|
import { DynamicCrewBorder } from "./dynamic-border.ts";
|
|
9
|
-
import { keyOf } from "./key-utils.ts";
|
|
9
|
+
import { keyOf, matchesKey } from "./key-utils.ts";
|
|
10
10
|
import type { CrewTheme } from "./theme-adapter.ts";
|
|
11
11
|
import { discoverPiThemes, getActivePiTheme } from "./theme-discovery.ts";
|
|
12
12
|
|
|
@@ -608,7 +608,7 @@ class TextinputSubmenu {
|
|
|
608
608
|
return;
|
|
609
609
|
}
|
|
610
610
|
// Backspace
|
|
611
|
-
if (data
|
|
611
|
+
if (matchesKey(data, "backspace") || k === "backspace") {
|
|
612
612
|
this.buffer = this.buffer.slice(0, -1);
|
|
613
613
|
return;
|
|
614
614
|
}
|
|
@@ -761,7 +761,7 @@ class AgentOverridesSubmenu {
|
|
|
761
761
|
this.editField = null;
|
|
762
762
|
return;
|
|
763
763
|
}
|
|
764
|
-
if (data
|
|
764
|
+
if (matchesKey(data, "backspace") || k === "backspace") {
|
|
765
765
|
this.editBuffer = this.editBuffer.slice(0, -1);
|
|
766
766
|
return;
|
|
767
767
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import * as fs from "node:fs";
|
|
2
|
+
import { matchesKey } from "@earendil-works/pi-tui";
|
|
2
3
|
import { agentOutputPath, readCrewAgents } from "../runtime/crew-agent-records.ts";
|
|
3
4
|
import type { TeamRunManifest } from "../state/types.ts";
|
|
4
5
|
import { resolveRealContainedPath } from "../utils/safe-paths.ts";
|
|
@@ -340,7 +341,7 @@ export class DurableTextViewer implements Component {
|
|
|
340
341
|
}
|
|
341
342
|
|
|
342
343
|
handleInput(data: string): void {
|
|
343
|
-
if (data
|
|
344
|
+
if (matchesKey(data, "q") || matchesKey(data, "escape")) {
|
|
344
345
|
this.done(undefined);
|
|
345
346
|
return;
|
|
346
347
|
}
|
|
@@ -441,7 +442,7 @@ export class DurableTranscriptViewer implements Component {
|
|
|
441
442
|
}
|
|
442
443
|
|
|
443
444
|
handleInput(data: string): void {
|
|
444
|
-
if (data
|
|
445
|
+
if (matchesKey(data, "q") || matchesKey(data, "escape")) {
|
|
445
446
|
this.done(undefined);
|
|
446
447
|
return;
|
|
447
448
|
}
|