@immediately-run/sdk 0.45.3 → 0.47.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/dist/MDXProvider.cjs +1 -5
- package/dist/MDXProvider.cjs.map +1 -1
- package/dist/MDXProvider.js +1 -5
- package/dist/MDXProvider.js.map +1 -1
- package/dist/RoutingSpec.cjs.map +1 -1
- package/dist/TinkerableContext.cjs.map +1 -1
- package/dist/TinkerableContext.js.map +1 -1
- package/dist/auth.cjs.map +1 -1
- package/dist/auth.js.map +1 -1
- package/dist/boot.cjs +2 -9
- package/dist/boot.cjs.map +1 -1
- package/dist/boot.d.cts +2 -2
- package/dist/boot.d.ts +2 -2
- package/dist/boot.js +2 -9
- package/dist/boot.js.map +1 -1
- package/dist/catalog.cjs.map +1 -1
- package/dist/catalog.js.map +1 -1
- package/dist/components/Admonition.cjs +4 -13
- package/dist/components/Admonition.cjs.map +1 -1
- package/dist/components/Admonition.js +4 -13
- package/dist/components/Admonition.js.map +1 -1
- package/dist/components/FileRouter.cjs +11 -3
- package/dist/components/FileRouter.cjs.map +1 -1
- package/dist/components/FileRouter.js +11 -3
- package/dist/components/FileRouter.js.map +1 -1
- package/dist/components/HeadingAnchor.cjs.map +1 -1
- package/dist/components/HeadingAnchor.js.map +1 -1
- package/dist/components/Link.cjs.map +1 -1
- package/dist/components/Link.js.map +1 -1
- package/dist/components/MainContent.cjs +11 -1
- package/dist/components/MainContent.cjs.map +1 -1
- package/dist/components/MainContent.js +11 -1
- package/dist/components/MainContent.js.map +1 -1
- package/dist/components/MountImage.cjs +1 -9
- package/dist/components/MountImage.cjs.map +1 -1
- package/dist/components/MountImage.js +1 -9
- package/dist/components/MountImage.js.map +1 -1
- package/dist/components/Routes.cjs +1 -4
- package/dist/components/Routes.cjs.map +1 -1
- package/dist/components/Routes.d.cts +1 -1
- package/dist/components/Routes.d.ts +1 -1
- package/dist/components/Routes.js +1 -4
- package/dist/components/Routes.js.map +1 -1
- package/dist/components/SafeInclude.cjs +1 -4
- package/dist/components/SafeInclude.cjs.map +1 -1
- package/dist/components/SafeInclude.js +1 -4
- package/dist/components/SafeInclude.js.map +1 -1
- package/dist/components/WikiLink.cjs +1 -10
- package/dist/components/WikiLink.cjs.map +1 -1
- package/dist/components/WikiLink.js +1 -10
- package/dist/components/WikiLink.js.map +1 -1
- package/dist/components/defaults.cjs.map +1 -1
- package/dist/components/defaults.d.cts +1 -1
- package/dist/components/defaults.d.ts +1 -1
- package/dist/components/defaults.js.map +1 -1
- package/dist/components/errors.cjs +3 -1
- package/dist/components/errors.cjs.map +1 -1
- package/dist/components/errors.js +3 -1
- package/dist/components/errors.js.map +1 -1
- package/dist/contextUtils.cjs.map +1 -1
- package/dist/contextUtils.js.map +1 -1
- package/dist/contribute.cjs.map +1 -1
- package/dist/contribute.js.map +1 -1
- package/dist/debug.cjs +2 -1
- package/dist/debug.cjs.map +1 -1
- package/dist/debug.js +3 -8
- package/dist/debug.js.map +1 -1
- package/dist/diagnostics.cjs.map +1 -1
- package/dist/diagnostics.js.map +1 -1
- package/dist/editor.cjs.map +1 -1
- package/dist/editor.js.map +1 -1
- package/dist/editorContext.cjs.map +1 -1
- package/dist/editorContext.js.map +1 -1
- package/dist/fs.cjs.map +1 -1
- package/dist/fs.js.map +1 -1
- package/dist/hostAttention.cjs +57 -0
- package/dist/hostAttention.cjs.map +1 -0
- package/dist/hostAttention.d.cts +39 -0
- package/dist/hostAttention.d.ts +39 -0
- package/dist/hostAttention.js +31 -0
- package/dist/hostAttention.js.map +1 -0
- package/dist/hostTransport.cjs +55 -0
- package/dist/hostTransport.cjs.map +1 -0
- package/dist/hostTransport.d.cts +12 -0
- package/dist/hostTransport.d.ts +12 -0
- package/dist/hostTransport.js +30 -0
- package/dist/hostTransport.js.map +1 -0
- package/dist/index.cjs +4 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +3 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/injectedBundler.cjs.map +1 -1
- package/dist/injectedBundler.js.map +1 -1
- package/dist/ipc.cjs +1 -4
- package/dist/ipc.cjs.map +1 -1
- package/dist/ipc.js +1 -4
- package/dist/ipc.js.map +1 -1
- package/dist/irMarkers.cjs.map +1 -1
- package/dist/irMarkers.d.cts +11 -11
- package/dist/irMarkers.d.ts +11 -11
- package/dist/irMarkers.js.map +1 -1
- package/dist/launch.cjs.map +1 -1
- package/dist/launch.js.map +1 -1
- package/dist/linkSpace.cjs +11 -2
- package/dist/linkSpace.cjs.map +1 -1
- package/dist/linkSpace.d.cts +23 -0
- package/dist/linkSpace.d.ts +23 -0
- package/dist/linkSpace.js +11 -2
- package/dist/linkSpace.js.map +1 -1
- package/dist/llm.cjs +26 -8
- package/dist/llm.cjs.map +1 -1
- package/dist/llm.d.cts +75 -3
- package/dist/llm.d.ts +75 -3
- package/dist/llm.js +21 -7
- package/dist/llm.js.map +1 -1
- package/dist/loading.cjs +18 -14
- package/dist/loading.cjs.map +1 -1
- package/dist/loading.d.cts +2 -2
- package/dist/loading.d.ts +2 -2
- package/dist/loading.js +19 -19
- package/dist/loading.js.map +1 -1
- package/dist/markers.cjs.map +1 -1
- package/dist/markers.js.map +1 -1
- package/dist/metadataSource.cjs +1 -5
- package/dist/metadataSource.cjs.map +1 -1
- package/dist/metadataSource.d.cts +1 -1
- package/dist/metadataSource.d.ts +1 -1
- package/dist/metadataSource.js +1 -5
- package/dist/metadataSource.js.map +1 -1
- package/dist/moduleCache.cjs +4 -1
- package/dist/moduleCache.cjs.map +1 -1
- package/dist/moduleCache.d.cts +1 -1
- package/dist/moduleCache.d.ts +1 -1
- package/dist/moduleCache.js +4 -1
- package/dist/moduleCache.js.map +1 -1
- package/dist/mountMatch.cjs.map +1 -1
- package/dist/mountMatch.js.map +1 -1
- package/dist/mounts.cjs +6 -1
- package/dist/mounts.cjs.map +1 -1
- package/dist/mounts.d.cts +2 -2
- package/dist/mounts.d.ts +2 -2
- package/dist/mounts.js +6 -1
- package/dist/mounts.js.map +1 -1
- package/dist/netFetch.cjs +3 -5
- package/dist/netFetch.cjs.map +1 -1
- package/dist/netFetch.js +3 -5
- package/dist/netFetch.js.map +1 -1
- package/dist/onFsChange.cjs.map +1 -1
- package/dist/onFsChange.js.map +1 -1
- package/dist/pathUtils.cjs +9 -12
- package/dist/pathUtils.cjs.map +1 -1
- package/dist/pathUtils.js +9 -12
- package/dist/pathUtils.js.map +1 -1
- package/dist/protocolDeadline.cjs +205 -0
- package/dist/protocolDeadline.cjs.map +1 -0
- package/dist/protocolDeadline.d.cts +146 -0
- package/dist/protocolDeadline.d.ts +146 -0
- package/dist/protocolDeadline.js +168 -0
- package/dist/protocolDeadline.js.map +1 -0
- package/dist/protocolStream.cjs +49 -6
- package/dist/protocolStream.cjs.map +1 -1
- package/dist/protocolStream.d.cts +9 -2
- package/dist/protocolStream.d.ts +9 -2
- package/dist/protocolStream.js +56 -6
- package/dist/protocolStream.js.map +1 -1
- package/dist/pushChannel.cjs +13 -9
- package/dist/pushChannel.cjs.map +1 -1
- package/dist/pushChannel.js +12 -8
- package/dist/pushChannel.js.map +1 -1
- package/dist/ready.cjs.map +1 -1
- package/dist/ready.js.map +1 -1
- package/dist/routing.cjs +3 -1
- package/dist/routing.cjs.map +1 -1
- package/dist/routing.js +3 -1
- package/dist/routing.js.map +1 -1
- package/dist/safeContent/index.cjs.map +1 -1
- package/dist/safeContent/index.js.map +1 -1
- package/dist/safeContent/parseSafeMdast.cjs.map +1 -1
- package/dist/safeContent/parseSafeMdast.js.map +1 -1
- package/dist/safeContent/renderMdast.cjs.map +1 -1
- package/dist/safeContent/renderMdast.js.map +1 -1
- package/dist/sandboxTypes.cjs.map +1 -1
- package/dist/sandboxUtils.cjs +69 -24
- package/dist/sandboxUtils.cjs.map +1 -1
- package/dist/sandboxUtils.d.cts +37 -4
- package/dist/sandboxUtils.d.ts +37 -4
- package/dist/sandboxUtils.js +74 -22
- package/dist/sandboxUtils.js.map +1 -1
- package/dist/scrollToId.cjs.map +1 -1
- package/dist/scrollToId.js.map +1 -1
- package/dist/secrets.cjs.map +1 -1
- package/dist/secrets.js.map +1 -1
- package/dist/tasks.cjs +12 -2
- package/dist/tasks.cjs.map +1 -1
- package/dist/tasks.js +12 -2
- package/dist/tasks.js.map +1 -1
- package/dist/testing.cjs +1 -3
- package/dist/testing.cjs.map +1 -1
- package/dist/testing.js +1 -3
- package/dist/testing.js.map +1 -1
- package/dist/theme.cjs.map +1 -1
- package/dist/theme.js.map +1 -1
- package/dist/urlUtils.cjs +7 -18
- package/dist/urlUtils.cjs.map +1 -1
- package/dist/urlUtils.js +7 -18
- package/dist/urlUtils.js.map +1 -1
- package/dist/vcs.cjs +1 -3
- package/dist/vcs.cjs.map +1 -1
- package/dist/vcs.js +1 -3
- package/dist/vcs.js.map +1 -1
- package/dist/version.cjs +1 -1
- package/dist/version.cjs.map +1 -1
- package/dist/version.d.cts +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/version.js.map +1 -1
- package/package.json +11 -5
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/components/defaults.tsx"],"sourcesContent":["import { ErrorBoundaryPropsWithRender } from 'react-error-boundary';\nimport { Spinner } from '../loading';\n\n/**\n * Default Suspense fallback for the SDK content loaders (Include / FileRouter /\n * MainContent).\n *\n * It used to be bare `<>loading...</>` text, which React paints unstyled in the\n * top-left on the iframe's white canvas — reading as a \"white flash with loading…\"\n * between the host's skeleton and the app (LOADING_UX_SPEC §9 / I3). Instead, a\n * calm, centred indicator: `Spinner` renders **nothing** for the first ~150ms (the\n * §6.2 spinner floor), so a fast wait shows no flash at all; a longer wait gets a\n * subtle centred spinner that inherits the app's own text colour. The host themes\n * the iframe canvas behind it (sandbox `themeCanvas.ts`), so there is no white.\n */\nexport const defaultLoadingComponent = () => (\n <div\n style={{\n display: 'flex',\n alignItems: 'center',\n justifyContent: 'center',\n width: '100%',\n height: '100%',\n minHeight: 48,\n }}\n >\n <Spinner />\n </div>\n);\n\nexport const defaultErrorComponent:ErrorBoundaryPropsWithRender[
|
|
1
|
+
{"version":3,"sources":["../../src/components/defaults.tsx"],"sourcesContent":["import { ErrorBoundaryPropsWithRender } from 'react-error-boundary';\nimport { Spinner } from '../loading';\n\n/**\n * Default Suspense fallback for the SDK content loaders (Include / FileRouter /\n * MainContent).\n *\n * It used to be bare `<>loading...</>` text, which React paints unstyled in the\n * top-left on the iframe's white canvas — reading as a \"white flash with loading…\"\n * between the host's skeleton and the app (LOADING_UX_SPEC §9 / I3). Instead, a\n * calm, centred indicator: `Spinner` renders **nothing** for the first ~150ms (the\n * §6.2 spinner floor), so a fast wait shows no flash at all; a longer wait gets a\n * subtle centred spinner that inherits the app's own text colour. The host themes\n * the iframe canvas behind it (sandbox `themeCanvas.ts`), so there is no white.\n */\nexport const defaultLoadingComponent = () => (\n <div\n style={{\n display: 'flex',\n alignItems: 'center',\n justifyContent: 'center',\n width: '100%',\n height: '100%',\n minHeight: 48,\n }}\n >\n <Spinner />\n </div>\n);\n\nexport const defaultErrorComponent: ErrorBoundaryPropsWithRender['fallbackRender'] = ({ error }) => (\n <>ERROR {String(error)}</>\n);\n"],"mappings":";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AA0BI;AAzBJ,qBAAwB;AAcjB,MAAM,0BAA0B,MACrC;AAAA,EAAC;AAAA;AAAA,IACC,OAAO;AAAA,MACL,SAAS;AAAA,MACT,YAAY;AAAA,MACZ,gBAAgB;AAAA,MAChB,OAAO;AAAA,MACP,QAAQ;AAAA,MACR,WAAW;AAAA,IACb;AAAA,IAEA,sDAAC,0BAAQ;AAAA;AACX;AAGK,MAAM,wBAAwE,CAAC,EAAE,MAAM,MAC5F,4EAAE;AAAA;AAAA,EAAO,OAAO,KAAK;AAAA,GAAE;","names":[]}
|
|
@@ -14,6 +14,6 @@ import { ErrorBoundaryPropsWithRender } from 'react-error-boundary';
|
|
|
14
14
|
* the iframe canvas behind it (sandbox `themeCanvas.ts`), so there is no white.
|
|
15
15
|
*/
|
|
16
16
|
declare const defaultLoadingComponent: () => react.JSX.Element;
|
|
17
|
-
declare const defaultErrorComponent: ErrorBoundaryPropsWithRender[
|
|
17
|
+
declare const defaultErrorComponent: ErrorBoundaryPropsWithRender['fallbackRender'];
|
|
18
18
|
|
|
19
19
|
export { defaultErrorComponent, defaultLoadingComponent };
|
|
@@ -14,6 +14,6 @@ import { ErrorBoundaryPropsWithRender } from 'react-error-boundary';
|
|
|
14
14
|
* the iframe canvas behind it (sandbox `themeCanvas.ts`), so there is no white.
|
|
15
15
|
*/
|
|
16
16
|
declare const defaultLoadingComponent: () => react.JSX.Element;
|
|
17
|
-
declare const defaultErrorComponent: ErrorBoundaryPropsWithRender[
|
|
17
|
+
declare const defaultErrorComponent: ErrorBoundaryPropsWithRender['fallbackRender'];
|
|
18
18
|
|
|
19
19
|
export { defaultErrorComponent, defaultLoadingComponent };
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/components/defaults.tsx"],"sourcesContent":["import { ErrorBoundaryPropsWithRender } from 'react-error-boundary';\nimport { Spinner } from '../loading';\n\n/**\n * Default Suspense fallback for the SDK content loaders (Include / FileRouter /\n * MainContent).\n *\n * It used to be bare `<>loading...</>` text, which React paints unstyled in the\n * top-left on the iframe's white canvas — reading as a \"white flash with loading…\"\n * between the host's skeleton and the app (LOADING_UX_SPEC §9 / I3). Instead, a\n * calm, centred indicator: `Spinner` renders **nothing** for the first ~150ms (the\n * §6.2 spinner floor), so a fast wait shows no flash at all; a longer wait gets a\n * subtle centred spinner that inherits the app's own text colour. The host themes\n * the iframe canvas behind it (sandbox `themeCanvas.ts`), so there is no white.\n */\nexport const defaultLoadingComponent = () => (\n <div\n style={{\n display: 'flex',\n alignItems: 'center',\n justifyContent: 'center',\n width: '100%',\n height: '100%',\n minHeight: 48,\n }}\n >\n <Spinner />\n </div>\n);\n\nexport const defaultErrorComponent:ErrorBoundaryPropsWithRender[
|
|
1
|
+
{"version":3,"sources":["../../src/components/defaults.tsx"],"sourcesContent":["import { ErrorBoundaryPropsWithRender } from 'react-error-boundary';\nimport { Spinner } from '../loading';\n\n/**\n * Default Suspense fallback for the SDK content loaders (Include / FileRouter /\n * MainContent).\n *\n * It used to be bare `<>loading...</>` text, which React paints unstyled in the\n * top-left on the iframe's white canvas — reading as a \"white flash with loading…\"\n * between the host's skeleton and the app (LOADING_UX_SPEC §9 / I3). Instead, a\n * calm, centred indicator: `Spinner` renders **nothing** for the first ~150ms (the\n * §6.2 spinner floor), so a fast wait shows no flash at all; a longer wait gets a\n * subtle centred spinner that inherits the app's own text colour. The host themes\n * the iframe canvas behind it (sandbox `themeCanvas.ts`), so there is no white.\n */\nexport const defaultLoadingComponent = () => (\n <div\n style={{\n display: 'flex',\n alignItems: 'center',\n justifyContent: 'center',\n width: '100%',\n height: '100%',\n minHeight: 48,\n }}\n >\n <Spinner />\n </div>\n);\n\nexport const defaultErrorComponent: ErrorBoundaryPropsWithRender['fallbackRender'] = ({ error }) => (\n <>ERROR {String(error)}</>\n);\n"],"mappings":";AA0BI,SAKF,UALE,KAKF,YALE;AAzBJ,SAAS,eAAe;AAcjB,MAAM,0BAA0B,MACrC;AAAA,EAAC;AAAA;AAAA,IACC,OAAO;AAAA,MACL,SAAS;AAAA,MACT,YAAY;AAAA,MACZ,gBAAgB;AAAA,MAChB,OAAO;AAAA,MACP,QAAQ;AAAA,MACR,WAAW;AAAA,IACb;AAAA,IAEA,8BAAC,WAAQ;AAAA;AACX;AAGK,MAAM,wBAAwE,CAAC,EAAE,MAAM,MAC5F,iCAAE;AAAA;AAAA,EAAO,OAAO,KAAK;AAAA,GAAE;","names":[]}
|
|
@@ -25,7 +25,9 @@ var import_jsx_runtime = require("react/jsx-runtime");
|
|
|
25
25
|
var import_react = require("react");
|
|
26
26
|
var import_TinkerableContext = require("../TinkerableContext");
|
|
27
27
|
const ErrorNotFound = () => {
|
|
28
|
-
const {
|
|
28
|
+
const {
|
|
29
|
+
navigationState: { pathParameters }
|
|
30
|
+
} = (0, import_react.useContext)(import_TinkerableContext.TinkerableContext);
|
|
29
31
|
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
|
|
30
32
|
"No route registered for path ",
|
|
31
33
|
pathParameters?.path ?? "(unknown)"
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/components/errors.tsx"],"sourcesContent":["import { useContext } from
|
|
1
|
+
{"version":3,"sources":["../../src/components/errors.tsx"],"sourcesContent":["import { useContext } from 'react';\nimport { TinkerableContext } from '../TinkerableContext';\n\nexport const ErrorNotFound = () => {\n const {\n navigationState: { pathParameters },\n } = useContext(TinkerableContext);\n return <>No route registered for path {pathParameters?.path ?? '(unknown)'}</>;\n};\n"],"mappings":";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAOS;AAPT,mBAA2B;AAC3B,+BAAkC;AAE3B,MAAM,gBAAgB,MAAM;AACjC,QAAM;AAAA,IACJ,iBAAiB,EAAE,eAAe;AAAA,EACpC,QAAI,yBAAW,0CAAiB;AAChC,SAAO,4EAAE;AAAA;AAAA,IAA8B,gBAAgB,QAAQ;AAAA,KAAY;AAC7E;","names":[]}
|
|
@@ -3,7 +3,9 @@ import { Fragment, jsxs } from "react/jsx-runtime";
|
|
|
3
3
|
import { useContext } from "react";
|
|
4
4
|
import { TinkerableContext } from "../TinkerableContext";
|
|
5
5
|
const ErrorNotFound = () => {
|
|
6
|
-
const {
|
|
6
|
+
const {
|
|
7
|
+
navigationState: { pathParameters }
|
|
8
|
+
} = useContext(TinkerableContext);
|
|
7
9
|
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
8
10
|
"No route registered for path ",
|
|
9
11
|
pathParameters?.path ?? "(unknown)"
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/components/errors.tsx"],"sourcesContent":["import { useContext } from
|
|
1
|
+
{"version":3,"sources":["../../src/components/errors.tsx"],"sourcesContent":["import { useContext } from 'react';\nimport { TinkerableContext } from '../TinkerableContext';\n\nexport const ErrorNotFound = () => {\n const {\n navigationState: { pathParameters },\n } = useContext(TinkerableContext);\n return <>No route registered for path {pathParameters?.path ?? '(unknown)'}</>;\n};\n"],"mappings":";AAOS;AAPT,SAAS,kBAAkB;AAC3B,SAAS,yBAAyB;AAE3B,MAAM,gBAAgB,MAAM;AACjC,QAAM;AAAA,IACJ,iBAAiB,EAAE,eAAe;AAAA,EACpC,IAAI,WAAW,iBAAiB;AAChC,SAAO,iCAAE;AAAA;AAAA,IAA8B,gBAAgB,QAAQ;AAAA,KAAY;AAC7E;","names":[]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/contextUtils.ts"],"sourcesContent":["import type { RoutingSpec } from './RoutingSpec';\nimport { type TinkerableState } from './TinkerableContext';\nimport { parseHref, getSearchParams } from './urlUtils';\nimport { FilesMetadata } from './sandboxTypes';\nimport { applyRoutingRule } from './routing';\nimport { getInjectedMetadataSnapshot } from './injectedBundler';\n\nexport const getContextFromUrl = (routingSpec: RoutingSpec
|
|
1
|
+
{"version":3,"sources":["../src/contextUtils.ts"],"sourcesContent":["import type { RoutingSpec } from './RoutingSpec';\nimport { type TinkerableState } from './TinkerableContext';\nimport { parseHref, getSearchParams } from './urlUtils';\nimport { FilesMetadata } from './sandboxTypes';\nimport { applyRoutingRule } from './routing';\nimport { getInjectedMetadataSnapshot } from './injectedBundler';\n\nexport const getContextFromUrl = (\n routingSpec: RoutingSpec,\n outerHref: string,\n filesMetadata?: FilesMetadata,\n): TinkerableState => {\n const navigationState = parseHref(outerHref);\n const appliedRoutingRule = applyRoutingRule(routingSpec, navigationState);\n if (!appliedRoutingRule) {\n // TODO: better error\n throw new Error(`No route registered for path ${navigationState.sandboxPath}!`);\n }\n return {\n filesMetadata: filesMetadata ?? {},\n routingSpec,\n outerHref,\n navigationState: {\n ...navigationState,\n ...appliedRoutingRule,\n },\n };\n};\n\nexport const getInitialContext = (routingSpec: RoutingSpec): (() => TinkerableState) => {\n const searchParams = getSearchParams();\n // initial href is passed in 'href' search param value; seed filesMetadata from the\n // injected bundler's boot snapshot (§1.4) so the first synchronous frame already\n // holds the full MDX collection (injected path). Off-injection the snapshot is null\n // → `{}`, and the store fills from `metadata-update` events as before (event-fill).\n return () => getContextFromUrl(routingSpec, searchParams['href'], getInjectedMetadataSnapshot() ?? undefined);\n};\n\nexport const updateContext = (context: TinkerableState, href: string): TinkerableState => {\n // No update is necessary if outerHref has not changed.\n if (href === context.outerHref) {\n return context;\n }\n return getContextFromUrl(context.routingSpec, href, context.filesMetadata);\n};\n"],"mappings":";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAEA,sBAA2C;AAE3C,qBAAiC;AACjC,6BAA4C;AAErC,MAAM,oBAAoB,CAC/B,aACA,WACA,kBACoB;AACpB,QAAM,sBAAkB,2BAAU,SAAS;AAC3C,QAAM,yBAAqB,iCAAiB,aAAa,eAAe;AACxE,MAAI,CAAC,oBAAoB;AAEvB,UAAM,IAAI,MAAM,gCAAgC,gBAAgB,WAAW,GAAG;AAAA,EAChF;AACA,SAAO;AAAA,IACL,eAAe,iBAAiB,CAAC;AAAA,IACjC;AAAA,IACA;AAAA,IACA,iBAAiB;AAAA,MACf,GAAG;AAAA,MACH,GAAG;AAAA,IACL;AAAA,EACF;AACF;AAEO,MAAM,oBAAoB,CAAC,gBAAsD;AACtF,QAAM,mBAAe,iCAAgB;AAKrC,SAAO,MAAM,kBAAkB,aAAa,aAAa,MAAM,OAAG,oDAA4B,KAAK,MAAS;AAC9G;AAEO,MAAM,gBAAgB,CAAC,SAA0B,SAAkC;AAExF,MAAI,SAAS,QAAQ,WAAW;AAC9B,WAAO;AAAA,EACT;AACA,SAAO,kBAAkB,QAAQ,aAAa,MAAM,QAAQ,aAAa;AAC3E;","names":[]}
|
package/dist/contextUtils.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/contextUtils.ts"],"sourcesContent":["import type { RoutingSpec } from './RoutingSpec';\nimport { type TinkerableState } from './TinkerableContext';\nimport { parseHref, getSearchParams } from './urlUtils';\nimport { FilesMetadata } from './sandboxTypes';\nimport { applyRoutingRule } from './routing';\nimport { getInjectedMetadataSnapshot } from './injectedBundler';\n\nexport const getContextFromUrl = (routingSpec: RoutingSpec
|
|
1
|
+
{"version":3,"sources":["../src/contextUtils.ts"],"sourcesContent":["import type { RoutingSpec } from './RoutingSpec';\nimport { type TinkerableState } from './TinkerableContext';\nimport { parseHref, getSearchParams } from './urlUtils';\nimport { FilesMetadata } from './sandboxTypes';\nimport { applyRoutingRule } from './routing';\nimport { getInjectedMetadataSnapshot } from './injectedBundler';\n\nexport const getContextFromUrl = (\n routingSpec: RoutingSpec,\n outerHref: string,\n filesMetadata?: FilesMetadata,\n): TinkerableState => {\n const navigationState = parseHref(outerHref);\n const appliedRoutingRule = applyRoutingRule(routingSpec, navigationState);\n if (!appliedRoutingRule) {\n // TODO: better error\n throw new Error(`No route registered for path ${navigationState.sandboxPath}!`);\n }\n return {\n filesMetadata: filesMetadata ?? {},\n routingSpec,\n outerHref,\n navigationState: {\n ...navigationState,\n ...appliedRoutingRule,\n },\n };\n};\n\nexport const getInitialContext = (routingSpec: RoutingSpec): (() => TinkerableState) => {\n const searchParams = getSearchParams();\n // initial href is passed in 'href' search param value; seed filesMetadata from the\n // injected bundler's boot snapshot (§1.4) so the first synchronous frame already\n // holds the full MDX collection (injected path). Off-injection the snapshot is null\n // → `{}`, and the store fills from `metadata-update` events as before (event-fill).\n return () => getContextFromUrl(routingSpec, searchParams['href'], getInjectedMetadataSnapshot() ?? undefined);\n};\n\nexport const updateContext = (context: TinkerableState, href: string): TinkerableState => {\n // No update is necessary if outerHref has not changed.\n if (href === context.outerHref) {\n return context;\n }\n return getContextFromUrl(context.routingSpec, href, context.filesMetadata);\n};\n"],"mappings":";AAEA,SAAS,WAAW,uBAAuB;AAE3C,SAAS,wBAAwB;AACjC,SAAS,mCAAmC;AAErC,MAAM,oBAAoB,CAC/B,aACA,WACA,kBACoB;AACpB,QAAM,kBAAkB,UAAU,SAAS;AAC3C,QAAM,qBAAqB,iBAAiB,aAAa,eAAe;AACxE,MAAI,CAAC,oBAAoB;AAEvB,UAAM,IAAI,MAAM,gCAAgC,gBAAgB,WAAW,GAAG;AAAA,EAChF;AACA,SAAO;AAAA,IACL,eAAe,iBAAiB,CAAC;AAAA,IACjC;AAAA,IACA;AAAA,IACA,iBAAiB;AAAA,MACf,GAAG;AAAA,MACH,GAAG;AAAA,IACL;AAAA,EACF;AACF;AAEO,MAAM,oBAAoB,CAAC,gBAAsD;AACtF,QAAM,eAAe,gBAAgB;AAKrC,SAAO,MAAM,kBAAkB,aAAa,aAAa,MAAM,GAAG,4BAA4B,KAAK,MAAS;AAC9G;AAEO,MAAM,gBAAgB,CAAC,SAA0B,SAAkC;AAExF,MAAI,SAAS,QAAQ,WAAW;AAC9B,WAAO;AAAA,EACT;AACA,SAAO,kBAAkB,QAAQ,aAAa,MAAM,QAAQ,aAAa;AAC3E;","names":[]}
|
package/dist/contribute.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/contribute.ts"],"sourcesContent":["// Contribute — stream a save (PR or direct commit) from app code\n// (UI_AS_APPS_SPEC §5.1, CONTRIBUTE_SPEC). The host runs the *existing*\n// contribution orchestrator and streams its stages back; this is the typed\n// front door over `protocolStream`.\n//\n// The OAuth token never reaches here — it stays on the host. The app passes a\n// commit message + save mode and watches stages go by; the host owns the token,\n// the GitHub API calls, and the user-facing consent.\nimport { protocolStream } from './protocolStream';\nimport { PROTOCOL_CONTRIBUTE } from './generated/protocol';\n\n/** The save strategy. `direct` requires the first-party `contribute:direct`\n * capability and a scarier consent line — a `contribute:any` app asking for it\n * is REJECTED (`forbidden`), never silently downgraded to a PR (threat T11). */\nexport type ContributeMode = 'pr' | 'direct';\n\n/** A stage emitted as the contribution runs. Mirrors the host orchestrator's\n * event union; carries progress metadata only — never the token or file blobs. */\nexport type ContributionEvent =\n | { stage: 'auth-check' }\n | { stage: 'diff-compute' }\n | { stage: 'permission-check' }\n | { stage: 'install-required'; targetOwner: string; targetRepo: string; installUrl: string }\n | { stage: 'conflict-check' }\n | { stage: 'fork-prepare'; forkOwner: string; alreadyExists: boolean }\n | { stage: 'upload-blob'; path: string; index: number; total: number }\n | { stage: 'create-tree' }\n | { stage: 'create-commit' }\n | { stage: 'create-branch'; branchName: string }\n | { stage: 'create-pr' }\n | { stage: 'pr-updated'; prNumber: number; prUrl: string; commitSha: string }\n | { stage: 'commit-pushed'; ref: string; commitSha: string }\n | { stage: 'switch-branch'; provider: 'github'; pushOwner: string; repository: string; branchName: string }\n | { stage: 'done'; prUrl?: string; prNumber?: number; commitSha: string }\n | { stage: 'warning'; message: string; details?: unknown }\n | { stage: 'error'; message: string; recoverable: boolean };\n\n/** The settled outcome (the stream's return value). */\nexport interface ContributionResult {\n prUrl?: string;\n prNumber?: number;\n commitSha: string;\n treeSha: string;\n branchName: string;\n mode: 'direct-commit' | 'new-branch-pr' | 'extend-existing';\n}\n\n/** Options for a contribution: the commit message, save {@link ContributeMode},\n * and (PR mode) an optional branch name. */\nexport interface ContributeOptions {\n /** The commit message / PR title. */\n commitMessage: string;\n /** `'pr'` (default) opens a PR; `'direct'` commits to the branch and needs\n * the first-party `contribute:direct` capability. */\n mode?: ContributeMode;\n /** Override the generated branch name (PR mode). */\n branchName?: string;\n}\n\n/**\n * Save the current working tree, streaming each stage.\n *\n * ```ts\n * for await (const ev of contribute({ commitMessage: 'Edit post' })) {\n * if (ev.stage === 'done') console.log(ev.prUrl);\n * }\n * ```\n *\n * Yields {@link ContributionEvent}s and returns a {@link ContributionResult}.\n * Throws a `StreamError` (`.code`) if the host rejects the request — notably\n * `forbidden` when a `contribute:any` app asks for `mode: 'direct'` (T11).\n */\nexport function contribute(
|
|
1
|
+
{"version":3,"sources":["../src/contribute.ts"],"sourcesContent":["// Contribute — stream a save (PR or direct commit) from app code\n// (UI_AS_APPS_SPEC §5.1, CONTRIBUTE_SPEC). The host runs the *existing*\n// contribution orchestrator and streams its stages back; this is the typed\n// front door over `protocolStream`.\n//\n// The OAuth token never reaches here — it stays on the host. The app passes a\n// commit message + save mode and watches stages go by; the host owns the token,\n// the GitHub API calls, and the user-facing consent.\nimport { protocolStream } from './protocolStream';\nimport { PROTOCOL_CONTRIBUTE } from './generated/protocol';\n\n/** The save strategy. `direct` requires the first-party `contribute:direct`\n * capability and a scarier consent line — a `contribute:any` app asking for it\n * is REJECTED (`forbidden`), never silently downgraded to a PR (threat T11). */\nexport type ContributeMode = 'pr' | 'direct';\n\n/** A stage emitted as the contribution runs. Mirrors the host orchestrator's\n * event union; carries progress metadata only — never the token or file blobs. */\nexport type ContributionEvent =\n | { stage: 'auth-check' }\n | { stage: 'diff-compute' }\n | { stage: 'permission-check' }\n | { stage: 'install-required'; targetOwner: string; targetRepo: string; installUrl: string }\n | { stage: 'conflict-check' }\n | { stage: 'fork-prepare'; forkOwner: string; alreadyExists: boolean }\n | { stage: 'upload-blob'; path: string; index: number; total: number }\n | { stage: 'create-tree' }\n | { stage: 'create-commit' }\n | { stage: 'create-branch'; branchName: string }\n | { stage: 'create-pr' }\n | { stage: 'pr-updated'; prNumber: number; prUrl: string; commitSha: string }\n | { stage: 'commit-pushed'; ref: string; commitSha: string }\n | { stage: 'switch-branch'; provider: 'github'; pushOwner: string; repository: string; branchName: string }\n | { stage: 'done'; prUrl?: string; prNumber?: number; commitSha: string }\n | { stage: 'warning'; message: string; details?: unknown }\n | { stage: 'error'; message: string; recoverable: boolean };\n\n/** The settled outcome (the stream's return value). */\nexport interface ContributionResult {\n prUrl?: string;\n prNumber?: number;\n commitSha: string;\n treeSha: string;\n branchName: string;\n mode: 'direct-commit' | 'new-branch-pr' | 'extend-existing';\n}\n\n/** Options for a contribution: the commit message, save {@link ContributeMode},\n * and (PR mode) an optional branch name. */\nexport interface ContributeOptions {\n /** The commit message / PR title. */\n commitMessage: string;\n /** `'pr'` (default) opens a PR; `'direct'` commits to the branch and needs\n * the first-party `contribute:direct` capability. */\n mode?: ContributeMode;\n /** Override the generated branch name (PR mode). */\n branchName?: string;\n}\n\n/**\n * Save the current working tree, streaming each stage.\n *\n * ```ts\n * for await (const ev of contribute({ commitMessage: 'Edit post' })) {\n * if (ev.stage === 'done') console.log(ev.prUrl);\n * }\n * ```\n *\n * Yields {@link ContributionEvent}s and returns a {@link ContributionResult}.\n * Throws a `StreamError` (`.code`) if the host rejects the request — notably\n * `forbidden` when a `contribute:any` app asks for `mode: 'direct'` (T11).\n */\nexport function contribute(opts: ContributeOptions): AsyncGenerator<ContributionEvent, ContributionResult, void> {\n return protocolStream<ContributionEvent, ContributionResult>(PROTOCOL_CONTRIBUTE, 'run', [opts]);\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAQA,4BAA+B;AAC/B,sBAAoC;AA+D7B,SAAS,WAAW,MAAsF;AAC/G,aAAO,sCAAsD,qCAAqB,OAAO,CAAC,IAAI,CAAC;AACjG;","names":[]}
|
package/dist/contribute.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/contribute.ts"],"sourcesContent":["// Contribute — stream a save (PR or direct commit) from app code\n// (UI_AS_APPS_SPEC §5.1, CONTRIBUTE_SPEC). The host runs the *existing*\n// contribution orchestrator and streams its stages back; this is the typed\n// front door over `protocolStream`.\n//\n// The OAuth token never reaches here — it stays on the host. The app passes a\n// commit message + save mode and watches stages go by; the host owns the token,\n// the GitHub API calls, and the user-facing consent.\nimport { protocolStream } from './protocolStream';\nimport { PROTOCOL_CONTRIBUTE } from './generated/protocol';\n\n/** The save strategy. `direct` requires the first-party `contribute:direct`\n * capability and a scarier consent line — a `contribute:any` app asking for it\n * is REJECTED (`forbidden`), never silently downgraded to a PR (threat T11). */\nexport type ContributeMode = 'pr' | 'direct';\n\n/** A stage emitted as the contribution runs. Mirrors the host orchestrator's\n * event union; carries progress metadata only — never the token or file blobs. */\nexport type ContributionEvent =\n | { stage: 'auth-check' }\n | { stage: 'diff-compute' }\n | { stage: 'permission-check' }\n | { stage: 'install-required'; targetOwner: string; targetRepo: string; installUrl: string }\n | { stage: 'conflict-check' }\n | { stage: 'fork-prepare'; forkOwner: string; alreadyExists: boolean }\n | { stage: 'upload-blob'; path: string; index: number; total: number }\n | { stage: 'create-tree' }\n | { stage: 'create-commit' }\n | { stage: 'create-branch'; branchName: string }\n | { stage: 'create-pr' }\n | { stage: 'pr-updated'; prNumber: number; prUrl: string; commitSha: string }\n | { stage: 'commit-pushed'; ref: string; commitSha: string }\n | { stage: 'switch-branch'; provider: 'github'; pushOwner: string; repository: string; branchName: string }\n | { stage: 'done'; prUrl?: string; prNumber?: number; commitSha: string }\n | { stage: 'warning'; message: string; details?: unknown }\n | { stage: 'error'; message: string; recoverable: boolean };\n\n/** The settled outcome (the stream's return value). */\nexport interface ContributionResult {\n prUrl?: string;\n prNumber?: number;\n commitSha: string;\n treeSha: string;\n branchName: string;\n mode: 'direct-commit' | 'new-branch-pr' | 'extend-existing';\n}\n\n/** Options for a contribution: the commit message, save {@link ContributeMode},\n * and (PR mode) an optional branch name. */\nexport interface ContributeOptions {\n /** The commit message / PR title. */\n commitMessage: string;\n /** `'pr'` (default) opens a PR; `'direct'` commits to the branch and needs\n * the first-party `contribute:direct` capability. */\n mode?: ContributeMode;\n /** Override the generated branch name (PR mode). */\n branchName?: string;\n}\n\n/**\n * Save the current working tree, streaming each stage.\n *\n * ```ts\n * for await (const ev of contribute({ commitMessage: 'Edit post' })) {\n * if (ev.stage === 'done') console.log(ev.prUrl);\n * }\n * ```\n *\n * Yields {@link ContributionEvent}s and returns a {@link ContributionResult}.\n * Throws a `StreamError` (`.code`) if the host rejects the request — notably\n * `forbidden` when a `contribute:any` app asks for `mode: 'direct'` (T11).\n */\nexport function contribute(
|
|
1
|
+
{"version":3,"sources":["../src/contribute.ts"],"sourcesContent":["// Contribute — stream a save (PR or direct commit) from app code\n// (UI_AS_APPS_SPEC §5.1, CONTRIBUTE_SPEC). The host runs the *existing*\n// contribution orchestrator and streams its stages back; this is the typed\n// front door over `protocolStream`.\n//\n// The OAuth token never reaches here — it stays on the host. The app passes a\n// commit message + save mode and watches stages go by; the host owns the token,\n// the GitHub API calls, and the user-facing consent.\nimport { protocolStream } from './protocolStream';\nimport { PROTOCOL_CONTRIBUTE } from './generated/protocol';\n\n/** The save strategy. `direct` requires the first-party `contribute:direct`\n * capability and a scarier consent line — a `contribute:any` app asking for it\n * is REJECTED (`forbidden`), never silently downgraded to a PR (threat T11). */\nexport type ContributeMode = 'pr' | 'direct';\n\n/** A stage emitted as the contribution runs. Mirrors the host orchestrator's\n * event union; carries progress metadata only — never the token or file blobs. */\nexport type ContributionEvent =\n | { stage: 'auth-check' }\n | { stage: 'diff-compute' }\n | { stage: 'permission-check' }\n | { stage: 'install-required'; targetOwner: string; targetRepo: string; installUrl: string }\n | { stage: 'conflict-check' }\n | { stage: 'fork-prepare'; forkOwner: string; alreadyExists: boolean }\n | { stage: 'upload-blob'; path: string; index: number; total: number }\n | { stage: 'create-tree' }\n | { stage: 'create-commit' }\n | { stage: 'create-branch'; branchName: string }\n | { stage: 'create-pr' }\n | { stage: 'pr-updated'; prNumber: number; prUrl: string; commitSha: string }\n | { stage: 'commit-pushed'; ref: string; commitSha: string }\n | { stage: 'switch-branch'; provider: 'github'; pushOwner: string; repository: string; branchName: string }\n | { stage: 'done'; prUrl?: string; prNumber?: number; commitSha: string }\n | { stage: 'warning'; message: string; details?: unknown }\n | { stage: 'error'; message: string; recoverable: boolean };\n\n/** The settled outcome (the stream's return value). */\nexport interface ContributionResult {\n prUrl?: string;\n prNumber?: number;\n commitSha: string;\n treeSha: string;\n branchName: string;\n mode: 'direct-commit' | 'new-branch-pr' | 'extend-existing';\n}\n\n/** Options for a contribution: the commit message, save {@link ContributeMode},\n * and (PR mode) an optional branch name. */\nexport interface ContributeOptions {\n /** The commit message / PR title. */\n commitMessage: string;\n /** `'pr'` (default) opens a PR; `'direct'` commits to the branch and needs\n * the first-party `contribute:direct` capability. */\n mode?: ContributeMode;\n /** Override the generated branch name (PR mode). */\n branchName?: string;\n}\n\n/**\n * Save the current working tree, streaming each stage.\n *\n * ```ts\n * for await (const ev of contribute({ commitMessage: 'Edit post' })) {\n * if (ev.stage === 'done') console.log(ev.prUrl);\n * }\n * ```\n *\n * Yields {@link ContributionEvent}s and returns a {@link ContributionResult}.\n * Throws a `StreamError` (`.code`) if the host rejects the request — notably\n * `forbidden` when a `contribute:any` app asks for `mode: 'direct'` (T11).\n */\nexport function contribute(opts: ContributeOptions): AsyncGenerator<ContributionEvent, ContributionResult, void> {\n return protocolStream<ContributionEvent, ContributionResult>(PROTOCOL_CONTRIBUTE, 'run', [opts]);\n}\n"],"mappings":";AAQA,SAAS,sBAAsB;AAC/B,SAAS,2BAA2B;AA+D7B,SAAS,WAAW,MAAsF;AAC/G,SAAO,eAAsD,qBAAqB,OAAO,CAAC,IAAI,CAAC;AACjG;","names":[]}
|
package/dist/debug.cjs
CHANGED
|
@@ -108,7 +108,8 @@ function computedStyle(params) {
|
|
|
108
108
|
if (!el) return null;
|
|
109
109
|
const cs = getComputedStyle(el);
|
|
110
110
|
const out = {};
|
|
111
|
-
for (const p of params.props.slice(0, 50))
|
|
111
|
+
for (const p of params.props.slice(0, 50))
|
|
112
|
+
out[p] = cs.getPropertyValue(p) || cs[p]?.toString?.() || "";
|
|
112
113
|
return out;
|
|
113
114
|
}
|
|
114
115
|
function rects(params) {
|
package/dist/debug.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/debug.ts"],"sourcesContent":["// System-app devtools — the app-facing surface (plan: docs/plans/system-app-devtools.md).\n//\n// Two opt-in, DEV-ONLY instruments for debugging a sandboxed UI-as-app region:\n// 1. `debug.log(...)` — an app→host one-way log surfaced in the host dev panel\n// / CLI `/debug` stream (instead of hand-fishing console output out of a\n// cross-origin iframe's devtools).\n// 2. a READ-ONLY DOM/layout responder the host can query from outside — the\n// thing a cross-origin screenshot can't reliably give you (a blank capture\n// is ambiguous between a real 0-height collapse and a paint artifact).\n//\n// SECURITY (the gating constraint — see the plan's §0):\n// - Both are inert unless the HOST signals dev mode via the `debug-enabled`\n// channel. The host only sets it for a dev/override session (the `ir-dev-*`\n// deep link) or an explicit operator developer-mode. A published app served\n// to a normal user gets `enabled:false` → `debug.log` is a no-op and the\n// responder never answers. Production isolation is therefore unchanged.\n// - The responder is READ-ONLY with a fixed vocabulary (snapshotDom /\n// computedStyle / rect). There is deliberately NO eval bridge — that would\n// turn a debug aid into remote code execution into the sandbox.\n// - The responder reads only its OWN `document` (it lives in its own opaque\n// iframe and cannot reach a sibling app), so there is no app↔app leak even\n// in dev.\n// - Output is bounded (node/depth/text caps) so a query can't exfiltrate an\n// unbounded payload or wedge the app.\n//\n// Apps that want the strongest guarantee can additionally guard their own usage\n// behind `import.meta.env.DEV` so the calls are tree-shaken from prod bundles;\n// the runtime gate here is the backstop that holds regardless.\n\nimport { createPushChannel } from './pushChannel';\nimport { sendMessage, addListener } from './sandboxUtils';\nimport {\n DEBUG_ENABLED,\n DEBUG_LOG,\n DEBUG_QUERY,\n DEBUG_QUERY_RESULT,\n REQUEST_DEBUG_ENABLED,\n} from './generated/protocol';\n\n/** Severity of a {@link debug.log} entry. */\nexport type DebugLevel = 'debug' | 'info' | 'warn' | 'error';\n\n// ── Dev gate ────────────────────────────────────────────────────────────────\n// The host pushes `debug-enabled:true` only for a dev/override session. Until\n// then (and always in production) it stays false and every instrument is inert.\nconst enabledChannel = createPushChannel<boolean>({\n pushType: DEBUG_ENABLED,\n requestType: REQUEST_DEBUG_ENABLED,\n initial: false,\n parse: (msg) => (typeof msg.enabled === 'boolean' ? msg.enabled : undefined),\n});\n\n/** Is the host dev-debug surface active for this session? `false` in production. */\nexport const isDebugEnabled = (): boolean => enabledChannel.get();\n\n/** React hook: whether the host dev-debug surface is active (re-renders on change).\n * Handy for showing a debug affordance only when it would do something. */\nexport const useDebugEnabled = (): boolean => enabledChannel.use();\n\n// ── 1. App→host debug log ─────────────────────────────────────────────────────\n// Best-effort: a value that can't be structured-cloned is replaced with a marker\n// rather than throwing — `debug.log` must never break the app.\nconst MAX_DATA_BYTES = 16 * 1024;\n\nfunction safeData(data: unknown): unknown {\n if (data === undefined) return undefined;\n try {\n const json = JSON.stringify(data);\n if (json === undefined) return '[unserializable]';\n if (json.length > MAX_DATA_BYTES) return `[truncated ${json.length}B]`;\n return JSON.parse(json);\n } catch {\n return '[unserializable]';\n }\n}\n\n/**\n * Emit a structured debug entry to the host dev surface. A NO-OP unless the host\n * has enabled the dev-debug session ({@link isDebugEnabled}); in production it\n * does nothing and sends nothing.\n *\n * debug.log('info', 'mounted', { activeFile });\n */\nexport function log(level: DebugLevel, message: string, data?: unknown): void {\n if (!enabledChannel.get()) return; // inert in prod / non-dev sessions\n try {\n sendMessage(DEBUG_LOG, { level, message: String(message), data: safeData(data) });\n } catch {\n /* transport not ready — drop silently; logging must never throw */\n }\n}\n\n// ── 2. Read-only DOM / layout responder ───────────────────────────────────────\n// The host sends `debug-query` { id, method, params }; we reply with\n// `debug-query-result` { id, ok, result | error }. Only ever active while the dev\n// gate is enabled. Vocabulary is fixed and read-only.\n\ninterface DomNode {\n tag: string;\n id?: string;\n classes?: string[];\n attrs?: Record<string, string>;\n rect?: { x: number; y: number; w: number; h: number };\n text?: string;\n children?: DomNode[];\n truncated?: true;\n}\n\nconst ATTR_ALLOW = new Set(['role', 'aria-hidden', 'data-theme', 'data-active', 'href', 'type', 'hidden']);\nconst MAX_NODES = 2000;\nconst MAX_DEPTH = 25;\nconst MAX_TEXT = 200;\n\nfunction round(n: number): number {\n return Math.round(n);\n}\n\nfunction snapshotDom(params: { selector?: string; maxDepth?: number; maxNodes?: number }): DomNode | null {\n if (typeof document === 'undefined') return null;\n const root = params.selector ? document.querySelector(params.selector) : document.body;\n if (!root) return null;\n const maxDepth = Math.min(params.maxDepth ?? MAX_DEPTH, MAX_DEPTH);\n const maxNodes = Math.min(params.maxNodes ?? MAX_NODES, MAX_NODES);\n let budget = maxNodes;\n\n const walk = (el: Element, depth: number): DomNode => {\n budget--;\n const r = el.getBoundingClientRect();\n const classes = el.classList.length ? [...el.classList] : undefined;\n const attrs: Record<string, string> = {};\n for (const name of el.getAttributeNames()) {\n if (ATTR_ALLOW.has(name)) attrs[name] = el.getAttribute(name) ?? '';\n }\n // Direct text (not descendants') so a leaf's label is visible without dumping\n // the whole subtree's text.\n const ownText = [...el.childNodes]\n .filter((n) => n.nodeType === 3)\n .map((n) => (n.textContent ?? '').trim())\n .join(' ')\n .trim();\n const node: DomNode = {\n tag: el.tagName.toLowerCase(),\n ...(el.id ? { id: el.id } : {}),\n ...(classes ? { classes } : {}),\n ...(Object.keys(attrs).length ? { attrs } : {}),\n rect: { x: round(r.x), y: round(r.y), w: round(r.width), h: round(r.height) },\n ...(ownText ? { text: ownText.slice(0, MAX_TEXT) } : {}),\n };\n if (depth < maxDepth && el.children.length && budget > 0) {\n const children: DomNode[] = [];\n for (const child of el.children) {\n if (budget <= 0) {\n node.truncated = true;\n break;\n }\n children.push(walk(child, depth + 1));\n }\n if (children.length) node.children = children;\n } else if (el.children.length) {\n node.truncated = true;\n }\n return node;\n };\n\n return walk(root, 0);\n}\n\nfunction computedStyle(params: { selector: string; props: string[] }): Record<string, string> | null {\n if (typeof document === 'undefined') return null;\n const el = document.querySelector(params.selector);\n if (!el) return null;\n const cs = getComputedStyle(el);\n const out: Record<string, string> = {};\n for (const p of params.props.slice(0, 50)) out[p] = cs.getPropertyValue(p) || cs[p as keyof CSSStyleDeclaration]?.toString?.() || '';\n return out;\n}\n\nfunction rects(params: { selector: string }): Array<{ x: number; y: number; w: number; h: number }> {\n if (typeof document === 'undefined') return [];\n return [...document.querySelectorAll(params.selector)].slice(0, 200).map((el) => {\n const r = el.getBoundingClientRect();\n return { x: round(r.x), y: round(r.y), w: round(r.width), h: round(r.height) };\n });\n}\n\nlet responderStarted = false;\n\n/** Wire the read-only DOM/layout responder. Idempotent; called lazily once the\n * dev gate turns on. No effect when `document` is absent (non-browser realm). */\nfunction startResponder(): void {\n if (responderStarted || typeof window === 'undefined') return;\n responderStarted = true;\n addListener(DEBUG_QUERY, (msg: { id?: unknown; method?: unknown; params?: unknown }) => {\n if (!enabledChannel.get()) return; // gate: ignore unless dev-enabled\n const id = msg.id;\n const method = msg.method;\n const params = (msg.params ?? {}) as Record<string, unknown>;\n let ok = true;\n let result: unknown = null;\n let error: string | undefined;\n try {\n switch (method) {\n case 'snapshotDom':\n result = snapshotDom(params as never);\n break;\n case 'computedStyle':\n result = computedStyle(params as never);\n break;\n case 'rect':\n result = rects(params as never);\n break;\n default:\n ok = false;\n error = `unknown debug method: ${String(method)}`;\n }\n } catch (e) {\n ok = false;\n error = e instanceof Error ? e.message : String(e);\n }\n try {\n sendMessage(DEBUG_QUERY_RESULT, { id, ok, result, error });\n } catch {\n /* transport gone — nothing to do */\n }\n });\n}\n\n// Start the responder as soon as the gate flips on (and not before).\nenabledChannel.onChange((enabled) => {\n if (enabled) startResponder();\n});\n\n/** The dev-only debug surface. Inert unless the host enables it ({@link isDebugEnabled}). */\nexport const debug = { log, isEnabled: isDebugEnabled } as const;\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/debug.ts"],"sourcesContent":["// System-app devtools — the app-facing surface (plan: docs/plans/system-app-devtools.md).\n//\n// Two opt-in, DEV-ONLY instruments for debugging a sandboxed UI-as-app region:\n// 1. `debug.log(...)` — an app→host one-way log surfaced in the host dev panel\n// / CLI `/debug` stream (instead of hand-fishing console output out of a\n// cross-origin iframe's devtools).\n// 2. a READ-ONLY DOM/layout responder the host can query from outside — the\n// thing a cross-origin screenshot can't reliably give you (a blank capture\n// is ambiguous between a real 0-height collapse and a paint artifact).\n//\n// SECURITY (the gating constraint — see the plan's §0):\n// - Both are inert unless the HOST signals dev mode via the `debug-enabled`\n// channel. The host only sets it for a dev/override session (the `ir-dev-*`\n// deep link) or an explicit operator developer-mode. A published app served\n// to a normal user gets `enabled:false` → `debug.log` is a no-op and the\n// responder never answers. Production isolation is therefore unchanged.\n// - The responder is READ-ONLY with a fixed vocabulary (snapshotDom /\n// computedStyle / rect). There is deliberately NO eval bridge — that would\n// turn a debug aid into remote code execution into the sandbox.\n// - The responder reads only its OWN `document` (it lives in its own opaque\n// iframe and cannot reach a sibling app), so there is no app↔app leak even\n// in dev.\n// - Output is bounded (node/depth/text caps) so a query can't exfiltrate an\n// unbounded payload or wedge the app.\n//\n// Apps that want the strongest guarantee can additionally guard their own usage\n// behind `import.meta.env.DEV` so the calls are tree-shaken from prod bundles;\n// the runtime gate here is the backstop that holds regardless.\n\nimport { createPushChannel } from './pushChannel';\nimport { sendMessage, addListener } from './sandboxUtils';\nimport { DEBUG_ENABLED, DEBUG_LOG, DEBUG_QUERY, DEBUG_QUERY_RESULT, REQUEST_DEBUG_ENABLED } from './generated/protocol';\n\n/** Severity of a {@link debug.log} entry. */\nexport type DebugLevel = 'debug' | 'info' | 'warn' | 'error';\n\n// ── Dev gate ────────────────────────────────────────────────────────────────\n// The host pushes `debug-enabled:true` only for a dev/override session. Until\n// then (and always in production) it stays false and every instrument is inert.\nconst enabledChannel = createPushChannel<boolean>({\n pushType: DEBUG_ENABLED,\n requestType: REQUEST_DEBUG_ENABLED,\n initial: false,\n parse: (msg) => (typeof msg.enabled === 'boolean' ? msg.enabled : undefined),\n});\n\n/** Is the host dev-debug surface active for this session? `false` in production. */\nexport const isDebugEnabled = (): boolean => enabledChannel.get();\n\n/** React hook: whether the host dev-debug surface is active (re-renders on change).\n * Handy for showing a debug affordance only when it would do something. */\nexport const useDebugEnabled = (): boolean => enabledChannel.use();\n\n// ── 1. App→host debug log ─────────────────────────────────────────────────────\n// Best-effort: a value that can't be structured-cloned is replaced with a marker\n// rather than throwing — `debug.log` must never break the app.\nconst MAX_DATA_BYTES = 16 * 1024;\n\nfunction safeData(data: unknown): unknown {\n if (data === undefined) return undefined;\n try {\n const json = JSON.stringify(data);\n if (json === undefined) return '[unserializable]';\n if (json.length > MAX_DATA_BYTES) return `[truncated ${json.length}B]`;\n return JSON.parse(json);\n } catch {\n return '[unserializable]';\n }\n}\n\n/**\n * Emit a structured debug entry to the host dev surface. A NO-OP unless the host\n * has enabled the dev-debug session ({@link isDebugEnabled}); in production it\n * does nothing and sends nothing.\n *\n * debug.log('info', 'mounted', { activeFile });\n */\nexport function log(level: DebugLevel, message: string, data?: unknown): void {\n if (!enabledChannel.get()) return; // inert in prod / non-dev sessions\n try {\n sendMessage(DEBUG_LOG, { level, message: String(message), data: safeData(data) });\n } catch {\n /* transport not ready — drop silently; logging must never throw */\n }\n}\n\n// ── 2. Read-only DOM / layout responder ───────────────────────────────────────\n// The host sends `debug-query` { id, method, params }; we reply with\n// `debug-query-result` { id, ok, result | error }. Only ever active while the dev\n// gate is enabled. Vocabulary is fixed and read-only.\n\ninterface DomNode {\n tag: string;\n id?: string;\n classes?: string[];\n attrs?: Record<string, string>;\n rect?: { x: number; y: number; w: number; h: number };\n text?: string;\n children?: DomNode[];\n truncated?: true;\n}\n\nconst ATTR_ALLOW = new Set(['role', 'aria-hidden', 'data-theme', 'data-active', 'href', 'type', 'hidden']);\nconst MAX_NODES = 2000;\nconst MAX_DEPTH = 25;\nconst MAX_TEXT = 200;\n\nfunction round(n: number): number {\n return Math.round(n);\n}\n\nfunction snapshotDom(params: { selector?: string; maxDepth?: number; maxNodes?: number }): DomNode | null {\n if (typeof document === 'undefined') return null;\n const root = params.selector ? document.querySelector(params.selector) : document.body;\n if (!root) return null;\n const maxDepth = Math.min(params.maxDepth ?? MAX_DEPTH, MAX_DEPTH);\n const maxNodes = Math.min(params.maxNodes ?? MAX_NODES, MAX_NODES);\n let budget = maxNodes;\n\n const walk = (el: Element, depth: number): DomNode => {\n budget--;\n const r = el.getBoundingClientRect();\n const classes = el.classList.length ? [...el.classList] : undefined;\n const attrs: Record<string, string> = {};\n for (const name of el.getAttributeNames()) {\n if (ATTR_ALLOW.has(name)) attrs[name] = el.getAttribute(name) ?? '';\n }\n // Direct text (not descendants') so a leaf's label is visible without dumping\n // the whole subtree's text.\n const ownText = [...el.childNodes]\n .filter((n) => n.nodeType === 3)\n .map((n) => (n.textContent ?? '').trim())\n .join(' ')\n .trim();\n const node: DomNode = {\n tag: el.tagName.toLowerCase(),\n ...(el.id ? { id: el.id } : {}),\n ...(classes ? { classes } : {}),\n ...(Object.keys(attrs).length ? { attrs } : {}),\n rect: { x: round(r.x), y: round(r.y), w: round(r.width), h: round(r.height) },\n ...(ownText ? { text: ownText.slice(0, MAX_TEXT) } : {}),\n };\n if (depth < maxDepth && el.children.length && budget > 0) {\n const children: DomNode[] = [];\n for (const child of el.children) {\n if (budget <= 0) {\n node.truncated = true;\n break;\n }\n children.push(walk(child, depth + 1));\n }\n if (children.length) node.children = children;\n } else if (el.children.length) {\n node.truncated = true;\n }\n return node;\n };\n\n return walk(root, 0);\n}\n\nfunction computedStyle(params: { selector: string; props: string[] }): Record<string, string> | null {\n if (typeof document === 'undefined') return null;\n const el = document.querySelector(params.selector);\n if (!el) return null;\n const cs = getComputedStyle(el);\n const out: Record<string, string> = {};\n for (const p of params.props.slice(0, 50))\n out[p] = cs.getPropertyValue(p) || cs[p as keyof CSSStyleDeclaration]?.toString?.() || '';\n return out;\n}\n\nfunction rects(params: { selector: string }): Array<{ x: number; y: number; w: number; h: number }> {\n if (typeof document === 'undefined') return [];\n return [...document.querySelectorAll(params.selector)].slice(0, 200).map((el) => {\n const r = el.getBoundingClientRect();\n return { x: round(r.x), y: round(r.y), w: round(r.width), h: round(r.height) };\n });\n}\n\nlet responderStarted = false;\n\n/** Wire the read-only DOM/layout responder. Idempotent; called lazily once the\n * dev gate turns on. No effect when `document` is absent (non-browser realm). */\nfunction startResponder(): void {\n if (responderStarted || typeof window === 'undefined') return;\n responderStarted = true;\n addListener(DEBUG_QUERY, (msg: { id?: unknown; method?: unknown; params?: unknown }) => {\n if (!enabledChannel.get()) return; // gate: ignore unless dev-enabled\n const id = msg.id;\n const method = msg.method;\n const params = (msg.params ?? {}) as Record<string, unknown>;\n let ok = true;\n let result: unknown = null;\n let error: string | undefined;\n try {\n switch (method) {\n case 'snapshotDom':\n result = snapshotDom(params as never);\n break;\n case 'computedStyle':\n result = computedStyle(params as never);\n break;\n case 'rect':\n result = rects(params as never);\n break;\n default:\n ok = false;\n error = `unknown debug method: ${String(method)}`;\n }\n } catch (e) {\n ok = false;\n error = e instanceof Error ? e.message : String(e);\n }\n try {\n sendMessage(DEBUG_QUERY_RESULT, { id, ok, result, error });\n } catch {\n /* transport gone — nothing to do */\n }\n });\n}\n\n// Start the responder as soon as the gate flips on (and not before).\nenabledChannel.onChange((enabled) => {\n if (enabled) startResponder();\n});\n\n/** The dev-only debug surface. Inert unless the host enables it ({@link isDebugEnabled}). */\nexport const debug = { log, isEnabled: isDebugEnabled } as const;\n"],"mappings":"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","names":[]}
|
package/dist/debug.js
CHANGED
|
@@ -1,13 +1,7 @@
|
|
|
1
1
|
import "./chunk-VHAA22YE.js";
|
|
2
2
|
import { createPushChannel } from "./pushChannel";
|
|
3
3
|
import { sendMessage, addListener } from "./sandboxUtils";
|
|
4
|
-
import {
|
|
5
|
-
DEBUG_ENABLED,
|
|
6
|
-
DEBUG_LOG,
|
|
7
|
-
DEBUG_QUERY,
|
|
8
|
-
DEBUG_QUERY_RESULT,
|
|
9
|
-
REQUEST_DEBUG_ENABLED
|
|
10
|
-
} from "./generated/protocol";
|
|
4
|
+
import { DEBUG_ENABLED, DEBUG_LOG, DEBUG_QUERY, DEBUG_QUERY_RESULT, REQUEST_DEBUG_ENABLED } from "./generated/protocol";
|
|
11
5
|
const enabledChannel = createPushChannel({
|
|
12
6
|
pushType: DEBUG_ENABLED,
|
|
13
7
|
requestType: REQUEST_DEBUG_ENABLED,
|
|
@@ -89,7 +83,8 @@ function computedStyle(params) {
|
|
|
89
83
|
if (!el) return null;
|
|
90
84
|
const cs = getComputedStyle(el);
|
|
91
85
|
const out = {};
|
|
92
|
-
for (const p of params.props.slice(0, 50))
|
|
86
|
+
for (const p of params.props.slice(0, 50))
|
|
87
|
+
out[p] = cs.getPropertyValue(p) || cs[p]?.toString?.() || "";
|
|
93
88
|
return out;
|
|
94
89
|
}
|
|
95
90
|
function rects(params) {
|
package/dist/debug.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/debug.ts"],"sourcesContent":["// System-app devtools — the app-facing surface (plan: docs/plans/system-app-devtools.md).\n//\n// Two opt-in, DEV-ONLY instruments for debugging a sandboxed UI-as-app region:\n// 1. `debug.log(...)` — an app→host one-way log surfaced in the host dev panel\n// / CLI `/debug` stream (instead of hand-fishing console output out of a\n// cross-origin iframe's devtools).\n// 2. a READ-ONLY DOM/layout responder the host can query from outside — the\n// thing a cross-origin screenshot can't reliably give you (a blank capture\n// is ambiguous between a real 0-height collapse and a paint artifact).\n//\n// SECURITY (the gating constraint — see the plan's §0):\n// - Both are inert unless the HOST signals dev mode via the `debug-enabled`\n// channel. The host only sets it for a dev/override session (the `ir-dev-*`\n// deep link) or an explicit operator developer-mode. A published app served\n// to a normal user gets `enabled:false` → `debug.log` is a no-op and the\n// responder never answers. Production isolation is therefore unchanged.\n// - The responder is READ-ONLY with a fixed vocabulary (snapshotDom /\n// computedStyle / rect). There is deliberately NO eval bridge — that would\n// turn a debug aid into remote code execution into the sandbox.\n// - The responder reads only its OWN `document` (it lives in its own opaque\n// iframe and cannot reach a sibling app), so there is no app↔app leak even\n// in dev.\n// - Output is bounded (node/depth/text caps) so a query can't exfiltrate an\n// unbounded payload or wedge the app.\n//\n// Apps that want the strongest guarantee can additionally guard their own usage\n// behind `import.meta.env.DEV` so the calls are tree-shaken from prod bundles;\n// the runtime gate here is the backstop that holds regardless.\n\nimport { createPushChannel } from './pushChannel';\nimport { sendMessage, addListener } from './sandboxUtils';\nimport {\n DEBUG_ENABLED,\n DEBUG_LOG,\n DEBUG_QUERY,\n DEBUG_QUERY_RESULT,\n REQUEST_DEBUG_ENABLED,\n} from './generated/protocol';\n\n/** Severity of a {@link debug.log} entry. */\nexport type DebugLevel = 'debug' | 'info' | 'warn' | 'error';\n\n// ── Dev gate ────────────────────────────────────────────────────────────────\n// The host pushes `debug-enabled:true` only for a dev/override session. Until\n// then (and always in production) it stays false and every instrument is inert.\nconst enabledChannel = createPushChannel<boolean>({\n pushType: DEBUG_ENABLED,\n requestType: REQUEST_DEBUG_ENABLED,\n initial: false,\n parse: (msg) => (typeof msg.enabled === 'boolean' ? msg.enabled : undefined),\n});\n\n/** Is the host dev-debug surface active for this session? `false` in production. */\nexport const isDebugEnabled = (): boolean => enabledChannel.get();\n\n/** React hook: whether the host dev-debug surface is active (re-renders on change).\n * Handy for showing a debug affordance only when it would do something. */\nexport const useDebugEnabled = (): boolean => enabledChannel.use();\n\n// ── 1. App→host debug log ─────────────────────────────────────────────────────\n// Best-effort: a value that can't be structured-cloned is replaced with a marker\n// rather than throwing — `debug.log` must never break the app.\nconst MAX_DATA_BYTES = 16 * 1024;\n\nfunction safeData(data: unknown): unknown {\n if (data === undefined) return undefined;\n try {\n const json = JSON.stringify(data);\n if (json === undefined) return '[unserializable]';\n if (json.length > MAX_DATA_BYTES) return `[truncated ${json.length}B]`;\n return JSON.parse(json);\n } catch {\n return '[unserializable]';\n }\n}\n\n/**\n * Emit a structured debug entry to the host dev surface. A NO-OP unless the host\n * has enabled the dev-debug session ({@link isDebugEnabled}); in production it\n * does nothing and sends nothing.\n *\n * debug.log('info', 'mounted', { activeFile });\n */\nexport function log(level: DebugLevel, message: string, data?: unknown): void {\n if (!enabledChannel.get()) return; // inert in prod / non-dev sessions\n try {\n sendMessage(DEBUG_LOG, { level, message: String(message), data: safeData(data) });\n } catch {\n /* transport not ready — drop silently; logging must never throw */\n }\n}\n\n// ── 2. Read-only DOM / layout responder ───────────────────────────────────────\n// The host sends `debug-query` { id, method, params }; we reply with\n// `debug-query-result` { id, ok, result | error }. Only ever active while the dev\n// gate is enabled. Vocabulary is fixed and read-only.\n\ninterface DomNode {\n tag: string;\n id?: string;\n classes?: string[];\n attrs?: Record<string, string>;\n rect?: { x: number; y: number; w: number; h: number };\n text?: string;\n children?: DomNode[];\n truncated?: true;\n}\n\nconst ATTR_ALLOW = new Set(['role', 'aria-hidden', 'data-theme', 'data-active', 'href', 'type', 'hidden']);\nconst MAX_NODES = 2000;\nconst MAX_DEPTH = 25;\nconst MAX_TEXT = 200;\n\nfunction round(n: number): number {\n return Math.round(n);\n}\n\nfunction snapshotDom(params: { selector?: string; maxDepth?: number; maxNodes?: number }): DomNode | null {\n if (typeof document === 'undefined') return null;\n const root = params.selector ? document.querySelector(params.selector) : document.body;\n if (!root) return null;\n const maxDepth = Math.min(params.maxDepth ?? MAX_DEPTH, MAX_DEPTH);\n const maxNodes = Math.min(params.maxNodes ?? MAX_NODES, MAX_NODES);\n let budget = maxNodes;\n\n const walk = (el: Element, depth: number): DomNode => {\n budget--;\n const r = el.getBoundingClientRect();\n const classes = el.classList.length ? [...el.classList] : undefined;\n const attrs: Record<string, string> = {};\n for (const name of el.getAttributeNames()) {\n if (ATTR_ALLOW.has(name)) attrs[name] = el.getAttribute(name) ?? '';\n }\n // Direct text (not descendants') so a leaf's label is visible without dumping\n // the whole subtree's text.\n const ownText = [...el.childNodes]\n .filter((n) => n.nodeType === 3)\n .map((n) => (n.textContent ?? '').trim())\n .join(' ')\n .trim();\n const node: DomNode = {\n tag: el.tagName.toLowerCase(),\n ...(el.id ? { id: el.id } : {}),\n ...(classes ? { classes } : {}),\n ...(Object.keys(attrs).length ? { attrs } : {}),\n rect: { x: round(r.x), y: round(r.y), w: round(r.width), h: round(r.height) },\n ...(ownText ? { text: ownText.slice(0, MAX_TEXT) } : {}),\n };\n if (depth < maxDepth && el.children.length && budget > 0) {\n const children: DomNode[] = [];\n for (const child of el.children) {\n if (budget <= 0) {\n node.truncated = true;\n break;\n }\n children.push(walk(child, depth + 1));\n }\n if (children.length) node.children = children;\n } else if (el.children.length) {\n node.truncated = true;\n }\n return node;\n };\n\n return walk(root, 0);\n}\n\nfunction computedStyle(params: { selector: string; props: string[] }): Record<string, string> | null {\n if (typeof document === 'undefined') return null;\n const el = document.querySelector(params.selector);\n if (!el) return null;\n const cs = getComputedStyle(el);\n const out: Record<string, string> = {};\n for (const p of params.props.slice(0, 50)) out[p] = cs.getPropertyValue(p) || cs[p as keyof CSSStyleDeclaration]?.toString?.() || '';\n return out;\n}\n\nfunction rects(params: { selector: string }): Array<{ x: number; y: number; w: number; h: number }> {\n if (typeof document === 'undefined') return [];\n return [...document.querySelectorAll(params.selector)].slice(0, 200).map((el) => {\n const r = el.getBoundingClientRect();\n return { x: round(r.x), y: round(r.y), w: round(r.width), h: round(r.height) };\n });\n}\n\nlet responderStarted = false;\n\n/** Wire the read-only DOM/layout responder. Idempotent; called lazily once the\n * dev gate turns on. No effect when `document` is absent (non-browser realm). */\nfunction startResponder(): void {\n if (responderStarted || typeof window === 'undefined') return;\n responderStarted = true;\n addListener(DEBUG_QUERY, (msg: { id?: unknown; method?: unknown; params?: unknown }) => {\n if (!enabledChannel.get()) return; // gate: ignore unless dev-enabled\n const id = msg.id;\n const method = msg.method;\n const params = (msg.params ?? {}) as Record<string, unknown>;\n let ok = true;\n let result: unknown = null;\n let error: string | undefined;\n try {\n switch (method) {\n case 'snapshotDom':\n result = snapshotDom(params as never);\n break;\n case 'computedStyle':\n result = computedStyle(params as never);\n break;\n case 'rect':\n result = rects(params as never);\n break;\n default:\n ok = false;\n error = `unknown debug method: ${String(method)}`;\n }\n } catch (e) {\n ok = false;\n error = e instanceof Error ? e.message : String(e);\n }\n try {\n sendMessage(DEBUG_QUERY_RESULT, { id, ok, result, error });\n } catch {\n /* transport gone — nothing to do */\n }\n });\n}\n\n// Start the responder as soon as the gate flips on (and not before).\nenabledChannel.onChange((enabled) => {\n if (enabled) startResponder();\n});\n\n/** The dev-only debug surface. Inert unless the host enables it ({@link isDebugEnabled}). */\nexport const debug = { log, isEnabled: isDebugEnabled } as const;\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/debug.ts"],"sourcesContent":["// System-app devtools — the app-facing surface (plan: docs/plans/system-app-devtools.md).\n//\n// Two opt-in, DEV-ONLY instruments for debugging a sandboxed UI-as-app region:\n// 1. `debug.log(...)` — an app→host one-way log surfaced in the host dev panel\n// / CLI `/debug` stream (instead of hand-fishing console output out of a\n// cross-origin iframe's devtools).\n// 2. a READ-ONLY DOM/layout responder the host can query from outside — the\n// thing a cross-origin screenshot can't reliably give you (a blank capture\n// is ambiguous between a real 0-height collapse and a paint artifact).\n//\n// SECURITY (the gating constraint — see the plan's §0):\n// - Both are inert unless the HOST signals dev mode via the `debug-enabled`\n// channel. The host only sets it for a dev/override session (the `ir-dev-*`\n// deep link) or an explicit operator developer-mode. A published app served\n// to a normal user gets `enabled:false` → `debug.log` is a no-op and the\n// responder never answers. Production isolation is therefore unchanged.\n// - The responder is READ-ONLY with a fixed vocabulary (snapshotDom /\n// computedStyle / rect). There is deliberately NO eval bridge — that would\n// turn a debug aid into remote code execution into the sandbox.\n// - The responder reads only its OWN `document` (it lives in its own opaque\n// iframe and cannot reach a sibling app), so there is no app↔app leak even\n// in dev.\n// - Output is bounded (node/depth/text caps) so a query can't exfiltrate an\n// unbounded payload or wedge the app.\n//\n// Apps that want the strongest guarantee can additionally guard their own usage\n// behind `import.meta.env.DEV` so the calls are tree-shaken from prod bundles;\n// the runtime gate here is the backstop that holds regardless.\n\nimport { createPushChannel } from './pushChannel';\nimport { sendMessage, addListener } from './sandboxUtils';\nimport { DEBUG_ENABLED, DEBUG_LOG, DEBUG_QUERY, DEBUG_QUERY_RESULT, REQUEST_DEBUG_ENABLED } from './generated/protocol';\n\n/** Severity of a {@link debug.log} entry. */\nexport type DebugLevel = 'debug' | 'info' | 'warn' | 'error';\n\n// ── Dev gate ────────────────────────────────────────────────────────────────\n// The host pushes `debug-enabled:true` only for a dev/override session. Until\n// then (and always in production) it stays false and every instrument is inert.\nconst enabledChannel = createPushChannel<boolean>({\n pushType: DEBUG_ENABLED,\n requestType: REQUEST_DEBUG_ENABLED,\n initial: false,\n parse: (msg) => (typeof msg.enabled === 'boolean' ? msg.enabled : undefined),\n});\n\n/** Is the host dev-debug surface active for this session? `false` in production. */\nexport const isDebugEnabled = (): boolean => enabledChannel.get();\n\n/** React hook: whether the host dev-debug surface is active (re-renders on change).\n * Handy for showing a debug affordance only when it would do something. */\nexport const useDebugEnabled = (): boolean => enabledChannel.use();\n\n// ── 1. App→host debug log ─────────────────────────────────────────────────────\n// Best-effort: a value that can't be structured-cloned is replaced with a marker\n// rather than throwing — `debug.log` must never break the app.\nconst MAX_DATA_BYTES = 16 * 1024;\n\nfunction safeData(data: unknown): unknown {\n if (data === undefined) return undefined;\n try {\n const json = JSON.stringify(data);\n if (json === undefined) return '[unserializable]';\n if (json.length > MAX_DATA_BYTES) return `[truncated ${json.length}B]`;\n return JSON.parse(json);\n } catch {\n return '[unserializable]';\n }\n}\n\n/**\n * Emit a structured debug entry to the host dev surface. A NO-OP unless the host\n * has enabled the dev-debug session ({@link isDebugEnabled}); in production it\n * does nothing and sends nothing.\n *\n * debug.log('info', 'mounted', { activeFile });\n */\nexport function log(level: DebugLevel, message: string, data?: unknown): void {\n if (!enabledChannel.get()) return; // inert in prod / non-dev sessions\n try {\n sendMessage(DEBUG_LOG, { level, message: String(message), data: safeData(data) });\n } catch {\n /* transport not ready — drop silently; logging must never throw */\n }\n}\n\n// ── 2. Read-only DOM / layout responder ───────────────────────────────────────\n// The host sends `debug-query` { id, method, params }; we reply with\n// `debug-query-result` { id, ok, result | error }. Only ever active while the dev\n// gate is enabled. Vocabulary is fixed and read-only.\n\ninterface DomNode {\n tag: string;\n id?: string;\n classes?: string[];\n attrs?: Record<string, string>;\n rect?: { x: number; y: number; w: number; h: number };\n text?: string;\n children?: DomNode[];\n truncated?: true;\n}\n\nconst ATTR_ALLOW = new Set(['role', 'aria-hidden', 'data-theme', 'data-active', 'href', 'type', 'hidden']);\nconst MAX_NODES = 2000;\nconst MAX_DEPTH = 25;\nconst MAX_TEXT = 200;\n\nfunction round(n: number): number {\n return Math.round(n);\n}\n\nfunction snapshotDom(params: { selector?: string; maxDepth?: number; maxNodes?: number }): DomNode | null {\n if (typeof document === 'undefined') return null;\n const root = params.selector ? document.querySelector(params.selector) : document.body;\n if (!root) return null;\n const maxDepth = Math.min(params.maxDepth ?? MAX_DEPTH, MAX_DEPTH);\n const maxNodes = Math.min(params.maxNodes ?? MAX_NODES, MAX_NODES);\n let budget = maxNodes;\n\n const walk = (el: Element, depth: number): DomNode => {\n budget--;\n const r = el.getBoundingClientRect();\n const classes = el.classList.length ? [...el.classList] : undefined;\n const attrs: Record<string, string> = {};\n for (const name of el.getAttributeNames()) {\n if (ATTR_ALLOW.has(name)) attrs[name] = el.getAttribute(name) ?? '';\n }\n // Direct text (not descendants') so a leaf's label is visible without dumping\n // the whole subtree's text.\n const ownText = [...el.childNodes]\n .filter((n) => n.nodeType === 3)\n .map((n) => (n.textContent ?? '').trim())\n .join(' ')\n .trim();\n const node: DomNode = {\n tag: el.tagName.toLowerCase(),\n ...(el.id ? { id: el.id } : {}),\n ...(classes ? { classes } : {}),\n ...(Object.keys(attrs).length ? { attrs } : {}),\n rect: { x: round(r.x), y: round(r.y), w: round(r.width), h: round(r.height) },\n ...(ownText ? { text: ownText.slice(0, MAX_TEXT) } : {}),\n };\n if (depth < maxDepth && el.children.length && budget > 0) {\n const children: DomNode[] = [];\n for (const child of el.children) {\n if (budget <= 0) {\n node.truncated = true;\n break;\n }\n children.push(walk(child, depth + 1));\n }\n if (children.length) node.children = children;\n } else if (el.children.length) {\n node.truncated = true;\n }\n return node;\n };\n\n return walk(root, 0);\n}\n\nfunction computedStyle(params: { selector: string; props: string[] }): Record<string, string> | null {\n if (typeof document === 'undefined') return null;\n const el = document.querySelector(params.selector);\n if (!el) return null;\n const cs = getComputedStyle(el);\n const out: Record<string, string> = {};\n for (const p of params.props.slice(0, 50))\n out[p] = cs.getPropertyValue(p) || cs[p as keyof CSSStyleDeclaration]?.toString?.() || '';\n return out;\n}\n\nfunction rects(params: { selector: string }): Array<{ x: number; y: number; w: number; h: number }> {\n if (typeof document === 'undefined') return [];\n return [...document.querySelectorAll(params.selector)].slice(0, 200).map((el) => {\n const r = el.getBoundingClientRect();\n return { x: round(r.x), y: round(r.y), w: round(r.width), h: round(r.height) };\n });\n}\n\nlet responderStarted = false;\n\n/** Wire the read-only DOM/layout responder. Idempotent; called lazily once the\n * dev gate turns on. No effect when `document` is absent (non-browser realm). */\nfunction startResponder(): void {\n if (responderStarted || typeof window === 'undefined') return;\n responderStarted = true;\n addListener(DEBUG_QUERY, (msg: { id?: unknown; method?: unknown; params?: unknown }) => {\n if (!enabledChannel.get()) return; // gate: ignore unless dev-enabled\n const id = msg.id;\n const method = msg.method;\n const params = (msg.params ?? {}) as Record<string, unknown>;\n let ok = true;\n let result: unknown = null;\n let error: string | undefined;\n try {\n switch (method) {\n case 'snapshotDom':\n result = snapshotDom(params as never);\n break;\n case 'computedStyle':\n result = computedStyle(params as never);\n break;\n case 'rect':\n result = rects(params as never);\n break;\n default:\n ok = false;\n error = `unknown debug method: ${String(method)}`;\n }\n } catch (e) {\n ok = false;\n error = e instanceof Error ? e.message : String(e);\n }\n try {\n sendMessage(DEBUG_QUERY_RESULT, { id, ok, result, error });\n } catch {\n /* transport gone — nothing to do */\n }\n });\n}\n\n// Start the responder as soon as the gate flips on (and not before).\nenabledChannel.onChange((enabled) => {\n if (enabled) startResponder();\n});\n\n/** The dev-only debug surface. Inert unless the host enables it ({@link isDebugEnabled}). */\nexport const debug = { log, isEnabled: isDebugEnabled } as const;\n"],"mappings":";AA6BA,SAAS,yBAAyB;AAClC,SAAS,aAAa,mBAAmB;AACzC,SAAS,eAAe,WAAW,aAAa,oBAAoB,6BAA6B;AAQjG,MAAM,iBAAiB,kBAA2B;AAAA,EAChD,UAAU;AAAA,EACV,aAAa;AAAA,EACb,SAAS;AAAA,EACT,OAAO,CAAC,QAAS,OAAO,IAAI,YAAY,YAAY,IAAI,UAAU;AACpE,CAAC;AAGM,MAAM,iBAAiB,MAAe,eAAe,IAAI;AAIzD,MAAM,kBAAkB,MAAe,eAAe,IAAI;AAKjE,MAAM,iBAAiB,KAAK;AAE5B,SAAS,SAAS,MAAwB;AACxC,MAAI,SAAS,OAAW,QAAO;AAC/B,MAAI;AACF,UAAM,OAAO,KAAK,UAAU,IAAI;AAChC,QAAI,SAAS,OAAW,QAAO;AAC/B,QAAI,KAAK,SAAS,eAAgB,QAAO,cAAc,KAAK,MAAM;AAClE,WAAO,KAAK,MAAM,IAAI;AAAA,EACxB,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AASO,SAAS,IAAI,OAAmB,SAAiB,MAAsB;AAC5E,MAAI,CAAC,eAAe,IAAI,EAAG;AAC3B,MAAI;AACF,gBAAY,WAAW,EAAE,OAAO,SAAS,OAAO,OAAO,GAAG,MAAM,SAAS,IAAI,EAAE,CAAC;AAAA,EAClF,QAAQ;AAAA,EAER;AACF;AAkBA,MAAM,aAAa,oBAAI,IAAI,CAAC,QAAQ,eAAe,cAAc,eAAe,QAAQ,QAAQ,QAAQ,CAAC;AACzG,MAAM,YAAY;AAClB,MAAM,YAAY;AAClB,MAAM,WAAW;AAEjB,SAAS,MAAM,GAAmB;AAChC,SAAO,KAAK,MAAM,CAAC;AACrB;AAEA,SAAS,YAAY,QAAqF;AACxG,MAAI,OAAO,aAAa,YAAa,QAAO;AAC5C,QAAM,OAAO,OAAO,WAAW,SAAS,cAAc,OAAO,QAAQ,IAAI,SAAS;AAClF,MAAI,CAAC,KAAM,QAAO;AAClB,QAAM,WAAW,KAAK,IAAI,OAAO,YAAY,WAAW,SAAS;AACjE,QAAM,WAAW,KAAK,IAAI,OAAO,YAAY,WAAW,SAAS;AACjE,MAAI,SAAS;AAEb,QAAM,OAAO,CAAC,IAAa,UAA2B;AACpD;AACA,UAAM,IAAI,GAAG,sBAAsB;AACnC,UAAM,UAAU,GAAG,UAAU,SAAS,CAAC,GAAG,GAAG,SAAS,IAAI;AAC1D,UAAM,QAAgC,CAAC;AACvC,eAAW,QAAQ,GAAG,kBAAkB,GAAG;AACzC,UAAI,WAAW,IAAI,IAAI,EAAG,OAAM,IAAI,IAAI,GAAG,aAAa,IAAI,KAAK;AAAA,IACnE;AAGA,UAAM,UAAU,CAAC,GAAG,GAAG,UAAU,EAC9B,OAAO,CAAC,MAAM,EAAE,aAAa,CAAC,EAC9B,IAAI,CAAC,OAAO,EAAE,eAAe,IAAI,KAAK,CAAC,EACvC,KAAK,GAAG,EACR,KAAK;AACR,UAAM,OAAgB;AAAA,MACpB,KAAK,GAAG,QAAQ,YAAY;AAAA,MAC5B,GAAI,GAAG,KAAK,EAAE,IAAI,GAAG,GAAG,IAAI,CAAC;AAAA,MAC7B,GAAI,UAAU,EAAE,QAAQ,IAAI,CAAC;AAAA,MAC7B,GAAI,OAAO,KAAK,KAAK,EAAE,SAAS,EAAE,MAAM,IAAI,CAAC;AAAA,MAC7C,MAAM,EAAE,GAAG,MAAM,EAAE,CAAC,GAAG,GAAG,MAAM,EAAE,CAAC,GAAG,GAAG,MAAM,EAAE,KAAK,GAAG,GAAG,MAAM,EAAE,MAAM,EAAE;AAAA,MAC5E,GAAI,UAAU,EAAE,MAAM,QAAQ,MAAM,GAAG,QAAQ,EAAE,IAAI,CAAC;AAAA,IACxD;AACA,QAAI,QAAQ,YAAY,GAAG,SAAS,UAAU,SAAS,GAAG;AACxD,YAAM,WAAsB,CAAC;AAC7B,iBAAW,SAAS,GAAG,UAAU;AAC/B,YAAI,UAAU,GAAG;AACf,eAAK,YAAY;AACjB;AAAA,QACF;AACA,iBAAS,KAAK,KAAK,OAAO,QAAQ,CAAC,CAAC;AAAA,MACtC;AACA,UAAI,SAAS,OAAQ,MAAK,WAAW;AAAA,IACvC,WAAW,GAAG,SAAS,QAAQ;AAC7B,WAAK,YAAY;AAAA,IACnB;AACA,WAAO;AAAA,EACT;AAEA,SAAO,KAAK,MAAM,CAAC;AACrB;AAEA,SAAS,cAAc,QAA8E;AACnG,MAAI,OAAO,aAAa,YAAa,QAAO;AAC5C,QAAM,KAAK,SAAS,cAAc,OAAO,QAAQ;AACjD,MAAI,CAAC,GAAI,QAAO;AAChB,QAAM,KAAK,iBAAiB,EAAE;AAC9B,QAAM,MAA8B,CAAC;AACrC,aAAW,KAAK,OAAO,MAAM,MAAM,GAAG,EAAE;AACtC,QAAI,CAAC,IAAI,GAAG,iBAAiB,CAAC,KAAK,GAAG,CAA8B,GAAG,WAAW,KAAK;AACzF,SAAO;AACT;AAEA,SAAS,MAAM,QAAqF;AAClG,MAAI,OAAO,aAAa,YAAa,QAAO,CAAC;AAC7C,SAAO,CAAC,GAAG,SAAS,iBAAiB,OAAO,QAAQ,CAAC,EAAE,MAAM,GAAG,GAAG,EAAE,IAAI,CAAC,OAAO;AAC/E,UAAM,IAAI,GAAG,sBAAsB;AACnC,WAAO,EAAE,GAAG,MAAM,EAAE,CAAC,GAAG,GAAG,MAAM,EAAE,CAAC,GAAG,GAAG,MAAM,EAAE,KAAK,GAAG,GAAG,MAAM,EAAE,MAAM,EAAE;AAAA,EAC/E,CAAC;AACH;AAEA,IAAI,mBAAmB;AAIvB,SAAS,iBAAuB;AAC9B,MAAI,oBAAoB,OAAO,WAAW,YAAa;AACvD,qBAAmB;AACnB,cAAY,aAAa,CAAC,QAA8D;AACtF,QAAI,CAAC,eAAe,IAAI,EAAG;AAC3B,UAAM,KAAK,IAAI;AACf,UAAM,SAAS,IAAI;AACnB,UAAM,SAAU,IAAI,UAAU,CAAC;AAC/B,QAAI,KAAK;AACT,QAAI,SAAkB;AACtB,QAAI;AACJ,QAAI;AACF,cAAQ,QAAQ;AAAA,QACd,KAAK;AACH,mBAAS,YAAY,MAAe;AACpC;AAAA,QACF,KAAK;AACH,mBAAS,cAAc,MAAe;AACtC;AAAA,QACF,KAAK;AACH,mBAAS,MAAM,MAAe;AAC9B;AAAA,QACF;AACE,eAAK;AACL,kBAAQ,yBAAyB,OAAO,MAAM,CAAC;AAAA,MACnD;AAAA,IACF,SAAS,GAAG;AACV,WAAK;AACL,cAAQ,aAAa,QAAQ,EAAE,UAAU,OAAO,CAAC;AAAA,IACnD;AACA,QAAI;AACF,kBAAY,oBAAoB,EAAE,IAAI,IAAI,QAAQ,MAAM,CAAC;AAAA,IAC3D,QAAQ;AAAA,IAER;AAAA,EACF,CAAC;AACH;AAGA,eAAe,SAAS,CAAC,YAAY;AACnC,MAAI,QAAS,gBAAe;AAC9B,CAAC;AAGM,MAAM,QAAQ,EAAE,KAAK,WAAW,eAAe;","names":[]}
|
package/dist/diagnostics.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/diagnostics.ts"],"sourcesContent":["// The `diagnostics:read` channel — app-facing surface (LLM_AND_AGENTS_SPEC §3.3/§4,\n// D4; roadmap R3-74 / P3-72).\n//\n// A *sibling* agent app (one holding `diagnostics:read`) observes the result of its\n// edits to the PREVIEWED app: the build/transpile errors from the sandbox bundler\n// and the previewed app's captured `console.*`. This is the in-browser analogue of\n// a local coding agent reading compiler/test output (P3-73's `get_diagnostics()`\n// tool). Read-only and scoped host-side to the paired previewed app's OWN\n// diagnostics — never another app's (the host channel projection enforces it; the\n// previewed app itself holds no `diagnostics:read`, so it is origin-excluded).\n//\n// Recipe-A push channel, identical get/onChange/use trio as `secrets`/`catalog`:\n// the host pushes `diagnostics` on change and answers a `request-diagnostics` poll,\n// gated per-frame by the read ACL. Inert until the host wires the channel\n// (site-main `channelBridge`); the contract ships here so the agent app (P3-73) can\n// be written against it.\nimport { createPushChannel } from './pushChannel';\nimport { DIAGNOSTICS, REQUEST_DIAGNOSTICS } from './generated/protocol';\n\n/** One build/transpile error from the sandbox bundler's compile of the previewed\n * app. `path` is repo-relative (leading slash) when the error is file-located. */\nexport interface BuildError {\n message: string;\n path?: string;\n line?: number;\n column?: number;\n}\n\n/** The `console.*` method a captured entry came from. */\nexport type ConsoleLevel = 'log' | 'info' | 'warn' | 'error' | 'debug';\n\n/** One captured `console.*` entry from the previewed app. The host renders the\n * console arguments to text host-side — the agent never receives live object\n * handles across the boundary. */\nexport interface ConsoleEntry {\n level: ConsoleLevel;\n text: string;\n /** Host-side timestamp (ms) at capture. */\n at: number;\n}\n\n/** Provenance (D4 / EDITOR_AS_APP_SPEC §12.3): WHICH previewed app + compile this\n * snapshot describes, so a consumer can tell stale output from fresh and never\n * conflates two apps' diagnostics. `null` until a first compile is observed. */\nexport interface DiagnosticsProvenance {\n /** The previewed app's stable key (`provider/ns/repo`). */\n appKey?: string;\n /** Monotonic id of the compile that produced these build errors. */\n compileId?: string;\n}\n\n/** A snapshot of the previewed app's diagnostics: its build errors, captured\n * console output, and the {@link DiagnosticsProvenance} of the compile. */\nexport interface Diagnostics {\n buildErrors: BuildError[];\n consoleEntries: ConsoleEntry[];\n provenance: DiagnosticsProvenance | null;\n}\n\n/** Value before the host answers — also the value when the app may not read the\n * channel (no `diagnostics:read`): an empty, provenance-less snapshot. */\nconst EMPTY: Diagnostics = { buildErrors: [], consoleEntries: [], provenance: null };\n\nconst channel = createPushChannel<Diagnostics>({\n pushType: DIAGNOSTICS,\n requestType: REQUEST_DIAGNOSTICS,\n initial: EMPTY,\n parse: (msg) => {\n // Require both arrays; tolerate an absent/partial provenance. A malformed push\n // is ignored (returns undefined) so the last good snapshot stands.\n if (!Array.isArray(msg.buildErrors) || !Array.isArray(msg.consoleEntries)) return undefined;\n const provenance =\n msg.provenance && typeof msg.provenance === 'object'
|
|
1
|
+
{"version":3,"sources":["../src/diagnostics.ts"],"sourcesContent":["// The `diagnostics:read` channel — app-facing surface (LLM_AND_AGENTS_SPEC §3.3/§4,\n// D4; roadmap R3-74 / P3-72).\n//\n// A *sibling* agent app (one holding `diagnostics:read`) observes the result of its\n// edits to the PREVIEWED app: the build/transpile errors from the sandbox bundler\n// and the previewed app's captured `console.*`. This is the in-browser analogue of\n// a local coding agent reading compiler/test output (P3-73's `get_diagnostics()`\n// tool). Read-only and scoped host-side to the paired previewed app's OWN\n// diagnostics — never another app's (the host channel projection enforces it; the\n// previewed app itself holds no `diagnostics:read`, so it is origin-excluded).\n//\n// Recipe-A push channel, identical get/onChange/use trio as `secrets`/`catalog`:\n// the host pushes `diagnostics` on change and answers a `request-diagnostics` poll,\n// gated per-frame by the read ACL. Inert until the host wires the channel\n// (site-main `channelBridge`); the contract ships here so the agent app (P3-73) can\n// be written against it.\nimport { createPushChannel } from './pushChannel';\nimport { DIAGNOSTICS, REQUEST_DIAGNOSTICS } from './generated/protocol';\n\n/** One build/transpile error from the sandbox bundler's compile of the previewed\n * app. `path` is repo-relative (leading slash) when the error is file-located. */\nexport interface BuildError {\n message: string;\n path?: string;\n line?: number;\n column?: number;\n}\n\n/** The `console.*` method a captured entry came from. */\nexport type ConsoleLevel = 'log' | 'info' | 'warn' | 'error' | 'debug';\n\n/** One captured `console.*` entry from the previewed app. The host renders the\n * console arguments to text host-side — the agent never receives live object\n * handles across the boundary. */\nexport interface ConsoleEntry {\n level: ConsoleLevel;\n text: string;\n /** Host-side timestamp (ms) at capture. */\n at: number;\n}\n\n/** Provenance (D4 / EDITOR_AS_APP_SPEC §12.3): WHICH previewed app + compile this\n * snapshot describes, so a consumer can tell stale output from fresh and never\n * conflates two apps' diagnostics. `null` until a first compile is observed. */\nexport interface DiagnosticsProvenance {\n /** The previewed app's stable key (`provider/ns/repo`). */\n appKey?: string;\n /** Monotonic id of the compile that produced these build errors. */\n compileId?: string;\n}\n\n/** A snapshot of the previewed app's diagnostics: its build errors, captured\n * console output, and the {@link DiagnosticsProvenance} of the compile. */\nexport interface Diagnostics {\n buildErrors: BuildError[];\n consoleEntries: ConsoleEntry[];\n provenance: DiagnosticsProvenance | null;\n}\n\n/** Value before the host answers — also the value when the app may not read the\n * channel (no `diagnostics:read`): an empty, provenance-less snapshot. */\nconst EMPTY: Diagnostics = { buildErrors: [], consoleEntries: [], provenance: null };\n\nconst channel = createPushChannel<Diagnostics>({\n pushType: DIAGNOSTICS,\n requestType: REQUEST_DIAGNOSTICS,\n initial: EMPTY,\n parse: (msg) => {\n // Require both arrays; tolerate an absent/partial provenance. A malformed push\n // is ignored (returns undefined) so the last good snapshot stands.\n if (!Array.isArray(msg.buildErrors) || !Array.isArray(msg.consoleEntries)) return undefined;\n const provenance =\n msg.provenance && typeof msg.provenance === 'object' ? (msg.provenance as DiagnosticsProvenance) : null;\n return {\n buildErrors: msg.buildErrors as BuildError[],\n consoleEntries: msg.consoleEntries as ConsoleEntry[],\n provenance,\n };\n },\n});\n\n/** One-off read of the previewed app's current diagnostics. Returns the empty\n * snapshot until the host answers (or if the app lacks `diagnostics:read`). Use\n * {@link onDiagnosticsChange}/{@link useDiagnostics} to react to live updates. */\nexport const getDiagnostics = (): Diagnostics => channel.get();\n\n/** Subscribe to diagnostics. Invoked immediately with the current value, then on\n * every host push. Returns an unsubscribe. */\nexport const onDiagnosticsChange = (listener: (d: Diagnostics) => void): (() => void) => channel.onChange(listener);\n\n/** React hook: the current diagnostics, re-rendering on every change. */\nexport const useDiagnostics = (): Diagnostics => channel.use();\n"],"mappings":";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAgBA,yBAAkC;AAClC,sBAAiD;AA4CjD,MAAM,QAAqB,EAAE,aAAa,CAAC,GAAG,gBAAgB,CAAC,GAAG,YAAY,KAAK;AAEnF,MAAM,cAAU,sCAA+B;AAAA,EAC7C,UAAU;AAAA,EACV,aAAa;AAAA,EACb,SAAS;AAAA,EACT,OAAO,CAAC,QAAQ;AAGd,QAAI,CAAC,MAAM,QAAQ,IAAI,WAAW,KAAK,CAAC,MAAM,QAAQ,IAAI,cAAc,EAAG,QAAO;AAClF,UAAM,aACJ,IAAI,cAAc,OAAO,IAAI,eAAe,WAAY,IAAI,aAAuC;AACrG,WAAO;AAAA,MACL,aAAa,IAAI;AAAA,MACjB,gBAAgB,IAAI;AAAA,MACpB;AAAA,IACF;AAAA,EACF;AACF,CAAC;AAKM,MAAM,iBAAiB,MAAmB,QAAQ,IAAI;AAItD,MAAM,sBAAsB,CAAC,aAAqD,QAAQ,SAAS,QAAQ;AAG3G,MAAM,iBAAiB,MAAmB,QAAQ,IAAI;","names":[]}
|
package/dist/diagnostics.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/diagnostics.ts"],"sourcesContent":["// The `diagnostics:read` channel — app-facing surface (LLM_AND_AGENTS_SPEC §3.3/§4,\n// D4; roadmap R3-74 / P3-72).\n//\n// A *sibling* agent app (one holding `diagnostics:read`) observes the result of its\n// edits to the PREVIEWED app: the build/transpile errors from the sandbox bundler\n// and the previewed app's captured `console.*`. This is the in-browser analogue of\n// a local coding agent reading compiler/test output (P3-73's `get_diagnostics()`\n// tool). Read-only and scoped host-side to the paired previewed app's OWN\n// diagnostics — never another app's (the host channel projection enforces it; the\n// previewed app itself holds no `diagnostics:read`, so it is origin-excluded).\n//\n// Recipe-A push channel, identical get/onChange/use trio as `secrets`/`catalog`:\n// the host pushes `diagnostics` on change and answers a `request-diagnostics` poll,\n// gated per-frame by the read ACL. Inert until the host wires the channel\n// (site-main `channelBridge`); the contract ships here so the agent app (P3-73) can\n// be written against it.\nimport { createPushChannel } from './pushChannel';\nimport { DIAGNOSTICS, REQUEST_DIAGNOSTICS } from './generated/protocol';\n\n/** One build/transpile error from the sandbox bundler's compile of the previewed\n * app. `path` is repo-relative (leading slash) when the error is file-located. */\nexport interface BuildError {\n message: string;\n path?: string;\n line?: number;\n column?: number;\n}\n\n/** The `console.*` method a captured entry came from. */\nexport type ConsoleLevel = 'log' | 'info' | 'warn' | 'error' | 'debug';\n\n/** One captured `console.*` entry from the previewed app. The host renders the\n * console arguments to text host-side — the agent never receives live object\n * handles across the boundary. */\nexport interface ConsoleEntry {\n level: ConsoleLevel;\n text: string;\n /** Host-side timestamp (ms) at capture. */\n at: number;\n}\n\n/** Provenance (D4 / EDITOR_AS_APP_SPEC §12.3): WHICH previewed app + compile this\n * snapshot describes, so a consumer can tell stale output from fresh and never\n * conflates two apps' diagnostics. `null` until a first compile is observed. */\nexport interface DiagnosticsProvenance {\n /** The previewed app's stable key (`provider/ns/repo`). */\n appKey?: string;\n /** Monotonic id of the compile that produced these build errors. */\n compileId?: string;\n}\n\n/** A snapshot of the previewed app's diagnostics: its build errors, captured\n * console output, and the {@link DiagnosticsProvenance} of the compile. */\nexport interface Diagnostics {\n buildErrors: BuildError[];\n consoleEntries: ConsoleEntry[];\n provenance: DiagnosticsProvenance | null;\n}\n\n/** Value before the host answers — also the value when the app may not read the\n * channel (no `diagnostics:read`): an empty, provenance-less snapshot. */\nconst EMPTY: Diagnostics = { buildErrors: [], consoleEntries: [], provenance: null };\n\nconst channel = createPushChannel<Diagnostics>({\n pushType: DIAGNOSTICS,\n requestType: REQUEST_DIAGNOSTICS,\n initial: EMPTY,\n parse: (msg) => {\n // Require both arrays; tolerate an absent/partial provenance. A malformed push\n // is ignored (returns undefined) so the last good snapshot stands.\n if (!Array.isArray(msg.buildErrors) || !Array.isArray(msg.consoleEntries)) return undefined;\n const provenance =\n msg.provenance && typeof msg.provenance === 'object'
|
|
1
|
+
{"version":3,"sources":["../src/diagnostics.ts"],"sourcesContent":["// The `diagnostics:read` channel — app-facing surface (LLM_AND_AGENTS_SPEC §3.3/§4,\n// D4; roadmap R3-74 / P3-72).\n//\n// A *sibling* agent app (one holding `diagnostics:read`) observes the result of its\n// edits to the PREVIEWED app: the build/transpile errors from the sandbox bundler\n// and the previewed app's captured `console.*`. This is the in-browser analogue of\n// a local coding agent reading compiler/test output (P3-73's `get_diagnostics()`\n// tool). Read-only and scoped host-side to the paired previewed app's OWN\n// diagnostics — never another app's (the host channel projection enforces it; the\n// previewed app itself holds no `diagnostics:read`, so it is origin-excluded).\n//\n// Recipe-A push channel, identical get/onChange/use trio as `secrets`/`catalog`:\n// the host pushes `diagnostics` on change and answers a `request-diagnostics` poll,\n// gated per-frame by the read ACL. Inert until the host wires the channel\n// (site-main `channelBridge`); the contract ships here so the agent app (P3-73) can\n// be written against it.\nimport { createPushChannel } from './pushChannel';\nimport { DIAGNOSTICS, REQUEST_DIAGNOSTICS } from './generated/protocol';\n\n/** One build/transpile error from the sandbox bundler's compile of the previewed\n * app. `path` is repo-relative (leading slash) when the error is file-located. */\nexport interface BuildError {\n message: string;\n path?: string;\n line?: number;\n column?: number;\n}\n\n/** The `console.*` method a captured entry came from. */\nexport type ConsoleLevel = 'log' | 'info' | 'warn' | 'error' | 'debug';\n\n/** One captured `console.*` entry from the previewed app. The host renders the\n * console arguments to text host-side — the agent never receives live object\n * handles across the boundary. */\nexport interface ConsoleEntry {\n level: ConsoleLevel;\n text: string;\n /** Host-side timestamp (ms) at capture. */\n at: number;\n}\n\n/** Provenance (D4 / EDITOR_AS_APP_SPEC §12.3): WHICH previewed app + compile this\n * snapshot describes, so a consumer can tell stale output from fresh and never\n * conflates two apps' diagnostics. `null` until a first compile is observed. */\nexport interface DiagnosticsProvenance {\n /** The previewed app's stable key (`provider/ns/repo`). */\n appKey?: string;\n /** Monotonic id of the compile that produced these build errors. */\n compileId?: string;\n}\n\n/** A snapshot of the previewed app's diagnostics: its build errors, captured\n * console output, and the {@link DiagnosticsProvenance} of the compile. */\nexport interface Diagnostics {\n buildErrors: BuildError[];\n consoleEntries: ConsoleEntry[];\n provenance: DiagnosticsProvenance | null;\n}\n\n/** Value before the host answers — also the value when the app may not read the\n * channel (no `diagnostics:read`): an empty, provenance-less snapshot. */\nconst EMPTY: Diagnostics = { buildErrors: [], consoleEntries: [], provenance: null };\n\nconst channel = createPushChannel<Diagnostics>({\n pushType: DIAGNOSTICS,\n requestType: REQUEST_DIAGNOSTICS,\n initial: EMPTY,\n parse: (msg) => {\n // Require both arrays; tolerate an absent/partial provenance. A malformed push\n // is ignored (returns undefined) so the last good snapshot stands.\n if (!Array.isArray(msg.buildErrors) || !Array.isArray(msg.consoleEntries)) return undefined;\n const provenance =\n msg.provenance && typeof msg.provenance === 'object' ? (msg.provenance as DiagnosticsProvenance) : null;\n return {\n buildErrors: msg.buildErrors as BuildError[],\n consoleEntries: msg.consoleEntries as ConsoleEntry[],\n provenance,\n };\n },\n});\n\n/** One-off read of the previewed app's current diagnostics. Returns the empty\n * snapshot until the host answers (or if the app lacks `diagnostics:read`). Use\n * {@link onDiagnosticsChange}/{@link useDiagnostics} to react to live updates. */\nexport const getDiagnostics = (): Diagnostics => channel.get();\n\n/** Subscribe to diagnostics. Invoked immediately with the current value, then on\n * every host push. Returns an unsubscribe. */\nexport const onDiagnosticsChange = (listener: (d: Diagnostics) => void): (() => void) => channel.onChange(listener);\n\n/** React hook: the current diagnostics, re-rendering on every change. */\nexport const useDiagnostics = (): Diagnostics => channel.use();\n"],"mappings":";AAgBA,SAAS,yBAAyB;AAClC,SAAS,aAAa,2BAA2B;AA4CjD,MAAM,QAAqB,EAAE,aAAa,CAAC,GAAG,gBAAgB,CAAC,GAAG,YAAY,KAAK;AAEnF,MAAM,UAAU,kBAA+B;AAAA,EAC7C,UAAU;AAAA,EACV,aAAa;AAAA,EACb,SAAS;AAAA,EACT,OAAO,CAAC,QAAQ;AAGd,QAAI,CAAC,MAAM,QAAQ,IAAI,WAAW,KAAK,CAAC,MAAM,QAAQ,IAAI,cAAc,EAAG,QAAO;AAClF,UAAM,aACJ,IAAI,cAAc,OAAO,IAAI,eAAe,WAAY,IAAI,aAAuC;AACrG,WAAO;AAAA,MACL,aAAa,IAAI;AAAA,MACjB,gBAAgB,IAAI;AAAA,MACpB;AAAA,IACF;AAAA,EACF;AACF,CAAC;AAKM,MAAM,iBAAiB,MAAmB,QAAQ,IAAI;AAItD,MAAM,sBAAsB,CAAC,aAAqD,QAAQ,SAAS,QAAQ;AAG3G,MAAM,iBAAiB,MAAmB,QAAQ,IAAI;","names":[]}
|
package/dist/editor.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/editor.ts"],"sourcesContent":["import { protocolRequest } from './sandboxUtils';\nimport { SCHEMES } from './protocolSchemes';\nimport { PROTOCOL_EDITOR } from './generated/protocol';\n\n/**\n * Open a working-tree file in the immediately.run host editor (UI_AS_APPS_SPEC §4 —\n * the file explorer's click-to-open). This is an INTENT: the app asks, the HOST\n * validates the path and drives the CodeMirror editor — the editor itself stays\n * host-owned (§2 recursion boundary), so an app can never own or script it beyond\n * \"please show this file\".\n *\n * Requires the elevated `editor:open` capability — a previewed app does not hold it\n * (it must not move the host's focus), so only a system app whose binding grants it\n * (the file explorer) can call this; anyone else is refused at the gate.\n */\n\n/** An error from {@link openInEditor}, carrying a machine-readable `.code`. */\nexport interface EditorOpenError extends Error {\n code:\n | 'forbidden' // the frame lacks `editor:open`\n | 'not-found' // no such file in the live working tree (the host never creates)\n | 'invalid-params' // the path was empty / contained `..` / looked like a URI\n | 'no-target' // there is no host editor session to open files in\n | 'unknown';\n}\n\ntype EditorResult
|
|
1
|
+
{"version":3,"sources":["../src/editor.ts"],"sourcesContent":["import { protocolRequest } from './sandboxUtils';\nimport { SCHEMES } from './protocolSchemes';\nimport { PROTOCOL_EDITOR } from './generated/protocol';\n\n/**\n * Open a working-tree file in the immediately.run host editor (UI_AS_APPS_SPEC §4 —\n * the file explorer's click-to-open). This is an INTENT: the app asks, the HOST\n * validates the path and drives the CodeMirror editor — the editor itself stays\n * host-owned (§2 recursion boundary), so an app can never own or script it beyond\n * \"please show this file\".\n *\n * Requires the elevated `editor:open` capability — a previewed app does not hold it\n * (it must not move the host's focus), so only a system app whose binding grants it\n * (the file explorer) can call this; anyone else is refused at the gate.\n */\n\n/** An error from {@link openInEditor}, carrying a machine-readable `.code`. */\nexport interface EditorOpenError extends Error {\n code:\n | 'forbidden' // the frame lacks `editor:open`\n | 'not-found' // no such file in the live working tree (the host never creates)\n | 'invalid-params' // the path was empty / contained `..` / looked like a URI\n | 'no-target' // there is no host editor session to open files in\n | 'unknown';\n}\n\ntype EditorResult = { ok: true; data: unknown } | { ok: false; code: string; message: string };\n\nconst editorRequest = async (method: string, arg: Record<string, unknown>): Promise<void> => {\n const res = (await protocolRequest(SCHEMES[PROTOCOL_EDITOR], method, [arg])) as EditorResult;\n if (!res || res.ok !== true) {\n const err = new Error(res?.message ?? `editor ${method} failed`) as EditorWriteError;\n err.code = (res?.code as EditorWriteError['code']) ?? 'unknown';\n throw err;\n }\n};\n\n/**\n * Ask the host to open `path` (a repo-relative working-tree path, e.g. `src/App.tsx`\n * or `/src/App.tsx`) in the editor. Resolves once the editor switches to it; rejects\n * with an {@link EditorOpenError} (`.code`) if the path is invalid, missing, or this\n * app may not open files.\n */\nexport const openInEditor = (path: string): Promise<void> => editorRequest('open', { path });\n\n/**\n * Where to land when entering the edit experience (EDITOR_FIRST_EDITING_SPEC §6\n * Delta A). v1 supports only an optional repo-relative `path` in the CURRENT repo\n * (self-scoped — the app you are already running; the host navigates within the\n * current route, never to another repo). A URI or `..` path is refused\n * `invalid-params`. Editing a file in one of your *mounts* (a space) is the\n * `edit-file` task, not this.\n */\nexport interface EditTarget {\n /** A repo-relative working-tree path in the current repo to focus once in edit\n * mode (e.g. `src/App.tsx`). Omit to edit the current route's entry. */\n path?: string;\n}\n\n/** An error from {@link requestEdit}, carrying a machine-readable `.code`. */\nexport interface RequestEditError extends Error {\n code:\n | 'read-only' // editing isn't possible here (a `ro` mount / anonymous viewer) — HIDE the affordance\n | 'forbidden' // the host refuses (e.g. a cross-repo / out-of-scope target)\n | 'invalid-params' // the target was malformed (URI / `..` / a non-current repo)\n | 'no-target' // there is no host editor session to enter\n | 'unknown';\n}\n\n/**\n * Ask the host to enter the **edit experience** for the app you are running —\n * the present→edit transition (`/present/...` → `/edit/...`) an app cannot make\n * itself. This is an INTENT (§2 recursion boundary): the app asks, the HOST\n * performs the visible, user-observable navigation and draws all editor chrome;\n * the app never navigates or paints chrome.\n *\n * Use it to offer an \"edit this\" affordance from a run/present-mode app that opens\n * the app's own source in the platform editor — instead of shipping a bespoke\n * in-app editor (EDITOR_FIRST_EDITING_SPEC §1).\n *\n * Resolves once the host begins the transition; rejects with a\n * {@link RequestEditError} (`.code`). Treat `read-only`/`forbidden` as \"editing is\n * not available — hide the affordance,\" never as an error to surface to the user.\n */\nexport const requestEdit = (target?: EditTarget): Promise<void> =>\n editorRequest('requestEdit', target ? { ...target } : {});\n\n// ---------------------------------------------------------------------------\n// Editor SESSION management (EDITOR_AS_APP_SPEC §5.1; editor-as-app plan Phase\n// 03). Unlike `openInEditor` (the explorer's cross-app intent, `editor:open`),\n// these drive the editor's OWN open-tab set + active file, so they are gated by\n// the editor app's `editor:document` capability — a file explorer holding only\n// `editor:open` cannot call them. The host re-validates the path against the live\n// working tree; the editor itself stays host-owned (§2 recursion boundary).\n// ---------------------------------------------------------------------------\n\n/** An error from a session intent ({@link setActiveFile} / {@link closeFile}),\n * carrying a machine-readable `.code`. */\nexport interface EditorSessionError extends Error {\n code:\n | 'forbidden' // the frame lacks `editor:document`\n | 'not-found' // no such file in the live working tree\n | 'invalid-params' // the path was empty / contained `..` / looked like a URI\n | 'no-target' // there is no host editor session\n | 'unknown';\n}\n\n/** Switch the editor's active file to `path`, opening it (adding a tab) if it is\n * not already open — native `setActiveFile` parity. Rejects with an\n * {@link EditorSessionError} (`.code`) if the path is missing/invalid or this app\n * lacks `editor:document`. */\nexport const setActiveFile = (path: string): Promise<void> => editorRequest('setActive', { path });\n\n/** Close `path`'s tab in the editor (remove it from the open set) — native\n * `closeFile` parity. Rejects with an {@link EditorSessionError} (`.code`). */\nexport const closeFile = (path: string): Promise<void> => editorRequest('close', { path });\n\n// ---------------------------------------------------------------------------\n// Working-tree mutation (UI_AS_APPS_SPEC §4 / EDITOR_AS_APP_SPEC §5.2). The file\n// explorer NAMES a working-tree path and the HOST performs the COW write (and\n// refreshes the preview) — the app holds no write port; it asks. Gated by the\n// first-party `editor:write` capability, so only a first-party chrome app (the\n// file explorer) can call these; anyone else is refused at the gate.\n// ---------------------------------------------------------------------------\n\n/** An error from a working-tree mutation, carrying a machine-readable `.code`. */\nexport interface EditorWriteError extends Error {\n code:\n | 'forbidden' // the frame lacks `editor:write` (first-party-only)\n | 'not-found' // the target file/folder does not exist (delete/rename)\n | 'exists' // the target already exists (create/rename would clobber)\n | 'protected' // the host refuses to delete this file (e.g. package.json)\n | 'too-large' // an upload exceeds the host's size limit\n | 'invalid-params' // a path was empty / contained `..` / looked like a URI\n | 'no-target' // there is no host editor session\n | 'unknown';\n}\n\n/** Create an empty working-tree file at `path` and open it. Rejects `exists` if a\n * file is already there. */\nexport const createFile = (path: string): Promise<void> => editorRequest('createFile', { path });\n\n/** Create a working-tree folder at `path` (materialised with a `.gitkeep`). */\nexport const createFolder = (path: string): Promise<void> => editorRequest('createFolder', { path });\n\n/** Delete a working-tree file, or a folder and everything under it. Rejects\n * `protected` for files the host won't remove, `not-found` if absent. */\nexport const deleteEntry = (path: string): Promise<void> => editorRequest('deleteEntry', { path });\n\n/** Rename/move a working-tree file from `from` to `to`. Rejects `exists` if `to`\n * is taken, `not-found` if `from` is absent. */\nexport const renameEntry = (from: string, to: string): Promise<void> => editorRequest('rename', { from, to });\n\n/** Upload binary/text `bytes` to a working-tree file at `path`. Rejects\n * `too-large` past the host's size limit. The bytes are transferred (zero-copy). */\nexport const uploadFile = (path: string, bytes: Uint8Array): Promise<void> => editorRequest('upload', { path, bytes });\n"],"mappings":";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,0BAAgC;AAChC,6BAAwB;AACxB,sBAAgC;AA0BhC,MAAM,gBAAgB,OAAO,QAAgB,QAAgD;AAC3F,QAAM,MAAO,UAAM,qCAAgB,+BAAQ,+BAAe,GAAG,QAAQ,CAAC,GAAG,CAAC;AAC1E,MAAI,CAAC,OAAO,IAAI,OAAO,MAAM;AAC3B,UAAM,MAAM,IAAI,MAAM,KAAK,WAAW,UAAU,MAAM,SAAS;AAC/D,QAAI,OAAQ,KAAK,QAAqC;AACtD,UAAM;AAAA,EACR;AACF;AAQO,MAAM,eAAe,CAAC,SAAgC,cAAc,QAAQ,EAAE,KAAK,CAAC;AAyCpF,MAAM,cAAc,CAAC,WAC1B,cAAc,eAAe,SAAS,EAAE,GAAG,OAAO,IAAI,CAAC,CAAC;AA0BnD,MAAM,gBAAgB,CAAC,SAAgC,cAAc,aAAa,EAAE,KAAK,CAAC;AAI1F,MAAM,YAAY,CAAC,SAAgC,cAAc,SAAS,EAAE,KAAK,CAAC;AAyBlF,MAAM,aAAa,CAAC,SAAgC,cAAc,cAAc,EAAE,KAAK,CAAC;AAGxF,MAAM,eAAe,CAAC,SAAgC,cAAc,gBAAgB,EAAE,KAAK,CAAC;AAI5F,MAAM,cAAc,CAAC,SAAgC,cAAc,eAAe,EAAE,KAAK,CAAC;AAI1F,MAAM,cAAc,CAAC,MAAc,OAA8B,cAAc,UAAU,EAAE,MAAM,GAAG,CAAC;AAIrG,MAAM,aAAa,CAAC,MAAc,UAAqC,cAAc,UAAU,EAAE,MAAM,MAAM,CAAC;","names":[]}
|
package/dist/editor.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/editor.ts"],"sourcesContent":["import { protocolRequest } from './sandboxUtils';\nimport { SCHEMES } from './protocolSchemes';\nimport { PROTOCOL_EDITOR } from './generated/protocol';\n\n/**\n * Open a working-tree file in the immediately.run host editor (UI_AS_APPS_SPEC §4 —\n * the file explorer's click-to-open). This is an INTENT: the app asks, the HOST\n * validates the path and drives the CodeMirror editor — the editor itself stays\n * host-owned (§2 recursion boundary), so an app can never own or script it beyond\n * \"please show this file\".\n *\n * Requires the elevated `editor:open` capability — a previewed app does not hold it\n * (it must not move the host's focus), so only a system app whose binding grants it\n * (the file explorer) can call this; anyone else is refused at the gate.\n */\n\n/** An error from {@link openInEditor}, carrying a machine-readable `.code`. */\nexport interface EditorOpenError extends Error {\n code:\n | 'forbidden' // the frame lacks `editor:open`\n | 'not-found' // no such file in the live working tree (the host never creates)\n | 'invalid-params' // the path was empty / contained `..` / looked like a URI\n | 'no-target' // there is no host editor session to open files in\n | 'unknown';\n}\n\ntype EditorResult
|
|
1
|
+
{"version":3,"sources":["../src/editor.ts"],"sourcesContent":["import { protocolRequest } from './sandboxUtils';\nimport { SCHEMES } from './protocolSchemes';\nimport { PROTOCOL_EDITOR } from './generated/protocol';\n\n/**\n * Open a working-tree file in the immediately.run host editor (UI_AS_APPS_SPEC §4 —\n * the file explorer's click-to-open). This is an INTENT: the app asks, the HOST\n * validates the path and drives the CodeMirror editor — the editor itself stays\n * host-owned (§2 recursion boundary), so an app can never own or script it beyond\n * \"please show this file\".\n *\n * Requires the elevated `editor:open` capability — a previewed app does not hold it\n * (it must not move the host's focus), so only a system app whose binding grants it\n * (the file explorer) can call this; anyone else is refused at the gate.\n */\n\n/** An error from {@link openInEditor}, carrying a machine-readable `.code`. */\nexport interface EditorOpenError extends Error {\n code:\n | 'forbidden' // the frame lacks `editor:open`\n | 'not-found' // no such file in the live working tree (the host never creates)\n | 'invalid-params' // the path was empty / contained `..` / looked like a URI\n | 'no-target' // there is no host editor session to open files in\n | 'unknown';\n}\n\ntype EditorResult = { ok: true; data: unknown } | { ok: false; code: string; message: string };\n\nconst editorRequest = async (method: string, arg: Record<string, unknown>): Promise<void> => {\n const res = (await protocolRequest(SCHEMES[PROTOCOL_EDITOR], method, [arg])) as EditorResult;\n if (!res || res.ok !== true) {\n const err = new Error(res?.message ?? `editor ${method} failed`) as EditorWriteError;\n err.code = (res?.code as EditorWriteError['code']) ?? 'unknown';\n throw err;\n }\n};\n\n/**\n * Ask the host to open `path` (a repo-relative working-tree path, e.g. `src/App.tsx`\n * or `/src/App.tsx`) in the editor. Resolves once the editor switches to it; rejects\n * with an {@link EditorOpenError} (`.code`) if the path is invalid, missing, or this\n * app may not open files.\n */\nexport const openInEditor = (path: string): Promise<void> => editorRequest('open', { path });\n\n/**\n * Where to land when entering the edit experience (EDITOR_FIRST_EDITING_SPEC §6\n * Delta A). v1 supports only an optional repo-relative `path` in the CURRENT repo\n * (self-scoped — the app you are already running; the host navigates within the\n * current route, never to another repo). A URI or `..` path is refused\n * `invalid-params`. Editing a file in one of your *mounts* (a space) is the\n * `edit-file` task, not this.\n */\nexport interface EditTarget {\n /** A repo-relative working-tree path in the current repo to focus once in edit\n * mode (e.g. `src/App.tsx`). Omit to edit the current route's entry. */\n path?: string;\n}\n\n/** An error from {@link requestEdit}, carrying a machine-readable `.code`. */\nexport interface RequestEditError extends Error {\n code:\n | 'read-only' // editing isn't possible here (a `ro` mount / anonymous viewer) — HIDE the affordance\n | 'forbidden' // the host refuses (e.g. a cross-repo / out-of-scope target)\n | 'invalid-params' // the target was malformed (URI / `..` / a non-current repo)\n | 'no-target' // there is no host editor session to enter\n | 'unknown';\n}\n\n/**\n * Ask the host to enter the **edit experience** for the app you are running —\n * the present→edit transition (`/present/...` → `/edit/...`) an app cannot make\n * itself. This is an INTENT (§2 recursion boundary): the app asks, the HOST\n * performs the visible, user-observable navigation and draws all editor chrome;\n * the app never navigates or paints chrome.\n *\n * Use it to offer an \"edit this\" affordance from a run/present-mode app that opens\n * the app's own source in the platform editor — instead of shipping a bespoke\n * in-app editor (EDITOR_FIRST_EDITING_SPEC §1).\n *\n * Resolves once the host begins the transition; rejects with a\n * {@link RequestEditError} (`.code`). Treat `read-only`/`forbidden` as \"editing is\n * not available — hide the affordance,\" never as an error to surface to the user.\n */\nexport const requestEdit = (target?: EditTarget): Promise<void> =>\n editorRequest('requestEdit', target ? { ...target } : {});\n\n// ---------------------------------------------------------------------------\n// Editor SESSION management (EDITOR_AS_APP_SPEC §5.1; editor-as-app plan Phase\n// 03). Unlike `openInEditor` (the explorer's cross-app intent, `editor:open`),\n// these drive the editor's OWN open-tab set + active file, so they are gated by\n// the editor app's `editor:document` capability — a file explorer holding only\n// `editor:open` cannot call them. The host re-validates the path against the live\n// working tree; the editor itself stays host-owned (§2 recursion boundary).\n// ---------------------------------------------------------------------------\n\n/** An error from a session intent ({@link setActiveFile} / {@link closeFile}),\n * carrying a machine-readable `.code`. */\nexport interface EditorSessionError extends Error {\n code:\n | 'forbidden' // the frame lacks `editor:document`\n | 'not-found' // no such file in the live working tree\n | 'invalid-params' // the path was empty / contained `..` / looked like a URI\n | 'no-target' // there is no host editor session\n | 'unknown';\n}\n\n/** Switch the editor's active file to `path`, opening it (adding a tab) if it is\n * not already open — native `setActiveFile` parity. Rejects with an\n * {@link EditorSessionError} (`.code`) if the path is missing/invalid or this app\n * lacks `editor:document`. */\nexport const setActiveFile = (path: string): Promise<void> => editorRequest('setActive', { path });\n\n/** Close `path`'s tab in the editor (remove it from the open set) — native\n * `closeFile` parity. Rejects with an {@link EditorSessionError} (`.code`). */\nexport const closeFile = (path: string): Promise<void> => editorRequest('close', { path });\n\n// ---------------------------------------------------------------------------\n// Working-tree mutation (UI_AS_APPS_SPEC §4 / EDITOR_AS_APP_SPEC §5.2). The file\n// explorer NAMES a working-tree path and the HOST performs the COW write (and\n// refreshes the preview) — the app holds no write port; it asks. Gated by the\n// first-party `editor:write` capability, so only a first-party chrome app (the\n// file explorer) can call these; anyone else is refused at the gate.\n// ---------------------------------------------------------------------------\n\n/** An error from a working-tree mutation, carrying a machine-readable `.code`. */\nexport interface EditorWriteError extends Error {\n code:\n | 'forbidden' // the frame lacks `editor:write` (first-party-only)\n | 'not-found' // the target file/folder does not exist (delete/rename)\n | 'exists' // the target already exists (create/rename would clobber)\n | 'protected' // the host refuses to delete this file (e.g. package.json)\n | 'too-large' // an upload exceeds the host's size limit\n | 'invalid-params' // a path was empty / contained `..` / looked like a URI\n | 'no-target' // there is no host editor session\n | 'unknown';\n}\n\n/** Create an empty working-tree file at `path` and open it. Rejects `exists` if a\n * file is already there. */\nexport const createFile = (path: string): Promise<void> => editorRequest('createFile', { path });\n\n/** Create a working-tree folder at `path` (materialised with a `.gitkeep`). */\nexport const createFolder = (path: string): Promise<void> => editorRequest('createFolder', { path });\n\n/** Delete a working-tree file, or a folder and everything under it. Rejects\n * `protected` for files the host won't remove, `not-found` if absent. */\nexport const deleteEntry = (path: string): Promise<void> => editorRequest('deleteEntry', { path });\n\n/** Rename/move a working-tree file from `from` to `to`. Rejects `exists` if `to`\n * is taken, `not-found` if `from` is absent. */\nexport const renameEntry = (from: string, to: string): Promise<void> => editorRequest('rename', { from, to });\n\n/** Upload binary/text `bytes` to a working-tree file at `path`. Rejects\n * `too-large` past the host's size limit. The bytes are transferred (zero-copy). */\nexport const uploadFile = (path: string, bytes: Uint8Array): Promise<void> => editorRequest('upload', { path, bytes });\n"],"mappings":";AAAA,SAAS,uBAAuB;AAChC,SAAS,eAAe;AACxB,SAAS,uBAAuB;AA0BhC,MAAM,gBAAgB,OAAO,QAAgB,QAAgD;AAC3F,QAAM,MAAO,MAAM,gBAAgB,QAAQ,eAAe,GAAG,QAAQ,CAAC,GAAG,CAAC;AAC1E,MAAI,CAAC,OAAO,IAAI,OAAO,MAAM;AAC3B,UAAM,MAAM,IAAI,MAAM,KAAK,WAAW,UAAU,MAAM,SAAS;AAC/D,QAAI,OAAQ,KAAK,QAAqC;AACtD,UAAM;AAAA,EACR;AACF;AAQO,MAAM,eAAe,CAAC,SAAgC,cAAc,QAAQ,EAAE,KAAK,CAAC;AAyCpF,MAAM,cAAc,CAAC,WAC1B,cAAc,eAAe,SAAS,EAAE,GAAG,OAAO,IAAI,CAAC,CAAC;AA0BnD,MAAM,gBAAgB,CAAC,SAAgC,cAAc,aAAa,EAAE,KAAK,CAAC;AAI1F,MAAM,YAAY,CAAC,SAAgC,cAAc,SAAS,EAAE,KAAK,CAAC;AAyBlF,MAAM,aAAa,CAAC,SAAgC,cAAc,cAAc,EAAE,KAAK,CAAC;AAGxF,MAAM,eAAe,CAAC,SAAgC,cAAc,gBAAgB,EAAE,KAAK,CAAC;AAI5F,MAAM,cAAc,CAAC,SAAgC,cAAc,eAAe,EAAE,KAAK,CAAC;AAI1F,MAAM,cAAc,CAAC,MAAc,OAA8B,cAAc,UAAU,EAAE,MAAM,GAAG,CAAC;AAIrG,MAAM,aAAa,CAAC,MAAc,UAAqC,cAAc,UAAU,EAAE,MAAM,MAAM,CAAC;","names":[]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/editorContext.ts"],"sourcesContent":["import { createPushChannel } from './pushChannel';\nimport { EDITOR_CONTEXT, REQUEST_EDITOR_CONTEXT } from './generated/protocol';\n\n/**\n * The editor \"dirty set\" mirrored from the immediately.run host into the sandbox\n * (UI_AS_APPS_SPEC §5.3): which files the user has changed but not yet saved.\n *\n * This is the ELEVATED `editor:read` capability — only a system app whose binding\n * grants it (e.g. the contribute dialog) receives it. The active file and ref are\n * already available to every app via routing (`useNavigationState`), so this\n * channel carries only the genuine delta: the unsaved paths. An app without\n * `editor:read` simply sees an empty dirty set.\n */\nexport interface EditorContext {\n /** Repo-relative paths the user has modified but not yet saved. */\n dirtyPaths: string[];\n /** Repo-relative paths currently open as tabs in the host editor (§4.2). */\n openFiles: string[];\n /**\n * The one file currently focused in the host editor (repo-relative, leading\n * slash — e.g. `/src/index.ts`), or `null` when no file is open. Distinct from\n * {@link dirtyPaths} (the unsaved SET) and {@link openFiles} (the open-tab set):\n * this is the single ACTIVE file. A baseline app reads its own active file from\n * the route, but a self-routed system panel (`drivesHostRoute=false`, e.g. the\n * file explorer) does not see the host editor's route, so it receives the active\n * file here instead (UI_AS_APPS_SPEC §5.3 self-routed-panel refinement).\n */\n activeFile: string | null;\n /**\n * The viewed-document HINT (R3-268): the working-tree file the STAGE app is\n * currently rendering (leading slash), or `null`. Distinct from\n * {@link activeFile} (the editor's focus): this is what is ON STAGE. It is an\n * app-supplied claim the host validated for existence only — consume it as a\n * highlight and nothing more (never scroll, move focus, or switch files on\n * it; that contract is what keeps the signal activation-free).\n */\n viewedFile: string | null;\n}\n\n// Read over the transport (SDK_PACKAGING_SPEC §4): the host pushes `editor-context`\n// and answers `request-editor-context` — but only for a frame holding `editor:read`\n// (gated by the channel router). An app without it gets no reply, so the empty\n// default below stands. Wire format: site-main channelBridge.ts.\nconst isStringArray = (v: unknown): v is string[]
|
|
1
|
+
{"version":3,"sources":["../src/editorContext.ts"],"sourcesContent":["import { createPushChannel } from './pushChannel';\nimport { EDITOR_CONTEXT, REQUEST_EDITOR_CONTEXT } from './generated/protocol';\n\n/**\n * The editor \"dirty set\" mirrored from the immediately.run host into the sandbox\n * (UI_AS_APPS_SPEC §5.3): which files the user has changed but not yet saved.\n *\n * This is the ELEVATED `editor:read` capability — only a system app whose binding\n * grants it (e.g. the contribute dialog) receives it. The active file and ref are\n * already available to every app via routing (`useNavigationState`), so this\n * channel carries only the genuine delta: the unsaved paths. An app without\n * `editor:read` simply sees an empty dirty set.\n */\nexport interface EditorContext {\n /** Repo-relative paths the user has modified but not yet saved. */\n dirtyPaths: string[];\n /** Repo-relative paths currently open as tabs in the host editor (§4.2). */\n openFiles: string[];\n /**\n * The one file currently focused in the host editor (repo-relative, leading\n * slash — e.g. `/src/index.ts`), or `null` when no file is open. Distinct from\n * {@link dirtyPaths} (the unsaved SET) and {@link openFiles} (the open-tab set):\n * this is the single ACTIVE file. A baseline app reads its own active file from\n * the route, but a self-routed system panel (`drivesHostRoute=false`, e.g. the\n * file explorer) does not see the host editor's route, so it receives the active\n * file here instead (UI_AS_APPS_SPEC §5.3 self-routed-panel refinement).\n */\n activeFile: string | null;\n /**\n * The viewed-document HINT (R3-268): the working-tree file the STAGE app is\n * currently rendering (leading slash), or `null`. Distinct from\n * {@link activeFile} (the editor's focus): this is what is ON STAGE. It is an\n * app-supplied claim the host validated for existence only — consume it as a\n * highlight and nothing more (never scroll, move focus, or switch files on\n * it; that contract is what keeps the signal activation-free).\n */\n viewedFile: string | null;\n}\n\n// Read over the transport (SDK_PACKAGING_SPEC §4): the host pushes `editor-context`\n// and answers `request-editor-context` — but only for a frame holding `editor:read`\n// (gated by the channel router). An app without it gets no reply, so the empty\n// default below stands. Wire format: site-main channelBridge.ts.\nconst isStringArray = (v: unknown): v is string[] => Array.isArray(v) && v.every((p) => typeof p === 'string');\n\nconst channel = createPushChannel<EditorContext>({\n pushType: EDITOR_CONTEXT,\n requestType: REQUEST_EDITOR_CONTEXT,\n initial: { dirtyPaths: [], openFiles: [], activeFile: null, viewedFile: null },\n parse: (msg) =>\n isStringArray(msg.dirtyPaths)\n ? {\n dirtyPaths: msg.dirtyPaths,\n // `openFiles` is newer than `dirtyPaths`; tolerate an older host that\n // omits it (defensive SDK — defaults to empty rather than rejecting).\n openFiles: isStringArray(msg.openFiles) ? msg.openFiles : [],\n // `activeFile` is newer still; an older host that omits it (or sends a\n // non-string) reads as `null` — no file highlighted, never a throw.\n activeFile: typeof msg.activeFile === 'string' ? msg.activeFile : null,\n // `viewedFile` is the newest (R3-268); same tolerance — an older host\n // that omits it reads as `null` (no stage highlight), never a throw.\n viewedFile: typeof msg.viewedFile === 'string' ? msg.viewedFile : null,\n }\n : undefined,\n});\n\n/**\n * Returns the current editor context (dirty set). Poll this for a one-off read;\n * use {@link onEditorContextChange} or {@link useEditorContext} to react.\n */\nexport const getEditorContext = (): EditorContext => channel.get();\n\n/**\n * Subscribe to editor-context changes. The listener is invoked immediately with\n * the current context, then again on every change. Returns an unsubscribe fn.\n */\nexport const onEditorContextChange = (listener: (context: EditorContext) => void): (() => void) =>\n channel.onChange(listener);\n\n/**\n * React hook returning the current editor context (dirty set), re-rendering when\n * it changes. Handy for a contribute dialog: `const { dirtyPaths } =\n * useEditorContext()` to show \"you'll save N files\" before calling `contribute()`.\n */\nexport const useEditorContext = (): EditorContext => channel.use();\n"],"mappings":";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,yBAAkC;AAClC,sBAAuD;AA0CvD,MAAM,gBAAgB,CAAC,MAA8B,MAAM,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC,MAAM,OAAO,MAAM,QAAQ;AAE7G,MAAM,cAAU,sCAAiC;AAAA,EAC/C,UAAU;AAAA,EACV,aAAa;AAAA,EACb,SAAS,EAAE,YAAY,CAAC,GAAG,WAAW,CAAC,GAAG,YAAY,MAAM,YAAY,KAAK;AAAA,EAC7E,OAAO,CAAC,QACN,cAAc,IAAI,UAAU,IACxB;AAAA,IACE,YAAY,IAAI;AAAA;AAAA;AAAA,IAGhB,WAAW,cAAc,IAAI,SAAS,IAAI,IAAI,YAAY,CAAC;AAAA;AAAA;AAAA,IAG3D,YAAY,OAAO,IAAI,eAAe,WAAW,IAAI,aAAa;AAAA;AAAA;AAAA,IAGlE,YAAY,OAAO,IAAI,eAAe,WAAW,IAAI,aAAa;AAAA,EACpE,IACA;AACR,CAAC;AAMM,MAAM,mBAAmB,MAAqB,QAAQ,IAAI;AAM1D,MAAM,wBAAwB,CAAC,aACpC,QAAQ,SAAS,QAAQ;AAOpB,MAAM,mBAAmB,MAAqB,QAAQ,IAAI;","names":[]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/editorContext.ts"],"sourcesContent":["import { createPushChannel } from './pushChannel';\nimport { EDITOR_CONTEXT, REQUEST_EDITOR_CONTEXT } from './generated/protocol';\n\n/**\n * The editor \"dirty set\" mirrored from the immediately.run host into the sandbox\n * (UI_AS_APPS_SPEC §5.3): which files the user has changed but not yet saved.\n *\n * This is the ELEVATED `editor:read` capability — only a system app whose binding\n * grants it (e.g. the contribute dialog) receives it. The active file and ref are\n * already available to every app via routing (`useNavigationState`), so this\n * channel carries only the genuine delta: the unsaved paths. An app without\n * `editor:read` simply sees an empty dirty set.\n */\nexport interface EditorContext {\n /** Repo-relative paths the user has modified but not yet saved. */\n dirtyPaths: string[];\n /** Repo-relative paths currently open as tabs in the host editor (§4.2). */\n openFiles: string[];\n /**\n * The one file currently focused in the host editor (repo-relative, leading\n * slash — e.g. `/src/index.ts`), or `null` when no file is open. Distinct from\n * {@link dirtyPaths} (the unsaved SET) and {@link openFiles} (the open-tab set):\n * this is the single ACTIVE file. A baseline app reads its own active file from\n * the route, but a self-routed system panel (`drivesHostRoute=false`, e.g. the\n * file explorer) does not see the host editor's route, so it receives the active\n * file here instead (UI_AS_APPS_SPEC §5.3 self-routed-panel refinement).\n */\n activeFile: string | null;\n /**\n * The viewed-document HINT (R3-268): the working-tree file the STAGE app is\n * currently rendering (leading slash), or `null`. Distinct from\n * {@link activeFile} (the editor's focus): this is what is ON STAGE. It is an\n * app-supplied claim the host validated for existence only — consume it as a\n * highlight and nothing more (never scroll, move focus, or switch files on\n * it; that contract is what keeps the signal activation-free).\n */\n viewedFile: string | null;\n}\n\n// Read over the transport (SDK_PACKAGING_SPEC §4): the host pushes `editor-context`\n// and answers `request-editor-context` — but only for a frame holding `editor:read`\n// (gated by the channel router). An app without it gets no reply, so the empty\n// default below stands. Wire format: site-main channelBridge.ts.\nconst isStringArray = (v: unknown): v is string[]
|
|
1
|
+
{"version":3,"sources":["../src/editorContext.ts"],"sourcesContent":["import { createPushChannel } from './pushChannel';\nimport { EDITOR_CONTEXT, REQUEST_EDITOR_CONTEXT } from './generated/protocol';\n\n/**\n * The editor \"dirty set\" mirrored from the immediately.run host into the sandbox\n * (UI_AS_APPS_SPEC §5.3): which files the user has changed but not yet saved.\n *\n * This is the ELEVATED `editor:read` capability — only a system app whose binding\n * grants it (e.g. the contribute dialog) receives it. The active file and ref are\n * already available to every app via routing (`useNavigationState`), so this\n * channel carries only the genuine delta: the unsaved paths. An app without\n * `editor:read` simply sees an empty dirty set.\n */\nexport interface EditorContext {\n /** Repo-relative paths the user has modified but not yet saved. */\n dirtyPaths: string[];\n /** Repo-relative paths currently open as tabs in the host editor (§4.2). */\n openFiles: string[];\n /**\n * The one file currently focused in the host editor (repo-relative, leading\n * slash — e.g. `/src/index.ts`), or `null` when no file is open. Distinct from\n * {@link dirtyPaths} (the unsaved SET) and {@link openFiles} (the open-tab set):\n * this is the single ACTIVE file. A baseline app reads its own active file from\n * the route, but a self-routed system panel (`drivesHostRoute=false`, e.g. the\n * file explorer) does not see the host editor's route, so it receives the active\n * file here instead (UI_AS_APPS_SPEC §5.3 self-routed-panel refinement).\n */\n activeFile: string | null;\n /**\n * The viewed-document HINT (R3-268): the working-tree file the STAGE app is\n * currently rendering (leading slash), or `null`. Distinct from\n * {@link activeFile} (the editor's focus): this is what is ON STAGE. It is an\n * app-supplied claim the host validated for existence only — consume it as a\n * highlight and nothing more (never scroll, move focus, or switch files on\n * it; that contract is what keeps the signal activation-free).\n */\n viewedFile: string | null;\n}\n\n// Read over the transport (SDK_PACKAGING_SPEC §4): the host pushes `editor-context`\n// and answers `request-editor-context` — but only for a frame holding `editor:read`\n// (gated by the channel router). An app without it gets no reply, so the empty\n// default below stands. Wire format: site-main channelBridge.ts.\nconst isStringArray = (v: unknown): v is string[] => Array.isArray(v) && v.every((p) => typeof p === 'string');\n\nconst channel = createPushChannel<EditorContext>({\n pushType: EDITOR_CONTEXT,\n requestType: REQUEST_EDITOR_CONTEXT,\n initial: { dirtyPaths: [], openFiles: [], activeFile: null, viewedFile: null },\n parse: (msg) =>\n isStringArray(msg.dirtyPaths)\n ? {\n dirtyPaths: msg.dirtyPaths,\n // `openFiles` is newer than `dirtyPaths`; tolerate an older host that\n // omits it (defensive SDK — defaults to empty rather than rejecting).\n openFiles: isStringArray(msg.openFiles) ? msg.openFiles : [],\n // `activeFile` is newer still; an older host that omits it (or sends a\n // non-string) reads as `null` — no file highlighted, never a throw.\n activeFile: typeof msg.activeFile === 'string' ? msg.activeFile : null,\n // `viewedFile` is the newest (R3-268); same tolerance — an older host\n // that omits it reads as `null` (no stage highlight), never a throw.\n viewedFile: typeof msg.viewedFile === 'string' ? msg.viewedFile : null,\n }\n : undefined,\n});\n\n/**\n * Returns the current editor context (dirty set). Poll this for a one-off read;\n * use {@link onEditorContextChange} or {@link useEditorContext} to react.\n */\nexport const getEditorContext = (): EditorContext => channel.get();\n\n/**\n * Subscribe to editor-context changes. The listener is invoked immediately with\n * the current context, then again on every change. Returns an unsubscribe fn.\n */\nexport const onEditorContextChange = (listener: (context: EditorContext) => void): (() => void) =>\n channel.onChange(listener);\n\n/**\n * React hook returning the current editor context (dirty set), re-rendering when\n * it changes. Handy for a contribute dialog: `const { dirtyPaths } =\n * useEditorContext()` to show \"you'll save N files\" before calling `contribute()`.\n */\nexport const useEditorContext = (): EditorContext => channel.use();\n"],"mappings":";AAAA,SAAS,yBAAyB;AAClC,SAAS,gBAAgB,8BAA8B;AA0CvD,MAAM,gBAAgB,CAAC,MAA8B,MAAM,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC,MAAM,OAAO,MAAM,QAAQ;AAE7G,MAAM,UAAU,kBAAiC;AAAA,EAC/C,UAAU;AAAA,EACV,aAAa;AAAA,EACb,SAAS,EAAE,YAAY,CAAC,GAAG,WAAW,CAAC,GAAG,YAAY,MAAM,YAAY,KAAK;AAAA,EAC7E,OAAO,CAAC,QACN,cAAc,IAAI,UAAU,IACxB;AAAA,IACE,YAAY,IAAI;AAAA;AAAA;AAAA,IAGhB,WAAW,cAAc,IAAI,SAAS,IAAI,IAAI,YAAY,CAAC;AAAA;AAAA;AAAA,IAG3D,YAAY,OAAO,IAAI,eAAe,WAAW,IAAI,aAAa;AAAA;AAAA;AAAA,IAGlE,YAAY,OAAO,IAAI,eAAe,WAAW,IAAI,aAAa;AAAA,EACpE,IACA;AACR,CAAC;AAMM,MAAM,mBAAmB,MAAqB,QAAQ,IAAI;AAM1D,MAAM,wBAAwB,CAAC,aACpC,QAAQ,SAAS,QAAQ;AAOpB,MAAM,mBAAmB,MAAqB,QAAQ,IAAI;","names":[]}
|