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.
- package/CHANGELOG.md +15 -0
- package/README.md +58 -13
- package/package.json +1 -1
- package/src/index.ts +1 -1
- package/src/ui/App.tsx +26 -14
- package/src/ui/CommandPalette.tsx +23 -13
- package/src/ui/DealDeskHeader.tsx +248 -80
- package/src/ui/MarkdownBody.tsx +382 -0
- package/src/ui/MascotIntro.tsx +568 -73
- package/src/ui/Message.tsx +28 -15
- package/src/ui/Spinner.tsx +11 -9
- package/src/ui/StatusBar.tsx +11 -43
- package/src/ui/SynergyEvent.tsx +3 -2
- package/src/ui/TranscriptViewport.tsx +271 -30
- package/src/ui/displayContent.ts +114 -0
- package/src/ui/graphemes.ts +1 -0
|
@@ -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
|
|
27
|
-
idle: "
|
|
28
|
-
streaming: "LIVE",
|
|
29
|
-
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
|
-
|
|
33
|
-
|
|
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
|
-
|
|
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 =
|
|
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 -
|
|
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
|
|
60
|
-
|
|
61
|
-
return `${messageCount}
|
|
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
|
-
`${
|
|
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
|
|
113
|
-
const
|
|
114
|
-
const
|
|
115
|
-
|
|
116
|
-
|
|
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
|
-
?
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
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
|
-
|
|
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("
|
|
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({
|
|
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
|
-
<
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
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.
|
|
384
|
+
<Text color={t.primary}>{lines[lines.length - 1]}</Text>
|
|
217
385
|
</Box>
|
|
218
386
|
);
|
|
219
387
|
}
|