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/CHANGELOG.md +17 -0
- package/README.md +25 -5
- package/package.json +2 -1
- package/src/commands.ts +515 -32
- package/src/config.ts +46 -11
- package/src/index.ts +47 -27
- package/src/renderer.ts +18 -14
- package/src/repl.ts +31 -5
- package/src/types.ts +18 -1
- package/src/ui/App.tsx +309 -107
- package/src/ui/CommandPalette.tsx +103 -8
- package/src/ui/DealDeskHeader.tsx +219 -0
- package/src/ui/InputBox.tsx +115 -10
- package/src/ui/Message.tsx +94 -24
- package/src/ui/SetupPrompt.tsx +85 -0
- package/src/ui/Spinner.tsx +45 -6
- package/src/ui/StatusBar.tsx +39 -7
- package/src/ui/TranscriptViewport.tsx +255 -0
- package/src/ui/graphemes.ts +119 -0
- package/src/ui/themes.ts +36 -3
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 {
|
|
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
|
-
|
|
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 {
|
|
18
|
-
import {
|
|
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 {
|
|
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 = () =>
|
|
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 =
|
|
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 [
|
|
81
|
-
const
|
|
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
|
|
134
|
-
const
|
|
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:
|
|
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
|
-
|
|
248
|
-
|
|
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
|
-
|
|
298
|
-
|
|
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
|
-
|
|
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 =
|
|
314
|
-
|
|
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
|
-
|
|
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
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
379
|
-
setCursor(0);
|
|
554
|
+
updateDraft({ value: "", cursor: 0 });
|
|
380
555
|
} else {
|
|
381
556
|
const entry = history[next] ?? "";
|
|
382
557
|
setHistoryIdx(next);
|
|
383
|
-
|
|
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
|
-
|
|
563
|
+
updateDraft((prev) => ({ value: prev.value, cursor: 0 }));
|
|
390
564
|
return;
|
|
391
565
|
}
|
|
392
566
|
if (key.ctrl && char === "e") {
|
|
393
|
-
|
|
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
|
-
|
|
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
|
-
|
|
407
|
-
|
|
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
|
-
<
|
|
610
|
+
<ThemeProvider value={activeTheme}>
|
|
429
611
|
<Box flexDirection="column">
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
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
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
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
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
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
|
-
</
|
|
681
|
+
</ThemeProvider>
|
|
480
682
|
);
|
|
481
683
|
}
|