@wizardingcode/shibaox-tui 0.1.3 → 0.1.5
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/LICENSE +216 -0
- package/dist/main.js +8188 -0
- package/dist/main.js.map +67 -0
- package/package.json +9 -12
- package/src/app.tsx +0 -300
- package/src/component/dialogs/confirm.tsx +0 -29
- package/src/component/dialogs/diff.tsx +0 -143
- package/src/component/dialogs/help.tsx +0 -99
- package/src/component/dialogs/keys.tsx +0 -201
- package/src/component/dialogs/palette.tsx +0 -95
- package/src/component/dialogs/runs.tsx +0 -115
- package/src/component/dialogs/tiers.tsx +0 -240
- package/src/component/footer.tsx +0 -66
- package/src/component/logo.tsx +0 -29
- package/src/component/prompt/autocomplete.tsx +0 -44
- package/src/component/prompt/index.tsx +0 -213
- package/src/component/reconnecting.tsx +0 -44
- package/src/component/sidebar.tsx +0 -235
- package/src/component/tabs.tsx +0 -116
- package/src/component/too-small.tsx +0 -15
- package/src/context/client.tsx +0 -54
- package/src/context/commands.tsx +0 -44
- package/src/context/config.tsx +0 -22
- package/src/context/data-state.ts +0 -41
- package/src/context/data.tsx +0 -308
- package/src/context/exit.tsx +0 -13
- package/src/context/keys.tsx +0 -70
- package/src/context/poller.ts +0 -352
- package/src/context/prefs.tsx +0 -54
- package/src/context/route.tsx +0 -23
- package/src/main.tsx +0 -55
- package/src/model/format.ts +0 -38
- package/src/model/prompt-commands.ts +0 -315
- package/src/model/status.ts +0 -55
- package/src/model/stream.ts +0 -464
- package/src/motion/animation.ts +0 -235
- package/src/motion/config.tsx +0 -15
- package/src/motion/fade-in-text.tsx +0 -93
- package/src/motion/masked-text.ts +0 -62
- package/src/motion/one-cell-motion.ts +0 -100
- package/src/motion/pulse.ts +0 -38
- package/src/motion/shimmer-text.tsx +0 -78
- package/src/motion/spinner-renderable.ts +0 -171
- package/src/motion/spinner.tsx +0 -100
- package/src/motion/subcell.ts +0 -40
- package/src/routes/home.tsx +0 -365
- package/src/routes/session/approval-bar.tsx +0 -136
- package/src/routes/session/cards.tsx +0 -495
- package/src/routes/session/context-line.tsx +0 -129
- package/src/routes/session/continue.tsx +0 -104
- package/src/routes/session/index.tsx +0 -340
- package/src/routes/session/request.tsx +0 -87
- package/src/routes/session/timeline.tsx +0 -313
- package/src/testing/fake-client.ts +0 -249
- package/src/theme/context.tsx +0 -44
- package/src/theme/resolve.ts +0 -196
- package/src/theme/shibaox.json +0 -159
- package/src/ui/border.ts +0 -20
- package/src/ui/delayed-presence.ts +0 -23
- package/src/ui/dialog.tsx +0 -123
- package/src/ui/layout.ts +0 -23
- package/src/ui/marquee.ts +0 -75
- package/src/ui/pane-resize.ts +0 -77
- package/src/ui/toast.tsx +0 -203
- package/tsconfig.json +0 -19
package/package.json
CHANGED
|
@@ -1,18 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wizardingcode/shibaox-tui",
|
|
3
|
-
"version": "0.1.
|
|
4
|
-
"license": "
|
|
3
|
+
"version": "0.1.5",
|
|
4
|
+
"license": "Apache-2.0",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"files": [
|
|
7
|
-
"
|
|
7
|
+
"LICENSE",
|
|
8
8
|
"bunfig.toml",
|
|
9
|
-
"
|
|
9
|
+
"dist"
|
|
10
10
|
],
|
|
11
11
|
"exports": {
|
|
12
|
-
".":
|
|
13
|
-
"types": "./src/app.tsx",
|
|
14
|
-
"import": "./src/app.tsx"
|
|
15
|
-
},
|
|
12
|
+
".": "./dist/main.js",
|
|
16
13
|
"./package.json": "./package.json"
|
|
17
14
|
},
|
|
18
15
|
"dependencies": {
|
|
@@ -20,9 +17,9 @@
|
|
|
20
17
|
"@opentui/solid": "0.5.12",
|
|
21
18
|
"fuzzysort": "^4.0.2",
|
|
22
19
|
"solid-js": "1.9.12",
|
|
23
|
-
"@wizardingcode/shibaox-
|
|
24
|
-
"@wizardingcode/shibaox-
|
|
25
|
-
"@wizardingcode/shibaox-schemas": "0.1.
|
|
20
|
+
"@wizardingcode/shibaox-daemon": "0.1.5",
|
|
21
|
+
"@wizardingcode/shibaox-core": "0.1.5",
|
|
22
|
+
"@wizardingcode/shibaox-schemas": "0.1.5"
|
|
26
23
|
},
|
|
27
24
|
"devDependencies": {
|
|
28
25
|
"@types/bun": "^1.3.0",
|
|
@@ -36,7 +33,7 @@
|
|
|
36
33
|
"url": "git+https://github.com/WizardingCode-io/shibaox.git"
|
|
37
34
|
},
|
|
38
35
|
"scripts": {
|
|
39
|
-
"build": "tsc -p tsconfig.json --noEmit",
|
|
36
|
+
"build": "tsc -p tsconfig.json --noEmit && bun run build.ts",
|
|
40
37
|
"test": "vitest run && bun test",
|
|
41
38
|
"typecheck": "tsc -p tsconfig.json --noEmit"
|
|
42
39
|
}
|
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
|
-
}
|