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

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
@@ -105,6 +105,65 @@ window.__ModuleLoader__.load({
105
105
  context.fillStyle = context.strokeStyle;
106
106
  }
107
107
  //#endregion
108
+ //#region src/sketch-text.js
109
+ const cache = /* @__PURE__ */ new WeakMap();
110
+ function layoutSketchText(stroke, context, width, height) {
111
+ const saved = cache.get(stroke);
112
+ if (saved?.width === width && saved?.height === height) return saved.layout;
113
+ const [a, b] = stroke.points;
114
+ const x = Math.min(a.x, b.x) * width, y = Math.min(a.y, b.y) * height;
115
+ const w = Math.abs(b.x - a.x) * width, h = Math.abs(b.y - a.y) * height;
116
+ const wrap = (size) => {
117
+ context.font = `${size}px system-ui, sans-serif`;
118
+ const lines = [];
119
+ for (const paragraph of stroke.text.split("\n")) {
120
+ let line = "";
121
+ for (const char of paragraph) {
122
+ if (line && context.measureText(line + char).width > w) {
123
+ lines.push(line);
124
+ line = "";
125
+ }
126
+ line += char;
127
+ }
128
+ lines.push(line);
129
+ }
130
+ return lines;
131
+ };
132
+ let size = stroke.width, lines = wrap(size);
133
+ if (lines.length * size * 1.2 > h || lines.some((line) => context.measureText(line).width > w)) {
134
+ let low = 0, high = size;
135
+ for (let i = 0; i < 14; i++) {
136
+ const mid = (low + high) / 2, candidate = wrap(mid);
137
+ if (candidate.length * mid * 1.2 <= h && candidate.every((line) => context.measureText(line).width <= w)) low = mid;
138
+ else high = mid;
139
+ }
140
+ size = low;
141
+ lines = wrap(size);
142
+ }
143
+ const layout = {
144
+ x,
145
+ y,
146
+ size,
147
+ lines
148
+ };
149
+ cache.set(stroke, {
150
+ width,
151
+ height,
152
+ layout
153
+ });
154
+ return layout;
155
+ }
156
+ function newTextBounds(point) {
157
+ const a = {
158
+ x: Math.min(point.x, .65),
159
+ y: Math.min(point.y, .85)
160
+ };
161
+ return [a, {
162
+ x: a.x + .35,
163
+ y: a.y + .15
164
+ }];
165
+ }
166
+ //#endregion
108
167
  //#region src/sketch-document.js
109
168
  const SKETCH_SIZE = 1024;
110
169
  const MAX_SKETCH_STROKES = 2e3;
@@ -134,11 +193,10 @@ window.__ModuleLoader__.load({
134
193
  context.beginPath();
135
194
  const last = stroke.points.at(-1);
136
195
  if (stroke.shape === "text") {
137
- const x = Math.min(first.x, last.x) * size, y = Math.min(first.y, last.y) * height, w = Math.abs(last.x - first.x) * size, h = Math.abs(last.y - first.y) * height;
138
- const lines = stroke.text.split("\n"), fontSize = Math.min(stroke.width, h / Math.max(1, lines.length) / 1.2);
196
+ const { x, y, lines, size: fontSize } = layoutSketchText(stroke, context, size, height);
139
197
  context.font = `${fontSize}px system-ui, sans-serif`;
140
198
  context.textBaseline = "top";
141
- lines.forEach((line, i) => context.fillText(line, x, y + i * fontSize * 1.2, w));
199
+ lines.forEach((line, i) => context.fillText(line, x, y + i * fontSize * 1.2));
142
200
  } else if (stroke.shape === "arrow") {
143
201
  const x = last.x * size, y = last.y * height, a = Math.atan2(y - first.y * height, x - first.x * size), head = Math.min(Math.hypot(x - first.x * size, y - first.y * height) * .4, Math.max(12, stroke.width * 3));
144
202
  context.moveTo(first.x * size, first.y * height);
@@ -372,21 +430,55 @@ window.__ModuleLoader__.load({
372
430
  dirty: ref(false),
373
431
  documentId: ref(crypto.randomUUID()),
374
432
  documentRevision: ref(0),
433
+ restoreId: ref(null),
434
+ mounts: 0,
375
435
  agentAdapter: ref({}),
376
436
  agentSession: ref(null),
377
437
  agentRun: ref(null)
378
438
  };
379
439
  }
380
- function createSketchSessionRegistry() {
381
- const sessions = /* @__PURE__ */ new Map();
440
+ function createSketchSessionRegistry({ maxIdle = 8 } = {}) {
441
+ const sessions = /* @__PURE__ */ new Map(), archived = /* @__PURE__ */ new Map();
442
+ const prune = () => {
443
+ const idle = [...sessions].filter(([, s]) => !s.mounts && !s.dirty.current && !s.agentRun.current?.locked && s.agentRun.current?.state !== "stopped");
444
+ for (const [id, state] of idle.slice(0, Math.max(0, idle.length - maxIdle))) {
445
+ if (!state.saved.current && state.doc.current.layers.some((l) => l.image || l.strokes.length)) continue;
446
+ const restoreId = state.saved.current?.id ?? state.restoreId.current;
447
+ if (restoreId) archived.set(id, restoreId);
448
+ state.agentRun.current?.dispose();
449
+ state.images.current.clear();
450
+ sessions.delete(id);
451
+ }
452
+ };
382
453
  return {
383
454
  get(id) {
384
- if (!sessions.has(id)) sessions.set(id, createSketchSessionState());
385
- return sessions.get(id);
455
+ let state = sessions.get(id);
456
+ if (!state) {
457
+ state = createSketchSessionState();
458
+ state.restoreId.current = archived.get(id) ?? null;
459
+ archived.delete(id);
460
+ sessions.set(id, state);
461
+ }
462
+ state.retain = () => {
463
+ state.mounts++;
464
+ return () => {
465
+ state.mounts--;
466
+ prune();
467
+ };
468
+ };
469
+ sessions.delete(id);
470
+ sessions.set(id, state);
471
+ return state;
386
472
  },
473
+ prune,
474
+ stats: () => ({
475
+ resident: sessions.size,
476
+ archived: archived.size
477
+ }),
387
478
  dispose() {
388
479
  for (const value of sessions.values()) value.agentRun.current?.dispose();
389
480
  sessions.clear();
481
+ archived.clear();
390
482
  }
391
483
  };
392
484
  }
@@ -1068,100 +1160,231 @@ window.__ModuleLoader__.load({
1068
1160
  });
1069
1161
  }
1070
1162
  //#endregion
1071
- //#region src/sketch-run-status.jsx
1072
- function SketchRunStatus({ state, t, floating = false, onOpen, onStop, onResume, onDismiss }) {
1073
- if (state === "idle") return null;
1074
- const drawing = state === "drawing", recover = state === "stopped" || state === "failed";
1075
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1076
- className: floating ? "codexSketchBackgroundStatus" : "codexSketchAgentStatus",
1077
- role: "status",
1078
- "aria-live": "polite",
1163
+ //#region src/sketch-layer-panel.jsx
1164
+ function SketchLayerPanel({ document, disabled, change, t }) {
1165
+ const current = document.layers.find((layer) => layer.id === document.active);
1166
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("aside", {
1167
+ className: "codexSketchLayers",
1168
+ "aria-label": t("sketchLayers"),
1079
1169
  children: [
1080
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(WorkspaceIcon, {
1081
- name: drawing ? "pen" : state === "finished" ? "check" : "rectangle",
1082
- size: 15
1083
- }),
1084
- floating ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1170
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("header", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("strong", { children: t("sketchLayers") }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1085
1171
  type: "button",
1086
- onClick: onOpen,
1087
- children: t(`sketchRun_${state}`)
1088
- }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t(`sketchRun_${state}`) }),
1089
- drawing ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
1172
+ title: t("sketchLayerAdd"),
1173
+ "aria-label": t("sketchLayerAdd"),
1174
+ disabled: disabled || document.layers.length >= 8,
1175
+ onClick: () => change("add"),
1176
+ children: "+"
1177
+ })] }),
1178
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1179
+ className: "codexLayerList",
1180
+ children: document.layers.slice().reverse().map((layer) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1181
+ className: "codexLayerRow",
1182
+ "data-active": layer.id === document.active,
1183
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1184
+ type: "button",
1185
+ disabled,
1186
+ "aria-label": `${t("sketchLayerVisible")} ${layer.id}`,
1187
+ "aria-pressed": layer.visible,
1188
+ onClick: () => change("visible", layer.id),
1189
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(WorkspaceIcon, {
1190
+ name: layer.visible ? "eye" : "eyeOff",
1191
+ size: 18
1192
+ })
1193
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1194
+ type: "button",
1195
+ disabled,
1196
+ "aria-pressed": layer.id === document.active,
1197
+ onClick: () => change("select", layer.id),
1198
+ children: layer.name || `${t("sketchLayer")} ${layer.id}`
1199
+ })]
1200
+ }, layer.id))
1201
+ }),
1202
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("label", {
1203
+ className: "codexSketchLayerLabel",
1204
+ children: t("sketchLayerName")
1205
+ }),
1206
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
1207
+ disabled,
1208
+ "aria-label": t("sketchLayerName"),
1209
+ defaultValue: current.name,
1210
+ placeholder: `${t("sketchLayer")} ${current.id}`,
1211
+ maxLength: 40,
1212
+ onBlur: (e) => {
1213
+ if (e.target.value !== current.name) change("rename", current.id, e.target.value);
1214
+ },
1215
+ onKeyDown: (e) => {
1216
+ if (e.key === "Enter") e.currentTarget.blur();
1217
+ }
1218
+ }, current.id + "-" + current.name),
1219
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1220
+ className: "codexLayerActions",
1221
+ children: [
1222
+ "duplicate",
1223
+ "up",
1224
+ "down",
1225
+ "delete"
1226
+ ].map((action) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
1227
+ type: "button",
1228
+ title: t(`sketchLayer_${action}`),
1229
+ "aria-label": t(`sketchLayer_${action}`),
1230
+ 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],
1231
+ onClick: () => change(action),
1232
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(WorkspaceIcon, {
1233
+ name: action === "delete" ? "clear" : action,
1234
+ size: 17
1235
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t(`sketchLayer_${action}`) })]
1236
+ }, action))
1237
+ }),
1238
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
1090
1239
  type: "button",
1091
- className: "codexSketchStop",
1092
- onClick: onStop,
1240
+ className: "codexSketchClearLayer",
1241
+ disabled: disabled || !current.strokes.length && !current.image,
1242
+ onClick: () => change("clear"),
1093
1243
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(WorkspaceIcon, {
1094
- name: "stop",
1095
- size: 12
1096
- }), t("sketchRunStop")]
1097
- }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [recover ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1098
- type: "button",
1099
- title: t("sketchRunResumeHint"),
1100
- onClick: onResume,
1101
- children: t("sketchRunResume")
1102
- }) : null, !recover ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1244
+ name: "clear",
1245
+ size: 16
1246
+ }), t("sketchClearLayer")]
1247
+ })
1248
+ ]
1249
+ });
1250
+ }
1251
+ //#endregion
1252
+ //#region src/sketch-tool-picker.jsx
1253
+ function SketchToolPicker({ t, disabled, tool, brush, chooseBrush, chooseTool, shapesOpen, setShapesOpen }) {
1254
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1255
+ className: "codexSketchPill",
1256
+ role: "toolbar",
1257
+ "aria-label": t("sketchTitle"),
1258
+ title: t("sketchShortcuts"),
1259
+ children: [
1260
+ [
1261
+ "select",
1262
+ "pen",
1263
+ "pencil",
1264
+ "marker",
1265
+ "text",
1266
+ "eraser"
1267
+ ].map((name) => {
1268
+ const drawing = [
1269
+ "pen",
1270
+ "pencil",
1271
+ "marker"
1272
+ ].includes(name);
1273
+ const label = t(drawing ? `sketchBrush_${name}` : `sketchTool_${name}`);
1274
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
1275
+ type: "button",
1276
+ "aria-label": label,
1277
+ title: drawing ? `${label} · ${t(`sketchBrushHint_${name}`)}` : label,
1278
+ "aria-pressed": drawing ? tool === "pen" && brush === name : tool === name,
1279
+ disabled,
1280
+ onClick: () => {
1281
+ if (drawing) chooseBrush(name);
1282
+ else chooseTool(name);
1283
+ },
1284
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(WorkspaceIcon, {
1285
+ name,
1286
+ size: 23
1287
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: label })]
1288
+ }, name);
1289
+ }),
1290
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
1291
+ className: "codexSketchShapeToggle",
1103
1292
  type: "button",
1104
- "aria-label": t("sketchDismissStatus"),
1105
- title: t("sketchDismissStatus"),
1106
- onClick: onDismiss,
1107
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(WorkspaceIcon, {
1108
- name: "close",
1109
- size: 14
1110
- })
1111
- }) : null] })
1293
+ "aria-label": t("sketchShapes"),
1294
+ "aria-expanded": shapesOpen,
1295
+ "aria-pressed": [
1296
+ "line",
1297
+ "arrow",
1298
+ "rectangle",
1299
+ "circle"
1300
+ ].includes(tool),
1301
+ disabled,
1302
+ onClick: () => setShapesOpen((v) => !v),
1303
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(WorkspaceIcon, {
1304
+ name: [
1305
+ "line",
1306
+ "arrow",
1307
+ "rectangle",
1308
+ "circle"
1309
+ ].includes(tool) ? tool : "rectangle",
1310
+ size: 23
1311
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t([
1312
+ "line",
1313
+ "arrow",
1314
+ "rectangle",
1315
+ "circle"
1316
+ ].includes(tool) ? `sketchTool_${tool}` : "sketchShapes") })]
1317
+ }),
1318
+ shapesOpen ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1319
+ className: "codexSketchShapeMenu",
1320
+ children: [
1321
+ "line",
1322
+ "arrow",
1323
+ "rectangle",
1324
+ "circle"
1325
+ ].map((name) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1326
+ disabled,
1327
+ type: "button",
1328
+ "aria-pressed": tool === name,
1329
+ onClick: () => {
1330
+ chooseTool(name);
1331
+ setShapesOpen(false);
1332
+ },
1333
+ children: t(`sketchTool_${name}`)
1334
+ }, name))
1335
+ }) : null
1112
1336
  ]
1113
1337
  });
1114
1338
  }
1115
1339
  //#endregion
1116
- //#region src/sketch-layer-renderer.js
1117
- const NO_IMAGES = /* @__PURE__ */ new Map();
1118
- const surface = (width, height) => {
1119
- const c = document.createElement("canvas");
1120
- c.width = width;
1121
- c.height = height;
1122
- return c;
1123
- };
1124
- function paintSketchLayers(context, doc, cache, size = doc.width ?? 1024, height = doc.height ?? size, activeLayer, images = NO_IMAGES) {
1125
- context.globalCompositeOperation = "source-over";
1126
- context.globalAlpha = 1;
1127
- context.fillStyle = "#fff";
1128
- context.fillRect(0, 0, size, height);
1129
- for (const id of cache.keys()) if (!doc.layers.some((layer) => layer.id === id)) cache.delete(id);
1130
- for (const layer of doc.layers) {
1131
- if (!layer.visible) continue;
1132
- let entry = cache.get(layer.id);
1133
- if (!entry || entry.surface.width !== size || entry.surface.height !== height) {
1134
- entry = {
1135
- surface: surface(size, height),
1136
- base: surface(size, height)
1137
- };
1138
- cache.set(layer.id, entry);
1139
- }
1140
- const moving = layer.id === activeLayer;
1141
- const count = Math.max(0, layer.strokes.length - (moving ? 1 : 0));
1142
- const prefix = layer.strokes[count - 1];
1143
- if (entry.count !== count || entry.prefix !== prefix || entry.image !== layer.image || entry.strokes !== layer.strokes) {
1144
- const ctx = entry.base.getContext("2d");
1145
- const append = entry.count !== void 0 && count >= entry.count && entry.image === layer.image && (entry.strokes === layer.strokes || entry.strokes.slice(0, entry.count).every((stroke, index) => stroke === layer.strokes[index]));
1146
- if (!append) {
1147
- ctx.clearRect(0, 0, size, height);
1148
- const ref = layer.image, image = ref && images.get(ref.src);
1149
- if (image) ctx.drawImage(image, ref.x * size, ref.y * height, ref.width * size, ref.height * height);
1150
- }
1151
- paintSketch(ctx, layer.strokes, size, true, height, append ? entry.count : 0, count);
1152
- entry.count = count;
1153
- entry.prefix = prefix;
1154
- entry.image = layer.image;
1155
- entry.strokes = layer.strokes;
1156
- }
1157
- if (moving) {
1158
- const ctx = entry.surface.getContext("2d");
1159
- ctx.clearRect(0, 0, size, height);
1160
- ctx.drawImage(entry.base, 0, 0);
1161
- paintSketch(ctx, layer.strokes, size, true, height, layer.strokes.length - 1);
1162
- context.drawImage(entry.surface, 0, 0);
1163
- } else context.drawImage(entry.base, 0, 0);
1164
- }
1340
+ //#region src/sketch-objects.js
1341
+ const objectId = (stroke, index) => stroke.id ?? `legacy-${index}`;
1342
+ const identifyObjects = (doc) => ({
1343
+ ...doc,
1344
+ layers: doc.layers.map((layer) => ({
1345
+ ...layer,
1346
+ strokes: layer.strokes.map((s, i) => s.id ? s : {
1347
+ ...s,
1348
+ id: objectId(s, i)
1349
+ })
1350
+ }))
1351
+ });
1352
+ function objectBounds(stroke) {
1353
+ const xs = stroke.points.map((p) => p.x), ys = stroke.points.map((p) => p.y);
1354
+ return {
1355
+ x: Math.min(...xs),
1356
+ y: Math.min(...ys),
1357
+ width: Math.max(...xs) - Math.min(...xs),
1358
+ height: Math.max(...ys) - Math.min(...ys)
1359
+ };
1360
+ }
1361
+ function transformObject(stroke, { dx = 0, dy = 0, scaleX = 1, scaleY = 1 }) {
1362
+ if (![
1363
+ dx,
1364
+ dy,
1365
+ scaleX,
1366
+ scaleY
1367
+ ].every(Number.isFinite) || scaleX <= 0 || scaleY <= 0) throw Error("Invalid object transform");
1368
+ const box = objectBounds(stroke);
1369
+ const points = stroke.points.map((p) => ({
1370
+ x: box.x + (p.x - box.x) * scaleX + dx,
1371
+ y: box.y + (p.y - box.y) * scaleY + dy
1372
+ }));
1373
+ if (points.some((p) => p.x < 0 || p.x > 1 || p.y < 0 || p.y > 1)) throw Error("Object would leave the canvas");
1374
+ return {
1375
+ ...stroke,
1376
+ points
1377
+ };
1378
+ }
1379
+ function sketchObjectSummary(doc) {
1380
+ return doc.layers.flatMap((layer) => layer.strokes.map((stroke, i) => ({
1381
+ layer: layer.id,
1382
+ id: objectId(stroke, i),
1383
+ shape: stroke.shape,
1384
+ color: stroke.color,
1385
+ bounds: objectBounds(stroke),
1386
+ ...stroke.text ? { text: stroke.text } : {}
1387
+ })));
1165
1388
  }
1166
1389
  //#endregion
1167
1390
  //#region src/sketch-input.js
@@ -1193,39 +1416,192 @@ window.__ModuleLoader__.load({
1193
1416
  });
1194
1417
  }
1195
1418
  //#endregion
1419
+ //#region src/sketch-gesture.js
1420
+ function updateSketchGesture(doc, gesture, samples, rect, width, shiftKey = false) {
1421
+ if (!samples.length) return doc;
1422
+ if (gesture.object) {
1423
+ const point = sketchPoint(samples.at(-1).clientX, samples.at(-1).clientY, rect);
1424
+ if (!point) return doc;
1425
+ const box = objectBounds(gesture.object);
1426
+ const stroke = gesture.handle === "end" ? {
1427
+ ...gesture.object,
1428
+ points: [gesture.object.points[0], point]
1429
+ } : gesture.handle === "size" ? transformObject(gesture.object, {
1430
+ scaleX: Math.max(.001, point.x - box.x) / Math.max(.001, box.width),
1431
+ scaleY: Math.max(.001, point.y - box.y) / Math.max(.001, box.height)
1432
+ }) : transformObject(gesture.object, {
1433
+ dx: Math.max(-box.x, Math.min(1 - box.x - box.width, point.x - gesture.start.x)),
1434
+ dy: Math.max(-box.y, Math.min(1 - box.y - box.height, point.y - gesture.start.y))
1435
+ });
1436
+ doc = {
1437
+ ...doc,
1438
+ layers: doc.layers.map((l) => l.id === gesture.layer ? {
1439
+ ...l,
1440
+ strokes: l.strokes.map((s) => s.id === gesture.object.id ? stroke : s)
1441
+ } : l)
1442
+ };
1443
+ return doc;
1444
+ }
1445
+ for (const sample of samples) {
1446
+ let point = sketchPoint(sample.clientX, sample.clientY, rect);
1447
+ if (!point) continue;
1448
+ const layer = doc.layers.find((layer) => layer.id === gesture.layer);
1449
+ if (!layer) return doc;
1450
+ if (gesture.eraseStroke) {
1451
+ const previous = gesture.last ?? point;
1452
+ 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))));
1453
+ layer.strokes = layer.strokes.filter((stroke) => {
1454
+ for (let i = 1; i <= steps; i++) if (strokeHit(stroke, {
1455
+ x: previous.x + (point.x - previous.x) * i / steps,
1456
+ y: previous.y + (point.y - previous.y) * i / steps
1457
+ }, width / 2, doc.width, doc.height)) return false;
1458
+ return true;
1459
+ });
1460
+ } else {
1461
+ const stroke = layer.strokes.at(-1);
1462
+ if (!stroke) return doc;
1463
+ if ([
1464
+ "line",
1465
+ "arrow",
1466
+ "rectangle",
1467
+ "circle"
1468
+ ].includes(stroke.shape)) {
1469
+ if (stroke.shape === "line" && shiftKey) {
1470
+ const w = doc.width ?? 1024, h = doc.height ?? 1024, a = stroke.points[0];
1471
+ const snapped = snapLine({
1472
+ x: a.x * w,
1473
+ y: a.y * h
1474
+ }, {
1475
+ x: point.x * w,
1476
+ y: point.y * h
1477
+ });
1478
+ point = {
1479
+ x: snapped.x / w,
1480
+ y: snapped.y / h
1481
+ };
1482
+ }
1483
+ stroke.points = [stroke.points[0], point];
1484
+ } else {
1485
+ const last = stroke.points.at(-1);
1486
+ if (Math.hypot(last.x - point.x, last.y - point.y) < 1e-4) continue;
1487
+ if (stroke.points.length >= 2e3) stroke.points = stroke.points.filter((_, i) => i % 2 === 0);
1488
+ stroke.points.push(point);
1489
+ }
1490
+ }
1491
+ gesture.last = point;
1492
+ }
1493
+ return doc;
1494
+ }
1495
+ //#endregion
1196
1496
  //#region src/sketch-drafts.js
1197
1497
  const DATABASE = "dsh-codex-sketches-v1";
1198
- async function sketchDrafts(action, value) {
1498
+ const MAX_STORAGE = 32 * 1024 * 1024;
1499
+ const metadata = (kind, row) => ({
1500
+ key: `${kind}:${row.id}`,
1501
+ kind,
1502
+ id: row.id,
1503
+ name: row.name,
1504
+ updated: row.updated,
1505
+ size: JSON.stringify(row).length
1506
+ });
1507
+ async function sketchDrafts(action, value, recoverySession) {
1199
1508
  const db = await new Promise((resolve, reject) => {
1200
- const request = indexedDB.open(DATABASE, 1);
1201
- request.onupgradeneeded = () => request.result.createObjectStore("drafts", { keyPath: "id" });
1202
- request.onsuccess = () => resolve(request.result);
1509
+ let blocked = false;
1510
+ const request = indexedDB.open(DATABASE, 2);
1511
+ request.onblocked = () => {
1512
+ blocked = true;
1513
+ reject(Object.assign(Error("Close other sketch windows and retry"), { code: "SKETCH_STORAGE_BLOCKED" }));
1514
+ };
1515
+ request.onupgradeneeded = () => {
1516
+ if (blocked) {
1517
+ request.transaction.abort();
1518
+ return;
1519
+ }
1520
+ const db = request.result, tx = request.transaction;
1521
+ if (!db.objectStoreNames.contains("drafts")) db.createObjectStore("drafts", { keyPath: "id" });
1522
+ const meta = db.createObjectStore("metadata", { keyPath: "key" });
1523
+ db.createObjectStore("recovery", { keyPath: "id" });
1524
+ const cursor = tx.objectStore("drafts").openCursor();
1525
+ cursor.onsuccess = () => {
1526
+ const row = cursor.result;
1527
+ if (row) {
1528
+ meta.put(metadata("drafts", row.value));
1529
+ row.continue();
1530
+ }
1531
+ };
1532
+ };
1533
+ request.onsuccess = () => {
1534
+ if (blocked) {
1535
+ request.result.close();
1536
+ return;
1537
+ }
1538
+ request.result.onversionchange = () => request.result.close();
1539
+ resolve(request.result);
1540
+ };
1203
1541
  request.onerror = () => reject(request.error);
1204
1542
  });
1205
1543
  try {
1206
1544
  return await new Promise((resolve, reject) => {
1207
- const tx = db.transaction("drafts", action === "list" ? "readonly" : "readwrite"), store = tx.objectStore("drafts");
1208
- let result;
1545
+ const write = [
1546
+ "save",
1547
+ "delete",
1548
+ "checkpoint",
1549
+ "clearRecovery"
1550
+ ].includes(action);
1551
+ const tx = db.transaction([
1552
+ "drafts",
1553
+ "metadata",
1554
+ "recovery"
1555
+ ], write ? "readwrite" : "readonly");
1556
+ const meta = tx.objectStore("metadata"), kind = [
1557
+ "checkpoint",
1558
+ "recover",
1559
+ "clearRecovery"
1560
+ ].includes(action) ? "recovery" : "drafts", store = tx.objectStore(kind);
1561
+ let result, failure;
1209
1562
  tx.oncomplete = () => resolve(result);
1210
1563
  tx.onerror = () => reject(tx.error);
1211
- tx.onabort = () => reject(tx.error ?? Error("Draft limit reached"));
1212
- const request = store.getAll();
1564
+ tx.onabort = () => reject(failure ?? tx.error ?? Error("Draft transaction aborted"));
1565
+ if (action === "get" || action === "recover") {
1566
+ const req = store.get(value);
1567
+ req.onsuccess = () => {
1568
+ result = req.result;
1569
+ };
1570
+ return;
1571
+ }
1572
+ if (action === "delete" || action === "clearRecovery") {
1573
+ store.delete(value);
1574
+ meta.delete(`${kind}:${value}`);
1575
+ return;
1576
+ }
1577
+ if (![
1578
+ "list",
1579
+ "save",
1580
+ "checkpoint"
1581
+ ].includes(action)) {
1582
+ tx.abort();
1583
+ return;
1584
+ }
1585
+ const request = meta.getAll();
1213
1586
  request.onsuccess = () => {
1214
1587
  const rows = request.result;
1215
1588
  if (action === "list") {
1216
- result = rows.sort((a, b) => b.updated - a.updated);
1217
- return;
1218
- }
1219
- if (action === "delete") {
1220
- store.delete(value);
1589
+ result = rows.filter((row) => row.kind === "drafts").sort((a, b) => b.updated - a.updated);
1221
1590
  return;
1222
1591
  }
1223
- const others = rows.filter((row) => row.id !== value.id);
1224
- if (others.length >= 20 || JSON.stringify([...others, value]).length > 32 * 1024 * 1024) {
1592
+ const next = metadata(kind, value), others = rows.filter((row) => row.key !== next.key && !(action === "save" && recoverySession && row.key === `recovery:${recoverySession}`));
1593
+ 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;
1594
+ if (code) {
1595
+ failure = Object.assign(Error("Draft storage limit reached"), { code });
1225
1596
  tx.abort();
1226
1597
  return;
1227
1598
  }
1228
1599
  store.put(value);
1600
+ meta.put(next);
1601
+ if (action === "save" && recoverySession) {
1602
+ tx.objectStore("recovery").delete(recoverySession);
1603
+ meta.delete(`recovery:${recoverySession}`);
1604
+ }
1229
1605
  result = value;
1230
1606
  };
1231
1607
  });
@@ -1268,847 +1644,1155 @@ window.__ModuleLoader__.load({
1268
1644
  }
1269
1645
  }
1270
1646
  //#endregion
1271
- //#region src/sketch-interactions.js
1272
- function useSketchDismiss(open, close, host, selectors) {
1273
- const latest = (0, react.useRef)(close);
1274
- latest.current = close;
1275
- (0, react.useEffect)(() => {
1276
- if (!open) return;
1277
- const dialog = host.current?.closest("dialog") ?? host.current;
1278
- if (!dialog) return;
1279
- const pointer = (event) => {
1280
- if (selectors.some((selector) => event.target.closest?.(selector))) return;
1281
- latest.current(false);
1282
- if (event.target.matches?.("canvas")) {
1283
- event.preventDefault();
1284
- event.stopPropagation();
1285
- event.target.focus({ preventScroll: true });
1647
+ //#region src/sketch-presets.js
1648
+ const SKETCH_PRESETS = [
1649
+ "triangle",
1650
+ "diamond",
1651
+ "star"
1652
+ ];
1653
+ function expandSketchPreset(command) {
1654
+ if (!SKETCH_PRESETS.includes(command.shape)) return command;
1655
+ const points = command.points;
1656
+ if (!Array.isArray(points) || points.length !== 2 || points.some((p) => !p || ![p.x, p.y].every((v) => Number.isFinite(v) && v >= 0 && v <= 1))) throw Error("Shape preset requires two normalized bounding-box corners");
1657
+ const [a, b] = points, x = Math.min(a.x, b.x), y = Math.min(a.y, b.y);
1658
+ const w = Math.abs(a.x - b.x), h = Math.abs(a.y - b.y);
1659
+ if (!w || !h) throw Error("Shape preset requires a non-empty box");
1660
+ const vertices = command.shape === "triangle" ? [
1661
+ [.5, 0],
1662
+ [1, 1],
1663
+ [0, 1]
1664
+ ] : command.shape === "diamond" ? [
1665
+ [.5, 0],
1666
+ [1, .5],
1667
+ [.5, 1],
1668
+ [0, .5]
1669
+ ] : Array.from({ length: 10 }, (_, i) => {
1670
+ const angle = i * Math.PI / 5 - Math.PI / 2, radius = i % 2 ? .22 : .5;
1671
+ return [.5 + Math.cos(angle) * radius, .5 + Math.sin(angle) * radius];
1672
+ });
1673
+ return {
1674
+ ...command,
1675
+ shape: "polygon",
1676
+ points: vertices.map(([u, v]) => ({
1677
+ x: x + u * w,
1678
+ y: y + v * h
1679
+ }))
1680
+ };
1681
+ }
1682
+ const SKETCH_COMMAND_HELP = {
1683
+ 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.",
1684
+ 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.",
1685
+ commands: {
1686
+ preset: "triangle/diamond/star: {op:\"stroke\",id:\"badge\",shape:\"star\",points:[{x:0.1,y:0.1},{x:0.3,y:0.3}],color:\"#ffcc00\",fill:true}. Two opposite box corners; stored as editable polygon. Inspect objectId for one object without repeating full help.",
1687
+ 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},...]}",
1688
+ 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.",
1689
+ 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.",
1690
+ 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.",
1691
+ resize: "{op:\"resize\",ratio:\"1:1|4:3|3:4|16:9|9:16\"}"
1692
+ },
1693
+ limits: {
1694
+ strokes: MAX_SKETCH_STROKES,
1695
+ pointsPerStroke: MAX_STROKE_POINTS,
1696
+ pointsTotal: 2e5,
1697
+ commandsPerBatch: 256
1698
+ }
1699
+ };
1700
+ const finite = (value, min, max) => typeof value === "number" && Number.isFinite(value) && value >= min && value <= max;
1701
+ function applySketchCommands(source, commands) {
1702
+ if (!Array.isArray(commands) || !commands.length || commands.length > 256) throw Error("Expected 1–256 commands");
1703
+ let doc = identifyObjects(source);
1704
+ for (let command of commands) {
1705
+ if (!command || typeof command !== "object") throw Error("Invalid command");
1706
+ if (command.op === "stroke") command = expandSketchPreset(command);
1707
+ if (command.op === "resize") {
1708
+ doc = resizeSketch(doc, command.ratio);
1709
+ continue;
1710
+ }
1711
+ if (command.op === "layer") {
1712
+ if (![
1713
+ "add",
1714
+ "select",
1715
+ "rename",
1716
+ "visible",
1717
+ "duplicate",
1718
+ "up",
1719
+ "down",
1720
+ "delete",
1721
+ "clear"
1722
+ ].includes(command.action)) throw Error("Unknown layer action");
1723
+ if (command.action === "add") {
1724
+ const id = command.id ?? doc.nextId, after = command.after ?? doc.active;
1725
+ 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");
1726
+ const next = changeSketchLayer(doc, "add", after);
1727
+ if (next === doc) throw Error("Cannot add layer: check the existing after layer and the 8-layer limit");
1728
+ doc = {
1729
+ ...next,
1730
+ active: id,
1731
+ nextId: Math.max(next.nextId, id + 1),
1732
+ layers: next.layers.map((l) => l.id === next.active ? {
1733
+ ...l,
1734
+ id,
1735
+ name: String(command.value ?? "").trim().slice(0, 40)
1736
+ } : l)
1737
+ };
1738
+ continue;
1286
1739
  }
1740
+ const next = changeSketchLayer(doc, command.action, command.id ?? doc.active, command.value);
1741
+ if (next === doc) throw Error("Layer action unavailable; inspect the document first");
1742
+ doc = next;
1743
+ continue;
1744
+ }
1745
+ if (command.op === "object") {
1746
+ const layer = doc.layers.find((l) => l.id === (command.layer ?? doc.active)), index = layer?.strokes.findIndex((s) => s.id === command.id);
1747
+ if (!layer?.visible || index < 0 || index === void 0) throw Error("Object missing or hidden; inspect again");
1748
+ const strokes = layer.strokes.slice(), original = strokes[index];
1749
+ if (command.action === "delete") strokes.splice(index, 1);
1750
+ else if (command.action === "duplicate") strokes.splice(index + 1, 0, {
1751
+ ...original,
1752
+ id: crypto.randomUUID(),
1753
+ points: original.points.map((p) => ({ ...p }))
1754
+ });
1755
+ else if (command.action === "update") {
1756
+ const patch = command.patch ?? {};
1757
+ if (Object.keys(patch).some((k) => ![
1758
+ "color",
1759
+ "width",
1760
+ "opacity",
1761
+ "fill",
1762
+ "text",
1763
+ "points"
1764
+ ].includes(k))) throw Error("Unsupported object property");
1765
+ const changed = command.transform ? transformObject({
1766
+ ...original,
1767
+ ...patch
1768
+ }, command.transform) : {
1769
+ ...original,
1770
+ ...patch
1771
+ };
1772
+ strokes[index] = {
1773
+ ...applySketchCommands({
1774
+ ...doc,
1775
+ layers: [{
1776
+ ...layer,
1777
+ strokes: []
1778
+ }]
1779
+ }, [{
1780
+ ...changed,
1781
+ op: "stroke",
1782
+ layer: layer.id
1783
+ }]).layers[0].strokes[0],
1784
+ brush: original.brush ?? "pen",
1785
+ ...original.pressure !== void 0 ? { pressure: original.pressure } : {},
1786
+ ...original.brushVersion === 2 ? { brushVersion: 2 } : {}
1787
+ };
1788
+ } else throw Error("Unknown object action");
1789
+ doc = {
1790
+ ...doc,
1791
+ layers: doc.layers.map((l) => l === layer ? {
1792
+ ...l,
1793
+ strokes
1794
+ } : l)
1795
+ };
1796
+ continue;
1797
+ }
1798
+ if (command.op !== "stroke") throw Error("Unknown command");
1799
+ const shape = command.shape === "ellipse" ? "circle" : command.shape ?? "pen";
1800
+ const { color, width = shape === "text" ? 24 : 2, opacity = 1, fill = false } = command;
1801
+ let points = command.points;
1802
+ if (command.start !== void 0 || command.segments !== void 0) {
1803
+ 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");
1804
+ points = [command.start, ...command.segments.flatMap((s) => [
1805
+ s?.control1,
1806
+ s?.control2,
1807
+ s?.end
1808
+ ])];
1809
+ }
1810
+ if (![
1811
+ "pen",
1812
+ "line",
1813
+ "rectangle",
1814
+ "circle",
1815
+ "polygon",
1816
+ "bezier",
1817
+ "arrow",
1818
+ "text",
1819
+ "eraser"
1820
+ ].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");
1821
+ if ([
1822
+ "line",
1823
+ "arrow",
1824
+ "text",
1825
+ "rectangle",
1826
+ "circle"
1827
+ ].includes(shape) && points.length !== 2 || shape === "polygon" && points.length < 3) throw Error("Invalid shape points");
1828
+ 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.`);
1829
+ if (fill && ![
1830
+ "rectangle",
1831
+ "circle",
1832
+ "polygon",
1833
+ "bezier"
1834
+ ].includes(shape)) throw Error("Fill requires a closed shape");
1835
+ const layer = doc.layers.find((layer) => layer.id === (command.layer ?? doc.active));
1836
+ if (!layer?.visible) throw Error("Target layer is missing or hidden");
1837
+ 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");
1838
+ const id = command.id ?? crypto.randomUUID();
1839
+ if (typeof id !== "string" || !id.length || id.length > 100 || layer.strokes.some((s) => s.id === id)) throw Error("Invalid or duplicate object id");
1840
+ const stroke = {
1841
+ id,
1842
+ ...shape === "text" ? { text: command.text } : {},
1843
+ shape,
1844
+ color,
1845
+ width,
1846
+ opacity,
1847
+ fill,
1848
+ brush: "pen",
1849
+ points: points.map((p) => ({
1850
+ x: p.x,
1851
+ y: p.y
1852
+ }))
1287
1853
  };
1288
- const key = (event) => {
1289
- if (event.key !== "Escape") return;
1290
- event.preventDefault();
1291
- event.stopPropagation();
1292
- latest.current(false);
1293
- dialog.querySelector("canvas")?.focus({ preventScroll: true });
1854
+ doc = {
1855
+ ...doc,
1856
+ layers: doc.layers.map((item) => item === layer ? {
1857
+ ...item,
1858
+ strokes: [...item.strokes, stroke]
1859
+ } : item)
1294
1860
  };
1295
- const hidden = () => latest.current(false);
1296
- document.addEventListener("pointerdown", pointer, true);
1297
- document.addEventListener("keydown", key, true);
1298
- dialog.addEventListener("close", hidden);
1299
- return () => {
1300
- document.removeEventListener("pointerdown", pointer, true);
1301
- document.removeEventListener("keydown", key, true);
1302
- dialog.removeEventListener("close", hidden);
1861
+ }
1862
+ 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");
1863
+ return doc;
1864
+ }
1865
+ function createSketchCommandSession(adapter) {
1866
+ const completed = /* @__PURE__ */ new Map();
1867
+ let pending = false, cachedCharacters = 0;
1868
+ return async (request) => {
1869
+ if (!request || typeof request !== "object") throw Error("Invalid sketch request");
1870
+ if (!adapter.available()) throw Error("Open the sketch board for this session first");
1871
+ const current = adapter.snapshot();
1872
+ if (request.action === "inspect") {
1873
+ const offset = request.offset ?? 0, objects = adapter.objects?.() ?? [];
1874
+ if (!Number.isInteger(offset) || offset < 0) throw Error("offset must be a non-negative integer");
1875
+ return {
1876
+ ...current,
1877
+ protocolVersion: 2,
1878
+ objects: request.objectId ? [] : objects.slice(offset, offset + 50),
1879
+ objectCount: objects.length,
1880
+ ...!request.objectId && offset + 50 < objects.length ? { nextOffset: offset + 50 } : {},
1881
+ ...request.objectId ? { object: adapter.object?.(request.objectId, request.layer) } : {},
1882
+ recentRequests: [...completed.values()].slice(-8).map((entry) => entry.receipt),
1883
+ ...!request.objectId && offset === 0 ? { help: SKETCH_COMMAND_HELP } : {}
1884
+ };
1885
+ }
1886
+ if (request.documentId !== current.documentId) throw Error("Document changed; inspect again");
1887
+ if (pending || adapter.busy()) throw Error("Sketch is being edited; retry after it settles");
1888
+ if (request.action === "preview") return {
1889
+ ...current,
1890
+ png: await adapter.preview()
1891
+ };
1892
+ if (!["apply", "save"].includes(request.action)) throw Error("Unknown sketch action");
1893
+ if (typeof request.requestId !== "string" || !request.requestId.length || request.requestId.length > 100) throw Error("A unique requestId is required");
1894
+ const key = `${current.documentId}:${request.requestId}`, fingerprint = JSON.stringify({
1895
+ ...request,
1896
+ runId: void 0
1897
+ });
1898
+ const cached = completed.get(key);
1899
+ if (cached) {
1900
+ if (cached.fingerprint !== fingerprint) throw Error("requestId reused with different content");
1901
+ return cached.result;
1902
+ }
1903
+ if (request.revision !== current.revision || adapter.busy()) throw Error("Sketch changed or is being edited; inspect again");
1904
+ let changedObjects;
1905
+ if (request.action === "apply") {
1906
+ const before = adapter.document();
1907
+ let next;
1908
+ try {
1909
+ next = applySketchCommands(before, request.commands);
1910
+ } catch (cause) {
1911
+ const error = new Error(`${cause.message} Correct the batch and retry with the same runId and revision; nothing was applied.`, { cause });
1912
+ error.code = "SKETCH_INVALID_BATCH";
1913
+ throw error;
1914
+ }
1915
+ adapter.commit(next);
1916
+ const previous = new Map(before.layers.flatMap((l) => l.strokes.map((s) => [`${l.id}:${s.id}`, s])));
1917
+ changedObjects = next.layers.flatMap((l) => l.strokes.filter((s) => previous.get(`${l.id}:${s.id}`) !== s).map((s) => ({
1918
+ layer: l.id,
1919
+ id: s.id
1920
+ })));
1921
+ } else {
1922
+ if (request.name !== void 0 && (typeof request.name !== "string" || request.name.length > 60)) throw Error("Invalid draft name");
1923
+ pending = true;
1924
+ try {
1925
+ await adapter.save(request.name);
1926
+ } finally {
1927
+ pending = false;
1928
+ }
1929
+ }
1930
+ const result = {
1931
+ ...adapter.snapshot(),
1932
+ ...changedObjects ? {
1933
+ changedObjects: changedObjects.slice(0, 100),
1934
+ changedObjectCount: changedObjects.length
1935
+ } : {}
1936
+ };
1937
+ completed.set(key, {
1938
+ fingerprint,
1939
+ result,
1940
+ receipt: {
1941
+ requestId: request.requestId,
1942
+ action: request.action,
1943
+ revision: result.revision
1944
+ }
1945
+ });
1946
+ cachedCharacters += fingerprint.length;
1947
+ while (completed.size > 1 && (completed.size > 128 || cachedCharacters > 4e6)) {
1948
+ const oldest = completed.keys().next().value;
1949
+ cachedCharacters -= completed.get(oldest).fingerprint.length;
1950
+ completed.delete(oldest);
1951
+ }
1952
+ return result;
1953
+ };
1954
+ }
1955
+ //#endregion
1956
+ //#region src/sketch-formats.js
1957
+ const SKETCH_FILE_ACCEPT = ".psd,.dsh-sketch.json,image/png,image/jpeg,image/webp";
1958
+ const canvas = (w, h) => {
1959
+ const c = document.createElement("canvas");
1960
+ c.width = w;
1961
+ c.height = h;
1962
+ return c;
1963
+ };
1964
+ function runPsdCodec(action, payload) {
1965
+ return new Promise((resolve, reject) => {
1966
+ const worker = new Worker("/api/codex-subscription/sketch-psd-worker", { type: "module" });
1967
+ const finish = (callback, value) => {
1968
+ clearTimeout(timer);
1969
+ worker.terminate();
1970
+ callback(value);
1303
1971
  };
1304
- }, [
1305
- open,
1306
- host,
1307
- selectors.join("|")
1308
- ]);
1972
+ const timer = setTimeout(() => finish(reject, Error("PSD operation timed out")), 3e4);
1973
+ worker.onerror = () => finish(reject, Error("PSD codec could not be loaded"));
1974
+ worker.onmessage = (event) => event.data.ok ? finish(resolve, event.data.value) : finish(reject, Error(event.data.error));
1975
+ worker.postMessage({
1976
+ action,
1977
+ payload
1978
+ });
1979
+ });
1309
1980
  }
1310
- function useSketchCursor(canvas, ring, width, brush, zoom, hidden) {
1311
- const last = (0, react.useRef)(null), heldPressure = (0, react.useRef)(1);
1312
- const update = (event, bounds) => {
1313
- if (event) last.current = event;
1314
- const pointer = last.current, node = canvas.current, cursor = ring.current;
1315
- if (!pointer || !node || !cursor) return;
1316
- const rect = bounds ?? node.getBoundingClientRect();
1317
- if (hidden || pointer.pointerType === "touch" || pointer.clientX < rect.left || pointer.clientX > rect.right || pointer.clientY < rect.top || pointer.clientY > rect.bottom) {
1318
- cursor.hidden = true;
1319
- return;
1320
- }
1321
- const diameter = width * (node.hasPointerCapture(pointer.pointerId) && pointer.pointerType === "pen" ? heldPressure.current : 1) * rect.width / node.width;
1322
- cursor.hidden = false;
1323
- cursor.style.width = `${diameter}px`;
1324
- cursor.style.height = `${diameter}px`;
1325
- cursor.style.transform = `translate(${pointer.clientX - diameter / 2}px,${pointer.clientY - diameter / 2}px)`;
1981
+ function encodeSketchDocument(doc) {
1982
+ return JSON.stringify({
1983
+ format: "dsh-sketch",
1984
+ version: 1,
1985
+ doc
1986
+ });
1987
+ }
1988
+ function decodeSketchDocument(text) {
1989
+ if (text.length > 32 * 1024 * 1024) throw Error("Draft exceeds 32 MB");
1990
+ const file = JSON.parse(text), source = file.doc;
1991
+ 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");
1992
+ const w = source.width ?? 1024, h = source.height ?? 1024;
1993
+ if (!Number.isInteger(w) || !Number.isInteger(h) || w < 1 || h < 1 || w > 2048 || h > 2048) throw Error("Invalid canvas size");
1994
+ let doc = {
1995
+ ...createSketchLayers(),
1996
+ width: w,
1997
+ height: h,
1998
+ ratio: Object.keys(SKETCH_RATIOS).find((k) => SKETCH_RATIOS[k][0] === w && SKETCH_RATIOS[k][1] === h) ?? "custom"
1326
1999
  };
1327
- (0, react.useEffect)(() => {
1328
- update();
1329
- const observer = new ResizeObserver(() => update());
1330
- if (canvas.current) observer.observe(canvas.current);
1331
- return () => observer.disconnect();
1332
- }, [
1333
- width,
1334
- brush,
1335
- zoom,
1336
- hidden
1337
- ]);
1338
- return {
1339
- down: (event) => {
1340
- heldPressure.current = event.pointerType === "pen" ? Math.max(.2, event.pressure) : 1;
1341
- },
1342
- move: (event, bounds) => update({
1343
- clientX: event.clientX,
1344
- clientY: event.clientY,
1345
- pointerId: event.pointerId,
1346
- pointerType: event.pointerType,
1347
- pressure: event.pressure
1348
- }, bounds),
1349
- leave: () => {
1350
- last.current = null;
1351
- if (ring.current) ring.current.hidden = true;
2000
+ for (let i = 0; i < source.layers.length; i++) {
2001
+ const layer = source.layers[i];
2002
+ if (i) doc = applySketchCommands(doc, [{
2003
+ op: "layer",
2004
+ action: "add"
2005
+ }]);
2006
+ if (!Array.isArray(layer.strokes)) throw Error("Invalid strokes");
2007
+ for (let j = 0; j < layer.strokes.length; j += 256) {
2008
+ const strokes = layer.strokes.slice(j, j + 256);
2009
+ doc = applySketchCommands(doc, strokes.map((s) => ({
2010
+ ...s,
2011
+ op: "stroke",
2012
+ layer: doc.active,
2013
+ fill: s.fill ?? false
2014
+ })));
2015
+ const added = doc.layers.at(-1).strokes;
2016
+ for (let k = 0; k < strokes.length; k++) {
2017
+ const s = strokes[k];
2018
+ if (s.brush !== void 0 && ![
2019
+ "pen",
2020
+ "pencil",
2021
+ "marker"
2022
+ ].includes(s.brush)) throw Error("Invalid brush");
2023
+ if (s.pressure !== void 0 && (!Number.isFinite(s.pressure) || s.pressure < .2 || s.pressure > 1)) throw Error("Invalid pressure");
2024
+ if (s.brushVersion !== void 0 && s.brushVersion !== 2) throw Error("Unsupported brush version");
2025
+ Object.assign(added[added.length - strokes.length + k], {
2026
+ brush: s.brush ?? "pen",
2027
+ pressure: s.pressure ?? 1,
2028
+ ...s.brushVersion === 2 ? { brushVersion: 2 } : {}
2029
+ });
2030
+ }
2031
+ }
2032
+ const target = doc.layers.at(-1);
2033
+ target.name = String(layer.name ?? "").slice(0, 40);
2034
+ target.visible = layer.visible !== false;
2035
+ if (layer.image) {
2036
+ const image = layer.image;
2037
+ if (typeof image.src !== "string" || !/^data:image\/png;base64,/.test(image.src) || image.src.length > 8 * 1024 * 1024 || [
2038
+ "x",
2039
+ "y",
2040
+ "width",
2041
+ "height"
2042
+ ].some((k) => !Number.isFinite(image[k]) || image[k] < 0 || image[k] > 1)) throw Error("Invalid draft image");
2043
+ const bytes = Uint8Array.from(atob(image.src.slice(image.src.indexOf(",") + 1)), (c) => c.charCodeAt(0));
2044
+ if (bytes.length < 24) throw Error("Invalid draft image");
2045
+ const header = new DataView(bytes.buffer);
2046
+ 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");
2047
+ target.image = {
2048
+ src: image.src,
2049
+ x: image.x,
2050
+ y: image.y,
2051
+ width: image.width,
2052
+ height: image.height
2053
+ };
1352
2054
  }
2055
+ }
2056
+ const activeIndex = source.layers.findIndex((layer) => layer.id === source.active);
2057
+ doc.active = doc.layers[Math.max(0, activeIndex)].id;
2058
+ return doc;
2059
+ }
2060
+ async function exportSketchPsd(doc, images, composite) {
2061
+ const width = doc.width ?? 1024, height = doc.height ?? 1024;
2062
+ const children = doc.layers.map((layer, i) => {
2063
+ const ctx = canvas(width, height).getContext("2d"), ref = layer.image;
2064
+ if (ref) ctx.drawImage(images.get(ref.src), ref.x * width, ref.y * height, ref.width * width, ref.height * height);
2065
+ paintSketch(ctx, layer.strokes, width, true, height);
2066
+ return {
2067
+ name: layer.name || `Layer ${i + 1}`,
2068
+ hidden: !layer.visible,
2069
+ opacity: 1,
2070
+ blendMode: "normal",
2071
+ imageData: ctx.getImageData(0, 0, width, height)
2072
+ };
2073
+ });
2074
+ const context = canvas(width, height).getContext("2d");
2075
+ context.fillStyle = "#fff";
2076
+ context.fillRect(0, 0, width, height);
2077
+ if (children[0] && !children[0].hidden) {
2078
+ const bottom = canvas(width, height);
2079
+ bottom.getContext("2d").putImageData(children[0].imageData, 0, 0);
2080
+ context.drawImage(bottom, 0, 0);
2081
+ children[0].imageData = context.getImageData(0, 0, width, height);
2082
+ } else {
2083
+ if (children.length >= 8) throw Error("Show the bottom layer before exporting this eight-layer drawing");
2084
+ children.unshift({
2085
+ name: "Paper",
2086
+ opacity: 1,
2087
+ blendMode: "normal",
2088
+ imageData: context.getImageData(0, 0, width, height)
2089
+ });
2090
+ }
2091
+ return runPsdCodec("write", {
2092
+ width,
2093
+ height,
2094
+ children,
2095
+ imageData: composite.getContext("2d").getImageData(0, 0, width, height)
2096
+ });
2097
+ }
2098
+ async function importSketchPsd(file) {
2099
+ if (file.size > 32 * 1024 * 1024) throw Error("PSD exceeds 32 MB");
2100
+ const psd = await runPsdCodec("read", await file.arrayBuffer());
2101
+ 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));
2102
+ const doc = {
2103
+ ...createSketchLayers(),
2104
+ width,
2105
+ height,
2106
+ ratio: Object.keys(SKETCH_RATIOS).find((k) => SKETCH_RATIOS[k][0] === width && SKETCH_RATIOS[k][1] === height) ?? "custom",
2107
+ layers: [],
2108
+ nextId: psd.layers.length + 1
1353
2109
  };
2110
+ for (const [i, layer] of psd.layers.entries()) {
2111
+ const src = canvas(layer.imageData.width, layer.imageData.height);
2112
+ src.getContext("2d").putImageData(new ImageData(new Uint8ClampedArray(layer.imageData.data), layer.imageData.width, layer.imageData.height), 0, 0);
2113
+ const out = canvas(width, height), ctx = out.getContext("2d");
2114
+ ctx.globalAlpha = layer.opacity;
2115
+ ctx.drawImage(src, layer.left * scale, layer.top * scale, src.width * scale, src.height * scale);
2116
+ doc.layers.push({
2117
+ id: i + 1,
2118
+ name: layer.name,
2119
+ visible: !layer.hidden,
2120
+ strokes: [],
2121
+ image: {
2122
+ src: out.toDataURL("image/png"),
2123
+ x: 0,
2124
+ y: 0,
2125
+ width: 1,
2126
+ height: 1
2127
+ }
2128
+ });
2129
+ }
2130
+ return doc;
1354
2131
  }
1355
2132
  //#endregion
1356
- //#region src/sketch-view.jsx
1357
- const DEFAULT_KEYS = {
1358
- pen: "b",
1359
- eraser: "e",
1360
- line: "l",
1361
- rectangle: "r",
1362
- circle: "o",
1363
- pan: " ",
1364
- zoomIn: "=",
1365
- zoomOut: "-",
1366
- fit: "0"
1367
- };
1368
- function useSketchView(canvas, open) {
1369
- const [view, setView] = (0, react.useState)({
1370
- scale: 1,
1371
- x: 0,
1372
- y: 0
1373
- }), [keys, setKeys] = (0, react.useState)(() => {
2133
+ //#region src/sketch-document-lifecycle.js
2134
+ function createSketchDocumentLifecycle(state, { sessionId, t, schedule, checkpoint, cache, setSelection, setTextEdit, setRecovered, store = sketchDrafts, decodeImages = decodeSketchImages, readImage = importSketchImage }) {
2135
+ const { doc, undo, redo, images, saved, dirty, documentId, documentRevision } = state;
2136
+ const hasContent = () => doc.current.layers.some((layer) => layer.image || layer.strokes.length);
2137
+ const save = async (name) => {
2138
+ const savingDocument = documentId.current, savingRevision = documentRevision.current;
2139
+ const row = {
2140
+ id: saved.current?.id ?? crypto.randomUUID(),
2141
+ name: name?.trim() || saved.current?.name || `${t("sketchTitle")} ${(/* @__PURE__ */ new Date()).toLocaleString()}`,
2142
+ updated: Date.now(),
2143
+ doc: structuredClone(doc.current)
2144
+ };
1374
2145
  try {
1375
- return {
1376
- ...DEFAULT_KEYS,
1377
- ...JSON.parse(localStorage.getItem("codex-sketch-keys"))
2146
+ await store("save", row, sessionId);
2147
+ } catch (error) {
2148
+ if (error.code === "SKETCH_DRAFT_LIMIT") error.message = t("sketchDraftLimit");
2149
+ if (error.code === "SKETCH_STORAGE_LIMIT") error.message = t("sketchStorageLimit");
2150
+ throw error;
2151
+ }
2152
+ if (documentId.current === savingDocument) {
2153
+ saved.current = {
2154
+ id: row.id,
2155
+ name: row.name
1378
2156
  };
1379
- } catch {
1380
- return DEFAULT_KEYS;
2157
+ if (documentRevision.current === savingRevision) {
2158
+ dirty.current = false;
2159
+ setRecovered(false);
2160
+ }
1381
2161
  }
1382
- });
1383
- const [shortcuts, setShortcuts] = (0, react.useState)(() => {
1384
- try {
1385
- return localStorage.getItem("codex-sketch-shortcuts") !== "off";
1386
- } catch {
1387
- return true;
2162
+ };
2163
+ const saveChanges = async () => {
2164
+ if (!dirty.current) return;
2165
+ if (hasContent() || saved.current) return save();
2166
+ const id = documentId.current, revision = documentRevision.current;
2167
+ await store("clearRecovery", sessionId);
2168
+ if (documentId.current === id && documentRevision.current === revision) {
2169
+ dirty.current = false;
2170
+ setRecovered(false);
1388
2171
  }
1389
- }), [space, setSpace] = (0, react.useState)(false);
1390
- const drag = (0, react.useRef)(null), viewRef = (0, react.useRef)(view);
1391
- viewRef.current = view;
1392
- const zoom = (factor) => setView((v) => ({
1393
- ...v,
1394
- scale: Math.max(.25, Math.min(8, v.scale * factor))
1395
- }));
1396
- const reset = () => setView({
1397
- scale: 1,
1398
- x: 0,
1399
- y: 0
1400
- });
1401
- (0, react.useEffect)(() => {
1402
- const node = canvas.current;
1403
- if (!node || !open) return;
1404
- const wheel = (e) => {
1405
- if (!shortcuts || !e.altKey) return;
1406
- e.preventDefault();
1407
- zoom(e.deltaY < 0 ? 1.1 : 1 / 1.1);
2172
+ };
2173
+ const replace = (next, decoded, identity) => {
2174
+ documentId.current = crypto.randomUUID();
2175
+ documentRevision.current++;
2176
+ doc.current = identifyObjects(structuredClone(next));
2177
+ setSelection(null);
2178
+ setTextEdit(null);
2179
+ images.current = decoded;
2180
+ cache.current.clear();
2181
+ undo.current = [];
2182
+ redo.current = [];
2183
+ saved.current = identity;
2184
+ dirty.current = false;
2185
+ schedule();
2186
+ };
2187
+ const fresh = async () => {
2188
+ await saveChanges();
2189
+ replace(createSketchLayers(), /* @__PURE__ */ new Map(), null);
2190
+ };
2191
+ const load = async (row) => {
2192
+ if (row.id === saved.current?.id) return;
2193
+ row = await store("get", row.id);
2194
+ if (!row) throw Error("Draft no longer exists");
2195
+ await saveChanges();
2196
+ const decoded = /* @__PURE__ */ new Map();
2197
+ await decodeImages(row.doc, decoded);
2198
+ replace(row.doc, decoded, {
2199
+ id: row.id,
2200
+ name: row.name
2201
+ });
2202
+ };
2203
+ const importImage = async (file) => {
2204
+ if (file.name?.toLowerCase().endsWith(".psd") || file.name?.toLowerCase().endsWith(".dsh-sketch.json")) {
2205
+ if (file.size > 32 * 1024 * 1024) throw Error("File exceeds 32 MB");
2206
+ const next = file.name.toLowerCase().endsWith(".psd") ? await importSketchPsd(file) : decodeSketchDocument(await file.text());
2207
+ const decoded = /* @__PURE__ */ new Map();
2208
+ await decodeImages(next, decoded);
2209
+ await saveChanges();
2210
+ replace(next, decoded, null);
2211
+ dirty.current = true;
2212
+ return;
2213
+ }
2214
+ if (doc.current.layers.length >= 8) throw Error("Layer limit");
2215
+ const image = await readImage(file), w = doc.current.width ?? 1024, h = doc.current.height ?? 1024;
2216
+ const scale = Math.min(w / image.width, h / image.height), width = image.width * scale / w, height = image.height * scale / h;
2217
+ const layer = {
2218
+ id: doc.current.nextId,
2219
+ name: file.name?.slice(0, 40) || t("sketchImport"),
2220
+ visible: true,
2221
+ strokes: [],
2222
+ image: {
2223
+ src: image.src,
2224
+ x: (1 - width) / 2,
2225
+ y: (1 - height) / 2,
2226
+ width,
2227
+ height
2228
+ }
1408
2229
  };
1409
- node.addEventListener("wheel", wheel, { passive: false });
1410
- return () => node.removeEventListener("wheel", wheel);
1411
- }, [open, shortcuts]);
1412
- (0, react.useEffect)(() => {
1413
- const stop = () => {
1414
- drag.current = null;
1415
- setSpace(false);
2230
+ await decodeImages({ layers: [layer] }, images.current);
2231
+ checkpoint();
2232
+ doc.current = {
2233
+ ...doc.current,
2234
+ nextId: layer.id + 1,
2235
+ active: layer.id,
2236
+ layers: [...doc.current.layers, layer]
1416
2237
  };
1417
- window.addEventListener("blur", stop);
1418
- return () => window.removeEventListener("blur", stop);
1419
- }, []);
1420
- (0, react.useEffect)(() => {
1421
- if (!open || !shortcuts) {
1422
- setSpace(false);
1423
- drag.current = null;
2238
+ schedule();
2239
+ };
2240
+ const restore = async (isCurrent = () => true) => {
2241
+ if (dirty.current || hasContent()) return;
2242
+ const id = documentId.current, revision = documentRevision.current;
2243
+ const recovery = await store("recover", sessionId);
2244
+ const archived = state.restoreId.current;
2245
+ const row = recovery ?? (archived ? await store("get", archived) : null);
2246
+ const decoded = /* @__PURE__ */ new Map();
2247
+ if (row) await decodeImages(row.doc, decoded);
2248
+ if (!isCurrent() || documentId.current !== id || documentRevision.current !== revision || dirty.current) return;
2249
+ if (row) {
2250
+ replace(row.doc, decoded, recovery ? null : {
2251
+ id: row.id,
2252
+ name: row.name
2253
+ });
2254
+ dirty.current = Boolean(recovery);
2255
+ setRecovered(Boolean(recovery));
1424
2256
  }
1425
- }, [open, shortcuts]);
1426
- const setKey = (action, key) => {
1427
- key = key.toLowerCase();
1428
- if (!key || Object.entries(keys).some(([a, k]) => a !== action && k === key) || ["[", "]"].includes(key)) return;
1429
- const next = {
1430
- ...keys,
1431
- [action]: key
1432
- };
1433
- setKeys(next);
1434
- try {
1435
- localStorage.setItem("codex-sketch-keys", JSON.stringify(next));
1436
- } catch {}
2257
+ state.restoreId.current = null;
1437
2258
  };
1438
2259
  return {
1439
- view,
1440
- keys,
1441
- shortcuts,
1442
- space,
1443
- zoom,
1444
- reset,
1445
- setKey,
1446
- toggle: () => setShortcuts((v) => {
2260
+ hasContent,
2261
+ save,
2262
+ saveChanges,
2263
+ replace,
2264
+ fresh,
2265
+ load,
2266
+ importImage,
2267
+ restore
2268
+ };
2269
+ }
2270
+ //#endregion
2271
+ //#region src/sketch-operation-gate.js
2272
+ function createSketchOperationGate() {
2273
+ let running = false;
2274
+ return {
2275
+ get running() {
2276
+ return running;
2277
+ },
2278
+ async run(operation, { blocked = false, working, report, rethrow = false }) {
2279
+ if (blocked || running) return false;
2280
+ running = true;
1447
2281
  try {
1448
- localStorage.setItem("codex-sketch-shortcuts", v ? "off" : "on");
1449
- } catch {}
1450
- return !v;
1451
- }),
1452
- keyDown: (e) => {
1453
- if (!shortcuts || e.ctrlKey || e.metaKey || e.altKey) return false;
1454
- const key = e.key.toLowerCase();
1455
- if (key === keys.pan && !e.ctrlKey && !e.metaKey) {
1456
- e.preventDefault();
1457
- setSpace(true);
1458
- return true;
1459
- }
1460
- if (key === keys.zoomIn || key === "+" || key === keys.zoomOut || key === keys.fit) {
1461
- e.preventDefault();
1462
- if (key === keys.fit) reset();
1463
- else zoom(key === keys.zoomOut ? 1 / 1.2 : 1.2);
2282
+ working(true);
2283
+ report(null);
2284
+ await operation();
1464
2285
  return true;
2286
+ } catch (error) {
2287
+ report(error);
2288
+ if (rethrow) throw error;
2289
+ return false;
2290
+ } finally {
2291
+ running = false;
2292
+ working(false);
1465
2293
  }
1466
- return false;
1467
- },
1468
- keyUp: (e) => {
1469
- if (e.key.toLowerCase() === keys.pan) setSpace(false);
1470
- },
1471
- down: (e) => {
1472
- if (e.button !== 1 && !space) return false;
1473
- e.preventDefault();
1474
- drag.current = {
1475
- id: e.pointerId,
1476
- x: e.clientX,
1477
- y: e.clientY,
1478
- view: viewRef.current
1479
- };
1480
- canvas.current.setPointerCapture(e.pointerId);
1481
- return true;
1482
- },
1483
- move: (e) => {
1484
- const d = drag.current;
1485
- if (!d || d.id !== e.pointerId) return false;
1486
- setView({
1487
- ...d.view,
1488
- x: d.view.x + e.clientX - d.x,
1489
- y: d.view.y + e.clientY - d.y
1490
- });
1491
- return true;
1492
- },
1493
- end: (e) => {
1494
- if (drag.current?.id !== e.pointerId) return false;
1495
- drag.current = null;
1496
- if (canvas.current.hasPointerCapture(e.pointerId)) canvas.current.releasePointerCapture(e.pointerId);
1497
- return true;
1498
2294
  }
1499
2295
  };
1500
2296
  }
1501
- function SketchViewControls({ navigation, t }) {
1502
- const [open, setOpen] = (0, react.useState)(false), [placement, setPlacement] = (0, react.useState)(null);
1503
- const host = (0, react.useRef)(null), trigger = (0, react.useRef)(null);
1504
- const close = () => {
1505
- setOpen(false);
1506
- trigger.current?.focus({ preventScroll: true });
1507
- };
1508
- useSketchDismiss(open, setOpen, host, [".codexSketchViewControls", ".codexSketchKeyPanel"]);
1509
- (0, react.useLayoutEffect)(() => {
1510
- if (!open) return;
1511
- const dialog = host.current.closest("dialog");
1512
- const place = () => {
1513
- const box = dialog.getBoundingClientRect(), anchor = trigger.current.getBoundingClientRect();
1514
- const width = Math.min(360, box.width - 24);
1515
- setPlacement({
1516
- dialog,
1517
- style: {
1518
- width,
1519
- left: Math.max(12, Math.min(anchor.left - box.left, box.width - width - 12)),
1520
- bottom: box.bottom - anchor.top + 8,
1521
- maxHeight: Math.max(80, anchor.top - box.top - 24)
1522
- }
1523
- });
1524
- };
1525
- place();
1526
- const observer = new ResizeObserver(place);
1527
- observer.observe(dialog);
1528
- observer.observe(host.current);
1529
- window.addEventListener("resize", place);
1530
- return () => {
1531
- observer.disconnect();
1532
- window.removeEventListener("resize", place);
2297
+ //#endregion
2298
+ //#region src/sketch-agent-export.js
2299
+ async function exportSketchAgentFile(format, { gate, blocked, working, report, exportFile }) {
2300
+ let result;
2301
+ if (!await gate.run(async () => {
2302
+ const { blob, extension } = await exportFile(format);
2303
+ const data = new Uint8Array(await blob.arrayBuffer());
2304
+ let raw = "";
2305
+ for (let i = 0; i < data.length; i += 8192) raw += String.fromCharCode(...data.subarray(i, i + 8192));
2306
+ result = {
2307
+ extension,
2308
+ mediaType: blob.type,
2309
+ base64: btoa(raw)
1533
2310
  };
1534
- }, [open]);
2311
+ }, {
2312
+ blocked,
2313
+ working,
2314
+ report,
2315
+ rethrow: true
2316
+ })) throw Error("Sketch is being edited; retry after it settles");
2317
+ return result;
2318
+ }
2319
+ //#endregion
2320
+ //#region src/sketch-run-status.jsx
2321
+ function SketchRunStatus({ state, t, floating = false, onOpen, onStop, onResume, onDismiss }) {
2322
+ if (state === "idle") return null;
2323
+ const drawing = state === "drawing", recover = state === "stopped" || state === "failed";
1535
2324
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1536
- ref: host,
1537
- className: "codexSketchViewControls",
2325
+ className: floating ? "codexSketchBackgroundStatus" : "codexSketchAgentStatus",
2326
+ role: "status",
2327
+ "aria-live": "polite",
1538
2328
  children: [
1539
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1540
- type: "button",
1541
- "aria-label": t("sketchZoomOut"),
1542
- onClick: () => navigation.zoom(1 / 1.2),
1543
- children: "−"
1544
- }),
1545
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
1546
- type: "button",
1547
- title: t("sketchFit"),
1548
- onClick: navigation.reset,
1549
- children: [Math.round(navigation.view.scale * 100), "%"]
1550
- }),
1551
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1552
- type: "button",
1553
- "aria-label": t("sketchZoomIn"),
1554
- onClick: () => navigation.zoom(1.2),
1555
- children: "+"
2329
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(WorkspaceIcon, {
2330
+ name: drawing ? "pen" : state === "finished" ? "check" : "rectangle",
2331
+ size: 15
1556
2332
  }),
1557
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1558
- ref: trigger,
2333
+ floating ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1559
2334
  type: "button",
1560
- "aria-expanded": open,
1561
- onClick: () => setOpen(!open),
1562
- children: t("sketchKeys")
1563
- }),
1564
- open && placement ? (0, react_dom.createPortal)(/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
1565
- className: "codexSketchKeyPanel",
1566
- "aria-label": t("sketchKeys"),
1567
- style: placement.style,
1568
- onKeyDown: (e) => {
1569
- if (e.key === "Escape") {
1570
- e.preventDefault();
1571
- e.stopPropagation();
1572
- close();
1573
- }
1574
- },
1575
- children: [
1576
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("header", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("strong", { children: t("sketchKeys") }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1577
- type: "button",
1578
- "aria-label": t("sketchFileClose"),
1579
- onClick: close,
1580
- children: "×"
1581
- })] }),
1582
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
1583
- className: "codexSketchKeysEnabled",
1584
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("sketchKeysEnabled") }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
1585
- type: "checkbox",
1586
- checked: navigation.shortcuts,
1587
- onChange: navigation.toggle
1588
- })]
1589
- }),
1590
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", { children: t("sketchNavigationHint") }),
1591
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1592
- className: "codexSketchKeyGrid",
1593
- children: Object.entries(navigation.keys).map(([action, key]) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", { children: [t(`sketchKey_${action}`), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
1594
- "aria-label": t(`sketchKey_${action}`),
1595
- value: key === " " ? "Space" : key,
1596
- readOnly: true,
1597
- onKeyDown: (e) => {
1598
- if (e.key === "Tab" || e.key === "Escape") return;
1599
- e.preventDefault();
1600
- e.stopPropagation();
1601
- if (e.key.length === 1 && !e.ctrlKey && !e.metaKey && !e.altKey) navigation.setKey(action, e.key);
1602
- }
1603
- })] }, action))
1604
- }),
1605
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("small", { children: t("sketchKeyHint") })
1606
- ]
1607
- }), placement.dialog) : null
2335
+ onClick: onOpen,
2336
+ children: t(`sketchRun_${state}`)
2337
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t(`sketchRun_${state}`) }),
2338
+ drawing ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
2339
+ type: "button",
2340
+ className: "codexSketchStop",
2341
+ onClick: onStop,
2342
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(WorkspaceIcon, {
2343
+ name: "stop",
2344
+ size: 12
2345
+ }), t("sketchRunStop")]
2346
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [recover ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2347
+ type: "button",
2348
+ title: t("sketchRunResumeHint"),
2349
+ onClick: onResume,
2350
+ children: t("sketchRunResume")
2351
+ }) : null, !recover ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2352
+ type: "button",
2353
+ "aria-label": t("sketchDismissStatus"),
2354
+ title: t("sketchDismissStatus"),
2355
+ onClick: onDismiss,
2356
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(WorkspaceIcon, {
2357
+ name: "close",
2358
+ size: 14
2359
+ })
2360
+ }) : null] })
1608
2361
  ]
1609
2362
  });
1610
2363
  }
1611
2364
  //#endregion
1612
- //#region src/sketch-objects.js
1613
- const objectId = (stroke, index) => stroke.id ?? `legacy-${index}`;
1614
- const identifyObjects = (doc) => ({
1615
- ...doc,
1616
- layers: doc.layers.map((layer) => ({
1617
- ...layer,
1618
- strokes: layer.strokes.map((s, i) => s.id ? s : {
1619
- ...s,
1620
- id: objectId(s, i)
1621
- })
1622
- }))
1623
- });
1624
- function objectBounds(stroke) {
1625
- const xs = stroke.points.map((p) => p.x), ys = stroke.points.map((p) => p.y);
1626
- return {
1627
- x: Math.min(...xs),
1628
- y: Math.min(...ys),
1629
- width: Math.max(...xs) - Math.min(...xs),
1630
- height: Math.max(...ys) - Math.min(...ys)
1631
- };
2365
+ //#region src/sketch-tool-widths.js
2366
+ const defaults = {
2367
+ pen: 12,
2368
+ pencil: 6,
2369
+ marker: 28,
2370
+ eraser: 24,
2371
+ text: 32,
2372
+ line: 12,
2373
+ arrow: 12,
2374
+ rectangle: 12,
2375
+ circle: 12
2376
+ };
2377
+ const keyFor = ({ tool, brush }) => tool === "pen" ? brush : tool;
2378
+ function switchSketchToolWidth(memory, current, next) {
2379
+ if (current.tool !== "select") memory[keyFor(current)] = current.width;
2380
+ if (next.tool === "select") return current.width;
2381
+ const key = keyFor(next);
2382
+ return memory[key] ?? defaults[key] ?? 12;
1632
2383
  }
1633
- function transformObject(stroke, { dx = 0, dy = 0, scaleX = 1, scaleY = 1 }) {
1634
- if (![
1635
- dx,
1636
- dy,
1637
- scaleX,
1638
- scaleY
1639
- ].every(Number.isFinite) || scaleX <= 0 || scaleY <= 0) throw Error("Invalid object transform");
1640
- const box = objectBounds(stroke);
1641
- const points = stroke.points.map((p) => ({
1642
- x: box.x + (p.x - box.x) * scaleX + dx,
1643
- y: box.y + (p.y - box.y) * scaleY + dy
1644
- }));
1645
- if (points.some((p) => p.x < 0 || p.x > 1 || p.y < 0 || p.y > 1)) throw Error("Object would leave the canvas");
1646
- return {
1647
- ...stroke,
1648
- points
1649
- };
2384
+ function stepSketchWidth(width, direction) {
2385
+ return Math.max(1, Math.min(256, width + direction * 2));
1650
2386
  }
1651
- function sketchObjectSummary(doc) {
1652
- return doc.layers.flatMap((layer) => layer.strokes.map((stroke, i) => ({
1653
- layer: layer.id,
1654
- id: objectId(stroke, i),
1655
- shape: stroke.shape,
1656
- color: stroke.color,
1657
- bounds: objectBounds(stroke),
1658
- ...stroke.text ? { text: stroke.text } : {}
1659
- })));
2387
+ //#endregion
2388
+ //#region src/sketch-shortcuts.js
2389
+ function sketchShortcutAction(keys, key) {
2390
+ key = key.toLowerCase();
2391
+ return Object.entries(keys).find(([, value]) => value === key)?.[0] ?? {
2392
+ v: "select",
2393
+ t: "text",
2394
+ "+": "zoomIn"
2395
+ }[key];
1660
2396
  }
1661
- const SKETCH_COMMAND_HELP = {
1662
- coordinates: "Assign short meaningful stroke id values for later edits. Text uses two opposite box corners and text content; width is font size in pixels, automatic fitting within the box. Arrow uses two endpoints. Normalized x/y in [0,1]; width is canvas pixels. Read documentId and revision before editing.",
1663
- 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.",
1664
- commands: {
1665
- 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},...]}",
1666
- 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.",
1667
- 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.",
1668
- 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.",
1669
- resize: "{op:\"resize\",ratio:\"1:1|4:3|3:4|16:9|9:16\"}"
1670
- },
1671
- limits: {
1672
- strokes: MAX_SKETCH_STROKES,
1673
- pointsPerStroke: MAX_STROKE_POINTS,
1674
- pointsTotal: 2e5,
1675
- commandsPerBatch: 256
1676
- }
2397
+ //#endregion
2398
+ //#region src/sketch-layer-renderer.js
2399
+ const NO_IMAGES = /* @__PURE__ */ new Map();
2400
+ const surface = (width, height) => {
2401
+ const c = document.createElement("canvas");
2402
+ c.width = width;
2403
+ c.height = height;
2404
+ return c;
1677
2405
  };
1678
- const finite = (value, min, max) => typeof value === "number" && Number.isFinite(value) && value >= min && value <= max;
1679
- function applySketchCommands(source, commands) {
1680
- if (!Array.isArray(commands) || !commands.length || commands.length > 256) throw Error("Expected 1–256 commands");
1681
- let doc = identifyObjects(source);
1682
- for (const command of commands) {
1683
- if (!command || typeof command !== "object") throw Error("Invalid command");
1684
- if (command.op === "resize") {
1685
- doc = resizeSketch(doc, command.ratio);
1686
- continue;
1687
- }
1688
- if (command.op === "layer") {
1689
- if (![
1690
- "add",
1691
- "select",
1692
- "rename",
1693
- "visible",
1694
- "duplicate",
1695
- "up",
1696
- "down",
1697
- "delete",
1698
- "clear"
1699
- ].includes(command.action)) throw Error("Unknown layer action");
1700
- if (command.action === "add") {
1701
- const id = command.id ?? doc.nextId, after = command.after ?? doc.active;
1702
- 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");
1703
- const next = changeSketchLayer(doc, "add", after);
1704
- if (next === doc) throw Error("Cannot add layer: check the existing after layer and the 8-layer limit");
1705
- doc = {
1706
- ...next,
1707
- active: id,
1708
- nextId: Math.max(next.nextId, id + 1),
1709
- layers: next.layers.map((l) => l.id === next.active ? {
1710
- ...l,
1711
- id,
1712
- name: String(command.value ?? "").trim().slice(0, 40)
1713
- } : l)
1714
- };
1715
- continue;
1716
- }
1717
- const next = changeSketchLayer(doc, command.action, command.id ?? doc.active, command.value);
1718
- if (next === doc) throw Error("Layer action unavailable; inspect the document first");
1719
- doc = next;
1720
- continue;
1721
- }
1722
- if (command.op === "object") {
1723
- const layer = doc.layers.find((l) => l.id === (command.layer ?? doc.active)), index = layer?.strokes.findIndex((s) => s.id === command.id);
1724
- if (!layer?.visible || index < 0 || index === void 0) throw Error("Object missing or hidden; inspect again");
1725
- const strokes = layer.strokes.slice(), original = strokes[index];
1726
- if (command.action === "delete") strokes.splice(index, 1);
1727
- else if (command.action === "duplicate") strokes.splice(index + 1, 0, {
1728
- ...original,
1729
- id: crypto.randomUUID(),
1730
- points: original.points.map((p) => ({ ...p }))
1731
- });
1732
- else if (command.action === "update") {
1733
- const patch = command.patch ?? {};
1734
- if (Object.keys(patch).some((k) => ![
1735
- "color",
1736
- "width",
1737
- "opacity",
1738
- "fill",
1739
- "text",
1740
- "points"
1741
- ].includes(k))) throw Error("Unsupported object property");
1742
- const changed = command.transform ? transformObject({
1743
- ...original,
1744
- ...patch
1745
- }, command.transform) : {
1746
- ...original,
1747
- ...patch
1748
- };
1749
- strokes[index] = {
1750
- ...applySketchCommands({
1751
- ...doc,
1752
- layers: [{
1753
- ...layer,
1754
- strokes: []
1755
- }]
1756
- }, [{
1757
- ...changed,
1758
- op: "stroke",
1759
- layer: layer.id
1760
- }]).layers[0].strokes[0],
1761
- brush: original.brush ?? "pen",
1762
- ...original.pressure !== void 0 ? { pressure: original.pressure } : {},
1763
- ...original.brushVersion === 2 ? { brushVersion: 2 } : {}
1764
- };
1765
- } else throw Error("Unknown object action");
1766
- doc = {
1767
- ...doc,
1768
- layers: doc.layers.map((l) => l === layer ? {
1769
- ...l,
1770
- strokes
1771
- } : l)
2406
+ function paintSketchLayers(context, doc, cache, size = doc.width ?? 1024, height = doc.height ?? size, activeLayer, images = NO_IMAGES) {
2407
+ context.globalCompositeOperation = "source-over";
2408
+ context.globalAlpha = 1;
2409
+ context.fillStyle = "#fff";
2410
+ context.fillRect(0, 0, size, height);
2411
+ for (const id of cache.keys()) if (!doc.layers.some((layer) => layer.id === id)) cache.delete(id);
2412
+ for (const layer of doc.layers) {
2413
+ if (!layer.visible) continue;
2414
+ let entry = cache.get(layer.id);
2415
+ if (!entry || entry.surface.width !== size || entry.surface.height !== height) {
2416
+ entry = {
2417
+ surface: surface(size, height),
2418
+ base: surface(size, height)
1772
2419
  };
1773
- continue;
2420
+ cache.set(layer.id, entry);
1774
2421
  }
1775
- if (command.op !== "stroke") throw Error("Unknown command");
1776
- const shape = command.shape === "ellipse" ? "circle" : command.shape ?? "pen";
1777
- const { color, width = shape === "text" ? 24 : 2, opacity = 1, fill = false } = command;
1778
- let points = command.points;
1779
- if (command.start !== void 0 || command.segments !== void 0) {
1780
- 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");
1781
- points = [command.start, ...command.segments.flatMap((s) => [
1782
- s?.control1,
1783
- s?.control2,
1784
- s?.end
1785
- ])];
2422
+ const moving = layer.id === activeLayer;
2423
+ const count = Math.max(0, layer.strokes.length - (moving ? 1 : 0));
2424
+ const prefix = layer.strokes[count - 1];
2425
+ if (entry.count !== count || entry.prefix !== prefix || entry.image !== layer.image || entry.strokes !== layer.strokes) {
2426
+ const ctx = entry.base.getContext("2d");
2427
+ 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]));
2428
+ if (!append) {
2429
+ ctx.clearRect(0, 0, size, height);
2430
+ const ref = layer.image, image = ref && images.get(ref.src);
2431
+ if (image) ctx.drawImage(image, ref.x * size, ref.y * height, ref.width * size, ref.height * height);
2432
+ }
2433
+ paintSketch(ctx, layer.strokes, size, true, height, append ? entry.count : 0, count);
2434
+ entry.count = count;
2435
+ entry.prefix = prefix;
2436
+ entry.image = layer.image;
2437
+ entry.strokes = layer.strokes;
1786
2438
  }
1787
- if (![
1788
- "pen",
1789
- "line",
1790
- "rectangle",
1791
- "circle",
1792
- "polygon",
1793
- "bezier",
1794
- "arrow",
1795
- "text",
1796
- "eraser"
1797
- ].includes(shape) || !/^#[0-9a-f]{6}$/i.test(color ?? "") || !finite(width, 1, 256) || !finite(opacity, 0, 1) || typeof fill !== "boolean" || !Array.isArray(points) || !points.length || points.length > 2e3 || points.some((p) => !p || !finite(p.x, 0, 1) || !finite(p.y, 0, 1))) throw Error("Invalid stroke");
1798
- if ([
1799
- "line",
1800
- "arrow",
1801
- "text",
1802
- "rectangle",
1803
- "circle"
1804
- ].includes(shape) && points.length !== 2 || shape === "polygon" && points.length < 3) throw Error("Invalid shape points");
1805
- if (shape === "bezier" && (points.length < 4 || points.length > 193 || (points.length - 1) % 3 !== 0)) throw Error(`commands[${commands.indexOf(command)}]: Bezier has ${points.length} points; expected 4, 7, 10, ... 193 (start + control1/control2/end per segment). No commands in this batch were applied.`);
1806
- if (fill && ![
1807
- "rectangle",
1808
- "circle",
1809
- "polygon",
1810
- "bezier"
1811
- ].includes(shape)) throw Error("Fill requires a closed shape");
1812
- const layer = doc.layers.find((layer) => layer.id === (command.layer ?? doc.active));
1813
- if (!layer?.visible) throw Error("Target layer is missing or hidden");
1814
- if (shape === "text" && (typeof command.text !== "string" || !command.text.trim() || command.text.length > 500 || points[0].x === points[1].x || points[0].y === points[1].y)) throw Error("Text requires 1–500 characters and a non-empty bounding box");
1815
- const id = command.id ?? crypto.randomUUID();
1816
- if (typeof id !== "string" || !id.length || id.length > 100 || layer.strokes.some((s) => s.id === id)) throw Error("Invalid or duplicate object id");
1817
- const stroke = {
1818
- id,
1819
- ...shape === "text" ? { text: command.text } : {},
1820
- shape,
1821
- color,
1822
- width,
1823
- opacity,
1824
- fill,
1825
- brush: "pen",
1826
- points: points.map((p) => ({
1827
- x: p.x,
1828
- y: p.y
1829
- }))
2439
+ if (moving) {
2440
+ const ctx = entry.surface.getContext("2d");
2441
+ ctx.clearRect(0, 0, size, height);
2442
+ ctx.drawImage(entry.base, 0, 0);
2443
+ paintSketch(ctx, layer.strokes, size, true, height, layer.strokes.length - 1);
2444
+ context.drawImage(entry.surface, 0, 0);
2445
+ } else context.drawImage(entry.base, 0, 0);
2446
+ }
2447
+ }
2448
+ //#endregion
2449
+ //#region src/sketch-interactions.js
2450
+ function useSketchDismiss(open, close, host, selectors) {
2451
+ const latest = (0, react.useRef)(close);
2452
+ latest.current = close;
2453
+ (0, react.useEffect)(() => {
2454
+ if (!open) return;
2455
+ const dialog = host.current?.closest("dialog") ?? host.current;
2456
+ if (!dialog) return;
2457
+ const pointer = (event) => {
2458
+ if (selectors.some((selector) => event.target.closest?.(selector))) return;
2459
+ latest.current(false);
2460
+ if (event.target.matches?.("canvas")) {
2461
+ event.preventDefault();
2462
+ event.stopPropagation();
2463
+ event.target.focus({ preventScroll: true });
2464
+ }
1830
2465
  };
1831
- doc = {
1832
- ...doc,
1833
- layers: doc.layers.map((item) => item === layer ? {
1834
- ...item,
1835
- strokes: [...item.strokes, stroke]
1836
- } : item)
2466
+ const key = (event) => {
2467
+ if (event.key !== "Escape") return;
2468
+ event.preventDefault();
2469
+ event.stopPropagation();
2470
+ latest.current(false);
2471
+ dialog.querySelector("canvas")?.focus({ preventScroll: true });
1837
2472
  };
1838
- }
1839
- if (strokeCount(doc) > 2e3 || doc.layers.reduce((n, l) => n + l.strokes.reduce((m, s) => m + s.points.length, 0), 0) > 2e5) throw Error("Sketch resource budget exceeded");
1840
- return doc;
2473
+ const hidden = () => latest.current(false);
2474
+ document.addEventListener("pointerdown", pointer, true);
2475
+ document.addEventListener("keydown", key, true);
2476
+ dialog.addEventListener("close", hidden);
2477
+ return () => {
2478
+ document.removeEventListener("pointerdown", pointer, true);
2479
+ document.removeEventListener("keydown", key, true);
2480
+ dialog.removeEventListener("close", hidden);
2481
+ };
2482
+ }, [
2483
+ open,
2484
+ host,
2485
+ selectors.join("|")
2486
+ ]);
1841
2487
  }
1842
- function createSketchCommandSession(adapter) {
1843
- const completed = /* @__PURE__ */ new Map();
1844
- let pending = false, cachedCharacters = 0;
1845
- return async (request) => {
1846
- if (!request || typeof request !== "object") throw Error("Invalid sketch request");
1847
- if (!adapter.available()) throw Error("Open the sketch board for this session first");
1848
- const current = adapter.snapshot();
1849
- if (request.action === "inspect") {
1850
- const offset = request.offset ?? 0, objects = adapter.objects?.() ?? [];
1851
- if (!Number.isInteger(offset) || offset < 0) throw Error("offset must be a non-negative integer");
2488
+ function useSketchCursor(canvas, ring, width, brush, zoom, hidden) {
2489
+ const last = (0, react.useRef)(null), heldPressure = (0, react.useRef)(1);
2490
+ const update = (event, bounds) => {
2491
+ if (event) last.current = event;
2492
+ const pointer = last.current, node = canvas.current, cursor = ring.current;
2493
+ if (!pointer || !node || !cursor) return;
2494
+ const rect = bounds ?? node.getBoundingClientRect();
2495
+ if (hidden || pointer.pointerType === "touch" || pointer.clientX < rect.left || pointer.clientX > rect.right || pointer.clientY < rect.top || pointer.clientY > rect.bottom) {
2496
+ cursor.hidden = true;
2497
+ return;
2498
+ }
2499
+ const diameter = width * (node.hasPointerCapture(pointer.pointerId) && pointer.pointerType === "pen" ? heldPressure.current : 1) * rect.width / node.width;
2500
+ cursor.hidden = false;
2501
+ cursor.style.width = `${diameter}px`;
2502
+ cursor.style.height = `${diameter}px`;
2503
+ cursor.style.transform = `translate(${pointer.clientX - diameter / 2}px,${pointer.clientY - diameter / 2}px)`;
2504
+ };
2505
+ (0, react.useEffect)(() => {
2506
+ update();
2507
+ const observer = new ResizeObserver(() => update());
2508
+ if (canvas.current) observer.observe(canvas.current);
2509
+ return () => observer.disconnect();
2510
+ }, [
2511
+ width,
2512
+ brush,
2513
+ zoom,
2514
+ hidden
2515
+ ]);
2516
+ return {
2517
+ down: (event) => {
2518
+ heldPressure.current = event.pointerType === "pen" ? Math.max(.2, event.pressure) : 1;
2519
+ },
2520
+ move: (event, bounds) => update({
2521
+ clientX: event.clientX,
2522
+ clientY: event.clientY,
2523
+ pointerId: event.pointerId,
2524
+ pointerType: event.pointerType,
2525
+ pressure: event.pressure
2526
+ }, bounds),
2527
+ leave: () => {
2528
+ last.current = null;
2529
+ if (ring.current) ring.current.hidden = true;
2530
+ }
2531
+ };
2532
+ }
2533
+ //#endregion
2534
+ //#region src/sketch-view.jsx
2535
+ const DEFAULT_KEYS = {
2536
+ pen: "b",
2537
+ eraser: "e",
2538
+ line: "l",
2539
+ rectangle: "r",
2540
+ circle: "o",
2541
+ pan: " ",
2542
+ zoomIn: "=",
2543
+ zoomOut: "-",
2544
+ fit: "0"
2545
+ };
2546
+ function useSketchView(canvas, open) {
2547
+ const [view, setView] = (0, react.useState)({
2548
+ scale: 1,
2549
+ x: 0,
2550
+ y: 0
2551
+ }), [keys, setKeys] = (0, react.useState)(() => {
2552
+ try {
1852
2553
  return {
1853
- ...current,
1854
- protocolVersion: 2,
1855
- objects: objects.slice(offset, offset + 50),
1856
- objectCount: objects.length,
1857
- ...offset + 50 < objects.length ? { nextOffset: offset + 50 } : {},
1858
- ...request.objectId ? { object: adapter.object?.(request.objectId, request.layer) } : {},
1859
- recentRequests: [...completed.values()].slice(-8).map((entry) => entry.receipt),
1860
- help: SKETCH_COMMAND_HELP
2554
+ ...DEFAULT_KEYS,
2555
+ ...JSON.parse(localStorage.getItem("codex-sketch-keys"))
1861
2556
  };
2557
+ } catch {
2558
+ return DEFAULT_KEYS;
1862
2559
  }
1863
- if (request.documentId !== current.documentId) throw Error("Document changed; inspect again");
1864
- if (pending || adapter.busy()) throw Error("Sketch is being edited; retry after it settles");
1865
- if (request.action === "preview") return {
1866
- ...current,
1867
- png: await adapter.preview()
1868
- };
1869
- if (!["apply", "save"].includes(request.action)) throw Error("Unknown sketch action");
1870
- if (typeof request.requestId !== "string" || !request.requestId.length || request.requestId.length > 100) throw Error("A unique requestId is required");
1871
- const key = `${current.documentId}:${request.requestId}`, fingerprint = JSON.stringify({
1872
- ...request,
1873
- runId: void 0
1874
- });
1875
- const cached = completed.get(key);
1876
- if (cached) {
1877
- if (cached.fingerprint !== fingerprint) throw Error("requestId reused with different content");
1878
- return cached.result;
1879
- }
1880
- if (request.revision !== current.revision || adapter.busy()) throw Error("Sketch changed or is being edited; inspect again");
1881
- let changedObjects;
1882
- if (request.action === "apply") {
1883
- const before = adapter.document();
1884
- let next;
1885
- try {
1886
- next = applySketchCommands(before, request.commands);
1887
- } catch (cause) {
1888
- const error = new Error(`${cause.message} Correct the batch and retry with the same runId and revision; nothing was applied.`, { cause });
1889
- error.code = "SKETCH_INVALID_BATCH";
1890
- throw error;
1891
- }
1892
- adapter.commit(next);
1893
- const previous = new Map(before.layers.flatMap((l) => l.strokes.map((s) => [`${l.id}:${s.id}`, s])));
1894
- changedObjects = next.layers.flatMap((l) => l.strokes.filter((s) => previous.get(`${l.id}:${s.id}`) !== s).map((s) => ({
1895
- layer: l.id,
1896
- id: s.id
1897
- })));
1898
- } else {
1899
- if (request.name !== void 0 && (typeof request.name !== "string" || request.name.length > 60)) throw Error("Invalid draft name");
1900
- pending = true;
1901
- try {
1902
- await adapter.save(request.name);
1903
- } finally {
1904
- pending = false;
1905
- }
2560
+ });
2561
+ const [shortcuts, setShortcuts] = (0, react.useState)(() => {
2562
+ try {
2563
+ return localStorage.getItem("codex-sketch-shortcuts") !== "off";
2564
+ } catch {
2565
+ return true;
1906
2566
  }
1907
- const result = {
1908
- ...adapter.snapshot(),
1909
- ...changedObjects ? {
1910
- changedObjects: changedObjects.slice(0, 100),
1911
- changedObjectCount: changedObjects.length
1912
- } : {}
2567
+ }), [space, setSpace] = (0, react.useState)(false);
2568
+ const drag = (0, react.useRef)(null), viewRef = (0, react.useRef)(view);
2569
+ viewRef.current = view;
2570
+ const zoom = (factor) => setView((v) => ({
2571
+ ...v,
2572
+ scale: Math.max(.25, Math.min(8, v.scale * factor))
2573
+ }));
2574
+ const reset = () => setView({
2575
+ scale: 1,
2576
+ x: 0,
2577
+ y: 0
2578
+ });
2579
+ (0, react.useEffect)(() => {
2580
+ const node = canvas.current;
2581
+ if (!node || !open) return;
2582
+ const wheel = (e) => {
2583
+ if (!shortcuts || !e.altKey) return;
2584
+ e.preventDefault();
2585
+ zoom(e.deltaY < 0 ? 1.1 : 1 / 1.1);
1913
2586
  };
1914
- completed.set(key, {
1915
- fingerprint,
1916
- result,
1917
- receipt: {
1918
- requestId: request.requestId,
1919
- action: request.action,
1920
- revision: result.revision
1921
- }
1922
- });
1923
- cachedCharacters += fingerprint.length;
1924
- while (completed.size > 1 && (completed.size > 128 || cachedCharacters > 4e6)) {
1925
- const oldest = completed.keys().next().value;
1926
- cachedCharacters -= completed.get(oldest).fingerprint.length;
1927
- completed.delete(oldest);
2587
+ node.addEventListener("wheel", wheel, { passive: false });
2588
+ return () => node.removeEventListener("wheel", wheel);
2589
+ }, [open, shortcuts]);
2590
+ (0, react.useEffect)(() => {
2591
+ const stop = () => {
2592
+ drag.current = null;
2593
+ setSpace(false);
2594
+ };
2595
+ window.addEventListener("blur", stop);
2596
+ return () => window.removeEventListener("blur", stop);
2597
+ }, []);
2598
+ (0, react.useEffect)(() => {
2599
+ if (!open || !shortcuts) {
2600
+ setSpace(false);
2601
+ drag.current = null;
1928
2602
  }
1929
- return result;
1930
- };
1931
- }
1932
- //#endregion
1933
- //#region src/sketch-formats.js
1934
- const SKETCH_FILE_ACCEPT = ".psd,.dsh-sketch.json,image/png,image/jpeg,image/webp";
1935
- const canvas = (w, h) => {
1936
- const c = document.createElement("canvas");
1937
- c.width = w;
1938
- c.height = h;
1939
- return c;
1940
- };
1941
- function runPsdCodec(action, payload) {
1942
- return new Promise((resolve, reject) => {
1943
- const worker = new Worker("/api/codex-subscription/sketch-psd-worker", { type: "module" });
1944
- const finish = (callback, value) => {
1945
- clearTimeout(timer);
1946
- worker.terminate();
1947
- callback(value);
2603
+ }, [open, shortcuts]);
2604
+ const setKey = (action, key) => {
2605
+ key = key.toLowerCase();
2606
+ if (!key || Object.entries(keys).some(([a, k]) => a !== action && k === key) || ["[", "]"].includes(key)) return;
2607
+ const next = {
2608
+ ...keys,
2609
+ [action]: key
1948
2610
  };
1949
- const timer = setTimeout(() => finish(reject, Error("PSD operation timed out")), 3e4);
1950
- worker.onerror = () => finish(reject, Error("PSD codec could not be loaded"));
1951
- worker.onmessage = (event) => event.data.ok ? finish(resolve, event.data.value) : finish(reject, Error(event.data.error));
1952
- worker.postMessage({
1953
- action,
1954
- payload
1955
- });
1956
- });
1957
- }
1958
- function encodeSketchDocument(doc) {
1959
- return JSON.stringify({
1960
- format: "dsh-sketch",
1961
- version: 1,
1962
- doc
1963
- });
1964
- }
1965
- function decodeSketchDocument(text) {
1966
- if (text.length > 32 * 1024 * 1024) throw Error("Draft exceeds 32 MB");
1967
- const file = JSON.parse(text), source = file.doc;
1968
- if (file.format !== "dsh-sketch" || file.version !== 1 || !source || !Array.isArray(source.layers) || !source.layers.length || source.layers.length > 8) throw Error("Invalid sketch file");
1969
- const w = source.width ?? 1024, h = source.height ?? 1024;
1970
- if (!Number.isInteger(w) || !Number.isInteger(h) || w < 1 || h < 1 || w > 2048 || h > 2048) throw Error("Invalid canvas size");
1971
- let doc = {
1972
- ...createSketchLayers(),
1973
- width: w,
1974
- height: h,
1975
- ratio: Object.keys(SKETCH_RATIOS).find((k) => SKETCH_RATIOS[k][0] === w && SKETCH_RATIOS[k][1] === h) ?? "custom"
2611
+ setKeys(next);
2612
+ try {
2613
+ localStorage.setItem("codex-sketch-keys", JSON.stringify(next));
2614
+ } catch {}
1976
2615
  };
1977
- for (let i = 0; i < source.layers.length; i++) {
1978
- const layer = source.layers[i];
1979
- if (i) doc = applySketchCommands(doc, [{
1980
- op: "layer",
1981
- action: "add"
1982
- }]);
1983
- if (!Array.isArray(layer.strokes)) throw Error("Invalid strokes");
1984
- for (let j = 0; j < layer.strokes.length; j += 256) {
1985
- const strokes = layer.strokes.slice(j, j + 256);
1986
- doc = applySketchCommands(doc, strokes.map((s) => ({
1987
- ...s,
1988
- op: "stroke",
1989
- layer: doc.active,
1990
- fill: s.fill ?? false
1991
- })));
1992
- const added = doc.layers.at(-1).strokes;
1993
- for (let k = 0; k < strokes.length; k++) {
1994
- const s = strokes[k];
1995
- if (s.brush !== void 0 && ![
1996
- "pen",
1997
- "pencil",
1998
- "marker"
1999
- ].includes(s.brush)) throw Error("Invalid brush");
2000
- if (s.pressure !== void 0 && (!Number.isFinite(s.pressure) || s.pressure < .2 || s.pressure > 1)) throw Error("Invalid pressure");
2001
- if (s.brushVersion !== void 0 && s.brushVersion !== 2) throw Error("Unsupported brush version");
2002
- Object.assign(added[added.length - strokes.length + k], {
2003
- brush: s.brush ?? "pen",
2004
- pressure: s.pressure ?? 1,
2005
- ...s.brushVersion === 2 ? { brushVersion: 2 } : {}
2006
- });
2616
+ return {
2617
+ view,
2618
+ keys,
2619
+ shortcuts,
2620
+ space,
2621
+ zoom,
2622
+ reset,
2623
+ setKey,
2624
+ toggle: () => setShortcuts((v) => {
2625
+ try {
2626
+ localStorage.setItem("codex-sketch-shortcuts", v ? "off" : "on");
2627
+ } catch {}
2628
+ return !v;
2629
+ }),
2630
+ keyDown: (e) => {
2631
+ if (!shortcuts || e.ctrlKey || e.metaKey || e.altKey) return false;
2632
+ const action = sketchShortcutAction(keys, e.key);
2633
+ if (action === "pan") {
2634
+ e.preventDefault();
2635
+ setSpace(true);
2636
+ return true;
2007
2637
  }
2008
- }
2009
- const target = doc.layers.at(-1);
2010
- target.name = String(layer.name ?? "").slice(0, 40);
2011
- target.visible = layer.visible !== false;
2012
- if (layer.image) {
2013
- const image = layer.image;
2014
- if (typeof image.src !== "string" || !/^data:image\/png;base64,/.test(image.src) || image.src.length > 8 * 1024 * 1024 || [
2015
- "x",
2016
- "y",
2017
- "width",
2018
- "height"
2019
- ].some((k) => !Number.isFinite(image[k]) || image[k] < 0 || image[k] > 1)) throw Error("Invalid draft image");
2020
- const bytes = Uint8Array.from(atob(image.src.slice(image.src.indexOf(",") + 1)), (c) => c.charCodeAt(0));
2021
- if (bytes.length < 24) throw Error("Invalid draft image");
2022
- const header = new DataView(bytes.buffer);
2023
- if (header.getUint32(0) !== 2303741511 || header.getUint32(4) !== 218765834 || header.getUint32(16) < 1 || header.getUint32(20) < 1 || header.getUint32(16) > 4096 || header.getUint32(20) > 4096) throw Error("Invalid draft image size");
2024
- target.image = {
2025
- src: image.src,
2026
- x: image.x,
2027
- y: image.y,
2028
- width: image.width,
2029
- height: image.height
2638
+ if ([
2639
+ "zoomIn",
2640
+ "zoomOut",
2641
+ "fit"
2642
+ ].includes(action)) {
2643
+ e.preventDefault();
2644
+ if (action === "fit") reset();
2645
+ else zoom(action === "zoomOut" ? 1 / 1.2 : 1.2);
2646
+ return true;
2647
+ }
2648
+ return false;
2649
+ },
2650
+ keyUp: (e) => {
2651
+ if (e.key.toLowerCase() === keys.pan) setSpace(false);
2652
+ },
2653
+ down: (e) => {
2654
+ if (e.button !== 1 && !space) return false;
2655
+ e.preventDefault();
2656
+ drag.current = {
2657
+ id: e.pointerId,
2658
+ x: e.clientX,
2659
+ y: e.clientY,
2660
+ view: viewRef.current
2030
2661
  };
2031
- }
2032
- }
2033
- const activeIndex = source.layers.findIndex((layer) => layer.id === source.active);
2034
- doc.active = doc.layers[Math.max(0, activeIndex)].id;
2035
- return doc;
2036
- }
2037
- async function exportSketchPsd(doc, images, composite) {
2038
- const width = doc.width ?? 1024, height = doc.height ?? 1024;
2039
- const children = doc.layers.map((layer, i) => {
2040
- const ctx = canvas(width, height).getContext("2d"), ref = layer.image;
2041
- if (ref) ctx.drawImage(images.get(ref.src), ref.x * width, ref.y * height, ref.width * width, ref.height * height);
2042
- paintSketch(ctx, layer.strokes, width, true, height);
2043
- return {
2044
- name: layer.name || `Layer ${i + 1}`,
2045
- hidden: !layer.visible,
2046
- opacity: 1,
2047
- blendMode: "normal",
2048
- imageData: ctx.getImageData(0, 0, width, height)
2049
- };
2050
- });
2051
- const context = canvas(width, height).getContext("2d");
2052
- context.fillStyle = "#fff";
2053
- context.fillRect(0, 0, width, height);
2054
- if (children[0] && !children[0].hidden) {
2055
- const bottom = canvas(width, height);
2056
- bottom.getContext("2d").putImageData(children[0].imageData, 0, 0);
2057
- context.drawImage(bottom, 0, 0);
2058
- children[0].imageData = context.getImageData(0, 0, width, height);
2059
- } else {
2060
- if (children.length >= 8) throw Error("Show the bottom layer before exporting this eight-layer drawing");
2061
- children.unshift({
2062
- name: "Paper",
2063
- opacity: 1,
2064
- blendMode: "normal",
2065
- imageData: context.getImageData(0, 0, width, height)
2066
- });
2067
- }
2068
- return runPsdCodec("write", {
2069
- width,
2070
- height,
2071
- children,
2072
- imageData: composite.getContext("2d").getImageData(0, 0, width, height)
2073
- });
2662
+ canvas.current.setPointerCapture(e.pointerId);
2663
+ return true;
2664
+ },
2665
+ move: (e) => {
2666
+ const d = drag.current;
2667
+ if (!d || d.id !== e.pointerId) return false;
2668
+ setView({
2669
+ ...d.view,
2670
+ x: d.view.x + e.clientX - d.x,
2671
+ y: d.view.y + e.clientY - d.y
2672
+ });
2673
+ return true;
2674
+ },
2675
+ end: (e) => {
2676
+ if (drag.current?.id !== e.pointerId) return false;
2677
+ drag.current = null;
2678
+ if (canvas.current.hasPointerCapture(e.pointerId)) canvas.current.releasePointerCapture(e.pointerId);
2679
+ return true;
2680
+ }
2681
+ };
2074
2682
  }
2075
- async function importSketchPsd(file) {
2076
- if (file.size > 32 * 1024 * 1024) throw Error("PSD exceeds 32 MB");
2077
- const psd = await runPsdCodec("read", await file.arrayBuffer());
2078
- const scale = Math.min(1, 1024 / Math.max(psd.width, psd.height)), width = Math.max(1, Math.round(psd.width * scale)), height = Math.max(1, Math.round(psd.height * scale));
2079
- const doc = {
2080
- ...createSketchLayers(),
2081
- width,
2082
- height,
2083
- ratio: Object.keys(SKETCH_RATIOS).find((k) => SKETCH_RATIOS[k][0] === width && SKETCH_RATIOS[k][1] === height) ?? "custom",
2084
- layers: [],
2085
- nextId: psd.layers.length + 1
2683
+ function SketchViewControls({ navigation, t }) {
2684
+ const [open, setOpen] = (0, react.useState)(false), [placement, setPlacement] = (0, react.useState)(null);
2685
+ const host = (0, react.useRef)(null), trigger = (0, react.useRef)(null);
2686
+ const close = () => {
2687
+ setOpen(false);
2688
+ trigger.current?.focus({ preventScroll: true });
2086
2689
  };
2087
- for (const [i, layer] of psd.layers.entries()) {
2088
- const src = canvas(layer.imageData.width, layer.imageData.height);
2089
- src.getContext("2d").putImageData(new ImageData(new Uint8ClampedArray(layer.imageData.data), layer.imageData.width, layer.imageData.height), 0, 0);
2090
- const out = canvas(width, height), ctx = out.getContext("2d");
2091
- ctx.globalAlpha = layer.opacity;
2092
- ctx.drawImage(src, layer.left * scale, layer.top * scale, src.width * scale, src.height * scale);
2093
- doc.layers.push({
2094
- id: i + 1,
2095
- name: layer.name,
2096
- visible: !layer.hidden,
2097
- strokes: [],
2098
- image: {
2099
- src: out.toDataURL("image/png"),
2100
- x: 0,
2101
- y: 0,
2102
- width: 1,
2103
- height: 1
2104
- }
2105
- });
2106
- }
2107
- return doc;
2690
+ useSketchDismiss(open, setOpen, host, [".codexSketchViewControls", ".codexSketchKeyPanel"]);
2691
+ (0, react.useLayoutEffect)(() => {
2692
+ if (!open) return;
2693
+ const dialog = host.current.closest("dialog");
2694
+ const place = () => {
2695
+ const box = dialog.getBoundingClientRect(), anchor = trigger.current.getBoundingClientRect();
2696
+ const width = Math.min(360, box.width - 24);
2697
+ setPlacement({
2698
+ dialog,
2699
+ style: {
2700
+ width,
2701
+ left: Math.max(12, Math.min(anchor.left - box.left, box.width - width - 12)),
2702
+ bottom: box.bottom - anchor.top + 8,
2703
+ maxHeight: Math.max(80, anchor.top - box.top - 24)
2704
+ }
2705
+ });
2706
+ };
2707
+ place();
2708
+ const observer = new ResizeObserver(place);
2709
+ observer.observe(dialog);
2710
+ observer.observe(host.current);
2711
+ window.addEventListener("resize", place);
2712
+ return () => {
2713
+ observer.disconnect();
2714
+ window.removeEventListener("resize", place);
2715
+ };
2716
+ }, [open]);
2717
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2718
+ ref: host,
2719
+ className: "codexSketchViewControls",
2720
+ children: [
2721
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2722
+ type: "button",
2723
+ "aria-label": t("sketchZoomOut"),
2724
+ onClick: () => navigation.zoom(1 / 1.2),
2725
+ children: "−"
2726
+ }),
2727
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
2728
+ type: "button",
2729
+ title: t("sketchFit"),
2730
+ onClick: navigation.reset,
2731
+ children: [Math.round(navigation.view.scale * 100), "%"]
2732
+ }),
2733
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2734
+ type: "button",
2735
+ "aria-label": t("sketchZoomIn"),
2736
+ onClick: () => navigation.zoom(1.2),
2737
+ children: "+"
2738
+ }),
2739
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2740
+ ref: trigger,
2741
+ type: "button",
2742
+ "aria-expanded": open,
2743
+ onClick: () => setOpen(!open),
2744
+ children: t("sketchKeys")
2745
+ }),
2746
+ open && placement ? (0, react_dom.createPortal)(/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
2747
+ className: "codexSketchKeyPanel",
2748
+ "aria-label": t("sketchKeys"),
2749
+ style: placement.style,
2750
+ onKeyDown: (e) => {
2751
+ if (e.key === "Escape") {
2752
+ e.preventDefault();
2753
+ e.stopPropagation();
2754
+ close();
2755
+ }
2756
+ },
2757
+ children: [
2758
+ /* @__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", {
2759
+ type: "button",
2760
+ "aria-label": t("sketchFileClose"),
2761
+ onClick: close,
2762
+ children: "×"
2763
+ })] }),
2764
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
2765
+ className: "codexSketchKeysEnabled",
2766
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("sketchKeysEnabled") }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
2767
+ type: "checkbox",
2768
+ checked: navigation.shortcuts,
2769
+ onChange: navigation.toggle
2770
+ })]
2771
+ }),
2772
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", { children: t("sketchNavigationHint") }),
2773
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2774
+ className: "codexSketchKeyGrid",
2775
+ 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", {
2776
+ "aria-label": t(`sketchKey_${action}`),
2777
+ value: key === " " ? "Space" : key,
2778
+ readOnly: true,
2779
+ onKeyDown: (e) => {
2780
+ if (e.key === "Tab" || e.key === "Escape") return;
2781
+ e.preventDefault();
2782
+ e.stopPropagation();
2783
+ if (e.key.length === 1 && !e.ctrlKey && !e.metaKey && !e.altKey) navigation.setKey(action, e.key);
2784
+ }
2785
+ })] }, action))
2786
+ }),
2787
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("small", { children: t("sketchKeyHint") })
2788
+ ]
2789
+ }), placement.dialog) : null
2790
+ ]
2791
+ });
2108
2792
  }
2109
2793
  //#endregion
2110
2794
  //#region src/sketch-files.jsx
2111
- function SketchFiles({ save, load, fresh, importImage, download, hasContent, disabled, t, report, onWorking }) {
2795
+ function SketchFiles({ save, load, fresh, importImage, download, hasContent, disabled, t, runOperation }) {
2112
2796
  const [open, setOpen] = (0, react.useState)(false), [rows, setRows] = (0, react.useState)([]), [name, setName] = (0, react.useState)(""), [remove, setRemove] = (0, react.useState)(null), [working, setWorking] = (0, react.useState)(false);
2113
2797
  const [format, setFormat] = (0, react.useState)("png");
2114
2798
  const input = (0, react.useRef)(null), host = (0, react.useRef)(null);
@@ -2120,19 +2804,14 @@ window.__ModuleLoader__.load({
2120
2804
  }
2121
2805
  }, [open, working]);
2122
2806
  useSketchDismiss(open, setOpen, host, [".codexSketchFiles"]);
2123
- const run = async (operation) => {
2124
- if (disabled || working) return;
2807
+ const run = (operation) => runOperation(async () => {
2125
2808
  setWorking(true);
2126
- onWorking(true);
2127
2809
  try {
2128
2810
  await operation();
2129
- } catch (error) {
2130
- report(error?.message || t("sketchStorageFailed"));
2131
2811
  } finally {
2132
2812
  setWorking(false);
2133
- onWorking(false);
2134
2813
  }
2135
- };
2814
+ });
2136
2815
  const refresh = async () => setRows(await sketchDrafts("list"));
2137
2816
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2138
2817
  ref: host,
@@ -2429,13 +3108,15 @@ window.__ModuleLoader__.load({
2429
3108
  //#endregion
2430
3109
  //#region src/sketch-agent-client.js
2431
3110
  function connectSketchAgent(rpc, sessionId, execute, report, pollDelay = () => 350) {
2432
- let stopped = false, token, timer, attempts = 0, failures = 0;
3111
+ let stopped = false, token, timer, attempts = 0, failures = 0, pending = false;
2433
3112
  const call = (endpoint, payload) => rpc.call(CHANNEL, `sketch/${endpoint}`, {
2434
3113
  sessionId,
2435
3114
  token,
2436
3115
  ...payload
2437
3116
  }).then(unwrap);
2438
3117
  const poll = async () => {
3118
+ if (stopped || pending) return;
3119
+ pending = true;
2439
3120
  try {
2440
3121
  const tasks = await call("poll");
2441
3122
  for (const task of tasks) {
@@ -2470,25 +3151,53 @@ window.__ModuleLoader__.load({
2470
3151
  timer = setTimeout(connect, Math.min(1e4, 1e3 * 2 ** (failures - 1)));
2471
3152
  }
2472
3153
  return;
3154
+ } finally {
3155
+ pending = false;
2473
3156
  }
2474
3157
  failures = 0;
2475
3158
  if (!stopped) timer = setTimeout(poll, pollDelay());
2476
3159
  };
2477
- const connect = () => void call("connect").then((value) => {
2478
- token = value.token;
3160
+ const connect = () => {
3161
+ if (stopped || pending) return;
3162
+ pending = true;
3163
+ call("connect").then((value) => {
3164
+ pending = false;
3165
+ token = value.token;
3166
+ attempts = 0;
3167
+ if (stopped) call("disconnect").catch(() => {});
3168
+ else poll();
3169
+ }, (error) => {
3170
+ pending = false;
3171
+ if (stopped) return;
3172
+ const leaseConflict = /Another board is connected/.test(error.message);
3173
+ if (++attempts < (leaseConflict ? 8 : 3)) timer = setTimeout(connect, Math.min(3e3, 500 * attempts));
3174
+ else report(error.message);
3175
+ });
3176
+ };
3177
+ const wake = () => {
3178
+ if (stopped || pending) return;
3179
+ clearTimeout(timer);
2479
3180
  attempts = 0;
2480
- if (stopped) call("disconnect").catch(() => {});
2481
- else poll();
2482
- }, (error) => {
2483
- if (stopped) return;
2484
- const leaseConflict = /Another board is connected/.test(error.message);
2485
- if (++attempts < (leaseConflict ? 8 : 3)) timer = setTimeout(connect, Math.min(3e3, 500 * attempts));
2486
- else report(error.message);
2487
- });
3181
+ failures = 0;
3182
+ token ? poll() : connect();
3183
+ };
3184
+ const visible = () => {
3185
+ if (document.visibilityState === "visible") wake();
3186
+ };
3187
+ if (typeof window !== "undefined") {
3188
+ window.addEventListener("online", wake);
3189
+ window.addEventListener("focus", wake);
3190
+ document.addEventListener("visibilitychange", visible);
3191
+ }
2488
3192
  connect();
2489
3193
  return () => {
2490
3194
  stopped = true;
2491
3195
  clearTimeout(timer);
3196
+ if (typeof window !== "undefined") {
3197
+ window.removeEventListener("online", wake);
3198
+ window.removeEventListener("focus", wake);
3199
+ document.removeEventListener("visibilitychange", visible);
3200
+ }
2492
3201
  if (token) call("disconnect").catch(() => {});
2493
3202
  };
2494
3203
  }
@@ -2515,11 +3224,7 @@ window.__ModuleLoader__.load({
2515
3224
  const [stability, setStability] = (0, react.useState)(0), [flow, setFlow] = (0, react.useState)(100), [picturesOpen, setPicturesOpen] = (0, react.useState)(false);
2516
3225
  const pictureInput = (0, react.useRef)(null), received = (0, react.useRef)(null);
2517
3226
  const navigation = useSketchView(canvas, open);
2518
- const brushWidths = (0, react.useRef)({
2519
- pen: 12,
2520
- pencil: 6,
2521
- marker: 28
2522
- });
3227
+ const toolWidths = (0, react.useRef)({});
2523
3228
  const [revision, redraw] = (0, react.useState)(0), [tool, setTool] = (0, react.useState)("pen"), [brush, setBrush] = (0, react.useState)("pen");
2524
3229
  const [eraser, setEraser] = (0, react.useState)("pixel"), [color, setColor] = (0, react.useState)("#0088ff"), [width, setWidth] = (0, react.useState)(12);
2525
3230
  const [selection, setSelection] = (0, react.useState)(null), [textEdit, setTextEdit] = (0, react.useState)(null), [shapesOpen, setShapesOpen] = (0, react.useState)(false);
@@ -2549,15 +3254,24 @@ window.__ModuleLoader__.load({
2549
3254
  setColor(value);
2550
3255
  if (selected) editObject({ color: value });
2551
3256
  };
2552
- const chooseBrush = (name) => {
2553
- brushWidths.current[brush] = width;
2554
- setWidth(brushWidths.current[name]);
2555
- setBrush(name);
2556
- setTool("pen");
2557
- setSelection(null);
3257
+ const chooseTool = (name, nextBrush = brush) => {
3258
+ setWidth(switchSketchToolWidth(toolWidths.current, {
3259
+ tool,
3260
+ brush,
3261
+ width
3262
+ }, {
3263
+ tool: name,
3264
+ brush: nextBrush
3265
+ }));
3266
+ setBrush(nextBrush);
3267
+ setTool(name);
3268
+ if (name !== "select") setSelection(null);
2558
3269
  };
3270
+ const chooseBrush = (name) => chooseTool("pen", name);
2559
3271
  const [fillShape, setFillShape] = (0, react.useState)(false);
2560
- const [layersOpen, setLayersOpen] = (0, react.useState)(false), [busy, setBusy] = (0, react.useState)(false), [error, setError] = (0, react.useState)("");
3272
+ const [hydrated, setHydrated] = (0, react.useState)(false), [recovered, setRecovered] = (0, react.useState)(false);
3273
+ const [restoreAttempt, retryRestore] = (0, react.useState)(0);
3274
+ const [layersOpen, setLayersOpen] = (0, react.useState)(false), [busy, setBusy] = (0, react.useState)(true), [error, setError] = (0, react.useState)("");
2561
3275
  const cursorRing = (0, react.useRef)(null);
2562
3276
  const cursor = useSketchCursor(canvas, cursorRing, width, tool === "pen" ? brush : "pen", navigation.view.scale, !open || navigation.space || busy || agentLocked || tool === "select" || tool === "text");
2563
3277
  useSketchDismiss(shapesOpen, setShapesOpen, dialog, [".codexSketchShapeMenu", ".codexSketchShapeToggle"]);
@@ -2595,144 +3309,100 @@ window.__ModuleLoader__.load({
2595
3309
  if (next === doc.current) return;
2596
3310
  if (action !== "select") checkpoint();
2597
3311
  else documentRevision.current++;
2598
- doc.current = next;
2599
- setError("");
2600
- schedule();
2601
- };
2602
- (0, react.useEffect)(() => {
2603
- if (open) {
2604
- dialog.current.showModal();
2605
- canvas.current.width = doc.current.width ?? 1024;
2606
- paint();
2607
- } else dialog.current?.close();
2608
- }, [open]);
2609
- (0, react.useEffect)(() => () => {
2610
- cancelAnimationFrame(frame.current);
2611
- cache.current.clear();
2612
- }, []);
2613
- const hasContent = () => doc.current.layers.some((layer) => layer.image || layer.strokes.length);
2614
- const save = async (name) => {
2615
- const savingDocument = documentId.current, savingRevision = documentRevision.current;
2616
- const row = {
2617
- id: saved.current?.id ?? crypto.randomUUID(),
2618
- name: name?.trim() || saved.current?.name || `${t("sketchTitle")} ${(/* @__PURE__ */ new Date()).toLocaleString()}`,
2619
- updated: Date.now(),
2620
- doc: structuredClone(doc.current)
2621
- };
2622
- await sketchDrafts("save", row);
2623
- if (documentId.current === savingDocument) {
2624
- saved.current = {
2625
- id: row.id,
2626
- name: row.name
2627
- };
2628
- if (documentRevision.current === savingRevision) dirty.current = false;
2629
- }
2630
- };
2631
- const saveChanges = async () => {
2632
- if (dirty.current && (hasContent() || saved.current)) await save();
2633
- };
2634
- const replace = (next, decoded, identity) => {
2635
- documentId.current = crypto.randomUUID();
2636
- documentRevision.current++;
2637
- doc.current = identifyObjects(structuredClone(next));
2638
- setSelection(null);
2639
- setTextEdit(null);
2640
- images.current = decoded;
2641
- cache.current.clear();
2642
- undo.current = [];
2643
- redo.current = [];
2644
- saved.current = identity;
2645
- dirty.current = false;
2646
- schedule();
2647
- };
2648
- const fresh = async () => {
2649
- await saveChanges();
2650
- replace(createSketchLayers(), /* @__PURE__ */ new Map(), null);
2651
- };
2652
- const load = async (row) => {
2653
- if (row.id === saved.current?.id) return;
2654
- await saveChanges();
2655
- const decoded = /* @__PURE__ */ new Map();
2656
- await decodeSketchImages(row.doc, decoded);
2657
- replace(row.doc, decoded, {
2658
- id: row.id,
2659
- name: row.name
2660
- });
2661
- };
2662
- const importImage = async (file) => {
2663
- if (file.name?.toLowerCase().endsWith(".psd") || file.name?.toLowerCase().endsWith(".dsh-sketch.json")) {
2664
- if (file.size > 32 * 1024 * 1024) throw Error("File exceeds 32 MB");
2665
- const next = file.name.toLowerCase().endsWith(".psd") ? await importSketchPsd(file) : decodeSketchDocument(await file.text());
2666
- const decoded = /* @__PURE__ */ new Map();
2667
- await decodeSketchImages(next, decoded);
2668
- await saveChanges();
2669
- replace(next, decoded, null);
2670
- dirty.current = true;
2671
- return;
2672
- }
2673
- if (doc.current.layers.length >= 8) throw Error("Layer limit");
2674
- const image = await importSketchImage(file), w = doc.current.width ?? 1024, h = doc.current.height ?? 1024;
2675
- const scale = Math.min(w / image.width, h / image.height), width = image.width * scale / w, height = image.height * scale / h;
2676
- const layer = {
2677
- id: doc.current.nextId,
2678
- name: file.name?.slice(0, 40) || t("sketchImport"),
2679
- visible: true,
2680
- strokes: [],
2681
- image: {
2682
- src: image.src,
2683
- x: (1 - width) / 2,
2684
- y: (1 - height) / 2,
2685
- width,
2686
- height
2687
- }
2688
- };
2689
- await decodeSketchImages({ layers: [layer] }, images.current);
2690
- checkpoint();
2691
- doc.current = {
2692
- ...doc.current,
2693
- nextId: layer.id + 1,
2694
- active: layer.id,
2695
- layers: [...doc.current.layers, layer]
2696
- };
3312
+ doc.current = next;
3313
+ setError("");
2697
3314
  schedule();
2698
3315
  };
2699
- const close = async () => {
2700
- if (agentRun.current?.locked) {
3316
+ (0, react.useEffect)(() => {
3317
+ if (open) {
3318
+ dialog.current.showModal();
3319
+ canvas.current.width = doc.current.width ?? 1024;
3320
+ paint();
3321
+ } else dialog.current?.close();
3322
+ }, [open]);
3323
+ (0, react.useEffect)(() => () => {
3324
+ cancelAnimationFrame(frame.current);
3325
+ cache.current.clear();
3326
+ }, []);
3327
+ const { save, saveChanges, fresh, load, importImage, restore } = createSketchDocumentLifecycle(localSession.current, {
3328
+ sessionId,
3329
+ t,
3330
+ schedule,
3331
+ checkpoint,
3332
+ cache,
3333
+ setSelection,
3334
+ setTextEdit,
3335
+ setRecovered
3336
+ });
3337
+ (0, react.useEffect)(() => localSession.current.retain?.(), []);
3338
+ (0, react.useEffect)(() => {
3339
+ let live = true;
3340
+ setHydrated(false);
3341
+ setBusy(true);
3342
+ setError("");
3343
+ (enabled ? restore(() => live) : Promise.resolve()).then(() => {
3344
+ if (live) {
3345
+ setHydrated(true);
3346
+ setBusy(false);
3347
+ }
3348
+ }).catch((error) => {
3349
+ if (live) setError(t(error.code === "SKETCH_STORAGE_BLOCKED" ? "sketchStorageBlocked" : "sketchStorageFailed"));
3350
+ });
3351
+ return () => {
3352
+ live = false;
3353
+ };
3354
+ }, [
3355
+ enabled,
3356
+ sessionId,
3357
+ restoreAttempt
3358
+ ]);
3359
+ (0, react.useEffect)(() => {
3360
+ if (!hydrated || !enabled || !dirty.current || busy || agentLocked) return;
3361
+ const timer = setTimeout(() => {
3362
+ if (active.current || sizeGesture.current || !dirty.current) return;
3363
+ sketchDrafts("checkpoint", {
3364
+ id: sessionId,
3365
+ updated: Date.now(),
3366
+ doc: structuredClone(doc.current)
3367
+ }).catch(() => setError(t("sketchRecoveryFailed")));
3368
+ }, 1500);
3369
+ return () => clearTimeout(timer);
3370
+ }, [
3371
+ revision,
3372
+ hydrated,
3373
+ enabled,
3374
+ busy,
3375
+ agentLocked,
3376
+ sessionId
3377
+ ]);
3378
+ const close = () => {
3379
+ if (!hydrated || agentRun.current?.locked) {
2701
3380
  onClose();
2702
3381
  return;
2703
3382
  }
2704
- if (busy || active.current) return;
2705
- setBusy(true);
2706
- try {
3383
+ return runFile(async () => {
2707
3384
  await saveChanges();
2708
3385
  onClose();
2709
- } catch {
2710
- setError(t("sketchStorageFailed"));
2711
- } finally {
2712
- setBusy(false);
2713
- }
2714
- };
2715
- const runFile = async (operation) => {
2716
- if (busy || agentRun.current?.locked || active.current) return;
2717
- setBusy(true);
2718
- setError("");
2719
- try {
2720
- await operation();
2721
- } catch {
2722
- setError(t("sketchStorageFailed"));
2723
- } finally {
2724
- setBusy(false);
2725
- }
3386
+ });
2726
3387
  };
3388
+ const operationGate = (0, react.useRef)(null);
3389
+ operationGate.current ??= createSketchOperationGate();
3390
+ const runFile = (operation) => operationGate.current.run(operation, {
3391
+ blocked: busy || agentRun.current?.locked || Boolean(active.current),
3392
+ working: setBusy,
3393
+ report: (error) => setError(error ? error?.message || t("sketchStorageFailed") : "")
3394
+ });
2727
3395
  (0, react.useEffect)(() => {
2728
- if (open && !agentLocked && incoming && incoming !== received.current) {
3396
+ if (open && hydrated && !busy && !agentLocked && incoming && incoming !== received.current) {
2729
3397
  received.current = incoming;
2730
3398
  runFile(() => importImage(incoming.file));
2731
3399
  }
2732
3400
  }, [
2733
3401
  open,
2734
3402
  incoming,
2735
- agentLocked
3403
+ agentLocked,
3404
+ hydrated,
3405
+ busy
2736
3406
  ]);
2737
3407
  const keyDown = (event) => {
2738
3408
  if (event.target.closest("input,textarea,select,[contenteditable=true]") || event.isComposing || busy || active.current) return;
@@ -2744,14 +3414,6 @@ window.__ModuleLoader__.load({
2744
3414
  editObject({}, "delete");
2745
3415
  return;
2746
3416
  }
2747
- if (!event.ctrlKey && !event.metaKey && !event.altKey && event.key.toLowerCase() === "v") {
2748
- setTool("select");
2749
- return;
2750
- }
2751
- if (!event.ctrlKey && !event.metaKey && !event.altKey && event.key.toLowerCase() === "t") {
2752
- setTool("text");
2753
- return;
2754
- }
2755
3417
  const key = event.key.toLowerCase(), command = event.ctrlKey || event.metaKey;
2756
3418
  if (command && [
2757
3419
  "z",
@@ -2765,20 +3427,24 @@ window.__ModuleLoader__.load({
2765
3427
  return;
2766
3428
  }
2767
3429
  if (command || event.altKey) return;
2768
- const tools = Object.fromEntries([
3430
+ const action = sketchShortcutAction(navigation.keys, key);
3431
+ if ([
2769
3432
  "pen",
2770
3433
  "eraser",
2771
3434
  "line",
2772
3435
  "rectangle",
2773
- "circle"
2774
- ].map((action) => [navigation.keys[action], action]));
2775
- if (tools[key]) {
3436
+ "circle",
3437
+ "select",
3438
+ "text"
3439
+ ].includes(action)) {
2776
3440
  event.preventDefault();
2777
- setTool(tools[key]);
3441
+ chooseTool(action);
2778
3442
  }
2779
3443
  if (key === "[" || key === "]") {
2780
3444
  event.preventDefault();
2781
- setWidth((value) => Math.max(2, Math.min(64, value + (key === "]" ? 2 : -2))));
3445
+ const value = stepSketchWidth(selected?.width ?? width, key === "]" ? 1 : -1);
3446
+ if (selected) editObject({ width: value });
3447
+ else setWidth(value);
2782
3448
  }
2783
3449
  };
2784
3450
  const current = doc.current.layers.find((layer) => layer.id === doc.current.active);
@@ -2787,81 +3453,14 @@ window.__ModuleLoader__.load({
2787
3453
  const gesture = active.current;
2788
3454
  if (!gesture || gesture.id !== event.pointerId || busy) return;
2789
3455
  const rect = bounds ?? canvas.current.getBoundingClientRect();
2790
- if (gesture.object) {
2791
- const point = sketchPoint(event.clientX, event.clientY, rect);
2792
- if (!point) return;
2793
- try {
2794
- const box = objectBounds(gesture.object);
2795
- const stroke = gesture.handle === "end" ? {
2796
- ...gesture.object,
2797
- points: [gesture.object.points[0], point]
2798
- } : gesture.handle === "size" ? transformObject(gesture.object, {
2799
- scaleX: Math.max(.001, point.x - box.x) / Math.max(.001, box.width),
2800
- scaleY: Math.max(.001, point.y - box.y) / Math.max(.001, box.height)
2801
- }) : transformObject(gesture.object, {
2802
- dx: point.x - gesture.start.x,
2803
- dy: point.y - gesture.start.y
2804
- });
2805
- doc.current = {
2806
- ...doc.current,
2807
- layers: doc.current.layers.map((l) => l.id === gesture.layer ? {
2808
- ...l,
2809
- strokes: l.strokes.map((s) => s.id === gesture.object.id ? stroke : s)
2810
- } : l)
2811
- };
2812
- schedule();
2813
- } catch {}
2814
- return;
2815
- }
2816
3456
  const native = event.nativeEvent ?? event;
2817
- const events = native.getCoalescedEvents?.() ?? [];
2818
- for (const sample of events.length ? [...events, native] : [native]) {
2819
- let point = sketchPoint(sample.clientX, sample.clientY, rect);
2820
- if (!point) continue;
2821
- const layer = doc.current.layers.find((layer) => layer.id === gesture.layer);
2822
- if (gesture.eraseStroke) {
2823
- const previous = gesture.last ?? point;
2824
- const steps = Math.min(256, Math.max(1, Math.ceil(Math.hypot(point.x - previous.x, point.y - previous.y) * SKETCH_SIZE / Math.max(2, width / 2))));
2825
- layer.strokes = layer.strokes.filter((stroke) => {
2826
- for (let i = 1; i <= steps; i++) if (strokeHit(stroke, {
2827
- x: previous.x + (point.x - previous.x) * i / steps,
2828
- y: previous.y + (point.y - previous.y) * i / steps
2829
- }, width / 2, doc.current.width, doc.current.height)) return false;
2830
- return true;
2831
- });
2832
- } else {
2833
- const stroke = layer.strokes.at(-1);
2834
- if ([
2835
- "line",
2836
- "arrow",
2837
- "rectangle",
2838
- "circle"
2839
- ].includes(stroke.shape)) {
2840
- if (stroke.shape === "line" && event.shiftKey) {
2841
- const w = doc.current.width ?? 1024, h = doc.current.height ?? 1024, a = stroke.points[0];
2842
- const snapped = snapLine({
2843
- x: a.x * w,
2844
- y: a.y * h
2845
- }, {
2846
- x: point.x * w,
2847
- y: point.y * h
2848
- });
2849
- point = {
2850
- x: snapped.x / w,
2851
- y: snapped.y / h
2852
- };
2853
- }
2854
- stroke.points = [stroke.points[0], point];
2855
- } else {
2856
- const last = stroke.points.at(-1);
2857
- if (Math.hypot(last.x - point.x, last.y - point.y) < 1e-4) continue;
2858
- if (stroke.points.length >= 2e3) stroke.points = stroke.points.filter((_, i) => i % 2 === 0);
2859
- stroke.points.push(point);
2860
- }
2861
- }
2862
- gesture.last = point;
3457
+ const samples = native.getCoalescedEvents?.() ?? [];
3458
+ try {
3459
+ doc.current = updateSketchGesture(doc.current, gesture, samples.length ? [...samples, native] : [native], rect, width, event.shiftKey);
3460
+ schedule(Boolean(gesture.object));
3461
+ } catch (error) {
3462
+ setError(error?.message || t("sketchFailed"));
2863
3463
  }
2864
- schedule(false);
2865
3464
  };
2866
3465
  const end = (event, cancel = false) => {
2867
3466
  if (navigation.end(event)) return;
@@ -2884,7 +3483,7 @@ window.__ModuleLoader__.load({
2884
3483
  layer: drawn.layer,
2885
3484
  id: stroke.id
2886
3485
  });
2887
- setTool("select");
3486
+ chooseTool("select");
2888
3487
  }
2889
3488
  }
2890
3489
  active.current = null;
@@ -2910,7 +3509,7 @@ window.__ModuleLoader__.load({
2910
3509
  available: () => enabled && agentEnabled,
2911
3510
  previewEnabled: () => agentPreview,
2912
3511
  open: () => onOpen(),
2913
- busy: () => busy || Boolean(active.current) || Boolean(textEdit) || sizeGesture.current,
3512
+ busy: () => operationGate.current.running || busy || Boolean(active.current) || Boolean(textEdit) || sizeGesture.current,
2914
3513
  document: () => doc.current,
2915
3514
  snapshot: () => ({
2916
3515
  documentId: documentId.current,
@@ -2957,23 +3556,18 @@ window.__ModuleLoader__.load({
2957
3556
  previewEnabled: () => agentAdapter.current.previewEnabled()
2958
3557
  });
2959
3558
  (0, react.useEffect)(() => {
2960
- if (!enabled || !agentEnabled) return;
3559
+ if (!enabled || !agentEnabled || !hydrated) return;
2961
3560
  const api = Object.freeze({
2962
3561
  version: 2,
2963
3562
  sessionId,
2964
3563
  execute: (request) => agentRun.current.execute(request),
2965
- export: async (format) => {
2966
- if (agentAdapter.current.busy() || agentRun.current.locked) throw Error("Sketch is being edited");
2967
- const { blob, extension } = await agentAdapter.current.export(format);
2968
- const data = new Uint8Array(await blob.arrayBuffer());
2969
- let raw = "";
2970
- for (let i = 0; i < data.length; i += 8192) raw += String.fromCharCode(...data.subarray(i, i + 8192));
2971
- return {
2972
- extension,
2973
- mediaType: blob.type,
2974
- base64: btoa(raw)
2975
- };
2976
- }
3564
+ export: (format) => exportSketchAgentFile(format, {
3565
+ gate: operationGate.current,
3566
+ blocked: agentAdapter.current.busy() || agentRun.current.locked,
3567
+ working: setBusy,
3568
+ report: (error) => setError(error ? error.message || t("sketchFailed") : ""),
3569
+ exportFile: (value) => agentAdapter.current.export(value)
3570
+ })
2977
3571
  });
2978
3572
  window.dshSketchAgent = api;
2979
3573
  return () => {
@@ -2983,14 +3577,15 @@ window.__ModuleLoader__.load({
2983
3577
  enabled,
2984
3578
  agentEnabled,
2985
3579
  rpc,
2986
- sessionId
3580
+ sessionId,
3581
+ hydrated
2987
3582
  ]);
2988
3583
  (0, react.useEffect)(() => {
2989
3584
  if (!enabled || !agentEnabled) {
2990
3585
  if (agentRun.current.locked) agentRun.current.stop();
2991
3586
  return;
2992
3587
  }
2993
- if (!rpc || !sessionId) return;
3588
+ if (!rpc || !sessionId || !hydrated) return;
2994
3589
  let live = true;
2995
3590
  const disconnect = connectSketchAgent(rpc, sessionId, (request) => {
2996
3591
  if (!live) throw Error("Sketch session disconnected");
@@ -2998,7 +3593,7 @@ window.__ModuleLoader__.load({
2998
3593
  }, (message) => {
2999
3594
  agentRun.current.fail();
3000
3595
  setError(message);
3001
- }, () => 350);
3596
+ }, () => agentRun.current.locked ? 350 : 2e3);
3002
3597
  return () => {
3003
3598
  live = false;
3004
3599
  disconnect();
@@ -3007,23 +3602,18 @@ window.__ModuleLoader__.load({
3007
3602
  enabled,
3008
3603
  agentEnabled,
3009
3604
  rpc,
3010
- sessionId
3605
+ sessionId,
3606
+ hydrated
3011
3607
  ]);
3012
- const attach = async () => {
3013
- if (!enabled || busy || agentRun.current?.locked) return;
3014
- setBusy(true);
3015
- setError("");
3016
- try {
3608
+ const attach = () => {
3609
+ if (!enabled) return;
3610
+ return runFile(async () => {
3017
3611
  paint();
3018
- const blob = await new Promise((resolve, reject) => canvas.current.toBlob((blob) => blob ? resolve(blob) : reject(Error("PNG")), "image/png"));
3612
+ const blob = await new Promise((resolve, reject) => canvas.current.toBlob((blob) => blob ? resolve(blob) : reject(Error(t("sketchFailed"))), "image/png"));
3019
3613
  await saveChanges();
3020
3614
  await attachSketch(blob);
3021
3615
  onClose();
3022
- } catch {
3023
- setError(t("sketchFailed"));
3024
- } finally {
3025
- setBusy(false);
3026
- }
3616
+ });
3027
3617
  };
3028
3618
  const exportFile = async (format = "png") => {
3029
3619
  paint();
@@ -3043,8 +3633,8 @@ window.__ModuleLoader__.load({
3043
3633
  };
3044
3634
  agentAdapter.current.export = exportFile;
3045
3635
  const download = async (format) => {
3636
+ setError("");
3046
3637
  const { blob, extension } = await exportFile(format);
3047
- await saveChanges();
3048
3638
  const url = URL.createObjectURL(blob), link = document.createElement("a");
3049
3639
  link.href = url;
3050
3640
  link.download = `${(saved.current?.name || "sketch").replace(/[\\/:*?"<>|\u0000-\u001f]/g, "-").slice(0, 80)}.${extension}`;
@@ -3110,7 +3700,7 @@ window.__ModuleLoader__.load({
3110
3700
  type: "button",
3111
3701
  "aria-label": t("sketchCancel"),
3112
3702
  title: t("sketchCancel"),
3113
- disabled: busy && !agentLocked,
3703
+ disabled: busy && hydrated && !agentLocked,
3114
3704
  onClick: close,
3115
3705
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(WorkspaceIcon, { name: "close" })
3116
3706
  }),
@@ -3123,8 +3713,7 @@ window.__ModuleLoader__.load({
3123
3713
  hasContent: doc.current.layers.some((l) => l.visible && (l.image || l.strokes.length)),
3124
3714
  disabled: agentLocked || busy,
3125
3715
  t,
3126
- report: setError,
3127
- onWorking: setBusy
3716
+ runOperation: runFile
3128
3717
  }),
3129
3718
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3130
3719
  className: "codexSketchHeading",
@@ -3215,6 +3804,19 @@ window.__ModuleLoader__.load({
3215
3804
  },
3216
3805
  onDismiss: () => setNoticeHidden(true)
3217
3806
  }) : null,
3807
+ recovered ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3808
+ className: "codexSketchAgentStatus",
3809
+ role: "status",
3810
+ children: [t("sketchRecovered"), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
3811
+ type: "button",
3812
+ onClick: () => setRecovered(false),
3813
+ "aria-label": t("sketchDismissStatus"),
3814
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(WorkspaceIcon, {
3815
+ name: "close",
3816
+ size: 14
3817
+ })
3818
+ })]
3819
+ }) : null,
3218
3820
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3219
3821
  className: `codexLayerBody ${layersOpen ? "withLayers" : ""}`,
3220
3822
  children: [
@@ -3229,6 +3831,12 @@ window.__ModuleLoader__.load({
3229
3831
  width: SKETCH_SIZE,
3230
3832
  height: SKETCH_SIZE,
3231
3833
  "aria-label": t("sketchTitle"),
3834
+ onDoubleClick: () => {
3835
+ if (!busy && !agentRun.current?.locked && tool === "select" && selected?.shape === "text") setTextEdit({
3836
+ selection,
3837
+ value: selected.text
3838
+ });
3839
+ },
3232
3840
  onPointerDown: (event) => {
3233
3841
  if (busy || !enabled || active.current) return;
3234
3842
  if (navigation.down(event)) return;
@@ -3405,13 +4013,10 @@ window.__ModuleLoader__.load({
3405
4013
  sizeGesture.current = false;
3406
4014
  },
3407
4015
  onChange: (value) => {
3408
- if (opacityMode) {
3409
- setFlow(value);
3410
- if (selected) editObject({ opacity: value / 100 });
3411
- } else {
3412
- setWidth(value);
3413
- if (selected) editObject({ width: value });
3414
- }
4016
+ if (opacityMode) if (selected) editObject({ opacity: value / 100 });
4017
+ else setFlow(value);
4018
+ else if (selected) editObject({ width: value });
4019
+ else setWidth(value);
3415
4020
  }
3416
4021
  }),
3417
4022
  textEdit ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("form", {
@@ -3425,10 +4030,7 @@ window.__ModuleLoader__.load({
3425
4030
  try {
3426
4031
  if (textEdit.selection) editObject({ text: textEdit.value });
3427
4032
  else {
3428
- const a = textEdit.point, b = {
3429
- x: Math.min(1, a.x + .35),
3430
- y: Math.min(1, a.y + .15)
3431
- }, id = crypto.randomUUID();
4033
+ const [a, b] = newTextBounds(textEdit.point), id = crypto.randomUUID();
3432
4034
  const next = applySketchCommands(doc.current, [{
3433
4035
  op: "stroke",
3434
4036
  id,
@@ -3447,7 +4049,7 @@ window.__ModuleLoader__.load({
3447
4049
  schedule();
3448
4050
  }
3449
4051
  setTextEdit(null);
3450
- setTool("select");
4052
+ chooseTool("select");
3451
4053
  } catch (e) {
3452
4054
  setError(e.message);
3453
4055
  }
@@ -3457,6 +4059,18 @@ window.__ModuleLoader__.load({
3457
4059
  autoFocus: true,
3458
4060
  "aria-label": t("sketchText"),
3459
4061
  maxLength: 500,
4062
+ onKeyDown: (event) => {
4063
+ if (event.nativeEvent.isComposing) return;
4064
+ if (event.key === "Escape") {
4065
+ event.preventDefault();
4066
+ event.stopPropagation();
4067
+ setTextEdit(null);
4068
+ }
4069
+ if (event.key === "Enter" && (event.ctrlKey || event.metaKey)) {
4070
+ event.preventDefault();
4071
+ event.currentTarget.form.requestSubmit();
4072
+ }
4073
+ },
3460
4074
  value: textEdit.value,
3461
4075
  onChange: (e) => setTextEdit({
3462
4076
  ...textEdit,
@@ -3516,89 +4130,11 @@ window.__ModuleLoader__.load({
3516
4130
  !doc.current.layers.some((layer) => layer.image) ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("small", { children: t("sketchPicturesEmpty") }) : null
3517
4131
  ]
3518
4132
  }) : null,
3519
- layersOpen ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("aside", {
3520
- className: "codexSketchLayers",
3521
- "aria-label": t("sketchLayers"),
3522
- children: [
3523
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("header", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("strong", { children: t("sketchLayers") }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
3524
- type: "button",
3525
- title: t("sketchLayerAdd"),
3526
- "aria-label": t("sketchLayerAdd"),
3527
- disabled: agentLocked || busy || doc.current.layers.length >= 8,
3528
- onClick: () => change("add"),
3529
- children: "+"
3530
- })] }),
3531
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
3532
- className: "codexLayerList",
3533
- children: doc.current.layers.slice().reverse().map((layer) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3534
- className: "codexLayerRow",
3535
- "data-active": layer.id === doc.current.active,
3536
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
3537
- type: "button",
3538
- disabled: agentLocked || busy,
3539
- "aria-label": `${t("sketchLayerVisible")} ${layer.id}`,
3540
- "aria-pressed": layer.visible,
3541
- onClick: () => change("visible", layer.id),
3542
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(WorkspaceIcon, {
3543
- name: layer.visible ? "eye" : "eyeOff",
3544
- size: 18
3545
- })
3546
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
3547
- type: "button",
3548
- disabled: agentLocked || busy,
3549
- "aria-pressed": layer.id === doc.current.active,
3550
- onClick: () => change("select", layer.id),
3551
- children: layer.name || `${t("sketchLayer")} ${layer.id}`
3552
- })]
3553
- }, layer.id))
3554
- }),
3555
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("label", {
3556
- className: "codexSketchLayerLabel",
3557
- children: t("sketchLayerName")
3558
- }),
3559
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
3560
- disabled: agentLocked || busy,
3561
- "aria-label": t("sketchLayerName"),
3562
- defaultValue: current.name,
3563
- placeholder: `${t("sketchLayer")} ${current.id}`,
3564
- maxLength: 40,
3565
- onBlur: (e) => {
3566
- if (e.target.value !== current.name) change("rename", current.id, e.target.value);
3567
- },
3568
- onKeyDown: (e) => {
3569
- if (e.key === "Enter") e.currentTarget.blur();
3570
- }
3571
- }, current.id + "-" + current.name),
3572
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
3573
- className: "codexLayerActions",
3574
- children: [
3575
- "duplicate",
3576
- "up",
3577
- "down",
3578
- "delete"
3579
- ].map((action) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
3580
- type: "button",
3581
- title: t(`sketchLayer_${action}`),
3582
- "aria-label": t(`sketchLayer_${action}`),
3583
- disabled: agentLocked || busy || action === "delete" && doc.current.layers.length === 1 || action === "duplicate" && (doc.current.layers.length >= 8 || strokeCount(doc.current) + current.strokes.length > 2e3) || action === "up" && current === doc.current.layers.at(-1) || action === "down" && current === doc.current.layers[0],
3584
- onClick: () => change(action),
3585
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(WorkspaceIcon, {
3586
- name: action === "delete" ? "clear" : action,
3587
- size: 17
3588
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t(`sketchLayer_${action}`) })]
3589
- }, action))
3590
- }),
3591
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
3592
- type: "button",
3593
- className: "codexSketchClearLayer",
3594
- disabled: agentLocked || busy || !current.strokes.length && !current.image,
3595
- onClick: () => change("clear"),
3596
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(WorkspaceIcon, {
3597
- name: "clear",
3598
- size: 16
3599
- }), t("sketchClearLayer")]
3600
- })
3601
- ]
4133
+ layersOpen ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SketchLayerPanel, {
4134
+ document: doc.current,
4135
+ disabled: agentLocked || busy,
4136
+ change,
4137
+ t
3602
4138
  }) : null
3603
4139
  ]
3604
4140
  }),
@@ -3616,92 +4152,15 @@ window.__ModuleLoader__.load({
3616
4152
  navigation,
3617
4153
  t
3618
4154
  }),
3619
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3620
- className: "codexSketchPill",
3621
- role: "toolbar",
3622
- "aria-label": t("sketchTitle"),
3623
- title: t("sketchShortcuts"),
3624
- children: [
3625
- [
3626
- "select",
3627
- "pen",
3628
- "pencil",
3629
- "marker",
3630
- "text",
3631
- "eraser"
3632
- ].map((name) => {
3633
- const drawing = [
3634
- "pen",
3635
- "pencil",
3636
- "marker"
3637
- ].includes(name);
3638
- const label = t(drawing ? `sketchBrush_${name}` : `sketchTool_${name}`);
3639
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
3640
- type: "button",
3641
- "aria-label": label,
3642
- title: drawing ? `${label} · ${t(`sketchBrushHint_${name}`)}` : label,
3643
- "aria-pressed": drawing ? tool === "pen" && brush === name : tool === name,
3644
- disabled: agentLocked || busy,
3645
- onClick: () => {
3646
- if (drawing) chooseBrush(name);
3647
- else {
3648
- setTool(name);
3649
- if (name !== "select") setSelection(null);
3650
- }
3651
- },
3652
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(WorkspaceIcon, {
3653
- name,
3654
- size: 23
3655
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: label })]
3656
- }, name);
3657
- }),
3658
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
3659
- className: "codexSketchShapeToggle",
3660
- type: "button",
3661
- "aria-label": t("sketchShapes"),
3662
- "aria-expanded": shapesOpen,
3663
- "aria-pressed": [
3664
- "line",
3665
- "arrow",
3666
- "rectangle",
3667
- "circle"
3668
- ].includes(tool),
3669
- disabled: agentLocked || busy,
3670
- onClick: () => setShapesOpen((v) => !v),
3671
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(WorkspaceIcon, {
3672
- name: [
3673
- "line",
3674
- "arrow",
3675
- "rectangle",
3676
- "circle"
3677
- ].includes(tool) ? tool : "rectangle",
3678
- size: 23
3679
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t([
3680
- "line",
3681
- "arrow",
3682
- "rectangle",
3683
- "circle"
3684
- ].includes(tool) ? `sketchTool_${tool}` : "sketchShapes") })]
3685
- }),
3686
- shapesOpen ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
3687
- className: "codexSketchShapeMenu",
3688
- children: [
3689
- "line",
3690
- "arrow",
3691
- "rectangle",
3692
- "circle"
3693
- ].map((name) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
3694
- type: "button",
3695
- "aria-pressed": tool === name,
3696
- onClick: () => {
3697
- setTool(name);
3698
- setSelection(null);
3699
- setShapesOpen(false);
3700
- },
3701
- children: t(`sketchTool_${name}`)
3702
- }, name))
3703
- }) : null
3704
- ]
4155
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SketchToolPicker, {
4156
+ t,
4157
+ disabled: agentLocked || busy,
4158
+ tool,
4159
+ brush,
4160
+ chooseBrush,
4161
+ chooseTool,
4162
+ shapesOpen,
4163
+ setShapesOpen
3705
4164
  }),
3706
4165
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3707
4166
  className: "codexLayerBrush",
@@ -3793,10 +4252,14 @@ window.__ModuleLoader__.load({
3793
4252
  })
3794
4253
  ]
3795
4254
  }),
3796
- error ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
4255
+ error ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
3797
4256
  className: "codexSketchHint",
3798
4257
  role: "alert",
3799
- children: error
4258
+ children: [error, !hydrated ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
4259
+ type: "button",
4260
+ onClick: () => retryRestore((value) => value + 1),
4261
+ children: t("accountRetry")
4262
+ }) : null]
3800
4263
  }) : null
3801
4264
  ]
3802
4265
  })] });
@@ -3999,6 +4462,23 @@ window.__ModuleLoader__.load({
3999
4462
  //#endregion
4000
4463
  //#region src/client-locales.js
4001
4464
  const zh = {
4465
+ compactionTitle: "云端压缩",
4466
+ compaction_dsh: "关闭",
4467
+ compaction_cloud: "开启",
4468
+ compactionHint: "实验功能,默认关闭。长上下文由 Codex 压缩后继续请求,保留 DSH 历史与原生压缩。开启时该请求使用 SSE;历史改变后重新建立压缩状态。",
4469
+ advancedModelSearch: "模型与搜索",
4470
+ subagentBackendTitle: "独立子任务",
4471
+ subagentBackend_dsh: "DSH",
4472
+ subagentBackend_codex: "Codex",
4473
+ connectionTitle: "连接方式",
4474
+ connectionHint: "默认 SSE。WebSocket 实验性复用连接与上下文传输;连接失败可回退 SSE。下次请求生效,不扩大上下文容量。",
4475
+ subagentBackendHint: "Codex 复用订阅登录,跟随当前订阅模型和工作区权限;其他模型会话使用 Luna low。共享上下文子任务仍用 DSH。",
4476
+ subagentBackendUnavailable: "当前宿主缺少子代理服务,请更新 DSH。",
4477
+ sketchRecovered: "已恢复未保存草稿",
4478
+ sketchRecoveryFailed: "恢复检查点保存失败,请手动保存或导出草稿。",
4479
+ sketchStorageBlocked: "草稿正被其他窗口占用,请关闭其他草图窗口后重试。",
4480
+ sketchDraftLimit: "已达 20 份草稿上限。请先导出,或删除不需要的草稿后保存。",
4481
+ sketchStorageLimit: "草稿存储空间已满。请先导出,或删除不需要的草稿后保存。",
4002
4482
  sketchSizeShort: "粗细",
4003
4483
  sketchObjectDuplicate: "复制对象",
4004
4484
  sketchObjectDelete: "删除对象",
@@ -4410,6 +4890,23 @@ window.__ModuleLoader__.load({
4410
4890
  imageRemoveAnnotation: "删除标注"
4411
4891
  };
4412
4892
  const en = {
4893
+ compactionTitle: "Cloud compaction",
4894
+ compaction_dsh: "Off",
4895
+ compaction_cloud: "On",
4896
+ compactionHint: "Experimental, off by default. Codex compacts long requests while DSH history and native compaction remain available. These requests use SSE; changed history invalidates previous checkpoints.",
4897
+ advancedModelSearch: "Models and search",
4898
+ subagentBackendTitle: "Independent subtasks",
4899
+ subagentBackend_dsh: "DSH",
4900
+ subagentBackend_codex: "Codex",
4901
+ connectionTitle: "Connection",
4902
+ connectionHint: "SSE by default. Experimental WebSocket reuses connections and context transfers and can fall back to SSE on connection failure. Applies to the next request; context limits stay the same.",
4903
+ subagentBackendHint: "Codex uses your subscription login, current subscription model and workspace permissions; other model sessions use Luna low. Shared-context subtasks stay in DSH.",
4904
+ subagentBackendUnavailable: "Subagent services are unavailable. Update DSH to use this option.",
4905
+ sketchRecovered: "Unsaved sketch recovered",
4906
+ sketchRecoveryFailed: "Recovery checkpoint failed. Save or export your draft.",
4907
+ sketchStorageBlocked: "Draft storage is in use. Close other sketch windows and retry.",
4908
+ sketchDraftLimit: "The 20-draft limit is reached. Export first, or remove an unwanted draft before saving.",
4909
+ sketchStorageLimit: "Draft storage is full. Export first, or remove an unwanted draft before saving.",
4413
4910
  sketchSizeShort: "Size",
4414
4911
  sketchObjectDuplicate: "Duplicate object",
4415
4912
  sketchObjectDelete: "Delete object",
@@ -4823,6 +5320,15 @@ window.__ModuleLoader__.load({
4823
5320
  //#endregion
4824
5321
  //#region src/client-styles.js
4825
5322
  const STYLE = `
5323
+ .codexSubscriptionAdvancedPreferences{display:flex;flex-direction:column;gap:10px}
5324
+ .codexSubscriptionSearchChoices.codexSubscriptionQuotaModes{display:flex;flex:0 0 auto;gap:0;grid-template-columns:none}
5325
+ .codexSubscriptionSettingsDisclosure>summary{display:flex;align-items:center;gap:10px;min-height:28px;cursor:pointer;list-style:none;font-size:14px;font-weight:500}
5326
+ .codexSubscriptionSettingsDisclosure>summary::-webkit-details-marker{display:none}
5327
+ .codexSubscriptionSettingsDisclosure>summary>.codexSubscriptionPreferenceHint{margin-left:auto;font-weight:400}
5328
+ .codexSubscriptionSettingsDisclosure>summary>svg{flex:none;transition:transform .15s}
5329
+ .codexSubscriptionSettingsDisclosure[open]>summary>svg{transform:rotate(180deg)}
5330
+ .codexSubscriptionSettingsDisclosure>summary:focus-visible{outline:2px solid var(--dsw-alias-border-l3);outline-offset:4px;border-radius:4px}
5331
+ .codexSubscriptionSettingsDisclosureBody{padding-top:8px;margin-top:8px;border-top:1px solid var(--dsw-alias-border-l2)}
4826
5332
  .codexComposerQuota[data-warning=true]{color:var(--dsw-alias-state-error-primary)}
4827
5333
  .codexComposerQuota[data-warning=true] progress{accent-color:var(--dsw-alias-state-error-primary)}
4828
5334
  .codexComposerQuota[data-warning=true] progress::-webkit-progress-value{background:var(--dsw-alias-state-error-primary)}
@@ -4875,20 +5381,9 @@ window.__ModuleLoader__.load({
4875
5381
  .codexSubscriptionContextModelCopy span{font-size:11px;line-height:16px;color:var(--dsw-alias-label-tertiary)}
4876
5382
  .codexSubscriptionContextInput{width:116px}
4877
5383
  .codexSubscriptionSearch{display:flex;flex-direction:column;gap:7px}
4878
- .codexSubscriptionSearchChoices{display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:6px}
4879
- .codexSubscriptionSearchChoice{display:grid;grid-template-columns:14px minmax(0,1fr);align-items:center;column-gap:8px;min-width:0;border:1px solid var(--dsw-alias-border-l2);border-radius:10px;background:var(--dsw-alias-bg-module-platform);color:var(--dsw-alias-label-primary);padding:9px 10px;text-align:left;cursor:pointer}
4880
- .codexSubscriptionSearchChoice:has(input:disabled){cursor:not-allowed;opacity:.5}
4881
- .codexSubscriptionSearchChoice:has(input:checked){border-color:var(--dsw-alias-label-primary);background:var(--dsw-alias-bg-layer-2)}
4882
- .codexSubscriptionSearchChoice:has(input:focus-visible){outline:2px solid var(--dsw-alias-border-l3);outline-offset:2px}
4883
- .codexSubscriptionSearchInput{width:14px;height:14px;margin:0;accent-color:var(--dsw-alias-label-primary);cursor:inherit}
4884
- .codexSubscriptionSearchCopy{display:block;min-width:0;pointer-events:none}
4885
- .codexSubscriptionSearchCopy strong,.codexSubscriptionSearchCopy span{display:block}
4886
- .codexSubscriptionSearchCopy strong{font-size:12px;line-height:18px;font-weight:500;color:var(--dsw-alias-label-secondary)}
4887
- .codexSubscriptionSearchChoice:has(input:checked) strong{color:var(--dsw-alias-label-primary)}
4888
- .codexSubscriptionSearchCopy span{font-size:11px;line-height:16px;color:var(--dsw-alias-label-tertiary)}
4889
5384
  .codexSubscriptionDivider{height:1px;background:var(--dsw-alias-border-l2)}
4890
5385
  .codexSubscriptionQuotaModes[data-saving=true] .codexSubscriptionQuotaMode:has(input:disabled){cursor:wait;opacity:1}
4891
- .codexSubscriptionSearchChoices[data-saving=true] .codexSubscriptionSearchChoice:has(input:disabled){cursor:wait;opacity:1}
5386
+ .codexSubscriptionSearchChoices[data-saving=true] .codexSubscriptionQuotaMode:has(input:disabled){cursor:wait;opacity:1}
4892
5387
  .codexSubscriptionAccountRow,.codexSubscriptionSectionHead{display:flex;align-items:center;justify-content:space-between;gap:12px}
4893
5388
  .codexSubscriptionStatus{display:flex;align-items:center;gap:8px;font-size:14px;line-height:22px;font-weight:500}
4894
5389
  .codexSubscriptionAccounts{display:flex;flex-direction:column;border-top:1px solid var(--dsw-alias-border-l2)}
@@ -6176,6 +6671,7 @@ window.__ModuleLoader__.load({
6176
6671
  let modelRefreshGeneration = 0;
6177
6672
  let modelRefreshStarted = false;
6178
6673
  let disposed = false;
6674
+ let subagentBackendAvailable = false;
6179
6675
  const sameModels = (left, right) => left.length === right.length && left.every((model, index) => JSON.stringify(model) === JSON.stringify(right[index]));
6180
6676
  const nativeSnapshot = () => scope.getSnapshot();
6181
6677
  const read = () => {
@@ -6189,6 +6685,10 @@ window.__ModuleLoader__.load({
6189
6685
  return Object.freeze({
6190
6686
  status: current.status,
6191
6687
  ...capabilities,
6688
+ connectionMode: value?.connectionMode === "websocket" ? "websocket" : "sse",
6689
+ compactionMode: value?.compactionMode === "cloud" ? "cloud" : "dsh",
6690
+ subagentBackend: value?.subagentBackend === "codex" ? "codex" : "dsh",
6691
+ subagentBackendAvailable,
6192
6692
  quickQuotaMode: normalizeQuickQuotaMode(value?.[QUICK_QUOTA_MODE_FIELD], value?.[LEGACY_QUICK_QUOTA_FIELD]),
6193
6693
  searchProvider: normalizeSearchProvider(value?.[SEARCH_PROVIDER_FIELD]),
6194
6694
  speedMode: normalizeSpeedMode(value?.[SPEED_MODE_FIELD]),
@@ -6222,6 +6722,7 @@ window.__ModuleLoader__.load({
6222
6722
  publish();
6223
6723
  });
6224
6724
  const acceptFallback = (value) => {
6725
+ subagentBackendAvailable = value?.subagentBackendAvailable === true;
6225
6726
  if (!modelRefreshStarted) {
6226
6727
  contextModels = Array.isArray(value?.contextModels) ? value.contextModels : [];
6227
6728
  verbosityModels = Array.isArray(value?.verbosityModels) ? value.verbosityModels : [];
@@ -6232,6 +6733,9 @@ window.__ModuleLoader__.load({
6232
6733
  fallback = {
6233
6734
  status: "ready",
6234
6735
  value: {
6736
+ connectionMode: value?.connectionMode === "websocket" ? "websocket" : "sse",
6737
+ compactionMode: value?.compactionMode === "cloud" ? "cloud" : "dsh",
6738
+ subagentBackend: value?.subagentBackend === "codex" ? "codex" : "dsh",
6235
6739
  ...readCapabilitySettings(value),
6236
6740
  [QUICK_QUOTA_MODE_FIELD]: normalizeQuickQuotaMode(value?.[QUICK_QUOTA_MODE_FIELD], value?.[LEGACY_QUICK_QUOTA_FIELD]),
6237
6741
  [SEARCH_PROVIDER_FIELD]: normalizeSearchProvider(value?.[SEARCH_PROVIDER_FIELD]),
@@ -6255,6 +6759,7 @@ window.__ModuleLoader__.load({
6255
6759
  try {
6256
6760
  const value = unwrap(await rpc.call(CHANNEL, "preferences/status", {}));
6257
6761
  if (current !== generation || disposed) return;
6762
+ subagentBackendAvailable = value?.subagentBackendAvailable === true;
6258
6763
  if (nativeSnapshot().status === "ready") {
6259
6764
  if (!modelRefreshStarted) {
6260
6765
  contextModels = Array.isArray(value?.contextModels) ? value.contextModels : [];
@@ -6315,7 +6820,7 @@ window.__ModuleLoader__.load({
6315
6820
  failedPatch = void 0;
6316
6821
  publish();
6317
6822
  try {
6318
- if (nativeSnapshot().status === "ready") {
6823
+ if (nativeSnapshot().status === "ready" && !Object.hasOwn(patch, "subagentBackend")) {
6319
6824
  for (const [field, value] of entries) {
6320
6825
  if (current !== generation) return;
6321
6826
  await scope.set(field, value);
@@ -6641,7 +7146,7 @@ window.__ModuleLoader__.load({
6641
7146
  }
6642
7147
  //#endregion
6643
7148
  //#region src/version.js
6644
- const PACKAGE_VERSION = "2.1.0";
7149
+ const PACKAGE_VERSION = "2.1.1-beta.2";
6645
7150
  //#endregion
6646
7151
  //#region src/client-recovery.js
6647
7152
  async function recoveryCall(rpc, endpoint, payload = {}, timeoutMs = 1e4) {
@@ -7280,64 +7785,76 @@ window.__ModuleLoader__.load({
7280
7785
  const snapshot = (0, react.useSyncExternalStore)(preference.subscribe, preference.getSnapshot);
7281
7786
  const disabled = snapshot.status !== "ready" || !snapshot.writable || snapshot.saving;
7282
7787
  const active = snapshot.imageGeneration || snapshot.imageEditing;
7283
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
7788
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("section", {
7284
7789
  className: "codexSubscriptionCard codexImageSettings",
7285
7790
  "aria-label": t("imageSettings"),
7286
- children: [
7287
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", { children: t("imageSettings") }),
7288
- Object.keys(IMAGE_SETTING_GROUPS).map((group) => {
7289
- const value = imageGroupValue(snapshot, group);
7290
- return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ImageChoice, {
7291
- label: t(group),
7292
- hint: t(`${group}Hint`),
7293
- value,
7294
- text: t(value === "mixed" ? "imageGroupMixed" : `${group}_${value}`),
7295
- disabled,
7296
- items: ["on", "off"].map((id) => ({
7297
- id,
7298
- label: t(`${group}_${id}`)
7299
- })),
7300
- onSelect: (id) => {
7301
- preference.set(imageGroupPatch(group, id === "on"));
7302
- }
7303
- }, group);
7304
- }),
7305
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
7306
- className: "codexImageDefaultsGroup",
7307
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(ImageChoice, {
7308
- label: t("imageModel"),
7309
- value: snapshot.imageModel,
7310
- text: modelLabel(snapshot.imageModel),
7311
- disabled: disabled || !active,
7312
- items: Object.keys(IMAGE_MODELS).map((id) => ({
7313
- id,
7314
- label: `${modelLabel(id)}${id.includes("2.5") ? ` · ${t("imageExperimental")}` : ""}`
7315
- })),
7316
- onSelect: (imageModel) => {
7317
- preference.set({
7318
- imageModel,
7319
- imageQuality: "auto"
7320
- });
7321
- }
7322
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ImageChoice, {
7323
- label: t("imageQuality"),
7324
- value: snapshot.imageQuality,
7325
- text: t(`imageQuality_${snapshot.imageQuality}`),
7326
- disabled: disabled || !active,
7327
- items: IMAGE_MODELS[snapshot.imageModel].map((id) => ({
7328
- id,
7329
- label: t(`imageQuality_${id}`)
7330
- })),
7331
- onSelect: (imageQuality) => {
7332
- preference.set({ imageQuality });
7333
- }
7334
- })]
7335
- }),
7336
- snapshot.imageModel.includes("2.5") ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
7337
- className: "codexSubscriptionPreferenceHint",
7338
- children: t("imageModelHint")
7339
- }) : null
7340
- ]
7791
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("details", {
7792
+ className: "codexSubscriptionSettingsDisclosure",
7793
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("summary", { children: [
7794
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("imageSettings") }),
7795
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
7796
+ className: "codexSubscriptionPreferenceHint",
7797
+ children: active ? modelLabel(snapshot.imageModel) : t("imageCapability_off")
7798
+ }),
7799
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconChevronDownOutline14, {})
7800
+ ] }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
7801
+ className: "codexSubscriptionSettingsDisclosureBody",
7802
+ children: [
7803
+ Object.keys(IMAGE_SETTING_GROUPS).map((group) => {
7804
+ const value = imageGroupValue(snapshot, group);
7805
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ImageChoice, {
7806
+ label: t(group),
7807
+ hint: t(`${group}Hint`),
7808
+ value,
7809
+ text: t(value === "mixed" ? "imageGroupMixed" : `${group}_${value}`),
7810
+ disabled,
7811
+ items: ["on", "off"].map((id) => ({
7812
+ id,
7813
+ label: t(`${group}_${id}`)
7814
+ })),
7815
+ onSelect: (id) => {
7816
+ preference.set(imageGroupPatch(group, id === "on"));
7817
+ }
7818
+ }, group);
7819
+ }),
7820
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
7821
+ className: "codexImageDefaultsGroup",
7822
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(ImageChoice, {
7823
+ label: t("imageModel"),
7824
+ value: snapshot.imageModel,
7825
+ text: modelLabel(snapshot.imageModel),
7826
+ disabled: disabled || !active,
7827
+ items: Object.keys(IMAGE_MODELS).map((id) => ({
7828
+ id,
7829
+ label: `${modelLabel(id)}${id.includes("2.5") ? ` · ${t("imageExperimental")}` : ""}`
7830
+ })),
7831
+ onSelect: (imageModel) => {
7832
+ preference.set({
7833
+ imageModel,
7834
+ imageQuality: "auto"
7835
+ });
7836
+ }
7837
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ImageChoice, {
7838
+ label: t("imageQuality"),
7839
+ value: snapshot.imageQuality,
7840
+ text: t(`imageQuality_${snapshot.imageQuality}`),
7841
+ disabled: disabled || !active,
7842
+ items: IMAGE_MODELS[snapshot.imageModel].map((id) => ({
7843
+ id,
7844
+ label: t(`imageQuality_${id}`)
7845
+ })),
7846
+ onSelect: (imageQuality) => {
7847
+ preference.set({ imageQuality });
7848
+ }
7849
+ })]
7850
+ }),
7851
+ snapshot.imageModel.includes("2.5") ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
7852
+ className: "codexSubscriptionPreferenceHint",
7853
+ children: t("imageModelHint")
7854
+ }) : null
7855
+ ]
7856
+ })]
7857
+ })
7341
7858
  });
7342
7859
  }
7343
7860
  //#endregion
@@ -7552,10 +8069,9 @@ window.__ModuleLoader__.load({
7552
8069
  function SearchProviderPreference({ preference, t }) {
7553
8070
  const snapshot = usePreferenceSnapshot(preference);
7554
8071
  const writable = snapshot.status === "ready" && snapshot.writable === true;
7555
- const choice = (value, label, hint) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
7556
- className: "codexSubscriptionSearchChoice",
8072
+ const choice = (value, label) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
8073
+ className: "codexSubscriptionQuotaMode",
7557
8074
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
7558
- className: "codexSubscriptionSearchInput",
7559
8075
  type: "radio",
7560
8076
  name: "codex-subscription-search-provider",
7561
8077
  checked: snapshot.searchProvider === value,
@@ -7563,39 +8079,39 @@ window.__ModuleLoader__.load({
7563
8079
  onChange: () => {
7564
8080
  preference.set({ [SEARCH_PROVIDER_FIELD]: value });
7565
8081
  }
7566
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
7567
- className: "codexSubscriptionSearchCopy",
7568
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("strong", { children: label }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: hint })]
7569
- })]
8082
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: label })]
7570
8083
  });
8084
+ const hint = snapshot.searchProvider === "dsh" ? "searchDshHint" : snapshot.searchProvider === "codex" ? "searchCodexHint" : "searchAutoHint";
7571
8085
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
7572
8086
  className: "codexSubscriptionSearch",
7573
- children: [
7574
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
7575
- className: "codexSubscriptionSearchHead",
7576
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", { children: t("searchTitle") }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
7577
- className: "codexSubscriptionSearchScope",
7578
- children: t("searchScope")
8087
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
8088
+ className: "codexSubscriptionPreference",
8089
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
8090
+ className: "codexSubscriptionPreferenceCopy",
8091
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
8092
+ className: "codexSubscriptionPreferenceLabel",
8093
+ children: t("searchTitle")
8094
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
8095
+ className: "codexSubscriptionPreferenceHint",
8096
+ children: t(hint)
7579
8097
  })]
7580
- }),
7581
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
7582
- className: "codexSubscriptionSearchChoices",
8098
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
8099
+ className: "codexSubscriptionSearchChoices codexSubscriptionQuotaModes",
7583
8100
  "data-saving": snapshot.saving || void 0,
7584
8101
  "aria-busy": snapshot.saving || void 0,
7585
8102
  role: "radiogroup",
7586
8103
  "aria-label": t("searchTitle"),
7587
8104
  children: [
7588
- choice(SEARCH_PROVIDER_AUTO, t("searchAuto"), t("searchAutoHint")),
7589
- choice("dsh", t("searchDsh"), t("searchDshHint")),
7590
- choice(SEARCH_PROVIDER_CODEX, t("searchCodex"), t("searchCodexHint"))
8105
+ choice(SEARCH_PROVIDER_AUTO, t("searchAuto")),
8106
+ choice("dsh", "DSH"),
8107
+ choice(SEARCH_PROVIDER_CODEX, "Codex")
7591
8108
  ]
7592
- }),
7593
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CapabilityPreferences, {
7594
- preference,
7595
- t,
7596
- section: "search"
7597
- })
7598
- ]
8109
+ })]
8110
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CapabilityPreferences, {
8111
+ preference,
8112
+ t,
8113
+ section: "search"
8114
+ })]
7599
8115
  });
7600
8116
  }
7601
8117
  function ContextWindowPreference({ preference, t }) {
@@ -7761,16 +8277,137 @@ window.__ModuleLoader__.load({
7761
8277
  function PreferencesCard({ preference, t, section = "display" }) {
7762
8278
  const snapshot = usePreferenceSnapshot(preference);
7763
8279
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
7764
- className: "codexSubscriptionCard codexSubscriptionPreferencesCard",
8280
+ className: section === "advanced" ? "codexSubscriptionAdvancedPreferences" : "codexSubscriptionCard codexSubscriptionPreferencesCard",
7765
8281
  children: [section === "advanced" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
7766
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SearchProviderPreference, {
7767
- preference,
7768
- t
8282
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
8283
+ className: "codexSubscriptionCard codexSubscriptionPreferencesCard",
8284
+ "aria-label": t("advancedModelSearch"),
8285
+ children: [
8286
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", { children: t("advancedModelSearch") }),
8287
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SearchProviderPreference, {
8288
+ preference,
8289
+ t
8290
+ }),
8291
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { className: "codexSubscriptionDivider" }),
8292
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ContextWindowPreference, {
8293
+ preference,
8294
+ t
8295
+ })
8296
+ ]
7769
8297
  }),
7770
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { className: "codexSubscriptionDivider" }),
7771
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ContextWindowPreference, {
7772
- preference,
7773
- t
8298
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("section", {
8299
+ className: "codexSubscriptionCard codexSubscriptionPreferencesCard",
8300
+ "aria-label": t("connectionTitle"),
8301
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
8302
+ className: "codexSubscriptionPreference",
8303
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
8304
+ className: "codexSubscriptionPreferenceCopy",
8305
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
8306
+ className: "codexSubscriptionPreferenceLabel",
8307
+ children: [
8308
+ t("connectionTitle"),
8309
+ " ",
8310
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("small", { children: "Beta" })
8311
+ ]
8312
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
8313
+ className: "codexSubscriptionPreferenceHint",
8314
+ children: t("connectionHint")
8315
+ })]
8316
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
8317
+ className: "codexSubscriptionQuotaModes",
8318
+ role: "radiogroup",
8319
+ "aria-label": t("connectionTitle"),
8320
+ "aria-busy": snapshot.saving || void 0,
8321
+ children: ["sse", "websocket"].map((value) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
8322
+ className: "codexSubscriptionQuotaMode",
8323
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
8324
+ type: "radio",
8325
+ name: "codex-connection-mode",
8326
+ checked: snapshot.connectionMode === value,
8327
+ disabled: !snapshot.writable,
8328
+ onChange: () => {
8329
+ preference.set({ connectionMode: value });
8330
+ }
8331
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: value === "sse" ? "SSE" : "WebSocket" })]
8332
+ }, value))
8333
+ })]
8334
+ })
8335
+ }),
8336
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("section", {
8337
+ className: "codexSubscriptionCard codexSubscriptionPreferencesCard",
8338
+ "aria-label": t("compactionTitle"),
8339
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
8340
+ className: "codexSubscriptionPreference",
8341
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
8342
+ className: "codexSubscriptionPreferenceCopy",
8343
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
8344
+ className: "codexSubscriptionPreferenceLabel",
8345
+ children: [
8346
+ t("compactionTitle"),
8347
+ " ",
8348
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("small", { children: "Beta" })
8349
+ ]
8350
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
8351
+ className: "codexSubscriptionPreferenceHint",
8352
+ children: t("compactionHint")
8353
+ })]
8354
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
8355
+ className: "codexSubscriptionQuotaModes",
8356
+ role: "radiogroup",
8357
+ "aria-label": t("compactionTitle"),
8358
+ "aria-busy": snapshot.saving || void 0,
8359
+ children: ["dsh", "cloud"].map((value) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
8360
+ className: "codexSubscriptionQuotaMode",
8361
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
8362
+ type: "radio",
8363
+ name: "codex-compaction-mode",
8364
+ checked: snapshot.compactionMode === value,
8365
+ disabled: !snapshot.writable,
8366
+ onChange: () => {
8367
+ preference.set({ compactionMode: value });
8368
+ }
8369
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t(`compaction_${value}`) })]
8370
+ }, value))
8371
+ })]
8372
+ })
8373
+ }),
8374
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("section", {
8375
+ className: "codexSubscriptionCard codexSubscriptionPreferencesCard",
8376
+ "aria-label": t("subagentBackendTitle"),
8377
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
8378
+ className: "codexSubscriptionPreference",
8379
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
8380
+ className: "codexSubscriptionPreferenceCopy",
8381
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
8382
+ className: "codexSubscriptionPreferenceLabel",
8383
+ children: [
8384
+ t("subagentBackendTitle"),
8385
+ " ",
8386
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("small", { children: "Beta" })
8387
+ ]
8388
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
8389
+ className: "codexSubscriptionPreferenceHint",
8390
+ children: t(snapshot.subagentBackendAvailable ? "subagentBackendHint" : "subagentBackendUnavailable")
8391
+ })]
8392
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
8393
+ className: "codexSubscriptionQuotaModes",
8394
+ role: "radiogroup",
8395
+ "aria-label": t("subagentBackendTitle"),
8396
+ "aria-busy": snapshot.saving || void 0,
8397
+ children: ["dsh", "codex"].map((value) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
8398
+ className: "codexSubscriptionQuotaMode",
8399
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
8400
+ type: "radio",
8401
+ name: "codex-subagent-backend",
8402
+ checked: snapshot.subagentBackend === value,
8403
+ disabled: !snapshot.writable || !snapshot.subagentBackendAvailable,
8404
+ onChange: () => {
8405
+ preference.set({ subagentBackend: value });
8406
+ }
8407
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t(`subagentBackend_${value}`) })]
8408
+ }, value))
8409
+ })]
8410
+ })
7774
8411
  })
7775
8412
  ] }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(QuickQuotaPreference, {
7776
8413
  preference,