@timurproko/a1 0.2.7-dev.806 → 0.2.7-dev.809
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/app/session-shell/dialog-coordinator.d.ts +42 -0
- package/dist/app/session-shell/dialog-coordinator.js +133 -0
- package/dist/app/session-shell/dock-chrome.d.ts +34 -0
- package/dist/app/session-shell/dock-chrome.js +109 -0
- package/dist/app/session-shell/exit-notice.d.ts +1 -1
- package/dist/app/session-shell/exit-notice.js +1 -1
- package/dist/app/session-shell/extension-surface.d.ts +30 -0
- package/dist/app/session-shell/extension-surface.js +105 -0
- package/dist/app/session-shell/input-surface-switch.d.ts +36 -0
- package/dist/app/session-shell/input-surface-switch.js +85 -0
- package/dist/app/session-shell/login-interaction.d.ts +27 -0
- package/dist/app/session-shell/login-interaction.js +91 -0
- package/dist/app/session-shell/prompt-submission.d.ts +48 -0
- package/dist/app/session-shell/prompt-submission.js +276 -0
- package/dist/app/session-shell/resident-tab-coordinator.d.ts +41 -0
- package/dist/app/session-shell/resident-tab-coordinator.js +120 -0
- package/dist/app/session-shell/session-concern.d.ts +22 -0
- package/dist/app/session-shell/session-concern.js +29 -0
- package/dist/app/session-shell/session-lifecycle.d.ts +29 -0
- package/dist/app/session-shell/session-lifecycle.js +49 -0
- package/dist/app/session-shell/session-shell-root.d.ts +1 -1
- package/dist/app/session-shell/session-shell-root.js +182 -931
- package/dist/app/session-shell/session-shell.d.ts +2 -8
- package/dist/app/session-shell/session-shell.js +209 -897
- package/dist/app/session-shell/settings-bindings.d.ts +17 -0
- package/dist/app/session-shell/settings-bindings.js +91 -0
- package/dist/app/session-shell/shell-editor-factory.d.ts +27 -0
- package/dist/app/session-shell/shell-editor-factory.js +102 -0
- package/dist/app/session-shell/shell-host.d.ts +6 -0
- package/dist/app/session-shell/shell-host.js +26 -4
- package/dist/app/session-shell/shell-layout.d.ts +64 -0
- package/dist/app/session-shell/shell-layout.js +18 -0
- package/dist/app/session-shell/suggestion-surface.d.ts +25 -0
- package/dist/app/session-shell/suggestion-surface.js +63 -0
- package/dist/app/session-shell/transcript-document.d.ts +78 -0
- package/dist/app/session-shell/transcript-document.js +365 -0
- package/dist/app/session-shell/viewport-layout.d.ts +31 -0
- package/dist/app/session-shell/viewport-layout.js +160 -0
- package/dist/app/session-shell/workflow-output.d.ts +33 -0
- package/dist/app/session-shell/workflow-output.js +174 -0
- package/dist/app/session-shell/workflow-router.d.ts +25 -0
- package/dist/app/session-shell/workflow-router.js +39 -0
- package/dist/composition/owned-ui.d.ts +6 -25
- package/dist/composition/owned-ui.js +48 -242
- package/dist/composition/resident-tab-bridge.d.ts +16 -0
- package/dist/composition/resident-tab-bridge.js +159 -0
- package/dist/composition/session-backend.d.ts +19 -0
- package/dist/composition/session-backend.js +1 -0
- package/dist/composition/session-runtime-registry.d.ts +20 -0
- package/dist/composition/session-runtime-registry.js +85 -0
- package/dist/composition/settings-route-host.d.ts +6 -1
- package/dist/composition/settings-route-host.js +7 -6
- package/dist/composition/startup-route.d.ts +32 -0
- package/dist/composition/startup-route.js +72 -0
- package/dist/features/owned-ui/settings-app.d.ts +4 -1
- package/dist/features/owned-ui/settings-app.js +9 -3
- package/dist/features/resident-tab/shortcut-declarations.js +1 -1
- package/dist/features/resident-tab/status-deriver.d.ts +4 -3
- package/dist/features/resident-tab/status-deriver.js +2 -5
- package/dist/foundation/release/bootstrap.js +2 -2
- package/dist/foundation/resident-tabs/bridge-client.d.ts +1 -1
- package/dist/foundation/resident-tabs/bridge-client.js +2 -1
- package/dist/foundation/resident-tabs/fixture-control.d.ts +7 -0
- package/dist/foundation/resident-tabs/fixture-control.js +35 -0
- package/dist/foundation/resident-tabs/progress-observation.d.ts +7 -0
- package/dist/foundation/resident-tabs/progress-observation.js +119 -0
- package/dist/foundation/terminal-cleanup/index.d.ts +1 -1
- package/dist/foundation/terminal-cleanup/index.js +1 -1
- package/dist/foundation/terminal-cleanup/terminal-reset.d.ts +5 -0
- package/dist/foundation/terminal-cleanup/terminal-reset.js +5 -0
- package/dist/integrations/pi/engine/adapter.d.ts +4 -77
- package/dist/integrations/pi/engine/adapter.js +85 -393
- package/dist/integrations/pi/engine/command-dispatch.d.ts +13 -0
- package/dist/integrations/pi/engine/command-dispatch.js +57 -0
- package/dist/integrations/pi/engine/host.js +2 -2
- package/dist/integrations/pi/engine/ports.d.ts +56 -0
- package/dist/integrations/pi/engine/ports.js +89 -0
- package/dist/integrations/pi/engine/session-runtime.js +5 -2
- package/dist/integrations/pi/engine/view-state.d.ts +42 -0
- package/dist/integrations/pi/engine/view-state.js +126 -0
- package/dist/integrations/pi/tui-runtime/adapter.d.ts +6 -0
- package/dist/integrations/pi/tui-runtime/adapter.js +28 -1
- package/dist/integrations/pi/tui-runtime/damage-aware-terminal.d.ts +11 -0
- package/dist/integrations/pi/tui-runtime/damage-aware-terminal.js +30 -1
- package/dist/native/darwin-arm64/manifest.json +1 -1
- package/dist/native/darwin-arm64/terminal-host.manifest.json +3 -3
- package/dist/native/darwin-arm64/terminal-host.node +0 -0
- package/dist/native/darwin-x64/manifest.json +1 -1
- package/dist/native/darwin-x64/terminal-host.manifest.json +3 -3
- package/dist/native/darwin-x64/terminal-host.node +0 -0
- package/dist/native/linux-x64/manifest.json +1 -1
- package/dist/native/linux-x64/terminal-host.manifest.json +3 -3
- package/dist/native/linux-x64/terminal-host.node +0 -0
- package/dist/native/win32-x64/manifest.json +2 -2
- package/dist/native/win32-x64/process-guardian.node +0 -0
- package/dist/native/win32-x64/terminal-host.manifest.json +3 -3
- package/dist/native/win32-x64/terminal-host.node +0 -0
- package/dist/product-identity.d.ts +2 -2
- package/dist/product-identity.js +2 -2
- package/dist/product-identity.json +6 -0
- package/dist/ui/settings/manager.d.ts +6 -0
- package/dist/ui/settings/manager.js +25 -0
- package/docs/architecture/boundaries.md +14 -0
- package/docs/architecture/resident-tabs.md +23 -18
- package/package.json +2 -1
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { createPiShellExtensionSelector, createPiShellLoginDialog, } from "../../integrations/pi/components/shell-selectors-dialogs.js";
|
|
2
|
+
import { componentRuntime } from "./shell-host.js";
|
|
3
|
+
/**
|
|
4
|
+
* The engine's login flow in the dock: one login dialog at a time, the prompts and selections the
|
|
5
|
+
* engine asks through it, and the terminal's blocked-on-authentication status while it is open.
|
|
6
|
+
*/
|
|
7
|
+
export class LoginInteraction {
|
|
8
|
+
#ports;
|
|
9
|
+
#dialog;
|
|
10
|
+
constructor(ports) {
|
|
11
|
+
this.#ports = ports;
|
|
12
|
+
}
|
|
13
|
+
start(request) {
|
|
14
|
+
this.finish();
|
|
15
|
+
const dialog = createPiShellLoginDialog(componentRuntime(this.#ports.terminal), request.providerId, success => {
|
|
16
|
+
if (!success)
|
|
17
|
+
this.finish();
|
|
18
|
+
}, request.providerName);
|
|
19
|
+
this.#dialog = dialog;
|
|
20
|
+
this.#ports.root.setInputSurface(dialog);
|
|
21
|
+
this.#ports.programStatus.setBlocked("authentication", {
|
|
22
|
+
kind: "auth",
|
|
23
|
+
message: `Log in to ${request.providerName}`,
|
|
24
|
+
});
|
|
25
|
+
this.#ports.syncProgress();
|
|
26
|
+
this.#ports.terminal.requestRender();
|
|
27
|
+
}
|
|
28
|
+
/** Answers an engine prompt through the open dialog; null when no login is in progress or it is cancelled. */
|
|
29
|
+
requestInput(request) {
|
|
30
|
+
const dialog = this.#dialog;
|
|
31
|
+
if (!dialog)
|
|
32
|
+
return Promise.resolve(null);
|
|
33
|
+
if (request.type === "select") {
|
|
34
|
+
return new Promise(resolve => {
|
|
35
|
+
const options = request.options ?? [];
|
|
36
|
+
const labels = options.map(option => option.label);
|
|
37
|
+
const restoreDialog = () => {
|
|
38
|
+
if (this.#dialog === dialog)
|
|
39
|
+
this.#ports.root.setInputSurface(dialog);
|
|
40
|
+
this.#ports.terminal.requestRender();
|
|
41
|
+
};
|
|
42
|
+
const selector = createPiShellExtensionSelector(request.message, labels, label => {
|
|
43
|
+
const id = options.find(option => option.label === label)?.id;
|
|
44
|
+
restoreDialog();
|
|
45
|
+
resolve(id ?? null);
|
|
46
|
+
}, () => {
|
|
47
|
+
restoreDialog();
|
|
48
|
+
resolve(null);
|
|
49
|
+
});
|
|
50
|
+
this.#ports.root.setInputSurface(selector, false);
|
|
51
|
+
this.#ports.terminal.requestRender();
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
const response = request.type === "manual-code"
|
|
55
|
+
? dialog.showManualInput(request.message)
|
|
56
|
+
: dialog.showPrompt(request.message, request.placeholder);
|
|
57
|
+
this.#ports.terminal.requestRender();
|
|
58
|
+
return response.then(value => value, () => null);
|
|
59
|
+
}
|
|
60
|
+
notify(event) {
|
|
61
|
+
const dialog = this.#dialog;
|
|
62
|
+
if (!dialog)
|
|
63
|
+
return;
|
|
64
|
+
if (event.type === "auth_url")
|
|
65
|
+
dialog.showAuth(event.url, event.instructions);
|
|
66
|
+
else if (event.type === "device_code") {
|
|
67
|
+
dialog.showDeviceCode(event);
|
|
68
|
+
dialog.showWaiting("Waiting for authentication...");
|
|
69
|
+
}
|
|
70
|
+
else if (event.type === "info")
|
|
71
|
+
dialog.showInfo(event.message, event.links);
|
|
72
|
+
else if (event.type === "waiting")
|
|
73
|
+
dialog.showWaiting(event.message);
|
|
74
|
+
else
|
|
75
|
+
dialog.showProgress(event.message);
|
|
76
|
+
this.#ports.terminal.requestRender();
|
|
77
|
+
}
|
|
78
|
+
finish() {
|
|
79
|
+
if (!this.#dialog)
|
|
80
|
+
return;
|
|
81
|
+
this.#dialog = undefined;
|
|
82
|
+
this.#ports.root.setInputSurface(null);
|
|
83
|
+
this.#ports.programStatus.setBlocked("authentication", undefined);
|
|
84
|
+
this.#ports.syncProgress();
|
|
85
|
+
this.#ports.terminal.requestRender();
|
|
86
|
+
}
|
|
87
|
+
/** A replaced session forgets its dialog; the presenter clears the input surface as part of the same replacement. */
|
|
88
|
+
onSessionReplaced() {
|
|
89
|
+
this.#dialog = undefined;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import type { OwnedUiCommand, OwnedUiCommandResult, OwnedUiSessionBackend, OwnedUiSessionViewModel, PromptHistoryImageSidecarPort } from "../../contracts/owned-ui/index.js";
|
|
2
|
+
import type { PromptHistoryController } from "./prompt-history-controller.js";
|
|
3
|
+
import type { ResidentTabCoordinator } from "./resident-tab-coordinator.js";
|
|
4
|
+
import type { SessionConcern } from "./session-concern.js";
|
|
5
|
+
import type { OwnedUiSessionShellRoot } from "./session-shell-root.js";
|
|
6
|
+
import type { OwnedUiPresenterTerminal } from "./shell-host.js";
|
|
7
|
+
export interface PromptSubmissionPorts {
|
|
8
|
+
readonly backend: Pick<OwnedUiSessionBackend, "session" | "identity" | "workflows">;
|
|
9
|
+
readonly root: Pick<OwnedUiSessionShellRoot, "editor" | "hasPendingPastes" | "waitForPromptPastes" | "setExtensionWidget" | "prepareHistoryText" | "imageChipAttachmentsFromEditor" | "preparePromptSubmission" | "omitUnsentPromptImages" | "resumeViewportFollowing" | "beginImageSubmissionStatus" | "appendWorkflowResult">;
|
|
10
|
+
readonly terminal: Pick<OwnedUiPresenterTerminal, "requestRender">;
|
|
11
|
+
readonly residentTab: ResidentTabCoordinator;
|
|
12
|
+
readonly cwd: string;
|
|
13
|
+
promptHistory(): PromptHistoryController | null;
|
|
14
|
+
imageSidecar(): PromptHistoryImageSidecarPort | undefined;
|
|
15
|
+
disposed(): boolean;
|
|
16
|
+
correlation(prefix: string): string;
|
|
17
|
+
lifecycle(): OwnedUiSessionViewModel["lifecycle"];
|
|
18
|
+
currentSessionFile(): string | null;
|
|
19
|
+
/** Slash input the presenter routes itself: owned routes, workflows, and skills. */
|
|
20
|
+
slashCommand(text: string): Promise<OwnedUiCommandResult>;
|
|
21
|
+
/** A prompt is about to be submitted; a pending suggestion for the old draft no longer applies. */
|
|
22
|
+
invalidateSuggestions(): void;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Turns editor input into engine commands: prompts, steering, follow-ups, and bash, each recorded
|
|
26
|
+
* in history first. It holds a submission whose image pastes are still preparing, journals prompts
|
|
27
|
+
* for resident tabs, and recovers the draft when a submission is rejected.
|
|
28
|
+
*/
|
|
29
|
+
export declare class PromptSubmission implements SessionConcern {
|
|
30
|
+
#private;
|
|
31
|
+
constructor(ports: PromptSubmissionPorts);
|
|
32
|
+
/** True while a submission waits for its image pastes. */
|
|
33
|
+
get waiting(): boolean;
|
|
34
|
+
/** The editor changed; a later recovery never overwrites input typed after its submission. */
|
|
35
|
+
noteEditorChange(): void;
|
|
36
|
+
submit(text: string): Promise<OwnedUiCommandResult>;
|
|
37
|
+
queueFollowUp(draft: string): Promise<OwnedUiCommandResult>;
|
|
38
|
+
cancelWaiting(): void;
|
|
39
|
+
/** Puts waiting submissions and the engine's queued input back into the editor. */
|
|
40
|
+
restoreQueued(): void;
|
|
41
|
+
/** Sends slash text the shell does not own as engine prompt input; history keeps what was typed. */
|
|
42
|
+
sendSlashPrompt(text: string, typed?: string): Promise<OwnedUiCommandResult>;
|
|
43
|
+
/** Executes a command; with a draft, a rejected or failed submission puts the draft back for recovery. */
|
|
44
|
+
execute(command: OwnedUiCommand, draft?: string): Promise<OwnedUiCommandResult>;
|
|
45
|
+
reportError(error?: unknown, message?: string): void;
|
|
46
|
+
onSessionReplaced(): void;
|
|
47
|
+
dispose(): void;
|
|
48
|
+
}
|
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
import { assertOwnedUiCommand, assertPromptImages, ImageAttachmentError, } from "../../contracts/owned-ui/index.js";
|
|
2
|
+
import { renderPiShellStatusText } from "../../integrations/pi/components/shell-presenters-info.js";
|
|
3
|
+
const rejected = (diagnostic) => ({ outcome: "rejected", diagnostic });
|
|
4
|
+
/**
|
|
5
|
+
* Turns editor input into engine commands: prompts, steering, follow-ups, and bash, each recorded
|
|
6
|
+
* in history first. It holds a submission whose image pastes are still preparing, journals prompts
|
|
7
|
+
* for resident tabs, and recovers the draft when a submission is rejected.
|
|
8
|
+
*/
|
|
9
|
+
export class PromptSubmission {
|
|
10
|
+
#ports;
|
|
11
|
+
#waiting = new Map();
|
|
12
|
+
#editorRevision = 0;
|
|
13
|
+
constructor(ports) {
|
|
14
|
+
this.#ports = ports;
|
|
15
|
+
}
|
|
16
|
+
/** True while a submission waits for its image pastes. */
|
|
17
|
+
get waiting() {
|
|
18
|
+
return this.#waiting.size > 0;
|
|
19
|
+
}
|
|
20
|
+
/** The editor changed; a later recovery never overwrites input typed after its submission. */
|
|
21
|
+
noteEditorChange() {
|
|
22
|
+
this.#editorRevision++;
|
|
23
|
+
}
|
|
24
|
+
submit(text) {
|
|
25
|
+
return this.#whenReady(text, () => this.#submit(text));
|
|
26
|
+
}
|
|
27
|
+
queueFollowUp(draft) {
|
|
28
|
+
return this.#whenReady(draft, () => this.#queueFollowUp(draft));
|
|
29
|
+
}
|
|
30
|
+
cancelWaiting() {
|
|
31
|
+
for (const item of this.#waiting.values())
|
|
32
|
+
item.controller.abort();
|
|
33
|
+
}
|
|
34
|
+
/** Puts waiting submissions and the engine's queued input back into the editor. */
|
|
35
|
+
restoreQueued() {
|
|
36
|
+
const queued = [...this.#waiting.keys(), ...this.#ports.backend.workflows.clearQueuedWorkflows()];
|
|
37
|
+
this.cancelWaiting();
|
|
38
|
+
if (queued.length === 0)
|
|
39
|
+
return;
|
|
40
|
+
this.#ports.root.editor.setText(queued.join("\n"));
|
|
41
|
+
this.#ports.terminal.requestRender();
|
|
42
|
+
}
|
|
43
|
+
/** Sends slash text the shell does not own as engine prompt input; history keeps what was typed. */
|
|
44
|
+
sendSlashPrompt(text, typed = text) {
|
|
45
|
+
this.#rememberInput(typed, "slash");
|
|
46
|
+
this.#ports.root.resumeViewportFollowing();
|
|
47
|
+
return this.execute({
|
|
48
|
+
type: this.#ports.lifecycle() === "busy" ? "steer" : "prompt",
|
|
49
|
+
correlationId: this.#ports.correlation("prompt-command"),
|
|
50
|
+
sessionId: this.#ports.backend.identity.sessionId,
|
|
51
|
+
text,
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
/** Executes a command; with a draft, a rejected or failed submission puts the draft back for recovery. */
|
|
55
|
+
async execute(command, draft) {
|
|
56
|
+
const { backend } = this.#ports;
|
|
57
|
+
if (draft === undefined)
|
|
58
|
+
return backend.session.execute(command);
|
|
59
|
+
const revision = this.#editorRevision;
|
|
60
|
+
try {
|
|
61
|
+
assertOwnedUiCommand(command);
|
|
62
|
+
}
|
|
63
|
+
catch (error) {
|
|
64
|
+
return this.#recover(draft, revision, error);
|
|
65
|
+
}
|
|
66
|
+
try {
|
|
67
|
+
const result = await backend.session.execute(command);
|
|
68
|
+
if (result.outcome === "rejected")
|
|
69
|
+
return this.#recover(draft, revision);
|
|
70
|
+
return result;
|
|
71
|
+
}
|
|
72
|
+
catch {
|
|
73
|
+
// Security: dispatch might already have reached the provider. Never retry automatically.
|
|
74
|
+
this.#ports.root.editor.addToHistory(draft);
|
|
75
|
+
this.#ports.promptHistory()?.rememberRecovery(draft);
|
|
76
|
+
this.reportError(undefined, "Submission failed; delivery is uncertain. Check the conversation before retrying. Press Up to recover the draft.");
|
|
77
|
+
return { outcome: "failed", diagnostic: "submission delivery is uncertain" };
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
reportError(error, message) {
|
|
81
|
+
// Security: arbitrary provider/extension error messages can contain the entire request.
|
|
82
|
+
try {
|
|
83
|
+
this.#ports.root.appendWorkflowResult({ command: "debug", outcome: "failed", message: message
|
|
84
|
+
?? (error instanceof ImageAttachmentError ? error.message : "Submission failed. Check the prompt and try again.") }, error instanceof ImageAttachmentError ? error.code : undefined);
|
|
85
|
+
this.#ports.terminal.requestRender();
|
|
86
|
+
}
|
|
87
|
+
catch { /* Security: error presentation cannot create another rejected submission callback. */ }
|
|
88
|
+
}
|
|
89
|
+
onSessionReplaced() {
|
|
90
|
+
this.cancelWaiting();
|
|
91
|
+
}
|
|
92
|
+
dispose() {
|
|
93
|
+
this.cancelWaiting();
|
|
94
|
+
}
|
|
95
|
+
#whenReady(draft, action) {
|
|
96
|
+
const { root, backend } = this.#ports;
|
|
97
|
+
const previous = this.#waiting.get(draft);
|
|
98
|
+
if (previous !== undefined)
|
|
99
|
+
return previous.result;
|
|
100
|
+
if (!root.hasPendingPastes(draft))
|
|
101
|
+
return this.#guard(draft, action);
|
|
102
|
+
const controller = new AbortController();
|
|
103
|
+
const generation = backend.identity.sessionGeneration;
|
|
104
|
+
const result = this.#guard(draft, async () => {
|
|
105
|
+
await root.waitForPromptPastes(draft, controller.signal);
|
|
106
|
+
if (controller.signal.aborted || this.#ports.disposed() || backend.identity.sessionGeneration !== generation)
|
|
107
|
+
return rejected("image submission canceled");
|
|
108
|
+
return action();
|
|
109
|
+
}).finally(() => {
|
|
110
|
+
this.#waiting.delete(draft);
|
|
111
|
+
if (!this.#ports.disposed())
|
|
112
|
+
this.#showWaiting();
|
|
113
|
+
});
|
|
114
|
+
this.#waiting.set(draft, { controller, result });
|
|
115
|
+
if (root.editor.getText() === draft)
|
|
116
|
+
root.editor.setText("");
|
|
117
|
+
this.#showWaiting();
|
|
118
|
+
return result;
|
|
119
|
+
}
|
|
120
|
+
#showWaiting() {
|
|
121
|
+
const count = this.#waiting.size;
|
|
122
|
+
this.#ports.root.setExtensionWidget("owned-image-preparation", count === 0 ? null : {
|
|
123
|
+
render: width => [...renderPiShellStatusText(`Waiting for images (${count} submission${count === 1 ? "" : "s"}) — Esc cancels; dequeue restores`, width)],
|
|
124
|
+
invalidate: () => { },
|
|
125
|
+
}, "aboveEditor");
|
|
126
|
+
this.#ports.terminal.requestRender();
|
|
127
|
+
}
|
|
128
|
+
async #guard(draft, action) {
|
|
129
|
+
const revision = this.#editorRevision;
|
|
130
|
+
try {
|
|
131
|
+
return await action();
|
|
132
|
+
}
|
|
133
|
+
catch (error) {
|
|
134
|
+
if (error instanceof ImageAttachmentError && error.code === "image-canceled")
|
|
135
|
+
return rejected("image submission canceled");
|
|
136
|
+
return this.#recover(draft, revision, error);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
#recover(draft, revision, error) {
|
|
140
|
+
const { root } = this.#ports;
|
|
141
|
+
root.editor.addToHistory(draft);
|
|
142
|
+
this.#ports.promptHistory()?.rememberRecovery(draft);
|
|
143
|
+
// Concurrency: never overwrite input typed (even typed and cleared) after this submission.
|
|
144
|
+
if (revision === this.#editorRevision && root.editor.getText().length === 0)
|
|
145
|
+
root.editor.setText(draft);
|
|
146
|
+
const message = error instanceof ImageAttachmentError ? error.message : "Submission rejected. Check the prompt and attachments.";
|
|
147
|
+
this.reportError(error, error instanceof ImageAttachmentError && error.code === "image-count"
|
|
148
|
+
? message
|
|
149
|
+
: `${message} Press Up to recover the draft.`);
|
|
150
|
+
return rejected(message);
|
|
151
|
+
}
|
|
152
|
+
#rememberInput(text, kind) {
|
|
153
|
+
const { root } = this.#ports;
|
|
154
|
+
root.editor.addToHistory(text);
|
|
155
|
+
const history = this.#ports.promptHistory();
|
|
156
|
+
if (history === null)
|
|
157
|
+
return;
|
|
158
|
+
const reusable = root.prepareHistoryText(text);
|
|
159
|
+
if (reusable.length === 0) {
|
|
160
|
+
history.rememberRecovery(text);
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
// Rationale: write the image chip payloads referenced by an editor draft to the sidecar
|
|
164
|
+
// directory before their history row commits. Missing sidecar dependency = silent skip; the
|
|
165
|
+
// recall path treats an absent sidecar the same as a corrupt one.
|
|
166
|
+
const sidecar = this.#ports.imageSidecar();
|
|
167
|
+
if (sidecar !== undefined) {
|
|
168
|
+
for (const record of root.imageChipAttachmentsFromEditor(text)) {
|
|
169
|
+
sidecar.write(record.id, { tag: record.tag, data: record.image.data, mimeType: record.image.mimeType, savedAt: new Date().toISOString() });
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
history.capture(reusable, kind, this.#ports.cwd, this.#ports.backend.identity.sessionId);
|
|
173
|
+
}
|
|
174
|
+
async #submit(text) {
|
|
175
|
+
this.#ports.invalidateSuggestions();
|
|
176
|
+
const displayInput = text.trim();
|
|
177
|
+
if (!displayInput)
|
|
178
|
+
return { outcome: "completed", diagnostic: null };
|
|
179
|
+
if (displayInput.startsWith("/"))
|
|
180
|
+
return this.#ports.slashCommand(displayInput);
|
|
181
|
+
const { root, backend } = this.#ports;
|
|
182
|
+
const prepared = root.preparePromptSubmission(displayInput);
|
|
183
|
+
assertPromptImages(prepared.images);
|
|
184
|
+
const input = prepared.text.trim();
|
|
185
|
+
if (input.startsWith("!")) {
|
|
186
|
+
const bash = await this.#bash(input, displayInput);
|
|
187
|
+
if (bash !== undefined)
|
|
188
|
+
return bash;
|
|
189
|
+
}
|
|
190
|
+
// Compatibility: match interactive Pi: input during compaction is queued steering; the engine
|
|
191
|
+
// shows it in the pending rows and delivers it when compaction ends.
|
|
192
|
+
const type = this.#ports.lifecycle() === "busy" ? "steer" : "prompt";
|
|
193
|
+
this.#rememberInput(root.omitUnsentPromptImages(displayInput).trim(), type);
|
|
194
|
+
root.resumeViewportFollowing();
|
|
195
|
+
const command = {
|
|
196
|
+
type,
|
|
197
|
+
correlationId: this.#ports.correlation(type),
|
|
198
|
+
sessionId: backend.identity.sessionId,
|
|
199
|
+
text: input,
|
|
200
|
+
...(prepared.images.length === 0 ? {} : { images: prepared.images }),
|
|
201
|
+
};
|
|
202
|
+
let submission;
|
|
203
|
+
try {
|
|
204
|
+
submission = await this.#ports.residentTab.admit({
|
|
205
|
+
sessionId: command.sessionId,
|
|
206
|
+
sessionFile: this.#ports.currentSessionFile(),
|
|
207
|
+
kind: type,
|
|
208
|
+
text: input,
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
catch {
|
|
212
|
+
if (root.editor.getText().length === 0)
|
|
213
|
+
root.editor.setText(displayInput);
|
|
214
|
+
const message = "Prompt was not sent because its recovery journal could not be synchronized.";
|
|
215
|
+
this.reportError(undefined, message);
|
|
216
|
+
return rejected(message);
|
|
217
|
+
}
|
|
218
|
+
const finishSending = prepared.images.length === 0 ? () => { } : root.beginImageSubmissionStatus();
|
|
219
|
+
try {
|
|
220
|
+
const result = await this.execute(command, displayInput);
|
|
221
|
+
if (submission !== undefined && result.outcome !== "rejected") {
|
|
222
|
+
await this.#ports.residentTab.dispatched(submission.id, command.correlationId);
|
|
223
|
+
}
|
|
224
|
+
return result;
|
|
225
|
+
}
|
|
226
|
+
finally {
|
|
227
|
+
finishSending();
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
// Protocol: `!command` runs bash with its output in context, `!!command` keeps it out; an empty command is prompt text.
|
|
231
|
+
async #bash(input, displayInput) {
|
|
232
|
+
const excludeFromContext = input.startsWith("!!");
|
|
233
|
+
const command = input.slice(excludeFromContext ? 2 : 1).trim();
|
|
234
|
+
if (!command)
|
|
235
|
+
return undefined;
|
|
236
|
+
this.#rememberInput(displayInput, "bash");
|
|
237
|
+
try {
|
|
238
|
+
const result = await this.#ports.backend.workflows.executeBashWorkflow(command, excludeFromContext);
|
|
239
|
+
const workflow = {
|
|
240
|
+
command: "debug",
|
|
241
|
+
outcome: result.cancelled ? "cancelled" : result.exitCode === 0 || result.exitCode === undefined ? "completed" : "failed",
|
|
242
|
+
message: result.cancelled ? "Bash command cancelled" : `Bash exited ${result.exitCode ?? 0}: ${command}`,
|
|
243
|
+
...(result.output ? { detail: result.output } : {}),
|
|
244
|
+
};
|
|
245
|
+
this.#ports.root.appendWorkflowResult(workflow);
|
|
246
|
+
this.#ports.terminal.requestRender();
|
|
247
|
+
return workflow.outcome === "failed" ? rejected(workflow.message) : { outcome: "completed", diagnostic: null };
|
|
248
|
+
}
|
|
249
|
+
catch (error) {
|
|
250
|
+
const message = `Bash command failed: ${error instanceof Error ? error.message : String(error)}`;
|
|
251
|
+
this.#ports.root.appendWorkflowResult({ command: "debug", outcome: "failed", message });
|
|
252
|
+
this.#ports.terminal.requestRender();
|
|
253
|
+
return rejected(message);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
async #queueFollowUp(draft) {
|
|
257
|
+
const { root, backend } = this.#ports;
|
|
258
|
+
const displayInput = draft.trim();
|
|
259
|
+
if (!displayInput)
|
|
260
|
+
return rejected("nothing to queue");
|
|
261
|
+
const prepared = root.preparePromptSubmission(displayInput);
|
|
262
|
+
assertPromptImages(prepared.images);
|
|
263
|
+
const text = prepared.text.trim();
|
|
264
|
+
this.#rememberInput(displayInput, "follow-up");
|
|
265
|
+
if (root.editor.getText() === draft)
|
|
266
|
+
root.editor.setText("");
|
|
267
|
+
root.resumeViewportFollowing();
|
|
268
|
+
return this.execute({
|
|
269
|
+
type: "follow-up",
|
|
270
|
+
correlationId: this.#ports.correlation("follow-up"),
|
|
271
|
+
sessionId: backend.identity.sessionId,
|
|
272
|
+
text,
|
|
273
|
+
...(prepared.images.length === 0 ? {} : { images: prepared.images }),
|
|
274
|
+
}, displayInput);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { OwnedUiEvent, PromptHistoryImageSidecarPort } from "../../contracts/owned-ui/index.js";
|
|
2
|
+
import type { SessionConcern } from "./session-concern.js";
|
|
3
|
+
import type { OwnedUiShellEngineOptions, OwnedUiSessionShellRoot } from "./session-shell-root.js";
|
|
4
|
+
import type { OwnedUiPresenterTerminal } from "./shell-host.js";
|
|
5
|
+
export type ResidentTabOptions = NonNullable<OwnedUiShellEngineOptions["residentTab"]>;
|
|
6
|
+
export interface ResidentTabCoordinatorPorts {
|
|
7
|
+
readonly root: Pick<OwnedUiSessionShellRoot, "appendWorkflowStatus" | "rehydrateHistoryText" | "editor">;
|
|
8
|
+
readonly terminal: Pick<OwnedUiPresenterTerminal, "requestRender">;
|
|
9
|
+
readonly disposed: () => boolean;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* The session presenter's side of a resident tab: recovered input, the progress and stall status
|
|
13
|
+
* it reports to the strip, prompt admission and correlation through the recovery journal, draft
|
|
14
|
+
* checkpoints, detach on quit, and the graceful stop when the presenter is released. Outside a
|
|
15
|
+
* resident tab every operation is a no-op.
|
|
16
|
+
*/
|
|
17
|
+
export declare class ResidentTabCoordinator implements SessionConcern {
|
|
18
|
+
#private;
|
|
19
|
+
constructor(options: ResidentTabOptions | undefined, ports: ResidentTabCoordinatorPorts);
|
|
20
|
+
/** True inside a resident tab, where Settings and the session name live in the global strip. */
|
|
21
|
+
get active(): boolean;
|
|
22
|
+
checkpoint(text: string): void;
|
|
23
|
+
closeOwnedRoute(route: string): void;
|
|
24
|
+
/** Puts the prompts and draft a previous tab process left unfinished back into the editor. */
|
|
25
|
+
restoreInput(imageSidecar?: PromptHistoryImageSidecarPort): void;
|
|
26
|
+
/** Tracks working, idle, and stalled status from session events and arms the stall notice. */
|
|
27
|
+
observeProgress(event: OwnedUiEvent): void;
|
|
28
|
+
/** Journals a prompt before it is sent; undefined outside a resident tab. Rejects when the journal cannot sync. */
|
|
29
|
+
admit(input: Parameters<NonNullable<ResidentTabOptions["admit"]>>[0]): Promise<{
|
|
30
|
+
readonly id: string;
|
|
31
|
+
} | undefined>;
|
|
32
|
+
/** The admitted prompt reached the engine; its journal entry is correlated with the engine's entry. */
|
|
33
|
+
dispatched(submissionId: string, entryId: string): Promise<void>;
|
|
34
|
+
/**
|
|
35
|
+
* Leaves the tab without stopping its engine. Returns undefined outside a resident tab, true when
|
|
36
|
+
* the client detached, or the notice to show when detaching is unavailable.
|
|
37
|
+
*/
|
|
38
|
+
detach(): Promise<true | string | undefined>;
|
|
39
|
+
gracefulStop(sessionFile: string | null): Promise<void>;
|
|
40
|
+
dispose(): void;
|
|
41
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
const PROGRESS_EVENTS = new Set([
|
|
2
|
+
"transcript-block", "session-view", "assistant-message-completed", "agent-run-started",
|
|
3
|
+
]);
|
|
4
|
+
/**
|
|
5
|
+
* The session presenter's side of a resident tab: recovered input, the progress and stall status
|
|
6
|
+
* it reports to the strip, prompt admission and correlation through the recovery journal, draft
|
|
7
|
+
* checkpoints, detach on quit, and the graceful stop when the presenter is released. Outside a
|
|
8
|
+
* resident tab every operation is a no-op.
|
|
9
|
+
*/
|
|
10
|
+
export class ResidentTabCoordinator {
|
|
11
|
+
#options;
|
|
12
|
+
#ports;
|
|
13
|
+
#progressTimer;
|
|
14
|
+
#working = false;
|
|
15
|
+
#stalled = false;
|
|
16
|
+
constructor(options, ports) {
|
|
17
|
+
this.#options = options;
|
|
18
|
+
this.#ports = ports;
|
|
19
|
+
}
|
|
20
|
+
/** True inside a resident tab, where Settings and the session name live in the global strip. */
|
|
21
|
+
get active() {
|
|
22
|
+
return this.#options !== undefined;
|
|
23
|
+
}
|
|
24
|
+
checkpoint(text) {
|
|
25
|
+
this.#options?.checkpoint?.(text);
|
|
26
|
+
}
|
|
27
|
+
closeOwnedRoute(route) {
|
|
28
|
+
this.#options?.closeOwnedRoute?.(route);
|
|
29
|
+
}
|
|
30
|
+
/** Puts the prompts and draft a previous tab process left unfinished back into the editor. */
|
|
31
|
+
restoreInput(imageSidecar) {
|
|
32
|
+
const recovered = this.#options?.recovered;
|
|
33
|
+
if (recovered === undefined)
|
|
34
|
+
return;
|
|
35
|
+
const { root } = this.#ports;
|
|
36
|
+
const prompts = recovered.prompts
|
|
37
|
+
.map(prompt => root.rehydrateHistoryText(prompt.text, id => imageSidecar?.readAttachment(id) ?? null))
|
|
38
|
+
.filter(Boolean);
|
|
39
|
+
if (prompts.length > 0) {
|
|
40
|
+
root.editor.setText(prompts[0]);
|
|
41
|
+
if (recovered.draft)
|
|
42
|
+
root.editor.addToHistory(recovered.draft);
|
|
43
|
+
for (const additional of prompts.slice(1))
|
|
44
|
+
root.editor.addToHistory(additional);
|
|
45
|
+
root.appendWorkflowStatus(recovered.draft
|
|
46
|
+
? `Recovered an unfinished prompt; the prior draft and ${Math.max(0, prompts.length - 1)} additional prompt${prompts.length === 2 ? "" : "s"} are in editor history.`
|
|
47
|
+
: `Recovered an unfinished prompt${prompts.length > 1 ? `; ${prompts.length - 1} additional prompts are in editor history` : ""}.`);
|
|
48
|
+
}
|
|
49
|
+
else if (recovered.draft) {
|
|
50
|
+
root.editor.setText(recovered.draft);
|
|
51
|
+
root.appendWorkflowStatus("Recovered the editor draft from the previous tab process.");
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
/** Tracks working, idle, and stalled status from session events and arms the stall notice. */
|
|
55
|
+
observeProgress(event) {
|
|
56
|
+
const resident = this.#options;
|
|
57
|
+
const seconds = resident?.stallNoticeSeconds;
|
|
58
|
+
if (resident === undefined || seconds === undefined)
|
|
59
|
+
return;
|
|
60
|
+
if (event.type === "session-lifecycle") {
|
|
61
|
+
this.#working = event.lifecycle === "busy";
|
|
62
|
+
if (!this.#working) {
|
|
63
|
+
this.#clearTimer();
|
|
64
|
+
this.#stalled = false;
|
|
65
|
+
resident.reportStatus?.("idle");
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
else if (!this.#working || !PROGRESS_EVENTS.has(event.type)) {
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
if (this.#stalled) {
|
|
73
|
+
this.#stalled = false;
|
|
74
|
+
resident.reportStatus?.("working");
|
|
75
|
+
}
|
|
76
|
+
this.#clearTimer();
|
|
77
|
+
this.#progressTimer = setTimeout(() => {
|
|
78
|
+
this.#progressTimer = undefined;
|
|
79
|
+
if (this.#ports.disposed() || !this.#working)
|
|
80
|
+
return;
|
|
81
|
+
this.#stalled = true;
|
|
82
|
+
resident.reportStatus?.("stalled");
|
|
83
|
+
const minutes = Math.max(1, Math.round(seconds / 60));
|
|
84
|
+
this.#ports.root.appendWorkflowStatus(`No progress for ${minutes} min — Esc interrupts · use the tab restart action to restart`);
|
|
85
|
+
this.#ports.terminal.requestRender();
|
|
86
|
+
}, seconds * 1_000);
|
|
87
|
+
this.#progressTimer.unref?.();
|
|
88
|
+
}
|
|
89
|
+
/** Journals a prompt before it is sent; undefined outside a resident tab. Rejects when the journal cannot sync. */
|
|
90
|
+
admit(input) {
|
|
91
|
+
return this.#options?.admit?.(input) ?? Promise.resolve(undefined);
|
|
92
|
+
}
|
|
93
|
+
/** The admitted prompt reached the engine; its journal entry is correlated with the engine's entry. */
|
|
94
|
+
async dispatched(submissionId, entryId) {
|
|
95
|
+
this.#options?.dispatched?.();
|
|
96
|
+
await this.#options?.correlated?.(submissionId, entryId).catch(() => undefined);
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Leaves the tab without stopping its engine. Returns undefined outside a resident tab, true when
|
|
100
|
+
* the client detached, or the notice to show when detaching is unavailable.
|
|
101
|
+
*/
|
|
102
|
+
async detach() {
|
|
103
|
+
const resident = this.#options;
|
|
104
|
+
if (resident === undefined)
|
|
105
|
+
return undefined;
|
|
106
|
+
const detached = await resident.requestDetach().catch(() => false);
|
|
107
|
+
return detached ? true : resident.unavailableNotice ?? "Press Ctrl+C twice to leave; this agent keeps running.";
|
|
108
|
+
}
|
|
109
|
+
gracefulStop(sessionFile) {
|
|
110
|
+
return this.#options?.gracefulStop?.(sessionFile) ?? Promise.resolve();
|
|
111
|
+
}
|
|
112
|
+
dispose() {
|
|
113
|
+
this.#clearTimer();
|
|
114
|
+
}
|
|
115
|
+
#clearTimer() {
|
|
116
|
+
if (this.#progressTimer !== undefined)
|
|
117
|
+
clearTimeout(this.#progressTimer);
|
|
118
|
+
this.#progressTimer = undefined;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One piece of a session presenter's state that resets itself when the backend replaces the
|
|
3
|
+
* session and releases itself when the presenter is released.
|
|
4
|
+
*/
|
|
5
|
+
export interface SessionConcern {
|
|
6
|
+
/** The backend replaced the session: drop the pending work, timers, and surfaces that belonged to it. */
|
|
7
|
+
onSessionReplaced?(): void;
|
|
8
|
+
/** Release everything this concern holds; called at most once. */
|
|
9
|
+
dispose?(): void;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* The presenter's concerns in their declared order. Session replacement and disposal fan out in
|
|
13
|
+
* that order, because later resets depend on earlier ones (copy intent before pastes before the
|
|
14
|
+
* input surface), and disposal is idempotent.
|
|
15
|
+
*/
|
|
16
|
+
export declare class SessionConcerns {
|
|
17
|
+
#private;
|
|
18
|
+
constructor(concerns: readonly SessionConcern[]);
|
|
19
|
+
replaceSession(): void;
|
|
20
|
+
/** Disposes each concern once, through `attempt` so one failure does not skip the rest. */
|
|
21
|
+
dispose(attempt?: (action: () => void) => void): void;
|
|
22
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The presenter's concerns in their declared order. Session replacement and disposal fan out in
|
|
3
|
+
* that order, because later resets depend on earlier ones (copy intent before pastes before the
|
|
4
|
+
* input surface), and disposal is idempotent.
|
|
5
|
+
*/
|
|
6
|
+
export class SessionConcerns {
|
|
7
|
+
#concerns;
|
|
8
|
+
#disposed = false;
|
|
9
|
+
constructor(concerns) {
|
|
10
|
+
this.#concerns = concerns;
|
|
11
|
+
}
|
|
12
|
+
replaceSession() {
|
|
13
|
+
if (this.#disposed)
|
|
14
|
+
return;
|
|
15
|
+
for (const concern of this.#concerns)
|
|
16
|
+
concern.onSessionReplaced?.();
|
|
17
|
+
}
|
|
18
|
+
/** Disposes each concern once, through `attempt` so one failure does not skip the rest. */
|
|
19
|
+
dispose(attempt = action => action()) {
|
|
20
|
+
if (this.#disposed)
|
|
21
|
+
return;
|
|
22
|
+
this.#disposed = true;
|
|
23
|
+
for (const concern of this.#concerns) {
|
|
24
|
+
const dispose = concern.dispose;
|
|
25
|
+
if (dispose !== undefined)
|
|
26
|
+
attempt(() => dispose.call(concern));
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
export interface SessionResumeCommandMetadata {
|
|
2
|
+
readonly sessionId: string;
|
|
3
|
+
readonly sessionDir: string;
|
|
4
|
+
readonly usesDefaultSessionDir: boolean;
|
|
5
|
+
}
|
|
6
|
+
export declare function formatSessionResumeCommand(metadata: SessionResumeCommandMetadata): string;
|
|
7
|
+
export declare function quoteCommandArgument(value: string): string;
|
|
8
|
+
export interface ExitTextInput {
|
|
9
|
+
readonly terminalMode: "regular" | "fullscreen";
|
|
10
|
+
readonly customViewport: boolean;
|
|
11
|
+
readonly exitMode: "transcript" | "resume-hint";
|
|
12
|
+
readonly resume: SessionResumeCommandMetadata | null;
|
|
13
|
+
/** The styled transcript, rendered only when the pinned profile asks for it. */
|
|
14
|
+
readonly transcript: () => string;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* What a fullscreen leave prints into the parent terminal: nothing after a regular-screen session,
|
|
18
|
+
* only the resume hint for bare A1 or when configured, and otherwise the transcript then the hint.
|
|
19
|
+
*/
|
|
20
|
+
export declare function composeExitText(input: ExitTextInput): string;
|
|
21
|
+
/** Resolves once when the session stops, whether it stopped itself, was quit, or was disposed. */
|
|
22
|
+
export declare class SessionStopLatch {
|
|
23
|
+
#private;
|
|
24
|
+
readonly stopped: Promise<void>;
|
|
25
|
+
constructor();
|
|
26
|
+
resolve(): void;
|
|
27
|
+
/** The engine stopped on its own: dispose the shell, then resolve whether or not disposal failed. */
|
|
28
|
+
settleAfter(dispose: () => Promise<void>): void;
|
|
29
|
+
}
|