pi-blackhole 0.4.2 → 0.4.3
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 +15 -0
- package/dist/index.js +11660 -0
- package/dist/index.js.map +1 -0
- package/example-config.json +1 -1
- package/index.ts +37 -63
- package/package.json +21 -9
- package/src/commands/cleanup.ts +279 -240
- package/src/commands/memory.ts +236 -184
- package/src/commands/pi-vcc.ts +202 -152
- package/src/commands/vcc-recall.ts +126 -95
- package/src/core/brief.ts +167 -33
- package/src/core/build-sections.ts +8 -2
- package/src/core/config-env.ts +117 -0
- package/src/core/content.ts +31 -7
- package/src/core/drill-down.ts +41 -11
- package/src/core/filter-noise.ts +9 -3
- package/src/core/format-recall.ts +15 -6
- package/src/core/format.ts +14 -4
- package/src/core/lineage.ts +9 -3
- package/src/core/load-messages.ts +24 -5
- package/src/core/normalize.ts +38 -14
- package/src/core/recall-scope.ts +11 -3
- package/src/core/render-entries.ts +22 -6
- package/src/core/sanitize.ts +5 -1
- package/src/core/search-entries.ts +111 -19
- package/src/core/settings.ts +1 -3
- package/src/core/summarize.ts +42 -21
- package/src/core/unified-config.ts +549 -411
- package/src/extract/commits.ts +4 -2
- package/src/extract/files.ts +10 -5
- package/src/extract/goals.ts +7 -2
- package/src/hooks/before-compact.ts +210 -88
- package/src/om/agents/dropper/agent.ts +380 -265
- package/src/om/agents/dropper/coverage.ts +102 -82
- package/src/om/agents/observer/agent.ts +242 -206
- package/src/om/agents/reflector/agent.ts +212 -153
- package/src/om/cleanup.ts +239 -218
- package/src/om/clipboard.ts +59 -51
- package/src/om/compaction-trigger.ts +448 -333
- package/src/om/config.ts +13 -6
- package/src/om/configure-overlay.ts +518 -355
- package/src/om/consolidation.ts +1460 -953
- package/src/om/cooldown.ts +75 -65
- package/src/om/debug-log.ts +86 -68
- package/src/om/ids.ts +1 -1
- package/src/om/ledger/fold.ts +89 -78
- package/src/om/ledger/progress.ts +181 -153
- package/src/om/ledger/projection.ts +248 -185
- package/src/om/ledger/recall.ts +247 -196
- package/src/om/ledger/render-summary.ts +79 -50
- package/src/om/ledger/types.ts +146 -117
- package/src/om/model-budget.ts +23 -13
- package/src/om/pending.ts +243 -179
- package/src/om/provider-stream.ts +52 -7
- package/src/om/retryable-error.ts +12 -16
- package/src/om/reverse-recall.ts +97 -91
- package/src/om/runtime.ts +474 -375
- package/src/om/serialize.ts +190 -166
- package/src/om/status-overlay.ts +246 -195
- package/src/om/tokens.ts +28 -21
- package/src/pi-base/blackhole-settings.ts +437 -0
- package/src/pi-base/config-manager.ts +440 -0
- package/src/pi-base/config.ts +469 -0
- package/src/pi-base/env.ts +43 -0
- package/src/pi-base/paths.ts +47 -0
- package/src/pi-base/settings/body.ts +1648 -0
- package/src/pi-base/settings/fields/action.ts +43 -0
- package/src/pi-base/settings/fields/boolean.ts +47 -0
- package/src/pi-base/settings/fields/custom.ts +72 -0
- package/src/pi-base/settings/fields/enum.ts +310 -0
- package/src/pi-base/settings/fields/index.ts +46 -0
- package/src/pi-base/settings/fields/model.ts +452 -0
- package/src/pi-base/settings/fields/string.ts +527 -0
- package/src/pi-base/settings/fields/text.ts +115 -0
- package/src/pi-base/settings/frame.ts +197 -0
- package/src/pi-base/settings/index.ts +77 -0
- package/src/pi-base/settings/inline-edit.ts +313 -0
- package/src/pi-base/settings/modal.ts +152 -0
- package/src/pi-base/settings/types.ts +500 -0
- package/src/pi-base/settings/validate-field.ts +113 -0
- package/src/pi-base/shell.ts +117 -0
- package/src/pi-base/types.ts +6 -0
- package/src/pi-base/ui.ts +32 -0
- package/src/tools/recall.ts +347 -225
- package/src/types.ts +20 -3
- package/tsup.config.ts +23 -0
- package/vitest.config.ts +15 -15
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `createSettingsModal` and `openSettingsModal` — the public modal
|
|
3
|
+
* entry points. Both wrap `createSettingsModalBody` and shape it for
|
|
4
|
+
* `ctx.ui.custom`.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { join } from "node:path";
|
|
8
|
+
import type { ExtensionContext, Theme } from "@earendil-works/pi-coding-agent";
|
|
9
|
+
import type {
|
|
10
|
+
Component,
|
|
11
|
+
KeybindingsManager,
|
|
12
|
+
OverlayOptions,
|
|
13
|
+
TUI,
|
|
14
|
+
} from "@earendil-works/pi-tui";
|
|
15
|
+
import { createSettingsModalBody } from "./body";
|
|
16
|
+
import {
|
|
17
|
+
getExtensionsDir,
|
|
18
|
+
readConfig,
|
|
19
|
+
writeConfig,
|
|
20
|
+
deleteConfig,
|
|
21
|
+
} from "../config.ts";
|
|
22
|
+
import type {
|
|
23
|
+
Field,
|
|
24
|
+
SettingsModalFactory,
|
|
25
|
+
SettingsModalOptions,
|
|
26
|
+
} from "./types";
|
|
27
|
+
|
|
28
|
+
const DEFAULT_OVERLAY: OverlayOptions = {
|
|
29
|
+
anchor: "center",
|
|
30
|
+
width: "92%",
|
|
31
|
+
maxHeight: "85%",
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
// Singleton guard: track one open modal per ExtensionContext.
|
|
35
|
+
// WeakMap so entries are GC'd when the context is no longer referenced.
|
|
36
|
+
const openModals = new WeakMap<ExtensionContext, (result: void) => void>();
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Build a `ctx.ui.custom`-compatible factory for the settings modal.
|
|
40
|
+
* Useful for callers that already manage their own overlay lifecycle.
|
|
41
|
+
*
|
|
42
|
+
* The returned factory captures `ctx` from `openSettingsModal`'s call
|
|
43
|
+
* site — when used standalone, the caller is expected to invoke it via
|
|
44
|
+
* `ctx.ui.custom(createSettingsModal(opts), …)`, and pi will pass the
|
|
45
|
+
* tui/theme/keybindings/done arguments at mount time.
|
|
46
|
+
*/
|
|
47
|
+
export function createSettingsModal<F extends Field>(
|
|
48
|
+
ctx: ExtensionContext,
|
|
49
|
+
options: SettingsModalOptions<F>,
|
|
50
|
+
): SettingsModalFactory<void> {
|
|
51
|
+
return (
|
|
52
|
+
tui: TUI,
|
|
53
|
+
theme: Theme,
|
|
54
|
+
_keybindings: KeybindingsManager,
|
|
55
|
+
done: (result: void) => void,
|
|
56
|
+
): Component => {
|
|
57
|
+
const close = (): void => {
|
|
58
|
+
try {
|
|
59
|
+
options.onClose?.();
|
|
60
|
+
} catch {
|
|
61
|
+
// Caller-supplied onClose must not break the modal teardown.
|
|
62
|
+
}
|
|
63
|
+
openModals.delete(ctx);
|
|
64
|
+
done();
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
// Singleton guard: if a modal is already open on this ctx, close
|
|
68
|
+
// it before opening the new one. This prevents two in-memory
|
|
69
|
+
// buffers racing to write.
|
|
70
|
+
const existing = openModals.get(ctx);
|
|
71
|
+
if (existing) {
|
|
72
|
+
try {
|
|
73
|
+
existing(void 0);
|
|
74
|
+
} catch {
|
|
75
|
+
// Defensive: existing modal's close must not break the new one.
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
openModals.set(ctx, close);
|
|
79
|
+
|
|
80
|
+
// Auto-inject default scope action handlers when configFilename is
|
|
81
|
+
// set but the callbacks are absent. This ensures that any modal using
|
|
82
|
+
// scope tabs always has reset/delete actions without requiring every
|
|
83
|
+
// caller to supply them explicitly.
|
|
84
|
+
if (options.configFilename) {
|
|
85
|
+
if (!options.onResetScope) {
|
|
86
|
+
options.onResetScope = async (scope: "global" | "project") => {
|
|
87
|
+
const dir =
|
|
88
|
+
scope === "project"
|
|
89
|
+
? join(ctx.cwd, ".pi")
|
|
90
|
+
: (options.globalConfigDir ?? getExtensionsDir());
|
|
91
|
+
const knownKeys = new Set(Object.keys(options.defaults ?? {}));
|
|
92
|
+
const existing = readConfig<Record<string, unknown>>(
|
|
93
|
+
options.configFilename!,
|
|
94
|
+
dir,
|
|
95
|
+
);
|
|
96
|
+
const unknownKeys: Record<string, unknown> = {};
|
|
97
|
+
if (existing && typeof existing === "object") {
|
|
98
|
+
for (const [key, val] of Object.entries(existing)) {
|
|
99
|
+
if (!knownKeys.has(key)) unknownKeys[key] = val;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
if (Object.keys(unknownKeys).length > 0) {
|
|
103
|
+
writeConfig(options.configFilename!, unknownKeys, dir);
|
|
104
|
+
} else {
|
|
105
|
+
deleteConfig(options.configFilename!, dir);
|
|
106
|
+
}
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
if (!options.onDeleteScope) {
|
|
110
|
+
options.onDeleteScope = async (scope: "global" | "project") => {
|
|
111
|
+
const dir =
|
|
112
|
+
scope === "project"
|
|
113
|
+
? join(ctx.cwd, ".pi")
|
|
114
|
+
: (options.globalConfigDir ?? getExtensionsDir());
|
|
115
|
+
deleteConfig(options.configFilename!, dir);
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return createSettingsModalBody<F>(options, { tui, theme, ctx, close });
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Convenience: open a settings modal as a centered overlay and resolve
|
|
126
|
+
* when the user closes it. This is the **happy-path** entry point most
|
|
127
|
+
* callers want.
|
|
128
|
+
*
|
|
129
|
+
* Defaults: anchor center, width 92%, maxHeight 85%. Override via
|
|
130
|
+
* `options.overlayOptions`.
|
|
131
|
+
*
|
|
132
|
+
* @example
|
|
133
|
+
* ```ts
|
|
134
|
+
* await openSettingsModal(ctx, {
|
|
135
|
+
* title: "@k0valik/pi-voice",
|
|
136
|
+
* fields: [
|
|
137
|
+
* { key: "muted", type: "boolean", label: "Muted", value: cfg.muted },
|
|
138
|
+
* ],
|
|
139
|
+
* onChange: (key, value) => { cfg[key] = value; saveConfig(cfg); },
|
|
140
|
+
* });
|
|
141
|
+
* ```
|
|
142
|
+
*/
|
|
143
|
+
export async function openSettingsModal<F extends Field>(
|
|
144
|
+
ctx: ExtensionContext,
|
|
145
|
+
options: SettingsModalOptions<F>,
|
|
146
|
+
): Promise<void> {
|
|
147
|
+
const overlayOptions = options.overlayOptions ?? DEFAULT_OVERLAY;
|
|
148
|
+
await ctx.ui.custom<void>(createSettingsModal(ctx, options), {
|
|
149
|
+
overlay: true,
|
|
150
|
+
overlayOptions,
|
|
151
|
+
});
|
|
152
|
+
}
|
|
@@ -0,0 +1,500 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Public types for the `@k0valik/pi-base` settings modal (originally
|
|
3
|
+
* vendored from wierdbytes/pi-common; since heavily reworked).
|
|
4
|
+
*
|
|
5
|
+
* The modal accepts a flat array of `Field`s (or, when `tabs` is set,
|
|
6
|
+
* one such array per tab) and a single `onChange` callback. Every
|
|
7
|
+
* built-in field is a discriminated-union variant of `Field`. Callers
|
|
8
|
+
* needing more exotic widgets can drop down to the `custom` variant and
|
|
9
|
+
* implement a `FieldRenderer` directly.
|
|
10
|
+
*
|
|
11
|
+
* Persistence is intentionally out of scope here — the caller passes
|
|
12
|
+
* each field's current value at open time and decides what to do in
|
|
13
|
+
* `onChange`. Re-opening the modal re-reads the values.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import type { ExtensionContext, Theme } from "@earendil-works/pi-coding-agent";
|
|
17
|
+
import type { Api, Model, ModelThinkingLevel } from "@earendil-works/pi-ai";
|
|
18
|
+
import type {
|
|
19
|
+
Component,
|
|
20
|
+
KeybindingsManager,
|
|
21
|
+
OverlayOptions,
|
|
22
|
+
TUI,
|
|
23
|
+
} from "@earendil-works/pi-tui";
|
|
24
|
+
|
|
25
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
26
|
+
// Visibility context for visibleWhen
|
|
27
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
28
|
+
|
|
29
|
+
export interface VisibilityContext {
|
|
30
|
+
get(key: string): unknown;
|
|
31
|
+
getScoped(key: string, scope?: string): unknown;
|
|
32
|
+
scope: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
36
|
+
// Field discriminated union
|
|
37
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
38
|
+
|
|
39
|
+
/** Common shape for every field variant. */
|
|
40
|
+
export interface FieldBase {
|
|
41
|
+
/** Stable id; passed back as the first argument to `onChange`. */
|
|
42
|
+
key: string;
|
|
43
|
+
/** Left-hand label shown in the row. */
|
|
44
|
+
label: string;
|
|
45
|
+
/** Optional help text rendered under the row when it's focused. */
|
|
46
|
+
description?: string;
|
|
47
|
+
/** Optional tab id this field belongs to (when the modal uses tabs). */
|
|
48
|
+
tab?: string;
|
|
49
|
+
/** Disable interactions; the row still renders but Enter is a no-op. */
|
|
50
|
+
disabled?: boolean;
|
|
51
|
+
/** Optional default value used when resetting the field. */
|
|
52
|
+
default?: unknown;
|
|
53
|
+
/**
|
|
54
|
+
* Opt-in to alt+↑ / alt+↓ reorder. When set, the modal swaps this
|
|
55
|
+
* row with the immediate neighbour (in `visibleRowIndices` order)
|
|
56
|
+
* if and only if that neighbour is also reorderable. The caller's
|
|
57
|
+
* `SettingsModalOptions.onReorder` is invoked after the swap so it
|
|
58
|
+
* can mirror the change into persistent state. Non-reorderable
|
|
59
|
+
* neighbours block the move (no skip-and-swap), so callers should
|
|
60
|
+
* group reorderable rows contiguously inside a tab.
|
|
61
|
+
*/
|
|
62
|
+
reorderable?: boolean;
|
|
63
|
+
/**
|
|
64
|
+
* Visual nesting level. Rows with `depth > 0` are indented by
|
|
65
|
+
* `depth * 2` spaces. Useful for grouping related fields under a
|
|
66
|
+
* parent concept without a nested schema.
|
|
67
|
+
*/
|
|
68
|
+
depth?: number;
|
|
69
|
+
/**
|
|
70
|
+
* Conditionally hide this field in the settings editor. The callback
|
|
71
|
+
* receives a context object that allows reading sibling values and
|
|
72
|
+
* the current scope.
|
|
73
|
+
*
|
|
74
|
+
* Hidden values persist in the config file until explicitly cleared
|
|
75
|
+
* — this is purely a UI affordance, not a data filter.
|
|
76
|
+
*/
|
|
77
|
+
visibleWhen?: (ctx: VisibilityContext) => boolean;
|
|
78
|
+
/**
|
|
79
|
+
* Override the row's label color. The default behaviour is
|
|
80
|
+
* `selected ? "text" : "muted"` — fields opt-in here to express
|
|
81
|
+
* "this row is active" (always rendered with the bright `text`
|
|
82
|
+
* color) or "this row is disabled" (always muted, even when
|
|
83
|
+
* focused) regardless of focus state.
|
|
84
|
+
*
|
|
85
|
+
* Accepts a boolean (`true` ⇒ muted, `false` ⇒ active) or a
|
|
86
|
+
* thunk re-evaluated on every render so the color can track live
|
|
87
|
+
* external state (e.g. a per-block visibility toggle that updates
|
|
88
|
+
* via `onChange`).
|
|
89
|
+
*
|
|
90
|
+
* Unset means "follow default".
|
|
91
|
+
*/
|
|
92
|
+
dim?: boolean | (() => boolean);
|
|
93
|
+
/**
|
|
94
|
+
* When true, changes to this field require the user to run `/reload`
|
|
95
|
+
* for the new value to take effect. The modal aggregates dirty
|
|
96
|
+
* `requiresReload` fields and shows a hint in the confirm prompt.
|
|
97
|
+
*/
|
|
98
|
+
requiresReload?: boolean;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export interface BooleanField extends FieldBase {
|
|
102
|
+
type: "boolean";
|
|
103
|
+
value: boolean;
|
|
104
|
+
default?: boolean;
|
|
105
|
+
/** Per-value help text, keyed by "on" or "off". */
|
|
106
|
+
valueDescriptions?: Partial<Record<"on" | "off", string>>;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export interface EnumField<T extends string = string> extends FieldBase {
|
|
110
|
+
type: "enum";
|
|
111
|
+
value: T;
|
|
112
|
+
options: readonly T[];
|
|
113
|
+
/** Long label shown in the cycle / submenu (defaults to `value`). */
|
|
114
|
+
optionLabels?: Partial<Record<T, string>>;
|
|
115
|
+
/**
|
|
116
|
+
* When `options.length` is greater than this, Enter opens a
|
|
117
|
+
* `SelectList` submenu instead of cycling. Default `4`.
|
|
118
|
+
*/
|
|
119
|
+
cycleThreshold?: number;
|
|
120
|
+
/**
|
|
121
|
+
* Per-value help text shown in the description area when this
|
|
122
|
+
* row is focused. Keyed by option value.
|
|
123
|
+
*/
|
|
124
|
+
valueDescriptions?: Partial<Record<T, string>>;
|
|
125
|
+
/**
|
|
126
|
+
* When true, Enter always opens a filterable list submenu (even
|
|
127
|
+
* for short option lists). The user can type to fuzzy-search
|
|
128
|
+
* options instead of cycling through them one by one.
|
|
129
|
+
* Default `false`.
|
|
130
|
+
*/
|
|
131
|
+
search?: boolean;
|
|
132
|
+
default?: T;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export interface StringField extends FieldBase {
|
|
136
|
+
type: "string";
|
|
137
|
+
value: string;
|
|
138
|
+
/** Optional placeholder shown when the value is empty. */
|
|
139
|
+
placeholder?: string;
|
|
140
|
+
default?: string;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export interface TextField extends FieldBase {
|
|
144
|
+
type: "text";
|
|
145
|
+
value: string;
|
|
146
|
+
/** Optional placeholder shown when the value is empty. */
|
|
147
|
+
placeholder?: string;
|
|
148
|
+
default?: string;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export interface NumberField extends FieldBase {
|
|
152
|
+
type: "number";
|
|
153
|
+
value: number;
|
|
154
|
+
/** Inclusive bounds; values outside are rejected with a notify(). */
|
|
155
|
+
min?: number;
|
|
156
|
+
max?: number;
|
|
157
|
+
/** When true, rejects non-integers. */
|
|
158
|
+
integer?: boolean;
|
|
159
|
+
/**
|
|
160
|
+
* Step size for arrow-key cycling within the range.
|
|
161
|
+
* When set, up-arrow / down-arrow increment/decrement the value by `step`
|
|
162
|
+
* clamped to `[min, max]`. Mutually exclusive with `values`.
|
|
163
|
+
*/
|
|
164
|
+
step?: number;
|
|
165
|
+
/** Discrete list of valid values (mutually exclusive with min/max/integer/step). */
|
|
166
|
+
values?: readonly number[];
|
|
167
|
+
/** Per-value help text, keyed by stringified number. */
|
|
168
|
+
valueDescriptions?: Record<string, string>;
|
|
169
|
+
default?: number;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
export interface SecretField extends FieldBase {
|
|
173
|
+
type: "secret";
|
|
174
|
+
value: string;
|
|
175
|
+
default?: string;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
export interface PathField extends FieldBase {
|
|
179
|
+
type: "path";
|
|
180
|
+
value: string;
|
|
181
|
+
default?: string;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
export interface ActionField extends FieldBase {
|
|
185
|
+
type: "action";
|
|
186
|
+
/** Right-hand display value (defaults to `(run)`). */
|
|
187
|
+
display?: string;
|
|
188
|
+
/** Fired on Enter. */
|
|
189
|
+
onActivate: (ctx: ExtensionContext) => void | Promise<void>;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/** Composite value for a model + reasoning-effort field. */
|
|
193
|
+
export interface ModelValue {
|
|
194
|
+
/** Canonical `<provider>/<id>` string, or `""` for "session model". */
|
|
195
|
+
id: string;
|
|
196
|
+
/** One of pi-ai's six levels (`off|minimal|low|medium|high|xhigh`). */
|
|
197
|
+
thinking?: ModelThinkingLevel;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/** Pre-resolved model option (skips registry discovery when provided). */
|
|
201
|
+
export interface ModelOption {
|
|
202
|
+
/** Canonical `<provider>/<id>`, or `""` for the "session model" sentinel. */
|
|
203
|
+
value: string;
|
|
204
|
+
/** Human-readable label shown in the SelectList. */
|
|
205
|
+
label: string;
|
|
206
|
+
/** Optional concrete model — used to compute supported effort levels. */
|
|
207
|
+
model?: Model<Api>;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
export interface ModelField extends FieldBase {
|
|
211
|
+
type: "model";
|
|
212
|
+
value: ModelValue;
|
|
213
|
+
/** Default sentinel label. Defaults to `"(session model)"`. */
|
|
214
|
+
sessionLabel?: string;
|
|
215
|
+
/** Override the auto-discovered list of models. */
|
|
216
|
+
models?: ModelOption[];
|
|
217
|
+
/** Filter the auto-discovered list (ignored when `models` is set). */
|
|
218
|
+
filter?: (model: Model<Api>) => boolean;
|
|
219
|
+
/** Hide the `(session model)` sentinel. Defaults to `false`. */
|
|
220
|
+
hideSession?: boolean;
|
|
221
|
+
/**
|
|
222
|
+
* Hide the reasoning-effort axis. Useful for tools that don't have
|
|
223
|
+
* a thinking concept at all (e.g. Anthropic server-side `web_search`,
|
|
224
|
+
* which only takes a model id). When `true`:
|
|
225
|
+
* - the submenu only renders the model `SelectList` (no effort row);
|
|
226
|
+
* - `←/→` are not consumed;
|
|
227
|
+
* - the saved value's `thinking` is always `undefined`;
|
|
228
|
+
* - the row label drops the `· effort` suffix.
|
|
229
|
+
* Defaults to `false`.
|
|
230
|
+
*/
|
|
231
|
+
hideEffort?: boolean;
|
|
232
|
+
default?: ModelValue;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Escape hatch for arbitrary widgets. The modal calls `render(args)` to
|
|
237
|
+
* draw the row; `handleInput(data, args)` consumes terminal input when
|
|
238
|
+
* the row is focused. If `openSubmenu` is provided, Enter on the row
|
|
239
|
+
* mounts the returned component full-screen-inside-the-frame; the
|
|
240
|
+
* submenu calls `done(value)` to commit (or `done()` to cancel).
|
|
241
|
+
*/
|
|
242
|
+
export interface CustomField<T = unknown> extends FieldBase {
|
|
243
|
+
type: "custom";
|
|
244
|
+
value: T;
|
|
245
|
+
/** Render the row's right-hand value cell (left part is the label). */
|
|
246
|
+
render: (args: CustomFieldRenderArgs<T>) => string;
|
|
247
|
+
/** Optional inline-edit input handler. Return true to consume. */
|
|
248
|
+
handleInput?: (data: string, args: CustomFieldRenderArgs<T>) => boolean;
|
|
249
|
+
/**
|
|
250
|
+
* Optional submenu mounted on Enter. The factory receives a `done`
|
|
251
|
+
* callback that commits (`done(value)`) or cancels (`done()`).
|
|
252
|
+
*/
|
|
253
|
+
openSubmenu?: (args: CustomFieldSubmenuArgs<T>) => Component;
|
|
254
|
+
/**
|
|
255
|
+
* Override the auto-generated footer hints for this row. The default
|
|
256
|
+
* heuristic in `customRenderer.hints` shows `enter open` when an
|
|
257
|
+
* `openSubmenu` is provided and `enter edit` when only `handleInput`
|
|
258
|
+
* is. Set this to advertise a different set (e.g. `space toggle`
|
|
259
|
+
* when `handleInput` consumes space but Enter is a no-op) without
|
|
260
|
+
* sub-classing the field renderer.
|
|
261
|
+
*/
|
|
262
|
+
hints?: FieldKeyHint[];
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
export interface CustomFieldRenderArgs<T> {
|
|
266
|
+
value: T;
|
|
267
|
+
width: number;
|
|
268
|
+
selected: boolean;
|
|
269
|
+
theme: Theme;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
export interface CustomFieldSubmenuArgs<T> {
|
|
273
|
+
value: T;
|
|
274
|
+
theme: Theme;
|
|
275
|
+
tui: TUI;
|
|
276
|
+
done: (newValue?: T) => void;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/** Discriminated union of every field variant. */
|
|
280
|
+
export type Field =
|
|
281
|
+
| BooleanField
|
|
282
|
+
| EnumField
|
|
283
|
+
| StringField
|
|
284
|
+
| TextField
|
|
285
|
+
| NumberField
|
|
286
|
+
| SecretField
|
|
287
|
+
| PathField
|
|
288
|
+
| ActionField
|
|
289
|
+
| ModelField
|
|
290
|
+
| CustomField;
|
|
291
|
+
|
|
292
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
293
|
+
// Renderer interface (internal, but exported for advanced callers)
|
|
294
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
295
|
+
|
|
296
|
+
export interface FieldRenderContext {
|
|
297
|
+
theme: Theme;
|
|
298
|
+
tui: TUI;
|
|
299
|
+
ctx: ExtensionContext;
|
|
300
|
+
requestRender: () => void;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
export interface FieldKeyHint {
|
|
304
|
+
key: string;
|
|
305
|
+
label: string;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/** Live state of a row — passed to renderers and input handlers. */
|
|
309
|
+
export interface FieldRow<F extends Field = Field, V = unknown> {
|
|
310
|
+
field: F;
|
|
311
|
+
/** Current displayed value (may be ahead of disk if onChange is async). */
|
|
312
|
+
value: V;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* A `FieldRenderer` knows how to draw, focus, and edit one variant of
|
|
317
|
+
* `Field`. Built-in renderers live in `./fields/*.ts`; custom callers
|
|
318
|
+
* should use `type: "custom"` instead of implementing this interface
|
|
319
|
+
* directly.
|
|
320
|
+
*/
|
|
321
|
+
export interface FieldRenderer<F extends Field = Field, V = unknown> {
|
|
322
|
+
/** The discriminator this renderer handles. */
|
|
323
|
+
type: F["type"];
|
|
324
|
+
/** Render the right-hand value cell for one row. */
|
|
325
|
+
renderValue(
|
|
326
|
+
row: FieldRow<F, V>,
|
|
327
|
+
args: {
|
|
328
|
+
width: number;
|
|
329
|
+
selected: boolean;
|
|
330
|
+
isEditing: boolean;
|
|
331
|
+
ctx: FieldRenderContext;
|
|
332
|
+
},
|
|
333
|
+
): string;
|
|
334
|
+
/** Footer-hint pieces shown when this row is focused. */
|
|
335
|
+
hints(row: FieldRow<F, V>, args: { isEditing: boolean }): FieldKeyHint[];
|
|
336
|
+
/**
|
|
337
|
+
* Handle a key event. Return `consumed` to suppress default handling
|
|
338
|
+
* (navigation, esc-close); return a `commit` value to persist; return
|
|
339
|
+
* a `submenu` to mount one. Called only when the row is focused.
|
|
340
|
+
*/
|
|
341
|
+
handleKey(
|
|
342
|
+
row: FieldRow<F, V>,
|
|
343
|
+
data: string,
|
|
344
|
+
args: {
|
|
345
|
+
isEditing: boolean;
|
|
346
|
+
ctx: FieldRenderContext;
|
|
347
|
+
setEditing: (v: boolean) => void;
|
|
348
|
+
},
|
|
349
|
+
): FieldKeyResult<V>;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
/**
|
|
353
|
+
* Submenu factory passed back from a renderer's `handleKey`. The modal
|
|
354
|
+
* supplies the `done` callback when mounting; calling `done(value)`
|
|
355
|
+
* commits and unmounts, calling `done()` cancels and unmounts.
|
|
356
|
+
*/
|
|
357
|
+
export type SubmenuFactory<V> = (done: (value?: V) => void) => Component;
|
|
358
|
+
|
|
359
|
+
export interface FieldKeyResult<V> {
|
|
360
|
+
consumed?: boolean;
|
|
361
|
+
/** A new value to commit (modal calls onChange). */
|
|
362
|
+
commit?: V;
|
|
363
|
+
/** Submenu factory to mount; modal supplies the `done` callback. */
|
|
364
|
+
submenu?: SubmenuFactory<V>;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
368
|
+
// Tabs and modal options
|
|
369
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
370
|
+
|
|
371
|
+
export interface Tab {
|
|
372
|
+
/** Stable id; `field.tab` matches against this. */
|
|
373
|
+
id: string;
|
|
374
|
+
/** Pill label shown in the tab strip. */
|
|
375
|
+
label: string;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
export interface SettingsTheme {
|
|
379
|
+
/** Override the default frame title colour. Optional. */
|
|
380
|
+
titleColor?: (text: string) => string;
|
|
381
|
+
/** Override the focused-row background. Optional. */
|
|
382
|
+
rowSelected?: (text: string) => string;
|
|
383
|
+
/** Override the inline-edit value colour. Optional. */
|
|
384
|
+
editingValue?: (text: string) => string;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
export interface SettingsModalOptions<F extends Field = Field> {
|
|
388
|
+
/** Title rendered in the frame's top border (e.g. `"@k0valik/pi-voice"`). */
|
|
389
|
+
title?: string;
|
|
390
|
+
/** Field rows. May span multiple tabs via each field's optional `tab` id. */
|
|
391
|
+
fields: F[];
|
|
392
|
+
/** Optional tab strip; rendered only when length ≥ 1. */
|
|
393
|
+
tabs?: Tab[];
|
|
394
|
+
/** Optional config filename to enable tabulated global vs project local views. */
|
|
395
|
+
configFilename?: string;
|
|
396
|
+
/** Optional defaults for the configuration to fallback to when loading. */
|
|
397
|
+
defaults?: Record<string, unknown>;
|
|
398
|
+
/** Initial tab id (defaults to the first tab). */
|
|
399
|
+
initialTab?: string;
|
|
400
|
+
/** Show a fuzzy-search bar above the list. */
|
|
401
|
+
enableSearch?: boolean;
|
|
402
|
+
/** Theme overrides (mostly used for callers with fixed-palette aesthetics). */
|
|
403
|
+
theme?: SettingsTheme;
|
|
404
|
+
/** Override the overlay positioning (defaults: anchor center, 92% × 85%). */
|
|
405
|
+
overlayOptions?: OverlayOptions | (() => OverlayOptions);
|
|
406
|
+
/** Override the global config directory used for scope-tab loading. Defaults to `getExtensionsDir()`. */
|
|
407
|
+
globalConfigDir?: string;
|
|
408
|
+
/**
|
|
409
|
+
* Called whenever a field's value changes. The modal calls this
|
|
410
|
+
* synchronously after updating its own row state, so any throw here
|
|
411
|
+
* is surfaced via `ctx.ui.notify` and does NOT roll back the row.
|
|
412
|
+
*/
|
|
413
|
+
onChange?: <K extends F["key"]>(
|
|
414
|
+
key: K,
|
|
415
|
+
value: ValueOfField<F, K>,
|
|
416
|
+
field: F,
|
|
417
|
+
) => void | Promise<void>;
|
|
418
|
+
/**
|
|
419
|
+
* Buffered mode: persist all edits at once on explicit save.
|
|
420
|
+
* When `mode` is `"buffered"`, the modal holds edits in memory and
|
|
421
|
+
* calls `onSave` only when the user confirms. `onChange` is optional
|
|
422
|
+
* and serves only live preview; dirty detection is modal-owned.
|
|
423
|
+
*/
|
|
424
|
+
mode?: "immediate" | "buffered";
|
|
425
|
+
/**
|
|
426
|
+
* Called when the user confirms a buffered save. Receives the
|
|
427
|
+
* full buffer (all field values, including untouched defaults) and
|
|
428
|
+
* the chosen scope. May be async.
|
|
429
|
+
*/
|
|
430
|
+
onSave?: (
|
|
431
|
+
values: Record<string, unknown>,
|
|
432
|
+
scope: "global" | "project",
|
|
433
|
+
) => void | Promise<void>;
|
|
434
|
+
/**
|
|
435
|
+
* Called when the user discards buffered edits. The modal closes
|
|
436
|
+
* immediately after.
|
|
437
|
+
*/
|
|
438
|
+
onCancel?: () => void;
|
|
439
|
+
/**
|
|
440
|
+
* Called when the user presses alt+↑ / alt+↓ on a `reorderable: true`
|
|
441
|
+
* row. The modal has already swapped its internal `rows` array and
|
|
442
|
+
* moved focus to follow the row by the time this fires; the caller
|
|
443
|
+
* just needs to mirror the change into persistent state.
|
|
444
|
+
*
|
|
445
|
+
* Indices are 0-based positions **within the active tab's visible
|
|
446
|
+
* rows** (so they map directly to e.g. `layout.order` positions for
|
|
447
|
+
* statusline-style use cases). Both `fromIndex` and `toIndex` count
|
|
448
|
+
* only `reorderable: true` peers — non-reorderable rows interleaved
|
|
449
|
+
* with the reorderable ones do NOT participate and do NOT shift the
|
|
450
|
+
* peer index.
|
|
451
|
+
*/
|
|
452
|
+
onReorder?: (info: {
|
|
453
|
+
fieldKey: string;
|
|
454
|
+
fromIndex: number;
|
|
455
|
+
toIndex: number;
|
|
456
|
+
}) => void;
|
|
457
|
+
/**
|
|
458
|
+
* Called once when the modal closes. Useful for fire-and-forget
|
|
459
|
+
* cleanup (e.g. saving a debounced config).
|
|
460
|
+
*/
|
|
461
|
+
onClose?: () => void;
|
|
462
|
+
/**
|
|
463
|
+
* Infer the default scope for the confirm prompt. Called once at
|
|
464
|
+
* modal mount. Return "project" if a project-local config file
|
|
465
|
+
* exists, "global" otherwise. Optional — defaults to "global".
|
|
466
|
+
*
|
|
467
|
+
* This keeps the modal framework independent of any specific
|
|
468
|
+
* config library (pi-base, custom fs, etc.) — the extension
|
|
469
|
+
* decides what "project-local" means for its own config system.
|
|
470
|
+
*/
|
|
471
|
+
inferDefaultScope?: () => "global" | "project";
|
|
472
|
+
/**
|
|
473
|
+
* Called when the user activates "Reset scope to defaults" from
|
|
474
|
+
* the bottom of a scope tab. The callback should delete the
|
|
475
|
+
* scope's config file or otherwise clear all overrides so the
|
|
476
|
+
* next load falls back to defaults entirely.
|
|
477
|
+
*
|
|
478
|
+
* After this callback completes, the modal re-reads configs and
|
|
479
|
+
* updates all rows with their default values.
|
|
480
|
+
*/
|
|
481
|
+
onResetScope?: (scope: "global" | "project") => void | Promise<void>;
|
|
482
|
+
/**
|
|
483
|
+
* Called when the user activates "Delete config" from the bottom of
|
|
484
|
+
* a scope tab. The callback should delete the scope's config file
|
|
485
|
+
* entirely (not just reset it).
|
|
486
|
+
*/
|
|
487
|
+
onDeleteScope?: (scope: "global" | "project") => void | Promise<void>;
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
/** Value type of the field with key `K` inside a union `F`. */
|
|
491
|
+
export type ValueOfField<F extends Field, K extends string> =
|
|
492
|
+
Extract<F, { key: K }> extends { value: infer V } ? V : never;
|
|
493
|
+
|
|
494
|
+
/** Component factory shape required by `ctx.ui.custom`. */
|
|
495
|
+
export type SettingsModalFactory<T = void> = (
|
|
496
|
+
tui: TUI,
|
|
497
|
+
theme: Theme,
|
|
498
|
+
keybindings: KeybindingsManager,
|
|
499
|
+
done: (result: T) => void,
|
|
500
|
+
) => Component;
|