@timurproko/a1 0.1.8-dev.299 → 0.1.8-dev.322
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 -4
- package/bin/guardian.js +4 -3
- package/bin/ui.js +4 -2
- package/bin/update-recovery.js +3 -1
- package/dist/composition/owned-ui.js +2 -1
- package/dist/features/launch/profile-paths.d.ts +2 -0
- package/dist/features/launch/profile-paths.js +11 -0
- package/dist/foundation/launch-context/index.d.ts +29 -0
- package/dist/foundation/launch-context/index.js +83 -0
- package/dist/foundation/launch-guardian/main.js +4 -3
- package/dist/foundation/release/bootstrap.d.ts +1 -1
- package/dist/foundation/release/bootstrap.js +18 -12
- package/dist/foundation/release/cohort-state.d.ts +1 -0
- package/dist/foundation/release/cohort-state.js +4 -0
- package/dist/foundation/release/release-gc.js +2 -2
- package/dist/foundation/release/release-store.js +10 -3
- package/dist/foundation/release/release.d.ts +3 -1
- package/dist/foundation/release/release.js +7 -3
- package/dist/foundation/release/restart-certification.d.ts +1 -0
- package/dist/foundation/release/restart-certification.js +5 -0
- package/dist/foundation/release/update-recovery.d.ts +2 -0
- package/dist/foundation/release/update-recovery.js +4 -0
- package/dist/foundation/release/update.js +5 -0
- package/dist/foundation/release/warmup.d.ts +1 -1
- package/dist/foundation/release/warmup.js +5 -4
- package/dist/foundation/startup/startup-runtime.js +13 -11
- package/dist/foundation/supervision/main.js +8 -7
- package/dist/integrations/pi/components/owned-editor-ux.d.ts +1 -0
- package/dist/integrations/pi/components/owned-editor-ux.js +54 -3
- package/dist/integrations/pi/components/shell-editor-autocomplete.js +3 -0
- package/dist/integrations/pi/components/shell-presenters-info.d.ts +9 -2
- package/dist/integrations/pi/components/shell-presenters-info.js +35 -4
- package/dist/integrations/pi/components/shell-selectors-dialogs.d.ts +3 -2
- package/dist/integrations/pi/components/shell-selectors-dialogs.js +8 -5
- package/dist/integrations/pi/components/shell-shared-facade.d.ts +9 -2
- package/dist/integrations/pi/components/upstream/assets/earendil-image.json +12 -0
- package/dist/integrations/pi/components/upstream/components/earendil-announcement.js +25 -2
- package/dist/integrations/pi/engine/adapter.d.ts +13 -1
- package/dist/integrations/pi/engine/adapter.js +86 -25
- package/dist/integrations/pi/engine/changelog.d.ts +2 -0
- package/dist/integrations/pi/engine/changelog.js +63 -0
- package/dist/integrations/pi/engine/resources/changelog.json +12 -0
- package/dist/integrations/pi/engine/workflow-controllers.d.ts +1 -1
- package/dist/integrations/pi/engine/workflows.d.ts +1 -1
- package/dist/integrations/pi/session-ui/prompt-chips.d.ts +3 -1
- package/dist/integrations/pi/session-ui/prompt-chips.js +37 -13
- package/dist/integrations/pi/session-ui/session-shell-root.d.ts +25 -0
- package/dist/integrations/pi/session-ui/session-shell-root.js +65 -74
- package/dist/integrations/pi/session-ui/session-shell.d.ts +1 -1
- package/dist/integrations/pi/session-ui/session-shell.js +58 -7
- package/dist/integrations/pi/session-ui/session-viewport-controller.d.ts +5 -0
- package/dist/integrations/pi/session-ui/session-viewport-controller.js +21 -15
- package/dist/integrations/pi/tui-runtime/damage-aware-terminal.d.ts +7 -0
- package/dist/integrations/pi/tui-runtime/damage-aware-terminal.js +7 -6
- 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 +2 -35
- package/dist/product-identity.json +0 -22
- package/dist/ui/components/transcript-viewport.d.ts +8 -0
- package/dist/ui/components/transcript-viewport.js +23 -2
- package/dist/ui/components/visible-hyperlinks.d.ts +7 -0
- package/dist/ui/components/visible-hyperlinks.js +1 -0
- package/dist/ui/settings/declarations.js +1 -0
- package/dist/ui/settings/sections.js +11 -1
- package/docs/architecture/boundaries.md +2 -1
- package/docs/architecture/internal-naming.md +89 -0
- package/docs/architecture/resource-and-data-policy.md +9 -4
- package/docs/architecture/toolchain.md +15 -1
- package/docs/features/prompt-history.md +39 -11
- package/package.json +8 -3
|
@@ -5,6 +5,7 @@ import { dirname, isAbsolute, relative, resolve, sep } from "node:path";
|
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
|
6
6
|
import { PRODUCT_IDENTITY, PRODUCT_TEXT } from "../../product-identity.js";
|
|
7
7
|
import { processIsAlive } from "./process-cleanup.js";
|
|
8
|
+
import { PRIVATE_LAUNCH_CONTRACT, assertCurrentLaunchContract } from "../launch-context/index.js";
|
|
8
9
|
export const UPDATE_RECOVERY_SCHEMA = "a1-update-recovery-v1";
|
|
9
10
|
const WORKER_TIMEOUT_MS = 15 * 60 * 1_000;
|
|
10
11
|
const POLL_MS = 50;
|
|
@@ -18,6 +19,7 @@ export function updateLauncherPaths(globalRoot, platform = process.platform) {
|
|
|
18
19
|
/** Validate transaction-scoped recovery authority without trusting npm temporary names. */
|
|
19
20
|
export async function readUpdateRecoveryCapsule(manifestPath, platform = process.platform) {
|
|
20
21
|
const capsule = JSON.parse(await readFile(manifestPath, "utf8"));
|
|
22
|
+
assertCurrentLaunchContract(capsule);
|
|
21
23
|
if (capsule.schema !== UPDATE_RECOVERY_SCHEMA || !/^[0-9a-f-]{36}$/i.test(capsule.transactionId) || typeof capsule.targetVersion !== "string"
|
|
22
24
|
|| capsule.packageName !== PRODUCT_TEXT.packageName || typeof capsule.packageRoot !== "string" || typeof capsule.globalRoot !== "string"
|
|
23
25
|
|| typeof capsule.launcherRoot !== "string" || !Array.isArray(capsule.launchers) || typeof capsule.priorReleaseId !== "string"
|
|
@@ -59,6 +61,7 @@ export async function readUpdateRecoveryCapsule(manifestPath, platform = process
|
|
|
59
61
|
if (priorReleaseRoot.split(sep).at(-1) !== capsule.priorReleaseId)
|
|
60
62
|
throw new Error("recovery prior release identity is invalid");
|
|
61
63
|
const priorManifest = JSON.parse(await readFile(resolve(priorReleaseRoot, capsule.releaseManifestName), "utf8"));
|
|
64
|
+
assertCurrentLaunchContract(priorManifest);
|
|
62
65
|
if (priorManifest.releaseId !== capsule.priorReleaseId || priorManifest.contentDigest !== capsule.priorContentDigest) {
|
|
63
66
|
throw new Error("recovery prior release manifest differs from the capsule");
|
|
64
67
|
}
|
|
@@ -95,6 +98,7 @@ export async function prepareUpdateRecoveryCapsule(options) {
|
|
|
95
98
|
const npmCli = await resolveNpmCli(canonicalGlobal, options.environment ?? process.env);
|
|
96
99
|
const capsule = {
|
|
97
100
|
schema: UPDATE_RECOVERY_SCHEMA,
|
|
101
|
+
launchContract: PRIVATE_LAUNCH_CONTRACT,
|
|
98
102
|
transactionId: options.transaction.transactionId,
|
|
99
103
|
packageName: PRODUCT_TEXT.packageName,
|
|
100
104
|
targetVersion: options.transaction.targetVersion,
|
|
@@ -13,6 +13,7 @@ import { cleanupVerifiedOwner, processIsAlive } from "./process-cleanup.js";
|
|
|
13
13
|
import { materializeRelease, readMaterializedRelease } from "./release-store.js";
|
|
14
14
|
import { scheduleReleaseCleanup } from "./release-gc.js";
|
|
15
15
|
import { warmMaterializedRelease } from "./warmup.js";
|
|
16
|
+
import { assertCurrentLaunchContract } from "../launch-context/index.js";
|
|
16
17
|
import { UpdateTransactionStore } from "./update-transaction.js";
|
|
17
18
|
import { removeUpdateRecoveryCapsule, runProtectedPackageReplacement } from "./update-recovery.js";
|
|
18
19
|
export const PRODUCT_PACKAGE = PRODUCT_TEXT.packageName;
|
|
@@ -77,6 +78,8 @@ export function createUpdateLifecycleCoordinator(environment = process.env, file
|
|
|
77
78
|
async targetIsActive(targetVersion) {
|
|
78
79
|
const state = await stateStore.read();
|
|
79
80
|
const activeId = state.references.active;
|
|
81
|
+
if (activeId)
|
|
82
|
+
assertCurrentLaunchContract(state.releases[activeId] ?? {});
|
|
80
83
|
if (!activeId || state.releases[activeId]?.packageVersion !== targetVersion)
|
|
81
84
|
return false;
|
|
82
85
|
const endpoint = await readActiveEndpoint(paths, activeId, environment);
|
|
@@ -85,6 +88,8 @@ export function createUpdateLifecycleCoordinator(environment = process.env, file
|
|
|
85
88
|
async shutdownVerifiedOwners(targetVersion) {
|
|
86
89
|
const state = await stateStore.read();
|
|
87
90
|
const activeId = state.references.active;
|
|
91
|
+
if (activeId)
|
|
92
|
+
assertCurrentLaunchContract(state.releases[activeId] ?? {});
|
|
88
93
|
const priorActiveVersion = activeId ? state.releases[activeId]?.packageVersion ?? null : null;
|
|
89
94
|
const owner = await readActiveEndpoint(paths, activeId, environment);
|
|
90
95
|
if (!owner)
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type MaterializedRelease } from "./release-store.js";
|
|
2
2
|
/** Import the exact immutable startup graph in a terminal-free bounded child process. */
|
|
3
3
|
export declare function warmMaterializedRelease(release: MaterializedRelease, environment: NodeJS.ProcessEnv, timeoutMs?: number): Promise<void>;
|
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
|
-
import {
|
|
2
|
+
import { resolveReleaseEntryPoint } from "./release-store.js";
|
|
3
3
|
import { releaseEnvironment } from "./bootstrap.js";
|
|
4
|
-
import {
|
|
4
|
+
import { PRIVATE_ENVIRONMENT, assertCurrentLaunchContract } from "../launch-context/index.js";
|
|
5
5
|
/** Import the exact immutable startup graph in a terminal-free bounded child process. */
|
|
6
6
|
export async function warmMaterializedRelease(release, environment, timeoutMs = 30_000) {
|
|
7
|
-
|
|
7
|
+
assertCurrentLaunchContract(release);
|
|
8
|
+
const entry = await resolveReleaseEntryPoint(release, "bin/warmup.js");
|
|
8
9
|
await new Promise((resolvePromise, rejectPromise) => {
|
|
9
10
|
const child = spawn(process.execPath, [entry], {
|
|
10
|
-
env: { ...releaseEnvironment(environment, release), [
|
|
11
|
+
env: { ...releaseEnvironment(environment, release), [PRIVATE_ENVIRONMENT.immutableWarmup]: "1" },
|
|
11
12
|
stdio: "ignore",
|
|
12
13
|
windowsHide: true,
|
|
13
14
|
});
|
|
@@ -5,18 +5,20 @@ import { dirname, resolve } from "node:path";
|
|
|
5
5
|
import { homedir, platform } from "node:os";
|
|
6
6
|
import { performance } from "node:perf_hooks";
|
|
7
7
|
import { PRODUCT_IDENTITY } from "../../product-identity.js";
|
|
8
|
+
import { readLaunchContext } from "../launch-context/index.js";
|
|
8
9
|
/** Initialize opt-in startup tracing from a caller-provided evidence path. */
|
|
9
10
|
export function initializeStartupTrace(environment, profileId, startedAtMs = performance.timeOrigin + performance.now()) {
|
|
10
11
|
const configured = environment[PRODUCT_IDENTITY.environment.startupTrace];
|
|
11
12
|
if (!configured || parseContext(configured) !== null)
|
|
12
13
|
return;
|
|
14
|
+
const launch = readLaunchContext(environment);
|
|
13
15
|
const context = {
|
|
14
16
|
path: resolve(configured),
|
|
15
17
|
traceId: randomUUID(),
|
|
16
18
|
startedAtMs,
|
|
17
19
|
profileId,
|
|
18
|
-
releaseId:
|
|
19
|
-
dependencyLayerIds: parseLayerIds(
|
|
20
|
+
releaseId: launch.releaseId ?? null,
|
|
21
|
+
dependencyLayerIds: parseLayerIds(launch.releaseLayers),
|
|
20
22
|
};
|
|
21
23
|
environment[PRODUCT_IDENTITY.environment.startupTrace] = JSON.stringify(context);
|
|
22
24
|
}
|
|
@@ -25,17 +27,18 @@ export async function markStartupPhase(environment, phase) {
|
|
|
25
27
|
const context = parseContext(environment[PRODUCT_IDENTITY.environment.startupTrace]);
|
|
26
28
|
if (context === null)
|
|
27
29
|
return;
|
|
30
|
+
const elapsedMs = Math.max(0, performance.timeOrigin + performance.now() - context.startedAtMs);
|
|
31
|
+
const launch = readLaunchContext(environment);
|
|
32
|
+
const dependencyLayerIds = parseLayerIds(launch.releaseLayers);
|
|
28
33
|
const event = {
|
|
29
34
|
schema: PRODUCT_IDENTITY.evidence.startupTraceSchema,
|
|
30
35
|
traceId: context.traceId,
|
|
31
36
|
phase,
|
|
32
|
-
elapsedMs
|
|
37
|
+
elapsedMs,
|
|
33
38
|
processId: process.pid,
|
|
34
39
|
profileId: context.profileId,
|
|
35
|
-
releaseId:
|
|
36
|
-
dependencyLayerIds:
|
|
37
|
-
? parseLayerIds(environment[PRODUCT_IDENTITY.environment.releaseLayers])
|
|
38
|
-
: context.dependencyLayerIds,
|
|
40
|
+
releaseId: launch.releaseId ?? context.releaseId,
|
|
41
|
+
dependencyLayerIds: dependencyLayerIds.length > 0 ? dependencyLayerIds : context.dependencyLayerIds,
|
|
39
42
|
nodeVersion: process.version,
|
|
40
43
|
fileReadOperations: process.resourceUsage().fsRead,
|
|
41
44
|
};
|
|
@@ -43,9 +46,7 @@ export async function markStartupPhase(environment, phase) {
|
|
|
43
46
|
await appendFile(context.path, `${JSON.stringify(event)}\n`, { encoding: "utf8", mode: 0o600 });
|
|
44
47
|
}
|
|
45
48
|
export function assertImmutableWarmupEnvironment(environment) {
|
|
46
|
-
|
|
47
|
-
throw new Error("warmup entry is private to verified update activation");
|
|
48
|
-
}
|
|
49
|
+
readLaunchContext(environment, "warmup");
|
|
49
50
|
}
|
|
50
51
|
/** Enable the compile cache directly from launch environment without loading profile services. */
|
|
51
52
|
export function enableEnvironmentCompileCache(environment) {
|
|
@@ -54,7 +55,8 @@ export function enableEnvironmentCompileCache(environment) {
|
|
|
54
55
|
?? (platform() === "win32"
|
|
55
56
|
? resolve(environment.LOCALAPPDATA ?? home, PRODUCT_IDENTITY.state.windowsControlDirectory)
|
|
56
57
|
: resolve(environment.XDG_DATA_HOME ?? resolve(home, ".local", "share"), PRODUCT_IDENTITY.state.unixControlDirectory)));
|
|
57
|
-
|
|
58
|
+
const launch = readLaunchContext(environment);
|
|
59
|
+
return enableStartupCompileCache(dataDir, launch.releaseId ?? null, parseLayerIds(launch.releaseLayers));
|
|
58
60
|
}
|
|
59
61
|
/** Enable Node's supported persistent compile cache in an immutable-identity namespace. */
|
|
60
62
|
export function enableStartupCompileCache(dataDir, releaseId, dependencyLayerIds) {
|
|
@@ -6,16 +6,18 @@ import { publishSupervisorStartupResult, supervisorStartupFailure, supervisorSta
|
|
|
6
6
|
import { ControlStore } from "../storage/index.js";
|
|
7
7
|
import { resolveCohortEndpoint, resolveProductPaths } from "./paths.js";
|
|
8
8
|
import { SupervisorServer } from "./server.js";
|
|
9
|
-
import {
|
|
9
|
+
import { PRODUCT_TEXT } from "../../product-identity.js";
|
|
10
|
+
import { readLaunchContext } from "../launch-context/index.js";
|
|
10
11
|
export async function runSupervisor(arguments_ = []) {
|
|
11
12
|
const productPaths = resolveProductPaths();
|
|
12
13
|
mkdirSync(productPaths.runtimeDir, { recursive: true, mode: 0o700 });
|
|
13
14
|
mkdirSync(productPaths.endpointsDir, { recursive: true, mode: 0o700 });
|
|
14
15
|
let paths = productPaths;
|
|
15
16
|
const log = (message) => appendFileSync(paths.supervisorLogPath, `${new Date().toISOString()} ${message}\n`);
|
|
16
|
-
const
|
|
17
|
-
const
|
|
18
|
-
const
|
|
17
|
+
const context = readLaunchContext(process.env, "release");
|
|
18
|
+
const releaseRoot = context.releaseRoot;
|
|
19
|
+
const releaseId = context.releaseId;
|
|
20
|
+
const contentDigest = context.releaseDigest;
|
|
19
21
|
const attemptId = startupAttempt(arguments_);
|
|
20
22
|
const resultPath = attemptId ? supervisorStartupResultPath(productPaths.runtimeDir, attemptId) : null;
|
|
21
23
|
let stage = "release-environment";
|
|
@@ -24,9 +26,8 @@ export async function runSupervisor(arguments_ = []) {
|
|
|
24
26
|
if (!releaseRoot || !releaseId || !contentDigest)
|
|
25
27
|
throw new Error(PRODUCT_TEXT.diagnostic("supervisor must be launched from a verified immutable release"));
|
|
26
28
|
stage = "release-certification";
|
|
27
|
-
const release = await readCertifiedReleaseManifest({ releaseRoot, releaseId, contentDigest }, resolve(paths.dataDir, "releases")
|
|
28
|
-
//
|
|
29
|
-
// existed, even when its layer certification has already been upgraded by a retry.
|
|
29
|
+
const release = await readCertifiedReleaseManifest({ releaseRoot, releaseId, contentDigest }, resolve(paths.dataDir, "releases"));
|
|
30
|
+
// Invariant: readiness follows current-contract certification, never an old-updater upgrade path.
|
|
30
31
|
await recordParentCertifiedRelease(release, paths.dataDir);
|
|
31
32
|
stage = "immutable-root";
|
|
32
33
|
await assertImmutableExecutionRoot(release, paths.dataDir);
|
|
@@ -52,6 +52,7 @@ export interface PromptSelectionUxOptions {
|
|
|
52
52
|
};
|
|
53
53
|
readonly transformPastedContent: (content: PiShellClipboardContent) => string;
|
|
54
54
|
readonly atomicRanges: (line: string) => readonly PiShellEditorTextRange[];
|
|
55
|
+
readonly hiddenRanges?: (line: string) => readonly PiShellEditorTextRange[];
|
|
55
56
|
readonly expandCopiedText: (text: string) => string;
|
|
56
57
|
readonly paintSelection: (line: string, from: number, to: number, atomic: boolean) => string;
|
|
57
58
|
readonly decorateRow: (row: string, width: number) => string;
|
|
@@ -202,6 +202,47 @@ class PromptSelectionInterceptor {
|
|
|
202
202
|
this.#redoStack = [];
|
|
203
203
|
}
|
|
204
204
|
render(width, next) {
|
|
205
|
+
const state = editorState(this.editor);
|
|
206
|
+
const hidden = state.lines.map(line => this.options.hiddenRanges?.(line) ?? []);
|
|
207
|
+
if (!hidden.some(ranges => ranges.length > 0))
|
|
208
|
+
return this.#renderVisible(width, next);
|
|
209
|
+
const original = { ...state, lines: [...state.lines] };
|
|
210
|
+
const selection = this.#selection;
|
|
211
|
+
const project = (position) => ({
|
|
212
|
+
line: position.line,
|
|
213
|
+
col: hideColumn(position.col, hidden[position.line] ?? []),
|
|
214
|
+
});
|
|
215
|
+
const lines = state.lines.map((line, index) => {
|
|
216
|
+
for (const range of [...(hidden[index] ?? [])].reverse())
|
|
217
|
+
line = line.slice(0, range.start) + line.slice(range.end);
|
|
218
|
+
return line;
|
|
219
|
+
});
|
|
220
|
+
// Rationale: project pending clipboard identities out before layout, not after painting.
|
|
221
|
+
// Keep the semantic draft/undo/submission markers intact and restore them even if rendering throws.
|
|
222
|
+
try {
|
|
223
|
+
this.#setRenderState(lines, project({ line: original.cursorLine, col: original.cursorCol }));
|
|
224
|
+
if (selection !== undefined)
|
|
225
|
+
this.#selection = { anchor: project(selection.anchor), head: project(selection.head) };
|
|
226
|
+
const rows = this.#renderVisible(width, next);
|
|
227
|
+
if (this.#geometry !== undefined) {
|
|
228
|
+
this.#geometry.hiddenRanges = hidden;
|
|
229
|
+
this.#geometry.lines = lines;
|
|
230
|
+
}
|
|
231
|
+
return rows;
|
|
232
|
+
}
|
|
233
|
+
finally {
|
|
234
|
+
this.#setRenderState(original.lines, { line: original.cursorLine, col: original.cursorCol });
|
|
235
|
+
this.#selection = selection;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
#setRenderState(lines, cursor) {
|
|
239
|
+
if (this.editor.interaction !== undefined)
|
|
240
|
+
this.editor.interaction.replaceLines(lines);
|
|
241
|
+
else
|
|
242
|
+
editorState(this.editor).lines = lines;
|
|
243
|
+
this.#setCursor(cursor);
|
|
244
|
+
}
|
|
245
|
+
#renderVisible(width, next) {
|
|
205
246
|
const rows = next().map(row => row.replaceAll(ATOMIC_SPACE_SENTINEL, " "));
|
|
206
247
|
const maxPadding = Math.max(0, Math.floor((width - 1) / 2));
|
|
207
248
|
const padding = Math.min(this.editor.getPaddingX(), maxPadding);
|
|
@@ -214,7 +255,7 @@ class PromptSelectionInterceptor {
|
|
|
214
255
|
const scrollOffset = this.editor.interaction?.scrollOffset() ?? numericProperty(this.editor, "scrollOffset");
|
|
215
256
|
const maxVisibleLines = Math.max(5, Math.floor(this.options.getRows() * 0.3));
|
|
216
257
|
const textRows = Math.max(0, Math.min(visualLines.length - scrollOffset, maxVisibleLines, rows.length - 2));
|
|
217
|
-
this.#geometry = { width, padding, layoutWidth, visualLines, scrollOffset, textRows };
|
|
258
|
+
this.#geometry = { width, padding, layoutWidth, visualLines, scrollOffset, textRows, hiddenRanges: [] };
|
|
218
259
|
if (this.#atomicFocus() !== undefined) {
|
|
219
260
|
for (let row = 0; row < rows.length; row += 1) {
|
|
220
261
|
rows[row] = (rows[row] ?? "").replaceAll(CURSOR_MARKER, "");
|
|
@@ -344,12 +385,12 @@ class PromptSelectionInterceptor {
|
|
|
344
385
|
const visual = geometry.visualLines[geometry.scrollOffset + textRow];
|
|
345
386
|
if (visual === undefined)
|
|
346
387
|
return undefined;
|
|
347
|
-
const line = editorState(this.editor).lines[visual.logicalLine] ?? "";
|
|
388
|
+
const line = (geometry.lines ?? editorState(this.editor).lines)[visual.logicalLine] ?? "";
|
|
348
389
|
const segment = line.slice(visual.startCol, visual.startCol + visual.length);
|
|
349
390
|
const displayColumn = Math.max(0, column - 1 - geometry.padding - (this.options.promptPrefixWidth ?? 0));
|
|
350
391
|
return {
|
|
351
392
|
line: visual.logicalLine,
|
|
352
|
-
col: visual.startCol + indexAtDisplayWidth(segment, displayColumn),
|
|
393
|
+
col: restoreColumn(visual.startCol + indexAtDisplayWidth(segment, displayColumn), geometry.hiddenRanges[visual.logicalLine] ?? []),
|
|
353
394
|
};
|
|
354
395
|
}
|
|
355
396
|
#selectWord(position) {
|
|
@@ -804,6 +845,16 @@ function samePosition(left, right) {
|
|
|
804
845
|
function sameSnapshot(left, right) {
|
|
805
846
|
return left.text === right.text && samePosition(left.cursor, right.cursor);
|
|
806
847
|
}
|
|
848
|
+
function hideColumn(column, ranges) {
|
|
849
|
+
return column - ranges.reduce((removed, range) => removed + Math.max(0, Math.min(column, range.end) - range.start), 0);
|
|
850
|
+
}
|
|
851
|
+
function restoreColumn(column, ranges) {
|
|
852
|
+
let restored = column;
|
|
853
|
+
for (const range of ranges)
|
|
854
|
+
if (restored >= range.start)
|
|
855
|
+
restored += range.end - range.start;
|
|
856
|
+
return restored;
|
|
857
|
+
}
|
|
807
858
|
function editorState(editor) {
|
|
808
859
|
if (editor.interaction !== undefined)
|
|
809
860
|
return editor.interaction.snapshot();
|
|
@@ -64,6 +64,7 @@ export function createPiShellEditor(options) {
|
|
|
64
64
|
...(options.beginClipboardPaste === undefined ? {} : { beginClipboardPaste: options.beginClipboardPaste }),
|
|
65
65
|
transformPastedContent: options.transformPastedContent ?? (content => content.kind === "text" ? content.text : ""),
|
|
66
66
|
atomicRanges: options.editorAtomicRanges ?? (() => []),
|
|
67
|
+
hiddenRanges: options.editorHiddenRanges ?? (() => []),
|
|
67
68
|
expandCopiedText: options.expandCopiedEditorText ?? (text => text),
|
|
68
69
|
paintSelection: options.paintEditorSelection ?? (line => line),
|
|
69
70
|
decorateRow: options.decorateEditorRow ?? (row => row),
|
|
@@ -149,6 +150,8 @@ export function createPiShellEditor(options) {
|
|
|
149
150
|
return {
|
|
150
151
|
render: width => editorUx?.render(width) ?? editor.render(width),
|
|
151
152
|
activateKeybindings: () => setKeybindings(keybindings),
|
|
153
|
+
keybindingConfig: () => keybindings.getEffectiveConfig(),
|
|
154
|
+
reloadKeybindings: () => { keybindings.reload(); setKeybindings(keybindings); },
|
|
152
155
|
matchesTerminalKey: (data, key) => matchesKey(data, key),
|
|
153
156
|
handleInput: data => {
|
|
154
157
|
if (editorUx === undefined)
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { type
|
|
1
|
+
import { type KeybindingsConfig } from "#pi-tui";
|
|
2
|
+
import { type PiShellComponentPort, type PiShellExtensionRendererResolver } from "./shell-shared-facade.js";
|
|
2
3
|
export interface PiShellSessionInfoPresentation {
|
|
3
4
|
readonly sessionName?: string;
|
|
4
5
|
readonly stats: {
|
|
@@ -30,7 +31,13 @@ export interface PiShellSessionInfoPresentation {
|
|
|
30
31
|
}[];
|
|
31
32
|
}
|
|
32
33
|
export declare function renderPiShellStatusText(message: string, width: number, outputPad?: 0 | 1): readonly string[];
|
|
34
|
+
export interface PiShellCommandMessagePresentation {
|
|
35
|
+
readonly kind: "error" | "warning" | "accent" | "new" | "name" | "debug";
|
|
36
|
+
readonly message: string;
|
|
37
|
+
readonly detail?: string;
|
|
38
|
+
}
|
|
39
|
+
export declare function renderPiShellCommandMessage(presentation: PiShellCommandMessagePresentation, width: number, outputPad?: 0 | 1): readonly string[];
|
|
33
40
|
export declare function createPiShellSessionInfo(presentation: PiShellSessionInfoPresentation): PiShellComponentPort;
|
|
34
41
|
export declare function createPiShellCollapsedChangelog(): PiShellComponentPort;
|
|
35
42
|
export declare function createPiShellChangelog(markdown: string): PiShellComponentPort;
|
|
36
|
-
export declare function createPiShellHotkeys(): PiShellComponentPort;
|
|
43
|
+
export declare function createPiShellHotkeys(bindings?: KeybindingsConfig, getShortcuts?: NonNullable<PiShellExtensionRendererResolver["getShortcuts"]>, profile?: "pi" | "a1"): PiShellComponentPort;
|
|
@@ -7,6 +7,22 @@ export function renderPiShellStatusText(message, width, outputPad = PINNED_PI_LA
|
|
|
7
7
|
ensureTheme();
|
|
8
8
|
return new Text(piTheme().fg("dim", message), outputPad, 0).render(width);
|
|
9
9
|
}
|
|
10
|
+
export function renderPiShellCommandMessage(presentation, width, outputPad = PINNED_PI_LAYOUT.outputPad) {
|
|
11
|
+
ensureTheme();
|
|
12
|
+
const { kind, message, detail } = presentation;
|
|
13
|
+
const prefix = kind === "error" ? "Error: " : kind === "warning" ? "Warning: " : "";
|
|
14
|
+
const color = kind === "name" ? "dim" : kind === "error" || kind === "warning" ? kind : "accent";
|
|
15
|
+
const content = piTheme().fg(color, `${prefix}${message}`)
|
|
16
|
+
+ (kind === "debug" && detail ? `\n${piTheme().fg("muted", detail)}` : "");
|
|
17
|
+
// Compatibility: pinned errors honor outputPad; warnings and command notices retain
|
|
18
|
+
// one-cell padding. New-session/debug notices also own one vertical padding row.
|
|
19
|
+
const text = new Text(content, kind === "error" ? outputPad : 1, kind === "new" || kind === "debug" ? 1 : 0);
|
|
20
|
+
return [
|
|
21
|
+
...(kind === "name" && detail ? renderPiShellCommandMessage({ kind: "warning", message: detail }, width) : []),
|
|
22
|
+
...new Spacer(1).render(width),
|
|
23
|
+
...text.render(width),
|
|
24
|
+
];
|
|
25
|
+
}
|
|
10
26
|
export function createPiShellSessionInfo(presentation) {
|
|
11
27
|
ensureTheme();
|
|
12
28
|
const { stats, sessionName, cacheWaste, usageBreakdown } = presentation;
|
|
@@ -61,12 +77,27 @@ export function createPiShellChangelog(markdown) {
|
|
|
61
77
|
container.addChild(new DynamicBorder());
|
|
62
78
|
return componentPort(container);
|
|
63
79
|
}
|
|
64
|
-
|
|
80
|
+
function shortcutDisplay(key) {
|
|
81
|
+
// Platform: pinned Pi labels Alt as Option on macOS, including extension shortcuts.
|
|
82
|
+
return key.split("/").map(binding => binding.split("+").map(part => {
|
|
83
|
+
const label = process.platform === "darwin" && part.toLowerCase() === "alt" ? "option" : part;
|
|
84
|
+
return label.charAt(0).toUpperCase() + label.slice(1);
|
|
85
|
+
}).join("+")).join("/");
|
|
86
|
+
}
|
|
87
|
+
export function createPiShellHotkeys(bindings, getShortcuts = () => [], profile = "pi") {
|
|
65
88
|
ensureTheme();
|
|
66
|
-
const keys = new KeybindingsManager();
|
|
67
|
-
|
|
89
|
+
const keys = new KeybindingsManager(bindings);
|
|
90
|
+
// Compatibility: the owned viewport consumes these physical chords before editor actions.
|
|
91
|
+
const display = (action) => keys.getKeys(action)
|
|
92
|
+
.filter(key => profile !== "a1" || (key !== "ctrl+home" && key !== "ctrl+end"))
|
|
93
|
+
.map(shortcutDisplay).join("/");
|
|
68
94
|
const row = (actions, description) => `| ${actions.map(action => `\`${display(action)}\``).join(" / ")} | ${description} |`;
|
|
69
|
-
|
|
95
|
+
let markdown = ["**Navigation**", "| Key | Action |", "|-----|--------|", row(["tui.editor.cursorUp", "tui.editor.cursorDown", "tui.editor.cursorLeft", "tui.editor.cursorRight"], "Move cursor / browse history"), row(["tui.editor.cursorWordLeft", "tui.editor.cursorWordRight"], "Move by word"), row(["tui.editor.cursorLineStart"], profile === "a1" ? "Start of prompt line" : "Start of line"), row(["tui.editor.cursorLineEnd"], profile === "a1" ? "End of prompt line" : "End of line"), ...(profile === "a1" ? ["| `Ctrl+Home` | Start of content |", "| `Ctrl+End` | End of content / follow output |"] : []), row(["tui.editor.jumpForward"], "Jump forward to character"), row(["tui.editor.jumpBackward"], "Jump backward to character"), row(["tui.editor.pageUp", "tui.editor.pageDown"], "Scroll by page"), "", "**Editing**", "| Key | Action |", "|-----|--------|", row(["tui.input.submit"], "Send message"), row(["tui.input.newLine"], `New line${process.platform === "win32" ? " (Ctrl+Enter on Windows Terminal)" : ""}`), row(["tui.editor.deleteWordBackward"], "Delete word backwards"), row(["tui.editor.deleteWordForward"], "Delete word forwards"), row(["tui.editor.deleteToLineStart"], "Delete to start of line"), row(["tui.editor.deleteToLineEnd"], "Delete to end of line"), row(["tui.editor.yank"], "Paste the most-recently-deleted text"), row(["tui.editor.yankPop"], "Cycle through the deleted text after pasting"), row(["tui.editor.undo"], "Undo"), "", "**Other**", "| Key | Action |", "|-----|--------|", row(["tui.input.tab"], "Path completion / accept autocomplete"), row(["app.interrupt"], "Cancel autocomplete / abort streaming"), row(["app.clear"], "Clear editor (first) / exit (second)"), row(["app.exit"], "Exit (when editor is empty)"), row(["app.suspend"], "Suspend to background"), row(["app.thinking.cycle"], "Cycle thinking level"), row(["app.model.cycleForward", "app.model.cycleBackward"], "Cycle models"), row(["app.model.select"], "Open model selector"), row(["app.tools.expand"], "Toggle tool output expansion"), row(["app.thinking.toggle"], "Toggle thinking block visibility"), row(["app.editor.external"], "Edit message in external editor"), row(["app.message.copy"], "Copy last assistant message"), row(["app.message.followUp"], "Queue follow-up message"), row(["app.message.dequeue"], "Restore queued messages"), row(["app.clipboard.pasteImage"], "Paste image or text from clipboard"), "| `/` | Slash commands |", "| `!` | Run bash command |", "| `!!` | Run bash command (excluded from context) |"].join("\n");
|
|
96
|
+
const shortcuts = getShortcuts(bindings ?? keys.getEffectiveConfig());
|
|
97
|
+
if (shortcuts.length > 0) {
|
|
98
|
+
markdown += "\n\n**Extensions**\n| Key | Action |\n|-----|--------|\n";
|
|
99
|
+
markdown += shortcuts.map(shortcut => `| \`${shortcutDisplay(shortcut.key)}\` | ${shortcut.description} |`).join("\n");
|
|
100
|
+
}
|
|
70
101
|
const container = new Container();
|
|
71
102
|
container.addChild(new Spacer(1));
|
|
72
103
|
container.addChild(new DynamicBorder());
|
|
@@ -67,6 +67,7 @@ export declare function createPiShellTreeSelector(options: {
|
|
|
67
67
|
readonly onSelect: (id: string) => void;
|
|
68
68
|
readonly onCancel: () => void;
|
|
69
69
|
readonly onLabelChange: (entryId: string, label: string | undefined) => void;
|
|
70
|
+
readonly onCopy?: (text: string | undefined) => void;
|
|
70
71
|
readonly initialSelectedId?: string;
|
|
71
72
|
readonly initialFilterMode?: "default" | "no-tools" | "user-only" | "labeled-only" | "all";
|
|
72
73
|
}): PiShellComponentPort;
|
|
@@ -87,7 +88,7 @@ export interface PiShellLoginDialogPort extends PiShellComponentPort {
|
|
|
87
88
|
showWaiting(message: string): void;
|
|
88
89
|
showProgress(message: string): void;
|
|
89
90
|
}
|
|
90
|
-
export declare function createPiShellLoginDialog(runtime: Pick<PiShellEditorOptions, "getColumns" | "getRows" | "requestRender">, providerId: string, onComplete: (success: boolean, message?: string) => void, providerName?: string): PiShellLoginDialogPort;
|
|
91
|
+
export declare function createPiShellLoginDialog(runtime: Pick<PiShellEditorOptions, "getColumns" | "getRows" | "requestRender">, providerId: string, onComplete: (success: boolean, message?: string) => void, providerName?: string, title?: string): PiShellLoginDialogPort;
|
|
91
92
|
export declare function createPiShellArmin(runtime: Pick<PiShellEditorOptions, "getColumns" | "getRows" | "requestRender">): PiShellComponentPort;
|
|
92
93
|
export declare function createPiShellDaxnuts(runtime: Pick<PiShellEditorOptions, "getColumns" | "getRows" | "requestRender">): PiShellComponentPort;
|
|
93
94
|
export declare function createPiShellEarendilAnnouncement(): PiShellComponentPort;
|
|
@@ -104,7 +105,7 @@ export interface PiShellAuthProviderOption extends PiShellSelectorOption {
|
|
|
104
105
|
readonly source?: string;
|
|
105
106
|
};
|
|
106
107
|
}
|
|
107
|
-
export declare function createPiShellAuthProviderSelector(mode: "login" | "logout", providers: readonly PiShellAuthProviderOption[], onSelect: (id: string) => void, onCancel: () => void): PiShellComponentPort;
|
|
108
|
+
export declare function createPiShellAuthProviderSelector(mode: "login" | "logout", providers: readonly PiShellAuthProviderOption[], onSelect: (id: string) => void, onCancel: () => void, initialSearchInput?: string): PiShellComponentPort;
|
|
108
109
|
export declare function createPiShellExtensionSelector(title: string, options: readonly string[], onSelect: (value: string) => void, onCancel: () => void): PiShellComponentPort;
|
|
109
110
|
export declare function createPiShellThinkingSelector(currentLevel: "off" | "minimal" | "low" | "medium" | "high" | "xhigh" | "max", availableLevels: readonly ("off" | "minimal" | "low" | "medium" | "high" | "xhigh" | "max")[], onSelect: (level: string) => void, onCancel: () => void): PiShellComponentPort;
|
|
110
111
|
export declare function createPiShellThemeSelector(currentTheme: string, onSelect: (theme: string) => void, onCancel: () => void, onPreview: (theme: string) => void): PiShellComponentPort;
|
|
@@ -111,7 +111,10 @@ export function createPiShellSessionSelector(options) {
|
|
|
111
111
|
}
|
|
112
112
|
export function createPiShellTreeSelector(options) {
|
|
113
113
|
ensureTheme();
|
|
114
|
-
|
|
114
|
+
const selector = new TreeSelectorComponent([...options.tree], options.currentLeafId, options.terminalHeight, options.onSelect, options.onCancel, options.onLabelChange, options.initialSelectedId, options.initialFilterMode);
|
|
115
|
+
if (options.onCopy)
|
|
116
|
+
selector.onCopy = options.onCopy;
|
|
117
|
+
return componentPort(selector);
|
|
115
118
|
}
|
|
116
119
|
export function createPiShellUserMessageSelector(messages, onSelect, onCancel, initialSelectedId) {
|
|
117
120
|
ensureTheme();
|
|
@@ -119,9 +122,9 @@ export function createPiShellUserMessageSelector(messages, onSelect, onCancel, i
|
|
|
119
122
|
const list = selector.getMessageList();
|
|
120
123
|
return componentPort(selector, data => list.handleInput(data));
|
|
121
124
|
}
|
|
122
|
-
export function createPiShellLoginDialog(runtime, providerId, onComplete, providerName) {
|
|
125
|
+
export function createPiShellLoginDialog(runtime, providerId, onComplete, providerName, title) {
|
|
123
126
|
ensureTheme();
|
|
124
|
-
const dialog = new LoginDialogComponent(createTuiFacade(runtime), providerId, onComplete, providerName);
|
|
127
|
+
const dialog = new LoginDialogComponent(createTuiFacade(runtime), providerId, onComplete, providerName, title);
|
|
125
128
|
return {
|
|
126
129
|
...componentPort(dialog),
|
|
127
130
|
showAuth: (url, instructions) => dialog.showAuth(url, instructions),
|
|
@@ -162,7 +165,7 @@ export function createPiShellReloadBox() {
|
|
|
162
165
|
container.addChild(new DynamicBorder(borderColor));
|
|
163
166
|
return componentPort(container);
|
|
164
167
|
}
|
|
165
|
-
export function createPiShellAuthProviderSelector(mode, providers, onSelect, onCancel) {
|
|
168
|
+
export function createPiShellAuthProviderSelector(mode, providers, onSelect, onCancel, initialSearchInput) {
|
|
166
169
|
ensureTheme();
|
|
167
170
|
const selector = new OAuthSelectorComponent(mode, providers.map(provider => ({
|
|
168
171
|
id: provider.providerId,
|
|
@@ -173,7 +176,7 @@ export function createPiShellAuthProviderSelector(mode, providers, onSelect, onC
|
|
|
173
176
|
const selected = providers.find(provider => provider.providerId === providerId && provider.authType === authType);
|
|
174
177
|
if (selected)
|
|
175
178
|
onSelect(selected.id);
|
|
176
|
-
}, onCancel);
|
|
179
|
+
}, onCancel, initialSearchInput);
|
|
177
180
|
return componentPort(selector);
|
|
178
181
|
}
|
|
179
182
|
export function createPiShellExtensionSelector(title, options, onSelect, onCancel) {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { visibleWidth, type AutocompleteProvider, type Component, type TUI } from "#pi-tui";
|
|
1
|
+
import { visibleWidth, type AutocompleteProvider, type Component, type KeybindingsConfig, type TUI } from "#pi-tui";
|
|
2
2
|
import type { OwnedUiImageAttachment, OwnedUiSessionViewModel, OwnedUiThinkingLevel, OwnedUiTranscriptBlock } from "../../../contracts/owned-ui/index.js";
|
|
3
3
|
import type { HistoryEditorConstructor } from "./history-editor-loader.js";
|
|
4
4
|
import type { EditorRecallPort } from "./editor-interaction.js";
|
|
@@ -31,8 +31,10 @@ export interface PiShellEditorPort extends PiShellComponentPort {
|
|
|
31
31
|
readonly historyReplacement?: typeof import("../../../contracts/owned-ui/index.js").PROMPT_HISTORY_EDITOR_REPLACEMENT;
|
|
32
32
|
/** Restores this editor's profile after another Pi component changed the global manager. */
|
|
33
33
|
activateKeybindings(): void;
|
|
34
|
+
keybindingConfig(): KeybindingsConfig;
|
|
35
|
+
reloadKeybindings(): void;
|
|
34
36
|
/** Uses Pi's terminal decoder rather than assuming one terminal escape spelling. */
|
|
35
|
-
matchesTerminalKey(data: string, key: "home" | "end" | "ctrl+v"): boolean;
|
|
37
|
+
matchesTerminalKey(data: string, key: "home" | "end" | "ctrl+home" | "ctrl+end" | "ctrl+v"): boolean;
|
|
36
38
|
getText(): string;
|
|
37
39
|
setText(text: string): void;
|
|
38
40
|
insertText(text: string): void;
|
|
@@ -94,6 +96,10 @@ export interface PiShellImageAssetResolver {
|
|
|
94
96
|
export interface PiShellExtensionRendererResolver {
|
|
95
97
|
getMessageRenderer(customType: string): unknown;
|
|
96
98
|
getToolDefinition(toolName: string): unknown;
|
|
99
|
+
getShortcuts?(bindings: KeybindingsConfig): readonly {
|
|
100
|
+
readonly key: string;
|
|
101
|
+
readonly description: string;
|
|
102
|
+
}[];
|
|
97
103
|
}
|
|
98
104
|
export interface PiShellTranscriptComponentPort extends PiShellComponentPort {
|
|
99
105
|
readonly id: string;
|
|
@@ -147,6 +153,7 @@ export interface PiShellEditorOptions {
|
|
|
147
153
|
readonly marker: string;
|
|
148
154
|
readonly result: Promise<string>;
|
|
149
155
|
};
|
|
156
|
+
readonly editorHiddenRanges?: (line: string) => readonly PiShellEditorTextRange[];
|
|
150
157
|
readonly transformPastedContent?: (content: PiShellClipboardContent) => string;
|
|
151
158
|
readonly editorAtomicRanges?: (line: string) => readonly PiShellEditorTextRange[];
|
|
152
159
|
readonly expandCopiedEditorText?: (text: string) => string;
|