@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
package/README.md CHANGED
@@ -53,10 +53,40 @@ shown on the level name in the status bar, not on the input bars.
53
53
 
54
54
  In bare A1, **Ctrl+L cycles the thinking level**. **Shift+Tab is unassigned** and
55
55
  performs no agent-input action, including while a suggestion is visible. Model
56
- selection has **no default shortcut**; use `/model`. Explicit keybinding overrides
56
+ selection has **no default shortcut**; use `/models`. Explicit keybinding overrides
57
57
  remain configurable, and `/hotkeys` shows the resolved bindings. The `a1 pi`
58
58
  comparison profile keeps its original presentation and shortcuts.
59
59
 
60
+ ## Models
61
+
62
+ Bare A1 has one model command, `/models`, which replaces the pinned `/model` and
63
+ `/scoped-models` pair (those remain in the `a1 pi` comparison profile). The dialog
64
+ lists every authenticated model with `all` and `scoped` filters (Tab), a search box,
65
+ a scope marker before each model id, and the active checkmark after `[provider]`.
66
+ Enter switches to the highlighted model and persists it as the default; Space adds or
67
+ removes it from the Ctrl+P cycling scope for this session only; Ctrl+S saves that scope
68
+ to settings. While the scope or its order differs from the last save, the title reads
69
+ `Models (unsaved)`; Escape closes without saving and keeps the session-only scope.
70
+ `/models <text>` seeds the search instead of switching. The bulk, provider, and reorder
71
+ scope actions keep their existing bindings and appear under `/hotkeys`.
72
+
73
+ ## Reference screens
74
+
75
+ In bare A1, `/changelog` and `/hotkeys` open a full-screen read-only reference
76
+ screen over the session instead of appending a long document to the
77
+ conversation: `What's New` shows the complete pinned changelog and
78
+ `Keyboard Shortcuts` shows the bare-A1 keybinding tables, including extension
79
+ shortcuts as bound when the screen opens. Scroll with ↑/↓, PageUp/PageDown,
80
+ Home/End, the mouse wheel, or the scrollbar rail, which follows the
81
+ `/settings` → Scroll appearance, style, and speed; press `Esc` to close. The
82
+ screen is framed between two border-coloured rules with the hint below, as the
83
+ v2 extension drew it. After
84
+ an upgrade, the release notes newer than the last acknowledged version open the
85
+ same `What's New` screen once at startup while the feed keeps only the
86
+ `Run /changelog to view the full release notes.` hint. If another dialog is up
87
+ at that moment, the screen is not opened for that launch. The `a1 pi`
88
+ comparison profile keeps Pi's in-feed changelog and hotkeys documents.
89
+
60
90
  ## Prompt suggestions
61
91
 
62
92
  Bare A1's `/settings` → Agent → Prompt suggestions controls faint next-prompt
@@ -71,6 +101,19 @@ preferred when consistent with your intent; genuinely unclear cases can remain
71
101
  empty. [Private diagnostic capture](docs/architecture/prompt-suggestions.md)
72
102
  explains how to distinguish skipped, empty, rejected, and timed-out suggestions.
73
103
 
104
+ ## Skills
105
+
106
+ Bare A1's `/settings` → Agent → Skills chooses how discovered skills appear in the
107
+ slash-command menu. `collapse` (the default) replaces the per-skill `/skill:<name>`
108
+ entries with one `/skills` command: bare `/skills` opens a searchable Skills dialog,
109
+ `/skills <name> [args]` applies a skill directly (an unknown name reports
110
+ `Unknown skill: <name>`), and typing `/skills:` lists every skill inline so
111
+ `/skills:<name> args` runs as `/skill:<name> args`. With the `skills` row selected in
112
+ the menu, typing `:` completes to `/skills:`. `expand` lists every `/skill:<name>`
113
+ command as before. Both take effect immediately; a typed `/skill:<name>` always
114
+ works, and the engine's own Skill commands toggle still governs whether skills are
115
+ offered at all. The `a1 pi` comparison profile is unchanged.
116
+
74
117
  ## Quitting
75
118
 
76
119
  `/quit`, `Ctrl+C` twice, and `Ctrl+D` on an empty prompt end a bare A1 session.
@@ -2,6 +2,6 @@ export { ClipboardDiagnosticCapture } from "./clipboard-diagnostics.js";
2
2
  export { ContextualPromptSuggestionController, samePromptSuggestionIdentity } from "./prompt-suggestion-controller.js";
3
3
  export type { ContextualPromptSuggestionControllerOptions, ContextualPromptSuggestionSurface } from "./prompt-suggestion-controller.js";
4
4
  export { OwnedUiSessionShell, OwnedUiSessionShellRoot, formatSessionResumeCommand, quoteCommandArgument } from "./session-shell.js";
5
- export type { OwnedUiSessionShellOptions, OwnedUiShellDiagnosticOptions, OwnedUiShellEngineOptions, OwnedUiShellHistoryOptions, OwnedUiShellPresentationOptions, OwnedUiShellSuggestionOptions, SessionResumeCommandMetadata, } from "./session-shell.js";
5
+ export type { OwnedUiSessionShellOptions, OwnedUiShellDiagnosticOptions, OwnedUiShellEngineOptions, OwnedUiShellHistoryOptions, OwnedUiShellPresentationOptions, OwnedUiShellSkillsOptions, OwnedUiShellSuggestionOptions, SessionResumeCommandMetadata, } from "./session-shell.js";
6
6
  export { STREAM_PRESENTATION_INTERVAL_MS, StreamPresentationCoalescer } from "./stream-presentation-coalescer.js";
7
7
  export type { StreamPresentationScheduler } from "./stream-presentation-coalescer.js";
@@ -29,6 +29,8 @@ export declare class ContextualPromptSuggestionController {
29
29
  settle(identity: OwnedUiPromptSuggestionIdentity): void;
30
30
  accept(): void;
31
31
  invalidate(): void;
32
+ /** Retires only a generating request; a prepared or available suggestion survives the draft edit. */
33
+ abortPending(): void;
32
34
  dispose(): void;
33
35
  }
34
36
  export declare function samePromptSuggestionIdentity(left: OwnedUiPromptSuggestionIdentity, right: OwnedUiPromptSuggestionIdentity): boolean;
@@ -88,6 +88,18 @@ export class ContextualPromptSuggestionController {
88
88
  }
89
89
  accept() { this.#invalidate(false); }
90
90
  invalidate() { this.#invalidate(true); }
91
+ /** Retires only a generating request; a prepared or available suggestion survives the draft edit. */
92
+ abortPending() {
93
+ if (this.#state.status !== "generating")
94
+ return;
95
+ const request = this.#request;
96
+ this.#request = null;
97
+ this.#state = { status: "idle" };
98
+ if (request === null)
99
+ return;
100
+ this.#finish(request, "cancelled");
101
+ request.abort.abort();
102
+ }
91
103
  dispose() {
92
104
  this.#disposed = true;
93
105
  this.invalidate();
@@ -4,6 +4,7 @@ import type { UiRouteHost } from "../../ui/apps/contracts.js";
4
4
  import type { TranscriptViewportFrame, TranscriptViewportFrameDescriptor } from "../../ui/components/transcript-viewport.js";
5
5
  import type { PiEngineAdapter } from "../../integrations/pi/engine/adapter.js";
6
6
  import type { PiWorkflowMessage, PiWorkflowResult } from "../../integrations/pi/engine/workflows.js";
7
+ import { type PiShellHotkeysPresentation } from "../../integrations/pi/components/shell-presenters-info.js";
7
8
  import { type PiShellClipboardContent, type PiShellComponentPort, type PiShellEditorPort, type PiShellExtensionRendererResolver, type PiShellHeaderOptions, type PiShellHeaderPort, type PiShellImageAssetResolver, type PiShellLoadedResourcesPort, type PiShellResourceEntry, type PiShellTranscriptComponentPort } from "../../integrations/pi/components/shell-shared-facade.js";
8
9
  import { type PiTuiInputCoordinationScheduler, type PiTuiInputSurfaceKind } from "../../integrations/pi/tui-runtime/input-presentation-coordinator.js";
9
10
  import type { PiTuiComponentPort, PiTuiInputDiagnosticsEvent, PiTuiLayoutNode, PiTuiTerminalPort } from "../../integrations/pi/tui-runtime/contracts.js";
@@ -109,12 +110,19 @@ export interface OwnedUiShellDiagnosticOptions {
109
110
  /** Test seams for isolated paste preparation: an in-process starter, or the forked helper's entry. */
110
111
  readonly pastePreparation?: Omit<PastePreparationClientOptions, "onEvent" | "spareIdleMs">;
111
112
  }
113
+ /** The skills presentation choice: collapse the per-skill commands into one skills command or expand them. */
114
+ export interface OwnedUiShellSkillsOptions {
115
+ readonly presentation: () => "collapse" | "expand";
116
+ readonly onChange: (listener: () => void) => () => void;
117
+ }
112
118
  /** What composes an owned session shell, grouped by the collaborator that provides each part. */
113
119
  export interface OwnedUiSessionShellOptions {
114
120
  readonly engine: OwnedUiShellEngineOptions;
115
121
  readonly presentation?: OwnedUiShellPresentationOptions;
116
122
  readonly history?: OwnedUiShellHistoryOptions;
117
123
  readonly suggestions?: OwnedUiShellSuggestionOptions;
124
+ /** Supplied only to the bare-A1 composition; absent keeps the pinned per-skill command list. */
125
+ readonly skills?: OwnedUiShellSkillsOptions;
118
126
  readonly diagnostics?: OwnedUiShellDiagnosticOptions;
119
127
  }
120
128
  /** Composes backend session state into the owned transcript, viewport, editor, and dock presentation. */
@@ -152,6 +160,7 @@ export declare class OwnedUiSessionShellRoot implements PiTuiComponentPort {
152
160
  readonly captureClipboardPaste?: () => PasteSource;
153
161
  readonly pasteDiagnostics?: (event: PasteEvent) => void;
154
162
  readonly pastePreparation?: Omit<PastePreparationClientOptions, "onEvent">;
163
+ readonly skillsPresentation?: () => "collapse" | "expand";
155
164
  }, startup?: PiShellHeaderOptions, agentDir?: string, extensionRenderers?: PiShellExtensionRendererResolver, sessionLayout?: "pinned" | "custom-viewport", imageAssets?: PiShellImageAssetResolver);
156
165
  setEditorPaddingX(padding: number): void;
157
166
  setAutocompleteMaxVisible(maxVisible: number): void;
@@ -244,6 +253,7 @@ export declare class OwnedUiSessionShellRoot implements PiTuiComponentPort {
244
253
  setExtensionFooter(component: PiShellComponentPort | null): void;
245
254
  setExtensionStatus(key: string, text: string | undefined): void;
246
255
  setExtensionWorking(message: string | undefined, visible?: boolean): void;
256
+ hotkeysPresentation(): PiShellHotkeysPresentation;
247
257
  resetWorkflowPresentation(): void;
248
258
  resetExtensionUi(): void;
249
259
  addExtensionNotification(message: string, type: "info" | "warning" | "error"): void;
@@ -682,7 +682,9 @@ export class OwnedUiSessionShellRoot {
682
682
  .flatMap(diagnostic => renderPiShellPackageUpdateNotice(diagnostic.message.split("\n").filter(line => line.startsWith("- ")).map(line => line.slice(2)), width)));
683
683
  rows.push(...diagnostics
684
684
  .filter(diagnostic => diagnostic.code === "changelog-collapsed" || diagnostic.code === "changelog-expanded")
685
- .flatMap(diagnostic => diagnostic.code === "changelog-collapsed"
685
+ // Rationale: the custom viewport shows the new entries on the What's New screen, so its feed
686
+ // carries only the hint for either diagnostic; the pinned layout keeps the full document.
687
+ .flatMap(diagnostic => diagnostic.code === "changelog-collapsed" || this.#customViewport
686
688
  ? createPiShellCollapsedChangelog().render(width)
687
689
  : createPiShellChangelog(diagnostic.message).render(width)));
688
690
  rows.push(...diagnostics
@@ -809,7 +811,8 @@ export class OwnedUiSessionShellRoot {
809
811
  }
810
812
  if (result.command === "hotkeys" && result.outcome === "completed") {
811
813
  this.#lastWorkflowStatusId = undefined;
812
- const hotkeys = createPiShellHotkeys(this.editor.keybindingConfig(), bindings => this.#extensionRenderers.getShortcuts?.(bindings) ?? [], this.#customViewport ? "a1" : "pi");
814
+ const presentation = this.hotkeysPresentation();
815
+ const hotkeys = createPiShellHotkeys(presentation.bindings, presentation.getShortcuts, presentation.profile);
813
816
  this.#appendAnchoredWorkflowComponent(width => hotkeys.render(width), () => hotkeys.dispose?.());
814
817
  return;
815
818
  }
@@ -903,6 +906,13 @@ export class OwnedUiSessionShellRoot {
903
906
  this.#status.setWorkingOverride(visible ? message : undefined);
904
907
  this.invalidate();
905
908
  }
909
+ hotkeysPresentation() {
910
+ return {
911
+ bindings: this.editor.keybindingConfig(),
912
+ getShortcuts: bindings => this.#extensionRenderers.getShortcuts?.(bindings) ?? [],
913
+ profile: this.#customViewport ? "a1" : "pi",
914
+ };
915
+ }
906
916
  resetWorkflowPresentation() {
907
917
  this.#dockNotice = undefined;
908
918
  for (const id of this.#workflowStatusAnchors.keys()) {
@@ -1,12 +1,14 @@
1
1
  import type { OwnedUiSessionViewModel, OwnedUiThinkingLevel } from "../../contracts/owned-ui/index.js";
2
2
  import type { AdapterCommandResult } from "../../integrations/pi/engine/adapter.js";
3
3
  import type { PiWorkflowRequest } from "../../integrations/pi/engine/workflows.js";
4
+ import { type PiShellSkillSummary } from "../../integrations/pi/components/skills-command.js";
5
+ import { type PiShellHotkeysPresentation } from "../../integrations/pi/components/shell-presenters-info.js";
4
6
  import type { PiShellSelectorOption } from "../../integrations/pi/components/shell-shared-facade.js";
5
7
  import { type PiTuiDamageDecision } from "../../integrations/pi/tui-runtime/damage-aware-terminal.js";
6
8
  import { PiTuiRuntimeAdapter } from "../../integrations/pi/tui-runtime/adapter.js";
7
9
  import type { PiTuiOverlayHandle } from "../../integrations/pi/tui-runtime/contracts.js";
8
10
  import { OwnedUiSessionShellRoot, type OwnedUiBackendPort, type OwnedUiSessionShellOptions } from "./session-shell-root.js";
9
- export { OwnedUiSessionShellRoot, type OwnedUiSessionShellOptions, type OwnedUiShellDiagnosticOptions, type OwnedUiShellEngineOptions, type OwnedUiShellHistoryOptions, type OwnedUiShellPresentationOptions, type OwnedUiShellSuggestionOptions, } from "./session-shell-root.js";
11
+ export { OwnedUiSessionShellRoot, type OwnedUiSessionShellOptions, type OwnedUiShellDiagnosticOptions, type OwnedUiShellEngineOptions, type OwnedUiShellHistoryOptions, type OwnedUiShellPresentationOptions, type OwnedUiShellSkillsOptions, type OwnedUiShellSuggestionOptions, } from "./session-shell-root.js";
10
12
  /** Coordinates backend, owned presentation, and Pi TUI lifecycles for one interactive session. */
11
13
  export declare class OwnedUiSessionShell {
12
14
  #private;
@@ -35,6 +37,8 @@ export declare class OwnedUiSessionShell {
35
37
  restoreQueuedInput(): void;
36
38
  showSelector(title: string, options: readonly PiShellSelectorOption[], onSelect: (id: string) => void, onCancel?: () => void): PiTuiOverlayHandle;
37
39
  showThinkingSelector(): void;
40
+ /** The Skills dialog: a replacement input like the model selector, applying the chosen skill through the prompt path. */
41
+ showSkillsSelector(skills?: readonly PiShellSkillSummary[]): void;
38
42
  showModelSelector(initialSearchInput?: string): void;
39
43
  showForkSelector(): void;
40
44
  showLogoutSelector(): Promise<void>;
@@ -48,7 +52,10 @@ export declare class OwnedUiSessionShell {
48
52
  runWorkflow(request: PiWorkflowRequest): Promise<AdapterCommandResult>;
49
53
  showTrustSelector(): void;
50
54
  showScopedModelsSelector(): void;
55
+ /** The bare-A1 unified Models dialog: switch on Enter, scope on Space, persist on Ctrl+S, all through the engine. */
56
+ showModelsDialog(initialQuery?: string): void;
51
57
  dispose(): Promise<void>;
58
+ hotkeysPresentation(): PiShellHotkeysPresentation;
52
59
  }
53
60
  export interface SessionResumeCommandMetadata {
54
61
  readonly sessionId: string;
@@ -9,12 +9,14 @@ import { assertPromptImages, ImageAttachmentError } from "../../contracts/owned-
9
9
  import { ContextualPromptSuggestionController } from "./prompt-suggestion-controller.js";
10
10
  import { MOUSE_TRACKING_OFF, MOUSE_TRACKING_ON, parseMouseInput } from "../../ui/components/mouse.js";
11
11
  import { readVisibleHyperlinks } from "../../ui/components/visible-hyperlinks.js";
12
- import { PINNED_PI_HIDDEN_COMMAND_NAMES, PINNED_PI_WORKFLOW_COMMAND_NAMES } from "../../integrations/pi/engine/workflows.js";
12
+ import { PINNED_PI_HIDDEN_COMMAND_NAMES, workflowCommandNames } from "../../integrations/pi/engine/workflows.js";
13
13
  import { createPiExtensionUiBridge } from "../../integrations/pi/components/shell-extension-ui.js";
14
14
  import { createPiShellEditor } from "../../integrations/pi/components/shell-editor-autocomplete.js";
15
+ import { SKILLS_COMMAND_NAME, findSkillByArgument, rewriteSkillsTunnelSubmission, skillPrompt, skillsFromCommands, } from "../../integrations/pi/components/skills-command.js";
15
16
  import { createPiQueuedInputStatus, createPiShellFooter, createPiShellHeader, createPiShellLoadedResources, createPiShellStatus, } from "../../integrations/pi/components/shell-footer-status.js";
16
- import { createPiShellArmin, createPiShellAuthProviderSelector, createPiShellDaxnuts, createPiShellDialog, createPiShellEarendilAnnouncement, createPiShellExtensionSelector, createPiShellLoginDialog, createPiShellModelSelector, createPiShellOperationLoader, createPiShellReloadBox, createPiShellScopedModelsSelector, createPiShellSelector, createPiShellSessionSelector, createPiShellSettingsSelector, createPiShellThinkingSelector, createPiShellTreeSelector, createPiShellTrustSelector, createPiShellUserMessageSelector, } from "../../integrations/pi/components/shell-selectors-dialogs.js";
17
+ import { createPiShellArmin, createPiShellAuthProviderSelector, createPiShellDaxnuts, createPiShellDialog, createPiShellEarendilAnnouncement, createPiShellExtensionSelector, createPiShellLoginDialog, createPiShellModelSelector, createPiShellModelsDialog, createPiShellOperationLoader, createPiShellReloadBox, createPiShellScopedModelsSelector, createPiShellSelector, createPiShellSessionSelector, createPiShellSettingsSelector, createPiShellSkillsSelector, createPiShellThinkingSelector, createPiShellTreeSelector, createPiShellTrustSelector, createPiShellUserMessageSelector, } from "../../integrations/pi/components/shell-selectors-dialogs.js";
17
18
  import { createPiShellChangelog, createPiShellHotkeys, createPiShellSessionInfo, renderPiShellStatusText, } from "../../integrations/pi/components/shell-presenters-info.js";
19
+ import { CHANGELOG_ROUTE } from "../../features/owned-ui/reference-routes.js";
18
20
  import { createPiShellTranscriptComponent, renderPiShellPackageUpdateNotice, renderPiShellStartupDiagnostic, renderPiShellTranscriptBlock, } from "../../integrations/pi/components/shell-presenters-transcript.js";
19
21
  import { onPiThemeChange, piTheme } from "../../integrations/pi/components/upstream/theme/theme.js";
20
22
  import { DamageAwareTerminalAdapter } from "../../integrations/pi/tui-runtime/damage-aware-terminal.js";
@@ -35,6 +37,9 @@ export class OwnedUiSessionShell {
35
37
  #unsubscribe;
36
38
  #unsubscribePromptSuggestions;
37
39
  #promptSuggestions;
40
+ #skills;
41
+ #unsubscribeSkills;
42
+ #installedCommandSignature = "";
38
43
  #promptHistory = null;
39
44
  #promptHistoryImageSidecar;
40
45
  #extensionBridge;
@@ -51,6 +56,8 @@ export class OwnedUiSessionShell {
51
56
  #shutdownPromise;
52
57
  #disposePromise;
53
58
  #pointerReporting = false;
59
+ // Invariant: the startup release notes open at most once per shell, whatever the view does later.
60
+ #startupChangelogHandled = false;
54
61
  #customViewport;
55
62
  #responseCopy;
56
63
  // Rationale: only an executor this shell created owns a spare copy helper worth warming and disposing.
@@ -96,6 +103,8 @@ export class OwnedUiSessionShell {
96
103
  let streamPresentation;
97
104
  let pendingClipboardWrite = Promise.resolve();
98
105
  let promptSuggestionController = null;
106
+ // Invariant: the collapsed skills presentation is a bare-A1 replacement; comparison profiles keep the pinned list.
107
+ this.#skills = this.#customViewport ? options.skills ?? null : null;
99
108
  const terminalCopy = terminal !== undefined || hasAsyncClipboardOutput();
100
109
  this.#responseCopy = this.#customViewport ? new ResponseCopyCoordinator({
101
110
  execute: responseCopy?.execute ?? (this.#copyExecutor = createResponseCopyExecutor({
@@ -160,7 +169,7 @@ export class OwnedUiSessionShell {
160
169
  onInterrupt: () => { void this.interrupt(); },
161
170
  onClear: () => { void this.clearOrExit(); },
162
171
  onExit: () => { void this.shutdown(); },
163
- onModelSelect: () => this.showModelSelector(),
172
+ onModelSelect: () => this.#customViewport ? this.showModelsDialog() : this.showModelSelector(),
164
173
  onModelCycle: direction => { void this.cycleModel(direction); },
165
174
  onThinkingCycle: () => { void this.cycleThinkingLevel(); },
166
175
  onThinkingToggle: () => {
@@ -170,7 +179,7 @@ export class OwnedUiSessionShell {
170
179
  onMessageCopy: () => { void this.runWorkflow({ command: "copy", argument: "" }); },
171
180
  onFollowUp: () => { void this.queueFollowUp().catch(() => this.#reportSubmissionError()); },
172
181
  onDequeue: () => this.restoreQueuedInput(),
173
- onEditorChange: () => { this.#editorRevision++; promptSuggestionController?.invalidate(); },
182
+ onEditorChange: () => { this.#editorRevision++; promptSuggestionController?.abortPending(); },
174
183
  onPromptSuggestionAccepted: () => promptSuggestionController?.accept(),
175
184
  onInputSurfaceChanged: () => {
176
185
  promptSuggestionController?.invalidate();
@@ -207,6 +216,7 @@ export class OwnedUiSessionShell {
207
216
  },
208
217
  ...(pasteDiagnostics === undefined ? {} : { pasteDiagnostics: pasteDiagnostics }),
209
218
  ...(pastePreparation === undefined ? {} : { pastePreparation }),
219
+ skillsPresentation: () => this.#skills?.presentation() ?? "expand",
210
220
  }, {
211
221
  ...startup,
212
222
  resources: startup?.resources ?? shellResourceEntries(this.backend),
@@ -287,6 +297,15 @@ export class OwnedUiSessionShell {
287
297
  this.#unsubscribePromptSuggestions = promptSuggestionOptions === undefined
288
298
  ? () => { }
289
299
  : promptSuggestionOptions.onChange(enabled => this.#promptSuggestions?.setEnabled(enabled));
300
+ // Rationale: the same listener refreshes the menu for the A1 presentation choice and for the engine's
301
+ // skill-command registration, both of which the owned settings manager reports through one change.
302
+ this.#unsubscribeSkills = this.#skills === null ? () => { } : this.#skills.onChange(() => {
303
+ // Invariant: reinstalling drops extension provider wrappers, so an unrelated setting change leaves the list alone.
304
+ if (this.#disposed || this.#commandListSignature() === this.#installedCommandSignature)
305
+ return;
306
+ this.#installAutocompleteCommands();
307
+ this.runtime.requestRender();
308
+ });
290
309
  const initialPiSettings = this.backend.pinnedSettingsSnapshot();
291
310
  this.runtime.setHardwareCursor(initialPiSettings.showHardwareCursor);
292
311
  this.runtime.setClearOnShrink(initialPiSettings.clearOnShrink);
@@ -453,7 +472,7 @@ export class OwnedUiSessionShell {
453
472
  },
454
473
  finishLogin: () => this.#finishWorkflowLogin(),
455
474
  });
456
- this.root.editor.setAutocompleteCommands(this.backend.workflowAutocompleteCommands());
475
+ this.#installAutocompleteCommands();
457
476
  this.#unsubscribe = this.backend.onEvent(event => {
458
477
  // Performance: a streamed chunk names one block, and touching only that block is what keeps the
459
478
  // cost of a chunk the same in a long session as in a new one. Everything else
@@ -669,7 +688,7 @@ export class OwnedUiSessionShell {
669
688
  }, displayInput);
670
689
  }
671
690
  async clearOrExit(now = Date.now()) {
672
- this.#promptSuggestions?.invalidate();
691
+ this.#promptSuggestions?.abortPending();
673
692
  if (now - this.#lastClearTime < 500)
674
693
  return this.shutdown();
675
694
  this.root.editor.setText("");
@@ -834,6 +853,23 @@ export class OwnedUiSessionShell {
834
853
  this.root.setInputSurface(component);
835
854
  this.runtime.requestRender();
836
855
  }
856
+ /** The Skills dialog: a replacement input like the model selector, applying the chosen skill through the prompt path. */
857
+ showSkillsSelector(skills = this.#skillSummaries()) {
858
+ const close = () => {
859
+ this.root.setInputSurface(null);
860
+ this.runtime.requestRender();
861
+ };
862
+ const component = createPiShellSkillsSelector({
863
+ skills,
864
+ onSelect: name => {
865
+ close();
866
+ void this.#submitSkillPrompt(skillPrompt(name), skillPrompt(name)).catch(() => this.#reportSubmissionError());
867
+ },
868
+ onCancel: close,
869
+ });
870
+ this.root.setInputSurface(component);
871
+ this.runtime.requestRender();
872
+ }
837
873
  showModelSelector(initialSearchInput) {
838
874
  const context = this.backend.pinnedModelSelectorContext();
839
875
  const close = () => {
@@ -1139,6 +1175,10 @@ export class OwnedUiSessionShell {
1139
1175
  this.showScopedModelsSelector();
1140
1176
  return { outcome: "completed", diagnostic: null };
1141
1177
  }
1178
+ if (request.command === "models" && request.selection === undefined) {
1179
+ this.showModelsDialog(request.argument.trim() || undefined);
1180
+ return { outcome: "completed", diagnostic: null };
1181
+ }
1142
1182
  if (request.command === "model" && request.selection === undefined && request.confirmed === undefined && request.argument.trim().length === 0) {
1143
1183
  this.showModelSelector();
1144
1184
  return { outcome: "completed", diagnostic: null };
@@ -1247,10 +1287,10 @@ export class OwnedUiSessionShell {
1247
1287
  if (request.command === "reload" && result.outcome === "completed") {
1248
1288
  this.root.resetWorkflowPresentation();
1249
1289
  this.root.editor.reloadKeybindings();
1250
- this.root.editor.setAutocompleteCommands(this.backend.workflowAutocompleteCommands());
1290
+ this.#installAutocompleteCommands();
1251
1291
  }
1252
1292
  this.root.appendWorkflowResult(result);
1253
- if (request.command === "model" && result.outcome === "completed")
1293
+ if ((request.command === "model" || request.command === "models") && result.outcome === "completed")
1254
1294
  this.#showDaxnutsForActiveModel();
1255
1295
  this.runtime.requestRender();
1256
1296
  return workflowAdapterResult(result);
@@ -1357,6 +1397,90 @@ export class OwnedUiSessionShell {
1357
1397
  this.runtime.requestRender();
1358
1398
  }).finally(() => clearTimeout(timeout));
1359
1399
  }
1400
+ // Invariant: bare A1 routes `models`; the comparison profile keeps the pinned `model`/`scoped-models` pair. Hidden routes are shared.
1401
+ #isWorkflowRoute(value) {
1402
+ return workflowCommandNames(this.#customViewport ? "bare" : "comparison").includes(value)
1403
+ || PINNED_PI_HIDDEN_COMMAND_NAMES.includes(value);
1404
+ }
1405
+ /** The bare-A1 unified Models dialog: switch on Enter, scope on Space, persist on Ctrl+S, all through the engine. */
1406
+ showModelsDialog(initialQuery) {
1407
+ const context = this.backend.modelsContext();
1408
+ const available = new Set(context.models.map(model => `${model.provider}/${model.id}`));
1409
+ const savedScopeIds = context.persistedScopeIds.filter(id => available.has(id));
1410
+ // Invariant: an explicit session scope wins; otherwise the dialog starts from what is persisted, never from "all rows scoped".
1411
+ const scopeIds = context.sessionScopeIds.length > 0 ? context.sessionScopeIds : savedScopeIds;
1412
+ let disposed = false;
1413
+ let timedOut = false;
1414
+ const controller = new AbortController();
1415
+ const timeout = setTimeout(() => {
1416
+ timedOut = true;
1417
+ controller.abort();
1418
+ }, 15_000);
1419
+ const close = () => {
1420
+ disposed = true;
1421
+ clearTimeout(timeout);
1422
+ controller.abort();
1423
+ this.root.setInputSurface(null);
1424
+ this.runtime.requestRender();
1425
+ };
1426
+ const dialog = createPiShellModelsDialog({
1427
+ models: context.models,
1428
+ activeModelId: context.activeModelId,
1429
+ scopeIds,
1430
+ savedScopeIds,
1431
+ ...(initialQuery === undefined ? {} : { initialQuery }),
1432
+ refreshStatus: "Refreshing model catalogs…",
1433
+ requestRender: () => this.runtime.requestRender(),
1434
+ onSelect: modelId => {
1435
+ void this.runWorkflow({ command: "models", argument: "", selection: modelId }).then(result => {
1436
+ if (!disposed && result.outcome === "completed")
1437
+ close();
1438
+ });
1439
+ },
1440
+ onScopeChange: ids => {
1441
+ this.backend.setSessionModelScope(ids);
1442
+ this.runtime.requestRender();
1443
+ },
1444
+ onSave: ids => {
1445
+ try {
1446
+ this.backend.persistModelScope(ids);
1447
+ }
1448
+ catch (error) {
1449
+ this.root.appendWorkflowResult({ command: "models", outcome: "failed", message: error instanceof Error ? error.message : String(error) });
1450
+ this.runtime.requestRender();
1451
+ throw error;
1452
+ }
1453
+ this.root.appendWorkflowStatus("Model selection saved to settings");
1454
+ this.runtime.requestRender();
1455
+ },
1456
+ onCancel: close,
1457
+ });
1458
+ const component = {
1459
+ ...dialog,
1460
+ dispose: () => {
1461
+ disposed = true;
1462
+ clearTimeout(timeout);
1463
+ controller.abort();
1464
+ dialog.dispose?.();
1465
+ },
1466
+ };
1467
+ this.root.setInputSurface(component);
1468
+ this.runtime.requestRender();
1469
+ void this.backend.refreshModels(controller.signal).then(refreshed => {
1470
+ if (disposed)
1471
+ return;
1472
+ component.updateModels(refreshed.models);
1473
+ component.setRefreshStatus(timedOut ? "Model refresh timed out; showing cached models." : refreshed.status, timedOut ? "warning" : refreshed.statusKind);
1474
+ this.runtime.requestRender();
1475
+ }).catch(error => {
1476
+ if (disposed)
1477
+ return;
1478
+ component.setRefreshStatus(timedOut
1479
+ ? "Model refresh timed out; showing cached models."
1480
+ : `Could not refresh model catalogs: ${error instanceof Error ? error.message : String(error)}`, "warning");
1481
+ this.runtime.requestRender();
1482
+ }).finally(() => clearTimeout(timeout));
1483
+ }
1360
1484
  #showDaxnutsForActiveModel() {
1361
1485
  const model = this.view().activeModel;
1362
1486
  if (model?.providerId === "opencode" && model.modelId.toLowerCase().includes("kimi-k2.5")) {
@@ -1409,6 +1533,7 @@ export class OwnedUiSessionShell {
1409
1533
  attempt(() => this.#streamPresentation.dispose());
1410
1534
  attempt(() => this.#promptSuggestions?.dispose());
1411
1535
  attempt(() => this.#unsubscribePromptSuggestions());
1536
+ attempt(() => this.#unsubscribeSkills());
1412
1537
  attempt(() => this.#unsubscribeSettings());
1413
1538
  let fullscreenExitText = "";
1414
1539
  attempt(() => {
@@ -1453,11 +1578,13 @@ export class OwnedUiSessionShell {
1453
1578
  if (!this.runtime.active || this.runtime.mode !== "fullscreen")
1454
1579
  return null;
1455
1580
  try {
1456
- const { enabled, effect, durationMs } = outro.snapshot();
1457
- if (!enabled)
1581
+ if (!outro.snapshot().enabled)
1458
1582
  return null;
1459
1583
  const viewport = this.runtime.viewport();
1460
- return { rows: this.#damageTerminal.presentedRows(), columns: viewport.columns, height: viewport.rows, settings: { effect, durationMs } };
1584
+ return {
1585
+ rows: this.#damageTerminal.presentedRows(), columns: viewport.columns, height: viewport.rows,
1586
+ settings: { effect: QUIT_OUTRO_EFFECT, durationMs: QUIT_OUTRO_DURATION_MS },
1587
+ };
1461
1588
  }
1462
1589
  catch {
1463
1590
  return null;
@@ -1550,13 +1677,31 @@ export class OwnedUiSessionShell {
1550
1677
  }
1551
1678
  this.root.update(view);
1552
1679
  this.#syncDialog(view.dialog);
1680
+ this.#presentStartupChangelog(view);
1553
1681
  this.runtime.requestRender();
1554
1682
  for (const listener of this.#listeners)
1555
1683
  listener(view);
1556
1684
  return view;
1557
1685
  }
1558
- #openOwnedRoute(route) {
1559
- const surface = this.#routeHost?.open(route) ?? null;
1686
+ // Rationale: the engine has already stored the current version when the diagnostic arrives, so the
1687
+ // entries newer than the last acknowledged one exist only in its message; the screen shows exactly those.
1688
+ #presentStartupChangelog(view) {
1689
+ if (this.#startupChangelogHandled || !this.#customViewport || this.#routeHost === null)
1690
+ return;
1691
+ const diagnostic = view.diagnostics.find(candidate => candidate.code === "changelog-expanded");
1692
+ // Invariant: a view synchronized before the runtime starts is not the moment of arrival; start() replays it.
1693
+ if (diagnostic === undefined || !this.runtime.active)
1694
+ return;
1695
+ this.#startupChangelogHandled = true;
1696
+ // Invariant: a dialog, selector, or owned screen already up keeps the screen; the hint stays in the feed.
1697
+ if (this.#dialogHandle !== undefined || !this.root.usesDefaultInputSurface())
1698
+ return;
1699
+ if (!this.#routeHost.claims(CHANGELOG_ROUTE))
1700
+ return;
1701
+ this.#openOwnedRoute(CHANGELOG_ROUTE, { document: diagnostic.message });
1702
+ }
1703
+ #openOwnedRoute(route, input) {
1704
+ const surface = this.#routeHost?.open(route, input) ?? null;
1560
1705
  if (surface === null)
1561
1706
  return { outcome: "failed", diagnostic: `route is unavailable: ${route}` };
1562
1707
  if (!this.runtime.active)
@@ -1683,6 +1828,9 @@ export class OwnedUiSessionShell {
1683
1828
  });
1684
1829
  this.#dialogId = dialog.id;
1685
1830
  }
1831
+ hotkeysPresentation() {
1832
+ return this.root.hotkeysPresentation();
1833
+ }
1686
1834
  async #slashCommand(text) {
1687
1835
  const body = text.slice(1).trim();
1688
1836
  const separator = body.search(/\s/);
@@ -1690,8 +1838,15 @@ export class OwnedUiSessionShell {
1690
1838
  const argument = separator < 0 ? "" : body.slice(separator + 1).trimStart();
1691
1839
  if (this.#routeHost?.claims(name))
1692
1840
  return this.#openOwnedRoute(name);
1693
- if (isWorkflowRoute(name))
1841
+ if (this.#isWorkflowRoute(name))
1694
1842
  return this.runWorkflow({ command: name, argument });
1843
+ if (this.#skillsCollapsed()) {
1844
+ if (name === SKILLS_COMMAND_NAME)
1845
+ return this.#runSkillsCommand(text, argument);
1846
+ const rewritten = rewriteSkillsTunnelSubmission(text);
1847
+ if (rewritten !== text)
1848
+ return this.#submitSkillPrompt(rewritten, text);
1849
+ }
1695
1850
  // Compatibility: unknown slash input, prompt templates, skills, and extension commands remain Pi prompt input.
1696
1851
  this.#rememberInput(text, "slash");
1697
1852
  this.root.resumeViewportFollowing();
@@ -1702,6 +1857,51 @@ export class OwnedUiSessionShell {
1702
1857
  text,
1703
1858
  });
1704
1859
  }
1860
+ #installAutocompleteCommands() {
1861
+ this.root.editor.setAutocompleteCommands(this.backend.workflowAutocompleteCommands());
1862
+ this.#installedCommandSignature = this.#commandListSignature();
1863
+ }
1864
+ #commandListSignature() {
1865
+ return JSON.stringify([this.#skills?.presentation() ?? "expand", this.backend.disposed ? [] : this.backend.workflowAutocompleteCommands().map(command => command.name)]);
1866
+ }
1867
+ // Invariant: collapse applies only while the engine registers skill commands; otherwise there is nothing to collapse.
1868
+ #skillsCollapsed() {
1869
+ return this.#skills !== null && this.#skills.presentation() === "collapse"
1870
+ && !this.backend.disposed && this.backend.pinnedSettingsSnapshot().enableSkillCommands;
1871
+ }
1872
+ #skillSummaries() {
1873
+ return skillsFromCommands(this.backend.workflowAutocompleteCommands());
1874
+ }
1875
+ // Protocol: bare "/skills" opens the dialog, "/skills <name> [args]" applies directly, and an unknown name is a command outcome.
1876
+ async #runSkillsCommand(text, argument) {
1877
+ const skills = this.#skillSummaries();
1878
+ const trimmed = argument.trim();
1879
+ if (trimmed.length === 0) {
1880
+ this.showSkillsSelector(skills);
1881
+ return { outcome: "completed", diagnostic: null };
1882
+ }
1883
+ const separator = trimmed.search(/\s/u);
1884
+ const token = separator < 0 ? trimmed : trimmed.slice(0, separator);
1885
+ const skill = findSkillByArgument(skills, token);
1886
+ if (skill === undefined) {
1887
+ const message = "Unknown skill: " + token;
1888
+ this.root.appendWorkflowMessage({ kind: "error", message });
1889
+ this.runtime.requestRender();
1890
+ return { outcome: "failed", diagnostic: message };
1891
+ }
1892
+ return this.#submitSkillPrompt(skillPrompt(skill.name, separator < 0 ? "" : trimmed.slice(separator + 1)), text);
1893
+ }
1894
+ // Rationale: the engine expands "/skill:<name> args" itself; history keeps the form the user typed so recall restores the invocation.
1895
+ #submitSkillPrompt(prompt, typed) {
1896
+ this.#rememberInput(typed, "slash");
1897
+ this.root.resumeViewportFollowing();
1898
+ return this.#execute({
1899
+ type: this.view().lifecycle === "busy" ? "steer" : "prompt",
1900
+ correlationId: this.#correlation("prompt-command"),
1901
+ sessionId: this.backend.sessionId,
1902
+ text: prompt,
1903
+ });
1904
+ }
1705
1905
  async #completeTreeSelection(entryId, skipSummaryPrompt) {
1706
1906
  let summarize = false;
1707
1907
  let customInstructions;
@@ -1911,7 +2111,6 @@ function workflowAdapterResult(result) {
1911
2111
  const INTERRUPT = "\u0003";
1912
2112
  const INTERRUPT_CHORD_MS = 1_500;
1913
2113
  const RELOAD_SURFACE_MIN_VISIBLE_MS = 400;
1914
- function isWorkflowRoute(value) {
1915
- return PINNED_PI_WORKFLOW_COMMAND_NAMES.includes(value)
1916
- || PINNED_PI_HIDDEN_COMMAND_NAMES.includes(value);
1917
- }
2114
+ // Rationale: the outro is not configurable; the switch only decides whether this plan plays.
2115
+ const QUIT_OUTRO_EFFECT = "fall";
2116
+ const QUIT_OUTRO_DURATION_MS = 800;
@@ -1,2 +1,2 @@
1
1
  export { composeOwnedUi, composeOwnedUiApplication, type OwnedUiComposition, type OwnedUiCompositionOptions, } from "./owned-ui.js";
2
- export { createOwnedRouteHost } from "./settings-route-host.js";
2
+ export { createOwnedRouteHost, type OwnedReferenceProviders, type OwnedReferenceRows } from "./settings-route-host.js";