@alisio/alisio-code 0.1.0-alpha.9 → 0.1.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/README.md +95 -16
- package/dist/analysis.d.ts +27 -0
- package/dist/analysis.js +145 -0
- package/dist/banner.js +2 -1
- package/dist/builtin.d.ts +3 -1
- package/dist/builtin.js +0 -24
- package/dist/main.js +133 -6
- package/dist/serve.d.ts +15 -0
- package/dist/serve.js +99 -0
- package/dist/tui/agent-manager.d.ts +105 -0
- package/dist/tui/agent-manager.js +269 -0
- package/dist/tui/agents.d.ts +5 -0
- package/dist/tui/agents.js +5 -0
- package/dist/tui/app.d.ts +42 -1
- package/dist/tui/app.js +1895 -97
- package/dist/tui/artifact-preview.d.ts +52 -0
- package/dist/tui/artifact-preview.js +212 -0
- package/dist/tui/artifacts.d.ts +54 -0
- package/dist/tui/artifacts.js +207 -0
- package/dist/tui/btw.d.ts +57 -0
- package/dist/tui/btw.js +76 -0
- package/dist/tui/components.d.ts +174 -12
- package/dist/tui/components.js +962 -66
- package/dist/tui/connect-input.d.ts +2 -0
- package/dist/tui/connect-input.js +1 -1
- package/dist/tui/exit.d.ts +10 -0
- package/dist/tui/exit.js +15 -0
- package/dist/tui/git-branch.d.ts +48 -0
- package/dist/tui/git-branch.js +99 -0
- package/dist/tui/goal.d.ts +75 -0
- package/dist/tui/goal.js +171 -0
- package/dist/tui/modes.d.ts +71 -0
- package/dist/tui/modes.js +90 -0
- package/dist/tui/open-path.d.ts +38 -0
- package/dist/tui/open-path.js +80 -0
- package/dist/tui/plan-review.d.ts +48 -0
- package/dist/tui/plan-review.js +76 -0
- package/dist/tui/queue.d.ts +2 -0
- package/dist/tui/queue.js +5 -1
- package/dist/tui/scroll.d.ts +8 -0
- package/dist/tui/scroll.js +12 -0
- package/dist/tui/settings-menu.d.ts +40 -0
- package/dist/tui/settings-menu.js +132 -0
- package/dist/tui/settings.d.ts +194 -0
- package/dist/tui/settings.js +482 -0
- package/dist/tui/state.d.ts +343 -7
- package/dist/tui/state.js +823 -54
- package/dist/tui/tasks.d.ts +123 -0
- package/dist/tui/tasks.js +369 -0
- package/dist/tui/theme.d.ts +42 -1
- package/dist/tui/theme.js +516 -1
- package/dist/tui/viewer-state.d.ts +7 -0
- package/dist/tui/viewer-state.js +36 -0
- package/dist/version.d.ts +7 -5
- package/dist/version.js +44 -8
- package/package.json +7 -9
|
@@ -0,0 +1,482 @@
|
|
|
1
|
+
/** Ordered candidates of the `websearch.provider` enum, aligned exactly with the config schema. */
|
|
2
|
+
export const WEBSEARCH_PROVIDERS = [
|
|
3
|
+
"searxng",
|
|
4
|
+
"duckduckgo-instant",
|
|
5
|
+
"duckduckgo-html",
|
|
6
|
+
"tavily",
|
|
7
|
+
"brave",
|
|
8
|
+
"serpapi",
|
|
9
|
+
"native",
|
|
10
|
+
];
|
|
11
|
+
/** The config view with the schema defaults; also the canonical fixture for tests. */
|
|
12
|
+
export const defaultConfig = {
|
|
13
|
+
compaction: { auto: true, threshold: 0.85, keepTurns: 2, maxOutputTokens: 16_000 },
|
|
14
|
+
context: { claudeMdFallback: false, maxBytes: 32 * 1024 },
|
|
15
|
+
limits: { maxTurns: 100, maxContextChars: 800_000, timeoutMs: 300_000 },
|
|
16
|
+
tui: { paddingX: 1, contentPaddingX: 2, skillSlashCommands: true },
|
|
17
|
+
mcp: { allow: false },
|
|
18
|
+
websearch: { provider: undefined },
|
|
19
|
+
pluginHooks: { timeoutMs: 15_000 },
|
|
20
|
+
analysis: {
|
|
21
|
+
enabled: true,
|
|
22
|
+
limits: { timeoutMs: 120_000 },
|
|
23
|
+
retention: { jobsDays: 30, intermediateDays: 7, artifactsDays: 0 },
|
|
24
|
+
},
|
|
25
|
+
tasks: { enabled: true, maxPerSession: 4, retentionDays: 7 },
|
|
26
|
+
goal: {
|
|
27
|
+
enabled: true,
|
|
28
|
+
maxTurns: 50,
|
|
29
|
+
maxMinutes: 120,
|
|
30
|
+
repeatedReplyLimit: 3,
|
|
31
|
+
noToolTurnsLimit: 3,
|
|
32
|
+
blockedRepeats: 2,
|
|
33
|
+
},
|
|
34
|
+
};
|
|
35
|
+
/** Strictly ascending number sequence with exact step arithmetic (0.85 stays 0.85). */
|
|
36
|
+
function stepValues(start, end, step) {
|
|
37
|
+
const values = [];
|
|
38
|
+
for (let value = start; value <= end + 1e-9; value += step)
|
|
39
|
+
values.push(Number(value.toFixed(3)));
|
|
40
|
+
return values;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Every real, wired setting the menu offers. `read` maps the live config to the current value;
|
|
44
|
+
* the keys are exactly the ones `setConfigValue` accepts (`compaction.*`, `context.*`, `limits.*`,
|
|
45
|
+
* `pluginHooks.timeoutMs`, `tui.*` and `websearch.provider`), and `mcp.allow` flows through the
|
|
46
|
+
* application's consent path instead. `limits.timeoutMs` is displayed in seconds (the values are
|
|
47
|
+
* seconds); the app host multiplies by 1000 before persisting the milliseconds.
|
|
48
|
+
*/
|
|
49
|
+
export const SETTINGS_DEFINITIONS = [
|
|
50
|
+
{
|
|
51
|
+
id: "compaction.auto",
|
|
52
|
+
label: "Auto-compact",
|
|
53
|
+
category: "Compaction",
|
|
54
|
+
valueType: "boolean",
|
|
55
|
+
values: [false, true],
|
|
56
|
+
read: (config) => config.compaction.auto,
|
|
57
|
+
description: "Summarize older history automatically when context usage crosses the threshold. Off keeps history verbatim until you run /compact manually.",
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
id: "compaction.threshold",
|
|
61
|
+
label: "Compaction threshold",
|
|
62
|
+
category: "Compaction",
|
|
63
|
+
valueType: "percent",
|
|
64
|
+
values: stepValues(0.5, 0.95, 0.05),
|
|
65
|
+
read: (config) => config.compaction.threshold,
|
|
66
|
+
description: "Share of the model context window that triggers auto-compaction (or of the char-budget fallback when the window is unknown). Applied from the next run.",
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
id: "compaction.keepTurns",
|
|
70
|
+
label: "Keep latest turns",
|
|
71
|
+
category: "Compaction",
|
|
72
|
+
valueType: "number",
|
|
73
|
+
values: stepValues(0, 20, 1),
|
|
74
|
+
read: (config) => config.compaction.keepTurns,
|
|
75
|
+
description: "Recent user turns kept verbatim after an auto or manual compaction. 0 summarizes everything; the newest turns above the cap are kept intact.",
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
id: "compaction.maxOutputTokens",
|
|
79
|
+
label: "Compaction max output tokens",
|
|
80
|
+
category: "Compaction",
|
|
81
|
+
valueType: "number",
|
|
82
|
+
values: [8_000, 12_000, 16_000, 24_000, 32_000],
|
|
83
|
+
read: (config) => config.compaction.maxOutputTokens,
|
|
84
|
+
description: "Output token budget for the summarizer call. A summary cut by this budget is kept as partial; raise it for very long sessions.",
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
id: "context.claudeMdFallback",
|
|
88
|
+
label: "CLAUDE.md fallback",
|
|
89
|
+
category: "Context",
|
|
90
|
+
valueType: "boolean",
|
|
91
|
+
values: [false, true],
|
|
92
|
+
read: (config) => config.context.claudeMdFallback,
|
|
93
|
+
description: "Use a directory's CLAUDE.md when it has no AGENTS.md (agents.md convention). Loaded as project instructions from the next turn.",
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
id: "context.maxBytes",
|
|
97
|
+
label: "AGENTS.md max bytes",
|
|
98
|
+
category: "Context",
|
|
99
|
+
valueType: "number",
|
|
100
|
+
// 4 KiB steps aligned to the default (32 KiB is on-grid); 1024 stays reachable by hand-edit.
|
|
101
|
+
values: stepValues(4096, 1_048_576, 4096),
|
|
102
|
+
read: (config) => config.context.maxBytes,
|
|
103
|
+
description: "Total AGENTS.md bytes injected. The closest instruction files are kept up to this budget; raise it for very large repositories.",
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
id: "websearch.provider",
|
|
107
|
+
label: "Web search provider",
|
|
108
|
+
category: "Web",
|
|
109
|
+
valueType: "enum",
|
|
110
|
+
values: [...WEBSEARCH_PROVIDERS],
|
|
111
|
+
read: (config) => config.websearch.provider,
|
|
112
|
+
description: "Backend used by websearch tools. Unset falls back to a plugin extension, then a public SearXNG instance; `native` runs search server-side and must be supported by the active provider.",
|
|
113
|
+
},
|
|
114
|
+
{
|
|
115
|
+
id: "mcp.allow",
|
|
116
|
+
label: "Remember MCP consent",
|
|
117
|
+
category: "MCP",
|
|
118
|
+
valueType: "boolean",
|
|
119
|
+
values: [false, true],
|
|
120
|
+
read: (_config, mcpAllowPersisted) => mcpAllowPersisted,
|
|
121
|
+
description: "Persist process/network consent for MCP servers in your user configuration (mcp.allow). On: every start grants permission and auto-connects enabled servers; use /mcps to connect now. Off: revokes the persisted consent and disconnects servers.",
|
|
122
|
+
},
|
|
123
|
+
{
|
|
124
|
+
id: "limits.maxTurns",
|
|
125
|
+
label: "Max turns",
|
|
126
|
+
category: "Limits",
|
|
127
|
+
valueType: "number",
|
|
128
|
+
values: [5, 10, 15, 20, 30, 50, 100],
|
|
129
|
+
read: (config) => config.limits.maxTurns,
|
|
130
|
+
description: "Maximum agent-loop turns per run. Reaching the limit ends the run softly: everything so far is kept and the session stays usable, so you can just say continue. Raise it for long read-heavy audits. Applied from the next run.",
|
|
131
|
+
},
|
|
132
|
+
{
|
|
133
|
+
id: "limits.maxOutputTokens",
|
|
134
|
+
label: "Agent max output tokens",
|
|
135
|
+
category: "Limits",
|
|
136
|
+
valueType: "number",
|
|
137
|
+
values: [1_024, 2_048, 4_096, 8_192, 16_384, 32_768, 65_536],
|
|
138
|
+
// Unset: each request uses the model's declared limit (capped at 65536), else 16384.
|
|
139
|
+
read: (config) => config.limits.maxOutputTokens ?? 16_384,
|
|
140
|
+
description: "Per-call output token budget for agent turns. While you leave it unchanged, each request uses the output limit the model's catalog declares (capped at 65536), or 16384 when it declares none; a value set here always wins. A cut response shows a notice suggesting a higher value. Applied from the next run.",
|
|
141
|
+
},
|
|
142
|
+
{
|
|
143
|
+
id: "limits.maxContextChars",
|
|
144
|
+
label: "Context char budget",
|
|
145
|
+
category: "Limits",
|
|
146
|
+
valueType: "number",
|
|
147
|
+
values: [80_000, 120_000, 160_000, 240_000, 320_000, 800_000],
|
|
148
|
+
read: (config) => config.limits.maxContextChars,
|
|
149
|
+
description: "Hard context limit in characters (instruction files + transcript + tool list) per run; the budget fallback that auto-compaction measures when the model window is unknown. Applied from the next run.",
|
|
150
|
+
},
|
|
151
|
+
{
|
|
152
|
+
id: "limits.timeoutMs",
|
|
153
|
+
label: "Run timeout",
|
|
154
|
+
category: "Limits",
|
|
155
|
+
valueType: "number",
|
|
156
|
+
// Displayed in seconds; the app host multiplies by 1000 before persisting milliseconds.
|
|
157
|
+
values: stepValues(30, 600, 30),
|
|
158
|
+
read: (config) => config.limits.timeoutMs / 1000,
|
|
159
|
+
description: "Per-run timeout. A run over the limit is aborted; raise it for very long autonomous tasks.",
|
|
160
|
+
},
|
|
161
|
+
{
|
|
162
|
+
id: "pluginHooks.timeoutMs",
|
|
163
|
+
label: "Plugin hook timeout",
|
|
164
|
+
category: "Plugins",
|
|
165
|
+
valueType: "number",
|
|
166
|
+
values: stepValues(1000, 120_000, 1000),
|
|
167
|
+
read: (config) => config.pluginHooks.timeoutMs,
|
|
168
|
+
description: "Host-enforced plugin hook timeout. Hooks (compaction, session start/end) are aborted when they exceed it; raise it for plugins that summarize slowly.",
|
|
169
|
+
},
|
|
170
|
+
{
|
|
171
|
+
id: "analysis.enabled",
|
|
172
|
+
label: "Data analysis",
|
|
173
|
+
category: "Data analysis",
|
|
174
|
+
valueType: "boolean",
|
|
175
|
+
values: [false, true],
|
|
176
|
+
read: (config) => config.analysis.enabled,
|
|
177
|
+
description: "Python analysis (python_run), downloadable artifacts and tabular data tools. Off removes them. Applies the next time Alisio starts.",
|
|
178
|
+
},
|
|
179
|
+
{
|
|
180
|
+
id: "analysis.limits.timeoutMs",
|
|
181
|
+
label: "Analysis timeout",
|
|
182
|
+
category: "Data analysis",
|
|
183
|
+
valueType: "number",
|
|
184
|
+
// Displayed in seconds; the app host multiplies by 1000 before persisting milliseconds.
|
|
185
|
+
values: [30, 60, 120, 300, 600, 900],
|
|
186
|
+
read: (config) => config.analysis.limits.timeoutMs / 1000,
|
|
187
|
+
description: "Maximum run time of one python_run call, in seconds. The model can ask for less, never more. Applied from the next call.",
|
|
188
|
+
},
|
|
189
|
+
{
|
|
190
|
+
id: "analysis.retention.jobsDays",
|
|
191
|
+
label: "Keep analysis jobs (days)",
|
|
192
|
+
category: "Data analysis",
|
|
193
|
+
valueType: "number",
|
|
194
|
+
values: [0, 7, 14, 30, 60, 90, 180, 365],
|
|
195
|
+
read: (config) => config.analysis.retention.jobsDays,
|
|
196
|
+
description: "Logs and staging of an analysis job, its script once no artifact is ready, and datasets unused for this long. 0 never deletes them. Swept at most once a day.",
|
|
197
|
+
},
|
|
198
|
+
{
|
|
199
|
+
id: "analysis.retention.intermediateDays",
|
|
200
|
+
label: "Keep intermediates (days)",
|
|
201
|
+
category: "Data analysis",
|
|
202
|
+
valueType: "number",
|
|
203
|
+
values: [0, 1, 3, 7, 14, 30, 60],
|
|
204
|
+
read: (config) => config.analysis.retention.intermediateDays,
|
|
205
|
+
description: "The scratch folder (work/) of an analysis job. 0 never deletes it. Swept at most once a day.",
|
|
206
|
+
},
|
|
207
|
+
{
|
|
208
|
+
id: "analysis.retention.artifactsDays",
|
|
209
|
+
label: "Expire artifacts after (days)",
|
|
210
|
+
category: "Data analysis",
|
|
211
|
+
valueType: "number",
|
|
212
|
+
values: [0, 7, 30, 90, 180, 365, 730],
|
|
213
|
+
read: (config) => config.analysis.retention.artifactsDays,
|
|
214
|
+
description: "Artifacts older than this expire: their files are deleted and the card shows Expired (Rerun can recreate them). 0 keeps them forever.",
|
|
215
|
+
},
|
|
216
|
+
{
|
|
217
|
+
id: "tasks.enabled",
|
|
218
|
+
label: "Background tasks",
|
|
219
|
+
category: "Background tasks",
|
|
220
|
+
valueType: "boolean",
|
|
221
|
+
values: [false, true],
|
|
222
|
+
read: (config) => config.tasks.enabled,
|
|
223
|
+
description: "The bg_run, bg_list, bg_output and bg_stop tools. They are not sandboxed and end when Alisio exits. Off removes them. Applies the next time Alisio starts.",
|
|
224
|
+
},
|
|
225
|
+
{
|
|
226
|
+
id: "tasks.maxPerSession",
|
|
227
|
+
label: "Live tasks per session",
|
|
228
|
+
category: "Background tasks",
|
|
229
|
+
valueType: "number",
|
|
230
|
+
values: [1, 2, 4, 8, 16, 32],
|
|
231
|
+
read: (config) => config.tasks.maxPerSession,
|
|
232
|
+
description: "How many background tasks one session can have running at once. Applied from the next task.",
|
|
233
|
+
},
|
|
234
|
+
{
|
|
235
|
+
id: "tasks.retentionDays",
|
|
236
|
+
label: "Keep finished tasks (days)",
|
|
237
|
+
category: "Background tasks",
|
|
238
|
+
valueType: "number",
|
|
239
|
+
values: [0, 1, 3, 7, 14, 30, 90],
|
|
240
|
+
read: (config) => config.tasks.retentionDays,
|
|
241
|
+
description: "Finished tasks and their output logs are deleted after this many days. 0 keeps them. Swept at most once a day.",
|
|
242
|
+
},
|
|
243
|
+
{
|
|
244
|
+
id: "goal.enabled",
|
|
245
|
+
label: "Session goals",
|
|
246
|
+
category: "Goals",
|
|
247
|
+
valueType: "boolean",
|
|
248
|
+
values: [false, true],
|
|
249
|
+
read: (config) => config.goal.enabled,
|
|
250
|
+
description: "/goal: the agent keeps working on one objective, turn after turn, with hard limits. Off refuses new goals and stops continuing the current one.",
|
|
251
|
+
},
|
|
252
|
+
{
|
|
253
|
+
id: "goal.maxTurns",
|
|
254
|
+
label: "Goal turn limit",
|
|
255
|
+
category: "Goals",
|
|
256
|
+
valueType: "number",
|
|
257
|
+
values: [10, 25, 50, 100, 200, 500],
|
|
258
|
+
read: (config) => config.goal.maxTurns,
|
|
259
|
+
description: "A goal pauses after this many turns (a turn is one run: the kickoff, a continuation or your own prompt). Applies to goals started afterwards.",
|
|
260
|
+
},
|
|
261
|
+
{
|
|
262
|
+
id: "goal.maxMinutes",
|
|
263
|
+
label: "Goal time limit (minutes)",
|
|
264
|
+
category: "Goals",
|
|
265
|
+
valueType: "number",
|
|
266
|
+
values: [15, 30, 60, 120, 240, 480],
|
|
267
|
+
read: (config) => config.goal.maxMinutes,
|
|
268
|
+
description: "A goal pauses after this much time spent inside runs (time spent waiting for you or for background tasks does not count). Applies to goals started afterwards. Without a token budget, the turn and time limits are the only hard stops.",
|
|
269
|
+
},
|
|
270
|
+
{
|
|
271
|
+
id: "goal.repeatedReplyLimit",
|
|
272
|
+
label: "Pause after identical replies",
|
|
273
|
+
category: "Goals",
|
|
274
|
+
valueType: "number",
|
|
275
|
+
values: [2, 3, 4, 5, 8],
|
|
276
|
+
read: (config) => config.goal.repeatedReplyLimit,
|
|
277
|
+
description: "A goal pauses (no_progress) after this many identical final replies in a row. The first repeat is logged and the next continuation carries a nudge.",
|
|
278
|
+
},
|
|
279
|
+
{
|
|
280
|
+
id: "goal.noToolTurnsLimit",
|
|
281
|
+
label: "Pause after turns without tools",
|
|
282
|
+
category: "Goals",
|
|
283
|
+
valueType: "number",
|
|
284
|
+
values: [2, 3, 4, 5, 8],
|
|
285
|
+
read: (config) => config.goal.noToolTurnsLimit,
|
|
286
|
+
description: "A goal pauses (no_progress) after this many turns in a row in which the agent called no tool.",
|
|
287
|
+
},
|
|
288
|
+
{
|
|
289
|
+
id: "goal.blockedRepeats",
|
|
290
|
+
label: "Blocker reports needed",
|
|
291
|
+
category: "Goals",
|
|
292
|
+
valueType: "number",
|
|
293
|
+
values: [1, 2, 3, 4],
|
|
294
|
+
read: (config) => config.goal.blockedRepeats,
|
|
295
|
+
description: "How many consecutive turns the agent must report the same blocker (with evidence) before the goal stops as blocked. A permission denial blocks at once.",
|
|
296
|
+
},
|
|
297
|
+
{
|
|
298
|
+
id: "tui.paddingX",
|
|
299
|
+
label: "Editor padding",
|
|
300
|
+
category: "TUI",
|
|
301
|
+
valueType: "number",
|
|
302
|
+
values: [0, 1, 2, 3, 4],
|
|
303
|
+
read: (config) => config.tui.paddingX,
|
|
304
|
+
description: "Horizontal padding (columns) around the editor input box. Applied immediately to the current editor.",
|
|
305
|
+
},
|
|
306
|
+
{
|
|
307
|
+
id: "tui.contentPaddingX",
|
|
308
|
+
label: "Content inset",
|
|
309
|
+
category: "TUI",
|
|
310
|
+
valueType: "number",
|
|
311
|
+
values: [0, 1, 2, 3, 4, 6, 8, 12],
|
|
312
|
+
read: (config) => config.tui.contentPaddingX,
|
|
313
|
+
description: "Horizontal inset (columns) on each side of the transcript content, keeping agent-written rows clear of the window borders. Clamped on narrow terminals so the content column stays usable. Applied immediately.",
|
|
314
|
+
},
|
|
315
|
+
{
|
|
316
|
+
id: "tui.skillSlashCommands",
|
|
317
|
+
label: "Skill slash commands",
|
|
318
|
+
category: "TUI",
|
|
319
|
+
valueType: "boolean",
|
|
320
|
+
values: [false, true],
|
|
321
|
+
read: (config) => config.tui.skillSlashCommands,
|
|
322
|
+
description: "Offer effective skills as first-class `skill:<id>` editor autocomplete entries. Off hides those entries; the `/skills` manager and its argument completion stay available. Applied immediately to the editor.",
|
|
323
|
+
},
|
|
324
|
+
];
|
|
325
|
+
/**
|
|
326
|
+
* Builds the full settings menu: the real, wired setting rows followed by the navigation rows the
|
|
327
|
+
* host wires to the existing managers (`/model`, `/connect`, `/plugins`, `/skills`, `/mcps`, ...).
|
|
328
|
+
* Under `--read-only` every setting row is marked read-only: nothing can be persisted.
|
|
329
|
+
*/
|
|
330
|
+
export function settingsMenuRows(input, navigation) {
|
|
331
|
+
const rows = [];
|
|
332
|
+
for (const def of SETTINGS_DEFINITIONS) {
|
|
333
|
+
rows.push({
|
|
334
|
+
id: def.id,
|
|
335
|
+
label: def.label,
|
|
336
|
+
category: def.category,
|
|
337
|
+
description: def.description,
|
|
338
|
+
kind: "setting",
|
|
339
|
+
current: def.read(input.config, input.mcpAllowPersisted),
|
|
340
|
+
values: [...def.values],
|
|
341
|
+
valueType: def.valueType,
|
|
342
|
+
...(input.readOnly ? { readOnly: true } : {}),
|
|
343
|
+
});
|
|
344
|
+
}
|
|
345
|
+
rows.push(...navigation.map((entry) => ({
|
|
346
|
+
id: entry.id,
|
|
347
|
+
label: entry.label,
|
|
348
|
+
description: entry.description,
|
|
349
|
+
category: "Go to",
|
|
350
|
+
kind: "navigation",
|
|
351
|
+
action: entry.id,
|
|
352
|
+
})));
|
|
353
|
+
return rows;
|
|
354
|
+
}
|
|
355
|
+
/** Whether a row matches every whitespace-separated filter term, on label/id/category/description. */
|
|
356
|
+
export function matchSettingRow(row, terms) {
|
|
357
|
+
const haystack = [row.label, row.id, row.category, row.description].join(" ").toLowerCase();
|
|
358
|
+
return terms.every((term) => haystack.includes(term));
|
|
359
|
+
}
|
|
360
|
+
/** Filters rows by name, id, category (type) and description; an empty query returns everything. */
|
|
361
|
+
export function filterSettingRows(rows, filter) {
|
|
362
|
+
const terms = filter.toLowerCase().split(/\s+/).filter(Boolean);
|
|
363
|
+
if (!terms.length)
|
|
364
|
+
return rows;
|
|
365
|
+
return rows.filter((row) => matchSettingRow(row, terms));
|
|
366
|
+
}
|
|
367
|
+
/** The value shown in the right-hand column of a setting row. */
|
|
368
|
+
export function formatSettingValue(row, value) {
|
|
369
|
+
if (row.valueType === "percent" && typeof value === "number")
|
|
370
|
+
return `${Math.round(value * 100)}%`;
|
|
371
|
+
if (row.valueType === "boolean")
|
|
372
|
+
return value === true ? "true" : "false";
|
|
373
|
+
return value === undefined || value === null ? "" : String(value);
|
|
374
|
+
}
|
|
375
|
+
/**
|
|
376
|
+
* The next candidate after the current value in the row's ordered `values`, wrapping around.
|
|
377
|
+
* A current value that is not in the list (hand-edited config) moves to the next candidate at or
|
|
378
|
+
* above it for numbers, or wraps to the first candidate otherwise. Never mutates the row.
|
|
379
|
+
*/
|
|
380
|
+
export function cycleSettingValue(row, current) {
|
|
381
|
+
const values = row.values;
|
|
382
|
+
if (!values?.length || row.readOnly)
|
|
383
|
+
return undefined;
|
|
384
|
+
const index = values.findIndex((value) => Object.is(value, current));
|
|
385
|
+
if (index >= 0)
|
|
386
|
+
return values[(index + 1) % values.length];
|
|
387
|
+
if (typeof current === "number" && values.every((value) => typeof value === "number"))
|
|
388
|
+
return values.find((value) => value >= current) ?? values[0];
|
|
389
|
+
return values[0];
|
|
390
|
+
}
|
|
391
|
+
/** OpenCode-style `(n/total)` counter; empty when there are no rows to count. */
|
|
392
|
+
export function settingsCounter(rows, selected) {
|
|
393
|
+
if (!rows.length)
|
|
394
|
+
return "";
|
|
395
|
+
const clamped = Math.min(Math.max(0, selected), rows.length - 1);
|
|
396
|
+
return `(${clamped + 1}/${rows.length})`;
|
|
397
|
+
}
|
|
398
|
+
export const initialSettingsMenuState = () => ({ filter: "", selected: 0 });
|
|
399
|
+
const PAGE_SIZE = 10;
|
|
400
|
+
export function settingsKeyAction(data, state) {
|
|
401
|
+
if (!data)
|
|
402
|
+
return { kind: "ignore" };
|
|
403
|
+
if (data === "\x1b" || data === "\x1b[")
|
|
404
|
+
return { kind: "cancel" };
|
|
405
|
+
if (data === "\r" || data === "\n")
|
|
406
|
+
return { kind: "change" };
|
|
407
|
+
if (data === " " && !state.filter)
|
|
408
|
+
return { kind: "change" };
|
|
409
|
+
if (data === "\x1b[A")
|
|
410
|
+
return { kind: "move", direction: "up" };
|
|
411
|
+
if (data === "\x1b[B")
|
|
412
|
+
return { kind: "move", direction: "down" };
|
|
413
|
+
if (data === "\x1b[H" || data === "\x1b[1~")
|
|
414
|
+
return { kind: "move", direction: "start" };
|
|
415
|
+
if (data === "\x1b[F" || data === "\x1b[4~")
|
|
416
|
+
return { kind: "move", direction: "end" };
|
|
417
|
+
if (data === "\x1b[5~")
|
|
418
|
+
return { kind: "move", direction: "pageUp" };
|
|
419
|
+
if (data === "\x1b[6~")
|
|
420
|
+
return { kind: "move", direction: "pageDown" };
|
|
421
|
+
if (data === "\x7f" || data === "\b" || data === "\x08" || data === "\x1b[3~")
|
|
422
|
+
return { kind: "backspace" };
|
|
423
|
+
if (data.length === 1 && data >= " " && data <= "~")
|
|
424
|
+
return { kind: "type", char: data };
|
|
425
|
+
return { kind: "ignore" };
|
|
426
|
+
}
|
|
427
|
+
/**
|
|
428
|
+
* Pure keyboard reducer over the menu state. `rows` is the current FILTERED list (navigation rows
|
|
429
|
+
* and settings rows alike); Enter/Space changes a setting or navigates, Esc cancels, typing
|
|
430
|
+
* filters, Backspace edits the filter, and movement keys move/clamp the selection.
|
|
431
|
+
*/
|
|
432
|
+
export function reduceSettingsInput(state, action, rows) {
|
|
433
|
+
switch (action.kind) {
|
|
434
|
+
case "cancel":
|
|
435
|
+
return { state, effect: { type: "cancel" } };
|
|
436
|
+
case "change": {
|
|
437
|
+
const row = rows[state.selected];
|
|
438
|
+
if (!row)
|
|
439
|
+
return { state, effect: { type: "none" } };
|
|
440
|
+
if (row.kind === "navigation")
|
|
441
|
+
return { state, effect: { type: "navigate", row } };
|
|
442
|
+
const value = cycleSettingValue(row, row.current);
|
|
443
|
+
if (value === undefined)
|
|
444
|
+
return { state, effect: { type: "none" } };
|
|
445
|
+
return { state, effect: { type: "change", row, value } };
|
|
446
|
+
}
|
|
447
|
+
case "type": {
|
|
448
|
+
const filter = `${state.filter}${action.char}`;
|
|
449
|
+
return { state: { filter, selected: 0 }, effect: { type: "none" } };
|
|
450
|
+
}
|
|
451
|
+
case "backspace": {
|
|
452
|
+
const filter = state.filter.slice(0, -1);
|
|
453
|
+
return {
|
|
454
|
+
state: { filter, selected: 0 },
|
|
455
|
+
effect: { type: "none" },
|
|
456
|
+
};
|
|
457
|
+
}
|
|
458
|
+
case "move": {
|
|
459
|
+
if (!rows.length)
|
|
460
|
+
return { state, effect: { type: "none" } };
|
|
461
|
+
const last = rows.length - 1;
|
|
462
|
+
const selected = action.direction === "up"
|
|
463
|
+
? state.selected === 0
|
|
464
|
+
? last
|
|
465
|
+
: state.selected - 1
|
|
466
|
+
: action.direction === "down"
|
|
467
|
+
? state.selected === last
|
|
468
|
+
? 0
|
|
469
|
+
: state.selected + 1
|
|
470
|
+
: action.direction === "start"
|
|
471
|
+
? 0
|
|
472
|
+
: action.direction === "end"
|
|
473
|
+
? last
|
|
474
|
+
: action.direction === "pageUp"
|
|
475
|
+
? Math.max(0, state.selected - PAGE_SIZE)
|
|
476
|
+
: Math.min(last, state.selected + PAGE_SIZE);
|
|
477
|
+
return { state: { ...state, selected }, effect: { type: "none" } };
|
|
478
|
+
}
|
|
479
|
+
case "ignore":
|
|
480
|
+
return { state, effect: { type: "none" } };
|
|
481
|
+
}
|
|
482
|
+
}
|