@timurproko/a1 0.1.8-dev.226 → 0.1.8-dev.259

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 (94) hide show
  1. package/bin/guardian.js +4 -2
  2. package/bin/supervisor.js +1 -1
  3. package/bin/ui.js +44 -36
  4. package/bin/warmup.js +18 -9
  5. package/dist/cli/dispatch.d.ts +9 -1
  6. package/dist/cli/dispatch.js +73 -14
  7. package/dist/cli/package-messages.d.ts +26 -0
  8. package/dist/cli/package-messages.js +71 -0
  9. package/dist/cli/packages.d.ts +2 -1
  10. package/dist/cli/packages.js +12 -19
  11. package/dist/composition/owned-ui.d.ts +5 -2
  12. package/dist/composition/owned-ui.js +3 -1
  13. package/dist/contracts/agent-engine/package-ports.d.ts +6 -0
  14. package/dist/features/launch/intent.d.ts +5 -3
  15. package/dist/features/launch/intent.js +7 -2
  16. package/dist/features/owned-ui/index.d.ts +1 -0
  17. package/dist/features/owned-ui/index.js +1 -0
  18. package/dist/features/owned-ui/session-fork-prompt.d.ts +8 -0
  19. package/dist/features/owned-ui/session-fork-prompt.js +34 -0
  20. package/dist/foundation/launch-guardian/main.d.ts +2 -0
  21. package/dist/foundation/launch-guardian/main.js +8 -3
  22. package/dist/foundation/lifecycle/index.d.ts +2 -0
  23. package/dist/foundation/lifecycle/index.js +2 -0
  24. package/dist/foundation/lifecycle/paths.d.ts +2 -0
  25. package/dist/foundation/lifecycle/paths.js +9 -2
  26. package/dist/foundation/lifecycle/session-selection.d.ts +9 -0
  27. package/dist/foundation/lifecycle/session-selection.js +39 -0
  28. package/dist/foundation/lifecycle/supervisor-startup.d.ts +30 -0
  29. package/dist/foundation/lifecycle/supervisor-startup.js +106 -0
  30. package/dist/foundation/process-containment/darwin-process-inspector.d.ts +5 -0
  31. package/dist/foundation/process-containment/darwin-process-inspector.js +7 -0
  32. package/dist/foundation/process-containment/index.d.ts +1 -0
  33. package/dist/foundation/process-containment/index.js +1 -0
  34. package/dist/foundation/process-containment/windows-process-inspector.d.ts +2 -1
  35. package/dist/foundation/process-containment/windows-process-inspector.js +4 -2
  36. package/dist/foundation/release/bootstrap.d.ts +10 -3
  37. package/dist/foundation/release/bootstrap.js +58 -25
  38. package/dist/foundation/release/dependency-layer.js +11 -10
  39. package/dist/foundation/release/immutable-platform.d.ts +6 -0
  40. package/dist/foundation/release/immutable-platform.js +13 -0
  41. package/dist/foundation/release/index.d.ts +1 -0
  42. package/dist/foundation/release/index.js +1 -0
  43. package/dist/foundation/release/release-store.d.ts +1 -1
  44. package/dist/foundation/release/release-store.js +4 -2
  45. package/dist/foundation/release/restart-certification.d.ts +67 -0
  46. package/dist/foundation/release/restart-certification.js +211 -0
  47. package/dist/foundation/release/update.js +4 -4
  48. package/dist/foundation/release/warmup.js +1 -1
  49. package/dist/foundation/startup/startup-runtime.d.ts +2 -2
  50. package/dist/foundation/startup/startup-runtime.js +1 -1
  51. package/dist/foundation/supervision/main.d.ts +1 -1
  52. package/dist/foundation/supervision/main.js +60 -30
  53. package/dist/foundation/supervision/server.js +14 -1
  54. package/dist/integrations/pi/components/shell-footer-status.js +28 -12
  55. package/dist/integrations/pi/components/shell-selectors-dialogs.d.ts +4 -1
  56. package/dist/integrations/pi/components/shell-selectors-dialogs.js +2 -1
  57. package/dist/integrations/pi/components/shell-shared-facade.d.ts +5 -0
  58. package/dist/integrations/pi/engine/adapter.d.ts +6 -0
  59. package/dist/integrations/pi/engine/adapter.js +354 -53
  60. package/dist/integrations/pi/engine/index.d.ts +1 -0
  61. package/dist/integrations/pi/engine/index.js +1 -0
  62. package/dist/integrations/pi/engine/package-integration.js +31 -25
  63. package/dist/integrations/pi/engine/runtime-integration.d.ts +3 -0
  64. package/dist/integrations/pi/engine/runtime-integration.js +11 -4
  65. package/dist/integrations/pi/engine/session-selection.d.ts +28 -0
  66. package/dist/integrations/pi/engine/session-selection.js +90 -0
  67. package/dist/integrations/pi/engine/workflows.d.ts +18 -1
  68. package/dist/integrations/pi/session-ui/session-shell-root.d.ts +5 -1
  69. package/dist/integrations/pi/session-ui/session-shell-root.js +76 -24
  70. package/dist/integrations/pi/session-ui/session-shell.d.ts +1 -1
  71. package/dist/integrations/pi/session-ui/session-shell.js +46 -14
  72. package/dist/integrations/pi/session-ui/session-viewport-controller.d.ts +2 -1
  73. package/dist/integrations/pi/session-ui/session-viewport-controller.js +47 -18
  74. package/dist/integrations/pi/tui-runtime/damage-aware-terminal.d.ts +12 -1
  75. package/dist/integrations/pi/tui-runtime/damage-aware-terminal.js +166 -26
  76. package/dist/native/darwin-arm64/manifest.json +4 -4
  77. package/dist/native/darwin-arm64/process-guardian +0 -0
  78. package/dist/native/linux-x64/manifest.json +1 -1
  79. package/dist/native/linux-x64/process-guardian +0 -0
  80. package/dist/native/win32-x64/manifest.json +2 -2
  81. package/dist/native/win32-x64/process-guardian.exe +0 -0
  82. package/dist/ui/components/index.d.ts +1 -0
  83. package/dist/ui/components/index.js +1 -0
  84. package/dist/ui/components/text-selection.js +8 -35
  85. package/dist/ui/components/transcript-viewport.d.ts +8 -2
  86. package/dist/ui/components/transcript-viewport.js +8 -6
  87. package/dist/ui/components/visible-hyperlinks.d.ts +19 -0
  88. package/dist/ui/components/visible-hyperlinks.js +64 -0
  89. package/docs/architecture/process-guardian-provenance.md +3 -1
  90. package/docs/architecture/ui-reference-provenance.md +2 -0
  91. package/docs/ci-release-runbook.md +1 -1
  92. package/docs/features/launch-profiles.md +17 -1
  93. package/docs/manual-ghost-link-baseline.md +89 -0
  94. package/package.json +1 -1
@@ -1,6 +1,7 @@
1
1
  export * from "./adapter.js";
2
2
  export * from "./conformance.js";
3
3
  export * from "./runtime-integration.js";
4
+ export * from "./session-selection.js";
4
5
  export * from "./session-integration.js";
5
6
  export * from "./model-auth-integration.js";
6
7
  export * from "./settings-integration.js";
@@ -1,6 +1,7 @@
1
1
  export * from "./adapter.js";
2
2
  export * from "./conformance.js";
3
3
  export * from "./runtime-integration.js";
4
+ export * from "./session-selection.js";
4
5
  export * from "./session-integration.js";
5
6
  export * from "./model-auth-integration.js";
6
7
  export * from "./settings-integration.js";
@@ -15,21 +15,33 @@ const MODEL_REFRESH_TIMEOUT_MS = 15_000;
15
15
  */
16
16
  export function createPiPackagesPort(input) {
17
17
  const { profileRoot, cwd } = input;
18
- const settingsManager = SettingsManager.create(cwd, profileRoot, { projectTrusted: false });
19
- const packageManager = new DefaultPackageManager({ cwd, agentDir: profileRoot, settingsManager });
20
- packageManager.setProgressCallback(event => {
21
- if (event.type !== "start" || !event.message)
22
- return;
23
- input.onProgress?.({ operation: operationFor(event.action), message: event.message });
24
- });
25
- const configured = () => packageManager.listConfiguredPackages()
18
+ // Compatibility: model-only refresh bypasses package settings and their diagnostics, as Pi does.
19
+ let packageManager;
20
+ const manager = () => {
21
+ if (packageManager)
22
+ return packageManager;
23
+ const settingsManager = SettingsManager.create(cwd, profileRoot, { projectTrusted: false });
24
+ for (const { scope, error } of settingsManager.drainErrors()) {
25
+ input.onDiagnostic?.({
26
+ message: `Warning (package command, ${scope} settings): ${error.message}`,
27
+ ...(error.stack ? { detail: error.stack } : {}),
28
+ });
29
+ }
30
+ packageManager = new DefaultPackageManager({ cwd, agentDir: profileRoot, settingsManager });
31
+ packageManager.setProgressCallback(event => {
32
+ if (event.type !== "start")
33
+ return;
34
+ input.onProgress?.({ operation: operationFor(event.action), message: String(event.message) });
35
+ });
36
+ return packageManager;
37
+ };
38
+ const configured = () => manager().listConfiguredPackages()
26
39
  .filter(entry => entry.scope === "user")
27
40
  .map(entry => Object.freeze({
28
41
  source: entry.source,
29
42
  installedPath: entry.installedPath ?? null,
30
43
  filtered: entry.filtered,
31
44
  }));
32
- const isConfigured = (source) => configured().some(entry => entry.source === source || entry.source.startsWith(`${source}@`));
33
45
  return Object.freeze({
34
46
  capabilities: Object.freeze({ install: true, remove: true, update: true, refreshModels: true }),
35
47
  profileRoot,
@@ -38,13 +50,13 @@ export function createPiPackagesPort(input) {
38
50
  },
39
51
  async install(source) {
40
52
  return await attempt("install", source, async () => {
41
- await packageManager.installAndPersist(source);
53
+ await manager().installAndPersist(source);
42
54
  return agentPackageOutcome("install", "completed", null, source, configured());
43
55
  });
44
56
  },
45
57
  async remove(source) {
46
58
  return await attempt("remove", source, async () => {
47
- const removed = await packageManager.removeAndPersist(source);
59
+ const removed = await manager().removeAndPersist(source);
48
60
  if (!removed)
49
61
  return agentPackageOutcome("remove", "not-found", null, source);
50
62
  return agentPackageOutcome("remove", "completed", null, source, configured());
@@ -52,11 +64,8 @@ export function createPiPackagesPort(input) {
52
64
  },
53
65
  async update(source) {
54
66
  return await attempt("update", source ?? null, async () => {
55
- // Rationale: asking first keeps "that package is not installed here" a structural
56
- // answer rather than a string a caller would have to recognize.
57
- if (source !== undefined && !isConfigured(source))
58
- return agentPackageOutcome("update", "not-found", null, source);
59
- await packageManager.update(source);
67
+ // Compatibility: Pi owns source identity and its contextual no-match suggestions.
68
+ await manager().update(source);
60
69
  return agentPackageOutcome("update", "completed", null, source ?? null, configured());
61
70
  });
62
71
  },
@@ -73,7 +82,9 @@ async function attempt(operation, source, run) {
73
82
  return await run();
74
83
  }
75
84
  catch (error) {
76
- return agentPackageOutcome(operation, "failed", describe(error), source);
85
+ const detail = error instanceof Error ? error.message : operation === "refresh-models"
86
+ ? "Unknown model catalog refresh error" : "Unknown package command error";
87
+ return agentPackageOutcome(operation, "failed", detail, source);
77
88
  }
78
89
  }
79
90
  async function refreshModelCatalogs(profileRoot) {
@@ -88,9 +99,10 @@ async function refreshModelCatalogs(profileRoot) {
88
99
  });
89
100
  const result = await modelRuntime.refresh({ allowNetwork: true, force: true, signal: controller.signal });
90
101
  if (result.aborted)
91
- throw new Error(`model catalog refresh timed out after ${MODEL_REFRESH_TIMEOUT_MS}ms`);
102
+ throw new Error("Model catalog refresh timed out.");
92
103
  if (result.errors.size > 0) {
93
- throw new Error(Array.from(result.errors, ([provider, error]) => `${provider}: ${error.message}`).join("; "));
104
+ const details = Array.from(result.errors, ([provider, error]) => `${provider}: ${error.message}`).join("; ");
105
+ throw new Error(`Could not refresh model catalogs: ${details}`);
94
106
  }
95
107
  }
96
108
  finally {
@@ -104,9 +116,3 @@ function operationFor(action) {
104
116
  return "remove";
105
117
  return "update";
106
118
  }
107
- function describe(error) {
108
- const message = (error instanceof Error ? error.message : String(error)).replace(/\s+/g, " ").trim();
109
- if (message.length === 0)
110
- return "unknown package failure";
111
- return message.length > 600 ? `${message.slice(0, 597)}...` : message;
112
- }
@@ -1,10 +1,13 @@
1
1
  import { createAgentSessionServices, SettingsManager, type AgentSession, type AgentSessionRuntime, type AgentSessionServices, type ScopedModel } from "@earendil-works/pi-coding-agent";
2
2
  import { resolvePiProjectTrustPreflight, type PiProjectTrustPreflightPrompt } from "./project-trust-preflight.js";
3
+ import { type PiSessionForkPrompt, type PiSessionSelection } from "./session-selection.js";
3
4
  export interface PiRuntimeIntegrationOptions {
4
5
  readonly cwd: string;
5
6
  readonly agentDir: string;
6
7
  readonly sessionDir?: string;
7
8
  readonly sessionPath?: string;
9
+ readonly sessionSelection?: PiSessionSelection;
10
+ readonly sessionForkPrompt?: PiSessionForkPrompt;
8
11
  readonly projectTrustPrompt?: PiProjectTrustPreflightPrompt;
9
12
  readonly preflightDependencies?: PiRuntimePreflightDependencies;
10
13
  }
@@ -1,5 +1,6 @@
1
1
  import { createAgentSessionFromServices, createAgentSessionRuntime, createAgentSessionServices, resolveModelScopeWithDiagnostics, SessionManager, SettingsManager, } from "@earendil-works/pi-coding-agent";
2
2
  import { resolvePiProjectTrustPreflight, } from "./project-trust-preflight.js";
3
+ import { openSelectedPiSession, resolveSessionArgumentPath } from "./session-selection.js";
3
4
  import { markStartupPhase } from "../../../foundation/startup/index.js";
4
5
  /**
5
6
  * Mirrors pinned Pi's CLI startup: resolve the `models` patterns from settings
@@ -49,10 +50,16 @@ export async function createPiRuntimeServicesAfterTrust(options) {
49
50
  return { services, trust };
50
51
  }
51
52
  export async function createPiRuntimeIntegration(options) {
52
- const sessionDir = options.sessionDir ?? process.env.PI_CODING_AGENT_SESSION_DIR;
53
- const sessionManager = options.sessionPath === undefined
53
+ const directory = options.sessionDir ?? process.env.PI_CODING_AGENT_SESSION_DIR;
54
+ const sessionDir = directory === undefined ? undefined : resolveSessionArgumentPath(directory, options.cwd);
55
+ const selection = options.sessionSelection ?? (options.sessionPath === undefined ? undefined : { target: resolveSessionArgumentPath(options.sessionPath, options.cwd) });
56
+ const sessionManager = selection === undefined
54
57
  ? SessionManager.create(options.cwd, sessionDir)
55
- : SessionManager.open(options.sessionPath, sessionDir, options.cwd);
58
+ : await openSelectedPiSession({
59
+ cwd: options.cwd, selection,
60
+ ...(sessionDir === undefined ? {} : { sessionDir }),
61
+ ...(options.sessionForkPrompt === undefined ? {} : { forkPrompt: options.sessionForkPrompt }),
62
+ });
56
63
  const createRuntime = async ({ cwd, sessionManager: targetSessionManager, sessionStartEvent, }) => {
57
64
  const { services, trust } = await createPiRuntimeServicesAfterTrust({
58
65
  cwd,
@@ -81,7 +88,7 @@ export async function createPiRuntimeIntegration(options) {
81
88
  };
82
89
  };
83
90
  return createAgentSessionRuntime(createRuntime, {
84
- cwd: options.cwd,
91
+ cwd: sessionManager.getCwd(),
85
92
  agentDir: options.agentDir,
86
93
  sessionManager,
87
94
  });
@@ -0,0 +1,28 @@
1
+ import { SessionManager, type SessionInfo } from "@earendil-works/pi-coding-agent";
2
+ export interface PiSessionSelection {
3
+ readonly target: string;
4
+ readonly sessionDir?: string;
5
+ }
6
+ export type PiSessionForkPrompt = (request: {
7
+ readonly sourceCwd: string;
8
+ }) => Promise<boolean>;
9
+ /** Expected resume outcomes are concise CLI diagnostics, not engine crashes. */
10
+ export declare class PiSessionSelectionError extends Error {
11
+ readonly exitCode: number;
12
+ constructor(message: string, exitCode?: number);
13
+ }
14
+ interface SessionSelectionOptions {
15
+ readonly cwd: string;
16
+ readonly selection: PiSessionSelection;
17
+ readonly sessionDir?: string;
18
+ readonly forkPrompt?: PiSessionForkPrompt;
19
+ /** Public API seam for deterministic lookup/open race and listing-order evidence. */
20
+ readonly sessions?: Pick<typeof SessionManager, "list" | "open" | "forkFrom"> & {
21
+ readonly listAll: (sessionDir?: string) => Promise<SessionInfo[]>;
22
+ };
23
+ }
24
+ /** Pinned Pi 0.84.2 main.ts policy, using SDK APIs rather than importing the CLI. */
25
+ export declare function openSelectedPiSession(options: SessionSelectionOptions): Promise<SessionManager>;
26
+ /** Resolve before switching effective cwd, including quoted home-relative paths. */
27
+ export declare function resolveSessionArgumentPath(path: string, cwd: string): string;
28
+ export {};
@@ -0,0 +1,90 @@
1
+ import { accessSync, constants, existsSync, realpathSync, statSync } from "node:fs";
2
+ import { homedir } from "node:os";
3
+ import { resolve } from "node:path";
4
+ import { SessionManager } from "@earendil-works/pi-coding-agent";
5
+ /** Expected resume outcomes are concise CLI diagnostics, not engine crashes. */
6
+ export class PiSessionSelectionError extends Error {
7
+ exitCode;
8
+ constructor(message, exitCode = 1) {
9
+ super(message);
10
+ this.exitCode = exitCode;
11
+ this.name = "PiSessionSelectionError";
12
+ }
13
+ }
14
+ /** Pinned Pi 0.84.2 main.ts policy, using SDK APIs rather than importing the CLI. */
15
+ export async function openSelectedPiSession(options) {
16
+ try {
17
+ const sessions = options.sessions ?? SessionManager;
18
+ const cwd = resolve(options.cwd);
19
+ const directory = options.selection.sessionDir ?? options.sessionDir ?? process.env.PI_CODING_AGENT_SESSION_DIR;
20
+ const sessionDir = directory === undefined ? undefined : resolveSessionArgumentPath(directory, cwd);
21
+ const target = options.selection.target;
22
+ let path;
23
+ let global;
24
+ if (target.includes("/") || target.includes("\\") || target.endsWith(".jsonl")) {
25
+ path = resolveSessionArgumentPath(target, cwd);
26
+ }
27
+ else {
28
+ const local = matchSession(await sessions.list(cwd, sessionDir), target);
29
+ const globalMatch = local ? undefined : matchSession(await sessions.listAll(sessionDir), target);
30
+ // Compatibility: macOS may expose one temporary directory as /var to one process
31
+ // and /private/var to another. Physical path identity must not turn that alias into
32
+ // a cross-project fork prompt.
33
+ global = globalMatch && !sameProjectDirectory(globalMatch.cwd, cwd) ? globalMatch : undefined;
34
+ const matched = local ?? globalMatch;
35
+ if (!matched)
36
+ throw new PiSessionSelectionError(`No session found matching '${target}'`);
37
+ path = matched.path;
38
+ }
39
+ assertResumeFile(path);
40
+ if (global) {
41
+ if (!options.forkPrompt || !await options.forkPrompt({ sourceCwd: global.cwd })) {
42
+ throw new PiSessionSelectionError("Session resume cancelled.", 0);
43
+ }
44
+ assertResumeFile(path);
45
+ assertCwd(cwd);
46
+ return sessions.forkFrom(path, cwd, sessionDir);
47
+ }
48
+ // Invariant: no await between the final guard and SDK open. A disappeared target
49
+ // can otherwise produce a new in-memory header instead of an existing session.
50
+ const manager = sessions.open(path, sessionDir);
51
+ assertResumeFile(path);
52
+ assertCwd(manager.getCwd());
53
+ return manager;
54
+ }
55
+ catch (error) {
56
+ if (error instanceof PiSessionSelectionError)
57
+ throw error;
58
+ throw new PiSessionSelectionError(`Could not resume session: ${error instanceof Error ? error.message : String(error)}`);
59
+ }
60
+ }
61
+ function sameProjectDirectory(left, right) {
62
+ try {
63
+ const first = realpathSync.native(resolve(left));
64
+ const second = realpathSync.native(resolve(right));
65
+ return process.platform === "win32" ? first.toLowerCase() === second.toLowerCase() : first === second;
66
+ }
67
+ catch {
68
+ return resolve(left) === resolve(right);
69
+ }
70
+ }
71
+ function matchSession(sessions, target) {
72
+ return sessions.find(session => session.id === target) ?? sessions.find(session => session.id.startsWith(target));
73
+ }
74
+ /** Resolve before switching effective cwd, including quoted home-relative paths. */
75
+ export function resolveSessionArgumentPath(path, cwd) {
76
+ const expanded = path === "~" ? homedir() : /^~[/\\]/.test(path) ? resolve(homedir(), path.slice(2)) : path;
77
+ return resolve(cwd, expanded);
78
+ }
79
+ function assertResumeFile(path) {
80
+ if (!existsSync(path))
81
+ throw new PiSessionSelectionError(`Session file does not exist: ${path}`);
82
+ const metadata = statSync(path);
83
+ if (!metadata.isFile() || metadata.size === 0)
84
+ throw new PiSessionSelectionError(`Session file is empty or not a regular file: ${path}`);
85
+ accessSync(path, constants.R_OK);
86
+ }
87
+ function assertCwd(cwd) {
88
+ if (!existsSync(cwd) || !statSync(cwd).isDirectory())
89
+ throw new PiSessionSelectionError(`Session working directory does not exist: ${cwd}`);
90
+ }
@@ -62,6 +62,10 @@ export interface PiWorkflowRequest {
62
62
  readonly argument: string;
63
63
  readonly selection?: string;
64
64
  readonly confirmed?: boolean;
65
+ /** Recovery cwd selected after an import/resume source cwd is unavailable. */
66
+ readonly cwdOverride?: string;
67
+ /** Cancellation for an owned operation surface such as `/share`. */
68
+ readonly signal?: AbortSignal;
65
69
  readonly treeSummary?: {
66
70
  readonly summarize: boolean;
67
71
  readonly customInstructions?: string;
@@ -100,10 +104,19 @@ export interface PiSessionInfoPresentation {
100
104
  }[];
101
105
  }
102
106
  export type PiWorkflowPresentation = PiSessionInfoPresentation;
107
+ export type PiWorkflowMessageKind = "status" | "warning" | "error" | "accent" | "silent";
108
+ export interface PiWorkflowMessage {
109
+ readonly kind: Exclude<PiWorkflowMessageKind, "silent">;
110
+ readonly message: string;
111
+ }
103
112
  export interface PiWorkflowResult {
104
113
  readonly command: PiWorkflowRoute;
105
114
  readonly outcome: PiWorkflowOutcome;
106
115
  readonly message: string;
116
+ /** Explicit presentation semantics; never inferred from message prefixes. */
117
+ readonly messageKind?: PiWorkflowMessageKind;
118
+ /** Ordered messages for truthful partial-success outcomes. */
119
+ readonly messages?: readonly PiWorkflowMessage[];
107
120
  readonly detail?: string;
108
121
  readonly selectorTitle?: string;
109
122
  readonly options?: readonly PiWorkflowOption[];
@@ -153,11 +166,15 @@ export interface PiWorkflowInteractionHost {
153
166
  startLogin?(request: PiWorkflowLoginStart): void;
154
167
  prompt(request: PiWorkflowInteractionRequest): Promise<string | null>;
155
168
  notify(event: PiWorkflowLoginNotification): void;
169
+ /** Deliver a post-login status or warning after the authentication dialog closes. */
170
+ publish?(message: PiWorkflowMessage): void;
156
171
  finishLogin?(): void;
157
172
  }
158
173
  export interface PiWorkflowHost {
159
174
  copyText(text: string): Promise<void>;
160
- runCommand(command: string, arguments_: readonly string[]): Promise<{
175
+ runCommand(command: string, arguments_: readonly string[], options?: {
176
+ readonly signal?: AbortSignal;
177
+ }): Promise<{
161
178
  readonly stdout: string;
162
179
  readonly stderr: string;
163
180
  }>;
@@ -1,7 +1,7 @@
1
1
  import type { OwnedUiSessionViewModel, OwnedUiViewportSettings, OwnedUiViewportSettingsPort } from "../../../contracts/owned-ui/index.js";
2
2
  import type { UiRouteHost } from "../../../ui/apps/index.js";
3
3
  import { type TranscriptViewportFrame, type TranscriptViewportFrameDescriptor } from "../../../ui/components/index.js";
4
- import { type PiEngineAdapter, type PiWorkflowResult } from "../engine/index.js";
4
+ import { type PiEngineAdapter, type PiWorkflowMessage, type PiWorkflowResult } from "../engine/index.js";
5
5
  import { type PiShellClipboardContent, type PiShellComponentPort, type PiShellEditorPort, type PiShellExtensionRendererResolver, type PiShellHeaderOptions, type PiShellHeaderPort, type PiShellImageAssetResolver, type PiShellLoadedResourcesPort, type PiShellResourceEntry, type PiShellTranscriptComponentPort } from "../components/index.js";
6
6
  import { type PiTuiComponentPort, type PiTuiInputSurfaceKind, type PiTuiInputCoordinationScheduler, type PiTuiInputDiagnosticsEvent, type PiTuiLayoutNode, type PiTuiTerminalPort } from "../tui-runtime/index.js";
7
7
  import { type PreparedPrompt } from "./prompt-chips.js";
@@ -60,6 +60,7 @@ export declare class OwnedUiSessionShellRoot implements PiTuiComponentPort {
60
60
  readonly getRows: () => number;
61
61
  readonly requestRender: (force?: boolean) => void;
62
62
  readonly onViewportFrame?: (frame: TranscriptViewportFrame) => void;
63
+ readonly requestHyperlinkCleanup?: () => void;
63
64
  readonly enableDockInputReuse?: boolean;
64
65
  readonly onSubmit: (text: string) => void;
65
66
  readonly onInterrupt: () => void;
@@ -91,6 +92,8 @@ export declare class OwnedUiSessionShellRoot implements PiTuiComponentPort {
91
92
  applyTranscriptBlock(block: OwnedUiSessionViewModel["transcript"][number]): void;
92
93
  render(width: number): readonly string[];
93
94
  viewportFrameDescriptor(): TranscriptViewportFrameDescriptor | null;
95
+ /** Visible rows of the transient working-status tail, for rendering evidence. */
96
+ viewportTransientTailRowCount(): number;
94
97
  hasActiveSelection(): boolean;
95
98
  setViewportConfig(config: OwnedUiViewportSettings): void;
96
99
  resumeViewportFollowing(): void;
@@ -106,6 +109,7 @@ export declare class OwnedUiSessionShellRoot implements PiTuiComponentPort {
106
109
  transcriptComponent(id: string): PiShellTranscriptComponentPort | undefined;
107
110
  exitTranscript(width: number): string;
108
111
  appendWorkflowStatus(message: string): void;
112
+ appendWorkflowMessage(message: PiWorkflowMessage): void;
109
113
  appendWorkflowResult(result: PiWorkflowResult): void;
110
114
  appendDaxnuts(): void;
111
115
  toggleThinkingVisibility(): void;
@@ -44,6 +44,8 @@ export class OwnedUiSessionShellRoot {
44
44
  #inputSurfaceCoordination = "editor";
45
45
  #dockInputReuseEnabled;
46
46
  #dockInputCandidate = false;
47
+ #dockInputSnapshot;
48
+ #dockInputStatusSignature;
47
49
  #visibleViewportSnapshot;
48
50
  #fullViewportCompositions = 0;
49
51
  #dockOnlyViewportCompositions = 0;
@@ -111,6 +113,7 @@ export class OwnedUiSessionShellRoot {
111
113
  enabled: this.#customViewport,
112
114
  editor: this.editor,
113
115
  requestRender: force => this.#componentRuntime.requestRender(force),
116
+ requestHyperlinkCleanup: () => handlers.requestHyperlinkCleanup?.(),
114
117
  hasEditorLinks: () => this.#promptChips.hyperlinkRanges(this.editor.getText()).length > 0,
115
118
  });
116
119
  // Performance: stable painter identities let the neutral viewport retain row-level
@@ -229,9 +232,14 @@ export class OwnedUiSessionShellRoot {
229
232
  // intentional final gutter after their right-aligned timestamp.
230
233
  const documentWidth = width;
231
234
  const document = this.#renderDocumentLayout(documentWidth);
232
- // Invariant: prompt-adjacent transient surfaces always belong to the dock.
233
- // Changing from fitting to overflowing content reallocates transcript rows
234
- // only; it never moves queued or Working rows into selectable history.
235
+ const statusRows = this.#renderStatus(width);
236
+ const statusSignature = statusRows.length === 0 ? undefined : `${statusRows.length}\u0000${statusRows.join("\u0000")}`;
237
+ const snapshot = this.#visibleViewportSnapshot;
238
+ const dockInputCandidate = this.#dockInputCandidate;
239
+ const dockInputSnapshot = dockInputCandidate ? this.#dockInputSnapshot : undefined;
240
+ const dockInputStatusSignature = dockInputCandidate ? this.#dockInputStatusSignature : statusSignature;
241
+ // Invariant: queued rows stay docked while live Working rows form one scrollable tail.
242
+ const scrollRows = [...document.rows, ...statusRows];
235
243
  const dockRows = dock.rows;
236
244
  const selectableDocumentRowCount = document.rows.length;
237
245
  const dockStartRow = height - dockRows.length + 1;
@@ -242,29 +250,32 @@ export class OwnedUiSessionShellRoot {
242
250
  rowEnd: dockStartRow + editorOffset + dock.inputRows - 1,
243
251
  }
244
252
  : undefined);
245
- const snapshot = this.#visibleViewportSnapshot;
246
- const dockInputCandidate = this.#dockInputCandidate;
247
253
  this.#dockInputCandidate = false;
248
254
  let frame = dockInputCandidate
249
255
  && snapshot !== undefined
250
256
  && snapshot.width === width
251
257
  && snapshot.height === height
252
- && snapshot.documentRows === document.rows
253
- && snapshot.promptAnchors === document.promptAnchors
258
+ && snapshot.documentRows === (dockInputSnapshot?.documentRows ?? scrollRows)
259
+ && snapshot.statusSignature === dockInputStatusSignature
260
+ && snapshot.promptAnchors === (dockInputSnapshot?.promptAnchors ?? document.promptAnchors)
254
261
  && snapshot.dockLength === dockRows.length
255
- && snapshot.inputSurface === this.#inputSurface
256
- && snapshot.viewportRevision === this.#viewportController.presentationRevision
257
- && snapshot.selectionRevision === this.#viewportController.selectionRevision
262
+ && snapshot.inputSurface === (dockInputSnapshot?.inputSurface ?? this.#inputSurface)
263
+ && snapshot.viewportRevision === (dockInputSnapshot?.viewportRevision ?? this.#viewportController.presentationRevision)
264
+ && snapshot.selectionRevision === (dockInputSnapshot?.selectionRevision ?? this.#viewportController.selectionRevision)
258
265
  ? this.#viewportController.composeDockOnly(dockRows, width, height)
259
266
  : null;
267
+ // Performance: a still-active spinner can tick before its first delayed presentation.
268
+ // Its newest tail signature is recomputed below and therefore cannot hide that change.
269
+ if (frame !== null && dockInputStatusSignature !== statusSignature)
270
+ frame = null;
260
271
  if (frame === null) {
261
272
  frame = this.#viewportController.compose({
262
- documentRows: document.rows,
273
+ documentRows: scrollRows,
263
274
  ...(this.#viewportController.transcriptPointerSelecting
264
275
  ? { paintDocumentRow: heldNativeHyperlinkStyle }
265
276
  : {}),
266
- // Invariant: selection and copying stop at the real document tail; queued
267
- // and Working rows remain dock-owned status chrome at every fit boundary.
277
+ // Invariant: selection and copying stop at the real document tail; live Working
278
+ // rows remain transient presentation chrome at every fit boundary.
268
279
  selectableDocumentRowCount,
269
280
  dockRows,
270
281
  promptAnchors: document.promptAnchors,
@@ -282,7 +293,9 @@ export class OwnedUiSessionShellRoot {
282
293
  this.#visibleViewportSnapshot = {
283
294
  width,
284
295
  height,
285
- documentRows: document.rows,
296
+ documentRows: scrollRows,
297
+ statusSignature,
298
+ dockInputStatusSignature,
286
299
  promptAnchors: document.promptAnchors,
287
300
  dockLength: dockRows.length,
288
301
  inputSurface: this.#inputSurface,
@@ -295,6 +308,10 @@ export class OwnedUiSessionShellRoot {
295
308
  viewportFrameDescriptor() {
296
309
  return this.#viewportController.frame?.descriptor ?? null;
297
310
  }
311
+ /** Visible rows of the transient working-status tail, for rendering evidence. */
312
+ viewportTransientTailRowCount() {
313
+ return this.#viewportController.frame?.hits.transientTail.length ?? 0;
314
+ }
298
315
  hasActiveSelection() {
299
316
  return this.#viewportController.hasSelection || this.editor.hasSelection();
300
317
  }
@@ -322,7 +339,7 @@ export class OwnedUiSessionShellRoot {
322
339
  }
323
340
  #renderDockLayout(width) {
324
341
  const queued = this.#view.editor.queuedSubmissions.length === 0 ? [] : this.#queued.render(width);
325
- const statusRows = this.#renderStatus(width);
342
+ const statusRows = this.#customViewport ? this.#status.renderDock(width) : this.#renderStatus(width);
326
343
  const transientRows = [...queued, ...statusRows];
327
344
  const aboveWidgets = this.#renderWidgets("aboveEditor", width);
328
345
  const input = this.#inputSurface.render(width);
@@ -535,7 +552,25 @@ export class OwnedUiSessionShellRoot {
535
552
  this.#workflowStatusMessages.set(id, message);
536
553
  this.#lastWorkflowStatusId = id;
537
554
  }
555
+ appendWorkflowMessage(message) {
556
+ if (message.kind === "status") {
557
+ this.appendWorkflowStatus(message.message);
558
+ return;
559
+ }
560
+ this.#lastWorkflowStatusId = undefined;
561
+ const prefix = message.kind === "warning" ? "Warning" : message.kind === "error" ? "Error" : undefined;
562
+ const color = message.kind === "accent" ? "accent" : message.kind;
563
+ const text = prefix === undefined ? message.message : `${prefix}: ${message.message}`;
564
+ this.#appendAnchoredWorkflowComponent(() => ["", ` ${piTheme().fg(color, text)}`]);
565
+ }
538
566
  appendWorkflowResult(result) {
567
+ if (result.messages !== undefined) {
568
+ for (const message of result.messages)
569
+ this.appendWorkflowMessage(message);
570
+ return;
571
+ }
572
+ if (result.messageKind === "silent" || (result.outcome === "cancelled" && result.messageKind === undefined))
573
+ return;
539
574
  if (result.command === "reload" && result.outcome === "completed") {
540
575
  this.appendWorkflowStatus(result.message);
541
576
  return;
@@ -559,19 +594,16 @@ export class OwnedUiSessionShellRoot {
559
594
  return;
560
595
  }
561
596
  if (result.outcome === "failed") {
562
- this.#lastWorkflowStatusId = undefined;
563
- const warning = (result.command === "name" && result.message.startsWith("Usage:"))
564
- || (result.command === "reload" && result.message.startsWith("Wait for "));
565
- const prefix = warning ? "Warning" : "Error";
566
- const color = warning ? "warning" : "error";
567
- this.#appendAnchoredWorkflowComponent(() => ["", ` ${piTheme().fg(color, `${prefix}: ${result.message}`)}`]);
597
+ this.appendWorkflowMessage({
598
+ kind: result.messageKind === "warning" ? "warning" : "error",
599
+ message: result.message,
600
+ });
568
601
  return;
569
602
  }
570
603
  if (result.outcome === "completed" && (result.command === "quit" || result.command === "compact"))
571
604
  return;
572
605
  if (result.command === "new" && result.outcome === "completed") {
573
- this.#lastWorkflowStatusId = undefined;
574
- this.#appendAnchoredWorkflowComponent(() => ["", ` ${piTheme().fg("accent", result.message)}`]);
606
+ this.appendWorkflowMessage({ kind: "accent", message: result.message });
575
607
  return;
576
608
  }
577
609
  if (result.command === "name" && result.outcome === "completed") {
@@ -737,6 +769,8 @@ export class OwnedUiSessionShellRoot {
737
769
  handleInput(data) {
738
770
  this.#dockInputCandidate = this.#customViewport && this.#dockInputReuseEnabled
739
771
  && classifyPiTuiInput(data, this.#inputSurfaceCoordination) === "safe";
772
+ if (this.#dockInputCandidate)
773
+ this.#captureDockInputSnapshot();
740
774
  try {
741
775
  this.#inputSurface.handleInput?.(data);
742
776
  }
@@ -745,6 +779,22 @@ export class OwnedUiSessionShellRoot {
745
779
  throw error;
746
780
  }
747
781
  }
782
+ #captureDockInputSnapshot() {
783
+ const width = Math.max(1, this.#componentRuntime.getColumns());
784
+ const statusRows = this.#renderStatus(width);
785
+ this.#dockInputStatusSignature = statusRows.length === 0 ? undefined : `${statusRows.length}\u0000${statusRows.join("\u0000")}`;
786
+ const snapshot = this.#visibleViewportSnapshot;
787
+ this.#dockInputSnapshot = snapshot === undefined
788
+ ? undefined
789
+ : {
790
+ documentRows: snapshot.documentRows,
791
+ statusSignature: this.#dockInputStatusSignature,
792
+ promptAnchors: snapshot.promptAnchors,
793
+ inputSurface: this.#inputSurface,
794
+ viewportRevision: this.#viewportController.presentationRevision,
795
+ selectionRevision: this.#viewportController.selectionRevision,
796
+ };
797
+ }
748
798
  invalidate() {
749
799
  this.header.invalidate();
750
800
  this.resources.invalidate();
@@ -773,6 +823,8 @@ export class OwnedUiSessionShellRoot {
773
823
  this.#documentLayouts.clear();
774
824
  this.#visibleViewportSnapshot = undefined;
775
825
  this.#dockInputCandidate = false;
826
+ this.#dockInputSnapshot = undefined;
827
+ this.#dockInputStatusSignature = undefined;
776
828
  if (this.#inputSurface !== this.editor)
777
829
  this.#inputSurface.dispose?.();
778
830
  this.#extensionHeader?.dispose?.();
@@ -869,7 +921,7 @@ export class OwnedUiSessionShellRoot {
869
921
  return placement === "aboveEditor" ? ["", ...rows] : rows;
870
922
  }
871
923
  #renderStatus(width) {
872
- return this.#status.render(width);
924
+ return this.#customViewport ? this.#status.renderLive(width) : this.#status.render(width);
873
925
  }
874
926
  #renderFooter(width) {
875
927
  return (this.#extensionFooter ?? this.#footer).render(width);
@@ -31,7 +31,7 @@ export declare class OwnedUiSessionShell {
31
31
  queueFollowUp(): Promise<AdapterCommandResult>;
32
32
  restoreQueuedInput(): void;
33
33
  showSelector(title: string, options: readonly PiShellSelectorOption[], onSelect: (id: string) => void, onCancel?: () => void): PiTuiOverlayHandle;
34
- showModelSelector(): void;
34
+ showModelSelector(initialSearchInput?: string): void;
35
35
  showForkSelector(): void;
36
36
  showLogoutSelector(): Promise<void>;
37
37
  showLoginMethodSelector(providerReference: string): void;