drexler 0.2.14 → 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.
@@ -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;
@@ -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
+ }
@@ -23,9 +23,20 @@ type Block =
23
23
  const BULLET_RE = /^(\s*)([*+\-]|\d+\.)\s+(.*)$/;
24
24
  const HEADING_RE = /^(#{1,6})\s+(.*)$/;
25
25
  const HR_RE = /^\s*([-*_])\1\1[-*_\s]*$/;
26
- const FENCE_RE = /^\s*```(.*)$/;
26
+ const FENCE_RE = /^\s*(`{3,}|~{3,})(.*)$/;
27
27
  const QUOTE_RE = /^\s*>\s?(.*)$/;
28
28
 
29
+ function isFenceClose(line: string, marker: string): boolean {
30
+ const fenceChar = marker[0];
31
+ if (!fenceChar) return false;
32
+ const trimmed = line.trim();
33
+ if (trimmed.length < marker.length) return false;
34
+ for (const char of trimmed) {
35
+ if (char !== fenceChar) return false;
36
+ }
37
+ return true;
38
+ }
39
+
29
40
  export function tokenizeInline(input: string): InlineToken[] {
30
41
  const tokens: InlineToken[] = [];
31
42
  let buf = "";
@@ -87,7 +98,19 @@ export function tokenizeInline(input: string): InlineToken[] {
87
98
  if (ch === "[") {
88
99
  const closeBracket = input.indexOf("]", i + 1);
89
100
  if (closeBracket !== -1 && input[closeBracket + 1] === "(") {
90
- const closeParen = input.indexOf(")", closeBracket + 2);
101
+ let depth = 1;
102
+ let closeParen = -1;
103
+ for (let k = closeBracket + 2; k < input.length; k++) {
104
+ const c = input[k];
105
+ if (c === "(") depth += 1;
106
+ else if (c === ")") {
107
+ depth -= 1;
108
+ if (depth === 0) {
109
+ closeParen = k;
110
+ break;
111
+ }
112
+ }
113
+ }
91
114
  if (closeParen !== -1) {
92
115
  flushBuf();
93
116
  const text = input.slice(i + 1, closeBracket);
@@ -115,14 +138,15 @@ export function parseBlocks(input: string): Block[] {
115
138
  const line = lines[i]!;
116
139
  const fence = FENCE_RE.exec(line);
117
140
  if (fence) {
118
- const lang = fence[1]?.trim() || undefined;
141
+ const marker = fence[1]!;
142
+ const lang = fence[2]?.trim() || undefined;
119
143
  const codeLines: string[] = [];
120
144
  i += 1;
121
- while (i < lines.length && !FENCE_RE.test(lines[i]!)) {
145
+ while (i < lines.length && !isFenceClose(lines[i]!, marker)) {
122
146
  codeLines.push(lines[i]!);
123
147
  i += 1;
124
148
  }
125
- i += 1;
149
+ if (i < lines.length) i += 1;
126
150
  blocks.push({ kind: "code", lang, lines: codeLines });
127
151
  continue;
128
152
  }
@@ -136,6 +136,119 @@ const SPLIT_DIVIDER_ROWS: number[] = Array.from(
136
136
  );
137
137
  const BOOT_BAR_WIDTH = MASCOT_WIDTH - 1;
138
138
 
139
+ // Width breakpoints (terminal columns).
140
+ const TINY_BREAKPOINT = 21;
141
+ const NARROW_BREAKPOINT = 24;
142
+ const COMPACT_BREAKPOINT = 72;
143
+ const WIDE_BREAKPOINT = 112;
144
+
145
+ // Inner-panel sizing floors / glue.
146
+ const MIN_DASHBOARD_WIDTH = 28;
147
+ const MIN_INNER_WIDTH = 24;
148
+ const MIN_COPY_WIDTH = 18;
149
+ const MIN_RIGHT_COLUMN_WIDTH = 20;
150
+ const MIN_MOOD_PANEL_WIDTH = 18;
151
+ const MAX_MOOD_PANEL_WIDTH = 44;
152
+ const RIGHT_COLUMN_INSET = 1;
153
+ const RIGHT_COLUMN_PAD_RIGHT = 1;
154
+ const LEFT_PANEL_MIN_COPY = 24;
155
+
156
+ export type MascotLayoutMode = "tiny" | "compact" | "stacked" | "split";
157
+
158
+ export interface MascotPanelBox {
159
+ width: number;
160
+ inset: number;
161
+ }
162
+
163
+ export interface MascotLayout {
164
+ mode: MascotLayoutMode;
165
+ available: number;
166
+ innerWidth: number;
167
+ leftPanel: MascotPanelBox;
168
+ rightColumn: MascotPanelBox;
169
+ rightChildWidth: number;
170
+ copy: MascotPanelBox;
171
+ mood: MascotPanelBox;
172
+ tips: MascotPanelBox;
173
+ dealDesk: MascotPanelBox;
174
+ }
175
+
176
+ export function computeMascotLayout(width: number): MascotLayout {
177
+ const safeWidth = Math.max(1, Math.floor(width));
178
+ if (safeWidth < TINY_BREAKPOINT) {
179
+ const w = safeWidth;
180
+ return {
181
+ mode: "tiny",
182
+ available: w,
183
+ innerWidth: w,
184
+ leftPanel: { width: w, inset: 0 },
185
+ rightColumn: { width: 0, inset: 0 },
186
+ rightChildWidth: 0,
187
+ copy: { width: w, inset: 0 },
188
+ mood: { width: w, inset: 0 },
189
+ tips: { width: w, inset: 0 },
190
+ dealDesk: { width: w, inset: 0 },
191
+ };
192
+ }
193
+ if (safeWidth < COMPACT_BREAKPOINT) {
194
+ const w = Math.max(1, safeWidth - 1);
195
+ return {
196
+ mode: "compact",
197
+ available: w,
198
+ innerWidth: w,
199
+ leftPanel: { width: w, inset: 1 },
200
+ rightColumn: { width: 0, inset: 0 },
201
+ rightChildWidth: 0,
202
+ copy: { width: w, inset: 0 },
203
+ mood: { width: w, inset: 0 },
204
+ tips: { width: w, inset: 0 },
205
+ dealDesk: { width: w, inset: 0 },
206
+ };
207
+ }
208
+ const available = Math.max(MIN_DASHBOARD_WIDTH, safeWidth);
209
+ const innerWidth = Math.max(MIN_INNER_WIDTH, available - FRAME_CHROME_WIDTH);
210
+ if (safeWidth < WIDE_BREAKPOINT) {
211
+ return {
212
+ mode: "stacked",
213
+ available,
214
+ innerWidth,
215
+ leftPanel: { width: innerWidth, inset: 0 },
216
+ rightColumn: { width: 0, inset: 0 },
217
+ rightChildWidth: innerWidth,
218
+ copy: { width: innerWidth, inset: 0 },
219
+ mood: { width: innerWidth, inset: 0 },
220
+ tips: { width: innerWidth, inset: 0 },
221
+ dealDesk: { width: innerWidth, inset: 0 },
222
+ };
223
+ }
224
+ const leftPanelWidth = Math.max(
225
+ MASCOT_WIDTH + GUTTER_WIDTH + LEFT_PANEL_MIN_COPY,
226
+ Math.floor((innerWidth - SPLIT_DIVIDER_WIDTH) / 2),
227
+ );
228
+ const rightColumnWidth = Math.max(
229
+ MIN_RIGHT_COLUMN_WIDTH,
230
+ innerWidth - leftPanelWidth - SPLIT_DIVIDER_WIDTH,
231
+ );
232
+ const rightInner = Math.max(1, rightColumnWidth - RIGHT_COLUMN_PAD_RIGHT);
233
+ const rightChildWidth = Math.max(1, rightInner - RIGHT_COLUMN_INSET);
234
+ const copyWidth = Math.max(
235
+ MIN_COPY_WIDTH,
236
+ leftPanelWidth - MASCOT_WIDTH - GUTTER_WIDTH - 1,
237
+ );
238
+ return {
239
+ mode: "split",
240
+ available,
241
+ innerWidth,
242
+ leftPanel: { width: leftPanelWidth, inset: 0 },
243
+ rightColumn: { width: rightColumnWidth, inset: 0 },
244
+ rightChildWidth,
245
+ copy: { width: copyWidth, inset: 0 },
246
+ mood: { width: copyWidth, inset: 0 },
247
+ tips: { width: rightChildWidth, inset: RIGHT_COLUMN_INSET },
248
+ dealDesk: { width: rightChildWidth, inset: RIGHT_COLUMN_INSET },
249
+ };
250
+ }
251
+
139
252
  interface IntroProps {
140
253
  greeting: string;
141
254
  }
@@ -219,11 +332,13 @@ function fixedDisplayRows(
219
332
  type IntroColorPhase = "early" | "middle" | "late";
220
333
 
221
334
  function introTotalFrames(width: number): number {
222
- return width < 72 ? COMPACT_INTRO_NOTES.length : INTRO_FRAMES.length;
335
+ return width < COMPACT_BREAKPOINT
336
+ ? COMPACT_INTRO_NOTES.length
337
+ : INTRO_FRAMES.length;
223
338
  }
224
339
 
225
340
  function introFrameDelayMs(frameIdx: number, width: number): number {
226
- if (width < 72) return COMPACT_INTRO_DELAY_MS;
341
+ if (width < COMPACT_BREAKPOINT) return COMPACT_INTRO_DELAY_MS;
227
342
  return (
228
343
  INTRO_FRAMES[frameIdx] ?? INTRO_FRAMES[INTRO_FRAMES.length - 1]!
229
344
  ).delayMs;
@@ -247,7 +362,7 @@ export function introPhaseColor(
247
362
  }
248
363
 
249
364
  function introSnapshot(frameIdx: number, width: number) {
250
- const compact = width < 72;
365
+ const compact = width < COMPACT_BREAKPOINT;
251
366
  const total = introTotalFrames(width);
252
367
  const boundedFrameIdx = Math.min(frameIdx, total - 1);
253
368
  const state =
@@ -545,7 +660,7 @@ function MoodReadout({
545
660
  .toString()
546
661
  .padStart(3, " ")}%`;
547
662
 
548
- if (width < 24) {
663
+ if (width < NARROW_BREAKPOINT) {
549
664
  const tinyText =
550
665
  boundedProgress >= 1
551
666
  ? `${normalizedMood} / ${posture.badge}`
@@ -563,7 +678,7 @@ function MoodReadout({
563
678
  );
564
679
  }
565
680
 
566
- const panelWidth = Math.max(18, Math.min(44, width));
681
+ const panelWidth = Math.max(MIN_MOOD_PANEL_WIDTH, Math.min(MAX_MOOD_PANEL_WIDTH, width));
567
682
  const innerWidth = Math.max(1, panelWidth - 4);
568
683
  const title = "Mood";
569
684
  const isSettled = boundedProgress >= 1;
@@ -678,42 +793,15 @@ export function MascotDashboard({
678
793
  }: MascotDashboardProps) {
679
794
  const t = useTheme();
680
795
  const resolvedBarColor = barColor ?? t.primaryLight;
681
- const tinyTerminal = width < 21;
682
- const compact = width < 72;
683
- const sideBySide = width >= 112;
684
- const available = compact ? Math.max(1, width - 1) : Math.max(28, width);
685
- const innerWidth = compact
686
- ? available
687
- : Math.max(24, available - FRAME_CHROME_WIDTH);
688
- const leftPanelWidth = compact
689
- ? available
690
- : sideBySide
691
- ? Math.max(
692
- MASCOT_WIDTH + GUTTER_WIDTH + 24,
693
- Math.floor((innerWidth - SPLIT_DIVIDER_WIDTH) / 2),
694
- )
695
- : innerWidth;
696
- const rightColumnWidth = sideBySide
697
- ? Math.max(20, innerWidth - leftPanelWidth - SPLIT_DIVIDER_WIDTH)
698
- : innerWidth;
699
- const rightInnerWidth = sideBySide
700
- ? Math.max(1, rightColumnWidth - 1)
701
- : rightColumnWidth;
702
- const tipsWidth = sideBySide
703
- ? rightInnerWidth
704
- : innerWidth;
705
- const copyWidth = compact
706
- ? available
707
- : sideBySide
708
- ? Math.max(18, leftPanelWidth - MASCOT_WIDTH - GUTTER_WIDTH - 1)
709
- : innerWidth;
796
+ const layout = computeMascotLayout(width);
797
+ const sideBySide = layout.mode === "split";
710
798
  const wideGreetingRows = sideBySide
711
- ? fixedDisplayRows(greeting, copyWidth, 2)
799
+ ? fixedDisplayRows(greeting, layout.copy.width, 2)
712
800
  : [];
713
801
 
714
- if (tinyTerminal) {
802
+ if (layout.mode === "tiny") {
715
803
  return (
716
- <Box width={available} flexDirection="column">
804
+ <Box width={layout.available} flexDirection="column">
717
805
  <Text color={resolvedBarColor}>{mascotStatus}</Text>
718
806
  <Text bold color={t.primaryLight}>
719
807
  Drexler™
@@ -724,17 +812,23 @@ export function MascotDashboard({
724
812
  mood={mood}
725
813
  progress={bootProgress}
726
814
  progressColor={resolvedBarColor}
727
- width={available}
815
+ width={layout.mood.width}
728
816
  />
729
817
  </Box>
730
- {dealDesk ? <Box marginTop={1}>{dealDesk(Math.max(1, available))}</Box> : null}
818
+ {dealDesk ? (
819
+ <Box marginTop={1}>{dealDesk(layout.dealDesk.width)}</Box>
820
+ ) : null}
731
821
  </Box>
732
822
  );
733
823
  }
734
824
 
735
- if (compact) {
825
+ if (layout.mode === "compact") {
736
826
  return (
737
- <Box marginLeft={1} width={available} flexDirection="column">
827
+ <Box
828
+ marginLeft={layout.leftPanel.inset}
829
+ width={layout.available}
830
+ flexDirection="column"
831
+ >
738
832
  <Text color={resolvedBarColor}>{bar}</Text>
739
833
  <Text color={resolvedBarColor}>{mascotStatus}</Text>
740
834
  <Text bold color={t.primaryLight}>
@@ -746,25 +840,30 @@ export function MascotDashboard({
746
840
  mood={mood}
747
841
  progress={bootProgress}
748
842
  progressColor={resolvedBarColor}
749
- width={available}
843
+ width={layout.mood.width}
750
844
  />
751
845
  </Box>
752
- {dealDesk ? <Box marginTop={1}>{dealDesk(Math.max(1, available))}</Box> : null}
846
+ {dealDesk ? (
847
+ <Box marginTop={1}>{dealDesk(layout.dealDesk.width)}</Box>
848
+ ) : null}
753
849
  </Box>
754
850
  );
755
851
  }
756
852
 
757
853
  return (
758
- <Box width={available}>
854
+ <Box width={layout.available}>
759
855
  <Box
760
- width={available}
856
+ width={layout.available}
761
857
  borderStyle="round"
762
858
  borderColor={t.primary}
763
859
  paddingX={1}
764
860
  flexDirection={sideBySide ? "row" : "column"}
765
861
  alignItems={sideBySide ? "flex-start" : "center"}
766
862
  >
767
- <Box flexDirection={sideBySide ? "row" : "column"} width={leftPanelWidth}>
863
+ <Box
864
+ flexDirection={sideBySide ? "row" : "column"}
865
+ width={layout.leftPanel.width}
866
+ >
768
867
  <Box
769
868
  width={MASCOT_WIDTH}
770
869
  flexShrink={0}
@@ -778,7 +877,7 @@ export function MascotDashboard({
778
877
  <Box
779
878
  flexDirection="column"
780
879
  justifyContent="center"
781
- width={copyWidth}
880
+ width={layout.copy.width}
782
881
  marginTop={sideBySide ? 1 : 0}
783
882
  >
784
883
  <Text bold color={t.primaryLight}>
@@ -799,7 +898,7 @@ export function MascotDashboard({
799
898
  mood={mood}
800
899
  progress={bootProgress}
801
900
  progressColor={resolvedBarColor}
802
- width={copyWidth}
901
+ width={layout.mood.width}
803
902
  />
804
903
  </Box>
805
904
  </Box>
@@ -814,23 +913,25 @@ export function MascotDashboard({
814
913
  </Box>
815
914
  <Box
816
915
  flexDirection="column"
817
- width={rightColumnWidth}
818
- paddingRight={1}
916
+ width={layout.rightColumn.width}
917
+ paddingRight={RIGHT_COLUMN_PAD_RIGHT}
819
918
  >
820
- <Box marginLeft={1}>
821
- <TipsPanel width={Math.max(1, rightInnerWidth - 1)} />
919
+ <Box marginLeft={layout.tips.inset}>
920
+ <TipsPanel width={layout.tips.width} />
822
921
  </Box>
823
922
  {dealDesk ? (
824
- <Box marginLeft={1}>
825
- {dealDesk(Math.max(1, rightInnerWidth - 1))}
923
+ <Box marginLeft={layout.dealDesk.inset}>
924
+ {dealDesk(layout.dealDesk.width)}
826
925
  </Box>
827
926
  ) : null}
828
927
  </Box>
829
928
  </>
830
929
  ) : (
831
- <Box marginTop={1} width={tipsWidth} flexDirection="column">
832
- <TipsPanel width={tipsWidth} />
833
- {dealDesk ? <Box marginTop={1}>{dealDesk(tipsWidth)}</Box> : null}
930
+ <Box marginTop={1} width={layout.tips.width} flexDirection="column">
931
+ <TipsPanel width={layout.tips.width} />
932
+ {dealDesk ? (
933
+ <Box marginTop={1}>{dealDesk(layout.dealDesk.width)}</Box>
934
+ ) : null}
834
935
  </Box>
835
936
  )}
836
937
  </Box>
@@ -1,6 +1,5 @@
1
1
  import { Box, Text } from "ink";
2
- import { memo, useMemo } from "react";
3
- import { renderMarkdown } from "../renderer.ts";
2
+ import { memo } from "react";
4
3
  import {
5
4
  firstDisplayLine,
6
5
  normalizeAssistantDisplayContent,
@@ -10,109 +9,7 @@ import { fitDisplayText } from "./graphemes.ts";
10
9
  import { MarkdownBody } from "./MarkdownBody.tsx";
11
10
  import { useTheme } from "./ThemeContext.tsx";
12
11
 
13
- interface MessageItem {
14
- role: "user" | "assistant" | "system";
15
- content: string;
16
- }
17
-
18
- const SEPARATOR_WIDTH = 44;
19
-
20
- const ROLE_LABELS: Record<MessageItem["role"], string> = {
21
- user: "YOU",
22
- assistant: "DREXLER",
23
- system: "SYSTEM",
24
- };
25
-
26
- function Separator() {
27
- const t = useTheme();
28
- return (
29
- <Box paddingX={1} marginBottom={1} flexShrink={1}>
30
- <Text color={t.primaryDim} wrap="truncate">
31
- {"─".repeat(SEPARATOR_WIDTH)}
32
- </Text>
33
- </Box>
34
- );
35
- }
36
-
37
- function MessageInner({ role, content }: MessageItem) {
38
- const t = useTheme();
39
- const displayContent =
40
- role === "assistant"
41
- ? normalizeAssistantMarkdownRenderContent(content)
42
- : content;
43
- const assistantLines = useMemo(
44
- () =>
45
- role === "assistant"
46
- ? renderMarkdown(displayContent).trimEnd().split("\n")
47
- : [],
48
- [displayContent, role],
49
- );
50
-
51
- if (role === "user") {
52
- return (
53
- <>
54
- <Box paddingX={1} marginBottom={1} flexDirection="column">
55
- <Box>
56
- <Text color={t.primaryLight} bold>
57
- {ROLE_LABELS.user}
58
- </Text>
59
- <Text color={t.primaryDim}> ─ </Text>
60
- <Text color={t.dim}>incoming memo</Text>
61
- </Box>
62
- <Box paddingLeft={1}>
63
- <Text color={t.primary}>› </Text>
64
- <Text color={t.text} wrap="wrap">
65
- {content}
66
- </Text>
67
- </Box>
68
- </Box>
69
- </>
70
- );
71
- }
72
- if (role === "system") {
73
- return (
74
- <Box paddingX={1} marginBottom={1} flexDirection="column">
75
- <Box>
76
- <Text color={t.warning} bold>
77
- {ROLE_LABELS.system}
78
- </Text>
79
- <Text color={t.primaryDim}> ─ </Text>
80
- <Text color={t.dim}>notice</Text>
81
- </Box>
82
- <Box paddingLeft={1}>
83
- <Text color={t.dim} italic wrap="wrap">
84
- {content}
85
- </Text>
86
- </Box>
87
- </Box>
88
- );
89
- }
90
-
91
- return (
92
- <>
93
- <Box flexDirection="column" marginBottom={1} paddingX={1}>
94
- <Box>
95
- <Text color={t.primaryLight} bold>
96
- {ROLE_LABELS.assistant}
97
- </Text>
98
- <Text color={t.primaryDim}> ─ </Text>
99
- <Text color={t.dim}>response ledger</Text>
100
- </Box>
101
- {assistantLines.map((ln, i) => (
102
- <Box key={i} paddingLeft={1}>
103
- <Text color={i === 0 ? t.primary : t.primaryDim}>│ </Text>
104
- <Text color={t.text} wrap="wrap">
105
- {ln}
106
- </Text>
107
- </Box>
108
- ))}
109
- </Box>
110
- <Separator />
111
- </>
112
- );
113
- }
114
-
115
- export const Message = memo(MessageInner);
12
+ const ROLE_LABELS = { assistant: "DREXLER" } as const;
116
13
 
117
14
  interface StreamingProps {
118
15
  content: string;