drexler 0.2.5 → 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 CHANGED
@@ -1,5 +1,10 @@
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
+
3
8
  ## 0.2.5
4
9
 
5
10
  - 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "drexler",
3
- "version": "0.2.5",
3
+ "version": "0.2.6",
4
4
  "description": "CLI chat with Drexler, a corporate-executive AI persona built on OpenRouter Gemma 4 31B.",
5
5
  "license": "MIT",
6
6
  "author": "showOS",
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("SYNERGY. You promoted. Award: continued employment.");
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,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={maxTranscriptRows}
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>
@@ -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({ value, cursor, disabled, width }: Props) {
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
+ }