@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.
Files changed (75) hide show
  1. package/README.md +44 -1
  2. package/bin/activate.js +8 -0
  3. package/bin/sync-pi-tui-proxy.js +7 -0
  4. package/dist/app/session-shell/index.d.ts +1 -1
  5. package/dist/app/session-shell/prompt-suggestion-controller.d.ts +2 -0
  6. package/dist/app/session-shell/prompt-suggestion-controller.js +12 -0
  7. package/dist/app/session-shell/session-shell-root.d.ts +10 -0
  8. package/dist/app/session-shell/session-shell-root.js +12 -2
  9. package/dist/app/session-shell/session-shell.d.ts +8 -1
  10. package/dist/app/session-shell/session-shell.js +217 -18
  11. package/dist/composition/index.d.ts +1 -1
  12. package/dist/composition/owned-ui.js +21 -3
  13. package/dist/composition/settings-route-host.d.ts +17 -4
  14. package/dist/composition/settings-route-host.js +47 -16
  15. package/dist/contracts/owned-ui/index.d.ts +1 -1
  16. package/dist/contracts/owned-ui/model.d.ts +1 -5
  17. package/dist/features/owned-ui/index.d.ts +3 -0
  18. package/dist/features/owned-ui/index.js +2 -0
  19. package/dist/features/owned-ui/reference-routes.d.ts +6 -0
  20. package/dist/features/owned-ui/reference-routes.js +6 -0
  21. package/dist/features/owned-ui/reference-screen-app.d.ts +37 -0
  22. package/dist/features/owned-ui/reference-screen-app.js +232 -0
  23. package/dist/features/owned-ui/settings-app.js +2 -7
  24. package/dist/foundation/release/index.d.ts +2 -0
  25. package/dist/foundation/release/index.js +1 -0
  26. package/dist/foundation/release/update-activation.d.ts +72 -0
  27. package/dist/foundation/release/update-activation.js +204 -0
  28. package/dist/foundation/release/update.d.ts +5 -10
  29. package/dist/foundation/release/update.js +39 -38
  30. package/dist/integrations/pi/components/index.d.ts +4 -2
  31. package/dist/integrations/pi/components/index.js +2 -1
  32. package/dist/integrations/pi/components/models-dialog.d.ts +55 -0
  33. package/dist/integrations/pi/components/models-dialog.js +310 -0
  34. package/dist/integrations/pi/components/shell-editor-autocomplete.d.ts +10 -0
  35. package/dist/integrations/pi/components/shell-editor-autocomplete.js +29 -5
  36. package/dist/integrations/pi/components/shell-footer-status.js +2 -1
  37. package/dist/integrations/pi/components/shell-presenters-info.d.ts +10 -0
  38. package/dist/integrations/pi/components/shell-presenters-info.js +31 -11
  39. package/dist/integrations/pi/components/shell-selectors-dialogs.d.ts +11 -1
  40. package/dist/integrations/pi/components/shell-selectors-dialogs.js +13 -0
  41. package/dist/integrations/pi/components/shell-shared-facade.d.ts +8 -0
  42. package/dist/integrations/pi/components/skills-command.d.ts +47 -0
  43. package/dist/integrations/pi/components/skills-command.js +118 -0
  44. package/dist/integrations/pi/components/skills-dialog.d.ts +9 -0
  45. package/dist/integrations/pi/components/skills-dialog.js +112 -0
  46. package/dist/integrations/pi/components/upstream/components/owned-editor.d.ts +8 -2
  47. package/dist/integrations/pi/components/upstream/components/owned-editor.js +42 -7
  48. package/dist/integrations/pi/engine/adapter.d.ts +5 -1
  49. package/dist/integrations/pi/engine/adapter.js +13 -1
  50. package/dist/integrations/pi/engine/index.d.ts +2 -2
  51. package/dist/integrations/pi/engine/index.js +1 -1
  52. package/dist/integrations/pi/engine/resource-catalog.d.ts +2 -1
  53. package/dist/integrations/pi/engine/resource-catalog.js +19 -9
  54. package/dist/integrations/pi/engine/workflow-contexts.d.ts +8 -1
  55. package/dist/integrations/pi/engine/workflow-contexts.js +52 -8
  56. package/dist/integrations/pi/engine/workflow-runner.js +98 -74
  57. package/dist/integrations/pi/engine/workflows.d.ts +26 -2
  58. package/dist/integrations/pi/engine/workflows.js +8 -0
  59. package/dist/native/darwin-arm64/manifest.json +1 -1
  60. package/dist/native/linux-x64/manifest.json +1 -1
  61. package/dist/native/win32-x64/manifest.json +2 -2
  62. package/dist/native/win32-x64/process-guardian.exe +0 -0
  63. package/dist/runtime-payload-inventory.json +1 -0
  64. package/dist/ui/apps/contracts.d.ts +9 -1
  65. package/dist/ui/apps/index.d.ts +1 -1
  66. package/dist/ui/settings/declarations.d.ts +15 -29
  67. package/dist/ui/settings/declarations.js +14 -25
  68. package/dist/ui/settings/index.d.ts +1 -1
  69. package/dist/ui/settings/index.js +1 -1
  70. package/dist/ui/settings/migrations.js +18 -0
  71. package/docs/architecture/internal-naming.md +2 -0
  72. package/docs/architecture/prompt-suggestions.md +1 -1
  73. package/docs/architecture/ui-reference-provenance.md +1 -0
  74. package/docs/features/modal-content-interaction.md +3 -2
  75. 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 { PINNED_PI_WORKFLOW_COMMAND_NAMES } from "./workflows.js";
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
- name: "model",
164
- description: "Select model (opens selector UI)",
165
- argumentHint: "<provider/model>",
166
- argumentOptions: this.#contexts.modelOptions(),
167
- source: "builtin",
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
- const usedNames = new Set(PINNED_PI_WORKFLOW_COMMAND_NAMES);
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 { ...context, status: "Model refresh timed out; showing cached models.", statusKind: "warning" };
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 { ...context, status: "Model catalogs refreshed.", statusKind: "success" };
219
+ return { status: "Model catalogs refreshed.", statusKind: "success" };
176
220
  }
177
221
  pinnedLoginMethodOptions(providerReference) {
178
222
  const runtime = this.#ports.runtime();