@timurproko/a1 0.1.8-dev.512 → 0.1.8-dev.518

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 (65) hide show
  1. package/README.md +44 -1
  2. package/dist/app/session-shell/index.d.ts +1 -1
  3. package/dist/app/session-shell/prompt-suggestion-controller.d.ts +2 -0
  4. package/dist/app/session-shell/prompt-suggestion-controller.js +12 -0
  5. package/dist/app/session-shell/session-shell-root.d.ts +10 -0
  6. package/dist/app/session-shell/session-shell-root.js +12 -2
  7. package/dist/app/session-shell/session-shell.d.ts +8 -1
  8. package/dist/app/session-shell/session-shell.js +217 -18
  9. package/dist/composition/index.d.ts +1 -1
  10. package/dist/composition/owned-ui.js +21 -3
  11. package/dist/composition/settings-route-host.d.ts +17 -4
  12. package/dist/composition/settings-route-host.js +47 -16
  13. package/dist/contracts/owned-ui/index.d.ts +1 -1
  14. package/dist/contracts/owned-ui/model.d.ts +1 -5
  15. package/dist/features/owned-ui/index.d.ts +3 -0
  16. package/dist/features/owned-ui/index.js +2 -0
  17. package/dist/features/owned-ui/reference-routes.d.ts +6 -0
  18. package/dist/features/owned-ui/reference-routes.js +6 -0
  19. package/dist/features/owned-ui/reference-screen-app.d.ts +37 -0
  20. package/dist/features/owned-ui/reference-screen-app.js +232 -0
  21. package/dist/features/owned-ui/settings-app.js +2 -7
  22. package/dist/integrations/pi/components/index.d.ts +4 -2
  23. package/dist/integrations/pi/components/index.js +2 -1
  24. package/dist/integrations/pi/components/models-dialog.d.ts +55 -0
  25. package/dist/integrations/pi/components/models-dialog.js +310 -0
  26. package/dist/integrations/pi/components/shell-editor-autocomplete.d.ts +10 -0
  27. package/dist/integrations/pi/components/shell-editor-autocomplete.js +29 -5
  28. package/dist/integrations/pi/components/shell-footer-status.js +2 -1
  29. package/dist/integrations/pi/components/shell-presenters-info.d.ts +10 -0
  30. package/dist/integrations/pi/components/shell-presenters-info.js +31 -11
  31. package/dist/integrations/pi/components/shell-selectors-dialogs.d.ts +11 -1
  32. package/dist/integrations/pi/components/shell-selectors-dialogs.js +13 -0
  33. package/dist/integrations/pi/components/shell-shared-facade.d.ts +8 -0
  34. package/dist/integrations/pi/components/skills-command.d.ts +47 -0
  35. package/dist/integrations/pi/components/skills-command.js +118 -0
  36. package/dist/integrations/pi/components/skills-dialog.d.ts +9 -0
  37. package/dist/integrations/pi/components/skills-dialog.js +113 -0
  38. package/dist/integrations/pi/components/upstream/components/owned-editor.d.ts +8 -2
  39. package/dist/integrations/pi/components/upstream/components/owned-editor.js +42 -7
  40. package/dist/integrations/pi/engine/adapter.d.ts +5 -1
  41. package/dist/integrations/pi/engine/adapter.js +13 -1
  42. package/dist/integrations/pi/engine/index.d.ts +2 -2
  43. package/dist/integrations/pi/engine/index.js +1 -1
  44. package/dist/integrations/pi/engine/resource-catalog.d.ts +2 -1
  45. package/dist/integrations/pi/engine/resource-catalog.js +19 -9
  46. package/dist/integrations/pi/engine/workflow-contexts.d.ts +8 -1
  47. package/dist/integrations/pi/engine/workflow-contexts.js +52 -8
  48. package/dist/integrations/pi/engine/workflow-runner.js +98 -74
  49. package/dist/integrations/pi/engine/workflows.d.ts +26 -2
  50. package/dist/integrations/pi/engine/workflows.js +8 -0
  51. package/dist/native/darwin-arm64/manifest.json +1 -1
  52. package/dist/native/linux-x64/manifest.json +1 -1
  53. package/dist/native/win32-x64/manifest.json +2 -2
  54. package/dist/native/win32-x64/process-guardian.exe +0 -0
  55. package/dist/ui/apps/contracts.d.ts +9 -1
  56. package/dist/ui/apps/index.d.ts +1 -1
  57. package/dist/ui/settings/declarations.d.ts +15 -29
  58. package/dist/ui/settings/declarations.js +14 -25
  59. package/dist/ui/settings/index.d.ts +1 -1
  60. package/dist/ui/settings/index.js +1 -1
  61. package/dist/ui/settings/migrations.js +18 -0
  62. package/docs/architecture/prompt-suggestions.md +1 -1
  63. package/docs/architecture/ui-reference-provenance.md +1 -0
  64. package/docs/features/modal-content-interaction.md +3 -2
  65. package/package.json +1 -1
@@ -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;
@@ -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,113 @@
1
+ import { DynamicBorder, keyHint, rawKeyHint } from "../startup-public.js";
2
+ import { Container, getKeybindings, Input, Spacer, Text, TruncatedText } 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 description truncated to one line under the rows, the pinned scroll counter on overflow,
11
+ * and the two 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
+ // Invariant: the description never wraps; it is cut to the viewport width with an ellipsis.
106
+ this.#listContainer.addChild(new TruncatedText(theme.fg("muted", ` ${description}`), 0, 0));
107
+ }
108
+ }
109
+ }
110
+ export function createPiShellSkillsSelector(options) {
111
+ ensureTheme();
112
+ return componentPort(new SkillsSelectorComponent(options));
113
+ }
@@ -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[];