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/layout.js ADDED
@@ -0,0 +1,134 @@
1
+ const CHAT_HEADER_MASCOT_MIN_COLUMNS = 72;
2
+ const MINI_COLUMN_GAP = 1;
3
+ const MINI_INFO_RATIO = 0.32;
4
+ const MINI_INFO_MIN_COLUMNS = 24;
5
+ const MINI_INFO_MAX_COLUMNS = 40;
6
+ const MINI_INFO_LEFT_PADDING = 4;
7
+ const MINI_COVER_MIN_COLUMNS = 32;
8
+ const MINI_COVER_MIN_ROWS = 16;
9
+ const MINI_COVER_TARGET_COLUMNS = 80;
10
+ const MINI_CONTENT_START_ROW = 1;
11
+ const MINI_CONTENT_START_COLUMN = 1;
12
+ const MINI_INFO_ROWS = 4;
13
+ const SPOTIFY_IMMERSIVE_ROWS = 10;
14
+ const SPOTIFY_PROGRESS_MIN_COLUMNS = 18;
15
+ const SPOTIFY_PROGRESS_MAX_COLUMNS = 48;
16
+ export function resolveChatHeaderVariant(columns) {
17
+ const availableColumns = columns ?? 0;
18
+ if (availableColumns >= CHAT_HEADER_MASCOT_MIN_COLUMNS)
19
+ return 'mascot';
20
+ return 'compact';
21
+ }
22
+ export function resolveMiniPlayerLayout(size) {
23
+ const columns = Math.max(0, size.columns ?? 0);
24
+ const rows = Math.max(0, size.rows ?? 0);
25
+ const contentColumns = columns;
26
+ const contentRows = rows;
27
+ const hasArtworkSpace = contentColumns >= MINI_INFO_MIN_COLUMNS + MINI_COLUMN_GAP + MINI_COVER_MIN_COLUMNS
28
+ && contentRows >= MINI_COVER_MIN_ROWS;
29
+ const mode = hasArtworkSpace ? 'artwork' : 'infoOnly';
30
+ const gap = hasArtworkSpace ? MINI_COLUMN_GAP : 0;
31
+ const availableSplitColumns = Math.max(0, contentColumns - gap);
32
+ const targetCoverRowsFit = contentRows >= MINI_COVER_TARGET_COLUMNS / 2;
33
+ const priorityCoverWidth = targetCoverRowsFit
34
+ ? Math.min(MINI_COVER_TARGET_COLUMNS, Math.max(MINI_COVER_MIN_COLUMNS, availableSplitColumns - MINI_INFO_MIN_COLUMNS))
35
+ : 0;
36
+ const preferredInfoWidth = targetCoverRowsFit
37
+ ? availableSplitColumns - priorityCoverWidth
38
+ : Math.floor(availableSplitColumns * MINI_INFO_RATIO);
39
+ const maxInfoWidth = Math.min(MINI_INFO_MAX_COLUMNS, Math.max(0, availableSplitColumns - MINI_COVER_MIN_COLUMNS));
40
+ const infoWidth = hasArtworkSpace
41
+ ? Math.min(Math.max(preferredInfoWidth, MINI_INFO_MIN_COLUMNS), maxInfoWidth)
42
+ : contentColumns;
43
+ const infoLeftPadding = hasArtworkSpace ? MINI_INFO_LEFT_PADDING : 0;
44
+ const infoInnerWidth = Math.max(0, infoWidth - infoLeftPadding);
45
+ const coverWidth = hasArtworkSpace ? Math.max(0, availableSplitColumns - infoWidth) : 0;
46
+ const infoTop = Math.max(0, Math.floor((contentRows - MINI_INFO_ROWS) / 2));
47
+ return {
48
+ mode,
49
+ contentColumns,
50
+ contentRows,
51
+ infoWidth,
52
+ infoLeftPadding,
53
+ infoTop,
54
+ gap,
55
+ coverWidth,
56
+ progressSlot: {
57
+ row: Math.max(1, Math.min(Math.max(1, rows - 1), MINI_CONTENT_START_ROW + infoTop + MINI_INFO_ROWS - 2)),
58
+ column: MINI_CONTENT_START_COLUMN + infoLeftPadding,
59
+ width: infoInnerWidth,
60
+ },
61
+ statusIconSlot: {
62
+ row: Math.max(1, Math.min(Math.max(1, rows - 1), MINI_CONTENT_START_ROW + infoTop + MINI_INFO_ROWS - 1)),
63
+ column: MINI_CONTENT_START_COLUMN + infoLeftPadding,
64
+ width: infoInnerWidth,
65
+ },
66
+ };
67
+ }
68
+ export function resolveSpotifyImmersiveLayout(size) {
69
+ const columns = Math.max(0, size.columns ?? 0);
70
+ const rows = Math.max(0, size.rows ?? 0);
71
+ const topPadding = Math.max(1, Math.floor((Math.max(rows, 1) - SPOTIFY_IMMERSIVE_ROWS) / 2));
72
+ const preferredWidth = Math.min(SPOTIFY_PROGRESS_MAX_COLUMNS, Math.max(SPOTIFY_PROGRESS_MIN_COLUMNS, Math.max(columns - 16, 0)));
73
+ const width = Math.max(0, Math.min(Math.max(columns - 4, 0), preferredWidth));
74
+ const row = Math.max(1, Math.min(Math.max(1, rows - 1), topPadding + 6));
75
+ const deviceRow = Math.max(1, Math.min(Math.max(1, rows), row + 1));
76
+ const column = Math.max(1, Math.floor((Math.max(columns, width) - width) / 2) + 1);
77
+ return {
78
+ topPadding,
79
+ progressSlot: {
80
+ row,
81
+ column,
82
+ width,
83
+ },
84
+ deviceSlot: {
85
+ row: deviceRow,
86
+ column,
87
+ width,
88
+ },
89
+ };
90
+ }
91
+ function hasTrackIdentity(player) {
92
+ return Boolean(player.session_id
93
+ || player.provider
94
+ || player.source
95
+ || player.name !== '-'
96
+ || player.artist !== '-'
97
+ || player.album !== '-');
98
+ }
99
+ export function hasActivePlaybackSession(player, _wasSessionActive) {
100
+ if (player.ended === true)
101
+ return false;
102
+ if (player.is_playing === true)
103
+ return true;
104
+ return hasTrackIdentity(player);
105
+ }
106
+ export function resolveRegionAfterPlayerEvent({ currentRegion, wasSessionActive, player, spotifyModeEnabled = false, providerMode = null, }) {
107
+ const sessionActive = hasActivePlaybackSession(player, wasSessionActive);
108
+ if (!sessionActive) {
109
+ return { region: 'chat', sessionActive: false };
110
+ }
111
+ if (providerMode && !wasSessionActive) {
112
+ return { region: 'providerImmersive', sessionActive: true };
113
+ }
114
+ if (spotifyModeEnabled && !wasSessionActive) {
115
+ return { region: 'spotifyImmersive', sessionActive: true };
116
+ }
117
+ if (!wasSessionActive) {
118
+ return { region: 'miniPlayer', sessionActive: true };
119
+ }
120
+ return { region: currentRegion, sessionActive: true };
121
+ }
122
+ export function toggleShellRegion(currentRegion, sessionActive, spotifyModeEnabled = false, providerModeEnabled = false) {
123
+ if (!sessionActive)
124
+ return 'chat';
125
+ if (currentRegion === 'providerImmersive')
126
+ return 'chat';
127
+ if (currentRegion === 'spotifyImmersive')
128
+ return 'chat';
129
+ if (providerModeEnabled && currentRegion === 'chat')
130
+ return 'providerImmersive';
131
+ if (spotifyModeEnabled && currentRegion === 'chat')
132
+ return 'spotifyImmersive';
133
+ return currentRegion === 'chat' ? 'miniPlayer' : 'chat';
134
+ }
package/dist/list.js ADDED
@@ -0,0 +1,3 @@
1
+ export function trimList(items, limit) {
2
+ return items.slice(Math.max(0, items.length - limit));
3
+ }
@@ -0,0 +1,7 @@
1
+ export function resolveLoginProviderSelectionIndex(providers, currentProvider) {
2
+ const providerIndex = providers.findIndex((choice) => choice.value === currentProvider);
3
+ if (providerIndex >= 0)
4
+ return providerIndex;
5
+ const activeIndex = providers.findIndex((choice) => choice.connection_status === 'active');
6
+ return activeIndex >= 0 ? activeIndex : 0;
7
+ }
@@ -0,0 +1,122 @@
1
+ import React from 'react';
2
+ import { buildProgressBar, formatDuration } from './format.js';
3
+ import { isPlaybackProgressFrozen, isPlaybackStarting, playbackProgressAt, PLAYBACK_PROGRESS_INTERVAL_MS } from './hooks.js';
4
+ const PLAYING_STATUS_ICON = '▶';
5
+ const PAUSED_STATUS_ICON = '▌▌';
6
+ const SAVED_PLAYLIST_ICON = '✔';
7
+ const ADD_TO_PLAYLIST_ICON = '+';
8
+ const ANSI_GREEN = '\u001B[32m';
9
+ const ANSI_RESET_FG = '\u001B[39m';
10
+ export function resolvePlaybackProgressUpdateMode(enabled, player) {
11
+ if (!enabled || player.ended === true)
12
+ return 'off';
13
+ if (isPlaybackProgressFrozen(player))
14
+ return 'once';
15
+ return player.is_playing === true ? 'interval' : 'once';
16
+ }
17
+ export function shouldRefreshMiniSnapshot(reason) {
18
+ return reason === 'region' || reason === 'resize';
19
+ }
20
+ function playbackProgressLabel(player, progressMs) {
21
+ if (isPlaybackStarting(player))
22
+ return 'starting';
23
+ if (player.progress_sync_lost === true)
24
+ return 'syncing';
25
+ if (player.paused_for_cache === true)
26
+ return 'buffering';
27
+ if (player.diagnostic_notice)
28
+ return 'diagnostic';
29
+ return formatDuration(progressMs);
30
+ }
31
+ function playbackProgressLayout(player, now, width) {
32
+ const progress = playbackProgressLabel(player, playbackProgressAt(player, now));
33
+ const duration = formatDuration(player.duration_ms);
34
+ const barWidth = Math.max(6, width - progress.length - duration.length - 2);
35
+ return {
36
+ progress,
37
+ duration,
38
+ barWidth,
39
+ barStart: progress.length + 1,
40
+ };
41
+ }
42
+ export function buildPlaybackProgressLine(player, now, width) {
43
+ const progressMs = playbackProgressAt(player, now);
44
+ const layout = playbackProgressLayout(player, now, width);
45
+ const progressBar = buildProgressBar(progressMs, player.duration_ms, layout.barWidth);
46
+ return `${layout.progress} ${progressBar} ${layout.duration}`;
47
+ }
48
+ export function playbackStatusIconSegments(player, width, now) {
49
+ const playbackIcon = player.is_playing === true ? PLAYING_STATUS_ICON : PAUSED_STATUS_ICON;
50
+ const progressLayout = playbackProgressLayout(player, now, width);
51
+ const centeredStart = progressLayout.barStart + Math.floor((progressLayout.barWidth - playbackIcon.length) / 2);
52
+ const playbackStart = Math.max(0, Math.min(centeredStart, Math.max(0, width - playbackIcon.length)));
53
+ const visiblePlaybackIcon = sliceVisible(playbackIcon, Math.max(0, width - playbackStart));
54
+ const segments = [];
55
+ if (playbackStart > 0)
56
+ segments.push({ text: ' '.repeat(playbackStart) });
57
+ if (visiblePlaybackIcon)
58
+ segments.push({ text: visiblePlaybackIcon });
59
+ const playlistIcon = player.is_in_playlist === true ? SAVED_PLAYLIST_ICON : ADD_TO_PLAYLIST_ICON;
60
+ const remainingWidth = width - playbackStart - visiblePlaybackIcon.length;
61
+ if (remainingWidth >= playlistIcon.length + 1) {
62
+ segments.push({ text: ' ' });
63
+ segments.push({
64
+ text: playlistIcon,
65
+ color: player.is_in_playlist === true ? 'green' : undefined,
66
+ });
67
+ }
68
+ return segments;
69
+ }
70
+ function visibleText(segments) {
71
+ return segments.map((segment) => segment.text).join('');
72
+ }
73
+ function sliceVisible(text, width) {
74
+ return Array.from(text).slice(0, Math.max(0, width)).join('');
75
+ }
76
+ export function buildPlaybackStatusIconLine(player, width, now = Date.now()) {
77
+ const segments = playbackStatusIconSegments(player, width, now);
78
+ const text = visibleText(segments);
79
+ return { text, segments };
80
+ }
81
+ function renderTerminalLine(line) {
82
+ if (typeof line === 'string')
83
+ return line;
84
+ return line.segments.map((segment) => {
85
+ if (segment.color === 'green')
86
+ return `${ANSI_GREEN}${segment.text}${ANSI_RESET_FG}`;
87
+ return segment.text;
88
+ }).join('');
89
+ }
90
+ export function writeTerminalLine(stdout, position, line) {
91
+ if (position.width <= 0)
92
+ return;
93
+ const text = typeof line === 'string' ? line : line.text;
94
+ const visible = sliceVisible(text, position.width);
95
+ const rendered = renderTerminalLine(line);
96
+ const padded = `${rendered}${' '.repeat(Math.max(0, position.width - visible.length))}`;
97
+ stdout.write(`\u001B7\u001B[${position.row};${position.column}H${padded}\u001B8`);
98
+ }
99
+ export function usePlaybackProgressWriter({ enabled, player, position, stdout, }) {
100
+ const playerRef = React.useRef(player);
101
+ playerRef.current = player;
102
+ const writeProgress = React.useCallback(() => {
103
+ writeTerminalLine(stdout, position, buildPlaybackProgressLine(playerRef.current, Date.now(), position.width));
104
+ }, [position.column, position.row, position.width, stdout]);
105
+ const updateMode = resolvePlaybackProgressUpdateMode(enabled, player);
106
+ React.useEffect(() => {
107
+ if (updateMode === 'off')
108
+ return;
109
+ writeProgress();
110
+ if (updateMode === 'once')
111
+ return;
112
+ const timer = setInterval(writeProgress, PLAYBACK_PROGRESS_INTERVAL_MS);
113
+ return () => clearInterval(timer);
114
+ }, [player, updateMode, writeProgress]);
115
+ }
116
+ export function usePlaybackStatusIconWriter({ enabled, player, position, stdout, }) {
117
+ React.useEffect(() => {
118
+ if (!enabled)
119
+ return;
120
+ writeTerminalLine(stdout, position, buildPlaybackStatusIconLine(player, position.width, Date.now()));
121
+ }, [enabled, player.is_in_playlist, player.is_playing, player.playback_status, player.progress_ms, position.column, position.row, position.width, stdout]);
122
+ }
@@ -0,0 +1,77 @@
1
+ import React from 'react';
2
+ import { buildProgressBar, formatDuration } from './format.js';
3
+ import { playbackProgressAt, PLAYBACK_PROGRESS_INTERVAL_MS } from './hooks.js';
4
+ export function resolveMiniProgressUpdateMode(enabled, player) {
5
+ if (!enabled || player.ended === true)
6
+ return 'off';
7
+ return player.is_playing === true ? 'interval' : 'once';
8
+ }
9
+ /**
10
+ * Coordinates the build mini progress line operation for the CLI UI runtime.
11
+ *
12
+ * @param player Input value used by the build mini progress line operation.
13
+ * @param now Input value used by the build mini progress line operation.
14
+ * @param width Input value used by the build mini progress line operation.
15
+ * @returns The computed result for the surrounding CLI UI flow.
16
+ */
17
+ export function buildMiniProgressLine(player, now, width) {
18
+ const progress = formatDuration(playbackProgressAt(player, now));
19
+ const duration = formatDuration(player.duration_ms);
20
+ const barWidth = Math.max(6, width - progress.length - duration.length - 2);
21
+ const progressBar = buildProgressBar(playbackProgressAt(player, now), player.duration_ms, barWidth);
22
+ return `${progress} ${progressBar} ${duration}`;
23
+ }
24
+ /**
25
+ * Coordinates the resolve mini progress position operation for the CLI UI runtime.
26
+ *
27
+ * @param size Input value used by the resolve mini progress position operation.
28
+ * @returns The computed result for the surrounding CLI UI flow.
29
+ */
30
+ export function resolveMiniProgressPosition(size) {
31
+ if (!size.columns || !size.rows || size.columns < 18 || size.rows < 6) {
32
+ return null;
33
+ }
34
+ return {
35
+ row: Math.max(1, size.rows - 2),
36
+ column: 3,
37
+ width: Math.max(6, size.columns - 6),
38
+ };
39
+ }
40
+ /**
41
+ * Coordinates the write terminal line operation for the CLI UI runtime.
42
+ *
43
+ * @param stdout Input value used by the write terminal line operation.
44
+ * @param position Input value used by the write terminal line operation.
45
+ * @param text Input value used by the write terminal line operation.
46
+ * @returns The computed result for the surrounding CLI UI flow.
47
+ */
48
+ export function writeTerminalLine(stdout, position, text) {
49
+ const padded = text.slice(0, position.width).padEnd(position.width, ' ');
50
+ stdout.write(`\u001B7\u001B[${position.row};${position.column}H\u001B[2K${padded}\u001B8`);
51
+ }
52
+ /**
53
+ * Coordinates the use mini progress writer operation for the CLI UI runtime.
54
+ *
55
+ * @param enabled,player,terminalSize,stdout, Input value used by the use mini progress writer operation.
56
+ * @returns The computed result for the surrounding CLI UI flow.
57
+ */
58
+ export function useMiniProgressWriter({ enabled, player, terminalSize, stdout, }) {
59
+ const playerRef = React.useRef(player);
60
+ playerRef.current = player;
61
+ const writeProgress = React.useCallback(() => {
62
+ const position = resolveMiniProgressPosition(terminalSize);
63
+ if (!position)
64
+ return;
65
+ writeTerminalLine(stdout, position, buildMiniProgressLine(playerRef.current, Date.now(), position.width));
66
+ }, [stdout, terminalSize.columns, terminalSize.rows]);
67
+ const updateMode = resolveMiniProgressUpdateMode(enabled, player);
68
+ React.useEffect(() => {
69
+ if (updateMode === 'off')
70
+ return;
71
+ writeProgress();
72
+ if (updateMode === 'once')
73
+ return;
74
+ const timer = setInterval(writeProgress, PLAYBACK_PROGRESS_INTERVAL_MS);
75
+ return () => clearInterval(timer);
76
+ }, [player, updateMode, writeProgress]);
77
+ }
@@ -0,0 +1,20 @@
1
+ import stringWidth from 'string-width';
2
+ export const modelPanelLabelWidth = (choices) => (Math.max(0, ...choices.map((choice) => stringWidth(choice.label))) + 1);
3
+ export const formatModelPanelLabel = (choice, width) => {
4
+ const label = choice.label;
5
+ const labelWidth = stringWidth(label);
6
+ if (labelWidth >= width)
7
+ return label;
8
+ return `${label}${" ".repeat(width - labelWidth)}`;
9
+ };
10
+ export const filterModelChoices = (choices, query) => {
11
+ const tokens = query.toLocaleLowerCase().trim().split(/\s+/).filter(Boolean);
12
+ if (tokens.length === 0)
13
+ return choices;
14
+ return choices.filter((choice) => {
15
+ const searchable = [choice.value, choice.label, choice.provider ?? '']
16
+ .join(' ')
17
+ .toLocaleLowerCase();
18
+ return tokens.every((token) => searchable.includes(token));
19
+ });
20
+ };
@@ -0,0 +1,34 @@
1
+ const PROVIDER_BRANDS = {
2
+ openai: 'OpenAI',
3
+ anthropic: 'Anthropic',
4
+ gemini: 'Google Gemini',
5
+ deepseek: 'DeepSeek',
6
+ openrouter: 'OpenRouter',
7
+ zai: 'Z.AI',
8
+ kimi_global: 'Kimi Global',
9
+ kimi_cn: 'Kimi CN',
10
+ minimax_global: 'MiniMax Global',
11
+ minimax_cn: 'MiniMax CN',
12
+ xai: 'xAI',
13
+ custom: 'Custom',
14
+ };
15
+ export const formatTokenCount = (value) => {
16
+ const tokens = Math.max(0, Math.floor(value));
17
+ return tokens > 1_000 ? `${Math.floor(tokens / 1_000)}k` : String(tokens);
18
+ };
19
+ export const formatModelStatus = (input, tokenUsage) => {
20
+ if (!input.ready)
21
+ return null;
22
+ const provider = input.provider.trim();
23
+ const model = (input.model_label || input.model).trim();
24
+ if (!provider || !model)
25
+ return null;
26
+ const normalizedProvider = provider.toLowerCase();
27
+ const providerLabel = normalizedProvider.startsWith('custom__')
28
+ ? 'Custom'
29
+ : PROVIDER_BRANDS[normalizedProvider] ?? provider;
30
+ const modelLabel = `[${providerLabel}] ${model}`;
31
+ if (tokenUsage.inputTokens <= 0 && tokenUsage.outputTokens <= 0)
32
+ return modelLabel;
33
+ return `↑${formatTokenCount(tokenUsage.inputTokens)} ↓${formatTokenCount(tokenUsage.outputTokens)} ${modelLabel}`;
34
+ };
@@ -0,0 +1,173 @@
1
+ import { Transform } from 'node:stream';
2
+ export const MOUSE_TRACKING_ENABLE = "\u001B[?1000h\u001B[?1006h";
3
+ export const MOUSE_TRACKING_DISABLE = "\u001B[?1006l\u001B[?1000l";
4
+ const ESCAPE = 0x1B;
5
+ const CSI = 0x5B;
6
+ const SGR_MARKER = 0x3C;
7
+ const X10_MARKER = 0x4D;
8
+ const SGR_RELEASE = 0x6D;
9
+ const MAX_SGR_SEQUENCE_BYTES = 64;
10
+ const isAsciiDigit = (value) => value >= 0x30 && value <= 0x39;
11
+ const wheelEventForButton = (button) => {
12
+ const baseButton = button & ~(4 | 8 | 16 | 32);
13
+ if (baseButton === 64)
14
+ return "up";
15
+ if (baseButton === 65)
16
+ return "down";
17
+ return null;
18
+ };
19
+ export class MouseSequenceFilter {
20
+ onWheel;
21
+ pending = Buffer.alloc(0);
22
+ constructor(onWheel) {
23
+ this.onWheel = onWheel;
24
+ }
25
+ transform(chunk) {
26
+ const input = this.pending.length > 0
27
+ ? Buffer.concat([this.pending, chunk])
28
+ : chunk;
29
+ const output = [];
30
+ let plainStart = 0;
31
+ let index = 0;
32
+ const flushPlain = (end) => {
33
+ if (end > plainStart)
34
+ output.push(input.subarray(plainStart, end));
35
+ };
36
+ while (index < input.length) {
37
+ if (input[index] !== ESCAPE) {
38
+ index += 1;
39
+ continue;
40
+ }
41
+ const remaining = input.length - index;
42
+ if (remaining < 2) {
43
+ flushPlain(index);
44
+ this.pending = input.subarray(index);
45
+ return Buffer.concat(output);
46
+ }
47
+ if (input[index + 1] !== CSI) {
48
+ index += 1;
49
+ continue;
50
+ }
51
+ if (remaining < 3) {
52
+ flushPlain(index);
53
+ this.pending = input.subarray(index);
54
+ return Buffer.concat(output);
55
+ }
56
+ const protocol = input[index + 2];
57
+ if (protocol === X10_MARKER) {
58
+ if (remaining < 6) {
59
+ flushPlain(index);
60
+ this.pending = input.subarray(index);
61
+ return Buffer.concat(output);
62
+ }
63
+ flushPlain(index);
64
+ const event = wheelEventForButton(input[index + 3] - 32);
65
+ if (event)
66
+ this.onWheel(event);
67
+ index += 6;
68
+ plainStart = index;
69
+ continue;
70
+ }
71
+ if (protocol !== SGR_MARKER) {
72
+ index += 1;
73
+ continue;
74
+ }
75
+ let cursor = index + 3;
76
+ let validPayload = true;
77
+ while (cursor < input.length && input[cursor] !== X10_MARKER && input[cursor] !== SGR_RELEASE) {
78
+ if (!isAsciiDigit(input[cursor]) && input[cursor] !== 0x3B) {
79
+ validPayload = false;
80
+ break;
81
+ }
82
+ cursor += 1;
83
+ }
84
+ if (!validPayload || cursor - index > MAX_SGR_SEQUENCE_BYTES) {
85
+ index += 1;
86
+ continue;
87
+ }
88
+ if (cursor >= input.length) {
89
+ flushPlain(index);
90
+ this.pending = input.subarray(index);
91
+ return Buffer.concat(output);
92
+ }
93
+ const match = input.subarray(index, cursor + 1).toString("ascii")
94
+ .match(/^\u001B\[<(\d+);(\d+);(\d+)[Mm]$/);
95
+ if (!match) {
96
+ index += 1;
97
+ continue;
98
+ }
99
+ flushPlain(index);
100
+ const event = wheelEventForButton(Number(match[1]));
101
+ if (event)
102
+ this.onWheel(event);
103
+ index = cursor + 1;
104
+ plainStart = index;
105
+ }
106
+ flushPlain(input.length);
107
+ this.pending = Buffer.alloc(0);
108
+ return output.length === 0 ? Buffer.alloc(0) : Buffer.concat(output);
109
+ }
110
+ flush() {
111
+ const pending = this.pending;
112
+ this.pending = Buffer.alloc(0);
113
+ return pending;
114
+ }
115
+ }
116
+ class FilteredMouseInput extends Transform {
117
+ source;
118
+ sequenceFilter;
119
+ constructor(source, onWheel) {
120
+ super();
121
+ this.source = source;
122
+ this.sequenceFilter = new MouseSequenceFilter(onWheel);
123
+ source.pipe(this);
124
+ }
125
+ get isTTY() {
126
+ return Boolean(this.source.isTTY);
127
+ }
128
+ setRawMode(mode) {
129
+ this.source.setRawMode?.(mode);
130
+ return this;
131
+ }
132
+ ref() {
133
+ this.source.ref?.();
134
+ return this;
135
+ }
136
+ unref() {
137
+ this.source.unref?.();
138
+ return this;
139
+ }
140
+ _transform(chunk, encoding, callback) {
141
+ const input = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk, encoding);
142
+ const output = this.sequenceFilter.transform(input);
143
+ callback(null, output.length > 0 ? output : undefined);
144
+ }
145
+ _flush(callback) {
146
+ const output = this.sequenceFilter.flush();
147
+ callback(null, output.length > 0 ? output : undefined);
148
+ }
149
+ _destroy(error, callback) {
150
+ this.source.unpipe(this);
151
+ callback(error);
152
+ }
153
+ }
154
+ export const createMouseInputAdapter = (stdin) => {
155
+ const listeners = new Set();
156
+ const filteredInput = new FilteredMouseInput(stdin, (event) => {
157
+ for (const listener of listeners)
158
+ listener(event);
159
+ });
160
+ return {
161
+ stdin: filteredInput,
162
+ wheelSource: {
163
+ subscribe(listener) {
164
+ listeners.add(listener);
165
+ return () => listeners.delete(listener);
166
+ },
167
+ },
168
+ dispose() {
169
+ listeners.clear();
170
+ filteredInput.destroy();
171
+ },
172
+ };
173
+ };
@@ -0,0 +1,86 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import React from 'react';
3
+ import { Box, Text, Transform } from 'ink';
4
+ import stringWidth from 'string-width';
5
+ import { wrapChatMessageContent } from './chat-message.js';
6
+ import { BORDER_BLUE, SPOTIFY_GREEN } from './constants.js';
7
+ import { withTrueColorBackground } from './terminal-frame-writer.js';
8
+ export const PANEL_BACKGROUND = "#48273e";
9
+ export const PANEL_TITLE = "#c8a6ff";
10
+ export const PANEL_PRIMARY = "#fff4f6";
11
+ export const PANEL_SECONDARY = "#808791";
12
+ export const withPanelBackground = (value) => (withTrueColorBackground(value, PANEL_BACKGROUND));
13
+ const boundedPanelWidth = (width) => Math.max(3, Math.floor(width));
14
+ export const panelContentWidth = (width, paddingX = 1) => (Math.max(1, boundedPanelWidth(width) - Math.max(0, Math.floor(paddingX)) * 2));
15
+ export const fitPanelSegments = (segments, width) => {
16
+ const targetWidth = Math.max(0, Math.floor(width));
17
+ const fitted = [];
18
+ let usedWidth = 0;
19
+ for (const segment of segments) {
20
+ let text = "";
21
+ for (const character of Array.from(segment.text)) {
22
+ const characterWidth = stringWidth(character);
23
+ if (usedWidth + characterWidth > targetWidth)
24
+ break;
25
+ text += character;
26
+ usedWidth += characterWidth;
27
+ }
28
+ if (text)
29
+ fitted.push({ ...segment, text });
30
+ if (usedWidth >= targetWidth)
31
+ break;
32
+ }
33
+ return fitted;
34
+ };
35
+ export const resolvePanelChoiceSegments = (item, selected, spotifyTheme) => {
36
+ if (!selected) {
37
+ return item.unselectedBold
38
+ ? item.segments.map((segment) => ({ ...segment, bold: true }))
39
+ : item.segments;
40
+ }
41
+ if (item.segments.some((segment) => segment.preserveColorWhenSelected)) {
42
+ const selectedColor = spotifyTheme ? SPOTIFY_GREEN : BORDER_BLUE;
43
+ return item.segments.map((segment) => ({
44
+ ...segment,
45
+ color: item.selectedColor ?? (segment.preserveColorWhenSelected ? segment.color : selectedColor),
46
+ bold: true,
47
+ }));
48
+ }
49
+ return [{
50
+ text: item.segments.map((segment) => segment.text).join(""),
51
+ color: item.selectedColor ?? (spotifyTheme ? SPOTIFY_GREEN : BORDER_BLUE),
52
+ bold: true,
53
+ }];
54
+ };
55
+ export const PanelEmptyRow = ({ width }) => (_jsx(Text, { children: _jsx(Transform, { transform: withPanelBackground, children: _jsx(Text, { children: " ".repeat(boundedPanelWidth(width)) }) }) }));
56
+ export const PanelRow = ({ width, segments, paddingX = 1, }) => {
57
+ const boundedWidth = boundedPanelWidth(width);
58
+ const boundedPadding = Math.max(0, Math.min(Math.floor(paddingX), Math.floor((boundedWidth - 1) / 2)));
59
+ const contentWidth = panelContentWidth(boundedWidth, boundedPadding);
60
+ const fittedSegments = fitPanelSegments(segments, contentWidth);
61
+ const fittedWidth = fittedSegments.reduce((total, segment) => total + stringWidth(segment.text), 0);
62
+ const rightPadding = Math.max(boundedPadding, boundedWidth - boundedPadding - fittedWidth);
63
+ return (_jsx(Text, { children: _jsx(Transform, { transform: withPanelBackground, children: _jsxs(Text, { children: [" ".repeat(boundedPadding), fittedSegments.map((segment, index) => (_jsx(Text, { color: segment.color ?? PANEL_PRIMARY, bold: segment.bold, italic: segment.italic, children: segment.text }, `${index}-${segment.text}`))), " ".repeat(rightPadding)] }) }) }));
64
+ };
65
+ export const PanelChoiceList = ({ items, selectedIndex, width, paddingX = 1, spotifyTheme = false, visibleLimit, }) => {
66
+ if (items.length === 0)
67
+ return null;
68
+ const boundedIndex = selectedIndex < 0
69
+ ? -1
70
+ : Math.min(selectedIndex, items.length - 1);
71
+ const limit = Math.max(1, Math.min(visibleLimit ?? items.length, items.length));
72
+ const maxStart = Math.max(0, items.length - limit);
73
+ const startIndex = Math.min(Math.max(0, boundedIndex - limit + 1), maxStart);
74
+ const visibleItems = items.slice(startIndex, startIndex + limit);
75
+ return (_jsx(Box, { flexDirection: "column", children: visibleItems.map((item, index) => {
76
+ const selected = startIndex + index === boundedIndex;
77
+ return (_jsxs(React.Fragment, { children: [item.gapBefore ? _jsx(PanelEmptyRow, { width: width }) : null, _jsx(PanelRow, { width: width, paddingX: paddingX, segments: resolvePanelChoiceSegments(item, selected, spotifyTheme) })] }, item.key));
78
+ }) }));
79
+ };
80
+ export const PanelFrame = ({ width, title, titleSegments = null, hint = null, hintColor = PANEL_SECONDARY, titleDetailSegments = null, paddingX = 1, children, }) => {
81
+ const boundedWidth = boundedPanelWidth(width);
82
+ const contentWidth = panelContentWidth(boundedWidth, paddingX);
83
+ const titleRows = wrapChatMessageContent(title, contentWidth);
84
+ const hintRows = hint ? wrapChatMessageContent(hint, contentWidth) : [];
85
+ return (_jsxs(Box, { width: boundedWidth, flexDirection: "column", flexShrink: 0, children: [_jsx(PanelEmptyRow, { width: boundedWidth }), titleSegments ? (_jsx(PanelRow, { width: boundedWidth, paddingX: paddingX, segments: titleSegments })) : titleRows.map((row, index) => (_jsx(PanelRow, { width: boundedWidth, paddingX: paddingX, segments: [{ text: row, color: PANEL_TITLE, bold: true }] }, `panel-title-${index}`))), hintRows.map((row, index) => (_jsx(PanelRow, { width: boundedWidth, paddingX: paddingX, segments: [{ text: row, color: hintColor }] }, `panel-hint-${index}`))), titleDetailSegments ? (_jsx(PanelRow, { width: boundedWidth, paddingX: paddingX, segments: titleDetailSegments })) : null, _jsx(PanelEmptyRow, { width: boundedWidth }), children, _jsx(PanelEmptyRow, { width: boundedWidth })] }));
86
+ };
@@ -0,0 +1,17 @@
1
+ const plan = (close, resetSelection = []) => ({ close, resetSelection });
2
+ export function planPanelLifecycle(trigger) {
3
+ switch (trigger) {
4
+ case 'input':
5
+ case 'info':
6
+ case 'safe_exit':
7
+ return plan(['help', 'track', 'language'], ['help', 'track']);
8
+ case 'extension_event':
9
+ return plan(['track', 'memory', 'help', 'language']);
10
+ case 'help_event':
11
+ return plan(['track', 'language'], ['help']);
12
+ case 'setup_event':
13
+ return plan(['help'], ['help']);
14
+ case 'bye':
15
+ return plan(['help', 'track'], ['help', 'track']);
16
+ }
17
+ }