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,146 +0,0 @@
|
|
|
1
|
-
import type { Suggestion } from "../../services/standins.ts";
|
|
2
|
-
import { type Catalog, rungInfo, scoresOf } from "../../domain/catalog.ts";
|
|
3
|
-
import { parseRung, tryParseRung } from "../../domain/ids.ts";
|
|
4
|
-
import { NOTIFY, type Profile, type ProfilePatch } from "../../domain/profile.ts";
|
|
5
|
-
import { inferredScores, quotaOf } from "../../domain/profile-rules.ts";
|
|
6
|
-
import { ACCESS } from "../../domain/record.ts";
|
|
7
|
-
import type { Role } from "../../domain/roles.ts";
|
|
8
|
-
import type { CatalogModel } from "../../services/catalog-service.ts";
|
|
9
|
-
import type { RowAction } from "./profile-tree.ts";
|
|
10
|
-
import type { NumberPath, SelectOption } from "./state.ts";
|
|
11
|
-
import { shortRung } from "./text.ts";
|
|
12
|
-
|
|
13
|
-
// What the Profiles tab's rows change: the patch a toggle or cycle makes, number fields, and the options
|
|
14
|
-
// each picker dialog offers. profile-tree.ts builds the rows themselves.
|
|
15
|
-
|
|
16
|
-
const next = <T>(list: readonly T[], cur: T): T => list[(list.indexOf(cur) + 1) % list.length] as T;
|
|
17
|
-
|
|
18
|
-
/**
|
|
19
|
-
* The patch a toggle (space) or a cycle (enter) makes on the draft; null for rows that open a dialog
|
|
20
|
-
* or expand instead. Space on a model row never changes a rung (research K5).
|
|
21
|
-
*/
|
|
22
|
-
export function patchFor(p: Profile, a: RowAction): ProfilePatch | null {
|
|
23
|
-
switch (a.type) {
|
|
24
|
-
case "role":
|
|
25
|
-
return { roles: { [a.role]: { enabled: !p.roles[a.role].enabled } } };
|
|
26
|
-
case "access":
|
|
27
|
-
return { roles: { [a.role]: { access: next(ACCESS, p.roles[a.role].access) } } };
|
|
28
|
-
case "rung": {
|
|
29
|
-
const rc = p.roles[a.role];
|
|
30
|
-
if (!rc.rungs.includes(a.rung)) return { roles: { [a.role]: { rungs: [...rc.rungs, a.rung] } } };
|
|
31
|
-
return {
|
|
32
|
-
roles: {
|
|
33
|
-
[a.role]: {
|
|
34
|
-
rungs: rc.rungs.filter((r) => r !== a.rung),
|
|
35
|
-
...(rc.defaultRung === a.rung ? { defaultRung: null } : {}),
|
|
36
|
-
},
|
|
37
|
-
},
|
|
38
|
-
};
|
|
39
|
-
}
|
|
40
|
-
case "objective":
|
|
41
|
-
return { objective: p.objective === "cost" ? "speed" : "cost" };
|
|
42
|
-
case "jev":
|
|
43
|
-
return { jev: { use: p.jev.use === "auto" ? "off" : "auto" } };
|
|
44
|
-
case "isolated":
|
|
45
|
-
return { harness: { [a.harness]: { isolated: !(p.harness[a.harness]?.isolated ?? false) } } };
|
|
46
|
-
case "notify":
|
|
47
|
-
return {
|
|
48
|
-
notify: p.notify.includes(a.moment)
|
|
49
|
-
? p.notify.filter((m) => m !== a.moment)
|
|
50
|
-
: NOTIFY.filter((m) => m === a.moment || p.notify.includes(m)),
|
|
51
|
-
};
|
|
52
|
-
default:
|
|
53
|
-
return null;
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
/** A number field set, or cleared with null (a budget cap); timeouts cannot be cleared. */
|
|
58
|
-
export function numberPatch(path: NumberPath, value: number | null): ProfilePatch {
|
|
59
|
-
const [head, leaf] = path.split(".") as ["budget" | "timeouts", string];
|
|
60
|
-
return { [head]: { [leaf]: value } } as ProfilePatch;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
/** Parses a value editor's text: a positive number, or empty for no cap where the field allows one. */
|
|
64
|
-
export function parseNumber(path: NumberPath, text: string): { value: number | null } | { error: string } {
|
|
65
|
-
const t = text.trim();
|
|
66
|
-
if (t === "")
|
|
67
|
-
return path.startsWith("budget.") ? { value: null } : { error: "enter a number of minutes above 0" };
|
|
68
|
-
const n = Number(t);
|
|
69
|
-
if (!Number.isFinite(n) || n <= 0) return { error: `"${t}" is not a number above 0` };
|
|
70
|
-
if (path === "budget.tokens" && !Number.isInteger(n)) return { error: "tokens are a whole number" };
|
|
71
|
-
return { value: n };
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
export function numberValue(p: Profile, path: NumberPath): number | undefined {
|
|
75
|
-
const [head, leaf] = path.split(".") as ["budget" | "timeouts", string];
|
|
76
|
-
return (p[head] as Record<string, number | undefined>)[leaf];
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
/** The default-rung picker: the role's rungs, and none. */
|
|
80
|
-
export function startOptions(p: Profile, role: Role): SelectOption[] {
|
|
81
|
-
const rc = p.roles[role];
|
|
82
|
-
return [
|
|
83
|
-
{
|
|
84
|
-
value: "",
|
|
85
|
-
title: "none: the cheapest rung that clears the bar",
|
|
86
|
-
current: rc.defaultRung === undefined,
|
|
87
|
-
},
|
|
88
|
-
...rc.rungs.map((r) => ({ value: r, title: shortRung(r), detail: r, current: r === rc.defaultRung })),
|
|
89
|
-
];
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
/** The failover picker: every usable rung on another quota (Ruling 3 of plan 5), and none. */
|
|
93
|
-
export function failoverOptions(
|
|
94
|
-
p: Profile,
|
|
95
|
-
models: CatalogModel[],
|
|
96
|
-
c: Catalog,
|
|
97
|
-
rung: string,
|
|
98
|
-
): SelectOption[] {
|
|
99
|
-
const parsed = tryParseRung(rung);
|
|
100
|
-
if (!parsed) return [];
|
|
101
|
-
const quota = quotaOf(parsed);
|
|
102
|
-
const cur = p.failover[rung];
|
|
103
|
-
const out: SelectOption[] = [{ value: "", title: "none", current: cur === undefined }];
|
|
104
|
-
// usable: scored in catalog_query, or through a treat-like staged in `c` (`withStaged`)
|
|
105
|
-
const usable = (x: string) => {
|
|
106
|
-
try {
|
|
107
|
-
return scoresOf(c, rungInfo(c, x).canonical) !== null;
|
|
108
|
-
} catch {
|
|
109
|
-
return false;
|
|
110
|
-
}
|
|
111
|
-
};
|
|
112
|
-
for (const m of models)
|
|
113
|
-
for (const r of m.rungs) {
|
|
114
|
-
if (!(r.enabled || usable(r.rung)) || r.rung.startsWith("claude:")) continue;
|
|
115
|
-
const q = quotaOf(parseRung(r.rung));
|
|
116
|
-
if (q === quota) continue;
|
|
117
|
-
const note = inferredScores(c, rungInfo(c, r.rung)).note;
|
|
118
|
-
out.push({
|
|
119
|
-
value: r.rung,
|
|
120
|
-
title: shortRung(r.rung),
|
|
121
|
-
group: m.billing,
|
|
122
|
-
detail: note ?? "",
|
|
123
|
-
current: r.rung === cur,
|
|
124
|
-
});
|
|
125
|
-
}
|
|
126
|
-
return out;
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
/**
|
|
130
|
-
* The treat-like picker: every rung with scores of its own, by model; spec 1.2 §6.4: the suggested stand-ins
|
|
131
|
-
* (`suggestions`, nearest first) say their distance and what they would lend.
|
|
132
|
-
*/
|
|
133
|
-
export function treatLikeOptions(c: Catalog, suggestions: Suggestion[] = []): SelectOption[] {
|
|
134
|
-
return Object.keys(c.scores)
|
|
135
|
-
.filter((canonical) => scoresOf(c, canonical)?.via === null)
|
|
136
|
-
.sort()
|
|
137
|
-
.map((canonical) => {
|
|
138
|
-
const s = suggestions.find((x) => x.like === canonical);
|
|
139
|
-
return {
|
|
140
|
-
value: canonical,
|
|
141
|
-
title: canonical,
|
|
142
|
-
group: canonical.slice(0, canonical.lastIndexOf("#")),
|
|
143
|
-
...(s ? { detail: `distance ${s.distance.toFixed(2)} · lends ${s.lends.join(", ")}` } : {}),
|
|
144
|
-
};
|
|
145
|
-
});
|
|
146
|
-
}
|
|
@@ -1,429 +0,0 @@
|
|
|
1
|
-
import { billingKeyOf, type Catalog, rungInfo } from "../../domain/catalog.ts";
|
|
2
|
-
import { tryParseRung } from "../../domain/ids.ts";
|
|
3
|
-
import { NOTIFY, type NotifyMoment, type Profile } from "../../domain/profile.ts";
|
|
4
|
-
import { type Issue, inferredScores, routingProfileOf, type Validation } from "../../domain/profile-rules.ts";
|
|
5
|
-
import type { Access } from "../../domain/record.ts";
|
|
6
|
-
import { ROLES, type Role } from "../../domain/roles.ts";
|
|
7
|
-
import { candidates } from "../../domain/select.ts";
|
|
8
|
-
import type { CatalogModel } from "../../services/catalog-service.ts";
|
|
9
|
-
import type { NumberPath } from "./state.ts";
|
|
10
|
-
import { shortRung } from "./text.ts";
|
|
11
|
-
|
|
12
|
-
export type RowAction =
|
|
13
|
-
| { type: "section" }
|
|
14
|
-
| { type: "role"; role: Role }
|
|
15
|
-
| { type: "access"; role: Role }
|
|
16
|
-
| { type: "start"; role: Role }
|
|
17
|
-
| { type: "group" }
|
|
18
|
-
| { type: "model" }
|
|
19
|
-
| { type: "rung"; role: Role; rung: string; scored: boolean }
|
|
20
|
-
| { type: "objective" }
|
|
21
|
-
| { type: "jev" }
|
|
22
|
-
| { type: "isolated"; harness: string }
|
|
23
|
-
| { type: "number"; path: NumberPath }
|
|
24
|
-
| { type: "failover"; rung: string }
|
|
25
|
-
| { type: "notify"; moment: NotifyMoment };
|
|
26
|
-
|
|
27
|
-
/** One line of the Profiles tree (spec §9.1: roles → access + backend → model → efforts, then settings). */
|
|
28
|
-
export interface Row {
|
|
29
|
-
key: string;
|
|
30
|
-
parent: string | null;
|
|
31
|
-
depth: number;
|
|
32
|
-
label: string;
|
|
33
|
-
value: string;
|
|
34
|
-
/** a checkbox's state; null for a row that is not one */
|
|
35
|
-
check: boolean | null;
|
|
36
|
-
expandable: boolean;
|
|
37
|
-
expanded: boolean;
|
|
38
|
-
/** headings are skipped by the cursor */
|
|
39
|
-
selectable: boolean;
|
|
40
|
-
/** drawn muted: an unusable rung, the children of a role that is off */
|
|
41
|
-
dim: boolean;
|
|
42
|
-
issue: (Issue & { level: "error" | "warning" }) | null;
|
|
43
|
-
action: RowAction;
|
|
44
|
-
/** what the filter matches: the labels of the row's parents, its own label and its value */
|
|
45
|
-
search: string;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
export interface TreeInput {
|
|
49
|
-
/** the draft, resolved */
|
|
50
|
-
profile: Profile;
|
|
51
|
-
models: CatalogModel[];
|
|
52
|
-
/** the catalog with the draft's staged treat-likes merged in (`withStaged`) */
|
|
53
|
-
catalog: Catalog;
|
|
54
|
-
staged: Record<string, string>;
|
|
55
|
-
expanded: ReadonlySet<string>;
|
|
56
|
-
/** the harnesses catherd can isolate: the registered adapters */
|
|
57
|
-
harnesses: readonly string[];
|
|
58
|
-
enforcement: (rung: string, access: Access) => "enforced" | "advisory";
|
|
59
|
-
validation: Validation;
|
|
60
|
-
/** every expandable row open, for filtering */
|
|
61
|
-
expandAll?: boolean;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
/** The catalog as it will be once the staged treat-likes are saved. */
|
|
65
|
-
export function withStaged(c: Catalog, staged: Record<string, string>): Catalog {
|
|
66
|
-
if (Object.keys(staged).length === 0) return c;
|
|
67
|
-
const treatLike = { ...c.treatLike };
|
|
68
|
-
for (const [rung, like] of Object.entries(staged)) {
|
|
69
|
-
try {
|
|
70
|
-
treatLike[rungInfo(c, rung).canonical] = { like, source: "user" };
|
|
71
|
-
} catch {
|
|
72
|
-
// a staged rung that does not parse is never offered
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
return { ...c, treatLike };
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
const GROUP_ORDER = [
|
|
79
|
-
"claude",
|
|
80
|
-
"claude-code",
|
|
81
|
-
"codex",
|
|
82
|
-
"opencode-go",
|
|
83
|
-
"opencode",
|
|
84
|
-
"cursor",
|
|
85
|
-
"grok",
|
|
86
|
-
"antigravity",
|
|
87
|
-
];
|
|
88
|
-
const GROUP_NOTE: Record<string, string> = { claude: "native subagent", "claude-code": "headless" };
|
|
89
|
-
|
|
90
|
-
interface ModelEntry {
|
|
91
|
-
backend: string;
|
|
92
|
-
model: string;
|
|
93
|
-
key: string;
|
|
94
|
-
efforts: string[];
|
|
95
|
-
listed: boolean | null;
|
|
96
|
-
/** efforts catalog_query scores (their own or through a saved treat-like) */
|
|
97
|
-
scored: Set<string>;
|
|
98
|
-
inferred: Set<string>;
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
function issueFor(v: Validation, match: (path: string) => boolean): Row["issue"] {
|
|
102
|
-
const e = v.errors.find((i) => match(i.path));
|
|
103
|
-
if (e) return { ...e, level: "error" };
|
|
104
|
-
const w = v.warnings.find((i) => match(i.path));
|
|
105
|
-
return w ? { ...w, level: "warning" } : null;
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
/** A role's models: those catalog_query says can fill it, plus any rung the profile holds that it lacks. */
|
|
109
|
-
function modelsFor(models: CatalogModel[], role: Role, rungs: string[]): ModelEntry[] {
|
|
110
|
-
const out: ModelEntry[] = [];
|
|
111
|
-
const find = (backend: string, model: string) =>
|
|
112
|
-
out.find((m) => m.backend === backend && m.model === model);
|
|
113
|
-
for (const m of models) {
|
|
114
|
-
if (!m.roles.includes(role)) continue;
|
|
115
|
-
const scored = new Set<string>();
|
|
116
|
-
const inferred = new Set<string>();
|
|
117
|
-
for (const r of m.rungs) {
|
|
118
|
-
const effort = r.rung.slice(r.rung.lastIndexOf("#") + 1);
|
|
119
|
-
if (r.enabled) scored.add(effort);
|
|
120
|
-
if (r.treatLike || Object.values(r.scores).some((s) => s.confidence === "inferred"))
|
|
121
|
-
inferred.add(effort);
|
|
122
|
-
}
|
|
123
|
-
out.push({
|
|
124
|
-
backend: m.backend,
|
|
125
|
-
model: m.model,
|
|
126
|
-
key: m.billing,
|
|
127
|
-
efforts: m.efforts.length ? [...m.efforts] : ["default"],
|
|
128
|
-
listed: m.listed,
|
|
129
|
-
scored,
|
|
130
|
-
inferred,
|
|
131
|
-
});
|
|
132
|
-
}
|
|
133
|
-
for (const rung of rungs) {
|
|
134
|
-
const r = tryParseRung(rung);
|
|
135
|
-
if (!r) continue;
|
|
136
|
-
const m = find(r.backend, r.model);
|
|
137
|
-
if (m) {
|
|
138
|
-
if (!m.efforts.includes(r.effort)) m.efforts.push(r.effort);
|
|
139
|
-
continue;
|
|
140
|
-
}
|
|
141
|
-
out.push({
|
|
142
|
-
backend: r.backend,
|
|
143
|
-
model: r.model,
|
|
144
|
-
key: billingKeyOf(r),
|
|
145
|
-
efforts: [r.effort],
|
|
146
|
-
listed: null,
|
|
147
|
-
scored: new Set(),
|
|
148
|
-
inferred: new Set(),
|
|
149
|
-
});
|
|
150
|
-
}
|
|
151
|
-
return out;
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
const rungOf = (m: ModelEntry, effort: string) => `${m.backend}:${m.model}#${effort}`;
|
|
155
|
-
|
|
156
|
-
/** The ladder routing would climb, cheapest first, as short rungs. */
|
|
157
|
-
function ladderOf(i: TreeInput, role: Role): string {
|
|
158
|
-
const rc = i.profile.roles[role];
|
|
159
|
-
if (!rc.enabled) return "off";
|
|
160
|
-
const ladder = candidates(i.catalog, routingProfileOf(i.profile, role), role).map((c) => shortRung(c.rung));
|
|
161
|
-
return ladder.length ? ladder.join(" → ") : "no usable rung";
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
/** Spec §9.1's Profiles tree for the draft: one row per line, in order. */
|
|
165
|
-
export function buildRows(i: TreeInput): Row[] {
|
|
166
|
-
const rows: Row[] = [];
|
|
167
|
-
const labels = new Map<string, string>();
|
|
168
|
-
const open = (key: string) => i.expandAll === true || i.expanded.has(key);
|
|
169
|
-
const add = (
|
|
170
|
-
r: Omit<Row, "search" | "dim" | "issue" | "expandable" | "expanded" | "selectable" | "check"> &
|
|
171
|
-
Partial<Pick<Row, "dim" | "issue" | "expandable" | "selectable" | "check">>,
|
|
172
|
-
) => {
|
|
173
|
-
const path = `${r.parent ? (labels.get(r.parent) ?? "") : ""} ${r.label}`;
|
|
174
|
-
labels.set(r.key, path);
|
|
175
|
-
const row: Row = {
|
|
176
|
-
check: null,
|
|
177
|
-
dim: false,
|
|
178
|
-
issue: null,
|
|
179
|
-
expandable: false,
|
|
180
|
-
selectable: true,
|
|
181
|
-
...r,
|
|
182
|
-
expanded: r.expandable === true && open(r.key),
|
|
183
|
-
search: `${path} ${r.value}`.toLowerCase(),
|
|
184
|
-
};
|
|
185
|
-
rows.push(row);
|
|
186
|
-
return row;
|
|
187
|
-
};
|
|
188
|
-
const section = (key: string, label: string) =>
|
|
189
|
-
add({ key, parent: null, depth: 0, label, value: "", selectable: false, action: { type: "section" } });
|
|
190
|
-
|
|
191
|
-
section("section:roles", "ROLES");
|
|
192
|
-
for (const role of ROLES) {
|
|
193
|
-
const rc = i.profile.roles[role];
|
|
194
|
-
const roleKey = `role:${role}`;
|
|
195
|
-
const roleRow = add({
|
|
196
|
-
key: roleKey,
|
|
197
|
-
parent: "section:roles",
|
|
198
|
-
depth: 1,
|
|
199
|
-
label: role,
|
|
200
|
-
value: ladderOf(i, role),
|
|
201
|
-
check: rc.enabled,
|
|
202
|
-
expandable: true,
|
|
203
|
-
issue: issueFor(
|
|
204
|
-
i.validation,
|
|
205
|
-
(p) =>
|
|
206
|
-
p === `roles.${role}.enabled` || p === `roles.${role}.rungs` || p === `roles.${role}.defaultRung`,
|
|
207
|
-
),
|
|
208
|
-
action: { type: "role", role },
|
|
209
|
-
});
|
|
210
|
-
if (!roleRow.expanded) continue;
|
|
211
|
-
const dim = !rc.enabled;
|
|
212
|
-
const weakest = rc.rungs.some((r) => i.enforcement(r, rc.access) === "advisory")
|
|
213
|
-
? "advisory"
|
|
214
|
-
: "enforced";
|
|
215
|
-
add({
|
|
216
|
-
key: `access:${role}`,
|
|
217
|
-
parent: roleKey,
|
|
218
|
-
depth: 2,
|
|
219
|
-
label: "access",
|
|
220
|
-
value: `${rc.access} · ${rc.rungs.length ? weakest : "no rung"}`,
|
|
221
|
-
dim,
|
|
222
|
-
issue: issueFor(i.validation, (p) => p === `roles.${role}.access`),
|
|
223
|
-
action: { type: "access", role },
|
|
224
|
-
});
|
|
225
|
-
add({
|
|
226
|
-
key: `start:${role}`,
|
|
227
|
-
parent: roleKey,
|
|
228
|
-
depth: 2,
|
|
229
|
-
label: "default rung",
|
|
230
|
-
value: rc.defaultRung ? shortRung(rc.defaultRung) : "the cheapest that clears the bar",
|
|
231
|
-
dim,
|
|
232
|
-
action: { type: "start", role },
|
|
233
|
-
});
|
|
234
|
-
const entries = modelsFor(i.models, role, rc.rungs);
|
|
235
|
-
const keys = [...new Set(entries.map((m) => m.key))].sort(
|
|
236
|
-
(a, b) => GROUP_ORDER.indexOf(a) - GROUP_ORDER.indexOf(b) || a.localeCompare(b),
|
|
237
|
-
);
|
|
238
|
-
for (const key of keys) {
|
|
239
|
-
const groupKey = `group:${role}:${key}`;
|
|
240
|
-
const note = GROUP_NOTE[key];
|
|
241
|
-
add({
|
|
242
|
-
key: groupKey,
|
|
243
|
-
parent: roleKey,
|
|
244
|
-
depth: 2,
|
|
245
|
-
label: note ? `${key} (${note})` : key,
|
|
246
|
-
value: i.profile.billing[key] ?? "",
|
|
247
|
-
selectable: false,
|
|
248
|
-
dim: true,
|
|
249
|
-
action: { type: "group" },
|
|
250
|
-
});
|
|
251
|
-
for (const m of entries.filter((e) => e.key === key)) {
|
|
252
|
-
const modelKey = `model:${role}:${m.backend}:${m.model}`;
|
|
253
|
-
const ticked = m.efforts.filter((e) => rc.rungs.includes(rungOf(m, e))).length;
|
|
254
|
-
const usable = m.efforts.some((e) => m.scored.has(e) || i.staged[rungOf(m, e)]);
|
|
255
|
-
const flags = [m.listed === false ? "not listed" : "", usable ? "" : "unscored"].filter(Boolean);
|
|
256
|
-
const modelRow = add({
|
|
257
|
-
key: modelKey,
|
|
258
|
-
parent: groupKey,
|
|
259
|
-
depth: 3,
|
|
260
|
-
label: m.model,
|
|
261
|
-
value: [`${ticked} of ${m.efforts.length}`, ...flags].join(" · "),
|
|
262
|
-
expandable: true,
|
|
263
|
-
dim: dim || !usable,
|
|
264
|
-
action: { type: "model" },
|
|
265
|
-
});
|
|
266
|
-
if (!modelRow.expanded) continue;
|
|
267
|
-
for (const effort of m.efforts) {
|
|
268
|
-
const rung = rungOf(m, effort);
|
|
269
|
-
const staged = i.staged[rung];
|
|
270
|
-
const scored = m.scored.has(effort) || staged !== undefined;
|
|
271
|
-
const value = staged
|
|
272
|
-
? `treated like ${staged} · unsaved`
|
|
273
|
-
: !scored
|
|
274
|
-
? "unscored · enter: treat like"
|
|
275
|
-
: m.inferred.has(effort)
|
|
276
|
-
? "inferred"
|
|
277
|
-
: "";
|
|
278
|
-
add({
|
|
279
|
-
key: `rung:${role}:${rung}`,
|
|
280
|
-
parent: modelKey,
|
|
281
|
-
depth: 4,
|
|
282
|
-
label: effort,
|
|
283
|
-
value,
|
|
284
|
-
check: rc.rungs.includes(rung),
|
|
285
|
-
dim: dim || !scored,
|
|
286
|
-
action: { type: "rung", role, rung, scored },
|
|
287
|
-
});
|
|
288
|
-
}
|
|
289
|
-
}
|
|
290
|
-
}
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
section("section:routing", "ROUTING");
|
|
294
|
-
add({
|
|
295
|
-
key: "objective",
|
|
296
|
-
parent: "section:routing",
|
|
297
|
-
depth: 1,
|
|
298
|
-
label: "objective",
|
|
299
|
-
value: i.profile.objective,
|
|
300
|
-
action: { type: "objective" },
|
|
301
|
-
});
|
|
302
|
-
add({
|
|
303
|
-
key: "jev",
|
|
304
|
-
parent: "section:routing",
|
|
305
|
-
depth: 1,
|
|
306
|
-
label: "Jev",
|
|
307
|
-
value: i.profile.jev.use,
|
|
308
|
-
action: { type: "jev" },
|
|
309
|
-
});
|
|
310
|
-
|
|
311
|
-
section("section:harness", "HARNESS");
|
|
312
|
-
for (const h of i.harnesses) {
|
|
313
|
-
const isolated = i.profile.harness[h]?.isolated ?? false;
|
|
314
|
-
add({
|
|
315
|
-
key: `harness:${h}`,
|
|
316
|
-
parent: "section:harness",
|
|
317
|
-
depth: 1,
|
|
318
|
-
label: h,
|
|
319
|
-
value: isolated ? "isolated" : "your own config",
|
|
320
|
-
check: isolated,
|
|
321
|
-
action: { type: "isolated", harness: h },
|
|
322
|
-
});
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
section("section:budget", "BUDGET");
|
|
326
|
-
const b = i.profile.budget;
|
|
327
|
-
for (const [path, label, v] of [
|
|
328
|
-
["budget.minutes", "minutes", b.minutes],
|
|
329
|
-
["budget.tokens", "tokens", b.tokens],
|
|
330
|
-
["budget.usd", "usd", b.usd],
|
|
331
|
-
] as const)
|
|
332
|
-
add({
|
|
333
|
-
key: path,
|
|
334
|
-
parent: "section:budget",
|
|
335
|
-
depth: 1,
|
|
336
|
-
label,
|
|
337
|
-
value: v === undefined ? "no cap" : String(v),
|
|
338
|
-
action: { type: "number", path },
|
|
339
|
-
});
|
|
340
|
-
|
|
341
|
-
section("section:failover", "FAILOVER");
|
|
342
|
-
const onLadders = ROLES.filter((r) => i.profile.roles[r].enabled).flatMap((r) => i.profile.roles[r].rungs);
|
|
343
|
-
const fromRungs = [
|
|
344
|
-
...new Set([...onLadders.filter((r) => !r.startsWith("claude:")), ...Object.keys(i.profile.failover)]),
|
|
345
|
-
].sort();
|
|
346
|
-
if (fromRungs.length === 0)
|
|
347
|
-
add({
|
|
348
|
-
key: "failover:none",
|
|
349
|
-
parent: "section:failover",
|
|
350
|
-
depth: 1,
|
|
351
|
-
label: "no rung to stand in for",
|
|
352
|
-
value: "",
|
|
353
|
-
selectable: false,
|
|
354
|
-
dim: true,
|
|
355
|
-
action: { type: "section" },
|
|
356
|
-
});
|
|
357
|
-
for (const rung of fromRungs) {
|
|
358
|
-
const to = i.profile.failover[rung];
|
|
359
|
-
let mark = "";
|
|
360
|
-
if (to) {
|
|
361
|
-
try {
|
|
362
|
-
const note = inferredScores(i.catalog, rungInfo(i.catalog, to)).note;
|
|
363
|
-
mark = note ? ` (${note})` : "";
|
|
364
|
-
} catch {
|
|
365
|
-
mark = "";
|
|
366
|
-
}
|
|
367
|
-
}
|
|
368
|
-
add({
|
|
369
|
-
key: `failover:${rung}`,
|
|
370
|
-
parent: "section:failover",
|
|
371
|
-
depth: 1,
|
|
372
|
-
label: shortRung(rung),
|
|
373
|
-
value: to ? `→ ${shortRung(to)}${mark}` : "none",
|
|
374
|
-
issue: issueFor(i.validation, (p) => p === `failover.${rung}`),
|
|
375
|
-
action: { type: "failover", rung },
|
|
376
|
-
});
|
|
377
|
-
}
|
|
378
|
-
|
|
379
|
-
section("section:timeouts", "TIMEOUTS");
|
|
380
|
-
add({
|
|
381
|
-
key: "timeouts.idleMin",
|
|
382
|
-
parent: "section:timeouts",
|
|
383
|
-
depth: 1,
|
|
384
|
-
label: "idle minutes",
|
|
385
|
-
value: String(i.profile.timeouts.idleMin),
|
|
386
|
-
action: { type: "number", path: "timeouts.idleMin" },
|
|
387
|
-
});
|
|
388
|
-
add({
|
|
389
|
-
key: "timeouts.wallMin",
|
|
390
|
-
parent: "section:timeouts",
|
|
391
|
-
depth: 1,
|
|
392
|
-
label: "wall minutes",
|
|
393
|
-
value: String(i.profile.timeouts.wallMin),
|
|
394
|
-
action: { type: "number", path: "timeouts.wallMin" },
|
|
395
|
-
});
|
|
396
|
-
|
|
397
|
-
section("section:notify", "NOTIFY");
|
|
398
|
-
for (const moment of NOTIFY)
|
|
399
|
-
add({
|
|
400
|
-
key: `notify:${moment}`,
|
|
401
|
-
parent: "section:notify",
|
|
402
|
-
depth: 1,
|
|
403
|
-
label: moment,
|
|
404
|
-
value: "",
|
|
405
|
-
check: i.profile.notify.includes(moment),
|
|
406
|
-
action: { type: "notify", moment },
|
|
407
|
-
});
|
|
408
|
-
return rows;
|
|
409
|
-
}
|
|
410
|
-
|
|
411
|
-
/** Rows matching every word of `text`, with their parents so each keeps its place in the tree. */
|
|
412
|
-
export function filterRows(rows: Row[], text: string): Row[] {
|
|
413
|
-
const words = text.toLowerCase().split(/\s+/).filter(Boolean);
|
|
414
|
-
if (words.length === 0) return rows;
|
|
415
|
-
const keep = new Set<string>();
|
|
416
|
-
const byKey = new Map(rows.map((r) => [r.key, r]));
|
|
417
|
-
for (const r of rows)
|
|
418
|
-
if (r.selectable && words.every((w) => r.search.includes(w)))
|
|
419
|
-
for (let k: string | null = r.key; k && !keep.has(k); k = byKey.get(k)?.parent ?? null) keep.add(k);
|
|
420
|
-
return rows.filter((r) => keep.has(r.key));
|
|
421
|
-
}
|
|
422
|
-
|
|
423
|
-
/** The first row the filter matches itself (not only as a parent of a match): where the cursor goes. */
|
|
424
|
-
export function firstMatch(rows: Row[], text: string): Row | null {
|
|
425
|
-
const words = text.toLowerCase().split(/\s+/).filter(Boolean);
|
|
426
|
-
return (
|
|
427
|
-
rows.find((r) => r.selectable && words.length > 0 && words.every((w) => r.search.includes(w))) ?? null
|
|
428
|
-
);
|
|
429
|
-
}
|