catherd-cli 1.4.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 +188 -169
- package/THIRD_PARTY_NOTICES.md +217 -35
- package/catalog/jev.json +27 -1
- package/catalog/models.json +56 -239
- 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 -356
- package/plugin/skills/catherd-setup/SKILL.md +21 -20
- package/src/adapters/all.ts +0 -4
- package/src/adapters/backend.ts +19 -4
- package/src/adapters/claude-code/index.ts +126 -14
- package/src/adapters/codex/events.ts +10 -0
- package/src/adapters/codex/index.ts +17 -5
- package/src/adapters/cursor/home.ts +1 -1
- package/src/adapters/cursor/index.ts +10 -4
- package/src/adapters/opencode/index.ts +38 -1
- package/src/cli.ts +27 -27
- package/src/domain/agents.ts +3 -1
- 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 +106 -0
- package/src/domain/catalog.ts +69 -313
- package/src/domain/changes.ts +23 -1
- package/src/domain/cost.ts +19 -29
- package/src/domain/errors.ts +6 -2
- package/src/domain/fail-lines.ts +26 -0
- package/src/domain/failover.ts +37 -75
- package/src/domain/findings.ts +47 -0
- package/src/domain/gate-brief.ts +53 -0
- package/src/domain/hints.ts +13 -2
- 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 +118 -3
- package/src/domain/milestone-memory.ts +53 -0
- package/src/domain/notice.ts +32 -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 +175 -138
- package/src/domain/profile.ts +140 -63
- package/src/domain/record.ts +68 -3
- package/src/domain/reply.ts +29 -0
- package/src/domain/role-prompts.ts +182 -56
- package/src/domain/role-scope.ts +76 -0
- package/src/domain/role-tools.ts +34 -0
- package/src/domain/roles.ts +2 -0
- package/src/domain/route.ts +90 -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 +229 -72
- package/src/domain/slopalytics.ts +72 -0
- package/src/domain/state.ts +5 -0
- package/src/domain/util.ts +22 -0
- package/src/domain/workspace.ts +130 -0
- package/src/entry/answer-command.ts +39 -0
- package/src/entry/catalog-command.ts +24 -165
- package/src/entry/check-bin.ts +17 -0
- package/src/entry/cli-kit.ts +18 -2
- package/src/entry/daemon-bin.ts +22 -0
- package/src/entry/deps.ts +2 -0
- package/src/entry/doctor-command.ts +20 -2
- package/src/entry/glyphs.ts +15 -2
- package/src/entry/host-arg.ts +28 -0
- package/src/entry/init-command.ts +20 -69
- package/src/entry/knowledge-command.ts +121 -5
- package/src/entry/lock-command.ts +103 -8
- package/src/entry/mcp/handshake.ts +6 -2
- package/src/entry/mcp/role-bin.ts +20 -0
- package/src/entry/mcp/role-server.ts +134 -0
- package/src/entry/mcp/server.ts +85 -21
- 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/pause-command.ts +67 -0
- package/src/entry/profile-command.ts +33 -18
- package/src/entry/role-cli-command.ts +159 -0
- package/src/entry/runs-command.ts +81 -27
- package/src/entry/scorecard-command.ts +187 -0
- package/src/entry/supervise-bin.ts +1 -1
- package/src/entry/supervise-command.ts +30 -3
- package/src/entry/workspace-command.ts +44 -0
- package/src/infra/boot-lock.ts +25 -0
- package/src/infra/delivery.ts +3 -0
- package/src/infra/dispatch-dir.ts +2 -0
- package/src/infra/env.ts +11 -3
- package/src/infra/git.ts +70 -0
- package/src/infra/heavy-lock.ts +133 -6
- package/src/infra/host-probe.ts +16 -0
- package/src/infra/launch.ts +63 -7
- package/src/infra/lock-activity.ts +88 -0
- package/src/infra/login-env.ts +48 -0
- 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 +37 -2
- package/src/infra/role-mcp.ts +69 -0
- package/src/infra/slopalytics.ts +30 -0
- package/src/infra/store.ts +6 -1
- package/src/infra/supervisor.ts +107 -16
- package/src/services/admission.ts +133 -31
- package/src/services/backends.ts +4 -6
- package/src/services/budget.ts +26 -6
- package/src/services/capture.ts +1 -22
- package/src/services/catalog-service.ts +155 -254
- package/src/services/credentials.ts +15 -26
- package/src/services/daemon.ts +181 -0
- package/src/services/dispatch-service.ts +209 -17
- package/src/services/dispatches.ts +30 -2
- package/src/services/doctor-access.ts +8 -3
- package/src/services/doctor-backends.ts +1 -1
- package/src/services/doctor-checks.ts +47 -0
- package/src/services/doctor-docker.ts +221 -0
- package/src/services/doctor-role-mcp.ts +65 -0
- package/src/services/doctor.ts +72 -15
- package/src/services/end-push.ts +157 -0
- package/src/services/failover-rungs.ts +109 -0
- package/src/services/finalize.ts +24 -2
- package/src/services/gate-env.ts +217 -0
- package/src/services/gate-service.ts +231 -45
- package/src/services/global-install.ts +46 -13
- package/src/services/jev-service.ts +2 -4
- package/src/services/lane-edit.ts +154 -0
- package/src/services/lane-service.ts +291 -55
- package/src/services/milestone-check.ts +433 -0
- package/src/services/milestone-tree.ts +170 -0
- package/src/services/milestones.ts +167 -29
- package/src/services/notifier.ts +124 -4
- package/src/services/notify-service.ts +26 -0
- package/src/services/orchestrator-wait.ts +82 -0
- package/src/services/panel-setup.ts +116 -0
- package/src/services/pause.ts +161 -0
- package/src/services/peek.ts +79 -17
- package/src/services/plan-input.ts +151 -0
- package/src/services/ports.ts +60 -5
- package/src/services/preflight.ts +232 -24
- package/src/services/profile-service.ts +31 -3
- package/src/services/profile-store.ts +18 -6
- package/src/services/protocol.ts +123 -25
- package/src/services/provenance.ts +34 -89
- package/src/services/questions.ts +24 -0
- package/src/services/reconcile.ts +33 -1
- package/src/services/reentry.ts +7 -6
- package/src/services/role-access.ts +27 -0
- package/src/services/routing-service.ts +161 -42
- 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-pin.ts +125 -0
- package/src/services/run-read.ts +221 -0
- package/src/services/run-report.ts +176 -0
- package/src/services/run-service.ts +204 -23
- package/src/services/run-store.ts +159 -18
- package/src/services/scorecard-metrics.ts +86 -0
- package/src/services/scorecard.ts +382 -0
- package/src/services/scratch.ts +60 -0
- package/src/services/sessions.ts +3 -1
- package/src/services/state.ts +21 -0
- package/src/services/summary.ts +76 -12
- package/src/services/tree-path.ts +27 -0
- package/src/services/verifier-brief.ts +23 -0
- package/src/services/verifier-step.ts +48 -0
- package/src/services/workspace-admission.ts +259 -0
- package/src/services/workspace-service.ts +259 -0
- package/src/services/workspace-store.ts +95 -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 -36
- 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 -320
- package/src/adapters/antigravity/models.ts +0 -37
- package/src/adapters/grok/events.ts +0 -112
- package/src/adapters/grok/home.ts +0 -132
- package/src/adapters/grok/index.ts +0 -339
- package/src/adapters/grok/models.ts +0 -48
- package/src/domain/calibration.ts +0 -125
- package/src/domain/sources.ts +0 -144
- package/src/entry/mcp/dispatch-tools.ts +0 -52
- package/src/entry/mcp/lane-tools.ts +0 -84
- package/src/entry/mcp/protocol-tools.ts +0 -57
- package/src/entry/mcp/run-tools.ts +0 -148
- package/src/entry/mcp/setup-tools.ts +0 -108
- package/src/entry/tui/commands.ts +0 -563
- package/src/entry/tui/effects.ts +0 -381
- 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 -373
- 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 -573
- package/src/entry/tui/views/save-dialog.tsx +0 -340
- package/src/entry/tui/views/status.tsx +0 -209
- 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 -87
- package/src/infra/sources/artificial-analysis.ts +0 -112
- package/src/infra/sources/cache.ts +0 -124
- 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 -255
- package/src/services/source-derive.ts +0 -374
- package/src/services/source-sync.ts +0 -305
- package/src/services/standins.ts +0 -204
- package/src/services/treat-likes.ts +0 -97
|
@@ -1,162 +0,0 @@
|
|
|
1
|
-
import type { Clock } from "@opentui/core";
|
|
2
|
-
import { useKeymap } from "@opentui/keymap/react";
|
|
3
|
-
import {
|
|
4
|
-
createContext,
|
|
5
|
-
type ReactNode,
|
|
6
|
-
useContext,
|
|
7
|
-
useEffect,
|
|
8
|
-
useMemo,
|
|
9
|
-
useReducer,
|
|
10
|
-
useRef,
|
|
11
|
-
useState,
|
|
12
|
-
} from "react";
|
|
13
|
-
import type { ProfileDoc } from "../../../domain/profile.ts";
|
|
14
|
-
import type { Effects } from "../effects.ts";
|
|
15
|
-
import { type Action, type AppState, type Purpose, reduce } from "../state.ts";
|
|
16
|
-
import { setModal } from "./keymap.tsx";
|
|
17
|
-
import { type Toast, useToasts } from "./toast.tsx";
|
|
18
|
-
|
|
19
|
-
export type BackKind = "input" | "view";
|
|
20
|
-
|
|
21
|
-
/** What an answer carries besides its value. */
|
|
22
|
-
interface AnswerExtra {
|
|
23
|
-
/** the save dialog's: the profile file as its last preview read it, the only one the save may write over */
|
|
24
|
-
shown?: ProfileDoc;
|
|
25
|
-
/** the save dialog's: reads the file again and shows the new preview with the changed-on-disk notice */
|
|
26
|
-
changed?: () => void;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
export type DialogHandler = (purpose: Purpose, value: string, extra: AnswerExtra) => void | Promise<void>;
|
|
30
|
-
|
|
31
|
-
export interface AppApi {
|
|
32
|
-
/** the state this render drew */
|
|
33
|
-
state: AppState;
|
|
34
|
-
/** the state now, after every dispatch so far: what a command handler must read */
|
|
35
|
-
getState(): AppState;
|
|
36
|
-
/** runs the reducer now, so the keymap's mode follows the dialog stack before the next key */
|
|
37
|
-
dispatch(a: Action): void;
|
|
38
|
-
effects: Effects;
|
|
39
|
-
clock: Clock;
|
|
40
|
-
toast(t: Toast): void;
|
|
41
|
-
/** lines printed to stdout once the TUI has exited: what the user must keep (spec §9.4); a line held already is dropped */
|
|
42
|
-
keep(lines: string[]): void;
|
|
43
|
-
exit(code: number): void;
|
|
44
|
-
/** copies to the clipboard (OSC 52); false when the terminal cannot */
|
|
45
|
-
copy(text: string): boolean;
|
|
46
|
-
/** answers the top dialog: runs the handler registered for its purpose */
|
|
47
|
-
answer(value: string, extra?: AnswerExtra): void;
|
|
48
|
-
onDialog(type: Purpose["type"], fn: DialogHandler): () => void;
|
|
49
|
-
/**
|
|
50
|
-
* esc runs the newest back handler (clear a filter, leave a run); ctrl+c runs only the newest one that
|
|
51
|
-
* clears a text input.
|
|
52
|
-
*/
|
|
53
|
-
onBack(fn: () => void, kind: BackKind): () => void;
|
|
54
|
-
back(kind?: BackKind): boolean;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
const AppContext = createContext<AppApi | null>(null);
|
|
58
|
-
|
|
59
|
-
export function useApp(): AppApi {
|
|
60
|
-
const app = useContext(AppContext);
|
|
61
|
-
if (!app) throw new Error("useApp must be used inside AppProvider");
|
|
62
|
-
return app;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
export function AppProvider(props: {
|
|
66
|
-
effects: Effects;
|
|
67
|
-
clock: Clock;
|
|
68
|
-
initial: AppState;
|
|
69
|
-
copy: (text: string) => boolean;
|
|
70
|
-
onExit: (code: number, kept: string[]) => void;
|
|
71
|
-
children: ReactNode;
|
|
72
|
-
}) {
|
|
73
|
-
const keymap = useKeymap();
|
|
74
|
-
const toasts = useToasts();
|
|
75
|
-
const [state, setState] = useState(props.initial);
|
|
76
|
-
const stateRef = useRef(props.initial);
|
|
77
|
-
const kept = useRef<string[]>([]);
|
|
78
|
-
const handlers = useRef(new Map<string, DialogHandler[]>());
|
|
79
|
-
const backs = useRef<{ fn: () => void; kind: BackKind }[]>([]);
|
|
80
|
-
const propsRef = useRef(props);
|
|
81
|
-
propsRef.current = props;
|
|
82
|
-
const api = useMemo<Omit<AppApi, "state">>(
|
|
83
|
-
() => ({
|
|
84
|
-
getState: () => stateRef.current,
|
|
85
|
-
dispatch(a) {
|
|
86
|
-
const next = reduce(stateRef.current, a);
|
|
87
|
-
if (next === stateRef.current) return;
|
|
88
|
-
stateRef.current = next;
|
|
89
|
-
setModal(keymap, next.dialogs.length > 0);
|
|
90
|
-
setState(next);
|
|
91
|
-
},
|
|
92
|
-
effects: props.effects,
|
|
93
|
-
clock: props.clock,
|
|
94
|
-
toast: (t) => toasts.show(t),
|
|
95
|
-
keep(lines) {
|
|
96
|
-
// a line already kept (an agent linked by both Save and Make active) prints once
|
|
97
|
-
for (const l of lines) if (!kept.current.includes(l)) kept.current.push(l);
|
|
98
|
-
},
|
|
99
|
-
exit: (code) => propsRef.current.onExit(code, kept.current),
|
|
100
|
-
copy: (text) => propsRef.current.copy(text),
|
|
101
|
-
answer(value, extra = {}) {
|
|
102
|
-
const top = stateRef.current.dialogs.at(-1);
|
|
103
|
-
if (!top) return;
|
|
104
|
-
const fn = handlers.current.get(top.purpose.type)?.at(-1);
|
|
105
|
-
if (fn) void fn(top.purpose, value, extra);
|
|
106
|
-
},
|
|
107
|
-
onDialog(type, fn) {
|
|
108
|
-
const list = handlers.current.get(type) ?? [];
|
|
109
|
-
handlers.current.set(type, [...list, fn]);
|
|
110
|
-
return () =>
|
|
111
|
-
handlers.current.set(
|
|
112
|
-
type,
|
|
113
|
-
(handlers.current.get(type) ?? []).filter((f) => f !== fn),
|
|
114
|
-
);
|
|
115
|
-
},
|
|
116
|
-
onBack(fn, kind) {
|
|
117
|
-
const entry = { fn, kind };
|
|
118
|
-
backs.current.push(entry);
|
|
119
|
-
return () => {
|
|
120
|
-
backs.current = backs.current.filter((b) => b !== entry);
|
|
121
|
-
};
|
|
122
|
-
},
|
|
123
|
-
back(kind) {
|
|
124
|
-
const b = backs.current.findLast((x) => kind === undefined || x.kind === kind);
|
|
125
|
-
if (!b) return false;
|
|
126
|
-
b.fn();
|
|
127
|
-
return true;
|
|
128
|
-
},
|
|
129
|
-
}),
|
|
130
|
-
// the toast api and the keymap are stable for the app's life
|
|
131
|
-
[keymap, props.effects, props.clock, toasts.show],
|
|
132
|
-
);
|
|
133
|
-
const value = useMemo(() => ({ ...api, state }), [api, state]);
|
|
134
|
-
return <AppContext.Provider value={value}>{props.children}</AppContext.Provider>;
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
/** Registers the handler for a dialog purpose while the caller is mounted. */
|
|
138
|
-
export function useDialogHandler(type: Purpose["type"], fn: DialogHandler): void {
|
|
139
|
-
const app = useApp();
|
|
140
|
-
const ref = useRef(fn);
|
|
141
|
-
ref.current = fn;
|
|
142
|
-
useEffect(() => app.onDialog(type, (p, v, x) => ref.current(p, v, x)), [app.onDialog, type]);
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
/** Registers a back handler while `active`: `input` clears a text input, `view` leaves a view. */
|
|
146
|
-
export function useBack(active: boolean, kind: BackKind, fn: () => void): void {
|
|
147
|
-
const app = useApp();
|
|
148
|
-
const ref = useRef(fn);
|
|
149
|
-
ref.current = fn;
|
|
150
|
-
useEffect(() => (active ? app.onBack(() => ref.current(), kind) : undefined), [active, kind, app.onBack]);
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
/** The clock's time, re-read every `everyMs` (for "updated 3s ago"). */
|
|
154
|
-
export function useNow(everyMs: number): number {
|
|
155
|
-
const { clock } = useApp();
|
|
156
|
-
const [, tick] = useReducer((n: number) => n + 1, 0);
|
|
157
|
-
useEffect(() => {
|
|
158
|
-
const h = clock.setInterval(tick, everyMs);
|
|
159
|
-
return () => clock.clearInterval(h);
|
|
160
|
-
}, [clock, everyMs]);
|
|
161
|
-
return clock.now();
|
|
162
|
-
}
|
|
@@ -1,57 +0,0 @@
|
|
|
1
|
-
import { useTerminalDimensions } from "@opentui/react";
|
|
2
|
-
import { Component, type ReactNode, useEffect } from "react";
|
|
3
|
-
import { errorMessage, isCatherdError } from "../../../domain/errors.ts";
|
|
4
|
-
import { wrap } from "../text.ts";
|
|
5
|
-
import { Line } from "../widgets/line.tsx";
|
|
6
|
-
import { useApp } from "./app.tsx";
|
|
7
|
-
import { setModal, useCommandLayer, useKeymap } from "./keymap.tsx";
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* What shows when a view throws while drawing: the error, its fix and a way out. Everything under the
|
|
11
|
-
* boundary is gone, with its keys, so this registers its own quit: q or ctrl+c exits 1 and prints the
|
|
12
|
-
* error after the terminal is given back.
|
|
13
|
-
*/
|
|
14
|
-
function Crashed(props: { error: unknown }) {
|
|
15
|
-
const app = useApp();
|
|
16
|
-
const keymap = useKeymap();
|
|
17
|
-
const dims = useTerminalDimensions();
|
|
18
|
-
const e = props.error;
|
|
19
|
-
const message = errorMessage(e);
|
|
20
|
-
const fix = isCatherdError(e) ? e.fix : undefined;
|
|
21
|
-
// a dialog open when it broke is gone with the rest: its modal mode would keep q from reaching quit
|
|
22
|
-
useEffect(() => setModal(keymap, false), [keymap]);
|
|
23
|
-
const quit = () => {
|
|
24
|
-
app.keep([`catherd: ${message}`, ...(fix ? [`fix: ${fix}`] : [])]);
|
|
25
|
-
app.exit(1);
|
|
26
|
-
};
|
|
27
|
-
useCommandLayer("global", { "app.interrupt": quit });
|
|
28
|
-
useCommandLayer("app", { "app.quit": quit });
|
|
29
|
-
const w = Math.max(10, dims.width - 2);
|
|
30
|
-
const lines: { text: string; tone?: "error" | "muted" }[] = [
|
|
31
|
-
{ text: "" },
|
|
32
|
-
...wrap(`catherd stopped: ${message}`, w).map((text) => ({ text, tone: "error" as const })),
|
|
33
|
-
...(fix ? wrap(`fix: ${fix}`, w).map((text) => ({ text, tone: "muted" as const })) : []),
|
|
34
|
-
{ text: "" },
|
|
35
|
-
{ text: "press q or ctrl+c to quit; unsaved changes are lost", tone: "muted" },
|
|
36
|
-
];
|
|
37
|
-
return (
|
|
38
|
-
<box flexDirection="column" width={dims.width} height={dims.height}>
|
|
39
|
-
{lines.map((l, i) => (
|
|
40
|
-
<Line key={i} width={dims.width} parts={[{ text: ` ${l.text}`, tone: l.tone }]} />
|
|
41
|
-
))}
|
|
42
|
-
</box>
|
|
43
|
-
);
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
/** Catches an error thrown while drawing, so the user keeps a way out of the alternate screen. */
|
|
47
|
-
export class AppErrorBoundary extends Component<{ children: ReactNode }, { error: unknown }> {
|
|
48
|
-
override state: { error: unknown } = { error: null };
|
|
49
|
-
|
|
50
|
-
static getDerivedStateFromError(error: unknown): { error: unknown } {
|
|
51
|
-
return { error: error ?? new Error("unknown error") };
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
override render(): ReactNode {
|
|
55
|
-
return this.state.error !== null ? <Crashed error={this.state.error} /> : this.props.children;
|
|
56
|
-
}
|
|
57
|
-
}
|
|
@@ -1,160 +0,0 @@
|
|
|
1
|
-
import { createContext, type ReactNode, useContext, useEffect, useMemo, useRef, useState } from "react";
|
|
2
|
-
import { errorMessage, isCatherdError } from "../../../domain/errors.ts";
|
|
3
|
-
import type { DoctorReport } from "../../../services/doctor.ts";
|
|
4
|
-
import type { Effects, RunRow, SessionRow } from "../effects.ts";
|
|
5
|
-
import { useApp } from "./app.tsx";
|
|
6
|
-
|
|
7
|
-
export interface Polled<T> {
|
|
8
|
-
value: T | null;
|
|
9
|
-
error: string | null;
|
|
10
|
-
/** what to do about `error`, when the failure says (a CatherdError's fix) */
|
|
11
|
-
fix: string | null;
|
|
12
|
-
/** clock time of the last good read, null before the first */
|
|
13
|
-
at: number | null;
|
|
14
|
-
refresh(): void;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
/**
|
|
18
|
-
* Reads `read` now and every `everyMs` while not paused, off the render path (spec §9.4: runs data is
|
|
19
|
-
* polled off the render path): a read runs in a timer callback, never while React renders. It reads at once
|
|
20
|
-
* when it mounts, when `key` changes, on `refresh`, when it resumes and when it is turned back on; a new
|
|
21
|
-
* cadence or a pause only reschedules. `off`: no read at all (the data is not shown).
|
|
22
|
-
*/
|
|
23
|
-
export function usePoll<T>(
|
|
24
|
-
read: () => T,
|
|
25
|
-
everyMs: number,
|
|
26
|
-
o: { paused?: boolean; key?: string; off?: boolean } = {},
|
|
27
|
-
): Polled<T> {
|
|
28
|
-
const { clock } = useApp();
|
|
29
|
-
const [state, setState] = useState<{
|
|
30
|
-
value: T | null;
|
|
31
|
-
error: string | null;
|
|
32
|
-
fix: string | null;
|
|
33
|
-
at: number | null;
|
|
34
|
-
}>({ value: null, error: null, fix: null, at: null });
|
|
35
|
-
const readRef = useRef(read);
|
|
36
|
-
readRef.current = read;
|
|
37
|
-
const [nonce, setNonce] = useState(0);
|
|
38
|
-
// what the last effect run read for: a read at once is due only when this moves on
|
|
39
|
-
const last = useRef<{ clock: unknown; key: string | undefined; nonce: number; paused: boolean } | null>(
|
|
40
|
-
null,
|
|
41
|
-
);
|
|
42
|
-
const paused = o.paused ?? false;
|
|
43
|
-
useEffect(() => {
|
|
44
|
-
if (o.off) {
|
|
45
|
-
last.current = null;
|
|
46
|
-
return;
|
|
47
|
-
}
|
|
48
|
-
const was = last.current;
|
|
49
|
-
last.current = { clock, key: o.key, nonce, paused };
|
|
50
|
-
const now =
|
|
51
|
-
was === null ||
|
|
52
|
-
was.clock !== clock ||
|
|
53
|
-
was.key !== o.key ||
|
|
54
|
-
was.nonce !== nonce ||
|
|
55
|
-
(was.paused && !paused);
|
|
56
|
-
const run = () => {
|
|
57
|
-
try {
|
|
58
|
-
setState({ value: readRef.current(), error: null, fix: null, at: clock.now() });
|
|
59
|
-
} catch (e) {
|
|
60
|
-
// `at` stays the time of the last good read: old rows must not read as fresh
|
|
61
|
-
setState((s) => ({
|
|
62
|
-
...s,
|
|
63
|
-
error: errorMessage(e),
|
|
64
|
-
fix: isCatherdError(e) && e.fix ? e.fix : null,
|
|
65
|
-
}));
|
|
66
|
-
}
|
|
67
|
-
};
|
|
68
|
-
const first = now ? clock.setTimeout(run, 0) : null;
|
|
69
|
-
const every = paused ? null : clock.setInterval(run, everyMs);
|
|
70
|
-
return () => {
|
|
71
|
-
if (first !== null) clock.clearTimeout(first);
|
|
72
|
-
if (every !== null) clock.clearInterval(every);
|
|
73
|
-
};
|
|
74
|
-
}, [clock, everyMs, paused, o.key, o.off, nonce]);
|
|
75
|
-
return { ...state, refresh: () => setNonce((n) => n + 1) };
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
/** `read` once, off the render path, and again whenever `key` changes or `refresh` is called. */
|
|
79
|
-
export function useLoad<T>(read: () => T, key = ""): Polled<T> {
|
|
80
|
-
return usePoll(read, 0, { paused: true, key });
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
/** How often the run list is read again, unless paused (spec §9.1 Runs: "updated Ns ago"; `p` pauses). */
|
|
84
|
-
export const RUNS_EVERY_MS = 2_000;
|
|
85
|
-
|
|
86
|
-
export interface Data {
|
|
87
|
-
report: DoctorReport | null;
|
|
88
|
-
checking: boolean;
|
|
89
|
-
checkedAt: number | null;
|
|
90
|
-
checkError: string | null;
|
|
91
|
-
recheck(): void;
|
|
92
|
-
runs: Polled<{ rows: RunRow[]; warnings: string[] }>;
|
|
93
|
-
/** spec §4: the Runs tab's sessions, read on the runs' cadence while their list is shown */
|
|
94
|
-
sessions: Polled<{ rows: SessionRow[]; warnings: string[] }>;
|
|
95
|
-
/** the profile names and the active one; read on the runs' cadence and after every profile write */
|
|
96
|
-
profiles: Polled<ReturnType<Effects["profiles"]>>;
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
const DataContext = createContext<Data | null>(null);
|
|
100
|
-
|
|
101
|
-
export function useData(): Data {
|
|
102
|
-
const d = useContext(DataContext);
|
|
103
|
-
if (!d) throw new Error("useData must be used inside DataProvider");
|
|
104
|
-
return d;
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
/** Spec §9.4 providers: the data every tab and the header share, read off the render path. */
|
|
108
|
-
export function DataProvider(props: { children: ReactNode }) {
|
|
109
|
-
const app = useApp();
|
|
110
|
-
const [report, setReport] = useState<DoctorReport | null>(null);
|
|
111
|
-
const [checking, setChecking] = useState(false);
|
|
112
|
-
const [checkedAt, setCheckedAt] = useState<number | null>(null);
|
|
113
|
-
const [checkError, setCheckError] = useState<string | null>(null);
|
|
114
|
-
const [nonce, setNonce] = useState(0);
|
|
115
|
-
useEffect(() => {
|
|
116
|
-
let live = true;
|
|
117
|
-
setChecking(true);
|
|
118
|
-
const h = app.clock.setTimeout(() => {
|
|
119
|
-
app.effects
|
|
120
|
-
.doctor()
|
|
121
|
-
.then((r) => {
|
|
122
|
-
if (!live) return;
|
|
123
|
-
setReport(r);
|
|
124
|
-
setCheckError(null);
|
|
125
|
-
})
|
|
126
|
-
.catch((e: unknown) => live && setCheckError(errorMessage(e)))
|
|
127
|
-
.finally(() => {
|
|
128
|
-
if (!live) return;
|
|
129
|
-
setChecking(false);
|
|
130
|
-
setCheckedAt(app.clock.now());
|
|
131
|
-
});
|
|
132
|
-
}, 0);
|
|
133
|
-
return () => {
|
|
134
|
-
live = false;
|
|
135
|
-
app.clock.clearTimeout(h);
|
|
136
|
-
};
|
|
137
|
-
}, [app.effects, app.clock, nonce]);
|
|
138
|
-
const runs = usePoll(() => app.effects.runs(), RUNS_EVERY_MS, { paused: app.state.paused });
|
|
139
|
-
// spec §9.4: reading the sessions scans every run, so only while their list is on screen
|
|
140
|
-
const sessions = usePoll(() => app.effects.sessions(), RUNS_EVERY_MS, {
|
|
141
|
-
paused: app.state.paused,
|
|
142
|
-
off: app.state.tab !== "runs" || app.state.session !== null,
|
|
143
|
-
});
|
|
144
|
-
// cheap (a directory listing and two small reads), and another terminal may change them at any time
|
|
145
|
-
const profiles = usePoll(() => app.effects.profiles(), RUNS_EVERY_MS);
|
|
146
|
-
const value = useMemo(
|
|
147
|
-
() => ({
|
|
148
|
-
report,
|
|
149
|
-
checking,
|
|
150
|
-
checkedAt,
|
|
151
|
-
checkError,
|
|
152
|
-
recheck: () => setNonce((n) => n + 1),
|
|
153
|
-
runs,
|
|
154
|
-
sessions,
|
|
155
|
-
profiles,
|
|
156
|
-
}),
|
|
157
|
-
[report, checking, checkedAt, checkError, runs, sessions, profiles],
|
|
158
|
-
);
|
|
159
|
-
return <DataContext.Provider value={value}>{props.children}</DataContext.Provider>;
|
|
160
|
-
}
|
|
@@ -1,166 +0,0 @@
|
|
|
1
|
-
// Adapted from anomalyco/opencode packages/tui/src/context/keymap.tsx (MIT, © 2025 opencode);
|
|
2
|
-
// see THIRD_PARTY_NOTICES.md.
|
|
3
|
-
import type { CliRenderer, KeyEvent, Renderable } from "@opentui/core";
|
|
4
|
-
import type { Keymap } from "@opentui/keymap";
|
|
5
|
-
import {
|
|
6
|
-
registerBackspacePopsPendingSequence,
|
|
7
|
-
registerCommaBindings,
|
|
8
|
-
registerEscapeClearsPendingSequence,
|
|
9
|
-
registerTimedLeader,
|
|
10
|
-
} from "@opentui/keymap/addons/opentui";
|
|
11
|
-
import { createDefaultOpenTuiKeymap } from "@opentui/keymap/opentui";
|
|
12
|
-
import { KeymapProvider, useBindings, useKeymap } from "@opentui/keymap/react";
|
|
13
|
-
import { flushSync, useRenderer } from "@opentui/react";
|
|
14
|
-
import { createContext, type ReactNode, useContext, useEffect, useMemo, useReducer, useRef } from "react";
|
|
15
|
-
import {
|
|
16
|
-
type CommandDef,
|
|
17
|
-
type CommandId,
|
|
18
|
-
commandsIn,
|
|
19
|
-
DEFAULT_KEYS,
|
|
20
|
-
isGuarded,
|
|
21
|
-
isPrintable,
|
|
22
|
-
type Keybinds,
|
|
23
|
-
LEADER,
|
|
24
|
-
LEADER_TIMEOUT_MS,
|
|
25
|
-
type Scope,
|
|
26
|
-
} from "../commands.ts";
|
|
27
|
-
|
|
28
|
-
export type AppKeymap = Keymap<Renderable, KeyEvent>;
|
|
29
|
-
export type Handlers = Partial<Record<CommandId, () => void>>;
|
|
30
|
-
|
|
31
|
-
/** The keymap data key that holds the mode: `base`, or `modal` while a dialog is open (spec §9.2). */
|
|
32
|
-
export const MODE = "catherd.mode";
|
|
33
|
-
|
|
34
|
-
/** opencode's keymap setup: comma lists, esc and backspace for pending sequences, the timed leader, modes. */
|
|
35
|
-
export function createAppKeymap(renderer: CliRenderer): AppKeymap {
|
|
36
|
-
const k = createDefaultOpenTuiKeymap(renderer);
|
|
37
|
-
registerCommaBindings(k);
|
|
38
|
-
registerEscapeClearsPendingSequence(k);
|
|
39
|
-
registerBackspacePopsPendingSequence(k);
|
|
40
|
-
registerTimedLeader(k, { trigger: LEADER, name: "leader", timeoutMs: LEADER_TIMEOUT_MS });
|
|
41
|
-
k.setData(MODE, "base");
|
|
42
|
-
// a layer names its mode; "global" layers are live in every mode
|
|
43
|
-
k.registerLayerFields({
|
|
44
|
-
mode(value, ctx) {
|
|
45
|
-
if (value !== "global") ctx.require(MODE, value);
|
|
46
|
-
},
|
|
47
|
-
});
|
|
48
|
-
const attr = (name: string) => (value: unknown, ctx: { attr(n: string, v: unknown): void }) =>
|
|
49
|
-
ctx.attr(name, value);
|
|
50
|
-
k.registerCommandFields({
|
|
51
|
-
palette: attr("palette"),
|
|
52
|
-
suggested: attr("suggested"),
|
|
53
|
-
hint: attr("hint"),
|
|
54
|
-
short: attr("short"),
|
|
55
|
-
scope: attr("scope"),
|
|
56
|
-
cli: attr("cli"),
|
|
57
|
-
});
|
|
58
|
-
return k;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
/** Puts the keymap in modal mode while a dialog is open; synchronous, so a key right after sees it. */
|
|
62
|
-
export const setModal = (k: AppKeymap, modal: boolean): void => k.setData(MODE, modal ? "modal" : "base");
|
|
63
|
-
|
|
64
|
-
const KeybindsContext = createContext<Keybinds>(DEFAULT_KEYS);
|
|
65
|
-
export const useKeybinds = (): Keybinds => useContext(KeybindsContext);
|
|
66
|
-
|
|
67
|
-
/** The keymap for the renderer, and the keys in force. */
|
|
68
|
-
export function AppKeymapProvider(props: { keybinds: Keybinds; keymap?: AppKeymap; children: ReactNode }) {
|
|
69
|
-
const renderer = useRenderer();
|
|
70
|
-
const keymap = useMemo(() => props.keymap ?? createAppKeymap(renderer), [props.keymap, renderer]);
|
|
71
|
-
return (
|
|
72
|
-
<KeymapProvider keymap={keymap}>
|
|
73
|
-
<KeybindsContext.Provider value={props.keybinds}>{props.children}</KeybindsContext.Provider>
|
|
74
|
-
</KeymapProvider>
|
|
75
|
-
);
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
const modeOf = (scope: Scope): "global" | "modal" | "base" =>
|
|
79
|
-
scope === "global" ? "global" : scope === "dialog" ? "modal" : "base";
|
|
80
|
-
|
|
81
|
-
/**
|
|
82
|
-
* Registers the commands of `scope` that have a handler, bound to their keys, while the calling component
|
|
83
|
-
* is mounted. Printable keys of guarded scopes, and every key of a `row.*` scope, go quiet while a text
|
|
84
|
-
* input has focus, so the input gets them as text; a `filter` layer is live only then.
|
|
85
|
-
*/
|
|
86
|
-
export function useCommandLayer(scope: Scope, handlers: Handlers): void {
|
|
87
|
-
const renderer = useRenderer();
|
|
88
|
-
const keys = useKeybinds();
|
|
89
|
-
const ref = useRef(handlers);
|
|
90
|
-
ref.current = handlers;
|
|
91
|
-
const ids = commandsIn(scope)
|
|
92
|
-
.filter((c) => handlers[c.id as CommandId])
|
|
93
|
-
.map((c) => c.id as CommandId);
|
|
94
|
-
const idsKey = ids.join(" ");
|
|
95
|
-
const mode = modeOf(scope);
|
|
96
|
-
const typing = () => renderer.currentFocusedEditor !== null;
|
|
97
|
-
const layerEnabled = scope === "filter" ? typing : scope.startsWith("row.") ? () => !typing() : () => true;
|
|
98
|
-
const command = (c: CommandDef) => ({
|
|
99
|
-
name: c.id,
|
|
100
|
-
title: c.title,
|
|
101
|
-
group: c.group,
|
|
102
|
-
scope: c.scope,
|
|
103
|
-
...(c.palette ? { palette: true } : {}),
|
|
104
|
-
...(c.suggested ? { suggested: true } : {}),
|
|
105
|
-
...(c.hint !== undefined ? { hint: c.hint } : {}),
|
|
106
|
-
...(c.short ? { short: c.short } : {}),
|
|
107
|
-
...(c.cli ? { cli: c.cli } : {}),
|
|
108
|
-
// a key's update is drawn before the next key of the same chunk is read: a dialog or a filter it
|
|
109
|
-
// opens has its input mounted and focused by then, so no letter typed right after it is lost
|
|
110
|
-
run: () => {
|
|
111
|
-
flushSync(() => ref.current[c.id as CommandId]?.());
|
|
112
|
-
},
|
|
113
|
-
});
|
|
114
|
-
const defs = commandsIn(scope).filter((c) => ids.includes(c.id as CommandId));
|
|
115
|
-
const guarded = isGuarded(scope);
|
|
116
|
-
const bindingsFor = (printable: boolean) =>
|
|
117
|
-
defs.flatMap((c) =>
|
|
118
|
-
keys[c.id as CommandId]
|
|
119
|
-
.filter((k) => (guarded ? isPrintable(k) === printable : !printable))
|
|
120
|
-
.map((k) => ({ key: k, cmd: c.id })),
|
|
121
|
-
);
|
|
122
|
-
useBindings(
|
|
123
|
-
() => ({ mode, enabled: layerEnabled, commands: defs.map(command), bindings: bindingsFor(false) }),
|
|
124
|
-
[scope, idsKey, keys],
|
|
125
|
-
);
|
|
126
|
-
// printable keys of a guarded scope: their own layer, quiet while a text input has focus
|
|
127
|
-
useBindings(
|
|
128
|
-
() => ({ mode, enabled: () => !typing(), bindings: guarded ? bindingsFor(true) : [] }),
|
|
129
|
-
[scope, idsKey, keys],
|
|
130
|
-
);
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
export interface CommandEntry {
|
|
134
|
-
id: CommandId;
|
|
135
|
-
title: string;
|
|
136
|
-
group: string;
|
|
137
|
-
palette: boolean;
|
|
138
|
-
suggested: boolean;
|
|
139
|
-
hint: number | null;
|
|
140
|
-
short: string | null;
|
|
141
|
-
cli: string | null;
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
/** The commands a key could run right now, or that `dispatch` could; what the palette and help list. */
|
|
145
|
-
export function reachableCommands(k: AppKeymap): CommandEntry[] {
|
|
146
|
-
return k.getCommandEntries({ visibility: "reachable" }).map(({ command: c }) => ({
|
|
147
|
-
id: c.name as CommandId,
|
|
148
|
-
title: String(c.title ?? c.name),
|
|
149
|
-
group: String(c.group ?? ""),
|
|
150
|
-
palette: c.palette === true,
|
|
151
|
-
suggested: c.suggested === true,
|
|
152
|
-
hint: typeof c.hint === "number" ? c.hint : null,
|
|
153
|
-
short: typeof c.short === "string" ? c.short : null,
|
|
154
|
-
cli: typeof c.cli === "string" ? c.cli : null,
|
|
155
|
-
}));
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
/** Re-renders the caller whenever the keymap's live bindings change (a layer, a mode, focus). */
|
|
159
|
-
export function useKeymapVersion(): number {
|
|
160
|
-
const k = useKeymap();
|
|
161
|
-
const [v, bump] = useReducer((n: number) => n + 1, 0);
|
|
162
|
-
useEffect(() => k.on("state", () => bump()), [k]);
|
|
163
|
-
return v;
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
export { useKeymap };
|
|
@@ -1,45 +0,0 @@
|
|
|
1
|
-
import type { Clock } from "@opentui/core";
|
|
2
|
-
import type { ReactNode } from "react";
|
|
3
|
-
import type { Keybinds } from "../commands.ts";
|
|
4
|
-
import type { Effects } from "../effects.ts";
|
|
5
|
-
import type { AppState } from "../state.ts";
|
|
6
|
-
import type { Ui } from "../theme.ts";
|
|
7
|
-
import { AppProvider } from "./app.tsx";
|
|
8
|
-
import { AppErrorBoundary } from "./boundary.tsx";
|
|
9
|
-
import { type AppKeymap, AppKeymapProvider } from "./keymap.tsx";
|
|
10
|
-
import { ThemeProvider } from "./theme.tsx";
|
|
11
|
-
import { ToastProvider } from "./toast.tsx";
|
|
12
|
-
|
|
13
|
-
/**
|
|
14
|
-
* Every provider, in dependency order: theme → keymap → toasts → app state (dialogs, handlers), and
|
|
15
|
-
* under them the boundary that keeps a way out when a view throws.
|
|
16
|
-
*/
|
|
17
|
-
export function Providers(props: {
|
|
18
|
-
ui: Ui;
|
|
19
|
-
keybinds: Keybinds;
|
|
20
|
-
keymap?: AppKeymap;
|
|
21
|
-
effects: Effects;
|
|
22
|
-
clock: Clock;
|
|
23
|
-
initial: AppState;
|
|
24
|
-
copy: (text: string) => boolean;
|
|
25
|
-
onExit: (code: number, kept: string[]) => void;
|
|
26
|
-
children: ReactNode;
|
|
27
|
-
}) {
|
|
28
|
-
return (
|
|
29
|
-
<ThemeProvider ui={props.ui}>
|
|
30
|
-
<AppKeymapProvider keybinds={props.keybinds} keymap={props.keymap}>
|
|
31
|
-
<ToastProvider clock={props.clock}>
|
|
32
|
-
<AppProvider
|
|
33
|
-
effects={props.effects}
|
|
34
|
-
clock={props.clock}
|
|
35
|
-
initial={props.initial}
|
|
36
|
-
copy={props.copy}
|
|
37
|
-
onExit={props.onExit}
|
|
38
|
-
>
|
|
39
|
-
<AppErrorBoundary>{props.children}</AppErrorBoundary>
|
|
40
|
-
</AppProvider>
|
|
41
|
-
</ToastProvider>
|
|
42
|
-
</AppKeymapProvider>
|
|
43
|
-
</ThemeProvider>
|
|
44
|
-
);
|
|
45
|
-
}
|
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
import { createContext, type ReactNode, useContext } from "react";
|
|
2
|
-
import { paint, type Token, type Ui } from "../theme.ts";
|
|
3
|
-
|
|
4
|
-
const ThemeContext = createContext<Ui>({ plain: false, color: false, reducedMotion: true, mode: "dark" });
|
|
5
|
-
|
|
6
|
-
/** Spec §9.4 providers: the theme, one per concern. */
|
|
7
|
-
export function ThemeProvider(props: { ui: Ui; children: ReactNode }) {
|
|
8
|
-
return <ThemeContext.Provider value={props.ui}>{props.children}</ThemeContext.Provider>;
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
export const useUi = (): Ui => useContext(ThemeContext);
|
|
12
|
-
|
|
13
|
-
/** A token's colour for this terminal: undefined under NO_COLOR and --plain. */
|
|
14
|
-
export function useTone(): (t: Token) => string | undefined {
|
|
15
|
-
const ui = useUi();
|
|
16
|
-
return (t) => paint(ui, t);
|
|
17
|
-
}
|
|
@@ -1,77 +0,0 @@
|
|
|
1
|
-
// Adapted from anomalyco/opencode packages/tui/src/ui/toast.tsx (MIT, © 2025 opencode);
|
|
2
|
-
// see THIRD_PARTY_NOTICES.md.
|
|
3
|
-
import type { Clock } from "@opentui/core";
|
|
4
|
-
import { useRenderer } from "@opentui/react";
|
|
5
|
-
import {
|
|
6
|
-
createContext,
|
|
7
|
-
type ReactNode,
|
|
8
|
-
useCallback,
|
|
9
|
-
useContext,
|
|
10
|
-
useEffect,
|
|
11
|
-
useMemo,
|
|
12
|
-
useRef,
|
|
13
|
-
useState,
|
|
14
|
-
} from "react";
|
|
15
|
-
import { errorMessage, isCatherdError } from "../../../domain/errors.ts";
|
|
16
|
-
|
|
17
|
-
type ToastVariant = "info" | "success" | "warning" | "error";
|
|
18
|
-
export interface Toast {
|
|
19
|
-
variant: ToastVariant;
|
|
20
|
-
message: string;
|
|
21
|
-
/** the command that fixes it, shown in full on its own lines; the toast stays until the next key */
|
|
22
|
-
fix?: string;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
/** How long a toast stays before the next one in the queue shows (one with a fix waits for a key). */
|
|
26
|
-
export const TOAST_MS = 4_000;
|
|
27
|
-
|
|
28
|
-
/** An error as a toast: its message, and its fix command when it has one. */
|
|
29
|
-
export const errorToast = (e: unknown): Toast => ({
|
|
30
|
-
variant: "error",
|
|
31
|
-
message: errorMessage(e),
|
|
32
|
-
...(isCatherdError(e) && e.fix ? { fix: e.fix } : {}),
|
|
33
|
-
});
|
|
34
|
-
|
|
35
|
-
interface ToastApi {
|
|
36
|
-
current: Toast | null;
|
|
37
|
-
queued: number;
|
|
38
|
-
show(t: Toast): void;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
const ToastContext = createContext<ToastApi>({ current: null, queued: 0, show: () => {} });
|
|
42
|
-
|
|
43
|
-
/**
|
|
44
|
-
* Spec §9.3: toasts queue at the top right; one shows at a time, for TOAST_MS each. A toast with a fix
|
|
45
|
-
* command stays until the next key, so the command the user must act on is not gone before it is read.
|
|
46
|
-
*/
|
|
47
|
-
export function ToastProvider(props: { clock: Clock; children: ReactNode }) {
|
|
48
|
-
const renderer = useRenderer();
|
|
49
|
-
const [queue, setQueue] = useState<Toast[]>([]);
|
|
50
|
-
const queueRef = useRef(queue);
|
|
51
|
-
queueRef.current = queue;
|
|
52
|
-
const head = queue[0] ?? null;
|
|
53
|
-
useEffect(() => {
|
|
54
|
-
if (!head || head.fix) return;
|
|
55
|
-
const h = props.clock.setTimeout(() => setQueue((q) => q.slice(1)), TOAST_MS);
|
|
56
|
-
return () => props.clock.clearTimeout(h);
|
|
57
|
-
}, [head, props.clock]);
|
|
58
|
-
useEffect(() => {
|
|
59
|
-
// first in line, before the keymap: a toast the same key shows is not the one it dismisses
|
|
60
|
-
const onKey = () => {
|
|
61
|
-
const shown = queueRef.current[0];
|
|
62
|
-
if (shown?.fix) setQueue((q) => (q[0] === shown ? q.slice(1) : q));
|
|
63
|
-
};
|
|
64
|
-
renderer.keyInput.prependListener("keypress", onKey);
|
|
65
|
-
return () => {
|
|
66
|
-
renderer.keyInput.off("keypress", onKey);
|
|
67
|
-
};
|
|
68
|
-
}, [renderer]);
|
|
69
|
-
const show = useCallback((t: Toast) => setQueue((q) => [...q, t]), []);
|
|
70
|
-
const api = useMemo(
|
|
71
|
-
() => ({ current: head, queued: Math.max(0, queue.length - 1), show }),
|
|
72
|
-
[head, queue.length, show],
|
|
73
|
-
);
|
|
74
|
-
return <ToastContext.Provider value={api}>{props.children}</ToastContext.Provider>;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
export const useToasts = (): ToastApi => useContext(ToastContext);
|