@timurproko/a1 0.1.8-dev.512 → 0.1.8-dev.521
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 +64 -20
- package/dist/app/session-shell/index.d.ts +1 -1
- package/dist/app/session-shell/prompt-suggestion-controller.d.ts +2 -0
- package/dist/app/session-shell/prompt-suggestion-controller.js +12 -0
- package/dist/app/session-shell/session-shell-root.d.ts +10 -0
- package/dist/app/session-shell/session-shell-root.js +12 -2
- package/dist/app/session-shell/session-shell.d.ts +8 -1
- package/dist/app/session-shell/session-shell.js +217 -18
- package/dist/composition/index.d.ts +1 -1
- package/dist/composition/owned-ui.js +21 -3
- package/dist/composition/settings-route-host.d.ts +17 -4
- package/dist/composition/settings-route-host.js +47 -16
- package/dist/contracts/owned-ui/index.d.ts +1 -1
- package/dist/contracts/owned-ui/model.d.ts +1 -5
- package/dist/features/owned-ui/index.d.ts +3 -0
- package/dist/features/owned-ui/index.js +2 -0
- package/dist/features/owned-ui/reference-routes.d.ts +6 -0
- package/dist/features/owned-ui/reference-routes.js +6 -0
- package/dist/features/owned-ui/reference-screen-app.d.ts +37 -0
- package/dist/features/owned-ui/reference-screen-app.js +232 -0
- package/dist/features/owned-ui/settings-app.js +2 -7
- package/dist/integrations/pi/components/index.d.ts +4 -2
- package/dist/integrations/pi/components/index.js +2 -1
- package/dist/integrations/pi/components/models-dialog.d.ts +55 -0
- package/dist/integrations/pi/components/models-dialog.js +310 -0
- package/dist/integrations/pi/components/shell-editor-autocomplete.d.ts +10 -0
- package/dist/integrations/pi/components/shell-editor-autocomplete.js +29 -5
- package/dist/integrations/pi/components/shell-footer-status.js +2 -1
- package/dist/integrations/pi/components/shell-presenters-info.d.ts +10 -0
- package/dist/integrations/pi/components/shell-presenters-info.js +31 -11
- package/dist/integrations/pi/components/shell-selectors-dialogs.d.ts +11 -1
- package/dist/integrations/pi/components/shell-selectors-dialogs.js +13 -0
- package/dist/integrations/pi/components/shell-shared-facade.d.ts +8 -0
- package/dist/integrations/pi/components/skills-command.d.ts +47 -0
- package/dist/integrations/pi/components/skills-command.js +118 -0
- package/dist/integrations/pi/components/skills-dialog.d.ts +9 -0
- package/dist/integrations/pi/components/skills-dialog.js +123 -0
- package/dist/integrations/pi/components/upstream/components/owned-editor.d.ts +8 -2
- package/dist/integrations/pi/components/upstream/components/owned-editor.js +42 -7
- package/dist/integrations/pi/engine/adapter.d.ts +5 -1
- package/dist/integrations/pi/engine/adapter.js +13 -1
- package/dist/integrations/pi/engine/index.d.ts +2 -2
- package/dist/integrations/pi/engine/index.js +1 -1
- package/dist/integrations/pi/engine/resource-catalog.d.ts +2 -1
- package/dist/integrations/pi/engine/resource-catalog.js +19 -9
- package/dist/integrations/pi/engine/workflow-contexts.d.ts +8 -1
- package/dist/integrations/pi/engine/workflow-contexts.js +52 -8
- package/dist/integrations/pi/engine/workflow-runner.js +98 -74
- package/dist/integrations/pi/engine/workflows.d.ts +26 -2
- package/dist/integrations/pi/engine/workflows.js +8 -0
- package/dist/native/darwin-arm64/manifest.json +1 -1
- package/dist/native/linux-x64/manifest.json +1 -1
- package/dist/native/win32-x64/manifest.json +2 -2
- package/dist/native/win32-x64/process-guardian.exe +0 -0
- package/dist/ui/apps/contracts.d.ts +9 -1
- package/dist/ui/apps/index.d.ts +1 -1
- package/dist/ui/settings/declarations.d.ts +15 -29
- package/dist/ui/settings/declarations.js +14 -25
- package/dist/ui/settings/index.d.ts +1 -1
- package/dist/ui/settings/index.js +1 -1
- package/dist/ui/settings/migrations.js +18 -0
- package/docs/architecture/prompt-suggestions.md +1 -1
- package/docs/architecture/ui-reference-provenance.md +1 -0
- package/docs/ci-release-runbook.md +31 -23
- package/docs/features/modal-content-interaction.md +3 -2
- package/package.json +1 -1
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
import { Input, Key, getKeybindings, matchesKey, truncateToWidth, } from "@earendil-works/pi-tui";
|
|
2
|
+
import { piTheme } from "./theme.js";
|
|
3
|
+
const MAX_VISIBLE_ROWS = 10;
|
|
4
|
+
const MODELS_TITLE = "Models";
|
|
5
|
+
function fullModelId(model) {
|
|
6
|
+
return `${model.provider}/${model.id}`;
|
|
7
|
+
}
|
|
8
|
+
function sameOrder(left, right) {
|
|
9
|
+
return left.length === right.length && left.every((value, index) => value === right[index]);
|
|
10
|
+
}
|
|
11
|
+
function matchesQuery(model, query) {
|
|
12
|
+
const normalized = query.trim().toLowerCase();
|
|
13
|
+
if (normalized.length === 0)
|
|
14
|
+
return true;
|
|
15
|
+
return `${model.id} ${model.name} ${model.provider} ${fullModelId(model)}`.toLowerCase().includes(normalized);
|
|
16
|
+
}
|
|
17
|
+
function keyLabel(action) {
|
|
18
|
+
return getKeybindings().getKeys(action)
|
|
19
|
+
.map(key => key.split("+").map(part => process.platform === "darwin" && part.toLowerCase() === "alt" ? "option" : part).join("+"))
|
|
20
|
+
.join("/");
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Bare A1's one model-management dialog: a searchable catalog with `all`/`scoped` filters where
|
|
24
|
+
* Enter switches the active model, Space edits the session's cycling scope, and Ctrl+S persists
|
|
25
|
+
* that scope. The component owns only presentation state (query, filter, selection, desired and
|
|
26
|
+
* saved scope, refresh status); every switch, scope write, and save goes through its callbacks.
|
|
27
|
+
*/
|
|
28
|
+
export class ModelsDialogComponent {
|
|
29
|
+
#input = new Input();
|
|
30
|
+
#callbacks;
|
|
31
|
+
#models = [];
|
|
32
|
+
#activeModelId;
|
|
33
|
+
#scopeIds;
|
|
34
|
+
#savedScopeIds;
|
|
35
|
+
#filter;
|
|
36
|
+
#selectedIndex = 0;
|
|
37
|
+
#preferredId;
|
|
38
|
+
#refreshStatus;
|
|
39
|
+
#disposed = false;
|
|
40
|
+
#focused = false;
|
|
41
|
+
constructor(config, callbacks) {
|
|
42
|
+
this.#callbacks = callbacks;
|
|
43
|
+
this.#activeModelId = config.activeModelId;
|
|
44
|
+
this.#scopeIds = [...config.scopeIds];
|
|
45
|
+
this.#savedScopeIds = [...config.savedScopeIds];
|
|
46
|
+
this.#filter = config.initialFilter ?? "all";
|
|
47
|
+
this.#refreshStatus = config.refreshStatus === undefined ? undefined : { message: config.refreshStatus, kind: "muted" };
|
|
48
|
+
if (config.initialQuery)
|
|
49
|
+
this.#input.setValue(config.initialQuery);
|
|
50
|
+
this.#replaceModels(config.models);
|
|
51
|
+
const rows = this.#rows();
|
|
52
|
+
const activeIndex = rows.findIndex(row => row.fullId === this.#activeModelId);
|
|
53
|
+
this.#selectedIndex = activeIndex >= 0 ? activeIndex : 0;
|
|
54
|
+
}
|
|
55
|
+
get focused() {
|
|
56
|
+
return this.#focused;
|
|
57
|
+
}
|
|
58
|
+
set focused(value) {
|
|
59
|
+
this.#focused = value;
|
|
60
|
+
this.#input.focused = value;
|
|
61
|
+
}
|
|
62
|
+
/** True while the desired scope or order differs from the last successful save. */
|
|
63
|
+
get dirty() {
|
|
64
|
+
return !sameOrder(this.#scopeIds, this.#savedScopeIds);
|
|
65
|
+
}
|
|
66
|
+
get filter() {
|
|
67
|
+
return this.#filter;
|
|
68
|
+
}
|
|
69
|
+
get query() {
|
|
70
|
+
return this.#input.getValue();
|
|
71
|
+
}
|
|
72
|
+
get scopeIds() {
|
|
73
|
+
return [...this.#scopeIds];
|
|
74
|
+
}
|
|
75
|
+
get selectedModelId() {
|
|
76
|
+
return this.#rows()[this.#selectedIndex]?.fullId;
|
|
77
|
+
}
|
|
78
|
+
/** Replace the catalog after a refresh; query, filter, surviving selection, scope edits, and dirty state stay. */
|
|
79
|
+
updateModels(models) {
|
|
80
|
+
const selectedId = this.selectedModelId;
|
|
81
|
+
this.#replaceModels(models);
|
|
82
|
+
this.#restoreSelection(selectedId);
|
|
83
|
+
}
|
|
84
|
+
setRefreshStatus(message, kind) {
|
|
85
|
+
this.#refreshStatus = { message, kind };
|
|
86
|
+
}
|
|
87
|
+
dispose() {
|
|
88
|
+
this.#disposed = true;
|
|
89
|
+
}
|
|
90
|
+
invalidate() {
|
|
91
|
+
this.#input.invalidate();
|
|
92
|
+
}
|
|
93
|
+
handleInput(data) {
|
|
94
|
+
const kb = getKeybindings();
|
|
95
|
+
const rows = this.#rows();
|
|
96
|
+
this.#clampSelection(rows);
|
|
97
|
+
const selected = rows[this.#selectedIndex];
|
|
98
|
+
// Rationale: an emptied filter keeps the last highlighted model so switching filters lands back on it.
|
|
99
|
+
const preferredId = selected?.fullId ?? this.#preferredId;
|
|
100
|
+
this.#preferredId = preferredId;
|
|
101
|
+
if (kb.matches(data, "tui.select.up")) {
|
|
102
|
+
if (rows.length > 0)
|
|
103
|
+
this.#selectedIndex = this.#selectedIndex === 0 ? rows.length - 1 : this.#selectedIndex - 1;
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
if (kb.matches(data, "tui.select.down")) {
|
|
107
|
+
if (rows.length > 0)
|
|
108
|
+
this.#selectedIndex = this.#selectedIndex === rows.length - 1 ? 0 : this.#selectedIndex + 1;
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
if (kb.matches(data, "tui.input.tab")) {
|
|
112
|
+
this.#filter = this.#filter === "all" ? "scoped" : "all";
|
|
113
|
+
this.#restoreSelection(preferredId);
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
if (data === " ") {
|
|
117
|
+
if (selected)
|
|
118
|
+
this.#setScope(this.#scopeIds.includes(selected.fullId)
|
|
119
|
+
? this.#scopeIds.filter(id => id !== selected.fullId)
|
|
120
|
+
: [...this.#scopeIds, selected.fullId], selected.fullId);
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
if (kb.matches(data, "tui.select.confirm")) {
|
|
124
|
+
if (selected)
|
|
125
|
+
this.#callbacks.onSelect(selected.fullId);
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
if (kb.matches(data, "app.models.save")) {
|
|
129
|
+
this.#save();
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
if (kb.matches(data, "app.models.enableAll")) {
|
|
133
|
+
const targets = this.#bulkTargets(rows);
|
|
134
|
+
this.#setScope([...this.#scopeIds, ...targets.filter(id => !this.#scopeIds.includes(id))], selected?.fullId);
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
if (kb.matches(data, "app.models.clearAll")) {
|
|
138
|
+
const targets = new Set(this.#bulkTargets(rows));
|
|
139
|
+
this.#setScope(this.#scopeIds.filter(id => !targets.has(id)), selected?.fullId);
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
if (kb.matches(data, "app.models.toggleProvider")) {
|
|
143
|
+
if (!selected)
|
|
144
|
+
return;
|
|
145
|
+
const providerIds = this.#models.filter(model => model.provider === selected.model.provider).map(fullModelId);
|
|
146
|
+
const allScoped = providerIds.every(id => this.#scopeIds.includes(id));
|
|
147
|
+
this.#setScope(allScoped
|
|
148
|
+
? this.#scopeIds.filter(id => !providerIds.includes(id))
|
|
149
|
+
: [...this.#scopeIds, ...providerIds.filter(id => !this.#scopeIds.includes(id))], selected.fullId);
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
const reorderUp = kb.matches(data, "app.models.reorderUp");
|
|
153
|
+
if (reorderUp || kb.matches(data, "app.models.reorderDown")) {
|
|
154
|
+
if (!selected)
|
|
155
|
+
return;
|
|
156
|
+
const index = this.#scopeIds.indexOf(selected.fullId);
|
|
157
|
+
const target = index + (reorderUp ? -1 : 1);
|
|
158
|
+
if (index < 0 || target < 0 || target >= this.#scopeIds.length)
|
|
159
|
+
return;
|
|
160
|
+
const next = [...this.#scopeIds];
|
|
161
|
+
next[index] = next[target];
|
|
162
|
+
next[target] = selected.fullId;
|
|
163
|
+
this.#setScope(next, selected.fullId);
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
if (matchesKey(data, Key.ctrl("c"))) {
|
|
167
|
+
if (this.#input.getValue().length > 0) {
|
|
168
|
+
this.#input.setValue("");
|
|
169
|
+
this.#selectedIndex = 0;
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
this.#callbacks.onCancel();
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
if (kb.matches(data, "tui.select.cancel")) {
|
|
176
|
+
this.#callbacks.onCancel();
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
const before = this.#input.getValue();
|
|
180
|
+
this.#input.handleInput(data);
|
|
181
|
+
if (this.#input.getValue() !== before)
|
|
182
|
+
this.#selectedIndex = 0;
|
|
183
|
+
}
|
|
184
|
+
render(width) {
|
|
185
|
+
const theme = piTheme();
|
|
186
|
+
const lines = [];
|
|
187
|
+
const push = (line = "") => { lines.push(truncateToWidth(line, width)); };
|
|
188
|
+
const border = theme.fg("border", "─".repeat(Math.max(1, width)));
|
|
189
|
+
const rows = this.#rows();
|
|
190
|
+
this.#clampSelection(rows);
|
|
191
|
+
push(border);
|
|
192
|
+
push();
|
|
193
|
+
push(theme.fg("accent", theme.bold(MODELS_TITLE)) + (this.dirty ? theme.fg("warning", " (unsaved)") : ""));
|
|
194
|
+
push(theme.fg("dim", "Filter: ")
|
|
195
|
+
+ theme.fg(this.#filter === "all" ? "accent" : "dim", "all")
|
|
196
|
+
+ theme.fg("dim", " | ")
|
|
197
|
+
+ theme.fg(this.#filter === "scoped" ? "accent" : "dim", "scoped"));
|
|
198
|
+
push();
|
|
199
|
+
for (const line of this.#input.render(width))
|
|
200
|
+
push(line);
|
|
201
|
+
push();
|
|
202
|
+
if (rows.length === 0) {
|
|
203
|
+
push(theme.fg("muted", ` ${this.#emptyText()}`));
|
|
204
|
+
}
|
|
205
|
+
else {
|
|
206
|
+
const startIndex = Math.max(0, Math.min(this.#selectedIndex - Math.floor(MAX_VISIBLE_ROWS / 2), rows.length - MAX_VISIBLE_ROWS));
|
|
207
|
+
const endIndex = Math.min(startIndex + MAX_VISIBLE_ROWS, rows.length);
|
|
208
|
+
for (let index = startIndex; index < endIndex; index += 1) {
|
|
209
|
+
const row = rows[index];
|
|
210
|
+
const selected = index === this.#selectedIndex;
|
|
211
|
+
const scoped = this.#scopeIds.includes(row.fullId);
|
|
212
|
+
// Invariant: arrow, scope marker, model id, [provider], then the active checkmark, in that order.
|
|
213
|
+
const prefix = selected ? theme.fg("accent", "→ ") : " ";
|
|
214
|
+
const marker = scoped ? theme.fg("success", "●") : theme.fg("dim", "○");
|
|
215
|
+
const label = selected ? theme.fg("accent", row.model.id) : row.model.id;
|
|
216
|
+
const provider = theme.fg("muted", `[${row.model.provider}]`);
|
|
217
|
+
const active = row.fullId === this.#activeModelId ? ` ${theme.fg("success", "✓")}` : "";
|
|
218
|
+
push(`${prefix}${marker} ${label} ${provider}${active}`);
|
|
219
|
+
}
|
|
220
|
+
if (startIndex > 0 || endIndex < rows.length)
|
|
221
|
+
push(theme.fg("muted", ` (${this.#selectedIndex + 1}/${rows.length})`));
|
|
222
|
+
const selectedRow = rows[this.#selectedIndex];
|
|
223
|
+
if (selectedRow) {
|
|
224
|
+
push();
|
|
225
|
+
push(theme.fg("muted", ` Model Name: ${selectedRow.model.name}`));
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
push();
|
|
229
|
+
if (this.#refreshStatus !== undefined)
|
|
230
|
+
push(theme.fg(this.#refreshStatus.kind, ` ${this.#refreshStatus.message}`));
|
|
231
|
+
push(theme.fg("dim", ` ${this.#hints().join(" · ")}`));
|
|
232
|
+
push(border);
|
|
233
|
+
return lines;
|
|
234
|
+
}
|
|
235
|
+
#hints() {
|
|
236
|
+
const tab = keyLabel("tui.input.tab");
|
|
237
|
+
const confirm = keyLabel("tui.select.confirm");
|
|
238
|
+
const save = keyLabel("app.models.save");
|
|
239
|
+
return [
|
|
240
|
+
"type to search",
|
|
241
|
+
"↑↓ navigate",
|
|
242
|
+
...(tab.length === 0 ? [] : [`${tab} filter`]),
|
|
243
|
+
...(confirm.length === 0 ? [] : [`${confirm} switch`]),
|
|
244
|
+
"space scope",
|
|
245
|
+
...(save.length === 0 ? [] : [`${save} save`]),
|
|
246
|
+
"esc close",
|
|
247
|
+
];
|
|
248
|
+
}
|
|
249
|
+
#emptyText() {
|
|
250
|
+
if (this.#input.getValue().trim().length > 0)
|
|
251
|
+
return "No matching models";
|
|
252
|
+
if (this.#filter === "scoped")
|
|
253
|
+
return "No scoped models";
|
|
254
|
+
return "No models available. Use /login to add providers.";
|
|
255
|
+
}
|
|
256
|
+
#replaceModels(models) {
|
|
257
|
+
this.#models = [...models].sort((left, right) => left.provider.localeCompare(right.provider) || left.id.localeCompare(right.id));
|
|
258
|
+
}
|
|
259
|
+
#rows() {
|
|
260
|
+
const query = this.#input.getValue();
|
|
261
|
+
const byId = new Map(this.#models.map(model => [fullModelId(model), model]));
|
|
262
|
+
const ordered = this.#filter === "scoped"
|
|
263
|
+
// Rationale: the scoped view lists the desired cycling order, then saved members removed in this session so they can be restored.
|
|
264
|
+
? [...this.#scopeIds, ...this.#savedScopeIds.filter(id => !this.#scopeIds.includes(id))].flatMap(id => {
|
|
265
|
+
const model = byId.get(id);
|
|
266
|
+
return model === undefined ? [] : [model];
|
|
267
|
+
})
|
|
268
|
+
: this.#models;
|
|
269
|
+
return ordered.filter(model => matchesQuery(model, query)).map(model => ({ fullId: fullModelId(model), model }));
|
|
270
|
+
}
|
|
271
|
+
#bulkTargets(rows) {
|
|
272
|
+
// Compatibility: like the pinned scoped selector, a live search narrows bulk actions to the visible rows.
|
|
273
|
+
return this.#input.getValue().length > 0 || this.#filter === "scoped" ? rows.map(row => row.fullId) : this.#models.map(fullModelId);
|
|
274
|
+
}
|
|
275
|
+
#clampSelection(rows) {
|
|
276
|
+
this.#selectedIndex = Math.min(this.#selectedIndex, Math.max(0, rows.length - 1));
|
|
277
|
+
}
|
|
278
|
+
#restoreSelection(fullId) {
|
|
279
|
+
const rows = this.#rows();
|
|
280
|
+
const index = fullId === undefined ? -1 : rows.findIndex(row => row.fullId === fullId);
|
|
281
|
+
this.#selectedIndex = index >= 0 ? index : Math.min(this.#selectedIndex, Math.max(0, rows.length - 1));
|
|
282
|
+
}
|
|
283
|
+
#setScope(next, keepSelected) {
|
|
284
|
+
if (sameOrder(next, this.#scopeIds))
|
|
285
|
+
return;
|
|
286
|
+
this.#scopeIds = [...next];
|
|
287
|
+
this.#restoreSelection(keepSelected);
|
|
288
|
+
this.#callbacks.onScopeChange([...this.#scopeIds]);
|
|
289
|
+
}
|
|
290
|
+
#save() {
|
|
291
|
+
const snapshot = [...this.#scopeIds];
|
|
292
|
+
let outcome;
|
|
293
|
+
try {
|
|
294
|
+
outcome = this.#callbacks.onSave(snapshot);
|
|
295
|
+
}
|
|
296
|
+
catch {
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
Promise.resolve(outcome).then(() => {
|
|
300
|
+
if (this.#disposed)
|
|
301
|
+
return;
|
|
302
|
+
// Invariant: only the exact snapshot that persisted becomes the baseline; later edits stay dirty.
|
|
303
|
+
this.#savedScopeIds = snapshot;
|
|
304
|
+
this.#callbacks.requestRender();
|
|
305
|
+
}, () => {
|
|
306
|
+
if (!this.#disposed)
|
|
307
|
+
this.#callbacks.requestRender();
|
|
308
|
+
});
|
|
309
|
+
}
|
|
310
|
+
}
|
|
@@ -8,4 +8,14 @@ export declare const PINNED_PI_BUILTIN_SLASH_COMMANDS: ({
|
|
|
8
8
|
description: string;
|
|
9
9
|
argumentHint: string;
|
|
10
10
|
})[];
|
|
11
|
+
/** Bare A1's built-in catalog: one `models` command replaces the pinned `model` and `scoped-models` entries. */
|
|
12
|
+
export declare const OWNED_BUILTIN_SLASH_COMMANDS: ({
|
|
13
|
+
name: string;
|
|
14
|
+
description: string;
|
|
15
|
+
argumentHint?: never;
|
|
16
|
+
} | {
|
|
17
|
+
name: string;
|
|
18
|
+
description: string;
|
|
19
|
+
argumentHint: string;
|
|
20
|
+
})[];
|
|
11
21
|
export declare function createPiShellEditor(options: PiShellEditorOptions): PiShellEditorPort;
|
|
@@ -6,6 +6,9 @@ import { OwnedEditor } from "./upstream/components/owned-editor.js";
|
|
|
6
6
|
import { OwnedEditorUxInterception, createPromptSelectionInterceptor, editorVisualLineCount, } from "./owned-editor-ux.js";
|
|
7
7
|
import { PINNED_PI_LAYOUT, piTheme, } from "./theme.js";
|
|
8
8
|
import { createTuiFacade, ensureTheme, isAutocompleteProvider, } from "./shell-shared-facade.js";
|
|
9
|
+
import { SKILLS_COMMAND_NAME, collapseSkillCommands, createSkillsTunnelProvider, } from "./skills-command.js";
|
|
10
|
+
/** A selected tunnel row: the accent label, then at least two spaces, then the description. */
|
|
11
|
+
const SELECTED_TUNNEL_ROW = /^(→ skills:\S+)(\s{2,}.*)$/u;
|
|
9
12
|
export const PINNED_PI_BUILTIN_SLASH_COMMANDS = [
|
|
10
13
|
{ name: "settings", description: "Open settings menu" },
|
|
11
14
|
{ name: "model", description: "Select model (opens selector UI)", argumentHint: "<provider/model>" },
|
|
@@ -30,6 +33,10 @@ export const PINNED_PI_BUILTIN_SLASH_COMMANDS = [
|
|
|
30
33
|
{ name: "reload", description: "Reload keybindings, extensions, skills, prompts, themes, and context files" },
|
|
31
34
|
{ name: "quit", description: "Quit" },
|
|
32
35
|
];
|
|
36
|
+
/** Bare A1's built-in catalog: one `models` command replaces the pinned `model` and `scoped-models` entries. */
|
|
37
|
+
export const OWNED_BUILTIN_SLASH_COMMANDS = PINNED_PI_BUILTIN_SLASH_COMMANDS.flatMap(command => command.name === "model"
|
|
38
|
+
? [{ name: "models", description: "Switch models and manage scoped model cycling" }]
|
|
39
|
+
: command.name === "scoped-models" ? [] : [command]);
|
|
33
40
|
export function createPiShellEditor(options) {
|
|
34
41
|
ensureTheme();
|
|
35
42
|
const tui = createTuiFacade(options);
|
|
@@ -43,6 +50,7 @@ export function createPiShellEditor(options) {
|
|
|
43
50
|
}
|
|
44
51
|
const scrollInfo = { emitted: false, counter: undefined };
|
|
45
52
|
const selectListTheme = getSelectListTheme();
|
|
53
|
+
let tunnelSkills = [];
|
|
46
54
|
const EditorClass = options.keybindingProfile === "a1" && options.persistentHistory === true ? options.historyEditor : OwnedEditor;
|
|
47
55
|
const editor = new EditorClass(tui, {
|
|
48
56
|
borderColor: (value) => inputPresentation === undefined ? piTheme().fg("borderMuted", value) : inputPresentation.styleRule(value),
|
|
@@ -54,6 +62,12 @@ export function createPiShellEditor(options) {
|
|
|
54
62
|
scrollInfo.counter = /^ \((\d+\/\d+)\)$/u.exec(text)?.[1];
|
|
55
63
|
return selectListTheme.scrollInfo(text);
|
|
56
64
|
},
|
|
65
|
+
selectedText: text => {
|
|
66
|
+
// Rationale: a selected tunnel row keeps its description muted like the unselected rows (v2 behavior);
|
|
67
|
+
// the pinned list styles the whole selected row, so the split happens in the owned theme.
|
|
68
|
+
const row = tunnelSkills.length === 0 ? null : SELECTED_TUNNEL_ROW.exec(text);
|
|
69
|
+
return row === null ? selectListTheme.selectedText(text) : selectListTheme.selectedText(row[1]) + selectListTheme.description(row[2]);
|
|
70
|
+
},
|
|
57
71
|
},
|
|
58
72
|
}, keybindings, {
|
|
59
73
|
paddingX: PINNED_PI_LAYOUT.editorPaddingX,
|
|
@@ -64,6 +78,7 @@ export function createPiShellEditor(options) {
|
|
|
64
78
|
terminalRows: options.getRows,
|
|
65
79
|
getVisualLineCount: (width) => editorVisualLineCount(editor, width),
|
|
66
80
|
clearCommandSearchOnEscape: true,
|
|
81
|
+
commandTunnels: () => tunnelSkills.length === 0 ? [] : [SKILLS_COMMAND_NAME],
|
|
67
82
|
} : {}),
|
|
68
83
|
...(options.keybindingProfile === "a1" && options.promptPresentation !== undefined ? {
|
|
69
84
|
...(inputPresentation === undefined ? {} : { inputPresentation }),
|
|
@@ -105,12 +120,21 @@ export function createPiShellEditor(options) {
|
|
|
105
120
|
tui.requestRender();
|
|
106
121
|
};
|
|
107
122
|
let autocompleteProvider;
|
|
108
|
-
const
|
|
123
|
+
const builtInCatalog = options.keybindingProfile === "a1" ? OWNED_BUILTIN_SLASH_COMMANDS : PINNED_PI_BUILTIN_SLASH_COMMANDS;
|
|
124
|
+
const setAutocompleteCommands = (installed) => {
|
|
125
|
+
// Invariant: collapse is a bare-A1 presentation; the comparison profile installs the pinned list.
|
|
126
|
+
const collapsed = options.keybindingProfile === "a1" && options.skillsPresentation?.() === "collapse"
|
|
127
|
+
? collapseSkillCommands(installed)
|
|
128
|
+
: { commands: installed, skills: [] };
|
|
129
|
+
tunnelSkills = collapsed.skills;
|
|
130
|
+
const commands = collapsed.commands;
|
|
109
131
|
const additions = new Map(commands.map(command => [command.name, command]));
|
|
110
|
-
const builtInNames = new Set(
|
|
111
|
-
const builtIns =
|
|
112
|
-
|
|
113
|
-
|
|
132
|
+
const builtInNames = new Set(builtInCatalog.map(command => command.name));
|
|
133
|
+
const builtIns = builtInCatalog.map(command => autocompleteCommand(command, additions.get(command.name)));
|
|
134
|
+
// Invariant: the engine's built-in additions only decorate this profile's catalog; they never surface as resources.
|
|
135
|
+
const resources = commands.filter(command => command.source !== "builtin" && !builtInNames.has(command.name)).map(command => autocompleteCommand(command));
|
|
136
|
+
const combined = new CombinedAutocompleteProvider([...builtIns, ...resources], options.cwd ?? process.cwd());
|
|
137
|
+
autocompleteProvider = tunnelSkills.length === 0 ? combined : createSkillsTunnelProvider(combined, tunnelSkills);
|
|
114
138
|
editor.setAutocompleteProvider(autocompleteProvider);
|
|
115
139
|
};
|
|
116
140
|
setAutocompleteCommands(options.autocompleteCommands ?? []);
|
|
@@ -222,7 +222,8 @@ function expandedHeaderText(bindings) {
|
|
|
222
222
|
rawKeyHint("ctrl+k", "to delete to end"),
|
|
223
223
|
rawKeyHint(keys?.getKeys("app.thinking.cycle").join("/") ?? "shift+tab", "to cycle thinking level"),
|
|
224
224
|
rawKeyHint("ctrl+p/shift+ctrl+p", "to cycle models"),
|
|
225
|
-
|
|
225
|
+
// Invariant: only bare A1 supplies live bindings, and its unbound fallback is the unified `/models` command.
|
|
226
|
+
rawKeyHint(keys === undefined ? "ctrl+l" : keys.getKeys("app.model.select").join("/") || "/models", "to select model"),
|
|
226
227
|
rawKeyHint("ctrl+o", "to expand tools"),
|
|
227
228
|
rawKeyHint("ctrl+t", "to expand thinking"),
|
|
228
229
|
rawKeyHint("ctrl+g", "for external editor"),
|
|
@@ -40,4 +40,14 @@ export declare function renderPiShellCommandMessage(presentation: PiShellCommand
|
|
|
40
40
|
export declare function createPiShellSessionInfo(presentation: PiShellSessionInfoPresentation): PiShellComponentPort;
|
|
41
41
|
export declare function createPiShellCollapsedChangelog(): PiShellComponentPort;
|
|
42
42
|
export declare function createPiShellChangelog(markdown: string): PiShellComponentPort;
|
|
43
|
+
/** The changelog document rows the feed presenter shows, without its spacer, borders, and heading. */
|
|
44
|
+
export declare function renderPiShellChangelogLines(markdown: string, width: number): readonly string[];
|
|
45
|
+
/** What the hotkeys presenters render: the editor bindings and the extension shortcuts declared over them. */
|
|
46
|
+
export interface PiShellHotkeysPresentation {
|
|
47
|
+
readonly bindings?: KeybindingsConfig;
|
|
48
|
+
readonly getShortcuts?: NonNullable<PiShellExtensionRendererResolver["getShortcuts"]>;
|
|
49
|
+
readonly profile?: "pi" | "a1";
|
|
50
|
+
}
|
|
43
51
|
export declare function createPiShellHotkeys(bindings?: KeybindingsConfig, getShortcuts?: NonNullable<PiShellExtensionRendererResolver["getShortcuts"]>, profile?: "pi" | "a1"): PiShellComponentPort;
|
|
52
|
+
/** The keyboard-shortcut document rows the feed presenter shows, without its spacer, borders, and heading. */
|
|
53
|
+
export declare function renderPiShellHotkeysLines(presentation: PiShellHotkeysPresentation, width: number): readonly string[];
|
|
@@ -73,10 +73,18 @@ export function createPiShellChangelog(markdown) {
|
|
|
73
73
|
container.addChild(new DynamicBorder());
|
|
74
74
|
container.addChild(new Text(piTheme().bold(piTheme().fg("accent", "What's New")), 1, 0));
|
|
75
75
|
container.addChild(new Spacer(1));
|
|
76
|
-
container.addChild(
|
|
76
|
+
container.addChild(changelogMarkdown(markdown));
|
|
77
77
|
container.addChild(new DynamicBorder());
|
|
78
78
|
return componentPort(container);
|
|
79
79
|
}
|
|
80
|
+
/** The changelog document rows the feed presenter shows, without its spacer, borders, and heading. */
|
|
81
|
+
export function renderPiShellChangelogLines(markdown, width) {
|
|
82
|
+
ensureTheme();
|
|
83
|
+
return changelogMarkdown(markdown).render(width);
|
|
84
|
+
}
|
|
85
|
+
function changelogMarkdown(markdown) {
|
|
86
|
+
return new Markdown(markdown.trim() || "No changelog entries found.", 1, 1, getMarkdownTheme());
|
|
87
|
+
}
|
|
80
88
|
function shortcutDisplay(key) {
|
|
81
89
|
// Platform: pinned Pi labels Alt as Option on macOS, including extension shortcuts.
|
|
82
90
|
return key.split("/").map(binding => binding.split("+").map(part => {
|
|
@@ -86,6 +94,25 @@ function shortcutDisplay(key) {
|
|
|
86
94
|
}
|
|
87
95
|
export function createPiShellHotkeys(bindings, getShortcuts = () => [], profile = "pi") {
|
|
88
96
|
ensureTheme();
|
|
97
|
+
const markdown = hotkeysMarkdown(bindings, getShortcuts, profile);
|
|
98
|
+
const container = new Container();
|
|
99
|
+
container.addChild(new Spacer(1));
|
|
100
|
+
container.addChild(new DynamicBorder());
|
|
101
|
+
container.addChild(new Text(piTheme().bold(piTheme().fg("accent", "Keyboard Shortcuts")), 1, 0));
|
|
102
|
+
container.addChild(new Spacer(1));
|
|
103
|
+
container.addChild(hotkeysMarkdownComponent(markdown));
|
|
104
|
+
container.addChild(new DynamicBorder());
|
|
105
|
+
return componentPort(container);
|
|
106
|
+
}
|
|
107
|
+
/** The keyboard-shortcut document rows the feed presenter shows, without its spacer, borders, and heading. */
|
|
108
|
+
export function renderPiShellHotkeysLines(presentation, width) {
|
|
109
|
+
ensureTheme();
|
|
110
|
+
return hotkeysMarkdownComponent(hotkeysMarkdown(presentation.bindings, presentation.getShortcuts ?? (() => []), presentation.profile ?? "pi")).render(width);
|
|
111
|
+
}
|
|
112
|
+
function hotkeysMarkdownComponent(markdown) {
|
|
113
|
+
return new Markdown(markdown, 1, 1, getMarkdownTheme());
|
|
114
|
+
}
|
|
115
|
+
function hotkeysMarkdown(bindings, getShortcuts, profile) {
|
|
89
116
|
const keys = profile === "a1" ? KeybindingsManager.fromOwnedBindings(bindings) : new KeybindingsManager(bindings);
|
|
90
117
|
// Compatibility: the owned viewport consumes these physical chords before editor actions.
|
|
91
118
|
const display = (action) => keys.getKeys(action)
|
|
@@ -94,21 +121,14 @@ export function createPiShellHotkeys(bindings, getShortcuts = () => [], profile
|
|
|
94
121
|
const row = (actions, description) => `| ${actions.map(action => {
|
|
95
122
|
const label = display(action);
|
|
96
123
|
return profile === "a1" && label.length === 0
|
|
97
|
-
? action === "app.model.select" ? "Unbound (`/
|
|
124
|
+
? action === "app.model.select" ? "Unbound (`/models`)" : "Unbound"
|
|
98
125
|
: `\`${label}\``;
|
|
99
126
|
}).join(" / ")} | ${description} |`;
|
|
100
|
-
let markdown = ["**Navigation**", "| Key | Action |", "|-----|--------|", row(["tui.editor.cursorUp", "tui.editor.cursorDown", "tui.editor.cursorLeft", "tui.editor.cursorRight"], "Move cursor / browse history"), row(["tui.editor.cursorWordLeft", "tui.editor.cursorWordRight"], "Move by word"), row(["tui.editor.cursorLineStart"], profile === "a1" ? "Start of prompt line" : "Start of line"), row(["tui.editor.cursorLineEnd"], profile === "a1" ? "End of prompt line" : "End of line"), ...(profile === "a1" ? ["| `Ctrl+Home` | Start of content |", "| `Ctrl+End` | End of content / follow output |"] : []), row(["tui.editor.jumpForward"], "Jump forward to character"), row(["tui.editor.jumpBackward"], "Jump backward to character"), row(["tui.editor.pageUp", "tui.editor.pageDown"], "Scroll by page"), "", "**Editing**", "| Key | Action |", "|-----|--------|", row(["tui.input.submit"], "Send message"), row(["tui.input.newLine"], `New line${process.platform === "win32" ? " (Ctrl+Enter on Windows Terminal)" : ""}`), row(["tui.editor.deleteWordBackward"], "Delete word backwards"), row(["tui.editor.deleteWordForward"], "Delete word forwards"), row(["tui.editor.deleteToLineStart"], "Delete to start of line"), row(["tui.editor.deleteToLineEnd"], "Delete to end of line"), row(["tui.editor.yank"], "Paste the most-recently-deleted text"), row(["tui.editor.yankPop"], "Cycle through the deleted text after pasting"), row(["tui.editor.undo"], "Undo"), "", "**Other**", "| Key | Action |", "|-----|--------|", row(["tui.input.tab"], "Path completion / accept autocomplete"), row(["app.interrupt"], "Cancel autocomplete / abort streaming"), row(["app.clear"], "Clear editor (first) / exit (second)"), row(["app.exit"], "Exit (when editor is empty)"), row(["app.suspend"], "Suspend to background"), row(["app.thinking.cycle"], "Cycle thinking level"), row(["app.model.cycleForward", "app.model.cycleBackward"], "Cycle models"), row(["app.model.select"], "Open model selector"), row(["app.tools.expand"], "Toggle tool output expansion"), row(["app.thinking.toggle"], "Toggle thinking block visibility"), row(["app.editor.external"], "Edit message in external editor"), row(["app.message.copy"], "Copy last assistant message"), row(["app.message.followUp"], "Queue follow-up message"), row(["app.message.dequeue"], "Restore queued messages"), row(["app.clipboard.pasteImage"], "Paste image or text from clipboard"), "| `/` | Slash commands |", "| `!` | Run bash command |", "| `!!` | Run bash command (excluded from context) |"].join("\n");
|
|
127
|
+
let markdown = ["**Navigation**", "| Key | Action |", "|-----|--------|", row(["tui.editor.cursorUp", "tui.editor.cursorDown", "tui.editor.cursorLeft", "tui.editor.cursorRight"], "Move cursor / browse history"), row(["tui.editor.cursorWordLeft", "tui.editor.cursorWordRight"], "Move by word"), row(["tui.editor.cursorLineStart"], profile === "a1" ? "Start of prompt line" : "Start of line"), row(["tui.editor.cursorLineEnd"], profile === "a1" ? "End of prompt line" : "End of line"), ...(profile === "a1" ? ["| `Ctrl+Home` | Start of content |", "| `Ctrl+End` | End of content / follow output |"] : []), row(["tui.editor.jumpForward"], "Jump forward to character"), row(["tui.editor.jumpBackward"], "Jump backward to character"), row(["tui.editor.pageUp", "tui.editor.pageDown"], "Scroll by page"), "", "**Editing**", "| Key | Action |", "|-----|--------|", row(["tui.input.submit"], "Send message"), row(["tui.input.newLine"], `New line${process.platform === "win32" ? " (Ctrl+Enter on Windows Terminal)" : ""}`), row(["tui.editor.deleteWordBackward"], "Delete word backwards"), row(["tui.editor.deleteWordForward"], "Delete word forwards"), row(["tui.editor.deleteToLineStart"], "Delete to start of line"), row(["tui.editor.deleteToLineEnd"], "Delete to end of line"), row(["tui.editor.yank"], "Paste the most-recently-deleted text"), row(["tui.editor.yankPop"], "Cycle through the deleted text after pasting"), row(["tui.editor.undo"], "Undo"), "", "**Other**", "| Key | Action |", "|-----|--------|", row(["tui.input.tab"], "Path completion / accept autocomplete"), row(["app.interrupt"], "Cancel autocomplete / abort streaming"), row(["app.clear"], "Clear editor (first) / exit (second)"), row(["app.exit"], "Exit (when editor is empty)"), row(["app.suspend"], "Suspend to background"), row(["app.thinking.cycle"], "Cycle thinking level"), row(["app.model.cycleForward", "app.model.cycleBackward"], "Cycle models"), row(["app.model.select"], profile === "a1" ? "Open the Models dialog" : "Open model selector"), row(["app.tools.expand"], "Toggle tool output expansion"), row(["app.thinking.toggle"], "Toggle thinking block visibility"), row(["app.editor.external"], "Edit message in external editor"), row(["app.message.copy"], "Copy last assistant message"), row(["app.message.followUp"], "Queue follow-up message"), row(["app.message.dequeue"], "Restore queued messages"), row(["app.clipboard.pasteImage"], "Paste image or text from clipboard"), "| `/` | Slash commands |", "| `!` | Run bash command |", "| `!!` | Run bash command (excluded from context) |", ...(profile === "a1" ? ["", "**Models dialog**", "| Key | Action |", "|-----|--------|", "| `Space` | Toggle the selected model in the cycling scope |", "| `Tab` | Switch the all/scoped filter |", row(["app.models.save"], "Save the scope to settings"), row(["app.models.enableAll"], "Scope every listed model"), row(["app.models.clearAll"], "Clear the listed models from the scope"), row(["app.models.toggleProvider"], "Toggle the selected model's provider"), row(["app.models.reorderUp", "app.models.reorderDown"], "Reorder the cycling scope")] : [])].join("\n");
|
|
101
128
|
const shortcuts = getShortcuts(bindings ?? keys.getEffectiveConfig());
|
|
102
129
|
if (shortcuts.length > 0) {
|
|
103
130
|
markdown += "\n\n**Extensions**\n| Key | Action |\n|-----|--------|\n";
|
|
104
131
|
markdown += shortcuts.map(shortcut => `| \`${shortcutDisplay(shortcut.key)}\` | ${shortcut.description} |`).join("\n");
|
|
105
132
|
}
|
|
106
|
-
|
|
107
|
-
container.addChild(new Spacer(1));
|
|
108
|
-
container.addChild(new DynamicBorder());
|
|
109
|
-
container.addChild(new Text(piTheme().bold(piTheme().fg("accent", "Keyboard Shortcuts")), 1, 0));
|
|
110
|
-
container.addChild(new Spacer(1));
|
|
111
|
-
container.addChild(new Markdown(markdown, 1, 1, getMarkdownTheme()));
|
|
112
|
-
container.addChild(new DynamicBorder());
|
|
113
|
-
return componentPort(container);
|
|
133
|
+
return markdown;
|
|
114
134
|
}
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { ModelSelectorComponent, type SessionInfo, type SettingsCallbacks, type SettingsConfig } from "../startup-public.js";
|
|
2
2
|
import type { OwnedUiDialog } from "../../../contracts/owned-ui/index.js";
|
|
3
|
+
import { type ModelsDialogCallbacks, type ModelsDialogConfig } from "./models-dialog.js";
|
|
3
4
|
import { type TrustDecision, type TrustOption, type TrustUpdate } from "./upstream/components/trust-selector.js";
|
|
4
5
|
import { type PiShellComponentPort, type PiShellEditorOptions, type PiShellSelectorOption, type PiShellSelectorOptions } from "./shell-shared-facade.js";
|
|
6
|
+
export { createPiShellSkillsSelector, type PiShellSkillsSelectorOptions } from "./skills-dialog.js";
|
|
5
7
|
export declare function createPiShellSelector(options: PiShellSelectorOptions): PiShellComponentPort;
|
|
6
8
|
export interface PiShellSettingsSelectorOptions {
|
|
7
9
|
readonly config: SettingsConfig;
|
|
@@ -45,6 +47,15 @@ export interface PiShellScopedModelsSelectorPort extends PiShellComponentPort {
|
|
|
45
47
|
setRefreshStatus(message: string, kind: "muted" | "success" | "warning"): void;
|
|
46
48
|
}
|
|
47
49
|
export declare function createPiShellScopedModelsSelector(options: PiShellScopedModelsSelectorOptions): PiShellScopedModelsSelectorPort;
|
|
50
|
+
export interface PiShellModelsDialogOptions extends ModelsDialogConfig, ModelsDialogCallbacks {
|
|
51
|
+
}
|
|
52
|
+
export interface PiShellModelsDialogPort extends PiShellComponentPort {
|
|
53
|
+
updateModels(models: readonly PiShellScopedModelDescriptor[]): void;
|
|
54
|
+
setRefreshStatus(message: string, kind: "muted" | "success" | "warning"): void;
|
|
55
|
+
readonly dirty: () => boolean;
|
|
56
|
+
}
|
|
57
|
+
/** The bare-A1 unified Models dialog behind the owned component boundary; the pinned selectors above stay for `a1 pi`. */
|
|
58
|
+
export declare function createPiShellModelsDialog(options: PiShellModelsDialogOptions): PiShellModelsDialogPort;
|
|
48
59
|
export declare function createPiShellTrustSelector(options: {
|
|
49
60
|
readonly cwd: string;
|
|
50
61
|
readonly savedDecision: TrustDecision | null;
|
|
@@ -120,4 +131,3 @@ export declare function createPiShellDialog(dialog: OwnedUiDialog, handlers?: {
|
|
|
120
131
|
readonly onSelect?: (id: string) => void;
|
|
121
132
|
readonly onCancel?: () => void;
|
|
122
133
|
}): PiShellComponentPort;
|
|
123
|
-
export {};
|
|
@@ -5,9 +5,11 @@ import { SessionSelectorComponent, } from "./upstream/components/session-selecto
|
|
|
5
5
|
import { TreeSelectorComponent, } from "./upstream/components/tree-selector.js";
|
|
6
6
|
import { Box, Container, SelectList, Spacer, Text, } from "@earendil-works/pi-tui";
|
|
7
7
|
import { ScopedModelsSelectorComponent, } from "./upstream/components/scoped-models-selector.js";
|
|
8
|
+
import { ModelsDialogComponent, } from "./models-dialog.js";
|
|
8
9
|
import { TrustSelectorComponent, } from "./upstream/components/trust-selector.js";
|
|
9
10
|
import { PINNED_PI_LAYOUT, piTheme, } from "./theme.js";
|
|
10
11
|
import { componentFromPort, componentPort, createTuiFacade, ensureTheme, isRecord, } from "./shell-shared-facade.js";
|
|
12
|
+
export { createPiShellSkillsSelector } from "./skills-dialog.js";
|
|
11
13
|
export function createPiShellSelector(options) {
|
|
12
14
|
ensureTheme();
|
|
13
15
|
const items = options.options.map(toSelectItem);
|
|
@@ -103,6 +105,17 @@ export function createPiShellScopedModelsSelector(options) {
|
|
|
103
105
|
setRefreshStatus: (message, kind) => selector.setRefreshStatus(message, kind),
|
|
104
106
|
};
|
|
105
107
|
}
|
|
108
|
+
/** The bare-A1 unified Models dialog behind the owned component boundary; the pinned selectors above stay for `a1 pi`. */
|
|
109
|
+
export function createPiShellModelsDialog(options) {
|
|
110
|
+
ensureTheme();
|
|
111
|
+
const dialog = new ModelsDialogComponent(options, options);
|
|
112
|
+
return {
|
|
113
|
+
...componentPort(dialog),
|
|
114
|
+
updateModels: models => dialog.updateModels(models),
|
|
115
|
+
setRefreshStatus: (message, kind) => dialog.setRefreshStatus(message, kind),
|
|
116
|
+
dirty: () => dialog.dirty,
|
|
117
|
+
};
|
|
118
|
+
}
|
|
106
119
|
export function createPiShellTrustSelector(options) {
|
|
107
120
|
ensureTheme();
|
|
108
121
|
return componentPort(new TrustSelectorComponent(options));
|
|
@@ -73,6 +73,8 @@ export interface PiShellAutocompleteCommand {
|
|
|
73
73
|
readonly description?: string;
|
|
74
74
|
readonly argumentHint?: string;
|
|
75
75
|
readonly argumentOptions?: readonly PiShellSelectorOption[];
|
|
76
|
+
/** Built-in entries decorate the active profile's catalog; everything else is a discovered resource. */
|
|
77
|
+
readonly source?: "builtin" | "prompt" | "skill" | "extension";
|
|
76
78
|
}
|
|
77
79
|
export interface PiShellViewComponentPort extends PiShellComponentPort {
|
|
78
80
|
update(view: OwnedUiSessionViewModel): void;
|
|
@@ -188,6 +190,12 @@ export interface PiShellEditorOptions {
|
|
|
188
190
|
readonly cwd?: string;
|
|
189
191
|
readonly agentDir?: string;
|
|
190
192
|
readonly autocompleteCommands?: readonly PiShellAutocompleteCommand[];
|
|
193
|
+
/**
|
|
194
|
+
* Bare A1's skills presentation, read at every command-list installation. `collapse` replaces the
|
|
195
|
+
* `skill:<name>` entries with one `skills` command and its tunnel; absent or `expand` keeps the
|
|
196
|
+
* pinned per-skill entries. Comparison profiles ignore it.
|
|
197
|
+
*/
|
|
198
|
+
readonly skillsPresentation?: () => "collapse" | "expand";
|
|
191
199
|
readonly promptPresentation?: {
|
|
192
200
|
readonly input: PiShellPromptInputPresentation;
|
|
193
201
|
readonly styleSuggestion: (text: string) => string;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import type { AutocompleteItem, AutocompleteProvider } from "@earendil-works/pi-tui";
|
|
2
|
+
import type { PiShellAutocompleteCommand } from "./shell-shared-facade.js";
|
|
3
|
+
/** The collapsed bare-A1 replacement for the per-skill command listing. */
|
|
4
|
+
export declare const SKILLS_COMMAND_NAME = "skills";
|
|
5
|
+
export declare const SKILLS_COMMAND_DESCRIPTION = "Browse, search, and apply a skill";
|
|
6
|
+
/** Pi's own command prefix; the engine expands `/skill:<name>` regardless of how the menu presents it. */
|
|
7
|
+
export declare const SKILL_COMMAND_PREFIX = "skill:";
|
|
8
|
+
export interface PiShellSkillSummary {
|
|
9
|
+
readonly name: string;
|
|
10
|
+
readonly description: string;
|
|
11
|
+
}
|
|
12
|
+
export interface PiShellCollapsedSkillCommands {
|
|
13
|
+
readonly commands: readonly PiShellAutocompleteCommand[];
|
|
14
|
+
/** The skills withheld from the top-level menu, in the engine's discovery order. */
|
|
15
|
+
readonly skills: readonly PiShellSkillSummary[];
|
|
16
|
+
}
|
|
17
|
+
/** One line of description, as the v2 extension shows it beside a row. */
|
|
18
|
+
export declare function oneLineSkillDescription(description: string): string;
|
|
19
|
+
/** The skills the engine registered as `skill:<name>` commands, in its order. */
|
|
20
|
+
export declare function skillsFromCommands(commands: readonly PiShellAutocompleteCommand[]): readonly PiShellSkillSummary[];
|
|
21
|
+
/**
|
|
22
|
+
* Replace every `skill:<name>` entry with one `skills` command whose argument completions are the
|
|
23
|
+
* skill names. A list without skill entries (registration disabled, or no skills) is returned
|
|
24
|
+
* unchanged with no `skills` command, so the collapsed presentation never invents a command.
|
|
25
|
+
*/
|
|
26
|
+
export declare function collapseSkillCommands(commands: readonly PiShellAutocompleteCommand[]): PiShellCollapsedSkillCommands;
|
|
27
|
+
/** Case-insensitive substring match on the name or description; a leading `skill:`/`skills:` in the query is ignored for the name. */
|
|
28
|
+
export declare function skillMatchesQuery(skill: PiShellSkillSummary, query: string): boolean;
|
|
29
|
+
/** Resolve a `/skills` argument token to a skill, accepting the name with or without `skill:`. */
|
|
30
|
+
export declare function findSkillByArgument(skills: readonly PiShellSkillSummary[], token: string): PiShellSkillSummary | undefined;
|
|
31
|
+
/** The `/skill:<name>` prompt the engine expands, with any arguments after one space. */
|
|
32
|
+
export declare function skillPrompt(name: string, args?: string): string;
|
|
33
|
+
/**
|
|
34
|
+
* Rewrite a submitted tunnel form `/skills:<name> rest` to `/skill:<name> rest`; anything else is
|
|
35
|
+
* returned unchanged so ordinary prompts, `/skills <name>`, and `/skill:` input keep their text.
|
|
36
|
+
*/
|
|
37
|
+
export declare function rewriteSkillsTunnelSubmission(text: string): string;
|
|
38
|
+
/** The tunnel query when single-line content before the cursor is exactly `/skills:<query>`. */
|
|
39
|
+
export declare function skillsTunnelQuery(lines: readonly string[], cursorLine: number, cursorCol: number): string | null;
|
|
40
|
+
export declare function skillsTunnelItems(skills: readonly PiShellSkillSummary[], query: string): AutocompleteItem[];
|
|
41
|
+
/**
|
|
42
|
+
* Wrap the pinned combined provider with the `skills` tunnel: `/skills:<query>` lists matching
|
|
43
|
+
* skills as `skills:<name>` rows and shows nothing when none match; every other request, the
|
|
44
|
+
* completion application, and the file-completion decision are delegated unchanged so extension
|
|
45
|
+
* wrappers registered over this provider see one ordinary provider.
|
|
46
|
+
*/
|
|
47
|
+
export declare function createSkillsTunnelProvider(base: AutocompleteProvider, skills: readonly PiShellSkillSummary[]): AutocompleteProvider;
|