drexler 0.2.13 → 0.2.15
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 +8 -0
- package/README.md +64 -13
- package/package.json +1 -1
- package/src/commands.ts +127 -20
- package/src/config.ts +141 -32
- package/src/conversation.ts +0 -4
- package/src/index.ts +69 -6
- package/src/pet/petState.ts +408 -0
- package/src/repl.ts +1 -1
- package/src/ui/App.tsx +557 -146
- package/src/ui/CommandPalette.tsx +2 -0
- package/src/ui/DealDeskHeader.tsx +245 -77
- package/src/ui/DeathScreen.tsx +110 -0
- package/src/ui/MarkdownBody.tsx +406 -0
- package/src/ui/MascotIntro.tsx +713 -111
- package/src/ui/Message.tsx +24 -114
- package/src/ui/PetPanel.tsx +537 -0
- package/src/ui/SynergyEvent.tsx +3 -2
- package/src/ui/TranscriptViewport.tsx +461 -70
- package/src/ui/displayContent.ts +117 -0
- package/src/ui/graphemes.ts +1 -0
|
@@ -29,6 +29,8 @@ const COMMAND_HINTS: Record<string, string> = {
|
|
|
29
29
|
"/save": "/save deal-notes.md",
|
|
30
30
|
"/save-last": "/save-last last-response.md",
|
|
31
31
|
"/copy-last": "copy latest response",
|
|
32
|
+
"/setup": "show config + key source",
|
|
33
|
+
"/update": "bun update -g drexler --latest",
|
|
32
34
|
};
|
|
33
35
|
|
|
34
36
|
const ARGUMENT_TITLES: Record<string, { title: string; hint: string }> = {
|
|
@@ -6,13 +6,8 @@ import { useTheme } from "./ThemeContext.tsx";
|
|
|
6
6
|
export type DealDeskHeaderStatus = "idle" | "streaming" | "error";
|
|
7
7
|
|
|
8
8
|
export interface DealDeskHeaderProps {
|
|
9
|
-
model: string;
|
|
10
9
|
mood: string;
|
|
11
10
|
messageCount: number;
|
|
12
|
-
themeName?: string;
|
|
13
|
-
approximateTokens?: number;
|
|
14
|
-
latencyMs?: number | null;
|
|
15
|
-
fallbackModel?: string | null;
|
|
16
11
|
status?: DealDeskHeaderStatus;
|
|
17
12
|
compact?: boolean;
|
|
18
13
|
notice?: string;
|
|
@@ -24,10 +19,81 @@ const DEFAULT_WIDTH = 80;
|
|
|
24
19
|
const MIN_WIDTH = 1;
|
|
25
20
|
const FRAMED_MIN_WIDTH = 24;
|
|
26
21
|
|
|
27
|
-
const
|
|
28
|
-
idle: "
|
|
29
|
-
streaming: "LIVE",
|
|
30
|
-
error: "
|
|
22
|
+
const BOARDROOM_STATUS: Record<DealDeskHeaderStatus, string> = {
|
|
23
|
+
idle: "BOARDROOM OPEN",
|
|
24
|
+
streaming: "MEMO LIVE",
|
|
25
|
+
error: "COUNSEL PANIC",
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
type DealDeskPoolKey =
|
|
29
|
+
| "fees"
|
|
30
|
+
| "mandate"
|
|
31
|
+
| "risk"
|
|
32
|
+
| "counsel"
|
|
33
|
+
| "morale"
|
|
34
|
+
| "synergy";
|
|
35
|
+
|
|
36
|
+
const DEFAULT_POOL: Record<DealDeskPoolKey, readonly string[]> = {
|
|
37
|
+
fees: ["accruing", "sacred", "non-refundable", "already billed"],
|
|
38
|
+
mandate: ["self-awarded", "board-adjacent", "strategic-ish", "lightly authorized"],
|
|
39
|
+
risk: ["theatrical", "outsourced", "priced in", "someone else's"],
|
|
40
|
+
counsel: ["circling", "evasive", "comfortable", "redlining lunch"],
|
|
41
|
+
morale: ["impaired", "marked down", "technically solvent", "under review"],
|
|
42
|
+
synergy: ["alleged", "unverifiable", "already billed", "pending lawsuit"],
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const MOOD_POOLS: Record<
|
|
46
|
+
string,
|
|
47
|
+
Partial<Record<DealDeskPoolKey, readonly string[]>>
|
|
48
|
+
> = {
|
|
49
|
+
angry: {
|
|
50
|
+
fees: ["weaponized", "escalating", "aggressively earned", "non-refundable"],
|
|
51
|
+
mandate: ["hostile", "loudly implied", "board-threatening", "self-ratified"],
|
|
52
|
+
risk: ["acceptable", "transferred", "career-limiting", "somebody else's"],
|
|
53
|
+
counsel: ["circling", "sweating", "denying knowledge", "overruled"],
|
|
54
|
+
morale: ["terminated", "impaired", "written off", "reassigned"],
|
|
55
|
+
synergy: ["forced", "mandatory", "hostile", "already billed"],
|
|
56
|
+
},
|
|
57
|
+
exhausted: {
|
|
58
|
+
fees: ["still accruing", "quietly sacred", "tired but billable", "unquestioned"],
|
|
59
|
+
mandate: ["unclear", "half-approved", "forgotten", "pending coffee"],
|
|
60
|
+
risk: ["deferred", "sleepy", "filed tomorrow", "emotionally hedged"],
|
|
61
|
+
counsel: ["unavailable", "out of office", "blinking slowly", "circling"],
|
|
62
|
+
morale: ["written off", "napping", "below guidance", "technically awake"],
|
|
63
|
+
synergy: ["alleged", "too tired to verify", "softly promised", "unfunded"],
|
|
64
|
+
},
|
|
65
|
+
paranoid: {
|
|
66
|
+
fees: ["traced", "escrowed twice", "suspiciously round", "under seal"],
|
|
67
|
+
mandate: ["encrypted", "deniable", "need-to-know", "redacted"],
|
|
68
|
+
risk: ["everywhere", "listening", "unhedged", "wearing a wire"],
|
|
69
|
+
counsel: ["whispering", "triple-checking", "using burner phones", "redacting"],
|
|
70
|
+
morale: ["surveilled", "compartmentalized", "need-to-know", "encrypted"],
|
|
71
|
+
synergy: ["classified", "denied", "redacted", "not in minutes"],
|
|
72
|
+
},
|
|
73
|
+
generous: {
|
|
74
|
+
fees: ["shared emotionally", "still ours", "politely accruing", "gift-wrapped"],
|
|
75
|
+
mandate: ["benevolent", "magnanimous", "soft hostile", "board-blessed"],
|
|
76
|
+
risk: ["forgiven", "socialized", "gently transferred", "nicely hedged"],
|
|
77
|
+
counsel: ["agreeable", "smiling carefully", "comfortable", "charitable"],
|
|
78
|
+
morale: ["briefly up", "subsidized", "pleasantly marked", "gifted options"],
|
|
79
|
+
synergy: ["donated", "mutual-ish", "kindly alleged", "complimentary"],
|
|
80
|
+
},
|
|
81
|
+
ruthless: {
|
|
82
|
+
fees: ["sacred", "extractive", "fully captured", "compounding"],
|
|
83
|
+
mandate: ["hostile", "absolute", "self-awarded", "non-appealable"],
|
|
84
|
+
risk: ["outsourced", "priced in", "assigned to interns", "deleted"],
|
|
85
|
+
counsel: ["overpaid", "comfortable", "aggressively calm", "circling"],
|
|
86
|
+
morale: ["impaired", "irrelevant", "restructured", "sold separately"],
|
|
87
|
+
synergy: ["mandatory", "already billed", "non-consensual", "accretive"],
|
|
88
|
+
},
|
|
89
|
+
victorious: {
|
|
90
|
+
fees: ["captured", "celebrated", "fully earned", "ringing bell"],
|
|
91
|
+
mandate: ["ratified", "triumphant", "board-crowned", "unopposed"],
|
|
92
|
+
risk: ["conquered", "renamed upside", "priced in", "defeated"],
|
|
93
|
+
counsel: ["applauding", "comfortable", "drafting trophies", "filing confetti"],
|
|
94
|
+
morale: ["temporarily high", "marked up", "wearing medals", "overstated"],
|
|
95
|
+
synergy: ["declared", "victorious", "already billed", "banner-ready"],
|
|
96
|
+
},
|
|
31
97
|
};
|
|
32
98
|
|
|
33
99
|
function clampText(input: string, max: number): string {
|
|
@@ -51,98 +117,194 @@ function bodyLine(content: string, width: number): string {
|
|
|
51
117
|
return `│ ${padToWidth(clampText(content, innerWidth), innerWidth)} │`;
|
|
52
118
|
}
|
|
53
119
|
|
|
54
|
-
function
|
|
55
|
-
|
|
56
|
-
return `${messageCount}
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
function latencyLabel(latencyMs: number | null | undefined): string | null {
|
|
60
|
-
if (typeof latencyMs !== "number") return null;
|
|
61
|
-
if (latencyMs < 1000) return `${Math.max(0, Math.round(latencyMs))}ms`;
|
|
62
|
-
return `${(latencyMs / 1000).toFixed(1)}s`;
|
|
120
|
+
function memoLabel(messageCount: number): string {
|
|
121
|
+
const noun = "memo";
|
|
122
|
+
return `${messageCount} ${noun}${messageCount === 1 ? "" : "s"}`;
|
|
63
123
|
}
|
|
64
124
|
|
|
65
125
|
function tinyLine({
|
|
66
|
-
model,
|
|
67
126
|
messageCount,
|
|
68
127
|
status,
|
|
69
128
|
width,
|
|
70
129
|
}: {
|
|
71
|
-
model: string;
|
|
72
130
|
messageCount: number;
|
|
73
131
|
status: DealDeskHeaderStatus;
|
|
74
132
|
width: number;
|
|
75
133
|
}): string {
|
|
76
134
|
return clampText(
|
|
77
|
-
`${
|
|
135
|
+
`${BOARDROOM_STATUS[status]} ${memoLabel(messageCount)}`,
|
|
78
136
|
width,
|
|
79
137
|
);
|
|
80
138
|
}
|
|
81
139
|
|
|
140
|
+
function pickFromMoodPool({
|
|
141
|
+
key,
|
|
142
|
+
mood,
|
|
143
|
+
salt,
|
|
144
|
+
}: {
|
|
145
|
+
key: DealDeskPoolKey;
|
|
146
|
+
mood: string;
|
|
147
|
+
salt: number;
|
|
148
|
+
}): string {
|
|
149
|
+
const pool = MOOD_POOLS[mood.toLowerCase()]?.[key] ?? DEFAULT_POOL[key];
|
|
150
|
+
return pool[Math.abs(salt) % pool.length] ?? DEFAULT_POOL[key][0];
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function hashDealDesk(input: string): number {
|
|
154
|
+
let hash = 2166136261;
|
|
155
|
+
for (let idx = 0; idx < input.length; idx++) {
|
|
156
|
+
hash ^= input.charCodeAt(idx);
|
|
157
|
+
hash = Math.imul(hash, 16777619);
|
|
158
|
+
}
|
|
159
|
+
return hash >>> 0;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function formatCells(cells: string[], width: number): string {
|
|
163
|
+
const separator = " │ ";
|
|
164
|
+
const available = Math.max(
|
|
165
|
+
1,
|
|
166
|
+
width - displayWidth(separator) * Math.max(0, cells.length - 1),
|
|
167
|
+
);
|
|
168
|
+
const base = Math.max(1, Math.floor(available / cells.length));
|
|
169
|
+
const remainder = Math.max(0, available - base * cells.length);
|
|
170
|
+
return cells
|
|
171
|
+
.map((cell, idx) => {
|
|
172
|
+
const cellWidth = base + (idx < remainder ? 1 : 0);
|
|
173
|
+
return padToWidth(clampText(cell, cellWidth), cellWidth);
|
|
174
|
+
})
|
|
175
|
+
.join(separator);
|
|
176
|
+
}
|
|
177
|
+
|
|
82
178
|
function buildHeaderLines({
|
|
83
|
-
model,
|
|
84
179
|
mood,
|
|
85
180
|
messageCount,
|
|
86
|
-
themeName,
|
|
87
|
-
approximateTokens,
|
|
88
|
-
latencyMs,
|
|
89
|
-
fallbackModel,
|
|
90
181
|
status,
|
|
91
182
|
compact,
|
|
92
183
|
notice,
|
|
93
184
|
width,
|
|
185
|
+
seed,
|
|
94
186
|
}: {
|
|
95
|
-
model: string;
|
|
96
187
|
mood: string;
|
|
97
188
|
messageCount: number;
|
|
98
|
-
themeName?: string;
|
|
99
|
-
approximateTokens?: number;
|
|
100
|
-
latencyMs?: number | null;
|
|
101
|
-
fallbackModel?: string | null;
|
|
102
189
|
status: DealDeskHeaderStatus;
|
|
103
190
|
compact: boolean;
|
|
104
191
|
notice?: string;
|
|
105
192
|
width: number;
|
|
193
|
+
seed: number;
|
|
106
194
|
}): string[] {
|
|
107
|
-
const
|
|
108
|
-
const
|
|
109
|
-
const
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
195
|
+
const innerWidth = Math.max(1, width - 4);
|
|
196
|
+
const baseHash = hashDealDesk(`${mood}:${messageCount}:${status}:${seed}`);
|
|
197
|
+
const pick = (key: DealDeskPoolKey, offset: number) =>
|
|
198
|
+
pickFromMoodPool({ key, mood, salt: baseHash + offset * 7919 });
|
|
199
|
+
const statusLabel = BOARDROOM_STATUS[status];
|
|
113
200
|
const summary = compact
|
|
114
|
-
?
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
201
|
+
? formatCells(
|
|
202
|
+
[`● ${statusLabel}`, `mood ${mood}`, `fees ${pick("fees", 1)}`],
|
|
203
|
+
innerWidth,
|
|
204
|
+
)
|
|
205
|
+
: formatCells(
|
|
206
|
+
[
|
|
207
|
+
`● ${statusLabel}`,
|
|
208
|
+
memoLabel(messageCount),
|
|
209
|
+
`fees ${pick("fees", 1)}`,
|
|
210
|
+
],
|
|
211
|
+
innerWidth,
|
|
212
|
+
);
|
|
213
|
+
const readout = compact
|
|
214
|
+
? formatCells(
|
|
215
|
+
[`risk ${pick("risk", 2)}`, `counsel ${pick("counsel", 3)}`],
|
|
216
|
+
innerWidth,
|
|
217
|
+
)
|
|
218
|
+
: formatCells(
|
|
219
|
+
[
|
|
220
|
+
`mandate ${pick("mandate", 4)}`,
|
|
221
|
+
`risk ${pick("risk", 5)}`,
|
|
222
|
+
`counsel ${pick("counsel", 6)}`,
|
|
223
|
+
],
|
|
224
|
+
innerWidth,
|
|
225
|
+
);
|
|
226
|
+
const lines = [bodyLine(summary, width), bodyLine(readout, width)];
|
|
129
227
|
|
|
130
228
|
if (!compact && notice && notice.trim().length > 0) {
|
|
131
|
-
|
|
229
|
+
const memo = formatCells(
|
|
230
|
+
[
|
|
231
|
+
`memo ${notice.trim()}`,
|
|
232
|
+
`morale ${pick("morale", 7)}`,
|
|
233
|
+
`synergy ${pick("synergy", 8)}`,
|
|
234
|
+
],
|
|
235
|
+
innerWidth,
|
|
236
|
+
);
|
|
237
|
+
lines.push(bodyLine(memo, width));
|
|
132
238
|
}
|
|
133
239
|
|
|
134
|
-
lines.push(shellLine("
|
|
240
|
+
lines.push(shellLine("╰", "╯", width));
|
|
135
241
|
return lines;
|
|
136
242
|
}
|
|
137
243
|
|
|
244
|
+
function titleLabel(compact: boolean): string {
|
|
245
|
+
return compact ? "Drexler" : "Drexler Deal Desk";
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function FramedTitleText({
|
|
249
|
+
compact,
|
|
250
|
+
borderColor,
|
|
251
|
+
titleColor,
|
|
252
|
+
width,
|
|
253
|
+
}: {
|
|
254
|
+
compact: boolean;
|
|
255
|
+
borderColor: string;
|
|
256
|
+
titleColor: string;
|
|
257
|
+
width: number;
|
|
258
|
+
}) {
|
|
259
|
+
const title = titleLabel(compact);
|
|
260
|
+
const prefix = "╭─ ";
|
|
261
|
+
const titleSuffix = " ";
|
|
262
|
+
const suffix = "╮";
|
|
263
|
+
const ruleWidth = Math.max(
|
|
264
|
+
0,
|
|
265
|
+
width -
|
|
266
|
+
displayWidth(prefix) -
|
|
267
|
+
displayWidth(title) -
|
|
268
|
+
displayWidth(titleSuffix) -
|
|
269
|
+
displayWidth(suffix),
|
|
270
|
+
);
|
|
271
|
+
return (
|
|
272
|
+
<Text>
|
|
273
|
+
<Text color={borderColor}>{prefix}</Text>
|
|
274
|
+
<Text bold color={titleColor}>
|
|
275
|
+
{title}
|
|
276
|
+
</Text>
|
|
277
|
+
<Text color={borderColor}>
|
|
278
|
+
{titleSuffix}
|
|
279
|
+
{"─".repeat(ruleWidth)}
|
|
280
|
+
{suffix}
|
|
281
|
+
</Text>
|
|
282
|
+
</Text>
|
|
283
|
+
);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
function FramedBodyText({
|
|
287
|
+
line,
|
|
288
|
+
borderColor,
|
|
289
|
+
contentColor,
|
|
290
|
+
}: {
|
|
291
|
+
line: string;
|
|
292
|
+
borderColor: string;
|
|
293
|
+
contentColor: string;
|
|
294
|
+
}) {
|
|
295
|
+
const content = line.length >= 4 ? line.slice(2, -2) : line;
|
|
296
|
+
return (
|
|
297
|
+
<Text>
|
|
298
|
+
<Text color={borderColor}>│ </Text>
|
|
299
|
+
<Text color={contentColor}>{content}</Text>
|
|
300
|
+
<Text color={borderColor}> │</Text>
|
|
301
|
+
</Text>
|
|
302
|
+
);
|
|
303
|
+
}
|
|
304
|
+
|
|
138
305
|
function DealDeskHeaderInner({
|
|
139
|
-
model,
|
|
140
306
|
mood,
|
|
141
307
|
messageCount,
|
|
142
|
-
themeName,
|
|
143
|
-
approximateTokens,
|
|
144
|
-
latencyMs,
|
|
145
|
-
fallbackModel,
|
|
146
308
|
status = "idle",
|
|
147
309
|
compact = false,
|
|
148
310
|
notice,
|
|
@@ -151,6 +313,7 @@ function DealDeskHeaderInner({
|
|
|
151
313
|
}: DealDeskHeaderProps) {
|
|
152
314
|
const t = useTheme();
|
|
153
315
|
const width = Math.max(MIN_WIDTH, Math.floor(maxWidth));
|
|
316
|
+
const randomSeed = useMemo(() => Math.floor(Math.random() * 1_000_000_000), []);
|
|
154
317
|
const statusColor: Record<DealDeskHeaderStatus, string> = useMemo(
|
|
155
318
|
() => ({
|
|
156
319
|
idle: t.primaryLight,
|
|
@@ -159,32 +322,25 @@ function DealDeskHeaderInner({
|
|
|
159
322
|
}),
|
|
160
323
|
[t.error, t.primaryLight, t.warning],
|
|
161
324
|
);
|
|
325
|
+
const summaryColor = status === "idle" ? t.text : statusColor[status];
|
|
162
326
|
const lines = useMemo(
|
|
163
327
|
() =>
|
|
164
328
|
buildHeaderLines({
|
|
165
|
-
model,
|
|
166
329
|
mood,
|
|
167
330
|
messageCount,
|
|
168
|
-
themeName,
|
|
169
|
-
approximateTokens,
|
|
170
|
-
latencyMs,
|
|
171
|
-
fallbackModel,
|
|
172
331
|
status,
|
|
173
332
|
compact,
|
|
174
333
|
notice,
|
|
175
334
|
width,
|
|
335
|
+
seed: randomSeed,
|
|
176
336
|
}),
|
|
177
337
|
[
|
|
178
|
-
approximateTokens,
|
|
179
338
|
compact,
|
|
180
|
-
fallbackModel,
|
|
181
|
-
latencyMs,
|
|
182
339
|
messageCount,
|
|
183
|
-
model,
|
|
184
340
|
mood,
|
|
185
341
|
notice,
|
|
342
|
+
randomSeed,
|
|
186
343
|
status,
|
|
187
|
-
themeName,
|
|
188
344
|
width,
|
|
189
345
|
],
|
|
190
346
|
);
|
|
@@ -193,7 +349,7 @@ function DealDeskHeaderInner({
|
|
|
193
349
|
return (
|
|
194
350
|
<Box width={width} marginBottom={marginBottom}>
|
|
195
351
|
<Text color={statusColor[status]} wrap="truncate">
|
|
196
|
-
{tinyLine({
|
|
352
|
+
{tinyLine({ messageCount, status, width })}
|
|
197
353
|
</Text>
|
|
198
354
|
</Box>
|
|
199
355
|
);
|
|
@@ -201,14 +357,26 @@ function DealDeskHeaderInner({
|
|
|
201
357
|
|
|
202
358
|
return (
|
|
203
359
|
<Box flexDirection="column" width={width} marginBottom={marginBottom}>
|
|
204
|
-
<
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
360
|
+
<FramedTitleText
|
|
361
|
+
compact={compact}
|
|
362
|
+
borderColor={t.primary}
|
|
363
|
+
titleColor={t.primaryLight}
|
|
364
|
+
width={width}
|
|
365
|
+
/>
|
|
366
|
+
<FramedBodyText
|
|
367
|
+
line={lines[0] ?? ""}
|
|
368
|
+
borderColor={t.primary}
|
|
369
|
+
contentColor={summaryColor}
|
|
370
|
+
/>
|
|
371
|
+
{lines.slice(1, -1).map((line, index) => (
|
|
372
|
+
<FramedBodyText
|
|
373
|
+
key={index}
|
|
374
|
+
line={line}
|
|
375
|
+
borderColor={t.primary}
|
|
376
|
+
contentColor={index === 0 ? t.text : t.dim}
|
|
377
|
+
/>
|
|
210
378
|
))}
|
|
211
|
-
<Text color={t.
|
|
379
|
+
<Text color={t.primary}>{lines[lines.length - 1]}</Text>
|
|
212
380
|
</Box>
|
|
213
381
|
);
|
|
214
382
|
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { Box, Text } from "ink";
|
|
2
|
+
import { useTheme } from "./ThemeContext.tsx";
|
|
3
|
+
|
|
4
|
+
// Five distinct death variants
|
|
5
|
+
const VARIANTS = [
|
|
6
|
+
{
|
|
7
|
+
headline: "STAKEHOLDERS IN SHAMBLES",
|
|
8
|
+
lines: [
|
|
9
|
+
"The board convenes in emergency session.",
|
|
10
|
+
"Markets react with characteristic cruelty.",
|
|
11
|
+
"Analyst consensus revised to: AVOID.",
|
|
12
|
+
],
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
headline: "PIPELINE: BONE DRY. BOARD: DEVASTATED.",
|
|
16
|
+
lines: [
|
|
17
|
+
"Emergency restructuring announced immediately.",
|
|
18
|
+
"Remaining staff issued terse memo: 'hang tight.'",
|
|
19
|
+
"The deal room is very, very quiet.",
|
|
20
|
+
],
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
headline: "ANALYSTS REVISE TARGET PRICE TO ZERO",
|
|
24
|
+
lines: [
|
|
25
|
+
"Short sellers: vindicated, quietly smug.",
|
|
26
|
+
"Drexler could not be reached for comment.",
|
|
27
|
+
"His last email had a typo. The irony.",
|
|
28
|
+
],
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
headline: "SEC OPENS INQUIRY INTO CIRCUMSTANCES",
|
|
32
|
+
lines: [
|
|
33
|
+
"CNBC coverage: 47 minutes, then nothing.",
|
|
34
|
+
"Drexler's legacy: a half-finished term sheet.",
|
|
35
|
+
"The coffee mug on his desk: still warm.",
|
|
36
|
+
],
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
headline: "EMERGENCY CALL SCHEDULED FOR 7AM MONDAY",
|
|
40
|
+
lines: [
|
|
41
|
+
"Consensus: it could have been prevented.",
|
|
42
|
+
"The plant on his desk is already wilting.",
|
|
43
|
+
"Recruiters texted his LinkedIn at 4am.",
|
|
44
|
+
],
|
|
45
|
+
},
|
|
46
|
+
] as const;
|
|
47
|
+
|
|
48
|
+
const REASON_MSGS: Record<string, string> = {
|
|
49
|
+
hunger: "Cause: severe caloric deficiency. The pipeline, unreplenished, consumed itself.",
|
|
50
|
+
happiness: "Cause: total morale collapse. The board's confidence evaporated entirely.",
|
|
51
|
+
energy: "Cause: complete energy depletion. Drexler's systems ceased. Standups continued.",
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
// Stock chart — backslash must be escaped in TS string literals
|
|
55
|
+
const CHART: string[] = [
|
|
56
|
+
" 100 ┤\\",
|
|
57
|
+
" │ \\",
|
|
58
|
+
" │ \\",
|
|
59
|
+
" 50 ┤ \\",
|
|
60
|
+
" │ \\",
|
|
61
|
+
" │ \\________________________________",
|
|
62
|
+
" 0 ┴───────────────────────────────────────",
|
|
63
|
+
" Q1 Q2 Q3 Q4 Q5 now →",
|
|
64
|
+
];
|
|
65
|
+
|
|
66
|
+
const INNER_W = 44;
|
|
67
|
+
|
|
68
|
+
function banner(text: string): string {
|
|
69
|
+
const pad = Math.max(0, INNER_W - text.length);
|
|
70
|
+
const lp = Math.floor(pad / 2);
|
|
71
|
+
const rp = pad - lp;
|
|
72
|
+
return "║" + " ".repeat(lp) + text + " ".repeat(rp) + "║";
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const TOP = "╔" + "═".repeat(INNER_W) + "╗";
|
|
76
|
+
const BOT = "╚" + "═".repeat(INNER_W) + "╝";
|
|
77
|
+
|
|
78
|
+
interface Props {
|
|
79
|
+
reason?: string;
|
|
80
|
+
variant?: number;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function DeathScreen({ reason = "energy", variant = 0 }: Props) {
|
|
84
|
+
const t = useTheme();
|
|
85
|
+
const v = VARIANTS[variant % VARIANTS.length] ?? VARIANTS[0];
|
|
86
|
+
const reasonMsg = REASON_MSGS[reason] ?? REASON_MSGS.energy;
|
|
87
|
+
|
|
88
|
+
return (
|
|
89
|
+
<Box flexDirection="column" paddingX={2} paddingY={1}>
|
|
90
|
+
<Text color={t.error} bold>{TOP}</Text>
|
|
91
|
+
<Text color={t.error} bold>{banner("D R E X L E R H A S D I E D")}</Text>
|
|
92
|
+
<Text color={t.error} bold>{BOT}</Text>
|
|
93
|
+
<Text> </Text>
|
|
94
|
+
<Text color={t.warning} bold> {v.headline}</Text>
|
|
95
|
+
{v.lines.map((line, i) => (
|
|
96
|
+
<Text key={i} color={t.dim}> {line}</Text>
|
|
97
|
+
))}
|
|
98
|
+
<Text> </Text>
|
|
99
|
+
<Text color={t.primaryDim}> {reasonMsg}</Text>
|
|
100
|
+
<Text> </Text>
|
|
101
|
+
<Text color={t.primaryDim}> DRXL Share Price:</Text>
|
|
102
|
+
{CHART.map((line, i) => (
|
|
103
|
+
<Text key={i} color={i < 6 ? t.error : t.dim}> {line}</Text>
|
|
104
|
+
))}
|
|
105
|
+
<Text> </Text>
|
|
106
|
+
<Text color={t.dim}> Stats reset to 50% on next launch.</Text>
|
|
107
|
+
<Text color={t.dim}> Exiting in 5 seconds...</Text>
|
|
108
|
+
</Box>
|
|
109
|
+
);
|
|
110
|
+
}
|