@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,66 +0,0 @@
1
- import { TextAttributes } from '@opentui/core';
2
- import { useTerminalDimensions } from '@opentui/solid';
3
- import { For, type JSX } from 'solid-js';
4
- import { useTheme } from '../theme/context.js';
5
- import { stringWidth, takeWidth } from '../ui/marquee.js';
6
-
7
- export interface KeyHint {
8
- key: string;
9
- label: string;
10
- }
11
-
12
- /** `key label · key label` with the keys in bold, as one row of texts. */
13
- export function KeyHints(props: { hints: KeyHint[] }): JSX.Element {
14
- const theme = useTheme();
15
- return (
16
- <box flexDirection="row" flexShrink={0}>
17
- <For each={props.hints}>
18
- {(h, i) => (
19
- <>
20
- {i() > 0 ? <text fg={theme.text.muted}>{' '}</text> : null}
21
- <text fg={theme.text.base} attributes={TextAttributes.BOLD} wrapMode="none">
22
- {h.key}
23
- </text>
24
- <text fg={theme.text.muted} wrapMode="none">{` ${h.label}`}</text>
25
- </>
26
- )}
27
- </For>
28
- </box>
29
- );
30
- }
31
-
32
- /** The last line: status on the left, key hints on the right; the left side yields when space is short. */
33
- export function Footer(props: { left: string; hints?: KeyHint[]; right?: string }): JSX.Element {
34
- const theme = useTheme();
35
- const dimensions = useTerminalDimensions();
36
- const rightWidth = () =>
37
- props.right
38
- ? stringWidth(props.right)
39
- : (props.hints ?? []).reduce((w, h) => w + stringWidth(h.key) + stringWidth(h.label) + 3, 0);
40
- const left = () => {
41
- const room = dimensions().width - 4 - rightWidth();
42
- return stringWidth(props.left) > room ? takeWidth(props.left, Math.max(0, room)) : props.left;
43
- };
44
- return (
45
- <box
46
- height={1}
47
- flexShrink={0}
48
- flexDirection="row"
49
- paddingLeft={2}
50
- paddingRight={2}
51
- width="100%"
52
- >
53
- <text fg={theme.text.muted} flexShrink={1} wrapMode="none">
54
- {left()}
55
- </text>
56
- <box flexGrow={1} flexShrink={0} width={2} />
57
- {props.right ? (
58
- <text fg={theme.text.muted} flexShrink={0} wrapMode="none">
59
- {props.right}
60
- </text>
61
- ) : (
62
- <KeyHints hints={props.hints ?? []} />
63
- )}
64
- </box>
65
- );
66
- }
@@ -1,29 +0,0 @@
1
- import { RGBA } from '@opentui/core';
2
- import { useTerminalDimensions } from '@opentui/solid';
3
- import type { JSX } from 'solid-js';
4
- import { useTheme } from '../theme/context.js';
5
-
6
- /** Blends `overlay` over `base` by `alpha`. */
7
- export function tint(base: RGBA, overlay: RGBA, alpha: number): RGBA {
8
- const r = base.r + (overlay.r - base.r) * alpha;
9
- const g = base.g + (overlay.g - base.g) * alpha;
10
- const b = base.b + (overlay.b - base.b) * alpha;
11
- return RGBA.fromInts(Math.round(r * 255), Math.round(g * 255), Math.round(b * 255));
12
- }
13
-
14
- /** The wordmark in two tones: `shiba` muted, `ox` in the brand orange. Smaller under 60 columns, gone under 20 rows. */
15
- export function Logo(): JSX.Element {
16
- const theme = useTheme();
17
- const dimensions = useTerminalDimensions();
18
- const font = () => (dimensions().width < 60 ? 'tiny' : 'block');
19
- return (
20
- <box flexShrink={0} flexDirection="row">
21
- {dimensions().height < 20 ? null : (
22
- <>
23
- <ascii_font text="shiba" font={font()} color={theme.text.muted} selectable={false} />
24
- <ascii_font text="ox" font={font()} color={theme.syntax.heading} selectable={false} />
25
- </>
26
- )}
27
- </box>
28
- );
29
- }
@@ -1,44 +0,0 @@
1
- import { TextAttributes } from '@opentui/core';
2
- import { For, type JSX } from 'solid-js';
3
- import { SUGGESTIONS, type Suggestion } from '../../model/prompt-commands.js';
4
- import { useTheme } from '../../theme/context.js';
5
-
6
- export type { Suggestion } from '../../model/prompt-commands.js';
7
-
8
- /** The suggestion list above the prompt: up to ten rows, the selected one highlighted. */
9
- export function Autocomplete(props: { items: Suggestion[]; selected: number }): JSX.Element {
10
- const theme = useTheme();
11
- return (
12
- <box
13
- flexDirection="column"
14
- backgroundColor={theme.background.raised.base}
15
- paddingLeft={1}
16
- paddingRight={1}
17
- >
18
- <For each={props.items.slice(0, SUGGESTIONS)}>
19
- {(item, i) => (
20
- <box
21
- height={1}
22
- flexShrink={0}
23
- flexDirection="row"
24
- backgroundColor={
25
- i() === props.selected ? theme.background.action.primary.selected : undefined
26
- }
27
- >
28
- <text
29
- fg={i() === props.selected ? theme.text.action.primary.selected : theme.text.base}
30
- attributes={i() === props.selected ? TextAttributes.BOLD : undefined}
31
- wrapMode="none"
32
- flexShrink={0}
33
- >
34
- {item.label}
35
- </text>
36
- <text fg={theme.text.muted} wrapMode="none" flexShrink={1}>
37
- {item.hint ? ` ${item.hint}` : ''}
38
- </text>
39
- </box>
40
- )}
41
- </For>
42
- </box>
43
- );
44
- }
@@ -1,213 +0,0 @@
1
- import type { InputRenderable } from '@opentui/core';
2
- import { createEffect, createMemo, createSignal, type JSX, onCleanup, Show } from 'solid-js';
3
- import { useKeys } from '../../context/keys.js';
4
- import {
5
- completeCommand,
6
- type PromptCommand,
7
- type PromptCommandSpec,
8
- parsePromptCommand,
9
- } from '../../model/prompt-commands.js';
10
- import { useMotion } from '../../motion/config.js';
11
- import { useTheme } from '../../theme/context.js';
12
- import { Autocomplete } from './autocomplete.js';
13
-
14
- export interface PromptRef {
15
- focus(): void;
16
- set(text: string): void;
17
- current(): string;
18
- clear(): void;
19
- }
20
-
21
- const PLACEHOLDER_MS = 4000;
22
-
23
- /**
24
- * The input of the home screen: `/` commands autocomplete (↑/↓ choose, tab/enter insert), enter
25
- * with a command calls onCommand, enter with text calls onSubmit; ctrl+c clears the text.
26
- */
27
- export function Prompt(props: {
28
- ref?: (r: PromptRef) => void;
29
- placeholders: string[];
30
- disabled?: boolean;
31
- /** The `/` commands this prompt offers. */
32
- commands: PromptCommandSpec[];
33
- /** Only the input row and the footer: the caller draws the box around it. */
34
- bare?: boolean;
35
- onSubmit(text: string): void;
36
- onCommand(cmd: PromptCommand): void;
37
- /** The text changed (the screen clears its notices). */
38
- onInput?(text: string): void;
39
- /** A free-text command was entered without its value: the screen shows what to type. */
40
- onNeedValue?(cmd: string): void;
41
- /** A line drawn inside the box under the input (the run context). */
42
- footer?: JSX.Element;
43
- }): JSX.Element {
44
- const theme = useTheme();
45
- const motion = useMotion();
46
- let input: InputRenderable | undefined;
47
- const [text, setText] = createSignal('');
48
- const [selected, setSelected] = createSignal(0);
49
- const [placeholder, setPlaceholder] = createSignal(0);
50
- const suggestions = createMemo(() => completeCommand(text(), { commands: props.commands }));
51
- createEffect(() => {
52
- suggestions();
53
- setSelected(0);
54
- });
55
- createEffect(() => {
56
- if (!motion() || props.placeholders.length < 2) return;
57
- const t = setInterval(
58
- () => setPlaceholder((i) => (i + 1) % props.placeholders.length),
59
- PLACEHOLDER_MS,
60
- );
61
- onCleanup(() => clearInterval(t));
62
- });
63
-
64
- const set = (value: string) => {
65
- if (input && !input.isDestroyed) input.value = value;
66
- setText(value);
67
- };
68
- const api: PromptRef = {
69
- // the home may already be gone when a closed dialog hands focus back
70
- focus: () => {
71
- if (input && !input.isDestroyed) input.focus();
72
- },
73
- set,
74
- current: text,
75
- clear: () => set(''),
76
- };
77
- props.ref?.(api);
78
-
79
- const accept = () => {
80
- const s = suggestions()[selected()];
81
- if (!s) return;
82
- set(s.insert);
83
- };
84
- const submit = () => {
85
- if (props.disabled) return;
86
- const value = text().trim();
87
- if (!value) return;
88
- if (!value.startsWith('/')) return props.onSubmit(value);
89
- const s = suggestions()[selected()];
90
- const parsed = parsePromptCommand(value, props.commands);
91
- if (!parsed) {
92
- // only the command name so far: enter takes the highlighted command
93
- if (s) set(s.insert);
94
- return;
95
- }
96
- const kind = props.commands.find((c) => c.name === parsed.command)?.kind ?? 'action';
97
- if (kind === 'action') {
98
- set('');
99
- props.onCommand(parsed);
100
- return;
101
- }
102
- if (!parsed.arg && !value.endsWith(' ') && text() === value) {
103
- // `/adapter` + enter: step into its values (a list to pick from, or a hint of what to type)
104
- set(`/${parsed.command} `);
105
- if (kind === 'text') props.onNeedValue?.(parsed.command);
106
- return;
107
- }
108
- if (kind === 'choice') {
109
- // no value or a partial one: the highlighted choice is the value
110
- const chosen = s?.insert.startsWith(`/${parsed.command} `)
111
- ? parsePromptCommand(s.insert, props.commands)
112
- : undefined;
113
- const cmd = chosen?.arg ? chosen : parsed;
114
- if (!cmd.arg) return;
115
- set('');
116
- props.onCommand(cmd);
117
- return;
118
- }
119
- if (!parsed.arg) {
120
- props.onNeedValue?.(parsed.command);
121
- return;
122
- }
123
- set('');
124
- props.onCommand(parsed);
125
- };
126
-
127
- useKeys('prompt', (key) => {
128
- if (key.ctrl && key.name === 'c') {
129
- if (!text()) return false;
130
- set('');
131
- return true;
132
- }
133
- if (key.name === '?') return true; // typed into the input, never the help shortcut
134
- if (suggestions().length === 0) return false;
135
- if (key.name === 'up') {
136
- setSelected((i) => (i - 1 + suggestions().length) % suggestions().length);
137
- return true;
138
- }
139
- if (key.name === 'down') {
140
- setSelected((i) => (i + 1) % suggestions().length);
141
- return true;
142
- }
143
- if (key.name === 'tab') {
144
- accept();
145
- return true;
146
- }
147
- return false;
148
- });
149
-
150
- const inner = () => (
151
- <>
152
- <box flexDirection="row" width="100%" height={1}>
153
- <text fg={theme.text.action.primary.selected} flexShrink={0}>
154
- {'› '}
155
- </text>
156
- <input
157
- ref={(r: InputRenderable) => {
158
- input = r;
159
- }}
160
- focused={!props.disabled}
161
- placeholder={props.placeholders[placeholder()] ?? ''}
162
- onInput={(v: string) => {
163
- setText(v);
164
- props.onInput?.(v);
165
- }}
166
- onSubmit={submit}
167
- flexGrow={1}
168
- backgroundColor={theme.background.raised.base}
169
- focusedBackgroundColor={theme.background.raised.base}
170
- textColor={theme.text.base}
171
- placeholderColor={theme.text.muted}
172
- cursorColor={theme.text.action.primary.selected}
173
- />
174
- </box>
175
- <Show when={props.footer}>
176
- <box height={1} flexShrink={0} />
177
- <box height={1} flexShrink={0} flexDirection="row">
178
- {props.footer}
179
- </box>
180
- </Show>
181
- </>
182
- );
183
- return (
184
- <box flexDirection="column" width="100%">
185
- <Show when={suggestions().length > 0}>
186
- <Autocomplete items={suggestions()} selected={selected()} />
187
- </Show>
188
- <Show
189
- when={!props.bare}
190
- fallback={
191
- <box flexDirection="column" width="100%">
192
- {inner()}
193
- </box>
194
- }
195
- >
196
- <box flexDirection="row" width="100%">
197
- <box width={1} flexShrink={0} backgroundColor={theme.text.action.primary.selected} />
198
- <box
199
- flexGrow={1}
200
- flexDirection="column"
201
- paddingLeft={2}
202
- paddingRight={2}
203
- paddingTop={1}
204
- paddingBottom={1}
205
- backgroundColor={theme.background.raised.base}
206
- >
207
- {inner()}
208
- </box>
209
- </box>
210
- </Show>
211
- </box>
212
- );
213
- }
@@ -1,44 +0,0 @@
1
- // Adapted from opencode (MIT) — https://github.com/anomalyco/opencode
2
- import { RGBA } from '@opentui/core';
3
- import { type Accessor, type JSX, Show } from 'solid-js';
4
- import { Spinner } from '../motion/spinner.js';
5
- import { useTheme } from '../theme/context.js';
6
- import { createDelayedPresence } from '../ui/delayed-presence.js';
7
-
8
- const DELAY_MS = 2000;
9
-
10
- /** Covers the page while the daemon has been unreachable for more than two seconds. */
11
- export function Reconnecting(props: { since: Accessor<number | undefined> }): JSX.Element {
12
- const theme = useTheme();
13
- const visible = createDelayedPresence(props.since, DELAY_MS);
14
- return (
15
- <Show when={visible()}>
16
- <box
17
- position="absolute"
18
- zIndex={10_000}
19
- top={0}
20
- right={0}
21
- bottom={0}
22
- left={0}
23
- backgroundColor={RGBA.fromInts(0, 0, 0, 150)}
24
- alignItems="center"
25
- justifyContent="center"
26
- >
27
- <box
28
- width={48}
29
- maxWidth="90%"
30
- flexDirection="column"
31
- backgroundColor={theme.background.raised.base}
32
- paddingTop={1}
33
- paddingBottom={1}
34
- paddingLeft={2}
35
- paddingRight={2}
36
- gap={1}
37
- >
38
- <Spinner color={theme.text.base}>Connection lost…</Spinner>
39
- <text fg={theme.text.muted}>Reconnecting to the daemon automatically.</text>
40
- </box>
41
- </box>
42
- </Show>
43
- );
44
- }
@@ -1,235 +0,0 @@
1
- import { TextAttributes } from '@opentui/core';
2
- import type { InboxItem } from '@wizardingcode/shibaox-daemon';
3
- import { createMemo, createSignal, For, type JSX, Show } from 'solid-js';
4
- import { useConfig } from '../context/config.js';
5
- import { useData } from '../context/data.js';
6
- import { useKeys } from '../context/keys.js';
7
- import { duration, money, shortId } from '../model/format.js';
8
- import { tilde } from '../routes/home.js';
9
- import { pendingFor } from '../routes/session/approval-bar.js';
10
- import { requestText } from '../routes/session/request.js';
11
- import { useTheme } from '../theme/context.js';
12
- import { useDialog } from '../ui/dialog.js';
13
- import { marqueeText } from '../ui/marquee.js';
14
- import { useToast } from '../ui/toast.js';
15
- import { Confirm } from './dialogs/confirm.js';
16
-
17
- function Title(props: { text: string }): JSX.Element {
18
- const theme = useTheme();
19
- return (
20
- <box height={1} flexShrink={0} marginTop={1}>
21
- <text fg={theme.text.base} attributes={TextAttributes.BOLD} wrapMode="none">
22
- {props.text}
23
- </text>
24
- </box>
25
- );
26
- }
27
-
28
- const NODE_MARK: Record<string, string> = {
29
- completed: '✓',
30
- passed: '✓',
31
- running: '·',
32
- waiting: '▲',
33
- failed: '✗',
34
- gate_failed: '✗',
35
- };
36
-
37
- /** The current run at a glance: request, context, nodes as a checklist, files, and what needs you. */
38
- export function Sidebar(props: {
39
- /** The latest run of the tab (its nodes and pending items). */
40
- runId: string;
41
- /** The first run of the tab (its request titles the sidebar). */
42
- rootId?: string;
43
- width: number;
44
- focused: boolean;
45
- onFocusBack: () => void;
46
- }): JSX.Element {
47
- const data = useData();
48
- const config = useConfig();
49
- const theme = useTheme().surface('sidebar');
50
- const dialog = useDialog();
51
- const toast = useToast();
52
- const state = () => data.state.states[props.runId];
53
- const root = () => data.state.states[props.rootId ?? props.runId];
54
- const summary = () => data.state.runs.find((r) => r.runId === props.runId);
55
- const spent = () =>
56
- data
57
- .threadOf(props.rootId ?? props.runId)
58
- .reduce((n, id) => n + (data.state.states[id]?.spentUsd ?? 0), 0);
59
- const cards = data.timeline(props.runId);
60
- const inner = () => Math.max(8, props.width - 2);
61
-
62
- const nodes = createMemo(() => {
63
- const wf = state()?.workflowSnapshot;
64
- const st = state()?.nodes ?? {};
65
- const ids = wf ? Object.keys(wf.nodes) : Object.keys(st);
66
- return ids.map((id) => ({ id, status: st[id]?.status ?? 'pending' }));
67
- });
68
- const done = () =>
69
- nodes().filter((n) => n.status === 'completed' || n.status === 'passed').length;
70
- const files = createMemo(() => {
71
- const out = new Set<string>();
72
- for (const c of cards()) {
73
- if (c.kind === 'node') for (const b of c.blocks) if (b.kind === 'file') out.add(b.path);
74
- if (c.kind === 'summary') for (const f of c.files) out.add(f);
75
- }
76
- return [...out];
77
- });
78
- const elapsed = () => {
79
- const created = summary()?.createdAt;
80
- if (!created) return undefined;
81
- const from = Date.parse(created);
82
- const terminal = ['completed', 'failed', 'cancelled'].includes(summary()?.status ?? '');
83
- const to = terminal ? Date.parse(summary()?.updatedAt ?? created) : Date.now();
84
- return Number.isFinite(from) && Number.isFinite(to)
85
- ? duration(Math.max(0, to - from))
86
- : undefined;
87
- };
88
- const items = createMemo(() => pendingFor(data.state.inbox, props.runId));
89
- const [cursor, setCursor] = createSignal(0);
90
- const selectedItem = () => items()[Math.min(cursor(), Math.max(0, items().length - 1))];
91
-
92
- /** Answers like the approval bar: a command approval asks y first. */
93
- const answer = (item: InboxItem, approved: boolean) => {
94
- if (approved && item.kind === 'approval') {
95
- dialog.open(() => (
96
- <Confirm
97
- message={`Approve ${item.prompt}?`}
98
- onYes={() => {
99
- dialog.close();
100
- void data.actions.answer(item.id, true);
101
- }}
102
- onNo={() => dialog.close()}
103
- />
104
- ));
105
- return;
106
- }
107
- void data.actions.answer(item.id, approved);
108
- };
109
-
110
- useKeys('pane', (key) => {
111
- if (!props.focused || key.ctrl || key.meta) return false;
112
- switch (key.name) {
113
- case 'j':
114
- case 'down':
115
- if (cursor() < items().length - 1) setCursor((c) => c + 1);
116
- return true;
117
- case 'k':
118
- case 'up':
119
- if (cursor() > 0) setCursor((c) => c - 1);
120
- return true;
121
- case 'a':
122
- case 'd': {
123
- const item = selectedItem();
124
- if (item) answer(item, key.name === 'a');
125
- else toast.show({ message: 'Nothing waiting', variant: 'info' });
126
- return true;
127
- }
128
- default:
129
- return false;
130
- }
131
- });
132
-
133
- return (
134
- <box
135
- width={props.width}
136
- flexShrink={0}
137
- height="100%"
138
- flexDirection="column"
139
- backgroundColor={theme.background.base}
140
- paddingLeft={2}
141
- paddingRight={1}
142
- paddingTop={1}
143
- >
144
- <text fg={theme.text.base} attributes={TextAttributes.BOLD} wrapMode="word">
145
- {marqueeText(requestText(root()?.input) || (summary()?.workflow ?? ''), inner() * 3, 0)}
146
- </text>
147
- <Title text="Context" />
148
- <text
149
- fg={theme.text.muted}
150
- wrapMode="none"
151
- >{`${money(spent() || (summary()?.spentUsd ?? 0))} spent`}</text>
152
- <text
153
- fg={theme.text.muted}
154
- wrapMode="none"
155
- >{`${done()} of ${nodes().length} nodes done`}</text>
156
- <Show when={elapsed()}>
157
- <text fg={theme.text.muted} wrapMode="none">
158
- {elapsed() ?? ''}
159
- </text>
160
- </Show>
161
- <Title text="Nodes" />
162
- <For each={nodes()}>
163
- {(n) => (
164
- <box height={1} flexShrink={0}>
165
- <text
166
- fg={
167
- n.status === 'running'
168
- ? theme.text.action.primary.selected
169
- : n.status === 'failed' || n.status === 'gate_failed'
170
- ? theme.text.feedback.error
171
- : n.status === 'pending'
172
- ? theme.text.muted
173
- : theme.text.base
174
- }
175
- wrapMode="none"
176
- >
177
- {`[${NODE_MARK[n.status] ?? ' '}] ${n.id}`}
178
- </text>
179
- </box>
180
- )}
181
- </For>
182
- <Show when={files().length > 0}>
183
- <Title text="Files" />
184
- <For each={files()}>
185
- {(f) => (
186
- <box height={1} flexShrink={0}>
187
- <text fg={theme.text.base} wrapMode="none">
188
- {marqueeText(`± ${f}`, inner(), 0)}
189
- </text>
190
- </box>
191
- )}
192
- </For>
193
- </Show>
194
- <Show when={items().length > 0}>
195
- <Title text={`Needs you (${items().length})`} />
196
- <For each={items()}>
197
- {(i) => (
198
- <box
199
- height={1}
200
- flexShrink={0}
201
- backgroundColor={
202
- props.focused && selectedItem()?.id === i.id
203
- ? theme.background.action.primary.selected
204
- : undefined
205
- }
206
- >
207
- <text fg={theme.text.feedback.warning} wrapMode="none">
208
- {marqueeText(`▲ ${i.prompt}`, inner(), 0)}
209
- </text>
210
- </box>
211
- )}
212
- </For>
213
- </Show>
214
- <box flexGrow={1} />
215
- <Show when={props.focused}>
216
- <box height={1} flexShrink={0}>
217
- <text fg={theme.text.muted} wrapMode="none">
218
- {items().length > 0 ? 'j/k select · a/d answer · tab back' : 'tab back'}
219
- </text>
220
- </box>
221
- </Show>
222
- <box height={1} flexShrink={0}>
223
- <text fg={theme.text.muted} wrapMode="none">
224
- {marqueeText(tilde(state()?.project ?? state()?.workspace ?? config.cwd), inner(), 0)}
225
- </text>
226
- </box>
227
- <box height={1} flexShrink={0} marginBottom={1}>
228
- <text fg={theme.text.muted} wrapMode="none">
229
- <span style={{ fg: theme.text.feedback.success }}>● </span>
230
- {`shibaox ${config.version} · ${shortId(props.runId)}`}
231
- </text>
232
- </box>
233
- </box>
234
- );
235
- }