@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.
Files changed (124) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +126 -0
  3. package/dist/cli.js +22 -0
  4. package/package.json +79 -0
  5. package/src/ai/agent-turn-runner.ts +130 -0
  6. package/src/ai/daemon-ai.ts +403 -0
  7. package/src/ai/exa-client.ts +21 -0
  8. package/src/ai/exa-fetch-cache.ts +104 -0
  9. package/src/ai/model-config.ts +99 -0
  10. package/src/ai/sanitize-messages.ts +83 -0
  11. package/src/ai/system-prompt.ts +363 -0
  12. package/src/ai/tools/fetch-urls.ts +187 -0
  13. package/src/ai/tools/grounding-manager.ts +94 -0
  14. package/src/ai/tools/index.ts +52 -0
  15. package/src/ai/tools/read-file.ts +100 -0
  16. package/src/ai/tools/render-url.ts +275 -0
  17. package/src/ai/tools/run-bash.ts +224 -0
  18. package/src/ai/tools/subagents.ts +195 -0
  19. package/src/ai/tools/todo-manager.ts +150 -0
  20. package/src/ai/tools/web-search.ts +91 -0
  21. package/src/app/App.tsx +711 -0
  22. package/src/app/components/AppOverlays.tsx +131 -0
  23. package/src/app/components/AvatarLayer.tsx +51 -0
  24. package/src/app/components/ConversationPane.tsx +476 -0
  25. package/src/avatar/DaemonAvatarRenderable.ts +343 -0
  26. package/src/avatar/daemon-avatar-rig.ts +1165 -0
  27. package/src/avatar-preview.ts +186 -0
  28. package/src/cli.ts +26 -0
  29. package/src/components/ApiKeyInput.tsx +99 -0
  30. package/src/components/ApiKeyStep.tsx +95 -0
  31. package/src/components/ApprovalPicker.tsx +109 -0
  32. package/src/components/ContentBlockView.tsx +141 -0
  33. package/src/components/DaemonText.tsx +34 -0
  34. package/src/components/DeviceMenu.tsx +166 -0
  35. package/src/components/GroundingBadge.tsx +21 -0
  36. package/src/components/GroundingMenu.tsx +310 -0
  37. package/src/components/HotkeysPane.tsx +115 -0
  38. package/src/components/InlineStatusIndicator.tsx +106 -0
  39. package/src/components/ModelMenu.tsx +411 -0
  40. package/src/components/OnboardingOverlay.tsx +446 -0
  41. package/src/components/ProviderMenu.tsx +177 -0
  42. package/src/components/SessionMenu.tsx +297 -0
  43. package/src/components/SettingsMenu.tsx +291 -0
  44. package/src/components/StatusBar.tsx +126 -0
  45. package/src/components/TokenUsageDisplay.tsx +92 -0
  46. package/src/components/ToolCallView.tsx +113 -0
  47. package/src/components/TypingInputBar.tsx +131 -0
  48. package/src/components/tool-layouts/components.tsx +120 -0
  49. package/src/components/tool-layouts/defaults.ts +9 -0
  50. package/src/components/tool-layouts/index.ts +22 -0
  51. package/src/components/tool-layouts/layouts/bash.ts +110 -0
  52. package/src/components/tool-layouts/layouts/grounding.tsx +98 -0
  53. package/src/components/tool-layouts/layouts/index.ts +8 -0
  54. package/src/components/tool-layouts/layouts/read-file.ts +59 -0
  55. package/src/components/tool-layouts/layouts/subagent.tsx +118 -0
  56. package/src/components/tool-layouts/layouts/system-info.ts +8 -0
  57. package/src/components/tool-layouts/layouts/todo.tsx +139 -0
  58. package/src/components/tool-layouts/layouts/url-tools.ts +220 -0
  59. package/src/components/tool-layouts/layouts/web-search.ts +110 -0
  60. package/src/components/tool-layouts/registry.ts +17 -0
  61. package/src/components/tool-layouts/types.ts +94 -0
  62. package/src/hooks/daemon-event-handlers.ts +944 -0
  63. package/src/hooks/keyboard-handlers.ts +399 -0
  64. package/src/hooks/menu-navigation.ts +147 -0
  65. package/src/hooks/use-app-audio-devices-loader.ts +71 -0
  66. package/src/hooks/use-app-callbacks.ts +202 -0
  67. package/src/hooks/use-app-context-builder.ts +159 -0
  68. package/src/hooks/use-app-display-state.ts +162 -0
  69. package/src/hooks/use-app-menus.ts +51 -0
  70. package/src/hooks/use-app-model-pricing-loader.ts +45 -0
  71. package/src/hooks/use-app-model.ts +123 -0
  72. package/src/hooks/use-app-openrouter-models-loader.ts +44 -0
  73. package/src/hooks/use-app-openrouter-provider-loader.ts +35 -0
  74. package/src/hooks/use-app-preferences-bootstrap.ts +212 -0
  75. package/src/hooks/use-app-sessions.ts +105 -0
  76. package/src/hooks/use-app-settings.ts +62 -0
  77. package/src/hooks/use-conversation-manager.ts +163 -0
  78. package/src/hooks/use-copy-on-select.ts +50 -0
  79. package/src/hooks/use-daemon-events.ts +396 -0
  80. package/src/hooks/use-daemon-keyboard.ts +397 -0
  81. package/src/hooks/use-grounding.ts +46 -0
  82. package/src/hooks/use-input-history.ts +92 -0
  83. package/src/hooks/use-menu-keyboard.ts +93 -0
  84. package/src/hooks/use-playwright-notification.ts +23 -0
  85. package/src/hooks/use-reasoning-animation.ts +97 -0
  86. package/src/hooks/use-response-timer.ts +55 -0
  87. package/src/hooks/use-tool-approval.tsx +202 -0
  88. package/src/hooks/use-typing-mode.ts +137 -0
  89. package/src/hooks/use-voice-dependencies-notification.ts +37 -0
  90. package/src/index.tsx +48 -0
  91. package/src/scripts/setup-browsers.ts +42 -0
  92. package/src/state/app-context.tsx +160 -0
  93. package/src/state/daemon-events.ts +67 -0
  94. package/src/state/daemon-state.ts +493 -0
  95. package/src/state/migrations/001-init.ts +33 -0
  96. package/src/state/migrations/index.ts +8 -0
  97. package/src/state/model-history-store.ts +45 -0
  98. package/src/state/runtime-context.ts +21 -0
  99. package/src/state/session-store.ts +359 -0
  100. package/src/types/index.ts +405 -0
  101. package/src/types/theme.ts +52 -0
  102. package/src/ui/constants.ts +157 -0
  103. package/src/utils/clipboard.ts +89 -0
  104. package/src/utils/debug-logger.ts +69 -0
  105. package/src/utils/formatters.ts +242 -0
  106. package/src/utils/js-rendering.ts +77 -0
  107. package/src/utils/markdown-tables.ts +234 -0
  108. package/src/utils/model-metadata.ts +191 -0
  109. package/src/utils/openrouter-endpoints.ts +212 -0
  110. package/src/utils/openrouter-models.ts +205 -0
  111. package/src/utils/openrouter-pricing.ts +59 -0
  112. package/src/utils/openrouter-reported-cost.ts +16 -0
  113. package/src/utils/paste.ts +33 -0
  114. package/src/utils/preferences.ts +289 -0
  115. package/src/utils/text-fragment.ts +39 -0
  116. package/src/utils/tool-output-preview.ts +250 -0
  117. package/src/utils/voice-dependencies.ts +107 -0
  118. package/src/utils/workspace-manager.ts +85 -0
  119. package/src/voice/audio-recorder.ts +579 -0
  120. package/src/voice/mic-level.ts +35 -0
  121. package/src/voice/tts/openai-tts-stream.ts +222 -0
  122. package/src/voice/tts/speech-controller.ts +64 -0
  123. package/src/voice/tts/tts-player.ts +257 -0
  124. package/src/voice/voice-input-controller.ts +96 -0
@@ -0,0 +1,405 @@
1
+ /**
2
+ * Shared type definitions for the DAEMON application.
3
+ * Consolidates types used across multiple modules.
4
+ */
5
+
6
+ import type { ModelMessage } from "ai";
7
+
8
+ // Re-export AI SDK types for convenience
9
+ export type { ModelMessage } from "ai";
10
+
11
+ /**
12
+ * Tool result output format expected by the AI SDK.
13
+ */
14
+ export type ToolResultOutput =
15
+ | { type: "text"; value: string }
16
+ | { type: "json"; value: Record<string, unknown> | unknown[] | string | number | boolean | null }
17
+ | { type: "error-text"; value: string }
18
+ | { type: "error-json"; value: Record<string, unknown> | unknown[] | string | number | boolean | null };
19
+
20
+ /**
21
+ * DAEMON operational states
22
+ */
23
+ export enum DaemonState {
24
+ IDLE = "idle",
25
+ LISTENING = "listening",
26
+ TRANSCRIBING = "transcribing",
27
+ RESPONDING = "responding",
28
+ SPEAKING = "speaking",
29
+ TYPING = "typing",
30
+ }
31
+
32
+ /**
33
+ * Token usage information from API response
34
+ */
35
+ export interface TokenUsage {
36
+ promptTokens: number;
37
+ completionTokens: number;
38
+ totalTokens: number;
39
+ reasoningTokens?: number;
40
+ cachedInputTokens?: number;
41
+ cost?: number;
42
+ subagentTotalTokens?: number;
43
+ subagentPromptTokens?: number;
44
+ subagentCompletionTokens?: number;
45
+ }
46
+
47
+ /**
48
+ * Status of a tool call or subagent step
49
+ * - "streaming": Tool call detected, name known, but input still streaming
50
+ * - "running": Tool call input complete, execution in progress
51
+ * - "completed": Tool execution finished successfully
52
+ * - "failed": Tool execution failed
53
+ */
54
+ export type ToolCallStatus = "streaming" | "running" | "completed" | "failed" | "awaiting_approval";
55
+
56
+ /**
57
+ * A step/action taken by a subagent, displayed in the UI
58
+ */
59
+ export interface SubagentStep {
60
+ toolName: string;
61
+ status: ToolCallStatus;
62
+ input?: unknown;
63
+ summary?: string;
64
+ }
65
+
66
+ /**
67
+ * Tool call representation for UI rendering
68
+ */
69
+ export interface ToolCall {
70
+ name: string;
71
+ input: unknown;
72
+ toolCallId?: string;
73
+ /** For subagent tool calls - tracks nested tool invocations */
74
+ subagentSteps?: SubagentStep[];
75
+ /** Current status of the tool call */
76
+ status?: ToolCallStatus;
77
+ /** Snapshot of todos at the time of the tool call (for todoManager) */
78
+ todoSnapshot?: Array<{ content: string; status: string }>;
79
+ /** Error message when status is "failed" */
80
+ error?: string;
81
+ /** Result of user approval decision (only set for tools that required approval) */
82
+ approvalResult?: "approved" | "denied";
83
+ }
84
+
85
+ /**
86
+ * Content block types for interleaved UI display.
87
+ * These are derived from ModelMessage content for rendering.
88
+ */
89
+ export type ContentBlock =
90
+ | { type: "reasoning"; content: string; durationMs?: number }
91
+ | { type: "tool"; call: ToolCall; result?: unknown }
92
+ | { type: "text"; content: string };
93
+
94
+ /**
95
+ * UI-specific conversation message that wraps the core AI SDK messages.
96
+ * The `messages` field contains the actual AI SDK messages for API calls.
97
+ * The other fields are for UI display convenience.
98
+ */
99
+ export interface ConversationMessage {
100
+ id: number;
101
+ type: "user" | "daemon";
102
+ content: string;
103
+ messages: ModelMessage[];
104
+ contentBlocks?: ContentBlock[];
105
+ pending?: boolean;
106
+ }
107
+
108
+ /**
109
+ * Persisted session snapshot for reloading UI and model context.
110
+ */
111
+ export interface SessionSnapshot {
112
+ conversationHistory: ConversationMessage[];
113
+ sessionUsage: TokenUsage;
114
+ }
115
+
116
+ /**
117
+ * Session metadata for listing and selection.
118
+ */
119
+ export interface SessionInfo {
120
+ id: string;
121
+ title: string;
122
+ createdAt: string;
123
+ updatedAt: string;
124
+ }
125
+
126
+ /**
127
+ * Color theme for avatar states.
128
+ * Used by both daemon-state.ts and daemon-avatar-rig.ts.
129
+ */
130
+ export interface AvatarColorTheme {
131
+ primary: number;
132
+ glow: number;
133
+ eye: number;
134
+ }
135
+
136
+ /**
137
+ * Alias for avatar color theme used by the rig.
138
+ * @deprecated Use AvatarColorTheme directly
139
+ */
140
+ export type DaemonColorTheme = AvatarColorTheme;
141
+
142
+ /**
143
+ * Transcription result from voice input
144
+ */
145
+ export interface TranscriptionResult {
146
+ text: string;
147
+ }
148
+
149
+ /**
150
+ * Callbacks for streaming AI responses
151
+ */
152
+ export interface StreamCallbacks {
153
+ onToken?: (token: string) => void;
154
+ onReasoningToken?: (token: string) => void;
155
+ onToolCallStart?: (toolName: string, toolCallId: string) => void;
156
+ onToolCall?: (toolName: string, args: unknown, toolCallId?: string) => void;
157
+ onToolResult?: (toolName: string, result: unknown, toolCallId?: string) => void;
158
+ onToolApprovalRequest?: (request: ToolApprovalRequest) => void;
159
+ onAwaitingApprovals?: (
160
+ pendingApprovals: ToolApprovalRequest[],
161
+ respondToApprovals: (responses: ToolApprovalResponse[]) => void
162
+ ) => void;
163
+ onSubagentToolCall?: (toolCallId: string, toolName: string, input?: unknown) => void;
164
+ onSubagentUsage?: (usage: TokenUsage) => void;
165
+ onSubagentToolResult?: (toolCallId: string, toolName: string, success: boolean) => void;
166
+ onSubagentComplete?: (toolCallId: string, success: boolean) => void;
167
+ onStepUsage?: (usage: TokenUsage) => void;
168
+ onComplete?: (
169
+ fullText: string,
170
+ responseMessages: ModelMessage[],
171
+ usage?: TokenUsage,
172
+ /** The final assistant text only (for TTS - excludes intermediate text from tool-call steps) */
173
+ finalText?: string
174
+ ) => void;
175
+ onError?: (error: Error) => void;
176
+ }
177
+
178
+ /**
179
+ * Audio device information
180
+ */
181
+ export interface AudioDevice {
182
+ name: string;
183
+ isDefault?: boolean;
184
+ }
185
+
186
+ /**
187
+ * Interaction mode for DAEMON responses.
188
+ * - "text": Terminal output with markdown formatting
189
+ * - "voice": Speech-to-speech, natural conversational responses
190
+ */
191
+ export type InteractionMode = "text" | "voice";
192
+
193
+ /**
194
+ * Speech speed settings (1.0x to 2.0x)
195
+ */
196
+ export type SpeechSpeed = 1.0 | 1.25 | 1.5 | 1.75 | 2.0;
197
+
198
+ /**
199
+ * Reasoning effort / cognitive depth settings.
200
+ * Controls how deeply the model reasons about responses.
201
+ * - "low": SURFACE - minimal reasoning
202
+ * - "medium": DEEP - moderate reasoning (default)
203
+ * - "high": ABYSSAL - maximum reasoning depth
204
+ */
205
+ export type ReasoningEffort = "low" | "medium" | "high";
206
+
207
+ /** Display labels for reasoning effort levels */
208
+ export const REASONING_EFFORT_LABELS: Record<ReasoningEffort, string> = {
209
+ low: "LOW",
210
+ medium: "MEDIUM",
211
+ high: "HIGH",
212
+ };
213
+
214
+ /** Ordered list of reasoning effort levels for cycling */
215
+ export const REASONING_EFFORT_LEVELS: ReasoningEffort[] = ["low", "medium", "high"];
216
+
217
+ /**
218
+ * Bash tool approval level settings.
219
+ * Controls when user approval is required for bash commands.
220
+ * - "none": No approval required for any bash commands
221
+ * - "dangerous": Approval required only for potentially dangerous commands
222
+ * - "all": Approval required for all bash commands
223
+ */
224
+ export type BashApprovalLevel = "none" | "dangerous" | "all";
225
+
226
+ /** Display labels for bash approval levels */
227
+ export const BASH_APPROVAL_LABELS: Record<BashApprovalLevel, string> = {
228
+ none: "NONE",
229
+ dangerous: "DANGEROUS",
230
+ all: "ALL",
231
+ };
232
+
233
+ /** Ordered list of bash approval levels for cycling */
234
+ export const BASH_APPROVAL_LEVELS: BashApprovalLevel[] = ["none", "dangerous", "all"];
235
+
236
+ /**
237
+ * Onboarding flow steps.
238
+ * - intro: Welcome screen
239
+ * - openrouter_key: OpenRouter API key input (for AI models)
240
+ * - openai_key: OpenAI API key input (for transcription)
241
+ * - exa_key: Exa API key input (for web search)
242
+ * - device: Audio device selection
243
+ * - model: AI model selection
244
+ * - complete: Onboarding finished
245
+ */
246
+ export type OnboardingStep =
247
+ | "intro"
248
+ | "openrouter_key"
249
+ | "openai_key"
250
+ | "exa_key"
251
+ | "device"
252
+ | "model"
253
+ | "settings"
254
+ | "complete";
255
+
256
+ export type VoiceInteractionType = "direct" | "review";
257
+
258
+ /**
259
+ * Persisted user preferences.
260
+ */
261
+ export interface AppPreferences {
262
+ version: number;
263
+ createdAt: string;
264
+ updatedAt: string;
265
+ onboardingCompleted: boolean;
266
+ audioDeviceName?: string;
267
+ audioOutputDeviceName?: string;
268
+ modelId?: string;
269
+ /**
270
+ * OpenRouter inference provider slug (aka `provider` routing tag), e.g. "openai".
271
+ * When unset, OpenRouter will automatically route to the best available provider.
272
+ */
273
+ openRouterProviderTag?: string;
274
+ interactionMode?: InteractionMode;
275
+ voiceInteractionType?: VoiceInteractionType;
276
+ speechSpeed?: SpeechSpeed;
277
+ reasoningEffort?: ReasoningEffort;
278
+ /** OpenRouter API key for AI model responses */
279
+ openRouterApiKey?: string;
280
+ /** OpenAI API key for voice transcription */
281
+ openAiApiKey?: string;
282
+ /** Exa API key for web search */
283
+ exaApiKey?: string;
284
+ /** Show full reasoning blocks instead of compact ticker */
285
+ showFullReasoning?: boolean;
286
+ /** Show tool output previews */
287
+ showToolOutput?: boolean;
288
+ /** Bash command approval level */
289
+ bashApprovalLevel?: BashApprovalLevel;
290
+ /** Recent user inputs for up/down history navigation (max 20) */
291
+ inputHistory?: string[];
292
+ }
293
+
294
+ /**
295
+ * Model pricing information (per 1M tokens)
296
+ */
297
+ export interface ModelPricing {
298
+ prompt: number;
299
+ completion: number;
300
+ /**
301
+ * Discounted price for cache reads (per 1M input tokens).
302
+ * Present only for some providers/models on OpenRouter.
303
+ */
304
+ inputCacheRead?: number;
305
+ /**
306
+ * Price for cache writes (per 1M input tokens).
307
+ * Present only for some providers/models on OpenRouter.
308
+ */
309
+ inputCacheWrite?: number;
310
+ }
311
+
312
+ /**
313
+ * Model option for selection menus
314
+ */
315
+ export interface ModelOption {
316
+ id: string;
317
+ name: string;
318
+ pricing?: ModelPricing;
319
+ contextLength?: number;
320
+ supportsCaching?: boolean;
321
+ }
322
+
323
+ /**
324
+ * Todo item status
325
+ */
326
+ export type TodoStatus = "pending" | "in_progress" | "completed" | "cancelled";
327
+
328
+ /**
329
+ * Tool approval request from the AI SDK.
330
+ * Emitted when a tool with needsApproval: true is called.
331
+ */
332
+ export interface ToolApprovalRequest {
333
+ approvalId: string;
334
+ toolName: string;
335
+ toolCallId: string;
336
+ input: unknown;
337
+ }
338
+
339
+ /**
340
+ * Tool approval response to send back to the agent.
341
+ */
342
+ export interface ToolApprovalResponse {
343
+ approvalId: string;
344
+ approved: boolean;
345
+ reason?: string;
346
+ }
347
+
348
+ /**
349
+ * Todo item for task tracking
350
+ */
351
+ export interface TodoItem {
352
+ content: string;
353
+ status: TodoStatus;
354
+ }
355
+
356
+ /**
357
+ * Event emitter interface for subagent progress updates.
358
+ * Used to connect subagent tool execution to the UI.
359
+ */
360
+ export interface SubagentProgressEmitter {
361
+ onSubagentToolCall: (toolCallId: string, toolName: string, input?: unknown) => void;
362
+ onSubagentUsage: (usage: {
363
+ promptTokens: number;
364
+ completionTokens: number;
365
+ totalTokens: number;
366
+ reasoningTokens?: number;
367
+ cachedInputTokens?: number;
368
+ cost?: number;
369
+ }) => void;
370
+ onSubagentToolResult: (toolCallId: string, toolName: string, success: boolean) => void;
371
+ onSubagentComplete: (toolCallId: string, success: boolean) => void;
372
+ }
373
+
374
+ /**
375
+ * Grounding: Source information for a grounded statement.
376
+ */
377
+ export interface GroundingSource {
378
+ url: string;
379
+ title?: string;
380
+ /** Short excerpt copied from fetched content (human-readable). */
381
+ quote: string;
382
+ /** Text fragment for deep-linking (short verbatim phrase from source). */
383
+ textFragment: string;
384
+ }
385
+
386
+ /**
387
+ * Grounding: A single grounded statement with its source.
388
+ */
389
+ export interface GroundedStatement {
390
+ id: string;
391
+ statement: string;
392
+ source: GroundingSource;
393
+ }
394
+
395
+ /**
396
+ * Grounding: A map of grounded statements for a single response.
397
+ */
398
+ export interface GroundingMap {
399
+ id: string;
400
+ sessionId: string;
401
+ /** The DAEMON response message this map belongs to (or turn id). */
402
+ messageId: number;
403
+ createdAt: string;
404
+ items: GroundedStatement[];
405
+ }
@@ -0,0 +1,52 @@
1
+ /**
2
+ * Avatar and UI color themes for different daemon states.
3
+ */
4
+
5
+ import { DaemonState } from "./index";
6
+ import type { AvatarColorTheme } from "./index";
7
+
8
+ /**
9
+ * Avatar color themes for each daemon state
10
+ */
11
+ export const STATE_COLORS: Record<DaemonState, AvatarColorTheme> = {
12
+ [DaemonState.IDLE]: {
13
+ primary: 0x9ca3af, // Gray
14
+ glow: 0x67e8f9, // Cyan glow
15
+ eye: 0xff0000, // Red
16
+ },
17
+ [DaemonState.LISTENING]: {
18
+ primary: 0x22d3ee, // Bright cyan
19
+ glow: 0x67e8f9,
20
+ eye: 0x22d3ee,
21
+ },
22
+ [DaemonState.TRANSCRIBING]: {
23
+ primary: 0xa78bfa, // Purple
24
+ glow: 0xc4b5fd,
25
+ eye: 0xa78bfa,
26
+ },
27
+ [DaemonState.RESPONDING]: {
28
+ primary: 0x4ade80, // Green
29
+ glow: 0x86efac,
30
+ eye: 0x4ade80,
31
+ },
32
+ [DaemonState.SPEAKING]: {
33
+ primary: 0x22d3ee, // Cyan - voice output
34
+ glow: 0x67e8f9,
35
+ eye: 0x22d3ee,
36
+ },
37
+ [DaemonState.TYPING]: {
38
+ primary: 0x9ca3af,
39
+ glow: 0x67e8f9,
40
+ eye: 0xff0000,
41
+ },
42
+ };
43
+
44
+ /**
45
+ * Special color theme for reasoning phase (before speaking).
46
+ * Soft blue-gray - contemplative, calm, less aggressive.
47
+ */
48
+ export const REASONING_COLORS: AvatarColorTheme = {
49
+ primary: 0x64748b, // Slate gray-blue
50
+ glow: 0x94a3b8, // Lighter slate
51
+ eye: 0x94a3b8, // Soft glow
52
+ };
@@ -0,0 +1,157 @@
1
+ /**
2
+ * UI constants including colors, status text, and markdown syntax styles.
3
+ */
4
+
5
+ import { SyntaxStyle, RGBA } from "@opentui/core";
6
+ import { DaemonState } from "../types";
7
+
8
+ // Status text displayed for each daemon state
9
+ export const STATUS_TEXT: Record<DaemonState, string> = {
10
+ [DaemonState.IDLE]: "SPACE speak · SHIFT+TAB type · M models · S settings · L sessions · ? hotkeys",
11
+ [DaemonState.LISTENING]: "LISTENING · SPACE stop · ESC cancel",
12
+ [DaemonState.TRANSCRIBING]: "PROCESSING VOICE... · ESC cancel",
13
+ [DaemonState.RESPONDING]: "DAEMON SPEAKS... · ESC cancel · T reasoning",
14
+ [DaemonState.SPEAKING]: "DAEMON SPEAKS... · ESC stop",
15
+ [DaemonState.TYPING]: "TYPE MODE · ENTER submit · ESC cancel",
16
+ };
17
+
18
+ // Hex colors for status bar text per state
19
+ export const STATE_COLOR_HEX: Record<DaemonState, string> = {
20
+ [DaemonState.IDLE]: "#9ca3af",
21
+ [DaemonState.LISTENING]: "#22d3ee",
22
+ [DaemonState.TRANSCRIBING]: "#a78bfa",
23
+ [DaemonState.RESPONDING]: "#4ade80",
24
+ [DaemonState.SPEAKING]: "#22d3ee",
25
+ [DaemonState.TYPING]: "#fbbf24",
26
+ };
27
+
28
+ // Animation settings for reasoning text ticker
29
+ export const REASONING_ANIMATION = {
30
+ LINE_WIDTH: 120,
31
+ CHARS_PER_TICK: 6,
32
+ TICK_INTERVAL_MS: 16,
33
+ INTENSITY: 0.5,
34
+ } as const;
35
+
36
+ // UI colors
37
+ export const COLORS = {
38
+ BACKGROUND: "#050509",
39
+ LISTENING_DIM: "#050509A8",
40
+ ERROR: "#ef4444",
41
+ DAEMON_ERROR: "#ef4444",
42
+ REASONING: "#a78bfa",
43
+ REASONING_DIM: "#525252",
44
+ TOOLS: "#3f4651",
45
+ TOOL_INPUT_BG: "#0a0a0f60",
46
+ TOOL_INPUT_BORDER: "#3f465180",
47
+ TOOL_INPUT_TEXT: "#9ca3af",
48
+ USER_LABEL: "#bfdbfe",
49
+ USER_TEXT: "#ffffff",
50
+ USER_BG: "#1e293b",
51
+ DAEMON_LABEL: "#22d3ee",
52
+ DAEMON_TEXT: "#4ade80",
53
+ TYPING_PROMPT: "#fbbf24",
54
+ EMPTY_STATE: "#374151",
55
+ MENU_BORDER: "#22d3ee",
56
+ MENU_BG: "#0a0a0f",
57
+ MENU_SELECTED_BG: "#1a1a2e",
58
+ MENU_TEXT: "#9ca3af",
59
+ TOKEN_USAGE: "#6b7280",
60
+ TOKEN_USAGE_LABEL: "#525252",
61
+ STATUS_BORDER: "#3f4651",
62
+ WORKING_SPINNER_BORDER: "#1e293b",
63
+
64
+ // General status colors (used across tool views, todos, subagent steps)
65
+ STATUS_RUNNING: "#fbbf24",
66
+ STATUS_COMPLETED: "#4ade80",
67
+ STATUS_FAILED: "#ef4444",
68
+ STATUS_PENDING: "#9ca3af",
69
+ STATUS_DONE_DIM: "#2d333d",
70
+ STATUS_APPROVAL: "#f472b6",
71
+ } as const;
72
+
73
+ // Markdown syntax style for daemon responses - alien/cultic aesthetic
74
+ export const DAEMON_MARKDOWN_STYLE = SyntaxStyle.fromStyles({
75
+ // Default text inherits daemon green
76
+ default: { fg: RGBA.fromHex(COLORS.DAEMON_TEXT) },
77
+ // Headings - cyan hierarchy (daemon label color)
78
+ "markup.heading.1": { fg: RGBA.fromHex("#22d3ee"), bold: true },
79
+ "markup.heading.2": { fg: RGBA.fromHex("#06b6d4"), bold: true },
80
+ "markup.heading.3": { fg: RGBA.fromHex("#0891b2"), bold: true },
81
+ "markup.heading.4": { fg: RGBA.fromHex("#0e7490") },
82
+ "markup.heading.5": { fg: RGBA.fromHex("#155e75") },
83
+ "markup.heading.6": { fg: RGBA.fromHex("#164e63") },
84
+ // Emphasis
85
+ "markup.strong": { fg: RGBA.fromHex("#ffffff"), bold: true },
86
+ "markup.italic": { fg: RGBA.fromHex("#a78bfa"), italic: true },
87
+ "markup.strikethrough": { fg: RGBA.fromHex("#6b7280"), dim: true },
88
+ // Code - purple/violet (reasoning color)
89
+ "markup.raw": { fg: RGBA.fromHex("#c4b5fd") },
90
+ "markup.raw.block": { fg: RGBA.fromHex("#c4b5fd") },
91
+ // Links - cyan like daemon label
92
+ "markup.link": { fg: RGBA.fromHex("#22d3ee") },
93
+ "markup.link.url": { fg: RGBA.fromHex("#67e8f9"), underline: true },
94
+ "markup.link.label": { fg: RGBA.fromHex("#22d3ee") },
95
+ // Lists
96
+ "markup.list": { fg: RGBA.fromHex("#9ca3af") },
97
+ "markup.list.checked": { fg: RGBA.fromHex("#4ade80") },
98
+ "markup.list.unchecked": { fg: RGBA.fromHex("#6b7280") },
99
+ // Quotes - dimmed
100
+ "markup.quote": { fg: RGBA.fromHex("#9ca3af"), italic: true },
101
+ // Code block syntax highlighting
102
+ keyword: { fg: RGBA.fromHex("#f472b6") },
103
+ string: { fg: RGBA.fromHex("#a5d6ff") },
104
+ comment: { fg: RGBA.fromHex("#6b7280"), italic: true },
105
+ number: { fg: RGBA.fromHex("#fbbf24") },
106
+ function: { fg: RGBA.fromHex("#7dd3fc") },
107
+ type: { fg: RGBA.fromHex("#c4b5fd") },
108
+ variable: { fg: RGBA.fromHex("#4ade80") },
109
+ constant: { fg: RGBA.fromHex("#fbbf24") },
110
+ operator: { fg: RGBA.fromHex("#f472b6") },
111
+ punctuation: { fg: RGBA.fromHex("#9ca3af") },
112
+ "punctuation.special": { fg: RGBA.fromHex("#6b7280") },
113
+ label: { fg: RGBA.fromHex("#67e8f9") },
114
+ });
115
+
116
+ // Markdown syntax style for reasoning - dimmed/gray aesthetic
117
+ export const REASONING_MARKDOWN_STYLE = SyntaxStyle.fromStyles({
118
+ // Default text inherits reasoning dim color
119
+ default: { fg: RGBA.fromHex(COLORS.REASONING_DIM) },
120
+ // Headings - slightly brighter
121
+ "markup.heading.1": { fg: RGBA.fromHex("#a3a3a3"), bold: true },
122
+ "markup.heading.2": { fg: RGBA.fromHex("#a3a3a3"), bold: true },
123
+ "markup.heading.3": { fg: RGBA.fromHex("#a3a3a3"), bold: true },
124
+ "markup.heading.4": { fg: RGBA.fromHex("#a3a3a3") },
125
+ "markup.heading.5": { fg: RGBA.fromHex("#a3a3a3") },
126
+ "markup.heading.6": { fg: RGBA.fromHex("#a3a3a3") },
127
+ // Emphasis
128
+ "markup.strong": { fg: RGBA.fromHex("#d4d4d4"), bold: true },
129
+ "markup.italic": { fg: RGBA.fromHex("#a78bfa"), italic: true }, // Keep purple for italic
130
+ "markup.strikethrough": { fg: RGBA.fromHex("#404040"), dim: true },
131
+ // Code - keep purple theme but distinct
132
+ "markup.raw": { fg: RGBA.fromHex("#8b5cf6") }, // Violet-500
133
+ "markup.raw.block": { fg: RGBA.fromHex("#8b5cf6") },
134
+ // Links
135
+ "markup.link": { fg: RGBA.fromHex("#0891b2") }, // Cyan-600
136
+ "markup.link.url": { fg: RGBA.fromHex("#0891b2"), underline: true },
137
+ "markup.link.label": { fg: RGBA.fromHex("#0891b2") },
138
+ // Lists
139
+ "markup.list": { fg: RGBA.fromHex("#525252") },
140
+ "markup.list.checked": { fg: RGBA.fromHex("#a3a3a3") },
141
+ "markup.list.unchecked": { fg: RGBA.fromHex("#525252") },
142
+ // Quotes
143
+ "markup.quote": { fg: RGBA.fromHex("#525252"), italic: true },
144
+ // Code block syntax highlighting - dimmed versions
145
+ keyword: { fg: RGBA.fromHex("#db2777") }, // Pink-600
146
+ string: { fg: RGBA.fromHex("#60a5fa") }, // Blue-400
147
+ comment: { fg: RGBA.fromHex("#404040"), italic: true },
148
+ number: { fg: RGBA.fromHex("#d97706") }, // Amber-600
149
+ function: { fg: RGBA.fromHex("#0ea5e9") }, // Sky-500
150
+ type: { fg: RGBA.fromHex("#8b5cf6") }, // Violet-500
151
+ variable: { fg: RGBA.fromHex("#22c55e") }, // Green-500
152
+ constant: { fg: RGBA.fromHex("#d97706") },
153
+ operator: { fg: RGBA.fromHex("#db2777") },
154
+ punctuation: { fg: RGBA.fromHex("#525252") },
155
+ "punctuation.special": { fg: RGBA.fromHex("#525252") },
156
+ label: { fg: RGBA.fromHex("#0891b2") },
157
+ });
@@ -0,0 +1,89 @@
1
+ import { platform, release } from "os";
2
+
3
+ async function readFromCommand(command: string, args: string[]): Promise<string> {
4
+ try {
5
+ const proc = Bun.spawn([command, ...args], {
6
+ stdout: "pipe",
7
+ stderr: "ignore",
8
+ });
9
+ const text = await new Response(proc.stdout).text();
10
+ await proc.exited;
11
+ return text;
12
+ } catch {
13
+ return "";
14
+ }
15
+ }
16
+
17
+ async function writeToCommand(command: string, args: string[], text: string): Promise<boolean> {
18
+ try {
19
+ const proc = Bun.spawn([command, ...args], {
20
+ stdin: "pipe",
21
+ stdout: "ignore",
22
+ stderr: "ignore",
23
+ });
24
+ proc.stdin.write(text);
25
+ proc.stdin.end();
26
+ const exitCode = await proc.exited;
27
+ return exitCode === 0;
28
+ } catch {
29
+ return false;
30
+ }
31
+ }
32
+
33
+ export async function readClipboardText(): Promise<string> {
34
+ const os = platform();
35
+
36
+ if (os === "darwin" && Bun.which("pbpaste")) {
37
+ return readFromCommand("pbpaste", []);
38
+ }
39
+
40
+ if (os === "win32" || release().includes("WSL")) {
41
+ return readFromCommand("powershell", ["-command", "Get-Clipboard -Raw"]);
42
+ }
43
+
44
+ if (os === "linux") {
45
+ if (process.env.WAYLAND_DISPLAY && Bun.which("wl-paste")) {
46
+ return readFromCommand("wl-paste", ["-n"]);
47
+ }
48
+ if (Bun.which("xclip")) {
49
+ return readFromCommand("xclip", ["-selection", "clipboard", "-o"]);
50
+ }
51
+ if (Bun.which("xsel")) {
52
+ return readFromCommand("xsel", ["--clipboard", "--output"]);
53
+ }
54
+ }
55
+
56
+ return "";
57
+ }
58
+
59
+ export async function writeClipboardText(text: string): Promise<boolean> {
60
+ if (!text) return false;
61
+
62
+ const os = platform();
63
+
64
+ if (os === "darwin" && Bun.which("pbcopy")) {
65
+ return writeToCommand("pbcopy", [], text);
66
+ }
67
+
68
+ if (os === "win32" || release().includes("WSL")) {
69
+ return writeToCommand(
70
+ "powershell",
71
+ ["-command", "Set-Clipboard -Value ([Console]::In.ReadToEnd())"],
72
+ text
73
+ );
74
+ }
75
+
76
+ if (os === "linux") {
77
+ if (process.env.WAYLAND_DISPLAY && Bun.which("wl-copy")) {
78
+ return writeToCommand("wl-copy", [], text);
79
+ }
80
+ if (Bun.which("xclip")) {
81
+ return writeToCommand("xclip", ["-selection", "clipboard"], text);
82
+ }
83
+ if (Bun.which("xsel")) {
84
+ return writeToCommand("xsel", ["--clipboard", "--input"], text);
85
+ }
86
+ }
87
+
88
+ return false;
89
+ }