@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
|
@@ -24,6 +24,15 @@ import { REASONING_COLORS, STATE_COLORS } from "../types/theme";
|
|
|
24
24
|
import { REASONING_ANIMATION } from "../ui/constants";
|
|
25
25
|
import { debug } from "../utils/debug-logger";
|
|
26
26
|
import { hasVisibleText } from "../utils/formatters";
|
|
27
|
+
import {
|
|
28
|
+
INTERRUPTED_TOOL_RESULT,
|
|
29
|
+
buildInterruptedContentBlocks,
|
|
30
|
+
buildInterruptedModelMessages,
|
|
31
|
+
normalizeInterruptedToolBlockResult,
|
|
32
|
+
normalizeInterruptedToolResultOutput,
|
|
33
|
+
} from "./daemon-event-handlers/interrupted-turn";
|
|
34
|
+
|
|
35
|
+
export { buildInterruptedModelMessages };
|
|
27
36
|
|
|
28
37
|
function getToolCategory(toolName: string): ToolCategory | "fast" | undefined {
|
|
29
38
|
if (toolName === "subagent") return "subagent";
|
|
@@ -39,30 +48,6 @@ function getToolCategory(toolName: string): ToolCategory | "fast" | undefined {
|
|
|
39
48
|
return undefined;
|
|
40
49
|
}
|
|
41
50
|
|
|
42
|
-
const INTERRUPTED_TOOL_RESULT = "Tool execution interrupted by user";
|
|
43
|
-
|
|
44
|
-
function normalizeInterruptedToolBlockResult(result: unknown): unknown {
|
|
45
|
-
if (result !== undefined) return result;
|
|
46
|
-
return { success: false, error: INTERRUPTED_TOOL_RESULT };
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
function normalizeInterruptedToolResultOutput(result: unknown): ToolResultOutput {
|
|
50
|
-
if (result === undefined) {
|
|
51
|
-
return { type: "error-text", value: INTERRUPTED_TOOL_RESULT };
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
if (typeof result === "string") {
|
|
55
|
-
return { type: "text", value: result };
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
try {
|
|
59
|
-
JSON.stringify(result);
|
|
60
|
-
return { type: "json", value: result as ToolResultOutput["value"] };
|
|
61
|
-
} catch {
|
|
62
|
-
return { type: "text", value: String(result) };
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
|
|
66
51
|
function clearAvatarToolEffects(avatar: DaemonAvatarRenderable | null): void {
|
|
67
52
|
if (!avatar) return;
|
|
68
53
|
avatar.triggerToolComplete();
|
|
@@ -71,133 +56,6 @@ function clearAvatarToolEffects(avatar: DaemonAvatarRenderable | null): void {
|
|
|
71
56
|
avatar.setTypingMode(false);
|
|
72
57
|
}
|
|
73
58
|
|
|
74
|
-
function buildInterruptedContentBlocks(contentBlocks: ContentBlock[]): ContentBlock[] {
|
|
75
|
-
return contentBlocks.map((block) => {
|
|
76
|
-
if (block.type !== "tool") return { ...block };
|
|
77
|
-
|
|
78
|
-
const call = { ...block.call };
|
|
79
|
-
if (call.status === "running") {
|
|
80
|
-
call.status = "failed";
|
|
81
|
-
call.error = INTERRUPTED_TOOL_RESULT;
|
|
82
|
-
}
|
|
83
|
-
if (call.subagentSteps) {
|
|
84
|
-
call.subagentSteps = call.subagentSteps.map((step) =>
|
|
85
|
-
step.status === "running" ? { ...step, status: "failed" } : step
|
|
86
|
-
);
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
return {
|
|
90
|
-
...block,
|
|
91
|
-
call,
|
|
92
|
-
result: normalizeInterruptedToolBlockResult(block.result),
|
|
93
|
-
};
|
|
94
|
-
});
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
export function buildInterruptedModelMessages(contentBlocks: ContentBlock[]): ModelMessage[] {
|
|
98
|
-
const messages: ModelMessage[] = [];
|
|
99
|
-
|
|
100
|
-
type AssistantPart =
|
|
101
|
-
| { type: "text"; text: string }
|
|
102
|
-
| { type: "reasoning"; text: string }
|
|
103
|
-
| { type: "tool-call"; toolCallId: string; toolName: string; input: unknown };
|
|
104
|
-
|
|
105
|
-
type ToolResultPart = {
|
|
106
|
-
type: "tool-result";
|
|
107
|
-
toolCallId: string;
|
|
108
|
-
toolName: string;
|
|
109
|
-
output: ToolResultOutput;
|
|
110
|
-
};
|
|
111
|
-
|
|
112
|
-
let assistantParts: AssistantPart[] = [];
|
|
113
|
-
let toolResults: ToolResultPart[] = [];
|
|
114
|
-
|
|
115
|
-
for (const block of contentBlocks) {
|
|
116
|
-
if (block.type === "reasoning" && block.content) {
|
|
117
|
-
// Tool results must be emitted before new assistant reasoning.
|
|
118
|
-
if (toolResults.length > 0) {
|
|
119
|
-
messages.push({
|
|
120
|
-
role: "tool",
|
|
121
|
-
content: [...toolResults],
|
|
122
|
-
} as unknown as ModelMessage);
|
|
123
|
-
toolResults = [];
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
assistantParts.push({ type: "reasoning", text: block.content });
|
|
127
|
-
continue;
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
if (block.type === "text" && block.content) {
|
|
131
|
-
// Tool results must be emitted before new assistant text.
|
|
132
|
-
if (toolResults.length > 0) {
|
|
133
|
-
messages.push({
|
|
134
|
-
role: "tool",
|
|
135
|
-
content: [...toolResults],
|
|
136
|
-
} as unknown as ModelMessage);
|
|
137
|
-
toolResults = [];
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
assistantParts.push({ type: "text", text: block.content });
|
|
141
|
-
continue;
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
if (block.type === "tool") {
|
|
145
|
-
// Tool results must be emitted before a new tool call.
|
|
146
|
-
if (toolResults.length > 0) {
|
|
147
|
-
messages.push({
|
|
148
|
-
role: "tool",
|
|
149
|
-
content: [...toolResults],
|
|
150
|
-
} as unknown as ModelMessage);
|
|
151
|
-
toolResults = [];
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
const toolCallId = block.call.toolCallId;
|
|
155
|
-
if (!toolCallId) {
|
|
156
|
-
continue;
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
assistantParts.push({
|
|
160
|
-
type: "tool-call",
|
|
161
|
-
toolCallId,
|
|
162
|
-
toolName: block.call.name,
|
|
163
|
-
input: block.call.input ?? {},
|
|
164
|
-
});
|
|
165
|
-
|
|
166
|
-
// Tool calls must be emitted before their tool results.
|
|
167
|
-
if (assistantParts.length > 0) {
|
|
168
|
-
messages.push({
|
|
169
|
-
role: "assistant",
|
|
170
|
-
content: [...assistantParts],
|
|
171
|
-
} as unknown as ModelMessage);
|
|
172
|
-
assistantParts = [];
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
toolResults.push({
|
|
176
|
-
type: "tool-result",
|
|
177
|
-
toolCallId,
|
|
178
|
-
toolName: block.call.name,
|
|
179
|
-
output: normalizeInterruptedToolResultOutput(block.result),
|
|
180
|
-
});
|
|
181
|
-
}
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
if (assistantParts.length > 0) {
|
|
185
|
-
messages.push({
|
|
186
|
-
role: "assistant",
|
|
187
|
-
content: [...assistantParts],
|
|
188
|
-
} as unknown as ModelMessage);
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
if (toolResults.length > 0) {
|
|
192
|
-
messages.push({
|
|
193
|
-
role: "tool",
|
|
194
|
-
content: [...toolResults],
|
|
195
|
-
} as unknown as ModelMessage);
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
return messages;
|
|
199
|
-
}
|
|
200
|
-
|
|
201
59
|
function finalizePendingUserMessage(
|
|
202
60
|
prev: ConversationMessage[],
|
|
203
61
|
userText: string,
|
|
@@ -725,6 +583,8 @@ function mergeTokenUsage(prev: TokenUsage, usage: TokenUsage, isSubagent: boolea
|
|
|
725
583
|
subagentTotalTokens: prev.subagentTotalTokens,
|
|
726
584
|
subagentPromptTokens: prev.subagentPromptTokens,
|
|
727
585
|
subagentCompletionTokens: prev.subagentCompletionTokens,
|
|
586
|
+
latestTurnPromptTokens: usage.promptTokens,
|
|
587
|
+
latestTurnCompletionTokens: usage.completionTokens,
|
|
728
588
|
};
|
|
729
589
|
}
|
|
730
590
|
|
|
@@ -39,6 +39,8 @@ export interface UseAppContextBuilderParams {
|
|
|
39
39
|
setShowHotkeysPane: React.Dispatch<React.SetStateAction<boolean>>;
|
|
40
40
|
showGroundingMenu: boolean;
|
|
41
41
|
setShowGroundingMenu: React.Dispatch<React.SetStateAction<boolean>>;
|
|
42
|
+
showUrlMenu: boolean;
|
|
43
|
+
setShowUrlMenu: React.Dispatch<React.SetStateAction<boolean>>;
|
|
42
44
|
};
|
|
43
45
|
|
|
44
46
|
device: {
|
|
@@ -21,6 +21,9 @@ export interface UseAppMenusReturn {
|
|
|
21
21
|
|
|
22
22
|
showGroundingMenu: boolean;
|
|
23
23
|
setShowGroundingMenu: React.Dispatch<React.SetStateAction<boolean>>;
|
|
24
|
+
|
|
25
|
+
showUrlMenu: boolean;
|
|
26
|
+
setShowUrlMenu: React.Dispatch<React.SetStateAction<boolean>>;
|
|
24
27
|
}
|
|
25
28
|
|
|
26
29
|
export function useAppMenus(): UseAppMenusReturn {
|
|
@@ -31,6 +34,7 @@ export function useAppMenus(): UseAppMenusReturn {
|
|
|
31
34
|
const [showSessionMenu, setShowSessionMenu] = useState(false);
|
|
32
35
|
const [showHotkeysPane, setShowHotkeysPane] = useState(false);
|
|
33
36
|
const [showGroundingMenu, setShowGroundingMenu] = useState(false);
|
|
37
|
+
const [showUrlMenu, setShowUrlMenu] = useState(false);
|
|
34
38
|
|
|
35
39
|
return {
|
|
36
40
|
showDeviceMenu,
|
|
@@ -47,5 +51,7 @@ export function useAppMenus(): UseAppMenusReturn {
|
|
|
47
51
|
setShowHotkeysPane,
|
|
48
52
|
showGroundingMenu,
|
|
49
53
|
setShowGroundingMenu,
|
|
54
|
+
showUrlMenu,
|
|
55
|
+
setShowUrlMenu,
|
|
50
56
|
};
|
|
51
57
|
}
|
|
@@ -1,15 +1,11 @@
|
|
|
1
|
-
|
|
2
|
-
* Hook for handling keyboard input across all application states.
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
import { useCallback } from "react";
|
|
1
|
+
import { toast } from "@opentui-ui/toast/react";
|
|
6
2
|
import type { KeyEvent } from "@opentui/core";
|
|
3
|
+
import type { ScrollBoxRenderable } from "@opentui/core";
|
|
7
4
|
import { useKeyboard } from "@opentui/react";
|
|
8
|
-
import {
|
|
9
|
-
import { COLORS } from "../ui/constants";
|
|
5
|
+
import { useCallback } from "react";
|
|
10
6
|
import { getDaemonManager } from "../state/daemon-state";
|
|
11
|
-
import {
|
|
12
|
-
import
|
|
7
|
+
import { type AppPreferences, DaemonState } from "../types";
|
|
8
|
+
import { COLORS } from "../ui/constants";
|
|
13
9
|
export interface KeyboardHandlerState {
|
|
14
10
|
isOverlayOpen: boolean;
|
|
15
11
|
escPendingCancel: boolean;
|
|
@@ -27,6 +23,7 @@ export interface KeyboardHandlerActions {
|
|
|
27
23
|
setShowSessionMenu: (show: boolean) => void;
|
|
28
24
|
setShowHotkeysPane: (show: boolean) => void;
|
|
29
25
|
setShowGroundingMenu: (show: boolean) => void;
|
|
26
|
+
setShowUrlMenu: (show: boolean) => void;
|
|
30
27
|
setTypingInput: (input: string | ((prev: string) => string)) => void;
|
|
31
28
|
setCurrentTranscription: (text: string) => void;
|
|
32
29
|
setCurrentResponse: (text: string) => void;
|
|
@@ -46,6 +43,17 @@ export function useDaemonKeyboard(state: KeyboardHandlerState, actions: Keyboard
|
|
|
46
43
|
const manager = getDaemonManager();
|
|
47
44
|
const { isOverlayOpen, escPendingCancel, hasInteracted, hasGrounding } = state;
|
|
48
45
|
|
|
46
|
+
const closeAllMenus = useCallback(() => {
|
|
47
|
+
actions.setShowDeviceMenu(false);
|
|
48
|
+
actions.setShowSettingsMenu(false);
|
|
49
|
+
actions.setShowModelMenu(false);
|
|
50
|
+
actions.setShowProviderMenu(false);
|
|
51
|
+
actions.setShowSessionMenu(false);
|
|
52
|
+
actions.setShowHotkeysPane(false);
|
|
53
|
+
actions.setShowGroundingMenu(false);
|
|
54
|
+
actions.setShowUrlMenu(false);
|
|
55
|
+
}, [actions]);
|
|
56
|
+
|
|
49
57
|
const handleKeyPress = useCallback(
|
|
50
58
|
(key: KeyEvent) => {
|
|
51
59
|
const currentState = manager.state;
|
|
@@ -97,12 +105,7 @@ export function useDaemonKeyboard(state: KeyboardHandlerState, actions: Keyboard
|
|
|
97
105
|
currentState === DaemonState.IDLE &&
|
|
98
106
|
!hasInteracted
|
|
99
107
|
) {
|
|
100
|
-
|
|
101
|
-
actions.setShowProviderMenu(false);
|
|
102
|
-
actions.setShowSessionMenu(false);
|
|
103
|
-
actions.setShowSettingsMenu(false);
|
|
104
|
-
actions.setShowGroundingMenu(false);
|
|
105
|
-
actions.setShowHotkeysPane(false);
|
|
108
|
+
closeAllMenus();
|
|
106
109
|
actions.setShowDeviceMenu(true);
|
|
107
110
|
key.preventDefault();
|
|
108
111
|
return;
|
|
@@ -114,12 +117,7 @@ export function useDaemonKeyboard(state: KeyboardHandlerState, actions: Keyboard
|
|
|
114
117
|
key.eventType === "press" &&
|
|
115
118
|
(currentState === DaemonState.IDLE || currentState === DaemonState.SPEAKING)
|
|
116
119
|
) {
|
|
117
|
-
|
|
118
|
-
actions.setShowSettingsMenu(false);
|
|
119
|
-
actions.setShowModelMenu(false);
|
|
120
|
-
actions.setShowProviderMenu(false);
|
|
121
|
-
actions.setShowGroundingMenu(false);
|
|
122
|
-
actions.setShowHotkeysPane(false);
|
|
120
|
+
closeAllMenus();
|
|
123
121
|
actions.setShowSessionMenu(true);
|
|
124
122
|
key.preventDefault();
|
|
125
123
|
return;
|
|
@@ -133,12 +131,7 @@ export function useDaemonKeyboard(state: KeyboardHandlerState, actions: Keyboard
|
|
|
133
131
|
currentState === DaemonState.SPEAKING ||
|
|
134
132
|
currentState === DaemonState.RESPONDING)
|
|
135
133
|
) {
|
|
136
|
-
|
|
137
|
-
actions.setShowModelMenu(false);
|
|
138
|
-
actions.setShowProviderMenu(false);
|
|
139
|
-
actions.setShowSessionMenu(false);
|
|
140
|
-
actions.setShowGroundingMenu(false);
|
|
141
|
-
actions.setShowHotkeysPane(false);
|
|
134
|
+
closeAllMenus();
|
|
142
135
|
actions.setShowSettingsMenu(true);
|
|
143
136
|
key.preventDefault();
|
|
144
137
|
return;
|
|
@@ -152,12 +145,7 @@ export function useDaemonKeyboard(state: KeyboardHandlerState, actions: Keyboard
|
|
|
152
145
|
currentState === DaemonState.SPEAKING ||
|
|
153
146
|
currentState === DaemonState.RESPONDING)
|
|
154
147
|
) {
|
|
155
|
-
|
|
156
|
-
actions.setShowSettingsMenu(false);
|
|
157
|
-
actions.setShowProviderMenu(false);
|
|
158
|
-
actions.setShowSessionMenu(false);
|
|
159
|
-
actions.setShowGroundingMenu(false);
|
|
160
|
-
actions.setShowHotkeysPane(false);
|
|
148
|
+
closeAllMenus();
|
|
161
149
|
actions.setShowModelMenu(true);
|
|
162
150
|
key.preventDefault();
|
|
163
151
|
return;
|
|
@@ -169,12 +157,7 @@ export function useDaemonKeyboard(state: KeyboardHandlerState, actions: Keyboard
|
|
|
169
157
|
key.eventType === "press" &&
|
|
170
158
|
(currentState === DaemonState.IDLE || currentState === DaemonState.SPEAKING)
|
|
171
159
|
) {
|
|
172
|
-
|
|
173
|
-
actions.setShowSettingsMenu(false);
|
|
174
|
-
actions.setShowModelMenu(false);
|
|
175
|
-
actions.setShowSessionMenu(false);
|
|
176
|
-
actions.setShowGroundingMenu(false);
|
|
177
|
-
actions.setShowHotkeysPane(false);
|
|
160
|
+
closeAllMenus();
|
|
178
161
|
actions.setShowProviderMenu(true);
|
|
179
162
|
key.preventDefault();
|
|
180
163
|
return;
|
|
@@ -187,17 +170,25 @@ export function useDaemonKeyboard(state: KeyboardHandlerState, actions: Keyboard
|
|
|
187
170
|
(currentState === DaemonState.IDLE || currentState === DaemonState.SPEAKING) &&
|
|
188
171
|
hasGrounding
|
|
189
172
|
) {
|
|
190
|
-
|
|
191
|
-
actions.setShowSettingsMenu(false);
|
|
192
|
-
actions.setShowModelMenu(false);
|
|
193
|
-
actions.setShowProviderMenu(false);
|
|
194
|
-
actions.setShowSessionMenu(false);
|
|
195
|
-
actions.setShowHotkeysPane(false);
|
|
173
|
+
closeAllMenus();
|
|
196
174
|
actions.setShowGroundingMenu(true);
|
|
197
175
|
key.preventDefault();
|
|
198
176
|
return;
|
|
199
177
|
}
|
|
200
178
|
|
|
179
|
+
// 'U' key to open URL menu (in IDLE or SPEAKING state when hasInteracted)
|
|
180
|
+
if (
|
|
181
|
+
(key.sequence === "u" || key.sequence === "U") &&
|
|
182
|
+
key.eventType === "press" &&
|
|
183
|
+
(currentState === DaemonState.IDLE || currentState === DaemonState.SPEAKING) &&
|
|
184
|
+
hasInteracted
|
|
185
|
+
) {
|
|
186
|
+
closeAllMenus();
|
|
187
|
+
actions.setShowUrlMenu(true);
|
|
188
|
+
key.preventDefault();
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
|
|
201
192
|
// 'N' key to start a new session (in IDLE or SPEAKING state)
|
|
202
193
|
if (
|
|
203
194
|
(key.sequence === "n" || key.sequence === "N") &&
|
|
@@ -267,12 +258,7 @@ export function useDaemonKeyboard(state: KeyboardHandlerState, actions: Keyboard
|
|
|
267
258
|
|
|
268
259
|
// '?' key to show hotkeys pane
|
|
269
260
|
if (key.sequence === "?" && key.eventType === "press" && currentState !== DaemonState.TYPING) {
|
|
270
|
-
|
|
271
|
-
actions.setShowSettingsMenu(false);
|
|
272
|
-
actions.setShowModelMenu(false);
|
|
273
|
-
actions.setShowProviderMenu(false);
|
|
274
|
-
actions.setShowSessionMenu(false);
|
|
275
|
-
actions.setShowGroundingMenu(false);
|
|
261
|
+
closeAllMenus();
|
|
276
262
|
actions.setShowHotkeysPane(true);
|
|
277
263
|
key.preventDefault();
|
|
278
264
|
return;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { useCallback, useEffect, useMemo, useState } from "react";
|
|
2
|
+
import type { GroundingMap } from "../types";
|
|
3
|
+
import { openUrlInBrowser } from "../utils/preferences";
|
|
4
|
+
import { buildTextFragmentUrl } from "../utils/text-fragment";
|
|
5
|
+
|
|
6
|
+
export function useGroundingMenuController(params: {
|
|
7
|
+
sessionId: string | null;
|
|
8
|
+
latestGroundingMap: GroundingMap | null;
|
|
9
|
+
}) {
|
|
10
|
+
const { sessionId, latestGroundingMap } = params;
|
|
11
|
+
const [selectedIndex, setSelectedIndex] = useState(0);
|
|
12
|
+
|
|
13
|
+
useEffect(() => {
|
|
14
|
+
setSelectedIndex(0);
|
|
15
|
+
}, [sessionId]);
|
|
16
|
+
|
|
17
|
+
const initialIndex = useMemo(() => {
|
|
18
|
+
if (!latestGroundingMap) return 0;
|
|
19
|
+
return Math.min(selectedIndex, Math.max(0, latestGroundingMap.items.length - 1));
|
|
20
|
+
}, [latestGroundingMap, selectedIndex]);
|
|
21
|
+
|
|
22
|
+
const openGroundingSource = useCallback(
|
|
23
|
+
(idx: number) => {
|
|
24
|
+
if (!latestGroundingMap) return;
|
|
25
|
+
const item = latestGroundingMap.items[idx];
|
|
26
|
+
if (!item) return;
|
|
27
|
+
const { source } = item;
|
|
28
|
+
const url = source.textFragment
|
|
29
|
+
? buildTextFragmentUrl(source.url, { fragmentText: source.textFragment })
|
|
30
|
+
: source.url;
|
|
31
|
+
openUrlInBrowser(url);
|
|
32
|
+
},
|
|
33
|
+
[latestGroundingMap]
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
const handleSelect = useCallback(
|
|
37
|
+
(index: number) => {
|
|
38
|
+
setSelectedIndex(index);
|
|
39
|
+
openGroundingSource(index);
|
|
40
|
+
},
|
|
41
|
+
[openGroundingSource]
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
return {
|
|
45
|
+
groundingInitialIndex: initialIndex,
|
|
46
|
+
groundingSelectedIndex: selectedIndex,
|
|
47
|
+
setGroundingSelectedIndex: setSelectedIndex,
|
|
48
|
+
onGroundingSelect: handleSelect,
|
|
49
|
+
onGroundingIndexChange: setSelectedIndex,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { useCallback, useMemo } from "react";
|
|
2
|
+
|
|
3
|
+
export interface OverlayControllerState {
|
|
4
|
+
showDeviceMenu: boolean;
|
|
5
|
+
showSettingsMenu: boolean;
|
|
6
|
+
showModelMenu: boolean;
|
|
7
|
+
showProviderMenu: boolean;
|
|
8
|
+
showSessionMenu: boolean;
|
|
9
|
+
showHotkeysPane: boolean;
|
|
10
|
+
showGroundingMenu: boolean;
|
|
11
|
+
showUrlMenu: boolean;
|
|
12
|
+
onboardingActive: boolean;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface OverlayControllerActions {
|
|
16
|
+
setShowDeviceMenu: (show: boolean) => void;
|
|
17
|
+
setShowSettingsMenu: (show: boolean) => void;
|
|
18
|
+
setShowModelMenu: (show: boolean) => void;
|
|
19
|
+
setShowProviderMenu: (show: boolean) => void;
|
|
20
|
+
setShowSessionMenu: (show: boolean) => void;
|
|
21
|
+
setShowHotkeysPane: (show: boolean) => void;
|
|
22
|
+
setShowGroundingMenu: (show: boolean) => void;
|
|
23
|
+
setShowUrlMenu: (show: boolean) => void;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function useOverlayController(state: OverlayControllerState, actions: OverlayControllerActions) {
|
|
27
|
+
const {
|
|
28
|
+
showDeviceMenu,
|
|
29
|
+
showSettingsMenu,
|
|
30
|
+
showModelMenu,
|
|
31
|
+
showProviderMenu,
|
|
32
|
+
showSessionMenu,
|
|
33
|
+
showHotkeysPane,
|
|
34
|
+
showGroundingMenu,
|
|
35
|
+
showUrlMenu,
|
|
36
|
+
onboardingActive,
|
|
37
|
+
} = state;
|
|
38
|
+
|
|
39
|
+
const isOverlayOpen = useMemo(() => {
|
|
40
|
+
return (
|
|
41
|
+
showDeviceMenu ||
|
|
42
|
+
showSettingsMenu ||
|
|
43
|
+
showModelMenu ||
|
|
44
|
+
showProviderMenu ||
|
|
45
|
+
showSessionMenu ||
|
|
46
|
+
showHotkeysPane ||
|
|
47
|
+
showGroundingMenu ||
|
|
48
|
+
showUrlMenu ||
|
|
49
|
+
onboardingActive
|
|
50
|
+
);
|
|
51
|
+
}, [
|
|
52
|
+
showDeviceMenu,
|
|
53
|
+
showSettingsMenu,
|
|
54
|
+
showModelMenu,
|
|
55
|
+
showProviderMenu,
|
|
56
|
+
showSessionMenu,
|
|
57
|
+
showHotkeysPane,
|
|
58
|
+
showGroundingMenu,
|
|
59
|
+
showUrlMenu,
|
|
60
|
+
onboardingActive,
|
|
61
|
+
]);
|
|
62
|
+
|
|
63
|
+
const closeAllOverlays = useCallback(() => {
|
|
64
|
+
actions.setShowDeviceMenu(false);
|
|
65
|
+
actions.setShowSettingsMenu(false);
|
|
66
|
+
actions.setShowModelMenu(false);
|
|
67
|
+
actions.setShowProviderMenu(false);
|
|
68
|
+
actions.setShowSessionMenu(false);
|
|
69
|
+
actions.setShowHotkeysPane(false);
|
|
70
|
+
actions.setShowGroundingMenu(false);
|
|
71
|
+
actions.setShowUrlMenu(false);
|
|
72
|
+
}, [actions]);
|
|
73
|
+
|
|
74
|
+
return {
|
|
75
|
+
isOverlayOpen,
|
|
76
|
+
closeAllOverlays,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { useMemo } from "react";
|
|
2
|
+
import type { ContentBlock, ConversationMessage, GroundingMap, UrlMenuItem } from "../types";
|
|
3
|
+
import { deriveUrlMenuItems } from "../utils/derive-url-menu-items";
|
|
4
|
+
|
|
5
|
+
export function useUrlMenuItems(params: {
|
|
6
|
+
conversationHistory: ConversationMessage[];
|
|
7
|
+
currentContentBlocks: ContentBlock[];
|
|
8
|
+
latestGroundingMap: GroundingMap | null;
|
|
9
|
+
}): UrlMenuItem[] {
|
|
10
|
+
const { conversationHistory, currentContentBlocks, latestGroundingMap } = params;
|
|
11
|
+
|
|
12
|
+
return useMemo(() => {
|
|
13
|
+
return deriveUrlMenuItems({
|
|
14
|
+
conversationHistory,
|
|
15
|
+
currentContentBlocks,
|
|
16
|
+
latestGroundingMap,
|
|
17
|
+
});
|
|
18
|
+
}, [conversationHistory, currentContentBlocks, latestGroundingMap]);
|
|
19
|
+
}
|
|
@@ -29,6 +29,8 @@ export interface MenuState {
|
|
|
29
29
|
setShowHotkeysPane: React.Dispatch<React.SetStateAction<boolean>>;
|
|
30
30
|
showGroundingMenu: boolean;
|
|
31
31
|
setShowGroundingMenu: React.Dispatch<React.SetStateAction<boolean>>;
|
|
32
|
+
showUrlMenu: boolean;
|
|
33
|
+
setShowUrlMenu: React.Dispatch<React.SetStateAction<boolean>>;
|
|
32
34
|
}
|
|
33
35
|
|
|
34
36
|
export interface DeviceState {
|
|
@@ -131,6 +131,10 @@ function parseSessionUsage(raw: string): TokenUsage {
|
|
|
131
131
|
subagentPromptTokens: typeof parsed.subagentPromptTokens === "number" ? parsed.subagentPromptTokens : 0,
|
|
132
132
|
subagentCompletionTokens:
|
|
133
133
|
typeof parsed.subagentCompletionTokens === "number" ? parsed.subagentCompletionTokens : 0,
|
|
134
|
+
latestTurnPromptTokens:
|
|
135
|
+
typeof parsed.latestTurnPromptTokens === "number" ? parsed.latestTurnPromptTokens : undefined,
|
|
136
|
+
latestTurnCompletionTokens:
|
|
137
|
+
typeof parsed.latestTurnCompletionTokens === "number" ? parsed.latestTurnCompletionTokens : undefined,
|
|
134
138
|
};
|
|
135
139
|
} catch {
|
|
136
140
|
return { ...DEFAULT_SESSION_USAGE };
|
package/src/types/index.ts
CHANGED
|
@@ -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
|
/**
|
|
@@ -403,3 +407,13 @@ export interface GroundingMap {
|
|
|
403
407
|
createdAt: string;
|
|
404
408
|
items: GroundedStatement[];
|
|
405
409
|
}
|
|
410
|
+
|
|
411
|
+
export interface UrlMenuItem {
|
|
412
|
+
url: string;
|
|
413
|
+
groundedCount: number;
|
|
414
|
+
readPercent?: number;
|
|
415
|
+
highlightsCount?: number;
|
|
416
|
+
status: "ok" | "error";
|
|
417
|
+
error?: string;
|
|
418
|
+
lastSeenIndex: number;
|
|
419
|
+
}
|