@timurproko/a1 0.2.1 → 0.2.2
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 +71 -3
- 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 +154 -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
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import semver from "semver";
|
|
3
|
+
const RESOURCE_SCHEMA = "a1-release-notes-v1";
|
|
4
|
+
const MAX_RESOURCE_BYTES = 1024 * 1024;
|
|
5
|
+
const MAX_NOTE_BYTES = 128 * 1024;
|
|
6
|
+
const STABLE = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$/u;
|
|
7
|
+
export async function readPackagedReleaseNotes(path = new URL("./resources/release-notes.json", import.meta.url)) {
|
|
8
|
+
const bytes = await readFile(path);
|
|
9
|
+
if (bytes.length > MAX_RESOURCE_BYTES)
|
|
10
|
+
throw new Error("A1 release notes resource is too large");
|
|
11
|
+
const value = JSON.parse(bytes.toString("utf8"));
|
|
12
|
+
if (!value || typeof value !== "object" || value.schema !== RESOURCE_SCHEMA
|
|
13
|
+
|| !Array.isArray(value.releases))
|
|
14
|
+
throw new Error("A1 release notes resource is invalid");
|
|
15
|
+
const seen = new Set();
|
|
16
|
+
let previous = null;
|
|
17
|
+
const releases = value.releases.map((item) => {
|
|
18
|
+
const release = item;
|
|
19
|
+
if (typeof release.version !== "string" || !STABLE.test(release.version) || semver.valid(release.version) !== release.version
|
|
20
|
+
|| typeof release.markdown !== "string" || !validMarkdown(release.markdown, release.version)) {
|
|
21
|
+
throw new Error("A1 release note entry is invalid");
|
|
22
|
+
}
|
|
23
|
+
if (seen.has(release.version) || (previous !== null && semver.gte(release.version, previous))) {
|
|
24
|
+
throw new Error("A1 release note order is invalid");
|
|
25
|
+
}
|
|
26
|
+
seen.add(release.version);
|
|
27
|
+
previous = release.version;
|
|
28
|
+
return Object.freeze({ version: release.version, markdown: release.markdown.replaceAll("\r\n", "\n").trimEnd() });
|
|
29
|
+
});
|
|
30
|
+
const frozen = Object.freeze(releases);
|
|
31
|
+
return Object.freeze({
|
|
32
|
+
releases: frozen,
|
|
33
|
+
completeMarkdown: frozen.length === 0 ? "No A1 release notes found."
|
|
34
|
+
: frozen.map(release => release.markdown.startsWith(`## [${release.version}] - `)
|
|
35
|
+
? release.markdown : `## [${release.version}]\n\n${release.markdown}`).join("\n\n"),
|
|
36
|
+
current: (version) => STABLE.test(version) ? frozen.find(release => release.version === version) ?? null : null,
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
function validMarkdown(markdown, version) {
|
|
40
|
+
if (Buffer.byteLength(markdown, "utf8") > MAX_NOTE_BYTES
|
|
41
|
+
|| /[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f]/u.test(markdown)
|
|
42
|
+
|| [...markdown.matchAll(/^# A1 \d+\.\d+\.\d+$/gmu)].length !== 0
|
|
43
|
+
|| markdown.trim() === ""
|
|
44
|
+
|| /<\/?[A-Za-z][A-Za-z0-9-]*(?:\s[^>]*)?\/?>|\b(?:href|src)\s*=/iu.test(markdown))
|
|
45
|
+
return false;
|
|
46
|
+
const headings = [...markdown.matchAll(/^## \[((?:0|[1-9]\d*)\.(?:0|[1-9]\d*)\.(?:0|[1-9]\d*))\] - (\d{4}-\d{2}-\d{2})$/gmu)];
|
|
47
|
+
if (headings.length > 1 || (headings.length === 1 && (headings[0]?.[1] !== version || !validDate(headings[0]?.[2]))))
|
|
48
|
+
return false;
|
|
49
|
+
const links = [
|
|
50
|
+
...[...markdown.matchAll(/\[[^\]]*\]\(([^)\s]+)(?:\s+[^)]*)?\)/gu)].map(match => match[1] ?? ""),
|
|
51
|
+
...[...markdown.matchAll(/<([^>\s]+:[^>]*)>/gu)].map(match => match[1] ?? ""),
|
|
52
|
+
];
|
|
53
|
+
return links.every(safeLink);
|
|
54
|
+
}
|
|
55
|
+
function validDate(value) {
|
|
56
|
+
if (!value || !/^\d{4}-\d{2}-\d{2}$/u.test(value))
|
|
57
|
+
return false;
|
|
58
|
+
const parsed = new Date(`${value}T00:00:00.000Z`);
|
|
59
|
+
return !Number.isNaN(parsed.valueOf()) && parsed.toISOString().slice(0, 10) === value;
|
|
60
|
+
}
|
|
61
|
+
function safeLink(target) {
|
|
62
|
+
if (target.startsWith("https://")) {
|
|
63
|
+
try {
|
|
64
|
+
const url = new URL(target);
|
|
65
|
+
return Boolean(url.hostname) && !url.username && !url.password;
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
return false;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return Boolean(target) && !target.startsWith("/") && !target.includes("\\")
|
|
72
|
+
&& !target.split(/[/?#]/u).includes("..") && !/^[A-Za-z][A-Za-z0-9+.-]*:/u.test(target);
|
|
73
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
{
|
|
2
|
+
"schema": "a1-release-notes-v1",
|
|
3
|
+
"releases": [
|
|
4
|
+
{
|
|
5
|
+
"version": "0.2.2",
|
|
6
|
+
"markdown": "## [0.2.2] - 2026-09-30\n\n### Breaking Changes\n\n- publish from the native Publish release button ([#629](https://github.com/timurproko/a1/pull/629))\n\n### New Features\n\n- review changelog before publish ([#613](https://github.com/timurproko/a1/pull/613))\n- review notes in draft release ([#617](https://github.com/timurproko/a1/pull/617))\n- configure prompt image limit ([#622](https://github.com/timurproko/a1/pull/622))\n- refresh ready PRs after merges ([#625](https://github.com/timurproko/a1/pull/625))\n- refresh a draft bound to an older develop in place ([#635](https://github.com/timurproko/a1/pull/635))\n- show live validation jobs while waiting ([#637](https://github.com/timurproko/a1/pull/637))\n- keep one draft per stable version ([#638](https://github.com/timurproko/a1/pull/638))\n- delete a release tag left behind by a deleted Release ([#640](https://github.com/timurproko/a1/pull/640))\n- announce newer A1 releases on startup ([#643](https://github.com/timurproko/a1/pull/643))\n\n### Changed\n\n- rename publisher workflow ([#612](https://github.com/timurproko/a1/pull/612))\n- Update README.md ([#614](https://github.com/timurproko/a1/pull/614))\n- Update README.md ([#616](https://github.com/timurproko/a1/pull/616))\n- Update README.md ([#618](https://github.com/timurproko/a1/pull/618))\n- use npm x for installer commands ([#620](https://github.com/timurproko/a1/pull/620))\n- Streamline stable release handoff ([#627](https://github.com/timurproko/a1/pull/627))\n- shard Windows full regression ([#630](https://github.com/timurproko/a1/pull/630))\n- acknowledge candidate release note before resume launches ([#633](https://github.com/timurproko/a1/pull/633))\n- run the upstream sync at 03:23 Central European time ([#641](https://github.com/timurproko/a1/pull/641))\n\n### Fixed\n\n- retire blocked worktrees safely ([#610](https://github.com/timurproko/a1/pull/610))\n- approve publication from Actions ([#619](https://github.com/timurproko/a1/pull/619))\n- keep queued chips unbroken ([#621](https://github.com/timurproko/a1/pull/621))\n- retain pinned Pi runtime root ([#624](https://github.com/timurproko/a1/pull/624))\n- repair the 2026-09-29 full regression failure ([#626](https://github.com/timurproko/a1/pull/626))\n- repair the 2026-09-29-1 publish failure ([#628](https://github.com/timurproko/a1/pull/628))\n- wait for candidate validation and authorize dispatch wrappers ([#631](https://github.com/timurproko/a1/pull/631))\n- keep full-lanes directory in Windows lane artifacts ([#632](https://github.com/timurproko/a1/pull/632))\n- report preparation failures from the default runtime ([#634](https://github.com/timurproko/a1/pull/634))\n- keep the version tag when refreshing a draft ([#636](https://github.com/timurproko/a1/pull/636))\n- capture the stable version before checking the Release id ([#639](https://github.com/timurproko/a1/pull/639))\n- full changelog range, candidate-validated publish, and npm trust preflight ([#644](https://github.com/timurproko/a1/pull/644))\n- repair the 2026-09-30 full regression failure ([#645](https://github.com/timurproko/a1/pull/645))\n- repair the 2026-09-30-2 publish failure ([#646](https://github.com/timurproko/a1/pull/646))\n- stop the trust dialog flashing the terminal ([#647](https://github.com/timurproko/a1/pull/647))\n"
|
|
7
|
+
}
|
|
8
|
+
]
|
|
9
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/** Published release channels: stable `X.Y.Z` on dist-tag `latest`, development `X.Y.Z-dev.N` on `next`. */
|
|
2
|
+
export type ReleaseChannel = "stable" | "development";
|
|
3
|
+
export interface DistTagVersions {
|
|
4
|
+
readonly release: string | null;
|
|
5
|
+
readonly develop: string | null;
|
|
6
|
+
readonly error: string | null;
|
|
7
|
+
}
|
|
8
|
+
export type RegistryFetcher = (url: string) => Promise<{
|
|
9
|
+
readonly ok: boolean;
|
|
10
|
+
readonly status: number;
|
|
11
|
+
text(): Promise<string>;
|
|
12
|
+
}>;
|
|
13
|
+
export interface AvailableRelease {
|
|
14
|
+
readonly channel: ReleaseChannel;
|
|
15
|
+
readonly version: string;
|
|
16
|
+
/** The command that installs it: `a1 update` or `a1 update --develop`. */
|
|
17
|
+
readonly command: string;
|
|
18
|
+
/** The GitHub Release page; development releases have none. */
|
|
19
|
+
readonly changelogUrl: string | null;
|
|
20
|
+
}
|
|
21
|
+
export declare const DEFAULT_NPM_REGISTRY = "https://registry.npmjs.org";
|
|
22
|
+
export declare const UPDATE_CHECK_INTERVAL_MS: number;
|
|
23
|
+
/**
|
|
24
|
+
* The channel a running version updates on, or null for a version that was never published:
|
|
25
|
+
* a bare `X.Y.Z-dev` only comes from a source checkout, which has nothing to update to.
|
|
26
|
+
*/
|
|
27
|
+
export declare function releaseChannelOf(version: string): ReleaseChannel | null;
|
|
28
|
+
/** True only when both versions are valid semver and the candidate sorts strictly above the current one. */
|
|
29
|
+
export declare function isNewerRelease(candidate: string, current: string): boolean;
|
|
30
|
+
export declare function distTagsUrl(registry?: string): string;
|
|
31
|
+
/** Read `latest` (required) and `next` (optional) from a dist-tags object. */
|
|
32
|
+
export declare function parseDistTags(metadata: unknown, source: string): DistTagVersions;
|
|
33
|
+
export declare function queryRegistryDistTags(fetcher: RegistryFetcher, registry?: string): Promise<DistTagVersions>;
|
|
34
|
+
export declare function unavailableDistTags(error: string): DistTagVersions;
|
|
35
|
+
export declare function createRegistryFetcher(timeoutMs: number): RegistryFetcher;
|
|
36
|
+
export interface StartupReleaseCheckOptions {
|
|
37
|
+
readonly runningVersion: string;
|
|
38
|
+
readonly configDir: string;
|
|
39
|
+
readonly environment?: NodeJS.ProcessEnv;
|
|
40
|
+
/** Whether standard output is an interactive terminal. */
|
|
41
|
+
readonly interactive: boolean;
|
|
42
|
+
/** The bare-A1 `updateCheck` setting; comparison profiles omit it. */
|
|
43
|
+
readonly settingEnabled?: boolean;
|
|
44
|
+
readonly fetcher?: RegistryFetcher;
|
|
45
|
+
readonly now?: () => number;
|
|
46
|
+
}
|
|
47
|
+
/** Why the startup check is skipped before any cache read or network access, or null when it may run. */
|
|
48
|
+
export declare function startupReleaseCheckSkipReason(options: StartupReleaseCheckOptions): string | null;
|
|
49
|
+
/**
|
|
50
|
+
* Resolve whether a newer release exists on the running version's channel. A fresh cache for the
|
|
51
|
+
* same channel answers without network access; otherwise the registry is queried and a successful
|
|
52
|
+
* answer replaces the cache. Every failure resolves to null: the notice is best-effort.
|
|
53
|
+
*/
|
|
54
|
+
export declare function checkForNewerRelease(options: StartupReleaseCheckOptions): Promise<AvailableRelease | null>;
|
|
55
|
+
export declare function availableRelease(channel: ReleaseChannel, version: string): AvailableRelease;
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { mkdir, readFile, rename, rm, writeFile } from "node:fs/promises";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { gt, prerelease, valid as validSemver } from "semver";
|
|
5
|
+
import { PRODUCT_IDENTITY, PRODUCT_TEXT } from "../../product-identity.js";
|
|
6
|
+
export const DEFAULT_NPM_REGISTRY = "https://registry.npmjs.org";
|
|
7
|
+
export const UPDATE_CHECK_INTERVAL_MS = 24 * 60 * 60_000;
|
|
8
|
+
const STARTUP_FETCH_TIMEOUT_MS = 3_000;
|
|
9
|
+
const CACHE_FILENAME = "update-check.json";
|
|
10
|
+
const CACHE_VERSION = 1;
|
|
11
|
+
const MAX_CACHE_BYTES = 4096;
|
|
12
|
+
const DEVELOPMENT_BUILD = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)-dev\.(0|[1-9]\d*)$/u;
|
|
13
|
+
const RELEASE_PAGE = "https://github.com/timurproko/a1/releases/tag/v";
|
|
14
|
+
/**
|
|
15
|
+
* The channel a running version updates on, or null for a version that was never published:
|
|
16
|
+
* a bare `X.Y.Z-dev` only comes from a source checkout, which has nothing to update to.
|
|
17
|
+
*/
|
|
18
|
+
export function releaseChannelOf(version) {
|
|
19
|
+
if (validSemver(version) !== version)
|
|
20
|
+
return null;
|
|
21
|
+
if (prerelease(version) === null)
|
|
22
|
+
return "stable";
|
|
23
|
+
return DEVELOPMENT_BUILD.test(version) ? "development" : null;
|
|
24
|
+
}
|
|
25
|
+
/** True only when both versions are valid semver and the candidate sorts strictly above the current one. */
|
|
26
|
+
export function isNewerRelease(candidate, current) {
|
|
27
|
+
const left = validSemver(candidate);
|
|
28
|
+
const right = validSemver(current);
|
|
29
|
+
return left !== null && right !== null && gt(left, right);
|
|
30
|
+
}
|
|
31
|
+
export function distTagsUrl(registry = DEFAULT_NPM_REGISTRY) {
|
|
32
|
+
return `${registry.replace(/\/+$/u, "")}/-/package/${encodeURIComponent(PRODUCT_TEXT.packageName)}/dist-tags`;
|
|
33
|
+
}
|
|
34
|
+
/** Read `latest` (required) and `next` (optional) from a dist-tags object. */
|
|
35
|
+
export function parseDistTags(metadata, source) {
|
|
36
|
+
if (typeof metadata !== "object" || metadata === null || Array.isArray(metadata)) {
|
|
37
|
+
return unavailableDistTags(`${source} returned a non-object dist-tags value`);
|
|
38
|
+
}
|
|
39
|
+
try {
|
|
40
|
+
const tags = metadata;
|
|
41
|
+
const release = parseVersion(tags.latest, `${source} latest`);
|
|
42
|
+
const develop = tags.next === undefined ? null : parseVersion(tags.next, `${source} development channel`);
|
|
43
|
+
return { release, develop, error: null };
|
|
44
|
+
}
|
|
45
|
+
catch (error) {
|
|
46
|
+
return unavailableDistTags(errorMessage(error));
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
export async function queryRegistryDistTags(fetcher, registry) {
|
|
50
|
+
try {
|
|
51
|
+
const response = await fetcher(distTagsUrl(registry));
|
|
52
|
+
if (!response.ok)
|
|
53
|
+
return unavailableDistTags(`registry responded with status ${response.status}`);
|
|
54
|
+
return parseDistTags(JSON.parse(await response.text()), "registry");
|
|
55
|
+
}
|
|
56
|
+
catch (error) {
|
|
57
|
+
return unavailableDistTags(errorMessage(error));
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
export function unavailableDistTags(error) {
|
|
61
|
+
return { release: null, develop: null, error };
|
|
62
|
+
}
|
|
63
|
+
export function createRegistryFetcher(timeoutMs) {
|
|
64
|
+
return async (url) => await fetch(url, { headers: { accept: "application/json" }, signal: AbortSignal.timeout(timeoutMs) });
|
|
65
|
+
}
|
|
66
|
+
/** Why the startup check is skipped before any cache read or network access, or null when it may run. */
|
|
67
|
+
export function startupReleaseCheckSkipReason(options) {
|
|
68
|
+
const environment = options.environment ?? process.env;
|
|
69
|
+
if (truthy(environment.PI_OFFLINE))
|
|
70
|
+
return "offline";
|
|
71
|
+
if (truthy(environment[PRODUCT_IDENTITY.environment.skipVersionCheck]))
|
|
72
|
+
return "disabled-by-environment";
|
|
73
|
+
if (truthy(environment.CI))
|
|
74
|
+
return "ci";
|
|
75
|
+
if (!options.interactive)
|
|
76
|
+
return "not-interactive";
|
|
77
|
+
if (options.settingEnabled === false)
|
|
78
|
+
return "disabled-by-setting";
|
|
79
|
+
if (releaseChannelOf(options.runningVersion) === null)
|
|
80
|
+
return "unpublished-version";
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Resolve whether a newer release exists on the running version's channel. A fresh cache for the
|
|
85
|
+
* same channel answers without network access; otherwise the registry is queried and a successful
|
|
86
|
+
* answer replaces the cache. Every failure resolves to null: the notice is best-effort.
|
|
87
|
+
*/
|
|
88
|
+
export async function checkForNewerRelease(options) {
|
|
89
|
+
if (startupReleaseCheckSkipReason(options) !== null)
|
|
90
|
+
return null;
|
|
91
|
+
const channel = releaseChannelOf(options.runningVersion);
|
|
92
|
+
const environment = options.environment ?? process.env;
|
|
93
|
+
const now = options.now ?? Date.now;
|
|
94
|
+
const cachePath = join(options.configDir, CACHE_FILENAME);
|
|
95
|
+
try {
|
|
96
|
+
let latest = await readFreshCache(cachePath, channel, now());
|
|
97
|
+
if (latest === null) {
|
|
98
|
+
const tags = await queryRegistryDistTags(options.fetcher ?? createRegistryFetcher(STARTUP_FETCH_TIMEOUT_MS), environment.npm_config_registry?.trim() || undefined);
|
|
99
|
+
latest = channel === "stable" ? tags.release : tags.develop;
|
|
100
|
+
if (tags.error !== null || latest === null)
|
|
101
|
+
return null;
|
|
102
|
+
await writeCache(options.configDir, cachePath, { version: CACHE_VERSION, channel, latest, checkedAt: now() }).catch(() => undefined);
|
|
103
|
+
}
|
|
104
|
+
return isNewerRelease(latest, options.runningVersion) ? availableRelease(channel, latest) : null;
|
|
105
|
+
}
|
|
106
|
+
catch {
|
|
107
|
+
return null;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
export function availableRelease(channel, version) {
|
|
111
|
+
return {
|
|
112
|
+
channel,
|
|
113
|
+
version,
|
|
114
|
+
command: channel === "stable" ? `${PRODUCT_TEXT.commandName} update` : `${PRODUCT_TEXT.commandName} update --develop`,
|
|
115
|
+
changelogUrl: channel === "stable" ? `${RELEASE_PAGE}${version}` : null,
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
async function readFreshCache(path, channel, now) {
|
|
119
|
+
let parsed;
|
|
120
|
+
try {
|
|
121
|
+
const source = await readFile(path, "utf8");
|
|
122
|
+
if (source.length > MAX_CACHE_BYTES)
|
|
123
|
+
return null;
|
|
124
|
+
parsed = JSON.parse(source);
|
|
125
|
+
}
|
|
126
|
+
catch {
|
|
127
|
+
return null;
|
|
128
|
+
}
|
|
129
|
+
if (typeof parsed !== "object" || parsed === null)
|
|
130
|
+
return null;
|
|
131
|
+
const cache = parsed;
|
|
132
|
+
if (cache.version !== CACHE_VERSION || cache.channel !== channel)
|
|
133
|
+
return null;
|
|
134
|
+
if (typeof cache.latest !== "string" || validSemver(cache.latest) === null)
|
|
135
|
+
return null;
|
|
136
|
+
if (typeof cache.checkedAt !== "number" || !Number.isFinite(cache.checkedAt))
|
|
137
|
+
return null;
|
|
138
|
+
// Invariant: a timestamp from the future (clock moved back) is stale rather than fresh forever.
|
|
139
|
+
const age = now - cache.checkedAt;
|
|
140
|
+
return age >= 0 && age < UPDATE_CHECK_INTERVAL_MS ? cache.latest : null;
|
|
141
|
+
}
|
|
142
|
+
async function writeCache(directory, path, document) {
|
|
143
|
+
await mkdir(directory, { recursive: true });
|
|
144
|
+
const temporary = `${path}.${randomUUID()}.tmp`;
|
|
145
|
+
try {
|
|
146
|
+
await writeFile(temporary, `${JSON.stringify(document)}\n`, { encoding: "utf8", mode: 0o600 });
|
|
147
|
+
await rename(temporary, path);
|
|
148
|
+
}
|
|
149
|
+
catch (error) {
|
|
150
|
+
await rm(temporary, { force: true });
|
|
151
|
+
throw error;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
function truthy(value) {
|
|
155
|
+
if (value === undefined)
|
|
156
|
+
return false;
|
|
157
|
+
const normalized = value.trim().toLowerCase();
|
|
158
|
+
return normalized !== "" && normalized !== "0" && normalized !== "false";
|
|
159
|
+
}
|
|
160
|
+
function parseVersion(value, source) {
|
|
161
|
+
const parsed = typeof value === "string" ? validSemver(value) : null;
|
|
162
|
+
if (!parsed)
|
|
163
|
+
throw new Error(`${source} did not provide a valid semantic version`);
|
|
164
|
+
return parsed;
|
|
165
|
+
}
|
|
166
|
+
function errorMessage(error) { return error instanceof Error ? error.message : String(error); }
|
|
@@ -5,6 +5,7 @@ import { dirname, isAbsolute, relative, resolve, sep } from "node:path";
|
|
|
5
5
|
import crossSpawn from "cross-spawn";
|
|
6
6
|
import { valid as validSemver } from "semver";
|
|
7
7
|
import { PRODUCT_IDENTITY, PRODUCT_TEXT } from "../../product-identity.js";
|
|
8
|
+
import { isNewerRelease, releaseChannelOf } from "./latest-release.js";
|
|
8
9
|
import { certifyMaterializedRelease, ensureSupervisor, waitForProcessExit, } from "./bootstrap.js";
|
|
9
10
|
import { probeOwnership, readEndpointMetadata, removeEndpointArtifacts } from "./endpoints.js";
|
|
10
11
|
import { resolveCohortEndpoint, resolveProductPaths } from "../lifecycle/index.js";
|
|
@@ -495,6 +496,14 @@ export async function runSelfUpdate(options) {
|
|
|
495
496
|
if (resolved.version === null)
|
|
496
497
|
return resolved.exitCode;
|
|
497
498
|
const targetVersion = resolved.version;
|
|
499
|
+
// Invariant: a channel head below the running release on the same channel is never installed, so a
|
|
500
|
+
// lagging or rolled-back dist-tag cannot downgrade. Switching channels and named previews stay deliberate.
|
|
501
|
+
const sameChannel = releaseChannelOf(runningVersion) === (channel === "stable" ? "stable" : "development");
|
|
502
|
+
if ((requested === undefined || requested.length === 0) && sameChannel
|
|
503
|
+
&& targetVersion !== runningVersion && !isNewerRelease(targetVersion, runningVersion)) {
|
|
504
|
+
output.stdout(`${PRODUCT_TEXT.commandName} is up to date — no update needed.\n`);
|
|
505
|
+
return 0;
|
|
506
|
+
}
|
|
498
507
|
// Rationale: no full stop after a version: it already ends in a dot-separated identifier,
|
|
499
508
|
// and a trailing one reads as part of the version rather than as punctuation.
|
|
500
509
|
output.stdout(`${PRODUCT_TEXT.commandName} update: ${runningVersion} → ${targetVersion}\n`);
|
|
@@ -2,7 +2,7 @@ export { adaptPiAssistantMessage, adaptPiToolExecution, adaptPiUserMessage } fro
|
|
|
2
2
|
export { loadHistoryEditor, type HistoryEditorConstructor } from "./history-editor-loader.js";
|
|
3
3
|
export { PiComponentConformanceError, runPiComponentConformance } from "./conformance.js";
|
|
4
4
|
export type { PiComponentConformanceReport, PiComponentConformanceResult } from "./conformance.js";
|
|
5
|
-
export { OWNED_BUILTIN_SLASH_COMMANDS, PINNED_PI_BUILTIN_SLASH_COMMANDS, componentFromPort, componentPort, createPiExtensionUiBridge, createPiQueuedInputStatus, createPiShellArmin, createPiShellAuthProviderSelector, createPiShellChangelog, createPiShellCollapsedChangelog, createPiShellDaxnuts, createPiShellDialog, createPiShellEarendilAnnouncement, createPiShellEditor, createPiShellExtensionSelector, createPiShellFooter, createPiShellHeader, createPiShellHotkeys, createPiShellLoadedResources, createPiShellLoginDialog, createPiShellModelSelector, createPiShellModelsDialog, createPiShellOperationLoader, createPiShellReloadBox, createPiShellScopedModelsSelector, createPiShellSelector, createPiShellSessionInfo, createPiShellSessionSelector, createPiShellSettingsSelector, createPiShellShowImagesSelector, createPiShellSkillsSelector, createPiShellStatus, createPiShellThemeSelector, createPiShellTranscriptComponent, createPiShellTreeSelector, createPiShellTrustSelector, createPiShellUserMessageSelector, createTuiFacade, ensureTheme, formatSessionTokens, isAutocompleteProvider, isPiPromptStyleCompaction, isRecord, paintPiSubmittedPromptTimestamp, piShellHyperlink, piShellTruncateToWidth, piShellVisibleWidth, renderPiShellChangelogLines, renderPiShellCommandMessage, renderPiShellHotkeysLines, renderPiShellPackageUpdateNotice, renderPiShellStartupDiagnostic, renderPiShellStatusText, renderPiShellTranscriptBlock, validatedAssistantMessage, } from "./shell-components.js";
|
|
5
|
+
export { OWNED_BUILTIN_SLASH_COMMANDS, PINNED_PI_BUILTIN_SLASH_COMMANDS, componentFromPort, componentPort, createPiExtensionUiBridge, createPiQueuedInputStatus, createPiShellArmin, createPiShellAuthProviderSelector, createPiShellChangelog, createPiShellCollapsedChangelog, createPiShellDaxnuts, createPiShellDialog, createPiShellEarendilAnnouncement, createPiShellEditor, createPiShellExtensionSelector, createPiShellFooter, createPiShellHeader, createPiShellHotkeys, createPiShellLoadedResources, createPiShellLoginDialog, createPiShellModelSelector, createPiShellModelsDialog, createPiShellOperationLoader, createPiShellReloadBox, createPiShellScopedModelsSelector, createPiShellSelector, createPiShellSessionInfo, createPiShellSessionSelector, createPiShellSettingsSelector, createPiShellShowImagesSelector, createPiShellSkillsSelector, createPiShellStatus, createPiShellThemeSelector, createPiShellTranscriptComponent, createPiShellTreeSelector, createPiShellTrustSelector, createPiShellUserMessageSelector, createTuiFacade, ensureTheme, formatSessionTokens, isAutocompleteProvider, isPiPromptStyleCompaction, isRecord, paintPiSubmittedPromptTimestamp, piShellHyperlink, piShellTruncateToWidth, piShellVisibleWidth, renderPiShellChangelogLines, renderPiShellCommandMessage, renderPiShellHotkeysLines, renderPiShellPackageUpdateNotice, renderPiShellReleaseUpdateBanner, renderPiShellReleaseUpdateNotice, renderPiShellStartupDiagnostic, renderPiShellStatusText, renderPiShellTranscriptBlock, validatedAssistantMessage, } from "./shell-components.js";
|
|
6
6
|
export { renderPiShellHotkeySections } from "./shell-hotkey-sections.js";
|
|
7
7
|
export type { PiShellHotkeySection } from "./shell-hotkey-sections.js";
|
|
8
8
|
export type { PiExtensionUiBridge, PiExtensionUiBridgeHost, PiShellAuthProviderOption, PiShellAutocompleteCommand, PiShellClipboardContent, PiShellCommandMessagePresentation, PiShellComponentPort, PiShellEditorBodyGeometry, PiShellEditorOptions, PiShellEditorPointerEvent, PiShellEditorPort, PiShellEditorTextRange, PiShellExtensionRendererResolver, PiShellHeaderOptions, PiShellHeaderPort, PiShellHotkeysPresentation, PiShellImageAssetResolver, PiShellLoadedResourcesPort, PiShellLoginDialogPort, PiShellModelSelectorOptions, PiShellModelsDialogOptions, PiShellModelsDialogPort, PiShellOperationLoaderPort, PiShellPasteReservation, PiShellQueuedInputPort, PiShellResourceEntry, PiShellResourceSection, PiShellScopedModelDescriptor, PiShellScopedModelsSelectorOptions, PiShellScopedModelsSelectorPort, PiShellSelectorOption, PiShellSelectorOptions, PiShellSessionInfoPresentation, PiShellSettingsSelectorOptions, PiShellStartupNotice, PiShellStatusPlacement, PiShellStatusPort, PiShellSubmittedPromptComposer, PiShellTranscriptComponentPort, PiShellViewComponentPort, } from "./shell-components.js";
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
export { adaptPiAssistantMessage, adaptPiToolExecution, adaptPiUserMessage } from "./components.js";
|
|
2
2
|
export { loadHistoryEditor } from "./history-editor-loader.js";
|
|
3
3
|
export { PiComponentConformanceError, runPiComponentConformance } from "./conformance.js";
|
|
4
|
-
export { OWNED_BUILTIN_SLASH_COMMANDS, PINNED_PI_BUILTIN_SLASH_COMMANDS, componentFromPort, componentPort, createPiExtensionUiBridge, createPiQueuedInputStatus, createPiShellArmin, createPiShellAuthProviderSelector, createPiShellChangelog, createPiShellCollapsedChangelog, createPiShellDaxnuts, createPiShellDialog, createPiShellEarendilAnnouncement, createPiShellEditor, createPiShellExtensionSelector, createPiShellFooter, createPiShellHeader, createPiShellHotkeys, createPiShellLoadedResources, createPiShellLoginDialog, createPiShellModelSelector, createPiShellModelsDialog, createPiShellOperationLoader, createPiShellReloadBox, createPiShellScopedModelsSelector, createPiShellSelector, createPiShellSessionInfo, createPiShellSessionSelector, createPiShellSettingsSelector, createPiShellShowImagesSelector, createPiShellSkillsSelector, createPiShellStatus, createPiShellThemeSelector, createPiShellTranscriptComponent, createPiShellTreeSelector, createPiShellTrustSelector, createPiShellUserMessageSelector, createTuiFacade, ensureTheme, formatSessionTokens, isAutocompleteProvider, isPiPromptStyleCompaction, isRecord, paintPiSubmittedPromptTimestamp, piShellHyperlink, piShellTruncateToWidth, piShellVisibleWidth, renderPiShellChangelogLines, renderPiShellCommandMessage, renderPiShellHotkeysLines, renderPiShellPackageUpdateNotice, renderPiShellStartupDiagnostic, renderPiShellStatusText, renderPiShellTranscriptBlock, validatedAssistantMessage, } from "./shell-components.js";
|
|
4
|
+
export { OWNED_BUILTIN_SLASH_COMMANDS, PINNED_PI_BUILTIN_SLASH_COMMANDS, componentFromPort, componentPort, createPiExtensionUiBridge, createPiQueuedInputStatus, createPiShellArmin, createPiShellAuthProviderSelector, createPiShellChangelog, createPiShellCollapsedChangelog, createPiShellDaxnuts, createPiShellDialog, createPiShellEarendilAnnouncement, createPiShellEditor, createPiShellExtensionSelector, createPiShellFooter, createPiShellHeader, createPiShellHotkeys, createPiShellLoadedResources, createPiShellLoginDialog, createPiShellModelSelector, createPiShellModelsDialog, createPiShellOperationLoader, createPiShellReloadBox, createPiShellScopedModelsSelector, createPiShellSelector, createPiShellSessionInfo, createPiShellSessionSelector, createPiShellSettingsSelector, createPiShellShowImagesSelector, createPiShellSkillsSelector, createPiShellStatus, createPiShellThemeSelector, createPiShellTranscriptComponent, createPiShellTreeSelector, createPiShellTrustSelector, createPiShellUserMessageSelector, createTuiFacade, ensureTheme, formatSessionTokens, isAutocompleteProvider, isPiPromptStyleCompaction, isRecord, paintPiSubmittedPromptTimestamp, piShellHyperlink, piShellTruncateToWidth, piShellVisibleWidth, renderPiShellChangelogLines, renderPiShellCommandMessage, renderPiShellHotkeysLines, renderPiShellPackageUpdateNotice, renderPiShellReleaseUpdateBanner, renderPiShellReleaseUpdateNotice, renderPiShellStartupDiagnostic, renderPiShellStatusText, renderPiShellTranscriptBlock, validatedAssistantMessage, } from "./shell-components.js";
|
|
5
5
|
export { renderPiShellHotkeySections } from "./shell-hotkey-sections.js";
|
|
6
6
|
export { PINNED_PI_LAYOUT, applyConfiguredPiTheme, applyPiTheme, applyPiThemeInstance, currentPiThemeName, detectPiTerminalBackgroundFromEnv, detectPiTerminalBackgroundTheme, detectPiTerminalThemeForAuto, ensurePiTheme, getAvailablePiThemes, loadPiTheme, onPiThemeChange, parsePiAutoThemeSetting, piTheme, resolvePiThemeSetting, stopPiThemeWatcher, } from "./theme.js";
|
|
7
7
|
export { OwnedPiThemeController } from "./upstream/theme/theme-controller.js";
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import { type PromptChipWrapProtection } from "../../../contracts/owned-ui/index.js";
|
|
2
|
+
/** Keeps fitting chips atomic and presents an oversized chip as one width-bounded ellipsized token. */
|
|
3
|
+
export declare function protectPiPromptChipPresentation(text: string, width: number): PromptChipWrapProtection;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
|
|
2
|
+
import { protectPromptChipWrapping } from "../../../contracts/owned-ui/index.js";
|
|
3
|
+
/** Keeps fitting chips atomic and presents an oversized chip as one width-bounded ellipsized token. */
|
|
4
|
+
export function protectPiPromptChipPresentation(text, width) {
|
|
5
|
+
const available = Math.max(1, width);
|
|
6
|
+
return protectPromptChipWrapping(text, chip => truncateChip(chip, available));
|
|
7
|
+
}
|
|
8
|
+
function truncateChip(chip, width) {
|
|
9
|
+
if (visibleWidth(chip) <= width)
|
|
10
|
+
return chip;
|
|
11
|
+
if (width < 3)
|
|
12
|
+
return "…";
|
|
13
|
+
return `${truncateToWidth(chip.slice(0, -1), width - 2, "")}…]`;
|
|
14
|
+
}
|
|
@@ -3,6 +3,7 @@ import { Spacer, Text, } from "@earendil-works/pi-tui";
|
|
|
3
3
|
import { SessionFooter } from "./upstream/components/session-footer.js";
|
|
4
4
|
import { KeybindingsManager } from "./upstream/adjacent/core/keybindings.js";
|
|
5
5
|
import { StatusIndicator, WorkingStatusIndicator } from "./upstream/components/status-indicator.js";
|
|
6
|
+
import { protectPiPromptChipPresentation } from "./prompt-chip-presentation.js";
|
|
6
7
|
import { PINNED_PI_LAYOUT, piTheme, } from "./theme.js";
|
|
7
8
|
import { createTuiFacade, ensureTheme, } from "./shell-shared-facade.js";
|
|
8
9
|
export function createPiShellHeader(options = {}) {
|
|
@@ -74,14 +75,15 @@ export function createPiShellStatus(view, progressStatus, runtime) {
|
|
|
74
75
|
ensureTheme();
|
|
75
76
|
const statusUi = createTuiFacade(runtime ?? { getColumns: () => 80, getRows: () => 24, requestRender() { } });
|
|
76
77
|
let workingOverride;
|
|
78
|
+
let workingOverrideActive = false;
|
|
77
79
|
let outputPad = PINNED_PI_LAYOUT.outputPad;
|
|
78
80
|
let progressPresentation = "pinned";
|
|
79
|
-
let placement = statusPlacement(view, workingOverride);
|
|
81
|
+
let placement = statusPlacement(view, workingOverride, workingOverrideActive);
|
|
80
82
|
const liveStatusText = () => progressStatus.text(liveWorkingText(view, workingOverride, progressPresentation), progressPresentation);
|
|
81
83
|
let component = statusComponent(view, statusUi, outputPad, liveStatusText, placement, progressPresentation, progressStatus);
|
|
82
84
|
let signature = statusSignature(view, workingOverride, outputPad, placement, progressPresentation);
|
|
83
85
|
const rebuild = () => {
|
|
84
|
-
const nextPlacement = statusPlacement(view, workingOverride);
|
|
86
|
+
const nextPlacement = statusPlacement(view, workingOverride, workingOverrideActive);
|
|
85
87
|
const nextSignature = statusSignature(view, workingOverride, outputPad, nextPlacement, progressPresentation);
|
|
86
88
|
if (nextSignature === signature)
|
|
87
89
|
return;
|
|
@@ -112,8 +114,9 @@ export function createPiShellStatus(view, progressStatus, runtime) {
|
|
|
112
114
|
view = next;
|
|
113
115
|
rebuild();
|
|
114
116
|
},
|
|
115
|
-
setWorkingOverride(message) {
|
|
117
|
+
setWorkingOverride(message, active = false) {
|
|
116
118
|
workingOverride = message;
|
|
119
|
+
workingOverrideActive = active && message !== undefined;
|
|
117
120
|
rebuild();
|
|
118
121
|
},
|
|
119
122
|
setOutputPad(padding) {
|
|
@@ -143,20 +146,28 @@ export function createPiShellFooter(view, cwd, profile = "pi", showLevel = () =>
|
|
|
143
146
|
}
|
|
144
147
|
export function createPiQueuedInputStatus(submissions, presentation = "pinned", getKeybindings) {
|
|
145
148
|
let renderedText = queuedInputText(submissions, presentation, getKeybindings?.());
|
|
149
|
+
let chipWrapping = { text: renderedText, restore: value => value };
|
|
150
|
+
let protectedWidth;
|
|
146
151
|
const text = new Text(renderedText, 1, 0);
|
|
147
152
|
const refresh = () => {
|
|
148
153
|
const next = queuedInputText(submissions, presentation, getKeybindings?.());
|
|
149
154
|
if (next === renderedText)
|
|
150
155
|
return;
|
|
151
156
|
renderedText = next;
|
|
152
|
-
|
|
157
|
+
protectedWidth = undefined;
|
|
153
158
|
};
|
|
154
159
|
return {
|
|
155
160
|
render(width) {
|
|
156
161
|
if (submissions.length === 0)
|
|
157
162
|
return [];
|
|
158
163
|
refresh();
|
|
159
|
-
|
|
164
|
+
const contentWidth = queuedTextContentWidth(width);
|
|
165
|
+
if (contentWidth !== protectedWidth) {
|
|
166
|
+
chipWrapping = queuedInputChipWrapping(renderedText, presentation, contentWidth);
|
|
167
|
+
text.setText(chipWrapping.text);
|
|
168
|
+
protectedWidth = contentWidth;
|
|
169
|
+
}
|
|
170
|
+
return text.render(width).map(row => chipWrapping.restore(row));
|
|
160
171
|
},
|
|
161
172
|
invalidate: () => text.invalidate(),
|
|
162
173
|
update(next) {
|
|
@@ -165,10 +176,11 @@ export function createPiQueuedInputStatus(submissions, presentation = "pinned",
|
|
|
165
176
|
},
|
|
166
177
|
};
|
|
167
178
|
}
|
|
168
|
-
function statusPlacement(view, workingOverride) {
|
|
169
|
-
// Invariant:
|
|
170
|
-
//
|
|
171
|
-
|
|
179
|
+
function statusPlacement(view, workingOverride, workingOverrideActive) {
|
|
180
|
+
// Invariant: extension text alone cannot create a live spinner. A shell-owned operation may
|
|
181
|
+
// explicitly own the brief interval before the engine lifecycle itself becomes busy.
|
|
182
|
+
if (workingOverrideActive && workingOverride !== undefined)
|
|
183
|
+
return "live";
|
|
172
184
|
if (view.lifecycle === "busy")
|
|
173
185
|
return "live";
|
|
174
186
|
if (view.lifecycle === "failed")
|
|
@@ -214,6 +226,15 @@ function statusSignature(view, workingOverride, outputPad, placement, progressPr
|
|
|
214
226
|
const message = withMessage ? `${workingOverride ?? ""}\u0000${view.status.workingMessage ?? ""}\u0000${view.status.workingProgress ?? ""}` : "";
|
|
215
227
|
return `${placement}\u0000${outputPad}\u0000${view.lifecycle}\u0000${progressPresentation}\u0000${message}\u0000${view.status.diagnostics.at(-1) ?? ""}`;
|
|
216
228
|
}
|
|
229
|
+
function queuedInputChipWrapping(text, presentation, width) {
|
|
230
|
+
return presentation === "custom-viewport"
|
|
231
|
+
? protectPiPromptChipPresentation(text, width)
|
|
232
|
+
: { text, restore: value => value };
|
|
233
|
+
}
|
|
234
|
+
function queuedTextContentWidth(width) {
|
|
235
|
+
const padding = Math.min(1, Math.max(0, Math.floor((width - 1) / 2)));
|
|
236
|
+
return Math.max(1, width - padding * 2);
|
|
237
|
+
}
|
|
217
238
|
function queuedInputText(submissions, presentation, bindings) {
|
|
218
239
|
if (submissions.length === 0)
|
|
219
240
|
return "";
|
|
@@ -71,3 +71,39 @@ export declare function createPiShellHotkeys(bindings?: KeybindingsConfig, getSh
|
|
|
71
71
|
/** The keyboard-shortcut document rows the feed presenter shows, without its spacer, borders, and heading. */
|
|
72
72
|
export declare function renderPiShellHotkeysLines(presentation: PiShellHotkeysPresentation, width: number): readonly string[];
|
|
73
73
|
export declare function hotkeysMarkdown(bindings: KeybindingsConfig | undefined, getShortcuts: NonNullable<PiShellExtensionRendererResolver["getShortcuts"]>, profile: "pi" | "a1"): string;
|
|
74
|
+
/**
|
|
75
|
+
* Pinned Pi's `showPackageUpdateNotification` banner: warning-coloured dynamic
|
|
76
|
+
* borders around a bold warning title, the muted update instruction with the
|
|
77
|
+
* accent command, and the package list.
|
|
78
|
+
*/
|
|
79
|
+
export declare function renderPiShellPackageUpdateNotice(packages: readonly string[], width: number): readonly string[];
|
|
80
|
+
/**
|
|
81
|
+
* Pinned Pi's `showNewVersionNotification` banner, naming A1's update command: warning-coloured
|
|
82
|
+
* dynamic borders around a bold warning title, the muted instruction with the accent command, and
|
|
83
|
+
* for stable releases the muted changelog label with an accent hyperlink.
|
|
84
|
+
*/
|
|
85
|
+
export declare function renderPiShellReleaseUpdateNotice(release: {
|
|
86
|
+
readonly version: string;
|
|
87
|
+
readonly command: string;
|
|
88
|
+
readonly changelogUrl: string | null;
|
|
89
|
+
}, width: number): readonly string[];
|
|
90
|
+
export interface PiShellReleaseUpdateBanner {
|
|
91
|
+
readonly rows: readonly string[];
|
|
92
|
+
/** Clickable close area: zero-based rows within `rows`, one-based inclusive columns. */
|
|
93
|
+
readonly close: {
|
|
94
|
+
readonly rowStart: number;
|
|
95
|
+
readonly rowEnd: number;
|
|
96
|
+
readonly columnStart: number;
|
|
97
|
+
readonly columnEnd: number;
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Bare A1's docked variant of the release notice: Pi's notice wording and colours on the prompt
|
|
102
|
+
* band's background instead of borders, so it spans the same width as the editor rules, with a
|
|
103
|
+
* minimal close glyph at the right end of the title row, matching the settings screen's controls.
|
|
104
|
+
*/
|
|
105
|
+
export declare function renderPiShellReleaseUpdateBanner(release: {
|
|
106
|
+
readonly version: string;
|
|
107
|
+
readonly command: string;
|
|
108
|
+
readonly changelogUrl: string | null;
|
|
109
|
+
}, width: number, closeHovered: boolean): PiShellReleaseUpdateBanner;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { DynamicBorder, getMarkdownTheme } from "../startup-public.js";
|
|
2
|
-
import { Container, Markdown, Spacer, Text } from "@earendil-works/pi-tui";
|
|
2
|
+
import { Container, getCapabilities, hyperlink, Markdown, Spacer, Text, visibleWidth } from "@earendil-works/pi-tui";
|
|
3
|
+
import { PRODUCT_TEXT } from "../../../product-identity.js";
|
|
3
4
|
import { KeybindingsManager } from "./upstream/adjacent/core/keybindings.js";
|
|
4
5
|
import { PINNED_PI_LAYOUT, piTheme } from "./theme.js";
|
|
5
6
|
import { componentPort, ensureTheme, formatSessionTokens } from "./shell-shared-facade.js";
|
|
@@ -186,3 +187,78 @@ export function hotkeysMarkdown(bindings, getShortcuts, profile) {
|
|
|
186
187
|
}
|
|
187
188
|
return markdown;
|
|
188
189
|
}
|
|
190
|
+
/**
|
|
191
|
+
* Pinned Pi's `showPackageUpdateNotification` banner: warning-coloured dynamic
|
|
192
|
+
* borders around a bold warning title, the muted update instruction with the
|
|
193
|
+
* accent command, and the package list.
|
|
194
|
+
*/
|
|
195
|
+
export function renderPiShellPackageUpdateNotice(packages, width) {
|
|
196
|
+
ensureTheme();
|
|
197
|
+
const theme = piTheme();
|
|
198
|
+
const container = new Container();
|
|
199
|
+
container.addChild(new Spacer(1));
|
|
200
|
+
container.addChild(new DynamicBorder(text => theme.fg("warning", text)));
|
|
201
|
+
container.addChild(new Text(`${theme.bold(theme.fg("warning", "Package Updates Available"))}\n`
|
|
202
|
+
+ `${theme.fg("muted", "Package updates are available. Run ")}${theme.fg("accent", `${PRODUCT_TEXT.commandName} pi update --extensions`)}\n`
|
|
203
|
+
+ `${theme.fg("muted", "Packages:")}\n`
|
|
204
|
+
+ packages.map(name => `- ${name}`).join("\n"), 1, 0));
|
|
205
|
+
container.addChild(new DynamicBorder(text => theme.fg("warning", text)));
|
|
206
|
+
return container.render(width);
|
|
207
|
+
}
|
|
208
|
+
/**
|
|
209
|
+
* Pinned Pi's `showNewVersionNotification` banner, naming A1's update command: warning-coloured
|
|
210
|
+
* dynamic borders around a bold warning title, the muted instruction with the accent command, and
|
|
211
|
+
* for stable releases the muted changelog label with an accent hyperlink.
|
|
212
|
+
*/
|
|
213
|
+
export function renderPiShellReleaseUpdateNotice(release, width) {
|
|
214
|
+
ensureTheme();
|
|
215
|
+
const theme = piTheme();
|
|
216
|
+
const container = new Container();
|
|
217
|
+
container.addChild(new Spacer(1));
|
|
218
|
+
container.addChild(new DynamicBorder(text => theme.fg("warning", text)));
|
|
219
|
+
container.addChild(new Text(`${theme.bold(theme.fg("warning", "Update Available"))}\n`
|
|
220
|
+
+ `${theme.fg("muted", `New version ${release.version} is available. Run `)}${theme.fg("accent", release.command)}`, 1, 0));
|
|
221
|
+
if (release.changelogUrl !== null) {
|
|
222
|
+
const link = getCapabilities().hyperlinks
|
|
223
|
+
? hyperlink(theme.fg("accent", release.changelogUrl), release.changelogUrl)
|
|
224
|
+
: theme.fg("accent", release.changelogUrl);
|
|
225
|
+
container.addChild(new Text(`${theme.fg("muted", "Changelog: ")}${link}`, 1, 0));
|
|
226
|
+
}
|
|
227
|
+
container.addChild(new DynamicBorder(text => theme.fg("warning", text)));
|
|
228
|
+
return container.render(width);
|
|
229
|
+
}
|
|
230
|
+
const BANNER_PADDING_X = 2;
|
|
231
|
+
/**
|
|
232
|
+
* Bare A1's docked variant of the release notice: Pi's notice wording and colours on the prompt
|
|
233
|
+
* band's background instead of borders, so it spans the same width as the editor rules, with a
|
|
234
|
+
* minimal close glyph at the right end of the title row, matching the settings screen's controls.
|
|
235
|
+
*/
|
|
236
|
+
export function renderPiShellReleaseUpdateBanner(release, width, closeHovered) {
|
|
237
|
+
ensureTheme();
|
|
238
|
+
const theme = piTheme();
|
|
239
|
+
const bandBg = (text) => theme.bg("userMessageBg", text);
|
|
240
|
+
const title = theme.bold(theme.fg("warning", "Update Available"));
|
|
241
|
+
// Compatibility: paints like the settings stepper controls: dim at rest, plain text under the pointer.
|
|
242
|
+
const close = closeHovered ? "✕" : theme.fg("dim", "✕");
|
|
243
|
+
// Rationale: a glyph looks inset by its own side bearing, so one trailing cell after it reads the same
|
|
244
|
+
// as the text's two leading cells.
|
|
245
|
+
const gap = Math.max(1, width - BANNER_PADDING_X - visibleWidth(title) - 2);
|
|
246
|
+
const closeColumn = BANNER_PADDING_X + visibleWidth(title) + gap + 1;
|
|
247
|
+
const titleRow = bandBg(`${" ".repeat(BANNER_PADDING_X)}${title}${" ".repeat(gap)}${close} `);
|
|
248
|
+
const body = [
|
|
249
|
+
`${theme.fg("muted", `New version ${release.version} is available. Run `)}${theme.fg("accent", release.command)}`,
|
|
250
|
+
];
|
|
251
|
+
if (release.changelogUrl !== null) {
|
|
252
|
+
const link = getCapabilities().hyperlinks
|
|
253
|
+
? hyperlink(theme.fg("accent", release.changelogUrl), release.changelogUrl)
|
|
254
|
+
: theme.fg("accent", release.changelogUrl);
|
|
255
|
+
body.push(`${theme.fg("muted", "Changelog: ")}${link}`);
|
|
256
|
+
}
|
|
257
|
+
const padding = bandBg(" ".repeat(width));
|
|
258
|
+
// Invariant: rows are a leading spacer, the band's top padding, then the title row. Hover and click
|
|
259
|
+
// share one area, the glyph and one cell either side, so what lights up is exactly what closes.
|
|
260
|
+
return {
|
|
261
|
+
rows: ["", padding, titleRow, ...new Text(body.join("\n"), BANNER_PADDING_X, 0, bandBg).render(width), padding],
|
|
262
|
+
close: { rowStart: 2, rowEnd: 2, columnStart: closeColumn - 1, columnEnd: Math.min(width, closeColumn + 1) },
|
|
263
|
+
};
|
|
264
|
+
}
|
|
@@ -19,11 +19,5 @@ export declare function renderPiShellStartupDiagnostic(diagnostic: {
|
|
|
19
19
|
readonly severity: "info" | "warning" | "error";
|
|
20
20
|
readonly message: string;
|
|
21
21
|
}, width: number): readonly string[];
|
|
22
|
-
/**
|
|
23
|
-
* Pinned Pi's `showPackageUpdateNotification` banner: warning-coloured dynamic
|
|
24
|
-
* borders around a bold warning title, the muted update instruction with the
|
|
25
|
-
* accent command, and the package list.
|
|
26
|
-
*/
|
|
27
|
-
export declare function renderPiShellPackageUpdateNotice(packages: readonly string[], width: number): readonly string[];
|
|
28
22
|
type PiAssistantMessage = NonNullable<ConstructorParameters<typeof AssistantMessageComponent>[0]>;
|
|
29
23
|
export declare function validatedAssistantMessage(block: OwnedUiTranscriptBlock): PiAssistantMessage;
|