@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.
- package/dist/main.js +8188 -0
- package/dist/main.js.map +67 -0
- package/package.json +8 -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
|
@@ -1,104 +0,0 @@
|
|
|
1
|
-
import { createEffect, createMemo, createResource, type JSX, on } from 'solid-js';
|
|
2
|
-
import { Prompt, type PromptRef } from '../../component/prompt/index.js';
|
|
3
|
-
import { useClient } from '../../context/client.js';
|
|
4
|
-
import { useCommands } from '../../context/commands.js';
|
|
5
|
-
import { useData } from '../../context/data.js';
|
|
6
|
-
import { useKeys } from '../../context/keys.js';
|
|
7
|
-
import {
|
|
8
|
-
DEFAULT_MODEL,
|
|
9
|
-
isModelRef,
|
|
10
|
-
modelCommand,
|
|
11
|
-
type PromptCommandSpec,
|
|
12
|
-
} from '../../model/prompt-commands.js';
|
|
13
|
-
import { useDialog } from '../../ui/dialog.js';
|
|
14
|
-
import { useToast } from '../../ui/toast.js';
|
|
15
|
-
|
|
16
|
-
const PLACEHOLDERS = ['Continue… "Now add tests for it"', 'Continue… "/diff shows the changes"'];
|
|
17
|
-
|
|
18
|
-
/**
|
|
19
|
-
* The prompt at the bottom of a finished run: the next request runs in the same tab. `/`
|
|
20
|
-
* offers the commands of this screen (the same ones as the palette): `/diff`, `/cancel`,
|
|
21
|
-
* `/help`, `/home`…
|
|
22
|
-
*/
|
|
23
|
-
export function ContinuePrompt(props: {
|
|
24
|
-
/** The tab's root run: `/model` applies to its next turns. */
|
|
25
|
-
runId: string;
|
|
26
|
-
onSubmit(text: string): void;
|
|
27
|
-
onScroll?(lines: number): void;
|
|
28
|
-
footer?: JSX.Element;
|
|
29
|
-
}): JSX.Element {
|
|
30
|
-
const registry = useCommands();
|
|
31
|
-
const dialog = useDialog();
|
|
32
|
-
const client = useClient();
|
|
33
|
-
const data = useData();
|
|
34
|
-
const toast = useToast();
|
|
35
|
-
const [models] = createResource(() => client.models().catch(() => []));
|
|
36
|
-
let prompt: PromptRef | undefined;
|
|
37
|
-
// a dialog opened from here (diff, help, runs) takes the keys; the prompt gets them back after
|
|
38
|
-
createEffect(
|
|
39
|
-
on(
|
|
40
|
-
dialog.depth,
|
|
41
|
-
(d) => {
|
|
42
|
-
if (d === 0) setTimeout(() => prompt?.focus(), 0);
|
|
43
|
-
},
|
|
44
|
-
{ defer: true },
|
|
45
|
-
),
|
|
46
|
-
);
|
|
47
|
-
// the screen's own commands first, then the shell's
|
|
48
|
-
const ORDER = ['diff', 'cancel', 'resume', 'sidebar', 'home', 'runs', 'help', 'close', 'quit'];
|
|
49
|
-
const rank = (id: string) => {
|
|
50
|
-
const i = ORDER.indexOf(id);
|
|
51
|
-
return i < 0 ? ORDER.length : i;
|
|
52
|
-
};
|
|
53
|
-
const commands = createMemo<PromptCommandSpec[]>(() => [
|
|
54
|
-
modelCommand(() => models() ?? []),
|
|
55
|
-
...registry
|
|
56
|
-
.commands()
|
|
57
|
-
.filter((c) => c.when?.() ?? true)
|
|
58
|
-
.sort((a, b) => rank(a.id) - rank(b.id))
|
|
59
|
-
.map((c) => ({ name: c.id, kind: 'action' as const, hint: c.label })),
|
|
60
|
-
]);
|
|
61
|
-
// registered before the prompt mounts: its handler runs after the prompt's, so the arrows
|
|
62
|
-
// scroll the conversation only while no suggestion list is showing
|
|
63
|
-
useKeys('prompt', (key) => {
|
|
64
|
-
const scroll: Record<string, number> = { up: -2, down: 2, pageup: -20, pagedown: 20 };
|
|
65
|
-
const lines = scroll[key.name];
|
|
66
|
-
if (lines === undefined) return false;
|
|
67
|
-
props.onScroll?.(lines);
|
|
68
|
-
return true;
|
|
69
|
-
});
|
|
70
|
-
return (
|
|
71
|
-
<Prompt
|
|
72
|
-
ref={(r) => {
|
|
73
|
-
prompt = r;
|
|
74
|
-
}}
|
|
75
|
-
bare
|
|
76
|
-
disabled={dialog.depth() > 0}
|
|
77
|
-
placeholders={PLACEHOLDERS}
|
|
78
|
-
commands={commands()}
|
|
79
|
-
onSubmit={props.onSubmit}
|
|
80
|
-
onCommand={(cmd) => {
|
|
81
|
-
if (cmd.command === 'model') {
|
|
82
|
-
if (cmd.arg === DEFAULT_MODEL) {
|
|
83
|
-
data.setModel(props.runId, undefined);
|
|
84
|
-
toast.show({ message: 'Next turns follow the org routing', variant: 'info' });
|
|
85
|
-
} else if (!isModelRef(cmd.arg)) {
|
|
86
|
-
toast.show({
|
|
87
|
-
message: 'Model must look like provider/model (see /model for the list)',
|
|
88
|
-
variant: 'error',
|
|
89
|
-
});
|
|
90
|
-
} else {
|
|
91
|
-
data.setModel(props.runId, cmd.arg);
|
|
92
|
-
toast.show({ message: `Next turns run on ${cmd.arg}`, variant: 'info' });
|
|
93
|
-
}
|
|
94
|
-
return;
|
|
95
|
-
}
|
|
96
|
-
registry
|
|
97
|
-
.commands()
|
|
98
|
-
.find((c) => c.id === cmd.command)
|
|
99
|
-
?.run();
|
|
100
|
-
}}
|
|
101
|
-
footer={props.footer}
|
|
102
|
-
/>
|
|
103
|
-
);
|
|
104
|
-
}
|
|
@@ -1,340 +0,0 @@
|
|
|
1
|
-
import { type MouseEvent, TextAttributes } from '@opentui/core';
|
|
2
|
-
import { useTerminalDimensions } from '@opentui/solid';
|
|
3
|
-
import { createEffect, createMemo, createSignal, type JSX, onCleanup, Show } from 'solid-js';
|
|
4
|
-
import { Confirm } from '../../component/dialogs/confirm.js';
|
|
5
|
-
import { DiffDialog } from '../../component/dialogs/diff.js';
|
|
6
|
-
import { type KeyHint, KeyHints } from '../../component/footer.js';
|
|
7
|
-
import { Sidebar } from '../../component/sidebar.js';
|
|
8
|
-
import { useCommands } from '../../context/commands.js';
|
|
9
|
-
import { useData } from '../../context/data.js';
|
|
10
|
-
import { useKeys } from '../../context/keys.js';
|
|
11
|
-
import { usePrefs } from '../../context/prefs.js';
|
|
12
|
-
import { duration, shortId } from '../../model/format.js';
|
|
13
|
-
import { statusOf } from '../../model/status.js';
|
|
14
|
-
import { createAnimatable, spring } from '../../motion/animation.js';
|
|
15
|
-
import { useMotion } from '../../motion/config.js';
|
|
16
|
-
import { ShimmerText } from '../../motion/shimmer-text.js';
|
|
17
|
-
import { useTheme } from '../../theme/context.js';
|
|
18
|
-
import { useDialog } from '../../ui/dialog.js';
|
|
19
|
-
import { clampSidebarWidth, SIDEBAR_WIDTH, sidebarAuto } from '../../ui/layout.js';
|
|
20
|
-
import { createPaneResize } from '../../ui/pane-resize.js';
|
|
21
|
-
import { useToast } from '../../ui/toast.js';
|
|
22
|
-
import { ApprovalBar, pendingFor } from './approval-bar.js';
|
|
23
|
-
import { ContextLine } from './context-line.js';
|
|
24
|
-
import { ContinuePrompt } from './continue.js';
|
|
25
|
-
import { Timeline, type TimelineApi } from './timeline.js';
|
|
26
|
-
|
|
27
|
-
export const SESSION_HINTS: KeyHint[] = [
|
|
28
|
-
{ key: 'enter', label: 'expand' },
|
|
29
|
-
{ key: 'd', label: 'diff' },
|
|
30
|
-
{ key: 'tab', label: 'sidebar' },
|
|
31
|
-
{ key: 'ctrl+n', label: 'home' },
|
|
32
|
-
{ key: '?', label: 'help' },
|
|
33
|
-
];
|
|
34
|
-
export const STREAM_HINTS: KeyHint[] = [
|
|
35
|
-
{ key: 'enter', label: 'expand' },
|
|
36
|
-
{ key: 'd', label: 'diff' },
|
|
37
|
-
{ key: 'q', label: 'quit' },
|
|
38
|
-
];
|
|
39
|
-
|
|
40
|
-
const DOTS = ['·', '·', '·', '·', '·', '·'];
|
|
41
|
-
const DOT_MS = 120;
|
|
42
|
-
|
|
43
|
-
/** The dotted progress of a working run (the reference's `······` under the prompt). */
|
|
44
|
-
function Dots(props: { active: boolean }): JSX.Element {
|
|
45
|
-
const theme = useTheme();
|
|
46
|
-
const motion = useMotion();
|
|
47
|
-
const [head, setHead] = createSignal(0);
|
|
48
|
-
createEffect(() => {
|
|
49
|
-
if (!props.active || !motion()) return;
|
|
50
|
-
const t = setInterval(() => setHead((h) => (h + 1) % (DOTS.length + 2)), DOT_MS);
|
|
51
|
-
onCleanup(() => clearInterval(t));
|
|
52
|
-
});
|
|
53
|
-
const glyphs = () =>
|
|
54
|
-
DOTS.map((_, i) => (props.active && motion() && Math.abs(i - head()) <= 1 ? '▪' : '·')).join(
|
|
55
|
-
'',
|
|
56
|
-
);
|
|
57
|
-
return (
|
|
58
|
-
<text fg={props.active ? theme.text.action.primary.selected : theme.text.muted} wrapMode="none">
|
|
59
|
-
{glyphs()}
|
|
60
|
-
</text>
|
|
61
|
-
);
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
/** One run as a conversation: request, cards, the status box (or the approval bar), and the sidebar. */
|
|
65
|
-
export function SessionFrame(props: { runId: string; single?: boolean }): JSX.Element {
|
|
66
|
-
const data = useData();
|
|
67
|
-
const theme = useTheme();
|
|
68
|
-
const dialog = useDialog();
|
|
69
|
-
const toast = useToast();
|
|
70
|
-
const prefs = usePrefs();
|
|
71
|
-
const motion = useMotion();
|
|
72
|
-
const dimensions = useTerminalDimensions();
|
|
73
|
-
const [now, setNow] = createSignal(Date.now());
|
|
74
|
-
const tick = setInterval(() => setNow(Date.now()), 1000);
|
|
75
|
-
onCleanup(() => clearInterval(tick));
|
|
76
|
-
|
|
77
|
-
// the tab is a thread: the status, the pending items and the prompt belong to its latest run
|
|
78
|
-
const latest = () => {
|
|
79
|
-
const runs = data.threadOf(props.runId);
|
|
80
|
-
return runs[runs.length - 1] ?? props.runId;
|
|
81
|
-
};
|
|
82
|
-
const summary = () => data.state.runs.find((r) => r.runId === latest());
|
|
83
|
-
const state = () => data.state.states[latest()];
|
|
84
|
-
const status = () =>
|
|
85
|
-
data.state.ended[latest()] ?? state()?.status ?? summary()?.status ?? 'queued';
|
|
86
|
-
const look = () => statusOf({ status: status() });
|
|
87
|
-
const working = () => status() === 'running';
|
|
88
|
-
const elapsed = () => {
|
|
89
|
-
const created = summary()?.createdAt;
|
|
90
|
-
const updated = summary()?.updatedAt;
|
|
91
|
-
if (!created) return undefined;
|
|
92
|
-
const from = Date.parse(created);
|
|
93
|
-
const terminal = status() === 'completed' || status() === 'failed' || status() === 'cancelled';
|
|
94
|
-
const to = terminal && updated ? Date.parse(updated) : now();
|
|
95
|
-
return Number.isFinite(from) && Number.isFinite(to)
|
|
96
|
-
? duration(Math.max(0, to - from))
|
|
97
|
-
: undefined;
|
|
98
|
-
};
|
|
99
|
-
const activeNode = () => {
|
|
100
|
-
const nodes = state()?.nodes ?? {};
|
|
101
|
-
return Object.entries(nodes).find(
|
|
102
|
-
([, n]) => n.status === 'running' || n.status === 'waiting',
|
|
103
|
-
)?.[0];
|
|
104
|
-
};
|
|
105
|
-
const color = () => {
|
|
106
|
-
const f = look().feedback;
|
|
107
|
-
return f === 'muted' ? theme.text.muted : theme.text.feedback[f];
|
|
108
|
-
};
|
|
109
|
-
const pending = createMemo(() => pendingFor(data.state.inbox, latest()));
|
|
110
|
-
const finished = () => ['completed', 'failed', 'cancelled'].includes(status());
|
|
111
|
-
let timeline: TimelineApi | undefined;
|
|
112
|
-
|
|
113
|
-
// sidebar: automatic on wide terminals, toggled with ctrl+b, resizable with the mouse
|
|
114
|
-
const area = () => dimensions().width;
|
|
115
|
-
const [openedByKey, setOpenedByKey] = createSignal<boolean | undefined>(undefined);
|
|
116
|
-
const sidebarVisible = createMemo(() => {
|
|
117
|
-
if (props.single) return false;
|
|
118
|
-
if (openedByKey() !== undefined) return openedByKey() as boolean;
|
|
119
|
-
return prefs.data.sidebar !== 'hide' && sidebarAuto(dimensions().width, 0);
|
|
120
|
-
});
|
|
121
|
-
const resize = createPaneResize({
|
|
122
|
-
value: () => prefs.data.sidebarWidth ?? SIDEBAR_WIDTH,
|
|
123
|
-
defaultValue: () => SIDEBAR_WIDTH,
|
|
124
|
-
clamp: (w) => clampSidebarWidth(w, area()),
|
|
125
|
-
fromMouse: (e: MouseEvent) => dimensions().width - e.x - 1,
|
|
126
|
-
contains: (e: MouseEvent, w) =>
|
|
127
|
-
e.x >= dimensions().width - w - 1 && e.x <= dimensions().width - w,
|
|
128
|
-
onCommit: (w) => prefs.update({ sidebarWidth: w }),
|
|
129
|
-
});
|
|
130
|
-
const anim = createAnimatable(
|
|
131
|
-
{ w: sidebarVisible() ? resize.size() : 0 },
|
|
132
|
-
{ transition: spring({ visualDuration: 0.25 }), enabled: motion },
|
|
133
|
-
);
|
|
134
|
-
createEffect(() => anim.animate({ w: sidebarVisible() ? resize.size() : 0 }));
|
|
135
|
-
const sidebarWidth = () => Math.round(anim.value().w);
|
|
136
|
-
const [focus, setFocus] = createSignal<'conversation' | 'sidebar'>('conversation');
|
|
137
|
-
createEffect(() => {
|
|
138
|
-
if (!sidebarVisible()) setFocus('conversation');
|
|
139
|
-
});
|
|
140
|
-
|
|
141
|
-
const toggleSidebar = () => {
|
|
142
|
-
const next = !sidebarVisible();
|
|
143
|
-
setOpenedByKey(next);
|
|
144
|
-
prefs.update({ sidebar: next ? 'auto' : 'hide' });
|
|
145
|
-
};
|
|
146
|
-
const cancel = () =>
|
|
147
|
-
dialog.open(() => (
|
|
148
|
-
<Confirm
|
|
149
|
-
message={`Cancel run ${shortId(latest())}?`}
|
|
150
|
-
onYes={() => {
|
|
151
|
-
dialog.close();
|
|
152
|
-
void data.actions.cancel(latest());
|
|
153
|
-
}}
|
|
154
|
-
onNo={() => dialog.close()}
|
|
155
|
-
/>
|
|
156
|
-
));
|
|
157
|
-
const resume = () => {
|
|
158
|
-
if (status() === 'paused_budget') void data.actions.resume(latest());
|
|
159
|
-
else toast.show({ message: 'Nothing to resume', variant: 'info' });
|
|
160
|
-
};
|
|
161
|
-
const diff = () => dialog.open(() => <DiffDialog runId={latest()} />);
|
|
162
|
-
const unregister = useCommands().register([
|
|
163
|
-
{ id: 'diff', label: 'Diff of the run', keys: 'd', run: diff },
|
|
164
|
-
{
|
|
165
|
-
id: 'sidebar',
|
|
166
|
-
label: 'Toggle sidebar',
|
|
167
|
-
keys: 'ctrl+b',
|
|
168
|
-
run: toggleSidebar,
|
|
169
|
-
when: () => !props.single,
|
|
170
|
-
},
|
|
171
|
-
{ id: 'cancel', label: 'Cancel run', keys: 'c', run: cancel, when: () => !finished() },
|
|
172
|
-
{
|
|
173
|
-
id: 'resume',
|
|
174
|
-
label: 'Resume run',
|
|
175
|
-
keys: 'r',
|
|
176
|
-
run: resume,
|
|
177
|
-
when: () => status() === 'paused_budget',
|
|
178
|
-
},
|
|
179
|
-
]);
|
|
180
|
-
onCleanup(unregister);
|
|
181
|
-
useKeys('global', (key) => {
|
|
182
|
-
if (props.single || !(key.ctrl && key.name === 'b')) return false;
|
|
183
|
-
toggleSidebar();
|
|
184
|
-
return true;
|
|
185
|
-
});
|
|
186
|
-
useKeys('pane', (key) => {
|
|
187
|
-
if (key.ctrl || key.meta) return false;
|
|
188
|
-
if (key.name === 'tab' && sidebarVisible()) {
|
|
189
|
-
setFocus((f) => (f === 'sidebar' ? 'conversation' : 'sidebar'));
|
|
190
|
-
return true;
|
|
191
|
-
}
|
|
192
|
-
if (focus() !== 'conversation') return false;
|
|
193
|
-
if (key.name === 'c') {
|
|
194
|
-
cancel();
|
|
195
|
-
return true;
|
|
196
|
-
}
|
|
197
|
-
if (key.name === 'r') {
|
|
198
|
-
resume();
|
|
199
|
-
return true;
|
|
200
|
-
}
|
|
201
|
-
if (key.name === 'd' && pending().length === 0) {
|
|
202
|
-
diff();
|
|
203
|
-
return true;
|
|
204
|
-
}
|
|
205
|
-
return false;
|
|
206
|
-
});
|
|
207
|
-
const hints = () => {
|
|
208
|
-
const base = props.single ? STREAM_HINTS : SESSION_HINTS;
|
|
209
|
-
if (status() === 'paused_budget') return [{ key: 'r', label: 'resume' }, ...base];
|
|
210
|
-
return base;
|
|
211
|
-
};
|
|
212
|
-
|
|
213
|
-
return (
|
|
214
|
-
<box
|
|
215
|
-
flexDirection="row"
|
|
216
|
-
width="100%"
|
|
217
|
-
height="100%"
|
|
218
|
-
onMouseDrag={resize.onMouseDrag}
|
|
219
|
-
onMouseDragEnd={resize.onMouseDragEnd}
|
|
220
|
-
onMouseUp={resize.onMouseUp}
|
|
221
|
-
>
|
|
222
|
-
<box flexDirection="column" flexGrow={1} height="100%">
|
|
223
|
-
<Timeline
|
|
224
|
-
runId={props.runId}
|
|
225
|
-
focused={focus() === 'conversation'}
|
|
226
|
-
api={(a) => {
|
|
227
|
-
timeline = a;
|
|
228
|
-
}}
|
|
229
|
-
/>
|
|
230
|
-
<box flexDirection="row" width="100%" flexShrink={0} paddingLeft={2} paddingRight={2}>
|
|
231
|
-
<box
|
|
232
|
-
width={1}
|
|
233
|
-
flexShrink={0}
|
|
234
|
-
backgroundColor={
|
|
235
|
-
pending().length > 0
|
|
236
|
-
? theme.text.feedback.warning
|
|
237
|
-
: theme.text.action.primary.selected
|
|
238
|
-
}
|
|
239
|
-
/>
|
|
240
|
-
<box
|
|
241
|
-
flexGrow={1}
|
|
242
|
-
flexDirection="column"
|
|
243
|
-
paddingLeft={2}
|
|
244
|
-
paddingRight={2}
|
|
245
|
-
paddingTop={1}
|
|
246
|
-
paddingBottom={1}
|
|
247
|
-
backgroundColor={theme.background.raised.base}
|
|
248
|
-
>
|
|
249
|
-
<Show
|
|
250
|
-
when={pending().length > 0}
|
|
251
|
-
fallback={
|
|
252
|
-
<Show
|
|
253
|
-
when={!finished() || props.single}
|
|
254
|
-
fallback={
|
|
255
|
-
<ContinuePrompt
|
|
256
|
-
runId={props.runId}
|
|
257
|
-
onSubmit={(t) => void data.continueRun(props.runId, t)}
|
|
258
|
-
onScroll={(n) => timeline?.scrollBy(n)}
|
|
259
|
-
footer={<ContextLine runId={props.runId} elapsed={elapsed()} />}
|
|
260
|
-
/>
|
|
261
|
-
}
|
|
262
|
-
>
|
|
263
|
-
<box height={1} flexShrink={0} flexDirection="row">
|
|
264
|
-
<Show
|
|
265
|
-
when={working()}
|
|
266
|
-
fallback={
|
|
267
|
-
<text
|
|
268
|
-
fg={color()}
|
|
269
|
-
attributes={TextAttributes.BOLD}
|
|
270
|
-
wrapMode="none"
|
|
271
|
-
>{`${look().symbol} ${look().word}`}</text>
|
|
272
|
-
}
|
|
273
|
-
>
|
|
274
|
-
<ShimmerText
|
|
275
|
-
fg={color()}
|
|
276
|
-
shimmer={theme.text.base}
|
|
277
|
-
attributes={TextAttributes.BOLD}
|
|
278
|
-
wrapMode="none"
|
|
279
|
-
>
|
|
280
|
-
{`${look().symbol} ${look().word}`}
|
|
281
|
-
</ShimmerText>
|
|
282
|
-
</Show>
|
|
283
|
-
<text fg={theme.text.muted} wrapMode="none">
|
|
284
|
-
{activeNode() ? ` · ${activeNode()}` : ''}
|
|
285
|
-
</text>
|
|
286
|
-
</box>
|
|
287
|
-
<box height={1} flexShrink={0} flexDirection="row">
|
|
288
|
-
<ContextLine runId={props.runId} elapsed={elapsed()} />
|
|
289
|
-
</box>
|
|
290
|
-
</Show>
|
|
291
|
-
}
|
|
292
|
-
>
|
|
293
|
-
<ApprovalBar runId={latest()} />
|
|
294
|
-
</Show>
|
|
295
|
-
</box>
|
|
296
|
-
</box>
|
|
297
|
-
<box
|
|
298
|
-
height={1}
|
|
299
|
-
flexShrink={0}
|
|
300
|
-
flexDirection="row"
|
|
301
|
-
paddingLeft={2}
|
|
302
|
-
paddingRight={2}
|
|
303
|
-
width="100%"
|
|
304
|
-
>
|
|
305
|
-
<Dots active={working()} />
|
|
306
|
-
<text fg={theme.text.muted} wrapMode="none">
|
|
307
|
-
{' '}
|
|
308
|
-
</text>
|
|
309
|
-
<Show when={working() || status() === 'waiting_human' || status() === 'waiting_approval'}>
|
|
310
|
-
<KeyHints hints={[{ key: 'c', label: 'cancel' }]} />
|
|
311
|
-
</Show>
|
|
312
|
-
<box flexGrow={1} flexShrink={0} width={2} />
|
|
313
|
-
<KeyHints hints={hints()} />
|
|
314
|
-
</box>
|
|
315
|
-
</box>
|
|
316
|
-
<Show when={sidebarWidth() > 0}>
|
|
317
|
-
<box
|
|
318
|
-
width={1}
|
|
319
|
-
flexShrink={0}
|
|
320
|
-
height="100%"
|
|
321
|
-
backgroundColor={
|
|
322
|
-
resize.hovered() || resize.resizing()
|
|
323
|
-
? theme.background.raised.high
|
|
324
|
-
: theme.background.base
|
|
325
|
-
}
|
|
326
|
-
onMouseOver={resize.onMouseOver}
|
|
327
|
-
onMouseOut={resize.onMouseOut}
|
|
328
|
-
onMouseDown={resize.onMouseDown}
|
|
329
|
-
/>
|
|
330
|
-
<Sidebar
|
|
331
|
-
runId={latest()}
|
|
332
|
-
rootId={props.runId}
|
|
333
|
-
width={sidebarWidth()}
|
|
334
|
-
focused={focus() === 'sidebar'}
|
|
335
|
-
onFocusBack={() => setFocus('conversation')}
|
|
336
|
-
/>
|
|
337
|
-
</Show>
|
|
338
|
-
</box>
|
|
339
|
-
);
|
|
340
|
-
}
|
|
@@ -1,87 +0,0 @@
|
|
|
1
|
-
import { TextAttributes } from '@opentui/core';
|
|
2
|
-
import { type JSX, Show } from 'solid-js';
|
|
3
|
-
import { useData } from '../../context/data.js';
|
|
4
|
-
import { shortId } from '../../model/format.js';
|
|
5
|
-
import { isEventRun } from '../../model/stream.js';
|
|
6
|
-
import { useTheme } from '../../theme/context.js';
|
|
7
|
-
|
|
8
|
-
/** The text a run was asked for, from its state (`input.spec` for daemon and inline runs). */
|
|
9
|
-
export function requestText(input: Record<string, unknown> | undefined): string {
|
|
10
|
-
if (!input) return '';
|
|
11
|
-
for (const key of ['spec', 'input', 'text', 'prompt']) {
|
|
12
|
-
const v = input[key];
|
|
13
|
-
if (typeof v === 'string' && v.trim()) return v.trim();
|
|
14
|
-
}
|
|
15
|
-
try {
|
|
16
|
-
return JSON.stringify(input);
|
|
17
|
-
} catch {
|
|
18
|
-
return '';
|
|
19
|
-
}
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
/** The run's request at the top of the conversation: a block with the accent bar on the left. */
|
|
23
|
-
export function RequestBlock(props: { runId: string }): JSX.Element {
|
|
24
|
-
const data = useData();
|
|
25
|
-
const theme = useTheme();
|
|
26
|
-
const state = () => data.state.states[props.runId];
|
|
27
|
-
const summary = () => data.state.runs.find((r) => r.runId === props.runId);
|
|
28
|
-
const meta = () => {
|
|
29
|
-
const parts = [
|
|
30
|
-
summary()?.workflow ?? state()?.workflow ?? '',
|
|
31
|
-
state()?.adapter ?? '',
|
|
32
|
-
shortId(props.runId),
|
|
33
|
-
];
|
|
34
|
-
return parts.filter(Boolean).join(' · ');
|
|
35
|
-
};
|
|
36
|
-
const text = () => requestText(state()?.input);
|
|
37
|
-
const event = () => isEventRun(state());
|
|
38
|
-
const child = () => state()?.parentRunId !== undefined;
|
|
39
|
-
const workflow = () => summary()?.workflow ?? state()?.workflow ?? '';
|
|
40
|
-
return (
|
|
41
|
-
<Show
|
|
42
|
-
when={!event()}
|
|
43
|
-
fallback={
|
|
44
|
-
// a message from shibaox itself (a dispatched run ended): one quiet line, no block
|
|
45
|
-
<box width="100%" flexShrink={0} marginBottom={1} paddingLeft={1}>
|
|
46
|
-
<text fg={theme.text.muted} wrapMode="word">
|
|
47
|
-
<span style={{ fg: theme.text.action.primary.selected }}>{'↳ '}</span>
|
|
48
|
-
{text()}
|
|
49
|
-
</text>
|
|
50
|
-
</box>
|
|
51
|
-
}
|
|
52
|
-
>
|
|
53
|
-
<box flexDirection="row" width="100%" flexShrink={0} marginBottom={1}>
|
|
54
|
-
<box width={1} flexShrink={0} backgroundColor={theme.text.action.primary.selected} />
|
|
55
|
-
<box
|
|
56
|
-
flexGrow={1}
|
|
57
|
-
flexDirection="column"
|
|
58
|
-
paddingLeft={2}
|
|
59
|
-
paddingRight={2}
|
|
60
|
-
paddingTop={1}
|
|
61
|
-
paddingBottom={1}
|
|
62
|
-
backgroundColor={theme.background.raised.base}
|
|
63
|
-
>
|
|
64
|
-
<Show when={child()}>
|
|
65
|
-
<box height={1} flexShrink={0}>
|
|
66
|
-
<text
|
|
67
|
-
fg={theme.text.action.primary.selected}
|
|
68
|
-
attributes={TextAttributes.BOLD}
|
|
69
|
-
wrapMode="none"
|
|
70
|
-
>
|
|
71
|
-
{`→ ${workflow()}`}
|
|
72
|
-
</text>
|
|
73
|
-
</box>
|
|
74
|
-
</Show>
|
|
75
|
-
<text fg={theme.text.base} attributes={TextAttributes.BOLD} wrapMode="word">
|
|
76
|
-
{text() || '…'}
|
|
77
|
-
</text>
|
|
78
|
-
<box height={1} flexShrink={0}>
|
|
79
|
-
<text fg={theme.text.muted} wrapMode="none">
|
|
80
|
-
{meta()}
|
|
81
|
-
</text>
|
|
82
|
-
</box>
|
|
83
|
-
</box>
|
|
84
|
-
</box>
|
|
85
|
-
</Show>
|
|
86
|
-
);
|
|
87
|
-
}
|