dsh-codex-subscription 2.1.0-beta.5 → 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
@@ -71,6 +71,40 @@ window.__ModuleLoader__.load({
71
71
  return normalized;
72
72
  }
73
73
  //#endregion
74
+ //#region src/sketch-brushes.js
75
+ const grains = /* @__PURE__ */ new Map();
76
+ function pencilGrain(context, color) {
77
+ if (!context.createPattern || typeof document === "undefined") return color;
78
+ if (!grains.has(color)) {
79
+ const canvas = document.createElement("canvas");
80
+ canvas.width = 64;
81
+ canvas.height = 64;
82
+ const ctx = canvas.getContext("2d");
83
+ ctx.fillStyle = color;
84
+ let seed = 173;
85
+ for (let y = 0; y < 64; y++) for (let x = 0; x < 64; x++) {
86
+ seed = Math.imul(seed, 1664525) + 1013904223 >>> 0;
87
+ const value = seed / 4294967296;
88
+ if (value < .28) continue;
89
+ ctx.globalAlpha = .2 + value * .8;
90
+ ctx.fillRect(x, y, 1, 1);
91
+ }
92
+ if (grains.size >= 16) grains.delete(grains.keys().next().value);
93
+ grains.set(color, canvas);
94
+ }
95
+ return context.createPattern(grains.get(color), "repeat") ?? color;
96
+ }
97
+ function configureSketchBrush(context, stroke) {
98
+ const modern = stroke.brushVersion === 2 && stroke.shape === "pen";
99
+ const brush = stroke.brush ?? "pen";
100
+ context.lineCap = modern && brush === "marker" ? "butt" : "round";
101
+ context.lineJoin = "round";
102
+ context.globalAlpha = (stroke.opacity ?? 1) * (brush === "marker" ? .28 : brush === "pencil" ? modern ? .85 : .65 : 1);
103
+ context.lineWidth = stroke.width * (brush === "pencil" && !modern ? .55 : 1) * (stroke.pressure ?? 1);
104
+ context.strokeStyle = modern && brush === "pencil" ? pencilGrain(context, stroke.color) : stroke.color;
105
+ context.fillStyle = context.strokeStyle;
106
+ }
107
+ //#endregion
74
108
  //#region src/sketch-document.js
75
109
  const SKETCH_SIZE = 1024;
76
110
  const MAX_SKETCH_STROKES = 2e3;
@@ -96,10 +130,7 @@ window.__ModuleLoader__.load({
96
130
  const first = stroke.points[0];
97
131
  if (!first) continue;
98
132
  context.globalCompositeOperation = stroke.shape === "eraser" ? "destination-out" : "source-over";
99
- context.globalAlpha = (stroke.opacity ?? 1) * (stroke.brush === "marker" ? .28 : stroke.brush === "pencil" ? .65 : 1);
100
- context.strokeStyle = stroke.color;
101
- context.fillStyle = stroke.color;
102
- context.lineWidth = stroke.width * (stroke.brush === "pencil" ? .55 : 1) * (stroke.pressure ?? 1);
133
+ configureSketchBrush(context, stroke);
103
134
  context.beginPath();
104
135
  const last = stroke.points.at(-1);
105
136
  if (stroke.shape === "text") {
@@ -148,7 +179,8 @@ window.__ModuleLoader__.load({
148
179
  if (stroke.fill) context.fill();
149
180
  else context.stroke();
150
181
  } else if (stroke.points.length === 1) {
151
- context.arc(first.x * size, first.y * height, context.lineWidth / 2, 0, Math.PI * 2);
182
+ if (stroke.brushVersion === 2 && stroke.brush === "marker") context.rect(first.x * size - context.lineWidth / 2, first.y * height - context.lineWidth / 4, context.lineWidth, context.lineWidth / 2);
183
+ else context.arc(first.x * size, first.y * height, context.lineWidth / 2, 0, Math.PI * 2);
152
184
  context.fill();
153
185
  } else {
154
186
  context.moveTo(first.x * size, first.y * height);
@@ -340,21 +372,55 @@ window.__ModuleLoader__.load({
340
372
  dirty: ref(false),
341
373
  documentId: ref(crypto.randomUUID()),
342
374
  documentRevision: ref(0),
375
+ restoreId: ref(null),
376
+ mounts: 0,
343
377
  agentAdapter: ref({}),
344
378
  agentSession: ref(null),
345
379
  agentRun: ref(null)
346
380
  };
347
381
  }
348
- function createSketchSessionRegistry() {
349
- 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
+ };
350
395
  return {
351
396
  get(id) {
352
- if (!sessions.has(id)) sessions.set(id, createSketchSessionState());
353
- 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;
354
414
  },
415
+ prune,
416
+ stats: () => ({
417
+ resident: sessions.size,
418
+ archived: archived.size
419
+ }),
355
420
  dispose() {
356
421
  for (const value of sessions.values()) value.agentRun.current?.dispose();
357
422
  sessions.clear();
423
+ archived.clear();
358
424
  }
359
425
  };
360
426
  }
@@ -995,55 +1061,272 @@ window.__ModuleLoader__.load({
995
1061
  });
996
1062
  }
997
1063
  //#endregion
998
- //#region src/sketch-layer-renderer.js
999
- const NO_IMAGES = /* @__PURE__ */ new Map();
1000
- const surface = (width, height) => {
1001
- const c = document.createElement("canvas");
1002
- c.width = width;
1003
- c.height = height;
1004
- return c;
1005
- };
1006
- function paintSketchLayers(context, doc, cache, size = doc.width ?? 1024, height = doc.height ?? size, activeLayer, images = NO_IMAGES) {
1007
- context.globalCompositeOperation = "source-over";
1008
- context.globalAlpha = 1;
1009
- context.fillStyle = "#fff";
1010
- context.fillRect(0, 0, size, height);
1011
- for (const id of cache.keys()) if (!doc.layers.some((layer) => layer.id === id)) cache.delete(id);
1012
- for (const layer of doc.layers) {
1013
- if (!layer.visible) continue;
1014
- let entry = cache.get(layer.id);
1015
- if (!entry || entry.surface.width !== size || entry.surface.height !== height) {
1016
- entry = {
1017
- surface: surface(size, height),
1018
- base: surface(size, height)
1019
- };
1020
- cache.set(layer.id, entry);
1021
- }
1022
- const moving = layer.id === activeLayer;
1023
- const count = Math.max(0, layer.strokes.length - (moving ? 1 : 0));
1024
- const prefix = layer.strokes[count - 1];
1025
- if (entry.count !== count || entry.prefix !== prefix || entry.image !== layer.image || entry.strokes !== layer.strokes) {
1026
- const ctx = entry.base.getContext("2d");
1027
- 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]));
1028
- if (!append) {
1029
- ctx.clearRect(0, 0, size, height);
1030
- const ref = layer.image, image = ref && images.get(ref.src);
1031
- if (image) ctx.drawImage(image, ref.x * size, ref.y * height, ref.width * size, ref.height * height);
1032
- }
1033
- paintSketch(ctx, layer.strokes, size, true, height, append ? entry.count : 0, count);
1034
- entry.count = count;
1035
- entry.prefix = prefix;
1036
- entry.image = layer.image;
1037
- entry.strokes = layer.strokes;
1038
- }
1039
- if (moving) {
1040
- const ctx = entry.surface.getContext("2d");
1041
- ctx.clearRect(0, 0, size, height);
1042
- ctx.drawImage(entry.base, 0, 0);
1043
- paintSketch(ctx, layer.strokes, size, true, height, layer.strokes.length - 1);
1044
- context.drawImage(entry.surface, 0, 0);
1045
- } else context.drawImage(entry.base, 0, 0);
1046
- }
1064
+ //#region src/workspace-icons.jsx
1065
+ function WorkspaceIcon({ name, size = 24 }) {
1066
+ const paths = {
1067
+ select: "M5 3l14 9-7 2-3 7-4-18z",
1068
+ text: "M4 5h16M12 5v15M8 20h8M4 5v3M20 5v3",
1069
+ arrow: "M4 20L20 4M10 4h10v10",
1070
+ line: "M4 20L20 4",
1071
+ layers: "M12 3L2 8l10 5 10-5-10-5zM2 12l10 5 10-5M2 16l10 5 10-5",
1072
+ eye: "M2 12s4-7 10-7 10 7 10 7-4 7-10 7S2 12 2 12zM15 12a3 3 0 11-6 0 3 3 0 016 0",
1073
+ eyeOff: "M3 3l18 18M9 5a10 10 0 013 0c6 0 10 7 10 7l-3 4M6 6C3 8 2 12 2 12s4 7 10 7c2 0 4-1 5-2",
1074
+ duplicate: "M8 8h13v13H8zM16 8V3H3v13h5",
1075
+ up: "M12 20V4M5 11l7-7 7 7",
1076
+ down: "M12 4v16M5 13l7 7 7-7",
1077
+ pencil: "M4 20l2-6L17 3l4 4L10 18l-6 2zM14 6l4 4",
1078
+ marker: "M5 16l9-12 7 5-9 12-7-5zM5 16l-3 4 6 1M12 7l7 5",
1079
+ image: "M4 4h16v16H4zM4 16l5-5 4 4 3-3 4 4M15 8h.01",
1080
+ close: "M6 6l12 12M18 6L6 18",
1081
+ stop: "M6 6h12v12H6z",
1082
+ pen: "M4 17c3-7 12-15 12-11S4 19 8 19s10-10 10-6-6 8-2 7l4-3",
1083
+ eraser: "M4 14l9-10 7 7-9 10H9l-5-5zM8 10l7 7M11 21h10",
1084
+ undo: "M9 5L4 10l5 5M5 10h9a6 6 0 010 12",
1085
+ redo: "M15 5l5 5-5 5M19 10h-9a6 6 0 000 12",
1086
+ check: "M5 12l5 5 9-11",
1087
+ clear: "M4 7h16M9 7V4h6v3M7 7l1 13h8l1-13",
1088
+ rectangle: "M5 5h14v14H5z",
1089
+ circle: "M20 12a8 8 0 11-16 0 8 8 0 0116 0"
1090
+ };
1091
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("svg", {
1092
+ width: size,
1093
+ height: size,
1094
+ viewBox: "0 0 24 24",
1095
+ fill: "none",
1096
+ stroke: "currentColor",
1097
+ strokeWidth: "1.8",
1098
+ strokeLinecap: "round",
1099
+ strokeLinejoin: "round",
1100
+ "aria-hidden": "true",
1101
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: paths[name] ?? paths.pen })
1102
+ });
1103
+ }
1104
+ //#endregion
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"),
1111
+ children: [
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", {
1113
+ type: "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", {
1181
+ type: "button",
1182
+ className: "codexSketchClearLayer",
1183
+ disabled: disabled || !current.strokes.length && !current.image,
1184
+ onClick: () => change("clear"),
1185
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(WorkspaceIcon, {
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",
1234
+ type: "button",
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
1278
+ ]
1279
+ });
1280
+ }
1281
+ //#endregion
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
+ })));
1047
1330
  }
1048
1331
  //#endregion
1049
1332
  //#region src/sketch-input.js
@@ -1075,39 +1358,192 @@ window.__ModuleLoader__.load({
1075
1358
  });
1076
1359
  }
1077
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
1078
1438
  //#region src/sketch-drafts.js
1079
1439
  const DATABASE = "dsh-codex-sketches-v1";
1080
- 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) {
1081
1450
  const db = await new Promise((resolve, reject) => {
1082
- const request = indexedDB.open(DATABASE, 1);
1083
- request.onupgradeneeded = () => request.result.createObjectStore("drafts", { keyPath: "id" });
1084
- 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
+ };
1085
1483
  request.onerror = () => reject(request.error);
1086
1484
  });
1087
1485
  try {
1088
1486
  return await new Promise((resolve, reject) => {
1089
- const tx = db.transaction("drafts", action === "list" ? "readonly" : "readwrite"), store = tx.objectStore("drafts");
1090
- 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;
1091
1504
  tx.oncomplete = () => resolve(result);
1092
1505
  tx.onerror = () => reject(tx.error);
1093
- tx.onabort = () => reject(tx.error ?? Error("Draft limit reached"));
1094
- 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();
1095
1528
  request.onsuccess = () => {
1096
1529
  const rows = request.result;
1097
1530
  if (action === "list") {
1098
- result = rows.sort((a, b) => b.updated - a.updated);
1099
- return;
1100
- }
1101
- if (action === "delete") {
1102
- store.delete(value);
1531
+ result = rows.filter((row) => row.kind === "drafts").sort((a, b) => b.updated - a.updated);
1103
1532
  return;
1104
1533
  }
1105
- const others = rows.filter((row) => row.id !== value.id);
1106
- 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 });
1107
1538
  tx.abort();
1108
1539
  return;
1109
1540
  }
1110
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
+ }
1111
1547
  result = value;
1112
1548
  };
1113
1549
  });
@@ -1149,846 +1585,1118 @@ window.__ModuleLoader__.load({
1149
1585
  bitmap.close();
1150
1586
  }
1151
1587
  }
1152
- //#endregion
1153
- //#region src/sketch-interactions.js
1154
- function useSketchDismiss(open, close, host, selectors) {
1155
- const latest = (0, react.useRef)(close);
1156
- latest.current = close;
1157
- (0, react.useEffect)(() => {
1158
- if (!open) return;
1159
- const dialog = host.current?.closest("dialog") ?? host.current;
1160
- if (!dialog) return;
1161
- const pointer = (event) => {
1162
- if (selectors.some((selector) => event.target.closest?.(selector))) return;
1163
- latest.current(false);
1164
- if (event.target.matches?.("canvas")) {
1165
- event.preventDefault();
1166
- event.stopPropagation();
1167
- event.target.focus({ preventScroll: true });
1588
+ const SKETCH_COMMAND_HELP = {
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.",
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.",
1591
+ commands: {
1592
+ stroke: "{op:\"stroke\",layer:1,shape:\"pen|line|arrow|text|rectangle|circle|polygon|bezier\",color:\"#rrggbb\",width:2,opacity:1,fill:false,points:[{x:0.1,y:0.1},...]}",
1593
+ layer: "Add: {op:\"layer\",action:\"add\",value:\"name\"}; optional id is the NEW unique integer ID, otherwise allocated automatically. after is the existing insertion anchor, defaults to active layer. Other actions: {op:\"layer\",action:\"select|rename|visible|duplicate|up|down|delete|clear\",id:1,value:\"name\"}; id targets an existing layer.",
1594
+ curve: "Prefer {op:\"stroke\",shape:\"bezier\",start:{x:0,y:0},segments:[{control1:{x:0.2,y:0},control2:{x:0.8,y:1},end:{x:1,y:1}}],color:\"#123456\"}. Each segment has exactly two controls and an endpoint; no point counting required. Legacy points arrays still accepted. Do not provide both forms.",
1595
+ object: "{op:\"object\",layer:1,id:\"title\",action:\"update|duplicate|delete\",patch:{color:\"#0088ff\",text:\"Title\"},transform:{dx:0.05,dy:0,scaleX:1,scaleY:1}}. All patch and transform fields optional. Inspect returns object IDs and bounds. Prefer targeted edits over redrawing layers.",
1596
+ resize: "{op:\"resize\",ratio:\"1:1|4:3|3:4|16:9|9:16\"}"
1597
+ },
1598
+ limits: {
1599
+ strokes: MAX_SKETCH_STROKES,
1600
+ pointsPerStroke: MAX_STROKE_POINTS,
1601
+ pointsTotal: 2e5,
1602
+ commandsPerBatch: 256
1603
+ }
1604
+ };
1605
+ const finite = (value, min, max) => typeof value === "number" && Number.isFinite(value) && value >= min && value <= max;
1606
+ function applySketchCommands(source, commands) {
1607
+ if (!Array.isArray(commands) || !commands.length || commands.length > 256) throw Error("Expected 1–256 commands");
1608
+ let doc = identifyObjects(source);
1609
+ for (const command of commands) {
1610
+ if (!command || typeof command !== "object") throw Error("Invalid command");
1611
+ if (command.op === "resize") {
1612
+ doc = resizeSketch(doc, command.ratio);
1613
+ continue;
1614
+ }
1615
+ if (command.op === "layer") {
1616
+ if (![
1617
+ "add",
1618
+ "select",
1619
+ "rename",
1620
+ "visible",
1621
+ "duplicate",
1622
+ "up",
1623
+ "down",
1624
+ "delete",
1625
+ "clear"
1626
+ ].includes(command.action)) throw Error("Unknown layer action");
1627
+ if (command.action === "add") {
1628
+ const id = command.id ?? doc.nextId, after = command.after ?? doc.active;
1629
+ if (!Number.isSafeInteger(id) || id < 1 || id === Number.MAX_SAFE_INTEGER || doc.layers.some((l) => l.id === id)) throw Error("New layer id must be a unique positive integer; omit id to allocate automatically");
1630
+ const next = changeSketchLayer(doc, "add", after);
1631
+ if (next === doc) throw Error("Cannot add layer: check the existing after layer and the 8-layer limit");
1632
+ doc = {
1633
+ ...next,
1634
+ active: id,
1635
+ nextId: Math.max(next.nextId, id + 1),
1636
+ layers: next.layers.map((l) => l.id === next.active ? {
1637
+ ...l,
1638
+ id,
1639
+ name: String(command.value ?? "").trim().slice(0, 40)
1640
+ } : l)
1641
+ };
1642
+ continue;
1168
1643
  }
1644
+ const next = changeSketchLayer(doc, command.action, command.id ?? doc.active, command.value);
1645
+ if (next === doc) throw Error("Layer action unavailable; inspect the document first");
1646
+ doc = next;
1647
+ continue;
1648
+ }
1649
+ if (command.op === "object") {
1650
+ const layer = doc.layers.find((l) => l.id === (command.layer ?? doc.active)), index = layer?.strokes.findIndex((s) => s.id === command.id);
1651
+ if (!layer?.visible || index < 0 || index === void 0) throw Error("Object missing or hidden; inspect again");
1652
+ const strokes = layer.strokes.slice(), original = strokes[index];
1653
+ if (command.action === "delete") strokes.splice(index, 1);
1654
+ else if (command.action === "duplicate") strokes.splice(index + 1, 0, {
1655
+ ...original,
1656
+ id: crypto.randomUUID(),
1657
+ points: original.points.map((p) => ({ ...p }))
1658
+ });
1659
+ else if (command.action === "update") {
1660
+ const patch = command.patch ?? {};
1661
+ if (Object.keys(patch).some((k) => ![
1662
+ "color",
1663
+ "width",
1664
+ "opacity",
1665
+ "fill",
1666
+ "text",
1667
+ "points"
1668
+ ].includes(k))) throw Error("Unsupported object property");
1669
+ const changed = command.transform ? transformObject({
1670
+ ...original,
1671
+ ...patch
1672
+ }, command.transform) : {
1673
+ ...original,
1674
+ ...patch
1675
+ };
1676
+ strokes[index] = {
1677
+ ...applySketchCommands({
1678
+ ...doc,
1679
+ layers: [{
1680
+ ...layer,
1681
+ strokes: []
1682
+ }]
1683
+ }, [{
1684
+ ...changed,
1685
+ op: "stroke",
1686
+ layer: layer.id
1687
+ }]).layers[0].strokes[0],
1688
+ brush: original.brush ?? "pen",
1689
+ ...original.pressure !== void 0 ? { pressure: original.pressure } : {},
1690
+ ...original.brushVersion === 2 ? { brushVersion: 2 } : {}
1691
+ };
1692
+ } else throw Error("Unknown object action");
1693
+ doc = {
1694
+ ...doc,
1695
+ layers: doc.layers.map((l) => l === layer ? {
1696
+ ...l,
1697
+ strokes
1698
+ } : l)
1699
+ };
1700
+ continue;
1701
+ }
1702
+ if (command.op !== "stroke") throw Error("Unknown command");
1703
+ const shape = command.shape === "ellipse" ? "circle" : command.shape ?? "pen";
1704
+ const { color, width = shape === "text" ? 24 : 2, opacity = 1, fill = false } = command;
1705
+ let points = command.points;
1706
+ if (command.start !== void 0 || command.segments !== void 0) {
1707
+ if (shape !== "bezier" || points !== void 0 || !command.start || !Array.isArray(command.segments) || !command.segments.length || command.segments.length > 64) throw Error("Bezier requires start and 1–64 segments, without points");
1708
+ points = [command.start, ...command.segments.flatMap((s) => [
1709
+ s?.control1,
1710
+ s?.control2,
1711
+ s?.end
1712
+ ])];
1713
+ }
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
+ }))
1169
1757
  };
1170
- const key = (event) => {
1171
- if (event.key !== "Escape") return;
1172
- event.preventDefault();
1173
- event.stopPropagation();
1174
- latest.current(false);
1175
- dialog.querySelector("canvas")?.focus({ preventScroll: true });
1176
- };
1177
- const hidden = () => latest.current(false);
1178
- document.addEventListener("pointerdown", pointer, true);
1179
- document.addEventListener("keydown", key, true);
1180
- dialog.addEventListener("close", hidden);
1181
- return () => {
1182
- document.removeEventListener("pointerdown", pointer, true);
1183
- document.removeEventListener("keydown", key, true);
1184
- dialog.removeEventListener("close", hidden);
1758
+ doc = {
1759
+ ...doc,
1760
+ layers: doc.layers.map((item) => item === layer ? {
1761
+ ...item,
1762
+ strokes: [...item.strokes, stroke]
1763
+ } : item)
1185
1764
  };
1186
- }, [
1187
- open,
1188
- host,
1189
- selectors.join("|")
1190
- ]);
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;
1191
1768
  }
1192
- function useSketchCursor(canvas, ring, width, brush, zoom, hidden) {
1193
- const last = (0, react.useRef)(null), heldPressure = (0, react.useRef)(1);
1194
- const update = (event, bounds) => {
1195
- if (event) last.current = event;
1196
- const pointer = last.current, node = canvas.current, cursor = ring.current;
1197
- if (!pointer || !node || !cursor) return;
1198
- const rect = bounds ?? node.getBoundingClientRect();
1199
- if (hidden || pointer.pointerType === "touch" || pointer.clientX < rect.left || pointer.clientX > rect.right || pointer.clientY < rect.top || pointer.clientY > rect.bottom) {
1200
- cursor.hidden = true;
1201
- return;
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
+ };
1202
1789
  }
1203
- const pressure = node.hasPointerCapture(pointer.pointerId) && pointer.pointerType === "pen" ? heldPressure.current : 1;
1204
- const diameter = width * (brush === "pencil" ? .55 : 1) * pressure * rect.width / node.width;
1205
- cursor.hidden = false;
1206
- cursor.style.width = `${diameter}px`;
1207
- cursor.style.height = `${diameter}px`;
1208
- cursor.style.transform = `translate(${pointer.clientX - diameter / 2}px,${pointer.clientY - diameter / 2}px)`;
1209
- };
1210
- (0, react.useEffect)(() => {
1211
- update();
1212
- const observer = new ResizeObserver(() => update());
1213
- if (canvas.current) observer.observe(canvas.current);
1214
- return () => observer.disconnect();
1215
- }, [
1216
- width,
1217
- brush,
1218
- zoom,
1219
- hidden
1220
- ]);
1221
- return {
1222
- down: (event) => {
1223
- heldPressure.current = event.pointerType === "pen" ? Math.max(.2, event.pressure) : 1;
1224
- },
1225
- move: (event, bounds) => update({
1226
- clientX: event.clientX,
1227
- clientY: event.clientY,
1228
- pointerId: event.pointerId,
1229
- pointerType: event.pointerType,
1230
- pressure: event.pressure
1231
- }, bounds),
1232
- leave: () => {
1233
- last.current = null;
1234
- if (ring.current) ring.current.hidden = true;
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);
1235
1855
  }
1856
+ return result;
1236
1857
  };
1237
1858
  }
1238
1859
  //#endregion
1239
- //#region src/sketch-view.jsx
1240
- const DEFAULT_KEYS = {
1241
- pen: "b",
1242
- eraser: "e",
1243
- line: "l",
1244
- rectangle: "r",
1245
- circle: "o",
1246
- pan: " ",
1247
- zoomIn: "=",
1248
- zoomOut: "-",
1249
- fit: "0"
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;
1250
1867
  };
1251
- function useSketchView(canvas, open) {
1252
- const [view, setView] = (0, react.useState)({
1253
- scale: 1,
1254
- x: 0,
1255
- y: 0
1256
- }), [keys, setKeys] = (0, react.useState)(() => {
1257
- try {
1258
- return {
1259
- ...DEFAULT_KEYS,
1260
- ...JSON.parse(localStorage.getItem("codex-sketch-keys"))
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
1261
1957
  };
1262
- } catch {
1263
- return DEFAULT_KEYS;
1264
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)
1976
+ };
1265
1977
  });
1266
- const [shortcuts, setShortcuts] = (0, react.useState)(() => {
1267
- try {
1268
- return localStorage.getItem("codex-sketch-shortcuts") !== "off";
1269
- } catch {
1270
- return true;
1271
- }
1272
- }), [space, setSpace] = (0, react.useState)(false);
1273
- const drag = (0, react.useRef)(null), viewRef = (0, react.useRef)(view);
1274
- viewRef.current = view;
1275
- const zoom = (factor) => setView((v) => ({
1276
- ...v,
1277
- scale: Math.max(.25, Math.min(8, v.scale * factor))
1278
- }));
1279
- const reset = () => setView({
1280
- scale: 1,
1281
- x: 0,
1282
- y: 0
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)
1283
2000
  });
1284
- (0, react.useEffect)(() => {
1285
- const node = canvas.current;
1286
- if (!node || !open) return;
1287
- const wheel = (e) => {
1288
- if (!shortcuts || !e.altKey) return;
1289
- e.preventDefault();
1290
- zoom(e.deltaY < 0 ? 1.1 : 1 / 1.1);
1291
- };
1292
- node.addEventListener("wheel", wheel, { passive: false });
1293
- return () => node.removeEventListener("wheel", wheel);
1294
- }, [open, shortcuts]);
1295
- (0, react.useEffect)(() => {
1296
- const stop = () => {
1297
- drag.current = null;
1298
- setSpace(false);
1299
- };
1300
- window.addEventListener("blur", stop);
1301
- return () => window.removeEventListener("blur", stop);
1302
- }, []);
1303
- (0, react.useEffect)(() => {
1304
- if (!open || !shortcuts) {
1305
- setSpace(false);
1306
- drag.current = null;
1307
- }
1308
- }, [open, shortcuts]);
1309
- const setKey = (action, key) => {
1310
- key = key.toLowerCase();
1311
- if (!key || Object.entries(keys).some(([a, k]) => a !== action && k === key) || ["[", "]"].includes(key)) return;
1312
- const next = {
1313
- ...keys,
1314
- [action]: key
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
+ });
2033
+ }
2034
+ return doc;
2035
+ }
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)
1315
2048
  };
1316
- setKeys(next);
1317
2049
  try {
1318
- localStorage.setItem("codex-sketch-keys", JSON.stringify(next));
1319
- } catch {}
1320
- };
1321
- return {
1322
- view,
1323
- keys,
1324
- shortcuts,
1325
- space,
1326
- zoom,
1327
- reset,
1328
- setKey,
1329
- toggle: () => setShortcuts((v) => {
1330
- try {
1331
- localStorage.setItem("codex-sketch-shortcuts", v ? "off" : "on");
1332
- } catch {}
1333
- return !v;
1334
- }),
1335
- keyDown: (e) => {
1336
- if (!shortcuts || e.ctrlKey || e.metaKey || e.altKey) return false;
1337
- const key = e.key.toLowerCase();
1338
- if (key === keys.pan && !e.ctrlKey && !e.metaKey) {
1339
- e.preventDefault();
1340
- setSpace(true);
1341
- return true;
1342
- }
1343
- if (key === keys.zoomIn || key === "+" || key === keys.zoomOut || key === keys.fit) {
1344
- e.preventDefault();
1345
- if (key === keys.fit) reset();
1346
- else zoom(key === keys.zoomOut ? 1 / 1.2 : 1.2);
1347
- return true;
1348
- }
1349
- return false;
1350
- },
1351
- keyUp: (e) => {
1352
- if (e.key.toLowerCase() === keys.pan) setSpace(false);
1353
- },
1354
- down: (e) => {
1355
- if (e.button !== 1 && !space) return false;
1356
- e.preventDefault();
1357
- drag.current = {
1358
- id: e.pointerId,
1359
- x: e.clientX,
1360
- y: e.clientY,
1361
- view: viewRef.current
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
1362
2060
  };
1363
- canvas.current.setPointerCapture(e.pointerId);
1364
- return true;
1365
- },
1366
- move: (e) => {
1367
- const d = drag.current;
1368
- if (!d || d.id !== e.pointerId) return false;
1369
- setView({
1370
- ...d.view,
1371
- x: d.view.x + e.clientX - d.x,
1372
- y: d.view.y + e.clientY - d.y
1373
- });
1374
- return true;
1375
- },
1376
- end: (e) => {
1377
- if (drag.current?.id !== e.pointerId) return false;
1378
- drag.current = null;
1379
- if (canvas.current.hasPointerCapture(e.pointerId)) canvas.current.releasePointerCapture(e.pointerId);
1380
- return true;
2061
+ if (documentRevision.current === savingRevision) {
2062
+ dirty.current = false;
2063
+ setRecovered(false);
2064
+ }
1381
2065
  }
1382
2066
  };
1383
- }
1384
- function SketchViewControls({ navigation, t }) {
1385
- const [open, setOpen] = (0, react.useState)(false), [placement, setPlacement] = (0, react.useState)(null);
1386
- const host = (0, react.useRef)(null), trigger = (0, react.useRef)(null);
1387
- const close = () => {
1388
- setOpen(false);
1389
- trigger.current?.focus({ preventScroll: true });
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
2105
+ });
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));
2160
+ }
2161
+ state.restoreId.current = null;
1390
2162
  };
1391
- useSketchDismiss(open, setOpen, host, [".codexSketchViewControls", ".codexSketchKeyPanel"]);
1392
- (0, react.useLayoutEffect)(() => {
1393
- if (!open) return;
1394
- const dialog = host.current.closest("dialog");
1395
- const place = () => {
1396
- const box = dialog.getBoundingClientRect(), anchor = trigger.current.getBoundingClientRect();
1397
- const width = Math.min(360, box.width - 24);
1398
- setPlacement({
1399
- dialog,
1400
- style: {
1401
- width,
1402
- left: Math.max(12, Math.min(anchor.left - box.left, box.width - width - 12)),
1403
- bottom: box.bottom - anchor.top + 8,
1404
- maxHeight: Math.max(80, anchor.top - box.top - 24)
1405
- }
1406
- });
1407
- };
1408
- place();
1409
- const observer = new ResizeObserver(place);
1410
- observer.observe(dialog);
1411
- observer.observe(host.current);
1412
- window.addEventListener("resize", place);
1413
- return () => {
1414
- observer.disconnect();
1415
- window.removeEventListener("resize", place);
1416
- };
1417
- }, [open]);
1418
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1419
- ref: host,
1420
- className: "codexSketchViewControls",
1421
- children: [
1422
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1423
- type: "button",
1424
- "aria-label": t("sketchZoomOut"),
1425
- onClick: () => navigation.zoom(1 / 1.2),
1426
- children: "−"
1427
- }),
1428
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
1429
- type: "button",
1430
- title: t("sketchFit"),
1431
- onClick: navigation.reset,
1432
- children: [Math.round(navigation.view.scale * 100), "%"]
1433
- }),
1434
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1435
- type: "button",
1436
- "aria-label": t("sketchZoomIn"),
1437
- onClick: () => navigation.zoom(1.2),
1438
- children: "+"
1439
- }),
1440
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1441
- ref: trigger,
1442
- type: "button",
1443
- "aria-expanded": open,
1444
- onClick: () => setOpen(!open),
1445
- children: t("sketchKeys")
1446
- }),
1447
- open && placement ? (0, react_dom.createPortal)(/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
1448
- className: "codexSketchKeyPanel",
1449
- "aria-label": t("sketchKeys"),
1450
- style: placement.style,
1451
- onKeyDown: (e) => {
1452
- if (e.key === "Escape") {
1453
- e.preventDefault();
1454
- e.stopPropagation();
1455
- close();
1456
- }
1457
- },
1458
- children: [
1459
- /* @__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", {
1460
- type: "button",
1461
- "aria-label": t("sketchFileClose"),
1462
- onClick: close,
1463
- children: "×"
1464
- })] }),
1465
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
1466
- className: "codexSketchKeysEnabled",
1467
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("sketchKeysEnabled") }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
1468
- type: "checkbox",
1469
- checked: navigation.shortcuts,
1470
- onChange: navigation.toggle
1471
- })]
1472
- }),
1473
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", { children: t("sketchNavigationHint") }),
1474
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1475
- className: "codexSketchKeyGrid",
1476
- 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", {
1477
- "aria-label": t(`sketchKey_${action}`),
1478
- value: key === " " ? "Space" : key,
1479
- readOnly: true,
1480
- onKeyDown: (e) => {
1481
- if (e.key === "Tab" || e.key === "Escape") return;
1482
- e.preventDefault();
1483
- e.stopPropagation();
1484
- if (e.key.length === 1 && !e.ctrlKey && !e.metaKey && !e.altKey) navigation.setKey(action, e.key);
1485
- }
1486
- })] }, action))
1487
- }),
1488
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("small", { children: t("sketchKeyHint") })
1489
- ]
1490
- }), placement.dialog) : null
1491
- ]
1492
- });
1493
- }
1494
- //#endregion
1495
- //#region src/sketch-objects.js
1496
- const objectId = (stroke, index) => stroke.id ?? `legacy-${index}`;
1497
- const identifyObjects = (doc) => ({
1498
- ...doc,
1499
- layers: doc.layers.map((layer) => ({
1500
- ...layer,
1501
- strokes: layer.strokes.map((s, i) => s.id ? s : {
1502
- ...s,
1503
- id: objectId(s, i)
1504
- })
1505
- }))
1506
- });
1507
- function objectBounds(stroke) {
1508
- const xs = stroke.points.map((p) => p.x), ys = stroke.points.map((p) => p.y);
1509
2163
  return {
1510
- x: Math.min(...xs),
1511
- y: Math.min(...ys),
1512
- width: Math.max(...xs) - Math.min(...xs),
1513
- height: Math.max(...ys) - Math.min(...ys)
2164
+ hasContent,
2165
+ save,
2166
+ saveChanges,
2167
+ replace,
2168
+ fresh,
2169
+ load,
2170
+ importImage,
2171
+ restore
1514
2172
  };
1515
2173
  }
1516
- function transformObject(stroke, { dx = 0, dy = 0, scaleX = 1, scaleY = 1 }) {
1517
- if (![
1518
- dx,
1519
- dy,
1520
- scaleX,
1521
- scaleY
1522
- ].every(Number.isFinite) || scaleX <= 0 || scaleY <= 0) throw Error("Invalid object transform");
1523
- const box = objectBounds(stroke);
1524
- const points = stroke.points.map((p) => ({
1525
- x: box.x + (p.x - box.x) * scaleX + dx,
1526
- y: box.y + (p.y - box.y) * scaleY + dy
1527
- }));
1528
- if (points.some((p) => p.x < 0 || p.x > 1 || p.y < 0 || p.y > 1)) throw Error("Object would leave the canvas");
2174
+ //#endregion
2175
+ //#region src/sketch-operation-gate.js
2176
+ function createSketchOperationGate() {
2177
+ let running = false;
1529
2178
  return {
1530
- ...stroke,
1531
- points
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;
2185
+ try {
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;
2194
+ } finally {
2195
+ running = false;
2196
+ working(false);
2197
+ }
2198
+ }
1532
2199
  };
1533
2200
  }
1534
- function sketchObjectSummary(doc) {
1535
- return doc.layers.flatMap((layer) => layer.strokes.map((stroke, i) => ({
1536
- layer: layer.id,
1537
- id: objectId(stroke, i),
1538
- shape: stroke.shape,
1539
- color: stroke.color,
1540
- bounds: objectBounds(stroke),
1541
- ...stroke.text ? { text: stroke.text } : {}
1542
- })));
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)
2214
+ };
2215
+ }, {
2216
+ blocked,
2217
+ working,
2218
+ report,
2219
+ rethrow: true
2220
+ })) throw Error("Sketch is being edited; retry after it settles");
2221
+ return result;
1543
2222
  }
1544
- const SKETCH_COMMAND_HELP = {
1545
- 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.",
1546
- 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.",
1547
- commands: {
1548
- stroke: "{op:\"stroke\",layer:1,shape:\"pen|line|arrow|text|rectangle|circle|polygon|bezier\",color:\"#rrggbb\",width:2,opacity:1,fill:false,points:[{x:0.1,y:0.1},...]}",
1549
- layer: "Add: {op:\"layer\",action:\"add\",value:\"name\"}; optional id is the NEW unique integer ID, otherwise allocated automatically. after is the existing insertion anchor, defaults to active layer. Other actions: {op:\"layer\",action:\"select|rename|visible|duplicate|up|down|delete|clear\",id:1,value:\"name\"}; id targets an existing layer.",
1550
- curve: "Prefer {op:\"stroke\",shape:\"bezier\",start:{x:0,y:0},segments:[{control1:{x:0.2,y:0},control2:{x:0.8,y:1},end:{x:1,y:1}}],color:\"#123456\"}. Each segment has exactly two controls and an endpoint; no point counting required. Legacy points arrays still accepted. Do not provide both forms.",
1551
- object: "{op:\"object\",layer:1,id:\"title\",action:\"update|duplicate|delete\",patch:{color:\"#0088ff\",text:\"Title\"},transform:{dx:0.05,dy:0,scaleX:1,scaleY:1}}. All patch and transform fields optional. Inspect returns object IDs and bounds. Prefer targeted edits over redrawing layers.",
1552
- resize: "{op:\"resize\",ratio:\"1:1|4:3|3:4|16:9|9:16\"}"
1553
- },
1554
- limits: {
1555
- strokes: MAX_SKETCH_STROKES,
1556
- pointsPerStroke: MAX_STROKE_POINTS,
1557
- pointsTotal: 2e5,
1558
- commandsPerBatch: 256
1559
- }
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
1560
2280
  };
1561
- const finite = (value, min, max) => typeof value === "number" && Number.isFinite(value) && value >= min && value <= max;
1562
- function applySketchCommands(source, commands) {
1563
- if (!Array.isArray(commands) || !commands.length || commands.length > 256) throw Error("Expected 1–256 commands");
1564
- let doc = identifyObjects(source);
1565
- for (const command of commands) {
1566
- if (!command || typeof command !== "object") throw Error("Invalid command");
1567
- if (command.op === "resize") {
1568
- doc = resizeSketch(doc, command.ratio);
1569
- continue;
1570
- }
1571
- if (command.op === "layer") {
1572
- if (![
1573
- "add",
1574
- "select",
1575
- "rename",
1576
- "visible",
1577
- "duplicate",
1578
- "up",
1579
- "down",
1580
- "delete",
1581
- "clear"
1582
- ].includes(command.action)) throw Error("Unknown layer action");
1583
- if (command.action === "add") {
1584
- const id = command.id ?? doc.nextId, after = command.after ?? doc.active;
1585
- if (!Number.isSafeInteger(id) || id < 1 || id === Number.MAX_SAFE_INTEGER || doc.layers.some((l) => l.id === id)) throw Error("New layer id must be a unique positive integer; omit id to allocate automatically");
1586
- const next = changeSketchLayer(doc, "add", after);
1587
- if (next === doc) throw Error("Cannot add layer: check the existing after layer and the 8-layer limit");
1588
- doc = {
1589
- ...next,
1590
- active: id,
1591
- nextId: Math.max(next.nextId, id + 1),
1592
- layers: next.layers.map((l) => l.id === next.active ? {
1593
- ...l,
1594
- id,
1595
- name: String(command.value ?? "").trim().slice(0, 40)
1596
- } : l)
1597
- };
1598
- continue;
1599
- }
1600
- const next = changeSketchLayer(doc, command.action, command.id ?? doc.active, command.value);
1601
- if (next === doc) throw Error("Layer action unavailable; inspect the document first");
1602
- doc = next;
1603
- continue;
1604
- }
1605
- if (command.op === "object") {
1606
- const layer = doc.layers.find((l) => l.id === (command.layer ?? doc.active)), index = layer?.strokes.findIndex((s) => s.id === command.id);
1607
- if (!layer?.visible || index < 0 || index === void 0) throw Error("Object missing or hidden; inspect again");
1608
- const strokes = layer.strokes.slice(), original = strokes[index];
1609
- if (command.action === "delete") strokes.splice(index, 1);
1610
- else if (command.action === "duplicate") strokes.splice(index + 1, 0, {
1611
- ...original,
1612
- id: crypto.randomUUID(),
1613
- points: original.points.map((p) => ({ ...p }))
1614
- });
1615
- else if (command.action === "update") {
1616
- const patch = command.patch ?? {};
1617
- if (Object.keys(patch).some((k) => ![
1618
- "color",
1619
- "width",
1620
- "opacity",
1621
- "fill",
1622
- "text",
1623
- "points"
1624
- ].includes(k))) throw Error("Unsupported object property");
1625
- const changed = command.transform ? transformObject({
1626
- ...original,
1627
- ...patch
1628
- }, command.transform) : {
1629
- ...original,
1630
- ...patch
1631
- };
1632
- strokes[index] = {
1633
- ...applySketchCommands({
1634
- ...doc,
1635
- layers: [{
1636
- ...layer,
1637
- strokes: []
1638
- }]
1639
- }, [{
1640
- ...changed,
1641
- op: "stroke",
1642
- layer: layer.id
1643
- }]).layers[0].strokes[0],
1644
- brush: original.brush ?? "pen",
1645
- ...original.pressure !== void 0 ? { pressure: original.pressure } : {}
1646
- };
1647
- } else throw Error("Unknown object action");
1648
- doc = {
1649
- ...doc,
1650
- layers: doc.layers.map((l) => l === layer ? {
1651
- ...l,
1652
- strokes
1653
- } : l)
1654
- };
1655
- continue;
1656
- }
1657
- if (command.op !== "stroke") throw Error("Unknown command");
1658
- const shape = command.shape === "ellipse" ? "circle" : command.shape ?? "pen";
1659
- const { color, width = shape === "text" ? 24 : 2, opacity = 1, fill = false } = command;
1660
- let points = command.points;
1661
- if (command.start !== void 0 || command.segments !== void 0) {
1662
- if (shape !== "bezier" || points !== void 0 || !command.start || !Array.isArray(command.segments) || !command.segments.length || command.segments.length > 64) throw Error("Bezier requires start and 1–64 segments, without points");
1663
- points = [command.start, ...command.segments.flatMap((s) => [
1664
- s?.control1,
1665
- s?.control2,
1666
- s?.end
1667
- ])];
1668
- }
1669
- if (![
1670
- "pen",
1671
- "line",
1672
- "rectangle",
1673
- "circle",
1674
- "polygon",
1675
- "bezier",
1676
- "arrow",
1677
- "text",
1678
- "eraser"
1679
- ].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");
1680
- if ([
1681
- "line",
1682
- "arrow",
1683
- "text",
1684
- "rectangle",
1685
- "circle"
1686
- ].includes(shape) && points.length !== 2 || shape === "polygon" && points.length < 3) throw Error("Invalid shape points");
1687
- 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.`);
1688
- if (fill && ![
1689
- "rectangle",
1690
- "circle",
1691
- "polygon",
1692
- "bezier"
1693
- ].includes(shape)) throw Error("Fill requires a closed shape");
1694
- const layer = doc.layers.find((layer) => layer.id === (command.layer ?? doc.active));
1695
- if (!layer?.visible) throw Error("Target layer is missing or hidden");
1696
- 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");
1697
- const id = command.id ?? crypto.randomUUID();
1698
- if (typeof id !== "string" || !id.length || id.length > 100 || layer.strokes.some((s) => s.id === id)) throw Error("Invalid or duplicate object id");
1699
- const stroke = {
1700
- id,
1701
- ...shape === "text" ? { text: command.text } : {},
1702
- shape,
1703
- color,
1704
- width,
1705
- opacity,
1706
- fill,
1707
- brush: "pen",
1708
- points: points.map((p) => ({
1709
- x: p.x,
1710
- y: p.y
1711
- }))
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);
2336
+ }
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
+ }
1712
2369
  };
1713
- doc = {
1714
- ...doc,
1715
- layers: doc.layers.map((item) => item === layer ? {
1716
- ...item,
1717
- strokes: [...item.strokes, stroke]
1718
- } : item)
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 });
1719
2376
  };
1720
- }
1721
- 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");
1722
- return doc;
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
+ ]);
1723
2391
  }
1724
- function createSketchCommandSession(adapter) {
1725
- const completed = /* @__PURE__ */ new Map();
1726
- let pending = false, cachedCharacters = 0;
1727
- return async (request) => {
1728
- if (!request || typeof request !== "object") throw Error("Invalid sketch request");
1729
- if (!adapter.available()) throw Error("Open the sketch board for this session first");
1730
- const current = adapter.snapshot();
1731
- if (request.action === "inspect") {
1732
- const offset = request.offset ?? 0, objects = adapter.objects?.() ?? [];
1733
- if (!Number.isInteger(offset) || offset < 0) throw Error("offset must be a non-negative integer");
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;
2434
+ }
2435
+ };
2436
+ }
2437
+ //#endregion
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"
2449
+ };
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 {
1734
2457
  return {
1735
- ...current,
1736
- protocolVersion: 2,
1737
- objects: objects.slice(offset, offset + 50),
1738
- objectCount: objects.length,
1739
- ...offset + 50 < objects.length ? { nextOffset: offset + 50 } : {},
1740
- ...request.objectId ? { object: adapter.object?.(request.objectId, request.layer) } : {},
1741
- recentRequests: [...completed.values()].slice(-8).map((entry) => entry.receipt),
1742
- help: SKETCH_COMMAND_HELP
2458
+ ...DEFAULT_KEYS,
2459
+ ...JSON.parse(localStorage.getItem("codex-sketch-keys"))
1743
2460
  };
2461
+ } catch {
2462
+ return DEFAULT_KEYS;
1744
2463
  }
1745
- if (request.documentId !== current.documentId) throw Error("Document changed; inspect again");
1746
- if (pending || adapter.busy()) throw Error("Sketch is being edited; retry after it settles");
1747
- if (request.action === "preview") return {
1748
- ...current,
1749
- png: await adapter.preview()
1750
- };
1751
- if (!["apply", "save"].includes(request.action)) throw Error("Unknown sketch action");
1752
- if (typeof request.requestId !== "string" || !request.requestId.length || request.requestId.length > 100) throw Error("A unique requestId is required");
1753
- const key = `${current.documentId}:${request.requestId}`, fingerprint = JSON.stringify({
1754
- ...request,
1755
- runId: void 0
1756
- });
1757
- const cached = completed.get(key);
1758
- if (cached) {
1759
- if (cached.fingerprint !== fingerprint) throw Error("requestId reused with different content");
1760
- return cached.result;
1761
- }
1762
- if (request.revision !== current.revision || adapter.busy()) throw Error("Sketch changed or is being edited; inspect again");
1763
- let changedObjects;
1764
- if (request.action === "apply") {
1765
- const before = adapter.document();
1766
- let next;
1767
- try {
1768
- next = applySketchCommands(before, request.commands);
1769
- } catch (cause) {
1770
- const error = new Error(`${cause.message} Correct the batch and retry with the same runId and revision; nothing was applied.`, { cause });
1771
- error.code = "SKETCH_INVALID_BATCH";
1772
- throw error;
1773
- }
1774
- adapter.commit(next);
1775
- const previous = new Map(before.layers.flatMap((l) => l.strokes.map((s) => [`${l.id}:${s.id}`, s])));
1776
- changedObjects = next.layers.flatMap((l) => l.strokes.filter((s) => previous.get(`${l.id}:${s.id}`) !== s).map((s) => ({
1777
- layer: l.id,
1778
- id: s.id
1779
- })));
1780
- } else {
1781
- if (request.name !== void 0 && (typeof request.name !== "string" || request.name.length > 60)) throw Error("Invalid draft name");
1782
- pending = true;
1783
- try {
1784
- await adapter.save(request.name);
1785
- } finally {
1786
- pending = false;
1787
- }
2464
+ });
2465
+ const [shortcuts, setShortcuts] = (0, react.useState)(() => {
2466
+ try {
2467
+ return localStorage.getItem("codex-sketch-shortcuts") !== "off";
2468
+ } catch {
2469
+ return true;
1788
2470
  }
1789
- const result = {
1790
- ...adapter.snapshot(),
1791
- ...changedObjects ? {
1792
- changedObjects: changedObjects.slice(0, 100),
1793
- changedObjectCount: changedObjects.length
1794
- } : {}
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
2482
+ });
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);
1795
2498
  };
1796
- completed.set(key, {
1797
- fingerprint,
1798
- result,
1799
- receipt: {
1800
- requestId: request.requestId,
1801
- action: request.action,
1802
- revision: result.revision
1803
- }
1804
- });
1805
- cachedCharacters += fingerprint.length;
1806
- while (completed.size > 1 && (completed.size > 128 || cachedCharacters > 4e6)) {
1807
- const oldest = completed.keys().next().value;
1808
- cachedCharacters -= completed.get(oldest).fingerprint.length;
1809
- completed.delete(oldest);
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;
1810
2506
  }
1811
- return result;
1812
- };
1813
- }
1814
- //#endregion
1815
- //#region src/sketch-formats.js
1816
- const SKETCH_FILE_ACCEPT = ".psd,.dsh-sketch.json,image/png,image/jpeg,image/webp";
1817
- const canvas = (w, h) => {
1818
- const c = document.createElement("canvas");
1819
- c.width = w;
1820
- c.height = h;
1821
- return c;
1822
- };
1823
- function runPsdCodec(action, payload) {
1824
- return new Promise((resolve, reject) => {
1825
- const worker = new Worker("/api/codex-subscription/sketch-psd-worker", { type: "module" });
1826
- const finish = (callback, value) => {
1827
- clearTimeout(timer);
1828
- worker.terminate();
1829
- callback(value);
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
1830
2514
  };
1831
- const timer = setTimeout(() => finish(reject, Error("PSD operation timed out")), 3e4);
1832
- worker.onerror = () => finish(reject, Error("PSD codec could not be loaded"));
1833
- worker.onmessage = (event) => event.data.ok ? finish(resolve, event.data.value) : finish(reject, Error(event.data.error));
1834
- worker.postMessage({
1835
- action,
1836
- payload
1837
- });
1838
- });
1839
- }
1840
- function encodeSketchDocument(doc) {
1841
- return JSON.stringify({
1842
- format: "dsh-sketch",
1843
- version: 1,
1844
- doc
1845
- });
1846
- }
1847
- function decodeSketchDocument(text) {
1848
- if (text.length > 32 * 1024 * 1024) throw Error("Draft exceeds 32 MB");
1849
- const file = JSON.parse(text), source = file.doc;
1850
- 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");
1851
- const w = source.width ?? 1024, h = source.height ?? 1024;
1852
- if (!Number.isInteger(w) || !Number.isInteger(h) || w < 1 || h < 1 || w > 2048 || h > 2048) throw Error("Invalid canvas size");
1853
- let doc = {
1854
- ...createSketchLayers(),
1855
- width: w,
1856
- height: h,
1857
- ratio: Object.keys(SKETCH_RATIOS).find((k) => SKETCH_RATIOS[k][0] === w && SKETCH_RATIOS[k][1] === h) ?? "custom"
2515
+ setKeys(next);
2516
+ try {
2517
+ localStorage.setItem("codex-sketch-keys", JSON.stringify(next));
2518
+ } catch {}
1858
2519
  };
1859
- for (let i = 0; i < source.layers.length; i++) {
1860
- const layer = source.layers[i];
1861
- if (i) doc = applySketchCommands(doc, [{
1862
- op: "layer",
1863
- action: "add"
1864
- }]);
1865
- if (!Array.isArray(layer.strokes)) throw Error("Invalid strokes");
1866
- for (let j = 0; j < layer.strokes.length; j += 256) {
1867
- const strokes = layer.strokes.slice(j, j + 256);
1868
- doc = applySketchCommands(doc, strokes.map((s) => ({
1869
- ...s,
1870
- op: "stroke",
1871
- layer: doc.active,
1872
- fill: s.fill ?? false
1873
- })));
1874
- const added = doc.layers.at(-1).strokes;
1875
- for (let k = 0; k < strokes.length; k++) {
1876
- const s = strokes[k];
1877
- if (s.brush !== void 0 && ![
1878
- "pen",
1879
- "pencil",
1880
- "marker"
1881
- ].includes(s.brush)) throw Error("Invalid brush");
1882
- if (s.pressure !== void 0 && (!Number.isFinite(s.pressure) || s.pressure < .2 || s.pressure > 1)) throw Error("Invalid pressure");
1883
- Object.assign(added[added.length - strokes.length + k], {
1884
- brush: s.brush ?? "pen",
1885
- pressure: s.pressure ?? 1
1886
- });
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;
1887
2541
  }
1888
- }
1889
- const target = doc.layers.at(-1);
1890
- target.name = String(layer.name ?? "").slice(0, 40);
1891
- target.visible = layer.visible !== false;
1892
- if (layer.image) {
1893
- const image = layer.image;
1894
- if (typeof image.src !== "string" || !/^data:image\/png;base64,/.test(image.src) || image.src.length > 8 * 1024 * 1024 || [
1895
- "x",
1896
- "y",
1897
- "width",
1898
- "height"
1899
- ].some((k) => !Number.isFinite(image[k]) || image[k] < 0 || image[k] > 1)) throw Error("Invalid draft image");
1900
- const bytes = Uint8Array.from(atob(image.src.slice(image.src.indexOf(",") + 1)), (c) => c.charCodeAt(0));
1901
- if (bytes.length < 24) throw Error("Invalid draft image");
1902
- const header = new DataView(bytes.buffer);
1903
- 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");
1904
- target.image = {
1905
- src: image.src,
1906
- x: image.x,
1907
- y: image.y,
1908
- width: image.width,
1909
- 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
1910
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;
1911
2584
  }
1912
- }
1913
- const activeIndex = source.layers.findIndex((layer) => layer.id === source.active);
1914
- doc.active = doc.layers[Math.max(0, activeIndex)].id;
1915
- return doc;
2585
+ };
1916
2586
  }
1917
- async function exportSketchPsd(doc, images, composite) {
1918
- const width = doc.width ?? 1024, height = doc.height ?? 1024;
1919
- const children = doc.layers.map((layer, i) => {
1920
- const ctx = canvas(width, height).getContext("2d"), ref = layer.image;
1921
- if (ref) ctx.drawImage(images.get(ref.src), ref.x * width, ref.y * height, ref.width * width, ref.height * height);
1922
- paintSketch(ctx, layer.strokes, width, true, height);
1923
- return {
1924
- name: layer.name || `Layer ${i + 1}`,
1925
- hidden: !layer.visible,
1926
- opacity: 1,
1927
- blendMode: "normal",
1928
- 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
+ });
1929
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);
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
+ ]
1930
2695
  });
1931
- const context = canvas(width, height).getContext("2d");
1932
- context.fillStyle = "#fff";
1933
- context.fillRect(0, 0, width, height);
1934
- if (children[0] && !children[0].hidden) {
1935
- const bottom = canvas(width, height);
1936
- bottom.getContext("2d").putImageData(children[0].imageData, 0, 0);
1937
- context.drawImage(bottom, 0, 0);
1938
- children[0].imageData = context.getImageData(0, 0, width, height);
1939
- } else {
1940
- if (children.length >= 8) throw Error("Show the bottom layer before exporting this eight-layer drawing");
1941
- children.unshift({
1942
- name: "Paper",
1943
- opacity: 1,
1944
- blendMode: "normal",
1945
- imageData: context.getImageData(0, 0, width, height)
1946
- });
1947
- }
1948
- return runPsdCodec("write", {
1949
- width,
1950
- height,
1951
- children,
1952
- imageData: composite.getContext("2d").getImageData(0, 0, width, height)
1953
- });
1954
- }
1955
- async function importSketchPsd(file) {
1956
- if (file.size > 32 * 1024 * 1024) throw Error("PSD exceeds 32 MB");
1957
- const psd = await runPsdCodec("read", await file.arrayBuffer());
1958
- 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));
1959
- const doc = {
1960
- ...createSketchLayers(),
1961
- width,
1962
- height,
1963
- ratio: Object.keys(SKETCH_RATIOS).find((k) => SKETCH_RATIOS[k][0] === width && SKETCH_RATIOS[k][1] === height) ?? "custom",
1964
- layers: [],
1965
- nextId: psd.layers.length + 1
1966
- };
1967
- for (const [i, layer] of psd.layers.entries()) {
1968
- const src = canvas(layer.imageData.width, layer.imageData.height);
1969
- src.getContext("2d").putImageData(new ImageData(new Uint8ClampedArray(layer.imageData.data), layer.imageData.width, layer.imageData.height), 0, 0);
1970
- const out = canvas(width, height), ctx = out.getContext("2d");
1971
- ctx.globalAlpha = layer.opacity;
1972
- ctx.drawImage(src, layer.left * scale, layer.top * scale, src.width * scale, src.height * scale);
1973
- doc.layers.push({
1974
- id: i + 1,
1975
- name: layer.name,
1976
- visible: !layer.hidden,
1977
- strokes: [],
1978
- image: {
1979
- src: out.toDataURL("image/png"),
1980
- x: 0,
1981
- y: 0,
1982
- width: 1,
1983
- height: 1
1984
- }
1985
- });
1986
- }
1987
- return doc;
1988
2696
  }
1989
2697
  //#endregion
1990
2698
  //#region src/sketch-files.jsx
1991
- function SketchFiles({ save, load, fresh, importImage, download, hasContent, disabled, t, report, onWorking }) {
2699
+ function SketchFiles({ save, load, fresh, importImage, download, hasContent, disabled, t, runOperation }) {
1992
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);
1993
2701
  const [format, setFormat] = (0, react.useState)("png");
1994
2702
  const input = (0, react.useRef)(null), host = (0, react.useRef)(null);
@@ -2000,19 +2708,14 @@ window.__ModuleLoader__.load({
2000
2708
  }
2001
2709
  }, [open, working]);
2002
2710
  useSketchDismiss(open, setOpen, host, [".codexSketchFiles"]);
2003
- const run = async (operation) => {
2004
- if (disabled || working) return;
2711
+ const run = (operation) => runOperation(async () => {
2005
2712
  setWorking(true);
2006
- onWorking(true);
2007
2713
  try {
2008
2714
  await operation();
2009
- } catch (error) {
2010
- report(error?.message || t("sketchStorageFailed"));
2011
2715
  } finally {
2012
2716
  setWorking(false);
2013
- onWorking(false);
2014
2717
  }
2015
- };
2718
+ });
2016
2719
  const refresh = async () => setRows(await sketchDrafts("list"));
2017
2720
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2018
2721
  ref: host,
@@ -2154,46 +2857,6 @@ window.__ModuleLoader__.load({
2154
2857
  });
2155
2858
  }
2156
2859
  //#endregion
2157
- //#region src/workspace-icons.jsx
2158
- function WorkspaceIcon({ name, size = 24 }) {
2159
- const paths = {
2160
- select: "M5 3l14 9-7 2-3 7-4-18z",
2161
- text: "M4 5h16M12 5v15M8 20h8M4 5v3M20 5v3",
2162
- arrow: "M4 20L20 4M10 4h10v10",
2163
- line: "M4 20L20 4",
2164
- layers: "M12 3L2 8l10 5 10-5-10-5zM2 12l10 5 10-5M2 16l10 5 10-5",
2165
- eye: "M2 12s4-7 10-7 10 7 10 7-4 7-10 7S2 12 2 12zM15 12a3 3 0 11-6 0 3 3 0 016 0",
2166
- eyeOff: "M3 3l18 18M9 5a10 10 0 013 0c6 0 10 7 10 7l-3 4M6 6C3 8 2 12 2 12s4 7 10 7c2 0 4-1 5-2",
2167
- duplicate: "M8 8h13v13H8zM16 8V3H3v13h5",
2168
- up: "M12 20V4M5 11l7-7 7 7",
2169
- down: "M12 4v16M5 13l7 7 7-7",
2170
- pencil: "M4 20l2-6L17 3l4 4L10 18l-6 2zM14 6l4 4",
2171
- marker: "M5 16l9-12 7 5-9 12-7-5zM5 16l-3 4 6 1M12 7l7 5",
2172
- image: "M4 4h16v16H4zM4 16l5-5 4 4 3-3 4 4M15 8h.01",
2173
- close: "M6 6l12 12M18 6L6 18",
2174
- pen: "M4 17c3-7 12-15 12-11S4 19 8 19s10-10 10-6-6 8-2 7l4-3",
2175
- eraser: "M4 14l9-10 7 7-9 10H9l-5-5zM8 10l7 7M11 21h10",
2176
- undo: "M9 5L4 10l5 5M5 10h9a6 6 0 010 12",
2177
- redo: "M15 5l5 5-5 5M19 10h-9a6 6 0 000 12",
2178
- check: "M5 12l5 5 9-11",
2179
- clear: "M4 7h16M9 7V4h6v3M7 7l1 13h8l1-13",
2180
- rectangle: "M5 5h14v14H5z",
2181
- circle: "M20 12a8 8 0 11-16 0 8 8 0 0116 0"
2182
- };
2183
- return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("svg", {
2184
- width: size,
2185
- height: size,
2186
- viewBox: "0 0 24 24",
2187
- fill: "none",
2188
- stroke: "currentColor",
2189
- strokeWidth: "1.8",
2190
- strokeLinecap: "round",
2191
- strokeLinejoin: "round",
2192
- "aria-hidden": "true",
2193
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: paths[name] ?? paths.pen })
2194
- });
2195
- }
2196
- //#endregion
2197
2860
  //#region src/sketch-size-control.jsx
2198
2861
  function SketchSizeControl({ value, onChange, onStart, onEnd, label, disabled, min = 2, max = 128, mode, modes, onModeChange, suffix = "" }) {
2199
2862
  const active = (0, react.useRef)(false);
@@ -2349,13 +3012,15 @@ window.__ModuleLoader__.load({
2349
3012
  //#endregion
2350
3013
  //#region src/sketch-agent-client.js
2351
3014
  function connectSketchAgent(rpc, sessionId, execute, report, pollDelay = () => 350) {
2352
- let stopped = false, token, timer, attempts = 0, failures = 0;
3015
+ let stopped = false, token, timer, attempts = 0, failures = 0, pending = false;
2353
3016
  const call = (endpoint, payload) => rpc.call(CHANNEL, `sketch/${endpoint}`, {
2354
3017
  sessionId,
2355
3018
  token,
2356
3019
  ...payload
2357
3020
  }).then(unwrap);
2358
3021
  const poll = async () => {
3022
+ if (stopped || pending) return;
3023
+ pending = true;
2359
3024
  try {
2360
3025
  const tasks = await call("poll");
2361
3026
  for (const task of tasks) {
@@ -2390,25 +3055,53 @@ window.__ModuleLoader__.load({
2390
3055
  timer = setTimeout(connect, Math.min(1e4, 1e3 * 2 ** (failures - 1)));
2391
3056
  }
2392
3057
  return;
3058
+ } finally {
3059
+ pending = false;
2393
3060
  }
2394
3061
  failures = 0;
2395
3062
  if (!stopped) timer = setTimeout(poll, pollDelay());
2396
3063
  };
2397
- const connect = () => void call("connect").then((value) => {
2398
- 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);
2399
3084
  attempts = 0;
2400
- if (stopped) call("disconnect").catch(() => {});
2401
- else poll();
2402
- }, (error) => {
2403
- if (stopped) return;
2404
- const leaseConflict = /Another board is connected/.test(error.message);
2405
- if (++attempts < (leaseConflict ? 8 : 3)) timer = setTimeout(connect, Math.min(3e3, 500 * attempts));
2406
- else report(error.message);
2407
- });
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
+ }
2408
3096
  connect();
2409
3097
  return () => {
2410
3098
  stopped = true;
2411
3099
  clearTimeout(timer);
3100
+ if (typeof window !== "undefined") {
3101
+ window.removeEventListener("online", wake);
3102
+ window.removeEventListener("focus", wake);
3103
+ document.removeEventListener("visibilitychange", visible);
3104
+ }
2412
3105
  if (token) call("disconnect").catch(() => {});
2413
3106
  };
2414
3107
  }
@@ -2435,6 +3128,7 @@ window.__ModuleLoader__.load({
2435
3128
  const [stability, setStability] = (0, react.useState)(0), [flow, setFlow] = (0, react.useState)(100), [picturesOpen, setPicturesOpen] = (0, react.useState)(false);
2436
3129
  const pictureInput = (0, react.useRef)(null), received = (0, react.useRef)(null);
2437
3130
  const navigation = useSketchView(canvas, open);
3131
+ const toolWidths = (0, react.useRef)({});
2438
3132
  const [revision, redraw] = (0, react.useState)(0), [tool, setTool] = (0, react.useState)("pen"), [brush, setBrush] = (0, react.useState)("pen");
2439
3133
  const [eraser, setEraser] = (0, react.useState)("pixel"), [color, setColor] = (0, react.useState)("#0088ff"), [width, setWidth] = (0, react.useState)(12);
2440
3134
  const [selection, setSelection] = (0, react.useState)(null), [textEdit, setTextEdit] = (0, react.useState)(null), [shapesOpen, setShapesOpen] = (0, react.useState)(false);
@@ -2464,8 +3158,24 @@ window.__ModuleLoader__.load({
2464
3158
  setColor(value);
2465
3159
  if (selected) editObject({ color: value });
2466
3160
  };
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);
3173
+ };
3174
+ const chooseBrush = (name) => chooseTool("pen", name);
2467
3175
  const [fillShape, setFillShape] = (0, react.useState)(false);
2468
- 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)("");
2469
3179
  const cursorRing = (0, react.useRef)(null);
2470
3180
  const cursor = useSketchCursor(canvas, cursorRing, width, tool === "pen" ? brush : "pen", navigation.view.scale, !open || navigation.space || busy || agentLocked || tool === "select" || tool === "text");
2471
3181
  useSketchDismiss(shapesOpen, setShapesOpen, dialog, [".codexSketchShapeMenu", ".codexSketchShapeToggle"]);
@@ -2500,147 +3210,103 @@ window.__ModuleLoader__.load({
2500
3210
  const change = (action, id, value) => {
2501
3211
  if (busy || agentRun.current?.locked || active.current) return;
2502
3212
  const next = changeSketchLayer(doc.current, action, id, value);
2503
- if (next === doc.current) return;
2504
- if (action !== "select") checkpoint();
2505
- else documentRevision.current++;
2506
- doc.current = next;
2507
- setError("");
2508
- schedule();
2509
- };
2510
- (0, react.useEffect)(() => {
2511
- if (open) {
2512
- dialog.current.showModal();
2513
- canvas.current.width = doc.current.width ?? 1024;
2514
- paint();
2515
- } else dialog.current?.close();
2516
- }, [open]);
2517
- (0, react.useEffect)(() => () => {
2518
- cancelAnimationFrame(frame.current);
2519
- cache.current.clear();
2520
- }, []);
2521
- const hasContent = () => doc.current.layers.some((layer) => layer.image || layer.strokes.length);
2522
- const save = async (name) => {
2523
- const savingDocument = documentId.current, savingRevision = documentRevision.current;
2524
- const row = {
2525
- id: saved.current?.id ?? crypto.randomUUID(),
2526
- name: name?.trim() || saved.current?.name || `${t("sketchTitle")} ${(/* @__PURE__ */ new Date()).toLocaleString()}`,
2527
- updated: Date.now(),
2528
- doc: structuredClone(doc.current)
2529
- };
2530
- await sketchDrafts("save", row);
2531
- if (documentId.current === savingDocument) {
2532
- saved.current = {
2533
- id: row.id,
2534
- name: row.name
2535
- };
2536
- if (documentRevision.current === savingRevision) dirty.current = false;
2537
- }
2538
- };
2539
- const saveChanges = async () => {
2540
- if (dirty.current && (hasContent() || saved.current)) await save();
2541
- };
2542
- const replace = (next, decoded, identity) => {
2543
- documentId.current = crypto.randomUUID();
2544
- documentRevision.current++;
2545
- doc.current = identifyObjects(structuredClone(next));
2546
- setSelection(null);
2547
- setTextEdit(null);
2548
- images.current = decoded;
2549
- cache.current.clear();
2550
- undo.current = [];
2551
- redo.current = [];
2552
- saved.current = identity;
2553
- dirty.current = false;
2554
- schedule();
2555
- };
2556
- const fresh = async () => {
2557
- await saveChanges();
2558
- replace(createSketchLayers(), /* @__PURE__ */ new Map(), null);
2559
- };
2560
- const load = async (row) => {
2561
- if (row.id === saved.current?.id) return;
2562
- await saveChanges();
2563
- const decoded = /* @__PURE__ */ new Map();
2564
- await decodeSketchImages(row.doc, decoded);
2565
- replace(row.doc, decoded, {
2566
- id: row.id,
2567
- name: row.name
2568
- });
2569
- };
2570
- const importImage = async (file) => {
2571
- if (file.name?.toLowerCase().endsWith(".psd") || file.name?.toLowerCase().endsWith(".dsh-sketch.json")) {
2572
- if (file.size > 32 * 1024 * 1024) throw Error("File exceeds 32 MB");
2573
- const next = file.name.toLowerCase().endsWith(".psd") ? await importSketchPsd(file) : decodeSketchDocument(await file.text());
2574
- const decoded = /* @__PURE__ */ new Map();
2575
- await decodeSketchImages(next, decoded);
2576
- await saveChanges();
2577
- replace(next, decoded, null);
2578
- dirty.current = true;
2579
- return;
2580
- }
2581
- if (doc.current.layers.length >= 8) throw Error("Layer limit");
2582
- const image = await importSketchImage(file), w = doc.current.width ?? 1024, h = doc.current.height ?? 1024;
2583
- const scale = Math.min(w / image.width, h / image.height), width = image.width * scale / w, height = image.height * scale / h;
2584
- const layer = {
2585
- id: doc.current.nextId,
2586
- name: file.name?.slice(0, 40) || t("sketchImport"),
2587
- visible: true,
2588
- strokes: [],
2589
- image: {
2590
- src: image.src,
2591
- x: (1 - width) / 2,
2592
- y: (1 - height) / 2,
2593
- width,
2594
- height
2595
- }
2596
- };
2597
- await decodeSketchImages({ layers: [layer] }, images.current);
2598
- checkpoint();
2599
- doc.current = {
2600
- ...doc.current,
2601
- nextId: layer.id + 1,
2602
- active: layer.id,
2603
- layers: [...doc.current.layers, layer]
2604
- };
3213
+ if (next === doc.current) return;
3214
+ if (action !== "select") checkpoint();
3215
+ else documentRevision.current++;
3216
+ doc.current = next;
3217
+ setError("");
2605
3218
  schedule();
2606
3219
  };
2607
- const close = async () => {
2608
- 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) {
2609
3284
  onClose();
2610
3285
  return;
2611
3286
  }
2612
- if (busy || active.current) return;
2613
- setBusy(true);
2614
- try {
3287
+ return runFile(async () => {
2615
3288
  await saveChanges();
2616
3289
  onClose();
2617
- } catch {
2618
- setError(t("sketchStorageFailed"));
2619
- } finally {
2620
- setBusy(false);
2621
- }
2622
- };
2623
- const runFile = async (operation) => {
2624
- if (busy || agentRun.current?.locked || active.current) return;
2625
- setBusy(true);
2626
- setError("");
2627
- try {
2628
- await operation();
2629
- } catch {
2630
- setError(t("sketchStorageFailed"));
2631
- } finally {
2632
- setBusy(false);
2633
- }
3290
+ });
2634
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
+ });
2635
3299
  (0, react.useEffect)(() => {
2636
- if (open && !agentLocked && incoming && incoming !== received.current) {
3300
+ if (open && hydrated && !busy && !agentLocked && incoming && incoming !== received.current) {
2637
3301
  received.current = incoming;
2638
3302
  runFile(() => importImage(incoming.file));
2639
3303
  }
2640
3304
  }, [
2641
3305
  open,
2642
3306
  incoming,
2643
- agentLocked
3307
+ agentLocked,
3308
+ hydrated,
3309
+ busy
2644
3310
  ]);
2645
3311
  const keyDown = (event) => {
2646
3312
  if (event.target.closest("input,textarea,select,[contenteditable=true]") || event.isComposing || busy || active.current) return;
@@ -2652,14 +3318,6 @@ window.__ModuleLoader__.load({
2652
3318
  editObject({}, "delete");
2653
3319
  return;
2654
3320
  }
2655
- if (event.key.toLowerCase() === "v") {
2656
- setTool("select");
2657
- return;
2658
- }
2659
- if (event.key.toLowerCase() === "t") {
2660
- setTool("text");
2661
- return;
2662
- }
2663
3321
  const key = event.key.toLowerCase(), command = event.ctrlKey || event.metaKey;
2664
3322
  if (command && [
2665
3323
  "z",
@@ -2673,21 +3331,24 @@ window.__ModuleLoader__.load({
2673
3331
  return;
2674
3332
  }
2675
3333
  if (command || event.altKey) return;
2676
- const tools = Object.fromEntries([
3334
+ const action = sketchShortcutAction(navigation.keys, key);
3335
+ if ([
2677
3336
  "pen",
2678
3337
  "eraser",
2679
3338
  "line",
2680
3339
  "rectangle",
2681
- "circle"
2682
- ].map((action) => [navigation.keys[action], action]));
2683
- if (tools[key]) {
3340
+ "circle",
3341
+ "select",
3342
+ "text"
3343
+ ].includes(action)) {
2684
3344
  event.preventDefault();
2685
- setTool(tools[key]);
2686
- if (tools[key] === "pen") setBrush("pen");
3345
+ chooseTool(action);
2687
3346
  }
2688
3347
  if (key === "[" || key === "]") {
2689
3348
  event.preventDefault();
2690
- 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);
2691
3352
  }
2692
3353
  };
2693
3354
  const current = doc.current.layers.find((layer) => layer.id === doc.current.active);
@@ -2696,81 +3357,14 @@ window.__ModuleLoader__.load({
2696
3357
  const gesture = active.current;
2697
3358
  if (!gesture || gesture.id !== event.pointerId || busy) return;
2698
3359
  const rect = bounds ?? canvas.current.getBoundingClientRect();
2699
- if (gesture.object) {
2700
- const point = sketchPoint(event.clientX, event.clientY, rect);
2701
- if (!point) return;
2702
- try {
2703
- const box = objectBounds(gesture.object);
2704
- const stroke = gesture.handle === "end" ? {
2705
- ...gesture.object,
2706
- points: [gesture.object.points[0], point]
2707
- } : gesture.handle === "size" ? transformObject(gesture.object, {
2708
- scaleX: Math.max(.001, point.x - box.x) / Math.max(.001, box.width),
2709
- scaleY: Math.max(.001, point.y - box.y) / Math.max(.001, box.height)
2710
- }) : transformObject(gesture.object, {
2711
- dx: point.x - gesture.start.x,
2712
- dy: point.y - gesture.start.y
2713
- });
2714
- doc.current = {
2715
- ...doc.current,
2716
- layers: doc.current.layers.map((l) => l.id === gesture.layer ? {
2717
- ...l,
2718
- strokes: l.strokes.map((s) => s.id === gesture.object.id ? stroke : s)
2719
- } : l)
2720
- };
2721
- schedule();
2722
- } catch {}
2723
- return;
2724
- }
2725
3360
  const native = event.nativeEvent ?? event;
2726
- const events = native.getCoalescedEvents?.() ?? [];
2727
- for (const sample of events.length ? [...events, native] : [native]) {
2728
- let point = sketchPoint(sample.clientX, sample.clientY, rect);
2729
- if (!point) continue;
2730
- const layer = doc.current.layers.find((layer) => layer.id === gesture.layer);
2731
- if (gesture.eraseStroke) {
2732
- const previous = gesture.last ?? point;
2733
- 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))));
2734
- layer.strokes = layer.strokes.filter((stroke) => {
2735
- for (let i = 1; i <= steps; i++) if (strokeHit(stroke, {
2736
- x: previous.x + (point.x - previous.x) * i / steps,
2737
- y: previous.y + (point.y - previous.y) * i / steps
2738
- }, width / 2, doc.current.width, doc.current.height)) return false;
2739
- return true;
2740
- });
2741
- } else {
2742
- const stroke = layer.strokes.at(-1);
2743
- if ([
2744
- "line",
2745
- "arrow",
2746
- "rectangle",
2747
- "circle"
2748
- ].includes(stroke.shape)) {
2749
- if (stroke.shape === "line" && event.shiftKey) {
2750
- const w = doc.current.width ?? 1024, h = doc.current.height ?? 1024, a = stroke.points[0];
2751
- const snapped = snapLine({
2752
- x: a.x * w,
2753
- y: a.y * h
2754
- }, {
2755
- x: point.x * w,
2756
- y: point.y * h
2757
- });
2758
- point = {
2759
- x: snapped.x / w,
2760
- y: snapped.y / h
2761
- };
2762
- }
2763
- stroke.points = [stroke.points[0], point];
2764
- } else {
2765
- const last = stroke.points.at(-1);
2766
- if (Math.hypot(last.x - point.x, last.y - point.y) < 1e-4) continue;
2767
- if (stroke.points.length >= 2e3) stroke.points = stroke.points.filter((_, i) => i % 2 === 0);
2768
- stroke.points.push(point);
2769
- }
2770
- }
2771
- 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"));
2772
3367
  }
2773
- schedule(false);
2774
3368
  };
2775
3369
  const end = (event, cancel = false) => {
2776
3370
  if (navigation.end(event)) return;
@@ -2793,7 +3387,7 @@ window.__ModuleLoader__.load({
2793
3387
  layer: drawn.layer,
2794
3388
  id: stroke.id
2795
3389
  });
2796
- setTool("select");
3390
+ chooseTool("select");
2797
3391
  }
2798
3392
  }
2799
3393
  active.current = null;
@@ -2819,7 +3413,7 @@ window.__ModuleLoader__.load({
2819
3413
  available: () => enabled && agentEnabled,
2820
3414
  previewEnabled: () => agentPreview,
2821
3415
  open: () => onOpen(),
2822
- busy: () => busy || Boolean(active.current) || Boolean(textEdit) || sizeGesture.current,
3416
+ busy: () => operationGate.current.running || busy || Boolean(active.current) || Boolean(textEdit) || sizeGesture.current,
2823
3417
  document: () => doc.current,
2824
3418
  snapshot: () => ({
2825
3419
  documentId: documentId.current,
@@ -2866,23 +3460,18 @@ window.__ModuleLoader__.load({
2866
3460
  previewEnabled: () => agentAdapter.current.previewEnabled()
2867
3461
  });
2868
3462
  (0, react.useEffect)(() => {
2869
- if (!enabled || !agentEnabled) return;
3463
+ if (!enabled || !agentEnabled || !hydrated) return;
2870
3464
  const api = Object.freeze({
2871
3465
  version: 2,
2872
3466
  sessionId,
2873
3467
  execute: (request) => agentRun.current.execute(request),
2874
- export: async (format) => {
2875
- if (agentAdapter.current.busy() || agentRun.current.locked) throw Error("Sketch is being edited");
2876
- const { blob, extension } = await agentAdapter.current.export(format);
2877
- const data = new Uint8Array(await blob.arrayBuffer());
2878
- let raw = "";
2879
- for (let i = 0; i < data.length; i += 8192) raw += String.fromCharCode(...data.subarray(i, i + 8192));
2880
- return {
2881
- extension,
2882
- mediaType: blob.type,
2883
- base64: btoa(raw)
2884
- };
2885
- }
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
+ })
2886
3475
  });
2887
3476
  window.dshSketchAgent = api;
2888
3477
  return () => {
@@ -2892,14 +3481,15 @@ window.__ModuleLoader__.load({
2892
3481
  enabled,
2893
3482
  agentEnabled,
2894
3483
  rpc,
2895
- sessionId
3484
+ sessionId,
3485
+ hydrated
2896
3486
  ]);
2897
3487
  (0, react.useEffect)(() => {
2898
3488
  if (!enabled || !agentEnabled) {
2899
3489
  if (agentRun.current.locked) agentRun.current.stop();
2900
3490
  return;
2901
3491
  }
2902
- if (!rpc || !sessionId) return;
3492
+ if (!rpc || !sessionId || !hydrated) return;
2903
3493
  let live = true;
2904
3494
  const disconnect = connectSketchAgent(rpc, sessionId, (request) => {
2905
3495
  if (!live) throw Error("Sketch session disconnected");
@@ -2907,7 +3497,7 @@ window.__ModuleLoader__.load({
2907
3497
  }, (message) => {
2908
3498
  agentRun.current.fail();
2909
3499
  setError(message);
2910
- }, () => 350);
3500
+ }, () => agentRun.current.locked ? 350 : 2e3);
2911
3501
  return () => {
2912
3502
  live = false;
2913
3503
  disconnect();
@@ -2916,23 +3506,18 @@ window.__ModuleLoader__.load({
2916
3506
  enabled,
2917
3507
  agentEnabled,
2918
3508
  rpc,
2919
- sessionId
3509
+ sessionId,
3510
+ hydrated
2920
3511
  ]);
2921
- const attach = async () => {
2922
- if (!enabled || busy || agentRun.current?.locked) return;
2923
- setBusy(true);
2924
- setError("");
2925
- try {
3512
+ const attach = () => {
3513
+ if (!enabled) return;
3514
+ return runFile(async () => {
2926
3515
  paint();
2927
- 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"));
2928
3517
  await saveChanges();
2929
3518
  await attachSketch(blob);
2930
3519
  onClose();
2931
- } catch {
2932
- setError(t("sketchFailed"));
2933
- } finally {
2934
- setBusy(false);
2935
- }
3520
+ });
2936
3521
  };
2937
3522
  const exportFile = async (format = "png") => {
2938
3523
  paint();
@@ -2952,8 +3537,8 @@ window.__ModuleLoader__.load({
2952
3537
  };
2953
3538
  agentAdapter.current.export = exportFile;
2954
3539
  const download = async (format) => {
3540
+ setError("");
2955
3541
  const { blob, extension } = await exportFile(format);
2956
- await saveChanges();
2957
3542
  const url = URL.createObjectURL(blob), link = document.createElement("a");
2958
3543
  link.href = url;
2959
3544
  link.download = `${(saved.current?.name || "sketch").replace(/[\\/:*?"<>|\u0000-\u001f]/g, "-").slice(0, 80)}.${extension}`;
@@ -2962,24 +3547,17 @@ window.__ModuleLoader__.load({
2962
3547
  link.remove();
2963
3548
  setTimeout(() => URL.revokeObjectURL(url), 1e4);
2964
3549
  };
2965
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [enabled && agentEnabled && !open && !noticeHidden && agentState !== "idle" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2966
- className: "codexSketchBackgroundStatus",
2967
- role: "status",
2968
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2969
- type: "button",
2970
- onClick: onOpen,
2971
- children: t(`sketchRun_${agentState}`)
2972
- }), agentLocked ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2973
- type: "button",
2974
- "aria-label": t("sketchRunStop"),
2975
- onClick: () => agentRun.current.stop(),
2976
- children: "×"
2977
- }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2978
- type: "button",
2979
- "aria-label": t("sketchCancel"),
2980
- onClick: () => setNoticeHidden(true),
2981
- children: "×"
2982
- })]
3550
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [enabled && agentEnabled && !open && !noticeHidden ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SketchRunStatus, {
3551
+ state: agentState,
3552
+ floating: true,
3553
+ t,
3554
+ onOpen,
3555
+ onStop: () => agentRun.current.stop(),
3556
+ onResume: () => {
3557
+ setError("");
3558
+ agentRun.current.resume();
3559
+ },
3560
+ onDismiss: () => setNoticeHidden(true)
2983
3561
  }) : null, /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("dialog", {
2984
3562
  ref: dialog,
2985
3563
  className: "codexSketchDialog codexSketchStudio codexLayerStudio",
@@ -3026,7 +3604,7 @@ window.__ModuleLoader__.load({
3026
3604
  type: "button",
3027
3605
  "aria-label": t("sketchCancel"),
3028
3606
  title: t("sketchCancel"),
3029
- disabled: busy && !agentLocked,
3607
+ disabled: busy && hydrated && !agentLocked,
3030
3608
  onClick: close,
3031
3609
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(WorkspaceIcon, { name: "close" })
3032
3610
  }),
@@ -3039,8 +3617,7 @@ window.__ModuleLoader__.load({
3039
3617
  hasContent: doc.current.layers.some((l) => l.visible && (l.image || l.strokes.length)),
3040
3618
  disabled: agentLocked || busy,
3041
3619
  t,
3042
- report: setError,
3043
- onWorking: setBusy
3620
+ runOperation: runFile
3044
3621
  }),
3045
3622
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3046
3623
  className: "codexSketchHeading",
@@ -3121,24 +3698,28 @@ window.__ModuleLoader__.load({
3121
3698
  })
3122
3699
  ]
3123
3700
  }),
3124
- enabled && agentEnabled && agentState !== "idle" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3701
+ enabled && agentEnabled && !noticeHidden ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SketchRunStatus, {
3702
+ state: agentState,
3703
+ t,
3704
+ onStop: () => agentRun.current.stop(),
3705
+ onResume: () => {
3706
+ setError("");
3707
+ agentRun.current.resume();
3708
+ },
3709
+ onDismiss: () => setNoticeHidden(true)
3710
+ }) : null,
3711
+ recovered ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3125
3712
  className: "codexSketchAgentStatus",
3126
3713
  role: "status",
3127
- "aria-live": "polite",
3128
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t(`sketchRun_${agentState}`) }), agentLocked ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
3714
+ children: [t("sketchRecovered"), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
3129
3715
  type: "button",
3130
- "aria-label": t("sketchRunStop"),
3131
- title: t("sketchRunStop"),
3132
- onClick: () => agentRun.current.stop(),
3716
+ onClick: () => setRecovered(false),
3717
+ "aria-label": t("sketchDismissStatus"),
3133
3718
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(WorkspaceIcon, {
3134
3719
  name: "close",
3135
- size: 16
3720
+ size: 14
3136
3721
  })
3137
- }) : agentState === "stopped" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
3138
- type: "button",
3139
- onClick: () => agentRun.current.resume(),
3140
- children: t("sketchRunResume")
3141
- }) : null]
3722
+ })]
3142
3723
  }) : null,
3143
3724
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3144
3725
  className: `codexLayerBody ${layersOpen ? "withLayers" : ""}`,
@@ -3254,6 +3835,7 @@ window.__ModuleLoader__.load({
3254
3835
  width,
3255
3836
  fill: fillShape && ["rectangle", "circle"].includes(tool),
3256
3837
  brush: tool === "pen" ? brush : "pen",
3838
+ ...tool === "pen" ? { brushVersion: 2 } : {},
3257
3839
  pressure: event.pointerType === "pen" ? Math.max(.2, event.pressure) : 1,
3258
3840
  points: [start]
3259
3841
  });
@@ -3329,13 +3911,10 @@ window.__ModuleLoader__.load({
3329
3911
  sizeGesture.current = false;
3330
3912
  },
3331
3913
  onChange: (value) => {
3332
- if (opacityMode) {
3333
- setFlow(value);
3334
- if (selected) editObject({ opacity: value / 100 });
3335
- } else {
3336
- setWidth(value);
3337
- if (selected) editObject({ width: value });
3338
- }
3914
+ if (opacityMode) if (selected) editObject({ opacity: value / 100 });
3915
+ else setFlow(value);
3916
+ else if (selected) editObject({ width: value });
3917
+ else setWidth(value);
3339
3918
  }
3340
3919
  }),
3341
3920
  textEdit ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("form", {
@@ -3371,7 +3950,7 @@ window.__ModuleLoader__.load({
3371
3950
  schedule();
3372
3951
  }
3373
3952
  setTextEdit(null);
3374
- setTool("select");
3953
+ chooseTool("select");
3375
3954
  } catch (e) {
3376
3955
  setError(e.message);
3377
3956
  }
@@ -3440,89 +4019,11 @@ window.__ModuleLoader__.load({
3440
4019
  !doc.current.layers.some((layer) => layer.image) ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("small", { children: t("sketchPicturesEmpty") }) : null
3441
4020
  ]
3442
4021
  }) : null,
3443
- layersOpen ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("aside", {
3444
- className: "codexSketchLayers",
3445
- "aria-label": t("sketchLayers"),
3446
- children: [
3447
- /* @__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", {
3448
- type: "button",
3449
- title: t("sketchLayerAdd"),
3450
- "aria-label": t("sketchLayerAdd"),
3451
- disabled: agentLocked || busy || doc.current.layers.length >= 8,
3452
- onClick: () => change("add"),
3453
- children: "+"
3454
- })] }),
3455
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
3456
- className: "codexLayerList",
3457
- children: doc.current.layers.slice().reverse().map((layer) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3458
- className: "codexLayerRow",
3459
- "data-active": layer.id === doc.current.active,
3460
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
3461
- type: "button",
3462
- disabled: agentLocked || busy,
3463
- "aria-label": `${t("sketchLayerVisible")} ${layer.id}`,
3464
- "aria-pressed": layer.visible,
3465
- onClick: () => change("visible", layer.id),
3466
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(WorkspaceIcon, {
3467
- name: layer.visible ? "eye" : "eyeOff",
3468
- size: 18
3469
- })
3470
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
3471
- type: "button",
3472
- disabled: agentLocked || busy,
3473
- "aria-pressed": layer.id === doc.current.active,
3474
- onClick: () => change("select", layer.id),
3475
- children: layer.name || `${t("sketchLayer")} ${layer.id}`
3476
- })]
3477
- }, layer.id))
3478
- }),
3479
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("label", {
3480
- className: "codexSketchLayerLabel",
3481
- children: t("sketchLayerName")
3482
- }),
3483
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
3484
- disabled: agentLocked || busy,
3485
- "aria-label": t("sketchLayerName"),
3486
- defaultValue: current.name,
3487
- placeholder: `${t("sketchLayer")} ${current.id}`,
3488
- maxLength: 40,
3489
- onBlur: (e) => {
3490
- if (e.target.value !== current.name) change("rename", current.id, e.target.value);
3491
- },
3492
- onKeyDown: (e) => {
3493
- if (e.key === "Enter") e.currentTarget.blur();
3494
- }
3495
- }, current.id + "-" + current.name),
3496
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
3497
- className: "codexLayerActions",
3498
- children: [
3499
- "duplicate",
3500
- "up",
3501
- "down",
3502
- "delete"
3503
- ].map((action) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
3504
- type: "button",
3505
- title: t(`sketchLayer_${action}`),
3506
- "aria-label": t(`sketchLayer_${action}`),
3507
- 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],
3508
- onClick: () => change(action),
3509
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(WorkspaceIcon, {
3510
- name: action === "delete" ? "clear" : action,
3511
- size: 17
3512
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t(`sketchLayer_${action}`) })]
3513
- }, action))
3514
- }),
3515
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
3516
- type: "button",
3517
- className: "codexSketchClearLayer",
3518
- disabled: agentLocked || busy || !current.strokes.length && !current.image,
3519
- onClick: () => change("clear"),
3520
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(WorkspaceIcon, {
3521
- name: "clear",
3522
- size: 16
3523
- }), t("sketchClearLayer")]
3524
- })
3525
- ]
4022
+ layersOpen ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SketchLayerPanel, {
4023
+ document: doc.current,
4024
+ disabled: agentLocked || busy,
4025
+ change,
4026
+ t
3526
4027
  }) : null
3527
4028
  ]
3528
4029
  }),
@@ -3540,70 +4041,15 @@ window.__ModuleLoader__.load({
3540
4041
  navigation,
3541
4042
  t
3542
4043
  }),
3543
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3544
- className: "codexSketchPill",
3545
- role: "toolbar",
3546
- "aria-label": t("sketchTitle"),
3547
- title: t("sketchShortcuts"),
3548
- children: [
3549
- [
3550
- "select",
3551
- "pen",
3552
- "text",
3553
- "eraser"
3554
- ].map((name) => {
3555
- const drawing = [
3556
- "pen",
3557
- "pencil",
3558
- "marker"
3559
- ].includes(name);
3560
- const label = t(drawing ? `sketchBrush_${name}` : `sketchTool_${name}`);
3561
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
3562
- type: "button",
3563
- "aria-label": label,
3564
- title: label,
3565
- "aria-pressed": drawing ? tool === "pen" && brush === name : tool === name,
3566
- disabled: agentLocked || busy,
3567
- onClick: () => {
3568
- setTool(drawing ? "pen" : name);
3569
- if (name !== "select") setSelection(null);
3570
- if (drawing) setBrush(name);
3571
- },
3572
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(WorkspaceIcon, {
3573
- name,
3574
- size: 23
3575
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: label })]
3576
- }, name);
3577
- }),
3578
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
3579
- className: "codexSketchShapeToggle",
3580
- type: "button",
3581
- "aria-expanded": shapesOpen,
3582
- disabled: agentLocked || busy,
3583
- onClick: () => setShapesOpen((v) => !v),
3584
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(WorkspaceIcon, {
3585
- name: "rectangle",
3586
- size: 23
3587
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("sketchShapes") })]
3588
- }),
3589
- shapesOpen ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
3590
- className: "codexSketchShapeMenu",
3591
- children: [
3592
- "line",
3593
- "arrow",
3594
- "rectangle",
3595
- "circle"
3596
- ].map((name) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
3597
- type: "button",
3598
- onClick: () => {
3599
- setTool(name);
3600
- setSelection(null);
3601
- setShapesOpen(false);
3602
- },
3603
- children: t(`sketchTool_${name}`)
3604
- }, name))
3605
- }) : null
3606
- ]
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
3607
4053
  }),
3608
4054
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3609
4055
  className: "codexLayerBrush",
@@ -3614,20 +4060,6 @@ window.__ModuleLoader__.load({
3614
4060
  checked: fillShape,
3615
4061
  onChange: (e) => setFillShape(e.target.checked)
3616
4062
  }), t("sketchFill")] }) : null,
3617
- tool === "pen" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("select", {
3618
- "aria-label": t("sketchBrush"),
3619
- value: brush,
3620
- onChange: (e) => setBrush(e.target.value),
3621
- disabled: agentLocked || busy,
3622
- children: [
3623
- "pen",
3624
- "pencil",
3625
- "marker"
3626
- ].map((b) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
3627
- value: b,
3628
- children: t(`sketchBrush_${b}`)
3629
- }, b))
3630
- }) : null,
3631
4063
  selected ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3632
4064
  className: "codexSketchObjectActions",
3633
4065
  children: [
@@ -3709,10 +4141,14 @@ window.__ModuleLoader__.load({
3709
4141
  })
3710
4142
  ]
3711
4143
  }),
3712
- error ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
4144
+ error ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
3713
4145
  className: "codexSketchHint",
3714
4146
  role: "alert",
3715
- 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]
3716
4152
  }) : null
3717
4153
  ]
3718
4154
  })] });
@@ -3734,7 +4170,7 @@ window.__ModuleLoader__.load({
3734
4170
  .codexLayerBrush>select{background:var(--sketch-bg);color:inherit;border:1px solid var(--sketch-line);border-radius:8px;padding:6px}
3735
4171
 
3736
4172
  .codexSketchBackgroundStatus{position:fixed;top:16px;left:50%;transform:translateX(-50%);z-index:1000;display:flex;gap:8px;align-items:center;padding:6px 10px;border-radius:12px;background:var(--dsw-alias-bg-layer-1,#f5f5f7);color:var(--dsw-alias-label-primary,#202124);border:1px solid #8883;box-shadow:0 4px 16px #0002;font:12px system-ui}.codexSketchBackgroundStatus button{color:inherit;background:none;border:0;cursor:pointer}
3737
- .codexSketchAgentStatus{display:flex;align-items:center;justify-content:center;gap:8px;min-height:24px;color:var(--sketch-muted);font-size:12px}.codexSketchAgentStatus button{padding:3px 6px;border-radius:6px;background:var(--sketch-line)}
4173
+ .codexSketchAgentStatus{display:flex;align-items:center;justify-content:center;gap:8px;min-height:24px;color:var(--sketch-muted);font-size:12px}.codexSketchAgentStatus button{min-height:30px;padding:4px 10px;border-radius:16px;background:var(--sketch-line)}.codexSketchAgentStatus{align-self:center;max-width:100%;min-height:34px;padding:0 6px;flex-wrap:wrap}.codexSketchBackgroundStatus button{min-height:30px;display:inline-flex;align-items:center;gap:5px}.codexSketchAgentStatus .codexSketchStop,.codexSketchBackgroundStatus .codexSketchStop{font-weight:500;background:var(--sketch-line,#8882);border-radius:16px;padding:4px 10px}
3738
4174
  .codexSketchDialog{--sketch-bg:var(--dsw-alias-bg-layer-1,#f5f5f7);--sketch-fg:var(--dsw-alias-label-primary,#202124);--sketch-muted:var(--dsw-alias-label-secondary,#727279);--sketch-line:var(--dsw-alias-border-l2,#8883);--sketch-glass:color-mix(in srgb,var(--sketch-bg) 90%,transparent);box-sizing:border-box;width:min(1280px,calc(100vw - 24px));height:94dvh;max-height:94dvh;margin:auto;padding:10px;border:1px solid var(--sketch-line);border-radius:18px;background:var(--sketch-bg);color:var(--sketch-fg);box-shadow:0 24px 90px #0004;overflow:hidden;font:13px/1.4 system-ui}
3739
4175
  .codexSketchDialog[open]{display:flex;flex-direction:column;gap:8px}
3740
4176
  .codexSketchDialog::backdrop{background:#0005;backdrop-filter:blur(12px)}
@@ -3915,9 +4351,27 @@ window.__ModuleLoader__.load({
3915
4351
  //#endregion
3916
4352
  //#region src/client-locales.js
3917
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: "草稿存储空间已满。请先导出,或删除不需要的草稿后保存。",
3918
4367
  sketchSizeShort: "粗细",
3919
4368
  sketchObjectDuplicate: "复制对象",
3920
4369
  sketchObjectDelete: "删除对象",
4370
+ sketchBrushHint_pen: "实色圆头墨线",
4371
+ sketchBrushHint_pencil: "细腻颗粒,叠画加深",
4372
+ sketchBrushHint_marker: "半透明平头,适合高亮",
4373
+ sketchDismissStatus: "收起提示",
4374
+ sketchRunResumeHint: "允许 Agent 接收后续绘图请求,不会自动重发消息",
3921
4375
  sketchTool_select: "选择",
3922
4376
  sketchTool_text: "文字",
3923
4377
  sketchTool_arrow: "箭头",
@@ -4321,9 +4775,27 @@ window.__ModuleLoader__.load({
4321
4775
  imageRemoveAnnotation: "删除标注"
4322
4776
  };
4323
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.",
4324
4791
  sketchSizeShort: "Size",
4325
4792
  sketchObjectDuplicate: "Duplicate object",
4326
4793
  sketchObjectDelete: "Delete object",
4794
+ sketchBrushHint_pen: "Solid round ink",
4795
+ sketchBrushHint_pencil: "Grain builds with repeated strokes",
4796
+ sketchBrushHint_marker: "Translucent flat tip for highlights",
4797
+ sketchDismissStatus: "Dismiss status",
4798
+ sketchRunResumeHint: "Allow subsequent drawing requests; does not resend a message",
4327
4799
  sketchTool_select: "Select",
4328
4800
  sketchTool_text: "Text",
4329
4801
  sketchTool_arrow: "Arrow",
@@ -4729,6 +5201,15 @@ window.__ModuleLoader__.load({
4729
5201
  //#endregion
4730
5202
  //#region src/client-styles.js
4731
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)}
4732
5213
  .codexComposerQuota[data-warning=true]{color:var(--dsw-alias-state-error-primary)}
4733
5214
  .codexComposerQuota[data-warning=true] progress{accent-color:var(--dsw-alias-state-error-primary)}
4734
5215
  .codexComposerQuota[data-warning=true] progress::-webkit-progress-value{background:var(--dsw-alias-state-error-primary)}
@@ -4781,20 +5262,9 @@ window.__ModuleLoader__.load({
4781
5262
  .codexSubscriptionContextModelCopy span{font-size:11px;line-height:16px;color:var(--dsw-alias-label-tertiary)}
4782
5263
  .codexSubscriptionContextInput{width:116px}
4783
5264
  .codexSubscriptionSearch{display:flex;flex-direction:column;gap:7px}
4784
- .codexSubscriptionSearchChoices{display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:6px}
4785
- .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}
4786
- .codexSubscriptionSearchChoice:has(input:disabled){cursor:not-allowed;opacity:.5}
4787
- .codexSubscriptionSearchChoice:has(input:checked){border-color:var(--dsw-alias-label-primary);background:var(--dsw-alias-bg-layer-2)}
4788
- .codexSubscriptionSearchChoice:has(input:focus-visible){outline:2px solid var(--dsw-alias-border-l3);outline-offset:2px}
4789
- .codexSubscriptionSearchInput{width:14px;height:14px;margin:0;accent-color:var(--dsw-alias-label-primary);cursor:inherit}
4790
- .codexSubscriptionSearchCopy{display:block;min-width:0;pointer-events:none}
4791
- .codexSubscriptionSearchCopy strong,.codexSubscriptionSearchCopy span{display:block}
4792
- .codexSubscriptionSearchCopy strong{font-size:12px;line-height:18px;font-weight:500;color:var(--dsw-alias-label-secondary)}
4793
- .codexSubscriptionSearchChoice:has(input:checked) strong{color:var(--dsw-alias-label-primary)}
4794
- .codexSubscriptionSearchCopy span{font-size:11px;line-height:16px;color:var(--dsw-alias-label-tertiary)}
4795
5265
  .codexSubscriptionDivider{height:1px;background:var(--dsw-alias-border-l2)}
4796
5266
  .codexSubscriptionQuotaModes[data-saving=true] .codexSubscriptionQuotaMode:has(input:disabled){cursor:wait;opacity:1}
4797
- .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}
4798
5268
  .codexSubscriptionAccountRow,.codexSubscriptionSectionHead{display:flex;align-items:center;justify-content:space-between;gap:12px}
4799
5269
  .codexSubscriptionStatus{display:flex;align-items:center;gap:8px;font-size:14px;line-height:22px;font-weight:500}
4800
5270
  .codexSubscriptionAccounts{display:flex;flex-direction:column;border-top:1px solid var(--dsw-alias-border-l2)}
@@ -6082,6 +6552,7 @@ window.__ModuleLoader__.load({
6082
6552
  let modelRefreshGeneration = 0;
6083
6553
  let modelRefreshStarted = false;
6084
6554
  let disposed = false;
6555
+ let subagentBackendAvailable = false;
6085
6556
  const sameModels = (left, right) => left.length === right.length && left.every((model, index) => JSON.stringify(model) === JSON.stringify(right[index]));
6086
6557
  const nativeSnapshot = () => scope.getSnapshot();
6087
6558
  const read = () => {
@@ -6095,6 +6566,9 @@ window.__ModuleLoader__.load({
6095
6566
  return Object.freeze({
6096
6567
  status: current.status,
6097
6568
  ...capabilities,
6569
+ connectionMode: value?.connectionMode === "websocket" ? "websocket" : "sse",
6570
+ subagentBackend: value?.subagentBackend === "codex" ? "codex" : "dsh",
6571
+ subagentBackendAvailable,
6098
6572
  quickQuotaMode: normalizeQuickQuotaMode(value?.[QUICK_QUOTA_MODE_FIELD], value?.[LEGACY_QUICK_QUOTA_FIELD]),
6099
6573
  searchProvider: normalizeSearchProvider(value?.[SEARCH_PROVIDER_FIELD]),
6100
6574
  speedMode: normalizeSpeedMode(value?.[SPEED_MODE_FIELD]),
@@ -6128,6 +6602,7 @@ window.__ModuleLoader__.load({
6128
6602
  publish();
6129
6603
  });
6130
6604
  const acceptFallback = (value) => {
6605
+ subagentBackendAvailable = value?.subagentBackendAvailable === true;
6131
6606
  if (!modelRefreshStarted) {
6132
6607
  contextModels = Array.isArray(value?.contextModels) ? value.contextModels : [];
6133
6608
  verbosityModels = Array.isArray(value?.verbosityModels) ? value.verbosityModels : [];
@@ -6138,6 +6613,8 @@ window.__ModuleLoader__.load({
6138
6613
  fallback = {
6139
6614
  status: "ready",
6140
6615
  value: {
6616
+ connectionMode: value?.connectionMode === "websocket" ? "websocket" : "sse",
6617
+ subagentBackend: value?.subagentBackend === "codex" ? "codex" : "dsh",
6141
6618
  ...readCapabilitySettings(value),
6142
6619
  [QUICK_QUOTA_MODE_FIELD]: normalizeQuickQuotaMode(value?.[QUICK_QUOTA_MODE_FIELD], value?.[LEGACY_QUICK_QUOTA_FIELD]),
6143
6620
  [SEARCH_PROVIDER_FIELD]: normalizeSearchProvider(value?.[SEARCH_PROVIDER_FIELD]),
@@ -6161,6 +6638,7 @@ window.__ModuleLoader__.load({
6161
6638
  try {
6162
6639
  const value = unwrap(await rpc.call(CHANNEL, "preferences/status", {}));
6163
6640
  if (current !== generation || disposed) return;
6641
+ subagentBackendAvailable = value?.subagentBackendAvailable === true;
6164
6642
  if (nativeSnapshot().status === "ready") {
6165
6643
  if (!modelRefreshStarted) {
6166
6644
  contextModels = Array.isArray(value?.contextModels) ? value.contextModels : [];
@@ -6221,7 +6699,7 @@ window.__ModuleLoader__.load({
6221
6699
  failedPatch = void 0;
6222
6700
  publish();
6223
6701
  try {
6224
- if (nativeSnapshot().status === "ready") {
6702
+ if (nativeSnapshot().status === "ready" && !Object.hasOwn(patch, "subagentBackend")) {
6225
6703
  for (const [field, value] of entries) {
6226
6704
  if (current !== generation) return;
6227
6705
  await scope.set(field, value);
@@ -6547,7 +7025,7 @@ window.__ModuleLoader__.load({
6547
7025
  }
6548
7026
  //#endregion
6549
7027
  //#region src/version.js
6550
- const PACKAGE_VERSION = "2.1.0-beta.5";
7028
+ const PACKAGE_VERSION = "2.1.1-beta.1";
6551
7029
  //#endregion
6552
7030
  //#region src/client-recovery.js
6553
7031
  async function recoveryCall(rpc, endpoint, payload = {}, timeoutMs = 1e4) {
@@ -7186,64 +7664,76 @@ window.__ModuleLoader__.load({
7186
7664
  const snapshot = (0, react.useSyncExternalStore)(preference.subscribe, preference.getSnapshot);
7187
7665
  const disabled = snapshot.status !== "ready" || !snapshot.writable || snapshot.saving;
7188
7666
  const active = snapshot.imageGeneration || snapshot.imageEditing;
7189
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
7667
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("section", {
7190
7668
  className: "codexSubscriptionCard codexImageSettings",
7191
7669
  "aria-label": t("imageSettings"),
7192
- children: [
7193
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", { children: t("imageSettings") }),
7194
- Object.keys(IMAGE_SETTING_GROUPS).map((group) => {
7195
- const value = imageGroupValue(snapshot, group);
7196
- return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ImageChoice, {
7197
- label: t(group),
7198
- hint: t(`${group}Hint`),
7199
- value,
7200
- text: t(value === "mixed" ? "imageGroupMixed" : `${group}_${value}`),
7201
- disabled,
7202
- items: ["on", "off"].map((id) => ({
7203
- id,
7204
- label: t(`${group}_${id}`)
7205
- })),
7206
- onSelect: (id) => {
7207
- preference.set(imageGroupPatch(group, id === "on"));
7208
- }
7209
- }, group);
7210
- }),
7211
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
7212
- className: "codexImageDefaultsGroup",
7213
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(ImageChoice, {
7214
- label: t("imageModel"),
7215
- value: snapshot.imageModel,
7216
- text: modelLabel(snapshot.imageModel),
7217
- disabled: disabled || !active,
7218
- items: Object.keys(IMAGE_MODELS).map((id) => ({
7219
- id,
7220
- label: `${modelLabel(id)}${id.includes("2.5") ? ` · ${t("imageExperimental")}` : ""}`
7221
- })),
7222
- onSelect: (imageModel) => {
7223
- preference.set({
7224
- imageModel,
7225
- imageQuality: "auto"
7226
- });
7227
- }
7228
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ImageChoice, {
7229
- label: t("imageQuality"),
7230
- value: snapshot.imageQuality,
7231
- text: t(`imageQuality_${snapshot.imageQuality}`),
7232
- disabled: disabled || !active,
7233
- items: IMAGE_MODELS[snapshot.imageModel].map((id) => ({
7234
- id,
7235
- label: t(`imageQuality_${id}`)
7236
- })),
7237
- onSelect: (imageQuality) => {
7238
- preference.set({ imageQuality });
7239
- }
7240
- })]
7241
- }),
7242
- snapshot.imageModel.includes("2.5") ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
7243
- className: "codexSubscriptionPreferenceHint",
7244
- children: t("imageModelHint")
7245
- }) : null
7246
- ]
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
+ })
7247
7737
  });
7248
7738
  }
7249
7739
  //#endregion
@@ -7458,10 +7948,9 @@ window.__ModuleLoader__.load({
7458
7948
  function SearchProviderPreference({ preference, t }) {
7459
7949
  const snapshot = usePreferenceSnapshot(preference);
7460
7950
  const writable = snapshot.status === "ready" && snapshot.writable === true;
7461
- const choice = (value, label, hint) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
7462
- className: "codexSubscriptionSearchChoice",
7951
+ const choice = (value, label) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
7952
+ className: "codexSubscriptionQuotaMode",
7463
7953
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
7464
- className: "codexSubscriptionSearchInput",
7465
7954
  type: "radio",
7466
7955
  name: "codex-subscription-search-provider",
7467
7956
  checked: snapshot.searchProvider === value,
@@ -7469,39 +7958,39 @@ window.__ModuleLoader__.load({
7469
7958
  onChange: () => {
7470
7959
  preference.set({ [SEARCH_PROVIDER_FIELD]: value });
7471
7960
  }
7472
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
7473
- className: "codexSubscriptionSearchCopy",
7474
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("strong", { children: label }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: hint })]
7475
- })]
7961
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: label })]
7476
7962
  });
7963
+ const hint = snapshot.searchProvider === "dsh" ? "searchDshHint" : snapshot.searchProvider === "codex" ? "searchCodexHint" : "searchAutoHint";
7477
7964
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
7478
7965
  className: "codexSubscriptionSearch",
7479
- children: [
7480
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
7481
- className: "codexSubscriptionSearchHead",
7482
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", { children: t("searchTitle") }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
7483
- className: "codexSubscriptionSearchScope",
7484
- 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)
7485
7976
  })]
7486
- }),
7487
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
7488
- className: "codexSubscriptionSearchChoices",
7977
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
7978
+ className: "codexSubscriptionSearchChoices codexSubscriptionQuotaModes",
7489
7979
  "data-saving": snapshot.saving || void 0,
7490
7980
  "aria-busy": snapshot.saving || void 0,
7491
7981
  role: "radiogroup",
7492
7982
  "aria-label": t("searchTitle"),
7493
7983
  children: [
7494
- choice(SEARCH_PROVIDER_AUTO, t("searchAuto"), t("searchAutoHint")),
7495
- choice("dsh", t("searchDsh"), t("searchDshHint")),
7496
- 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")
7497
7987
  ]
7498
- }),
7499
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CapabilityPreferences, {
7500
- preference,
7501
- t,
7502
- section: "search"
7503
- })
7504
- ]
7988
+ })]
7989
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CapabilityPreferences, {
7990
+ preference,
7991
+ t,
7992
+ section: "search"
7993
+ })]
7505
7994
  });
7506
7995
  }
7507
7996
  function ContextWindowPreference({ preference, t }) {
@@ -7667,16 +8156,99 @@ window.__ModuleLoader__.load({
7667
8156
  function PreferencesCard({ preference, t, section = "display" }) {
7668
8157
  const snapshot = usePreferenceSnapshot(preference);
7669
8158
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
7670
- className: "codexSubscriptionCard codexSubscriptionPreferencesCard",
8159
+ className: section === "advanced" ? "codexSubscriptionAdvancedPreferences" : "codexSubscriptionCard codexSubscriptionPreferencesCard",
7671
8160
  children: [section === "advanced" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
7672
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SearchProviderPreference, {
7673
- preference,
7674
- 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
+ ]
7675
8176
  }),
7676
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { className: "codexSubscriptionDivider" }),
7677
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ContextWindowPreference, {
7678
- preference,
7679
- 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
+ })
7680
8252
  })
7681
8253
  ] }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(QuickQuotaPreference, {
7682
8254
  preference,