@makefinks/daemon 0.3.0 → 0.4.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/package.json +1 -1
- package/src/components/ContentBlockView.tsx +5 -11
- package/src/components/ModelMenu.tsx +59 -13
- package/src/hooks/use-app-preferences-bootstrap.ts +3 -6
- package/src/hooks/use-reasoning-animation.ts +9 -4
- package/src/ui/constants.ts +5 -3
- package/src/ui/reasoning-ticker.tsx +39 -0
package/package.json
CHANGED
|
@@ -2,11 +2,12 @@
|
|
|
2
2
|
* Component for rendering a single content block (reasoning, tool, or text).
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
import { DaemonText } from "./DaemonText";
|
|
6
|
-
import { ToolCallView } from "./ToolCallView";
|
|
7
|
-
import { COLORS, REASONING_MARKDOWN_STYLE } from "../ui/constants";
|
|
8
5
|
import type { ContentBlock } from "../types";
|
|
6
|
+
import { COLORS, REASONING_MARKDOWN_STYLE } from "../ui/constants";
|
|
7
|
+
import { renderReasoningTicker } from "../ui/reasoning-ticker";
|
|
9
8
|
import { formatElapsedTime, hasVisibleText } from "../utils/formatters";
|
|
9
|
+
import { DaemonText } from "./DaemonText";
|
|
10
|
+
import { ToolCallView } from "./ToolCallView";
|
|
10
11
|
|
|
11
12
|
interface ContentBlockViewProps {
|
|
12
13
|
block: ContentBlock;
|
|
@@ -64,14 +65,7 @@ export function ContentBlockView({
|
|
|
64
65
|
|
|
65
66
|
// For non-full-reasoning mode, show animated display only for the latest reasoning block
|
|
66
67
|
if (showReasoningTicker && isLastReasoningBlock && reasoningDisplay) {
|
|
67
|
-
return (
|
|
68
|
-
<text>
|
|
69
|
-
<span fg={COLORS.REASONING_DIM}>
|
|
70
|
-
{"// "}
|
|
71
|
-
{reasoningDisplay}
|
|
72
|
-
</span>
|
|
73
|
-
</text>
|
|
74
|
-
);
|
|
68
|
+
return renderReasoningTicker(reasoningDisplay);
|
|
75
69
|
}
|
|
76
70
|
const durationLabel =
|
|
77
71
|
block.durationMs !== undefined
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import type { ScrollBoxRenderable, TextareaRenderable } from "@opentui/core";
|
|
2
2
|
import { useKeyboard } from "@opentui/react";
|
|
3
3
|
import { useEffect, useMemo, useRef, useState } from "react";
|
|
4
|
-
import type { ModelOption } from "../types";
|
|
5
4
|
import { useMenuKeyboard } from "../hooks/use-menu-keyboard";
|
|
5
|
+
import type { ModelOption } from "../types";
|
|
6
6
|
import { COLORS } from "../ui/constants";
|
|
7
7
|
import { formatContextWindowK, formatPrice } from "../utils/formatters";
|
|
8
8
|
|
|
@@ -58,15 +58,26 @@ export function ModelMenu({
|
|
|
58
58
|
|
|
59
59
|
const curatedIdSet = useMemo(() => new Set(sortedCurated.map((model) => model.id)), [sortedCurated]);
|
|
60
60
|
|
|
61
|
+
const savedModel = useMemo(() => {
|
|
62
|
+
if (!currentModelId) return null;
|
|
63
|
+
if (curatedIdSet.has(currentModelId)) return null;
|
|
64
|
+
const match = allModels.find((model) => model.id === currentModelId);
|
|
65
|
+
return match ?? { id: currentModelId, name: currentModelId };
|
|
66
|
+
}, [allModels, curatedIdSet, currentModelId]);
|
|
67
|
+
|
|
68
|
+
const savedModels = useMemo(() => (savedModel ? [savedModel] : []), [savedModel]);
|
|
69
|
+
|
|
61
70
|
const allModelsWithFallback = useMemo(() => {
|
|
62
71
|
if (!currentModelId) return allModels;
|
|
63
72
|
if (curatedIdSet.has(currentModelId)) return allModels;
|
|
64
|
-
if (
|
|
73
|
+
if (savedModel) return allModels;
|
|
65
74
|
return [...allModels, { id: currentModelId, name: currentModelId }];
|
|
66
|
-
}, [allModels, curatedIdSet, currentModelId]);
|
|
75
|
+
}, [allModels, curatedIdSet, currentModelId, savedModel]);
|
|
67
76
|
|
|
68
77
|
const filteredAllModels = useMemo(() => {
|
|
69
|
-
const filtered = allModelsWithFallback.filter(
|
|
78
|
+
const filtered = allModelsWithFallback.filter(
|
|
79
|
+
(model) => !curatedIdSet.has(model.id) && model.id !== savedModel?.id
|
|
80
|
+
);
|
|
70
81
|
const query = searchQuery.trim().toLowerCase();
|
|
71
82
|
if (query.length < MIN_ALL_MODEL_QUERY_LENGTH) {
|
|
72
83
|
return [];
|
|
@@ -78,28 +89,44 @@ export function ModelMenu({
|
|
|
78
89
|
: filtered;
|
|
79
90
|
|
|
80
91
|
return matching.sort((a, b) => a.name.localeCompare(b.name));
|
|
81
|
-
}, [allModelsWithFallback, curatedIdSet, searchQuery]);
|
|
92
|
+
}, [allModelsWithFallback, curatedIdSet, savedModel?.id, searchQuery]);
|
|
82
93
|
|
|
83
|
-
const totalItems = sortedCurated.length + filteredAllModels.length;
|
|
94
|
+
const totalItems = sortedCurated.length + savedModels.length + filteredAllModels.length;
|
|
84
95
|
|
|
85
96
|
const initialIndex = useMemo(() => {
|
|
86
97
|
if (totalItems === 0) return 0;
|
|
87
98
|
const curatedIdx = sortedCurated.findIndex((model) => model.id === currentModelId);
|
|
88
99
|
if (curatedIdx >= 0) return curatedIdx;
|
|
100
|
+
const savedIdx = savedModels.findIndex((model) => model.id === currentModelId);
|
|
101
|
+
if (savedIdx >= 0) return sortedCurated.length + savedIdx;
|
|
89
102
|
const allIdx = filteredAllModels.findIndex((model) => model.id === currentModelId);
|
|
90
|
-
if (allIdx >= 0) return sortedCurated.length + allIdx;
|
|
103
|
+
if (allIdx >= 0) return sortedCurated.length + savedModels.length + allIdx;
|
|
91
104
|
return 0;
|
|
92
|
-
}, [sortedCurated, filteredAllModels, currentModelId, totalItems]);
|
|
105
|
+
}, [sortedCurated, savedModels, filteredAllModels, currentModelId, totalItems]);
|
|
93
106
|
|
|
94
107
|
const { selectedIndex } = useMenuKeyboard({
|
|
95
108
|
itemCount: totalItems,
|
|
96
109
|
initialIndex,
|
|
97
110
|
onClose,
|
|
98
111
|
onSelect: (selectedIdx) => {
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
112
|
+
if (selectedIdx < sortedCurated.length) {
|
|
113
|
+
const model = sortedCurated[selectedIdx];
|
|
114
|
+
if (model) {
|
|
115
|
+
onSelect(model);
|
|
116
|
+
}
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const afterCurated = selectedIdx - sortedCurated.length;
|
|
121
|
+
if (afterCurated < savedModels.length) {
|
|
122
|
+
const model = savedModels[afterCurated];
|
|
123
|
+
if (model) {
|
|
124
|
+
onSelect(model);
|
|
125
|
+
}
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const model = filteredAllModels[afterCurated - savedModels.length];
|
|
103
130
|
if (model) {
|
|
104
131
|
onSelect(model);
|
|
105
132
|
}
|
|
@@ -124,7 +151,7 @@ export function ModelMenu({
|
|
|
124
151
|
}
|
|
125
152
|
});
|
|
126
153
|
|
|
127
|
-
const allSelectedIndex = selectedIndex - sortedCurated.length;
|
|
154
|
+
const allSelectedIndex = selectedIndex - sortedCurated.length - savedModels.length;
|
|
128
155
|
const isAllSectionSelected = allSelectedIndex >= 0;
|
|
129
156
|
|
|
130
157
|
const scrollRef = useRef<ScrollBoxRenderable | null>(null);
|
|
@@ -345,6 +372,25 @@ export function ModelMenu({
|
|
|
345
372
|
</>
|
|
346
373
|
)}
|
|
347
374
|
|
|
375
|
+
{savedModels.length > 0 ? (
|
|
376
|
+
<>
|
|
377
|
+
<box marginBottom={1} marginTop={1}>
|
|
378
|
+
<text>
|
|
379
|
+
<span fg={COLORS.DAEMON_LABEL}>[ SAVED ]</span>
|
|
380
|
+
</text>
|
|
381
|
+
</box>
|
|
382
|
+
<box flexDirection="column">
|
|
383
|
+
{savedModels.map((model, idx) =>
|
|
384
|
+
renderModelRow(
|
|
385
|
+
model,
|
|
386
|
+
sortedCurated.length + idx === selectedIndex,
|
|
387
|
+
model.id === currentModelId
|
|
388
|
+
)
|
|
389
|
+
)}
|
|
390
|
+
</box>
|
|
391
|
+
</>
|
|
392
|
+
) : null}
|
|
393
|
+
|
|
348
394
|
<box marginBottom={1} marginTop={1}>
|
|
349
395
|
<text>
|
|
350
396
|
<span fg={COLORS.DAEMON_LABEL}>[ ALL MODELS ]</span>
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { useCallback, useEffect, useRef } from "react";
|
|
2
|
-
import {
|
|
2
|
+
import { setOpenRouterProviderTag, setResponseModel } from "../ai/model-config";
|
|
3
3
|
import type {
|
|
4
4
|
AppPreferences,
|
|
5
5
|
BashApprovalLevel,
|
|
@@ -94,11 +94,8 @@ export function useAppPreferencesBootstrap(
|
|
|
94
94
|
}
|
|
95
95
|
|
|
96
96
|
if (prefs?.modelId) {
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
setResponseModel(prefs.modelId);
|
|
100
|
-
setCurrentModelId(prefs.modelId);
|
|
101
|
-
}
|
|
97
|
+
setResponseModel(prefs.modelId);
|
|
98
|
+
setCurrentModelId(prefs.modelId);
|
|
102
99
|
}
|
|
103
100
|
|
|
104
101
|
if (prefs?.openRouterProviderTag) {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { useEffect, useRef, useState } from "react";
|
|
2
2
|
import { REASONING_ANIMATION } from "../ui/constants";
|
|
3
3
|
|
|
4
4
|
export interface ReasoningState {
|
|
@@ -57,11 +57,16 @@ export function useReasoningAnimation(): UseReasoningAnimationReturn {
|
|
|
57
57
|
const movedChars = queue.slice(0, charsToMove);
|
|
58
58
|
const remainingQueue = queue.slice(charsToMove);
|
|
59
59
|
|
|
60
|
-
|
|
60
|
+
const terminalWidth =
|
|
61
|
+
typeof process !== "undefined" && process.stdout?.columns ? process.stdout.columns : undefined;
|
|
62
|
+
const maxWidth = terminalWidth ? Math.max(20, terminalWidth - 12) : REASONING_ANIMATION.LINE_WIDTH;
|
|
63
|
+
const lineWidth = Math.min(REASONING_ANIMATION.LINE_WIDTH, maxWidth);
|
|
64
|
+
|
|
65
|
+
// Add to display, restart when reaching the line width
|
|
61
66
|
setReasoningDisplay((display: string) => {
|
|
62
67
|
const newDisplay = display + movedChars;
|
|
63
|
-
if (newDisplay.length
|
|
64
|
-
return
|
|
68
|
+
if (newDisplay.length >= lineWidth) {
|
|
69
|
+
return movedChars;
|
|
65
70
|
}
|
|
66
71
|
return newDisplay;
|
|
67
72
|
});
|
package/src/ui/constants.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* UI constants including colors, status text, and markdown syntax styles.
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
import {
|
|
5
|
+
import { RGBA, SyntaxStyle } from "@opentui/core";
|
|
6
6
|
import { DaemonState } from "../types";
|
|
7
7
|
|
|
8
8
|
// Status text displayed for each daemon state
|
|
@@ -27,9 +27,11 @@ export const STATE_COLOR_HEX: Record<DaemonState, string> = {
|
|
|
27
27
|
|
|
28
28
|
// Animation settings for reasoning text ticker
|
|
29
29
|
export const REASONING_ANIMATION = {
|
|
30
|
-
LINE_WIDTH:
|
|
30
|
+
LINE_WIDTH: 200,
|
|
31
31
|
CHARS_PER_TICK: 6,
|
|
32
|
-
TICK_INTERVAL_MS:
|
|
32
|
+
TICK_INTERVAL_MS: 12,
|
|
33
|
+
SEGMENT_LENGTH: 3,
|
|
34
|
+
PREFIX_COLOR: "#7a7a7a",
|
|
33
35
|
INTENSITY: 0.5,
|
|
34
36
|
} as const;
|
|
35
37
|
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { TextAttributes } from "@opentui/core";
|
|
2
|
+
import { COLORS, REASONING_ANIMATION } from "./constants";
|
|
3
|
+
|
|
4
|
+
export function renderReasoningTicker(reasoningDisplay: string) {
|
|
5
|
+
const segmentLength = REASONING_ANIMATION.SEGMENT_LENGTH;
|
|
6
|
+
const segments: Array<{ text: string; color: string }> = [];
|
|
7
|
+
const segmentCount = Math.max(1, Math.ceil(reasoningDisplay.length / segmentLength));
|
|
8
|
+
for (let index = 0; index < segmentCount; index += 1) {
|
|
9
|
+
const start = index * segmentLength;
|
|
10
|
+
const text = reasoningDisplay.slice(start, start + segmentLength);
|
|
11
|
+
const normalized = segmentCount > 1 ? index / (segmentCount - 1) : 1;
|
|
12
|
+
|
|
13
|
+
let color: string = COLORS.REASONING_DIM;
|
|
14
|
+
if (normalized >= 0.9) {
|
|
15
|
+
color = "#b2a2e0";
|
|
16
|
+
} else if (normalized >= 0.75) {
|
|
17
|
+
color = "#9c8ac8";
|
|
18
|
+
} else if (normalized >= 0.6) {
|
|
19
|
+
color = "#8774b0";
|
|
20
|
+
} else if (normalized >= 0.45) {
|
|
21
|
+
color = "#725e98";
|
|
22
|
+
} else if (normalized >= 0.3) {
|
|
23
|
+
color = "#5e4a80";
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
segments.push({ text, color });
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return (
|
|
30
|
+
<text>
|
|
31
|
+
<span fg={REASONING_ANIMATION.PREFIX_COLOR}>{"// "}</span>
|
|
32
|
+
{segments.map((segment, index) => (
|
|
33
|
+
<span fg={segment.color} key={`reasoning-seg-${index}`} attributes={TextAttributes.ITALIC}>
|
|
34
|
+
{segment.text}
|
|
35
|
+
</span>
|
|
36
|
+
))}
|
|
37
|
+
</text>
|
|
38
|
+
);
|
|
39
|
+
}
|