dsh-wsr-execution 0.2.1 → 0.2.2
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/lib/client.js +379 -105
- package/package.json +1 -1
- package/src/action-presentation/model.js +13 -7
- package/src/action-presentation/view.js +145 -18
- package/src/client/browser-entry.js +23 -4
- package/src/client/delivery/control-plane-port.js +11 -2
- package/src/client/delivery/session-delivery-view.js +148 -37
- package/src/client/delivery-inventory/model.js +17 -3
- package/src/host/delivery-control-plane.js +39 -12
- package/src/intake/binding-repository.js +118 -41
- package/src/intake/command.js +10 -0
- package/src/intake/plugin.js +131 -37
package/package.json
CHANGED
|
@@ -61,9 +61,12 @@ function hasValidTypedData(value) {
|
|
|
61
61
|
&& (channel === "action" || channel === "tool");
|
|
62
62
|
}
|
|
63
63
|
if (value.kind === "terminal-result") {
|
|
64
|
-
const
|
|
64
|
+
const hasFinalOutput = Object.hasOwn(value.data, "finalOutput");
|
|
65
|
+
const hasSummary = Object.hasOwn(value.data, "summary");
|
|
66
|
+
const outputValid = hasFinalOutput
|
|
65
67
|
? typeof value.data.finalOutput === "string" && value.data.finalOutput.length > 0
|
|
66
|
-
: typeof value.data.summary === "string" && value.data.summary.length > 0
|
|
68
|
+
: hasSummary ? typeof value.data.summary === "string" && value.data.summary.length > 0
|
|
69
|
+
: value.data.outcome === "SUCCEEDED";
|
|
67
70
|
return typeof value.data.outcome === "string" && TERMINAL_OUTCOMES.has(value.data.outcome) && outputValid;
|
|
68
71
|
}
|
|
69
72
|
if (value.kind === "delivery-running" || value.kind === "delivery-status") {
|
|
@@ -145,7 +148,7 @@ export function projectExecutionPresentation(event) {
|
|
|
145
148
|
return model({
|
|
146
149
|
correlation, layer: "final", state, title: "Final result",
|
|
147
150
|
summary: data.outcome[0] + data.outcome.slice(1).toLowerCase(), body,
|
|
148
|
-
defaultOpen:
|
|
151
|
+
defaultOpen: false, focusPolicy: "none", role: "article",
|
|
149
152
|
compatibility: typeof data.finalOutput === "string" ? "current" : "legacy-summary",
|
|
150
153
|
});
|
|
151
154
|
}
|
|
@@ -163,7 +166,7 @@ export function projectExecutionPresentation(event) {
|
|
|
163
166
|
correlation, layer: data.channel === "tool" ? "tool" : "action", state,
|
|
164
167
|
title: typeof data.label === "string" ? data.label : "Workflow Action",
|
|
165
168
|
summary: STATE_LABELS[state], body: text(data.content) ?? "WSR content unavailable",
|
|
166
|
-
defaultOpen:
|
|
169
|
+
defaultOpen: false, focusPolicy: "none", role: "status", compatibility: "current",
|
|
167
170
|
});
|
|
168
171
|
}
|
|
169
172
|
if (event.kind === "error") {
|
|
@@ -171,7 +174,7 @@ export function projectExecutionPresentation(event) {
|
|
|
171
174
|
correlation, layer: "progress", state: "failed", title: "Workflow presentation",
|
|
172
175
|
summary: typeof data.code === "string" ? data.code : "WSR_ERROR",
|
|
173
176
|
body: typeof data.message === "string" ? data.message : "WSR presentation unavailable",
|
|
174
|
-
defaultOpen:
|
|
177
|
+
defaultOpen: false, focusPolicy: "none", role: "alert", compatibility: "current",
|
|
175
178
|
});
|
|
176
179
|
}
|
|
177
180
|
|
|
@@ -182,7 +185,7 @@ export function projectExecutionPresentation(event) {
|
|
|
182
185
|
return model({
|
|
183
186
|
correlation, layer: "progress", state, title: "Workflow delivery",
|
|
184
187
|
summary: `${STATE_LABELS[state]}${deliveryId === undefined ? "" : ` · ${deliveryId}`}`,
|
|
185
|
-
body: undefined, defaultOpen:
|
|
188
|
+
body: undefined, defaultOpen: false, focusPolicy: "none", role: "status", compatibility: "current",
|
|
186
189
|
});
|
|
187
190
|
}
|
|
188
191
|
|
|
@@ -190,7 +193,6 @@ export function projectExecutionPresentation(event) {
|
|
|
190
193
|
export function resolveDisclosureOpen({ current, previousState, nextState, containsFocus }) {
|
|
191
194
|
if (nextState === "waiting") return true;
|
|
192
195
|
if (nextState === "completed" && previousState !== "completed") return containsFocus ? true : false;
|
|
193
|
-
if (["running", "recovering", "failed", "cancelled"].includes(nextState) && nextState !== previousState) return true;
|
|
194
196
|
return current;
|
|
195
197
|
}
|
|
196
198
|
|
|
@@ -217,6 +219,10 @@ export function createExecutionPresentationDefinition() {
|
|
|
217
219
|
if (event.kind === "delivery-list") {
|
|
218
220
|
return Object.freeze({ ...context.state, presentation: undefined });
|
|
219
221
|
}
|
|
222
|
+
if (event.kind === "terminal-result" && event.data.outcome === "SUCCEEDED"
|
|
223
|
+
&& !Object.hasOwn(event.data, "finalOutput") && !Object.hasOwn(event.data, "summary")) {
|
|
224
|
+
return Object.freeze({ ...context.state, presentation: undefined });
|
|
225
|
+
}
|
|
220
226
|
return Object.freeze({ ...context.state, presentation: projectExecutionPresentation(event) });
|
|
221
227
|
},
|
|
222
228
|
buildViewNode(context) {
|
|
@@ -1,7 +1,4 @@
|
|
|
1
|
-
import {
|
|
2
|
-
createExecutionPresentationDefinition,
|
|
3
|
-
resolveDisclosureOpen,
|
|
4
|
-
} from "./model.js";
|
|
1
|
+
import { parseExecutionPresentation, projectExecutionPresentation, resolveDisclosureOpen } from "./model.js";
|
|
5
2
|
|
|
6
3
|
const DOT_STATE = Object.freeze({
|
|
7
4
|
running: "ongoing",
|
|
@@ -12,18 +9,46 @@ const DOT_STATE = Object.freeze({
|
|
|
12
9
|
cancelled: "error",
|
|
13
10
|
});
|
|
14
11
|
|
|
12
|
+
const ACTIONS_STYLE_ID = "dsh-wsr-execution-final-actions";
|
|
13
|
+
const ACTIONS_CSS = ".wsr-answer-actions{align-items:center;gap:10px;height:28px;margin-top:16px;margin-left:-6px;display:flex}.wsr-answer-action{width:28px;height:28px;color:var(--dsw-alias-label-tertiary);cursor:pointer;background:transparent;border:none;border-radius:28px;justify-content:center;align-items:center;padding:6px;display:inline-flex}.wsr-answer-action:hover{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-secondary)}";
|
|
14
|
+
|
|
15
|
+
export function installActionPresentationStyle() {
|
|
16
|
+
if (typeof document === "undefined" || document.getElementById(ACTIONS_STYLE_ID) !== null) return;
|
|
17
|
+
const tag = document.createElement("style");
|
|
18
|
+
tag.id = ACTIONS_STYLE_ID;
|
|
19
|
+
tag.dataset.plugin = "dsh-wsr-execution";
|
|
20
|
+
tag.textContent = ACTIONS_CSS;
|
|
21
|
+
document.head.append(tag);
|
|
22
|
+
}
|
|
23
|
+
|
|
15
24
|
/**
|
|
16
25
|
* Build the WSR renderer from Harness-owned, public UI primitives. Dependency
|
|
17
26
|
* injection keeps the projection testable without copying any DSH component.
|
|
18
27
|
*/
|
|
19
|
-
export function createActionPresentationView({
|
|
28
|
+
export function createActionPresentationView({
|
|
29
|
+
React,
|
|
30
|
+
DisclosureRow,
|
|
31
|
+
MessageText,
|
|
32
|
+
StateDot,
|
|
33
|
+
JsonTree,
|
|
34
|
+
Tooltip,
|
|
35
|
+
IconCopyOutline16,
|
|
36
|
+
IconCheckOutline16,
|
|
37
|
+
writeClipboard,
|
|
38
|
+
observe = () => undefined,
|
|
39
|
+
}) {
|
|
20
40
|
if (typeof DisclosureRow !== "function") throw new TypeError("DSH_DISCLOSURE_ROW_REQUIRED");
|
|
41
|
+
installActionPresentationStyle();
|
|
21
42
|
|
|
22
|
-
return function WsrExecutionPresentationView({ node }) {
|
|
43
|
+
return function WsrExecutionPresentationView({ node, technicalDetails }) {
|
|
23
44
|
const presentation = node.data;
|
|
24
45
|
const [open, setOpen] = React.useState(presentation.defaultOpen);
|
|
46
|
+
const [copyState, setCopyState] = React.useState("idle");
|
|
25
47
|
const bodyRef = React.useRef(null);
|
|
26
48
|
const previousState = React.useRef(presentation.state);
|
|
49
|
+
const copyPending = React.useRef(false);
|
|
50
|
+
const copyEpoch = React.useRef(0);
|
|
51
|
+
const copyTimer = React.useRef(null);
|
|
27
52
|
|
|
28
53
|
React.useEffect(() => {
|
|
29
54
|
setOpen((current) => resolveDisclosureOpen({
|
|
@@ -37,9 +62,38 @@ export function createActionPresentationView({ React, DisclosureRow, MessageText
|
|
|
37
62
|
previousState.current = presentation.state;
|
|
38
63
|
}, [presentation.state]);
|
|
39
64
|
|
|
65
|
+
React.useEffect(() => {
|
|
66
|
+
copyEpoch.current += 1;
|
|
67
|
+
copyPending.current = false;
|
|
68
|
+
if (copyTimer.current !== null) clearTimeout(copyTimer.current);
|
|
69
|
+
copyTimer.current = null;
|
|
70
|
+
setCopyState("idle");
|
|
71
|
+
return () => {
|
|
72
|
+
copyEpoch.current += 1;
|
|
73
|
+
copyPending.current = false;
|
|
74
|
+
if (copyTimer.current !== null) clearTimeout(copyTimer.current);
|
|
75
|
+
};
|
|
76
|
+
}, [presentation.body, presentation.correlation]);
|
|
77
|
+
|
|
40
78
|
observe(presentation);
|
|
41
79
|
|
|
42
|
-
if (presentation.layer === "final") {
|
|
80
|
+
if (presentation.layer === "final" && presentation.state === "completed") {
|
|
81
|
+
const label = copyState === "copied" ? "Copied" : copyState === "failed" ? "Copy failed" : "Copy";
|
|
82
|
+
const onCopy = async () => {
|
|
83
|
+
if (copyState === "copied" || copyPending.current) return;
|
|
84
|
+
const epoch = copyEpoch.current;
|
|
85
|
+
copyPending.current = true;
|
|
86
|
+
let accepted = false;
|
|
87
|
+
try { accepted = await writeClipboard(presentation.body); }
|
|
88
|
+
catch { accepted = false; }
|
|
89
|
+
if (epoch !== copyEpoch.current) return;
|
|
90
|
+
copyPending.current = false;
|
|
91
|
+
setCopyState(accepted ? "copied" : "failed");
|
|
92
|
+
copyTimer.current = globalThis.setTimeout(() => {
|
|
93
|
+
copyTimer.current = null;
|
|
94
|
+
setCopyState("idle");
|
|
95
|
+
}, 1_000);
|
|
96
|
+
};
|
|
43
97
|
return React.createElement("article", {
|
|
44
98
|
"data-wsr-presentation": "true",
|
|
45
99
|
"data-wsr-layer": "final",
|
|
@@ -48,12 +102,23 @@ export function createActionPresentationView({ React, DisclosureRow, MessageText
|
|
|
48
102
|
"data-wsr-chat-role": "assistant",
|
|
49
103
|
"data-wsr-compatibility": presentation.compatibility,
|
|
50
104
|
"aria-label": presentation.title,
|
|
51
|
-
},
|
|
105
|
+
},
|
|
106
|
+
React.createElement(MessageText, { text: presentation.body }),
|
|
107
|
+
React.createElement("div", {
|
|
108
|
+
className: "wsr-answer-actions",
|
|
109
|
+
"data-wsr-answer-actions": "true",
|
|
110
|
+
}, React.createElement(Tooltip, { label, side: "bottom" }, React.createElement("button", {
|
|
111
|
+
type: "button",
|
|
112
|
+
className: "wsr-answer-action",
|
|
113
|
+
"aria-label": label,
|
|
114
|
+
"data-copy-state": copyState,
|
|
115
|
+
onClick: onCopy,
|
|
116
|
+
}, React.createElement(copyState === "copied" ? IconCheckOutline16 : IconCopyOutline16, null)))));
|
|
52
117
|
}
|
|
53
118
|
|
|
54
119
|
const waiting = presentation.state === "waiting";
|
|
55
|
-
const expandable = presentation.body !== undefined && !waiting;
|
|
56
|
-
const body = presentation.body === undefined ? undefined : React.createElement("div", {
|
|
120
|
+
const expandable = (presentation.body !== undefined || technicalDetails !== undefined) && !waiting;
|
|
121
|
+
const body = presentation.body === undefined && technicalDetails === undefined ? undefined : React.createElement("div", {
|
|
57
122
|
ref: bodyRef,
|
|
58
123
|
"data-wsr-presentation": "true",
|
|
59
124
|
"data-wsr-layer": presentation.layer,
|
|
@@ -64,9 +129,13 @@ export function createActionPresentationView({ React, DisclosureRow, MessageText
|
|
|
64
129
|
tabIndex: waiting ? 0 : undefined,
|
|
65
130
|
"aria-label": waiting ? presentation.summary : undefined,
|
|
66
131
|
"aria-live": waiting ? "polite" : undefined,
|
|
67
|
-
}, React.createElement("pre", {
|
|
132
|
+
}, presentation.body === undefined ? null : React.createElement("pre", {
|
|
68
133
|
style: { margin: 0, maxHeight: "20rem", overflow: "auto", whiteSpace: "pre-wrap", wordBreak: "break-word" },
|
|
69
|
-
}, presentation.body)
|
|
134
|
+
}, presentation.body), technicalDetails === undefined ? null : React.createElement("details", null,
|
|
135
|
+
React.createElement("summary", null, "Technical details"),
|
|
136
|
+
JsonTree === undefined
|
|
137
|
+
? React.createElement("pre", null, JSON.stringify(technicalDetails, null, 2))
|
|
138
|
+
: React.createElement(JsonTree, { data: technicalDetails, label: "WSR presentation", copyable: true, expandTopLevel: true })));
|
|
70
139
|
|
|
71
140
|
return React.createElement(DisclosureRow, {
|
|
72
141
|
icon: React.createElement(StateDot, { state: DOT_STATE[presentation.state], size: 10 }),
|
|
@@ -88,11 +157,69 @@ export function createActionPresentationView({ React, DisclosureRow, MessageText
|
|
|
88
157
|
};
|
|
89
158
|
}
|
|
90
159
|
|
|
91
|
-
|
|
160
|
+
const TERMINAL_PRESENTATION = Object.freeze({
|
|
161
|
+
SUCCEEDED: Object.freeze({ state: "completed", label: "Succeeded" }),
|
|
162
|
+
FAILED: Object.freeze({ state: "failed", label: "Failed" }),
|
|
163
|
+
CANCELLED: Object.freeze({ state: "cancelled", label: "Cancelled" }),
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
function reconcileDeliveryPresentation(presentation, admitted, inventoryState) {
|
|
167
|
+
const deliveryId = admitted?.kind === "delivery-running" && typeof admitted.data.deliveryId === "string"
|
|
168
|
+
? admitted.data.deliveryId
|
|
169
|
+
: undefined;
|
|
170
|
+
const deliveries = ["ready", "reconnecting"].includes(inventoryState?.kind)
|
|
171
|
+
&& Array.isArray(inventoryState.snapshot?.deliveries)
|
|
172
|
+
? inventoryState.snapshot.deliveries
|
|
173
|
+
: [];
|
|
174
|
+
const matches = deliveryId === undefined ? [] : deliveries.filter((delivery) => delivery?.deliveryId === deliveryId);
|
|
175
|
+
const terminal = matches.length === 1 && matches[0]?.lifecycle === "TERMINAL"
|
|
176
|
+
? TERMINAL_PRESENTATION[matches[0]?.terminal?.outcome]
|
|
177
|
+
: undefined;
|
|
178
|
+
if (terminal === undefined) return presentation;
|
|
179
|
+
return Object.freeze({
|
|
180
|
+
...presentation,
|
|
181
|
+
state: terminal.state,
|
|
182
|
+
summary: `${terminal.label} · ${deliveryId}`,
|
|
183
|
+
defaultOpen: false,
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function commandPresentation(node, admitted, inventoryState) {
|
|
188
|
+
if (node.outcome === null) return Object.freeze({
|
|
189
|
+
correlation: String(node.commandId), layer: "progress", state: "running",
|
|
190
|
+
title: "Workflow delivery", summary: "Running", body: undefined,
|
|
191
|
+
defaultOpen: false, focusPolicy: "none", role: "status", compatibility: "current",
|
|
192
|
+
});
|
|
193
|
+
const event = admitted ?? parseExecutionPresentation(node.outcome?.text);
|
|
194
|
+
if (event.kind === "delivery-list") {
|
|
195
|
+
const count = Array.isArray(event.data.items) ? event.data.items.length : 0;
|
|
196
|
+
return Object.freeze({
|
|
197
|
+
correlation: event.correlation, layer: "progress", state: "completed",
|
|
198
|
+
title: "Delivery list", summary: `${count} ${count === 1 ? "delivery" : "deliveries"}`,
|
|
199
|
+
body: count === 0 ? "No deliveries." : JSON.stringify(event.data.items, null, 2),
|
|
200
|
+
defaultOpen: false, focusPolicy: "none", role: "status", compatibility: "current",
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
return reconcileDeliveryPresentation(projectExecutionPresentation(event), event, inventoryState);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/** Replace the generic command card so one-line durable JSON remains inspectable. */
|
|
207
|
+
export function createWsrCommandView(options) {
|
|
208
|
+
const View = createActionPresentationView(options);
|
|
209
|
+
const { React, inventory } = options;
|
|
210
|
+
return function WsrCommandView({ node }) {
|
|
211
|
+
const admitted = node.outcome === null ? undefined : parseExecutionPresentation(node.outcome?.text);
|
|
212
|
+
const inventoryState = inventory === undefined
|
|
213
|
+
? undefined
|
|
214
|
+
: React.useSyncExternalStore(inventory.subscribe, inventory.getSnapshot, inventory.getSnapshot);
|
|
215
|
+
return View({ node: { data: commandPresentation(node, admitted, inventoryState) }, technicalDetails: admitted });
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/** Hide the earlier native command row and render the ordered presentation row. */
|
|
92
220
|
export function registerActionPresentation(ctx, View) {
|
|
93
|
-
ctx.
|
|
94
|
-
|
|
95
|
-
name: "conversation.chat.
|
|
96
|
-
|
|
97
|
-
}, View));
|
|
221
|
+
ctx.slots.inject("conversation.chat.commandview", () => {
|
|
222
|
+
ctx.slots.register({ name: "conversation.chat.commandview", key: "wsr" }, () => null);
|
|
223
|
+
ctx.slots.register({ name: "conversation.chat.commandview", key: "wsr-presentation" }, View);
|
|
224
|
+
});
|
|
98
225
|
}
|
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
import React from "react";
|
|
2
|
-
import { DisclosureRow, MessageText, StateDot } from "@deepseek-ai/dsh-client-ui-primitives";
|
|
2
|
+
import { Button, DisclosureRow, IconCheckOutline16, IconCopyOutline16, JsonTree, MessageText, Pill, StateDot, Tooltip, writeClipboard } from "@deepseek-ai/dsh-client-ui-primitives";
|
|
3
3
|
import * as workspaceUi from "@deepseek-ai/dsh-client-ui-workspace";
|
|
4
4
|
|
|
5
|
-
import {
|
|
5
|
+
import { createWsrCommandView, registerActionPresentation } from "../action-presentation/view.js";
|
|
6
6
|
import { createDeliveryControlPlaneClient } from "./delivery/control-plane-port.js";
|
|
7
7
|
import { registerSessionDeliveryView } from "./delivery/session-delivery-view.js";
|
|
8
8
|
import { applyDeliverySidebar } from "./delivery-inventory/sidebar.js";
|
|
9
9
|
|
|
10
10
|
export const name = "wsr-execution-client";
|
|
11
11
|
export const inject = Object.freeze([
|
|
12
|
-
"connection", "
|
|
12
|
+
"connection", "sessions", "slots", "workspaces", "locale",
|
|
13
13
|
]);
|
|
14
14
|
|
|
15
15
|
export function apply(ctx) {
|
|
@@ -22,11 +22,30 @@ export function apply(ctx) {
|
|
|
22
22
|
applyDeliverySidebar(ctx, { React, workspaceUi, inventory: controlPlane.inventory });
|
|
23
23
|
registerSessionDeliveryView(ctx, {
|
|
24
24
|
React,
|
|
25
|
+
Button,
|
|
26
|
+
DisclosureRow,
|
|
27
|
+
IconCheckOutline16,
|
|
28
|
+
IconCopyOutline16,
|
|
29
|
+
Pill,
|
|
30
|
+
StateDot,
|
|
31
|
+
Tooltip,
|
|
32
|
+
writeClipboard,
|
|
25
33
|
bindProjection(sessionId) {
|
|
26
34
|
const source = controlPlane.bindSession(String(sessionId));
|
|
27
35
|
void source.refresh();
|
|
28
36
|
return source;
|
|
29
37
|
},
|
|
30
38
|
});
|
|
31
|
-
registerActionPresentation(ctx,
|
|
39
|
+
registerActionPresentation(ctx, createWsrCommandView({
|
|
40
|
+
React,
|
|
41
|
+
DisclosureRow,
|
|
42
|
+
IconCheckOutline16,
|
|
43
|
+
IconCopyOutline16,
|
|
44
|
+
JsonTree,
|
|
45
|
+
MessageText,
|
|
46
|
+
StateDot,
|
|
47
|
+
Tooltip,
|
|
48
|
+
writeClipboard,
|
|
49
|
+
inventory: controlPlane.inventory,
|
|
50
|
+
}));
|
|
32
51
|
}
|
|
@@ -1,4 +1,10 @@
|
|
|
1
1
|
const CHANNEL = "/wsr-execution";
|
|
2
|
+
const ERROR_CODES = new Set([
|
|
3
|
+
"DELIVERY_PROJECTION_CORRUPT",
|
|
4
|
+
"DELIVERY_PROJECTION_STALE_BINDING",
|
|
5
|
+
"DELIVERY_PROJECTION_RECOVERY_MISMATCH",
|
|
6
|
+
"DELIVERY_PROJECTION_UNAVAILABLE",
|
|
7
|
+
]);
|
|
2
8
|
|
|
3
9
|
function createStore(initial) {
|
|
4
10
|
let snapshot = initial;
|
|
@@ -23,7 +29,9 @@ export function createDeliveryControlPlaneClient(rpc) {
|
|
|
23
29
|
const sessions = new Map();
|
|
24
30
|
const read = async (endpoint, payload) => {
|
|
25
31
|
const result = await rpc.call(CHANNEL, endpoint, payload);
|
|
26
|
-
if (result?.ok !== true) throw new Error(message(result?.error))
|
|
32
|
+
if (result?.ok !== true) throw Object.assign(new Error(message(result?.error)), {
|
|
33
|
+
code: ERROR_CODES.has(result?.error?.code) ? result.error.code : "DELIVERY_PROJECTION_UNAVAILABLE",
|
|
34
|
+
});
|
|
27
35
|
return result.value;
|
|
28
36
|
};
|
|
29
37
|
const client = {
|
|
@@ -37,6 +45,7 @@ export function createDeliveryControlPlaneClient(rpc) {
|
|
|
37
45
|
const previous = inventory.getSnapshot();
|
|
38
46
|
inventory.publish({
|
|
39
47
|
kind: previous.kind === "ready" ? "reconnecting" : "error",
|
|
48
|
+
code: typeof error?.code === "string" ? error.code : "DELIVERY_PROJECTION_UNAVAILABLE",
|
|
40
49
|
message: message(error),
|
|
41
50
|
...(previous.kind === "ready" ? { snapshot: previous.snapshot } : {}),
|
|
42
51
|
});
|
|
@@ -53,7 +62,7 @@ export function createDeliveryControlPlaneClient(rpc) {
|
|
|
53
62
|
subscribe: store.subscribe,
|
|
54
63
|
async refresh() {
|
|
55
64
|
try { store.publish({ kind: "ready", view: await read("session/read", { sessionCorrelation }) }); }
|
|
56
|
-
catch (error) { store.publish({ kind: "error", code: "DELIVERY_PROJECTION_UNAVAILABLE", message: message(error) }); }
|
|
65
|
+
catch (error) { store.publish({ kind: "error", code: typeof error?.code === "string" ? error.code : "DELIVERY_PROJECTION_UNAVAILABLE", message: message(error) }); }
|
|
57
66
|
},
|
|
58
67
|
});
|
|
59
68
|
sessions.set(sessionCorrelation, source);
|
|
@@ -6,6 +6,39 @@ const LIFECYCLES = new Set([
|
|
|
6
6
|
"BOUND", "START_UNCERTAIN", "RUNNING_CORRELATED", "START_FAILED",
|
|
7
7
|
"RESULT_UNRESOLVED", "TERMINAL_HANDLING", "TERMINAL",
|
|
8
8
|
]);
|
|
9
|
+
const DELIVERY_STYLE_ID = "dsh-wsr-execution-delivery-view";
|
|
10
|
+
const DELIVERY_CSS = `
|
|
11
|
+
.wsr-delivery-view { box-sizing: border-box; width: 100%; max-width: 960px; margin: 0 auto; padding: 20px; color: var(--dsw-alias-label-primary); }
|
|
12
|
+
.wsr-delivery-heading { margin: 0 0 16px; font-size: 20px; line-height: 28px; }
|
|
13
|
+
.wsr-delivery-summary { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, 180px), 1fr)); gap: 8px; margin: 0 0 16px; }
|
|
14
|
+
.wsr-delivery-summary-item { min-width: 0; padding: 10px 12px; border: 1px solid var(--dsw-alias-border-l2); border-radius: 8px; background: var(--dsw-alias-bg-layer-1); }
|
|
15
|
+
.wsr-delivery-summary-item dt, .wsr-delivery-identity dt { margin: 0 0 3px; color: var(--dsw-alias-label-tertiary); font-size: 12px; line-height: 16px; }
|
|
16
|
+
.wsr-delivery-summary-item dd, .wsr-delivery-identity dd { min-width: 0; margin: 0; font-size: 13px; line-height: 20px; overflow-wrap: anywhere; }
|
|
17
|
+
.wsr-delivery-status { display: inline-flex; min-width: 0; align-items: center; gap: 6px; }
|
|
18
|
+
.wsr-delivery-status > span:last-child { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
19
|
+
.wsr-delivery-identities { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, 260px), 1fr)); gap: 8px 16px; margin: 8px 0 0; }
|
|
20
|
+
.wsr-delivery-identity { min-width: 0; margin: 0; }
|
|
21
|
+
.wsr-delivery-identity dd { display: grid; grid-template-columns: minmax(0, 1fr) auto; align-items: center; gap: 6px; }
|
|
22
|
+
.wsr-delivery-identity code { display: block; min-width: 0; max-width: 100%; color: inherit; font-family: var(--dsw-font-family-mono, ui-monospace, monospace); overflow-wrap: anywhere; white-space: normal; }
|
|
23
|
+
.wsr-delivery-preview { display: block; min-width: 0; max-width: 100%; margin-inline-start: 8px; overflow: hidden; color: var(--dsw-alias-label-tertiary); text-overflow: ellipsis; white-space: nowrap; }
|
|
24
|
+
.wsr-delivery-copy-feedback { min-height: 20px; margin: 8px 0 0; color: var(--dsw-alias-label-secondary); font-size: 12px; line-height: 20px; }
|
|
25
|
+
.wsr-delivery-condition { margin-top: 12px; padding: 10px 12px; border-left: 3px solid var(--dsw-alias-state-warn-primary); border-radius: 4px; background: var(--dsw-alias-bg-layer-1); }
|
|
26
|
+
.wsr-delivery-condition h3 { margin: 0 0 4px; font-size: 13px; line-height: 20px; }
|
|
27
|
+
.wsr-delivery-condition code, .wsr-delivery-state code { overflow-wrap: anywhere; }
|
|
28
|
+
.wsr-delivery-state { display: grid; gap: 8px; }
|
|
29
|
+
.wsr-delivery-state p { margin: 0; }
|
|
30
|
+
@media (max-width: 720px) { .wsr-delivery-summary { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
|
|
31
|
+
@media (max-width: 420px) { .wsr-delivery-view { padding: 12px; } .wsr-delivery-summary, .wsr-delivery-identities { grid-template-columns: minmax(0, 1fr); } }
|
|
32
|
+
@media (prefers-reduced-motion: reduce) { .wsr-delivery-view, .wsr-delivery-view * { scroll-behavior: auto !important; transition: none !important; } }
|
|
33
|
+
`;
|
|
34
|
+
|
|
35
|
+
function ensureDeliveryStyles() {
|
|
36
|
+
if (typeof document === "undefined" || document.getElementById(DELIVERY_STYLE_ID) !== null) return;
|
|
37
|
+
const tag = document.createElement("style");
|
|
38
|
+
tag.id = DELIVERY_STYLE_ID;
|
|
39
|
+
tag.textContent = DELIVERY_CSS;
|
|
40
|
+
document.head.appendChild(tag);
|
|
41
|
+
}
|
|
9
42
|
|
|
10
43
|
function nonEmpty(value) {
|
|
11
44
|
return typeof value === "string" && value.length > 0;
|
|
@@ -46,79 +79,157 @@ function safeSubscribe(source, notify) {
|
|
|
46
79
|
catch { return () => undefined; }
|
|
47
80
|
}
|
|
48
81
|
|
|
49
|
-
function
|
|
50
|
-
return
|
|
51
|
-
React.createElement("dt",
|
|
52
|
-
React.createElement("dd",
|
|
53
|
-
];
|
|
82
|
+
function summaryItem(React, label, value, extra = {}) {
|
|
83
|
+
return React.createElement("div", { className: "wsr-delivery-summary-item", ...extra },
|
|
84
|
+
React.createElement("dt", null, label),
|
|
85
|
+
React.createElement("dd", null, value));
|
|
54
86
|
}
|
|
55
87
|
|
|
56
|
-
function statePanel(React, role, code, message) {
|
|
88
|
+
function statePanel(React, StateDot, role, code, message) {
|
|
57
89
|
return React.createElement("section", {
|
|
90
|
+
className: "wsr-delivery-view wsr-delivery-state",
|
|
58
91
|
"aria-labelledby": "wsr-delivery-view-title", "aria-live": role === "alert" ? "assertive" : "polite",
|
|
59
92
|
"data-wsr-delivery-view": "true", role,
|
|
60
|
-
}, React.createElement("h2", { id: "wsr-delivery-view-title" }, "Delivery"),
|
|
61
|
-
React.createElement("p", null,
|
|
93
|
+
}, React.createElement("h2", { className: "wsr-delivery-heading", id: "wsr-delivery-view-title" }, "Delivery"),
|
|
94
|
+
React.createElement("p", null,
|
|
95
|
+
React.createElement(StateDot, { state: role === "alert" ? "error" : "ongoing", size: 10 }), " ", message),
|
|
96
|
+
code === undefined ? null : React.createElement("code", null, code));
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function statusState(delivery, failed) {
|
|
100
|
+
if (failed) return "error";
|
|
101
|
+
if (delivery.terminal?.outcome === "SUCCEEDED") return "done";
|
|
102
|
+
if (delivery.terminal !== null || ["START_UNCERTAIN", "RESULT_UNRESOLVED", "START_FAILED"].includes(delivery.lifecycle)) return "warning";
|
|
103
|
+
return "ongoing";
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function identityCard(React, primitives, label, value, displayValue = value) {
|
|
107
|
+
const { Button, IconCheckOutline16, IconCopyOutline16, Tooltip, onCopy, copiedLabel } = primitives;
|
|
108
|
+
const exact = React.createElement("code", {
|
|
109
|
+
"aria-label": `${label}: ${value}`,
|
|
110
|
+
"data-wsr-delivery-identity": label,
|
|
111
|
+
title: value,
|
|
112
|
+
}, displayValue);
|
|
113
|
+
const copied = copiedLabel === label;
|
|
114
|
+
const control = React.createElement(Tooltip, { label: copied ? `${label} copied` : `Copy ${label}`, side: "bottom" },
|
|
115
|
+
React.createElement(Button, {
|
|
116
|
+
"aria-label": `Copy ${label}`,
|
|
117
|
+
icon: React.createElement(copied ? IconCheckOutline16 : IconCopyOutline16, null),
|
|
118
|
+
onClick: () => onCopy(label, value),
|
|
119
|
+
size: "sm",
|
|
120
|
+
type: "button",
|
|
121
|
+
variant: "toolbar",
|
|
122
|
+
}, copied ? "Copied" : "Copy"));
|
|
123
|
+
return React.createElement("div", { className: "wsr-delivery-identity", key: label },
|
|
124
|
+
React.createElement("dt", null, label),
|
|
125
|
+
React.createElement("dd", null, exact, control));
|
|
62
126
|
}
|
|
63
127
|
|
|
64
128
|
/** Render the exact owner `SessionDeliveryView` without a shadow projection. */
|
|
65
|
-
export function createSessionDeliveryView(React) {
|
|
129
|
+
export function createSessionDeliveryView(React, primitives = {}) {
|
|
66
130
|
if (typeof React?.createElement !== "function" || typeof React?.useSyncExternalStore !== "function") {
|
|
67
131
|
throw new TypeError("DELIVERY_VIEW_REACT_INVALID");
|
|
68
132
|
}
|
|
133
|
+
const DisclosureRow = primitives.DisclosureRow ?? "div";
|
|
134
|
+
const Button = primitives.Button ?? "button";
|
|
135
|
+
const IconCheckOutline16 = primitives.IconCheckOutline16 ?? "span";
|
|
136
|
+
const IconCopyOutline16 = primitives.IconCopyOutline16 ?? "span";
|
|
137
|
+
const Pill = primitives.Pill ?? "span";
|
|
138
|
+
const StateDot = primitives.StateDot ?? "span";
|
|
139
|
+
const Tooltip = primitives.Tooltip ?? "span";
|
|
140
|
+
const writeClipboard = primitives.writeClipboard ?? (async () => false);
|
|
141
|
+
ensureDeliveryStyles();
|
|
69
142
|
return function SessionDeliveryView({ sessionId, source }) {
|
|
143
|
+
const [identitiesOpen, setIdentitiesOpen] = typeof React.useState === "function"
|
|
144
|
+
? React.useState(false)
|
|
145
|
+
: [false, () => undefined];
|
|
146
|
+
const [copiedLabel, setCopiedLabel] = typeof React.useState === "function"
|
|
147
|
+
? React.useState("")
|
|
148
|
+
: ["", () => undefined];
|
|
70
149
|
const state = React.useSyncExternalStore(
|
|
71
150
|
(notify) => safeSubscribe(source, notify),
|
|
72
151
|
() => safeSnapshot(source),
|
|
73
152
|
() => safeSnapshot(source),
|
|
74
153
|
);
|
|
75
|
-
if (state.kind === "loading") return statePanel(React, "status", undefined, "Loading Delivery…");
|
|
76
|
-
if (state.kind === "error") return statePanel(React, "alert", state.code ?? "DELIVERY_PROJECTION_UNAVAILABLE", state.message ?? "Execution projection unavailable");
|
|
154
|
+
if (state.kind === "loading") return statePanel(React, StateDot, "status", undefined, "Loading Delivery…");
|
|
155
|
+
if (state.kind === "error") return statePanel(React, StateDot, "alert", state.code ?? "DELIVERY_PROJECTION_UNAVAILABLE", state.message ?? "Execution projection unavailable");
|
|
77
156
|
const view = state.view;
|
|
78
157
|
if (state.kind !== "ready" || view?.sessionCorrelation !== sessionId) {
|
|
79
|
-
return statePanel(React, "alert", "DELIVERY_PROJECTION_CORRUPT", "Delivery projection invalid");
|
|
158
|
+
return statePanel(React, StateDot, "alert", "DELIVERY_PROJECTION_CORRUPT", "Delivery projection invalid");
|
|
80
159
|
}
|
|
81
|
-
if (view.kind === "UNBOUND") return statePanel(React, "status", undefined, "No Delivery bound to this Session");
|
|
160
|
+
if (view.kind === "UNBOUND") return statePanel(React, StateDot, "status", undefined, "No Delivery bound to this Session");
|
|
82
161
|
if (view.kind !== "BOUND" || !validDelivery(view.delivery, sessionId)) {
|
|
83
|
-
return statePanel(React, "alert", "DELIVERY_PROJECTION_CORRUPT", "Delivery projection invalid");
|
|
162
|
+
return statePanel(React, StateDot, "alert", "DELIVERY_PROJECTION_CORRUPT", "Delivery projection invalid");
|
|
84
163
|
}
|
|
85
164
|
const delivery = view.delivery;
|
|
86
165
|
const failed = delivery.terminal?.outcome === "FAILED" || delivery.error !== null;
|
|
87
166
|
const identityRows = [
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
167
|
+
["Delivery", delivery.deliveryId],
|
|
168
|
+
["Task", delivery.task.identity, delivery.task.displayName === null ? delivery.task.identity : `${delivery.task.displayName} · ${delivery.task.identity}`],
|
|
169
|
+
["Workflow", delivery.workflow.identity],
|
|
170
|
+
["Package", `${delivery.workflow.packageName}@${delivery.workflow.exactPackageVersion}`],
|
|
171
|
+
["Package digest", delivery.workflow.packageDigest],
|
|
172
|
+
["Snapshot", delivery.workflow.snapshotIdentity],
|
|
173
|
+
["Snapshot digest", delivery.workflow.snapshotDigest],
|
|
174
|
+
["Binding", delivery.deliveryBindingIdentity],
|
|
175
|
+
...(nonEmpty(delivery.worktree) ? [["Worktree", delivery.worktree]] : []),
|
|
96
176
|
];
|
|
97
|
-
const
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
177
|
+
const statusLabel = delivery.terminal?.outcome ?? delivery.lifecycle;
|
|
178
|
+
const workflowLabel = `${delivery.workflow.identity} · ${delivery.workflow.packageName}@${delivery.workflow.exactPackageVersion}`;
|
|
179
|
+
const summary = [
|
|
180
|
+
summaryItem(React, "Status", React.createElement("span", { className: "wsr-delivery-status" },
|
|
181
|
+
React.createElement(StateDot, { state: statusState(delivery, failed), size: 10 }),
|
|
182
|
+
React.createElement(Pill, { "aria-label": `Delivery status ${statusLabel}` }, statusLabel))),
|
|
183
|
+
summaryItem(React, "Workflow", workflowLabel),
|
|
184
|
+
...(delivery.current === null ? [] : [summaryItem(
|
|
185
|
+
React,
|
|
186
|
+
delivery.current.kind === "ACTION" ? "Current Action" : "Current Intervention",
|
|
187
|
+
delivery.current.identity,
|
|
188
|
+
{ "data-wsr-delivery-conditional": "current" },
|
|
189
|
+
)]),
|
|
190
|
+
...(delivery.terminal === null ? [] : [summaryItem(React, "Outcome", delivery.terminal.outcome, { "data-wsr-delivery-conditional": "terminal" })]),
|
|
191
|
+
summaryItem(React, "Elapsed", duration(delivery.timing.elapsedMs)),
|
|
192
|
+
summaryItem(React, "Started", new Date(delivery.timing.startedAt).toISOString()),
|
|
193
|
+
...(delivery.terminal === null ? [] : [summaryItem(React, "Ended", new Date(delivery.terminal.finishedAt).toISOString())]),
|
|
108
194
|
];
|
|
109
195
|
return React.createElement("section", {
|
|
196
|
+
className: "wsr-delivery-view",
|
|
110
197
|
"aria-labelledby": "wsr-delivery-view-title", "aria-live": failed ? "assertive" : "polite",
|
|
111
198
|
"data-wsr-delivery-id": delivery.deliveryId, "data-wsr-delivery-view": "true", role: failed ? "alert" : "region",
|
|
112
|
-
}, React.createElement("h2", { id: "wsr-delivery-view-title" }, "Delivery"),
|
|
113
|
-
React.createElement("dl", { "aria-label": "Delivery
|
|
114
|
-
React.createElement(
|
|
199
|
+
}, React.createElement("h2", { className: "wsr-delivery-heading", id: "wsr-delivery-view-title" }, "Delivery"),
|
|
200
|
+
React.createElement("dl", { "aria-label": "Delivery summary", "data-wsr-delivery-summary": "true", className: "wsr-delivery-summary" }, summary),
|
|
201
|
+
React.createElement(DisclosureRow, {
|
|
202
|
+
title: "Identity details",
|
|
203
|
+
icon: React.createElement(StateDot, { state: statusState(delivery, failed), size: 10 }),
|
|
204
|
+
open: identitiesOpen,
|
|
205
|
+
expandable: true,
|
|
206
|
+
expandOnRowClick: true,
|
|
207
|
+
onToggle: () => setIdentitiesOpen((open) => !open),
|
|
208
|
+
collapsedContent: React.createElement("code", { className: "wsr-delivery-preview" }, delivery.deliveryId),
|
|
209
|
+
}, React.createElement("dl", { "aria-label": "Delivery identity", className: "wsr-delivery-identities" },
|
|
210
|
+
identityRows.map(([label, value, displayValue]) => identityCard(React, {
|
|
211
|
+
Button,
|
|
212
|
+
IconCheckOutline16,
|
|
213
|
+
IconCopyOutline16,
|
|
214
|
+
Tooltip,
|
|
215
|
+
copiedLabel,
|
|
216
|
+
async onCopy(copyLabel, value) {
|
|
217
|
+
setCopiedLabel(await writeClipboard(value) ? copyLabel : `${copyLabel} copy failed`);
|
|
218
|
+
},
|
|
219
|
+
}, label, value, displayValue))),
|
|
220
|
+
React.createElement("p", {
|
|
221
|
+
"aria-live": "polite", className: "wsr-delivery-copy-feedback", role: "status",
|
|
222
|
+
}, copiedLabel === "" ? "" : copiedLabel.endsWith("copy failed") ? copiedLabel : `${copiedLabel} copied`)),
|
|
223
|
+
delivery.error === null ? null : React.createElement("section", {
|
|
224
|
+
className: "wsr-delivery-condition", "data-wsr-delivery-conditional": "error", role: "alert",
|
|
225
|
+
}, React.createElement("h3", null, "Failure diagnostic"), React.createElement("code", null, delivery.error.code)));
|
|
115
226
|
};
|
|
116
227
|
}
|
|
117
228
|
|
|
118
229
|
export function registerSessionDeliveryView(ctx, options) {
|
|
119
230
|
if (typeof ctx?.slots?.inject !== "function" || typeof ctx?.slots?.register !== "function"
|
|
120
231
|
|| typeof options?.bindProjection !== "function") throw new TypeError("DELIVERY_VIEW_REGISTRATION_INVALID");
|
|
121
|
-
const View = createSessionDeliveryView(options.React);
|
|
232
|
+
const View = createSessionDeliveryView(options.React, options);
|
|
122
233
|
ctx.slots.inject("conversation.view", () => ctx.slots.register({
|
|
123
234
|
name: "conversation.view", id: DELIVERY_VIEW_ID, order: DELIVERY_VIEW_ORDER, label: "Delivery",
|
|
124
235
|
inject: (sessionId) => ({ source: options.bindProjection(sessionId) }),
|