@wizardingcode/shibaox-tui 0.1.2 → 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 +9 -13
  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
package/package.json CHANGED
@@ -1,28 +1,24 @@
1
1
  {
2
2
  "name": "@wizardingcode/shibaox-tui",
3
- "version": "0.1.2",
3
+ "version": "0.1.4",
4
4
  "license": "UNLICENSED",
5
5
  "type": "module",
6
6
  "files": [
7
- "src",
8
- "bunfig.toml",
9
- "tsconfig.json"
7
+ "dist",
8
+ "bunfig.toml"
10
9
  ],
11
10
  "exports": {
12
- ".": {
13
- "types": "./src/app.tsx",
14
- "import": "./src/app.tsx"
15
- },
11
+ ".": "./dist/main.js",
16
12
  "./package.json": "./package.json"
17
13
  },
18
14
  "dependencies": {
19
15
  "@opentui/core": "0.5.12",
20
16
  "@opentui/solid": "0.5.12",
21
17
  "fuzzysort": "^4.0.2",
22
- "solid-js": "1.9.15",
23
- "@wizardingcode/shibaox-core": "0.1.2",
24
- "@wizardingcode/shibaox-daemon": "0.1.2",
25
- "@wizardingcode/shibaox-schemas": "0.1.2"
18
+ "solid-js": "1.9.12",
19
+ "@wizardingcode/shibaox-core": "0.1.4",
20
+ "@wizardingcode/shibaox-schemas": "0.1.4",
21
+ "@wizardingcode/shibaox-daemon": "0.1.4"
26
22
  },
27
23
  "devDependencies": {
28
24
  "@types/bun": "^1.3.0",
@@ -36,7 +32,7 @@
36
32
  "url": "git+https://github.com/WizardingCode-io/shibaox.git"
37
33
  },
38
34
  "scripts": {
39
- "build": "tsc -p tsconfig.json --noEmit",
35
+ "build": "tsc -p tsconfig.json --noEmit && bun run build.ts",
40
36
  "test": "vitest run && bun test",
41
37
  "typecheck": "tsc -p tsconfig.json --noEmit"
42
38
  }
package/src/app.tsx DELETED
@@ -1,300 +0,0 @@
1
- import { type CliRenderer, createCliRenderer } from '@opentui/core';
2
- import { render, useTerminalDimensions } from '@opentui/solid';
3
- import { createEffect, type JSX, Match, Show, Switch } from 'solid-js';
4
- import { HelpDialog } from './component/dialogs/help.js';
5
- import { PaletteDialog } from './component/dialogs/palette.js';
6
- import { RunsDialog } from './component/dialogs/runs.js';
7
- import { Reconnecting } from './component/reconnecting.js';
8
- import { Tabs } from './component/tabs.js';
9
- import { TooSmall } from './component/too-small.js';
10
- import { ClientProvider, type DaemonClientLike } from './context/client.js';
11
- import { CommandsProvider, useCommands } from './context/commands.js';
12
- import { ConfigProvider } from './context/config.js';
13
- import { type Data, DataProvider, useData } from './context/data.js';
14
- import { ExitProvider, useExit } from './context/exit.js';
15
- import { KeysProvider, useKeys } from './context/keys.js';
16
- import { loadPrefs, PrefsProvider } from './context/prefs.js';
17
- import { RouteProvider } from './context/route.js';
18
- import { MotionProvider, motionEnabled } from './motion/config.js';
19
- import { Home } from './routes/home.js';
20
- import { SessionFrame } from './routes/session/index.js';
21
- import { ThemeProvider, useTheme } from './theme/context.js';
22
- import { DialogProvider, useDialog } from './ui/dialog.js';
23
- import { tooSmall } from './ui/layout.js';
24
- import { Toast, ToastProvider, useToast } from './ui/toast.js';
25
-
26
- export interface AppOptions {
27
- version: string;
28
- home: string;
29
- cwd?: string;
30
- env?: NodeJS.ProcessEnv;
31
- renderer?: CliRenderer;
32
- log?: (line: string) => void;
33
- }
34
-
35
- /** What tests and the stream entry get from a mounted app. */
36
- export interface AppHooks {
37
- data: Data;
38
- }
39
-
40
- export interface AppProps extends AppOptions {
41
- client: DaemonClientLike;
42
- onExit: (code: number) => void;
43
- /** Stream mode: a single run, no home, no tabs (`shibaox follow`). */
44
- runId?: string;
45
- /** Stream mode: the run reached a terminal status. */
46
- onEnded?: (status: string) => void;
47
- onMount?: (hooks: AppHooks) => void;
48
- /** Where stream failures and malformed frames are noted (`~/.shibaox/tui.log`). */
49
- log?: (line: string) => void;
50
- }
51
-
52
- /** Tabs rail, the active tab's screen (home or a run), overlays and toasts. */
53
- function Shell(props: { single?: string }): JSX.Element {
54
- const data = useData();
55
- const dialog = useDialog();
56
- const commands = useCommands();
57
- const exit = useExit();
58
- const theme = useTheme();
59
- const dimensions = useTerminalDimensions();
60
- const active = () => props.single ?? data.state.active;
61
- const help = () => dialog.open(() => <HelpDialog />);
62
- const shell = [
63
- {
64
- id: 'home',
65
- label: 'New run',
66
- keys: 'ctrl+n',
67
- run: () => data.activate(undefined),
68
- when: () => !props.single,
69
- },
70
- {
71
- id: 'runs',
72
- label: 'Open a run',
73
- keys: 'ctrl+o',
74
- run: () => dialog.open(() => <RunsDialog />),
75
- when: () => !props.single,
76
- },
77
- {
78
- id: 'next',
79
- label: 'Next tab',
80
- keys: 'ctrl+]',
81
- run: () => data.nextTab(1),
82
- when: () => !props.single,
83
- },
84
- {
85
- id: 'prev',
86
- label: 'Previous tab',
87
- keys: 'ctrl+p',
88
- run: () => data.nextTab(-1),
89
- when: () => !props.single,
90
- },
91
- {
92
- id: 'close',
93
- label: 'Close tab',
94
- keys: 'ctrl+w',
95
- run: () => data.state.active && data.closeRun(data.state.active),
96
- when: () => !!data.state.active && !props.single,
97
- },
98
- { id: 'help', label: 'Help', keys: '?', run: help },
99
- { id: 'quit', label: 'Quit', keys: 'ctrl+q', run: () => exit(0) },
100
- ];
101
- commands.register(shell);
102
- useKeys('global', (key) => {
103
- if (key.name === '?' && !key.ctrl) {
104
- help();
105
- return true;
106
- }
107
- if (!key.ctrl) return false;
108
- if (key.name === 'k') {
109
- dialog.open(() => <PaletteDialog />);
110
- return true;
111
- }
112
- if (props.single) return false;
113
- const cmd = shell.find((c) => c.keys === `ctrl+${key.name}`);
114
- if (!cmd) return false;
115
- cmd.run();
116
- return true;
117
- });
118
- return (
119
- <box width="100%" height="100%" flexDirection="column" backgroundColor={theme.background.base}>
120
- <Show when={!tooSmall(dimensions().width, dimensions().height)} fallback={<TooSmall />}>
121
- <box width="100%" height="100%" flexDirection="column">
122
- <Show when={!props.single && data.state.open.length > 0}>
123
- <Tabs />
124
- </Show>
125
- <box flexGrow={1} flexDirection="column" width="100%">
126
- <Switch>
127
- <Match when={!active()}>
128
- <Home />
129
- </Match>
130
- <Match when={active()}>
131
- {(id) => <SessionFrame runId={id()} single={!!props.single} />}
132
- </Match>
133
- </Switch>
134
- </box>
135
- </box>
136
- </Show>
137
- <Reconnecting since={() => data.state.unreachableSince} />
138
- <Toast />
139
- </box>
140
- );
141
- }
142
-
143
- function Hooks(props: {
144
- onMount?: (h: AppHooks) => void;
145
- runId?: string;
146
- onEnded?: (status: string) => void;
147
- }): JSX.Element {
148
- const data = useData();
149
- props.onMount?.({ data });
150
- createEffect(() => {
151
- const status = props.runId ? data.state.ended[props.runId] : undefined;
152
- if (status) props.onEnded?.(status);
153
- });
154
- return null;
155
- }
156
-
157
- function WithData(props: AppProps): JSX.Element {
158
- const toast = useToast();
159
- return (
160
- <DataProvider
161
- client={props.client}
162
- single={props.runId}
163
- toast={(t) => toast.show(t)}
164
- log={props.log}
165
- >
166
- <RouteProvider
167
- initial={props.runId ? { type: 'session', runId: props.runId } : { type: 'home' }}
168
- >
169
- <Hooks onMount={props.onMount} runId={props.runId} onEnded={props.onEnded} />
170
- <CommandsProvider>
171
- <DialogProvider>
172
- <Shell single={props.runId} />
173
- </DialogProvider>
174
- </CommandsProvider>
175
- </RouteProvider>
176
- </DataProvider>
177
- );
178
- }
179
-
180
- /** The whole dashboard: providers around the shell. */
181
- export function App(props: AppProps): JSX.Element {
182
- const env = props.env ?? process.env;
183
- const cwd = props.cwd ?? process.cwd();
184
- return (
185
- <ClientProvider client={props.client}>
186
- <ConfigProvider config={{ version: props.version, home: props.home, cwd, env }}>
187
- <PrefsProvider home={props.home}>
188
- <MotionProvider enabled={motionEnabled(env, loadPrefs(props.home))}>
189
- <ThemeProvider>
190
- <KeysProvider onExit={props.onExit}>
191
- <ExitProvider onExit={props.onExit}>
192
- <ToastProvider>
193
- <WithData {...props} />
194
- </ToastProvider>
195
- </ExitProvider>
196
- </KeysProvider>
197
- </ThemeProvider>
198
- </MotionProvider>
199
- </PrefsProvider>
200
- </ConfigProvider>
201
- </ClientProvider>
202
- );
203
- }
204
-
205
- async function rendererFor(o: AppOptions): Promise<{ renderer: CliRenderer; owned: boolean }> {
206
- if (o.renderer) return { renderer: o.renderer, owned: false };
207
- const renderer = await createCliRenderer({
208
- exitOnCtrlC: false,
209
- screenMode: 'alternate-screen',
210
- useMouse: true,
211
- });
212
- return { renderer, owned: true };
213
- }
214
-
215
- /** Ctrl-C outside Solid so a crash before the first frame can never trap the terminal. */
216
- function onCtrlC(renderer: CliRenderer, fn: () => void): () => void {
217
- const handler = (key: { name: string; ctrl: boolean }) => {
218
- if (key.ctrl && key.name === 'c') fn();
219
- };
220
- renderer.keyInput.on('keypress', handler);
221
- return () => renderer.keyInput.off('keypress', handler);
222
- }
223
-
224
- type Outcome = { code: number; ended?: string };
225
-
226
- function mount(
227
- client: DaemonClientLike,
228
- o: AppOptions & { runId?: string },
229
- finish: (outcome: Outcome) => void,
230
- ): Promise<() => void> {
231
- return rendererFor(o).then(async ({ renderer, owned }) => {
232
- let done = false;
233
- let rendered = false;
234
- const end = (outcome: Outcome) => {
235
- if (done) return;
236
- done = true;
237
- off();
238
- if (owned) renderer.destroy();
239
- finish(outcome);
240
- };
241
- // the in-app handler decides ctrl+c (a prompt may consume it); this listener only saves
242
- // the terminal when the app never rendered
243
- const off = onCtrlC(renderer, () => {
244
- if (!rendered) end({ code: 0 });
245
- });
246
- await render(
247
- () => (
248
- <App
249
- client={client}
250
- version={o.version}
251
- home={o.home}
252
- cwd={o.cwd}
253
- env={o.env}
254
- runId={o.runId}
255
- log={o.log}
256
- onExit={(code) => end({ code })}
257
- onEnded={(status) => end({ code: status === 'completed' ? 0 : 2, ended: status })}
258
- onMount={() => {
259
- rendered = true;
260
- }}
261
- />
262
- ),
263
- renderer,
264
- );
265
- return () => end({ code: 0 });
266
- });
267
- }
268
-
269
- /** `shibaox` / `shibaox ui`: resolves with the exit code when the user quits. */
270
- export function runDashboard(client: DaemonClientLike, o: AppOptions): Promise<number> {
271
- return new Promise((resolve) => {
272
- void mount(client, o, (r) => resolve(r.code));
273
- });
274
- }
275
-
276
- /**
277
- * `shibaox run|follow` with a TTY: one run; ends with the run (0 done, 2 otherwise) or when the
278
- * user leaves (then the message says the run keeps running).
279
- */
280
- export function runStream(
281
- client: DaemonClientLike,
282
- runId: string,
283
- o: AppOptions & { signal?: AbortSignal },
284
- ): Promise<{ code: number; message?: string }> {
285
- return new Promise((resolve) => {
286
- void mount(client, { ...o, runId }, (r) =>
287
- resolve(
288
- r.ended
289
- ? { code: r.code }
290
- : {
291
- code: r.code,
292
- message:
293
- r.code === 0
294
- ? `Run ${runId} keeps running. Follow it with: shibaox follow ${runId}`
295
- : undefined,
296
- },
297
- ),
298
- ).then((stop) => o.signal?.addEventListener('abort', stop, { once: true }));
299
- });
300
- }
@@ -1,29 +0,0 @@
1
- import type { JSX } from 'solid-js';
2
- import { useKeys } from '../../context/keys.js';
3
- import { useTheme } from '../../theme/context.js';
4
- import { Dialog } from '../../ui/dialog.js';
5
-
6
- /** `<message> (y/n)`: y runs onYes, n and esc run onNo. */
7
- export function Confirm(props: {
8
- message: string;
9
- onYes: () => void;
10
- onNo: () => void;
11
- }): JSX.Element {
12
- const theme = useTheme().surface('dialog');
13
- useKeys('dialog', (key) => {
14
- if (key.name === 'y') {
15
- props.onYes();
16
- return true;
17
- }
18
- if (key.name === 'n') {
19
- props.onNo();
20
- return true;
21
- }
22
- return false;
23
- });
24
- return (
25
- <Dialog centered onClose={props.onNo}>
26
- <text fg={theme.text.base}>{`${props.message} (y/n)`}</text>
27
- </Dialog>
28
- );
29
- }
@@ -1,143 +0,0 @@
1
- import { extname } from 'node:path';
2
- import { TextAttributes } from '@opentui/core';
3
- import type { DiffResult } from '@wizardingcode/shibaox-daemon';
4
- import { DaemonHttpError } from '@wizardingcode/shibaox-daemon/client';
5
- import { createMemo, createResource, createSignal, For, type JSX, Show } from 'solid-js';
6
- import { useClient } from '../../context/client.js';
7
- import { useKeys } from '../../context/keys.js';
8
- import { Spinner } from '../../motion/spinner.js';
9
- import { useSyntax, useTheme } from '../../theme/context.js';
10
- import { Dialog, useDialog } from '../../ui/dialog.js';
11
-
12
- const LIST_WIDTH = 24;
13
- const FILETYPES: Record<string, string> = {
14
- '.ts': 'typescript',
15
- '.tsx': 'tsx',
16
- '.js': 'javascript',
17
- '.jsx': 'jsx',
18
- '.json': 'json',
19
- '.md': 'markdown',
20
- '.py': 'python',
21
- '.go': 'go',
22
- '.rs': 'rust',
23
- '.sh': 'bash',
24
- '.yaml': 'yaml',
25
- '.yml': 'yaml',
26
- '.css': 'css',
27
- '.html': 'html',
28
- };
29
-
30
- /** The part of a multi-file patch that belongs to `path`. */
31
- export function patchOf(patch: string, path: string): string {
32
- const header = `diff --git a/${path} b/${path}`;
33
- const start = patch.indexOf(header);
34
- if (start < 0) return '';
35
- const next = patch.indexOf('\ndiff --git ', start + header.length);
36
- return next < 0 ? patch.slice(start) : patch.slice(start, next + 1);
37
- }
38
-
39
- /** The run's changes: a file list on the left, the selected file's diff on the right. */
40
- export function DiffDialog(props: { runId: string }): JSX.Element {
41
- const client = useClient();
42
- const dialog = useDialog();
43
- const theme = useTheme().surface('dialog');
44
- const syntax = useSyntax();
45
- const [result] = createResource<{ diff?: DiffResult; error?: string }>(async () => {
46
- try {
47
- return { diff: await client.diff(props.runId) };
48
- } catch (e) {
49
- if (e instanceof DaemonHttpError && e.code === 'no_workspace')
50
- return { error: 'Workspace is gone' };
51
- return { error: e instanceof Error ? e.message : String(e) };
52
- }
53
- });
54
- const [cursor, setCursor] = createSignal(0);
55
- const files = () => result()?.diff?.files ?? [];
56
- const selected = () => files()[Math.min(cursor(), Math.max(0, files().length - 1))];
57
- const patch = createMemo(() => {
58
- const d = result()?.diff;
59
- const f = selected();
60
- return d && f ? patchOf(d.patch, f.path) : '';
61
- });
62
- const totals = () => {
63
- const fs = files();
64
- return `${fs.length} file${fs.length === 1 ? '' : 's'} · +${fs.reduce((a, f) => a + f.additions, 0)} −${fs.reduce((a, f) => a + f.deletions, 0)}`;
65
- };
66
- useKeys('dialog', (key) => {
67
- if (key.name === ']' || key.name === 'down' || key.name === 'j') {
68
- setCursor((c) => Math.min(files().length - 1, c + 1));
69
- return true;
70
- }
71
- if (key.name === '[' || key.name === 'up' || key.name === 'k') {
72
- setCursor((c) => Math.max(0, c - 1));
73
- return true;
74
- }
75
- return false;
76
- });
77
- return (
78
- <Dialog
79
- size="xlarge"
80
- title={result()?.diff ? `Diff · ${totals()}` : 'Diff'}
81
- onClose={() => dialog.close()}
82
- >
83
- <Show
84
- when={!result.loading}
85
- fallback={<Spinner color={theme.text.muted}>Reading the workspace…</Spinner>}
86
- >
87
- <Show
88
- when={!result()?.error}
89
- fallback={<text fg={theme.text.feedback.error}>{result()?.error ?? ''}</text>}
90
- >
91
- <Show when={files().length > 0} fallback={<text fg={theme.text.muted}>No changes</text>}>
92
- <box flexDirection="row" width="100%" height={22}>
93
- <box width={LIST_WIDTH} flexShrink={0} flexDirection="column" paddingRight={1}>
94
- <For each={files()}>
95
- {(f, i) => (
96
- <box
97
- height={1}
98
- flexShrink={0}
99
- backgroundColor={
100
- i() === cursor() ? theme.background.action.primary.selected : undefined
101
- }
102
- onMouseUp={() => setCursor(i())}
103
- >
104
- <text
105
- fg={
106
- f.status === 'added'
107
- ? theme.diff.added
108
- : f.status === 'deleted'
109
- ? theme.diff.removed
110
- : theme.text.base
111
- }
112
- attributes={i() === cursor() ? TextAttributes.BOLD : undefined}
113
- wrapMode="none"
114
- >
115
- {`${f.status === 'added' ? '+' : f.status === 'deleted' ? '−' : '±'} ${f.path}`}
116
- </text>
117
- </box>
118
- )}
119
- </For>
120
- </box>
121
- <scrollbox flexGrow={1} height="100%">
122
- <diff
123
- diff={patch()}
124
- view="unified"
125
- filetype={FILETYPES[extname(selected()?.path ?? '')] ?? 'text'}
126
- syntaxStyle={syntax}
127
- showLineNumbers
128
- addedBg={theme.diff.addedBg}
129
- removedBg={theme.diff.removedBg}
130
- />
131
- </scrollbox>
132
- </box>
133
- <box height={1} flexShrink={0} marginTop={1}>
134
- <text fg={theme.text.muted} wrapMode="none">
135
- {`${result()?.diff?.truncated ? '… truncated · ' : ''}] / [ next / previous file · esc close`}
136
- </text>
137
- </box>
138
- </Show>
139
- </Show>
140
- </Show>
141
- </Dialog>
142
- );
143
- }
@@ -1,99 +0,0 @@
1
- import { TextAttributes } from '@opentui/core';
2
- import { For, type JSX } from 'solid-js';
3
- import { useTheme } from '../../theme/context.js';
4
- import { Dialog, useDialog } from '../../ui/dialog.js';
5
-
6
- type Section = { title: string; keys: [string, string][] };
7
-
8
- const LEFT: Section[] = [
9
- {
10
- title: 'Global',
11
- keys: [
12
- ['ctrl+n', 'home / new run'],
13
- ['ctrl+o', 'open a run'],
14
- ['ctrl+k', 'command palette'],
15
- ['ctrl+b', 'toggle the sidebar'],
16
- ['ctrl+] · ctrl+p', 'next / previous tab'],
17
- ['ctrl+w', 'close the tab'],
18
- ['?', 'this help (outside inputs)'],
19
- ['ctrl+q', 'quit; runs keep going'],
20
- ],
21
- },
22
- {
23
- title: 'Dialogs',
24
- keys: [
25
- ['esc', 'close'],
26
- ['↑/↓ · enter', 'choose'],
27
- ['typing', 'filters'],
28
- ],
29
- },
30
- ];
31
- const RIGHT: Section[] = [
32
- {
33
- title: 'Session',
34
- keys: [
35
- ['j / k', 'move the cursor'],
36
- ['↑ / ↓ · wheel', 'scroll the view'],
37
- ['enter', 'expand / collapse'],
38
- ['g · G', 'top · follow the end'],
39
- ['d', 'diff (deny while waiting)'],
40
- ['c', 'cancel the run'],
41
- ['r', 'resume a paused run'],
42
- ['a · d · n', 'approve · deny · note'],
43
- ['tab', 'focus the sidebar'],
44
- ],
45
- },
46
- {
47
- title: 'Sidebar',
48
- keys: [
49
- ['j/k', 'select a run'],
50
- ['enter', 'open it'],
51
- ['a · d', 'answer its item'],
52
- ['tab', 'back'],
53
- ],
54
- },
55
- ];
56
-
57
- function Column(props: { sections: Section[] }): JSX.Element {
58
- const theme = useTheme().surface('dialog');
59
- return (
60
- <box flexDirection="column" flexGrow={1} flexBasis={0}>
61
- <For each={props.sections}>
62
- {(s) => (
63
- <box flexDirection="column" flexShrink={0} marginBottom={1}>
64
- <text fg={theme.text.action.primary.selected} attributes={TextAttributes.BOLD}>
65
- {s.title}
66
- </text>
67
- <For each={s.keys}>
68
- {([k, what]) => (
69
- <box height={1} flexShrink={0} flexDirection="row">
70
- <box width={16} flexShrink={0}>
71
- <text fg={theme.text.base} wrapMode="none">
72
- {k}
73
- </text>
74
- </box>
75
- <text fg={theme.text.muted} wrapMode="none">
76
- {what}
77
- </text>
78
- </box>
79
- )}
80
- </For>
81
- </box>
82
- )}
83
- </For>
84
- </box>
85
- );
86
- }
87
-
88
- /** Every key, by context, in two columns. */
89
- export function HelpDialog(): JSX.Element {
90
- const dialog = useDialog();
91
- return (
92
- <Dialog size="large" title="Keys" onClose={() => dialog.close()}>
93
- <box flexDirection="row" width="100%" gap={2}>
94
- <Column sections={LEFT} />
95
- <Column sections={RIGHT} />
96
- </box>
97
- </Dialog>
98
- );
99
- }