@wizardingcode/shibaox-tui 0.1.0

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 (61) hide show
  1. package/bunfig.toml +5 -0
  2. package/package.json +42 -0
  3. package/src/app.tsx +300 -0
  4. package/src/component/dialogs/confirm.tsx +29 -0
  5. package/src/component/dialogs/diff.tsx +143 -0
  6. package/src/component/dialogs/help.tsx +99 -0
  7. package/src/component/dialogs/keys.tsx +201 -0
  8. package/src/component/dialogs/palette.tsx +95 -0
  9. package/src/component/dialogs/runs.tsx +115 -0
  10. package/src/component/dialogs/tiers.tsx +240 -0
  11. package/src/component/footer.tsx +66 -0
  12. package/src/component/logo.tsx +29 -0
  13. package/src/component/prompt/autocomplete.tsx +44 -0
  14. package/src/component/prompt/index.tsx +213 -0
  15. package/src/component/reconnecting.tsx +44 -0
  16. package/src/component/sidebar.tsx +235 -0
  17. package/src/component/tabs.tsx +116 -0
  18. package/src/component/too-small.tsx +15 -0
  19. package/src/context/client.tsx +54 -0
  20. package/src/context/commands.tsx +44 -0
  21. package/src/context/config.tsx +22 -0
  22. package/src/context/data-state.ts +41 -0
  23. package/src/context/data.tsx +308 -0
  24. package/src/context/exit.tsx +13 -0
  25. package/src/context/keys.tsx +70 -0
  26. package/src/context/poller.ts +352 -0
  27. package/src/context/prefs.tsx +54 -0
  28. package/src/context/route.tsx +23 -0
  29. package/src/main.tsx +55 -0
  30. package/src/model/format.ts +38 -0
  31. package/src/model/prompt-commands.ts +315 -0
  32. package/src/model/status.ts +55 -0
  33. package/src/model/stream.ts +464 -0
  34. package/src/motion/animation.ts +235 -0
  35. package/src/motion/config.tsx +15 -0
  36. package/src/motion/fade-in-text.tsx +93 -0
  37. package/src/motion/masked-text.ts +62 -0
  38. package/src/motion/one-cell-motion.ts +100 -0
  39. package/src/motion/pulse.ts +38 -0
  40. package/src/motion/shimmer-text.tsx +78 -0
  41. package/src/motion/spinner.tsx +101 -0
  42. package/src/motion/subcell.ts +40 -0
  43. package/src/routes/home.tsx +365 -0
  44. package/src/routes/session/approval-bar.tsx +136 -0
  45. package/src/routes/session/cards.tsx +495 -0
  46. package/src/routes/session/context-line.tsx +129 -0
  47. package/src/routes/session/continue.tsx +104 -0
  48. package/src/routes/session/index.tsx +340 -0
  49. package/src/routes/session/request.tsx +87 -0
  50. package/src/routes/session/timeline.tsx +313 -0
  51. package/src/testing/fake-client.ts +249 -0
  52. package/src/theme/context.tsx +44 -0
  53. package/src/theme/resolve.ts +196 -0
  54. package/src/theme/shibaox.json +159 -0
  55. package/src/ui/border.ts +20 -0
  56. package/src/ui/delayed-presence.ts +23 -0
  57. package/src/ui/dialog.tsx +123 -0
  58. package/src/ui/layout.ts +23 -0
  59. package/src/ui/marquee.ts +75 -0
  60. package/src/ui/pane-resize.ts +77 -0
  61. package/src/ui/toast.tsx +203 -0
@@ -0,0 +1,203 @@
1
+ // Adapted from opencode (MIT) — https://github.com/anomalyco/opencode
2
+ import { TextAttributes } from '@opentui/core';
3
+ import { useTerminalDimensions } from '@opentui/solid';
4
+ import {
5
+ type Accessor,
6
+ createContext,
7
+ createSignal,
8
+ type JSX,
9
+ onCleanup,
10
+ type ParentProps,
11
+ Show,
12
+ useContext,
13
+ } from 'solid-js';
14
+ import { useTheme } from '../theme/context.js';
15
+ import type { Feedback } from '../theme/resolve.js';
16
+ import { SplitBorder } from './border.js';
17
+
18
+ export interface ToastOptions {
19
+ title?: string;
20
+ message: string;
21
+ variant: Feedback;
22
+ duration: number;
23
+ action?: { label: string; run: () => void };
24
+ }
25
+ export type ToastInput = Omit<ToastOptions, 'duration'> & { duration?: number };
26
+
27
+ export interface ToastApi {
28
+ show(t: ToastInput): void;
29
+ error(e: unknown): void;
30
+ pause(): void;
31
+ resume(): void;
32
+ dismiss(): void;
33
+ activate(): void;
34
+ current: Accessor<ToastOptions | undefined>;
35
+ pending: Accessor<number>;
36
+ }
37
+
38
+ const DEFAULT_MS = 5000;
39
+
40
+ function ToastSurface(props: { toast: ToastOptions; pending: number; api: ToastApi }): JSX.Element {
41
+ const theme = useTheme().surface('toast');
42
+ const dimensions = useTerminalDimensions();
43
+ const [hovered, setHovered] = createSignal(false);
44
+ const hover = (v: boolean) => {
45
+ setHovered(v);
46
+ if (v) props.api.pause();
47
+ else props.api.resume();
48
+ };
49
+ const affordance = () => (
50
+ <text
51
+ flexShrink={0}
52
+ marginLeft={2}
53
+ wrapMode="none"
54
+ attributes={hovered() && props.toast.action ? TextAttributes.BOLD : undefined}
55
+ fg={hovered() ? theme.text.action.primary.selected : theme.text.muted}
56
+ >
57
+ {props.toast.action ? `› ${props.toast.action.label}` : 'x'}
58
+ </text>
59
+ );
60
+ return (
61
+ <box
62
+ position="absolute"
63
+ top={1}
64
+ right={2}
65
+ zIndex={4000}
66
+ maxWidth={Math.min(60, dimensions().width - 6)}
67
+ borderColor={theme.text.feedback[props.toast.variant]}
68
+ border={SplitBorder.border}
69
+ customBorderChars={SplitBorder.customBorderChars}
70
+ onMouseOver={() => hover(true)}
71
+ onMouseOut={() => hover(false)}
72
+ onMouseUp={() => props.api.activate()}
73
+ >
74
+ <box
75
+ width="100%"
76
+ paddingLeft={2}
77
+ paddingRight={2}
78
+ paddingTop={1}
79
+ paddingBottom={1}
80
+ backgroundColor={theme.background.raised.high}
81
+ >
82
+ <Show
83
+ when={props.toast.title}
84
+ fallback={
85
+ <box flexDirection="row" width="100%">
86
+ <text fg={theme.text.base} wrapMode="word" flexGrow={1}>
87
+ {props.toast.message}
88
+ </text>
89
+ {affordance()}
90
+ </box>
91
+ }
92
+ >
93
+ <box flexDirection="row" width="100%" marginBottom={1}>
94
+ <text attributes={TextAttributes.BOLD} fg={theme.text.base}>
95
+ {props.toast.title ?? ''}
96
+ </text>
97
+ <box flexGrow={1} />
98
+ {affordance()}
99
+ </box>
100
+ <text fg={theme.text.base} wrapMode="word" width="100%">
101
+ {props.toast.message}
102
+ </text>
103
+ </Show>
104
+ <Show when={props.pending > 0}>
105
+ <text fg={theme.text.muted} marginTop={1}>{`+${props.pending} more`}</text>
106
+ </Show>
107
+ </box>
108
+ </box>
109
+ );
110
+ }
111
+
112
+ /** Draws the current toast (top-right); mount it once, above everything else. */
113
+ export function Toast(): JSX.Element {
114
+ const api = useToast();
115
+ return (
116
+ <Show when={api.current()}>
117
+ {(current) => <ToastSurface toast={current()} pending={api.pending()} api={api} />}
118
+ </Show>
119
+ );
120
+ }
121
+
122
+ function init(): ToastApi {
123
+ // signals, not a store: a store would merge the next toast into the current one
124
+ const [current, setCurrent] = createSignal<ToastOptions | undefined>(undefined);
125
+ const [queue, setQueue] = createSignal<ToastOptions[]>([]);
126
+ let timeout: NodeJS.Timeout | undefined;
127
+ let startedAt = 0;
128
+ let remaining = 0;
129
+ let paused = false;
130
+
131
+ const clear = () => {
132
+ if (timeout) clearTimeout(timeout);
133
+ timeout = undefined;
134
+ };
135
+ const start = (duration: number) => {
136
+ clear();
137
+ remaining = duration;
138
+ startedAt = Date.now();
139
+ timeout = setTimeout(() => dismiss(), duration);
140
+ timeout.unref?.();
141
+ };
142
+ const dismiss = () => {
143
+ clear();
144
+ const next = queue()[0];
145
+ setQueue((q) => q.slice(1));
146
+ setCurrent(next);
147
+ if (!next) {
148
+ paused = false;
149
+ return;
150
+ }
151
+ remaining = next.duration;
152
+ if (!paused) start(next.duration);
153
+ };
154
+ const api: ToastApi = {
155
+ show(input) {
156
+ const t: ToastOptions = { ...input, duration: input.duration ?? DEFAULT_MS };
157
+ if (current()) {
158
+ setQueue((q) => [...q, t]);
159
+ return;
160
+ }
161
+ setCurrent(t);
162
+ start(t.duration);
163
+ },
164
+ error(e) {
165
+ api.show({ variant: 'error', message: e instanceof Error ? e.message : String(e) });
166
+ },
167
+ pause() {
168
+ if (!current() || paused) return;
169
+ paused = true;
170
+ remaining = Math.max(0, remaining - (Date.now() - startedAt));
171
+ clear();
172
+ },
173
+ resume() {
174
+ if (!current() || !paused) return;
175
+ paused = false;
176
+ start(remaining);
177
+ },
178
+ dismiss,
179
+ activate() {
180
+ const action = current()?.action;
181
+ paused = false; // the pointer is still over the spot; the next toast must expire on its own
182
+ dismiss();
183
+ action?.run();
184
+ },
185
+ current,
186
+ pending: () => queue().length,
187
+ };
188
+ onCleanup(clear);
189
+ return api;
190
+ }
191
+
192
+ const Context = createContext<ToastApi>();
193
+
194
+ export function ToastProvider(props: ParentProps): JSX.Element {
195
+ const value = init();
196
+ return <Context.Provider value={value}>{props.children}</Context.Provider>;
197
+ }
198
+
199
+ export function useToast(): ToastApi {
200
+ const value = useContext(Context);
201
+ if (!value) throw new Error('useToast outside ToastProvider');
202
+ return value;
203
+ }