@makefinks/daemon 0.1.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/LICENSE +21 -0
- package/README.md +126 -0
- package/dist/cli.js +22 -0
- package/package.json +79 -0
- package/src/ai/agent-turn-runner.ts +130 -0
- package/src/ai/daemon-ai.ts +403 -0
- package/src/ai/exa-client.ts +21 -0
- package/src/ai/exa-fetch-cache.ts +104 -0
- package/src/ai/model-config.ts +99 -0
- package/src/ai/sanitize-messages.ts +83 -0
- package/src/ai/system-prompt.ts +363 -0
- package/src/ai/tools/fetch-urls.ts +187 -0
- package/src/ai/tools/grounding-manager.ts +94 -0
- package/src/ai/tools/index.ts +52 -0
- package/src/ai/tools/read-file.ts +100 -0
- package/src/ai/tools/render-url.ts +275 -0
- package/src/ai/tools/run-bash.ts +224 -0
- package/src/ai/tools/subagents.ts +195 -0
- package/src/ai/tools/todo-manager.ts +150 -0
- package/src/ai/tools/web-search.ts +91 -0
- package/src/app/App.tsx +711 -0
- package/src/app/components/AppOverlays.tsx +131 -0
- package/src/app/components/AvatarLayer.tsx +51 -0
- package/src/app/components/ConversationPane.tsx +476 -0
- package/src/avatar/DaemonAvatarRenderable.ts +343 -0
- package/src/avatar/daemon-avatar-rig.ts +1165 -0
- package/src/avatar-preview.ts +186 -0
- package/src/cli.ts +26 -0
- package/src/components/ApiKeyInput.tsx +99 -0
- package/src/components/ApiKeyStep.tsx +95 -0
- package/src/components/ApprovalPicker.tsx +109 -0
- package/src/components/ContentBlockView.tsx +141 -0
- package/src/components/DaemonText.tsx +34 -0
- package/src/components/DeviceMenu.tsx +166 -0
- package/src/components/GroundingBadge.tsx +21 -0
- package/src/components/GroundingMenu.tsx +310 -0
- package/src/components/HotkeysPane.tsx +115 -0
- package/src/components/InlineStatusIndicator.tsx +106 -0
- package/src/components/ModelMenu.tsx +411 -0
- package/src/components/OnboardingOverlay.tsx +446 -0
- package/src/components/ProviderMenu.tsx +177 -0
- package/src/components/SessionMenu.tsx +297 -0
- package/src/components/SettingsMenu.tsx +291 -0
- package/src/components/StatusBar.tsx +126 -0
- package/src/components/TokenUsageDisplay.tsx +92 -0
- package/src/components/ToolCallView.tsx +113 -0
- package/src/components/TypingInputBar.tsx +131 -0
- package/src/components/tool-layouts/components.tsx +120 -0
- package/src/components/tool-layouts/defaults.ts +9 -0
- package/src/components/tool-layouts/index.ts +22 -0
- package/src/components/tool-layouts/layouts/bash.ts +110 -0
- package/src/components/tool-layouts/layouts/grounding.tsx +98 -0
- package/src/components/tool-layouts/layouts/index.ts +8 -0
- package/src/components/tool-layouts/layouts/read-file.ts +59 -0
- package/src/components/tool-layouts/layouts/subagent.tsx +118 -0
- package/src/components/tool-layouts/layouts/system-info.ts +8 -0
- package/src/components/tool-layouts/layouts/todo.tsx +139 -0
- package/src/components/tool-layouts/layouts/url-tools.ts +220 -0
- package/src/components/tool-layouts/layouts/web-search.ts +110 -0
- package/src/components/tool-layouts/registry.ts +17 -0
- package/src/components/tool-layouts/types.ts +94 -0
- package/src/hooks/daemon-event-handlers.ts +944 -0
- package/src/hooks/keyboard-handlers.ts +399 -0
- package/src/hooks/menu-navigation.ts +147 -0
- package/src/hooks/use-app-audio-devices-loader.ts +71 -0
- package/src/hooks/use-app-callbacks.ts +202 -0
- package/src/hooks/use-app-context-builder.ts +159 -0
- package/src/hooks/use-app-display-state.ts +162 -0
- package/src/hooks/use-app-menus.ts +51 -0
- package/src/hooks/use-app-model-pricing-loader.ts +45 -0
- package/src/hooks/use-app-model.ts +123 -0
- package/src/hooks/use-app-openrouter-models-loader.ts +44 -0
- package/src/hooks/use-app-openrouter-provider-loader.ts +35 -0
- package/src/hooks/use-app-preferences-bootstrap.ts +212 -0
- package/src/hooks/use-app-sessions.ts +105 -0
- package/src/hooks/use-app-settings.ts +62 -0
- package/src/hooks/use-conversation-manager.ts +163 -0
- package/src/hooks/use-copy-on-select.ts +50 -0
- package/src/hooks/use-daemon-events.ts +396 -0
- package/src/hooks/use-daemon-keyboard.ts +397 -0
- package/src/hooks/use-grounding.ts +46 -0
- package/src/hooks/use-input-history.ts +92 -0
- package/src/hooks/use-menu-keyboard.ts +93 -0
- package/src/hooks/use-playwright-notification.ts +23 -0
- package/src/hooks/use-reasoning-animation.ts +97 -0
- package/src/hooks/use-response-timer.ts +55 -0
- package/src/hooks/use-tool-approval.tsx +202 -0
- package/src/hooks/use-typing-mode.ts +137 -0
- package/src/hooks/use-voice-dependencies-notification.ts +37 -0
- package/src/index.tsx +48 -0
- package/src/scripts/setup-browsers.ts +42 -0
- package/src/state/app-context.tsx +160 -0
- package/src/state/daemon-events.ts +67 -0
- package/src/state/daemon-state.ts +493 -0
- package/src/state/migrations/001-init.ts +33 -0
- package/src/state/migrations/index.ts +8 -0
- package/src/state/model-history-store.ts +45 -0
- package/src/state/runtime-context.ts +21 -0
- package/src/state/session-store.ts +359 -0
- package/src/types/index.ts +405 -0
- package/src/types/theme.ts +52 -0
- package/src/ui/constants.ts +157 -0
- package/src/utils/clipboard.ts +89 -0
- package/src/utils/debug-logger.ts +69 -0
- package/src/utils/formatters.ts +242 -0
- package/src/utils/js-rendering.ts +77 -0
- package/src/utils/markdown-tables.ts +234 -0
- package/src/utils/model-metadata.ts +191 -0
- package/src/utils/openrouter-endpoints.ts +212 -0
- package/src/utils/openrouter-models.ts +205 -0
- package/src/utils/openrouter-pricing.ts +59 -0
- package/src/utils/openrouter-reported-cost.ts +16 -0
- package/src/utils/paste.ts +33 -0
- package/src/utils/preferences.ts +289 -0
- package/src/utils/text-fragment.ts +39 -0
- package/src/utils/tool-output-preview.ts +250 -0
- package/src/utils/voice-dependencies.ts +107 -0
- package/src/utils/workspace-manager.ts +85 -0
- package/src/voice/audio-recorder.ts +579 -0
- package/src/voice/mic-level.ts +35 -0
- package/src/voice/tts/openai-tts-stream.ts +222 -0
- package/src/voice/tts/speech-controller.ts +64 -0
- package/src/voice/tts/tts-player.ts +257 -0
- package/src/voice/voice-input-controller.ts +96 -0
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Component for displaying token usage statistics.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { COLORS } from "../ui/constants";
|
|
6
|
+
import { formatTokenCount } from "../utils/formatters";
|
|
7
|
+
import { calculateCost, formatCost, formatContextUsage, type ModelMetadata } from "../utils/model-metadata";
|
|
8
|
+
import type { TokenUsage } from "../types";
|
|
9
|
+
|
|
10
|
+
interface TokenUsageDisplayProps {
|
|
11
|
+
usage: TokenUsage;
|
|
12
|
+
modelMetadata?: ModelMetadata | null;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function TokenUsageDisplay({ usage, modelMetadata }: TokenUsageDisplayProps) {
|
|
16
|
+
const mainPromptTokens = usage.promptTokens;
|
|
17
|
+
const mainCompletionTokens = usage.completionTokens;
|
|
18
|
+
const mainTotalTokens = mainPromptTokens + mainCompletionTokens;
|
|
19
|
+
|
|
20
|
+
// Calculate cost if we have pricing info
|
|
21
|
+
const cost =
|
|
22
|
+
typeof usage.cost === "number"
|
|
23
|
+
? usage.cost
|
|
24
|
+
: modelMetadata?.pricing
|
|
25
|
+
? calculateCost(
|
|
26
|
+
mainPromptTokens + (usage.subagentPromptTokens ?? 0),
|
|
27
|
+
mainCompletionTokens + (usage.subagentCompletionTokens ?? 0),
|
|
28
|
+
modelMetadata.pricing,
|
|
29
|
+
usage.cachedInputTokens
|
|
30
|
+
)
|
|
31
|
+
: null;
|
|
32
|
+
|
|
33
|
+
// Calculate context usage percentage
|
|
34
|
+
const contextTotalTokens = mainTotalTokens;
|
|
35
|
+
const contextUsage = modelMetadata?.contextLength
|
|
36
|
+
? formatContextUsage(contextTotalTokens, modelMetadata.contextLength)
|
|
37
|
+
: null;
|
|
38
|
+
|
|
39
|
+
return (
|
|
40
|
+
<box
|
|
41
|
+
height={1}
|
|
42
|
+
width="100%"
|
|
43
|
+
flexShrink={0}
|
|
44
|
+
flexDirection="row"
|
|
45
|
+
justifyContent="center"
|
|
46
|
+
alignItems="center"
|
|
47
|
+
>
|
|
48
|
+
<text>
|
|
49
|
+
{/* Context usage: X/Y (Z%) */}
|
|
50
|
+
{modelMetadata?.contextLength && (
|
|
51
|
+
<>
|
|
52
|
+
<span fg={COLORS.TOKEN_USAGE_LABEL}>Tokens: </span>
|
|
53
|
+
<span fg={COLORS.TOKEN_USAGE}>{formatTokenCount(contextTotalTokens)}</span>
|
|
54
|
+
<span fg={COLORS.TOKEN_USAGE_LABEL}>/</span>
|
|
55
|
+
<span fg={COLORS.TOKEN_USAGE}>{formatTokenCount(modelMetadata.contextLength)}</span>
|
|
56
|
+
<span fg={COLORS.REASONING_DIM}> ({contextUsage})</span>
|
|
57
|
+
<span fg={COLORS.TOKEN_USAGE_LABEL}> · </span>
|
|
58
|
+
</>
|
|
59
|
+
)}
|
|
60
|
+
|
|
61
|
+
{/* Token breakdown: in/out */}
|
|
62
|
+
<span fg={COLORS.TOKEN_USAGE}>{formatTokenCount(mainPromptTokens)} in</span>
|
|
63
|
+
<span fg={COLORS.TOKEN_USAGE_LABEL}> / </span>
|
|
64
|
+
<span fg={COLORS.TOKEN_USAGE}>{formatTokenCount(mainCompletionTokens)} out</span>
|
|
65
|
+
|
|
66
|
+
{/* Reasoning tokens */}
|
|
67
|
+
{usage.reasoningTokens !== undefined && usage.reasoningTokens > 0 && (
|
|
68
|
+
<>
|
|
69
|
+
<span fg={COLORS.TOKEN_USAGE_LABEL}> / </span>
|
|
70
|
+
<span fg={COLORS.REASONING_DIM}>{formatTokenCount(usage.reasoningTokens)} reasoning</span>
|
|
71
|
+
</>
|
|
72
|
+
)}
|
|
73
|
+
|
|
74
|
+
{/* Cached tokens */}
|
|
75
|
+
{usage.cachedInputTokens !== undefined && usage.cachedInputTokens > 0 && (
|
|
76
|
+
<>
|
|
77
|
+
<span fg={COLORS.TOKEN_USAGE_LABEL}> / </span>
|
|
78
|
+
<span fg={COLORS.DAEMON_LABEL}>{formatTokenCount(usage.cachedInputTokens)} cached</span>
|
|
79
|
+
</>
|
|
80
|
+
)}
|
|
81
|
+
|
|
82
|
+
{/* Cost */}
|
|
83
|
+
{cost !== null && (
|
|
84
|
+
<>
|
|
85
|
+
<span fg={COLORS.TOKEN_USAGE_LABEL}> · </span>
|
|
86
|
+
<span fg={COLORS.TYPING_PROMPT}>{formatCost(cost)}</span>
|
|
87
|
+
</>
|
|
88
|
+
)}
|
|
89
|
+
</text>
|
|
90
|
+
</box>
|
|
91
|
+
);
|
|
92
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { useMemo } from "react";
|
|
2
|
+
import { COLORS } from "../ui/constants";
|
|
3
|
+
import type { ToolCall } from "../types";
|
|
4
|
+
import {
|
|
5
|
+
getToolLayout,
|
|
6
|
+
defaultToolLayout,
|
|
7
|
+
getDefaultAbbreviation,
|
|
8
|
+
ToolHeaderView,
|
|
9
|
+
ToolBodyView,
|
|
10
|
+
ResultPreviewView,
|
|
11
|
+
ErrorPreviewView,
|
|
12
|
+
getStatusBorderColor,
|
|
13
|
+
} from "./tool-layouts";
|
|
14
|
+
import { ApprovalPicker } from "./ApprovalPicker";
|
|
15
|
+
import { useToolApprovalForCall } from "../hooks/use-tool-approval";
|
|
16
|
+
|
|
17
|
+
interface ToolCallViewProps {
|
|
18
|
+
call: ToolCall;
|
|
19
|
+
result?: unknown;
|
|
20
|
+
showOutput?: boolean;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function ApprovalResultBadge({ result }: { result: "approved" | "denied" }) {
|
|
24
|
+
const isApproved = result === "approved";
|
|
25
|
+
const color = isApproved ? COLORS.STATUS_COMPLETED : COLORS.STATUS_FAILED;
|
|
26
|
+
const label = isApproved ? "APPROVED" : "DENIED";
|
|
27
|
+
|
|
28
|
+
return (
|
|
29
|
+
<box marginTop={1}>
|
|
30
|
+
<text>
|
|
31
|
+
<span fg={color}>
|
|
32
|
+
{">> "}
|
|
33
|
+
{label}
|
|
34
|
+
</span>
|
|
35
|
+
</text>
|
|
36
|
+
</box>
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function ToolCallView({ call, result, showOutput = true }: ToolCallViewProps) {
|
|
41
|
+
const layout = getToolLayout(call.name) ?? defaultToolLayout;
|
|
42
|
+
const isAwaitingApproval = call.status === "awaiting_approval";
|
|
43
|
+
const isRunning = call.status === "running" || call.status === "streaming";
|
|
44
|
+
const isFailed = call.status === "failed";
|
|
45
|
+
|
|
46
|
+
const { needsApproval, isActive, approve, deny, approveAll, denyAll } = useToolApprovalForCall(
|
|
47
|
+
call.toolCallId
|
|
48
|
+
);
|
|
49
|
+
|
|
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
|
+
);
|
|
56
|
+
|
|
57
|
+
const resultPreviewLines = useMemo(() => {
|
|
58
|
+
if (!showOutput) return null;
|
|
59
|
+
return layout.formatResult?.(result) ?? null;
|
|
60
|
+
}, [result, showOutput, layout]);
|
|
61
|
+
|
|
62
|
+
const toolColor =
|
|
63
|
+
call.status === "completed"
|
|
64
|
+
? COLORS.STATUS_COMPLETED
|
|
65
|
+
: isAwaitingApproval
|
|
66
|
+
? COLORS.STATUS_APPROVAL
|
|
67
|
+
: COLORS.TOOLS;
|
|
68
|
+
const toolName = layout.abbreviation ?? getDefaultAbbreviation(call.name);
|
|
69
|
+
const borderColor = getStatusBorderColor(call.status);
|
|
70
|
+
|
|
71
|
+
const customBody = useMemo(() => {
|
|
72
|
+
if (!layout.renderBody) return null;
|
|
73
|
+
return layout.renderBody({ call, result, showOutput });
|
|
74
|
+
}, [layout, call, result, showOutput]);
|
|
75
|
+
|
|
76
|
+
return (
|
|
77
|
+
<box
|
|
78
|
+
flexDirection="column"
|
|
79
|
+
backgroundColor={COLORS.TOOL_INPUT_BG}
|
|
80
|
+
borderStyle="single"
|
|
81
|
+
borderColor={borderColor}
|
|
82
|
+
paddingLeft={1}
|
|
83
|
+
paddingRight={1}
|
|
84
|
+
paddingTop={0}
|
|
85
|
+
paddingBottom={0}
|
|
86
|
+
width="100%"
|
|
87
|
+
>
|
|
88
|
+
<ToolHeaderView toolName={toolName} header={header} isRunning={isRunning} toolColor={toolColor} />
|
|
89
|
+
|
|
90
|
+
{customBody}
|
|
91
|
+
|
|
92
|
+
{!customBody && body && <ToolBodyView body={body} />}
|
|
93
|
+
|
|
94
|
+
{needsApproval && (
|
|
95
|
+
<ApprovalPicker
|
|
96
|
+
onApprove={approve}
|
|
97
|
+
onDeny={deny}
|
|
98
|
+
onApproveAll={approveAll}
|
|
99
|
+
onDenyAll={denyAll}
|
|
100
|
+
focused={isActive}
|
|
101
|
+
/>
|
|
102
|
+
)}
|
|
103
|
+
|
|
104
|
+
{showOutput && resultPreviewLines && resultPreviewLines.length > 0 && (
|
|
105
|
+
<ResultPreviewView lines={resultPreviewLines} />
|
|
106
|
+
)}
|
|
107
|
+
|
|
108
|
+
{isFailed && call.error && <ErrorPreviewView error={call.error} />}
|
|
109
|
+
|
|
110
|
+
{call.approvalResult && <ApprovalResultBadge result={call.approvalResult} />}
|
|
111
|
+
</box>
|
|
112
|
+
);
|
|
113
|
+
}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared typing input row used in both initial and conversation layouts.
|
|
3
|
+
* Ensures the OpenTUI input has a usable width and supports multi-line text.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { KeyBinding, TextareaRenderable, PasteEvent, KeyEvent } from "@opentui/core";
|
|
7
|
+
import { useRef, type RefObject } from "react";
|
|
8
|
+
import { COLORS } from "../ui/constants";
|
|
9
|
+
import { debug } from "../utils/debug-logger";
|
|
10
|
+
import { pasteClipboardIntoTextarea } from "../utils/paste";
|
|
11
|
+
|
|
12
|
+
export interface TypingInputBarProps {
|
|
13
|
+
onSubmit: () => void;
|
|
14
|
+
onContentChange?: (value: string) => void;
|
|
15
|
+
onHistoryUp?: () => void;
|
|
16
|
+
onHistoryDown?: () => void;
|
|
17
|
+
placeholder?: string;
|
|
18
|
+
width?: number | "auto" | `${number}%`;
|
|
19
|
+
maxWidth?: number | "auto" | `${number}%`;
|
|
20
|
+
minWidth?: number | "auto" | `${number}%`;
|
|
21
|
+
height?: number;
|
|
22
|
+
textareaRef?: RefObject<TextareaRenderable | null>;
|
|
23
|
+
keyBindings?: KeyBinding[];
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function TypingInputBar({
|
|
27
|
+
onSubmit,
|
|
28
|
+
onContentChange,
|
|
29
|
+
onHistoryUp,
|
|
30
|
+
onHistoryDown,
|
|
31
|
+
placeholder = "",
|
|
32
|
+
width = "100%",
|
|
33
|
+
maxWidth = "100%",
|
|
34
|
+
minWidth = 20,
|
|
35
|
+
height = 4,
|
|
36
|
+
textareaRef,
|
|
37
|
+
keyBindings = [
|
|
38
|
+
{ name: "return", action: "submit" },
|
|
39
|
+
{ name: "linefeed", action: "newline" },
|
|
40
|
+
],
|
|
41
|
+
}: TypingInputBarProps) {
|
|
42
|
+
const localRef = useRef<TextareaRenderable | null>(null);
|
|
43
|
+
const activeRef = textareaRef ?? localRef;
|
|
44
|
+
|
|
45
|
+
const handleContentChange = () => {
|
|
46
|
+
const text = activeRef.current?.plainText ?? "";
|
|
47
|
+
onContentChange?.(text);
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const handlePaste = (event: PasteEvent) => {
|
|
51
|
+
debug.log("[TypingInputBar] onPaste received", {
|
|
52
|
+
textLength: event.text.length,
|
|
53
|
+
textPreview: event.text.slice(0, 50),
|
|
54
|
+
defaultPrevented: event.defaultPrevented,
|
|
55
|
+
});
|
|
56
|
+
if (!event.text.trim()) {
|
|
57
|
+
event.preventDefault();
|
|
58
|
+
void pasteClipboardIntoTextarea(activeRef.current, { source: "typing-onPaste" });
|
|
59
|
+
}
|
|
60
|
+
// Otherwise, let the textarea handle it
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
const handleKeyDown = (key: KeyEvent) => {
|
|
64
|
+
if (key.eventType !== "press") return;
|
|
65
|
+
|
|
66
|
+
if (key.name === "up" && onHistoryUp) {
|
|
67
|
+
key.preventDefault();
|
|
68
|
+
onHistoryUp();
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
if (key.name === "down" && onHistoryDown) {
|
|
72
|
+
key.preventDefault();
|
|
73
|
+
onHistoryDown();
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (key.name !== "v") return;
|
|
78
|
+
if (!(key.ctrl || key.meta || key.super)) return;
|
|
79
|
+
key.preventDefault();
|
|
80
|
+
void pasteClipboardIntoTextarea(activeRef.current, { source: "typing-shortcut" });
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
return (
|
|
84
|
+
<box flexDirection="column" width={width} maxWidth={maxWidth} minWidth={minWidth}>
|
|
85
|
+
<box
|
|
86
|
+
border={true}
|
|
87
|
+
borderStyle="single"
|
|
88
|
+
borderColor="#3f4651"
|
|
89
|
+
backgroundColor="#0d0d14"
|
|
90
|
+
flexDirection="row"
|
|
91
|
+
alignItems="stretch"
|
|
92
|
+
width="100%"
|
|
93
|
+
height={height}
|
|
94
|
+
paddingLeft={1}
|
|
95
|
+
paddingRight={1}
|
|
96
|
+
>
|
|
97
|
+
<box paddingTop={0} paddingRight={2}>
|
|
98
|
+
<text>
|
|
99
|
+
<span fg="#6b7280">{">"}</span>
|
|
100
|
+
</text>
|
|
101
|
+
</box>
|
|
102
|
+
<textarea
|
|
103
|
+
ref={activeRef}
|
|
104
|
+
placeholder={placeholder}
|
|
105
|
+
focused={true}
|
|
106
|
+
wrapMode="word"
|
|
107
|
+
keyBindings={keyBindings}
|
|
108
|
+
onContentChange={handleContentChange}
|
|
109
|
+
onPaste={handlePaste}
|
|
110
|
+
onKeyDown={handleKeyDown}
|
|
111
|
+
onSubmit={() => onSubmit()}
|
|
112
|
+
width="100%"
|
|
113
|
+
height="100%"
|
|
114
|
+
style={{
|
|
115
|
+
backgroundColor: "transparent",
|
|
116
|
+
focusedBackgroundColor: "transparent",
|
|
117
|
+
textColor: "#9ca3af",
|
|
118
|
+
focusedTextColor: "#e5e7eb",
|
|
119
|
+
cursorColor: COLORS.TYPING_PROMPT,
|
|
120
|
+
cursorStyle: { style: "block", blinking: true },
|
|
121
|
+
}}
|
|
122
|
+
/>
|
|
123
|
+
</box>
|
|
124
|
+
<box justifyContent="center" width="100%" marginTop={0}>
|
|
125
|
+
<text>
|
|
126
|
+
<span fg="#4b5563">Ctrl+V to paste · Ctrl+J for new line</span>
|
|
127
|
+
</text>
|
|
128
|
+
</box>
|
|
129
|
+
</box>
|
|
130
|
+
);
|
|
131
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { TextAttributes } from "@opentui/core";
|
|
2
|
+
import { COLORS } from "../../ui/constants";
|
|
3
|
+
import type { ToolHeader, ToolBody, ToolBodyLine } from "./types";
|
|
4
|
+
import type { ToolCallStatus } from "../../types";
|
|
5
|
+
|
|
6
|
+
interface ToolHeaderViewProps {
|
|
7
|
+
toolName: string;
|
|
8
|
+
header: ToolHeader | null;
|
|
9
|
+
isRunning: boolean;
|
|
10
|
+
toolColor: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function ToolHeaderView({ toolName, header, isRunning, toolColor }: ToolHeaderViewProps) {
|
|
14
|
+
return (
|
|
15
|
+
<box flexDirection="row" alignItems="center" justifyContent="space-between" width="100%">
|
|
16
|
+
<text>
|
|
17
|
+
<span fg={toolColor}>{"↯ "}</span>
|
|
18
|
+
<span fg={toolColor}>{toolName}</span>
|
|
19
|
+
{header?.primary && <span fg={COLORS.TOOL_INPUT_TEXT}>{` ${header.primary}`}</span>}
|
|
20
|
+
{header?.secondary && (
|
|
21
|
+
<span
|
|
22
|
+
fg={COLORS.REASONING_DIM}
|
|
23
|
+
attributes={header.secondaryStyle === "italic" ? TextAttributes.ITALIC : TextAttributes.NONE}
|
|
24
|
+
>
|
|
25
|
+
{` ${header.secondary}`}
|
|
26
|
+
</span>
|
|
27
|
+
)}
|
|
28
|
+
</text>
|
|
29
|
+
{isRunning && <spinner name="dots" color={COLORS.STATUS_RUNNING} />}
|
|
30
|
+
</box>
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
interface ToolBodyViewProps {
|
|
35
|
+
body: ToolBody;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function getLineColor(line: ToolBodyLine): string {
|
|
39
|
+
if (line.color) return line.color;
|
|
40
|
+
if (line.status) {
|
|
41
|
+
switch (line.status) {
|
|
42
|
+
case "running":
|
|
43
|
+
return COLORS.STATUS_RUNNING;
|
|
44
|
+
case "completed":
|
|
45
|
+
return COLORS.STATUS_COMPLETED;
|
|
46
|
+
case "failed":
|
|
47
|
+
return COLORS.STATUS_FAILED;
|
|
48
|
+
default:
|
|
49
|
+
return COLORS.STATUS_PENDING;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return COLORS.TOOL_INPUT_TEXT;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function ToolBodyView({ body }: ToolBodyViewProps) {
|
|
56
|
+
return (
|
|
57
|
+
<box flexDirection="column" paddingLeft={2} marginTop={0}>
|
|
58
|
+
{body.lines.map((line, idx) => (
|
|
59
|
+
<box key={idx} flexDirection="row" alignItems="center">
|
|
60
|
+
{line.status === "running" ? (
|
|
61
|
+
<spinner name="dots" color={getLineColor(line)} />
|
|
62
|
+
) : line.icon ? (
|
|
63
|
+
<text>
|
|
64
|
+
<span fg={getLineColor(line)}>{line.icon}</span>
|
|
65
|
+
</text>
|
|
66
|
+
) : null}
|
|
67
|
+
<text marginLeft={line.icon || line.status === "running" ? 1 : 0}>
|
|
68
|
+
<span fg={getLineColor(line)} attributes={line.attributes ?? TextAttributes.NONE}>
|
|
69
|
+
{line.text}
|
|
70
|
+
</span>
|
|
71
|
+
</text>
|
|
72
|
+
</box>
|
|
73
|
+
))}
|
|
74
|
+
</box>
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
interface ResultPreviewViewProps {
|
|
79
|
+
lines: string[];
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function ResultPreviewView({ lines }: ResultPreviewViewProps) {
|
|
83
|
+
return (
|
|
84
|
+
<box flexDirection="column" paddingLeft={2}>
|
|
85
|
+
{lines.map((line, idx) => (
|
|
86
|
+
<text key={idx}>
|
|
87
|
+
<span fg={COLORS.REASONING_DIM}>{`› ${line}`}</span>
|
|
88
|
+
</text>
|
|
89
|
+
))}
|
|
90
|
+
</box>
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
interface ErrorPreviewViewProps {
|
|
95
|
+
error: string;
|
|
96
|
+
maxLength?: number;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export function ErrorPreviewView({ error, maxLength = 120 }: ErrorPreviewViewProps) {
|
|
100
|
+
const displayError = error.length > maxLength ? `${error.slice(0, maxLength)}…` : error;
|
|
101
|
+
|
|
102
|
+
return (
|
|
103
|
+
<box flexDirection="column" paddingLeft={2}>
|
|
104
|
+
<text>
|
|
105
|
+
<span fg={COLORS.STATUS_FAILED}>{`⚠ ${displayError}`}</span>
|
|
106
|
+
</text>
|
|
107
|
+
</box>
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export function getStatusBorderColor(status: ToolCallStatus | undefined): string {
|
|
112
|
+
switch (status) {
|
|
113
|
+
case "completed":
|
|
114
|
+
return COLORS.TOOL_INPUT_BORDER;
|
|
115
|
+
case "failed":
|
|
116
|
+
return COLORS.STATUS_FAILED;
|
|
117
|
+
default:
|
|
118
|
+
return COLORS.TOOL_INPUT_BORDER;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { ToolLayoutConfig } from "./types";
|
|
2
|
+
|
|
3
|
+
export const defaultToolLayout: ToolLayoutConfig = {
|
|
4
|
+
abbreviation: "tool",
|
|
5
|
+
};
|
|
6
|
+
|
|
7
|
+
export function getDefaultAbbreviation(toolName: string): string {
|
|
8
|
+
return toolName.length > 8 ? toolName.slice(0, 8) : toolName;
|
|
9
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export type {
|
|
2
|
+
ToolLayoutConfig,
|
|
3
|
+
ToolHeader,
|
|
4
|
+
ToolBody,
|
|
5
|
+
ToolBodyLine,
|
|
6
|
+
ToolLayoutRenderProps,
|
|
7
|
+
ToolLayoutRegistry,
|
|
8
|
+
} from "./types";
|
|
9
|
+
|
|
10
|
+
export { registerToolLayout, getToolLayout, hasToolLayout, registry } from "./registry";
|
|
11
|
+
|
|
12
|
+
export { defaultToolLayout, getDefaultAbbreviation } from "./defaults";
|
|
13
|
+
|
|
14
|
+
export {
|
|
15
|
+
ToolHeaderView,
|
|
16
|
+
ToolBodyView,
|
|
17
|
+
ResultPreviewView,
|
|
18
|
+
ErrorPreviewView,
|
|
19
|
+
getStatusBorderColor,
|
|
20
|
+
} from "./components";
|
|
21
|
+
|
|
22
|
+
import "./layouts";
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import type { ToolLayoutConfig, ToolHeader, ToolBody } from "../types";
|
|
2
|
+
import { registerToolLayout } from "../registry";
|
|
3
|
+
|
|
4
|
+
type UnknownRecord = Record<string, unknown>;
|
|
5
|
+
|
|
6
|
+
function isRecord(value: unknown): value is UnknownRecord {
|
|
7
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
interface BashInput {
|
|
11
|
+
command: string;
|
|
12
|
+
description: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function extractBashInput(input: unknown): BashInput | null {
|
|
16
|
+
if (!isRecord(input)) return null;
|
|
17
|
+
if (!("command" in input) || typeof input.command !== "string") return null;
|
|
18
|
+
const description =
|
|
19
|
+
"description" in input && typeof input.description === "string" ? input.description : "";
|
|
20
|
+
return { command: input.command, description };
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function pickFirstNonEmpty(...parts: Array<string | undefined | null>): string {
|
|
24
|
+
for (const part of parts) {
|
|
25
|
+
if (typeof part === "string" && part.trim().length > 0) return part;
|
|
26
|
+
}
|
|
27
|
+
return "";
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function formatBashResult(result: unknown): string[] | null {
|
|
31
|
+
if (!isRecord(result)) return null;
|
|
32
|
+
const success = "success" in result ? result.success : undefined;
|
|
33
|
+
const exitCode =
|
|
34
|
+
typeof result.exitCode === "number" || result.exitCode === null ? result.exitCode : undefined;
|
|
35
|
+
|
|
36
|
+
const stdout = typeof result.stdout === "string" ? result.stdout : "";
|
|
37
|
+
const stderr = typeof result.stderr === "string" ? result.stderr : "";
|
|
38
|
+
const error = typeof result.error === "string" ? result.error : "";
|
|
39
|
+
|
|
40
|
+
const body = pickFirstNonEmpty(stdout, stderr, error);
|
|
41
|
+
if (!body) {
|
|
42
|
+
if (typeof success === "boolean") {
|
|
43
|
+
const line = `success=${success}${exitCode !== undefined ? ` exit=${String(exitCode)}` : ""}`;
|
|
44
|
+
return [line];
|
|
45
|
+
}
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const label = stdout.trim().length > 0 ? "stdout" : stderr.trim().length > 0 ? "stderr" : "error";
|
|
50
|
+
const meta =
|
|
51
|
+
typeof success === "boolean" || exitCode !== undefined
|
|
52
|
+
? ` (${typeof success === "boolean" ? `success=${success}` : ""}${exitCode !== undefined ? `${typeof success === "boolean" ? " " : ""}exit=${String(exitCode)}` : ""})`
|
|
53
|
+
: "";
|
|
54
|
+
|
|
55
|
+
const MAX_LINES = 4;
|
|
56
|
+
const MAX_CHARS_PER_LINE = 160;
|
|
57
|
+
const lines = body
|
|
58
|
+
.split("\n")
|
|
59
|
+
.map((l) => l.trimEnd())
|
|
60
|
+
.filter((l) => l.length > 0)
|
|
61
|
+
.slice(0, MAX_LINES)
|
|
62
|
+
.map((l) => (l.length > MAX_CHARS_PER_LINE ? `${l.slice(0, MAX_CHARS_PER_LINE - 1)}…` : l));
|
|
63
|
+
|
|
64
|
+
if (lines.length === 0) return [`${label}${meta}: (empty)`];
|
|
65
|
+
lines[0] = `${label}${meta}: ${lines[0]}`;
|
|
66
|
+
return lines;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export const bashLayout: ToolLayoutConfig = {
|
|
70
|
+
abbreviation: "bash",
|
|
71
|
+
|
|
72
|
+
getHeader: (input): ToolHeader | null => {
|
|
73
|
+
const bashInput = extractBashInput(input);
|
|
74
|
+
if (!bashInput) return null;
|
|
75
|
+
return {
|
|
76
|
+
secondary: bashInput.description,
|
|
77
|
+
secondaryStyle: "italic",
|
|
78
|
+
};
|
|
79
|
+
},
|
|
80
|
+
|
|
81
|
+
getBody: (input): ToolBody | null => {
|
|
82
|
+
const bashInput = extractBashInput(input);
|
|
83
|
+
if (!bashInput) return null;
|
|
84
|
+
|
|
85
|
+
const command = bashInput.command;
|
|
86
|
+
const lines = command.split("\n");
|
|
87
|
+
const isMultiLine = lines.length > 1;
|
|
88
|
+
const MAX_DISPLAY_LENGTH = 120;
|
|
89
|
+
|
|
90
|
+
let displayText: string;
|
|
91
|
+
if (isMultiLine) {
|
|
92
|
+
const firstLine = lines[0]?.trimEnd() ?? "";
|
|
93
|
+
const truncatedFirst =
|
|
94
|
+
firstLine.length > MAX_DISPLAY_LENGTH ? `${firstLine.slice(0, MAX_DISPLAY_LENGTH - 1)}…` : firstLine;
|
|
95
|
+
displayText = `${truncatedFirst} (+${lines.length - 1} more lines)`;
|
|
96
|
+
} else if (command.length > MAX_DISPLAY_LENGTH) {
|
|
97
|
+
displayText = `${command.slice(0, MAX_DISPLAY_LENGTH - 1)}…`;
|
|
98
|
+
} else {
|
|
99
|
+
displayText = command;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return {
|
|
103
|
+
lines: [{ text: displayText }],
|
|
104
|
+
};
|
|
105
|
+
},
|
|
106
|
+
|
|
107
|
+
formatResult: formatBashResult,
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
registerToolLayout("runBash", bashLayout);
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { COLORS } from "../../../ui/constants";
|
|
2
|
+
import { registerToolLayout } from "../registry";
|
|
3
|
+
import type { ToolHeader, ToolLayoutConfig, ToolLayoutRenderProps } from "../types";
|
|
4
|
+
|
|
5
|
+
type UnknownRecord = Record<string, unknown>;
|
|
6
|
+
|
|
7
|
+
function isRecord(value: unknown): value is UnknownRecord {
|
|
8
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
interface GroundingSource {
|
|
12
|
+
url: string;
|
|
13
|
+
quote: string;
|
|
14
|
+
textFragment?: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
interface GroundedStatement {
|
|
18
|
+
id: string;
|
|
19
|
+
statement: string;
|
|
20
|
+
source: GroundingSource;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
interface GroundingInput {
|
|
24
|
+
action: "set" | "append";
|
|
25
|
+
items: GroundedStatement[];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function extractGroundingInput(input: unknown): GroundingInput | null {
|
|
29
|
+
if (!isRecord(input)) return null;
|
|
30
|
+
if (!("items" in input) || !Array.isArray(input.items)) return null;
|
|
31
|
+
|
|
32
|
+
const items = input.items as GroundedStatement[];
|
|
33
|
+
const action =
|
|
34
|
+
"action" in input && (input.action === "set" || input.action === "append")
|
|
35
|
+
? (input.action as "set" | "append")
|
|
36
|
+
: "append";
|
|
37
|
+
|
|
38
|
+
return { action, items };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function GroundingBody({ call }: ToolLayoutRenderProps) {
|
|
42
|
+
const input = extractGroundingInput(call.input);
|
|
43
|
+
if (!input || input.items.length === 0) return null;
|
|
44
|
+
|
|
45
|
+
const MAX_ITEMS = 4;
|
|
46
|
+
const visibleItems = input.items.slice(0, MAX_ITEMS);
|
|
47
|
+
const remainingCount = input.items.length - MAX_ITEMS;
|
|
48
|
+
|
|
49
|
+
return (
|
|
50
|
+
<box flexDirection="column" paddingLeft={2} marginTop={1}>
|
|
51
|
+
{visibleItems.map((item, idx) => {
|
|
52
|
+
let domain = "";
|
|
53
|
+
try {
|
|
54
|
+
const url = new URL(item.source.url);
|
|
55
|
+
domain = url.hostname;
|
|
56
|
+
} catch {
|
|
57
|
+
domain = item.source.url;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const MAX_LEN = 90;
|
|
61
|
+
const statement =
|
|
62
|
+
item.statement.length > MAX_LEN ? item.statement.slice(0, MAX_LEN) + "..." : item.statement;
|
|
63
|
+
|
|
64
|
+
return (
|
|
65
|
+
<box key={idx} flexDirection="column">
|
|
66
|
+
<text>
|
|
67
|
+
<span fg={COLORS.MENU_TEXT}>{statement}</span>
|
|
68
|
+
</text>
|
|
69
|
+
<text>
|
|
70
|
+
<span fg={COLORS.TOOLS}> └─ {domain}</span>
|
|
71
|
+
</text>
|
|
72
|
+
</box>
|
|
73
|
+
);
|
|
74
|
+
})}
|
|
75
|
+
{remainingCount > 0 && (
|
|
76
|
+
<text>
|
|
77
|
+
<span fg={COLORS.TOOLS}> + {remainingCount} more statements</span>
|
|
78
|
+
</text>
|
|
79
|
+
)}
|
|
80
|
+
</box>
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export const groundingLayout: ToolLayoutConfig = {
|
|
85
|
+
abbreviation: "grounding",
|
|
86
|
+
|
|
87
|
+
getHeader: (input): ToolHeader | null => {
|
|
88
|
+
const data = extractGroundingInput(input);
|
|
89
|
+
if (!data) return null;
|
|
90
|
+
return {
|
|
91
|
+
secondary: `${data.action} ${data.items.length} item${data.items.length === 1 ? "" : "s"}`,
|
|
92
|
+
};
|
|
93
|
+
},
|
|
94
|
+
|
|
95
|
+
renderBody: GroundingBody,
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
registerToolLayout("groundingManager", groundingLayout);
|