@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 CHANGED
@@ -28,7 +28,7 @@
28
28
  },
29
29
  "module": "src/index.tsx",
30
30
  "type": "module",
31
- "version": "0.3.0",
31
+ "version": "0.4.0",
32
32
  "bin": {
33
33
  "daemon": "dist/cli.js"
34
34
  },
@@ -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 (allModels.some((model) => model.id === currentModelId)) return allModels;
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((model) => !curatedIdSet.has(model.id));
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
- const isCurated = selectedIdx < sortedCurated.length;
100
- const model = isCurated
101
- ? sortedCurated[selectedIdx]
102
- : filteredAllModels[selectedIdx - sortedCurated.length];
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 { AVAILABLE_MODELS, setOpenRouterProviderTag, setResponseModel } from "../ai/model-config";
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
- const modelIdx = AVAILABLE_MODELS.findIndex((m) => m.id === prefs.modelId);
98
- if (modelIdx >= 0) {
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 { useState, useEffect, useRef } from "react";
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
- // Add to display, keeping it at max width by trimming from the left
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 > REASONING_ANIMATION.LINE_WIDTH) {
64
- return newDisplay.slice(-REASONING_ANIMATION.LINE_WIDTH);
68
+ if (newDisplay.length >= lineWidth) {
69
+ return movedChars;
65
70
  }
66
71
  return newDisplay;
67
72
  });
@@ -2,7 +2,7 @@
2
2
  * UI constants including colors, status text, and markdown syntax styles.
3
3
  */
4
4
 
5
- import { SyntaxStyle, RGBA } from "@opentui/core";
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: 120,
30
+ LINE_WIDTH: 200,
31
31
  CHARS_PER_TICK: 6,
32
- TICK_INTERVAL_MS: 16,
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
+ }