@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,131 @@
1
+ import { memo } from "react";
2
+ import { DeviceMenu } from "../../components/DeviceMenu";
3
+ import { GroundingMenu } from "../../components/GroundingMenu";
4
+ import { HotkeysPane } from "../../components/HotkeysPane";
5
+ import { ModelMenu } from "../../components/ModelMenu";
6
+ import { OnboardingOverlay } from "../../components/OnboardingOverlay";
7
+ import { ProviderMenu } from "../../components/ProviderMenu";
8
+ import { SessionMenu } from "../../components/SessionMenu";
9
+ import { SettingsMenu } from "../../components/SettingsMenu";
10
+ import { useAppContext } from "../../state/app-context";
11
+
12
+ function AppOverlaysImpl() {
13
+ const ctx = useAppContext();
14
+ const { menus, device, settings, model, session, grounding, onboarding } = ctx;
15
+ const {
16
+ deviceCallbacks,
17
+ settingsCallbacks,
18
+ modelCallbacks,
19
+ sessionCallbacks,
20
+ groundingCallbacks,
21
+ onboardingCallbacks,
22
+ } = ctx;
23
+
24
+ return (
25
+ <>
26
+ {menus.showDeviceMenu && (
27
+ <DeviceMenu
28
+ devices={device.devices}
29
+ currentDevice={device.currentDevice}
30
+ currentOutputDevice={device.currentOutputDevice}
31
+ soxAvailable={device.soxAvailable}
32
+ soxInstallHint={device.soxInstallHint}
33
+ onClose={() => menus.setShowDeviceMenu(false)}
34
+ onSelect={deviceCallbacks.onDeviceSelect}
35
+ onOutputSelect={deviceCallbacks.onOutputDeviceSelect}
36
+ />
37
+ )}
38
+
39
+ {menus.showSettingsMenu && (
40
+ <SettingsMenu
41
+ interactionMode={settings.interactionMode}
42
+ voiceInteractionType={settings.voiceInteractionType}
43
+ speechSpeed={settings.speechSpeed}
44
+ reasoningEffort={settings.reasoningEffort}
45
+ bashApprovalLevel={settings.bashApprovalLevel}
46
+ supportsReasoning={settings.supportsReasoning}
47
+ canEnableVoiceOutput={settings.canEnableVoiceOutput}
48
+ showFullReasoning={settings.showFullReasoning}
49
+ showToolOutput={settings.showToolOutput}
50
+ onClose={() => menus.setShowSettingsMenu(false)}
51
+ toggleInteractionMode={settingsCallbacks.onToggleInteractionMode}
52
+ setVoiceInteractionType={settingsCallbacks.onSetVoiceInteractionType}
53
+ setSpeechSpeed={settingsCallbacks.onSetSpeechSpeed}
54
+ setReasoningEffort={settingsCallbacks.onSetReasoningEffort}
55
+ setBashApprovalLevel={settingsCallbacks.onSetBashApprovalLevel}
56
+ setShowFullReasoning={settings.setShowFullReasoning}
57
+ setShowToolOutput={settings.setShowToolOutput}
58
+ persistPreferences={settings.persistPreferences}
59
+ />
60
+ )}
61
+
62
+ {menus.showModelMenu && (
63
+ <ModelMenu
64
+ curatedModels={model.curatedModels}
65
+ allModels={model.openRouterModels}
66
+ allModelsLoading={model.openRouterModelsLoading}
67
+ allModelsUpdatedAt={model.openRouterModelsUpdatedAt}
68
+ currentModelId={model.currentModelId}
69
+ onClose={() => menus.setShowModelMenu(false)}
70
+ onSelect={modelCallbacks.onModelSelect}
71
+ onRefreshAllModels={modelCallbacks.onModelRefresh}
72
+ />
73
+ )}
74
+
75
+ {menus.showProviderMenu && (
76
+ <ProviderMenu
77
+ items={model.providerMenuItems}
78
+ currentProviderTag={model.currentOpenRouterProviderTag}
79
+ modelId={model.currentModelId}
80
+ onClose={() => menus.setShowProviderMenu(false)}
81
+ onSelect={modelCallbacks.onProviderSelect}
82
+ />
83
+ )}
84
+
85
+ {menus.showSessionMenu && (
86
+ <SessionMenu
87
+ items={session.sessionMenuItems}
88
+ currentSessionId={session.currentSessionId}
89
+ onClose={() => menus.setShowSessionMenu(false)}
90
+ onSelect={sessionCallbacks.onSessionSelect}
91
+ onDelete={sessionCallbacks.onSessionDelete}
92
+ />
93
+ )}
94
+
95
+ {menus.showHotkeysPane && <HotkeysPane onClose={() => menus.setShowHotkeysPane(false)} />}
96
+
97
+ {menus.showGroundingMenu && grounding.latestGroundingMap && (
98
+ <GroundingMenu
99
+ groundingMap={grounding.latestGroundingMap}
100
+ initialIndex={grounding.groundingInitialIndex}
101
+ onClose={() => menus.setShowGroundingMenu(false)}
102
+ onSelect={groundingCallbacks.onGroundingSelect}
103
+ onSelectedIndexChange={groundingCallbacks.onGroundingIndexChange}
104
+ />
105
+ )}
106
+
107
+ {onboarding.onboardingActive && (
108
+ <OnboardingOverlay
109
+ step={onboarding.onboardingStep}
110
+ preferences={onboarding.onboardingPreferences}
111
+ devices={device.devices}
112
+ currentDevice={device.currentDevice}
113
+ currentOutputDevice={device.currentOutputDevice}
114
+ models={model.curatedModels}
115
+ currentModelId={model.currentModelId}
116
+ deviceLoadTimedOut={device.deviceLoadTimedOut}
117
+ setCurrentDevice={device.setCurrentDevice}
118
+ setCurrentOutputDevice={device.setCurrentOutputDevice}
119
+ setCurrentModelId={model.setCurrentModelId}
120
+ setOnboardingStep={onboarding.setOnboardingStep}
121
+ completeOnboarding={onboardingCallbacks.completeOnboarding}
122
+ persistPreferences={settings.persistPreferences}
123
+ onKeySubmit={onboardingCallbacks.onKeySubmit}
124
+ apiKeyTextareaRef={onboarding.apiKeyTextareaRef}
125
+ />
126
+ )}
127
+ </>
128
+ );
129
+ }
130
+
131
+ export const AppOverlays = memo(AppOverlaysImpl);
@@ -0,0 +1,51 @@
1
+ import { memo, useCallback } from "react";
2
+ import type { RefObject } from "react";
3
+ import type { DaemonAvatarRenderable } from "../../avatar/DaemonAvatarRenderable";
4
+ import { DaemonState } from "../../types";
5
+
6
+ export interface AvatarLayerProps {
7
+ avatarRef: RefObject<DaemonAvatarRenderable | null>;
8
+ daemonState: DaemonState;
9
+ applyAvatarForState: (state: DaemonState) => void;
10
+ width: number;
11
+ height: number;
12
+ zIndex?: number;
13
+ }
14
+
15
+ function AvatarLayerImpl(props: AvatarLayerProps) {
16
+ const { avatarRef, daemonState, applyAvatarForState, width, height, zIndex = 0 } = props;
17
+
18
+ const handleAvatarRef = useCallback(
19
+ (ref: DaemonAvatarRenderable | null) => {
20
+ avatarRef.current = ref;
21
+ if (ref) {
22
+ applyAvatarForState(daemonState);
23
+ }
24
+ },
25
+ [avatarRef, applyAvatarForState, daemonState]
26
+ );
27
+
28
+ return (
29
+ <box
30
+ position="absolute"
31
+ top={0}
32
+ left={0}
33
+ width="100%"
34
+ height="100%"
35
+ alignItems="center"
36
+ justifyContent="center"
37
+ zIndex={zIndex}
38
+ >
39
+ <daemon-avatar
40
+ id="daemon-avatar"
41
+ live
42
+ width={width}
43
+ height={height}
44
+ respectAlpha={true}
45
+ ref={handleAvatarRef}
46
+ />
47
+ </box>
48
+ );
49
+ }
50
+
51
+ export const AvatarLayer = memo(AvatarLayerImpl);
@@ -0,0 +1,476 @@
1
+ import type { ScrollBoxRenderable, TextareaRenderable } from "@opentui/core";
2
+ import { memo } from "react";
3
+ import type { MutableRefObject } from "react";
4
+ import {
5
+ ContentBlockView,
6
+ isLastReasoningBlockInList,
7
+ isLastTextBlockInList,
8
+ shouldHideContentBlock,
9
+ } from "../../components/ContentBlockView";
10
+ import { DaemonText } from "../../components/DaemonText";
11
+ import { GroundingBadge } from "../../components/GroundingBadge";
12
+ import { InlineStatusIndicator } from "../../components/InlineStatusIndicator";
13
+ import { StatusBar } from "../../components/StatusBar";
14
+ import { TokenUsageDisplay } from "../../components/TokenUsageDisplay";
15
+ import { TypingInputBar } from "../../components/TypingInputBar";
16
+ import type { ContentBlock, ConversationMessage, TokenUsage } from "../../types";
17
+ import { DaemonState } from "../../types";
18
+ import { COLORS, REASONING_MARKDOWN_STYLE } from "../../ui/constants";
19
+ import type { ModelMetadata } from "../../utils/model-metadata";
20
+
21
+ export interface ConversationDisplayState {
22
+ conversationHistory: ConversationMessage[];
23
+ currentTranscription: string;
24
+ currentResponse: string;
25
+ currentContentBlocks: ContentBlock[];
26
+ }
27
+
28
+ export interface StatusDisplayState {
29
+ daemonState: DaemonState;
30
+ statusText: string;
31
+ statusColor: string;
32
+ apiKeyMissingError: string;
33
+ error: string;
34
+ resetNotification: string;
35
+ escPendingCancel: boolean;
36
+ }
37
+
38
+ export interface ReasoningDisplayState {
39
+ showFullReasoning: boolean;
40
+ showToolOutput: boolean;
41
+ reasoningQueue: string;
42
+ reasoningDisplay: string;
43
+ fullReasoning: string;
44
+ }
45
+
46
+ export interface ProgressDisplayState {
47
+ showWorkingSpinner: boolean;
48
+ workingSpinnerLabel: string;
49
+ isToolCalling: boolean;
50
+ responseElapsedMs: number;
51
+ }
52
+
53
+ export interface TypingInputState {
54
+ typingTextareaRef: MutableRefObject<TextareaRenderable | null>;
55
+ conversationScrollRef: MutableRefObject<ScrollBoxRenderable | null>;
56
+ onTypingContentChange: (value: string) => void;
57
+ onTypingSubmit: () => void;
58
+ onHistoryUp?: () => void;
59
+ onHistoryDown?: () => void;
60
+ }
61
+
62
+ export interface ConversationPaneProps {
63
+ conversation: ConversationDisplayState;
64
+ status: StatusDisplayState;
65
+ reasoning: ReasoningDisplayState;
66
+ progress: ProgressDisplayState;
67
+ typing: TypingInputState;
68
+ sessionUsage: TokenUsage;
69
+ modelMetadata: ModelMetadata | null;
70
+ hasInteracted: boolean;
71
+ suppressStatusBar?: boolean;
72
+ frostColor: string;
73
+ initialStatusTop: number | "auto" | `${number}%`;
74
+ hasGrounding?: boolean;
75
+ groundingCount?: number;
76
+ modelName?: string;
77
+ sessionTitle?: string;
78
+ isVoiceOutputEnabled?: boolean;
79
+ }
80
+
81
+ function ConversationPaneImpl(props: ConversationPaneProps) {
82
+ const {
83
+ conversation,
84
+ status,
85
+ reasoning,
86
+ progress,
87
+ typing,
88
+ sessionUsage,
89
+ modelMetadata,
90
+ hasInteracted,
91
+ suppressStatusBar = false,
92
+ frostColor,
93
+ initialStatusTop,
94
+ hasGrounding,
95
+ groundingCount,
96
+ modelName,
97
+ sessionTitle,
98
+ isVoiceOutputEnabled,
99
+ } = props;
100
+
101
+ const { conversationHistory, currentTranscription, currentContentBlocks } = conversation;
102
+ const {
103
+ daemonState,
104
+ statusText,
105
+ statusColor,
106
+ apiKeyMissingError,
107
+ error,
108
+ resetNotification,
109
+ escPendingCancel,
110
+ } = status;
111
+ const { showFullReasoning, showToolOutput, reasoningQueue, reasoningDisplay, fullReasoning } = reasoning;
112
+ const { showWorkingSpinner, isToolCalling, responseElapsedMs } = progress;
113
+ const {
114
+ typingTextareaRef,
115
+ conversationScrollRef,
116
+ onTypingContentChange,
117
+ onTypingSubmit,
118
+ onHistoryUp,
119
+ onHistoryDown,
120
+ } = typing;
121
+
122
+ const showSessionDebug = Boolean(process.env.DEBUG_SESSION);
123
+
124
+ const renderHistoryBlock = (block: ContentBlock, idx: number, blocks: ContentBlock[]) => {
125
+ if (shouldHideContentBlock(block)) {
126
+ return null;
127
+ }
128
+
129
+ let nextBlock: ContentBlock | undefined;
130
+ for (let i = idx + 1; i < blocks.length; i++) {
131
+ const b = blocks[i];
132
+ if (b && !shouldHideContentBlock(b)) {
133
+ nextBlock = b;
134
+ break;
135
+ }
136
+ }
137
+
138
+ const isTool = block.type === "tool";
139
+ const isNextTool = nextBlock?.type === "tool";
140
+ const marginBottom = isTool && isNextTool ? 0 : 1;
141
+
142
+ return (
143
+ <box key={idx} flexDirection="column" marginBottom={marginBottom}>
144
+ <ContentBlockView
145
+ block={block}
146
+ isLastReasoningBlock={isLastReasoningBlockInList(blocks, block)}
147
+ isLastTextBlock={isLastTextBlockInList(blocks, block)}
148
+ isLastBlock={idx === blocks.length - 1}
149
+ isStreaming={false}
150
+ showFullReasoning={showFullReasoning}
151
+ showToolOutput={showToolOutput}
152
+ />
153
+ </box>
154
+ );
155
+ };
156
+
157
+ const renderMessageDebug = (msg: ConversationMessage) => {
158
+ if (!showSessionDebug) return null;
159
+ const roles = msg.messages?.map((m) => m.role).join(",") ?? "none";
160
+ const pendingLabel = msg.pending ? " · pending" : "";
161
+ return (
162
+ <box marginBottom={1}>
163
+ <text>
164
+ <span fg={COLORS.REASONING_DIM}>
165
+ #id:{msg.id} · type:{msg.type} · roles:{roles} · blocks:
166
+ {msg.contentBlocks?.length ?? 0}
167
+ {pendingLabel}
168
+ </span>
169
+ </text>
170
+ </box>
171
+ );
172
+ };
173
+
174
+ const showTypingInput = hasInteracted && daemonState === DaemonState.TYPING;
175
+ const isReasoning =
176
+ daemonState === DaemonState.RESPONDING &&
177
+ (!conversation.currentResponse || !!reasoningDisplay || !!reasoningQueue);
178
+
179
+ return (
180
+ <>
181
+ {hasInteracted && !suppressStatusBar && (
182
+ <StatusBar
183
+ statusText={statusText}
184
+ statusColor={statusColor}
185
+ errorText={apiKeyMissingError || error}
186
+ modelName={modelName}
187
+ sessionTitle={sessionTitle}
188
+ hasInteracted={hasInteracted}
189
+ />
190
+ )}
191
+
192
+ {!hasInteracted && (
193
+ <box
194
+ position="absolute"
195
+ left={0}
196
+ top={initialStatusTop}
197
+ width="100%"
198
+ alignItems="center"
199
+ justifyContent="center"
200
+ zIndex={2}
201
+ >
202
+ <box flexDirection="column" alignItems="center">
203
+ <StatusBar
204
+ statusText={statusText}
205
+ statusColor={statusColor}
206
+ errorText={apiKeyMissingError || error}
207
+ modelName={modelName}
208
+ />
209
+
210
+ {isVoiceOutputEnabled && daemonState === DaemonState.IDLE && (
211
+ <box marginTop={1}>
212
+ <text>
213
+ <span fg={COLORS.REASONING_DIM}>◉ voice output active</span>
214
+ </text>
215
+ </box>
216
+ )}
217
+
218
+ <box marginTop={2} width="100%" justifyContent="center">
219
+ {daemonState === DaemonState.TYPING ? (
220
+ <TypingInputBar
221
+ onContentChange={onTypingContentChange}
222
+ onSubmit={onTypingSubmit}
223
+ onHistoryUp={onHistoryUp}
224
+ onHistoryDown={onHistoryDown}
225
+ textareaRef={typingTextareaRef}
226
+ placeholder="Enter instructions..."
227
+ width="75%"
228
+ maxWidth={140}
229
+ minWidth={55}
230
+ height={5}
231
+ />
232
+ ) : (
233
+ <></>
234
+ )}
235
+ </box>
236
+ </box>
237
+ </box>
238
+ )}
239
+
240
+ {hasInteracted &&
241
+ (sessionUsage.totalTokens > 0 ||
242
+ (sessionUsage.subagentTotalTokens ?? 0) > 0 ||
243
+ typeof sessionUsage.cost === "number") && (
244
+ <TokenUsageDisplay usage={sessionUsage} modelMetadata={modelMetadata} />
245
+ )}
246
+
247
+ {hasInteracted && resetNotification && (
248
+ <box
249
+ height={1}
250
+ width="100%"
251
+ flexShrink={0}
252
+ flexDirection="row"
253
+ justifyContent="center"
254
+ alignItems="center"
255
+ marginTop={1}
256
+ >
257
+ <text>
258
+ <span fg={COLORS.REASONING}>[ {resetNotification} ]</span>
259
+ </text>
260
+ </box>
261
+ )}
262
+
263
+ {hasInteracted && escPendingCancel && (
264
+ <box
265
+ height={1}
266
+ width="100%"
267
+ flexShrink={0}
268
+ flexDirection="row"
269
+ justifyContent="center"
270
+ alignItems="center"
271
+ >
272
+ <text>
273
+ <span fg={COLORS.ERROR}>[ Press ESC again to cancel ]</span>
274
+ </text>
275
+ </box>
276
+ )}
277
+
278
+ {hasInteracted && (
279
+ <scrollbox
280
+ flexGrow={1}
281
+ flexShrink={1}
282
+ focused={false}
283
+ stickyScroll={true}
284
+ stickyStart="bottom"
285
+ ref={conversationScrollRef}
286
+ style={{
287
+ rootOptions: { backgroundColor: frostColor },
288
+ contentOptions: {
289
+ backgroundColor: frostColor,
290
+ paddingLeft: 2,
291
+ paddingRight: 2,
292
+ },
293
+ }}
294
+ >
295
+ <box
296
+ flexDirection="column"
297
+ paddingTop={1}
298
+ paddingBottom={2}
299
+ width="100%"
300
+ backgroundColor={frostColor}
301
+ >
302
+ {conversationHistory.map((msg: ConversationMessage) => (
303
+ <box key={msg.id} flexDirection="column">
304
+ {msg.type === "user" ? (
305
+ <box
306
+ marginBottom={1}
307
+ paddingLeft={2}
308
+ paddingRight={2}
309
+ paddingTop={1}
310
+ paddingBottom={1}
311
+ backgroundColor={COLORS.USER_BG}
312
+ width="100%"
313
+ >
314
+ <>
315
+ {renderMessageDebug(msg)}
316
+ <text>
317
+ <span fg={COLORS.USER_LABEL}>YOU: </span>
318
+ <span fg={COLORS.USER_TEXT}>{msg.content}</span>
319
+ </text>
320
+ </>
321
+ </box>
322
+ ) : msg.contentBlocks && msg.contentBlocks.length > 0 ? (
323
+ <>
324
+ {renderMessageDebug(msg)}
325
+ {msg.contentBlocks.map((block, idx) =>
326
+ renderHistoryBlock(block, idx, msg.contentBlocks!)
327
+ )}
328
+ </>
329
+ ) : null}
330
+ </box>
331
+ ))}
332
+
333
+ {currentTranscription && (
334
+ <box
335
+ marginBottom={1}
336
+ paddingLeft={2}
337
+ paddingRight={2}
338
+ paddingTop={1}
339
+ paddingBottom={1}
340
+ backgroundColor={COLORS.USER_BG}
341
+ width="100%"
342
+ >
343
+ <text>
344
+ <span fg={COLORS.USER_LABEL}>YOU: </span>
345
+ <span fg={COLORS.USER_TEXT}>{currentTranscription}</span>
346
+ </text>
347
+ </box>
348
+ )}
349
+
350
+ {currentContentBlocks.length > 0 && (
351
+ <box flexDirection="column">
352
+ {currentContentBlocks.map((block, idx) => {
353
+ if (shouldHideContentBlock(block)) {
354
+ return null;
355
+ }
356
+
357
+ let nextBlock: ContentBlock | undefined;
358
+ for (let i = idx + 1; i < currentContentBlocks.length; i++) {
359
+ const b = currentContentBlocks[i];
360
+ if (b && !shouldHideContentBlock(b)) {
361
+ nextBlock = b;
362
+ break;
363
+ }
364
+ }
365
+
366
+ const isLastBlock = idx === currentContentBlocks.length - 1;
367
+ const isLastText = isLastTextBlockInList(currentContentBlocks, block);
368
+ const isLastReasoning = isLastReasoningBlockInList(currentContentBlocks, block);
369
+ const isStreaming =
370
+ daemonState === DaemonState.RESPONDING &&
371
+ isLastBlock &&
372
+ (block.type === "text" || block.type === "reasoning");
373
+
374
+ const hasReasoningContent = !!(reasoningQueue || reasoningDisplay);
375
+
376
+ const isTool = block.type === "tool";
377
+ const isNextTool = nextBlock?.type === "tool";
378
+ const marginBottom = isTool && isNextTool ? 0 : 1;
379
+
380
+ return (
381
+ <box key={idx} flexDirection="column" marginBottom={marginBottom}>
382
+ <ContentBlockView
383
+ block={block}
384
+ isLastReasoningBlock={isLastReasoning}
385
+ isLastTextBlock={isLastText}
386
+ isLastBlock={isLastBlock}
387
+ isStreaming={isStreaming}
388
+ showFullReasoning={showFullReasoning}
389
+ showToolOutput={showToolOutput}
390
+ reasoningDisplay={reasoningDisplay}
391
+ showReasoningTicker={hasReasoningContent}
392
+ />
393
+ </box>
394
+ );
395
+ })}
396
+ </box>
397
+ )}
398
+
399
+ {currentContentBlocks.length === 0 && (reasoningDisplay || reasoningQueue) && (
400
+ <box marginBottom={1}>
401
+ {showFullReasoning && fullReasoning ? (
402
+ <box
403
+ flexDirection="column"
404
+ border={["left"]}
405
+ borderStyle="heavy"
406
+ borderColor={COLORS.REASONING_DIM}
407
+ paddingLeft={1}
408
+ >
409
+ <text>
410
+ <span fg={COLORS.REASONING}>{"REASONING"}</span>
411
+ </text>
412
+ <code
413
+ content={fullReasoning}
414
+ filetype="markdown"
415
+ syntaxStyle={REASONING_MARKDOWN_STYLE}
416
+ streaming={true}
417
+ drawUnstyledText={false}
418
+ />
419
+ </box>
420
+ ) : reasoningDisplay ? (
421
+ <text>
422
+ <span fg={COLORS.REASONING_DIM}>
423
+ {"⟡ "}
424
+ {reasoningDisplay}
425
+ </span>
426
+ </text>
427
+ ) : null}
428
+ </box>
429
+ )}
430
+
431
+ {hasGrounding && groundingCount && daemonState === DaemonState.IDLE && (
432
+ <GroundingBadge count={groundingCount} />
433
+ )}
434
+
435
+ {showWorkingSpinner && (
436
+ <InlineStatusIndicator
437
+ daemonState={daemonState}
438
+ isToolCalling={isToolCalling}
439
+ isReasoning={isReasoning}
440
+ responseElapsedMs={responseElapsedMs}
441
+ />
442
+ )}
443
+
444
+ {isReasoning && <box height={0} />}
445
+ </box>
446
+ </scrollbox>
447
+ )}
448
+
449
+ {showTypingInput && (
450
+ <box
451
+ flexShrink={0}
452
+ marginTop={1}
453
+ marginBottom={1}
454
+ width="100%"
455
+ justifyContent="center"
456
+ alignItems="center"
457
+ >
458
+ <TypingInputBar
459
+ onContentChange={onTypingContentChange}
460
+ onSubmit={onTypingSubmit}
461
+ onHistoryUp={onHistoryUp}
462
+ onHistoryDown={onHistoryDown}
463
+ textareaRef={typingTextareaRef}
464
+ placeholder="Enter instructions..."
465
+ height={4}
466
+ width="92%"
467
+ maxWidth={170}
468
+ minWidth={80}
469
+ />
470
+ </box>
471
+ )}
472
+ </>
473
+ );
474
+ }
475
+
476
+ export const ConversationPane = memo(ConversationPaneImpl);