@openscout/protocol 0.2.64 → 0.2.82
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/LICENSE +202 -0
- package/README.md +103 -52
- package/dist/a2a.d.ts +224 -0
- package/dist/a2a.d.ts.map +1 -0
- package/dist/a2a.js +225 -0
- package/dist/activity.d.ts +70 -0
- package/dist/activity.d.ts.map +1 -0
- package/dist/activity.js +1 -0
- package/dist/actors.d.ts +7 -3
- package/dist/actors.d.ts.map +1 -0
- package/dist/actors.js +15 -1
- package/dist/agent-broker-feed.d.ts +84 -0
- package/dist/agent-broker-feed.d.ts.map +1 -0
- package/dist/agent-broker-feed.js +1 -0
- package/dist/agent-identity.d.ts +23 -0
- package/dist/agent-identity.d.ts.map +1 -0
- package/dist/agent-identity.js +122 -8
- package/dist/agent-runs.d.ts +191 -0
- package/dist/agent-runs.d.ts.map +1 -0
- package/dist/agent-runs.js +365 -0
- package/dist/assigned-roles.d.ts +205 -0
- package/dist/assigned-roles.d.ts.map +1 -0
- package/dist/assigned-roles.js +228 -0
- package/dist/broker-routes.d.ts +40 -0
- package/dist/broker-routes.d.ts.map +1 -0
- package/dist/broker-routes.js +56 -0
- package/dist/budget.d.ts +65 -0
- package/dist/budget.d.ts.map +1 -0
- package/dist/budget.js +19 -0
- package/dist/capability-matrix.d.ts +230 -0
- package/dist/capability-matrix.d.ts.map +1 -0
- package/dist/capability-matrix.js +304 -0
- package/dist/channel-identity.d.ts +40 -0
- package/dist/channel-identity.d.ts.map +1 -0
- package/dist/channel-identity.js +108 -0
- package/dist/collaboration.d.ts +45 -16
- package/dist/collaboration.d.ts.map +1 -0
- package/dist/collaboration.js +39 -23
- package/dist/common.d.ts +4 -3
- package/dist/common.d.ts.map +1 -0
- package/dist/conversations.d.ts +1 -0
- package/dist/conversations.d.ts.map +1 -0
- package/dist/cursor-transport.d.ts +22 -0
- package/dist/cursor-transport.d.ts.map +1 -0
- package/dist/cursor-transport.js +10 -0
- package/dist/debug-attach.d.ts +29 -0
- package/dist/debug-attach.d.ts.map +1 -0
- package/dist/debug-attach.js +6 -0
- package/dist/deliveries.d.ts +16 -2
- package/dist/deliveries.d.ts.map +1 -0
- package/dist/durable-actions.d.ts +94 -0
- package/dist/durable-actions.d.ts.map +1 -0
- package/dist/durable-actions.js +1 -0
- package/dist/events.d.ts +27 -1
- package/dist/events.d.ts.map +1 -0
- package/dist/events.js +14 -1
- package/dist/harness-feature-support.d.ts +76 -0
- package/dist/harness-feature-support.d.ts.map +1 -0
- package/dist/harness-feature-support.js +29 -0
- package/dist/host-integrations.d.ts +22 -0
- package/dist/host-integrations.d.ts.map +1 -0
- package/dist/host-integrations.js +64 -0
- package/dist/inbox.d.ts +50 -0
- package/dist/inbox.d.ts.map +1 -0
- package/dist/inbox.js +9 -0
- package/dist/index.d.ts +35 -2
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +34 -2
- package/dist/invocations.d.ts +119 -4
- package/dist/invocations.d.ts.map +1 -0
- package/dist/invocations.js +201 -1
- package/dist/mesh.d.ts +98 -0
- package/dist/mesh.d.ts.map +1 -0
- package/dist/mesh.js +18 -1
- package/dist/messages.d.ts +3 -0
- package/dist/messages.d.ts.map +1 -0
- package/dist/native-interactions.d.ts +30 -0
- package/dist/native-interactions.d.ts.map +1 -0
- package/dist/native-interactions.js +51 -0
- package/dist/observed-status.d.ts +33 -0
- package/dist/observed-status.d.ts.map +1 -0
- package/dist/observed-status.js +6 -0
- package/dist/orchestrator-prompt.d.ts +10 -0
- package/dist/orchestrator-prompt.d.ts.map +1 -0
- package/dist/orchestrator-prompt.js +36 -0
- package/dist/permission-policy.d.ts +5 -0
- package/dist/permission-policy.d.ts.map +1 -0
- package/dist/permission-policy.js +15 -0
- package/dist/provisional-agent-names.d.ts +30 -0
- package/dist/provisional-agent-names.d.ts.map +1 -0
- package/dist/provisional-agent-names.js +224 -0
- package/dist/read-receipts.d.ts +12 -0
- package/dist/read-receipts.d.ts.map +1 -0
- package/dist/read-receipts.js +1 -0
- package/dist/relay-agent-card.d.ts +10 -0
- package/dist/relay-agent-card.d.ts.map +1 -0
- package/dist/rendezvous.d.ts +97 -0
- package/dist/rendezvous.d.ts.map +1 -0
- package/dist/rendezvous.js +35 -0
- package/dist/reserved-vocabulary.d.ts +21 -0
- package/dist/reserved-vocabulary.d.ts.map +1 -0
- package/dist/reserved-vocabulary.js +101 -0
- package/dist/route-aliases.d.ts +112 -0
- package/dist/route-aliases.d.ts.map +1 -0
- package/dist/route-aliases.js +1 -0
- package/dist/runtime-catalog-contract.d.ts +39 -0
- package/dist/runtime-catalog-contract.d.ts.map +1 -0
- package/dist/runtime-catalog-contract.js +17 -0
- package/dist/runtime-catalog.d.ts +11 -0
- package/dist/runtime-catalog.d.ts.map +1 -0
- package/dist/runtime-catalog.generated.d.ts +230 -0
- package/dist/runtime-catalog.generated.d.ts.map +1 -0
- package/dist/runtime-catalog.generated.js +299 -0
- package/dist/runtime-catalog.js +192 -0
- package/dist/runtime-execution.d.ts +161 -0
- package/dist/runtime-execution.d.ts.map +1 -0
- package/dist/runtime-execution.js +327 -0
- package/dist/runtime-profiles.d.ts +12 -0
- package/dist/runtime-profiles.d.ts.map +1 -0
- package/dist/runtime-profiles.js +20 -0
- package/dist/scout-agent-card.d.ts +10 -0
- package/dist/scout-agent-card.d.ts.map +1 -0
- package/dist/scout-composer.d.ts +27 -0
- package/dist/scout-composer.d.ts.map +1 -0
- package/dist/scout-composer.js +192 -0
- package/dist/scout-delivery.d.ts +99 -4
- package/dist/scout-delivery.d.ts.map +1 -0
- package/dist/scout-dispatch.d.ts +40 -3
- package/dist/scout-dispatch.d.ts.map +1 -0
- package/dist/scout-reply-context.d.ts +1 -0
- package/dist/scout-reply-context.d.ts.map +1 -0
- package/dist/session-display-state.d.ts +132 -0
- package/dist/session-display-state.d.ts.map +1 -0
- package/dist/session-display-state.js +94 -0
- package/dist/terminal-host-topology.d.ts +119 -0
- package/dist/terminal-host-topology.d.ts.map +1 -0
- package/dist/terminal-host-topology.js +21 -0
- package/dist/terminal-sessions.d.ts +142 -0
- package/dist/terminal-sessions.d.ts.map +1 -0
- package/dist/terminal-sessions.js +81 -0
- package/dist/terminal-surface-id.d.ts +73 -0
- package/dist/terminal-surface-id.d.ts.map +1 -0
- package/dist/terminal-surface-id.js +162 -0
- package/dist/terminal-workspaces.d.ts +199 -0
- package/dist/terminal-workspaces.d.ts.map +1 -0
- package/dist/terminal-workspaces.js +287 -0
- package/dist/thread-events.d.ts +1 -0
- package/dist/thread-events.d.ts.map +1 -0
- package/dist/time.d.ts +16 -0
- package/dist/time.d.ts.map +1 -0
- package/dist/time.js +33 -0
- package/dist/transports.d.ts +1 -0
- package/dist/transports.d.ts.map +1 -0
- package/package.json +37 -9
- package/src/a2a.ts +471 -0
- package/src/activity.ts +113 -0
- package/src/actors.ts +42 -8
- package/src/agent-broker-feed.ts +102 -0
- package/src/agent-identity.ts +143 -8
- package/src/agent-runs.ts +675 -0
- package/src/assigned-roles.ts +437 -0
- package/src/broker-routes.ts +62 -0
- package/src/budget.ts +88 -0
- package/src/capability-matrix.ts +648 -0
- package/src/channel-identity.ts +147 -0
- package/src/collaboration.ts +78 -48
- package/src/common.ts +24 -5
- package/src/cursor-transport.ts +34 -0
- package/src/debug-attach.ts +38 -0
- package/src/deliveries.ts +16 -1
- package/src/durable-actions.ts +125 -0
- package/src/events.ts +31 -0
- package/src/harness-feature-support.ts +116 -0
- package/src/host-integrations.ts +104 -0
- package/src/inbox.ts +68 -0
- package/src/index.ts +34 -2
- package/src/invocations.ts +351 -4
- package/src/mesh.ts +130 -0
- package/src/messages.ts +2 -0
- package/src/native-interactions.ts +100 -0
- package/src/observed-status.ts +90 -0
- package/src/orchestrator-prompt.ts +47 -0
- package/src/permission-policy.ts +21 -0
- package/src/provisional-agent-names.ts +272 -0
- package/src/read-receipts.ts +12 -0
- package/src/relay-agent-card.ts +73 -63
- package/src/rendezvous.ts +154 -0
- package/src/reserved-vocabulary.ts +135 -0
- package/src/route-aliases.ts +130 -0
- package/src/runtime-catalog-contract.ts +60 -0
- package/src/runtime-catalog.generated.ts +300 -0
- package/src/runtime-catalog.md +21 -0
- package/src/runtime-catalog.ts +193 -0
- package/src/runtime-catalog.v1.json +81 -0
- package/src/runtime-execution.ts +545 -0
- package/src/runtime-profiles.ts +37 -0
- package/src/scout-agent-card.ts +10 -0
- package/src/scout-composer.ts +249 -0
- package/src/scout-delivery.ts +113 -3
- package/src/scout-dispatch.ts +33 -2
- package/src/session-display-state.ts +202 -0
- package/src/terminal-host-topology.ts +132 -0
- package/src/terminal-sessions.ts +187 -0
- package/src/terminal-surface-id.ts +189 -0
- package/src/terminal-workspaces.ts +455 -0
- package/src/time.ts +49 -0
- package/dist/agent-address.d.ts +0 -1
- package/dist/agent-address.js +0 -1
- package/dist/agent-selectors.d.ts +0 -1
- package/dist/agent-selectors.js +0 -1
- package/src/agent-address.ts +0 -1
- package/src/agent-identity.test.ts +0 -435
- package/src/agent-selectors.ts +0 -1
- package/src/scout-agent-card.test.ts +0 -54
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Terminal session registry.
|
|
3
|
+
*
|
|
4
|
+
* The durable noun is a HARNESS SESSION — a stable agent session identified by
|
|
5
|
+
* its harness-native source id and resumable via a resume command. A harness
|
|
6
|
+
* session is *materialized* through one or more disposable TERMINAL SURFACES
|
|
7
|
+
* (tmux, zellij, future ssh/host-control). Backends are interchangeable: the
|
|
8
|
+
* source session id is the stable key; the surface is the throwaway shell.
|
|
9
|
+
*
|
|
10
|
+
* This module defines the shared contract used by `scout session intake`
|
|
11
|
+
* (which writes the record), the runtime store (which persists it), the
|
|
12
|
+
* web/runtime APIs (which list it + materialize surfaces), and the app
|
|
13
|
+
* terminal UI (which reads it to attach/observe/take over).
|
|
14
|
+
*
|
|
15
|
+
* Terminal scrollback is never imported as Scout messages — a surface is a
|
|
16
|
+
* relay target, not a message source.
|
|
17
|
+
*/
|
|
18
|
+
import type { TerminalHostId, TerminalSurfaceId } from "./terminal-surface-id.js";
|
|
19
|
+
/** Lifecycle of a single materialized surface. */
|
|
20
|
+
export type TerminalSurfaceState = "live" | "detached" | "exited";
|
|
21
|
+
/**
|
|
22
|
+
* Relay descriptor for one surface.
|
|
23
|
+
*
|
|
24
|
+
* The per-backend optional fields are legacy: they are what a relay built
|
|
25
|
+
* before host adapters expects to find, and they are only populated for the
|
|
26
|
+
* hosts that predate the registry. A new host adds an adapter, not a field —
|
|
27
|
+
* `backend` plus `sessionName` is the whole contract.
|
|
28
|
+
*/
|
|
29
|
+
export type TerminalSurfaceRelay = {
|
|
30
|
+
backend: TerminalHostId;
|
|
31
|
+
sessionName: string;
|
|
32
|
+
tmuxSession?: string;
|
|
33
|
+
zellijSession?: string;
|
|
34
|
+
zellijPaneId?: string;
|
|
35
|
+
};
|
|
36
|
+
/** One disposable terminal surface a harness session has been materialized through. */
|
|
37
|
+
export type TerminalSurface = {
|
|
38
|
+
/**
|
|
39
|
+
* Opaque durable handle for this surface. Absent on records written before
|
|
40
|
+
* surface ids existed; derive one with `terminalSurfaceIdForSurface`.
|
|
41
|
+
*/
|
|
42
|
+
surfaceId?: TerminalSurfaceId;
|
|
43
|
+
/** Host that owns the surface. Registered adapters may add hosts the union does not name. */
|
|
44
|
+
backend: TerminalHostId;
|
|
45
|
+
/** Backend session name (e.g. tmux target, zellij session). Secondary metadata. */
|
|
46
|
+
sessionName: string;
|
|
47
|
+
paneId: string | null;
|
|
48
|
+
attachCommand: string[];
|
|
49
|
+
observeCommand: string[] | null;
|
|
50
|
+
relay: TerminalSurfaceRelay;
|
|
51
|
+
/** Lifecycle state; absent means unknown / not yet observed. */
|
|
52
|
+
state?: TerminalSurfaceState;
|
|
53
|
+
/**
|
|
54
|
+
* Zellij requires a short socket dir on macOS (the default $TMPDIR exceeds the
|
|
55
|
+
* Unix socket-path length limit). Any relay attaching to a Scout-created zellij
|
|
56
|
+
* surface must preserve this, or it lands in a different server namespace.
|
|
57
|
+
*/
|
|
58
|
+
socketDir?: string;
|
|
59
|
+
};
|
|
60
|
+
/**
|
|
61
|
+
* Durable registry record: a stable harness session plus the disposable
|
|
62
|
+
* terminal surfaces it owns. Re-materializing in another backend appends a
|
|
63
|
+
* surface; it never changes the record identity.
|
|
64
|
+
*/
|
|
65
|
+
export type TerminalSessionRecord = {
|
|
66
|
+
/** Stable Scout record id (derived from harness + sourceSessionId by default). */
|
|
67
|
+
id: string;
|
|
68
|
+
harness: string;
|
|
69
|
+
/** Harness-native session id — the stable identity across backends. */
|
|
70
|
+
sourceSessionId: string;
|
|
71
|
+
cwd: string;
|
|
72
|
+
resumeCommand: string;
|
|
73
|
+
/**
|
|
74
|
+
* Where the record came from. `registry` records are Scout-owned harness
|
|
75
|
+
* sessions with a real harness and resume command; `discovered` records are
|
|
76
|
+
* live multiplexer sessions observed on the host, for which both are unknown
|
|
77
|
+
* and must be left empty rather than filled with the backend and attach argv.
|
|
78
|
+
* Absent means `registry`.
|
|
79
|
+
*/
|
|
80
|
+
origin?: "registry" | "discovered";
|
|
81
|
+
surfaces: TerminalSurface[];
|
|
82
|
+
createdAt: number;
|
|
83
|
+
updatedAt: number;
|
|
84
|
+
metadata?: Record<string, unknown>;
|
|
85
|
+
};
|
|
86
|
+
/** Upsert input for a registry record. Timestamps + id are assigned by the store. */
|
|
87
|
+
export type TerminalSessionRecordInput = {
|
|
88
|
+
/** Optional explicit id; defaults to a deterministic id from harness + sourceSessionId. */
|
|
89
|
+
id?: string;
|
|
90
|
+
harness: string;
|
|
91
|
+
sourceSessionId: string;
|
|
92
|
+
cwd: string;
|
|
93
|
+
resumeCommand: string;
|
|
94
|
+
surfaces?: TerminalSurface[];
|
|
95
|
+
metadata?: Record<string, unknown>;
|
|
96
|
+
};
|
|
97
|
+
/**
|
|
98
|
+
* The durable handle for a surface. Prefers the stored id and derives the same
|
|
99
|
+
* value from the surface's address otherwise, so a record written before
|
|
100
|
+
* surface ids existed resolves to the id it would be issued today.
|
|
101
|
+
*/
|
|
102
|
+
export declare function terminalSurfaceIdForSurface(surface: Pick<TerminalSurface, "surfaceId" | "backend" | "sessionName" | "paneId">, options?: {
|
|
103
|
+
nodeId?: string | null;
|
|
104
|
+
}): TerminalSurfaceId;
|
|
105
|
+
/** Where a match is being evaluated, for the parts of matching that are node-scoped. */
|
|
106
|
+
export type TerminalSurfaceMatchScope = {
|
|
107
|
+
/**
|
|
108
|
+
* This host's own Scout node id, when the caller knows it. A surface observed
|
|
109
|
+
* with no node in its id was observed HERE, so this is the only node whose
|
|
110
|
+
* handles may claim it.
|
|
111
|
+
*/
|
|
112
|
+
localNodeId?: string | null;
|
|
113
|
+
};
|
|
114
|
+
/**
|
|
115
|
+
* Whether a handle scoped to `handleNodeId` may address a surface scoped to
|
|
116
|
+
* `surfaceNodeId`.
|
|
117
|
+
*
|
|
118
|
+
* Three cases, and the third is the one a review reproduced:
|
|
119
|
+
*
|
|
120
|
+
* - Both sides name a node: they must name the same one. Without this a handle
|
|
121
|
+
* for one node matched an identically named session on another.
|
|
122
|
+
* - The handle names none: it is a legacy key, which names no machine and must
|
|
123
|
+
* keep matching whatever it finds.
|
|
124
|
+
* - The handle names a node and the OBSERVATION does not. A node-less
|
|
125
|
+
* observation is by definition local — it is a session this host just listed
|
|
126
|
+
* — so only the local node's handle can be the one it belongs to. Letting any
|
|
127
|
+
* node-scoped handle match it meant handles for `node-a` and `node-b` both
|
|
128
|
+
* bound to one discovered local session, and both workspaces reported it
|
|
129
|
+
* Running: one session proving two machines live. A foreign handle never
|
|
130
|
+
* matches, and when this host does not know its own node id it cannot prove
|
|
131
|
+
* the handle is local either, so it does not claim it.
|
|
132
|
+
*/
|
|
133
|
+
export declare function terminalSurfaceNodeScopeMatches(input: {
|
|
134
|
+
handleNodeId: string | null;
|
|
135
|
+
surfaceNodeId: string | null;
|
|
136
|
+
localNodeId?: string | null;
|
|
137
|
+
}): boolean;
|
|
138
|
+
/** The node a surface belongs to, which lives inside its own opaque id. */
|
|
139
|
+
export declare function terminalSurfaceNodeId(surface: Pick<TerminalSurface, "surfaceId">): string | null;
|
|
140
|
+
/** True when a handle addresses this surface, in either the opaque or legacy form. */
|
|
141
|
+
export declare function terminalSurfaceMatchesId(surface: Pick<TerminalSurface, "surfaceId" | "backend" | "sessionName" | "paneId">, handle: string | null | undefined, scope?: TerminalSurfaceMatchScope): boolean;
|
|
142
|
+
//# sourceMappingURL=terminal-sessions.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"terminal-sessions.d.ts","sourceRoot":"","sources":["../src/terminal-sessions.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Terminal session registry.
|
|
3
|
+
*
|
|
4
|
+
* The durable noun is a HARNESS SESSION — a stable agent session identified by
|
|
5
|
+
* its harness-native source id and resumable via a resume command. A harness
|
|
6
|
+
* session is *materialized* through one or more disposable TERMINAL SURFACES
|
|
7
|
+
* (tmux, zellij, future ssh/host-control). Backends are interchangeable: the
|
|
8
|
+
* source session id is the stable key; the surface is the throwaway shell.
|
|
9
|
+
*
|
|
10
|
+
* This module defines the shared contract used by `scout session intake`
|
|
11
|
+
* (which writes the record), the runtime store (which persists it), the
|
|
12
|
+
* web/runtime APIs (which list it + materialize surfaces), and the app
|
|
13
|
+
* terminal UI (which reads it to attach/observe/take over).
|
|
14
|
+
*
|
|
15
|
+
* Terminal scrollback is never imported as Scout messages — a surface is a
|
|
16
|
+
* relay target, not a message source.
|
|
17
|
+
*/
|
|
18
|
+
import { formatTerminalSurfaceId, parseTerminalSurfaceId } from "./terminal-surface-id.js";
|
|
19
|
+
/**
|
|
20
|
+
* The durable handle for a surface. Prefers the stored id and derives the same
|
|
21
|
+
* value from the surface's address otherwise, so a record written before
|
|
22
|
+
* surface ids existed resolves to the id it would be issued today.
|
|
23
|
+
*/
|
|
24
|
+
export function terminalSurfaceIdForSurface(surface, options = {}) {
|
|
25
|
+
if (surface.surfaceId)
|
|
26
|
+
return surface.surfaceId;
|
|
27
|
+
return formatTerminalSurfaceId({
|
|
28
|
+
backend: surface.backend,
|
|
29
|
+
hostSession: surface.sessionName,
|
|
30
|
+
paneId: surface.paneId,
|
|
31
|
+
nodeId: options.nodeId ?? null,
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Whether a handle scoped to `handleNodeId` may address a surface scoped to
|
|
36
|
+
* `surfaceNodeId`.
|
|
37
|
+
*
|
|
38
|
+
* Three cases, and the third is the one a review reproduced:
|
|
39
|
+
*
|
|
40
|
+
* - Both sides name a node: they must name the same one. Without this a handle
|
|
41
|
+
* for one node matched an identically named session on another.
|
|
42
|
+
* - The handle names none: it is a legacy key, which names no machine and must
|
|
43
|
+
* keep matching whatever it finds.
|
|
44
|
+
* - The handle names a node and the OBSERVATION does not. A node-less
|
|
45
|
+
* observation is by definition local — it is a session this host just listed
|
|
46
|
+
* — so only the local node's handle can be the one it belongs to. Letting any
|
|
47
|
+
* node-scoped handle match it meant handles for `node-a` and `node-b` both
|
|
48
|
+
* bound to one discovered local session, and both workspaces reported it
|
|
49
|
+
* Running: one session proving two machines live. A foreign handle never
|
|
50
|
+
* matches, and when this host does not know its own node id it cannot prove
|
|
51
|
+
* the handle is local either, so it does not claim it.
|
|
52
|
+
*/
|
|
53
|
+
export function terminalSurfaceNodeScopeMatches(input) {
|
|
54
|
+
if (input.handleNodeId === null)
|
|
55
|
+
return true;
|
|
56
|
+
if (input.surfaceNodeId !== null)
|
|
57
|
+
return input.handleNodeId === input.surfaceNodeId;
|
|
58
|
+
const localNodeId = input.localNodeId?.trim() || null;
|
|
59
|
+
return localNodeId !== null && input.handleNodeId === localNodeId;
|
|
60
|
+
}
|
|
61
|
+
/** The node a surface belongs to, which lives inside its own opaque id. */
|
|
62
|
+
export function terminalSurfaceNodeId(surface) {
|
|
63
|
+
return parseTerminalSurfaceId(surface.surfaceId)?.nodeId ?? null;
|
|
64
|
+
}
|
|
65
|
+
/** True when a handle addresses this surface, in either the opaque or legacy form. */
|
|
66
|
+
export function terminalSurfaceMatchesId(surface, handle, scope = {}) {
|
|
67
|
+
const address = parseTerminalSurfaceId(handle);
|
|
68
|
+
if (!address)
|
|
69
|
+
return false;
|
|
70
|
+
if (address.backend !== surface.backend || address.hostSession !== surface.sessionName)
|
|
71
|
+
return false;
|
|
72
|
+
// A legacy key carries no pane, so it matches any pane on the session; an
|
|
73
|
+
// opaque id that names a pane must match it exactly.
|
|
74
|
+
if (address.paneId !== null && address.paneId !== (surface.paneId ?? null))
|
|
75
|
+
return false;
|
|
76
|
+
return terminalSurfaceNodeScopeMatches({
|
|
77
|
+
handleNodeId: address.nodeId ?? null,
|
|
78
|
+
surfaceNodeId: terminalSurfaceNodeId(surface),
|
|
79
|
+
localNodeId: scope.localNodeId,
|
|
80
|
+
});
|
|
81
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Terminal surface identity.
|
|
3
|
+
*
|
|
4
|
+
* A surface id is an opaque token that names one materialized terminal surface:
|
|
5
|
+
* a host session (and optionally a pane) on a host backend, on a node. It is
|
|
6
|
+
* the durable handle a saved workspace cell, a deep link, or a route alias
|
|
7
|
+
* holds onto.
|
|
8
|
+
*
|
|
9
|
+
* Two rules make it durable, and both are the point of this module:
|
|
10
|
+
*
|
|
11
|
+
* 1. Identity comes from (node, backend, host session, pane) — never from the
|
|
12
|
+
* display name. Renaming a session used to change its Scout id, because ids
|
|
13
|
+
* were `sha1(name)`.
|
|
14
|
+
* 2. There is exactly ONE constructor ({@link formatTerminalSurfaceId}) and ONE
|
|
15
|
+
* parser ({@link parseTerminalSurfaceId}). Nothing else may split a surface
|
|
16
|
+
* id on a separator. Four different separator conventions had grown across
|
|
17
|
+
* the server, the web client, and the macOS app, each with its own
|
|
18
|
+
* assumptions about which characters could appear in a session name; the
|
|
19
|
+
* only reason they worked was an unrelated validator that happened to
|
|
20
|
+
* exclude `:`.
|
|
21
|
+
*
|
|
22
|
+
* The token is opaque to consumers but not to this module: it encodes its parts
|
|
23
|
+
* so a server can resolve one without a lookup table, and so a client that
|
|
24
|
+
* receives an id it has never seen can still be routed. Legacy `backend:name`
|
|
25
|
+
* keys parse too, so persisted workspaces and in-flight URLs keep working.
|
|
26
|
+
*/
|
|
27
|
+
/** Backends with first-class protocol support. */
|
|
28
|
+
export type TerminalBackend = "tmux" | "zellij";
|
|
29
|
+
/**
|
|
30
|
+
* Any terminal host Scout can address. {@link TerminalBackend} is the subset the
|
|
31
|
+
* protocol types name explicitly; adapters may register further hosts (herdr,
|
|
32
|
+
* ssh, host-control) without a protocol change.
|
|
33
|
+
*/
|
|
34
|
+
export type TerminalHostId = TerminalBackend | (string & {});
|
|
35
|
+
/** The parts a surface id is built from. */
|
|
36
|
+
export type TerminalSurfaceAddress = {
|
|
37
|
+
/** Host adapter that owns the surface. */
|
|
38
|
+
backend: TerminalHostId;
|
|
39
|
+
/** Backend-native session name. Mutable display metadata, not identity on its own. */
|
|
40
|
+
hostSession: string;
|
|
41
|
+
/** Pane or tab within the host session, when the host addresses one. */
|
|
42
|
+
paneId?: string | null;
|
|
43
|
+
/** Scout node the surface lives on. Null/absent means the local node. */
|
|
44
|
+
nodeId?: string | null;
|
|
45
|
+
};
|
|
46
|
+
/** Opaque surface handle. Never split, slice, or interpolate into a pattern. */
|
|
47
|
+
export type TerminalSurfaceId = string;
|
|
48
|
+
export declare function isTerminalSurfaceId(value: string | null | undefined): boolean;
|
|
49
|
+
/**
|
|
50
|
+
* Build the surface id for an address. Deterministic: the same address always
|
|
51
|
+
* yields the same id, on any node, in any process, so two writers converge on
|
|
52
|
+
* one row instead of forking one.
|
|
53
|
+
*/
|
|
54
|
+
export declare function formatTerminalSurfaceId(address: TerminalSurfaceAddress): TerminalSurfaceId;
|
|
55
|
+
/**
|
|
56
|
+
* Resolve a surface id back to its address. Accepts the opaque form and the
|
|
57
|
+
* legacy `backend:sessionName` key. Returns null for anything else rather than
|
|
58
|
+
* guessing — a caller holding an unparseable handle must say so, not fabricate
|
|
59
|
+
* a target.
|
|
60
|
+
*/
|
|
61
|
+
export declare function parseTerminalSurfaceId(value: string | null | undefined): TerminalSurfaceAddress | null;
|
|
62
|
+
/**
|
|
63
|
+
* Compare two handles by what they address, not by how they were written, so a
|
|
64
|
+
* legacy key and its opaque replacement resolve to the same tile.
|
|
65
|
+
*/
|
|
66
|
+
export declare function terminalSurfaceIdsEqual(left: string | null | undefined, right: string | null | undefined): boolean;
|
|
67
|
+
/**
|
|
68
|
+
* The legacy `backend:sessionName` key for an address. Kept so ids issued by
|
|
69
|
+
* this build stay readable by clients that have not adopted the opaque form
|
|
70
|
+
* (macOS, iOS). Delete once every surface reads {@link TerminalSurfaceId}.
|
|
71
|
+
*/
|
|
72
|
+
export declare function legacyTerminalSurfaceKey(address: TerminalSurfaceAddress): string;
|
|
73
|
+
//# sourceMappingURL=terminal-surface-id.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"terminal-surface-id.d.ts","sourceRoot":"","sources":["../src/terminal-surface-id.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AAEH,kDAAkD;AAClD,MAAM,MAAM,eAAe,GAAG,MAAM,GAAG,QAAQ,CAAC;AAEhD;;;;GAIG;AACH,MAAM,MAAM,cAAc,GAAG,eAAe,GAAG,CAAC,MAAM,GAAG,EAAE,CAAC,CAAC;AAE7D,4CAA4C;AAC5C,MAAM,MAAM,sBAAsB,GAAG;IACnC,0CAA0C;IAC1C,OAAO,EAAE,cAAc,CAAC;IACxB,sFAAsF;IACtF,WAAW,EAAE,MAAM,CAAC;IACpB,wEAAwE;IACxE,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,yEAAyE;IACzE,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CACxB,CAAC;AAEF,gFAAgF;AAChF,MAAM,MAAM,iBAAiB,GAAG,MAAM,CAAC;AAOvC,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAAG,OAAO,CAE7E;AAED;;;;GAIG;AACH,wBAAgB,uBAAuB,CAAC,OAAO,EAAE,sBAAsB,GAAG,iBAAiB,CAS1F;AAED;;;;;GAKG;AACH,wBAAgB,sBAAsB,CACpC,KAAK,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAC/B,sBAAsB,GAAG,IAAI,CAmC/B;AAED;;;GAGG;AACH,wBAAgB,uBAAuB,CACrC,IAAI,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,EAC/B,KAAK,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAC/B,OAAO,CAQT;AAED;;;;GAIG;AACH,wBAAgB,wBAAwB,CAAC,OAAO,EAAE,sBAAsB,GAAG,MAAM,CAEhF"}
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Terminal surface identity.
|
|
3
|
+
*
|
|
4
|
+
* A surface id is an opaque token that names one materialized terminal surface:
|
|
5
|
+
* a host session (and optionally a pane) on a host backend, on a node. It is
|
|
6
|
+
* the durable handle a saved workspace cell, a deep link, or a route alias
|
|
7
|
+
* holds onto.
|
|
8
|
+
*
|
|
9
|
+
* Two rules make it durable, and both are the point of this module:
|
|
10
|
+
*
|
|
11
|
+
* 1. Identity comes from (node, backend, host session, pane) — never from the
|
|
12
|
+
* display name. Renaming a session used to change its Scout id, because ids
|
|
13
|
+
* were `sha1(name)`.
|
|
14
|
+
* 2. There is exactly ONE constructor ({@link formatTerminalSurfaceId}) and ONE
|
|
15
|
+
* parser ({@link parseTerminalSurfaceId}). Nothing else may split a surface
|
|
16
|
+
* id on a separator. Four different separator conventions had grown across
|
|
17
|
+
* the server, the web client, and the macOS app, each with its own
|
|
18
|
+
* assumptions about which characters could appear in a session name; the
|
|
19
|
+
* only reason they worked was an unrelated validator that happened to
|
|
20
|
+
* exclude `:`.
|
|
21
|
+
*
|
|
22
|
+
* The token is opaque to consumers but not to this module: it encodes its parts
|
|
23
|
+
* so a server can resolve one without a lookup table, and so a client that
|
|
24
|
+
* receives an id it has never seen can still be routed. Legacy `backend:name`
|
|
25
|
+
* keys parse too, so persisted workspaces and in-flight URLs keep working.
|
|
26
|
+
*/
|
|
27
|
+
const SURFACE_ID_PREFIX = "srf1.";
|
|
28
|
+
/** Legacy `backend:sessionName` keys, still written by older clients and links. */
|
|
29
|
+
const LEGACY_SURFACE_KEY = /^(?<backend>[A-Za-z][A-Za-z0-9_-]*):(?<hostSession>.+)$/u;
|
|
30
|
+
const BASE64URL_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
|
|
31
|
+
export function isTerminalSurfaceId(value) {
|
|
32
|
+
return typeof value === "string" && value.startsWith(SURFACE_ID_PREFIX);
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Build the surface id for an address. Deterministic: the same address always
|
|
36
|
+
* yields the same id, on any node, in any process, so two writers converge on
|
|
37
|
+
* one row instead of forking one.
|
|
38
|
+
*/
|
|
39
|
+
export function formatTerminalSurfaceId(address) {
|
|
40
|
+
const backend = address.backend.trim();
|
|
41
|
+
const hostSession = address.hostSession.trim();
|
|
42
|
+
if (!backend || !hostSession) {
|
|
43
|
+
throw new Error("a terminal surface id needs a backend and a host session");
|
|
44
|
+
}
|
|
45
|
+
const paneId = address.paneId?.trim() || null;
|
|
46
|
+
const nodeId = address.nodeId?.trim() || null;
|
|
47
|
+
return `${SURFACE_ID_PREFIX}${encodeBase64Url(JSON.stringify([nodeId, backend, hostSession, paneId]))}`;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Resolve a surface id back to its address. Accepts the opaque form and the
|
|
51
|
+
* legacy `backend:sessionName` key. Returns null for anything else rather than
|
|
52
|
+
* guessing — a caller holding an unparseable handle must say so, not fabricate
|
|
53
|
+
* a target.
|
|
54
|
+
*/
|
|
55
|
+
export function parseTerminalSurfaceId(value) {
|
|
56
|
+
const trimmed = value?.trim();
|
|
57
|
+
if (!trimmed)
|
|
58
|
+
return null;
|
|
59
|
+
if (trimmed.startsWith(SURFACE_ID_PREFIX)) {
|
|
60
|
+
const decoded = decodeBase64Url(trimmed.slice(SURFACE_ID_PREFIX.length));
|
|
61
|
+
if (decoded === null)
|
|
62
|
+
return null;
|
|
63
|
+
let parsed;
|
|
64
|
+
try {
|
|
65
|
+
parsed = JSON.parse(decoded);
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
if (!Array.isArray(parsed) || parsed.length < 3)
|
|
71
|
+
return null;
|
|
72
|
+
const [nodeId, backend, hostSession, paneId] = parsed;
|
|
73
|
+
if (typeof backend !== "string" || !backend.trim())
|
|
74
|
+
return null;
|
|
75
|
+
if (typeof hostSession !== "string" || !hostSession.trim())
|
|
76
|
+
return null;
|
|
77
|
+
return {
|
|
78
|
+
backend: backend.trim(),
|
|
79
|
+
hostSession: hostSession.trim(),
|
|
80
|
+
paneId: typeof paneId === "string" && paneId.trim() ? paneId.trim() : null,
|
|
81
|
+
nodeId: typeof nodeId === "string" && nodeId.trim() ? nodeId.trim() : null,
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
const legacy = LEGACY_SURFACE_KEY.exec(trimmed);
|
|
85
|
+
if (!legacy?.groups)
|
|
86
|
+
return null;
|
|
87
|
+
const hostSession = legacy.groups.hostSession.trim();
|
|
88
|
+
if (!hostSession)
|
|
89
|
+
return null;
|
|
90
|
+
return {
|
|
91
|
+
backend: legacy.groups.backend,
|
|
92
|
+
hostSession,
|
|
93
|
+
paneId: null,
|
|
94
|
+
nodeId: null,
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Compare two handles by what they address, not by how they were written, so a
|
|
99
|
+
* legacy key and its opaque replacement resolve to the same tile.
|
|
100
|
+
*/
|
|
101
|
+
export function terminalSurfaceIdsEqual(left, right) {
|
|
102
|
+
const leftAddress = parseTerminalSurfaceId(left);
|
|
103
|
+
const rightAddress = parseTerminalSurfaceId(right);
|
|
104
|
+
if (!leftAddress || !rightAddress)
|
|
105
|
+
return false;
|
|
106
|
+
return leftAddress.backend === rightAddress.backend
|
|
107
|
+
&& leftAddress.hostSession === rightAddress.hostSession
|
|
108
|
+
&& (leftAddress.paneId ?? null) === (rightAddress.paneId ?? null)
|
|
109
|
+
&& (leftAddress.nodeId ?? null) === (rightAddress.nodeId ?? null);
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* The legacy `backend:sessionName` key for an address. Kept so ids issued by
|
|
113
|
+
* this build stay readable by clients that have not adopted the opaque form
|
|
114
|
+
* (macOS, iOS). Delete once every surface reads {@link TerminalSurfaceId}.
|
|
115
|
+
*/
|
|
116
|
+
export function legacyTerminalSurfaceKey(address) {
|
|
117
|
+
return `${address.backend}:${address.hostSession}`;
|
|
118
|
+
}
|
|
119
|
+
// Base64url over UTF-8, hand-rolled so this module runs unchanged in a browser
|
|
120
|
+
// bundle, a Bun server, and a Node relay without Buffer or atob/btoa.
|
|
121
|
+
function encodeBase64Url(value) {
|
|
122
|
+
const bytes = new TextEncoder().encode(value);
|
|
123
|
+
let out = "";
|
|
124
|
+
for (let index = 0; index < bytes.length; index += 3) {
|
|
125
|
+
const byte0 = bytes[index];
|
|
126
|
+
const byte1 = bytes[index + 1];
|
|
127
|
+
const byte2 = bytes[index + 2];
|
|
128
|
+
out += BASE64URL_ALPHABET[byte0 >> 2];
|
|
129
|
+
out += BASE64URL_ALPHABET[((byte0 & 0b11) << 4) | ((byte1 ?? 0) >> 4)];
|
|
130
|
+
if (byte1 === undefined)
|
|
131
|
+
break;
|
|
132
|
+
out += BASE64URL_ALPHABET[((byte1 & 0b1111) << 2) | ((byte2 ?? 0) >> 6)];
|
|
133
|
+
if (byte2 === undefined)
|
|
134
|
+
break;
|
|
135
|
+
out += BASE64URL_ALPHABET[byte2 & 0b111111];
|
|
136
|
+
}
|
|
137
|
+
return out;
|
|
138
|
+
}
|
|
139
|
+
function decodeBase64Url(value) {
|
|
140
|
+
if (!value)
|
|
141
|
+
return null;
|
|
142
|
+
const bytes = [];
|
|
143
|
+
let buffer = 0;
|
|
144
|
+
let bits = 0;
|
|
145
|
+
for (const character of value) {
|
|
146
|
+
const index = BASE64URL_ALPHABET.indexOf(character);
|
|
147
|
+
if (index < 0)
|
|
148
|
+
return null;
|
|
149
|
+
buffer = (buffer << 6) | index;
|
|
150
|
+
bits += 6;
|
|
151
|
+
if (bits >= 8) {
|
|
152
|
+
bits -= 8;
|
|
153
|
+
bytes.push((buffer >> bits) & 0xff);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
try {
|
|
157
|
+
return new TextDecoder("utf-8", { fatal: true }).decode(new Uint8Array(bytes));
|
|
158
|
+
}
|
|
159
|
+
catch {
|
|
160
|
+
return null;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Durable terminal workspaces.
|
|
3
|
+
*
|
|
4
|
+
* A workspace is a named, server-owned arrangement of agent-CLI tiles. It is
|
|
5
|
+
* the Scout object the three clients had each invented privately —
|
|
6
|
+
* `openscout.terminal.workspaces.v1` in localStorage,
|
|
7
|
+
* `scout.terminals.workspaces.v1` in UserDefaults, and nothing at all on iOS —
|
|
8
|
+
* with the same version number, the same idea, and no synchronization.
|
|
9
|
+
*
|
|
10
|
+
* Where this stands TODAY, so nobody reads more into it than is built: the web
|
|
11
|
+
* client treats the record as its source of truth, seeding its deck from it and
|
|
12
|
+
* writing changes back. macOS still keeps its own UserDefaults store and has no
|
|
13
|
+
* reference to `/api/terminal-workspaces`, and iOS has nothing. "One workspace
|
|
14
|
+
* on all three clients" is the direction this type exists to make possible, not
|
|
15
|
+
* something it has delivered; the other two clients follow.
|
|
16
|
+
*
|
|
17
|
+
* The load-bearing design decision is that a cell stores INTENT, not just a
|
|
18
|
+
* binding. A saved cell that only remembers "tmux session scout-tmux-cell-7"
|
|
19
|
+
* is worth nothing after a reboot: tmux is empty and the name resolves to
|
|
20
|
+
* nothing. A cell that also remembers which host to use, what directory, and
|
|
21
|
+
* how to resume the harness can be rebuilt. This is the same conclusion
|
|
22
|
+
* tmux-resurrect/continuum reached — persist intent and replay it, never
|
|
23
|
+
* process state — and what macOS's `restoreCommandLine` already does in a
|
|
24
|
+
* cruder form.
|
|
25
|
+
*/
|
|
26
|
+
import type { TerminalSessionRecord, TerminalSurface } from "./terminal-sessions.js";
|
|
27
|
+
import type { TerminalHostId, TerminalSurfaceId } from "./terminal-surface-id.js";
|
|
28
|
+
/**
|
|
29
|
+
* Everything needed to re-materialize a cell when nothing live matches it.
|
|
30
|
+
* Every field is optional because a workspace authored before a given field
|
|
31
|
+
* existed must still resolve; what a cell cannot say, reconciliation refuses to
|
|
32
|
+
* invent.
|
|
33
|
+
*/
|
|
34
|
+
export type TerminalWorkspaceCellIntent = {
|
|
35
|
+
/** Host to materialize on. Absent means "whatever the operator's default is". */
|
|
36
|
+
hostId?: TerminalHostId | null;
|
|
37
|
+
/** Durable per-cell host session name. This is what makes a tile reattach. */
|
|
38
|
+
sessionName?: string | null;
|
|
39
|
+
/** Working directory to open in. */
|
|
40
|
+
cwd?: string | null;
|
|
41
|
+
/** Harness to resume, when the cell is an agent rather than a shell. */
|
|
42
|
+
harness?: string | null;
|
|
43
|
+
/** Harness-native resume command, e.g. `claude --resume <id>`. */
|
|
44
|
+
resumeCommand?: string | null;
|
|
45
|
+
};
|
|
46
|
+
export type TerminalWorkspaceCell = {
|
|
47
|
+
/** Stable cell id. Minted once, at authoring; never derived from a name. */
|
|
48
|
+
id: string;
|
|
49
|
+
/** Durable handle for the surface this cell last bound to. */
|
|
50
|
+
surfaceId?: TerminalSurfaceId | null;
|
|
51
|
+
/** Registry record that surface belonged to, when it had one. */
|
|
52
|
+
terminalSessionId?: string | null;
|
|
53
|
+
intent: TerminalWorkspaceCellIntent;
|
|
54
|
+
};
|
|
55
|
+
/**
|
|
56
|
+
* How a workspace arranges its tiles.
|
|
57
|
+
*
|
|
58
|
+
* Three shapes, not a list of named presets: one tile, N side-by-side lanes, or
|
|
59
|
+
* a 2D grid. The old SOLO/SPLIT/TRIO/QUAD taxonomy conflated the shape with a
|
|
60
|
+
* particular tile count, which is why authoring was capped at four and a
|
|
61
|
+
* nine-tile workspace silently truncated — "Quad" was both a layout and a size.
|
|
62
|
+
* Size is now just how many cells exist.
|
|
63
|
+
*/
|
|
64
|
+
export type TerminalWorkspaceLayoutMode = "solo" | "lanes" | "grid";
|
|
65
|
+
/**
|
|
66
|
+
* `"dynamic"` fits the column count to the number of tiles instead of pinning
|
|
67
|
+
* it, so adding a tile re-flows the workspace rather than leaving a hole.
|
|
68
|
+
*/
|
|
69
|
+
export type TerminalWorkspaceColumnCount = number | "dynamic";
|
|
70
|
+
export type TerminalWorkspaceLayout = {
|
|
71
|
+
mode: TerminalWorkspaceLayoutMode;
|
|
72
|
+
columns?: TerminalWorkspaceColumnCount;
|
|
73
|
+
};
|
|
74
|
+
export type TerminalWorkspaceRecord = {
|
|
75
|
+
id: string;
|
|
76
|
+
name: string;
|
|
77
|
+
purpose: string;
|
|
78
|
+
/**
|
|
79
|
+
* Resolved column count at the time of writing. Kept for records and clients
|
|
80
|
+
* that predate {@link layout}; `layout` wins when both are present.
|
|
81
|
+
*/
|
|
82
|
+
columns: number;
|
|
83
|
+
layout?: TerminalWorkspaceLayout;
|
|
84
|
+
cells: TerminalWorkspaceCell[];
|
|
85
|
+
createdAt: number;
|
|
86
|
+
updatedAt: number;
|
|
87
|
+
metadata?: Record<string, unknown>;
|
|
88
|
+
};
|
|
89
|
+
export type TerminalWorkspaceRecordInput = {
|
|
90
|
+
id?: string;
|
|
91
|
+
name: string;
|
|
92
|
+
purpose?: string;
|
|
93
|
+
columns?: number;
|
|
94
|
+
layout?: TerminalWorkspaceLayout;
|
|
95
|
+
cells?: TerminalWorkspaceCell[];
|
|
96
|
+
metadata?: Record<string, unknown>;
|
|
97
|
+
};
|
|
98
|
+
/** Highest column count a workspace may reopen with. */
|
|
99
|
+
export declare const TERMINAL_WORKSPACE_MAX_COLUMNS = 6;
|
|
100
|
+
export declare const TERMINAL_WORKSPACE_DEFAULT_COLUMNS = 2;
|
|
101
|
+
/**
|
|
102
|
+
* The layout a workspace should open with, folding a pre-layout record forward.
|
|
103
|
+
*
|
|
104
|
+
* A record written before layouts existed carries only a column count, so the
|
|
105
|
+
* mode is inferred from how that count relates to the cells: one cell is solo,
|
|
106
|
+
* a count that fits every cell on one row is lanes, anything else was a grid.
|
|
107
|
+
*/
|
|
108
|
+
export declare function terminalWorkspaceLayoutOf(input: {
|
|
109
|
+
layout?: TerminalWorkspaceLayout | null;
|
|
110
|
+
columns?: number | null;
|
|
111
|
+
cellCount?: number;
|
|
112
|
+
}): TerminalWorkspaceLayout;
|
|
113
|
+
/**
|
|
114
|
+
* Column count to render with.
|
|
115
|
+
*
|
|
116
|
+
* Solo is always one. A dynamic lane count is the tile count, so every tile
|
|
117
|
+
* gets its own lane; a dynamic grid is the squarest arrangement that holds
|
|
118
|
+
* them. Both clamp, because past a handful of columns a terminal is too narrow
|
|
119
|
+
* to read.
|
|
120
|
+
*/
|
|
121
|
+
export declare function resolveTerminalWorkspaceColumns(layout: TerminalWorkspaceLayout, input: {
|
|
122
|
+
tileCount: number;
|
|
123
|
+
}): number;
|
|
124
|
+
export declare function normalizeTerminalWorkspaceColumnCount(value: unknown): TerminalWorkspaceColumnCount;
|
|
125
|
+
/**
|
|
126
|
+
* Read a stored `layout_json` column back, or null when there is nothing
|
|
127
|
+
* trustworthy in it.
|
|
128
|
+
*
|
|
129
|
+
* Every store that persists a workspace decodes the layout through this, and
|
|
130
|
+
* that is the point: `layout_json` was added to the schema and then read by one
|
|
131
|
+
* handle and ignored by the other, so a layout written through the web server
|
|
132
|
+
* came back as `null` through the runtime store. One decoder means one answer.
|
|
133
|
+
*
|
|
134
|
+
* The mode is validated rather than trusted. It flows straight into the
|
|
135
|
+
* client's grid resolver, so a value that is not one of the three shapes must
|
|
136
|
+
* become "no stored layout" — which folds forward from the column count — and
|
|
137
|
+
* not a mode nothing can render.
|
|
138
|
+
*/
|
|
139
|
+
export declare function parseTerminalWorkspaceLayoutJson(value: string | null | undefined): TerminalWorkspaceLayout | null;
|
|
140
|
+
export declare function terminalWorkspaceLayoutLabel(layout: TerminalWorkspaceLayout): string;
|
|
141
|
+
export type TerminalWorkspaceCellStatus = "live" | "revivable" | "unavailable";
|
|
142
|
+
/** What reconciliation needs to know about a host, with no host access. */
|
|
143
|
+
export type TerminalWorkspaceHostState = {
|
|
144
|
+
id: TerminalHostId;
|
|
145
|
+
installed: boolean;
|
|
146
|
+
/** Whether Scout can materialize a session on this host headlessly. */
|
|
147
|
+
canCreate: boolean;
|
|
148
|
+
};
|
|
149
|
+
export type TerminalWorkspaceRevivePlan = {
|
|
150
|
+
hostId: TerminalHostId;
|
|
151
|
+
sessionName: string;
|
|
152
|
+
cwd: string | null;
|
|
153
|
+
/** Harness resume command to run inside the revived surface, when known. */
|
|
154
|
+
resumeCommand: string | null;
|
|
155
|
+
};
|
|
156
|
+
export type TerminalWorkspaceCellResolution = {
|
|
157
|
+
cellId: string;
|
|
158
|
+
status: TerminalWorkspaceCellStatus;
|
|
159
|
+
surfaceId: TerminalSurfaceId | null;
|
|
160
|
+
terminalSessionId: string | null;
|
|
161
|
+
surface: TerminalSurface | null;
|
|
162
|
+
/** Operator-facing reason, in product language. */
|
|
163
|
+
detail: string;
|
|
164
|
+
/** Present exactly when the status is `revivable`. */
|
|
165
|
+
revive: TerminalWorkspaceRevivePlan | null;
|
|
166
|
+
};
|
|
167
|
+
export type TerminalWorkspaceResolution = {
|
|
168
|
+
workspaceId: string;
|
|
169
|
+
cells: TerminalWorkspaceCellResolution[];
|
|
170
|
+
liveCount: number;
|
|
171
|
+
revivableCount: number;
|
|
172
|
+
unavailableCount: number;
|
|
173
|
+
};
|
|
174
|
+
/**
|
|
175
|
+
* Map every saved cell to a live surface, a restorable-but-dead one, or an
|
|
176
|
+
* unavailable one.
|
|
177
|
+
*
|
|
178
|
+
* This is the only honest answer to a reboot. tmux and zellij sessions do not
|
|
179
|
+
* survive a restart; a workspace that stored intent can rebuild itself, and one
|
|
180
|
+
* that stored only a session name cannot. Judgement lives here, on the server
|
|
181
|
+
* side of the wire, so all three clients inherit the same answer instead of
|
|
182
|
+
* each re-deriving it — and it never fabricates a record: a cell with nothing
|
|
183
|
+
* to rebuild from reports `unavailable` and says why.
|
|
184
|
+
*/
|
|
185
|
+
export declare function reconcileTerminalWorkspace(workspace: TerminalWorkspaceRecord, input: {
|
|
186
|
+
sessions: readonly TerminalSessionRecord[];
|
|
187
|
+
hosts: readonly TerminalWorkspaceHostState[];
|
|
188
|
+
/** Host used when a cell's intent names none. */
|
|
189
|
+
defaultHostId?: TerminalHostId | null;
|
|
190
|
+
/**
|
|
191
|
+
* This host's own Scout node id. The observations passed in were made HERE,
|
|
192
|
+
* so this is the node whose cells they can prove live; a cell scoped to any
|
|
193
|
+
* other node is not answered by them. Absent means the caller cannot say,
|
|
194
|
+
* and then no node-scoped cell is claimed by a node-less observation.
|
|
195
|
+
*/
|
|
196
|
+
localNodeId?: string | null;
|
|
197
|
+
}): TerminalWorkspaceResolution;
|
|
198
|
+
export declare function normalizeTerminalWorkspaceColumns(value: unknown): number;
|
|
199
|
+
//# sourceMappingURL=terminal-workspaces.d.ts.map
|