@timurproko/a1 0.2.7-dev.1255 → 0.2.7-dev.1265

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 (81) hide show
  1. package/dist/app/session-shell/prompt-submission.d.ts +1 -1
  2. package/dist/app/session-shell/prompt-submission.js +9 -9
  3. package/dist/app/session-shell/resource-labels.d.ts +2 -0
  4. package/dist/app/session-shell/resource-labels.js +158 -0
  5. package/dist/app/session-shell/session-prompt-history.d.ts +29 -0
  6. package/dist/app/session-shell/session-prompt-history.js +80 -0
  7. package/dist/app/session-shell/session-selectors.d.ts +46 -0
  8. package/dist/app/session-shell/session-selectors.js +269 -0
  9. package/dist/app/session-shell/session-shell-root.d.ts +6 -28
  10. package/dist/app/session-shell/session-shell-root.js +26 -220
  11. package/dist/app/session-shell/session-shell.d.ts +3 -23
  12. package/dist/app/session-shell/session-shell.js +70 -388
  13. package/dist/app/session-shell/session-terminal-presentation.d.ts +16 -0
  14. package/dist/app/session-shell/session-terminal-presentation.js +35 -0
  15. package/dist/app/session-shell/session-viewport-controller.d.ts +2 -10
  16. package/dist/app/session-shell/session-viewport-controller.js +40 -318
  17. package/dist/app/session-shell/settings-bindings.d.ts +1 -1
  18. package/dist/app/session-shell/settings-bindings.js +2 -2
  19. package/dist/app/session-shell/viewport-pointer.d.ts +83 -0
  20. package/dist/app/session-shell/viewport-pointer.js +361 -0
  21. package/dist/composition/owned-ui.js +63 -59
  22. package/dist/contracts/owned-ui/index.d.ts +1 -1
  23. package/dist/contracts/owned-ui/index.js +1 -1
  24. package/dist/contracts/owned-ui/validation.d.ts +1 -11
  25. package/dist/contracts/owned-ui/validation.js +8 -104
  26. package/dist/contracts/presentation/index.d.ts +1 -1
  27. package/dist/contracts/presentation/terminal.d.ts +0 -6
  28. package/dist/features/owned-ui/settings-app.js +13 -208
  29. package/dist/features/owned-ui/settings-model-dialog.d.ts +41 -0
  30. package/dist/features/owned-ui/settings-model-dialog.js +194 -0
  31. package/dist/foundation/release/automatic-update.js +1 -8
  32. package/dist/foundation/release/dependency-layer.js +3 -3
  33. package/dist/foundation/release/latest-release.js +1 -1
  34. package/dist/foundation/release/managed-path.d.ts +10 -0
  35. package/dist/foundation/release/managed-path.js +26 -0
  36. package/dist/foundation/release/release-gc.js +6 -8
  37. package/dist/foundation/release/restart-certification.js +3 -4
  38. package/dist/foundation/release/update-activation.js +1 -1
  39. package/dist/foundation/release/update-execution-lease.js +1 -7
  40. package/dist/foundation/release/update-installer.d.ts +45 -0
  41. package/dist/foundation/release/update-installer.js +57 -0
  42. package/dist/foundation/release/update-recovery.js +2 -9
  43. package/dist/foundation/release/update.d.ts +4 -23
  44. package/dist/foundation/release/update.js +160 -179
  45. package/dist/integrations/pi/components/bracketed-paste.d.ts +33 -0
  46. package/dist/integrations/pi/components/bracketed-paste.js +123 -0
  47. package/dist/integrations/pi/components/owned-editor-ux.js +46 -140
  48. package/dist/integrations/pi/engine/adapter.d.ts +1 -8
  49. package/dist/integrations/pi/engine/adapter.js +6 -49
  50. package/dist/integrations/pi/engine/host.js +1 -1
  51. package/dist/integrations/pi/engine/repository-metadata.d.ts +49 -0
  52. package/dist/integrations/pi/engine/repository-metadata.js +189 -0
  53. package/dist/integrations/pi/engine/session-events.js +8 -6
  54. package/dist/integrations/pi/engine/session-runtime.d.ts +6 -12
  55. package/dist/integrations/pi/engine/session-runtime.js +21 -178
  56. package/dist/integrations/pi/engine/view-state.d.ts +21 -2
  57. package/dist/integrations/pi/engine/view-state.js +129 -1
  58. package/dist/integrations/pi/engine/workflow-runner.d.ts +0 -1
  59. package/dist/integrations/pi/engine/workflow-runner.js +254 -365
  60. package/dist/integrations/pi/engine/workflow-share.d.ts +9 -0
  61. package/dist/integrations/pi/engine/workflow-share.js +168 -0
  62. package/dist/integrations/pi/engine/workflow-support.d.ts +0 -14
  63. package/dist/integrations/pi/engine/workflow-support.js +0 -47
  64. package/dist/integrations/pi/tui-runtime/adapter.d.ts +9 -19
  65. package/dist/integrations/pi/tui-runtime/adapter.js +35 -144
  66. package/dist/integrations/pi/tui-runtime/damage-aware-terminal.d.ts +4 -4
  67. package/dist/integrations/pi/tui-runtime/index.d.ts +0 -1
  68. package/dist/integrations/pi/tui-runtime/overlay-geometry.d.ts +2 -2
  69. package/dist/integrations/pi/tui-runtime/presentation-adapter.d.ts +2 -2
  70. package/dist/ui/components/transcript-viewport-frame.d.ts +120 -0
  71. package/dist/ui/components/transcript-viewport-frame.js +314 -0
  72. package/dist/ui/components/transcript-viewport-types.d.ts +147 -0
  73. package/dist/ui/components/transcript-viewport.d.ts +2 -147
  74. package/dist/ui/components/transcript-viewport.js +87 -315
  75. package/package.json +3 -3
  76. package/dist/integrations/pi/engine/diagnostics-state.d.ts +0 -9
  77. package/dist/integrations/pi/engine/diagnostics-state.js +0 -34
  78. package/dist/integrations/pi/engine/usage-view.d.ts +0 -4
  79. package/dist/integrations/pi/engine/usage-view.js +0 -56
  80. package/dist/integrations/pi/tui-runtime/contracts.d.ts +0 -22
  81. /package/dist/{integrations/pi/tui-runtime/contracts.js → ui/components/transcript-viewport-types.js} +0 -0
@@ -6,7 +6,7 @@ import type { OwnedUiSessionShellRoot } from "./session-shell-root.js";
6
6
  import type { OwnedUiPresenterTerminal } from "./shell-host.js";
7
7
  interface PromptSubmissionPorts {
8
8
  readonly backend: Pick<OwnedUiSessionBackend, "session" | "identity" | "workflows">;
9
- readonly root: Pick<OwnedUiSessionShellRoot, "editor" | "hasPendingPastes" | "waitForPromptPastes" | "setExtensionWidget" | "prepareHistoryText" | "imageChipAttachmentsFromEditor" | "preparePromptSubmission" | "omitUnsentPromptImages" | "resumeViewportFollowing" | "beginImageSubmissionStatus" | "workflow">;
9
+ readonly root: Pick<OwnedUiSessionShellRoot, "editor" | "promptChips" | "viewport" | "waitForPromptPastes" | "setExtensionWidget" | "beginImageSubmissionStatus" | "workflow">;
10
10
  readonly terminal: Pick<OwnedUiPresenterTerminal, "requestRender">;
11
11
  readonly cwd: string;
12
12
  readonly statusText: OwnedUiShellComponents["info"]["statusText"];
@@ -42,7 +42,7 @@ export class PromptSubmission {
42
42
  /** Sends slash text the shell does not own as engine prompt input; history keeps what was typed. */
43
43
  sendSlashPrompt(text, typed = text) {
44
44
  this.#rememberInput(typed, "slash");
45
- this.#ports.root.resumeViewportFollowing();
45
+ this.#ports.root.viewport.resumeFollowing();
46
46
  return this.execute({
47
47
  type: this.#ports.lifecycle() === "busy" ? "steer" : "prompt",
48
48
  correlationId: this.#ports.correlation("prompt-command"),
@@ -96,7 +96,7 @@ export class PromptSubmission {
96
96
  const previous = this.#waiting.get(draft);
97
97
  if (previous !== undefined)
98
98
  return previous.result;
99
- if (!root.hasPendingPastes(draft))
99
+ if (!root.promptChips.hasPending(draft))
100
100
  return this.#guard(draft, action);
101
101
  const controller = new AbortController();
102
102
  const generation = backend.identity.sessionGeneration;
@@ -154,7 +154,7 @@ export class PromptSubmission {
154
154
  const history = this.#ports.promptHistory();
155
155
  if (history === null)
156
156
  return;
157
- const reusable = root.prepareHistoryText(text);
157
+ const reusable = root.promptChips.prepareHistoryText(text);
158
158
  if (reusable.length === 0) {
159
159
  history.rememberRecovery(text);
160
160
  return;
@@ -164,7 +164,7 @@ export class PromptSubmission {
164
164
  // recall path treats an absent sidecar the same as a corrupt one.
165
165
  const sidecar = this.#ports.imageSidecar();
166
166
  if (sidecar !== undefined) {
167
- for (const record of root.imageChipAttachmentsFromEditor(text)) {
167
+ for (const record of root.promptChips.imageChipAttachments(text)) {
168
168
  sidecar.write(record.id, { tag: record.tag, data: record.image.data, mimeType: record.image.mimeType, savedAt: new Date().toISOString() });
169
169
  }
170
170
  }
@@ -178,7 +178,7 @@ export class PromptSubmission {
178
178
  if (displayInput.startsWith("/"))
179
179
  return this.#ports.slashCommand(displayInput);
180
180
  const { root, backend } = this.#ports;
181
- const prepared = root.preparePromptSubmission(displayInput);
181
+ const prepared = root.promptChips.prepareSubmission(displayInput);
182
182
  assertPromptImages(prepared.images);
183
183
  const input = prepared.text.trim();
184
184
  if (input.startsWith("!")) {
@@ -189,8 +189,8 @@ export class PromptSubmission {
189
189
  // Compatibility: match interactive Pi: input during compaction is queued steering; the engine
190
190
  // shows it in the pending rows and delivers it when compaction ends.
191
191
  const type = this.#ports.lifecycle() === "busy" ? "steer" : "prompt";
192
- this.#rememberInput(root.omitUnsentPromptImages(displayInput).trim(), type);
193
- root.resumeViewportFollowing();
192
+ this.#rememberInput(root.promptChips.omitUnsentImages(displayInput).trim(), type);
193
+ root.viewport.resumeFollowing();
194
194
  const command = {
195
195
  type,
196
196
  correlationId: this.#ports.correlation(type),
@@ -237,13 +237,13 @@ export class PromptSubmission {
237
237
  const displayInput = draft.trim();
238
238
  if (!displayInput)
239
239
  return rejected("nothing to queue");
240
- const prepared = root.preparePromptSubmission(displayInput);
240
+ const prepared = root.promptChips.prepareSubmission(displayInput);
241
241
  assertPromptImages(prepared.images);
242
242
  const text = prepared.text.trim();
243
243
  this.#rememberInput(displayInput, "follow-up");
244
244
  if (root.editor.getText() === draft)
245
245
  root.editor.setText("");
246
- root.resumeViewportFollowing();
246
+ root.viewport.resumeFollowing();
247
247
  return this.execute({
248
248
  type: "follow-up",
249
249
  correlationId: this.#ports.correlation("follow-up"),
@@ -0,0 +1,2 @@
1
+ import type { OwnedUiSessionBackend, OwnedUiShellResourceEntry } from "../../contracts/owned-ui/index.js";
2
+ export declare function shellResourceEntries(backend: OwnedUiSessionBackend): readonly OwnedUiShellResourceEntry[];
@@ -0,0 +1,158 @@
1
+ // Rationale: the startup header lists loaded resources with the shortest label that stays unambiguous.
2
+ export function shellResourceEntries(backend) {
3
+ const resources = backend.extensions.nonVisualResources().map(resource => ({
4
+ section: resource.kind === "skill"
5
+ ? "Skills"
6
+ : resource.kind === "prompt-template"
7
+ ? "Prompts"
8
+ : resource.kind === "theme"
9
+ ? "Themes"
10
+ : "Context",
11
+ label: resource.kind === "prompt-template"
12
+ ? `/${resource.label}`
13
+ : resource.kind === "agent-context" || resource.kind === "system-prompt"
14
+ ? compactResourceLabel(resource.sourcePath ?? resource.label)
15
+ : resource.label,
16
+ sourcePath: resource.sourcePath,
17
+ diagnostic: resource.diagnostic,
18
+ }));
19
+ const extensions = backend.extensions.extensionResources().filter(extension => !extension.hidden);
20
+ const loadedExtensions = extensions.filter(extension => extension.diagnostic === null);
21
+ const extensionLabels = compactExtensionLabels(loadedExtensions);
22
+ for (const extension of extensions) {
23
+ const labelIndex = loadedExtensions.indexOf(extension);
24
+ resources.push({
25
+ section: "Extensions",
26
+ label: extensionLabels[labelIndex] ?? compactResourceLabel(extension.sourcePath ?? extension.resolvedPath ?? "extension"),
27
+ sourcePath: extension.sourcePath ?? extension.resolvedPath,
28
+ diagnostic: extension.diagnostic,
29
+ });
30
+ }
31
+ return resources;
32
+ }
33
+ function compactResourceLabel(path) {
34
+ const segments = compactPathSegments(path);
35
+ const leaf = segments.at(-1) ?? path;
36
+ if ((leaf === "index.ts" || leaf === "index.js") && segments.length > 1)
37
+ return segments.at(-2) ?? leaf;
38
+ return leaf;
39
+ }
40
+ /**
41
+ * Pinned from InteractiveMode's compact extension-label helpers at Pi commit
42
+ * 914cf1472e715297caa30db4b9535d534a9eb718. The source metadata crosses an
43
+ * A1-owned boundary first; the owned shell never inspects Pi's private root.
44
+ */
45
+ function compactExtensionLabels(extensions) {
46
+ const localExtensions = extensions
47
+ .filter(extension => !isPackageExtensionSource(extension.sourceInfo))
48
+ .map(extension => {
49
+ const path = extension.sourcePath ?? extension.resolvedPath ?? "extension";
50
+ const segments = compactPathSegments(path);
51
+ if (segments.length > 1 && (segments.at(-1) === "index.ts" || segments.at(-1) === "index.js"))
52
+ segments.pop();
53
+ return { extension, segments };
54
+ });
55
+ return extensions.map(extension => {
56
+ const resourcePath = extension.sourcePath ?? extension.resolvedPath ?? "extension";
57
+ if (isPackageExtensionSource(extension.sourceInfo)) {
58
+ return compactPackageExtensionLabel(resourcePath, extension.sourceInfo);
59
+ }
60
+ const localIndex = localExtensions.findIndex(item => item.extension === extension);
61
+ const segments = localExtensions[localIndex]?.segments;
62
+ if (!segments || segments.length === 0)
63
+ return compactResourceLabel(resourcePath);
64
+ for (let count = 1; count <= segments.length; count += 1) {
65
+ const candidate = segments.slice(-count).join("/");
66
+ if (localExtensions.every((item, itemIndex) => itemIndex === localIndex || item.segments.slice(-count).join("/") !== candidate)) {
67
+ return candidate;
68
+ }
69
+ }
70
+ return segments.join("/");
71
+ });
72
+ }
73
+ function compactPackageExtensionLabel(resourcePath, sourceInfo) {
74
+ const sourceLabel = compactPackageSourceLabel(sourceInfo.source);
75
+ if (!sourceLabel)
76
+ return compactResourceLabel(resourcePath);
77
+ const shortPath = shortPackagePath(resourcePath, sourceInfo).replaceAll("\\", "/");
78
+ const packagePath = shortPath.startsWith("extensions/") ? shortPath.slice("extensions/".length) : shortPath;
79
+ const slash = packagePath.lastIndexOf("/");
80
+ const fileName = slash < 0 ? packagePath : packagePath.slice(slash + 1);
81
+ const directory = slash < 0 ? "" : packagePath.slice(0, slash);
82
+ const extension = fileName.lastIndexOf(".");
83
+ const name = extension <= 0 ? fileName : fileName.slice(0, extension);
84
+ if (name === "index")
85
+ return !directory || directory === "." ? sourceLabel : `${sourceLabel}:${directory}`;
86
+ return `${sourceLabel}:${packagePath}`;
87
+ }
88
+ function compactPackageSourceLabel(source) {
89
+ if (source.startsWith("npm:"))
90
+ return source.slice("npm:".length) || source;
91
+ if (!source.startsWith("git:"))
92
+ return source;
93
+ const gitSource = source.slice("git:".length).trim();
94
+ let repositoryPath;
95
+ const scpLike = gitSource.match(/^git@[^:]+:(.+)$/);
96
+ if (scpLike?.[1]) {
97
+ repositoryPath = scpLike[1];
98
+ }
99
+ else if (/^[a-z]+:\/\//i.test(gitSource)) {
100
+ try {
101
+ repositoryPath = new URL(gitSource).pathname.replace(/^\/+/, "");
102
+ }
103
+ catch {
104
+ return source;
105
+ }
106
+ }
107
+ else {
108
+ const slash = gitSource.indexOf("/");
109
+ if (slash >= 0)
110
+ repositoryPath = gitSource.slice(slash + 1);
111
+ }
112
+ if (!repositoryPath)
113
+ return source;
114
+ const ref = repositoryPath.indexOf("@");
115
+ const withoutRef = ref < 0 ? repositoryPath : repositoryPath.slice(0, ref);
116
+ return withoutRef.replace(/\.git$/, "") || source;
117
+ }
118
+ function shortPackagePath(resourcePath, sourceInfo) {
119
+ const fullPath = normalizeResourcePath(resourcePath);
120
+ const baseDir = sourceInfo.baseDir === null ? undefined : normalizeResourcePath(sourceInfo.baseDir).replace(/\/$/, "");
121
+ if (baseDir) {
122
+ const npmRoot = baseDir.match(/^(.*\/node_modules)\/(@?[^/]+(?:\/[^/]+)?)$/);
123
+ if (npmRoot?.[1] && fullPath.startsWith(`${npmRoot[1]}/`))
124
+ return relativeResourcePath(baseDir, fullPath);
125
+ if (fullPath === baseDir)
126
+ return ".";
127
+ if (fullPath.startsWith(`${baseDir}/`))
128
+ return fullPath.slice(baseDir.length + 1);
129
+ }
130
+ const npmMatch = fullPath.match(/node_modules\/(@?[^/]+(?:\/[^/]+)?)\/(.*)/);
131
+ if (npmMatch?.[2] && sourceInfo.source.startsWith("npm:"))
132
+ return npmMatch[2];
133
+ const gitMatch = fullPath.match(/git\/[^/]+\/[^/]+\/(.*)/);
134
+ if (gitMatch?.[1] && sourceInfo.source.startsWith("git:"))
135
+ return gitMatch[1];
136
+ return resourcePath;
137
+ }
138
+ function relativeResourcePath(from, to) {
139
+ const fromSegments = from.split("/").filter(Boolean);
140
+ const toSegments = to.split("/").filter(Boolean);
141
+ let common = 0;
142
+ while (common < fromSegments.length && common < toSegments.length
143
+ && fromSegments[common]?.toLowerCase() === toSegments[common]?.toLowerCase())
144
+ common += 1;
145
+ return [...fromSegments.slice(common).map(() => ".."), ...toSegments.slice(common)].join("/") || ".";
146
+ }
147
+ function compactPathSegments(path) {
148
+ return normalizeResourcePath(path).split("/").filter(segment => segment.length > 0 && segment !== "~");
149
+ }
150
+ function normalizeResourcePath(path) {
151
+ return path.replaceAll("\\", "/");
152
+ }
153
+ function isPackageExtensionSource(sourceInfo) {
154
+ const source = sourceInfo?.source ?? "";
155
+ return source.startsWith("npm:") || source.startsWith("git:");
156
+ }
157
+ // Performance: one painter set per root keeps identities stable, so the neutral viewport retains row-level
158
+ // selection variants while each callback still resolves the live theme.
@@ -0,0 +1,29 @@
1
+ import type { OwnedUiSessionViewModel, PromptHistoryImageSidecarAttachment, PromptHistoryImageSidecarPort } from "../../contracts/owned-ui/index.js";
2
+ import type { OwnedUiShellEditorPort } from "../../contracts/owned-ui/index.js";
3
+ import { PromptHistoryController } from "./prompt-history-controller.js";
4
+ import type { OwnedUiShellHistoryOptions } from "./session-shell-root.js";
5
+ type HistoryScope = "unique" | "shared";
6
+ /** What prompt history needs from the session: the editor, the transcript, and a render request. */
7
+ export interface SessionPromptHistoryPorts {
8
+ readonly editor: OwnedUiShellEditorPort;
9
+ usesDefaultInputSurface(): boolean;
10
+ rehydrate(text: string, resolveImage: (id: string) => PromptHistoryImageSidecarAttachment | null): string;
11
+ requestRender(): void;
12
+ transcript(): OwnedUiSessionViewModel["transcript"];
13
+ currentHistorySessionId(): string | null;
14
+ }
15
+ /**
16
+ * Binds persistent prompt history to the current session and scope, and replaces the store when either changes.
17
+ * Invariant: the controller is built in one place, so the initial and rebound histories behave the same.
18
+ */
19
+ export declare class SessionPromptHistory {
20
+ #private;
21
+ constructor(options: OwnedUiShellHistoryOptions, ports: SessionPromptHistoryPorts);
22
+ get controller(): PromptHistoryController | null;
23
+ get imageSidecar(): PromptHistoryImageSidecarPort | undefined;
24
+ start(): void;
25
+ synchronize(): void;
26
+ close(): Promise<boolean>;
27
+ rebind(replaced: boolean, scope?: HistoryScope): void;
28
+ }
29
+ export {};
@@ -0,0 +1,80 @@
1
+ import { PromptHistoryController } from "./prompt-history-controller.js";
2
+ /** The user prompts in a transcript, oldest first: the recall a store-less or replaced history starts from. */
3
+ function userTexts(transcript) {
4
+ return transcript.flatMap(block => block.kind === "user" ? [block.text] : []);
5
+ }
6
+ /**
7
+ * Binds persistent prompt history to the current session and scope, and replaces the store when either changes.
8
+ * Invariant: the controller is built in one place, so the initial and rebound histories behave the same.
9
+ */
10
+ export class SessionPromptHistory {
11
+ #options;
12
+ #ports;
13
+ #controller;
14
+ #scope;
15
+ #sessionId;
16
+ #imageSidecar;
17
+ #started = false;
18
+ constructor(options, ports) {
19
+ this.#options = options;
20
+ this.#ports = ports;
21
+ this.#scope = options.scope ?? "unique";
22
+ this.#sessionId = options.sessionId;
23
+ this.#imageSidecar = options.imageSidecar;
24
+ this.#controller = this.#createController(options.store, userTexts(ports.transcript()));
25
+ }
26
+ get controller() { return this.#controller; }
27
+ get imageSidecar() { return this.#imageSidecar; }
28
+ start() {
29
+ this.#started = true;
30
+ this.#controller?.start();
31
+ }
32
+ synchronize() { this.#controller?.synchronize(); }
33
+ close() { return this.#controller?.close() ?? Promise.resolve(true); }
34
+ // Invariant: delivery recovery does not replace a store; conversation or live scope changes can.
35
+ rebind(replaced, scope) {
36
+ if (this.#options.forSession !== undefined || this.#options.forScope !== undefined) {
37
+ const sessionId = this.#ports.currentHistorySessionId();
38
+ const selectedScope = scope ?? this.#scope;
39
+ if (selectedScope !== this.#scope || sessionId !== this.#sessionId) {
40
+ if (!replaced)
41
+ this.#ports.editor.recall?.restoreDraft?.();
42
+ const previous = this.#controller;
43
+ this.#controller = null;
44
+ this.#imageSidecar = undefined;
45
+ this.#scope = selectedScope;
46
+ this.#sessionId = sessionId ?? undefined;
47
+ void previous?.close().catch(() => false);
48
+ const destination = this.#options.forScope !== undefined
49
+ ? this.#options.forScope(selectedScope, sessionId)
50
+ : sessionId === null ? null : this.#options.forSession(sessionId);
51
+ if (destination !== null) {
52
+ this.#imageSidecar = destination.imageSidecar;
53
+ this.#controller = this.#createController(destination.store, replaced ? userTexts(this.#ports.transcript()) : []);
54
+ if (this.#started)
55
+ this.#controller.start();
56
+ }
57
+ else {
58
+ // Security: missing identity must not reopen Shared or retain another session's recall.
59
+ const fallback = replaced ? userTexts(this.#ports.transcript()).reverse() : [];
60
+ this.#ports.editor.recall?.reset();
61
+ this.#ports.editor.recall?.replace(fallback);
62
+ }
63
+ return;
64
+ }
65
+ }
66
+ if (replaced)
67
+ this.#controller?.reset(userTexts(this.#ports.transcript()));
68
+ }
69
+ #createController(store, fallback) {
70
+ return new PromptHistoryController({
71
+ editor: this.#ports.editor,
72
+ store,
73
+ limit: this.#options.limit,
74
+ fallback,
75
+ active: () => this.#ports.usesDefaultInputSurface(),
76
+ render: () => this.#ports.requestRender(),
77
+ rehydrate: value => this.#ports.rehydrate(value, id => this.#imageSidecar?.readAttachment(id) ?? null),
78
+ });
79
+ }
80
+ }
@@ -0,0 +1,46 @@
1
+ import type { OwnedUiCommandResult, OwnedUiSessionBackend, OwnedUiShellSkillSummary, OwnedUiWorkflowRequest } from "../../contracts/owned-ui/index.js";
2
+ import type { SessionDialogCoordinator } from "./dialog-coordinator.js";
3
+ import type { OwnedUiPresenterTerminal } from "./shell-host.js";
4
+ import type { OwnedUiSessionShellOptions, OwnedUiSessionShellRoot } from "./session-shell-root.js";
5
+ import type { WorkflowSelectors } from "./workflow-router.js";
6
+ /** What the selectors need from the session; typed ports, never the presenter. */
7
+ export interface SessionSelectorPorts {
8
+ readonly root: Pick<OwnedUiSessionShellRoot, "setInputSurface" | "setFooterLevel" | "workflow" | "editor">;
9
+ readonly terminal: OwnedUiPresenterTerminal;
10
+ readonly dialogs: SessionDialogCoordinator;
11
+ readonly presenters: OwnedUiSessionShellOptions["presenters"];
12
+ readonly backend: Pick<OwnedUiSessionBackend, "catalog" | "settings">;
13
+ readonly customViewport: boolean;
14
+ disposed(): boolean;
15
+ runWorkflow(request: OwnedUiWorkflowRequest): Promise<OwnedUiCommandResult>;
16
+ sendSlashPrompt(text: string, typed: string): Promise<OwnedUiCommandResult>;
17
+ reportError(): void;
18
+ input(title: string, placeholder: string, options?: {
19
+ readonly retainSurfaceOnSettle?: boolean;
20
+ }): Promise<string | undefined>;
21
+ shutdown(): void;
22
+ }
23
+ /**
24
+ * Opens the session's selectors and dialogs in the input dock and runs each choice as a workflow.
25
+ * Invariant: the workflow router reaches dialogs only through this collaborator, never through the presenter.
26
+ */
27
+ export declare class SessionSelectors implements WorkflowSelectors {
28
+ #private;
29
+ constructor(ports: SessionSelectorPorts);
30
+ showThinkingSelector(): Promise<void>;
31
+ /** The Skills dialog: a replacement input like the model selector, applying the chosen skill through the prompt path. */
32
+ showSkillsSelector(skills: readonly OwnedUiShellSkillSummary[]): void;
33
+ showModelSelector(initialSearchInput?: string): void;
34
+ showForkSelector(): void;
35
+ showLogoutSelector(): Promise<void>;
36
+ showLoginMethodSelector(providerReference: string): void;
37
+ showTreeSelector(initialSelectedId?: string): Promise<void>;
38
+ showLoginAuthTypeSelector(): void;
39
+ showLoginProviderSelector(authType?: "oauth" | "api_key", initialSearchInput?: string): void;
40
+ showSessionSelector(): Promise<void>;
41
+ showSettingsSelector(): void;
42
+ showTrustSelector(): void;
43
+ showScopedModelsSelector(): void;
44
+ /** The bare-A1 unified Models dialog: switch on Enter, scope on Space, persist on Ctrl+S, all through the engine. */
45
+ showModelsDialog(initialQuery?: string): void;
46
+ }
@@ -0,0 +1,269 @@
1
+ /**
2
+ * Opens the session's selectors and dialogs in the input dock and runs each choice as a workflow.
3
+ * Invariant: the workflow router reaches dialogs only through this collaborator, never through the presenter.
4
+ */
5
+ export class SessionSelectors {
6
+ #ports;
7
+ #root;
8
+ #terminal;
9
+ #dialogs;
10
+ #presenters;
11
+ #backend;
12
+ #customViewport;
13
+ // Invariant: the host's optional members follow the layout, so it is built only once the layout is known.
14
+ #dialogHost;
15
+ constructor(ports) {
16
+ this.#ports = ports;
17
+ this.#root = ports.root;
18
+ this.#terminal = ports.terminal;
19
+ this.#dialogs = ports.dialogs;
20
+ this.#presenters = ports.presenters;
21
+ this.#backend = ports.backend;
22
+ this.#customViewport = ports.customViewport;
23
+ this.#dialogHost = this.#createDialogHost();
24
+ }
25
+ // Rationale: presenters see the shell only as a dialog host; selectors open in the input dock and run choices as workflows.
26
+ #createDialogHost() {
27
+ const isDisposed = () => this.#ports.disposed();
28
+ return {
29
+ get disposed() { return isDisposed(); },
30
+ setInputSurface: surface => this.#root.setInputSurface(surface),
31
+ requestRender: () => this.#terminal.requestRender(),
32
+ viewport: () => this.#terminal.viewport(),
33
+ appendWorkflowStatus: text => this.#root.workflow.appendStatus(text),
34
+ appendWorkflowResult: result => this.#root.workflow.appendResult(result),
35
+ runWorkflow: request => this.#ports.runWorkflow(request),
36
+ // Invariant: the custom bare-A1 surface holds frames while optional selectors load and is permanently
37
+ // fullscreen; the pinned comparison profiles render immediately and keep Pi's TUI mode switch.
38
+ ...(this.#customViewport
39
+ ? { beginPresentationHold: () => this.#terminal.beginPresentationHold() }
40
+ : { switchTuiMode: mode => this.#terminal.switchMode(mode) }),
41
+ };
42
+ }
43
+ async showThinkingSelector() {
44
+ const snapshot = this.#backend.settings.snapshot();
45
+ const close = () => {
46
+ this.#root.setFooterLevel(true);
47
+ this.#root.setInputSurface(null);
48
+ this.#terminal.requestRender();
49
+ };
50
+ const select = (level) => {
51
+ close();
52
+ void this.#ports.runWorkflow({ command: "thinking", argument: "", selection: level });
53
+ };
54
+ const releasePresentation = this.#customViewport ? this.#terminal.beginPresentationHold() : undefined;
55
+ try {
56
+ const selectors = await this.#presenters.components.dialogs.lazySelectors();
57
+ const component = await selectors.createThinking({
58
+ currentLevel: snapshot.thinkingLevel,
59
+ availableLevels: snapshot.availableThinkingLevels,
60
+ onSelect: select,
61
+ onCancel: close,
62
+ onSelectAsDefault: level => this.#backend.settings.setDefaultThinkingLevel(level),
63
+ defaultLevel: snapshot.defaultThinkingLevel,
64
+ ...(this.#backend.settings.productMode === "bare" ? {
65
+ presentation: {
66
+ profile: "bare",
67
+ cycleBinding: this.#root.editor.keybindingConfig()["app.thinking.cycle"] ?? [],
68
+ },
69
+ } : {}),
70
+ });
71
+ if (this.#ports.disposed())
72
+ return;
73
+ this.#root.setFooterLevel(false);
74
+ this.#present(component);
75
+ }
76
+ finally {
77
+ releasePresentation?.();
78
+ }
79
+ }
80
+ /** The Skills dialog: a replacement input like the model selector, applying the chosen skill through the prompt path. */
81
+ showSkillsSelector(skills) {
82
+ const close = this.#dialogs.closeInputSurface;
83
+ const component = this.#presenters.components.dialogs.skillsSelector({
84
+ skills,
85
+ onSelect: name => {
86
+ close();
87
+ void this.#ports.sendSlashPrompt(this.#presenters.components.skills.prompt(name), this.#presenters.components.skills.prompt(name)).catch(() => this.#ports.reportError());
88
+ },
89
+ onCancel: close,
90
+ });
91
+ this.#present(component);
92
+ }
93
+ showModelSelector(initialSearchInput) {
94
+ this.#presenters.openModelSelector(this.#dialogHost, initialSearchInput);
95
+ }
96
+ showForkSelector() {
97
+ const options = this.#backend.catalog.pinnedForkOptions();
98
+ if (options.length === 0) {
99
+ this.#root.workflow.appendResult({ command: "fork", outcome: "completed", message: "No messages to fork from", messageKind: "status" });
100
+ this.#terminal.requestRender();
101
+ return;
102
+ }
103
+ const close = this.#dialogs.closeInputSurface;
104
+ const component = this.#presenters.components.dialogs.userMessageSelector(options, selection => {
105
+ close();
106
+ void this.#ports.runWorkflow({ command: "fork", argument: "", selection });
107
+ }, close);
108
+ this.#present(component);
109
+ }
110
+ async showLogoutSelector() {
111
+ let options;
112
+ try {
113
+ options = await this.#backend.catalog.pinnedLogoutOptions();
114
+ }
115
+ catch (error) {
116
+ this.#root.workflow.appendResult({
117
+ command: "logout",
118
+ outcome: "failed",
119
+ message: `Could not read stored credentials: ${error instanceof Error ? error.message : String(error)}`,
120
+ });
121
+ this.#terminal.requestRender();
122
+ return;
123
+ }
124
+ if (options.length === 0) {
125
+ this.#root.workflow.appendStatus("No stored credentials to remove. /logout only removes credentials saved by /login; environment variables and models.json config are unchanged.");
126
+ this.#terminal.requestRender();
127
+ return;
128
+ }
129
+ const close = this.#dialogs.closeInputSurface;
130
+ const component = this.#presenters.components.dialogs.authProviderSelector("logout", options, selection => {
131
+ close();
132
+ void this.#ports.runWorkflow({ command: "logout", argument: "", selection });
133
+ }, close);
134
+ this.#present(component);
135
+ }
136
+ showLoginMethodSelector(providerReference) {
137
+ const method = this.#backend.catalog.pinnedLoginMethodOptions(providerReference);
138
+ if (method.options.length === 0) {
139
+ this.showLoginProviderSelector(undefined, providerReference);
140
+ return;
141
+ }
142
+ if (method.options.length === 1) {
143
+ const selection = method.options[0]?.id;
144
+ if (selection)
145
+ void this.#ports.runWorkflow({ command: "login", argument: "", selection });
146
+ return;
147
+ }
148
+ const close = this.#dialogs.closeInputSurface;
149
+ const labels = method.options.map(option => option.label);
150
+ const component = this.#presenters.components.dialogs.extensionSelector(method.title, labels, label => {
151
+ const selection = method.options.find(option => option.label === label)?.id;
152
+ if (!selection)
153
+ return;
154
+ close();
155
+ void this.#ports.runWorkflow({ command: "login", argument: "", selection });
156
+ }, close);
157
+ this.#present(component);
158
+ }
159
+ showTreeSelector(initialSelectedId) {
160
+ return this.#presenters.openTreeSelector(this.#dialogHost, (entryId, skipSummaryPrompt) => {
161
+ void this.#completeTreeSelection(entryId, skipSummaryPrompt);
162
+ }, initialSelectedId);
163
+ }
164
+ showLoginAuthTypeSelector() {
165
+ const close = this.#dialogs.closeInputSurface;
166
+ const labels = ["Sign in with an account", "Sign in with an API key"];
167
+ const component = this.#presenters.components.dialogs.extensionSelector("Select authentication method:", labels, label => {
168
+ close();
169
+ this.showLoginProviderSelector(label === labels[0] ? "oauth" : "api_key");
170
+ }, close);
171
+ this.#present(component);
172
+ }
173
+ showLoginProviderSelector(authType, initialSearchInput) {
174
+ const options = this.#backend.catalog.pinnedLoginOptions(authType);
175
+ if (options.length === 0) {
176
+ this.#root.workflow.appendStatus(authType === "oauth" ? "No account providers available." : authType === "api_key" ? "No API key providers available." : "No login providers available.");
177
+ this.#terminal.requestRender();
178
+ return;
179
+ }
180
+ const close = this.#dialogs.closeInputSurface;
181
+ const component = this.#presenters.components.dialogs.authProviderSelector("login", options, id => {
182
+ close();
183
+ void this.#ports.runWorkflow({ command: "login", argument: "", selection: id });
184
+ }, () => {
185
+ close();
186
+ if (authType !== undefined)
187
+ this.showLoginAuthTypeSelector();
188
+ }, initialSearchInput);
189
+ this.#present(component);
190
+ }
191
+ showSessionSelector() {
192
+ return this.#presenters.openSessionSelector(this.#dialogHost, () => { void this.#ports.shutdown(); });
193
+ }
194
+ showSettingsSelector() {
195
+ this.#presenters.openSettingsSelector(this.#dialogHost);
196
+ }
197
+ showTrustSelector() {
198
+ const context = this.#backend.catalog.pinnedProjectTrustContext();
199
+ const close = this.#dialogs.closeInputSurface;
200
+ const component = this.#presenters.components.dialogs.trustSelector({
201
+ ...context,
202
+ onSelect: selection => {
203
+ try {
204
+ this.#backend.catalog.persistProjectTrust(selection.updates);
205
+ close();
206
+ this.#root.workflow.appendStatus(`Saved trust decision: ${selection.trusted ? "trusted" : "untrusted"}. Restart pi for this to take effect.`);
207
+ }
208
+ catch (error) {
209
+ close();
210
+ this.#root.workflow.appendResult({ command: "trust", outcome: "failed", message: error instanceof Error ? error.message : String(error) });
211
+ }
212
+ this.#terminal.requestRender();
213
+ },
214
+ onCancel: close,
215
+ });
216
+ this.#present(component);
217
+ }
218
+ showScopedModelsSelector() {
219
+ this.#presenters.openScopedModelsSelector(this.#dialogHost);
220
+ }
221
+ /** The bare-A1 unified Models dialog: switch on Enter, scope on Space, persist on Ctrl+S, all through the engine. */
222
+ showModelsDialog(initialQuery) {
223
+ this.#presenters.openModelsDialog(this.#dialogHost, initialQuery);
224
+ }
225
+ #treeSummary() {
226
+ return new Promise(resolve => {
227
+ this.#root.setInputSurface(this.#presenters.components.dialogs.extensionSelector("Summarize Branch?", ["No summary", "Summarize", "Summarize with custom prompt"], resolve, () => resolve(undefined)), true, "opaque");
228
+ this.#terminal.requestRender();
229
+ });
230
+ }
231
+ async #completeTreeSelection(entryId, skipSummaryPrompt) {
232
+ let summarize = false;
233
+ let customInstructions;
234
+ if (!skipSummaryPrompt) {
235
+ while (true) {
236
+ const choice = await this.#treeSummary();
237
+ if (choice === undefined) {
238
+ await this.showTreeSelector(entryId);
239
+ return;
240
+ }
241
+ summarize = choice !== "No summary";
242
+ if (choice === "Summarize with custom prompt") {
243
+ customInstructions = await this.#ports.input("Custom Summarization Instructions", "", { retainSurfaceOnSettle: true });
244
+ if (customInstructions === undefined)
245
+ continue;
246
+ }
247
+ break;
248
+ }
249
+ }
250
+ this.#root.setInputSurface(null);
251
+ this.#terminal.requestRender();
252
+ const result = await this.#ports.runWorkflow({
253
+ command: "tree",
254
+ argument: "",
255
+ selection: entryId,
256
+ treeSummary: {
257
+ summarize,
258
+ ...(customInstructions === undefined ? {} : { customInstructions }),
259
+ },
260
+ });
261
+ if (result.diagnostic === "Branch summarization cancelled")
262
+ await this.showTreeSelector(entryId);
263
+ }
264
+ // Invariant: a selector replaces the editor in the input dock and paints in the same frame.
265
+ #present(component) {
266
+ this.#root.setInputSurface(component);
267
+ this.#terminal.requestRender();
268
+ }
269
+ }