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
package/src/ui/MascotIntro.tsx
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { Box, Text, useApp, useInput, useStdout } from "ink";
|
|
2
|
-
import { useEffect,
|
|
2
|
+
import { useEffect, useMemo, useState, type ReactNode } from "react";
|
|
3
3
|
import { STARTUP_TIPS } from "../startupTips.ts";
|
|
4
4
|
import {
|
|
5
5
|
MascotFrame,
|
|
6
6
|
MASCOT_WIDTH,
|
|
7
7
|
type MascotState,
|
|
8
8
|
} from "./MascotFrame.tsx";
|
|
9
|
+
import { displayWidth, fitDisplayText } from "./graphemes.ts";
|
|
9
10
|
import { useTheme } from "./ThemeContext.tsx";
|
|
10
11
|
|
|
11
12
|
interface IntroFrame extends MascotState {
|
|
@@ -29,7 +30,7 @@ export const INTRO_BOOT_NOTES = [
|
|
|
29
30
|
|
|
30
31
|
type IntroBootNote = (typeof INTRO_BOOT_NOTES)[number];
|
|
31
32
|
|
|
32
|
-
const
|
|
33
|
+
const INTRO_FRAMES: IntroFrame[] = [
|
|
33
34
|
{
|
|
34
35
|
walls: "dim",
|
|
35
36
|
brows: "hidden",
|
|
@@ -122,18 +123,18 @@ const FRAMES: IntroFrame[] = [
|
|
|
122
123
|
},
|
|
123
124
|
];
|
|
124
125
|
|
|
125
|
-
const
|
|
126
|
-
const
|
|
126
|
+
const COMPACT_INTRO_NOTES = ["Booting", "Scanning", "Online"];
|
|
127
|
+
const COMPACT_INTRO_DELAY_MS = 850;
|
|
127
128
|
const SETTLE_HOLD_MS = 1200;
|
|
128
129
|
const FRAME_CHROME_WIDTH = 4;
|
|
129
130
|
const GUTTER_WIDTH = 4;
|
|
130
131
|
const SPLIT_DIVIDER_WIDTH = 3;
|
|
131
|
-
const
|
|
132
|
-
const SPLIT_DIVIDER_HEIGHT = 9;
|
|
132
|
+
const SPLIT_DIVIDER_HEIGHT = 10;
|
|
133
133
|
const SPLIT_DIVIDER_ROWS: number[] = Array.from(
|
|
134
134
|
{ length: SPLIT_DIVIDER_HEIGHT },
|
|
135
135
|
(_, i) => i,
|
|
136
136
|
);
|
|
137
|
+
const BOOT_BAR_WIDTH = MASCOT_WIDTH - 1;
|
|
137
138
|
|
|
138
139
|
interface IntroProps {
|
|
139
140
|
greeting: string;
|
|
@@ -142,14 +143,16 @@ interface IntroProps {
|
|
|
142
143
|
interface MascotDashboardProps {
|
|
143
144
|
greeting: string;
|
|
144
145
|
width: number;
|
|
146
|
+
mood?: string;
|
|
147
|
+
bootProgress?: number;
|
|
145
148
|
state?: MascotState;
|
|
146
149
|
bar?: string;
|
|
147
150
|
barColor?: string;
|
|
148
151
|
mascotStatus?: string;
|
|
149
|
-
dealDesk?: ReactNode;
|
|
152
|
+
dealDesk?: (width: number) => ReactNode;
|
|
150
153
|
}
|
|
151
154
|
|
|
152
|
-
function
|
|
155
|
+
function introBootBar(frameIdx: number, total: number): string {
|
|
153
156
|
const active = Math.max(
|
|
154
157
|
1,
|
|
155
158
|
Math.ceil(((frameIdx + 1) / total) * BOOT_BAR_WIDTH),
|
|
@@ -157,22 +160,507 @@ function bootBar(frameIdx: number, total: number): string {
|
|
|
157
160
|
return " " + "▰".repeat(active) + "▱".repeat(BOOT_BAR_WIDTH - active);
|
|
158
161
|
}
|
|
159
162
|
|
|
163
|
+
function introBootProgress(frameIdx: number, total: number): number {
|
|
164
|
+
return Math.max(0, Math.min(1, (frameIdx + 1) / total));
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function gaugeBar(progress: number, width: number): string {
|
|
168
|
+
const safeWidth = Math.max(1, width);
|
|
169
|
+
const filled = Math.max(
|
|
170
|
+
0,
|
|
171
|
+
Math.min(safeWidth, Math.round(progress * safeWidth)),
|
|
172
|
+
);
|
|
173
|
+
return `${"█".repeat(filled)}${"░".repeat(safeWidth - filled)}`;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function titledPanelBottom(width: number): string {
|
|
177
|
+
return `╰${"─".repeat(Math.max(0, width - 2))}╯`;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function fixedDisplayRows(
|
|
181
|
+
input: string,
|
|
182
|
+
width: number,
|
|
183
|
+
rowCount: number,
|
|
184
|
+
): string[] {
|
|
185
|
+
const safeWidth = Math.max(1, width);
|
|
186
|
+
const rows: string[] = [];
|
|
187
|
+
const words = input.replace(/\s+/g, " ").trim().split(" ").filter(Boolean);
|
|
188
|
+
let cursor = 0;
|
|
189
|
+
|
|
190
|
+
while (rows.length < rowCount && cursor < words.length) {
|
|
191
|
+
const remaining = words.slice(cursor).join(" ");
|
|
192
|
+
if (rows.length === rowCount - 1 || displayWidth(remaining) <= safeWidth) {
|
|
193
|
+
rows.push(fitDisplayText(remaining, safeWidth));
|
|
194
|
+
cursor = words.length;
|
|
195
|
+
break;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
let row = words[cursor] ?? "";
|
|
199
|
+
cursor += 1;
|
|
200
|
+
if (displayWidth(row) > safeWidth) {
|
|
201
|
+
rows.push(fitDisplayText(row, safeWidth));
|
|
202
|
+
continue;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
while (cursor < words.length) {
|
|
206
|
+
const next = words[cursor] ?? "";
|
|
207
|
+
const candidate = `${row} ${next}`;
|
|
208
|
+
if (displayWidth(candidate) > safeWidth) break;
|
|
209
|
+
row = candidate;
|
|
210
|
+
cursor += 1;
|
|
211
|
+
}
|
|
212
|
+
rows.push(row);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
while (rows.length < rowCount) rows.push("");
|
|
216
|
+
return rows;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
type IntroColorPhase = "early" | "middle" | "late";
|
|
220
|
+
|
|
221
|
+
function introTotalFrames(width: number): number {
|
|
222
|
+
return width < 72 ? COMPACT_INTRO_NOTES.length : INTRO_FRAMES.length;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function introFrameDelayMs(frameIdx: number, width: number): number {
|
|
226
|
+
if (width < 72) return COMPACT_INTRO_DELAY_MS;
|
|
227
|
+
return (
|
|
228
|
+
INTRO_FRAMES[frameIdx] ?? INTRO_FRAMES[INTRO_FRAMES.length - 1]!
|
|
229
|
+
).delayMs;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function introColorPhase(frameIdx: number, total: number): IntroColorPhase {
|
|
233
|
+
if (frameIdx < total / 3) return "early";
|
|
234
|
+
if (frameIdx < (total * 2) / 3) return "middle";
|
|
235
|
+
return "late";
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
export function introPhaseColor(
|
|
239
|
+
phase: IntroColorPhase,
|
|
240
|
+
colors: { error: string; warning: string; primaryLight: string },
|
|
241
|
+
): string {
|
|
242
|
+
return phase === "early"
|
|
243
|
+
? colors.error
|
|
244
|
+
: phase === "middle"
|
|
245
|
+
? colors.warning
|
|
246
|
+
: colors.primaryLight;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function introSnapshot(frameIdx: number, width: number) {
|
|
250
|
+
const compact = width < 72;
|
|
251
|
+
const total = introTotalFrames(width);
|
|
252
|
+
const boundedFrameIdx = Math.min(frameIdx, total - 1);
|
|
253
|
+
const state =
|
|
254
|
+
INTRO_FRAMES[boundedFrameIdx] ?? INTRO_FRAMES[INTRO_FRAMES.length - 1]!;
|
|
255
|
+
const note = compact
|
|
256
|
+
? COMPACT_INTRO_NOTES[
|
|
257
|
+
Math.min(boundedFrameIdx, COMPACT_INTRO_NOTES.length - 1)
|
|
258
|
+
]!
|
|
259
|
+
: state.note;
|
|
260
|
+
return {
|
|
261
|
+
bar: introBootBar(boundedFrameIdx, total),
|
|
262
|
+
colorPhase: introColorPhase(frameIdx, total),
|
|
263
|
+
frameIdx: boundedFrameIdx,
|
|
264
|
+
note,
|
|
265
|
+
progress: introBootProgress(boundedFrameIdx, total),
|
|
266
|
+
state,
|
|
267
|
+
status: `${INTRO_STATUS_PREFIX}${note}`,
|
|
268
|
+
total,
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
export function useIntroAnimation(
|
|
273
|
+
width: number,
|
|
274
|
+
active: boolean,
|
|
275
|
+
onComplete?: () => void,
|
|
276
|
+
) {
|
|
277
|
+
const [frameIdx, setFrameIdx] = useState(0);
|
|
278
|
+
|
|
279
|
+
useEffect(() => {
|
|
280
|
+
if (!active) {
|
|
281
|
+
setFrameIdx(0);
|
|
282
|
+
return;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
const total = introTotalFrames(width);
|
|
286
|
+
if (frameIdx >= total - 1) {
|
|
287
|
+
if (!onComplete) return;
|
|
288
|
+
const handle = setTimeout(onComplete, SETTLE_HOLD_MS);
|
|
289
|
+
return () => clearTimeout(handle);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
const handle = setTimeout(() => {
|
|
293
|
+
setFrameIdx((idx) => Math.min(idx + 1, total - 1));
|
|
294
|
+
}, introFrameDelayMs(frameIdx, width));
|
|
295
|
+
return () => clearTimeout(handle);
|
|
296
|
+
}, [active, frameIdx, onComplete, width]);
|
|
297
|
+
|
|
298
|
+
return useMemo(() => introSnapshot(frameIdx, width), [frameIdx, width]);
|
|
299
|
+
}
|
|
300
|
+
|
|
160
301
|
function TipsPanel({ width }: { width: number }) {
|
|
161
302
|
const t = useTheme();
|
|
162
303
|
const textWidth = Math.max(1, width);
|
|
304
|
+
const innerWidth = Math.max(1, textWidth - 4);
|
|
305
|
+
const title = "Tips";
|
|
306
|
+
const titlePrefix = "╭─ ";
|
|
307
|
+
const titleSuffix = " ";
|
|
308
|
+
const titleRule = "─".repeat(
|
|
309
|
+
Math.max(
|
|
310
|
+
0,
|
|
311
|
+
textWidth -
|
|
312
|
+
displayWidth(titlePrefix) -
|
|
313
|
+
displayWidth(title) -
|
|
314
|
+
displayWidth(titleSuffix) -
|
|
315
|
+
displayWidth("╮"),
|
|
316
|
+
),
|
|
317
|
+
);
|
|
163
318
|
return (
|
|
164
319
|
<Box flexDirection="column" width={textWidth}>
|
|
165
|
-
<Text
|
|
166
|
-
|
|
320
|
+
<Text color={t.primary}>
|
|
321
|
+
{titlePrefix}
|
|
322
|
+
<Text bold color={t.primaryLight}>{title}</Text>
|
|
323
|
+
{titleSuffix}
|
|
324
|
+
{titleRule}
|
|
325
|
+
╮
|
|
167
326
|
</Text>
|
|
168
|
-
|
|
169
|
-
{
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
327
|
+
{STARTUP_TIPS.map((tip, idx) => {
|
|
328
|
+
const label = `${idx + 1}. `;
|
|
329
|
+
const tipWidth = Math.max(1, innerWidth - displayWidth(label));
|
|
330
|
+
const clippedTip = fitDisplayText(tip, tipWidth);
|
|
331
|
+
const content = `${label}${clippedTip}`;
|
|
332
|
+
return (
|
|
333
|
+
<Text key={tip}>
|
|
334
|
+
<Text color={t.primary}>│ </Text>
|
|
335
|
+
<Text color={t.primaryLight}>{label}</Text>
|
|
336
|
+
<Text color={t.dim}>{clippedTip}</Text>
|
|
337
|
+
<Text color={t.primary}>
|
|
338
|
+
{" ".repeat(Math.max(0, innerWidth - displayWidth(content)))} │
|
|
339
|
+
</Text>
|
|
173
340
|
</Text>
|
|
174
|
-
)
|
|
341
|
+
);
|
|
342
|
+
})}
|
|
343
|
+
<Text color={t.primary}>{titledPanelBottom(textWidth)}</Text>
|
|
344
|
+
</Box>
|
|
345
|
+
);
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
type MoodTone = "error" | "primaryLight" | "warning";
|
|
349
|
+
|
|
350
|
+
interface MoodPosture {
|
|
351
|
+
badge: string;
|
|
352
|
+
detail: string;
|
|
353
|
+
tone: MoodTone;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
const NAMED_MOOD_POSTURES: Record<string, readonly MoodPosture[]> = {
|
|
357
|
+
angry: [
|
|
358
|
+
{
|
|
359
|
+
badge: "HOSTILE TENDER",
|
|
360
|
+
detail: "board patience: vaporized",
|
|
361
|
+
tone: "error",
|
|
362
|
+
},
|
|
363
|
+
{
|
|
364
|
+
badge: "REDLINE FEVER",
|
|
365
|
+
detail: "counsel posture: braced",
|
|
366
|
+
tone: "error",
|
|
367
|
+
},
|
|
368
|
+
{
|
|
369
|
+
badge: "FEE HAWK",
|
|
370
|
+
detail: "intern confidence: first pass only",
|
|
371
|
+
tone: "warning",
|
|
372
|
+
},
|
|
373
|
+
],
|
|
374
|
+
exhausted: [
|
|
375
|
+
{
|
|
376
|
+
badge: "COFFEE DEBT",
|
|
377
|
+
detail: "intern confidence: ceremonial",
|
|
378
|
+
tone: "warning",
|
|
379
|
+
},
|
|
380
|
+
{
|
|
381
|
+
badge: "QUORUM NAPPING",
|
|
382
|
+
detail: "board patience: on fumes",
|
|
383
|
+
tone: "primaryLight",
|
|
384
|
+
},
|
|
385
|
+
{
|
|
386
|
+
badge: "LATE CLOSE",
|
|
387
|
+
detail: "risk posture: blinking slowly",
|
|
388
|
+
tone: "warning",
|
|
389
|
+
},
|
|
390
|
+
],
|
|
391
|
+
generous: [
|
|
392
|
+
{
|
|
393
|
+
badge: "FEE HOLIDAY",
|
|
394
|
+
detail: "board patience: briefly subsidized",
|
|
395
|
+
tone: "primaryLight",
|
|
396
|
+
},
|
|
397
|
+
{
|
|
398
|
+
badge: "SOFT CLOSE",
|
|
399
|
+
detail: "risk posture: laminated optimism",
|
|
400
|
+
tone: "primaryLight",
|
|
401
|
+
},
|
|
402
|
+
{
|
|
403
|
+
badge: "COUNSEL NEAR",
|
|
404
|
+
detail: "intern confidence: first pass only",
|
|
405
|
+
tone: "error",
|
|
406
|
+
},
|
|
407
|
+
],
|
|
408
|
+
manic: [
|
|
409
|
+
{
|
|
410
|
+
badge: "DEAL SPIRAL",
|
|
411
|
+
detail: "committee pulse: overclocked",
|
|
412
|
+
tone: "warning",
|
|
413
|
+
},
|
|
414
|
+
{
|
|
415
|
+
badge: "CALENDAR HEAT",
|
|
416
|
+
detail: "board patience: rescheduled twice",
|
|
417
|
+
tone: "warning",
|
|
418
|
+
},
|
|
419
|
+
{
|
|
420
|
+
badge: "TERM SHEET TORNADO",
|
|
421
|
+
detail: "risk posture: wearing a helmet",
|
|
422
|
+
tone: "error",
|
|
423
|
+
},
|
|
424
|
+
],
|
|
425
|
+
paranoid: [
|
|
426
|
+
{
|
|
427
|
+
badge: "RISK BUNKER",
|
|
428
|
+
detail: "board patience: subpoena-ready",
|
|
429
|
+
tone: "error",
|
|
430
|
+
},
|
|
431
|
+
{
|
|
432
|
+
badge: "BURNER ROOM",
|
|
433
|
+
detail: "counsel posture: whispering",
|
|
434
|
+
tone: "warning",
|
|
435
|
+
},
|
|
436
|
+
{
|
|
437
|
+
badge: "BOARD LOCKED",
|
|
438
|
+
detail: "committee pulse: encrypted",
|
|
439
|
+
tone: "error",
|
|
440
|
+
},
|
|
441
|
+
],
|
|
442
|
+
ruthless: [
|
|
443
|
+
{
|
|
444
|
+
badge: "FEE HAWK",
|
|
445
|
+
detail: "risk posture: smiling through counsel",
|
|
446
|
+
tone: "primaryLight",
|
|
447
|
+
},
|
|
448
|
+
{
|
|
449
|
+
badge: "MANDATE CLAW",
|
|
450
|
+
detail: "board patience: non-appealable",
|
|
451
|
+
tone: "warning",
|
|
452
|
+
},
|
|
453
|
+
{
|
|
454
|
+
badge: "COVENANT TEETH",
|
|
455
|
+
detail: "intern confidence: collateralized",
|
|
456
|
+
tone: "error",
|
|
457
|
+
},
|
|
458
|
+
],
|
|
459
|
+
victorious: [
|
|
460
|
+
{
|
|
461
|
+
badge: "TAPE PARADE",
|
|
462
|
+
detail: "intern confidence: legally inadvisable",
|
|
463
|
+
tone: "warning",
|
|
464
|
+
},
|
|
465
|
+
{
|
|
466
|
+
badge: "BELL RUNG",
|
|
467
|
+
detail: "board patience: temporarily restored",
|
|
468
|
+
tone: "primaryLight",
|
|
469
|
+
},
|
|
470
|
+
{
|
|
471
|
+
badge: "TROPHY FILING",
|
|
472
|
+
detail: "counsel posture: drafting confetti",
|
|
473
|
+
tone: "warning",
|
|
474
|
+
},
|
|
475
|
+
],
|
|
476
|
+
};
|
|
477
|
+
|
|
478
|
+
const FALLBACK_MOOD_POSTURES: readonly MoodPosture[] = [
|
|
479
|
+
{
|
|
480
|
+
badge: "CALENDAR HEAT",
|
|
481
|
+
detail: "board patience: rescheduled twice",
|
|
482
|
+
tone: "warning",
|
|
483
|
+
},
|
|
484
|
+
{
|
|
485
|
+
badge: "SOFT CLOSE",
|
|
486
|
+
detail: "risk posture: laminated optimism",
|
|
487
|
+
tone: "primaryLight",
|
|
488
|
+
},
|
|
489
|
+
{
|
|
490
|
+
badge: "COUNSEL NEAR",
|
|
491
|
+
detail: "intern confidence: first pass only",
|
|
492
|
+
tone: "error",
|
|
493
|
+
},
|
|
494
|
+
];
|
|
495
|
+
|
|
496
|
+
function hashText(input: string): number {
|
|
497
|
+
return Array.from(input).reduce(
|
|
498
|
+
(sum, char) => sum + (char.codePointAt(0) ?? 0),
|
|
499
|
+
0,
|
|
500
|
+
);
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
function moodPosture(mood: string, seed: number): MoodPosture {
|
|
504
|
+
const key = mood.trim().toLowerCase();
|
|
505
|
+
const pool = NAMED_MOOD_POSTURES[key] ?? FALLBACK_MOOD_POSTURES;
|
|
506
|
+
return pool[Math.abs(hashText(key) + seed) % pool.length] ?? pool[0]!;
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
function moodToneColor(t: ReturnType<typeof useTheme>, tone: MoodTone): string {
|
|
510
|
+
return tone === "error"
|
|
511
|
+
? t.error
|
|
512
|
+
: tone === "warning"
|
|
513
|
+
? t.warning
|
|
514
|
+
: t.primaryLight;
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
function bootPostureDetail(progress: number): string {
|
|
518
|
+
if (progress < 0.25) return "committee pulse: suspicious";
|
|
519
|
+
if (progress < 0.5) return "fee antenna: extending";
|
|
520
|
+
if (progress < 0.75) return "counsel posture: stiffening";
|
|
521
|
+
return "board patience: nearly loaded";
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
function MoodReadout({
|
|
525
|
+
mood,
|
|
526
|
+
progress = 1,
|
|
527
|
+
progressColor,
|
|
528
|
+
width,
|
|
529
|
+
}: {
|
|
530
|
+
mood?: string;
|
|
531
|
+
progress?: number;
|
|
532
|
+
progressColor?: string;
|
|
533
|
+
width: number;
|
|
534
|
+
}) {
|
|
535
|
+
const t = useTheme();
|
|
536
|
+
if (!mood) return null;
|
|
537
|
+
|
|
538
|
+
const postureSeed = useMemo(() => Math.floor(Math.random() * 1_000_000_000), []);
|
|
539
|
+
const boundedProgress = Math.max(0, Math.min(1, progress));
|
|
540
|
+
const normalizedMood = mood.toUpperCase();
|
|
541
|
+
const posture = moodPosture(mood, postureSeed);
|
|
542
|
+
const postureColor = moodToneColor(t, posture.tone);
|
|
543
|
+
const moodPrefix = "";
|
|
544
|
+
const pct = `${Math.round(boundedProgress * 100)
|
|
545
|
+
.toString()
|
|
546
|
+
.padStart(3, " ")}%`;
|
|
547
|
+
|
|
548
|
+
if (width < 24) {
|
|
549
|
+
const tinyText =
|
|
550
|
+
boundedProgress >= 1
|
|
551
|
+
? `${normalizedMood} / ${posture.badge}`
|
|
552
|
+
: pct;
|
|
553
|
+
return (
|
|
554
|
+
<Box width={Math.max(1, width)}>
|
|
555
|
+
<Text
|
|
556
|
+
color={
|
|
557
|
+
boundedProgress >= 1 ? postureColor : progressColor ?? t.primaryLight
|
|
558
|
+
}
|
|
559
|
+
>
|
|
560
|
+
{fitDisplayText(tinyText, Math.max(1, width))}
|
|
561
|
+
</Text>
|
|
175
562
|
</Box>
|
|
563
|
+
);
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
const panelWidth = Math.max(18, Math.min(44, width));
|
|
567
|
+
const innerWidth = Math.max(1, panelWidth - 4);
|
|
568
|
+
const title = "Mood";
|
|
569
|
+
const isSettled = boundedProgress >= 1;
|
|
570
|
+
const topPrefix = "╭─ ";
|
|
571
|
+
const topSuffix = " ";
|
|
572
|
+
const topRule = "─".repeat(
|
|
573
|
+
Math.max(
|
|
574
|
+
0,
|
|
575
|
+
panelWidth -
|
|
576
|
+
displayWidth(topPrefix) -
|
|
577
|
+
displayWidth(title) -
|
|
578
|
+
displayWidth(topSuffix) -
|
|
579
|
+
displayWidth("╮"),
|
|
580
|
+
),
|
|
581
|
+
);
|
|
582
|
+
const settledSuffix = ` / ${posture.badge}`;
|
|
583
|
+
const compactSettledSuffix = ` / ${fitDisplayText(posture.badge, 8)}`;
|
|
584
|
+
const activeSettledSuffix =
|
|
585
|
+
displayWidth(moodPrefix) +
|
|
586
|
+
displayWidth(normalizedMood) +
|
|
587
|
+
displayWidth(settledSuffix) <=
|
|
588
|
+
innerWidth
|
|
589
|
+
? settledSuffix
|
|
590
|
+
: compactSettledSuffix;
|
|
591
|
+
const moodTextWidth = Math.max(
|
|
592
|
+
1,
|
|
593
|
+
innerWidth -
|
|
594
|
+
displayWidth(moodPrefix) -
|
|
595
|
+
(isSettled ? displayWidth(activeSettledSuffix) : 0),
|
|
596
|
+
);
|
|
597
|
+
const moodText = fitDisplayText(normalizedMood, moodTextWidth);
|
|
598
|
+
const settledContent = `${moodPrefix}${moodText}${activeSettledSuffix}`;
|
|
599
|
+
const detailText = fitDisplayText(
|
|
600
|
+
isSettled ? posture.detail : bootPostureDetail(boundedProgress),
|
|
601
|
+
innerWidth,
|
|
602
|
+
);
|
|
603
|
+
const pctWidth = displayWidth(pct);
|
|
604
|
+
const barWidth = Math.max(4, innerWidth - pctWidth - 4);
|
|
605
|
+
const bar = gaugeBar(boundedProgress, barWidth);
|
|
606
|
+
const gaugeContent = `[${bar}] ${pct}`;
|
|
607
|
+
|
|
608
|
+
return (
|
|
609
|
+
<Box flexDirection="column" width={panelWidth}>
|
|
610
|
+
<Text color={t.primaryDim}>
|
|
611
|
+
{topPrefix}
|
|
612
|
+
<Text bold color={t.warning}>
|
|
613
|
+
{title}
|
|
614
|
+
</Text>
|
|
615
|
+
{topSuffix}
|
|
616
|
+
{topRule}
|
|
617
|
+
╮
|
|
618
|
+
</Text>
|
|
619
|
+
{isSettled ? (
|
|
620
|
+
<>
|
|
621
|
+
<Text>
|
|
622
|
+
<Text color={t.primaryDim}>│ </Text>
|
|
623
|
+
<Text bold color={postureColor}>
|
|
624
|
+
{moodText}
|
|
625
|
+
</Text>
|
|
626
|
+
<Text color={t.primaryDim}>{activeSettledSuffix}</Text>
|
|
627
|
+
<Text color={t.primaryDim}>
|
|
628
|
+
{" ".repeat(
|
|
629
|
+
Math.max(0, innerWidth - displayWidth(settledContent)),
|
|
630
|
+
)}
|
|
631
|
+
{" │"}
|
|
632
|
+
</Text>
|
|
633
|
+
</Text>
|
|
634
|
+
<Text>
|
|
635
|
+
<Text color={t.primaryDim}>│ </Text>
|
|
636
|
+
<Text color={t.dim}>{detailText}</Text>
|
|
637
|
+
<Text color={t.primaryDim}>
|
|
638
|
+
{" ".repeat(Math.max(0, innerWidth - displayWidth(detailText)))} │
|
|
639
|
+
</Text>
|
|
640
|
+
</Text>
|
|
641
|
+
</>
|
|
642
|
+
) : (
|
|
643
|
+
<>
|
|
644
|
+
<Text>
|
|
645
|
+
<Text color={t.primaryDim}>│ </Text>
|
|
646
|
+
<Text color={t.primaryDim}>[</Text>
|
|
647
|
+
<Text color={progressColor ?? t.primaryLight}>{bar}</Text>
|
|
648
|
+
<Text color={t.primaryDim}>] </Text>
|
|
649
|
+
<Text color={t.primaryLight}>{pct}</Text>
|
|
650
|
+
<Text color={t.primaryDim}>
|
|
651
|
+
{" ".repeat(Math.max(0, innerWidth - displayWidth(gaugeContent)))} │
|
|
652
|
+
</Text>
|
|
653
|
+
</Text>
|
|
654
|
+
<Text>
|
|
655
|
+
<Text color={t.primaryDim}>│ </Text>
|
|
656
|
+
<Text color={t.dim}>{detailText}</Text>
|
|
657
|
+
<Text color={t.primaryDim}>
|
|
658
|
+
{" ".repeat(Math.max(0, innerWidth - displayWidth(detailText)))} │
|
|
659
|
+
</Text>
|
|
660
|
+
</Text>
|
|
661
|
+
</>
|
|
662
|
+
)}
|
|
663
|
+
<Text color={t.primaryDim}>{titledPanelBottom(panelWidth)}</Text>
|
|
176
664
|
</Box>
|
|
177
665
|
);
|
|
178
666
|
}
|
|
@@ -180,8 +668,10 @@ function TipsPanel({ width }: { width: number }) {
|
|
|
180
668
|
export function MascotDashboard({
|
|
181
669
|
greeting,
|
|
182
670
|
width,
|
|
183
|
-
|
|
184
|
-
|
|
671
|
+
mood,
|
|
672
|
+
bootProgress = 1,
|
|
673
|
+
state = INTRO_FRAMES[INTRO_FRAMES.length - 1]!,
|
|
674
|
+
bar = introBootBar(INTRO_FRAMES.length - 1, INTRO_FRAMES.length),
|
|
185
675
|
barColor,
|
|
186
676
|
mascotStatus = `${INTRO_STATUS_PREFIX}${INTRO_BOOT_NOTES[INTRO_BOOT_NOTES.length - 1]}`,
|
|
187
677
|
dealDesk,
|
|
@@ -203,14 +693,23 @@ export function MascotDashboard({
|
|
|
203
693
|
Math.floor((innerWidth - SPLIT_DIVIDER_WIDTH) / 2),
|
|
204
694
|
)
|
|
205
695
|
: innerWidth;
|
|
206
|
-
const
|
|
696
|
+
const rightColumnWidth = sideBySide
|
|
207
697
|
? Math.max(20, innerWidth - leftPanelWidth - SPLIT_DIVIDER_WIDTH)
|
|
208
698
|
: innerWidth;
|
|
699
|
+
const rightInnerWidth = sideBySide
|
|
700
|
+
? Math.max(1, rightColumnWidth - 1)
|
|
701
|
+
: rightColumnWidth;
|
|
702
|
+
const tipsWidth = sideBySide
|
|
703
|
+
? rightInnerWidth
|
|
704
|
+
: innerWidth;
|
|
209
705
|
const copyWidth = compact
|
|
210
706
|
? available
|
|
211
707
|
: sideBySide
|
|
212
|
-
? Math.max(18, leftPanelWidth - MASCOT_WIDTH - GUTTER_WIDTH)
|
|
708
|
+
? Math.max(18, leftPanelWidth - MASCOT_WIDTH - GUTTER_WIDTH - 1)
|
|
213
709
|
: innerWidth;
|
|
710
|
+
const wideGreetingRows = sideBySide
|
|
711
|
+
? fixedDisplayRows(greeting, copyWidth, 2)
|
|
712
|
+
: [];
|
|
214
713
|
|
|
215
714
|
if (tinyTerminal) {
|
|
216
715
|
return (
|
|
@@ -220,7 +719,15 @@ export function MascotDashboard({
|
|
|
220
719
|
Drexler™
|
|
221
720
|
</Text>
|
|
222
721
|
<Text color={t.primaryLight}>{greeting}</Text>
|
|
223
|
-
|
|
722
|
+
<Box marginTop={1}>
|
|
723
|
+
<MoodReadout
|
|
724
|
+
mood={mood}
|
|
725
|
+
progress={bootProgress}
|
|
726
|
+
progressColor={resolvedBarColor}
|
|
727
|
+
width={available}
|
|
728
|
+
/>
|
|
729
|
+
</Box>
|
|
730
|
+
{dealDesk ? <Box marginTop={1}>{dealDesk(Math.max(1, available))}</Box> : null}
|
|
224
731
|
</Box>
|
|
225
732
|
);
|
|
226
733
|
}
|
|
@@ -234,7 +741,15 @@ export function MascotDashboard({
|
|
|
234
741
|
Drexler International™
|
|
235
742
|
</Text>
|
|
236
743
|
<Text color={t.primaryLight}>{greeting}</Text>
|
|
237
|
-
|
|
744
|
+
<Box marginTop={1}>
|
|
745
|
+
<MoodReadout
|
|
746
|
+
mood={mood}
|
|
747
|
+
progress={bootProgress}
|
|
748
|
+
progressColor={resolvedBarColor}
|
|
749
|
+
width={available}
|
|
750
|
+
/>
|
|
751
|
+
</Box>
|
|
752
|
+
{dealDesk ? <Box marginTop={1}>{dealDesk(Math.max(1, available))}</Box> : null}
|
|
238
753
|
</Box>
|
|
239
754
|
);
|
|
240
755
|
}
|
|
@@ -270,13 +785,27 @@ export function MascotDashboard({
|
|
|
270
785
|
Drexler International™
|
|
271
786
|
</Text>
|
|
272
787
|
<Box height={1} />
|
|
273
|
-
|
|
788
|
+
{sideBySide ? (
|
|
789
|
+
wideGreetingRows.map((row, idx) => (
|
|
790
|
+
<Text key={idx} color={t.primaryLight}>
|
|
791
|
+
{row || " "}
|
|
792
|
+
</Text>
|
|
793
|
+
))
|
|
794
|
+
) : (
|
|
795
|
+
<Text color={t.primaryLight}>{greeting}</Text>
|
|
796
|
+
)}
|
|
274
797
|
<Box height={1} />
|
|
798
|
+
<MoodReadout
|
|
799
|
+
mood={mood}
|
|
800
|
+
progress={bootProgress}
|
|
801
|
+
progressColor={resolvedBarColor}
|
|
802
|
+
width={copyWidth}
|
|
803
|
+
/>
|
|
275
804
|
</Box>
|
|
276
805
|
</Box>
|
|
277
806
|
{sideBySide ? (
|
|
278
807
|
<>
|
|
279
|
-
<Box flexDirection="column" width={SPLIT_DIVIDER_WIDTH}>
|
|
808
|
+
<Box flexDirection="column" width={SPLIT_DIVIDER_WIDTH} flexShrink={0}>
|
|
280
809
|
{SPLIT_DIVIDER_ROWS.map((idx) => (
|
|
281
810
|
<Text key={idx} color={t.primaryDim}>
|
|
282
811
|
{" │ "}
|
|
@@ -285,17 +814,23 @@ export function MascotDashboard({
|
|
|
285
814
|
</Box>
|
|
286
815
|
<Box
|
|
287
816
|
flexDirection="column"
|
|
288
|
-
width={
|
|
817
|
+
width={rightColumnWidth}
|
|
289
818
|
paddingRight={1}
|
|
290
819
|
>
|
|
291
|
-
<
|
|
292
|
-
|
|
820
|
+
<Box marginLeft={1}>
|
|
821
|
+
<TipsPanel width={Math.max(1, rightInnerWidth - 1)} />
|
|
822
|
+
</Box>
|
|
823
|
+
{dealDesk ? (
|
|
824
|
+
<Box marginLeft={1}>
|
|
825
|
+
{dealDesk(Math.max(1, rightInnerWidth - 1))}
|
|
826
|
+
</Box>
|
|
827
|
+
) : null}
|
|
293
828
|
</Box>
|
|
294
829
|
</>
|
|
295
830
|
) : (
|
|
296
831
|
<Box marginTop={1} width={tipsWidth} flexDirection="column">
|
|
297
832
|
<TipsPanel width={tipsWidth} />
|
|
298
|
-
{dealDesk ? <Box marginTop={1}>{dealDesk}</Box> : null}
|
|
833
|
+
{dealDesk ? <Box marginTop={1}>{dealDesk(tipsWidth)}</Box> : null}
|
|
299
834
|
</Box>
|
|
300
835
|
)}
|
|
301
836
|
</Box>
|
|
@@ -308,7 +843,7 @@ export function MascotIntro({ greeting }: IntroProps) {
|
|
|
308
843
|
const { exit } = useApp();
|
|
309
844
|
const { stdout } = useStdout();
|
|
310
845
|
const [cols, setCols] = useState(stdout?.columns ?? 80);
|
|
311
|
-
const
|
|
846
|
+
const intro = useIntroAnimation(cols, true, exit);
|
|
312
847
|
|
|
313
848
|
useEffect(() => {
|
|
314
849
|
if (!stdout) return;
|
|
@@ -323,56 +858,16 @@ export function MascotIntro({ greeting }: IntroProps) {
|
|
|
323
858
|
if (key.escape || key.return || (key.ctrl && _input === "c")) exit();
|
|
324
859
|
});
|
|
325
860
|
|
|
326
|
-
const
|
|
327
|
-
useEffect(() => {
|
|
328
|
-
return () => {
|
|
329
|
-
mountedRef.current = false;
|
|
330
|
-
};
|
|
331
|
-
}, []);
|
|
332
|
-
|
|
333
|
-
useEffect(() => {
|
|
334
|
-
const compact = cols < 72;
|
|
335
|
-
const total = compact ? COMPACT_NOTES.length : FRAMES.length;
|
|
336
|
-
if (frameIdx >= total - 1) {
|
|
337
|
-
const handle = setTimeout(() => {
|
|
338
|
-
if (mountedRef.current) exit();
|
|
339
|
-
}, SETTLE_HOLD_MS);
|
|
340
|
-
return () => clearTimeout(handle);
|
|
341
|
-
}
|
|
342
|
-
const delay = compact
|
|
343
|
-
? COMPACT_DELAY_MS
|
|
344
|
-
: (FRAMES[frameIdx] ?? FRAMES[FRAMES.length - 1]!).delayMs;
|
|
345
|
-
const handle = setTimeout(() => {
|
|
346
|
-
if (mountedRef.current) setFrameIdx((i) => i + 1);
|
|
347
|
-
}, delay);
|
|
348
|
-
return () => clearTimeout(handle);
|
|
349
|
-
}, [cols, frameIdx, exit]);
|
|
350
|
-
|
|
351
|
-
const state = FRAMES[frameIdx] ?? FRAMES[FRAMES.length - 1]!;
|
|
352
|
-
const compact = cols < 72;
|
|
353
|
-
const bar = bootBar(
|
|
354
|
-
Math.min(frameIdx, compact ? COMPACT_NOTES.length - 1 : FRAMES.length - 1),
|
|
355
|
-
compact ? COMPACT_NOTES.length : FRAMES.length,
|
|
356
|
-
);
|
|
357
|
-
const barColor =
|
|
358
|
-
frameIdx < (compact ? COMPACT_NOTES.length : FRAMES.length) / 3
|
|
359
|
-
? t.error
|
|
360
|
-
: frameIdx < ((compact ? COMPACT_NOTES.length : FRAMES.length) * 2) / 3
|
|
361
|
-
? t.warning
|
|
362
|
-
: t.primaryLight;
|
|
363
|
-
const note = compact
|
|
364
|
-
? COMPACT_NOTES[Math.min(frameIdx, COMPACT_NOTES.length - 1)]!
|
|
365
|
-
: state.note;
|
|
366
|
-
const mascotStatus = `${INTRO_STATUS_PREFIX}${note}`;
|
|
861
|
+
const barColor = introPhaseColor(intro.colorPhase, t);
|
|
367
862
|
|
|
368
863
|
return (
|
|
369
864
|
<MascotDashboard
|
|
370
865
|
greeting={greeting}
|
|
371
866
|
width={cols}
|
|
372
|
-
state={state}
|
|
373
|
-
bar={bar}
|
|
867
|
+
state={intro.state}
|
|
868
|
+
bar={intro.bar}
|
|
374
869
|
barColor={barColor}
|
|
375
|
-
mascotStatus={
|
|
870
|
+
mascotStatus={intro.status}
|
|
376
871
|
/>
|
|
377
872
|
);
|
|
378
873
|
}
|