drexler 0.2.12 → 0.2.14

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.
@@ -1,5 +1,6 @@
1
1
  import { Box, Text } from "ink";
2
2
  import { memo, useMemo } from "react";
3
+ import { displayWidth, fitDisplayText } from "./graphemes.ts";
3
4
  import { useTheme } from "./ThemeContext.tsx";
4
5
 
5
6
  export type DealDeskHeaderStatus = "idle" | "streaming" | "error";
@@ -23,31 +24,96 @@ const DEFAULT_WIDTH = 80;
23
24
  const MIN_WIDTH = 1;
24
25
  const FRAMED_MIN_WIDTH = 24;
25
26
 
26
- const STATUS_LABEL: Record<DealDeskHeaderStatus, string> = {
27
- idle: "READY",
28
- streaming: "LIVE",
29
- error: "ERROR",
27
+ const BOARDROOM_STATUS: Record<DealDeskHeaderStatus, string> = {
28
+ idle: "BOARDROOM OPEN",
29
+ streaming: "MEMO LIVE",
30
+ error: "COUNSEL PANIC",
30
31
  };
31
32
 
32
- function visibleLength(input: string): number {
33
- return Array.from(input).length;
34
- }
33
+ type DealDeskPoolKey =
34
+ | "fees"
35
+ | "mandate"
36
+ | "risk"
37
+ | "counsel"
38
+ | "morale"
39
+ | "synergy";
40
+
41
+ const DEFAULT_POOL: Record<DealDeskPoolKey, readonly string[]> = {
42
+ fees: ["accruing", "sacred", "non-refundable", "already billed"],
43
+ mandate: ["self-awarded", "board-adjacent", "strategic-ish", "lightly authorized"],
44
+ risk: ["theatrical", "outsourced", "priced in", "someone else's"],
45
+ counsel: ["circling", "evasive", "comfortable", "redlining lunch"],
46
+ morale: ["impaired", "marked down", "technically solvent", "under review"],
47
+ synergy: ["alleged", "unverifiable", "already billed", "pending lawsuit"],
48
+ };
49
+
50
+ const MOOD_POOLS: Record<
51
+ string,
52
+ Partial<Record<DealDeskPoolKey, readonly string[]>>
53
+ > = {
54
+ angry: {
55
+ fees: ["weaponized", "escalating", "aggressively earned", "non-refundable"],
56
+ mandate: ["hostile", "loudly implied", "board-threatening", "self-ratified"],
57
+ risk: ["acceptable", "transferred", "career-limiting", "somebody else's"],
58
+ counsel: ["circling", "sweating", "denying knowledge", "overruled"],
59
+ morale: ["terminated", "impaired", "written off", "reassigned"],
60
+ synergy: ["forced", "mandatory", "hostile", "already billed"],
61
+ },
62
+ exhausted: {
63
+ fees: ["still accruing", "quietly sacred", "tired but billable", "unquestioned"],
64
+ mandate: ["unclear", "half-approved", "forgotten", "pending coffee"],
65
+ risk: ["deferred", "sleepy", "filed tomorrow", "emotionally hedged"],
66
+ counsel: ["unavailable", "out of office", "blinking slowly", "circling"],
67
+ morale: ["written off", "napping", "below guidance", "technically awake"],
68
+ synergy: ["alleged", "too tired to verify", "softly promised", "unfunded"],
69
+ },
70
+ paranoid: {
71
+ fees: ["traced", "escrowed twice", "suspiciously round", "under seal"],
72
+ mandate: ["encrypted", "deniable", "need-to-know", "redacted"],
73
+ risk: ["everywhere", "listening", "unhedged", "wearing a wire"],
74
+ counsel: ["whispering", "triple-checking", "using burner phones", "redacting"],
75
+ morale: ["surveilled", "compartmentalized", "need-to-know", "encrypted"],
76
+ synergy: ["classified", "denied", "redacted", "not in minutes"],
77
+ },
78
+ generous: {
79
+ fees: ["shared emotionally", "still ours", "politely accruing", "gift-wrapped"],
80
+ mandate: ["benevolent", "magnanimous", "soft hostile", "board-blessed"],
81
+ risk: ["forgiven", "socialized", "gently transferred", "nicely hedged"],
82
+ counsel: ["agreeable", "smiling carefully", "comfortable", "charitable"],
83
+ morale: ["briefly up", "subsidized", "pleasantly marked", "gifted options"],
84
+ synergy: ["donated", "mutual-ish", "kindly alleged", "complimentary"],
85
+ },
86
+ ruthless: {
87
+ fees: ["sacred", "extractive", "fully captured", "compounding"],
88
+ mandate: ["hostile", "absolute", "self-awarded", "non-appealable"],
89
+ risk: ["outsourced", "priced in", "assigned to interns", "deleted"],
90
+ counsel: ["overpaid", "comfortable", "aggressively calm", "circling"],
91
+ morale: ["impaired", "irrelevant", "restructured", "sold separately"],
92
+ synergy: ["mandatory", "already billed", "non-consensual", "accretive"],
93
+ },
94
+ victorious: {
95
+ fees: ["captured", "celebrated", "fully earned", "ringing bell"],
96
+ mandate: ["ratified", "triumphant", "board-crowned", "unopposed"],
97
+ risk: ["conquered", "renamed upside", "priced in", "defeated"],
98
+ counsel: ["applauding", "comfortable", "drafting trophies", "filing confetti"],
99
+ morale: ["temporarily high", "marked up", "wearing medals", "overstated"],
100
+ synergy: ["declared", "victorious", "already billed", "banner-ready"],
101
+ },
102
+ };
35
103
 
36
104
  function clampText(input: string, max: number): string {
37
105
  if (max <= 0) return "";
38
- if (visibleLength(input) <= max) return input;
39
- if (max === 1) return "…";
40
- return `${Array.from(input).slice(0, max - 1).join("")}…`;
106
+ return fitDisplayText(input, max);
41
107
  }
42
108
 
43
109
  function padToWidth(input: string, width: number): string {
44
- const len = visibleLength(input);
110
+ const len = displayWidth(input);
45
111
  if (len >= width) return input;
46
112
  return `${input}${" ".repeat(width - len)}`;
47
113
  }
48
114
 
49
115
  function shellLine(left: string, right: string, width: number): string {
50
- const available = Math.max(0, width - visibleLength(left) - visibleLength(right));
116
+ const available = Math.max(0, width - displayWidth(left) - displayWidth(right));
51
117
  return `${left}${"─".repeat(available)}${right}`;
52
118
  }
53
119
 
@@ -56,98 +122,194 @@ function bodyLine(content: string, width: number): string {
56
122
  return `│ ${padToWidth(clampText(content, innerWidth), innerWidth)} │`;
57
123
  }
58
124
 
59
- function countLabel(messageCount: number, compact: boolean): string {
60
- if (compact) return `${messageCount} msg${messageCount === 1 ? "" : "s"}`;
61
- return `${messageCount} message${messageCount === 1 ? "" : "s"}`;
62
- }
63
-
64
- function latencyLabel(latencyMs: number | null | undefined): string | null {
65
- if (typeof latencyMs !== "number") return null;
66
- if (latencyMs < 1000) return `${Math.max(0, Math.round(latencyMs))}ms`;
67
- return `${(latencyMs / 1000).toFixed(1)}s`;
125
+ function memoLabel(messageCount: number): string {
126
+ const noun = "memo";
127
+ return `${messageCount} ${noun}${messageCount === 1 ? "" : "s"}`;
68
128
  }
69
129
 
70
130
  function tinyLine({
71
- model,
72
131
  messageCount,
73
132
  status,
74
133
  width,
75
134
  }: {
76
- model: string;
77
135
  messageCount: number;
78
136
  status: DealDeskHeaderStatus;
79
137
  width: number;
80
138
  }): string {
81
139
  return clampText(
82
- `${STATUS_LABEL[status]} ${countLabel(messageCount, true)} ${model}`,
140
+ `${BOARDROOM_STATUS[status]} ${memoLabel(messageCount)}`,
83
141
  width,
84
142
  );
85
143
  }
86
144
 
145
+ function pickFromMoodPool({
146
+ key,
147
+ mood,
148
+ salt,
149
+ }: {
150
+ key: DealDeskPoolKey;
151
+ mood: string;
152
+ salt: number;
153
+ }): string {
154
+ const pool = MOOD_POOLS[mood.toLowerCase()]?.[key] ?? DEFAULT_POOL[key];
155
+ return pool[Math.abs(salt) % pool.length] ?? DEFAULT_POOL[key][0];
156
+ }
157
+
158
+ function hashDealDesk(input: string): number {
159
+ let hash = 2166136261;
160
+ for (let idx = 0; idx < input.length; idx++) {
161
+ hash ^= input.charCodeAt(idx);
162
+ hash = Math.imul(hash, 16777619);
163
+ }
164
+ return hash >>> 0;
165
+ }
166
+
167
+ function formatCells(cells: string[], width: number): string {
168
+ const separator = " │ ";
169
+ const available = Math.max(
170
+ 1,
171
+ width - displayWidth(separator) * Math.max(0, cells.length - 1),
172
+ );
173
+ const base = Math.max(1, Math.floor(available / cells.length));
174
+ const remainder = Math.max(0, available - base * cells.length);
175
+ return cells
176
+ .map((cell, idx) => {
177
+ const cellWidth = base + (idx < remainder ? 1 : 0);
178
+ return padToWidth(clampText(cell, cellWidth), cellWidth);
179
+ })
180
+ .join(separator);
181
+ }
182
+
87
183
  function buildHeaderLines({
88
- model,
89
184
  mood,
90
185
  messageCount,
91
- themeName,
92
- approximateTokens,
93
- latencyMs,
94
- fallbackModel,
95
186
  status,
96
187
  compact,
97
188
  notice,
98
189
  width,
190
+ seed,
99
191
  }: {
100
- model: string;
101
192
  mood: string;
102
193
  messageCount: number;
103
- themeName?: string;
104
- approximateTokens?: number;
105
- latencyMs?: number | null;
106
- fallbackModel?: string | null;
107
194
  status: DealDeskHeaderStatus;
108
195
  compact: boolean;
109
196
  notice?: string;
110
197
  width: number;
198
+ seed: number;
111
199
  }): string[] {
112
- const statusLabel = STATUS_LABEL[status];
113
- const latency = latencyLabel(latencyMs);
114
- const top = compact
115
- ? shellLine("┌ Drexler ", "┐", width)
116
- : shellLine("┌ Drexler Deal Desk ", "┐", width);
117
-
200
+ const innerWidth = Math.max(1, width - 4);
201
+ const baseHash = hashDealDesk(`${mood}:${messageCount}:${status}:${seed}`);
202
+ const pick = (key: DealDeskPoolKey, offset: number) =>
203
+ pickFromMoodPool({ key, mood, salt: baseHash + offset * 7919 });
204
+ const statusLabel = BOARDROOM_STATUS[status];
118
205
  const summary = compact
119
- ? `● ${statusLabel} ${model} ${countLabel(messageCount, true)}${
120
- latency ? ` ${latency}` : ""
121
- }`
122
- : `● ${statusLabel} │ ${countLabel(
123
- messageCount,
124
- false,
125
- )} │ ~${approximateTokens ?? 0} tok │ ${latency ?? "no run yet"}`;
126
- const detail = `model ${model} │ mood ${mood} │ theme ${
127
- themeName ?? "apollo"
128
- }${fallbackModel ? ` │ fallback ${fallbackModel}` : ""}`;
129
- const lines = [top, bodyLine(summary, width)];
130
-
131
- if (!compact) {
132
- lines.push(bodyLine(detail, width));
133
- }
206
+ ? formatCells(
207
+ [`● ${statusLabel}`, `mood ${mood}`, `fees ${pick("fees", 1)}`],
208
+ innerWidth,
209
+ )
210
+ : formatCells(
211
+ [
212
+ `● ${statusLabel}`,
213
+ memoLabel(messageCount),
214
+ `fees ${pick("fees", 1)}`,
215
+ ],
216
+ innerWidth,
217
+ );
218
+ const readout = compact
219
+ ? formatCells(
220
+ [`risk ${pick("risk", 2)}`, `counsel ${pick("counsel", 3)}`],
221
+ innerWidth,
222
+ )
223
+ : formatCells(
224
+ [
225
+ `mandate ${pick("mandate", 4)}`,
226
+ `risk ${pick("risk", 5)}`,
227
+ `counsel ${pick("counsel", 6)}`,
228
+ ],
229
+ innerWidth,
230
+ );
231
+ const lines = [bodyLine(summary, width), bodyLine(readout, width)];
134
232
 
135
233
  if (!compact && notice && notice.trim().length > 0) {
136
- lines.push(bodyLine(`notice ${notice.trim()}`, width));
234
+ const memo = formatCells(
235
+ [
236
+ `memo ${notice.trim()}`,
237
+ `morale ${pick("morale", 7)}`,
238
+ `synergy ${pick("synergy", 8)}`,
239
+ ],
240
+ innerWidth,
241
+ );
242
+ lines.push(bodyLine(memo, width));
137
243
  }
138
244
 
139
- lines.push(shellLine("", "", width));
245
+ lines.push(shellLine("", "", width));
140
246
  return lines;
141
247
  }
142
248
 
249
+ function titleLabel(compact: boolean): string {
250
+ return compact ? "Drexler" : "Drexler Deal Desk";
251
+ }
252
+
253
+ function FramedTitleText({
254
+ compact,
255
+ borderColor,
256
+ titleColor,
257
+ width,
258
+ }: {
259
+ compact: boolean;
260
+ borderColor: string;
261
+ titleColor: string;
262
+ width: number;
263
+ }) {
264
+ const title = titleLabel(compact);
265
+ const prefix = "╭─ ";
266
+ const titleSuffix = " ";
267
+ const suffix = "╮";
268
+ const ruleWidth = Math.max(
269
+ 0,
270
+ width -
271
+ displayWidth(prefix) -
272
+ displayWidth(title) -
273
+ displayWidth(titleSuffix) -
274
+ displayWidth(suffix),
275
+ );
276
+ return (
277
+ <Text>
278
+ <Text color={borderColor}>{prefix}</Text>
279
+ <Text bold color={titleColor}>
280
+ {title}
281
+ </Text>
282
+ <Text color={borderColor}>
283
+ {titleSuffix}
284
+ {"─".repeat(ruleWidth)}
285
+ {suffix}
286
+ </Text>
287
+ </Text>
288
+ );
289
+ }
290
+
291
+ function FramedBodyText({
292
+ line,
293
+ borderColor,
294
+ contentColor,
295
+ }: {
296
+ line: string;
297
+ borderColor: string;
298
+ contentColor: string;
299
+ }) {
300
+ const content = line.length >= 4 ? line.slice(2, -2) : line;
301
+ return (
302
+ <Text>
303
+ <Text color={borderColor}>│ </Text>
304
+ <Text color={contentColor}>{content}</Text>
305
+ <Text color={borderColor}> │</Text>
306
+ </Text>
307
+ );
308
+ }
309
+
143
310
  function DealDeskHeaderInner({
144
- model,
145
311
  mood,
146
312
  messageCount,
147
- themeName,
148
- approximateTokens,
149
- latencyMs,
150
- fallbackModel,
151
313
  status = "idle",
152
314
  compact = false,
153
315
  notice,
@@ -156,6 +318,7 @@ function DealDeskHeaderInner({
156
318
  }: DealDeskHeaderProps) {
157
319
  const t = useTheme();
158
320
  const width = Math.max(MIN_WIDTH, Math.floor(maxWidth));
321
+ const randomSeed = useMemo(() => Math.floor(Math.random() * 1_000_000_000), []);
159
322
  const statusColor: Record<DealDeskHeaderStatus, string> = useMemo(
160
323
  () => ({
161
324
  idle: t.primaryLight,
@@ -164,32 +327,25 @@ function DealDeskHeaderInner({
164
327
  }),
165
328
  [t.error, t.primaryLight, t.warning],
166
329
  );
330
+ const summaryColor = status === "idle" ? t.text : statusColor[status];
167
331
  const lines = useMemo(
168
332
  () =>
169
333
  buildHeaderLines({
170
- model,
171
334
  mood,
172
335
  messageCount,
173
- themeName,
174
- approximateTokens,
175
- latencyMs,
176
- fallbackModel,
177
336
  status,
178
337
  compact,
179
338
  notice,
180
339
  width,
340
+ seed: randomSeed,
181
341
  }),
182
342
  [
183
- approximateTokens,
184
343
  compact,
185
- fallbackModel,
186
- latencyMs,
187
344
  messageCount,
188
- model,
189
345
  mood,
190
346
  notice,
347
+ randomSeed,
191
348
  status,
192
- themeName,
193
349
  width,
194
350
  ],
195
351
  );
@@ -198,7 +354,7 @@ function DealDeskHeaderInner({
198
354
  return (
199
355
  <Box width={width} marginBottom={marginBottom}>
200
356
  <Text color={statusColor[status]} wrap="truncate">
201
- {tinyLine({ model, messageCount, status, width })}
357
+ {tinyLine({ messageCount, status, width })}
202
358
  </Text>
203
359
  </Box>
204
360
  );
@@ -206,14 +362,26 @@ function DealDeskHeaderInner({
206
362
 
207
363
  return (
208
364
  <Box flexDirection="column" width={width} marginBottom={marginBottom}>
209
- <Text color={t.primaryDim}>{lines[0]}</Text>
210
- <Text color={statusColor[status]}>{lines[1]}</Text>
211
- {lines.slice(2, -1).map((line, index) => (
212
- <Text key={index} color={index === 0 ? t.primaryLight : t.dim}>
213
- {line}
214
- </Text>
365
+ <FramedTitleText
366
+ compact={compact}
367
+ borderColor={t.primary}
368
+ titleColor={t.primaryLight}
369
+ width={width}
370
+ />
371
+ <FramedBodyText
372
+ line={lines[0] ?? ""}
373
+ borderColor={t.primary}
374
+ contentColor={summaryColor}
375
+ />
376
+ {lines.slice(1, -1).map((line, index) => (
377
+ <FramedBodyText
378
+ key={index}
379
+ line={line}
380
+ borderColor={t.primary}
381
+ contentColor={index === 0 ? t.text : t.dim}
382
+ />
215
383
  ))}
216
- <Text color={t.primaryDim}>{lines[lines.length - 1]}</Text>
384
+ <Text color={t.primary}>{lines[lines.length - 1]}</Text>
217
385
  </Box>
218
386
  );
219
387
  }