@timurproko/a1 0.2.7-dev.1255 → 0.2.7-dev.1264
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/app/session-shell/prompt-submission.d.ts +1 -1
- package/dist/app/session-shell/prompt-submission.js +9 -9
- package/dist/app/session-shell/resource-labels.d.ts +2 -0
- package/dist/app/session-shell/resource-labels.js +158 -0
- package/dist/app/session-shell/session-prompt-history.d.ts +29 -0
- package/dist/app/session-shell/session-prompt-history.js +80 -0
- package/dist/app/session-shell/session-selectors.d.ts +46 -0
- package/dist/app/session-shell/session-selectors.js +269 -0
- package/dist/app/session-shell/session-shell-root.d.ts +6 -28
- package/dist/app/session-shell/session-shell-root.js +26 -220
- package/dist/app/session-shell/session-shell.d.ts +3 -23
- package/dist/app/session-shell/session-shell.js +54 -365
- package/dist/app/session-shell/session-viewport-controller.d.ts +2 -10
- package/dist/app/session-shell/session-viewport-controller.js +40 -318
- package/dist/app/session-shell/settings-bindings.d.ts +1 -1
- package/dist/app/session-shell/settings-bindings.js +2 -2
- package/dist/app/session-shell/viewport-pointer.d.ts +83 -0
- package/dist/app/session-shell/viewport-pointer.js +361 -0
- package/dist/composition/owned-ui.js +63 -59
- package/dist/contracts/owned-ui/index.d.ts +1 -1
- package/dist/contracts/owned-ui/index.js +1 -1
- package/dist/contracts/owned-ui/validation.d.ts +1 -11
- package/dist/contracts/owned-ui/validation.js +8 -104
- package/dist/contracts/presentation/index.d.ts +1 -1
- package/dist/contracts/presentation/terminal.d.ts +0 -6
- package/dist/features/owned-ui/settings-app.js +13 -208
- package/dist/features/owned-ui/settings-model-dialog.d.ts +41 -0
- package/dist/features/owned-ui/settings-model-dialog.js +194 -0
- package/dist/foundation/release/automatic-update.js +1 -8
- package/dist/foundation/release/dependency-layer.js +3 -3
- package/dist/foundation/release/latest-release.js +1 -1
- package/dist/foundation/release/managed-path.d.ts +10 -0
- package/dist/foundation/release/managed-path.js +26 -0
- package/dist/foundation/release/release-gc.js +6 -8
- package/dist/foundation/release/restart-certification.js +3 -4
- package/dist/foundation/release/update-activation.js +1 -1
- package/dist/foundation/release/update-execution-lease.js +1 -7
- package/dist/foundation/release/update-installer.d.ts +45 -0
- package/dist/foundation/release/update-installer.js +57 -0
- package/dist/foundation/release/update-recovery.js +2 -9
- package/dist/foundation/release/update.d.ts +4 -23
- package/dist/foundation/release/update.js +160 -179
- package/dist/integrations/pi/components/bracketed-paste.d.ts +33 -0
- package/dist/integrations/pi/components/bracketed-paste.js +123 -0
- package/dist/integrations/pi/components/owned-editor-ux.js +46 -140
- package/dist/integrations/pi/engine/adapter.d.ts +1 -8
- package/dist/integrations/pi/engine/adapter.js +6 -49
- package/dist/integrations/pi/engine/host.js +1 -1
- package/dist/integrations/pi/engine/repository-metadata.d.ts +49 -0
- package/dist/integrations/pi/engine/repository-metadata.js +189 -0
- package/dist/integrations/pi/engine/session-runtime.d.ts +6 -12
- package/dist/integrations/pi/engine/session-runtime.js +21 -178
- package/dist/integrations/pi/engine/view-state.d.ts +21 -2
- package/dist/integrations/pi/engine/view-state.js +129 -1
- package/dist/integrations/pi/engine/workflow-runner.d.ts +0 -1
- package/dist/integrations/pi/engine/workflow-runner.js +254 -365
- package/dist/integrations/pi/engine/workflow-share.d.ts +9 -0
- package/dist/integrations/pi/engine/workflow-share.js +168 -0
- package/dist/integrations/pi/engine/workflow-support.d.ts +0 -14
- package/dist/integrations/pi/engine/workflow-support.js +0 -47
- package/dist/integrations/pi/tui-runtime/adapter.d.ts +9 -19
- package/dist/integrations/pi/tui-runtime/adapter.js +35 -144
- package/dist/integrations/pi/tui-runtime/damage-aware-terminal.d.ts +4 -4
- package/dist/integrations/pi/tui-runtime/index.d.ts +0 -1
- package/dist/integrations/pi/tui-runtime/overlay-geometry.d.ts +2 -2
- package/dist/integrations/pi/tui-runtime/presentation-adapter.d.ts +2 -2
- package/dist/ui/components/transcript-viewport-frame.d.ts +120 -0
- package/dist/ui/components/transcript-viewport-frame.js +314 -0
- package/dist/ui/components/transcript-viewport-types.d.ts +147 -0
- package/dist/ui/components/transcript-viewport.d.ts +2 -147
- package/dist/ui/components/transcript-viewport.js +87 -315
- package/package.json +3 -3
- package/dist/integrations/pi/engine/diagnostics-state.d.ts +0 -9
- package/dist/integrations/pi/engine/diagnostics-state.js +0 -34
- package/dist/integrations/pi/engine/usage-view.d.ts +0 -4
- package/dist/integrations/pi/engine/usage-view.js +0 -56
- package/dist/integrations/pi/tui-runtime/contracts.d.ts +0 -22
- /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" | "
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
193
|
-
root.
|
|
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.
|
|
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.
|
|
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,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
|
+
}
|