drexler 0.2.4 → 0.2.6
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/CHANGELOG.md +10 -0
- package/package.json +1 -1
- package/src/commands.ts +134 -17
- package/src/ui/App.tsx +85 -3
- package/src/ui/CommandPalette.tsx +32 -5
- package/src/ui/InputBox.tsx +9 -2
- package/src/ui/SynergyEvent.tsx +284 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.2.6
|
|
4
|
+
|
|
5
|
+
- Upgraded `/synergy` into a rotating animated Ink event with staged reveals, progress, KPI tickers, and themed finale copy.
|
|
6
|
+
- Added compact synergy rendering and a non-interactive fallback line for classic command dispatch.
|
|
7
|
+
|
|
8
|
+
## 0.2.5
|
|
9
|
+
|
|
10
|
+
- Made constrained slash commands open smoother option choosers, with `/theme` showing all theme choices as soon as the command is typed.
|
|
11
|
+
- Added richer theme descriptions and contextual hints in the command palette.
|
|
12
|
+
|
|
3
13
|
## 0.2.4
|
|
4
14
|
|
|
5
15
|
- Improved transcript readability with distinct user and Drexler turn blocks, role-specific accents, and clearer body markers.
|
package/package.json
CHANGED
package/src/commands.ts
CHANGED
|
@@ -53,6 +53,7 @@ const WHITESPACE_RE = /\s+/;
|
|
|
53
53
|
export interface SlashCommand {
|
|
54
54
|
readonly name: string;
|
|
55
55
|
readonly description: string;
|
|
56
|
+
readonly hint?: string;
|
|
56
57
|
}
|
|
57
58
|
|
|
58
59
|
export const COMMAND_PALETTE: ReadonlyArray<SlashCommand> = [
|
|
@@ -76,49 +77,151 @@ export const COMMAND_PALETTE: ReadonlyArray<SlashCommand> = [
|
|
|
76
77
|
{ name: "/copy-last", description: "Copy last response" },
|
|
77
78
|
];
|
|
78
79
|
|
|
80
|
+
const THEME_PALETTE_COPY: Record<
|
|
81
|
+
ThemeName,
|
|
82
|
+
{ readonly description: string; readonly hint: string }
|
|
83
|
+
> = {
|
|
84
|
+
apollo: {
|
|
85
|
+
description: "Signature Drexler green",
|
|
86
|
+
hint: "default executive terminal",
|
|
87
|
+
},
|
|
88
|
+
amber: {
|
|
89
|
+
description: "Warm amber deal glow",
|
|
90
|
+
hint: "low-light command room",
|
|
91
|
+
},
|
|
92
|
+
mono: {
|
|
93
|
+
description: "Plain high-contrast text",
|
|
94
|
+
hint: "NO_COLOR friendly",
|
|
95
|
+
},
|
|
96
|
+
terminal: {
|
|
97
|
+
description: "Classic ANSI terminal",
|
|
98
|
+
hint: "green/cyan legacy mode",
|
|
99
|
+
},
|
|
100
|
+
dealroom: {
|
|
101
|
+
description: "Teal boardroom desk",
|
|
102
|
+
hint: "quiet professional palette",
|
|
103
|
+
},
|
|
104
|
+
midnight: {
|
|
105
|
+
description: "Cool blue night desk",
|
|
106
|
+
hint: "focused late-session work",
|
|
107
|
+
},
|
|
108
|
+
paper: {
|
|
109
|
+
description: "Clean document mode",
|
|
110
|
+
hint: "bright memo-style contrast",
|
|
111
|
+
},
|
|
112
|
+
plasma: {
|
|
113
|
+
description: "Magenta trading floor",
|
|
114
|
+
hint: "high-energy neon accent",
|
|
115
|
+
},
|
|
116
|
+
};
|
|
117
|
+
|
|
79
118
|
const ARGUMENT_PALETTE: ReadonlyArray<{
|
|
80
119
|
readonly command: string;
|
|
120
|
+
readonly baseDescription: string;
|
|
121
|
+
readonly baseHint: string;
|
|
81
122
|
readonly values: ReadonlyArray<SlashCommand>;
|
|
82
123
|
}> = [
|
|
83
124
|
{
|
|
84
125
|
command: "/theme",
|
|
126
|
+
baseDescription: "Theme chooser",
|
|
127
|
+
baseHint: "select a look below",
|
|
85
128
|
values: [
|
|
86
|
-
...THEME_NAMES.map((name) =>
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
129
|
+
...THEME_NAMES.map((name) => {
|
|
130
|
+
const copy = THEME_PALETTE_COPY[name];
|
|
131
|
+
return {
|
|
132
|
+
name: `/theme ${name}`,
|
|
133
|
+
description: copy.description,
|
|
134
|
+
hint: copy.hint,
|
|
135
|
+
};
|
|
136
|
+
}),
|
|
137
|
+
{
|
|
138
|
+
name: "/theme save",
|
|
139
|
+
description: "Persist current theme",
|
|
140
|
+
hint: "use after previewing",
|
|
141
|
+
},
|
|
91
142
|
],
|
|
92
143
|
},
|
|
93
144
|
{
|
|
94
145
|
command: "/startup",
|
|
146
|
+
baseDescription: "Startup mode chooser",
|
|
147
|
+
baseHint: "pick launch behavior",
|
|
95
148
|
values: [
|
|
96
|
-
{
|
|
97
|
-
|
|
98
|
-
|
|
149
|
+
{
|
|
150
|
+
name: "/startup fast",
|
|
151
|
+
description: "Persist fast startup",
|
|
152
|
+
hint: "skip ceremony",
|
|
153
|
+
},
|
|
154
|
+
{
|
|
155
|
+
name: "/startup no-intro",
|
|
156
|
+
description: "Skip intro on launch",
|
|
157
|
+
hint: "keep normal runtime",
|
|
158
|
+
},
|
|
159
|
+
{
|
|
160
|
+
name: "/startup normal",
|
|
161
|
+
description: "Restore full intro",
|
|
162
|
+
hint: "show full opening",
|
|
163
|
+
},
|
|
99
164
|
],
|
|
100
165
|
},
|
|
101
166
|
{
|
|
102
167
|
command: "/retry",
|
|
168
|
+
baseDescription: "Retry style chooser",
|
|
169
|
+
baseHint: "reshape last answer",
|
|
103
170
|
values: [
|
|
104
|
-
{
|
|
105
|
-
|
|
171
|
+
{
|
|
172
|
+
name: "/retry terse",
|
|
173
|
+
description: "Retry in two sentences",
|
|
174
|
+
hint: "short and direct",
|
|
175
|
+
},
|
|
176
|
+
{
|
|
177
|
+
name: "/retry brutal",
|
|
178
|
+
description: "Retry more forcefully",
|
|
179
|
+
hint: "sharper critique",
|
|
180
|
+
},
|
|
106
181
|
],
|
|
107
182
|
},
|
|
108
183
|
{
|
|
109
184
|
command: "/export",
|
|
185
|
+
baseDescription: "Export format chooser",
|
|
186
|
+
baseHint: "pick transcript output",
|
|
110
187
|
values: [
|
|
111
|
-
{
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
188
|
+
{
|
|
189
|
+
name: "/export md",
|
|
190
|
+
description: "Export markdown transcript",
|
|
191
|
+
hint: "portable notes",
|
|
192
|
+
},
|
|
193
|
+
{
|
|
194
|
+
name: "/export txt",
|
|
195
|
+
description: "Export plain text transcript",
|
|
196
|
+
hint: "clean copy",
|
|
197
|
+
},
|
|
198
|
+
{
|
|
199
|
+
name: "/export json",
|
|
200
|
+
description: "Export structured JSON",
|
|
201
|
+
hint: "machine-readable",
|
|
202
|
+
},
|
|
203
|
+
{
|
|
204
|
+
name: "/export html",
|
|
205
|
+
description: "Export printable HTML",
|
|
206
|
+
hint: "browser-ready memo",
|
|
207
|
+
},
|
|
115
208
|
],
|
|
116
209
|
},
|
|
117
210
|
{
|
|
118
211
|
command: "/model",
|
|
212
|
+
baseDescription: "Model chooser",
|
|
213
|
+
baseHint: "select inference desk",
|
|
119
214
|
values: [
|
|
120
|
-
{
|
|
121
|
-
|
|
215
|
+
{
|
|
216
|
+
name: "/model 31b",
|
|
217
|
+
description: "Use primary 31b model",
|
|
218
|
+
hint: "best default",
|
|
219
|
+
},
|
|
220
|
+
{
|
|
221
|
+
name: "/model 26b",
|
|
222
|
+
description: "Use fallback 26b model",
|
|
223
|
+
hint: "faster backup",
|
|
224
|
+
},
|
|
122
225
|
],
|
|
123
226
|
},
|
|
124
227
|
];
|
|
@@ -126,6 +229,16 @@ const ARGUMENT_PALETTE: ReadonlyArray<{
|
|
|
126
229
|
function filterArgumentPalette(input: string): ReadonlyArray<SlashCommand> {
|
|
127
230
|
const lower = input.toLowerCase();
|
|
128
231
|
for (const group of ARGUMENT_PALETTE) {
|
|
232
|
+
if (lower === group.command) {
|
|
233
|
+
return [
|
|
234
|
+
{
|
|
235
|
+
name: group.command,
|
|
236
|
+
description: group.baseDescription,
|
|
237
|
+
hint: group.baseHint,
|
|
238
|
+
},
|
|
239
|
+
...group.values,
|
|
240
|
+
];
|
|
241
|
+
}
|
|
129
242
|
const prefix = `${group.command} `;
|
|
130
243
|
if (!lower.startsWith(prefix)) continue;
|
|
131
244
|
const argPrefix = lower.slice(prefix.length);
|
|
@@ -143,6 +256,8 @@ export function filterPaletteByPrefix(
|
|
|
143
256
|
input: string,
|
|
144
257
|
): ReadonlyArray<SlashCommand> {
|
|
145
258
|
if (!input.startsWith("/")) return [];
|
|
259
|
+
const exactArgumentPalette = filterArgumentPalette(input);
|
|
260
|
+
if (exactArgumentPalette.length > 0) return exactArgumentPalette;
|
|
146
261
|
if (input.includes(" ")) return filterArgumentPalette(input);
|
|
147
262
|
const prefix = input.toLowerCase();
|
|
148
263
|
return COMMAND_PALETTE.filter((c) =>
|
|
@@ -199,7 +314,9 @@ export function dispatch(input: string, ctx: CommandContext): CommandAction {
|
|
|
199
314
|
};
|
|
200
315
|
|
|
201
316
|
case "synergy":
|
|
202
|
-
ctx.print(
|
|
317
|
+
ctx.print(
|
|
318
|
+
"SYNERGY EVENT: alignment protocol completed. Award: continued employment.",
|
|
319
|
+
);
|
|
203
320
|
return { type: "continue" };
|
|
204
321
|
|
|
205
322
|
case "model":
|
package/src/ui/App.tsx
CHANGED
|
@@ -37,6 +37,13 @@ import { InputBox } from "./InputBox.tsx";
|
|
|
37
37
|
import { StreamingMessage } from "./Message.tsx";
|
|
38
38
|
import { Spinner } from "./Spinner.tsx";
|
|
39
39
|
import { StatusBar } from "./StatusBar.tsx";
|
|
40
|
+
import {
|
|
41
|
+
pickSynergyEvent,
|
|
42
|
+
SynergyEvent,
|
|
43
|
+
SYNERGY_EVENT_FRAMES,
|
|
44
|
+
synergyEventRows,
|
|
45
|
+
type SynergyEventDefinition,
|
|
46
|
+
} from "./SynergyEvent.tsx";
|
|
40
47
|
import { ThemeProvider } from "./ThemeContext.tsx";
|
|
41
48
|
import { TranscriptViewport } from "./TranscriptViewport.tsx";
|
|
42
49
|
import { getActiveTheme, THEMES } from "./themes.ts";
|
|
@@ -84,6 +91,11 @@ interface ChatItem {
|
|
|
84
91
|
content: string;
|
|
85
92
|
}
|
|
86
93
|
|
|
94
|
+
interface ActiveSynergyEvent {
|
|
95
|
+
event: SynergyEventDefinition;
|
|
96
|
+
frame: number;
|
|
97
|
+
}
|
|
98
|
+
|
|
87
99
|
interface AppProps {
|
|
88
100
|
conversation: Conversation;
|
|
89
101
|
config: Config;
|
|
@@ -155,6 +167,9 @@ export function App({ conversation, config, mood = "neutral", fetchFn }: AppProp
|
|
|
155
167
|
const cursor = draft.cursor;
|
|
156
168
|
const [streaming, setStreaming] = useState<string | null>(null);
|
|
157
169
|
const [thinking, setThinking] = useState<string | null>(null);
|
|
170
|
+
const [synergyEvent, setSynergyEvent] = useState<ActiveSynergyEvent | null>(
|
|
171
|
+
null,
|
|
172
|
+
);
|
|
158
173
|
const [exitMsg, setExitMsg] = useState<string | null>(null);
|
|
159
174
|
const [witticism, setWitticism] = useState<string>(pick(WITTICISMS));
|
|
160
175
|
const [model, setModel] = useState<string>(config.model);
|
|
@@ -207,6 +222,7 @@ export function App({ conversation, config, mood = "neutral", fetchFn }: AppProp
|
|
|
207
222
|
const mountedRef = useRef(true);
|
|
208
223
|
const exitingRef = useRef(false);
|
|
209
224
|
const exitTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
225
|
+
const synergyTimerRef = useRef<ReturnType<typeof setInterval> | null>(null);
|
|
210
226
|
const flushStream = useCallback(() => {
|
|
211
227
|
if (!mountedRef.current) return;
|
|
212
228
|
setStreaming(streamBufRef.current);
|
|
@@ -234,6 +250,45 @@ export function App({ conversation, config, mood = "neutral", fetchFn }: AppProp
|
|
|
234
250
|
[flushStream],
|
|
235
251
|
);
|
|
236
252
|
|
|
253
|
+
const runSynergyEvent = useCallback(() => {
|
|
254
|
+
if (synergyTimerRef.current !== null) {
|
|
255
|
+
clearInterval(synergyTimerRef.current);
|
|
256
|
+
synergyTimerRef.current = null;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
const event = pickSynergyEvent();
|
|
260
|
+
let frame = 0;
|
|
261
|
+
const finalFrame = SYNERGY_EVENT_FRAMES - 1;
|
|
262
|
+
const holdFrames = 8;
|
|
263
|
+
|
|
264
|
+
setThinking(null);
|
|
265
|
+
setStreaming(null);
|
|
266
|
+
setDeskStatus("idle");
|
|
267
|
+
setDeskNotice("synergy event");
|
|
268
|
+
setSynergyEvent({ event, frame });
|
|
269
|
+
|
|
270
|
+
synergyTimerRef.current = setInterval(() => {
|
|
271
|
+
frame += 1;
|
|
272
|
+
if (!mountedRef.current) return;
|
|
273
|
+
|
|
274
|
+
if (frame <= finalFrame) {
|
|
275
|
+
setSynergyEvent({ event, frame });
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
if (frame >= finalFrame + holdFrames) {
|
|
280
|
+
if (synergyTimerRef.current !== null) {
|
|
281
|
+
clearInterval(synergyTimerRef.current);
|
|
282
|
+
synergyTimerRef.current = null;
|
|
283
|
+
}
|
|
284
|
+
setSynergyEvent(null);
|
|
285
|
+
setDeskNotice("synergy logged");
|
|
286
|
+
setWitticism(event.finalLine);
|
|
287
|
+
addItem("system", event.transcriptLine);
|
|
288
|
+
}
|
|
289
|
+
}, 60);
|
|
290
|
+
}, [addItem]);
|
|
291
|
+
|
|
237
292
|
const runLLM = useCallback(async (instruction?: string) => {
|
|
238
293
|
const startedAt = Date.now();
|
|
239
294
|
setThinking(pick(THINKING_LINES));
|
|
@@ -349,6 +404,10 @@ export function App({ conversation, config, mood = "neutral", fetchFn }: AppProp
|
|
|
349
404
|
},
|
|
350
405
|
});
|
|
351
406
|
const lower = line.toLowerCase().trim();
|
|
407
|
+
if (lower === "/synergy") {
|
|
408
|
+
runSynergyEvent();
|
|
409
|
+
return;
|
|
410
|
+
}
|
|
352
411
|
if (lower === "/clear" || lower.startsWith("/clear ")) {
|
|
353
412
|
setItems([]);
|
|
354
413
|
setLastLatencyMs(null);
|
|
@@ -399,6 +458,7 @@ export function App({ conversation, config, mood = "neutral", fetchFn }: AppProp
|
|
|
399
458
|
model,
|
|
400
459
|
removeLastAssistantItem,
|
|
401
460
|
runLLM,
|
|
461
|
+
runSynergyEvent,
|
|
402
462
|
triggerExit,
|
|
403
463
|
],
|
|
404
464
|
);
|
|
@@ -424,8 +484,11 @@ export function App({ conversation, config, mood = "neutral", fetchFn }: AppProp
|
|
|
424
484
|
);
|
|
425
485
|
|
|
426
486
|
useInput((char, key) => {
|
|
427
|
-
if (streaming !== null || thinking !== null) {
|
|
487
|
+
if (streaming !== null || thinking !== null || synergyEvent !== null) {
|
|
428
488
|
if (key.escape) {
|
|
489
|
+
if (synergyEvent !== null) {
|
|
490
|
+
return;
|
|
491
|
+
}
|
|
429
492
|
cancelledRef.current = true;
|
|
430
493
|
abortRef.current?.abort();
|
|
431
494
|
return;
|
|
@@ -593,11 +656,17 @@ export function App({ conversation, config, mood = "neutral", fetchFn }: AppProp
|
|
|
593
656
|
if (exitTimerRef.current !== null) {
|
|
594
657
|
clearTimeout(exitTimerRef.current);
|
|
595
658
|
}
|
|
659
|
+
if (synergyTimerRef.current !== null) {
|
|
660
|
+
clearInterval(synergyTimerRef.current);
|
|
661
|
+
}
|
|
596
662
|
};
|
|
597
663
|
}, []);
|
|
598
664
|
|
|
599
|
-
const isBusy = streaming !== null || thinking !== null;
|
|
665
|
+
const isBusy = streaming !== null || thinking !== null || synergyEvent !== null;
|
|
600
666
|
const headerStatus = isBusy ? "streaming" : deskStatus;
|
|
667
|
+
const visibleTranscriptRows = synergyEvent
|
|
668
|
+
? Math.max(1, maxTranscriptRows - synergyEventRows(chromeWidth, isCompact))
|
|
669
|
+
: maxTranscriptRows;
|
|
601
670
|
|
|
602
671
|
return (
|
|
603
672
|
<ThemeProvider value={activeTheme}>
|
|
@@ -617,7 +686,7 @@ export function App({ conversation, config, mood = "neutral", fetchFn }: AppProp
|
|
|
617
686
|
/>
|
|
618
687
|
<TranscriptViewport
|
|
619
688
|
items={items}
|
|
620
|
-
maxRows={
|
|
689
|
+
maxRows={visibleTranscriptRows}
|
|
621
690
|
cols={chromeWidth}
|
|
622
691
|
compact={isCompact}
|
|
623
692
|
scrollOffset={scrollOffset}
|
|
@@ -634,6 +703,14 @@ export function App({ conversation, config, mood = "neutral", fetchFn }: AppProp
|
|
|
634
703
|
<Spinner label={thinking} width={chromeWidth} />
|
|
635
704
|
</Box>
|
|
636
705
|
)}
|
|
706
|
+
{synergyEvent !== null && (
|
|
707
|
+
<SynergyEvent
|
|
708
|
+
event={synergyEvent.event}
|
|
709
|
+
frame={synergyEvent.frame}
|
|
710
|
+
width={chromeWidth}
|
|
711
|
+
compact={isCompact}
|
|
712
|
+
/>
|
|
713
|
+
)}
|
|
637
714
|
{exitMsg !== null ? (
|
|
638
715
|
<Box paddingX={1} marginBottom={1}>
|
|
639
716
|
<Text color={t.primaryLight} bold>
|
|
@@ -654,6 +731,11 @@ export function App({ conversation, config, mood = "neutral", fetchFn }: AppProp
|
|
|
654
731
|
value={input}
|
|
655
732
|
cursor={cursor}
|
|
656
733
|
disabled={isBusy}
|
|
734
|
+
disabledLabel={
|
|
735
|
+
synergyEvent !== null
|
|
736
|
+
? "(Synergy event running... boardroom locked)"
|
|
737
|
+
: undefined
|
|
738
|
+
}
|
|
657
739
|
width={inputWidth}
|
|
658
740
|
/>
|
|
659
741
|
</Box>
|
|
@@ -31,10 +31,34 @@ const COMMAND_HINTS: Record<string, string> = {
|
|
|
31
31
|
"/copy-last": "copy latest response",
|
|
32
32
|
};
|
|
33
33
|
|
|
34
|
+
const ARGUMENT_TITLES: Record<string, { title: string; hint: string }> = {
|
|
35
|
+
"/theme": { title: "THEMES", hint: "tab fill, ↑↓ choose, enter apply" },
|
|
36
|
+
"/startup": { title: "STARTUP", hint: "tab fill, ↑↓ choose, enter save" },
|
|
37
|
+
"/retry": { title: "RETRY", hint: "tab fill, ↑↓ choose, enter reroll" },
|
|
38
|
+
"/export": { title: "EXPORT", hint: "tab fill, ↑↓ choose, enter run" },
|
|
39
|
+
"/model": { title: "MODELS", hint: "tab fill, ↑↓ choose, enter switch" },
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
function paletteHeading(items: ReadonlyArray<SlashCommand>): {
|
|
43
|
+
title: string;
|
|
44
|
+
hint: string;
|
|
45
|
+
} {
|
|
46
|
+
const firstToken = items[0]?.name.split(" ")[0] ?? "";
|
|
47
|
+
const hasArguments = items.some((item) => item.name.includes(" "));
|
|
48
|
+
if (hasArguments && ARGUMENT_TITLES[firstToken]) {
|
|
49
|
+
return ARGUMENT_TITLES[firstToken];
|
|
50
|
+
}
|
|
51
|
+
return {
|
|
52
|
+
title: "DIRECTIVES",
|
|
53
|
+
hint: "tab/↑↓ select, enter execute",
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
34
57
|
function CommandPaletteInner({ items, selectedIdx, width = 80 }: Props) {
|
|
35
58
|
const t = useTheme();
|
|
36
59
|
const safeWidth = Math.max(1, Math.floor(width));
|
|
37
60
|
const tiny = safeWidth < 26;
|
|
61
|
+
const heading = useMemo(() => paletteHeading(items), [items]);
|
|
38
62
|
const maxNameW = useMemo(
|
|
39
63
|
() => items.reduce((m, i) => Math.max(m, i.name.length), 0),
|
|
40
64
|
[items],
|
|
@@ -81,19 +105,22 @@ function CommandPaletteInner({ items, selectedIdx, width = 80 }: Props) {
|
|
|
81
105
|
>
|
|
82
106
|
<Box marginBottom={1}>
|
|
83
107
|
<Text color={t.primaryLight} bold>
|
|
84
|
-
|
|
108
|
+
{heading.title}
|
|
85
109
|
</Text>
|
|
86
110
|
<Text color={t.primaryDim}> ─ </Text>
|
|
87
111
|
<Text color={t.dim} wrap="truncate">
|
|
88
|
-
{fitDisplayText(
|
|
112
|
+
{fitDisplayText(
|
|
113
|
+
heading.hint,
|
|
114
|
+
Math.max(1, innerWidth - displayWidth(heading.title) - 3),
|
|
115
|
+
)}
|
|
89
116
|
</Text>
|
|
90
117
|
</Box>
|
|
91
118
|
{items.map((item, idx) => {
|
|
92
119
|
const sel = idx === selectedIdx;
|
|
93
120
|
const isArgumentSuggestion = item.name.includes(" ");
|
|
94
|
-
const hint =
|
|
95
|
-
|
|
96
|
-
: COMMAND_HINTS[item.name] ?? item.description;
|
|
121
|
+
const hint =
|
|
122
|
+
item.hint ??
|
|
123
|
+
(isArgumentSuggestion ? "" : COMMAND_HINTS[item.name] ?? item.description);
|
|
97
124
|
const name = item.name.padEnd(maxNameW + 1);
|
|
98
125
|
const desc = fitDisplayText(item.description, descBudget);
|
|
99
126
|
const clippedHint =
|
package/src/ui/InputBox.tsx
CHANGED
|
@@ -8,6 +8,7 @@ interface Props {
|
|
|
8
8
|
cursor: number;
|
|
9
9
|
disabled: boolean;
|
|
10
10
|
width: number;
|
|
11
|
+
disabledLabel?: string;
|
|
11
12
|
}
|
|
12
13
|
|
|
13
14
|
const PROMPT_WIDTH = 2;
|
|
@@ -80,13 +81,19 @@ function fitPlainText(chars: string[], cursor: number, maxWidth: number): string
|
|
|
80
81
|
return out || " ";
|
|
81
82
|
}
|
|
82
83
|
|
|
83
|
-
function InputBoxInner({
|
|
84
|
+
function InputBoxInner({
|
|
85
|
+
value,
|
|
86
|
+
cursor,
|
|
87
|
+
disabled,
|
|
88
|
+
width,
|
|
89
|
+
disabledLabel,
|
|
90
|
+
}: Props) {
|
|
84
91
|
const t = useTheme();
|
|
85
92
|
const chars = splitGraphemes(value);
|
|
86
93
|
const safeCursor = clampCursor(value, cursor);
|
|
87
94
|
const boxWidth = Math.max(1, width);
|
|
88
95
|
const inputBudget = Math.max(1, boxWidth - BOX_CHROME_WIDTH - PROMPT_WIDTH);
|
|
89
|
-
const disabledText = "(Drexler thinking... ESC to cancel)";
|
|
96
|
+
const disabledText = disabledLabel ?? "(Drexler thinking... ESC to cancel)";
|
|
90
97
|
const window = fitWindow(chars, safeCursor, inputBudget);
|
|
91
98
|
const visible = chars.slice(window.start, window.end);
|
|
92
99
|
const visibleCursor = clamp(safeCursor - window.start, 0, visible.length);
|
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
import { Box, Text } from "ink";
|
|
2
|
+
import { memo } from "react";
|
|
3
|
+
import { displayWidth, fitDisplayText } from "./graphemes.ts";
|
|
4
|
+
import { useTheme } from "./ThemeContext.tsx";
|
|
5
|
+
|
|
6
|
+
export interface SynergyEventDefinition {
|
|
7
|
+
id: string;
|
|
8
|
+
title: string;
|
|
9
|
+
subtitle: string;
|
|
10
|
+
art: readonly string[];
|
|
11
|
+
stages: readonly string[];
|
|
12
|
+
kpis: readonly string[];
|
|
13
|
+
finalLine: string;
|
|
14
|
+
transcriptLine: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export const SYNERGY_EVENT_FRAMES = 28;
|
|
18
|
+
|
|
19
|
+
export const SYNERGY_EVENTS: readonly SynergyEventDefinition[] = [
|
|
20
|
+
{
|
|
21
|
+
id: "alignment-protocol",
|
|
22
|
+
title: "ALIGNMENT PROTOCOL",
|
|
23
|
+
subtitle: "cross-functional theater detected",
|
|
24
|
+
art: [
|
|
25
|
+
" ██████╗ ██╗ ██╗███╗ ██╗",
|
|
26
|
+
" ██╔════╝ ╚██╗ ██╔╝████╗ ██║",
|
|
27
|
+
" ╚█████╗ ╚████╔╝ ██╔██╗ ██║",
|
|
28
|
+
" ╚═══██╗ ╚██╔╝ ██║╚██╗██║",
|
|
29
|
+
" ██████╔╝ ██║ ██║ ╚████║",
|
|
30
|
+
" ╚═════╝ ╚═╝ ╚═╝ ╚═══╝",
|
|
31
|
+
],
|
|
32
|
+
stages: [
|
|
33
|
+
"initiating alignment protocol",
|
|
34
|
+
"harmonizing action items",
|
|
35
|
+
"converting meetings into margin",
|
|
36
|
+
"minting provisional shareholder value",
|
|
37
|
+
],
|
|
38
|
+
kpis: [
|
|
39
|
+
"EBITDA +0.4%",
|
|
40
|
+
"morale provisionally approved",
|
|
41
|
+
"consultants +7",
|
|
42
|
+
"clarity -3",
|
|
43
|
+
],
|
|
44
|
+
finalLine:
|
|
45
|
+
"Synergy achieved. Headcount unchanged. Morale amortized.",
|
|
46
|
+
transcriptLine: "SYNERGY EVENT: shareholder value allegedly unlocked.",
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
id: "boardroom-alert",
|
|
50
|
+
title: "BOARDROOM ALERT",
|
|
51
|
+
subtitle: "value creation siren armed",
|
|
52
|
+
art: [
|
|
53
|
+
" [!] BOARDROOM ALERT",
|
|
54
|
+
" [!] ALIGNMENT DETECTED",
|
|
55
|
+
" [!] VALUE CREATION IMMINENT",
|
|
56
|
+
" [!] ASK NO FOLLOW-UP QUESTIONS",
|
|
57
|
+
],
|
|
58
|
+
stages: [
|
|
59
|
+
"paging senior stakeholders",
|
|
60
|
+
"escalating morale to committee",
|
|
61
|
+
"routing accountability offshore",
|
|
62
|
+
"closing the loop with no loop",
|
|
63
|
+
],
|
|
64
|
+
kpis: [
|
|
65
|
+
"risk committee awake",
|
|
66
|
+
"action items multiplying",
|
|
67
|
+
"status: billable",
|
|
68
|
+
"decision rights unclear",
|
|
69
|
+
],
|
|
70
|
+
finalLine: "Drexler approve synergy. Nobody ask what changed.",
|
|
71
|
+
transcriptLine: "SYNERGY EVENT: boardroom siren produced measurable vibes.",
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
id: "briefcase-cameo",
|
|
75
|
+
title: "BRIEFCASE CAMEO",
|
|
76
|
+
subtitle: "executive artifact opening",
|
|
77
|
+
art: [
|
|
78
|
+
" _________",
|
|
79
|
+
" _/ ___ \\_",
|
|
80
|
+
" | $ $ |",
|
|
81
|
+
" | ───┬─── |",
|
|
82
|
+
" |_____|_______|",
|
|
83
|
+
" / | \\",
|
|
84
|
+
],
|
|
85
|
+
stages: [
|
|
86
|
+
"unlocking sealed mandate",
|
|
87
|
+
"counting invisible efficiencies",
|
|
88
|
+
"deploying tasteful corporate sparkle",
|
|
89
|
+
"reclassifying excitement as asset",
|
|
90
|
+
],
|
|
91
|
+
kpis: [
|
|
92
|
+
"briefcase yield +12 bps",
|
|
93
|
+
"sparkle reserve funded",
|
|
94
|
+
"memo density rising",
|
|
95
|
+
"bonus pool unchanged",
|
|
96
|
+
],
|
|
97
|
+
finalLine: "Briefcase open. Synergy escaped. Legal says it was planned.",
|
|
98
|
+
transcriptLine: "SYNERGY EVENT: briefcase opened and released approved optimism.",
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
id: "achievement-unlocked",
|
|
102
|
+
title: "ACHIEVEMENT UNLOCKED",
|
|
103
|
+
subtitle: "cross-functional theater",
|
|
104
|
+
art: [
|
|
105
|
+
" ╔══════════════════╗",
|
|
106
|
+
" ║ DEAL TROPHY +1 ║",
|
|
107
|
+
" ╚══════════════════╝",
|
|
108
|
+
" Reward: meeting",
|
|
109
|
+
" Status: billable",
|
|
110
|
+
],
|
|
111
|
+
stages: [
|
|
112
|
+
"checking performance conditions",
|
|
113
|
+
"unlocking meeting about meeting",
|
|
114
|
+
"allocating credit to leadership",
|
|
115
|
+
"filing victory under recurring revenue",
|
|
116
|
+
],
|
|
117
|
+
kpis: [
|
|
118
|
+
"achievement: unlocked",
|
|
119
|
+
"reward: one calendar invite",
|
|
120
|
+
"prestige +8",
|
|
121
|
+
"substance pending",
|
|
122
|
+
],
|
|
123
|
+
finalLine: "Achievement unlocked: Cross-Functional Theater.",
|
|
124
|
+
transcriptLine: "SYNERGY EVENT: achievement unlocked, substance pending.",
|
|
125
|
+
},
|
|
126
|
+
];
|
|
127
|
+
|
|
128
|
+
export function pickSynergyEvent(
|
|
129
|
+
random: () => number = Math.random,
|
|
130
|
+
): SynergyEventDefinition {
|
|
131
|
+
const idx = Math.min(
|
|
132
|
+
SYNERGY_EVENTS.length - 1,
|
|
133
|
+
Math.floor(random() * SYNERGY_EVENTS.length),
|
|
134
|
+
);
|
|
135
|
+
return SYNERGY_EVENTS[idx]!;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function frameProgress(frame: number): number {
|
|
139
|
+
return Math.max(0, Math.min(1, frame / (SYNERGY_EVENT_FRAMES - 1)));
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function bar(progress: number, width: number): string {
|
|
143
|
+
const safeWidth = Math.max(1, width);
|
|
144
|
+
const filled = Math.max(0, Math.min(safeWidth, Math.round(progress * safeWidth)));
|
|
145
|
+
return `${"█".repeat(filled)}${"░".repeat(safeWidth - filled)}`;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function stageAt(event: SynergyEventDefinition, frame: number): string {
|
|
149
|
+
const progress = frameProgress(frame);
|
|
150
|
+
const idx = Math.min(
|
|
151
|
+
event.stages.length - 1,
|
|
152
|
+
Math.floor(progress * event.stages.length),
|
|
153
|
+
);
|
|
154
|
+
return event.stages[idx]!;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function visibleArt(event: SynergyEventDefinition, frame: number): readonly string[] {
|
|
158
|
+
const progress = frameProgress(frame);
|
|
159
|
+
const count = Math.max(1, Math.ceil(progress * event.art.length));
|
|
160
|
+
return event.art.slice(0, count);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function kpiAt(event: SynergyEventDefinition, frame: number): string {
|
|
164
|
+
return event.kpis[Math.floor(frame / 3) % event.kpis.length]!;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
interface Props {
|
|
168
|
+
event: SynergyEventDefinition;
|
|
169
|
+
frame: number;
|
|
170
|
+
width?: number;
|
|
171
|
+
compact?: boolean;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function SynergyEventInner({
|
|
175
|
+
event,
|
|
176
|
+
frame,
|
|
177
|
+
width = 80,
|
|
178
|
+
compact = false,
|
|
179
|
+
}: Props) {
|
|
180
|
+
const t = useTheme();
|
|
181
|
+
const safeWidth = Math.max(1, Math.floor(width));
|
|
182
|
+
const progress = frameProgress(frame);
|
|
183
|
+
const done = progress >= 0.94;
|
|
184
|
+
const tiny = safeWidth < 38 || compact;
|
|
185
|
+
|
|
186
|
+
if (tiny) {
|
|
187
|
+
const label = done ? event.finalLine : stageAt(event, frame);
|
|
188
|
+
const miniBarWidth = Math.max(4, Math.min(18, safeWidth - 12));
|
|
189
|
+
const line = `SYNC ${bar(progress, miniBarWidth)} ${label}`;
|
|
190
|
+
return (
|
|
191
|
+
<Box width={safeWidth} flexShrink={1}>
|
|
192
|
+
<Text color={done ? t.primaryLight : t.warning} bold wrap="truncate">
|
|
193
|
+
{fitDisplayText(line, safeWidth)}
|
|
194
|
+
</Text>
|
|
195
|
+
</Box>
|
|
196
|
+
);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const innerWidth = Math.max(1, safeWidth - 4);
|
|
200
|
+
const title = `${event.title} · ${event.subtitle}`;
|
|
201
|
+
const progressWidth = Math.max(8, Math.min(34, innerWidth - 18));
|
|
202
|
+
const progressPct = `${Math.round(progress * 100)
|
|
203
|
+
.toString()
|
|
204
|
+
.padStart(3, " ")}%`;
|
|
205
|
+
const artWidth = Math.max(1, innerWidth - 2);
|
|
206
|
+
const kpi = kpiAt(event, frame);
|
|
207
|
+
|
|
208
|
+
return (
|
|
209
|
+
<Box
|
|
210
|
+
flexDirection="column"
|
|
211
|
+
borderStyle="round"
|
|
212
|
+
borderColor={done ? t.primaryLight : t.warning}
|
|
213
|
+
paddingX={1}
|
|
214
|
+
marginBottom={1}
|
|
215
|
+
width={safeWidth}
|
|
216
|
+
flexShrink={1}
|
|
217
|
+
>
|
|
218
|
+
<Box>
|
|
219
|
+
<Text color={t.warning} bold>
|
|
220
|
+
SYNERGY EVENT
|
|
221
|
+
</Text>
|
|
222
|
+
<Text color={t.primaryDim}> ─ </Text>
|
|
223
|
+
<Text color={t.dim} wrap="truncate">
|
|
224
|
+
{fitDisplayText(title, Math.max(1, innerWidth - 16))}
|
|
225
|
+
</Text>
|
|
226
|
+
</Box>
|
|
227
|
+
<Box marginTop={1} flexDirection="column">
|
|
228
|
+
{visibleArt(event, frame).map((line, idx) => (
|
|
229
|
+
<Text key={`${event.id}-${idx}`} color={t.primaryLight} wrap="truncate">
|
|
230
|
+
{fitDisplayText(line, artWidth)}
|
|
231
|
+
</Text>
|
|
232
|
+
))}
|
|
233
|
+
</Box>
|
|
234
|
+
<Box marginTop={1}>
|
|
235
|
+
<Text color={t.primaryDim}>[</Text>
|
|
236
|
+
<Text color={done ? t.primaryLight : t.warning}>
|
|
237
|
+
{bar(progress, progressWidth)}
|
|
238
|
+
</Text>
|
|
239
|
+
<Text color={t.primaryDim}>] </Text>
|
|
240
|
+
<Text color={t.dim}>{progressPct}</Text>
|
|
241
|
+
</Box>
|
|
242
|
+
<Box>
|
|
243
|
+
<Text color={t.primaryLight} bold>
|
|
244
|
+
◆{" "}
|
|
245
|
+
</Text>
|
|
246
|
+
<Text color={t.text} wrap="truncate">
|
|
247
|
+
{fitDisplayText(stageAt(event, frame), Math.max(1, innerWidth - 2))}
|
|
248
|
+
</Text>
|
|
249
|
+
</Box>
|
|
250
|
+
<Box>
|
|
251
|
+
<Text color={t.primaryDim}>ticker </Text>
|
|
252
|
+
<Text color={t.warning} wrap="truncate">
|
|
253
|
+
{fitDisplayText(kpi, Math.max(1, innerWidth - 7))}
|
|
254
|
+
</Text>
|
|
255
|
+
</Box>
|
|
256
|
+
{done ? (
|
|
257
|
+
<Box marginTop={1}>
|
|
258
|
+
<Text color={t.primaryLight} bold wrap="truncate">
|
|
259
|
+
{fitDisplayText(event.finalLine, innerWidth)}
|
|
260
|
+
</Text>
|
|
261
|
+
</Box>
|
|
262
|
+
) : null}
|
|
263
|
+
</Box>
|
|
264
|
+
);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
export const SynergyEvent = memo(SynergyEventInner);
|
|
268
|
+
|
|
269
|
+
export function synergyEventRows(width: number, compact = false): number {
|
|
270
|
+
if (compact || width < 38) return 1;
|
|
271
|
+
return 12;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
export function synergyEventMaxRowWidth(
|
|
275
|
+
rendered: string,
|
|
276
|
+
stripAnsi: (s: string) => string = (s) => s,
|
|
277
|
+
): number {
|
|
278
|
+
return Math.max(
|
|
279
|
+
0,
|
|
280
|
+
...stripAnsi(rendered)
|
|
281
|
+
.split("\n")
|
|
282
|
+
.map((row) => displayWidth(row)),
|
|
283
|
+
);
|
|
284
|
+
}
|