@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.
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  getExternalHandoffExtension,
3
3
  registerExternalHandoffExtension
4
- } from "./chunk-XBD55BCF.js";
4
+ } from "./chunk-BXRMVHNT.js";
5
5
  import {
6
6
  createButton,
7
7
  createMarkedElement
@@ -9,24 +9,18 @@ import {
9
9
 
10
10
  // src/controller/external-handoff-workflow.ts
11
11
  import {
12
- ERROR_CODES,
12
+ ERROR_CODES as ERROR_CODES2,
13
+ EXTERNAL_AGENT_CONTROL_LIMITS as EXTERNAL_AGENT_CONTROL_LIMITS2,
14
+ EXTERNAL_AGENT_MANAGED_PROFILE,
13
15
  EXTERNAL_HANDOFF_BROKER_PROTOCOL_VERSION,
14
16
  EXTERNAL_HANDOFF_LIMITS,
15
17
  EXTERNAL_HANDOFF_SNAPSHOT_SCHEMA_VERSION,
16
- SPOTPATCH_ENDPOINTS,
17
- SPOTPATCH_TOKEN_HEADER,
18
+ SPOTPATCH_ENDPOINTS as SPOTPATCH_ENDPOINTS2,
19
+ SPOTPATCH_TOKEN_HEADER as SPOTPATCH_TOKEN_HEADER2,
18
20
  isErrorCode
19
21
  } from "@spotpatch/shared/external-handoff-browser";
20
- var MAX_SUMMARY_RESPONSE_BYTES = 64 * 1024;
21
- var OPAQUE_ID_PATTERN = /^[A-Za-z0-9_-]{22,128}$/u;
22
- var ExternalHandoffApiError = class extends Error {
23
- constructor(code) {
24
- super("SpotPatch external handoff API request failed.");
25
- this.code = code;
26
- this.name = "ExternalHandoffApiError";
27
- }
28
- code;
29
- };
22
+
23
+ // src/controller/browser-api.ts
30
24
  function record(value) {
31
25
  return typeof value === "object" && value !== null && !Array.isArray(value);
32
26
  }
@@ -36,8 +30,374 @@ function exactKeys(value, keys) {
36
30
  return actual.length === expected.length && actual.every((key, index) => key === expected[index]);
37
31
  }
38
32
  function validTimestamp(value) {
39
- return typeof value === "string" && Number.isFinite(Date.parse(value));
33
+ if (typeof value !== "string") return false;
34
+ const match = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})(?:\.\d+)?Z$/u.exec(
35
+ value
36
+ );
37
+ if (match === null) return false;
38
+ const timestamp = Date.parse(value);
39
+ if (!Number.isFinite(timestamp)) return false;
40
+ const date = new Date(timestamp);
41
+ 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]);
42
+ }
43
+ async function readBoundedJson(response, maximumBytes) {
44
+ const declaredLength = Number(response.headers.get("content-length"));
45
+ if (Number.isFinite(declaredLength) && declaredLength > maximumBytes) {
46
+ await response.body?.cancel();
47
+ throw new TypeError("SpotPatch API response exceeds its safety limit.");
48
+ }
49
+ if (response.body === null) {
50
+ throw new TypeError("SpotPatch API response body is unavailable.");
51
+ }
52
+ const reader = response.body.getReader();
53
+ const chunks = [];
54
+ let total = 0;
55
+ try {
56
+ for (; ; ) {
57
+ const chunk = await reader.read();
58
+ if (chunk.done) break;
59
+ total += chunk.value.byteLength;
60
+ if (total > maximumBytes) {
61
+ await reader.cancel();
62
+ throw new TypeError("SpotPatch API response exceeds its safety limit.");
63
+ }
64
+ chunks.push(chunk.value);
65
+ }
66
+ } finally {
67
+ reader.releaseLock();
68
+ }
69
+ const payload = new Uint8Array(total);
70
+ let offset = 0;
71
+ for (const chunk of chunks) {
72
+ payload.set(chunk, offset);
73
+ offset += chunk.byteLength;
74
+ }
75
+ return JSON.parse(
76
+ new TextDecoder("utf-8", { fatal: true }).decode(payload)
77
+ );
78
+ }
79
+ function successData(value) {
80
+ if (!record(value) || !exactKeys(value, ["data", "ok"]) || value.ok !== true) {
81
+ throw new TypeError("SpotPatch API success envelope is invalid.");
82
+ }
83
+ return value.data;
84
+ }
85
+
86
+ // src/controller/external-agent-control-client.ts
87
+ import {
88
+ EXTERNAL_AGENT_ACTIONS as ACTIONS,
89
+ EXTERNAL_AGENT_AUTH_READINESS as AUTH_STATES,
90
+ EXTERNAL_AGENT_CHECK_OUTCOMES as CHECK_OUTCOMES,
91
+ EXTERNAL_AGENT_CONNECTION_STATES as CONNECTION_STATES,
92
+ EXTERNAL_AGENT_CONTROL_LIMITS,
93
+ EXTERNAL_AGENT_DELIVERY_STATUSES as DELIVERY_STATES,
94
+ EXTERNAL_AGENT_ERROR_CODES as ERROR_CODES,
95
+ EXTERNAL_AGENT_ERROR_RECOVERABILITY as RECOVERABILITY,
96
+ EXTERNAL_AGENT_ERROR_STAGES as ERROR_STAGES,
97
+ EXTERNAL_AGENT_EXECUTION_STATUSES as EXECUTION_STATES,
98
+ EXTERNAL_AGENT_GRANT_STATES as GRANT_STATES,
99
+ EXTERNAL_AGENT_MANAGED_PHASES as MANAGED_PHASES,
100
+ EXTERNAL_AGENT_MODES as MODES,
101
+ EXTERNAL_AGENT_VALIDATION_OUTCOMES as VALIDATION_OUTCOMES,
102
+ SPOTPATCH_ENDPOINTS,
103
+ SPOTPATCH_TOKEN_HEADER
104
+ } from "@spotpatch/shared/external-handoff-browser";
105
+ var RESPONSE_OVERHEAD_BYTES = 64 * 1024;
106
+ function member(values, value) {
107
+ return typeof value === "string" && values.includes(value);
108
+ }
109
+ function integer(value, minimum = 0) {
110
+ return Number.isSafeInteger(value) && value >= minimum;
111
+ }
112
+ function optionalKeys(value, base, optional) {
113
+ return exactKeys(value, [
114
+ ...base,
115
+ ...optional.filter((key) => value[key] !== void 0)
116
+ ]);
117
+ }
118
+ function safePath(value) {
119
+ if (typeof value !== "string" || value.length === 0 || value.length > 4096 || value.startsWith("/") || value.includes("\\")) {
120
+ return false;
121
+ }
122
+ return value.split("/").every((segment) => segment.length > 0 && segment !== "." && segment !== "..");
123
+ }
124
+ function parseFiles(value) {
125
+ if (!Array.isArray(value) || value.length > EXTERNAL_AGENT_CONTROL_LIMITS.maximumChangedFiles) {
126
+ throw new TypeError("Invalid managed file summaries.");
127
+ }
128
+ return value.map((file) => {
129
+ if (!record(file) || !exactKeys(file, ["additions", "deletions", "path"]) || !safePath(file.path) || !integer(file.additions) || !integer(file.deletions)) {
130
+ throw new TypeError("Invalid managed file summary.");
131
+ }
132
+ return Object.freeze({
133
+ path: file.path,
134
+ additions: file.additions,
135
+ deletions: file.deletions
136
+ });
137
+ });
40
138
  }
139
+ function parseChecks(value) {
140
+ if (!Array.isArray(value) || value.length > EXTERNAL_AGENT_CONTROL_LIMITS.maximumChecks) {
141
+ throw new TypeError("Invalid managed check summaries.");
142
+ }
143
+ return value.map((check) => {
144
+ if (!record(check) || !optionalKeys(check, ["durationMs", "id", "outcome"], ["exitCode"]) || typeof check.id !== "string" || !/^[A-Za-z0-9._-]{1,128}$/u.test(check.id) || !member(CHECK_OUTCOMES, check.outcome) || !integer(check.durationMs) || check.exitCode !== void 0 && !Number.isSafeInteger(check.exitCode)) {
145
+ throw new TypeError("Invalid managed check summary.");
146
+ }
147
+ return Object.freeze({
148
+ id: check.id,
149
+ outcome: check.outcome,
150
+ durationMs: check.durationMs,
151
+ ...check.exitCode === void 0 ? {} : { exitCode: check.exitCode }
152
+ });
153
+ });
154
+ }
155
+ function parseTimings(value) {
156
+ const keys = [
157
+ "preparing",
158
+ "agent",
159
+ "auditing",
160
+ "validating",
161
+ "applying",
162
+ "total"
163
+ ];
164
+ if (!record(value) || !optionalKeys(value, [], keys)) {
165
+ throw new TypeError("Invalid managed timings.");
166
+ }
167
+ for (const key of keys) {
168
+ if (value[key] !== void 0 && !integer(value[key])) {
169
+ throw new TypeError("Invalid managed timing.");
170
+ }
171
+ }
172
+ return Object.freeze(
173
+ Object.fromEntries(
174
+ keys.flatMap((key) => value[key] === void 0 ? [] : [[key, value[key]]])
175
+ )
176
+ );
177
+ }
178
+ function parseTask(value) {
179
+ if (!record(value) || !optionalKeys(
180
+ value,
181
+ [
182
+ "checks",
183
+ "deliveryStatus",
184
+ "executionStatus",
185
+ "files",
186
+ "managedPhase",
187
+ "revision",
188
+ "timings"
189
+ ],
190
+ ["resultExpiresAt", "validationOutcome"]
191
+ ) || !integer(value.revision, 1) || !member(DELIVERY_STATES, value.deliveryStatus) || !member(EXECUTION_STATES, value.executionStatus) || !member(MANAGED_PHASES, value.managedPhase) || value.validationOutcome !== void 0 && !member(VALIDATION_OUTCOMES, value.validationOutcome) || value.resultExpiresAt !== void 0 && !validTimestamp(value.resultExpiresAt)) {
192
+ throw new TypeError("Invalid managed task status.");
193
+ }
194
+ return Object.freeze({
195
+ revision: value.revision,
196
+ deliveryStatus: value.deliveryStatus,
197
+ executionStatus: value.executionStatus,
198
+ managedPhase: value.managedPhase,
199
+ ...value.validationOutcome === void 0 ? {} : { validationOutcome: value.validationOutcome },
200
+ files: parseFiles(value.files),
201
+ checks: parseChecks(value.checks),
202
+ timings: parseTimings(value.timings),
203
+ ...value.resultExpiresAt === void 0 ? {} : { resultExpiresAt: value.resultExpiresAt }
204
+ });
205
+ }
206
+ function parseExternalAgentControlStatus(value) {
207
+ if (!record(value) || !optionalKeys(
208
+ value,
209
+ [
210
+ "adapter",
211
+ "authReadiness",
212
+ "connectionState",
213
+ "grantState",
214
+ "mode",
215
+ "schemaVersion",
216
+ "sequence",
217
+ "updatedAt"
218
+ ],
219
+ ["effectiveModel", "error", "requestedModel", "task"]
220
+ ) || value.schemaVersion !== 1 || !integer(value.sequence) || !member(MODES, value.mode) || !member(CONNECTION_STATES, value.connectionState) || !member(AUTH_STATES, value.authReadiness) || !member(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") {
221
+ throw new TypeError("Invalid external Agent control status.");
222
+ }
223
+ let error;
224
+ if (value.error !== void 0) {
225
+ if (!record(value.error) || !exactKeys(value.error, ["action", "code", "recoverability", "stage"]) || !member(ERROR_CODES, value.error.code) || !member(ERROR_STAGES, value.error.stage) || !member(RECOVERABILITY, value.error.recoverability) || !member(ACTIONS, value.error.action)) {
226
+ throw new TypeError("Invalid external Agent error.");
227
+ }
228
+ error = Object.freeze({
229
+ code: value.error.code,
230
+ stage: value.error.stage,
231
+ recoverability: value.error.recoverability,
232
+ action: value.error.action
233
+ });
234
+ }
235
+ return Object.freeze({
236
+ schemaVersion: 1,
237
+ sequence: value.sequence,
238
+ mode: value.mode,
239
+ adapter: Object.freeze({
240
+ kind: "codex",
241
+ maturity: "experimental",
242
+ availability: value.adapter.availability
243
+ }),
244
+ connectionState: value.connectionState,
245
+ authReadiness: value.authReadiness,
246
+ grantState: value.grantState,
247
+ ...value.requestedModel === void 0 ? {} : { requestedModel: value.requestedModel },
248
+ ...value.effectiveModel === void 0 ? {} : { effectiveModel: value.effectiveModel },
249
+ ...value.task === void 0 ? {} : { task: parseTask(value.task) },
250
+ ...error === void 0 ? {} : { error },
251
+ updatedAt: value.updatedAt
252
+ });
253
+ }
254
+ function parseExternalAgentManagedResult(value) {
255
+ if (!record(value) || !exactKeys(value, [
256
+ "checks",
257
+ "diff",
258
+ "expiresAt",
259
+ "files",
260
+ "revision",
261
+ "timings",
262
+ "validationOutcome"
263
+ ]) || !integer(value.revision, 1) || typeof value.diff !== "string" || value.diff.length > EXTERNAL_AGENT_CONTROL_LIMITS.maximumResultDiffBytes || !validTimestamp(value.expiresAt) || !member(VALIDATION_OUTCOMES, value.validationOutcome)) {
264
+ throw new TypeError("Invalid managed Agent result.");
265
+ }
266
+ return Object.freeze({
267
+ revision: value.revision,
268
+ diff: value.diff,
269
+ files: parseFiles(value.files),
270
+ checks: parseChecks(value.checks),
271
+ timings: parseTimings(value.timings),
272
+ validationOutcome: value.validationOutcome,
273
+ expiresAt: value.expiresAt
274
+ });
275
+ }
276
+ function parseEvent(value) {
277
+ if (!record(value) || typeof value.type !== "string") {
278
+ throw new TypeError("Invalid external Agent event.");
279
+ }
280
+ if (value.type === "status" && exactKeys(value, ["data", "type"])) {
281
+ return Object.freeze({
282
+ type: "status",
283
+ data: parseExternalAgentControlStatus(value.data)
284
+ });
285
+ }
286
+ if (value.type === "heartbeat" && exactKeys(value, ["emittedAt", "sequence", "type"]) && integer(value.sequence) && validTimestamp(value.emittedAt)) {
287
+ return Object.freeze({
288
+ type: "heartbeat",
289
+ sequence: value.sequence,
290
+ emittedAt: value.emittedAt
291
+ });
292
+ }
293
+ throw new TypeError("Invalid external Agent event.");
294
+ }
295
+ function createExternalAgentControlClient(fetch, sessionToken) {
296
+ const request = async (endpoint, body, maximumBytes = RESPONSE_OVERHEAD_BYTES) => {
297
+ const response = await fetch(endpoint, {
298
+ method: "POST",
299
+ headers: {
300
+ "Content-Type": "application/json",
301
+ [SPOTPATCH_TOKEN_HEADER]: sessionToken
302
+ },
303
+ body: JSON.stringify(body)
304
+ });
305
+ const envelope = await readBoundedJson(response, maximumBytes);
306
+ if (!response.ok) throw new TypeError("External Agent control request failed.");
307
+ return successData(envelope);
308
+ };
309
+ return Object.freeze({
310
+ async status() {
311
+ return parseExternalAgentControlStatus(
312
+ await request(SPOTPATCH_ENDPOINTS.externalAgentControlStatus, {})
313
+ );
314
+ },
315
+ async connect(value) {
316
+ return parseExternalAgentControlStatus(
317
+ await request(SPOTPATCH_ENDPOINTS.externalAgentControlConnect, value)
318
+ );
319
+ },
320
+ async disconnect(value) {
321
+ return parseExternalAgentControlStatus(
322
+ await request(SPOTPATCH_ENDPOINTS.externalAgentControlDisconnect, value)
323
+ );
324
+ },
325
+ async cancel(value) {
326
+ return parseExternalAgentControlStatus(
327
+ await request(SPOTPATCH_ENDPOINTS.externalAgentControlCancel, value)
328
+ );
329
+ },
330
+ async result(revision) {
331
+ return parseExternalAgentManagedResult(
332
+ await request(
333
+ SPOTPATCH_ENDPOINTS.externalAgentResult,
334
+ { revision },
335
+ EXTERNAL_AGENT_CONTROL_LIMITS.maximumResultDiffBytes + RESPONSE_OVERHEAD_BYTES
336
+ )
337
+ );
338
+ },
339
+ async events(afterSequence, signal, onEvent) {
340
+ const response = await fetch(SPOTPATCH_ENDPOINTS.externalAgentEvents, {
341
+ method: "POST",
342
+ headers: {
343
+ "Content-Type": "application/json",
344
+ [SPOTPATCH_TOKEN_HEADER]: sessionToken
345
+ },
346
+ body: JSON.stringify(afterSequence === void 0 ? {} : { afterSequence }),
347
+ signal
348
+ });
349
+ if (!response.ok || response.body === null) {
350
+ await response.body?.cancel();
351
+ throw new TypeError("External Agent event stream is unavailable.");
352
+ }
353
+ const reader = response.body.getReader();
354
+ let pending = new Uint8Array(0);
355
+ try {
356
+ for (; ; ) {
357
+ const chunk = await reader.read();
358
+ if (chunk.done) break;
359
+ const combined = new Uint8Array(pending.byteLength + chunk.value.byteLength);
360
+ combined.set(pending);
361
+ combined.set(chunk.value, pending.byteLength);
362
+ pending = combined;
363
+ for (; ; ) {
364
+ const newline = pending.indexOf(10);
365
+ if (newline === -1) break;
366
+ if (newline === 0 || newline > EXTERNAL_AGENT_CONTROL_LIMITS.maximumEventLineBytes) {
367
+ throw new TypeError("External Agent event line is invalid.");
368
+ }
369
+ const line = pending.slice(0, newline);
370
+ pending = pending.slice(newline + 1);
371
+ const value = JSON.parse(
372
+ new TextDecoder("utf-8", { fatal: true }).decode(line)
373
+ );
374
+ onEvent(parseEvent(value));
375
+ }
376
+ if (pending.byteLength > EXTERNAL_AGENT_CONTROL_LIMITS.maximumEventLineBytes) {
377
+ throw new TypeError("External Agent event line exceeds its limit.");
378
+ }
379
+ }
380
+ if (pending.byteLength !== 0) {
381
+ throw new TypeError("External Agent event stream ended mid-record.");
382
+ }
383
+ } finally {
384
+ reader.releaseLock();
385
+ }
386
+ }
387
+ });
388
+ }
389
+
390
+ // src/controller/external-handoff-workflow.ts
391
+ var MAX_SUMMARY_RESPONSE_BYTES = 64 * 1024;
392
+ var OPAQUE_ID_PATTERN = /^[A-Za-z0-9_-]{22,128}$/u;
393
+ var ExternalHandoffApiError = class extends Error {
394
+ constructor(code) {
395
+ super("SpotPatch external handoff API request failed.");
396
+ this.code = code;
397
+ this.name = "ExternalHandoffApiError";
398
+ }
399
+ code;
400
+ };
41
401
  function parseActiveAdapter(value) {
42
402
  if (value === null) return null;
43
403
  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)) {
@@ -162,53 +522,9 @@ function parseSummary(value) {
162
522
  ...value.pickedUpAt === void 0 ? {} : { pickedUpAt: value.pickedUpAt }
163
523
  });
164
524
  }
165
- async function boundedJson(response) {
166
- const declaredLength = Number(response.headers.get("content-length"));
167
- if (Number.isFinite(declaredLength) && declaredLength > MAX_SUMMARY_RESPONSE_BYTES) {
168
- await response.body?.cancel();
169
- throw new ExternalHandoffApiError();
170
- }
171
- if (response.body === null) throw new ExternalHandoffApiError();
172
- const reader = response.body.getReader();
173
- const chunks = [];
174
- let total = 0;
175
- try {
176
- for (; ; ) {
177
- const chunk = await reader.read();
178
- if (chunk.done) break;
179
- total += chunk.value.byteLength;
180
- if (total > MAX_SUMMARY_RESPONSE_BYTES) {
181
- await reader.cancel();
182
- throw new ExternalHandoffApiError();
183
- }
184
- chunks.push(chunk.value);
185
- }
186
- } finally {
187
- reader.releaseLock();
188
- }
189
- const payload = new Uint8Array(total);
190
- let offset = 0;
191
- for (const chunk of chunks) {
192
- payload.set(chunk, offset);
193
- offset += chunk.byteLength;
194
- }
195
- try {
196
- return JSON.parse(
197
- new TextDecoder("utf-8", { fatal: true }).decode(payload)
198
- );
199
- } catch {
200
- throw new ExternalHandoffApiError();
201
- }
202
- }
203
525
  function failureCode(value) {
204
526
  return record(value) && value.ok === false && record(value.error) && isErrorCode(value.error.code) ? value.error.code : void 0;
205
527
  }
206
- function successData(value) {
207
- if (!record(value) || !exactKeys(value, ["data", "ok"]) || value.ok !== true) {
208
- throw new ExternalHandoffApiError();
209
- }
210
- return value.data;
211
- }
212
528
  function omitBrowserCode(annotation) {
213
529
  return {
214
530
  ...annotation,
@@ -241,14 +557,23 @@ function api(options) {
241
557
  method: "POST",
242
558
  headers: {
243
559
  "Content-Type": "application/json",
244
- [SPOTPATCH_TOKEN_HEADER]: options.sessionToken
560
+ [SPOTPATCH_TOKEN_HEADER2]: options.sessionToken
245
561
  },
246
562
  body: JSON.stringify(body),
247
563
  signal: controller.signal
248
564
  });
249
- const envelope = await boundedJson(response);
565
+ let envelope;
566
+ try {
567
+ envelope = await readBoundedJson(response, MAX_SUMMARY_RESPONSE_BYTES);
568
+ } catch {
569
+ throw new ExternalHandoffApiError();
570
+ }
250
571
  if (!response.ok) throw new ExternalHandoffApiError(failureCode(envelope));
251
- return successData(envelope);
572
+ try {
573
+ return successData(envelope);
574
+ } catch {
575
+ throw new ExternalHandoffApiError();
576
+ }
252
577
  } finally {
253
578
  pending.delete(controller);
254
579
  }
@@ -260,12 +585,12 @@ function api(options) {
260
585
  },
261
586
  async capability() {
262
587
  return parseCapability(
263
- await request(SPOTPATCH_ENDPOINTS.externalHandoffCapability, {})
588
+ await request(SPOTPATCH_ENDPOINTS2.externalHandoffCapability, {})
264
589
  );
265
590
  },
266
591
  async publish(requestId, annotation) {
267
592
  return parsePublishResult(
268
- await request(SPOTPATCH_ENDPOINTS.externalHandoffPublish, {
593
+ await request(SPOTPATCH_ENDPOINTS2.externalHandoffPublish, {
269
594
  annotation: omitBrowserCode(annotation),
270
595
  requestId
271
596
  })
@@ -274,14 +599,14 @@ function api(options) {
274
599
  async status(cursor) {
275
600
  return parseStatusResult(
276
601
  await request(
277
- SPOTPATCH_ENDPOINTS.externalHandoffStatus,
602
+ SPOTPATCH_ENDPOINTS2.externalHandoffStatus,
278
603
  cursor === void 0 ? {} : { cursor }
279
604
  )
280
605
  );
281
606
  },
282
607
  async resolveDelivery(cursor) {
283
608
  return parseStatusResult(
284
- await request(SPOTPATCH_ENDPOINTS.externalHandoffResolveDelivery, {
609
+ await request(SPOTPATCH_ENDPOINTS2.externalHandoffResolveDelivery, {
285
610
  confirmation: "workspace-reviewed",
286
611
  cursor
287
612
  })
@@ -298,6 +623,7 @@ function createExternalHandoffWorkflow(fetch, panel, selectedAnnotation, session
298
623
  window
299
624
  };
300
625
  const client = api(options);
626
+ const controlClient = createExternalAgentControlClient(fetch, sessionToken);
301
627
  const timers = /* @__PURE__ */ new Set();
302
628
  const lifecycle = {
303
629
  disposed: false,
@@ -306,12 +632,152 @@ function createExternalHandoffWorkflow(fetch, panel, selectedAnnotation, session
306
632
  userActionPending: false
307
633
  };
308
634
  let capability;
635
+ let controlStatus;
636
+ let controlEventController;
637
+ let controlReconnectDelay = EXTERNAL_AGENT_CONTROL_LIMITS2.eventReconnectMinimumMs;
638
+ let controlReconnectTimer;
639
+ let controlActionPending = false;
640
+ let managedResultRevision;
641
+ let managedResultLoadingRevision;
309
642
  let current;
310
643
  let retryablePublish;
644
+ const isDisposed = () => lifecycle.disposed;
311
645
  const clearTimers = () => {
312
646
  for (const timer of timers) options.window.clearTimeout(timer);
313
647
  timers.clear();
314
648
  };
649
+ const clearControlReconnect = () => {
650
+ if (controlReconnectTimer === void 0) return;
651
+ options.window.clearTimeout(controlReconnectTimer);
652
+ controlReconnectTimer = void 0;
653
+ };
654
+ const renderManagedResult = async (revision) => {
655
+ if (lifecycle.disposed || managedResultRevision === revision || managedResultLoadingRevision === revision) {
656
+ return;
657
+ }
658
+ managedResultLoadingRevision = revision;
659
+ try {
660
+ const result = await controlClient.result(revision);
661
+ if (isDisposed() || controlStatus?.task?.revision !== revision) return;
662
+ managedResultRevision = revision;
663
+ options.panel.renderManagedResult(result);
664
+ } catch {
665
+ } finally {
666
+ if (managedResultLoadingRevision === revision) {
667
+ managedResultLoadingRevision = void 0;
668
+ }
669
+ }
670
+ };
671
+ const applyControlStatus = (value) => {
672
+ if (controlStatus !== void 0 && value.sequence < controlStatus.sequence) return;
673
+ controlStatus = value;
674
+ controlReconnectDelay = EXTERNAL_AGENT_CONTROL_LIMITS2.eventReconnectMinimumMs;
675
+ options.panel.renderControlStatus(value);
676
+ const resultRevision = value.task?.resultExpiresAt === void 0 ? void 0 : value.task.revision;
677
+ if (resultRevision !== void 0) void renderManagedResult(resultRevision);
678
+ };
679
+ const handleControlEvent = (event) => {
680
+ if (event.type === "status") {
681
+ applyControlStatus(event.data);
682
+ return;
683
+ }
684
+ const resultRevision = controlStatus?.task?.resultExpiresAt === void 0 ? void 0 : controlStatus.task.revision;
685
+ if (resultRevision !== void 0) void renderManagedResult(resultRevision);
686
+ };
687
+ const startControlEventStream = () => {
688
+ if (lifecycle.disposed || !lifecycle.mounted || controlEventController !== void 0) {
689
+ return;
690
+ }
691
+ clearControlReconnect();
692
+ const controller = new AbortController();
693
+ controlEventController = controller;
694
+ void controlClient.events(controlStatus?.sequence, controller.signal, handleControlEvent).catch((error) => {
695
+ if (lifecycle.disposed || error instanceof DOMException && error.name === "AbortError") {
696
+ return;
697
+ }
698
+ if (controlStatus === void 0) options.panel.renderControlUnavailable();
699
+ }).finally(() => {
700
+ if (controlEventController === controller) {
701
+ controlEventController = void 0;
702
+ }
703
+ if (lifecycle.disposed || !lifecycle.mounted) return;
704
+ const delay = controlReconnectDelay;
705
+ controlReconnectDelay = Math.min(
706
+ delay * 2,
707
+ EXTERNAL_AGENT_CONTROL_LIMITS2.eventReconnectMaximumMs
708
+ );
709
+ controlReconnectTimer = options.window.setTimeout(() => {
710
+ controlReconnectTimer = void 0;
711
+ startControlEventStream();
712
+ }, delay);
713
+ });
714
+ };
715
+ const refreshControlStatus = async () => {
716
+ applyControlStatus(await controlClient.status());
717
+ };
718
+ const bootstrapControl = async () => {
719
+ try {
720
+ await refreshControlStatus();
721
+ } catch {
722
+ if (!lifecycle.disposed) options.panel.renderControlUnavailable();
723
+ } finally {
724
+ if (!lifecycle.disposed) startControlEventStream();
725
+ }
726
+ };
727
+ const performControlAction = async (action) => {
728
+ if (lifecycle.disposed || controlActionPending) return;
729
+ controlActionPending = true;
730
+ options.panel.setControlBusy(true);
731
+ try {
732
+ applyControlStatus(await action());
733
+ } catch {
734
+ try {
735
+ await refreshControlStatus();
736
+ } catch {
737
+ if (!isDisposed()) options.panel.renderControlUnavailable();
738
+ }
739
+ } finally {
740
+ controlActionPending = false;
741
+ if (!isDisposed()) options.panel.setControlBusy(false);
742
+ }
743
+ };
744
+ const handleConnectClick = () => {
745
+ void performControlAction(
746
+ () => controlClient.connect({
747
+ requestId: createRequestId(options.window),
748
+ adapterKind: "codex",
749
+ profile: EXTERNAL_AGENT_MANAGED_PROFILE
750
+ })
751
+ );
752
+ };
753
+ const handleDisconnectClick = () => {
754
+ void performControlAction(
755
+ () => controlClient.disconnect({
756
+ requestId: createRequestId(options.window),
757
+ adapterKind: "codex",
758
+ revokeGrant: false
759
+ })
760
+ );
761
+ };
762
+ const handleRevokeClick = () => {
763
+ void performControlAction(
764
+ () => controlClient.disconnect({
765
+ requestId: createRequestId(options.window),
766
+ adapterKind: "codex",
767
+ revokeGrant: true
768
+ })
769
+ );
770
+ };
771
+ const handleManagedCancelClick = () => {
772
+ const revision = controlStatus?.task?.revision;
773
+ if (revision === void 0) return;
774
+ void performControlAction(
775
+ () => controlClient.cancel({
776
+ requestId: createRequestId(options.window),
777
+ revision
778
+ })
779
+ );
780
+ };
315
781
  const errorCode = (error) => error instanceof ExternalHandoffApiError ? error.code : void 0;
316
782
  const restorePanel = () => {
317
783
  if (current !== void 0) options.panel.renderStatus(current);
@@ -374,7 +840,7 @@ function createExternalHandoffWorkflow(fetch, panel, selectedAnnotation, session
374
840
  const annotation = options.selectedAnnotation();
375
841
  if (annotation === void 0) {
376
842
  lifecycle.userActionPending = false;
377
- options.panel.renderError(ERROR_CODES.HANDOFF_VALIDATION_FAILED);
843
+ options.panel.renderError(ERROR_CODES2.HANDOFF_VALIDATION_FAILED);
378
844
  return;
379
845
  }
380
846
  const disclosureRevision = lifecycle.operation;
@@ -495,7 +961,15 @@ function createExternalHandoffWorkflow(fetch, panel, selectedAnnotation, session
495
961
  options.panel.sendButton.addEventListener("click", handleSendClick);
496
962
  options.panel.refreshButton.addEventListener("click", handleRefreshClick);
497
963
  options.panel.resolveButton.addEventListener("click", handleResolveClick);
964
+ options.panel.connectButton.addEventListener("click", handleConnectClick);
965
+ options.panel.disconnectButton.addEventListener("click", handleDisconnectClick);
966
+ options.panel.revokeButton.addEventListener("click", handleRevokeClick);
967
+ options.panel.cancelManagedButton.addEventListener(
968
+ "click",
969
+ handleManagedCancelClick
970
+ );
498
971
  void refreshCapability(lifecycle.operation);
972
+ void bootstrapControl();
499
973
  },
500
974
  cancelPending,
501
975
  dispose() {
@@ -505,10 +979,23 @@ function createExternalHandoffWorkflow(fetch, panel, selectedAnnotation, session
505
979
  lifecycle.userActionPending = false;
506
980
  retryablePublish = void 0;
507
981
  clearTimers();
982
+ clearControlReconnect();
983
+ controlEventController?.abort("workflow-disposed");
984
+ controlEventController = void 0;
508
985
  client.cancel();
509
986
  options.panel.sendButton.removeEventListener("click", handleSendClick);
510
987
  options.panel.refreshButton.removeEventListener("click", handleRefreshClick);
511
988
  options.panel.resolveButton.removeEventListener("click", handleResolveClick);
989
+ options.panel.connectButton.removeEventListener("click", handleConnectClick);
990
+ options.panel.disconnectButton.removeEventListener(
991
+ "click",
992
+ handleDisconnectClick
993
+ );
994
+ options.panel.revokeButton.removeEventListener("click", handleRevokeClick);
995
+ options.panel.cancelManagedButton.removeEventListener(
996
+ "click",
997
+ handleManagedCancelClick
998
+ );
512
999
  }
513
1000
  });
514
1001
  }
@@ -517,6 +1004,55 @@ function createExternalHandoffWorkflow(fetch, panel, selectedAnnotation, session
517
1004
  var MESSAGES = Object.freeze({
518
1005
  "en-US": Object.freeze({
519
1006
  title: "External Agent connection",
1007
+ agentLabel: "Agent",
1008
+ codexManaged: "Codex \xB7 managed (experimental)",
1009
+ connectManaged: "Connect Codex",
1010
+ disconnectManaged: "Disconnect",
1011
+ revokeManaged: "Revoke grant",
1012
+ cancelManaged: "Cancel managed task",
1013
+ resultTitle: "Managed result",
1014
+ controlConnection: (state, mode) => `Connection: ${state} \xB7 Mode: ${mode}`,
1015
+ controlAuth: (auth, grant) => `Auth: ${auth} \xB7 Grant: ${grant}`,
1016
+ controlModel: (model) => `Model: ${model}`,
1017
+ controlRevision: (task) => `Revision ${String(task.revision)}: ${task.deliveryStatus} / ${task.executionStatus} / ${task.managedPhase}`,
1018
+ controlValidation: (outcome) => `Validation: ${outcome}`,
1019
+ controlFailure: (error, action) => `Error: ${error} \xB7 Next: ${action}`,
1020
+ controlUnavailable: "Page connection management is unavailable; external handoffs still fall back honestly to the Agent inbox.",
1021
+ controlErrorText: Object.freeze({
1022
+ AGENT_BINARY_NOT_FOUND: "Codex is not installed or is not on PATH.",
1023
+ AGENT_BINARY_UNTRUSTED: "The resolved Codex executable is not trusted.",
1024
+ AGENT_VERSION_UNSUPPORTED: "The installed Codex version is unsupported.",
1025
+ APP_SERVER_HANDSHAKE_FAILED: "Codex App Server did not complete startup.",
1026
+ AGENT_AUTH_REQUIRED: "Codex requires an authenticated account.",
1027
+ AGENT_MODEL_UNAVAILABLE: "The configured Codex model is unavailable.",
1028
+ AGENT_PROTOCOL_INCOMPATIBLE: "The Codex App Server protocol is incompatible.",
1029
+ CODEX_CONFIG_ISOLATION_UNSUPPORTED: "This Codex version cannot prove managed configuration isolation.",
1030
+ MANAGED_GRANT_INVALID: "The saved project grant failed validation.",
1031
+ MANAGED_PLATFORM_UNSUPPORTED: "Managed execution is not proven safe on this platform.",
1032
+ MANAGED_GIT_REQUIRED: "Managed execution requires a Git repository.",
1033
+ MANAGED_SNAPSHOT_FAILED: "The independent workspace snapshot could not be created.",
1034
+ MANAGED_SCOPE_VIOLATION: "The candidate change exceeded its authorized paths.",
1035
+ MANAGED_CHANGE_LIMIT_EXCEEDED: "The candidate change exceeded safety limits.",
1036
+ MANAGED_VALIDATION_FAILED: "A required check failed or changed the candidate diff.",
1037
+ MANAGED_WORKSPACE_CONFLICT: "An authorized source file changed during execution.",
1038
+ MANAGED_APPLY_FAILED: "The audited change could not be applied safely.",
1039
+ MANAGED_CLEANUP_INCOMPLETE: "Managed thread or workspace cleanup is incomplete."
1040
+ }),
1041
+ controlActionText: Object.freeze({
1042
+ "install-agent": "Install Codex and retry.",
1043
+ "use-supported-version": "Use a supported Codex version.",
1044
+ "sign-in": "Sign in with Codex, then retry.",
1045
+ "choose-available-model": "Choose an available model in Codex configuration.",
1046
+ "use-inbox": "Continue with the Agent inbox fallback.",
1047
+ "confirm-managed-access": "Revoke the invalid grant and confirm access again.",
1048
+ "review-candidate-diff": "Review the candidate diff and validation output.",
1049
+ "review-workspace-conflict": "Review the workspace changes before retrying.",
1050
+ retry: "Retry the managed connection.",
1051
+ "inspect-cleanup-warning": "Inspect cleanup status before reconnecting."
1052
+ }),
1053
+ resultHeader: (revision, outcome) => `Revision ${String(revision)} \xB7 Validation ${outcome}`,
1054
+ resultDisposition: (outcome) => outcome === "passed" ? "Applied to the project after validation passed." : `Candidate only; it was not applied because validation is ${outcome}.`,
1055
+ resultTiming: (stage, durationMs) => `Timing ${stage}: ${String(durationMs)} ms`,
520
1056
  description: "Send through a ready active adapter, or publish to the project Agent inbox when no active adapter is connected.",
521
1057
  ready: "Local Broker ready. No Agent connection is assumed until a connector reads or waits.",
522
1058
  readyWaiting: (count) => `${String(count)} active connector wait request(s); publishing can wake them immediately.`,
@@ -540,19 +1076,69 @@ var MESSAGES = Object.freeze({
540
1076
  refresh: "Refresh pickup status",
541
1077
  resolveUnknown: "Workspace reviewed \u2014 allow a new task",
542
1078
  settingsTitle: "Project connection setup",
543
- 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.",
1079
+ 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.",
544
1080
  disclosureTitle: "Confirm external Agent handoff",
545
1081
  disclosureIntro: (targets, files) => `Publish ${String(targets)} selected target(s) referencing ${String(files)} relative file(s).`,
546
1082
  disclosureData: "The handoff includes your instructions, component/page context, sanitized DOM/CSS, and bounded source read again by the local Node service.",
547
1083
  disclosureInbox: "Any SpotPatch connector configured for this project may read it during the 15-minute lifetime.",
548
1084
  disclosureProvider: "The Agent host may send the content to its cloud provider under that product's data policy.",
549
1085
  disclosureNoGuarantee: "External edits do not receive SpotPatch's built-in worktree, Apply, or Revert guarantees.",
1086
+ disclosureManagedGuarantee: "Managed Codex edits run in an independent temporary snapshot. SpotPatch audits the diff and only applies it when every trusted required check passes.",
550
1087
  cancel: "Cancel",
551
1088
  confirm: "Confirm and send",
552
1089
  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."
553
1090
  }),
554
1091
  "zh-CN": Object.freeze({
555
1092
  title: "\u5916\u90E8 Agent \u8FDE\u63A5",
1093
+ agentLabel: "Agent",
1094
+ codexManaged: "Codex \xB7 \u53D7\u7BA1\u6A21\u5F0F\uFF08\u5B9E\u9A8C\u6027\uFF09",
1095
+ connectManaged: "\u8FDE\u63A5 Codex",
1096
+ disconnectManaged: "\u65AD\u5F00\u8FDE\u63A5",
1097
+ revokeManaged: "\u64A4\u9500\u6388\u6743",
1098
+ cancelManaged: "\u53D6\u6D88\u53D7\u7BA1\u4EFB\u52A1",
1099
+ resultTitle: "\u53D7\u7BA1\u6267\u884C\u7ED3\u679C",
1100
+ controlConnection: (state, mode) => `\u8FDE\u63A5\uFF1A${state} \xB7 \u6A21\u5F0F\uFF1A${mode}`,
1101
+ controlAuth: (auth, grant) => `\u8BA4\u8BC1\uFF1A${auth} \xB7 \u6388\u6743\uFF1A${grant}`,
1102
+ controlModel: (model) => `\u6A21\u578B\uFF1A${model}`,
1103
+ controlRevision: (task) => `revision ${String(task.revision)}\uFF1A${task.deliveryStatus} / ${task.executionStatus} / ${task.managedPhase}`,
1104
+ controlValidation: (outcome) => `\u9A8C\u8BC1\uFF1A${outcome}`,
1105
+ controlFailure: (error, action) => `\u9519\u8BEF\uFF1A${error} \xB7 \u4E0B\u4E00\u6B65\uFF1A${action}`,
1106
+ 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",
1107
+ controlErrorText: Object.freeze({
1108
+ AGENT_BINARY_NOT_FOUND: "\u672A\u5B89\u88C5 Codex\uFF0C\u6216 Codex \u4E0D\u5728 PATH \u4E2D\u3002",
1109
+ AGENT_BINARY_UNTRUSTED: "\u89E3\u6790\u5230\u7684 Codex \u53EF\u6267\u884C\u6587\u4EF6\u4E0D\u53EF\u4FE1\u3002",
1110
+ AGENT_VERSION_UNSUPPORTED: "\u5DF2\u5B89\u88C5\u7684 Codex \u7248\u672C\u4E0D\u53D7\u652F\u6301\u3002",
1111
+ APP_SERVER_HANDSHAKE_FAILED: "Codex App Server \u672A\u5B8C\u6210\u542F\u52A8\u63E1\u624B\u3002",
1112
+ AGENT_AUTH_REQUIRED: "Codex \u9700\u8981\u5DF2\u767B\u5F55\u7684\u8D26\u6237\u3002",
1113
+ AGENT_MODEL_UNAVAILABLE: "Codex \u914D\u7F6E\u7684\u6A21\u578B\u5F53\u524D\u4E0D\u53EF\u7528\u3002",
1114
+ AGENT_PROTOCOL_INCOMPATIBLE: "Codex App Server \u534F\u8BAE\u4E0D\u517C\u5BB9\u3002",
1115
+ CODEX_CONFIG_ISOLATION_UNSUPPORTED: "\u5F53\u524D Codex \u7248\u672C\u65E0\u6CD5\u8BC1\u660E\u53D7\u7BA1\u914D\u7F6E\u9694\u79BB\u3002",
1116
+ MANAGED_GRANT_INVALID: "\u4FDD\u5B58\u7684\u9879\u76EE\u6388\u6743\u672A\u901A\u8FC7\u6821\u9A8C\u3002",
1117
+ MANAGED_PLATFORM_UNSUPPORTED: "\u5F53\u524D\u5E73\u53F0\u5C1A\u672A\u8BC1\u660E\u53EF\u5B89\u5168\u8FD0\u884C\u53D7\u7BA1\u6267\u884C\u3002",
1118
+ MANAGED_GIT_REQUIRED: "\u53D7\u7BA1\u6267\u884C\u8981\u6C42\u9879\u76EE\u4F4D\u4E8E Git \u4ED3\u5E93\u4E2D\u3002",
1119
+ MANAGED_SNAPSHOT_FAILED: "\u65E0\u6CD5\u521B\u5EFA\u72EC\u7ACB\u5DE5\u4F5C\u533A\u5FEB\u7167\u3002",
1120
+ MANAGED_SCOPE_VIOLATION: "\u5019\u9009\u4FEE\u6539\u8D85\u51FA\u6388\u6743\u8DEF\u5F84\u3002",
1121
+ MANAGED_CHANGE_LIMIT_EXCEEDED: "\u5019\u9009\u4FEE\u6539\u8D85\u8FC7\u5B89\u5168\u4E0A\u9650\u3002",
1122
+ MANAGED_VALIDATION_FAILED: "required check \u5931\u8D25\u6216\u6539\u53D8\u4E86\u5019\u9009 diff\u3002",
1123
+ MANAGED_WORKSPACE_CONFLICT: "\u6267\u884C\u671F\u95F4\u6388\u6743\u6E90\u7801\u53D1\u751F\u4E86\u53D8\u5316\u3002",
1124
+ MANAGED_APPLY_FAILED: "\u5DF2\u5BA1\u8BA1\u4FEE\u6539\u65E0\u6CD5\u5B89\u5168\u5199\u5165\u3002",
1125
+ MANAGED_CLEANUP_INCOMPLETE: "\u53D7\u7BA1 thread \u6216\u5DE5\u4F5C\u533A\u6E05\u7406\u4E0D\u5B8C\u6574\u3002"
1126
+ }),
1127
+ controlActionText: Object.freeze({
1128
+ "install-agent": "\u5B89\u88C5 Codex \u540E\u91CD\u8BD5\u3002",
1129
+ "use-supported-version": "\u6539\u7528\u53D7\u652F\u6301\u7684 Codex \u7248\u672C\u3002",
1130
+ "sign-in": "\u767B\u5F55 Codex \u540E\u91CD\u8BD5\u3002",
1131
+ "choose-available-model": "\u5728 Codex \u914D\u7F6E\u4E2D\u9009\u62E9\u53EF\u7528\u6A21\u578B\u3002",
1132
+ "use-inbox": "\u7EE7\u7EED\u4F7F\u7528 Agent \u6536\u4EF6\u7BB1\u964D\u7EA7\u8DEF\u5F84\u3002",
1133
+ "confirm-managed-access": "\u64A4\u9500\u65E0\u6548\u6388\u6743\u5E76\u91CD\u65B0\u786E\u8BA4\u3002",
1134
+ "review-candidate-diff": "\u68C0\u67E5\u5019\u9009 diff \u548C\u9A8C\u8BC1\u8F93\u51FA\u3002",
1135
+ "review-workspace-conflict": "\u68C0\u67E5\u5DE5\u4F5C\u533A\u53D8\u5316\u540E\u518D\u91CD\u8BD5\u3002",
1136
+ retry: "\u91CD\u8BD5\u53D7\u7BA1\u8FDE\u63A5\u3002",
1137
+ "inspect-cleanup-warning": "\u786E\u8BA4\u6E05\u7406\u72B6\u6001\u540E\u518D\u8FDE\u63A5\u3002"
1138
+ }),
1139
+ resultHeader: (revision, outcome) => `revision ${String(revision)} \xB7 \u9A8C\u8BC1 ${outcome}`,
1140
+ 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`,
1141
+ resultTiming: (stage, durationMs) => `\u8017\u65F6 ${stage}: ${String(durationMs)} ms`,
556
1142
  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",
557
1143
  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",
558
1144
  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`,
@@ -576,13 +1162,14 @@ var MESSAGES = Object.freeze({
576
1162
  refresh: "\u5237\u65B0\u53D6\u4EF6\u72B6\u6001",
577
1163
  resolveUnknown: "\u5DF2\u6838\u5BF9\u5DE5\u4F5C\u533A\uFF0C\u5141\u8BB8\u65B0\u4EFB\u52A1",
578
1164
  settingsTitle: "\u9879\u76EE\u8FDE\u63A5\u8BBE\u7F6E",
579
- 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",
1165
+ 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",
580
1166
  disclosureTitle: "\u786E\u8BA4\u53D1\u5E03\u5230\u5916\u90E8 Agent",
581
1167
  disclosureIntro: (targets, files) => `\u5C06\u53D1\u5E03 ${String(targets)} \u4E2A\u76EE\u6807\uFF0C\u6D89\u53CA ${String(files)} \u4E2A\u9879\u76EE\u76F8\u5BF9\u6587\u4EF6\u3002`,
582
1168
  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",
583
1169
  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",
584
1170
  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",
585
1171
  disclosureNoGuarantee: "\u5916\u90E8\u4FEE\u6539\u4E0D\u5177\u5907 SpotPatch \u5185\u5EFA Agent \u7684 worktree\u3001Apply \u6216 Revert \u4FDD\u8BC1\u3002",
1172
+ 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",
586
1173
  cancel: "\u53D6\u6D88",
587
1174
  confirm: "\u786E\u8BA4\u5E76\u53D1\u9001",
588
1175
  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"
@@ -599,9 +1186,17 @@ function createStyles(document) {
599
1186
  .spotpatch-external-status { padding-left: 10px; border-left: 2px solid var(--spotpatch-accent-cyan); color: #cbd5e1; }
600
1187
  .spotpatch-external-status[data-state="error"] { border-color: var(--spotpatch-danger); color: #fecdd3; }
601
1188
  .spotpatch-external-status[data-state="picked-up"] { border-color: var(--spotpatch-success); color: #a7f3d0; }
1189
+ .spotpatch-external-control { margin-top: 9px; padding: 9px; border: 1px solid var(--spotpatch-border); border-radius: 8px; background: rgb(3 7 18 / 28%); }
1190
+ .spotpatch-external-control label { display: grid; gap: 4px; color: var(--spotpatch-text-secondary); font-size: 10px; }
1191
+ .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; }
1192
+ .spotpatch-external-control-status { margin: 7px 0 0; color: #cbd5e1; font-size: 10.5px; line-height: 1.5; white-space: pre-wrap; }
1193
+ .spotpatch-external-control-actions { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 8px; }
1194
+ .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; }
1195
+ .spotpatch-external-control-actions button:disabled { cursor: default; opacity: .45; }
1196
+ .spotpatch-external-result { margin-top: 8px; color: var(--spotpatch-text-secondary); font-size: 10px; }
1197
+ .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; }
602
1198
  .spotpatch-external-settings { margin-top: 9px; }
603
1199
  .spotpatch-external-settings summary { cursor: pointer; color: #c4b5fd; font-size: 11px; }
604
- .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; }
605
1200
  .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; }
606
1201
  .spotpatch-external-refresh:disabled { cursor: default; opacity: .45; }
607
1202
  .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; }
@@ -662,6 +1257,44 @@ function disclosurePaths(annotation) {
662
1257
  )
663
1258
  ]);
664
1259
  }
1260
+ function controlStatusText(value, messages) {
1261
+ const model = value.effectiveModel ?? value.requestedModel;
1262
+ const task = value.task;
1263
+ const parts = [
1264
+ messages.controlConnection(value.connectionState, value.mode),
1265
+ messages.controlAuth(value.authReadiness, value.grantState),
1266
+ ...model === void 0 ? [] : [messages.controlModel(model)],
1267
+ ...task === void 0 ? [] : [
1268
+ messages.controlRevision(task),
1269
+ ...task.validationOutcome === void 0 ? [] : [messages.controlValidation(task.validationOutcome)]
1270
+ ],
1271
+ ...value.error === void 0 ? [] : [
1272
+ messages.controlFailure(
1273
+ messages.controlErrorText[value.error.code],
1274
+ messages.controlActionText[value.error.action]
1275
+ )
1276
+ ]
1277
+ ];
1278
+ return parts.join("\n");
1279
+ }
1280
+ function managedResultText(result, messages) {
1281
+ const files = result.files.map(
1282
+ (file) => `${file.path} +${String(file.additions)} -${String(file.deletions)}`
1283
+ );
1284
+ const checks = result.checks.map(
1285
+ (check) => `${check.id}: ${check.outcome} (${String(check.durationMs)} ms${check.exitCode === void 0 ? "" : `, exit ${String(check.exitCode)}`})`
1286
+ );
1287
+ const timings = Object.entries(result.timings).flatMap(
1288
+ ([stage, durationMs]) => durationMs === void 0 ? [] : [messages.resultTiming(stage, durationMs)]
1289
+ );
1290
+ return [
1291
+ messages.resultHeader(result.revision, result.validationOutcome),
1292
+ messages.resultDisposition(result.validationOutcome),
1293
+ ...files,
1294
+ ...checks,
1295
+ ...timings
1296
+ ].join("\n");
1297
+ }
665
1298
  function createExternalHandoffPanel(document, framework, locale, sessionId, subscribeLocale, onViewChange) {
666
1299
  const options = {
667
1300
  document,
@@ -679,7 +1312,10 @@ function createExternalHandoffPanel(document, framework, locale, sessionId, subs
679
1312
  let retryable = false;
680
1313
  let unknownDelivery = false;
681
1314
  let contextReady = false;
1315
+ let controlOperationBusy = false;
682
1316
  let visible = false;
1317
+ let control;
1318
+ let managedResultValue;
683
1319
  let pendingDisclosure;
684
1320
  let previousFocus;
685
1321
  const consentKey = `spotpatch:external-handoff-consent:${options.sessionId}`;
@@ -697,27 +1333,46 @@ function createExternalHandoffPanel(document, framework, locale, sessionId, subs
697
1333
  status.className = "spotpatch-external-status";
698
1334
  status.setAttribute("role", "status");
699
1335
  status.setAttribute("aria-live", "polite");
1336
+ const controlRoot = createMarkedElement(document, "div");
1337
+ controlRoot.className = "spotpatch-external-control";
1338
+ const agentLabel = createMarkedElement(document, "label");
1339
+ const agentLabelText = createMarkedElement(document, "span");
1340
+ const agentSelect = createMarkedElement(document, "select");
1341
+ const codexOption = createMarkedElement(document, "option");
1342
+ codexOption.value = "codex";
1343
+ agentSelect.append(codexOption);
1344
+ agentLabel.append(agentLabelText, agentSelect);
1345
+ const controlStatus = createMarkedElement(document, "p");
1346
+ controlStatus.className = "spotpatch-external-control-status";
1347
+ controlStatus.setAttribute("role", "status");
1348
+ const controlActions = createMarkedElement(document, "div");
1349
+ controlActions.className = "spotpatch-external-control-actions";
1350
+ const connectButton = createButton(document, "");
1351
+ const disconnectButton = createButton(document, "");
1352
+ const revokeButton = createButton(document, "");
1353
+ const cancelManagedButton = createButton(document, "");
1354
+ controlActions.append(
1355
+ connectButton,
1356
+ disconnectButton,
1357
+ revokeButton,
1358
+ cancelManagedButton
1359
+ );
1360
+ const managedResult = createMarkedElement(document, "details");
1361
+ managedResult.className = "spotpatch-external-result";
1362
+ managedResult.hidden = true;
1363
+ const managedResultTitle = createMarkedElement(document, "summary");
1364
+ const managedResultSummary = createMarkedElement(document, "p");
1365
+ const managedResultDiff = createMarkedElement(document, "pre");
1366
+ managedResult.append(managedResultTitle, managedResultSummary, managedResultDiff);
1367
+ controlRoot.append(agentLabel, controlStatus, controlActions, managedResult);
700
1368
  const resolveButton = createButton(document, "", "spotpatch-external-resolve");
701
1369
  resolveButton.hidden = true;
702
1370
  const settings = createMarkedElement(document, "details");
703
1371
  settings.className = "spotpatch-external-settings";
704
1372
  const settingsTitle = createMarkedElement(document, "summary");
705
1373
  const settingsHelp = createMarkedElement(document, "p");
706
- const cliPrefix = `node ./node_modules/@spotpatch/${options.framework}/dist/cli.js`;
707
- const bridgePrefix = `${cliPrefix} bridge`;
708
- const commands = [
709
- `${bridgePrefix} setup --client claude --scope project --mode active`,
710
- "MCP_PROTOCOL_NEGOTIATION=legacy claude --dangerously-load-development-channels server:spotpatch",
711
- `${cliPrefix} connect codex --allow-workspace-write`,
712
- `${bridgePrefix} setup --client cursor --scope project`
713
- ].map((value) => {
714
- const command = createMarkedElement(document, "code");
715
- command.className = "spotpatch-external-command";
716
- command.textContent = value;
717
- return command;
718
- });
719
- settings.append(settingsTitle, settingsHelp, ...commands);
720
- root.append(heading, description, status, resolveButton, settings);
1374
+ settings.append(settingsTitle, settingsHelp);
1375
+ root.append(heading, description, controlRoot, status, resolveButton, settings);
721
1376
  const sendButton = createButton(document, "", "spotpatch-primary");
722
1377
  sendButton.hidden = true;
723
1378
  const disclosure = createMarkedElement(document, "div");
@@ -774,6 +1429,23 @@ function createExternalHandoffPanel(document, framework, locale, sessionId, subs
774
1429
  resolveButton.hidden = !unknownDelivery;
775
1430
  resolveButton.disabled = !visible || operationBusy || !unknownDelivery;
776
1431
  };
1432
+ const refreshControlActions = () => {
1433
+ if (control === void 0) {
1434
+ connectButton.disabled = true;
1435
+ disconnectButton.disabled = true;
1436
+ revokeButton.disabled = true;
1437
+ cancelManagedButton.hidden = true;
1438
+ return;
1439
+ }
1440
+ const state = control.connectionState;
1441
+ const operationPending = controlOperationBusy || state === "diagnosing" || state === "connecting" || state === "disconnecting";
1442
+ connectButton.disabled = operationPending || state === "ready" || state === "busy";
1443
+ disconnectButton.disabled = operationPending || state === "disconnected";
1444
+ revokeButton.disabled = operationPending || control.grantState !== "valid";
1445
+ const phase = control.task?.managedPhase;
1446
+ cancelManagedButton.hidden = phase === void 0 || phase === "completed" || phase === "review-required" || phase === "failed" || phase === "cancelled" || phase === "cleanup-warning";
1447
+ cancelManagedButton.disabled = operationPending;
1448
+ };
777
1449
  const settleDisclosure = (confirmed) => {
778
1450
  if (pendingDisclosure === void 0) return;
779
1451
  const resolve = pendingDisclosure;
@@ -817,10 +1489,28 @@ function createExternalHandoffPanel(document, framework, locale, sessionId, subs
817
1489
  disclosureInbox.textContent = messages.disclosureInbox;
818
1490
  disclosureProvider.textContent = messages.disclosureProvider;
819
1491
  disclosureNoGuarantee.textContent = messages.disclosureNoGuarantee;
1492
+ agentLabelText.textContent = messages.agentLabel;
1493
+ codexOption.textContent = messages.codexManaged;
1494
+ connectButton.textContent = messages.connectManaged;
1495
+ disconnectButton.textContent = messages.disconnectManaged;
1496
+ revokeButton.textContent = messages.revokeManaged;
1497
+ cancelManagedButton.textContent = messages.cancelManaged;
1498
+ managedResultTitle.textContent = messages.resultTitle;
1499
+ if (control !== void 0) {
1500
+ controlStatus.textContent = controlStatusText(control, messages);
1501
+ disclosureNoGuarantee.textContent = control.mode === "managed" ? messages.disclosureManagedGuarantee : messages.disclosureNoGuarantee;
1502
+ }
1503
+ if (managedResultValue !== void 0) {
1504
+ managedResultSummary.textContent = managedResultText(
1505
+ managedResultValue,
1506
+ messages
1507
+ );
1508
+ }
820
1509
  cancelButton.textContent = messages.cancel;
821
1510
  confirmButton.textContent = messages.confirm;
822
1511
  resolveButton.textContent = messages.resolveUnknown;
823
1512
  refreshActions();
1513
+ refreshControlActions();
824
1514
  };
825
1515
  cancelButton.addEventListener("click", () => {
826
1516
  settleDisclosure(false);
@@ -830,10 +1520,13 @@ function createExternalHandoffPanel(document, framework, locale, sessionId, subs
830
1520
  });
831
1521
  disclosure.addEventListener("keydown", handleDisclosureKeydown);
832
1522
  settings.addEventListener("toggle", options.onViewChange);
1523
+ managedResult.addEventListener("toggle", options.onViewChange);
833
1524
  const unsubscribeLocale = options.subscribeLocale(applyMessages);
834
1525
  applyMessages();
835
1526
  status.textContent = messages.ready;
1527
+ controlStatus.textContent = options.locale() === "zh-CN" ? "\u6B63\u5728\u8BFB\u53D6\u672C\u5730\u8FDE\u63A5\u72B6\u6001\u2026\u2026" : "Reading local connection status\u2026";
836
1528
  refreshActions();
1529
+ refreshControlActions();
837
1530
  const summaryMessage = (summary) => summary.state === "expired" ? messages.expired(summary.revision) : summary.state === "superseded" ? messages.superseded(summary.revision) : summary.pickupCount > 0 ? messages.pickedUp(
838
1531
  summary.revision,
839
1532
  summary.pickupCount,
@@ -867,6 +1560,10 @@ function createExternalHandoffPanel(document, framework, locale, sessionId, subs
867
1560
  sendButton,
868
1561
  refreshButton,
869
1562
  resolveButton,
1563
+ cancelManagedButton,
1564
+ connectButton,
1565
+ disconnectButton,
1566
+ revokeButton,
870
1567
  confirmDisclosure(annotation) {
871
1568
  if (hasConsent()) return Promise.resolve(true);
872
1569
  if (pendingDisclosure !== void 0) return Promise.resolve(false);
@@ -918,6 +1615,30 @@ function createExternalHandoffPanel(document, framework, locale, sessionId, subs
918
1615
  renderStatus(result) {
919
1616
  applyStatus(result);
920
1617
  },
1618
+ renderControlStatus(value) {
1619
+ control = value;
1620
+ controlStatus.textContent = controlStatusText(value, messages);
1621
+ controlStatus.dataset.state = value.connectionState;
1622
+ disclosureNoGuarantee.textContent = value.mode === "managed" ? messages.disclosureManagedGuarantee : messages.disclosureNoGuarantee;
1623
+ refreshControlActions();
1624
+ options.onViewChange();
1625
+ },
1626
+ renderControlUnavailable() {
1627
+ control = void 0;
1628
+ controlStatus.dataset.state = "unavailable";
1629
+ controlStatus.textContent = messages.controlUnavailable;
1630
+ disclosureNoGuarantee.textContent = messages.disclosureNoGuarantee;
1631
+ refreshControlActions();
1632
+ options.onViewChange();
1633
+ },
1634
+ renderManagedResult(result) {
1635
+ managedResultValue = result;
1636
+ managedResult.hidden = false;
1637
+ managedResult.open = true;
1638
+ managedResultSummary.textContent = managedResultText(result, messages);
1639
+ managedResultDiff.textContent = result.diff;
1640
+ options.onViewChange();
1641
+ },
921
1642
  renderError(code, canRetry = false) {
922
1643
  operationBusy = false;
923
1644
  retryable = canRetry;
@@ -931,6 +1652,10 @@ function createExternalHandoffPanel(document, framework, locale, sessionId, subs
931
1652
  operationBusy = nextBusy;
932
1653
  refreshActions();
933
1654
  },
1655
+ setControlBusy(nextBusy) {
1656
+ controlOperationBusy = nextBusy;
1657
+ refreshControlActions();
1658
+ },
934
1659
  setContextReady(ready) {
935
1660
  contextReady = ready;
936
1661
  refreshActions();
@@ -946,6 +1671,7 @@ function createExternalHandoffPanel(document, framework, locale, sessionId, subs
946
1671
  settleDisclosure(false);
947
1672
  disclosure.removeEventListener("keydown", handleDisclosureKeydown);
948
1673
  settings.removeEventListener("toggle", options.onViewChange);
1674
+ managedResult.removeEventListener("toggle", options.onViewChange);
949
1675
  unsubscribeLocale();
950
1676
  }
951
1677
  });