@timurproko/a1 0.2.2-dev.611 → 0.2.3-dev.651
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/README.md +16 -8
- package/bin/pinned-pi-public.js +11 -4
- package/dist/app/session-shell/index.d.ts +1 -1
- package/dist/app/session-shell/paste-executor.js +6 -1
- package/dist/app/session-shell/prompt-chips.d.ts +8 -0
- package/dist/app/session-shell/prompt-chips.js +58 -13
- package/dist/app/session-shell/session-shell-root.d.ts +21 -3
- package/dist/app/session-shell/session-shell-root.js +110 -17
- package/dist/app/session-shell/session-shell.d.ts +1 -1
- package/dist/app/session-shell/session-shell.js +76 -31
- package/dist/app/session-shell/session-viewport-controller.d.ts +13 -0
- package/dist/app/session-shell/session-viewport-controller.js +28 -1
- package/dist/cli/version-stats.js +4 -7
- package/dist/composition/owned-ui.d.ts +7 -0
- package/dist/composition/owned-ui.js +74 -6
- package/dist/contracts/owned-ui/image-attachments.d.ts +7 -4
- package/dist/contracts/owned-ui/image-attachments.js +20 -7
- package/dist/contracts/owned-ui/index.d.ts +1 -1
- package/dist/contracts/owned-ui/index.js +1 -1
- package/dist/contracts/owned-ui/prompt-chips.d.ts +2 -2
- package/dist/contracts/owned-ui/prompt-chips.js +9 -6
- package/dist/features/owned-ui/project-trust-prompt.js +39 -3
- package/dist/features/owned-ui/release-note-state.d.ts +13 -0
- package/dist/features/owned-ui/release-note-state.js +109 -0
- package/dist/features/owned-ui/release-notes.d.ts +10 -0
- package/dist/features/owned-ui/release-notes.js +73 -0
- package/dist/features/owned-ui/resources/release-notes.json +9 -0
- package/dist/foundation/release/latest-release.d.ts +55 -0
- package/dist/foundation/release/latest-release.js +166 -0
- package/dist/foundation/release/update.js +9 -0
- package/dist/integrations/pi/components/index.d.ts +1 -1
- package/dist/integrations/pi/components/index.js +1 -1
- package/dist/integrations/pi/components/prompt-chip-presentation.d.ts +3 -0
- package/dist/integrations/pi/components/prompt-chip-presentation.js +14 -0
- package/dist/integrations/pi/components/shell-footer-status.js +30 -9
- package/dist/integrations/pi/components/shell-presenters-info.d.ts +36 -0
- package/dist/integrations/pi/components/shell-presenters-info.js +77 -1
- package/dist/integrations/pi/components/shell-presenters-transcript.d.ts +0 -6
- package/dist/integrations/pi/components/shell-presenters-transcript.js +0 -19
- package/dist/integrations/pi/components/shell-shared-facade.d.ts +2 -1
- package/dist/integrations/pi/components/submitted-prompt-adapter.d.ts +1 -1
- package/dist/integrations/pi/components/submitted-prompt-adapter.js +13 -4
- package/dist/integrations/pi/engine/adapter.d.ts +12 -0
- package/dist/integrations/pi/engine/adapter.js +13 -0
- package/dist/integrations/pi/engine/session-runtime.d.ts +1 -0
- package/dist/integrations/pi/engine/session-runtime.js +2 -1
- package/dist/native/darwin-arm64/manifest.json +1 -1
- package/dist/native/linux-x64/manifest.json +1 -1
- package/dist/native/win32-x64/manifest.json +2 -2
- package/dist/native/win32-x64/process-guardian.exe +0 -0
- package/dist/product-identity.d.ts +1 -1
- package/dist/product-identity.js +1 -1
- package/dist/product-identity.json +1 -0
- package/dist/ui/settings/declarations.d.ts +25 -1
- package/dist/ui/settings/declarations.js +19 -1
- package/dist/ui/settings/migrations.js +14 -0
- package/docs/architecture/internal-naming.md +2 -2
- package/docs/architecture/resource-and-data-policy.md +11 -0
- package/docs/architecture/toolchain.md +1 -1
- package/docs/architecture/ui-reference-provenance.md +1 -0
- package/docs/ci-release-runbook.md +159 -70
- package/docs/local-worktree-cleanup.md +21 -4
- package/docs/manual-owned-ui-checkpoint.md +1 -0
- package/docs/openspec-archive-automation.md +1 -1
- package/docs/releases/0.2.2.md +47 -0
- package/docs/validation.md +13 -3
- package/package.json +3 -3
|
@@ -20,14 +20,11 @@ export async function runVersionStats(options) {
|
|
|
20
20
|
output.stderr(`${PRODUCT_TEXT.diagnostic(`could not read its installed version: ${message(error)}`)}\n`);
|
|
21
21
|
return 1;
|
|
22
22
|
}
|
|
23
|
-
//
|
|
24
|
-
//
|
|
25
|
-
if (prerelease(installed) === null) {
|
|
26
|
-
output.stdout(`${installed}\n`);
|
|
27
|
-
return 0;
|
|
28
|
-
}
|
|
23
|
+
// Rationale: unlike Pi, stable builds also show the release channel head so users can see they are
|
|
24
|
+
// behind without starting a session. Only development builds show the preview channel.
|
|
29
25
|
const remote = await queryDistTags(runner, options.fetcher ?? defaultRegistryFetcher);
|
|
30
|
-
|
|
26
|
+
const develop = prerelease(installed) === null ? "" : `Develop: ${remote.develop ?? "unavailable"}\n`;
|
|
27
|
+
output.stdout(`Current: ${installed}\n${develop}Release: ${remote.release ?? "unavailable"}\n`);
|
|
31
28
|
if (remote.error)
|
|
32
29
|
output.stderr(`${PRODUCT_TEXT.diagnostic(`could not resolve npm dist-tags: ${remote.error}`)}\n`);
|
|
33
30
|
return 0;
|
|
@@ -1,9 +1,11 @@
|
|
|
1
|
+
import type { AvailableRelease, StartupReleaseCheckOptions } from "../foundation/release/latest-release.js";
|
|
1
2
|
import type { SessionSelection } from "../foundation/lifecycle/session-selection.js";
|
|
2
3
|
import type { PiEngineAdapter } from "../integrations/pi/engine/adapter.js";
|
|
3
4
|
import type { PiProjectTrustPreflightPrompt } from "../integrations/pi/engine/project-trust-preflight.js";
|
|
4
5
|
import type { PiSessionForkPrompt } from "../integrations/pi/engine/session-selection.js";
|
|
5
6
|
import { OwnedSettingsManager } from "../ui/settings/manager.js";
|
|
6
7
|
import type { OwnedUiApplicationPort, PresentationTerminalPort } from "../contracts/presentation/index.js";
|
|
8
|
+
import type { ReleaseNoteCatalog } from "../features/owned-ui/release-notes.js";
|
|
7
9
|
export interface OwnedUiCompositionOptions {
|
|
8
10
|
readonly cwd?: string;
|
|
9
11
|
readonly terminal?: PresentationTerminalPort;
|
|
@@ -27,6 +29,11 @@ export interface OwnedUiCompositionOptions {
|
|
|
27
29
|
readonly suggestionDiagnosticsPath?: string;
|
|
28
30
|
/** Optional local metadata-only clipboard diagnostics; never enabled in comparison profiles. */
|
|
29
31
|
readonly clipboardDiagnosticsPath?: string;
|
|
32
|
+
/** Deterministic release-note seams for composition tests. */
|
|
33
|
+
readonly packageVersion?: string;
|
|
34
|
+
readonly releaseNotes?: ReleaseNoteCatalog;
|
|
35
|
+
/** Startup release-availability seam; defaults to the throttled registry check. */
|
|
36
|
+
readonly checkForNewerRelease?: (options: StartupReleaseCheckOptions) => Promise<AvailableRelease | null>;
|
|
30
37
|
}
|
|
31
38
|
export interface OwnedUiComposition {
|
|
32
39
|
readonly application: OwnedUiApplicationPort;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
1
2
|
import { SuggestionDiagnosticCapture } from "../features/prompt-suggestions/diagnostics.js";
|
|
2
3
|
import { PRODUCT_IDENTITY } from "../product-identity.js";
|
|
3
4
|
import { PromptHistoryService } from "../features/prompt-history/service.js";
|
|
@@ -14,7 +15,7 @@ import { OwnedSettingsManager } from "../ui/settings/manager.js";
|
|
|
14
15
|
import { createPiTerminalBridge } from "../integrations/pi/tui-runtime/presentation-adapter.js";
|
|
15
16
|
import { createOwnedRouteHost } from "./settings-route-host.js";
|
|
16
17
|
import { renderPiShellChangelogLines } from "../integrations/pi/components/shell-presenters-info.js";
|
|
17
|
-
import {
|
|
18
|
+
import { nativeHyperlinkStyle } from "../ui/components/spans.js";
|
|
18
19
|
export async function composeOwnedUiApplication(options = {}) {
|
|
19
20
|
return (await composeOwnedUi(options)).application;
|
|
20
21
|
}
|
|
@@ -27,6 +28,7 @@ export async function composeOwnedUi(options = {}) {
|
|
|
27
28
|
cwd,
|
|
28
29
|
availableThemes: () => getAvailablePiThemes().map(theme => theme.name),
|
|
29
30
|
settingsProductMode: ownedSurfaces ? "bare" : "comparison",
|
|
31
|
+
announceStartupChangelog: !ownedSurfaces,
|
|
30
32
|
...(ownedSurfaces ? {
|
|
31
33
|
repositoryContextReader: async (sessionId, sessionFile, signal) => await readSessionRepositoryContext({ sessionId, sessionFile }, { signal }),
|
|
32
34
|
} : {}),
|
|
@@ -35,6 +37,7 @@ export async function composeOwnedUi(options = {}) {
|
|
|
35
37
|
...(options.sessionForkPrompt === undefined ? {} : { sessionForkPrompt: options.sessionForkPrompt }),
|
|
36
38
|
...(options.projectTrustPrompt === undefined ? {} : { projectTrustPrompt: options.projectTrustPrompt }),
|
|
37
39
|
});
|
|
40
|
+
const productPaths = resolveProductPaths();
|
|
38
41
|
const settings = options.profileId === undefined
|
|
39
42
|
? null
|
|
40
43
|
: new OwnedSettingsManager({
|
|
@@ -42,16 +45,37 @@ export async function composeOwnedUi(options = {}) {
|
|
|
42
45
|
profileId: options.profileId,
|
|
43
46
|
agentProvider: () => adapter.settingsPort(),
|
|
44
47
|
});
|
|
48
|
+
let releaseNotes = null;
|
|
49
|
+
let releaseNotesFailure;
|
|
50
|
+
if (ownedSurfaces) {
|
|
51
|
+
try {
|
|
52
|
+
releaseNotes = options.releaseNotes
|
|
53
|
+
?? await import("../features/owned-ui/release-notes.js").then(module => module.readPackagedReleaseNotes());
|
|
54
|
+
}
|
|
55
|
+
catch (error) {
|
|
56
|
+
releaseNotesFailure = error;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
const packageVersion = ownedSurfaces
|
|
60
|
+
? options.packageVersion ?? await readPackageVersion()
|
|
61
|
+
: null;
|
|
62
|
+
const currentReleaseNote = packageVersion === null ? null : releaseNotes?.current(packageVersion) ?? null;
|
|
63
|
+
const releaseNoteClaim = currentReleaseNote === null || options.profileId === undefined
|
|
64
|
+
? null
|
|
65
|
+
: await import("../features/owned-ui/release-note-state.js").then(module => module.claimReleaseNote({
|
|
66
|
+
configDir: productPaths.configDir, profileId: options.profileId, version: currentReleaseNote.version,
|
|
67
|
+
}));
|
|
45
68
|
// Compatibility: bare A1 intentionally ships one visual target while its UI is being completed:
|
|
46
69
|
// dark, regardless of terminal detection or a previously stored Pi theme. The
|
|
47
70
|
// comparison profile keeps Pi's configured theme behavior and settings surface.
|
|
48
71
|
applyConfiguredPiTheme(ownedSurfaces ? "dark" : adapter.configuredTheme());
|
|
49
|
-
// Rationale:
|
|
50
|
-
// cannot open before the shell exists, so the closure is settled by the time it runs.
|
|
72
|
+
// Rationale: Routes open only after shell construction.
|
|
51
73
|
const references = {
|
|
52
74
|
changelog: async (input) => {
|
|
53
|
-
|
|
54
|
-
|
|
75
|
+
if (input?.document === undefined && releaseNotesFailure !== undefined)
|
|
76
|
+
throw releaseNotesFailure;
|
|
77
|
+
const markdown = input?.document ?? releaseNotes?.completeMarkdown ?? "No A1 release notes found.";
|
|
78
|
+
return { rows: width => renderPiShellChangelogLines(markdown, width).map(row => nativeHyperlinkStyle(row)) };
|
|
55
79
|
},
|
|
56
80
|
hotkeys: async () => {
|
|
57
81
|
const presentation = shell.hotkeysPresentation();
|
|
@@ -73,6 +97,10 @@ export async function composeOwnedUi(options = {}) {
|
|
|
73
97
|
enabled: () => settings.value("promptSuggestions"),
|
|
74
98
|
onChange: (listener) => settings.onChange(() => listener(settings.value("promptSuggestions"))),
|
|
75
99
|
};
|
|
100
|
+
const promptImages = settings === null || !ownedSurfaces ? null : {
|
|
101
|
+
limit: () => settings.value("promptImageLimit"),
|
|
102
|
+
onChange: (listener) => settings.onChange(listener),
|
|
103
|
+
};
|
|
76
104
|
const skills = settings === null || !ownedSurfaces ? null : {
|
|
77
105
|
presentation: () => settings.value("skillsPresentation"),
|
|
78
106
|
// Rationale: one listener covers both the A1 presentation choice and the engine skill-command toggle.
|
|
@@ -95,12 +123,23 @@ export async function composeOwnedUi(options = {}) {
|
|
|
95
123
|
const clipboardDiagnostics = settings !== null && ownedSurfaces && clipboardDestination?.trim()
|
|
96
124
|
? new ClipboardDiagnosticCapture(clipboardDestination) : null;
|
|
97
125
|
let shell;
|
|
126
|
+
let releaseNoteAcknowledgement = null;
|
|
98
127
|
try {
|
|
99
128
|
shell = new OwnedUiSessionShell({
|
|
100
129
|
engine: {
|
|
101
130
|
backend: adapter,
|
|
102
131
|
cwd: adapter.cwd,
|
|
103
132
|
...(routeHost === null ? {} : { routeHost }),
|
|
133
|
+
...(currentReleaseNote === null || releaseNoteClaim === null ? {} : {
|
|
134
|
+
startupRoute: {
|
|
135
|
+
route: "changelog",
|
|
136
|
+
input: { document: currentReleaseNote.markdown },
|
|
137
|
+
onClosed: () => {
|
|
138
|
+
releaseNoteAcknowledgement ??= releaseNoteClaim.acknowledge();
|
|
139
|
+
return releaseNoteAcknowledgement;
|
|
140
|
+
},
|
|
141
|
+
},
|
|
142
|
+
}),
|
|
104
143
|
...(ownedSurfaces ? { sessionLayout: "custom-viewport" } : {}),
|
|
105
144
|
},
|
|
106
145
|
presentation: {
|
|
@@ -116,6 +155,7 @@ export async function composeOwnedUi(options = {}) {
|
|
|
116
155
|
paste: event => clipboardDiagnostics.paste(event),
|
|
117
156
|
} }),
|
|
118
157
|
...(promptSuggestions === null ? {} : { suggestions: promptSuggestions }),
|
|
158
|
+
...(promptImages === null ? {} : { promptImages }),
|
|
119
159
|
...(skills === null ? {} : { skills }),
|
|
120
160
|
...(promptHistory === null ? {} : { history: {
|
|
121
161
|
...promptHistory,
|
|
@@ -124,20 +164,41 @@ export async function composeOwnedUi(options = {}) {
|
|
|
124
164
|
});
|
|
125
165
|
}
|
|
126
166
|
catch (error) {
|
|
167
|
+
await releaseNoteClaim?.release();
|
|
127
168
|
clipboardDiagnostics?.dispose();
|
|
128
169
|
suggestionDiagnostics?.dispose();
|
|
129
170
|
throw error;
|
|
130
171
|
}
|
|
172
|
+
// Rationale: pinned Pi's version check is unreachable because the owned shell never runs its
|
|
173
|
+
// InteractiveMode, so A1 checks its own channel. The result is never awaited on the startup path;
|
|
174
|
+
// settings are read here because the runner resolves them only just before start.
|
|
175
|
+
const announceNewerRelease = async () => {
|
|
176
|
+
const check = options.checkForNewerRelease
|
|
177
|
+
?? (await import("../foundation/release/latest-release.js")).checkForNewerRelease;
|
|
178
|
+
const release = await check({
|
|
179
|
+
runningVersion: packageVersion ?? await readPackageVersion(),
|
|
180
|
+
configDir: productPaths.configDir,
|
|
181
|
+
interactive: process.stdout.isTTY === true,
|
|
182
|
+
...(settings === null || !ownedSurfaces ? {} : { settingEnabled: settings.value("updateCheck") }),
|
|
183
|
+
});
|
|
184
|
+
if (release !== null)
|
|
185
|
+
adapter.announceReleaseUpdate(release);
|
|
186
|
+
};
|
|
131
187
|
const application = {
|
|
132
188
|
get disposed() { return adapter.disposed; },
|
|
133
|
-
start: () =>
|
|
189
|
+
start: () => {
|
|
190
|
+
shell.start();
|
|
191
|
+
void announceNewerRelease().catch(() => undefined);
|
|
192
|
+
},
|
|
134
193
|
flush: () => adapter.flushEvents(),
|
|
135
194
|
waitUntilStopped: () => shell.waitUntilStopped(),
|
|
136
195
|
dispose: async () => {
|
|
137
196
|
try {
|
|
138
197
|
await shell.dispose();
|
|
198
|
+
await releaseNoteAcknowledgement?.catch(() => undefined);
|
|
139
199
|
}
|
|
140
200
|
finally {
|
|
201
|
+
await releaseNoteClaim?.release();
|
|
141
202
|
suggestionDiagnostics?.dispose();
|
|
142
203
|
clipboardDiagnostics?.dispose();
|
|
143
204
|
}
|
|
@@ -145,6 +206,13 @@ export async function composeOwnedUi(options = {}) {
|
|
|
145
206
|
};
|
|
146
207
|
return { application, settings };
|
|
147
208
|
}
|
|
209
|
+
async function readPackageVersion() {
|
|
210
|
+
const manifest = JSON.parse(await readFile(new URL("../../package.json", import.meta.url), "utf8"));
|
|
211
|
+
const version = manifest?.version;
|
|
212
|
+
if (typeof version !== "string")
|
|
213
|
+
throw new Error("A1 package version is unavailable");
|
|
214
|
+
return version;
|
|
215
|
+
}
|
|
148
216
|
function quitOutroSettingsSnapshot(settings) {
|
|
149
217
|
return {
|
|
150
218
|
enabled: settings.value("quitAnimation"),
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import type { OwnedUiImageAttachment } from "./model.js";
|
|
2
|
-
export declare const
|
|
2
|
+
export declare const DEFAULT_PROMPT_IMAGE_LIMIT = 8;
|
|
3
|
+
/** Absolute command boundary; the bare-A1 editor may choose a lower live limit. */
|
|
4
|
+
export declare const MAX_PROMPT_IMAGES = 16;
|
|
3
5
|
/** Owned clipboard text is bounded before transfer, normalization, or editor insertion. */
|
|
4
6
|
export declare const MAX_CLIPBOARD_TEXT_BYTES: number;
|
|
5
7
|
/** Limit applies to canonical ASCII base64, not decoded image bytes. */
|
|
@@ -11,7 +13,7 @@ declare const MESSAGES: {
|
|
|
11
13
|
readonly "paste-busy": "Clipboard preparation is full (8 pending pastes).";
|
|
12
14
|
readonly "paste-write-failed": "Paste skipped because the preceding copy failed.";
|
|
13
15
|
readonly "image-size": "Image exceeds the 8 MiB encoded base64 limit. Reduce the image size and paste it again.";
|
|
14
|
-
readonly "image-count": "A prompt
|
|
16
|
+
readonly "image-count": "A prompt has too many images.";
|
|
15
17
|
readonly "image-data": "Image data is invalid. Paste a valid image again.";
|
|
16
18
|
readonly "image-mime": "Image MIME type is invalid. Paste a supported image again.";
|
|
17
19
|
readonly "image-source-size": "Source image exceeds 20 MiB. Reduce the source size and paste it again.";
|
|
@@ -27,10 +29,11 @@ declare const MESSAGES: {
|
|
|
27
29
|
/** Trusted, payload-free diagnostics for user-correctable attachment failures. */
|
|
28
30
|
export declare class ImageAttachmentError extends TypeError {
|
|
29
31
|
readonly code: keyof typeof MESSAGES;
|
|
30
|
-
|
|
32
|
+
readonly imageLimit: number | null;
|
|
33
|
+
constructor(code: keyof typeof MESSAGES, imageLimit?: number);
|
|
31
34
|
}
|
|
32
35
|
/** Checks encoded length before decoding or copying a clipboard payload. */
|
|
33
36
|
export declare function assertImageEncodedSize(data: string): void;
|
|
34
37
|
/** Final command admission also covers restored and deferred non-clipboard inputs. */
|
|
35
|
-
export declare function assertPromptImages(images: readonly OwnedUiImageAttachment[]): void;
|
|
38
|
+
export declare function assertPromptImages(images: readonly OwnedUiImageAttachment[], imageLimit?: number): void;
|
|
36
39
|
export {};
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
export const
|
|
1
|
+
export const DEFAULT_PROMPT_IMAGE_LIMIT = 8;
|
|
2
|
+
/** Absolute command boundary; the bare-A1 editor may choose a lower live limit. */
|
|
3
|
+
export const MAX_PROMPT_IMAGES = 16;
|
|
2
4
|
/** Owned clipboard text is bounded before transfer, normalization, or editor insertion. */
|
|
3
5
|
export const MAX_CLIPBOARD_TEXT_BYTES = 16 * 1024 * 1024;
|
|
4
6
|
/** Limit applies to canonical ASCII base64, not decoded image bytes. */
|
|
@@ -10,7 +12,7 @@ const MESSAGES = {
|
|
|
10
12
|
"paste-busy": "Clipboard preparation is full (8 pending pastes).",
|
|
11
13
|
"paste-write-failed": "Paste skipped because the preceding copy failed.",
|
|
12
14
|
"image-size": "Image exceeds the 8 MiB encoded base64 limit. Reduce the image size and paste it again.",
|
|
13
|
-
"image-count": "A prompt
|
|
15
|
+
"image-count": "A prompt has too many images.",
|
|
14
16
|
"image-data": "Image data is invalid. Paste a valid image again.",
|
|
15
17
|
"image-mime": "Image MIME type is invalid. Paste a supported image again.",
|
|
16
18
|
"image-source-size": "Source image exceeds 20 MiB. Reduce the source size and paste it again.",
|
|
@@ -26,9 +28,14 @@ const MESSAGES = {
|
|
|
26
28
|
/** Trusted, payload-free diagnostics for user-correctable attachment failures. */
|
|
27
29
|
export class ImageAttachmentError extends TypeError {
|
|
28
30
|
code;
|
|
29
|
-
|
|
30
|
-
|
|
31
|
+
imageLimit;
|
|
32
|
+
constructor(code, imageLimit = DEFAULT_PROMPT_IMAGE_LIMIT) {
|
|
33
|
+
assertPromptImageLimit(imageLimit);
|
|
34
|
+
super(code === "image-count"
|
|
35
|
+
? `A prompt is limited to ${imageLimit} ${imageLimit === 1 ? "image" : "images"}. To change the limit /settings.`
|
|
36
|
+
: MESSAGES[code]);
|
|
31
37
|
this.code = code;
|
|
38
|
+
this.imageLimit = code === "image-count" ? imageLimit : null;
|
|
32
39
|
this.name = "ImageAttachmentError";
|
|
33
40
|
}
|
|
34
41
|
}
|
|
@@ -42,9 +49,10 @@ export function assertImageEncodedSize(data) {
|
|
|
42
49
|
const VALIDATED_IMAGES = new WeakSet();
|
|
43
50
|
const BASE64_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
|
44
51
|
/** Final command admission also covers restored and deferred non-clipboard inputs. */
|
|
45
|
-
export function assertPromptImages(images) {
|
|
46
|
-
|
|
47
|
-
|
|
52
|
+
export function assertPromptImages(images, imageLimit = MAX_PROMPT_IMAGES) {
|
|
53
|
+
assertPromptImageLimit(imageLimit);
|
|
54
|
+
if (!Array.isArray(images) || images.length > imageLimit)
|
|
55
|
+
throw new ImageAttachmentError("image-count", imageLimit);
|
|
48
56
|
for (const image of images) {
|
|
49
57
|
if (!image || image.type !== "image")
|
|
50
58
|
throw new ImageAttachmentError("image-data");
|
|
@@ -65,3 +73,8 @@ export function assertPromptImages(images) {
|
|
|
65
73
|
}
|
|
66
74
|
}
|
|
67
75
|
}
|
|
76
|
+
function assertPromptImageLimit(imageLimit) {
|
|
77
|
+
if (!Number.isSafeInteger(imageLimit) || imageLimit < 1 || imageLimit > MAX_PROMPT_IMAGES) {
|
|
78
|
+
throw new RangeError(`Prompt image limit must be an integer from 1 through ${MAX_PROMPT_IMAGES}`);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
@@ -3,7 +3,7 @@ export type { OwnedUiExtensionAutocompleteItem, OwnedUiExtensionAutocompleteProv
|
|
|
3
3
|
export { OWNED_UI_CONTRACT_VERSION } from "./model.js";
|
|
4
4
|
export type { OwnedUiBlockStatus, OwnedUiCommand, OwnedUiCommandOutcome, OwnedUiCorrelationId, OwnedUiCustomization, OwnedUiDiagnostics, OwnedUiDialog, OwnedUiEditorState, OwnedUiEntityId, OwnedUiEvent, OwnedUiFooterView, OwnedUiImageAttachment, OwnedUiLifecycle, OwnedUiModelInfo, OwnedUiOverlay, OwnedUiPromptSuggestionGeneratorPort, OwnedUiPromptSuggestionIdentity, OwnedUiPromptSuggestionReasoning, OwnedUiPromptSuggestionRequest, OwnedUiPromptSuggestionResult, OwnedUiPromptSuggestionState, OwnedUiQuitOutroSettings, OwnedUiQuitOutroSettingsPort, OwnedUiSessionId, OwnedUiSessionViewModel, OwnedUiSeverity, OwnedUiSlotId, OwnedUiSnapshot, OwnedUiStatusView, OwnedUiTerminalSurface, OwnedUiThinkingLevel, OwnedUiToolRendering, OwnedUiToolResultPart, OwnedUiToolState, OwnedUiTranscriptBlock, OwnedUiTranscriptBlockKind, OwnedUiTranscriptImageReference, OwnedUiUsageView, OwnedUiViewportSettings, OwnedUiViewportSettingsPort, } from "./model.js";
|
|
5
5
|
export { acceptsTranscriptUpdate, transcriptToolState } from "./transcript-lifecycle.js";
|
|
6
|
-
export { ImageAttachmentError, MAX_CLIPBOARD_TEXT_BYTES, MAX_IMAGE_DATA_BYTES, MAX_PROMPT_IMAGES, assertImageEncodedSize, assertPromptImages, } from "./image-attachments.js";
|
|
6
|
+
export { DEFAULT_PROMPT_IMAGE_LIMIT, ImageAttachmentError, MAX_CLIPBOARD_TEXT_BYTES, MAX_IMAGE_DATA_BYTES, MAX_PROMPT_IMAGES, assertImageEncodedSize, assertPromptImages, } from "./image-attachments.js";
|
|
7
7
|
export { CONTEXTUAL_PROMPT_SUGGESTION_INSTRUCTION, normalizePromptSuggestionCandidate } from "./prompt-suggestions.js";
|
|
8
8
|
export { canonicalPromptChipMatches, protectPromptChipWrapping, replaceCanonicalPromptChips } from "./prompt-chips.js";
|
|
9
9
|
export type { PromptChipTextMatch, PromptChipWrapProtection } from "./prompt-chips.js";
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
export { OWNED_UI_EXTENSION_CONTRACT_VERSION, OWNED_UI_EXTENSION_RENDER_CALLBACKS, OWNED_UI_EXTENSION_UI_CALLBACKS, OWNED_UI_EXTENSION_UI_PROPERTIES, assertOwnedUiExtensionUiPort, } from "./extension-ui.js";
|
|
2
2
|
export { OWNED_UI_CONTRACT_VERSION } from "./model.js";
|
|
3
3
|
export { acceptsTranscriptUpdate, transcriptToolState } from "./transcript-lifecycle.js";
|
|
4
|
-
export { ImageAttachmentError, MAX_CLIPBOARD_TEXT_BYTES, MAX_IMAGE_DATA_BYTES, MAX_PROMPT_IMAGES, assertImageEncodedSize, assertPromptImages, } from "./image-attachments.js";
|
|
4
|
+
export { DEFAULT_PROMPT_IMAGE_LIMIT, ImageAttachmentError, MAX_CLIPBOARD_TEXT_BYTES, MAX_IMAGE_DATA_BYTES, MAX_PROMPT_IMAGES, assertImageEncodedSize, assertPromptImages, } from "./image-attachments.js";
|
|
5
5
|
export { CONTEXTUAL_PROMPT_SUGGESTION_INSTRUCTION, normalizePromptSuggestionCandidate } from "./prompt-suggestions.js";
|
|
6
6
|
export { canonicalPromptChipMatches, protectPromptChipWrapping, replaceCanonicalPromptChips } from "./prompt-chips.js";
|
|
7
7
|
export { SUGGESTION_DECISION_REASONS, SUGGESTION_DIAGNOSTIC_EVENTS } from "./suggestion-diagnostics.js";
|
|
@@ -11,5 +11,5 @@ export interface PromptChipWrapProtection {
|
|
|
11
11
|
export declare function canonicalPromptChipMatches(text: string): readonly PromptChipTextMatch[];
|
|
12
12
|
/** Replaces only canonical prompt-chip labels while preserving every intervening source byte. */
|
|
13
13
|
export declare function replaceCanonicalPromptChips(text: string, replacement: (match: PromptChipTextMatch) => string): string;
|
|
14
|
-
/** Protects
|
|
15
|
-
export declare function protectPromptChipWrapping(text: string): PromptChipWrapProtection;
|
|
14
|
+
/** Protects chip labels for wrapping, optionally presents a derived label, and removes all sentinels. */
|
|
15
|
+
export declare function protectPromptChipWrapping(text: string, present?: (chip: string) => string): PromptChipWrapProtection;
|
|
@@ -12,20 +12,23 @@ export function replaceCanonicalPromptChips(text, replacement) {
|
|
|
12
12
|
text: value, start: offset, end: offset + value.length,
|
|
13
13
|
}));
|
|
14
14
|
}
|
|
15
|
-
/** Protects
|
|
16
|
-
export function protectPromptChipWrapping(text) {
|
|
15
|
+
/** Protects chip labels for wrapping, optionally presents a derived label, and removes all sentinels. */
|
|
16
|
+
export function protectPromptChipWrapping(text, present = identity) {
|
|
17
17
|
const matches = canonicalPromptChipMatches(text);
|
|
18
18
|
if (matches.length === 0)
|
|
19
19
|
return { text, restore: identity };
|
|
20
20
|
const spaceMarker = unusedPrivateMarker(text);
|
|
21
21
|
const boundaryMarker = unusedZeroWidthMarker(text);
|
|
22
|
+
const boundary = `${boundaryMarker} `;
|
|
22
23
|
let protectedText = "", cursor = 0;
|
|
23
24
|
for (const match of matches) {
|
|
24
25
|
protectedText += text.slice(cursor, match.start);
|
|
25
|
-
// Rationale:
|
|
26
|
-
if (match.start
|
|
27
|
-
protectedText +=
|
|
28
|
-
protectedText += match.text.replaceAll(" ", spaceMarker);
|
|
26
|
+
// Rationale: temporary break opportunities keep each chip independent from adjacent chips or prose.
|
|
27
|
+
if (match.start > 0 && match.start !== cursor && !/\s/u.test(text[match.start - 1]))
|
|
28
|
+
protectedText += boundary;
|
|
29
|
+
protectedText += present(match.text).replaceAll(" ", spaceMarker);
|
|
30
|
+
if (match.end < text.length && !/\s/u.test(text[match.end]))
|
|
31
|
+
protectedText += boundary;
|
|
29
32
|
cursor = match.end;
|
|
30
33
|
}
|
|
31
34
|
protectedText += text.slice(cursor);
|
|
@@ -2,6 +2,13 @@ import { EMERGENCY_TERMINAL_RESET } from "../../foundation/terminal-cleanup/term
|
|
|
2
2
|
const ENTER_ALTERNATE_SCREEN = "\u001b[?1049h";
|
|
3
3
|
const HIDE_CURSOR = "\u001b[?25l";
|
|
4
4
|
const CLEAR_HOME = "\u001b[2J\u001b[H";
|
|
5
|
+
const HOME = "\u001b[H";
|
|
6
|
+
const ERASE_LINE_END = "\u001b[K";
|
|
7
|
+
const ERASE_BELOW = "\u001b[J";
|
|
8
|
+
const BEGIN_SYNC = "\u001b[?2026h";
|
|
9
|
+
const END_SYNC = "\u001b[?2026l";
|
|
10
|
+
const REQUEST_CURSOR_POSITION = "\u001b[6n";
|
|
11
|
+
const CURSOR_POSITION_REPORT = /^\u001b\[(\d+);(\d+)R/u;
|
|
5
12
|
const ACCENT = "\u001b[38;2;138;190;183m";
|
|
6
13
|
const MUTED = "\u001b[38;2;128;128;128m";
|
|
7
14
|
const DIM = "\u001b[38;2;102;102;102m";
|
|
@@ -32,6 +39,8 @@ export function createConsoleProjectTrustPrompt(options = {}) {
|
|
|
32
39
|
const wasRaw = input.isRaw === true;
|
|
33
40
|
let selected = 0;
|
|
34
41
|
let restored = false;
|
|
42
|
+
// Protocol: the parent screen's cursor, reported before the first frame moves it.
|
|
43
|
+
let origin;
|
|
35
44
|
const restore = () => {
|
|
36
45
|
if (restored)
|
|
37
46
|
return;
|
|
@@ -39,6 +48,21 @@ export function createConsoleProjectTrustPrompt(options = {}) {
|
|
|
39
48
|
input.setRawMode?.(wasRaw);
|
|
40
49
|
output.write(`${CLEAR_HOME}${EMERGENCY_TERMINAL_RESET}`);
|
|
41
50
|
};
|
|
51
|
+
// Protocol: a resolved bare selection hands the alternate screen to the shell, which
|
|
52
|
+
// enters it again. Leaving it here would show the parent screen until the shell's first
|
|
53
|
+
// frame. The shell's own enter saves the cursor, so park it where the parent had it and
|
|
54
|
+
// its leave restores the parent position. Without a position report, restore instead.
|
|
55
|
+
const handOff = () => {
|
|
56
|
+
if (renderBareDialog === undefined || origin === undefined) {
|
|
57
|
+
restore();
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
if (restored)
|
|
61
|
+
return;
|
|
62
|
+
restored = true;
|
|
63
|
+
input.setRawMode?.(wasRaw);
|
|
64
|
+
output.write(`${BEGIN_SYNC}${CLEAR_HOME}\u001b[${origin.row};${origin.column}H${END_SYNC}`);
|
|
65
|
+
};
|
|
42
66
|
const render = () => {
|
|
43
67
|
const width = Math.max(renderBareDialog ? 1 : 20, output.columns ?? 80);
|
|
44
68
|
const rows = Math.max(1, output.rows ?? 24);
|
|
@@ -52,12 +76,17 @@ export function createConsoleProjectTrustPrompt(options = {}) {
|
|
|
52
76
|
"",
|
|
53
77
|
`${DIM}↑/↓${MUTED} to navigate ${DIM}Enter${MUTED} to select ${DIM}Esc${MUTED} to cancel${RESET_FG}`,
|
|
54
78
|
]).map(line => clipAnsiSafe(line, width));
|
|
55
|
-
const
|
|
56
|
-
|
|
79
|
+
const padded = renderBareDialog === undefined
|
|
80
|
+
? lines : [...Array(Math.max(0, rows - lines.length)).fill(""), ...lines];
|
|
81
|
+
// Rationale: overwrite in place within one synchronized frame; clearing first flickers.
|
|
82
|
+
output.write(`${BEGIN_SYNC}${HOME}${padded.map(line => `${line}${ERASE_LINE_END}`).join("\n")}${ERASE_BELOW}${END_SYNC}`);
|
|
57
83
|
};
|
|
58
84
|
try {
|
|
59
85
|
output.write(`${ENTER_ALTERNATE_SCREEN}${HIDE_CURSOR}`);
|
|
60
86
|
input.setRawMode?.(true);
|
|
87
|
+
// Invariant: entering the alternate screen keeps the cursor, so this reports the parent's.
|
|
88
|
+
if (renderBareDialog !== undefined)
|
|
89
|
+
output.write(REQUEST_CURSOR_POSITION);
|
|
61
90
|
render();
|
|
62
91
|
return await new Promise((resolve, reject) => {
|
|
63
92
|
let settled = false;
|
|
@@ -66,7 +95,7 @@ export function createConsoleProjectTrustPrompt(options = {}) {
|
|
|
66
95
|
return;
|
|
67
96
|
settled = true;
|
|
68
97
|
cleanup();
|
|
69
|
-
|
|
98
|
+
handOff();
|
|
70
99
|
resolve(value);
|
|
71
100
|
};
|
|
72
101
|
const fail = (error) => {
|
|
@@ -81,6 +110,13 @@ export function createConsoleProjectTrustPrompt(options = {}) {
|
|
|
81
110
|
const onData = (chunk) => {
|
|
82
111
|
const data = chunk.toString();
|
|
83
112
|
for (let index = 0; index < data.length;) {
|
|
113
|
+
const report = origin === undefined && renderBareDialog !== undefined
|
|
114
|
+
? CURSOR_POSITION_REPORT.exec(data.slice(index)) : null;
|
|
115
|
+
if (report !== null) {
|
|
116
|
+
origin = { row: Number(report[1]), column: Number(report[2]) };
|
|
117
|
+
index += report[0].length;
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
84
120
|
if (data.startsWith("\u001b[A", index)) {
|
|
85
121
|
selected = (selected - 1 + choices.length) % choices.length;
|
|
86
122
|
index += 3;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export interface ReleaseNoteClaim {
|
|
2
|
+
readonly version: string;
|
|
3
|
+
acknowledge(): Promise<void>;
|
|
4
|
+
release(): Promise<void>;
|
|
5
|
+
}
|
|
6
|
+
export interface ReleaseNoteStateOptions {
|
|
7
|
+
readonly configDir: string;
|
|
8
|
+
readonly profileId: string;
|
|
9
|
+
readonly version: string;
|
|
10
|
+
readonly now?: () => number;
|
|
11
|
+
}
|
|
12
|
+
/** Claims one stable note across concurrent sessions and acknowledges it only after presentation closes. */
|
|
13
|
+
export declare function claimReleaseNote(options: ReleaseNoteStateOptions): Promise<ReleaseNoteClaim | null>;
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { mkdir, open, readFile, rename, rm, stat, writeFile } from "node:fs/promises";
|
|
3
|
+
import { join, resolve } from "node:path";
|
|
4
|
+
import semver from "semver";
|
|
5
|
+
const STATE_VERSION = 1;
|
|
6
|
+
const MAX_STATE_BYTES = 4096;
|
|
7
|
+
const CLAIM_MAX_AGE_MS = 60 * 60_000;
|
|
8
|
+
const PROFILE = /^[a-z][a-z0-9-]{0,63}$/u;
|
|
9
|
+
const STABLE = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$/u;
|
|
10
|
+
/** Claims one stable note across concurrent sessions and acknowledges it only after presentation closes. */
|
|
11
|
+
export async function claimReleaseNote(options) {
|
|
12
|
+
if (!PROFILE.test(options.profileId))
|
|
13
|
+
throw new Error("release-note profile is invalid");
|
|
14
|
+
if (!STABLE.test(options.version) || semver.valid(options.version) !== options.version)
|
|
15
|
+
return null;
|
|
16
|
+
const directory = resolve(options.configDir, "release-notes");
|
|
17
|
+
const statePath = join(directory, `${options.profileId}.json`);
|
|
18
|
+
const claimPath = join(directory, `${options.profileId}.claim.json`);
|
|
19
|
+
const acknowledged = await readAcknowledged(statePath);
|
|
20
|
+
if (acknowledged !== null && semver.gte(acknowledged, options.version))
|
|
21
|
+
return null;
|
|
22
|
+
await mkdir(directory, { recursive: true, mode: 0o700 });
|
|
23
|
+
const now = options.now ?? Date.now;
|
|
24
|
+
const token = randomUUID();
|
|
25
|
+
const claim = { version: STATE_VERSION, token, release: options.version, claimedAt: now() };
|
|
26
|
+
if (!await createClaim(claimPath, claim)) {
|
|
27
|
+
if (!await staleClaim(claimPath, now()))
|
|
28
|
+
return null;
|
|
29
|
+
await rm(claimPath, { force: true });
|
|
30
|
+
if (!await createClaim(claimPath, claim))
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
let settled = false;
|
|
34
|
+
const ownsClaim = async () => {
|
|
35
|
+
try {
|
|
36
|
+
const current = await boundedJson(claimPath);
|
|
37
|
+
return current.token === token;
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
return false;
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
const release = async () => {
|
|
44
|
+
if (settled)
|
|
45
|
+
return;
|
|
46
|
+
settled = true;
|
|
47
|
+
if (await ownsClaim())
|
|
48
|
+
await rm(claimPath, { force: true });
|
|
49
|
+
};
|
|
50
|
+
return Object.freeze({
|
|
51
|
+
version: options.version,
|
|
52
|
+
acknowledge: async () => {
|
|
53
|
+
if (settled || !await ownsClaim())
|
|
54
|
+
return;
|
|
55
|
+
const prior = await readAcknowledged(statePath);
|
|
56
|
+
const version = prior !== null && semver.gt(prior, options.version) ? prior : options.version;
|
|
57
|
+
const temporary = `${statePath}.${process.pid}.${token}.tmp`;
|
|
58
|
+
await writeFile(temporary, `${JSON.stringify({ version: STATE_VERSION, acknowledged: version }, null, 2)}\n`, { encoding: "utf8", mode: 0o600 });
|
|
59
|
+
await rename(temporary, statePath);
|
|
60
|
+
settled = true;
|
|
61
|
+
await rm(claimPath, { force: true });
|
|
62
|
+
},
|
|
63
|
+
release,
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
async function readAcknowledged(path) {
|
|
67
|
+
try {
|
|
68
|
+
const value = await boundedJson(path);
|
|
69
|
+
if (value.version !== STATE_VERSION || typeof value.acknowledged !== "string"
|
|
70
|
+
|| !STABLE.test(value.acknowledged) || semver.valid(value.acknowledged) !== value.acknowledged)
|
|
71
|
+
return null;
|
|
72
|
+
return value.acknowledged;
|
|
73
|
+
}
|
|
74
|
+
catch (error) {
|
|
75
|
+
return error?.code === "ENOENT" ? null : null;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
async function boundedJson(path) {
|
|
79
|
+
const info = await stat(path);
|
|
80
|
+
if (!info.isFile() || info.size > MAX_STATE_BYTES)
|
|
81
|
+
throw new Error("release-note state is invalid");
|
|
82
|
+
return JSON.parse(await readFile(path, "utf8"));
|
|
83
|
+
}
|
|
84
|
+
async function createClaim(path, value) {
|
|
85
|
+
try {
|
|
86
|
+
const handle = await open(path, "wx", 0o600);
|
|
87
|
+
try {
|
|
88
|
+
await handle.writeFile(`${JSON.stringify(value)}\n`, "utf8");
|
|
89
|
+
}
|
|
90
|
+
finally {
|
|
91
|
+
await handle.close();
|
|
92
|
+
}
|
|
93
|
+
return true;
|
|
94
|
+
}
|
|
95
|
+
catch (error) {
|
|
96
|
+
if (error?.code === "EEXIST")
|
|
97
|
+
return false;
|
|
98
|
+
throw error;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
async function staleClaim(path, now) {
|
|
102
|
+
try {
|
|
103
|
+
const value = await boundedJson(path);
|
|
104
|
+
return typeof value.claimedAt !== "number" || !Number.isFinite(value.claimedAt) || now - value.claimedAt > CLAIM_MAX_AGE_MS;
|
|
105
|
+
}
|
|
106
|
+
catch {
|
|
107
|
+
return true;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export interface PackagedReleaseNote {
|
|
2
|
+
readonly version: string;
|
|
3
|
+
readonly markdown: string;
|
|
4
|
+
}
|
|
5
|
+
export interface ReleaseNoteCatalog {
|
|
6
|
+
readonly releases: readonly PackagedReleaseNote[];
|
|
7
|
+
readonly completeMarkdown: string;
|
|
8
|
+
current(version: string): PackagedReleaseNote | null;
|
|
9
|
+
}
|
|
10
|
+
export declare function readPackagedReleaseNotes(path?: URL): Promise<ReleaseNoteCatalog>;
|