@timurproko/a1 0.1.8-dev.457 → 0.1.8-dev.465
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/dist/composition/owned-ui.js +2 -1
- package/dist/contracts/owned-ui/model.d.ts +5 -1
- package/dist/contracts/owned-ui/validation.js +3 -0
- package/dist/features/owned-ui/settings-app.d.ts +1 -0
- package/dist/features/owned-ui/settings-app.js +129 -17
- package/dist/integrations/pi/components/shell-footer-status.js +32 -8
- package/dist/integrations/pi/components/shell-shared-facade.d.ts +2 -0
- package/dist/integrations/pi/components/upstream/components/owned-editor.js +2 -2
- package/dist/integrations/pi/engine/adapter.js +42 -4
- package/dist/integrations/pi/engine/compaction-progress.d.ts +28 -0
- package/dist/integrations/pi/engine/compaction-progress.js +96 -0
- package/dist/integrations/pi/engine/conformance.d.ts +1 -1
- package/dist/integrations/pi/engine/conformance.js +3 -3
- package/dist/integrations/pi/engine/session-integration.d.ts +25 -3
- package/dist/integrations/pi/engine/session-integration.js +93 -8
- package/dist/integrations/pi/session-ui/session-shell-root.js +1 -0
- package/dist/integrations/pi/session-ui/session-shell.js +14 -42
- 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/ui/components/list-view.js +5 -6
- package/dist/ui/components/surface.d.ts +7 -2
- package/dist/ui/components/surface.js +13 -3
- package/dist/ui/settings/declarations.d.ts +1 -1
- package/dist/ui/settings/declarations.js +13 -2
- package/dist/ui/settings/migrations.js +13 -0
- package/docs/architecture/ui-reference-provenance.md +2 -2
- package/docs/local-worktree-cleanup.md +49 -6
- package/docs/openspec-archive-automation.md +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/** Expected summary size for a first compaction, when no previous summary on the branch can serve as the estimate. */
|
|
2
|
+
export const DEFAULT_EXPECTED_COMPACTION_SUMMARY_CHARS = 4000;
|
|
3
|
+
/** The percent shown while a compaction is still running never reaches this value. */
|
|
4
|
+
const MAX_RUNNING_PERCENT = 99;
|
|
5
|
+
/**
|
|
6
|
+
* Observes the summarization stream of a compaction through the session agent's public stream
|
|
7
|
+
* function and reports an estimated integer percent. Pi never iterates that stream itself (it
|
|
8
|
+
* reads only its final result), so the observer iterates it in the background between
|
|
9
|
+
* `begin()` and `end()` and returns the same stream object to Pi. Outside that window the
|
|
10
|
+
* wrapper is a pass-through. Returns null when the agent exposes no callable stream function,
|
|
11
|
+
* in which case compaction proceeds without progress.
|
|
12
|
+
*/
|
|
13
|
+
export function observeCompactionProgress(session, onProgress) {
|
|
14
|
+
const agent = session.agent;
|
|
15
|
+
if (agent === undefined || typeof agent.streamFunction !== "function")
|
|
16
|
+
return null;
|
|
17
|
+
const original = agent.streamFunction;
|
|
18
|
+
let active = false;
|
|
19
|
+
let disposed = false;
|
|
20
|
+
let generation = 0;
|
|
21
|
+
let streamed = 0;
|
|
22
|
+
let expected = DEFAULT_EXPECTED_COMPACTION_SUMMARY_CHARS;
|
|
23
|
+
let reported = null;
|
|
24
|
+
const report = () => {
|
|
25
|
+
const percent = Math.min(MAX_RUNNING_PERCENT, Math.floor((100 * streamed) / expected));
|
|
26
|
+
if (percent === reported)
|
|
27
|
+
return;
|
|
28
|
+
reported = percent;
|
|
29
|
+
onProgress(percent);
|
|
30
|
+
};
|
|
31
|
+
const wrapped = async (model, context, options) => {
|
|
32
|
+
const stream = await original(model, context, options);
|
|
33
|
+
if (!active || disposed)
|
|
34
|
+
return stream;
|
|
35
|
+
const observed = generation;
|
|
36
|
+
void (async () => {
|
|
37
|
+
report();
|
|
38
|
+
// Invariant: observation never affects the request; a stream that cannot be iterated ends observation only.
|
|
39
|
+
try {
|
|
40
|
+
for await (const event of stream) {
|
|
41
|
+
if (observed !== generation || !active)
|
|
42
|
+
return;
|
|
43
|
+
if (event.type === "text_delta") {
|
|
44
|
+
streamed += event.delta.length;
|
|
45
|
+
report();
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
})();
|
|
53
|
+
return stream;
|
|
54
|
+
};
|
|
55
|
+
agent.streamFunction = wrapped;
|
|
56
|
+
return {
|
|
57
|
+
begin() {
|
|
58
|
+
generation += 1;
|
|
59
|
+
streamed = 0;
|
|
60
|
+
reported = null;
|
|
61
|
+
expected = latestCompactionSummaryLength(session) ?? DEFAULT_EXPECTED_COMPACTION_SUMMARY_CHARS;
|
|
62
|
+
active = true;
|
|
63
|
+
},
|
|
64
|
+
end() {
|
|
65
|
+
active = false;
|
|
66
|
+
generation += 1;
|
|
67
|
+
},
|
|
68
|
+
dispose() {
|
|
69
|
+
if (disposed)
|
|
70
|
+
return;
|
|
71
|
+
disposed = true;
|
|
72
|
+
active = false;
|
|
73
|
+
generation += 1;
|
|
74
|
+
if (agent.streamFunction === wrapped)
|
|
75
|
+
agent.streamFunction = original;
|
|
76
|
+
},
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
function latestCompactionSummaryLength(session) {
|
|
80
|
+
let entries;
|
|
81
|
+
try {
|
|
82
|
+
entries = session.sessionManager?.getBranch?.() ?? [];
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
for (let index = entries.length - 1; index >= 0; index -= 1) {
|
|
88
|
+
const entry = entries[index];
|
|
89
|
+
if (typeof entry !== "object" || entry === null)
|
|
90
|
+
continue;
|
|
91
|
+
const { type, summary } = entry;
|
|
92
|
+
if (type === "compaction" && typeof summary === "string" && summary.length > 0)
|
|
93
|
+
return summary.length;
|
|
94
|
+
}
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
@@ -6,7 +6,7 @@ export interface PiCapabilityConformanceResult {
|
|
|
6
6
|
export declare const REQUIRED_PI_CAPABILITY_OPERATIONS: Readonly<{
|
|
7
7
|
readonly "public-exports": readonly ["services.create", "session.create", "runtime.create"];
|
|
8
8
|
readonly "session-lifecycle": readonly ["session.new", "session.resume", "session.rebind", "session.dispose"];
|
|
9
|
-
readonly "commands-events": readonly ["prompt", "steer", "followUp", "abort", "compact", "setModel", "setThinkingLevel", "subscribe", "dispose"];
|
|
9
|
+
readonly "commands-events": readonly ["prompt", "steer", "followUp", "clearQueue", "abort", "compact", "setModel", "setThinkingLevel", "subscribe", "dispose"];
|
|
10
10
|
readonly "models-authentication": readonly ["models.list", "models.refresh", "models.completeSimple", "auth.status", "auth.login", "auth.logout", "auth.cancel"];
|
|
11
11
|
readonly settings: readonly ["settings.read", "settings.write", "settings.flush"];
|
|
12
12
|
readonly "resources-extensions": readonly ["resources.discover", "extensions.inline", "extensions.bind", "extensions.reload", "renderers.invoke"];
|
|
@@ -8,7 +8,7 @@ import { createWindowsNulCleanupExtension } from "./windows-filesystem-hygiene.j
|
|
|
8
8
|
export const REQUIRED_PI_CAPABILITY_OPERATIONS = Object.freeze({
|
|
9
9
|
"public-exports": ["services.create", "session.create", "runtime.create"],
|
|
10
10
|
"session-lifecycle": ["session.new", "session.resume", "session.rebind", "session.dispose"],
|
|
11
|
-
"commands-events": ["prompt", "steer", "followUp", "abort", "compact", "setModel", "setThinkingLevel", "subscribe", "dispose"],
|
|
11
|
+
"commands-events": ["prompt", "steer", "followUp", "clearQueue", "abort", "compact", "setModel", "setThinkingLevel", "subscribe", "dispose"],
|
|
12
12
|
"models-authentication": ["models.list", "models.refresh", "models.completeSimple", "auth.status", "auth.login", "auth.logout", "auth.cancel"],
|
|
13
13
|
settings: ["settings.read", "settings.write", "settings.flush"],
|
|
14
14
|
"resources-extensions": ["resources.discover", "extensions.inline", "extensions.bind", "extensions.reload", "renderers.invoke"],
|
|
@@ -85,7 +85,7 @@ export async function runPiUpgradeConformance() {
|
|
|
85
85
|
});
|
|
86
86
|
const session = created.session;
|
|
87
87
|
sessionId = session.sessionId;
|
|
88
|
-
requireMethods(session, "session commands", ["prompt", "steer", "followUp", "abort", "compact", "setModel", "setThinkingLevel", "subscribe", "dispose"]);
|
|
88
|
+
requireMethods(session, "session commands", ["prompt", "steer", "followUp", "clearQueue", "abort", "compact", "setModel", "setThinkingLevel", "subscribe", "dispose"]);
|
|
89
89
|
requireMethods(services.modelRuntime, "models/authentication", ["getModels", "getModel", "completeSimple", "checkAuth", "login", "logout", "refresh"]);
|
|
90
90
|
requireMethods(services.settingsManager, "settings", ["getGlobalSettings", "getProjectSettings", "flush"]);
|
|
91
91
|
requireMethods(services.resourceLoader, "resources/extensions", ["getExtensions", "getSkills", "getPrompts", "getThemes", "reload"]);
|
|
@@ -96,7 +96,7 @@ export async function runPiUpgradeConformance() {
|
|
|
96
96
|
catch (error) {
|
|
97
97
|
throw new PiUpgradeConformanceError("session", error);
|
|
98
98
|
}
|
|
99
|
-
const commandSurface = ["prompt", "steer", "followUp", "abort", "compact", "setModel", "setThinkingLevel", "subscribe", "dispose"];
|
|
99
|
+
const commandSurface = ["prompt", "steer", "followUp", "clearQueue", "abort", "compact", "setModel", "setThinkingLevel", "subscribe", "dispose"];
|
|
100
100
|
const capabilities = Object.entries(REQUIRED_PI_CAPABILITY_OPERATIONS)
|
|
101
101
|
.map(([capabilityName, operations]) => capability(capabilityName, operations));
|
|
102
102
|
validatePiCapabilityResults(VERSION, capabilities);
|
|
@@ -1,24 +1,33 @@
|
|
|
1
1
|
import type { AgentSession, AgentSessionEvent, PromptOptions } from "../startup-public.js";
|
|
2
2
|
import { type AgentCommandOutcome, type AgentEvent } from "../../../contracts/agent-engine/index.js";
|
|
3
|
+
type PiPromptImages = NonNullable<PromptOptions["images"]>;
|
|
3
4
|
export interface PiDocumentedSessionCommands {
|
|
4
5
|
readonly isStreaming: AgentSession["isStreaming"];
|
|
5
6
|
readonly isRetrying: AgentSession["isRetrying"];
|
|
6
7
|
readonly isCompacting: AgentSession["isCompacting"];
|
|
7
8
|
prompt(text: string, options?: Parameters<AgentSession["prompt"]>[1]): Promise<void>;
|
|
8
|
-
steer(text: string): Promise<void>;
|
|
9
|
-
followUp(text: string): Promise<void>;
|
|
9
|
+
steer(text: string, images?: PiPromptImages): Promise<void>;
|
|
10
|
+
followUp(text: string, images?: PiPromptImages): Promise<void>;
|
|
10
11
|
abort(): Promise<void>;
|
|
11
12
|
abortRetry(): void;
|
|
12
13
|
abortCompaction(): void;
|
|
13
14
|
compact(customInstructions?: Parameters<AgentSession["compact"]>[0]): Promise<unknown>;
|
|
15
|
+
clearQueue(): {
|
|
16
|
+
readonly steering: readonly string[];
|
|
17
|
+
readonly followUp: readonly string[];
|
|
18
|
+
};
|
|
14
19
|
executeBash?(command: string, onChunk: unknown, options: {
|
|
15
20
|
readonly excludeFromContext: boolean;
|
|
16
21
|
}): Promise<unknown>;
|
|
22
|
+
/** Optional: extension commands cannot be queued, so they run immediately during compaction as in pinned Pi. */
|
|
23
|
+
readonly extensionRunner?: {
|
|
24
|
+
getCommand(name: string): unknown;
|
|
25
|
+
} | undefined;
|
|
17
26
|
}
|
|
18
27
|
export type PiSessionCommand = {
|
|
19
28
|
readonly type: "prompt" | "steer" | "follow-up";
|
|
20
29
|
readonly text: string;
|
|
21
|
-
readonly images?:
|
|
30
|
+
readonly images?: PiPromptImages;
|
|
22
31
|
} | {
|
|
23
32
|
readonly type: "abort" | "retry" | "compact";
|
|
24
33
|
} | {
|
|
@@ -37,9 +46,22 @@ export declare class PiSessionCommandIntegration {
|
|
|
37
46
|
private readonly session;
|
|
38
47
|
constructor(session: PiDocumentedSessionCommands);
|
|
39
48
|
execute(command: PiSessionCommand): Promise<PiSessionCommandResult>;
|
|
49
|
+
/**
|
|
50
|
+
* Delivers the messages queued during a manual compaction. The session is idle when a manual
|
|
51
|
+
* compaction ends, so nothing would consume the queue: the first message starts one run
|
|
52
|
+
* through the ordinary prompt path with its mode, and the rest are re-queued for that run,
|
|
53
|
+
* whose initial poll injects them in order. Automatic compaction needs no delivery: its
|
|
54
|
+
* continuing run or pending prompt consumes the queue itself. The run is not awaited; when
|
|
55
|
+
* the start is refused before the message is accepted, the queue is restored and the error
|
|
56
|
+
* reported through `onFailure`.
|
|
57
|
+
*/
|
|
58
|
+
deliverQueuedAfterCompaction(onFailure: (error: unknown) => void): Promise<void>;
|
|
59
|
+
/** Forgets the attachments kept for queued messages; the queue itself was cleared by the caller. */
|
|
60
|
+
forgetQueuedImages(): void;
|
|
40
61
|
}
|
|
41
62
|
export interface PiOrderedEventIntegration {
|
|
42
63
|
dispose(): void;
|
|
43
64
|
}
|
|
44
65
|
export declare function subscribeToPiSessionEvents(session: Pick<AgentSession, "subscribe">, sessionId: string, emit: (event: AgentEvent) => void, malformed: (diagnostic: string) => void): PiOrderedEventIntegration;
|
|
45
66
|
export declare function convertPiSessionEvent(event: AgentSessionEvent, sessionId: string, sequence: number): AgentEvent | null;
|
|
67
|
+
export {};
|
|
@@ -3,6 +3,9 @@ import { AGENT_ENGINE_CONTRACT_VERSION, } from "../../../contracts/agent-engine/
|
|
|
3
3
|
export class PiSessionCommandIntegration {
|
|
4
4
|
session;
|
|
5
5
|
#lastPrompt = null;
|
|
6
|
+
// Rationale: Pi's queue restore returns text only, so the attachments of messages queued
|
|
7
|
+
// during compaction are kept here until the queue is delivered or cleared.
|
|
8
|
+
#queuedImages = [];
|
|
6
9
|
constructor(session) {
|
|
7
10
|
this.session = session;
|
|
8
11
|
}
|
|
@@ -18,22 +21,24 @@ export class PiSessionCommandIntegration {
|
|
|
18
21
|
});
|
|
19
22
|
return { outcome: "completed" };
|
|
20
23
|
case "steer":
|
|
24
|
+
case "follow-up": {
|
|
21
25
|
this.#lastPrompt = command.text;
|
|
26
|
+
const mode = command.type === "steer" ? "steer" : "followUp";
|
|
27
|
+
if (this.session.isCompacting && !this.#isExtensionCommand(command.text)) {
|
|
28
|
+
// Compatibility: match interactive Pi: prompt() refuses input during manual compaction,
|
|
29
|
+
// while the engine queue accepts it at any time and delivers it when compaction ends.
|
|
30
|
+
await this.#queue(mode, command.text, command.images);
|
|
31
|
+
return { outcome: "completed" };
|
|
32
|
+
}
|
|
22
33
|
// Compatibility: match interactive Pi: prompt() owns template/extension expansion and
|
|
23
34
|
// turns the accepted steering message into the visible user row while
|
|
24
35
|
// later messages remain in the pending queue.
|
|
25
36
|
await this.session.prompt(command.text, {
|
|
26
|
-
streamingBehavior:
|
|
27
|
-
...(command.images === undefined ? {} : { images: [...command.images] }),
|
|
28
|
-
});
|
|
29
|
-
return { outcome: "completed" };
|
|
30
|
-
case "follow-up":
|
|
31
|
-
this.#lastPrompt = command.text;
|
|
32
|
-
await this.session.prompt(command.text, {
|
|
33
|
-
streamingBehavior: "followUp",
|
|
37
|
+
streamingBehavior: mode,
|
|
34
38
|
...(command.images === undefined ? {} : { images: [...command.images] }),
|
|
35
39
|
});
|
|
36
40
|
return { outcome: "completed" };
|
|
41
|
+
}
|
|
37
42
|
case "abort":
|
|
38
43
|
if (this.session.isRetrying)
|
|
39
44
|
this.session.abortRetry();
|
|
@@ -61,6 +66,86 @@ export class PiSessionCommandIntegration {
|
|
|
61
66
|
}
|
|
62
67
|
}
|
|
63
68
|
}
|
|
69
|
+
/**
|
|
70
|
+
* Delivers the messages queued during a manual compaction. The session is idle when a manual
|
|
71
|
+
* compaction ends, so nothing would consume the queue: the first message starts one run
|
|
72
|
+
* through the ordinary prompt path with its mode, and the rest are re-queued for that run,
|
|
73
|
+
* whose initial poll injects them in order. Automatic compaction needs no delivery: its
|
|
74
|
+
* continuing run or pending prompt consumes the queue itself. The run is not awaited; when
|
|
75
|
+
* the start is refused before the message is accepted, the queue is restored and the error
|
|
76
|
+
* reported through `onFailure`.
|
|
77
|
+
*/
|
|
78
|
+
async deliverQueuedAfterCompaction(onFailure) {
|
|
79
|
+
if (this.session.isStreaming || this.session.isCompacting)
|
|
80
|
+
return;
|
|
81
|
+
const { steering, followUp } = this.session.clearQueue();
|
|
82
|
+
const queued = [
|
|
83
|
+
...steering.map(text => ({ mode: "steer", text, images: this.#takeImages(text) })),
|
|
84
|
+
...followUp.map(text => ({ mode: "followUp", text, images: this.#takeImages(text) })),
|
|
85
|
+
];
|
|
86
|
+
this.#queuedImages = [];
|
|
87
|
+
const [first, ...rest] = queued;
|
|
88
|
+
if (first === undefined)
|
|
89
|
+
return;
|
|
90
|
+
this.#lastPrompt = first.text;
|
|
91
|
+
let accepted = false;
|
|
92
|
+
const started = this.session.prompt(first.text, {
|
|
93
|
+
streamingBehavior: first.mode,
|
|
94
|
+
preflightResult: success => { accepted = success; },
|
|
95
|
+
...(first.images === undefined ? {} : { images: [...first.images] }),
|
|
96
|
+
});
|
|
97
|
+
for (const item of rest)
|
|
98
|
+
await this.#queue(item.mode, item.text, item.images);
|
|
99
|
+
started.catch(async (error) => {
|
|
100
|
+
// Security: a run that reached the provider is never resent; only a refused start restores.
|
|
101
|
+
try {
|
|
102
|
+
if (!accepted)
|
|
103
|
+
await this.#restoreQueue(queued);
|
|
104
|
+
}
|
|
105
|
+
finally {
|
|
106
|
+
onFailure(error);
|
|
107
|
+
}
|
|
108
|
+
}).catch(() => undefined);
|
|
109
|
+
}
|
|
110
|
+
/** Forgets the attachments kept for queued messages; the queue itself was cleared by the caller. */
|
|
111
|
+
forgetQueuedImages() {
|
|
112
|
+
this.#queuedImages = [];
|
|
113
|
+
}
|
|
114
|
+
// Rationale: the rest were re-queued before the refused start surfaced, so the queue is rebuilt
|
|
115
|
+
// in the original order; anything queued in between keeps its place after it.
|
|
116
|
+
async #restoreQueue(queued) {
|
|
117
|
+
const { steering, followUp } = this.session.clearQueue();
|
|
118
|
+
const restored = new Set(queued.map(item => item.text));
|
|
119
|
+
const extra = [
|
|
120
|
+
...steering.filter(text => !restored.has(text)).map(text => ({ mode: "steer", text, images: this.#takeImages(text) })),
|
|
121
|
+
...followUp.filter(text => !restored.has(text)).map(text => ({ mode: "followUp", text, images: this.#takeImages(text) })),
|
|
122
|
+
];
|
|
123
|
+
this.#queuedImages = [];
|
|
124
|
+
for (const item of [...queued, ...extra])
|
|
125
|
+
await this.#queue(item.mode, item.text, item.images);
|
|
126
|
+
}
|
|
127
|
+
async #queue(mode, text, images) {
|
|
128
|
+
if (images !== undefined && images.length > 0)
|
|
129
|
+
this.#queuedImages.push({ text, images: [...images] });
|
|
130
|
+
if (mode === "steer")
|
|
131
|
+
await this.session.steer(text, images === undefined ? undefined : [...images]);
|
|
132
|
+
else
|
|
133
|
+
await this.session.followUp(text, images === undefined ? undefined : [...images]);
|
|
134
|
+
}
|
|
135
|
+
#takeImages(text) {
|
|
136
|
+
const index = this.#queuedImages.findIndex(item => item.text === text);
|
|
137
|
+
if (index === -1)
|
|
138
|
+
return undefined;
|
|
139
|
+
const [item] = this.#queuedImages.splice(index, 1);
|
|
140
|
+
return item?.images;
|
|
141
|
+
}
|
|
142
|
+
#isExtensionCommand(text) {
|
|
143
|
+
if (!text.startsWith("/"))
|
|
144
|
+
return false;
|
|
145
|
+
const spaceIndex = text.indexOf(" ");
|
|
146
|
+
const name = spaceIndex === -1 ? text.slice(1) : text.slice(1, spaceIndex);
|
|
147
|
+
return this.session.extensionRunner?.getCommand(name) !== undefined;
|
|
148
|
+
}
|
|
64
149
|
}
|
|
65
150
|
export function subscribeToPiSessionEvents(session, sessionId, emit, malformed) {
|
|
66
151
|
let sequence = 0;
|
|
@@ -103,6 +103,7 @@ export class OwnedUiSessionShellRoot {
|
|
|
103
103
|
});
|
|
104
104
|
this.resources = createPiShellLoadedResources(startup.resources ?? [], startup.expanded ?? false);
|
|
105
105
|
this.#status = createPiShellStatus(view, progressStatusText, handlers);
|
|
106
|
+
this.#status.setProgressPresentation(this.#customViewport ? "custom-viewport" : "pinned");
|
|
106
107
|
this.#footer = createPiShellFooter(this.#viewWithExtensionStatuses(view), cwd, this.#customViewport ? "a1" : "pi");
|
|
107
108
|
this.#queued = createPiQueuedInputStatus(view.editor.queuedSubmissions, this.#customViewport ? "custom-viewport" : "pinned");
|
|
108
109
|
this.editor = createPiShellEditor({
|
|
@@ -66,7 +66,6 @@ export class OwnedUiSessionShell {
|
|
|
66
66
|
#showImages = true;
|
|
67
67
|
#imageWidthCells = 80;
|
|
68
68
|
#fullscreenExitOutput = "transcript";
|
|
69
|
-
#compactionQueue = [];
|
|
70
69
|
#waitingImages = new Map();
|
|
71
70
|
#lastClearTime = 0;
|
|
72
71
|
#lastEscapeTime = 0;
|
|
@@ -494,8 +493,6 @@ export class OwnedUiSessionShell {
|
|
|
494
493
|
model: event.model,
|
|
495
494
|
});
|
|
496
495
|
}
|
|
497
|
-
if (view.lifecycle === "ready" && this.#compactionQueue.length > 0)
|
|
498
|
-
void this.#flushCompactionQueue();
|
|
499
496
|
if (event.type === "session-lifecycle" && event.lifecycle === "stopped")
|
|
500
497
|
this.#settleStoppedLifecycle();
|
|
501
498
|
});
|
|
@@ -642,18 +639,8 @@ export class OwnedUiSessionShell {
|
|
|
642
639
|
}
|
|
643
640
|
}
|
|
644
641
|
}
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
this.#compactionQueue.push({
|
|
648
|
-
text: input,
|
|
649
|
-
draft: displayInput,
|
|
650
|
-
type: "steer",
|
|
651
|
-
...(prepared.images.length === 0 ? {} : { images: prepared.images }),
|
|
652
|
-
});
|
|
653
|
-
this.root.appendWorkflowResult({ command: "compact", outcome: "completed", message: `Queued during compaction: ${input}` });
|
|
654
|
-
this.runtime.requestRender();
|
|
655
|
-
return { outcome: "completed", diagnostic: null };
|
|
656
|
-
}
|
|
642
|
+
// Compatibility: match interactive Pi: input during compaction is queued steering; the engine
|
|
643
|
+
// shows it in the pending rows and delivers it when compaction ends.
|
|
657
644
|
const type = this.view().lifecycle === "busy" ? "steer" : "prompt";
|
|
658
645
|
this.#rememberInput(displayInput, type);
|
|
659
646
|
this.root.resumeViewportFollowing();
|
|
@@ -772,15 +759,6 @@ export class OwnedUiSessionShell {
|
|
|
772
759
|
if (this.root.editor.getText() === draft)
|
|
773
760
|
this.root.editor.setText("");
|
|
774
761
|
this.root.resumeViewportFollowing();
|
|
775
|
-
if (this.view().status.workingMessage?.startsWith("Compacting") === true) {
|
|
776
|
-
this.#compactionQueue.push({
|
|
777
|
-
text,
|
|
778
|
-
draft: displayInput,
|
|
779
|
-
type: "follow-up",
|
|
780
|
-
...(prepared.images.length === 0 ? {} : { images: prepared.images }),
|
|
781
|
-
});
|
|
782
|
-
return { outcome: "completed", diagnostic: null };
|
|
783
|
-
}
|
|
784
762
|
return this.#execute({
|
|
785
763
|
type: "follow-up",
|
|
786
764
|
correlationId: this.#correlation("follow-up"),
|
|
@@ -790,9 +768,8 @@ export class OwnedUiSessionShell {
|
|
|
790
768
|
}, displayInput);
|
|
791
769
|
}
|
|
792
770
|
restoreQueuedInput() {
|
|
793
|
-
const queued = [...this.#waitingImages.keys(), ...this.backend.clearQueuedWorkflows()
|
|
771
|
+
const queued = [...this.#waitingImages.keys(), ...this.backend.clearQueuedWorkflows()];
|
|
794
772
|
this.#cancelWaitingImages();
|
|
795
|
-
this.#compactionQueue = [];
|
|
796
773
|
if (queued.length === 0)
|
|
797
774
|
return;
|
|
798
775
|
this.root.editor.setText(queued.join("\n"));
|
|
@@ -1394,6 +1371,10 @@ export class OwnedUiSessionShell {
|
|
|
1394
1371
|
attempt(() => this.#unsubscribe());
|
|
1395
1372
|
attempt(() => this.#dialogHandle?.hide());
|
|
1396
1373
|
attempt(() => this.#extensionBridge.dispose());
|
|
1374
|
+
// Invariant: from here to the leave nothing but the outro paints. A throttled frame the
|
|
1375
|
+
// renderer still has queued would otherwise land during the stop-time input drain and
|
|
1376
|
+
// flash the prompt and footer, whether or not an effect plays.
|
|
1377
|
+
attempt(() => this.#freezeQuitPresentation());
|
|
1397
1378
|
await this.#playQuitOutro(outroFrame);
|
|
1398
1379
|
// Invariant: terminal restoration precedes any potentially stalled backend teardown. The
|
|
1399
1380
|
// fullscreen leave preserves the screen: the pinned runtime never dumps its final document
|
|
@@ -1415,8 +1396,8 @@ export class OwnedUiSessionShell {
|
|
|
1415
1396
|
if (!this.runtime.active || this.runtime.mode !== "fullscreen")
|
|
1416
1397
|
return null;
|
|
1417
1398
|
try {
|
|
1418
|
-
const { effect, durationMs } = outro.snapshot();
|
|
1419
|
-
if (
|
|
1399
|
+
const { enabled, effect, durationMs } = outro.snapshot();
|
|
1400
|
+
if (!enabled)
|
|
1420
1401
|
return null;
|
|
1421
1402
|
const viewport = this.runtime.viewport();
|
|
1422
1403
|
return { rows: this.#damageTerminal.presentedRows(), columns: viewport.columns, height: viewport.rows, settings: { effect, durationMs } };
|
|
@@ -1425,6 +1406,11 @@ export class OwnedUiSessionShell {
|
|
|
1425
1406
|
return null;
|
|
1426
1407
|
}
|
|
1427
1408
|
}
|
|
1409
|
+
#freezeQuitPresentation() {
|
|
1410
|
+
if (!this.#customViewport || !this.runtime.active || this.runtime.mode !== "fullscreen")
|
|
1411
|
+
return;
|
|
1412
|
+
this.runtime.freezePresentation();
|
|
1413
|
+
}
|
|
1428
1414
|
// Rationale: any failure here only skips the effect; restoration always follows.
|
|
1429
1415
|
async #playQuitOutro(capture) {
|
|
1430
1416
|
const outro = this.#quitOutro;
|
|
@@ -1436,7 +1422,6 @@ export class OwnedUiSessionShell {
|
|
|
1436
1422
|
const frame = captureQuitOutroFrame(capture.rows, capture.columns, capture.height);
|
|
1437
1423
|
if (frame === null || !this.runtime.active)
|
|
1438
1424
|
return;
|
|
1439
|
-
this.runtime.freezePresentation();
|
|
1440
1425
|
await playQuitOutro(frame, capture.settings.effect, capture.settings.durationMs, {
|
|
1441
1426
|
write: data => this.runtime.writeControl(data),
|
|
1442
1427
|
...(outro.now === undefined ? {} : { now: outro.now }),
|
|
@@ -1765,19 +1750,6 @@ export class OwnedUiSessionShell {
|
|
|
1765
1750
|
this.root.setInputSurface(null);
|
|
1766
1751
|
this.runtime.requestRender();
|
|
1767
1752
|
}
|
|
1768
|
-
async #flushCompactionQueue() {
|
|
1769
|
-
const queued = this.#compactionQueue;
|
|
1770
|
-
this.#compactionQueue = [];
|
|
1771
|
-
for (const item of queued) {
|
|
1772
|
-
await this.#execute({
|
|
1773
|
-
type: item.type,
|
|
1774
|
-
correlationId: this.#correlation(`compaction-${item.type}`),
|
|
1775
|
-
sessionId: this.backend.sessionId,
|
|
1776
|
-
text: item.text,
|
|
1777
|
-
...(item.images === undefined ? {} : { images: item.images }),
|
|
1778
|
-
}, item.draft);
|
|
1779
|
-
}
|
|
1780
|
-
}
|
|
1781
1753
|
async #execute(command, draft) {
|
|
1782
1754
|
if (draft === undefined)
|
|
1783
1755
|
return this.backend.execute(command);
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
"platform": "darwin",
|
|
6
6
|
"architecture": "arm64",
|
|
7
7
|
"capability": "supported",
|
|
8
|
-
"builtAt": "2026-09-
|
|
8
|
+
"builtAt": "2026-09-17T17:09:52.350Z",
|
|
9
9
|
"artifact": {
|
|
10
10
|
"filename": "process-guardian",
|
|
11
11
|
"sha256": "9db1726bbe3fc2e8292f2ead1e7e9d0fd4d7d0dc9217b372582bf354b0f565dc",
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
"platform": "linux",
|
|
6
6
|
"architecture": "x64",
|
|
7
7
|
"capability": "supported",
|
|
8
|
-
"builtAt": "2026-09-
|
|
8
|
+
"builtAt": "2026-09-17T17:09:48.434Z",
|
|
9
9
|
"artifact": {
|
|
10
10
|
"filename": "process-guardian",
|
|
11
11
|
"sha256": "d8cda6b0c7cb36c0cc41e90802aceebf6c02eaebbc08a83d7d963d2e918dbd7a",
|
|
@@ -5,10 +5,10 @@
|
|
|
5
5
|
"platform": "win32",
|
|
6
6
|
"architecture": "x64",
|
|
7
7
|
"capability": "supported",
|
|
8
|
-
"builtAt": "2026-09-
|
|
8
|
+
"builtAt": "2026-09-17T17:10:27.708Z",
|
|
9
9
|
"artifact": {
|
|
10
10
|
"filename": "process-guardian.exe",
|
|
11
|
-
"sha256": "
|
|
11
|
+
"sha256": "7f414d3e5005949040b86ae78a4f73375ac5773c6ad9f6a3b3f22e7d9f7d5b11",
|
|
12
12
|
"size": 177664
|
|
13
13
|
},
|
|
14
14
|
"provenance": {
|
|
Binary file
|
|
@@ -29,14 +29,13 @@ export function renderListRow(row, state, valueColumn, width, theme) {
|
|
|
29
29
|
? `${theme.fg("accent", cursor)}${theme.fg("accent", labelPadded)}`
|
|
30
30
|
: `${cursor}${theme.plain(labelPadded)}`;
|
|
31
31
|
const gap = Math.max(2, valueColumn - displayWidth(leftRaw));
|
|
32
|
-
//
|
|
33
|
-
//
|
|
34
|
-
//
|
|
32
|
+
// Rationale: a declared difference from pinned SettingsList, which paints the selected
|
|
33
|
+
// value in the accent too. Here only the cursor and label carry the selection; the
|
|
34
|
+
// value reads the same on every row, and pointer hover brightens it without moving
|
|
35
|
+
// the keyboard selection.
|
|
35
36
|
const valueHovered = state.hovered && state.region !== "label";
|
|
36
37
|
const stepper = row.stepper !== undefined && valueHovered;
|
|
37
|
-
const value =
|
|
38
|
-
? theme.fg("accent", row.value)
|
|
39
|
-
: valueHovered ? theme.plain(row.value) : theme.fg("muted", row.value);
|
|
38
|
+
const value = valueHovered ? theme.plain(row.value) : theme.fg("muted", row.value);
|
|
40
39
|
const minus = stepper
|
|
41
40
|
? row.stepper?.lower === true
|
|
42
41
|
? state.region === "minus" ? theme.plain("- ") : theme.fg("dim", "- ")
|
|
@@ -1,12 +1,17 @@
|
|
|
1
|
-
import { type ScrollbarGeometry } from "./scrollbar.js";
|
|
1
|
+
import { type ScrollbarGeometry, type ScrollbarPresentation } from "./scrollbar.js";
|
|
2
2
|
import type { UiTheme } from "./theme.js";
|
|
3
3
|
/** Columns the rail occupies: its own, plus the gap before it. */
|
|
4
4
|
export declare const RAIL_COLUMNS = 2;
|
|
5
5
|
export interface RailOptions {
|
|
6
6
|
/** Rows at the top the rail does not run beside, such as a sticky header. */
|
|
7
7
|
readonly topInset?: number;
|
|
8
|
+
/** How the rail shows. Absent draws the thin rail whenever the content overflows. */
|
|
9
|
+
readonly presentation?: ScrollbarPresentation;
|
|
8
10
|
}
|
|
9
|
-
/**
|
|
11
|
+
/**
|
|
12
|
+
* Draws the rail beside each row, padding the rows to a common width first.
|
|
13
|
+
* A presentation that reserves no space returns the rows untouched.
|
|
14
|
+
*/
|
|
10
15
|
export declare function withScrollbarRail(lines: readonly string[], geometry: ScrollbarGeometry | null, contentWidth: number, theme: UiTheme, options?: RailOptions): readonly string[];
|
|
11
16
|
/**
|
|
12
17
|
* What a list shows instead of rows: a mark and a line, both quiet, sitting in
|
|
@@ -3,13 +3,23 @@ import { displayWidth } from "./text.js";
|
|
|
3
3
|
// Rationale: shared list chrome belongs here rather than in individual screens.
|
|
4
4
|
/** Columns the rail occupies: its own, plus the gap before it. */
|
|
5
5
|
export const RAIL_COLUMNS = 2;
|
|
6
|
-
/**
|
|
6
|
+
/**
|
|
7
|
+
* Draws the rail beside each row, padding the rows to a common width first.
|
|
8
|
+
* A presentation that reserves no space returns the rows untouched.
|
|
9
|
+
*/
|
|
7
10
|
export function withScrollbarRail(lines, geometry, contentWidth, theme, options = {}) {
|
|
8
11
|
const inset = options.topInset ?? 0;
|
|
12
|
+
const presentation = options.presentation
|
|
13
|
+
?? { visible: geometry !== null, reservesSpace: true, trackGlyph: "│", thumbGlyph: "│" };
|
|
14
|
+
if (!presentation.reservesSpace)
|
|
15
|
+
return lines;
|
|
16
|
+
const drawn = presentation.visible && geometry !== null;
|
|
9
17
|
return lines.map((line, offset) => {
|
|
10
|
-
const cell = offset < inset ||
|
|
18
|
+
const cell = offset < inset || !drawn
|
|
11
19
|
? " "
|
|
12
|
-
: isThumbRow(geometry, offset - inset)
|
|
20
|
+
: isThumbRow(geometry, offset - inset)
|
|
21
|
+
? theme.fg("accent", presentation.thumbGlyph)
|
|
22
|
+
: theme.fg("dim", presentation.trackGlyph);
|
|
13
23
|
return `${pad(line, contentWidth)} ${cell}`;
|
|
14
24
|
});
|
|
15
25
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export declare const OWNED_UI_SETTINGS_VERSION =
|
|
1
|
+
export declare const OWNED_UI_SETTINGS_VERSION = 6;
|
|
2
2
|
export type OwnedUiSettingValue = string | number | boolean;
|
|
3
3
|
export type OwnedUiSettingApplication = "live" | "restart";
|
|
4
4
|
export interface OwnedUiSettingDeclaration {
|
|
@@ -1,11 +1,22 @@
|
|
|
1
|
-
export const OWNED_UI_SETTINGS_VERSION =
|
|
1
|
+
export const OWNED_UI_SETTINGS_VERSION = 6;
|
|
2
2
|
const MAX_ID_LENGTH = 64;
|
|
3
3
|
const ID_PATTERN = /^[a-z][a-z0-9]*(?:[A-Z][a-z0-9]*)*$/;
|
|
4
|
+
/** Declared first so the settings screen opens on it; sections follow first-declaration order. */
|
|
5
|
+
const GENERIC_SECTION = Object.freeze({ id: "generic", title: "Generic" });
|
|
4
6
|
const SCROLL_SECTION = Object.freeze({ id: "scroll", title: "Scroll" });
|
|
5
7
|
const QUIT_SECTION = Object.freeze({ id: "quit", title: "Quit" });
|
|
6
8
|
/** Playback lengths the quit outro offers, in milliseconds. */
|
|
7
9
|
export const QUIT_EFFECT_DURATIONS_MS = Object.freeze(Array.from({ length: 18 }, (_, index) => 300 + index * 100));
|
|
8
10
|
export const OWNED_UI_SETTING_DECLARATIONS = Object.freeze([
|
|
11
|
+
Object.freeze({
|
|
12
|
+
id: "quitAnimation",
|
|
13
|
+
label: "Exit animation",
|
|
14
|
+
section: GENERIC_SECTION,
|
|
15
|
+
description: "Play the quit effect when the session quits. Off returns to the terminal immediately.",
|
|
16
|
+
application: "live",
|
|
17
|
+
defaultValue: true,
|
|
18
|
+
allowedValues: Object.freeze([true, false]),
|
|
19
|
+
}),
|
|
9
20
|
Object.freeze({
|
|
10
21
|
id: "scrollbarAppearance",
|
|
11
22
|
label: "Scrollbar mode",
|
|
@@ -58,7 +69,7 @@ export const OWNED_UI_SETTING_DECLARATIONS = Object.freeze([
|
|
|
58
69
|
description: "Animation played over the last screen when the session quits.",
|
|
59
70
|
application: "live",
|
|
60
71
|
defaultValue: "fall",
|
|
61
|
-
allowedValues: Object.freeze(["fall", "dissolve", "starburst", "waves"
|
|
72
|
+
allowedValues: Object.freeze(["fall", "dissolve", "starburst", "waves"]),
|
|
62
73
|
}),
|
|
63
74
|
Object.freeze({
|
|
64
75
|
id: "quitEffectDurationMs",
|
|
@@ -36,6 +36,19 @@ export const OWNED_UI_SETTINGS_MIGRATIONS = Object.freeze([
|
|
|
36
36
|
return { ...values };
|
|
37
37
|
},
|
|
38
38
|
}),
|
|
39
|
+
Object.freeze({
|
|
40
|
+
to: 6,
|
|
41
|
+
description: "Move the disabled quit effect into the exit-animation toggle.",
|
|
42
|
+
migrate(values) {
|
|
43
|
+
// Invariant: a profile that chose off keeps quitting without an animation; the
|
|
44
|
+
// effect it stored is no longer a choice, so it resolves to the default.
|
|
45
|
+
if (values.quitEffect !== "off")
|
|
46
|
+
return { ...values };
|
|
47
|
+
const migrated = { ...values, quitAnimation: false };
|
|
48
|
+
delete migrated.quitEffect;
|
|
49
|
+
return migrated;
|
|
50
|
+
},
|
|
51
|
+
}),
|
|
39
52
|
]);
|
|
40
53
|
export function assertOwnedUiSettingsMigrations(migrations, currentVersion = OWNED_UI_SETTINGS_VERSION) {
|
|
41
54
|
const firstProduced = currentVersion - migrations.length + 1;
|