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
@@ -0,0 +1,662 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import React from 'react';
3
+ import { Box, Static, Text, Transform, measureElement } from 'ink';
4
+ import TextInput from 'ink-text-input';
5
+ import stringWidth from 'string-width';
6
+ import { chatDocumentSegments } from './chat-document.js';
7
+ import { CHAT_SYSTEM_MARKER_COLOR, CHAT_USER_MARKER_COLOR, resolveChatContentColor, resolveChatMarkerColor, wrapChatMessageContent, wrapChatMessageSegments } from './chat-message.js';
8
+ import { resolveAgentChatTheme } from './chat-theme.js';
9
+ import { APP_VERSION, BORDER_BLUE, BORDER_BLUE_SOFT, FALLBACK_MODEL_NAME, MAX_VISIBLE_MODEL_CHOICES, MAX_VISIBLE_SLASH_COMMANDS, SONEX_MASCOT, SONEX_MASCOT_MICRO, SPOTIFY_GREEN, TOOL_NAVY, TOOL_VALUE } from './constants.js';
10
+ import { HELP_PANEL_VISIBLE_COMMANDS, helpPanelCommands, visibleCommandWindow } from './command-panel.js';
11
+ import { getVisibleConfirmChoices, resolveConfirmChoiceDisplayIndex } from './confirm-choice.js';
12
+ import { buildProgressBar, formatDuration, formatMiniTrackSubtitle, formatMusicCandidateDisplayLabel } from './format.js';
13
+ import { formatWorkingDirectory } from './info-banner.js';
14
+ import { isHttpCoverSource, useCoverArt } from './hooks.js';
15
+ import { hideInputCursor, INPUT_CURSOR_BLINK_INTERVAL_MS } from './input-cursor.js';
16
+ import { languageLabel, t } from './i18n.js';
17
+ import { coverVisualFromSource } from './cover-visual.js';
18
+ import { renderCoverPatternHalfBlocks, resolveCoverPatternDisplay } from './cover-pattern.js';
19
+ import { resolveMiniPlayerLayout } from './layout.js';
20
+ import { filterModelChoices, formatModelPanelLabel, modelPanelLabelWidth } from './model-selection.js';
21
+ import { buildPlaybackStatusIconLine } from './mini-progress-writer.js';
22
+ import { PANEL_BACKGROUND, PANEL_PRIMARY, PANEL_SECONDARY, PanelChoiceList, PanelEmptyRow, PanelFrame, PanelRow, resolvePanelChoiceSegments } from './panel-frame.js';
23
+ import { SONEX_LOGO } from './sonex-logo.js';
24
+ import { formatTrackPanelLine, trackPanelTrackKey } from './track-panel.js';
25
+ import { withTrueColorBackground } from './terminal-frame-writer.js';
26
+ import { ExtensionPanelOverlay } from './extension-panel.js';
27
+ const Mascot = () => {
28
+ return (_jsx(Box, { width: 16, flexDirection: "column", marginRight: 3, children: SONEX_MASCOT.map((row, rowIndex) => (_jsx(Text, { children: row.map((segment, segmentIndex) => (_jsx(Text, { color: segment.fg, backgroundColor: segment.bg, children: segment.text }, segmentIndex))) }, rowIndex))) }));
29
+ };
30
+ export const SonexLogo = () => {
31
+ const useColor = process.env.NO_COLOR === undefined;
32
+ return (_jsx(Box, { width: "100%", flexDirection: "column", children: SONEX_LOGO.map((line, rowIndex) => (_jsx(Text, { color: useColor ? BORDER_BLUE_SOFT : undefined, wrap: "truncate-end", children: line }, rowIndex))) }));
33
+ };
34
+ const MiniMascotStatus = () => {
35
+ return (_jsx(Box, { height: 1, flexShrink: 0, paddingLeft: 1, paddingRight: 1, flexDirection: "column", alignItems: "flex-start", children: SONEX_MASCOT_MICRO.map((row, rowIndex) => (_jsx(Text, { children: row.map((segment, segmentIndex) => (_jsx(Text, { color: segment.fg, backgroundColor: segment.bg, children: segment.text }, segmentIndex))) }, rowIndex))) }));
36
+ };
37
+ const WORKING_SPINNER_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
38
+ const WORKING_SPINNER_INTERVAL_MS = 100;
39
+ export const AgentWorkingStatus = () => {
40
+ const [frame, setFrame] = React.useState(0);
41
+ React.useEffect(() => {
42
+ const timer = setInterval(() => setFrame((current) => (current + 1) % WORKING_SPINNER_FRAMES.length), WORKING_SPINNER_INTERVAL_MS);
43
+ return () => clearInterval(timer);
44
+ }, []);
45
+ return (_jsxs(Box, { height: 1, flexShrink: 0, paddingLeft: 1, paddingRight: 1, alignItems: "flex-start", children: [_jsxs(Text, { color: CHAT_SYSTEM_MARKER_COLOR, children: [WORKING_SPINNER_FRAMES[frame], " "] }), _jsx(Text, { color: CHAT_SYSTEM_MARKER_COLOR, italic: true, children: "Working" }), _jsx(Text, { color: "#808791", bold: true, children: " \u2022 Esc to interrupt" })] }));
46
+ };
47
+ export const formatAuthLabel = (state) => {
48
+ if (state.credential_source === "local" || state.auth_type === "local") {
49
+ return "local";
50
+ }
51
+ if (!state.ready) {
52
+ return "sign-in required";
53
+ }
54
+ if (state.auth_type === "api_key") {
55
+ return "API billing";
56
+ }
57
+ if (state.auth_type === "oauth") {
58
+ return "OAuth";
59
+ }
60
+ return state.auth_type || state.credential_source || "auth";
61
+ };
62
+ export const HeaderFrame = ({ authState, cwd, sessionId, variant, language = "en" }) => {
63
+ const identityModel = authState.model_label || authState.model || authState.provider || FALLBACK_MODEL_NAME;
64
+ const displayCwd = formatWorkingDirectory(cwd);
65
+ const runtimeInformation = (_jsxs(Box, { flexDirection: "column", flexGrow: 1, flexShrink: 1, minWidth: 0, children: [_jsxs(Text, { wrap: "truncate-end", children: [_jsx(Text, { bold: true, color: "#fff4f6", children: "Sonex CLI" }), " ", _jsxs(Text, { bold: true, color: BORDER_BLUE, children: ["v", APP_VERSION] })] }), _jsx(Box, { height: 1 }), _jsxs(Text, { color: "#d8bcc7", wrap: "truncate-end", children: [identityModel, " \u2022 ", authState.ready
66
+ ? formatAuthLabel(authState)
67
+ : _jsx(Text, { color: "#facc15", bold: true, children: "Not logged in" })] }), _jsx(Text, { color: "#fff4f6", wrap: "truncate-end", children: displayCwd }), sessionId ? (_jsxs(_Fragment, { children: [_jsx(Text, { color: "#808791", children: "session id:" }), _jsx(Text, { color: "#fff4f6", wrap: "truncate-end", children: sessionId })] })) : null] }));
68
+ if (variant === 'compact') {
69
+ return (_jsx(Box, { width: "100%", flexDirection: "column", marginBottom: 2, children: runtimeInformation }));
70
+ }
71
+ return (_jsxs(Box, { width: "100%", marginBottom: 2, children: [_jsx(Mascot, {}), runtimeInformation] }));
72
+ };
73
+ export const isGenericAuthSetup = (setup) => {
74
+ return Boolean(setup?.active);
75
+ };
76
+ const LoginChoiceList = ({ choices, selectedIndex, visibleLimit, showConnectionStatus = false }) => {
77
+ return (_jsx(PanelChoiceList, { items: choices.map((choice) => {
78
+ const detail = choice.description
79
+ ? [{ text: ` ${choice.description}`, color: PANEL_SECONDARY }]
80
+ : [];
81
+ const connectionStatus = choice.connection_status ?? (choice.connected ? "active" : "missing");
82
+ const connectionColor = connectionStatus === "active"
83
+ ? SPOTIFY_GREEN
84
+ : connectionStatus === "missing"
85
+ ? "#ef4444"
86
+ : PANEL_SECONDARY;
87
+ return {
88
+ key: choice.value,
89
+ segments: showConnectionStatus
90
+ ? [
91
+ {
92
+ text: "• ",
93
+ color: connectionColor,
94
+ preserveColorWhenSelected: true,
95
+ },
96
+ { text: choice.label, color: PANEL_PRIMARY },
97
+ ]
98
+ : [{ text: choice.label, color: PANEL_PRIMARY }, ...detail],
99
+ };
100
+ }), selectedIndex: selectedIndex, visibleLimit: visibleLimit, width: 74, paddingX: 2 }));
101
+ };
102
+ export const MAX_VISIBLE_LOGIN_PROVIDERS = 8;
103
+ export const LoginScreen = ({ authSetup, selectedIndex, apiKeyInput, setApiKeyInput, onApiKeySubmit, inputFocus = true, language = "en", }) => {
104
+ if (!authSetup)
105
+ return null;
106
+ const providerChoices = authSetup.providers ?? [];
107
+ const methodChoices = authSetup.methods ?? [];
108
+ const isProviderStep = authSetup.step === "provider";
109
+ const isMethodStep = authSetup.step === "method";
110
+ const modelChoices = authSetup.models ?? [];
111
+ const isApiKeyStep = authSetup.step === "api_key";
112
+ const isModelStep = authSetup.step === "model";
113
+ const isOauthWait = authSetup.step === "oauth_wait";
114
+ const isTextStep = !isProviderStep && !isMethodStep && !isModelStep && !isOauthWait;
115
+ const choices = isProviderStep ? providerChoices : isMethodStep ? methodChoices : isModelStep ? modelChoices : [];
116
+ const showProviderConnectionStatus = isProviderStep
117
+ && providerChoices.length > 0
118
+ && providerChoices.every((choice) => Boolean(choice.connection_status) || typeof choice.connected === "boolean");
119
+ const displayMessage = isProviderStep
120
+ ? t(language, "login.warmup")
121
+ : authSetup.message;
122
+ const helpRows = authSetup.help_text
123
+ ? wrapChatMessageContent(authSetup.help_text, 70)
124
+ : [];
125
+ return (_jsxs(PanelFrame, { width: 74, paddingX: 2, title: authSetup.title, hint: displayMessage, children: [(isProviderStep || isMethodStep || isModelStep) ? (_jsxs(_Fragment, { children: [_jsx(LoginChoiceList, { choices: choices, selectedIndex: selectedIndex, visibleLimit: isProviderStep ? MAX_VISIBLE_LOGIN_PROVIDERS : isModelStep ? MAX_VISIBLE_MODEL_CHOICES : undefined, showConnectionStatus: showProviderConnectionStatus }), _jsx(PanelRow, { width: 74, paddingX: 2, segments: [{ text: t(language, "login.continue"), color: PANEL_SECONDARY, bold: true }] })] })) : null, isTextStep ? (_jsxs(_Fragment, { children: [_jsx(PanelRow, { width: 74, paddingX: 2, segments: [{ text: authSetup.prompt ?? "Value", color: PANEL_SECONDARY }] }), _jsx(PromptInput, { input: apiKeyInput, setInput: setApiKeyInput, onSubmit: onApiKeySubmit, focus: inputFocus, placeholder: authSetup.placeholder ?? authSetup.prompt ?? "Value", mask: authSetup.mask || isApiKeyStep ? "*" : undefined, backgroundColor: PANEL_BACKGROUND, backgroundWidth: 74, backgroundPaddingX: 2 }), helpRows.map((row, index) => (_jsx(PanelRow, { width: 74, paddingX: 2, segments: [{ text: row, color: PANEL_SECONDARY, italic: true }] }, `auth-help-${index}`)))] })) : null, isOauthWait ? (_jsxs(_Fragment, { children: [_jsx(PanelRow, { width: 74, paddingX: 2, segments: [{ text: t(language, "auth.oauth.waiting"), color: BORDER_BLUE_SOFT }] }), _jsx(PanelRow, { width: 74, paddingX: 2, segments: [{ text: t(language, "auth.oauth.return"), color: PANEL_SECONDARY }] })] })) : null] }));
126
+ };
127
+ const fillPromptInputBackground = (output, backgroundWidth, backgroundPaddingX = 0) => {
128
+ if (!backgroundWidth)
129
+ return output;
130
+ const width = Math.max(1, Math.floor(backgroundWidth));
131
+ const paddingX = Math.max(0, Math.min(Math.floor(backgroundPaddingX), Math.floor(width / 2)));
132
+ return output.split("\n").map((line) => (`${" ".repeat(paddingX)}${line}${" ".repeat(Math.max(paddingX, width - paddingX - stringWidth(line)))}`)).join("\n");
133
+ };
134
+ const PromptInput = ({ input, setInput, onSubmit, focus, placeholder, mask, inputRevision, backgroundColor, backgroundWidth, backgroundPaddingX = 0, }) => {
135
+ const [cursorVisible, setCursorVisible] = React.useState(true);
136
+ React.useEffect(() => {
137
+ setCursorVisible(true);
138
+ if (!focus)
139
+ return;
140
+ const timer = setInterval(() => setCursorVisible((visible) => !visible), INPUT_CURSOR_BLINK_INTERVAL_MS);
141
+ return () => clearInterval(timer);
142
+ }, [focus, input, inputRevision]);
143
+ return (_jsx(Text, { children: _jsx(Transform, { transform: (output) => {
144
+ const visibleOutput = focus && cursorVisible ? output : hideInputCursor(output);
145
+ const filledOutput = fillPromptInputBackground(visibleOutput, backgroundWidth, backgroundPaddingX);
146
+ return backgroundColor
147
+ ? withTrueColorBackground(filledOutput, backgroundColor)
148
+ : filledOutput;
149
+ }, children: _jsx(TextInput, { value: input, onChange: setInput, onSubmit: onSubmit, focus: focus, placeholder: placeholder, mask: mask }, inputRevision) }) }));
150
+ };
151
+ const COMMAND_LIST_LABEL_WIDTH = 12;
152
+ const CONFIRM_CHOICE_LABEL_WIDTH = 18;
153
+ const CONFIRM_CHOICE_ROW_LABEL_WIDTH = 102;
154
+ const PLAYLIST_BROWSE_NAME_WIDTH = 32;
155
+ const MEMORY_SETTING_LABEL_WIDTH = 48;
156
+ const TRACK_PANEL_ROW_WIDTH = 96;
157
+ const truncateDisplayWidth = (value, width) => {
158
+ const normalized = value.trim() || "-";
159
+ let rendered = "";
160
+ for (const char of normalized) {
161
+ if (stringWidth(rendered + char) > width)
162
+ break;
163
+ rendered += char;
164
+ }
165
+ return rendered || "-";
166
+ };
167
+ const fitDisplayWidth = (value, width) => {
168
+ const rendered = truncateDisplayWidth(value, width);
169
+ return rendered + " ".repeat(Math.max(0, width - stringWidth(rendered)));
170
+ };
171
+ const formatMemorySettingRow = (label, value) => (`${fitDisplayWidth(label, MEMORY_SETTING_LABEL_WIDTH)}${value}`);
172
+ const formatCommandListLabel = (command) => (`/${command.name}`.slice(0, COMMAND_LIST_LABEL_WIDTH).padEnd(COMMAND_LIST_LABEL_WIDTH, " "));
173
+ const formatPlaylistBrowseName = (label) => (fitDisplayWidth(label, PLAYLIST_BROWSE_NAME_WIDTH));
174
+ const playlistBrowseTrackCount = (choice) => {
175
+ const numericCount = typeof choice.track_count === "number"
176
+ ? choice.track_count
177
+ : Number.parseInt(choice.description ?? "", 10);
178
+ const count = Number.isFinite(numericCount) && numericCount > 0 ? Math.trunc(numericCount) : 0;
179
+ return `${count} track${count === 1 ? "" : "s"}`;
180
+ };
181
+ const formatConfirmChoiceLabel = (row) => (row.display?.kind === "music_candidate"
182
+ ? formatMusicCandidateDisplayLabel(row.display, CONFIRM_CHOICE_ROW_LABEL_WIDTH, row.description)
183
+ : row.labelWidth
184
+ ? row.label + " ".repeat(Math.max(0, row.labelWidth - stringWidth(row.label)))
185
+ : row.label);
186
+ const SlashCommandList = ({ suggestions, selectedIndex, spotifyTheme = false }) => {
187
+ if (suggestions.length === 0)
188
+ return null;
189
+ const { items: visibleSuggestions, boundedIndex, startIndex } = visibleCommandWindow(suggestions, selectedIndex, MAX_VISIBLE_SLASH_COMMANDS);
190
+ return (_jsx(Box, { flexDirection: "column", children: visibleSuggestions.map((command, index) => {
191
+ const absoluteIndex = startIndex + index;
192
+ const selected = absoluteIndex === boundedIndex;
193
+ const commandColor = selected ? (spotifyTheme ? SPOTIFY_GREEN : BORDER_BLUE) : "#fff4f6";
194
+ const descriptionColor = selected ? commandColor : "#808791";
195
+ return (_jsxs(Text, { color: commandColor, bold: selected, wrap: "truncate-end", children: [_jsx(Text, { children: formatCommandListLabel(command) }), _jsx(Text, { color: descriptionColor, children: command.description })] }, command.name));
196
+ }) }));
197
+ };
198
+ const HelpPanel = ({ panel, selectedIndex, width, language = "en" }) => {
199
+ if (!panel)
200
+ return null;
201
+ const commands = helpPanelCommands(panel.commands);
202
+ const items = commands.map((command) => ({
203
+ key: command.name,
204
+ segments: [
205
+ { text: formatCommandListLabel(command), color: PANEL_PRIMARY },
206
+ { text: command.description, color: PANEL_SECONDARY },
207
+ ],
208
+ }));
209
+ return (_jsx(PanelFrame, { width: width, title: panel.title, hint: panel.hint, children: panel.commands.length === 0 ? (_jsx(PanelRow, { width: width, segments: [{ text: t(language, "help.empty"), color: PANEL_SECONDARY }] })) : (_jsx(PanelChoiceList, { items: items, selectedIndex: selectedIndex, visibleLimit: HELP_PANEL_VISIBLE_COMMANDS, width: width })) }));
210
+ };
211
+ export const ChatBubble = ({ role, content, contentWidth, theme = null, tone = null, segments = null, document = null, showDivider = true }) => {
212
+ const isUser = role === "user";
213
+ const markerColor = resolveChatMarkerColor(role, theme, tone);
214
+ const contentColor = resolveChatContentColor(role, tone);
215
+ const semanticTheme = resolveAgentChatTheme(theme);
216
+ const useToolSegmentStyles = !isUser && tone === null;
217
+ const semanticSegments = !isUser && tone === null && document?.version === 1
218
+ ? chatDocumentSegments(document)
219
+ : null;
220
+ const candidateSegments = semanticSegments ?? segments;
221
+ const validSegments = candidateSegments && candidateSegments.map((segment) => segment.text).join("") === content
222
+ ? candidateSegments
223
+ : null;
224
+ const richLines = validSegments ? wrapChatMessageSegments(validSegments, contentWidth) : null;
225
+ const lines = richLines ?? wrapChatMessageContent(content, contentWidth);
226
+ return (_jsxs(Box, { marginBottom: 1, flexDirection: "column", width: "100%", children: [lines.map((line, index) => {
227
+ const marker = index === 0 ? "•" : " ";
228
+ if (typeof line === "string") {
229
+ return (_jsxs(Text, { children: [_jsx(Text, { bold: true, color: markerColor, children: marker }), _jsx(Text, { color: contentColor, children: ` ${line}` })] }, `${index}_${line}`));
230
+ }
231
+ return (_jsxs(Text, { children: [_jsx(Text, { bold: true, color: markerColor, children: marker }), _jsx(Text, { color: contentColor, children: " " }), line.map((segment, segmentIndex) => (_jsx(Text, { color: useToolSegmentStyles && segment.style === "tool_name"
232
+ ? TOOL_NAVY
233
+ : segment.style === "heading" || segment.style === "strong" || segment.style === "list_marker"
234
+ ? semanticTheme.strongText
235
+ : segment.style === "link"
236
+ ? semanticTheme.linkText
237
+ : contentColor, backgroundColor: segment.style === "highlight"
238
+ ? semanticTheme.highlightBackground
239
+ : segment.style === "code"
240
+ ? semanticTheme.codeBackground
241
+ : undefined, bold: (useToolSegmentStyles && segment.style === "tool_name")
242
+ || ["heading", "strong", "highlight", "list_marker"].includes(segment.style), underline: segment.style === "link", children: segment.text }, `${segmentIndex}_${segment.text}`)))] }, `${index}_${line.map((segment) => segment.text).join("")}`));
243
+ }), showDivider ? (_jsx(Box, { marginTop: 1, children: _jsx(Text, { color: CHAT_USER_MARKER_COLOR, children: "─".repeat(contentWidth + 2) }) })) : null] }));
244
+ };
245
+ export const CommittedRecord = ({ record, }) => (_jsx(Box, { flexDirection: "column", paddingX: 1, children: record.item.type === "info_banner" ? (_jsxs(_Fragment, { children: [record.item.showLogo ? (_jsxs(_Fragment, { children: [_jsx(SonexLogo, {}), _jsx(Box, { height: 1 })] })) : null, _jsx(HeaderFrame, { authState: record.item.authState, cwd: record.item.cwd, sessionId: record.item.sessionId, variant: record.presentation.headerVariant, language: record.presentation.language })] })) : (_jsx(ChatBubble, { role: record.item.role, content: record.item.content, contentWidth: record.presentation.contentWidth, theme: record.item.theme, tone: record.item.tone, segments: record.item.segments, document: record.item.document })) }));
246
+ export const CommittedTranscript = ({ records, }) => (_jsx(Static, { items: records, children: (record) => (_jsx(CommittedRecord, { record: record }, record.sequence)) }));
247
+ const localizeTrackPanelTitle = (panel, language) => {
248
+ if (panel.panel === "queue")
249
+ return t(language, "trackPanel.queue");
250
+ const playlistPrefix = "Playlist:";
251
+ if (panel.title.startsWith(playlistPrefix)) {
252
+ const playlistName = panel.title.slice(playlistPrefix.length).trim();
253
+ return playlistName ? `${t(language, "trackPanel.playlist")}: ${playlistName}` : t(language, "trackPanel.playlist");
254
+ }
255
+ if (panel.title === "Playlist")
256
+ return t(language, "trackPanel.playlist");
257
+ return panel.title;
258
+ };
259
+ const trackPanelEmptyText = (panel, language) => (panel.panel === "queue" ? t(language, "trackPanel.queueEmpty") : t(language, "trackPanel.playlistEmpty"));
260
+ const TRACK_PANEL_MIN_VISIBLE_ROWS = 4;
261
+ const TrackPanel = ({ panel, panelWidth, expanded = false, selectedIndex = 0, spotifyTheme = false, language = "en", }) => {
262
+ if (!panel)
263
+ return null;
264
+ const panelRef = React.useRef(null);
265
+ const [panelHeight, setPanelHeight] = React.useState(0);
266
+ React.useEffect(() => {
267
+ if (!panelRef.current)
268
+ return;
269
+ const { height } = measureElement(panelRef.current);
270
+ if (height > 0 && height !== panelHeight) {
271
+ setPanelHeight(height);
272
+ }
273
+ });
274
+ const panelTitle = localizeTrackPanelTitle(panel, language);
275
+ const titleRows = 1;
276
+ const hintRows = panel.hint ? 1 : 0;
277
+ const paddingRows = 1;
278
+ const availableRows = Math.max(TRACK_PANEL_MIN_VISIBLE_ROWS, panelHeight > 0 ? panelHeight - titleRows - hintRows - paddingRows - 2 : TRACK_PANEL_MIN_VISIBLE_ROWS);
279
+ const visibleRowCount = panel.tracks.length === 0
280
+ ? 1
281
+ : Math.min(panel.tracks.length, availableRows);
282
+ const fillerRowCount = Math.max(0, availableRows - visibleRowCount);
283
+ const items = panel.tracks.map((track) => ({
284
+ key: `${track.index}-${trackPanelTrackKey(track)}`,
285
+ segments: [{
286
+ text: formatTrackPanelLine(track, TRACK_PANEL_ROW_WIDTH),
287
+ color: PANEL_PRIMARY,
288
+ }],
289
+ }));
290
+ return (_jsx(Box, { ref: panelRef, flexDirection: "column", flexGrow: expanded ? 1 : 0, flexShrink: 0, minHeight: 0, height: expanded ? "100%" : undefined, children: _jsxs(PanelFrame, { width: panelWidth, paddingX: 2, title: panelTitle, hint: panel.hint ? `${panel.hint}; Esc to hide` : null, children: [panel.tracks.length === 0 ? (_jsx(PanelRow, { width: panelWidth, paddingX: 2, segments: [{ text: trackPanelEmptyText(panel, language), color: PANEL_SECONDARY }] })) : (_jsx(PanelChoiceList, { items: items, selectedIndex: selectedIndex, visibleLimit: availableRows, width: panelWidth, paddingX: 2, spotifyTheme: spotifyTheme })), Array.from({ length: fillerRowCount }, (_unused, index) => (_jsx(PanelEmptyRow, { width: panelWidth }, `track-panel-filler-${index}`)))] }) }));
291
+ };
292
+ const TrackPanelOverlay = ({ trackPanel, selectedIndex = 0, panelWidth, spotifyTheme = false, language = "en", }) => (_jsx(Box, { width: "100%", height: "100%", flexDirection: "column", flexGrow: 1, flexShrink: 1, minHeight: 0, children: _jsx(TrackPanel, { panel: trackPanel, panelWidth: panelWidth, expanded: true, selectedIndex: selectedIndex, spotifyTheme: spotifyTheme, language: language }) }));
293
+ const MemoryPanelOverlay = ({ panel, selectedIndex = 0, searchQuery = "", editor = null, panelWidth = 74 }) => {
294
+ if (!panel)
295
+ return null;
296
+ const width = Math.max(3, Math.min(74, Math.floor(panelWidth)));
297
+ if (editor) {
298
+ return (_jsx(Box, { width: "100%", flexDirection: "column", flexShrink: 0, paddingX: 1, children: _jsx(PanelFrame, { width: width, paddingX: 2, title: editor.mode === "search"
299
+ ? "Search memory"
300
+ : editor.mode === "add"
301
+ ? "Add memory"
302
+ : editor.mode === "edit"
303
+ ? "Edit memory"
304
+ : "Change memory setting", hint: editor.mode === "search" || editor.mode === "setting"
305
+ ? "Enter to apply; Esc to cancel"
306
+ : "Ctrl+S to save; Enter for a new line; Esc to cancel", children: _jsx(PanelRow, { width: width, paddingX: 2, segments: [{ text: `${editor.mode === "search" ? "/" : ""}${editor.value || " "}`, color: PANEL_PRIMARY }] }) }) }));
307
+ }
308
+ const rootItems = panel.view === "root"
309
+ ? ["view memory entries", "reset memory"]
310
+ : panel.view === "sources"
311
+ ? ["USER.md", "MEMORY.md", "Memory Dump"]
312
+ : panel.view === "format"
313
+ ? ["USER.md", "MEMORY.md", "All memory"]
314
+ : panel.view === "settings"
315
+ ? [
316
+ formatMemorySettingRow("Forget retention", `${panel.settings?.forget_retention_days ?? 7} days`),
317
+ formatMemorySettingRow("USER.md capacity", String(panel.settings?.user_capacity ?? "Unlimited")),
318
+ formatMemorySettingRow("MEMORY.md capacity", String(panel.settings?.memory_capacity ?? "Unlimited")),
319
+ formatMemorySettingRow("Automatic forgetting", String(panel.settings?.automatic_forgetting ?? "off")),
320
+ formatMemorySettingRow("Idle threshold", `${panel.settings?.idle_threshold_days ?? 30} days`),
321
+ formatMemorySettingRow("Automatic refinement", panel.settings?.automatic_refinement === false ? "Off" : "On"),
322
+ formatMemorySettingRow("USER.md refinement window", String(panel.settings?.user_refinement_window ?? 8)),
323
+ formatMemorySettingRow("MEMORY.md refinement window", String(panel.settings?.memory_refinement_window ?? 12)),
324
+ ]
325
+ : [];
326
+ if (panel.view === "detail") {
327
+ const entry = panel.entries[0];
328
+ const revisions = Array.isArray(panel.settings?.revisions) ? panel.settings.revisions : [];
329
+ return (_jsx(Box, { width: "100%", flexDirection: "column", flexShrink: 0, paddingX: 1, children: _jsx(PanelFrame, { width: width, paddingX: 2, title: `${panel.title}${panel.readOnly ? " · Read only" : ""}`, hint: panel.hint, children: entry ? (_jsxs(Box, { flexDirection: "column", paddingX: 2, children: [_jsx(Text, { color: PANEL_PRIMARY, children: entry.content }), _jsxs(Text, { color: PANEL_SECONDARY, children: [entry.protected ? "Protected" : "Inferred", " \u00B7 ", entry.source] }), _jsxs(Text, { color: PANEL_SECONDARY, children: ["Updated: ", entry.updated_at ?? "Never"] }), _jsxs(Text, { color: PANEL_SECONDARY, children: ["Recalled: ", entry.recall_count ?? 0, " \u00B7 Last: ", entry.last_recalled_at ?? "Never"] }), entry.reason ? _jsxs(Text, { color: PANEL_SECONDARY, children: ["Reason: ", entry.reason] }) : null, entry.expires_at ? _jsxs(Text, { color: PANEL_SECONDARY, children: ["Expires: ", entry.expires_at] }) : null, entry.review_pending ? _jsx(Text, { color: "#facc15", bold: true, children: "Review pending" }) : null, _jsxs(Text, { color: PANEL_SECONDARY, children: ["Revisions: ", revisions.length] })] })) : _jsx(PanelRow, { width: width, paddingX: 2, segments: [{ text: "Memory entry is unavailable.", color: PANEL_SECONDARY }] }) }) }));
330
+ }
331
+ const items = rootItems.length > 0
332
+ ? rootItems.map((label) => ({ key: label, segments: [{ text: label, color: PANEL_PRIMARY }] }))
333
+ : panel.entries
334
+ .filter((entry) => entry.content.toLocaleLowerCase().includes(searchQuery.toLocaleLowerCase()))
335
+ .map((entry) => ({
336
+ key: entry.entry_id,
337
+ segments: [{
338
+ text: panel.view === "revisions"
339
+ ? `${entry.content.replaceAll("\n", " ").slice(0, 56)} ${entry.source ?? "unknown"}`
340
+ : `${entry.content.replaceAll("\n", " ").slice(0, 64)} ${entry.protected ? "Protected" : "Inferred"}${entry.review_pending ? " · Review pending" : ""}`,
341
+ color: PANEL_PRIMARY,
342
+ }],
343
+ }));
344
+ return (_jsx(Box, { width: "100%", flexDirection: "column", flexShrink: 0, paddingX: 1, children: _jsx(PanelFrame, { width: width, paddingX: 2, title: `${panel.title}${panel.readOnly ? " · Read only" : ""}`, hint: searchQuery ? `Filter: /${searchQuery} · ${panel.hint ?? ""}` : panel.hint, children: items.length > 0 ? (_jsx(PanelChoiceList, { items: items, selectedIndex: selectedIndex, visibleLimit: 12, width: width, paddingX: 2 })) : (_jsx(PanelRow, { width: width, paddingX: 2, segments: [{ text: "No memory entries.", color: PANEL_SECONDARY }] })) }) }));
345
+ };
346
+ const CoverAtmosphere = ({ visual, art, compact }) => {
347
+ if (!compact && art) {
348
+ return _jsx(Text, { children: art });
349
+ }
350
+ const rows = compact ? visual.blocks.slice(0, 5) : visual.blocks;
351
+ const columns = compact ? 7 : 14;
352
+ return (_jsx(Box, { flexDirection: "column", children: rows.map((row, rowIndex) => (_jsx(Text, { children: row.slice(0, columns).map((color, columnIndex) => (_jsx(Text, { backgroundColor: color, children: " " }, `${rowIndex}-${columnIndex}`))) }, rowIndex))) }));
353
+ };
354
+ const CoverPatternArt = React.memo(({ pattern, variant }) => {
355
+ const rows = React.useMemo(() => renderCoverPatternHalfBlocks(variant.grid, pattern.palette), [variant, pattern.palette]);
356
+ return (_jsx(Box, { flexDirection: "column", children: rows.map((row, rowIndex) => (_jsx(Text, { children: row.map((cell, columnIndex) => (_jsx(Text, { color: cell.foreground, backgroundColor: cell.background, children: cell.char }, `${rowIndex}-${columnIndex}`))) }, rowIndex))) }));
357
+ });
358
+ const MINI_COVER_PATTERN_MAX_SIZE = 80;
359
+ const StaticCover = React.memo(({ visual, coverUrl, coverPattern, terminalSpace, compact, maxPatternSize }) => {
360
+ const maxSize = maxPatternSize ?? (compact ? MINI_COVER_PATTERN_MAX_SIZE : 32);
361
+ const patternDisplay = coverPattern
362
+ ? resolveCoverPatternDisplay(coverPattern, terminalSpace, maxSize ? { maxSize } : undefined)
363
+ : resolveCoverPatternDisplay(null, terminalSpace);
364
+ const compactCoverWidth = Math.max(22, Math.min(48, (terminalSpace?.columns ?? 40) - 6));
365
+ const compactCoverHeight = Math.max(8, Math.min(24, (terminalSpace?.rows ?? 22) - 8));
366
+ const fetchableCoverUrl = patternDisplay.status === 'none' && isHttpCoverSource(coverUrl) ? coverUrl : null;
367
+ const { art, failed } = useCoverArt(fetchableCoverUrl, compact ? compactCoverWidth : 32, compact ? compactCoverHeight : 16);
368
+ const resolvedVisual = React.useMemo(() => coverVisualFromSource(coverUrl, failed), [coverUrl, failed]);
369
+ const patternRequestedAt = React.useRef(null);
370
+ React.useEffect(() => {
371
+ if (process.env.SONEX_PLAYER_DEBUG !== '1')
372
+ return;
373
+ if (!coverUrl) {
374
+ patternRequestedAt.current = null;
375
+ return;
376
+ }
377
+ if (!coverPattern) {
378
+ if (patternRequestedAt.current === null) {
379
+ patternRequestedAt.current = Date.now();
380
+ }
381
+ return;
382
+ }
383
+ if (patternRequestedAt.current !== null) {
384
+ console.error(`[sonex-player-debug] cover pattern arrived in ${Date.now() - patternRequestedAt.current}ms url=${coverUrl}`);
385
+ patternRequestedAt.current = null;
386
+ }
387
+ }, [coverUrl, coverPattern]);
388
+ if (patternDisplay.status === 'unavailable') {
389
+ return compact
390
+ ? _jsx(Box, { flexGrow: 1, flexShrink: 1, minHeight: compactCoverHeight })
391
+ : _jsx(Box, { width: 36, paddingRight: 2 });
392
+ }
393
+ if (patternDisplay.status === 'renderable' && coverPattern) {
394
+ return (_jsx(Box, { flexGrow: compact ? 1 : 0, flexShrink: 1, minHeight: compact ? compactCoverHeight : undefined, alignItems: "center", justifyContent: compact ? 'flex-end' : 'center', children: _jsx(CoverPatternArt, { pattern: coverPattern, variant: patternDisplay.variant }) }));
395
+ }
396
+ if (patternDisplay.status === 'unfit') {
397
+ return compact
398
+ ? _jsx(Box, { flexGrow: 1, flexShrink: 1, minHeight: compactCoverHeight })
399
+ : _jsx(Box, { width: 36, paddingRight: 2 });
400
+ }
401
+ if (compact) {
402
+ return (_jsx(Box, { flexGrow: 1, flexShrink: 1, minHeight: compactCoverHeight, alignItems: "center", justifyContent: "center", children: _jsx(CoverAtmosphere, { visual: resolvedVisual, art: art, compact: compact }) }));
403
+ }
404
+ return (_jsxs(Box, { width: 36, paddingRight: 2, flexDirection: "column", children: [_jsx(CoverAtmosphere, { visual: resolvedVisual, art: art, compact: compact }), _jsx(Text, { color: visual.muted, children: resolvedVisual.status === "fallback" ? "cover atmosphere" : "cover palette" })] }));
405
+ });
406
+ const PlayerMascot = ({ visual, frame, compact }) => {
407
+ const pulse = ["▁", "▃", "▅", "▃"];
408
+ const left = pulse[frame] ?? "▁";
409
+ const right = pulse[(frame + 2) % pulse.length] ?? "▁";
410
+ if (compact) {
411
+ return (_jsxs(Text, { children: [_jsx(Text, { color: visual.secondary, children: left }), _jsx(Text, { color: visual.accent, children: " sonex " }), _jsx(Text, { color: visual.secondary, children: right })] }));
412
+ }
413
+ const lift = frame === 1 || frame === 2 ? " " : " ";
414
+ return (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsxs(Text, { children: [_jsxs(Text, { color: visual.secondary, children: [left, left] }), _jsx(Text, { color: visual.accent, children: " sonex signal " }), _jsxs(Text, { color: visual.secondary, children: [right, right] })] }), _jsxs(Text, { children: [_jsx(Text, { color: visual.muted, children: lift }), _jsx(Text, { color: visual.primary, children: "\u256D\u2500\u256E" }), _jsx(Text, { color: visual.accent, children: "\u25CF" }), _jsx(Text, { color: visual.primary, children: "\u256D\u2500\u256E" })] }), _jsxs(Text, { children: [_jsx(Text, { color: visual.muted, children: frame === 3 ? " " : " " }), _jsx(Text, { color: visual.primary, children: "\u2570\u2565\u256F" }), _jsx(Text, { color: visual.secondary, children: "\u2594" }), _jsx(Text, { color: visual.primary, children: "\u2570\u2565\u256F" })] })] }));
415
+ };
416
+ const TrackDetails = React.memo(({ player, compact }) => (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { bold: true, color: "#fff4f6", children: player.name }), _jsx(Text, { color: "#bf98a7", children: player.artist }), !compact || player.album !== "-" ? _jsx(Text, { color: "#bf98a7", children: player.album }) : null] })));
417
+ const MiniPlayerStaticBody = React.memo(({ player, visual, coverUrl, coverPattern, layout, }) => {
418
+ const infoInnerWidth = Math.max(0, layout.infoWidth - layout.infoLeftPadding);
419
+ const statusLine = buildPlaybackStatusIconLine(player, infoInnerWidth, Date.now());
420
+ return (_jsxs(Box, { flexDirection: "row", width: layout.contentColumns, height: layout.contentRows, children: [_jsxs(Box, { flexDirection: "column", width: layout.infoWidth, flexShrink: 0, paddingTop: layout.infoTop, paddingLeft: layout.infoLeftPadding, children: [_jsx(Box, { width: infoInnerWidth, justifyContent: "center", children: _jsx(Text, { bold: true, color: BORDER_BLUE_SOFT, wrap: "truncate-end", children: player.name }) }), _jsx(Box, { width: infoInnerWidth, justifyContent: "center", children: _jsx(Text, { color: "#ffffff", wrap: "truncate-end", children: formatMiniTrackSubtitle(player.artist, player.album) }) }), _jsx(Text, { children: ' '.repeat(infoInnerWidth) }), _jsx(Box, { width: infoInnerWidth, children: _jsx(Text, { children: statusLine.segments.map((segment, index) => (_jsx(Text, { color: segment.color, children: segment.text }, index))) }) })] }), layout.mode === 'artwork' ? (_jsx(Box, { marginLeft: layout.gap, width: layout.coverWidth, height: layout.contentRows, alignItems: "center", justifyContent: "flex-end", flexShrink: 0, children: _jsx(StaticCover, { visual: visual, coverUrl: coverUrl, coverPattern: coverPattern, terminalSpace: { columns: layout.coverWidth, rows: layout.contentRows }, compact: true }) })) : null] }));
421
+ }, (prev, next) => (prev.player === next.player
422
+ && prev.visual === next.visual
423
+ && prev.coverUrl === next.coverUrl
424
+ && prev.coverPattern === next.coverPattern
425
+ && prev.layout === next.layout));
426
+ const PlaybackMeter = ({ player, visual, compact = false, active = true }) => {
427
+ const progressMs = player.progress_ms ?? 0;
428
+ const progress = formatDuration(progressMs);
429
+ const duration = formatDuration(player.duration_ms);
430
+ const progressBar = buildProgressBar(progressMs, player.duration_ms, 18);
431
+ const isPlaying = active && player.is_playing === true;
432
+ return (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsxs(Text, { children: [_jsx(Text, { color: "#bf98a7", children: progress }), " ", _jsx(Text, { color: visual.secondary, children: progressBar }), " ", _jsx(Text, { color: "#bf98a7", children: duration })] }), _jsx(Text, { color: isPlaying ? visual.accent : "#7f5d6b", children: isPlaying ? "playing" : "paused" })] }));
433
+ };
434
+ const PlayerPane = ({ player, coverUrl, coverPattern, terminalSpace, miniLayout, variant = "full", active = true }) => {
435
+ const compact = variant === "compact";
436
+ const visual = React.useMemo(() => coverVisualFromSource(coverUrl, false), [coverUrl]);
437
+ if (compact) {
438
+ const layout = miniLayout ?? resolveMiniPlayerLayout(terminalSpace ?? { columns: null, rows: null });
439
+ return (_jsx(Box, { flexDirection: "column", flexGrow: 1, flexShrink: 1, minHeight: 0, children: _jsx(MiniPlayerStaticBody, { player: player, visual: visual, coverUrl: coverUrl, coverPattern: coverPattern ?? null, layout: layout }) }));
440
+ }
441
+ return (_jsxs(Box, { flexDirection: "column", flexGrow: compact ? 1 : 1, flexShrink: 1, minHeight: compact ? 8 : 20, padding: 1, paddingX: compact ? 1 : 2, children: [!compact ? (_jsx(Box, { marginBottom: 1, children: _jsx(Text, { bold: true, color: visual.accent, children: "Now playing" }) })) : null, _jsxs(Box, { marginTop: compact ? 0 : 1, children: [!compact ? _jsx(StaticCover, { visual: visual, coverUrl: coverUrl, coverPattern: coverPattern ?? null, terminalSpace: terminalSpace, compact: compact }) : null, _jsxs(Box, { flexDirection: "column", flexGrow: compact ? 0 : 1, flexShrink: 0, paddingTop: compact ? 1 : 1, children: [_jsx(TrackDetails, { player: player, compact: compact }), _jsx(PlaybackMeter, { player: player, visual: visual, compact: compact, active: active }), !compact ? _jsx(PlayerMascot, { visual: visual, frame: 0, compact: compact }) : null] })] })] }));
442
+ };
443
+ const LANGUAGE_CHOICES = ["en", "zh-CN"];
444
+ const orderedLanguageChoices = (current) => [
445
+ current,
446
+ ...LANGUAGE_CHOICES.filter((choice) => choice !== current),
447
+ ];
448
+ const confirmCancelHint = (choices) => (choices.some((choice) => choice.value === "deny" || choice.value === "cancel")
449
+ ? "press Esc to cancel"
450
+ : "press Esc to close");
451
+ export const CompactConfirm = ({ confirm, confirmIndex, input, setInput, onSubmit, inputFocus, inputRevision, panelWidth, spotifyTheme = false, }) => {
452
+ if (!confirm)
453
+ return null;
454
+ const includeCancelChoice = confirm.tool_name === "provider_mode_exit";
455
+ const visibleChoices = getVisibleConfirmChoices(confirm.choices, includeCancelChoice);
456
+ const selectedDisplayIndex = resolveConfirmChoiceDisplayIndex(confirm.choices, confirmIndex, includeCancelChoice);
457
+ const isSpotifyConfirm = spotifyTheme || confirm.tool_name === "spotify_device";
458
+ const isSongCandidateConfirm = confirm.tool_name === "song_candidate";
459
+ if (confirm.variant === "tool_call_review") {
460
+ const contentWidth = Math.max(1, panelWidth - 2);
461
+ return (_jsxs(PanelFrame, { width: panelWidth, title: confirm.message, hint: confirm.warning ?? "Please review the Bash command(s) below before permission.", hintColor: "#facc15", children: [(confirm.commands ?? []).map((command, commandIndex) => (_jsxs(React.Fragment, { children: [commandIndex > 0 ? _jsx(PanelEmptyRow, { width: panelWidth }) : null, wrapChatMessageContent(command, contentWidth).map((row, rowIndex) => (_jsx(PanelRow, { width: panelWidth, segments: [{ text: row, color: TOOL_VALUE }] }, `${commandIndex}-${rowIndex}-${row}`)))] }, `${commandIndex}-${command}`))), _jsx(PanelEmptyRow, { width: panelWidth }), _jsx(PanelChoiceList, { items: visibleChoices.map((choice) => ({
462
+ key: choice.value,
463
+ segments: [{ text: choice.label, color: PANEL_PRIMARY }],
464
+ })), selectedIndex: selectedDisplayIndex, width: panelWidth })] }));
465
+ }
466
+ if (isSongCandidateConfirm) {
467
+ const contentWidth = Math.max(1, panelWidth - 2);
468
+ const boundedIndex = selectedDisplayIndex;
469
+ return (_jsx(PanelFrame, { width: panelWidth, title: confirm.message, hint: "press Esc to cancel", children: visibleChoices.map((choice, index) => {
470
+ const selected = index === boundedIndex;
471
+ const isSupplementChoice = Boolean(choice.input);
472
+ const label = formatConfirmChoiceLabel({
473
+ key: choice.value,
474
+ label: choice.label,
475
+ description: choice.description,
476
+ display: choice.display,
477
+ labelWidth: CONFIRM_CHOICE_LABEL_WIDTH,
478
+ });
479
+ const visibleLabel = truncateDisplayWidth(label, contentWidth);
480
+ const item = {
481
+ key: choice.value,
482
+ segments: [{
483
+ text: visibleLabel,
484
+ color: isSupplementChoice ? PANEL_SECONDARY : PANEL_PRIMARY,
485
+ }],
486
+ unselectedBold: isSupplementChoice,
487
+ };
488
+ return (_jsxs(React.Fragment, { children: [isSupplementChoice ? (_jsx(PanelEmptyRow, { width: panelWidth })) : null, isSupplementChoice && selected ? (_jsx(PromptInput, { input: input, setInput: setInput, onSubmit: onSubmit, focus: selected && inputFocus, placeholder: "", inputRevision: inputRevision, backgroundColor: PANEL_BACKGROUND, backgroundWidth: panelWidth, backgroundPaddingX: 1 })) : (_jsx(PanelRow, { width: panelWidth, segments: resolvePanelChoiceSegments(item, selected, false) }))] }, choice.value));
489
+ }) }));
490
+ }
491
+ const choiceItems = visibleChoices.map((choice) => ({
492
+ key: choice.value,
493
+ segments: choice.display?.kind === "music_candidate"
494
+ ? [{ text: formatMusicCandidateDisplayLabel(choice.display, CONFIRM_CHOICE_ROW_LABEL_WIDTH, choice.description), color: PANEL_PRIMARY }]
495
+ : [
496
+ {
497
+ text: choice.label + " ".repeat(Math.max(0, CONFIRM_CHOICE_LABEL_WIDTH - stringWidth(choice.label))),
498
+ color: choice.disabled ? PANEL_SECONDARY : PANEL_PRIMARY,
499
+ },
500
+ ...((choice.disabled_reason ?? choice.description)
501
+ ? [{ text: choice.disabled_reason ?? choice.description ?? "", color: PANEL_SECONDARY }]
502
+ : []),
503
+ ],
504
+ }));
505
+ if (confirm.tool_name === "playlist_browse") {
506
+ const playlistItems = visibleChoices.map((choice) => ({
507
+ key: choice.value,
508
+ segments: [
509
+ { text: formatPlaylistBrowseName(choice.label), color: PANEL_PRIMARY },
510
+ { text: ` ${playlistBrowseTrackCount(choice)}`, color: PANEL_SECONDARY },
511
+ ],
512
+ }));
513
+ return (_jsx(PanelFrame, { width: panelWidth, title: confirm.message, hint: confirmCancelHint(confirm.choices), children: _jsx(PanelChoiceList, { items: playlistItems, selectedIndex: selectedDisplayIndex, width: panelWidth, spotifyTheme: isSpotifyConfirm }) }));
514
+ }
515
+ if (confirm.tool_name === "provider_mode_exit") {
516
+ return (_jsx(PanelFrame, { width: panelWidth, title: confirm.message, hint: null, titleDetailSegments: confirm.warning ? [
517
+ { text: "Warning: ", color: "#facc15", bold: true },
518
+ { text: confirm.warning, color: "#facc15", italic: true },
519
+ ] : null, children: _jsx(PanelChoiceList, { items: choiceItems, selectedIndex: selectedDisplayIndex, width: panelWidth, spotifyTheme: isSpotifyConfirm }) }));
520
+ }
521
+ return (_jsx(PanelFrame, { width: panelWidth, title: confirm.message, hint: confirm.hide_hint ? null : confirmCancelHint(confirm.choices), children: _jsx(PanelChoiceList, { items: choiceItems, selectedIndex: selectedDisplayIndex, width: panelWidth, spotifyTheme: isSpotifyConfirm }) }));
522
+ };
523
+ const LanguagePanel = ({ panel, selectedIndex, width, language = "en" }) => {
524
+ if (!panel)
525
+ return null;
526
+ const choices = orderedLanguageChoices(panel.selected);
527
+ const boundedIndex = Math.min(Math.max(selectedIndex, 0), choices.length - 1);
528
+ return (_jsxs(PanelFrame, { width: width, title: t(language, "language.title"), hint: t(language, "language.hint"), children: [_jsx(PanelChoiceList, { items: choices.map((choice) => ({
529
+ key: choice,
530
+ segments: [{
531
+ text: choice === panel.selected ? `* ${languageLabel(choice)}` : languageLabel(choice),
532
+ color: PANEL_PRIMARY,
533
+ }],
534
+ })), selectedIndex: boundedIndex, width: width }), panel.saveError ? (_jsx(PanelRow, { width: width, segments: [{ text: panel.saveError, color: "#ff9c9c" }] })) : null] }));
535
+ };
536
+ const setupDoneHint = (setupPanel, _language) => {
537
+ if (setupPanel.active)
538
+ return null;
539
+ return "press Esc to hide";
540
+ };
541
+ const setupMessageColor = (setupPanel) => {
542
+ const text = `${setupPanel.title} ${setupPanel.message}`.toLowerCase();
543
+ if (text.includes("failed") || text.includes("失败"))
544
+ return "#ff6b6b";
545
+ if (text.includes("connected") || text.includes("success") || text.includes("成功"))
546
+ return BORDER_BLUE_SOFT;
547
+ return "#bf98a7";
548
+ };
549
+ const CompactSetup = ({ setupPanel, input, setInput, onSubmit, inputPlaceholder, inputMask, inputFocus, inputRevision, terminalColumns, language = "en", }) => {
550
+ if (!setupPanel)
551
+ return null;
552
+ const panelWidth = Math.max(3, Math.floor(terminalColumns ?? 80));
553
+ const contentWidth = Math.max(1, panelWidth - 2);
554
+ const messageRows = wrapChatMessageContent(setupPanel.message, contentWidth);
555
+ return (_jsxs(PanelFrame, { width: panelWidth, title: setupPanel.title, children: [messageRows.map((row, index) => (_jsx(PanelRow, { width: panelWidth, segments: [{
556
+ text: row,
557
+ color: setupMessageColor(setupPanel),
558
+ }] }, `setup-message-${index}`))), setupDoneHint(setupPanel, language) ? (_jsx(PanelRow, { width: panelWidth, segments: [{ text: setupDoneHint(setupPanel, language) ?? "", color: PANEL_SECONDARY }] })) : null, "provider" in setupPanel && setupPanel.providers && setupPanel.providers.length > 0 ? (_jsx(PanelRow, { width: panelWidth, segments: [{
559
+ text: `${t(language, "providers.label")}: ${setupPanel.providers.map((provider) => provider.value).join(" / ")}`,
560
+ color: PANEL_SECONDARY,
561
+ }] })) : null, "provider" in setupPanel && setupPanel.methods && setupPanel.methods.length > 0 ? (_jsx(PanelRow, { width: panelWidth, segments: [{
562
+ text: `${t(language, "methods.label")}: ${setupPanel.methods.map((method) => method.value).join(" / ")}`,
563
+ color: PANEL_SECONDARY,
564
+ }] })) : null, setupPanel.active && setupPanel.prompt ? (_jsx(PromptInput, { input: input, setInput: setInput, onSubmit: onSubmit, focus: inputFocus, placeholder: setupPanel.prompt ?? inputPlaceholder, mask: setupPanel.mask ? "*" : inputMask, inputRevision: inputRevision, backgroundColor: PANEL_BACKGROUND, backgroundWidth: panelWidth, backgroundPaddingX: 1 })) : null] }));
565
+ };
566
+ const InputDock = ({ input, setInput, onSubmit, inputPlaceholder, inputMask, inputFocus, inputRevision, confirm, confirmIndex, spotifyMode, providerMode, spotifySetup, authSetup, modelStatus, slashSuggestions, slashIndex, helpPanel, helpPanelIndex, languagePanel, languagePanelIndex, modelPanelIndex, terminalColumns, minimal = false, switchHint = null, language = "en", }) => {
567
+ const selectedChoice = confirm?.choices[Math.min(confirmIndex, Math.max(0, confirm.choices.length - 1))] ?? null;
568
+ const setupPanel = spotifySetup ?? (authSetup && authSetup.step !== "model" ? authSetup : null);
569
+ const spotifyTheme = Boolean(spotifyMode?.enabled || spotifySetup);
570
+ const isSongCandidateConfirm = confirm?.tool_name === "song_candidate";
571
+ const insetPanelWidth = Math.max(3, Math.floor(terminalColumns ?? 80) - 2);
572
+ const allModelChoices = authSetup?.models ?? [];
573
+ const filteredModelChoices = filterModelChoices(allModelChoices, input);
574
+ const modelLabelWidth = modelPanelLabelWidth(allModelChoices);
575
+ const modelPanel = authSetup?.active && authSetup.step === "model"
576
+ ? {
577
+ title: authSetup.title,
578
+ hint: authSetup.message,
579
+ items: filteredModelChoices.map((model) => ({
580
+ key: model.value,
581
+ segments: [
582
+ { text: formatModelPanelLabel(model, modelLabelWidth), color: PANEL_PRIMARY },
583
+ { text: model.provider ?? model.value, color: PANEL_SECONDARY },
584
+ ],
585
+ })),
586
+ }
587
+ : null;
588
+ const showInput = !setupPanel
589
+ && !helpPanel
590
+ && !languagePanel
591
+ && !modelPanel
592
+ && (!confirm || Boolean(selectedChoice?.input) && !isSongCandidateConfirm);
593
+ const spotifyModeBorderLabel = " 🎧 Spotify Mode ";
594
+ return (_jsxs(Box, { flexDirection: "column", children: [!minimal ? (_jsxs(Box, { flexDirection: "column", flexShrink: 0, paddingX: 1, children: [_jsx(HelpPanel, { panel: helpPanel, selectedIndex: helpPanelIndex, width: insetPanelWidth, language: language }), _jsx(SlashCommandList, { suggestions: slashSuggestions, selectedIndex: slashIndex, spotifyTheme: spotifyTheme }), !isSongCandidateConfirm ? (_jsx(CompactConfirm, { confirm: confirm, confirmIndex: confirmIndex, input: input, setInput: setInput, onSubmit: onSubmit, inputFocus: inputFocus, inputRevision: inputRevision, panelWidth: insetPanelWidth, spotifyTheme: spotifyTheme })) : null, _jsx(LanguagePanel, { panel: languagePanel, selectedIndex: languagePanelIndex, width: insetPanelWidth, language: language }), modelPanel ? (_jsxs(PanelFrame, { width: insetPanelWidth, title: modelPanel.title, hint: modelPanel.hint, children: [_jsx(PanelRow, { width: insetPanelWidth, segments: [
595
+ { text: "Search: ", color: PANEL_SECONDARY },
596
+ { text: input || "type to filter", color: input ? PANEL_PRIMARY : PANEL_SECONDARY },
597
+ ] }), _jsx(PanelChoiceList, { items: modelPanel.items, selectedIndex: modelPanelIndex, visibleLimit: MAX_VISIBLE_MODEL_CHOICES, width: insetPanelWidth, spotifyTheme: spotifyTheme }), _jsx(PanelRow, { width: insetPanelWidth, segments: [{ text: t(language, "login.continue"), color: PANEL_SECONDARY, bold: true }] })] })) : null] })) : null, isSongCandidateConfirm || minimal ? (_jsx(CompactConfirm, { confirm: confirm, confirmIndex: confirmIndex, input: input, setInput: setInput, onSubmit: onSubmit, inputFocus: inputFocus, inputRevision: inputRevision, panelWidth: isSongCandidateConfirm
598
+ ? Math.max(3, Math.floor(terminalColumns ?? 80))
599
+ : insetPanelWidth, spotifyTheme: spotifyTheme })) : null, setupPanel ? _jsx(CompactSetup, { setupPanel: setupPanel, input: input, setInput: setInput, onSubmit: onSubmit, inputPlaceholder: inputPlaceholder, inputMask: inputMask, inputFocus: inputFocus, inputRevision: inputRevision, terminalColumns: terminalColumns, language: language }) : null, showInput ? (_jsxs(_Fragment, { children: [_jsx(Box, { borderTop: true, borderBottom: true, borderLeft: false, borderRight: false, borderStyle: "single", borderColor: "#808791", paddingX: 1, paddingTop: 0, flexDirection: "column", minHeight: 3, flexShrink: 0, children: _jsxs(Box, { flexDirection: "row", children: [_jsx(Text, { color: "#7f5d6b", children: minimal && switchHint ? `${switchHint} · ` : "" }), _jsx(PromptInput, { input: input, setInput: setInput, onSubmit: onSubmit, focus: inputFocus, placeholder: inputPlaceholder, mask: inputMask, inputRevision: inputRevision })] }) }), _jsxs(Box, { height: 1, paddingX: 1, flexDirection: "row", children: [_jsx(Box, { flexGrow: 1, minWidth: 0, children: modelStatus ? (_jsx(Text, { color: "#808791", wrap: "truncate-end", children: modelStatus })) : null }), _jsx(Box, { flexShrink: 0, children: spotifyMode?.enabled ? (_jsx(Text, { bold: true, color: SPOTIFY_GREEN, children: spotifyModeBorderLabel })) : null })] })] })) : null] }));
600
+ };
601
+ export const DynamicTail = ({ input, setInput, onSubmit, inputPlaceholder, inputMask, inputFocus, inputRevision, confirm, confirmIndex, spotifyMode, providerMode, spotifySetup, authSetup, modelStatus, slashSuggestions, slashIndex, helpPanel, helpPanelIndex, languagePanel, languagePanelIndex, modelPanelIndex, memoryPanel, memoryPanelIndex, memorySearchQuery, memoryEditor, terminalColumns, agentWorking, streamingMessage, language = "en", }) => {
602
+ const selectedChoice = confirm?.choices[Math.min(confirmIndex, Math.max(0, confirm.choices.length - 1))] ?? null;
603
+ const hasModelPanel = authSetup?.active && authSetup.step === "model";
604
+ const hasSetupPanel = Boolean(spotifySetup) || Boolean(authSetup && authSetup.step !== "model");
605
+ const hasSlashPanel = slashSuggestions.length > 0;
606
+ const showInput = !helpPanel && !languagePanel && !hasModelPanel && !memoryPanel && (!confirm || Boolean(selectedChoice?.input));
607
+ const showMiniMascotStatus = showInput && !confirm && !hasSlashPanel && !hasSetupPanel;
608
+ return (_jsxs(Box, { flexDirection: "column", children: [streamingMessage ? (_jsx(Box, { flexDirection: "column", paddingX: 1, children: _jsx(ChatBubble, { role: streamingMessage.role, content: streamingMessage.content, contentWidth: Math.max(1, (terminalColumns ?? 80) - 4), theme: streamingMessage.theme, tone: streamingMessage.tone, showDivider: false }) })) : null, showMiniMascotStatus ? (agentWorking ? _jsx(AgentWorkingStatus, {}) : _jsx(MiniMascotStatus, {})) : null, memoryPanel ? (_jsx(MemoryPanelOverlay, { panel: memoryPanel, selectedIndex: memoryPanelIndex, searchQuery: memorySearchQuery, editor: memoryEditor, panelWidth: Math.max(3, Math.floor(terminalColumns ?? 80) - 2) })) : null, !memoryPanel ? (_jsx(InputDock, { input: input, setInput: setInput, onSubmit: onSubmit, inputPlaceholder: inputPlaceholder, inputMask: inputMask, inputFocus: inputFocus, inputRevision: inputRevision, 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: modelPanelIndex, terminalColumns: terminalColumns, language: language })) : null] }));
609
+ };
610
+ /**
611
+ * Coordinates the use visible snapshot on revision operation for the CLI UI runtime.
612
+ *
613
+ * @param value Input value used by the use visible snapshot on revision operation.
614
+ * @param active Input value used by the use visible snapshot on revision operation.
615
+ * @param snapshotRevision Input value used by the use visible snapshot on revision operation.
616
+ * @returns The computed result for the surrounding CLI UI flow.
617
+ */
618
+ function useVisibleSnapshotOnRevision(value, active, snapshotRevision) {
619
+ const snapshotRef = React.useRef(value);
620
+ const revisionRef = React.useRef(null);
621
+ if (active && revisionRef.current !== snapshotRevision) {
622
+ snapshotRef.current = value;
623
+ revisionRef.current = snapshotRevision;
624
+ return value;
625
+ }
626
+ if (!active) {
627
+ revisionRef.current = null;
628
+ }
629
+ return snapshotRef.current;
630
+ }
631
+ const MiniPlayerRegion = ({ player, coverUrl, coverPattern, terminalSpace, miniLayout, snapshotRevision, }) => {
632
+ const miniSnapshot = useVisibleSnapshotOnRevision({
633
+ player,
634
+ coverUrl,
635
+ coverPattern,
636
+ terminalSpace,
637
+ miniLayout,
638
+ }, true, snapshotRevision);
639
+ return (_jsx(Box, { width: "100%", height: "100%", padding: 0, flexDirection: "column", flexGrow: 1, flexShrink: 1, minHeight: 0, children: _jsx(PlayerPane, { player: miniSnapshot.player, coverUrl: miniSnapshot.coverUrl, coverPattern: miniSnapshot.coverPattern, terminalSpace: miniSnapshot.terminalSpace, miniLayout: miniSnapshot.miniLayout, variant: "compact", active: true }) }));
640
+ };
641
+ const ProviderImmersiveRegion = ({ player, spotifyMode, providerMode, spotifyImmersiveLayout, }) => {
642
+ const deviceName = spotifyMode.device_name ?? "Spotify Connect";
643
+ const deviceStatus = player.is_playing ? "playing" : "paused";
644
+ const topPadding = spotifyImmersiveLayout.topPadding;
645
+ const deviceWidth = spotifyImmersiveLayout.deviceSlot.width;
646
+ return (_jsxs(Box, { width: "100%", height: "100%", flexDirection: "column", flexGrow: 1, flexShrink: 1, minHeight: 0, paddingX: 2, paddingTop: topPadding, children: [_jsx(Box, { justifyContent: "center", children: _jsx(Text, { bold: true, color: SPOTIFY_GREEN, children: "Spotify Mode" }) }), _jsx(Box, { justifyContent: "center", marginTop: 1, children: _jsx(Text, { color: "#fff4f6", wrap: "truncate-end", children: player.name }) }), _jsx(Box, { justifyContent: "center", children: _jsx(Text, { color: "#bf98a7", wrap: "truncate-end", children: formatMiniTrackSubtitle(player.artist, player.album) }) }), _jsx(Box, { height: 1, marginTop: 1 }), _jsx(Box, { justifyContent: "center", children: _jsx(Box, { width: deviceWidth > 0 ? deviceWidth : undefined, justifyContent: "center", children: _jsx(Text, { color: SPOTIFY_GREEN, wrap: "truncate-end", children: providerMode.connection_status === "disconnected" ? "reconnecting" : `${deviceStatus} on ${deviceName}` }) }) })] }));
647
+ };
648
+ export const DynamicShell = ({ input, setInput, onSubmit, inputPlaceholder, inputMask, inputFocus, inputRevision, player, coverUrl, coverPattern, confirm, confirmIndex, spotifyMode, providerMode, spotifySetup, authSetup, modelStatus, slashSuggestions, slashIndex, helpPanel, helpPanelIndex, languagePanel, languagePanelIndex, modelPanelIndex, trackPanel, trackPanelIndex, extensionPanel, extensionPanelIndex, extensionInputFocused, memoryPanel, memoryPanelIndex, memorySearchQuery, memoryEditor, activeRegion, miniSnapshotRevision, miniLayout, spotifyImmersiveLayout, terminalSpace, agentWorking, streamingMessage, language = "en", }) => {
649
+ if (activeRegion === "miniPlayer") {
650
+ return (_jsx(MiniPlayerRegion, { player: player, coverUrl: coverUrl, coverPattern: coverPattern, terminalSpace: terminalSpace, miniLayout: miniLayout, snapshotRevision: miniSnapshotRevision }));
651
+ }
652
+ if (activeRegion === "spotifyImmersive" || activeRegion === "providerImmersive") {
653
+ return (_jsx(ProviderImmersiveRegion, { player: player, spotifyMode: spotifyMode, providerMode: providerMode, spotifyImmersiveLayout: spotifyImmersiveLayout }));
654
+ }
655
+ if (activeRegion === "trackPanel" && trackPanel) {
656
+ return (_jsx(TrackPanelOverlay, { trackPanel: trackPanel, selectedIndex: trackPanelIndex, panelWidth: Math.max(3, Math.floor(terminalSpace.columns ?? 80)), spotifyTheme: spotifyMode.enabled, language: language }));
657
+ }
658
+ if (extensionPanel) {
659
+ return (_jsx(ExtensionPanelOverlay, { panel: extensionPanel, selectedIndex: extensionPanelIndex, width: Math.max(3, Math.floor(terminalSpace.columns ?? 80) - 2), input: input, setInput: setInput, onSubmit: onSubmit, inputFocus: extensionPanel.view === "setup" && extensionInputFocused }));
660
+ }
661
+ return (_jsx(DynamicTail, { input: input, setInput: setInput, onSubmit: onSubmit, inputPlaceholder: inputPlaceholder, inputMask: inputMask, inputFocus: inputFocus, inputRevision: inputRevision, 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: modelPanelIndex, memoryPanel: activeRegion === "memoryPanel" ? memoryPanel : null, memoryPanelIndex: memoryPanelIndex, memorySearchQuery: memorySearchQuery, memoryEditor: memoryEditor, terminalColumns: terminalSpace.columns, agentWorking: agentWorking, streamingMessage: streamingMessage, language: language }));
662
+ };