@wizardingcode/shibaox-tui 0.1.3 → 0.1.4

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.
Files changed (64) hide show
  1. package/dist/main.js +8188 -0
  2. package/dist/main.js.map +67 -0
  3. package/package.json +8 -12
  4. package/src/app.tsx +0 -300
  5. package/src/component/dialogs/confirm.tsx +0 -29
  6. package/src/component/dialogs/diff.tsx +0 -143
  7. package/src/component/dialogs/help.tsx +0 -99
  8. package/src/component/dialogs/keys.tsx +0 -201
  9. package/src/component/dialogs/palette.tsx +0 -95
  10. package/src/component/dialogs/runs.tsx +0 -115
  11. package/src/component/dialogs/tiers.tsx +0 -240
  12. package/src/component/footer.tsx +0 -66
  13. package/src/component/logo.tsx +0 -29
  14. package/src/component/prompt/autocomplete.tsx +0 -44
  15. package/src/component/prompt/index.tsx +0 -213
  16. package/src/component/reconnecting.tsx +0 -44
  17. package/src/component/sidebar.tsx +0 -235
  18. package/src/component/tabs.tsx +0 -116
  19. package/src/component/too-small.tsx +0 -15
  20. package/src/context/client.tsx +0 -54
  21. package/src/context/commands.tsx +0 -44
  22. package/src/context/config.tsx +0 -22
  23. package/src/context/data-state.ts +0 -41
  24. package/src/context/data.tsx +0 -308
  25. package/src/context/exit.tsx +0 -13
  26. package/src/context/keys.tsx +0 -70
  27. package/src/context/poller.ts +0 -352
  28. package/src/context/prefs.tsx +0 -54
  29. package/src/context/route.tsx +0 -23
  30. package/src/main.tsx +0 -55
  31. package/src/model/format.ts +0 -38
  32. package/src/model/prompt-commands.ts +0 -315
  33. package/src/model/status.ts +0 -55
  34. package/src/model/stream.ts +0 -464
  35. package/src/motion/animation.ts +0 -235
  36. package/src/motion/config.tsx +0 -15
  37. package/src/motion/fade-in-text.tsx +0 -93
  38. package/src/motion/masked-text.ts +0 -62
  39. package/src/motion/one-cell-motion.ts +0 -100
  40. package/src/motion/pulse.ts +0 -38
  41. package/src/motion/shimmer-text.tsx +0 -78
  42. package/src/motion/spinner-renderable.ts +0 -171
  43. package/src/motion/spinner.tsx +0 -100
  44. package/src/motion/subcell.ts +0 -40
  45. package/src/routes/home.tsx +0 -365
  46. package/src/routes/session/approval-bar.tsx +0 -136
  47. package/src/routes/session/cards.tsx +0 -495
  48. package/src/routes/session/context-line.tsx +0 -129
  49. package/src/routes/session/continue.tsx +0 -104
  50. package/src/routes/session/index.tsx +0 -340
  51. package/src/routes/session/request.tsx +0 -87
  52. package/src/routes/session/timeline.tsx +0 -313
  53. package/src/testing/fake-client.ts +0 -249
  54. package/src/theme/context.tsx +0 -44
  55. package/src/theme/resolve.ts +0 -196
  56. package/src/theme/shibaox.json +0 -159
  57. package/src/ui/border.ts +0 -20
  58. package/src/ui/delayed-presence.ts +0 -23
  59. package/src/ui/dialog.tsx +0 -123
  60. package/src/ui/layout.ts +0 -23
  61. package/src/ui/marquee.ts +0 -75
  62. package/src/ui/pane-resize.ts +0 -77
  63. package/src/ui/toast.tsx +0 -203
  64. package/tsconfig.json +0 -19
@@ -1,201 +0,0 @@
1
- import { TextAttributes } from '@opentui/core';
2
- import type { KeyRow } from '@wizardingcode/shibaox-daemon';
3
- import { createMemo, createResource, createSignal, For, type JSX, Show } from 'solid-js';
4
- import { useClient } from '../../context/client.js';
5
- import { useKeys } from '../../context/keys.js';
6
- import { useTheme } from '../../theme/context.js';
7
- import { Dialog, useDialog } from '../../ui/dialog.js';
8
- import { useToast } from '../../ui/toast.js';
9
-
10
- const VISIBLE = 16;
11
- /** The keys most people set first, ahead of the rest of the catalog. */
12
- const PRIORITY = [
13
- 'ANTHROPIC_API_KEY',
14
- 'OPENROUTER_API_KEY',
15
- 'OPENAI_API_KEY',
16
- 'GOOGLE_GENERATIVE_AI_API_KEY',
17
- 'TYPESAFE_API_KEY',
18
- 'SHIBAOX_TELEGRAM_TOKEN',
19
- ];
20
-
21
- /** Set keys first, then the usual ones, then the rest by name. */
22
- export function orderKeys(rows: readonly KeyRow[]): KeyRow[] {
23
- const rank = (r: KeyRow) => {
24
- const p = PRIORITY.indexOf(r.name);
25
- return (r.set ? 0 : 1000) + (p < 0 ? PRIORITY.length : p);
26
- };
27
- return [...rows].sort((a, b) => rank(a) - rank(b) || a.name.localeCompare(b.name));
28
- }
29
-
30
- /**
31
- * The key vault: ↑/↓ pick a key, enter types its value (masked) and enter again saves it,
32
- * `x` removes the selected one. Values never show on screen.
33
- */
34
- export function KeysDialog(): JSX.Element {
35
- const client = useClient();
36
- const dialog = useDialog();
37
- const toast = useToast();
38
- const theme = useTheme().surface('dialog');
39
- const [rows, { refetch }] = createResource<KeyRow[]>(() => client.keys().catch(() => []));
40
- const sorted = createMemo(() => orderKeys(rows() ?? []));
41
- const [cursor, setCursor] = createSignal(0);
42
- /** After a save the list reorders (set keys first): the cursor stays on the same key. */
43
- const reload = async () => {
44
- const name = selected()?.name;
45
- await refetch();
46
- if (name) {
47
- const i = sorted().findIndex((r) => r.name === name);
48
- if (i >= 0) setCursor(i);
49
- }
50
- };
51
- const [editing, setEditing] = createSignal<string | undefined>();
52
- const [value, setValue] = createSignal('');
53
- const selected = () => sorted()[Math.min(cursor(), Math.max(0, sorted().length - 1))];
54
- const save = async () => {
55
- const name = editing();
56
- const v = value().trim();
57
- if (!name) return;
58
- if (!v) {
59
- setEditing(undefined);
60
- return;
61
- }
62
- try {
63
- await client.setKey(name, v);
64
- toast.show({ message: `${name} saved in the vault`, variant: 'success' });
65
- await reload();
66
- } catch (e) {
67
- toast.show({
68
- message: `Could not save ${name}: ${e instanceof Error ? e.message : String(e)}`,
69
- variant: 'error',
70
- });
71
- }
72
- setValue('');
73
- setEditing(undefined);
74
- };
75
- const remove = async () => {
76
- const r = selected();
77
- if (!r?.set || r.source !== 'vault') {
78
- if (r?.source === 'env')
79
- toast.show({ message: `${r.name} comes from the shell, not the vault`, variant: 'info' });
80
- return;
81
- }
82
- await client.unsetKey(r.name).catch(() => undefined);
83
- toast.show({ message: `${r.name} removed from the vault`, variant: 'info' });
84
- await reload();
85
- };
86
- useKeys('dialog', (key) => {
87
- if (editing()) return false; // the input owns the keys while a value is typed
88
- if (key.name === 'up') {
89
- setCursor((c) => Math.max(0, c - 1));
90
- return true;
91
- }
92
- if (key.name === 'down') {
93
- setCursor((c) => Math.min(sorted().length - 1, c + 1));
94
- return true;
95
- }
96
- if (key.name === 'return') {
97
- const r = selected();
98
- // the input mounts on the next tick: a focused input would receive this same enter
99
- if (r)
100
- setTimeout(() => {
101
- setValue('');
102
- setEditing(r.name);
103
- }, 0);
104
- return true;
105
- }
106
- if (key.name === 'x' && !key.ctrl) {
107
- void remove();
108
- return true;
109
- }
110
- return false;
111
- });
112
- const start = () =>
113
- Math.max(0, Math.min(cursor() - Math.floor(VISIBLE / 2), sorted().length - VISIBLE));
114
- const window = () => sorted().slice(start(), start() + VISIBLE);
115
- return (
116
- <Dialog size="large" title="Keys" onClose={() => dialog.close()}>
117
- <box flexDirection="column" width="100%">
118
- <Show when={rows.loading}>
119
- <text fg={theme.text.muted}>Reading the vault…</text>
120
- </Show>
121
- <For each={window()}>
122
- {(r, i) => {
123
- const active = () => start() + i() === cursor();
124
- return (
125
- <box
126
- height={1}
127
- flexShrink={0}
128
- flexDirection="row"
129
- backgroundColor={active() ? theme.background.action.primary.selected : undefined}
130
- >
131
- <text
132
- fg={r.set ? theme.text.feedback.success : theme.text.muted}
133
- wrapMode="none"
134
- flexShrink={0}
135
- >
136
- {r.set ? ' ● ' : ' ○ '}
137
- </text>
138
- <text
139
- fg={active() ? theme.text.action.primary.selected : theme.text.base}
140
- attributes={TextAttributes.BOLD}
141
- wrapMode="none"
142
- flexShrink={0}
143
- >
144
- {r.name.padEnd(28)}
145
- </text>
146
- <text fg={theme.text.muted} wrapMode="none" flexShrink={0}>
147
- {(r.set ? `${r.masked ?? ''} ${r.source ?? ''}` : 'missing').padEnd(22)}
148
- </text>
149
- <text fg={theme.text.muted} wrapMode="none" flexShrink={1}>
150
- {r.description}
151
- </text>
152
- </box>
153
- );
154
- }}
155
- </For>
156
- <Show when={sorted().length > VISIBLE}>
157
- <box height={1} flexShrink={0}>
158
- <text fg={theme.text.muted} wrapMode="none">
159
- {`${cursor() + 1} of ${sorted().length}`}
160
- </text>
161
- </box>
162
- </Show>
163
- <box height={1} flexShrink={0} marginTop={1} flexDirection="row">
164
- <Show
165
- when={editing()}
166
- fallback={
167
- <text fg={theme.text.muted} wrapMode="none">
168
- {'↑/↓ choose · enter set a value · x remove · esc close'}
169
- </text>
170
- }
171
- >
172
- <text fg={theme.text.base} wrapMode="none" flexShrink={0}>
173
- {`value for ${editing() ?? ''}: `}
174
- </text>
175
- <text fg={theme.text.action.primary.selected} wrapMode="none" flexShrink={1}>
176
- {'•'.repeat(Math.min(value().length, 40))}
177
- </text>
178
- <text fg={theme.text.muted} wrapMode="none" flexShrink={0}>
179
- {value().length > 40 ? ` (${value().length} chars)` : ''}
180
- </text>
181
- {/* the real input carries the keys; its text is hidden behind the dots */}
182
- <input
183
- focused
184
- width={1}
185
- onInput={setValue}
186
- onSubmit={() => void save()}
187
- backgroundColor={theme.background.base}
188
- focusedBackgroundColor={theme.background.base}
189
- textColor={theme.background.base}
190
- placeholderColor={theme.background.base}
191
- cursorColor={theme.text.action.primary.selected}
192
- />
193
- <text fg={theme.text.muted} wrapMode="none" flexShrink={0}>
194
- {' enter saves'}
195
- </text>
196
- </Show>
197
- </box>
198
- </box>
199
- </Dialog>
200
- );
201
- }
@@ -1,95 +0,0 @@
1
- import { TextAttributes } from '@opentui/core';
2
- import fuzzysort from 'fuzzysort';
3
- import { createMemo, createSignal, For, type JSX } from 'solid-js';
4
- import { useCommands } from '../../context/commands.js';
5
- import { useKeys } from '../../context/keys.js';
6
- import { useTheme } from '../../theme/context.js';
7
- import { Dialog, useDialog } from '../../ui/dialog.js';
8
-
9
- /** Every available command with its keys; enter runs the selected one. */
10
- export function PaletteDialog(): JSX.Element {
11
- const registry = useCommands();
12
- const dialog = useDialog();
13
- const theme = useTheme().surface('dialog');
14
- const [query, setQuery] = createSignal('');
15
- const [cursor, setCursor] = createSignal(0);
16
- const rows = createMemo(() => {
17
- const all = registry.commands().filter((c) => c.when?.() ?? true);
18
- const q = query().trim();
19
- if (!q) return all;
20
- return fuzzysort.go(q, all, { key: 'label' }).map((r) => r.obj);
21
- });
22
- const run = () => {
23
- const c = rows()[Math.min(cursor(), rows().length - 1)];
24
- if (!c) return;
25
- dialog.close();
26
- c.run();
27
- };
28
- useKeys('dialog', (key) => {
29
- if (key.name === 'up') {
30
- setCursor((c) => Math.max(0, c - 1));
31
- return true;
32
- }
33
- if (key.name === 'down') {
34
- setCursor((c) => Math.min(rows().length - 1, c + 1));
35
- return true;
36
- }
37
- if (key.name === 'return') {
38
- run();
39
- return true;
40
- }
41
- return false;
42
- });
43
- return (
44
- <Dialog title="Commands" onClose={() => dialog.close()}>
45
- <box height={1} flexShrink={0} flexDirection="row" marginBottom={1}>
46
- <text fg={theme.text.action.primary.selected} flexShrink={0}>
47
- {'› '}
48
- </text>
49
- <input
50
- focused
51
- placeholder="type a command"
52
- onInput={(v: string) => {
53
- setQuery(v);
54
- setCursor(0);
55
- }}
56
- flexGrow={1}
57
- backgroundColor={theme.background.base}
58
- focusedBackgroundColor={theme.background.base}
59
- textColor={theme.text.base}
60
- placeholderColor={theme.text.muted}
61
- cursorColor={theme.text.action.primary.selected}
62
- />
63
- </box>
64
- <For each={rows().slice(0, 12)}>
65
- {(c, i) => {
66
- const selected = () => i() === Math.min(cursor(), rows().length - 1);
67
- return (
68
- <box
69
- height={1}
70
- flexShrink={0}
71
- flexDirection="row"
72
- backgroundColor={selected() ? theme.background.action.primary.selected : undefined}
73
- onMouseUp={() => {
74
- setCursor(i());
75
- run();
76
- }}
77
- >
78
- <text
79
- fg={theme.text.base}
80
- attributes={selected() ? TextAttributes.BOLD : undefined}
81
- wrapMode="none"
82
- >
83
- {c.label}
84
- </text>
85
- <box flexGrow={1} />
86
- <text fg={theme.text.muted} wrapMode="none">
87
- {c.keys}
88
- </text>
89
- </box>
90
- );
91
- }}
92
- </For>
93
- </Dialog>
94
- );
95
- }
@@ -1,115 +0,0 @@
1
- import { TextAttributes } from '@opentui/core';
2
- import fuzzysort from 'fuzzysort';
3
- import { createMemo, createSignal, For, type JSX } from 'solid-js';
4
- import { useData } from '../../context/data.js';
5
- import { useKeys } from '../../context/keys.js';
6
- import { age, money, shortId } from '../../model/format.js';
7
- import { statusOf } from '../../model/status.js';
8
- import { useTheme } from '../../theme/context.js';
9
- import { Dialog, useDialog } from '../../ui/dialog.js';
10
-
11
- const VISIBLE = 20;
12
-
13
- /** Every run the daemon knows, filtered as you type; enter opens the selected one in a tab. */
14
- export function RunsDialog(): JSX.Element {
15
- const data = useData();
16
- const dialog = useDialog();
17
- const theme = useTheme().surface('dialog');
18
- const [query, setQuery] = createSignal('');
19
- const [cursor, setCursor] = createSignal(0);
20
- const rows = createMemo(() => {
21
- const runs = [...data.state.runs].sort((a, b) => b.createdAt.localeCompare(a.createdAt));
22
- const q = query().trim();
23
- if (!q) return runs;
24
- return fuzzysort
25
- .go(q, runs, { key: (r) => `${r.runId} ${r.workflow} ${r.status}` })
26
- .map((r) => r.obj);
27
- });
28
- const open = () => {
29
- const r = rows()[Math.min(cursor(), rows().length - 1)];
30
- if (!r) return;
31
- dialog.close();
32
- data.openRun(r.runId);
33
- };
34
- useKeys('dialog', (key) => {
35
- if (key.name === 'up') {
36
- setCursor((c) => Math.max(0, c - 1));
37
- return true;
38
- }
39
- if (key.name === 'down') {
40
- setCursor((c) => Math.min(rows().length - 1, c + 1));
41
- return true;
42
- }
43
- if (key.name === 'return') {
44
- open();
45
- return true;
46
- }
47
- return false;
48
- });
49
- const start = () =>
50
- Math.max(0, Math.min(cursor() - Math.floor(VISIBLE / 2), rows().length - VISIBLE));
51
- return (
52
- <Dialog size="large" title="Runs" onClose={() => dialog.close()}>
53
- <box height={1} flexShrink={0} flexDirection="row" marginBottom={1}>
54
- <text fg={theme.text.action.primary.selected} flexShrink={0}>
55
- {'› '}
56
- </text>
57
- <input
58
- focused
59
- placeholder="type to filter"
60
- onInput={(v: string) => {
61
- setQuery(v);
62
- setCursor(0);
63
- }}
64
- flexGrow={1}
65
- backgroundColor={theme.background.base}
66
- focusedBackgroundColor={theme.background.base}
67
- textColor={theme.text.base}
68
- placeholderColor={theme.text.muted}
69
- cursorColor={theme.text.action.primary.selected}
70
- />
71
- </box>
72
- <For each={rows().slice(start(), start() + VISIBLE)}>
73
- {(r, i) => {
74
- const selected = () => start() + i() === Math.min(cursor(), rows().length - 1);
75
- const look = () => statusOf({ status: r.status });
76
- const color = () => {
77
- const f = look().feedback;
78
- return f === 'muted' ? theme.text.muted : theme.text.feedback[f];
79
- };
80
- return (
81
- <box
82
- height={1}
83
- flexShrink={0}
84
- flexDirection="row"
85
- backgroundColor={selected() ? theme.background.action.primary.selected : undefined}
86
- onMouseUp={() => {
87
- setCursor(start() + i());
88
- open();
89
- }}
90
- >
91
- <text fg={color()} wrapMode="none">{`${look().symbol} `}</text>
92
- <text
93
- fg={theme.text.base}
94
- attributes={selected() ? TextAttributes.BOLD : undefined}
95
- wrapMode="none"
96
- >
97
- {`${shortId(r.runId)} ${r.workflow}`}
98
- </text>
99
- <box flexGrow={1} />
100
- <text
101
- fg={theme.text.muted}
102
- wrapMode="none"
103
- >{`${look().word} · ${age(r.createdAt)} · ${money(r.spentUsd)}`}</text>
104
- </box>
105
- );
106
- }}
107
- </For>
108
- <box height={1} flexShrink={0} marginTop={1}>
109
- <text fg={theme.text.muted} wrapMode="none">
110
- {rows().length === 0 ? 'No runs match' : '↑/↓ select · enter open · esc close'}
111
- </text>
112
- </box>
113
- </Dialog>
114
- );
115
- }
@@ -1,240 +0,0 @@
1
- import { TextAttributes } from '@opentui/core';
2
- import type { ModelChoice, OrgConfig, OrgConfigPatch } from '@wizardingcode/shibaox-daemon';
3
- import fuzzysort from 'fuzzysort';
4
- import { createMemo, createResource, createSignal, For, type JSX, Show } from 'solid-js';
5
- import { useClient } from '../../context/client.js';
6
- import { useKeys } from '../../context/keys.js';
7
- import { useTheme } from '../../theme/context.js';
8
- import { Dialog, useDialog } from '../../ui/dialog.js';
9
- import { useToast } from '../../ui/toast.js';
10
-
11
- const VISIBLE = 12;
12
- type RowName = 'strong' | 'cheap' | 'decision' | 'judge' | 'adapter';
13
- interface Row {
14
- name: RowName;
15
- value?: string;
16
- note: string;
17
- }
18
- const ADAPTERS = ['direct', 'claude-code', 'mock'];
19
-
20
- /** What the dialog lists for an org config, in the order people read it. */
21
- export function tierRows(c: OrgConfig | undefined): Row[] {
22
- return [
23
- { name: 'strong', value: c?.tiers.strong, note: 'tasks that need the best model' },
24
- { name: 'cheap', value: c?.tiers.cheap, note: 'the orchestrator and light roles' },
25
- { name: 'decision', value: c?.tiers.decision, note: 'decide nodes (model ref or jev-latest)' },
26
- { name: 'judge', value: c?.judge, note: 'judge checks (default: decision, then strong)' },
27
- { name: 'adapter', value: c?.adapter, note: 'default runtime; a chosen model overrides it' },
28
- ];
29
- }
30
-
31
- /** The choices for a row: the daemon's models (usable first) for a tier, the adapters otherwise. */
32
- export function tierChoices(name: RowName, models: readonly ModelChoice[]): string[] {
33
- if (name === 'adapter') return ADAPTERS;
34
- const refs = [...models]
35
- .sort((a, b) => Number(b.configured) - Number(a.configured))
36
- .map((m) => m.ref);
37
- return name === 'decision' ? ['jev-latest', ...refs] : refs;
38
- }
39
-
40
- /**
41
- * The org's tiers, judge and adapter: ↑/↓ pick a row, enter opens the list of models (typed
42
- * text filters it), enter again saves through the daemon; `x` clears judge/adapter.
43
- */
44
- export function TiersDialog(props: { orgRoot: string; onSaved?: () => void }): JSX.Element {
45
- const client = useClient();
46
- const dialog = useDialog();
47
- const toast = useToast();
48
- const theme = useTheme().surface('dialog');
49
- const [config, { refetch }] = createResource(() => client.orgConfig(props.orgRoot));
50
- const [models] = createResource(() => client.models().catch(() => []));
51
- const rows = createMemo(() => tierRows(config()));
52
- const [cursor, setCursor] = createSignal(0);
53
- const [editing, setEditing] = createSignal<RowName | undefined>();
54
- const [query, setQuery] = createSignal('');
55
- const [pick, setPick] = createSignal(0);
56
- const choices = createMemo(() => {
57
- const name = editing();
58
- if (!name) return [];
59
- const all = tierChoices(name, models() ?? []);
60
- const q = query().trim();
61
- if (!q) return all;
62
- return fuzzysort.go(q, all).map((r) => r.target);
63
- });
64
- const selected = () => rows()[Math.min(cursor(), rows().length - 1)];
65
- const patchFor = (name: RowName, value: string | null): OrgConfigPatch =>
66
- name === 'judge'
67
- ? { judge: value }
68
- : name === 'adapter'
69
- ? { adapter: value as OrgConfigPatch['adapter'] }
70
- : { tiers: { [name]: value } };
71
- const save = async (name: RowName, value: string | null) => {
72
- try {
73
- await client.setOrgConfig(props.orgRoot, patchFor(name, value));
74
- toast.show({ message: value ? `${name} → ${value}` : `${name} cleared`, variant: 'success' });
75
- await refetch();
76
- props.onSaved?.();
77
- } catch (e) {
78
- toast.show({
79
- message: `Could not set ${name}: ${e instanceof Error ? e.message : String(e)}`,
80
- variant: 'error',
81
- });
82
- }
83
- };
84
- const choose = () => {
85
- const name = editing();
86
- const value = choices()[Math.min(pick(), choices().length - 1)];
87
- setEditing(undefined);
88
- setQuery('');
89
- if (name && value) void save(name, value);
90
- };
91
- useKeys('dialog', (key) => {
92
- if (editing()) {
93
- // the input has the text; the list keys are ours
94
- if (key.name === 'up') {
95
- setPick((c) => Math.max(0, c - 1));
96
- return true;
97
- }
98
- if (key.name === 'down') {
99
- setPick((c) => Math.min(choices().length - 1, c + 1));
100
- return true;
101
- }
102
- return false;
103
- }
104
- if (key.name === 'up') {
105
- setCursor((c) => Math.max(0, c - 1));
106
- return true;
107
- }
108
- if (key.name === 'down') {
109
- setCursor((c) => Math.min(rows().length - 1, c + 1));
110
- return true;
111
- }
112
- if (key.name === 'return') {
113
- const r = selected();
114
- // the input mounts on the next tick: a focused input would receive this same enter
115
- if (r)
116
- setTimeout(() => {
117
- setQuery('');
118
- setPick(0);
119
- setEditing(r.name);
120
- }, 0);
121
- return true;
122
- }
123
- if (key.name === 'x' && !key.ctrl) {
124
- const r = selected();
125
- if (r && (r.name === 'judge' || r.name === 'adapter')) void save(r.name, null);
126
- else toast.show({ message: 'Tiers cannot be cleared, pick another model', variant: 'info' });
127
- return true;
128
- }
129
- return false;
130
- });
131
- const start = () =>
132
- Math.max(0, Math.min(pick() - Math.floor(VISIBLE / 2), choices().length - VISIBLE));
133
- const title = () => `Tiers · ${config()?.organization ?? ''}`;
134
- // the Dialog sees escape first (its handler is the newest): back out of the list, else close
135
- const back = () => {
136
- if (!editing()) return dialog.close();
137
- setEditing(undefined);
138
- setQuery('');
139
- };
140
- return (
141
- <Dialog size="large" title={title()} onClose={back}>
142
- <box flexDirection="column" width="100%">
143
- <Show when={config.error}>
144
- <text fg={theme.text.feedback.error}>
145
- {`Could not read the org: ${String((config.error as Error)?.message ?? config.error)}`}
146
- </text>
147
- </Show>
148
- <For each={rows()}>
149
- {(r, i) => {
150
- const active = () => i() === Math.min(cursor(), rows().length - 1);
151
- return (
152
- <box
153
- height={1}
154
- flexShrink={0}
155
- flexDirection="row"
156
- backgroundColor={active() ? theme.background.action.primary.selected : undefined}
157
- >
158
- <text
159
- fg={active() ? theme.text.action.primary.selected : theme.text.base}
160
- attributes={TextAttributes.BOLD}
161
- wrapMode="none"
162
- flexShrink={0}
163
- >
164
- {` ${r.name.padEnd(10)}`}
165
- </text>
166
- <text
167
- fg={r.value ? theme.text.base : theme.text.muted}
168
- wrapMode="none"
169
- flexShrink={0}
170
- >
171
- {(r.value ?? '—').padEnd(44)}
172
- </text>
173
- <text fg={theme.text.muted} wrapMode="none" flexShrink={1}>
174
- {r.note}
175
- </text>
176
- </box>
177
- );
178
- }}
179
- </For>
180
- <Show
181
- when={editing()}
182
- fallback={
183
- <box height={1} flexShrink={0} marginTop={1}>
184
- <text fg={theme.text.muted} wrapMode="none">
185
- {`${config()?.per_run_usd !== undefined ? `budget $${config()?.per_run_usd} per run (/budget for this run) · ` : ''}↑/↓ choose · enter change · x clear judge/adapter · esc close`}
186
- </text>
187
- </box>
188
- }
189
- >
190
- <box height={1} flexShrink={0} marginTop={1} flexDirection="row">
191
- <text fg={theme.text.base} wrapMode="none" flexShrink={0}>
192
- {`${editing() ?? ''} › `}
193
- </text>
194
- <input
195
- focused
196
- placeholder="type to filter"
197
- onInput={(v: string) => {
198
- setQuery(v);
199
- setPick(0);
200
- }}
201
- onSubmit={choose}
202
- flexGrow={1}
203
- backgroundColor={theme.background.base}
204
- focusedBackgroundColor={theme.background.base}
205
- textColor={theme.text.base}
206
- placeholderColor={theme.text.muted}
207
- cursorColor={theme.text.action.primary.selected}
208
- />
209
- </box>
210
- <For each={choices().slice(start(), start() + VISIBLE)}>
211
- {(c, i) => {
212
- const active = () => start() + i() === Math.min(pick(), choices().length - 1);
213
- return (
214
- <box
215
- height={1}
216
- flexShrink={0}
217
- backgroundColor={active() ? theme.background.action.primary.selected : undefined}
218
- >
219
- <text
220
- fg={active() ? theme.text.action.primary.selected : theme.text.base}
221
- wrapMode="none"
222
- >
223
- {` ${c}`}
224
- </text>
225
- </box>
226
- );
227
- }}
228
- </For>
229
- <box height={1} flexShrink={0}>
230
- <text fg={theme.text.muted} wrapMode="none">
231
- {choices().length === 0
232
- ? 'No model matches'
233
- : `${choices().length} choices · ↑/↓ pick · enter save · esc back`}
234
- </text>
235
- </box>
236
- </Show>
237
- </box>
238
- </Dialog>
239
- );
240
- }