dsh-codex-subscription 2.1.0-beta.2 → 2.1.0-beta.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
@@ -386,6 +386,7 @@ window.__ModuleLoader__.load({
386
386
  "image/original/chunk",
387
387
  "sketch/connect",
388
388
  "sketch/poll",
389
+ "sketch/claim",
389
390
  "sketch/result",
390
391
  "sketch/disconnect"
391
392
  ]);
@@ -697,7 +698,34 @@ window.__ModuleLoader__.load({
697
698
  context.lineWidth = stroke.width * (stroke.brush === "pencil" ? .55 : 1) * (stroke.pressure ?? 1);
698
699
  context.beginPath();
699
700
  const last = stroke.points.at(-1);
700
- if (stroke.shape === "line") {
701
+ if (stroke.shape === "text") {
702
+ const x = Math.min(first.x, last.x) * size, y = Math.min(first.y, last.y) * height, w = Math.abs(last.x - first.x) * size, h = Math.abs(last.y - first.y) * height;
703
+ const lines = stroke.text.split("\n"), fontSize = Math.min(stroke.width, h / Math.max(1, lines.length) / 1.2);
704
+ context.font = `${fontSize}px system-ui, sans-serif`;
705
+ context.textBaseline = "top";
706
+ lines.forEach((line, i) => context.fillText(line, x, y + i * fontSize * 1.2, w));
707
+ } else if (stroke.shape === "arrow") {
708
+ const x = last.x * size, y = last.y * height, a = Math.atan2(y - first.y * height, x - first.x * size), head = Math.min(Math.hypot(x - first.x * size, y - first.y * height) * .4, Math.max(12, stroke.width * 3));
709
+ context.moveTo(first.x * size, first.y * height);
710
+ context.lineTo(x, y);
711
+ context.stroke();
712
+ context.beginPath();
713
+ context.moveTo(x, y);
714
+ context.lineTo(x - head * Math.cos(a - .5), y - head * Math.sin(a - .5));
715
+ context.lineTo(x - head * Math.cos(a + .5), y - head * Math.sin(a + .5));
716
+ context.closePath();
717
+ context.fill();
718
+ } else if (stroke.shape === "bezier") {
719
+ context.moveTo(first.x * size, first.y * height);
720
+ for (let i = 1; i < stroke.points.length; i += 3) {
721
+ const [a, b, c] = stroke.points.slice(i, i + 3);
722
+ context.bezierCurveTo(a.x * size, a.y * height, b.x * size, b.y * height, c.x * size, c.y * height);
723
+ }
724
+ if (stroke.fill) {
725
+ context.closePath();
726
+ context.fill();
727
+ } else context.stroke();
728
+ } else if (stroke.shape === "line") {
701
729
  context.moveTo(first.x * size, first.y * height);
702
730
  context.lineTo(last.x * size, last.y * height);
703
731
  context.stroke();
@@ -732,6 +760,46 @@ window.__ModuleLoader__.load({
732
760
  context.globalCompositeOperation = "source-over";
733
761
  context.globalAlpha = 1;
734
762
  }
763
+ //#endregion
764
+ //#region src/sketch-curves.js
765
+ const cached = /* @__PURE__ */ new WeakMap();
766
+ const midpoint = (a, b) => ({
767
+ x: (a.x + b.x) / 2,
768
+ y: (a.y + b.y) / 2
769
+ });
770
+ function flattenSketchCurve(stroke, width, height) {
771
+ const previous = cached.get(stroke);
772
+ if (previous?.width === width && previous.height === height) return previous.points;
773
+ const controls = stroke.points.map((p) => ({
774
+ x: p.x * width,
775
+ y: p.y * height
776
+ })), points = [controls[0]];
777
+ const distance = (p, a, b) => {
778
+ const dx = b.x - a.x, dy = b.y - a.y, d = dx * dx + dy * dy;
779
+ const t = d ? Math.max(0, Math.min(1, ((p.x - a.x) * dx + (p.y - a.y) * dy) / d)) : 0;
780
+ return Math.hypot(p.x - a.x - t * dx, p.y - a.y - t * dy);
781
+ };
782
+ const split = (a, b, c, d, depth) => {
783
+ if (depth === 10 || Math.max(distance(b, a, d), distance(c, a, d)) <= .5) {
784
+ points.push(d);
785
+ return;
786
+ }
787
+ const ab = midpoint(a, b), bc = midpoint(b, c), cd = midpoint(c, d), abc = midpoint(ab, bc), bcd = midpoint(bc, cd), m = midpoint(abc, bcd);
788
+ split(a, ab, abc, m, depth + 1);
789
+ split(m, bcd, cd, d, depth + 1);
790
+ };
791
+ for (let i = 1; i < controls.length; i += 3) split(controls[i - 1], controls[i], controls[i + 1], controls[i + 2], 0);
792
+ const normalized = points.map((p) => ({
793
+ x: p.x / width,
794
+ y: p.y / height
795
+ }));
796
+ cached.set(stroke, {
797
+ width,
798
+ height,
799
+ points: normalized
800
+ });
801
+ return normalized;
802
+ }
735
803
  const createSketchLayers = () => ({
736
804
  active: 1,
737
805
  nextId: 2,
@@ -809,15 +877,15 @@ window.__ModuleLoader__.load({
809
877
  return Math.hypot(p.x - a.x - k * dx, p.y - a.y - k * dy);
810
878
  };
811
879
  function strokeHit(stroke, point, radius, width = SKETCH_SIZE, height = width) {
812
- let points = stroke.points;
880
+ let points = stroke.shape === "bezier" ? flattenSketchCurve(stroke, width, height) : stroke.points;
813
881
  if (!points.length) return false;
814
882
  const a = points[0], b = points.at(-1);
815
- if (stroke.fill && stroke.shape === "rectangle" && point.x >= Math.min(a.x, b.x) && point.x <= Math.max(a.x, b.x) && point.y >= Math.min(a.y, b.y) && point.y <= Math.max(a.y, b.y)) return true;
883
+ if ((stroke.shape === "text" || stroke.fill && stroke.shape === "rectangle") && point.x >= Math.min(a.x, b.x) && point.x <= Math.max(a.x, b.x) && point.y >= Math.min(a.y, b.y) && point.y <= Math.max(a.y, b.y)) return true;
816
884
  if (stroke.fill && stroke.shape === "circle") {
817
885
  const rx = Math.abs(b.x - a.x) / 2, ry = Math.abs(b.y - a.y) / 2;
818
886
  if (rx && ry && ((point.x - (a.x + b.x) / 2) / rx) ** 2 + ((point.y - (a.y + b.y) / 2) / ry) ** 2 <= 1) return true;
819
887
  }
820
- if (stroke.shape === "polygon") {
888
+ if (stroke.shape === "polygon" || stroke.shape === "bezier" && stroke.fill) {
821
889
  if (stroke.fill) {
822
890
  let inside = false;
823
891
  for (let i = 0, j = points.length - 1; i < points.length; j = i++) {
@@ -1345,12 +1413,63 @@ window.__ModuleLoader__.load({
1345
1413
  ]
1346
1414
  });
1347
1415
  }
1416
+ //#endregion
1417
+ //#region src/sketch-objects.js
1418
+ const objectId = (stroke, index) => stroke.id ?? `legacy-${index}`;
1419
+ const identifyObjects = (doc) => ({
1420
+ ...doc,
1421
+ layers: doc.layers.map((layer) => ({
1422
+ ...layer,
1423
+ strokes: layer.strokes.map((s, i) => s.id ? s : {
1424
+ ...s,
1425
+ id: objectId(s, i)
1426
+ })
1427
+ }))
1428
+ });
1429
+ function objectBounds(stroke) {
1430
+ const xs = stroke.points.map((p) => p.x), ys = stroke.points.map((p) => p.y);
1431
+ return {
1432
+ x: Math.min(...xs),
1433
+ y: Math.min(...ys),
1434
+ width: Math.max(...xs) - Math.min(...xs),
1435
+ height: Math.max(...ys) - Math.min(...ys)
1436
+ };
1437
+ }
1438
+ function transformObject(stroke, { dx = 0, dy = 0, scaleX = 1, scaleY = 1 }) {
1439
+ if (![
1440
+ dx,
1441
+ dy,
1442
+ scaleX,
1443
+ scaleY
1444
+ ].every(Number.isFinite) || scaleX <= 0 || scaleY <= 0) throw Error("Invalid object transform");
1445
+ const box = objectBounds(stroke);
1446
+ const points = stroke.points.map((p) => ({
1447
+ x: box.x + (p.x - box.x) * scaleX + dx,
1448
+ y: box.y + (p.y - box.y) * scaleY + dy
1449
+ }));
1450
+ if (points.some((p) => p.x < 0 || p.x > 1 || p.y < 0 || p.y > 1)) throw Error("Object would leave the canvas");
1451
+ return {
1452
+ ...stroke,
1453
+ points
1454
+ };
1455
+ }
1456
+ function sketchObjectSummary(doc) {
1457
+ return doc.layers.flatMap((layer) => layer.strokes.map((stroke, i) => ({
1458
+ layer: layer.id,
1459
+ id: objectId(stroke, i),
1460
+ shape: stroke.shape,
1461
+ color: stroke.color,
1462
+ bounds: objectBounds(stroke),
1463
+ ...stroke.text ? { text: stroke.text } : {}
1464
+ })));
1465
+ }
1348
1466
  const SKETCH_COMMAND_HELP = {
1349
- coordinates: "Normalized x/y in [0,1]; width is canvas pixels. Read documentId and revision before editing.",
1350
- shapes: "line: exactly two endpoints; rectangle/circle: exactly two opposite bounding-box corners (circle draws an ellipse within that box); polygon: three or more vertices, closed automatically; pen: ordered path points. 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.",
1467
+ 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.",
1468
+ shapes: "line: exactly two endpoints; rectangle/circle: 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.",
1351
1469
  commands: {
1352
- stroke: "{op:\"stroke\",layer:1,shape:\"pen|line|rectangle|circle|polygon\",color:\"#rrggbb\",width:2,opacity:1,fill:false,points:[{x:0.1,y:0.1},...]}",
1470
+ 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},...]}",
1353
1471
  layer: "{op:\"layer\",action:\"add|select|rename|visible|duplicate|up|down|delete|clear\",id:1,value:\"name\"}",
1472
+ 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.",
1354
1473
  resize: "{op:\"resize\",ratio:\"1:1|4:3|3:4|16:9|9:16\"}"
1355
1474
  },
1356
1475
  limits: {
@@ -1363,7 +1482,7 @@ window.__ModuleLoader__.load({
1363
1482
  const finite = (value, min, max) => typeof value === "number" && Number.isFinite(value) && value >= min && value <= max;
1364
1483
  function applySketchCommands(source, commands) {
1365
1484
  if (!Array.isArray(commands) || !commands.length || commands.length > 256) throw Error("Expected 1–256 commands");
1366
- let doc = source;
1485
+ let doc = identifyObjects(source);
1367
1486
  for (const command of commands) {
1368
1487
  if (!command || typeof command !== "object") throw Error("Invalid command");
1369
1488
  if (command.op === "resize") {
@@ -1387,29 +1506,93 @@ window.__ModuleLoader__.load({
1387
1506
  doc = next;
1388
1507
  continue;
1389
1508
  }
1509
+ if (command.op === "object") {
1510
+ const layer = doc.layers.find((l) => l.id === (command.layer ?? doc.active)), index = layer?.strokes.findIndex((s) => s.id === command.id);
1511
+ if (!layer?.visible || index < 0 || index === void 0) throw Error("Object missing or hidden; inspect again");
1512
+ const strokes = layer.strokes.slice(), original = strokes[index];
1513
+ if (command.action === "delete") strokes.splice(index, 1);
1514
+ else if (command.action === "duplicate") strokes.splice(index + 1, 0, {
1515
+ ...original,
1516
+ id: crypto.randomUUID(),
1517
+ points: original.points.map((p) => ({ ...p }))
1518
+ });
1519
+ else if (command.action === "update") {
1520
+ const patch = command.patch ?? {};
1521
+ if (Object.keys(patch).some((k) => ![
1522
+ "color",
1523
+ "width",
1524
+ "opacity",
1525
+ "fill",
1526
+ "text",
1527
+ "points"
1528
+ ].includes(k))) throw Error("Unsupported object property");
1529
+ const changed = command.transform ? transformObject({
1530
+ ...original,
1531
+ ...patch
1532
+ }, command.transform) : {
1533
+ ...original,
1534
+ ...patch
1535
+ };
1536
+ strokes[index] = {
1537
+ ...applySketchCommands({
1538
+ ...doc,
1539
+ layers: [{
1540
+ ...layer,
1541
+ strokes: []
1542
+ }]
1543
+ }, [{
1544
+ ...changed,
1545
+ op: "stroke",
1546
+ layer: layer.id
1547
+ }]).layers[0].strokes[0],
1548
+ brush: original.brush ?? "pen",
1549
+ ...original.pressure !== void 0 ? { pressure: original.pressure } : {}
1550
+ };
1551
+ } else throw Error("Unknown object action");
1552
+ doc = {
1553
+ ...doc,
1554
+ layers: doc.layers.map((l) => l === layer ? {
1555
+ ...l,
1556
+ strokes
1557
+ } : l)
1558
+ };
1559
+ continue;
1560
+ }
1390
1561
  if (command.op !== "stroke") throw Error("Unknown command");
1391
- const { points, color, shape = "pen", width = 2, opacity = 1, fill = false } = command;
1562
+ const { points, color, shape = "pen", width = shape === "text" ? 24 : 2, opacity = 1, fill = false } = command;
1392
1563
  if (![
1393
1564
  "pen",
1394
1565
  "line",
1395
1566
  "rectangle",
1396
1567
  "circle",
1397
1568
  "polygon",
1569
+ "bezier",
1570
+ "arrow",
1571
+ "text",
1398
1572
  "eraser"
1399
1573
  ].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");
1400
1574
  if ([
1401
1575
  "line",
1576
+ "arrow",
1577
+ "text",
1402
1578
  "rectangle",
1403
1579
  "circle"
1404
1580
  ].includes(shape) && points.length !== 2 || shape === "polygon" && points.length < 3) throw Error("Invalid shape points");
1581
+ if (shape === "bezier" && (points.length < 4 || points.length > 193 || (points.length - 1) % 3 !== 0)) throw Error("Bezier needs a start point followed by groups of two controls and an endpoint (max 64 segments)");
1405
1582
  if (fill && ![
1406
1583
  "rectangle",
1407
1584
  "circle",
1408
- "polygon"
1585
+ "polygon",
1586
+ "bezier"
1409
1587
  ].includes(shape)) throw Error("Fill requires a closed shape");
1410
1588
  const layer = doc.layers.find((layer) => layer.id === (command.layer ?? doc.active));
1411
1589
  if (!layer?.visible) throw Error("Target layer is missing or hidden");
1590
+ 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");
1591
+ const id = command.id ?? crypto.randomUUID();
1592
+ if (typeof id !== "string" || !id.length || id.length > 100 || layer.strokes.some((s) => s.id === id)) throw Error("Invalid or duplicate object id");
1412
1593
  const stroke = {
1594
+ id,
1595
+ ...shape === "text" ? { text: command.text } : {},
1413
1596
  shape,
1414
1597
  color,
1415
1598
  width,
@@ -1434,15 +1617,24 @@ window.__ModuleLoader__.load({
1434
1617
  }
1435
1618
  function createSketchCommandSession(adapter) {
1436
1619
  const completed = /* @__PURE__ */ new Map();
1437
- let pending = false;
1620
+ let pending = false, cachedCharacters = 0;
1438
1621
  return async (request) => {
1439
1622
  if (!request || typeof request !== "object") throw Error("Invalid sketch request");
1440
1623
  if (!adapter.available()) throw Error("Open the sketch board for this session first");
1441
1624
  const current = adapter.snapshot();
1442
- if (request.action === "inspect") return {
1443
- ...current,
1444
- help: SKETCH_COMMAND_HELP
1445
- };
1625
+ if (request.action === "inspect") {
1626
+ const offset = request.offset ?? 0, objects = adapter.objects?.() ?? [];
1627
+ if (!Number.isInteger(offset) || offset < 0) throw Error("offset must be a non-negative integer");
1628
+ return {
1629
+ ...current,
1630
+ objects: objects.slice(offset, offset + 50),
1631
+ objectCount: objects.length,
1632
+ ...offset + 50 < objects.length ? { nextOffset: offset + 50 } : {},
1633
+ ...request.objectId ? { object: adapter.object?.(request.objectId, request.layer) } : {},
1634
+ recentRequests: [...completed.values()].slice(-8).map((entry) => entry.receipt),
1635
+ help: SKETCH_COMMAND_HELP
1636
+ };
1637
+ }
1446
1638
  if (request.documentId !== current.documentId) throw Error("Document changed; inspect again");
1447
1639
  if (pending || adapter.busy()) throw Error("Sketch is being edited; retry after it settles");
1448
1640
  if (request.action === "preview") return {
@@ -1451,16 +1643,25 @@ window.__ModuleLoader__.load({
1451
1643
  };
1452
1644
  if (!["apply", "save"].includes(request.action)) throw Error("Unknown sketch action");
1453
1645
  if (typeof request.requestId !== "string" || !request.requestId.length || request.requestId.length > 100) throw Error("A unique requestId is required");
1454
- const key = `${current.documentId}:${request.requestId}`, fingerprint = JSON.stringify(request);
1646
+ const key = `${current.documentId}:${request.requestId}`, fingerprint = JSON.stringify({
1647
+ ...request,
1648
+ runId: void 0
1649
+ });
1455
1650
  const cached = completed.get(key);
1456
1651
  if (cached) {
1457
1652
  if (cached.fingerprint !== fingerprint) throw Error("requestId reused with different content");
1458
1653
  return cached.result;
1459
1654
  }
1460
1655
  if (request.revision !== current.revision || adapter.busy()) throw Error("Sketch changed or is being edited; inspect again");
1656
+ let changedObjects;
1461
1657
  if (request.action === "apply") {
1462
- const next = applySketchCommands(adapter.document(), request.commands);
1658
+ const before = adapter.document(), next = applySketchCommands(before, request.commands);
1463
1659
  adapter.commit(next);
1660
+ const previous = new Map(before.layers.flatMap((l) => l.strokes.map((s) => [`${l.id}:${s.id}`, s])));
1661
+ changedObjects = next.layers.flatMap((l) => l.strokes.filter((s) => previous.get(`${l.id}:${s.id}`) !== s).map((s) => ({
1662
+ layer: l.id,
1663
+ id: s.id
1664
+ })));
1464
1665
  } else {
1465
1666
  if (request.name !== void 0 && (typeof request.name !== "string" || request.name.length > 60)) throw Error("Invalid draft name");
1466
1667
  pending = true;
@@ -1470,12 +1671,28 @@ window.__ModuleLoader__.load({
1470
1671
  pending = false;
1471
1672
  }
1472
1673
  }
1473
- const result = adapter.snapshot();
1674
+ const result = {
1675
+ ...adapter.snapshot(),
1676
+ ...changedObjects ? {
1677
+ changedObjects: changedObjects.slice(0, 100),
1678
+ changedObjectCount: changedObjects.length
1679
+ } : {}
1680
+ };
1474
1681
  completed.set(key, {
1475
1682
  fingerprint,
1476
- result
1683
+ result,
1684
+ receipt: {
1685
+ requestId: request.requestId,
1686
+ action: request.action,
1687
+ revision: result.revision
1688
+ }
1477
1689
  });
1478
- if (completed.size > 128) completed.delete(completed.keys().next().value);
1690
+ cachedCharacters += fingerprint.length;
1691
+ while (completed.size > 1 && (completed.size > 128 || cachedCharacters > 4e6)) {
1692
+ const oldest = completed.keys().next().value;
1693
+ cachedCharacters -= completed.get(oldest).fingerprint.length;
1694
+ completed.delete(oldest);
1695
+ }
1479
1696
  return result;
1480
1697
  };
1481
1698
  }
@@ -1669,6 +1886,7 @@ window.__ModuleLoader__.load({
1669
1886
  }, [open, working]);
1670
1887
  useSketchDismiss(open, setOpen, host, [".codexSketchFiles"]);
1671
1888
  const run = async (operation) => {
1889
+ if (disabled || working) return;
1672
1890
  setWorking(true);
1673
1891
  onWorking(true);
1674
1892
  try {
@@ -1724,7 +1942,7 @@ window.__ModuleLoader__.load({
1724
1942
  className: "codexSketchFileActions",
1725
1943
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1726
1944
  type: "button",
1727
- disabled: working,
1945
+ disabled: disabled || working,
1728
1946
  onClick: () => void run(async () => {
1729
1947
  await fresh();
1730
1948
  setName("");
@@ -1733,7 +1951,7 @@ window.__ModuleLoader__.load({
1733
1951
  children: t("sketchNew")
1734
1952
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1735
1953
  type: "button",
1736
- disabled: working,
1954
+ disabled: disabled || working,
1737
1955
  onClick: () => input.current.click(),
1738
1956
  children: t("sketchImport")
1739
1957
  })]
@@ -1747,7 +1965,7 @@ window.__ModuleLoader__.load({
1747
1965
  }),
1748
1966
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1749
1967
  type: "button",
1750
- disabled: working,
1968
+ disabled: disabled || working,
1751
1969
  onClick: () => void run(async () => {
1752
1970
  await save(name);
1753
1971
  await refresh();
@@ -1765,14 +1983,14 @@ window.__ModuleLoader__.load({
1765
1983
  ].map(([value, label]) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1766
1984
  type: "button",
1767
1985
  "aria-pressed": format === value,
1768
- disabled: working,
1986
+ disabled: disabled || working,
1769
1987
  onClick: () => setFormat(value),
1770
1988
  children: label
1771
1989
  }, value))
1772
1990
  }),
1773
1991
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1774
1992
  type: "button",
1775
- disabled: working || !hasContent,
1993
+ disabled: disabled || working || !hasContent,
1776
1994
  onClick: () => void run(async () => {
1777
1995
  await download(format);
1778
1996
  setOpen(false);
@@ -1785,7 +2003,7 @@ window.__ModuleLoader__.load({
1785
2003
  className: "codexSketchDraftList",
1786
2004
  children: rows.map((row) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1787
2005
  type: "button",
1788
- disabled: working,
2006
+ disabled: disabled || working,
1789
2007
  onClick: () => void run(async () => {
1790
2008
  await load(row);
1791
2009
  setName(row.name);
@@ -1794,7 +2012,7 @@ window.__ModuleLoader__.load({
1794
2012
  children: row.name
1795
2013
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1796
2014
  type: "button",
1797
- disabled: working,
2015
+ disabled: disabled || working,
1798
2016
  "aria-label": `${t("sketchDeleteDraft")} ${row.name}`,
1799
2017
  onClick: () => {
1800
2018
  if (remove !== row.id) {
@@ -1824,6 +2042,9 @@ window.__ModuleLoader__.load({
1824
2042
  //#region src/workspace-icons.jsx
1825
2043
  function WorkspaceIcon({ name, size = 24 }) {
1826
2044
  const paths = {
2045
+ select: "M5 3l14 9-7 2-3 7-4-18z",
2046
+ text: "M4 5h16M12 5v15M8 20h8M4 5v3M20 5v3",
2047
+ arrow: "M4 20L20 4M10 4h10v10",
1827
2048
  line: "M4 20L20 4",
1828
2049
  layers: "M12 3L2 8l10 5 10-5-10-5zM2 12l10 5 10-5M2 16l10 5 10-5",
1829
2050
  eye: "M2 12s4-7 10-7 10 7 10 7-4 7-10 7S2 12 2 12zM15 12a3 3 0 11-6 0 3 3 0 016 0",
@@ -1858,9 +2079,158 @@ window.__ModuleLoader__.load({
1858
2079
  });
1859
2080
  }
1860
2081
  //#endregion
2082
+ //#region src/sketch-size-control.jsx
2083
+ function SketchSizeControl({ value, onChange, onStart, onEnd, label, disabled, min = 2, max = 128, mode, modes, onModeChange, suffix = "" }) {
2084
+ const active = (0, react.useRef)(false);
2085
+ const start = () => {
2086
+ if (!active.current) {
2087
+ active.current = true;
2088
+ onStart?.();
2089
+ }
2090
+ };
2091
+ const end = () => {
2092
+ if (active.current) {
2093
+ active.current = false;
2094
+ onEnd?.();
2095
+ }
2096
+ };
2097
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2098
+ className: "codexSketchSizeControl",
2099
+ title: label,
2100
+ children: [
2101
+ modes ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2102
+ className: "codexSketchSizeModes",
2103
+ children: modes.map((item) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2104
+ type: "button",
2105
+ "aria-pressed": mode === item.value,
2106
+ disabled,
2107
+ onClick: () => {
2108
+ end();
2109
+ onModeChange(item.value);
2110
+ },
2111
+ children: item.label
2112
+ }, item.value))
2113
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: label }),
2114
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
2115
+ type: "range",
2116
+ "aria-label": label,
2117
+ "aria-orientation": "vertical",
2118
+ min,
2119
+ max,
2120
+ value,
2121
+ disabled,
2122
+ onPointerDown: start,
2123
+ onPointerUp: end,
2124
+ onPointerCancel: end,
2125
+ onBlur: end,
2126
+ onKeyDown: start,
2127
+ onKeyUp: end,
2128
+ onChange: (event) => {
2129
+ start();
2130
+ onChange(Number(event.target.value));
2131
+ }
2132
+ }),
2133
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("output", { children: [Math.round(value), suffix] })
2134
+ ]
2135
+ });
2136
+ }
2137
+ //#endregion
2138
+ //#region src/sketch-agent-run.js
2139
+ function createSketchAgentRun({ execute, open, changed, busy = () => false, previewEnabled = () => false, idleMs = 18e4 }) {
2140
+ let state = "idle", generation = 0, runNumber = 0, pending = false, runId, timer, completed;
2141
+ const update = (next) => {
2142
+ state = next;
2143
+ changed(next);
2144
+ };
2145
+ const clear = () => {
2146
+ clearTimeout(timer);
2147
+ timer = void 0;
2148
+ };
2149
+ const expire = () => {
2150
+ clear();
2151
+ generation++;
2152
+ if (state !== "stopped") update("failed");
2153
+ };
2154
+ return {
2155
+ get state() {
2156
+ return state;
2157
+ },
2158
+ get locked() {
2159
+ return state === "drawing";
2160
+ },
2161
+ stop() {
2162
+ clear();
2163
+ generation++;
2164
+ update("stopped");
2165
+ },
2166
+ resume() {
2167
+ clear();
2168
+ generation++;
2169
+ update("idle");
2170
+ },
2171
+ fail: expire,
2172
+ dispose() {
2173
+ clear();
2174
+ generation++;
2175
+ state = "idle";
2176
+ },
2177
+ async execute(request) {
2178
+ if (!request || typeof request !== "object") throw Error("Invalid sketch request");
2179
+ if (state === "stopped") throw Error("Drawing stopped by the user. Do not retry until they enable drawing again.");
2180
+ if (pending || busy()) throw Error("Sketch is being edited; retry after it settles");
2181
+ if (request.action !== "inspect" && request.runId !== runId) throw Error("Drawing run changed; inspect again");
2182
+ if (state === "finished" && completed?.key === JSON.stringify(request)) return completed.value;
2183
+ if (state !== "drawing" && request.action !== "inspect") throw Error("Start drawing with inspect");
2184
+ clear();
2185
+ const version = generation;
2186
+ pending = true;
2187
+ try {
2188
+ if (state !== "drawing") {
2189
+ runId = `run-${++runNumber}`;
2190
+ completed = void 0;
2191
+ update("drawing");
2192
+ open();
2193
+ }
2194
+ const value = await execute(request.action === "finish" ? {
2195
+ ...request,
2196
+ action: "save"
2197
+ } : request);
2198
+ if (version !== generation) throw Error("Drawing interrupted");
2199
+ if (request.action !== "finish") return {
2200
+ ...value,
2201
+ runId
2202
+ };
2203
+ const result = previewEnabled() ? await execute({
2204
+ action: "preview",
2205
+ documentId: value.documentId
2206
+ }) : value;
2207
+ if (version !== generation) throw Error("Drawing interrupted");
2208
+ completed = {
2209
+ key: JSON.stringify(request),
2210
+ value: {
2211
+ ...result,
2212
+ runId
2213
+ }
2214
+ };
2215
+ update("finished");
2216
+ return completed.value;
2217
+ } catch (error) {
2218
+ if (version === generation) update("failed");
2219
+ throw error;
2220
+ } finally {
2221
+ pending = false;
2222
+ if (state === "drawing") {
2223
+ timer = setTimeout(expire, idleMs);
2224
+ timer.unref?.();
2225
+ }
2226
+ }
2227
+ }
2228
+ };
2229
+ }
2230
+ //#endregion
1861
2231
  //#region src/sketch-agent-client.js
1862
2232
  function connectSketchAgent(rpc, sessionId, execute, report, pollDelay = () => 350) {
1863
- let stopped = false, token, timer;
2233
+ let stopped = false, token, timer, attempts = 0;
1864
2234
  const call = (endpoint, payload) => rpc.call(CHANNEL, `sketch/${endpoint}`, {
1865
2235
  sessionId,
1866
2236
  token,
@@ -1871,9 +2241,14 @@ window.__ModuleLoader__.load({
1871
2241
  const tasks = await call("poll");
1872
2242
  for (const task of tasks) {
1873
2243
  if (stopped) break;
2244
+ if (task.cancelled) {
2245
+ report("Sketch operation interrupted; completed strokes are preserved.");
2246
+ continue;
2247
+ }
1874
2248
  let value, error;
1875
2249
  try {
1876
- if (task.expiresAt < Date.now()) throw Error("Sketch command expired; inspect before retrying");
2250
+ if (task.expiresAt < Date.now() || !await call("claim", { id: task.id })) throw Error("Sketch command expired or cancelled; inspect before retrying");
2251
+ if (stopped) break;
1877
2252
  value = await execute(task.request);
1878
2253
  } catch (cause) {
1879
2254
  error = cause.message;
@@ -1890,29 +2265,23 @@ window.__ModuleLoader__.load({
1890
2265
  }
1891
2266
  if (!stopped) timer = setTimeout(poll, pollDelay());
1892
2267
  };
1893
- call("connect").then((value) => {
2268
+ const connect = () => void call("connect").then((value) => {
1894
2269
  token = value.token;
1895
2270
  if (stopped) call("disconnect").catch(() => {});
1896
2271
  else poll();
1897
2272
  }, (error) => {
1898
- if (!stopped) report(error.message);
2273
+ if (stopped) return;
2274
+ const leaseConflict = /Another board is connected/.test(error.message);
2275
+ if (++attempts < (leaseConflict ? 8 : 3)) timer = setTimeout(connect, Math.min(3e3, 500 * attempts));
2276
+ else report(error.message);
1899
2277
  });
2278
+ connect();
1900
2279
  return () => {
1901
2280
  stopped = true;
1902
2281
  clearTimeout(timer);
1903
2282
  if (token) call("disconnect").catch(() => {});
1904
2283
  };
1905
2284
  }
1906
- async function executeSketchFromAgent(request, { available, open, execute, live = () => true, wait = () => new Promise((resolve) => setTimeout(resolve, 20)) }) {
1907
- if (!available()) {
1908
- if (request.action !== "inspect") throw Error("Sketch board closed; inspect before editing");
1909
- if (!live()) throw Error("Sketch session disconnected");
1910
- open();
1911
- for (let i = 0; i < 100 && live() && !available(); i++) await wait();
1912
- }
1913
- if (!live() || !available()) throw Error("Sketch board could not open in the current session");
1914
- return execute(request);
1915
- }
1916
2285
  //#endregion
1917
2286
  //#region src/sketch-studio.jsx
1918
2287
  const PALETTE = [
@@ -1924,20 +2293,51 @@ window.__ModuleLoader__.load({
1924
2293
  "#34c759",
1925
2294
  "#0088ff"
1926
2295
  ];
1927
- function SketchStudio({ open, agentEnabled, onOpen, onClose, attachSketch, enabled, t, incoming, sessionId, rpc }) {
2296
+ function SketchStudio({ open, agentEnabled, agentPreview, onOpen, onClose, attachSketch, enabled, t, incoming, sessionId, rpc }) {
1928
2297
  const dialog = (0, react.useRef)(null), canvas = (0, react.useRef)(null), doc = (0, react.useRef)(createSketchLayers()), cache = (0, react.useRef)(/* @__PURE__ */ new Map());
1929
2298
  const undo = (0, react.useRef)([]), redo = (0, react.useRef)([]), active = (0, react.useRef)(null), frame = (0, react.useRef)(null);
1930
2299
  const images = (0, react.useRef)(/* @__PURE__ */ new Map()), saved = (0, react.useRef)(null), dirty = (0, react.useRef)(false), updateUi = (0, react.useRef)(false);
1931
2300
  const documentId = (0, react.useRef)(crypto.randomUUID()), documentRevision = (0, react.useRef)(0), agentAdapter = (0, react.useRef)({}), agentSession = (0, react.useRef)(null);
2301
+ const [agentState, setAgentState] = (0, react.useState)("idle"), agentRun = (0, react.useRef)(null);
2302
+ const agentLocked = agentState === "drawing";
2303
+ const [noticeHidden, setNoticeHidden] = (0, react.useState)(false);
1932
2304
  const [stability, setStability] = (0, react.useState)(0), [flow, setFlow] = (0, react.useState)(100), [picturesOpen, setPicturesOpen] = (0, react.useState)(false);
1933
2305
  const pictureInput = (0, react.useRef)(null), received = (0, react.useRef)(null);
1934
2306
  const navigation = useSketchView(canvas, open);
1935
2307
  const [revision, redraw] = (0, react.useState)(0), [tool, setTool] = (0, react.useState)("pen"), [brush, setBrush] = (0, react.useState)("pen");
1936
2308
  const [eraser, setEraser] = (0, react.useState)("pixel"), [color, setColor] = (0, react.useState)("#0088ff"), [width, setWidth] = (0, react.useState)(12);
2309
+ const [selection, setSelection] = (0, react.useState)(null), [textEdit, setTextEdit] = (0, react.useState)(null), [shapesOpen, setShapesOpen] = (0, react.useState)(false);
2310
+ const sizeGesture = (0, react.useRef)(false);
2311
+ const [sizeMode, setSizeMode] = (0, react.useState)("size");
2312
+ const showOpacity = tool !== "eraser";
2313
+ const opacityMode = showOpacity && sizeMode === "opacity";
2314
+ const selected = doc.current.layers.find((l) => l.id === selection?.layer)?.strokes.find((s, i) => objectId(s, i) === selection?.id);
2315
+ const editObject = (patch, action = "update") => {
2316
+ if (agentRun.current?.locked || busy || !selected) return;
2317
+ try {
2318
+ const next = applySketchCommands(doc.current, [{
2319
+ op: "object",
2320
+ ...selection,
2321
+ action,
2322
+ patch
2323
+ }]);
2324
+ if (!sizeGesture.current) checkpoint();
2325
+ doc.current = next;
2326
+ schedule();
2327
+ if (action === "delete") setSelection(null);
2328
+ } catch (e) {
2329
+ setError(e.message);
2330
+ }
2331
+ };
2332
+ const pickColor = (value) => {
2333
+ setColor(value);
2334
+ if (selected) editObject({ color: value });
2335
+ };
1937
2336
  const [fillShape, setFillShape] = (0, react.useState)(false);
1938
2337
  const [layersOpen, setLayersOpen] = (0, react.useState)(false), [busy, setBusy] = (0, react.useState)(false), [error, setError] = (0, react.useState)("");
1939
2338
  const cursorRing = (0, react.useRef)(null);
1940
- const cursor = useSketchCursor(canvas, cursorRing, width, tool === "pen" ? brush : "pen", navigation.view.scale, !open || navigation.space || busy);
2339
+ const cursor = useSketchCursor(canvas, cursorRing, width, tool === "pen" ? brush : "pen", navigation.view.scale, !open || navigation.space || busy || agentLocked || tool === "select" || tool === "text");
2340
+ useSketchDismiss(shapesOpen, setShapesOpen, dialog, [".codexSketchShapeMenu", ".codexSketchShapeToggle"]);
1941
2341
  useSketchDismiss(layersOpen, setLayersOpen, dialog, [".codexSketchLayers", ".codexSketchLayersToggle"]);
1942
2342
  useSketchDismiss(picturesOpen, setPicturesOpen, dialog, [".codexSketchPictures", ".codexSketchPicturesToggle"]);
1943
2343
  const paint = () => {
@@ -1967,7 +2367,7 @@ window.__ModuleLoader__.load({
1967
2367
  redo.current = [];
1968
2368
  };
1969
2369
  const change = (action, id, value) => {
1970
- if (busy || active.current) return;
2370
+ if (busy || agentRun.current?.locked || active.current) return;
1971
2371
  const next = changeSketchLayer(doc.current, action, id, value);
1972
2372
  if (next === doc.current) return;
1973
2373
  if (action !== "select") checkpoint();
@@ -1979,12 +2379,14 @@ window.__ModuleLoader__.load({
1979
2379
  (0, react.useEffect)(() => {
1980
2380
  if (open) {
1981
2381
  dialog.current.showModal();
2382
+ canvas.current.width = doc.current.width ?? 1024;
1982
2383
  paint();
1983
2384
  } else dialog.current?.close();
1984
2385
  }, [open]);
1985
2386
  (0, react.useEffect)(() => () => {
1986
2387
  cancelAnimationFrame(frame.current);
1987
2388
  cache.current.clear();
2389
+ agentRun.current?.dispose();
1988
2390
  }, []);
1989
2391
  const hasContent = () => doc.current.layers.some((layer) => layer.image || layer.strokes.length);
1990
2392
  const save = async (name) => {
@@ -2007,7 +2409,9 @@ window.__ModuleLoader__.load({
2007
2409
  const replace = (next, decoded, identity) => {
2008
2410
  documentId.current = crypto.randomUUID();
2009
2411
  documentRevision.current++;
2010
- doc.current = structuredClone(next);
2412
+ doc.current = identifyObjects(structuredClone(next));
2413
+ setSelection(null);
2414
+ setTextEdit(null);
2011
2415
  images.current = decoded;
2012
2416
  cache.current.clear();
2013
2417
  undo.current = [];
@@ -2068,6 +2472,10 @@ window.__ModuleLoader__.load({
2068
2472
  schedule();
2069
2473
  };
2070
2474
  const close = async () => {
2475
+ if (agentRun.current?.locked) {
2476
+ onClose();
2477
+ return;
2478
+ }
2071
2479
  if (busy || active.current) return;
2072
2480
  setBusy(true);
2073
2481
  try {
@@ -2080,7 +2488,7 @@ window.__ModuleLoader__.load({
2080
2488
  }
2081
2489
  };
2082
2490
  const runFile = async (operation) => {
2083
- if (busy || active.current) return;
2491
+ if (busy || agentRun.current?.locked || active.current) return;
2084
2492
  setBusy(true);
2085
2493
  setError("");
2086
2494
  try {
@@ -2092,15 +2500,33 @@ window.__ModuleLoader__.load({
2092
2500
  }
2093
2501
  };
2094
2502
  (0, react.useEffect)(() => {
2095
- if (open && incoming && incoming !== received.current) {
2503
+ if (open && !agentLocked && incoming && incoming !== received.current) {
2096
2504
  received.current = incoming;
2097
2505
  runFile(() => importImage(incoming.file));
2098
2506
  }
2099
- }, [open, incoming]);
2507
+ }, [
2508
+ open,
2509
+ incoming,
2510
+ agentLocked
2511
+ ]);
2100
2512
  const keyDown = (event) => {
2101
2513
  if (event.target.closest("input,textarea,select,[contenteditable=true]") || event.isComposing || busy || active.current) return;
2102
2514
  if (!navigation.shortcuts) return;
2103
2515
  if (navigation.keyDown(event)) return;
2516
+ if (agentRun.current?.locked) return;
2517
+ if (selection && ["Delete", "Backspace"].includes(event.key)) {
2518
+ event.preventDefault();
2519
+ editObject({}, "delete");
2520
+ return;
2521
+ }
2522
+ if (event.key.toLowerCase() === "v") {
2523
+ setTool("select");
2524
+ return;
2525
+ }
2526
+ if (event.key.toLowerCase() === "t") {
2527
+ setTool("text");
2528
+ return;
2529
+ }
2104
2530
  const key = event.key.toLowerCase(), command = event.ctrlKey || event.metaKey;
2105
2531
  if (command && [
2106
2532
  "z",
@@ -2137,6 +2563,32 @@ window.__ModuleLoader__.load({
2137
2563
  const gesture = active.current;
2138
2564
  if (!gesture || gesture.id !== event.pointerId || busy) return;
2139
2565
  const rect = bounds ?? canvas.current.getBoundingClientRect();
2566
+ if (gesture.object) {
2567
+ const point = sketchPoint(event.clientX, event.clientY, rect);
2568
+ if (!point) return;
2569
+ try {
2570
+ const box = objectBounds(gesture.object);
2571
+ const stroke = gesture.handle === "end" ? {
2572
+ ...gesture.object,
2573
+ points: [gesture.object.points[0], point]
2574
+ } : gesture.handle === "size" ? transformObject(gesture.object, {
2575
+ scaleX: Math.max(.001, point.x - box.x) / Math.max(.001, box.width),
2576
+ scaleY: Math.max(.001, point.y - box.y) / Math.max(.001, box.height)
2577
+ }) : transformObject(gesture.object, {
2578
+ dx: point.x - gesture.start.x,
2579
+ dy: point.y - gesture.start.y
2580
+ });
2581
+ doc.current = {
2582
+ ...doc.current,
2583
+ layers: doc.current.layers.map((l) => l.id === gesture.layer ? {
2584
+ ...l,
2585
+ strokes: l.strokes.map((s) => s.id === gesture.object.id ? stroke : s)
2586
+ } : l)
2587
+ };
2588
+ schedule();
2589
+ } catch {}
2590
+ return;
2591
+ }
2140
2592
  const native = event.nativeEvent ?? event;
2141
2593
  const events = native.getCoalescedEvents?.() ?? [];
2142
2594
  for (const sample of events.length ? [...events, native] : [native]) {
@@ -2157,6 +2609,7 @@ window.__ModuleLoader__.load({
2157
2609
  const stroke = layer.strokes.at(-1);
2158
2610
  if ([
2159
2611
  "line",
2612
+ "arrow",
2160
2613
  "rectangle",
2161
2614
  "circle"
2162
2615
  ].includes(stroke.shape)) {
@@ -2194,13 +2647,29 @@ window.__ModuleLoader__.load({
2194
2647
  const gesture = active.current, stroke = doc.current.layers.find((layer) => layer.id === gesture.layer)?.strokes.at(-1);
2195
2648
  if (gesture.smoothing && stroke) stroke.points = smoothStrokePoints(stroke.points, gesture.smoothing);
2196
2649
  }
2650
+ const drawn = active.current;
2651
+ if (!cancel && !drawn.object && [
2652
+ "line",
2653
+ "arrow",
2654
+ "rectangle",
2655
+ "circle"
2656
+ ].includes(tool)) {
2657
+ const stroke = doc.current.layers.find((l) => l.id === drawn.layer)?.strokes.at(-1);
2658
+ if (stroke) {
2659
+ setSelection({
2660
+ layer: drawn.layer,
2661
+ id: stroke.id
2662
+ });
2663
+ setTool("select");
2664
+ }
2665
+ }
2197
2666
  active.current = null;
2198
2667
  if (canvas.current.hasPointerCapture(event.pointerId)) canvas.current.releasePointerCapture(event.pointerId);
2199
2668
  if (cancel) doc.current = undo.current.pop() ?? doc.current;
2200
2669
  schedule();
2201
2670
  };
2202
2671
  const history = (direction) => {
2203
- if (busy || active.current) return;
2672
+ if (busy || agentRun.current?.locked || active.current) return;
2204
2673
  const source = direction === "undo" ? undo : redo, target = direction === "undo" ? redo : undo;
2205
2674
  if (!source.current.length) return;
2206
2675
  documentRevision.current++;
@@ -2210,9 +2679,10 @@ window.__ModuleLoader__.load({
2210
2679
  schedule();
2211
2680
  };
2212
2681
  Object.assign(agentAdapter.current, {
2213
- available: () => open && enabled,
2682
+ available: () => enabled && agentEnabled,
2683
+ previewEnabled: () => agentPreview,
2214
2684
  open: () => onOpen(),
2215
- busy: () => busy || Boolean(active.current),
2685
+ busy: () => busy || Boolean(active.current) || Boolean(textEdit) || sizeGesture.current,
2216
2686
  document: () => doc.current,
2217
2687
  snapshot: () => ({
2218
2688
  documentId: documentId.current,
@@ -2229,6 +2699,15 @@ window.__ModuleLoader__.load({
2229
2699
  })),
2230
2700
  strokeCount: strokeCount(doc.current)
2231
2701
  }),
2702
+ objects: () => sketchObjectSummary(doc.current),
2703
+ object: (id, layer = doc.current.active) => {
2704
+ const target = doc.current.layers.find((l) => l.id === layer)?.strokes.find((s, i) => objectId(s, i) === id);
2705
+ if (!target) throw Error("Object not found");
2706
+ return {
2707
+ ...target,
2708
+ id
2709
+ };
2710
+ },
2232
2711
  commit: (next) => {
2233
2712
  checkpoint();
2234
2713
  doc.current = next;
@@ -2239,24 +2718,27 @@ window.__ModuleLoader__.load({
2239
2718
  paint();
2240
2719
  return canvas.current.toDataURL("image/png");
2241
2720
  },
2242
- save: async (name) => {
2243
- setBusy(true);
2244
- try {
2245
- await save(name);
2246
- } finally {
2247
- setBusy(false);
2248
- }
2249
- }
2721
+ save
2250
2722
  });
2251
2723
  agentSession.current ??= createSketchCommandSession(agentAdapter.current);
2724
+ agentRun.current ??= createSketchAgentRun({
2725
+ execute: (request) => agentSession.current(request),
2726
+ open: () => agentAdapter.current.open(),
2727
+ changed: (state) => {
2728
+ setAgentState(state);
2729
+ setNoticeHidden(false);
2730
+ },
2731
+ busy: () => agentAdapter.current.busy(),
2732
+ previewEnabled: () => agentAdapter.current.previewEnabled()
2733
+ });
2252
2734
  (0, react.useEffect)(() => {
2253
- if (!open || !enabled || !agentEnabled) return;
2735
+ if (!enabled || !agentEnabled) return;
2254
2736
  const api = Object.freeze({
2255
2737
  version: 1,
2256
2738
  sessionId,
2257
- execute: (request) => agentSession.current(request),
2739
+ execute: (request) => agentRun.current.execute(request),
2258
2740
  export: async (format) => {
2259
- if (agentAdapter.current.busy()) throw Error("Sketch is being edited");
2741
+ if (agentAdapter.current.busy() || agentRun.current.locked) throw Error("Sketch is being edited");
2260
2742
  const { blob, extension } = await agentAdapter.current.export(format);
2261
2743
  const data = new Uint8Array(await blob.arrayBuffer());
2262
2744
  let raw = "";
@@ -2273,7 +2755,6 @@ window.__ModuleLoader__.load({
2273
2755
  if (window.dshSketchAgent === api) delete window.dshSketchAgent;
2274
2756
  };
2275
2757
  }, [
2276
- open,
2277
2758
  enabled,
2278
2759
  agentEnabled,
2279
2760
  rpc,
@@ -2282,14 +2763,17 @@ window.__ModuleLoader__.load({
2282
2763
  (0, react.useEffect)(() => {
2283
2764
  if (!enabled || !agentEnabled || !rpc || !sessionId) return;
2284
2765
  let live = true;
2285
- const disconnect = connectSketchAgent(rpc, sessionId, (request) => executeSketchFromAgent(request, {
2286
- available: () => agentAdapter.current.available(),
2287
- open: () => agentAdapter.current.open(),
2288
- execute: (request) => agentSession.current(request),
2289
- live: () => live
2290
- }), (message) => setError(message), () => agentAdapter.current.available() ? 350 : 1500);
2766
+ agentRun.current.resume();
2767
+ const disconnect = connectSketchAgent(rpc, sessionId, (request) => {
2768
+ if (!live) throw Error("Sketch session disconnected");
2769
+ return agentRun.current.execute(request);
2770
+ }, (message) => {
2771
+ agentRun.current.fail();
2772
+ setError(message);
2773
+ }, () => 350);
2291
2774
  return () => {
2292
2775
  live = false;
2776
+ agentRun.current.stop();
2293
2777
  disconnect();
2294
2778
  };
2295
2779
  }, [
@@ -2299,7 +2783,7 @@ window.__ModuleLoader__.load({
2299
2783
  sessionId
2300
2784
  ]);
2301
2785
  const attach = async () => {
2302
- if (!enabled || busy) return;
2786
+ if (!enabled || busy || agentRun.current?.locked) return;
2303
2787
  setBusy(true);
2304
2788
  setError("");
2305
2789
  try {
@@ -2342,13 +2826,36 @@ window.__ModuleLoader__.load({
2342
2826
  link.remove();
2343
2827
  setTimeout(() => URL.revokeObjectURL(url), 1e4);
2344
2828
  };
2345
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("dialog", {
2829
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [enabled && agentEnabled && !open && !noticeHidden && agentState !== "idle" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2830
+ className: "codexSketchBackgroundStatus",
2831
+ role: "status",
2832
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2833
+ type: "button",
2834
+ onClick: onOpen,
2835
+ children: t(`sketchRun_${agentState}`)
2836
+ }), agentLocked ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2837
+ type: "button",
2838
+ "aria-label": t("sketchRunStop"),
2839
+ onClick: () => agentRun.current.stop(),
2840
+ children: "×"
2841
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2842
+ type: "button",
2843
+ "aria-label": t("sketchCancel"),
2844
+ onClick: () => setNoticeHidden(true),
2845
+ children: "×"
2846
+ })]
2847
+ }) : null, /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("dialog", {
2346
2848
  ref: dialog,
2347
2849
  className: "codexSketchDialog codexSketchStudio codexLayerStudio",
2348
2850
  "aria-label": t("sketchTitle"),
2349
2851
  onKeyDown: keyDown,
2350
2852
  onKeyUp: navigation.keyUp,
2351
2853
  onPaste: (event) => {
2854
+ if (agentRun.current?.locked) {
2855
+ event.preventDefault();
2856
+ event.stopPropagation();
2857
+ return;
2858
+ }
2352
2859
  if (event.target.closest("input,textarea")) return;
2353
2860
  const file = Array.from(event.clipboardData.items).find((item) => item.type.startsWith("image/"))?.getAsFile();
2354
2861
  if (file) {
@@ -2383,7 +2890,7 @@ window.__ModuleLoader__.load({
2383
2890
  type: "button",
2384
2891
  "aria-label": t("sketchCancel"),
2385
2892
  title: t("sketchCancel"),
2386
- disabled: busy,
2893
+ disabled: busy && !agentLocked,
2387
2894
  onClick: close,
2388
2895
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(WorkspaceIcon, { name: "close" })
2389
2896
  }),
@@ -2394,7 +2901,7 @@ window.__ModuleLoader__.load({
2394
2901
  importImage,
2395
2902
  download,
2396
2903
  hasContent: doc.current.layers.some((l) => l.visible && (l.image || l.strokes.length)),
2397
- disabled: busy,
2904
+ disabled: agentLocked || busy,
2398
2905
  t,
2399
2906
  report: setError,
2400
2907
  onWorking: setBusy
@@ -2413,7 +2920,7 @@ window.__ModuleLoader__.load({
2413
2920
  className: "codexSketchRound",
2414
2921
  "aria-label": t(name === "undo" ? "sketchUndo" : "sketchRedo"),
2415
2922
  title: t(name === "undo" ? "sketchUndo" : "sketchRedo"),
2416
- disabled: busy || !(name === "undo" ? undo : redo).current.length,
2923
+ disabled: agentLocked || busy || !(name === "undo" ? undo : redo).current.length,
2417
2924
  onClick: () => history(name),
2418
2925
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(WorkspaceIcon, {
2419
2926
  name,
@@ -2426,9 +2933,9 @@ window.__ModuleLoader__.load({
2426
2933
  "aria-label": t("sketchRatio"),
2427
2934
  title: t("sketchRatioHint"),
2428
2935
  value: doc.current.ratio ?? "1:1",
2429
- disabled: busy,
2936
+ disabled: agentLocked || busy,
2430
2937
  onChange: (event) => {
2431
- if (active.current) return;
2938
+ if (active.current || agentRun.current?.locked) return;
2432
2939
  const next = resizeSketch(doc.current, event.target.value);
2433
2940
  if (next === doc.current) return;
2434
2941
  checkpoint();
@@ -2469,7 +2976,7 @@ window.__ModuleLoader__.load({
2469
2976
  type: "button",
2470
2977
  className: "codexSketchConfirm",
2471
2978
  "aria-label": t("sketchAttach"),
2472
- disabled: busy || !enabled || !doc.current.layers.some((l) => l.visible && (l.strokes.length || l.image)),
2979
+ disabled: agentLocked || busy || !enabled || !doc.current.layers.some((l) => l.visible && (l.strokes.length || l.image)),
2473
2980
  onClick: () => void attach(),
2474
2981
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(WorkspaceIcon, {
2475
2982
  name: "check",
@@ -2478,6 +2985,25 @@ window.__ModuleLoader__.load({
2478
2985
  })
2479
2986
  ]
2480
2987
  }),
2988
+ enabled && agentEnabled && agentState !== "idle" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2989
+ className: "codexSketchAgentStatus",
2990
+ role: "status",
2991
+ "aria-live": "polite",
2992
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t(`sketchRun_${agentState}`) }), agentLocked ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2993
+ type: "button",
2994
+ "aria-label": t("sketchRunStop"),
2995
+ title: t("sketchRunStop"),
2996
+ onClick: () => agentRun.current.stop(),
2997
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(WorkspaceIcon, {
2998
+ name: "close",
2999
+ size: 16
3000
+ })
3001
+ }) : agentState === "stopped" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
3002
+ type: "button",
3003
+ onClick: () => agentRun.current.resume(),
3004
+ children: t("sketchRunResume")
3005
+ }) : null]
3006
+ }) : null,
2481
3007
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2482
3008
  className: `codexLayerBody ${layersOpen ? "withLayers" : ""}`,
2483
3009
  children: [
@@ -2485,7 +3011,7 @@ window.__ModuleLoader__.load({
2485
3011
  tabIndex: 0,
2486
3012
  style: {
2487
3013
  transform: `translate(${navigation.view.x}px,${navigation.view.y}px) scale(${navigation.view.scale})`,
2488
- cursor: navigation.space ? "grab" : "none",
3014
+ cursor: navigation.space ? "grab" : agentLocked ? "default" : tool === "select" ? "default" : tool === "text" ? "text" : "none",
2489
3015
  "--sketch-ratio": (doc.current.width ?? 1024) / (doc.current.height ?? 1024)
2490
3016
  },
2491
3017
  ref: canvas,
@@ -2495,7 +3021,69 @@ window.__ModuleLoader__.load({
2495
3021
  onPointerDown: (event) => {
2496
3022
  if (busy || !enabled || active.current) return;
2497
3023
  if (navigation.down(event)) return;
3024
+ if (agentRun.current?.locked) return;
2498
3025
  if (event.button !== 0) return;
3026
+ const point = sketchPoint(event.clientX, event.clientY, canvas.current.getBoundingClientRect());
3027
+ if (!point) return;
3028
+ if (tool === "text") {
3029
+ setTextEdit({
3030
+ point,
3031
+ value: ""
3032
+ });
3033
+ return;
3034
+ }
3035
+ if (tool === "select") {
3036
+ doc.current = identifyObjects(doc.current);
3037
+ if (selected) {
3038
+ const b = objectBounds(selected), end = ["line", "arrow"].includes(selected.shape) ? selected.points.at(-1) : {
3039
+ x: b.x + b.width,
3040
+ y: b.y + b.height
3041
+ }, rect = canvas.current.getBoundingClientRect();
3042
+ if (Math.hypot((end.x - point.x) * rect.width, (end.y - point.y) * rect.height) < 12) {
3043
+ checkpoint();
3044
+ active.current = {
3045
+ id: event.pointerId,
3046
+ layer: selection.layer,
3047
+ object: {
3048
+ ...selected,
3049
+ id: selection.id
3050
+ },
3051
+ start: point,
3052
+ handle: ["line", "arrow"].includes(selected.shape) ? "end" : "size"
3053
+ };
3054
+ canvas.current.setPointerCapture(event.pointerId);
3055
+ return;
3056
+ }
3057
+ }
3058
+ let hit;
3059
+ for (const l of doc.current.layers.slice().reverse()) {
3060
+ if (!l.visible) continue;
3061
+ const stroke = l.strokes.slice().reverse().find((s) => s.shape !== "eraser" && strokeHit(s, point, 6, doc.current.width, doc.current.height));
3062
+ if (stroke) {
3063
+ hit = {
3064
+ layer: l.id,
3065
+ stroke
3066
+ };
3067
+ break;
3068
+ }
3069
+ }
3070
+ setSelection(hit ? {
3071
+ layer: hit.layer,
3072
+ id: hit.stroke.id
3073
+ } : null);
3074
+ if (hit) {
3075
+ checkpoint();
3076
+ active.current = {
3077
+ id: event.pointerId,
3078
+ layer: hit.layer,
3079
+ object: hit.stroke,
3080
+ start: point
3081
+ };
3082
+ canvas.current.setPointerCapture(event.pointerId);
3083
+ }
3084
+ return;
3085
+ }
3086
+ setSelection(null);
2499
3087
  cursor.down(event);
2500
3088
  if (!current.visible) {
2501
3089
  setError(t("sketchHiddenLayer"));
@@ -2523,6 +3111,7 @@ window.__ModuleLoader__.load({
2523
3111
  const layer = layers.find((layer) => layer.id === doc.current.active);
2524
3112
  const eraseStroke = tool === "eraser" && eraser === "stroke";
2525
3113
  if (!eraseStroke) layer.strokes.push({
3114
+ id: crypto.randomUUID(),
2526
3115
  color,
2527
3116
  opacity: flow / 100,
2528
3117
  shape: tool,
@@ -2554,11 +3143,131 @@ window.__ModuleLoader__.load({
2554
3143
  onPointerUp: (event) => end(event),
2555
3144
  onPointerCancel: (event) => end(event, true)
2556
3145
  }),
3146
+ selected && tool === "select" && !agentLocked ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("svg", {
3147
+ className: "codexSketchSelection",
3148
+ viewBox: "0 0 1 1",
3149
+ preserveAspectRatio: "none",
3150
+ style: {
3151
+ "--sketch-ratio": (doc.current.width ?? 1024) / (doc.current.height ?? 1024),
3152
+ transform: `translate(${navigation.view.x}px,${navigation.view.y}px) scale(${navigation.view.scale})`
3153
+ },
3154
+ "aria-hidden": "true",
3155
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("rect", {
3156
+ ...objectBounds(selected),
3157
+ fill: "none",
3158
+ stroke: "#0088ff",
3159
+ strokeWidth: ".002",
3160
+ strokeDasharray: ".008 .005"
3161
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("circle", {
3162
+ cx: ["line", "arrow"].includes(selected.shape) ? selected.points.at(-1).x : objectBounds(selected).x + objectBounds(selected).width,
3163
+ cy: ["line", "arrow"].includes(selected.shape) ? selected.points.at(-1).y : objectBounds(selected).y + objectBounds(selected).height,
3164
+ r: ".007",
3165
+ fill: "white",
3166
+ stroke: "#0088ff",
3167
+ strokeWidth: ".002"
3168
+ })]
3169
+ }) : null,
3170
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SketchSizeControl, {
3171
+ label: t(opacityMode ? "sketchFlow" : selected?.shape === "text" || tool === "text" ? "sketchTextSize" : "sketchWidth"),
3172
+ mode: opacityMode ? "opacity" : "size",
3173
+ modes: showOpacity ? [{
3174
+ value: "size",
3175
+ label: t(selected?.shape === "text" || tool === "text" ? "sketchTextSize" : "sketchSizeShort")
3176
+ }, {
3177
+ value: "opacity",
3178
+ label: t("sketchFlow")
3179
+ }] : void 0,
3180
+ onModeChange: setSizeMode,
3181
+ min: opacityMode ? 5 : 1,
3182
+ max: opacityMode ? 100 : 256,
3183
+ suffix: opacityMode ? "%" : "",
3184
+ value: opacityMode ? (selected?.opacity ?? flow / 100) * 100 : selected?.width ?? width,
3185
+ disabled: agentLocked || busy,
3186
+ onStart: () => {
3187
+ if (selected) {
3188
+ checkpoint();
3189
+ sizeGesture.current = true;
3190
+ }
3191
+ },
3192
+ onEnd: () => {
3193
+ sizeGesture.current = false;
3194
+ },
3195
+ onChange: (value) => {
3196
+ if (opacityMode) {
3197
+ setFlow(value);
3198
+ if (selected) editObject({ opacity: value / 100 });
3199
+ } else {
3200
+ setWidth(value);
3201
+ if (selected) editObject({ width: value });
3202
+ }
3203
+ }
3204
+ }),
3205
+ textEdit ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("form", {
3206
+ className: "codexSketchTextEditor",
3207
+ onSubmit: (event) => {
3208
+ event.preventDefault();
3209
+ if (!textEdit.value.trim()) {
3210
+ setTextEdit(null);
3211
+ return;
3212
+ }
3213
+ try {
3214
+ if (textEdit.selection) editObject({ text: textEdit.value });
3215
+ else {
3216
+ const a = textEdit.point, b = {
3217
+ x: Math.min(1, a.x + .35),
3218
+ y: Math.min(1, a.y + .15)
3219
+ }, id = crypto.randomUUID();
3220
+ const next = applySketchCommands(doc.current, [{
3221
+ op: "stroke",
3222
+ id,
3223
+ shape: "text",
3224
+ text: textEdit.value,
3225
+ color,
3226
+ width: Math.max(24, width),
3227
+ points: [a, b]
3228
+ }]);
3229
+ checkpoint();
3230
+ doc.current = next;
3231
+ setSelection({
3232
+ layer: doc.current.active,
3233
+ id
3234
+ });
3235
+ schedule();
3236
+ }
3237
+ setTextEdit(null);
3238
+ setTool("select");
3239
+ } catch (e) {
3240
+ setError(e.message);
3241
+ }
3242
+ },
3243
+ children: [
3244
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("textarea", {
3245
+ autoFocus: true,
3246
+ "aria-label": t("sketchText"),
3247
+ maxLength: 500,
3248
+ value: textEdit.value,
3249
+ onChange: (e) => setTextEdit({
3250
+ ...textEdit,
3251
+ value: e.target.value
3252
+ })
3253
+ }),
3254
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
3255
+ type: "submit",
3256
+ children: t("sketchTextDone")
3257
+ }),
3258
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
3259
+ type: "button",
3260
+ onClick: () => setTextEdit(null),
3261
+ children: t("sketchCancel")
3262
+ })
3263
+ ]
3264
+ }) : null,
2557
3265
  picturesOpen ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("aside", {
2558
3266
  className: "codexSketchPictures",
2559
3267
  children: [
2560
3268
  /* @__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", {
2561
3269
  type: "button",
3270
+ disabled: agentLocked || busy,
2562
3271
  onClick: () => pictureInput.current.click(),
2563
3272
  children: t("sketchPictureAdd")
2564
3273
  })] }),
@@ -2577,6 +3286,7 @@ window.__ModuleLoader__.load({
2577
3286
  "data-active": layer.id === doc.current.active,
2578
3287
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2579
3288
  type: "button",
3289
+ disabled: agentLocked || busy,
2580
3290
  "aria-label": `${t("sketchPictureSelect")} ${layer.name}`,
2581
3291
  onClick: () => change("select", layer.id),
2582
3292
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("img", {
@@ -2585,6 +3295,7 @@ window.__ModuleLoader__.load({
2585
3295
  })
2586
3296
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2587
3297
  type: "button",
3298
+ disabled: agentLocked || busy,
2588
3299
  "aria-label": `${t("sketchDeleteDraft")} ${layer.name}`,
2589
3300
  onClick: () => change(doc.current.layers.length === 1 ? "clear" : "delete", layer.id),
2590
3301
  children: "×"
@@ -2601,7 +3312,7 @@ window.__ModuleLoader__.load({
2601
3312
  type: "button",
2602
3313
  title: t("sketchLayerAdd"),
2603
3314
  "aria-label": t("sketchLayerAdd"),
2604
- disabled: busy || doc.current.layers.length >= 8,
3315
+ disabled: agentLocked || busy || doc.current.layers.length >= 8,
2605
3316
  onClick: () => change("add"),
2606
3317
  children: "+"
2607
3318
  })] }),
@@ -2612,6 +3323,7 @@ window.__ModuleLoader__.load({
2612
3323
  "data-active": layer.id === doc.current.active,
2613
3324
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2614
3325
  type: "button",
3326
+ disabled: agentLocked || busy,
2615
3327
  "aria-label": `${t("sketchLayerVisible")} ${layer.id}`,
2616
3328
  "aria-pressed": layer.visible,
2617
3329
  onClick: () => change("visible", layer.id),
@@ -2621,6 +3333,7 @@ window.__ModuleLoader__.load({
2621
3333
  })
2622
3334
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2623
3335
  type: "button",
3336
+ disabled: agentLocked || busy,
2624
3337
  "aria-pressed": layer.id === doc.current.active,
2625
3338
  onClick: () => change("select", layer.id),
2626
3339
  children: layer.name || `${t("sketchLayer")} ${layer.id}`
@@ -2632,6 +3345,7 @@ window.__ModuleLoader__.load({
2632
3345
  children: t("sketchLayerName")
2633
3346
  }),
2634
3347
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
3348
+ disabled: agentLocked || busy,
2635
3349
  "aria-label": t("sketchLayerName"),
2636
3350
  defaultValue: current.name,
2637
3351
  placeholder: `${t("sketchLayer")} ${current.id}`,
@@ -2654,7 +3368,7 @@ window.__ModuleLoader__.load({
2654
3368
  type: "button",
2655
3369
  title: t(`sketchLayer_${action}`),
2656
3370
  "aria-label": t(`sketchLayer_${action}`),
2657
- disabled: busy || action === "delete" && doc.current.layers.length === 1 || action === "duplicate" && (doc.current.layers.length >= 8 || strokeCount(doc.current) + current.strokes.length > 2e3) || action === "up" && current === doc.current.layers.at(-1) || action === "down" && current === doc.current.layers[0],
3371
+ disabled: agentLocked || busy || action === "delete" && doc.current.layers.length === 1 || action === "duplicate" && (doc.current.layers.length >= 8 || strokeCount(doc.current) + current.strokes.length > 2e3) || action === "up" && current === doc.current.layers.at(-1) || action === "down" && current === doc.current.layers[0],
2658
3372
  onClick: () => change(action),
2659
3373
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(WorkspaceIcon, {
2660
3374
  name: action === "delete" ? "clear" : action,
@@ -2665,7 +3379,7 @@ window.__ModuleLoader__.load({
2665
3379
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
2666
3380
  type: "button",
2667
3381
  className: "codexSketchClearLayer",
2668
- disabled: busy || !current.strokes.length && !current.image,
3382
+ disabled: agentLocked || busy || !current.strokes.length && !current.image,
2669
3383
  onClick: () => change("clear"),
2670
3384
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(WorkspaceIcon, {
2671
3385
  name: "clear",
@@ -2690,57 +3404,123 @@ window.__ModuleLoader__.load({
2690
3404
  navigation,
2691
3405
  t
2692
3406
  }),
2693
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
3407
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2694
3408
  className: "codexSketchPill",
2695
3409
  role: "toolbar",
2696
3410
  "aria-label": t("sketchTitle"),
2697
3411
  title: t("sketchShortcuts"),
2698
3412
  children: [
2699
- "pen",
2700
- "pencil",
2701
- "marker",
2702
- "eraser",
2703
- "line",
2704
- "rectangle",
2705
- "circle"
2706
- ].map((name) => {
2707
- const drawing = [
3413
+ [
3414
+ "select",
2708
3415
  "pen",
2709
- "pencil",
2710
- "marker"
2711
- ].includes(name);
2712
- const label = t(drawing ? `sketchBrush_${name}` : `sketchTool_${name}`);
2713
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
3416
+ "text",
3417
+ "eraser"
3418
+ ].map((name) => {
3419
+ const drawing = [
3420
+ "pen",
3421
+ "pencil",
3422
+ "marker"
3423
+ ].includes(name);
3424
+ const label = t(drawing ? `sketchBrush_${name}` : `sketchTool_${name}`);
3425
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
3426
+ type: "button",
3427
+ "aria-label": label,
3428
+ title: label,
3429
+ "aria-pressed": drawing ? tool === "pen" && brush === name : tool === name,
3430
+ disabled: agentLocked || busy,
3431
+ onClick: () => {
3432
+ setTool(drawing ? "pen" : name);
3433
+ if (name !== "select") setSelection(null);
3434
+ if (drawing) setBrush(name);
3435
+ },
3436
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(WorkspaceIcon, {
3437
+ name,
3438
+ size: 23
3439
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: label })]
3440
+ }, name);
3441
+ }),
3442
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
3443
+ className: "codexSketchShapeToggle",
2714
3444
  type: "button",
2715
- "aria-label": label,
2716
- title: label,
2717
- "aria-pressed": drawing ? tool === "pen" && brush === name : tool === name,
2718
- disabled: busy,
2719
- onClick: () => {
2720
- setTool(drawing ? "pen" : name);
2721
- if (drawing) setBrush(name);
2722
- },
3445
+ "aria-expanded": shapesOpen,
3446
+ disabled: agentLocked || busy,
3447
+ onClick: () => setShapesOpen((v) => !v),
2723
3448
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(WorkspaceIcon, {
2724
- name,
3449
+ name: "rectangle",
2725
3450
  size: 23
2726
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: label })]
2727
- }, name);
2728
- })
3451
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("sketchShapes") })]
3452
+ }),
3453
+ shapesOpen ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
3454
+ className: "codexSketchShapeMenu",
3455
+ children: [
3456
+ "line",
3457
+ "arrow",
3458
+ "rectangle",
3459
+ "circle"
3460
+ ].map((name) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
3461
+ type: "button",
3462
+ onClick: () => {
3463
+ setTool(name);
3464
+ setSelection(null);
3465
+ setShapesOpen(false);
3466
+ },
3467
+ children: t(`sketchTool_${name}`)
3468
+ }, name))
3469
+ }) : null
3470
+ ]
2729
3471
  }),
2730
3472
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2731
3473
  className: "codexLayerBrush",
2732
3474
  children: [
2733
3475
  ["rectangle", "circle"].includes(tool) ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
2734
3476
  type: "checkbox",
3477
+ disabled: agentLocked || busy,
2735
3478
  checked: fillShape,
2736
3479
  onChange: (e) => setFillShape(e.target.checked)
2737
3480
  }), t("sketchFill")] }) : null,
3481
+ tool === "pen" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("select", {
3482
+ "aria-label": t("sketchBrush"),
3483
+ value: brush,
3484
+ onChange: (e) => setBrush(e.target.value),
3485
+ disabled: agentLocked || busy,
3486
+ children: [
3487
+ "pen",
3488
+ "pencil",
3489
+ "marker"
3490
+ ].map((b) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
3491
+ value: b,
3492
+ children: t(`sketchBrush_${b}`)
3493
+ }, b))
3494
+ }) : null,
3495
+ selected ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3496
+ className: "codexSketchObjectActions",
3497
+ children: [
3498
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
3499
+ disabled: agentLocked || busy,
3500
+ onClick: () => editObject({}, "duplicate"),
3501
+ children: t("sketchObjectDuplicate")
3502
+ }),
3503
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
3504
+ disabled: agentLocked || busy,
3505
+ onClick: () => editObject({}, "delete"),
3506
+ children: t("sketchObjectDelete")
3507
+ }),
3508
+ selected.shape === "text" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
3509
+ disabled: agentLocked || busy,
3510
+ onClick: () => setTextEdit({
3511
+ selection,
3512
+ value: selected.text
3513
+ }),
3514
+ children: t("sketchText")
3515
+ }) : null
3516
+ ]
3517
+ }) : null,
2738
3518
  tool === "pen" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
2739
3519
  className: "codexSketchStability",
2740
3520
  children: [t("sketchStability"), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("select", {
2741
3521
  "aria-label": t("sketchStability"),
2742
3522
  value: stability,
2743
- disabled: busy,
3523
+ disabled: agentLocked || busy,
2744
3524
  onChange: (e) => setStability(Number(e.target.value)),
2745
3525
  children: [
2746
3526
  0,
@@ -2760,43 +3540,11 @@ window.__ModuleLoader__.load({
2760
3540
  children: ["pixel", "stroke"].map((value) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2761
3541
  type: "button",
2762
3542
  "aria-pressed": eraser === value,
2763
- disabled: busy,
3543
+ disabled: agentLocked || busy,
2764
3544
  onClick: () => setEraser(value),
2765
3545
  children: t(`sketchErase_${value}`)
2766
3546
  }, value))
2767
- }) : null,
2768
- tool !== "eraser" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
2769
- className: "codexSketchWidth",
2770
- children: [
2771
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("sketchFlow") }),
2772
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
2773
- type: "range",
2774
- min: 5,
2775
- max: 100,
2776
- step: 5,
2777
- value: flow,
2778
- "aria-label": t("sketchFlow"),
2779
- onChange: (e) => setFlow(Number(e.target.value))
2780
- }),
2781
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("output", { children: flow })
2782
- ]
2783
- }) : null,
2784
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
2785
- className: "codexSketchWidth",
2786
- children: [
2787
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("sketchWidth") }),
2788
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
2789
- type: "range",
2790
- min: 2,
2791
- max: 64,
2792
- value: width,
2793
- "aria-label": t("sketchWidth"),
2794
- onChange: (e) => setWidth(Number(e.target.value)),
2795
- disabled: busy
2796
- }),
2797
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("output", { children: width })
2798
- ]
2799
- })
3547
+ }) : null
2800
3548
  ]
2801
3549
  }),
2802
3550
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
@@ -2808,9 +3556,9 @@ window.__ModuleLoader__.load({
2808
3556
  className: "codexSketchSwatch",
2809
3557
  style: { "--swatch": value },
2810
3558
  "aria-label": `${t("sketchColor")} ${value}`,
2811
- "aria-pressed": color === value,
2812
- disabled: busy,
2813
- onClick: () => setColor(value)
3559
+ "aria-pressed": (selected?.color ?? color) === value,
3560
+ disabled: agentLocked || busy,
3561
+ onClick: () => pickColor(value)
2814
3562
  }, value)), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
2815
3563
  className: "codexSketchCustom",
2816
3564
  title: t("sketchColor"),
@@ -2818,8 +3566,8 @@ window.__ModuleLoader__.load({
2818
3566
  type: "color",
2819
3567
  "aria-label": t("sketchColor"),
2820
3568
  value: color,
2821
- disabled: busy,
2822
- onChange: (e) => setColor(e.target.value)
3569
+ disabled: agentLocked || busy,
3570
+ onChange: (e) => pickColor(e.target.value)
2823
3571
  })]
2824
3572
  })]
2825
3573
  })
@@ -2831,11 +3579,26 @@ window.__ModuleLoader__.load({
2831
3579
  children: error
2832
3580
  }) : null
2833
3581
  ]
2834
- });
3582
+ })] });
2835
3583
  }
2836
3584
  //#endregion
2837
3585
  //#region src/sketch-styles.js
2838
3586
  const SKETCH_CSS = `
3587
+ .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)}
3588
+
3589
+ .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}
3590
+ .codexSketchSizeControl>span{font-size:10px;color:var(--sketch-muted);max-width:42px;text-align:center}.codexSketchSizeControl output{font-size:11px;font-variant-numeric:tabular-nums;color:var(--sketch-muted)}
3591
+ .codexSketchSizeControl input{writing-mode:vertical-lr;direction:rtl;width:28px;height:150px;appearance:none;background:transparent;cursor:ns-resize;touch-action:none}
3592
+ .codexSketchSizeControl input::-webkit-slider-runnable-track{width:4px;border-radius:4px;background:var(--sketch-line)}
3593
+ .codexSketchSizeControl input::-webkit-slider-thumb{appearance:none;width:20px;height:20px;margin-left:-8px;border-radius:50%;background:var(--sketch-fg);border:2px solid var(--sketch-bg);box-shadow:0 1px 5px #0004}
3594
+ .codexSketchSelection{position:absolute;width:min(100cqw,calc(100cqh * var(--sketch-ratio,1)));height:auto;aspect-ratio:var(--sketch-ratio);pointer-events:none;overflow:visible}
3595
+ .codexSketchShapeMenu{position:absolute;bottom:65px;display:grid;grid-template-columns:1fr 1fr;padding:8px;border:1px solid var(--sketch-line);border-radius:14px;background:var(--sketch-bg);box-shadow:0 8px 24px #0003;z-index:5}.codexSketchShapeMenu button{min-height:40px;padding:8px 14px}
3596
+ .codexSketchTextEditor{position:absolute;z-index:4;left:50%;top:50%;transform:translate(-50%,-50%);display:flex;flex-wrap:wrap;gap:8px;width:min(320px,80%);padding:12px;background:var(--sketch-bg);border:1px solid var(--sketch-line);border-radius:12px;box-shadow:0 6px 24px #0003}.codexSketchTextEditor textarea{width:100%;min-height:72px;resize:vertical;background:transparent;color:inherit;border:1px solid var(--sketch-line);border-radius:8px;padding:8px;font:inherit}.codexSketchTextEditor button{padding:6px 10px;border-radius:8px;background:var(--sketch-line)}
3597
+ .codexSketchObjectActions{display:flex;gap:8px}.codexSketchObjectActions button{padding:6px 8px;border-radius:8px;background:var(--sketch-line)}
3598
+ .codexLayerBrush>select{background:var(--sketch-bg);color:inherit;border:1px solid var(--sketch-line);border-radius:8px;padding:6px}
3599
+
3600
+ .codexSketchBackgroundStatus{position:fixed;top:16px;left:50%;transform:translateX(-50%);z-index:1000;display:flex;gap:8px;align-items:center;padding:6px 10px;border-radius:12px;background:var(--dsw-alias-bg-layer-1,#f5f5f7);color:var(--dsw-alias-label-primary,#202124);border:1px solid #8883;box-shadow:0 4px 16px #0002;font:12px system-ui}.codexSketchBackgroundStatus button{color:inherit;background:none;border:0;cursor:pointer}
3601
+ .codexSketchAgentStatus{display:flex;align-items:center;justify-content:center;gap:8px;min-height:24px;color:var(--sketch-muted);font-size:12px}.codexSketchAgentStatus button{padding:3px 6px;border-radius:6px;background:var(--sketch-line)}
2839
3602
  .codexSketchDialog{--sketch-bg:var(--dsw-alias-bg-layer-1,#f5f5f7);--sketch-fg:var(--dsw-alias-label-primary,#202124);--sketch-muted:var(--dsw-alias-label-secondary,#727279);--sketch-line:var(--dsw-alias-border-l2,#8883);--sketch-glass:color-mix(in srgb,var(--sketch-bg) 90%,transparent);box-sizing:border-box;width:min(1280px,calc(100vw - 24px));height:94dvh;max-height:94dvh;margin:auto;padding:10px;border:1px solid var(--sketch-line);border-radius:18px;background:var(--sketch-bg);color:var(--sketch-fg);box-shadow:0 24px 90px #0004;overflow:hidden;font:13px/1.4 system-ui}
2840
3603
  .codexSketchDialog[open]{display:flex;flex-direction:column;gap:8px}
2841
3604
  .codexSketchDialog::backdrop{background:#0005;backdrop-filter:blur(12px)}
@@ -2906,6 +3669,7 @@ window.__ModuleLoader__.load({
2906
3669
  }
2907
3670
  }), [registerOpen]);
2908
3671
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(react_jsx_runtime.Fragment, { children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SketchStudio, {
3672
+ agentPreview: settings.imageSketchAgentPreview,
2909
3673
  agentEnabled: settings.imageSketchAgent,
2910
3674
  onOpen: () => {
2911
3675
  opener.current = document.activeElement;
@@ -2953,7 +3717,7 @@ window.__ModuleLoader__.load({
2953
3717
  }) : null, /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SketchWorkspace, {
2954
3718
  ...props,
2955
3719
  registerOpen: registerWorkspace
2956
- })] });
3720
+ }, props.sessionId)] });
2957
3721
  }
2958
3722
  //#endregion
2959
3723
  //#region src/image-composer.js
@@ -3000,9 +3764,10 @@ window.__ModuleLoader__.load({
3000
3764
  }
3001
3765
  const createSketchTrigger = (options) => createWorkspaceTrigger({
3002
3766
  ...options,
3767
+ open: () => {},
3003
3768
  name: "Sketch · Beta",
3004
3769
  aliases: ["sketch", "草图"],
3005
- description: "Draw a reference image"
3770
+ description: "Ask the Agent to draw or edit a sketch"
3006
3771
  });
3007
3772
  const createImageTrigger = (options) => createWorkspaceTrigger({
3008
3773
  ...options,
@@ -3013,6 +3778,17 @@ window.__ModuleLoader__.load({
3013
3778
  //#endregion
3014
3779
  //#region src/client-locales.js
3015
3780
  const zh = {
3781
+ sketchSizeShort: "粗细",
3782
+ sketchObjectDuplicate: "复制对象",
3783
+ sketchObjectDelete: "删除对象",
3784
+ sketchTool_select: "选择",
3785
+ sketchTool_text: "文字",
3786
+ sketchTool_arrow: "箭头",
3787
+ sketchShapes: "图形",
3788
+ sketchText: "编辑文字",
3789
+ sketchTextDone: "完成",
3790
+ sketchTextSize: "字号",
3791
+ sketchBrush: "笔型",
3016
3792
  sketchFill: "填色",
3017
3793
  sketchDownload: "下载",
3018
3794
  sketchExportFormat: "导出格式",
@@ -3122,7 +3898,17 @@ window.__ModuleLoader__.load({
3122
3898
  sketchCanvas_on: "开启",
3123
3899
  sketchCanvas_off: "关闭",
3124
3900
  sketchAgent: "Agent 绘图 · Beta",
3125
- sketchAgentHint: "默认关闭;开启后向模型提供绘图工具,需要同时开启草图画板。",
3901
+ sketchAgentHint: "默认关闭;开启后可用 @sketch 请求 Agent 绘图,需要同时开启草图画板。",
3902
+ sketchRun_drawing: "Agent 正在绘制…",
3903
+ sketchRun_finished: "绘制完成",
3904
+ sketchRun_stopped: "绘制已停止",
3905
+ sketchRun_failed: "绘制失败",
3906
+ sketchRunStop: "停止绘制",
3907
+ sketchRunResume: "允许继续绘制",
3908
+ sketchAgentPreview: "完成后返回预览 · Beta",
3909
+ sketchAgentPreviewHint: "默认关闭;完成绘制后向模型返回画布图片,会增加图片输入用量。",
3910
+ sketchAgentPreview_on: "开启",
3911
+ sketchAgentPreview_off: "关闭",
3126
3912
  sketchAgent_on: "开启",
3127
3913
  sketchAgent_off: "关闭",
3128
3914
  imageSettings: "图片",
@@ -3398,6 +4184,17 @@ window.__ModuleLoader__.load({
3398
4184
  imageRemoveAnnotation: "删除标注"
3399
4185
  };
3400
4186
  const en = {
4187
+ sketchSizeShort: "Size",
4188
+ sketchObjectDuplicate: "Duplicate object",
4189
+ sketchObjectDelete: "Delete object",
4190
+ sketchTool_select: "Select",
4191
+ sketchTool_text: "Text",
4192
+ sketchTool_arrow: "Arrow",
4193
+ sketchShapes: "Shapes",
4194
+ sketchText: "Edit text",
4195
+ sketchTextDone: "Done",
4196
+ sketchTextSize: "Text size",
4197
+ sketchBrush: "Brush",
3401
4198
  sketchFill: "Fill",
3402
4199
  sketchDownload: "Download",
3403
4200
  sketchExportFormat: "Export format",
@@ -3507,7 +4304,17 @@ window.__ModuleLoader__.load({
3507
4304
  sketchCanvas_on: "On",
3508
4305
  sketchCanvas_off: "Off",
3509
4306
  sketchAgent: "Agent drawing · Beta",
3510
- sketchAgentHint: "Off by default. Exposes drawing tools to the model; requires the sketch canvas.",
4307
+ sketchAgentHint: "Off by default. Use @sketch to ask the Agent to draw; requires the sketch canvas.",
4308
+ sketchRun_drawing: "Agent is drawing…",
4309
+ sketchRun_finished: "Drawing complete",
4310
+ sketchRun_stopped: "Drawing stopped",
4311
+ sketchRun_failed: "Drawing failed",
4312
+ sketchRunStop: "Stop drawing",
4313
+ sketchRunResume: "Allow drawing again",
4314
+ sketchAgentPreview: "Preview on completion · Beta",
4315
+ sketchAgentPreviewHint: "Off by default. Returns the canvas to the model on completion, adding image input usage.",
4316
+ sketchAgentPreview_on: "On",
4317
+ sketchAgentPreview_off: "Off",
3511
4318
  sketchAgent_on: "On",
3512
4319
  sketchAgent_off: "Off",
3513
4320
  imageSettings: "Images",
@@ -4964,7 +5771,8 @@ window.__ModuleLoader__.load({
4964
5771
  imageViewer: true,
4965
5772
  imageAnnotations: true,
4966
5773
  imageSketch: false,
4967
- imageSketchAgent: false
5774
+ imageSketchAgent: false,
5775
+ imageSketchAgentPreview: false
4968
5776
  });
4969
5777
  function readImageFeatures(value = {}) {
4970
5778
  return Object.fromEntries(Object.entries(IMAGE_FEATURE_DEFAULTS).map(([key, fallback]) => [key, typeof value?.[key] === "boolean" ? value[key] : fallback]));
@@ -5602,7 +6410,7 @@ window.__ModuleLoader__.load({
5602
6410
  }
5603
6411
  //#endregion
5604
6412
  //#region src/version.js
5605
- const PACKAGE_VERSION = "2.1.0-beta.2";
6413
+ const PACKAGE_VERSION = "2.1.0-beta.3";
5606
6414
  //#endregion
5607
6415
  //#region src/client-recovery.js
5608
6416
  async function recoveryCall(rpc, endpoint, payload = {}, timeoutMs = 1e4) {
@@ -6172,6 +6980,7 @@ window.__ModuleLoader__.load({
6172
6980
  imageEntryPoints: ["imageShortcut"],
6173
6981
  sketchCanvas: ["imageSketch"],
6174
6982
  sketchAgent: ["imageSketchAgent"],
6983
+ sketchAgentPreview: ["imageSketchAgentPreview"],
6175
6984
  imageBrowsing: ["imageViewer", "imageAnnotations"]
6176
6985
  });
6177
6986
  function imageGroupValue(snapshot, group) {
@@ -7921,15 +8730,14 @@ window.__ModuleLoader__.load({
7921
8730
  ctx.inject(["inputTriggers"], (triggerContext) => triggerContext.effect(() => triggerContext.get("inputTriggers").registerSource(createSketchTrigger({
7922
8731
  enabled: () => {
7923
8732
  const value = preference.getSnapshot();
7924
- return value.imageSketch && value.imageEditing;
8733
+ return value.imageSketchAgent && value.imageSketch && value.imageEditing;
7925
8734
  },
7926
- open: (sessionId) => sketchOpeners.get(sessionId)?.(),
7927
8735
  consume: (sessionId, span) => {
7928
8736
  const actx = sessions.scope(sessionId);
7929
- return sketchOpeners.has(sessionId) && actx?.bail(actx, "slash/input-consume-token", { guard: {
7930
- kind: "span",
8737
+ return sketchOpeners.has(sessionId) && actx?.bail(actx, "slash/input-insert-text", {
8738
+ text: "@sketch ",
7931
8739
  span
7932
- } }) === true;
8740
+ }) === true;
7933
8741
  }
7934
8742
  })), "codex-subscription: Sketch trigger"));
7935
8743
  ctx.inject(["inputTriggers"], (triggerContext) => triggerContext.effect(() => triggerContext.get("inputTriggers").registerSource(createImageTrigger({