@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.
Files changed (67) hide show
  1. package/README.md +16 -8
  2. package/bin/pinned-pi-public.js +11 -4
  3. package/dist/app/session-shell/index.d.ts +1 -1
  4. package/dist/app/session-shell/paste-executor.js +6 -1
  5. package/dist/app/session-shell/prompt-chips.d.ts +8 -0
  6. package/dist/app/session-shell/prompt-chips.js +58 -13
  7. package/dist/app/session-shell/session-shell-root.d.ts +21 -3
  8. package/dist/app/session-shell/session-shell-root.js +110 -17
  9. package/dist/app/session-shell/session-shell.d.ts +1 -1
  10. package/dist/app/session-shell/session-shell.js +76 -31
  11. package/dist/app/session-shell/session-viewport-controller.d.ts +13 -0
  12. package/dist/app/session-shell/session-viewport-controller.js +28 -1
  13. package/dist/cli/version-stats.js +4 -7
  14. package/dist/composition/owned-ui.d.ts +7 -0
  15. package/dist/composition/owned-ui.js +71 -3
  16. package/dist/contracts/owned-ui/image-attachments.d.ts +7 -4
  17. package/dist/contracts/owned-ui/image-attachments.js +20 -7
  18. package/dist/contracts/owned-ui/index.d.ts +1 -1
  19. package/dist/contracts/owned-ui/index.js +1 -1
  20. package/dist/contracts/owned-ui/prompt-chips.d.ts +2 -2
  21. package/dist/contracts/owned-ui/prompt-chips.js +9 -6
  22. package/dist/features/owned-ui/project-trust-prompt.js +39 -3
  23. package/dist/features/owned-ui/release-note-state.d.ts +13 -0
  24. package/dist/features/owned-ui/release-note-state.js +109 -0
  25. package/dist/features/owned-ui/release-notes.d.ts +10 -0
  26. package/dist/features/owned-ui/release-notes.js +73 -0
  27. package/dist/features/owned-ui/resources/release-notes.json +9 -0
  28. package/dist/foundation/release/latest-release.d.ts +55 -0
  29. package/dist/foundation/release/latest-release.js +166 -0
  30. package/dist/foundation/release/update.js +9 -0
  31. package/dist/integrations/pi/components/index.d.ts +1 -1
  32. package/dist/integrations/pi/components/index.js +1 -1
  33. package/dist/integrations/pi/components/prompt-chip-presentation.d.ts +3 -0
  34. package/dist/integrations/pi/components/prompt-chip-presentation.js +14 -0
  35. package/dist/integrations/pi/components/shell-footer-status.js +30 -9
  36. package/dist/integrations/pi/components/shell-presenters-info.d.ts +36 -0
  37. package/dist/integrations/pi/components/shell-presenters-info.js +77 -1
  38. package/dist/integrations/pi/components/shell-presenters-transcript.d.ts +0 -6
  39. package/dist/integrations/pi/components/shell-presenters-transcript.js +0 -19
  40. package/dist/integrations/pi/components/shell-shared-facade.d.ts +2 -1
  41. package/dist/integrations/pi/components/submitted-prompt-adapter.d.ts +1 -1
  42. package/dist/integrations/pi/components/submitted-prompt-adapter.js +13 -4
  43. package/dist/integrations/pi/engine/adapter.d.ts +12 -0
  44. package/dist/integrations/pi/engine/adapter.js +13 -0
  45. package/dist/integrations/pi/engine/session-runtime.d.ts +1 -0
  46. package/dist/integrations/pi/engine/session-runtime.js +2 -1
  47. package/dist/native/darwin-arm64/manifest.json +1 -1
  48. package/dist/native/linux-x64/manifest.json +1 -1
  49. package/dist/native/win32-x64/manifest.json +2 -2
  50. package/dist/native/win32-x64/process-guardian.exe +0 -0
  51. package/dist/product-identity.d.ts +1 -1
  52. package/dist/product-identity.js +1 -1
  53. package/dist/product-identity.json +1 -0
  54. package/dist/ui/settings/declarations.d.ts +25 -1
  55. package/dist/ui/settings/declarations.js +19 -1
  56. package/dist/ui/settings/migrations.js +14 -0
  57. package/docs/architecture/internal-naming.md +2 -2
  58. package/docs/architecture/resource-and-data-policy.md +11 -0
  59. package/docs/architecture/toolchain.md +1 -1
  60. package/docs/architecture/ui-reference-provenance.md +1 -0
  61. package/docs/ci-release-runbook.md +154 -70
  62. package/docs/local-worktree-cleanup.md +21 -4
  63. package/docs/manual-owned-ui-checkpoint.md +1 -0
  64. package/docs/openspec-archive-automation.md +1 -1
  65. package/docs/releases/0.2.2.md +47 -0
  66. package/docs/validation.md +13 -3
  67. 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
- text.setText(next);
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
- return text.render(width);
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: live spinner placement follows semantic lifecycle, not text. An extension
170
- // override is visible only when its lifecycle is busy, so a stale override cannot spin
171
- // after completion or make an idle informational status scrollable.
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;