@alisio/alisio-code 0.1.0-alpha.8 → 0.1.0

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 (56) hide show
  1. package/README.md +95 -16
  2. package/dist/analysis.d.ts +27 -0
  3. package/dist/analysis.js +145 -0
  4. package/dist/banner.js +2 -1
  5. package/dist/builtin.d.ts +3 -1
  6. package/dist/builtin.js +0 -24
  7. package/dist/main.js +140 -7
  8. package/dist/serve.d.ts +15 -0
  9. package/dist/serve.js +99 -0
  10. package/dist/tui/agent-manager.d.ts +105 -0
  11. package/dist/tui/agent-manager.js +269 -0
  12. package/dist/tui/agents.d.ts +5 -0
  13. package/dist/tui/agents.js +5 -0
  14. package/dist/tui/app.d.ts +42 -1
  15. package/dist/tui/app.js +1895 -97
  16. package/dist/tui/artifact-preview.d.ts +52 -0
  17. package/dist/tui/artifact-preview.js +212 -0
  18. package/dist/tui/artifacts.d.ts +54 -0
  19. package/dist/tui/artifacts.js +207 -0
  20. package/dist/tui/btw.d.ts +57 -0
  21. package/dist/tui/btw.js +76 -0
  22. package/dist/tui/components.d.ts +174 -12
  23. package/dist/tui/components.js +962 -66
  24. package/dist/tui/connect-input.d.ts +2 -0
  25. package/dist/tui/connect-input.js +1 -1
  26. package/dist/tui/exit.d.ts +10 -0
  27. package/dist/tui/exit.js +15 -0
  28. package/dist/tui/git-branch.d.ts +48 -0
  29. package/dist/tui/git-branch.js +99 -0
  30. package/dist/tui/goal.d.ts +75 -0
  31. package/dist/tui/goal.js +171 -0
  32. package/dist/tui/modes.d.ts +71 -0
  33. package/dist/tui/modes.js +90 -0
  34. package/dist/tui/open-path.d.ts +38 -0
  35. package/dist/tui/open-path.js +80 -0
  36. package/dist/tui/plan-review.d.ts +48 -0
  37. package/dist/tui/plan-review.js +76 -0
  38. package/dist/tui/queue.d.ts +2 -0
  39. package/dist/tui/queue.js +5 -1
  40. package/dist/tui/scroll.d.ts +8 -0
  41. package/dist/tui/scroll.js +12 -0
  42. package/dist/tui/settings-menu.d.ts +40 -0
  43. package/dist/tui/settings-menu.js +132 -0
  44. package/dist/tui/settings.d.ts +194 -0
  45. package/dist/tui/settings.js +482 -0
  46. package/dist/tui/state.d.ts +343 -7
  47. package/dist/tui/state.js +823 -54
  48. package/dist/tui/tasks.d.ts +123 -0
  49. package/dist/tui/tasks.js +369 -0
  50. package/dist/tui/theme.d.ts +42 -1
  51. package/dist/tui/theme.js +516 -1
  52. package/dist/tui/viewer-state.d.ts +7 -0
  53. package/dist/tui/viewer-state.js +36 -0
  54. package/dist/version.d.ts +7 -5
  55. package/dist/version.js +44 -8
  56. package/package.json +7 -9
@@ -0,0 +1,80 @@
1
+ /**
2
+ * Opening and revealing local files with the desktop's default application, portably and without
3
+ * a shell: arguments are an array (no `cmd /c start`, no `sh -c`), so a file name can never inject
4
+ * a command. Only paths returned by the artifact store are opened, never a path the model wrote.
5
+ */
6
+ import { spawn } from "node:child_process";
7
+ import { dirname } from "node:path";
8
+ /** Commands that open `path` with the default app, in fallback order. */
9
+ export function openCommands(path, platform) {
10
+ if (platform === "darwin")
11
+ return [{ command: "open", args: [path] }];
12
+ if (platform === "win32")
13
+ return [{ command: "explorer.exe", args: [path] }];
14
+ return [
15
+ { command: "xdg-open", args: [path] },
16
+ { command: "gio", args: ["open", path] },
17
+ ];
18
+ }
19
+ /** Commands that show `path` selected in the file manager (its folder on Linux/BSD). */
20
+ export function revealCommands(path, platform) {
21
+ if (platform === "darwin")
22
+ return [{ command: "open", args: ["-R", path] }];
23
+ if (platform === "win32")
24
+ return [{ command: "explorer.exe", args: [`/select,${path}`] }];
25
+ return [
26
+ { command: "xdg-open", args: [dirname(path)] },
27
+ { command: "gio", args: ["open", dirname(path)] },
28
+ ];
29
+ }
30
+ /**
31
+ * Whether a desktop session can show a window: never over SSH; on Linux/BSD only with a display
32
+ * (`DISPLAY` or `WAYLAND_DISPLAY`). macOS and Windows always have one locally.
33
+ */
34
+ export function desktopAvailable(platform, env) {
35
+ if (env.SSH_CONNECTION || env.SSH_TTY)
36
+ return false;
37
+ if (platform === "darwin" || platform === "win32")
38
+ return true;
39
+ return !!(env.DISPLAY || env.WAYLAND_DISPLAY);
40
+ }
41
+ /** Starts one detached launcher; resolves false when it cannot start (e.g. `ENOENT`). */
42
+ function start(launch, deps) {
43
+ return new Promise((resolve) => {
44
+ let child;
45
+ try {
46
+ child = (deps.spawn ?? spawn)(launch.command, launch.args, {
47
+ detached: true,
48
+ stdio: "ignore",
49
+ shell: false,
50
+ windowsHide: true,
51
+ });
52
+ }
53
+ catch {
54
+ resolve(false);
55
+ return;
56
+ }
57
+ child.once("error", () => resolve(false));
58
+ child.once("spawn", () => {
59
+ child.unref();
60
+ resolve(true);
61
+ });
62
+ });
63
+ }
64
+ async function run(launches, deps) {
65
+ const platform = deps.platform ?? process.platform;
66
+ if (!desktopAvailable(platform, deps.env ?? process.env))
67
+ return { ok: false, reason: "no-desktop" };
68
+ for (const launch of launches)
69
+ if (await start(launch, deps))
70
+ return { ok: true };
71
+ return { ok: false, reason: "unavailable" };
72
+ }
73
+ /** Opens a file with the default application. */
74
+ export const openPath = (path, deps = {}) => run(openCommands(path, deps.platform ?? process.platform), deps);
75
+ /** Shows a file in the system file manager. */
76
+ export const revealPath = (path, deps = {}) => run(revealCommands(path, deps.platform ?? process.platform), deps);
77
+ /** The notice shown when nothing could be opened (the caller offers "Copy path"). */
78
+ export const cannotOpenMessage = (path, outcome) => outcome.reason === "no-desktop"
79
+ ? `Can't open files here (no desktop session). Path: ${path}`
80
+ : `No program to open files was found. Path: ${path}`;
@@ -0,0 +1,48 @@
1
+ /**
2
+ * Pure, pi-tui-free reducer of the plan review panel (`exit_plan`): three decisions and, for "Add
3
+ * context", a one-line text field. Same convention as `questions.ts`: state + action ->
4
+ * {state, effect}, fully testable without a terminal. Esc means "Skip for now" while choosing and
5
+ * "back to the choices" while typing; Enter submits; an empty text never submits.
6
+ */
7
+ import { type ConnectInputState } from "./connect-input.ts";
8
+ export interface PlanReviewOption {
9
+ value: string;
10
+ label: string;
11
+ description?: string;
12
+ recommended?: boolean;
13
+ /** Choosing it asks for free text first. */
14
+ textInput?: boolean;
15
+ }
16
+ export interface PlanReviewState {
17
+ options: PlanReviewOption[];
18
+ cursor: number;
19
+ mode: "choose" | "text";
20
+ text: ConnectInputState;
21
+ done: boolean;
22
+ }
23
+ export type PlanReviewAction = {
24
+ type: "up" | "down" | "confirm" | "escape";
25
+ } | {
26
+ type: "left" | "right" | "home" | "end" | "backspace" | "delete";
27
+ } | {
28
+ type: "insert";
29
+ text: string;
30
+ };
31
+ export interface PlanReviewEffect {
32
+ type: "submit";
33
+ /** The option value: `approve`, `skip` or `context`. */
34
+ value: string;
35
+ text?: string;
36
+ }
37
+ export declare const SKIP_VALUE = "skip";
38
+ export declare function initialPlanReviewState(options: PlanReviewOption[]): PlanReviewState;
39
+ export declare function reducePlanReview(state: PlanReviewState, action: PlanReviewAction): {
40
+ state: PlanReviewState;
41
+ effect?: PlanReviewEffect;
42
+ };
43
+ /** The plan as the transcript shows it while the decision is pending. */
44
+ export declare function planTranscriptMarkdown(plan: {
45
+ title: string;
46
+ revision: number;
47
+ markdown: string;
48
+ }): string;
@@ -0,0 +1,76 @@
1
+ /**
2
+ * Pure, pi-tui-free reducer of the plan review panel (`exit_plan`): three decisions and, for "Add
3
+ * context", a one-line text field. Same convention as `questions.ts`: state + action ->
4
+ * {state, effect}, fully testable without a terminal. Esc means "Skip for now" while choosing and
5
+ * "back to the choices" while typing; Enter submits; an empty text never submits.
6
+ */
7
+ import { printableInput, reduceConnectInput } from "./connect-input.js";
8
+ export const SKIP_VALUE = "skip";
9
+ export function initialPlanReviewState(options) {
10
+ const recommended = options.findIndex((option) => option.recommended);
11
+ return {
12
+ options,
13
+ cursor: recommended >= 0 ? recommended : 0,
14
+ mode: "choose",
15
+ text: { value: "", cursor: 0 },
16
+ done: false,
17
+ };
18
+ }
19
+ export function reducePlanReview(state, action) {
20
+ if (state.done || !state.options.length)
21
+ return { state };
22
+ const option = state.options[state.cursor];
23
+ if (state.mode === "text") {
24
+ switch (action.type) {
25
+ case "escape":
26
+ return { state: { ...state, mode: "choose" } };
27
+ case "confirm": {
28
+ const text = state.text.value.trim();
29
+ if (!text)
30
+ return { state };
31
+ return {
32
+ state: { ...state, done: true },
33
+ effect: { type: "submit", value: option.value, text },
34
+ };
35
+ }
36
+ case "insert": {
37
+ const text = printableInput(action.text);
38
+ return text
39
+ ? { state: { ...state, text: reduceConnectInput(state.text, { type: "insert", text }) } }
40
+ : { state };
41
+ }
42
+ case "left":
43
+ case "right":
44
+ case "home":
45
+ case "end":
46
+ case "backspace":
47
+ case "delete":
48
+ return { state: { ...state, text: reduceConnectInput(state.text, { type: action.type }) } };
49
+ default:
50
+ return { state };
51
+ }
52
+ }
53
+ switch (action.type) {
54
+ case "up":
55
+ return {
56
+ state: {
57
+ ...state,
58
+ cursor: (state.cursor - 1 + state.options.length) % state.options.length,
59
+ },
60
+ };
61
+ case "down":
62
+ return { state: { ...state, cursor: (state.cursor + 1) % state.options.length } };
63
+ case "confirm":
64
+ if (option.textInput)
65
+ return { state: { ...state, mode: "text" } };
66
+ return { state: { ...state, done: true }, effect: { type: "submit", value: option.value } };
67
+ case "escape":
68
+ return { state: { ...state, done: true }, effect: { type: "submit", value: SKIP_VALUE } };
69
+ default:
70
+ return { state };
71
+ }
72
+ }
73
+ /** The plan as the transcript shows it while the decision is pending. */
74
+ export function planTranscriptMarkdown(plan) {
75
+ return `**Plan: ${plan.title}**${plan.revision > 1 ? ` (revision ${plan.revision})` : ""}\n\n${plan.markdown}`;
76
+ }
@@ -15,6 +15,8 @@
15
15
  export interface QueueAsker {
16
16
  sessionId?: string;
17
17
  label?: string;
18
+ /** What is being asked (a goal waits differently for a permission than for an answer). */
19
+ kind?: "approval" | "question";
18
20
  }
19
21
  export interface QueueJob<T> extends QueueAsker {
20
22
  signal?: AbortSignal;
package/dist/tui/queue.js CHANGED
@@ -18,7 +18,11 @@ export class InteractiveQueue {
18
18
  submit(job) {
19
19
  return new Promise((resolve, reject) => {
20
20
  let settled = false;
21
- const asker = { sessionId: job.sessionId, label: job.label };
21
+ const asker = {
22
+ sessionId: job.sessionId,
23
+ label: job.label,
24
+ ...(job.kind ? { kind: job.kind } : {}),
25
+ };
22
26
  const entry = {
23
27
  asker,
24
28
  start: () => {
@@ -0,0 +1,8 @@
1
+ /** Pure scrolling helper shared by the panels that show long text in the picker slot. */
2
+ /** The visible slice of `lines` for `scroll` (clamped) and whether more lines exist. */
3
+ export declare function scrollWindow(lines: string[], scroll: number, height: number): {
4
+ lines: string[];
5
+ scroll: number;
6
+ above: boolean;
7
+ below: boolean;
8
+ };
@@ -0,0 +1,12 @@
1
+ /** Pure scrolling helper shared by the panels that show long text in the picker slot. */
2
+ /** The visible slice of `lines` for `scroll` (clamped) and whether more lines exist. */
3
+ export function scrollWindow(lines, scroll, height) {
4
+ const max = Math.max(0, lines.length - height);
5
+ const start = Math.min(Math.max(0, scroll), max);
6
+ return {
7
+ lines: lines.slice(start, start + height),
8
+ scroll: start,
9
+ above: start > 0,
10
+ below: start + height < lines.length,
11
+ };
12
+ }
@@ -0,0 +1,40 @@
1
+ /**
2
+ * OpenCode-style settings menu for the TUI: two-column rows (name + current value), live
3
+ * type-to-search, Enter/Space to change a setting (or navigate), Esc to leave, a `(n/total)`
4
+ * counter and a footer with the highlighted row's description.
5
+ *
6
+ * This component is a thin shell: all decision logic (key decoding, filtering, cycling, counter)
7
+ * lives in the pure `settings.ts` module, which is fully unit-tested without pi-tui.
8
+ */
9
+ import { type Component } from "@earendil-works/pi-tui";
10
+ import { type SettingRow } from "./settings.ts";
11
+ export declare class SettingsMenu implements Component {
12
+ private title;
13
+ /** A setting value changed: persist and apply it. The host re-refreshes on success/failure. */
14
+ private onChange;
15
+ /** A navigation row was activated (Enter/Space): route to its manager. */
16
+ private onNavigate;
17
+ /** Esc pressed: leave the menu (the host returns to the editor). */
18
+ private onCancel;
19
+ /** Optional dim lines under the title (e.g. the current provider/model). */
20
+ private detail?;
21
+ private full;
22
+ private filtered;
23
+ private state;
24
+ constructor(title: string, rows: SettingRow[],
25
+ /** A setting value changed: persist and apply it. The host re-refreshes on success/failure. */
26
+ onChange: (row: SettingRow, value: unknown) => void,
27
+ /** A navigation row was activated (Enter/Space): route to its manager. */
28
+ onNavigate: (row: SettingRow) => void,
29
+ /** Esc pressed: leave the menu (the host returns to the editor). */
30
+ onCancel: () => void,
31
+ /** Optional dim lines under the title (e.g. the current provider/model). */
32
+ detail?: string | undefined);
33
+ /** Rebuild rows from the live config after a write; keeps the filter and re-selects the row. */
34
+ refresh(rows: SettingRow[]): void;
35
+ /** Update the displayed value of one row without rebuilding the whole list. */
36
+ updateValue(id: string, value: unknown): void;
37
+ invalidate(): void;
38
+ handleInput(data: string): void;
39
+ render(width: number): string[];
40
+ }
@@ -0,0 +1,132 @@
1
+ /**
2
+ * OpenCode-style settings menu for the TUI: two-column rows (name + current value), live
3
+ * type-to-search, Enter/Space to change a setting (or navigate), Esc to leave, a `(n/total)`
4
+ * counter and a footer with the highlighted row's description.
5
+ *
6
+ * This component is a thin shell: all decision logic (key decoding, filtering, cycling, counter)
7
+ * lives in the pure `settings.ts` module, which is fully unit-tested without pi-tui.
8
+ */
9
+ import { truncateToWidth } from "@earendil-works/pi-tui";
10
+ import { filterSettingRows, formatSettingValue, initialSettingsMenuState, reduceSettingsInput, settingsCounter, settingsKeyAction, } from "./settings.js";
11
+ import { style } from "./theme.js";
12
+ const MAX_VISIBLE = 10;
13
+ const wrapText = (text, width) => {
14
+ const rows = [];
15
+ let row = "";
16
+ for (const word of text.split(/\s+/)) {
17
+ if (row && [...`${row} ${word}`].length > width) {
18
+ rows.push(row);
19
+ row = word;
20
+ }
21
+ else
22
+ row = row ? `${row} ${word}` : word;
23
+ }
24
+ if (row)
25
+ rows.push(row);
26
+ return rows;
27
+ };
28
+ export class SettingsMenu {
29
+ title;
30
+ onChange;
31
+ onNavigate;
32
+ onCancel;
33
+ detail;
34
+ full;
35
+ filtered;
36
+ state = initialSettingsMenuState();
37
+ constructor(title, rows,
38
+ /** A setting value changed: persist and apply it. The host re-refreshes on success/failure. */
39
+ onChange,
40
+ /** A navigation row was activated (Enter/Space): route to its manager. */
41
+ onNavigate,
42
+ /** Esc pressed: leave the menu (the host returns to the editor). */
43
+ onCancel,
44
+ /** Optional dim lines under the title (e.g. the current provider/model). */
45
+ detail) {
46
+ this.title = title;
47
+ this.onChange = onChange;
48
+ this.onNavigate = onNavigate;
49
+ this.onCancel = onCancel;
50
+ this.detail = detail;
51
+ this.full = rows;
52
+ this.filtered = rows;
53
+ }
54
+ /** Rebuild rows from the live config after a write; keeps the filter and re-selects the row. */
55
+ refresh(rows) {
56
+ const previous = this.filtered[this.state.selected]?.id;
57
+ this.full = rows;
58
+ this.filtered = filterSettingRows(rows, this.state.filter);
59
+ const index = previous ? this.filtered.findIndex((row) => row.id === previous) : -1;
60
+ this.state = { ...this.state, selected: index >= 0 ? index : 0 };
61
+ }
62
+ /** Update the displayed value of one row without rebuilding the whole list. */
63
+ updateValue(id, value) {
64
+ for (const row of this.full)
65
+ if (row.id === id)
66
+ row.current = value;
67
+ }
68
+ invalidate() { }
69
+ handleInput(data) {
70
+ const action = settingsKeyAction(data, this.state);
71
+ const { state, effect } = reduceSettingsInput(this.state, action, this.filtered);
72
+ this.state = state;
73
+ // Re-apply the live filter so typing narrows the list before the next render/input.
74
+ this.filtered = filterSettingRows(this.full, state.filter);
75
+ switch (effect.type) {
76
+ case "change":
77
+ // Optimistically reflect the new value; the host refreshes from the config on success.
78
+ effect.row.current = effect.value;
79
+ this.onChange(effect.row, effect.value);
80
+ break;
81
+ case "navigate":
82
+ this.onNavigate(effect.row);
83
+ break;
84
+ case "cancel":
85
+ this.onCancel();
86
+ break;
87
+ case "none":
88
+ break;
89
+ }
90
+ }
91
+ render(width) {
92
+ const lines = [truncateToWidth(style.bold(style.yellow(this.title)), width)];
93
+ if (this.detail?.trim())
94
+ for (const line of wrapText(this.detail, Math.max(20, width - 2)))
95
+ lines.push(truncateToWidth(style.dim(` ${line}`), width));
96
+ if (this.state.filter)
97
+ lines.push(truncateToWidth(style.dim(` filter: ${this.state.filter}`), width));
98
+ const rows = this.filtered;
99
+ if (!rows.length) {
100
+ lines.push(truncateToWidth(style.yellow(this.full.length ? " No matching settings" : " No settings available"), width));
101
+ lines.push("");
102
+ lines.push(truncateToWidth(style.dim(" Type to search · Esc to cancel"), width));
103
+ return lines;
104
+ }
105
+ // Two-column layout: labels padded to the widest row (capped), values right beside them.
106
+ const maxLabel = Math.min(36, Math.max(...rows.map((row) => [...row.label].length)));
107
+ const start = Math.max(0, Math.min(this.state.selected - Math.floor(MAX_VISIBLE / 2), rows.length - MAX_VISIBLE));
108
+ const visible = rows.slice(start, start + MAX_VISIBLE);
109
+ for (let i = 0; i < visible.length; i++) {
110
+ const row = visible[i];
111
+ if (!row)
112
+ continue;
113
+ const selected = start + i === this.state.selected;
114
+ const label = row.label.padEnd(maxLabel);
115
+ const value = row.kind === "setting" ? formatSettingValue(row, row.current) : "";
116
+ const rendered = selected
117
+ ? `${style.cyan("›")} ${style.bold(style.cyan(label))}${value ? ` ${style.bold(style.cyan(value))}` : ""}`
118
+ : ` ${label}${value ? ` ${value}` : ""}`;
119
+ lines.push(truncateToWidth(rendered, width));
120
+ }
121
+ lines.push(truncateToWidth(style.dim(` ${settingsCounter(rows, this.state.selected)}`), width));
122
+ const selectedRow = rows[this.state.selected];
123
+ if (selectedRow?.description) {
124
+ lines.push("");
125
+ for (const wrapped of wrapText(selectedRow.description, Math.max(20, width - 4)))
126
+ lines.push(truncateToWidth(style.gray(` ${wrapped}`), width));
127
+ lines.push("");
128
+ }
129
+ lines.push(truncateToWidth(style.dim(" Type to search · Enter/Space to change · Esc to cancel"), width));
130
+ return lines;
131
+ }
132
+ }
@@ -0,0 +1,194 @@
1
+ /**
2
+ * Pure settings-menu logic for the TUI: row building, filtering, value cycling, counter and
3
+ * keyboard reduction. No terminal or pi-tui imports here — the `SettingsMenu` component in
4
+ * `settings-menu.ts` is a thin shell over this module.
5
+ *
6
+ * Only settings Alisio actually supports are defined here. The row set is the honest, wired
7
+ * list: every "setting" row maps to a real config key persisted through the atomic
8
+ * `setConfigValue` writer (or the MCP consent path) and is applied to the running process.
9
+ */
10
+ export type SettingValueType = "boolean" | "number" | "percent" | "enum";
11
+ export interface SettingRow {
12
+ /** Config key (or MCP consent id) this row edits; navigation rows carry their action id. */
13
+ id: string;
14
+ label: string;
15
+ description: string;
16
+ /** Group the row belongs to; also matched by the filter ("type" of the row). */
17
+ category: string;
18
+ kind: "setting" | "navigation";
19
+ /** Current raw value for setting rows (read live from the config at build time). */
20
+ current?: unknown;
21
+ /** Ordered candidate values cycled by Enter/Space; wraps around at the ends. */
22
+ values?: unknown[];
23
+ valueType?: SettingValueType;
24
+ /** Displayed but not editable in this run (e.g. under `--read-only`). */
25
+ readOnly?: boolean;
26
+ /** Navigation target id for navigation rows; the host maps it to a concrete action. */
27
+ action?: string;
28
+ }
29
+ export interface SettingsNavigationAction {
30
+ id: string;
31
+ label: string;
32
+ description: string;
33
+ }
34
+ /** Ordered candidates of the `websearch.provider` enum, aligned exactly with the config schema. */
35
+ export declare const WEBSEARCH_PROVIDERS: readonly ["searxng", "duckduckgo-instant", "duckduckgo-html", "tavily", "brave", "serpapi", "native"];
36
+ /**
37
+ * The config surface the menu reads. A structural view of the loaded config: rows are built only
38
+ * from the documented fields below, so unknown keys in a real config are ignored by construction.
39
+ */
40
+ export interface SettingsConfigView {
41
+ compaction: {
42
+ auto: boolean;
43
+ threshold: number;
44
+ keepTurns: number;
45
+ maxOutputTokens: number;
46
+ };
47
+ context: {
48
+ claudeMdFallback: boolean;
49
+ maxBytes: number;
50
+ };
51
+ limits: {
52
+ maxTurns: number;
53
+ /** Only present when the user set it; unset means "the model's declared limit, else 16384". */
54
+ maxOutputTokens?: number | undefined;
55
+ maxContextChars: number;
56
+ timeoutMs: number;
57
+ };
58
+ tui: {
59
+ paddingX: number;
60
+ contentPaddingX: number;
61
+ skillSlashCommands: boolean;
62
+ };
63
+ mcp: {
64
+ allow?: boolean;
65
+ };
66
+ websearch: {
67
+ provider?: (typeof WEBSEARCH_PROVIDERS)[number];
68
+ };
69
+ pluginHooks: {
70
+ timeoutMs: number;
71
+ };
72
+ analysis: {
73
+ enabled: boolean;
74
+ limits: {
75
+ timeoutMs: number;
76
+ };
77
+ retention: {
78
+ jobsDays: number;
79
+ intermediateDays: number;
80
+ artifactsDays: number;
81
+ };
82
+ };
83
+ tasks: {
84
+ enabled: boolean;
85
+ maxPerSession: number;
86
+ retentionDays: number;
87
+ };
88
+ goal: {
89
+ enabled: boolean;
90
+ maxTurns: number;
91
+ maxMinutes: number;
92
+ repeatedReplyLimit: number;
93
+ noToolTurnsLimit: number;
94
+ blockedRepeats: number;
95
+ };
96
+ }
97
+ export interface SettingsMenuInput {
98
+ config: SettingsConfigView;
99
+ /** Whether global MCP consent (`mcp.allow`) is persisted for this user right now. */
100
+ mcpAllowPersisted: boolean;
101
+ /** `--read-only` run: every row is shown but none can be changed. */
102
+ readOnly: boolean;
103
+ }
104
+ /** The config view with the schema defaults; also the canonical fixture for tests. */
105
+ export declare const defaultConfig: SettingsConfigView;
106
+ interface SettingDefinition {
107
+ id: string;
108
+ label: string;
109
+ category: string;
110
+ description: string;
111
+ valueType: SettingValueType;
112
+ values: unknown[];
113
+ read: (config: SettingsConfigView, mcpAllowPersisted: boolean) => unknown;
114
+ }
115
+ /**
116
+ * Every real, wired setting the menu offers. `read` maps the live config to the current value;
117
+ * the keys are exactly the ones `setConfigValue` accepts (`compaction.*`, `context.*`, `limits.*`,
118
+ * `pluginHooks.timeoutMs`, `tui.*` and `websearch.provider`), and `mcp.allow` flows through the
119
+ * application's consent path instead. `limits.timeoutMs` is displayed in seconds (the values are
120
+ * seconds); the app host multiplies by 1000 before persisting the milliseconds.
121
+ */
122
+ export declare const SETTINGS_DEFINITIONS: readonly SettingDefinition[];
123
+ /**
124
+ * Builds the full settings menu: the real, wired setting rows followed by the navigation rows the
125
+ * host wires to the existing managers (`/model`, `/connect`, `/plugins`, `/skills`, `/mcps`, ...).
126
+ * Under `--read-only` every setting row is marked read-only: nothing can be persisted.
127
+ */
128
+ export declare function settingsMenuRows(input: SettingsMenuInput, navigation: SettingsNavigationAction[]): SettingRow[];
129
+ /** Whether a row matches every whitespace-separated filter term, on label/id/category/description. */
130
+ export declare function matchSettingRow(row: SettingRow, terms: string[]): boolean;
131
+ /** Filters rows by name, id, category (type) and description; an empty query returns everything. */
132
+ export declare function filterSettingRows(rows: SettingRow[], filter: string): SettingRow[];
133
+ /** The value shown in the right-hand column of a setting row. */
134
+ export declare function formatSettingValue(row: SettingRow, value: unknown): string;
135
+ /**
136
+ * The next candidate after the current value in the row's ordered `values`, wrapping around.
137
+ * A current value that is not in the list (hand-edited config) moves to the next candidate at or
138
+ * above it for numbers, or wraps to the first candidate otherwise. Never mutates the row.
139
+ */
140
+ export declare function cycleSettingValue(row: SettingRow, current: unknown): unknown | undefined;
141
+ /** OpenCode-style `(n/total)` counter; empty when there are no rows to count. */
142
+ export declare function settingsCounter(rows: SettingRow[], selected: number): string;
143
+ export interface SettingsMenuState {
144
+ /** Live type-to-search filter (matched against name, id, category and description). */
145
+ filter: string;
146
+ /** Index into the FILTERED row list. */
147
+ selected: number;
148
+ }
149
+ export declare const initialSettingsMenuState: () => SettingsMenuState;
150
+ /**
151
+ * Normalized keyboard actions for the settings menu, decoded from raw terminal input with plain
152
+ * string matching so the logic stays testable without pi-tui. The component just forwards
153
+ * `handleInput(data)` here and maps the resulting effect to its callbacks.
154
+ */
155
+ export type SettingsKeyAction = {
156
+ kind: "move";
157
+ direction: "up" | "down" | "start" | "end" | "pageUp" | "pageDown";
158
+ } | {
159
+ kind: "type";
160
+ char: string;
161
+ } | {
162
+ kind: "backspace";
163
+ }
164
+ /** Enter, or Space while the filter is empty (Space with an active filter types a space). */
165
+ | {
166
+ kind: "change";
167
+ } | {
168
+ kind: "cancel";
169
+ } | {
170
+ kind: "ignore";
171
+ };
172
+ export declare function settingsKeyAction(data: string, state: SettingsMenuState): SettingsKeyAction;
173
+ export type SettingsMenuEffect = {
174
+ type: "change";
175
+ row: SettingRow;
176
+ value: unknown;
177
+ } | {
178
+ type: "navigate";
179
+ row: SettingRow;
180
+ } | {
181
+ type: "cancel";
182
+ } | {
183
+ type: "none";
184
+ };
185
+ /**
186
+ * Pure keyboard reducer over the menu state. `rows` is the current FILTERED list (navigation rows
187
+ * and settings rows alike); Enter/Space changes a setting or navigates, Esc cancels, typing
188
+ * filters, Backspace edits the filter, and movement keys move/clamp the selection.
189
+ */
190
+ export declare function reduceSettingsInput(state: SettingsMenuState, action: SettingsKeyAction, rows: SettingRow[]): {
191
+ state: SettingsMenuState;
192
+ effect: SettingsMenuEffect;
193
+ };
194
+ export {};