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

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
@@ -29,8 +29,8 @@ window.__ModuleLoader__.load({
29
29
  let react = require("react");
30
30
  react = __toESM(react, 1);
31
31
  let react_jsx_runtime = require("react/jsx-runtime");
32
- let _deepseek_ai_dsh_client_ui_primitives = require("@deepseek-ai/dsh-client-ui-primitives");
33
32
  let react_dom = require("react-dom");
33
+ let _deepseek_ai_dsh_client_ui_primitives = require("@deepseek-ai/dsh-client-ui-primitives");
34
34
  //#region src/image-edit.js
35
35
  const isRecord = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
36
36
  const validCoordinate = (value) => typeof value === "number" && Number.isFinite(value) && value >= 0 && value <= 1;
@@ -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++) {
@@ -1283,8 +1351,39 @@ window.__ModuleLoader__.load({
1283
1351
  };
1284
1352
  }
1285
1353
  function SketchViewControls({ navigation, t }) {
1286
- const [open, setOpen] = (0, react.useState)(false), host = (0, react.useRef)(null);
1287
- useSketchDismiss(open, setOpen, host, [".codexSketchViewControls"]);
1354
+ const [open, setOpen] = (0, react.useState)(false), [placement, setPlacement] = (0, react.useState)(null);
1355
+ const host = (0, react.useRef)(null), trigger = (0, react.useRef)(null);
1356
+ const close = () => {
1357
+ setOpen(false);
1358
+ trigger.current?.focus({ preventScroll: true });
1359
+ };
1360
+ useSketchDismiss(open, setOpen, host, [".codexSketchViewControls", ".codexSketchKeyPanel"]);
1361
+ (0, react.useLayoutEffect)(() => {
1362
+ if (!open) return;
1363
+ const dialog = host.current.closest("dialog");
1364
+ const place = () => {
1365
+ const box = dialog.getBoundingClientRect(), anchor = trigger.current.getBoundingClientRect();
1366
+ const width = Math.min(360, box.width - 24);
1367
+ setPlacement({
1368
+ dialog,
1369
+ style: {
1370
+ width,
1371
+ left: Math.max(12, Math.min(anchor.left - box.left, box.width - width - 12)),
1372
+ bottom: box.bottom - anchor.top + 8,
1373
+ maxHeight: Math.max(80, anchor.top - box.top - 24)
1374
+ }
1375
+ });
1376
+ };
1377
+ place();
1378
+ const observer = new ResizeObserver(place);
1379
+ observer.observe(dialog);
1380
+ observer.observe(host.current);
1381
+ window.addEventListener("resize", place);
1382
+ return () => {
1383
+ observer.disconnect();
1384
+ window.removeEventListener("resize", place);
1385
+ };
1386
+ }, [open]);
1288
1387
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1289
1388
  ref: host,
1290
1389
  className: "codexSketchViewControls",
@@ -1308,49 +1407,116 @@ window.__ModuleLoader__.load({
1308
1407
  children: "+"
1309
1408
  }),
1310
1409
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1410
+ ref: trigger,
1311
1411
  type: "button",
1412
+ "aria-expanded": open,
1312
1413
  onClick: () => setOpen(!open),
1313
1414
  children: t("sketchKeys")
1314
1415
  }),
1315
- open ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
1416
+ open && placement ? (0, react_dom.createPortal)(/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
1316
1417
  className: "codexSketchKeyPanel",
1418
+ "aria-label": t("sketchKeys"),
1419
+ style: placement.style,
1317
1420
  onKeyDown: (e) => {
1318
1421
  if (e.key === "Escape") {
1319
1422
  e.preventDefault();
1320
1423
  e.stopPropagation();
1321
- setOpen(false);
1424
+ close();
1322
1425
  }
1323
1426
  },
1324
1427
  children: [
1325
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
1326
- type: "checkbox",
1327
- checked: navigation.shortcuts,
1328
- onChange: navigation.toggle
1329
- }), t("sketchKeysEnabled")] }),
1428
+ /* @__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", {
1429
+ type: "button",
1430
+ "aria-label": t("sketchFileClose"),
1431
+ onClick: close,
1432
+ children: "×"
1433
+ })] }),
1434
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
1435
+ className: "codexSketchKeysEnabled",
1436
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("sketchKeysEnabled") }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
1437
+ type: "checkbox",
1438
+ checked: navigation.shortcuts,
1439
+ onChange: navigation.toggle
1440
+ })]
1441
+ }),
1330
1442
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", { children: t("sketchNavigationHint") }),
1331
- 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", {
1332
- "aria-label": t(`sketchKey_${action}`),
1333
- value: key === " " ? "Space" : key,
1334
- readOnly: true,
1335
- onKeyDown: (e) => {
1336
- if (e.key === "Tab" || e.key === "Escape") return;
1337
- e.preventDefault();
1338
- e.stopPropagation();
1339
- if (e.key.length === 1 && !e.ctrlKey && !e.metaKey && !e.altKey) navigation.setKey(action, e.key);
1340
- }
1341
- })] }, action)),
1443
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1444
+ className: "codexSketchKeyGrid",
1445
+ 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", {
1446
+ "aria-label": t(`sketchKey_${action}`),
1447
+ value: key === " " ? "Space" : key,
1448
+ readOnly: true,
1449
+ onKeyDown: (e) => {
1450
+ if (e.key === "Tab" || e.key === "Escape") return;
1451
+ e.preventDefault();
1452
+ e.stopPropagation();
1453
+ if (e.key.length === 1 && !e.ctrlKey && !e.metaKey && !e.altKey) navigation.setKey(action, e.key);
1454
+ }
1455
+ })] }, action))
1456
+ }),
1342
1457
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("small", { children: t("sketchKeyHint") })
1343
1458
  ]
1344
- }) : null
1459
+ }), placement.dialog) : null
1345
1460
  ]
1346
1461
  });
1347
1462
  }
1463
+ //#endregion
1464
+ //#region src/sketch-objects.js
1465
+ const objectId = (stroke, index) => stroke.id ?? `legacy-${index}`;
1466
+ const identifyObjects = (doc) => ({
1467
+ ...doc,
1468
+ layers: doc.layers.map((layer) => ({
1469
+ ...layer,
1470
+ strokes: layer.strokes.map((s, i) => s.id ? s : {
1471
+ ...s,
1472
+ id: objectId(s, i)
1473
+ })
1474
+ }))
1475
+ });
1476
+ function objectBounds(stroke) {
1477
+ const xs = stroke.points.map((p) => p.x), ys = stroke.points.map((p) => p.y);
1478
+ return {
1479
+ x: Math.min(...xs),
1480
+ y: Math.min(...ys),
1481
+ width: Math.max(...xs) - Math.min(...xs),
1482
+ height: Math.max(...ys) - Math.min(...ys)
1483
+ };
1484
+ }
1485
+ function transformObject(stroke, { dx = 0, dy = 0, scaleX = 1, scaleY = 1 }) {
1486
+ if (![
1487
+ dx,
1488
+ dy,
1489
+ scaleX,
1490
+ scaleY
1491
+ ].every(Number.isFinite) || scaleX <= 0 || scaleY <= 0) throw Error("Invalid object transform");
1492
+ const box = objectBounds(stroke);
1493
+ const points = stroke.points.map((p) => ({
1494
+ x: box.x + (p.x - box.x) * scaleX + dx,
1495
+ y: box.y + (p.y - box.y) * scaleY + dy
1496
+ }));
1497
+ if (points.some((p) => p.x < 0 || p.x > 1 || p.y < 0 || p.y > 1)) throw Error("Object would leave the canvas");
1498
+ return {
1499
+ ...stroke,
1500
+ points
1501
+ };
1502
+ }
1503
+ function sketchObjectSummary(doc) {
1504
+ return doc.layers.flatMap((layer) => layer.strokes.map((stroke, i) => ({
1505
+ layer: layer.id,
1506
+ id: objectId(stroke, i),
1507
+ shape: stroke.shape,
1508
+ color: stroke.color,
1509
+ bounds: objectBounds(stroke),
1510
+ ...stroke.text ? { text: stroke.text } : {}
1511
+ })));
1512
+ }
1348
1513
  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.",
1514
+ 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.",
1515
+ 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.",
1351
1516
  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},...]}",
1517
+ 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
1518
  layer: "{op:\"layer\",action:\"add|select|rename|visible|duplicate|up|down|delete|clear\",id:1,value:\"name\"}",
1519
+ 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
1520
  resize: "{op:\"resize\",ratio:\"1:1|4:3|3:4|16:9|9:16\"}"
1355
1521
  },
1356
1522
  limits: {
@@ -1363,7 +1529,7 @@ window.__ModuleLoader__.load({
1363
1529
  const finite = (value, min, max) => typeof value === "number" && Number.isFinite(value) && value >= min && value <= max;
1364
1530
  function applySketchCommands(source, commands) {
1365
1531
  if (!Array.isArray(commands) || !commands.length || commands.length > 256) throw Error("Expected 1–256 commands");
1366
- let doc = source;
1532
+ let doc = identifyObjects(source);
1367
1533
  for (const command of commands) {
1368
1534
  if (!command || typeof command !== "object") throw Error("Invalid command");
1369
1535
  if (command.op === "resize") {
@@ -1387,29 +1553,94 @@ window.__ModuleLoader__.load({
1387
1553
  doc = next;
1388
1554
  continue;
1389
1555
  }
1556
+ if (command.op === "object") {
1557
+ const layer = doc.layers.find((l) => l.id === (command.layer ?? doc.active)), index = layer?.strokes.findIndex((s) => s.id === command.id);
1558
+ if (!layer?.visible || index < 0 || index === void 0) throw Error("Object missing or hidden; inspect again");
1559
+ const strokes = layer.strokes.slice(), original = strokes[index];
1560
+ if (command.action === "delete") strokes.splice(index, 1);
1561
+ else if (command.action === "duplicate") strokes.splice(index + 1, 0, {
1562
+ ...original,
1563
+ id: crypto.randomUUID(),
1564
+ points: original.points.map((p) => ({ ...p }))
1565
+ });
1566
+ else if (command.action === "update") {
1567
+ const patch = command.patch ?? {};
1568
+ if (Object.keys(patch).some((k) => ![
1569
+ "color",
1570
+ "width",
1571
+ "opacity",
1572
+ "fill",
1573
+ "text",
1574
+ "points"
1575
+ ].includes(k))) throw Error("Unsupported object property");
1576
+ const changed = command.transform ? transformObject({
1577
+ ...original,
1578
+ ...patch
1579
+ }, command.transform) : {
1580
+ ...original,
1581
+ ...patch
1582
+ };
1583
+ strokes[index] = {
1584
+ ...applySketchCommands({
1585
+ ...doc,
1586
+ layers: [{
1587
+ ...layer,
1588
+ strokes: []
1589
+ }]
1590
+ }, [{
1591
+ ...changed,
1592
+ op: "stroke",
1593
+ layer: layer.id
1594
+ }]).layers[0].strokes[0],
1595
+ brush: original.brush ?? "pen",
1596
+ ...original.pressure !== void 0 ? { pressure: original.pressure } : {}
1597
+ };
1598
+ } else throw Error("Unknown object action");
1599
+ doc = {
1600
+ ...doc,
1601
+ layers: doc.layers.map((l) => l === layer ? {
1602
+ ...l,
1603
+ strokes
1604
+ } : l)
1605
+ };
1606
+ continue;
1607
+ }
1390
1608
  if (command.op !== "stroke") throw Error("Unknown command");
1391
- const { points, color, shape = "pen", width = 2, opacity = 1, fill = false } = command;
1609
+ const shape = command.shape === "ellipse" ? "circle" : command.shape ?? "pen";
1610
+ const { points, color, width = shape === "text" ? 24 : 2, opacity = 1, fill = false } = command;
1392
1611
  if (![
1393
1612
  "pen",
1394
1613
  "line",
1395
1614
  "rectangle",
1396
1615
  "circle",
1397
1616
  "polygon",
1617
+ "bezier",
1618
+ "arrow",
1619
+ "text",
1398
1620
  "eraser"
1399
1621
  ].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
1622
  if ([
1401
1623
  "line",
1624
+ "arrow",
1625
+ "text",
1402
1626
  "rectangle",
1403
1627
  "circle"
1404
1628
  ].includes(shape) && points.length !== 2 || shape === "polygon" && points.length < 3) throw Error("Invalid shape points");
1629
+ 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.`);
1405
1630
  if (fill && ![
1406
1631
  "rectangle",
1407
1632
  "circle",
1408
- "polygon"
1633
+ "polygon",
1634
+ "bezier"
1409
1635
  ].includes(shape)) throw Error("Fill requires a closed shape");
1410
1636
  const layer = doc.layers.find((layer) => layer.id === (command.layer ?? doc.active));
1411
1637
  if (!layer?.visible) throw Error("Target layer is missing or hidden");
1638
+ 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");
1639
+ const id = command.id ?? crypto.randomUUID();
1640
+ if (typeof id !== "string" || !id.length || id.length > 100 || layer.strokes.some((s) => s.id === id)) throw Error("Invalid or duplicate object id");
1412
1641
  const stroke = {
1642
+ id,
1643
+ ...shape === "text" ? { text: command.text } : {},
1413
1644
  shape,
1414
1645
  color,
1415
1646
  width,
@@ -1434,15 +1665,24 @@ window.__ModuleLoader__.load({
1434
1665
  }
1435
1666
  function createSketchCommandSession(adapter) {
1436
1667
  const completed = /* @__PURE__ */ new Map();
1437
- let pending = false;
1668
+ let pending = false, cachedCharacters = 0;
1438
1669
  return async (request) => {
1439
1670
  if (!request || typeof request !== "object") throw Error("Invalid sketch request");
1440
1671
  if (!adapter.available()) throw Error("Open the sketch board for this session first");
1441
1672
  const current = adapter.snapshot();
1442
- if (request.action === "inspect") return {
1443
- ...current,
1444
- help: SKETCH_COMMAND_HELP
1445
- };
1673
+ if (request.action === "inspect") {
1674
+ const offset = request.offset ?? 0, objects = adapter.objects?.() ?? [];
1675
+ if (!Number.isInteger(offset) || offset < 0) throw Error("offset must be a non-negative integer");
1676
+ return {
1677
+ ...current,
1678
+ objects: objects.slice(offset, offset + 50),
1679
+ objectCount: objects.length,
1680
+ ...offset + 50 < objects.length ? { nextOffset: offset + 50 } : {},
1681
+ ...request.objectId ? { object: adapter.object?.(request.objectId, request.layer) } : {},
1682
+ recentRequests: [...completed.values()].slice(-8).map((entry) => entry.receipt),
1683
+ help: SKETCH_COMMAND_HELP
1684
+ };
1685
+ }
1446
1686
  if (request.documentId !== current.documentId) throw Error("Document changed; inspect again");
1447
1687
  if (pending || adapter.busy()) throw Error("Sketch is being edited; retry after it settles");
1448
1688
  if (request.action === "preview") return {
@@ -1451,16 +1691,25 @@ window.__ModuleLoader__.load({
1451
1691
  };
1452
1692
  if (!["apply", "save"].includes(request.action)) throw Error("Unknown sketch action");
1453
1693
  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);
1694
+ const key = `${current.documentId}:${request.requestId}`, fingerprint = JSON.stringify({
1695
+ ...request,
1696
+ runId: void 0
1697
+ });
1455
1698
  const cached = completed.get(key);
1456
1699
  if (cached) {
1457
1700
  if (cached.fingerprint !== fingerprint) throw Error("requestId reused with different content");
1458
1701
  return cached.result;
1459
1702
  }
1460
1703
  if (request.revision !== current.revision || adapter.busy()) throw Error("Sketch changed or is being edited; inspect again");
1704
+ let changedObjects;
1461
1705
  if (request.action === "apply") {
1462
- const next = applySketchCommands(adapter.document(), request.commands);
1706
+ const before = adapter.document(), next = applySketchCommands(before, request.commands);
1463
1707
  adapter.commit(next);
1708
+ const previous = new Map(before.layers.flatMap((l) => l.strokes.map((s) => [`${l.id}:${s.id}`, s])));
1709
+ changedObjects = next.layers.flatMap((l) => l.strokes.filter((s) => previous.get(`${l.id}:${s.id}`) !== s).map((s) => ({
1710
+ layer: l.id,
1711
+ id: s.id
1712
+ })));
1464
1713
  } else {
1465
1714
  if (request.name !== void 0 && (typeof request.name !== "string" || request.name.length > 60)) throw Error("Invalid draft name");
1466
1715
  pending = true;
@@ -1470,12 +1719,28 @@ window.__ModuleLoader__.load({
1470
1719
  pending = false;
1471
1720
  }
1472
1721
  }
1473
- const result = adapter.snapshot();
1722
+ const result = {
1723
+ ...adapter.snapshot(),
1724
+ ...changedObjects ? {
1725
+ changedObjects: changedObjects.slice(0, 100),
1726
+ changedObjectCount: changedObjects.length
1727
+ } : {}
1728
+ };
1474
1729
  completed.set(key, {
1475
1730
  fingerprint,
1476
- result
1731
+ result,
1732
+ receipt: {
1733
+ requestId: request.requestId,
1734
+ action: request.action,
1735
+ revision: result.revision
1736
+ }
1477
1737
  });
1478
- if (completed.size > 128) completed.delete(completed.keys().next().value);
1738
+ cachedCharacters += fingerprint.length;
1739
+ while (completed.size > 1 && (completed.size > 128 || cachedCharacters > 4e6)) {
1740
+ const oldest = completed.keys().next().value;
1741
+ cachedCharacters -= completed.get(oldest).fingerprint.length;
1742
+ completed.delete(oldest);
1743
+ }
1479
1744
  return result;
1480
1745
  };
1481
1746
  }
@@ -1669,6 +1934,7 @@ window.__ModuleLoader__.load({
1669
1934
  }, [open, working]);
1670
1935
  useSketchDismiss(open, setOpen, host, [".codexSketchFiles"]);
1671
1936
  const run = async (operation) => {
1937
+ if (disabled || working) return;
1672
1938
  setWorking(true);
1673
1939
  onWorking(true);
1674
1940
  try {
@@ -1724,7 +1990,7 @@ window.__ModuleLoader__.load({
1724
1990
  className: "codexSketchFileActions",
1725
1991
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1726
1992
  type: "button",
1727
- disabled: working,
1993
+ disabled: disabled || working,
1728
1994
  onClick: () => void run(async () => {
1729
1995
  await fresh();
1730
1996
  setName("");
@@ -1733,7 +1999,7 @@ window.__ModuleLoader__.load({
1733
1999
  children: t("sketchNew")
1734
2000
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1735
2001
  type: "button",
1736
- disabled: working,
2002
+ disabled: disabled || working,
1737
2003
  onClick: () => input.current.click(),
1738
2004
  children: t("sketchImport")
1739
2005
  })]
@@ -1747,7 +2013,7 @@ window.__ModuleLoader__.load({
1747
2013
  }),
1748
2014
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1749
2015
  type: "button",
1750
- disabled: working,
2016
+ disabled: disabled || working,
1751
2017
  onClick: () => void run(async () => {
1752
2018
  await save(name);
1753
2019
  await refresh();
@@ -1765,14 +2031,14 @@ window.__ModuleLoader__.load({
1765
2031
  ].map(([value, label]) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1766
2032
  type: "button",
1767
2033
  "aria-pressed": format === value,
1768
- disabled: working,
2034
+ disabled: disabled || working,
1769
2035
  onClick: () => setFormat(value),
1770
2036
  children: label
1771
2037
  }, value))
1772
2038
  }),
1773
2039
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1774
2040
  type: "button",
1775
- disabled: working || !hasContent,
2041
+ disabled: disabled || working || !hasContent,
1776
2042
  onClick: () => void run(async () => {
1777
2043
  await download(format);
1778
2044
  setOpen(false);
@@ -1785,7 +2051,7 @@ window.__ModuleLoader__.load({
1785
2051
  className: "codexSketchDraftList",
1786
2052
  children: rows.map((row) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1787
2053
  type: "button",
1788
- disabled: working,
2054
+ disabled: disabled || working,
1789
2055
  onClick: () => void run(async () => {
1790
2056
  await load(row);
1791
2057
  setName(row.name);
@@ -1794,7 +2060,7 @@ window.__ModuleLoader__.load({
1794
2060
  children: row.name
1795
2061
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1796
2062
  type: "button",
1797
- disabled: working,
2063
+ disabled: disabled || working,
1798
2064
  "aria-label": `${t("sketchDeleteDraft")} ${row.name}`,
1799
2065
  onClick: () => {
1800
2066
  if (remove !== row.id) {
@@ -1824,6 +2090,9 @@ window.__ModuleLoader__.load({
1824
2090
  //#region src/workspace-icons.jsx
1825
2091
  function WorkspaceIcon({ name, size = 24 }) {
1826
2092
  const paths = {
2093
+ select: "M5 3l14 9-7 2-3 7-4-18z",
2094
+ text: "M4 5h16M12 5v15M8 20h8M4 5v3M20 5v3",
2095
+ arrow: "M4 20L20 4M10 4h10v10",
1827
2096
  line: "M4 20L20 4",
1828
2097
  layers: "M12 3L2 8l10 5 10-5-10-5zM2 12l10 5 10-5M2 16l10 5 10-5",
1829
2098
  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 +2127,161 @@ window.__ModuleLoader__.load({
1858
2127
  });
1859
2128
  }
1860
2129
  //#endregion
2130
+ //#region src/sketch-size-control.jsx
2131
+ function SketchSizeControl({ value, onChange, onStart, onEnd, label, disabled, min = 2, max = 128, mode, modes, onModeChange, suffix = "" }) {
2132
+ const active = (0, react.useRef)(false);
2133
+ const start = () => {
2134
+ if (!active.current) {
2135
+ active.current = true;
2136
+ onStart?.();
2137
+ }
2138
+ };
2139
+ const end = () => {
2140
+ if (active.current) {
2141
+ active.current = false;
2142
+ onEnd?.();
2143
+ }
2144
+ };
2145
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2146
+ className: "codexSketchSizeControl",
2147
+ title: label,
2148
+ children: [
2149
+ modes ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2150
+ className: "codexSketchSizeModes",
2151
+ children: modes.map((item) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2152
+ type: "button",
2153
+ "aria-pressed": mode === item.value,
2154
+ disabled,
2155
+ onClick: () => {
2156
+ end();
2157
+ onModeChange(item.value);
2158
+ },
2159
+ children: item.label
2160
+ }, item.value))
2161
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: label }),
2162
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
2163
+ type: "range",
2164
+ "aria-label": label,
2165
+ "aria-orientation": "vertical",
2166
+ min,
2167
+ max,
2168
+ value,
2169
+ disabled,
2170
+ onPointerDown: start,
2171
+ onPointerUp: end,
2172
+ onPointerCancel: end,
2173
+ onBlur: end,
2174
+ onKeyDown: start,
2175
+ onKeyUp: end,
2176
+ onChange: (event) => {
2177
+ start();
2178
+ onChange(Number(event.target.value));
2179
+ }
2180
+ }),
2181
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("output", { children: [Math.round(value), suffix] })
2182
+ ]
2183
+ });
2184
+ }
2185
+ //#endregion
2186
+ //#region src/sketch-agent-run.js
2187
+ function createSketchAgentRun({ execute, open, changed, busy = () => false, previewEnabled = () => false, idleMs = 18e4 }) {
2188
+ let state = "idle", generation = 0, runNumber = 0, pending = false, runId, timer, completed;
2189
+ const update = (next) => {
2190
+ state = next;
2191
+ changed(next);
2192
+ };
2193
+ const clear = () => {
2194
+ clearTimeout(timer);
2195
+ timer = void 0;
2196
+ };
2197
+ const expire = () => {
2198
+ clear();
2199
+ generation++;
2200
+ if (state !== "stopped") update("failed");
2201
+ };
2202
+ return {
2203
+ get state() {
2204
+ return state;
2205
+ },
2206
+ get locked() {
2207
+ return state === "drawing";
2208
+ },
2209
+ stop() {
2210
+ clear();
2211
+ generation++;
2212
+ update("stopped");
2213
+ },
2214
+ resume() {
2215
+ clear();
2216
+ generation++;
2217
+ update("idle");
2218
+ },
2219
+ fail: expire,
2220
+ dispose() {
2221
+ clear();
2222
+ generation++;
2223
+ state = "idle";
2224
+ },
2225
+ async execute(request) {
2226
+ if (!request || typeof request !== "object") throw Error("Invalid sketch request");
2227
+ if (state === "stopped") throw Error("Drawing stopped by the user. Do not retry until they enable drawing again.");
2228
+ if (pending || busy()) throw Error("Sketch is being edited; retry after it settles");
2229
+ if (request.action !== "inspect" && request.runId !== runId) throw Error("Drawing run changed; inspect again");
2230
+ if (state === "finished" && completed?.key === JSON.stringify(request)) return completed.value;
2231
+ if (state !== "drawing" && request.action !== "inspect") throw Error("Start drawing with inspect");
2232
+ clear();
2233
+ const version = generation;
2234
+ pending = true;
2235
+ try {
2236
+ if (state !== "drawing") {
2237
+ runId = `run-${++runNumber}`;
2238
+ completed = void 0;
2239
+ update("drawing");
2240
+ open();
2241
+ }
2242
+ const value = await execute(request.action === "finish" ? {
2243
+ ...request,
2244
+ action: "save"
2245
+ } : request);
2246
+ if (version !== generation) throw Error("Drawing interrupted");
2247
+ if (request.action !== "finish") return {
2248
+ ...value,
2249
+ runId
2250
+ };
2251
+ const result = previewEnabled() ? await execute({
2252
+ action: "preview",
2253
+ documentId: value.documentId
2254
+ }) : value;
2255
+ if (version !== generation) throw Error("Drawing interrupted");
2256
+ completed = {
2257
+ key: JSON.stringify(request),
2258
+ value: {
2259
+ ...result,
2260
+ runId
2261
+ }
2262
+ };
2263
+ update("finished");
2264
+ return completed.value;
2265
+ } catch (error) {
2266
+ if (version === generation) {
2267
+ update("failed");
2268
+ throw new Error(`${error.message} Call inspect to obtain the current runId and revision before retrying.`, { cause: error });
2269
+ }
2270
+ throw error;
2271
+ } finally {
2272
+ pending = false;
2273
+ if (state === "drawing") {
2274
+ timer = setTimeout(expire, idleMs);
2275
+ timer.unref?.();
2276
+ }
2277
+ }
2278
+ }
2279
+ };
2280
+ }
2281
+ //#endregion
1861
2282
  //#region src/sketch-agent-client.js
1862
2283
  function connectSketchAgent(rpc, sessionId, execute, report, pollDelay = () => 350) {
1863
- let stopped = false, token, timer;
2284
+ let stopped = false, token, timer, attempts = 0;
1864
2285
  const call = (endpoint, payload) => rpc.call(CHANNEL, `sketch/${endpoint}`, {
1865
2286
  sessionId,
1866
2287
  token,
@@ -1871,9 +2292,14 @@ window.__ModuleLoader__.load({
1871
2292
  const tasks = await call("poll");
1872
2293
  for (const task of tasks) {
1873
2294
  if (stopped) break;
2295
+ if (task.cancelled) {
2296
+ report("Sketch operation interrupted; completed strokes are preserved.");
2297
+ continue;
2298
+ }
1874
2299
  let value, error;
1875
2300
  try {
1876
- if (task.expiresAt < Date.now()) throw Error("Sketch command expired; inspect before retrying");
2301
+ if (task.expiresAt < Date.now() || !await call("claim", { id: task.id })) throw Error("Sketch command expired or cancelled; inspect before retrying");
2302
+ if (stopped) break;
1877
2303
  value = await execute(task.request);
1878
2304
  } catch (cause) {
1879
2305
  error = cause.message;
@@ -1890,29 +2316,23 @@ window.__ModuleLoader__.load({
1890
2316
  }
1891
2317
  if (!stopped) timer = setTimeout(poll, pollDelay());
1892
2318
  };
1893
- call("connect").then((value) => {
2319
+ const connect = () => void call("connect").then((value) => {
1894
2320
  token = value.token;
1895
2321
  if (stopped) call("disconnect").catch(() => {});
1896
2322
  else poll();
1897
2323
  }, (error) => {
1898
- if (!stopped) report(error.message);
2324
+ if (stopped) return;
2325
+ const leaseConflict = /Another board is connected/.test(error.message);
2326
+ if (++attempts < (leaseConflict ? 8 : 3)) timer = setTimeout(connect, Math.min(3e3, 500 * attempts));
2327
+ else report(error.message);
1899
2328
  });
2329
+ connect();
1900
2330
  return () => {
1901
2331
  stopped = true;
1902
2332
  clearTimeout(timer);
1903
2333
  if (token) call("disconnect").catch(() => {});
1904
2334
  };
1905
2335
  }
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
2336
  //#endregion
1917
2337
  //#region src/sketch-studio.jsx
1918
2338
  const PALETTE = [
@@ -1924,20 +2344,51 @@ window.__ModuleLoader__.load({
1924
2344
  "#34c759",
1925
2345
  "#0088ff"
1926
2346
  ];
1927
- function SketchStudio({ open, agentEnabled, onOpen, onClose, attachSketch, enabled, t, incoming, sessionId, rpc }) {
2347
+ function SketchStudio({ open, agentEnabled, agentPreview, onOpen, onClose, attachSketch, enabled, t, incoming, sessionId, rpc }) {
1928
2348
  const dialog = (0, react.useRef)(null), canvas = (0, react.useRef)(null), doc = (0, react.useRef)(createSketchLayers()), cache = (0, react.useRef)(/* @__PURE__ */ new Map());
1929
2349
  const undo = (0, react.useRef)([]), redo = (0, react.useRef)([]), active = (0, react.useRef)(null), frame = (0, react.useRef)(null);
1930
2350
  const images = (0, react.useRef)(/* @__PURE__ */ new Map()), saved = (0, react.useRef)(null), dirty = (0, react.useRef)(false), updateUi = (0, react.useRef)(false);
1931
2351
  const documentId = (0, react.useRef)(crypto.randomUUID()), documentRevision = (0, react.useRef)(0), agentAdapter = (0, react.useRef)({}), agentSession = (0, react.useRef)(null);
2352
+ const [agentState, setAgentState] = (0, react.useState)("idle"), agentRun = (0, react.useRef)(null);
2353
+ const agentLocked = agentState === "drawing";
2354
+ const [noticeHidden, setNoticeHidden] = (0, react.useState)(false);
1932
2355
  const [stability, setStability] = (0, react.useState)(0), [flow, setFlow] = (0, react.useState)(100), [picturesOpen, setPicturesOpen] = (0, react.useState)(false);
1933
2356
  const pictureInput = (0, react.useRef)(null), received = (0, react.useRef)(null);
1934
2357
  const navigation = useSketchView(canvas, open);
1935
2358
  const [revision, redraw] = (0, react.useState)(0), [tool, setTool] = (0, react.useState)("pen"), [brush, setBrush] = (0, react.useState)("pen");
1936
2359
  const [eraser, setEraser] = (0, react.useState)("pixel"), [color, setColor] = (0, react.useState)("#0088ff"), [width, setWidth] = (0, react.useState)(12);
2360
+ const [selection, setSelection] = (0, react.useState)(null), [textEdit, setTextEdit] = (0, react.useState)(null), [shapesOpen, setShapesOpen] = (0, react.useState)(false);
2361
+ const sizeGesture = (0, react.useRef)(false);
2362
+ const [sizeMode, setSizeMode] = (0, react.useState)("size");
2363
+ const showOpacity = tool !== "eraser";
2364
+ const opacityMode = showOpacity && sizeMode === "opacity";
2365
+ const selected = doc.current.layers.find((l) => l.id === selection?.layer)?.strokes.find((s, i) => objectId(s, i) === selection?.id);
2366
+ const editObject = (patch, action = "update") => {
2367
+ if (agentRun.current?.locked || busy || !selected) return;
2368
+ try {
2369
+ const next = applySketchCommands(doc.current, [{
2370
+ op: "object",
2371
+ ...selection,
2372
+ action,
2373
+ patch
2374
+ }]);
2375
+ if (!sizeGesture.current) checkpoint();
2376
+ doc.current = next;
2377
+ schedule();
2378
+ if (action === "delete") setSelection(null);
2379
+ } catch (e) {
2380
+ setError(e.message);
2381
+ }
2382
+ };
2383
+ const pickColor = (value) => {
2384
+ setColor(value);
2385
+ if (selected) editObject({ color: value });
2386
+ };
1937
2387
  const [fillShape, setFillShape] = (0, react.useState)(false);
1938
2388
  const [layersOpen, setLayersOpen] = (0, react.useState)(false), [busy, setBusy] = (0, react.useState)(false), [error, setError] = (0, react.useState)("");
1939
2389
  const cursorRing = (0, react.useRef)(null);
1940
- const cursor = useSketchCursor(canvas, cursorRing, width, tool === "pen" ? brush : "pen", navigation.view.scale, !open || navigation.space || busy);
2390
+ const cursor = useSketchCursor(canvas, cursorRing, width, tool === "pen" ? brush : "pen", navigation.view.scale, !open || navigation.space || busy || agentLocked || tool === "select" || tool === "text");
2391
+ useSketchDismiss(shapesOpen, setShapesOpen, dialog, [".codexSketchShapeMenu", ".codexSketchShapeToggle"]);
1941
2392
  useSketchDismiss(layersOpen, setLayersOpen, dialog, [".codexSketchLayers", ".codexSketchLayersToggle"]);
1942
2393
  useSketchDismiss(picturesOpen, setPicturesOpen, dialog, [".codexSketchPictures", ".codexSketchPicturesToggle"]);
1943
2394
  const paint = () => {
@@ -1967,7 +2418,7 @@ window.__ModuleLoader__.load({
1967
2418
  redo.current = [];
1968
2419
  };
1969
2420
  const change = (action, id, value) => {
1970
- if (busy || active.current) return;
2421
+ if (busy || agentRun.current?.locked || active.current) return;
1971
2422
  const next = changeSketchLayer(doc.current, action, id, value);
1972
2423
  if (next === doc.current) return;
1973
2424
  if (action !== "select") checkpoint();
@@ -1979,12 +2430,14 @@ window.__ModuleLoader__.load({
1979
2430
  (0, react.useEffect)(() => {
1980
2431
  if (open) {
1981
2432
  dialog.current.showModal();
2433
+ canvas.current.width = doc.current.width ?? 1024;
1982
2434
  paint();
1983
2435
  } else dialog.current?.close();
1984
2436
  }, [open]);
1985
2437
  (0, react.useEffect)(() => () => {
1986
2438
  cancelAnimationFrame(frame.current);
1987
2439
  cache.current.clear();
2440
+ agentRun.current?.dispose();
1988
2441
  }, []);
1989
2442
  const hasContent = () => doc.current.layers.some((layer) => layer.image || layer.strokes.length);
1990
2443
  const save = async (name) => {
@@ -2007,7 +2460,9 @@ window.__ModuleLoader__.load({
2007
2460
  const replace = (next, decoded, identity) => {
2008
2461
  documentId.current = crypto.randomUUID();
2009
2462
  documentRevision.current++;
2010
- doc.current = structuredClone(next);
2463
+ doc.current = identifyObjects(structuredClone(next));
2464
+ setSelection(null);
2465
+ setTextEdit(null);
2011
2466
  images.current = decoded;
2012
2467
  cache.current.clear();
2013
2468
  undo.current = [];
@@ -2068,6 +2523,10 @@ window.__ModuleLoader__.load({
2068
2523
  schedule();
2069
2524
  };
2070
2525
  const close = async () => {
2526
+ if (agentRun.current?.locked) {
2527
+ onClose();
2528
+ return;
2529
+ }
2071
2530
  if (busy || active.current) return;
2072
2531
  setBusy(true);
2073
2532
  try {
@@ -2080,7 +2539,7 @@ window.__ModuleLoader__.load({
2080
2539
  }
2081
2540
  };
2082
2541
  const runFile = async (operation) => {
2083
- if (busy || active.current) return;
2542
+ if (busy || agentRun.current?.locked || active.current) return;
2084
2543
  setBusy(true);
2085
2544
  setError("");
2086
2545
  try {
@@ -2092,15 +2551,33 @@ window.__ModuleLoader__.load({
2092
2551
  }
2093
2552
  };
2094
2553
  (0, react.useEffect)(() => {
2095
- if (open && incoming && incoming !== received.current) {
2554
+ if (open && !agentLocked && incoming && incoming !== received.current) {
2096
2555
  received.current = incoming;
2097
2556
  runFile(() => importImage(incoming.file));
2098
2557
  }
2099
- }, [open, incoming]);
2558
+ }, [
2559
+ open,
2560
+ incoming,
2561
+ agentLocked
2562
+ ]);
2100
2563
  const keyDown = (event) => {
2101
2564
  if (event.target.closest("input,textarea,select,[contenteditable=true]") || event.isComposing || busy || active.current) return;
2102
2565
  if (!navigation.shortcuts) return;
2103
2566
  if (navigation.keyDown(event)) return;
2567
+ if (agentRun.current?.locked) return;
2568
+ if (selection && ["Delete", "Backspace"].includes(event.key)) {
2569
+ event.preventDefault();
2570
+ editObject({}, "delete");
2571
+ return;
2572
+ }
2573
+ if (event.key.toLowerCase() === "v") {
2574
+ setTool("select");
2575
+ return;
2576
+ }
2577
+ if (event.key.toLowerCase() === "t") {
2578
+ setTool("text");
2579
+ return;
2580
+ }
2104
2581
  const key = event.key.toLowerCase(), command = event.ctrlKey || event.metaKey;
2105
2582
  if (command && [
2106
2583
  "z",
@@ -2137,6 +2614,32 @@ window.__ModuleLoader__.load({
2137
2614
  const gesture = active.current;
2138
2615
  if (!gesture || gesture.id !== event.pointerId || busy) return;
2139
2616
  const rect = bounds ?? canvas.current.getBoundingClientRect();
2617
+ if (gesture.object) {
2618
+ const point = sketchPoint(event.clientX, event.clientY, rect);
2619
+ if (!point) return;
2620
+ try {
2621
+ const box = objectBounds(gesture.object);
2622
+ const stroke = gesture.handle === "end" ? {
2623
+ ...gesture.object,
2624
+ points: [gesture.object.points[0], point]
2625
+ } : gesture.handle === "size" ? transformObject(gesture.object, {
2626
+ scaleX: Math.max(.001, point.x - box.x) / Math.max(.001, box.width),
2627
+ scaleY: Math.max(.001, point.y - box.y) / Math.max(.001, box.height)
2628
+ }) : transformObject(gesture.object, {
2629
+ dx: point.x - gesture.start.x,
2630
+ dy: point.y - gesture.start.y
2631
+ });
2632
+ doc.current = {
2633
+ ...doc.current,
2634
+ layers: doc.current.layers.map((l) => l.id === gesture.layer ? {
2635
+ ...l,
2636
+ strokes: l.strokes.map((s) => s.id === gesture.object.id ? stroke : s)
2637
+ } : l)
2638
+ };
2639
+ schedule();
2640
+ } catch {}
2641
+ return;
2642
+ }
2140
2643
  const native = event.nativeEvent ?? event;
2141
2644
  const events = native.getCoalescedEvents?.() ?? [];
2142
2645
  for (const sample of events.length ? [...events, native] : [native]) {
@@ -2157,6 +2660,7 @@ window.__ModuleLoader__.load({
2157
2660
  const stroke = layer.strokes.at(-1);
2158
2661
  if ([
2159
2662
  "line",
2663
+ "arrow",
2160
2664
  "rectangle",
2161
2665
  "circle"
2162
2666
  ].includes(stroke.shape)) {
@@ -2194,13 +2698,29 @@ window.__ModuleLoader__.load({
2194
2698
  const gesture = active.current, stroke = doc.current.layers.find((layer) => layer.id === gesture.layer)?.strokes.at(-1);
2195
2699
  if (gesture.smoothing && stroke) stroke.points = smoothStrokePoints(stroke.points, gesture.smoothing);
2196
2700
  }
2701
+ const drawn = active.current;
2702
+ if (!cancel && !drawn.object && [
2703
+ "line",
2704
+ "arrow",
2705
+ "rectangle",
2706
+ "circle"
2707
+ ].includes(tool)) {
2708
+ const stroke = doc.current.layers.find((l) => l.id === drawn.layer)?.strokes.at(-1);
2709
+ if (stroke) {
2710
+ setSelection({
2711
+ layer: drawn.layer,
2712
+ id: stroke.id
2713
+ });
2714
+ setTool("select");
2715
+ }
2716
+ }
2197
2717
  active.current = null;
2198
2718
  if (canvas.current.hasPointerCapture(event.pointerId)) canvas.current.releasePointerCapture(event.pointerId);
2199
2719
  if (cancel) doc.current = undo.current.pop() ?? doc.current;
2200
2720
  schedule();
2201
2721
  };
2202
2722
  const history = (direction) => {
2203
- if (busy || active.current) return;
2723
+ if (busy || agentRun.current?.locked || active.current) return;
2204
2724
  const source = direction === "undo" ? undo : redo, target = direction === "undo" ? redo : undo;
2205
2725
  if (!source.current.length) return;
2206
2726
  documentRevision.current++;
@@ -2210,9 +2730,10 @@ window.__ModuleLoader__.load({
2210
2730
  schedule();
2211
2731
  };
2212
2732
  Object.assign(agentAdapter.current, {
2213
- available: () => open && enabled,
2733
+ available: () => enabled && agentEnabled,
2734
+ previewEnabled: () => agentPreview,
2214
2735
  open: () => onOpen(),
2215
- busy: () => busy || Boolean(active.current),
2736
+ busy: () => busy || Boolean(active.current) || Boolean(textEdit) || sizeGesture.current,
2216
2737
  document: () => doc.current,
2217
2738
  snapshot: () => ({
2218
2739
  documentId: documentId.current,
@@ -2229,6 +2750,15 @@ window.__ModuleLoader__.load({
2229
2750
  })),
2230
2751
  strokeCount: strokeCount(doc.current)
2231
2752
  }),
2753
+ objects: () => sketchObjectSummary(doc.current),
2754
+ object: (id, layer = doc.current.active) => {
2755
+ const target = doc.current.layers.find((l) => l.id === layer)?.strokes.find((s, i) => objectId(s, i) === id);
2756
+ if (!target) throw Error("Object not found");
2757
+ return {
2758
+ ...target,
2759
+ id
2760
+ };
2761
+ },
2232
2762
  commit: (next) => {
2233
2763
  checkpoint();
2234
2764
  doc.current = next;
@@ -2239,24 +2769,27 @@ window.__ModuleLoader__.load({
2239
2769
  paint();
2240
2770
  return canvas.current.toDataURL("image/png");
2241
2771
  },
2242
- save: async (name) => {
2243
- setBusy(true);
2244
- try {
2245
- await save(name);
2246
- } finally {
2247
- setBusy(false);
2248
- }
2249
- }
2772
+ save
2250
2773
  });
2251
2774
  agentSession.current ??= createSketchCommandSession(agentAdapter.current);
2775
+ agentRun.current ??= createSketchAgentRun({
2776
+ execute: (request) => agentSession.current(request),
2777
+ open: () => agentAdapter.current.open(),
2778
+ changed: (state) => {
2779
+ setAgentState(state);
2780
+ setNoticeHidden(false);
2781
+ },
2782
+ busy: () => agentAdapter.current.busy(),
2783
+ previewEnabled: () => agentAdapter.current.previewEnabled()
2784
+ });
2252
2785
  (0, react.useEffect)(() => {
2253
- if (!open || !enabled || !agentEnabled) return;
2786
+ if (!enabled || !agentEnabled) return;
2254
2787
  const api = Object.freeze({
2255
2788
  version: 1,
2256
2789
  sessionId,
2257
- execute: (request) => agentSession.current(request),
2790
+ execute: (request) => agentRun.current.execute(request),
2258
2791
  export: async (format) => {
2259
- if (agentAdapter.current.busy()) throw Error("Sketch is being edited");
2792
+ if (agentAdapter.current.busy() || agentRun.current.locked) throw Error("Sketch is being edited");
2260
2793
  const { blob, extension } = await agentAdapter.current.export(format);
2261
2794
  const data = new Uint8Array(await blob.arrayBuffer());
2262
2795
  let raw = "";
@@ -2273,7 +2806,6 @@ window.__ModuleLoader__.load({
2273
2806
  if (window.dshSketchAgent === api) delete window.dshSketchAgent;
2274
2807
  };
2275
2808
  }, [
2276
- open,
2277
2809
  enabled,
2278
2810
  agentEnabled,
2279
2811
  rpc,
@@ -2282,14 +2814,17 @@ window.__ModuleLoader__.load({
2282
2814
  (0, react.useEffect)(() => {
2283
2815
  if (!enabled || !agentEnabled || !rpc || !sessionId) return;
2284
2816
  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);
2817
+ agentRun.current.resume();
2818
+ const disconnect = connectSketchAgent(rpc, sessionId, (request) => {
2819
+ if (!live) throw Error("Sketch session disconnected");
2820
+ return agentRun.current.execute(request);
2821
+ }, (message) => {
2822
+ agentRun.current.fail();
2823
+ setError(message);
2824
+ }, () => 350);
2291
2825
  return () => {
2292
2826
  live = false;
2827
+ agentRun.current.stop();
2293
2828
  disconnect();
2294
2829
  };
2295
2830
  }, [
@@ -2299,7 +2834,7 @@ window.__ModuleLoader__.load({
2299
2834
  sessionId
2300
2835
  ]);
2301
2836
  const attach = async () => {
2302
- if (!enabled || busy) return;
2837
+ if (!enabled || busy || agentRun.current?.locked) return;
2303
2838
  setBusy(true);
2304
2839
  setError("");
2305
2840
  try {
@@ -2342,13 +2877,36 @@ window.__ModuleLoader__.load({
2342
2877
  link.remove();
2343
2878
  setTimeout(() => URL.revokeObjectURL(url), 1e4);
2344
2879
  };
2345
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("dialog", {
2880
+ 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", {
2881
+ className: "codexSketchBackgroundStatus",
2882
+ role: "status",
2883
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2884
+ type: "button",
2885
+ onClick: onOpen,
2886
+ children: t(`sketchRun_${agentState}`)
2887
+ }), agentLocked ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2888
+ type: "button",
2889
+ "aria-label": t("sketchRunStop"),
2890
+ onClick: () => agentRun.current.stop(),
2891
+ children: "×"
2892
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2893
+ type: "button",
2894
+ "aria-label": t("sketchCancel"),
2895
+ onClick: () => setNoticeHidden(true),
2896
+ children: "×"
2897
+ })]
2898
+ }) : null, /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("dialog", {
2346
2899
  ref: dialog,
2347
2900
  className: "codexSketchDialog codexSketchStudio codexLayerStudio",
2348
2901
  "aria-label": t("sketchTitle"),
2349
2902
  onKeyDown: keyDown,
2350
2903
  onKeyUp: navigation.keyUp,
2351
2904
  onPaste: (event) => {
2905
+ if (agentRun.current?.locked) {
2906
+ event.preventDefault();
2907
+ event.stopPropagation();
2908
+ return;
2909
+ }
2352
2910
  if (event.target.closest("input,textarea")) return;
2353
2911
  const file = Array.from(event.clipboardData.items).find((item) => item.type.startsWith("image/"))?.getAsFile();
2354
2912
  if (file) {
@@ -2383,7 +2941,7 @@ window.__ModuleLoader__.load({
2383
2941
  type: "button",
2384
2942
  "aria-label": t("sketchCancel"),
2385
2943
  title: t("sketchCancel"),
2386
- disabled: busy,
2944
+ disabled: busy && !agentLocked,
2387
2945
  onClick: close,
2388
2946
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(WorkspaceIcon, { name: "close" })
2389
2947
  }),
@@ -2394,7 +2952,7 @@ window.__ModuleLoader__.load({
2394
2952
  importImage,
2395
2953
  download,
2396
2954
  hasContent: doc.current.layers.some((l) => l.visible && (l.image || l.strokes.length)),
2397
- disabled: busy,
2955
+ disabled: agentLocked || busy,
2398
2956
  t,
2399
2957
  report: setError,
2400
2958
  onWorking: setBusy
@@ -2413,7 +2971,7 @@ window.__ModuleLoader__.load({
2413
2971
  className: "codexSketchRound",
2414
2972
  "aria-label": t(name === "undo" ? "sketchUndo" : "sketchRedo"),
2415
2973
  title: t(name === "undo" ? "sketchUndo" : "sketchRedo"),
2416
- disabled: busy || !(name === "undo" ? undo : redo).current.length,
2974
+ disabled: agentLocked || busy || !(name === "undo" ? undo : redo).current.length,
2417
2975
  onClick: () => history(name),
2418
2976
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(WorkspaceIcon, {
2419
2977
  name,
@@ -2426,9 +2984,9 @@ window.__ModuleLoader__.load({
2426
2984
  "aria-label": t("sketchRatio"),
2427
2985
  title: t("sketchRatioHint"),
2428
2986
  value: doc.current.ratio ?? "1:1",
2429
- disabled: busy,
2987
+ disabled: agentLocked || busy,
2430
2988
  onChange: (event) => {
2431
- if (active.current) return;
2989
+ if (active.current || agentRun.current?.locked) return;
2432
2990
  const next = resizeSketch(doc.current, event.target.value);
2433
2991
  if (next === doc.current) return;
2434
2992
  checkpoint();
@@ -2469,7 +3027,7 @@ window.__ModuleLoader__.load({
2469
3027
  type: "button",
2470
3028
  className: "codexSketchConfirm",
2471
3029
  "aria-label": t("sketchAttach"),
2472
- disabled: busy || !enabled || !doc.current.layers.some((l) => l.visible && (l.strokes.length || l.image)),
3030
+ disabled: agentLocked || busy || !enabled || !doc.current.layers.some((l) => l.visible && (l.strokes.length || l.image)),
2473
3031
  onClick: () => void attach(),
2474
3032
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(WorkspaceIcon, {
2475
3033
  name: "check",
@@ -2478,6 +3036,25 @@ window.__ModuleLoader__.load({
2478
3036
  })
2479
3037
  ]
2480
3038
  }),
3039
+ enabled && agentEnabled && agentState !== "idle" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3040
+ className: "codexSketchAgentStatus",
3041
+ role: "status",
3042
+ "aria-live": "polite",
3043
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t(`sketchRun_${agentState}`) }), agentLocked ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
3044
+ type: "button",
3045
+ "aria-label": t("sketchRunStop"),
3046
+ title: t("sketchRunStop"),
3047
+ onClick: () => agentRun.current.stop(),
3048
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(WorkspaceIcon, {
3049
+ name: "close",
3050
+ size: 16
3051
+ })
3052
+ }) : agentState === "stopped" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
3053
+ type: "button",
3054
+ onClick: () => agentRun.current.resume(),
3055
+ children: t("sketchRunResume")
3056
+ }) : null]
3057
+ }) : null,
2481
3058
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2482
3059
  className: `codexLayerBody ${layersOpen ? "withLayers" : ""}`,
2483
3060
  children: [
@@ -2485,7 +3062,7 @@ window.__ModuleLoader__.load({
2485
3062
  tabIndex: 0,
2486
3063
  style: {
2487
3064
  transform: `translate(${navigation.view.x}px,${navigation.view.y}px) scale(${navigation.view.scale})`,
2488
- cursor: navigation.space ? "grab" : "none",
3065
+ cursor: navigation.space ? "grab" : agentLocked ? "default" : tool === "select" ? "default" : tool === "text" ? "text" : "none",
2489
3066
  "--sketch-ratio": (doc.current.width ?? 1024) / (doc.current.height ?? 1024)
2490
3067
  },
2491
3068
  ref: canvas,
@@ -2495,7 +3072,69 @@ window.__ModuleLoader__.load({
2495
3072
  onPointerDown: (event) => {
2496
3073
  if (busy || !enabled || active.current) return;
2497
3074
  if (navigation.down(event)) return;
3075
+ if (agentRun.current?.locked) return;
2498
3076
  if (event.button !== 0) return;
3077
+ const point = sketchPoint(event.clientX, event.clientY, canvas.current.getBoundingClientRect());
3078
+ if (!point) return;
3079
+ if (tool === "text") {
3080
+ setTextEdit({
3081
+ point,
3082
+ value: ""
3083
+ });
3084
+ return;
3085
+ }
3086
+ if (tool === "select") {
3087
+ doc.current = identifyObjects(doc.current);
3088
+ if (selected) {
3089
+ const b = objectBounds(selected), end = ["line", "arrow"].includes(selected.shape) ? selected.points.at(-1) : {
3090
+ x: b.x + b.width,
3091
+ y: b.y + b.height
3092
+ }, rect = canvas.current.getBoundingClientRect();
3093
+ if (Math.hypot((end.x - point.x) * rect.width, (end.y - point.y) * rect.height) < 12) {
3094
+ checkpoint();
3095
+ active.current = {
3096
+ id: event.pointerId,
3097
+ layer: selection.layer,
3098
+ object: {
3099
+ ...selected,
3100
+ id: selection.id
3101
+ },
3102
+ start: point,
3103
+ handle: ["line", "arrow"].includes(selected.shape) ? "end" : "size"
3104
+ };
3105
+ canvas.current.setPointerCapture(event.pointerId);
3106
+ return;
3107
+ }
3108
+ }
3109
+ let hit;
3110
+ for (const l of doc.current.layers.slice().reverse()) {
3111
+ if (!l.visible) continue;
3112
+ const stroke = l.strokes.slice().reverse().find((s) => s.shape !== "eraser" && strokeHit(s, point, 6, doc.current.width, doc.current.height));
3113
+ if (stroke) {
3114
+ hit = {
3115
+ layer: l.id,
3116
+ stroke
3117
+ };
3118
+ break;
3119
+ }
3120
+ }
3121
+ setSelection(hit ? {
3122
+ layer: hit.layer,
3123
+ id: hit.stroke.id
3124
+ } : null);
3125
+ if (hit) {
3126
+ checkpoint();
3127
+ active.current = {
3128
+ id: event.pointerId,
3129
+ layer: hit.layer,
3130
+ object: hit.stroke,
3131
+ start: point
3132
+ };
3133
+ canvas.current.setPointerCapture(event.pointerId);
3134
+ }
3135
+ return;
3136
+ }
3137
+ setSelection(null);
2499
3138
  cursor.down(event);
2500
3139
  if (!current.visible) {
2501
3140
  setError(t("sketchHiddenLayer"));
@@ -2523,6 +3162,7 @@ window.__ModuleLoader__.load({
2523
3162
  const layer = layers.find((layer) => layer.id === doc.current.active);
2524
3163
  const eraseStroke = tool === "eraser" && eraser === "stroke";
2525
3164
  if (!eraseStroke) layer.strokes.push({
3165
+ id: crypto.randomUUID(),
2526
3166
  color,
2527
3167
  opacity: flow / 100,
2528
3168
  shape: tool,
@@ -2554,11 +3194,131 @@ window.__ModuleLoader__.load({
2554
3194
  onPointerUp: (event) => end(event),
2555
3195
  onPointerCancel: (event) => end(event, true)
2556
3196
  }),
3197
+ selected && tool === "select" && !agentLocked ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("svg", {
3198
+ className: "codexSketchSelection",
3199
+ viewBox: "0 0 1 1",
3200
+ preserveAspectRatio: "none",
3201
+ style: {
3202
+ "--sketch-ratio": (doc.current.width ?? 1024) / (doc.current.height ?? 1024),
3203
+ transform: `translate(${navigation.view.x}px,${navigation.view.y}px) scale(${navigation.view.scale})`
3204
+ },
3205
+ "aria-hidden": "true",
3206
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("rect", {
3207
+ ...objectBounds(selected),
3208
+ fill: "none",
3209
+ stroke: "#0088ff",
3210
+ strokeWidth: ".002",
3211
+ strokeDasharray: ".008 .005"
3212
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("circle", {
3213
+ cx: ["line", "arrow"].includes(selected.shape) ? selected.points.at(-1).x : objectBounds(selected).x + objectBounds(selected).width,
3214
+ cy: ["line", "arrow"].includes(selected.shape) ? selected.points.at(-1).y : objectBounds(selected).y + objectBounds(selected).height,
3215
+ r: ".007",
3216
+ fill: "white",
3217
+ stroke: "#0088ff",
3218
+ strokeWidth: ".002"
3219
+ })]
3220
+ }) : null,
3221
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SketchSizeControl, {
3222
+ label: t(opacityMode ? "sketchFlow" : selected?.shape === "text" || tool === "text" ? "sketchTextSize" : "sketchWidth"),
3223
+ mode: opacityMode ? "opacity" : "size",
3224
+ modes: showOpacity ? [{
3225
+ value: "size",
3226
+ label: t(selected?.shape === "text" || tool === "text" ? "sketchTextSize" : "sketchSizeShort")
3227
+ }, {
3228
+ value: "opacity",
3229
+ label: t("sketchFlow")
3230
+ }] : void 0,
3231
+ onModeChange: setSizeMode,
3232
+ min: opacityMode ? 5 : 1,
3233
+ max: opacityMode ? 100 : 256,
3234
+ suffix: opacityMode ? "%" : "",
3235
+ value: opacityMode ? (selected?.opacity ?? flow / 100) * 100 : selected?.width ?? width,
3236
+ disabled: agentLocked || busy,
3237
+ onStart: () => {
3238
+ if (selected) {
3239
+ checkpoint();
3240
+ sizeGesture.current = true;
3241
+ }
3242
+ },
3243
+ onEnd: () => {
3244
+ sizeGesture.current = false;
3245
+ },
3246
+ onChange: (value) => {
3247
+ if (opacityMode) {
3248
+ setFlow(value);
3249
+ if (selected) editObject({ opacity: value / 100 });
3250
+ } else {
3251
+ setWidth(value);
3252
+ if (selected) editObject({ width: value });
3253
+ }
3254
+ }
3255
+ }),
3256
+ textEdit ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("form", {
3257
+ className: "codexSketchTextEditor",
3258
+ onSubmit: (event) => {
3259
+ event.preventDefault();
3260
+ if (!textEdit.value.trim()) {
3261
+ setTextEdit(null);
3262
+ return;
3263
+ }
3264
+ try {
3265
+ if (textEdit.selection) editObject({ text: textEdit.value });
3266
+ else {
3267
+ const a = textEdit.point, b = {
3268
+ x: Math.min(1, a.x + .35),
3269
+ y: Math.min(1, a.y + .15)
3270
+ }, id = crypto.randomUUID();
3271
+ const next = applySketchCommands(doc.current, [{
3272
+ op: "stroke",
3273
+ id,
3274
+ shape: "text",
3275
+ text: textEdit.value,
3276
+ color,
3277
+ width: Math.max(24, width),
3278
+ points: [a, b]
3279
+ }]);
3280
+ checkpoint();
3281
+ doc.current = next;
3282
+ setSelection({
3283
+ layer: doc.current.active,
3284
+ id
3285
+ });
3286
+ schedule();
3287
+ }
3288
+ setTextEdit(null);
3289
+ setTool("select");
3290
+ } catch (e) {
3291
+ setError(e.message);
3292
+ }
3293
+ },
3294
+ children: [
3295
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("textarea", {
3296
+ autoFocus: true,
3297
+ "aria-label": t("sketchText"),
3298
+ maxLength: 500,
3299
+ value: textEdit.value,
3300
+ onChange: (e) => setTextEdit({
3301
+ ...textEdit,
3302
+ value: e.target.value
3303
+ })
3304
+ }),
3305
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
3306
+ type: "submit",
3307
+ children: t("sketchTextDone")
3308
+ }),
3309
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
3310
+ type: "button",
3311
+ onClick: () => setTextEdit(null),
3312
+ children: t("sketchCancel")
3313
+ })
3314
+ ]
3315
+ }) : null,
2557
3316
  picturesOpen ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("aside", {
2558
3317
  className: "codexSketchPictures",
2559
3318
  children: [
2560
3319
  /* @__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
3320
  type: "button",
3321
+ disabled: agentLocked || busy,
2562
3322
  onClick: () => pictureInput.current.click(),
2563
3323
  children: t("sketchPictureAdd")
2564
3324
  })] }),
@@ -2577,6 +3337,7 @@ window.__ModuleLoader__.load({
2577
3337
  "data-active": layer.id === doc.current.active,
2578
3338
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2579
3339
  type: "button",
3340
+ disabled: agentLocked || busy,
2580
3341
  "aria-label": `${t("sketchPictureSelect")} ${layer.name}`,
2581
3342
  onClick: () => change("select", layer.id),
2582
3343
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("img", {
@@ -2585,6 +3346,7 @@ window.__ModuleLoader__.load({
2585
3346
  })
2586
3347
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2587
3348
  type: "button",
3349
+ disabled: agentLocked || busy,
2588
3350
  "aria-label": `${t("sketchDeleteDraft")} ${layer.name}`,
2589
3351
  onClick: () => change(doc.current.layers.length === 1 ? "clear" : "delete", layer.id),
2590
3352
  children: "×"
@@ -2601,7 +3363,7 @@ window.__ModuleLoader__.load({
2601
3363
  type: "button",
2602
3364
  title: t("sketchLayerAdd"),
2603
3365
  "aria-label": t("sketchLayerAdd"),
2604
- disabled: busy || doc.current.layers.length >= 8,
3366
+ disabled: agentLocked || busy || doc.current.layers.length >= 8,
2605
3367
  onClick: () => change("add"),
2606
3368
  children: "+"
2607
3369
  })] }),
@@ -2612,6 +3374,7 @@ window.__ModuleLoader__.load({
2612
3374
  "data-active": layer.id === doc.current.active,
2613
3375
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2614
3376
  type: "button",
3377
+ disabled: agentLocked || busy,
2615
3378
  "aria-label": `${t("sketchLayerVisible")} ${layer.id}`,
2616
3379
  "aria-pressed": layer.visible,
2617
3380
  onClick: () => change("visible", layer.id),
@@ -2621,6 +3384,7 @@ window.__ModuleLoader__.load({
2621
3384
  })
2622
3385
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2623
3386
  type: "button",
3387
+ disabled: agentLocked || busy,
2624
3388
  "aria-pressed": layer.id === doc.current.active,
2625
3389
  onClick: () => change("select", layer.id),
2626
3390
  children: layer.name || `${t("sketchLayer")} ${layer.id}`
@@ -2632,6 +3396,7 @@ window.__ModuleLoader__.load({
2632
3396
  children: t("sketchLayerName")
2633
3397
  }),
2634
3398
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
3399
+ disabled: agentLocked || busy,
2635
3400
  "aria-label": t("sketchLayerName"),
2636
3401
  defaultValue: current.name,
2637
3402
  placeholder: `${t("sketchLayer")} ${current.id}`,
@@ -2654,7 +3419,7 @@ window.__ModuleLoader__.load({
2654
3419
  type: "button",
2655
3420
  title: t(`sketchLayer_${action}`),
2656
3421
  "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],
3422
+ 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
3423
  onClick: () => change(action),
2659
3424
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(WorkspaceIcon, {
2660
3425
  name: action === "delete" ? "clear" : action,
@@ -2665,7 +3430,7 @@ window.__ModuleLoader__.load({
2665
3430
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
2666
3431
  type: "button",
2667
3432
  className: "codexSketchClearLayer",
2668
- disabled: busy || !current.strokes.length && !current.image,
3433
+ disabled: agentLocked || busy || !current.strokes.length && !current.image,
2669
3434
  onClick: () => change("clear"),
2670
3435
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(WorkspaceIcon, {
2671
3436
  name: "clear",
@@ -2690,57 +3455,123 @@ window.__ModuleLoader__.load({
2690
3455
  navigation,
2691
3456
  t
2692
3457
  }),
2693
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
3458
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2694
3459
  className: "codexSketchPill",
2695
3460
  role: "toolbar",
2696
3461
  "aria-label": t("sketchTitle"),
2697
3462
  title: t("sketchShortcuts"),
2698
3463
  children: [
2699
- "pen",
2700
- "pencil",
2701
- "marker",
2702
- "eraser",
2703
- "line",
2704
- "rectangle",
2705
- "circle"
2706
- ].map((name) => {
2707
- const drawing = [
3464
+ [
3465
+ "select",
2708
3466
  "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", {
3467
+ "text",
3468
+ "eraser"
3469
+ ].map((name) => {
3470
+ const drawing = [
3471
+ "pen",
3472
+ "pencil",
3473
+ "marker"
3474
+ ].includes(name);
3475
+ const label = t(drawing ? `sketchBrush_${name}` : `sketchTool_${name}`);
3476
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
3477
+ type: "button",
3478
+ "aria-label": label,
3479
+ title: label,
3480
+ "aria-pressed": drawing ? tool === "pen" && brush === name : tool === name,
3481
+ disabled: agentLocked || busy,
3482
+ onClick: () => {
3483
+ setTool(drawing ? "pen" : name);
3484
+ if (name !== "select") setSelection(null);
3485
+ if (drawing) setBrush(name);
3486
+ },
3487
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(WorkspaceIcon, {
3488
+ name,
3489
+ size: 23
3490
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: label })]
3491
+ }, name);
3492
+ }),
3493
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
3494
+ className: "codexSketchShapeToggle",
2714
3495
  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
- },
3496
+ "aria-expanded": shapesOpen,
3497
+ disabled: agentLocked || busy,
3498
+ onClick: () => setShapesOpen((v) => !v),
2723
3499
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(WorkspaceIcon, {
2724
- name,
3500
+ name: "rectangle",
2725
3501
  size: 23
2726
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: label })]
2727
- }, name);
2728
- })
3502
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("sketchShapes") })]
3503
+ }),
3504
+ shapesOpen ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
3505
+ className: "codexSketchShapeMenu",
3506
+ children: [
3507
+ "line",
3508
+ "arrow",
3509
+ "rectangle",
3510
+ "circle"
3511
+ ].map((name) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
3512
+ type: "button",
3513
+ onClick: () => {
3514
+ setTool(name);
3515
+ setSelection(null);
3516
+ setShapesOpen(false);
3517
+ },
3518
+ children: t(`sketchTool_${name}`)
3519
+ }, name))
3520
+ }) : null
3521
+ ]
2729
3522
  }),
2730
3523
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2731
3524
  className: "codexLayerBrush",
2732
3525
  children: [
2733
3526
  ["rectangle", "circle"].includes(tool) ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
2734
3527
  type: "checkbox",
3528
+ disabled: agentLocked || busy,
2735
3529
  checked: fillShape,
2736
3530
  onChange: (e) => setFillShape(e.target.checked)
2737
3531
  }), t("sketchFill")] }) : null,
3532
+ tool === "pen" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("select", {
3533
+ "aria-label": t("sketchBrush"),
3534
+ value: brush,
3535
+ onChange: (e) => setBrush(e.target.value),
3536
+ disabled: agentLocked || busy,
3537
+ children: [
3538
+ "pen",
3539
+ "pencil",
3540
+ "marker"
3541
+ ].map((b) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
3542
+ value: b,
3543
+ children: t(`sketchBrush_${b}`)
3544
+ }, b))
3545
+ }) : null,
3546
+ selected ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3547
+ className: "codexSketchObjectActions",
3548
+ children: [
3549
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
3550
+ disabled: agentLocked || busy,
3551
+ onClick: () => editObject({}, "duplicate"),
3552
+ children: t("sketchObjectDuplicate")
3553
+ }),
3554
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
3555
+ disabled: agentLocked || busy,
3556
+ onClick: () => editObject({}, "delete"),
3557
+ children: t("sketchObjectDelete")
3558
+ }),
3559
+ selected.shape === "text" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
3560
+ disabled: agentLocked || busy,
3561
+ onClick: () => setTextEdit({
3562
+ selection,
3563
+ value: selected.text
3564
+ }),
3565
+ children: t("sketchText")
3566
+ }) : null
3567
+ ]
3568
+ }) : null,
2738
3569
  tool === "pen" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
2739
3570
  className: "codexSketchStability",
2740
3571
  children: [t("sketchStability"), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("select", {
2741
3572
  "aria-label": t("sketchStability"),
2742
3573
  value: stability,
2743
- disabled: busy,
3574
+ disabled: agentLocked || busy,
2744
3575
  onChange: (e) => setStability(Number(e.target.value)),
2745
3576
  children: [
2746
3577
  0,
@@ -2760,43 +3591,11 @@ window.__ModuleLoader__.load({
2760
3591
  children: ["pixel", "stroke"].map((value) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2761
3592
  type: "button",
2762
3593
  "aria-pressed": eraser === value,
2763
- disabled: busy,
3594
+ disabled: agentLocked || busy,
2764
3595
  onClick: () => setEraser(value),
2765
3596
  children: t(`sketchErase_${value}`)
2766
3597
  }, 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
- })
3598
+ }) : null
2800
3599
  ]
2801
3600
  }),
2802
3601
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
@@ -2808,9 +3607,9 @@ window.__ModuleLoader__.load({
2808
3607
  className: "codexSketchSwatch",
2809
3608
  style: { "--swatch": value },
2810
3609
  "aria-label": `${t("sketchColor")} ${value}`,
2811
- "aria-pressed": color === value,
2812
- disabled: busy,
2813
- onClick: () => setColor(value)
3610
+ "aria-pressed": (selected?.color ?? color) === value,
3611
+ disabled: agentLocked || busy,
3612
+ onClick: () => pickColor(value)
2814
3613
  }, value)), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
2815
3614
  className: "codexSketchCustom",
2816
3615
  title: t("sketchColor"),
@@ -2818,8 +3617,8 @@ window.__ModuleLoader__.load({
2818
3617
  type: "color",
2819
3618
  "aria-label": t("sketchColor"),
2820
3619
  value: color,
2821
- disabled: busy,
2822
- onChange: (e) => setColor(e.target.value)
3620
+ disabled: agentLocked || busy,
3621
+ onChange: (e) => pickColor(e.target.value)
2823
3622
  })]
2824
3623
  })]
2825
3624
  })
@@ -2831,11 +3630,26 @@ window.__ModuleLoader__.load({
2831
3630
  children: error
2832
3631
  }) : null
2833
3632
  ]
2834
- });
3633
+ })] });
2835
3634
  }
2836
3635
  //#endregion
2837
3636
  //#region src/sketch-styles.js
2838
3637
  const SKETCH_CSS = `
3638
+ .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)}
3639
+
3640
+ .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}
3641
+ .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)}
3642
+ .codexSketchSizeControl input{writing-mode:vertical-lr;direction:rtl;width:28px;height:150px;appearance:none;background:transparent;cursor:ns-resize;touch-action:none}
3643
+ .codexSketchSizeControl input::-webkit-slider-runnable-track{width:4px;border-radius:4px;background:var(--sketch-line)}
3644
+ .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}
3645
+ .codexSketchSelection{position:absolute;width:min(100cqw,calc(100cqh * var(--sketch-ratio,1)));height:auto;aspect-ratio:var(--sketch-ratio);pointer-events:none;overflow:visible}
3646
+ .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}
3647
+ .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)}
3648
+ .codexSketchObjectActions{display:flex;gap:8px}.codexSketchObjectActions button{padding:6px 8px;border-radius:8px;background:var(--sketch-line)}
3649
+ .codexLayerBrush>select{background:var(--sketch-bg);color:inherit;border:1px solid var(--sketch-line);border-radius:8px;padding:6px}
3650
+
3651
+ .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}
3652
+ .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
3653
  .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
3654
  .codexSketchDialog[open]{display:flex;flex-direction:column;gap:8px}
2841
3655
  .codexSketchDialog::backdrop{background:#0005;backdrop-filter:blur(12px)}
@@ -2887,7 +3701,7 @@ window.__ModuleLoader__.load({
2887
3701
  .codexLayerBody{overflow:hidden}.codexLayerStudio canvas{cursor:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24'%3E%3Ccircle cx='12' cy='12' r='7' fill='none' stroke='white' stroke-width='3'/%3E%3Ccircle cx='12' cy='12' r='7' fill='none' stroke='%23333' stroke-width='1'/%3E%3C/svg%3E") 12 12,crosshair}
2888
3702
  .codexSketchDialog .codexSketchSwatch{width:32px;height:32px;padding:0;border-radius:50%;corner-shape:round}.codexSketchSwatch::before{width:22px;height:22px;flex-shrink:0;border-radius:50%;corner-shape:round;clip-path:circle(50%)}.codexSketchSwatch[aria-pressed=true]::after{inset:1px;border:1.5px solid var(--sketch-fg);border-radius:50%;corner-shape:round}.codexSketchCustom{width:30px;height:30px;margin:1px 4px;corner-shape:round;clip-path:circle(50%)}.codexSketchCustom span{width:22px;height:22px;border-width:2px;corner-shape:round}
2889
3703
  .codexSketchPictures{position:absolute;left:0;top:0;z-index:3;background:var(--sketch-bg);border:1px solid var(--sketch-line);border-radius:12px;padding:10px;width:160px;max-height:100%;overflow:auto}.codexSketchPictures header{display:flex;align-items:center;justify-content:space-between;margin-bottom:8px}.codexSketchPictures>div{display:flex;gap:5px;align-items:center;padding:4px;border:1px solid transparent;border-radius:8px}.codexSketchPictures>div[data-active=true]{border-color:#0a84ff}.codexSketchPictures img{width:90px;height:65px;object-fit:contain}.codexSketchPictures small{color:var(--sketch-muted)}
2890
- .codexSketchViewControls{display:flex;gap:2px;align-items:center}.codexSketchViewControls button{min-height:32px;padding:0 7px;border-radius:8px}.codexSketchKeyPanel{position:absolute;bottom:80px;left:12px;width:280px;max-height:65%;overflow:auto;padding:14px;border:1px solid var(--sketch-line);border-radius:12px;background:var(--sketch-bg);z-index:5;box-shadow:0 8px 30px #0003}.codexSketchKeyPanel label{display:flex;align-items:center;justify-content:space-between;min-height:32px;gap:12px}.codexSketchKeyPanel input:not([type=checkbox]){width:64px;border:1px solid var(--sketch-line);border-radius:6px;color:inherit;background:transparent;padding:4px;text-align:center}.codexSketchKeyPanel p,.codexSketchKeyPanel small{color:var(--sketch-muted);font-size:11px}.codexSketchControls{position:static}
3704
+ .codexSketchViewControls{display:flex;gap:2px;align-items:center}.codexSketchViewControls button{min-height:32px;padding:0 7px;border-radius:8px}.codexSketchKeyPanel{position:absolute;overflow:auto;overscroll-behavior:contain;padding:12px;border:1px solid var(--sketch-line);border-radius:14px;background:var(--sketch-bg);z-index:5;box-shadow:0 8px 30px #0003;scrollbar-width:thin}.codexSketchKeyPanel header{display:flex;align-items:center;justify-content:space-between;margin-bottom:8px}.codexSketchKeyPanel header button{width:28px;height:28px;border-radius:8px;font-size:20px}.codexSketchKeyPanel label{display:flex;align-items:center;justify-content:space-between;min-height:32px;gap:8px}.codexSketchKeysEnabled{padding-bottom:8px;border-bottom:1px solid var(--sketch-line)}.codexSketchKeysEnabled input{margin:0;accent-color:#0a84ff}.codexSketchKeyGrid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:4px 16px;margin:10px 0}.codexSketchKeyPanel input:not([type=checkbox]){width:52px;min-width:0;border:1px solid var(--sketch-line);border-radius:6px;color:inherit;background:color-mix(in srgb,var(--sketch-fg) 4%,transparent);padding:4px;text-align:center;font:inherit}.codexSketchKeyPanel p,.codexSketchKeyPanel small{display:block;color:var(--sketch-muted);font-size:11px;line-height:1.5;margin:0}.codexSketchKeyPanel small{border-top:1px solid var(--sketch-line);padding-top:8px}@media(max-width:380px){.codexSketchKeyGrid{grid-template-columns:1fr}}.codexSketchControls{position:static}
2891
3705
 
2892
3706
  .codexSketchCursor{position:fixed;left:0;top:0;z-index:100;pointer-events:none;border:1px solid #222;border-radius:50%;corner-shape:round;box-shadow:0 0 0 1px #fff;box-sizing:border-box}.codexSketchCursor[hidden]{display:none}
2893
3707
  `;
@@ -2906,6 +3720,7 @@ window.__ModuleLoader__.load({
2906
3720
  }
2907
3721
  }), [registerOpen]);
2908
3722
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(react_jsx_runtime.Fragment, { children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SketchStudio, {
3723
+ agentPreview: settings.imageSketchAgentPreview,
2909
3724
  agentEnabled: settings.imageSketchAgent,
2910
3725
  onOpen: () => {
2911
3726
  opener.current = document.activeElement;
@@ -2953,7 +3768,7 @@ window.__ModuleLoader__.load({
2953
3768
  }) : null, /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SketchWorkspace, {
2954
3769
  ...props,
2955
3770
  registerOpen: registerWorkspace
2956
- })] });
3771
+ }, props.sessionId)] });
2957
3772
  }
2958
3773
  //#endregion
2959
3774
  //#region src/image-composer.js
@@ -3000,9 +3815,10 @@ window.__ModuleLoader__.load({
3000
3815
  }
3001
3816
  const createSketchTrigger = (options) => createWorkspaceTrigger({
3002
3817
  ...options,
3818
+ open: () => {},
3003
3819
  name: "Sketch · Beta",
3004
3820
  aliases: ["sketch", "草图"],
3005
- description: "Draw a reference image"
3821
+ description: "Ask the Agent to draw or edit a sketch"
3006
3822
  });
3007
3823
  const createImageTrigger = (options) => createWorkspaceTrigger({
3008
3824
  ...options,
@@ -3013,6 +3829,17 @@ window.__ModuleLoader__.load({
3013
3829
  //#endregion
3014
3830
  //#region src/client-locales.js
3015
3831
  const zh = {
3832
+ sketchSizeShort: "粗细",
3833
+ sketchObjectDuplicate: "复制对象",
3834
+ sketchObjectDelete: "删除对象",
3835
+ sketchTool_select: "选择",
3836
+ sketchTool_text: "文字",
3837
+ sketchTool_arrow: "箭头",
3838
+ sketchShapes: "图形",
3839
+ sketchText: "编辑文字",
3840
+ sketchTextDone: "完成",
3841
+ sketchTextSize: "字号",
3842
+ sketchBrush: "笔型",
3016
3843
  sketchFill: "填色",
3017
3844
  sketchDownload: "下载",
3018
3845
  sketchExportFormat: "导出格式",
@@ -3122,7 +3949,17 @@ window.__ModuleLoader__.load({
3122
3949
  sketchCanvas_on: "开启",
3123
3950
  sketchCanvas_off: "关闭",
3124
3951
  sketchAgent: "Agent 绘图 · Beta",
3125
- sketchAgentHint: "默认关闭;开启后向模型提供绘图工具,需要同时开启草图画板。",
3952
+ sketchAgentHint: "默认关闭;开启后可用 @sketch 请求 Agent 绘图,需要同时开启草图画板。",
3953
+ sketchRun_drawing: "Agent 正在绘制…",
3954
+ sketchRun_finished: "绘制完成",
3955
+ sketchRun_stopped: "绘制已停止",
3956
+ sketchRun_failed: "绘制失败",
3957
+ sketchRunStop: "停止绘制",
3958
+ sketchRunResume: "允许继续绘制",
3959
+ sketchAgentPreview: "完成后返回预览 · Beta",
3960
+ sketchAgentPreviewHint: "默认关闭;完成绘制后向模型返回画布图片,会增加图片输入用量。",
3961
+ sketchAgentPreview_on: "开启",
3962
+ sketchAgentPreview_off: "关闭",
3126
3963
  sketchAgent_on: "开启",
3127
3964
  sketchAgent_off: "关闭",
3128
3965
  imageSettings: "图片",
@@ -3398,6 +4235,17 @@ window.__ModuleLoader__.load({
3398
4235
  imageRemoveAnnotation: "删除标注"
3399
4236
  };
3400
4237
  const en = {
4238
+ sketchSizeShort: "Size",
4239
+ sketchObjectDuplicate: "Duplicate object",
4240
+ sketchObjectDelete: "Delete object",
4241
+ sketchTool_select: "Select",
4242
+ sketchTool_text: "Text",
4243
+ sketchTool_arrow: "Arrow",
4244
+ sketchShapes: "Shapes",
4245
+ sketchText: "Edit text",
4246
+ sketchTextDone: "Done",
4247
+ sketchTextSize: "Text size",
4248
+ sketchBrush: "Brush",
3401
4249
  sketchFill: "Fill",
3402
4250
  sketchDownload: "Download",
3403
4251
  sketchExportFormat: "Export format",
@@ -3507,7 +4355,17 @@ window.__ModuleLoader__.load({
3507
4355
  sketchCanvas_on: "On",
3508
4356
  sketchCanvas_off: "Off",
3509
4357
  sketchAgent: "Agent drawing · Beta",
3510
- sketchAgentHint: "Off by default. Exposes drawing tools to the model; requires the sketch canvas.",
4358
+ sketchAgentHint: "Off by default. Use @sketch to ask the Agent to draw; requires the sketch canvas.",
4359
+ sketchRun_drawing: "Agent is drawing…",
4360
+ sketchRun_finished: "Drawing complete",
4361
+ sketchRun_stopped: "Drawing stopped",
4362
+ sketchRun_failed: "Drawing failed",
4363
+ sketchRunStop: "Stop drawing",
4364
+ sketchRunResume: "Allow drawing again",
4365
+ sketchAgentPreview: "Preview on completion · Beta",
4366
+ sketchAgentPreviewHint: "Off by default. Returns the canvas to the model on completion, adding image input usage.",
4367
+ sketchAgentPreview_on: "On",
4368
+ sketchAgentPreview_off: "Off",
3511
4369
  sketchAgent_on: "On",
3512
4370
  sketchAgent_off: "Off",
3513
4371
  imageSettings: "Images",
@@ -4964,7 +5822,8 @@ window.__ModuleLoader__.load({
4964
5822
  imageViewer: true,
4965
5823
  imageAnnotations: true,
4966
5824
  imageSketch: false,
4967
- imageSketchAgent: false
5825
+ imageSketchAgent: false,
5826
+ imageSketchAgentPreview: false
4968
5827
  });
4969
5828
  function readImageFeatures(value = {}) {
4970
5829
  return Object.fromEntries(Object.entries(IMAGE_FEATURE_DEFAULTS).map(([key, fallback]) => [key, typeof value?.[key] === "boolean" ? value[key] : fallback]));
@@ -5602,7 +6461,7 @@ window.__ModuleLoader__.load({
5602
6461
  }
5603
6462
  //#endregion
5604
6463
  //#region src/version.js
5605
- const PACKAGE_VERSION = "2.1.0-beta.2";
6464
+ const PACKAGE_VERSION = "2.1.0-beta.4";
5606
6465
  //#endregion
5607
6466
  //#region src/client-recovery.js
5608
6467
  async function recoveryCall(rpc, endpoint, payload = {}, timeoutMs = 1e4) {
@@ -6172,6 +7031,7 @@ window.__ModuleLoader__.load({
6172
7031
  imageEntryPoints: ["imageShortcut"],
6173
7032
  sketchCanvas: ["imageSketch"],
6174
7033
  sketchAgent: ["imageSketchAgent"],
7034
+ sketchAgentPreview: ["imageSketchAgentPreview"],
6175
7035
  imageBrowsing: ["imageViewer", "imageAnnotations"]
6176
7036
  });
6177
7037
  function imageGroupValue(snapshot, group) {
@@ -7921,15 +8781,14 @@ window.__ModuleLoader__.load({
7921
8781
  ctx.inject(["inputTriggers"], (triggerContext) => triggerContext.effect(() => triggerContext.get("inputTriggers").registerSource(createSketchTrigger({
7922
8782
  enabled: () => {
7923
8783
  const value = preference.getSnapshot();
7924
- return value.imageSketch && value.imageEditing;
8784
+ return value.imageSketchAgent && value.imageSketch && value.imageEditing;
7925
8785
  },
7926
- open: (sessionId) => sketchOpeners.get(sessionId)?.(),
7927
8786
  consume: (sessionId, span) => {
7928
8787
  const actx = sessions.scope(sessionId);
7929
- return sketchOpeners.has(sessionId) && actx?.bail(actx, "slash/input-consume-token", { guard: {
7930
- kind: "span",
8788
+ return sketchOpeners.has(sessionId) && actx?.bail(actx, "slash/input-insert-text", {
8789
+ text: "@sketch ",
7931
8790
  span
7932
- } }) === true;
8791
+ }) === true;
7933
8792
  }
7934
8793
  })), "codex-subscription: Sketch trigger"));
7935
8794
  ctx.inject(["inputTriggers"], (triggerContext) => triggerContext.effect(() => triggerContext.get("inputTriggers").registerSource(createImageTrigger({