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.
- package/LICENSE +21 -0
- package/README.md +155 -0
- package/dist/ai-invalidation.d.ts +29 -0
- package/dist/ai-invalidation.d.ts.map +1 -0
- package/dist/ai-invalidation.js +121 -0
- package/dist/ai-invalidation.js.map +1 -0
- package/dist/contrast.d.ts +87 -0
- package/dist/contrast.d.ts.map +1 -0
- package/dist/contrast.js +151 -0
- package/dist/contrast.js.map +1 -0
- package/dist/directive.d.ts +37 -0
- package/dist/directive.d.ts.map +1 -0
- package/dist/directive.js +87 -0
- package/dist/directive.js.map +1 -0
- package/dist/dom.d.ts +83 -0
- package/dist/dom.d.ts.map +1 -0
- package/dist/dom.js +146 -0
- package/dist/dom.js.map +1 -0
- package/dist/host-actions.d.ts +192 -0
- package/dist/host-actions.d.ts.map +1 -0
- package/dist/host-actions.js +443 -0
- package/dist/host-actions.js.map +1 -0
- package/dist/icons.d.ts +21 -0
- package/dist/icons.d.ts.map +1 -0
- package/dist/icons.js +84 -0
- package/dist/icons.js.map +1 -0
- package/dist/index.d.ts +600 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +4833 -0
- package/dist/index.js.map +1 -0
- package/dist/labels.d.ts +187 -0
- package/dist/labels.d.ts.map +1 -0
- package/dist/labels.js +232 -0
- package/dist/labels.js.map +1 -0
- package/dist/limits.d.ts +43 -0
- package/dist/limits.d.ts.map +1 -0
- package/dist/limits.js +48 -0
- package/dist/limits.js.map +1 -0
- package/dist/markdown.d.ts +23 -0
- package/dist/markdown.d.ts.map +1 -0
- package/dist/markdown.js +0 -0
- package/dist/markdown.js.map +1 -0
- package/dist/message-chrome.d.ts +45 -0
- package/dist/message-chrome.d.ts.map +1 -0
- package/dist/message-chrome.js +101 -0
- package/dist/message-chrome.js.map +1 -0
- package/dist/pane-follow.d.ts +67 -0
- package/dist/pane-follow.d.ts.map +1 -0
- package/dist/pane-follow.js +61 -0
- package/dist/pane-follow.js.map +1 -0
- package/dist/prefix.d.ts +9 -0
- package/dist/prefix.d.ts.map +1 -0
- package/dist/prefix.js +9 -0
- package/dist/prefix.js.map +1 -0
- package/dist/proposal-summary.d.ts +19 -0
- package/dist/proposal-summary.d.ts.map +1 -0
- package/dist/proposal-summary.js +73 -0
- package/dist/proposal-summary.js.map +1 -0
- package/dist/replay.d.ts +125 -0
- package/dist/replay.d.ts.map +1 -0
- package/dist/replay.js +172 -0
- package/dist/replay.js.map +1 -0
- package/dist/safe-url.d.ts +47 -0
- package/dist/safe-url.d.ts.map +1 -0
- package/dist/safe-url.js +80 -0
- package/dist/safe-url.js.map +1 -0
- package/dist/sgiant-ai-widget.global.js +778 -0
- package/dist/sgiant-ai-widget.global.js.map +7 -0
- package/dist/specs.d.ts +173 -0
- package/dist/specs.d.ts.map +1 -0
- package/dist/specs.js +231 -0
- package/dist/specs.js.map +1 -0
- package/dist/storage.d.ts +77 -0
- package/dist/storage.d.ts.map +1 -0
- package/dist/storage.js +124 -0
- package/dist/storage.js.map +1 -0
- package/dist/styles.d.ts +29 -0
- package/dist/styles.d.ts.map +1 -0
- package/dist/styles.js +876 -0
- package/dist/styles.js.map +1 -0
- package/dist/ui-control.d.ts +8 -0
- package/dist/ui-control.d.ts.map +1 -0
- package/dist/ui-control.js +8 -0
- package/dist/ui-control.js.map +1 -0
- package/dist/ui-render.d.ts +75 -0
- package/dist/ui-render.d.ts.map +1 -0
- package/dist/ui-render.js +617 -0
- package/dist/ui-render.js.map +1 -0
- package/dist/ui-spec.d.ts +143 -0
- package/dist/ui-spec.d.ts.map +1 -0
- package/dist/ui-spec.js +288 -0
- package/dist/ui-spec.js.map +1 -0
- 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"}
|
package/dist/prefix.d.ts
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 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"}
|
package/dist/replay.d.ts
ADDED
|
@@ -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"}
|
package/dist/safe-url.js
ADDED
|
@@ -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"}
|