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/hooks.js ADDED
@@ -0,0 +1,216 @@
1
+ import React from 'react';
2
+ import WebSocket from 'ws';
3
+ import terminalImage from 'terminal-image';
4
+ import { API_NOT_RUNNING_DETAIL, API_NOT_RUNNING_MESSAGE } from './constants.js';
5
+ export const PLAYBACK_PROGRESS_INTERVAL_MS = 1000;
6
+ export function isPlaybackStarting(player) {
7
+ return player.playback_status === "starting";
8
+ }
9
+ export function isPlaybackProgressFrozen(player) {
10
+ return isPlaybackStarting(player)
11
+ || player.progress_sync_lost === true
12
+ || player.paused_for_cache === true;
13
+ }
14
+ /**
15
+ * Coordinates the should use playback progress timer operation for the CLI UI runtime.
16
+ *
17
+ * @param player Input value used by the should use playback progress timer operation.
18
+ * @param active Input value used by the should use playback progress timer operation.
19
+ * @returns The computed result for the surrounding CLI UI flow.
20
+ */
21
+ export function shouldUsePlaybackProgressTimer(player, active = true) {
22
+ return active
23
+ && player.is_playing === true
24
+ && !isPlaybackProgressFrozen(player);
25
+ }
26
+ /**
27
+ * Coordinates the playback progress at operation for the CLI UI runtime.
28
+ *
29
+ * @param player Input value used by the playback progress at operation.
30
+ * @param now Input value used by the playback progress at operation.
31
+ * @returns The computed result for the surrounding CLI UI flow.
32
+ */
33
+ export function playbackProgressAt(player, now) {
34
+ const base = player.progress_ms ?? 0;
35
+ if (isPlaybackProgressFrozen(player))
36
+ return base;
37
+ const reference = player.progress_anchor_ms ?? player.timestamp ?? player.started_at;
38
+ const liveOffset = player.is_playing && reference ? Math.max(0, now - reference) : 0;
39
+ const progress = base + liveOffset;
40
+ return player.duration_ms > 0 ? Math.min(player.duration_ms, progress) : progress;
41
+ }
42
+ /**
43
+ * Coordinates the use playback progress operation for the CLI UI runtime.
44
+ *
45
+ * @param player Input value used by the use playback progress operation.
46
+ * @param active Input value used by the use playback progress operation.
47
+ * @returns The computed result for the surrounding CLI UI flow.
48
+ */
49
+ export function usePlaybackProgress(player, active = true) {
50
+ const [now, setNow] = React.useState(Date.now());
51
+ React.useEffect(() => {
52
+ if (!shouldUsePlaybackProgressTimer(player, active)) {
53
+ setNow(Date.now());
54
+ return;
55
+ }
56
+ const timer = setInterval(() => setNow(Date.now()), PLAYBACK_PROGRESS_INTERVAL_MS);
57
+ return () => clearInterval(timer);
58
+ }, [
59
+ active,
60
+ player.is_playing,
61
+ player.paused_for_cache,
62
+ player.progress_anchor_ms,
63
+ player.progress_ms,
64
+ player.progress_sync_lost,
65
+ player.started_at,
66
+ player.timestamp,
67
+ ]);
68
+ return playbackProgressAt(player, now);
69
+ }
70
+ /**
71
+ * Coordinates the use cover art operation for the CLI UI runtime.
72
+ *
73
+ * @param url Input value used by the use cover art operation.
74
+ * @param width Input value used by the use cover art operation.
75
+ * @param height Input value used by the use cover art operation.
76
+ * @returns The computed result for the surrounding CLI UI flow.
77
+ */
78
+ export function useCoverArt(url, width = 32, height = 16) {
79
+ const [art, setArt] = React.useState(null);
80
+ const [failed, setFailed] = React.useState(false);
81
+ React.useEffect(() => {
82
+ if (!url) {
83
+ setArt(null);
84
+ setFailed(false);
85
+ return;
86
+ }
87
+ let cancelled = false;
88
+ setArt(null);
89
+ setFailed(false);
90
+ const load = async () => {
91
+ const start = Date.now();
92
+ try {
93
+ const response = await fetch(url);
94
+ if (!response.ok) {
95
+ throw new Error(`HTTP ${response.status}`);
96
+ }
97
+ const fetchedAt = Date.now();
98
+ const arrayBuffer = await response.arrayBuffer();
99
+ const rendered = await terminalImage.buffer(Buffer.from(arrayBuffer), {
100
+ width,
101
+ height,
102
+ preserveAspectRatio: true,
103
+ });
104
+ if (process.env.SONEX_PLAYER_DEBUG === '1') {
105
+ const decodedAt = Date.now();
106
+ console.error(`[sonex-player-debug] cover fetch ${fetchedAt - start}ms decode ${decodedAt - fetchedAt}ms url=${url}`);
107
+ }
108
+ if (!cancelled) {
109
+ setArt(rendered);
110
+ }
111
+ }
112
+ catch (err) {
113
+ if (process.env.SONEX_PLAYER_DEBUG === '1') {
114
+ const detail = err instanceof Error ? err.message : String(err);
115
+ console.error(`[sonex-player-debug] cover fetch/decode failed after ${Date.now() - start}ms: ${detail}`);
116
+ }
117
+ if (!cancelled) {
118
+ setFailed(true);
119
+ }
120
+ }
121
+ };
122
+ void load();
123
+ return () => {
124
+ cancelled = true;
125
+ };
126
+ }, [url, width, height]);
127
+ return { art, failed };
128
+ }
129
+ /**
130
+ * Coordinates the is http cover source operation for the CLI UI runtime.
131
+ *
132
+ * @param url Input value used by the is http cover source operation.
133
+ * @returns The computed result for the surrounding CLI UI flow.
134
+ */
135
+ export function isHttpCoverSource(url) {
136
+ return Boolean(url && /^https?:\/\//i.test(url));
137
+ }
138
+ /**
139
+ * Coordinates the use latest callback operation for the CLI UI runtime.
140
+ *
141
+ * @param callback Input value used by the use latest callback operation.
142
+ * @returns The computed result for the surrounding CLI UI flow.
143
+ */
144
+ export function useLatestCallback(callback) {
145
+ const ref = React.useRef(callback);
146
+ React.useEffect(() => {
147
+ ref.current = callback;
148
+ }, [callback]);
149
+ return ref;
150
+ }
151
+ /**
152
+ * Coordinates the use sonex socket operation for the CLI UI runtime.
153
+ *
154
+ * @param url,onEvent,onConnectionChange,onClientError Input value used by the use sonex socket operation.
155
+ * @returns The computed result for the surrounding CLI UI flow.
156
+ */
157
+ export function useSonexSocket({ url, onEvent, onConnectionChange, onClientError }) {
158
+ const wsRef = React.useRef(null);
159
+ const onEventRef = useLatestCallback(onEvent);
160
+ const onConnectionChangeRef = useLatestCallback(onConnectionChange);
161
+ const onClientErrorRef = useLatestCallback(onClientError);
162
+ React.useEffect(() => {
163
+ let closedByUser = false;
164
+ let connectionErrorShown = false;
165
+ let reconnectTimer = null;
166
+ const connect = () => {
167
+ const ws = new WebSocket(url);
168
+ wsRef.current = ws;
169
+ ws.onopen = () => {
170
+ connectionErrorShown = false;
171
+ onConnectionChangeRef.current?.(true);
172
+ };
173
+ ws.onclose = () => {
174
+ onConnectionChangeRef.current?.(false);
175
+ if (!closedByUser) {
176
+ reconnectTimer = setTimeout(connect, 1500);
177
+ }
178
+ };
179
+ ws.onerror = (err) => {
180
+ onConnectionChangeRef.current?.(false);
181
+ if (!connectionErrorShown) {
182
+ const detail = err.message || undefined;
183
+ onClientErrorRef.current?.(`${API_NOT_RUNNING_MESSAGE}. ${API_NOT_RUNNING_DETAIL}`, detail);
184
+ connectionErrorShown = true;
185
+ }
186
+ };
187
+ ws.onmessage = (msg) => {
188
+ try {
189
+ const data = JSON.parse(msg.data.toString());
190
+ onEventRef.current(data);
191
+ }
192
+ catch (err) {
193
+ const detail = err instanceof Error ? err.message : String(err);
194
+ onClientErrorRef.current?.("Invalid server message", detail);
195
+ }
196
+ };
197
+ };
198
+ connect();
199
+ return () => {
200
+ closedByUser = true;
201
+ if (reconnectTimer) {
202
+ clearTimeout(reconnectTimer);
203
+ }
204
+ wsRef.current?.close();
205
+ };
206
+ }, [url]);
207
+ const send = React.useCallback((payload) => {
208
+ const ws = wsRef.current;
209
+ if (ws && ws.readyState === ws.OPEN) {
210
+ ws.send(JSON.stringify(payload));
211
+ return true;
212
+ }
213
+ return false;
214
+ }, []);
215
+ return { send };
216
+ }
package/dist/i18n.js ADDED
@@ -0,0 +1,291 @@
1
+ export const OFFICIAL_UI_LANGUAGE = "en";
2
+ const messages = {
3
+ en: {
4
+ "activity.empty": "Waiting for agent activity.",
5
+ "api.notRunning.detail": "Start with `sonex`, or run `sonex api` before `sonex tui`.",
6
+ "api.notRunning.message": "Sonex API is not running",
7
+ "auth.oauth.return": "Complete the OAuth flow in your browser, then return here.",
8
+ "auth.oauth.waiting": "Waiting for browser authorization...",
9
+ "chat.empty": "No messages yet.",
10
+ "command.lang.description": "Choose the TUI display language.",
11
+ "help.empty": "No matching commands.",
12
+ "help.hint": "Use Up/Down to choose, Esc to close.",
13
+ "help.title": "Sonex commands",
14
+ "input.label": "Input",
15
+ "input.placeholder": "Ask Sonex anything.",
16
+ "input.recommendPending": "Waiting for recommendations...",
17
+ "language.english": "English",
18
+ "language.hint": "Esc to close without changing.",
19
+ "language.saveError": "Language changed for this session, but the setting was not saved.",
20
+ "language.saved": "Language set to {language}.",
21
+ "language.simplifiedChinese": "简体中文",
22
+ "language.title": "Language",
23
+ "launch.preparing": "Preparing playback",
24
+ "login.continue": "↑/↓ to select · Enter to continue · Esc to close",
25
+ "login.warmup": "Complete setup to continue.",
26
+ "methods.label": "Methods",
27
+ "panel.confirmHidden": "Confirmation panel hidden.",
28
+ "panel.helpHidden": "Help panel hidden.",
29
+ "panel.languageHidden": "Language panel hidden.",
30
+ "panel.modelHidden": "Model selection panel hidden.",
31
+ "panel.setupHidden": "Setup panel hidden.",
32
+ "panel.spotifySetupHidden": "Spotify setup panel hidden.",
33
+ "providers.label": "Providers",
34
+ "status.saving": "Saving session...",
35
+ "status.snoozing": "Idle...",
36
+ "tips.placeholder": "Tip: use /random to play a recent song.",
37
+ "trackPanel.playlist": "Playlist",
38
+ "trackPanel.playlistHidden": "Playlist panel hidden.",
39
+ "trackPanel.playlistEmpty": "Playlist is empty.",
40
+ "trackPanel.queue": "Queue",
41
+ "trackPanel.queueHidden": "Queue panel hidden.",
42
+ "trackPanel.queueEmpty": "Queue is empty.",
43
+ },
44
+ "zh-CN": {
45
+ "activity.empty": "等待代理活动。",
46
+ "api.notRunning.detail": "先运行 `sonex`,或在 `sonex tui` 前运行 `sonex api`。",
47
+ "api.notRunning.message": "Sonex API 未运行",
48
+ "auth.oauth.return": "在浏览器中完成 OAuth 流程,然后回到这里。",
49
+ "auth.oauth.waiting": "等待浏览器授权...",
50
+ "chat.empty": "还没有消息。",
51
+ "command.lang.description": "选择 TUI 显示语言。",
52
+ "help.empty": "没有匹配的命令。",
53
+ "help.hint": "使用上下键选择,Esc 关闭。",
54
+ "help.title": "Sonex 命令",
55
+ "input.label": "输入",
56
+ "input.placeholder": "和 Sonex 说点什么。",
57
+ "input.recommendPending": "等待Sonex推荐中...",
58
+ "language.english": "English",
59
+ "language.hint": "Esc 关闭且不更改。",
60
+ "language.saveError": "语言已在本会话切换,但设置未保存。",
61
+ "language.saved": "语言已设置为 {language}。",
62
+ "language.simplifiedChinese": "简体中文",
63
+ "language.title": "语言",
64
+ "launch.preparing": "启动准备中",
65
+ "login.continue": "↑/↓ to select · Enter to continue · Esc to close",
66
+ "login.warmup": "开始前先完成一个小设置。",
67
+ "methods.label": "方式",
68
+ "panel.confirmHidden": "确认面板已收起。",
69
+ "panel.helpHidden": "帮助面板已收起。",
70
+ "panel.languageHidden": "语言面板已收起。",
71
+ "panel.modelHidden": "模型选择面板已收起。",
72
+ "panel.setupHidden": "配置面板已收起。",
73
+ "panel.spotifySetupHidden": "Spotify 配置面板已收起。",
74
+ "providers.label": "服务",
75
+ "status.saving": "正在保存会话...",
76
+ "status.snoozing": "休眠中...",
77
+ "tips.placeholder": "提示:试试 /random 随机播放。",
78
+ "trackPanel.playlist": "歌单",
79
+ "trackPanel.playlistHidden": "歌单面板已收起。",
80
+ "trackPanel.playlistEmpty": "歌单为空。",
81
+ "trackPanel.queue": "播放队列",
82
+ "trackPanel.queueHidden": "播放队列已收起。",
83
+ "trackPanel.queueEmpty": "播放队列为空。",
84
+ },
85
+ };
86
+ const shortcutCommandDescriptions = {
87
+ bye: { en: "save and exit", "zh-CN": "保存会话并退出" },
88
+ help: { en: "show commands", "zh-CN": "显示可用的 Sonex 命令" },
89
+ info: { en: "show runtime info", "zh-CN": "显示当前运行信息" },
90
+ lang: { en: "choose display language", "zh-CN": "选择 TUI 显示语言" },
91
+ logout: { en: "sign out and exit", "zh-CN": "退出当前 LLM 服务登录并关闭" },
92
+ model: { en: "switch active model", "zh-CN": "切换当前模型" },
93
+ player: { en: "detect and set default player", "zh-CN": "检测并设置默认播放器" },
94
+ playlist: { en: "browse or save playlists", "zh-CN": "浏览或保存播放列表" },
95
+ queue: { en: "show playback queue", "zh-CN": "显示播放队列" },
96
+ quit: { en: "save and exit", "zh-CN": "保存会话并退出" },
97
+ random: { en: "play a recent song", "zh-CN": "从最近歌曲中播放" },
98
+ recommend: { en: "recommend songs", "zh-CN": "按偏好的音乐口味推荐歌曲" },
99
+ resume: { en: "resume playback", "zh-CN": "继续当前播放" },
100
+ sandbox: { en: "check Agent Bash sandbox", "zh-CN": "检查 Agent Bash 沙箱" },
101
+ spotify: { en: "toggle Spotify mode", "zh-CN": "进入或退出持久化 Spotify 模式" },
102
+ };
103
+ const helpCommandDescriptions = {
104
+ bye: { en: "save the current session and exit safely", "zh-CN": "保存会话并退出" },
105
+ exit: { en: "save the current session and exit safely", "zh-CN": "保存会话并退出" },
106
+ help: { en: "show available Sonex commands", "zh-CN": "显示可用的 Sonex 命令" },
107
+ info: { en: "show current runtime information", "zh-CN": "显示当前运行信息" },
108
+ lang: { en: "choose the TUI display language", "zh-CN": "选择 TUI 显示语言" },
109
+ logout: { en: "sign out from the current LLM provider and exit", "zh-CN": "退出当前 LLM 服务登录并关闭" },
110
+ model: { en: "switch the active model for this session", "zh-CN": "切换当前模型" },
111
+ player: { en: "detect available players and set the device default", "zh-CN": "检测可用播放器并设置设备默认值" },
112
+ playlist: { en: "browse playlists or save the current song", "zh-CN": "浏览或保存播放列表" },
113
+ queue: { en: "show the playback queue", "zh-CN": "显示播放队列" },
114
+ random: { en: "play a random song from the recent Sonex queue", "zh-CN": "从最近歌曲中播放" },
115
+ recommend: { en: "recommend songs based on a taste hint", "zh-CN": "按偏好的音乐口味推荐歌曲" },
116
+ resume: { en: "resume current local playback", "zh-CN": "继续当前播放" },
117
+ sandbox: { en: "check or configure the Agent Bash sandbox", "zh-CN": "检查或配置 Agent Bash 沙箱" },
118
+ spotify: { en: "enter or exit persistent Spotify mode", "zh-CN": "进入或退出持久化 Spotify 模式" },
119
+ };
120
+ const knownText = {
121
+ "Snoozing...": {
122
+ en: "Idle...",
123
+ "zh-CN": "休眠中...",
124
+ },
125
+ "Launch preparing...": {
126
+ en: "Preparing playback...",
127
+ "zh-CN": "启动准备中...",
128
+ },
129
+ "Sonex commands": {
130
+ en: "Sonex commands",
131
+ "zh-CN": "Sonex 命令",
132
+ },
133
+ "Use Up/Down to choose, Esc to close.": {
134
+ en: "Use Up/Down to choose, Esc to close.",
135
+ "zh-CN": "使用上下键选择,Esc 关闭。",
136
+ },
137
+ "Spotify setup": {
138
+ en: "Spotify setup",
139
+ "zh-CN": "Spotify 设置",
140
+ },
141
+ "Paste your Spotify client ID.": {
142
+ en: "Paste your Spotify client ID.",
143
+ "zh-CN": "粘贴你的 Spotify Client ID。",
144
+ },
145
+ "Spotify client ID": {
146
+ en: "Spotify client ID",
147
+ "zh-CN": "Spotify Client ID",
148
+ },
149
+ "The /lang command is handled by the TUI for this session.": {
150
+ en: "The /lang command is handled by the TUI for this session.",
151
+ "zh-CN": "/lang 命令由本次 TUI 会话处理。",
152
+ },
153
+ "Confirm player launch.": {
154
+ en: "Confirm player launch.",
155
+ "zh-CN": "确认启动播放器。",
156
+ },
157
+ "Allow Sonex to open mpv?": {
158
+ en: "Allow Sonex to open mpv?",
159
+ "zh-CN": "允许 Sonex 打开 mpv 吗?",
160
+ },
161
+ "YouTube playback is not configured. Open /extension to configure it.": {
162
+ en: "YouTube playback is not configured. Open /extension to configure it.",
163
+ "zh-CN": "YouTube 播放尚未配置,请打开 /extension 进行配置。",
164
+ },
165
+ "Another YouTube request is still running. Try again shortly.": {
166
+ en: "Another YouTube request is still running. Try again shortly.",
167
+ "zh-CN": "另一个 YouTube 请求仍在运行,请稍后再试。",
168
+ },
169
+ "Selected YouTube result requires age verification. Choose another candidate or refine the search.": {
170
+ en: "Selected YouTube result requires age verification. Choose another candidate or refine the search.",
171
+ "zh-CN": "所选 YouTube 结果需要年龄验证,请选择其他候选或优化搜索。",
172
+ },
173
+ "Selected YouTube result is not available. Choose another candidate or refine the search.": {
174
+ en: "Selected YouTube result is not available. Choose another candidate or refine the search.",
175
+ "zh-CN": "所选 YouTube 结果不可用,请选择其他候选或优化搜索。",
176
+ },
177
+ "YouTube is temporarily unavailable; playback is cooling down.": {
178
+ en: "YouTube is temporarily unavailable; playback is cooling down.",
179
+ "zh-CN": "YouTube 暂时不可用,播放正在进入冷却。",
180
+ },
181
+ "YouTube is temporarily unavailable; search is cooling down.": {
182
+ en: "YouTube is temporarily unavailable; search is cooling down.",
183
+ "zh-CN": "YouTube 暂时不可用,搜索正在进入冷却。",
184
+ },
185
+ };
186
+ const playerConfirmChoices = {
187
+ mpv: {
188
+ en: { label: "mpv", description: "default backend for smooth background playback" },
189
+ "zh-CN": { label: "mpv", description: "默认播放后端,提供更丝滑的播放体验" },
190
+ },
191
+ deny: {
192
+ en: { label: "Cancel" },
193
+ "zh-CN": { label: "取消" },
194
+ },
195
+ };
196
+ export function t(language, key, values = {}) {
197
+ let text = messages[language][key] ?? messages.en[key];
198
+ for (const [name, value] of Object.entries(values)) {
199
+ text = text.replaceAll(`{${name}}`, value);
200
+ }
201
+ return text;
202
+ }
203
+ export function languageLabel(language) {
204
+ return language === "zh-CN" ? t(language, "language.simplifiedChinese") : t(language, "language.english");
205
+ }
206
+ export function localizeSlashCommands(commands, language) {
207
+ return commands.map((command) => ({
208
+ ...command,
209
+ description: shortcutCommandDescriptions[command.name]?.[language] ?? command.description,
210
+ }));
211
+ }
212
+ export function helpCommandsForLanguage(commands, language) {
213
+ return commands.map((command) => ({
214
+ ...command,
215
+ description: helpCommandDescriptions[command.name]?.[language] ?? command.description,
216
+ }));
217
+ }
218
+ function translateKnown(value, language) {
219
+ if (value == null)
220
+ return value;
221
+ return knownText[value]?.[language] ?? value;
222
+ }
223
+ function localizeConfirmChoice(choice, stage, language) {
224
+ const value = String(choice.value || "");
225
+ const table = stage === "player_confirm" ? playerConfirmChoices : null;
226
+ const mapped = table?.[value]?.[language];
227
+ if (!mapped)
228
+ return choice;
229
+ return {
230
+ ...choice,
231
+ ...mapped,
232
+ input: choice.input,
233
+ };
234
+ }
235
+ export function applyLanguageToServerEvent(event, language) {
236
+ switch (event.type) {
237
+ case "status":
238
+ return { ...event, message: translateKnown(event.message, language) ?? event.message };
239
+ case "activity":
240
+ return {
241
+ ...event,
242
+ title: translateKnown(event.title, language) ?? event.title,
243
+ detail: translateKnown(event.detail, language) ?? event.detail,
244
+ };
245
+ case "spotify_setup":
246
+ return {
247
+ ...event,
248
+ title: translateKnown(event.title, language) ?? event.title,
249
+ message: translateKnown(event.message, language) ?? event.message,
250
+ prompt: translateKnown(event.prompt, language) ?? event.prompt,
251
+ };
252
+ case "auth_setup":
253
+ return {
254
+ ...event,
255
+ title: translateKnown(event.title, language) ?? event.title,
256
+ message: translateKnown(event.message, language) ?? event.message,
257
+ prompt: translateKnown(event.prompt, language) ?? event.prompt,
258
+ };
259
+ case "help_panel":
260
+ return {
261
+ ...event,
262
+ title: translateKnown(event.title, language) ?? event.title,
263
+ hint: translateKnown(event.hint, language) ?? event.hint,
264
+ commands: helpCommandsForLanguage(event.commands, language),
265
+ };
266
+ case "error":
267
+ return {
268
+ ...event,
269
+ message: translateKnown(event.message, language) ?? event.message,
270
+ detail: translateKnown(event.detail, language) ?? event.detail,
271
+ };
272
+ case "confirm":
273
+ return {
274
+ ...event,
275
+ message: translateKnown(event.message, language) ?? event.message,
276
+ choices: event.choices?.map((choice) => localizeConfirmChoice(choice, event.tool_args.stage, language)) ?? event.choices,
277
+ };
278
+ case "bye":
279
+ return {
280
+ ...event,
281
+ message: translateKnown(event.message, language) ?? event.message,
282
+ };
283
+ case "chat":
284
+ return {
285
+ ...event,
286
+ text: knownText[event.text]?.[language] ?? event.text,
287
+ };
288
+ default:
289
+ return event;
290
+ }
291
+ }
package/dist/index.js ADDED
@@ -0,0 +1,46 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { render } from 'ink';
3
+ import { App } from './App.js';
4
+ import { createIncrementalStdout } from './terminal-frame-writer.js';
5
+ import { TerminalSurfaceController } from './terminal-surface.js';
6
+ const incrementalStdout = createIncrementalStdout(process.stdout);
7
+ const terminalSurface = new TerminalSurfaceController({
8
+ isTTY: process.stdout.isTTY === true,
9
+ write: (value) => {
10
+ process.stdout.write(value);
11
+ },
12
+ resetFrame: incrementalStdout.reset,
13
+ });
14
+ terminalSurface.prepare();
15
+ let app;
16
+ try {
17
+ app = render(_jsx(App, { terminalSurface: terminalSurface, terminalStdout: process.stdout }), {
18
+ exitOnCtrlC: false,
19
+ stdin: process.stdin,
20
+ stdout: incrementalStdout,
21
+ });
22
+ }
23
+ catch (error) {
24
+ terminalSurface.dispose();
25
+ throw error;
26
+ }
27
+ terminalSurface.attachRendererClear(() => app.clear());
28
+ const cleanup = () => terminalSurface.dispose();
29
+ const stopFromSignal = (exitCode) => {
30
+ process.exitCode = exitCode;
31
+ try {
32
+ app.unmount();
33
+ }
34
+ finally {
35
+ cleanup();
36
+ }
37
+ };
38
+ const onSigint = () => stopFromSignal(130);
39
+ const onSigterm = () => stopFromSignal(143);
40
+ process.once('SIGINT', onSigint);
41
+ process.once('SIGTERM', onSigterm);
42
+ void app.waitUntilExit().finally(() => {
43
+ process.removeListener('SIGINT', onSigint);
44
+ process.removeListener('SIGTERM', onSigterm);
45
+ cleanup();
46
+ });
@@ -0,0 +1,37 @@
1
+ import { homedir } from 'node:os';
2
+ export function formatWorkingDirectory(cwd, homeDirectory = homedir()) {
3
+ if (!homeDirectory)
4
+ return cwd;
5
+ const windowsStyle = /^[A-Za-z]:[\\/]/.test(cwd) || cwd.includes("\\");
6
+ const normalizeForComparison = (value) => (windowsStyle ? value.replaceAll("/", "\\").toLowerCase() : value.replaceAll("\\", "/"));
7
+ const separator = windowsStyle ? "\\" : "/";
8
+ const normalizedCwd = normalizeForComparison(cwd).replace(/[\\/]+$/, "");
9
+ const normalizedHome = normalizeForComparison(homeDirectory).replace(/[\\/]+$/, "");
10
+ if (normalizedCwd === normalizedHome)
11
+ return "~";
12
+ if (!normalizedCwd.startsWith(`${normalizedHome}${separator}`))
13
+ return cwd;
14
+ const relative = cwd.slice(homeDirectory.replace(/[\\/]+$/, "").length).replace(/^[\\/]+/, "");
15
+ return relative ? `~${separator}${relative}` : "~";
16
+ }
17
+ export function createInfoBannerItem(authState, cwd, sessionId, { showLogo = false } = {}) {
18
+ return {
19
+ type: "info_banner",
20
+ authState: { ...authState },
21
+ cwd,
22
+ sessionId,
23
+ showLogo,
24
+ };
25
+ }
26
+ export function isChatMessageItem(item) {
27
+ return item.type === "message";
28
+ }
29
+ export function chatMessagesForTranscript(items) {
30
+ return items.filter(isChatMessageItem).map(({ role, content, theme, segments, document }) => ({
31
+ role,
32
+ content,
33
+ ...(theme == null ? {} : { theme }),
34
+ ...(segments == null ? {} : { segments }),
35
+ ...(document == null ? {} : { document }),
36
+ }));
37
+ }
@@ -0,0 +1,6 @@
1
+ const ANSI_INVERSE_ON = "\u001B[7m";
2
+ const ANSI_INVERSE_OFF = "\u001B[27m";
3
+ export const INPUT_CURSOR_BLINK_INTERVAL_MS = 500;
4
+ export const hideInputCursor = (output) => (output
5
+ .replaceAll(ANSI_INVERSE_ON, "")
6
+ .replaceAll(ANSI_INVERSE_OFF, ""));
@@ -0,0 +1,41 @@
1
+ import { hasSlashCommandArguments, matchingSlashCommand } from './commands.js';
2
+ import { resolveConfirmDecisionFromInput, resolveConfirmInputDecision } from './confirm-choice.js';
3
+ export function resolveInputRoute(value, context) {
4
+ const text = value.trim();
5
+ if (!text)
6
+ return { type: 'empty' };
7
+ if (context.confirm) {
8
+ const inputDecision = resolveConfirmInputDecision(text, context.selectedConfirmChoice);
9
+ if (inputDecision)
10
+ return { type: 'confirm', decision: inputDecision };
11
+ const decision = resolveConfirmDecisionFromInput(text, context.selectableConfirmChoices);
12
+ return decision ? { type: 'confirm', decision } : { type: 'ignore' };
13
+ }
14
+ if (context.extensionPanelActive) {
15
+ return context.extensionInputFocused
16
+ && context.extensionSetupInput
17
+ ? { type: 'extension_input', value: text }
18
+ : { type: 'ignore' };
19
+ }
20
+ const command = matchingSlashCommand(text);
21
+ if (!context.authSetupActive && !context.spotifySetupActive) {
22
+ if (command?.name === 'bye' || command?.name === 'exit') {
23
+ return { type: 'safe_exit', reason: command.name };
24
+ }
25
+ if (command?.name === 'info')
26
+ return { type: 'info' };
27
+ if (text.startsWith('/') && !command) {
28
+ return context.selectedSlashCommand
29
+ ? { type: 'slash_completion', command: context.selectedSlashCommand }
30
+ : { type: 'unknown_slash', value: text };
31
+ }
32
+ if (command?.needsArgument && !hasSlashCommandArguments(text)) {
33
+ return { type: 'slash_completion', command };
34
+ }
35
+ }
36
+ if (context.spotifySetupActive)
37
+ return { type: 'setup_input', channel: 'spotify', value: text };
38
+ if (context.authSetupActive)
39
+ return { type: 'setup_input', channel: 'auth', value: text };
40
+ return { type: 'user_input', value: text, command };
41
+ }
@@ -0,0 +1,30 @@
1
+ export const LAUNCH_PREPARING_INTERVAL_MS = 1000;
2
+ /**
3
+ * Coordinates the launch preparing text operation for the CLI UI runtime.
4
+ *
5
+ * @param frame Input value used by the launch preparing text operation.
6
+ * @returns The computed result for the surrounding CLI UI flow.
7
+ */
8
+ export function launchPreparingText(frame, language = "en") {
9
+ const dotCount = (Math.max(0, frame) % 3) + 1;
10
+ const base = language === "zh-CN" ? "播放准备中" : "Preparing playback";
11
+ return `${base}${'.'.repeat(dotCount)}`;
12
+ }
13
+ /**
14
+ * Coordinates the should start launch preparing operation for the CLI UI runtime.
15
+ *
16
+ * @param activity Input value used by the should start launch preparing operation.
17
+ * @returns The computed result for the surrounding CLI UI flow.
18
+ */
19
+ export function shouldStartLaunchPreparing(activity) {
20
+ const launchPreparingTitles = new Set([
21
+ 'Searching Spotify',
22
+ 'Searching online audio',
23
+ 'Caching online audio',
24
+ 'Searching YouTube',
25
+ 'Caching YouTube audio',
26
+ ]);
27
+ return activity.kind === 'tool'
28
+ && activity.status === 'pending'
29
+ && launchPreparingTitles.has(activity.title);
30
+ }