sgiant-ai-widget 1.0.0

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.
Files changed (93) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +155 -0
  3. package/dist/ai-invalidation.d.ts +29 -0
  4. package/dist/ai-invalidation.d.ts.map +1 -0
  5. package/dist/ai-invalidation.js +121 -0
  6. package/dist/ai-invalidation.js.map +1 -0
  7. package/dist/contrast.d.ts +87 -0
  8. package/dist/contrast.d.ts.map +1 -0
  9. package/dist/contrast.js +151 -0
  10. package/dist/contrast.js.map +1 -0
  11. package/dist/directive.d.ts +37 -0
  12. package/dist/directive.d.ts.map +1 -0
  13. package/dist/directive.js +87 -0
  14. package/dist/directive.js.map +1 -0
  15. package/dist/dom.d.ts +83 -0
  16. package/dist/dom.d.ts.map +1 -0
  17. package/dist/dom.js +146 -0
  18. package/dist/dom.js.map +1 -0
  19. package/dist/host-actions.d.ts +192 -0
  20. package/dist/host-actions.d.ts.map +1 -0
  21. package/dist/host-actions.js +443 -0
  22. package/dist/host-actions.js.map +1 -0
  23. package/dist/icons.d.ts +21 -0
  24. package/dist/icons.d.ts.map +1 -0
  25. package/dist/icons.js +84 -0
  26. package/dist/icons.js.map +1 -0
  27. package/dist/index.d.ts +600 -0
  28. package/dist/index.d.ts.map +1 -0
  29. package/dist/index.js +4833 -0
  30. package/dist/index.js.map +1 -0
  31. package/dist/labels.d.ts +187 -0
  32. package/dist/labels.d.ts.map +1 -0
  33. package/dist/labels.js +232 -0
  34. package/dist/labels.js.map +1 -0
  35. package/dist/limits.d.ts +43 -0
  36. package/dist/limits.d.ts.map +1 -0
  37. package/dist/limits.js +48 -0
  38. package/dist/limits.js.map +1 -0
  39. package/dist/markdown.d.ts +23 -0
  40. package/dist/markdown.d.ts.map +1 -0
  41. package/dist/markdown.js +0 -0
  42. package/dist/markdown.js.map +1 -0
  43. package/dist/message-chrome.d.ts +45 -0
  44. package/dist/message-chrome.d.ts.map +1 -0
  45. package/dist/message-chrome.js +101 -0
  46. package/dist/message-chrome.js.map +1 -0
  47. package/dist/pane-follow.d.ts +67 -0
  48. package/dist/pane-follow.d.ts.map +1 -0
  49. package/dist/pane-follow.js +61 -0
  50. package/dist/pane-follow.js.map +1 -0
  51. package/dist/prefix.d.ts +9 -0
  52. package/dist/prefix.d.ts.map +1 -0
  53. package/dist/prefix.js +9 -0
  54. package/dist/prefix.js.map +1 -0
  55. package/dist/proposal-summary.d.ts +19 -0
  56. package/dist/proposal-summary.d.ts.map +1 -0
  57. package/dist/proposal-summary.js +73 -0
  58. package/dist/proposal-summary.js.map +1 -0
  59. package/dist/replay.d.ts +125 -0
  60. package/dist/replay.d.ts.map +1 -0
  61. package/dist/replay.js +172 -0
  62. package/dist/replay.js.map +1 -0
  63. package/dist/safe-url.d.ts +47 -0
  64. package/dist/safe-url.d.ts.map +1 -0
  65. package/dist/safe-url.js +80 -0
  66. package/dist/safe-url.js.map +1 -0
  67. package/dist/sgiant-ai-widget.global.js +778 -0
  68. package/dist/sgiant-ai-widget.global.js.map +7 -0
  69. package/dist/specs.d.ts +173 -0
  70. package/dist/specs.d.ts.map +1 -0
  71. package/dist/specs.js +231 -0
  72. package/dist/specs.js.map +1 -0
  73. package/dist/storage.d.ts +77 -0
  74. package/dist/storage.d.ts.map +1 -0
  75. package/dist/storage.js +124 -0
  76. package/dist/storage.js.map +1 -0
  77. package/dist/styles.d.ts +29 -0
  78. package/dist/styles.d.ts.map +1 -0
  79. package/dist/styles.js +876 -0
  80. package/dist/styles.js.map +1 -0
  81. package/dist/ui-control.d.ts +8 -0
  82. package/dist/ui-control.d.ts.map +1 -0
  83. package/dist/ui-control.js +8 -0
  84. package/dist/ui-control.js.map +1 -0
  85. package/dist/ui-render.d.ts +75 -0
  86. package/dist/ui-render.d.ts.map +1 -0
  87. package/dist/ui-render.js +617 -0
  88. package/dist/ui-render.js.map +1 -0
  89. package/dist/ui-spec.d.ts +143 -0
  90. package/dist/ui-spec.d.ts.map +1 -0
  91. package/dist/ui-spec.js +288 -0
  92. package/dist/ui-spec.js.map +1 -0
  93. package/package.json +61 -0
@@ -0,0 +1,61 @@
1
+ /**
2
+ * TWO DECISIONS THE APP PANE MAKES, extracted so they can be checked.
3
+ *
4
+ * Both live inside `createAiChatWidget`'s closure in the real widget, where
5
+ * they are unreachable from a test — and both are the kind of rule that fails
6
+ * politely: get them wrong and the pane either never follows a long operation
7
+ * (the feature silently does nothing) or follows it too eagerly (it yanks a
8
+ * reader off the page they chose, repeatedly, on every poll). Neither shows up
9
+ * as an error.
10
+ */
11
+ /** Same-path test used for both decisions. A query string is a filter within a
12
+ * page, not a different destination; a trailing slash is not a destination
13
+ * either. */
14
+ export function samePanePath(a, b) {
15
+ if (!a || !b)
16
+ return false;
17
+ const norm = (p) => {
18
+ const path = p.split("?")[0] ?? "";
19
+ return path.length > 1 && path.endsWith("/") ? path.slice(0, -1) : path;
20
+ };
21
+ return norm(a) === norm(b);
22
+ }
23
+ /**
24
+ * Should the pane follow a long operation to its live page?
25
+ *
26
+ * Every `false` here is a way this becomes obnoxious rather than helpful, and
27
+ * the third one is the one that bites hardest: a report card polls every couple
28
+ * of seconds, so a decision that does not remember it already followed would
29
+ * reload the page continuously — turning "watch it build" into a surface that
30
+ * cannot be read at all.
31
+ */
32
+ export function shouldAutoNavigate(i) {
33
+ if (!i.advanced || !i.canNavigate)
34
+ return false;
35
+ if (!i.userConfirmedThisSession)
36
+ return false;
37
+ if (i.userDriven || i.alreadyFollowed)
38
+ return false;
39
+ if (!i.path || !i.path.startsWith("/") || i.path.startsWith("//")) {
40
+ return false;
41
+ }
42
+ // Already there: following again would be a pointless full document load.
43
+ if (samePanePath(i.currentPath, i.path))
44
+ return false;
45
+ return true;
46
+ }
47
+ /**
48
+ * Should the chat column collapse its narration to one line?
49
+ *
50
+ * True exactly when the pane is showing the same run — the owner's decision for
51
+ * advanced mode. The report page owns the detail (it has the stage rail and it
52
+ * is the authority on stage), and two renderings of one job on one screen is
53
+ * the divergence advanced view makes visible. When the pane is elsewhere the
54
+ * chat is the ONLY account of what is happening, so it narrates in full.
55
+ */
56
+ export function shouldCollapseNarration(i) {
57
+ if (!i.advanced)
58
+ return false;
59
+ return samePanePath(i.currentPath, i.livePath);
60
+ }
61
+ //# sourceMappingURL=pane-follow.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pane-follow.js","sourceRoot":"","sources":["../src/pane-follow.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH;;cAEc;AACd,MAAM,UAAU,YAAY,CAC1B,CAA4B,EAC5B,CAA4B;IAE5B,IAAI,CAAC,CAAC,IAAI,CAAC,CAAC;QAAE,OAAO,KAAK,CAAC;IAC3B,MAAM,IAAI,GAAG,CAAC,CAAS,EAAU,EAAE;QACjC,MAAM,IAAI,GAAG,CAAC,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;QACnC,OAAO,IAAI,CAAC,MAAM,GAAG,CAAC,IAAI,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;IAC1E,CAAC,CAAC;IACF,OAAO,IAAI,CAAC,CAAC,CAAC,KAAK,IAAI,CAAC,CAAC,CAAC,CAAC;AAC7B,CAAC;AA+BD;;;;;;;;GAQG;AACH,MAAM,UAAU,kBAAkB,CAAC,CAAoB;IACrD,IAAI,CAAC,CAAC,CAAC,QAAQ,IAAI,CAAC,CAAC,CAAC,WAAW;QAAE,OAAO,KAAK,CAAC;IAChD,IAAI,CAAC,CAAC,CAAC,wBAAwB;QAAE,OAAO,KAAK,CAAC;IAC9C,IAAI,CAAC,CAAC,UAAU,IAAI,CAAC,CAAC,eAAe;QAAE,OAAO,KAAK,CAAC;IACpD,IAAI,CAAC,CAAC,CAAC,IAAI,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC,EAAE,CAAC;QAClE,OAAO,KAAK,CAAC;IACf,CAAC;IACD,0EAA0E;IAC1E,IAAI,YAAY,CAAC,CAAC,CAAC,WAAW,EAAE,CAAC,CAAC,IAAI,CAAC;QAAE,OAAO,KAAK,CAAC;IACtD,OAAO,IAAI,CAAC;AACd,CAAC;AAED;;;;;;;;GAQG;AACH,MAAM,UAAU,uBAAuB,CAAC,CAIvC;IACC,IAAI,CAAC,CAAC,CAAC,QAAQ;QAAE,OAAO,KAAK,CAAC;IAC9B,OAAO,YAAY,CAAC,CAAC,CAAC,WAAW,EAAE,CAAC,CAAC,QAAQ,CAAC,CAAC;AACjD,CAAC"}
@@ -0,0 +1,9 @@
1
+ /**
2
+ * The one class prefix every rule and every element in this widget carries.
3
+ *
4
+ * Its own module because both `index.ts` and `styles.ts` need it and neither
5
+ * should import the other: the stylesheet must not pull in the 5800-line
6
+ * widget closure just to know what to call a class (#320).
7
+ */
8
+ export declare const PREFIX = "sgiant-aiw";
9
+ //# sourceMappingURL=prefix.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"prefix.d.ts","sourceRoot":"","sources":["../src/prefix.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,eAAO,MAAM,MAAM,eAAe,CAAC"}
package/dist/prefix.js ADDED
@@ -0,0 +1,9 @@
1
+ /**
2
+ * The one class prefix every rule and every element in this widget carries.
3
+ *
4
+ * Its own module because both `index.ts` and `styles.ts` need it and neither
5
+ * should import the other: the stylesheet must not pull in the 5800-line
6
+ * widget closure just to know what to call a class (#320).
7
+ */
8
+ export const PREFIX = "sgiant-aiw";
9
+ //# sourceMappingURL=prefix.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"prefix.js","sourceRoot":"","sources":["../src/prefix.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,MAAM,GAAG,YAAY,CAAC"}
@@ -0,0 +1,19 @@
1
+ /**
2
+ * The GENERIC half of a confirm card: describe a tool call nobody has taught us
3
+ * about, well enough that a person can approve or refuse it.
4
+ *
5
+ * The sgiant-specific half — fifteen branches phrasing sgiant's own tools, and
6
+ * the titles that go with them — moved to `@sgiant/ai-apply` on 2026-09-02.
7
+ * This package is published for anyone to embed, and a table of one company's
8
+ * tool names is not something a stranger's users should be reading.
9
+ *
10
+ * What is left is what makes the widget SAFE to publish rather than merely safe
11
+ * for sgiant: every host's own tools land here, and a blank card is a confirm
12
+ * gate that confirms nothing. the confirm-card tests pin that.
13
+ *
14
+ * A host phrases its own tools by passing `proposalSummary` — return a string to
15
+ * override, or null to fall through to this.
16
+ */
17
+ /** Render an unknown tool call from its arguments alone. */
18
+ export declare function genericProposalSummary(args: Record<string, unknown>): string;
19
+ //# sourceMappingURL=proposal-summary.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"proposal-summary.d.ts","sourceRoot":"","sources":["../src/proposal-summary.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,4DAA4D;AAC5D,wBAAgB,sBAAsB,CAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM,CAK5E"}
@@ -0,0 +1,73 @@
1
+ /**
2
+ * The GENERIC half of a confirm card: describe a tool call nobody has taught us
3
+ * about, well enough that a person can approve or refuse it.
4
+ *
5
+ * The sgiant-specific half — fifteen branches phrasing sgiant's own tools, and
6
+ * the titles that go with them — moved to `@sgiant/ai-apply` on 2026-09-02.
7
+ * This package is published for anyone to embed, and a table of one company's
8
+ * tool names is not something a stranger's users should be reading.
9
+ *
10
+ * What is left is what makes the widget SAFE to publish rather than merely safe
11
+ * for sgiant: every host's own tools land here, and a blank card is a confirm
12
+ * gate that confirms nothing. the confirm-card tests pin that.
13
+ *
14
+ * A host phrases its own tools by passing `proposalSummary` — return a string to
15
+ * override, or null to fall through to this.
16
+ */
17
+ /** Render an unknown tool call from its arguments alone. */
18
+ export function genericProposalSummary(args) {
19
+ return Object.entries(args)
20
+ .filter(([k]) => k !== "id")
21
+ .map(([k, v]) => `${k}: ${describeArg(v)}`)
22
+ .join("\n");
23
+ }
24
+ /**
25
+ * One argument value, as a person can actually read it.
26
+ *
27
+ * `String(v)` on anything structured yields "[object Object]" — and on a list
28
+ * of them, "[object Object],[object Object],[object Object]". A confirm card
29
+ * that says that is asking the client to approve something they cannot see,
30
+ * which is the one thing a confirm card must never do. Seen for real on a
31
+ * three-scene storyboard: the whole point of the card is that the client
32
+ * agrees the PLAN, and the plan was invisible.
33
+ *
34
+ * Deliberately a SUMMARY, not a JSON dump — a raw blob is technically honest
35
+ * and just as unreadable. Objects are described by whichever of the handful
36
+ * of naming fields they carry, so a scene reads as its title.
37
+ */
38
+ function describeArg(v, depth = 0) {
39
+ if (v === null || v === undefined)
40
+ return "—";
41
+ if (Array.isArray(v)) {
42
+ if (!v.length)
43
+ return "(none)";
44
+ // Nested lists are summarised by count rather than expanded: a card is a
45
+ // glance, not a document.
46
+ if (depth > 0)
47
+ return `${v.length} items`;
48
+ return v.map((x) => `\n • ${describeArg(x, depth + 1)}`).join("");
49
+ }
50
+ if (typeof v === "object") {
51
+ const rec = v;
52
+ // The fields something calls itself by, in the order a human would look.
53
+ const named = ["title", "name", "label", "key", "id"]
54
+ .map((k) => (typeof rec[k] === "string" ? rec[k] : ""))
55
+ .find(Boolean);
56
+ const detail = typeof rec.prompt === "string"
57
+ ? rec.prompt
58
+ : typeof rec.caption === "string"
59
+ ? rec.caption
60
+ : typeof rec.description === "string"
61
+ ? rec.description
62
+ : "";
63
+ if (named || detail)
64
+ return [named, detail && `— ${detail.slice(0, 90)}`]
65
+ .filter(Boolean)
66
+ .join(" ");
67
+ // Nothing self-describing: name its shape rather than lie about it.
68
+ const keys = Object.keys(rec);
69
+ return keys.length ? `{${keys.slice(0, 4).join(", ")}}` : "{}";
70
+ }
71
+ return String(v);
72
+ }
73
+ //# sourceMappingURL=proposal-summary.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"proposal-summary.js","sourceRoot":"","sources":["../src/proposal-summary.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,4DAA4D;AAC5D,MAAM,UAAU,sBAAsB,CAAC,IAA6B;IAClE,OAAO,MAAM,CAAC,OAAO,CAAC,IAAI,CAAC;SACxB,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,KAAK,IAAI,CAAC;SAC3B,GAAG,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,GAAG,CAAC,KAAK,WAAW,CAAC,CAAC,CAAC,EAAE,CAAC;SAC1C,IAAI,CAAC,IAAI,CAAC,CAAC;AAChB,CAAC;AAED;;;;;;;;;;;;;GAaG;AACH,SAAS,WAAW,CAAC,CAAU,EAAE,KAAK,GAAG,CAAC;IACxC,IAAI,CAAC,KAAK,IAAI,IAAI,CAAC,KAAK,SAAS;QAAE,OAAO,GAAG,CAAC;IAC9C,IAAI,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,CAAC;QACrB,IAAI,CAAC,CAAC,CAAC,MAAM;YAAE,OAAO,QAAQ,CAAC;QAC/B,yEAAyE;QACzE,0BAA0B;QAC1B,IAAI,KAAK,GAAG,CAAC;YAAE,OAAO,GAAG,CAAC,CAAC,MAAM,QAAQ,CAAC;QAC1C,OAAO,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,SAAS,WAAW,CAAC,CAAC,EAAE,KAAK,GAAG,CAAC,CAAC,EAAE,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IACrE,CAAC;IACD,IAAI,OAAO,CAAC,KAAK,QAAQ,EAAE,CAAC;QAC1B,MAAM,GAAG,GAAG,CAA4B,CAAC;QACzC,yEAAyE;QACzE,MAAM,KAAK,GAAG,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,KAAK,EAAE,IAAI,CAAC;aAClD,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,OAAO,GAAG,CAAC,CAAC,CAAC,KAAK,QAAQ,CAAC,CAAC,CAAE,GAAG,CAAC,CAAC,CAAY,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;aAClE,IAAI,CAAC,OAAO,CAAC,CAAC;QACjB,MAAM,MAAM,GACV,OAAO,GAAG,CAAC,MAAM,KAAK,QAAQ;YAC5B,CAAC,CAAC,GAAG,CAAC,MAAM;YACZ,CAAC,CAAC,OAAO,GAAG,CAAC,OAAO,KAAK,QAAQ;gBAC/B,CAAC,CAAC,GAAG,CAAC,OAAO;gBACb,CAAC,CAAC,OAAO,GAAG,CAAC,WAAW,KAAK,QAAQ;oBACnC,CAAC,CAAC,GAAG,CAAC,WAAW;oBACjB,CAAC,CAAC,EAAE,CAAC;QACb,IAAI,KAAK,IAAI,MAAM;YACjB,OAAO,CAAC,KAAK,EAAE,MAAM,IAAI,KAAK,MAAM,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,EAAE,CAAC;iBACjD,MAAM,CAAC,OAAO,CAAC;iBACf,IAAI,CAAC,GAAG,CAAC,CAAC;QACf,oEAAoE;QACpE,MAAM,IAAI,GAAG,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;QAC9B,OAAO,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,IAAI,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC;IACjE,CAAC;IACD,OAAO,MAAM,CAAC,CAAC,CAAC,CAAC;AACnB,CAAC"}
@@ -0,0 +1,125 @@
1
+ /**
2
+ * Replaying a saved thread, and the ‹n/m› branch navigation over it.
3
+ *
4
+ * Extracted from index.ts (#320). Measured at ZERO leakage: nothing in these
5
+ * 250 lines reaches a top-level declaration in the file it came from, which is
6
+ * why it moves as a unit and why the move is reviewable.
7
+ *
8
+ * The shape is worth naming, because it is the seam #306 needs: a PURE
9
+ * TRANSFORM — a server payload in, a list of render items out — with no DOM,
10
+ * no transport and no host context. `buildThreadReplay` is already documented
11
+ * as taking a report ID rather than a path for exactly that reason ("a pure
12
+ * function with no host context, and the widget runs in three shells with
13
+ * different route shapes"). A published widget needs more regions like this
14
+ * one and fewer inside the 6000-line closure.
15
+ */
16
+ /** One item replayed from a past thread: a chat message, or an inline data
17
+ * widget (so reopening restores the conversation's charts/tables, not just
18
+ * text). The render hooks / fallback handle the actual drawing. */
19
+ export type LoadedThreadItem = {
20
+ role: "user" | "assistant";
21
+ content: string;
22
+ /** Stable message id (branching). Absent on hosts that predate branching. */
23
+ id?: string;
24
+ /** The model that produced this message, so a vote cast on REPLAYED
25
+ * history is attributable per model like a live one (#299). */
26
+ model?: string;
27
+ /** Parent turn in the conversation tree (null on a root message). */
28
+ parentId?: string | null;
29
+ /** Persisted per-turn token spend (assistant messages) — replays the
30
+ * ↑/↓ tokens caption that the live stream draws, so it survives the
31
+ * end-of-turn thread reload and thread reopen. Shown even on the free
32
+ * staff lane: not billed there, still worth seeing. */
33
+ inputTokens?: number;
34
+ outputTokens?: number;
35
+ /** ‹n/m› sibling switcher data — present when this turn has siblings. */
36
+ branch?: {
37
+ index: number;
38
+ count: number;
39
+ prevLeaf?: string;
40
+ nextLeaf?: string;
41
+ };
42
+ } | {
43
+ kind: "widget";
44
+ spec: unknown;
45
+ rows: unknown;
46
+ comparisonRows?: unknown;
47
+ } | {
48
+ kind: "activity";
49
+ label: string;
50
+ status: string;
51
+ agent?: string;
52
+ model?: string;
53
+ /** The REPORT this step produced, if any.
54
+ *
55
+ * An ID and not a path: `buildThreadReplay` is a pure function with no
56
+ * host context, and the widget runs in three shells with different route
57
+ * shapes. The renderer turns it into a path via `opts.reportHref`. */
58
+ reportId?: string;
59
+ };
60
+ /** The message variant of a replay item (carries the branch metadata). */
61
+ export type ReplayMessageItem = Extract<LoadedThreadItem, {
62
+ role: "user" | "assistant";
63
+ }>;
64
+ /** ‹n/m› sibling navigation for one message (mirrors the panel's BranchNav). */
65
+ export interface BranchNav {
66
+ index: number;
67
+ count: number;
68
+ prevLeaf?: string;
69
+ nextLeaf?: string;
70
+ }
71
+ /** A stored message reduced to what branch navigation needs. */
72
+ export interface BranchMsg {
73
+ id?: string;
74
+ parentId?: string | null;
75
+ createdAt?: string;
76
+ }
77
+ export declare const ROOT_KEY = "__root__";
78
+ /** Deepest leaf under a message, following the most recent child at each step
79
+ * (memoised, cycle-guarded) — the target the ‹n/m› switcher jumps to. Mirrors
80
+ * the full-page panel's `leafUnder`. */
81
+ export declare function leafUnder(id: string, childrenByParent: Map<string, BranchMsg[]>, memo: Map<string, string>, guard?: number): string;
82
+ /**
83
+ * Compute the ‹n/m› sibling switcher for every message on the active path.
84
+ * Siblings share a parent; editing a user turn or regenerating an assistant
85
+ * reply adds one. The prev/next targets are the deepest leaf under the adjacent
86
+ * sibling, so switching lands on a full branch. Mirrors the panel's
87
+ * `computeBranchNav` exactly.
88
+ */
89
+ export declare function computeBranchNav(messages: BranchMsg[], activePathIds: string[]): Map<string, BranchNav>;
90
+ /**
91
+ * Map a thread's messages + artifacts (the `/ai/threads/:id/messages` response)
92
+ * into an ordered replay list — text messages interleaved with their data
93
+ * widgets by `createdAt`, exactly like the full-page assistant. Pure; hosts pass
94
+ * it the fetched payload so the widget reopens a conversation WITH its charts.
95
+ *
96
+ * Branching: when the server sends a non-empty `activePath`, the visible
97
+ * transcript is that branch ONLY — messages (and message-scoped artifacts) are
98
+ * filtered to it, and each surviving message carries its ‹n/m› sibling switcher
99
+ * (computed over ALL messages). Absent/empty path → behave exactly as before
100
+ * (back-compat with pre-branching hosts).
101
+ */
102
+ export declare function buildThreadReplay(payload: {
103
+ messages?: Array<{
104
+ id?: string;
105
+ parentId?: string | null;
106
+ role: string;
107
+ content: string;
108
+ createdAt?: string;
109
+ /** Persisted per-turn token spend (assistant rows; both read endpoints
110
+ * return them) — carried into the replay's tokens caption. */
111
+ inputTokens?: number | null;
112
+ outputTokens?: number | null;
113
+ }>;
114
+ artifacts?: Array<{
115
+ kind: string;
116
+ messageId?: string | null;
117
+ payload?: unknown;
118
+ /** proposed | applied | discarded — what tells a replayed proposal card
119
+ * whether Apply is still a live decision. */
120
+ status?: string;
121
+ createdAt?: string;
122
+ }>;
123
+ activePath?: string[];
124
+ }): LoadedThreadItem[];
125
+ //# sourceMappingURL=replay.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"replay.d.ts","sourceRoot":"","sources":["../src/replay.ts"],"names":[],"mappings":"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"}
package/dist/replay.js ADDED
@@ -0,0 +1,172 @@
1
+ /**
2
+ * Replaying a saved thread, and the ‹n/m› branch navigation over it.
3
+ *
4
+ * Extracted from index.ts (#320). Measured at ZERO leakage: nothing in these
5
+ * 250 lines reaches a top-level declaration in the file it came from, which is
6
+ * why it moves as a unit and why the move is reviewable.
7
+ *
8
+ * The shape is worth naming, because it is the seam #306 needs: a PURE
9
+ * TRANSFORM — a server payload in, a list of render items out — with no DOM,
10
+ * no transport and no host context. `buildThreadReplay` is already documented
11
+ * as taking a report ID rather than a path for exactly that reason ("a pure
12
+ * function with no host context, and the widget runs in three shells with
13
+ * different route shapes"). A published widget needs more regions like this
14
+ * one and fewer inside the 6000-line closure.
15
+ */
16
+ export const ROOT_KEY = "__root__";
17
+ /** Deepest leaf under a message, following the most recent child at each step
18
+ * (memoised, cycle-guarded) — the target the ‹n/m› switcher jumps to. Mirrors
19
+ * the full-page panel's `leafUnder`. */
20
+ export function leafUnder(id, childrenByParent, memo, guard = 0) {
21
+ const cached = memo.get(id);
22
+ if (cached)
23
+ return cached;
24
+ const kids = childrenByParent.get(id);
25
+ if (!kids || kids.length === 0 || guard > 1000) {
26
+ memo.set(id, id);
27
+ return id;
28
+ }
29
+ const last = kids[kids.length - 1];
30
+ const leaf = last?.id
31
+ ? leafUnder(last.id, childrenByParent, memo, guard + 1)
32
+ : id;
33
+ memo.set(id, leaf);
34
+ return leaf;
35
+ }
36
+ /**
37
+ * Compute the ‹n/m› sibling switcher for every message on the active path.
38
+ * Siblings share a parent; editing a user turn or regenerating an assistant
39
+ * reply adds one. The prev/next targets are the deepest leaf under the adjacent
40
+ * sibling, so switching lands on a full branch. Mirrors the panel's
41
+ * `computeBranchNav` exactly.
42
+ */
43
+ export function computeBranchNav(messages, activePathIds) {
44
+ const childrenByParent = new Map();
45
+ for (const m of messages) {
46
+ const key = m.parentId ?? ROOT_KEY;
47
+ const arr = childrenByParent.get(key);
48
+ if (arr)
49
+ arr.push(m);
50
+ else
51
+ childrenByParent.set(key, [m]);
52
+ }
53
+ for (const arr of childrenByParent.values()) {
54
+ arr.sort((a, b) => (a.createdAt ?? "").localeCompare(b.createdAt ?? ""));
55
+ }
56
+ const memo = new Map();
57
+ const byId = new Map();
58
+ for (const m of messages)
59
+ if (m.id)
60
+ byId.set(m.id, m);
61
+ const nav = new Map();
62
+ for (const id of activePathIds) {
63
+ const m = byId.get(id);
64
+ if (!m)
65
+ continue;
66
+ const siblings = childrenByParent.get(m.parentId ?? ROOT_KEY) ?? [];
67
+ if (siblings.length < 2)
68
+ continue;
69
+ const i = siblings.findIndex((s) => s.id === id);
70
+ if (i < 0)
71
+ continue;
72
+ const prev = siblings[i - 1];
73
+ const next = siblings[i + 1];
74
+ nav.set(id, {
75
+ index: i + 1,
76
+ count: siblings.length,
77
+ ...(prev?.id
78
+ ? { prevLeaf: leafUnder(prev.id, childrenByParent, memo) }
79
+ : {}),
80
+ ...(next?.id
81
+ ? { nextLeaf: leafUnder(next.id, childrenByParent, memo) }
82
+ : {}),
83
+ });
84
+ }
85
+ return nav;
86
+ }
87
+ /**
88
+ * Map a thread's messages + artifacts (the `/ai/threads/:id/messages` response)
89
+ * into an ordered replay list — text messages interleaved with their data
90
+ * widgets by `createdAt`, exactly like the full-page assistant. Pure; hosts pass
91
+ * it the fetched payload so the widget reopens a conversation WITH its charts.
92
+ *
93
+ * Branching: when the server sends a non-empty `activePath`, the visible
94
+ * transcript is that branch ONLY — messages (and message-scoped artifacts) are
95
+ * filtered to it, and each surviving message carries its ‹n/m› sibling switcher
96
+ * (computed over ALL messages). Absent/empty path → behave exactly as before
97
+ * (back-compat with pre-branching hosts).
98
+ */
99
+ export function buildThreadReplay(payload) {
100
+ const activePathIds = payload.activePath && payload.activePath.length > 0
101
+ ? payload.activePath
102
+ : null;
103
+ const activeSet = activePathIds ? new Set(activePathIds) : null;
104
+ const branchNav = activePathIds
105
+ ? computeBranchNav(payload.messages ?? [], activePathIds)
106
+ : null;
107
+ const items = [];
108
+ for (const m of payload.messages ?? []) {
109
+ // Off-branch messages don't appear in the active transcript.
110
+ if (activeSet && m.id && !activeSet.has(m.id))
111
+ continue;
112
+ if ((m.role === "user" || m.role === "assistant") && m.content.trim()) {
113
+ const item = {
114
+ role: m.role,
115
+ content: m.content,
116
+ ...(m.id ? { id: m.id } : {}),
117
+ ...(m.parentId !== undefined ? { parentId: m.parentId } : {}),
118
+ ...(m.inputTokens ? { inputTokens: m.inputTokens } : {}),
119
+ ...(m.outputTokens ? { outputTokens: m.outputTokens } : {}),
120
+ ...(m.id && branchNav?.has(m.id)
121
+ ? { branch: branchNav.get(m.id) }
122
+ : {}),
123
+ };
124
+ items.push({ t: m.createdAt ?? "", item });
125
+ }
126
+ }
127
+ for (const a of payload.artifacts ?? []) {
128
+ // Branch-scoped artifacts (tied to a message) only belong to the active
129
+ // branch; artifacts with no messageId are thread-wide and always stay.
130
+ if (activeSet && a.messageId && !activeSet.has(a.messageId))
131
+ continue;
132
+ if (a.kind === "widget") {
133
+ const p = (a.payload ?? {});
134
+ if (!p.spec)
135
+ continue;
136
+ items.push({
137
+ t: a.createdAt ?? "",
138
+ item: {
139
+ kind: "widget",
140
+ spec: p.spec,
141
+ rows: p.rows ?? [],
142
+ comparisonRows: p.comparisonRows ?? null,
143
+ },
144
+ });
145
+ }
146
+ else if (a.kind === "activity") {
147
+ // A persisted process step — replays the timeline (+ acting agent) on reopen.
148
+ const p = (a.payload ?? {});
149
+ if (!p.label)
150
+ continue;
151
+ // The report id was persisted on the artifact and DROPPED here, so a
152
+ // finished report replayed as a chip you could read and not open.
153
+ const reportId = p.status !== "error" && typeof p.reportId === "string" && p.reportId
154
+ ? p.reportId
155
+ : undefined;
156
+ items.push({
157
+ t: a.createdAt ?? "",
158
+ item: {
159
+ kind: "activity",
160
+ label: p.label,
161
+ status: p.status ?? "ok",
162
+ agent: p.agent,
163
+ model: p.model,
164
+ ...(reportId ? { reportId } : {}),
165
+ },
166
+ });
167
+ }
168
+ }
169
+ items.sort((x, y) => x.t.localeCompare(y.t));
170
+ return items.map((s) => s.item);
171
+ }
172
+ //# sourceMappingURL=replay.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"replay.js","sourceRoot":"","sources":["../src/replay.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,47 @@
1
+ /**
2
+ * What the widget will and will not navigate to.
3
+ *
4
+ * Extracted from index.ts (#320) at zero leakage, and kept apart from the
5
+ * directive shapes next door on purpose: this is the security surface, not the
6
+ * vocabulary. Model output is untrusted — it can carry text scraped from
7
+ * third-party sites — so every URL the assistant proposes passes through here.
8
+ *
9
+ * It becomes a public surface when the widget publishes (#306): an embedder
10
+ * pointing the widget at their own backend inherits these rules, so they have
11
+ * to be findable and readable rather than buried mid-file.
12
+ */
13
+ /**
14
+ * Is this an AI-supplied in-app path we're willing to navigate to? Model output
15
+ * is untrusted (it can carry text scraped from third-party sites), so a path is
16
+ * only ever a SITE-ROOT-RELATIVE route: it must start with a single `/` and
17
+ * must not start with `//` or `/\` (both read as protocol-relative, i.e. another
18
+ * origin) — which also rules out `javascript:`/`data:` payloads.
19
+ */
20
+ export declare function isSafeRelPath(path: unknown): path is string;
21
+ /**
22
+ * THE navigation gate — the one the advanced view's frame stands behind.
23
+ *
24
+ * Module-level and exported on purpose. `dispatchAction` is a closure over a
25
+ * live DOM, so with the decision inline there the single check that keeps a
26
+ * model-authored path out of an un-sandboxed, same-origin, session-bearing
27
+ * iframe was unreachable by any test: deleting it left the whole suite green.
28
+ * Here it is exercised directly by the host repo's nav-gate test and the
29
+ * call site is pinned by source assertion.
30
+ *
31
+ * Throws rather than returning a verdict: an undefined return from
32
+ * `dispatchAction` resolves as SUCCESS, and the chip ticks green over a path we
33
+ * had just declined.
34
+ *
35
+ * `getTargets` is a getter, not an array: `refreshTargets` REPLACES the cache,
36
+ * and the second read has to see what the first one fetched.
37
+ */
38
+ export declare function gateNavigationTarget(path: unknown, getTargets: () => readonly {
39
+ path?: string;
40
+ }[], refreshTargets: () => Promise<void>): Promise<void>;
41
+ /**
42
+ * Final gate in front of the advanced-view iframe's `src`. That frame is
43
+ * deliberately un-sandboxed (it hosts our own app with the live session), so
44
+ * only a same-origin http(s) URL may ever be loaded into it.
45
+ */
46
+ export declare function isSafeFrameUrl(url: string): boolean;
47
+ //# sourceMappingURL=safe-url.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"safe-url.d.ts","sourceRoot":"","sources":["../src/safe-url.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAIH;;;;;;GAMG;AACH,wBAAgB,aAAa,CAAC,IAAI,EAAE,OAAO,GAAG,IAAI,IAAI,MAAM,CAO3D;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAsB,oBAAoB,CACxC,IAAI,EAAE,OAAO,EACb,UAAU,EAAE,MAAM,SAAS;IAAE,IAAI,CAAC,EAAE,MAAM,CAAA;CAAE,EAAE,EAC9C,cAAc,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,GAClC,OAAO,CAAC,IAAI,CAAC,CAmBf;AAED;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAUnD"}
@@ -0,0 +1,80 @@
1
+ /**
2
+ * What the widget will and will not navigate to.
3
+ *
4
+ * Extracted from index.ts (#320) at zero leakage, and kept apart from the
5
+ * directive shapes next door on purpose: this is the security surface, not the
6
+ * vocabulary. Model output is untrusted — it can carry text scraped from
7
+ * third-party sites — so every URL the assistant proposes passes through here.
8
+ *
9
+ * It becomes a public surface when the widget publishes (#306): an embedder
10
+ * pointing the widget at their own backend inherits these rules, so they have
11
+ * to be findable and readable rather than buried mid-file.
12
+ */
13
+ import { isKnownNavTarget } from "./host-actions.js";
14
+ /**
15
+ * Is this an AI-supplied in-app path we're willing to navigate to? Model output
16
+ * is untrusted (it can carry text scraped from third-party sites), so a path is
17
+ * only ever a SITE-ROOT-RELATIVE route: it must start with a single `/` and
18
+ * must not start with `//` or `/\` (both read as protocol-relative, i.e. another
19
+ * origin) — which also rules out `javascript:`/`data:` payloads.
20
+ */
21
+ export function isSafeRelPath(path) {
22
+ return (typeof path === "string" &&
23
+ path.startsWith("/") &&
24
+ !path.startsWith("//") &&
25
+ !path.startsWith("/\\"));
26
+ }
27
+ /**
28
+ * THE navigation gate — the one the advanced view's frame stands behind.
29
+ *
30
+ * Module-level and exported on purpose. `dispatchAction` is a closure over a
31
+ * live DOM, so with the decision inline there the single check that keeps a
32
+ * model-authored path out of an un-sandboxed, same-origin, session-bearing
33
+ * iframe was unreachable by any test: deleting it left the whole suite green.
34
+ * Here it is exercised directly by the host repo's nav-gate test and the
35
+ * call site is pinned by source assertion.
36
+ *
37
+ * Throws rather than returning a verdict: an undefined return from
38
+ * `dispatchAction` resolves as SUCCESS, and the chip ticks green over a path we
39
+ * had just declined.
40
+ *
41
+ * `getTargets` is a getter, not an array: `refreshTargets` REPLACES the cache,
42
+ * and the second read has to see what the first one fetched.
43
+ */
44
+ export async function gateNavigationTarget(path, getTargets, refreshTargets) {
45
+ // AI-supplied navigation targets are untrusted: only a root-relative in-app
46
+ // route is ever followed — in the advanced-view frame OR by the host router.
47
+ if (!isSafeRelPath(path))
48
+ throw new Error("that navigation target is not an in-app page");
49
+ // …and the shape test is not the gate. It admits every path in the app,
50
+ // including another tenant's: `/accounts/<someone-else>/settings` is
51
+ // root-relative, and the host's `orgPath`/`adminPath` return an
52
+ // already-`/accounts/`-prefixed path untouched. The manifest this turn was
53
+ // built from is the real allow-list; it has been present as `knownNavTargets`
54
+ // since #111 and nothing but the prose-nav fallback ever read it.
55
+ //
56
+ // The cache holds the LAST TURN THIS SESSION SENT, which is not every chip
57
+ // that can be clicked: `persistKey` restores a transcript across a reload and
58
+ // its nav chips are live before a word has been sent. Ask the host for its
59
+ // catalogue rather than refuse a page the manifest declares.
60
+ if (!getTargets().length)
61
+ await refreshTargets();
62
+ if (!isKnownNavTarget(path, getTargets()))
63
+ throw new Error("that page is not one I can open");
64
+ }
65
+ /**
66
+ * Final gate in front of the advanced-view iframe's `src`. That frame is
67
+ * deliberately un-sandboxed (it hosts our own app with the live session), so
68
+ * only a same-origin http(s) URL may ever be loaded into it.
69
+ */
70
+ export function isSafeFrameUrl(url) {
71
+ try {
72
+ const u = new URL(url, location.origin);
73
+ return (u.origin === location.origin &&
74
+ (u.protocol === "http:" || u.protocol === "https:"));
75
+ }
76
+ catch {
77
+ return false;
78
+ }
79
+ }
80
+ //# sourceMappingURL=safe-url.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"safe-url.js","sourceRoot":"","sources":["../src/safe-url.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,EAAE,gBAAgB,EAAE,MAAM,gBAAgB,CAAC;AAElD;;;;;;GAMG;AACH,MAAM,UAAU,aAAa,CAAC,IAAa;IACzC,OAAO,CACL,OAAO,IAAI,KAAK,QAAQ;QACxB,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC;QACpB,CAAC,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC;QACtB,CAAC,IAAI,CAAC,UAAU,CAAC,KAAK,CAAC,CACxB,CAAC;AACJ,CAAC;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,CAAC,KAAK,UAAU,oBAAoB,CACxC,IAAa,EACb,UAA8C,EAC9C,cAAmC;IAEnC,4EAA4E;IAC5E,6EAA6E;IAC7E,IAAI,CAAC,aAAa,CAAC,IAAI,CAAC;QACtB,MAAM,IAAI,KAAK,CAAC,8CAA8C,CAAC,CAAC;IAClE,wEAAwE;IACxE,qEAAqE;IACrE,gEAAgE;IAChE,2EAA2E;IAC3E,8EAA8E;IAC9E,kEAAkE;IAClE,EAAE;IACF,2EAA2E;IAC3E,8EAA8E;IAC9E,2EAA2E;IAC3E,6DAA6D;IAC7D,IAAI,CAAC,UAAU,EAAE,CAAC,MAAM;QAAE,MAAM,cAAc,EAAE,CAAC;IACjD,IAAI,CAAC,gBAAgB,CAAC,IAAI,EAAE,UAAU,EAAE,CAAC;QACvC,MAAM,IAAI,KAAK,CAAC,iCAAiC,CAAC,CAAC;AACvD,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,cAAc,CAAC,GAAW;IACxC,IAAI,CAAC;QACH,MAAM,CAAC,GAAG,IAAI,GAAG,CAAC,GAAG,EAAE,QAAQ,CAAC,MAAM,CAAC,CAAC;QACxC,OAAO,CACL,CAAC,CAAC,MAAM,KAAK,QAAQ,CAAC,MAAM;YAC5B,CAAC,CAAC,CAAC,QAAQ,KAAK,OAAO,IAAI,CAAC,CAAC,QAAQ,KAAK,QAAQ,CAAC,CACpD,CAAC;IACJ,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,KAAK,CAAC;IACf,CAAC;AACH,CAAC"}