dsh-codex-subscription 2.2.1 → 2.2.3

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
@@ -877,6 +877,8 @@ window.__ModuleLoader__.load({
877
877
  "runtime/install",
878
878
  "runtime/remove",
879
879
  "runtime/cancel",
880
+ "storage/status",
881
+ "storage/clear-forecast",
880
882
  "reset-credit/inspect",
881
883
  "reset-credit/prepare",
882
884
  "reset-credit/consume",
@@ -1294,6 +1296,53 @@ window.__ModuleLoader__.load({
1294
1296
  });
1295
1297
  }
1296
1298
  //#endregion
1299
+ //#region src/sketch-picture-panel.jsx
1300
+ function SketchPicturePanel({ document, disabled, pictureInput, importImage, runFile, change, t }) {
1301
+ const pictures = document.layers.filter((layer) => layer.image);
1302
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("aside", {
1303
+ className: "codexSketchPictures",
1304
+ children: [
1305
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("header", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("strong", { children: t("sketchPictures") }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1306
+ type: "button",
1307
+ disabled,
1308
+ onClick: () => pictureInput.current.click(),
1309
+ children: t("sketchPictureAdd")
1310
+ })] }),
1311
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
1312
+ ref: pictureInput,
1313
+ hidden: true,
1314
+ type: "file",
1315
+ accept: "image/png,image/jpeg,image/webp",
1316
+ onChange: (event) => {
1317
+ const file = event.target.files?.[0];
1318
+ event.target.value = "";
1319
+ if (file) runFile(() => importImage(file));
1320
+ }
1321
+ }),
1322
+ pictures.map((layer) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1323
+ "data-active": layer.id === document.active,
1324
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1325
+ type: "button",
1326
+ disabled,
1327
+ "aria-label": `${t("sketchPictureSelect")} ${layer.name}`,
1328
+ onClick: () => change("select", layer.id),
1329
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("img", {
1330
+ src: layer.image.src,
1331
+ alt: layer.name
1332
+ })
1333
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1334
+ type: "button",
1335
+ disabled,
1336
+ "aria-label": `${t("sketchDeleteDraft")} ${layer.name}`,
1337
+ onClick: () => change(document.layers.length === 1 ? "clear" : "delete", layer.id),
1338
+ children: "×"
1339
+ })]
1340
+ }, layer.id)),
1341
+ pictures.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("small", { children: t("sketchPicturesEmpty") }) : null
1342
+ ]
1343
+ });
1344
+ }
1345
+ //#endregion
1297
1346
  //#region src/sketch-tool-picker.jsx
1298
1347
  function SketchToolPicker({ t, disabled, tool, brush, chooseBrush, chooseTool, shapesOpen, setShapesOpen }) {
1299
1348
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
@@ -1382,1460 +1431,1641 @@ window.__ModuleLoader__.load({
1382
1431
  });
1383
1432
  }
1384
1433
  //#endregion
1385
- //#region src/sketch-objects.js
1386
- const objectId = (stroke, index) => stroke.id ?? `legacy-${index}`;
1387
- const identifyObjects = (doc) => ({
1388
- ...doc,
1389
- layers: doc.layers.map((layer) => ({
1390
- ...layer,
1391
- strokes: layer.strokes.map((s, i) => s.id ? s : {
1392
- ...s,
1393
- id: objectId(s, i)
1394
- })
1395
- }))
1396
- });
1397
- function objectBounds(stroke) {
1398
- const xs = stroke.points.map((p) => p.x), ys = stroke.points.map((p) => p.y);
1399
- return {
1400
- x: Math.min(...xs),
1401
- y: Math.min(...ys),
1402
- width: Math.max(...xs) - Math.min(...xs),
1403
- height: Math.max(...ys) - Math.min(...ys)
1404
- };
1434
+ //#region src/sketch-interactions.js
1435
+ function useSketchDismiss(open, close, host, selectors) {
1436
+ const latest = (0, react.useRef)(close);
1437
+ latest.current = close;
1438
+ (0, react.useEffect)(() => {
1439
+ if (!open) return;
1440
+ const dialog = host.current?.closest("dialog") ?? host.current;
1441
+ if (!dialog) return;
1442
+ const pointer = (event) => {
1443
+ if (selectors.some((selector) => event.target.closest?.(selector))) return;
1444
+ latest.current(false);
1445
+ if (event.target.matches?.("canvas")) {
1446
+ event.preventDefault();
1447
+ event.stopPropagation();
1448
+ event.target.focus({ preventScroll: true });
1449
+ }
1450
+ };
1451
+ const key = (event) => {
1452
+ if (event.key !== "Escape" || event.target.closest?.("dialog") !== dialog) return;
1453
+ event.preventDefault();
1454
+ event.stopPropagation();
1455
+ latest.current(false);
1456
+ dialog.querySelector("canvas")?.focus({ preventScroll: true });
1457
+ };
1458
+ const hidden = () => latest.current(false);
1459
+ document.addEventListener("pointerdown", pointer, true);
1460
+ document.addEventListener("keydown", key, true);
1461
+ dialog.addEventListener("close", hidden);
1462
+ return () => {
1463
+ document.removeEventListener("pointerdown", pointer, true);
1464
+ document.removeEventListener("keydown", key, true);
1465
+ dialog.removeEventListener("close", hidden);
1466
+ };
1467
+ }, [
1468
+ open,
1469
+ host,
1470
+ selectors.join("|")
1471
+ ]);
1405
1472
  }
1406
- function transformObject(stroke, { dx = 0, dy = 0, scaleX = 1, scaleY = 1 }) {
1407
- if (![
1408
- dx,
1409
- dy,
1410
- scaleX,
1411
- scaleY
1412
- ].every(Number.isFinite) || scaleX <= 0 || scaleY <= 0) throw Error("Invalid object transform");
1413
- const box = objectBounds(stroke);
1414
- const points = stroke.points.map((p) => ({
1415
- x: box.x + (p.x - box.x) * scaleX + dx,
1416
- y: box.y + (p.y - box.y) * scaleY + dy
1417
- }));
1418
- if (points.some((p) => p.x < 0 || p.x > 1 || p.y < 0 || p.y > 1)) throw Error("Object would leave the canvas");
1473
+ function useSketchCursor(canvas, ring, width, brush, zoom, hidden) {
1474
+ const last = (0, react.useRef)(null), heldPressure = (0, react.useRef)(1);
1475
+ const update = (event, bounds) => {
1476
+ if (event) last.current = event;
1477
+ const pointer = last.current, node = canvas.current, cursor = ring.current;
1478
+ if (!pointer || !node || !cursor) return;
1479
+ const rect = bounds ?? node.getBoundingClientRect();
1480
+ if (hidden || pointer.pointerType === "touch" || pointer.clientX < rect.left || pointer.clientX > rect.right || pointer.clientY < rect.top || pointer.clientY > rect.bottom) {
1481
+ cursor.hidden = true;
1482
+ return;
1483
+ }
1484
+ const diameter = width * (node.hasPointerCapture(pointer.pointerId) && pointer.pointerType === "pen" ? heldPressure.current : 1) * rect.width / node.width;
1485
+ cursor.hidden = false;
1486
+ cursor.style.width = `${diameter}px`;
1487
+ cursor.style.height = `${diameter}px`;
1488
+ cursor.style.transform = `translate(${pointer.clientX - diameter / 2}px,${pointer.clientY - diameter / 2}px)`;
1489
+ };
1490
+ (0, react.useEffect)(() => {
1491
+ update();
1492
+ const observer = new ResizeObserver(() => update());
1493
+ if (canvas.current) observer.observe(canvas.current);
1494
+ return () => observer.disconnect();
1495
+ }, [
1496
+ width,
1497
+ brush,
1498
+ zoom,
1499
+ hidden
1500
+ ]);
1419
1501
  return {
1420
- ...stroke,
1421
- points
1502
+ down: (event) => {
1503
+ heldPressure.current = event.pointerType === "pen" ? Math.max(.2, event.pressure) : 1;
1504
+ },
1505
+ move: (event, bounds) => update({
1506
+ clientX: event.clientX,
1507
+ clientY: event.clientY,
1508
+ pointerId: event.pointerId,
1509
+ pointerType: event.pointerType,
1510
+ pressure: event.pressure
1511
+ }, bounds),
1512
+ leave: () => {
1513
+ last.current = null;
1514
+ if (ring.current) ring.current.hidden = true;
1515
+ }
1422
1516
  };
1423
1517
  }
1424
- function sketchObjectSummary(doc) {
1425
- return doc.layers.flatMap((layer) => layer.strokes.map((stroke, i) => ({
1426
- layer: layer.id,
1427
- id: objectId(stroke, i),
1428
- shape: stroke.shape,
1429
- color: stroke.color,
1430
- bounds: objectBounds(stroke),
1431
- ...stroke.text ? { text: stroke.text } : {}
1432
- })));
1433
- }
1434
1518
  //#endregion
1435
- //#region src/sketch-input.js
1436
- function snapLine(start, end) {
1437
- const angle = Math.round(Math.atan2(end.y - start.y, end.x - start.x) / (Math.PI / 4)) * Math.PI / 4;
1438
- const length = Math.hypot(end.x - start.x, end.y - start.y);
1439
- return {
1440
- x: start.x + Math.cos(angle) * length,
1441
- y: start.y + Math.sin(angle) * length
1442
- };
1519
+ //#region src/sketch-shortcuts.js
1520
+ function sketchShortcutAction(keys, key) {
1521
+ key = key.toLowerCase();
1522
+ return Object.entries(keys).find(([, value]) => value === key)?.[0] ?? {
1523
+ v: "select",
1524
+ t: "text",
1525
+ "+": "zoomIn"
1526
+ }[key];
1443
1527
  }
1444
- function smoothStrokePoints(points, strength = 0) {
1445
- if (!strength || points.length < 3) return points;
1446
- const radius = Math.max(1, Math.round(strength / 25)), amount = Math.min(1, strength / 75);
1447
- return points.map((point, i) => {
1448
- if (i === 0 || i === points.length - 1) return point;
1449
- let x = 0, y = 0, weight = 0;
1450
- for (let j = Math.max(0, i - radius); j <= Math.min(points.length - 1, i + radius); j++) {
1451
- const w = radius + 1 - Math.abs(j - i);
1452
- x += points[j].x * w;
1453
- y += points[j].y * w;
1454
- weight += w;
1528
+ //#endregion
1529
+ //#region src/sketch-view.jsx
1530
+ const DEFAULT_KEYS = {
1531
+ pen: "b",
1532
+ eraser: "e",
1533
+ line: "l",
1534
+ rectangle: "r",
1535
+ circle: "o",
1536
+ pan: " ",
1537
+ zoomIn: "=",
1538
+ zoomOut: "-",
1539
+ fit: "0"
1540
+ };
1541
+ function useSketchView(canvas, open) {
1542
+ const [view, setView] = (0, react.useState)({
1543
+ scale: 1,
1544
+ x: 0,
1545
+ y: 0
1546
+ }), [keys, setKeys] = (0, react.useState)(() => {
1547
+ try {
1548
+ return {
1549
+ ...DEFAULT_KEYS,
1550
+ ...JSON.parse(localStorage.getItem("codex-sketch-keys"))
1551
+ };
1552
+ } catch {
1553
+ return DEFAULT_KEYS;
1455
1554
  }
1456
- return {
1457
- ...point,
1458
- x: point.x + (x / weight - point.x) * amount,
1459
- y: point.y + (y / weight - point.y) * amount
1460
- };
1461
1555
  });
1462
- }
1463
- //#endregion
1464
- //#region src/sketch-gesture.js
1465
- function updateSketchGesture(doc, gesture, samples, rect, width, shiftKey = false) {
1466
- if (!samples.length) return doc;
1467
- if (gesture.object) {
1468
- const point = sketchPoint(samples.at(-1).clientX, samples.at(-1).clientY, rect);
1469
- if (!point) return doc;
1470
- const box = objectBounds(gesture.object);
1471
- const stroke = gesture.handle === "end" ? {
1472
- ...gesture.object,
1473
- points: [gesture.object.points[0], point]
1474
- } : gesture.handle === "size" ? transformObject(gesture.object, {
1475
- scaleX: Math.max(.001, point.x - box.x) / Math.max(.001, box.width),
1476
- scaleY: Math.max(.001, point.y - box.y) / Math.max(.001, box.height)
1477
- }) : transformObject(gesture.object, {
1478
- dx: Math.max(-box.x, Math.min(1 - box.x - box.width, point.x - gesture.start.x)),
1479
- dy: Math.max(-box.y, Math.min(1 - box.y - box.height, point.y - gesture.start.y))
1480
- });
1481
- doc = {
1482
- ...doc,
1483
- layers: doc.layers.map((l) => l.id === gesture.layer ? {
1484
- ...l,
1485
- strokes: l.strokes.map((s) => s.id === gesture.object.id ? stroke : s)
1486
- } : l)
1556
+ const [shortcuts, setShortcuts] = (0, react.useState)(() => {
1557
+ try {
1558
+ return localStorage.getItem("codex-sketch-shortcuts") !== "off";
1559
+ } catch {
1560
+ return true;
1561
+ }
1562
+ }), [space, setSpace] = (0, react.useState)(false);
1563
+ const drag = (0, react.useRef)(null), viewRef = (0, react.useRef)(view);
1564
+ viewRef.current = view;
1565
+ const zoom = (factor) => setView((v) => ({
1566
+ ...v,
1567
+ scale: Math.max(.25, Math.min(8, v.scale * factor))
1568
+ }));
1569
+ const reset = () => setView({
1570
+ scale: 1,
1571
+ x: 0,
1572
+ y: 0
1573
+ });
1574
+ (0, react.useEffect)(() => {
1575
+ const node = canvas.current;
1576
+ if (!node || !open) return;
1577
+ const wheel = (e) => {
1578
+ if (!shortcuts || !e.altKey) return;
1579
+ e.preventDefault();
1580
+ zoom(e.deltaY < 0 ? 1.1 : 1 / 1.1);
1487
1581
  };
1488
- return doc;
1489
- }
1490
- for (const sample of samples) {
1491
- let point = sketchPoint(sample.clientX, sample.clientY, rect);
1492
- if (!point) continue;
1493
- const layer = doc.layers.find((layer) => layer.id === gesture.layer);
1494
- if (!layer) return doc;
1495
- if (gesture.eraseStroke) {
1496
- const previous = gesture.last ?? point;
1497
- 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))));
1498
- layer.strokes = layer.strokes.filter((stroke) => {
1499
- for (let i = 1; i <= steps; i++) if (strokeHit(stroke, {
1500
- x: previous.x + (point.x - previous.x) * i / steps,
1501
- y: previous.y + (point.y - previous.y) * i / steps
1502
- }, width / 2, doc.width, doc.height)) return false;
1582
+ node.addEventListener("wheel", wheel, { passive: false });
1583
+ return () => node.removeEventListener("wheel", wheel);
1584
+ }, [open, shortcuts]);
1585
+ (0, react.useEffect)(() => {
1586
+ const stop = () => {
1587
+ drag.current = null;
1588
+ setSpace(false);
1589
+ };
1590
+ window.addEventListener("blur", stop);
1591
+ return () => window.removeEventListener("blur", stop);
1592
+ }, []);
1593
+ (0, react.useEffect)(() => {
1594
+ if (!open || !shortcuts) {
1595
+ setSpace(false);
1596
+ drag.current = null;
1597
+ }
1598
+ }, [open, shortcuts]);
1599
+ const setKey = (action, key) => {
1600
+ key = key.toLowerCase();
1601
+ if (!key || Object.entries(keys).some(([a, k]) => a !== action && k === key) || ["[", "]"].includes(key)) return;
1602
+ const next = {
1603
+ ...keys,
1604
+ [action]: key
1605
+ };
1606
+ setKeys(next);
1607
+ try {
1608
+ localStorage.setItem("codex-sketch-keys", JSON.stringify(next));
1609
+ } catch {}
1610
+ };
1611
+ return {
1612
+ view,
1613
+ keys,
1614
+ shortcuts,
1615
+ space,
1616
+ zoom,
1617
+ reset,
1618
+ setKey,
1619
+ toggle: () => setShortcuts((v) => {
1620
+ try {
1621
+ localStorage.setItem("codex-sketch-shortcuts", v ? "off" : "on");
1622
+ } catch {}
1623
+ return !v;
1624
+ }),
1625
+ keyDown: (e) => {
1626
+ if (!shortcuts || e.ctrlKey || e.metaKey || e.altKey) return false;
1627
+ const action = sketchShortcutAction(keys, e.key);
1628
+ if (action === "pan") {
1629
+ e.preventDefault();
1630
+ setSpace(true);
1503
1631
  return true;
1504
- });
1505
- } else {
1506
- const stroke = layer.strokes.at(-1);
1507
- if (!stroke) return doc;
1632
+ }
1508
1633
  if ([
1509
- "line",
1510
- "arrow",
1511
- "rectangle",
1512
- "circle"
1513
- ].includes(stroke.shape)) {
1514
- if (stroke.shape === "line" && shiftKey) {
1515
- const w = doc.width ?? 1024, h = doc.height ?? 1024, a = stroke.points[0];
1516
- const snapped = snapLine({
1517
- x: a.x * w,
1518
- y: a.y * h
1519
- }, {
1520
- x: point.x * w,
1521
- y: point.y * h
1522
- });
1523
- point = {
1524
- x: snapped.x / w,
1525
- y: snapped.y / h
1526
- };
1527
- }
1528
- stroke.points = [stroke.points[0], point];
1529
- } else {
1530
- const last = stroke.points.at(-1);
1531
- if (Math.hypot(last.x - point.x, last.y - point.y) < 1e-4) continue;
1532
- if (stroke.points.length >= 2e3) stroke.points = stroke.points.filter((_, i) => i % 2 === 0);
1533
- stroke.points.push(point);
1634
+ "zoomIn",
1635
+ "zoomOut",
1636
+ "fit"
1637
+ ].includes(action)) {
1638
+ e.preventDefault();
1639
+ if (action === "fit") reset();
1640
+ else zoom(action === "zoomOut" ? 1 / 1.2 : 1.2);
1641
+ return true;
1534
1642
  }
1643
+ return false;
1644
+ },
1645
+ keyUp: (e) => {
1646
+ if (e.key.toLowerCase() === keys.pan) setSpace(false);
1647
+ },
1648
+ down: (e) => {
1649
+ if (e.button !== 1 && !space) return false;
1650
+ e.preventDefault();
1651
+ drag.current = {
1652
+ id: e.pointerId,
1653
+ x: e.clientX,
1654
+ y: e.clientY,
1655
+ view: viewRef.current
1656
+ };
1657
+ canvas.current.setPointerCapture(e.pointerId);
1658
+ return true;
1659
+ },
1660
+ move: (e) => {
1661
+ const d = drag.current;
1662
+ if (!d || d.id !== e.pointerId) return false;
1663
+ setView({
1664
+ ...d.view,
1665
+ x: d.view.x + e.clientX - d.x,
1666
+ y: d.view.y + e.clientY - d.y
1667
+ });
1668
+ return true;
1669
+ },
1670
+ end: (e) => {
1671
+ if (drag.current?.id !== e.pointerId) return false;
1672
+ drag.current = null;
1673
+ if (canvas.current.hasPointerCapture(e.pointerId)) canvas.current.releasePointerCapture(e.pointerId);
1674
+ return true;
1535
1675
  }
1536
- gesture.last = point;
1537
- }
1538
- return doc;
1676
+ };
1539
1677
  }
1540
- //#endregion
1541
- //#region src/sketch-drafts.js
1542
- const DATABASE = "dsh-codex-sketches-v1";
1543
- const MAX_STORAGE = 32 * 1024 * 1024;
1544
- const metadata = (kind, row) => ({
1545
- key: `${kind}:${row.id}`,
1546
- kind,
1547
- id: row.id,
1548
- name: row.name,
1549
- updated: row.updated,
1550
- size: JSON.stringify(row).length
1551
- });
1552
- async function sketchDrafts(action, value, recoverySession) {
1553
- const db = await new Promise((resolve, reject) => {
1554
- let blocked = false;
1555
- const request = indexedDB.open(DATABASE, 2);
1556
- request.onblocked = () => {
1557
- blocked = true;
1558
- reject(Object.assign(Error("Close other sketch windows and retry"), { code: "SKETCH_STORAGE_BLOCKED" }));
1559
- };
1560
- request.onupgradeneeded = () => {
1561
- if (blocked) {
1562
- request.transaction.abort();
1563
- return;
1564
- }
1565
- const db = request.result, tx = request.transaction;
1566
- if (!db.objectStoreNames.contains("drafts")) db.createObjectStore("drafts", { keyPath: "id" });
1567
- const meta = db.createObjectStore("metadata", { keyPath: "key" });
1568
- db.createObjectStore("recovery", { keyPath: "id" });
1569
- const cursor = tx.objectStore("drafts").openCursor();
1570
- cursor.onsuccess = () => {
1571
- const row = cursor.result;
1572
- if (row) {
1573
- meta.put(metadata("drafts", row.value));
1574
- row.continue();
1678
+ function SketchViewControls({ navigation, t }) {
1679
+ const [open, setOpen] = (0, react.useState)(false), [placement, setPlacement] = (0, react.useState)(null);
1680
+ const host = (0, react.useRef)(null), trigger = (0, react.useRef)(null);
1681
+ const close = () => {
1682
+ setOpen(false);
1683
+ trigger.current?.focus({ preventScroll: true });
1684
+ };
1685
+ useSketchDismiss(open, setOpen, host, [".codexSketchViewControls", ".codexSketchKeyPanel"]);
1686
+ (0, react.useLayoutEffect)(() => {
1687
+ if (!open) return;
1688
+ const dialog = host.current.closest("dialog");
1689
+ const place = () => {
1690
+ const box = dialog.getBoundingClientRect(), anchor = trigger.current.getBoundingClientRect();
1691
+ const width = Math.min(360, box.width - 24);
1692
+ setPlacement({
1693
+ dialog,
1694
+ style: {
1695
+ width,
1696
+ left: Math.max(12, Math.min(anchor.left - box.left, box.width - width - 12)),
1697
+ bottom: box.bottom - anchor.top + 8,
1698
+ maxHeight: Math.max(80, anchor.top - box.top - 24)
1575
1699
  }
1576
- };
1700
+ });
1577
1701
  };
1578
- request.onsuccess = () => {
1579
- if (blocked) {
1580
- request.result.close();
1581
- return;
1582
- }
1583
- request.result.onversionchange = () => request.result.close();
1584
- resolve(request.result);
1702
+ place();
1703
+ const observer = new ResizeObserver(place);
1704
+ observer.observe(dialog);
1705
+ observer.observe(host.current);
1706
+ window.addEventListener("resize", place);
1707
+ return () => {
1708
+ observer.disconnect();
1709
+ window.removeEventListener("resize", place);
1585
1710
  };
1586
- request.onerror = () => reject(request.error);
1587
- });
1588
- try {
1589
- return await new Promise((resolve, reject) => {
1590
- const write = [
1591
- "save",
1592
- "delete",
1593
- "checkpoint",
1594
- "clearRecovery"
1595
- ].includes(action);
1596
- const tx = db.transaction([
1597
- "drafts",
1598
- "metadata",
1599
- "recovery"
1600
- ], write ? "readwrite" : "readonly");
1601
- const meta = tx.objectStore("metadata"), kind = [
1602
- "checkpoint",
1603
- "recover",
1604
- "clearRecovery"
1605
- ].includes(action) ? "recovery" : "drafts", store = tx.objectStore(kind);
1606
- let result, failure;
1607
- tx.oncomplete = () => resolve(result);
1608
- tx.onerror = () => reject(tx.error);
1609
- tx.onabort = () => reject(failure ?? tx.error ?? Error("Draft transaction aborted"));
1610
- if (action === "get" || action === "recover") {
1611
- const req = store.get(value);
1612
- req.onsuccess = () => {
1613
- result = req.result;
1614
- };
1615
- return;
1616
- }
1617
- if (action === "delete" || action === "clearRecovery") {
1618
- store.delete(value);
1619
- meta.delete(`${kind}:${value}`);
1620
- return;
1621
- }
1622
- if (![
1623
- "list",
1624
- "save",
1625
- "checkpoint"
1626
- ].includes(action)) {
1627
- tx.abort();
1628
- return;
1629
- }
1630
- const request = meta.getAll();
1631
- request.onsuccess = () => {
1632
- const rows = request.result;
1633
- if (action === "list") {
1634
- result = rows.filter((row) => row.kind === "drafts").sort((a, b) => b.updated - a.updated);
1635
- return;
1636
- }
1637
- const next = metadata(kind, value), others = rows.filter((row) => row.key !== next.key && !(action === "save" && recoverySession && row.key === `recovery:${recoverySession}`));
1638
- 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;
1639
- if (code) {
1640
- failure = Object.assign(Error("Draft storage limit reached"), { code });
1641
- tx.abort();
1642
- return;
1643
- }
1644
- store.put(value);
1645
- meta.put(next);
1646
- if (action === "save" && recoverySession) {
1647
- tx.objectStore("recovery").delete(recoverySession);
1648
- meta.delete(`recovery:${recoverySession}`);
1649
- }
1650
- result = value;
1651
- };
1652
- });
1653
- } finally {
1654
- db.close();
1655
- }
1656
- }
1657
- async function decodeSketchImages(doc, images) {
1658
- for (const layer of doc.layers) {
1659
- const src = layer.image?.src;
1660
- if (!src || images.has(src)) continue;
1661
- if (!src.startsWith("data:image/png;base64,") || src.length > 8 * 1024 * 1024) throw Error("Invalid image");
1662
- const image = new Image();
1663
- image.src = src;
1664
- await image.decode();
1665
- images.set(src, image);
1666
- }
1667
- }
1668
- async function importSketchImage(file) {
1669
- if (![
1670
- "image/png",
1671
- "image/jpeg",
1672
- "image/webp"
1673
- ].includes(file.type) || file.size > 20 * 1024 * 1024) throw Error("Image must be PNG, JPEG or WebP under 20 MB");
1674
- const bitmap = await createImageBitmap(file);
1675
- try {
1676
- if (bitmap.width * bitmap.height > 32 * 1024 * 1024) throw Error("Image too large");
1677
- const scale = Math.min(1, 1024 / Math.max(bitmap.width, bitmap.height));
1678
- const canvas = document.createElement("canvas");
1679
- canvas.width = Math.max(1, Math.round(bitmap.width * scale));
1680
- canvas.height = Math.max(1, Math.round(bitmap.height * scale));
1681
- canvas.getContext("2d").drawImage(bitmap, 0, 0, canvas.width, canvas.height);
1682
- return {
1683
- src: canvas.toDataURL("image/png"),
1684
- width: canvas.width,
1685
- height: canvas.height
1686
- };
1687
- } finally {
1688
- bitmap.close();
1689
- }
1711
+ }, [open]);
1712
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1713
+ ref: host,
1714
+ className: "codexSketchViewControls",
1715
+ children: [
1716
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1717
+ type: "button",
1718
+ "aria-label": t("sketchZoomOut"),
1719
+ onClick: () => navigation.zoom(1 / 1.2),
1720
+ children: "−"
1721
+ }),
1722
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
1723
+ type: "button",
1724
+ title: t("sketchFit"),
1725
+ onClick: navigation.reset,
1726
+ children: [Math.round(navigation.view.scale * 100), "%"]
1727
+ }),
1728
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1729
+ type: "button",
1730
+ "aria-label": t("sketchZoomIn"),
1731
+ onClick: () => navigation.zoom(1.2),
1732
+ children: "+"
1733
+ }),
1734
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1735
+ ref: trigger,
1736
+ type: "button",
1737
+ "aria-expanded": open,
1738
+ onClick: () => setOpen(!open),
1739
+ children: t("sketchKeys")
1740
+ }),
1741
+ open && placement ? (0, react_dom.createPortal)(/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
1742
+ className: "codexSketchKeyPanel",
1743
+ "aria-label": t("sketchKeys"),
1744
+ style: placement.style,
1745
+ onKeyDown: (e) => {
1746
+ if (e.key === "Escape") {
1747
+ e.preventDefault();
1748
+ e.stopPropagation();
1749
+ close();
1750
+ }
1751
+ },
1752
+ children: [
1753
+ /* @__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", {
1754
+ type: "button",
1755
+ "aria-label": t("sketchFileClose"),
1756
+ onClick: close,
1757
+ children: "×"
1758
+ })] }),
1759
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
1760
+ className: "codexSketchKeysEnabled",
1761
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("sketchKeysEnabled") }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
1762
+ type: "checkbox",
1763
+ checked: navigation.shortcuts,
1764
+ onChange: navigation.toggle
1765
+ })]
1766
+ }),
1767
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", { children: t("sketchNavigationHint") }),
1768
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1769
+ className: "codexSketchKeyGrid",
1770
+ 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", {
1771
+ "aria-label": t(`sketchKey_${action}`),
1772
+ value: key === " " ? "Space" : key,
1773
+ readOnly: true,
1774
+ onKeyDown: (e) => {
1775
+ if (e.key === "Tab" || e.key === "Escape") return;
1776
+ e.preventDefault();
1777
+ e.stopPropagation();
1778
+ if (e.key.length === 1 && !e.ctrlKey && !e.metaKey && !e.altKey) navigation.setKey(action, e.key);
1779
+ }
1780
+ })] }, action))
1781
+ }),
1782
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("small", { children: t("sketchKeyHint") })
1783
+ ]
1784
+ }), placement.dialog) : null
1785
+ ]
1786
+ });
1690
1787
  }
1691
1788
  //#endregion
1692
- //#region src/sketch-presets.js
1693
- const SKETCH_PRESETS = [
1694
- "triangle",
1695
- "diamond",
1696
- "star"
1789
+ //#region src/sketch-controls.jsx
1790
+ const PALETTE = [
1791
+ "#18181b",
1792
+ "#929398",
1793
+ "#ff3936",
1794
+ "#ff9500",
1795
+ "#ffcc00",
1796
+ "#34c759",
1797
+ "#0088ff"
1697
1798
  ];
1698
- function expandSketchPreset(command) {
1699
- if (!SKETCH_PRESETS.includes(command.shape)) return command;
1700
- const points = command.points;
1701
- 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");
1702
- const [a, b] = points, x = Math.min(a.x, b.x), y = Math.min(a.y, b.y);
1703
- const w = Math.abs(a.x - b.x), h = Math.abs(a.y - b.y);
1704
- if (!w || !h) throw Error("Shape preset requires a non-empty box");
1705
- const vertices = command.shape === "triangle" ? [
1706
- [.5, 0],
1707
- [1, 1],
1708
- [0, 1]
1709
- ] : command.shape === "diamond" ? [
1710
- [.5, 0],
1711
- [1, .5],
1712
- [.5, 1],
1713
- [0, .5]
1714
- ] : Array.from({ length: 10 }, (_, i) => {
1715
- const angle = i * Math.PI / 5 - Math.PI / 2, radius = i % 2 ? .22 : .5;
1716
- return [.5 + Math.cos(angle) * radius, .5 + Math.sin(angle) * radius];
1799
+ function SketchControls({ t, navigation, picturesOpen, setPicturesOpen, disabled, tool, brush, chooseBrush, chooseTool, shapesOpen, setShapesOpen, fillShape, setFillShape, selected, editObject, selection, setTextEdit, stability, setStability, eraser, setEraser, color, pickColor }) {
1800
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1801
+ className: "codexSketchControls",
1802
+ children: [
1803
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1804
+ type: "button",
1805
+ className: "codexSketchPicturesToggle",
1806
+ "aria-expanded": picturesOpen,
1807
+ onClick: () => setPicturesOpen((value) => !value),
1808
+ children: t("sketchPictures")
1809
+ }),
1810
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SketchViewControls, {
1811
+ navigation,
1812
+ t
1813
+ }),
1814
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SketchToolPicker, {
1815
+ t,
1816
+ disabled,
1817
+ tool,
1818
+ brush,
1819
+ chooseBrush,
1820
+ chooseTool,
1821
+ shapesOpen,
1822
+ setShapesOpen
1823
+ }),
1824
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1825
+ className: "codexLayerBrush",
1826
+ children: [
1827
+ ["rectangle", "circle"].includes(tool) ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
1828
+ type: "checkbox",
1829
+ disabled,
1830
+ checked: fillShape,
1831
+ onChange: (event) => setFillShape(event.target.checked)
1832
+ }), t("sketchFill")] }) : null,
1833
+ selected ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1834
+ className: "codexSketchObjectActions",
1835
+ children: [
1836
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1837
+ disabled,
1838
+ onClick: () => editObject({}, "duplicate"),
1839
+ children: t("sketchObjectDuplicate")
1840
+ }),
1841
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1842
+ disabled,
1843
+ onClick: () => editObject({}, "delete"),
1844
+ children: t("sketchObjectDelete")
1845
+ }),
1846
+ selected.shape === "text" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1847
+ disabled,
1848
+ onClick: () => setTextEdit({
1849
+ selection,
1850
+ value: selected.text
1851
+ }),
1852
+ children: t("sketchText")
1853
+ }) : null
1854
+ ]
1855
+ }) : null,
1856
+ tool === "pen" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
1857
+ className: "codexSketchStability",
1858
+ children: [t("sketchStability"), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("select", {
1859
+ "aria-label": t("sketchStability"),
1860
+ value: stability,
1861
+ disabled,
1862
+ onChange: (event) => setStability(Number(event.target.value)),
1863
+ children: [
1864
+ 0,
1865
+ 25,
1866
+ 50,
1867
+ 75
1868
+ ].map((value) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
1869
+ value,
1870
+ children: t(`sketchStability${value}`)
1871
+ }, value))
1872
+ })]
1873
+ }) : null,
1874
+ tool === "eraser" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1875
+ className: "codexSketchSegment",
1876
+ role: "group",
1877
+ "aria-label": t("sketchEraserMode"),
1878
+ children: ["pixel", "stroke"].map((value) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1879
+ type: "button",
1880
+ "aria-pressed": eraser === value,
1881
+ disabled,
1882
+ onClick: () => setEraser(value),
1883
+ children: t(`sketchErase_${value}`)
1884
+ }, value))
1885
+ }) : null
1886
+ ]
1887
+ }),
1888
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1889
+ className: "codexSketchPalette",
1890
+ role: "group",
1891
+ "aria-label": t("sketchColor"),
1892
+ children: [PALETTE.map((value) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1893
+ type: "button",
1894
+ className: "codexSketchSwatch",
1895
+ style: { "--swatch": value },
1896
+ "aria-label": `${t("sketchColor")} ${value}`,
1897
+ "aria-pressed": (selected?.color ?? color) === value,
1898
+ disabled,
1899
+ onClick: () => pickColor(value)
1900
+ }, value)), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
1901
+ className: "codexSketchCustom",
1902
+ title: t("sketchColor"),
1903
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { style: { background: color } }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
1904
+ type: "color",
1905
+ "aria-label": t("sketchColor"),
1906
+ value: color,
1907
+ disabled,
1908
+ onChange: (event) => pickColor(event.target.value)
1909
+ })]
1910
+ })]
1911
+ })
1912
+ ]
1717
1913
  });
1914
+ }
1915
+ //#endregion
1916
+ //#region src/sketch-import-confirm.jsx
1917
+ function useSketchImportConfirm(t) {
1918
+ const [pending, setPending] = (0, react.useState)(false);
1919
+ const resolve = (0, react.useRef)(null);
1920
+ const dialog = (0, react.useRef)(null);
1921
+ const finish = (accepted) => {
1922
+ const done = resolve.current;
1923
+ resolve.current = null;
1924
+ setPending(false);
1925
+ done?.(accepted);
1926
+ };
1927
+ (0, react.useEffect)(() => {
1928
+ if (pending) dialog.current?.showModal();
1929
+ else dialog.current?.close();
1930
+ }, [pending]);
1931
+ (0, react.useEffect)(() => () => {
1932
+ resolve.current?.(false);
1933
+ resolve.current = null;
1934
+ }, []);
1718
1935
  return {
1719
- ...command,
1720
- shape: "polygon",
1721
- points: vertices.map(([u, v]) => ({
1722
- x: x + u * w,
1723
- y: y + v * h
1724
- }))
1936
+ confirmPsd: () => new Promise((done) => {
1937
+ resolve.current?.(false);
1938
+ resolve.current = done;
1939
+ setPending(true);
1940
+ }),
1941
+ prompt: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("dialog", {
1942
+ ref: dialog,
1943
+ className: "codexSketchImportConfirm",
1944
+ "aria-label": t("sketchPsdConfirmTitle"),
1945
+ onKeyDown: (event) => event.stopPropagation(),
1946
+ onCancel: (event) => {
1947
+ event.preventDefault();
1948
+ event.stopPropagation();
1949
+ finish(false);
1950
+ },
1951
+ children: [
1952
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", { children: t("sketchPsdConfirmTitle") }),
1953
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", { children: t("sketchPsdConfirmBody") }),
1954
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1955
+ type: "button",
1956
+ autoFocus: true,
1957
+ onClick: () => finish(false),
1958
+ children: t("sketchCancel")
1959
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1960
+ type: "button",
1961
+ onClick: () => finish(true),
1962
+ children: t("sketchImport")
1963
+ })] })
1964
+ ]
1965
+ })
1725
1966
  };
1726
1967
  }
1727
- const SKETCH_COMMAND_HELP = {
1728
- 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.",
1729
- 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.",
1730
- commands: {
1731
- 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.",
1732
- 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},...]}",
1733
- 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.",
1734
- 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.",
1735
- 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.",
1736
- resize: "{op:\"resize\",ratio:\"1:1|4:3|3:4|16:9|9:16\"}"
1737
- },
1738
- limits: {
1739
- strokes: MAX_SKETCH_STROKES,
1740
- pointsPerStroke: MAX_STROKE_POINTS,
1741
- pointsTotal: 2e5,
1742
- commandsPerBatch: 256
1743
- }
1744
- };
1745
- const finite = (value, min, max) => typeof value === "number" && Number.isFinite(value) && value >= min && value <= max;
1746
- function applySketchCommands(source, commands) {
1747
- if (!Array.isArray(commands) || !commands.length || commands.length > 256) throw Error("Expected 1–256 commands");
1748
- let doc = identifyObjects(source);
1749
- for (let command of commands) {
1750
- if (!command || typeof command !== "object") throw Error("Invalid command");
1751
- if (command.op === "stroke") command = expandSketchPreset(command);
1752
- if (command.op === "resize") {
1753
- doc = resizeSketch(doc, command.ratio);
1754
- continue;
1755
- }
1756
- if (command.op === "layer") {
1757
- if (![
1758
- "add",
1759
- "select",
1760
- "rename",
1761
- "visible",
1762
- "duplicate",
1763
- "up",
1764
- "down",
1765
- "delete",
1766
- "clear"
1767
- ].includes(command.action)) throw Error("Unknown layer action");
1768
- if (command.action === "add") {
1769
- const id = command.id ?? doc.nextId, after = command.after ?? doc.active;
1770
- 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");
1771
- const next = changeSketchLayer(doc, "add", after);
1772
- if (next === doc) throw Error("Cannot add layer: check the existing after layer and the 8-layer limit");
1773
- doc = {
1774
- ...next,
1775
- active: id,
1776
- nextId: Math.max(next.nextId, id + 1),
1777
- layers: next.layers.map((l) => l.id === next.active ? {
1778
- ...l,
1779
- id,
1780
- name: String(command.value ?? "").trim().slice(0, 40)
1781
- } : l)
1782
- };
1783
- continue;
1784
- }
1785
- const next = changeSketchLayer(doc, command.action, command.id ?? doc.active, command.value);
1786
- if (next === doc) throw Error("Layer action unavailable; inspect the document first");
1787
- doc = next;
1788
- continue;
1789
- }
1790
- if (command.op === "object") {
1791
- const layer = doc.layers.find((l) => l.id === (command.layer ?? doc.active)), index = layer?.strokes.findIndex((s) => s.id === command.id);
1792
- if (!layer?.visible || index < 0 || index === void 0) throw Error("Object missing or hidden; inspect again");
1793
- const strokes = layer.strokes.slice(), original = strokes[index];
1794
- if (command.action === "delete") strokes.splice(index, 1);
1795
- else if (command.action === "duplicate") strokes.splice(index + 1, 0, {
1796
- ...original,
1797
- id: crypto.randomUUID(),
1798
- points: original.points.map((p) => ({ ...p }))
1799
- });
1800
- else if (command.action === "update") {
1801
- const patch = command.patch ?? {};
1802
- if (Object.keys(patch).some((k) => ![
1803
- "color",
1804
- "width",
1805
- "opacity",
1806
- "fill",
1807
- "text",
1808
- "points"
1809
- ].includes(k))) throw Error("Unsupported object property");
1810
- const changed = command.transform ? transformObject({
1811
- ...original,
1812
- ...patch
1813
- }, command.transform) : {
1814
- ...original,
1815
- ...patch
1816
- };
1817
- strokes[index] = {
1818
- ...applySketchCommands({
1819
- ...doc,
1820
- layers: [{
1821
- ...layer,
1822
- strokes: []
1823
- }]
1824
- }, [{
1825
- ...changed,
1826
- op: "stroke",
1827
- layer: layer.id
1828
- }]).layers[0].strokes[0],
1829
- brush: original.brush ?? "pen",
1830
- ...original.pressure !== void 0 ? { pressure: original.pressure } : {},
1831
- ...original.brushVersion === 2 ? { brushVersion: 2 } : {}
1832
- };
1833
- } else throw Error("Unknown object action");
1834
- doc = {
1835
- ...doc,
1836
- layers: doc.layers.map((l) => l === layer ? {
1837
- ...l,
1838
- strokes
1839
- } : l)
1840
- };
1841
- continue;
1842
- }
1843
- if (command.op !== "stroke") throw Error("Unknown command");
1844
- const shape = command.shape === "ellipse" ? "circle" : command.shape ?? "pen";
1845
- const { color, width = shape === "text" ? 24 : 2, opacity = 1, fill = false } = command;
1846
- let points = command.points;
1847
- if (command.start !== void 0 || command.segments !== void 0) {
1848
- 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");
1849
- points = [command.start, ...command.segments.flatMap((s) => [
1850
- s?.control1,
1851
- s?.control2,
1852
- s?.end
1853
- ])];
1968
+ //#endregion
1969
+ //#region src/sketch-objects.js
1970
+ const objectId = (stroke, index) => stroke.id ?? `legacy-${index}`;
1971
+ const identifyObjects = (doc) => ({
1972
+ ...doc,
1973
+ layers: doc.layers.map((layer) => ({
1974
+ ...layer,
1975
+ strokes: layer.strokes.map((s, i) => s.id ? s : {
1976
+ ...s,
1977
+ id: objectId(s, i)
1978
+ })
1979
+ }))
1980
+ });
1981
+ function objectBounds(stroke) {
1982
+ const xs = stroke.points.map((p) => p.x), ys = stroke.points.map((p) => p.y);
1983
+ return {
1984
+ x: Math.min(...xs),
1985
+ y: Math.min(...ys),
1986
+ width: Math.max(...xs) - Math.min(...xs),
1987
+ height: Math.max(...ys) - Math.min(...ys)
1988
+ };
1989
+ }
1990
+ function transformObject(stroke, { dx = 0, dy = 0, scaleX = 1, scaleY = 1 }) {
1991
+ if (![
1992
+ dx,
1993
+ dy,
1994
+ scaleX,
1995
+ scaleY
1996
+ ].every(Number.isFinite) || scaleX <= 0 || scaleY <= 0) throw Error("Invalid object transform");
1997
+ const box = objectBounds(stroke);
1998
+ const points = stroke.points.map((p) => ({
1999
+ x: box.x + (p.x - box.x) * scaleX + dx,
2000
+ y: box.y + (p.y - box.y) * scaleY + dy
2001
+ }));
2002
+ if (points.some((p) => p.x < 0 || p.x > 1 || p.y < 0 || p.y > 1)) throw Error("Object would leave the canvas");
2003
+ return {
2004
+ ...stroke,
2005
+ points
2006
+ };
2007
+ }
2008
+ function sketchObjectSummary(doc) {
2009
+ return doc.layers.flatMap((layer) => layer.strokes.map((stroke, i) => ({
2010
+ layer: layer.id,
2011
+ id: objectId(stroke, i),
2012
+ shape: stroke.shape,
2013
+ color: stroke.color,
2014
+ bounds: objectBounds(stroke),
2015
+ ...stroke.text ? { text: stroke.text } : {}
2016
+ })));
2017
+ }
2018
+ //#endregion
2019
+ //#region src/sketch-input.js
2020
+ function snapLine(start, end) {
2021
+ const angle = Math.round(Math.atan2(end.y - start.y, end.x - start.x) / (Math.PI / 4)) * Math.PI / 4;
2022
+ const length = Math.hypot(end.x - start.x, end.y - start.y);
2023
+ return {
2024
+ x: start.x + Math.cos(angle) * length,
2025
+ y: start.y + Math.sin(angle) * length
2026
+ };
2027
+ }
2028
+ function smoothStrokePoints(points, strength = 0) {
2029
+ if (!strength || points.length < 3) return points;
2030
+ const radius = Math.max(1, Math.round(strength / 25)), amount = Math.min(1, strength / 75);
2031
+ return points.map((point, i) => {
2032
+ if (i === 0 || i === points.length - 1) return point;
2033
+ let x = 0, y = 0, weight = 0;
2034
+ for (let j = Math.max(0, i - radius); j <= Math.min(points.length - 1, i + radius); j++) {
2035
+ const w = radius + 1 - Math.abs(j - i);
2036
+ x += points[j].x * w;
2037
+ y += points[j].y * w;
2038
+ weight += w;
1854
2039
  }
1855
- if (![
1856
- "pen",
1857
- "line",
1858
- "rectangle",
1859
- "circle",
1860
- "polygon",
1861
- "bezier",
1862
- "arrow",
1863
- "text",
1864
- "eraser"
1865
- ].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");
1866
- if ([
1867
- "line",
1868
- "arrow",
1869
- "text",
1870
- "rectangle",
1871
- "circle"
1872
- ].includes(shape) && points.length !== 2 || shape === "polygon" && points.length < 3) throw Error("Invalid shape points");
1873
- 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.`);
1874
- if (fill && ![
1875
- "rectangle",
1876
- "circle",
1877
- "polygon",
1878
- "bezier"
1879
- ].includes(shape)) throw Error("Fill requires a closed shape");
1880
- const layer = doc.layers.find((layer) => layer.id === (command.layer ?? doc.active));
1881
- if (!layer?.visible) throw Error("Target layer is missing or hidden");
1882
- 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");
1883
- const id = command.id ?? crypto.randomUUID();
1884
- if (typeof id !== "string" || !id.length || id.length > 100 || layer.strokes.some((s) => s.id === id)) throw Error("Invalid or duplicate object id");
1885
- const stroke = {
1886
- id,
1887
- ...shape === "text" ? { text: command.text } : {},
1888
- shape,
1889
- color,
1890
- width,
1891
- opacity,
1892
- fill,
1893
- brush: "pen",
1894
- points: points.map((p) => ({
1895
- x: p.x,
1896
- y: p.y
1897
- }))
2040
+ return {
2041
+ ...point,
2042
+ x: point.x + (x / weight - point.x) * amount,
2043
+ y: point.y + (y / weight - point.y) * amount
1898
2044
  };
2045
+ });
2046
+ }
2047
+ //#endregion
2048
+ //#region src/sketch-gesture.js
2049
+ function updateSketchGesture(doc, gesture, samples, rect, width, shiftKey = false) {
2050
+ if (!samples.length) return doc;
2051
+ if (gesture.object) {
2052
+ const point = sketchPoint(samples.at(-1).clientX, samples.at(-1).clientY, rect);
2053
+ if (!point) return doc;
2054
+ const box = objectBounds(gesture.object);
2055
+ const stroke = gesture.handle === "end" ? {
2056
+ ...gesture.object,
2057
+ points: [gesture.object.points[0], point]
2058
+ } : gesture.handle === "size" ? transformObject(gesture.object, {
2059
+ scaleX: Math.max(.001, point.x - box.x) / Math.max(.001, box.width),
2060
+ scaleY: Math.max(.001, point.y - box.y) / Math.max(.001, box.height)
2061
+ }) : transformObject(gesture.object, {
2062
+ dx: Math.max(-box.x, Math.min(1 - box.x - box.width, point.x - gesture.start.x)),
2063
+ dy: Math.max(-box.y, Math.min(1 - box.y - box.height, point.y - gesture.start.y))
2064
+ });
1899
2065
  doc = {
1900
2066
  ...doc,
1901
- layers: doc.layers.map((item) => item === layer ? {
1902
- ...item,
1903
- strokes: [...item.strokes, stroke]
1904
- } : item)
2067
+ layers: doc.layers.map((l) => l.id === gesture.layer ? {
2068
+ ...l,
2069
+ strokes: l.strokes.map((s) => s.id === gesture.object.id ? stroke : s)
2070
+ } : l)
1905
2071
  };
2072
+ return doc;
2073
+ }
2074
+ for (const sample of samples) {
2075
+ let point = sketchPoint(sample.clientX, sample.clientY, rect);
2076
+ if (!point) continue;
2077
+ const layer = doc.layers.find((layer) => layer.id === gesture.layer);
2078
+ if (!layer) return doc;
2079
+ if (gesture.eraseStroke) {
2080
+ const previous = gesture.last ?? point;
2081
+ 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))));
2082
+ layer.strokes = layer.strokes.filter((stroke) => {
2083
+ for (let i = 1; i <= steps; i++) if (strokeHit(stroke, {
2084
+ x: previous.x + (point.x - previous.x) * i / steps,
2085
+ y: previous.y + (point.y - previous.y) * i / steps
2086
+ }, width / 2, doc.width, doc.height)) return false;
2087
+ return true;
2088
+ });
2089
+ } else {
2090
+ const stroke = layer.strokes.at(-1);
2091
+ if (!stroke) return doc;
2092
+ if ([
2093
+ "line",
2094
+ "arrow",
2095
+ "rectangle",
2096
+ "circle"
2097
+ ].includes(stroke.shape)) {
2098
+ if (stroke.shape === "line" && shiftKey) {
2099
+ const w = doc.width ?? 1024, h = doc.height ?? 1024, a = stroke.points[0];
2100
+ const snapped = snapLine({
2101
+ x: a.x * w,
2102
+ y: a.y * h
2103
+ }, {
2104
+ x: point.x * w,
2105
+ y: point.y * h
2106
+ });
2107
+ point = {
2108
+ x: snapped.x / w,
2109
+ y: snapped.y / h
2110
+ };
2111
+ }
2112
+ stroke.points = [stroke.points[0], point];
2113
+ } else {
2114
+ const last = stroke.points.at(-1);
2115
+ if (Math.hypot(last.x - point.x, last.y - point.y) < 1e-4) continue;
2116
+ if (stroke.points.length >= 2e3) stroke.points = stroke.points.filter((_, i) => i % 2 === 0);
2117
+ stroke.points.push(point);
2118
+ }
2119
+ }
2120
+ gesture.last = point;
1906
2121
  }
1907
- 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");
1908
2122
  return doc;
1909
2123
  }
1910
- function createSketchCommandSession(adapter) {
1911
- const completed = /* @__PURE__ */ new Map();
1912
- let pending = false, cachedCharacters = 0;
1913
- return async (request) => {
1914
- if (!request || typeof request !== "object") throw Error("Invalid sketch request");
1915
- if (!adapter.available()) throw Error("Open the sketch board for this session first");
1916
- const current = adapter.snapshot();
1917
- if (request.action === "inspect") {
1918
- const offset = request.offset ?? 0, objects = adapter.objects?.() ?? [];
1919
- if (!Number.isInteger(offset) || offset < 0) throw Error("offset must be a non-negative integer");
1920
- return {
1921
- ...current,
1922
- protocolVersion: 2,
1923
- objects: request.objectId ? [] : objects.slice(offset, offset + 50),
1924
- objectCount: objects.length,
1925
- ...!request.objectId && offset + 50 < objects.length ? { nextOffset: offset + 50 } : {},
1926
- ...request.objectId ? { object: adapter.object?.(request.objectId, request.layer) } : {},
1927
- recentRequests: [...completed.values()].slice(-8).map((entry) => entry.receipt),
1928
- ...!request.objectId && offset === 0 ? { help: SKETCH_COMMAND_HELP } : {}
1929
- };
1930
- }
1931
- if (request.documentId !== current.documentId) throw Error("Document changed; inspect again");
1932
- if (pending || adapter.busy()) throw Error("Sketch is being edited; retry after it settles");
1933
- if (request.action === "preview") return {
1934
- ...current,
1935
- png: await adapter.preview()
2124
+ //#endregion
2125
+ //#region src/sketch-drafts.js
2126
+ const DATABASE = "dsh-codex-sketches-v1";
2127
+ const MAX_STORAGE = 32 * 1024 * 1024;
2128
+ const metadata = (kind, row) => ({
2129
+ key: `${kind}:${row.id}`,
2130
+ kind,
2131
+ id: row.id,
2132
+ name: row.name,
2133
+ updated: row.updated,
2134
+ size: JSON.stringify(row).length
2135
+ });
2136
+ async function sketchDrafts(action, value, recoverySession) {
2137
+ const db = await new Promise((resolve, reject) => {
2138
+ let blocked = false;
2139
+ const request = indexedDB.open(DATABASE, 2);
2140
+ request.onblocked = () => {
2141
+ blocked = true;
2142
+ reject(Object.assign(Error("Close other sketch windows and retry"), { code: "SKETCH_STORAGE_BLOCKED" }));
1936
2143
  };
1937
- if (!["apply", "save"].includes(request.action)) throw Error("Unknown sketch action");
1938
- if (typeof request.requestId !== "string" || !request.requestId.length || request.requestId.length > 100) throw Error("A unique requestId is required");
1939
- const key = `${current.documentId}:${request.requestId}`, fingerprint = JSON.stringify({
1940
- ...request,
1941
- runId: void 0
1942
- });
1943
- const cached = completed.get(key);
1944
- if (cached) {
1945
- if (cached.fingerprint !== fingerprint) throw Error("requestId reused with different content");
1946
- return cached.result;
1947
- }
1948
- if (request.revision !== current.revision || adapter.busy()) throw Error("Sketch changed or is being edited; inspect again");
1949
- let changedObjects;
1950
- if (request.action === "apply") {
1951
- const before = adapter.document();
1952
- let next;
1953
- try {
1954
- next = applySketchCommands(before, request.commands);
1955
- } catch (cause) {
1956
- const error = new Error(`${cause.message} Correct the batch and retry with the same runId and revision; nothing was applied.`, { cause });
1957
- error.code = "SKETCH_INVALID_BATCH";
1958
- throw error;
1959
- }
1960
- adapter.commit(next);
1961
- const previous = new Map(before.layers.flatMap((l) => l.strokes.map((s) => [`${l.id}:${s.id}`, s])));
1962
- changedObjects = next.layers.flatMap((l) => l.strokes.filter((s) => previous.get(`${l.id}:${s.id}`) !== s).map((s) => ({
1963
- layer: l.id,
1964
- id: s.id
1965
- })));
1966
- } else {
1967
- if (request.name !== void 0 && (typeof request.name !== "string" || request.name.length > 60)) throw Error("Invalid draft name");
1968
- pending = true;
1969
- try {
1970
- await adapter.save(request.name);
1971
- } finally {
1972
- pending = false;
2144
+ request.onupgradeneeded = () => {
2145
+ if (blocked) {
2146
+ request.transaction.abort();
2147
+ return;
1973
2148
  }
1974
- }
1975
- const result = {
1976
- ...adapter.snapshot(),
1977
- ...changedObjects ? {
1978
- changedObjects: changedObjects.slice(0, 100),
1979
- changedObjectCount: changedObjects.length
1980
- } : {}
2149
+ const db = request.result, tx = request.transaction;
2150
+ if (!db.objectStoreNames.contains("drafts")) db.createObjectStore("drafts", { keyPath: "id" });
2151
+ const meta = db.createObjectStore("metadata", { keyPath: "key" });
2152
+ db.createObjectStore("recovery", { keyPath: "id" });
2153
+ const cursor = tx.objectStore("drafts").openCursor();
2154
+ cursor.onsuccess = () => {
2155
+ const row = cursor.result;
2156
+ if (row) {
2157
+ meta.put(metadata("drafts", row.value));
2158
+ row.continue();
2159
+ }
2160
+ };
1981
2161
  };
1982
- completed.set(key, {
1983
- fingerprint,
1984
- result,
1985
- receipt: {
1986
- requestId: request.requestId,
1987
- action: request.action,
1988
- revision: result.revision
2162
+ request.onsuccess = () => {
2163
+ if (blocked) {
2164
+ request.result.close();
2165
+ return;
1989
2166
  }
1990
- });
1991
- cachedCharacters += fingerprint.length;
1992
- while (completed.size > 1 && (completed.size > 128 || cachedCharacters > 4e6)) {
1993
- const oldest = completed.keys().next().value;
1994
- cachedCharacters -= completed.get(oldest).fingerprint.length;
1995
- completed.delete(oldest);
1996
- }
1997
- return result;
1998
- };
1999
- }
2000
- //#endregion
2001
- //#region src/sketch-formats.js
2002
- const SKETCH_FILE_ACCEPT = ".psd,.dsh-sketch.json,image/png,image/jpeg,image/webp";
2003
- const canvas = (w, h) => {
2004
- const c = document.createElement("canvas");
2005
- c.width = w;
2006
- c.height = h;
2007
- return c;
2008
- };
2009
- function runPsdCodec(action, payload) {
2010
- return new Promise((resolve, reject) => {
2011
- const worker = new Worker("/api/codex-subscription/sketch-psd-worker", { type: "module" });
2012
- const finish = (callback, value) => {
2013
- clearTimeout(timer);
2014
- worker.terminate();
2015
- callback(value);
2167
+ request.result.onversionchange = () => request.result.close();
2168
+ resolve(request.result);
2016
2169
  };
2017
- const timer = setTimeout(() => finish(reject, Error("PSD operation timed out")), 3e4);
2018
- worker.onerror = () => finish(reject, Error("PSD codec could not be loaded"));
2019
- worker.onmessage = (event) => event.data.ok ? finish(resolve, event.data.value) : finish(reject, Error(event.data.error));
2020
- worker.postMessage({
2021
- action,
2022
- payload
2023
- });
2024
- });
2025
- }
2026
- function encodeSketchDocument(doc) {
2027
- return JSON.stringify({
2028
- format: "dsh-sketch",
2029
- version: 1,
2030
- doc
2031
- });
2032
- }
2033
- function decodeSketchDocument(text) {
2034
- if (text.length > 32 * 1024 * 1024) throw Error("Draft exceeds 32 MB");
2035
- const file = JSON.parse(text), source = file.doc;
2036
- 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");
2037
- const w = source.width ?? 1024, h = source.height ?? 1024;
2038
- if (!Number.isInteger(w) || !Number.isInteger(h) || w < 1 || h < 1 || w > 2048 || h > 2048) throw Error("Invalid canvas size");
2039
- let doc = {
2040
- ...createSketchLayers(),
2041
- width: w,
2042
- height: h,
2043
- ratio: Object.keys(SKETCH_RATIOS).find((k) => SKETCH_RATIOS[k][0] === w && SKETCH_RATIOS[k][1] === h) ?? "custom"
2044
- };
2045
- for (let i = 0; i < source.layers.length; i++) {
2046
- const layer = source.layers[i];
2047
- if (i) doc = applySketchCommands(doc, [{
2048
- op: "layer",
2049
- action: "add"
2050
- }]);
2051
- if (!Array.isArray(layer.strokes)) throw Error("Invalid strokes");
2052
- for (let j = 0; j < layer.strokes.length; j += 256) {
2053
- const strokes = layer.strokes.slice(j, j + 256);
2054
- doc = applySketchCommands(doc, strokes.map((s) => ({
2055
- ...s,
2056
- op: "stroke",
2057
- layer: doc.active,
2058
- fill: s.fill ?? false
2059
- })));
2060
- const added = doc.layers.at(-1).strokes;
2061
- for (let k = 0; k < strokes.length; k++) {
2062
- const s = strokes[k];
2063
- if (s.brush !== void 0 && ![
2064
- "pen",
2065
- "pencil",
2066
- "marker"
2067
- ].includes(s.brush)) throw Error("Invalid brush");
2068
- if (s.pressure !== void 0 && (!Number.isFinite(s.pressure) || s.pressure < .2 || s.pressure > 1)) throw Error("Invalid pressure");
2069
- if (s.brushVersion !== void 0 && s.brushVersion !== 2) throw Error("Unsupported brush version");
2070
- Object.assign(added[added.length - strokes.length + k], {
2071
- brush: s.brush ?? "pen",
2072
- pressure: s.pressure ?? 1,
2073
- ...s.brushVersion === 2 ? { brushVersion: 2 } : {}
2074
- });
2170
+ request.onerror = () => reject(request.error);
2171
+ });
2172
+ try {
2173
+ return await new Promise((resolve, reject) => {
2174
+ const write = [
2175
+ "save",
2176
+ "delete",
2177
+ "checkpoint",
2178
+ "clearRecovery"
2179
+ ].includes(action);
2180
+ const tx = db.transaction([
2181
+ "drafts",
2182
+ "metadata",
2183
+ "recovery"
2184
+ ], write ? "readwrite" : "readonly");
2185
+ const meta = tx.objectStore("metadata"), kind = [
2186
+ "checkpoint",
2187
+ "recover",
2188
+ "clearRecovery"
2189
+ ].includes(action) ? "recovery" : "drafts", store = tx.objectStore(kind);
2190
+ let result, failure;
2191
+ tx.oncomplete = () => resolve(result);
2192
+ tx.onerror = () => reject(tx.error);
2193
+ tx.onabort = () => reject(failure ?? tx.error ?? Error("Draft transaction aborted"));
2194
+ if (action === "get" || action === "recover") {
2195
+ const req = store.get(value);
2196
+ req.onsuccess = () => {
2197
+ result = req.result;
2198
+ };
2199
+ return;
2075
2200
  }
2076
- }
2077
- const target = doc.layers.at(-1);
2078
- target.name = String(layer.name ?? "").slice(0, 40);
2079
- target.visible = layer.visible !== false;
2080
- if (layer.image) {
2081
- const image = layer.image;
2082
- if (typeof image.src !== "string" || !/^data:image\/png;base64,/.test(image.src) || image.src.length > 8 * 1024 * 1024 || [
2083
- "x",
2084
- "y",
2085
- "width",
2086
- "height"
2087
- ].some((k) => !Number.isFinite(image[k]) || image[k] < 0 || image[k] > 1)) throw Error("Invalid draft image");
2088
- const bytes = Uint8Array.from(atob(image.src.slice(image.src.indexOf(",") + 1)), (c) => c.charCodeAt(0));
2089
- if (bytes.length < 24) throw Error("Invalid draft image");
2090
- const header = new DataView(bytes.buffer);
2091
- 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");
2092
- target.image = {
2093
- src: image.src,
2094
- x: image.x,
2095
- y: image.y,
2096
- width: image.width,
2097
- height: image.height
2201
+ if (action === "delete" || action === "clearRecovery") {
2202
+ store.delete(value);
2203
+ meta.delete(`${kind}:${value}`);
2204
+ return;
2205
+ }
2206
+ if (![
2207
+ "list",
2208
+ "save",
2209
+ "checkpoint"
2210
+ ].includes(action)) {
2211
+ tx.abort();
2212
+ return;
2213
+ }
2214
+ const request = meta.getAll();
2215
+ request.onsuccess = () => {
2216
+ const rows = request.result;
2217
+ if (action === "list") {
2218
+ result = rows.filter((row) => row.kind === "drafts").sort((a, b) => b.updated - a.updated);
2219
+ return;
2220
+ }
2221
+ const next = metadata(kind, value), others = rows.filter((row) => row.key !== next.key && !(action === "save" && recoverySession && row.key === `recovery:${recoverySession}`));
2222
+ 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;
2223
+ if (code) {
2224
+ failure = Object.assign(Error("Draft storage limit reached"), { code });
2225
+ tx.abort();
2226
+ return;
2227
+ }
2228
+ store.put(value);
2229
+ meta.put(next);
2230
+ if (action === "save" && recoverySession) {
2231
+ tx.objectStore("recovery").delete(recoverySession);
2232
+ meta.delete(`recovery:${recoverySession}`);
2233
+ }
2234
+ result = value;
2098
2235
  };
2099
- }
2236
+ });
2237
+ } finally {
2238
+ db.close();
2100
2239
  }
2101
- const activeIndex = source.layers.findIndex((layer) => layer.id === source.active);
2102
- doc.active = doc.layers[Math.max(0, activeIndex)].id;
2103
- return doc;
2104
2240
  }
2105
- async function exportSketchPsd(doc, images, composite) {
2106
- const width = doc.width ?? 1024, height = doc.height ?? 1024;
2107
- const children = doc.layers.map((layer, i) => {
2108
- const ctx = canvas(width, height).getContext("2d"), ref = layer.image;
2109
- if (ref) ctx.drawImage(images.get(ref.src), ref.x * width, ref.y * height, ref.width * width, ref.height * height);
2110
- paintSketch(ctx, layer.strokes, width, true, height);
2241
+ async function decodeSketchImages(doc, images) {
2242
+ for (const layer of doc.layers) {
2243
+ const src = layer.image?.src;
2244
+ if (!src || images.has(src)) continue;
2245
+ if (!src.startsWith("data:image/png;base64,") || src.length > 8 * 1024 * 1024) throw Error("Invalid image");
2246
+ const image = new Image();
2247
+ image.src = src;
2248
+ await image.decode();
2249
+ images.set(src, image);
2250
+ }
2251
+ }
2252
+ async function importSketchImage(file) {
2253
+ if (![
2254
+ "image/png",
2255
+ "image/jpeg",
2256
+ "image/webp"
2257
+ ].includes(file.type) || file.size > 20 * 1024 * 1024) throw Error("Image must be PNG, JPEG or WebP under 20 MB");
2258
+ const bitmap = await createImageBitmap(file);
2259
+ try {
2260
+ if (bitmap.width * bitmap.height > 32 * 1024 * 1024) throw Error("Image too large");
2261
+ const scale = Math.min(1, 1024 / Math.max(bitmap.width, bitmap.height));
2262
+ const canvas = document.createElement("canvas");
2263
+ canvas.width = Math.max(1, Math.round(bitmap.width * scale));
2264
+ canvas.height = Math.max(1, Math.round(bitmap.height * scale));
2265
+ canvas.getContext("2d").drawImage(bitmap, 0, 0, canvas.width, canvas.height);
2111
2266
  return {
2112
- name: layer.name || `Layer ${i + 1}`,
2113
- hidden: !layer.visible,
2114
- opacity: 1,
2115
- blendMode: "normal",
2116
- imageData: ctx.getImageData(0, 0, width, height)
2267
+ src: canvas.toDataURL("image/png"),
2268
+ width: canvas.width,
2269
+ height: canvas.height
2117
2270
  };
2118
- });
2119
- const context = canvas(width, height).getContext("2d");
2120
- context.fillStyle = "#fff";
2121
- context.fillRect(0, 0, width, height);
2122
- if (children[0] && !children[0].hidden) {
2123
- const bottom = canvas(width, height);
2124
- bottom.getContext("2d").putImageData(children[0].imageData, 0, 0);
2125
- context.drawImage(bottom, 0, 0);
2126
- children[0].imageData = context.getImageData(0, 0, width, height);
2127
- } else {
2128
- if (children.length >= 8) throw Error("Show the bottom layer before exporting this eight-layer drawing");
2129
- children.unshift({
2130
- name: "Paper",
2131
- opacity: 1,
2132
- blendMode: "normal",
2133
- imageData: context.getImageData(0, 0, width, height)
2134
- });
2271
+ } finally {
2272
+ bitmap.close();
2135
2273
  }
2136
- return runPsdCodec("write", {
2137
- width,
2138
- height,
2139
- children,
2140
- imageData: composite.getContext("2d").getImageData(0, 0, width, height)
2141
- });
2142
2274
  }
2143
- async function importSketchPsd(file) {
2144
- if (file.size > 32 * 1024 * 1024) throw Error("PSD exceeds 32 MB");
2145
- const psd = await runPsdCodec("read", await file.arrayBuffer());
2146
- 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));
2147
- const doc = {
2148
- ...createSketchLayers(),
2149
- width,
2150
- height,
2151
- ratio: Object.keys(SKETCH_RATIOS).find((k) => SKETCH_RATIOS[k][0] === width && SKETCH_RATIOS[k][1] === height) ?? "custom",
2152
- layers: [],
2153
- nextId: psd.layers.length + 1
2275
+ //#endregion
2276
+ //#region src/sketch-presets.js
2277
+ const SKETCH_PRESETS = [
2278
+ "triangle",
2279
+ "diamond",
2280
+ "star"
2281
+ ];
2282
+ function expandSketchPreset(command) {
2283
+ if (!SKETCH_PRESETS.includes(command.shape)) return command;
2284
+ const points = command.points;
2285
+ 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");
2286
+ const [a, b] = points, x = Math.min(a.x, b.x), y = Math.min(a.y, b.y);
2287
+ const w = Math.abs(a.x - b.x), h = Math.abs(a.y - b.y);
2288
+ if (!w || !h) throw Error("Shape preset requires a non-empty box");
2289
+ const vertices = command.shape === "triangle" ? [
2290
+ [.5, 0],
2291
+ [1, 1],
2292
+ [0, 1]
2293
+ ] : command.shape === "diamond" ? [
2294
+ [.5, 0],
2295
+ [1, .5],
2296
+ [.5, 1],
2297
+ [0, .5]
2298
+ ] : Array.from({ length: 10 }, (_, i) => {
2299
+ const angle = i * Math.PI / 5 - Math.PI / 2, radius = i % 2 ? .22 : .5;
2300
+ return [.5 + Math.cos(angle) * radius, .5 + Math.sin(angle) * radius];
2301
+ });
2302
+ return {
2303
+ ...command,
2304
+ shape: "polygon",
2305
+ points: vertices.map(([u, v]) => ({
2306
+ x: x + u * w,
2307
+ y: y + v * h
2308
+ }))
2154
2309
  };
2155
- for (const [i, layer] of psd.layers.entries()) {
2156
- const src = canvas(layer.imageData.width, layer.imageData.height);
2157
- src.getContext("2d").putImageData(new ImageData(new Uint8ClampedArray(layer.imageData.data), layer.imageData.width, layer.imageData.height), 0, 0);
2158
- const out = canvas(width, height), ctx = out.getContext("2d");
2159
- ctx.globalAlpha = layer.opacity;
2160
- ctx.drawImage(src, layer.left * scale, layer.top * scale, src.width * scale, src.height * scale);
2161
- doc.layers.push({
2162
- id: i + 1,
2163
- name: layer.name,
2164
- visible: !layer.hidden,
2165
- strokes: [],
2166
- image: {
2167
- src: out.toDataURL("image/png"),
2168
- x: 0,
2169
- y: 0,
2170
- width: 1,
2171
- height: 1
2172
- }
2173
- });
2174
- }
2175
- return doc;
2176
2310
  }
2177
- //#endregion
2178
- //#region src/sketch-document-lifecycle.js
2179
- function createSketchDocumentLifecycle(state, { sessionId, t, schedule, checkpoint, cache, setSelection, setTextEdit, setRecovered, store = sketchDrafts, decodeImages = decodeSketchImages, readImage = importSketchImage }) {
2180
- const { doc, undo, redo, images, saved, dirty, documentId, documentRevision } = state;
2181
- const hasContent = () => doc.current.layers.some((layer) => layer.image || layer.strokes.length);
2182
- const save = async (name) => {
2183
- const savingDocument = documentId.current, savingRevision = documentRevision.current;
2184
- const row = {
2185
- id: saved.current?.id ?? crypto.randomUUID(),
2186
- name: name?.trim() || saved.current?.name || `${t("sketchTitle")} ${(/* @__PURE__ */ new Date()).toLocaleString()}`,
2187
- updated: Date.now(),
2188
- doc: structuredClone(doc.current)
2189
- };
2190
- try {
2191
- await store("save", row, sessionId);
2192
- } catch (error) {
2193
- if (error.code === "SKETCH_DRAFT_LIMIT") error.message = t("sketchDraftLimit");
2194
- if (error.code === "SKETCH_STORAGE_LIMIT") error.message = t("sketchStorageLimit");
2195
- throw error;
2311
+ const SKETCH_COMMAND_HELP = {
2312
+ 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.",
2313
+ 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.",
2314
+ commands: {
2315
+ 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.",
2316
+ 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},...]}",
2317
+ 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.",
2318
+ 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.",
2319
+ 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.",
2320
+ resize: "{op:\"resize\",ratio:\"1:1|4:3|3:4|16:9|9:16\"}"
2321
+ },
2322
+ limits: {
2323
+ strokes: MAX_SKETCH_STROKES,
2324
+ pointsPerStroke: MAX_STROKE_POINTS,
2325
+ pointsTotal: 2e5,
2326
+ commandsPerBatch: 256
2327
+ }
2328
+ };
2329
+ const finite = (value, min, max) => typeof value === "number" && Number.isFinite(value) && value >= min && value <= max;
2330
+ function applySketchCommands(source, commands) {
2331
+ if (!Array.isArray(commands) || !commands.length || commands.length > 256) throw Error("Expected 1–256 commands");
2332
+ let doc = identifyObjects(source);
2333
+ for (let command of commands) {
2334
+ if (!command || typeof command !== "object") throw Error("Invalid command");
2335
+ if (command.op === "stroke") command = expandSketchPreset(command);
2336
+ if (command.op === "resize") {
2337
+ doc = resizeSketch(doc, command.ratio);
2338
+ continue;
2196
2339
  }
2197
- if (documentId.current === savingDocument) {
2198
- saved.current = {
2199
- id: row.id,
2200
- name: row.name
2201
- };
2202
- if (documentRevision.current === savingRevision) {
2203
- dirty.current = false;
2204
- setRecovered(false);
2340
+ if (command.op === "layer") {
2341
+ if (![
2342
+ "add",
2343
+ "select",
2344
+ "rename",
2345
+ "visible",
2346
+ "duplicate",
2347
+ "up",
2348
+ "down",
2349
+ "delete",
2350
+ "clear"
2351
+ ].includes(command.action)) throw Error("Unknown layer action");
2352
+ if (command.action === "add") {
2353
+ const id = command.id ?? doc.nextId, after = command.after ?? doc.active;
2354
+ 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");
2355
+ const next = changeSketchLayer(doc, "add", after);
2356
+ if (next === doc) throw Error("Cannot add layer: check the existing after layer and the 8-layer limit");
2357
+ doc = {
2358
+ ...next,
2359
+ active: id,
2360
+ nextId: Math.max(next.nextId, id + 1),
2361
+ layers: next.layers.map((l) => l.id === next.active ? {
2362
+ ...l,
2363
+ id,
2364
+ name: String(command.value ?? "").trim().slice(0, 40)
2365
+ } : l)
2366
+ };
2367
+ continue;
2205
2368
  }
2369
+ const next = changeSketchLayer(doc, command.action, command.id ?? doc.active, command.value);
2370
+ if (next === doc) throw Error("Layer action unavailable; inspect the document first");
2371
+ doc = next;
2372
+ continue;
2206
2373
  }
2207
- };
2208
- const saveChanges = async () => {
2209
- if (!dirty.current) return;
2210
- if (hasContent() || saved.current) return save();
2211
- const id = documentId.current, revision = documentRevision.current;
2212
- await store("clearRecovery", sessionId);
2213
- if (documentId.current === id && documentRevision.current === revision) {
2214
- dirty.current = false;
2215
- setRecovered(false);
2374
+ if (command.op === "object") {
2375
+ const layer = doc.layers.find((l) => l.id === (command.layer ?? doc.active)), index = layer?.strokes.findIndex((s) => s.id === command.id);
2376
+ if (!layer?.visible || index < 0 || index === void 0) throw Error("Object missing or hidden; inspect again");
2377
+ const strokes = layer.strokes.slice(), original = strokes[index];
2378
+ if (command.action === "delete") strokes.splice(index, 1);
2379
+ else if (command.action === "duplicate") strokes.splice(index + 1, 0, {
2380
+ ...original,
2381
+ id: crypto.randomUUID(),
2382
+ points: original.points.map((p) => ({ ...p }))
2383
+ });
2384
+ else if (command.action === "update") {
2385
+ const patch = command.patch ?? {};
2386
+ if (Object.keys(patch).some((k) => ![
2387
+ "color",
2388
+ "width",
2389
+ "opacity",
2390
+ "fill",
2391
+ "text",
2392
+ "points"
2393
+ ].includes(k))) throw Error("Unsupported object property");
2394
+ const changed = command.transform ? transformObject({
2395
+ ...original,
2396
+ ...patch
2397
+ }, command.transform) : {
2398
+ ...original,
2399
+ ...patch
2400
+ };
2401
+ strokes[index] = {
2402
+ ...applySketchCommands({
2403
+ ...doc,
2404
+ layers: [{
2405
+ ...layer,
2406
+ strokes: []
2407
+ }]
2408
+ }, [{
2409
+ ...changed,
2410
+ op: "stroke",
2411
+ layer: layer.id
2412
+ }]).layers[0].strokes[0],
2413
+ brush: original.brush ?? "pen",
2414
+ ...original.pressure !== void 0 ? { pressure: original.pressure } : {},
2415
+ ...original.brushVersion === 2 ? { brushVersion: 2 } : {}
2416
+ };
2417
+ } else throw Error("Unknown object action");
2418
+ doc = {
2419
+ ...doc,
2420
+ layers: doc.layers.map((l) => l === layer ? {
2421
+ ...l,
2422
+ strokes
2423
+ } : l)
2424
+ };
2425
+ continue;
2216
2426
  }
2217
- };
2218
- const replace = (next, decoded, identity) => {
2219
- documentId.current = crypto.randomUUID();
2220
- documentRevision.current++;
2221
- doc.current = identifyObjects(structuredClone(next));
2222
- setSelection(null);
2223
- setTextEdit(null);
2224
- images.current = decoded;
2225
- cache.current.clear();
2226
- undo.current = [];
2227
- redo.current = [];
2228
- saved.current = identity;
2229
- dirty.current = false;
2230
- schedule();
2231
- };
2232
- const fresh = async () => {
2233
- await saveChanges();
2234
- replace(createSketchLayers(), /* @__PURE__ */ new Map(), null);
2235
- };
2236
- const load = async (row) => {
2237
- if (row.id === saved.current?.id) return;
2238
- row = await store("get", row.id);
2239
- if (!row) throw Error("Draft no longer exists");
2240
- await saveChanges();
2241
- const decoded = /* @__PURE__ */ new Map();
2242
- await decodeImages(row.doc, decoded);
2243
- replace(row.doc, decoded, {
2244
- id: row.id,
2245
- name: row.name
2246
- });
2247
- };
2248
- const importImage = async (file) => {
2249
- if (file.name?.toLowerCase().endsWith(".psd") || file.name?.toLowerCase().endsWith(".dsh-sketch.json")) {
2250
- if (file.size > 32 * 1024 * 1024) throw Error("File exceeds 32 MB");
2251
- const next = file.name.toLowerCase().endsWith(".psd") ? await importSketchPsd(file) : decodeSketchDocument(await file.text());
2252
- const decoded = /* @__PURE__ */ new Map();
2253
- await decodeImages(next, decoded);
2254
- await saveChanges();
2255
- replace(next, decoded, null);
2256
- dirty.current = true;
2257
- return;
2427
+ if (command.op !== "stroke") throw Error("Unknown command");
2428
+ const shape = command.shape === "ellipse" ? "circle" : command.shape ?? "pen";
2429
+ const { color, width = shape === "text" ? 24 : 2, opacity = 1, fill = false } = command;
2430
+ let points = command.points;
2431
+ if (command.start !== void 0 || command.segments !== void 0) {
2432
+ 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");
2433
+ points = [command.start, ...command.segments.flatMap((s) => [
2434
+ s?.control1,
2435
+ s?.control2,
2436
+ s?.end
2437
+ ])];
2258
2438
  }
2259
- if (doc.current.layers.length >= 8) throw Error("Layer limit");
2260
- const image = await readImage(file), w = doc.current.width ?? 1024, h = doc.current.height ?? 1024;
2261
- const scale = Math.min(w / image.width, h / image.height), width = image.width * scale / w, height = image.height * scale / h;
2262
- const layer = {
2263
- id: doc.current.nextId,
2264
- name: file.name?.slice(0, 40) || t("sketchImport"),
2265
- visible: true,
2266
- strokes: [],
2267
- image: {
2268
- src: image.src,
2269
- x: (1 - width) / 2,
2270
- y: (1 - height) / 2,
2271
- width,
2272
- height
2273
- }
2439
+ if (![
2440
+ "pen",
2441
+ "line",
2442
+ "rectangle",
2443
+ "circle",
2444
+ "polygon",
2445
+ "bezier",
2446
+ "arrow",
2447
+ "text",
2448
+ "eraser"
2449
+ ].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");
2450
+ if ([
2451
+ "line",
2452
+ "arrow",
2453
+ "text",
2454
+ "rectangle",
2455
+ "circle"
2456
+ ].includes(shape) && points.length !== 2 || shape === "polygon" && points.length < 3) throw Error("Invalid shape points");
2457
+ 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.`);
2458
+ if (fill && ![
2459
+ "rectangle",
2460
+ "circle",
2461
+ "polygon",
2462
+ "bezier"
2463
+ ].includes(shape)) throw Error("Fill requires a closed shape");
2464
+ const layer = doc.layers.find((layer) => layer.id === (command.layer ?? doc.active));
2465
+ if (!layer?.visible) throw Error("Target layer is missing or hidden");
2466
+ 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");
2467
+ const id = command.id ?? crypto.randomUUID();
2468
+ if (typeof id !== "string" || !id.length || id.length > 100 || layer.strokes.some((s) => s.id === id)) throw Error("Invalid or duplicate object id");
2469
+ const stroke = {
2470
+ id,
2471
+ ...shape === "text" ? { text: command.text } : {},
2472
+ shape,
2473
+ color,
2474
+ width,
2475
+ opacity,
2476
+ fill,
2477
+ brush: "pen",
2478
+ points: points.map((p) => ({
2479
+ x: p.x,
2480
+ y: p.y
2481
+ }))
2274
2482
  };
2275
- await decodeImages({ layers: [layer] }, images.current);
2276
- checkpoint();
2277
- doc.current = {
2278
- ...doc.current,
2279
- nextId: layer.id + 1,
2280
- active: layer.id,
2281
- layers: [...doc.current.layers, layer]
2483
+ doc = {
2484
+ ...doc,
2485
+ layers: doc.layers.map((item) => item === layer ? {
2486
+ ...item,
2487
+ strokes: [...item.strokes, stroke]
2488
+ } : item)
2489
+ };
2490
+ }
2491
+ 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");
2492
+ return doc;
2493
+ }
2494
+ function createSketchCommandSession(adapter) {
2495
+ const completed = /* @__PURE__ */ new Map();
2496
+ let pending = false, cachedCharacters = 0;
2497
+ return async (request) => {
2498
+ if (!request || typeof request !== "object") throw Error("Invalid sketch request");
2499
+ if (!adapter.available()) throw Error("Open the sketch board for this session first");
2500
+ const current = adapter.snapshot();
2501
+ if (request.action === "inspect") {
2502
+ const offset = request.offset ?? 0, objects = adapter.objects?.() ?? [];
2503
+ if (!Number.isInteger(offset) || offset < 0) throw Error("offset must be a non-negative integer");
2504
+ return {
2505
+ ...current,
2506
+ protocolVersion: 2,
2507
+ objects: request.objectId ? [] : objects.slice(offset, offset + 50),
2508
+ objectCount: objects.length,
2509
+ ...!request.objectId && offset + 50 < objects.length ? { nextOffset: offset + 50 } : {},
2510
+ ...request.objectId ? { object: adapter.object?.(request.objectId, request.layer) } : {},
2511
+ recentRequests: [...completed.values()].slice(-8).map((entry) => entry.receipt),
2512
+ ...!request.objectId && offset === 0 ? { help: SKETCH_COMMAND_HELP } : {}
2513
+ };
2514
+ }
2515
+ if (request.documentId !== current.documentId) throw Error("Document changed; inspect again");
2516
+ if (pending || adapter.busy()) throw Error("Sketch is being edited; retry after it settles");
2517
+ if (request.action === "preview") return {
2518
+ ...current,
2519
+ png: await adapter.preview()
2282
2520
  };
2283
- schedule();
2284
- };
2285
- const restore = async (isCurrent = () => true) => {
2286
- if (dirty.current || hasContent()) return;
2287
- const id = documentId.current, revision = documentRevision.current;
2288
- const recovery = await store("recover", sessionId);
2289
- const archived = state.restoreId.current;
2290
- const row = recovery ?? (archived ? await store("get", archived) : null);
2291
- const decoded = /* @__PURE__ */ new Map();
2292
- if (row) await decodeImages(row.doc, decoded);
2293
- if (!isCurrent() || documentId.current !== id || documentRevision.current !== revision || dirty.current) return;
2294
- if (row) {
2295
- replace(row.doc, decoded, recovery ? null : {
2296
- id: row.id,
2297
- name: row.name
2298
- });
2299
- dirty.current = Boolean(recovery);
2300
- setRecovered(Boolean(recovery));
2521
+ if (!["apply", "save"].includes(request.action)) throw Error("Unknown sketch action");
2522
+ if (typeof request.requestId !== "string" || !request.requestId.length || request.requestId.length > 100) throw Error("A unique requestId is required");
2523
+ const key = `${current.documentId}:${request.requestId}`, fingerprint = JSON.stringify({
2524
+ ...request,
2525
+ runId: void 0
2526
+ });
2527
+ const cached = completed.get(key);
2528
+ if (cached) {
2529
+ if (cached.fingerprint !== fingerprint) throw Error("requestId reused with different content");
2530
+ return cached.result;
2301
2531
  }
2302
- state.restoreId.current = null;
2303
- };
2304
- return {
2305
- hasContent,
2306
- save,
2307
- saveChanges,
2308
- replace,
2309
- fresh,
2310
- load,
2311
- importImage,
2312
- restore
2313
- };
2314
- }
2315
- //#endregion
2316
- //#region src/sketch-operation-gate.js
2317
- function createSketchOperationGate() {
2318
- let running = false;
2319
- return {
2320
- get running() {
2321
- return running;
2322
- },
2323
- async run(operation, { blocked = false, working, report, rethrow = false }) {
2324
- if (blocked || running) return false;
2325
- running = true;
2532
+ if (request.revision !== current.revision || adapter.busy()) throw Error("Sketch changed or is being edited; inspect again");
2533
+ let changedObjects;
2534
+ if (request.action === "apply") {
2535
+ const before = adapter.document();
2536
+ let next;
2326
2537
  try {
2327
- working(true);
2328
- report(null);
2329
- await operation();
2330
- return true;
2331
- } catch (error) {
2332
- report(error);
2333
- if (rethrow) throw error;
2334
- return false;
2538
+ next = applySketchCommands(before, request.commands);
2539
+ } catch (cause) {
2540
+ const error = new Error(`${cause.message} Correct the batch and retry with the same runId and revision; nothing was applied.`, { cause });
2541
+ error.code = "SKETCH_INVALID_BATCH";
2542
+ throw error;
2543
+ }
2544
+ adapter.commit(next);
2545
+ const previous = new Map(before.layers.flatMap((l) => l.strokes.map((s) => [`${l.id}:${s.id}`, s])));
2546
+ changedObjects = next.layers.flatMap((l) => l.strokes.filter((s) => previous.get(`${l.id}:${s.id}`) !== s).map((s) => ({
2547
+ layer: l.id,
2548
+ id: s.id
2549
+ })));
2550
+ } else {
2551
+ if (request.name !== void 0 && (typeof request.name !== "string" || request.name.length > 60)) throw Error("Invalid draft name");
2552
+ pending = true;
2553
+ try {
2554
+ await adapter.save(request.name);
2335
2555
  } finally {
2336
- running = false;
2337
- working(false);
2556
+ pending = false;
2338
2557
  }
2339
2558
  }
2340
- };
2341
- }
2342
- //#endregion
2343
- //#region src/sketch-agent-export.js
2344
- async function exportSketchAgentFile(format, { gate, blocked, working, report, exportFile }) {
2345
- let result;
2346
- if (!await gate.run(async () => {
2347
- const { blob, extension } = await exportFile(format);
2348
- const data = new Uint8Array(await blob.arrayBuffer());
2349
- let raw = "";
2350
- for (let i = 0; i < data.length; i += 8192) raw += String.fromCharCode(...data.subarray(i, i + 8192));
2351
- result = {
2352
- extension,
2353
- mediaType: blob.type,
2354
- base64: btoa(raw)
2559
+ const result = {
2560
+ ...adapter.snapshot(),
2561
+ ...changedObjects ? {
2562
+ changedObjects: changedObjects.slice(0, 100),
2563
+ changedObjectCount: changedObjects.length
2564
+ } : {}
2355
2565
  };
2356
- }, {
2357
- blocked,
2358
- working,
2359
- report,
2360
- rethrow: true
2361
- })) throw Error("Sketch is being edited; retry after it settles");
2362
- return result;
2363
- }
2364
- //#endregion
2365
- //#region src/sketch-run-status.jsx
2366
- function SketchRunStatus({ state, t, floating = false, onOpen, onStop, onResume, onDismiss }) {
2367
- if (state === "idle") return null;
2368
- const drawing = state === "drawing", recover = state === "stopped" || state === "failed";
2369
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2370
- className: floating ? "codexSketchBackgroundStatus" : "codexSketchAgentStatus",
2371
- role: "status",
2372
- "aria-live": "polite",
2373
- children: [
2374
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(WorkspaceIcon, {
2375
- name: drawing ? "pen" : state === "finished" ? "check" : "rectangle",
2376
- size: 15
2377
- }),
2378
- floating ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2379
- type: "button",
2380
- onClick: onOpen,
2381
- children: t(`sketchRun_${state}`)
2382
- }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t(`sketchRun_${state}`) }),
2383
- drawing ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
2384
- type: "button",
2385
- className: "codexSketchStop",
2386
- onClick: onStop,
2387
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(WorkspaceIcon, {
2388
- name: "stop",
2389
- size: 12
2390
- }), t("sketchRunStop")]
2391
- }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [recover ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2392
- type: "button",
2393
- title: t("sketchRunResumeHint"),
2394
- onClick: onResume,
2395
- children: t("sketchRunResume")
2396
- }) : null, !recover ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2397
- type: "button",
2398
- "aria-label": t("sketchDismissStatus"),
2399
- title: t("sketchDismissStatus"),
2400
- onClick: onDismiss,
2401
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(WorkspaceIcon, {
2402
- name: "close",
2403
- size: 14
2404
- })
2405
- }) : null] })
2406
- ]
2407
- });
2408
- }
2409
- //#endregion
2410
- //#region src/sketch-tool-widths.js
2411
- const defaults = {
2412
- pen: 12,
2413
- pencil: 6,
2414
- marker: 28,
2415
- eraser: 24,
2416
- text: 32,
2417
- line: 12,
2418
- arrow: 12,
2419
- rectangle: 12,
2420
- circle: 12
2421
- };
2422
- const keyFor = ({ tool, brush }) => tool === "pen" ? brush : tool;
2423
- function switchSketchToolWidth(memory, current, next) {
2424
- if (current.tool !== "select") memory[keyFor(current)] = current.width;
2425
- if (next.tool === "select") return current.width;
2426
- const key = keyFor(next);
2427
- return memory[key] ?? defaults[key] ?? 12;
2428
- }
2429
- function stepSketchWidth(width, direction) {
2430
- return Math.max(1, Math.min(256, width + direction * 2));
2431
- }
2432
- //#endregion
2433
- //#region src/sketch-shortcuts.js
2434
- function sketchShortcutAction(keys, key) {
2435
- key = key.toLowerCase();
2436
- return Object.entries(keys).find(([, value]) => value === key)?.[0] ?? {
2437
- v: "select",
2438
- t: "text",
2439
- "+": "zoomIn"
2440
- }[key];
2566
+ completed.set(key, {
2567
+ fingerprint,
2568
+ result,
2569
+ receipt: {
2570
+ requestId: request.requestId,
2571
+ action: request.action,
2572
+ revision: result.revision
2573
+ }
2574
+ });
2575
+ cachedCharacters += fingerprint.length;
2576
+ while (completed.size > 1 && (completed.size > 128 || cachedCharacters > 4e6)) {
2577
+ const oldest = completed.keys().next().value;
2578
+ cachedCharacters -= completed.get(oldest).fingerprint.length;
2579
+ completed.delete(oldest);
2580
+ }
2581
+ return result;
2582
+ };
2441
2583
  }
2442
2584
  //#endregion
2443
- //#region src/sketch-layer-renderer.js
2444
- const NO_IMAGES = /* @__PURE__ */ new Map();
2445
- const surface = (width, height) => {
2585
+ //#region src/sketch-formats.js
2586
+ const SKETCH_FILE_ACCEPT = ".psd,.dsh-sketch.json,image/png,image/jpeg,image/webp";
2587
+ const canvas = (w, h) => {
2446
2588
  const c = document.createElement("canvas");
2447
- c.width = width;
2448
- c.height = height;
2589
+ c.width = w;
2590
+ c.height = h;
2449
2591
  return c;
2450
2592
  };
2451
- function paintSketchLayers(context, doc, cache, size = doc.width ?? 1024, height = doc.height ?? size, activeLayer, images = NO_IMAGES) {
2452
- context.globalCompositeOperation = "source-over";
2453
- context.globalAlpha = 1;
2454
- context.fillStyle = "#fff";
2455
- context.fillRect(0, 0, size, height);
2456
- for (const id of cache.keys()) if (!doc.layers.some((layer) => layer.id === id)) cache.delete(id);
2457
- for (const layer of doc.layers) {
2458
- if (!layer.visible) continue;
2459
- let entry = cache.get(layer.id);
2460
- if (!entry || entry.surface.width !== size || entry.surface.height !== height) {
2461
- entry = {
2462
- surface: surface(size, height),
2463
- base: surface(size, height)
2464
- };
2465
- cache.set(layer.id, entry);
2466
- }
2467
- const moving = layer.id === activeLayer;
2468
- const count = Math.max(0, layer.strokes.length - (moving ? 1 : 0));
2469
- const prefix = layer.strokes[count - 1];
2470
- if (entry.count !== count || entry.prefix !== prefix || entry.image !== layer.image || entry.strokes !== layer.strokes) {
2471
- const ctx = entry.base.getContext("2d");
2472
- 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]));
2473
- if (!append) {
2474
- ctx.clearRect(0, 0, size, height);
2475
- const ref = layer.image, image = ref && images.get(ref.src);
2476
- if (image) ctx.drawImage(image, ref.x * size, ref.y * height, ref.width * size, ref.height * height);
2593
+ function runPsdCodec(action, payload) {
2594
+ return new Promise((resolve, reject) => {
2595
+ const worker = new Worker("/api/codex-subscription/sketch-psd-worker", { type: "module" });
2596
+ const finish = (callback, value) => {
2597
+ clearTimeout(timer);
2598
+ worker.terminate();
2599
+ callback(value);
2600
+ };
2601
+ const timer = setTimeout(() => finish(reject, Error("PSD operation timed out")), 3e4);
2602
+ worker.onerror = () => finish(reject, Error("PSD codec could not be loaded"));
2603
+ worker.onmessage = (event) => event.data.ok ? finish(resolve, event.data.value) : finish(reject, Error(event.data.error));
2604
+ worker.postMessage({
2605
+ action,
2606
+ payload
2607
+ });
2608
+ });
2609
+ }
2610
+ function encodeSketchDocument(doc) {
2611
+ return JSON.stringify({
2612
+ format: "dsh-sketch",
2613
+ version: 1,
2614
+ doc
2615
+ });
2616
+ }
2617
+ function decodeSketchDocument(text) {
2618
+ if (text.length > 32 * 1024 * 1024) throw Error("Draft exceeds 32 MB");
2619
+ const file = JSON.parse(text), source = file.doc;
2620
+ 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");
2621
+ const w = source.width ?? 1024, h = source.height ?? 1024;
2622
+ if (!Number.isInteger(w) || !Number.isInteger(h) || w < 1 || h < 1 || w > 2048 || h > 2048) throw Error("Invalid canvas size");
2623
+ let doc = {
2624
+ ...createSketchLayers(),
2625
+ width: w,
2626
+ height: h,
2627
+ ratio: Object.keys(SKETCH_RATIOS).find((k) => SKETCH_RATIOS[k][0] === w && SKETCH_RATIOS[k][1] === h) ?? "custom"
2628
+ };
2629
+ for (let i = 0; i < source.layers.length; i++) {
2630
+ const layer = source.layers[i];
2631
+ if (i) doc = applySketchCommands(doc, [{
2632
+ op: "layer",
2633
+ action: "add"
2634
+ }]);
2635
+ if (!Array.isArray(layer.strokes)) throw Error("Invalid strokes");
2636
+ for (let j = 0; j < layer.strokes.length; j += 256) {
2637
+ const strokes = layer.strokes.slice(j, j + 256);
2638
+ doc = applySketchCommands(doc, strokes.map((s) => ({
2639
+ ...s,
2640
+ op: "stroke",
2641
+ layer: doc.active,
2642
+ fill: s.fill ?? false
2643
+ })));
2644
+ const added = doc.layers.at(-1).strokes;
2645
+ for (let k = 0; k < strokes.length; k++) {
2646
+ const s = strokes[k];
2647
+ if (s.brush !== void 0 && ![
2648
+ "pen",
2649
+ "pencil",
2650
+ "marker"
2651
+ ].includes(s.brush)) throw Error("Invalid brush");
2652
+ if (s.pressure !== void 0 && (!Number.isFinite(s.pressure) || s.pressure < .2 || s.pressure > 1)) throw Error("Invalid pressure");
2653
+ if (s.brushVersion !== void 0 && s.brushVersion !== 2) throw Error("Unsupported brush version");
2654
+ Object.assign(added[added.length - strokes.length + k], {
2655
+ brush: s.brush ?? "pen",
2656
+ pressure: s.pressure ?? 1,
2657
+ ...s.brushVersion === 2 ? { brushVersion: 2 } : {}
2658
+ });
2477
2659
  }
2478
- paintSketch(ctx, layer.strokes, size, true, height, append ? entry.count : 0, count);
2479
- entry.count = count;
2480
- entry.prefix = prefix;
2481
- entry.image = layer.image;
2482
- entry.strokes = layer.strokes;
2483
2660
  }
2484
- if (moving) {
2485
- const ctx = entry.surface.getContext("2d");
2486
- ctx.clearRect(0, 0, size, height);
2487
- ctx.drawImage(entry.base, 0, 0);
2488
- paintSketch(ctx, layer.strokes, size, true, height, layer.strokes.length - 1);
2489
- context.drawImage(entry.surface, 0, 0);
2490
- } else context.drawImage(entry.base, 0, 0);
2661
+ const target = doc.layers.at(-1);
2662
+ target.name = String(layer.name ?? "").slice(0, 40);
2663
+ target.visible = layer.visible !== false;
2664
+ if (layer.image) {
2665
+ const image = layer.image;
2666
+ if (typeof image.src !== "string" || !/^data:image\/png;base64,/.test(image.src) || image.src.length > 8 * 1024 * 1024 || [
2667
+ "x",
2668
+ "y",
2669
+ "width",
2670
+ "height"
2671
+ ].some((k) => !Number.isFinite(image[k]) || image[k] < 0 || image[k] > 1)) throw Error("Invalid draft image");
2672
+ const bytes = Uint8Array.from(atob(image.src.slice(image.src.indexOf(",") + 1)), (c) => c.charCodeAt(0));
2673
+ if (bytes.length < 24) throw Error("Invalid draft image");
2674
+ const header = new DataView(bytes.buffer);
2675
+ 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");
2676
+ target.image = {
2677
+ src: image.src,
2678
+ x: image.x,
2679
+ y: image.y,
2680
+ width: image.width,
2681
+ height: image.height
2682
+ };
2683
+ }
2491
2684
  }
2685
+ const activeIndex = source.layers.findIndex((layer) => layer.id === source.active);
2686
+ doc.active = doc.layers[Math.max(0, activeIndex)].id;
2687
+ return doc;
2492
2688
  }
2493
- //#endregion
2494
- //#region src/sketch-interactions.js
2495
- function useSketchDismiss(open, close, host, selectors) {
2496
- const latest = (0, react.useRef)(close);
2497
- latest.current = close;
2498
- (0, react.useEffect)(() => {
2499
- if (!open) return;
2500
- const dialog = host.current?.closest("dialog") ?? host.current;
2501
- if (!dialog) return;
2502
- const pointer = (event) => {
2503
- if (selectors.some((selector) => event.target.closest?.(selector))) return;
2504
- latest.current(false);
2505
- if (event.target.matches?.("canvas")) {
2506
- event.preventDefault();
2507
- event.stopPropagation();
2508
- event.target.focus({ preventScroll: true });
2509
- }
2510
- };
2511
- const key = (event) => {
2512
- if (event.key !== "Escape") return;
2513
- event.preventDefault();
2514
- event.stopPropagation();
2515
- latest.current(false);
2516
- dialog.querySelector("canvas")?.focus({ preventScroll: true });
2517
- };
2518
- const hidden = () => latest.current(false);
2519
- document.addEventListener("pointerdown", pointer, true);
2520
- document.addEventListener("keydown", key, true);
2521
- dialog.addEventListener("close", hidden);
2522
- return () => {
2523
- document.removeEventListener("pointerdown", pointer, true);
2524
- document.removeEventListener("keydown", key, true);
2525
- dialog.removeEventListener("close", hidden);
2689
+ async function exportSketchPsd(doc, images, composite) {
2690
+ const width = doc.width ?? 1024, height = doc.height ?? 1024;
2691
+ const children = doc.layers.map((layer, i) => {
2692
+ const ctx = canvas(width, height).getContext("2d"), ref = layer.image;
2693
+ if (ref) ctx.drawImage(images.get(ref.src), ref.x * width, ref.y * height, ref.width * width, ref.height * height);
2694
+ paintSketch(ctx, layer.strokes, width, true, height);
2695
+ return {
2696
+ name: layer.name || `Layer ${i + 1}`,
2697
+ hidden: !layer.visible,
2698
+ opacity: 1,
2699
+ blendMode: "normal",
2700
+ imageData: ctx.getImageData(0, 0, width, height)
2526
2701
  };
2527
- }, [
2528
- open,
2529
- host,
2530
- selectors.join("|")
2531
- ]);
2702
+ });
2703
+ const context = canvas(width, height).getContext("2d");
2704
+ context.fillStyle = "#fff";
2705
+ context.fillRect(0, 0, width, height);
2706
+ if (children[0] && !children[0].hidden) {
2707
+ const bottom = canvas(width, height);
2708
+ bottom.getContext("2d").putImageData(children[0].imageData, 0, 0);
2709
+ context.drawImage(bottom, 0, 0);
2710
+ children[0].imageData = context.getImageData(0, 0, width, height);
2711
+ } else {
2712
+ if (children.length >= 8) throw Error("Show the bottom layer before exporting this eight-layer drawing");
2713
+ children.unshift({
2714
+ name: "Paper",
2715
+ opacity: 1,
2716
+ blendMode: "normal",
2717
+ imageData: context.getImageData(0, 0, width, height)
2718
+ });
2719
+ }
2720
+ return runPsdCodec("write", {
2721
+ width,
2722
+ height,
2723
+ children,
2724
+ imageData: composite.getContext("2d").getImageData(0, 0, width, height)
2725
+ });
2532
2726
  }
2533
- function useSketchCursor(canvas, ring, width, brush, zoom, hidden) {
2534
- const last = (0, react.useRef)(null), heldPressure = (0, react.useRef)(1);
2535
- const update = (event, bounds) => {
2536
- if (event) last.current = event;
2537
- const pointer = last.current, node = canvas.current, cursor = ring.current;
2538
- if (!pointer || !node || !cursor) return;
2539
- const rect = bounds ?? node.getBoundingClientRect();
2540
- if (hidden || pointer.pointerType === "touch" || pointer.clientX < rect.left || pointer.clientX > rect.right || pointer.clientY < rect.top || pointer.clientY > rect.bottom) {
2541
- cursor.hidden = true;
2542
- return;
2543
- }
2544
- const diameter = width * (node.hasPointerCapture(pointer.pointerId) && pointer.pointerType === "pen" ? heldPressure.current : 1) * rect.width / node.width;
2545
- cursor.hidden = false;
2546
- cursor.style.width = `${diameter}px`;
2547
- cursor.style.height = `${diameter}px`;
2548
- cursor.style.transform = `translate(${pointer.clientX - diameter / 2}px,${pointer.clientY - diameter / 2}px)`;
2549
- };
2550
- (0, react.useEffect)(() => {
2551
- update();
2552
- const observer = new ResizeObserver(() => update());
2553
- if (canvas.current) observer.observe(canvas.current);
2554
- return () => observer.disconnect();
2555
- }, [
2727
+ async function importSketchPsd(file) {
2728
+ if (file.size > 32 * 1024 * 1024) throw Error("PSD exceeds 32 MB");
2729
+ const psd = await runPsdCodec("read", await file.arrayBuffer());
2730
+ 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));
2731
+ const doc = {
2732
+ ...createSketchLayers(),
2556
2733
  width,
2557
- brush,
2558
- zoom,
2559
- hidden
2560
- ]);
2561
- return {
2562
- down: (event) => {
2563
- heldPressure.current = event.pointerType === "pen" ? Math.max(.2, event.pressure) : 1;
2564
- },
2565
- move: (event, bounds) => update({
2566
- clientX: event.clientX,
2567
- clientY: event.clientY,
2568
- pointerId: event.pointerId,
2569
- pointerType: event.pointerType,
2570
- pressure: event.pressure
2571
- }, bounds),
2572
- leave: () => {
2573
- last.current = null;
2574
- if (ring.current) ring.current.hidden = true;
2575
- }
2734
+ height,
2735
+ ratio: Object.keys(SKETCH_RATIOS).find((k) => SKETCH_RATIOS[k][0] === width && SKETCH_RATIOS[k][1] === height) ?? "custom",
2736
+ layers: [],
2737
+ nextId: psd.layers.length + 1
2576
2738
  };
2577
- }
2578
- //#endregion
2579
- //#region src/sketch-view.jsx
2580
- const DEFAULT_KEYS = {
2581
- pen: "b",
2582
- eraser: "e",
2583
- line: "l",
2584
- rectangle: "r",
2585
- circle: "o",
2586
- pan: " ",
2587
- zoomIn: "=",
2588
- zoomOut: "-",
2589
- fit: "0"
2590
- };
2591
- function useSketchView(canvas, open) {
2592
- const [view, setView] = (0, react.useState)({
2593
- scale: 1,
2594
- x: 0,
2595
- y: 0
2596
- }), [keys, setKeys] = (0, react.useState)(() => {
2739
+ for (const [i, layer] of psd.layers.entries()) {
2740
+ const src = canvas(layer.imageData.width, layer.imageData.height);
2741
+ src.getContext("2d").putImageData(new ImageData(new Uint8ClampedArray(layer.imageData.data), layer.imageData.width, layer.imageData.height), 0, 0);
2742
+ const out = canvas(width, height), ctx = out.getContext("2d");
2743
+ ctx.globalAlpha = layer.opacity;
2744
+ ctx.drawImage(src, layer.left * scale, layer.top * scale, src.width * scale, src.height * scale);
2745
+ doc.layers.push({
2746
+ id: i + 1,
2747
+ name: layer.name,
2748
+ visible: !layer.hidden,
2749
+ strokes: [],
2750
+ image: {
2751
+ src: out.toDataURL("image/png"),
2752
+ x: 0,
2753
+ y: 0,
2754
+ width: 1,
2755
+ height: 1
2756
+ }
2757
+ });
2758
+ }
2759
+ return doc;
2760
+ }
2761
+ //#endregion
2762
+ //#region src/sketch-document-lifecycle.js
2763
+ function createSketchDocumentLifecycle(state, { sessionId, t, schedule, checkpoint, cache, setSelection, setTextEdit, setRecovered, confirmPsd = async () => false, store = sketchDrafts, decodeImages = decodeSketchImages, readImage = importSketchImage }) {
2764
+ const { doc, undo, redo, images, saved, dirty, documentId, documentRevision } = state;
2765
+ const hasContent = () => doc.current.layers.some((layer) => layer.image || layer.strokes.length);
2766
+ const save = async (name) => {
2767
+ const savingDocument = documentId.current, savingRevision = documentRevision.current;
2768
+ const row = {
2769
+ id: saved.current?.id ?? crypto.randomUUID(),
2770
+ name: name?.trim() || saved.current?.name || `${t("sketchTitle")} ${(/* @__PURE__ */ new Date()).toLocaleString()}`,
2771
+ updated: Date.now(),
2772
+ doc: structuredClone(doc.current)
2773
+ };
2597
2774
  try {
2598
- return {
2599
- ...DEFAULT_KEYS,
2600
- ...JSON.parse(localStorage.getItem("codex-sketch-keys"))
2775
+ await store("save", row, sessionId);
2776
+ } catch (error) {
2777
+ if (error.code === "SKETCH_DRAFT_LIMIT") error.message = t("sketchDraftLimit");
2778
+ if (error.code === "SKETCH_STORAGE_LIMIT") error.message = t("sketchStorageLimit");
2779
+ throw error;
2780
+ }
2781
+ if (documentId.current === savingDocument) {
2782
+ saved.current = {
2783
+ id: row.id,
2784
+ name: row.name
2601
2785
  };
2602
- } catch {
2603
- return DEFAULT_KEYS;
2786
+ if (documentRevision.current === savingRevision) {
2787
+ dirty.current = false;
2788
+ setRecovered(false);
2789
+ }
2604
2790
  }
2605
- });
2606
- const [shortcuts, setShortcuts] = (0, react.useState)(() => {
2607
- try {
2608
- return localStorage.getItem("codex-sketch-shortcuts") !== "off";
2609
- } catch {
2610
- return true;
2791
+ };
2792
+ const saveChanges = async () => {
2793
+ if (!dirty.current) return;
2794
+ if (hasContent() || saved.current) return save();
2795
+ const id = documentId.current, revision = documentRevision.current;
2796
+ await store("clearRecovery", sessionId);
2797
+ if (documentId.current === id && documentRevision.current === revision) {
2798
+ dirty.current = false;
2799
+ setRecovered(false);
2611
2800
  }
2612
- }), [space, setSpace] = (0, react.useState)(false);
2613
- const drag = (0, react.useRef)(null), viewRef = (0, react.useRef)(view);
2614
- viewRef.current = view;
2615
- const zoom = (factor) => setView((v) => ({
2616
- ...v,
2617
- scale: Math.max(.25, Math.min(8, v.scale * factor))
2618
- }));
2619
- const reset = () => setView({
2620
- scale: 1,
2621
- x: 0,
2622
- y: 0
2623
- });
2624
- (0, react.useEffect)(() => {
2625
- const node = canvas.current;
2626
- if (!node || !open) return;
2627
- const wheel = (e) => {
2628
- if (!shortcuts || !e.altKey) return;
2629
- e.preventDefault();
2630
- zoom(e.deltaY < 0 ? 1.1 : 1 / 1.1);
2801
+ };
2802
+ const replace = (next, decoded, identity) => {
2803
+ documentId.current = crypto.randomUUID();
2804
+ documentRevision.current++;
2805
+ doc.current = identifyObjects(structuredClone(next));
2806
+ setSelection(null);
2807
+ setTextEdit(null);
2808
+ images.current = decoded;
2809
+ cache.current.clear();
2810
+ undo.current = [];
2811
+ redo.current = [];
2812
+ saved.current = identity;
2813
+ dirty.current = false;
2814
+ schedule();
2815
+ };
2816
+ const fresh = async () => {
2817
+ await saveChanges();
2818
+ replace(createSketchLayers(), /* @__PURE__ */ new Map(), null);
2819
+ };
2820
+ const load = async (row) => {
2821
+ if (row.id === saved.current?.id) return;
2822
+ row = await store("get", row.id);
2823
+ if (!row) throw Error("Draft no longer exists");
2824
+ await saveChanges();
2825
+ const decoded = /* @__PURE__ */ new Map();
2826
+ await decodeImages(row.doc, decoded);
2827
+ replace(row.doc, decoded, {
2828
+ id: row.id,
2829
+ name: row.name
2830
+ });
2831
+ };
2832
+ const importImage = async (file) => {
2833
+ if (file.name?.toLowerCase().endsWith(".psd") || file.name?.toLowerCase().endsWith(".dsh-sketch.json")) {
2834
+ if (file.size > 32 * 1024 * 1024) throw Error("File exceeds 32 MB");
2835
+ if (file.name.toLowerCase().endsWith(".psd") && !await confirmPsd()) return;
2836
+ const next = file.name.toLowerCase().endsWith(".psd") ? await importSketchPsd(file) : decodeSketchDocument(await file.text());
2837
+ const decoded = /* @__PURE__ */ new Map();
2838
+ await decodeImages(next, decoded);
2839
+ await saveChanges();
2840
+ replace(next, decoded, null);
2841
+ dirty.current = true;
2842
+ return;
2843
+ }
2844
+ if (doc.current.layers.length >= 8) throw Error("Layer limit");
2845
+ const image = await readImage(file), w = doc.current.width ?? 1024, h = doc.current.height ?? 1024;
2846
+ const scale = Math.min(w / image.width, h / image.height), width = image.width * scale / w, height = image.height * scale / h;
2847
+ const layer = {
2848
+ id: doc.current.nextId,
2849
+ name: file.name?.slice(0, 40) || t("sketchImport"),
2850
+ visible: true,
2851
+ strokes: [],
2852
+ image: {
2853
+ src: image.src,
2854
+ x: (1 - width) / 2,
2855
+ y: (1 - height) / 2,
2856
+ width,
2857
+ height
2858
+ }
2631
2859
  };
2632
- node.addEventListener("wheel", wheel, { passive: false });
2633
- return () => node.removeEventListener("wheel", wheel);
2634
- }, [open, shortcuts]);
2635
- (0, react.useEffect)(() => {
2636
- const stop = () => {
2637
- drag.current = null;
2638
- setSpace(false);
2860
+ await decodeImages({ layers: [layer] }, images.current);
2861
+ checkpoint();
2862
+ doc.current = {
2863
+ ...doc.current,
2864
+ nextId: layer.id + 1,
2865
+ active: layer.id,
2866
+ layers: [...doc.current.layers, layer]
2639
2867
  };
2640
- window.addEventListener("blur", stop);
2641
- return () => window.removeEventListener("blur", stop);
2642
- }, []);
2643
- (0, react.useEffect)(() => {
2644
- if (!open || !shortcuts) {
2645
- setSpace(false);
2646
- drag.current = null;
2868
+ schedule();
2869
+ };
2870
+ const restore = async (isCurrent = () => true) => {
2871
+ if (dirty.current || hasContent()) return;
2872
+ const id = documentId.current, revision = documentRevision.current;
2873
+ const recovery = await store("recover", sessionId);
2874
+ const archived = state.restoreId.current;
2875
+ const row = recovery ?? (archived ? await store("get", archived) : null);
2876
+ const decoded = /* @__PURE__ */ new Map();
2877
+ if (row) await decodeImages(row.doc, decoded);
2878
+ if (!isCurrent() || documentId.current !== id || documentRevision.current !== revision || dirty.current) return;
2879
+ if (row) {
2880
+ replace(row.doc, decoded, recovery ? null : {
2881
+ id: row.id,
2882
+ name: row.name
2883
+ });
2884
+ dirty.current = Boolean(recovery);
2885
+ setRecovered(Boolean(recovery));
2647
2886
  }
2648
- }, [open, shortcuts]);
2649
- const setKey = (action, key) => {
2650
- key = key.toLowerCase();
2651
- if (!key || Object.entries(keys).some(([a, k]) => a !== action && k === key) || ["[", "]"].includes(key)) return;
2652
- const next = {
2653
- ...keys,
2654
- [action]: key
2655
- };
2656
- setKeys(next);
2657
- try {
2658
- localStorage.setItem("codex-sketch-keys", JSON.stringify(next));
2659
- } catch {}
2887
+ state.restoreId.current = null;
2660
2888
  };
2661
2889
  return {
2662
- view,
2663
- keys,
2664
- shortcuts,
2665
- space,
2666
- zoom,
2667
- reset,
2668
- setKey,
2669
- toggle: () => setShortcuts((v) => {
2670
- try {
2671
- localStorage.setItem("codex-sketch-shortcuts", v ? "off" : "on");
2672
- } catch {}
2673
- return !v;
2674
- }),
2675
- keyDown: (e) => {
2676
- if (!shortcuts || e.ctrlKey || e.metaKey || e.altKey) return false;
2677
- const action = sketchShortcutAction(keys, e.key);
2678
- if (action === "pan") {
2679
- e.preventDefault();
2680
- setSpace(true);
2681
- return true;
2682
- }
2683
- if ([
2684
- "zoomIn",
2685
- "zoomOut",
2686
- "fit"
2687
- ].includes(action)) {
2688
- e.preventDefault();
2689
- if (action === "fit") reset();
2690
- else zoom(action === "zoomOut" ? 1 / 1.2 : 1.2);
2691
- return true;
2692
- }
2693
- return false;
2694
- },
2695
- keyUp: (e) => {
2696
- if (e.key.toLowerCase() === keys.pan) setSpace(false);
2697
- },
2698
- down: (e) => {
2699
- if (e.button !== 1 && !space) return false;
2700
- e.preventDefault();
2701
- drag.current = {
2702
- id: e.pointerId,
2703
- x: e.clientX,
2704
- y: e.clientY,
2705
- view: viewRef.current
2706
- };
2707
- canvas.current.setPointerCapture(e.pointerId);
2708
- return true;
2709
- },
2710
- move: (e) => {
2711
- const d = drag.current;
2712
- if (!d || d.id !== e.pointerId) return false;
2713
- setView({
2714
- ...d.view,
2715
- x: d.view.x + e.clientX - d.x,
2716
- y: d.view.y + e.clientY - d.y
2717
- });
2718
- return true;
2890
+ hasContent,
2891
+ save,
2892
+ saveChanges,
2893
+ replace,
2894
+ fresh,
2895
+ load,
2896
+ importImage,
2897
+ restore
2898
+ };
2899
+ }
2900
+ //#endregion
2901
+ //#region src/sketch-operation-gate.js
2902
+ function createSketchOperationGate() {
2903
+ let running = false;
2904
+ return {
2905
+ get running() {
2906
+ return running;
2719
2907
  },
2720
- end: (e) => {
2721
- if (drag.current?.id !== e.pointerId) return false;
2722
- drag.current = null;
2723
- if (canvas.current.hasPointerCapture(e.pointerId)) canvas.current.releasePointerCapture(e.pointerId);
2724
- return true;
2908
+ async run(operation, { blocked = false, working, report, rethrow = false }) {
2909
+ if (blocked || running) return false;
2910
+ running = true;
2911
+ try {
2912
+ working(true);
2913
+ report(null);
2914
+ await operation();
2915
+ return true;
2916
+ } catch (error) {
2917
+ report(error);
2918
+ if (rethrow) throw error;
2919
+ return false;
2920
+ } finally {
2921
+ running = false;
2922
+ working(false);
2923
+ }
2725
2924
  }
2726
2925
  };
2727
2926
  }
2728
- function SketchViewControls({ navigation, t }) {
2729
- const [open, setOpen] = (0, react.useState)(false), [placement, setPlacement] = (0, react.useState)(null);
2730
- const host = (0, react.useRef)(null), trigger = (0, react.useRef)(null);
2731
- const close = () => {
2732
- setOpen(false);
2733
- trigger.current?.focus({ preventScroll: true });
2734
- };
2735
- useSketchDismiss(open, setOpen, host, [".codexSketchViewControls", ".codexSketchKeyPanel"]);
2736
- (0, react.useLayoutEffect)(() => {
2737
- if (!open) return;
2738
- const dialog = host.current.closest("dialog");
2739
- const place = () => {
2740
- const box = dialog.getBoundingClientRect(), anchor = trigger.current.getBoundingClientRect();
2741
- const width = Math.min(360, box.width - 24);
2742
- setPlacement({
2743
- dialog,
2744
- style: {
2745
- width,
2746
- left: Math.max(12, Math.min(anchor.left - box.left, box.width - width - 12)),
2747
- bottom: box.bottom - anchor.top + 8,
2748
- maxHeight: Math.max(80, anchor.top - box.top - 24)
2749
- }
2750
- });
2751
- };
2752
- place();
2753
- const observer = new ResizeObserver(place);
2754
- observer.observe(dialog);
2755
- observer.observe(host.current);
2756
- window.addEventListener("resize", place);
2757
- return () => {
2758
- observer.disconnect();
2759
- window.removeEventListener("resize", place);
2927
+ //#endregion
2928
+ //#region src/sketch-agent-export.js
2929
+ async function exportSketchAgentFile(format, { gate, blocked, working, report, exportFile }) {
2930
+ let result;
2931
+ if (!await gate.run(async () => {
2932
+ const { blob, extension } = await exportFile(format);
2933
+ const data = new Uint8Array(await blob.arrayBuffer());
2934
+ let raw = "";
2935
+ for (let i = 0; i < data.length; i += 8192) raw += String.fromCharCode(...data.subarray(i, i + 8192));
2936
+ result = {
2937
+ extension,
2938
+ mediaType: blob.type,
2939
+ base64: btoa(raw)
2760
2940
  };
2761
- }, [open]);
2941
+ }, {
2942
+ blocked,
2943
+ working,
2944
+ report,
2945
+ rethrow: true
2946
+ })) throw Error("Sketch is being edited; retry after it settles");
2947
+ return result;
2948
+ }
2949
+ //#endregion
2950
+ //#region src/sketch-run-status.jsx
2951
+ function SketchRunStatus({ state, t, floating = false, onOpen, onStop, onResume, onDismiss }) {
2952
+ if (state === "idle") return null;
2953
+ const drawing = state === "drawing", recover = state === "stopped" || state === "failed";
2762
2954
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2763
- ref: host,
2764
- className: "codexSketchViewControls",
2955
+ className: floating ? "codexSketchBackgroundStatus" : "codexSketchAgentStatus",
2956
+ role: "status",
2957
+ "aria-live": "polite",
2765
2958
  children: [
2766
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2767
- type: "button",
2768
- "aria-label": t("sketchZoomOut"),
2769
- onClick: () => navigation.zoom(1 / 1.2),
2770
- children: "−"
2959
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(WorkspaceIcon, {
2960
+ name: drawing ? "pen" : state === "finished" ? "check" : "rectangle",
2961
+ size: 15
2771
2962
  }),
2772
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
2963
+ floating ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2773
2964
  type: "button",
2774
- title: t("sketchFit"),
2775
- onClick: navigation.reset,
2776
- children: [Math.round(navigation.view.scale * 100), "%"]
2777
- }),
2778
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2965
+ onClick: onOpen,
2966
+ children: t(`sketchRun_${state}`)
2967
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t(`sketchRun_${state}`) }),
2968
+ drawing ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
2779
2969
  type: "button",
2780
- "aria-label": t("sketchZoomIn"),
2781
- onClick: () => navigation.zoom(1.2),
2782
- children: "+"
2783
- }),
2784
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2785
- ref: trigger,
2970
+ className: "codexSketchStop",
2971
+ onClick: onStop,
2972
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(WorkspaceIcon, {
2973
+ name: "stop",
2974
+ size: 12
2975
+ }), t("sketchRunStop")]
2976
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [recover ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2786
2977
  type: "button",
2787
- "aria-expanded": open,
2788
- onClick: () => setOpen(!open),
2789
- children: t("sketchKeys")
2790
- }),
2791
- open && placement ? (0, react_dom.createPortal)(/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
2792
- className: "codexSketchKeyPanel",
2793
- "aria-label": t("sketchKeys"),
2794
- style: placement.style,
2795
- onKeyDown: (e) => {
2796
- if (e.key === "Escape") {
2797
- e.preventDefault();
2798
- e.stopPropagation();
2799
- close();
2800
- }
2801
- },
2802
- children: [
2803
- /* @__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", {
2804
- type: "button",
2805
- "aria-label": t("sketchFileClose"),
2806
- onClick: close,
2807
- children: "×"
2808
- })] }),
2809
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
2810
- className: "codexSketchKeysEnabled",
2811
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("sketchKeysEnabled") }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
2812
- type: "checkbox",
2813
- checked: navigation.shortcuts,
2814
- onChange: navigation.toggle
2815
- })]
2816
- }),
2817
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", { children: t("sketchNavigationHint") }),
2818
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2819
- className: "codexSketchKeyGrid",
2820
- 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", {
2821
- "aria-label": t(`sketchKey_${action}`),
2822
- value: key === " " ? "Space" : key,
2823
- readOnly: true,
2824
- onKeyDown: (e) => {
2825
- if (e.key === "Tab" || e.key === "Escape") return;
2826
- e.preventDefault();
2827
- e.stopPropagation();
2828
- if (e.key.length === 1 && !e.ctrlKey && !e.metaKey && !e.altKey) navigation.setKey(action, e.key);
2829
- }
2830
- })] }, action))
2831
- }),
2832
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("small", { children: t("sketchKeyHint") })
2833
- ]
2834
- }), placement.dialog) : null
2978
+ title: t("sketchRunResumeHint"),
2979
+ onClick: onResume,
2980
+ children: t("sketchRunResume")
2981
+ }) : null, !recover ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2982
+ type: "button",
2983
+ "aria-label": t("sketchDismissStatus"),
2984
+ title: t("sketchDismissStatus"),
2985
+ onClick: onDismiss,
2986
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(WorkspaceIcon, {
2987
+ name: "close",
2988
+ size: 14
2989
+ })
2990
+ }) : null] })
2835
2991
  ]
2836
2992
  });
2837
2993
  }
2838
2994
  //#endregion
2995
+ //#region src/sketch-tool-widths.js
2996
+ const defaults = {
2997
+ pen: 12,
2998
+ pencil: 6,
2999
+ marker: 28,
3000
+ eraser: 24,
3001
+ text: 32,
3002
+ line: 12,
3003
+ arrow: 12,
3004
+ rectangle: 12,
3005
+ circle: 12
3006
+ };
3007
+ const keyFor = ({ tool, brush }) => tool === "pen" ? brush : tool;
3008
+ function switchSketchToolWidth(memory, current, next) {
3009
+ if (current.tool !== "select") memory[keyFor(current)] = current.width;
3010
+ if (next.tool === "select") return current.width;
3011
+ const key = keyFor(next);
3012
+ return memory[key] ?? defaults[key] ?? 12;
3013
+ }
3014
+ function stepSketchWidth(width, direction) {
3015
+ return Math.max(1, Math.min(256, width + direction * 2));
3016
+ }
3017
+ //#endregion
3018
+ //#region src/sketch-layer-renderer.js
3019
+ const NO_IMAGES = /* @__PURE__ */ new Map();
3020
+ const surface = (width, height) => {
3021
+ const c = document.createElement("canvas");
3022
+ c.width = width;
3023
+ c.height = height;
3024
+ return c;
3025
+ };
3026
+ function paintSketchLayers(context, doc, cache, size = doc.width ?? 1024, height = doc.height ?? size, activeLayer, images = NO_IMAGES) {
3027
+ context.globalCompositeOperation = "source-over";
3028
+ context.globalAlpha = 1;
3029
+ context.fillStyle = "#fff";
3030
+ context.fillRect(0, 0, size, height);
3031
+ for (const id of cache.keys()) if (!doc.layers.some((layer) => layer.id === id)) cache.delete(id);
3032
+ for (const layer of doc.layers) {
3033
+ if (!layer.visible) continue;
3034
+ let entry = cache.get(layer.id);
3035
+ if (!entry || entry.surface.width !== size || entry.surface.height !== height) {
3036
+ entry = {
3037
+ surface: surface(size, height),
3038
+ base: surface(size, height)
3039
+ };
3040
+ cache.set(layer.id, entry);
3041
+ }
3042
+ const moving = layer.id === activeLayer;
3043
+ const count = Math.max(0, layer.strokes.length - (moving ? 1 : 0));
3044
+ const prefix = layer.strokes[count - 1];
3045
+ if (entry.count !== count || entry.prefix !== prefix || entry.image !== layer.image || entry.strokes !== layer.strokes) {
3046
+ const ctx = entry.base.getContext("2d");
3047
+ 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]));
3048
+ if (!append) {
3049
+ ctx.clearRect(0, 0, size, height);
3050
+ const ref = layer.image, image = ref && images.get(ref.src);
3051
+ if (image) ctx.drawImage(image, ref.x * size, ref.y * height, ref.width * size, ref.height * height);
3052
+ }
3053
+ paintSketch(ctx, layer.strokes, size, true, height, append ? entry.count : 0, count);
3054
+ entry.count = count;
3055
+ entry.prefix = prefix;
3056
+ entry.image = layer.image;
3057
+ entry.strokes = layer.strokes;
3058
+ }
3059
+ if (moving) {
3060
+ const ctx = entry.surface.getContext("2d");
3061
+ ctx.clearRect(0, 0, size, height);
3062
+ ctx.drawImage(entry.base, 0, 0);
3063
+ paintSketch(ctx, layer.strokes, size, true, height, layer.strokes.length - 1);
3064
+ context.drawImage(entry.surface, 0, 0);
3065
+ } else context.drawImage(entry.base, 0, 0);
3066
+ }
3067
+ }
3068
+ //#endregion
2839
3069
  //#region src/sketch-files.jsx
2840
3070
  function SketchFiles({ save, load, fresh, importImage, download, hasContent, disabled, t, runOperation }) {
2841
3071
  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);
@@ -3248,17 +3478,9 @@ window.__ModuleLoader__.load({
3248
3478
  }
3249
3479
  //#endregion
3250
3480
  //#region src/sketch-studio.jsx
3251
- const PALETTE = [
3252
- "#18181b",
3253
- "#929398",
3254
- "#ff3936",
3255
- "#ff9500",
3256
- "#ffcc00",
3257
- "#34c759",
3258
- "#0088ff"
3259
- ];
3260
3481
  function SketchStudio({ open, agentEnabled, agentPreview, onOpen, onClose, attachSketch, enabled, t, incoming, sessionId, rpc, sessionState }) {
3261
3482
  const localSession = (0, react.useRef)(null);
3483
+ const psdImport = useSketchImportConfirm(t);
3262
3484
  localSession.current ??= sessionState ?? createSketchSessionState();
3263
3485
  const { doc, undo, redo, images, saved, dirty, documentId, documentRevision, agentAdapter, agentSession, agentRun } = localSession.current;
3264
3486
  const dialog = (0, react.useRef)(null), canvas = (0, react.useRef)(null), cache = (0, react.useRef)(/* @__PURE__ */ new Map());
@@ -3377,6 +3599,7 @@ window.__ModuleLoader__.load({
3377
3599
  cache,
3378
3600
  setSelection,
3379
3601
  setTextEdit,
3602
+ confirmPsd: psdImport.confirmPsd,
3380
3603
  setRecovered
3381
3604
  });
3382
3605
  (0, react.useEffect)(() => localSession.current.retain?.(), []);
@@ -4133,47 +4356,14 @@ window.__ModuleLoader__.load({
4133
4356
  })
4134
4357
  ]
4135
4358
  }) : null,
4136
- picturesOpen ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("aside", {
4137
- className: "codexSketchPictures",
4138
- children: [
4139
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("header", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("strong", { children: t("sketchPictures") }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
4140
- type: "button",
4141
- disabled: agentLocked || busy,
4142
- onClick: () => pictureInput.current.click(),
4143
- children: t("sketchPictureAdd")
4144
- })] }),
4145
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
4146
- ref: pictureInput,
4147
- hidden: true,
4148
- type: "file",
4149
- accept: "image/png,image/jpeg,image/webp",
4150
- onChange: (e) => {
4151
- const file = e.target.files?.[0];
4152
- e.target.value = "";
4153
- if (file) runFile(() => importImage(file));
4154
- }
4155
- }),
4156
- doc.current.layers.filter((layer) => layer.image).map((layer) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
4157
- "data-active": layer.id === doc.current.active,
4158
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
4159
- type: "button",
4160
- disabled: agentLocked || busy,
4161
- "aria-label": `${t("sketchPictureSelect")} ${layer.name}`,
4162
- onClick: () => change("select", layer.id),
4163
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("img", {
4164
- src: layer.image.src,
4165
- alt: layer.name
4166
- })
4167
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
4168
- type: "button",
4169
- disabled: agentLocked || busy,
4170
- "aria-label": `${t("sketchDeleteDraft")} ${layer.name}`,
4171
- onClick: () => change(doc.current.layers.length === 1 ? "clear" : "delete", layer.id),
4172
- children: "×"
4173
- })]
4174
- }, layer.id)),
4175
- !doc.current.layers.some((layer) => layer.image) ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("small", { children: t("sketchPicturesEmpty") }) : null
4176
- ]
4359
+ picturesOpen ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SketchPicturePanel, {
4360
+ document: doc.current,
4361
+ disabled: agentLocked || busy,
4362
+ pictureInput,
4363
+ importImage,
4364
+ runFile,
4365
+ change,
4366
+ t
4177
4367
  }) : null,
4178
4368
  layersOpen ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SketchLayerPanel, {
4179
4369
  document: doc.current,
@@ -4183,119 +4373,30 @@ window.__ModuleLoader__.load({
4183
4373
  }) : null
4184
4374
  ]
4185
4375
  }),
4186
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
4187
- className: "codexSketchControls",
4188
- children: [
4189
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
4190
- type: "button",
4191
- className: "codexSketchPicturesToggle",
4192
- "aria-expanded": picturesOpen,
4193
- onClick: () => setPicturesOpen((v) => !v),
4194
- children: t("sketchPictures")
4195
- }),
4196
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SketchViewControls, {
4197
- navigation,
4198
- t
4199
- }),
4200
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SketchToolPicker, {
4201
- t,
4202
- disabled: agentLocked || busy,
4203
- tool,
4204
- brush,
4205
- chooseBrush,
4206
- chooseTool,
4207
- shapesOpen,
4208
- setShapesOpen
4209
- }),
4210
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
4211
- className: "codexLayerBrush",
4212
- children: [
4213
- ["rectangle", "circle"].includes(tool) ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
4214
- type: "checkbox",
4215
- disabled: agentLocked || busy,
4216
- checked: fillShape,
4217
- onChange: (e) => setFillShape(e.target.checked)
4218
- }), t("sketchFill")] }) : null,
4219
- selected ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
4220
- className: "codexSketchObjectActions",
4221
- children: [
4222
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
4223
- disabled: agentLocked || busy,
4224
- onClick: () => editObject({}, "duplicate"),
4225
- children: t("sketchObjectDuplicate")
4226
- }),
4227
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
4228
- disabled: agentLocked || busy,
4229
- onClick: () => editObject({}, "delete"),
4230
- children: t("sketchObjectDelete")
4231
- }),
4232
- selected.shape === "text" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
4233
- disabled: agentLocked || busy,
4234
- onClick: () => setTextEdit({
4235
- selection,
4236
- value: selected.text
4237
- }),
4238
- children: t("sketchText")
4239
- }) : null
4240
- ]
4241
- }) : null,
4242
- tool === "pen" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
4243
- className: "codexSketchStability",
4244
- children: [t("sketchStability"), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("select", {
4245
- "aria-label": t("sketchStability"),
4246
- value: stability,
4247
- disabled: agentLocked || busy,
4248
- onChange: (e) => setStability(Number(e.target.value)),
4249
- children: [
4250
- 0,
4251
- 25,
4252
- 50,
4253
- 75
4254
- ].map((value) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
4255
- value,
4256
- children: t(`sketchStability${value}`)
4257
- }, value))
4258
- })]
4259
- }) : null,
4260
- tool === "eraser" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
4261
- className: "codexSketchSegment",
4262
- role: "group",
4263
- "aria-label": t("sketchEraserMode"),
4264
- children: ["pixel", "stroke"].map((value) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
4265
- type: "button",
4266
- "aria-pressed": eraser === value,
4267
- disabled: agentLocked || busy,
4268
- onClick: () => setEraser(value),
4269
- children: t(`sketchErase_${value}`)
4270
- }, value))
4271
- }) : null
4272
- ]
4273
- }),
4274
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
4275
- className: "codexSketchPalette",
4276
- role: "group",
4277
- "aria-label": t("sketchColor"),
4278
- children: [PALETTE.map((value) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
4279
- type: "button",
4280
- className: "codexSketchSwatch",
4281
- style: { "--swatch": value },
4282
- "aria-label": `${t("sketchColor")} ${value}`,
4283
- "aria-pressed": (selected?.color ?? color) === value,
4284
- disabled: agentLocked || busy,
4285
- onClick: () => pickColor(value)
4286
- }, value)), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
4287
- className: "codexSketchCustom",
4288
- title: t("sketchColor"),
4289
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { style: { background: color } }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
4290
- type: "color",
4291
- "aria-label": t("sketchColor"),
4292
- value: color,
4293
- disabled: agentLocked || busy,
4294
- onChange: (e) => pickColor(e.target.value)
4295
- })]
4296
- })]
4297
- })
4298
- ]
4376
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SketchControls, {
4377
+ t,
4378
+ navigation,
4379
+ picturesOpen,
4380
+ setPicturesOpen,
4381
+ disabled: agentLocked || busy,
4382
+ tool,
4383
+ brush,
4384
+ chooseBrush,
4385
+ chooseTool,
4386
+ shapesOpen,
4387
+ setShapesOpen,
4388
+ fillShape,
4389
+ setFillShape,
4390
+ selected,
4391
+ editObject,
4392
+ selection,
4393
+ setTextEdit,
4394
+ stability,
4395
+ setStability,
4396
+ eraser,
4397
+ setEraser,
4398
+ color,
4399
+ pickColor
4299
4400
  }),
4300
4401
  error ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
4301
4402
  className: "codexSketchHint",
@@ -4305,13 +4406,15 @@ window.__ModuleLoader__.load({
4305
4406
  onClick: () => retryRestore((value) => value + 1),
4306
4407
  children: t("accountRetry")
4307
4408
  }) : null]
4308
- }) : null
4409
+ }) : null,
4410
+ psdImport.prompt
4309
4411
  ]
4310
4412
  })] });
4311
4413
  }
4312
4414
  //#endregion
4313
4415
  //#region src/sketch-styles.js
4314
4416
  const SKETCH_CSS = `
4417
+ .codexSketchImportConfirm{width:min(440px,calc(100vw - 32px));box-sizing:border-box;padding:24px;border:1px solid var(--sketch-line);border-radius:20px;background:var(--sketch-bg);color:var(--sketch-fg);box-shadow:0 12px 60px #0005}.codexSketchImportConfirm::backdrop{background:#0007}.codexSketchImportConfirm h3{margin:0 0 12px;font-size:16px}.codexSketchImportConfirm p{font-size:14px;line-height:1.7;color:var(--sketch-muted)}.codexSketchImportConfirm>div{display:flex;justify-content:flex-end;gap:12px}.codexSketchImportConfirm button{padding:8px 16px;border-radius:12px;border:1px solid var(--sketch-line);background:var(--sketch-line);color:var(--sketch-fg)}
4315
4418
  .codexSketchSizeModes{display:flex;flex-direction:column;gap:3px}.codexSketchSizeModes button{font-size:11px;padding:4px 6px;border-radius:10px;color:var(--sketch-muted)}.codexSketchSizeModes button[aria-pressed=true]{background:var(--sketch-line);color:var(--sketch-fg)}
4316
4419
 
4317
4420
  .codexSketchSizeControl{position:absolute;left:10px;top:50%;transform:translateY(-50%);z-index:2;display:flex;flex-direction:column;align-items:center;gap:10px;padding:12px 6px;border:1px solid var(--sketch-line);border-radius:24px;background:var(--sketch-glass);backdrop-filter:blur(18px);box-shadow:0 3px 14px #0001}
@@ -4524,6 +4627,59 @@ window.__ModuleLoader__.load({
4524
4627
  //#endregion
4525
4628
  //#region src/client-locales.js
4526
4629
  const zh = {
4630
+ "diagnosticReason_catalog-online": "在线目录已载入",
4631
+ "diagnosticReason_catalog-fallback": "使用备用目录",
4632
+ "diagnosticReason_storage-readable": "预测缓存可读",
4633
+ "diagnosticReason_tool-registered": "全局工具已注册",
4634
+ "diagnosticReason_tool-missing": "未找到全局工具",
4635
+ "diagnosticsCoverageHint": "分别展示已检查的前置条件与最近 15 分钟的操作结果。请求完成不代表整个功能通过验收;未验证不等于故障。",
4636
+ "diagnosticsVersionMismatch": "前后端插件版本不同,请重载后重试。",
4637
+ "diagnosticsPartial": "部分信息未能采集,其余检查结果已保留。请在详情中查看失败或超时项。",
4638
+ "diagnosticsCapability": "功能",
4639
+ "diagnosticsReadiness": "前置条件",
4640
+ "diagnosticsEvidence": "近期操作证据",
4641
+ "diagnosticsUnverified": "尚未验证",
4642
+ "diagnosticsHostCollected": "已采集宿主注册与选中结果(详情内)。这不证明界面可见或功能执行正常。",
4643
+ "diagnosticsHostUnknown": "宿主界面检查不可用或未挂载,不能据此判断插件干扰。",
4644
+ "diagnosticsDetails": "展开脱敏详情",
4645
+ "diagnosticsCopyFailed": "复制失败,可展开详情手动复制。",
4646
+ "diagnosticSource_rpc": "接口响应",
4647
+ "diagnosticSource_tool": "工具",
4648
+ "diagnosticOutcome_completed": "完成",
4649
+ "diagnosticOutcome_failed": "失败",
4650
+ "diagnosticOutcome_cancelled": "取消",
4651
+ "diagnosticCapability_account": "登录",
4652
+ "diagnosticCapability_accounts": "多账号",
4653
+ "diagnosticCapability_settings": "设置",
4654
+ "diagnosticCapability_models": "模型",
4655
+ "diagnosticCapability_quota": "额度",
4656
+ "diagnosticCapability_search": "搜索",
4657
+ "diagnosticCapability_images": "生图与编辑",
4658
+ "diagnosticCapability_sketch": "草图",
4659
+ "diagnosticCapability_subagents": "子任务",
4660
+ "diagnosticCapability_compaction": "压缩",
4661
+ "diagnosticCapability_transport": "连接",
4662
+ "diagnosticCapability_storage": "存储",
4663
+ "diagnosticCapability_host-ui": "宿主界面",
4664
+ "diagnosticReason_not-instrumented": "证据不足",
4665
+ "diagnosticReason_disabled-or-host-owned": "未启用或由宿主管理",
4666
+ "diagnosticReason_sign-in-required": "依赖登录",
4667
+ "diagnosticReason_local-sign-in-present": "本地登录存在",
4668
+ "diagnosticReason_sign-in-unverified": "登录未确认",
4669
+ "diagnosticReason_settings-writable": "可写",
4670
+ "diagnosticReason_settings-readonly": "不可写",
4671
+ "diagnosticReason_restart-required": "需要重启",
4672
+ "diagnosticReason_runtime-present": "组件已安装",
4673
+ "diagnosticReason_runtime-missing": "组件未安装",
4674
+ storageLoading: "正在读取缓存占用…",
4675
+ storageTitle: "存储与缓存",
4676
+ storageForecast: "额度预测缓存",
4677
+ storageClear: "清除预测历史",
4678
+ storageHint: "仅清理本地预测历史,保留原图、草稿、登录信息和共享依赖。子任务组件可在“模型与运行”中管理。",
4679
+ storageConfirm: "清除所有已登录账号的本地预测历史后,需要重新积累采样。服务端额度不变,后续查询会重新生成缓存。",
4680
+ storageFailed: "未能读取或清除预测缓存,请重试。",
4681
+ sketchPsdConfirmTitle: "导入 PSD 副本",
4682
+ sketchPsdConfirmBody: "支持最多 8 层、8 位 RGB 的简单 PSD(32 MB 内,边长不超过 4096)。导入后各层转为图片,最长边缩至 1024;不支持蒙版、调整层和特殊混合效果。原文件不会修改。需要保留草图对象的可编辑性,请使用 DSH 草图格式。继续导入?",
4527
4683
  runtimeIncompatible: "检测到不兼容的 Codex 子任务组件,请卸载后重启,再安装受支持版本。",
4528
4684
  runtimeManagedElsewhere: "此组件由宿主或其他依赖管理,请在 DSH 插件管理中查看;这里不能直接卸载。",
4529
4685
  runtimeLoading: "正在检查组件…",
@@ -4891,9 +5047,16 @@ window.__ModuleLoader__.load({
4891
5047
  contextTokens: "Token 上限",
4892
5048
  contextFixed: "固定 {value}",
4893
5049
  contextMaximum: "范围 {minimum}–{value}",
4894
- settingsTab_account: "账号与偏好",
5050
+ settingsTab_account: "账号与额度",
4895
5051
  settingsTab_display: "使用偏好",
4896
- settingsTab_advanced: "高级与诊断",
5052
+ settingsTab_advanced: "模型与运行",
5053
+ settingsTab_creative: "图片与草图",
5054
+ settingsTab_maintenance: "维护",
5055
+ creativeSketch: "草图画板",
5056
+ diagnosticsHint: "遇到问题时生成报告,可复制后用于反馈。不含登录凭据和账号标识。",
5057
+ sketchCanvasSetting: "画板入口",
5058
+ sketchAgentSetting: "Agent 绘图",
5059
+ sketchAgentPreviewSetting: "完成后返回预览",
4897
5060
  quotaShowIndicator: "显示额度",
4898
5061
  quotaIndicatorHint: "输入框只显示图标,点击查看详情。关闭时暂停续航采样。",
4899
5062
  quotaForecastOptional: "续航预测 · 实验",
@@ -4980,6 +5143,59 @@ window.__ModuleLoader__.load({
4980
5143
  imageRemoveAnnotation: "删除标注"
4981
5144
  };
4982
5145
  const en = {
5146
+ "diagnosticReason_catalog-online": "Online catalog loaded",
5147
+ "diagnosticReason_catalog-fallback": "Fallback catalog",
5148
+ "diagnosticReason_storage-readable": "Forecast cache readable",
5149
+ "diagnosticReason_tool-registered": "Global tool registered",
5150
+ "diagnosticReason_tool-missing": "Global tool not found",
5151
+ "diagnosticsCoverageHint": "Shows inspected prerequisites and operations observed in the last 15 minutes. Request completion is not end-to-end validation; unverified does not mean broken.",
5152
+ "diagnosticsVersionMismatch": "Client and server versions differ. Reload and retry.",
5153
+ "diagnosticsPartial": "Some inspections failed or timed out. Other results are preserved; see details.",
5154
+ "diagnosticsCapability": "Capability",
5155
+ "diagnosticsReadiness": "Prerequisites",
5156
+ "diagnosticsEvidence": "Recent evidence",
5157
+ "diagnosticsUnverified": "Unverified",
5158
+ "diagnosticsHostCollected": "Host registrations and selection are included in details, not proof of visibility or execution.",
5159
+ "diagnosticsHostUnknown": "Host inspection unavailable or unmounted; interference cannot be determined.",
5160
+ "diagnosticsDetails": "Redacted details",
5161
+ "diagnosticsCopyFailed": "Copy failed. Expand the details to copy manually.",
5162
+ "diagnosticSource_rpc": "RPC response",
5163
+ "diagnosticSource_tool": "Tool",
5164
+ "diagnosticOutcome_completed": "Completed",
5165
+ "diagnosticOutcome_failed": "Failed",
5166
+ "diagnosticOutcome_cancelled": "Cancelled",
5167
+ "diagnosticCapability_account": "Sign-in",
5168
+ "diagnosticCapability_accounts": "Accounts",
5169
+ "diagnosticCapability_settings": "Settings",
5170
+ "diagnosticCapability_models": "Models",
5171
+ "diagnosticCapability_quota": "Quota",
5172
+ "diagnosticCapability_search": "Search",
5173
+ "diagnosticCapability_images": "Images",
5174
+ "diagnosticCapability_sketch": "Sketch",
5175
+ "diagnosticCapability_subagents": "Subtasks",
5176
+ "diagnosticCapability_compaction": "Compaction",
5177
+ "diagnosticCapability_transport": "Connection",
5178
+ "diagnosticCapability_storage": "Storage",
5179
+ "diagnosticCapability_host-ui": "Host UI",
5180
+ "diagnosticReason_not-instrumented": "Insufficient evidence",
5181
+ "diagnosticReason_disabled-or-host-owned": "Disabled or host-managed",
5182
+ "diagnosticReason_sign-in-required": "Sign-in required",
5183
+ "diagnosticReason_local-sign-in-present": "Local sign-in present",
5184
+ "diagnosticReason_sign-in-unverified": "Sign-in unverified",
5185
+ "diagnosticReason_settings-writable": "Writable",
5186
+ "diagnosticReason_settings-readonly": "Read-only",
5187
+ "diagnosticReason_restart-required": "Restart required",
5188
+ "diagnosticReason_runtime-present": "Runtime installed",
5189
+ "diagnosticReason_runtime-missing": "Runtime missing",
5190
+ storageLoading: "Reading cache size…",
5191
+ storageTitle: "Storage and cache",
5192
+ storageForecast: "Quota forecast cache",
5193
+ storageClear: "Clear forecast history",
5194
+ storageHint: "Clears local forecast history only. Original images, drafts, sign-in data and shared dependencies are preserved. Manage subtask components under Models & runtime.",
5195
+ storageConfirm: "This clears local forecast samples for all signed-in accounts. Forecasts will need new samples. Server quotas stay unchanged; subsequent checks recreate the cache.",
5196
+ storageFailed: "Could not read or clear the forecast cache. Try again.",
5197
+ sketchPsdConfirmTitle: "Import a PSD copy",
5198
+ sketchPsdConfirmBody: "Supports simple 8-bit RGB PSDs with up to 8 layers, 32 MB and 4096 pixels per side. Layers become images and the longest side is reduced to 1024 pixels. Masks, adjustments and special blending effects are unsupported. The original file stays unchanged. Use the DSH sketch format to preserve editable sketch objects. Continue?",
4983
5199
  runtimeIncompatible: "An incompatible Codex subtask component was found. Uninstall it, restart DSH, then install the supported version.",
4984
5200
  runtimeManagedElsewhere: "This component is managed by the host or another dependency. Check DSH plugin management; it cannot be removed here.",
4985
5201
  runtimeLoading: "Checking component…",
@@ -5347,9 +5563,16 @@ window.__ModuleLoader__.load({
5347
5563
  contextTokens: "Token limit",
5348
5564
  contextFixed: "Fixed {value}",
5349
5565
  contextMaximum: "{minimum}–{value}",
5350
- settingsTab_account: "Account & preferences",
5566
+ settingsTab_account: "Account",
5351
5567
  settingsTab_display: "Preferences",
5352
- settingsTab_advanced: "Advanced",
5568
+ settingsTab_advanced: "Models & runtime",
5569
+ settingsTab_creative: "Images & sketch",
5570
+ settingsTab_maintenance: "Maintenance",
5571
+ creativeSketch: "Sketch board",
5572
+ diagnosticsHint: "Generate a report to copy into a support request. Sign-in credentials and account identifiers are excluded.",
5573
+ sketchCanvasSetting: "Board entry",
5574
+ sketchAgentSetting: "Agent drawing",
5575
+ sketchAgentPreviewSetting: "Return preview on completion",
5353
5576
  quotaShowIndicator: "Show quota",
5354
5577
  quotaIndicatorHint: "Show an icon in the composer; click for details. Hiding it pauses forecast sampling.",
5355
5578
  quotaForecastOptional: "Runway forecast · Experimental",
@@ -5530,7 +5753,7 @@ window.__ModuleLoader__.load({
5530
5753
  .codexSubscriptionRecover{display:flex;align-items:center;justify-content:space-between;gap:12px}
5531
5754
  .codexSubscriptionRecover .codexSubscriptionError{flex:1}
5532
5755
  .codexSubscriptionRecover button{flex:0 0 auto}
5533
- .codexSubscriptionDiagnostics{padding:8px 12px;gap:8px;background:transparent;color:var(--dsw-alias-label-secondary)}
5756
+ .codexSubscriptionDiagnostics{color:var(--dsw-alias-label-secondary)}
5534
5757
  .codexSubscriptionDiagnostics pre{max-height:240px;margin:0;padding:10px 12px;border-radius:8px;background:var(--dsw-alias-bg-module-platform);overflow:auto;white-space:pre-wrap;overflow-wrap:anywhere;font:11px/17px ui-monospace,SFMono-Regular,Consolas,monospace;color:var(--dsw-alias-label-secondary)}
5535
5758
  .codexSubscriptionLink{box-sizing:border-box;display:inline-flex;align-items:center;justify-content:center;min-height:32px;padding:0 13px;border:1px solid var(--dsw-alias-border-l2);border-radius:999px;background:transparent;color:var(--dsw-alias-label-primary);font-size:13px;line-height:20px;text-decoration:none;white-space:nowrap}
5536
5759
  .codexSubscriptionLink:hover{background:var(--dsw-alias-bg-module-platform)}
@@ -5588,8 +5811,9 @@ window.__ModuleLoader__.load({
5588
5811
  .codexSubscriptionSpendLimit progress::-webkit-progress-bar{background:var(--dsw-alias-border-l3);border-radius:999px}
5589
5812
  .codexSubscriptionSpendLimit progress::-webkit-progress-value{background:var(--dsw-alias-brand-primary,#3964fe);border-radius:999px}
5590
5813
  .codexSubscriptionSpendLimit progress::-moz-progress-bar{background:var(--dsw-alias-brand-primary,#3964fe);border-radius:999px}
5591
- .codexSettingsTabs{display:flex;gap:4px;padding:3px;background:var(--dsw-alias-bg-module-platform);border-radius:10px}
5592
- .codexSettingsTabs button{flex:1;border:0;border-radius:8px;padding:7px 8px;background:transparent;color:var(--dsw-alias-label-secondary);font:inherit;font-size:13px;cursor:pointer}
5814
+ .codexSettingsTabs{position:sticky;top:0;z-index:2;display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:4px;padding:4px;background:var(--dsw-alias-bg-module-platform);border-radius:12px}
5815
+ .codexSettingsTabs button{min-width:0;min-height:36px;border:0;border-radius:9px;padding:7px 6px;background:transparent;color:var(--dsw-alias-label-secondary);font:inherit;font-size:12px;line-height:18px;cursor:pointer}
5816
+ .codexSettingsTabs button:focus-visible{outline:2px solid var(--dsw-alias-border-l3);outline-offset:1px}
5593
5817
  .codexSettingsTabs button[aria-selected=true]{background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-primary);box-shadow:0 1px 3px #0001}
5594
5818
  .codexSubscription>[role=tabpanel]:not([hidden]){display:flex;flex-direction:column;gap:10px}
5595
5819
  .codexComposerQuota:focus-visible{outline:1px solid var(--dsw-alias-border-l3);outline-offset:2px}
@@ -5616,7 +5840,7 @@ window.__ModuleLoader__.load({
5616
5840
  .codexModelSelectEffort{flex:none;color:var(--dsw-alias-label-caption)}
5617
5841
  .codexModelSelectChevron{flex:none;color:var(--dsw-alias-label-caption);transition:transform 120ms}
5618
5842
  .codexModelSelectTrigger[aria-expanded=true] .codexModelSelectChevron{transform:rotate(180deg)}
5619
- .codexModelSelectMenu,.codexModelSelectSubmenu{position:absolute;z-index:30;box-sizing:border-box;width:max-content;min-width:min(240px,calc(100vw - 32px));max-width:min(420px,calc(100vw - 32px));max-height:min(360px,calc(100vh - 96px));padding:4px;border:1px solid var(--dsw-alias-border-inverted);border-radius:12px;background:var(--dsw-specific-menu);box-shadow:var(--dsw-shadow-lv3);color:var(--dsw-alias-label-primary);overflow:hidden}
5843
+ .codexModelSelectMenu,.codexModelSelectSubmenu{position:absolute;z-index:30;box-sizing:border-box;width:max-content;min-width:min(240px,calc(100vw - 32px));max-width:min(420px,calc(100vw - 32px));max-height:min(360px,calc(100vh - 96px));padding:4px;border:1px solid var(--dsw-alias-border-inverted);border-radius:12px;background:var(--dsw-alias-bg-layer-2,#2c2c2e);box-shadow:var(--dsw-shadow-lv3);color:var(--dsw-alias-label-primary);overflow:hidden}
5620
5844
  .codexModelSelectMenu{right:0;bottom:calc(100% + 8px)}
5621
5845
  .codexModelSelectSubmenu{right:calc(100% + 8px);bottom:0;min-width:min(230px,calc(100vw - 32px))}
5622
5846
  .codexModelSelectCell{display:flex;align-items:center;gap:8px;width:100%;min-width:100%;height:40px;box-sizing:border-box;padding:0 10px;border:0;border-radius:10px;background:transparent;color:inherit;font-size:14px;line-height:22px;text-align:left;cursor:pointer}
@@ -5627,7 +5851,7 @@ window.__ModuleLoader__.load({
5627
5851
  .codexModelSelectCellChevron{flex:none;color:var(--dsw-alias-label-tertiary)}
5628
5852
  .codexModelSelectGroups{min-height:0;max-height:352px;overflow-y:auto}
5629
5853
  .codexModelSelectGroup+.codexModelSelectGroup{margin-top:4px}
5630
- .codexModelSelectGroupTitle{position:sticky;top:0;z-index:1;padding:5px 8px 3px;background:var(--dsw-specific-menu);color:var(--dsw-alias-label-tertiary);font-size:12px;font-weight:500;line-height:18px}
5854
+ .codexModelSelectGroupTitle{position:sticky;top:0;z-index:1;padding:5px 8px 3px;background:var(--dsw-alias-bg-layer-2,#2c2c2e);color:var(--dsw-alias-label-tertiary);font-size:12px;font-weight:500;line-height:18px}
5631
5855
  .codexModelSelectOption{display:flex;align-items:center;gap:8px;width:100%;min-width:100%;min-height:38px;box-sizing:border-box;padding:6px 8px;border:0;border-radius:10px;outline:0;background:transparent;color:inherit;text-align:left;cursor:pointer}
5632
5856
  .codexModelSelectOption:hover:not(:disabled),.codexModelSelectOption:focus-visible{background:var(--dsw-alias-interactive-bg-hover)}
5633
5857
  .codexModelSelectOption:disabled{color:var(--dsw-alias-label-dimmed);cursor:default}
@@ -5669,6 +5893,25 @@ window.__ModuleLoader__.load({
5669
5893
  .codexSubscriptionRuntime button{flex-shrink:0;white-space:nowrap}
5670
5894
  .codexSubscriptionRuntime code{display:block;overflow-wrap:anywhere;padding:8px 10px;border:1px solid var(--dsw-alias-border-l2);border-radius:8px;user-select:text}
5671
5895
  @media(max-width:640px){.codexSubscriptionCard{padding:14px}}
5896
+ .codexSubscriptionHelp{font-size:12px;line-height:1.65;color:var(--dsw-alias-label-tertiary)}
5897
+ .codexSubscriptionBadge{flex:none;padding:3px 8px;border-radius:6px;background:var(--dsw-alias-bg-module-platform);font-size:11px;line-height:16px;color:var(--dsw-alias-label-secondary)}
5898
+ .codexImageSettings>.codexSubscriptionSectionHead{flex-direction:row;align-items:center}
5899
+ .codexSubscriptionSettingRows>.codexImagePreference+.codexImagePreference{border-top:1px solid var(--dsw-alias-border-l2)}
5900
+ .codexSubscriptionDiagnostics .codexSubscriptionSectionHead{align-items:flex-start;flex-direction:column;gap:12px}
5901
+ .codexSubscriptionDiagnostics .codexSubscriptionSectionTitle{display:flex;flex-direction:column;gap:5px}
5902
+ .codexSubscriptionStorageMeter{display:flex;flex-direction:column;gap:9px;padding:12px;border-radius:10px;background:var(--dsw-alias-bg-module-platform)}
5903
+ .codexSubscriptionStorageMeter>div{display:flex;justify-content:space-between;gap:8px;flex-wrap:wrap;font-size:12px;line-height:18px}
5904
+ .codexSubscriptionStorageMeter output{font-variant-numeric:tabular-nums;color:var(--dsw-alias-label-secondary)}
5905
+ .codexSubscriptionStorageMeter progress{width:100%;height:4px;border:0;border-radius:4px;overflow:hidden;accent-color:var(--dsw-alias-label-secondary)}
5906
+ .codexSubscriptionStorageMeter progress::-webkit-progress-bar{background:var(--dsw-alias-border-l2)}
5907
+ .codexSubscriptionStorageMeter progress::-webkit-progress-value{background:var(--dsw-alias-label-secondary)}
5908
+ .codexSubscriptionConfirm{padding:12px;border:1px solid var(--dsw-alias-border-l2);border-radius:10px;font-size:12px;line-height:1.65}
5909
+ .codexSubscriptionConfirm p{margin-bottom:12px}
5910
+ .codexSubscriptionRuntime{padding-top:10px;border-top:1px solid var(--dsw-alias-border-l2);margin-top:2px}
5911
+ .codexSubscriptionRuntime details{margin-top:8px}.codexSubscriptionRuntime details[open]>summary{margin-bottom:10px}
5912
+ @container (max-width:480px){.codexSubscriptionPreference,.codexSubscriptionContextHead{flex-wrap:wrap}.codexSubscriptionPreferenceHint{max-width:none}.codexSubscriptionPreference>.codexSubscriptionPreferenceCopy{flex:1 1 220px}.codexSubscriptionQuotaModes{max-width:100%;flex-wrap:wrap}.codexSubscriptionQuotaMode{min-height:30px}.codexSubscriptionActions{flex-wrap:wrap}}
5913
+ @container (max-width:340px){.codexSettingsTabs{grid-template-columns:repeat(2,minmax(0,1fr))}}
5914
+ @media(prefers-reduced-motion:reduce){.codexSubscription *{transition:none!important}}
5672
5915
  `;
5673
5916
  //#endregion
5674
5917
  //#region src/image-edit-reference.js
@@ -7290,7 +7533,7 @@ window.__ModuleLoader__.load({
7290
7533
  }
7291
7534
  //#endregion
7292
7535
  //#region src/version.js
7293
- const PACKAGE_VERSION = "2.2.1";
7536
+ const PACKAGE_VERSION = "2.2.3";
7294
7537
  //#endregion
7295
7538
  //#region src/client-recovery.js
7296
7539
  async function recoveryCall(rpc, endpoint, payload = {}, timeoutMs = 1e4) {
@@ -7925,29 +8168,30 @@ window.__ModuleLoader__.load({
7925
8168
  });
7926
8169
  }
7927
8170
  const modelLabel = (model) => model.replace("gpt-image-", "GPT Image ").replace("-flare", " Flare").replace("-sunburst", " Sunburst");
7928
- function ImagePreferences({ preference, t }) {
8171
+ function ImagePreferences({ preference, t, section = "image" }) {
7929
8172
  const snapshot = (0, react.useSyncExternalStore)(preference.subscribe, preference.getSnapshot);
7930
8173
  const disabled = snapshot.status !== "ready" || !snapshot.writable || snapshot.saving;
7931
8174
  const active = snapshot.imageGeneration || snapshot.imageEditing;
7932
- return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("section", {
8175
+ const sketch = section === "sketch";
8176
+ const title = t(sketch ? "creativeSketch" : "imageSettings");
8177
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
7933
8178
  className: "codexSubscriptionCard codexImageSettings",
7934
- "aria-label": t("imageSettings"),
7935
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("details", {
7936
- className: "codexSubscriptionSettingsDisclosure",
7937
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("summary", { children: [
7938
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("imageSettings") }),
7939
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
7940
- className: "codexSubscriptionPreferenceHint",
7941
- children: active ? modelLabel(snapshot.imageModel) : t("imageCapability_off")
7942
- }),
7943
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(IconChevronDownOutline14, {})
7944
- ] }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
7945
- className: "codexSubscriptionSettingsDisclosureBody",
8179
+ "aria-label": title,
8180
+ children: [
8181
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
8182
+ className: "codexSubscriptionSectionHead",
8183
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", { children: title }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
8184
+ className: "codexSubscriptionBadge",
8185
+ children: sketch ? "Beta" : active ? modelLabel(snapshot.imageModel) : t("imageCapability_off")
8186
+ })]
8187
+ }),
8188
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
8189
+ className: "codexSubscriptionSettingRows",
7946
8190
  children: [
7947
- Object.keys(IMAGE_SETTING_GROUPS).map((group) => {
8191
+ Object.keys(IMAGE_SETTING_GROUPS).filter((group) => group.startsWith("sketch") === sketch).map((group) => {
7948
8192
  const value = imageGroupValue(snapshot, group);
7949
8193
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ImageChoice, {
7950
- label: t(group),
8194
+ label: t(sketch ? `${group}Setting` : group),
7951
8195
  hint: t(`${group}Hint`),
7952
8196
  value,
7953
8197
  text: t(value === "mixed" ? "imageGroupMixed" : `${group}_${value}`),
@@ -7961,7 +8205,7 @@ window.__ModuleLoader__.load({
7961
8205
  }
7962
8206
  }, group);
7963
8207
  }),
7964
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
8208
+ !sketch ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
7965
8209
  className: "codexImageDefaultsGroup",
7966
8210
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(ImageChoice, {
7967
8211
  label: t("imageModel"),
@@ -7991,14 +8235,29 @@ window.__ModuleLoader__.load({
7991
8235
  preference.set({ imageQuality });
7992
8236
  }
7993
8237
  })]
7994
- }),
7995
- snapshot.imageModel.includes("2.5") ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
8238
+ }) : null,
8239
+ !sketch && snapshot.imageModel.includes("2.5") ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
7996
8240
  className: "codexSubscriptionPreferenceHint",
7997
8241
  children: t("imageModelHint")
7998
8242
  }) : null
7999
8243
  ]
8000
- })]
8001
- })
8244
+ }),
8245
+ snapshot.error ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
8246
+ className: "codexSubscriptionRecover",
8247
+ role: "alert",
8248
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
8249
+ className: "codexSubscriptionError",
8250
+ children: t("preferenceFailed")
8251
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Button, {
8252
+ type: "button",
8253
+ variant: "outline",
8254
+ onClick: () => {
8255
+ preference.retry();
8256
+ },
8257
+ children: t("preferenceRetry")
8258
+ })]
8259
+ }) : null
8260
+ ]
8002
8261
  });
8003
8262
  }
8004
8263
  //#endregion
@@ -8225,16 +8484,15 @@ window.__ModuleLoader__.load({
8225
8484
  className: "codexSubscriptionPreference",
8226
8485
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
8227
8486
  role: "status",
8228
- children: t(!state ? "runtimeLoading" : state.restartRequired ? "runtimeRestart" : busy ? `runtime_${state.phase}` : state.installed ? "subagentRuntimeInstalled" : state.present ? "runtimeIncompatible" : "subagentRuntimeMissing")
8229
- }), state?.available && (state.present || state.installable) ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Button, {
8487
+ children: t(!state ? "runtimeLoading" : state.restartRequired ? "runtimeRestart" : busy ? `runtime_${state.phase}` : state.installed ? "subagentRuntimeInstalled" : !state.componentVersion ? "runtimeHostUnsupported" : state.present ? state.removable ? "runtimeIncompatible" : "runtimeManagedElsewhere" : "subagentRuntimeMissing")
8488
+ }), state?.available && (state.present ? state.removable : state.installable) ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Button, {
8230
8489
  type: "button",
8231
8490
  variant: "outline",
8232
- disabled: locked || state.present && !state.removable,
8491
+ disabled: locked,
8233
8492
  onClick: () => state.present ? setConfirm(true) : void act("install"),
8234
8493
  children: t(state.present ? "runtimeRemove" : "runtimeInstall")
8235
8494
  }) : null]
8236
8495
  }),
8237
- state?.present && state.available && !state.removable ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", { children: t("runtimeManagedElsewhere") }) : null,
8238
8496
  state?.active > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", { children: t("runtimeActive") }) : null,
8239
8497
  busy && state.phase === "installing" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Button, {
8240
8498
  type: "button",
@@ -8276,21 +8534,22 @@ window.__ModuleLoader__.load({
8276
8534
  role: "status",
8277
8535
  children: t("runtimeCancelled")
8278
8536
  }) : null,
8279
- !busy ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Button, {
8280
- type: "button",
8281
- variant: "outline",
8282
- disabled: sending,
8283
- onClick: () => {
8284
- read();
8285
- preference.load();
8286
- },
8287
- children: t("runtimeRefresh")
8288
- }) : null,
8289
8537
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("details", { children: [
8290
8538
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("summary", { children: t("subagentRuntimeManage") }),
8539
+ state?.present && state.available && !state.removable ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", { children: t("runtimeManagedElsewhere") }) : null,
8540
+ !busy ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Button, {
8541
+ type: "button",
8542
+ variant: "outline",
8543
+ disabled: sending,
8544
+ onClick: () => {
8545
+ read();
8546
+ preference.load();
8547
+ },
8548
+ children: t("runtimeRefresh")
8549
+ }) : null,
8291
8550
  state && !state.available ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", { children: t("runtimeUnavailable") }) : null,
8292
8551
  state?.available && !state.installable && !state.present ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", { children: t("runtimeHostUnsupported") }) : null,
8293
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", { children: t("runtimeInstallHint") }),
8552
+ state?.installable ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", { children: t("runtimeInstallHint") }) : null,
8294
8553
  state?.componentVersion ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("code", { children: ["@deepseek-ai/dsh-subagent-codex@", state.componentVersion] }) : null,
8295
8554
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", { children: t("subagentRuntimeCacheHint") }),
8296
8555
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("a", {
@@ -9255,32 +9514,41 @@ window.__ModuleLoader__.load({
9255
9514
  }
9256
9515
  //#endregion
9257
9516
  //#region src/client-diagnostics.jsx
9258
- function DiagnosticsCard({ rpc, t }) {
9517
+ function DiagnosticsCard({ rpc, t, diagnostics }) {
9259
9518
  const [report, setReport] = (0, react.useState)();
9260
9519
  const [busy, setBusy] = (0, react.useState)(false);
9261
9520
  const [copied, setCopied] = (0, react.useState)(false);
9262
9521
  const [error, setError] = (0, react.useState)(false);
9522
+ const [copyError, setCopyError] = (0, react.useState)(false);
9263
9523
  const load = () => {
9264
9524
  setBusy(true);
9265
9525
  setError(false);
9266
9526
  setCopied(false);
9267
- recoveryCall(rpc, "diagnostics").then(setReport).catch((error) => {
9268
- setReport(clientDiagnostic(error));
9269
- setError(true);
9527
+ setCopyError(false);
9528
+ Promise.allSettled([recoveryCall(rpc, "diagnostics"), Promise.resolve().then(() => diagnostics?.collect())]).then(([server, client]) => {
9529
+ setError(server.status === "rejected");
9530
+ setReport({
9531
+ ...server.status === "fulfilled" ? server.value : clientDiagnostic(server.reason),
9532
+ client: client.status === "fulfilled" && client.value ? client.value : { status: "unknown" }
9533
+ });
9270
9534
  }).finally(() => setBusy(false));
9271
9535
  };
9272
9536
  const copy = () => {
9273
9537
  if (report === void 0) return;
9274
- navigator.clipboard.writeText(JSON.stringify(report, null, 2)).then(() => setCopied(true)).catch(() => setError(true));
9538
+ setCopyError(false);
9539
+ Promise.resolve().then(() => navigator.clipboard.writeText(JSON.stringify(report, null, 2))).then(() => setCopied(true)).catch(() => setCopyError(true));
9275
9540
  };
9276
9541
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
9277
9542
  className: "codexSubscriptionCard codexSubscriptionDiagnostics",
9278
9543
  children: [
9279
9544
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
9280
9545
  className: "codexSubscriptionSectionHead",
9281
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
9546
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
9282
9547
  className: "codexSubscriptionSectionTitle",
9283
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", { children: t("diagnostics") })
9548
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", { children: t("diagnostics") }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
9549
+ className: "codexSubscriptionHelp",
9550
+ children: t("diagnosticsHint")
9551
+ })]
9284
9552
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
9285
9553
  className: "codexSubscriptionActions",
9286
9554
  children: [
@@ -9307,11 +9575,151 @@ window.__ModuleLoader__.load({
9307
9575
  ]
9308
9576
  })]
9309
9577
  }),
9310
- report === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("pre", { children: JSON.stringify(report, null, 2) }),
9578
+ report === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
9579
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
9580
+ className: "codexSubscriptionHelp",
9581
+ children: t("diagnosticsCoverageHint")
9582
+ }),
9583
+ report.version && report.client?.version && report.version !== report.client.version ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
9584
+ role: "status",
9585
+ children: t("diagnosticsVersionMismatch")
9586
+ }) : null,
9587
+ (report.inspection?.checks ?? []).some((item) => ["inspection-failed", "inspection-timeout"].includes(item.reason)) ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
9588
+ role: "status",
9589
+ children: t("diagnosticsPartial")
9590
+ }) : null,
9591
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("table", {
9592
+ style: {
9593
+ width: "100%",
9594
+ fontSize: 12,
9595
+ textAlign: "left",
9596
+ borderSpacing: "0 8px"
9597
+ },
9598
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("thead", { children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("tr", { children: [
9599
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("th", { children: t("diagnosticsCapability") }),
9600
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("th", { children: t("diagnosticsReadiness") }),
9601
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("th", { children: t("diagnosticsEvidence") })
9602
+ ] }) }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("tbody", { children: (report.inspection?.capabilities ?? []).map((item) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("tr", { children: [
9603
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", { children: t(`diagnosticCapability_${item.id}`) }),
9604
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("td", { children: [t(`diagnosticReason_${item.reason}`), item.blockedBy ? ` · ${t(`diagnosticCapability_${item.blockedBy}`)}` : ""] }),
9605
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", { children: item.latest ? `${item.latest.action} · ${t(`diagnosticSource_${item.latest.source}`)} · ${t(`diagnosticOutcome_${item.latest.status}`)} · ${new Date(item.latest.observedAt).toLocaleTimeString()}` : t("diagnosticsUnverified") })
9606
+ ] }, item.id)) })]
9607
+ }),
9608
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
9609
+ className: "codexSubscriptionHelp",
9610
+ children: report.client?.topology?.length ? t("diagnosticsHostCollected") : t("diagnosticsHostUnknown")
9611
+ }),
9612
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("details", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("summary", { children: t("diagnosticsDetails") }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("pre", { children: JSON.stringify(report, null, 2) })] })
9613
+ ] }),
9311
9614
  error ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
9312
9615
  className: "codexSubscriptionError",
9313
9616
  role: "alert",
9314
9617
  children: t("diagnosticsFailed")
9618
+ }) : null,
9619
+ copyError ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
9620
+ className: "codexSubscriptionError",
9621
+ role: "alert",
9622
+ children: t("diagnosticsCopyFailed")
9623
+ }) : null
9624
+ ]
9625
+ });
9626
+ }
9627
+ //#endregion
9628
+ //#region src/client-storage.jsx
9629
+ function StorageCard({ rpc, t }) {
9630
+ const [usage, setUsage] = (0, react.useState)();
9631
+ const [busy, setBusy] = (0, react.useState)(false);
9632
+ const [confirm, setConfirm] = (0, react.useState)(false);
9633
+ const [error, setError] = (0, react.useState)(false);
9634
+ const mounted = (0, react.useRef)(false);
9635
+ const pending = (0, react.useRef)(false);
9636
+ const read = async (clear) => {
9637
+ if (pending.current) return;
9638
+ pending.current = true;
9639
+ setBusy(true);
9640
+ setError(false);
9641
+ try {
9642
+ const result = unwrap(await rpc.call(CHANNEL, clear ? "storage/clear-forecast" : "storage/status", {}));
9643
+ if (mounted.current) {
9644
+ setUsage(result);
9645
+ setConfirm(false);
9646
+ }
9647
+ } catch {
9648
+ if (mounted.current) setError(true);
9649
+ } finally {
9650
+ pending.current = false;
9651
+ if (mounted.current) setBusy(false);
9652
+ }
9653
+ };
9654
+ (0, react.useEffect)(() => {
9655
+ mounted.current = true;
9656
+ read(false);
9657
+ return () => {
9658
+ mounted.current = false;
9659
+ };
9660
+ }, [rpc]);
9661
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
9662
+ className: "codexSubscriptionCard codexSubscriptionStorage",
9663
+ "aria-label": t("storageTitle"),
9664
+ children: [
9665
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", { children: t("storageTitle") }),
9666
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
9667
+ className: "codexSubscriptionStorageMeter",
9668
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("storageForecast") }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("output", {
9669
+ role: "status",
9670
+ children: usage ? `${(usage.bytes / 1024).toFixed(1)} / ${(usage.limit / 1024).toFixed(0)} KiB` : t("storageLoading")
9671
+ })] }), usage ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("progress", {
9672
+ "aria-label": t("storageForecast"),
9673
+ value: usage.bytes,
9674
+ max: usage.limit
9675
+ }) : null]
9676
+ }),
9677
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
9678
+ className: "codexSubscriptionHelp",
9679
+ children: t("storageHint")
9680
+ }),
9681
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
9682
+ className: "codexSubscriptionActions",
9683
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Button, {
9684
+ type: "button",
9685
+ variant: "outline",
9686
+ disabled: busy,
9687
+ onClick: () => void read(false),
9688
+ children: t("runtimeRefresh")
9689
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Button, {
9690
+ type: "button",
9691
+ variant: "outline",
9692
+ disabled: busy || !usage?.bytes,
9693
+ onClick: () => setConfirm(true),
9694
+ children: t("storageClear")
9695
+ })]
9696
+ }),
9697
+ confirm ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
9698
+ className: "codexSubscriptionConfirm",
9699
+ role: "group",
9700
+ "aria-label": t("storageClear"),
9701
+ children: [
9702
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", { children: t("storageConfirm") }),
9703
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Button, {
9704
+ type: "button",
9705
+ variant: "outline",
9706
+ disabled: busy,
9707
+ onClick: () => setConfirm(false),
9708
+ children: t("sketchCancel")
9709
+ }),
9710
+ " ",
9711
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Button, {
9712
+ type: "button",
9713
+ variant: "outline",
9714
+ disabled: busy,
9715
+ onClick: () => void read(true),
9716
+ children: t("storageClear")
9717
+ })
9718
+ ]
9719
+ }) : null,
9720
+ error ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
9721
+ role: "alert",
9722
+ children: t("storageFailed")
9315
9723
  }) : null
9316
9724
  ]
9317
9725
  });
@@ -9722,10 +10130,15 @@ window.__ModuleLoader__.load({
9722
10130
  }
9723
10131
  //#endregion
9724
10132
  //#region src/client-section.jsx
9725
- function CodexSection({ preference, rpc, accountStatus, t }) {
10133
+ function CodexSection({ preference, rpc, accountStatus, t, diagnostics }) {
9726
10134
  const [tab, setTab] = (0, react.useState)("account");
9727
10135
  const id = (0, react.useId)();
9728
- const tabs = ["account", "advanced"];
10136
+ const tabs = [
10137
+ "account",
10138
+ "advanced",
10139
+ "creative",
10140
+ "maintenance"
10141
+ ];
9729
10142
  const accountSnapshot = useAccountStatusSnapshot(accountStatus);
9730
10143
  const account = accountSnapshot.account;
9731
10144
  const [resetKey, setResetKey] = (0, react.useState)(0);
@@ -9800,32 +10213,203 @@ window.__ModuleLoader__.load({
9800
10213
  })
9801
10214
  ]
9802
10215
  }),
9803
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
10216
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
9804
10217
  role: "tabpanel",
9805
10218
  id: `${id}-advanced`,
9806
10219
  "aria-labelledby": `${id}-advanced-tab`,
9807
10220
  hidden: tab !== "advanced",
9808
- children: [
9809
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(PreferencesCard, {
9810
- rpc,
9811
- preference,
9812
- t,
9813
- section: "advanced"
9814
- }),
9815
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ImagePreferences, {
9816
- preference,
9817
- t
9818
- }),
9819
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(DiagnosticsCard, {
9820
- rpc,
9821
- t
9822
- })
9823
- ]
10221
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(PreferencesCard, {
10222
+ rpc,
10223
+ preference,
10224
+ t,
10225
+ section: "advanced"
10226
+ })
10227
+ }),
10228
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
10229
+ role: "tabpanel",
10230
+ id: `${id}-creative`,
10231
+ "aria-labelledby": `${id}-creative-tab`,
10232
+ hidden: tab !== "creative",
10233
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(ImagePreferences, {
10234
+ preference,
10235
+ t
10236
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ImagePreferences, {
10237
+ preference,
10238
+ t,
10239
+ section: "sketch"
10240
+ })]
10241
+ }),
10242
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
10243
+ role: "tabpanel",
10244
+ id: `${id}-maintenance`,
10245
+ "aria-labelledby": `${id}-maintenance-tab`,
10246
+ hidden: tab !== "maintenance",
10247
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(StorageCard, {
10248
+ rpc,
10249
+ t
10250
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(DiagnosticsCard, {
10251
+ rpc,
10252
+ t,
10253
+ diagnostics
10254
+ })]
9824
10255
  })
9825
10256
  ]
9826
10257
  });
9827
10258
  }
9828
10259
  //#endregion
10260
+ //#region src/diagnostic-checks.js
10261
+ const states = /* @__PURE__ */ new Set([
10262
+ "pass",
10263
+ "fail",
10264
+ "warn",
10265
+ "unknown",
10266
+ "not-applicable"
10267
+ ]);
10268
+ /** Adapters return bounded reason codes, never exceptions or raw service data. */
10269
+ async function collectChecks(definitions, { timeoutMs = 1500, now = Date.now, source = "server", scope = "plugin-process" } = {}) {
10270
+ return Promise.all(definitions.map(async ({ id, capability, run }) => {
10271
+ const startedAt = now();
10272
+ let timer;
10273
+ const controller = new AbortController();
10274
+ const base = {
10275
+ id,
10276
+ capability,
10277
+ source,
10278
+ scope
10279
+ };
10280
+ try {
10281
+ if (!run) return {
10282
+ ...base,
10283
+ status: "unknown",
10284
+ reason: "not-instrumented",
10285
+ observedAt: now()
10286
+ };
10287
+ const result = await Promise.race([Promise.resolve().then(() => run(controller.signal)), new Promise((resolve) => {
10288
+ timer = setTimeout(() => {
10289
+ controller.abort();
10290
+ resolve({
10291
+ status: "unknown",
10292
+ reason: "inspection-timeout"
10293
+ });
10294
+ }, timeoutMs);
10295
+ })]);
10296
+ const valid = states.has(result?.status) && /^[a-z][a-z0-9-]{0,63}$/u.test(result?.reason ?? "");
10297
+ return {
10298
+ ...base,
10299
+ status: valid ? result.status : "unknown",
10300
+ reason: valid ? result.reason : "invalid-inspection-result",
10301
+ observedAt: now(),
10302
+ elapsedMs: Math.max(0, now() - startedAt)
10303
+ };
10304
+ } catch {
10305
+ return {
10306
+ ...base,
10307
+ status: "unknown",
10308
+ reason: "inspection-failed",
10309
+ observedAt: now(),
10310
+ elapsedMs: Math.max(0, now() - startedAt)
10311
+ };
10312
+ } finally {
10313
+ clearTimeout(timer);
10314
+ }
10315
+ }));
10316
+ }
10317
+ Object.freeze([
10318
+ "account",
10319
+ "accounts",
10320
+ "settings",
10321
+ "models",
10322
+ "quota",
10323
+ "search",
10324
+ "images",
10325
+ "sketch",
10326
+ "subagents",
10327
+ "compaction",
10328
+ "transport",
10329
+ "storage",
10330
+ "host-ui"
10331
+ ]);
10332
+ //#endregion
10333
+ //#region src/diagnostic-host.js
10334
+ const roots = [
10335
+ "settings.section",
10336
+ "conversation.input.right",
10337
+ "conversation.input.model",
10338
+ "conversation.input.attachments",
10339
+ "conversation.message.images",
10340
+ "conversation.trajectory.images",
10341
+ "shell.overlay"
10342
+ ];
10343
+ const packageName = /^(?:@[a-z0-9_.-]+\/)?[a-z0-9][a-z0-9_.-]{0,99}$/u;
10344
+ function projectHostSlots(slots) {
10345
+ if (typeof slots?.snapshot !== "function") return void 0;
10346
+ return roots.map((name) => {
10347
+ const nodes = slots.snapshot(name);
10348
+ const node = Array.isArray(nodes) ? nodes.find((item) => item.type === "slot" && item.name === name) : void 0;
10349
+ const occupants = Array.isArray(node?.occupants) ? node.occupants : [];
10350
+ return {
10351
+ name,
10352
+ declared: !!node,
10353
+ truncated: occupants.length > 16,
10354
+ occupants: occupants.slice(0, 16).map((value) => ({
10355
+ owner: typeof value.registrant === "string" && packageName.test(value.registrant) ? value.registrant : "unknown",
10356
+ active: value.active === true
10357
+ }))
10358
+ };
10359
+ });
10360
+ }
10361
+ function createHostDiagnostics() {
10362
+ const scopes = /* @__PURE__ */ new Map();
10363
+ let sequence = 0;
10364
+ return {
10365
+ register(slots) {
10366
+ const item = scopes.get(slots) ?? {
10367
+ alias: `scope-${++sequence}`,
10368
+ count: 0
10369
+ };
10370
+ item.count++;
10371
+ scopes.set(slots, item);
10372
+ return () => {
10373
+ if (--item.count === 0) scopes.delete(slots);
10374
+ };
10375
+ },
10376
+ async collect() {
10377
+ const topology = [];
10378
+ const active = [...scopes.entries()].slice(0, 8);
10379
+ const checks = await collectChecks(active.map(([slots, item]) => ({
10380
+ id: `host-slots-${item.alias}`,
10381
+ capability: "host-ui",
10382
+ run: () => {
10383
+ const value = projectHostSlots(slots);
10384
+ if (!value) return {
10385
+ status: "unknown",
10386
+ reason: "host-inspection-unavailable"
10387
+ };
10388
+ topology.push({
10389
+ scope: item.alias,
10390
+ slots: value
10391
+ });
10392
+ return {
10393
+ status: "pass",
10394
+ reason: "inspection-completed"
10395
+ };
10396
+ }
10397
+ })), {
10398
+ source: "client",
10399
+ scope: "mounted-host-scopes"
10400
+ });
10401
+ return {
10402
+ version: PACKAGE_VERSION,
10403
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
10404
+ checks,
10405
+ topology,
10406
+ truncated: scopes.size > active.length,
10407
+ interpretation: "host-selection-not-visual-or-execution-proof"
10408
+ };
10409
+ }
10410
+ };
10411
+ }
10412
+ //#endregion
9829
10413
  //#region src/client.jsx
9830
10414
  const inject = [
9831
10415
  "slots",
@@ -9838,6 +10422,8 @@ window.__ModuleLoader__.load({
9838
10422
  "sessions"
9839
10423
  ];
9840
10424
  function apply(ctx) {
10425
+ const diagnostics = createHostDiagnostics();
10426
+ ctx.effect(() => diagnostics.register(ctx.slots), "codex-subscription: host inspection");
9841
10427
  const imageViewer = new SubscriptionImageViewerService();
9842
10428
  ctx.effect(() => ctx.locale.register(NS, {
9843
10429
  zh,
@@ -9898,11 +10484,13 @@ window.__ModuleLoader__.load({
9898
10484
  preference,
9899
10485
  rpc,
9900
10486
  accountStatus,
9901
- t
10487
+ t,
10488
+ diagnostics
9902
10489
  })
9903
10490
  }, CodexSection));
9904
10491
  const sessions = ctx.get("sessions");
9905
10492
  const installDirectorySlots = (scope) => {
10493
+ scope.effect(() => diagnostics.register(scope.slots), "codex-subscription: scoped host inspection");
9906
10494
  const modelDirectories = scope.get("modelDirectories");
9907
10495
  scope.slots.inject("conversation.input.right", () => scope.slots.register({
9908
10496
  name: "conversation.input.right",