sonex-agent 0.1.0-alpha.1

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 (53) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +15 -0
  3. package/bin/sonex.js +235 -0
  4. package/dist/App.js +1360 -0
  5. package/dist/activity.js +18 -0
  6. package/dist/chat-document.js +40 -0
  7. package/dist/chat-message.js +93 -0
  8. package/dist/chat-theme.js +18 -0
  9. package/dist/chat-window.js +104 -0
  10. package/dist/command-panel.js +45 -0
  11. package/dist/commands.js +70 -0
  12. package/dist/components.js +662 -0
  13. package/dist/confirm-choice.js +65 -0
  14. package/dist/constants.js +109 -0
  15. package/dist/conversation-flow.js +21 -0
  16. package/dist/cover-pattern.js +58 -0
  17. package/dist/cover-visual.js +158 -0
  18. package/dist/extension-panel.js +136 -0
  19. package/dist/format.js +99 -0
  20. package/dist/hooks.js +216 -0
  21. package/dist/i18n.js +291 -0
  22. package/dist/index.js +46 -0
  23. package/dist/info-banner.js +37 -0
  24. package/dist/input-cursor.js +6 -0
  25. package/dist/input-routing.js +41 -0
  26. package/dist/launch-preparing.js +30 -0
  27. package/dist/layout.js +134 -0
  28. package/dist/list.js +3 -0
  29. package/dist/login-navigation.js +7 -0
  30. package/dist/mini-progress-writer.js +122 -0
  31. package/dist/mini-progress.js +77 -0
  32. package/dist/model-selection.js +20 -0
  33. package/dist/model-status.js +34 -0
  34. package/dist/mouse-input.js +173 -0
  35. package/dist/panel-frame.js +86 -0
  36. package/dist/panel-lifecycle.js +17 -0
  37. package/dist/playback-keymap.js +59 -0
  38. package/dist/provider-state.js +67 -0
  39. package/dist/runtime-state.js +91 -0
  40. package/dist/shell-state.js +37 -0
  41. package/dist/sonex-logo.js +9 -0
  42. package/dist/terminal-clear.js +10 -0
  43. package/dist/terminal-frame-writer.js +133 -0
  44. package/dist/terminal-surface.js +80 -0
  45. package/dist/text-stream.js +17 -0
  46. package/dist/track-panel.js +95 -0
  47. package/dist/transcript.js +92 -0
  48. package/dist/types.js +1 -0
  49. package/dist/ui-settings.js +30 -0
  50. package/dist/usage-animation.js +14 -0
  51. package/package.json +57 -0
  52. package/vendor/requirements-linux-py312.txt +2659 -0
  53. package/vendor/sonex-0.1.0a1-py3-none-any.whl +0 -0
package/dist/App.js ADDED
@@ -0,0 +1,1360 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import React, { useState } from 'react';
3
+ import { Box, useApp, useInput, useStdin } from 'ink';
4
+ import { completeSlashCommand, hasSlashCommandArguments, matchingSlashCommand, slashCommandSuggestions, spotifyModeSlashCommands, unknownSlashCommandMessage } from './commands.js';
5
+ import { getSelectableConfirmChoices } from './confirm-choice.js';
6
+ import { selectedHelpPanelCommand } from './command-panel.js';
7
+ import { API_NOT_RUNNING_DETAIL, API_NOT_RUNNING_MESSAGE, DEFAULT_CONFIRM_CHOICES, FALLBACK_MODEL_NAME, wsUrl } from './constants.js';
8
+ import { CommittedTranscript, DynamicShell, HeaderFrame, isGenericAuthSetup, LoginScreen } from './components.js';
9
+ import { useSonexSocket } from './hooks.js';
10
+ import { chatMessagesForTranscript, createInfoBannerItem } from './info-banner.js';
11
+ import { applyLanguageToServerEvent, helpCommandsForLanguage, OFFICIAL_UI_LANGUAGE, t } from './i18n.js';
12
+ import { LAUNCH_PREPARING_INTERVAL_MS } from './launch-preparing.js';
13
+ import { resolveChatHeaderVariant, resolveMiniPlayerLayout, resolveSpotifyImmersiveLayout } from './layout.js';
14
+ import { shouldRefreshMiniSnapshot, usePlaybackProgressWriter, usePlaybackStatusIconWriter } from './mini-progress-writer.js';
15
+ import { formatModelStatus } from './model-status.js';
16
+ import { filterModelChoices } from './model-selection.js';
17
+ import { resolveLoginProviderSelectionIndex } from './login-navigation.js';
18
+ import { isLocalPlaybackShortcutSource, isSpotifyPlaybackShortcutSource, playbackCommandForShortcut, playbackShortcutFromInput } from './playback-keymap.js';
19
+ import { markQueuedTracks } from './track-panel.js';
20
+ import { TEXT_STREAM_INTERVAL_MS, nextTextStreamOffset, streamedChatMessage, textStreamUnits } from './text-stream.js';
21
+ import { allTranscriptItems, classifyServerEventForTranscript, createTranscriptState, transcriptReducer } from './transcript.js';
22
+ import { initialShellState, planShellSurfaceTransition, reduceShellState, surfaceForShellRegion } from './shell-state.js';
23
+ import { createInitialRuntimeState, reduceRuntimeState } from './runtime-state.js';
24
+ import { resolveInputRoute } from './input-routing.js';
25
+ import { initialProviderState, reduceProviderState } from './provider-state.js';
26
+ import { planPanelLifecycle } from './panel-lifecycle.js';
27
+ import { TOKEN_USAGE_ANIMATION_INTERVAL_MS, nextAnimatedTokenUsage } from './usage-animation.js';
28
+ const RUNTIME_WORKING_DIRECTORY = process.env.SONEX_LAUNCH_CWD?.trim() || process.cwd();
29
+ const isTrackPanelQueueShortcut = (inputKey, key) => {
30
+ return Boolean(key.ctrl && (inputKey === "\x01" || inputKey.toLowerCase() === "a"));
31
+ };
32
+ export const App = ({ terminalSurface, terminalStdout: stdout }) => {
33
+ const { exit } = useApp();
34
+ const { isRawModeSupported } = useStdin();
35
+ const rawModeAvailable = Boolean(isRawModeSupported && typeof process.stdin.setRawMode === "function");
36
+ const [language] = useState(OFFICIAL_UI_LANGUAGE);
37
+ const [input, setInput] = useState("");
38
+ const [inputRevision, setInputRevision] = useState(0);
39
+ const [runtimeState, dispatchRuntimeState] = React.useReducer(reduceRuntimeState, undefined, () => createInitialRuntimeState(t(OFFICIAL_UI_LANGUAGE, "status.snoozing")));
40
+ const { sessionId, tokenUsage, agentWorkingTurnId, activityItems, statusText, launchPreparing, recommendInputLocked } = runtimeState;
41
+ const [displayedTokenUsage, setDisplayedTokenUsage] = useState({ inputTokens: 0, outputTokens: 0 });
42
+ const [activeTextStream, setActiveTextStream] = useState(null);
43
+ const [transcript, dispatchTranscript] = React.useReducer(transcriptReducer, undefined, createTranscriptState);
44
+ const [queueItems, setQueueItems] = useState([]);
45
+ const [searchItems, setSearchItems] = useState([]);
46
+ const [trackPanel, setTrackPanel] = useState(null);
47
+ const [memoryPanel, setMemoryPanel] = useState(null);
48
+ const [extensionPanel, setExtensionPanel] = useState(null);
49
+ const [extensionPanelIndex, setExtensionPanelIndex] = useState(0);
50
+ const [extensionInputFocused, setExtensionInputFocused] = useState(false);
51
+ const [memorySearchQuery, setMemorySearchQuery] = useState("");
52
+ const [memoryEditor, setMemoryEditor] = useState(null);
53
+ const [player, setPlayer] = useState({ name: "-", artist: "-", album: "-", duration_ms: 0, progress_ms: 0, is_playing: false });
54
+ const [launchPreparingFrame, setLaunchPreparingFrame] = useState(0);
55
+ const [coverUrl, setCoverUrl] = useState(null);
56
+ const [coverPattern, setCoverPattern] = useState(null);
57
+ const coverUrlRef = React.useRef(null);
58
+ const [confirm, setConfirm] = useState(null);
59
+ const [confirmIndex, setConfirmIndex] = useState(0); // 0=Yes, 1=No
60
+ const [providerState, dispatchProviderState] = React.useReducer(reduceProviderState, initialProviderState);
61
+ const { spotifyMode, providerMode, spotifySetup, authSetup } = providerState;
62
+ const [authState, setAuthState] = useState({
63
+ ready: false,
64
+ provider: "openai",
65
+ model: FALLBACK_MODEL_NAME,
66
+ auth_type: "none",
67
+ credential_source: "pending",
68
+ });
69
+ const [shellState, dispatchShellState] = React.useReducer(reduceShellState, initialShellState);
70
+ const activeRegion = shellState.region;
71
+ const playbackSessionActive = shellState.playbackSessionActive;
72
+ const [miniSnapshotRevision, setMiniSnapshotRevision] = useState(0);
73
+ const [terminalSize, setTerminalSize] = useState({
74
+ columns: stdout.columns ?? null,
75
+ rows: stdout.rows ?? null,
76
+ });
77
+ const [slashIndex, setSlashIndex] = useState(0);
78
+ const [slashMenuDismissedFor, setSlashMenuDismissedFor] = useState(null);
79
+ const [isExiting, setIsExiting] = useState(false);
80
+ const [helpPanel, setHelpPanel] = useState(null);
81
+ const [helpPanelIndex, setHelpPanelIndex] = useState(0);
82
+ const [languagePanel, setLanguagePanel] = useState(null);
83
+ const [languagePanelIndex, setLanguagePanelIndex] = useState(0);
84
+ const [trackPanelIndex, setTrackPanelIndex] = useState(0);
85
+ const [memoryPanelIndex, setMemoryPanelIndex] = useState(0);
86
+ const [loginSelectionIndex, setLoginSelectionIndex] = useState(0);
87
+ const [loginApiKeyInput, setLoginApiKeyInput] = useState("");
88
+ const runtimeStateRef = React.useRef(runtimeState);
89
+ const providerStateRef = React.useRef(providerState);
90
+ const shellStateRef = React.useRef(shellState);
91
+ const playerRef = React.useRef(player);
92
+ const confirmRef = React.useRef(null);
93
+ const dismissedConfirmIdsRef = React.useRef(new Set());
94
+ const spotifyModeRef = React.useRef(spotifyMode);
95
+ const providerModeRef = React.useRef(providerMode);
96
+ const spotifySetupActiveRef = React.useRef(false);
97
+ const authSetupActiveRef = React.useRef(false);
98
+ const slashMenuActiveRef = React.useRef(false);
99
+ const sessionIdRef = React.useRef(null);
100
+ const startupInfoCapturedRef = React.useRef(false);
101
+ const activeTextStreamRef = React.useRef(null);
102
+ const nextTextStreamIdRef = React.useRef(0);
103
+ const isModelPanelActive = authSetup?.active && authSetup.step === "model";
104
+ const isLoginScreenActive = isGenericAuthSetup(authSetup) && !isModelPanelActive;
105
+ const extensionSetupInput = extensionPanel?.view === "setup" ? extensionPanel.setup?.input : null;
106
+ const authInterfaceActive = Boolean(authSetup?.active || spotifySetup?.active);
107
+ const showFixedHeader = activeRegion === "chat" && authInterfaceActive && !isLoginScreenActive;
108
+ const slashSuggestions = authSetup?.active || spotifySetup?.active || languagePanel?.active || extensionPanel
109
+ ? []
110
+ : spotifyMode.enabled
111
+ ? spotifyModeSlashCommands(input, language)
112
+ : slashCommandSuggestions(input, language);
113
+ const slashInput = input.trimStart();
114
+ const isSlashInput = slashInput.startsWith("/");
115
+ const isSlashMenuActive = rawModeAvailable && !confirm && isSlashInput && slashMenuDismissedFor !== input && slashSuggestions.length > 0;
116
+ const isUnknownSlashInput = (rawModeAvailable
117
+ && activeRegion === "chat"
118
+ && !confirm
119
+ && !authSetup?.active
120
+ && !spotifySetup?.active
121
+ && !helpPanel
122
+ && !languagePanel?.active
123
+ && !recommendInputLocked
124
+ && slashInput.length > 1
125
+ && isSlashInput
126
+ && slashSuggestions.length === 0
127
+ && !matchingSlashCommand(input));
128
+ const selectedSlashCommand = slashSuggestions[Math.min(slashIndex, Math.max(0, slashSuggestions.length - 1))];
129
+ const selectableConfirmChoices = React.useMemo(() => confirm ? getSelectableConfirmChoices(confirm.choices, confirm.tool_name === "provider_mode_exit") : [], [confirm]);
130
+ const selectedConfirmChoice = selectableConfirmChoices[Math.min(confirmIndex, Math.max(0, selectableConfirmChoices.length - 1))] ?? null;
131
+ const selectedConfirmInput = selectedConfirmChoice?.input ?? null;
132
+ const miniVisible = activeRegion === "miniPlayer";
133
+ const spotifyImmersiveVisible = activeRegion === "spotifyImmersive" || activeRegion === "providerImmersive";
134
+ const miniLayout = React.useMemo(() => resolveMiniPlayerLayout(terminalSize), [terminalSize.columns, terminalSize.rows]);
135
+ const spotifyImmersiveLayout = React.useMemo(() => resolveSpotifyImmersiveLayout(terminalSize), [terminalSize.columns, terminalSize.rows]);
136
+ const headerVariant = resolveChatHeaderVariant(terminalSize.columns);
137
+ const transcriptContentWidth = Math.max(1, (terminalSize.columns ?? 80) - 4);
138
+ const dynamicSurfaceHeight = terminalSize.rows === null
139
+ ? undefined
140
+ : Math.max(0, terminalSize.rows - 1);
141
+ const transcriptPresentation = React.useMemo(() => ({
142
+ contentWidth: transcriptContentWidth,
143
+ headerVariant,
144
+ language,
145
+ }), [headerVariant, language, transcriptContentWidth]);
146
+ const modelStatus = formatModelStatus(authState, displayedTokenUsage);
147
+ const streamingMessage = React.useMemo(() => {
148
+ if (!activeTextStream)
149
+ return null;
150
+ return streamedChatMessage(activeTextStream.item, activeTextStream.units, activeTextStream.visibleUnitCount);
151
+ }, [activeTextStream]);
152
+ const baseLanguageChoices = React.useMemo(() => ["en", "zh-CN"], []);
153
+ const languageChoices = React.useMemo(() => [language, ...baseLanguageChoices.filter((choice) => choice !== language)], [baseLanguageChoices, language]);
154
+ React.useEffect(() => {
155
+ playerRef.current = player;
156
+ }, [player]);
157
+ React.useEffect(() => {
158
+ confirmRef.current = confirm;
159
+ }, [confirm]);
160
+ React.useEffect(() => {
161
+ slashMenuActiveRef.current = isSlashMenuActive;
162
+ }, [isSlashMenuActive]);
163
+ React.useEffect(() => {
164
+ let resizeTimer = null;
165
+ const updateTerminalSize = () => {
166
+ if (resizeTimer)
167
+ clearTimeout(resizeTimer);
168
+ resizeTimer = setTimeout(() => {
169
+ const nextSize = {
170
+ columns: stdout.columns ?? null,
171
+ rows: stdout.rows ?? null,
172
+ };
173
+ const updateSize = () => {
174
+ setTerminalSize(nextSize);
175
+ if (shellStateRef.current.region === "miniPlayer" && shouldRefreshMiniSnapshot("resize")) {
176
+ setMiniSnapshotRevision((prev) => prev + 1);
177
+ }
178
+ };
179
+ if (surfaceForShellRegion(shellStateRef.current.region) === "main") {
180
+ updateSize();
181
+ }
182
+ else {
183
+ terminalSurface.transition("alternate", updateSize);
184
+ }
185
+ }, 80);
186
+ };
187
+ const initializeTerminalSize = () => {
188
+ setTerminalSize({
189
+ columns: stdout.columns ?? null,
190
+ rows: stdout.rows ?? null,
191
+ });
192
+ };
193
+ initializeTerminalSize();
194
+ stdout.on("resize", updateTerminalSize);
195
+ return () => {
196
+ if (resizeTimer)
197
+ clearTimeout(resizeTimer);
198
+ stdout.off("resize", updateTerminalSize);
199
+ };
200
+ }, [stdout, terminalSurface]);
201
+ const finishActiveTextStream = React.useCallback(() => {
202
+ const active = activeTextStreamRef.current;
203
+ if (!active)
204
+ return;
205
+ activeTextStreamRef.current = null;
206
+ setActiveTextStream(null);
207
+ dispatchTranscript({
208
+ type: "commit",
209
+ items: [active.item],
210
+ presentation: active.presentation,
211
+ });
212
+ }, []);
213
+ const commitItems = React.useCallback((items) => {
214
+ if (items.length === 0)
215
+ return;
216
+ finishActiveTextStream();
217
+ dispatchTranscript({ type: "commit", items, presentation: transcriptPresentation });
218
+ }, [finishActiveTextStream, transcriptPresentation]);
219
+ const startTextStream = React.useCallback((item) => {
220
+ finishActiveTextStream();
221
+ const units = textStreamUnits(item.content);
222
+ if (units.length === 0) {
223
+ dispatchTranscript({ type: "commit", items: [item], presentation: transcriptPresentation });
224
+ return;
225
+ }
226
+ const active = {
227
+ id: nextTextStreamIdRef.current,
228
+ item,
229
+ units,
230
+ visibleUnitCount: nextTextStreamOffset(0, units.length),
231
+ presentation: transcriptPresentation,
232
+ };
233
+ nextTextStreamIdRef.current += 1;
234
+ activeTextStreamRef.current = active;
235
+ setActiveTextStream(active);
236
+ }, [finishActiveTextStream, transcriptPresentation]);
237
+ React.useEffect(() => {
238
+ if (!activeTextStream)
239
+ return;
240
+ if (activeTextStream.visibleUnitCount >= activeTextStream.units.length) {
241
+ finishActiveTextStream();
242
+ return;
243
+ }
244
+ const timer = setTimeout(() => {
245
+ setActiveTextStream((current) => {
246
+ if (!current || current.id !== activeTextStream.id)
247
+ return current;
248
+ const next = {
249
+ ...current,
250
+ visibleUnitCount: nextTextStreamOffset(current.visibleUnitCount, current.units.length),
251
+ };
252
+ activeTextStreamRef.current = next;
253
+ return next;
254
+ });
255
+ }, TEXT_STREAM_INTERVAL_MS);
256
+ return () => clearTimeout(timer);
257
+ }, [activeTextStream, finishActiveTextStream]);
258
+ const applyShellAction = React.useCallback((action) => {
259
+ const nextState = reduceShellState(shellStateRef.current, action);
260
+ shellStateRef.current = nextState;
261
+ dispatchShellState({ type: "replace", state: nextState });
262
+ }, []);
263
+ const applyRuntimeAction = React.useCallback((action) => {
264
+ const nextState = reduceRuntimeState(runtimeStateRef.current, action);
265
+ runtimeStateRef.current = nextState;
266
+ if (action.type === "event" && action.event.type === "session_state") {
267
+ sessionIdRef.current = action.event.session_id;
268
+ }
269
+ dispatchRuntimeState({ type: "replace", state: nextState });
270
+ }, []);
271
+ const applyProviderAction = React.useCallback((action) => {
272
+ const nextState = reduceProviderState(providerStateRef.current, action);
273
+ providerStateRef.current = nextState;
274
+ spotifyModeRef.current = nextState.spotifyMode;
275
+ providerModeRef.current = nextState.providerMode;
276
+ spotifySetupActiveRef.current = Boolean(nextState.spotifySetup?.active);
277
+ authSetupActiveRef.current = Boolean(nextState.authSetup?.active);
278
+ dispatchProviderState({ type: "replace", state: nextState });
279
+ }, []);
280
+ const switchRegion = React.useCallback((nextRegion) => {
281
+ const transition = planShellSurfaceTransition(shellStateRef.current.region, nextRegion);
282
+ if (!transition.changed)
283
+ return;
284
+ const nextSurface = transition.target;
285
+ terminalSurface.transition(nextSurface, (surface) => {
286
+ dispatchTranscript({ type: "setSurface", surface });
287
+ applyShellAction({ type: "set_region", region: nextRegion });
288
+ if (nextRegion === "miniPlayer" && shouldRefreshMiniSnapshot("region")) {
289
+ setMiniSnapshotRevision((prev) => prev + 1);
290
+ }
291
+ });
292
+ }, [applyShellAction, terminalSurface]);
293
+ usePlaybackProgressWriter({
294
+ enabled: stdout.isTTY === true && (miniVisible || spotifyImmersiveVisible),
295
+ player,
296
+ position: spotifyImmersiveVisible ? spotifyImmersiveLayout.progressSlot : miniLayout.progressSlot,
297
+ stdout,
298
+ });
299
+ usePlaybackStatusIconWriter({
300
+ enabled: stdout.isTTY === true && miniVisible,
301
+ player,
302
+ position: miniLayout.statusIconSlot,
303
+ stdout,
304
+ });
305
+ React.useEffect(() => {
306
+ setSlashIndex((prev) => Math.min(prev, Math.max(0, slashSuggestions.length - 1)));
307
+ }, [slashSuggestions.length]);
308
+ React.useEffect(() => {
309
+ if (!isSlashInput || authSetup?.active || spotifySetup?.active || slashSuggestions.length === 0) {
310
+ setSlashMenuDismissedFor(null);
311
+ setSlashIndex(0);
312
+ }
313
+ }, [authSetup?.active, isSlashInput, slashSuggestions.length, spotifySetup?.active]);
314
+ React.useEffect(() => {
315
+ setLoginApiKeyInput("");
316
+ }, [authSetup?.step, authSetup?.provider]);
317
+ React.useEffect(() => {
318
+ if (!launchPreparing)
319
+ return;
320
+ const timer = setInterval(() => {
321
+ setLaunchPreparingFrame((prev) => prev + 1);
322
+ }, LAUNCH_PREPARING_INTERVAL_MS);
323
+ return () => clearInterval(timer);
324
+ }, [launchPreparing]);
325
+ const applyPanelLifecycle = React.useCallback((trigger) => {
326
+ const lifecycle = planPanelLifecycle(trigger);
327
+ for (const panel of lifecycle.close) {
328
+ if (panel === 'track')
329
+ setTrackPanel(null);
330
+ if (panel === 'memory')
331
+ setMemoryPanel(null);
332
+ if (panel === 'extension')
333
+ setExtensionPanel(null);
334
+ if (panel === 'help')
335
+ setHelpPanel(null);
336
+ if (panel === 'language')
337
+ setLanguagePanel(null);
338
+ }
339
+ for (const panel of lifecycle.resetSelection) {
340
+ if (panel === 'track')
341
+ setTrackPanelIndex(0);
342
+ if (panel === 'help')
343
+ setHelpPanelIndex(0);
344
+ }
345
+ }, []);
346
+ React.useEffect(() => {
347
+ if (displayedTokenUsage.inputTokens === tokenUsage.inputTokens
348
+ && displayedTokenUsage.outputTokens === tokenUsage.outputTokens)
349
+ return;
350
+ const timer = setTimeout(() => {
351
+ setDisplayedTokenUsage((current) => nextAnimatedTokenUsage(current, tokenUsage));
352
+ }, TOKEN_USAGE_ANIMATION_INTERVAL_MS);
353
+ return () => clearTimeout(timer);
354
+ }, [displayedTokenUsage, tokenUsage]);
355
+ const updateInput = React.useCallback((value) => {
356
+ if (recommendInputLocked)
357
+ return;
358
+ const sanitized = value.replace(/\x1B/g, "");
359
+ setInput(sanitized);
360
+ if (sanitized) {
361
+ applyPanelLifecycle("input");
362
+ }
363
+ if (sanitized !== slashMenuDismissedFor) {
364
+ setSlashMenuDismissedFor(null);
365
+ }
366
+ }, [applyPanelLifecycle, recommendInputLocked, slashMenuDismissedFor]);
367
+ const showError = React.useCallback((message, detail) => {
368
+ const content = detail ? `${message}\n${detail}` : message;
369
+ commitItems([{
370
+ type: "message",
371
+ role: "agent",
372
+ content,
373
+ theme: spotifyModeRef.current.enabled ? "spotify" : undefined,
374
+ tone: "error",
375
+ }]);
376
+ }, [commitItems]);
377
+ const appendUnknownCommandWarning = React.useCallback((value) => {
378
+ commitItems([{
379
+ type: "message",
380
+ role: "agent",
381
+ content: unknownSlashCommandMessage(value),
382
+ tone: "warning",
383
+ }]);
384
+ }, [commitItems]);
385
+ const inputPlaceholder = selectedConfirmInput
386
+ ? selectedConfirmInput.placeholder
387
+ : authSetup?.active && authSetup.prompt
388
+ ? authSetup.prompt
389
+ : spotifySetup?.active && spotifySetup.prompt
390
+ ? spotifySetup.prompt
391
+ : extensionSetupInput
392
+ ? extensionSetupInput.placeholder
393
+ : recommendInputLocked
394
+ ? t(language, "input.recommendPending")
395
+ : "";
396
+ const inputMask = authSetup?.active && authSetup.mask
397
+ ? "*"
398
+ : spotifySetup?.active && spotifySetup.mask
399
+ ? "*"
400
+ : extensionSetupInput?.mask
401
+ ? "*"
402
+ : undefined;
403
+ const onEvent = React.useCallback((rawEvent) => {
404
+ const evt = applyLanguageToServerEvent(rawEvent, language);
405
+ const transcriptClass = classifyServerEventForTranscript(evt);
406
+ if (transcriptClass === "chat" && evt.type === "chat") {
407
+ const item = {
408
+ type: "message",
409
+ role: evt.role,
410
+ content: evt.text,
411
+ theme: evt.theme,
412
+ tone: evt.tone,
413
+ segments: evt.segments,
414
+ document: evt.document,
415
+ };
416
+ if (evt.role === "agent" && evt.stream) {
417
+ startTextStream(item);
418
+ return;
419
+ }
420
+ if (evt.role === "user") {
421
+ finishActiveTextStream();
422
+ dispatchTranscript({
423
+ type: "receiveUser",
424
+ item,
425
+ presentation: transcriptPresentation,
426
+ });
427
+ }
428
+ else {
429
+ commitItems([item]);
430
+ }
431
+ return;
432
+ }
433
+ if (transcriptClass === "error" && evt.type === "error") {
434
+ showError(evt.message, evt.detail);
435
+ return;
436
+ }
437
+ applyRuntimeAction({ type: "event", event: evt, rawEvent });
438
+ if (evt.type === "spotify_mode" || evt.type === "provider_mode" || evt.type === "spotify_setup" || evt.type === "auth_setup") {
439
+ applyProviderAction({ type: "event", event: evt });
440
+ }
441
+ switch (evt.type) {
442
+ case "session_state":
443
+ break;
444
+ case "usage_state":
445
+ break;
446
+ case "agent_working_state":
447
+ break;
448
+ case "activity":
449
+ if (runtimeStateRef.current.launchPreparing)
450
+ setLaunchPreparingFrame(0);
451
+ break;
452
+ case "status":
453
+ break;
454
+ case "input_state":
455
+ if (evt.disabled && evt.reason === "recommendation") {
456
+ setInput("");
457
+ setInputRevision((prev) => prev + 1);
458
+ setSlashMenuDismissedFor(null);
459
+ }
460
+ break;
461
+ case "queue":
462
+ setQueueItems(evt.tracks);
463
+ setTrackPanel((current) => current ? { ...current, tracks: markQueuedTracks(current.panel === "queue" ? evt.tracks : current.tracks, evt.tracks) } : current);
464
+ break;
465
+ case "track_panel":
466
+ setTrackPanel({
467
+ panel: evt.panel,
468
+ title: evt.title,
469
+ hint: evt.hint,
470
+ tracks: markQueuedTracks(evt.tracks, queueItems),
471
+ });
472
+ setTrackPanelIndex(0);
473
+ switchRegion("trackPanel");
474
+ break;
475
+ case "memory_panel":
476
+ setMemoryPanel({
477
+ view: evt.view,
478
+ target: evt.target,
479
+ title: evt.title,
480
+ hint: evt.hint,
481
+ readOnly: Boolean(evt.read_only),
482
+ entries: evt.entries ?? [],
483
+ settings: evt.settings,
484
+ });
485
+ setMemoryPanelIndex(0);
486
+ setMemorySearchQuery("");
487
+ setMemoryEditor(null);
488
+ switchRegion("memoryPanel");
489
+ break;
490
+ case "extension_panel": {
491
+ const selected = evt.selected_extension ?? null;
492
+ setExtensionPanel({
493
+ view: evt.view,
494
+ title: evt.title,
495
+ hint: evt.hint,
496
+ selectedExtension: selected,
497
+ extensions: evt.extensions,
498
+ detail: evt.detail,
499
+ setup: evt.setup,
500
+ });
501
+ const nextIndex = evt.view === "detail"
502
+ ? (() => {
503
+ const detailActions = evt.detail?.actions ?? [];
504
+ const focused = evt.detail?.selected_action;
505
+ const focusedIndex = focused ? detailActions.indexOf(focused) : 0;
506
+ return focusedIndex >= 0 ? focusedIndex : 0;
507
+ })()
508
+ : evt.view === "setup"
509
+ ? evt.setup?.dependencies && evt.setup.selected_dependency
510
+ ? Math.max(0, evt.setup.dependencies.findIndex((dependency) => dependency.id === evt.setup?.selected_dependency))
511
+ : 0
512
+ : selected
513
+ ? evt.extensions.findIndex((extension) => extension.id === selected)
514
+ : 0;
515
+ setExtensionPanelIndex(Math.max(0, nextIndex));
516
+ setExtensionInputFocused(false);
517
+ applyPanelLifecycle("extension_event");
518
+ break;
519
+ }
520
+ case "search_results": {
521
+ setSearchItems(evt.tracks);
522
+ const first = evt.tracks[0];
523
+ if (first) {
524
+ setPlayer({
525
+ name: first.title || first.name || "-",
526
+ artist: first.artist || "-",
527
+ album: first.album || "-",
528
+ duration_ms: first.duration_ms || 0,
529
+ progress_ms: 0,
530
+ is_playing: false,
531
+ });
532
+ coverUrlRef.current = first.album_cover_url ?? null;
533
+ setCoverUrl(first.album_cover_url ?? null);
534
+ setCoverPattern(null);
535
+ }
536
+ break;
537
+ }
538
+ case "player":
539
+ setPlayer(evt.state);
540
+ const nextShellState = reduceShellState(shellStateRef.current, {
541
+ type: "player_event",
542
+ player: evt.state,
543
+ spotifyModeEnabled: false,
544
+ providerMode: providerModeRef.current.enabled && providerModeRef.current.provider !== "normal"
545
+ ? providerModeRef.current.provider
546
+ : null,
547
+ });
548
+ if (nextShellState.region !== shellStateRef.current.region) {
549
+ switchRegion(nextShellState.region);
550
+ }
551
+ applyShellAction({ type: "replace", state: nextShellState });
552
+ break;
553
+ case "spotify_mode":
554
+ if (!evt.enabled && shellStateRef.current.region === "spotifyImmersive") {
555
+ switchRegion("chat");
556
+ }
557
+ break;
558
+ case "provider_mode": {
559
+ if (!evt.enabled && shellStateRef.current.region === "providerImmersive") {
560
+ switchRegion("chat");
561
+ }
562
+ break;
563
+ }
564
+ case "cover":
565
+ coverUrlRef.current = evt.url;
566
+ setCoverUrl(evt.url);
567
+ setCoverPattern(null);
568
+ break;
569
+ case "cover_pattern":
570
+ setCoverPattern((prev) => {
571
+ if (evt.source_url !== coverUrlRef.current)
572
+ return prev;
573
+ return evt;
574
+ });
575
+ break;
576
+ case "cover_pattern_unavailable":
577
+ setCoverPattern((prev) => {
578
+ if (evt.source_url !== coverUrlRef.current)
579
+ return prev;
580
+ return {
581
+ type: "cover_pattern",
582
+ source_url: evt.source_url,
583
+ palette: [],
584
+ variants: {},
585
+ unavailable_reason: evt.reason,
586
+ };
587
+ });
588
+ break;
589
+ case "confirm":
590
+ if (dismissedConfirmIdsRef.current.has(evt.id)) {
591
+ break;
592
+ }
593
+ setInput("");
594
+ switchRegion("chat");
595
+ setConfirm({
596
+ id: evt.id,
597
+ tool_name: evt.tool_name,
598
+ tool_args: evt.tool_args ?? {},
599
+ message: evt.message || `Confirm ${evt.tool_name}`,
600
+ warning: evt.warning,
601
+ hide_hint: evt.hide_hint === true,
602
+ choices: evt.choices && evt.choices.length > 0 ? evt.choices : DEFAULT_CONFIRM_CHOICES,
603
+ variant: evt.variant,
604
+ commands: evt.commands ?? [],
605
+ page_index: evt.page_index,
606
+ page_count: evt.page_count,
607
+ });
608
+ if (evt.tool_args?.preserve_selection !== true) {
609
+ setConfirmIndex(0);
610
+ }
611
+ break;
612
+ case "confirm_dismiss": {
613
+ const currentConfirm = confirmRef.current;
614
+ dismissedConfirmIdsRef.current.add(evt.id);
615
+ if (currentConfirm?.id === evt.id) {
616
+ setConfirm(null);
617
+ }
618
+ break;
619
+ }
620
+ case "spotify_setup":
621
+ applyPanelLifecycle("setup_event");
622
+ if (evt.active !== false) {
623
+ switchRegion("chat");
624
+ }
625
+ break;
626
+ case "auth_setup":
627
+ applyPanelLifecycle("setup_event");
628
+ if (evt.active === false && evt.step === "model") {
629
+ setInput("");
630
+ break;
631
+ }
632
+ if (evt.active !== false) {
633
+ switchRegion("chat");
634
+ }
635
+ if (evt.step === "provider") {
636
+ const providers = evt.providers ?? [];
637
+ setLoginSelectionIndex(resolveLoginProviderSelectionIndex(providers, evt.provider));
638
+ }
639
+ else {
640
+ setLoginSelectionIndex(0);
641
+ }
642
+ if (evt.step === "model") {
643
+ setInput("");
644
+ }
645
+ break;
646
+ case "auth_state":
647
+ const nextAuthState = {
648
+ ready: evt.ready,
649
+ provider: evt.provider,
650
+ model: evt.model,
651
+ model_label: evt.model_label,
652
+ auth_type: evt.auth_type,
653
+ credential_source: evt.credential_source,
654
+ reason: evt.reason,
655
+ };
656
+ setAuthState(nextAuthState);
657
+ if (!startupInfoCapturedRef.current) {
658
+ startupInfoCapturedRef.current = true;
659
+ commitItems([createInfoBannerItem(nextAuthState, RUNTIME_WORKING_DIRECTORY, sessionIdRef.current, { showLogo: true })]);
660
+ }
661
+ break;
662
+ case "help_panel":
663
+ switchRegion("chat");
664
+ applyPanelLifecycle("help_event");
665
+ setHelpPanel({
666
+ title: evt.title,
667
+ hint: evt.hint,
668
+ commands: helpCommandsForLanguage(evt.commands, language),
669
+ });
670
+ break;
671
+ case "bye":
672
+ setIsExiting(true);
673
+ switchRegion("chat");
674
+ applyPanelLifecycle("bye");
675
+ setTimeout(() => exit(), 80);
676
+ break;
677
+ }
678
+ }, [applyPanelLifecycle, applyRuntimeAction, applyShellAction, commitItems, exit, finishActiveTextStream, language, queueItems, showError, startTextStream, switchRegion, transcriptPresentation]);
679
+ const { send } = useSonexSocket({
680
+ url: wsUrl,
681
+ onEvent,
682
+ onClientError: (message, detail) => showError(language === "zh-CN" && message.startsWith("Sonex API is not running")
683
+ ? `${t(language, "api.notRunning.message")}。 ${t(language, "api.notRunning.detail")}`
684
+ : message, detail),
685
+ });
686
+ React.useEffect(() => {
687
+ if (!rawModeAvailable)
688
+ return;
689
+ const handlePlaybackShortcut = (chunk) => {
690
+ const action = playbackShortcutFromInput(chunk.toString("utf8"));
691
+ if (!action)
692
+ return;
693
+ if (!shellStateRef.current.playbackSessionActive)
694
+ return;
695
+ if (confirmRef.current)
696
+ return;
697
+ if (spotifySetupActiveRef.current)
698
+ return;
699
+ if (authSetupActiveRef.current)
700
+ return;
701
+ if (slashMenuActiveRef.current)
702
+ return;
703
+ const localShortcut = shellStateRef.current.region === "miniPlayer"
704
+ && isLocalPlaybackShortcutSource(playerRef.current);
705
+ const spotifyShortcut = shellStateRef.current.region === "spotifyImmersive"
706
+ && spotifyModeRef.current.enabled
707
+ && action === "togglePlayback"
708
+ && isSpotifyPlaybackShortcutSource(playerRef.current);
709
+ const providerShortcut = shellStateRef.current.region === "providerImmersive"
710
+ && providerModeRef.current.enabled
711
+ && action === "togglePlayback"
712
+ && providerModeRef.current.provider === "spotify"
713
+ && isSpotifyPlaybackShortcutSource(playerRef.current);
714
+ if (!localShortcut && !spotifyShortcut && !providerShortcut)
715
+ return;
716
+ const command = playbackCommandForShortcut(action, playerRef.current);
717
+ send({ type: "internal_command", text: command });
718
+ };
719
+ process.stdin.on("data", handlePlaybackShortcut);
720
+ return () => {
721
+ process.stdin.off("data", handlePlaybackShortcut);
722
+ };
723
+ }, [rawModeAvailable, send]);
724
+ const requestSafeExit = React.useCallback((reason) => {
725
+ if (isExiting)
726
+ return;
727
+ setIsExiting(true);
728
+ setInput("");
729
+ switchRegion("chat");
730
+ setSlashMenuDismissedFor(null);
731
+ applyPanelLifecycle("safe_exit");
732
+ applyRuntimeAction({ type: "set_status", text: t(language, "status.saving") });
733
+ applyRuntimeAction({ type: "event", event: {
734
+ type: "activity",
735
+ id: "bye_saving",
736
+ kind: "status",
737
+ title: "Saving session",
738
+ detail: "Writing transcript before exit.",
739
+ status: "pending",
740
+ timestamp: Date.now(),
741
+ } });
742
+ const transcriptItems = allTranscriptItems(transcript);
743
+ const sent = send({ type: "bye", messages: chatMessagesForTranscript(transcriptItems), reason });
744
+ if (!sent) {
745
+ setIsExiting(false);
746
+ showError("Session could not be saved before exit.", "The Sonex API connection is not open.");
747
+ }
748
+ }, [applyPanelLifecycle, applyRuntimeAction, isExiting, language, send, showError, switchRegion, transcript]);
749
+ const loginChoices = authSetup?.step === "provider"
750
+ ? authSetup.providers ?? []
751
+ : authSetup?.step === "method"
752
+ ? authSetup.methods ?? []
753
+ : authSetup?.step === "model"
754
+ ? authSetup.models ?? []
755
+ : [];
756
+ const submitLoginChoice = React.useCallback(() => {
757
+ if (!authSetup?.active)
758
+ return;
759
+ const choices = authSetup.step === "provider"
760
+ ? authSetup.providers ?? []
761
+ : authSetup.step === "method"
762
+ ? authSetup.methods ?? []
763
+ : authSetup.step === "model"
764
+ ? authSetup.models ?? []
765
+ : [];
766
+ const choice = choices[Math.min(loginSelectionIndex, Math.max(0, choices.length - 1))];
767
+ if (choice) {
768
+ send({ type: "auth_setup_input", value: choice.value });
769
+ }
770
+ }, [authSetup, loginSelectionIndex, send]);
771
+ const submitLoginApiKey = React.useCallback((value) => {
772
+ const text = value.trim();
773
+ if (!text)
774
+ return;
775
+ setLoginApiKeyInput("");
776
+ send({ type: "auth_setup_input", value: text });
777
+ }, [send]);
778
+ const applySlashCompletion = React.useCallback((command) => {
779
+ setInput(completeSlashCommand(command));
780
+ setInputRevision((prev) => prev + 1);
781
+ setSlashMenuDismissedFor(null);
782
+ }, []);
783
+ const submitInput = React.useCallback((value) => {
784
+ if (recommendInputLocked)
785
+ return;
786
+ const route = resolveInputRoute(value, {
787
+ confirm,
788
+ selectedConfirmChoice,
789
+ selectableConfirmChoices,
790
+ extensionPanelActive: Boolean(extensionPanel),
791
+ extensionInputFocused,
792
+ extensionSetupInput: extensionSetupInput ?? null,
793
+ authSetupActive: Boolean(authSetup?.active),
794
+ spotifySetupActive: Boolean(spotifySetup?.active),
795
+ selectedSlashCommand,
796
+ });
797
+ if (route.type === "empty")
798
+ return;
799
+ finishActiveTextStream();
800
+ switch (route.type) {
801
+ case "ignore":
802
+ return;
803
+ case "confirm":
804
+ setInput("");
805
+ send({ type: "confirm_result", id: confirm.id, decision: route.decision });
806
+ setConfirm(null);
807
+ return;
808
+ case "extension_input":
809
+ setInput("");
810
+ setExtensionInputFocused(false);
811
+ send({ type: "extension_panel_input", value: route.value });
812
+ return;
813
+ case "safe_exit":
814
+ requestSafeExit(route.reason);
815
+ return;
816
+ case "info":
817
+ setInput("");
818
+ setSlashMenuDismissedFor(null);
819
+ applyPanelLifecycle("info");
820
+ commitItems([createInfoBannerItem(authState, RUNTIME_WORKING_DIRECTORY, sessionIdRef.current)]);
821
+ return;
822
+ case "slash_completion":
823
+ applySlashCompletion(route.command);
824
+ setSlashIndex(0);
825
+ return;
826
+ case "unknown_slash":
827
+ setInput("");
828
+ setSlashMenuDismissedFor(null);
829
+ appendUnknownCommandWarning(route.value);
830
+ return;
831
+ case "setup_input":
832
+ setInput("");
833
+ setSlashMenuDismissedFor(null);
834
+ setLanguagePanel(null);
835
+ send({ type: route.channel === "spotify" ? "setup_input" : "auth_setup_input", value: route.value });
836
+ return;
837
+ case "user_input": {
838
+ setInput("");
839
+ setSlashMenuDismissedFor(null);
840
+ if (route.command?.name !== "help") {
841
+ setHelpPanel(null);
842
+ setHelpPanelIndex(0);
843
+ }
844
+ setLanguagePanel(null);
845
+ const sent = send({ type: "user_input", text: route.value });
846
+ if (!sent)
847
+ showError(API_NOT_RUNNING_MESSAGE, API_NOT_RUNNING_DETAIL);
848
+ return;
849
+ }
850
+ }
851
+ }, [applyPanelLifecycle, applySlashCompletion, appendUnknownCommandWarning, authState, commitItems, confirm, extensionInputFocused, extensionPanel, extensionSetupInput, finishActiveTextStream, recommendInputLocked, requestSafeExit, selectableConfirmChoices, selectedConfirmChoice, selectedSlashCommand, send, showError, authSetup?.active, spotifySetup?.active]);
852
+ useInput((inputKey, key) => {
853
+ if (!extensionPanel)
854
+ return;
855
+ if (key.escape) {
856
+ setExtensionInputFocused(false);
857
+ setInput("");
858
+ if (extensionPanel.view === "list") {
859
+ setExtensionPanel(null);
860
+ send({ type: "extension_panel_action", action: "close" });
861
+ }
862
+ else {
863
+ send({
864
+ type: "extension_panel_action",
865
+ action: "back",
866
+ extension_id: extensionPanel.selectedExtension ?? undefined,
867
+ });
868
+ }
869
+ return;
870
+ }
871
+ if (extensionPanel.view === "list") {
872
+ if (key.upArrow) {
873
+ setExtensionPanelIndex((prev) => (prev - 1 + extensionPanel.extensions.length) % extensionPanel.extensions.length);
874
+ }
875
+ else if (key.downArrow) {
876
+ setExtensionPanelIndex((prev) => (prev + 1) % extensionPanel.extensions.length);
877
+ }
878
+ else if (key.return) {
879
+ const selected = extensionPanel.extensions[extensionPanelIndex];
880
+ if (selected)
881
+ send({ type: "extension_panel_action", action: "open_detail", extension_id: selected.id });
882
+ }
883
+ return;
884
+ }
885
+ if (extensionPanel.view === "setup") {
886
+ if (extensionPanel.setup?.dependencies && extensionPanel.setup.dependencies.length > 0) {
887
+ if (key.upArrow) {
888
+ setExtensionPanelIndex((prev) => (prev - 1 + extensionPanel.setup.dependencies.length) % extensionPanel.setup.dependencies.length);
889
+ }
890
+ else if (key.downArrow) {
891
+ setExtensionPanelIndex((prev) => (prev + 1) % extensionPanel.setup.dependencies.length);
892
+ }
893
+ else if (key.return) {
894
+ const dependency = extensionPanel.setup.dependencies[extensionPanelIndex];
895
+ if (dependency && dependency.state !== "installed") {
896
+ send({ type: "extension_panel_action", action: "install_dependency", extension_id: extensionPanel.selectedExtension ?? undefined, dependency_id: dependency.id });
897
+ }
898
+ }
899
+ }
900
+ else if (key.leftArrow) {
901
+ send({ type: "extension_panel_action", action: "prev_page", extension_id: extensionPanel.selectedExtension ?? undefined });
902
+ }
903
+ else if (key.rightArrow) {
904
+ send({ type: "extension_panel_action", action: "next_page", extension_id: extensionPanel.selectedExtension ?? undefined });
905
+ }
906
+ else if (key.return) {
907
+ if (extensionPanel.setup?.input && !extensionInputFocused) {
908
+ setExtensionInputFocused(true);
909
+ }
910
+ else if (!extensionPanel.setup?.input) {
911
+ send({ type: "extension_panel_input", value: "" });
912
+ }
913
+ }
914
+ return;
915
+ }
916
+ const detail = extensionPanel.detail;
917
+ const extension = extensionPanel.extensions.find((item) => item.id === extensionPanel.selectedExtension);
918
+ if (!detail || !extension)
919
+ return;
920
+ const actions = detail.actions ?? [];
921
+ if (actions.length === 0)
922
+ return;
923
+ if (key.upArrow) {
924
+ setExtensionPanelIndex((prev) => (prev - 1 + actions.length) % actions.length);
925
+ }
926
+ else if (key.downArrow) {
927
+ setExtensionPanelIndex((prev) => (prev + 1) % actions.length);
928
+ }
929
+ else if (key.return) {
930
+ const selectedAction = actions[Math.min(extensionPanelIndex, Math.max(0, actions.length - 1))];
931
+ if (selectedAction)
932
+ send({
933
+ type: "extension_panel_action",
934
+ action: selectedAction,
935
+ extension_id: extension.id,
936
+ token: selectedAction === "confirm_reset" || selectedAction === "confirm_restart" ? detail.armed_token : undefined,
937
+ revision: detail.revision,
938
+ });
939
+ }
940
+ }, { isActive: rawModeAvailable && Boolean(extensionPanel) && !confirm });
941
+ useInput((inputKey, key) => {
942
+ if (key.ctrl && inputKey === "c") {
943
+ requestSafeExit("ctrl_c");
944
+ }
945
+ }, { isActive: rawModeAvailable });
946
+ useInput((inputKey, key) => {
947
+ if (!isLoginScreenActive)
948
+ return;
949
+ if (key.escape) {
950
+ applyProviderAction({ type: "clear_auth_setup" });
951
+ setLoginSelectionIndex(0);
952
+ setLoginApiKeyInput("");
953
+ send({ type: "auth_setup_input", value: "__cancel__" });
954
+ return;
955
+ }
956
+ if (authSetup?.step === "api_key")
957
+ return;
958
+ if ((authSetup?.step === "provider" || authSetup?.step === "method") && loginChoices.length > 0) {
959
+ if (key.upArrow) {
960
+ setLoginSelectionIndex((prev) => (prev - 1 + loginChoices.length) % loginChoices.length);
961
+ }
962
+ else if (key.downArrow) {
963
+ setLoginSelectionIndex((prev) => (prev + 1) % loginChoices.length);
964
+ }
965
+ else if (key.return) {
966
+ submitLoginChoice();
967
+ }
968
+ }
969
+ }, { isActive: rawModeAvailable && isLoginScreenActive });
970
+ useInput((_inputKey, key) => {
971
+ if (!spotifySetup?.active || !key.escape)
972
+ return;
973
+ applyProviderAction({ type: "clear_spotify_setup" });
974
+ setInput("");
975
+ send({ type: "setup_input", value: "__cancel__" });
976
+ }, { isActive: rawModeAvailable && Boolean(spotifySetup?.active) });
977
+ useInput((inputKey, key) => {
978
+ if (!isModelPanelActive)
979
+ return;
980
+ const choices = filterModelChoices(authSetup?.models ?? [], input);
981
+ if (key.upArrow && choices.length > 0) {
982
+ setLoginSelectionIndex((prev) => (prev - 1 + choices.length) % choices.length);
983
+ }
984
+ else if (key.downArrow && choices.length > 0) {
985
+ setLoginSelectionIndex((prev) => (prev + 1) % choices.length);
986
+ }
987
+ else if (key.return && choices.length > 0) {
988
+ const choice = choices[Math.min(loginSelectionIndex, Math.max(0, choices.length - 1))];
989
+ if (choice) {
990
+ send({ type: "auth_setup_input", value: choice.value });
991
+ setInput("");
992
+ }
993
+ }
994
+ else if (key.escape) {
995
+ applyProviderAction({ type: "clear_auth_setup" });
996
+ setLoginSelectionIndex(0);
997
+ setInput("");
998
+ send({ type: "auth_setup_input", value: "__cancel__" });
999
+ }
1000
+ else if (key.backspace || key.delete) {
1001
+ setInput((previous) => previous.slice(0, -1));
1002
+ setLoginSelectionIndex(0);
1003
+ }
1004
+ else if (inputKey
1005
+ && !key.ctrl
1006
+ && !key.meta
1007
+ && !key.return
1008
+ && !key.upArrow
1009
+ && !key.downArrow) {
1010
+ setInput((previous) => previous + inputKey);
1011
+ setLoginSelectionIndex(0);
1012
+ }
1013
+ }, { isActive: rawModeAvailable && Boolean(isModelPanelActive) });
1014
+ useInput((inputKey, key) => {
1015
+ if (!isSlashMenuActive || !selectedSlashCommand)
1016
+ return;
1017
+ if (key.upArrow) {
1018
+ setSlashIndex((prev) => (prev - 1 + slashSuggestions.length) % slashSuggestions.length);
1019
+ }
1020
+ else if (key.downArrow) {
1021
+ setSlashIndex((prev) => (prev + 1) % slashSuggestions.length);
1022
+ }
1023
+ else if (key.tab || inputKey === "\t") {
1024
+ applySlashCompletion(selectedSlashCommand);
1025
+ }
1026
+ else if (key.return) {
1027
+ const command = matchingSlashCommand(input);
1028
+ if (!command || (command.needsArgument && !hasSlashCommandArguments(input))) {
1029
+ applySlashCompletion(selectedSlashCommand);
1030
+ }
1031
+ }
1032
+ else if (key.escape) {
1033
+ setSlashMenuDismissedFor(input);
1034
+ }
1035
+ }, { isActive: isSlashMenuActive });
1036
+ useInput((inputKey, key) => {
1037
+ if (!isUnknownSlashInput)
1038
+ return;
1039
+ if (key.tab || inputKey === "\t") {
1040
+ appendUnknownCommandWarning(input);
1041
+ }
1042
+ }, { isActive: isUnknownSlashInput });
1043
+ useInput((inputKey, key) => {
1044
+ if (!languagePanel?.active)
1045
+ return;
1046
+ if (key.escape) {
1047
+ setLanguagePanel(null);
1048
+ }
1049
+ }, { isActive: Boolean(languagePanel?.active) && rawModeAvailable });
1050
+ useInput((inputKey, key) => {
1051
+ if (spotifySetup && spotifySetup.active === false && key.escape) {
1052
+ applyProviderAction({ type: "clear_spotify_setup" });
1053
+ }
1054
+ else if (authSetup && authSetup.active === false && key.escape) {
1055
+ applyProviderAction({ type: "clear_auth_setup" });
1056
+ }
1057
+ }, { isActive: rawModeAvailable && (Boolean(spotifySetup && spotifySetup.active === false) || Boolean(authSetup && authSetup.active === false)) });
1058
+ useInput((inputKey, key) => {
1059
+ if (!confirm)
1060
+ return;
1061
+ if (key.upArrow) {
1062
+ setInput("");
1063
+ setConfirmIndex((prev) => Math.max(0, prev - 1));
1064
+ }
1065
+ else if (key.downArrow) {
1066
+ setInput("");
1067
+ setConfirmIndex((prev) => selectableConfirmChoices.length > 0 ? Math.min(selectableConfirmChoices.length - 1, prev + 1) : 0);
1068
+ }
1069
+ else if (key.return) {
1070
+ if (selectableConfirmChoices.length === 0)
1071
+ return;
1072
+ if (selectedConfirmChoice?.input)
1073
+ return;
1074
+ send({
1075
+ type: "confirm_result",
1076
+ id: confirm.id,
1077
+ decision: selectedConfirmChoice?.value ?? "allow_once",
1078
+ });
1079
+ setConfirm(null);
1080
+ }
1081
+ else if (key.escape) {
1082
+ dismissedConfirmIdsRef.current.add(confirm.id);
1083
+ send({ type: "confirm_result", id: confirm.id, decision: "deny" });
1084
+ setConfirm(null);
1085
+ }
1086
+ }, {
1087
+ isActive: Boolean(confirm)
1088
+ && rawModeAvailable
1089
+ && !authSetup?.active
1090
+ && !spotifySetup?.active,
1091
+ });
1092
+ useInput((inputKey, key) => {
1093
+ if (!helpPanel || confirm || isSlashMenuActive || languagePanel?.active)
1094
+ return;
1095
+ if (key.upArrow && helpPanel.commands.length > 0) {
1096
+ setHelpPanelIndex((prev) => (prev - 1 + helpPanel.commands.length) % helpPanel.commands.length);
1097
+ }
1098
+ else if (key.downArrow && helpPanel.commands.length > 0) {
1099
+ setHelpPanelIndex((prev) => (prev + 1) % helpPanel.commands.length);
1100
+ }
1101
+ else if (key.return && helpPanel.commands.length > 0) {
1102
+ const selectedHelpPanelItem = selectedHelpPanelCommand(helpPanel.commands, helpPanelIndex);
1103
+ const selectedHelpCommand = selectedHelpPanelItem ? matchingSlashCommand(`/${selectedHelpPanelItem.name}`) : null;
1104
+ if (selectedHelpCommand) {
1105
+ setHelpPanel(null);
1106
+ setHelpPanelIndex(0);
1107
+ setInput(completeSlashCommand(selectedHelpCommand));
1108
+ setInputRevision((prev) => prev + 1);
1109
+ }
1110
+ }
1111
+ else if (key.escape) {
1112
+ setHelpPanel(null);
1113
+ setHelpPanelIndex(0);
1114
+ }
1115
+ }, { isActive: Boolean(helpPanel) && rawModeAvailable && !confirm && !isSlashMenuActive && !languagePanel?.active });
1116
+ useInput((inputKey, key) => {
1117
+ if (activeRegion !== "trackPanel" || !trackPanel || confirm || isSlashMenuActive || languagePanel?.active || isModelPanelActive)
1118
+ return;
1119
+ const selectedTrackPanelTrack = trackPanel.tracks[Math.min(trackPanelIndex, Math.max(0, trackPanel.tracks.length - 1))] ?? null;
1120
+ if (key.escape) {
1121
+ setTrackPanel(null);
1122
+ setTrackPanelIndex(0);
1123
+ switchRegion("chat");
1124
+ }
1125
+ else if (isTrackPanelQueueShortcut(inputKey, key) && selectedTrackPanelTrack) {
1126
+ send({ type: "track_panel_action", action: "queue_add", track: selectedTrackPanelTrack, panel: trackPanel.panel, title: trackPanel.title });
1127
+ }
1128
+ else if (key.return && selectedTrackPanelTrack) {
1129
+ setTrackPanel(null);
1130
+ setTrackPanelIndex(0);
1131
+ switchRegion("chat");
1132
+ send({ type: "track_panel_action", action: "play", track: selectedTrackPanelTrack, panel: trackPanel.panel, title: trackPanel.title });
1133
+ }
1134
+ else if (key.upArrow) {
1135
+ setTrackPanelIndex((prev) => Math.max(0, prev - 1));
1136
+ }
1137
+ else if (key.downArrow) {
1138
+ setTrackPanelIndex((prev) => Math.min(trackPanel.tracks.length - 1, prev + 1));
1139
+ }
1140
+ }, {
1141
+ isActive: activeRegion === "trackPanel"
1142
+ && Boolean(trackPanel)
1143
+ && rawModeAvailable
1144
+ && !confirm
1145
+ && !isSlashMenuActive
1146
+ && !languagePanel?.active
1147
+ && !isModelPanelActive,
1148
+ });
1149
+ useInput((inputKey, key) => {
1150
+ if (activeRegion !== "memoryPanel" || !memoryPanel || confirm)
1151
+ return;
1152
+ const visibleEntries = memoryPanel.entries.filter((entry) => entry.content.toLocaleLowerCase().includes(memorySearchQuery.toLocaleLowerCase()));
1153
+ const count = memoryPanel.view === "root" ? 2 : memoryPanel.view === "sources" || memoryPanel.view === "format" ? 3 : memoryPanel.view === "settings" ? 8 : visibleEntries.length;
1154
+ const selected = visibleEntries[Math.min(memoryPanelIndex, Math.max(0, visibleEntries.length - 1))] ?? null;
1155
+ if (memoryEditor) {
1156
+ if (key.escape) {
1157
+ setMemoryEditor(null);
1158
+ }
1159
+ else if (memoryEditor.mode === "search" && key.return) {
1160
+ setMemorySearchQuery(memoryEditor.value);
1161
+ setMemoryPanelIndex(0);
1162
+ setMemoryEditor(null);
1163
+ }
1164
+ else if (memoryEditor.mode === "setting" && key.return) {
1165
+ const settingValue = memoryEditor.value.trim();
1166
+ if (memoryEditor.settingKey && settingValue) {
1167
+ send({
1168
+ type: "memory_panel_action",
1169
+ action: "setting",
1170
+ entry_id: memoryEditor.settingKey,
1171
+ value: settingValue,
1172
+ });
1173
+ }
1174
+ setMemoryEditor(null);
1175
+ }
1176
+ else if (memoryEditor.mode !== "search" && key.ctrl && inputKey.toLowerCase() === "s") {
1177
+ if (memoryEditor.value.trim()) {
1178
+ send({
1179
+ type: "memory_panel_action",
1180
+ action: memoryEditor.mode,
1181
+ target: memoryPanel.target ?? undefined,
1182
+ entry_id: memoryEditor.mode === "edit" ? selected?.entry_id : undefined,
1183
+ content: memoryEditor.value,
1184
+ });
1185
+ }
1186
+ setMemoryEditor(null);
1187
+ }
1188
+ else if (memoryEditor.mode !== "search" && memoryEditor.mode !== "setting" && key.return) {
1189
+ setMemoryEditor((current) => current ? { ...current, value: `${current.value}\n` } : current);
1190
+ }
1191
+ else if (key.backspace || key.delete) {
1192
+ setMemoryEditor((current) => current ? { ...current, value: Array.from(current.value).slice(0, -1).join("") } : current);
1193
+ }
1194
+ else if (inputKey && !key.ctrl && !key.meta) {
1195
+ setMemoryEditor((current) => current ? { ...current, value: current.value + inputKey } : current);
1196
+ }
1197
+ return;
1198
+ }
1199
+ if (key.escape) {
1200
+ if (memorySearchQuery) {
1201
+ setMemorySearchQuery("");
1202
+ setMemoryPanelIndex(0);
1203
+ return;
1204
+ }
1205
+ if (memoryPanel.view === "detail") {
1206
+ send({ type: "memory_panel_action", action: "open", target: memoryPanel.target ?? undefined });
1207
+ }
1208
+ else if (memoryPanel.view === "revisions") {
1209
+ send({
1210
+ type: "memory_panel_action",
1211
+ action: "detail",
1212
+ target: memoryPanel.target ?? undefined,
1213
+ entry_id: String(memoryPanel.settings?.entry_id ?? ""),
1214
+ });
1215
+ }
1216
+ else if (memoryPanel.view === "entries") {
1217
+ send({ type: "memory_panel_action", action: "sources" });
1218
+ }
1219
+ else if (memoryPanel.view === "sources" || memoryPanel.view === "format") {
1220
+ send({ type: "memory_panel_action", action: "root" });
1221
+ }
1222
+ else {
1223
+ send({ type: "memory_panel_action", action: "close" });
1224
+ setMemoryPanel(null);
1225
+ switchRegion("chat");
1226
+ }
1227
+ }
1228
+ else if (key.upArrow) {
1229
+ setMemoryPanelIndex((current) => Math.max(0, current - 1));
1230
+ }
1231
+ else if (key.downArrow) {
1232
+ setMemoryPanelIndex((current) => Math.min(Math.max(0, count - 1), current + 1));
1233
+ }
1234
+ else if (key.return) {
1235
+ if (memoryPanel.view === "root" && memoryPanelIndex === 0) {
1236
+ send({ type: "memory_panel_action", action: "sources" });
1237
+ }
1238
+ else if (memoryPanel.view === "root" && memoryPanelIndex === 1) {
1239
+ send({ type: "memory_panel_action", action: "format_scopes" });
1240
+ }
1241
+ else if (memoryPanel.view === "sources") {
1242
+ const target = ["user", "memory", "dump"][memoryPanelIndex];
1243
+ send({ type: "memory_panel_action", action: "open", target });
1244
+ }
1245
+ else if (memoryPanel.view === "format") {
1246
+ const target = ["user", "memory", "all"][memoryPanelIndex];
1247
+ send({ type: "memory_panel_action", action: "format_confirm", target });
1248
+ }
1249
+ else if (memoryPanel.view === "settings" && !memoryPanel.readOnly) {
1250
+ const settings = memoryPanel.settings ?? {};
1251
+ const keys = [
1252
+ "forget_retention_days", "user_capacity", "memory_capacity", "automatic_forgetting",
1253
+ "idle_threshold_days", "automatic_refinement", "user_refinement_window", "memory_refinement_window",
1254
+ ];
1255
+ const keyName = keys[memoryPanelIndex];
1256
+ if (keyName?.includes("capacity") || keyName?.includes("refinement_window")) {
1257
+ const currentValue = settings[keyName];
1258
+ setMemoryEditor({
1259
+ mode: "setting",
1260
+ settingKey: keyName,
1261
+ value: currentValue == null ? "Unlimited" : String(currentValue),
1262
+ });
1263
+ return;
1264
+ }
1265
+ const nextValue = keyName === "forget_retention_days"
1266
+ ? { 1: 3, 3: 7, 7: 1 }[Number(settings[keyName] ?? 7)]
1267
+ : keyName === "idle_threshold_days"
1268
+ ? { 7: 15, 15: 30, 30: 7 }[Number(settings[keyName] ?? 30)]
1269
+ : keyName === "automatic_refinement"
1270
+ ? settings[keyName] === false
1271
+ : keyName === "automatic_forgetting"
1272
+ ? { off: "idle", idle: "capacity", capacity: "idle_capacity", idle_capacity: "off" }[String(settings[keyName] ?? "off")]
1273
+ : settings[keyName];
1274
+ if (keyName)
1275
+ send({ type: "memory_panel_action", action: "setting", entry_id: keyName, value: nextValue });
1276
+ }
1277
+ else if (memoryPanel.view === "entries" && selected) {
1278
+ send({ type: "memory_panel_action", action: "detail", target: memoryPanel.target ?? undefined, entry_id: selected.entry_id });
1279
+ }
1280
+ else if (memoryPanel.view === "revisions" && selected && !memoryPanel.readOnly) {
1281
+ send({
1282
+ type: "memory_panel_action",
1283
+ action: "restore_revision",
1284
+ target: memoryPanel.target ?? undefined,
1285
+ entry_id: String(memoryPanel.settings?.entry_id ?? ""),
1286
+ value: Number(selected.entry_id),
1287
+ });
1288
+ }
1289
+ }
1290
+ else if (inputKey.toLowerCase() === "f" && selected && !memoryPanel.readOnly && memoryPanel.target !== "dump") {
1291
+ send({ type: "memory_panel_action", action: "forget", target: memoryPanel.target ?? undefined, entry_id: selected.entry_id });
1292
+ }
1293
+ else if (inputKey.toLowerCase() === "r" && selected && !memoryPanel.readOnly && memoryPanel.target === "dump") {
1294
+ send({ type: "memory_panel_action", action: "recall", target: "dump", entry_id: selected.entry_id });
1295
+ }
1296
+ else if (inputKey === "/" && memoryPanel.view === "entries") {
1297
+ setMemoryEditor({ mode: "search", value: memorySearchQuery });
1298
+ }
1299
+ else if (inputKey.toLowerCase() === "a" && memoryPanel.view === "entries" && memoryPanel.target !== "dump" && !memoryPanel.readOnly) {
1300
+ setMemoryEditor({ mode: "add", value: "" });
1301
+ }
1302
+ else if (inputKey.toLowerCase() === "e" && selected && memoryPanel.target !== "dump" && !memoryPanel.readOnly) {
1303
+ setMemoryEditor({ mode: "edit", value: selected.content });
1304
+ }
1305
+ else if (inputKey.toLowerCase() === "m" && selected && memoryPanel.target !== "dump" && !memoryPanel.readOnly) {
1306
+ const target = memoryPanel.target === "user" ? "memory" : "user";
1307
+ send({ type: "memory_panel_action", action: "move", target, entry_id: selected.entry_id });
1308
+ }
1309
+ else if (inputKey.toLowerCase() === "y" && selected?.review_pending && !memoryPanel.readOnly) {
1310
+ send({ type: "memory_panel_action", action: "review_accept", target: memoryPanel.target ?? undefined, entry_id: selected.entry_id });
1311
+ }
1312
+ else if (inputKey.toLowerCase() === "n" && selected?.review_pending && !memoryPanel.readOnly) {
1313
+ send({ type: "memory_panel_action", action: "review_reject", target: memoryPanel.target ?? undefined, entry_id: selected.entry_id });
1314
+ }
1315
+ else if (inputKey.toLowerCase() === "v" && selected && memoryPanel.view === "detail" && !memoryPanel.readOnly) {
1316
+ send({ type: "memory_panel_action", action: "revisions", target: memoryPanel.target ?? undefined, entry_id: selected.entry_id });
1317
+ }
1318
+ else if (inputKey.toLowerCase() === "b" && memoryPanel.view === "root" && memoryPanel.readOnly && memoryPanel.hint?.includes("rebuild")) {
1319
+ send({ type: "memory_panel_action", action: "rebuild" });
1320
+ }
1321
+ }, { isActive: activeRegion === "memoryPanel" && Boolean(memoryPanel) && rawModeAvailable && !confirm });
1322
+ useInput((inputKey, key) => {
1323
+ if (!playbackSessionActive || confirm || isSlashMenuActive || languagePanel?.active || isModelPanelActive)
1324
+ return;
1325
+ if (key.tab || inputKey === "\t") {
1326
+ const nextShellState = reduceShellState(shellStateRef.current, {
1327
+ type: "toggle_region",
1328
+ spotifyModeEnabled: false,
1329
+ providerModeEnabled: providerModeRef.current.enabled,
1330
+ });
1331
+ if (nextShellState.region !== shellStateRef.current.region) {
1332
+ switchRegion(nextShellState.region);
1333
+ }
1334
+ applyShellAction({ type: "replace", state: nextShellState });
1335
+ }
1336
+ }, { isActive: rawModeAvailable && playbackSessionActive && !confirm && !isSlashMenuActive && !languagePanel?.active && !isModelPanelActive });
1337
+ useInput((_inputKey, key) => {
1338
+ if (!key.escape || !agentWorkingTurnId)
1339
+ return;
1340
+ const sent = send({
1341
+ type: "agent_turn_interrupt",
1342
+ turn_id: agentWorkingTurnId,
1343
+ });
1344
+ if (sent) {
1345
+ applyRuntimeAction({ type: "clear_agent_working" });
1346
+ }
1347
+ }, {
1348
+ isActive: rawModeAvailable
1349
+ && activeRegion === "chat"
1350
+ && agentWorkingTurnId !== null
1351
+ && !confirm
1352
+ && !isSlashMenuActive
1353
+ && !helpPanel
1354
+ && !languagePanel?.active
1355
+ && !spotifySetup?.active
1356
+ && !authSetup?.active
1357
+ && !trackPanel,
1358
+ });
1359
+ return (_jsxs(_Fragment, { children: [_jsx(CommittedTranscript, { records: transcript.records }), _jsxs(Box, { flexDirection: "column", width: terminalSize.columns ?? "100%", height: activeRegion === "chat" || activeRegion === "memoryPanel" ? undefined : dynamicSurfaceHeight, minHeight: 0, children: [showFixedHeader ? (_jsx(HeaderFrame, { authState: authState, cwd: RUNTIME_WORKING_DIRECTORY, sessionId: sessionId, variant: headerVariant, language: language })) : null, isLoginScreenActive ? (_jsx(LoginScreen, { authSetup: authSetup, selectedIndex: loginSelectionIndex, apiKeyInput: loginApiKeyInput, setApiKeyInput: setLoginApiKeyInput, onApiKeySubmit: submitLoginApiKey, language: language })) : (_jsx(DynamicShell, { input: input, setInput: updateInput, onSubmit: submitInput, inputPlaceholder: inputPlaceholder, inputMask: inputMask, inputFocus: (!confirm || Boolean(selectedConfirmInput)) && rawModeAvailable && !isExiting && !helpPanel && !languagePanel?.active && !isModelPanelActive && !recommendInputLocked, inputRevision: inputRevision, player: player, coverUrl: coverUrl, coverPattern: coverPattern, confirm: confirm, confirmIndex: confirmIndex, spotifyMode: spotifyMode, providerMode: providerMode, spotifySetup: spotifySetup, authSetup: authSetup, modelStatus: modelStatus, slashSuggestions: slashSuggestions, slashIndex: slashIndex, helpPanel: helpPanel, helpPanelIndex: helpPanelIndex, languagePanel: languagePanel, languagePanelIndex: languagePanelIndex, modelPanelIndex: loginSelectionIndex, trackPanel: trackPanel, trackPanelIndex: trackPanelIndex, extensionPanel: extensionPanel, extensionPanelIndex: extensionPanelIndex, extensionInputFocused: extensionInputFocused, memoryPanel: memoryPanel, memoryPanelIndex: memoryPanelIndex, memorySearchQuery: memorySearchQuery, memoryEditor: memoryEditor, activeRegion: activeRegion, miniSnapshotRevision: miniSnapshotRevision, miniLayout: miniLayout, spotifyImmersiveLayout: spotifyImmersiveLayout, terminalSpace: terminalSize, agentWorking: agentWorkingTurnId !== null, streamingMessage: streamingMessage, language: language }))] })] }));
1360
+ };