@timurproko/a1 0.1.8-dev.508 → 0.1.8-dev.510
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 +44 -1
- package/bin/activate.js +8 -0
- package/bin/sync-pi-tui-proxy.js +7 -0
- 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/foundation/release/index.d.ts +2 -0
- package/dist/foundation/release/index.js +1 -0
- package/dist/foundation/release/update-activation.d.ts +72 -0
- package/dist/foundation/release/update-activation.js +204 -0
- package/dist/foundation/release/update.d.ts +5 -10
- package/dist/foundation/release/update.js +39 -38
- 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 +112 -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/runtime-payload-inventory.json +1 -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/internal-naming.md +2 -0
- package/docs/architecture/prompt-suggestions.md +1 -1
- package/docs/architecture/ui-reference-provenance.md +1 -0
- package/docs/features/modal-content-interaction.md +3 -2
- package/package.json +4 -1
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/** The collapsed bare-A1 replacement for the per-skill command listing. */
|
|
2
|
+
export const SKILLS_COMMAND_NAME = "skills";
|
|
3
|
+
export const SKILLS_COMMAND_DESCRIPTION = "Browse, search, and apply a skill";
|
|
4
|
+
/** Pi's own command prefix; the engine expands `/skill:<name>` regardless of how the menu presents it. */
|
|
5
|
+
export const SKILL_COMMAND_PREFIX = "skill:";
|
|
6
|
+
const SKILLS_TUNNEL_PREFIX = `${SKILLS_COMMAND_NAME}:`;
|
|
7
|
+
/** The tunnel form follows v2: a name without whitespace right after the colon. */
|
|
8
|
+
const SKILLS_TUNNEL_INPUT = /^\/skills:(\S*)$/u;
|
|
9
|
+
const SKILLS_TUNNEL_SUBMISSION = /^\/skills:([A-Za-z0-9][A-Za-z0-9_-]*)(\s[\s\S]*)?$/u;
|
|
10
|
+
/** One line of description, as the v2 extension shows it beside a row. */
|
|
11
|
+
export function oneLineSkillDescription(description) {
|
|
12
|
+
return description.replace(/\s+/gu, " ").trim();
|
|
13
|
+
}
|
|
14
|
+
/** The skills the engine registered as `skill:<name>` commands, in its order. */
|
|
15
|
+
export function skillsFromCommands(commands) {
|
|
16
|
+
return commands.flatMap(command => command.name.startsWith(SKILL_COMMAND_PREFIX) && command.name.length > SKILL_COMMAND_PREFIX.length
|
|
17
|
+
? [{ name: command.name.slice(SKILL_COMMAND_PREFIX.length), description: oneLineSkillDescription(command.description ?? "") }]
|
|
18
|
+
: []);
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Replace every `skill:<name>` entry with one `skills` command whose argument completions are the
|
|
22
|
+
* skill names. A list without skill entries (registration disabled, or no skills) is returned
|
|
23
|
+
* unchanged with no `skills` command, so the collapsed presentation never invents a command.
|
|
24
|
+
*/
|
|
25
|
+
export function collapseSkillCommands(commands) {
|
|
26
|
+
const skills = skillsFromCommands(commands);
|
|
27
|
+
if (skills.length === 0)
|
|
28
|
+
return { commands, skills };
|
|
29
|
+
const collapsed = [];
|
|
30
|
+
let inserted = false;
|
|
31
|
+
for (const command of commands) {
|
|
32
|
+
if (!command.name.startsWith(SKILL_COMMAND_PREFIX)) {
|
|
33
|
+
collapsed.push(command);
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
if (inserted)
|
|
37
|
+
continue;
|
|
38
|
+
inserted = true;
|
|
39
|
+
collapsed.push({
|
|
40
|
+
name: SKILLS_COMMAND_NAME,
|
|
41
|
+
description: SKILLS_COMMAND_DESCRIPTION,
|
|
42
|
+
argumentOptions: skills.map(skill => ({
|
|
43
|
+
id: skill.name,
|
|
44
|
+
label: skill.name,
|
|
45
|
+
...(skill.description.length === 0 ? {} : { description: skill.description }),
|
|
46
|
+
})),
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
return { commands: collapsed, skills };
|
|
50
|
+
}
|
|
51
|
+
/** Case-insensitive substring match on the name or description; a leading `skill:`/`skills:` in the query is ignored for the name. */
|
|
52
|
+
export function skillMatchesQuery(skill, query) {
|
|
53
|
+
const normalized = query.trim().toLowerCase();
|
|
54
|
+
if (normalized.length === 0)
|
|
55
|
+
return true;
|
|
56
|
+
const withoutPrefix = normalized.startsWith(SKILLS_TUNNEL_PREFIX)
|
|
57
|
+
? normalized.slice(SKILLS_TUNNEL_PREFIX.length)
|
|
58
|
+
: normalized.startsWith(SKILL_COMMAND_PREFIX) ? normalized.slice(SKILL_COMMAND_PREFIX.length) : normalized;
|
|
59
|
+
return skill.name.toLowerCase().includes(withoutPrefix)
|
|
60
|
+
|| `${SKILL_COMMAND_PREFIX}${skill.name}`.toLowerCase().includes(normalized)
|
|
61
|
+
|| skill.description.toLowerCase().includes(normalized);
|
|
62
|
+
}
|
|
63
|
+
/** Resolve a `/skills` argument token to a skill, accepting the name with or without `skill:`. */
|
|
64
|
+
export function findSkillByArgument(skills, token) {
|
|
65
|
+
const name = token.startsWith(SKILL_COMMAND_PREFIX) ? token.slice(SKILL_COMMAND_PREFIX.length) : token;
|
|
66
|
+
return skills.find(skill => skill.name === name);
|
|
67
|
+
}
|
|
68
|
+
/** The `/skill:<name>` prompt the engine expands, with any arguments after one space. */
|
|
69
|
+
export function skillPrompt(name, args = "") {
|
|
70
|
+
const trimmed = args.trim();
|
|
71
|
+
return trimmed.length === 0 ? `/${SKILL_COMMAND_PREFIX}${name}` : `/${SKILL_COMMAND_PREFIX}${name} ${trimmed}`;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Rewrite a submitted tunnel form `/skills:<name> rest` to `/skill:<name> rest`; anything else is
|
|
75
|
+
* returned unchanged so ordinary prompts, `/skills <name>`, and `/skill:` input keep their text.
|
|
76
|
+
*/
|
|
77
|
+
export function rewriteSkillsTunnelSubmission(text) {
|
|
78
|
+
const match = SKILLS_TUNNEL_SUBMISSION.exec(text);
|
|
79
|
+
if (match === null)
|
|
80
|
+
return text;
|
|
81
|
+
return `/${SKILL_COMMAND_PREFIX}${match[1]}${match[2] ?? ""}`;
|
|
82
|
+
}
|
|
83
|
+
/** The tunnel query when single-line content before the cursor is exactly `/skills:<query>`. */
|
|
84
|
+
export function skillsTunnelQuery(lines, cursorLine, cursorCol) {
|
|
85
|
+
if (cursorLine !== 0 || lines.length !== 1)
|
|
86
|
+
return null;
|
|
87
|
+
const match = SKILLS_TUNNEL_INPUT.exec((lines[0] ?? "").slice(0, cursorCol));
|
|
88
|
+
return match === null ? null : match[1] ?? "";
|
|
89
|
+
}
|
|
90
|
+
export function skillsTunnelItems(skills, query) {
|
|
91
|
+
return skills.filter(skill => skillMatchesQuery(skill, query)).map(skill => ({
|
|
92
|
+
value: `${SKILLS_TUNNEL_PREFIX}${skill.name}`,
|
|
93
|
+
label: `${SKILLS_TUNNEL_PREFIX}${skill.name}`,
|
|
94
|
+
...(skill.description.length === 0 ? {} : { description: skill.description }),
|
|
95
|
+
}));
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Wrap the pinned combined provider with the `skills` tunnel: `/skills:<query>` lists matching
|
|
99
|
+
* skills as `skills:<name>` rows and shows nothing when none match; every other request, the
|
|
100
|
+
* completion application, and the file-completion decision are delegated unchanged so extension
|
|
101
|
+
* wrappers registered over this provider see one ordinary provider.
|
|
102
|
+
*/
|
|
103
|
+
export function createSkillsTunnelProvider(base, skills) {
|
|
104
|
+
return {
|
|
105
|
+
...(base.triggerCharacters === undefined ? {} : { triggerCharacters: base.triggerCharacters }),
|
|
106
|
+
async getSuggestions(lines, cursorLine, cursorCol, options) {
|
|
107
|
+
const query = skillsTunnelQuery(lines, cursorLine, cursorCol);
|
|
108
|
+
if (query === null)
|
|
109
|
+
return base.getSuggestions(lines, cursorLine, cursorCol, options);
|
|
110
|
+
const items = skillsTunnelItems(skills, query);
|
|
111
|
+
if (options.signal.aborted || items.length === 0)
|
|
112
|
+
return null;
|
|
113
|
+
return { prefix: (lines[cursorLine] ?? "").slice(0, cursorCol), items };
|
|
114
|
+
},
|
|
115
|
+
applyCompletion: (lines, cursorLine, cursorCol, item, prefix) => base.applyCompletion(lines, cursorLine, cursorCol, item, prefix),
|
|
116
|
+
shouldTriggerFileCompletion: (lines, cursorLine, cursorCol) => base.shouldTriggerFileCompletion?.(lines, cursorLine, cursorCol) ?? true,
|
|
117
|
+
};
|
|
118
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { type PiShellComponentPort } from "./shell-shared-facade.js";
|
|
2
|
+
import { type PiShellSkillSummary } from "./skills-command.js";
|
|
3
|
+
export interface PiShellSkillsSelectorOptions {
|
|
4
|
+
/** The skills the engine registered, in its discovery order. */
|
|
5
|
+
readonly skills: readonly PiShellSkillSummary[];
|
|
6
|
+
readonly onSelect: (name: string) => void;
|
|
7
|
+
readonly onCancel: () => void;
|
|
8
|
+
}
|
|
9
|
+
export declare function createPiShellSkillsSelector(options: PiShellSkillsSelectorOptions): PiShellComponentPort;
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { DynamicBorder, keyHint, rawKeyHint } from "../startup-public.js";
|
|
2
|
+
import { Container, getKeybindings, Input, Spacer, Text } from "@earendil-works/pi-tui";
|
|
3
|
+
import { PINNED_PI_LAYOUT, piTheme } from "./theme.js";
|
|
4
|
+
import { componentPort, ensureTheme } from "./shell-shared-facade.js";
|
|
5
|
+
import { SKILL_COMMAND_PREFIX, skillMatchesQuery } from "./skills-command.js";
|
|
6
|
+
/**
|
|
7
|
+
* The A1-owned searchable Skills dialog. Its composition is the model selector's (border, spacer,
|
|
8
|
+
* search input, spacer, list, spacer, hint footer, border) built from public pi-tui components; its
|
|
9
|
+
* content follows the v2 skills extension: an accent bold title, `skill:<name>` rows, the selected
|
|
10
|
+
* skill's one-line description under the rows, the pinned scroll counter on overflow, and the two
|
|
11
|
+
* empty states. Enter applies the selected skill with no arguments; the query is never appended.
|
|
12
|
+
*/
|
|
13
|
+
class SkillsSelectorComponent extends Container {
|
|
14
|
+
#searchInput = new Input();
|
|
15
|
+
#listContainer = new Container();
|
|
16
|
+
#skills;
|
|
17
|
+
#onSelect;
|
|
18
|
+
#onCancel;
|
|
19
|
+
#filtered;
|
|
20
|
+
#selectedIndex = 0;
|
|
21
|
+
#focused = false;
|
|
22
|
+
constructor(options) {
|
|
23
|
+
super();
|
|
24
|
+
this.#skills = options.skills;
|
|
25
|
+
this.#filtered = options.skills;
|
|
26
|
+
this.#onSelect = options.onSelect;
|
|
27
|
+
this.#onCancel = options.onCancel;
|
|
28
|
+
this.#searchInput.onSubmit = () => this.#selectCurrent();
|
|
29
|
+
this.addChild(new DynamicBorder());
|
|
30
|
+
this.addChild(new Spacer(1));
|
|
31
|
+
this.addChild(new Text(piTheme().fg("accent", piTheme().bold("Skills")), 0, 0));
|
|
32
|
+
this.addChild(new Spacer(1));
|
|
33
|
+
this.addChild(this.#searchInput);
|
|
34
|
+
this.addChild(new Spacer(1));
|
|
35
|
+
this.addChild(this.#listContainer);
|
|
36
|
+
this.addChild(new Spacer(1));
|
|
37
|
+
this.addChild(new Text(` ${rawKeyHint("↑↓", "navigate")} ${keyHint("tui.select.confirm", "select")} ${keyHint("tui.select.cancel", "cancel")}`, 0, 0));
|
|
38
|
+
this.addChild(new DynamicBorder());
|
|
39
|
+
this.#updateList();
|
|
40
|
+
}
|
|
41
|
+
// Protocol: focus propagates to the search input so the terminal cursor follows the query.
|
|
42
|
+
get focused() { return this.#focused; }
|
|
43
|
+
set focused(value) {
|
|
44
|
+
this.#focused = value;
|
|
45
|
+
this.#searchInput.focused = value;
|
|
46
|
+
}
|
|
47
|
+
handleInput(data) {
|
|
48
|
+
const keybindings = getKeybindings();
|
|
49
|
+
if (keybindings.matches(data, "tui.select.up")) {
|
|
50
|
+
if (this.#filtered.length === 0)
|
|
51
|
+
return;
|
|
52
|
+
this.#selectedIndex = this.#selectedIndex === 0 ? this.#filtered.length - 1 : this.#selectedIndex - 1;
|
|
53
|
+
this.#updateList();
|
|
54
|
+
}
|
|
55
|
+
else if (keybindings.matches(data, "tui.select.down")) {
|
|
56
|
+
if (this.#filtered.length === 0)
|
|
57
|
+
return;
|
|
58
|
+
this.#selectedIndex = this.#selectedIndex === this.#filtered.length - 1 ? 0 : this.#selectedIndex + 1;
|
|
59
|
+
this.#updateList();
|
|
60
|
+
}
|
|
61
|
+
else if (keybindings.matches(data, "tui.select.confirm")) {
|
|
62
|
+
this.#selectCurrent();
|
|
63
|
+
}
|
|
64
|
+
else if (keybindings.matches(data, "tui.select.cancel")) {
|
|
65
|
+
this.#onCancel();
|
|
66
|
+
}
|
|
67
|
+
else {
|
|
68
|
+
const before = this.#searchInput.getValue();
|
|
69
|
+
this.#searchInput.handleInput(data);
|
|
70
|
+
const query = this.#searchInput.getValue();
|
|
71
|
+
if (query === before)
|
|
72
|
+
return;
|
|
73
|
+
this.#filtered = this.#skills.filter(skill => skillMatchesQuery(skill, query));
|
|
74
|
+
this.#selectedIndex = 0;
|
|
75
|
+
this.#updateList();
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
#selectCurrent() {
|
|
79
|
+
const selected = this.#filtered[this.#selectedIndex];
|
|
80
|
+
if (selected !== undefined)
|
|
81
|
+
this.#onSelect(selected.name);
|
|
82
|
+
}
|
|
83
|
+
#updateList() {
|
|
84
|
+
this.#listContainer.clear();
|
|
85
|
+
const theme = piTheme();
|
|
86
|
+
if (this.#filtered.length === 0) {
|
|
87
|
+
this.#listContainer.addChild(new Text(theme.fg("muted", this.#skills.length === 0 ? " No skills yet" : " No matching skills"), 0, 0));
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
const maxVisible = PINNED_PI_LAYOUT.selectorMaxVisible;
|
|
91
|
+
const start = Math.max(0, Math.min(this.#selectedIndex - Math.floor(maxVisible / 2), this.#filtered.length - maxVisible));
|
|
92
|
+
const end = Math.min(start + maxVisible, this.#filtered.length);
|
|
93
|
+
for (let index = start; index < end; index++) {
|
|
94
|
+
const skill = this.#filtered[index];
|
|
95
|
+
const selected = index === this.#selectedIndex;
|
|
96
|
+
const label = `${SKILL_COMMAND_PREFIX}${skill.name}`;
|
|
97
|
+
this.#listContainer.addChild(new Text(`${selected ? theme.fg("accent", "→ ") : " "}${selected ? theme.fg("accent", label) : label}`, 0, 0));
|
|
98
|
+
}
|
|
99
|
+
if (start > 0 || end < this.#filtered.length) {
|
|
100
|
+
this.#listContainer.addChild(new Text(theme.fg("muted", ` (${this.#selectedIndex + 1}/${this.#filtered.length})`), 0, 0));
|
|
101
|
+
}
|
|
102
|
+
const description = this.#filtered[this.#selectedIndex]?.description ?? "";
|
|
103
|
+
if (description.length > 0) {
|
|
104
|
+
this.#listContainer.addChild(new Spacer(1));
|
|
105
|
+
this.#listContainer.addChild(new Text(theme.fg("muted", ` ${description}`), 0, 0));
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
export function createPiShellSkillsSelector(options) {
|
|
110
|
+
ensureTheme();
|
|
111
|
+
return componentPort(new SkillsSelectorComponent(options));
|
|
112
|
+
}
|
|
@@ -4,9 +4,10 @@
|
|
|
4
4
|
* Modifications: A1-owned class name and synchronized A1 keybinding contract replace the nominal
|
|
5
5
|
* private upstream keybinding constructor dependency; bare A1 injects the shared Settings/agent input
|
|
6
6
|
* frame, transient contextual-suggestion branch, and explicit body geometry for selection and
|
|
7
|
-
* above-prompt autocomplete. Bare A1 also clears a sole top-level slash-command search on Escape.
|
|
7
|
+
* above-prompt autocomplete. Bare A1 also clears a sole top-level slash-command search on Escape. Bare
|
|
8
|
+
* A1 also completes a selected tunnel command with `:` and reopens the menu on its tunnel rows.
|
|
8
9
|
* Deviations: owned-shared-input-frame, above-prompt-autocomplete-placement,
|
|
9
|
-
* clear-command-search-on-escape.
|
|
10
|
+
* clear-command-search-on-escape, command-tunnel-colon-completion.
|
|
10
11
|
*/
|
|
11
12
|
import { type EditorOptions, type EditorTheme, type TUI } from "@earendil-works/pi-tui";
|
|
12
13
|
import type { AppKeybinding, KeybindingsManager } from "../adjacent/core/keybindings.js";
|
|
@@ -23,6 +24,11 @@ export interface OwnedEditorOptions extends EditorOptions {
|
|
|
23
24
|
readonly getVisualLineCount?: (width: number) => number | undefined;
|
|
24
25
|
/** Bare-A1 exception: Escape on a sole top-level slash-command search also clears the prompt. */
|
|
25
26
|
readonly clearCommandSearchOnEscape?: boolean;
|
|
27
|
+
/**
|
|
28
|
+
* Bare-A1 exception: the slash commands that own a `:` tunnel. Typing `:` while one of them is
|
|
29
|
+
* the selected row of a sole top-level search completes to `/<command>:` and reopens the menu.
|
|
30
|
+
*/
|
|
31
|
+
readonly commandTunnels?: () => readonly string[];
|
|
26
32
|
}
|
|
27
33
|
export interface ShellEditorInstance extends EditorSurface {
|
|
28
34
|
readonly actionHandlers: Map<AppKeybinding, () => void>;
|
|
@@ -4,9 +4,10 @@
|
|
|
4
4
|
* Modifications: A1-owned class name and synchronized A1 keybinding contract replace the nominal
|
|
5
5
|
* private upstream keybinding constructor dependency; bare A1 injects the shared Settings/agent input
|
|
6
6
|
* frame, transient contextual-suggestion branch, and explicit body geometry for selection and
|
|
7
|
-
* above-prompt autocomplete. Bare A1 also clears a sole top-level slash-command search on Escape.
|
|
7
|
+
* above-prompt autocomplete. Bare A1 also clears a sole top-level slash-command search on Escape. Bare
|
|
8
|
+
* A1 also completes a selected tunnel command with `:` and reopens the menu on its tunnel rows.
|
|
8
9
|
* Deviations: owned-shared-input-frame, above-prompt-autocomplete-placement,
|
|
9
|
-
* clear-command-search-on-escape.
|
|
10
|
+
* clear-command-search-on-escape, command-tunnel-colon-completion.
|
|
10
11
|
*/
|
|
11
12
|
import { CURSOR_MARKER, Editor, visibleWidth, wrapTextWithAnsi, } from "@earendil-works/pi-tui";
|
|
12
13
|
const PROMPT_GRAPHEMES = new Intl.Segmenter(undefined, { granularity: "grapheme" });
|
|
@@ -25,6 +26,7 @@ export function createOwnedEditorClass(Base) {
|
|
|
25
26
|
#terminalRows;
|
|
26
27
|
#getVisualLineCount;
|
|
27
28
|
#clearCommandSearchOnEscape;
|
|
29
|
+
#commandTunnels;
|
|
28
30
|
#renderedBodyRowCount = 0;
|
|
29
31
|
keybindings;
|
|
30
32
|
constructor(tui, theme, keybindings, options = {}) {
|
|
@@ -36,6 +38,7 @@ export function createOwnedEditorClass(Base) {
|
|
|
36
38
|
this.#terminalRows = options.terminalRows ?? (() => 24);
|
|
37
39
|
this.#getVisualLineCount = options.getVisualLineCount;
|
|
38
40
|
this.#clearCommandSearchOnEscape = options.clearCommandSearchOnEscape === true;
|
|
41
|
+
this.#commandTunnels = options.commandTunnels ?? (() => []);
|
|
39
42
|
}
|
|
40
43
|
getRenderedBodyRowCount() { return this.#renderedBodyRowCount; }
|
|
41
44
|
setPromptSuggestion(text) {
|
|
@@ -58,11 +61,6 @@ export function createOwnedEditorClass(Base) {
|
|
|
58
61
|
const cursor = this.getCursor();
|
|
59
62
|
return cursor.line === 0 && cursor.col === text.length;
|
|
60
63
|
}
|
|
61
|
-
setText(text) {
|
|
62
|
-
if (text.length > 0)
|
|
63
|
-
this.#promptSuggestion = null;
|
|
64
|
-
super.setText(text);
|
|
65
|
-
}
|
|
66
64
|
render(width) {
|
|
67
65
|
if (this.#inputPresentation === undefined) {
|
|
68
66
|
const rows = super.render(width);
|
|
@@ -80,6 +78,8 @@ export function createOwnedEditorClass(Base) {
|
|
|
80
78
|
handleInput(data) {
|
|
81
79
|
if (this.onExtensionShortcut?.(data))
|
|
82
80
|
return;
|
|
81
|
+
if (data === ":" && this.#completeSelectedCommandTunnel())
|
|
82
|
+
return;
|
|
83
83
|
if (this.#promptSuggestion !== null
|
|
84
84
|
&& !this.isShowingAutocomplete()
|
|
85
85
|
&& this.canPresentPromptSuggestion()
|
|
@@ -134,6 +134,21 @@ export function createOwnedEditorClass(Base) {
|
|
|
134
134
|
}
|
|
135
135
|
super.handleInput(data);
|
|
136
136
|
}
|
|
137
|
+
/**
|
|
138
|
+
* Replace a sole top-level slash search whose selected row is a tunnel command with `/<command>:`
|
|
139
|
+
* and reopen the menu on the tunnel rows. The replacement goes through the public setText, which
|
|
140
|
+
* records the undo snapshot; every other colon stays ordinary text.
|
|
141
|
+
*/
|
|
142
|
+
#completeSelectedCommandTunnel() {
|
|
143
|
+
if (!this.isTopLevelCommandSearch())
|
|
144
|
+
return false;
|
|
145
|
+
const selected = selectedAutocompleteValue(this);
|
|
146
|
+
if (selected === undefined || !this.#commandTunnels().includes(selected))
|
|
147
|
+
return false;
|
|
148
|
+
this.setText(`/${selected}:`);
|
|
149
|
+
triggerAutocomplete(this);
|
|
150
|
+
return true;
|
|
151
|
+
}
|
|
137
152
|
#renderSuggestion(width) {
|
|
138
153
|
const presentation = this.#inputPresentation;
|
|
139
154
|
const { paddingX, contentWidth, layoutWidth } = presentation.geometry(width, this.getPaddingX());
|
|
@@ -185,3 +200,23 @@ export function createOwnedEditorClass(Base) {
|
|
|
185
200
|
}
|
|
186
201
|
export class OwnedEditor extends createOwnedEditorClass(Editor) {
|
|
187
202
|
}
|
|
203
|
+
/** The value of the row the open autocomplete list highlights, read from either editor's list. */
|
|
204
|
+
function selectedAutocompleteValue(editor) {
|
|
205
|
+
const list = Reflect.get(editor, "autocompleteList");
|
|
206
|
+
if (typeof list !== "object" || list === null)
|
|
207
|
+
return undefined;
|
|
208
|
+
const getSelectedItem = Reflect.get(list, "getSelectedItem");
|
|
209
|
+
if (typeof getSelectedItem !== "function")
|
|
210
|
+
return undefined;
|
|
211
|
+
const item = getSelectedItem.call(list);
|
|
212
|
+
if (typeof item !== "object" || item === null)
|
|
213
|
+
return undefined;
|
|
214
|
+
const value = Reflect.get(item, "value");
|
|
215
|
+
return typeof value === "string" ? value : undefined;
|
|
216
|
+
}
|
|
217
|
+
/** Request the slash-command menu for the current text, as typing a command character would. */
|
|
218
|
+
function triggerAutocomplete(editor) {
|
|
219
|
+
const trigger = Reflect.get(editor, "tryTriggerAutocomplete");
|
|
220
|
+
if (typeof trigger === "function")
|
|
221
|
+
trigger.call(editor);
|
|
222
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { type AgentSession } from "../startup-public.js";
|
|
2
2
|
import { type OwnedUiCommand, type OwnedUiImageAttachment, type OwnedUiPromptSuggestionGeneratorPort, type OwnedUiPromptSuggestionReasoning, type OwnedUiPromptSuggestionRequest, type OwnedUiPromptSuggestionResult, type OwnedUiEvent, type OwnedUiSessionViewModel, type OwnedUiSnapshot } from "../../../contracts/owned-ui/index.js";
|
|
3
|
-
import type { PiAuthenticationProviderOption, PiBashWorkflowResult, PiPinnedSettingsCallback, PiPinnedSettingsSnapshot, PiProjectTrustContext, PiProjectTrustUpdate, PiScopedModelsContext, PiScopedModelsRefreshResult, PiSessionResumeMetadata, PiSessionSelectorContext, PiTreeSelectorContext, PiWorkflowAutocompleteCommand, PiWorkflowHost, PiWorkflowInteractionHost, PiWorkflowOption, PiWorkflowRequest, PiWorkflowResult } from "./workflows.js";
|
|
3
|
+
import type { PiAuthenticationProviderOption, PiBashWorkflowResult, PiModelsContext, PiModelsRefreshResult, PiPinnedSettingsCallback, PiPinnedSettingsSnapshot, PiProjectTrustContext, PiProjectTrustUpdate, PiScopedModelsContext, PiScopedModelsRefreshResult, PiSessionResumeMetadata, PiSessionSelectorContext, PiTreeSelectorContext, PiWorkflowAutocompleteCommand, PiWorkflowHost, PiWorkflowInteractionHost, PiWorkflowOption, PiWorkflowRequest, PiWorkflowResult } from "./workflows.js";
|
|
4
4
|
import type { PiSettingsBridge } from "./settings-bridge.js";
|
|
5
5
|
import type { PiSettingOwnerHandlers } from "./settings-bridge.js";
|
|
6
6
|
import type { PiProjectTrustPreflightPrompt } from "./project-trust-preflight.js";
|
|
@@ -91,6 +91,10 @@ export declare class PiEngineAdapter implements OwnedUiPromptSuggestionGenerator
|
|
|
91
91
|
updateScopedModels(enabledModelIds: readonly string[] | null): void;
|
|
92
92
|
persistScopedModels(enabledModelIds: readonly string[] | null): void;
|
|
93
93
|
refreshScopedModels(signal: AbortSignal): Promise<PiScopedModelsRefreshResult>;
|
|
94
|
+
modelsContext(): PiModelsContext;
|
|
95
|
+
setSessionModelScope(scopeIds: readonly string[]): void;
|
|
96
|
+
persistModelScope(scopeIds: readonly string[]): void;
|
|
97
|
+
refreshModels(signal: AbortSignal): Promise<PiModelsRefreshResult>;
|
|
94
98
|
pinnedLoginOptions(authType?: "oauth" | "api_key"): readonly PiAuthenticationProviderOption[];
|
|
95
99
|
pinnedLoginMethodOptions(providerReference: string): {
|
|
96
100
|
readonly title: string;
|
|
@@ -193,7 +193,7 @@ export class PiEngineAdapter {
|
|
|
193
193
|
emitView: () => { this.#emitView(); },
|
|
194
194
|
diagnostic: (severity, code, message, recoverable) => { this.#addDiagnostic(severity, code, message, recoverable); },
|
|
195
195
|
});
|
|
196
|
-
this.#resources = new PiResourceCatalog({ contexts: this.#contexts }, {
|
|
196
|
+
this.#resources = new PiResourceCatalog({ contexts: this.#contexts, productMode: options.settingsProductMode ?? "bare" }, {
|
|
197
197
|
session: () => this.#engine.session,
|
|
198
198
|
runtime: () => this.#engine.runtime,
|
|
199
199
|
});
|
|
@@ -321,6 +321,18 @@ export class PiEngineAdapter {
|
|
|
321
321
|
refreshScopedModels(signal) {
|
|
322
322
|
return this.#contexts.refreshScopedModels(signal);
|
|
323
323
|
}
|
|
324
|
+
modelsContext() {
|
|
325
|
+
return this.#contexts.modelsContext();
|
|
326
|
+
}
|
|
327
|
+
setSessionModelScope(scopeIds) {
|
|
328
|
+
this.#contexts.setSessionModelScope(scopeIds);
|
|
329
|
+
}
|
|
330
|
+
persistModelScope(scopeIds) {
|
|
331
|
+
this.#contexts.persistModelScope(scopeIds);
|
|
332
|
+
}
|
|
333
|
+
refreshModels(signal) {
|
|
334
|
+
return this.#contexts.refreshModels(signal);
|
|
335
|
+
}
|
|
324
336
|
pinnedLoginOptions(authType) {
|
|
325
337
|
return this.#contexts.loginOptions(authType);
|
|
326
338
|
}
|
|
@@ -14,5 +14,5 @@ export { resolvePiProjectTrustPreflight } from "./project-trust-preflight.js";
|
|
|
14
14
|
export type { PiProjectTrustPreflightPrompt, PiProjectTrustPreflightRequest, PiProjectTrustPreflightResult, ResolvePiProjectTrustPreflightOptions, } from "./project-trust-preflight.js";
|
|
15
15
|
export { configureOwnedHttpDispatcher, configuredOwnedHttpIdleTimeoutMs } from "./http-dispatcher.js";
|
|
16
16
|
export { createPiPackagesPort } from "./package-integration.js";
|
|
17
|
-
export { PINNED_PI_HIDDEN_COMMAND_NAMES, PINNED_PI_SETTINGS_CALLBACKS, PINNED_PI_WORKFLOW_COMMAND_NAMES } from "./workflows.js";
|
|
18
|
-
export type { PiAuthenticationProviderOption, PiAuthenticationProviderStatus, PiBashWorkflowResult, PiHiddenWorkflowCommandName, PiPinnedSettingsCallback, PiPinnedSettingsSnapshot, PiProjectTrustContext, PiProjectTrustUpdate, PiScopedModelDescriptor, PiScopedModelsContext, PiScopedModelsRefreshResult, PiSessionInfoPresentation, PiSessionResumeMetadata, PiSessionSelectorContext, PiTreeSelectorContext, PiWorkflowAutocompleteCommand, PiWorkflowCommandName, PiWorkflowHost, PiWorkflowInteractionHost, PiWorkflowInteractionRequest, PiWorkflowLoginNotification, PiWorkflowLoginStart, PiWorkflowMessage, PiWorkflowMessageKind, PiWorkflowOption, PiWorkflowOutcome, PiWorkflowPresentation, PiWorkflowRequest, PiWorkflowResult, PiWorkflowRoute, } from "./workflows.js";
|
|
17
|
+
export { OWNED_MODELS_COMMAND_NAME, OWNED_WORKFLOW_COMMAND_NAMES, PINNED_PI_HIDDEN_COMMAND_NAMES, PINNED_PI_SETTINGS_CALLBACKS, PINNED_PI_WORKFLOW_COMMAND_NAMES, workflowCommandNames, } from "./workflows.js";
|
|
18
|
+
export type { PiAuthenticationProviderOption, PiAuthenticationProviderStatus, PiBashWorkflowResult, PiHiddenWorkflowCommandName, PiModelsContext, PiModelsRefreshResult, PiPinnedSettingsCallback, PiPinnedSettingsSnapshot, PiProjectTrustContext, PiProductMode, PiProjectTrustUpdate, PiScopedModelDescriptor, PiScopedModelsContext, PiScopedModelsRefreshResult, PiSessionInfoPresentation, PiSessionResumeMetadata, PiSessionSelectorContext, PiTreeSelectorContext, PiWorkflowAutocompleteCommand, PiWorkflowCommandName, PiWorkflowHost, PiWorkflowInteractionHost, PiWorkflowInteractionRequest, PiWorkflowLoginNotification, PiWorkflowLoginStart, PiWorkflowMessage, PiWorkflowMessageKind, PiWorkflowOption, PiWorkflowOutcome, PiWorkflowPresentation, PiWorkflowRequest, PiWorkflowResult, PiWorkflowRoute, } from "./workflows.js";
|
|
@@ -7,4 +7,4 @@ export { assertPiSettingsMetadata, loadPiSettingsMetadata, PI_SETTINGS_METADATA_
|
|
|
7
7
|
export { resolvePiProjectTrustPreflight } from "./project-trust-preflight.js";
|
|
8
8
|
export { configureOwnedHttpDispatcher, configuredOwnedHttpIdleTimeoutMs } from "./http-dispatcher.js";
|
|
9
9
|
export { createPiPackagesPort } from "./package-integration.js";
|
|
10
|
-
export { PINNED_PI_HIDDEN_COMMAND_NAMES, PINNED_PI_SETTINGS_CALLBACKS, PINNED_PI_WORKFLOW_COMMAND_NAMES } from "./workflows.js";
|
|
10
|
+
export { OWNED_MODELS_COMMAND_NAME, OWNED_WORKFLOW_COMMAND_NAMES, PINNED_PI_HIDDEN_COMMAND_NAMES, PINNED_PI_SETTINGS_CALLBACKS, PINNED_PI_WORKFLOW_COMMAND_NAMES, workflowCommandNames, } from "./workflows.js";
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { AgentSession, AgentSessionRuntime } from "../startup-public.js";
|
|
2
2
|
import type { PiWorkflowContexts } from "./workflow-contexts.js";
|
|
3
|
-
import { type PiWorkflowAutocompleteCommand } from "./workflows.js";
|
|
3
|
+
import { type PiProductMode, type PiWorkflowAutocompleteCommand } from "./workflows.js";
|
|
4
4
|
export interface OwnedPiResourceSummary {
|
|
5
5
|
readonly kind: "skill" | "prompt-template" | "agent-context" | "system-prompt" | "theme";
|
|
6
6
|
readonly id: string;
|
|
@@ -38,6 +38,7 @@ export declare class PiResourceCatalog {
|
|
|
38
38
|
#private;
|
|
39
39
|
constructor(options: {
|
|
40
40
|
readonly contexts: PiWorkflowContexts;
|
|
41
|
+
readonly productMode?: PiProductMode;
|
|
41
42
|
}, ports: PiResourceCatalogPorts);
|
|
42
43
|
nonVisualResources(): readonly OwnedPiResourceSummary[];
|
|
43
44
|
extensionResources(): readonly OwnedPiExtensionResourceSummary[];
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { isRecord, stringProperty } from "./message-values.js";
|
|
2
|
-
import {
|
|
2
|
+
import { OWNED_MODELS_COMMAND_NAME, workflowCommandNames } from "./workflows.js";
|
|
3
3
|
/**
|
|
4
4
|
* The resources pinned Pi's loader discovered for the current runtime (skills, prompt templates,
|
|
5
5
|
* agent context files, system prompts, themes, extensions) and the slash-command autocomplete
|
|
@@ -9,8 +9,10 @@ import { PINNED_PI_WORKFLOW_COMMAND_NAMES } from "./workflows.js";
|
|
|
9
9
|
export class PiResourceCatalog {
|
|
10
10
|
#ports;
|
|
11
11
|
#contexts;
|
|
12
|
+
#productMode;
|
|
12
13
|
constructor(options, ports) {
|
|
13
14
|
this.#contexts = options.contexts;
|
|
15
|
+
this.#productMode = options.productMode ?? "bare";
|
|
14
16
|
this.#ports = ports;
|
|
15
17
|
}
|
|
16
18
|
nonVisualResources() {
|
|
@@ -159,13 +161,20 @@ export class PiResourceCatalog {
|
|
|
159
161
|
}
|
|
160
162
|
workflowAutocompleteCommands() {
|
|
161
163
|
const commands = [
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
164
|
+
this.#productMode === "bare"
|
|
165
|
+
? {
|
|
166
|
+
name: OWNED_MODELS_COMMAND_NAME,
|
|
167
|
+
description: "Switch models and manage scoped model cycling",
|
|
168
|
+
argumentOptions: this.#contexts.modelOptions(),
|
|
169
|
+
source: "builtin",
|
|
170
|
+
}
|
|
171
|
+
: {
|
|
172
|
+
name: "model",
|
|
173
|
+
description: "Select model (opens selector UI)",
|
|
174
|
+
argumentHint: "<provider/model>",
|
|
175
|
+
argumentOptions: this.#contexts.modelOptions(),
|
|
176
|
+
source: "builtin",
|
|
177
|
+
},
|
|
169
178
|
{
|
|
170
179
|
name: "login",
|
|
171
180
|
description: "Configure provider authentication",
|
|
@@ -174,7 +183,8 @@ export class PiResourceCatalog {
|
|
|
174
183
|
source: "builtin",
|
|
175
184
|
},
|
|
176
185
|
];
|
|
177
|
-
|
|
186
|
+
// Invariant: only the active product mode's catalog reserves names; the other mode's replaced routes stay free for resources.
|
|
187
|
+
const usedNames = new Set(workflowCommandNames(this.#productMode));
|
|
178
188
|
const loader = this.#ports.runtime()?.services.resourceLoader;
|
|
179
189
|
if (!loader)
|
|
180
190
|
return commands;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { type AgentSession, type AgentSessionRuntime } from "../startup-public.js";
|
|
2
2
|
import type { OwnedUiModelInfo } from "../../../contracts/owned-ui/index.js";
|
|
3
|
-
import type { PiAuthenticationProviderOption, PiProjectTrustContext, PiProjectTrustUpdate, PiScopedModelsContext, PiScopedModelsRefreshResult, PiSessionSelectorContext, PiTreeSelectorContext, PiWorkflowOption } from "./workflows.js";
|
|
3
|
+
import type { PiAuthenticationProviderOption, PiModelsContext, PiModelsRefreshResult, PiProjectTrustContext, PiProjectTrustUpdate, PiScopedModelsContext, PiScopedModelsRefreshResult, PiSessionSelectorContext, PiTreeSelectorContext, PiWorkflowOption } from "./workflows.js";
|
|
4
4
|
type PiSessionApi = AgentSession;
|
|
5
5
|
type PiRuntimeApi = AgentSessionRuntime;
|
|
6
6
|
export interface PiWorkflowContextPorts {
|
|
@@ -40,6 +40,13 @@ export declare class PiWorkflowContexts {
|
|
|
40
40
|
updateScopedModels(enabledModelIds: readonly string[] | null): void;
|
|
41
41
|
persistScopedModels(enabledModelIds: readonly string[] | null): void;
|
|
42
42
|
refreshScopedModels(signal: AbortSignal): Promise<PiScopedModelsRefreshResult>;
|
|
43
|
+
/** The bare-A1 Models dialog's read: catalog, active model, and the session and persisted scopes kept separate. */
|
|
44
|
+
modelsContext(): PiModelsContext;
|
|
45
|
+
/** Replace the session's cycling scope with exactly these available models, in order; empty restores the all-model fallback. */
|
|
46
|
+
setSessionModelScope(scopeIds: readonly string[]): void;
|
|
47
|
+
/** Persist the explicit scope as `enabledModels`; an empty scope removes the setting so cycling falls back to every model. */
|
|
48
|
+
persistModelScope(scopeIds: readonly string[]): void;
|
|
49
|
+
refreshModels(signal: AbortSignal): Promise<PiModelsRefreshResult>;
|
|
43
50
|
pinnedLoginMethodOptions(providerReference: string): {
|
|
44
51
|
readonly title: string;
|
|
45
52
|
readonly options: readonly PiWorkflowOption[];
|
|
@@ -156,23 +156,67 @@ export class PiWorkflowContexts {
|
|
|
156
156
|
requireCapability(runtime.services.settingsManager?.setEnabledModels, "setEnabledModels").call(runtime.services.settingsManager, patterns === undefined ? undefined : [...patterns]);
|
|
157
157
|
}
|
|
158
158
|
async refreshScopedModels(signal) {
|
|
159
|
+
const outcome = await this.#refreshCatalogs(signal);
|
|
160
|
+
return { ...this.pinnedScopedModelsContext(), ...outcome };
|
|
161
|
+
}
|
|
162
|
+
/** The bare-A1 Models dialog's read: catalog, active model, and the session and persisted scopes kept separate. */
|
|
163
|
+
modelsContext() {
|
|
164
|
+
const session = this.#requireSession();
|
|
165
|
+
const runtime = this.#ports.runtime();
|
|
166
|
+
if (!runtime)
|
|
167
|
+
throw new Error("engine runtime is unavailable");
|
|
168
|
+
const models = scopedModelRecords(runtime.services.modelRuntime);
|
|
169
|
+
const scoped = session.scopedModels;
|
|
170
|
+
const active = this.#ports.activeModel();
|
|
171
|
+
const patterns = runtime.services.settingsManager?.getEnabledModels?.();
|
|
172
|
+
return {
|
|
173
|
+
models: models.map(item => item.descriptor),
|
|
174
|
+
activeModelId: active === null ? null : `${active.providerId}/${active.modelId}`,
|
|
175
|
+
sessionScopeIds: Array.isArray(scoped) ? scoped.map(scopedModelReference).filter((id) => id !== undefined) : [],
|
|
176
|
+
persistedScopeIds: Array.isArray(patterns)
|
|
177
|
+
? resolveConfiguredModelIds(patterns.filter((value) => typeof value === "string"), models)
|
|
178
|
+
: [],
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
/** Replace the session's cycling scope with exactly these available models, in order; empty restores the all-model fallback. */
|
|
182
|
+
setSessionModelScope(scopeIds) {
|
|
183
|
+
const session = this.#requireSession();
|
|
184
|
+
const runtime = this.#ports.runtime();
|
|
185
|
+
if (!runtime)
|
|
186
|
+
throw new Error("engine runtime is unavailable");
|
|
187
|
+
const models = scopedModelRecords(runtime.services.modelRuntime);
|
|
188
|
+
const scoped = scopeIds.flatMap(id => {
|
|
189
|
+
const item = models.find(candidate => `${candidate.descriptor.provider}/${candidate.descriptor.id}` === id);
|
|
190
|
+
return item === undefined ? [] : [{ model: item.model }];
|
|
191
|
+
});
|
|
192
|
+
requireCapability(session.setScopedModels, "setScopedModels").call(session, scoped);
|
|
193
|
+
this.#ports.emitView();
|
|
194
|
+
}
|
|
195
|
+
/** Persist the explicit scope as `enabledModels`; an empty scope removes the setting so cycling falls back to every model. */
|
|
196
|
+
persistModelScope(scopeIds) {
|
|
197
|
+
const runtime = this.#ports.runtime();
|
|
198
|
+
if (!runtime)
|
|
199
|
+
throw new Error("engine runtime is unavailable");
|
|
200
|
+
requireCapability(runtime.services.settingsManager?.setEnabledModels, "setEnabledModels")
|
|
201
|
+
.call(runtime.services.settingsManager, scopeIds.length === 0 ? undefined : [...scopeIds]);
|
|
202
|
+
}
|
|
203
|
+
async refreshModels(signal) {
|
|
204
|
+
const outcome = await this.#refreshCatalogs(signal);
|
|
205
|
+
return { models: this.modelsContext().models, ...outcome };
|
|
206
|
+
}
|
|
207
|
+
async #refreshCatalogs(signal) {
|
|
159
208
|
const runtime = this.#ports.runtime();
|
|
160
209
|
if (!runtime)
|
|
161
210
|
throw new Error("engine runtime is unavailable");
|
|
162
211
|
const result = await runtime.services.modelRuntime.refresh?.({ signal });
|
|
163
|
-
const context = this.pinnedScopedModelsContext();
|
|
164
212
|
if (isRecord(result) && result.aborted === true) {
|
|
165
|
-
return {
|
|
213
|
+
return { status: "Model refresh timed out; showing cached models.", statusKind: "warning" };
|
|
166
214
|
}
|
|
167
215
|
const errors = isRecord(result) ? result.errors : undefined;
|
|
168
216
|
if (errors instanceof Map && errors.size > 0) {
|
|
169
|
-
return {
|
|
170
|
-
...context,
|
|
171
|
-
status: `Could not refresh ${[...errors.keys()].join(", ")}; showing cached models.`,
|
|
172
|
-
statusKind: "warning",
|
|
173
|
-
};
|
|
217
|
+
return { status: `Could not refresh ${[...errors.keys()].join(", ")}; showing cached models.`, statusKind: "warning" };
|
|
174
218
|
}
|
|
175
|
-
return {
|
|
219
|
+
return { status: "Model catalogs refreshed.", statusKind: "success" };
|
|
176
220
|
}
|
|
177
221
|
pinnedLoginMethodOptions(providerReference) {
|
|
178
222
|
const runtime = this.#ports.runtime();
|