@inventec/dsh-copilot-auth 1.2.7 → 1.2.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/client.js CHANGED
@@ -34,68 +34,250 @@ var import_react = require("react");
34
34
 
35
35
  // src/shared.mjs
36
36
  var ROUTE_PREFIX = "/copilot-auth";
37
+ var PROTOCOL_VERSION = 2;
37
38
  var routes = () => ({
38
39
  start: `${ROUTE_PREFIX}/start`,
39
40
  state: `${ROUTE_PREFIX}/state`,
40
41
  status: `${ROUTE_PREFIX}/status`,
41
42
  logout: `${ROUTE_PREFIX}/logout`,
43
+ cancel: `${ROUTE_PREFIX}/cancel`,
42
44
  refreshPreview: `${ROUTE_PREFIX}/refresh/preview`,
43
- refreshApply: `${ROUTE_PREFIX}/refresh/apply`
45
+ refreshApply: `${ROUTE_PREFIX}/refresh/apply`,
46
+ refreshRetire: `${ROUTE_PREFIX}/refresh/retire`
44
47
  });
45
48
 
49
+ // src/client-http.mjs
50
+ var SAFE_ERROR_CODE = /^[A-Za-z][A-Za-z0-9 -]{0,79}$/;
51
+ var SAFE_CONTENT_TYPE = /^[\w./+-]{0,100}$/;
52
+ function sanitizeErrorCode(value) {
53
+ return typeof value === "string" && SAFE_ERROR_CODE.test(value) ? value : "http-error";
54
+ }
55
+ var SAFE_STRING = /^[A-Za-z0-9][A-Za-z0-9 -]{0,127}$/;
56
+ function deepSanitize(value) {
57
+ if (typeof value === "string") return SAFE_STRING.test(value) ? value : void 0;
58
+ if (value === null || typeof value === "number" || typeof value === "boolean") return value;
59
+ if (Array.isArray(value)) return value.map(deepSanitize);
60
+ if (value && typeof value === "object") {
61
+ const out = {};
62
+ for (const [k, v] of Object.entries(value)) {
63
+ const s = deepSanitize(v);
64
+ if (s !== void 0) out[k] = s;
65
+ }
66
+ return out;
67
+ }
68
+ return void 0;
69
+ }
70
+ function sanitizeContentType(value) {
71
+ if (typeof value !== "string" || !SAFE_CONTENT_TYPE.test(value)) return void 0;
72
+ return value;
73
+ }
74
+ async function requestJson(fetchImpl, url, init = {}) {
75
+ let res;
76
+ try {
77
+ res = await fetchImpl(url, init);
78
+ } catch {
79
+ return { ok: false, httpStatus: null, body: void 0, error: { messageKey: "network-error", details: {} } };
80
+ }
81
+ const httpStatus = res.status;
82
+ let text = "";
83
+ try {
84
+ text = await res.text();
85
+ } catch {
86
+ text = "";
87
+ }
88
+ if (typeof text !== "string" || text.trim() === "") {
89
+ return {
90
+ ok: false,
91
+ httpStatus,
92
+ body: void 0,
93
+ error: { messageKey: httpStatus >= 500 ? "bad-gateway" : "bad-response", details: { httpStatus } }
94
+ };
95
+ }
96
+ let body;
97
+ try {
98
+ body = JSON.parse(text);
99
+ } catch {
100
+ const contentType = sanitizeContentType(res.headers?.get?.("content-type"));
101
+ const looksHtml = contentType?.includes("html") || /^\s*<(?:!doctype|html)/i.test(text);
102
+ const messageKey = httpStatus >= 500 || looksHtml ? "bad-gateway" : "bad-response";
103
+ const details2 = { httpStatus };
104
+ if (contentType !== void 0) details2.contentType = contentType;
105
+ return { ok: false, httpStatus, body: void 0, error: { messageKey, details: details2 } };
106
+ }
107
+ const ok = res.ok && body?.ok !== false;
108
+ if (ok) return { ok: true, httpStatus, body, error: void 0 };
109
+ const details = {};
110
+ if (!res.ok) details.httpStatus = httpStatus;
111
+ const errorCode = sanitizeErrorCode(body?.error);
112
+ if (errorCode !== void 0) details.errorCode = errorCode;
113
+ return { ok: false, httpStatus, body: deepSanitize(body), error: { messageKey: "http-error", details } };
114
+ }
115
+
46
116
  // src/refresh-flow.mjs
47
- var initial = Object.freeze({ name: "idle", catalogWritable: null, catalogMode: null });
117
+ var initial = Object.freeze({
118
+ name: "idle",
119
+ flags: { catalogMode: null, blockedReason: null, scopeAvailable: true, legacyStateDetected: false },
120
+ lastResult: null
121
+ });
122
+ var fromRefresh = (state, refresh) => ({
123
+ ...state,
124
+ flags: {
125
+ catalogMode: refresh && "catalogMode" in refresh ? refresh.catalogMode : state.flags.catalogMode,
126
+ blockedReason: refresh && "blockedReason" in refresh ? refresh.blockedReason : state.flags.blockedReason,
127
+ scopeAvailable: refresh ? refresh.scopeAvailable !== false : state.flags.scopeAvailable,
128
+ legacyStateDetected: refresh?.legacyStateDetected === true
129
+ },
130
+ lastResult: refresh?.lastResult ?? null
131
+ });
132
+ var restartStates = /* @__PURE__ */ new Set(["pending-restart"]);
133
+ function refreshBlocked(flags) {
134
+ const mode = flags?.catalogMode;
135
+ return flags?.scopeAvailable === false || mode === "blocked" || mode === "unknown";
136
+ }
48
137
  function reduce(state, event) {
49
138
  switch (state.name) {
50
139
  case "idle":
51
140
  case "failed":
52
- case "restartNeeded": {
141
+ case "result":
142
+ case "pendingRestart":
143
+ case "busy":
144
+ case "resultUnknown": {
53
145
  if (event.type === "start") {
54
- return [{ name: "previewing", mode: event.mode, stale: false }, { type: "preview", mode: event.mode }];
146
+ return [{
147
+ name: "previewing",
148
+ flags: state.flags ?? initial.flags,
149
+ operation: event.operation === "rebuild" ? "rebuild" : "supplement",
150
+ catalogSource: event.catalogSource,
151
+ stale: false
152
+ }, {
153
+ type: "preview",
154
+ operation: event.operation === "rebuild" ? "rebuild" : "supplement",
155
+ catalogSource: event.catalogSource
156
+ }];
157
+ }
158
+ if (event.type === "init" && state.name === "idle") return [state, { type: "status" }];
159
+ if (event.type === "check" && state.name === "resultUnknown") {
160
+ return [{ ...state, name: "checking", operationId: state.operationId }, { type: "statusQuery", operationId: state.operationId }];
161
+ }
162
+ if (event.type === "retire-request" && state.name === "result") {
163
+ return [{ name: "retireConfirm", result: state.result }, null];
164
+ }
165
+ if (event.type === "dismiss" && state.name === "failed") {
166
+ return [{ ...initial, flags: state.flags }, null];
55
167
  }
56
- if (state.name === "failed" && event.type === "dismiss") return [initial, null];
57
- if (state.name === "idle" && event.type === "init") return [state, { type: "status" }];
58
168
  if (event.type === "hydrate") {
59
169
  const refresh = event.status?.refresh;
60
170
  if (!event.status) return [state, null];
61
- if (refresh?.lastError === "state-corrupt") return [{ name: "failed", error: "state-corrupt" }, null];
62
- if (refresh?.pendingRestart === true) {
63
- return [{ name: "restartNeeded", catalogWritable: refresh?.catalogWritable ?? null, catalogMode: refresh?.catalogMode ?? null }, null];
171
+ if (refresh?.lastError === "state-corrupt") return [{ ...state, name: "failed", error: "state-corrupt" }, null];
172
+ const base = fromRefresh({ ...initial, name: "idle" }, refresh);
173
+ if (restartStates.has(refresh?.lastResult?.status ?? "") || refresh?.pendingRestart === true) {
174
+ return [{ ...base, name: "pendingRestart", result: refresh.lastResult }, null];
175
+ }
176
+ const st = refresh?.lastResult?.status;
177
+ if (st && ["recovery-needed", "rollback-conflict", "conflict", "partial", "rolled-back", "intent-retired", "applied"].includes(st)) {
178
+ return [{ ...base, name: "result", result: refresh.lastResult }, null];
64
179
  }
65
- const writable = refresh && "catalogWritable" in refresh ? refresh.catalogWritable : state.catalogWritable ?? null;
66
- const mode = refresh && "catalogMode" in refresh ? refresh.catalogMode : state.catalogMode ?? null;
67
- return [{ ...state, catalogWritable: writable, catalogMode: mode }, null];
180
+ if (refresh?.activeOperation) return [{ ...base, name: "busy", active: refresh.activeOperation }, null];
181
+ return [base, null];
68
182
  }
69
183
  return [state, null];
70
184
  }
71
185
  case "previewing": {
72
186
  if (event.type === "preview-ok") {
73
- return [{ name: "confirming", preview: event.preview, mode: state.mode, staleNotice: state.stale === true }, null];
187
+ return [{ name: "confirming", flags: state.flags ?? initial.flags, preview: event.preview, staleNotice: state.stale === true }, null];
74
188
  }
75
- if (event.type === "preview-fail") return [{ name: "failed", error: event.error }, null];
189
+ if (event.type === "preview-fail") return [{ name: "failed", flags: state.flags ?? initial.flags, error: event.error }, null];
76
190
  return [state, null];
77
191
  }
78
192
  case "confirming": {
193
+ const p = state.preview;
194
+ if (event.type === "select") {
195
+ return [{
196
+ name: "previewing",
197
+ flags: state.flags ?? initial.flags,
198
+ operation: p.operation,
199
+ catalogSource: p.catalogSource,
200
+ stale: false
201
+ }, {
202
+ type: "preview",
203
+ operation: p.operation,
204
+ catalogSource: p.catalogSource,
205
+ basePreviewId: p.previewId,
206
+ selectedIds: event.selectedIds ?? [],
207
+ confirmEmpty: event.confirmEmpty === true
208
+ }];
209
+ }
79
210
  if (event.type === "start") {
80
- return [{ name: "previewing", mode: event.mode, stale: false }, { type: "preview", mode: event.mode }];
211
+ return [{
212
+ name: "previewing",
213
+ operation: event.operation === "rebuild" ? "rebuild" : p.operation,
214
+ catalogSource: event.catalogSource,
215
+ stale: false
216
+ }, {
217
+ type: "preview",
218
+ operation: event.operation === "rebuild" ? "rebuild" : p.operation,
219
+ catalogSource: event.catalogSource
220
+ }];
81
221
  }
82
222
  if (event.type === "confirm") {
83
- return [
84
- { name: "applying", mode: state.mode, digests: state.preview.digests },
85
- { type: "apply", mode: state.mode, digests: state.preview.digests }
86
- ];
223
+ const isRebuild = p.operation === "rebuild";
224
+ const emptyTarget = isRebuild && (p.diff?.targetView?.models ?? []).length === 0;
225
+ if (isRebuild && event.second !== true) return [state, null];
226
+ if (emptyTarget && event.empty !== true) return [state, null];
227
+ if (p.evidence?.stale === true) return [state, null];
228
+ return [{ name: "applying", flags: state.flags ?? initial.flags, preview: p }, { type: "apply", previewId: p.previewId, operationId: p.operationId }];
87
229
  }
88
- if (event.type === "cancel") return [initial, null];
230
+ if (event.type === "cancel") return [{ ...initial, flags: state.flags ?? initial.flags, lastResult: state.lastResult ?? null }, null];
89
231
  return [state, null];
90
232
  }
91
233
  case "applying": {
92
234
  if (event.type === "apply-ok") {
93
- return event.restartRequired === false ? [{ name: "applied", mode: event.mode ?? null, injected: event.injected ?? 0 }, null] : [{ name: "restartNeeded", catalogMode: state.catalogMode ?? null }, null];
235
+ return restartStates.has(event.result?.status ?? "") ? [{ ...state, name: "pendingRestart", result: event.result }, null] : [{ ...state, name: "result", result: event.result }, null];
94
236
  }
95
237
  if (event.type === "apply-stale") {
96
- return [{ name: "previewing", mode: state.mode, stale: true }, { type: "preview", mode: state.mode }];
238
+ return [{
239
+ name: "previewing",
240
+ flags: state.flags ?? initial.flags,
241
+ operation: state.preview.operation,
242
+ catalogSource: state.preview.catalogSource,
243
+ stale: true
244
+ }, { type: "preview", operation: state.preview.operation, catalogSource: state.preview.catalogSource }];
245
+ }
246
+ if (event.type === "apply-busy") {
247
+ return [{ ...state, name: "checking" }, { type: "status" }];
248
+ }
249
+ if (event.type === "apply-unknown") {
250
+ return [{ ...state, name: "checking", operationId: state.preview.operationId }, { type: "statusQuery", operationId: state.preview.operationId }];
97
251
  }
98
- if (event.type === "apply-fail") return [{ name: "failed", error: event.error }, null];
252
+ if (event.type === "apply-fail") return [{ ...state, name: "failed", error: event.error }, null];
253
+ return [state, null];
254
+ }
255
+ case "checking": {
256
+ if (event.type === "query-result") {
257
+ if (event.query === "active") return [{ ...state, name: "busy", active: event.active }, null];
258
+ if (event.query === "last") {
259
+ return restartStates.has(event.lastResult?.status ?? "") ? [{ ...state, name: "pendingRestart", result: event.lastResult }, null] : [{ ...state, name: "result", result: event.lastResult }, null];
260
+ }
261
+ return [{ ...state, name: "resultUnknown", operationId: state.operationId }, null];
262
+ }
263
+ if (event.type === "hydrate") {
264
+ const refresh = event.status?.refresh;
265
+ if (refresh?.activeOperation) return [{ ...state, name: "busy", active: refresh.activeOperation }, null];
266
+ if (refresh?.lastResult) return [{ ...state, name: "result", result: refresh.lastResult }, null];
267
+ return [{ ...state, name: "resultUnknown", operationId: state.operationId }, null];
268
+ }
269
+ return [state, null];
270
+ }
271
+ case "retireConfirm": {
272
+ if (event.type === "retire-confirm") {
273
+ return [{ name: "retiring", operationId: state.result?.operationId, result: state.result }, { type: "retire", operationId: state.result?.operationId }];
274
+ }
275
+ if (event.type === "cancel") return [{ name: "result", result: state.result }, null];
276
+ return [state, null];
277
+ }
278
+ case "retiring": {
279
+ if (event.type === "retire-ok") return [{ ...state, name: "checking" }, { type: "status" }];
280
+ if (event.type === "retire-fail") return [{ name: "result", result: state.result, retireError: event.error }, null];
99
281
  return [state, null];
100
282
  }
101
283
  default:
@@ -104,52 +286,58 @@ function reduce(state, event) {
104
286
  }
105
287
  async function runEffect(effect, fetchImpl) {
106
288
  const r = routes();
107
- const post = (url, body) => fetchImpl(url, {
289
+ const post = (url, body) => requestJson(fetchImpl, url, {
108
290
  method: "POST",
109
291
  headers: { "content-type": "application/json" },
110
- body: JSON.stringify(body)
292
+ body: JSON.stringify({ protocolVersion: PROTOCOL_VERSION, ...body })
111
293
  });
112
294
  if (effect.type === "preview") {
113
- try {
114
- const res = await post(r.refreshPreview, effect.mode ? { mode: effect.mode } : {});
115
- const body = await res.json();
116
- return res.status === 200 && body?.ok ? { type: "preview-ok", preview: body } : { type: "preview-fail", error: body?.error ?? `HTTP ${res.status}` };
117
- } catch (err) {
118
- return { type: "preview-fail", error: String(err?.message ?? err) };
119
- }
295
+ const res = await post(r.refreshPreview, {
296
+ operation: effect.operation,
297
+ ...effect.catalogSource ? { catalogSource: effect.catalogSource } : {},
298
+ ...effect.basePreviewId ? { basePreviewId: effect.basePreviewId, selectedIds: effect.selectedIds ?? [] } : {},
299
+ ...effect.confirmEmpty ? { confirmEmpty: true } : {}
300
+ });
301
+ if (res.ok) return { type: "preview-ok", preview: res.body };
302
+ return { type: "preview-fail", error: res.error?.details?.errorCode ?? res.error?.messageKey };
120
303
  }
121
304
  if (effect.type === "apply") {
122
- try {
123
- const res = await post(r.refreshApply, {
124
- digests: effect.digests,
125
- ...effect.mode ? { mode: effect.mode } : {}
126
- });
127
- const body = await res.json().catch(() => null);
128
- if (res.status === 409) return { type: "apply-stale" };
129
- return res.status === 200 && body?.ok ? {
130
- type: "apply-ok",
131
- restartRequired: body.restartRequired !== false,
132
- mode: body.mode ?? null,
133
- injected: Array.isArray(body.injected) ? body.injected.length : 0
134
- } : { type: "apply-fail", error: body?.error ?? `HTTP ${res.status}` };
135
- } catch (err) {
136
- return { type: "apply-fail", error: String(err?.message ?? err) };
305
+ const res = await post(r.refreshApply, { previewId: effect.previewId, operationId: effect.operationId });
306
+ if (res.ok) return { type: "apply-ok", result: res.body?.result ?? { status: "unknown" } };
307
+ const code = res.error?.details?.errorCode ?? "";
308
+ const status = res.httpStatus;
309
+ if (status === null) return { type: "apply-unknown" };
310
+ if (status === 423 || code === "resource-busy") return { type: "apply-busy" };
311
+ if (status === 409 && ["preview-stale", "auth-changed", "preview-invalid"].includes(code)) {
312
+ return { type: "apply-stale", error: code };
137
313
  }
314
+ return { type: "apply-fail", error: code || res.error?.messageKey };
315
+ }
316
+ if (effect.type === "retire") {
317
+ const res = await post(r.refreshRetire, { operationId: effect.operationId });
318
+ if (res.ok) return { type: "retire-ok", result: res.body?.result ?? null };
319
+ return { type: "retire-fail", error: res.error?.details?.errorCode ?? res.error?.messageKey };
138
320
  }
139
321
  if (effect.type === "status") {
140
- try {
141
- const res = await fetchImpl(r.status);
142
- return { type: "hydrate", status: await res.json() };
143
- } catch {
144
- return { type: "hydrate", status: null };
322
+ const res = await requestJson(fetchImpl, r.status, { method: "GET" });
323
+ if (res.ok) return { type: "hydrate", status: res.body };
324
+ return { type: "hydrate", status: null };
325
+ }
326
+ if (effect.type === "statusQuery") {
327
+ const res = await requestJson(fetchImpl, `${r.status}?operationId=${encodeURIComponent(effect.operationId ?? "")}`, { method: "GET" });
328
+ if (res.ok && res.body?.operation) {
329
+ return { type: "query-result", query: res.body.operation.query, active: res.body.operation.active, lastResult: res.body.operation.lastResult };
145
330
  }
331
+ return { type: "query-result", query: "unknown" };
146
332
  }
147
333
  throw new Error(`unknown effect: ${effect.type}`);
148
334
  }
149
335
  async function advance(state, event, fetchImpl, onState) {
150
336
  let [s, fx] = reduce(state, event);
151
337
  onState?.(s);
338
+ let guard = 0;
152
339
  while (fx) {
340
+ if (guard++ > 8) throw new Error("effect loop guard");
153
341
  const resultEvent = await runEffect(fx, fetchImpl);
154
342
  [s, fx] = reduce(s, resultEvent);
155
343
  onState?.(s);
@@ -157,6 +345,174 @@ async function advance(state, event, fetchImpl, onState) {
157
345
  return s;
158
346
  }
159
347
 
348
+ // src/auth-flow.mjs
349
+ var POLL_INTERVAL_MS = 1e3;
350
+ var BACKOFF_STEPS_S = [1, 2, 4, 8, 15];
351
+ var AUTO_RETRY_LIMIT_MS = 6e4;
352
+ var LIVE_STATUSES = ["starting", "waiting", "finishing"];
353
+ var defaultClock = () => ({
354
+ now: () => Date.now(),
355
+ setTimeout: (fn, ms) => setTimeout(fn, ms),
356
+ clearTimeout: (id) => clearTimeout(id)
357
+ });
358
+ function createAuthFlow({ fetchImpl, clock = defaultClock(), onState = () => {
359
+ } } = {}) {
360
+ let generation = 0;
361
+ let timer = null;
362
+ let failures = 0;
363
+ let backoffIdx = 0;
364
+ let failureWindowStart = null;
365
+ let last = { phase: "loading", connectivity: "online", shared: false };
366
+ const emit = (patch) => {
367
+ last = { ...last, ...patch };
368
+ onState({ ...last });
369
+ };
370
+ const stale = (gen) => gen !== generation;
371
+ const clearTimer = () => {
372
+ if (timer !== null) {
373
+ clock.clearTimeout(timer);
374
+ timer = null;
375
+ }
376
+ };
377
+ const schedule = (gen, delayMs) => {
378
+ if (stale(gen)) return;
379
+ clearTimer();
380
+ timer = clock.setTimeout(() => {
381
+ timer = null;
382
+ void pollTick(gen);
383
+ }, delayMs);
384
+ };
385
+ function applySnapshot(gen, s) {
386
+ if (stale(gen)) return;
387
+ emit({ attempt: s, riskLatch: s?.riskLatch ?? null });
388
+ if (LIVE_STATUSES.includes(s?.status)) {
389
+ emit({ phase: "waiting" });
390
+ schedule(gen, POLL_INTERVAL_MS);
391
+ return;
392
+ }
393
+ if (s?.status === "authorized") {
394
+ emit({ phase: "authorized", error: void 0 });
395
+ return;
396
+ }
397
+ if (s?.status === "failed") {
398
+ emit({ phase: "failed", error: s?.error ?? "authorization-failed" });
399
+ return;
400
+ }
401
+ if (s?.status === "withdrawal-pending-unverified" || s?.status === "timed-out-unverified") {
402
+ emit({ phase: "risk", riskKind: s.status });
403
+ return;
404
+ }
405
+ emit({ phase: "idle" });
406
+ }
407
+ async function pollTick(gen) {
408
+ const r = await requestJson(fetchImpl, "/copilot-auth/state", { method: "GET" });
409
+ if (stale(gen)) return;
410
+ if (r.ok) {
411
+ failures = 0;
412
+ backoffIdx = 0;
413
+ failureWindowStart = null;
414
+ emit({ connectivity: "online" });
415
+ applySnapshot(gen, r.body);
416
+ return;
417
+ }
418
+ failures += 1;
419
+ if (failureWindowStart === null) failureWindowStart = clock.now();
420
+ const cumulative = clock.now() - failureWindowStart;
421
+ if (cumulative >= AUTO_RETRY_LIMIT_MS) {
422
+ emit({ connectivity: "manual" });
423
+ return;
424
+ }
425
+ emit({ connectivity: "retrying" });
426
+ const delayS = BACKOFF_STEPS_S[Math.min(backoffIdx, BACKOFF_STEPS_S.length - 1)];
427
+ backoffIdx += 1;
428
+ schedule(gen, delayS * 1e3);
429
+ }
430
+ function beginPolling(gen) {
431
+ void pollTick(gen);
432
+ }
433
+ async function init() {
434
+ const gen = ++generation;
435
+ clearTimer();
436
+ const status = await requestJson(fetchImpl, "/copilot-auth/status", { method: "GET" });
437
+ if (stale(gen)) return;
438
+ if (!status.ok) {
439
+ emit({ phase: "idle", error: status.error?.messageKey });
440
+ return;
441
+ }
442
+ const body = status.body ?? {};
443
+ if (body.refresh?.scopeAvailable === false) {
444
+ emit({ phase: "unavailable" });
445
+ return;
446
+ }
447
+ emit({ status: body });
448
+ if (body.authorization?.riskLatch) {
449
+ emit({ riskLatch: body.authorization.riskLatch });
450
+ }
451
+ if (body.configured === true) {
452
+ emit({ phase: "authorized" });
453
+ return;
454
+ }
455
+ const r = await requestJson(fetchImpl, "/copilot-auth/state", { method: "GET" });
456
+ if (stale(gen)) return;
457
+ if (!r.ok) {
458
+ emit({ phase: "idle" });
459
+ return;
460
+ }
461
+ applySnapshot(gen, r.body);
462
+ }
463
+ async function start() {
464
+ const gen = ++generation;
465
+ clearTimer();
466
+ failures = 0;
467
+ backoffIdx = 0;
468
+ failureWindowStart = null;
469
+ emit({ phase: "waiting", shared: false, error: void 0, connectivity: "online" });
470
+ const r = await requestJson(fetchImpl, "/copilot-auth/start", { method: "POST" });
471
+ if (stale(gen)) return;
472
+ if (r.ok) {
473
+ beginPolling(gen);
474
+ return;
475
+ }
476
+ const errorCode = r.error?.details?.errorCode;
477
+ if (r.httpStatus === 409) {
478
+ emit({ shared: true });
479
+ beginPolling(gen);
480
+ return;
481
+ }
482
+ if (errorCode === "auth-unsafe") {
483
+ emit({ phase: "risk", riskKind: "auth-unsafe" });
484
+ return;
485
+ }
486
+ if (errorCode === "scope-unavailable" || r.httpStatus === 503) {
487
+ emit({ phase: "unavailable" });
488
+ return;
489
+ }
490
+ emit({ phase: "failed", error: errorCode ?? r.error?.messageKey });
491
+ }
492
+ async function cancel() {
493
+ const r = await requestJson(fetchImpl, "/copilot-auth/cancel", { method: "POST" });
494
+ if (r.ok) {
495
+ emit({ withdrawalDelivery: r.body?.withdrawalDelivery ?? "unavailable" });
496
+ return;
497
+ }
498
+ emit({ withdrawalDelivery: "failed", error: r.error?.details?.errorCode ?? r.error?.messageKey });
499
+ }
500
+ function refresh() {
501
+ const gen = ++generation;
502
+ clearTimer();
503
+ failures = 0;
504
+ backoffIdx = 0;
505
+ failureWindowStart = null;
506
+ emit({ connectivity: "online" });
507
+ void pollTick(gen);
508
+ }
509
+ function dispose() {
510
+ generation += 1;
511
+ clearTimer();
512
+ }
513
+ return { init, start, cancel, refresh, dispose };
514
+ }
515
+
160
516
  // src/client.jsx
161
517
  var import_jsx_runtime = require("react/jsx-runtime");
162
518
  var name = "copilot-auth-ui";
@@ -164,107 +520,166 @@ var inject = ["slots", "locale"];
164
520
  var DICTS = {
165
521
  en: {
166
522
  nav: "GHC Settings",
167
- title: "GitHub Copilot Sign-in",
168
- intro: "Sign in to Copilot with your company GitHub account using the device code \u2014 no API token required.",
523
+ title: "GitHub Copilot",
524
+ intro: "Authorize with a personal or organization account on github.com that has Copilot access. No API token is required.",
169
525
  idle: "Not signed in",
170
- running: "Signing in\u2026",
526
+ waiting: "Waiting for you to authorize on GitHub",
171
527
  authorized: "Signed in",
172
528
  failed: "Failed",
173
529
  loading: "\u2026",
174
530
  login: "Sign in",
175
531
  logout: "Sign out",
532
+ reauthorize: "Reauthorize",
533
+ withdrawAuth: "Request withdrawal",
534
+ withdrawal: "Withdrawal requested; the outcome remains unverified.",
535
+ withdrawalUnavailable: "The withdrawal request could not be sent to the host; the outcome remains unverified.",
536
+ authUnsafe: "Safe completion of the authorization flow is unconfirmed. Sign-out and a new authorization attempt are unavailable.",
537
+ attemptTimeout: "Authorization wait timed out; the outcome remains unverified. A new authorization attempt is unavailable. Check the status or verify it manually.",
538
+ attemptShared: "An authorization attempt is already running in this instance.",
539
+ unavailable: "This runtime lacks the capabilities needed to apply changes safely.",
540
+ connection: "Connection lost. The last confirmed status is shown.",
541
+ retryNow: "Check status",
542
+ copyFailed: "Copy failed. Select and copy the device code manually.",
543
+ riskBadge: "Outcome unverified",
176
544
  codeHint: "Open the link below and enter this code to finish signing in:",
177
545
  copy: "Copy",
178
546
  copied: "Copied \u2713",
179
547
  unknown: "Unknown error",
180
- refreshNow: "Refresh model catalog",
181
- refreshTitle: "Refresh available model catalog",
182
- refreshDesc: "This pulls your account's available models and the latest pi-ai catalog data, then applies a data-level, add-only patch to the local catalog (pi-ai version stays unchanged). Review the diff before confirming.",
183
- riskRemoved: "Models no longer available to your account will DISAPPEAR from the model list after restart.",
184
- riskReset: "Your customizations (trimmed model list, per-model field tweaks, modelOverrides) will be RESET to the mirrored set.",
185
- riskRestart: "Takes effect only after a dsh web restart (two-phase: catalog patch now, settings sync on next boot).",
186
- addedModels: "New models",
187
- removedModels: "Removed (no longer available)",
548
+ // ---- Model management (protocol v2, design §5) ----
549
+ supplement: "Add models",
550
+ rebuild: "Rebuild model list",
551
+ supplementTitle: "Add models",
552
+ rebuildTitle: "Rebuild model list",
553
+ candidates: "Models available to add",
554
+ selectAll: "Select all",
555
+ supplementRisk: "Existing models and customizations will be kept. Only selected models will be added.",
556
+ rebuildRisk: "This rebuilds Copilot model configuration from the verified account list and clears the model customizations below. Other provider settings remain unchanged.",
557
+ clearRisk: "This clears the Copilot model list and its model customizations. Confirm clearing separately.",
558
+ secondConfirm: "This is destructive. Click again to confirm.",
559
+ applyChanges: "Apply changes",
560
+ addedModels: "Models to add",
561
+ removedModels: "Models to be removed",
188
562
  keptModels: "Kept",
563
+ removedAccount: "Not included in this account model list",
564
+ removedUnresolvable: "Not resolvable by the current catalog",
565
+ warnings: "Warnings",
189
566
  skippedModels: "Upstream entries skipped by validation",
190
- customReset: "These customizations will be reset:",
191
- customEntries: "field tweaks",
192
- customOverrides: "modelOverrides",
193
- confirmRefresh: "Confirm refresh",
194
- cancel: "Cancel",
195
- refreshing: "Fetching preview\u2026",
196
- applying: "Applying\u2026",
197
- restartNeeded: "Catalog patched. Restart dsh web to finish syncing the model list.",
198
- stalePreview: "Inputs changed \u2014 review the new diff",
199
- overlayBtn: "Preview with bundled overlay",
200
- stateCorrupt: "State file was corrupted and quarantined; auto self-heal is disabled \u2014 run a refresh to re-activate.",
201
- srcLive: "Account models: live fetch",
202
- srcCache: "Account models: credential cache (live fetch failed)",
567
+ srcLive: "Account models: live",
568
+ srcCache: "Account models: cached at {time}; add-only use",
569
+ srcStale: "The cache has no trusted timestamp, is expired, or belongs to changed authorization; reference only.",
203
570
  srcLatest: "Catalog source: latest pi-ai from npm",
204
- srcLocal: "Catalog source: local only (npm fetch failed)",
571
+ srcLocal: "Catalog source: local (npm fetch failed)",
205
572
  srcOverlay: "Catalog source: bundled overlay (offline bootstrap)",
573
+ overlayBtn: "Preview with bundled overlay",
574
+ previewStale: "Configuration or data changed. Preview and confirm again.",
206
575
  none: "(none)",
207
- notWritable: "This DSH installation packages the model catalog read-only (inside app.asar) \u2014 the refresh cannot be written. Use a web/service deployment for catalog refresh, or wait for a desktop build that unpacks pi-ai.",
208
- registryMode: "This installation's app.asar is read-only, so the refresh does not write the catalog file: new entries are injected into the running catalog registry instead. No restart needed.",
209
- registryApplied: "Injected %s new model(s) into the running catalog \u2014 live now. The boot sequence replays them after a restart.",
210
- registryFailed: "Catalog registry injection failed \u2014 nothing was written and your model list was left unchanged.",
211
- riskImmediate: "Takes effect immediately in this process (registry injection); the boot sequence replays it after a restart."
576
+ applying: "Applying\u2026",
577
+ refreshing: "Fetching preview\u2026",
578
+ cancel: "Cancel",
579
+ applied: "Changes are active. No restart is needed.",
580
+ pendingRestart: "Changes are saved and require restarting this instance's service or application. Refreshing the page alone is not enough.",
581
+ conflict: "Configuration changed. To protect your edits, preview and confirm again.",
582
+ partial: "Changes were only partially applied. Review the current status and recovery guidance.",
583
+ rolledBack: "This operation's model configuration changes were rolled back. Catalog data may still have been added.",
584
+ rollbackConflict: "Newer configuration was detected and preserved. Automatic rollback did not complete.",
585
+ busy: "Another operation is using this resource. Please retry shortly.",
586
+ unknownResult: "Completion has not been confirmed. Check the current status before submitting again.",
587
+ recoveryNeeded: "A previous configuration intent is stale or conflicted. End it before applying new changes.",
588
+ retireBtn: "End previous configuration intent",
589
+ retireTitle: "End previous configuration intent",
590
+ retireConfirmText: "End the intent only; existing changes are not rolled back or undone.",
591
+ legacy: "Legacy shared recovery state was found. It will not be applied automatically; preview and confirm in this profile.",
592
+ techDetails: "Technical details",
593
+ stateCorrupt: "State file was corrupted and quarantined. Preview and confirm again.",
594
+ blockedReasons: {
595
+ "inject-unavailable": "The in-process catalog registry is unavailable in this installation.",
596
+ "install-unresolved": "The pi-ai installation could not be located."
597
+ }
212
598
  },
213
599
  zh: {
214
600
  nav: "GHC\u8BBE\u7F6E",
215
- title: "GitHub Copilot \u767B\u5F55",
216
- intro: "\u4F7F\u7528\u516C\u53F8 GitHub \u8D26\u53F7\u901A\u8FC7\u8BBE\u5907\u7801\u6388\u6743\u767B\u5F55 Copilot\uFF0C\u65E0\u9700\u586B\u5199 API Token\u3002",
601
+ title: "GitHub Copilot",
602
+ intro: "\u4F7F\u7528\u5177\u6709 Copilot \u6743\u9650\u7684 github.com \u4E2A\u4EBA\u6216\u7EC4\u7EC7\u8D26\u53F7\u6388\u6743\uFF0C\u65E0\u9700\u586B\u5199 API Token\u3002",
217
603
  idle: "\u672A\u767B\u5F55",
218
- running: "\u8FDB\u884C\u4E2D\u2026",
604
+ waiting: "\u7B49\u5F85\u4F60\u5728 GitHub \u5B8C\u6210\u6388\u6743",
219
605
  authorized: "\u5DF2\u767B\u5F55",
220
606
  failed: "\u5931\u8D25",
221
607
  loading: "\u2026",
222
- login: "\u767B\u5F55",
223
- logout: "\u6CE8\u9500",
608
+ login: "\u6388\u6743\u767B\u5F55",
609
+ logout: "\u9000\u51FA\u767B\u5F55",
610
+ reauthorize: "\u91CD\u65B0\u6388\u6743",
611
+ withdrawAuth: "\u8BF7\u6C42\u64A4\u56DE",
612
+ withdrawal: "\u64A4\u56DE\u8BF7\u6C42\u5DF2\u53D1\u9001\uFF0C\u7ED3\u679C\u4ECD\u5F85\u6838\u5B9E\u3002",
613
+ withdrawalUnavailable: "\u672A\u80FD\u5411\u5BBF\u4E3B\u53D1\u9001\u64A4\u56DE\u8BF7\u6C42\uFF0C\u7ED3\u679C\u4ECD\u5F85\u6838\u5B9E\u3002",
614
+ authUnsafe: "\u5C1A\u4E0D\u80FD\u786E\u8BA4\u6388\u6743\u6D41\u7A0B\u5DF2\u5B89\u5168\u7ED3\u675F\uFF0C\u6682\u4E0D\u80FD\u9000\u51FA\u6216\u53D1\u8D77\u65B0\u7684\u6388\u6743\u5C1D\u8BD5\u3002",
615
+ attemptTimeout: "\u7B49\u5F85\u6388\u6743\u8D85\u65F6\uFF0C\u7ED3\u679C\u4ECD\u5F85\u6838\u5B9E\u3002\u5F53\u524D\u4E0D\u80FD\u53D1\u8D77\u65B0\u7684\u6388\u6743\u5C1D\u8BD5\uFF0C\u8BF7\u67E5\u8BE2\u72B6\u6001\u6216\u4EBA\u5DE5\u6838\u5B9E\u3002",
616
+ attemptShared: "\u6B64\u5B9E\u4F8B\u5DF2\u6709\u6388\u6743\u6B63\u5728\u8FDB\u884C\u3002",
617
+ connection: "\u8FDE\u63A5\u5F02\u5E38\uFF0C\u663E\u793A\u7684\u662F\u6700\u540E\u786E\u8BA4\u7684\u72B6\u6001\u3002",
618
+ retryNow: "\u67E5\u8BE2\u72B6\u6001",
619
+ copyFailed: "\u590D\u5236\u5931\u8D25\uFF0C\u8BF7\u624B\u52A8\u9009\u4E2D\u8BBE\u5907\u7801\u590D\u5236\u3002",
620
+ riskBadge: "\u7ED3\u679C\u5F85\u6838\u5B9E",
224
621
  codeHint: "\u5728\u6D4F\u89C8\u5668\u6253\u5F00\u4E0B\u9762\u7684\u94FE\u63A5\uFF0C\u8F93\u5165\u8FD9\u4E32\u4EE3\u7801\u5B8C\u6210\u6388\u6743\uFF1A",
225
622
  copy: "\u590D\u5236",
226
623
  copied: "\u5DF2\u590D\u5236 \u2713",
227
624
  unknown: "\u672A\u77E5\u9519\u8BEF",
228
- refreshNow: "\u5237\u65B0\u53EF\u7528\u6A21\u578B\u76EE\u5F55",
229
- refreshTitle: "\u5237\u65B0\u53EF\u7528\u6A21\u578B\u76EE\u5F55",
230
- refreshDesc: "\u5C06\u73B0\u573A\u62C9\u53D6\u8D26\u53F7\u53EF\u7528\u6A21\u578B\u4E0E\u6700\u65B0 pi-ai \u76EE\u5F55\u6570\u636E\uFF0C\u5BF9\u672C\u673A\u76EE\u5F55\u505A\u6570\u636E\u7EA7\u53EA\u589E\u8865\u4E01\uFF08pi-ai \u7248\u672C\u4E0D\u53D8\uFF09\u3002\u786E\u8BA4\u524D\u8BF7\u6838\u5BF9\u4E0B\u65B9\u5DEE\u5F02\u3002",
231
- riskRemoved: "\u5931\u6548\u6A21\u578B\uFF08\u8D26\u53F7\u5DF2\u4E0D\u518D\u53EF\u7528\u7684\u6A21\u578B\uFF09\u91CD\u542F\u540E\u5C06\u4ECE\u6A21\u578B\u5217\u8868\u6D88\u5931\u3002",
232
- riskReset: "\u4F60\u7684\u5B9A\u5236\uFF08\u7CBE\u7B80\u88C1\u526A\u3001\u6761\u76EE\u5B57\u6BB5\u5FAE\u8C03\u3001modelOverrides\uFF09\u5C06\u88AB\u91CD\u7F6E\u4E3A\u955C\u50CF\u96C6\u5408\u3002",
233
- riskRestart: "\u9700\u91CD\u542F dsh web \u540E\u751F\u6548\uFF08\u4E24\u9636\u6BB5\uFF1A\u73B0\u5728\u5199\u76EE\u5F55\u8865\u4E01\uFF0C\u4E0B\u4E2A boot \u540C\u6B65 settings\uFF09\u3002",
234
- addedModels: "\u65B0\u589E\u6A21\u578B",
235
- removedModels: "\u79FB\u9664\uFF08\u5DF2\u5931\u6548\uFF09",
625
+ // ---- 模型管理(协议 v2,设计§5) ----
626
+ supplement: "\u8865\u5145\u6A21\u578B",
627
+ rebuild: "\u91CD\u5EFA\u6A21\u578B\u5217\u8868",
628
+ supplementTitle: "\u8865\u5145\u6A21\u578B",
629
+ rebuildTitle: "\u91CD\u5EFA\u6A21\u578B\u5217\u8868",
630
+ candidates: "\u53EF\u65B0\u589E\u7684\u6A21\u578B",
631
+ selectAll: "\u5168\u9009",
632
+ supplementRisk: "\u5C06\u4FDD\u7559\u73B0\u6709\u6A21\u578B\u53CA\u5B9A\u5236\uFF0C\u53EA\u6DFB\u52A0\u4F60\u52FE\u9009\u7684\u6A21\u578B\u3002",
633
+ rebuildRisk: "\u5C06\u6309\u672C\u6B21\u53EF\u4FE1\u8D26\u53F7\u5217\u8868\u91CD\u5EFA Copilot \u6A21\u578B\u914D\u7F6E\uFF0C\u5E76\u6E05\u9664\u4E0B\u5217\u6A21\u578B\u53C2\u6570\u4E0E\u8986\u76D6\u914D\u7F6E\u3002\u5176\u4ED6\u63D0\u4F9B\u65B9\u8BBE\u7F6E\u4E0D\u53D8\u3002",
634
+ clearRisk: "\u672C\u6B21\u5C06\u6E05\u7A7A Copilot \u6A21\u578B\u5217\u8868\u5E76\u6E05\u9664\u5176\u6A21\u578B\u5B9A\u5236\u3002\u8BF7\u5355\u72EC\u786E\u8BA4\u6E05\u7A7A\u3002",
635
+ secondConfirm: "\u6B64\u64CD\u4F5C\u5177\u6709\u7834\u574F\u6027\uFF0C\u8BF7\u518D\u6B21\u70B9\u51FB\u786E\u8BA4\u3002",
636
+ applyChanges: "\u5E94\u7528\u66F4\u6539",
637
+ addedModels: "\u5C06\u6DFB\u52A0\u7684\u6A21\u578B",
638
+ removedModels: "\u5C06\u79FB\u9664\u7684\u6A21\u578B",
236
639
  keptModels: "\u4FDD\u7559",
640
+ removedAccount: "\u672A\u5305\u542B\u5728\u672C\u6B21\u8D26\u53F7\u6A21\u578B\u5217\u8868",
641
+ removedUnresolvable: "\u5F53\u524D\u76EE\u5F55\u65E0\u6CD5\u89E3\u6790",
642
+ warnings: "\u8B66\u544A",
237
643
  skippedModels: "\u88AB\u6821\u9A8C\u8DF3\u8FC7\u7684\u4E0A\u6E38\u6761\u76EE",
238
- customReset: "\u4EE5\u4E0B\u5B9A\u5236\u5C06\u88AB\u91CD\u7F6E\uFF1A",
239
- customEntries: "\u5B57\u6BB5\u5FAE\u8C03",
240
- customOverrides: "modelOverrides",
241
- confirmRefresh: "\u786E\u8BA4\u5237\u65B0",
242
- cancel: "\u53D6\u6D88",
243
- refreshing: "\u62C9\u53D6\u9884\u89C8\u4E2D\u2026",
244
- applying: "\u5E94\u7528\u4E2D\u2026",
245
- restartNeeded: "\u76EE\u5F55\u8865\u4E01\u5DF2\u5199\u5165\u3002\u91CD\u542F dsh web \u540E\u5B8C\u6210\u6A21\u578B\u5217\u8868\u540C\u6B65\u3002",
246
- stalePreview: "\u6570\u636E\u5DF2\u53D8\u5316\uFF0C\u8BF7\u91CD\u65B0\u786E\u8BA4\u5DEE\u5F02",
247
- overlayBtn: "\u6539\u7528\u5185\u7F6E\u8986\u76D6\u5C42\u9884\u89C8",
248
- stateCorrupt: "\u72B6\u6001\u6587\u4EF6\u5DF2\u635F\u574F\u5E76\u88AB\u9694\u79BB\uFF1B\u81EA\u52A8\u81EA\u6108\u5DF2\u505C\u7528\uFF0C\u8BF7\u91CD\u65B0\u6267\u884C\u4E00\u6B21\u5237\u65B0\u4EE5\u6FC0\u6D3B\u3002",
249
- srcLive: "\u8D26\u53F7\u6A21\u578B\uFF1A\u73B0\u573A\u62C9\u53D6",
250
- srcCache: "\u8D26\u53F7\u6A21\u578B\uFF1A\u51ED\u8BC1\u7F13\u5B58\uFF08\u73B0\u573A\u62C9\u53D6\u5931\u8D25\uFF09",
644
+ srcLive: "\u8D26\u53F7\u6A21\u578B\uFF1A\u5B9E\u65F6\u83B7\u53D6",
645
+ srcCache: "\u8D26\u53F7\u6A21\u578B\uFF1A\u7F13\u5B58\uFF0C\u83B7\u53D6\u4E8E {time}\uFF0C\u4EC5\u53EF\u7528\u4E8E\u8865\u5145",
646
+ srcStale: "\u7F13\u5B58\u7F3A\u5C11\u53EF\u4FE1\u65F6\u95F4\u3001\u5DF2\u8FC7\u671F\u6216\u6388\u6743\u5DF2\u53D8\u5316\uFF0C\u4EC5\u4F9B\u53C2\u8003\u3002",
251
647
  srcLatest: "\u76EE\u5F55\u6765\u6E90\uFF1Anpm \u6700\u65B0 pi-ai",
252
- srcLocal: "\u76EE\u5F55\u6765\u6E90\uFF1A\u4EC5\u672C\u5730\u76EE\u5F55\uFF08npm \u62C9\u53D6\u5931\u8D25\uFF09",
648
+ srcLocal: "\u76EE\u5F55\u6765\u6E90\uFF1A\u672C\u5730\u76EE\u5F55\uFF08npm \u62C9\u53D6\u5931\u8D25\uFF09",
253
649
  srcOverlay: "\u76EE\u5F55\u6765\u6E90\uFF1A\u5185\u7F6E\u8986\u76D6\u5C42\uFF08\u79BB\u7EBF bootstrap\uFF09",
650
+ overlayBtn: "\u6539\u7528\u5185\u7F6E\u76EE\u5F55\u6570\u636E\u9884\u89C8",
651
+ previewStale: "\u914D\u7F6E\u6216\u6570\u636E\u5DF2\u53D8\u5316\uFF0C\u8BF7\u91CD\u65B0\u9884\u89C8\u5E76\u786E\u8BA4\u3002",
254
652
  none: "\uFF08\u65E0\uFF09",
255
- notWritable: "\u5F53\u524D DSH \u5B89\u88C5\u628A\u6A21\u578B\u76EE\u5F55\u6253\u5305\u5728\u53EA\u8BFB\u7684 app.asar \u5185\uFF0C\u5237\u65B0\u65E0\u6CD5\u5199\u5165\u3002\u8BF7\u5728 Web/\u670D\u52A1\u90E8\u7F72\u5F62\u6001\u4E0B\u5237\u65B0\u76EE\u5F55\uFF0C\u6216\u7B49\u5F85\u684C\u9762\u7248\u63D0\u4F9B\u53EF\u5199\u5E03\u5C40\u540E\u91CD\u8BD5\u3002",
256
- registryMode: "\u5F53\u524D\u5B89\u88C5\u7684 app.asar \u53EA\u8BFB\uFF0C\u5237\u65B0\u4E0D\u5199\u76EE\u5F55\u6587\u4EF6\uFF1A\u65B0\u6761\u76EE\u76F4\u63A5\u6CE8\u5165\u8FD0\u884C\u4E2D\u7684\u76EE\u5F55\u6CE8\u518C\u8868\u3002\u65E0\u9700\u91CD\u542F\u3002",
257
- registryApplied: "\u5DF2\u5411\u8FD0\u884C\u4E2D\u7684\u76EE\u5F55\u6CE8\u5165 %s \u4E2A\u65B0\u6A21\u578B\u2014\u2014\u5373\u523B\u751F\u6548\uFF1B\u91CD\u542F\u540E\u7531\u542F\u52A8\u5E8F\u5217\u81EA\u52A8\u91CD\u653E\u3002",
258
- registryFailed: "\u76EE\u5F55\u6CE8\u518C\u8868\u6CE8\u5165\u5931\u8D25\u2014\u2014\u672A\u5199\u5165\u4EFB\u4F55\u5185\u5BB9\uFF0C\u6A21\u578B\u5217\u8868\u4FDD\u6301\u539F\u6837\u3002",
259
- riskImmediate: "\u672C\u8FDB\u7A0B\u5185\u5373\u523B\u751F\u6548\uFF08\u6CE8\u518C\u8868\u6CE8\u5165\uFF09\uFF1B\u91CD\u542F\u540E\u7531\u542F\u52A8\u5E8F\u5217\u81EA\u52A8\u91CD\u653E\u3002"
653
+ applying: "\u5E94\u7528\u4E2D\u2026",
654
+ refreshing: "\u62C9\u53D6\u9884\u89C8\u4E2D\u2026",
655
+ cancel: "\u53D6\u6D88",
656
+ applied: "\u66F4\u6539\u5DF2\u751F\u6548\uFF0C\u65E0\u9700\u91CD\u542F\u3002",
657
+ pendingRestart: "\u66F4\u6539\u5DF2\u4FDD\u5B58\uFF0C\u5F85\u91CD\u542F\u751F\u6548\u3002\u8BF7\u91CD\u542F\u8FD0\u884C\u6B64\u5B9E\u4F8B\u7684\u670D\u52A1\u6216\u5E94\u7528\uFF1B\u4EC5\u5237\u65B0\u9875\u9762\u4E0D\u4F1A\u751F\u6548\u3002",
658
+ conflict: "\u914D\u7F6E\u5DF2\u53D8\u5316\u3002\u4E3A\u4FDD\u62A4\u4F60\u7684\u4FEE\u6539\uFF0C\u8BF7\u91CD\u65B0\u9884\u89C8\u5E76\u786E\u8BA4\u3002",
659
+ partial: "\u66F4\u6539\u672A\u5B8C\u5168\u5E94\u7528\uFF0C\u8BF7\u67E5\u770B\u5F53\u524D\u72B6\u6001\u548C\u6062\u590D\u5EFA\u8BAE\u3002",
660
+ rolledBack: "\u672C\u6B21\u6A21\u578B\u914D\u7F6E\u66F4\u6539\u5DF2\u56DE\u6EDA\u3002\u76EE\u5F55\u6570\u636E\u53EF\u80FD\u4ECD\u5DF2\u8865\u5145\u3002",
661
+ rollbackConflict: "\u68C0\u6D4B\u5230\u66F4\u65B0\u7684\u914D\u7F6E\uFF0C\u5DF2\u4FDD\u7559\u4F60\u7684\u4FEE\u6539\uFF1B\u81EA\u52A8\u56DE\u6EDA\u672A\u5B8C\u6210\u3002",
662
+ busy: "\u53E6\u4E00\u4E2A\u64CD\u4F5C\u6B63\u5728\u5904\u7406\u6B64\u8D44\u6E90\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5\u3002",
663
+ unknownResult: "\u5C1A\u672A\u786E\u8BA4\u64CD\u4F5C\u5B8C\u6210\u3002\u8BF7\u6838\u5B9E\u5F53\u524D\u72B6\u6001\uFF0C\u52FF\u91CD\u590D\u63D0\u4EA4\u3002",
664
+ recoveryNeeded: "\u5B58\u5728\u5DF2\u5931\u6548\u6216\u51B2\u7A81\u7684\u65E7\u914D\u7F6E\u610F\u56FE\uFF0C\u8BF7\u5148\u7ED3\u675F\u65E7\u610F\u56FE\u518D\u5E94\u7528\u65B0\u66F4\u6539\u3002",
665
+ retireBtn: "\u7ED3\u675F\u65E7\u914D\u7F6E\u610F\u56FE",
666
+ retireTitle: "\u7ED3\u675F\u65E7\u914D\u7F6E\u610F\u56FE",
667
+ retireConfirmText: "\u53EA\u7ED3\u675F\u65E7\u610F\u56FE\uFF0C\u4E0D\u56DE\u6EDA\u6216\u64A4\u9500\u5DF2\u53D1\u751F\u7684\u66F4\u6539\u3002",
668
+ legacy: "\u68C0\u6D4B\u5230\u65E7\u7684\u5171\u4EAB\u6062\u590D\u72B6\u6001\u3002\u4E0D\u4F1A\u81EA\u52A8\u5E94\u7528\uFF0C\u8BF7\u5728\u5F53\u524D profile \u91CD\u65B0\u9884\u89C8\u786E\u8BA4\u3002",
669
+ techDetails: "\u6280\u672F\u8BE6\u60C5",
670
+ stateCorrupt: "\u6062\u590D\u72B6\u6001\u65E0\u6CD5\u8BFB\u53D6\uFF0C\u539F\u4EF6\u5DF2\u4FDD\u7559\u3002\u8BF7\u91CD\u65B0\u9884\u89C8\u786E\u8BA4\u3002",
671
+ unavailable: "\u5F53\u524D\u8FD0\u884C\u65F6\u7F3A\u5C11\u5B89\u5168\u5E94\u7528\u6240\u9700\u7684\u80FD\u529B\uFF0C\u6682\u4E0D\u80FD\u5E94\u7528\u66F4\u6539\u3002",
672
+ blockedReasons: {
673
+ "inject-unavailable": "\u5F53\u524D\u5B89\u88C5\u7684\u8FDB\u7A0B\u5185\u76EE\u5F55\u6CE8\u518C\u8868\u4E0D\u53EF\u7528\u3002",
674
+ "install-unresolved": "\u65E0\u6CD5\u5B9A\u4F4D pi-ai \u5B89\u88C5\u3002"
675
+ }
260
676
  }
261
677
  };
262
678
  var NAV_TEXTS = Object.keys(DICTS).map((locale) => DICTS[locale].nav);
263
- function isRegistryMode(flags) {
264
- return flags?.catalogWritable === false && flags?.catalogMode === "registry";
265
- }
266
- function refreshBlocked(flags) {
267
- return flags?.catalogWritable === false && flags?.catalogMode !== "registry";
679
+ function blockedReasonText(t, flags) {
680
+ const reason = flags?.blockedReason;
681
+ if (reason && t("blockedReasons")?.[reason]) return t("blockedReasons")[reason];
682
+ return t("unavailable");
268
683
  }
269
684
  var COPILOT_ICON_SVG = '<svg aria-hidden="true" width="16" height="16" viewBox="0 0 16 16" fill="currentColor" style="flex:none"><path d="M7.998 15.035c-4.562 0-7.873-2.914-7.998-3.749V9.338c.085-.628.677-1.686 1.588-2.065.013-.07.024-.143.036-.218.029-.183.06-.384.126-.612-.201-.508-.254-1.084-.254-1.656 0-.87.128-1.769.693-2.484.579-.733 1.494-1.124 2.724-1.261 1.206-.134 2.262.034 2.944.765.05.053.096.108.139.165.044-.057.094-.112.143-.165.682-.731 1.738-.899 2.944-.765 1.23.137 2.145.528 2.724 1.261.566.715.693 1.614.693 2.484 0 .572-.053 1.148-.254 1.656.066.228.098.429.126.612.924.385 1.522 1.471 1.591 2.095v1.872c0 .766-3.351 3.795-8.002 3.795Zm0-1.485c2.28 0 4.584-1.11 5.002-1.433V7.862l-.023-.116c-.49.21-1.075.291-1.727.291-1.146 0-2.059-.327-2.71-.991A3.222 3.222 0 0 1 8 6.303a3.24 3.24 0 0 1-.544.743c-.65.664-1.563.991-2.71.991-.652 0-1.236-.081-1.727-.291l-.023.116v4.255c.419.323 2.722 1.433 5.002 1.433ZM6.762 2.83c-.193-.206-.637-.413-1.682-.297-1.019.113-1.479.404-1.713.7-.247.312-.369.789-.369 1.554 0 .793.129 1.171.308 1.371.162.181.519.379 1.442.379.853 0 1.339-.235 1.638-.54.315-.322.527-.827.617-1.553.117-.935-.037-1.395-.241-1.614Zm4.155-.297c-1.044-.116-1.488.091-1.681.297-.204.219-.359.679-.242 1.614.091.726.303 1.231.618 1.553.299.305.784.54 1.638.54.922 0 1.28-.198 1.442-.379.179-.2.308-.578.308-1.371 0-.765-.123-1.242-.37-1.554-.233-.296-.693-.587-1.713-.7Z"/><path d="M6.25 9.037a.75.75 0 0 1 .75.75v1.501a.75.75 0 0 1-1.5 0V9.787a.75.75 0 0 1 .75-.75Zm4.25.75v1.501a.75.75 0 0 1-1.5 0V9.787a.75.75 0 0 1 1.5 0Z"/></svg>';
270
685
  function enforceNavIcon(labelText) {
@@ -373,7 +788,7 @@ var styles = {
373
788
  modalActions: { display: "flex", gap: 10, justifyContent: "flex-end", marginTop: 4 },
374
789
  mono: { fontFamily: "ui-monospace, monospace" }
375
790
  };
376
- var badgeColor = { idle: "#adb5bd", running: "#f59f00", authorized: "#37b24d", failed: "#e03131", loading: "#adb5bd" };
791
+ var badgeColor = { idle: "#adb5bd", waiting: "#f59f00", authorized: "#37b24d", failed: "#e03131", loading: "#adb5bd", risk: "#f59f00", unavailable: "#adb5bd" };
377
792
  function sampleThemeSurface() {
378
793
  if (typeof document === "undefined") return { bg: "#fff", fg: "inherit" };
379
794
  const cs = getComputedStyle(document.body);
@@ -381,108 +796,182 @@ function sampleThemeSurface() {
381
796
  const bg = m ? `rgb(${m[1].split(",").slice(0, 3).join(",")})` : "#fff";
382
797
  return { bg, fg: cs.color || "inherit" };
383
798
  }
384
- function RefreshModal({ t, flow, onConfirm, onCancel, onOverlay }) {
799
+ function RefreshModal({ t, flow, onSelect, onConfirm, onCancel, onOverlay }) {
385
800
  const p = flow.preview;
801
+ const diff = p.diff ?? {};
802
+ const isRebuild = p.operation === "rebuild";
803
+ const emptyTarget = isRebuild && (diff.targetView?.models ?? []).length === 0;
804
+ const candidates = diff.candidates ?? [];
805
+ const selectedIds = new Set(diff.added ?? []);
806
+ const [stage, setStage] = (0, import_react.useState)(0);
807
+ const cancelRef = (0, import_react.useRef)(null);
386
808
  const surface = (0, import_react.useRef)(null);
387
809
  if (!surface.current) surface.current = sampleThemeSurface();
388
- const sourceKeys = [
389
- p.source === "live" ? "srcLive" : "srcCache",
390
- p.catalogSource === "latest" ? "srcLatest" : p.catalogSource === "overlay" ? "srcOverlay" : "srcLocal"
391
- ];
392
- const list = (ids, style) => ids.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { style: { opacity: 0.6 }, children: t("none") }) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)("ul", { style: styles.diffList, children: ids.map((id) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)("li", { style: { ...style, ...styles.mono }, children: id }, id)) });
393
- const resets = [
394
- ...p.customizationReset.modelEntryIds.map((id) => `${id} (${t("customEntries")})`),
395
- ...p.customizationReset.modelOverrideIds.map((id) => `${id} (${t("customOverrides")})`)
396
- ];
397
- return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: styles.modalMask, role: "dialog", "aria-modal": "true", children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: { ...styles.modal, background: surface.current.bg, color: surface.current.fg }, children: [
398
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("h4", { style: styles.modalTitle, children: t("refreshTitle") }),
399
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { style: styles.modalText, children: t("refreshDesc") }),
810
+ (0, import_react.useEffect)(() => {
811
+ setStage(0);
812
+ }, [p.previewId]);
813
+ (0, import_react.useEffect)(() => {
814
+ if (stage > 0) cancelRef.current?.focus();
815
+ }, [stage]);
816
+ const escCancel = (e) => {
817
+ if (e.key === "Escape") onCancel();
818
+ };
819
+ const confirmClick = () => {
820
+ if (p.evidence?.stale === true) return;
821
+ if (!isRebuild) {
822
+ onConfirm({ second: false, empty: false });
823
+ return;
824
+ }
825
+ if (stage === 0) {
826
+ setStage(1);
827
+ return;
828
+ }
829
+ if (emptyTarget && stage === 1) {
830
+ setStage(2);
831
+ return;
832
+ }
833
+ onConfirm({ second: true, empty: emptyTarget });
834
+ };
835
+ const sourceLine = [
836
+ p.evidence?.source === "live" ? t("srcLive") : p.evidence?.source === "cache" ? t("srcCache").replace("{time}", String(p.evidence?.fetchedAt ?? "")) : null,
837
+ p.catalogSource === "latest" ? t("srcLatest") : p.catalogSource === "overlay" ? t("srcOverlay") : t("srcLocal")
838
+ ].filter(Boolean).join(" \xB7 ");
839
+ const list = (items, style, render) => items.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { style: { opacity: 0.6 }, children: t("none") }) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)("ul", { style: styles.diffList, children: items.map((x, i) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)("li", { style: { ...style, ...styles.mono }, children: render ? render(x) : x }, x?.id ?? x ?? i)) });
840
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: styles.modalMask, role: "dialog", "aria-modal": "true", onKeyDown: escCancel, children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: { ...styles.modal, background: surface.current.bg, color: surface.current.fg }, children: [
841
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("h4", { style: styles.modalTitle, children: t(isRebuild ? "rebuildTitle" : "supplementTitle") }),
400
842
  flow.staleNotice && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("p", { style: styles.banner, children: [
401
843
  "\u26A0 ",
402
- t("stalePreview")
844
+ t("previewStale")
845
+ ] }),
846
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { style: styles.modalText, children: sourceLine }),
847
+ p.evidence?.stale === true && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("p", { style: styles.banner, children: [
848
+ "\u26A0 ",
849
+ t("srcStale")
850
+ ] }),
851
+ !isRebuild && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
852
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("p", { style: styles.modalText, children: [
853
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("strong", { children: t("candidates") }),
854
+ "\uFF08",
855
+ candidates.length,
856
+ "\uFF09",
857
+ candidates.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { type: "button", style: { ...styles.button, ...styles.secondary, height: 24, marginLeft: 10, fontSize: 12 }, onClick: () => onSelect(candidates), children: t("selectAll") })
858
+ ] }),
859
+ list(candidates, styles.added, (id) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("label", { style: { display: "flex", gap: 8, alignItems: "center" }, children: [
860
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("input", { type: "checkbox", checked: selectedIds.has(id), onChange: () => {
861
+ const next = new Set(selectedIds);
862
+ if (next.has(id)) next.delete(id);
863
+ else next.add(id);
864
+ onSelect([...next]);
865
+ } }),
866
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { children: id })
867
+ ] })),
868
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("p", { style: styles.risk, children: [
869
+ "\u26A0 ",
870
+ t("supplementRisk")
871
+ ] })
403
872
  ] }),
404
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { style: styles.modalText, children: sourceKeys.map((k) => t(k)).join(" \xB7 ") }),
405
873
  /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("p", { style: styles.modalText, children: [
406
874
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)("strong", { children: t("addedModels") }),
407
875
  "\uFF08",
408
- p.added.length,
876
+ (diff.added ?? []).length,
409
877
  "\uFF09"
410
878
  ] }),
411
- list(p.added, styles.added),
879
+ list(diff.added ?? [], styles.added),
412
880
  /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("p", { style: styles.modalText, children: [
413
881
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)("strong", { children: t("removedModels") }),
414
882
  "\uFF08",
415
- p.removed.length,
883
+ (diff.removed ?? []).length,
416
884
  "\uFF09"
417
885
  ] }),
418
- list(p.removed, styles.removed),
886
+ list(diff.removed ?? [], styles.removed, (x) => `${x.id} \u2014 ${t(x.reason === "unresolvable" ? "removedUnresolvable" : "removedAccount")}`),
419
887
  /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("p", { style: styles.modalText, children: [
420
888
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)("strong", { children: t("keptModels") }),
421
889
  "\uFF08",
422
- p.kept.length,
890
+ (diff.kept ?? []).length,
423
891
  "\uFF09"
424
892
  ] }),
425
- p.skipped.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
893
+ (diff.warnings ?? []).length > 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
426
894
  /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("p", { style: styles.modalText, children: [
427
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("strong", { children: t("skippedModels") }),
895
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("strong", { children: t("warnings") }),
428
896
  "\uFF08",
429
- p.skipped.length,
897
+ diff.warnings.length,
430
898
  "\uFF09"
431
899
  ] }),
432
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("ul", { style: styles.diffList, children: p.skipped.map((s) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("li", { style: styles.skipped, children: [
433
- s.id,
434
- " \u2014 ",
435
- s.reason
436
- ] }, s.id)) })
437
- ] }),
438
- resets.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
439
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { style: styles.modalText, children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("strong", { children: t("customReset") }) }),
440
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("ul", { style: styles.diffList, children: resets.map((x) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)("li", { style: styles.skipped, children: x }, x)) })
900
+ list(diff.warnings, styles.skipped, (w) => `${w.id ?? ""} \u2014 ${w.reason ?? ""}`)
441
901
  ] }),
442
- /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("p", { style: styles.risk, children: [
902
+ isRebuild && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("p", { style: styles.risk, children: [
443
903
  "\u26A0 ",
444
- t("riskRemoved")
904
+ t("rebuildRisk")
445
905
  ] }),
446
- /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("p", { style: styles.risk, children: [
906
+ emptyTarget && stage >= 1 && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("p", { style: styles.risk, children: [
447
907
  "\u26A0 ",
448
- t("riskReset")
908
+ t("clearRisk")
449
909
  ] }),
450
- isRegistryMode(p) ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { style: styles.modalText, children: t("riskImmediate") }) : /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("p", { style: styles.risk, children: [
910
+ stage === 1 && !emptyTarget && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("p", { style: styles.risk, children: [
451
911
  "\u26A0 ",
452
- t("riskRestart")
912
+ t("secondConfirm")
453
913
  ] }),
454
- isRegistryMode(p) && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("p", { style: styles.banner, children: [
455
- "\u24D8 ",
456
- t("registryMode")
914
+ (p.skipped ?? []).length > 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("details", { style: styles.modalText, children: [
915
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("summary", { children: [
916
+ t("skippedModels"),
917
+ "\uFF08",
918
+ p.skipped.length,
919
+ "\uFF09"
920
+ ] }),
921
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("ul", { style: styles.diffList, children: p.skipped.map((s) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("li", { style: styles.skipped, children: [
922
+ s.id,
923
+ " \u2014 ",
924
+ s.reason
925
+ ] }, s.id)) })
457
926
  ] }),
458
- refreshBlocked(p) && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("p", { style: styles.error, children: [
459
- "\u26A0 ",
460
- t("notWritable")
927
+ p.catalogError && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("details", { style: styles.modalText, children: [
928
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("summary", { children: t("techDetails") }),
929
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { style: { ...styles.mono, fontSize: 12 }, children: p.catalogError })
461
930
  ] }),
462
931
  /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: styles.modalActions, children: [
463
932
  p.catalogSource === "local" && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { type: "button", style: { ...styles.button, ...styles.secondary, marginRight: "auto" }, onClick: onOverlay, children: t("overlayBtn") }),
464
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { type: "button", style: { ...styles.button, ...styles.secondary }, onClick: onCancel, children: t("cancel") }),
933
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { ref: cancelRef, type: "button", style: { ...styles.button, ...styles.secondary }, onClick: onCancel, children: t("cancel") }),
465
934
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
466
935
  "button",
467
936
  {
468
937
  type: "button",
469
- style: { ...styles.button, ...styles.primary, ...refreshBlocked(p) ? { opacity: 0.5, cursor: "not-allowed" } : null },
470
- disabled: refreshBlocked(p),
471
- onClick: onConfirm,
472
- children: t("confirmRefresh")
938
+ style: { ...styles.button, ...styles.primary, ...p.evidence?.stale === true ? { opacity: 0.5, cursor: "not-allowed" } : null },
939
+ disabled: p.evidence?.stale === true,
940
+ onClick: confirmClick,
941
+ children: t("applyChanges")
473
942
  }
474
943
  )
475
944
  ] })
476
945
  ] }) });
477
946
  }
947
+ function RetireDialog({ t, onConfirm, onCancel }) {
948
+ const cancelRef = (0, import_react.useRef)(null);
949
+ const surface = (0, import_react.useRef)(null);
950
+ if (!surface.current) surface.current = sampleThemeSurface();
951
+ (0, import_react.useEffect)(() => {
952
+ cancelRef.current?.focus();
953
+ }, []);
954
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: styles.modalMask, role: "dialog", "aria-modal": "true", onKeyDown: (e) => {
955
+ if (e.key === "Escape") onCancel();
956
+ }, children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: { ...styles.modal, background: surface.current.bg, color: surface.current.fg, maxWidth: 420 }, children: [
957
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("h4", { style: styles.modalTitle, children: t("retireTitle") }),
958
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { style: styles.modalText, children: t("retireConfirmText") }),
959
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: styles.modalActions, children: [
960
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { ref: cancelRef, type: "button", style: { ...styles.button, ...styles.secondary }, onClick: onCancel, children: t("cancel") }),
961
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { type: "button", style: { ...styles.button, ...styles.primary }, onClick: onConfirm, children: t("retireBtn") })
962
+ ] })
963
+ ] }) });
964
+ }
478
965
  function CopilotSection({ t = (key) => DICTS.en[key] ?? key }) {
479
- const [page, setPage] = (0, import_react.useState)("loading");
480
- const [notices, setNotices] = (0, import_react.useState)([]);
481
- const [error, setError] = (0, import_react.useState)(void 0);
966
+ const [auth, setAuth] = (0, import_react.useState)({ phase: "loading", connectivity: "online", shared: false });
482
967
  const [copied, setCopied] = (0, import_react.useState)(false);
483
- const timer = (0, import_react.useRef)(null);
968
+ const [copyFail, setCopyFail] = (0, import_react.useState)(false);
484
969
  const [flow, setFlow] = (0, import_react.useState)(initial);
485
970
  const flowRef = (0, import_react.useRef)(flow);
971
+ const authFlowRef = (0, import_react.useRef)(null);
972
+ if (!authFlowRef.current && typeof fetch === "function") {
973
+ authFlowRef.current = createAuthFlow({ onState: (s) => setAuth(s) });
974
+ }
486
975
  const drive = (event) => advance(flowRef.current, event, fetch, (next) => {
487
976
  flowRef.current = next;
488
977
  setFlow(next);
@@ -492,141 +981,183 @@ function CopilotSection({ t = (key) => DICTS.en[key] ?? key }) {
492
981
  drive({ type: "init" });
493
982
  }, []);
494
983
  (0, import_react.useEffect)(() => {
495
- let alive = true;
496
- fetch("/copilot-auth/status").then((r) => r.json()).then((d) => {
497
- if (!alive) return;
498
- if (d.configured) {
499
- setPage("authorized");
500
- return;
501
- }
502
- fetch("/copilot-auth/state").then((r) => r.json()).then((s) => {
503
- if (!alive) return;
504
- setNotices(s.notices ?? []);
505
- if (s.status === "failed") {
506
- setPage("failed");
507
- setError(s.error ?? t("unknown"));
508
- } else if (s.status === "running") {
509
- setPage("running");
510
- poll();
511
- } else {
512
- setPage("idle");
513
- }
514
- }).catch(() => {
515
- if (alive) setPage("idle");
516
- });
517
- }).catch(() => {
518
- if (alive) setPage("idle");
519
- });
520
- return () => {
521
- alive = false;
522
- if (timer.current) clearInterval(timer.current);
523
- };
984
+ void authFlowRef.current?.init();
985
+ return () => authFlowRef.current?.dispose();
524
986
  }, []);
525
- const poll = () => {
526
- if (timer.current) clearInterval(timer.current);
527
- timer.current = setInterval(() => {
528
- fetch("/copilot-auth/state").then((r) => r.json()).then((s) => {
529
- setNotices(s.notices ?? []);
530
- if (s.status !== "running") {
531
- if (timer.current) clearInterval(timer.current);
532
- timer.current = null;
533
- if (s.status === "authorized") {
534
- setPage("authorized");
535
- } else {
536
- setPage("failed");
537
- setError(s.error ?? t("unknown"));
538
- }
539
- }
540
- }).catch(() => {
541
- });
542
- }, 1e3);
543
- };
544
987
  const login = () => {
545
988
  setCopied(false);
546
- setError(void 0);
547
- setNotices([]);
548
- fetch("/copilot-auth/start", { method: "POST" }).catch(() => {
549
- });
550
- setPage("running");
551
- poll();
989
+ setCopyFail(false);
990
+ void authFlowRef.current?.start();
552
991
  };
553
- const logout = async () => {
554
- if (timer.current) {
555
- clearInterval(timer.current);
556
- timer.current = null;
557
- }
558
- try {
559
- await fetch("/copilot-auth/logout", { method: "POST" });
560
- } catch {
561
- }
562
- setNotices([]);
563
- setError(void 0);
564
- setPage("idle");
992
+ const withdraw = () => {
993
+ void authFlowRef.current?.cancel();
994
+ };
995
+ const checkStatus = () => {
996
+ void authFlowRef.current?.refresh();
565
997
  };
566
998
  const copyCode = async (code) => {
567
999
  try {
568
1000
  await navigator.clipboard.writeText(code);
569
1001
  setCopied(true);
1002
+ setCopyFail(false);
570
1003
  setTimeout(() => setCopied(false), 2e3);
571
1004
  } catch {
1005
+ setCopyFail(true);
572
1006
  }
573
1007
  };
574
- const codeNotice = [...notices].reverse().find((n) => n && typeof n.code === "string" && n.code !== "");
575
- const badgeText = page === "loading" ? t("loading") : t(page);
1008
+ const phase = auth.phase;
1009
+ const attempt = auth.attempt;
1010
+ const notices = [...attempt?.notices ?? []].reverse();
1011
+ const codeNotice = typeof attempt?.code === "string" && attempt.code !== "" ? { code: attempt.code, url: attempt.url } : (() => {
1012
+ const n = notices.find((x) => x && typeof x.code === "string" && x.code !== "");
1013
+ return n ? { code: n.code, url: n.url } : null;
1014
+ })();
1015
+ const badgeText = t(phase === "risk" ? "riskBadge" : phase === "loading" ? "loading" : phase);
576
1016
  return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: styles.section, children: [
577
1017
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)("h3", { style: styles.title, children: t("title") }),
578
1018
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { style: styles.intro, children: t("intro") }),
579
1019
  /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: styles.badgeRow, children: [
580
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { style: { ...styles.dot, background: badgeColor[page] ?? "#adb5bd" } }),
1020
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { style: { ...styles.dot, background: badgeColor[phase] ?? "#adb5bd" } }),
581
1021
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { style: styles.badge, children: badgeText })
582
1022
  ] }),
583
- page === "running" && codeNotice && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: styles.card, children: [
584
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { style: styles.codeHint, children: t("codeHint") }),
585
- /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: styles.codeRow, children: [
586
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { style: styles.code, children: codeNotice.code }),
587
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { type: "button", style: { ...styles.button, ...styles.secondary }, onClick: () => copyCode(codeNotice.code), children: copied ? t("copied") : t("copy") })
588
- ] }),
589
- codeNotice.url && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("a", { style: styles.link, href: codeNotice.url, target: "_blank", rel: "noreferrer", children: codeNotice.url })
1023
+ auth.connectivity !== "online" && phase !== "loading" && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("p", { style: styles.banner, children: [
1024
+ "\u26A0 ",
1025
+ t("connection")
590
1026
  ] }),
591
- page === "failed" && error && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { style: styles.error, children: error }),
592
- page === "authorized" && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: { display: "flex", gap: 10, alignItems: "center", flexWrap: "wrap" }, children: [
593
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { type: "button", style: { ...styles.button, ...styles.secondary }, onClick: logout, children: t("logout") }),
594
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
595
- "button",
596
- {
597
- type: "button",
598
- style: { ...styles.button, ...styles.primary, ...refreshBlocked(flow) ? { opacity: 0.5, cursor: "not-allowed" } : null },
599
- disabled: refreshBlocked(flow) || flow.name === "previewing" || flow.name === "applying",
600
- onClick: () => drive({ type: "start" }),
601
- children: flow.name === "previewing" ? t("refreshing") : flow.name === "applying" ? t("applying") : t("refreshNow")
602
- }
603
- )
1027
+ auth.connectivity === "manual" && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { type: "button", style: { ...styles.button, ...styles.secondary }, onClick: checkStatus, children: t("retryNow") }) }),
1028
+ phase === "waiting" && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
1029
+ auth.shared && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("p", { style: styles.banner, children: [
1030
+ "\u24D8 ",
1031
+ t("attemptShared")
1032
+ ] }),
1033
+ codeNotice && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: styles.card, children: [
1034
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { style: styles.codeHint, children: t("codeHint") }),
1035
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: styles.codeRow, children: [
1036
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { style: styles.code, children: codeNotice.code }),
1037
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { type: "button", style: { ...styles.button, ...styles.secondary }, onClick: () => copyCode(codeNotice.code), children: copied ? t("copied") : t("copy") })
1038
+ ] }),
1039
+ codeNotice.url && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("a", { style: styles.link, href: codeNotice.url, target: "_blank", rel: "noreferrer", children: codeNotice.url }),
1040
+ copyFail && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { style: styles.error, children: t("copyFailed") })
1041
+ ] }),
1042
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { type: "button", style: { ...styles.button, ...styles.secondary }, onClick: withdraw, children: t("withdrawAuth") }) })
604
1043
  ] }),
605
- page === "authorized" && isRegistryMode(flow) && flow.name !== "failed" && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("p", { style: styles.banner, children: [
606
- "\u24D8 ",
607
- t("registryMode")
1044
+ phase === "risk" && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: styles.card, children: [
1045
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("p", { style: styles.banner, children: [
1046
+ "\u26A0 ",
1047
+ t("authUnsafe")
1048
+ ] }),
1049
+ auth.riskKind === "withdrawal-pending-unverified" && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { style: styles.modalText, children: auth.withdrawalDelivery === "invoked" ? t("withdrawal") : t("withdrawalUnavailable") }),
1050
+ auth.riskKind === "timed-out-unverified" && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { style: styles.modalText, children: t("attemptTimeout") }),
1051
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { type: "button", style: { ...styles.button, ...styles.secondary }, onClick: checkStatus, children: t("retryNow") }) })
608
1052
  ] }),
609
- page === "authorized" && refreshBlocked(flow) && flow.name !== "failed" && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("p", { style: styles.banner, children: [
1053
+ phase === "unavailable" && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("p", { style: styles.banner, children: [
610
1054
  "\u26A0 ",
611
- t("notWritable")
1055
+ t("unavailable")
612
1056
  ] }),
613
- page === "authorized" && flow.name === "applied" && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { style: styles.banner, children: t("registryApplied").replace("%s", String(flow.injected ?? 0)) }),
614
- page === "authorized" && flow.name === "restartNeeded" && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("p", { style: styles.banner, children: [
615
- "\u26A0 ",
616
- t("restartNeeded")
1057
+ phase === "failed" && attempt?.error && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { style: styles.error, children: attempt.error }),
1058
+ phase === "authorized" && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: styles.card, children: [
1059
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: { display: "flex", gap: 10, alignItems: "center", flexWrap: "wrap" }, children: [
1060
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { type: "button", style: { ...styles.button, ...styles.secondary, opacity: 0.5, cursor: "not-allowed" }, disabled: true, title: t("authUnsafe"), children: t("logout") }),
1061
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { type: "button", style: { ...styles.button, ...styles.secondary, opacity: 0.5, cursor: "not-allowed" }, disabled: true, title: t("authUnsafe"), children: t("reauthorize") }),
1062
+ ["previewing", "applying", "checking", "retiring"].includes(flow.name) ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { type: "button", style: { ...styles.button, ...styles.primary, opacity: 0.5 }, disabled: true, children: flow.name === "applying" ? t("applying") : t("refreshing") }) : /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
1063
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1064
+ "button",
1065
+ {
1066
+ type: "button",
1067
+ style: { ...styles.button, ...styles.primary, ...refreshBlocked(flow.flags) ? { opacity: 0.5, cursor: "not-allowed" } : null },
1068
+ disabled: refreshBlocked(flow.flags),
1069
+ onClick: () => drive({ type: "start", operation: "supplement" }),
1070
+ children: t("supplement")
1071
+ }
1072
+ ),
1073
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1074
+ "button",
1075
+ {
1076
+ type: "button",
1077
+ style: { ...styles.button, ...styles.primary, ...refreshBlocked(flow.flags) ? { opacity: 0.5, cursor: "not-allowed" } : null },
1078
+ disabled: refreshBlocked(flow.flags),
1079
+ onClick: () => drive({ type: "start", operation: "rebuild" }),
1080
+ children: t("rebuild")
1081
+ }
1082
+ )
1083
+ ] })
1084
+ ] }),
1085
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("p", { style: styles.banner, children: [
1086
+ "\u24D8 ",
1087
+ t("authUnsafe")
1088
+ ] }),
1089
+ refreshBlocked(flow.flags) && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("p", { style: styles.banner, children: [
1090
+ "\u26A0 ",
1091
+ blockedReasonText(t, flow.flags)
1092
+ ] }),
1093
+ flow.flags?.legacyStateDetected && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("p", { style: styles.banner, children: [
1094
+ "\u24D8 ",
1095
+ t("legacy")
1096
+ ] }),
1097
+ flow.name === "pendingRestart" && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("p", { style: styles.banner, children: [
1098
+ "\u26A0 ",
1099
+ t("pendingRestart")
1100
+ ] }),
1101
+ flow.name === "busy" && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("p", { style: styles.banner, children: [
1102
+ "\u24D8 ",
1103
+ t("busy")
1104
+ ] }),
1105
+ flow.name === "resultUnknown" && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
1106
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("p", { style: styles.error, children: [
1107
+ "\u26A0 ",
1108
+ t("unknownResult")
1109
+ ] }),
1110
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { type: "button", style: { ...styles.button, ...styles.secondary }, onClick: () => drive({ type: "check" }), children: t("retryNow") }) })
1111
+ ] }),
1112
+ flow.name === "result" && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { children: [
1113
+ (() => {
1114
+ const st = flow.result?.status;
1115
+ const key = {
1116
+ applied: "applied",
1117
+ conflict: "conflict",
1118
+ partial: "partial",
1119
+ "rolled-back": "rolledBack",
1120
+ "rollback-conflict": "rollbackConflict",
1121
+ "recovery-needed": "recoveryNeeded",
1122
+ "intent-retired": "recoveryNeeded"
1123
+ }[st];
1124
+ return key ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("p", { style: st === "applied" ? styles.banner : styles.error, children: [
1125
+ st === "applied" ? "\u2713" : "\u26A0",
1126
+ " ",
1127
+ t(key)
1128
+ ] }) : null;
1129
+ })(),
1130
+ ["recovery-needed", "rollback-conflict", "intent-retired"].includes(flow.result?.status) && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { type: "button", style: { ...styles.button, ...styles.secondary }, onClick: () => drive({ type: "retire-request" }), children: t("retireBtn") }) }),
1131
+ (flow.result?.error || flow.result?.changes) && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("details", { style: styles.modalText, children: [
1132
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("summary", { children: t("techDetails") }),
1133
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", { style: { ...styles.mono, fontSize: 12 }, children: [
1134
+ flow.result?.error ?? "",
1135
+ flow.result?.changes ? ` ${JSON.stringify(flow.result.changes)}` : ""
1136
+ ] })
1137
+ ] })
1138
+ ] }),
1139
+ flow.name === "failed" && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { style: styles.error, children: flow.error === "state-corrupt" ? t("stateCorrupt") : flow.error })
617
1140
  ] }),
618
- page === "authorized" && flow.name === "failed" && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { style: styles.error, children: flow.error === "state-corrupt" ? t("stateCorrupt") : flow.error === "catalog-not-writable" ? t("notWritable") : flow.error === "registry-inject-failed" || flow.error === "registry-not-effective" ? t("registryFailed") : flow.error }),
619
1141
  flow.name === "confirming" && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
620
1142
  RefreshModal,
621
1143
  {
622
1144
  t,
623
1145
  flow,
624
- onConfirm: () => drive({ type: "confirm" }),
1146
+ onSelect: (selectedIds) => drive({ type: "select", selectedIds }),
1147
+ onConfirm: (opts) => drive({ type: "confirm", ...opts }),
625
1148
  onCancel: () => drive({ type: "cancel" }),
626
- onOverlay: () => drive({ type: "start", mode: "overlay" })
1149
+ onOverlay: () => drive({ type: "start", catalogSource: "overlay" })
1150
+ }
1151
+ ),
1152
+ flow.name === "retireConfirm" && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1153
+ RetireDialog,
1154
+ {
1155
+ t,
1156
+ onConfirm: () => drive({ type: "retire-confirm" }),
1157
+ onCancel: () => drive({ type: "cancel" })
627
1158
  }
628
1159
  ),
629
- (page === "idle" || page === "failed") && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { type: "button", style: { ...styles.button, ...styles.primary }, onClick: login, children: t("login") }) })
1160
+ (phase === "idle" || phase === "failed") && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { type: "button", style: { ...styles.button, ...styles.primary }, onClick: login, children: t("login") }) })
630
1161
  ] });
631
1162
  }
632
1163
  function apply(ctx) {