drexler 0.2.19 → 0.2.21

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "drexler",
3
- "version": "0.2.19",
3
+ "version": "0.2.21",
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/ui/App.tsx CHANGED
@@ -910,6 +910,21 @@ export function App({
910
910
  return;
911
911
  }
912
912
  if (isSlash(line)) {
913
+ // Bare /theme, /model, /startup, /retry, /export — repopulate the
914
+ // input with "<cmd> " so the palette catches the argument chooser
915
+ // and the user picks via ↑↓ + Enter. Avoids the "print current
916
+ // value and dead-end" feeling of dispatching the base command.
917
+ const lower = line.toLowerCase();
918
+ if (isArgumentParentCommand(lower)) {
919
+ const filled = `${lower} `;
920
+ updateDraft({ value: filled, cursor: graphemeLength(filled) });
921
+ setPaletteIdx(0);
922
+ addItem(
923
+ "system",
924
+ `Pick a ${lower.slice(1)} option below — ↑↓ to choose, Enter to apply, Esc to cancel.`,
925
+ );
926
+ return;
927
+ }
913
928
  await handleSlashWithMutation(line);
914
929
  return;
915
930
  }
@@ -918,7 +933,14 @@ export function App({
918
933
  setMsgCount(conversation.length);
919
934
  await runLLM();
920
935
  },
921
- [addItem, conversation, handleSlashWithMutation, runLLM],
936
+ [
937
+ addItem,
938
+ conversation,
939
+ handleSlashWithMutation,
940
+ runLLM,
941
+ setPaletteIdx,
942
+ updateDraft,
943
+ ],
922
944
  );
923
945
 
924
946
  const reportSubmitError = useCallback(
@@ -171,7 +171,7 @@ const RIGHT_COLUMN_PAD_RIGHT = 1;
171
171
  const LEFT_PANEL_MIN_COPY = 24;
172
172
  const PET_STATS_MIN_WIDTH = 24;
173
173
  const PET_STATS_MAX_WIDTH = 58;
174
- const PET_SPLIT_DIVIDER_HEIGHT = 15;
174
+ const PET_SPLIT_DIVIDER_HEIGHT = 19;
175
175
  const PET_SPLIT_DIVIDER_ROWS: number[] = Array.from(
176
176
  { length: PET_SPLIT_DIVIDER_HEIGHT },
177
177
  (_, i) => i,
@@ -18,14 +18,17 @@ export type Environment = "office" | "home" | "outdoors";
18
18
 
19
19
  const PANEL_BORDER_COLUMNS = 2;
20
20
  const PANEL_PADDING_COLUMNS = 2;
21
- const SCENE_ROWS = 14;
21
+ const SCENE_ROWS = 18;
22
22
  const R_WALL = 0;
23
23
  const R_WINDOW_TOP = 1;
24
- const R_WINDOW_BOTTOM = 3;
25
- const R_ACTIVITY = 4;
26
- const R_MASCOT_START = 5;
24
+ const R_WINDOW_BOTTOM = 4;
25
+ const R_ACTIVITY = 5;
26
+ const R_MASCOT_START = 6;
27
27
  const R_DESK_SURFACE = R_MASCOT_START + BRIEFCASE_FINAL.length;
28
28
  const R_DESK_FRONT = R_DESK_SURFACE + 1;
29
+ const R_DESK_DRAWERS = R_DESK_FRONT + 1;
30
+ const R_DESK_BOTTOM = R_DESK_DRAWERS + 1;
31
+ const R_FLOOR = R_DESK_BOTTOM + 1;
29
32
 
30
33
  export const PET_SCENE_WIDTH = 52;
31
34
 
@@ -36,6 +39,14 @@ function place(base: string, text: string, x: number): string {
36
39
  return base.slice(0, x) + fit + base.slice(end);
37
40
  }
38
41
 
42
+ function placeSprite(rows: string[], row: number, x: number, sprite: readonly string[]): void {
43
+ for (let i = 0; i < sprite.length; i++) {
44
+ const targetRow = row + i;
45
+ if (targetRow < 0 || targetRow >= rows.length) continue;
46
+ rows[targetRow] = place(rows[targetRow] ?? "", sprite[i] ?? "", x);
47
+ }
48
+ }
49
+
39
50
  function blankRow(width: number): string {
40
51
  return " ".repeat(width);
41
52
  }
@@ -46,6 +57,13 @@ function padDisplayText(input: string, width: number): string {
46
57
  return `${fitted}${" ".repeat(Math.max(0, safeWidth - displayWidth(fitted)))}`;
47
58
  }
48
59
 
60
+ function centerPadDisplayText(input: string, width: number): string {
61
+ const safeWidth = Math.max(1, width);
62
+ const fitted = fitDisplayText(input, safeWidth);
63
+ const left = Math.max(0, Math.floor((safeWidth - displayWidth(fitted)) / 2));
64
+ return `${" ".repeat(left)}${fitted}${" ".repeat(Math.max(0, safeWidth - left - displayWidth(fitted)))}`;
65
+ }
66
+
49
67
  function overlayFitted(row: string, text: string, x: number, width: number): string {
50
68
  return place(row, padDisplayText(text, width), x);
51
69
  }
@@ -73,23 +91,33 @@ function sceneBoxBottom(width: number): string {
73
91
  return `╰${"─".repeat(safeWidth - 2)}╯`;
74
92
  }
75
93
 
76
- function placeSceneBox(
94
+ function placeBoxLines(
77
95
  rows: string[],
78
96
  row: number,
79
97
  x: number,
80
98
  width: number,
81
99
  title: string,
82
- body: string,
100
+ body: readonly string[],
83
101
  ): void {
84
102
  rows[row] = place(rows[row] ?? "", sceneBoxTop(title, width), x);
85
- rows[row + 1] = place(rows[row + 1] ?? "", sceneBoxBody(body, width), x);
86
- rows[row + 2] = place(rows[row + 2] ?? "", sceneBoxBottom(width), x);
103
+ for (let i = 0; i < body.length; i++) {
104
+ rows[row + i + 1] = place(
105
+ rows[row + i + 1] ?? "",
106
+ sceneBoxBody(body[i] ?? "", width),
107
+ x,
108
+ );
109
+ }
110
+ rows[row + body.length + 1] = place(
111
+ rows[row + body.length + 1] ?? "",
112
+ sceneBoxBottom(width),
113
+ x,
114
+ );
87
115
  }
88
116
 
89
117
  function cupForEnergy(energy: number): string {
90
- if (energy > 60) return "coffee [c~]";
91
- if (energy > 30) return "coffee [c-]";
92
- return "coffee [c_]";
118
+ if (energy > 60) return "c~";
119
+ if (energy > 30) return "c-";
120
+ return "c_";
93
121
  }
94
122
 
95
123
  function progressTicker(frame: number): string {
@@ -196,27 +224,81 @@ function drawOfficeBackground(rows: string[], width: number, frame: number, stat
196
224
  Math.max(0, width - displayWidth(`pipe ${dealPct}`) - 1),
197
225
  );
198
226
 
199
- const windowWidth = Math.min(22, Math.max(16, Math.floor(width * 0.36)));
200
- const boardWidth = Math.min(28, Math.max(20, width - windowWidth - 6));
201
- const boardX = Math.max(windowWidth + 4, width - boardWidth - 2);
202
- const marketPulse = frame % 10 < 5 ? "market tape + + +" : "market tape + +";
203
-
204
- placeSceneBox(rows, R_WINDOW_TOP, 1, windowWidth, "Market Window", marketPulse);
205
- placeSceneBox(
227
+ const compact = width < 62;
228
+ const windowWidth = compact
229
+ ? 18
230
+ : Math.min(30, Math.max(20, Math.floor(width * 0.32)));
231
+ const boardWidth = compact
232
+ ? Math.min(26, Math.max(20, width - windowWidth - 5))
233
+ : Math.min(36, Math.max(26, Math.floor(width * 0.36)));
234
+ const boardX = Math.max(windowWidth + 3, width - boardWidth - 2);
235
+ const windowRight = 1 + windowWidth;
236
+ const gapWidth = boardX - windowRight;
237
+ const cloud = frame % 6 < 3 ? "(~~)" : " (~~)";
238
+ const sun = frame % 12 < 6 ? "\\o/" : "-o-";
239
+ const city = frame % 10 < 5 ? "▂▄▆ city" : "▃▅▇ city";
240
+ const tape = frame % 8 < 4 ? "▁▃▅▇" : "▂▄▆█";
241
+ const cursor = frame % 4 < 2 ? ">" : "*";
242
+
243
+ placeBoxLines(rows, R_WINDOW_TOP, 1, windowWidth, "Window", [
244
+ `╔╤╤╗ ${sun} ${cloud}`,
245
+ `║▥▥║ ${city}`,
246
+ ]);
247
+ placeBoxLines(
206
248
  rows,
207
249
  R_WINDOW_TOP,
208
250
  boardX,
209
251
  Math.min(boardWidth, width - boardX),
210
252
  "Deal Board",
211
- `DL:${dealPct} fees:${Math.round(stats.happiness)}%`,
253
+ [
254
+ `DL ${dealPct} FEE ${Math.round(stats.happiness).toString().padStart(3)}%`,
255
+ `PIPE ${tape} $ ${cursor}`,
256
+ ],
212
257
  );
213
258
 
259
+ if (gapWidth >= 7) {
260
+ const clockX = windowRight + Math.floor((gapWidth - 5) / 2);
261
+ const hour = frame % 8 < 4 ? "09" : "10";
262
+ placeSprite(rows, R_WINDOW_TOP, clockX, [
263
+ "╭──╮",
264
+ `│${hour}│`,
265
+ "╰──╯",
266
+ ]);
267
+ }
268
+
214
269
  rows[R_ACTIVITY] = centerText(
215
270
  "─".repeat(width),
216
271
  buildActivityLine("idle", frame),
217
272
  );
218
273
  }
219
274
 
275
+ function drawOfficeFurniture(rows: string[], width: number, frame: number): void {
276
+ const lampX = 1;
277
+ const cabinetX = Math.max(1, width - 8);
278
+ const shade = frame % 8 < 4 ? "╭░░░░╮" : "╭▒▒▒▒╮";
279
+ const plantTop = frame % 6 < 3 ? " ╲│╱ " : " ╱│╲ ";
280
+
281
+ placeSprite(rows, R_MASCOT_START, lampX, [
282
+ ` ${shade} `,
283
+ " ╱▒▒▒▒╲",
284
+ " ╰─┬──╯",
285
+ " │ ",
286
+ " ╭─┴─╮ ",
287
+ " │IN │ ",
288
+ " ╰───╯ ",
289
+ ]);
290
+
291
+ placeSprite(rows, R_MASCOT_START, cabinetX, [
292
+ plantTop,
293
+ " ╲│╱ ",
294
+ " ╰┬╯ ",
295
+ "╭FILE╮",
296
+ "│▤▤▤│",
297
+ "├────┤",
298
+ "│▤▤▤│",
299
+ ]);
300
+ }
301
+
220
302
  function drawActivityAccents(
221
303
  rows: string[],
222
304
  width: number,
@@ -230,35 +312,38 @@ function drawActivityAccents(
230
312
  );
231
313
 
232
314
  const mascotRight = mascotX + MASCOT_WIDTH;
233
- const leftAccentX = Math.max(1, mascotX - 10);
234
- const rightAccentX = Math.min(width - 10, mascotRight + 2);
315
+ const leftAccentX = Math.max(1, mascotX - 3);
316
+ const fileX = Math.max(1, width - 8);
317
+ const rightAccentX = Math.min(fileX - 6, mascotRight + 2);
235
318
 
236
319
  switch (activity) {
237
320
  case "eating":
238
- rows[R_DESK_SURFACE] = place(rows[R_DESK_SURFACE], "[$ memo]", rightAccentX);
321
+ rows[R_MASCOT_START + 5] = place(
322
+ rows[R_MASCOT_START + 5],
323
+ "╭$╮",
324
+ Math.max(1, Math.min(fileX - 5, rightAccentX + 1)),
325
+ );
239
326
  break;
240
327
  case "playing":
241
328
  rows[R_MASCOT_START + 2] = place(rows[R_MASCOT_START + 2], "*", leftAccentX);
242
- rows[R_MASCOT_START + 2] = place(rows[R_MASCOT_START + 2], "*", rightAccentX + 6);
329
+ rows[R_MASCOT_START + 2] = place(rows[R_MASCOT_START + 2], "*", Math.min(fileX - 2, rightAccentX + 6));
243
330
  break;
244
331
  case "working":
245
332
  rows[R_MASCOT_START + 1] = place(rows[R_MASCOT_START + 1], "$", leftAccentX);
246
- rows[R_MASCOT_START + 3] = place(rows[R_MASCOT_START + 3], "$", rightAccentX + 6);
333
+ rows[R_MASCOT_START + 3] = place(rows[R_MASCOT_START + 3], "$", Math.min(fileX - 2, rightAccentX + 6));
247
334
  break;
248
335
  case "sleeping":
249
336
  rows[R_MASCOT_START] = place(rows[R_MASCOT_START], "z z Z", rightAccentX);
250
337
  break;
251
338
  case "praised":
252
339
  rows[R_MASCOT_START + 1] = place(rows[R_MASCOT_START + 1], "* *", leftAccentX);
253
- rows[R_MASCOT_START + 1] = place(rows[R_MASCOT_START + 1], "* *", rightAccentX + 4);
340
+ rows[R_MASCOT_START + 1] = place(rows[R_MASCOT_START + 1], "* *", Math.min(fileX - 4, rightAccentX + 4));
254
341
  break;
255
342
  case "vibing":
256
343
  rows[R_MASCOT_START + 3] = place(rows[R_MASCOT_START + 3], "~ ~", leftAccentX);
257
- rows[R_MASCOT_START + 3] = place(rows[R_MASCOT_START + 3], "~ ~", rightAccentX + 4);
344
+ rows[R_MASCOT_START + 3] = place(rows[R_MASCOT_START + 3], "~ ~", Math.min(fileX - 4, rightAccentX + 4));
258
345
  break;
259
346
  default:
260
- rows[R_MASCOT_START + 2] = place(rows[R_MASCOT_START + 2], "[IN]", 2);
261
- rows[R_MASCOT_START + 2] = place(rows[R_MASCOT_START + 2], "[OUT]", Math.max(2, width - 8));
262
347
  break;
263
348
  }
264
349
  }
@@ -276,12 +361,57 @@ function drawMascot(rows: string[], width: number, activity: PetActivity, frame:
276
361
  return mascotX;
277
362
  }
278
363
 
364
+ function drawDesktopObjects(
365
+ rows: string[],
366
+ width: number,
367
+ activity: PetActivity,
368
+ frame: number,
369
+ stats: PetStats,
370
+ ): void {
371
+ const mascotX = Math.max(0, Math.floor((width - MASCOT_WIDTH) / 2));
372
+ const mascotRight = mascotX + MASCOT_WIDTH;
373
+ const cabinetX = Math.max(1, width - 8);
374
+ const laptopX = Math.max(8, mascotX - 9);
375
+ const papersX = Math.min(cabinetX - 10, mascotRight + 2);
376
+ const mugX = Math.min(cabinetX - 5, mascotRight + 6);
377
+ const cursor = frame % 2 === 0 ? "_" : " ";
378
+ const screen =
379
+ activity === "working"
380
+ ? `$>${cursor}DL`
381
+ : activity === "sleeping"
382
+ ? "zzz..."
383
+ : "DREX";
384
+ const steam = stats.energy > 30
385
+ ? frame % 4 < 2 ? " ((" : " ))"
386
+ : " ";
387
+ const paperFace = frame % 6 < 3 ? "▱▱▱" : "▰▱▱";
388
+
389
+ placeSprite(rows, R_MASCOT_START + 4, laptopX, [
390
+ "╭──────╮",
391
+ `│${padDisplayText(screen, 6)}│`,
392
+ "╰─┬──┬─╯",
393
+ ]);
394
+
395
+ if (papersX > mascotRight) {
396
+ rows[R_MASCOT_START + 6] = place(rows[R_MASCOT_START + 6], paperFace, papersX);
397
+ }
398
+
399
+ if (mugX > mascotRight + 1) {
400
+ rows[R_MASCOT_START + 4] = place(rows[R_MASCOT_START + 4], steam, mugX + 1);
401
+ rows[R_MASCOT_START + 5] = place(rows[R_MASCOT_START + 5], "╭─╮", mugX);
402
+ rows[R_MASCOT_START + 6] = place(rows[R_MASCOT_START + 6], `╰${cupForEnergy(stats.energy)}╯`, mugX);
403
+ }
404
+ }
405
+
279
406
  function drawDesk(rows: string[], width: number, stats: PetStats): void {
280
407
  const deskX = width > PET_SCENE_WIDTH ? 2 : 1;
281
408
  const deskWidth = Math.max(4, width - deskX * 2);
282
409
  const deskInner = Math.max(1, deskWidth - 2);
283
- const surface = `laptop [▣] papers [///] ${cupForEnergy(stats.energy)}`;
284
- const front = `DESK pipeline ${Math.round(stats.deals)}% covenants OK`;
410
+ const surface = `▱▱▱ [${cupForEnergy(stats.energy)}] ▬▬▬▬▬ COV OK`;
411
+ const front = `[IN] ║ DREXLER DEAL DESK ║ PIPE ${Math.round(stats.deals)}% ║ [OUT]`;
412
+ const drawers = width < 68
413
+ ? "╭────╮ ╭────╮ ╭────╮"
414
+ : "╭────╮ ╭────╮ ╭────╮ ╭────╮";
285
415
 
286
416
  rows[R_DESK_SURFACE] = place(
287
417
  rows[R_DESK_SURFACE],
@@ -290,9 +420,23 @@ function drawDesk(rows: string[], width: number, stats: PetStats): void {
290
420
  );
291
421
  rows[R_DESK_FRONT] = place(
292
422
  rows[R_DESK_FRONT],
293
- `╰${padDisplayText(front, deskInner)}╯`,
423
+ `│${padDisplayText(front, deskInner)}│`,
424
+ deskX,
425
+ );
426
+ rows[R_DESK_DRAWERS] = place(
427
+ rows[R_DESK_DRAWERS],
428
+ `│${centerPadDisplayText(drawers, deskInner)}│`,
429
+ deskX,
430
+ );
431
+ rows[R_DESK_BOTTOM] = place(
432
+ rows[R_DESK_BOTTOM],
433
+ `╰${"─".repeat(Math.max(0, deskInner))}╯`,
294
434
  deskX,
295
435
  );
436
+ rows[R_FLOOR] = centerText(
437
+ rows[R_FLOOR],
438
+ fitDisplayText("░░░░░░░ deal-room carpet shadow ░░░░░░░", width),
439
+ );
296
440
  }
297
441
 
298
442
  function buildScene(
@@ -305,8 +449,10 @@ function buildScene(
305
449
  const rows: string[] = Array.from({ length: SCENE_ROWS }, () => blankRow(sceneWidth));
306
450
 
307
451
  drawOfficeBackground(rows, sceneWidth, frame, stats);
308
- drawDesk(rows, sceneWidth, stats);
452
+ drawOfficeFurniture(rows, sceneWidth, frame);
309
453
  const mascotX = drawMascot(rows, sceneWidth, activity, frame);
454
+ drawDesktopObjects(rows, sceneWidth, activity, frame, stats);
455
+ drawDesk(rows, sceneWidth, stats);
310
456
  drawActivityAccents(rows, sceneWidth, activity, frame, mascotX);
311
457
  return rows.map((row) => overlayFitted(blankRow(sceneWidth), row, 0, sceneWidth));
312
458
  }
@@ -329,7 +475,13 @@ function rowColor(i: number, activity: PetActivity, frame: number, t: Theme): st
329
475
  if (activity === "playing") return t.primaryLight;
330
476
  return t.primaryLight;
331
477
  }
332
- if (i === R_DESK_SURFACE || i === R_DESK_FRONT) return t.primaryDim;
478
+ if (
479
+ i === R_DESK_SURFACE ||
480
+ i === R_DESK_FRONT ||
481
+ i === R_DESK_DRAWERS ||
482
+ i === R_DESK_BOTTOM ||
483
+ i === R_FLOOR
484
+ ) return t.primaryDim;
333
485
  if (i === R_WALL) return t.dim;
334
486
  if (i >= R_WINDOW_TOP && i <= R_WINDOW_BOTTOM) return t.primaryDim;
335
487
  return t.primaryDim;