dsh-codex-subscription 2.1.0 → 2.1.1-beta.1

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
@@ -372,21 +372,55 @@ window.__ModuleLoader__.load({
372
372
  dirty: ref(false),
373
373
  documentId: ref(crypto.randomUUID()),
374
374
  documentRevision: ref(0),
375
+ restoreId: ref(null),
376
+ mounts: 0,
375
377
  agentAdapter: ref({}),
376
378
  agentSession: ref(null),
377
379
  agentRun: ref(null)
378
380
  };
379
381
  }
380
- function createSketchSessionRegistry() {
381
- const sessions = /* @__PURE__ */ new Map();
382
+ function createSketchSessionRegistry({ maxIdle = 8 } = {}) {
383
+ const sessions = /* @__PURE__ */ new Map(), archived = /* @__PURE__ */ new Map();
384
+ const prune = () => {
385
+ const idle = [...sessions].filter(([, s]) => !s.mounts && !s.dirty.current && !s.agentRun.current?.locked && s.agentRun.current?.state !== "stopped");
386
+ for (const [id, state] of idle.slice(0, Math.max(0, idle.length - maxIdle))) {
387
+ if (!state.saved.current && state.doc.current.layers.some((l) => l.image || l.strokes.length)) continue;
388
+ const restoreId = state.saved.current?.id ?? state.restoreId.current;
389
+ if (restoreId) archived.set(id, restoreId);
390
+ state.agentRun.current?.dispose();
391
+ state.images.current.clear();
392
+ sessions.delete(id);
393
+ }
394
+ };
382
395
  return {
383
396
  get(id) {
384
- if (!sessions.has(id)) sessions.set(id, createSketchSessionState());
385
- return sessions.get(id);
397
+ let state = sessions.get(id);
398
+ if (!state) {
399
+ state = createSketchSessionState();
400
+ state.restoreId.current = archived.get(id) ?? null;
401
+ archived.delete(id);
402
+ sessions.set(id, state);
403
+ }
404
+ state.retain = () => {
405
+ state.mounts++;
406
+ return () => {
407
+ state.mounts--;
408
+ prune();
409
+ };
410
+ };
411
+ sessions.delete(id);
412
+ sessions.set(id, state);
413
+ return state;
386
414
  },
415
+ prune,
416
+ stats: () => ({
417
+ resident: sessions.size,
418
+ archived: archived.size
419
+ }),
387
420
  dispose() {
388
421
  for (const value of sessions.values()) value.agentRun.current?.dispose();
389
422
  sessions.clear();
423
+ archived.clear();
390
424
  }
391
425
  };
392
426
  }
@@ -1068,100 +1102,231 @@ window.__ModuleLoader__.load({
1068
1102
  });
1069
1103
  }
1070
1104
  //#endregion
1071
- //#region src/sketch-run-status.jsx
1072
- function SketchRunStatus({ state, t, floating = false, onOpen, onStop, onResume, onDismiss }) {
1073
- if (state === "idle") return null;
1074
- const drawing = state === "drawing", recover = state === "stopped" || state === "failed";
1075
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1076
- className: floating ? "codexSketchBackgroundStatus" : "codexSketchAgentStatus",
1077
- role: "status",
1078
- "aria-live": "polite",
1105
+ //#region src/sketch-layer-panel.jsx
1106
+ function SketchLayerPanel({ document, disabled, change, t }) {
1107
+ const current = document.layers.find((layer) => layer.id === document.active);
1108
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("aside", {
1109
+ className: "codexSketchLayers",
1110
+ "aria-label": t("sketchLayers"),
1079
1111
  children: [
1080
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(WorkspaceIcon, {
1081
- name: drawing ? "pen" : state === "finished" ? "check" : "rectangle",
1082
- size: 15
1083
- }),
1084
- floating ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1112
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("header", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("strong", { children: t("sketchLayers") }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1085
1113
  type: "button",
1086
- onClick: onOpen,
1087
- children: t(`sketchRun_${state}`)
1088
- }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t(`sketchRun_${state}`) }),
1089
- drawing ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
1114
+ title: t("sketchLayerAdd"),
1115
+ "aria-label": t("sketchLayerAdd"),
1116
+ disabled: disabled || document.layers.length >= 8,
1117
+ onClick: () => change("add"),
1118
+ children: "+"
1119
+ })] }),
1120
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1121
+ className: "codexLayerList",
1122
+ children: document.layers.slice().reverse().map((layer) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1123
+ className: "codexLayerRow",
1124
+ "data-active": layer.id === document.active,
1125
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1126
+ type: "button",
1127
+ disabled,
1128
+ "aria-label": `${t("sketchLayerVisible")} ${layer.id}`,
1129
+ "aria-pressed": layer.visible,
1130
+ onClick: () => change("visible", layer.id),
1131
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(WorkspaceIcon, {
1132
+ name: layer.visible ? "eye" : "eyeOff",
1133
+ size: 18
1134
+ })
1135
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1136
+ type: "button",
1137
+ disabled,
1138
+ "aria-pressed": layer.id === document.active,
1139
+ onClick: () => change("select", layer.id),
1140
+ children: layer.name || `${t("sketchLayer")} ${layer.id}`
1141
+ })]
1142
+ }, layer.id))
1143
+ }),
1144
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("label", {
1145
+ className: "codexSketchLayerLabel",
1146
+ children: t("sketchLayerName")
1147
+ }),
1148
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
1149
+ disabled,
1150
+ "aria-label": t("sketchLayerName"),
1151
+ defaultValue: current.name,
1152
+ placeholder: `${t("sketchLayer")} ${current.id}`,
1153
+ maxLength: 40,
1154
+ onBlur: (e) => {
1155
+ if (e.target.value !== current.name) change("rename", current.id, e.target.value);
1156
+ },
1157
+ onKeyDown: (e) => {
1158
+ if (e.key === "Enter") e.currentTarget.blur();
1159
+ }
1160
+ }, current.id + "-" + current.name),
1161
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1162
+ className: "codexLayerActions",
1163
+ children: [
1164
+ "duplicate",
1165
+ "up",
1166
+ "down",
1167
+ "delete"
1168
+ ].map((action) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
1169
+ type: "button",
1170
+ title: t(`sketchLayer_${action}`),
1171
+ "aria-label": t(`sketchLayer_${action}`),
1172
+ disabled: disabled || action === "delete" && document.layers.length === 1 || action === "duplicate" && (document.layers.length >= 8 || strokeCount(document) + current.strokes.length > 2e3) || action === "up" && current === document.layers.at(-1) || action === "down" && current === document.layers[0],
1173
+ onClick: () => change(action),
1174
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(WorkspaceIcon, {
1175
+ name: action === "delete" ? "clear" : action,
1176
+ size: 17
1177
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t(`sketchLayer_${action}`) })]
1178
+ }, action))
1179
+ }),
1180
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
1090
1181
  type: "button",
1091
- className: "codexSketchStop",
1092
- onClick: onStop,
1182
+ className: "codexSketchClearLayer",
1183
+ disabled: disabled || !current.strokes.length && !current.image,
1184
+ onClick: () => change("clear"),
1093
1185
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(WorkspaceIcon, {
1094
- name: "stop",
1095
- size: 12
1096
- }), t("sketchRunStop")]
1097
- }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [recover ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1098
- type: "button",
1099
- title: t("sketchRunResumeHint"),
1100
- onClick: onResume,
1101
- children: t("sketchRunResume")
1102
- }) : null, !recover ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1186
+ name: "clear",
1187
+ size: 16
1188
+ }), t("sketchClearLayer")]
1189
+ })
1190
+ ]
1191
+ });
1192
+ }
1193
+ //#endregion
1194
+ //#region src/sketch-tool-picker.jsx
1195
+ function SketchToolPicker({ t, disabled, tool, brush, chooseBrush, chooseTool, shapesOpen, setShapesOpen }) {
1196
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1197
+ className: "codexSketchPill",
1198
+ role: "toolbar",
1199
+ "aria-label": t("sketchTitle"),
1200
+ title: t("sketchShortcuts"),
1201
+ children: [
1202
+ [
1203
+ "select",
1204
+ "pen",
1205
+ "pencil",
1206
+ "marker",
1207
+ "text",
1208
+ "eraser"
1209
+ ].map((name) => {
1210
+ const drawing = [
1211
+ "pen",
1212
+ "pencil",
1213
+ "marker"
1214
+ ].includes(name);
1215
+ const label = t(drawing ? `sketchBrush_${name}` : `sketchTool_${name}`);
1216
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
1217
+ type: "button",
1218
+ "aria-label": label,
1219
+ title: drawing ? `${label} · ${t(`sketchBrushHint_${name}`)}` : label,
1220
+ "aria-pressed": drawing ? tool === "pen" && brush === name : tool === name,
1221
+ disabled,
1222
+ onClick: () => {
1223
+ if (drawing) chooseBrush(name);
1224
+ else chooseTool(name);
1225
+ },
1226
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(WorkspaceIcon, {
1227
+ name,
1228
+ size: 23
1229
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: label })]
1230
+ }, name);
1231
+ }),
1232
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
1233
+ className: "codexSketchShapeToggle",
1103
1234
  type: "button",
1104
- "aria-label": t("sketchDismissStatus"),
1105
- title: t("sketchDismissStatus"),
1106
- onClick: onDismiss,
1107
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(WorkspaceIcon, {
1108
- name: "close",
1109
- size: 14
1110
- })
1111
- }) : null] })
1235
+ "aria-label": t("sketchShapes"),
1236
+ "aria-expanded": shapesOpen,
1237
+ "aria-pressed": [
1238
+ "line",
1239
+ "arrow",
1240
+ "rectangle",
1241
+ "circle"
1242
+ ].includes(tool),
1243
+ disabled,
1244
+ onClick: () => setShapesOpen((v) => !v),
1245
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(WorkspaceIcon, {
1246
+ name: [
1247
+ "line",
1248
+ "arrow",
1249
+ "rectangle",
1250
+ "circle"
1251
+ ].includes(tool) ? tool : "rectangle",
1252
+ size: 23
1253
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t([
1254
+ "line",
1255
+ "arrow",
1256
+ "rectangle",
1257
+ "circle"
1258
+ ].includes(tool) ? `sketchTool_${tool}` : "sketchShapes") })]
1259
+ }),
1260
+ shapesOpen ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1261
+ className: "codexSketchShapeMenu",
1262
+ children: [
1263
+ "line",
1264
+ "arrow",
1265
+ "rectangle",
1266
+ "circle"
1267
+ ].map((name) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1268
+ disabled,
1269
+ type: "button",
1270
+ "aria-pressed": tool === name,
1271
+ onClick: () => {
1272
+ chooseTool(name);
1273
+ setShapesOpen(false);
1274
+ },
1275
+ children: t(`sketchTool_${name}`)
1276
+ }, name))
1277
+ }) : null
1112
1278
  ]
1113
1279
  });
1114
1280
  }
1115
1281
  //#endregion
1116
- //#region src/sketch-layer-renderer.js
1117
- const NO_IMAGES = /* @__PURE__ */ new Map();
1118
- const surface = (width, height) => {
1119
- const c = document.createElement("canvas");
1120
- c.width = width;
1121
- c.height = height;
1122
- return c;
1123
- };
1124
- function paintSketchLayers(context, doc, cache, size = doc.width ?? 1024, height = doc.height ?? size, activeLayer, images = NO_IMAGES) {
1125
- context.globalCompositeOperation = "source-over";
1126
- context.globalAlpha = 1;
1127
- context.fillStyle = "#fff";
1128
- context.fillRect(0, 0, size, height);
1129
- for (const id of cache.keys()) if (!doc.layers.some((layer) => layer.id === id)) cache.delete(id);
1130
- for (const layer of doc.layers) {
1131
- if (!layer.visible) continue;
1132
- let entry = cache.get(layer.id);
1133
- if (!entry || entry.surface.width !== size || entry.surface.height !== height) {
1134
- entry = {
1135
- surface: surface(size, height),
1136
- base: surface(size, height)
1137
- };
1138
- cache.set(layer.id, entry);
1139
- }
1140
- const moving = layer.id === activeLayer;
1141
- const count = Math.max(0, layer.strokes.length - (moving ? 1 : 0));
1142
- const prefix = layer.strokes[count - 1];
1143
- if (entry.count !== count || entry.prefix !== prefix || entry.image !== layer.image || entry.strokes !== layer.strokes) {
1144
- const ctx = entry.base.getContext("2d");
1145
- const append = entry.count !== void 0 && count >= entry.count && entry.image === layer.image && (entry.strokes === layer.strokes || entry.strokes.slice(0, entry.count).every((stroke, index) => stroke === layer.strokes[index]));
1146
- if (!append) {
1147
- ctx.clearRect(0, 0, size, height);
1148
- const ref = layer.image, image = ref && images.get(ref.src);
1149
- if (image) ctx.drawImage(image, ref.x * size, ref.y * height, ref.width * size, ref.height * height);
1150
- }
1151
- paintSketch(ctx, layer.strokes, size, true, height, append ? entry.count : 0, count);
1152
- entry.count = count;
1153
- entry.prefix = prefix;
1154
- entry.image = layer.image;
1155
- entry.strokes = layer.strokes;
1156
- }
1157
- if (moving) {
1158
- const ctx = entry.surface.getContext("2d");
1159
- ctx.clearRect(0, 0, size, height);
1160
- ctx.drawImage(entry.base, 0, 0);
1161
- paintSketch(ctx, layer.strokes, size, true, height, layer.strokes.length - 1);
1162
- context.drawImage(entry.surface, 0, 0);
1163
- } else context.drawImage(entry.base, 0, 0);
1164
- }
1282
+ //#region src/sketch-objects.js
1283
+ const objectId = (stroke, index) => stroke.id ?? `legacy-${index}`;
1284
+ const identifyObjects = (doc) => ({
1285
+ ...doc,
1286
+ layers: doc.layers.map((layer) => ({
1287
+ ...layer,
1288
+ strokes: layer.strokes.map((s, i) => s.id ? s : {
1289
+ ...s,
1290
+ id: objectId(s, i)
1291
+ })
1292
+ }))
1293
+ });
1294
+ function objectBounds(stroke) {
1295
+ const xs = stroke.points.map((p) => p.x), ys = stroke.points.map((p) => p.y);
1296
+ return {
1297
+ x: Math.min(...xs),
1298
+ y: Math.min(...ys),
1299
+ width: Math.max(...xs) - Math.min(...xs),
1300
+ height: Math.max(...ys) - Math.min(...ys)
1301
+ };
1302
+ }
1303
+ function transformObject(stroke, { dx = 0, dy = 0, scaleX = 1, scaleY = 1 }) {
1304
+ if (![
1305
+ dx,
1306
+ dy,
1307
+ scaleX,
1308
+ scaleY
1309
+ ].every(Number.isFinite) || scaleX <= 0 || scaleY <= 0) throw Error("Invalid object transform");
1310
+ const box = objectBounds(stroke);
1311
+ const points = stroke.points.map((p) => ({
1312
+ x: box.x + (p.x - box.x) * scaleX + dx,
1313
+ y: box.y + (p.y - box.y) * scaleY + dy
1314
+ }));
1315
+ if (points.some((p) => p.x < 0 || p.x > 1 || p.y < 0 || p.y > 1)) throw Error("Object would leave the canvas");
1316
+ return {
1317
+ ...stroke,
1318
+ points
1319
+ };
1320
+ }
1321
+ function sketchObjectSummary(doc) {
1322
+ return doc.layers.flatMap((layer) => layer.strokes.map((stroke, i) => ({
1323
+ layer: layer.id,
1324
+ id: objectId(stroke, i),
1325
+ shape: stroke.shape,
1326
+ color: stroke.color,
1327
+ bounds: objectBounds(stroke),
1328
+ ...stroke.text ? { text: stroke.text } : {}
1329
+ })));
1165
1330
  }
1166
1331
  //#endregion
1167
1332
  //#region src/sketch-input.js
@@ -1193,39 +1358,192 @@ window.__ModuleLoader__.load({
1193
1358
  });
1194
1359
  }
1195
1360
  //#endregion
1361
+ //#region src/sketch-gesture.js
1362
+ function updateSketchGesture(doc, gesture, samples, rect, width, shiftKey = false) {
1363
+ if (!samples.length) return doc;
1364
+ if (gesture.object) {
1365
+ const point = sketchPoint(samples.at(-1).clientX, samples.at(-1).clientY, rect);
1366
+ if (!point) return doc;
1367
+ const box = objectBounds(gesture.object);
1368
+ const stroke = gesture.handle === "end" ? {
1369
+ ...gesture.object,
1370
+ points: [gesture.object.points[0], point]
1371
+ } : gesture.handle === "size" ? transformObject(gesture.object, {
1372
+ scaleX: Math.max(.001, point.x - box.x) / Math.max(.001, box.width),
1373
+ scaleY: Math.max(.001, point.y - box.y) / Math.max(.001, box.height)
1374
+ }) : transformObject(gesture.object, {
1375
+ dx: point.x - gesture.start.x,
1376
+ dy: point.y - gesture.start.y
1377
+ });
1378
+ doc = {
1379
+ ...doc,
1380
+ layers: doc.layers.map((l) => l.id === gesture.layer ? {
1381
+ ...l,
1382
+ strokes: l.strokes.map((s) => s.id === gesture.object.id ? stroke : s)
1383
+ } : l)
1384
+ };
1385
+ return doc;
1386
+ }
1387
+ for (const sample of samples) {
1388
+ let point = sketchPoint(sample.clientX, sample.clientY, rect);
1389
+ if (!point) continue;
1390
+ const layer = doc.layers.find((layer) => layer.id === gesture.layer);
1391
+ if (!layer) return doc;
1392
+ if (gesture.eraseStroke) {
1393
+ const previous = gesture.last ?? point;
1394
+ const steps = Math.min(256, Math.max(1, Math.ceil(Math.hypot(point.x - previous.x, point.y - previous.y) * SKETCH_SIZE / Math.max(2, width / 2))));
1395
+ layer.strokes = layer.strokes.filter((stroke) => {
1396
+ for (let i = 1; i <= steps; i++) if (strokeHit(stroke, {
1397
+ x: previous.x + (point.x - previous.x) * i / steps,
1398
+ y: previous.y + (point.y - previous.y) * i / steps
1399
+ }, width / 2, doc.width, doc.height)) return false;
1400
+ return true;
1401
+ });
1402
+ } else {
1403
+ const stroke = layer.strokes.at(-1);
1404
+ if (!stroke) return doc;
1405
+ if ([
1406
+ "line",
1407
+ "arrow",
1408
+ "rectangle",
1409
+ "circle"
1410
+ ].includes(stroke.shape)) {
1411
+ if (stroke.shape === "line" && shiftKey) {
1412
+ const w = doc.width ?? 1024, h = doc.height ?? 1024, a = stroke.points[0];
1413
+ const snapped = snapLine({
1414
+ x: a.x * w,
1415
+ y: a.y * h
1416
+ }, {
1417
+ x: point.x * w,
1418
+ y: point.y * h
1419
+ });
1420
+ point = {
1421
+ x: snapped.x / w,
1422
+ y: snapped.y / h
1423
+ };
1424
+ }
1425
+ stroke.points = [stroke.points[0], point];
1426
+ } else {
1427
+ const last = stroke.points.at(-1);
1428
+ if (Math.hypot(last.x - point.x, last.y - point.y) < 1e-4) continue;
1429
+ if (stroke.points.length >= 2e3) stroke.points = stroke.points.filter((_, i) => i % 2 === 0);
1430
+ stroke.points.push(point);
1431
+ }
1432
+ }
1433
+ gesture.last = point;
1434
+ }
1435
+ return doc;
1436
+ }
1437
+ //#endregion
1196
1438
  //#region src/sketch-drafts.js
1197
1439
  const DATABASE = "dsh-codex-sketches-v1";
1198
- async function sketchDrafts(action, value) {
1440
+ const MAX_STORAGE = 32 * 1024 * 1024;
1441
+ const metadata = (kind, row) => ({
1442
+ key: `${kind}:${row.id}`,
1443
+ kind,
1444
+ id: row.id,
1445
+ name: row.name,
1446
+ updated: row.updated,
1447
+ size: JSON.stringify(row).length
1448
+ });
1449
+ async function sketchDrafts(action, value, recoverySession) {
1199
1450
  const db = await new Promise((resolve, reject) => {
1200
- const request = indexedDB.open(DATABASE, 1);
1201
- request.onupgradeneeded = () => request.result.createObjectStore("drafts", { keyPath: "id" });
1202
- request.onsuccess = () => resolve(request.result);
1451
+ let blocked = false;
1452
+ const request = indexedDB.open(DATABASE, 2);
1453
+ request.onblocked = () => {
1454
+ blocked = true;
1455
+ reject(Object.assign(Error("Close other sketch windows and retry"), { code: "SKETCH_STORAGE_BLOCKED" }));
1456
+ };
1457
+ request.onupgradeneeded = () => {
1458
+ if (blocked) {
1459
+ request.transaction.abort();
1460
+ return;
1461
+ }
1462
+ const db = request.result, tx = request.transaction;
1463
+ if (!db.objectStoreNames.contains("drafts")) db.createObjectStore("drafts", { keyPath: "id" });
1464
+ const meta = db.createObjectStore("metadata", { keyPath: "key" });
1465
+ db.createObjectStore("recovery", { keyPath: "id" });
1466
+ const cursor = tx.objectStore("drafts").openCursor();
1467
+ cursor.onsuccess = () => {
1468
+ const row = cursor.result;
1469
+ if (row) {
1470
+ meta.put(metadata("drafts", row.value));
1471
+ row.continue();
1472
+ }
1473
+ };
1474
+ };
1475
+ request.onsuccess = () => {
1476
+ if (blocked) {
1477
+ request.result.close();
1478
+ return;
1479
+ }
1480
+ request.result.onversionchange = () => request.result.close();
1481
+ resolve(request.result);
1482
+ };
1203
1483
  request.onerror = () => reject(request.error);
1204
1484
  });
1205
1485
  try {
1206
1486
  return await new Promise((resolve, reject) => {
1207
- const tx = db.transaction("drafts", action === "list" ? "readonly" : "readwrite"), store = tx.objectStore("drafts");
1208
- let result;
1487
+ const write = [
1488
+ "save",
1489
+ "delete",
1490
+ "checkpoint",
1491
+ "clearRecovery"
1492
+ ].includes(action);
1493
+ const tx = db.transaction([
1494
+ "drafts",
1495
+ "metadata",
1496
+ "recovery"
1497
+ ], write ? "readwrite" : "readonly");
1498
+ const meta = tx.objectStore("metadata"), kind = [
1499
+ "checkpoint",
1500
+ "recover",
1501
+ "clearRecovery"
1502
+ ].includes(action) ? "recovery" : "drafts", store = tx.objectStore(kind);
1503
+ let result, failure;
1209
1504
  tx.oncomplete = () => resolve(result);
1210
1505
  tx.onerror = () => reject(tx.error);
1211
- tx.onabort = () => reject(tx.error ?? Error("Draft limit reached"));
1212
- const request = store.getAll();
1506
+ tx.onabort = () => reject(failure ?? tx.error ?? Error("Draft transaction aborted"));
1507
+ if (action === "get" || action === "recover") {
1508
+ const req = store.get(value);
1509
+ req.onsuccess = () => {
1510
+ result = req.result;
1511
+ };
1512
+ return;
1513
+ }
1514
+ if (action === "delete" || action === "clearRecovery") {
1515
+ store.delete(value);
1516
+ meta.delete(`${kind}:${value}`);
1517
+ return;
1518
+ }
1519
+ if (![
1520
+ "list",
1521
+ "save",
1522
+ "checkpoint"
1523
+ ].includes(action)) {
1524
+ tx.abort();
1525
+ return;
1526
+ }
1527
+ const request = meta.getAll();
1213
1528
  request.onsuccess = () => {
1214
1529
  const rows = request.result;
1215
1530
  if (action === "list") {
1216
- result = rows.sort((a, b) => b.updated - a.updated);
1217
- return;
1218
- }
1219
- if (action === "delete") {
1220
- store.delete(value);
1531
+ result = rows.filter((row) => row.kind === "drafts").sort((a, b) => b.updated - a.updated);
1221
1532
  return;
1222
1533
  }
1223
- const others = rows.filter((row) => row.id !== value.id);
1224
- if (others.length >= 20 || JSON.stringify([...others, value]).length > 32 * 1024 * 1024) {
1534
+ const next = metadata(kind, value), others = rows.filter((row) => row.key !== next.key && !(action === "save" && recoverySession && row.key === `recovery:${recoverySession}`));
1535
+ const code = others.filter((row) => row.kind === kind).length >= 20 ? "SKETCH_DRAFT_LIMIT" : others.reduce((n, row) => n + row.size, 0) + next.size > MAX_STORAGE ? "SKETCH_STORAGE_LIMIT" : null;
1536
+ if (code) {
1537
+ failure = Object.assign(Error("Draft storage limit reached"), { code });
1225
1538
  tx.abort();
1226
1539
  return;
1227
1540
  }
1228
1541
  store.put(value);
1542
+ meta.put(next);
1543
+ if (action === "save" && recoverySession) {
1544
+ tx.objectStore("recovery").delete(recoverySession);
1545
+ meta.delete(`recovery:${recoverySession}`);
1546
+ }
1229
1547
  result = value;
1230
1548
  };
1231
1549
  });
@@ -1267,397 +1585,6 @@ window.__ModuleLoader__.load({
1267
1585
  bitmap.close();
1268
1586
  }
1269
1587
  }
1270
- //#endregion
1271
- //#region src/sketch-interactions.js
1272
- function useSketchDismiss(open, close, host, selectors) {
1273
- const latest = (0, react.useRef)(close);
1274
- latest.current = close;
1275
- (0, react.useEffect)(() => {
1276
- if (!open) return;
1277
- const dialog = host.current?.closest("dialog") ?? host.current;
1278
- if (!dialog) return;
1279
- const pointer = (event) => {
1280
- if (selectors.some((selector) => event.target.closest?.(selector))) return;
1281
- latest.current(false);
1282
- if (event.target.matches?.("canvas")) {
1283
- event.preventDefault();
1284
- event.stopPropagation();
1285
- event.target.focus({ preventScroll: true });
1286
- }
1287
- };
1288
- const key = (event) => {
1289
- if (event.key !== "Escape") return;
1290
- event.preventDefault();
1291
- event.stopPropagation();
1292
- latest.current(false);
1293
- dialog.querySelector("canvas")?.focus({ preventScroll: true });
1294
- };
1295
- const hidden = () => latest.current(false);
1296
- document.addEventListener("pointerdown", pointer, true);
1297
- document.addEventListener("keydown", key, true);
1298
- dialog.addEventListener("close", hidden);
1299
- return () => {
1300
- document.removeEventListener("pointerdown", pointer, true);
1301
- document.removeEventListener("keydown", key, true);
1302
- dialog.removeEventListener("close", hidden);
1303
- };
1304
- }, [
1305
- open,
1306
- host,
1307
- selectors.join("|")
1308
- ]);
1309
- }
1310
- function useSketchCursor(canvas, ring, width, brush, zoom, hidden) {
1311
- const last = (0, react.useRef)(null), heldPressure = (0, react.useRef)(1);
1312
- const update = (event, bounds) => {
1313
- if (event) last.current = event;
1314
- const pointer = last.current, node = canvas.current, cursor = ring.current;
1315
- if (!pointer || !node || !cursor) return;
1316
- const rect = bounds ?? node.getBoundingClientRect();
1317
- if (hidden || pointer.pointerType === "touch" || pointer.clientX < rect.left || pointer.clientX > rect.right || pointer.clientY < rect.top || pointer.clientY > rect.bottom) {
1318
- cursor.hidden = true;
1319
- return;
1320
- }
1321
- const diameter = width * (node.hasPointerCapture(pointer.pointerId) && pointer.pointerType === "pen" ? heldPressure.current : 1) * rect.width / node.width;
1322
- cursor.hidden = false;
1323
- cursor.style.width = `${diameter}px`;
1324
- cursor.style.height = `${diameter}px`;
1325
- cursor.style.transform = `translate(${pointer.clientX - diameter / 2}px,${pointer.clientY - diameter / 2}px)`;
1326
- };
1327
- (0, react.useEffect)(() => {
1328
- update();
1329
- const observer = new ResizeObserver(() => update());
1330
- if (canvas.current) observer.observe(canvas.current);
1331
- return () => observer.disconnect();
1332
- }, [
1333
- width,
1334
- brush,
1335
- zoom,
1336
- hidden
1337
- ]);
1338
- return {
1339
- down: (event) => {
1340
- heldPressure.current = event.pointerType === "pen" ? Math.max(.2, event.pressure) : 1;
1341
- },
1342
- move: (event, bounds) => update({
1343
- clientX: event.clientX,
1344
- clientY: event.clientY,
1345
- pointerId: event.pointerId,
1346
- pointerType: event.pointerType,
1347
- pressure: event.pressure
1348
- }, bounds),
1349
- leave: () => {
1350
- last.current = null;
1351
- if (ring.current) ring.current.hidden = true;
1352
- }
1353
- };
1354
- }
1355
- //#endregion
1356
- //#region src/sketch-view.jsx
1357
- const DEFAULT_KEYS = {
1358
- pen: "b",
1359
- eraser: "e",
1360
- line: "l",
1361
- rectangle: "r",
1362
- circle: "o",
1363
- pan: " ",
1364
- zoomIn: "=",
1365
- zoomOut: "-",
1366
- fit: "0"
1367
- };
1368
- function useSketchView(canvas, open) {
1369
- const [view, setView] = (0, react.useState)({
1370
- scale: 1,
1371
- x: 0,
1372
- y: 0
1373
- }), [keys, setKeys] = (0, react.useState)(() => {
1374
- try {
1375
- return {
1376
- ...DEFAULT_KEYS,
1377
- ...JSON.parse(localStorage.getItem("codex-sketch-keys"))
1378
- };
1379
- } catch {
1380
- return DEFAULT_KEYS;
1381
- }
1382
- });
1383
- const [shortcuts, setShortcuts] = (0, react.useState)(() => {
1384
- try {
1385
- return localStorage.getItem("codex-sketch-shortcuts") !== "off";
1386
- } catch {
1387
- return true;
1388
- }
1389
- }), [space, setSpace] = (0, react.useState)(false);
1390
- const drag = (0, react.useRef)(null), viewRef = (0, react.useRef)(view);
1391
- viewRef.current = view;
1392
- const zoom = (factor) => setView((v) => ({
1393
- ...v,
1394
- scale: Math.max(.25, Math.min(8, v.scale * factor))
1395
- }));
1396
- const reset = () => setView({
1397
- scale: 1,
1398
- x: 0,
1399
- y: 0
1400
- });
1401
- (0, react.useEffect)(() => {
1402
- const node = canvas.current;
1403
- if (!node || !open) return;
1404
- const wheel = (e) => {
1405
- if (!shortcuts || !e.altKey) return;
1406
- e.preventDefault();
1407
- zoom(e.deltaY < 0 ? 1.1 : 1 / 1.1);
1408
- };
1409
- node.addEventListener("wheel", wheel, { passive: false });
1410
- return () => node.removeEventListener("wheel", wheel);
1411
- }, [open, shortcuts]);
1412
- (0, react.useEffect)(() => {
1413
- const stop = () => {
1414
- drag.current = null;
1415
- setSpace(false);
1416
- };
1417
- window.addEventListener("blur", stop);
1418
- return () => window.removeEventListener("blur", stop);
1419
- }, []);
1420
- (0, react.useEffect)(() => {
1421
- if (!open || !shortcuts) {
1422
- setSpace(false);
1423
- drag.current = null;
1424
- }
1425
- }, [open, shortcuts]);
1426
- const setKey = (action, key) => {
1427
- key = key.toLowerCase();
1428
- if (!key || Object.entries(keys).some(([a, k]) => a !== action && k === key) || ["[", "]"].includes(key)) return;
1429
- const next = {
1430
- ...keys,
1431
- [action]: key
1432
- };
1433
- setKeys(next);
1434
- try {
1435
- localStorage.setItem("codex-sketch-keys", JSON.stringify(next));
1436
- } catch {}
1437
- };
1438
- return {
1439
- view,
1440
- keys,
1441
- shortcuts,
1442
- space,
1443
- zoom,
1444
- reset,
1445
- setKey,
1446
- toggle: () => setShortcuts((v) => {
1447
- try {
1448
- localStorage.setItem("codex-sketch-shortcuts", v ? "off" : "on");
1449
- } catch {}
1450
- return !v;
1451
- }),
1452
- keyDown: (e) => {
1453
- if (!shortcuts || e.ctrlKey || e.metaKey || e.altKey) return false;
1454
- const key = e.key.toLowerCase();
1455
- if (key === keys.pan && !e.ctrlKey && !e.metaKey) {
1456
- e.preventDefault();
1457
- setSpace(true);
1458
- return true;
1459
- }
1460
- if (key === keys.zoomIn || key === "+" || key === keys.zoomOut || key === keys.fit) {
1461
- e.preventDefault();
1462
- if (key === keys.fit) reset();
1463
- else zoom(key === keys.zoomOut ? 1 / 1.2 : 1.2);
1464
- return true;
1465
- }
1466
- return false;
1467
- },
1468
- keyUp: (e) => {
1469
- if (e.key.toLowerCase() === keys.pan) setSpace(false);
1470
- },
1471
- down: (e) => {
1472
- if (e.button !== 1 && !space) return false;
1473
- e.preventDefault();
1474
- drag.current = {
1475
- id: e.pointerId,
1476
- x: e.clientX,
1477
- y: e.clientY,
1478
- view: viewRef.current
1479
- };
1480
- canvas.current.setPointerCapture(e.pointerId);
1481
- return true;
1482
- },
1483
- move: (e) => {
1484
- const d = drag.current;
1485
- if (!d || d.id !== e.pointerId) return false;
1486
- setView({
1487
- ...d.view,
1488
- x: d.view.x + e.clientX - d.x,
1489
- y: d.view.y + e.clientY - d.y
1490
- });
1491
- return true;
1492
- },
1493
- end: (e) => {
1494
- if (drag.current?.id !== e.pointerId) return false;
1495
- drag.current = null;
1496
- if (canvas.current.hasPointerCapture(e.pointerId)) canvas.current.releasePointerCapture(e.pointerId);
1497
- return true;
1498
- }
1499
- };
1500
- }
1501
- function SketchViewControls({ navigation, t }) {
1502
- const [open, setOpen] = (0, react.useState)(false), [placement, setPlacement] = (0, react.useState)(null);
1503
- const host = (0, react.useRef)(null), trigger = (0, react.useRef)(null);
1504
- const close = () => {
1505
- setOpen(false);
1506
- trigger.current?.focus({ preventScroll: true });
1507
- };
1508
- useSketchDismiss(open, setOpen, host, [".codexSketchViewControls", ".codexSketchKeyPanel"]);
1509
- (0, react.useLayoutEffect)(() => {
1510
- if (!open) return;
1511
- const dialog = host.current.closest("dialog");
1512
- const place = () => {
1513
- const box = dialog.getBoundingClientRect(), anchor = trigger.current.getBoundingClientRect();
1514
- const width = Math.min(360, box.width - 24);
1515
- setPlacement({
1516
- dialog,
1517
- style: {
1518
- width,
1519
- left: Math.max(12, Math.min(anchor.left - box.left, box.width - width - 12)),
1520
- bottom: box.bottom - anchor.top + 8,
1521
- maxHeight: Math.max(80, anchor.top - box.top - 24)
1522
- }
1523
- });
1524
- };
1525
- place();
1526
- const observer = new ResizeObserver(place);
1527
- observer.observe(dialog);
1528
- observer.observe(host.current);
1529
- window.addEventListener("resize", place);
1530
- return () => {
1531
- observer.disconnect();
1532
- window.removeEventListener("resize", place);
1533
- };
1534
- }, [open]);
1535
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1536
- ref: host,
1537
- className: "codexSketchViewControls",
1538
- children: [
1539
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1540
- type: "button",
1541
- "aria-label": t("sketchZoomOut"),
1542
- onClick: () => navigation.zoom(1 / 1.2),
1543
- children: "−"
1544
- }),
1545
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
1546
- type: "button",
1547
- title: t("sketchFit"),
1548
- onClick: navigation.reset,
1549
- children: [Math.round(navigation.view.scale * 100), "%"]
1550
- }),
1551
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1552
- type: "button",
1553
- "aria-label": t("sketchZoomIn"),
1554
- onClick: () => navigation.zoom(1.2),
1555
- children: "+"
1556
- }),
1557
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1558
- ref: trigger,
1559
- type: "button",
1560
- "aria-expanded": open,
1561
- onClick: () => setOpen(!open),
1562
- children: t("sketchKeys")
1563
- }),
1564
- open && placement ? (0, react_dom.createPortal)(/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
1565
- className: "codexSketchKeyPanel",
1566
- "aria-label": t("sketchKeys"),
1567
- style: placement.style,
1568
- onKeyDown: (e) => {
1569
- if (e.key === "Escape") {
1570
- e.preventDefault();
1571
- e.stopPropagation();
1572
- close();
1573
- }
1574
- },
1575
- children: [
1576
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("header", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("strong", { children: t("sketchKeys") }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1577
- type: "button",
1578
- "aria-label": t("sketchFileClose"),
1579
- onClick: close,
1580
- children: "×"
1581
- })] }),
1582
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
1583
- className: "codexSketchKeysEnabled",
1584
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("sketchKeysEnabled") }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
1585
- type: "checkbox",
1586
- checked: navigation.shortcuts,
1587
- onChange: navigation.toggle
1588
- })]
1589
- }),
1590
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", { children: t("sketchNavigationHint") }),
1591
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1592
- className: "codexSketchKeyGrid",
1593
- children: Object.entries(navigation.keys).map(([action, key]) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", { children: [t(`sketchKey_${action}`), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
1594
- "aria-label": t(`sketchKey_${action}`),
1595
- value: key === " " ? "Space" : key,
1596
- readOnly: true,
1597
- onKeyDown: (e) => {
1598
- if (e.key === "Tab" || e.key === "Escape") return;
1599
- e.preventDefault();
1600
- e.stopPropagation();
1601
- if (e.key.length === 1 && !e.ctrlKey && !e.metaKey && !e.altKey) navigation.setKey(action, e.key);
1602
- }
1603
- })] }, action))
1604
- }),
1605
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("small", { children: t("sketchKeyHint") })
1606
- ]
1607
- }), placement.dialog) : null
1608
- ]
1609
- });
1610
- }
1611
- //#endregion
1612
- //#region src/sketch-objects.js
1613
- const objectId = (stroke, index) => stroke.id ?? `legacy-${index}`;
1614
- const identifyObjects = (doc) => ({
1615
- ...doc,
1616
- layers: doc.layers.map((layer) => ({
1617
- ...layer,
1618
- strokes: layer.strokes.map((s, i) => s.id ? s : {
1619
- ...s,
1620
- id: objectId(s, i)
1621
- })
1622
- }))
1623
- });
1624
- function objectBounds(stroke) {
1625
- const xs = stroke.points.map((p) => p.x), ys = stroke.points.map((p) => p.y);
1626
- return {
1627
- x: Math.min(...xs),
1628
- y: Math.min(...ys),
1629
- width: Math.max(...xs) - Math.min(...xs),
1630
- height: Math.max(...ys) - Math.min(...ys)
1631
- };
1632
- }
1633
- function transformObject(stroke, { dx = 0, dy = 0, scaleX = 1, scaleY = 1 }) {
1634
- if (![
1635
- dx,
1636
- dy,
1637
- scaleX,
1638
- scaleY
1639
- ].every(Number.isFinite) || scaleX <= 0 || scaleY <= 0) throw Error("Invalid object transform");
1640
- const box = objectBounds(stroke);
1641
- const points = stroke.points.map((p) => ({
1642
- x: box.x + (p.x - box.x) * scaleX + dx,
1643
- y: box.y + (p.y - box.y) * scaleY + dy
1644
- }));
1645
- if (points.some((p) => p.x < 0 || p.x > 1 || p.y < 0 || p.y > 1)) throw Error("Object would leave the canvas");
1646
- return {
1647
- ...stroke,
1648
- points
1649
- };
1650
- }
1651
- function sketchObjectSummary(doc) {
1652
- return doc.layers.flatMap((layer) => layer.strokes.map((stroke, i) => ({
1653
- layer: layer.id,
1654
- id: objectId(stroke, i),
1655
- shape: stroke.shape,
1656
- color: stroke.color,
1657
- bounds: objectBounds(stroke),
1658
- ...stroke.text ? { text: stroke.text } : {}
1659
- })));
1660
- }
1661
1588
  const SKETCH_COMMAND_HELP = {
1662
1589
  coordinates: "Assign short meaningful stroke id values for later edits. Text uses two opposite box corners and text content; width is font size in pixels, automatic fitting within the box. Arrow uses two endpoints. Normalized x/y in [0,1]; width is canvas pixels. Read documentId and revision before editing.",
1663
1590
  shapes: "line: exactly two endpoints; rectangle/circle (ellipse alias accepted): exactly two opposite bounding-box corners (circle draws an ellipse within that box); polygon: three or more vertices, closed automatically; pen: ordered path points. bezier: start point, then groups of control1/control2/end; use 4 points for one cubic curve, max 64 segments. Prefer bezier for smooth designed curves instead of many pen samples. fill:true closes and fills the curve. fill:true fills rectangle/circle/polygon. Layers and strokes paint in list order, later ones on top. All commands needed for drawing are described here; no source-code search is required.",
@@ -1784,331 +1711,992 @@ window.__ModuleLoader__.load({
1784
1711
  s?.end
1785
1712
  ])];
1786
1713
  }
1787
- if (![
1788
- "pen",
1789
- "line",
1790
- "rectangle",
1791
- "circle",
1792
- "polygon",
1793
- "bezier",
1794
- "arrow",
1795
- "text",
1796
- "eraser"
1797
- ].includes(shape) || !/^#[0-9a-f]{6}$/i.test(color ?? "") || !finite(width, 1, 256) || !finite(opacity, 0, 1) || typeof fill !== "boolean" || !Array.isArray(points) || !points.length || points.length > 2e3 || points.some((p) => !p || !finite(p.x, 0, 1) || !finite(p.y, 0, 1))) throw Error("Invalid stroke");
1798
- if ([
1799
- "line",
1800
- "arrow",
1801
- "text",
1802
- "rectangle",
1803
- "circle"
1804
- ].includes(shape) && points.length !== 2 || shape === "polygon" && points.length < 3) throw Error("Invalid shape points");
1805
- if (shape === "bezier" && (points.length < 4 || points.length > 193 || (points.length - 1) % 3 !== 0)) throw Error(`commands[${commands.indexOf(command)}]: Bezier has ${points.length} points; expected 4, 7, 10, ... 193 (start + control1/control2/end per segment). No commands in this batch were applied.`);
1806
- if (fill && ![
1807
- "rectangle",
1808
- "circle",
1809
- "polygon",
1810
- "bezier"
1811
- ].includes(shape)) throw Error("Fill requires a closed shape");
1812
- const layer = doc.layers.find((layer) => layer.id === (command.layer ?? doc.active));
1813
- if (!layer?.visible) throw Error("Target layer is missing or hidden");
1814
- if (shape === "text" && (typeof command.text !== "string" || !command.text.trim() || command.text.length > 500 || points[0].x === points[1].x || points[0].y === points[1].y)) throw Error("Text requires 1–500 characters and a non-empty bounding box");
1815
- const id = command.id ?? crypto.randomUUID();
1816
- if (typeof id !== "string" || !id.length || id.length > 100 || layer.strokes.some((s) => s.id === id)) throw Error("Invalid or duplicate object id");
1817
- const stroke = {
1818
- id,
1819
- ...shape === "text" ? { text: command.text } : {},
1820
- shape,
1821
- color,
1822
- width,
1823
- opacity,
1824
- fill,
1825
- brush: "pen",
1826
- points: points.map((p) => ({
1827
- x: p.x,
1828
- y: p.y
1829
- }))
1830
- };
1831
- doc = {
1832
- ...doc,
1833
- layers: doc.layers.map((item) => item === layer ? {
1834
- ...item,
1835
- strokes: [...item.strokes, stroke]
1836
- } : item)
1714
+ if (![
1715
+ "pen",
1716
+ "line",
1717
+ "rectangle",
1718
+ "circle",
1719
+ "polygon",
1720
+ "bezier",
1721
+ "arrow",
1722
+ "text",
1723
+ "eraser"
1724
+ ].includes(shape) || !/^#[0-9a-f]{6}$/i.test(color ?? "") || !finite(width, 1, 256) || !finite(opacity, 0, 1) || typeof fill !== "boolean" || !Array.isArray(points) || !points.length || points.length > 2e3 || points.some((p) => !p || !finite(p.x, 0, 1) || !finite(p.y, 0, 1))) throw Error("Invalid stroke");
1725
+ if ([
1726
+ "line",
1727
+ "arrow",
1728
+ "text",
1729
+ "rectangle",
1730
+ "circle"
1731
+ ].includes(shape) && points.length !== 2 || shape === "polygon" && points.length < 3) throw Error("Invalid shape points");
1732
+ if (shape === "bezier" && (points.length < 4 || points.length > 193 || (points.length - 1) % 3 !== 0)) throw Error(`commands[${commands.indexOf(command)}]: Bezier has ${points.length} points; expected 4, 7, 10, ... 193 (start + control1/control2/end per segment). No commands in this batch were applied.`);
1733
+ if (fill && ![
1734
+ "rectangle",
1735
+ "circle",
1736
+ "polygon",
1737
+ "bezier"
1738
+ ].includes(shape)) throw Error("Fill requires a closed shape");
1739
+ const layer = doc.layers.find((layer) => layer.id === (command.layer ?? doc.active));
1740
+ if (!layer?.visible) throw Error("Target layer is missing or hidden");
1741
+ if (shape === "text" && (typeof command.text !== "string" || !command.text.trim() || command.text.length > 500 || points[0].x === points[1].x || points[0].y === points[1].y)) throw Error("Text requires 1–500 characters and a non-empty bounding box");
1742
+ const id = command.id ?? crypto.randomUUID();
1743
+ if (typeof id !== "string" || !id.length || id.length > 100 || layer.strokes.some((s) => s.id === id)) throw Error("Invalid or duplicate object id");
1744
+ const stroke = {
1745
+ id,
1746
+ ...shape === "text" ? { text: command.text } : {},
1747
+ shape,
1748
+ color,
1749
+ width,
1750
+ opacity,
1751
+ fill,
1752
+ brush: "pen",
1753
+ points: points.map((p) => ({
1754
+ x: p.x,
1755
+ y: p.y
1756
+ }))
1757
+ };
1758
+ doc = {
1759
+ ...doc,
1760
+ layers: doc.layers.map((item) => item === layer ? {
1761
+ ...item,
1762
+ strokes: [...item.strokes, stroke]
1763
+ } : item)
1764
+ };
1765
+ }
1766
+ if (strokeCount(doc) > 2e3 || doc.layers.reduce((n, l) => n + l.strokes.reduce((m, s) => m + s.points.length, 0), 0) > 2e5) throw Error("Sketch resource budget exceeded");
1767
+ return doc;
1768
+ }
1769
+ function createSketchCommandSession(adapter) {
1770
+ const completed = /* @__PURE__ */ new Map();
1771
+ let pending = false, cachedCharacters = 0;
1772
+ return async (request) => {
1773
+ if (!request || typeof request !== "object") throw Error("Invalid sketch request");
1774
+ if (!adapter.available()) throw Error("Open the sketch board for this session first");
1775
+ const current = adapter.snapshot();
1776
+ if (request.action === "inspect") {
1777
+ const offset = request.offset ?? 0, objects = adapter.objects?.() ?? [];
1778
+ if (!Number.isInteger(offset) || offset < 0) throw Error("offset must be a non-negative integer");
1779
+ return {
1780
+ ...current,
1781
+ protocolVersion: 2,
1782
+ objects: objects.slice(offset, offset + 50),
1783
+ objectCount: objects.length,
1784
+ ...offset + 50 < objects.length ? { nextOffset: offset + 50 } : {},
1785
+ ...request.objectId ? { object: adapter.object?.(request.objectId, request.layer) } : {},
1786
+ recentRequests: [...completed.values()].slice(-8).map((entry) => entry.receipt),
1787
+ help: SKETCH_COMMAND_HELP
1788
+ };
1789
+ }
1790
+ if (request.documentId !== current.documentId) throw Error("Document changed; inspect again");
1791
+ if (pending || adapter.busy()) throw Error("Sketch is being edited; retry after it settles");
1792
+ if (request.action === "preview") return {
1793
+ ...current,
1794
+ png: await adapter.preview()
1795
+ };
1796
+ if (!["apply", "save"].includes(request.action)) throw Error("Unknown sketch action");
1797
+ if (typeof request.requestId !== "string" || !request.requestId.length || request.requestId.length > 100) throw Error("A unique requestId is required");
1798
+ const key = `${current.documentId}:${request.requestId}`, fingerprint = JSON.stringify({
1799
+ ...request,
1800
+ runId: void 0
1801
+ });
1802
+ const cached = completed.get(key);
1803
+ if (cached) {
1804
+ if (cached.fingerprint !== fingerprint) throw Error("requestId reused with different content");
1805
+ return cached.result;
1806
+ }
1807
+ if (request.revision !== current.revision || adapter.busy()) throw Error("Sketch changed or is being edited; inspect again");
1808
+ let changedObjects;
1809
+ if (request.action === "apply") {
1810
+ const before = adapter.document();
1811
+ let next;
1812
+ try {
1813
+ next = applySketchCommands(before, request.commands);
1814
+ } catch (cause) {
1815
+ const error = new Error(`${cause.message} Correct the batch and retry with the same runId and revision; nothing was applied.`, { cause });
1816
+ error.code = "SKETCH_INVALID_BATCH";
1817
+ throw error;
1818
+ }
1819
+ adapter.commit(next);
1820
+ const previous = new Map(before.layers.flatMap((l) => l.strokes.map((s) => [`${l.id}:${s.id}`, s])));
1821
+ changedObjects = next.layers.flatMap((l) => l.strokes.filter((s) => previous.get(`${l.id}:${s.id}`) !== s).map((s) => ({
1822
+ layer: l.id,
1823
+ id: s.id
1824
+ })));
1825
+ } else {
1826
+ if (request.name !== void 0 && (typeof request.name !== "string" || request.name.length > 60)) throw Error("Invalid draft name");
1827
+ pending = true;
1828
+ try {
1829
+ await adapter.save(request.name);
1830
+ } finally {
1831
+ pending = false;
1832
+ }
1833
+ }
1834
+ const result = {
1835
+ ...adapter.snapshot(),
1836
+ ...changedObjects ? {
1837
+ changedObjects: changedObjects.slice(0, 100),
1838
+ changedObjectCount: changedObjects.length
1839
+ } : {}
1840
+ };
1841
+ completed.set(key, {
1842
+ fingerprint,
1843
+ result,
1844
+ receipt: {
1845
+ requestId: request.requestId,
1846
+ action: request.action,
1847
+ revision: result.revision
1848
+ }
1849
+ });
1850
+ cachedCharacters += fingerprint.length;
1851
+ while (completed.size > 1 && (completed.size > 128 || cachedCharacters > 4e6)) {
1852
+ const oldest = completed.keys().next().value;
1853
+ cachedCharacters -= completed.get(oldest).fingerprint.length;
1854
+ completed.delete(oldest);
1855
+ }
1856
+ return result;
1857
+ };
1858
+ }
1859
+ //#endregion
1860
+ //#region src/sketch-formats.js
1861
+ const SKETCH_FILE_ACCEPT = ".psd,.dsh-sketch.json,image/png,image/jpeg,image/webp";
1862
+ const canvas = (w, h) => {
1863
+ const c = document.createElement("canvas");
1864
+ c.width = w;
1865
+ c.height = h;
1866
+ return c;
1867
+ };
1868
+ function runPsdCodec(action, payload) {
1869
+ return new Promise((resolve, reject) => {
1870
+ const worker = new Worker("/api/codex-subscription/sketch-psd-worker", { type: "module" });
1871
+ const finish = (callback, value) => {
1872
+ clearTimeout(timer);
1873
+ worker.terminate();
1874
+ callback(value);
1875
+ };
1876
+ const timer = setTimeout(() => finish(reject, Error("PSD operation timed out")), 3e4);
1877
+ worker.onerror = () => finish(reject, Error("PSD codec could not be loaded"));
1878
+ worker.onmessage = (event) => event.data.ok ? finish(resolve, event.data.value) : finish(reject, Error(event.data.error));
1879
+ worker.postMessage({
1880
+ action,
1881
+ payload
1882
+ });
1883
+ });
1884
+ }
1885
+ function encodeSketchDocument(doc) {
1886
+ return JSON.stringify({
1887
+ format: "dsh-sketch",
1888
+ version: 1,
1889
+ doc
1890
+ });
1891
+ }
1892
+ function decodeSketchDocument(text) {
1893
+ if (text.length > 32 * 1024 * 1024) throw Error("Draft exceeds 32 MB");
1894
+ const file = JSON.parse(text), source = file.doc;
1895
+ if (file.format !== "dsh-sketch" || file.version !== 1 || !source || !Array.isArray(source.layers) || !source.layers.length || source.layers.length > 8) throw Error("Invalid sketch file");
1896
+ const w = source.width ?? 1024, h = source.height ?? 1024;
1897
+ if (!Number.isInteger(w) || !Number.isInteger(h) || w < 1 || h < 1 || w > 2048 || h > 2048) throw Error("Invalid canvas size");
1898
+ let doc = {
1899
+ ...createSketchLayers(),
1900
+ width: w,
1901
+ height: h,
1902
+ ratio: Object.keys(SKETCH_RATIOS).find((k) => SKETCH_RATIOS[k][0] === w && SKETCH_RATIOS[k][1] === h) ?? "custom"
1903
+ };
1904
+ for (let i = 0; i < source.layers.length; i++) {
1905
+ const layer = source.layers[i];
1906
+ if (i) doc = applySketchCommands(doc, [{
1907
+ op: "layer",
1908
+ action: "add"
1909
+ }]);
1910
+ if (!Array.isArray(layer.strokes)) throw Error("Invalid strokes");
1911
+ for (let j = 0; j < layer.strokes.length; j += 256) {
1912
+ const strokes = layer.strokes.slice(j, j + 256);
1913
+ doc = applySketchCommands(doc, strokes.map((s) => ({
1914
+ ...s,
1915
+ op: "stroke",
1916
+ layer: doc.active,
1917
+ fill: s.fill ?? false
1918
+ })));
1919
+ const added = doc.layers.at(-1).strokes;
1920
+ for (let k = 0; k < strokes.length; k++) {
1921
+ const s = strokes[k];
1922
+ if (s.brush !== void 0 && ![
1923
+ "pen",
1924
+ "pencil",
1925
+ "marker"
1926
+ ].includes(s.brush)) throw Error("Invalid brush");
1927
+ if (s.pressure !== void 0 && (!Number.isFinite(s.pressure) || s.pressure < .2 || s.pressure > 1)) throw Error("Invalid pressure");
1928
+ if (s.brushVersion !== void 0 && s.brushVersion !== 2) throw Error("Unsupported brush version");
1929
+ Object.assign(added[added.length - strokes.length + k], {
1930
+ brush: s.brush ?? "pen",
1931
+ pressure: s.pressure ?? 1,
1932
+ ...s.brushVersion === 2 ? { brushVersion: 2 } : {}
1933
+ });
1934
+ }
1935
+ }
1936
+ const target = doc.layers.at(-1);
1937
+ target.name = String(layer.name ?? "").slice(0, 40);
1938
+ target.visible = layer.visible !== false;
1939
+ if (layer.image) {
1940
+ const image = layer.image;
1941
+ if (typeof image.src !== "string" || !/^data:image\/png;base64,/.test(image.src) || image.src.length > 8 * 1024 * 1024 || [
1942
+ "x",
1943
+ "y",
1944
+ "width",
1945
+ "height"
1946
+ ].some((k) => !Number.isFinite(image[k]) || image[k] < 0 || image[k] > 1)) throw Error("Invalid draft image");
1947
+ const bytes = Uint8Array.from(atob(image.src.slice(image.src.indexOf(",") + 1)), (c) => c.charCodeAt(0));
1948
+ if (bytes.length < 24) throw Error("Invalid draft image");
1949
+ const header = new DataView(bytes.buffer);
1950
+ if (header.getUint32(0) !== 2303741511 || header.getUint32(4) !== 218765834 || header.getUint32(16) < 1 || header.getUint32(20) < 1 || header.getUint32(16) > 4096 || header.getUint32(20) > 4096) throw Error("Invalid draft image size");
1951
+ target.image = {
1952
+ src: image.src,
1953
+ x: image.x,
1954
+ y: image.y,
1955
+ width: image.width,
1956
+ height: image.height
1957
+ };
1958
+ }
1959
+ }
1960
+ const activeIndex = source.layers.findIndex((layer) => layer.id === source.active);
1961
+ doc.active = doc.layers[Math.max(0, activeIndex)].id;
1962
+ return doc;
1963
+ }
1964
+ async function exportSketchPsd(doc, images, composite) {
1965
+ const width = doc.width ?? 1024, height = doc.height ?? 1024;
1966
+ const children = doc.layers.map((layer, i) => {
1967
+ const ctx = canvas(width, height).getContext("2d"), ref = layer.image;
1968
+ if (ref) ctx.drawImage(images.get(ref.src), ref.x * width, ref.y * height, ref.width * width, ref.height * height);
1969
+ paintSketch(ctx, layer.strokes, width, true, height);
1970
+ return {
1971
+ name: layer.name || `Layer ${i + 1}`,
1972
+ hidden: !layer.visible,
1973
+ opacity: 1,
1974
+ blendMode: "normal",
1975
+ imageData: ctx.getImageData(0, 0, width, height)
1837
1976
  };
1977
+ });
1978
+ const context = canvas(width, height).getContext("2d");
1979
+ context.fillStyle = "#fff";
1980
+ context.fillRect(0, 0, width, height);
1981
+ if (children[0] && !children[0].hidden) {
1982
+ const bottom = canvas(width, height);
1983
+ bottom.getContext("2d").putImageData(children[0].imageData, 0, 0);
1984
+ context.drawImage(bottom, 0, 0);
1985
+ children[0].imageData = context.getImageData(0, 0, width, height);
1986
+ } else {
1987
+ if (children.length >= 8) throw Error("Show the bottom layer before exporting this eight-layer drawing");
1988
+ children.unshift({
1989
+ name: "Paper",
1990
+ opacity: 1,
1991
+ blendMode: "normal",
1992
+ imageData: context.getImageData(0, 0, width, height)
1993
+ });
1994
+ }
1995
+ return runPsdCodec("write", {
1996
+ width,
1997
+ height,
1998
+ children,
1999
+ imageData: composite.getContext("2d").getImageData(0, 0, width, height)
2000
+ });
2001
+ }
2002
+ async function importSketchPsd(file) {
2003
+ if (file.size > 32 * 1024 * 1024) throw Error("PSD exceeds 32 MB");
2004
+ const psd = await runPsdCodec("read", await file.arrayBuffer());
2005
+ const scale = Math.min(1, 1024 / Math.max(psd.width, psd.height)), width = Math.max(1, Math.round(psd.width * scale)), height = Math.max(1, Math.round(psd.height * scale));
2006
+ const doc = {
2007
+ ...createSketchLayers(),
2008
+ width,
2009
+ height,
2010
+ ratio: Object.keys(SKETCH_RATIOS).find((k) => SKETCH_RATIOS[k][0] === width && SKETCH_RATIOS[k][1] === height) ?? "custom",
2011
+ layers: [],
2012
+ nextId: psd.layers.length + 1
2013
+ };
2014
+ for (const [i, layer] of psd.layers.entries()) {
2015
+ const src = canvas(layer.imageData.width, layer.imageData.height);
2016
+ src.getContext("2d").putImageData(new ImageData(new Uint8ClampedArray(layer.imageData.data), layer.imageData.width, layer.imageData.height), 0, 0);
2017
+ const out = canvas(width, height), ctx = out.getContext("2d");
2018
+ ctx.globalAlpha = layer.opacity;
2019
+ ctx.drawImage(src, layer.left * scale, layer.top * scale, src.width * scale, src.height * scale);
2020
+ doc.layers.push({
2021
+ id: i + 1,
2022
+ name: layer.name,
2023
+ visible: !layer.hidden,
2024
+ strokes: [],
2025
+ image: {
2026
+ src: out.toDataURL("image/png"),
2027
+ x: 0,
2028
+ y: 0,
2029
+ width: 1,
2030
+ height: 1
2031
+ }
2032
+ });
1838
2033
  }
1839
- if (strokeCount(doc) > 2e3 || doc.layers.reduce((n, l) => n + l.strokes.reduce((m, s) => m + s.points.length, 0), 0) > 2e5) throw Error("Sketch resource budget exceeded");
1840
2034
  return doc;
1841
2035
  }
1842
- function createSketchCommandSession(adapter) {
1843
- const completed = /* @__PURE__ */ new Map();
1844
- let pending = false, cachedCharacters = 0;
1845
- return async (request) => {
1846
- if (!request || typeof request !== "object") throw Error("Invalid sketch request");
1847
- if (!adapter.available()) throw Error("Open the sketch board for this session first");
1848
- const current = adapter.snapshot();
1849
- if (request.action === "inspect") {
1850
- const offset = request.offset ?? 0, objects = adapter.objects?.() ?? [];
1851
- if (!Number.isInteger(offset) || offset < 0) throw Error("offset must be a non-negative integer");
1852
- return {
1853
- ...current,
1854
- protocolVersion: 2,
1855
- objects: objects.slice(offset, offset + 50),
1856
- objectCount: objects.length,
1857
- ...offset + 50 < objects.length ? { nextOffset: offset + 50 } : {},
1858
- ...request.objectId ? { object: adapter.object?.(request.objectId, request.layer) } : {},
1859
- recentRequests: [...completed.values()].slice(-8).map((entry) => entry.receipt),
1860
- help: SKETCH_COMMAND_HELP
2036
+ //#endregion
2037
+ //#region src/sketch-document-lifecycle.js
2038
+ function createSketchDocumentLifecycle(state, { sessionId, t, schedule, checkpoint, cache, setSelection, setTextEdit, setRecovered, store = sketchDrafts, decodeImages = decodeSketchImages, readImage = importSketchImage }) {
2039
+ const { doc, undo, redo, images, saved, dirty, documentId, documentRevision } = state;
2040
+ const hasContent = () => doc.current.layers.some((layer) => layer.image || layer.strokes.length);
2041
+ const save = async (name) => {
2042
+ const savingDocument = documentId.current, savingRevision = documentRevision.current;
2043
+ const row = {
2044
+ id: saved.current?.id ?? crypto.randomUUID(),
2045
+ name: name?.trim() || saved.current?.name || `${t("sketchTitle")} ${(/* @__PURE__ */ new Date()).toLocaleString()}`,
2046
+ updated: Date.now(),
2047
+ doc: structuredClone(doc.current)
2048
+ };
2049
+ try {
2050
+ await store("save", row, sessionId);
2051
+ } catch (error) {
2052
+ if (error.code === "SKETCH_DRAFT_LIMIT") error.message = t("sketchDraftLimit");
2053
+ if (error.code === "SKETCH_STORAGE_LIMIT") error.message = t("sketchStorageLimit");
2054
+ throw error;
2055
+ }
2056
+ if (documentId.current === savingDocument) {
2057
+ saved.current = {
2058
+ id: row.id,
2059
+ name: row.name
1861
2060
  };
2061
+ if (documentRevision.current === savingRevision) {
2062
+ dirty.current = false;
2063
+ setRecovered(false);
2064
+ }
1862
2065
  }
1863
- if (request.documentId !== current.documentId) throw Error("Document changed; inspect again");
1864
- if (pending || adapter.busy()) throw Error("Sketch is being edited; retry after it settles");
1865
- if (request.action === "preview") return {
1866
- ...current,
1867
- png: await adapter.preview()
1868
- };
1869
- if (!["apply", "save"].includes(request.action)) throw Error("Unknown sketch action");
1870
- if (typeof request.requestId !== "string" || !request.requestId.length || request.requestId.length > 100) throw Error("A unique requestId is required");
1871
- const key = `${current.documentId}:${request.requestId}`, fingerprint = JSON.stringify({
1872
- ...request,
1873
- runId: void 0
2066
+ };
2067
+ const saveChanges = async () => {
2068
+ if (!dirty.current) return;
2069
+ if (hasContent() || saved.current) return save();
2070
+ const id = documentId.current, revision = documentRevision.current;
2071
+ await store("clearRecovery", sessionId);
2072
+ if (documentId.current === id && documentRevision.current === revision) {
2073
+ dirty.current = false;
2074
+ setRecovered(false);
2075
+ }
2076
+ };
2077
+ const replace = (next, decoded, identity) => {
2078
+ documentId.current = crypto.randomUUID();
2079
+ documentRevision.current++;
2080
+ doc.current = identifyObjects(structuredClone(next));
2081
+ setSelection(null);
2082
+ setTextEdit(null);
2083
+ images.current = decoded;
2084
+ cache.current.clear();
2085
+ undo.current = [];
2086
+ redo.current = [];
2087
+ saved.current = identity;
2088
+ dirty.current = false;
2089
+ schedule();
2090
+ };
2091
+ const fresh = async () => {
2092
+ await saveChanges();
2093
+ replace(createSketchLayers(), /* @__PURE__ */ new Map(), null);
2094
+ };
2095
+ const load = async (row) => {
2096
+ if (row.id === saved.current?.id) return;
2097
+ row = await store("get", row.id);
2098
+ if (!row) throw Error("Draft no longer exists");
2099
+ await saveChanges();
2100
+ const decoded = /* @__PURE__ */ new Map();
2101
+ await decodeImages(row.doc, decoded);
2102
+ replace(row.doc, decoded, {
2103
+ id: row.id,
2104
+ name: row.name
1874
2105
  });
1875
- const cached = completed.get(key);
1876
- if (cached) {
1877
- if (cached.fingerprint !== fingerprint) throw Error("requestId reused with different content");
1878
- return cached.result;
2106
+ };
2107
+ const importImage = async (file) => {
2108
+ if (file.name?.toLowerCase().endsWith(".psd") || file.name?.toLowerCase().endsWith(".dsh-sketch.json")) {
2109
+ if (file.size > 32 * 1024 * 1024) throw Error("File exceeds 32 MB");
2110
+ const next = file.name.toLowerCase().endsWith(".psd") ? await importSketchPsd(file) : decodeSketchDocument(await file.text());
2111
+ const decoded = /* @__PURE__ */ new Map();
2112
+ await decodeImages(next, decoded);
2113
+ await saveChanges();
2114
+ replace(next, decoded, null);
2115
+ dirty.current = true;
2116
+ return;
2117
+ }
2118
+ if (doc.current.layers.length >= 8) throw Error("Layer limit");
2119
+ const image = await readImage(file), w = doc.current.width ?? 1024, h = doc.current.height ?? 1024;
2120
+ const scale = Math.min(w / image.width, h / image.height), width = image.width * scale / w, height = image.height * scale / h;
2121
+ const layer = {
2122
+ id: doc.current.nextId,
2123
+ name: file.name?.slice(0, 40) || t("sketchImport"),
2124
+ visible: true,
2125
+ strokes: [],
2126
+ image: {
2127
+ src: image.src,
2128
+ x: (1 - width) / 2,
2129
+ y: (1 - height) / 2,
2130
+ width,
2131
+ height
2132
+ }
2133
+ };
2134
+ await decodeImages({ layers: [layer] }, images.current);
2135
+ checkpoint();
2136
+ doc.current = {
2137
+ ...doc.current,
2138
+ nextId: layer.id + 1,
2139
+ active: layer.id,
2140
+ layers: [...doc.current.layers, layer]
2141
+ };
2142
+ schedule();
2143
+ };
2144
+ const restore = async (isCurrent = () => true) => {
2145
+ if (dirty.current || hasContent()) return;
2146
+ const id = documentId.current, revision = documentRevision.current;
2147
+ const recovery = await store("recover", sessionId);
2148
+ const archived = state.restoreId.current;
2149
+ const row = recovery ?? (archived ? await store("get", archived) : null);
2150
+ const decoded = /* @__PURE__ */ new Map();
2151
+ if (row) await decodeImages(row.doc, decoded);
2152
+ if (!isCurrent() || documentId.current !== id || documentRevision.current !== revision || dirty.current) return;
2153
+ if (row) {
2154
+ replace(row.doc, decoded, recovery ? null : {
2155
+ id: row.id,
2156
+ name: row.name
2157
+ });
2158
+ dirty.current = Boolean(recovery);
2159
+ setRecovered(Boolean(recovery));
1879
2160
  }
1880
- if (request.revision !== current.revision || adapter.busy()) throw Error("Sketch changed or is being edited; inspect again");
1881
- let changedObjects;
1882
- if (request.action === "apply") {
1883
- const before = adapter.document();
1884
- let next;
1885
- try {
1886
- next = applySketchCommands(before, request.commands);
1887
- } catch (cause) {
1888
- const error = new Error(`${cause.message} Correct the batch and retry with the same runId and revision; nothing was applied.`, { cause });
1889
- error.code = "SKETCH_INVALID_BATCH";
1890
- throw error;
1891
- }
1892
- adapter.commit(next);
1893
- const previous = new Map(before.layers.flatMap((l) => l.strokes.map((s) => [`${l.id}:${s.id}`, s])));
1894
- changedObjects = next.layers.flatMap((l) => l.strokes.filter((s) => previous.get(`${l.id}:${s.id}`) !== s).map((s) => ({
1895
- layer: l.id,
1896
- id: s.id
1897
- })));
1898
- } else {
1899
- if (request.name !== void 0 && (typeof request.name !== "string" || request.name.length > 60)) throw Error("Invalid draft name");
1900
- pending = true;
2161
+ state.restoreId.current = null;
2162
+ };
2163
+ return {
2164
+ hasContent,
2165
+ save,
2166
+ saveChanges,
2167
+ replace,
2168
+ fresh,
2169
+ load,
2170
+ importImage,
2171
+ restore
2172
+ };
2173
+ }
2174
+ //#endregion
2175
+ //#region src/sketch-operation-gate.js
2176
+ function createSketchOperationGate() {
2177
+ let running = false;
2178
+ return {
2179
+ get running() {
2180
+ return running;
2181
+ },
2182
+ async run(operation, { blocked = false, working, report, rethrow = false }) {
2183
+ if (blocked || running) return false;
2184
+ running = true;
1901
2185
  try {
1902
- await adapter.save(request.name);
2186
+ working(true);
2187
+ report(null);
2188
+ await operation();
2189
+ return true;
2190
+ } catch (error) {
2191
+ report(error);
2192
+ if (rethrow) throw error;
2193
+ return false;
1903
2194
  } finally {
1904
- pending = false;
2195
+ running = false;
2196
+ working(false);
1905
2197
  }
1906
2198
  }
1907
- const result = {
1908
- ...adapter.snapshot(),
1909
- ...changedObjects ? {
1910
- changedObjects: changedObjects.slice(0, 100),
1911
- changedObjectCount: changedObjects.length
1912
- } : {}
2199
+ };
2200
+ }
2201
+ //#endregion
2202
+ //#region src/sketch-agent-export.js
2203
+ async function exportSketchAgentFile(format, { gate, blocked, working, report, exportFile }) {
2204
+ let result;
2205
+ if (!await gate.run(async () => {
2206
+ const { blob, extension } = await exportFile(format);
2207
+ const data = new Uint8Array(await blob.arrayBuffer());
2208
+ let raw = "";
2209
+ for (let i = 0; i < data.length; i += 8192) raw += String.fromCharCode(...data.subarray(i, i + 8192));
2210
+ result = {
2211
+ extension,
2212
+ mediaType: blob.type,
2213
+ base64: btoa(raw)
1913
2214
  };
1914
- completed.set(key, {
1915
- fingerprint,
1916
- result,
1917
- receipt: {
1918
- requestId: request.requestId,
1919
- action: request.action,
1920
- revision: result.revision
2215
+ }, {
2216
+ blocked,
2217
+ working,
2218
+ report,
2219
+ rethrow: true
2220
+ })) throw Error("Sketch is being edited; retry after it settles");
2221
+ return result;
2222
+ }
2223
+ //#endregion
2224
+ //#region src/sketch-run-status.jsx
2225
+ function SketchRunStatus({ state, t, floating = false, onOpen, onStop, onResume, onDismiss }) {
2226
+ if (state === "idle") return null;
2227
+ const drawing = state === "drawing", recover = state === "stopped" || state === "failed";
2228
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2229
+ className: floating ? "codexSketchBackgroundStatus" : "codexSketchAgentStatus",
2230
+ role: "status",
2231
+ "aria-live": "polite",
2232
+ children: [
2233
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(WorkspaceIcon, {
2234
+ name: drawing ? "pen" : state === "finished" ? "check" : "rectangle",
2235
+ size: 15
2236
+ }),
2237
+ floating ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2238
+ type: "button",
2239
+ onClick: onOpen,
2240
+ children: t(`sketchRun_${state}`)
2241
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t(`sketchRun_${state}`) }),
2242
+ drawing ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
2243
+ type: "button",
2244
+ className: "codexSketchStop",
2245
+ onClick: onStop,
2246
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(WorkspaceIcon, {
2247
+ name: "stop",
2248
+ size: 12
2249
+ }), t("sketchRunStop")]
2250
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [recover ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2251
+ type: "button",
2252
+ title: t("sketchRunResumeHint"),
2253
+ onClick: onResume,
2254
+ children: t("sketchRunResume")
2255
+ }) : null, !recover ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2256
+ type: "button",
2257
+ "aria-label": t("sketchDismissStatus"),
2258
+ title: t("sketchDismissStatus"),
2259
+ onClick: onDismiss,
2260
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(WorkspaceIcon, {
2261
+ name: "close",
2262
+ size: 14
2263
+ })
2264
+ }) : null] })
2265
+ ]
2266
+ });
2267
+ }
2268
+ //#endregion
2269
+ //#region src/sketch-tool-widths.js
2270
+ const defaults = {
2271
+ pen: 12,
2272
+ pencil: 6,
2273
+ marker: 28,
2274
+ eraser: 24,
2275
+ text: 32,
2276
+ line: 12,
2277
+ arrow: 12,
2278
+ rectangle: 12,
2279
+ circle: 12
2280
+ };
2281
+ const keyFor = ({ tool, brush }) => tool === "pen" ? brush : tool;
2282
+ function switchSketchToolWidth(memory, current, next) {
2283
+ if (current.tool !== "select") memory[keyFor(current)] = current.width;
2284
+ if (next.tool === "select") return current.width;
2285
+ const key = keyFor(next);
2286
+ return memory[key] ?? defaults[key] ?? 12;
2287
+ }
2288
+ function stepSketchWidth(width, direction) {
2289
+ return Math.max(1, Math.min(256, width + direction * 2));
2290
+ }
2291
+ //#endregion
2292
+ //#region src/sketch-shortcuts.js
2293
+ function sketchShortcutAction(keys, key) {
2294
+ key = key.toLowerCase();
2295
+ return Object.entries(keys).find(([, value]) => value === key)?.[0] ?? {
2296
+ v: "select",
2297
+ t: "text",
2298
+ "+": "zoomIn"
2299
+ }[key];
2300
+ }
2301
+ //#endregion
2302
+ //#region src/sketch-layer-renderer.js
2303
+ const NO_IMAGES = /* @__PURE__ */ new Map();
2304
+ const surface = (width, height) => {
2305
+ const c = document.createElement("canvas");
2306
+ c.width = width;
2307
+ c.height = height;
2308
+ return c;
2309
+ };
2310
+ function paintSketchLayers(context, doc, cache, size = doc.width ?? 1024, height = doc.height ?? size, activeLayer, images = NO_IMAGES) {
2311
+ context.globalCompositeOperation = "source-over";
2312
+ context.globalAlpha = 1;
2313
+ context.fillStyle = "#fff";
2314
+ context.fillRect(0, 0, size, height);
2315
+ for (const id of cache.keys()) if (!doc.layers.some((layer) => layer.id === id)) cache.delete(id);
2316
+ for (const layer of doc.layers) {
2317
+ if (!layer.visible) continue;
2318
+ let entry = cache.get(layer.id);
2319
+ if (!entry || entry.surface.width !== size || entry.surface.height !== height) {
2320
+ entry = {
2321
+ surface: surface(size, height),
2322
+ base: surface(size, height)
2323
+ };
2324
+ cache.set(layer.id, entry);
2325
+ }
2326
+ const moving = layer.id === activeLayer;
2327
+ const count = Math.max(0, layer.strokes.length - (moving ? 1 : 0));
2328
+ const prefix = layer.strokes[count - 1];
2329
+ if (entry.count !== count || entry.prefix !== prefix || entry.image !== layer.image || entry.strokes !== layer.strokes) {
2330
+ const ctx = entry.base.getContext("2d");
2331
+ const append = entry.count !== void 0 && count >= entry.count && entry.image === layer.image && (entry.strokes === layer.strokes || entry.strokes.slice(0, entry.count).every((stroke, index) => stroke === layer.strokes[index]));
2332
+ if (!append) {
2333
+ ctx.clearRect(0, 0, size, height);
2334
+ const ref = layer.image, image = ref && images.get(ref.src);
2335
+ if (image) ctx.drawImage(image, ref.x * size, ref.y * height, ref.width * size, ref.height * height);
1921
2336
  }
1922
- });
1923
- cachedCharacters += fingerprint.length;
1924
- while (completed.size > 1 && (completed.size > 128 || cachedCharacters > 4e6)) {
1925
- const oldest = completed.keys().next().value;
1926
- cachedCharacters -= completed.get(oldest).fingerprint.length;
1927
- completed.delete(oldest);
2337
+ paintSketch(ctx, layer.strokes, size, true, height, append ? entry.count : 0, count);
2338
+ entry.count = count;
2339
+ entry.prefix = prefix;
2340
+ entry.image = layer.image;
2341
+ entry.strokes = layer.strokes;
2342
+ }
2343
+ if (moving) {
2344
+ const ctx = entry.surface.getContext("2d");
2345
+ ctx.clearRect(0, 0, size, height);
2346
+ ctx.drawImage(entry.base, 0, 0);
2347
+ paintSketch(ctx, layer.strokes, size, true, height, layer.strokes.length - 1);
2348
+ context.drawImage(entry.surface, 0, 0);
2349
+ } else context.drawImage(entry.base, 0, 0);
2350
+ }
2351
+ }
2352
+ //#endregion
2353
+ //#region src/sketch-interactions.js
2354
+ function useSketchDismiss(open, close, host, selectors) {
2355
+ const latest = (0, react.useRef)(close);
2356
+ latest.current = close;
2357
+ (0, react.useEffect)(() => {
2358
+ if (!open) return;
2359
+ const dialog = host.current?.closest("dialog") ?? host.current;
2360
+ if (!dialog) return;
2361
+ const pointer = (event) => {
2362
+ if (selectors.some((selector) => event.target.closest?.(selector))) return;
2363
+ latest.current(false);
2364
+ if (event.target.matches?.("canvas")) {
2365
+ event.preventDefault();
2366
+ event.stopPropagation();
2367
+ event.target.focus({ preventScroll: true });
2368
+ }
2369
+ };
2370
+ const key = (event) => {
2371
+ if (event.key !== "Escape") return;
2372
+ event.preventDefault();
2373
+ event.stopPropagation();
2374
+ latest.current(false);
2375
+ dialog.querySelector("canvas")?.focus({ preventScroll: true });
2376
+ };
2377
+ const hidden = () => latest.current(false);
2378
+ document.addEventListener("pointerdown", pointer, true);
2379
+ document.addEventListener("keydown", key, true);
2380
+ dialog.addEventListener("close", hidden);
2381
+ return () => {
2382
+ document.removeEventListener("pointerdown", pointer, true);
2383
+ document.removeEventListener("keydown", key, true);
2384
+ dialog.removeEventListener("close", hidden);
2385
+ };
2386
+ }, [
2387
+ open,
2388
+ host,
2389
+ selectors.join("|")
2390
+ ]);
2391
+ }
2392
+ function useSketchCursor(canvas, ring, width, brush, zoom, hidden) {
2393
+ const last = (0, react.useRef)(null), heldPressure = (0, react.useRef)(1);
2394
+ const update = (event, bounds) => {
2395
+ if (event) last.current = event;
2396
+ const pointer = last.current, node = canvas.current, cursor = ring.current;
2397
+ if (!pointer || !node || !cursor) return;
2398
+ const rect = bounds ?? node.getBoundingClientRect();
2399
+ if (hidden || pointer.pointerType === "touch" || pointer.clientX < rect.left || pointer.clientX > rect.right || pointer.clientY < rect.top || pointer.clientY > rect.bottom) {
2400
+ cursor.hidden = true;
2401
+ return;
2402
+ }
2403
+ const diameter = width * (node.hasPointerCapture(pointer.pointerId) && pointer.pointerType === "pen" ? heldPressure.current : 1) * rect.width / node.width;
2404
+ cursor.hidden = false;
2405
+ cursor.style.width = `${diameter}px`;
2406
+ cursor.style.height = `${diameter}px`;
2407
+ cursor.style.transform = `translate(${pointer.clientX - diameter / 2}px,${pointer.clientY - diameter / 2}px)`;
2408
+ };
2409
+ (0, react.useEffect)(() => {
2410
+ update();
2411
+ const observer = new ResizeObserver(() => update());
2412
+ if (canvas.current) observer.observe(canvas.current);
2413
+ return () => observer.disconnect();
2414
+ }, [
2415
+ width,
2416
+ brush,
2417
+ zoom,
2418
+ hidden
2419
+ ]);
2420
+ return {
2421
+ down: (event) => {
2422
+ heldPressure.current = event.pointerType === "pen" ? Math.max(.2, event.pressure) : 1;
2423
+ },
2424
+ move: (event, bounds) => update({
2425
+ clientX: event.clientX,
2426
+ clientY: event.clientY,
2427
+ pointerId: event.pointerId,
2428
+ pointerType: event.pointerType,
2429
+ pressure: event.pressure
2430
+ }, bounds),
2431
+ leave: () => {
2432
+ last.current = null;
2433
+ if (ring.current) ring.current.hidden = true;
1928
2434
  }
1929
- return result;
1930
2435
  };
1931
2436
  }
1932
2437
  //#endregion
1933
- //#region src/sketch-formats.js
1934
- const SKETCH_FILE_ACCEPT = ".psd,.dsh-sketch.json,image/png,image/jpeg,image/webp";
1935
- const canvas = (w, h) => {
1936
- const c = document.createElement("canvas");
1937
- c.width = w;
1938
- c.height = h;
1939
- return c;
2438
+ //#region src/sketch-view.jsx
2439
+ const DEFAULT_KEYS = {
2440
+ pen: "b",
2441
+ eraser: "e",
2442
+ line: "l",
2443
+ rectangle: "r",
2444
+ circle: "o",
2445
+ pan: " ",
2446
+ zoomIn: "=",
2447
+ zoomOut: "-",
2448
+ fit: "0"
1940
2449
  };
1941
- function runPsdCodec(action, payload) {
1942
- return new Promise((resolve, reject) => {
1943
- const worker = new Worker("/api/codex-subscription/sketch-psd-worker", { type: "module" });
1944
- const finish = (callback, value) => {
1945
- clearTimeout(timer);
1946
- worker.terminate();
1947
- callback(value);
1948
- };
1949
- const timer = setTimeout(() => finish(reject, Error("PSD operation timed out")), 3e4);
1950
- worker.onerror = () => finish(reject, Error("PSD codec could not be loaded"));
1951
- worker.onmessage = (event) => event.data.ok ? finish(resolve, event.data.value) : finish(reject, Error(event.data.error));
1952
- worker.postMessage({
1953
- action,
1954
- payload
1955
- });
2450
+ function useSketchView(canvas, open) {
2451
+ const [view, setView] = (0, react.useState)({
2452
+ scale: 1,
2453
+ x: 0,
2454
+ y: 0
2455
+ }), [keys, setKeys] = (0, react.useState)(() => {
2456
+ try {
2457
+ return {
2458
+ ...DEFAULT_KEYS,
2459
+ ...JSON.parse(localStorage.getItem("codex-sketch-keys"))
2460
+ };
2461
+ } catch {
2462
+ return DEFAULT_KEYS;
2463
+ }
1956
2464
  });
1957
- }
1958
- function encodeSketchDocument(doc) {
1959
- return JSON.stringify({
1960
- format: "dsh-sketch",
1961
- version: 1,
1962
- doc
2465
+ const [shortcuts, setShortcuts] = (0, react.useState)(() => {
2466
+ try {
2467
+ return localStorage.getItem("codex-sketch-shortcuts") !== "off";
2468
+ } catch {
2469
+ return true;
2470
+ }
2471
+ }), [space, setSpace] = (0, react.useState)(false);
2472
+ const drag = (0, react.useRef)(null), viewRef = (0, react.useRef)(view);
2473
+ viewRef.current = view;
2474
+ const zoom = (factor) => setView((v) => ({
2475
+ ...v,
2476
+ scale: Math.max(.25, Math.min(8, v.scale * factor))
2477
+ }));
2478
+ const reset = () => setView({
2479
+ scale: 1,
2480
+ x: 0,
2481
+ y: 0
1963
2482
  });
1964
- }
1965
- function decodeSketchDocument(text) {
1966
- if (text.length > 32 * 1024 * 1024) throw Error("Draft exceeds 32 MB");
1967
- const file = JSON.parse(text), source = file.doc;
1968
- if (file.format !== "dsh-sketch" || file.version !== 1 || !source || !Array.isArray(source.layers) || !source.layers.length || source.layers.length > 8) throw Error("Invalid sketch file");
1969
- const w = source.width ?? 1024, h = source.height ?? 1024;
1970
- if (!Number.isInteger(w) || !Number.isInteger(h) || w < 1 || h < 1 || w > 2048 || h > 2048) throw Error("Invalid canvas size");
1971
- let doc = {
1972
- ...createSketchLayers(),
1973
- width: w,
1974
- height: h,
1975
- ratio: Object.keys(SKETCH_RATIOS).find((k) => SKETCH_RATIOS[k][0] === w && SKETCH_RATIOS[k][1] === h) ?? "custom"
2483
+ (0, react.useEffect)(() => {
2484
+ const node = canvas.current;
2485
+ if (!node || !open) return;
2486
+ const wheel = (e) => {
2487
+ if (!shortcuts || !e.altKey) return;
2488
+ e.preventDefault();
2489
+ zoom(e.deltaY < 0 ? 1.1 : 1 / 1.1);
2490
+ };
2491
+ node.addEventListener("wheel", wheel, { passive: false });
2492
+ return () => node.removeEventListener("wheel", wheel);
2493
+ }, [open, shortcuts]);
2494
+ (0, react.useEffect)(() => {
2495
+ const stop = () => {
2496
+ drag.current = null;
2497
+ setSpace(false);
2498
+ };
2499
+ window.addEventListener("blur", stop);
2500
+ return () => window.removeEventListener("blur", stop);
2501
+ }, []);
2502
+ (0, react.useEffect)(() => {
2503
+ if (!open || !shortcuts) {
2504
+ setSpace(false);
2505
+ drag.current = null;
2506
+ }
2507
+ }, [open, shortcuts]);
2508
+ const setKey = (action, key) => {
2509
+ key = key.toLowerCase();
2510
+ if (!key || Object.entries(keys).some(([a, k]) => a !== action && k === key) || ["[", "]"].includes(key)) return;
2511
+ const next = {
2512
+ ...keys,
2513
+ [action]: key
2514
+ };
2515
+ setKeys(next);
2516
+ try {
2517
+ localStorage.setItem("codex-sketch-keys", JSON.stringify(next));
2518
+ } catch {}
1976
2519
  };
1977
- for (let i = 0; i < source.layers.length; i++) {
1978
- const layer = source.layers[i];
1979
- if (i) doc = applySketchCommands(doc, [{
1980
- op: "layer",
1981
- action: "add"
1982
- }]);
1983
- if (!Array.isArray(layer.strokes)) throw Error("Invalid strokes");
1984
- for (let j = 0; j < layer.strokes.length; j += 256) {
1985
- const strokes = layer.strokes.slice(j, j + 256);
1986
- doc = applySketchCommands(doc, strokes.map((s) => ({
1987
- ...s,
1988
- op: "stroke",
1989
- layer: doc.active,
1990
- fill: s.fill ?? false
1991
- })));
1992
- const added = doc.layers.at(-1).strokes;
1993
- for (let k = 0; k < strokes.length; k++) {
1994
- const s = strokes[k];
1995
- if (s.brush !== void 0 && ![
1996
- "pen",
1997
- "pencil",
1998
- "marker"
1999
- ].includes(s.brush)) throw Error("Invalid brush");
2000
- if (s.pressure !== void 0 && (!Number.isFinite(s.pressure) || s.pressure < .2 || s.pressure > 1)) throw Error("Invalid pressure");
2001
- if (s.brushVersion !== void 0 && s.brushVersion !== 2) throw Error("Unsupported brush version");
2002
- Object.assign(added[added.length - strokes.length + k], {
2003
- brush: s.brush ?? "pen",
2004
- pressure: s.pressure ?? 1,
2005
- ...s.brushVersion === 2 ? { brushVersion: 2 } : {}
2006
- });
2520
+ return {
2521
+ view,
2522
+ keys,
2523
+ shortcuts,
2524
+ space,
2525
+ zoom,
2526
+ reset,
2527
+ setKey,
2528
+ toggle: () => setShortcuts((v) => {
2529
+ try {
2530
+ localStorage.setItem("codex-sketch-shortcuts", v ? "off" : "on");
2531
+ } catch {}
2532
+ return !v;
2533
+ }),
2534
+ keyDown: (e) => {
2535
+ if (!shortcuts || e.ctrlKey || e.metaKey || e.altKey) return false;
2536
+ const action = sketchShortcutAction(keys, e.key);
2537
+ if (action === "pan") {
2538
+ e.preventDefault();
2539
+ setSpace(true);
2540
+ return true;
2007
2541
  }
2008
- }
2009
- const target = doc.layers.at(-1);
2010
- target.name = String(layer.name ?? "").slice(0, 40);
2011
- target.visible = layer.visible !== false;
2012
- if (layer.image) {
2013
- const image = layer.image;
2014
- if (typeof image.src !== "string" || !/^data:image\/png;base64,/.test(image.src) || image.src.length > 8 * 1024 * 1024 || [
2015
- "x",
2016
- "y",
2017
- "width",
2018
- "height"
2019
- ].some((k) => !Number.isFinite(image[k]) || image[k] < 0 || image[k] > 1)) throw Error("Invalid draft image");
2020
- const bytes = Uint8Array.from(atob(image.src.slice(image.src.indexOf(",") + 1)), (c) => c.charCodeAt(0));
2021
- if (bytes.length < 24) throw Error("Invalid draft image");
2022
- const header = new DataView(bytes.buffer);
2023
- if (header.getUint32(0) !== 2303741511 || header.getUint32(4) !== 218765834 || header.getUint32(16) < 1 || header.getUint32(20) < 1 || header.getUint32(16) > 4096 || header.getUint32(20) > 4096) throw Error("Invalid draft image size");
2024
- target.image = {
2025
- src: image.src,
2026
- x: image.x,
2027
- y: image.y,
2028
- width: image.width,
2029
- height: image.height
2542
+ if ([
2543
+ "zoomIn",
2544
+ "zoomOut",
2545
+ "fit"
2546
+ ].includes(action)) {
2547
+ e.preventDefault();
2548
+ if (action === "fit") reset();
2549
+ else zoom(action === "zoomOut" ? 1 / 1.2 : 1.2);
2550
+ return true;
2551
+ }
2552
+ return false;
2553
+ },
2554
+ keyUp: (e) => {
2555
+ if (e.key.toLowerCase() === keys.pan) setSpace(false);
2556
+ },
2557
+ down: (e) => {
2558
+ if (e.button !== 1 && !space) return false;
2559
+ e.preventDefault();
2560
+ drag.current = {
2561
+ id: e.pointerId,
2562
+ x: e.clientX,
2563
+ y: e.clientY,
2564
+ view: viewRef.current
2030
2565
  };
2566
+ canvas.current.setPointerCapture(e.pointerId);
2567
+ return true;
2568
+ },
2569
+ move: (e) => {
2570
+ const d = drag.current;
2571
+ if (!d || d.id !== e.pointerId) return false;
2572
+ setView({
2573
+ ...d.view,
2574
+ x: d.view.x + e.clientX - d.x,
2575
+ y: d.view.y + e.clientY - d.y
2576
+ });
2577
+ return true;
2578
+ },
2579
+ end: (e) => {
2580
+ if (drag.current?.id !== e.pointerId) return false;
2581
+ drag.current = null;
2582
+ if (canvas.current.hasPointerCapture(e.pointerId)) canvas.current.releasePointerCapture(e.pointerId);
2583
+ return true;
2031
2584
  }
2032
- }
2033
- const activeIndex = source.layers.findIndex((layer) => layer.id === source.active);
2034
- doc.active = doc.layers[Math.max(0, activeIndex)].id;
2035
- return doc;
2585
+ };
2036
2586
  }
2037
- async function exportSketchPsd(doc, images, composite) {
2038
- const width = doc.width ?? 1024, height = doc.height ?? 1024;
2039
- const children = doc.layers.map((layer, i) => {
2040
- const ctx = canvas(width, height).getContext("2d"), ref = layer.image;
2041
- if (ref) ctx.drawImage(images.get(ref.src), ref.x * width, ref.y * height, ref.width * width, ref.height * height);
2042
- paintSketch(ctx, layer.strokes, width, true, height);
2043
- return {
2044
- name: layer.name || `Layer ${i + 1}`,
2045
- hidden: !layer.visible,
2046
- opacity: 1,
2047
- blendMode: "normal",
2048
- imageData: ctx.getImageData(0, 0, width, height)
2587
+ function SketchViewControls({ navigation, t }) {
2588
+ const [open, setOpen] = (0, react.useState)(false), [placement, setPlacement] = (0, react.useState)(null);
2589
+ const host = (0, react.useRef)(null), trigger = (0, react.useRef)(null);
2590
+ const close = () => {
2591
+ setOpen(false);
2592
+ trigger.current?.focus({ preventScroll: true });
2593
+ };
2594
+ useSketchDismiss(open, setOpen, host, [".codexSketchViewControls", ".codexSketchKeyPanel"]);
2595
+ (0, react.useLayoutEffect)(() => {
2596
+ if (!open) return;
2597
+ const dialog = host.current.closest("dialog");
2598
+ const place = () => {
2599
+ const box = dialog.getBoundingClientRect(), anchor = trigger.current.getBoundingClientRect();
2600
+ const width = Math.min(360, box.width - 24);
2601
+ setPlacement({
2602
+ dialog,
2603
+ style: {
2604
+ width,
2605
+ left: Math.max(12, Math.min(anchor.left - box.left, box.width - width - 12)),
2606
+ bottom: box.bottom - anchor.top + 8,
2607
+ maxHeight: Math.max(80, anchor.top - box.top - 24)
2608
+ }
2609
+ });
2610
+ };
2611
+ place();
2612
+ const observer = new ResizeObserver(place);
2613
+ observer.observe(dialog);
2614
+ observer.observe(host.current);
2615
+ window.addEventListener("resize", place);
2616
+ return () => {
2617
+ observer.disconnect();
2618
+ window.removeEventListener("resize", place);
2049
2619
  };
2620
+ }, [open]);
2621
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2622
+ ref: host,
2623
+ className: "codexSketchViewControls",
2624
+ children: [
2625
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2626
+ type: "button",
2627
+ "aria-label": t("sketchZoomOut"),
2628
+ onClick: () => navigation.zoom(1 / 1.2),
2629
+ children: "−"
2630
+ }),
2631
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
2632
+ type: "button",
2633
+ title: t("sketchFit"),
2634
+ onClick: navigation.reset,
2635
+ children: [Math.round(navigation.view.scale * 100), "%"]
2636
+ }),
2637
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2638
+ type: "button",
2639
+ "aria-label": t("sketchZoomIn"),
2640
+ onClick: () => navigation.zoom(1.2),
2641
+ children: "+"
2642
+ }),
2643
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2644
+ ref: trigger,
2645
+ type: "button",
2646
+ "aria-expanded": open,
2647
+ onClick: () => setOpen(!open),
2648
+ children: t("sketchKeys")
2649
+ }),
2650
+ open && placement ? (0, react_dom.createPortal)(/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
2651
+ className: "codexSketchKeyPanel",
2652
+ "aria-label": t("sketchKeys"),
2653
+ style: placement.style,
2654
+ onKeyDown: (e) => {
2655
+ if (e.key === "Escape") {
2656
+ e.preventDefault();
2657
+ e.stopPropagation();
2658
+ close();
2659
+ }
2660
+ },
2661
+ children: [
2662
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("header", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("strong", { children: t("sketchKeys") }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2663
+ type: "button",
2664
+ "aria-label": t("sketchFileClose"),
2665
+ onClick: close,
2666
+ children: "×"
2667
+ })] }),
2668
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
2669
+ className: "codexSketchKeysEnabled",
2670
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("sketchKeysEnabled") }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
2671
+ type: "checkbox",
2672
+ checked: navigation.shortcuts,
2673
+ onChange: navigation.toggle
2674
+ })]
2675
+ }),
2676
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", { children: t("sketchNavigationHint") }),
2677
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2678
+ className: "codexSketchKeyGrid",
2679
+ children: Object.entries(navigation.keys).map(([action, key]) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", { children: [t(`sketchKey_${action}`), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
2680
+ "aria-label": t(`sketchKey_${action}`),
2681
+ value: key === " " ? "Space" : key,
2682
+ readOnly: true,
2683
+ onKeyDown: (e) => {
2684
+ if (e.key === "Tab" || e.key === "Escape") return;
2685
+ e.preventDefault();
2686
+ e.stopPropagation();
2687
+ if (e.key.length === 1 && !e.ctrlKey && !e.metaKey && !e.altKey) navigation.setKey(action, e.key);
2688
+ }
2689
+ })] }, action))
2690
+ }),
2691
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("small", { children: t("sketchKeyHint") })
2692
+ ]
2693
+ }), placement.dialog) : null
2694
+ ]
2050
2695
  });
2051
- const context = canvas(width, height).getContext("2d");
2052
- context.fillStyle = "#fff";
2053
- context.fillRect(0, 0, width, height);
2054
- if (children[0] && !children[0].hidden) {
2055
- const bottom = canvas(width, height);
2056
- bottom.getContext("2d").putImageData(children[0].imageData, 0, 0);
2057
- context.drawImage(bottom, 0, 0);
2058
- children[0].imageData = context.getImageData(0, 0, width, height);
2059
- } else {
2060
- if (children.length >= 8) throw Error("Show the bottom layer before exporting this eight-layer drawing");
2061
- children.unshift({
2062
- name: "Paper",
2063
- opacity: 1,
2064
- blendMode: "normal",
2065
- imageData: context.getImageData(0, 0, width, height)
2066
- });
2067
- }
2068
- return runPsdCodec("write", {
2069
- width,
2070
- height,
2071
- children,
2072
- imageData: composite.getContext("2d").getImageData(0, 0, width, height)
2073
- });
2074
- }
2075
- async function importSketchPsd(file) {
2076
- if (file.size > 32 * 1024 * 1024) throw Error("PSD exceeds 32 MB");
2077
- const psd = await runPsdCodec("read", await file.arrayBuffer());
2078
- const scale = Math.min(1, 1024 / Math.max(psd.width, psd.height)), width = Math.max(1, Math.round(psd.width * scale)), height = Math.max(1, Math.round(psd.height * scale));
2079
- const doc = {
2080
- ...createSketchLayers(),
2081
- width,
2082
- height,
2083
- ratio: Object.keys(SKETCH_RATIOS).find((k) => SKETCH_RATIOS[k][0] === width && SKETCH_RATIOS[k][1] === height) ?? "custom",
2084
- layers: [],
2085
- nextId: psd.layers.length + 1
2086
- };
2087
- for (const [i, layer] of psd.layers.entries()) {
2088
- const src = canvas(layer.imageData.width, layer.imageData.height);
2089
- src.getContext("2d").putImageData(new ImageData(new Uint8ClampedArray(layer.imageData.data), layer.imageData.width, layer.imageData.height), 0, 0);
2090
- const out = canvas(width, height), ctx = out.getContext("2d");
2091
- ctx.globalAlpha = layer.opacity;
2092
- ctx.drawImage(src, layer.left * scale, layer.top * scale, src.width * scale, src.height * scale);
2093
- doc.layers.push({
2094
- id: i + 1,
2095
- name: layer.name,
2096
- visible: !layer.hidden,
2097
- strokes: [],
2098
- image: {
2099
- src: out.toDataURL("image/png"),
2100
- x: 0,
2101
- y: 0,
2102
- width: 1,
2103
- height: 1
2104
- }
2105
- });
2106
- }
2107
- return doc;
2108
2696
  }
2109
2697
  //#endregion
2110
2698
  //#region src/sketch-files.jsx
2111
- function SketchFiles({ save, load, fresh, importImage, download, hasContent, disabled, t, report, onWorking }) {
2699
+ function SketchFiles({ save, load, fresh, importImage, download, hasContent, disabled, t, runOperation }) {
2112
2700
  const [open, setOpen] = (0, react.useState)(false), [rows, setRows] = (0, react.useState)([]), [name, setName] = (0, react.useState)(""), [remove, setRemove] = (0, react.useState)(null), [working, setWorking] = (0, react.useState)(false);
2113
2701
  const [format, setFormat] = (0, react.useState)("png");
2114
2702
  const input = (0, react.useRef)(null), host = (0, react.useRef)(null);
@@ -2120,19 +2708,14 @@ window.__ModuleLoader__.load({
2120
2708
  }
2121
2709
  }, [open, working]);
2122
2710
  useSketchDismiss(open, setOpen, host, [".codexSketchFiles"]);
2123
- const run = async (operation) => {
2124
- if (disabled || working) return;
2711
+ const run = (operation) => runOperation(async () => {
2125
2712
  setWorking(true);
2126
- onWorking(true);
2127
2713
  try {
2128
2714
  await operation();
2129
- } catch (error) {
2130
- report(error?.message || t("sketchStorageFailed"));
2131
2715
  } finally {
2132
2716
  setWorking(false);
2133
- onWorking(false);
2134
2717
  }
2135
- };
2718
+ });
2136
2719
  const refresh = async () => setRows(await sketchDrafts("list"));
2137
2720
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2138
2721
  ref: host,
@@ -2429,13 +3012,15 @@ window.__ModuleLoader__.load({
2429
3012
  //#endregion
2430
3013
  //#region src/sketch-agent-client.js
2431
3014
  function connectSketchAgent(rpc, sessionId, execute, report, pollDelay = () => 350) {
2432
- let stopped = false, token, timer, attempts = 0, failures = 0;
3015
+ let stopped = false, token, timer, attempts = 0, failures = 0, pending = false;
2433
3016
  const call = (endpoint, payload) => rpc.call(CHANNEL, `sketch/${endpoint}`, {
2434
3017
  sessionId,
2435
3018
  token,
2436
3019
  ...payload
2437
3020
  }).then(unwrap);
2438
3021
  const poll = async () => {
3022
+ if (stopped || pending) return;
3023
+ pending = true;
2439
3024
  try {
2440
3025
  const tasks = await call("poll");
2441
3026
  for (const task of tasks) {
@@ -2470,25 +3055,53 @@ window.__ModuleLoader__.load({
2470
3055
  timer = setTimeout(connect, Math.min(1e4, 1e3 * 2 ** (failures - 1)));
2471
3056
  }
2472
3057
  return;
3058
+ } finally {
3059
+ pending = false;
2473
3060
  }
2474
3061
  failures = 0;
2475
3062
  if (!stopped) timer = setTimeout(poll, pollDelay());
2476
3063
  };
2477
- const connect = () => void call("connect").then((value) => {
2478
- token = value.token;
3064
+ const connect = () => {
3065
+ if (stopped || pending) return;
3066
+ pending = true;
3067
+ call("connect").then((value) => {
3068
+ pending = false;
3069
+ token = value.token;
3070
+ attempts = 0;
3071
+ if (stopped) call("disconnect").catch(() => {});
3072
+ else poll();
3073
+ }, (error) => {
3074
+ pending = false;
3075
+ if (stopped) return;
3076
+ const leaseConflict = /Another board is connected/.test(error.message);
3077
+ if (++attempts < (leaseConflict ? 8 : 3)) timer = setTimeout(connect, Math.min(3e3, 500 * attempts));
3078
+ else report(error.message);
3079
+ });
3080
+ };
3081
+ const wake = () => {
3082
+ if (stopped || pending) return;
3083
+ clearTimeout(timer);
2479
3084
  attempts = 0;
2480
- if (stopped) call("disconnect").catch(() => {});
2481
- else poll();
2482
- }, (error) => {
2483
- if (stopped) return;
2484
- const leaseConflict = /Another board is connected/.test(error.message);
2485
- if (++attempts < (leaseConflict ? 8 : 3)) timer = setTimeout(connect, Math.min(3e3, 500 * attempts));
2486
- else report(error.message);
2487
- });
3085
+ failures = 0;
3086
+ token ? poll() : connect();
3087
+ };
3088
+ const visible = () => {
3089
+ if (document.visibilityState === "visible") wake();
3090
+ };
3091
+ if (typeof window !== "undefined") {
3092
+ window.addEventListener("online", wake);
3093
+ window.addEventListener("focus", wake);
3094
+ document.addEventListener("visibilitychange", visible);
3095
+ }
2488
3096
  connect();
2489
3097
  return () => {
2490
3098
  stopped = true;
2491
3099
  clearTimeout(timer);
3100
+ if (typeof window !== "undefined") {
3101
+ window.removeEventListener("online", wake);
3102
+ window.removeEventListener("focus", wake);
3103
+ document.removeEventListener("visibilitychange", visible);
3104
+ }
2492
3105
  if (token) call("disconnect").catch(() => {});
2493
3106
  };
2494
3107
  }
@@ -2515,11 +3128,7 @@ window.__ModuleLoader__.load({
2515
3128
  const [stability, setStability] = (0, react.useState)(0), [flow, setFlow] = (0, react.useState)(100), [picturesOpen, setPicturesOpen] = (0, react.useState)(false);
2516
3129
  const pictureInput = (0, react.useRef)(null), received = (0, react.useRef)(null);
2517
3130
  const navigation = useSketchView(canvas, open);
2518
- const brushWidths = (0, react.useRef)({
2519
- pen: 12,
2520
- pencil: 6,
2521
- marker: 28
2522
- });
3131
+ const toolWidths = (0, react.useRef)({});
2523
3132
  const [revision, redraw] = (0, react.useState)(0), [tool, setTool] = (0, react.useState)("pen"), [brush, setBrush] = (0, react.useState)("pen");
2524
3133
  const [eraser, setEraser] = (0, react.useState)("pixel"), [color, setColor] = (0, react.useState)("#0088ff"), [width, setWidth] = (0, react.useState)(12);
2525
3134
  const [selection, setSelection] = (0, react.useState)(null), [textEdit, setTextEdit] = (0, react.useState)(null), [shapesOpen, setShapesOpen] = (0, react.useState)(false);
@@ -2549,15 +3158,24 @@ window.__ModuleLoader__.load({
2549
3158
  setColor(value);
2550
3159
  if (selected) editObject({ color: value });
2551
3160
  };
2552
- const chooseBrush = (name) => {
2553
- brushWidths.current[brush] = width;
2554
- setWidth(brushWidths.current[name]);
2555
- setBrush(name);
2556
- setTool("pen");
2557
- setSelection(null);
3161
+ const chooseTool = (name, nextBrush = brush) => {
3162
+ setWidth(switchSketchToolWidth(toolWidths.current, {
3163
+ tool,
3164
+ brush,
3165
+ width
3166
+ }, {
3167
+ tool: name,
3168
+ brush: nextBrush
3169
+ }));
3170
+ setBrush(nextBrush);
3171
+ setTool(name);
3172
+ if (name !== "select") setSelection(null);
2558
3173
  };
3174
+ const chooseBrush = (name) => chooseTool("pen", name);
2559
3175
  const [fillShape, setFillShape] = (0, react.useState)(false);
2560
- const [layersOpen, setLayersOpen] = (0, react.useState)(false), [busy, setBusy] = (0, react.useState)(false), [error, setError] = (0, react.useState)("");
3176
+ const [hydrated, setHydrated] = (0, react.useState)(false), [recovered, setRecovered] = (0, react.useState)(false);
3177
+ const [restoreAttempt, retryRestore] = (0, react.useState)(0);
3178
+ const [layersOpen, setLayersOpen] = (0, react.useState)(false), [busy, setBusy] = (0, react.useState)(true), [error, setError] = (0, react.useState)("");
2561
3179
  const cursorRing = (0, react.useRef)(null);
2562
3180
  const cursor = useSketchCursor(canvas, cursorRing, width, tool === "pen" ? brush : "pen", navigation.view.scale, !open || navigation.space || busy || agentLocked || tool === "select" || tool === "text");
2563
3181
  useSketchDismiss(shapesOpen, setShapesOpen, dialog, [".codexSketchShapeMenu", ".codexSketchShapeToggle"]);
@@ -2580,159 +3198,115 @@ window.__ModuleLoader__.load({
2580
3198
  updateUi.current = false;
2581
3199
  redraw((value) => value + 1);
2582
3200
  }
2583
- });
2584
- };
2585
- const checkpoint = () => {
2586
- documentRevision.current++;
2587
- dirty.current = true;
2588
- undo.current.push(doc.current);
2589
- if (undo.current.length > 30) undo.current.shift();
2590
- redo.current = [];
2591
- };
2592
- const change = (action, id, value) => {
2593
- if (busy || agentRun.current?.locked || active.current) return;
2594
- const next = changeSketchLayer(doc.current, action, id, value);
2595
- if (next === doc.current) return;
2596
- if (action !== "select") checkpoint();
2597
- else documentRevision.current++;
2598
- doc.current = next;
2599
- setError("");
2600
- schedule();
2601
- };
2602
- (0, react.useEffect)(() => {
2603
- if (open) {
2604
- dialog.current.showModal();
2605
- canvas.current.width = doc.current.width ?? 1024;
2606
- paint();
2607
- } else dialog.current?.close();
2608
- }, [open]);
2609
- (0, react.useEffect)(() => () => {
2610
- cancelAnimationFrame(frame.current);
2611
- cache.current.clear();
2612
- }, []);
2613
- const hasContent = () => doc.current.layers.some((layer) => layer.image || layer.strokes.length);
2614
- const save = async (name) => {
2615
- const savingDocument = documentId.current, savingRevision = documentRevision.current;
2616
- const row = {
2617
- id: saved.current?.id ?? crypto.randomUUID(),
2618
- name: name?.trim() || saved.current?.name || `${t("sketchTitle")} ${(/* @__PURE__ */ new Date()).toLocaleString()}`,
2619
- updated: Date.now(),
2620
- doc: structuredClone(doc.current)
2621
- };
2622
- await sketchDrafts("save", row);
2623
- if (documentId.current === savingDocument) {
2624
- saved.current = {
2625
- id: row.id,
2626
- name: row.name
2627
- };
2628
- if (documentRevision.current === savingRevision) dirty.current = false;
2629
- }
2630
- };
2631
- const saveChanges = async () => {
2632
- if (dirty.current && (hasContent() || saved.current)) await save();
2633
- };
2634
- const replace = (next, decoded, identity) => {
2635
- documentId.current = crypto.randomUUID();
2636
- documentRevision.current++;
2637
- doc.current = identifyObjects(structuredClone(next));
2638
- setSelection(null);
2639
- setTextEdit(null);
2640
- images.current = decoded;
2641
- cache.current.clear();
2642
- undo.current = [];
2643
- redo.current = [];
2644
- saved.current = identity;
2645
- dirty.current = false;
2646
- schedule();
2647
- };
2648
- const fresh = async () => {
2649
- await saveChanges();
2650
- replace(createSketchLayers(), /* @__PURE__ */ new Map(), null);
2651
- };
2652
- const load = async (row) => {
2653
- if (row.id === saved.current?.id) return;
2654
- await saveChanges();
2655
- const decoded = /* @__PURE__ */ new Map();
2656
- await decodeSketchImages(row.doc, decoded);
2657
- replace(row.doc, decoded, {
2658
- id: row.id,
2659
- name: row.name
2660
- });
2661
- };
2662
- const importImage = async (file) => {
2663
- if (file.name?.toLowerCase().endsWith(".psd") || file.name?.toLowerCase().endsWith(".dsh-sketch.json")) {
2664
- if (file.size > 32 * 1024 * 1024) throw Error("File exceeds 32 MB");
2665
- const next = file.name.toLowerCase().endsWith(".psd") ? await importSketchPsd(file) : decodeSketchDocument(await file.text());
2666
- const decoded = /* @__PURE__ */ new Map();
2667
- await decodeSketchImages(next, decoded);
2668
- await saveChanges();
2669
- replace(next, decoded, null);
2670
- dirty.current = true;
2671
- return;
2672
- }
2673
- if (doc.current.layers.length >= 8) throw Error("Layer limit");
2674
- const image = await importSketchImage(file), w = doc.current.width ?? 1024, h = doc.current.height ?? 1024;
2675
- const scale = Math.min(w / image.width, h / image.height), width = image.width * scale / w, height = image.height * scale / h;
2676
- const layer = {
2677
- id: doc.current.nextId,
2678
- name: file.name?.slice(0, 40) || t("sketchImport"),
2679
- visible: true,
2680
- strokes: [],
2681
- image: {
2682
- src: image.src,
2683
- x: (1 - width) / 2,
2684
- y: (1 - height) / 2,
2685
- width,
2686
- height
2687
- }
2688
- };
2689
- await decodeSketchImages({ layers: [layer] }, images.current);
2690
- checkpoint();
2691
- doc.current = {
2692
- ...doc.current,
2693
- nextId: layer.id + 1,
2694
- active: layer.id,
2695
- layers: [...doc.current.layers, layer]
2696
- };
3201
+ });
3202
+ };
3203
+ const checkpoint = () => {
3204
+ documentRevision.current++;
3205
+ dirty.current = true;
3206
+ undo.current.push(doc.current);
3207
+ if (undo.current.length > 30) undo.current.shift();
3208
+ redo.current = [];
3209
+ };
3210
+ const change = (action, id, value) => {
3211
+ if (busy || agentRun.current?.locked || active.current) return;
3212
+ const next = changeSketchLayer(doc.current, action, id, value);
3213
+ if (next === doc.current) return;
3214
+ if (action !== "select") checkpoint();
3215
+ else documentRevision.current++;
3216
+ doc.current = next;
3217
+ setError("");
2697
3218
  schedule();
2698
3219
  };
2699
- const close = async () => {
2700
- if (agentRun.current?.locked) {
3220
+ (0, react.useEffect)(() => {
3221
+ if (open) {
3222
+ dialog.current.showModal();
3223
+ canvas.current.width = doc.current.width ?? 1024;
3224
+ paint();
3225
+ } else dialog.current?.close();
3226
+ }, [open]);
3227
+ (0, react.useEffect)(() => () => {
3228
+ cancelAnimationFrame(frame.current);
3229
+ cache.current.clear();
3230
+ }, []);
3231
+ const { save, saveChanges, fresh, load, importImage, restore } = createSketchDocumentLifecycle(localSession.current, {
3232
+ sessionId,
3233
+ t,
3234
+ schedule,
3235
+ checkpoint,
3236
+ cache,
3237
+ setSelection,
3238
+ setTextEdit,
3239
+ setRecovered
3240
+ });
3241
+ (0, react.useEffect)(() => localSession.current.retain?.(), []);
3242
+ (0, react.useEffect)(() => {
3243
+ let live = true;
3244
+ setHydrated(false);
3245
+ setBusy(true);
3246
+ setError("");
3247
+ (enabled ? restore(() => live) : Promise.resolve()).then(() => {
3248
+ if (live) {
3249
+ setHydrated(true);
3250
+ setBusy(false);
3251
+ }
3252
+ }).catch((error) => {
3253
+ if (live) setError(t(error.code === "SKETCH_STORAGE_BLOCKED" ? "sketchStorageBlocked" : "sketchStorageFailed"));
3254
+ });
3255
+ return () => {
3256
+ live = false;
3257
+ };
3258
+ }, [
3259
+ enabled,
3260
+ sessionId,
3261
+ restoreAttempt
3262
+ ]);
3263
+ (0, react.useEffect)(() => {
3264
+ if (!hydrated || !enabled || !dirty.current || busy || agentLocked) return;
3265
+ const timer = setTimeout(() => {
3266
+ if (active.current || sizeGesture.current || !dirty.current) return;
3267
+ sketchDrafts("checkpoint", {
3268
+ id: sessionId,
3269
+ updated: Date.now(),
3270
+ doc: structuredClone(doc.current)
3271
+ }).catch(() => setError(t("sketchRecoveryFailed")));
3272
+ }, 1500);
3273
+ return () => clearTimeout(timer);
3274
+ }, [
3275
+ revision,
3276
+ hydrated,
3277
+ enabled,
3278
+ busy,
3279
+ agentLocked,
3280
+ sessionId
3281
+ ]);
3282
+ const close = () => {
3283
+ if (!hydrated || agentRun.current?.locked) {
2701
3284
  onClose();
2702
3285
  return;
2703
3286
  }
2704
- if (busy || active.current) return;
2705
- setBusy(true);
2706
- try {
3287
+ return runFile(async () => {
2707
3288
  await saveChanges();
2708
3289
  onClose();
2709
- } catch {
2710
- setError(t("sketchStorageFailed"));
2711
- } finally {
2712
- setBusy(false);
2713
- }
2714
- };
2715
- const runFile = async (operation) => {
2716
- if (busy || agentRun.current?.locked || active.current) return;
2717
- setBusy(true);
2718
- setError("");
2719
- try {
2720
- await operation();
2721
- } catch {
2722
- setError(t("sketchStorageFailed"));
2723
- } finally {
2724
- setBusy(false);
2725
- }
3290
+ });
2726
3291
  };
3292
+ const operationGate = (0, react.useRef)(null);
3293
+ operationGate.current ??= createSketchOperationGate();
3294
+ const runFile = (operation) => operationGate.current.run(operation, {
3295
+ blocked: busy || agentRun.current?.locked || Boolean(active.current),
3296
+ working: setBusy,
3297
+ report: (error) => setError(error ? error?.message || t("sketchStorageFailed") : "")
3298
+ });
2727
3299
  (0, react.useEffect)(() => {
2728
- if (open && !agentLocked && incoming && incoming !== received.current) {
3300
+ if (open && hydrated && !busy && !agentLocked && incoming && incoming !== received.current) {
2729
3301
  received.current = incoming;
2730
3302
  runFile(() => importImage(incoming.file));
2731
3303
  }
2732
3304
  }, [
2733
3305
  open,
2734
3306
  incoming,
2735
- agentLocked
3307
+ agentLocked,
3308
+ hydrated,
3309
+ busy
2736
3310
  ]);
2737
3311
  const keyDown = (event) => {
2738
3312
  if (event.target.closest("input,textarea,select,[contenteditable=true]") || event.isComposing || busy || active.current) return;
@@ -2744,14 +3318,6 @@ window.__ModuleLoader__.load({
2744
3318
  editObject({}, "delete");
2745
3319
  return;
2746
3320
  }
2747
- if (!event.ctrlKey && !event.metaKey && !event.altKey && event.key.toLowerCase() === "v") {
2748
- setTool("select");
2749
- return;
2750
- }
2751
- if (!event.ctrlKey && !event.metaKey && !event.altKey && event.key.toLowerCase() === "t") {
2752
- setTool("text");
2753
- return;
2754
- }
2755
3321
  const key = event.key.toLowerCase(), command = event.ctrlKey || event.metaKey;
2756
3322
  if (command && [
2757
3323
  "z",
@@ -2765,20 +3331,24 @@ window.__ModuleLoader__.load({
2765
3331
  return;
2766
3332
  }
2767
3333
  if (command || event.altKey) return;
2768
- const tools = Object.fromEntries([
3334
+ const action = sketchShortcutAction(navigation.keys, key);
3335
+ if ([
2769
3336
  "pen",
2770
3337
  "eraser",
2771
3338
  "line",
2772
3339
  "rectangle",
2773
- "circle"
2774
- ].map((action) => [navigation.keys[action], action]));
2775
- if (tools[key]) {
3340
+ "circle",
3341
+ "select",
3342
+ "text"
3343
+ ].includes(action)) {
2776
3344
  event.preventDefault();
2777
- setTool(tools[key]);
3345
+ chooseTool(action);
2778
3346
  }
2779
3347
  if (key === "[" || key === "]") {
2780
3348
  event.preventDefault();
2781
- setWidth((value) => Math.max(2, Math.min(64, value + (key === "]" ? 2 : -2))));
3349
+ const value = stepSketchWidth(selected?.width ?? width, key === "]" ? 1 : -1);
3350
+ if (selected) editObject({ width: value });
3351
+ else setWidth(value);
2782
3352
  }
2783
3353
  };
2784
3354
  const current = doc.current.layers.find((layer) => layer.id === doc.current.active);
@@ -2787,81 +3357,14 @@ window.__ModuleLoader__.load({
2787
3357
  const gesture = active.current;
2788
3358
  if (!gesture || gesture.id !== event.pointerId || busy) return;
2789
3359
  const rect = bounds ?? canvas.current.getBoundingClientRect();
2790
- if (gesture.object) {
2791
- const point = sketchPoint(event.clientX, event.clientY, rect);
2792
- if (!point) return;
2793
- try {
2794
- const box = objectBounds(gesture.object);
2795
- const stroke = gesture.handle === "end" ? {
2796
- ...gesture.object,
2797
- points: [gesture.object.points[0], point]
2798
- } : gesture.handle === "size" ? transformObject(gesture.object, {
2799
- scaleX: Math.max(.001, point.x - box.x) / Math.max(.001, box.width),
2800
- scaleY: Math.max(.001, point.y - box.y) / Math.max(.001, box.height)
2801
- }) : transformObject(gesture.object, {
2802
- dx: point.x - gesture.start.x,
2803
- dy: point.y - gesture.start.y
2804
- });
2805
- doc.current = {
2806
- ...doc.current,
2807
- layers: doc.current.layers.map((l) => l.id === gesture.layer ? {
2808
- ...l,
2809
- strokes: l.strokes.map((s) => s.id === gesture.object.id ? stroke : s)
2810
- } : l)
2811
- };
2812
- schedule();
2813
- } catch {}
2814
- return;
2815
- }
2816
3360
  const native = event.nativeEvent ?? event;
2817
- const events = native.getCoalescedEvents?.() ?? [];
2818
- for (const sample of events.length ? [...events, native] : [native]) {
2819
- let point = sketchPoint(sample.clientX, sample.clientY, rect);
2820
- if (!point) continue;
2821
- const layer = doc.current.layers.find((layer) => layer.id === gesture.layer);
2822
- if (gesture.eraseStroke) {
2823
- const previous = gesture.last ?? point;
2824
- const steps = Math.min(256, Math.max(1, Math.ceil(Math.hypot(point.x - previous.x, point.y - previous.y) * SKETCH_SIZE / Math.max(2, width / 2))));
2825
- layer.strokes = layer.strokes.filter((stroke) => {
2826
- for (let i = 1; i <= steps; i++) if (strokeHit(stroke, {
2827
- x: previous.x + (point.x - previous.x) * i / steps,
2828
- y: previous.y + (point.y - previous.y) * i / steps
2829
- }, width / 2, doc.current.width, doc.current.height)) return false;
2830
- return true;
2831
- });
2832
- } else {
2833
- const stroke = layer.strokes.at(-1);
2834
- if ([
2835
- "line",
2836
- "arrow",
2837
- "rectangle",
2838
- "circle"
2839
- ].includes(stroke.shape)) {
2840
- if (stroke.shape === "line" && event.shiftKey) {
2841
- const w = doc.current.width ?? 1024, h = doc.current.height ?? 1024, a = stroke.points[0];
2842
- const snapped = snapLine({
2843
- x: a.x * w,
2844
- y: a.y * h
2845
- }, {
2846
- x: point.x * w,
2847
- y: point.y * h
2848
- });
2849
- point = {
2850
- x: snapped.x / w,
2851
- y: snapped.y / h
2852
- };
2853
- }
2854
- stroke.points = [stroke.points[0], point];
2855
- } else {
2856
- const last = stroke.points.at(-1);
2857
- if (Math.hypot(last.x - point.x, last.y - point.y) < 1e-4) continue;
2858
- if (stroke.points.length >= 2e3) stroke.points = stroke.points.filter((_, i) => i % 2 === 0);
2859
- stroke.points.push(point);
2860
- }
2861
- }
2862
- gesture.last = point;
3361
+ const samples = native.getCoalescedEvents?.() ?? [];
3362
+ try {
3363
+ doc.current = updateSketchGesture(doc.current, gesture, samples.length ? [...samples, native] : [native], rect, width, event.shiftKey);
3364
+ schedule(Boolean(gesture.object));
3365
+ } catch (error) {
3366
+ setError(error?.message || t("sketchFailed"));
2863
3367
  }
2864
- schedule(false);
2865
3368
  };
2866
3369
  const end = (event, cancel = false) => {
2867
3370
  if (navigation.end(event)) return;
@@ -2884,7 +3387,7 @@ window.__ModuleLoader__.load({
2884
3387
  layer: drawn.layer,
2885
3388
  id: stroke.id
2886
3389
  });
2887
- setTool("select");
3390
+ chooseTool("select");
2888
3391
  }
2889
3392
  }
2890
3393
  active.current = null;
@@ -2910,7 +3413,7 @@ window.__ModuleLoader__.load({
2910
3413
  available: () => enabled && agentEnabled,
2911
3414
  previewEnabled: () => agentPreview,
2912
3415
  open: () => onOpen(),
2913
- busy: () => busy || Boolean(active.current) || Boolean(textEdit) || sizeGesture.current,
3416
+ busy: () => operationGate.current.running || busy || Boolean(active.current) || Boolean(textEdit) || sizeGesture.current,
2914
3417
  document: () => doc.current,
2915
3418
  snapshot: () => ({
2916
3419
  documentId: documentId.current,
@@ -2957,23 +3460,18 @@ window.__ModuleLoader__.load({
2957
3460
  previewEnabled: () => agentAdapter.current.previewEnabled()
2958
3461
  });
2959
3462
  (0, react.useEffect)(() => {
2960
- if (!enabled || !agentEnabled) return;
3463
+ if (!enabled || !agentEnabled || !hydrated) return;
2961
3464
  const api = Object.freeze({
2962
3465
  version: 2,
2963
3466
  sessionId,
2964
3467
  execute: (request) => agentRun.current.execute(request),
2965
- export: async (format) => {
2966
- if (agentAdapter.current.busy() || agentRun.current.locked) throw Error("Sketch is being edited");
2967
- const { blob, extension } = await agentAdapter.current.export(format);
2968
- const data = new Uint8Array(await blob.arrayBuffer());
2969
- let raw = "";
2970
- for (let i = 0; i < data.length; i += 8192) raw += String.fromCharCode(...data.subarray(i, i + 8192));
2971
- return {
2972
- extension,
2973
- mediaType: blob.type,
2974
- base64: btoa(raw)
2975
- };
2976
- }
3468
+ export: (format) => exportSketchAgentFile(format, {
3469
+ gate: operationGate.current,
3470
+ blocked: agentAdapter.current.busy() || agentRun.current.locked,
3471
+ working: setBusy,
3472
+ report: (error) => setError(error ? error.message || t("sketchFailed") : ""),
3473
+ exportFile: (value) => agentAdapter.current.export(value)
3474
+ })
2977
3475
  });
2978
3476
  window.dshSketchAgent = api;
2979
3477
  return () => {
@@ -2983,14 +3481,15 @@ window.__ModuleLoader__.load({
2983
3481
  enabled,
2984
3482
  agentEnabled,
2985
3483
  rpc,
2986
- sessionId
3484
+ sessionId,
3485
+ hydrated
2987
3486
  ]);
2988
3487
  (0, react.useEffect)(() => {
2989
3488
  if (!enabled || !agentEnabled) {
2990
3489
  if (agentRun.current.locked) agentRun.current.stop();
2991
3490
  return;
2992
3491
  }
2993
- if (!rpc || !sessionId) return;
3492
+ if (!rpc || !sessionId || !hydrated) return;
2994
3493
  let live = true;
2995
3494
  const disconnect = connectSketchAgent(rpc, sessionId, (request) => {
2996
3495
  if (!live) throw Error("Sketch session disconnected");
@@ -2998,7 +3497,7 @@ window.__ModuleLoader__.load({
2998
3497
  }, (message) => {
2999
3498
  agentRun.current.fail();
3000
3499
  setError(message);
3001
- }, () => 350);
3500
+ }, () => agentRun.current.locked ? 350 : 2e3);
3002
3501
  return () => {
3003
3502
  live = false;
3004
3503
  disconnect();
@@ -3007,23 +3506,18 @@ window.__ModuleLoader__.load({
3007
3506
  enabled,
3008
3507
  agentEnabled,
3009
3508
  rpc,
3010
- sessionId
3509
+ sessionId,
3510
+ hydrated
3011
3511
  ]);
3012
- const attach = async () => {
3013
- if (!enabled || busy || agentRun.current?.locked) return;
3014
- setBusy(true);
3015
- setError("");
3016
- try {
3512
+ const attach = () => {
3513
+ if (!enabled) return;
3514
+ return runFile(async () => {
3017
3515
  paint();
3018
- const blob = await new Promise((resolve, reject) => canvas.current.toBlob((blob) => blob ? resolve(blob) : reject(Error("PNG")), "image/png"));
3516
+ const blob = await new Promise((resolve, reject) => canvas.current.toBlob((blob) => blob ? resolve(blob) : reject(Error(t("sketchFailed"))), "image/png"));
3019
3517
  await saveChanges();
3020
3518
  await attachSketch(blob);
3021
3519
  onClose();
3022
- } catch {
3023
- setError(t("sketchFailed"));
3024
- } finally {
3025
- setBusy(false);
3026
- }
3520
+ });
3027
3521
  };
3028
3522
  const exportFile = async (format = "png") => {
3029
3523
  paint();
@@ -3043,8 +3537,8 @@ window.__ModuleLoader__.load({
3043
3537
  };
3044
3538
  agentAdapter.current.export = exportFile;
3045
3539
  const download = async (format) => {
3540
+ setError("");
3046
3541
  const { blob, extension } = await exportFile(format);
3047
- await saveChanges();
3048
3542
  const url = URL.createObjectURL(blob), link = document.createElement("a");
3049
3543
  link.href = url;
3050
3544
  link.download = `${(saved.current?.name || "sketch").replace(/[\\/:*?"<>|\u0000-\u001f]/g, "-").slice(0, 80)}.${extension}`;
@@ -3110,7 +3604,7 @@ window.__ModuleLoader__.load({
3110
3604
  type: "button",
3111
3605
  "aria-label": t("sketchCancel"),
3112
3606
  title: t("sketchCancel"),
3113
- disabled: busy && !agentLocked,
3607
+ disabled: busy && hydrated && !agentLocked,
3114
3608
  onClick: close,
3115
3609
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(WorkspaceIcon, { name: "close" })
3116
3610
  }),
@@ -3123,8 +3617,7 @@ window.__ModuleLoader__.load({
3123
3617
  hasContent: doc.current.layers.some((l) => l.visible && (l.image || l.strokes.length)),
3124
3618
  disabled: agentLocked || busy,
3125
3619
  t,
3126
- report: setError,
3127
- onWorking: setBusy
3620
+ runOperation: runFile
3128
3621
  }),
3129
3622
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3130
3623
  className: "codexSketchHeading",
@@ -3215,6 +3708,19 @@ window.__ModuleLoader__.load({
3215
3708
  },
3216
3709
  onDismiss: () => setNoticeHidden(true)
3217
3710
  }) : null,
3711
+ recovered ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3712
+ className: "codexSketchAgentStatus",
3713
+ role: "status",
3714
+ children: [t("sketchRecovered"), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
3715
+ type: "button",
3716
+ onClick: () => setRecovered(false),
3717
+ "aria-label": t("sketchDismissStatus"),
3718
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(WorkspaceIcon, {
3719
+ name: "close",
3720
+ size: 14
3721
+ })
3722
+ })]
3723
+ }) : null,
3218
3724
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3219
3725
  className: `codexLayerBody ${layersOpen ? "withLayers" : ""}`,
3220
3726
  children: [
@@ -3405,13 +3911,10 @@ window.__ModuleLoader__.load({
3405
3911
  sizeGesture.current = false;
3406
3912
  },
3407
3913
  onChange: (value) => {
3408
- if (opacityMode) {
3409
- setFlow(value);
3410
- if (selected) editObject({ opacity: value / 100 });
3411
- } else {
3412
- setWidth(value);
3413
- if (selected) editObject({ width: value });
3414
- }
3914
+ if (opacityMode) if (selected) editObject({ opacity: value / 100 });
3915
+ else setFlow(value);
3916
+ else if (selected) editObject({ width: value });
3917
+ else setWidth(value);
3415
3918
  }
3416
3919
  }),
3417
3920
  textEdit ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("form", {
@@ -3447,7 +3950,7 @@ window.__ModuleLoader__.load({
3447
3950
  schedule();
3448
3951
  }
3449
3952
  setTextEdit(null);
3450
- setTool("select");
3953
+ chooseTool("select");
3451
3954
  } catch (e) {
3452
3955
  setError(e.message);
3453
3956
  }
@@ -3516,89 +4019,11 @@ window.__ModuleLoader__.load({
3516
4019
  !doc.current.layers.some((layer) => layer.image) ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("small", { children: t("sketchPicturesEmpty") }) : null
3517
4020
  ]
3518
4021
  }) : null,
3519
- layersOpen ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("aside", {
3520
- className: "codexSketchLayers",
3521
- "aria-label": t("sketchLayers"),
3522
- children: [
3523
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("header", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("strong", { children: t("sketchLayers") }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
3524
- type: "button",
3525
- title: t("sketchLayerAdd"),
3526
- "aria-label": t("sketchLayerAdd"),
3527
- disabled: agentLocked || busy || doc.current.layers.length >= 8,
3528
- onClick: () => change("add"),
3529
- children: "+"
3530
- })] }),
3531
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
3532
- className: "codexLayerList",
3533
- children: doc.current.layers.slice().reverse().map((layer) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3534
- className: "codexLayerRow",
3535
- "data-active": layer.id === doc.current.active,
3536
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
3537
- type: "button",
3538
- disabled: agentLocked || busy,
3539
- "aria-label": `${t("sketchLayerVisible")} ${layer.id}`,
3540
- "aria-pressed": layer.visible,
3541
- onClick: () => change("visible", layer.id),
3542
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(WorkspaceIcon, {
3543
- name: layer.visible ? "eye" : "eyeOff",
3544
- size: 18
3545
- })
3546
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
3547
- type: "button",
3548
- disabled: agentLocked || busy,
3549
- "aria-pressed": layer.id === doc.current.active,
3550
- onClick: () => change("select", layer.id),
3551
- children: layer.name || `${t("sketchLayer")} ${layer.id}`
3552
- })]
3553
- }, layer.id))
3554
- }),
3555
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("label", {
3556
- className: "codexSketchLayerLabel",
3557
- children: t("sketchLayerName")
3558
- }),
3559
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
3560
- disabled: agentLocked || busy,
3561
- "aria-label": t("sketchLayerName"),
3562
- defaultValue: current.name,
3563
- placeholder: `${t("sketchLayer")} ${current.id}`,
3564
- maxLength: 40,
3565
- onBlur: (e) => {
3566
- if (e.target.value !== current.name) change("rename", current.id, e.target.value);
3567
- },
3568
- onKeyDown: (e) => {
3569
- if (e.key === "Enter") e.currentTarget.blur();
3570
- }
3571
- }, current.id + "-" + current.name),
3572
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
3573
- className: "codexLayerActions",
3574
- children: [
3575
- "duplicate",
3576
- "up",
3577
- "down",
3578
- "delete"
3579
- ].map((action) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
3580
- type: "button",
3581
- title: t(`sketchLayer_${action}`),
3582
- "aria-label": t(`sketchLayer_${action}`),
3583
- disabled: agentLocked || busy || action === "delete" && doc.current.layers.length === 1 || action === "duplicate" && (doc.current.layers.length >= 8 || strokeCount(doc.current) + current.strokes.length > 2e3) || action === "up" && current === doc.current.layers.at(-1) || action === "down" && current === doc.current.layers[0],
3584
- onClick: () => change(action),
3585
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(WorkspaceIcon, {
3586
- name: action === "delete" ? "clear" : action,
3587
- size: 17
3588
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t(`sketchLayer_${action}`) })]
3589
- }, action))
3590
- }),
3591
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
3592
- type: "button",
3593
- className: "codexSketchClearLayer",
3594
- disabled: agentLocked || busy || !current.strokes.length && !current.image,
3595
- onClick: () => change("clear"),
3596
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(WorkspaceIcon, {
3597
- name: "clear",
3598
- size: 16
3599
- }), t("sketchClearLayer")]
3600
- })
3601
- ]
4022
+ layersOpen ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SketchLayerPanel, {
4023
+ document: doc.current,
4024
+ disabled: agentLocked || busy,
4025
+ change,
4026
+ t
3602
4027
  }) : null
3603
4028
  ]
3604
4029
  }),
@@ -3616,92 +4041,15 @@ window.__ModuleLoader__.load({
3616
4041
  navigation,
3617
4042
  t
3618
4043
  }),
3619
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3620
- className: "codexSketchPill",
3621
- role: "toolbar",
3622
- "aria-label": t("sketchTitle"),
3623
- title: t("sketchShortcuts"),
3624
- children: [
3625
- [
3626
- "select",
3627
- "pen",
3628
- "pencil",
3629
- "marker",
3630
- "text",
3631
- "eraser"
3632
- ].map((name) => {
3633
- const drawing = [
3634
- "pen",
3635
- "pencil",
3636
- "marker"
3637
- ].includes(name);
3638
- const label = t(drawing ? `sketchBrush_${name}` : `sketchTool_${name}`);
3639
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
3640
- type: "button",
3641
- "aria-label": label,
3642
- title: drawing ? `${label} · ${t(`sketchBrushHint_${name}`)}` : label,
3643
- "aria-pressed": drawing ? tool === "pen" && brush === name : tool === name,
3644
- disabled: agentLocked || busy,
3645
- onClick: () => {
3646
- if (drawing) chooseBrush(name);
3647
- else {
3648
- setTool(name);
3649
- if (name !== "select") setSelection(null);
3650
- }
3651
- },
3652
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(WorkspaceIcon, {
3653
- name,
3654
- size: 23
3655
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: label })]
3656
- }, name);
3657
- }),
3658
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
3659
- className: "codexSketchShapeToggle",
3660
- type: "button",
3661
- "aria-label": t("sketchShapes"),
3662
- "aria-expanded": shapesOpen,
3663
- "aria-pressed": [
3664
- "line",
3665
- "arrow",
3666
- "rectangle",
3667
- "circle"
3668
- ].includes(tool),
3669
- disabled: agentLocked || busy,
3670
- onClick: () => setShapesOpen((v) => !v),
3671
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(WorkspaceIcon, {
3672
- name: [
3673
- "line",
3674
- "arrow",
3675
- "rectangle",
3676
- "circle"
3677
- ].includes(tool) ? tool : "rectangle",
3678
- size: 23
3679
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t([
3680
- "line",
3681
- "arrow",
3682
- "rectangle",
3683
- "circle"
3684
- ].includes(tool) ? `sketchTool_${tool}` : "sketchShapes") })]
3685
- }),
3686
- shapesOpen ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
3687
- className: "codexSketchShapeMenu",
3688
- children: [
3689
- "line",
3690
- "arrow",
3691
- "rectangle",
3692
- "circle"
3693
- ].map((name) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
3694
- type: "button",
3695
- "aria-pressed": tool === name,
3696
- onClick: () => {
3697
- setTool(name);
3698
- setSelection(null);
3699
- setShapesOpen(false);
3700
- },
3701
- children: t(`sketchTool_${name}`)
3702
- }, name))
3703
- }) : null
3704
- ]
4044
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SketchToolPicker, {
4045
+ t,
4046
+ disabled: agentLocked || busy,
4047
+ tool,
4048
+ brush,
4049
+ chooseBrush,
4050
+ chooseTool,
4051
+ shapesOpen,
4052
+ setShapesOpen
3705
4053
  }),
3706
4054
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3707
4055
  className: "codexLayerBrush",
@@ -3793,10 +4141,14 @@ window.__ModuleLoader__.load({
3793
4141
  })
3794
4142
  ]
3795
4143
  }),
3796
- error ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
4144
+ error ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
3797
4145
  className: "codexSketchHint",
3798
4146
  role: "alert",
3799
- children: error
4147
+ children: [error, !hydrated ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
4148
+ type: "button",
4149
+ onClick: () => retryRestore((value) => value + 1),
4150
+ children: t("accountRetry")
4151
+ }) : null]
3800
4152
  }) : null
3801
4153
  ]
3802
4154
  })] });
@@ -3999,6 +4351,19 @@ window.__ModuleLoader__.load({
3999
4351
  //#endregion
4000
4352
  //#region src/client-locales.js
4001
4353
  const zh = {
4354
+ advancedModelSearch: "模型与搜索",
4355
+ subagentBackendTitle: "独立子任务",
4356
+ subagentBackend_dsh: "DSH",
4357
+ subagentBackend_codex: "Codex",
4358
+ connectionTitle: "连接方式",
4359
+ connectionHint: "默认 SSE。WebSocket 实验性复用连接与上下文传输,跟随现有代理;连接失败可回退 SSE。下次请求生效,不扩大上下文容量。",
4360
+ subagentBackendHint: "Codex 复用订阅登录,跟随当前订阅模型和工作区权限;其他模型会话使用 Luna low。共享上下文子任务仍用 DSH。",
4361
+ subagentBackendUnavailable: "当前宿主缺少子代理服务,请更新 DSH。",
4362
+ sketchRecovered: "已恢复未保存草稿",
4363
+ sketchRecoveryFailed: "恢复检查点保存失败,请手动保存或导出草稿。",
4364
+ sketchStorageBlocked: "草稿正被其他窗口占用,请关闭其他草图窗口后重试。",
4365
+ sketchDraftLimit: "已达 20 份草稿上限。请先导出,或删除不需要的草稿后保存。",
4366
+ sketchStorageLimit: "草稿存储空间已满。请先导出,或删除不需要的草稿后保存。",
4002
4367
  sketchSizeShort: "粗细",
4003
4368
  sketchObjectDuplicate: "复制对象",
4004
4369
  sketchObjectDelete: "删除对象",
@@ -4410,6 +4775,19 @@ window.__ModuleLoader__.load({
4410
4775
  imageRemoveAnnotation: "删除标注"
4411
4776
  };
4412
4777
  const en = {
4778
+ advancedModelSearch: "Models and search",
4779
+ subagentBackendTitle: "Independent subtasks",
4780
+ subagentBackend_dsh: "DSH",
4781
+ subagentBackend_codex: "Codex",
4782
+ connectionTitle: "Connection",
4783
+ connectionHint: "SSE by default. Experimental WebSocket reuses connections and context transfers, follows your proxy, and can fall back to SSE on connection failure. Applies to the next request; context limits stay the same.",
4784
+ subagentBackendHint: "Codex uses your subscription login, current subscription model and workspace permissions; other model sessions use Luna low. Shared-context subtasks stay in DSH.",
4785
+ subagentBackendUnavailable: "Subagent services are unavailable. Update DSH to use this option.",
4786
+ sketchRecovered: "Unsaved sketch recovered",
4787
+ sketchRecoveryFailed: "Recovery checkpoint failed. Save or export your draft.",
4788
+ sketchStorageBlocked: "Draft storage is in use. Close other sketch windows and retry.",
4789
+ sketchDraftLimit: "The 20-draft limit is reached. Export first, or remove an unwanted draft before saving.",
4790
+ sketchStorageLimit: "Draft storage is full. Export first, or remove an unwanted draft before saving.",
4413
4791
  sketchSizeShort: "Size",
4414
4792
  sketchObjectDuplicate: "Duplicate object",
4415
4793
  sketchObjectDelete: "Delete object",
@@ -4823,6 +5201,15 @@ window.__ModuleLoader__.load({
4823
5201
  //#endregion
4824
5202
  //#region src/client-styles.js
4825
5203
  const STYLE = `
5204
+ .codexSubscriptionAdvancedPreferences{display:flex;flex-direction:column;gap:10px}
5205
+ .codexSubscriptionSearchChoices.codexSubscriptionQuotaModes{display:flex;flex:0 0 auto;gap:0;grid-template-columns:none}
5206
+ .codexSubscriptionSettingsDisclosure>summary{display:flex;align-items:center;gap:10px;min-height:28px;cursor:pointer;list-style:none;font-size:14px;font-weight:500}
5207
+ .codexSubscriptionSettingsDisclosure>summary::-webkit-details-marker{display:none}
5208
+ .codexSubscriptionSettingsDisclosure>summary>.codexSubscriptionPreferenceHint{margin-left:auto;font-weight:400}
5209
+ .codexSubscriptionSettingsDisclosure>summary>svg{flex:none;transition:transform .15s}
5210
+ .codexSubscriptionSettingsDisclosure[open]>summary>svg{transform:rotate(180deg)}
5211
+ .codexSubscriptionSettingsDisclosure>summary:focus-visible{outline:2px solid var(--dsw-alias-border-l3);outline-offset:4px;border-radius:4px}
5212
+ .codexSubscriptionSettingsDisclosureBody{padding-top:8px;margin-top:8px;border-top:1px solid var(--dsw-alias-border-l2)}
4826
5213
  .codexComposerQuota[data-warning=true]{color:var(--dsw-alias-state-error-primary)}
4827
5214
  .codexComposerQuota[data-warning=true] progress{accent-color:var(--dsw-alias-state-error-primary)}
4828
5215
  .codexComposerQuota[data-warning=true] progress::-webkit-progress-value{background:var(--dsw-alias-state-error-primary)}
@@ -4875,20 +5262,9 @@ window.__ModuleLoader__.load({
4875
5262
  .codexSubscriptionContextModelCopy span{font-size:11px;line-height:16px;color:var(--dsw-alias-label-tertiary)}
4876
5263
  .codexSubscriptionContextInput{width:116px}
4877
5264
  .codexSubscriptionSearch{display:flex;flex-direction:column;gap:7px}
4878
- .codexSubscriptionSearchChoices{display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:6px}
4879
- .codexSubscriptionSearchChoice{display:grid;grid-template-columns:14px minmax(0,1fr);align-items:center;column-gap:8px;min-width:0;border:1px solid var(--dsw-alias-border-l2);border-radius:10px;background:var(--dsw-alias-bg-module-platform);color:var(--dsw-alias-label-primary);padding:9px 10px;text-align:left;cursor:pointer}
4880
- .codexSubscriptionSearchChoice:has(input:disabled){cursor:not-allowed;opacity:.5}
4881
- .codexSubscriptionSearchChoice:has(input:checked){border-color:var(--dsw-alias-label-primary);background:var(--dsw-alias-bg-layer-2)}
4882
- .codexSubscriptionSearchChoice:has(input:focus-visible){outline:2px solid var(--dsw-alias-border-l3);outline-offset:2px}
4883
- .codexSubscriptionSearchInput{width:14px;height:14px;margin:0;accent-color:var(--dsw-alias-label-primary);cursor:inherit}
4884
- .codexSubscriptionSearchCopy{display:block;min-width:0;pointer-events:none}
4885
- .codexSubscriptionSearchCopy strong,.codexSubscriptionSearchCopy span{display:block}
4886
- .codexSubscriptionSearchCopy strong{font-size:12px;line-height:18px;font-weight:500;color:var(--dsw-alias-label-secondary)}
4887
- .codexSubscriptionSearchChoice:has(input:checked) strong{color:var(--dsw-alias-label-primary)}
4888
- .codexSubscriptionSearchCopy span{font-size:11px;line-height:16px;color:var(--dsw-alias-label-tertiary)}
4889
5265
  .codexSubscriptionDivider{height:1px;background:var(--dsw-alias-border-l2)}
4890
5266
  .codexSubscriptionQuotaModes[data-saving=true] .codexSubscriptionQuotaMode:has(input:disabled){cursor:wait;opacity:1}
4891
- .codexSubscriptionSearchChoices[data-saving=true] .codexSubscriptionSearchChoice:has(input:disabled){cursor:wait;opacity:1}
5267
+ .codexSubscriptionSearchChoices[data-saving=true] .codexSubscriptionQuotaMode:has(input:disabled){cursor:wait;opacity:1}
4892
5268
  .codexSubscriptionAccountRow,.codexSubscriptionSectionHead{display:flex;align-items:center;justify-content:space-between;gap:12px}
4893
5269
  .codexSubscriptionStatus{display:flex;align-items:center;gap:8px;font-size:14px;line-height:22px;font-weight:500}
4894
5270
  .codexSubscriptionAccounts{display:flex;flex-direction:column;border-top:1px solid var(--dsw-alias-border-l2)}
@@ -6176,6 +6552,7 @@ window.__ModuleLoader__.load({
6176
6552
  let modelRefreshGeneration = 0;
6177
6553
  let modelRefreshStarted = false;
6178
6554
  let disposed = false;
6555
+ let subagentBackendAvailable = false;
6179
6556
  const sameModels = (left, right) => left.length === right.length && left.every((model, index) => JSON.stringify(model) === JSON.stringify(right[index]));
6180
6557
  const nativeSnapshot = () => scope.getSnapshot();
6181
6558
  const read = () => {
@@ -6189,6 +6566,9 @@ window.__ModuleLoader__.load({
6189
6566
  return Object.freeze({
6190
6567
  status: current.status,
6191
6568
  ...capabilities,
6569
+ connectionMode: value?.connectionMode === "websocket" ? "websocket" : "sse",
6570
+ subagentBackend: value?.subagentBackend === "codex" ? "codex" : "dsh",
6571
+ subagentBackendAvailable,
6192
6572
  quickQuotaMode: normalizeQuickQuotaMode(value?.[QUICK_QUOTA_MODE_FIELD], value?.[LEGACY_QUICK_QUOTA_FIELD]),
6193
6573
  searchProvider: normalizeSearchProvider(value?.[SEARCH_PROVIDER_FIELD]),
6194
6574
  speedMode: normalizeSpeedMode(value?.[SPEED_MODE_FIELD]),
@@ -6222,6 +6602,7 @@ window.__ModuleLoader__.load({
6222
6602
  publish();
6223
6603
  });
6224
6604
  const acceptFallback = (value) => {
6605
+ subagentBackendAvailable = value?.subagentBackendAvailable === true;
6225
6606
  if (!modelRefreshStarted) {
6226
6607
  contextModels = Array.isArray(value?.contextModels) ? value.contextModels : [];
6227
6608
  verbosityModels = Array.isArray(value?.verbosityModels) ? value.verbosityModels : [];
@@ -6232,6 +6613,8 @@ window.__ModuleLoader__.load({
6232
6613
  fallback = {
6233
6614
  status: "ready",
6234
6615
  value: {
6616
+ connectionMode: value?.connectionMode === "websocket" ? "websocket" : "sse",
6617
+ subagentBackend: value?.subagentBackend === "codex" ? "codex" : "dsh",
6235
6618
  ...readCapabilitySettings(value),
6236
6619
  [QUICK_QUOTA_MODE_FIELD]: normalizeQuickQuotaMode(value?.[QUICK_QUOTA_MODE_FIELD], value?.[LEGACY_QUICK_QUOTA_FIELD]),
6237
6620
  [SEARCH_PROVIDER_FIELD]: normalizeSearchProvider(value?.[SEARCH_PROVIDER_FIELD]),
@@ -6255,6 +6638,7 @@ window.__ModuleLoader__.load({
6255
6638
  try {
6256
6639
  const value = unwrap(await rpc.call(CHANNEL, "preferences/status", {}));
6257
6640
  if (current !== generation || disposed) return;
6641
+ subagentBackendAvailable = value?.subagentBackendAvailable === true;
6258
6642
  if (nativeSnapshot().status === "ready") {
6259
6643
  if (!modelRefreshStarted) {
6260
6644
  contextModels = Array.isArray(value?.contextModels) ? value.contextModels : [];
@@ -6315,7 +6699,7 @@ window.__ModuleLoader__.load({
6315
6699
  failedPatch = void 0;
6316
6700
  publish();
6317
6701
  try {
6318
- if (nativeSnapshot().status === "ready") {
6702
+ if (nativeSnapshot().status === "ready" && !Object.hasOwn(patch, "subagentBackend")) {
6319
6703
  for (const [field, value] of entries) {
6320
6704
  if (current !== generation) return;
6321
6705
  await scope.set(field, value);
@@ -6641,7 +7025,7 @@ window.__ModuleLoader__.load({
6641
7025
  }
6642
7026
  //#endregion
6643
7027
  //#region src/version.js
6644
- const PACKAGE_VERSION = "2.1.0";
7028
+ const PACKAGE_VERSION = "2.1.1-beta.1";
6645
7029
  //#endregion
6646
7030
  //#region src/client-recovery.js
6647
7031
  async function recoveryCall(rpc, endpoint, payload = {}, timeoutMs = 1e4) {
@@ -7280,64 +7664,76 @@ window.__ModuleLoader__.load({
7280
7664
  const snapshot = (0, react.useSyncExternalStore)(preference.subscribe, preference.getSnapshot);
7281
7665
  const disabled = snapshot.status !== "ready" || !snapshot.writable || snapshot.saving;
7282
7666
  const active = snapshot.imageGeneration || snapshot.imageEditing;
7283
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
7667
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("section", {
7284
7668
  className: "codexSubscriptionCard codexImageSettings",
7285
7669
  "aria-label": t("imageSettings"),
7286
- children: [
7287
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", { children: t("imageSettings") }),
7288
- Object.keys(IMAGE_SETTING_GROUPS).map((group) => {
7289
- const value = imageGroupValue(snapshot, group);
7290
- return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ImageChoice, {
7291
- label: t(group),
7292
- hint: t(`${group}Hint`),
7293
- value,
7294
- text: t(value === "mixed" ? "imageGroupMixed" : `${group}_${value}`),
7295
- disabled,
7296
- items: ["on", "off"].map((id) => ({
7297
- id,
7298
- label: t(`${group}_${id}`)
7299
- })),
7300
- onSelect: (id) => {
7301
- preference.set(imageGroupPatch(group, id === "on"));
7302
- }
7303
- }, group);
7304
- }),
7305
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
7306
- className: "codexImageDefaultsGroup",
7307
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(ImageChoice, {
7308
- label: t("imageModel"),
7309
- value: snapshot.imageModel,
7310
- text: modelLabel(snapshot.imageModel),
7311
- disabled: disabled || !active,
7312
- items: Object.keys(IMAGE_MODELS).map((id) => ({
7313
- id,
7314
- label: `${modelLabel(id)}${id.includes("2.5") ? ` · ${t("imageExperimental")}` : ""}`
7315
- })),
7316
- onSelect: (imageModel) => {
7317
- preference.set({
7318
- imageModel,
7319
- imageQuality: "auto"
7320
- });
7321
- }
7322
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ImageChoice, {
7323
- label: t("imageQuality"),
7324
- value: snapshot.imageQuality,
7325
- text: t(`imageQuality_${snapshot.imageQuality}`),
7326
- disabled: disabled || !active,
7327
- items: IMAGE_MODELS[snapshot.imageModel].map((id) => ({
7328
- id,
7329
- label: t(`imageQuality_${id}`)
7330
- })),
7331
- onSelect: (imageQuality) => {
7332
- preference.set({ imageQuality });
7333
- }
7334
- })]
7335
- }),
7336
- snapshot.imageModel.includes("2.5") ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
7337
- className: "codexSubscriptionPreferenceHint",
7338
- children: t("imageModelHint")
7339
- }) : null
7340
- ]
7670
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("details", {
7671
+ className: "codexSubscriptionSettingsDisclosure",
7672
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("summary", { children: [
7673
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("imageSettings") }),
7674
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
7675
+ className: "codexSubscriptionPreferenceHint",
7676
+ children: active ? modelLabel(snapshot.imageModel) : t("imageCapability_off")
7677
+ }),
7678
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconChevronDownOutline14, {})
7679
+ ] }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
7680
+ className: "codexSubscriptionSettingsDisclosureBody",
7681
+ children: [
7682
+ Object.keys(IMAGE_SETTING_GROUPS).map((group) => {
7683
+ const value = imageGroupValue(snapshot, group);
7684
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ImageChoice, {
7685
+ label: t(group),
7686
+ hint: t(`${group}Hint`),
7687
+ value,
7688
+ text: t(value === "mixed" ? "imageGroupMixed" : `${group}_${value}`),
7689
+ disabled,
7690
+ items: ["on", "off"].map((id) => ({
7691
+ id,
7692
+ label: t(`${group}_${id}`)
7693
+ })),
7694
+ onSelect: (id) => {
7695
+ preference.set(imageGroupPatch(group, id === "on"));
7696
+ }
7697
+ }, group);
7698
+ }),
7699
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
7700
+ className: "codexImageDefaultsGroup",
7701
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(ImageChoice, {
7702
+ label: t("imageModel"),
7703
+ value: snapshot.imageModel,
7704
+ text: modelLabel(snapshot.imageModel),
7705
+ disabled: disabled || !active,
7706
+ items: Object.keys(IMAGE_MODELS).map((id) => ({
7707
+ id,
7708
+ label: `${modelLabel(id)}${id.includes("2.5") ? ` · ${t("imageExperimental")}` : ""}`
7709
+ })),
7710
+ onSelect: (imageModel) => {
7711
+ preference.set({
7712
+ imageModel,
7713
+ imageQuality: "auto"
7714
+ });
7715
+ }
7716
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ImageChoice, {
7717
+ label: t("imageQuality"),
7718
+ value: snapshot.imageQuality,
7719
+ text: t(`imageQuality_${snapshot.imageQuality}`),
7720
+ disabled: disabled || !active,
7721
+ items: IMAGE_MODELS[snapshot.imageModel].map((id) => ({
7722
+ id,
7723
+ label: t(`imageQuality_${id}`)
7724
+ })),
7725
+ onSelect: (imageQuality) => {
7726
+ preference.set({ imageQuality });
7727
+ }
7728
+ })]
7729
+ }),
7730
+ snapshot.imageModel.includes("2.5") ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
7731
+ className: "codexSubscriptionPreferenceHint",
7732
+ children: t("imageModelHint")
7733
+ }) : null
7734
+ ]
7735
+ })]
7736
+ })
7341
7737
  });
7342
7738
  }
7343
7739
  //#endregion
@@ -7552,10 +7948,9 @@ window.__ModuleLoader__.load({
7552
7948
  function SearchProviderPreference({ preference, t }) {
7553
7949
  const snapshot = usePreferenceSnapshot(preference);
7554
7950
  const writable = snapshot.status === "ready" && snapshot.writable === true;
7555
- const choice = (value, label, hint) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
7556
- className: "codexSubscriptionSearchChoice",
7951
+ const choice = (value, label) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
7952
+ className: "codexSubscriptionQuotaMode",
7557
7953
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
7558
- className: "codexSubscriptionSearchInput",
7559
7954
  type: "radio",
7560
7955
  name: "codex-subscription-search-provider",
7561
7956
  checked: snapshot.searchProvider === value,
@@ -7563,39 +7958,39 @@ window.__ModuleLoader__.load({
7563
7958
  onChange: () => {
7564
7959
  preference.set({ [SEARCH_PROVIDER_FIELD]: value });
7565
7960
  }
7566
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
7567
- className: "codexSubscriptionSearchCopy",
7568
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("strong", { children: label }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: hint })]
7569
- })]
7961
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: label })]
7570
7962
  });
7963
+ const hint = snapshot.searchProvider === "dsh" ? "searchDshHint" : snapshot.searchProvider === "codex" ? "searchCodexHint" : "searchAutoHint";
7571
7964
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
7572
7965
  className: "codexSubscriptionSearch",
7573
- children: [
7574
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
7575
- className: "codexSubscriptionSearchHead",
7576
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", { children: t("searchTitle") }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
7577
- className: "codexSubscriptionSearchScope",
7578
- children: t("searchScope")
7966
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
7967
+ className: "codexSubscriptionPreference",
7968
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
7969
+ className: "codexSubscriptionPreferenceCopy",
7970
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
7971
+ className: "codexSubscriptionPreferenceLabel",
7972
+ children: t("searchTitle")
7973
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
7974
+ className: "codexSubscriptionPreferenceHint",
7975
+ children: t(hint)
7579
7976
  })]
7580
- }),
7581
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
7582
- className: "codexSubscriptionSearchChoices",
7977
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
7978
+ className: "codexSubscriptionSearchChoices codexSubscriptionQuotaModes",
7583
7979
  "data-saving": snapshot.saving || void 0,
7584
7980
  "aria-busy": snapshot.saving || void 0,
7585
7981
  role: "radiogroup",
7586
7982
  "aria-label": t("searchTitle"),
7587
7983
  children: [
7588
- choice(SEARCH_PROVIDER_AUTO, t("searchAuto"), t("searchAutoHint")),
7589
- choice("dsh", t("searchDsh"), t("searchDshHint")),
7590
- choice(SEARCH_PROVIDER_CODEX, t("searchCodex"), t("searchCodexHint"))
7984
+ choice(SEARCH_PROVIDER_AUTO, t("searchAuto")),
7985
+ choice("dsh", "DSH"),
7986
+ choice(SEARCH_PROVIDER_CODEX, "Codex")
7591
7987
  ]
7592
- }),
7593
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CapabilityPreferences, {
7594
- preference,
7595
- t,
7596
- section: "search"
7597
- })
7598
- ]
7988
+ })]
7989
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CapabilityPreferences, {
7990
+ preference,
7991
+ t,
7992
+ section: "search"
7993
+ })]
7599
7994
  });
7600
7995
  }
7601
7996
  function ContextWindowPreference({ preference, t }) {
@@ -7761,16 +8156,99 @@ window.__ModuleLoader__.load({
7761
8156
  function PreferencesCard({ preference, t, section = "display" }) {
7762
8157
  const snapshot = usePreferenceSnapshot(preference);
7763
8158
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
7764
- className: "codexSubscriptionCard codexSubscriptionPreferencesCard",
8159
+ className: section === "advanced" ? "codexSubscriptionAdvancedPreferences" : "codexSubscriptionCard codexSubscriptionPreferencesCard",
7765
8160
  children: [section === "advanced" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
7766
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SearchProviderPreference, {
7767
- preference,
7768
- t
8161
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
8162
+ className: "codexSubscriptionCard codexSubscriptionPreferencesCard",
8163
+ "aria-label": t("advancedModelSearch"),
8164
+ children: [
8165
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", { children: t("advancedModelSearch") }),
8166
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SearchProviderPreference, {
8167
+ preference,
8168
+ t
8169
+ }),
8170
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { className: "codexSubscriptionDivider" }),
8171
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ContextWindowPreference, {
8172
+ preference,
8173
+ t
8174
+ })
8175
+ ]
7769
8176
  }),
7770
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { className: "codexSubscriptionDivider" }),
7771
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ContextWindowPreference, {
7772
- preference,
7773
- t
8177
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("section", {
8178
+ className: "codexSubscriptionCard codexSubscriptionPreferencesCard",
8179
+ "aria-label": t("connectionTitle"),
8180
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
8181
+ className: "codexSubscriptionPreference",
8182
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
8183
+ className: "codexSubscriptionPreferenceCopy",
8184
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
8185
+ className: "codexSubscriptionPreferenceLabel",
8186
+ children: [
8187
+ t("connectionTitle"),
8188
+ " ",
8189
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("small", { children: "Beta" })
8190
+ ]
8191
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
8192
+ className: "codexSubscriptionPreferenceHint",
8193
+ children: t("connectionHint")
8194
+ })]
8195
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
8196
+ className: "codexSubscriptionQuotaModes",
8197
+ role: "radiogroup",
8198
+ "aria-label": t("connectionTitle"),
8199
+ "aria-busy": snapshot.saving || void 0,
8200
+ children: ["sse", "websocket"].map((value) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
8201
+ className: "codexSubscriptionQuotaMode",
8202
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
8203
+ type: "radio",
8204
+ name: "codex-connection-mode",
8205
+ checked: snapshot.connectionMode === value,
8206
+ disabled: !snapshot.writable,
8207
+ onChange: () => {
8208
+ preference.set({ connectionMode: value });
8209
+ }
8210
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: value === "sse" ? "SSE" : "WebSocket" })]
8211
+ }, value))
8212
+ })]
8213
+ })
8214
+ }),
8215
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("section", {
8216
+ className: "codexSubscriptionCard codexSubscriptionPreferencesCard",
8217
+ "aria-label": t("subagentBackendTitle"),
8218
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
8219
+ className: "codexSubscriptionPreference",
8220
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
8221
+ className: "codexSubscriptionPreferenceCopy",
8222
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
8223
+ className: "codexSubscriptionPreferenceLabel",
8224
+ children: [
8225
+ t("subagentBackendTitle"),
8226
+ " ",
8227
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("small", { children: "Beta" })
8228
+ ]
8229
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
8230
+ className: "codexSubscriptionPreferenceHint",
8231
+ children: t(snapshot.subagentBackendAvailable ? "subagentBackendHint" : "subagentBackendUnavailable")
8232
+ })]
8233
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
8234
+ className: "codexSubscriptionQuotaModes",
8235
+ role: "radiogroup",
8236
+ "aria-label": t("subagentBackendTitle"),
8237
+ "aria-busy": snapshot.saving || void 0,
8238
+ children: ["dsh", "codex"].map((value) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
8239
+ className: "codexSubscriptionQuotaMode",
8240
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
8241
+ type: "radio",
8242
+ name: "codex-subagent-backend",
8243
+ checked: snapshot.subagentBackend === value,
8244
+ disabled: !snapshot.writable || !snapshot.subagentBackendAvailable,
8245
+ onChange: () => {
8246
+ preference.set({ subagentBackend: value });
8247
+ }
8248
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t(`subagentBackend_${value}`) })]
8249
+ }, value))
8250
+ })]
8251
+ })
7774
8252
  })
7775
8253
  ] }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(QuickQuotaPreference, {
7776
8254
  preference,