@insitue/sdk 0.1.0 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Rod Leviton
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  mountCaptureOnly
3
- } from "./chunk-BYR4ZXVS.js";
4
- import "./chunk-6SMY7D6U.js";
3
+ } from "./chunk-YUNYW2IC.js";
4
+ import "./chunk-X7V2UEBO.js";
5
5
  export {
6
6
  mountCaptureOnly
7
7
  };
@@ -9,7 +9,7 @@ import {
9
9
  k,
10
10
  runtimeErrorCount,
11
11
  y
12
- } from "./chunk-6SMY7D6U.js";
12
+ } from "./chunk-X7V2UEBO.js";
13
13
 
14
14
  // src/client.ts
15
15
  var CompanionClient = class {
@@ -173,6 +173,42 @@ function diffLines(diff) {
173
173
  return k("div", { style: `color:${color};white-space:pre` }, ln || " ");
174
174
  });
175
175
  }
176
+ function renderMessageBody(text) {
177
+ const parts = [];
178
+ const lines = text.split("\n");
179
+ let buf = [];
180
+ let inCode = false;
181
+ let lang = "";
182
+ const flush = () => {
183
+ if (buf.length === 0) return;
184
+ parts.push({ code: inCode, lang, text: buf.join("\n") });
185
+ buf = [];
186
+ };
187
+ for (const line of lines) {
188
+ const fence = /^```(\w*)\s*$/.exec(line);
189
+ if (fence) {
190
+ flush();
191
+ inCode = !inCode;
192
+ lang = inCode ? fence[1] ?? "" : "";
193
+ continue;
194
+ }
195
+ buf.push(line);
196
+ }
197
+ flush();
198
+ return parts.map(
199
+ (p) => p.code ? k(
200
+ "div",
201
+ {
202
+ style: `${card};padding:8px;margin:4px 0;font:${mono};color:#ececef;overflow-x:auto;white-space:pre`
203
+ },
204
+ p.text
205
+ ) : k(
206
+ "div",
207
+ { style: "white-space:pre-wrap;word-break:break-word" },
208
+ p.text
209
+ )
210
+ );
211
+ }
176
212
  function diffBlock(changes) {
177
213
  return changes.map(
178
214
  (c) => k("div", { style: "margin:6px 0" }, [
@@ -225,6 +261,7 @@ function App(props) {
225
261
  const changesRef = A([]);
226
262
  const activeTurnRef = A(null);
227
263
  const threadRef = A(null);
264
+ const panelRef = A(null);
228
265
  autoApplyRef.current = autoApply;
229
266
  changesRef.current = changes;
230
267
  activeTurnRef.current = activeTurn;
@@ -235,6 +272,32 @@ function App(props) {
235
272
  }
236
273
  return [...ms, { role: "agent", text: delta }];
237
274
  });
275
+ const storageKey = `insitue:session:${typeof window !== "undefined" ? window.location.origin : "default"}`;
276
+ const hydratedRef = A(false);
277
+ y(() => {
278
+ if (hydratedRef.current) return;
279
+ hydratedRef.current = true;
280
+ try {
281
+ const raw = window.localStorage.getItem(storageKey);
282
+ if (!raw) return;
283
+ const saved = JSON.parse(raw);
284
+ if (Array.isArray(saved.messages)) setMessages(saved.messages);
285
+ if (Array.isArray(saved.history)) setHistory(saved.history);
286
+ if (typeof saved.autoApply === "boolean") setAutoApply(saved.autoApply);
287
+ if (typeof saved.open === "boolean") setOpen(saved.open);
288
+ } catch {
289
+ }
290
+ }, [storageKey]);
291
+ y(() => {
292
+ if (!hydratedRef.current) return;
293
+ try {
294
+ window.localStorage.setItem(
295
+ storageKey,
296
+ JSON.stringify({ messages, history, autoApply, open })
297
+ );
298
+ } catch {
299
+ }
300
+ }, [messages, history, autoApply, open, storageKey]);
238
301
  y(() => {
239
302
  installRuntimeCollectors();
240
303
  const c = new CompanionClient(props.port, {
@@ -370,6 +433,25 @@ function App(props) {
370
433
  const el = threadRef.current;
371
434
  if (el) el.scrollTop = el.scrollHeight;
372
435
  }, [messages, changes, turnBusy, activity]);
436
+ y(() => {
437
+ const onKey = (ev) => {
438
+ const meta = ev.metaKey || ev.ctrlKey;
439
+ if (meta && ev.key === "k") {
440
+ ev.preventDefault();
441
+ setOpen(true);
442
+ setTimeout(() => {
443
+ const ta = panelRef.current?.querySelector(
444
+ "textarea"
445
+ );
446
+ ta?.focus();
447
+ }, 0);
448
+ } else if (ev.key === "Escape" && open && !chatInput.trim()) {
449
+ setOpen(false);
450
+ }
451
+ };
452
+ window.addEventListener("keydown", onKey);
453
+ return () => window.removeEventListener("keydown", onKey);
454
+ }, [open, chatInput]);
373
455
  const captureSel = async (sel) => {
374
456
  setLastSel(sel);
375
457
  const b = await buildBundle(sel);
@@ -394,9 +476,30 @@ function App(props) {
394
476
  }
395
477
  };
396
478
  const sendChat = () => {
397
- if (!client || !bundle || !chatInput.trim() || turnBusy) return;
398
- const turnId = bundle.id;
479
+ if (!client || !chatInput.trim() || turnBusy) return;
399
480
  const text = chatInput.trim();
481
+ if (text.startsWith("/")) {
482
+ const [cmd, ...rest] = text.split(/\s+/);
483
+ if (cmd === "/clear") {
484
+ setMessages([]);
485
+ setChatInput("");
486
+ return;
487
+ }
488
+ if (cmd === "/undo") {
489
+ const top = history.find((h) => h.status === "applied");
490
+ if (top) client.sendUndo(top.turnId);
491
+ setChatInput("");
492
+ return;
493
+ }
494
+ if (cmd === "/commit") {
495
+ const msg = rest.join(" ") || "Apply InSitue session changes";
496
+ client.sendCommitSession(msg);
497
+ setChatInput("");
498
+ return;
499
+ }
500
+ }
501
+ if (!bundle) return;
502
+ const turnId = bundle.id;
400
503
  setActiveTurn({ turnId, prompt: text, sel: lastSel });
401
504
  setMessages((ms) => [...ms, { role: "user", text }]);
402
505
  setChatInput("");
@@ -550,12 +653,26 @@ ${resolved.snippet}`
550
653
  },
551
654
  [
552
655
  ...messages.map(
553
- (m) => k(
656
+ (m, i) => k(
554
657
  "div",
555
658
  {
556
- style: m.role === "user" ? `align-self:flex-end;max-width:88%;${card};border-color:#2e2e3c;padding:6px 8px;color:#ececef;white-space:pre-wrap;word-break:break-word` : `align-self:flex-start;max-width:96%;padding:6px 8px;color:#ececef;white-space:pre-wrap;word-break:break-word`
659
+ style: m.role === "user" ? `align-self:flex-end;max-width:88%;${card};border-color:#2e2e3c;padding:6px 8px;color:#ececef;white-space:pre-wrap;word-break:break-word;cursor:pointer` : `align-self:flex-start;max-width:96%;padding:6px 8px;color:#ececef`,
660
+ // Click any prior user message to re-populate the
661
+ // input — matches Claude.ai / Cursor edit-and-retry.
662
+ // The original turn stays in the thread; sending
663
+ // creates a new turn.
664
+ onClick: m.role === "user" ? () => {
665
+ setChatInput(m.text);
666
+ setTimeout(() => {
667
+ const ta = panelRef.current?.querySelector(
668
+ "textarea"
669
+ );
670
+ ta?.focus();
671
+ }, 0);
672
+ } : void 0,
673
+ title: m.role === "user" ? "click to edit + retry" : void 0
557
674
  },
558
- m.text
675
+ m.role === "agent" ? renderMessageBody(m.text) : m.text
559
676
  )
560
677
  ),
561
678
  thinking && turnBusy ? k(
@@ -687,7 +804,7 @@ ${resolved.snippet}`
687
804
  proposed,
688
805
  k("textarea", {
689
806
  value: chatInput,
690
- placeholder: messages.length ? "reply\u2026 (the agent remembers this thread)" : "what does this do? \xB7 make the padding bigger \xB7 fix this bug",
807
+ placeholder: messages.length ? "reply\u2026 (the agent remembers this thread) \xB7 /undo /clear /commit" : "what does this do? \xB7 make the padding bigger \xB7 fix this bug \xB7 \u2318K to focus",
691
808
  rows: 2,
692
809
  onInput: (ev) => setChatInput(ev.target.value),
693
810
  onKeyDown: (ev) => {
@@ -852,6 +969,7 @@ ${resolved.snippet}`
852
969
  const panel = open ? k(
853
970
  "div",
854
971
  {
972
+ ref: panelRef,
855
973
  style: {
856
974
  position: "fixed",
857
975
  bottom: "64px",
@@ -1592,10 +1592,6 @@ async function toCanvas(node, options = {}) {
1592
1592
  context.drawImage(img, 0, 0, canvas.width, canvas.height);
1593
1593
  return canvas;
1594
1594
  }
1595
- async function toPng(node, options = {}) {
1596
- const canvas = await toCanvas(node, options);
1597
- return canvas.toDataURL();
1598
- }
1599
1595
 
1600
1596
  // src/capture.ts
1601
1597
  function crossOrigin(url) {
@@ -1632,6 +1628,63 @@ function crossOriginMediaReason(root) {
1632
1628
  }
1633
1629
  return null;
1634
1630
  }
1631
+ function findContextAncestor(el) {
1632
+ const minW = 420;
1633
+ const minH = 140;
1634
+ const maxW = window.innerWidth * 1.2;
1635
+ const maxH = window.innerHeight * 1.2;
1636
+ let cur = el;
1637
+ for (let depth = 0; depth < 8; depth++) {
1638
+ const r3 = cur.getBoundingClientRect();
1639
+ if (r3.width >= minW && r3.height >= minH) return cur;
1640
+ const parent = cur.parentElement;
1641
+ if (!parent) return cur;
1642
+ const pr = parent.getBoundingClientRect();
1643
+ if (pr.width > maxW || pr.height > maxH) return cur;
1644
+ cur = parent;
1645
+ }
1646
+ return cur;
1647
+ }
1648
+ async function renderViewportCrop(cropRect, pixelRatio) {
1649
+ const bodyBg = getComputedStyle(document.body).backgroundColor;
1650
+ const htmlBg = getComputedStyle(document.documentElement).backgroundColor;
1651
+ const backgroundColor = bodyBg && bodyBg !== "rgba(0, 0, 0, 0)" && bodyBg !== "transparent" ? bodyBg : htmlBg && htmlBg !== "rgba(0, 0, 0, 0)" && htmlBg !== "transparent" ? htmlBg : "#ffffff";
1652
+ const fullCanvas = await toCanvas(document.documentElement, {
1653
+ pixelRatio,
1654
+ cacheBust: true,
1655
+ backgroundColor,
1656
+ filter: (n2) => {
1657
+ if (n2 instanceof Element && n2.closest?.("#insitu-root, [data-insitu-layer]")) {
1658
+ return false;
1659
+ }
1660
+ if (n2 instanceof HTMLImageElement) {
1661
+ if (crossOrigin(n2.currentSrc || n2.src) && n2.crossOrigin == null) {
1662
+ return false;
1663
+ }
1664
+ }
1665
+ return true;
1666
+ }
1667
+ });
1668
+ const sx = window.scrollX;
1669
+ const sy = window.scrollY;
1670
+ const out = document.createElement("canvas");
1671
+ out.width = Math.max(1, Math.round(cropRect.width * pixelRatio));
1672
+ out.height = Math.max(1, Math.round(cropRect.height * pixelRatio));
1673
+ const ctx = out.getContext("2d");
1674
+ if (!ctx) return null;
1675
+ ctx.drawImage(
1676
+ fullCanvas,
1677
+ Math.round((cropRect.x + sx) * pixelRatio),
1678
+ Math.round((cropRect.y + sy) * pixelRatio),
1679
+ Math.round(cropRect.width * pixelRatio),
1680
+ Math.round(cropRect.height * pixelRatio),
1681
+ 0,
1682
+ 0,
1683
+ out.width,
1684
+ out.height
1685
+ );
1686
+ return out.toDataURL("image/png");
1687
+ }
1635
1688
  function elementFor(sel) {
1636
1689
  if (sel.mode === "element") return sel.pointerPath?.[0] ?? null;
1637
1690
  if (sel.rect) {
@@ -1648,30 +1701,50 @@ async function buildBundle(sel) {
1648
1701
  let screenshot;
1649
1702
  let screenshotUnavailable;
1650
1703
  if (el instanceof HTMLElement) {
1651
- const taint = crossOriginMediaReason(el);
1652
- if (taint) {
1653
- screenshotUnavailable = `${taint} \u2014 can't rasterise in-browser`;
1654
- } else {
1655
- try {
1656
- const r3 = el.getBoundingClientRect();
1657
- const dataUrl = await toPng(el, {
1658
- pixelRatio: Math.min(dpr, 2),
1659
- cacheBust: true,
1660
- // Don't try to screenshot our own overlay if it overlaps.
1661
- filter: (n2) => !(n2 instanceof Element && n2.closest?.("#insitu-root, [data-insitu-layer]"))
1662
- });
1663
- if (!dataUrl || dataUrl.length < 256) {
1664
- screenshotUnavailable = "rasterise produced an empty image";
1665
- } else {
1666
- screenshot = {
1667
- mime: "image/png",
1668
- dataUrl,
1669
- bounds: { x: r3.x, y: r3.y, width: r3.width, height: r3.height }
1670
- };
1671
- }
1672
- } catch {
1673
- screenshotUnavailable = "rasterise failed";
1704
+ const context = findContextAncestor(el);
1705
+ const cr = context.getBoundingClientRect();
1706
+ const cropRect = new DOMRect(
1707
+ Math.max(0, cr.x),
1708
+ Math.max(0, cr.y),
1709
+ Math.min(window.innerWidth, cr.right) - Math.max(0, cr.x),
1710
+ Math.min(window.innerHeight, cr.bottom) - Math.max(0, cr.y)
1711
+ );
1712
+ const orig = {
1713
+ outline: el.style.outline,
1714
+ outlineOffset: el.style.outlineOffset
1715
+ };
1716
+ el.style.outline = "3px solid #ff6b00";
1717
+ el.style.outlineOffset = "2px";
1718
+ try {
1719
+ const dataUrl = await renderViewportCrop(
1720
+ cropRect,
1721
+ // Cap pixel ratio for full-document rasterise — 2× of a
1722
+ // long page is enough to blow per-tab canvas memory caps
1723
+ // on some browsers (and 1.5× still looks crisp).
1724
+ Math.min(dpr, 1.5)
1725
+ );
1726
+ if (!dataUrl || dataUrl.length < 1024) {
1727
+ const taint = crossOriginMediaReason(el);
1728
+ screenshotUnavailable = taint ? `${taint} \u2014 can't rasterise in-browser` : "rasterise produced an empty image";
1729
+ } else {
1730
+ screenshot = {
1731
+ mime: "image/png",
1732
+ dataUrl,
1733
+ // Bounds describe the SCREENSHOT (the crop region) so
1734
+ // the dashboard knows what slice of viewport this is.
1735
+ bounds: {
1736
+ x: cropRect.x,
1737
+ y: cropRect.y,
1738
+ width: cropRect.width,
1739
+ height: cropRect.height
1740
+ }
1741
+ };
1674
1742
  }
1743
+ } catch {
1744
+ screenshotUnavailable = "rasterise failed";
1745
+ } finally {
1746
+ el.style.outline = orig.outline;
1747
+ el.style.outlineOffset = orig.outlineOffset;
1675
1748
  }
1676
1749
  }
1677
1750
  return {
@@ -7,7 +7,7 @@ import {
7
7
  installRuntimeCollectors,
8
8
  k,
9
9
  y
10
- } from "./chunk-6SMY7D6U.js";
10
+ } from "./chunk-X7V2UEBO.js";
11
11
 
12
12
  // src/capture-only.ts
13
13
  var DEFAULT_INGEST = "https://www.insitue.com/api/v1/capture";
package/dist/index.js CHANGED
@@ -1,10 +1,10 @@
1
1
  import {
2
2
  mountCaptureOnly
3
- } from "./chunk-BYR4ZXVS.js";
3
+ } from "./chunk-YUNYW2IC.js";
4
4
  import {
5
5
  mountInSitue
6
- } from "./chunk-LGN4LKXD.js";
7
- import "./chunk-6SMY7D6U.js";
6
+ } from "./chunk-VMBBJKFF.js";
7
+ import "./chunk-X7V2UEBO.js";
8
8
 
9
9
  // src/InSitue.tsx
10
10
  import { useEffect } from "react";
package/dist/overlay.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  mountInSitue
3
- } from "./chunk-LGN4LKXD.js";
4
- import "./chunk-6SMY7D6U.js";
3
+ } from "./chunk-VMBBJKFF.js";
4
+ import "./chunk-X7V2UEBO.js";
5
5
  export {
6
6
  mountInSitue
7
7
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@insitue/sdk",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "description": "InSitue capture SDK — drop one snippet into your deployed app; your users point at a bug, InSitue opens a verified pull request.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -10,19 +10,23 @@
10
10
  "exports": {
11
11
  ".": {
12
12
  "types": "./dist/index.d.ts",
13
- "import": "./dist/index.js"
13
+ "import": "./dist/index.js",
14
+ "default": "./dist/index.js"
14
15
  },
15
16
  "./overlay": {
16
17
  "types": "./dist/overlay.d.ts",
17
- "import": "./dist/overlay.js"
18
+ "import": "./dist/overlay.js",
19
+ "default": "./dist/overlay.js"
18
20
  },
19
21
  "./capture-only": {
20
22
  "types": "./dist/capture-only.d.ts",
21
- "import": "./dist/capture-only.js"
23
+ "import": "./dist/capture-only.js",
24
+ "default": "./dist/capture-only.js"
22
25
  },
23
26
  "./babel": {
24
27
  "types": "./dist/babel.d.ts",
25
- "import": "./dist/babel.js"
28
+ "import": "./dist/babel.js",
29
+ "default": "./dist/babel.js"
26
30
  }
27
31
  },
28
32
  "files": [
@@ -31,13 +35,6 @@
31
35
  "LICENSE"
32
36
  ],
33
37
  "sideEffects": false,
34
- "scripts": {
35
- "build": "tsup",
36
- "dev": "tsup --watch",
37
- "typecheck": "tsc --noEmit",
38
- "lint": "tsc --noEmit",
39
- "prepublishOnly": "pnpm build"
40
- },
41
38
  "peerDependencies": {
42
39
  "react": ">=18"
43
40
  },
@@ -51,12 +48,12 @@
51
48
  "preact": "^10.25.1"
52
49
  },
53
50
  "devDependencies": {
54
- "@insitue/capture-core": "workspace:*",
55
51
  "@types/node": "^22.9.0",
56
52
  "@types/react": "^19.0.0",
57
53
  "react": "^19.0.0",
58
54
  "tsup": "^8.3.5",
59
- "typescript": "^5.6.3"
55
+ "typescript": "^5.6.3",
56
+ "@insitue/capture-core": "0.0.0"
60
57
  },
61
58
  "repository": {
62
59
  "type": "git",
@@ -82,5 +79,11 @@
82
79
  "publishConfig": {
83
80
  "access": "public",
84
81
  "registry": "https://registry.npmjs.org/"
82
+ },
83
+ "scripts": {
84
+ "build": "tsup",
85
+ "dev": "tsup --watch",
86
+ "typecheck": "tsc --noEmit",
87
+ "lint": "tsc --noEmit"
85
88
  }
86
- }
89
+ }