catherd-cli 1.5.0 → 2.0.0-beta.20261022113309
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/README.md +164 -208
- package/THIRD_PARTY_NOTICES.md +217 -35
- package/catalog/jev.json +27 -1
- package/catalog/models.json +56 -247
- package/package.json +6 -8
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/.codex-plugin/plugin.json +1 -1
- package/plugin/bin/catherd-mcp +1 -1
- package/plugin/commands/catherd.md +2 -2
- package/plugin/skills/catherd/SKILL.md +97 -421
- package/plugin/skills/catherd-setup/SKILL.md +20 -19
- package/src/adapters/all.ts +0 -4
- package/src/adapters/backend.ts +1 -2
- package/src/adapters/cursor/home.ts +1 -1
- package/src/adapters/cursor/index.ts +4 -4
- package/src/cli.ts +22 -27
- package/src/domain/bars.ts +33 -107
- package/src/domain/blocked.ts +72 -0
- package/src/domain/branch.ts +51 -0
- package/src/domain/brief.ts +21 -6
- package/src/domain/catalog.ts +69 -324
- package/src/domain/cost.ts +15 -30
- package/src/domain/errors.ts +2 -2
- package/src/domain/fail-lines.ts +26 -0
- package/src/domain/failover.ts +37 -87
- package/src/domain/findings.ts +47 -0
- package/src/domain/gate-brief.ts +1 -0
- package/src/domain/ids.ts +13 -2
- package/src/domain/jev.ts +47 -2
- package/src/domain/judge.ts +124 -0
- package/src/domain/lane.ts +22 -3
- package/src/domain/milestone-memory.ts +53 -0
- package/src/domain/notice.ts +25 -8
- package/src/domain/notify.ts +32 -0
- package/src/domain/plan-doc.ts +198 -0
- package/src/domain/plan-lint.ts +141 -0
- package/src/domain/profile-rules.ts +101 -160
- package/src/domain/profile.ts +116 -63
- package/src/domain/record.ts +53 -2
- package/src/domain/role-prompts.ts +131 -45
- package/src/domain/role-scope.ts +4 -34
- package/src/domain/role-tools.ts +2 -2
- package/src/domain/roles.ts +2 -0
- package/src/domain/route.ts +17 -0
- package/src/domain/rules.ts +33 -0
- package/src/domain/run-events.ts +298 -0
- package/src/domain/run-machine.ts +668 -0
- package/src/domain/run-state.ts +1310 -0
- package/src/domain/scorecard.ts +197 -0
- package/src/domain/select.ts +151 -146
- package/src/domain/slopalytics.ts +72 -0
- package/src/entry/answer-command.ts +39 -0
- package/src/entry/catalog-command.ts +25 -174
- package/src/entry/check-bin.ts +17 -0
- package/src/entry/cli-kit.ts +1 -1
- package/src/entry/daemon-bin.ts +22 -0
- package/src/entry/glyphs.ts +15 -2
- package/src/entry/init-command.ts +18 -68
- package/src/entry/knowledge-command.ts +45 -4
- package/src/entry/lock-command.ts +6 -7
- package/src/entry/mcp/role-server.ts +18 -0
- package/src/entry/mcp/server.ts +13 -40
- package/src/entry/mcp/session-tools.ts +149 -0
- package/src/entry/panel/openapi.ts +189 -0
- package/src/entry/panel/server.ts +410 -0
- package/src/entry/panel-command.ts +68 -0
- package/src/entry/profile-command.ts +10 -14
- package/src/entry/role-cli-command.ts +47 -3
- package/src/entry/runs-command.ts +3 -19
- package/src/entry/scorecard-command.ts +187 -0
- package/src/entry/supervise-bin.ts +1 -1
- package/src/infra/env.ts +0 -2
- package/src/infra/git.ts +64 -0
- package/src/infra/heavy-lock.ts +45 -7
- package/src/infra/launch.ts +63 -7
- package/src/infra/memory.ts +45 -0
- package/src/infra/ntfy.ts +40 -0
- package/src/infra/open-url.ts +42 -0
- package/src/infra/paths.ts +2 -2
- package/src/infra/slopalytics.ts +30 -0
- package/src/infra/store.ts +6 -1
- package/src/services/admission.ts +24 -9
- package/src/services/backends.ts +4 -6
- package/src/services/capture.ts +1 -22
- package/src/services/catalog-service.ts +155 -262
- package/src/services/credentials.ts +4 -20
- package/src/services/daemon.ts +181 -0
- package/src/services/dispatch-service.ts +5 -1
- package/src/services/doctor-backends.ts +1 -1
- package/src/services/doctor-checks.ts +47 -0
- package/src/services/doctor.ts +4 -9
- package/src/services/failover-rungs.ts +109 -0
- package/src/services/finalize.ts +12 -0
- package/src/services/gate-env.ts +1 -2
- package/src/services/gate-service.ts +21 -6
- package/src/services/jev-service.ts +2 -4
- package/src/services/lane-service.ts +34 -3
- package/src/services/milestone-check.ts +433 -0
- package/src/services/milestone-tree.ts +170 -0
- package/src/services/milestones.ts +35 -8
- package/src/services/notifier.ts +69 -2
- package/src/services/notify-service.ts +26 -0
- package/src/services/panel-setup.ts +116 -0
- package/src/services/plan-input.ts +151 -0
- package/src/services/ports.ts +30 -4
- package/src/services/preflight.ts +82 -8
- package/src/services/profile-service.ts +30 -3
- package/src/services/profile-store.ts +8 -3
- package/src/services/protocol.ts +4 -0
- package/src/services/provenance.ts +31 -90
- package/src/services/questions.ts +9 -0
- package/src/services/reconcile.ts +11 -0
- package/src/services/routing-service.ts +26 -15
- package/src/services/run-briefs.ts +477 -0
- package/src/services/run-client.ts +217 -0
- package/src/services/run-effects.ts +837 -0
- package/src/services/run-events-store.ts +174 -0
- package/src/services/run-evidence.ts +1 -1
- package/src/services/run-read.ts +221 -0
- package/src/services/run-report.ts +176 -0
- package/src/services/run-service.ts +55 -6
- package/src/services/run-store.ts +54 -7
- package/src/services/scorecard-metrics.ts +86 -0
- package/src/services/scorecard.ts +382 -0
- package/src/services/summary.ts +27 -3
- package/src/services/tree-path.ts +27 -0
- package/src/services/workspace-admission.ts +5 -13
- package/src/services/workspace-service.ts +5 -0
- package/web/dist/assets/Brand-PKlUX5TE.js +1 -0
- package/web/dist/assets/Brand-PKlUX5TE.js.br +0 -0
- package/web/dist/assets/Brand-PKlUX5TE.js.gz +0 -0
- package/web/dist/assets/Modal-Dxc5so53.js +1 -0
- package/web/dist/assets/Modal-Dxc5so53.js.br +0 -0
- package/web/dist/assets/Modal-Dxc5so53.js.gz +0 -0
- package/web/dist/assets/Pressable-C8-T6Ed_.js +1 -0
- package/web/dist/assets/StatusChip-B5xfI_Mn.js +153 -0
- package/web/dist/assets/StatusChip-B5xfI_Mn.js.br +0 -0
- package/web/dist/assets/StatusChip-B5xfI_Mn.js.gz +0 -0
- package/web/dist/assets/_app-Ea5cpyKe.js +1 -0
- package/web/dist/assets/_app-Ea5cpyKe.js.br +0 -0
- package/web/dist/assets/_app-Ea5cpyKe.js.gz +0 -0
- package/web/dist/assets/_runId-pTG0fXVo.js +1 -0
- package/web/dist/assets/_runId-pTG0fXVo.js.br +0 -0
- package/web/dist/assets/_runId-pTG0fXVo.js.gz +0 -0
- package/web/dist/assets/drawer-Du3cdlok.js +1 -0
- package/web/dist/assets/drawer-Du3cdlok.js.br +0 -0
- package/web/dist/assets/drawer-Du3cdlok.js.gz +0 -0
- package/web/dist/assets/geist-cyrillic-ext-wght-normal-DjL33-gN.woff2 +0 -0
- package/web/dist/assets/geist-cyrillic-wght-normal-BEAKL7Jp.woff2 +0 -0
- package/web/dist/assets/geist-latin-ext-wght-normal-DC-KSUi6.woff2 +0 -0
- package/web/dist/assets/geist-latin-wght-normal-BgDaEnEv.woff2 +0 -0
- package/web/dist/assets/geist-mono-cyrillic-ext-wght-normal-X_5orZeX.woff2 +0 -0
- package/web/dist/assets/geist-mono-cyrillic-wght-normal-DiZS0aHC.woff2 +0 -0
- package/web/dist/assets/geist-mono-latin-ext-wght-normal-Bwz-egvJ.woff2 +0 -0
- package/web/dist/assets/geist-mono-latin-wght-normal-XN7g48iV.woff2 +0 -0
- package/web/dist/assets/geist-mono-symbols2-wght-normal-CO5SzqOn.woff2 +0 -0
- package/web/dist/assets/geist-mono-vietnamese-wght-normal-DadHysG0.woff2 +0 -0
- package/web/dist/assets/geist-vietnamese-wght-normal-6IgcOCM7.woff2 +0 -0
- package/web/dist/assets/index-B6GiwDkh.js +11 -0
- package/web/dist/assets/index-B6GiwDkh.js.br +0 -0
- package/web/dist/assets/index-B6GiwDkh.js.gz +0 -0
- package/web/dist/assets/index-C5IbylvM.css +2 -0
- package/web/dist/assets/index-C5IbylvM.css.br +0 -0
- package/web/dist/assets/index-C5IbylvM.css.gz +0 -0
- package/web/dist/assets/label-DFSYBc62.js +1 -0
- package/web/dist/assets/label-DFSYBc62.js.br +0 -0
- package/web/dist/assets/label-DFSYBc62.js.gz +0 -0
- package/web/dist/assets/link-D8EcKZUt.js +1 -0
- package/web/dist/assets/link-D8EcKZUt.js.br +0 -0
- package/web/dist/assets/link-D8EcKZUt.js.gz +0 -0
- package/web/dist/assets/login-DaN5HPPW.js +1 -0
- package/web/dist/assets/login-DaN5HPPW.js.br +0 -0
- package/web/dist/assets/login-DaN5HPPW.js.gz +0 -0
- package/web/dist/assets/query-Cu--ccT3.js +1 -0
- package/web/dist/assets/questions-shoO9OVj.js +1 -0
- package/web/dist/assets/questions-shoO9OVj.js.br +0 -0
- package/web/dist/assets/questions-shoO9OVj.js.gz +0 -0
- package/web/dist/assets/react-query.gen-Bu2yV-qJ.js +12 -0
- package/web/dist/assets/react-query.gen-Bu2yV-qJ.js.br +0 -0
- package/web/dist/assets/react-query.gen-Bu2yV-qJ.js.gz +0 -0
- package/web/dist/assets/rolldown-runtime-XKyaHtl9.js +1 -0
- package/web/dist/assets/run-status-C0DRuV7U.js +1 -0
- package/web/dist/assets/runs-C3rg2zZc.js +1 -0
- package/web/dist/assets/runs-C3rg2zZc.js.br +0 -0
- package/web/dist/assets/runs-C3rg2zZc.js.gz +0 -0
- package/web/dist/assets/setup-5I4Vl9fR.js +1 -0
- package/web/dist/assets/setup-5I4Vl9fR.js.br +0 -0
- package/web/dist/assets/setup-5I4Vl9fR.js.gz +0 -0
- package/web/dist/assets/table-BOxXhqS4.js +1 -0
- package/web/dist/assets/table-BOxXhqS4.js.br +0 -0
- package/web/dist/assets/table-BOxXhqS4.js.gz +0 -0
- package/web/dist/assets/textfield-BzRFkxBF.js +1 -0
- package/web/dist/assets/textfield-BzRFkxBF.js.br +0 -0
- package/web/dist/assets/textfield-BzRFkxBF.js.gz +0 -0
- package/web/dist/assets/time--9kSQbbp.js +1 -0
- package/web/dist/assets/use-css-variable-CpnN9WtA.js +5 -0
- package/web/dist/assets/use-css-variable-CpnN9WtA.js.br +0 -0
- package/web/dist/assets/use-css-variable-CpnN9WtA.js.gz +0 -0
- package/web/dist/assets/useMutation-CVPKlR34.js +1 -0
- package/web/dist/assets/useMutation-CVPKlR34.js.br +0 -0
- package/web/dist/assets/useMutation-CVPKlR34.js.gz +0 -0
- package/web/dist/index.html +15 -0
- package/catalog/ATTRIBUTION.md +0 -44
- package/catalog/scores.json +0 -3202
- package/catalog/sources.json +0 -97
- package/src/adapters/antigravity/events.ts +0 -131
- package/src/adapters/antigravity/home.ts +0 -46
- package/src/adapters/antigravity/index.ts +0 -335
- package/src/adapters/antigravity/models.ts +0 -37
- package/src/adapters/grok/events.ts +0 -112
- package/src/adapters/grok/home.ts +0 -187
- package/src/adapters/grok/index.ts +0 -358
- package/src/adapters/grok/models.ts +0 -48
- package/src/domain/calibration.ts +0 -132
- package/src/domain/sources.ts +0 -144
- package/src/entry/mcp/dispatch-tools.ts +0 -52
- package/src/entry/mcp/lane-tools.ts +0 -131
- package/src/entry/mcp/protocol-tools.ts +0 -63
- package/src/entry/mcp/push-tools.ts +0 -16
- package/src/entry/mcp/run-tools.ts +0 -164
- package/src/entry/mcp/setup-tools.ts +0 -109
- package/src/entry/mcp/workspace-tools.ts +0 -142
- package/src/entry/tui/commands.ts +0 -563
- package/src/entry/tui/effects.ts +0 -385
- package/src/entry/tui/fixtures.ts +0 -637
- package/src/entry/tui/profile-edits.ts +0 -146
- package/src/entry/tui/profile-tree.ts +0 -429
- package/src/entry/tui/providers/app.tsx +0 -162
- package/src/entry/tui/providers/boundary.tsx +0 -57
- package/src/entry/tui/providers/data.tsx +0 -160
- package/src/entry/tui/providers/keymap.tsx +0 -166
- package/src/entry/tui/providers/root.tsx +0 -45
- package/src/entry/tui/providers/theme.tsx +0 -17
- package/src/entry/tui/providers/toast.tsx +0 -77
- package/src/entry/tui/run.tsx +0 -114
- package/src/entry/tui/state.ts +0 -400
- package/src/entry/tui/stories.ts +0 -114
- package/src/entry/tui/text.ts +0 -130
- package/src/entry/tui/theme.ts +0 -140
- package/src/entry/tui/views/app.tsx +0 -208
- package/src/entry/tui/views/dialogs.tsx +0 -41
- package/src/entry/tui/views/profile-actions.ts +0 -331
- package/src/entry/tui/views/profiles.tsx +0 -520
- package/src/entry/tui/views/runs.tsx +0 -619
- package/src/entry/tui/views/save-dialog.tsx +0 -344
- package/src/entry/tui/views/status.tsx +0 -213
- package/src/entry/tui/widgets/chrome.tsx +0 -114
- package/src/entry/tui/widgets/dialog-confirm.tsx +0 -58
- package/src/entry/tui/widgets/dialog-prompt.tsx +0 -52
- package/src/entry/tui/widgets/dialog-select.tsx +0 -212
- package/src/entry/tui/widgets/dialog.tsx +0 -68
- package/src/entry/tui/widgets/line.tsx +0 -92
- package/src/entry/tui/widgets/list.tsx +0 -197
- package/src/entry/tui/widgets/spinner.tsx +0 -49
- package/src/entry/tui/widgets/toast.tsx +0 -62
- package/src/infra/sources/arena.ts +0 -95
- package/src/infra/sources/artificial-analysis.ts +0 -119
- package/src/infra/sources/cache.ts +0 -126
- package/src/infra/sources/csv.ts +0 -38
- package/src/infra/sources/epoch.ts +0 -82
- package/src/infra/sources/http.ts +0 -75
- package/src/infra/sources/litellm.ts +0 -58
- package/src/infra/sources/models-dev.ts +0 -59
- package/src/infra/sources/openrouter-endpoints.ts +0 -52
- package/src/infra/sources/openrouter-models.ts +0 -60
- package/src/infra/sources/rows.ts +0 -38
- package/src/infra/sources/vectara.ts +0 -69
- package/src/infra/sources/zip.ts +0 -48
- package/src/services/catalog-refresh.ts +0 -201
- package/src/services/doctor-sources.ts +0 -76
- package/src/services/runs-page.ts +0 -290
- package/src/services/source-derive.ts +0 -397
- package/src/services/source-sync.ts +0 -312
- package/src/services/standins.ts +0 -238
- package/src/services/treat-likes.ts +0 -113
|
@@ -1,619 +0,0 @@
|
|
|
1
|
-
import { type MutableRefObject, useEffect, useMemo, useRef, useState } from "react";
|
|
2
|
-
import { waitingLine } from "../../../services/orchestrator-wait.ts";
|
|
3
|
-
import { useApp, useBack, useNow } from "../providers/app.tsx";
|
|
4
|
-
import { useData, usePoll } from "../providers/data.tsx";
|
|
5
|
-
import { useCommandLayer } from "../providers/keymap.tsx";
|
|
6
|
-
import { errorToast } from "../providers/toast.tsx";
|
|
7
|
-
import { useUi } from "../providers/theme.tsx";
|
|
8
|
-
import { isArmed } from "../state.ts";
|
|
9
|
-
import { ago, clock, plural, shortRung, wrap, wrapHanging } from "../text.ts";
|
|
10
|
-
import { glyph, mascot, type Token } from "../theme.ts";
|
|
11
|
-
import type { RoleRow, SessionDetail, SessionRow, SessionRun } from "../effects.ts";
|
|
12
|
-
import { Line, type Part } from "../widgets/line.tsx";
|
|
13
|
-
import { List, type ListItem, useSelected } from "../widgets/list.tsx";
|
|
14
|
-
|
|
15
|
-
/** How often an open screen is read again when its run folders cannot be watched, unless paused (spec §4). */
|
|
16
|
-
export const RUN_EVERY_MS = 1_000;
|
|
17
|
-
/** How often a watched screen is read again all the same: a run that joins the session has no watch yet. */
|
|
18
|
-
export const WATCHED_EVERY_MS = 5_000;
|
|
19
|
-
|
|
20
|
-
/**
|
|
21
|
-
* The selected row, where moving the cursor takes back a first ctrl+d (Ruling 3: moving or esc disarms),
|
|
22
|
-
* as the dialog list does.
|
|
23
|
-
*/
|
|
24
|
-
function useSelection(
|
|
25
|
-
initial: string | null = null,
|
|
26
|
-
): [string | null, (key: string) => void, () => string | null] {
|
|
27
|
-
const app = useApp();
|
|
28
|
-
const sel = useSelected(initial);
|
|
29
|
-
const select = (key: string) => {
|
|
30
|
-
if (key !== sel.current() && app.getState().armed) app.dispatch({ type: "disarm" });
|
|
31
|
-
sel.select(key);
|
|
32
|
-
};
|
|
33
|
-
return [sel.selected, select, sel.current];
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
/** One session of the top level: `● live <name> 2 runs · 3 live roles · 1 landed 4m ago` (spec §4). */
|
|
37
|
-
export function sessionParts(s: SessionRow, now: number, plain: boolean): Part[] {
|
|
38
|
-
return [
|
|
39
|
-
s.live
|
|
40
|
-
? { text: `${glyph("live", plain)} live `, tone: "info" }
|
|
41
|
-
: { text: `${glyph("dot", plain)} idle `, tone: "muted" },
|
|
42
|
-
{ text: `${s.name} `, bold: true },
|
|
43
|
-
{
|
|
44
|
-
text: `${plural(s.runs, "run")} · ${plural(s.liveRoles, "live role")} · ${s.landed} landed `,
|
|
45
|
-
tone: "muted",
|
|
46
|
-
},
|
|
47
|
-
{ text: ago(now - Date.parse(s.lastActivity)), tone: "muted" },
|
|
48
|
-
];
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
const keyOf = (k: string | null) => (k === null ? "earlier" : `s:${k}`);
|
|
52
|
-
const fromKey = (k: string) => (k === "earlier" ? null : k.slice("s:".length));
|
|
53
|
-
const roleKey = (run: string, dispatchId: string) => `role:${run}:${dispatchId}`;
|
|
54
|
-
const milestoneKey = (run: string, name: string) => `ms:${run}:${name}`;
|
|
55
|
-
|
|
56
|
-
/** What `r` re-reads: the open screen registers its read here (the command lives on the tab). */
|
|
57
|
-
type Refresher = MutableRefObject<(() => void) | null>;
|
|
58
|
-
|
|
59
|
-
/** Registers `refresh` as the open screen's while it is mounted. */
|
|
60
|
-
function useRefresher(refresher: Refresher, refresh: () => void) {
|
|
61
|
-
// after every commit, and cleared only while still its own: the screen that replaces this one registers
|
|
62
|
-
// after this one's cleanup has run
|
|
63
|
-
useEffect(() => {
|
|
64
|
-
refresher.current = refresh;
|
|
65
|
-
return () => {
|
|
66
|
-
if (refresher.current === refresh) refresher.current = null;
|
|
67
|
-
};
|
|
68
|
-
});
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
function SessionList(props: { width: number; height: number; initial: string | null }) {
|
|
72
|
-
const app = useApp();
|
|
73
|
-
const data = useData();
|
|
74
|
-
const ui = useUi();
|
|
75
|
-
const now = useNow(1_000);
|
|
76
|
-
const [selected, setSelected, selectedNow] = useSelection(props.initial);
|
|
77
|
-
const rows = data.sessions.value?.rows ?? [];
|
|
78
|
-
useCommandLayer("row.runs", {
|
|
79
|
-
"runs.open": () => {
|
|
80
|
-
const k = selectedNow();
|
|
81
|
-
if (k) app.dispatch({ type: "session", key: fromKey(k) });
|
|
82
|
-
},
|
|
83
|
-
});
|
|
84
|
-
const updated = app.state.paused
|
|
85
|
-
? "paused"
|
|
86
|
-
: data.sessions.at !== null
|
|
87
|
-
? `updated ${ago(now - data.sessions.at)}`
|
|
88
|
-
: "";
|
|
89
|
-
const items: ListItem[] = rows.map((s) => ({
|
|
90
|
-
key: keyOf(s.key),
|
|
91
|
-
selectable: true,
|
|
92
|
-
render: (sel, w) => (
|
|
93
|
-
<Line width={w} selected={sel} parts={[{ text: " " }, ...sessionParts(s, now, ui.plain)]} />
|
|
94
|
-
),
|
|
95
|
-
}));
|
|
96
|
-
const warnings = data.sessions.value?.warnings ?? [];
|
|
97
|
-
// a failed read says so, whether rows from an earlier good read are shown, none were, or none exist
|
|
98
|
-
const failed = data.sessions.error;
|
|
99
|
-
const failure = failed ? (
|
|
100
|
-
<Line
|
|
101
|
-
width={props.width}
|
|
102
|
-
parts={[{ text: ` ${glyph("fail", ui.plain)} could not read the runs: ${failed}`, tone: "error" }]}
|
|
103
|
-
/>
|
|
104
|
-
) : null;
|
|
105
|
-
if (rows.length === 0 && data.sessions.value) {
|
|
106
|
-
const art = mascot("waiting");
|
|
107
|
-
return (
|
|
108
|
-
<box flexDirection="column" width={props.width} height={props.height} paddingTop={failed ? 1 : 2}>
|
|
109
|
-
{failure}
|
|
110
|
-
{art.map((l) => (
|
|
111
|
-
<Line key={l} width={props.width} parts={[{ text: ` ${l}`, tone: "muted" }]} />
|
|
112
|
-
))}
|
|
113
|
-
<Line
|
|
114
|
-
width={props.width}
|
|
115
|
-
parts={[
|
|
116
|
-
{ text: " No runs yet. Start one in Claude Code: /catherd <what to build>", tone: "muted" },
|
|
117
|
-
]}
|
|
118
|
-
/>
|
|
119
|
-
</box>
|
|
120
|
-
);
|
|
121
|
-
}
|
|
122
|
-
return (
|
|
123
|
-
<box flexDirection="column" width={props.width} height={props.height}>
|
|
124
|
-
<Line
|
|
125
|
-
width={props.width}
|
|
126
|
-
parts={[
|
|
127
|
-
{ text: " SESSIONS", bold: true },
|
|
128
|
-
{ text: ` ${updated}`, tone: app.state.paused ? "warning" : "muted" },
|
|
129
|
-
]}
|
|
130
|
-
/>
|
|
131
|
-
{failure}
|
|
132
|
-
<List
|
|
133
|
-
items={items}
|
|
134
|
-
selected={selected}
|
|
135
|
-
onSelect={setSelected}
|
|
136
|
-
width={props.width}
|
|
137
|
-
height={props.height - 1 - (failed ? 1 : 0) - Math.min(2, warnings.length)}
|
|
138
|
-
filter={null}
|
|
139
|
-
empty={failed ? "no runs read yet" : "reading runs…"}
|
|
140
|
-
/>
|
|
141
|
-
{warnings.slice(0, 2).map((w) => (
|
|
142
|
-
<Line
|
|
143
|
-
key={w}
|
|
144
|
-
width={props.width}
|
|
145
|
-
parts={[{ text: ` ${glyph("warn", ui.plain)} ${w}`, tone: "warning" }]}
|
|
146
|
-
/>
|
|
147
|
-
))}
|
|
148
|
-
</box>
|
|
149
|
-
);
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
/** A finished role's glyph and colour by its record's status; a live one's by running or starting. */
|
|
153
|
-
function roleMark(r: RoleRow, plain: boolean): Part {
|
|
154
|
-
if (r.live) return { text: glyph(r.status === "running" ? "live" : "waiting", plain), tone: "info" };
|
|
155
|
-
const tone: Token =
|
|
156
|
-
r.status === "ok"
|
|
157
|
-
? "success"
|
|
158
|
-
: r.status === "cancelled"
|
|
159
|
-
? "muted"
|
|
160
|
-
: r.status === "limit"
|
|
161
|
-
? "warning"
|
|
162
|
-
: "error";
|
|
163
|
-
return { text: glyph(r.status === "ok" ? "ok" : r.status === "cancelled" ? "skip" : "fail", plain), tone };
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
/** A run's heading on its session's screen: title, repo, when it started, its budget, where it moved. */
|
|
167
|
-
function runHeading(r: SessionRun, now: number, plain: boolean): Part[] {
|
|
168
|
-
const bits = [r.repo, `started ${ago(now - Date.parse(r.createdAt))}`];
|
|
169
|
-
if (r.budget !== null) bits.push(`budget ${Math.round(r.budget * 100)}%`);
|
|
170
|
-
const waiting = waitingLine(r.waiting, now);
|
|
171
|
-
if (waiting) bits.push(waiting);
|
|
172
|
-
const moved =
|
|
173
|
-
r.continued === "here" ? "continued here" : r.continuedIn ? `continued in ${r.continuedIn}` : null;
|
|
174
|
-
return [
|
|
175
|
-
{ text: ` ${glyph("shut", plain)} ` },
|
|
176
|
-
{ text: r.title, bold: true },
|
|
177
|
-
{ text: ` ${bits.join(" · ")}`, tone: "muted" },
|
|
178
|
-
...(moved ? [{ text: ` · ${moved}`, tone: "info" as Token }] : []),
|
|
179
|
-
];
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
/** Spec §4: the open screen redraws when a run file changes, else every second; `p` stops both. */
|
|
183
|
-
function useLiveRead<T extends { dirs: string[] }>(read: () => T, key: string) {
|
|
184
|
-
const app = useApp();
|
|
185
|
-
const [watching, setWatching] = useState(false);
|
|
186
|
-
const polled = usePoll(read, watching ? WATCHED_EVERY_MS : RUN_EVERY_MS, { paused: app.state.paused, key });
|
|
187
|
-
// the watch calls the latest refresh; the ref moves after each commit, never during a render
|
|
188
|
-
const refresh = useRef(polled.refresh);
|
|
189
|
-
useEffect(() => {
|
|
190
|
-
refresh.current = polled.refresh;
|
|
191
|
-
});
|
|
192
|
-
const dirs = polled.value?.dirs.join("\n") ?? "";
|
|
193
|
-
useEffect(() => {
|
|
194
|
-
if (app.state.paused || dirs === "") {
|
|
195
|
-
setWatching(false);
|
|
196
|
-
return;
|
|
197
|
-
}
|
|
198
|
-
const stop = app.effects.watch(dirs.split("\n"), () => refresh.current());
|
|
199
|
-
setWatching(stop !== null);
|
|
200
|
-
return () => stop?.();
|
|
201
|
-
}, [dirs, app.state.paused, app.effects]);
|
|
202
|
-
return { ...polled, watching };
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
/**
|
|
206
|
-
* The row a session's screen starts on before the cursor moves: the first row of the run it was opened for,
|
|
207
|
-
* else its first live role (live roles can sit below many landed milestones), else its first row.
|
|
208
|
-
*/
|
|
209
|
-
function startRow(d: SessionDetail, run: string | undefined): string | null {
|
|
210
|
-
const rows = (r: SessionDetail["runs"][number]) => [
|
|
211
|
-
...r.milestones.map((m) => milestoneKey(r.id, m.name)),
|
|
212
|
-
...r.roles.map((x) => roleKey(x.run, x.dispatchId)),
|
|
213
|
-
];
|
|
214
|
-
const asked = run === undefined ? undefined : d.runs.find((r) => r.id === run);
|
|
215
|
-
const first = asked ? rows(asked)[0] : undefined;
|
|
216
|
-
if (first) return first;
|
|
217
|
-
const live = d.runs.flatMap((r) => r.roles).find((x) => x.live);
|
|
218
|
-
if (live) return roleKey(live.run, live.dispatchId);
|
|
219
|
-
return d.runs.flatMap(rows)[0] ?? null;
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
function SessionView(props: {
|
|
223
|
-
sessionKey: string | null;
|
|
224
|
-
run: string | undefined;
|
|
225
|
-
width: number;
|
|
226
|
-
height: number;
|
|
227
|
-
initial: string | null;
|
|
228
|
-
refresher: Refresher;
|
|
229
|
-
}) {
|
|
230
|
-
const app = useApp();
|
|
231
|
-
const ui = useUi();
|
|
232
|
-
const now = useNow(1_000);
|
|
233
|
-
const [chosen, setSelected, chosenNow] = useSelection(props.initial);
|
|
234
|
-
const polled = useLiveRead(() => app.effects.session(props.sessionKey), String(props.sessionKey));
|
|
235
|
-
useRefresher(props.refresher, polled.refresh);
|
|
236
|
-
useBack(true, "view", () => app.dispatch({ type: "up" }));
|
|
237
|
-
const d = polled.value;
|
|
238
|
-
// each row's role or milestone by its key, built once per read rather than on every key
|
|
239
|
-
const byKey = useMemo(() => {
|
|
240
|
-
const roles = new Map<string, RoleRow>();
|
|
241
|
-
const milestones = new Map<string, { run: string; name: string }>();
|
|
242
|
-
for (const r of d?.runs ?? []) {
|
|
243
|
-
for (const m of r.milestones) milestones.set(milestoneKey(r.id, m.name), { run: r.id, name: m.name });
|
|
244
|
-
for (const x of r.roles) roles.set(roleKey(x.run, x.dispatchId), x);
|
|
245
|
-
}
|
|
246
|
-
return { roles, milestones, start: d ? startRow(d, props.run) : null };
|
|
247
|
-
}, [d, props.run]);
|
|
248
|
-
// until the cursor moves, it stands on the start row
|
|
249
|
-
const selected = chosen ?? byKey.start;
|
|
250
|
-
const selectedNow = () => chosenNow() ?? byKey.start;
|
|
251
|
-
const roleAt = (key: string | null) => (key === null ? null : (byKey.roles.get(key) ?? null));
|
|
252
|
-
const milestoneAt = (key: string | null) => (key === null ? null : (byKey.milestones.get(key) ?? null));
|
|
253
|
-
useCommandLayer("row.runs", {
|
|
254
|
-
"runs.open": () => {
|
|
255
|
-
const k = selectedNow();
|
|
256
|
-
const m = milestoneAt(k);
|
|
257
|
-
if (m) return app.dispatch({ type: "milestone", run: m.run, name: m.name });
|
|
258
|
-
const r = roleAt(k);
|
|
259
|
-
if (r) app.dispatch({ type: "role", run: r.run, dispatchId: r.dispatchId });
|
|
260
|
-
},
|
|
261
|
-
"runs.cancel": () => {
|
|
262
|
-
const r = roleAt(selectedNow());
|
|
263
|
-
if (!r?.live) return;
|
|
264
|
-
const target = `${r.run}/${r.name}`;
|
|
265
|
-
const t = app.clock.now();
|
|
266
|
-
if (!isArmed(app.getState(), "cancel", target, t))
|
|
267
|
-
return app.dispatch({ type: "arm", what: "cancel", target, at: t });
|
|
268
|
-
app.dispatch({ type: "disarm" });
|
|
269
|
-
void app.effects.cancel(r.run, r.name).then(
|
|
270
|
-
(msg) => {
|
|
271
|
-
app.toast({ variant: "success", message: msg });
|
|
272
|
-
polled.refresh();
|
|
273
|
-
},
|
|
274
|
-
(e: unknown) => app.toast(errorToast(e)),
|
|
275
|
-
);
|
|
276
|
-
},
|
|
277
|
-
});
|
|
278
|
-
if (!d)
|
|
279
|
-
return (
|
|
280
|
-
<Line
|
|
281
|
-
width={props.width}
|
|
282
|
-
parts={[
|
|
283
|
-
{
|
|
284
|
-
text: polled.error ? ` ${polled.error}` : " reading the session…",
|
|
285
|
-
tone: polled.error ? "error" : "muted",
|
|
286
|
-
},
|
|
287
|
-
]}
|
|
288
|
-
/>
|
|
289
|
-
);
|
|
290
|
-
const items: ListItem[] = [];
|
|
291
|
-
const text = (key: string, parts: Part[]) =>
|
|
292
|
-
items.push({ key, selectable: false, render: (_sel, w) => <Line width={w} parts={parts} /> });
|
|
293
|
-
for (const r of d.runs) {
|
|
294
|
-
text(`run:${r.id}`, runHeading(r, now, ui.plain));
|
|
295
|
-
// spec 1.1 §10: each milestone is a row; enter opens it on its digest
|
|
296
|
-
for (const m of r.milestones)
|
|
297
|
-
items.push({
|
|
298
|
-
key: milestoneKey(r.id, m.name),
|
|
299
|
-
selectable: true,
|
|
300
|
-
render: (sel, w) => (
|
|
301
|
-
<Line
|
|
302
|
-
width={w}
|
|
303
|
-
selected={sel}
|
|
304
|
-
parts={[
|
|
305
|
-
{ text: " " },
|
|
306
|
-
m.landed
|
|
307
|
-
? { text: `${glyph("ok", ui.plain)} ${m.name}`, tone: "success" }
|
|
308
|
-
: { text: `${glyph("waiting", ui.plain)} ${m.name}`, tone: "muted" },
|
|
309
|
-
m.landed
|
|
310
|
-
? { text: m.what ? ` ${m.what}` : "", tone: "muted" }
|
|
311
|
-
: { text: ` not landed${m.what ? ` · ${m.what}` : ""}`, tone: "muted" },
|
|
312
|
-
]}
|
|
313
|
-
/>
|
|
314
|
-
),
|
|
315
|
-
});
|
|
316
|
-
if (r.roles.length === 0) text(`none:${r.id}`, [{ text: " no role has run yet", tone: "muted" }]);
|
|
317
|
-
for (const x of r.roles) {
|
|
318
|
-
const secs = x.live ? (now - Date.parse(x.since)) / 1000 : (x.secs ?? 0);
|
|
319
|
-
const armed = isArmed(app.state, "cancel", `${x.run}/${x.name}`, app.clock.now());
|
|
320
|
-
items.push({
|
|
321
|
-
key: roleKey(x.run, x.dispatchId),
|
|
322
|
-
selectable: true,
|
|
323
|
-
render: (sel, w) => (
|
|
324
|
-
<Line
|
|
325
|
-
width={w}
|
|
326
|
-
selected={sel}
|
|
327
|
-
parts={[
|
|
328
|
-
{ text: " " },
|
|
329
|
-
roleMark(x, ui.plain),
|
|
330
|
-
{ text: ` ${x.name.padEnd(15)} `, bold: true },
|
|
331
|
-
{ text: shortRung(x.rung).padEnd(19) },
|
|
332
|
-
{ text: x.status.padEnd(9), tone: x.live ? "info" : "muted" },
|
|
333
|
-
{ text: clock(secs).padEnd(7) },
|
|
334
|
-
armed
|
|
335
|
-
? { text: "press ctrl+d again to cancel", tone: "warning" }
|
|
336
|
-
: { text: x.lastEvent ?? "", tone: "muted" },
|
|
337
|
-
]}
|
|
338
|
-
/>
|
|
339
|
-
),
|
|
340
|
-
});
|
|
341
|
-
}
|
|
342
|
-
}
|
|
343
|
-
const s = d.session;
|
|
344
|
-
const updated = app.state.paused ? "paused" : polled.at !== null ? `updated ${ago(now - polled.at)}` : "";
|
|
345
|
-
return (
|
|
346
|
-
<box flexDirection="column" width={props.width} height={props.height}>
|
|
347
|
-
<Line
|
|
348
|
-
width={props.width}
|
|
349
|
-
parts={[
|
|
350
|
-
{ text: ` ${s.name} `, bold: true },
|
|
351
|
-
s.live
|
|
352
|
-
? { text: `${glyph("live", ui.plain)} live`, tone: "info" }
|
|
353
|
-
: { text: `${glyph("dot", ui.plain)} idle`, tone: "muted" },
|
|
354
|
-
{ text: ` · ${plural(s.runs, "run")} · ${plural(s.liveRoles, "live role")} · `, tone: "muted" },
|
|
355
|
-
{ text: updated, tone: app.state.paused ? "warning" : "muted" },
|
|
356
|
-
{
|
|
357
|
-
text: polled.error ? ` · ${glyph("fail", ui.plain)} could not read it: ${polled.error}` : "",
|
|
358
|
-
tone: "error",
|
|
359
|
-
},
|
|
360
|
-
]}
|
|
361
|
-
/>
|
|
362
|
-
<List
|
|
363
|
-
items={items}
|
|
364
|
-
selected={selected}
|
|
365
|
-
onSelect={setSelected}
|
|
366
|
-
width={props.width}
|
|
367
|
-
height={props.height - 1}
|
|
368
|
-
filter={null}
|
|
369
|
-
empty=""
|
|
370
|
-
/>
|
|
371
|
-
</box>
|
|
372
|
-
);
|
|
373
|
-
}
|
|
374
|
-
|
|
375
|
-
function RoleView(props: {
|
|
376
|
-
run: string;
|
|
377
|
-
dispatchId: string;
|
|
378
|
-
width: number;
|
|
379
|
-
height: number;
|
|
380
|
-
refresher: Refresher;
|
|
381
|
-
}) {
|
|
382
|
-
const app = useApp();
|
|
383
|
-
const ui = useUi();
|
|
384
|
-
const [selected, setSelected] = useSelection();
|
|
385
|
-
// a role with its record is finished for good: nothing on its screen changes, so it is read no more
|
|
386
|
-
// (r still reads it again)
|
|
387
|
-
const [done, setDone] = useState(false);
|
|
388
|
-
const polled = usePoll(() => app.effects.role(props.run, props.dispatchId), RUN_EVERY_MS, {
|
|
389
|
-
paused: app.state.paused || done,
|
|
390
|
-
key: `${props.run}/${props.dispatchId}`,
|
|
391
|
-
});
|
|
392
|
-
const recorded = polled.value?.record != null;
|
|
393
|
-
useEffect(() => {
|
|
394
|
-
if (recorded) setDone(true);
|
|
395
|
-
}, [recorded]);
|
|
396
|
-
useRefresher(props.refresher, polled.refresh);
|
|
397
|
-
useBack(true, "view", () => app.dispatch({ type: "up" }));
|
|
398
|
-
const d = polled.value;
|
|
399
|
-
if (!d)
|
|
400
|
-
return (
|
|
401
|
-
<Line
|
|
402
|
-
width={props.width}
|
|
403
|
-
parts={[
|
|
404
|
-
{
|
|
405
|
-
text: polled.error ? ` ${polled.error}` : " reading the role…",
|
|
406
|
-
tone: polled.error ? "error" : "muted",
|
|
407
|
-
},
|
|
408
|
-
]}
|
|
409
|
-
/>
|
|
410
|
-
);
|
|
411
|
-
const items: ListItem[] = [];
|
|
412
|
-
const room = Math.max(10, props.width - 4);
|
|
413
|
-
let n = 0;
|
|
414
|
-
const line = (parts: Part[]) =>
|
|
415
|
-
items.push({
|
|
416
|
-
key: `l${n++}`,
|
|
417
|
-
selectable: true,
|
|
418
|
-
render: (sel, w) => <Line width={w} selected={sel} parts={parts} />,
|
|
419
|
-
});
|
|
420
|
-
const section = (title: string, body: string, none: string) => {
|
|
421
|
-
line([{ text: ` ${title}`, bold: true }]);
|
|
422
|
-
const lines = body.trimEnd() ? body.trimEnd().split("\n") : [];
|
|
423
|
-
if (lines.length === 0) line([{ text: ` ${none}`, tone: "muted" }]);
|
|
424
|
-
for (const l of lines) for (const w of wrap(l, room)) line([{ text: ` ${w}` }]);
|
|
425
|
-
};
|
|
426
|
-
section("BRIEF", d.brief, "no brief");
|
|
427
|
-
section("REPLY", d.reply, d.state === "finished" ? "no reply" : "no reply yet: the role is running");
|
|
428
|
-
line([{ text: " RECORD", bold: true }]);
|
|
429
|
-
const r = d.record;
|
|
430
|
-
if (!r) line([{ text: " none yet: the role is running", tone: "muted" }]);
|
|
431
|
-
else {
|
|
432
|
-
const k = (x: number) => `${Math.round(x / 1000)}k`;
|
|
433
|
-
line([
|
|
434
|
-
{ text: ` ${r.status}`, tone: r.status === "ok" ? "success" : "error" },
|
|
435
|
-
{
|
|
436
|
-
text: r.replyStatus
|
|
437
|
-
? ` · STATUS ${r.replyStatus}${r.replyWhy ? ` — ${r.replyWhy}` : ""}`
|
|
438
|
-
: " · no STATUS",
|
|
439
|
-
},
|
|
440
|
-
]);
|
|
441
|
-
line([
|
|
442
|
-
{
|
|
443
|
-
text: ` ${clock(r.secs)} · ${k(r.tokens.input)} in (${k(r.tokens.cached)} cached) · ${k(r.tokens.output)} out`,
|
|
444
|
-
tone: "muted",
|
|
445
|
-
},
|
|
446
|
-
]);
|
|
447
|
-
line([{ text: ` changed ${r.changedOwned.join(", ") || "nothing it owns"}`, tone: "muted" }]);
|
|
448
|
-
if (r.violations.length)
|
|
449
|
-
line([{ text: ` outside its lane: ${r.violations.join(", ")}`, tone: "warning" }]);
|
|
450
|
-
if (r.thread) line([{ text: ` thread ${r.thread}`, tone: "muted" }]);
|
|
451
|
-
}
|
|
452
|
-
return (
|
|
453
|
-
<box flexDirection="column" width={props.width} height={props.height}>
|
|
454
|
-
<Line
|
|
455
|
-
width={props.width}
|
|
456
|
-
parts={[
|
|
457
|
-
{ text: ` ${d.name}`, bold: true },
|
|
458
|
-
{
|
|
459
|
-
text: ` ${d.role} · ${shortRung(d.rung)} · ${r?.status ?? d.state} · ${d.runTitle}`,
|
|
460
|
-
tone: "muted",
|
|
461
|
-
},
|
|
462
|
-
{
|
|
463
|
-
text: polled.error ? ` · ${glyph("fail", ui.plain)} could not read it: ${polled.error}` : "",
|
|
464
|
-
tone: "error",
|
|
465
|
-
},
|
|
466
|
-
]}
|
|
467
|
-
/>
|
|
468
|
-
<List
|
|
469
|
-
items={items}
|
|
470
|
-
selected={selected}
|
|
471
|
-
onSelect={setSelected}
|
|
472
|
-
width={props.width}
|
|
473
|
-
height={props.height - 1}
|
|
474
|
-
filter={null}
|
|
475
|
-
empty=""
|
|
476
|
-
/>
|
|
477
|
-
</box>
|
|
478
|
-
);
|
|
479
|
-
}
|
|
480
|
-
|
|
481
|
-
/** Spec 1.1 §10: a milestone's screen, the digest `land` wrote for it. */
|
|
482
|
-
function MilestoneView(props: {
|
|
483
|
-
run: string;
|
|
484
|
-
name: string;
|
|
485
|
-
width: number;
|
|
486
|
-
height: number;
|
|
487
|
-
refresher: Refresher;
|
|
488
|
-
}) {
|
|
489
|
-
const app = useApp();
|
|
490
|
-
const ui = useUi();
|
|
491
|
-
const [selected, setSelected] = useSelection();
|
|
492
|
-
const polled = usePoll(() => app.effects.milestone(props.run, props.name), RUN_EVERY_MS, {
|
|
493
|
-
paused: app.state.paused,
|
|
494
|
-
key: `${props.run}/${props.name}`,
|
|
495
|
-
});
|
|
496
|
-
useRefresher(props.refresher, polled.refresh);
|
|
497
|
-
useBack(true, "view", () => app.dispatch({ type: "up" }));
|
|
498
|
-
const d = polled.value;
|
|
499
|
-
if (!d)
|
|
500
|
-
return (
|
|
501
|
-
<Line
|
|
502
|
-
width={props.width}
|
|
503
|
-
parts={[
|
|
504
|
-
{
|
|
505
|
-
text: polled.error ? ` ${polled.error}` : " reading the milestone…",
|
|
506
|
-
tone: polled.error ? "error" : "muted",
|
|
507
|
-
},
|
|
508
|
-
]}
|
|
509
|
-
/>
|
|
510
|
-
);
|
|
511
|
-
const items: ListItem[] = [];
|
|
512
|
-
const room = Math.max(10, props.width - 4);
|
|
513
|
-
let n = 0;
|
|
514
|
-
const line = (parts: Part[]) =>
|
|
515
|
-
items.push({
|
|
516
|
-
key: `l${n++}`,
|
|
517
|
-
selectable: true,
|
|
518
|
-
render: (sel, w) => <Line width={w} selected={sel} parts={parts} />,
|
|
519
|
-
});
|
|
520
|
-
line([{ text: " DIGEST", bold: true }]);
|
|
521
|
-
const body = d.digest?.trimEnd() ?? "";
|
|
522
|
-
if (!body)
|
|
523
|
-
line([
|
|
524
|
-
{
|
|
525
|
-
text: ` no digest yet${d.landed ? "" : ": the milestone has not landed"}`,
|
|
526
|
-
tone: "muted",
|
|
527
|
-
},
|
|
528
|
-
]);
|
|
529
|
-
else for (const l of body.split("\n")) for (const w of wrapHanging(l, room)) line([{ text: ` ${w}` }]);
|
|
530
|
-
return (
|
|
531
|
-
<box flexDirection="column" width={props.width} height={props.height}>
|
|
532
|
-
<Line
|
|
533
|
-
width={props.width}
|
|
534
|
-
parts={[
|
|
535
|
-
{ text: ` ${d.name}`, bold: true },
|
|
536
|
-
{
|
|
537
|
-
text: ` ${d.what ? `${d.what} · ` : ""}${d.landed ? "landed" : "not landed"} · ${d.runTitle}`,
|
|
538
|
-
tone: "muted",
|
|
539
|
-
},
|
|
540
|
-
{
|
|
541
|
-
text: polled.error ? ` · ${glyph("fail", ui.plain)} could not read it: ${polled.error}` : "",
|
|
542
|
-
tone: "error",
|
|
543
|
-
},
|
|
544
|
-
]}
|
|
545
|
-
/>
|
|
546
|
-
<List
|
|
547
|
-
items={items}
|
|
548
|
-
selected={selected}
|
|
549
|
-
onSelect={setSelected}
|
|
550
|
-
width={props.width}
|
|
551
|
-
height={props.height - 1}
|
|
552
|
-
filter={null}
|
|
553
|
-
empty=""
|
|
554
|
-
/>
|
|
555
|
-
</box>
|
|
556
|
-
);
|
|
557
|
-
}
|
|
558
|
-
|
|
559
|
-
/**
|
|
560
|
-
* Spec §4 (tab 3): the sessions, newest activity first; enter opens a session (its runs, milestones and roles,
|
|
561
|
-
* live ones first), enter on a milestone opens its digest (spec 1.1 §10), enter on a role opens it (brief,
|
|
562
|
-
* reply, record); esc goes back one level; p pauses.
|
|
563
|
-
*/
|
|
564
|
-
export function RunsView(props: { width: number; height: number }) {
|
|
565
|
-
const app = useApp();
|
|
566
|
-
const data = useData();
|
|
567
|
-
const refresher: Refresher = useRef(null);
|
|
568
|
-
useCommandLayer("tab.runs", {
|
|
569
|
-
"runs.refresh": () => (refresher.current ?? data.sessions.refresh)(),
|
|
570
|
-
"runs.pause": () => {
|
|
571
|
-
app.dispatch({ type: "pause" });
|
|
572
|
-
app.toast({ variant: "info", message: app.getState().paused ? "Updates paused" : "Updates resumed" });
|
|
573
|
-
},
|
|
574
|
-
});
|
|
575
|
-
const { milestone, role, session, back } = app.state;
|
|
576
|
-
// esc lands on the row it came from: the last session opened, and the last milestone or role opened in it
|
|
577
|
-
const backRow =
|
|
578
|
-
back && session && back.session === session.key
|
|
579
|
-
? back.milestone
|
|
580
|
-
? milestoneKey(back.milestone.run, back.milestone.name)
|
|
581
|
-
: back.role
|
|
582
|
-
? roleKey(back.role.run, back.role.dispatchId)
|
|
583
|
-
: null
|
|
584
|
-
: null;
|
|
585
|
-
if (milestone)
|
|
586
|
-
return (
|
|
587
|
-
<MilestoneView
|
|
588
|
-
run={milestone.run}
|
|
589
|
-
name={milestone.name}
|
|
590
|
-
width={props.width}
|
|
591
|
-
height={props.height}
|
|
592
|
-
refresher={refresher}
|
|
593
|
-
/>
|
|
594
|
-
);
|
|
595
|
-
if (role)
|
|
596
|
-
return (
|
|
597
|
-
<RoleView
|
|
598
|
-
run={role.run}
|
|
599
|
-
dispatchId={role.dispatchId}
|
|
600
|
-
width={props.width}
|
|
601
|
-
height={props.height}
|
|
602
|
-
refresher={refresher}
|
|
603
|
-
/>
|
|
604
|
-
);
|
|
605
|
-
if (session)
|
|
606
|
-
return (
|
|
607
|
-
<SessionView
|
|
608
|
-
sessionKey={session.key}
|
|
609
|
-
run={session.run}
|
|
610
|
-
width={props.width}
|
|
611
|
-
height={props.height}
|
|
612
|
-
initial={backRow}
|
|
613
|
-
refresher={refresher}
|
|
614
|
-
/>
|
|
615
|
-
);
|
|
616
|
-
return (
|
|
617
|
-
<SessionList width={props.width} height={props.height} initial={back ? keyOf(back.session) : null} />
|
|
618
|
-
);
|
|
619
|
-
}
|