drexler 0.2.5 → 0.2.7
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 +11 -0
- package/package.json +1 -1
- package/src/commands.ts +3 -1
- package/src/ui/App.tsx +205 -86
- package/src/ui/InputBox.tsx +9 -2
- package/src/ui/SynergyEvent.tsx +300 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,16 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.2.7
|
|
4
|
+
|
|
5
|
+
- Stabilized `/synergy` animation layout with fixed row budgeting, a capped centered event panel, and completion only at 100%.
|
|
6
|
+
- Hardened interactive busy-state handling so input stays locked during active LLM requests and synergy events.
|
|
7
|
+
- Added lifecycle and row-budget coverage for the animated synergy flow.
|
|
8
|
+
|
|
9
|
+
## 0.2.6
|
|
10
|
+
|
|
11
|
+
- Upgraded `/synergy` into a rotating animated Ink event with staged reveals, progress, KPI tickers, and themed finale copy.
|
|
12
|
+
- Added compact synergy rendering and a non-interactive fallback line for classic command dispatch.
|
|
13
|
+
|
|
3
14
|
## 0.2.5
|
|
4
15
|
|
|
5
16
|
- Made constrained slash commands open smoother option choosers, with `/theme` showing all theme choices as soon as the command is typed.
|
package/package.json
CHANGED
package/src/commands.ts
CHANGED
|
@@ -314,7 +314,9 @@ export function dispatch(input: string, ctx: CommandContext): CommandAction {
|
|
|
314
314
|
};
|
|
315
315
|
|
|
316
316
|
case "synergy":
|
|
317
|
-
ctx.print(
|
|
317
|
+
ctx.print(
|
|
318
|
+
"SYNERGY EVENT: alignment protocol completed. Award: continued employment.",
|
|
319
|
+
);
|
|
318
320
|
return { type: "continue" };
|
|
319
321
|
|
|
320
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,10 @@ 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 [requestInFlight, setRequestInFlight] = useState(false);
|
|
171
|
+
const [synergyEvent, setSynergyEvent] = useState<ActiveSynergyEvent | null>(
|
|
172
|
+
null,
|
|
173
|
+
);
|
|
158
174
|
const [exitMsg, setExitMsg] = useState<string | null>(null);
|
|
159
175
|
const [witticism, setWitticism] = useState<string>(pick(WITTICISMS));
|
|
160
176
|
const [model, setModel] = useState<string>(config.model);
|
|
@@ -204,9 +220,12 @@ export function App({ conversation, config, mood = "neutral", fetchFn }: AppProp
|
|
|
204
220
|
const streamTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
205
221
|
const abortRef = useRef<AbortController | null>(null);
|
|
206
222
|
const cancelledRef = useRef(false);
|
|
223
|
+
const requestInFlightRef = useRef(false);
|
|
224
|
+
const synergyActiveRef = useRef(false);
|
|
207
225
|
const mountedRef = useRef(true);
|
|
208
226
|
const exitingRef = useRef(false);
|
|
209
227
|
const exitTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
228
|
+
const synergyTimerRef = useRef<ReturnType<typeof setInterval> | null>(null);
|
|
210
229
|
const flushStream = useCallback(() => {
|
|
211
230
|
if (!mountedRef.current) return;
|
|
212
231
|
setStreaming(streamBufRef.current);
|
|
@@ -218,6 +237,18 @@ export function App({ conversation, config, mood = "neutral", fetchFn }: AppProp
|
|
|
218
237
|
if (exitingRef.current) return;
|
|
219
238
|
exitingRef.current = true;
|
|
220
239
|
abortRef.current?.abort();
|
|
240
|
+
if (streamTimerRef.current !== null) {
|
|
241
|
+
clearTimeout(streamTimerRef.current);
|
|
242
|
+
streamTimerRef.current = null;
|
|
243
|
+
}
|
|
244
|
+
if (synergyTimerRef.current !== null) {
|
|
245
|
+
clearInterval(synergyTimerRef.current);
|
|
246
|
+
synergyTimerRef.current = null;
|
|
247
|
+
}
|
|
248
|
+
requestInFlightRef.current = false;
|
|
249
|
+
synergyActiveRef.current = false;
|
|
250
|
+
setRequestInFlight(false);
|
|
251
|
+
setSynergyEvent(null);
|
|
221
252
|
setExitMsg(msg);
|
|
222
253
|
exitTimerRef.current = setTimeout(() => exit(), 50);
|
|
223
254
|
},
|
|
@@ -234,100 +265,151 @@ export function App({ conversation, config, mood = "neutral", fetchFn }: AppProp
|
|
|
234
265
|
[flushStream],
|
|
235
266
|
);
|
|
236
267
|
|
|
268
|
+
const runSynergyEvent = useCallback(() => {
|
|
269
|
+
if (synergyTimerRef.current !== null) {
|
|
270
|
+
clearInterval(synergyTimerRef.current);
|
|
271
|
+
synergyTimerRef.current = null;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
const event = pickSynergyEvent();
|
|
275
|
+
let frame = 0;
|
|
276
|
+
const finalFrame = SYNERGY_EVENT_FRAMES - 1;
|
|
277
|
+
const holdFrames = 8;
|
|
278
|
+
|
|
279
|
+
setThinking(null);
|
|
280
|
+
setStreaming(null);
|
|
281
|
+
synergyActiveRef.current = true;
|
|
282
|
+
setDeskStatus("idle");
|
|
283
|
+
setDeskNotice("synergy event");
|
|
284
|
+
setSynergyEvent({ event, frame });
|
|
285
|
+
|
|
286
|
+
synergyTimerRef.current = setInterval(() => {
|
|
287
|
+
frame += 1;
|
|
288
|
+
if (!mountedRef.current) return;
|
|
289
|
+
|
|
290
|
+
if (frame <= finalFrame) {
|
|
291
|
+
setSynergyEvent({ event, frame });
|
|
292
|
+
return;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
if (frame >= finalFrame + holdFrames) {
|
|
296
|
+
if (synergyTimerRef.current !== null) {
|
|
297
|
+
clearInterval(synergyTimerRef.current);
|
|
298
|
+
synergyTimerRef.current = null;
|
|
299
|
+
}
|
|
300
|
+
setSynergyEvent(null);
|
|
301
|
+
synergyActiveRef.current = false;
|
|
302
|
+
setDeskNotice("synergy complete");
|
|
303
|
+
setWitticism(event.finalLine);
|
|
304
|
+
addItem("system", event.transcriptLine);
|
|
305
|
+
}
|
|
306
|
+
}, 60);
|
|
307
|
+
}, [addItem]);
|
|
308
|
+
|
|
237
309
|
const runLLM = useCallback(async (instruction?: string) => {
|
|
310
|
+
if (requestInFlightRef.current) return;
|
|
311
|
+
requestInFlightRef.current = true;
|
|
312
|
+
setRequestInFlight(true);
|
|
238
313
|
const startedAt = Date.now();
|
|
239
|
-
setThinking(pick(THINKING_LINES));
|
|
240
|
-
setDeskStatus("idle");
|
|
241
|
-
setDeskNotice(null);
|
|
242
|
-
setFallbackModel(null);
|
|
243
|
-
streamBufRef.current = "";
|
|
244
|
-
setStreaming(null);
|
|
245
|
-
let firstToken = true;
|
|
246
|
-
abortRef.current = new AbortController();
|
|
247
|
-
let result: Awaited<ReturnType<typeof streamChat>> | undefined;
|
|
248
|
-
let caughtErr: unknown = null;
|
|
249
314
|
try {
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
315
|
+
setThinking(pick(THINKING_LINES));
|
|
316
|
+
setDeskStatus("idle");
|
|
317
|
+
setDeskNotice(null);
|
|
318
|
+
setFallbackModel(null);
|
|
319
|
+
streamBufRef.current = "";
|
|
320
|
+
setStreaming(null);
|
|
321
|
+
let firstToken = true;
|
|
322
|
+
abortRef.current = new AbortController();
|
|
323
|
+
let result: Awaited<ReturnType<typeof streamChat>> | undefined;
|
|
324
|
+
let caughtErr: unknown = null;
|
|
325
|
+
try {
|
|
326
|
+
result = await streamChat({
|
|
327
|
+
apiKey: config.apiKey,
|
|
328
|
+
model,
|
|
329
|
+
fallbackModel: pickFallback(model),
|
|
330
|
+
messages: instruction
|
|
331
|
+
? [
|
|
332
|
+
...buildMessagesWithReminder(conversation),
|
|
333
|
+
{ role: "system", content: instruction },
|
|
334
|
+
]
|
|
335
|
+
: buildMessagesWithReminder(conversation),
|
|
336
|
+
onToken: (t) => {
|
|
337
|
+
if (!mountedRef.current || exitingRef.current) return;
|
|
338
|
+
if (firstToken) {
|
|
339
|
+
setThinking(null);
|
|
340
|
+
firstToken = false;
|
|
341
|
+
}
|
|
342
|
+
pushTokenToStream(t);
|
|
343
|
+
},
|
|
344
|
+
signal: abortRef.current.signal,
|
|
345
|
+
fetchFn,
|
|
346
|
+
});
|
|
347
|
+
} catch (err) {
|
|
348
|
+
caughtErr = err;
|
|
349
|
+
} finally {
|
|
350
|
+
if (streamTimerRef.current !== null) {
|
|
351
|
+
clearTimeout(streamTimerRef.current);
|
|
352
|
+
streamTimerRef.current = null;
|
|
353
|
+
}
|
|
354
|
+
abortRef.current = null;
|
|
355
|
+
}
|
|
356
|
+
if (!mountedRef.current || exitingRef.current) return;
|
|
357
|
+
if (caughtErr) {
|
|
358
|
+
const msg = caughtErr instanceof Error ? caughtErr.message : String(caughtErr);
|
|
359
|
+
setThinking(null);
|
|
360
|
+
setStreaming(null);
|
|
361
|
+
addItem("system", `${STREAM_ERROR} [${msg}]`);
|
|
362
|
+
setDeskStatus("error");
|
|
363
|
+
setDeskNotice(msg);
|
|
364
|
+
setMsgCount(conversation.length);
|
|
365
|
+
return;
|
|
277
366
|
}
|
|
278
|
-
abortRef.current = null;
|
|
279
|
-
}
|
|
280
|
-
if (!mountedRef.current) return;
|
|
281
|
-
if (caughtErr) {
|
|
282
|
-
const msg = caughtErr instanceof Error ? caughtErr.message : String(caughtErr);
|
|
283
367
|
setThinking(null);
|
|
284
368
|
setStreaming(null);
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
cancelledRef.current = false;
|
|
296
|
-
if (result?.content) {
|
|
369
|
+
setLastLatencyMs(Date.now() - startedAt);
|
|
370
|
+
if (cancelledRef.current) {
|
|
371
|
+
cancelledRef.current = false;
|
|
372
|
+
if (result?.content) {
|
|
373
|
+
conversation.push("assistant", result.content);
|
|
374
|
+
addItem("assistant", result.content);
|
|
375
|
+
}
|
|
376
|
+
addItem("system", "(cancelled — Drexler taking lunch)");
|
|
377
|
+
setDeskNotice("response cancelled");
|
|
378
|
+
} else if (result?.ok) {
|
|
297
379
|
conversation.push("assistant", result.content);
|
|
298
380
|
addItem("assistant", result.content);
|
|
381
|
+
const notices: string[] = [];
|
|
382
|
+
if (result.fellBack) {
|
|
383
|
+
addItem("system", `(fell back to ${result.modelUsed})`);
|
|
384
|
+
notices.push(`fallback ${result.modelUsed}`);
|
|
385
|
+
setFallbackModel(result.modelUsed);
|
|
386
|
+
}
|
|
387
|
+
if (detectPersonaDrift(result.content)) {
|
|
388
|
+
addItem("system", `(persona drift detected — model used 'I')`);
|
|
389
|
+
notices.push("persona drift detected");
|
|
390
|
+
}
|
|
391
|
+
setDeskNotice(notices.length > 0 ? notices.join(" · ") : null);
|
|
392
|
+
} else if (result?.interrupted) {
|
|
393
|
+
conversation.push("assistant", result.content);
|
|
394
|
+
addItem("assistant", result.content);
|
|
395
|
+
addItem("system", "(stream interrupted — partial response saved)");
|
|
396
|
+
setDeskStatus("error");
|
|
397
|
+
setDeskNotice("stream interrupted; partial response saved");
|
|
398
|
+
} else {
|
|
399
|
+
const detail = result?.error ? ` [${result.error}]` : "";
|
|
400
|
+
addItem("system", `${STREAM_ERROR}${detail}`);
|
|
401
|
+
setDeskStatus("error");
|
|
402
|
+
setDeskNotice(result?.error ?? "stream error");
|
|
299
403
|
}
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
addItem("system", `(fell back to ${result.modelUsed})`);
|
|
308
|
-
notices.push(`fallback ${result.modelUsed}`);
|
|
309
|
-
setFallbackModel(result.modelUsed);
|
|
310
|
-
}
|
|
311
|
-
if (detectPersonaDrift(result.content)) {
|
|
312
|
-
addItem("system", `(persona drift detected — model used 'I')`);
|
|
313
|
-
notices.push("persona drift detected");
|
|
404
|
+
setMsgCount(conversation.length);
|
|
405
|
+
setTokenCount(conversation.approximateTokens());
|
|
406
|
+
setWitticism(pick(WITTICISMS));
|
|
407
|
+
} finally {
|
|
408
|
+
requestInFlightRef.current = false;
|
|
409
|
+
if (mountedRef.current) {
|
|
410
|
+
setRequestInFlight(false);
|
|
314
411
|
}
|
|
315
|
-
setDeskNotice(notices.length > 0 ? notices.join(" · ") : null);
|
|
316
|
-
} else if (result?.interrupted) {
|
|
317
|
-
conversation.push("assistant", result.content);
|
|
318
|
-
addItem("assistant", result.content);
|
|
319
|
-
addItem("system", "(stream interrupted — partial response saved)");
|
|
320
|
-
setDeskStatus("error");
|
|
321
|
-
setDeskNotice("stream interrupted; partial response saved");
|
|
322
|
-
} else {
|
|
323
|
-
const detail = result?.error ? ` [${result.error}]` : "";
|
|
324
|
-
addItem("system", `${STREAM_ERROR}${detail}`);
|
|
325
|
-
setDeskStatus("error");
|
|
326
|
-
setDeskNotice(result?.error ?? "stream error");
|
|
327
412
|
}
|
|
328
|
-
setMsgCount(conversation.length);
|
|
329
|
-
setTokenCount(conversation.approximateTokens());
|
|
330
|
-
setWitticism(pick(WITTICISMS));
|
|
331
413
|
}, [
|
|
332
414
|
config,
|
|
333
415
|
model,
|
|
@@ -349,6 +431,10 @@ export function App({ conversation, config, mood = "neutral", fetchFn }: AppProp
|
|
|
349
431
|
},
|
|
350
432
|
});
|
|
351
433
|
const lower = line.toLowerCase().trim();
|
|
434
|
+
if (lower === "/synergy") {
|
|
435
|
+
runSynergyEvent();
|
|
436
|
+
return;
|
|
437
|
+
}
|
|
352
438
|
if (lower === "/clear" || lower.startsWith("/clear ")) {
|
|
353
439
|
setItems([]);
|
|
354
440
|
setLastLatencyMs(null);
|
|
@@ -399,12 +485,14 @@ export function App({ conversation, config, mood = "neutral", fetchFn }: AppProp
|
|
|
399
485
|
model,
|
|
400
486
|
removeLastAssistantItem,
|
|
401
487
|
runLLM,
|
|
488
|
+
runSynergyEvent,
|
|
402
489
|
triggerExit,
|
|
403
490
|
],
|
|
404
491
|
);
|
|
405
492
|
|
|
406
493
|
const onSubmit = useCallback(
|
|
407
494
|
async (raw: string) => {
|
|
495
|
+
if (requestInFlightRef.current || synergyActiveRef.current) return;
|
|
408
496
|
const line = raw.trim();
|
|
409
497
|
if (line === "") {
|
|
410
498
|
addItem("system", EMPTY_NUDGE);
|
|
@@ -424,8 +512,17 @@ export function App({ conversation, config, mood = "neutral", fetchFn }: AppProp
|
|
|
424
512
|
);
|
|
425
513
|
|
|
426
514
|
useInput((char, key) => {
|
|
427
|
-
|
|
515
|
+
const busy =
|
|
516
|
+
requestInFlightRef.current ||
|
|
517
|
+
synergyActiveRef.current ||
|
|
518
|
+
streaming !== null ||
|
|
519
|
+
thinking !== null ||
|
|
520
|
+
synergyEvent !== null;
|
|
521
|
+
if (busy) {
|
|
428
522
|
if (key.escape) {
|
|
523
|
+
if (synergyActiveRef.current || synergyEvent !== null) {
|
|
524
|
+
return;
|
|
525
|
+
}
|
|
429
526
|
cancelledRef.current = true;
|
|
430
527
|
abortRef.current?.abort();
|
|
431
528
|
return;
|
|
@@ -593,11 +690,20 @@ export function App({ conversation, config, mood = "neutral", fetchFn }: AppProp
|
|
|
593
690
|
if (exitTimerRef.current !== null) {
|
|
594
691
|
clearTimeout(exitTimerRef.current);
|
|
595
692
|
}
|
|
693
|
+
if (synergyTimerRef.current !== null) {
|
|
694
|
+
clearInterval(synergyTimerRef.current);
|
|
695
|
+
}
|
|
696
|
+
requestInFlightRef.current = false;
|
|
697
|
+
synergyActiveRef.current = false;
|
|
596
698
|
};
|
|
597
699
|
}, []);
|
|
598
700
|
|
|
599
|
-
const isBusy =
|
|
701
|
+
const isBusy =
|
|
702
|
+
requestInFlight || streaming !== null || thinking !== null || synergyEvent !== null;
|
|
600
703
|
const headerStatus = isBusy ? "streaming" : deskStatus;
|
|
704
|
+
const visibleTranscriptRows = synergyEvent
|
|
705
|
+
? Math.max(1, maxTranscriptRows - synergyEventRows(chromeWidth, isCompact))
|
|
706
|
+
: maxTranscriptRows;
|
|
601
707
|
|
|
602
708
|
return (
|
|
603
709
|
<ThemeProvider value={activeTheme}>
|
|
@@ -617,7 +723,7 @@ export function App({ conversation, config, mood = "neutral", fetchFn }: AppProp
|
|
|
617
723
|
/>
|
|
618
724
|
<TranscriptViewport
|
|
619
725
|
items={items}
|
|
620
|
-
maxRows={
|
|
726
|
+
maxRows={visibleTranscriptRows}
|
|
621
727
|
cols={chromeWidth}
|
|
622
728
|
compact={isCompact}
|
|
623
729
|
scrollOffset={scrollOffset}
|
|
@@ -634,6 +740,14 @@ export function App({ conversation, config, mood = "neutral", fetchFn }: AppProp
|
|
|
634
740
|
<Spinner label={thinking} width={chromeWidth} />
|
|
635
741
|
</Box>
|
|
636
742
|
)}
|
|
743
|
+
{synergyEvent !== null && (
|
|
744
|
+
<SynergyEvent
|
|
745
|
+
event={synergyEvent.event}
|
|
746
|
+
frame={synergyEvent.frame}
|
|
747
|
+
width={chromeWidth}
|
|
748
|
+
compact={isCompact}
|
|
749
|
+
/>
|
|
750
|
+
)}
|
|
637
751
|
{exitMsg !== null ? (
|
|
638
752
|
<Box paddingX={1} marginBottom={1}>
|
|
639
753
|
<Text color={t.primaryLight} bold>
|
|
@@ -654,6 +768,11 @@ export function App({ conversation, config, mood = "neutral", fetchFn }: AppProp
|
|
|
654
768
|
value={input}
|
|
655
769
|
cursor={cursor}
|
|
656
770
|
disabled={isBusy}
|
|
771
|
+
disabledLabel={
|
|
772
|
+
synergyEvent !== null
|
|
773
|
+
? "(Synergy event running... boardroom locked)"
|
|
774
|
+
: undefined
|
|
775
|
+
}
|
|
657
776
|
width={inputWidth}
|
|
658
777
|
/>
|
|
659
778
|
</Box>
|
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,300 @@
|
|
|
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
|
+
const FULL_EVENT_WIDTH = 88;
|
|
19
|
+
const FULL_EVENT_ROWS = 12;
|
|
20
|
+
const FULL_EVENT_ART_ROWS = 4;
|
|
21
|
+
|
|
22
|
+
export const SYNERGY_EVENTS: readonly SynergyEventDefinition[] = [
|
|
23
|
+
{
|
|
24
|
+
id: "alignment-protocol",
|
|
25
|
+
title: "ALIGNMENT PROTOCOL",
|
|
26
|
+
subtitle: "cross-functional theater detected",
|
|
27
|
+
art: [
|
|
28
|
+
" ██████╗ ██╗ ██╗███╗ ██╗",
|
|
29
|
+
" ██╔════╝ ╚██╗ ██╔╝████╗ ██║",
|
|
30
|
+
" ╚█████╗ ╚████╔╝ ██╔██╗ ██║",
|
|
31
|
+
" ╚═══██╗ ╚██╔╝ ██║╚██╗██║",
|
|
32
|
+
" ██████╔╝ ██║ ██║ ╚████║",
|
|
33
|
+
" ╚═════╝ ╚═╝ ╚═╝ ╚═══╝",
|
|
34
|
+
],
|
|
35
|
+
stages: [
|
|
36
|
+
"initiating alignment protocol",
|
|
37
|
+
"harmonizing action items",
|
|
38
|
+
"converting meetings into margin",
|
|
39
|
+
"minting provisional shareholder value",
|
|
40
|
+
],
|
|
41
|
+
kpis: [
|
|
42
|
+
"EBITDA +0.4%",
|
|
43
|
+
"morale provisionally approved",
|
|
44
|
+
"consultants +7",
|
|
45
|
+
"clarity -3",
|
|
46
|
+
],
|
|
47
|
+
finalLine:
|
|
48
|
+
"Synergy achieved. Headcount unchanged. Morale amortized.",
|
|
49
|
+
transcriptLine: "SYNERGY EVENT: shareholder value allegedly unlocked.",
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
id: "boardroom-alert",
|
|
53
|
+
title: "BOARDROOM ALERT",
|
|
54
|
+
subtitle: "value creation siren armed",
|
|
55
|
+
art: [
|
|
56
|
+
" [!] BOARDROOM ALERT",
|
|
57
|
+
" [!] ALIGNMENT DETECTED",
|
|
58
|
+
" [!] VALUE CREATION IMMINENT",
|
|
59
|
+
" [!] ASK NO FOLLOW-UP QUESTIONS",
|
|
60
|
+
],
|
|
61
|
+
stages: [
|
|
62
|
+
"paging senior stakeholders",
|
|
63
|
+
"escalating morale to committee",
|
|
64
|
+
"routing accountability offshore",
|
|
65
|
+
"closing the loop with no loop",
|
|
66
|
+
],
|
|
67
|
+
kpis: [
|
|
68
|
+
"risk committee awake",
|
|
69
|
+
"action items multiplying",
|
|
70
|
+
"status: billable",
|
|
71
|
+
"decision rights unclear",
|
|
72
|
+
],
|
|
73
|
+
finalLine: "Drexler approves synergy. Nobody asks what changed.",
|
|
74
|
+
transcriptLine: "SYNERGY EVENT: boardroom siren produced measurable vibes.",
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
id: "briefcase-cameo",
|
|
78
|
+
title: "BRIEFCASE CAMEO",
|
|
79
|
+
subtitle: "executive artifact opening",
|
|
80
|
+
art: [
|
|
81
|
+
" _________",
|
|
82
|
+
" _/ ___ \\_",
|
|
83
|
+
" | $ $ |",
|
|
84
|
+
" | ───┬─── |",
|
|
85
|
+
" |_____|_______|",
|
|
86
|
+
" / | \\",
|
|
87
|
+
],
|
|
88
|
+
stages: [
|
|
89
|
+
"unlocking sealed mandate",
|
|
90
|
+
"counting invisible efficiencies",
|
|
91
|
+
"deploying tasteful corporate sparkle",
|
|
92
|
+
"reclassifying excitement as asset",
|
|
93
|
+
],
|
|
94
|
+
kpis: [
|
|
95
|
+
"briefcase yield +12 bps",
|
|
96
|
+
"sparkle reserve funded",
|
|
97
|
+
"memo density rising",
|
|
98
|
+
"bonus pool unchanged",
|
|
99
|
+
],
|
|
100
|
+
finalLine: "Briefcase open. Synergy escaped. Legal says it was planned.",
|
|
101
|
+
transcriptLine: "SYNERGY EVENT: briefcase opened and released approved optimism.",
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
id: "achievement-unlocked",
|
|
105
|
+
title: "ACHIEVEMENT UNLOCKED",
|
|
106
|
+
subtitle: "cross-functional theater",
|
|
107
|
+
art: [
|
|
108
|
+
" ╔══════════════════╗",
|
|
109
|
+
" ║ DEAL TROPHY +1 ║",
|
|
110
|
+
" ╚══════════════════╝",
|
|
111
|
+
" Reward: meeting",
|
|
112
|
+
" Status: billable",
|
|
113
|
+
],
|
|
114
|
+
stages: [
|
|
115
|
+
"checking performance conditions",
|
|
116
|
+
"unlocking meeting about meeting",
|
|
117
|
+
"allocating credit to leadership",
|
|
118
|
+
"filing victory under recurring revenue",
|
|
119
|
+
],
|
|
120
|
+
kpis: [
|
|
121
|
+
"achievement: unlocked",
|
|
122
|
+
"reward: one calendar invite",
|
|
123
|
+
"prestige +8",
|
|
124
|
+
"substance pending",
|
|
125
|
+
],
|
|
126
|
+
finalLine: "Achievement unlocked: Cross-Functional Theater.",
|
|
127
|
+
transcriptLine: "SYNERGY EVENT: achievement unlocked, substance pending.",
|
|
128
|
+
},
|
|
129
|
+
];
|
|
130
|
+
|
|
131
|
+
export function pickSynergyEvent(
|
|
132
|
+
random: () => number = Math.random,
|
|
133
|
+
): SynergyEventDefinition {
|
|
134
|
+
const idx = Math.min(
|
|
135
|
+
SYNERGY_EVENTS.length - 1,
|
|
136
|
+
Math.floor(random() * SYNERGY_EVENTS.length),
|
|
137
|
+
);
|
|
138
|
+
return SYNERGY_EVENTS[idx]!;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function frameProgress(frame: number): number {
|
|
142
|
+
return Math.max(0, Math.min(1, frame / (SYNERGY_EVENT_FRAMES - 1)));
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function bar(progress: number, width: number): string {
|
|
146
|
+
const safeWidth = Math.max(1, width);
|
|
147
|
+
const filled = Math.max(0, Math.min(safeWidth, Math.round(progress * safeWidth)));
|
|
148
|
+
return `${"█".repeat(filled)}${"░".repeat(safeWidth - filled)}`;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function stageAt(event: SynergyEventDefinition, frame: number): string {
|
|
152
|
+
const progress = frameProgress(frame);
|
|
153
|
+
const idx = Math.min(
|
|
154
|
+
event.stages.length - 1,
|
|
155
|
+
Math.floor(progress * event.stages.length),
|
|
156
|
+
);
|
|
157
|
+
return event.stages[idx]!;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function visibleArt(event: SynergyEventDefinition, frame: number): readonly string[] {
|
|
161
|
+
const progress = frameProgress(frame);
|
|
162
|
+
const count = Math.max(
|
|
163
|
+
1,
|
|
164
|
+
Math.ceil(progress * Math.min(event.art.length, FULL_EVENT_ART_ROWS)),
|
|
165
|
+
);
|
|
166
|
+
return event.art.slice(0, count);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function kpiAt(event: SynergyEventDefinition, frame: number): string {
|
|
170
|
+
return event.kpis[Math.floor(frame / 3) % event.kpis.length]!;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
interface Props {
|
|
174
|
+
event: SynergyEventDefinition;
|
|
175
|
+
frame: number;
|
|
176
|
+
width?: number;
|
|
177
|
+
compact?: boolean;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function SynergyEventInner({
|
|
181
|
+
event,
|
|
182
|
+
frame,
|
|
183
|
+
width = 80,
|
|
184
|
+
compact = false,
|
|
185
|
+
}: Props) {
|
|
186
|
+
const t = useTheme();
|
|
187
|
+
const safeWidth = Math.max(1, Math.floor(width));
|
|
188
|
+
const progress = frameProgress(frame);
|
|
189
|
+
const done = frame >= SYNERGY_EVENT_FRAMES - 1;
|
|
190
|
+
const tiny = safeWidth < 38 || compact;
|
|
191
|
+
|
|
192
|
+
if (tiny) {
|
|
193
|
+
const label = done ? event.finalLine : stageAt(event, frame);
|
|
194
|
+
const miniBarWidth = Math.max(4, Math.min(18, safeWidth - 12));
|
|
195
|
+
const line = `SYNC ${bar(progress, miniBarWidth)} ${label}`;
|
|
196
|
+
return (
|
|
197
|
+
<Box width={safeWidth} flexShrink={1}>
|
|
198
|
+
<Text color={done ? t.primaryLight : t.warning} bold wrap="truncate">
|
|
199
|
+
{fitDisplayText(line, safeWidth)}
|
|
200
|
+
</Text>
|
|
201
|
+
</Box>
|
|
202
|
+
);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const panelWidth = Math.min(safeWidth, FULL_EVENT_WIDTH);
|
|
206
|
+
const innerWidth = Math.max(1, panelWidth - 4);
|
|
207
|
+
const title = `${event.title} · ${event.subtitle}`;
|
|
208
|
+
const progressWidth = Math.max(8, Math.min(34, innerWidth - 18));
|
|
209
|
+
const progressPct = `${Math.round(progress * 100)
|
|
210
|
+
.toString()
|
|
211
|
+
.padStart(3, " ")}%`;
|
|
212
|
+
const artWidth = Math.max(1, innerWidth - 4);
|
|
213
|
+
const kpi = kpiAt(event, frame);
|
|
214
|
+
|
|
215
|
+
return (
|
|
216
|
+
<Box width={safeWidth} justifyContent="center" flexShrink={1}>
|
|
217
|
+
<Box
|
|
218
|
+
flexDirection="column"
|
|
219
|
+
borderStyle="round"
|
|
220
|
+
borderColor={done ? t.primaryLight : t.warning}
|
|
221
|
+
paddingX={1}
|
|
222
|
+
width={panelWidth}
|
|
223
|
+
flexShrink={1}
|
|
224
|
+
>
|
|
225
|
+
<Box>
|
|
226
|
+
<Text color={t.warning} bold>
|
|
227
|
+
SYNERGY EVENT
|
|
228
|
+
</Text>
|
|
229
|
+
<Text color={t.primaryDim}> ─ </Text>
|
|
230
|
+
<Text color={t.dim} wrap="truncate">
|
|
231
|
+
{fitDisplayText(title, Math.max(1, innerWidth - 18))}
|
|
232
|
+
</Text>
|
|
233
|
+
</Box>
|
|
234
|
+
<Box flexDirection="column">
|
|
235
|
+
{event.art.slice(0, FULL_EVENT_ART_ROWS).map((line, idx) => {
|
|
236
|
+
const revealed = idx < visibleArt(event, frame).length;
|
|
237
|
+
return (
|
|
238
|
+
<Text
|
|
239
|
+
key={`${event.id}-${idx}`}
|
|
240
|
+
color={revealed ? t.primaryLight : t.primaryDim}
|
|
241
|
+
wrap="truncate"
|
|
242
|
+
>
|
|
243
|
+
{revealed ? fitDisplayText(line, artWidth) : " "}
|
|
244
|
+
</Text>
|
|
245
|
+
);
|
|
246
|
+
})}
|
|
247
|
+
</Box>
|
|
248
|
+
<Box>
|
|
249
|
+
<Text color={t.primaryDim}>[</Text>
|
|
250
|
+
<Text color={done ? t.primaryLight : t.warning}>
|
|
251
|
+
{bar(progress, progressWidth)}
|
|
252
|
+
</Text>
|
|
253
|
+
<Text color={t.primaryDim}>] </Text>
|
|
254
|
+
<Text color={t.dim}>{progressPct}</Text>
|
|
255
|
+
</Box>
|
|
256
|
+
<Box>
|
|
257
|
+
<Text color={t.primaryLight} bold>
|
|
258
|
+
◆{" "}
|
|
259
|
+
</Text>
|
|
260
|
+
<Text color={t.text} wrap="truncate">
|
|
261
|
+
{fitDisplayText(stageAt(event, frame), Math.max(1, innerWidth - 4))}
|
|
262
|
+
</Text>
|
|
263
|
+
</Box>
|
|
264
|
+
<Box>
|
|
265
|
+
<Text color={t.primaryDim}>ticker </Text>
|
|
266
|
+
<Text color={t.warning} wrap="truncate">
|
|
267
|
+
{fitDisplayText(kpi, Math.max(1, innerWidth - 9))}
|
|
268
|
+
</Text>
|
|
269
|
+
</Box>
|
|
270
|
+
<Box>
|
|
271
|
+
<Text color={done ? t.primaryLight : t.primaryDim} bold={done} wrap="truncate">
|
|
272
|
+
{fitDisplayText(
|
|
273
|
+
done ? event.finalLine : "awaiting committee approval...",
|
|
274
|
+
Math.max(1, innerWidth - 2),
|
|
275
|
+
)}
|
|
276
|
+
</Text>
|
|
277
|
+
</Box>
|
|
278
|
+
</Box>
|
|
279
|
+
</Box>
|
|
280
|
+
);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
export const SynergyEvent = memo(SynergyEventInner);
|
|
284
|
+
|
|
285
|
+
export function synergyEventRows(width: number, compact = false): number {
|
|
286
|
+
if (compact || width < 38) return 1;
|
|
287
|
+
return FULL_EVENT_ROWS;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
export function synergyEventMaxRowWidth(
|
|
291
|
+
rendered: string,
|
|
292
|
+
stripAnsi: (s: string) => string = (s) => s,
|
|
293
|
+
): number {
|
|
294
|
+
return Math.max(
|
|
295
|
+
0,
|
|
296
|
+
...stripAnsi(rendered)
|
|
297
|
+
.split("\n")
|
|
298
|
+
.map((row) => displayWidth(row)),
|
|
299
|
+
);
|
|
300
|
+
}
|