@makefinks/daemon 0.1.4 → 0.2.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 +5 -4
- package/src/app/App.tsx +47 -37
- package/src/app/components/AppOverlays.tsx +17 -1
- package/src/app/components/ConversationPane.tsx +5 -3
- package/src/components/HotkeysPane.tsx +1 -0
- package/src/components/TokenUsageDisplay.tsx +11 -11
- package/src/components/UrlMenu.tsx +182 -0
- package/src/hooks/daemon-event-handlers/interrupted-turn.ts +148 -0
- package/src/hooks/daemon-event-handlers.ts +11 -151
- package/src/hooks/use-app-context-builder.ts +2 -0
- package/src/hooks/use-app-menus.ts +6 -0
- package/src/hooks/use-daemon-keyboard.ts +37 -51
- package/src/hooks/use-grounding-menu-controller.ts +51 -0
- package/src/hooks/use-overlay-controller.ts +78 -0
- package/src/hooks/use-url-menu-items.ts +19 -0
- package/src/state/app-context.tsx +2 -0
- package/src/state/session-store.ts +4 -0
- package/src/types/index.ts +14 -0
- package/src/utils/derive-url-menu-items.ts +155 -0
- package/src/utils/formatters.ts +1 -7
|
@@ -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
|
+
}
|
package/src/utils/formatters.ts
CHANGED
|
@@ -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
|
-
|
|
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 {
|