@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
package/package.json
CHANGED
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
},
|
|
29
29
|
"module": "src/index.tsx",
|
|
30
30
|
"type": "module",
|
|
31
|
-
"version": "0.
|
|
31
|
+
"version": "0.2.0",
|
|
32
32
|
"bin": {
|
|
33
33
|
"daemon": "dist/cli.js"
|
|
34
34
|
},
|
|
@@ -54,12 +54,13 @@
|
|
|
54
54
|
"preview:avatar": "bun run src/avatar-preview.ts",
|
|
55
55
|
"preview:avatar:mp4": "bun run src/avatar-preview.ts --mp4 tmp/avatar-preview.mp4",
|
|
56
56
|
"setup:browsers": "bun run src/scripts/setup-browsers.ts",
|
|
57
|
+
"setup:hooks": "bash .githooks/setup.sh",
|
|
57
58
|
"test": "bun test",
|
|
58
59
|
"test:watch": "bun test --watch",
|
|
59
60
|
"prepublishOnly": "bun run build:cli",
|
|
60
|
-
"release:patch": "npm version patch && git push && git push --tags && npm publish",
|
|
61
|
-
"release:minor": "npm version minor && git push && git push --tags && npm publish",
|
|
62
|
-
"release:major": "npm version major && git push && git push --tags && npm publish"
|
|
61
|
+
"release:patch": "npm version patch && git push && git push --tags && gh release create v$(node -p \"require('./package.json').version\") --generate-notes && npm publish",
|
|
62
|
+
"release:minor": "npm version minor && git push && git push --tags && gh release create v$(node -p \"require('./package.json').version\") --generate-notes && npm publish",
|
|
63
|
+
"release:major": "npm version major && git push && git push --tags && gh release create v$(node -p \"require('./package.json').version\") --generate-notes && npm publish"
|
|
63
64
|
},
|
|
64
65
|
"devDependencies": {
|
|
65
66
|
"@biomejs/biome": "^1.9.4",
|
package/src/app/App.tsx
CHANGED
|
@@ -19,7 +19,9 @@ import { useCopyOnSelect } from "../hooks/use-copy-on-select";
|
|
|
19
19
|
import { useDaemonEvents } from "../hooks/use-daemon-events";
|
|
20
20
|
import { useDaemonKeyboard } from "../hooks/use-daemon-keyboard";
|
|
21
21
|
import { useGrounding } from "../hooks/use-grounding";
|
|
22
|
+
import { useGroundingMenuController } from "../hooks/use-grounding-menu-controller";
|
|
22
23
|
import { useInputHistory } from "../hooks/use-input-history";
|
|
24
|
+
import { useOverlayController } from "../hooks/use-overlay-controller";
|
|
23
25
|
import { usePlaywrightNotification } from "../hooks/use-playwright-notification";
|
|
24
26
|
import { useReasoningAnimation } from "../hooks/use-reasoning-animation";
|
|
25
27
|
import { useResponseTimer } from "../hooks/use-response-timer";
|
|
@@ -30,11 +32,9 @@ import { AppProvider } from "../state/app-context";
|
|
|
30
32
|
import { daemonEvents } from "../state/daemon-events";
|
|
31
33
|
import { getDaemonManager } from "../state/daemon-state";
|
|
32
34
|
import { deleteSession } from "../state/session-store";
|
|
33
|
-
import { DaemonState } from "../types";
|
|
34
35
|
import type { AppPreferences, AudioDevice, OnboardingStep } from "../types";
|
|
36
|
+
import { DaemonState } from "../types";
|
|
35
37
|
import { COLORS } from "../ui/constants";
|
|
36
|
-
import { openUrlInBrowser } from "../utils/preferences";
|
|
37
|
-
import { buildTextFragmentUrl } from "../utils/text-fragment";
|
|
38
38
|
import { getSoxInstallHint, isSoxAvailable, setAudioDevice } from "../voice/audio-recorder";
|
|
39
39
|
import { AppOverlays } from "./components/AppOverlays";
|
|
40
40
|
import { AvatarLayer } from "./components/AvatarLayer";
|
|
@@ -115,6 +115,8 @@ export function App() {
|
|
|
115
115
|
setShowHotkeysPane,
|
|
116
116
|
showGroundingMenu,
|
|
117
117
|
setShowGroundingMenu,
|
|
118
|
+
showUrlMenu,
|
|
119
|
+
setShowUrlMenu,
|
|
118
120
|
} = menus;
|
|
119
121
|
|
|
120
122
|
const {
|
|
@@ -128,7 +130,13 @@ export function App() {
|
|
|
128
130
|
} = useAppSessions({ showSessionMenu });
|
|
129
131
|
|
|
130
132
|
const { latestGroundingMap, hasGrounding } = useGrounding(currentSessionId);
|
|
131
|
-
const
|
|
133
|
+
const {
|
|
134
|
+
groundingInitialIndex,
|
|
135
|
+
groundingSelectedIndex,
|
|
136
|
+
setGroundingSelectedIndex,
|
|
137
|
+
onGroundingSelect,
|
|
138
|
+
onGroundingIndexChange,
|
|
139
|
+
} = useGroundingMenuController({ sessionId: currentSessionId, latestGroundingMap });
|
|
132
140
|
|
|
133
141
|
const appSettings = useAppSettings();
|
|
134
142
|
const {
|
|
@@ -379,6 +387,7 @@ export function App() {
|
|
|
379
387
|
setShowSessionMenu,
|
|
380
388
|
setShowHotkeysPane,
|
|
381
389
|
setShowGroundingMenu,
|
|
390
|
+
setShowUrlMenu,
|
|
382
391
|
setTypingInput,
|
|
383
392
|
setCurrentTranscription,
|
|
384
393
|
setCurrentResponse,
|
|
@@ -401,6 +410,7 @@ export function App() {
|
|
|
401
410
|
setShowSessionMenu,
|
|
402
411
|
setShowHotkeysPane,
|
|
403
412
|
setShowGroundingMenu,
|
|
413
|
+
setShowUrlMenu,
|
|
404
414
|
setTypingInput,
|
|
405
415
|
setCurrentTranscription,
|
|
406
416
|
setCurrentResponse,
|
|
@@ -420,17 +430,33 @@ export function App() {
|
|
|
420
430
|
const hasInteracted =
|
|
421
431
|
conversationHistory.length > 0 || currentTranscription.length > 0 || currentContentBlocks.length > 0;
|
|
422
432
|
|
|
433
|
+
const { isOverlayOpen } = useOverlayController(
|
|
434
|
+
{
|
|
435
|
+
showDeviceMenu,
|
|
436
|
+
showSettingsMenu,
|
|
437
|
+
showModelMenu,
|
|
438
|
+
showProviderMenu,
|
|
439
|
+
showSessionMenu,
|
|
440
|
+
showHotkeysPane,
|
|
441
|
+
showGroundingMenu,
|
|
442
|
+
showUrlMenu,
|
|
443
|
+
onboardingActive,
|
|
444
|
+
},
|
|
445
|
+
{
|
|
446
|
+
setShowDeviceMenu,
|
|
447
|
+
setShowSettingsMenu,
|
|
448
|
+
setShowModelMenu,
|
|
449
|
+
setShowProviderMenu,
|
|
450
|
+
setShowSessionMenu,
|
|
451
|
+
setShowHotkeysPane,
|
|
452
|
+
setShowGroundingMenu,
|
|
453
|
+
setShowUrlMenu,
|
|
454
|
+
}
|
|
455
|
+
);
|
|
456
|
+
|
|
423
457
|
useDaemonKeyboard(
|
|
424
458
|
{
|
|
425
|
-
isOverlayOpen
|
|
426
|
-
showDeviceMenu ||
|
|
427
|
-
showSettingsMenu ||
|
|
428
|
-
showModelMenu ||
|
|
429
|
-
showProviderMenu ||
|
|
430
|
-
showSessionMenu ||
|
|
431
|
-
showHotkeysPane ||
|
|
432
|
-
showGroundingMenu ||
|
|
433
|
-
onboardingActive,
|
|
459
|
+
isOverlayOpen,
|
|
434
460
|
escPendingCancel,
|
|
435
461
|
hasInteracted,
|
|
436
462
|
hasGrounding,
|
|
@@ -481,24 +507,6 @@ export function App() {
|
|
|
481
507
|
}
|
|
482
508
|
}, [daemonState]);
|
|
483
509
|
|
|
484
|
-
const openGroundingSource = useCallback(
|
|
485
|
-
(idx: number) => {
|
|
486
|
-
if (!latestGroundingMap) return;
|
|
487
|
-
const item = latestGroundingMap.items[idx];
|
|
488
|
-
if (!item) return;
|
|
489
|
-
const { source } = item;
|
|
490
|
-
const url = source.textFragment
|
|
491
|
-
? buildTextFragmentUrl(source.url, { fragmentText: source.textFragment })
|
|
492
|
-
: source.url;
|
|
493
|
-
openUrlInBrowser(url);
|
|
494
|
-
},
|
|
495
|
-
[latestGroundingMap]
|
|
496
|
-
);
|
|
497
|
-
|
|
498
|
-
const groundingInitialIndex = latestGroundingMap
|
|
499
|
-
? Math.min(groundingSelectedIndex, Math.max(0, latestGroundingMap.items.length - 1))
|
|
500
|
-
: 0;
|
|
501
|
-
|
|
502
510
|
const conversationDisplayState: ConversationDisplayState = {
|
|
503
511
|
conversationHistory,
|
|
504
512
|
currentTranscription,
|
|
@@ -556,6 +564,8 @@ export function App() {
|
|
|
556
564
|
setShowHotkeysPane,
|
|
557
565
|
showGroundingMenu,
|
|
558
566
|
setShowGroundingMenu,
|
|
567
|
+
showUrlMenu,
|
|
568
|
+
setShowUrlMenu,
|
|
559
569
|
},
|
|
560
570
|
device: {
|
|
561
571
|
devices,
|
|
@@ -630,11 +640,8 @@ export function App() {
|
|
|
630
640
|
onSessionDelete: handleSessionDelete,
|
|
631
641
|
},
|
|
632
642
|
groundingCallbacks: {
|
|
633
|
-
onGroundingSelect
|
|
634
|
-
|
|
635
|
-
openGroundingSource(index);
|
|
636
|
-
},
|
|
637
|
-
onGroundingIndexChange: setGroundingSelectedIndex,
|
|
643
|
+
onGroundingSelect,
|
|
644
|
+
onGroundingIndexChange,
|
|
638
645
|
},
|
|
639
646
|
onboardingCallbacks: {
|
|
640
647
|
onKeySubmit: handleApiKeySubmit,
|
|
@@ -702,7 +709,10 @@ export function App() {
|
|
|
702
709
|
</box>
|
|
703
710
|
|
|
704
711
|
<AppProvider value={appContextValue}>
|
|
705
|
-
<AppOverlays
|
|
712
|
+
<AppOverlays
|
|
713
|
+
conversationHistory={conversationHistory}
|
|
714
|
+
currentContentBlocks={currentContentBlocks}
|
|
715
|
+
/>
|
|
706
716
|
</AppProvider>
|
|
707
717
|
</>
|
|
708
718
|
</box>
|
|
@@ -7,11 +7,25 @@ import { OnboardingOverlay } from "../../components/OnboardingOverlay";
|
|
|
7
7
|
import { ProviderMenu } from "../../components/ProviderMenu";
|
|
8
8
|
import { SessionMenu } from "../../components/SessionMenu";
|
|
9
9
|
import { SettingsMenu } from "../../components/SettingsMenu";
|
|
10
|
+
import { UrlMenu } from "../../components/UrlMenu";
|
|
11
|
+
import { useUrlMenuItems } from "../../hooks/use-url-menu-items";
|
|
10
12
|
import { useAppContext } from "../../state/app-context";
|
|
13
|
+
import type { ContentBlock, ConversationMessage } from "../../types";
|
|
11
14
|
|
|
12
|
-
|
|
15
|
+
interface AppOverlaysProps {
|
|
16
|
+
conversationHistory: ConversationMessage[];
|
|
17
|
+
currentContentBlocks: ContentBlock[];
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function AppOverlaysImpl({ conversationHistory, currentContentBlocks }: AppOverlaysProps) {
|
|
13
21
|
const ctx = useAppContext();
|
|
14
22
|
const { menus, device, settings, model, session, grounding, onboarding } = ctx;
|
|
23
|
+
|
|
24
|
+
const urlMenuItems = useUrlMenuItems({
|
|
25
|
+
conversationHistory,
|
|
26
|
+
currentContentBlocks,
|
|
27
|
+
latestGroundingMap: grounding.latestGroundingMap,
|
|
28
|
+
});
|
|
15
29
|
const {
|
|
16
30
|
deviceCallbacks,
|
|
17
31
|
settingsCallbacks,
|
|
@@ -104,6 +118,8 @@ function AppOverlaysImpl() {
|
|
|
104
118
|
/>
|
|
105
119
|
)}
|
|
106
120
|
|
|
121
|
+
{menus.showUrlMenu && <UrlMenu items={urlMenuItems} onClose={() => menus.setShowUrlMenu(false)} />}
|
|
122
|
+
|
|
107
123
|
{onboarding.onboardingActive && (
|
|
108
124
|
<OnboardingOverlay
|
|
109
125
|
step={onboarding.onboardingStep}
|
|
@@ -428,9 +428,11 @@ function ConversationPaneImpl(props: ConversationPaneProps) {
|
|
|
428
428
|
</box>
|
|
429
429
|
)}
|
|
430
430
|
|
|
431
|
-
{hasGrounding &&
|
|
432
|
-
|
|
433
|
-
|
|
431
|
+
{hasGrounding &&
|
|
432
|
+
groundingCount &&
|
|
433
|
+
(daemonState === DaemonState.IDLE || daemonState === DaemonState.SPEAKING) && (
|
|
434
|
+
<GroundingBadge count={groundingCount} />
|
|
435
|
+
)}
|
|
434
436
|
|
|
435
437
|
{showWorkingSpinner && (
|
|
436
438
|
<InlineStatusIndicator
|
|
@@ -40,6 +40,7 @@ export function HotkeysPane({ onClose }: HotkeysPaneProps) {
|
|
|
40
40
|
{ key: "O", label: "Toggle tool output previews" },
|
|
41
41
|
{ key: "N", label: "New session" },
|
|
42
42
|
{ key: "G", label: "Open Grounding Menu" },
|
|
43
|
+
{ key: "U", label: "Open URL Menu" },
|
|
43
44
|
{ key: "CTRL+X", label: "Undo last message" },
|
|
44
45
|
],
|
|
45
46
|
},
|
|
@@ -2,10 +2,10 @@
|
|
|
2
2
|
* Component for displaying token usage statistics.
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
+
import type { TokenUsage } from "../types";
|
|
5
6
|
import { COLORS } from "../ui/constants";
|
|
6
7
|
import { formatTokenCount } from "../utils/formatters";
|
|
7
|
-
import {
|
|
8
|
-
import type { TokenUsage } from "../types";
|
|
8
|
+
import { type ModelMetadata, calculateCost, formatContextUsage, formatCost } from "../utils/model-metadata";
|
|
9
9
|
|
|
10
10
|
interface TokenUsageDisplayProps {
|
|
11
11
|
usage: TokenUsage;
|
|
@@ -15,7 +15,6 @@ interface TokenUsageDisplayProps {
|
|
|
15
15
|
export function TokenUsageDisplay({ usage, modelMetadata }: TokenUsageDisplayProps) {
|
|
16
16
|
const mainPromptTokens = usage.promptTokens;
|
|
17
17
|
const mainCompletionTokens = usage.completionTokens;
|
|
18
|
-
const mainTotalTokens = mainPromptTokens + mainCompletionTokens;
|
|
19
18
|
|
|
20
19
|
// Calculate cost if we have pricing info
|
|
21
20
|
const cost =
|
|
@@ -30,11 +29,12 @@ export function TokenUsageDisplay({ usage, modelMetadata }: TokenUsageDisplayPro
|
|
|
30
29
|
)
|
|
31
30
|
: null;
|
|
32
31
|
|
|
33
|
-
//
|
|
34
|
-
const
|
|
35
|
-
const contextUsage =
|
|
36
|
-
|
|
37
|
-
|
|
32
|
+
// Context % uses latest turn only (prompt already includes full history, completion is that turn's output)
|
|
33
|
+
const latestTurnTotal = (usage.latestTurnPromptTokens ?? 0) + (usage.latestTurnCompletionTokens ?? 0);
|
|
34
|
+
const contextUsage =
|
|
35
|
+
modelMetadata?.contextLength && latestTurnTotal > 0
|
|
36
|
+
? formatContextUsage(latestTurnTotal, modelMetadata.contextLength)
|
|
37
|
+
: null;
|
|
38
38
|
|
|
39
39
|
return (
|
|
40
40
|
<box
|
|
@@ -47,12 +47,12 @@ export function TokenUsageDisplay({ usage, modelMetadata }: TokenUsageDisplayPro
|
|
|
47
47
|
>
|
|
48
48
|
<text>
|
|
49
49
|
{/* Context usage: X/Y (Z%) */}
|
|
50
|
-
{
|
|
50
|
+
{contextUsage && (
|
|
51
51
|
<>
|
|
52
52
|
<span fg={COLORS.TOKEN_USAGE_LABEL}>Tokens: </span>
|
|
53
|
-
<span fg={COLORS.TOKEN_USAGE}>{formatTokenCount(
|
|
53
|
+
<span fg={COLORS.TOKEN_USAGE}>{formatTokenCount(latestTurnTotal)}</span>
|
|
54
54
|
<span fg={COLORS.TOKEN_USAGE_LABEL}>/</span>
|
|
55
|
-
<span fg={COLORS.TOKEN_USAGE}>{formatTokenCount(modelMetadata
|
|
55
|
+
<span fg={COLORS.TOKEN_USAGE}>{formatTokenCount(modelMetadata!.contextLength)}</span>
|
|
56
56
|
<span fg={COLORS.REASONING_DIM}> ({contextUsage})</span>
|
|
57
57
|
<span fg={COLORS.TOKEN_USAGE_LABEL}> · </span>
|
|
58
58
|
</>
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import type { KeyEvent, ScrollBoxRenderable } from "@opentui/core";
|
|
2
|
+
import { useKeyboard, useRenderer } from "@opentui/react";
|
|
3
|
+
import { useCallback, useMemo, useRef } from "react";
|
|
4
|
+
import { COLORS } from "../ui/constants";
|
|
5
|
+
|
|
6
|
+
import type { UrlMenuItem } from "../types";
|
|
7
|
+
|
|
8
|
+
interface UrlMenuProps {
|
|
9
|
+
items: UrlMenuItem[];
|
|
10
|
+
onClose: () => void;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const SCROLL_AMOUNT = 1;
|
|
14
|
+
|
|
15
|
+
function splitUrl(url: string): { origin: string; path: string } {
|
|
16
|
+
try {
|
|
17
|
+
const parsed = new URL(url);
|
|
18
|
+
return { origin: parsed.origin, path: parsed.pathname + parsed.search + parsed.hash };
|
|
19
|
+
} catch {
|
|
20
|
+
const match = url.match(/^(https?:\/\/[^/]+)(\/.*)?$/);
|
|
21
|
+
if (match) {
|
|
22
|
+
return { origin: match[1] ?? url, path: match[2] ?? "" };
|
|
23
|
+
}
|
|
24
|
+
return { origin: url, path: "" };
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function UrlMenu({ items, onClose }: UrlMenuProps) {
|
|
29
|
+
const scrollRef = useRef<ScrollBoxRenderable | null>(null);
|
|
30
|
+
const renderer = useRenderer();
|
|
31
|
+
|
|
32
|
+
const sortedItems = useMemo(() => {
|
|
33
|
+
const next = [...items];
|
|
34
|
+
next.sort((a, b) => {
|
|
35
|
+
const groundedDelta = b.groundedCount - a.groundedCount;
|
|
36
|
+
if (groundedDelta !== 0) return groundedDelta;
|
|
37
|
+
|
|
38
|
+
const aPercent = a.readPercent ?? -1;
|
|
39
|
+
const bPercent = b.readPercent ?? -1;
|
|
40
|
+
if (aPercent !== bPercent) return bPercent - aPercent;
|
|
41
|
+
|
|
42
|
+
return b.lastSeenIndex - a.lastSeenIndex;
|
|
43
|
+
});
|
|
44
|
+
return next;
|
|
45
|
+
}, [items]);
|
|
46
|
+
|
|
47
|
+
const menuWidth = useMemo(() => {
|
|
48
|
+
return Math.max(80, Math.min(220, Math.floor(renderer.terminalWidth * 0.8)));
|
|
49
|
+
}, [renderer.terminalWidth]);
|
|
50
|
+
|
|
51
|
+
const menuHeight = useMemo(() => {
|
|
52
|
+
const headerHeight = 4;
|
|
53
|
+
const rowCount = sortedItems.length;
|
|
54
|
+
const contentHeight = rowCount > 0 ? rowCount : 1;
|
|
55
|
+
const minHeight = Math.floor(renderer.terminalHeight * 0.5);
|
|
56
|
+
const maxHeight = Math.floor(renderer.terminalHeight * 0.8);
|
|
57
|
+
return Math.max(minHeight, Math.min(headerHeight + contentHeight + 2, maxHeight));
|
|
58
|
+
}, [sortedItems.length, renderer.terminalHeight]);
|
|
59
|
+
|
|
60
|
+
const scrollBy = useCallback((delta: number) => {
|
|
61
|
+
const scrollbox = scrollRef.current;
|
|
62
|
+
if (!scrollbox) return;
|
|
63
|
+
const viewportHeight = scrollbox.viewport?.height ?? 0;
|
|
64
|
+
const maxScrollTop = Math.max(0, scrollbox.scrollHeight - viewportHeight);
|
|
65
|
+
const nextScrollTop = Math.max(0, Math.min(scrollbox.scrollTop + delta, maxScrollTop));
|
|
66
|
+
scrollbox.scrollTop = nextScrollTop;
|
|
67
|
+
}, []);
|
|
68
|
+
|
|
69
|
+
const handleKeyPress = useCallback(
|
|
70
|
+
(key: KeyEvent) => {
|
|
71
|
+
if (key.eventType !== "press") return;
|
|
72
|
+
|
|
73
|
+
if (key.name === "escape" || key.sequence === "u" || key.sequence === "U") {
|
|
74
|
+
onClose();
|
|
75
|
+
key.preventDefault();
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (key.sequence === "j" || key.sequence === "J" || key.name === "down") {
|
|
80
|
+
scrollBy(SCROLL_AMOUNT);
|
|
81
|
+
key.preventDefault();
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (key.sequence === "k" || key.sequence === "K" || key.name === "up") {
|
|
86
|
+
scrollBy(-SCROLL_AMOUNT);
|
|
87
|
+
key.preventDefault();
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
},
|
|
91
|
+
[onClose, scrollBy]
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
useKeyboard(handleKeyPress);
|
|
95
|
+
|
|
96
|
+
return (
|
|
97
|
+
<box
|
|
98
|
+
position="absolute"
|
|
99
|
+
left={0}
|
|
100
|
+
top={0}
|
|
101
|
+
width="100%"
|
|
102
|
+
height="100%"
|
|
103
|
+
flexDirection="column"
|
|
104
|
+
alignItems="center"
|
|
105
|
+
justifyContent="center"
|
|
106
|
+
zIndex={100}
|
|
107
|
+
>
|
|
108
|
+
<box
|
|
109
|
+
flexDirection="column"
|
|
110
|
+
backgroundColor={COLORS.MENU_BG}
|
|
111
|
+
borderStyle="single"
|
|
112
|
+
borderColor={COLORS.MENU_BORDER}
|
|
113
|
+
paddingLeft={2}
|
|
114
|
+
paddingRight={2}
|
|
115
|
+
paddingTop={1}
|
|
116
|
+
paddingBottom={1}
|
|
117
|
+
width={menuWidth}
|
|
118
|
+
height={menuHeight}
|
|
119
|
+
>
|
|
120
|
+
<box marginBottom={1} flexDirection="row" width="100%">
|
|
121
|
+
<text>
|
|
122
|
+
<span fg={COLORS.DAEMON_LABEL}>[ URLS ]</span>
|
|
123
|
+
<span fg={COLORS.REASONING_DIM}> — {sortedItems.length} fetched</span>
|
|
124
|
+
</text>
|
|
125
|
+
<box flexGrow={1} />
|
|
126
|
+
<text>
|
|
127
|
+
<span fg={COLORS.USER_LABEL}>
|
|
128
|
+
<span fg={COLORS.DAEMON_LABEL}>j/k</span> scroll · <span fg={COLORS.DAEMON_LABEL}>ESC</span>{" "}
|
|
129
|
+
close
|
|
130
|
+
</span>
|
|
131
|
+
</text>
|
|
132
|
+
</box>
|
|
133
|
+
|
|
134
|
+
<box marginBottom={1} flexDirection="row" width="100%" justifyContent="space-between">
|
|
135
|
+
<text>
|
|
136
|
+
<span fg={COLORS.REASONING_DIM}>
|
|
137
|
+
{"G".padEnd(2)}
|
|
138
|
+
{"READ".padEnd(6)}URL
|
|
139
|
+
</span>
|
|
140
|
+
</text>
|
|
141
|
+
<text>
|
|
142
|
+
<span fg={COLORS.REASONING_DIM}>(G=grounded, READ=% or HL=highlights)</span>
|
|
143
|
+
</text>
|
|
144
|
+
</box>
|
|
145
|
+
|
|
146
|
+
<scrollbox ref={scrollRef} flexGrow={1} width="100%" overflow="scroll">
|
|
147
|
+
{sortedItems.length === 0 ? (
|
|
148
|
+
<text>
|
|
149
|
+
<span fg={COLORS.REASONING_DIM}>No URLs fetched yet</span>
|
|
150
|
+
</text>
|
|
151
|
+
) : (
|
|
152
|
+
sortedItems.map((item, idx) => {
|
|
153
|
+
const { origin, path } = splitUrl(item.url);
|
|
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
|
+
: "—";
|
|
161
|
+
|
|
162
|
+
return (
|
|
163
|
+
<box key={idx} flexDirection="row" marginBottom={0}>
|
|
164
|
+
<text>
|
|
165
|
+
<span fg={grounded ? COLORS.DAEMON_TEXT : COLORS.REASONING_DIM}>
|
|
166
|
+
{grounded ? "G" : "·"}
|
|
167
|
+
</span>
|
|
168
|
+
<span fg={COLORS.REASONING_DIM}> </span>
|
|
169
|
+
<span fg={COLORS.REASONING_DIM}>{readLabel.padStart(4, " ")}</span>
|
|
170
|
+
<span fg={COLORS.REASONING_DIM}> </span>
|
|
171
|
+
<span fg={item.status === "error" ? COLORS.ERROR : COLORS.DAEMON_LABEL}>{origin}</span>
|
|
172
|
+
<span fg={COLORS.REASONING_DIM}>{path}</span>
|
|
173
|
+
</text>
|
|
174
|
+
</box>
|
|
175
|
+
);
|
|
176
|
+
})
|
|
177
|
+
)}
|
|
178
|
+
</scrollbox>
|
|
179
|
+
</box>
|
|
180
|
+
</box>
|
|
181
|
+
);
|
|
182
|
+
}
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import type { ContentBlock, ModelMessage, ToolResultOutput } from "../../types";
|
|
2
|
+
|
|
3
|
+
export const INTERRUPTED_TOOL_RESULT = "Tool execution interrupted by user";
|
|
4
|
+
|
|
5
|
+
export function normalizeInterruptedToolBlockResult(result: unknown): unknown {
|
|
6
|
+
if (result !== undefined) return result;
|
|
7
|
+
return { success: false, error: INTERRUPTED_TOOL_RESULT };
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function normalizeInterruptedToolResultOutput(result: unknown): ToolResultOutput {
|
|
11
|
+
if (result === undefined) {
|
|
12
|
+
return { type: "error-text", value: INTERRUPTED_TOOL_RESULT };
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
if (typeof result === "string") {
|
|
16
|
+
return { type: "text", value: result };
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
try {
|
|
20
|
+
JSON.stringify(result);
|
|
21
|
+
return { type: "json", value: result as ToolResultOutput["value"] };
|
|
22
|
+
} catch {
|
|
23
|
+
return { type: "text", value: String(result) };
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function buildInterruptedContentBlocks(contentBlocks: ContentBlock[]): ContentBlock[] {
|
|
28
|
+
return contentBlocks.map((block) => {
|
|
29
|
+
if (block.type !== "tool") return { ...block };
|
|
30
|
+
|
|
31
|
+
const call = { ...block.call };
|
|
32
|
+
if (call.status === "running") {
|
|
33
|
+
call.status = "failed";
|
|
34
|
+
call.error = INTERRUPTED_TOOL_RESULT;
|
|
35
|
+
}
|
|
36
|
+
if (call.subagentSteps) {
|
|
37
|
+
call.subagentSteps = call.subagentSteps.map((step) =>
|
|
38
|
+
step.status === "running" ? { ...step, status: "failed" } : step
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return {
|
|
43
|
+
...block,
|
|
44
|
+
call,
|
|
45
|
+
result: normalizeInterruptedToolBlockResult(block.result),
|
|
46
|
+
};
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function buildInterruptedModelMessages(contentBlocks: ContentBlock[]): ModelMessage[] {
|
|
51
|
+
const messages: ModelMessage[] = [];
|
|
52
|
+
|
|
53
|
+
type AssistantPart =
|
|
54
|
+
| { type: "text"; text: string }
|
|
55
|
+
| { type: "reasoning"; text: string }
|
|
56
|
+
| { type: "tool-call"; toolCallId: string; toolName: string; input: unknown };
|
|
57
|
+
|
|
58
|
+
type ToolResultPart = {
|
|
59
|
+
type: "tool-result";
|
|
60
|
+
toolCallId: string;
|
|
61
|
+
toolName: string;
|
|
62
|
+
output: ToolResultOutput;
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
let assistantParts: AssistantPart[] = [];
|
|
66
|
+
let toolResults: ToolResultPart[] = [];
|
|
67
|
+
|
|
68
|
+
for (const block of contentBlocks) {
|
|
69
|
+
if (block.type === "reasoning" && block.content) {
|
|
70
|
+
if (toolResults.length > 0) {
|
|
71
|
+
messages.push({
|
|
72
|
+
role: "tool",
|
|
73
|
+
content: [...toolResults],
|
|
74
|
+
} as unknown as ModelMessage);
|
|
75
|
+
toolResults = [];
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
assistantParts.push({ type: "reasoning", text: block.content });
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (block.type === "text" && block.content) {
|
|
83
|
+
if (toolResults.length > 0) {
|
|
84
|
+
messages.push({
|
|
85
|
+
role: "tool",
|
|
86
|
+
content: [...toolResults],
|
|
87
|
+
} as unknown as ModelMessage);
|
|
88
|
+
toolResults = [];
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
assistantParts.push({ type: "text", text: block.content });
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (block.type === "tool") {
|
|
96
|
+
if (toolResults.length > 0) {
|
|
97
|
+
messages.push({
|
|
98
|
+
role: "tool",
|
|
99
|
+
content: [...toolResults],
|
|
100
|
+
} as unknown as ModelMessage);
|
|
101
|
+
toolResults = [];
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const toolCallId = block.call.toolCallId;
|
|
105
|
+
if (!toolCallId) {
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
assistantParts.push({
|
|
110
|
+
type: "tool-call",
|
|
111
|
+
toolCallId,
|
|
112
|
+
toolName: block.call.name,
|
|
113
|
+
input: block.call.input ?? {},
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
if (assistantParts.length > 0) {
|
|
117
|
+
messages.push({
|
|
118
|
+
role: "assistant",
|
|
119
|
+
content: [...assistantParts],
|
|
120
|
+
} as unknown as ModelMessage);
|
|
121
|
+
assistantParts = [];
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
toolResults.push({
|
|
125
|
+
type: "tool-result",
|
|
126
|
+
toolCallId,
|
|
127
|
+
toolName: block.call.name,
|
|
128
|
+
output: normalizeInterruptedToolResultOutput(block.result),
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (assistantParts.length > 0) {
|
|
134
|
+
messages.push({
|
|
135
|
+
role: "assistant",
|
|
136
|
+
content: [...assistantParts],
|
|
137
|
+
} as unknown as ModelMessage);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (toolResults.length > 0) {
|
|
141
|
+
messages.push({
|
|
142
|
+
role: "tool",
|
|
143
|
+
content: [...toolResults],
|
|
144
|
+
} as unknown as ModelMessage);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return messages;
|
|
148
|
+
}
|