drexler 0.1.1 → 0.2.1

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/src/ui/App.tsx CHANGED
@@ -1,28 +1,76 @@
1
1
  import { Box, Text, useApp, useInput, useStdout } from "ink";
2
2
  import { useCallback, useEffect, useMemo, useRef, useState } from "react";
3
- import { dispatch, filterPaletteByPrefix, isSlash } from "../commands.ts";
3
+ import {
4
+ dispatch,
5
+ filterPaletteByPrefix,
6
+ isSlash,
7
+ type CommandAction,
8
+ } from "../commands.ts";
9
+ import { saveConfig } from "../config.ts";
4
10
  import type { Conversation } from "../conversation.ts";
5
11
  import { streamChat, type FetchFn } from "../llm.ts";
6
12
  import { pickLayout } from "../renderer.ts";
7
- import { detectPersonaDrift } from "../repl.ts";
8
13
  import {
9
- DRIFT_REMINDER,
14
+ buildMessagesWithReminder,
15
+ detectPersonaDrift,
16
+ pickFallback,
17
+ } from "../repl.ts";
18
+ import {
10
19
  EMPTY_NUDGE,
11
- REMINDER_INTERVAL,
12
20
  SIGINT_MSG,
13
21
  STREAM_ERROR,
14
22
  THINKING_LINES,
15
23
  WITTICISMS,
16
24
  } from "../sayings.ts";
17
- import { MODEL_FALLBACK, MODEL_PRIMARY, type Config } from "../types.ts";
18
- import { useTheme } from "./ThemeContext.tsx";
25
+ import { type Config } from "../types.ts";
26
+ import { THEME_NAMES } from "../types.ts";
19
27
  import { CommandPalette } from "./CommandPalette.tsx";
28
+ import { DealDeskHeader } from "./DealDeskHeader.tsx";
29
+ import {
30
+ clampCursor,
31
+ deleteAtCursor,
32
+ deleteBeforeCursor,
33
+ graphemeLength,
34
+ insertAtCursor,
35
+ } from "./graphemes.ts";
20
36
  import { InputBox } from "./InputBox.tsx";
21
- import { Message, StreamingMessage } from "./Message.tsx";
37
+ import { StreamingMessage } from "./Message.tsx";
22
38
  import { Spinner } from "./Spinner.tsx";
23
39
  import { StatusBar } from "./StatusBar.tsx";
40
+ import { ThemeProvider } from "./ThemeContext.tsx";
41
+ import { TranscriptViewport } from "./TranscriptViewport.tsx";
42
+ import { getActiveTheme, THEMES } from "./themes.ts";
24
43
 
25
44
  const MAX_INPUT_WIDTH = 80;
45
+ const TRANSCRIPT_CHROME_ROWS = 12;
46
+
47
+ export function transcriptRowsForTerminalRows(rows: number): number {
48
+ return Math.max(1, Math.min(24, rows - TRANSCRIPT_CHROME_ROWS));
49
+ }
50
+
51
+ export function nextTranscriptScrollOffset({
52
+ current,
53
+ itemCount,
54
+ direction,
55
+ step = 3,
56
+ }: {
57
+ current: number;
58
+ itemCount: number;
59
+ direction: "older" | "newer";
60
+ step?: number;
61
+ }): number {
62
+ const maxOffset = Math.max(0, itemCount - 1);
63
+ if (direction === "older") {
64
+ return Math.min(maxOffset, current + step);
65
+ }
66
+ return Math.max(0, current - step);
67
+ }
68
+
69
+ export function shouldRemoveVisibleAssistantForAction(
70
+ action: CommandAction,
71
+ ): boolean {
72
+ return action.type === "regenerate" && action.removedAssistant;
73
+ }
26
74
 
27
75
  function pick<T>(arr: readonly T[]): T {
28
76
  if (arr.length === 0) {
@@ -31,10 +79,6 @@ function pick<T>(arr: readonly T[]): T {
31
79
  return arr[Math.floor(Math.random() * arr.length)] as T;
32
80
  }
33
81
 
34
- function pickFallback(model: string): string {
35
- return model === MODEL_PRIMARY ? MODEL_FALLBACK : MODEL_PRIMARY;
36
- }
37
-
38
82
  interface ChatItem {
39
83
  id: number;
40
84
  role: "user" | "assistant" | "system";
@@ -44,24 +88,43 @@ interface ChatItem {
44
88
  interface AppProps {
45
89
  conversation: Conversation;
46
90
  config: Config;
91
+ mood?: string;
47
92
  fetchFn?: FetchFn;
48
93
  }
49
94
 
50
- export function App({ conversation, config, fetchFn }: AppProps) {
51
- const t = useTheme();
95
+ export function App({ conversation, config, mood = "neutral", fetchFn }: AppProps) {
52
96
  const { exit } = useApp();
53
97
  const { stdout } = useStdout();
98
+ const [activeTheme, setActiveThemeSnapshot] = useState(() => getActiveTheme());
99
+ const t = activeTheme;
54
100
  const [cols, setCols] = useState<number>(stdout?.columns ?? 80);
101
+ const [rows, setRows] = useState<number>(stdout?.rows ?? 24);
55
102
  useEffect(() => {
56
103
  if (!stdout) return;
57
- const handler = () => setCols(stdout.columns ?? 80);
104
+ const handler = () => {
105
+ setCols(stdout.columns ?? 80);
106
+ setRows(stdout.rows ?? 24);
107
+ };
58
108
  stdout.on("resize", handler);
59
109
  return () => {
60
110
  stdout.off("resize", handler);
61
111
  };
62
112
  }, [stdout]);
63
- const mode = pickLayout(cols);
64
- const inputWidth = Math.max(1, Math.min(cols, MAX_INPUT_WIDTH));
113
+ const mode = useMemo(() => pickLayout(cols), [cols]);
114
+ const inputWidth = useMemo(
115
+ () => Math.max(1, Math.min(cols, MAX_INPUT_WIDTH)),
116
+ [cols],
117
+ );
118
+ const chromeWidth = useMemo(
119
+ () => Math.max(1, Math.min(cols, MAX_INPUT_WIDTH)),
120
+ [cols],
121
+ );
122
+ const statusBarWidth = useMemo(() => Math.max(1, inputWidth - 2), [inputWidth]);
123
+ const isCompact = mode === "very-narrow";
124
+ const maxTranscriptRows = useMemo(
125
+ () => transcriptRowsForTerminalRows(rows),
126
+ [rows],
127
+ );
65
128
 
66
129
  const [items, setItems] = useState<ChatItem[]>([]);
67
130
  const itemIdRef = useRef(0);
@@ -77,17 +140,43 @@ export function App({ conversation, config, fetchFn }: AppProps) {
77
140
  });
78
141
  }, []);
79
142
 
80
- const [input, setInput] = useState("");
81
- const [cursor, setCursor] = useState(0);
143
+ const [draft, setDraft] = useState({ value: "", cursor: 0 });
144
+ const draftRef = useRef(draft);
145
+ const updateDraft = useCallback(
146
+ (
147
+ next:
148
+ | { value: string; cursor: number }
149
+ | ((prev: { value: string; cursor: number }) => {
150
+ value: string;
151
+ cursor: number;
152
+ }),
153
+ ) => {
154
+ const resolved =
155
+ typeof next === "function" ? next(draftRef.current) : next;
156
+ draftRef.current = resolved;
157
+ setDraft(resolved);
158
+ },
159
+ [],
160
+ );
161
+ const input = draft.value;
162
+ const cursor = draft.cursor;
82
163
  const [streaming, setStreaming] = useState<string | null>(null);
83
164
  const [thinking, setThinking] = useState<string | null>(null);
84
165
  const [exitMsg, setExitMsg] = useState<string | null>(null);
85
166
  const [witticism, setWitticism] = useState<string>(pick(WITTICISMS));
86
167
  const [model, setModel] = useState<string>(config.model);
87
168
  const [msgCount, setMsgCount] = useState<number>(0);
169
+ const [tokenCount, setTokenCount] = useState<number>(
170
+ conversation.approximateTokens(),
171
+ );
172
+ const [lastLatencyMs, setLastLatencyMs] = useState<number | null>(null);
173
+ const [fallbackModel, setFallbackModel] = useState<string | null>(null);
174
+ const [deskStatus, setDeskStatus] = useState<"idle" | "error">("idle");
175
+ const [deskNotice, setDeskNotice] = useState<string | null>(null);
88
176
  const [history, setHistory] = useState<string[]>([]);
89
177
  const [historyIdx, setHistoryIdx] = useState<number | null>(null);
90
178
  const [paletteIdx, setPaletteIdx] = useState(0);
179
+ const [scrollOffset, setScrollOffset] = useState(0);
91
180
 
92
181
  const paletteItems = useMemo(() => filterPaletteByPrefix(input), [input]);
93
182
  const paletteOpen = paletteItems.length > 0;
@@ -95,6 +184,28 @@ export function App({ conversation, config, fetchFn }: AppProps) {
95
184
  setPaletteIdx(0);
96
185
  }, [input]);
97
186
 
187
+ useEffect(() => {
188
+ setScrollOffset(0);
189
+ }, [items.length]);
190
+
191
+ useEffect(() => {
192
+ setTokenCount(conversation.approximateTokens());
193
+ }, [conversation, msgCount]);
194
+
195
+ const themeName = useMemo(() => {
196
+ const active = getActiveTheme();
197
+ return (
198
+ THEME_NAMES.find((name) => THEMES[name] === active) ??
199
+ config.theme ??
200
+ "apollo"
201
+ );
202
+ }, [activeTheme, config.theme]);
203
+
204
+ const scrollHint = useMemo(() => {
205
+ if (items.length <= maxTranscriptRows) return undefined;
206
+ return scrollOffset > 0 ? "PageDown newer" : "PageUp scrollback";
207
+ }, [items.length, maxTranscriptRows, scrollOffset]);
208
+
98
209
  // throttle streaming updates so React doesn't re-render every token
99
210
  const streamBufRef = useRef("");
100
211
  const streamTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
@@ -130,17 +241,12 @@ export function App({ conversation, config, fetchFn }: AppProps) {
130
241
  [flushStream],
131
242
  );
132
243
 
133
- const buildMessagesWithReminder = useCallback(() => {
134
- const snap = conversation.snapshot();
135
- const turns = conversation.userTurns;
136
- if (turns > 0 && turns % REMINDER_INTERVAL === 0) {
137
- return [...snap, { role: "system" as const, content: DRIFT_REMINDER }];
138
- }
139
- return snap;
140
- }, [conversation]);
141
-
142
- const runLLM = useCallback(async () => {
244
+ const runLLM = useCallback(async (instruction?: string) => {
245
+ const startedAt = Date.now();
143
246
  setThinking(pick(THINKING_LINES));
247
+ setDeskStatus("idle");
248
+ setDeskNotice(null);
249
+ setFallbackModel(null);
144
250
  streamBufRef.current = "";
145
251
  setStreaming(null);
146
252
  let firstToken = true;
@@ -152,7 +258,12 @@ export function App({ conversation, config, fetchFn }: AppProps) {
152
258
  apiKey: config.apiKey,
153
259
  model,
154
260
  fallbackModel: pickFallback(model),
155
- messages: buildMessagesWithReminder(),
261
+ messages: instruction
262
+ ? [
263
+ ...buildMessagesWithReminder(conversation),
264
+ { role: "system", content: instruction },
265
+ ]
266
+ : buildMessagesWithReminder(conversation),
156
267
  onToken: (t) => {
157
268
  if (!mountedRef.current) return;
158
269
  if (firstToken) {
@@ -179,11 +290,14 @@ export function App({ conversation, config, fetchFn }: AppProps) {
179
290
  setThinking(null);
180
291
  setStreaming(null);
181
292
  addItem("system", `${STREAM_ERROR} [${msg}]`);
293
+ setDeskStatus("error");
294
+ setDeskNotice(msg);
182
295
  setMsgCount(conversation.length);
183
296
  return;
184
297
  }
185
298
  setThinking(null);
186
299
  setStreaming(null);
300
+ setLastLatencyMs(Date.now() - startedAt);
187
301
  if (cancelledRef.current) {
188
302
  cancelledRef.current = false;
189
303
  if (result?.content) {
@@ -191,31 +305,41 @@ export function App({ conversation, config, fetchFn }: AppProps) {
191
305
  addItem("assistant", result.content);
192
306
  }
193
307
  addItem("system", "(cancelled — Drexler taking lunch)");
308
+ setDeskNotice("response cancelled");
194
309
  } else if (result?.ok) {
195
310
  conversation.push("assistant", result.content);
196
311
  addItem("assistant", result.content);
312
+ const notices: string[] = [];
197
313
  if (result.fellBack) {
198
314
  addItem("system", `(fell back to ${result.modelUsed})`);
315
+ notices.push(`fallback ${result.modelUsed}`);
316
+ setFallbackModel(result.modelUsed);
199
317
  }
200
318
  if (detectPersonaDrift(result.content)) {
201
319
  addItem("system", `(persona drift detected — model used 'I')`);
320
+ notices.push("persona drift detected");
202
321
  }
322
+ setDeskNotice(notices.length > 0 ? notices.join(" · ") : null);
203
323
  } else if (result?.interrupted) {
204
324
  conversation.push("assistant", result.content);
205
325
  addItem("assistant", result.content);
206
326
  addItem("system", "(stream interrupted — partial response saved)");
327
+ setDeskStatus("error");
328
+ setDeskNotice("stream interrupted; partial response saved");
207
329
  } else {
208
330
  const detail = result?.error ? ` [${result.error}]` : "";
209
331
  addItem("system", `${STREAM_ERROR}${detail}`);
332
+ setDeskStatus("error");
333
+ setDeskNotice(result?.error ?? "stream error");
210
334
  }
211
335
  setMsgCount(conversation.length);
336
+ setTokenCount(conversation.approximateTokens());
212
337
  setWitticism(pick(WITTICISMS));
213
338
  }, [
214
339
  config,
215
340
  model,
216
341
  fetchFn,
217
342
  addItem,
218
- buildMessagesWithReminder,
219
343
  conversation,
220
344
  pushTokenToStream,
221
345
  ]);
@@ -234,25 +358,51 @@ export function App({ conversation, config, fetchFn }: AppProps) {
234
358
  const lower = line.toLowerCase().trim();
235
359
  if (lower === "/clear" || lower.startsWith("/clear ")) {
236
360
  setItems([]);
361
+ setLastLatencyMs(null);
362
+ setFallbackModel(null);
237
363
  }
238
364
  if (mutableConfig.model !== model) {
239
365
  setModel(mutableConfig.model);
240
366
  }
367
+ const appliedTheme =
368
+ lower.startsWith("/theme ") && captured.includes("redecorate boardroom");
369
+ if (appliedTheme || getActiveTheme() !== activeTheme) {
370
+ setActiveThemeSnapshot(getActiveTheme());
371
+ if (mutableConfig.theme) {
372
+ setDeskStatus("idle");
373
+ setDeskNotice(`theme ${mutableConfig.theme}`);
374
+ }
375
+ }
376
+ if (action.type === "continue" && action.persistConfig) {
377
+ try {
378
+ await saveConfig(action.persistConfig);
379
+ captured += `${captured ? "\n" : ""}Drexler preferences filed.`;
380
+ } catch (e) {
381
+ const msg = e instanceof Error ? e.message : String(e);
382
+ captured += `${captured ? "\n" : ""}Could not save preferences: ${msg}`;
383
+ setDeskStatus("error");
384
+ setDeskNotice("preference save failed");
385
+ }
386
+ }
241
387
  if (captured) addItem("system", captured);
242
388
  if (action.type === "exit") {
243
389
  triggerExit(action.message ?? SIGINT_MSG);
244
390
  return;
245
391
  }
246
392
  if (action.type === "regenerate") {
247
- removeLastAssistantItem();
248
- await runLLM();
393
+ if (shouldRemoveVisibleAssistantForAction(action)) {
394
+ removeLastAssistantItem();
395
+ }
396
+ await runLLM(action.instruction);
249
397
  }
250
398
  setMsgCount(conversation.length);
399
+ setTokenCount(conversation.approximateTokens());
251
400
  },
252
401
  [
253
402
  addItem,
254
403
  conversation,
255
404
  config,
405
+ activeTheme,
256
406
  model,
257
407
  removeLastAssistantItem,
258
408
  runLLM,
@@ -274,6 +424,7 @@ export function App({ conversation, config, fetchFn }: AppProps) {
274
424
  addItem("user", line);
275
425
  conversation.push("user", line);
276
426
  setMsgCount(conversation.length);
427
+ setTokenCount(conversation.approximateTokens());
277
428
  await runLLM();
278
429
  },
279
430
  [addItem, conversation, handleSlashWithMutation, runLLM],
@@ -294,25 +445,45 @@ export function App({ conversation, config, fetchFn }: AppProps) {
294
445
  if (paletteOpen && key.tab) {
295
446
  const sel = paletteItems[paletteIdx];
296
447
  if (sel) {
297
- setInput(sel.name + " ");
298
- setCursor(sel.name.length + 1);
448
+ updateDraft({
449
+ value: sel.name + " ",
450
+ cursor: graphemeLength(sel.name) + 1,
451
+ });
299
452
  }
300
453
  return;
301
454
  }
455
+ if (key.pageUp) {
456
+ setScrollOffset((offset) =>
457
+ nextTranscriptScrollOffset({
458
+ current: offset,
459
+ itemCount: items.length,
460
+ direction: "older",
461
+ }),
462
+ );
463
+ return;
464
+ }
465
+ if (key.pageDown) {
466
+ setScrollOffset((offset) =>
467
+ nextTranscriptScrollOffset({
468
+ current: offset,
469
+ itemCount: items.length,
470
+ direction: "newer",
471
+ }),
472
+ );
473
+ return;
474
+ }
302
475
  if (paletteOpen && key.return) {
303
476
  const sel = paletteItems[paletteIdx];
304
477
  if (sel) {
305
- setInput("");
306
- setCursor(0);
478
+ updateDraft({ value: "", cursor: 0 });
307
479
  setHistoryIdx(null);
308
480
  void onSubmit(sel.name);
309
481
  }
310
482
  return;
311
483
  }
312
484
  if (key.return) {
313
- const submitted = input;
314
- setInput("");
315
- setCursor(0);
485
+ const submitted = draftRef.current.value;
486
+ updateDraft({ value: "", cursor: 0 });
316
487
  setHistoryIdx(null);
317
488
  const trimmedSubmit = submitted.trim();
318
489
  if (trimmedSubmit.length > 0) {
@@ -329,26 +500,32 @@ export function App({ conversation, config, fetchFn }: AppProps) {
329
500
  return;
330
501
  }
331
502
  if (paletteOpen && key.escape) {
332
- setInput("");
333
- setCursor(0);
503
+ updateDraft({ value: "", cursor: 0 });
334
504
  return;
335
505
  }
336
506
  if (key.tab) {
337
507
  return;
338
508
  }
339
- if (key.backspace || key.delete) {
340
- if (cursor > 0) {
341
- setInput((prev) => prev.slice(0, cursor - 1) + prev.slice(cursor));
342
- setCursor((c) => Math.max(0, c - 1));
343
- }
509
+ if (key.backspace) {
510
+ updateDraft((prev) => deleteBeforeCursor(prev.value, prev.cursor));
511
+ return;
512
+ }
513
+ if (key.delete) {
514
+ updateDraft((prev) => deleteAtCursor(prev.value, prev.cursor));
344
515
  return;
345
516
  }
346
517
  if (key.leftArrow) {
347
- setCursor((c) => Math.max(0, c - 1));
518
+ updateDraft((prev) => ({
519
+ value: prev.value,
520
+ cursor: Math.max(0, prev.cursor - 1),
521
+ }));
348
522
  return;
349
523
  }
350
524
  if (key.rightArrow) {
351
- setCursor((c) => Math.min(input.length, c + 1));
525
+ updateDraft((prev) => ({
526
+ value: prev.value,
527
+ cursor: Math.min(graphemeLength(prev.value), prev.cursor + 1),
528
+ }));
352
529
  return;
353
530
  }
354
531
  if (key.upArrow) {
@@ -362,8 +539,7 @@ export function App({ conversation, config, fetchFn }: AppProps) {
362
539
  const idx = historyIdx === null ? history.length - 1 : Math.max(0, historyIdx - 1);
363
540
  const entry = history[idx] ?? "";
364
541
  setHistoryIdx(idx);
365
- setInput(entry);
366
- setCursor(entry.length);
542
+ updateDraft({ value: entry, cursor: graphemeLength(entry) });
367
543
  return;
368
544
  }
369
545
  if (key.downArrow) {
@@ -375,27 +551,27 @@ export function App({ conversation, config, fetchFn }: AppProps) {
375
551
  const next = historyIdx + 1;
376
552
  if (next >= history.length) {
377
553
  setHistoryIdx(null);
378
- setInput("");
379
- setCursor(0);
554
+ updateDraft({ value: "", cursor: 0 });
380
555
  } else {
381
556
  const entry = history[next] ?? "";
382
557
  setHistoryIdx(next);
383
- setInput(entry);
384
- setCursor(entry.length);
558
+ updateDraft({ value: entry, cursor: graphemeLength(entry) });
385
559
  }
386
560
  return;
387
561
  }
388
562
  if (key.ctrl && char === "a") {
389
- setCursor(0);
563
+ updateDraft((prev) => ({ value: prev.value, cursor: 0 }));
390
564
  return;
391
565
  }
392
566
  if (key.ctrl && char === "e") {
393
- setCursor(input.length);
567
+ updateDraft((prev) => ({
568
+ value: prev.value,
569
+ cursor: graphemeLength(prev.value),
570
+ }));
394
571
  return;
395
572
  }
396
573
  if (key.ctrl && char === "u") {
397
- setInput("");
398
- setCursor(0);
574
+ updateDraft({ value: "", cursor: 0 });
399
575
  return;
400
576
  }
401
577
  // Plain text input. Filter out control chars except printable.
@@ -403,8 +579,13 @@ export function App({ conversation, config, fetchFn }: AppProps) {
403
579
  // accept multi-char (paste)
404
580
  const filtered = char.replace(/[\x00-\x1f]/g, "");
405
581
  if (filtered.length > 0) {
406
- setInput((prev) => prev.slice(0, cursor) + filtered + prev.slice(cursor));
407
- setCursor((c) => c + filtered.length);
582
+ updateDraft((prev) =>
583
+ insertAtCursor(
584
+ prev.value,
585
+ clampCursor(prev.value, prev.cursor),
586
+ filtered,
587
+ ),
588
+ );
408
589
  }
409
590
  }
410
591
  });
@@ -423,59 +604,80 @@ export function App({ conversation, config, fetchFn }: AppProps) {
423
604
  }, []);
424
605
 
425
606
  const isBusy = streaming !== null || thinking !== null;
607
+ const headerStatus = isBusy ? "streaming" : deskStatus;
426
608
 
427
609
  return (
428
- <Box flexDirection="column">
610
+ <ThemeProvider value={activeTheme}>
429
611
  <Box flexDirection="column">
430
- {items.map((item) => (
431
- <Box key={item.id} flexDirection="column">
432
- <Message role={item.role} content={item.content} />
433
- </Box>
434
- ))}
435
- </Box>
612
+ <DealDeskHeader
613
+ model={model}
614
+ mood={mood}
615
+ messageCount={msgCount}
616
+ themeName={themeName}
617
+ approximateTokens={tokenCount}
618
+ latencyMs={lastLatencyMs}
619
+ fallbackModel={fallbackModel}
620
+ status={headerStatus}
621
+ compact={isCompact}
622
+ notice={deskNotice ?? undefined}
623
+ maxWidth={chromeWidth}
624
+ />
625
+ <TranscriptViewport
626
+ items={items}
627
+ maxRows={maxTranscriptRows}
628
+ cols={chromeWidth}
629
+ compact={isCompact}
630
+ scrollOffset={scrollOffset}
631
+ />
436
632
 
437
- <Box flexDirection="column">
438
- {streaming !== null && (
439
- <Box marginBottom={1}>
440
- <StreamingMessage content={streaming} />
441
- </Box>
442
- )}
443
- {thinking !== null && streaming === null && (
444
- <Box paddingX={1} marginBottom={1}>
445
- <Spinner label={thinking} />
446
- </Box>
447
- )}
448
- {exitMsg !== null ? (
449
- <Box paddingX={1} marginBottom={1}>
450
- <Text color={t.primaryLight} bold>
451
- {exitMsg}
452
- </Text>
453
- </Box>
454
- ) : (
455
- <>
456
- {paletteOpen && (
457
- <CommandPalette items={paletteItems} selectedIdx={paletteIdx} />
458
- )}
459
- <Box flexDirection="column">
460
- <InputBox
461
- value={input}
462
- cursor={cursor}
463
- disabled={isBusy}
464
- width={inputWidth}
465
- />
633
+ <Box flexDirection="column">
634
+ {streaming !== null && (
635
+ <Box marginBottom={1}>
636
+ <StreamingMessage content={streaming} width={chromeWidth} />
637
+ </Box>
638
+ )}
639
+ {thinking !== null && streaming === null && (
640
+ <Box paddingX={1} marginBottom={1}>
641
+ <Spinner label={thinking} width={chromeWidth} />
466
642
  </Box>
467
- <Box paddingLeft={2}>
468
- <StatusBar
469
- messageCount={msgCount}
470
- witticism={witticism}
471
- maxWidth={Math.max(1, inputWidth - 2)}
472
- status={isBusy ? "streaming" : "idle"}
473
- compact={mode === "very-narrow"}
474
- />
643
+ )}
644
+ {exitMsg !== null ? (
645
+ <Box paddingX={1} marginBottom={1}>
646
+ <Text color={t.primaryLight} bold>
647
+ {exitMsg}
648
+ </Text>
475
649
  </Box>
476
- </>
477
- )}
650
+ ) : (
651
+ <>
652
+ {paletteOpen && (
653
+ <CommandPalette
654
+ items={paletteItems}
655
+ selectedIdx={paletteIdx}
656
+ width={chromeWidth}
657
+ />
658
+ )}
659
+ <Box flexDirection="column">
660
+ <InputBox
661
+ value={input}
662
+ cursor={cursor}
663
+ disabled={isBusy}
664
+ width={inputWidth}
665
+ />
666
+ </Box>
667
+ <Box paddingLeft={2}>
668
+ <StatusBar
669
+ messageCount={msgCount}
670
+ witticism={witticism}
671
+ maxWidth={statusBarWidth}
672
+ status={isBusy ? "streaming" : deskStatus}
673
+ compact={isCompact}
674
+ scrollHint={scrollHint}
675
+ />
676
+ </Box>
677
+ </>
678
+ )}
679
+ </Box>
478
680
  </Box>
479
- </Box>
681
+ </ThemeProvider>
480
682
  );
481
683
  }