@spotpatch/runtime 1.10.0 → 1.11.0

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.
@@ -28,7 +28,359 @@ __export(external_handoff_panel_entry_exports, {
28
28
  module.exports = __toCommonJS(external_handoff_panel_entry_exports);
29
29
 
30
30
  // src/controller/external-handoff-workflow.ts
31
+ var import_external_handoff_browser2 = require("@spotpatch/shared/external-handoff-browser");
32
+
33
+ // src/controller/browser-api.ts
34
+ function record(value) {
35
+ return typeof value === "object" && value !== null && !Array.isArray(value);
36
+ }
37
+ function exactKeys(value, keys) {
38
+ const actual = Object.keys(value).sort();
39
+ const expected = [...keys].sort();
40
+ return actual.length === expected.length && actual.every((key, index) => key === expected[index]);
41
+ }
42
+ function validTimestamp(value) {
43
+ if (typeof value !== "string") return false;
44
+ const match = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})(?:\.\d+)?Z$/u.exec(
45
+ value
46
+ );
47
+ if (match === null) return false;
48
+ const timestamp = Date.parse(value);
49
+ if (!Number.isFinite(timestamp)) return false;
50
+ const date = new Date(timestamp);
51
+ return date.getUTCFullYear() === Number(match[1]) && date.getUTCMonth() + 1 === Number(match[2]) && date.getUTCDate() === Number(match[3]) && date.getUTCHours() === Number(match[4]) && date.getUTCMinutes() === Number(match[5]) && date.getUTCSeconds() === Number(match[6]);
52
+ }
53
+ async function readBoundedJson(response, maximumBytes) {
54
+ const declaredLength = Number(response.headers.get("content-length"));
55
+ if (Number.isFinite(declaredLength) && declaredLength > maximumBytes) {
56
+ await response.body?.cancel();
57
+ throw new TypeError("SpotPatch API response exceeds its safety limit.");
58
+ }
59
+ if (response.body === null) {
60
+ throw new TypeError("SpotPatch API response body is unavailable.");
61
+ }
62
+ const reader = response.body.getReader();
63
+ const chunks = [];
64
+ let total = 0;
65
+ try {
66
+ for (; ; ) {
67
+ const chunk = await reader.read();
68
+ if (chunk.done) break;
69
+ total += chunk.value.byteLength;
70
+ if (total > maximumBytes) {
71
+ await reader.cancel();
72
+ throw new TypeError("SpotPatch API response exceeds its safety limit.");
73
+ }
74
+ chunks.push(chunk.value);
75
+ }
76
+ } finally {
77
+ reader.releaseLock();
78
+ }
79
+ const payload = new Uint8Array(total);
80
+ let offset = 0;
81
+ for (const chunk of chunks) {
82
+ payload.set(chunk, offset);
83
+ offset += chunk.byteLength;
84
+ }
85
+ return JSON.parse(
86
+ new TextDecoder("utf-8", { fatal: true }).decode(payload)
87
+ );
88
+ }
89
+ function successData(value) {
90
+ if (!record(value) || !exactKeys(value, ["data", "ok"]) || value.ok !== true) {
91
+ throw new TypeError("SpotPatch API success envelope is invalid.");
92
+ }
93
+ return value.data;
94
+ }
95
+
96
+ // src/controller/external-agent-control-client.ts
31
97
  var import_external_handoff_browser = require("@spotpatch/shared/external-handoff-browser");
98
+ var RESPONSE_OVERHEAD_BYTES = 64 * 1024;
99
+ function member(values, value) {
100
+ return typeof value === "string" && values.includes(value);
101
+ }
102
+ function integer(value, minimum = 0) {
103
+ return Number.isSafeInteger(value) && value >= minimum;
104
+ }
105
+ function optionalKeys(value, base, optional) {
106
+ return exactKeys(value, [
107
+ ...base,
108
+ ...optional.filter((key) => value[key] !== void 0)
109
+ ]);
110
+ }
111
+ function safePath(value) {
112
+ if (typeof value !== "string" || value.length === 0 || value.length > 4096 || value.startsWith("/") || value.includes("\\")) {
113
+ return false;
114
+ }
115
+ return value.split("/").every((segment) => segment.length > 0 && segment !== "." && segment !== "..");
116
+ }
117
+ function parseFiles(value) {
118
+ if (!Array.isArray(value) || value.length > import_external_handoff_browser.EXTERNAL_AGENT_CONTROL_LIMITS.maximumChangedFiles) {
119
+ throw new TypeError("Invalid managed file summaries.");
120
+ }
121
+ return value.map((file) => {
122
+ if (!record(file) || !exactKeys(file, ["additions", "deletions", "path"]) || !safePath(file.path) || !integer(file.additions) || !integer(file.deletions)) {
123
+ throw new TypeError("Invalid managed file summary.");
124
+ }
125
+ return Object.freeze({
126
+ path: file.path,
127
+ additions: file.additions,
128
+ deletions: file.deletions
129
+ });
130
+ });
131
+ }
132
+ function parseChecks(value) {
133
+ if (!Array.isArray(value) || value.length > import_external_handoff_browser.EXTERNAL_AGENT_CONTROL_LIMITS.maximumChecks) {
134
+ throw new TypeError("Invalid managed check summaries.");
135
+ }
136
+ return value.map((check) => {
137
+ if (!record(check) || !optionalKeys(check, ["durationMs", "id", "outcome"], ["exitCode"]) || typeof check.id !== "string" || !/^[A-Za-z0-9._-]{1,128}$/u.test(check.id) || !member(import_external_handoff_browser.EXTERNAL_AGENT_CHECK_OUTCOMES, check.outcome) || !integer(check.durationMs) || check.exitCode !== void 0 && !Number.isSafeInteger(check.exitCode)) {
138
+ throw new TypeError("Invalid managed check summary.");
139
+ }
140
+ return Object.freeze({
141
+ id: check.id,
142
+ outcome: check.outcome,
143
+ durationMs: check.durationMs,
144
+ ...check.exitCode === void 0 ? {} : { exitCode: check.exitCode }
145
+ });
146
+ });
147
+ }
148
+ function parseTimings(value) {
149
+ const keys = [
150
+ "preparing",
151
+ "agent",
152
+ "auditing",
153
+ "validating",
154
+ "applying",
155
+ "total"
156
+ ];
157
+ if (!record(value) || !optionalKeys(value, [], keys)) {
158
+ throw new TypeError("Invalid managed timings.");
159
+ }
160
+ for (const key of keys) {
161
+ if (value[key] !== void 0 && !integer(value[key])) {
162
+ throw new TypeError("Invalid managed timing.");
163
+ }
164
+ }
165
+ return Object.freeze(
166
+ Object.fromEntries(
167
+ keys.flatMap((key) => value[key] === void 0 ? [] : [[key, value[key]]])
168
+ )
169
+ );
170
+ }
171
+ function parseTask(value) {
172
+ if (!record(value) || !optionalKeys(
173
+ value,
174
+ [
175
+ "checks",
176
+ "deliveryStatus",
177
+ "executionStatus",
178
+ "files",
179
+ "managedPhase",
180
+ "revision",
181
+ "timings"
182
+ ],
183
+ ["resultExpiresAt", "validationOutcome"]
184
+ ) || !integer(value.revision, 1) || !member(import_external_handoff_browser.EXTERNAL_AGENT_DELIVERY_STATUSES, value.deliveryStatus) || !member(import_external_handoff_browser.EXTERNAL_AGENT_EXECUTION_STATUSES, value.executionStatus) || !member(import_external_handoff_browser.EXTERNAL_AGENT_MANAGED_PHASES, value.managedPhase) || value.validationOutcome !== void 0 && !member(import_external_handoff_browser.EXTERNAL_AGENT_VALIDATION_OUTCOMES, value.validationOutcome) || value.resultExpiresAt !== void 0 && !validTimestamp(value.resultExpiresAt)) {
185
+ throw new TypeError("Invalid managed task status.");
186
+ }
187
+ return Object.freeze({
188
+ revision: value.revision,
189
+ deliveryStatus: value.deliveryStatus,
190
+ executionStatus: value.executionStatus,
191
+ managedPhase: value.managedPhase,
192
+ ...value.validationOutcome === void 0 ? {} : { validationOutcome: value.validationOutcome },
193
+ files: parseFiles(value.files),
194
+ checks: parseChecks(value.checks),
195
+ timings: parseTimings(value.timings),
196
+ ...value.resultExpiresAt === void 0 ? {} : { resultExpiresAt: value.resultExpiresAt }
197
+ });
198
+ }
199
+ function parseExternalAgentControlStatus(value) {
200
+ if (!record(value) || !optionalKeys(
201
+ value,
202
+ [
203
+ "adapter",
204
+ "authReadiness",
205
+ "connectionState",
206
+ "grantState",
207
+ "mode",
208
+ "schemaVersion",
209
+ "sequence",
210
+ "updatedAt"
211
+ ],
212
+ ["effectiveModel", "error", "requestedModel", "task"]
213
+ ) || value.schemaVersion !== 1 || !integer(value.sequence) || !member(import_external_handoff_browser.EXTERNAL_AGENT_MODES, value.mode) || !member(import_external_handoff_browser.EXTERNAL_AGENT_CONNECTION_STATES, value.connectionState) || !member(import_external_handoff_browser.EXTERNAL_AGENT_AUTH_READINESS, value.authReadiness) || !member(import_external_handoff_browser.EXTERNAL_AGENT_GRANT_STATES, value.grantState) || !validTimestamp(value.updatedAt) || value.requestedModel !== void 0 && (typeof value.requestedModel !== "string" || value.requestedModel.length === 0 || value.requestedModel.length > 128) || value.effectiveModel !== void 0 && (typeof value.effectiveModel !== "string" || value.effectiveModel.length === 0 || value.effectiveModel.length > 128) || !record(value.adapter) || !exactKeys(value.adapter, ["availability", "kind", "maturity"]) || value.adapter.kind !== "codex" || value.adapter.maturity !== "experimental" || value.adapter.availability !== "available" && value.adapter.availability !== "unavailable") {
214
+ throw new TypeError("Invalid external Agent control status.");
215
+ }
216
+ let error;
217
+ if (value.error !== void 0) {
218
+ if (!record(value.error) || !exactKeys(value.error, ["action", "code", "recoverability", "stage"]) || !member(import_external_handoff_browser.EXTERNAL_AGENT_ERROR_CODES, value.error.code) || !member(import_external_handoff_browser.EXTERNAL_AGENT_ERROR_STAGES, value.error.stage) || !member(import_external_handoff_browser.EXTERNAL_AGENT_ERROR_RECOVERABILITY, value.error.recoverability) || !member(import_external_handoff_browser.EXTERNAL_AGENT_ACTIONS, value.error.action)) {
219
+ throw new TypeError("Invalid external Agent error.");
220
+ }
221
+ error = Object.freeze({
222
+ code: value.error.code,
223
+ stage: value.error.stage,
224
+ recoverability: value.error.recoverability,
225
+ action: value.error.action
226
+ });
227
+ }
228
+ return Object.freeze({
229
+ schemaVersion: 1,
230
+ sequence: value.sequence,
231
+ mode: value.mode,
232
+ adapter: Object.freeze({
233
+ kind: "codex",
234
+ maturity: "experimental",
235
+ availability: value.adapter.availability
236
+ }),
237
+ connectionState: value.connectionState,
238
+ authReadiness: value.authReadiness,
239
+ grantState: value.grantState,
240
+ ...value.requestedModel === void 0 ? {} : { requestedModel: value.requestedModel },
241
+ ...value.effectiveModel === void 0 ? {} : { effectiveModel: value.effectiveModel },
242
+ ...value.task === void 0 ? {} : { task: parseTask(value.task) },
243
+ ...error === void 0 ? {} : { error },
244
+ updatedAt: value.updatedAt
245
+ });
246
+ }
247
+ function parseExternalAgentManagedResult(value) {
248
+ if (!record(value) || !exactKeys(value, [
249
+ "checks",
250
+ "diff",
251
+ "expiresAt",
252
+ "files",
253
+ "revision",
254
+ "timings",
255
+ "validationOutcome"
256
+ ]) || !integer(value.revision, 1) || typeof value.diff !== "string" || value.diff.length > import_external_handoff_browser.EXTERNAL_AGENT_CONTROL_LIMITS.maximumResultDiffBytes || !validTimestamp(value.expiresAt) || !member(import_external_handoff_browser.EXTERNAL_AGENT_VALIDATION_OUTCOMES, value.validationOutcome)) {
257
+ throw new TypeError("Invalid managed Agent result.");
258
+ }
259
+ return Object.freeze({
260
+ revision: value.revision,
261
+ diff: value.diff,
262
+ files: parseFiles(value.files),
263
+ checks: parseChecks(value.checks),
264
+ timings: parseTimings(value.timings),
265
+ validationOutcome: value.validationOutcome,
266
+ expiresAt: value.expiresAt
267
+ });
268
+ }
269
+ function parseEvent(value) {
270
+ if (!record(value) || typeof value.type !== "string") {
271
+ throw new TypeError("Invalid external Agent event.");
272
+ }
273
+ if (value.type === "status" && exactKeys(value, ["data", "type"])) {
274
+ return Object.freeze({
275
+ type: "status",
276
+ data: parseExternalAgentControlStatus(value.data)
277
+ });
278
+ }
279
+ if (value.type === "heartbeat" && exactKeys(value, ["emittedAt", "sequence", "type"]) && integer(value.sequence) && validTimestamp(value.emittedAt)) {
280
+ return Object.freeze({
281
+ type: "heartbeat",
282
+ sequence: value.sequence,
283
+ emittedAt: value.emittedAt
284
+ });
285
+ }
286
+ throw new TypeError("Invalid external Agent event.");
287
+ }
288
+ function createExternalAgentControlClient(fetch, sessionToken) {
289
+ const request = async (endpoint, body, maximumBytes = RESPONSE_OVERHEAD_BYTES) => {
290
+ const response = await fetch(endpoint, {
291
+ method: "POST",
292
+ headers: {
293
+ "Content-Type": "application/json",
294
+ [import_external_handoff_browser.SPOTPATCH_TOKEN_HEADER]: sessionToken
295
+ },
296
+ body: JSON.stringify(body)
297
+ });
298
+ const envelope = await readBoundedJson(response, maximumBytes);
299
+ if (!response.ok) throw new TypeError("External Agent control request failed.");
300
+ return successData(envelope);
301
+ };
302
+ return Object.freeze({
303
+ async status() {
304
+ return parseExternalAgentControlStatus(
305
+ await request(import_external_handoff_browser.SPOTPATCH_ENDPOINTS.externalAgentControlStatus, {})
306
+ );
307
+ },
308
+ async connect(value) {
309
+ return parseExternalAgentControlStatus(
310
+ await request(import_external_handoff_browser.SPOTPATCH_ENDPOINTS.externalAgentControlConnect, value)
311
+ );
312
+ },
313
+ async disconnect(value) {
314
+ return parseExternalAgentControlStatus(
315
+ await request(import_external_handoff_browser.SPOTPATCH_ENDPOINTS.externalAgentControlDisconnect, value)
316
+ );
317
+ },
318
+ async cancel(value) {
319
+ return parseExternalAgentControlStatus(
320
+ await request(import_external_handoff_browser.SPOTPATCH_ENDPOINTS.externalAgentControlCancel, value)
321
+ );
322
+ },
323
+ async result(revision) {
324
+ return parseExternalAgentManagedResult(
325
+ await request(
326
+ import_external_handoff_browser.SPOTPATCH_ENDPOINTS.externalAgentResult,
327
+ { revision },
328
+ import_external_handoff_browser.EXTERNAL_AGENT_CONTROL_LIMITS.maximumResultDiffBytes + RESPONSE_OVERHEAD_BYTES
329
+ )
330
+ );
331
+ },
332
+ async events(afterSequence, signal, onEvent) {
333
+ const response = await fetch(import_external_handoff_browser.SPOTPATCH_ENDPOINTS.externalAgentEvents, {
334
+ method: "POST",
335
+ headers: {
336
+ "Content-Type": "application/json",
337
+ [import_external_handoff_browser.SPOTPATCH_TOKEN_HEADER]: sessionToken
338
+ },
339
+ body: JSON.stringify(afterSequence === void 0 ? {} : { afterSequence }),
340
+ signal
341
+ });
342
+ if (!response.ok || response.body === null) {
343
+ await response.body?.cancel();
344
+ throw new TypeError("External Agent event stream is unavailable.");
345
+ }
346
+ const reader = response.body.getReader();
347
+ let pending = new Uint8Array(0);
348
+ try {
349
+ for (; ; ) {
350
+ const chunk = await reader.read();
351
+ if (chunk.done) break;
352
+ const combined = new Uint8Array(pending.byteLength + chunk.value.byteLength);
353
+ combined.set(pending);
354
+ combined.set(chunk.value, pending.byteLength);
355
+ pending = combined;
356
+ for (; ; ) {
357
+ const newline = pending.indexOf(10);
358
+ if (newline === -1) break;
359
+ if (newline === 0 || newline > import_external_handoff_browser.EXTERNAL_AGENT_CONTROL_LIMITS.maximumEventLineBytes) {
360
+ throw new TypeError("External Agent event line is invalid.");
361
+ }
362
+ const line = pending.slice(0, newline);
363
+ pending = pending.slice(newline + 1);
364
+ const value = JSON.parse(
365
+ new TextDecoder("utf-8", { fatal: true }).decode(line)
366
+ );
367
+ onEvent(parseEvent(value));
368
+ }
369
+ if (pending.byteLength > import_external_handoff_browser.EXTERNAL_AGENT_CONTROL_LIMITS.maximumEventLineBytes) {
370
+ throw new TypeError("External Agent event line exceeds its limit.");
371
+ }
372
+ }
373
+ if (pending.byteLength !== 0) {
374
+ throw new TypeError("External Agent event stream ended mid-record.");
375
+ }
376
+ } finally {
377
+ reader.releaseLock();
378
+ }
379
+ }
380
+ });
381
+ }
382
+
383
+ // src/controller/external-handoff-workflow.ts
32
384
  var MAX_SUMMARY_RESPONSE_BYTES = 64 * 1024;
33
385
  var OPAQUE_ID_PATTERN = /^[A-Za-z0-9_-]{22,128}$/u;
34
386
  var ExternalHandoffApiError = class extends Error {
@@ -39,17 +391,6 @@ var ExternalHandoffApiError = class extends Error {
39
391
  }
40
392
  code;
41
393
  };
42
- function record(value) {
43
- return typeof value === "object" && value !== null && !Array.isArray(value);
44
- }
45
- function exactKeys(value, keys) {
46
- const actual = Object.keys(value).sort();
47
- const expected = [...keys].sort();
48
- return actual.length === expected.length && actual.every((key, index) => key === expected[index]);
49
- }
50
- function validTimestamp(value) {
51
- return typeof value === "string" && Number.isFinite(Date.parse(value));
52
- }
53
394
  function parseActiveAdapter(value) {
54
395
  if (value === null) return null;
55
396
  if (!record(value) || !exactKeys(value, ["canDispatch", "connectedAt", "kind", "state", "updatedAt"]) || value.kind !== "claude-channel" && value.kind !== "codex-app-server" || value.state !== "ready" && value.state !== "busy" && value.state !== "blocked" || typeof value.canDispatch !== "boolean" || value.canDispatch !== (value.state === "ready") || !validTimestamp(value.connectedAt) || !validTimestamp(value.updatedAt)) {
@@ -85,7 +426,7 @@ function parseCapability(value) {
85
426
  "dispatch",
86
427
  "enabled",
87
428
  "snapshotSchemaVersion"
88
- ]) || value.enabled !== true || typeof value.brokerReady !== "boolean" || !Number.isSafeInteger(value.activeWaitCount) || value.activeWaitCount < 0 || value.activeWaitCount > import_external_handoff_browser.EXTERNAL_HANDOFF_LIMITS.maximumWaiters || value.snapshotSchemaVersion !== import_external_handoff_browser.EXTERNAL_HANDOFF_SNAPSHOT_SCHEMA_VERSION || value.brokerProtocolVersion !== import_external_handoff_browser.EXTERNAL_HANDOFF_BROKER_PROTOCOL_VERSION) {
429
+ ]) || value.enabled !== true || typeof value.brokerReady !== "boolean" || !Number.isSafeInteger(value.activeWaitCount) || value.activeWaitCount < 0 || value.activeWaitCount > import_external_handoff_browser2.EXTERNAL_HANDOFF_LIMITS.maximumWaiters || value.snapshotSchemaVersion !== import_external_handoff_browser2.EXTERNAL_HANDOFF_SNAPSHOT_SCHEMA_VERSION || value.brokerProtocolVersion !== import_external_handoff_browser2.EXTERNAL_HANDOFF_BROKER_PROTOCOL_VERSION) {
89
430
  throw new ExternalHandoffApiError();
90
431
  }
91
432
  return Object.freeze({
@@ -94,8 +435,8 @@ function parseCapability(value) {
94
435
  activeWaitCount: value.activeWaitCount,
95
436
  activeAdapter: parseActiveAdapter(value.activeAdapter),
96
437
  dispatch: parseDispatch(value.dispatch),
97
- snapshotSchemaVersion: import_external_handoff_browser.EXTERNAL_HANDOFF_SNAPSHOT_SCHEMA_VERSION,
98
- brokerProtocolVersion: import_external_handoff_browser.EXTERNAL_HANDOFF_BROKER_PROTOCOL_VERSION
438
+ snapshotSchemaVersion: import_external_handoff_browser2.EXTERNAL_HANDOFF_SNAPSHOT_SCHEMA_VERSION,
439
+ brokerProtocolVersion: import_external_handoff_browser2.EXTERNAL_HANDOFF_BROKER_PROTOCOL_VERSION
99
440
  });
100
441
  }
101
442
  function parsePublishResult(value) {
@@ -157,7 +498,7 @@ function parseSummary(value) {
157
498
  "targetCount",
158
499
  ...value.pickedUpAt === void 0 ? [] : ["pickedUpAt"]
159
500
  ];
160
- if (!exactKeys(value, expectedKeys) || typeof value.sessionId !== "string" || !OPAQUE_ID_PATTERN.test(value.sessionId) || value.framework !== "vite" && value.framework !== "next" || !Number.isSafeInteger(value.revision) || value.revision <= 0 || typeof value.cursor !== "string" || !OPAQUE_ID_PATTERN.test(value.cursor) || !Number.isSafeInteger(value.targetCount) || value.targetCount <= 0 || !record(value.page) || !exactKeys(value.page, ["origin", "pathname"]) || typeof value.page.origin !== "string" || typeof value.page.pathname !== "string" || !validTimestamp(value.publishedAt) || !validTimestamp(value.expiresAt) || value.state !== "available" && value.state !== "expired" && value.state !== "superseded" || !Number.isSafeInteger(value.pickupCount) || value.pickupCount < 0 || value.pickupCount > import_external_handoff_browser.EXTERNAL_HANDOFF_LIMITS.maximumConnectorReceipts || value.pickedUpAt !== void 0 && !validTimestamp(value.pickedUpAt)) {
501
+ if (!exactKeys(value, expectedKeys) || typeof value.sessionId !== "string" || !OPAQUE_ID_PATTERN.test(value.sessionId) || value.framework !== "vite" && value.framework !== "next" || !Number.isSafeInteger(value.revision) || value.revision <= 0 || typeof value.cursor !== "string" || !OPAQUE_ID_PATTERN.test(value.cursor) || !Number.isSafeInteger(value.targetCount) || value.targetCount <= 0 || !record(value.page) || !exactKeys(value.page, ["origin", "pathname"]) || typeof value.page.origin !== "string" || typeof value.page.pathname !== "string" || !validTimestamp(value.publishedAt) || !validTimestamp(value.expiresAt) || value.state !== "available" && value.state !== "expired" && value.state !== "superseded" || !Number.isSafeInteger(value.pickupCount) || value.pickupCount < 0 || value.pickupCount > import_external_handoff_browser2.EXTERNAL_HANDOFF_LIMITS.maximumConnectorReceipts || value.pickedUpAt !== void 0 && !validTimestamp(value.pickedUpAt)) {
161
502
  throw new ExternalHandoffApiError();
162
503
  }
163
504
  return Object.freeze({
@@ -174,52 +515,8 @@ function parseSummary(value) {
174
515
  ...value.pickedUpAt === void 0 ? {} : { pickedUpAt: value.pickedUpAt }
175
516
  });
176
517
  }
177
- async function boundedJson(response) {
178
- const declaredLength = Number(response.headers.get("content-length"));
179
- if (Number.isFinite(declaredLength) && declaredLength > MAX_SUMMARY_RESPONSE_BYTES) {
180
- await response.body?.cancel();
181
- throw new ExternalHandoffApiError();
182
- }
183
- if (response.body === null) throw new ExternalHandoffApiError();
184
- const reader = response.body.getReader();
185
- const chunks = [];
186
- let total = 0;
187
- try {
188
- for (; ; ) {
189
- const chunk = await reader.read();
190
- if (chunk.done) break;
191
- total += chunk.value.byteLength;
192
- if (total > MAX_SUMMARY_RESPONSE_BYTES) {
193
- await reader.cancel();
194
- throw new ExternalHandoffApiError();
195
- }
196
- chunks.push(chunk.value);
197
- }
198
- } finally {
199
- reader.releaseLock();
200
- }
201
- const payload = new Uint8Array(total);
202
- let offset = 0;
203
- for (const chunk of chunks) {
204
- payload.set(chunk, offset);
205
- offset += chunk.byteLength;
206
- }
207
- try {
208
- return JSON.parse(
209
- new TextDecoder("utf-8", { fatal: true }).decode(payload)
210
- );
211
- } catch {
212
- throw new ExternalHandoffApiError();
213
- }
214
- }
215
518
  function failureCode(value) {
216
- return record(value) && value.ok === false && record(value.error) && (0, import_external_handoff_browser.isErrorCode)(value.error.code) ? value.error.code : void 0;
217
- }
218
- function successData(value) {
219
- if (!record(value) || !exactKeys(value, ["data", "ok"]) || value.ok !== true) {
220
- throw new ExternalHandoffApiError();
221
- }
222
- return value.data;
519
+ return record(value) && value.ok === false && record(value.error) && (0, import_external_handoff_browser2.isErrorCode)(value.error.code) ? value.error.code : void 0;
223
520
  }
224
521
  function omitBrowserCode(annotation) {
225
522
  return {
@@ -253,14 +550,23 @@ function api(options) {
253
550
  method: "POST",
254
551
  headers: {
255
552
  "Content-Type": "application/json",
256
- [import_external_handoff_browser.SPOTPATCH_TOKEN_HEADER]: options.sessionToken
553
+ [import_external_handoff_browser2.SPOTPATCH_TOKEN_HEADER]: options.sessionToken
257
554
  },
258
555
  body: JSON.stringify(body),
259
556
  signal: controller.signal
260
557
  });
261
- const envelope = await boundedJson(response);
558
+ let envelope;
559
+ try {
560
+ envelope = await readBoundedJson(response, MAX_SUMMARY_RESPONSE_BYTES);
561
+ } catch {
562
+ throw new ExternalHandoffApiError();
563
+ }
262
564
  if (!response.ok) throw new ExternalHandoffApiError(failureCode(envelope));
263
- return successData(envelope);
565
+ try {
566
+ return successData(envelope);
567
+ } catch {
568
+ throw new ExternalHandoffApiError();
569
+ }
264
570
  } finally {
265
571
  pending.delete(controller);
266
572
  }
@@ -272,12 +578,12 @@ function api(options) {
272
578
  },
273
579
  async capability() {
274
580
  return parseCapability(
275
- await request(import_external_handoff_browser.SPOTPATCH_ENDPOINTS.externalHandoffCapability, {})
581
+ await request(import_external_handoff_browser2.SPOTPATCH_ENDPOINTS.externalHandoffCapability, {})
276
582
  );
277
583
  },
278
584
  async publish(requestId, annotation) {
279
585
  return parsePublishResult(
280
- await request(import_external_handoff_browser.SPOTPATCH_ENDPOINTS.externalHandoffPublish, {
586
+ await request(import_external_handoff_browser2.SPOTPATCH_ENDPOINTS.externalHandoffPublish, {
281
587
  annotation: omitBrowserCode(annotation),
282
588
  requestId
283
589
  })
@@ -286,14 +592,14 @@ function api(options) {
286
592
  async status(cursor) {
287
593
  return parseStatusResult(
288
594
  await request(
289
- import_external_handoff_browser.SPOTPATCH_ENDPOINTS.externalHandoffStatus,
595
+ import_external_handoff_browser2.SPOTPATCH_ENDPOINTS.externalHandoffStatus,
290
596
  cursor === void 0 ? {} : { cursor }
291
597
  )
292
598
  );
293
599
  },
294
600
  async resolveDelivery(cursor) {
295
601
  return parseStatusResult(
296
- await request(import_external_handoff_browser.SPOTPATCH_ENDPOINTS.externalHandoffResolveDelivery, {
602
+ await request(import_external_handoff_browser2.SPOTPATCH_ENDPOINTS.externalHandoffResolveDelivery, {
297
603
  confirmation: "workspace-reviewed",
298
604
  cursor
299
605
  })
@@ -310,6 +616,7 @@ function createExternalHandoffWorkflow(fetch, panel, selectedAnnotation, session
310
616
  window
311
617
  };
312
618
  const client = api(options);
619
+ const controlClient = createExternalAgentControlClient(fetch, sessionToken);
313
620
  const timers = /* @__PURE__ */ new Set();
314
621
  const lifecycle = {
315
622
  disposed: false,
@@ -318,12 +625,152 @@ function createExternalHandoffWorkflow(fetch, panel, selectedAnnotation, session
318
625
  userActionPending: false
319
626
  };
320
627
  let capability;
628
+ let controlStatus;
629
+ let controlEventController;
630
+ let controlReconnectDelay = import_external_handoff_browser2.EXTERNAL_AGENT_CONTROL_LIMITS.eventReconnectMinimumMs;
631
+ let controlReconnectTimer;
632
+ let controlActionPending = false;
633
+ let managedResultRevision;
634
+ let managedResultLoadingRevision;
321
635
  let current;
322
636
  let retryablePublish;
637
+ const isDisposed = () => lifecycle.disposed;
323
638
  const clearTimers = () => {
324
639
  for (const timer of timers) options.window.clearTimeout(timer);
325
640
  timers.clear();
326
641
  };
642
+ const clearControlReconnect = () => {
643
+ if (controlReconnectTimer === void 0) return;
644
+ options.window.clearTimeout(controlReconnectTimer);
645
+ controlReconnectTimer = void 0;
646
+ };
647
+ const renderManagedResult = async (revision) => {
648
+ if (lifecycle.disposed || managedResultRevision === revision || managedResultLoadingRevision === revision) {
649
+ return;
650
+ }
651
+ managedResultLoadingRevision = revision;
652
+ try {
653
+ const result = await controlClient.result(revision);
654
+ if (isDisposed() || controlStatus?.task?.revision !== revision) return;
655
+ managedResultRevision = revision;
656
+ options.panel.renderManagedResult(result);
657
+ } catch {
658
+ } finally {
659
+ if (managedResultLoadingRevision === revision) {
660
+ managedResultLoadingRevision = void 0;
661
+ }
662
+ }
663
+ };
664
+ const applyControlStatus = (value) => {
665
+ if (controlStatus !== void 0 && value.sequence < controlStatus.sequence) return;
666
+ controlStatus = value;
667
+ controlReconnectDelay = import_external_handoff_browser2.EXTERNAL_AGENT_CONTROL_LIMITS.eventReconnectMinimumMs;
668
+ options.panel.renderControlStatus(value);
669
+ const resultRevision = value.task?.resultExpiresAt === void 0 ? void 0 : value.task.revision;
670
+ if (resultRevision !== void 0) void renderManagedResult(resultRevision);
671
+ };
672
+ const handleControlEvent = (event) => {
673
+ if (event.type === "status") {
674
+ applyControlStatus(event.data);
675
+ return;
676
+ }
677
+ const resultRevision = controlStatus?.task?.resultExpiresAt === void 0 ? void 0 : controlStatus.task.revision;
678
+ if (resultRevision !== void 0) void renderManagedResult(resultRevision);
679
+ };
680
+ const startControlEventStream = () => {
681
+ if (lifecycle.disposed || !lifecycle.mounted || controlEventController !== void 0) {
682
+ return;
683
+ }
684
+ clearControlReconnect();
685
+ const controller = new AbortController();
686
+ controlEventController = controller;
687
+ void controlClient.events(controlStatus?.sequence, controller.signal, handleControlEvent).catch((error) => {
688
+ if (lifecycle.disposed || error instanceof DOMException && error.name === "AbortError") {
689
+ return;
690
+ }
691
+ if (controlStatus === void 0) options.panel.renderControlUnavailable();
692
+ }).finally(() => {
693
+ if (controlEventController === controller) {
694
+ controlEventController = void 0;
695
+ }
696
+ if (lifecycle.disposed || !lifecycle.mounted) return;
697
+ const delay = controlReconnectDelay;
698
+ controlReconnectDelay = Math.min(
699
+ delay * 2,
700
+ import_external_handoff_browser2.EXTERNAL_AGENT_CONTROL_LIMITS.eventReconnectMaximumMs
701
+ );
702
+ controlReconnectTimer = options.window.setTimeout(() => {
703
+ controlReconnectTimer = void 0;
704
+ startControlEventStream();
705
+ }, delay);
706
+ });
707
+ };
708
+ const refreshControlStatus = async () => {
709
+ applyControlStatus(await controlClient.status());
710
+ };
711
+ const bootstrapControl = async () => {
712
+ try {
713
+ await refreshControlStatus();
714
+ } catch {
715
+ if (!lifecycle.disposed) options.panel.renderControlUnavailable();
716
+ } finally {
717
+ if (!lifecycle.disposed) startControlEventStream();
718
+ }
719
+ };
720
+ const performControlAction = async (action) => {
721
+ if (lifecycle.disposed || controlActionPending) return;
722
+ controlActionPending = true;
723
+ options.panel.setControlBusy(true);
724
+ try {
725
+ applyControlStatus(await action());
726
+ } catch {
727
+ try {
728
+ await refreshControlStatus();
729
+ } catch {
730
+ if (!isDisposed()) options.panel.renderControlUnavailable();
731
+ }
732
+ } finally {
733
+ controlActionPending = false;
734
+ if (!isDisposed()) options.panel.setControlBusy(false);
735
+ }
736
+ };
737
+ const handleConnectClick = () => {
738
+ void performControlAction(
739
+ () => controlClient.connect({
740
+ requestId: createRequestId(options.window),
741
+ adapterKind: "codex",
742
+ profile: import_external_handoff_browser2.EXTERNAL_AGENT_MANAGED_PROFILE
743
+ })
744
+ );
745
+ };
746
+ const handleDisconnectClick = () => {
747
+ void performControlAction(
748
+ () => controlClient.disconnect({
749
+ requestId: createRequestId(options.window),
750
+ adapterKind: "codex",
751
+ revokeGrant: false
752
+ })
753
+ );
754
+ };
755
+ const handleRevokeClick = () => {
756
+ void performControlAction(
757
+ () => controlClient.disconnect({
758
+ requestId: createRequestId(options.window),
759
+ adapterKind: "codex",
760
+ revokeGrant: true
761
+ })
762
+ );
763
+ };
764
+ const handleManagedCancelClick = () => {
765
+ const revision = controlStatus?.task?.revision;
766
+ if (revision === void 0) return;
767
+ void performControlAction(
768
+ () => controlClient.cancel({
769
+ requestId: createRequestId(options.window),
770
+ revision
771
+ })
772
+ );
773
+ };
327
774
  const errorCode = (error) => error instanceof ExternalHandoffApiError ? error.code : void 0;
328
775
  const restorePanel = () => {
329
776
  if (current !== void 0) options.panel.renderStatus(current);
@@ -370,7 +817,7 @@ function createExternalHandoffWorkflow(fetch, panel, selectedAnnotation, session
370
817
  void refreshStatus(revision).then(() => {
371
818
  if (continueWhilePending && !lifecycle.disposed && revision === lifecycle.operation && !lifecycle.userActionPending && current !== void 0 && dispatchIsPending(current.dispatch)) {
372
819
  scheduleRefresh(
373
- import_external_handoff_browser.EXTERNAL_HANDOFF_LIMITS.activeStatusPollMs,
820
+ import_external_handoff_browser2.EXTERNAL_HANDOFF_LIMITS.activeStatusPollMs,
374
821
  continueWhilePending
375
822
  );
376
823
  }
@@ -386,7 +833,7 @@ function createExternalHandoffWorkflow(fetch, panel, selectedAnnotation, session
386
833
  const annotation = options.selectedAnnotation();
387
834
  if (annotation === void 0) {
388
835
  lifecycle.userActionPending = false;
389
- options.panel.renderError(import_external_handoff_browser.ERROR_CODES.HANDOFF_VALIDATION_FAILED);
836
+ options.panel.renderError(import_external_handoff_browser2.ERROR_CODES.HANDOFF_VALIDATION_FAILED);
390
837
  return;
391
838
  }
392
839
  const disclosureRevision = lifecycle.operation;
@@ -419,7 +866,7 @@ function createExternalHandoffWorkflow(fetch, panel, selectedAnnotation, session
419
866
  });
420
867
  options.panel.renderPublishResult(result);
421
868
  scheduleRefresh(
422
- result.delivery.mode === "active" ? import_external_handoff_browser.EXTERNAL_HANDOFF_LIMITS.activeStatusPollMs : 500,
869
+ result.delivery.mode === "active" ? import_external_handoff_browser2.EXTERNAL_HANDOFF_LIMITS.activeStatusPollMs : 500,
423
870
  result.delivery.mode === "active"
424
871
  );
425
872
  } catch (error) {
@@ -507,7 +954,15 @@ function createExternalHandoffWorkflow(fetch, panel, selectedAnnotation, session
507
954
  options.panel.sendButton.addEventListener("click", handleSendClick);
508
955
  options.panel.refreshButton.addEventListener("click", handleRefreshClick);
509
956
  options.panel.resolveButton.addEventListener("click", handleResolveClick);
957
+ options.panel.connectButton.addEventListener("click", handleConnectClick);
958
+ options.panel.disconnectButton.addEventListener("click", handleDisconnectClick);
959
+ options.panel.revokeButton.addEventListener("click", handleRevokeClick);
960
+ options.panel.cancelManagedButton.addEventListener(
961
+ "click",
962
+ handleManagedCancelClick
963
+ );
510
964
  void refreshCapability(lifecycle.operation);
965
+ void bootstrapControl();
511
966
  },
512
967
  cancelPending,
513
968
  dispose() {
@@ -517,10 +972,23 @@ function createExternalHandoffWorkflow(fetch, panel, selectedAnnotation, session
517
972
  lifecycle.userActionPending = false;
518
973
  retryablePublish = void 0;
519
974
  clearTimers();
975
+ clearControlReconnect();
976
+ controlEventController?.abort("workflow-disposed");
977
+ controlEventController = void 0;
520
978
  client.cancel();
521
979
  options.panel.sendButton.removeEventListener("click", handleSendClick);
522
980
  options.panel.refreshButton.removeEventListener("click", handleRefreshClick);
523
981
  options.panel.resolveButton.removeEventListener("click", handleResolveClick);
982
+ options.panel.connectButton.removeEventListener("click", handleConnectClick);
983
+ options.panel.disconnectButton.removeEventListener(
984
+ "click",
985
+ handleDisconnectClick
986
+ );
987
+ options.panel.revokeButton.removeEventListener("click", handleRevokeClick);
988
+ options.panel.cancelManagedButton.removeEventListener(
989
+ "click",
990
+ handleManagedCancelClick
991
+ );
524
992
  }
525
993
  });
526
994
  }
@@ -550,6 +1018,55 @@ function createButton(document, label, className = "") {
550
1018
  var MESSAGES = Object.freeze({
551
1019
  "en-US": Object.freeze({
552
1020
  title: "External Agent connection",
1021
+ agentLabel: "Agent",
1022
+ codexManaged: "Codex \xB7 managed (experimental)",
1023
+ connectManaged: "Connect Codex",
1024
+ disconnectManaged: "Disconnect",
1025
+ revokeManaged: "Revoke grant",
1026
+ cancelManaged: "Cancel managed task",
1027
+ resultTitle: "Managed result",
1028
+ controlConnection: (state, mode) => `Connection: ${state} \xB7 Mode: ${mode}`,
1029
+ controlAuth: (auth, grant) => `Auth: ${auth} \xB7 Grant: ${grant}`,
1030
+ controlModel: (model) => `Model: ${model}`,
1031
+ controlRevision: (task) => `Revision ${String(task.revision)}: ${task.deliveryStatus} / ${task.executionStatus} / ${task.managedPhase}`,
1032
+ controlValidation: (outcome) => `Validation: ${outcome}`,
1033
+ controlFailure: (error, action) => `Error: ${error} \xB7 Next: ${action}`,
1034
+ controlUnavailable: "Page connection management is unavailable; external handoffs still fall back honestly to the Agent inbox.",
1035
+ controlErrorText: Object.freeze({
1036
+ AGENT_BINARY_NOT_FOUND: "Codex is not installed or is not on PATH.",
1037
+ AGENT_BINARY_UNTRUSTED: "The resolved Codex executable is not trusted.",
1038
+ AGENT_VERSION_UNSUPPORTED: "The installed Codex version is unsupported.",
1039
+ APP_SERVER_HANDSHAKE_FAILED: "Codex App Server did not complete startup.",
1040
+ AGENT_AUTH_REQUIRED: "Codex requires an authenticated account.",
1041
+ AGENT_MODEL_UNAVAILABLE: "The configured Codex model is unavailable.",
1042
+ AGENT_PROTOCOL_INCOMPATIBLE: "The Codex App Server protocol is incompatible.",
1043
+ CODEX_CONFIG_ISOLATION_UNSUPPORTED: "This Codex version cannot prove managed configuration isolation.",
1044
+ MANAGED_GRANT_INVALID: "The saved project grant failed validation.",
1045
+ MANAGED_PLATFORM_UNSUPPORTED: "Managed execution is not proven safe on this platform.",
1046
+ MANAGED_GIT_REQUIRED: "Managed execution requires a Git repository.",
1047
+ MANAGED_SNAPSHOT_FAILED: "The independent workspace snapshot could not be created.",
1048
+ MANAGED_SCOPE_VIOLATION: "The candidate change exceeded its authorized paths.",
1049
+ MANAGED_CHANGE_LIMIT_EXCEEDED: "The candidate change exceeded safety limits.",
1050
+ MANAGED_VALIDATION_FAILED: "A required check failed or changed the candidate diff.",
1051
+ MANAGED_WORKSPACE_CONFLICT: "An authorized source file changed during execution.",
1052
+ MANAGED_APPLY_FAILED: "The audited change could not be applied safely.",
1053
+ MANAGED_CLEANUP_INCOMPLETE: "Managed thread or workspace cleanup is incomplete."
1054
+ }),
1055
+ controlActionText: Object.freeze({
1056
+ "install-agent": "Install Codex and retry.",
1057
+ "use-supported-version": "Use a supported Codex version.",
1058
+ "sign-in": "Sign in with Codex, then retry.",
1059
+ "choose-available-model": "Choose an available model in Codex configuration.",
1060
+ "use-inbox": "Continue with the Agent inbox fallback.",
1061
+ "confirm-managed-access": "Revoke the invalid grant and confirm access again.",
1062
+ "review-candidate-diff": "Review the candidate diff and validation output.",
1063
+ "review-workspace-conflict": "Review the workspace changes before retrying.",
1064
+ retry: "Retry the managed connection.",
1065
+ "inspect-cleanup-warning": "Inspect cleanup status before reconnecting."
1066
+ }),
1067
+ resultHeader: (revision, outcome) => `Revision ${String(revision)} \xB7 Validation ${outcome}`,
1068
+ resultDisposition: (outcome) => outcome === "passed" ? "Applied to the project after validation passed." : `Candidate only; it was not applied because validation is ${outcome}.`,
1069
+ resultTiming: (stage, durationMs) => `Timing ${stage}: ${String(durationMs)} ms`,
553
1070
  description: "Send through a ready active adapter, or publish to the project Agent inbox when no active adapter is connected.",
554
1071
  ready: "Local Broker ready. No Agent connection is assumed until a connector reads or waits.",
555
1072
  readyWaiting: (count) => `${String(count)} active connector wait request(s); publishing can wake them immediately.`,
@@ -573,19 +1090,69 @@ var MESSAGES = Object.freeze({
573
1090
  refresh: "Refresh pickup status",
574
1091
  resolveUnknown: "Workspace reviewed \u2014 allow a new task",
575
1092
  settingsTitle: "Project connection setup",
576
- settingsHelp: "Setup commands are project-scoped dry runs; append --write only after review. Claude active mode also needs the shown Research Preview launch flag. Codex active mode is zero-setup: run its explicit connector command and keep it open.",
1093
+ settingsHelp: "Codex managed mode is owned by the current development server. The browser never receives vendor credentials or arbitrary command access. Generic and attached adapters remain honest Inbox fallbacks.",
577
1094
  disclosureTitle: "Confirm external Agent handoff",
578
1095
  disclosureIntro: (targets, files) => `Publish ${String(targets)} selected target(s) referencing ${String(files)} relative file(s).`,
579
1096
  disclosureData: "The handoff includes your instructions, component/page context, sanitized DOM/CSS, and bounded source read again by the local Node service.",
580
1097
  disclosureInbox: "Any SpotPatch connector configured for this project may read it during the 15-minute lifetime.",
581
1098
  disclosureProvider: "The Agent host may send the content to its cloud provider under that product's data policy.",
582
1099
  disclosureNoGuarantee: "External edits do not receive SpotPatch's built-in worktree, Apply, or Revert guarantees.",
1100
+ disclosureManagedGuarantee: "Managed Codex edits run in an independent temporary snapshot. SpotPatch audits the diff and only applies it when every trusted required check passes.",
583
1101
  cancel: "Cancel",
584
1102
  confirm: "Confirm and send",
585
1103
  error: (code) => code === "EXTERNAL_AGENT_BUSY" ? "The active Agent is still busy or blocked by an uncertain delivery." : code === "ACTIVE_DISPATCH_INVALID" ? "The active delivery state changed. Refresh before continuing." : code === "HANDOFF_SOURCE_STALE" ? "The selected source changed. Select the current component again." : code === "HANDOFF_RESPONSE_TOO_LARGE" ? "The handoff is too large. Reduce the selected context." : code === "EXTERNAL_HANDOFF_DISABLED" ? "External Agent handoff is disabled in trusted project configuration." : code === "EXTERNAL_HANDOFF_UNAVAILABLE" || code === "SESSION_CLOSED" ? "The local external Agent Broker is unavailable. Restart the development server." : "The external Agent handoff request failed without publishing a partial revision."
586
1104
  }),
587
1105
  "zh-CN": Object.freeze({
588
1106
  title: "\u5916\u90E8 Agent \u8FDE\u63A5",
1107
+ agentLabel: "Agent",
1108
+ codexManaged: "Codex \xB7 \u53D7\u7BA1\u6A21\u5F0F\uFF08\u5B9E\u9A8C\u6027\uFF09",
1109
+ connectManaged: "\u8FDE\u63A5 Codex",
1110
+ disconnectManaged: "\u65AD\u5F00\u8FDE\u63A5",
1111
+ revokeManaged: "\u64A4\u9500\u6388\u6743",
1112
+ cancelManaged: "\u53D6\u6D88\u53D7\u7BA1\u4EFB\u52A1",
1113
+ resultTitle: "\u53D7\u7BA1\u6267\u884C\u7ED3\u679C",
1114
+ controlConnection: (state, mode) => `\u8FDE\u63A5\uFF1A${state} \xB7 \u6A21\u5F0F\uFF1A${mode}`,
1115
+ controlAuth: (auth, grant) => `\u8BA4\u8BC1\uFF1A${auth} \xB7 \u6388\u6743\uFF1A${grant}`,
1116
+ controlModel: (model) => `\u6A21\u578B\uFF1A${model}`,
1117
+ controlRevision: (task) => `revision ${String(task.revision)}\uFF1A${task.deliveryStatus} / ${task.executionStatus} / ${task.managedPhase}`,
1118
+ controlValidation: (outcome) => `\u9A8C\u8BC1\uFF1A${outcome}`,
1119
+ controlFailure: (error, action) => `\u9519\u8BEF\uFF1A${error} \xB7 \u4E0B\u4E00\u6B65\uFF1A${action}`,
1120
+ controlUnavailable: "\u9875\u9762\u8FDE\u63A5\u7BA1\u7406\u4E0D\u53EF\u7528\uFF1B\u5916\u90E8\u4EA4\u63A5\u4ECD\u4F1A\u6309\u771F\u5B9E\u80FD\u529B\u964D\u7EA7\u4E3A Agent \u6536\u4EF6\u7BB1\u3002",
1121
+ controlErrorText: Object.freeze({
1122
+ AGENT_BINARY_NOT_FOUND: "\u672A\u5B89\u88C5 Codex\uFF0C\u6216 Codex \u4E0D\u5728 PATH \u4E2D\u3002",
1123
+ AGENT_BINARY_UNTRUSTED: "\u89E3\u6790\u5230\u7684 Codex \u53EF\u6267\u884C\u6587\u4EF6\u4E0D\u53EF\u4FE1\u3002",
1124
+ AGENT_VERSION_UNSUPPORTED: "\u5DF2\u5B89\u88C5\u7684 Codex \u7248\u672C\u4E0D\u53D7\u652F\u6301\u3002",
1125
+ APP_SERVER_HANDSHAKE_FAILED: "Codex App Server \u672A\u5B8C\u6210\u542F\u52A8\u63E1\u624B\u3002",
1126
+ AGENT_AUTH_REQUIRED: "Codex \u9700\u8981\u5DF2\u767B\u5F55\u7684\u8D26\u6237\u3002",
1127
+ AGENT_MODEL_UNAVAILABLE: "Codex \u914D\u7F6E\u7684\u6A21\u578B\u5F53\u524D\u4E0D\u53EF\u7528\u3002",
1128
+ AGENT_PROTOCOL_INCOMPATIBLE: "Codex App Server \u534F\u8BAE\u4E0D\u517C\u5BB9\u3002",
1129
+ CODEX_CONFIG_ISOLATION_UNSUPPORTED: "\u5F53\u524D Codex \u7248\u672C\u65E0\u6CD5\u8BC1\u660E\u53D7\u7BA1\u914D\u7F6E\u9694\u79BB\u3002",
1130
+ MANAGED_GRANT_INVALID: "\u4FDD\u5B58\u7684\u9879\u76EE\u6388\u6743\u672A\u901A\u8FC7\u6821\u9A8C\u3002",
1131
+ MANAGED_PLATFORM_UNSUPPORTED: "\u5F53\u524D\u5E73\u53F0\u5C1A\u672A\u8BC1\u660E\u53EF\u5B89\u5168\u8FD0\u884C\u53D7\u7BA1\u6267\u884C\u3002",
1132
+ MANAGED_GIT_REQUIRED: "\u53D7\u7BA1\u6267\u884C\u8981\u6C42\u9879\u76EE\u4F4D\u4E8E Git \u4ED3\u5E93\u4E2D\u3002",
1133
+ MANAGED_SNAPSHOT_FAILED: "\u65E0\u6CD5\u521B\u5EFA\u72EC\u7ACB\u5DE5\u4F5C\u533A\u5FEB\u7167\u3002",
1134
+ MANAGED_SCOPE_VIOLATION: "\u5019\u9009\u4FEE\u6539\u8D85\u51FA\u6388\u6743\u8DEF\u5F84\u3002",
1135
+ MANAGED_CHANGE_LIMIT_EXCEEDED: "\u5019\u9009\u4FEE\u6539\u8D85\u8FC7\u5B89\u5168\u4E0A\u9650\u3002",
1136
+ MANAGED_VALIDATION_FAILED: "required check \u5931\u8D25\u6216\u6539\u53D8\u4E86\u5019\u9009 diff\u3002",
1137
+ MANAGED_WORKSPACE_CONFLICT: "\u6267\u884C\u671F\u95F4\u6388\u6743\u6E90\u7801\u53D1\u751F\u4E86\u53D8\u5316\u3002",
1138
+ MANAGED_APPLY_FAILED: "\u5DF2\u5BA1\u8BA1\u4FEE\u6539\u65E0\u6CD5\u5B89\u5168\u5199\u5165\u3002",
1139
+ MANAGED_CLEANUP_INCOMPLETE: "\u53D7\u7BA1 thread \u6216\u5DE5\u4F5C\u533A\u6E05\u7406\u4E0D\u5B8C\u6574\u3002"
1140
+ }),
1141
+ controlActionText: Object.freeze({
1142
+ "install-agent": "\u5B89\u88C5 Codex \u540E\u91CD\u8BD5\u3002",
1143
+ "use-supported-version": "\u6539\u7528\u53D7\u652F\u6301\u7684 Codex \u7248\u672C\u3002",
1144
+ "sign-in": "\u767B\u5F55 Codex \u540E\u91CD\u8BD5\u3002",
1145
+ "choose-available-model": "\u5728 Codex \u914D\u7F6E\u4E2D\u9009\u62E9\u53EF\u7528\u6A21\u578B\u3002",
1146
+ "use-inbox": "\u7EE7\u7EED\u4F7F\u7528 Agent \u6536\u4EF6\u7BB1\u964D\u7EA7\u8DEF\u5F84\u3002",
1147
+ "confirm-managed-access": "\u64A4\u9500\u65E0\u6548\u6388\u6743\u5E76\u91CD\u65B0\u786E\u8BA4\u3002",
1148
+ "review-candidate-diff": "\u68C0\u67E5\u5019\u9009 diff \u548C\u9A8C\u8BC1\u8F93\u51FA\u3002",
1149
+ "review-workspace-conflict": "\u68C0\u67E5\u5DE5\u4F5C\u533A\u53D8\u5316\u540E\u518D\u91CD\u8BD5\u3002",
1150
+ retry: "\u91CD\u8BD5\u53D7\u7BA1\u8FDE\u63A5\u3002",
1151
+ "inspect-cleanup-warning": "\u786E\u8BA4\u6E05\u7406\u72B6\u6001\u540E\u518D\u8FDE\u63A5\u3002"
1152
+ }),
1153
+ resultHeader: (revision, outcome) => `revision ${String(revision)} \xB7 \u9A8C\u8BC1 ${outcome}`,
1154
+ resultDisposition: (outcome) => outcome === "passed" ? "\u9A8C\u8BC1\u901A\u8FC7\uFF0C\u4FEE\u6539\u5DF2\u5199\u5165\u9879\u76EE\u3002" : `\u8FD9\u53EA\u662F\u5019\u9009 diff\uFF1B\u9A8C\u8BC1\u72B6\u6001\u4E3A ${outcome}\uFF0C\u56E0\u6B64\u6CA1\u6709\u5199\u5165\u9879\u76EE\u3002`,
1155
+ resultTiming: (stage, durationMs) => `\u8017\u65F6 ${stage}: ${String(durationMs)} ms`,
589
1156
  description: "\u6709\u4E3B\u52A8\u9002\u914D\u5668\u5C31\u7ACB\u5373\u6D3E\u53D1\uFF1B\u672A\u8FDE\u63A5\u4E3B\u52A8\u9002\u914D\u5668\u65F6\uFF0C\u8BDA\u5B9E\u964D\u7EA7\u4E3A\u9879\u76EE Agent \u6536\u4EF6\u7BB1\u3002",
590
1157
  ready: "\u672C\u5730 Broker \u5DF2\u5C31\u7EEA\u3002\u53EA\u6709\u8FDE\u63A5\u5668\u8BFB\u53D6\u6216\u7B49\u5F85\u540E\uFF0C\u624D\u80FD\u8BC1\u660E\u53D1\u751F\u8FC7\u8FDE\u63A5\u3002",
591
1158
  readyWaiting: (count) => `\u5F53\u524D\u6709 ${String(count)} \u4E2A\u8FDE\u63A5\u5668\u7B49\u5F85\u8BF7\u6C42\uFF0C\u53D1\u5E03\u540E\u53EF\u7ACB\u5373\u5524\u9192\u3002`,
@@ -609,13 +1176,14 @@ var MESSAGES = Object.freeze({
609
1176
  refresh: "\u5237\u65B0\u53D6\u4EF6\u72B6\u6001",
610
1177
  resolveUnknown: "\u5DF2\u6838\u5BF9\u5DE5\u4F5C\u533A\uFF0C\u5141\u8BB8\u65B0\u4EFB\u52A1",
611
1178
  settingsTitle: "\u9879\u76EE\u8FDE\u63A5\u8BBE\u7F6E",
612
- settingsHelp: "setup \u547D\u4EE4\u5747\u4E3A\u9879\u76EE\u7EA7 dry-run\uFF0C\u6838\u5BF9\u540E\u624D\u8FFD\u52A0 --write\u3002Claude \u4E3B\u52A8\u6A21\u5F0F\u8FD8\u9700\u4F7F\u7528\u4E0B\u65B9 Research Preview \u542F\u52A8\u53C2\u6570\uFF1BCodex \u4E3B\u52A8\u6A21\u5F0F\u65E0\u9700 setup\uFF0C\u53EA\u9700\u663E\u5F0F\u542F\u52A8\u5E76\u4FDD\u6301 Connector \u547D\u4EE4\u8FD0\u884C\u3002",
1179
+ settingsHelp: "Codex \u53D7\u7BA1\u6A21\u5F0F\u7531\u5F53\u524D\u5F00\u53D1\u670D\u52A1\u6258\u7BA1\uFF0C\u6D4F\u89C8\u5668\u4E0D\u4F1A\u83B7\u5F97\u5382\u5546\u51ED\u8BC1\u6216\u4EFB\u610F\u547D\u4EE4\u6743\u9650\uFF1B\u901A\u7528\u4E0E attached adapter \u4ECD\u6309\u771F\u5B9E\u80FD\u529B\u964D\u7EA7\u4E3A\u6536\u4EF6\u7BB1\u3002",
613
1180
  disclosureTitle: "\u786E\u8BA4\u53D1\u5E03\u5230\u5916\u90E8 Agent",
614
1181
  disclosureIntro: (targets, files) => `\u5C06\u53D1\u5E03 ${String(targets)} \u4E2A\u76EE\u6807\uFF0C\u6D89\u53CA ${String(files)} \u4E2A\u9879\u76EE\u76F8\u5BF9\u6587\u4EF6\u3002`,
615
1182
  disclosureData: "\u4EA4\u63A5\u5305\u542B\u4FEE\u6539\u8BF4\u660E\u3001\u7EC4\u4EF6\u4E0E\u9875\u9762\u4E0A\u4E0B\u6587\u3001\u5DF2\u6E05\u6D17\u7684 DOM/CSS\uFF0C\u4EE5\u53CA\u672C\u5730 Node \u670D\u52A1\u91CD\u65B0\u8BFB\u53D6\u7684\u6709\u754C\u6E90\u7801\u3002",
616
1183
  disclosureInbox: "15 \u5206\u949F\u6709\u6548\u671F\u5185\uFF0C\u672C\u9879\u76EE\u4E2D\u4EFB\u4F55\u5DF2\u914D\u7F6E\u7684 SpotPatch Connector \u90FD\u53EF\u80FD\u8BFB\u53D6\u3002",
617
1184
  disclosureProvider: "Agent \u5BBF\u4E3B\u53EF\u80FD\u4F9D\u636E\u5176\u4EA7\u54C1\u6570\u636E\u7B56\u7565\uFF0C\u5C06\u5185\u5BB9\u53D1\u9001\u7ED9\u4E91\u7AEF\u6A21\u578B\u670D\u52A1\u3002",
618
1185
  disclosureNoGuarantee: "\u5916\u90E8\u4FEE\u6539\u4E0D\u5177\u5907 SpotPatch \u5185\u5EFA Agent \u7684 worktree\u3001Apply \u6216 Revert \u4FDD\u8BC1\u3002",
1186
+ disclosureManagedGuarantee: "\u53D7\u7BA1 Codex \u53EA\u5199\u72EC\u7ACB\u4E34\u65F6\u5FEB\u7167\u3002SpotPatch \u4F1A\u5BA1\u8BA1 diff\uFF0C\u4E14\u4EC5\u5728\u6240\u6709\u53EF\u4FE1 required checks \u901A\u8FC7\u540E\u624D\u5199\u5165\u4E1A\u52A1\u4ED3\u5E93\u3002",
619
1187
  cancel: "\u53D6\u6D88",
620
1188
  confirm: "\u786E\u8BA4\u5E76\u53D1\u9001",
621
1189
  error: (code) => code === "EXTERNAL_AGENT_BUSY" ? "\u4E3B\u52A8 Agent \u4ECD\u5728\u5DE5\u4F5C\uFF0C\u6216\u5B58\u5728\u5C1A\u672A\u6838\u5BF9\u7684\u6295\u9012\u672A\u77E5\u72B6\u6001\u3002" : code === "ACTIVE_DISPATCH_INVALID" ? "\u4E3B\u52A8\u6295\u9012\u72B6\u6001\u5DF2\u7ECF\u53D8\u5316\uFF0C\u8BF7\u5237\u65B0\u540E\u518D\u7EE7\u7EED\u3002" : code === "HANDOFF_SOURCE_STALE" ? "\u9009\u4E2D\u6E90\u7801\u5DF2\u7ECF\u53D8\u5316\uFF0C\u8BF7\u91CD\u65B0\u9009\u62E9\u5F53\u524D\u7EC4\u4EF6\u3002" : code === "HANDOFF_RESPONSE_TOO_LARGE" ? "\u4EA4\u63A5\u5185\u5BB9\u8D85\u8FC7\u4E0A\u9650\uFF0C\u8BF7\u51CF\u5C11\u6240\u9009\u4E0A\u4E0B\u6587\u3002" : code === "EXTERNAL_HANDOFF_DISABLED" ? "\u53EF\u4FE1\u9879\u76EE\u914D\u7F6E\u6CA1\u6709\u542F\u7528\u5916\u90E8 Agent \u4EA4\u63A5\u3002" : code === "EXTERNAL_HANDOFF_UNAVAILABLE" || code === "SESSION_CLOSED" ? "\u672C\u5730\u5916\u90E8 Agent Broker \u4E0D\u53EF\u7528\uFF0C\u8BF7\u91CD\u542F\u5F00\u53D1\u670D\u52A1\u3002" : "\u5916\u90E8 Agent \u4EA4\u63A5\u8BF7\u6C42\u5931\u8D25\uFF0C\u672A\u53D1\u5E03\u4EFB\u4F55\u90E8\u5206 revision\u3002"
@@ -632,9 +1200,17 @@ function createStyles(document) {
632
1200
  .spotpatch-external-status { padding-left: 10px; border-left: 2px solid var(--spotpatch-accent-cyan); color: #cbd5e1; }
633
1201
  .spotpatch-external-status[data-state="error"] { border-color: var(--spotpatch-danger); color: #fecdd3; }
634
1202
  .spotpatch-external-status[data-state="picked-up"] { border-color: var(--spotpatch-success); color: #a7f3d0; }
1203
+ .spotpatch-external-control { margin-top: 9px; padding: 9px; border: 1px solid var(--spotpatch-border); border-radius: 8px; background: rgb(3 7 18 / 28%); }
1204
+ .spotpatch-external-control label { display: grid; gap: 4px; color: var(--spotpatch-text-secondary); font-size: 10px; }
1205
+ .spotpatch-external-control select { width: 100%; padding: 6px 8px; border: 1px solid var(--spotpatch-border); border-radius: 6px; background: var(--spotpatch-bg-input); color: var(--spotpatch-text); font: inherit; }
1206
+ .spotpatch-external-control-status { margin: 7px 0 0; color: #cbd5e1; font-size: 10.5px; line-height: 1.5; white-space: pre-wrap; }
1207
+ .spotpatch-external-control-actions { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 8px; }
1208
+ .spotpatch-external-control-actions button { padding: 5px 8px; border: 1px solid var(--spotpatch-border); border-radius: 6px; background: var(--spotpatch-bg-active); color: var(--spotpatch-text); font: inherit; font-size: 10px; cursor: pointer; }
1209
+ .spotpatch-external-control-actions button:disabled { cursor: default; opacity: .45; }
1210
+ .spotpatch-external-result { margin-top: 8px; color: var(--spotpatch-text-secondary); font-size: 10px; }
1211
+ .spotpatch-external-result pre { max-height: 180px; overflow: auto; margin: 6px 0 0; padding: 7px; border-radius: 6px; background: var(--spotpatch-bg-input); color: #d8d6ff; font: 9px/1.45 ui-monospace, SFMono-Regular, Menlo, monospace; white-space: pre; }
635
1212
  .spotpatch-external-settings { margin-top: 9px; }
636
1213
  .spotpatch-external-settings summary { cursor: pointer; color: #c4b5fd; font-size: 11px; }
637
- .spotpatch-external-command { display: block; margin-top: 6px; padding: 6px 8px; overflow: auto; border-radius: 6px; background: var(--spotpatch-bg-input); color: #d8d6ff; font: 10px/1.45 ui-monospace, SFMono-Regular, Menlo, monospace; white-space: nowrap; }
638
1214
  .spotpatch-external-refresh { padding: 3px 7px; border: 1px solid var(--spotpatch-border); border-radius: 6px; background: transparent; color: var(--spotpatch-text-secondary); font: inherit; font-size: 10px; cursor: pointer; }
639
1215
  .spotpatch-external-refresh:disabled { cursor: default; opacity: .45; }
640
1216
  .spotpatch-external-resolve { margin-top: 8px; padding: 6px 8px; border: 1px solid #f59e0b; border-radius: 6px; background: rgb(245 158 11 / 10%); color: #fde68a; font: inherit; font-size: 10px; cursor: pointer; }
@@ -695,6 +1271,44 @@ function disclosurePaths(annotation) {
695
1271
  )
696
1272
  ]);
697
1273
  }
1274
+ function controlStatusText(value, messages) {
1275
+ const model = value.effectiveModel ?? value.requestedModel;
1276
+ const task = value.task;
1277
+ const parts = [
1278
+ messages.controlConnection(value.connectionState, value.mode),
1279
+ messages.controlAuth(value.authReadiness, value.grantState),
1280
+ ...model === void 0 ? [] : [messages.controlModel(model)],
1281
+ ...task === void 0 ? [] : [
1282
+ messages.controlRevision(task),
1283
+ ...task.validationOutcome === void 0 ? [] : [messages.controlValidation(task.validationOutcome)]
1284
+ ],
1285
+ ...value.error === void 0 ? [] : [
1286
+ messages.controlFailure(
1287
+ messages.controlErrorText[value.error.code],
1288
+ messages.controlActionText[value.error.action]
1289
+ )
1290
+ ]
1291
+ ];
1292
+ return parts.join("\n");
1293
+ }
1294
+ function managedResultText(result, messages) {
1295
+ const files = result.files.map(
1296
+ (file) => `${file.path} +${String(file.additions)} -${String(file.deletions)}`
1297
+ );
1298
+ const checks = result.checks.map(
1299
+ (check) => `${check.id}: ${check.outcome} (${String(check.durationMs)} ms${check.exitCode === void 0 ? "" : `, exit ${String(check.exitCode)}`})`
1300
+ );
1301
+ const timings = Object.entries(result.timings).flatMap(
1302
+ ([stage, durationMs]) => durationMs === void 0 ? [] : [messages.resultTiming(stage, durationMs)]
1303
+ );
1304
+ return [
1305
+ messages.resultHeader(result.revision, result.validationOutcome),
1306
+ messages.resultDisposition(result.validationOutcome),
1307
+ ...files,
1308
+ ...checks,
1309
+ ...timings
1310
+ ].join("\n");
1311
+ }
698
1312
  function createExternalHandoffPanel(document, framework, locale, sessionId, subscribeLocale, onViewChange) {
699
1313
  const options = {
700
1314
  document,
@@ -712,7 +1326,10 @@ function createExternalHandoffPanel(document, framework, locale, sessionId, subs
712
1326
  let retryable = false;
713
1327
  let unknownDelivery = false;
714
1328
  let contextReady = false;
1329
+ let controlOperationBusy = false;
715
1330
  let visible = false;
1331
+ let control;
1332
+ let managedResultValue;
716
1333
  let pendingDisclosure;
717
1334
  let previousFocus;
718
1335
  const consentKey = `spotpatch:external-handoff-consent:${options.sessionId}`;
@@ -730,27 +1347,46 @@ function createExternalHandoffPanel(document, framework, locale, sessionId, subs
730
1347
  status.className = "spotpatch-external-status";
731
1348
  status.setAttribute("role", "status");
732
1349
  status.setAttribute("aria-live", "polite");
1350
+ const controlRoot = createMarkedElement(document, "div");
1351
+ controlRoot.className = "spotpatch-external-control";
1352
+ const agentLabel = createMarkedElement(document, "label");
1353
+ const agentLabelText = createMarkedElement(document, "span");
1354
+ const agentSelect = createMarkedElement(document, "select");
1355
+ const codexOption = createMarkedElement(document, "option");
1356
+ codexOption.value = "codex";
1357
+ agentSelect.append(codexOption);
1358
+ agentLabel.append(agentLabelText, agentSelect);
1359
+ const controlStatus = createMarkedElement(document, "p");
1360
+ controlStatus.className = "spotpatch-external-control-status";
1361
+ controlStatus.setAttribute("role", "status");
1362
+ const controlActions = createMarkedElement(document, "div");
1363
+ controlActions.className = "spotpatch-external-control-actions";
1364
+ const connectButton = createButton(document, "");
1365
+ const disconnectButton = createButton(document, "");
1366
+ const revokeButton = createButton(document, "");
1367
+ const cancelManagedButton = createButton(document, "");
1368
+ controlActions.append(
1369
+ connectButton,
1370
+ disconnectButton,
1371
+ revokeButton,
1372
+ cancelManagedButton
1373
+ );
1374
+ const managedResult = createMarkedElement(document, "details");
1375
+ managedResult.className = "spotpatch-external-result";
1376
+ managedResult.hidden = true;
1377
+ const managedResultTitle = createMarkedElement(document, "summary");
1378
+ const managedResultSummary = createMarkedElement(document, "p");
1379
+ const managedResultDiff = createMarkedElement(document, "pre");
1380
+ managedResult.append(managedResultTitle, managedResultSummary, managedResultDiff);
1381
+ controlRoot.append(agentLabel, controlStatus, controlActions, managedResult);
733
1382
  const resolveButton = createButton(document, "", "spotpatch-external-resolve");
734
1383
  resolveButton.hidden = true;
735
1384
  const settings = createMarkedElement(document, "details");
736
1385
  settings.className = "spotpatch-external-settings";
737
1386
  const settingsTitle = createMarkedElement(document, "summary");
738
1387
  const settingsHelp = createMarkedElement(document, "p");
739
- const cliPrefix = `node ./node_modules/@spotpatch/${options.framework}/dist/cli.js`;
740
- const bridgePrefix = `${cliPrefix} bridge`;
741
- const commands = [
742
- `${bridgePrefix} setup --client claude --scope project --mode active`,
743
- "MCP_PROTOCOL_NEGOTIATION=legacy claude --dangerously-load-development-channels server:spotpatch",
744
- `${cliPrefix} connect codex --allow-workspace-write`,
745
- `${bridgePrefix} setup --client cursor --scope project`
746
- ].map((value) => {
747
- const command = createMarkedElement(document, "code");
748
- command.className = "spotpatch-external-command";
749
- command.textContent = value;
750
- return command;
751
- });
752
- settings.append(settingsTitle, settingsHelp, ...commands);
753
- root.append(heading, description, status, resolveButton, settings);
1388
+ settings.append(settingsTitle, settingsHelp);
1389
+ root.append(heading, description, controlRoot, status, resolveButton, settings);
754
1390
  const sendButton = createButton(document, "", "spotpatch-primary");
755
1391
  sendButton.hidden = true;
756
1392
  const disclosure = createMarkedElement(document, "div");
@@ -807,6 +1443,23 @@ function createExternalHandoffPanel(document, framework, locale, sessionId, subs
807
1443
  resolveButton.hidden = !unknownDelivery;
808
1444
  resolveButton.disabled = !visible || operationBusy || !unknownDelivery;
809
1445
  };
1446
+ const refreshControlActions = () => {
1447
+ if (control === void 0) {
1448
+ connectButton.disabled = true;
1449
+ disconnectButton.disabled = true;
1450
+ revokeButton.disabled = true;
1451
+ cancelManagedButton.hidden = true;
1452
+ return;
1453
+ }
1454
+ const state = control.connectionState;
1455
+ const operationPending = controlOperationBusy || state === "diagnosing" || state === "connecting" || state === "disconnecting";
1456
+ connectButton.disabled = operationPending || state === "ready" || state === "busy";
1457
+ disconnectButton.disabled = operationPending || state === "disconnected";
1458
+ revokeButton.disabled = operationPending || control.grantState !== "valid";
1459
+ const phase = control.task?.managedPhase;
1460
+ cancelManagedButton.hidden = phase === void 0 || phase === "completed" || phase === "review-required" || phase === "failed" || phase === "cancelled" || phase === "cleanup-warning";
1461
+ cancelManagedButton.disabled = operationPending;
1462
+ };
810
1463
  const settleDisclosure = (confirmed) => {
811
1464
  if (pendingDisclosure === void 0) return;
812
1465
  const resolve = pendingDisclosure;
@@ -850,10 +1503,28 @@ function createExternalHandoffPanel(document, framework, locale, sessionId, subs
850
1503
  disclosureInbox.textContent = messages.disclosureInbox;
851
1504
  disclosureProvider.textContent = messages.disclosureProvider;
852
1505
  disclosureNoGuarantee.textContent = messages.disclosureNoGuarantee;
1506
+ agentLabelText.textContent = messages.agentLabel;
1507
+ codexOption.textContent = messages.codexManaged;
1508
+ connectButton.textContent = messages.connectManaged;
1509
+ disconnectButton.textContent = messages.disconnectManaged;
1510
+ revokeButton.textContent = messages.revokeManaged;
1511
+ cancelManagedButton.textContent = messages.cancelManaged;
1512
+ managedResultTitle.textContent = messages.resultTitle;
1513
+ if (control !== void 0) {
1514
+ controlStatus.textContent = controlStatusText(control, messages);
1515
+ disclosureNoGuarantee.textContent = control.mode === "managed" ? messages.disclosureManagedGuarantee : messages.disclosureNoGuarantee;
1516
+ }
1517
+ if (managedResultValue !== void 0) {
1518
+ managedResultSummary.textContent = managedResultText(
1519
+ managedResultValue,
1520
+ messages
1521
+ );
1522
+ }
853
1523
  cancelButton.textContent = messages.cancel;
854
1524
  confirmButton.textContent = messages.confirm;
855
1525
  resolveButton.textContent = messages.resolveUnknown;
856
1526
  refreshActions();
1527
+ refreshControlActions();
857
1528
  };
858
1529
  cancelButton.addEventListener("click", () => {
859
1530
  settleDisclosure(false);
@@ -863,10 +1534,13 @@ function createExternalHandoffPanel(document, framework, locale, sessionId, subs
863
1534
  });
864
1535
  disclosure.addEventListener("keydown", handleDisclosureKeydown);
865
1536
  settings.addEventListener("toggle", options.onViewChange);
1537
+ managedResult.addEventListener("toggle", options.onViewChange);
866
1538
  const unsubscribeLocale = options.subscribeLocale(applyMessages);
867
1539
  applyMessages();
868
1540
  status.textContent = messages.ready;
1541
+ controlStatus.textContent = options.locale() === "zh-CN" ? "\u6B63\u5728\u8BFB\u53D6\u672C\u5730\u8FDE\u63A5\u72B6\u6001\u2026\u2026" : "Reading local connection status\u2026";
869
1542
  refreshActions();
1543
+ refreshControlActions();
870
1544
  const summaryMessage = (summary) => summary.state === "expired" ? messages.expired(summary.revision) : summary.state === "superseded" ? messages.superseded(summary.revision) : summary.pickupCount > 0 ? messages.pickedUp(
871
1545
  summary.revision,
872
1546
  summary.pickupCount,
@@ -900,6 +1574,10 @@ function createExternalHandoffPanel(document, framework, locale, sessionId, subs
900
1574
  sendButton,
901
1575
  refreshButton,
902
1576
  resolveButton,
1577
+ cancelManagedButton,
1578
+ connectButton,
1579
+ disconnectButton,
1580
+ revokeButton,
903
1581
  confirmDisclosure(annotation) {
904
1582
  if (hasConsent()) return Promise.resolve(true);
905
1583
  if (pendingDisclosure !== void 0) return Promise.resolve(false);
@@ -951,6 +1629,30 @@ function createExternalHandoffPanel(document, framework, locale, sessionId, subs
951
1629
  renderStatus(result) {
952
1630
  applyStatus(result);
953
1631
  },
1632
+ renderControlStatus(value) {
1633
+ control = value;
1634
+ controlStatus.textContent = controlStatusText(value, messages);
1635
+ controlStatus.dataset.state = value.connectionState;
1636
+ disclosureNoGuarantee.textContent = value.mode === "managed" ? messages.disclosureManagedGuarantee : messages.disclosureNoGuarantee;
1637
+ refreshControlActions();
1638
+ options.onViewChange();
1639
+ },
1640
+ renderControlUnavailable() {
1641
+ control = void 0;
1642
+ controlStatus.dataset.state = "unavailable";
1643
+ controlStatus.textContent = messages.controlUnavailable;
1644
+ disclosureNoGuarantee.textContent = messages.disclosureNoGuarantee;
1645
+ refreshControlActions();
1646
+ options.onViewChange();
1647
+ },
1648
+ renderManagedResult(result) {
1649
+ managedResultValue = result;
1650
+ managedResult.hidden = false;
1651
+ managedResult.open = true;
1652
+ managedResultSummary.textContent = managedResultText(result, messages);
1653
+ managedResultDiff.textContent = result.diff;
1654
+ options.onViewChange();
1655
+ },
954
1656
  renderError(code, canRetry = false) {
955
1657
  operationBusy = false;
956
1658
  retryable = canRetry;
@@ -964,6 +1666,10 @@ function createExternalHandoffPanel(document, framework, locale, sessionId, subs
964
1666
  operationBusy = nextBusy;
965
1667
  refreshActions();
966
1668
  },
1669
+ setControlBusy(nextBusy) {
1670
+ controlOperationBusy = nextBusy;
1671
+ refreshControlActions();
1672
+ },
967
1673
  setContextReady(ready) {
968
1674
  contextReady = ready;
969
1675
  refreshActions();
@@ -979,6 +1685,7 @@ function createExternalHandoffPanel(document, framework, locale, sessionId, subs
979
1685
  settleDisclosure(false);
980
1686
  disclosure.removeEventListener("keydown", handleDisclosureKeydown);
981
1687
  settings.removeEventListener("toggle", options.onViewChange);
1688
+ managedResult.removeEventListener("toggle", options.onViewChange);
982
1689
  unsubscribeLocale();
983
1690
  }
984
1691
  });