@makefinks/daemon 0.7.2 → 0.9.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.
@@ -35,6 +35,7 @@ interface SettingsMenuProps {
35
35
  canEnableVoiceOutput: boolean;
36
36
  showFullReasoning: boolean;
37
37
  showToolOutput: boolean;
38
+ memoryEnabled: boolean;
38
39
  onClose: () => void;
39
40
  toggleInteractionMode: () => void;
40
41
  setVoiceInteractionType: (type: VoiceInteractionType) => void;
@@ -43,6 +44,7 @@ interface SettingsMenuProps {
43
44
  setBashApprovalLevel: (level: BashApprovalLevel) => void;
44
45
  setShowFullReasoning: (show: boolean) => void;
45
46
  setShowToolOutput: (show: boolean) => void;
47
+ setMemoryEnabled: (enabled: boolean) => void;
46
48
  persistPreferences: (updates: Partial<AppPreferences>) => void;
47
49
  }
48
50
 
@@ -56,6 +58,7 @@ export function SettingsMenu({
56
58
  canEnableVoiceOutput,
57
59
  showFullReasoning,
58
60
  showToolOutput,
61
+ memoryEnabled,
59
62
  onClose,
60
63
  toggleInteractionMode,
61
64
  setVoiceInteractionType,
@@ -64,6 +67,7 @@ export function SettingsMenu({
64
67
  setBashApprovalLevel,
65
68
  setShowFullReasoning,
66
69
  setShowToolOutput,
70
+ setMemoryEnabled,
67
71
  persistPreferences,
68
72
  }: SettingsMenuProps) {
69
73
  const [selectedIdx, setSelectedIdx] = useState(0);
@@ -115,24 +119,33 @@ export function SettingsMenu({
115
119
  description: "Require approval for bash commands (NONE / DANGEROUS / ALL)",
116
120
  isCyclic: true,
117
121
  },
122
+ {
123
+ id: "memory-enabled",
124
+ label: "Memory",
125
+ value: memoryEnabled ? "ON" : "OFF",
126
+ description: "Auto-save messages + inject relevant memories",
127
+ isToggle: true,
128
+ },
118
129
  ];
119
130
 
120
- if (interactionMode === "voice") {
121
- items.push(
122
- {
123
- id: "header-audio",
124
- label: "AUDIO PARAMETERS",
125
- isHeader: true,
126
- },
127
- {
128
- id: "speech-speed",
129
- label: "Speech Speed",
130
- value: `${speechSpeed.toFixed(2)}x`,
131
- description: "Adjust speech rate (1.0x - 2.0x)",
132
- isCyclic: true,
133
- }
134
- );
135
- }
131
+ const audioSettingsDisabled = interactionMode !== "voice";
132
+ items.push(
133
+ {
134
+ id: "header-audio",
135
+ label: "AUDIO PARAMETERS",
136
+ isHeader: true,
137
+ },
138
+ {
139
+ id: "speech-speed",
140
+ label: "Speech Speed",
141
+ value: audioSettingsDisabled ? "N/A" : `${speechSpeed.toFixed(2)}x`,
142
+ description: audioSettingsDisabled
143
+ ? "Enable voice mode to adjust speech rate"
144
+ : "Adjust speech rate (1.0x - 2.0x)",
145
+ isCyclic: !audioSettingsDisabled,
146
+ disabled: audioSettingsDisabled,
147
+ }
148
+ );
136
149
 
137
150
  items.push(
138
151
  {
@@ -159,6 +172,7 @@ export function SettingsMenu({
159
172
  // Filter out headers for selection logic
160
173
  const selectableItems = items.filter((item) => !item.isHeader);
161
174
  const selectableCount = selectableItems.length;
175
+ const labelWidth = Math.max(0, ...selectableItems.map((item) => item.label.length)) + 4;
162
176
 
163
177
  useEffect(() => {
164
178
  if (selectableCount === 0) {
@@ -181,6 +195,7 @@ export function SettingsMenu({
181
195
  canEnableVoiceOutput,
182
196
  showFullReasoning,
183
197
  showToolOutput,
198
+ memoryEnabled,
184
199
  setSelectedIdx,
185
200
  toggleInteractionMode,
186
201
  setVoiceInteractionType,
@@ -189,6 +204,7 @@ export function SettingsMenu({
189
204
  setBashApprovalLevel,
190
205
  setShowFullReasoning,
191
206
  setShowToolOutput,
207
+ setMemoryEnabled,
192
208
  persistPreferences,
193
209
  onClose,
194
210
  manager,
@@ -263,19 +279,25 @@ export function SettingsMenu({
263
279
  paddingRight={1}
264
280
  flexDirection="column"
265
281
  >
266
- <box>
267
- <text>
268
- <span fg={labelColor}>
269
- {isSelected ? "▶ " : " "}
270
- {item.label}:{" "}
271
- </span>
272
- <span fg={valueColor}>{item.value}</span>
273
- {item.isToggle && !item.disabled && <span fg={COLORS.USER_LABEL}></span>}
274
- {item.isCyclic && !item.disabled && <span fg={COLORS.USER_LABEL}></span>}
275
- </text>
282
+ <box flexDirection="row">
283
+ <box width={labelWidth}>
284
+ <text>
285
+ <span fg={labelColor}>
286
+ {isSelected ? " " : " "}
287
+ {item.label}:{" "}
288
+ </span>
289
+ </text>
290
+ </box>
291
+ <box>
292
+ <text>
293
+ <span fg={valueColor}>{item.value}</span>
294
+ {item.isToggle && !item.disabled && <span fg={COLORS.USER_LABEL}></span>}
295
+ {item.isCyclic && !item.disabled && <span fg={COLORS.USER_LABEL}></span>}
296
+ </text>
297
+ </box>
276
298
  </box>
277
299
  {item.description && (
278
- <box marginLeft={4}>
300
+ <box marginLeft={labelWidth}>
279
301
  <text>
280
302
  <span fg={COLORS.REASONING_DIM}>{item.description}</span>
281
303
  </text>
@@ -1,7 +1,10 @@
1
1
  import { useMemo } from "react";
2
+ import { getMcpManager } from "../ai/mcp/mcp-manager";
2
3
  import { useToolApprovalForCall } from "../hooks/use-tool-approval";
3
4
  import type { ToolCall } from "../types";
4
5
  import { COLORS } from "../ui/constants";
6
+ import { formatToolInputLines } from "../utils/formatters";
7
+ import { formatGenericToolOutputPreview } from "../utils/tool-output-preview";
5
8
  import { ApprovalPicker } from "./ApprovalPicker";
6
9
  import {
7
10
  ErrorPreviewView,
@@ -37,8 +40,19 @@ function ApprovalResultBadge({ result }: { result: "approved" | "denied" }) {
37
40
  );
38
41
  }
39
42
 
43
+ function ToolSectionDivider({ label }: { label: string }) {
44
+ return (
45
+ <box flexDirection="column" paddingLeft={2} marginTop={1}>
46
+ <text>
47
+ <span fg={COLORS.REASONING_DIM}>{`--- ${label} ---`}</span>
48
+ </text>
49
+ </box>
50
+ );
51
+ }
52
+
40
53
  export function ToolCallView({ call, result, showOutput = true }: ToolCallViewProps) {
41
54
  const layout = getToolLayout(call.name) ?? defaultToolLayout;
55
+ const mcpMeta = useMemo(() => getMcpManager().getToolMeta(call.name), [call.name]);
42
56
  const isAwaitingApproval = call.status === "awaiting_approval";
43
57
  const isRunning = call.status === "running" || call.status === "streaming";
44
58
  const isFailed = call.status === "failed";
@@ -47,17 +61,42 @@ export function ToolCallView({ call, result, showOutput = true }: ToolCallViewPr
47
61
  call.toolCallId
48
62
  );
49
63
 
50
- const header = useMemo(() => layout.getHeader?.(call.input, result) ?? null, [call.input, result, layout]);
51
-
52
- const body = useMemo(
53
- () => layout.getBody?.(call.input, result, call) ?? null,
54
- [call.input, result, call, layout]
55
- );
64
+ const header = useMemo(() => {
65
+ const base = layout.getHeader?.(call.input, result) ?? null;
66
+ if (base) return base;
67
+ if (mcpMeta) {
68
+ return {
69
+ primary: mcpMeta.serverId,
70
+ secondary: mcpMeta.originalToolName,
71
+ secondaryStyle: "dim" as const,
72
+ };
73
+ }
74
+ return null;
75
+ }, [call.input, result, layout, mcpMeta]);
76
+
77
+ const body = useMemo(() => {
78
+ const base = layout.getBody?.(call.input, result, call) ?? null;
79
+ if (base) return base;
80
+ if (!mcpMeta) return null;
81
+ const lines = formatToolInputLines(call.input);
82
+ const normalized = lines.length > 0 ? lines : ["(no input)"];
83
+ return {
84
+ lines: normalized.map((text) => ({
85
+ text,
86
+ color: COLORS.REASONING_DIM,
87
+ })),
88
+ };
89
+ }, [call.input, result, call, layout, mcpMeta]);
56
90
 
57
91
  const resultPreviewLines = useMemo(() => {
58
92
  if (!showOutput) return null;
59
- return layout.formatResult?.(result) ?? null;
60
- }, [result, showOutput, layout]);
93
+ const formatted = layout.formatResult?.(result) ?? null;
94
+ if (formatted) return formatted;
95
+ if (mcpMeta) return formatGenericToolOutputPreview(result);
96
+ return null;
97
+ }, [result, showOutput, layout, mcpMeta]);
98
+
99
+ const hasResultPreview = Boolean(showOutput && resultPreviewLines && resultPreviewLines.length > 0);
61
100
 
62
101
  const toolColor =
63
102
  call.status === "completed"
@@ -65,7 +104,7 @@ export function ToolCallView({ call, result, showOutput = true }: ToolCallViewPr
65
104
  : isAwaitingApproval
66
105
  ? COLORS.STATUS_APPROVAL
67
106
  : COLORS.TOOLS;
68
- const toolName = layout.abbreviation ?? getDefaultAbbreviation(call.name);
107
+ const toolName = mcpMeta ? "mcp" : (layout.abbreviation ?? getDefaultAbbreviation(call.name));
69
108
  const borderColor = getStatusBorderColor(call.status);
70
109
 
71
110
  const customBody = layout.renderBody ? layout.renderBody({ call, result, showOutput }) : null;
@@ -98,10 +137,10 @@ export function ToolCallView({ call, result, showOutput = true }: ToolCallViewPr
98
137
  />
99
138
  )}
100
139
 
101
- {showOutput && resultPreviewLines && resultPreviewLines.length > 0 && (
102
- <ResultPreviewView lines={resultPreviewLines} />
103
- )}
140
+ {hasResultPreview && <ToolSectionDivider label="OUTPUT" />}
141
+ {hasResultPreview && <ResultPreviewView lines={resultPreviewLines ?? []} />}
104
142
 
143
+ {isFailed && call.error && <ToolSectionDivider label="ERROR" />}
105
144
  {isFailed && call.error && <ErrorPreviewView error={call.error} />}
106
145
 
107
146
  {call.approvalResult && <ApprovalResultBadge result={call.approvalResult} />}
@@ -1,5 +1,6 @@
1
1
  import { useEffect, useMemo, useState } from "react";
2
2
 
3
+ import { type McpServerStatus, getMcpManager } from "../ai/mcp/mcp-manager";
3
4
  import { invalidateDaemonToolsCache } from "../ai/tools/index";
4
5
  import { invalidateSubagentToolsCache } from "../ai/tools/subagents";
5
6
  import {
@@ -13,6 +14,7 @@ import { getDaemonManager } from "../state/daemon-state";
13
14
  import type { ToolToggleId, ToolToggles } from "../types";
14
15
  import { DEFAULT_TOOL_TOGGLES } from "../types";
15
16
  import { COLORS } from "../ui/constants";
17
+ import { getManualConfigPath } from "../utils/config";
16
18
 
17
19
  interface ToolsMenuProps {
18
20
  persistPreferences: (updates: Partial<{ toolToggles: ToolToggles }>) => void;
@@ -52,9 +54,21 @@ function getToolLabel(id: ToolToggleId): string {
52
54
  export function ToolsMenu({ persistPreferences, onClose }: ToolsMenuProps) {
53
55
  const manager = getDaemonManager();
54
56
  const [toggles, setToggles] = useState<ToolToggles>(manager.toolToggles ?? { ...DEFAULT_TOOL_TOGGLES });
57
+ const [mcpServers, setMcpServers] = useState<McpServerStatus[]>(() => getMcpManager().getServersSnapshot());
55
58
 
56
59
  const [toolAvailability, setToolAvailability] = useState<Record<ToolToggleId, MenuToolItem> | null>(null);
57
60
 
61
+ useEffect(() => {
62
+ const mcp = getMcpManager();
63
+ const handleUpdate = () => {
64
+ setMcpServers(mcp.getServersSnapshot());
65
+ };
66
+ mcp.on("update", handleUpdate);
67
+ return () => {
68
+ mcp.off("update", handleUpdate);
69
+ };
70
+ }, []);
71
+
58
72
  useEffect(() => {
59
73
  let cancelled = false;
60
74
  const loadAvailability = async () => {
@@ -134,12 +148,20 @@ export function ToolsMenu({ persistPreferences, onClose }: ToolsMenuProps) {
134
148
  }, [items, showReasonColumn]);
135
149
 
136
150
  const statusWidth = 8;
151
+ const mcpStatusWidth = 8;
137
152
 
138
153
  function truncateText(text: string, maxLen: number): string {
139
154
  if (text.length <= maxLen) return text;
140
155
  return text.slice(0, Math.max(0, maxLen - 1)) + "…";
141
156
  }
142
157
 
158
+ const mcpConfigPath = useMemo(() => getManualConfigPath(), []);
159
+
160
+ const mcpIdWidth = useMemo(() => {
161
+ const raw = mcpServers.reduce((max, server) => Math.max(max, server.id.length), 0);
162
+ return Math.min(Math.max(raw, 10), 28);
163
+ }, [mcpServers]);
164
+
143
165
  return (
144
166
  <box
145
167
  position="absolute"
@@ -176,15 +198,13 @@ export function ToolsMenu({ persistPreferences, onClose }: ToolsMenuProps) {
176
198
  </text>
177
199
  </box>
178
200
 
179
- {showReasonColumn && (
180
- <box marginBottom={1}>
181
- <text>
182
- <span fg={COLORS.REASONING_DIM}>
183
- {"TOOL".padEnd(labelWidth)} {"STATUS".padEnd(statusWidth)} REASON
184
- </span>
185
- </text>
186
- </box>
187
- )}
201
+ <box marginBottom={1}>
202
+ <text>
203
+ <span fg={COLORS.REASONING_DIM}>
204
+ {` ${"TOOL".padEnd(labelWidth)} ${"STATUS".padEnd(statusWidth)}${showReasonColumn ? " REASON" : ""}`}
205
+ </span>
206
+ </text>
207
+ </box>
188
208
 
189
209
  <box flexDirection="column">
190
210
  {items.map((item, idx) => {
@@ -218,7 +238,7 @@ export function ToolsMenu({ persistPreferences, onClose }: ToolsMenuProps) {
218
238
  <span fg={labelColor}>{labelText}</span>
219
239
  <span fg={COLORS.REASONING_DIM}> </span>
220
240
  <span fg={statusColor}>{statusText}</span>
221
- {showReasonColumn && reasonText ? (
241
+ {showReasonColumn ? (
222
242
  <>
223
243
  <span fg={COLORS.REASONING_DIM}> </span>
224
244
  <span fg={reasonColor}>{reasonText}</span>
@@ -229,6 +249,57 @@ export function ToolsMenu({ persistPreferences, onClose }: ToolsMenuProps) {
229
249
  );
230
250
  })}
231
251
  </box>
252
+
253
+ <box flexDirection="column" marginTop={1}>
254
+ <box marginBottom={1}>
255
+ <text>
256
+ <span fg={COLORS.DAEMON_LABEL}>[ MCP ]</span>
257
+ <span fg={COLORS.REASONING_DIM}>{` ${truncateText(mcpConfigPath, 80)}`}</span>
258
+ </text>
259
+ </box>
260
+
261
+ {mcpServers.length === 0 ? (
262
+ <text>
263
+ <span fg={COLORS.REASONING_DIM}>No MCP servers configured.</span>
264
+ </text>
265
+ ) : (
266
+ <box flexDirection="column">
267
+ <box marginBottom={1}>
268
+ <text>
269
+ <span fg={COLORS.REASONING_DIM}>
270
+ {`SERVER`.padEnd(mcpIdWidth)} {`STATUS`.padEnd(mcpStatusWidth)} TOOLS
271
+ </span>
272
+ </text>
273
+ </box>
274
+ {mcpServers.map((server) => {
275
+ const statusLabel = server.status.toUpperCase();
276
+ const statusColor =
277
+ server.status === "ready"
278
+ ? COLORS.STATUS_COMPLETED
279
+ : server.status === "loading"
280
+ ? COLORS.STATUS_RUNNING
281
+ : server.status === "error"
282
+ ? COLORS.STATUS_FAILED
283
+ : COLORS.REASONING_DIM;
284
+ const idText = truncateText(server.id, mcpIdWidth).padEnd(mcpIdWidth);
285
+ const toolsText = String(server.toolCount).padStart(4);
286
+ const errorText = server.error ? truncateText(server.error, 60) : "";
287
+
288
+ return (
289
+ <box key={server.id} flexDirection="column">
290
+ <text>
291
+ <span fg={COLORS.MENU_TEXT}>{idText}</span>
292
+ <span fg={COLORS.REASONING_DIM}> </span>
293
+ <span fg={statusColor}>{statusLabel.padEnd(mcpStatusWidth)}</span>
294
+ <span fg={COLORS.REASONING_DIM}> {toolsText}</span>
295
+ {errorText ? <span fg={COLORS.REASONING_DIM}>{` ${errorText}`}</span> : null}
296
+ </text>
297
+ </box>
298
+ );
299
+ })}
300
+ </box>
301
+ )}
302
+ </box>
232
303
  </box>
233
304
  </box>
234
305
  );
@@ -139,7 +139,7 @@ export function UrlMenu({ items, onClose }: UrlMenuProps) {
139
139
  </span>
140
140
  </text>
141
141
  <text>
142
- <span fg={COLORS.REASONING_DIM}>(G=grounded, READ=% or HL=highlights)</span>
142
+ <span fg={COLORS.REASONING_DIM}>(G=grounded, READ=%)</span>
143
143
  </text>
144
144
  </box>
145
145
 
@@ -152,12 +152,7 @@ export function UrlMenu({ items, onClose }: UrlMenuProps) {
152
152
  sortedItems.map((item, idx) => {
153
153
  const { origin, path } = splitUrl(item.url);
154
154
  const grounded = item.groundedCount > 0;
155
- const readLabel =
156
- item.readPercent !== undefined
157
- ? `${item.readPercent}%`
158
- : item.highlightsCount !== undefined
159
- ? `HL:${item.highlightsCount}`
160
- : "—";
155
+ const readLabel = item.readPercent !== undefined ? `${item.readPercent}%` : "—";
161
156
 
162
157
  return (
163
158
  <box key={idx} flexDirection="row" marginBottom={0}>
@@ -34,6 +34,12 @@ function extractUrl(input: unknown): string | null {
34
34
  if ("url" in input && typeof input.url === "string") {
35
35
  return input.url;
36
36
  }
37
+ if ("requests" in input && Array.isArray(input.requests)) {
38
+ const first = input.requests.find((item: unknown) => isRecord(item) && typeof item.url === "string");
39
+ if (isRecord(first) && typeof first.url === "string") {
40
+ return first.url;
41
+ }
42
+ }
37
43
  return null;
38
44
  }
39
45
 
@@ -88,6 +94,16 @@ function formatStepLabel(step: { toolName: string; input?: unknown }): string {
88
94
 
89
95
  if (step.toolName === "fetchUrls" || step.toolName === "renderUrl") {
90
96
  const url = extractUrl(step.input);
97
+ if (step.toolName === "fetchUrls" && isRecord(step.input) && Array.isArray(step.input.requests)) {
98
+ const count = step.input.requests.filter(
99
+ (item: unknown) => isRecord(item) && typeof item.url === "string"
100
+ ).length;
101
+ if (url) {
102
+ const suffix = count > 1 ? ` (+${count - 1})` : "";
103
+ return `${toolLabel}: ${truncateLabel(url, MAX_URL_LENGTH)}${suffix}`;
104
+ }
105
+ return toolLabel;
106
+ }
91
107
  if (url) {
92
108
  return `${toolLabel}: ${truncateLabel(url, MAX_URL_LENGTH)}`;
93
109
  }