@north-light/crouter 0.3.258 → 0.3.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.
- package/dist/api/dto/attach.d.ts +6 -0
- package/dist/api/dto/nodes.d.ts +15 -0
- package/dist/clients/attach/command.js +3 -1
- package/dist/clients/attach/input/capabilities.d.ts +3 -0
- package/dist/clients/attach/input/capabilities.js +7 -3
- package/dist/clients/attach/input/controller.d.ts +1 -1
- package/dist/clients/attach/input/controller.js +31 -17
- package/dist/clients/attach/session/connection.d.ts +12 -3
- package/dist/clients/attach/session/connection.js +40 -25
- package/dist/clients/attach/session/input-wiring.d.ts +1 -1
- package/dist/clients/attach/session/input-wiring.js +6 -2
- package/dist/clients/attach/session/reconnect.d.ts +7 -3
- package/dist/clients/attach/session/reconnect.js +13 -2
- package/dist/clients/attach/slash/dispatch.js +1 -1
- package/dist/clients/attach/viewer.d.ts +1 -1
- package/dist/clients/attach/viewer.js +689 -689
- package/dist/commands/surface/node/cycle.js +2 -2
- package/dist/commands/surface/node/focus.js +11 -11
- package/dist/commands/surface/node/placement.js +14 -5
- package/dist/core/broker-client/client.d.ts +4 -5
- package/dist/core/broker-client/client.js +9 -5
- package/dist/core/canvas/browse/model.d.ts +3 -3
- package/dist/core/canvas/browse/model.js +3 -3
- package/dist/core/canvas/browse/render.js +3 -3
- package/dist/core/runtime/revive.d.ts +19 -6
- package/dist/core/runtime/revive.js +34 -9
- package/dist/daemon/api/handlers/attach.js +24 -20
- package/dist/daemon/api/handlers/nodes.js +7 -0
- package/package.json +1 -1
- package/runtime.lock.json +2 -2
package/dist/api/dto/attach.d.ts
CHANGED
|
@@ -5,6 +5,9 @@ export interface AttachEnsureRequest {
|
|
|
5
5
|
resume?: boolean;
|
|
6
6
|
/** Revive if dormant (default true); false returns a possibly-dead socket. */
|
|
7
7
|
revive?: boolean;
|
|
8
|
+
/** Wake an exact unfinalized parked resident (default true). Focus/navigation
|
|
9
|
+
* sets false so it can open the saved conversation without launching. */
|
|
10
|
+
wakeParked?: boolean;
|
|
8
11
|
}
|
|
9
12
|
/** Result of a local attach-ensure. */
|
|
10
13
|
export interface AttachEnsureResultDTO {
|
|
@@ -13,4 +16,7 @@ export interface AttachEnsureResultDTO {
|
|
|
13
16
|
socket_path: string;
|
|
14
17
|
revived: boolean;
|
|
15
18
|
resumed: boolean;
|
|
19
|
+
/** True when a non-waking focus ensure found an exact parked resident with no
|
|
20
|
+
* live broker. `socket_path` is then the target the viewer will later redial. */
|
|
21
|
+
parked: boolean;
|
|
16
22
|
}
|
package/dist/api/dto/nodes.d.ts
CHANGED
|
@@ -239,8 +239,23 @@ export interface NodeSnapshotDTO {
|
|
|
239
239
|
node_id: NodeIdDTO;
|
|
240
240
|
snapshot: {
|
|
241
241
|
messages: unknown[];
|
|
242
|
+
messageIds?: string[];
|
|
242
243
|
stats: unknown;
|
|
243
244
|
state: Record<string, unknown>;
|
|
245
|
+
display: {
|
|
246
|
+
statuses: Record<string, string>;
|
|
247
|
+
widgets: Record<string, {
|
|
248
|
+
lines: string[];
|
|
249
|
+
placement: 'aboveEditor' | 'belowEditor';
|
|
250
|
+
}>;
|
|
251
|
+
title?: string;
|
|
252
|
+
};
|
|
253
|
+
queued?: {
|
|
254
|
+
steering: string[];
|
|
255
|
+
followUp: string[];
|
|
256
|
+
};
|
|
257
|
+
toolGroupSummaries?: Record<string, unknown>;
|
|
258
|
+
workingActivity?: string;
|
|
244
259
|
};
|
|
245
260
|
commands: {
|
|
246
261
|
name: string;
|
|
@@ -60,6 +60,8 @@ function attachToLeaf(namespace) {
|
|
|
60
60
|
const nodeId = input['node'];
|
|
61
61
|
const observer = input['observer'] ?? false;
|
|
62
62
|
const canvasName = input['canvas'];
|
|
63
|
+
const wakeParked = process.env['CRTR_ATTACH_WAKE_PARKED'] !== '0';
|
|
64
|
+
delete process.env['CRTR_ATTACH_WAKE_PARKED'];
|
|
63
65
|
// attach IS the in-pane interactive program — it only needs a TTY (crtr is
|
|
64
66
|
// tmux-only, but the pane itself is the surface). Non-TTY (piped) → static
|
|
65
67
|
// notice + exit, never a non-tmux interactive fallback. Guarded BEFORE the
|
|
@@ -73,7 +75,7 @@ function attachToLeaf(namespace) {
|
|
|
73
75
|
mark('viewer.bundle_import_start');
|
|
74
76
|
const { runAttach } = await import('./viewer.js');
|
|
75
77
|
mark('viewer.bundle_loaded');
|
|
76
|
-
await runAttach(nodeId, observer, canvasName);
|
|
78
|
+
await runAttach(nodeId, observer, canvasName, wakeParked);
|
|
77
79
|
// The session has sent its final broker frame and restored the terminal.
|
|
78
80
|
// Force this one-shot viewer process out now: pi's bundled UI dependencies
|
|
79
81
|
// can retain libuv handles after their visible work is finished. The exit
|
|
@@ -16,6 +16,9 @@ export interface ViewerCapabilityInput {
|
|
|
16
16
|
remote: boolean;
|
|
17
17
|
nodeId: string;
|
|
18
18
|
profileId: string | undefined;
|
|
19
|
+
/** Dynamic transport state. Authorization stays role-based; broker-only
|
|
20
|
+
* operations refuse locally while a parked viewer has no live engine. */
|
|
21
|
+
isBrokerOffline?: () => boolean;
|
|
19
22
|
}
|
|
20
23
|
/** Build the one authority for a viewer session. Remote viewers are observers
|
|
21
24
|
* even when a caller supplied a controller role; keep that invariant local to
|
|
@@ -25,14 +25,18 @@ export function createViewerCapabilities(input) {
|
|
|
25
25
|
permittedOperations,
|
|
26
26
|
permits: (operation) => permittedOperations.has(operation),
|
|
27
27
|
refusal: (operation, command) => {
|
|
28
|
+
const prefix = command === undefined ? '' : `/${command} `;
|
|
29
|
+
if (input.isBrokerOffline?.() === true
|
|
30
|
+
&& (operation === 'engine-drive' || operation === 'dequeue' || operation === 'auth')) {
|
|
31
|
+
return `${prefix}needs a live broker; this node is parked — submit a message to wake it`;
|
|
32
|
+
}
|
|
28
33
|
if (permittedOperations.has(operation))
|
|
29
34
|
return undefined;
|
|
30
|
-
const prefix = command === undefined ? '' : `/${command} `;
|
|
31
35
|
if (remote && (operation === 'canvas-resume' || operation === 'local-canvas-inspect' || operation === 'canvas-inbox')) {
|
|
32
|
-
return `${prefix}
|
|
36
|
+
return `${prefix}Read-only remote attach — not available with --canvas because it acts on this machine's local canvas`;
|
|
33
37
|
}
|
|
34
38
|
if (remote)
|
|
35
|
-
return `${prefix}
|
|
39
|
+
return `${prefix}Read-only remote attach — not available with --canvas (view-only)`;
|
|
36
40
|
return `${prefix}is not available to a read-only observer attach (--observer) — it would drive this node`;
|
|
37
41
|
},
|
|
38
42
|
};
|
|
@@ -61,7 +61,7 @@ export declare class InputController {
|
|
|
61
61
|
openAuthPicker(prepare: Parameters<InputOverlayOwner['openAuthPicker']>[0]): Promise<void>;
|
|
62
62
|
private readonly send;
|
|
63
63
|
private emitCommand;
|
|
64
|
-
private
|
|
64
|
+
private openBrokerPicker;
|
|
65
65
|
private wire;
|
|
66
66
|
private slashContext;
|
|
67
67
|
/** Record a leading slash command as used. Called only once the command has
|
|
@@ -69,15 +69,21 @@ export class InputController {
|
|
|
69
69
|
};
|
|
70
70
|
emitCommand(frame) {
|
|
71
71
|
const operation = viewerOperationForFrame(frame);
|
|
72
|
-
|
|
72
|
+
const refusal = operation === undefined ? undefined : this.capabilities.refusal(operation);
|
|
73
|
+
if (refusal === undefined) {
|
|
73
74
|
this.hooks.onCommand(frame);
|
|
74
75
|
return true;
|
|
75
76
|
}
|
|
76
|
-
this.notify(
|
|
77
|
+
this.notify(refusal);
|
|
77
78
|
return false;
|
|
78
79
|
}
|
|
79
|
-
|
|
80
|
-
|
|
80
|
+
openBrokerPicker(open) {
|
|
81
|
+
const refusal = this.capabilities.refusal('engine-drive');
|
|
82
|
+
if (refusal !== undefined) {
|
|
83
|
+
this.notify(refusal);
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
open();
|
|
81
87
|
}
|
|
82
88
|
wire() {
|
|
83
89
|
this.editor.onSubmit = (text) => this.handleSubmit(text);
|
|
@@ -91,7 +97,7 @@ export class InputController {
|
|
|
91
97
|
const now = Date.now();
|
|
92
98
|
if (now - this.lastEscapeAt < DOUBLE_ESCAPE_MS) {
|
|
93
99
|
this.lastEscapeAt = 0;
|
|
94
|
-
void this.overlays.openTreePicker();
|
|
100
|
+
this.openBrokerPicker(() => void this.overlays.openTreePicker());
|
|
95
101
|
}
|
|
96
102
|
else {
|
|
97
103
|
this.lastEscapeAt = now;
|
|
@@ -109,10 +115,10 @@ export class InputController {
|
|
|
109
115
|
this.editor.onAction('app.tools.expand', () => this.hooks.onToggleToolsExpand?.());
|
|
110
116
|
this.editor.onAction('app.thinking.toggle', () => this.hooks.onToggleThinking?.());
|
|
111
117
|
this.editor.onAction('app.editor.external', () => void this.openExternalEditor());
|
|
112
|
-
this.editor.onAction('app.model.select', () => void this.overlays.openModelPicker());
|
|
113
|
-
this.editor.onAction('app.session.tree', () => void this.overlays.openTreePicker());
|
|
114
|
-
this.editor.onAction('app.session.fork', () => void this.overlays.openForkPicker());
|
|
115
|
-
this.editor.onAction('app.session.resume', () => void this.overlays.openSessionPicker());
|
|
118
|
+
this.editor.onAction('app.model.select', () => this.openBrokerPicker(() => void this.overlays.openModelPicker()));
|
|
119
|
+
this.editor.onAction('app.session.tree', () => this.openBrokerPicker(() => void this.overlays.openTreePicker()));
|
|
120
|
+
this.editor.onAction('app.session.fork', () => this.openBrokerPicker(() => void this.overlays.openForkPicker()));
|
|
121
|
+
this.editor.onAction('app.session.resume', () => this.openBrokerPicker(() => void this.overlays.openSessionPicker()));
|
|
116
122
|
}
|
|
117
123
|
slashContext() {
|
|
118
124
|
return {
|
|
@@ -131,12 +137,12 @@ export class InputController {
|
|
|
131
137
|
onColor: this.hooks.onColor,
|
|
132
138
|
onNodeMetadata: this.hooks.onNodeMetadata,
|
|
133
139
|
onFoldTools: this.hooks.onFoldTools,
|
|
134
|
-
openModelPicker: () => void this.overlays.openModelPicker(),
|
|
135
|
-
openSessionPicker: () => void this.overlays.openSessionPicker(),
|
|
136
|
-
openForkPicker: () => void this.overlays.openForkPicker(),
|
|
137
|
-
openTreePicker: () => void this.overlays.openTreePicker(),
|
|
138
|
-
openSettingsPicker: () => void this.overlays.openSettingsPicker(),
|
|
139
|
-
openScopedModelsPicker: () => void this.overlays.openScopedModelsPicker(),
|
|
140
|
+
openModelPicker: () => this.openBrokerPicker(() => void this.overlays.openModelPicker()),
|
|
141
|
+
openSessionPicker: () => this.openBrokerPicker(() => void this.overlays.openSessionPicker()),
|
|
142
|
+
openForkPicker: () => this.openBrokerPicker(() => void this.overlays.openForkPicker()),
|
|
143
|
+
openTreePicker: () => this.openBrokerPicker(() => void this.overlays.openTreePicker()),
|
|
144
|
+
openSettingsPicker: () => this.openBrokerPicker(() => void this.overlays.openSettingsPicker()),
|
|
145
|
+
openScopedModelsPicker: () => this.openBrokerPicker(() => void this.overlays.openScopedModelsPicker()),
|
|
140
146
|
openMcpPicker: (serverName) => this.overlays.openMcpPicker(serverName),
|
|
141
147
|
openLoginPicker: this.hooks.openLoginPicker,
|
|
142
148
|
openLogoutPicker: this.hooks.openLogoutPicker,
|
|
@@ -188,6 +194,13 @@ export class InputController {
|
|
|
188
194
|
return;
|
|
189
195
|
}
|
|
190
196
|
const isDormant = this.hooks.isDormant?.() === true;
|
|
197
|
+
// Unknown slash commands and `!` bash are broker operations, not human
|
|
198
|
+
// conversation input. Keep their text and refuse visibly while offline;
|
|
199
|
+
// only an ordinary submitted message crosses the durable wake boundary.
|
|
200
|
+
if (isDormant && (trimmed.startsWith('/') || trimmed.startsWith('!'))) {
|
|
201
|
+
this.notify('This node is parked — submit a message to wake it; broker commands need a live engine');
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
191
204
|
const classification = classifySubmit(text, {
|
|
192
205
|
isDormant,
|
|
193
206
|
canWakeDormant: isDormant && this.capabilities.permits('wake-dormant') && this.hooks.onDormantSubmit !== undefined,
|
|
@@ -240,8 +253,9 @@ export class InputController {
|
|
|
240
253
|
this.clearPastedImages();
|
|
241
254
|
}
|
|
242
255
|
async handleDequeue() {
|
|
243
|
-
|
|
244
|
-
|
|
256
|
+
const refusal = this.capabilities.refusal('dequeue');
|
|
257
|
+
if (refusal !== undefined) {
|
|
258
|
+
this.notify(refusal);
|
|
245
259
|
return;
|
|
246
260
|
}
|
|
247
261
|
const request = this.hooks.onRequest;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ClientRole } from '../../../core/runtime/broker-protocol.js';
|
|
1
|
+
import type { BrokerSnapshot, ClientRole } from '../../../core/runtime/broker-protocol.js';
|
|
2
2
|
import type { CanvasSource } from '../../../core/canvas/source.js';
|
|
3
3
|
import type { NodeMeta } from '../../../core/canvas/types.js';
|
|
4
4
|
import { BrokerClient } from '../../../core/broker-client/index.js';
|
|
@@ -31,8 +31,17 @@ export interface AttachConnection {
|
|
|
31
31
|
/** The attached node's persona/lifecycle, when this canvas can tell us —
|
|
32
32
|
* `undefined` for a remote attach until the viewer refetches it. */
|
|
33
33
|
readonly nodeIdentity: Pick<NodeMeta, 'kind' | 'mode' | 'lifecycle' | 'icon'> | undefined;
|
|
34
|
-
/** The one socket
|
|
34
|
+
/** The one socket target for the node's headless broker. It is intentionally
|
|
35
|
+
* disconnected only when focus opened an exact parked resident offline. */
|
|
35
36
|
readonly socket: BrokerClient;
|
|
37
|
+
/** Saved snapshot used to seed an offline parked viewer. Absent for direct
|
|
38
|
+
* attach and for focus paths whose broker was live or legitimately woke. */
|
|
39
|
+
readonly initialSnapshot?: BrokerSnapshot;
|
|
40
|
+
readonly initialCommands?: ReadonlyArray<{
|
|
41
|
+
name: string;
|
|
42
|
+
description: string;
|
|
43
|
+
source: string;
|
|
44
|
+
}>;
|
|
36
45
|
/** Re-throw a connect-adjacent failure that landed AFTER `connect` resolved.
|
|
37
46
|
* Call once, synchronously, just before permanent socket wiring takes over;
|
|
38
47
|
* a no-op if nothing failed. See the never-removed error listener below. */
|
|
@@ -41,4 +50,4 @@ export interface AttachConnection {
|
|
|
41
50
|
/** Resolve the target, revive the node if it is dormant (server-side), and dial
|
|
42
51
|
* the broker. Rejects with a plain `InputError` — no terminal has been taken
|
|
43
52
|
* over at this point, so a failure here is an ordinary non-zero CLI exit. */
|
|
44
|
-
export declare function connectToNode(nodeId: string, canvasName?: string): Promise<AttachConnection>;
|
|
53
|
+
export declare function connectToNode(nodeId: string, canvasName?: string, wakeParked?: boolean): Promise<AttachConnection>;
|
|
@@ -53,7 +53,7 @@ export async function preWaitForViewSocket(nodeId) {
|
|
|
53
53
|
/** Resolve the target, revive the node if it is dormant (server-side), and dial
|
|
54
54
|
* the broker. Rejects with a plain `InputError` — no terminal has been taken
|
|
55
55
|
* over at this point, so a failure here is an ordinary non-zero CLI exit. */
|
|
56
|
-
export async function connectToNode(nodeId, canvasName) {
|
|
56
|
+
export async function connectToNode(nodeId, canvasName, wakeParked = true) {
|
|
57
57
|
const remote = canvasName !== undefined;
|
|
58
58
|
// Attach must NOT silently honor a durable `crtr canvas use` selection: the
|
|
59
59
|
// local branch has a hard local-unix-socket assumption baked into its
|
|
@@ -74,6 +74,8 @@ export async function connectToNode(nodeId, canvasName) {
|
|
|
74
74
|
let meta;
|
|
75
75
|
let nodeIdentity;
|
|
76
76
|
let transport;
|
|
77
|
+
let initialSnapshot;
|
|
78
|
+
let initialCommands;
|
|
77
79
|
if (remote) {
|
|
78
80
|
meta = { cwd: process.cwd() };
|
|
79
81
|
transport = new RelayTransport(nodeId, resolveRemoteCanvasTarget(canvasName));
|
|
@@ -97,14 +99,23 @@ export async function connectToNode(nodeId, canvasName) {
|
|
|
97
99
|
// revive here and let BrokerClient(nodeId) connect over the local socket as
|
|
98
100
|
// before — this LOCAL process never launches pi or writes the session.
|
|
99
101
|
try {
|
|
100
|
-
await cliClient().ensureAttach(nodeId, { resume: true, revive: true });
|
|
102
|
+
const attach = await cliClient().ensureAttach(nodeId, { resume: true, revive: true, wakeParked });
|
|
103
|
+
if (attach.parked) {
|
|
104
|
+
const saved = await cliClient().getSnapshot(nodeId);
|
|
105
|
+
// The API DTO stays dependency-light and structural; this viewer is the
|
|
106
|
+
// consumer that knows the complete broker snapshot contract.
|
|
107
|
+
initialSnapshot = saved.snapshot;
|
|
108
|
+
initialCommands = saved.commands;
|
|
109
|
+
}
|
|
101
110
|
}
|
|
102
111
|
catch (err) {
|
|
103
112
|
rethrowAsCliError(err, 'The node may be finalized or otherwise not attachable.');
|
|
104
113
|
}
|
|
105
114
|
// Local only: remote attach reaches crtrd over WebSocket rather than a
|
|
106
|
-
// local socket file, so `--canvas` has nothing to pre-wait on.
|
|
107
|
-
|
|
115
|
+
// local socket file, so `--canvas` has nothing to pre-wait on. An offline
|
|
116
|
+
// parked focus has deliberately no socket to wait for.
|
|
117
|
+
if (initialSnapshot === undefined)
|
|
118
|
+
await preWaitForViewSocket(nodeId);
|
|
108
119
|
}
|
|
109
120
|
const socket = new BrokerClient(nodeId, { transport });
|
|
110
121
|
/** One error shape for a connect-time failure, whether the connect race below
|
|
@@ -137,34 +148,38 @@ export async function connectToNode(nodeId, canvasName) {
|
|
|
137
148
|
terminalStartupError = err;
|
|
138
149
|
});
|
|
139
150
|
// Connect FIRST (race connect/error) so a missing broker exits non-zero
|
|
140
|
-
// cleanly, BEFORE the TUI takes over the terminal.
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
151
|
+
// cleanly, BEFORE the TUI takes over the terminal. Offline parked focus is
|
|
152
|
+
// the one intentional exception: reconnect owns its first dial after paint.
|
|
153
|
+
if (initialSnapshot === undefined) {
|
|
154
|
+
await new Promise((resolve, reject) => {
|
|
155
|
+
const onConnect = () => {
|
|
156
|
+
cleanup();
|
|
157
|
+
resolve();
|
|
158
|
+
};
|
|
159
|
+
const onErr = (err) => {
|
|
160
|
+
cleanup();
|
|
161
|
+
reject(err);
|
|
162
|
+
};
|
|
163
|
+
const cleanup = () => {
|
|
164
|
+
socket.off('connect', onConnect);
|
|
165
|
+
socket.off('error', onErr);
|
|
166
|
+
};
|
|
167
|
+
socket.on('connect', onConnect);
|
|
168
|
+
socket.on('error', onErr);
|
|
169
|
+
socket.connect();
|
|
170
|
+
}).catch((err) => {
|
|
171
|
+
throw toAttachError(err);
|
|
172
|
+
});
|
|
173
|
+
}
|
|
160
174
|
return {
|
|
161
175
|
remote,
|
|
162
176
|
canvasSource,
|
|
163
177
|
meta,
|
|
164
178
|
nodeIdentity,
|
|
165
179
|
socket,
|
|
180
|
+
...(initialSnapshot === undefined ? {} : { initialSnapshot, initialCommands }),
|
|
166
181
|
assertConnected: () => {
|
|
167
|
-
if (terminalStartupError !== undefined)
|
|
182
|
+
if (initialSnapshot === undefined && terminalStartupError !== undefined)
|
|
168
183
|
throw toAttachError(terminalStartupError);
|
|
169
184
|
},
|
|
170
185
|
};
|
|
@@ -45,6 +45,6 @@ export interface InputWiringHooks {
|
|
|
45
45
|
frecency: FrecencyStore;
|
|
46
46
|
}
|
|
47
47
|
/** Construct the viewer's one capability authority. */
|
|
48
|
-
export declare function createViewerCapabilitiesForInput(ctx: Pick<AttachSession, 'nodeId' | 'remote' | 'role' | 'meta'
|
|
48
|
+
export declare function createViewerCapabilitiesForInput(ctx: Pick<AttachSession, 'nodeId' | 'remote' | 'role' | 'meta'>, isBrokerOffline: () => boolean): ViewerCapabilities;
|
|
49
49
|
/** Build the viewer's InputController with every hook answered. */
|
|
50
50
|
export declare function createInputController(ctx: Pick<AttachSession, 'nodeId' | 'remote' | 'role' | 'meta' | 'socket' | 'tui' | 'editor' | 'containers'>, km: KeybindingsManager, capabilities: ViewerCapabilities, hooks: InputWiringHooks): InputController;
|
|
@@ -17,12 +17,13 @@ import { cliClient } from '../../../commands/api-client.js';
|
|
|
17
17
|
import { buildLoginPicker, buildLogoutPicker } from '../overlays/auth.js';
|
|
18
18
|
import { defaultAgentDir } from '../config.js';
|
|
19
19
|
/** Construct the viewer's one capability authority. */
|
|
20
|
-
export function createViewerCapabilitiesForInput(ctx) {
|
|
20
|
+
export function createViewerCapabilitiesForInput(ctx, isBrokerOffline) {
|
|
21
21
|
return createViewerCapabilities({
|
|
22
22
|
nodeId: ctx.nodeId,
|
|
23
23
|
profileId: ctx.meta.profile_id ?? undefined,
|
|
24
24
|
remote: ctx.remote,
|
|
25
25
|
role: ctx.role,
|
|
26
|
+
isBrokerOffline,
|
|
26
27
|
});
|
|
27
28
|
}
|
|
28
29
|
/** Build the viewer's InputController with every hook answered. */
|
|
@@ -93,7 +94,10 @@ export function createInputController(ctx, km, capabilities, hooks) {
|
|
|
93
94
|
// /fork, /tree, /settings, /scoped-models) fetch their payloads over.
|
|
94
95
|
// Without it every picker degrades to the "isn't available in this viewer"
|
|
95
96
|
// notice (see slash/dispatch.ts + InputController.openPicker).
|
|
96
|
-
onRequest: (frame) =>
|
|
97
|
+
onRequest: (frame) => {
|
|
98
|
+
const refusal = capabilities.refusal('engine-drive');
|
|
99
|
+
return refusal === undefined ? socket.request(frame) : Promise.reject(new Error(refusal));
|
|
100
|
+
},
|
|
97
101
|
// Mount pickers inline in the chrome stack, directly under the editor
|
|
98
102
|
// (queued-panel pattern) rather than as a floating centered modal, so a
|
|
99
103
|
// picker reads as part of the editor — like pi's own selectors.
|
|
@@ -16,15 +16,19 @@ export interface ReconnectHooks {
|
|
|
16
16
|
/** End the session. `node-done` is a clean finalize (the node pushed its
|
|
17
17
|
* final report) and closes the pane; `broker-gone` freezes the corpse. */
|
|
18
18
|
teardown: (reason: 'node-done' | 'broker-gone') => void;
|
|
19
|
-
/**
|
|
20
|
-
*
|
|
21
|
-
|
|
19
|
+
/** The viewer was bootstrapped from a saved parked snapshot before any socket
|
|
20
|
+
* connection. */
|
|
21
|
+
initialDormant?: boolean;
|
|
22
|
+
/** Called once each time the node is first seen as dormant. A stopped broker
|
|
23
|
+
* will relay no final session event, so the viewer clears live-only display. */
|
|
22
24
|
onEnterDormancy: () => void;
|
|
23
25
|
}
|
|
24
26
|
export interface ReconnectSupervisor {
|
|
25
27
|
/** Send the hello handshake. Called once after `tui.start()`, then again on
|
|
26
28
|
* every successful redial. */
|
|
27
29
|
sendHello: () => void;
|
|
30
|
+
/** Begin redialing an offline parked viewer after its saved snapshot paints. */
|
|
31
|
+
startDormant: () => void;
|
|
28
32
|
/** True while the node has no live broker — the input layer routes a
|
|
29
33
|
* submitted prompt through a wake message rather than a dead socket. */
|
|
30
34
|
isDormant: () => boolean;
|
|
@@ -16,7 +16,7 @@ export function createReconnect(ctx, hooks) {
|
|
|
16
16
|
const { nodeId, socket, canvasSource } = ctx;
|
|
17
17
|
let lastSocketError;
|
|
18
18
|
let reconnecting = false;
|
|
19
|
-
let viewerDormant =
|
|
19
|
+
let viewerDormant = hooks.initialDormant === true;
|
|
20
20
|
// Captured at give-up: a terminal `done` is a CLEAN finalize — the viewer
|
|
21
21
|
// closes its pane instead of freezing a scary "broker gone" corpse.
|
|
22
22
|
// `dead`/`canceled` keep the corpse.
|
|
@@ -97,7 +97,12 @@ export function createReconnect(ctx, hooks) {
|
|
|
97
97
|
// submit shows "waking node…" the moment you send — so a persistent
|
|
98
98
|
// "node dormant" banner is pure noise. Clear the footer when dormant;
|
|
99
99
|
// keep the reconnecting notice only for a transient broker relaunch.
|
|
100
|
-
const dormant = row?.status === 'idle'
|
|
100
|
+
const dormant = row?.status === 'idle'
|
|
101
|
+
|| row?.intent === 'idle-release'
|
|
102
|
+
|| (row?.lifecycle === 'resident'
|
|
103
|
+
&& row.status === 'done'
|
|
104
|
+
&& row.intent === 'parked'
|
|
105
|
+
&& row.final_report == null);
|
|
101
106
|
const enteredDormancy = dormant && !viewerDormant;
|
|
102
107
|
viewerDormant = dormant;
|
|
103
108
|
if (enteredDormancy)
|
|
@@ -150,6 +155,12 @@ export function createReconnect(ctx, hooks) {
|
|
|
150
155
|
};
|
|
151
156
|
return {
|
|
152
157
|
sendHello,
|
|
158
|
+
startDormant: () => {
|
|
159
|
+
if (reconnecting || stopped || hooks.isTornDown())
|
|
160
|
+
return;
|
|
161
|
+
hooks.onEnterDormancy();
|
|
162
|
+
void attemptReconnect();
|
|
163
|
+
},
|
|
153
164
|
isDormant: () => viewerDormant,
|
|
154
165
|
lastSocketError: () => lastSocketError,
|
|
155
166
|
install,
|
|
@@ -38,7 +38,7 @@ export const CANVAS_SLASH_COMMANDS = [
|
|
|
38
38
|
{ name: 'graph', description: 'Toggle the canvas GRAPH view (your local subscription tree)' },
|
|
39
39
|
{ name: 'inbox', description: 'Toggle the human inbox — pending asks and reviews for you' },
|
|
40
40
|
{ name: 'promote', description: 'Promote this node to an orchestrator — /promote, or /promote <kind> to specialize' },
|
|
41
|
-
{ name: 'resume-node', description: 'Open the canvas navigator
|
|
41
|
+
{ name: 'resume-node', description: 'Open the canvas navigator and focus the chosen node; parked residents wake only on submitted input' },
|
|
42
42
|
{ name: 'context', description: 'Browse this node + its subscriptions’ context dirs in a read-only nvim popup' },
|
|
43
43
|
{ name: 'plugins', description: 'Open the interactive plugin browser for this machine' },
|
|
44
44
|
{ name: 'node-metadata', description: 'Inspect this node — identity, lifecycle, runtime, graph, artifacts, paths, and agent surfaces' },
|
|
@@ -5,4 +5,4 @@ import './session/pi-oauth-flows.js';
|
|
|
5
5
|
* always restoring the terminal first. `canvasName`, when set, attaches over
|
|
6
6
|
* a relayed WebSocket to that `remoteCanvas.targets.<name>` instead of the
|
|
7
7
|
* local unix socket — see the `--canvas` flag on `attachToLeaf`. */
|
|
8
|
-
export declare function runAttach(nodeId: string, observer: boolean, canvasName?: string): Promise<void>;
|
|
8
|
+
export declare function runAttach(nodeId: string, observer: boolean, canvasName?: string, wakeParked?: boolean): Promise<void>;
|