@makefinks/daemon 0.1.4 → 0.3.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.
Files changed (34) hide show
  1. package/package.json +5 -4
  2. package/src/ai/daemon-ai.ts +30 -85
  3. package/src/ai/system-prompt.ts +134 -111
  4. package/src/ai/tool-approval-coordinator.ts +113 -0
  5. package/src/ai/tools/index.ts +12 -32
  6. package/src/ai/tools/subagents.ts +16 -30
  7. package/src/ai/tools/tool-registry.ts +203 -0
  8. package/src/app/App.tsx +23 -631
  9. package/src/app/components/AppOverlays.tsx +25 -1
  10. package/src/app/components/ConversationPane.tsx +5 -3
  11. package/src/components/HotkeysPane.tsx +3 -1
  12. package/src/components/TokenUsageDisplay.tsx +11 -11
  13. package/src/components/ToolsMenu.tsx +235 -0
  14. package/src/components/UrlMenu.tsx +182 -0
  15. package/src/hooks/daemon-event-handlers/interrupted-turn.ts +148 -0
  16. package/src/hooks/daemon-event-handlers.ts +11 -151
  17. package/src/hooks/use-app-context-builder.ts +4 -0
  18. package/src/hooks/use-app-controller.ts +546 -0
  19. package/src/hooks/use-app-menus.ts +12 -0
  20. package/src/hooks/use-app-preferences-bootstrap.ts +9 -0
  21. package/src/hooks/use-bootstrap-controller.ts +92 -0
  22. package/src/hooks/use-daemon-keyboard.ts +63 -57
  23. package/src/hooks/use-daemon-runtime-controller.ts +147 -0
  24. package/src/hooks/use-grounding-menu-controller.ts +51 -0
  25. package/src/hooks/use-overlay-controller.ts +84 -0
  26. package/src/hooks/use-session-controller.ts +79 -0
  27. package/src/hooks/use-url-menu-items.ts +19 -0
  28. package/src/state/app-context.tsx +4 -0
  29. package/src/state/daemon-state.ts +19 -8
  30. package/src/state/session-store.ts +4 -0
  31. package/src/types/index.ts +39 -0
  32. package/src/utils/derive-url-menu-items.ts +155 -0
  33. package/src/utils/formatters.ts +1 -7
  34. package/src/utils/preferences.ts +10 -0
@@ -42,6 +42,10 @@ export interface TokenUsage {
42
42
  subagentTotalTokens?: number;
43
43
  subagentPromptTokens?: number;
44
44
  subagentCompletionTokens?: number;
45
+ /** Latest turn's prompt tokens (for context window % calculation) */
46
+ latestTurnPromptTokens?: number;
47
+ /** Latest turn's completion tokens (for context window % calculation) */
48
+ latestTurnCompletionTokens?: number;
45
49
  }
46
50
 
47
51
  /**
@@ -255,6 +259,29 @@ export type OnboardingStep =
255
259
 
256
260
  export type VoiceInteractionType = "direct" | "review";
257
261
 
262
+ export type ToolToggleId =
263
+ | "readFile"
264
+ | "runBash"
265
+ | "webSearch"
266
+ | "fetchUrls"
267
+ | "renderUrl"
268
+ | "todoManager"
269
+ | "groundingManager"
270
+ | "subagent";
271
+
272
+ export type ToolToggles = Record<ToolToggleId, boolean>;
273
+
274
+ export const DEFAULT_TOOL_TOGGLES: ToolToggles = {
275
+ readFile: true,
276
+ runBash: true,
277
+ webSearch: true,
278
+ fetchUrls: true,
279
+ renderUrl: true,
280
+ todoManager: true,
281
+ groundingManager: true,
282
+ subagent: true,
283
+ };
284
+
258
285
  /**
259
286
  * Persisted user preferences.
260
287
  */
@@ -287,6 +314,8 @@ export interface AppPreferences {
287
314
  showToolOutput?: boolean;
288
315
  /** Bash command approval level */
289
316
  bashApprovalLevel?: BashApprovalLevel;
317
+ /** Tool toggles (on/off) */
318
+ toolToggles?: ToolToggles;
290
319
  /** Recent user inputs for up/down history navigation (max 20) */
291
320
  inputHistory?: string[];
292
321
  }
@@ -403,3 +432,13 @@ export interface GroundingMap {
403
432
  createdAt: string;
404
433
  items: GroundedStatement[];
405
434
  }
435
+
436
+ export interface UrlMenuItem {
437
+ url: string;
438
+ groundedCount: number;
439
+ readPercent?: number;
440
+ highlightsCount?: number;
441
+ status: "ok" | "error";
442
+ error?: string;
443
+ lastSeenIndex: number;
444
+ }
@@ -0,0 +1,155 @@
1
+ import type { ContentBlock, ConversationMessage, GroundingMap, UrlMenuItem } from "../types";
2
+
3
+ function normalizeUrlKey(rawUrl: string): string {
4
+ try {
5
+ const parsed = new URL(rawUrl);
6
+ parsed.hash = "";
7
+ return parsed.toString();
8
+ } catch {
9
+ return rawUrl;
10
+ }
11
+ }
12
+
13
+ function computeCoveragePercent(intervals: Array<[number, number]>, totalLines: number): number | undefined {
14
+ if (!Number.isFinite(totalLines) || totalLines <= 0) return undefined;
15
+ if (intervals.length === 0) return undefined;
16
+
17
+ const sorted = [...intervals].sort((a, b) => a[0] - b[0]);
18
+ let covered = 0;
19
+ let curStart = sorted[0]?.[0] ?? 0;
20
+ let curEnd = sorted[0]?.[1] ?? 0;
21
+
22
+ for (const [start, end] of sorted.slice(1)) {
23
+ if (start <= curEnd) {
24
+ curEnd = Math.max(curEnd, end);
25
+ continue;
26
+ }
27
+ covered += Math.max(0, curEnd - curStart);
28
+ curStart = start;
29
+ curEnd = end;
30
+ }
31
+ covered += Math.max(0, curEnd - curStart);
32
+
33
+ const percent = Math.round((covered / totalLines) * 100);
34
+ return Math.max(0, Math.min(100, percent));
35
+ }
36
+
37
+ export function deriveUrlMenuItems(params: {
38
+ conversationHistory: ConversationMessage[];
39
+ currentContentBlocks: ContentBlock[];
40
+ latestGroundingMap: GroundingMap | null;
41
+ }): UrlMenuItem[] {
42
+ const { conversationHistory, currentContentBlocks, latestGroundingMap } = params;
43
+
44
+ const intervalsByUrl = new Map<string, Array<[number, number]>>();
45
+ const totalLinesByUrl = new Map<string, number>();
46
+ const highlightsCountByUrl = new Map<string, number>();
47
+ const lastSeenIndexByUrl = new Map<string, number>();
48
+ const statusByUrl = new Map<string, "ok" | "error">();
49
+ const errorByUrl = new Map<string, string>();
50
+
51
+ const allBlocks = [
52
+ ...conversationHistory.flatMap((msg) => msg.contentBlocks ?? []),
53
+ ...currentContentBlocks,
54
+ ];
55
+
56
+ for (const [blockIndex, block] of allBlocks.entries()) {
57
+ if (block.type !== "tool") continue;
58
+ if (block.call.name !== "fetchUrls" && block.call.name !== "renderUrl") continue;
59
+
60
+ const input = block.call.input as { url?: string } | undefined;
61
+ const url = input?.url;
62
+ if (!url) continue;
63
+
64
+ lastSeenIndexByUrl.set(url, blockIndex);
65
+
66
+ const result = block.result as
67
+ | {
68
+ lineOffset?: number;
69
+ lineLimit?: number;
70
+ totalLines?: number;
71
+ highlights?: unknown[];
72
+ success?: boolean;
73
+ error?: string;
74
+ }
75
+ | undefined;
76
+
77
+ if (!result || typeof result !== "object") continue;
78
+
79
+ if (result.success === false && typeof result.error === "string" && result.error.trim().length > 0) {
80
+ statusByUrl.set(url, "error");
81
+ errorByUrl.set(url, result.error.trim());
82
+ } else if (result.success === true) {
83
+ statusByUrl.set(url, "ok");
84
+ }
85
+
86
+ if (Array.isArray(result.highlights)) {
87
+ highlightsCountByUrl.set(url, result.highlights.length);
88
+ }
89
+
90
+ if (
91
+ typeof result.totalLines === "number" &&
92
+ Number.isFinite(result.totalLines) &&
93
+ result.totalLines > 0
94
+ ) {
95
+ const prev = totalLinesByUrl.get(url) ?? 0;
96
+ totalLinesByUrl.set(url, Math.max(prev, result.totalLines));
97
+ }
98
+
99
+ if (
100
+ typeof result.lineOffset === "number" &&
101
+ typeof result.lineLimit === "number" &&
102
+ Number.isFinite(result.lineOffset) &&
103
+ Number.isFinite(result.lineLimit) &&
104
+ result.lineLimit > 0 &&
105
+ result.lineOffset >= 0
106
+ ) {
107
+ const start = result.lineOffset;
108
+ const end = result.lineOffset + result.lineLimit;
109
+ const list = intervalsByUrl.get(url) ?? [];
110
+ list.push([start, end]);
111
+ intervalsByUrl.set(url, list);
112
+ }
113
+ }
114
+
115
+ const groundedCountByUrl = new Map<string, number>();
116
+ for (const groundedItem of latestGroundingMap?.items ?? []) {
117
+ const groundedUrl = groundedItem.source?.url;
118
+ if (!groundedUrl) continue;
119
+ const next = (groundedCountByUrl.get(groundedUrl) ?? 0) + 1;
120
+ groundedCountByUrl.set(groundedUrl, next);
121
+ }
122
+
123
+ function lookupGroundedCount(url: string): number {
124
+ const direct = groundedCountByUrl.get(url);
125
+ if (direct !== undefined) return direct;
126
+
127
+ const key = normalizeUrlKey(url);
128
+ for (const [gUrl, count] of groundedCountByUrl.entries()) {
129
+ if (normalizeUrlKey(gUrl) === key) return count;
130
+ }
131
+ return 0;
132
+ }
133
+
134
+ const urls = [...lastSeenIndexByUrl.keys()];
135
+ return urls.map((url) => {
136
+ const groundedCount = lookupGroundedCount(url);
137
+ const highlightsCount = highlightsCountByUrl.get(url);
138
+ const totalLines = totalLinesByUrl.get(url);
139
+ const intervals = intervalsByUrl.get(url) ?? [];
140
+ const readPercent = totalLines !== undefined ? computeCoveragePercent(intervals, totalLines) : undefined;
141
+ const error = errorByUrl.get(url);
142
+ const status = statusByUrl.get(url) ?? (error ? "error" : "ok");
143
+ const lastSeenIndex = lastSeenIndexByUrl.get(url) ?? 0;
144
+
145
+ return {
146
+ url,
147
+ groundedCount,
148
+ readPercent,
149
+ highlightsCount,
150
+ status,
151
+ error,
152
+ lastSeenIndex,
153
+ };
154
+ });
155
+ }
@@ -209,14 +209,8 @@ export function isTodoInput(input: unknown): input is TodoInput {
209
209
  );
210
210
  }
211
211
 
212
- /**
213
- * Format token count with K suffix for thousands
214
- */
215
212
  export function formatTokenCount(count: number): string {
216
- // if (count >= 1000) {
217
- // return `${(count / 1000).toFixed(1)}k`;
218
- // }
219
- return String(count);
213
+ return count.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ".");
220
214
  }
221
215
 
222
216
  export function formatContextWindowK(contextLength: number): string {
@@ -109,6 +109,16 @@ export function parsePreferences(raw: unknown): AppPreferences | null {
109
109
  if (typeof raw.showToolOutput === "boolean") {
110
110
  prefs.showToolOutput = raw.showToolOutput;
111
111
  }
112
+ if (isRecord(raw.toolToggles)) {
113
+ const record = raw.toolToggles;
114
+ const next: Record<string, boolean> = {};
115
+ for (const [k, v] of Object.entries(record)) {
116
+ if (typeof v === "boolean") {
117
+ next[k] = v;
118
+ }
119
+ }
120
+ prefs.toolToggles = next as AppPreferences["toolToggles"];
121
+ }
112
122
  if (
113
123
  raw.bashApprovalLevel === "none" ||
114
124
  raw.bashApprovalLevel === "dangerous" ||