code-ollama 0.16.0 → 0.18.0
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/dist/assets/{tui-85A3pZD2.js → tui-4cX-bKvd.js} +1087 -362
- package/dist/cli.js +203 -10
- package/package.json +3 -4
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { A as
|
|
1
|
+
import { A as SYSTEM, B as REJECT, C as resetSystemMessage, D as LIST$1, E as WARNING, F as AUTO, H as LIST, I as LABEL, L as PLAN, M as PLAN_GENERATION_INSTRUCTION, N as BACK, O as getTheme, P as CATALOG, R as SAFE, S as saveConfig, T as HEADER_PREFIX, V as VERSION, _ as deleteModel, a as color, b as streamChat, c as createSession, d as listSessions, f as loadSession, g as setClearHandler, h as reset, i as WRITE_TOOLS, j as USER, k as ASSISTANT, l as deleteSession, m as clear, n as READ_TOOLS, o as write, p as updateSessionModel, r as TOOLS, s as appendMessage, t as executeTool, u as deleteSessionIfEmpty, v as listModels, w as withSystemMessage, x as loadConfig, y as pullModel, z as APPROVE } from "../cli.js";
|
|
2
2
|
import { readdirSync } from "node:fs";
|
|
3
3
|
import { homedir } from "node:os";
|
|
4
4
|
import { join, relative } from "node:path";
|
|
@@ -6,7 +6,7 @@ import { exec } from "node:child_process";
|
|
|
6
6
|
import { Box, Static, Text, render, useApp, useInput, useStdout } from "ink";
|
|
7
7
|
import { memo, useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
8
8
|
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
9
|
-
import { Select, Spinner } from "@inkjs/ui";
|
|
9
|
+
import { ProgressBar, Select, Spinner } from "@inkjs/ui";
|
|
10
10
|
import { Marked } from "marked";
|
|
11
11
|
import { markedTerminal } from "marked-terminal";
|
|
12
12
|
//#region src/components/CodeBlock/CodeBlock.tsx
|
|
@@ -17,7 +17,7 @@ function normalizeCodeBlockContent(content, indent = "") {
|
|
|
17
17
|
const indentPattern = new RegExp(`^${indent}`, "gm");
|
|
18
18
|
return content.replace(indentPattern, "").trim();
|
|
19
19
|
}
|
|
20
|
-
async function prewarmCodeBlocks(content) {
|
|
20
|
+
async function prewarmCodeBlocks(content, theme = getTheme()) {
|
|
21
21
|
const promises = [];
|
|
22
22
|
let match;
|
|
23
23
|
CODE_BLOCK_REGEX.lastIndex = 0;
|
|
@@ -26,35 +26,35 @@ async function prewarmCodeBlocks(content) {
|
|
|
26
26
|
const language = match[3];
|
|
27
27
|
const code = normalizeCodeBlockContent(match[4], indent);
|
|
28
28
|
// v8 ignore next 2
|
|
29
|
-
if (code) promises.push(prewarmHighlight(code, language));
|
|
29
|
+
if (code) promises.push(prewarmHighlight(code, language, theme));
|
|
30
30
|
}
|
|
31
31
|
await Promise.all(promises);
|
|
32
32
|
}
|
|
33
|
-
async function prewarmHighlight(code, language) {
|
|
33
|
+
async function prewarmHighlight(code, language, theme = getTheme()) {
|
|
34
34
|
// v8 ignore start
|
|
35
|
-
const cacheKey = `${language ?? ""}:${code}`;
|
|
35
|
+
const cacheKey = `${theme.codeTheme}:${language ?? ""}:${code}`;
|
|
36
36
|
if (highlightCache.has(cacheKey)) return;
|
|
37
37
|
// v8 ignore stop
|
|
38
|
-
const result = await highlightCode(code, language);
|
|
38
|
+
const result = await highlightCode(code, language, theme.codeTheme);
|
|
39
39
|
highlightCache.set(cacheKey, result);
|
|
40
40
|
}
|
|
41
|
-
async function highlightCode(code, language = "text") {
|
|
41
|
+
async function highlightCode(code, language = "text", codeTheme = getTheme().codeTheme) {
|
|
42
42
|
const { codeToANSI } = await import("@shikijs/cli");
|
|
43
43
|
try {
|
|
44
|
-
return await codeToANSI(code, language,
|
|
44
|
+
return await codeToANSI(code, language, codeTheme);
|
|
45
45
|
} catch {
|
|
46
46
|
// v8 ignore next
|
|
47
47
|
return code;
|
|
48
48
|
}
|
|
49
49
|
}
|
|
50
|
-
var CodeBlock = memo(function CodeBlock({ code, language, role }) {
|
|
51
|
-
const cacheKey = `${language ?? ""}:${code}`;
|
|
50
|
+
var CodeBlock = memo(function CodeBlock({ code, language, role, theme = getTheme() }) {
|
|
51
|
+
const cacheKey = `${theme.codeTheme}:${language ?? ""}:${code}`;
|
|
52
52
|
const [highlighted, setHighlighted] = useState(() => highlightCache.get(cacheKey) ?? code);
|
|
53
53
|
useEffect(() => {
|
|
54
54
|
let canceled = false;
|
|
55
55
|
async function loadHighlight() {
|
|
56
56
|
try {
|
|
57
|
-
const result = await highlightCode(code, language);
|
|
57
|
+
const result = await highlightCode(code, language, theme.codeTheme);
|
|
58
58
|
highlightCache.set(cacheKey, result);
|
|
59
59
|
if (!canceled) setHighlighted(result);
|
|
60
60
|
} catch {}
|
|
@@ -66,22 +66,31 @@ var CodeBlock = memo(function CodeBlock({ code, language, role }) {
|
|
|
66
66
|
}, [
|
|
67
67
|
cacheKey,
|
|
68
68
|
code,
|
|
69
|
-
language
|
|
69
|
+
language,
|
|
70
|
+
theme.codeTheme
|
|
70
71
|
]);
|
|
71
72
|
const isSystem = role === SYSTEM;
|
|
72
73
|
return /* @__PURE__ */ jsx(Box, {
|
|
73
74
|
flexDirection: "column",
|
|
74
75
|
borderStyle: "bold",
|
|
75
|
-
borderColor: isSystem ?
|
|
76
|
+
borderColor: isSystem ? theme.colors.secondary : theme.colors.codeBorder,
|
|
76
77
|
paddingX: 1,
|
|
77
78
|
marginY: 1,
|
|
78
79
|
children: /* @__PURE__ */ jsx(Text, {
|
|
80
|
+
color: isSystem ? theme.colors.messageSystem : void 0,
|
|
79
81
|
dimColor: isSystem,
|
|
80
82
|
children: highlighted
|
|
81
83
|
})
|
|
82
84
|
});
|
|
83
85
|
});
|
|
84
86
|
//#endregion
|
|
87
|
+
//#region src/components/Messages/constants.ts
|
|
88
|
+
var TURN_ABORTED_MESSAGE = [
|
|
89
|
+
"<turn_aborted>",
|
|
90
|
+
"The user interrupted the previous turn on purpose. Any running commands may still be running in the background. If any tools were aborted, they may have partially executed.",
|
|
91
|
+
"</turn_aborted>"
|
|
92
|
+
].join("\n");
|
|
93
|
+
//#endregion
|
|
85
94
|
//#region src/components/Markdown/extensions.ts
|
|
86
95
|
var LATEX_COMMANDS = {
|
|
87
96
|
"\\rightarrow": "→",
|
|
@@ -171,7 +180,7 @@ var inlineMathExtension = {
|
|
|
171
180
|
//#endregion
|
|
172
181
|
//#region src/components/Markdown/render.ts
|
|
173
182
|
var HR_PLACEHOLDER = "__CODE_OLLAMA_HR_PLACEHOLDER__";
|
|
174
|
-
function renderMarkdown(content, hrWidth) {
|
|
183
|
+
function renderMarkdown(content, hrWidth, syntaxTheme = "gitHub") {
|
|
175
184
|
const hr = "─".repeat(Math.max(1, hrWidth));
|
|
176
185
|
const markdown = new Marked();
|
|
177
186
|
const rendererExtension = {
|
|
@@ -187,7 +196,7 @@ function renderMarkdown(content, hrWidth) {
|
|
|
187
196
|
}
|
|
188
197
|
};
|
|
189
198
|
markdown.use(markedTerminal({
|
|
190
|
-
theme:
|
|
199
|
+
theme: syntaxTheme,
|
|
191
200
|
reflowText: true,
|
|
192
201
|
width: Math.max(1, hrWidth)
|
|
193
202
|
}));
|
|
@@ -202,23 +211,20 @@ function renderMarkdown(content, hrWidth) {
|
|
|
202
211
|
}
|
|
203
212
|
//#endregion
|
|
204
213
|
//#region src/components/Markdown/Markdown.tsx
|
|
205
|
-
var Markdown = memo(function Markdown({ content, color, dimColor }) {
|
|
214
|
+
var Markdown = memo(function Markdown({ content, color, dimColor, theme = getTheme() }) {
|
|
206
215
|
const { stdout } = useStdout();
|
|
207
216
|
const availableWidth = stdout.columns - 4;
|
|
208
217
|
return /* @__PURE__ */ jsx(Text, {
|
|
209
218
|
color,
|
|
210
219
|
dimColor,
|
|
211
|
-
children: useMemo(() => renderMarkdown(content, availableWidth), [
|
|
220
|
+
children: useMemo(() => renderMarkdown(content, availableWidth, theme.markdownTheme), [
|
|
221
|
+
content,
|
|
222
|
+
availableWidth,
|
|
223
|
+
theme.markdownTheme
|
|
224
|
+
])
|
|
212
225
|
});
|
|
213
226
|
});
|
|
214
227
|
//#endregion
|
|
215
|
-
//#region src/components/Messages/constants.ts
|
|
216
|
-
var TURN_ABORTED_MESSAGE = [
|
|
217
|
-
"<turn_aborted>",
|
|
218
|
-
"The user interrupted the previous turn on purpose. Any running commands may still be running in the background. If any tools were aborted, they may have partially executed.",
|
|
219
|
-
"</turn_aborted>"
|
|
220
|
-
].join("\n");
|
|
221
|
-
//#endregion
|
|
222
228
|
//#region src/components/Messages/layout.ts
|
|
223
229
|
var ANSI_REGEX = new RegExp(String.raw`\u001B\[[0-9;]*m`, "g");
|
|
224
230
|
var CODE_BLOCK_MARGIN_Y = 2;
|
|
@@ -371,118 +377,31 @@ function parseContent(content) {
|
|
|
371
377
|
return segments;
|
|
372
378
|
}
|
|
373
379
|
//#endregion
|
|
374
|
-
//#region src/components/Messages/streaming.ts
|
|
375
|
-
function isWordCharacter(char) {
|
|
376
|
-
return char !== void 0 && /[A-Za-z0-9]/.test(char);
|
|
377
|
-
}
|
|
378
|
-
function isEscaped(content, index) {
|
|
379
|
-
let slashCount = 0;
|
|
380
|
-
for (let cursor = index - 1; cursor >= 0 && content[cursor] === "\\"; cursor--) slashCount += 1;
|
|
381
|
-
return slashCount % 2 === 1;
|
|
382
|
-
}
|
|
383
|
-
function canOpenEmphasis(content, index, length) {
|
|
384
|
-
const previous = content[index - 1];
|
|
385
|
-
const next = content[index + length];
|
|
386
|
-
if (!next || /\s/.test(next)) return false;
|
|
387
|
-
return !isWordCharacter(previous);
|
|
388
|
-
}
|
|
389
|
-
function canCloseEmphasis(content, index, length) {
|
|
390
|
-
const previous = content[index - 1];
|
|
391
|
-
const next = content[index + length];
|
|
392
|
-
if (!previous || /\s/.test(previous)) return false;
|
|
393
|
-
return !isWordCharacter(next);
|
|
394
|
-
}
|
|
395
|
-
function findUnmatchedInlineDelimiter(content) {
|
|
396
|
-
const stack = [];
|
|
397
|
-
for (let index = 0; index < content.length; index += 1) {
|
|
398
|
-
const current = content[index];
|
|
399
|
-
if (isEscaped(content, index)) continue;
|
|
400
|
-
const top = stack.at(-1);
|
|
401
|
-
if (top?.kind === "code") {
|
|
402
|
-
if (current === "`") stack.pop();
|
|
403
|
-
continue;
|
|
404
|
-
}
|
|
405
|
-
if (top?.kind === "latex") {
|
|
406
|
-
if (current === "$") stack.pop();
|
|
407
|
-
continue;
|
|
408
|
-
}
|
|
409
|
-
if (current === "`") {
|
|
410
|
-
stack.push({
|
|
411
|
-
index,
|
|
412
|
-
length: 1,
|
|
413
|
-
kind: "code",
|
|
414
|
-
marker: "`"
|
|
415
|
-
});
|
|
416
|
-
continue;
|
|
417
|
-
}
|
|
418
|
-
if (current === "$") {
|
|
419
|
-
stack.push({
|
|
420
|
-
index,
|
|
421
|
-
length: 1,
|
|
422
|
-
kind: "latex",
|
|
423
|
-
marker: "$"
|
|
424
|
-
});
|
|
425
|
-
continue;
|
|
426
|
-
}
|
|
427
|
-
if (current !== "*") continue;
|
|
428
|
-
const marker = current;
|
|
429
|
-
const length = content[index + 1] === marker ? 2 : 1;
|
|
430
|
-
const token = marker.repeat(length);
|
|
431
|
-
const kind = length === 2 ? "bold" : "italic";
|
|
432
|
-
if (top?.marker === token && top.kind === kind && canCloseEmphasis(content, index, length)) {
|
|
433
|
-
stack.pop();
|
|
434
|
-
if (length === 2) index += 1;
|
|
435
|
-
continue;
|
|
436
|
-
}
|
|
437
|
-
if (canOpenEmphasis(content, index, length)) {
|
|
438
|
-
stack.push({
|
|
439
|
-
index,
|
|
440
|
-
length,
|
|
441
|
-
kind,
|
|
442
|
-
marker: token
|
|
443
|
-
});
|
|
444
|
-
if (length === 2) index += 1;
|
|
445
|
-
}
|
|
446
|
-
}
|
|
447
|
-
return stack[0] ?? null;
|
|
448
|
-
}
|
|
449
|
-
function splitStreamingInlineContent(content) {
|
|
450
|
-
const unmatched = findUnmatchedInlineDelimiter(content);
|
|
451
|
-
if (!unmatched) return [{
|
|
452
|
-
type: "markdown",
|
|
453
|
-
content
|
|
454
|
-
}];
|
|
455
|
-
const parts = [];
|
|
456
|
-
const prefix = content.slice(0, unmatched.index);
|
|
457
|
-
const plainSuffix = content.slice(unmatched.index + unmatched.length);
|
|
458
|
-
if (prefix) parts.push({
|
|
459
|
-
type: "markdown",
|
|
460
|
-
content: prefix
|
|
461
|
-
});
|
|
462
|
-
if (plainSuffix) parts.push({
|
|
463
|
-
type: "plain",
|
|
464
|
-
content: plainSuffix
|
|
465
|
-
});
|
|
466
|
-
return parts;
|
|
467
|
-
}
|
|
468
|
-
//#endregion
|
|
469
380
|
//#region src/components/Messages/styles.ts
|
|
470
|
-
function getMessageColor(role) {
|
|
381
|
+
function getMessageColor(role, theme) {
|
|
471
382
|
switch (role) {
|
|
472
|
-
case USER:
|
|
473
|
-
case ASSISTANT: return
|
|
474
|
-
case SYSTEM: return
|
|
383
|
+
case USER:
|
|
384
|
+
case ASSISTANT: return;
|
|
385
|
+
case SYSTEM: return theme.colors.messageSystem;
|
|
475
386
|
default: return;
|
|
476
387
|
}
|
|
477
388
|
}
|
|
478
389
|
//#endregion
|
|
479
|
-
//#region src/components/Messages/
|
|
480
|
-
function
|
|
481
|
-
|
|
482
|
-
|
|
390
|
+
//#region src/components/Messages/Message.tsx
|
|
391
|
+
function renderStickyPaddingLines(count) {
|
|
392
|
+
return Array.from({ length: count }, (_, index) => /* @__PURE__ */ jsx(
|
|
393
|
+
Text,
|
|
394
|
+
// v8 ignore start
|
|
395
|
+
{ children: " " },
|
|
396
|
+
index
|
|
397
|
+
));
|
|
398
|
+
}
|
|
399
|
+
function Message({ message, isStreaming = false, theme }) {
|
|
400
|
+
const messageColor = getMessageColor(message.role, theme);
|
|
483
401
|
const isSystem = message.role === SYSTEM;
|
|
484
402
|
const isUser = message.role === USER;
|
|
485
403
|
const isStreamingAssistant = isStreaming && !isUser && !isSystem;
|
|
404
|
+
const { stdout } = useStdout();
|
|
486
405
|
const stickyHeightRef = useRef({
|
|
487
406
|
columns: stdout.columns,
|
|
488
407
|
maxHeight: 0
|
|
@@ -506,7 +425,10 @@ function Message({ message, isStreaming = false }) {
|
|
|
506
425
|
const streamingHeight = isStreamingAssistant ? segments.reduce((height, segment) => {
|
|
507
426
|
if (segment.type === "code") return height + getCodeBlockHeight(segment.content, availableWidth);
|
|
508
427
|
if (segment.type === "raw") return height + getCodeBlockHeight(unwrapRawMarkdownFence(segment.content) ?? segment.content, availableWidth);
|
|
509
|
-
return height + getStreamingTextHeight(
|
|
428
|
+
return height + getStreamingTextHeight([{
|
|
429
|
+
type: "markdown",
|
|
430
|
+
content: segment.content
|
|
431
|
+
}], availableWidth);
|
|
510
432
|
}, 0) : 0;
|
|
511
433
|
if (isStreamingAssistant) stickyHeightRef.current.maxHeight = Math.max(stickyHeightRef.current.maxHeight, streamingHeight);
|
|
512
434
|
const stickyPaddingLines = isStreamingAssistant ? stickyHeightRef.current.maxHeight - streamingHeight : 0;
|
|
@@ -523,7 +445,8 @@ function Message({ message, isStreaming = false }) {
|
|
|
523
445
|
children: /* @__PURE__ */ jsx(CodeBlock, {
|
|
524
446
|
code: segment.content,
|
|
525
447
|
language: segment.language,
|
|
526
|
-
role: message.role
|
|
448
|
+
role: message.role,
|
|
449
|
+
theme
|
|
527
450
|
})
|
|
528
451
|
}, index);
|
|
529
452
|
if (segment.type === "raw") {
|
|
@@ -533,11 +456,12 @@ function Message({ message, isStreaming = false }) {
|
|
|
533
456
|
children: /* @__PURE__ */ jsx(CodeBlock, {
|
|
534
457
|
code: markdownSource ?? segment.content,
|
|
535
458
|
language: markdownSource ? "markdown" : segment.language,
|
|
536
|
-
role: message.role
|
|
459
|
+
role: message.role,
|
|
460
|
+
theme
|
|
537
461
|
})
|
|
538
462
|
}, index);
|
|
539
463
|
}
|
|
540
|
-
const textParts =
|
|
464
|
+
const textParts = [{
|
|
541
465
|
type: "markdown",
|
|
542
466
|
content: segment.content
|
|
543
467
|
}];
|
|
@@ -547,28 +471,31 @@ function Message({ message, isStreaming = false }) {
|
|
|
547
471
|
}, index) : /* @__PURE__ */ jsx(Box, {
|
|
548
472
|
flexDirection: "column",
|
|
549
473
|
marginX: 2,
|
|
550
|
-
children: textParts.map((part, partIndex) =>
|
|
551
|
-
color: messageColor,
|
|
552
|
-
children: part.content
|
|
553
|
-
}, partIndex) : /* @__PURE__ */ jsx(Markdown, {
|
|
474
|
+
children: textParts.map((part, partIndex) => /* @__PURE__ */ jsx(Markdown, {
|
|
554
475
|
content: part.content,
|
|
555
|
-
|
|
476
|
+
theme
|
|
556
477
|
}, partIndex))
|
|
557
478
|
}, index);
|
|
558
|
-
}),
|
|
479
|
+
}), renderStickyPaddingLines(stickyPaddingLines)]
|
|
559
480
|
});
|
|
560
481
|
}
|
|
561
|
-
|
|
482
|
+
//#endregion
|
|
483
|
+
//#region src/components/Messages/Messages.tsx
|
|
484
|
+
function Messages({ messages, isLoading, sessionId, streamingMessage, theme = getTheme() }) {
|
|
562
485
|
return /* @__PURE__ */ jsxs(Box, {
|
|
563
486
|
flexDirection: "column",
|
|
564
487
|
children: [
|
|
565
488
|
/* @__PURE__ */ jsx(Static, {
|
|
566
489
|
items: messages.filter(({ content }) => content !== TURN_ABORTED_MESSAGE),
|
|
567
|
-
children: (message, index) => /* @__PURE__ */ jsx(Message, {
|
|
490
|
+
children: (message, index) => /* @__PURE__ */ jsx(Message, {
|
|
491
|
+
message,
|
|
492
|
+
theme
|
|
493
|
+
}, index)
|
|
568
494
|
}, sessionId),
|
|
569
495
|
streamingMessage && /* @__PURE__ */ jsx(Message, {
|
|
570
496
|
isStreaming: true,
|
|
571
|
-
message: streamingMessage
|
|
497
|
+
message: streamingMessage,
|
|
498
|
+
theme
|
|
572
499
|
}),
|
|
573
500
|
isLoading && !streamingMessage?.content && /* @__PURE__ */ jsx(Box, {
|
|
574
501
|
marginTop: -1,
|
|
@@ -580,7 +507,7 @@ function Messages({ messages, isLoading, sessionId, streamingMessage }) {
|
|
|
580
507
|
});
|
|
581
508
|
}
|
|
582
509
|
//#endregion
|
|
583
|
-
//#region src/components/SelectPrompt.tsx
|
|
510
|
+
//#region src/components/SelectPrompt/SelectPrompt.tsx
|
|
584
511
|
function SelectPrompt({ borderStyle, children, onCancel, ...selectProps }) {
|
|
585
512
|
useInput((input, key) => {
|
|
586
513
|
if (key.escape || key.ctrl && input === "c") onCancel?.();
|
|
@@ -591,6 +518,8 @@ function SelectPrompt({ borderStyle, children, onCancel, ...selectProps }) {
|
|
|
591
518
|
children: [children, /* @__PURE__ */ jsx(Select, { ...selectProps })]
|
|
592
519
|
});
|
|
593
520
|
}
|
|
521
|
+
//#endregion
|
|
522
|
+
//#region src/components/SelectPrompt/SelectPromptHint.tsx
|
|
594
523
|
function SelectPromptHint({ message = "Select option", escapeLabel = "cancel" }) {
|
|
595
524
|
return /* @__PURE__ */ jsxs(Box, {
|
|
596
525
|
flexDirection: "row",
|
|
@@ -646,7 +575,7 @@ var options$1 = [
|
|
|
646
575
|
value: PLAN
|
|
647
576
|
}
|
|
648
577
|
];
|
|
649
|
-
function PlanApproval({ planContent, onModeChange }) {
|
|
578
|
+
function PlanApproval({ planContent, onModeChange, theme = getTheme() }) {
|
|
650
579
|
return /* @__PURE__ */ jsx(Box, {
|
|
651
580
|
marginX: 2,
|
|
652
581
|
children: /* @__PURE__ */ jsx(SelectPrompt, {
|
|
@@ -663,7 +592,7 @@ function PlanApproval({ planContent, onModeChange }) {
|
|
|
663
592
|
children: [
|
|
664
593
|
/* @__PURE__ */ jsx(Text, {
|
|
665
594
|
bold: true,
|
|
666
|
-
color:
|
|
595
|
+
color: theme.colors.accent,
|
|
667
596
|
children: "Plan Generated - Choose execution mode:"
|
|
668
597
|
}),
|
|
669
598
|
/* @__PURE__ */ jsx(Box, {
|
|
@@ -685,7 +614,7 @@ var options = [{
|
|
|
685
614
|
label: "Reject tool call",
|
|
686
615
|
value: REJECT
|
|
687
616
|
}];
|
|
688
|
-
function ToolApproval({ toolCall, onDecision }) {
|
|
617
|
+
function ToolApproval({ toolCall, onDecision, theme = getTheme() }) {
|
|
689
618
|
const handleChange = useCallback((value) => {
|
|
690
619
|
onDecision(value);
|
|
691
620
|
}, [onDecision]);
|
|
@@ -701,9 +630,13 @@ function ToolApproval({ toolCall, onDecision }) {
|
|
|
701
630
|
onChange: handleChange,
|
|
702
631
|
onCancel: handleEscape,
|
|
703
632
|
children: [
|
|
704
|
-
/* @__PURE__ */
|
|
705
|
-
color:
|
|
706
|
-
children:
|
|
633
|
+
/* @__PURE__ */ jsxs(Text, {
|
|
634
|
+
color: theme.colors.warning,
|
|
635
|
+
children: [
|
|
636
|
+
"Tool requires approval ",
|
|
637
|
+
WARNING,
|
|
638
|
+
" "
|
|
639
|
+
]
|
|
707
640
|
}),
|
|
708
641
|
/* @__PURE__ */ jsxs(Box, {
|
|
709
642
|
flexDirection: "column",
|
|
@@ -888,8 +821,57 @@ function CommandMenu({ input, onSubmit }) {
|
|
|
888
821
|
});
|
|
889
822
|
}
|
|
890
823
|
//#endregion
|
|
824
|
+
//#region src/components/Suggestions.tsx
|
|
825
|
+
var DEFAULT_MAX_VISIBLE_OPTIONS = 5;
|
|
826
|
+
function Suggestions({ options, isDisabled = false, maxVisibleOptions = DEFAULT_MAX_VISIBLE_OPTIONS, resetKey, onHighlight, onSelect }) {
|
|
827
|
+
const [focusedIndex, setFocusedIndex] = useState(0);
|
|
828
|
+
useEffect(() => {
|
|
829
|
+
setFocusedIndex(0);
|
|
830
|
+
}, [resetKey]);
|
|
831
|
+
useEffect(() => {
|
|
832
|
+
if (!options.length) {
|
|
833
|
+
setFocusedIndex(0);
|
|
834
|
+
onHighlight?.(null);
|
|
835
|
+
return;
|
|
836
|
+
}
|
|
837
|
+
setFocusedIndex((currentIndex) => Math.min(currentIndex, options.length - 1));
|
|
838
|
+
}, [onHighlight, options]);
|
|
839
|
+
useEffect(() => {
|
|
840
|
+
onHighlight?.(options[focusedIndex] ?? null);
|
|
841
|
+
}, [
|
|
842
|
+
focusedIndex,
|
|
843
|
+
onHighlight,
|
|
844
|
+
options
|
|
845
|
+
]);
|
|
846
|
+
useInput((_, key) => {
|
|
847
|
+
if (isDisabled || !options.length) return;
|
|
848
|
+
if (key.downArrow) {
|
|
849
|
+
setFocusedIndex((currentIndex) => Math.min(currentIndex + 1, options.length - 1));
|
|
850
|
+
return;
|
|
851
|
+
}
|
|
852
|
+
if (key.upArrow) {
|
|
853
|
+
setFocusedIndex((currentIndex) => Math.max(currentIndex - 1, 0));
|
|
854
|
+
return;
|
|
855
|
+
}
|
|
856
|
+
if (key.tab || key.return) onSelect(options[focusedIndex]);
|
|
857
|
+
});
|
|
858
|
+
if (!options.length) return null;
|
|
859
|
+
const visibleStart = Math.min(Math.max(0, focusedIndex - maxVisibleOptions + 1), Math.max(0, options.length - maxVisibleOptions));
|
|
860
|
+
return /* @__PURE__ */ jsx(Box, {
|
|
861
|
+
flexDirection: "column",
|
|
862
|
+
children: options.slice(visibleStart, visibleStart + maxVisibleOptions).map((option, index) => {
|
|
863
|
+
return /* @__PURE__ */ jsx(Box, {
|
|
864
|
+
marginLeft: 2,
|
|
865
|
+
children: /* @__PURE__ */ jsx(Text, {
|
|
866
|
+
color: visibleStart + index === focusedIndex ? "cyan" : void 0,
|
|
867
|
+
children: option.label
|
|
868
|
+
})
|
|
869
|
+
}, option.label);
|
|
870
|
+
})
|
|
871
|
+
});
|
|
872
|
+
}
|
|
873
|
+
//#endregion
|
|
891
874
|
//#region src/components/Chat/FileSuggestions.tsx
|
|
892
|
-
var MAX_VISIBLE_OPTIONS = 5;
|
|
893
875
|
var MENTION_PATTERN = /(^|.)@(\S+)/;
|
|
894
876
|
var RIPGREP_MAX_BUFFER = 10 * 1024 * 1024;
|
|
895
877
|
function normalizePath(filePath) {
|
|
@@ -968,7 +950,6 @@ async function listProjectFiles(rootDir) {
|
|
|
968
950
|
}
|
|
969
951
|
function FileSuggestions({ input, isDisabled = false, onChange, onSelect }) {
|
|
970
952
|
const [filePaths, setFilePaths] = useState([]);
|
|
971
|
-
const [focusedIndex, setFocusedIndex] = useState(0);
|
|
972
953
|
useEffect(() => {
|
|
973
954
|
async function loadProjectFiles() {
|
|
974
955
|
setFilePaths(await listProjectFiles(process.cwd()));
|
|
@@ -981,55 +962,29 @@ function FileSuggestions({ input, isDisabled = false, onChange, onSelect }) {
|
|
|
981
962
|
const normalizedQuery = mentionMatch.query.toLowerCase();
|
|
982
963
|
return filePaths.filter((filePath) => filePath.toLowerCase().includes(normalizedQuery));
|
|
983
964
|
}, [filePaths, mentionMatch]);
|
|
984
|
-
useEffect(() => {
|
|
985
|
-
setFocusedIndex(0);
|
|
986
|
-
}, [input]);
|
|
987
|
-
useEffect(() => {
|
|
988
|
-
if (!options.length) {
|
|
989
|
-
setFocusedIndex(0);
|
|
990
|
-
return;
|
|
991
|
-
}
|
|
992
|
-
setFocusedIndex((currentIndex) => Math.min(currentIndex, options.length - 1));
|
|
993
|
-
}, [options]);
|
|
994
965
|
useEffect(() => {
|
|
995
966
|
if (!onChange) return;
|
|
996
|
-
if (!mentionMatch || !options.length)
|
|
997
|
-
onChange(null);
|
|
998
|
-
return;
|
|
999
|
-
}
|
|
1000
|
-
onChange(buildNextInput(input, options[focusedIndex]).value);
|
|
967
|
+
if (!mentionMatch || !options.length) onChange(null);
|
|
1001
968
|
}, [
|
|
1002
|
-
focusedIndex,
|
|
1003
|
-
input,
|
|
1004
969
|
mentionMatch,
|
|
1005
970
|
onChange,
|
|
1006
971
|
options
|
|
1007
972
|
]);
|
|
1008
|
-
useInput((_, key) => {
|
|
1009
|
-
if (isDisabled || !options.length) return;
|
|
1010
|
-
if (key.downArrow) {
|
|
1011
|
-
setFocusedIndex((currentIndex) => Math.min(currentIndex + 1, options.length - 1));
|
|
1012
|
-
return;
|
|
1013
|
-
}
|
|
1014
|
-
if (key.upArrow) {
|
|
1015
|
-
setFocusedIndex((currentIndex) => Math.max(currentIndex - 1, 0));
|
|
1016
|
-
return;
|
|
1017
|
-
}
|
|
1018
|
-
if (key.tab || key.return) onSelect(buildNextInput(input, options[focusedIndex]));
|
|
1019
|
-
});
|
|
1020
973
|
if (!mentionMatch || !options.length) return null;
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
974
|
+
return /* @__PURE__ */ jsx(Suggestions, {
|
|
975
|
+
isDisabled,
|
|
976
|
+
options: options.map((option) => ({
|
|
977
|
+
label: option,
|
|
978
|
+
value: option
|
|
979
|
+
})),
|
|
980
|
+
resetKey: input,
|
|
981
|
+
onHighlight: (option) => {
|
|
982
|
+
// v8 ignore next
|
|
983
|
+
onChange?.(option ? buildNextInput(input, option.value).value : null);
|
|
984
|
+
},
|
|
985
|
+
onSelect: (option) => {
|
|
986
|
+
onSelect(buildNextInput(input, option.value));
|
|
987
|
+
}
|
|
1033
988
|
});
|
|
1034
989
|
}
|
|
1035
990
|
//#endregion
|
|
@@ -1205,7 +1160,7 @@ function hasExecutablePlan(content) {
|
|
|
1205
1160
|
}
|
|
1206
1161
|
//#endregion
|
|
1207
1162
|
//#region src/components/Chat/Chat.tsx
|
|
1208
|
-
function Chat({ initialMessages, model, onCommand, onMessagesChange, mode, onModeChange, sessionId }) {
|
|
1163
|
+
function Chat({ initialMessages, model, onCommand, onMessagesChange, mode, onModeChange, sessionId, theme = getTheme() }) {
|
|
1209
1164
|
const sessionMessages = initialMessages ?? [];
|
|
1210
1165
|
const history = useMemo(() => sessionMessages.flatMap(({ role, content }) => role === "user" && !content.startsWith("/") ? [content] : []), [sessionMessages]);
|
|
1211
1166
|
const [messages, setMessages] = useState(sessionMessages);
|
|
@@ -1322,13 +1277,13 @@ function Chat({ initialMessages, model, onCommand, onMessagesChange, mode, onMod
|
|
|
1322
1277
|
return;
|
|
1323
1278
|
}
|
|
1324
1279
|
}
|
|
1325
|
-
await prewarmCodeBlocks(assistantMessage.content);
|
|
1280
|
+
await prewarmCodeBlocks(assistantMessage.content, theme);
|
|
1326
1281
|
commitAssistantMessage();
|
|
1327
1282
|
} catch (error) {
|
|
1328
1283
|
// v8 ignore next
|
|
1329
1284
|
if (!controller.signal.aborted) {
|
|
1330
1285
|
assistantMessage.content = `Error: ${error instanceof Error ? error.message : String(error)}`;
|
|
1331
|
-
await prewarmCodeBlocks(assistantMessage.content);
|
|
1286
|
+
await prewarmCodeBlocks(assistantMessage.content, theme);
|
|
1332
1287
|
commitAssistantMessage();
|
|
1333
1288
|
}
|
|
1334
1289
|
} finally {
|
|
@@ -1338,7 +1293,8 @@ function Chat({ initialMessages, model, onCommand, onMessagesChange, mode, onMod
|
|
|
1338
1293
|
}, [
|
|
1339
1294
|
buildToolResultMessage,
|
|
1340
1295
|
model,
|
|
1341
|
-
mode
|
|
1296
|
+
mode,
|
|
1297
|
+
theme
|
|
1342
1298
|
]);
|
|
1343
1299
|
const processStreamReadOnly = useCallback(async (currentMessages) => {
|
|
1344
1300
|
const controller = new AbortController();
|
|
@@ -1394,7 +1350,7 @@ function Chat({ initialMessages, model, onCommand, onMessagesChange, mode, onMod
|
|
|
1394
1350
|
return;
|
|
1395
1351
|
}
|
|
1396
1352
|
}
|
|
1397
|
-
await prewarmCodeBlocks(assistantMessage.content);
|
|
1353
|
+
await prewarmCodeBlocks(assistantMessage.content, theme);
|
|
1398
1354
|
const researchMessages = commitAssistantMessage();
|
|
1399
1355
|
const planInstruction = {
|
|
1400
1356
|
role: SYSTEM,
|
|
@@ -1435,7 +1391,7 @@ function Chat({ initialMessages, model, onCommand, onMessagesChange, mode, onMod
|
|
|
1435
1391
|
// v8 ignore next
|
|
1436
1392
|
if (!controller.signal.aborted) {
|
|
1437
1393
|
assistantMessage.content = `Error: ${error instanceof Error ? error.message : String(error)}`;
|
|
1438
|
-
await prewarmCodeBlocks(assistantMessage.content);
|
|
1394
|
+
await prewarmCodeBlocks(assistantMessage.content, theme);
|
|
1439
1395
|
commitAssistantMessage();
|
|
1440
1396
|
}
|
|
1441
1397
|
} finally {
|
|
@@ -1445,7 +1401,8 @@ function Chat({ initialMessages, model, onCommand, onMessagesChange, mode, onMod
|
|
|
1445
1401
|
}, [
|
|
1446
1402
|
buildPlanModeCorrectionMessage,
|
|
1447
1403
|
buildToolResultMessage,
|
|
1448
|
-
model
|
|
1404
|
+
model,
|
|
1405
|
+
theme
|
|
1449
1406
|
]);
|
|
1450
1407
|
const handlePlanApproval = useCallback(async (mode) => {
|
|
1451
1408
|
// v8 ignore next
|
|
@@ -1537,21 +1494,24 @@ function Chat({ initialMessages, model, onCommand, onMessagesChange, mode, onMod
|
|
|
1537
1494
|
messages,
|
|
1538
1495
|
isLoading,
|
|
1539
1496
|
sessionId,
|
|
1540
|
-
streamingMessage
|
|
1497
|
+
streamingMessage,
|
|
1498
|
+
theme
|
|
1541
1499
|
}),
|
|
1542
1500
|
pendingPlan && /* @__PURE__ */ jsx(PlanApproval, {
|
|
1543
1501
|
planContent: pendingPlan.planContent,
|
|
1544
|
-
onModeChange: handlePlanApproval
|
|
1502
|
+
onModeChange: handlePlanApproval,
|
|
1503
|
+
theme
|
|
1545
1504
|
}),
|
|
1546
1505
|
!pendingPlan && pendingToolCall && /* @__PURE__ */ jsx(ToolApproval, {
|
|
1547
1506
|
toolCall: pendingToolCall,
|
|
1548
|
-
onDecision: handleToolApproval
|
|
1507
|
+
onDecision: handleToolApproval,
|
|
1508
|
+
theme
|
|
1549
1509
|
}),
|
|
1550
1510
|
interruptReason && !isLoading && /* @__PURE__ */ jsx(Box, {
|
|
1551
1511
|
marginBottom: 1,
|
|
1552
1512
|
children: /* @__PURE__ */ jsx(Text, {
|
|
1553
|
-
color:
|
|
1554
|
-
children: interruptReason === INTERRUPT_REASON.REJECTED ?
|
|
1513
|
+
color: theme.colors.error,
|
|
1514
|
+
children: interruptReason === INTERRUPT_REASON.REJECTED ? `❗ Tool call rejected.` : `❗ Execution interrupted.`
|
|
1555
1515
|
})
|
|
1556
1516
|
}),
|
|
1557
1517
|
!pendingPlan && !pendingToolCall && /* @__PURE__ */ jsx(Box, {
|
|
@@ -1568,29 +1528,31 @@ function Chat({ initialMessages, model, onCommand, onMessagesChange, mode, onMod
|
|
|
1568
1528
|
}
|
|
1569
1529
|
//#endregion
|
|
1570
1530
|
//#region src/components/Footer.tsx
|
|
1571
|
-
function getModeColor(mode) {
|
|
1531
|
+
function getModeColor(mode, theme) {
|
|
1572
1532
|
switch (mode) {
|
|
1573
|
-
case PLAN: return
|
|
1574
|
-
case AUTO: return
|
|
1575
|
-
case SAFE: return
|
|
1533
|
+
case PLAN: return theme.colors.modePlan;
|
|
1534
|
+
case AUTO: return theme.colors.modeAuto;
|
|
1535
|
+
case SAFE: return theme.colors.modeSafe;
|
|
1576
1536
|
// v8 ignore next
|
|
1577
1537
|
default: return;
|
|
1578
1538
|
}
|
|
1579
1539
|
}
|
|
1580
|
-
function Footer({ mode, model, onToggleMode }) {
|
|
1540
|
+
function Footer({ mode, model, onToggleMode, theme = getTheme() }) {
|
|
1581
1541
|
useInput((_, key) => {
|
|
1582
1542
|
if (key.tab && key.shift) onToggleMode();
|
|
1583
1543
|
});
|
|
1584
1544
|
const modeLabel = LABEL[mode];
|
|
1545
|
+
const modeColor = getModeColor(mode, theme);
|
|
1585
1546
|
return /* @__PURE__ */ jsx(Box, {
|
|
1586
1547
|
justifyContent: "space-between",
|
|
1587
1548
|
marginTop: 1,
|
|
1588
1549
|
children: /* @__PURE__ */ jsxs(Text, {
|
|
1550
|
+
color: theme.colors.secondary,
|
|
1589
1551
|
dimColor: true,
|
|
1590
1552
|
children: [
|
|
1591
1553
|
"Mode: ",
|
|
1592
1554
|
/* @__PURE__ */ jsx(Text, {
|
|
1593
|
-
color:
|
|
1555
|
+
color: modeColor,
|
|
1594
1556
|
children: modeLabel
|
|
1595
1557
|
}),
|
|
1596
1558
|
" (Shift+Tab to toggle)",
|
|
@@ -1598,7 +1560,7 @@ function Footer({ mode, model, onToggleMode }) {
|
|
|
1598
1560
|
"❖",
|
|
1599
1561
|
" Model: ",
|
|
1600
1562
|
/* @__PURE__ */ jsx(Text, {
|
|
1601
|
-
color:
|
|
1563
|
+
color: theme.colors.model,
|
|
1602
1564
|
children: model
|
|
1603
1565
|
})
|
|
1604
1566
|
]
|
|
@@ -1611,7 +1573,7 @@ function abbreviatePath(dir) {
|
|
|
1611
1573
|
const home = homedir();
|
|
1612
1574
|
return dir.startsWith(home) ? `~${dir.slice(home.length)}` : dir;
|
|
1613
1575
|
}
|
|
1614
|
-
function Header({ model, onLoad }) {
|
|
1576
|
+
function Header({ model, onLoad, theme = getTheme() }) {
|
|
1615
1577
|
const directory = abbreviatePath(process.cwd());
|
|
1616
1578
|
useEffect(() => {
|
|
1617
1579
|
onLoad();
|
|
@@ -1627,9 +1589,11 @@ function Header({ model, onLoad }) {
|
|
|
1627
1589
|
bold: true,
|
|
1628
1590
|
children: [HEADER_PREFIX, "Code Ollama"]
|
|
1629
1591
|
}), /* @__PURE__ */ jsxs(Text, {
|
|
1592
|
+
color: theme.colors.secondary,
|
|
1630
1593
|
dimColor: true,
|
|
1631
1594
|
children: [
|
|
1632
|
-
"
|
|
1595
|
+
" ",
|
|
1596
|
+
"(v",
|
|
1633
1597
|
VERSION,
|
|
1634
1598
|
")"
|
|
1635
1599
|
]
|
|
@@ -1638,21 +1602,24 @@ function Header({ model, onLoad }) {
|
|
|
1638
1602
|
marginTop: 1,
|
|
1639
1603
|
children: [
|
|
1640
1604
|
/* @__PURE__ */ jsx(Text, {
|
|
1605
|
+
color: theme.colors.secondary,
|
|
1641
1606
|
dimColor: true,
|
|
1642
1607
|
children: "model:".padEnd(11)
|
|
1643
1608
|
}),
|
|
1644
1609
|
/* @__PURE__ */ jsx(Text, { children: model.padEnd(model.length + 3) }),
|
|
1645
1610
|
/* @__PURE__ */ jsx(Text, {
|
|
1646
|
-
color:
|
|
1611
|
+
color: theme.colors.command,
|
|
1647
1612
|
children: "/model"
|
|
1648
1613
|
}),
|
|
1649
|
-
/* @__PURE__ */
|
|
1614
|
+
/* @__PURE__ */ jsxs(Text, {
|
|
1615
|
+
color: theme.colors.secondary,
|
|
1650
1616
|
dimColor: true,
|
|
1651
|
-
children: " to
|
|
1617
|
+
children: [" ", "to manage"]
|
|
1652
1618
|
})
|
|
1653
1619
|
]
|
|
1654
1620
|
}),
|
|
1655
1621
|
/* @__PURE__ */ jsxs(Box, { children: [/* @__PURE__ */ jsx(Text, {
|
|
1622
|
+
color: theme.colors.secondary,
|
|
1656
1623
|
dimColor: true,
|
|
1657
1624
|
children: "directory:".padEnd(11)
|
|
1658
1625
|
}), /* @__PURE__ */ jsx(Text, { children: directory })] })
|
|
@@ -1661,53 +1628,613 @@ function Header({ model, onLoad }) {
|
|
|
1661
1628
|
});
|
|
1662
1629
|
}
|
|
1663
1630
|
//#endregion
|
|
1664
|
-
//#region src/components/
|
|
1665
|
-
function
|
|
1666
|
-
const
|
|
1667
|
-
const
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1631
|
+
//#region src/components/ModelManager/ModelSuggestions.tsx
|
|
1632
|
+
function rankCatalogMatch(entry, normalizedInput) {
|
|
1633
|
+
const normalizedValue = entry.value.toLowerCase();
|
|
1634
|
+
const normalizedLabel = entry.label.toLowerCase();
|
|
1635
|
+
// v8 ignore start
|
|
1636
|
+
switch (true) {
|
|
1637
|
+
case normalizedValue.startsWith(normalizedInput): return 0;
|
|
1638
|
+
case normalizedLabel.startsWith(normalizedInput): return 1;
|
|
1639
|
+
case normalizedValue.includes(normalizedInput): return 2;
|
|
1640
|
+
case normalizedLabel.includes(normalizedInput): return 3;
|
|
1641
|
+
default: return Number.MAX_SAFE_INTEGER;
|
|
1642
|
+
}
|
|
1643
|
+
// v8 ignore stop
|
|
1644
|
+
}
|
|
1645
|
+
function ModelSuggestions({ catalog, input, isDisabled = false, onHighlight, onSelect }) {
|
|
1646
|
+
const normalizedInput = input.trim().toLowerCase();
|
|
1647
|
+
return /* @__PURE__ */ jsx(Suggestions, {
|
|
1648
|
+
isDisabled,
|
|
1649
|
+
options: useMemo(() => {
|
|
1650
|
+
if (!normalizedInput) return [];
|
|
1651
|
+
return catalog.filter((entry) => entry.value.toLowerCase().includes(normalizedInput) || entry.label.toLowerCase().includes(normalizedInput)).sort((left, right) => rankCatalogMatch(left, normalizedInput) - rankCatalogMatch(right, normalizedInput) || left.label.localeCompare(right.label)).map((entry) => ({
|
|
1652
|
+
label: entry.value,
|
|
1653
|
+
value: entry.value
|
|
1654
|
+
}));
|
|
1655
|
+
}, [catalog, normalizedInput]),
|
|
1656
|
+
resetKey: input,
|
|
1657
|
+
/* v8 ignore next */
|
|
1658
|
+
onHighlight: (option) => onHighlight?.(option?.value ?? null),
|
|
1659
|
+
onSelect: (option) => {
|
|
1660
|
+
onSelect(option.value);
|
|
1675
1661
|
}
|
|
1676
1662
|
});
|
|
1663
|
+
}
|
|
1664
|
+
//#endregion
|
|
1665
|
+
//#region src/components/ModelManager/types.ts
|
|
1666
|
+
var View = /* @__PURE__ */ function(View) {
|
|
1667
|
+
View["Menu"] = "menu";
|
|
1668
|
+
View["Switch"] = "switch";
|
|
1669
|
+
View["Download"] = "download";
|
|
1670
|
+
View["CustomDownload"] = "custom-download";
|
|
1671
|
+
View["Downloading"] = "downloading";
|
|
1672
|
+
View["Delete"] = "delete";
|
|
1673
|
+
View["DeleteConfirm"] = "delete-confirm";
|
|
1674
|
+
return View;
|
|
1675
|
+
}({});
|
|
1676
|
+
var MenuAction = /* @__PURE__ */ function(MenuAction) {
|
|
1677
|
+
MenuAction["Switch"] = "switch";
|
|
1678
|
+
MenuAction["Download"] = "download";
|
|
1679
|
+
MenuAction["Delete"] = "delete";
|
|
1680
|
+
MenuAction["Cancel"] = "cancel";
|
|
1681
|
+
return MenuAction;
|
|
1682
|
+
}({});
|
|
1683
|
+
var DownloadAction = /* @__PURE__ */ function(DownloadAction) {
|
|
1684
|
+
DownloadAction["Custom"] = "custom";
|
|
1685
|
+
return DownloadAction;
|
|
1686
|
+
}({});
|
|
1687
|
+
var ConfirmDeleteAction = /* @__PURE__ */ function(ConfirmDeleteAction) {
|
|
1688
|
+
ConfirmDeleteAction["Delete"] = "delete";
|
|
1689
|
+
return ConfirmDeleteAction;
|
|
1690
|
+
}({});
|
|
1691
|
+
//#endregion
|
|
1692
|
+
//#region src/components/ModelManager/utils.ts
|
|
1693
|
+
function buildMenuOptions() {
|
|
1694
|
+
return [
|
|
1695
|
+
{
|
|
1696
|
+
label: "Switch model",
|
|
1697
|
+
value: MenuAction.Switch
|
|
1698
|
+
},
|
|
1699
|
+
{
|
|
1700
|
+
label: "Download model",
|
|
1701
|
+
value: MenuAction.Download
|
|
1702
|
+
},
|
|
1703
|
+
{
|
|
1704
|
+
label: "Delete model",
|
|
1705
|
+
value: MenuAction.Delete
|
|
1706
|
+
},
|
|
1707
|
+
{
|
|
1708
|
+
label: "Cancel",
|
|
1709
|
+
value: MenuAction.Cancel
|
|
1710
|
+
}
|
|
1711
|
+
];
|
|
1712
|
+
}
|
|
1713
|
+
function buildInstalledModelOptions(models, currentModel) {
|
|
1714
|
+
const nextModels = [...models];
|
|
1715
|
+
if (nextModels.includes(currentModel)) {
|
|
1716
|
+
nextModels.splice(nextModels.indexOf(currentModel), 1);
|
|
1717
|
+
nextModels.unshift(currentModel);
|
|
1718
|
+
}
|
|
1719
|
+
return nextModels.map((model) => ({
|
|
1720
|
+
label: model === currentModel ? `${model} (current model)` : model,
|
|
1721
|
+
value: model
|
|
1722
|
+
}));
|
|
1723
|
+
}
|
|
1724
|
+
function buildDownloadOptions(installedModels) {
|
|
1725
|
+
const installedModelSet = new Set(installedModels);
|
|
1726
|
+
const availableCatalog = CATALOG.filter(({ value, alias }) => !installedModelSet.has(value) && !(alias && installedModelSet.has(alias)));
|
|
1727
|
+
return [
|
|
1728
|
+
{
|
|
1729
|
+
label: "Enter custom model...",
|
|
1730
|
+
value: DownloadAction.Custom
|
|
1731
|
+
},
|
|
1732
|
+
...availableCatalog.map(({ label, value }) => ({
|
|
1733
|
+
label,
|
|
1734
|
+
value
|
|
1735
|
+
})),
|
|
1736
|
+
BACK
|
|
1737
|
+
];
|
|
1738
|
+
}
|
|
1739
|
+
function getNoticeColor(tone, theme) {
|
|
1740
|
+
switch (tone) {
|
|
1741
|
+
case "error": return theme.colors.error;
|
|
1742
|
+
case "success": return theme.colors.status;
|
|
1743
|
+
default: return theme.colors.secondary;
|
|
1744
|
+
}
|
|
1745
|
+
}
|
|
1746
|
+
function formatBytes(bytes) {
|
|
1747
|
+
if (!Number.isFinite(bytes) || bytes <= 0) return "0 B";
|
|
1748
|
+
const units = [
|
|
1749
|
+
"B",
|
|
1750
|
+
"KB",
|
|
1751
|
+
"MB",
|
|
1752
|
+
"GB",
|
|
1753
|
+
"TB"
|
|
1754
|
+
];
|
|
1755
|
+
let value = bytes;
|
|
1756
|
+
let index = 0;
|
|
1757
|
+
while (value >= 1024 && index < units.length - 1) {
|
|
1758
|
+
value /= 1024;
|
|
1759
|
+
index += 1;
|
|
1760
|
+
}
|
|
1761
|
+
const fractionDigits = value >= 10 || index === 0 ? 0 : 1;
|
|
1762
|
+
return `${value.toFixed(fractionDigits)} ${units[index]}`;
|
|
1763
|
+
}
|
|
1764
|
+
function isAbortError(error) {
|
|
1765
|
+
return error instanceof Error && error.name === "AbortError";
|
|
1766
|
+
}
|
|
1767
|
+
function mergeDownloadProgress(previous, model, status, completed, total) {
|
|
1768
|
+
const nextCompleted = typeof completed === "number" && Number.isFinite(completed) && completed >= 0 ? completed : null;
|
|
1769
|
+
const nextTotal = typeof total === "number" && Number.isFinite(total) && total > 0 ? total : null;
|
|
1770
|
+
if (nextTotal !== null && nextCompleted !== null) return {
|
|
1771
|
+
model,
|
|
1772
|
+
status,
|
|
1773
|
+
completed: nextCompleted,
|
|
1774
|
+
total: nextTotal
|
|
1775
|
+
};
|
|
1776
|
+
const hasPreviousProgress = previous?.model === model && previous.total > 0;
|
|
1777
|
+
return {
|
|
1778
|
+
model,
|
|
1779
|
+
status,
|
|
1780
|
+
completed: hasPreviousProgress ? previous.completed : 0,
|
|
1781
|
+
total: hasPreviousProgress ? previous.total : 0
|
|
1782
|
+
};
|
|
1783
|
+
}
|
|
1784
|
+
//#endregion
|
|
1785
|
+
//#region src/components/ModelManager/ModelCustomDownloadView.tsx
|
|
1786
|
+
function ModelCustomDownloadView({ downloadDraft, notice, theme, onDraftChange, onHighlight, onSelectSuggestion, onSubmit }) {
|
|
1787
|
+
const renderNotice = () => notice ? /* @__PURE__ */ jsx(Text, {
|
|
1788
|
+
color: getNoticeColor(notice.tone, theme),
|
|
1789
|
+
children: notice.text
|
|
1790
|
+
}) : null;
|
|
1791
|
+
return /* @__PURE__ */ jsxs(Box, {
|
|
1792
|
+
flexDirection: "column",
|
|
1793
|
+
children: [
|
|
1794
|
+
/* @__PURE__ */ jsx(Text, { children: "Enter an Ollama model name to download." }),
|
|
1795
|
+
/* @__PURE__ */ jsxs(Box, { children: [/* @__PURE__ */ jsx(Text, { children: "> " }), /* @__PURE__ */ jsx(TextInput, {
|
|
1796
|
+
value: downloadDraft,
|
|
1797
|
+
placeholder: "name:tag",
|
|
1798
|
+
wrapIndent: 2,
|
|
1799
|
+
onChange: onDraftChange,
|
|
1800
|
+
onSubmit
|
|
1801
|
+
})] }),
|
|
1802
|
+
/* @__PURE__ */ jsx(ModelSuggestions, {
|
|
1803
|
+
catalog: [],
|
|
1804
|
+
input: downloadDraft,
|
|
1805
|
+
onHighlight,
|
|
1806
|
+
onSelect: onSelectSuggestion
|
|
1807
|
+
}),
|
|
1808
|
+
renderNotice(),
|
|
1809
|
+
/* @__PURE__ */ jsx(Text, {
|
|
1810
|
+
color: theme.colors.secondary,
|
|
1811
|
+
dimColor: true,
|
|
1812
|
+
children: "Press Enter to download, Esc or Ctrl+C to go back."
|
|
1813
|
+
})
|
|
1814
|
+
]
|
|
1815
|
+
});
|
|
1816
|
+
}
|
|
1817
|
+
//#endregion
|
|
1818
|
+
//#region src/components/ModelManager/ModelDeleteConfirmView.tsx
|
|
1819
|
+
function ModelDeleteConfirmView({ deleteCandidate, isDeleting, notice, theme, onCancel, onConfirm }) {
|
|
1820
|
+
if (isDeleting) return /* @__PURE__ */ jsx(Spinner, { label: `Deleting model ${deleteCandidate}...` });
|
|
1821
|
+
const renderNotice = () => notice ? /* @__PURE__ */ jsx(Text, {
|
|
1822
|
+
color: getNoticeColor(notice.tone, theme),
|
|
1823
|
+
children: notice.text
|
|
1824
|
+
}) : null;
|
|
1825
|
+
return /* @__PURE__ */ jsxs(SelectPrompt, {
|
|
1826
|
+
options: [{
|
|
1827
|
+
label: `Yes, delete ${deleteCandidate}`,
|
|
1828
|
+
value: ConfirmDeleteAction.Delete
|
|
1829
|
+
}, {
|
|
1830
|
+
...BACK,
|
|
1831
|
+
label: "No"
|
|
1832
|
+
}],
|
|
1833
|
+
onCancel,
|
|
1834
|
+
onChange: onConfirm,
|
|
1835
|
+
children: [
|
|
1836
|
+
/* @__PURE__ */ jsxs(Text, { children: [
|
|
1837
|
+
WARNING,
|
|
1838
|
+
" Delete model",
|
|
1839
|
+
" ",
|
|
1840
|
+
/* @__PURE__ */ jsx(Text, {
|
|
1841
|
+
color: theme.colors.model,
|
|
1842
|
+
children: deleteCandidate
|
|
1843
|
+
}),
|
|
1844
|
+
"?"
|
|
1845
|
+
] }),
|
|
1846
|
+
renderNotice(),
|
|
1847
|
+
/* @__PURE__ */ jsx(SelectPromptHint, { message: "This action cannot be undone" })
|
|
1848
|
+
]
|
|
1849
|
+
});
|
|
1850
|
+
}
|
|
1851
|
+
//#endregion
|
|
1852
|
+
//#region src/components/ModelManager/ModelDeleteView.tsx
|
|
1853
|
+
function ModelDeleteView({ currentModel, installedModels, isLoading, notice, theme, onCancel, onSelect }) {
|
|
1854
|
+
if (isLoading) return /* @__PURE__ */ jsx(Spinner, { label: "Loading models..." });
|
|
1855
|
+
return /* @__PURE__ */ jsxs(SelectPrompt, {
|
|
1856
|
+
options: [...buildInstalledModelOptions(installedModels.filter((model) => model !== currentModel), currentModel), BACK],
|
|
1857
|
+
onCancel,
|
|
1858
|
+
onChange: (value) => {
|
|
1859
|
+
if (value === BACK.value) onCancel();
|
|
1860
|
+
else onSelect(value);
|
|
1861
|
+
},
|
|
1862
|
+
children: [
|
|
1863
|
+
/* @__PURE__ */ jsxs(Text, { children: [
|
|
1864
|
+
"Delete an installed model (current model",
|
|
1865
|
+
" ",
|
|
1866
|
+
/* @__PURE__ */ jsx(Text, {
|
|
1867
|
+
color: theme.colors.model,
|
|
1868
|
+
children: currentModel
|
|
1869
|
+
}),
|
|
1870
|
+
" cannot be deleted)."
|
|
1871
|
+
] }),
|
|
1872
|
+
notice && /* @__PURE__ */ jsx(Text, {
|
|
1873
|
+
color: getNoticeColor(notice.tone, theme),
|
|
1874
|
+
children: notice.text
|
|
1875
|
+
}),
|
|
1876
|
+
/* @__PURE__ */ jsx(SelectPromptHint, { message: "Delete models" })
|
|
1877
|
+
]
|
|
1878
|
+
});
|
|
1879
|
+
}
|
|
1880
|
+
//#endregion
|
|
1881
|
+
//#region src/components/ModelManager/ModelDownloadingView.tsx
|
|
1882
|
+
function ModelDownloadingView({ progress, theme, onCancel }) {
|
|
1883
|
+
const percent = progress.total > 0 && Number.isFinite(progress.completed) && Number.isFinite(progress.total) ? Math.round(progress.completed / progress.total * 100) : null;
|
|
1884
|
+
return /* @__PURE__ */ jsxs(Box, {
|
|
1885
|
+
flexDirection: "column",
|
|
1886
|
+
children: [
|
|
1887
|
+
/* @__PURE__ */ jsxs(Text, { children: [
|
|
1888
|
+
"Downloading model:",
|
|
1889
|
+
" ",
|
|
1890
|
+
/* @__PURE__ */ jsx(Text, {
|
|
1891
|
+
color: theme.colors.model,
|
|
1892
|
+
children: progress.model
|
|
1893
|
+
})
|
|
1894
|
+
] }),
|
|
1895
|
+
/* @__PURE__ */ jsx(Text, { children: progress.status }),
|
|
1896
|
+
percent !== null ? /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsxs(Text, { children: [
|
|
1897
|
+
percent,
|
|
1898
|
+
"% (",
|
|
1899
|
+
formatBytes(progress.completed),
|
|
1900
|
+
" /",
|
|
1901
|
+
" ",
|
|
1902
|
+
formatBytes(progress.total),
|
|
1903
|
+
")"
|
|
1904
|
+
] }), /* @__PURE__ */ jsx(ProgressBar, { value: Math.max(0, Math.min(100, percent)) })] }) : /* @__PURE__ */ jsx(Text, {
|
|
1905
|
+
color: theme.colors.secondary,
|
|
1906
|
+
dimColor: true,
|
|
1907
|
+
children: "Progress details unavailable. Waiting for Ollama updates..."
|
|
1908
|
+
}),
|
|
1909
|
+
/* @__PURE__ */ jsx(SelectPrompt, {
|
|
1910
|
+
options: [{
|
|
1911
|
+
label: "Cancel download",
|
|
1912
|
+
value: "cancel-download"
|
|
1913
|
+
}],
|
|
1914
|
+
onCancel,
|
|
1915
|
+
onChange: onCancel,
|
|
1916
|
+
children: /* @__PURE__ */ jsx(SelectPromptHint, { message: "Press Enter, Esc, or Ctrl+C to cancel" })
|
|
1917
|
+
})
|
|
1918
|
+
]
|
|
1919
|
+
});
|
|
1920
|
+
}
|
|
1921
|
+
//#endregion
|
|
1922
|
+
//#region src/components/ModelManager/ModelDownloadView.tsx
|
|
1923
|
+
function ModelDownloadView({ installedModels, notice, theme, onCancel, onChange }) {
|
|
1924
|
+
return /* @__PURE__ */ jsxs(SelectPrompt, {
|
|
1925
|
+
options: buildDownloadOptions(installedModels),
|
|
1926
|
+
onCancel,
|
|
1927
|
+
onChange: (value) => {
|
|
1928
|
+
switch (value) {
|
|
1929
|
+
case "custom":
|
|
1930
|
+
onChange("custom");
|
|
1931
|
+
break;
|
|
1932
|
+
case "back":
|
|
1933
|
+
onCancel();
|
|
1934
|
+
break;
|
|
1935
|
+
default:
|
|
1936
|
+
onChange(value);
|
|
1937
|
+
break;
|
|
1938
|
+
}
|
|
1939
|
+
},
|
|
1940
|
+
children: [
|
|
1941
|
+
/* @__PURE__ */ jsx(Text, { children: "Choose a model to download or use a custom model name." }),
|
|
1942
|
+
notice && /* @__PURE__ */ jsx(Text, {
|
|
1943
|
+
color: getNoticeColor(notice.tone, theme),
|
|
1944
|
+
children: notice.text
|
|
1945
|
+
}),
|
|
1946
|
+
/* @__PURE__ */ jsx(SelectPromptHint, { message: "Download models" })
|
|
1947
|
+
]
|
|
1948
|
+
});
|
|
1949
|
+
}
|
|
1950
|
+
//#endregion
|
|
1951
|
+
//#region src/components/ModelManager/ModelSwitchView.tsx
|
|
1952
|
+
function ModelSwitchView({ currentModel, installedModels, isLoading, onCancel, onSelect }) {
|
|
1953
|
+
if (isLoading) return /* @__PURE__ */ jsx(Spinner, { label: "Loading models..." });
|
|
1954
|
+
return /* @__PURE__ */ jsx(SelectPrompt, {
|
|
1955
|
+
options: [...buildInstalledModelOptions(installedModels, currentModel), BACK],
|
|
1956
|
+
onCancel,
|
|
1957
|
+
onChange: (value) => {
|
|
1958
|
+
if (value === BACK.value) onCancel();
|
|
1959
|
+
else onSelect(value);
|
|
1960
|
+
},
|
|
1961
|
+
children: /* @__PURE__ */ jsx(SelectPromptHint, { message: "Switch models" })
|
|
1962
|
+
});
|
|
1963
|
+
}
|
|
1964
|
+
//#endregion
|
|
1965
|
+
//#region src/components/ModelManager/ModelManager.tsx
|
|
1966
|
+
function ModelManager({ currentModel, onSelect, onClose, theme = getTheme() }) {
|
|
1967
|
+
const [view, setView] = useState(View.Menu);
|
|
1968
|
+
const [installedModels, setInstalledModels] = useState([]);
|
|
1969
|
+
const [isLoadingModels, setIsLoadingModels] = useState(true);
|
|
1970
|
+
const [loadError, setLoadError] = useState(null);
|
|
1971
|
+
const [notice, setNotice] = useState(null);
|
|
1972
|
+
const [downloadDraft, setDownloadDraft] = useState("");
|
|
1973
|
+
const [highlightedSuggestion, setHighlightedSuggestion] = useState(null);
|
|
1974
|
+
const [downloadProgress, setDownloadProgress] = useState(null);
|
|
1975
|
+
const [deleteCandidate, setDeleteCandidate] = useState(null);
|
|
1976
|
+
const [isDeleting, setIsDeleting] = useState(false);
|
|
1977
|
+
const isDeletingRef = useRef(false);
|
|
1978
|
+
const pullRef = useRef(null);
|
|
1979
|
+
const loadInstalledModels = useCallback(async () => {
|
|
1980
|
+
setIsLoadingModels(true);
|
|
1981
|
+
setLoadError(null);
|
|
1982
|
+
try {
|
|
1983
|
+
setInstalledModels(await listModels());
|
|
1984
|
+
} catch (error) {
|
|
1985
|
+
setLoadError(error instanceof Error ? error.message : /* v8 ignore next */ String(error));
|
|
1986
|
+
} finally {
|
|
1987
|
+
setIsLoadingModels(false);
|
|
1988
|
+
}
|
|
1989
|
+
}, []);
|
|
1677
1990
|
useEffect(() => {
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1991
|
+
loadInstalledModels();
|
|
1992
|
+
}, [loadInstalledModels]);
|
|
1993
|
+
const resetDownloadState = useCallback(() => {
|
|
1994
|
+
setDownloadDraft("");
|
|
1995
|
+
setHighlightedSuggestion(null);
|
|
1996
|
+
setDownloadProgress(null);
|
|
1997
|
+
pullRef.current = null;
|
|
1998
|
+
}, []);
|
|
1999
|
+
const handleBackToMenu = useCallback(() => {
|
|
2000
|
+
setNotice(null);
|
|
2001
|
+
setDeleteCandidate(null);
|
|
2002
|
+
setIsDeleting(false);
|
|
2003
|
+
isDeletingRef.current = false;
|
|
2004
|
+
resetDownloadState();
|
|
2005
|
+
setView(View.Menu);
|
|
2006
|
+
}, [resetDownloadState]);
|
|
2007
|
+
const cancelActivePull = useCallback(() => {
|
|
2008
|
+
pullRef.current?.abort();
|
|
2009
|
+
}, []);
|
|
2010
|
+
useInput((input, key) => {
|
|
2011
|
+
if (view === View.CustomDownload && (key.escape || key.ctrl && input === "c")) {
|
|
2012
|
+
setNotice(null);
|
|
2013
|
+
setHighlightedSuggestion(null);
|
|
2014
|
+
setView(View.Download);
|
|
2015
|
+
return;
|
|
2016
|
+
}
|
|
2017
|
+
if (view === View.Downloading && (key.escape || key.ctrl && input === "c")) cancelActivePull();
|
|
2018
|
+
});
|
|
2019
|
+
const handleMenuChange = useCallback((value) => {
|
|
2020
|
+
setNotice(null);
|
|
2021
|
+
switch (value) {
|
|
2022
|
+
case "switch":
|
|
2023
|
+
setView(View.Switch);
|
|
2024
|
+
break;
|
|
2025
|
+
case "download":
|
|
2026
|
+
setView(View.Download);
|
|
2027
|
+
break;
|
|
2028
|
+
case "delete":
|
|
2029
|
+
setView(View.Delete);
|
|
2030
|
+
break;
|
|
2031
|
+
default: onClose();
|
|
2032
|
+
}
|
|
2033
|
+
}, [onClose]);
|
|
2034
|
+
const handleSwitchChange = useCallback((model) => {
|
|
2035
|
+
onSelect({ model });
|
|
2036
|
+
}, [onSelect]);
|
|
2037
|
+
const startPull = useCallback(async (model) => {
|
|
2038
|
+
const normalizedModel = model.trim();
|
|
2039
|
+
if (!normalizedModel) {
|
|
2040
|
+
setNotice({
|
|
2041
|
+
tone: "error",
|
|
2042
|
+
text: `❗ Enter a model name to download`
|
|
2043
|
+
});
|
|
2044
|
+
return;
|
|
2045
|
+
}
|
|
2046
|
+
if (installedModels.includes(normalizedModel)) {
|
|
2047
|
+
setNotice({
|
|
2048
|
+
tone: "info",
|
|
2049
|
+
text: `${normalizedModel} is already installed`
|
|
2050
|
+
});
|
|
2051
|
+
return;
|
|
2052
|
+
}
|
|
2053
|
+
setNotice(null);
|
|
2054
|
+
setDownloadProgress({
|
|
2055
|
+
model: normalizedModel,
|
|
2056
|
+
status: "Starting download...",
|
|
2057
|
+
completed: 0,
|
|
2058
|
+
total: 0
|
|
2059
|
+
});
|
|
2060
|
+
setView(View.Downloading);
|
|
2061
|
+
try {
|
|
2062
|
+
const pull = await pullModel(normalizedModel);
|
|
2063
|
+
pullRef.current = pull;
|
|
2064
|
+
for await (const update of pull) setDownloadProgress((previous) => {
|
|
2065
|
+
return mergeDownloadProgress(previous, normalizedModel, update.status, update.completed, update.total);
|
|
2066
|
+
});
|
|
2067
|
+
pullRef.current = null;
|
|
2068
|
+
resetDownloadState();
|
|
2069
|
+
await loadInstalledModels();
|
|
2070
|
+
setNotice({
|
|
2071
|
+
tone: "success",
|
|
2072
|
+
text: `✅ ${normalizedModel} downloaded successfully`
|
|
2073
|
+
});
|
|
2074
|
+
setView(View.Menu);
|
|
2075
|
+
} catch (error) {
|
|
2076
|
+
pullRef.current = null;
|
|
2077
|
+
if (isAbortError(error)) {
|
|
2078
|
+
setNotice({
|
|
2079
|
+
tone: "error",
|
|
2080
|
+
text: `❌ Download canceled for ${normalizedModel}`
|
|
2081
|
+
});
|
|
2082
|
+
setDownloadProgress(null);
|
|
2083
|
+
setView(View.Download);
|
|
2084
|
+
return;
|
|
1691
2085
|
}
|
|
2086
|
+
setNotice({
|
|
2087
|
+
tone: "error",
|
|
2088
|
+
text: `❗ Error downloading model: ${error instanceof Error ? error.message : /* v8 ignore next */ String(error)}`
|
|
2089
|
+
});
|
|
2090
|
+
setDownloadProgress(null);
|
|
2091
|
+
setView(View.CustomDownload);
|
|
2092
|
+
}
|
|
2093
|
+
}, [
|
|
2094
|
+
installedModels,
|
|
2095
|
+
loadInstalledModels,
|
|
2096
|
+
resetDownloadState
|
|
2097
|
+
]);
|
|
2098
|
+
const handleDownloadChange = useCallback((value) => {
|
|
2099
|
+
if (value === "custom") {
|
|
2100
|
+
setNotice(null);
|
|
2101
|
+
setView(View.CustomDownload);
|
|
2102
|
+
return;
|
|
2103
|
+
}
|
|
2104
|
+
// v8 ignore next 3
|
|
2105
|
+
if (value === "back") {
|
|
2106
|
+
handleBackToMenu();
|
|
2107
|
+
return;
|
|
2108
|
+
}
|
|
2109
|
+
setDownloadDraft(value);
|
|
2110
|
+
startPull(value);
|
|
2111
|
+
}, [handleBackToMenu, startPull]);
|
|
2112
|
+
const handleCustomDownloadSubmit = useCallback((value) => {
|
|
2113
|
+
const nextValue = highlightedSuggestion ?? value.trim();
|
|
2114
|
+
setDownloadDraft(nextValue);
|
|
2115
|
+
startPull(nextValue);
|
|
2116
|
+
}, [highlightedSuggestion, startPull]);
|
|
2117
|
+
const handleDeleteChange = useCallback((model) => {
|
|
2118
|
+
setNotice(null);
|
|
2119
|
+
setDeleteCandidate(model);
|
|
2120
|
+
setView(View.DeleteConfirm);
|
|
2121
|
+
}, []);
|
|
2122
|
+
const handleDeleteConfirm = useCallback(async (value) => {
|
|
2123
|
+
if (isDeletingRef.current) return;
|
|
2124
|
+
if (value === "back") {
|
|
2125
|
+
setView(View.Delete);
|
|
2126
|
+
return;
|
|
2127
|
+
}
|
|
2128
|
+
// v8 ignore next 3
|
|
2129
|
+
if (!deleteCandidate) {
|
|
2130
|
+
setView(View.Delete);
|
|
2131
|
+
return;
|
|
2132
|
+
}
|
|
2133
|
+
try {
|
|
2134
|
+
isDeletingRef.current = true;
|
|
2135
|
+
setIsDeleting(true);
|
|
2136
|
+
await deleteModel(deleteCandidate);
|
|
2137
|
+
await loadInstalledModels();
|
|
2138
|
+
setNotice({
|
|
2139
|
+
tone: "success",
|
|
2140
|
+
text: `✅ ${deleteCandidate} deleted successfully`
|
|
2141
|
+
});
|
|
2142
|
+
isDeletingRef.current = false;
|
|
2143
|
+
setIsDeleting(false);
|
|
2144
|
+
setDeleteCandidate(null);
|
|
2145
|
+
setView(View.Delete);
|
|
2146
|
+
} catch (error) {
|
|
2147
|
+
isDeletingRef.current = false;
|
|
2148
|
+
setIsDeleting(false);
|
|
2149
|
+
setNotice({
|
|
2150
|
+
tone: "error",
|
|
2151
|
+
text: `❗ Error deleting model: ${error instanceof Error ? error.message : /* v8 ignore next */ String(error)}`
|
|
2152
|
+
});
|
|
2153
|
+
setView(View.Delete);
|
|
1692
2154
|
}
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
2155
|
+
}, [deleteCandidate, loadInstalledModels]);
|
|
2156
|
+
const renderNotice = () => notice ? /* @__PURE__ */ jsx(Text, {
|
|
2157
|
+
color: notice.tone === "error" ? theme.colors.error : notice.tone === "success" ? theme.colors.status : theme.colors.secondary,
|
|
2158
|
+
children: notice.text
|
|
2159
|
+
}) : null;
|
|
2160
|
+
if (loadError && view !== View.Menu) return /* @__PURE__ */ jsxs(Box, {
|
|
2161
|
+
flexDirection: "column",
|
|
2162
|
+
children: [/* @__PURE__ */ jsxs(Text, {
|
|
2163
|
+
color: theme.colors.error,
|
|
2164
|
+
children: ["Error loading models: ", loadError]
|
|
2165
|
+
}), /* @__PURE__ */ jsx(Text, {
|
|
2166
|
+
color: theme.colors.secondary,
|
|
2167
|
+
dimColor: true,
|
|
2168
|
+
children: "Press Esc to go back."
|
|
2169
|
+
})]
|
|
1698
2170
|
});
|
|
1699
|
-
if (
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
2171
|
+
if (view === View.Downloading && downloadProgress) return /* @__PURE__ */ jsx(ModelDownloadingView, {
|
|
2172
|
+
progress: downloadProgress,
|
|
2173
|
+
theme,
|
|
2174
|
+
onCancel: cancelActivePull
|
|
2175
|
+
});
|
|
2176
|
+
if (view === View.CustomDownload) return /* @__PURE__ */ jsx(ModelCustomDownloadView, {
|
|
2177
|
+
downloadDraft,
|
|
2178
|
+
notice,
|
|
2179
|
+
theme,
|
|
2180
|
+
onDraftChange: setDownloadDraft,
|
|
2181
|
+
onHighlight: setHighlightedSuggestion,
|
|
2182
|
+
onSelectSuggestion: (value) => {
|
|
2183
|
+
setDownloadDraft(value);
|
|
2184
|
+
setHighlightedSuggestion(value);
|
|
2185
|
+
},
|
|
2186
|
+
onSubmit: handleCustomDownloadSubmit
|
|
2187
|
+
});
|
|
2188
|
+
if (view === View.Switch) return /* @__PURE__ */ jsx(ModelSwitchView, {
|
|
2189
|
+
currentModel,
|
|
2190
|
+
installedModels,
|
|
2191
|
+
isLoading: isLoadingModels,
|
|
2192
|
+
onCancel: handleBackToMenu,
|
|
2193
|
+
onSelect: handleSwitchChange
|
|
2194
|
+
});
|
|
2195
|
+
if (view === View.Download) return /* @__PURE__ */ jsx(ModelDownloadView, {
|
|
2196
|
+
installedModels,
|
|
2197
|
+
notice,
|
|
2198
|
+
theme,
|
|
2199
|
+
onCancel: handleBackToMenu,
|
|
2200
|
+
onChange: handleDownloadChange
|
|
2201
|
+
});
|
|
2202
|
+
if (view === View.Delete) return /* @__PURE__ */ jsx(ModelDeleteView, {
|
|
2203
|
+
currentModel,
|
|
2204
|
+
installedModels,
|
|
2205
|
+
isLoading: isLoadingModels,
|
|
2206
|
+
notice,
|
|
2207
|
+
theme,
|
|
2208
|
+
onCancel: handleBackToMenu,
|
|
2209
|
+
onSelect: handleDeleteChange
|
|
2210
|
+
});
|
|
2211
|
+
if (view === View.DeleteConfirm && deleteCandidate) return /* @__PURE__ */ jsx(ModelDeleteConfirmView, {
|
|
2212
|
+
deleteCandidate,
|
|
2213
|
+
isDeleting,
|
|
2214
|
+
notice,
|
|
2215
|
+
theme,
|
|
2216
|
+
onCancel: () => {
|
|
2217
|
+
setView(View.Delete);
|
|
2218
|
+
},
|
|
2219
|
+
onConfirm: handleDeleteConfirm
|
|
2220
|
+
});
|
|
2221
|
+
return /* @__PURE__ */ jsxs(SelectPrompt, {
|
|
2222
|
+
options: buildMenuOptions(),
|
|
1704
2223
|
onCancel: onClose,
|
|
1705
|
-
|
|
2224
|
+
onChange: handleMenuChange,
|
|
2225
|
+
children: [
|
|
2226
|
+
/* @__PURE__ */ jsxs(Text, { children: ["Current model: ", /* @__PURE__ */ jsx(Text, {
|
|
2227
|
+
color: theme.colors.model,
|
|
2228
|
+
children: currentModel
|
|
2229
|
+
})] }),
|
|
2230
|
+
renderNotice(),
|
|
2231
|
+
/* @__PURE__ */ jsx(SelectPromptHint, { message: "Manage models" })
|
|
2232
|
+
]
|
|
1706
2233
|
});
|
|
1707
2234
|
}
|
|
1708
2235
|
//#endregion
|
|
1709
2236
|
//#region src/components/SearchSettings.tsx
|
|
1710
|
-
function SearchSettings({ currentUrl, onClose, onSave }) {
|
|
2237
|
+
function SearchSettings({ currentUrl, onClose, onSave, theme = getTheme() }) {
|
|
1711
2238
|
const [view, setView] = useState("menu");
|
|
1712
2239
|
const [draftUrl, setDraftUrl] = useState(currentUrl ?? "");
|
|
1713
2240
|
const [error, setError] = useState(null);
|
|
@@ -1779,10 +2306,11 @@ function SearchSettings({ currentUrl, onClose, onSave }) {
|
|
|
1779
2306
|
placeholder: "http://localhost:8080"
|
|
1780
2307
|
})] }),
|
|
1781
2308
|
error && /* @__PURE__ */ jsx(Text, {
|
|
1782
|
-
color:
|
|
2309
|
+
color: theme.colors.error,
|
|
1783
2310
|
children: error
|
|
1784
2311
|
}),
|
|
1785
2312
|
/* @__PURE__ */ jsx(Text, {
|
|
2313
|
+
color: theme.colors.secondary,
|
|
1786
2314
|
dimColor: true,
|
|
1787
2315
|
children: "Press Enter to save, Esc to go back."
|
|
1788
2316
|
})
|
|
@@ -1793,10 +2321,14 @@ function SearchSettings({ currentUrl, onClose, onSave }) {
|
|
|
1793
2321
|
onChange: handleChange,
|
|
1794
2322
|
onCancel: onClose,
|
|
1795
2323
|
children: [
|
|
1796
|
-
/* @__PURE__ */ jsxs(Text, { children: [
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
2324
|
+
/* @__PURE__ */ jsxs(Text, { children: [
|
|
2325
|
+
"SearXNG URL:",
|
|
2326
|
+
" ",
|
|
2327
|
+
/* @__PURE__ */ jsx(Text, {
|
|
2328
|
+
color: theme.colors.status,
|
|
2329
|
+
children: currentUrl ?? "not set"
|
|
2330
|
+
})
|
|
2331
|
+
] }),
|
|
1800
2332
|
/* @__PURE__ */ jsx(Text, { children: "DuckDuckGo fallback remains available." }),
|
|
1801
2333
|
/* @__PURE__ */ jsx(SelectPromptHint, { message: "Manage web search settings" })
|
|
1802
2334
|
]
|
|
@@ -1805,7 +2337,6 @@ function SearchSettings({ currentUrl, onClose, onSave }) {
|
|
|
1805
2337
|
//#endregion
|
|
1806
2338
|
//#region src/components/SessionManager.tsx
|
|
1807
2339
|
var ACTION = {
|
|
1808
|
-
BACK: "back",
|
|
1809
2340
|
CLOSE: "close",
|
|
1810
2341
|
DELETE_MENU: "delete-menu",
|
|
1811
2342
|
DELETE_PREFIX: "delete:",
|
|
@@ -1823,7 +2354,7 @@ function formatSessionLabel(session, maxWidth, prefix = "") {
|
|
|
1823
2354
|
if (availableTitleWidth < 1) return truncate(`${prefix}${session.title}${suffix}`, maxWidth);
|
|
1824
2355
|
return `${prefix}${truncate(session.title, availableTitleWidth)}${suffix}`;
|
|
1825
2356
|
}
|
|
1826
|
-
function SessionManager({ currentSessionId, onClose, onDelete, onNew, onOpen }) {
|
|
2357
|
+
function SessionManager({ currentSessionId, onClose, onDelete, onNew, onOpen, theme = getTheme() }) {
|
|
1827
2358
|
const [view, setView] = useState("main");
|
|
1828
2359
|
const [error, setError] = useState();
|
|
1829
2360
|
const [, refreshSessionList] = useState(0);
|
|
@@ -1833,16 +2364,10 @@ function SessionManager({ currentSessionId, onClose, onDelete, onNew, onOpen })
|
|
|
1833
2364
|
const options = view === "open" ? [...sessions.filter(({ id }) => id !== currentSessionId).map((session) => ({
|
|
1834
2365
|
label: formatSessionLabel(session, maxLabelWidth),
|
|
1835
2366
|
value: `${ACTION.OPEN_PREFIX}${session.id}`
|
|
1836
|
-
})), {
|
|
1837
|
-
label: "Back",
|
|
1838
|
-
value: ACTION.BACK
|
|
1839
|
-
}] : view === "delete" ? [...sessions.filter(({ id }) => id !== currentSessionId).map((session) => ({
|
|
2367
|
+
})), BACK] : view === "delete" ? [...sessions.filter(({ id }) => id !== currentSessionId).map((session) => ({
|
|
1840
2368
|
label: formatSessionLabel(session, maxLabelWidth, "Delete "),
|
|
1841
2369
|
value: `${ACTION.DELETE_PREFIX}${session.id}`
|
|
1842
|
-
})),
|
|
1843
|
-
label: "Back",
|
|
1844
|
-
value: ACTION.BACK
|
|
1845
|
-
}] : [
|
|
2370
|
+
})), BACK] : [
|
|
1846
2371
|
{
|
|
1847
2372
|
label: "New session",
|
|
1848
2373
|
value: ACTION.NEW
|
|
@@ -1874,7 +2399,7 @@ function SessionManager({ currentSessionId, onClose, onDelete, onNew, onOpen })
|
|
|
1874
2399
|
case value === ACTION.OPEN_MENU:
|
|
1875
2400
|
setView("open");
|
|
1876
2401
|
break;
|
|
1877
|
-
case value ===
|
|
2402
|
+
case value === BACK.value:
|
|
1878
2403
|
setView("main");
|
|
1879
2404
|
break;
|
|
1880
2405
|
case value.startsWith(ACTION.DELETE_PREFIX):
|
|
@@ -1909,7 +2434,7 @@ function SessionManager({ currentSessionId, onClose, onDelete, onNew, onOpen })
|
|
|
1909
2434
|
error && /* @__PURE__ */ jsx(Box, {
|
|
1910
2435
|
marginBottom: 1,
|
|
1911
2436
|
children: /* @__PURE__ */ jsx(Text, {
|
|
1912
|
-
color:
|
|
2437
|
+
color: theme.colors.error,
|
|
1913
2438
|
children: error
|
|
1914
2439
|
})
|
|
1915
2440
|
}),
|
|
@@ -1922,25 +2447,177 @@ function SessionManager({ currentSessionId, onClose, onDelete, onNew, onOpen })
|
|
|
1922
2447
|
});
|
|
1923
2448
|
}
|
|
1924
2449
|
//#endregion
|
|
1925
|
-
//#region src/components/
|
|
1926
|
-
function
|
|
1927
|
-
|
|
2450
|
+
//#region src/components/ThemeSettings.tsx
|
|
2451
|
+
function ThemeSettings({ currentTheme, onClose, onPreview, onSave }) {
|
|
2452
|
+
const [selectedIndex, setSelectedIndex] = useState(() => {
|
|
2453
|
+
const initialIndex = LIST$1.findIndex(({ id }) => id === currentTheme);
|
|
2454
|
+
return initialIndex >= 0 ? initialIndex : 0;
|
|
2455
|
+
});
|
|
2456
|
+
const selectedTheme = useMemo(
|
|
2457
|
+
// v8 ignore next
|
|
2458
|
+
() => LIST$1[selectedIndex] ?? getTheme(),
|
|
2459
|
+
[selectedIndex]
|
|
2460
|
+
);
|
|
2461
|
+
useEffect(() => {
|
|
2462
|
+
onPreview(selectedTheme.id);
|
|
2463
|
+
}, [onPreview, selectedTheme.id]);
|
|
2464
|
+
useInput((input, key) => {
|
|
2465
|
+
if (key.escape || key.ctrl && input === "c") {
|
|
2466
|
+
onClose();
|
|
2467
|
+
return;
|
|
2468
|
+
}
|
|
2469
|
+
if (key.upArrow) {
|
|
2470
|
+
setSelectedIndex((current) => current === 0 ? LIST$1.length - 1 : current - 1);
|
|
2471
|
+
return;
|
|
2472
|
+
}
|
|
2473
|
+
if (key.downArrow) {
|
|
2474
|
+
setSelectedIndex((current) => current === LIST$1.length - 1 ? 0 : current + 1);
|
|
2475
|
+
return;
|
|
2476
|
+
}
|
|
2477
|
+
if (key.return) onSave(selectedTheme.id);
|
|
2478
|
+
});
|
|
2479
|
+
return /* @__PURE__ */ jsxs(Box, {
|
|
2480
|
+
flexDirection: "column",
|
|
2481
|
+
children: [
|
|
2482
|
+
/* @__PURE__ */ jsxs(Text, { children: [
|
|
2483
|
+
"Theme:",
|
|
2484
|
+
" ",
|
|
2485
|
+
/* @__PURE__ */ jsx(Text, {
|
|
2486
|
+
color: selectedTheme.colors.accent,
|
|
2487
|
+
children: selectedTheme.label
|
|
2488
|
+
})
|
|
2489
|
+
] }),
|
|
2490
|
+
/* @__PURE__ */ jsx(Text, {
|
|
2491
|
+
color: selectedTheme.colors.secondary,
|
|
2492
|
+
children: selectedTheme.description
|
|
2493
|
+
}),
|
|
2494
|
+
/* @__PURE__ */ jsx(Box, {
|
|
2495
|
+
flexDirection: "column",
|
|
2496
|
+
marginTop: 1,
|
|
2497
|
+
children: LIST$1.map((theme, index) => {
|
|
2498
|
+
const isSelected = index === selectedIndex;
|
|
2499
|
+
return /* @__PURE__ */ jsxs(Text, {
|
|
2500
|
+
color: isSelected ? selectedTheme.colors.accent : void 0,
|
|
2501
|
+
children: [
|
|
2502
|
+
isSelected ? "›" : " ",
|
|
2503
|
+
" ",
|
|
2504
|
+
theme.label
|
|
2505
|
+
]
|
|
2506
|
+
}, theme.id);
|
|
2507
|
+
})
|
|
2508
|
+
}),
|
|
2509
|
+
/* @__PURE__ */ jsxs(Box, {
|
|
2510
|
+
borderColor: selectedTheme.colors.border,
|
|
2511
|
+
borderStyle: "round",
|
|
2512
|
+
flexDirection: "column",
|
|
2513
|
+
marginTop: 1,
|
|
2514
|
+
paddingX: 1,
|
|
2515
|
+
children: [
|
|
2516
|
+
/* @__PURE__ */ jsxs(Text, {
|
|
2517
|
+
color: selectedTheme.colors.status,
|
|
2518
|
+
children: [HEADER_PREFIX, " Preview"]
|
|
2519
|
+
}),
|
|
2520
|
+
/* @__PURE__ */ jsx(Text, {
|
|
2521
|
+
color: selectedTheme.colors.secondary,
|
|
2522
|
+
children: "Markdown and code styling follow the selected theme."
|
|
2523
|
+
}),
|
|
2524
|
+
/* @__PURE__ */ jsxs(Text, { children: [
|
|
2525
|
+
"Status accent:",
|
|
2526
|
+
" ",
|
|
2527
|
+
/* @__PURE__ */ jsx(Text, {
|
|
2528
|
+
color: selectedTheme.colors.status,
|
|
2529
|
+
children: "search enabled"
|
|
2530
|
+
})
|
|
2531
|
+
] }),
|
|
2532
|
+
/* @__PURE__ */ jsx(CodeBlock, {
|
|
2533
|
+
code: "const theme = 'preview';",
|
|
2534
|
+
language: "ts",
|
|
2535
|
+
role: "assistant",
|
|
2536
|
+
theme: selectedTheme
|
|
2537
|
+
})
|
|
2538
|
+
]
|
|
2539
|
+
}),
|
|
2540
|
+
/* @__PURE__ */ jsx(Box, {
|
|
2541
|
+
marginTop: 1,
|
|
2542
|
+
children: /* @__PURE__ */ jsx(SelectPromptHint, {
|
|
2543
|
+
message: "Preview theme",
|
|
2544
|
+
escapeLabel: "cancel and restore"
|
|
2545
|
+
})
|
|
2546
|
+
})
|
|
2547
|
+
]
|
|
2548
|
+
});
|
|
1928
2549
|
}
|
|
1929
|
-
|
|
2550
|
+
//#endregion
|
|
2551
|
+
//#region src/components/App/constants.ts
|
|
2552
|
+
var SCREEN = /* @__PURE__ */ function(SCREEN) {
|
|
2553
|
+
SCREEN["CHAT"] = "chat";
|
|
2554
|
+
SCREEN["MODEL_MANAGER"] = "model-manager";
|
|
2555
|
+
SCREEN["SEARCH_SETTINGS"] = "search-settings";
|
|
2556
|
+
SCREEN["SESSION_MANAGER"] = "session-manager";
|
|
2557
|
+
SCREEN["THEME_SETTINGS"] = "theme-settings";
|
|
2558
|
+
return SCREEN;
|
|
2559
|
+
}({});
|
|
2560
|
+
//#endregion
|
|
2561
|
+
//#region src/components/App/hooks/useScreenRouter.ts
|
|
2562
|
+
function useScreenRouter() {
|
|
1930
2563
|
const { exit } = useApp();
|
|
1931
|
-
const [
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
|
|
2564
|
+
const [currentScreen, setScreen] = useState(SCREEN.CHAT);
|
|
2565
|
+
return {
|
|
2566
|
+
currentScreen,
|
|
2567
|
+
setScreen,
|
|
2568
|
+
handleClose: useCallback(() => {
|
|
2569
|
+
setScreen(SCREEN.CHAT);
|
|
2570
|
+
}, []),
|
|
2571
|
+
handleCommand: useCallback((command, callbacks) => {
|
|
2572
|
+
const { onCreateSession, onSetPreviewThemeId, model, theme } = callbacks;
|
|
2573
|
+
switch (command) {
|
|
2574
|
+
case "/session":
|
|
2575
|
+
setScreen(SCREEN.SESSION_MANAGER);
|
|
2576
|
+
break;
|
|
2577
|
+
case "/model":
|
|
2578
|
+
setScreen(SCREEN.MODEL_MANAGER);
|
|
2579
|
+
break;
|
|
2580
|
+
case "/search":
|
|
2581
|
+
setScreen(SCREEN.SEARCH_SETTINGS);
|
|
2582
|
+
break;
|
|
2583
|
+
case "/theme":
|
|
2584
|
+
onSetPreviewThemeId(theme);
|
|
2585
|
+
setScreen(SCREEN.THEME_SETTINGS);
|
|
2586
|
+
break;
|
|
2587
|
+
case "/clear": {
|
|
2588
|
+
resetSystemMessage();
|
|
2589
|
+
const nextSession = onCreateSession(model);
|
|
2590
|
+
setScreen(SCREEN.CHAT);
|
|
2591
|
+
clear(nextSession.metadata.id);
|
|
2592
|
+
break;
|
|
2593
|
+
}
|
|
2594
|
+
case "/exit":
|
|
2595
|
+
exit();
|
|
2596
|
+
break;
|
|
2597
|
+
}
|
|
2598
|
+
}, [exit])
|
|
2599
|
+
};
|
|
2600
|
+
}
|
|
2601
|
+
//#endregion
|
|
2602
|
+
//#region src/components/App/hooks/useSessionManager.ts
|
|
2603
|
+
function useSessionManager({ sessionId, model, commandColor }) {
|
|
2604
|
+
const [activeSession, setSession] = useState(() => sessionId ? loadSession(sessionId) : createSession(model));
|
|
1936
2605
|
const sessionRef = useRef(activeSession);
|
|
2606
|
+
const commandColorRef = useRef(commandColor);
|
|
2607
|
+
const modelRef = useRef(model);
|
|
1937
2608
|
useEffect(() => {
|
|
1938
2609
|
sessionRef.current = activeSession;
|
|
1939
2610
|
}, [activeSession]);
|
|
2611
|
+
useEffect(() => {
|
|
2612
|
+
commandColorRef.current = commandColor;
|
|
2613
|
+
}, [commandColor]);
|
|
2614
|
+
useEffect(() => {
|
|
2615
|
+
modelRef.current = model;
|
|
2616
|
+
}, [model]);
|
|
1940
2617
|
useEffect(() => {
|
|
1941
2618
|
return () => {
|
|
1942
2619
|
const currentSession = sessionRef.current;
|
|
1943
|
-
if (!deleteSessionIfEmpty(currentSession.metadata.id) && currentSession.messages.length > 0) write(`Resume session: ${color(`code-ollama resume ${currentSession.metadata.id}`,
|
|
2620
|
+
if (!deleteSessionIfEmpty(currentSession.metadata.id) && currentSession.messages.length > 0) write(`Resume session: ${color(`code-ollama resume ${currentSession.metadata.id}`, commandColorRef.current)}\n`);
|
|
1944
2621
|
};
|
|
1945
2622
|
}, []);
|
|
1946
2623
|
const setActiveSession = useCallback((nextSession) => {
|
|
@@ -1949,73 +2626,76 @@ function App({ sessionId }) {
|
|
|
1949
2626
|
return nextSession;
|
|
1950
2627
|
});
|
|
1951
2628
|
}, []);
|
|
1952
|
-
|
|
1953
|
-
|
|
2629
|
+
return {
|
|
2630
|
+
activeSession,
|
|
2631
|
+
setSession,
|
|
2632
|
+
handleCreateSession: useCallback(() => {
|
|
2633
|
+
const nextSession = createSession(modelRef.current);
|
|
2634
|
+
setActiveSession(nextSession);
|
|
2635
|
+
clear(nextSession.metadata.id);
|
|
2636
|
+
return nextSession;
|
|
2637
|
+
}, [setActiveSession]),
|
|
2638
|
+
handleOpenSession: useCallback((sessionId) => {
|
|
2639
|
+
if (sessionRef.current.metadata.id === sessionId) return false;
|
|
2640
|
+
setActiveSession(loadSession(sessionId));
|
|
2641
|
+
clear(sessionId);
|
|
2642
|
+
return true;
|
|
2643
|
+
}, [setActiveSession]),
|
|
2644
|
+
handleDeleteSession: useCallback((sessionId) => {
|
|
2645
|
+
deleteSession(sessionId);
|
|
2646
|
+
setSession((current) => {
|
|
2647
|
+
if (current.metadata.id !== sessionId) return current;
|
|
2648
|
+
return createSession(modelRef.current);
|
|
2649
|
+
});
|
|
2650
|
+
}, []),
|
|
2651
|
+
handleMessagesChange: useCallback((messages) => {
|
|
2652
|
+
setSession((current) => {
|
|
2653
|
+
const persistedMessages = messages.filter(({ content }) => content !== TURN_ABORTED_MESSAGE);
|
|
2654
|
+
if (persistedMessages.length <= current.messages.length) return current;
|
|
2655
|
+
let metadata = current.metadata;
|
|
2656
|
+
for (const message of persistedMessages.slice(current.messages.length)) metadata = appendMessage(metadata.id, message, modelRef.current);
|
|
2657
|
+
return {
|
|
2658
|
+
metadata,
|
|
2659
|
+
messages: persistedMessages
|
|
2660
|
+
};
|
|
2661
|
+
});
|
|
2662
|
+
}, [])
|
|
2663
|
+
};
|
|
2664
|
+
}
|
|
2665
|
+
//#endregion
|
|
2666
|
+
//#region src/components/App/hooks/useThemeSettings.ts
|
|
2667
|
+
function useThemeSettings({ currentTheme, onUpdateConfig, setScreen }) {
|
|
2668
|
+
const [previewThemeId, setPreviewThemeId] = useState(null);
|
|
2669
|
+
const activeTheme = getTheme(previewThemeId ?? currentTheme);
|
|
2670
|
+
const handleThemePreview = useCallback((themeId) => {
|
|
2671
|
+
setPreviewThemeId(themeId);
|
|
1954
2672
|
}, []);
|
|
1955
|
-
|
|
1956
|
-
|
|
1957
|
-
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
|
|
1962
|
-
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
|
|
1976
|
-
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
|
|
1981
|
-
const persistedMessages = messages.filter(({ content }) => content !== TURN_ABORTED_MESSAGE);
|
|
1982
|
-
if (persistedMessages.length <= current.messages.length) return current;
|
|
1983
|
-
let metadata = current.metadata;
|
|
1984
|
-
for (const message of persistedMessages.slice(current.messages.length)) metadata = appendMessage(metadata.id, message, appConfig.model);
|
|
1985
|
-
return {
|
|
1986
|
-
metadata,
|
|
1987
|
-
messages: persistedMessages
|
|
1988
|
-
};
|
|
1989
|
-
});
|
|
1990
|
-
}, [appConfig.model]);
|
|
1991
|
-
const handleCommand = useCallback((command) => {
|
|
1992
|
-
switch (command) {
|
|
1993
|
-
case "/session":
|
|
1994
|
-
setScreen("session-manager");
|
|
1995
|
-
break;
|
|
1996
|
-
case "/model":
|
|
1997
|
-
setScreen("model-picker");
|
|
1998
|
-
break;
|
|
1999
|
-
case "/search":
|
|
2000
|
-
setScreen("search-settings");
|
|
2001
|
-
break;
|
|
2002
|
-
case "/clear": {
|
|
2003
|
-
resetSystemMessage();
|
|
2004
|
-
setScreen("chat");
|
|
2005
|
-
const nextSession = createSession$1(appConfig.model);
|
|
2006
|
-
setActiveSession(nextSession);
|
|
2007
|
-
clear(nextSession.metadata.id);
|
|
2008
|
-
break;
|
|
2009
|
-
}
|
|
2010
|
-
case "/exit":
|
|
2011
|
-
exit();
|
|
2012
|
-
break;
|
|
2013
|
-
}
|
|
2014
|
-
}, [
|
|
2015
|
-
appConfig.model,
|
|
2016
|
-
exit,
|
|
2017
|
-
setActiveSession
|
|
2018
|
-
]);
|
|
2673
|
+
return {
|
|
2674
|
+
activeTheme,
|
|
2675
|
+
handleThemeClose: useCallback(() => {
|
|
2676
|
+
setPreviewThemeId(null);
|
|
2677
|
+
setScreen(SCREEN.CHAT);
|
|
2678
|
+
}, [setScreen]),
|
|
2679
|
+
handleThemePreview,
|
|
2680
|
+
handleThemeSave: useCallback((themeId) => {
|
|
2681
|
+
setPreviewThemeId(null);
|
|
2682
|
+
onUpdateConfig({ theme: themeId });
|
|
2683
|
+
}, [onUpdateConfig]),
|
|
2684
|
+
setPreviewThemeId
|
|
2685
|
+
};
|
|
2686
|
+
}
|
|
2687
|
+
//#endregion
|
|
2688
|
+
//#region src/components/App/App.tsx
|
|
2689
|
+
function App({ sessionId }) {
|
|
2690
|
+
const [appConfig, setConfig] = useState(() => loadConfig());
|
|
2691
|
+
const [mode, setMode] = useState(SAFE);
|
|
2692
|
+
const [isHeaderLoaded, setIsHeaderLoaded] = useState(false);
|
|
2693
|
+
const { currentScreen, setScreen, handleClose, handleCommand } = useScreenRouter();
|
|
2694
|
+
const { activeSession, setSession, handleCreateSession, handleOpenSession, handleDeleteSession, handleMessagesChange } = useSessionManager({
|
|
2695
|
+
sessionId,
|
|
2696
|
+
model: appConfig.model,
|
|
2697
|
+
commandColor: getTheme(appConfig.theme).colors.command
|
|
2698
|
+
});
|
|
2019
2699
|
const handleUpdateConfig = useCallback((update) => {
|
|
2020
2700
|
setConfig((current) => ({
|
|
2021
2701
|
...current,
|
|
@@ -2027,10 +2707,15 @@ function App({ sessionId }) {
|
|
|
2027
2707
|
...current,
|
|
2028
2708
|
metadata: updateSessionModel(current.metadata.id, newModel)
|
|
2029
2709
|
}));
|
|
2030
|
-
setScreen(
|
|
2031
|
-
}, []);
|
|
2032
|
-
const
|
|
2033
|
-
|
|
2710
|
+
setScreen(SCREEN.CHAT);
|
|
2711
|
+
}, [setScreen, setSession]);
|
|
2712
|
+
const { activeTheme, handleThemeClose, handleThemePreview, handleThemeSave, setPreviewThemeId } = useThemeSettings({
|
|
2713
|
+
currentTheme: appConfig.theme,
|
|
2714
|
+
onUpdateConfig: handleUpdateConfig,
|
|
2715
|
+
setScreen
|
|
2716
|
+
});
|
|
2717
|
+
const handleHeaderLoad = useCallback(() => {
|
|
2718
|
+
setIsHeaderLoaded(true);
|
|
2034
2719
|
}, []);
|
|
2035
2720
|
const handleToggleMode = useCallback(() => {
|
|
2036
2721
|
setMode((mode) => {
|
|
@@ -2042,40 +2727,78 @@ function App({ sessionId }) {
|
|
|
2042
2727
|
}
|
|
2043
2728
|
});
|
|
2044
2729
|
}, []);
|
|
2730
|
+
const handleChatCommand = useCallback((command) => {
|
|
2731
|
+
handleCommand(command, {
|
|
2732
|
+
model: appConfig.model,
|
|
2733
|
+
theme: appConfig.theme,
|
|
2734
|
+
onCreateSession: handleCreateSession,
|
|
2735
|
+
onSetPreviewThemeId: setPreviewThemeId
|
|
2736
|
+
});
|
|
2737
|
+
}, [
|
|
2738
|
+
appConfig.model,
|
|
2739
|
+
appConfig.theme,
|
|
2740
|
+
handleCommand,
|
|
2741
|
+
handleCreateSession,
|
|
2742
|
+
setPreviewThemeId
|
|
2743
|
+
]);
|
|
2744
|
+
const handleDeleteSessionAndStay = useCallback((sid) => {
|
|
2745
|
+
handleDeleteSession(sid);
|
|
2746
|
+
setScreen(SCREEN.SESSION_MANAGER);
|
|
2747
|
+
}, [handleDeleteSession, setScreen]);
|
|
2748
|
+
const handleOpenSessionAndNavigate = useCallback((sid) => {
|
|
2749
|
+
handleOpenSession(sid);
|
|
2750
|
+
setScreen(SCREEN.CHAT);
|
|
2751
|
+
}, [handleOpenSession, setScreen]);
|
|
2752
|
+
const handleCreateSessionAndNavigate = useCallback(() => {
|
|
2753
|
+
handleCreateSession();
|
|
2754
|
+
setScreen(SCREEN.CHAT);
|
|
2755
|
+
}, [handleCreateSession, setScreen]);
|
|
2045
2756
|
let screenContent;
|
|
2046
2757
|
switch (currentScreen) {
|
|
2047
|
-
case
|
|
2048
|
-
screenContent = /* @__PURE__ */ jsx(
|
|
2758
|
+
case SCREEN.MODEL_MANAGER:
|
|
2759
|
+
screenContent = /* @__PURE__ */ jsx(ModelManager, {
|
|
2049
2760
|
currentModel: appConfig.model,
|
|
2050
2761
|
onSelect: handleUpdateConfig,
|
|
2051
|
-
onClose: handleClose
|
|
2762
|
+
onClose: handleClose,
|
|
2763
|
+
theme: activeTheme
|
|
2052
2764
|
});
|
|
2053
2765
|
break;
|
|
2054
|
-
case
|
|
2766
|
+
case SCREEN.SEARCH_SETTINGS:
|
|
2055
2767
|
screenContent = /* @__PURE__ */ jsx(SearchSettings, {
|
|
2056
2768
|
currentUrl: appConfig.searxngBaseUrl,
|
|
2057
2769
|
onSave: handleUpdateConfig,
|
|
2058
|
-
onClose: handleClose
|
|
2770
|
+
onClose: handleClose,
|
|
2771
|
+
theme: activeTheme
|
|
2059
2772
|
});
|
|
2060
2773
|
break;
|
|
2061
|
-
case
|
|
2774
|
+
case SCREEN.SESSION_MANAGER:
|
|
2062
2775
|
screenContent = /* @__PURE__ */ jsx(SessionManager, {
|
|
2063
2776
|
currentSessionId: activeSession.metadata.id,
|
|
2064
2777
|
onClose: handleClose,
|
|
2065
|
-
onDelete:
|
|
2066
|
-
onNew:
|
|
2067
|
-
onOpen:
|
|
2778
|
+
onDelete: handleDeleteSessionAndStay,
|
|
2779
|
+
onNew: handleCreateSessionAndNavigate,
|
|
2780
|
+
onOpen: handleOpenSessionAndNavigate,
|
|
2781
|
+
theme: activeTheme
|
|
2782
|
+
});
|
|
2783
|
+
break;
|
|
2784
|
+
case SCREEN.THEME_SETTINGS:
|
|
2785
|
+
screenContent = /* @__PURE__ */ jsx(ThemeSettings, {
|
|
2786
|
+
currentTheme: appConfig.theme,
|
|
2787
|
+
onClose: handleThemeClose,
|
|
2788
|
+
onPreview: handleThemePreview,
|
|
2789
|
+
onSave: handleThemeSave
|
|
2068
2790
|
});
|
|
2069
2791
|
break;
|
|
2070
|
-
case
|
|
2792
|
+
case SCREEN.CHAT:
|
|
2071
2793
|
screenContent = /* @__PURE__ */ jsx(Chat, {
|
|
2072
2794
|
initialMessages: activeSession.messages,
|
|
2073
2795
|
model: appConfig.model,
|
|
2074
|
-
onCommand:
|
|
2796
|
+
onCommand: handleChatCommand,
|
|
2075
2797
|
onMessagesChange: handleMessagesChange,
|
|
2076
2798
|
mode,
|
|
2077
2799
|
onModeChange: setMode,
|
|
2078
|
-
sessionId: activeSession.metadata.id
|
|
2800
|
+
sessionId: activeSession.metadata.id,
|
|
2801
|
+
theme: activeTheme
|
|
2079
2802
|
});
|
|
2080
2803
|
break;
|
|
2081
2804
|
}
|
|
@@ -2084,13 +2807,15 @@ function App({ sessionId }) {
|
|
|
2084
2807
|
children: [
|
|
2085
2808
|
/* @__PURE__ */ jsx(Header, {
|
|
2086
2809
|
model: appConfig.model,
|
|
2087
|
-
onLoad: handleHeaderLoad
|
|
2810
|
+
onLoad: handleHeaderLoad,
|
|
2811
|
+
theme: activeTheme
|
|
2088
2812
|
}),
|
|
2089
2813
|
isHeaderLoaded && screenContent,
|
|
2090
2814
|
/* @__PURE__ */ jsx(Footer, {
|
|
2091
2815
|
mode,
|
|
2092
2816
|
model: appConfig.model,
|
|
2093
|
-
onToggleMode: handleToggleMode
|
|
2817
|
+
onToggleMode: handleToggleMode,
|
|
2818
|
+
theme: activeTheme
|
|
2094
2819
|
})
|
|
2095
2820
|
]
|
|
2096
2821
|
});
|