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.
- package/LICENSE +21 -0
- package/README.md +15 -0
- package/bin/sonex.js +235 -0
- package/dist/App.js +1360 -0
- package/dist/activity.js +18 -0
- package/dist/chat-document.js +40 -0
- package/dist/chat-message.js +93 -0
- package/dist/chat-theme.js +18 -0
- package/dist/chat-window.js +104 -0
- package/dist/command-panel.js +45 -0
- package/dist/commands.js +70 -0
- package/dist/components.js +662 -0
- package/dist/confirm-choice.js +65 -0
- package/dist/constants.js +109 -0
- package/dist/conversation-flow.js +21 -0
- package/dist/cover-pattern.js +58 -0
- package/dist/cover-visual.js +158 -0
- package/dist/extension-panel.js +136 -0
- package/dist/format.js +99 -0
- package/dist/hooks.js +216 -0
- package/dist/i18n.js +291 -0
- package/dist/index.js +46 -0
- package/dist/info-banner.js +37 -0
- package/dist/input-cursor.js +6 -0
- package/dist/input-routing.js +41 -0
- package/dist/launch-preparing.js +30 -0
- package/dist/layout.js +134 -0
- package/dist/list.js +3 -0
- package/dist/login-navigation.js +7 -0
- package/dist/mini-progress-writer.js +122 -0
- package/dist/mini-progress.js +77 -0
- package/dist/model-selection.js +20 -0
- package/dist/model-status.js +34 -0
- package/dist/mouse-input.js +173 -0
- package/dist/panel-frame.js +86 -0
- package/dist/panel-lifecycle.js +17 -0
- package/dist/playback-keymap.js +59 -0
- package/dist/provider-state.js +67 -0
- package/dist/runtime-state.js +91 -0
- package/dist/shell-state.js +37 -0
- package/dist/sonex-logo.js +9 -0
- package/dist/terminal-clear.js +10 -0
- package/dist/terminal-frame-writer.js +133 -0
- package/dist/terminal-surface.js +80 -0
- package/dist/text-stream.js +17 -0
- package/dist/track-panel.js +95 -0
- package/dist/transcript.js +92 -0
- package/dist/types.js +1 -0
- package/dist/ui-settings.js +30 -0
- package/dist/usage-animation.js +14 -0
- package/package.json +57 -0
- package/vendor/requirements-linux-py312.txt +2659 -0
- package/vendor/sonex-0.1.0a1-py3-none-any.whl +0 -0
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
const normalize = (value) => value
|
|
2
|
+
.trim()
|
|
3
|
+
.toLowerCase()
|
|
4
|
+
.replace(/[^\p{L}\p{N}_-]+/gu, '');
|
|
5
|
+
export function isCancelConfirmChoice(choice) {
|
|
6
|
+
const value = normalize(choice.value);
|
|
7
|
+
const label = normalize(choice.label);
|
|
8
|
+
return value === 'cancel'
|
|
9
|
+
|| value === 'deny'
|
|
10
|
+
|| label === 'cancel'
|
|
11
|
+
|| label === 'no'
|
|
12
|
+
|| label === '取消';
|
|
13
|
+
}
|
|
14
|
+
export function getVisibleConfirmChoices(choices, includeCancel = false) {
|
|
15
|
+
return includeCancel
|
|
16
|
+
? choices
|
|
17
|
+
: choices.filter((choice) => !isCancelConfirmChoice(choice));
|
|
18
|
+
}
|
|
19
|
+
export function getSelectableConfirmChoices(choices, includeCancel = false) {
|
|
20
|
+
return getVisibleConfirmChoices(choices, includeCancel).filter((choice) => !choice.disabled);
|
|
21
|
+
}
|
|
22
|
+
export function resolveConfirmChoiceDisplayIndex(choices, selectableIndex, includeCancel = false) {
|
|
23
|
+
const visibleChoices = getVisibleConfirmChoices(choices, includeCancel);
|
|
24
|
+
const selectableChoices = getSelectableConfirmChoices(choices, includeCancel);
|
|
25
|
+
const selectedChoice = selectableChoices[Math.min(Math.max(selectableIndex, 0), Math.max(0, selectableChoices.length - 1))];
|
|
26
|
+
if (!selectedChoice)
|
|
27
|
+
return -1;
|
|
28
|
+
return Math.max(0, visibleChoices.findIndex((choice) => choice.value === selectedChoice.value));
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Coordinates the resolve confirm decision from input operation for the CLI UI runtime.
|
|
32
|
+
*
|
|
33
|
+
* @param input Input value used by the resolve confirm decision from input operation.
|
|
34
|
+
* @param choices Input value used by the resolve confirm decision from input operation.
|
|
35
|
+
* @returns The computed result for the surrounding CLI UI flow.
|
|
36
|
+
*/
|
|
37
|
+
export function resolveConfirmDecisionFromInput(input, choices) {
|
|
38
|
+
const selectableChoices = getSelectableConfirmChoices(choices);
|
|
39
|
+
const normalized = normalize(input);
|
|
40
|
+
if (!normalized)
|
|
41
|
+
return null;
|
|
42
|
+
const numeric = Number.parseInt(normalized, 10);
|
|
43
|
+
if (Number.isInteger(numeric) && String(numeric) === normalized) {
|
|
44
|
+
return selectableChoices[numeric - 1]?.value ?? null;
|
|
45
|
+
}
|
|
46
|
+
for (const choice of selectableChoices) {
|
|
47
|
+
if (normalize(choice.value) === normalized || normalize(choice.label) === normalized) {
|
|
48
|
+
return choice.value;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Coordinates the resolve confirm input decision operation for the CLI UI runtime.
|
|
55
|
+
*
|
|
56
|
+
* @param input Input value used by the resolve confirm input decision operation.
|
|
57
|
+
* @param choice Input value used by the resolve confirm input decision operation.
|
|
58
|
+
* @returns The computed result for the surrounding CLI UI flow.
|
|
59
|
+
*/
|
|
60
|
+
export function resolveConfirmInputDecision(input, choice) {
|
|
61
|
+
const text = input.trim();
|
|
62
|
+
if (!text || !choice?.input)
|
|
63
|
+
return null;
|
|
64
|
+
return `${choice.value}:${encodeURIComponent(text)}`;
|
|
65
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
export const wsUrl = process.env.SONEX_WS_URL ?? "ws://localhost:9001/ws";
|
|
2
|
+
export const APP_VERSION = "0.1.0-alpha.1";
|
|
3
|
+
export const FALLBACK_MODEL_NAME = "gpt-5.5";
|
|
4
|
+
export const BORDER_BLUE = "#3b82f6";
|
|
5
|
+
export const BORDER_BLUE_SOFT = "#9fd9ff";
|
|
6
|
+
export const TOOL_NAVY = "#182e66";
|
|
7
|
+
export const TOOL_VALUE = "#ffffff";
|
|
8
|
+
export const SPOTIFY_GREEN = "#1db954";
|
|
9
|
+
export const APP_TIP_PLACEHOLDER = "Tip: use /random to play a recent song.";
|
|
10
|
+
export const MAX_ACTIVITY_ITEMS = 80;
|
|
11
|
+
export const DEFAULT_CONFIRM_CHOICES = [
|
|
12
|
+
{ value: "allow_once", label: "Yes" },
|
|
13
|
+
{ value: "deny", label: "No" },
|
|
14
|
+
];
|
|
15
|
+
export const MAX_VISIBLE_SLASH_COMMANDS = 4;
|
|
16
|
+
export const MAX_VISIBLE_MODEL_CHOICES = 4;
|
|
17
|
+
export const SLASH_COMMANDS = [
|
|
18
|
+
{ name: "bye", usage: "/bye", description: "save and exit", needsArgument: false },
|
|
19
|
+
{ name: "extension", usage: "/extension", description: "check and manage music extensions", needsArgument: false },
|
|
20
|
+
{ name: "exit", usage: "/exit", description: "save and exit", needsArgument: false },
|
|
21
|
+
{ name: "help", usage: "/help", description: "show commands", needsArgument: false },
|
|
22
|
+
{ name: "info", usage: "/info", description: "show runtime info", needsArgument: false },
|
|
23
|
+
{ name: "lang", usage: "/lang", description: "choose display language", needsArgument: false, enabled: false },
|
|
24
|
+
{ name: "login", usage: "/login", description: "connect or switch LLM provider", needsArgument: false },
|
|
25
|
+
{ name: "logout", usage: "/logout", description: "sign out and exit", needsArgument: false },
|
|
26
|
+
{ name: "memory", usage: "/memory", description: "configure long-term memory", needsArgument: false },
|
|
27
|
+
{ name: "model", usage: "/model", description: "switch active model", needsArgument: false },
|
|
28
|
+
{ name: "playlist", usage: "/playlist [name]|save [name]", description: "browse or save playlists", needsArgument: false },
|
|
29
|
+
{ name: "queue", usage: "/queue", description: "show playback queue", needsArgument: false },
|
|
30
|
+
{ name: "random", usage: "/random", description: "play a recent song", needsArgument: false },
|
|
31
|
+
{ name: "recommend", usage: "/recommend", description: "recommend songs", needsArgument: false },
|
|
32
|
+
{ name: "resume", usage: "/resume", description: "resume playback", needsArgument: false },
|
|
33
|
+
{ name: "sandbox", usage: "/sandbox", description: "check Agent Bash sandbox", needsArgument: false },
|
|
34
|
+
{ name: "settings", usage: "/settings", description: "configure Sonex settings", needsArgument: false, aliases: ["setting"] },
|
|
35
|
+
{ name: "spotify", usage: "/spotify", description: "toggle Spotify mode", needsArgument: false },
|
|
36
|
+
].sort((a, b) => a.name.localeCompare(b.name));
|
|
37
|
+
export const API_NOT_RUNNING_MESSAGE = "Sonex API is not running";
|
|
38
|
+
export const API_NOT_RUNNING_DETAIL = "Start with `sonex`, or run `sonex api` before `sonex tui`.";
|
|
39
|
+
export const SONEX_MASCOT = [
|
|
40
|
+
[
|
|
41
|
+
{ text: " " },
|
|
42
|
+
{ text: "▄", fg: "#000000" },
|
|
43
|
+
{ text: "▀▀▀▀▀", fg: "#000000", bg: "#f4f1f3" },
|
|
44
|
+
{ text: "▄", fg: "#000000" },
|
|
45
|
+
],
|
|
46
|
+
[
|
|
47
|
+
{ text: " " },
|
|
48
|
+
{ text: "▄", fg: "#000000" },
|
|
49
|
+
{ text: "▀", fg: "#000000", bg: "#f4f1f3" },
|
|
50
|
+
{ text: "▀", fg: "#f4f1f3", bg: "#000000" },
|
|
51
|
+
{ text: "▀▀▀▀▀", fg: "#000000", bg: "#9fd9ff" },
|
|
52
|
+
{ text: "▀", fg: "#f4f1f3", bg: "#000000" },
|
|
53
|
+
{ text: "▄", fg: "#000000" },
|
|
54
|
+
],
|
|
55
|
+
[
|
|
56
|
+
{ text: " " },
|
|
57
|
+
{ text: "█", fg: "#000000" },
|
|
58
|
+
{ text: "▀", fg: "#f4f1f3", bg: "#000000" },
|
|
59
|
+
{ text: "▀", fg: "#000000", bg: "#9fd9ff" },
|
|
60
|
+
{ text: "██████", fg: "#9fd9ff" },
|
|
61
|
+
{ text: "▀", fg: "#4b5161", bg: "#9fd9ff" },
|
|
62
|
+
{ text: "▄", fg: "#4b5161" },
|
|
63
|
+
],
|
|
64
|
+
[
|
|
65
|
+
{ text: " " },
|
|
66
|
+
{ text: "█", fg: "#000000" },
|
|
67
|
+
{ text: "██", fg: "#f4f1f3" },
|
|
68
|
+
{ text: "█", fg: "#000000" },
|
|
69
|
+
{ text: "██", fg: "#9fd9ff" },
|
|
70
|
+
{ text: "█", fg: "#000000" },
|
|
71
|
+
{ text: "██", fg: "#9fd9ff" },
|
|
72
|
+
{ text: "█", fg: "#000000" },
|
|
73
|
+
{ text: "█", fg: "#9fd9ff" },
|
|
74
|
+
{ text: "█", fg: "#4b5161" },
|
|
75
|
+
],
|
|
76
|
+
[
|
|
77
|
+
{ text: " " },
|
|
78
|
+
{ text: "▀", fg: "#000000" },
|
|
79
|
+
{ text: "▀▀", fg: "#f4f1f3", bg: "#000000" },
|
|
80
|
+
{ text: "▀", fg: "#000000", bg: "#9fd9ff" },
|
|
81
|
+
{ text: "███████", fg: "#9fd9ff" },
|
|
82
|
+
{ text: "▀", fg: "#4b5161" },
|
|
83
|
+
],
|
|
84
|
+
[
|
|
85
|
+
{ text: " " },
|
|
86
|
+
{ text: "▄", fg: "#4b5161" },
|
|
87
|
+
{ text: "▀", fg: "#4b5161", bg: "#9fd9ff" },
|
|
88
|
+
{ text: "██████", fg: "#9fd9ff" },
|
|
89
|
+
{ text: "█", fg: "#9fd9ff" },
|
|
90
|
+
{ text: "▀", fg: "#9fd9ff" },
|
|
91
|
+
],
|
|
92
|
+
[
|
|
93
|
+
{ text: " " },
|
|
94
|
+
{ text: "▀", fg: "#4b5161" },
|
|
95
|
+
{ text: "▀", fg: "#9fd9ff" },
|
|
96
|
+
{ text: "▀", fg: "#9fd9ff" },
|
|
97
|
+
{ text: "█████", fg: "#9fd9ff" },
|
|
98
|
+
{ text: "▀", fg: "#9fd9ff" },
|
|
99
|
+
],
|
|
100
|
+
];
|
|
101
|
+
export const SONEX_MASCOT_MICRO = [
|
|
102
|
+
[
|
|
103
|
+
{ text: "█", fg: "#9fd9ff" },
|
|
104
|
+
{ text: "▀", fg: "#9fd9ff", bg: "#000000" },
|
|
105
|
+
{ text: "██", fg: "#9fd9ff" },
|
|
106
|
+
{ text: "▀", fg: "#9fd9ff", bg: "#000000" },
|
|
107
|
+
{ text: "█", fg: "#9fd9ff" },
|
|
108
|
+
],
|
|
109
|
+
];
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export const EMPTY_CONVERSATION_RESERVE_ROWS = 3;
|
|
2
|
+
function normalizeRows(value) {
|
|
3
|
+
return Number.isFinite(value) ? Math.max(0, Math.floor(value)) : 0;
|
|
4
|
+
}
|
|
5
|
+
export function resolveConversationFlow(input) {
|
|
6
|
+
const availableRows = Math.max(1, normalizeRows(input.availableRows));
|
|
7
|
+
const showTailDock = normalizeRows(input.scrollOffset) === 0;
|
|
8
|
+
if (!showTailDock) {
|
|
9
|
+
return { chatRows: availableRows, showTailDock: false, emptyReserveRows: 0 };
|
|
10
|
+
}
|
|
11
|
+
const emptyReserveRows = input.hasRuntimeBanner && !input.hasMessages
|
|
12
|
+
? EMPTY_CONVERSATION_RESERVE_ROWS
|
|
13
|
+
: 0;
|
|
14
|
+
const maximumChatRows = Math.max(1, availableRows - normalizeRows(input.tailRows));
|
|
15
|
+
const desiredChatRows = Math.max(1, normalizeRows(input.contentRows) + emptyReserveRows);
|
|
16
|
+
return {
|
|
17
|
+
chatRows: Math.min(desiredChatRows, maximumChatRows),
|
|
18
|
+
showTailDock: true,
|
|
19
|
+
emptyReserveRows,
|
|
20
|
+
};
|
|
21
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
const DEPRECATED_COVER_PATTERN_SIZES = new Set([32, 36]);
|
|
2
|
+
/**
|
|
3
|
+
* Coordinates the choose cover pattern variant operation for the CLI UI runtime.
|
|
4
|
+
*
|
|
5
|
+
* @param pattern Input value used by the choose cover pattern variant operation.
|
|
6
|
+
* @param space Input value used by the choose cover pattern variant operation.
|
|
7
|
+
* @param options Input value used by the choose cover pattern variant operation.
|
|
8
|
+
* @returns The computed result for the surrounding CLI UI flow.
|
|
9
|
+
*/
|
|
10
|
+
export function chooseCoverPatternVariant(pattern, space, options = {}) {
|
|
11
|
+
if (!pattern || !space.columns || !space.rows)
|
|
12
|
+
return null;
|
|
13
|
+
const sizes = Object.keys(pattern.variants)
|
|
14
|
+
.map((value) => Number(value))
|
|
15
|
+
.filter((value) => Number.isInteger(value) && value > 0 && !DEPRECATED_COVER_PATTERN_SIZES.has(value))
|
|
16
|
+
.sort((left, right) => right - left);
|
|
17
|
+
for (const size of sizes) {
|
|
18
|
+
if (options.maxSize && size > options.maxSize)
|
|
19
|
+
continue;
|
|
20
|
+
const grid = pattern.variants[String(size)];
|
|
21
|
+
if (!grid || grid.length !== size)
|
|
22
|
+
continue;
|
|
23
|
+
if (space.columns >= size && space.rows >= size / 2) {
|
|
24
|
+
return { size, grid };
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
export function resolveCoverPatternDisplay(pattern, space, options = {}) {
|
|
30
|
+
if (!pattern)
|
|
31
|
+
return { status: 'none' };
|
|
32
|
+
if (pattern.unavailable_reason)
|
|
33
|
+
return { status: 'unavailable' };
|
|
34
|
+
if (!space)
|
|
35
|
+
return { status: 'unfit' };
|
|
36
|
+
const variant = chooseCoverPatternVariant(pattern, space, options);
|
|
37
|
+
return variant ? { status: 'renderable', variant } : { status: 'unfit' };
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Coordinates the render cover pattern half blocks operation for the CLI UI runtime.
|
|
41
|
+
*
|
|
42
|
+
* @param grid Input value used by the render cover pattern half blocks operation.
|
|
43
|
+
* @param palette Input value used by the render cover pattern half blocks operation.
|
|
44
|
+
* @returns The computed result for the surrounding CLI UI flow.
|
|
45
|
+
*/
|
|
46
|
+
export function renderCoverPatternHalfBlocks(grid, palette) {
|
|
47
|
+
const rows = [];
|
|
48
|
+
for (let y = 0; y < grid.length; y += 2) {
|
|
49
|
+
const upper = grid[y] ?? [];
|
|
50
|
+
const lower = grid[y + 1] ?? upper;
|
|
51
|
+
rows.push(upper.map((upperIndex, x) => ({
|
|
52
|
+
char: '▀',
|
|
53
|
+
foreground: palette[upperIndex] ?? '#ffffff',
|
|
54
|
+
background: palette[lower[x] ?? upperIndex] ?? '#000000',
|
|
55
|
+
})));
|
|
56
|
+
}
|
|
57
|
+
return rows;
|
|
58
|
+
}
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
const FALLBACK_SEED = 0x5e9ec7;
|
|
2
|
+
const BLOCK_ROWS = 8;
|
|
3
|
+
const BLOCK_COLUMNS = 14;
|
|
4
|
+
/**
|
|
5
|
+
* Coordinates the cover visual from source operation for the CLI UI runtime.
|
|
6
|
+
*
|
|
7
|
+
* @param source Input value used by the cover visual from source operation.
|
|
8
|
+
* @param failed Input value used by the cover visual from source operation.
|
|
9
|
+
* @returns The computed result for the surrounding CLI UI flow.
|
|
10
|
+
*/
|
|
11
|
+
export function coverVisualFromSource(source, failed = false) {
|
|
12
|
+
const normalized = source?.trim() || '';
|
|
13
|
+
const seed = normalized && !failed ? hashSource(normalized) : FALLBACK_SEED;
|
|
14
|
+
const palette = paletteFromSeed(seed);
|
|
15
|
+
return {
|
|
16
|
+
status: normalized && !failed ? 'ready' : 'fallback',
|
|
17
|
+
seed,
|
|
18
|
+
...palette,
|
|
19
|
+
blocks: buildBlockMatrix(seed, palette),
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Coordinates the rhythm frame for playback operation for the CLI UI runtime.
|
|
24
|
+
*
|
|
25
|
+
* @param isPlaying Input value used by the rhythm frame for playback operation.
|
|
26
|
+
* @param progressMs Input value used by the rhythm frame for playback operation.
|
|
27
|
+
* @param seed Input value used by the rhythm frame for playback operation.
|
|
28
|
+
* @returns The computed result for the surrounding CLI UI flow.
|
|
29
|
+
*/
|
|
30
|
+
export function rhythmFrameForPlayback(isPlaying, progressMs, seed) {
|
|
31
|
+
if (!isPlaying) {
|
|
32
|
+
return 0;
|
|
33
|
+
}
|
|
34
|
+
const phase = Math.floor(Math.max(0, progressMs) / 700);
|
|
35
|
+
return (phase + (seed % 4)) % 4;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Coordinates the hash source operation for the CLI UI runtime.
|
|
39
|
+
*
|
|
40
|
+
* @param source Input value used by the hash source operation.
|
|
41
|
+
* @returns The computed result for the surrounding CLI UI flow.
|
|
42
|
+
*/
|
|
43
|
+
function hashSource(source) {
|
|
44
|
+
let hash = 2166136261;
|
|
45
|
+
for (let index = 0; index < source.length; index += 1) {
|
|
46
|
+
hash ^= source.charCodeAt(index);
|
|
47
|
+
hash = Math.imul(hash, 16777619);
|
|
48
|
+
}
|
|
49
|
+
return hash >>> 0;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Coordinates the palette from seed operation for the CLI UI runtime.
|
|
53
|
+
*
|
|
54
|
+
* @param seed Input value used by the palette from seed operation.
|
|
55
|
+
* @returns The computed result for the surrounding CLI UI flow.
|
|
56
|
+
*/
|
|
57
|
+
function paletteFromSeed(seed) {
|
|
58
|
+
const hue = seed % 360;
|
|
59
|
+
const secondaryHue = (hue + 46 + ((seed >>> 8) % 64)) % 360;
|
|
60
|
+
const accentHue = (hue + 172 + ((seed >>> 16) % 38)) % 360;
|
|
61
|
+
return {
|
|
62
|
+
primary: hslToHex(hue, 62, 56),
|
|
63
|
+
secondary: hslToHex(secondaryHue, 56, 42),
|
|
64
|
+
accent: hslToHex(accentHue, 72, 66),
|
|
65
|
+
muted: hslToHex((hue + 24) % 360, 28, 24),
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Coordinates the build block matrix operation for the CLI UI runtime.
|
|
70
|
+
*
|
|
71
|
+
* @param seed Input value used by the build block matrix operation.
|
|
72
|
+
* @param palette Input value used by the build block matrix operation.
|
|
73
|
+
* @returns The computed result for the surrounding CLI UI flow.
|
|
74
|
+
*/
|
|
75
|
+
function buildBlockMatrix(seed, palette) {
|
|
76
|
+
const colors = [palette.primary, palette.secondary, palette.accent, palette.muted];
|
|
77
|
+
return Array.from({ length: BLOCK_ROWS }, (_, row) => (Array.from({ length: BLOCK_COLUMNS }, (_, column) => {
|
|
78
|
+
const wave = Math.sin((row + 1) * 0.9 + (column + seed % 7) * 0.58);
|
|
79
|
+
const grain = seededUnit(seed + row * 97 + column * 53);
|
|
80
|
+
const index = Math.abs(Math.floor((wave + grain * 1.8 + row * 0.28) * colors.length)) % colors.length;
|
|
81
|
+
return colors[index] ?? palette.primary;
|
|
82
|
+
})));
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Coordinates the seeded unit operation for the CLI UI runtime.
|
|
86
|
+
*
|
|
87
|
+
* @param seed Input value used by the seeded unit operation.
|
|
88
|
+
* @returns The computed result for the surrounding CLI UI flow.
|
|
89
|
+
*/
|
|
90
|
+
function seededUnit(seed) {
|
|
91
|
+
let value = seed >>> 0;
|
|
92
|
+
value ^= value << 13;
|
|
93
|
+
value ^= value >>> 17;
|
|
94
|
+
value ^= value << 5;
|
|
95
|
+
return (value >>> 0) / 4294967295;
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Coordinates the hsl to hex operation for the CLI UI runtime.
|
|
99
|
+
*
|
|
100
|
+
* @param hue Input value used by the hsl to hex operation.
|
|
101
|
+
* @param saturation Input value used by the hsl to hex operation.
|
|
102
|
+
* @param lightness Input value used by the hsl to hex operation.
|
|
103
|
+
* @returns The computed result for the surrounding CLI UI flow.
|
|
104
|
+
*/
|
|
105
|
+
function hslToHex(hue, saturation, lightness) {
|
|
106
|
+
const normalizedHue = (((hue % 360) + 360) % 360) / 360;
|
|
107
|
+
const normalizedSaturation = clamp01(saturation / 100);
|
|
108
|
+
const normalizedLightness = clamp01(lightness / 100);
|
|
109
|
+
if (normalizedSaturation === 0) {
|
|
110
|
+
const gray = toHexChannel(normalizedLightness);
|
|
111
|
+
return `#${gray}${gray}${gray}`;
|
|
112
|
+
}
|
|
113
|
+
const q = normalizedLightness < 0.5
|
|
114
|
+
? normalizedLightness * (1 + normalizedSaturation)
|
|
115
|
+
: normalizedLightness + normalizedSaturation - normalizedLightness * normalizedSaturation;
|
|
116
|
+
const p = 2 * normalizedLightness - q;
|
|
117
|
+
return `#${toHexChannel(hueToRgb(p, q, normalizedHue + 1 / 3))}${toHexChannel(hueToRgb(p, q, normalizedHue))}${toHexChannel(hueToRgb(p, q, normalizedHue - 1 / 3))}`;
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Coordinates the hue to rgb operation for the CLI UI runtime.
|
|
121
|
+
*
|
|
122
|
+
* @param p Input value used by the hue to rgb operation.
|
|
123
|
+
* @param q Input value used by the hue to rgb operation.
|
|
124
|
+
* @param t Input value used by the hue to rgb operation.
|
|
125
|
+
* @returns The computed result for the surrounding CLI UI flow.
|
|
126
|
+
*/
|
|
127
|
+
function hueToRgb(p, q, t) {
|
|
128
|
+
let value = t;
|
|
129
|
+
if (value < 0)
|
|
130
|
+
value += 1;
|
|
131
|
+
if (value > 1)
|
|
132
|
+
value -= 1;
|
|
133
|
+
if (value < 1 / 6)
|
|
134
|
+
return p + (q - p) * 6 * value;
|
|
135
|
+
if (value < 1 / 2)
|
|
136
|
+
return q;
|
|
137
|
+
if (value < 2 / 3)
|
|
138
|
+
return p + (q - p) * (2 / 3 - value) * 6;
|
|
139
|
+
return p;
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Coordinates the to hex channel operation for the CLI UI runtime.
|
|
143
|
+
*
|
|
144
|
+
* @param value Input value used by the to hex channel operation.
|
|
145
|
+
* @returns The computed result for the surrounding CLI UI flow.
|
|
146
|
+
*/
|
|
147
|
+
function toHexChannel(value) {
|
|
148
|
+
return Math.round(clamp01(value) * 255).toString(16).padStart(2, '0');
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Coordinates the clamp01 operation for the CLI UI runtime.
|
|
152
|
+
*
|
|
153
|
+
* @param value Input value used by the clamp01 operation.
|
|
154
|
+
* @returns The computed result for the surrounding CLI UI flow.
|
|
155
|
+
*/
|
|
156
|
+
function clamp01(value) {
|
|
157
|
+
return Math.min(1, Math.max(0, value));
|
|
158
|
+
}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import React from "react";
|
|
3
|
+
import { Box, Text } from "ink";
|
|
4
|
+
import stringWidth from "string-width";
|
|
5
|
+
import TextInput from "ink-text-input";
|
|
6
|
+
import { PanelChoiceList, PanelFrame, PanelRow, PANEL_PRIMARY, PANEL_SECONDARY } from "./panel-frame.js";
|
|
7
|
+
const SIGNAL_COLORS = {
|
|
8
|
+
green: "#22c55e",
|
|
9
|
+
gray: "#808791",
|
|
10
|
+
red: "#ef4444",
|
|
11
|
+
yellow: "#facc15",
|
|
12
|
+
hollow: "#808791",
|
|
13
|
+
};
|
|
14
|
+
const STATUS_LABELS = {
|
|
15
|
+
enabled: "Enabled",
|
|
16
|
+
not_configured: "Not configured",
|
|
17
|
+
disabled: "Disabled",
|
|
18
|
+
unavailable: "Unavailable",
|
|
19
|
+
unapplied: "Unapplied",
|
|
20
|
+
unsupported: "Unsupported",
|
|
21
|
+
waiting: "Waiting for response…",
|
|
22
|
+
};
|
|
23
|
+
export const EXTENSION_NAME_WIDTH = 7;
|
|
24
|
+
export const extensionSignal = (extension) => (extension.signal === "hollow" ? "◦" : "•");
|
|
25
|
+
const extensionActionColor = (action) => {
|
|
26
|
+
if (action === "reset" || action === "prepare_reset")
|
|
27
|
+
return "#ef4444";
|
|
28
|
+
if (action === "repair" || action === "restart" || action === "prepare_restart")
|
|
29
|
+
return "#facc15";
|
|
30
|
+
return PANEL_PRIMARY;
|
|
31
|
+
};
|
|
32
|
+
const extensionActionBold = (action) => action === "restart" || action === "prepare_restart" || action === "prepare_reset";
|
|
33
|
+
const statusLabel = (status) => STATUS_LABELS[status];
|
|
34
|
+
const extensionActionLabel = (action) => ({
|
|
35
|
+
setup: "Setup",
|
|
36
|
+
enable: "Enable",
|
|
37
|
+
disable: "Disable",
|
|
38
|
+
repair: "Repair",
|
|
39
|
+
restart: "Restart",
|
|
40
|
+
prepare_restart: "Restart",
|
|
41
|
+
quick_check: "Quick Check",
|
|
42
|
+
prepare_reset: "Reset",
|
|
43
|
+
confirm_reset: "Insist",
|
|
44
|
+
confirm_restart: "Restart",
|
|
45
|
+
}[action] || action);
|
|
46
|
+
const DEPENDENCY_MARKER_WIDTH = Math.max(stringWidth("✔️"), stringWidth("❌"), stringWidth("□"));
|
|
47
|
+
const DEPENDENCY_PROGRESS_WIDTH = 18;
|
|
48
|
+
const dependencyGlyph = (state, spinnerFrame = 0) => (state === "installed" ? "✔️" : state === "failed" ? "❌" : state === "installing" ? ["⠦", "⠴", "⠧", "⠇", "⠏", "⠋", "⠙", "⠹", "⠸", "⠼"][spinnerFrame % 10] : "□");
|
|
49
|
+
const dependencyColor = (state) => (state === "installed" ? "#22c55e" : state === "failed" ? "#ef4444" : state === "installing" ? "#808791" : "#808791");
|
|
50
|
+
const dependencyProgressBar = (progress, spinnerFrame = 0) => {
|
|
51
|
+
if (progress == null) {
|
|
52
|
+
const position = spinnerFrame % DEPENDENCY_PROGRESS_WIDTH;
|
|
53
|
+
return `${"░".repeat(position)}█${"░".repeat(DEPENDENCY_PROGRESS_WIDTH - position - 1)}`;
|
|
54
|
+
}
|
|
55
|
+
const filled = Math.round(DEPENDENCY_PROGRESS_WIDTH * Math.min(1, Math.max(0, progress) / 100));
|
|
56
|
+
return `${"█".repeat(Math.max(1, filled))}${"░".repeat(Math.max(0, DEPENDENCY_PROGRESS_WIDTH - Math.max(1, filled)))}`;
|
|
57
|
+
};
|
|
58
|
+
export const dependencyLine = (dependency, labelWidth, spinnerFrame = 0) => {
|
|
59
|
+
const glyph = dependencyGlyph(dependency.state, spinnerFrame);
|
|
60
|
+
const marker = `${" ".repeat(Math.max(0, DEPENDENCY_MARKER_WIDTH - stringWidth(glyph)))}${glyph}`;
|
|
61
|
+
const paddedLabel = dependency.label + " ".repeat(Math.max(0, labelWidth - stringWidth(dependency.label) + 1));
|
|
62
|
+
const suffix = dependency.state === "installing"
|
|
63
|
+
? dependencyProgressBar(dependency.progress, spinnerFrame)
|
|
64
|
+
: dependency.version || dependency.error || "";
|
|
65
|
+
return [
|
|
66
|
+
{ text: `${marker} ${paddedLabel}`, color: dependencyColor(dependency.state), bold: dependency.state === "failed", preserveColorWhenSelected: true },
|
|
67
|
+
{ text: suffix ? ` ${suffix}` : "", color: dependency.state === "failed" ? "#ef4444" : dependency.state === "installing" ? "#808791" : PANEL_PRIMARY },
|
|
68
|
+
];
|
|
69
|
+
};
|
|
70
|
+
export const ExtensionPanelOverlay = ({ panel, selectedIndex, width, input = "", setInput = () => undefined, onSubmit = () => undefined, inputFocus = false, }) => {
|
|
71
|
+
if (!panel)
|
|
72
|
+
return null;
|
|
73
|
+
if (panel.view === "list") {
|
|
74
|
+
const items = panel.extensions.map((extension) => ({
|
|
75
|
+
key: extension.id,
|
|
76
|
+
segments: [
|
|
77
|
+
{ text: `${extensionSignal(extension)} `, color: SIGNAL_COLORS[extension.signal], preserveColorWhenSelected: true },
|
|
78
|
+
{ text: `${extension.name.padEnd(EXTENSION_NAME_WIDTH, " ")} `, color: PANEL_PRIMARY },
|
|
79
|
+
{ text: extension.description, color: PANEL_SECONDARY },
|
|
80
|
+
],
|
|
81
|
+
}));
|
|
82
|
+
return (_jsx(PanelFrame, { width: width, title: panel.title, hint: panel.hint || "↑/↓ select · Enter open · Esc close", children: _jsx(PanelChoiceList, { items: items, selectedIndex: selectedIndex, width: width }) }));
|
|
83
|
+
}
|
|
84
|
+
if (panel.view === "setup" && panel.setup) {
|
|
85
|
+
return _jsx(ExtensionSetupPanel, { setup: panel.setup, width: width, selectedIndex: selectedIndex, input: input, setInput: setInput, onSubmit: onSubmit, inputFocus: inputFocus });
|
|
86
|
+
}
|
|
87
|
+
const detail = panel.detail;
|
|
88
|
+
const extension = panel.extensions.find((item) => item.id === panel.selectedExtension) || panel.extensions[selectedIndex];
|
|
89
|
+
if (!detail || !extension)
|
|
90
|
+
return null;
|
|
91
|
+
const actions = (detail.actions ?? []).map((action) => {
|
|
92
|
+
if (action === "confirm_reset") {
|
|
93
|
+
return { key: action, segments: [
|
|
94
|
+
{ text: "Insist", color: "#ef4444", bold: true, preserveColorWhenSelected: true },
|
|
95
|
+
{ text: " local credentials will be deleted", color: "#facc15", preserveColorWhenSelected: true },
|
|
96
|
+
] };
|
|
97
|
+
}
|
|
98
|
+
if (action === "confirm_restart") {
|
|
99
|
+
return { key: action, segments: [
|
|
100
|
+
{ text: "Restart", color: "#facc15", bold: true, preserveColorWhenSelected: true },
|
|
101
|
+
{ text: " configuration will be applied", color: PANEL_SECONDARY, preserveColorWhenSelected: true },
|
|
102
|
+
] };
|
|
103
|
+
}
|
|
104
|
+
return {
|
|
105
|
+
key: action,
|
|
106
|
+
selectedColor: action === "prepare_reset" ? "#ef4444" : undefined,
|
|
107
|
+
segments: [{
|
|
108
|
+
text: extensionActionLabel(action),
|
|
109
|
+
color: extensionActionColor(action),
|
|
110
|
+
bold: extensionActionBold(action),
|
|
111
|
+
preserveColorWhenSelected: !["disable", "setup", "prepare_reset"].includes(action),
|
|
112
|
+
}],
|
|
113
|
+
};
|
|
114
|
+
});
|
|
115
|
+
const signalColor = SIGNAL_COLORS[extension.signal];
|
|
116
|
+
return (_jsxs(PanelFrame, { width: width, title: `${extensionSignal(extension)} ${extension.name}`, titleSegments: [
|
|
117
|
+
{ text: extensionSignal(extension), color: signalColor, bold: true },
|
|
118
|
+
{ text: ` ${extension.name}`, color: "#c8a6ff", bold: true },
|
|
119
|
+
], hint: panel.hint || "↑/↓ select · Enter act · Esc back", children: [_jsx(PanelRow, { width: width, segments: [{ text: `Status ${statusLabel(detail.status)}`, color: detail.status === "waiting" ? PANEL_SECONDARY : PANEL_PRIMARY }] }), _jsx(PanelRow, { width: width, segments: [
|
|
120
|
+
{ text: "Tag ", color: PANEL_PRIMARY },
|
|
121
|
+
{ text: extension.tags.join(" · "), color: "#183b8c", italic: true },
|
|
122
|
+
] }), _jsx(PanelChoiceList, { items: actions, selectedIndex: selectedIndex, width: width })] }));
|
|
123
|
+
};
|
|
124
|
+
const ExtensionSetupPanel = ({ setup, width, selectedIndex, input, setInput, onSubmit, inputFocus, }) => {
|
|
125
|
+
const [spinnerFrame, setSpinnerFrame] = React.useState(0);
|
|
126
|
+
React.useEffect(() => {
|
|
127
|
+
if (!setup.dependencies?.some((dependency) => dependency.state === "installing"))
|
|
128
|
+
return;
|
|
129
|
+
const timer = setInterval(() => setSpinnerFrame((frame) => frame + 1), 100);
|
|
130
|
+
return () => clearInterval(timer);
|
|
131
|
+
}, [setup.dependencies]);
|
|
132
|
+
return (_jsxs(PanelFrame, { width: width, title: setup.title, hint: "\u2190/\u2192 page \u00B7 Enter submit \u00B7 Esc back", children: [setup.body ? setup.body.split("\n").map((line, index) => (_jsx(PanelRow, { width: width, segments: [{ text: line, color: setup.dependencies ? PANEL_SECONDARY : PANEL_PRIMARY }] }, `${index}-${line}`))) : null, setup.dependencies ? (_jsx(_Fragment, { children: _jsx(PanelChoiceList, { items: setup.dependencies.map((dependency) => ({
|
|
133
|
+
key: dependency.id,
|
|
134
|
+
segments: dependencyLine(dependency, Math.max(...setup.dependencies.map((item) => stringWidth(item.label))), spinnerFrame),
|
|
135
|
+
})), selectedIndex: selectedIndex, width: width }) })) : null, setup.error ? _jsx(PanelRow, { width: width, segments: [{ text: setup.error, color: "#ff6b6b" }] }) : null, setup.input ? (_jsx(Box, { paddingX: 1, children: _jsx(Text, { color: PANEL_PRIMARY, children: _jsx(TextInput, { value: input, onChange: setInput, onSubmit: onSubmit, focus: inputFocus, showCursor: false, placeholder: setup.input.placeholder, mask: setup.input.mask ? "*" : undefined }) }) })) : null, _jsx(PanelRow, { width: width, segments: [{ text: `${setup.page}/${setup.page_count}`, color: PANEL_SECONDARY }] })] }));
|
|
136
|
+
};
|
package/dist/format.js
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import stringWidth from 'string-width';
|
|
2
|
+
/**
|
|
3
|
+
* Coordinates the format duration operation for the CLI UI runtime.
|
|
4
|
+
*
|
|
5
|
+
* @param ms Input value used by the format duration operation.
|
|
6
|
+
* @returns The computed result for the surrounding CLI UI flow.
|
|
7
|
+
*/
|
|
8
|
+
export function formatDuration(ms) {
|
|
9
|
+
const totalSeconds = Math.max(0, Math.floor(ms / 1000));
|
|
10
|
+
const minutes = Math.floor(totalSeconds / 60);
|
|
11
|
+
const seconds = (totalSeconds % 60).toString().padStart(2, "0");
|
|
12
|
+
return `${minutes}:${seconds}`;
|
|
13
|
+
}
|
|
14
|
+
export function formatMiniTrackSubtitle(artist, album) {
|
|
15
|
+
return [artist, album]
|
|
16
|
+
.map((value) => value.trim())
|
|
17
|
+
.filter((value) => value && value !== '-')
|
|
18
|
+
.join('-');
|
|
19
|
+
}
|
|
20
|
+
const MUSIC_CANDIDATE_ARTIST_WIDTH = 24;
|
|
21
|
+
const MUSIC_CANDIDATE_ALBUM_WIDTH = 32;
|
|
22
|
+
const ELLIPSIS = "...";
|
|
23
|
+
export function fitDisplayWidthWithEllipsis(value, width) {
|
|
24
|
+
const normalized = value.trim() || "-";
|
|
25
|
+
if (stringWidth(normalized) <= width) {
|
|
26
|
+
return normalized + " ".repeat(Math.max(0, width - stringWidth(normalized)));
|
|
27
|
+
}
|
|
28
|
+
const contentWidth = Math.max(0, width - stringWidth(ELLIPSIS));
|
|
29
|
+
let rendered = "";
|
|
30
|
+
let renderedWidth = 0;
|
|
31
|
+
for (const char of Array.from(normalized)) {
|
|
32
|
+
const charWidth = stringWidth(char);
|
|
33
|
+
if (renderedWidth + charWidth > contentWidth)
|
|
34
|
+
break;
|
|
35
|
+
rendered += char;
|
|
36
|
+
renderedWidth += charWidth;
|
|
37
|
+
}
|
|
38
|
+
const result = `${rendered}${ELLIPSIS}`;
|
|
39
|
+
return result + " ".repeat(Math.max(0, width - stringWidth(result)));
|
|
40
|
+
}
|
|
41
|
+
function truncateDisplayWidthWithEllipsis(value, width) {
|
|
42
|
+
const normalized = value.trim() || "-";
|
|
43
|
+
if (width <= 0)
|
|
44
|
+
return "";
|
|
45
|
+
if (stringWidth(normalized) <= width)
|
|
46
|
+
return normalized;
|
|
47
|
+
const contentWidth = Math.max(0, width - stringWidth(ELLIPSIS));
|
|
48
|
+
let rendered = "";
|
|
49
|
+
let renderedWidth = 0;
|
|
50
|
+
for (const char of Array.from(normalized)) {
|
|
51
|
+
const charWidth = stringWidth(char);
|
|
52
|
+
if (renderedWidth + charWidth > contentWidth)
|
|
53
|
+
break;
|
|
54
|
+
rendered += char;
|
|
55
|
+
renderedWidth += charWidth;
|
|
56
|
+
}
|
|
57
|
+
return `${rendered}${ELLIPSIS}`;
|
|
58
|
+
}
|
|
59
|
+
export function formatMusicCandidateDisplayLabel(display, rowWidth, trailingText) {
|
|
60
|
+
const prefix = [
|
|
61
|
+
fitDisplayWidthWithEllipsis(display.artist, MUSIC_CANDIDATE_ARTIST_WIDTH),
|
|
62
|
+
fitDisplayWidthWithEllipsis(display.album, MUSIC_CANDIDATE_ALBUM_WIDTH),
|
|
63
|
+
].join(" ");
|
|
64
|
+
const title = display.title.trim() || "-";
|
|
65
|
+
if (typeof rowWidth !== "number") {
|
|
66
|
+
return `${prefix} ${title}`;
|
|
67
|
+
}
|
|
68
|
+
const providerText = trailingText?.trim() ?? "";
|
|
69
|
+
if (providerText) {
|
|
70
|
+
const titlePrefix = `${prefix} `;
|
|
71
|
+
const providerMaxWidth = Math.max(0, rowWidth - stringWidth(titlePrefix) - 1);
|
|
72
|
+
const provider = truncateDisplayWidthWithEllipsis(providerText, providerMaxWidth);
|
|
73
|
+
const titleWidth = Math.max(0, rowWidth - stringWidth(titlePrefix) - stringWidth(provider) - 1);
|
|
74
|
+
const renderedTitle = truncateDisplayWidthWithEllipsis(title, titleWidth);
|
|
75
|
+
const gapWidth = Math.max(1, rowWidth - stringWidth(titlePrefix) - stringWidth(renderedTitle) - stringWidth(provider));
|
|
76
|
+
return `${titlePrefix}${renderedTitle}${" ".repeat(gapWidth)}${provider}`;
|
|
77
|
+
}
|
|
78
|
+
const titleWidth = Math.max(0, rowWidth - stringWidth(prefix) - 1);
|
|
79
|
+
return `${prefix} ${truncateDisplayWidthWithEllipsis(title, titleWidth)}`;
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Coordinates the build progress bar operation for the CLI UI runtime.
|
|
83
|
+
*
|
|
84
|
+
* @param progressMs Input value used by the build progress bar operation.
|
|
85
|
+
* @param durationMs Input value used by the build progress bar operation.
|
|
86
|
+
* @param width Input value used by the build progress bar operation.
|
|
87
|
+
* @returns The computed result for the surrounding CLI UI flow.
|
|
88
|
+
*/
|
|
89
|
+
export function buildProgressBar(progressMs, durationMs, width = 18) {
|
|
90
|
+
if (durationMs <= 0)
|
|
91
|
+
return "─".repeat(width);
|
|
92
|
+
const ratio = Math.min(1, Math.max(0, progressMs / durationMs));
|
|
93
|
+
const exact = ratio * width;
|
|
94
|
+
const filled = Math.floor(exact);
|
|
95
|
+
if (filled >= width)
|
|
96
|
+
return "━".repeat(width);
|
|
97
|
+
const partial = exact - filled >= 0.5 ? "╸" : "";
|
|
98
|
+
return "━".repeat(filled) + partial + "─".repeat(width - filled - partial.length);
|
|
99
|
+
}
|