@immediately-run/sdk 0.59.1 → 0.61.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/index.cjs +3 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +2 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/llm.cjs +17 -1
- package/dist/llm.cjs.map +1 -1
- package/dist/llm.d.cts +34 -15
- package/dist/llm.d.ts +34 -15
- package/dist/llm.js +17 -1
- package/dist/llm.js.map +1 -1
- package/dist/platformLink.cjs +42 -0
- package/dist/platformLink.cjs.map +1 -0
- package/dist/platformLink.d.cts +30 -0
- package/dist/platformLink.d.ts +30 -0
- package/dist/platformLink.js +18 -0
- package/dist/platformLink.js.map +1 -0
- package/dist/recents.cjs +9 -1
- package/dist/recents.cjs.map +1 -1
- package/dist/recents.d.cts +6 -0
- package/dist/recents.d.ts +6 -0
- package/dist/recents.js +9 -1
- package/dist/recents.js.map +1 -1
- package/dist/urlUtils.cjs +13 -0
- package/dist/urlUtils.cjs.map +1 -1
- package/dist/urlUtils.d.cts +15 -1
- package/dist/urlUtils.d.ts +15 -1
- package/dist/urlUtils.js +12 -0
- package/dist/urlUtils.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 +7 -3
package/dist/index.cjs
CHANGED
|
@@ -81,6 +81,7 @@ __reExport(index_exports, require("./metadataQueryTool"), module.exports);
|
|
|
81
81
|
__reExport(index_exports, require("./collectHeadings"), module.exports);
|
|
82
82
|
__reExport(index_exports, require("./agentContext"), module.exports);
|
|
83
83
|
__reExport(index_exports, require("./fence"), module.exports);
|
|
84
|
+
__reExport(index_exports, require("./platformLink"), module.exports);
|
|
84
85
|
// Annotate the CommonJS export names for ESM import in node:
|
|
85
86
|
0 && (module.exports = {
|
|
86
87
|
SafeInclude,
|
|
@@ -140,6 +141,7 @@ __reExport(index_exports, require("./fence"), module.exports);
|
|
|
140
141
|
...require("./metadataQueryTool"),
|
|
141
142
|
...require("./collectHeadings"),
|
|
142
143
|
...require("./agentContext"),
|
|
143
|
-
...require("./fence")
|
|
144
|
+
...require("./fence"),
|
|
145
|
+
...require("./platformLink")
|
|
144
146
|
});
|
|
145
147
|
//# sourceMappingURL=index.cjs.map
|
package/dist/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts"],"sourcesContent":["export * from './MDXProvider';\nexport * from './routing';\nexport * from './boot';\nexport * from './components/Include';\n// Only the component is public. `stripFrontmatter`/`appMountRelative` are module-level\n// exports so they can be unit-tested directly, NOT public API — the SDK's surface is\n// backwards-compatible forever, so an internal helper exported for a test's convenience is a\n// permanent commitment made for the wrong reason.\nexport { SafeInclude } from './components/SafeInclude';\nexport * from './sourceCache';\nexport * from './components/MDXComponents';\nexport * from './linkSpace';\nexport * from './bundle';\n// Deprecated `Corpus*` spellings of the above (R3-482); see `src/corpus.ts`.\nexport * from './corpus';\nexport * from './components/MountImage';\nexport * from './components/Routes';\nexport * from './hooks';\n// R3-276: the supported way for a viewer app to provide its own metadata store,\n// replacing a wholesale re-provision of `TinkerableContext` in app code.\nexport * from './metadataSource';\n// The deprecated injected-bundler adapters, re-exported so their deprecation notices\n// are visible in the published docs (R3-278; the window only narrows).\nexport { getInjectedMetadataEmitter, getInjectedMetadataSnapshot } from './injectedBundler';\nexport * from './auth';\nexport * from './theme';\nexport * from './editorContext';\nexport * from './editor';\nexport * from './formFactor';\nexport * from './chromeState';\nexport * from './workspace';\nexport * from './hostAttention';\nexport * from './region';\nexport * from './mounts';\nexport * from './analytics';\nexport * from './contribute';\nexport * from './catalog';\nexport * from './ipc';\nexport * from './dnd';\nexport * from './netFetch';\nexport * from './feed';\nexport * from './secrets';\nexport * from './recents'; // R3-485: the gated recent-projects read (page.home)\nexport * from './llm';\nexport * from './diagnostics';\nexport * from './vcs';\nexport * from './onFsChange';\nexport * from './fs';\nexport * from './debug';\nexport * from './tasks';\nexport * from './launch';\nexport * from './runtime';\nexport * from './irMarkers';\nexport * from './ready';\nexport * from './loading';\nexport * from './protocolStream';\nexport * from './protocolDeadline';\nexport * from './sandboxTypes';\nexport * from './safeContent';\n// R3-489 (GROVE_AGENT_SPEC): the embedded-agent seam every app shares — the tool-use\n// loop ported from agent-demo (`runAgent` over the host chat slot), the MDX metadata\n// query tool, the headings index collector, the deixis context block, and the fence\n// for corpus-derived bytes entering a loop.\nexport * from './agentLoop';\nexport * from './agentSteering';\nexport * from './agentChatClient';\nexport * from './metadataQueryTool';\nexport * from './collectHeadings';\nexport * from './agentContext';\nexport * from './fence';\n"],"mappings":";;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,0BAAc,0BAAd;AACA,0BAAc,sBADd;AAEA,0BAAc,mBAFd;AAGA,0BAAc,iCAHd;AAQA,yBAA4B;AAC5B,0BAAc,0BATd;AAUA,0BAAc,uCAVd;AAWA,0BAAc,wBAXd;AAYA,0BAAc,qBAZd;AAcA,0BAAc,qBAdd;AAeA,0BAAc,oCAfd;AAgBA,0BAAc,gCAhBd;AAiBA,0BAAc,oBAjBd;AAoBA,0BAAc,6BApBd;AAuBA,6BAAwE;AACxE,0BAAc,mBAxBd;AAyBA,0BAAc,oBAzBd;AA0BA,0BAAc,4BA1Bd;AA2BA,0BAAc,qBA3Bd;AA4BA,0BAAc,yBA5Bd;AA6BA,0BAAc,0BA7Bd;AA8BA,0BAAc,wBA9Bd;AA+BA,0BAAc,4BA/Bd;AAgCA,0BAAc,qBAhCd;AAiCA,0BAAc,qBAjCd;AAkCA,0BAAc,wBAlCd;AAmCA,0BAAc,yBAnCd;AAoCA,0BAAc,sBApCd;AAqCA,0BAAc,kBArCd;AAsCA,0BAAc,kBAtCd;AAuCA,0BAAc,uBAvCd;AAwCA,0BAAc,mBAxCd;AAyCA,0BAAc,sBAzCd;AA0CA,0BAAc,sBA1Cd;AA2CA,0BAAc,kBA3Cd;AA4CA,0BAAc,0BA5Cd;AA6CA,0BAAc,kBA7Cd;AA8CA,0BAAc,yBA9Cd;AA+CA,0BAAc,iBA/Cd;AAgDA,0BAAc,oBAhDd;AAiDA,0BAAc,oBAjDd;AAkDA,0BAAc,qBAlDd;AAmDA,0BAAc,sBAnDd;AAoDA,0BAAc,wBApDd;AAqDA,0BAAc,oBArDd;AAsDA,0BAAc,sBAtDd;AAuDA,0BAAc,6BAvDd;AAwDA,0BAAc,+BAxDd;AAyDA,0BAAc,2BAzDd;AA0DA,0BAAc,0BA1Dd;AA+DA,0BAAc,wBA/Dd;AAgEA,0BAAc,4BAhEd;AAiEA,0BAAc,8BAjEd;AAkEA,0BAAc,gCAlEd;AAmEA,0BAAc,8BAnEd;AAoEA,0BAAc,2BApEd;AAqEA,0BAAc,oBArEd;","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts"],"sourcesContent":["export * from './MDXProvider';\nexport * from './routing';\nexport * from './boot';\nexport * from './components/Include';\n// Only the component is public. `stripFrontmatter`/`appMountRelative` are module-level\n// exports so they can be unit-tested directly, NOT public API — the SDK's surface is\n// backwards-compatible forever, so an internal helper exported for a test's convenience is a\n// permanent commitment made for the wrong reason.\nexport { SafeInclude } from './components/SafeInclude';\nexport * from './sourceCache';\nexport * from './components/MDXComponents';\nexport * from './linkSpace';\nexport * from './bundle';\n// Deprecated `Corpus*` spellings of the above (R3-482); see `src/corpus.ts`.\nexport * from './corpus';\nexport * from './components/MountImage';\nexport * from './components/Routes';\nexport * from './hooks';\n// R3-276: the supported way for a viewer app to provide its own metadata store,\n// replacing a wholesale re-provision of `TinkerableContext` in app code.\nexport * from './metadataSource';\n// The deprecated injected-bundler adapters, re-exported so their deprecation notices\n// are visible in the published docs (R3-278; the window only narrows).\nexport { getInjectedMetadataEmitter, getInjectedMetadataSnapshot } from './injectedBundler';\nexport * from './auth';\nexport * from './theme';\nexport * from './editorContext';\nexport * from './editor';\nexport * from './formFactor';\nexport * from './chromeState';\nexport * from './workspace';\nexport * from './hostAttention';\nexport * from './region';\nexport * from './mounts';\nexport * from './analytics';\nexport * from './contribute';\nexport * from './catalog';\nexport * from './ipc';\nexport * from './dnd';\nexport * from './netFetch';\nexport * from './feed';\nexport * from './secrets';\nexport * from './recents'; // R3-485: the gated recent-projects read (page.home)\nexport * from './llm';\nexport * from './diagnostics';\nexport * from './vcs';\nexport * from './onFsChange';\nexport * from './fs';\nexport * from './debug';\nexport * from './tasks';\nexport * from './launch';\nexport * from './runtime';\nexport * from './irMarkers';\nexport * from './ready';\nexport * from './loading';\nexport * from './protocolStream';\nexport * from './protocolDeadline';\nexport * from './sandboxTypes';\nexport * from './safeContent';\n// R3-489 (GROVE_AGENT_SPEC): the embedded-agent seam every app shares — the tool-use\n// loop ported from agent-demo (`runAgent` over the host chat slot), the MDX metadata\n// query tool, the headings index collector, the deixis context block, and the fence\n// for corpus-derived bytes entering a loop.\nexport * from './agentLoop';\nexport * from './agentSteering';\nexport * from './agentChatClient';\nexport * from './metadataQueryTool';\nexport * from './collectHeadings';\nexport * from './agentContext';\nexport * from './fence';\nexport * from './platformLink';\n"],"mappings":";;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,0BAAc,0BAAd;AACA,0BAAc,sBADd;AAEA,0BAAc,mBAFd;AAGA,0BAAc,iCAHd;AAQA,yBAA4B;AAC5B,0BAAc,0BATd;AAUA,0BAAc,uCAVd;AAWA,0BAAc,wBAXd;AAYA,0BAAc,qBAZd;AAcA,0BAAc,qBAdd;AAeA,0BAAc,oCAfd;AAgBA,0BAAc,gCAhBd;AAiBA,0BAAc,oBAjBd;AAoBA,0BAAc,6BApBd;AAuBA,6BAAwE;AACxE,0BAAc,mBAxBd;AAyBA,0BAAc,oBAzBd;AA0BA,0BAAc,4BA1Bd;AA2BA,0BAAc,qBA3Bd;AA4BA,0BAAc,yBA5Bd;AA6BA,0BAAc,0BA7Bd;AA8BA,0BAAc,wBA9Bd;AA+BA,0BAAc,4BA/Bd;AAgCA,0BAAc,qBAhCd;AAiCA,0BAAc,qBAjCd;AAkCA,0BAAc,wBAlCd;AAmCA,0BAAc,yBAnCd;AAoCA,0BAAc,sBApCd;AAqCA,0BAAc,kBArCd;AAsCA,0BAAc,kBAtCd;AAuCA,0BAAc,uBAvCd;AAwCA,0BAAc,mBAxCd;AAyCA,0BAAc,sBAzCd;AA0CA,0BAAc,sBA1Cd;AA2CA,0BAAc,kBA3Cd;AA4CA,0BAAc,0BA5Cd;AA6CA,0BAAc,kBA7Cd;AA8CA,0BAAc,yBA9Cd;AA+CA,0BAAc,iBA/Cd;AAgDA,0BAAc,oBAhDd;AAiDA,0BAAc,oBAjDd;AAkDA,0BAAc,qBAlDd;AAmDA,0BAAc,sBAnDd;AAoDA,0BAAc,wBApDd;AAqDA,0BAAc,oBArDd;AAsDA,0BAAc,sBAtDd;AAuDA,0BAAc,6BAvDd;AAwDA,0BAAc,+BAxDd;AAyDA,0BAAc,2BAzDd;AA0DA,0BAAc,0BA1Dd;AA+DA,0BAAc,wBA/Dd;AAgEA,0BAAc,4BAhEd;AAiEA,0BAAc,8BAjEd;AAkEA,0BAAc,gCAlEd;AAmEA,0BAAc,8BAnEd;AAoEA,0BAAc,2BApEd;AAqEA,0BAAc,oBArEd;AAsEA,0BAAc,2BAtEd;","names":[]}
|
package/dist/index.d.cts
CHANGED
|
@@ -31,7 +31,7 @@ export { HostFetchInit, HostFetchResponse, HostFetchStreamEvent, HostFetchStream
|
|
|
31
31
|
export { FeedFetchResponse, FeedParams, feedFetch } from './feed.cjs';
|
|
32
32
|
export { SecretError, SecretGrant, SecretHints, SecretQuery, SecretType, SecretView, getSecrets, onSecretsChange, requestAddSecret, requestSecret, revokeSecret, useSecrets } from './secrets.cjs';
|
|
33
33
|
export { RecentProject, clearRecentProjects, listRecentProjects } from './recents.cjs';
|
|
34
|
-
export { ChatDelta, ChatFeatures, ChatMessage, ChatProviderInfo, ChatProviderState, ChatRequest, ChatResult, ChatRole, ChatStopReason, ContentPart, ToolDef, chat, describeChat, describeChatState, normalizeProviderInfo, onChatProviderChange, onChatProviderStateChange, useChatProvider, useChatProviderState } from './llm.cjs';
|
|
34
|
+
export { ChatDelta, ChatExecutor, ChatFeatures, ChatMessage, ChatProviderInfo, ChatProviderState, ChatRequest, ChatResult, ChatRole, ChatStopReason, ChatTierModels, ContentPart, ToolDef, chat, describeChat, describeChatState, normalizeProviderInfo, onChatProviderChange, onChatProviderStateChange, useChatProvider, useChatProviderState } from './llm.cjs';
|
|
35
35
|
export { BuildError, ConsoleEntry, ConsoleLevel, Diagnostics, DiagnosticsProvenance, getDiagnostics, onDiagnosticsChange, useDiagnostics } from './diagnostics.cjs';
|
|
36
36
|
export { VcsActionError, VcsBranch, VcsChange, VcsPR, VcsState, getVcsState, onVcsStateChange, refreshDiff, refreshPRs, resetWorkingTree, useVcsState } from './vcs.cjs';
|
|
37
37
|
export { FsChange, MountChange, getFsChange, onFsChange, useFsChange } from './onFsChange.cjs';
|
|
@@ -54,6 +54,7 @@ export { HeadingSummary, METADATA_HEADINGS_KEY, METADATA_QUERY_TOOL_DESCRIPTOR,
|
|
|
54
54
|
export { collectHeadings } from './collectHeadings.cjs';
|
|
55
55
|
export { AgentContextAppFields, AgentContextBlock, renderAgentContext, useAgentContext } from './agentContext.cjs';
|
|
56
56
|
export { fenceUntrusted } from './fence.cjs';
|
|
57
|
+
export { PlatformLink, PlatformLinkProps, usePlatformHref } from './platformLink.cjs';
|
|
57
58
|
export { Admonition, AdmonitionType } from './components/Admonition.cjs';
|
|
58
59
|
export { FS_PREFIX, LinkSpace, ResolvedLinkTarget, normalizeAbsolute, resolveLinkTarget } from '@immediately-run/mdx-plugins';
|
|
59
60
|
export { GrantRecord, Member, ResolvedUser, Role, SpaceInfo, getSpaceMembers, inviteToSpace, listAllSpaces, listGrants, listSpaces, lookupUser, revokeGrant, setSpaceRole, unshareSpace } from './generated/spaces.cjs';
|
package/dist/index.d.ts
CHANGED
|
@@ -31,7 +31,7 @@ export { HostFetchInit, HostFetchResponse, HostFetchStreamEvent, HostFetchStream
|
|
|
31
31
|
export { FeedFetchResponse, FeedParams, feedFetch } from './feed.js';
|
|
32
32
|
export { SecretError, SecretGrant, SecretHints, SecretQuery, SecretType, SecretView, getSecrets, onSecretsChange, requestAddSecret, requestSecret, revokeSecret, useSecrets } from './secrets.js';
|
|
33
33
|
export { RecentProject, clearRecentProjects, listRecentProjects } from './recents.js';
|
|
34
|
-
export { ChatDelta, ChatFeatures, ChatMessage, ChatProviderInfo, ChatProviderState, ChatRequest, ChatResult, ChatRole, ChatStopReason, ContentPart, ToolDef, chat, describeChat, describeChatState, normalizeProviderInfo, onChatProviderChange, onChatProviderStateChange, useChatProvider, useChatProviderState } from './llm.js';
|
|
34
|
+
export { ChatDelta, ChatExecutor, ChatFeatures, ChatMessage, ChatProviderInfo, ChatProviderState, ChatRequest, ChatResult, ChatRole, ChatStopReason, ChatTierModels, ContentPart, ToolDef, chat, describeChat, describeChatState, normalizeProviderInfo, onChatProviderChange, onChatProviderStateChange, useChatProvider, useChatProviderState } from './llm.js';
|
|
35
35
|
export { BuildError, ConsoleEntry, ConsoleLevel, Diagnostics, DiagnosticsProvenance, getDiagnostics, onDiagnosticsChange, useDiagnostics } from './diagnostics.js';
|
|
36
36
|
export { VcsActionError, VcsBranch, VcsChange, VcsPR, VcsState, getVcsState, onVcsStateChange, refreshDiff, refreshPRs, resetWorkingTree, useVcsState } from './vcs.js';
|
|
37
37
|
export { FsChange, MountChange, getFsChange, onFsChange, useFsChange } from './onFsChange.js';
|
|
@@ -54,6 +54,7 @@ export { HeadingSummary, METADATA_HEADINGS_KEY, METADATA_QUERY_TOOL_DESCRIPTOR,
|
|
|
54
54
|
export { collectHeadings } from './collectHeadings.js';
|
|
55
55
|
export { AgentContextAppFields, AgentContextBlock, renderAgentContext, useAgentContext } from './agentContext.js';
|
|
56
56
|
export { fenceUntrusted } from './fence.js';
|
|
57
|
+
export { PlatformLink, PlatformLinkProps, usePlatformHref } from './platformLink.js';
|
|
57
58
|
export { Admonition, AdmonitionType } from './components/Admonition.js';
|
|
58
59
|
export { FS_PREFIX, LinkSpace, ResolvedLinkTarget, normalizeAbsolute, resolveLinkTarget } from '@immediately-run/mdx-plugins';
|
|
59
60
|
export { GrantRecord, Member, ResolvedUser, Role, SpaceInfo, getSpaceMembers, inviteToSpace, listAllSpaces, listGrants, listSpaces, lookupUser, revokeGrant, setSpaceRole, unshareSpace } from './generated/spaces.js';
|
package/dist/index.js
CHANGED
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts"],"sourcesContent":["export * from './MDXProvider';\nexport * from './routing';\nexport * from './boot';\nexport * from './components/Include';\n// Only the component is public. `stripFrontmatter`/`appMountRelative` are module-level\n// exports so they can be unit-tested directly, NOT public API — the SDK's surface is\n// backwards-compatible forever, so an internal helper exported for a test's convenience is a\n// permanent commitment made for the wrong reason.\nexport { SafeInclude } from './components/SafeInclude';\nexport * from './sourceCache';\nexport * from './components/MDXComponents';\nexport * from './linkSpace';\nexport * from './bundle';\n// Deprecated `Corpus*` spellings of the above (R3-482); see `src/corpus.ts`.\nexport * from './corpus';\nexport * from './components/MountImage';\nexport * from './components/Routes';\nexport * from './hooks';\n// R3-276: the supported way for a viewer app to provide its own metadata store,\n// replacing a wholesale re-provision of `TinkerableContext` in app code.\nexport * from './metadataSource';\n// The deprecated injected-bundler adapters, re-exported so their deprecation notices\n// are visible in the published docs (R3-278; the window only narrows).\nexport { getInjectedMetadataEmitter, getInjectedMetadataSnapshot } from './injectedBundler';\nexport * from './auth';\nexport * from './theme';\nexport * from './editorContext';\nexport * from './editor';\nexport * from './formFactor';\nexport * from './chromeState';\nexport * from './workspace';\nexport * from './hostAttention';\nexport * from './region';\nexport * from './mounts';\nexport * from './analytics';\nexport * from './contribute';\nexport * from './catalog';\nexport * from './ipc';\nexport * from './dnd';\nexport * from './netFetch';\nexport * from './feed';\nexport * from './secrets';\nexport * from './recents'; // R3-485: the gated recent-projects read (page.home)\nexport * from './llm';\nexport * from './diagnostics';\nexport * from './vcs';\nexport * from './onFsChange';\nexport * from './fs';\nexport * from './debug';\nexport * from './tasks';\nexport * from './launch';\nexport * from './runtime';\nexport * from './irMarkers';\nexport * from './ready';\nexport * from './loading';\nexport * from './protocolStream';\nexport * from './protocolDeadline';\nexport * from './sandboxTypes';\nexport * from './safeContent';\n// R3-489 (GROVE_AGENT_SPEC): the embedded-agent seam every app shares — the tool-use\n// loop ported from agent-demo (`runAgent` over the host chat slot), the MDX metadata\n// query tool, the headings index collector, the deixis context block, and the fence\n// for corpus-derived bytes entering a loop.\nexport * from './agentLoop';\nexport * from './agentSteering';\nexport * from './agentChatClient';\nexport * from './metadataQueryTool';\nexport * from './collectHeadings';\nexport * from './agentContext';\nexport * from './fence';\n"],"mappings":";AAAA,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AAKd,SAAS,mBAAmB;AAC5B,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AAEd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AAGd,cAAc;AAGd,SAAS,4BAA4B,mCAAmC;AACxE,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AAKd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts"],"sourcesContent":["export * from './MDXProvider';\nexport * from './routing';\nexport * from './boot';\nexport * from './components/Include';\n// Only the component is public. `stripFrontmatter`/`appMountRelative` are module-level\n// exports so they can be unit-tested directly, NOT public API — the SDK's surface is\n// backwards-compatible forever, so an internal helper exported for a test's convenience is a\n// permanent commitment made for the wrong reason.\nexport { SafeInclude } from './components/SafeInclude';\nexport * from './sourceCache';\nexport * from './components/MDXComponents';\nexport * from './linkSpace';\nexport * from './bundle';\n// Deprecated `Corpus*` spellings of the above (R3-482); see `src/corpus.ts`.\nexport * from './corpus';\nexport * from './components/MountImage';\nexport * from './components/Routes';\nexport * from './hooks';\n// R3-276: the supported way for a viewer app to provide its own metadata store,\n// replacing a wholesale re-provision of `TinkerableContext` in app code.\nexport * from './metadataSource';\n// The deprecated injected-bundler adapters, re-exported so their deprecation notices\n// are visible in the published docs (R3-278; the window only narrows).\nexport { getInjectedMetadataEmitter, getInjectedMetadataSnapshot } from './injectedBundler';\nexport * from './auth';\nexport * from './theme';\nexport * from './editorContext';\nexport * from './editor';\nexport * from './formFactor';\nexport * from './chromeState';\nexport * from './workspace';\nexport * from './hostAttention';\nexport * from './region';\nexport * from './mounts';\nexport * from './analytics';\nexport * from './contribute';\nexport * from './catalog';\nexport * from './ipc';\nexport * from './dnd';\nexport * from './netFetch';\nexport * from './feed';\nexport * from './secrets';\nexport * from './recents'; // R3-485: the gated recent-projects read (page.home)\nexport * from './llm';\nexport * from './diagnostics';\nexport * from './vcs';\nexport * from './onFsChange';\nexport * from './fs';\nexport * from './debug';\nexport * from './tasks';\nexport * from './launch';\nexport * from './runtime';\nexport * from './irMarkers';\nexport * from './ready';\nexport * from './loading';\nexport * from './protocolStream';\nexport * from './protocolDeadline';\nexport * from './sandboxTypes';\nexport * from './safeContent';\n// R3-489 (GROVE_AGENT_SPEC): the embedded-agent seam every app shares — the tool-use\n// loop ported from agent-demo (`runAgent` over the host chat slot), the MDX metadata\n// query tool, the headings index collector, the deixis context block, and the fence\n// for corpus-derived bytes entering a loop.\nexport * from './agentLoop';\nexport * from './agentSteering';\nexport * from './agentChatClient';\nexport * from './metadataQueryTool';\nexport * from './collectHeadings';\nexport * from './agentContext';\nexport * from './fence';\nexport * from './platformLink';\n"],"mappings":";AAAA,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AAKd,SAAS,mBAAmB;AAC5B,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AAEd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AAGd,cAAc;AAGd,SAAS,4BAA4B,mCAAmC;AACxE,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AAKd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;","names":[]}
|
package/dist/llm.cjs
CHANGED
|
@@ -35,10 +35,26 @@ function chat(req) {
|
|
|
35
35
|
const { signal, ...params } = req;
|
|
36
36
|
return (0, import_catalog.invokeStream)("llm:chat", params, signal);
|
|
37
37
|
}
|
|
38
|
+
const EXECUTORS = ["browser-direct", "backend-proxied"];
|
|
39
|
+
const usableModels = (raw) => {
|
|
40
|
+
if (!raw || typeof raw !== "object") return void 0;
|
|
41
|
+
const { fast, smart } = raw;
|
|
42
|
+
return typeof fast === "string" && fast && typeof smart === "string" && smart ? { fast, smart } : void 0;
|
|
43
|
+
};
|
|
38
44
|
function normalizeProviderInfo(provider) {
|
|
39
45
|
if (!provider) return null;
|
|
46
|
+
const { displayName: rawName, executor: rawExecutor, models: rawModels, ...rest } = provider;
|
|
40
47
|
const wire = provider.features;
|
|
41
|
-
|
|
48
|
+
const executor = EXECUTORS.includes(rawExecutor) ? rawExecutor : void 0;
|
|
49
|
+
const displayName = typeof rawName === "string" && rawName ? rawName : void 0;
|
|
50
|
+
const models = usableModels(rawModels);
|
|
51
|
+
return {
|
|
52
|
+
...rest,
|
|
53
|
+
features: { ...wire, reasoning: wire.reasoning === true },
|
|
54
|
+
...displayName ? { displayName } : {},
|
|
55
|
+
...executor ? { executor } : {},
|
|
56
|
+
...models ? { models } : {}
|
|
57
|
+
};
|
|
42
58
|
}
|
|
43
59
|
let answered = false;
|
|
44
60
|
const channel = (0, import_pushChannel.createPushChannel)({
|
package/dist/llm.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/llm.ts"],"sourcesContent":["// Provider-agnostic LLM chat — the `llm.chat@1` slot (SERVICE_PROVIDERS_SPEC;\n// LLM_AND_AGENTS_SPEC §8 D5).\n//\n// An app calls ONE chat slot and never worries about which provider the user has a\n// key for: the HOST resolves which vendor answers from the key the user holds\n// (`SecretView.boundOrigin`) plus their `preferredImplementation` choice, normalizes\n// the wire format, injects the key host-side at the §6 net:fetch point (the\n// look-at-nothing proxy), and streams normalized deltas back. The app never names a\n// vendor, never sees the key, and needs NO `net:fetch`/`secrets` grant of its own —\n// only the `llm:chat` capability (elevated, app-scoped: a fork earns it by consent).\n//\n// Inert until the host implements `protocol-llm` (the `chat` stream) + the\n// `llm-provider` describe channel; the contract ships here so apps (the file-explorer\n// summarize fork) can be written against it — exactly how `secrets.ts` shipped ahead\n// of `protocol-secrets`.\nimport { invokeStream } from './catalog';\nimport { createPushChannel } from './pushChannel';\nimport { LLM_PROVIDER, REQUEST_LLM_PROVIDER } from './generated/protocol';\n\n/** Who authored a {@link ChatMessage}. */\nexport type ChatRole = 'system' | 'user' | 'assistant' | 'tool';\n\n/** A part of a message. `image` is only honored when the resolved provider\n * advertises `features.vision` (§2.5); `tool-use`/`tool-result` only when it\n * advertises `features.tools` — branch on {@link describeChat} first. */\nexport type ContentPart =\n | { type: 'text'; text: string }\n | { type: 'image'; mimeType: string; data: string } // data: base64, no data: URL prefix\n // A tool call the model emitted on a prior `assistant` turn — replay it in the\n // conversation so a follow-up request carries the agentic history. Pairs with the\n // streamed `tool-call` {@link ChatDelta} that first surfaced it.\n | { type: 'tool-use'; id: string; name: string; input: Record<string, unknown> }\n // A block of the model's own REASONING from a prior `assistant` turn (R3-335).\n // Honored only when the resolved provider advertises `features.reasoning`.\n //\n // Echo these back. On some providers a reasoning block must be replayed — with its\n // `signature` intact and BEFORE the turn's text/tool-use — for the following turn to\n // be accepted at all; a loop that drops them is quietly lossy across turns in a way\n // that shows up as degraded output rather than an error. Pairs with the streamed\n // `reasoning` {@link ChatDelta}.\n | { type: 'reasoning'; text: string; signature?: string }\n // Reasoning the provider REDACTED: opaque bytes with no readable text, which still\n // have to be echoed back in place to keep the chain valid. Never render it.\n | { type: 'reasoning-redacted'; data: string }\n // The result of executing a `tool-use`, fed back so the model can continue. Carried\n // on a `user`/`tool`-role message; `toolCallId` matches the `tool-use` `id`.\n | { type: 'tool-result'; toolCallId: string; content: string; isError?: boolean };\n\n/** One message in a {@link ChatRequest}: a role plus its content parts. */\nexport interface ChatMessage {\n role: ChatRole;\n content: ContentPart[];\n}\n\n/** A tool the model may call — honored only when `features.tools`. */\nexport interface ToolDef {\n name: string;\n description?: string;\n /** JSON-Schema for the tool's arguments. */\n inputSchema: Record<string, unknown>;\n}\n\n/** A host-brokered chat completion request: the messages plus optional tools,\n * response format, and model hint (each honored per the provider's features). */\nexport interface ChatRequest {\n messages: ChatMessage[];\n /** Honored only when the resolved provider advertises `features.tools`. */\n tools?: ToolDef[];\n /** `'json'` honored only when `features.jsonMode`. Defaults to `'text'`. */\n responseFormat?: 'text' | 'json';\n maxTokens?: number;\n /** An ABSTRACT tier hint, never a vendor model id — the host maps it to a concrete\n * model on the resolved provider. Omit to take the provider's default. */\n modelHint?: 'fast' | 'smart';\n /** Abort the completion mid-stream. When it fires, the SDK sends the host a cancel\n * frame so the host aborts the upstream provider request and STOPS BILLING the\n * user's key — not merely stops the app-side iterator (LLM_AND_AGENTS_SPEC §3.3\n * \"abort the in-flight LLM request\", R3-224). Not sent over the wire (an\n * `AbortSignal` isn't serializable); handled SDK-side. */\n signal?: AbortSignal;\n}\n\n/** One streamed chunk. Consumers typically accumulate `text-delta`s. */\nexport type ChatDelta =\n | { type: 'text-delta'; text: string }\n | { type: 'tool-call'; id: string; name: string; input: unknown }\n // R3-335 — the model's reasoning as it streams. `reasoning-delta` carries the text\n // incrementally (render it live); the terminal `reasoning` carries the WHOLE block\n // plus the `signature` the provider may require on the echo, and is what a caller\n // should put back into the conversation. A provider without reasoning emits neither.\n | { type: 'reasoning-delta'; text: string }\n | { type: 'reasoning'; text: string; signature?: string }\n | { type: 'reasoning-redacted'; data: string }\n // Token accounting for the turn. `cacheReadTokens`/`cacheWriteTokens` are present\n // only on providers that report prompt caching (R3-336) — they are what makes a\n // caching claim verifiable rather than believed, and their ABSENCE is meaningful:\n // it says this provider reports nothing, not that nothing was cached.\n | {\n type: 'usage';\n inputTokens: number;\n outputTokens: number;\n cacheReadTokens?: number;\n cacheWriteTokens?: number;\n };\n\n/** Why generation stopped: natural `end`, `length` cap, a `tool` call, or content `filtered`. */\nexport type ChatStopReason = 'end' | 'length' | 'tool' | 'filtered';\n\n/** The terminal value of the {@link chat} stream. */\nexport interface ChatResult {\n stopReason: ChatStopReason;\n}\n\n/**\n * Stream a chat completion from whichever provider the user has configured.\n *\n * ```ts\n * let summary = '';\n * for await (const d of chat({ messages: [{ role: 'user', content: [{ type: 'text', text }] }] })) {\n * if (d.type === 'text-delta') summary += d.text;\n * }\n * ```\n *\n * Requires the `llm:chat` capability. If no provider is bound, the host first\n * draws the SP-7 connect-me gate itself (R3-456: the app never draws a\n * credential prompt — that is host chrome, SECRETS_SPEC S3):\n * - the user connects a key → the call retries once and streams normally;\n * - the user declines → the generator throws `code: 'cancelled'` (the same code\n * a declined powerbox produces — a working degraded state: catch it and\n * degrade, e.g. skip the AI feature);\n * - an older host without the gate throws `code: 'provider-not-configured'`.\n * A signed-out user throws `code: 'auth-required'`; an un-granted call throws\n * `forbidden`.\n */\nexport function chat(req: ChatRequest): AsyncGenerator<ChatDelta, ChatResult, void> {\n // Peel `signal` out of the request before it becomes wire params — an AbortSignal\n // can't cross the postMessage boundary as data; it drives the SDK-side cancel frame.\n const { signal, ...params } = req;\n return invokeStream<ChatDelta, ChatResult>('llm:chat', params as unknown as Record<string, unknown>, signal);\n}\n\n/** The resolved provider's advertised abilities (SERVICE_PROVIDERS_SPEC §2.5) — read\n * to branch/degrade (offer image upload only when `vision`). */\nexport interface ChatFeatures {\n vision: boolean;\n tools: boolean;\n jsonMode: boolean;\n /** R3-335: the provider emits reasoning blocks. Read it to decide whether to render\n * a thinking surface at all — an empty affordance on a provider that never thinks\n * is worse than none. Normalized to `false` by the channel when a host predating\n * R3-335 omits it, so this is never `undefined` in practice. */\n reasoning: boolean;\n maxContextTokens: number;\n}\n\n/** Info about the provider the host resolved for this app. `null` when no provider\n * is bound (SP-7: prompt the user to add a key before calling {@link chat}). */\nexport interface ChatProviderInfo {\n /** Opaque provider id, e.g. `llm.chat.anthropic` — never a vendor secret or model id. */\n providerId: string;\n /** True for Host-proxied providers (host-vouched, SP-9); false for app-level ones,\n * whose `features` are an untrusted claim. */\n hostVouched: boolean;\n features: ChatFeatures;\n // NOTE (R3-300): `displayName`, `executor` and the resolved per-tier `models` belong\n // here — an app rendering provider state wants all three. They are NOT added yet,\n // deliberately: this interface IS the `llm-provider` channel's declared value, so\n // adding a field is a WIRE change, and the wire is owned by\n // `@immediately-run/sandbox-protocol` (descriptor edit → publish → pin bump on both\n // sides). The protocol snapshot gate enforces exactly that, and it is right to. The\n // enrichment rides R3-307's publish, which already has to touch those descriptors —\n // one publish for two additions rather than two.\n}\n\n/**\n * Whether the host has told us about a provider yet, and if so whether one is bound.\n *\n * THREE states, because two is the bug (R3-300). `describeChat()` returns `null` both\n * when no provider is configured AND when the channel has not answered — so an app\n * cannot tell \"you need a key\" from \"ask again in a moment\", and consuming apps\n * rendered a misleading \"connect a key\" banner at users who had one. `unknown` is the\n * state before the host answers; it is not an error and not a prompt to act.\n *\n * **`unknown` is TRANSIENT — the host answers every frame** (R3-419;\n * `LLM_AND_AGENTS_SPEC §4.1` R-LLM-1..3). An app that does not hold `llm:chat` is not\n * met with silence: it is answered `not-configured`, the same terminal state as a user\n * with no key, because from the app's side those are the same fact — do not render a\n * provider, do offer the connect path. So it is correct to treat a `unknown` that\n * persists as a host bug rather than as a state to design around, and WRONG to render a\n * spinner with no timeout on it. (Before R3-419 the host withheld the channel entirely\n * from an ungranted frame, and `unknown` stood forever — that is the failure this note\n * exists to keep from being re-created on the app side.)\n */\nexport type ChatProviderState =\n | { status: 'unknown' }\n | { status: 'not-configured' }\n | { status: 'configured'; provider: ChatProviderInfo };\n\n// The `llm-provider` describe channel (Recipe A): the host pushes the resolved\n// provider info on change and replays it on register-frame, gated by `llm:chat`.\n// A message with no `provider` key is ignored; an explicit `null` means \"no provider\n// bound\", which is now REPRESENTABLE as distinct from \"not yet answered\".\n// The channel's VALUE stays exactly what the wire carries — `ChatProviderInfo | null` —\n// because the wire did not change here and the protocol snapshot gate reads this type as\n// the channel's shape. The three-state lives BESIDE it: `answered` records whether the host\n// has ever spoken on this channel, which is the one bit `null` cannot carry. Deriving the\n// state rather than widening the channel keeps the wire contract byte-identical, which it\n// is (SDK_PACKAGING_SPEC §9: the wire is additive-only, and this is not a wire change).\n/**\n * Fill in feature flags a host older than the field does not send (R3-335).\n *\n * `features.reasoning` arrived after `ChatFeatures` shipped, so a host predating it\n * omits the key. `undefined` reads as falsy everywhere EXCEPT a `'reasoning' in\n * features` check, which is exactly the kind of difference that produces one wrong\n * branch a year later — so it is normalized here, once, rather than left to every\n * caller. Absent means \"does not reason\": the fail-closed reading.\n *\n * Exported for its own test; not part of the public surface (`index.ts` re-exports\n * this module wholesale, so it is reachable — it is documented as internal rather\n * than hidden behind a lie).\n * @internal\n */\nexport function normalizeProviderInfo(provider: ChatProviderInfo | null): ChatProviderInfo | null {\n if (!provider) return null;\n // The wire value is whatever the host sent, which may predate `reasoning` — so read\n // it as partial rather than trusting the declared type, and decide the flag explicitly.\n const wire = provider.features as Partial<ChatFeatures>;\n return { ...provider, features: { ...wire, reasoning: wire.reasoning === true } as ChatFeatures };\n}\n\nlet answered = false;\nconst channel = createPushChannel<ChatProviderInfo | null>({\n pushType: LLM_PROVIDER,\n requestType: REQUEST_LLM_PROVIDER,\n initial: null,\n parse: (msg) => {\n if (!('provider' in msg)) return undefined;\n answered = true;\n return normalizeProviderInfo((msg.provider as ChatProviderInfo | null) ?? null);\n },\n});\n\n/** Derive the three-state from the wire value plus whether the host has answered. */\nconst stateOf = (provider: ChatProviderInfo | null): ChatProviderState =>\n !answered ? { status: 'unknown' } : provider ? { status: 'configured', provider } : { status: 'not-configured' };\n\n/**\n * The provider the host resolved for this app, or `null`.\n *\n * Kept for compatibility (`ways_of_working §6`, additive-only): it collapses `unknown`\n * and `not-configured` to `null`. Prefer {@link describeChatState} when the difference\n * matters — which is any time you would render \"connect a key\", because doing that in\n * the `unknown` state is exactly the false banner R3-300 fixes.\n */\nexport const describeChat = (): ChatProviderInfo | null => channel.get();\n\n/** The three-state read: `unknown` before the host answers, then configured or not. */\nexport const describeChatState = (): ChatProviderState => stateOf(channel.get());\n\n/** Subscribe to provider changes (key added/revoked, preference changed). Invoked\n * immediately with the current value, then on every change. Returns unsubscribe. */\nexport const onChatProviderChange = (listener: (provider: ChatProviderInfo | null) => void): (() => void) =>\n channel.onChange(listener);\n\n/** Subscribe to the three-state provider description. */\nexport const onChatProviderStateChange = (listener: (state: ChatProviderState) => void): (() => void) =>\n channel.onChange((p) => listener(stateOf(p)));\n\n/** React hook returning the resolved chat provider (or `null`), re-rendering on\n * change — gate the summarize affordance on `provider !== null`. */\nexport const useChatProvider = (): ChatProviderInfo | null => channel.use();\n\n/**\n * React hook returning the three-state description.\n *\n * Use this to render provider state honestly: show nothing (or a neutral placeholder)\n * while `unknown`, the connect affordance only on `not-configured`, and the provider's\n * name on `configured`.\n */\nexport const useChatProviderState = (): ChatProviderState => stateOf(channel.use());\n"],"mappings":";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAeA,qBAA6B;AAC7B,yBAAkC;AAClC,sBAAmD;AAqH5C,SAAS,KAAK,KAA+D;AAGlF,QAAM,EAAE,QAAQ,GAAG,OAAO,IAAI;AAC9B,aAAO,6BAAoC,YAAY,QAA8C,MAAM;AAC7G;AAmFO,SAAS,sBAAsB,UAA4D;AAChG,MAAI,CAAC,SAAU,QAAO;AAGtB,QAAM,OAAO,SAAS;AACtB,SAAO,EAAE,GAAG,UAAU,UAAU,EAAE,GAAG,MAAM,WAAW,KAAK,cAAc,KAAK,EAAkB;AAClG;AAEA,IAAI,WAAW;AACf,MAAM,cAAU,sCAA2C;AAAA,EACzD,UAAU;AAAA,EACV,aAAa;AAAA,EACb,SAAS;AAAA,EACT,OAAO,CAAC,QAAQ;AACd,QAAI,EAAE,cAAc,KAAM,QAAO;AACjC,eAAW;AACX,WAAO,sBAAuB,IAAI,YAAwC,IAAI;AAAA,EAChF;AACF,CAAC;AAGD,MAAM,UAAU,CAAC,aACf,CAAC,WAAW,EAAE,QAAQ,UAAU,IAAI,WAAW,EAAE,QAAQ,cAAc,SAAS,IAAI,EAAE,QAAQ,iBAAiB;AAU1G,MAAM,eAAe,MAA+B,QAAQ,IAAI;AAGhE,MAAM,oBAAoB,MAAyB,QAAQ,QAAQ,IAAI,CAAC;AAIxE,MAAM,uBAAuB,CAAC,aACnC,QAAQ,SAAS,QAAQ;AAGpB,MAAM,4BAA4B,CAAC,aACxC,QAAQ,SAAS,CAAC,MAAM,SAAS,QAAQ,CAAC,CAAC,CAAC;AAIvC,MAAM,kBAAkB,MAA+B,QAAQ,IAAI;AASnE,MAAM,uBAAuB,MAAyB,QAAQ,QAAQ,IAAI,CAAC;","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/llm.ts"],"sourcesContent":["// Provider-agnostic LLM chat — the `llm.chat@1` slot (SERVICE_PROVIDERS_SPEC;\n// LLM_AND_AGENTS_SPEC §8 D5).\n//\n// An app calls ONE chat slot and never worries about which provider the user has a\n// key for: the HOST resolves which vendor answers from the key the user holds\n// (`SecretView.boundOrigin`) plus their `preferredImplementation` choice, normalizes\n// the wire format, injects the key host-side at the §6 net:fetch point (the\n// look-at-nothing proxy), and streams normalized deltas back. The app never names a\n// vendor, never sees the key, and needs NO `net:fetch`/`secrets` grant of its own —\n// only the `llm:chat` capability (elevated, app-scoped: a fork earns it by consent).\n//\n// Inert until the host implements `protocol-llm` (the `chat` stream) + the\n// `llm-provider` describe channel; the contract ships here so apps (the file-explorer\n// summarize fork) can be written against it — exactly how `secrets.ts` shipped ahead\n// of `protocol-secrets`.\nimport { invokeStream } from './catalog';\nimport { createPushChannel } from './pushChannel';\nimport { LLM_PROVIDER, REQUEST_LLM_PROVIDER } from './generated/protocol';\n\n/** Who authored a {@link ChatMessage}. */\nexport type ChatRole = 'system' | 'user' | 'assistant' | 'tool';\n\n/** A part of a message. `image` is only honored when the resolved provider\n * advertises `features.vision` (§2.5); `tool-use`/`tool-result` only when it\n * advertises `features.tools` — branch on {@link describeChat} first. */\nexport type ContentPart =\n | { type: 'text'; text: string }\n | { type: 'image'; mimeType: string; data: string } // data: base64, no data: URL prefix\n // A tool call the model emitted on a prior `assistant` turn — replay it in the\n // conversation so a follow-up request carries the agentic history. Pairs with the\n // streamed `tool-call` {@link ChatDelta} that first surfaced it.\n | { type: 'tool-use'; id: string; name: string; input: Record<string, unknown> }\n // A block of the model's own REASONING from a prior `assistant` turn (R3-335).\n // Honored only when the resolved provider advertises `features.reasoning`.\n //\n // Echo these back. On some providers a reasoning block must be replayed — with its\n // `signature` intact and BEFORE the turn's text/tool-use — for the following turn to\n // be accepted at all; a loop that drops them is quietly lossy across turns in a way\n // that shows up as degraded output rather than an error. Pairs with the streamed\n // `reasoning` {@link ChatDelta}.\n | { type: 'reasoning'; text: string; signature?: string }\n // Reasoning the provider REDACTED: opaque bytes with no readable text, which still\n // have to be echoed back in place to keep the chain valid. Never render it.\n | { type: 'reasoning-redacted'; data: string }\n // The result of executing a `tool-use`, fed back so the model can continue. Carried\n // on a `user`/`tool`-role message; `toolCallId` matches the `tool-use` `id`.\n | { type: 'tool-result'; toolCallId: string; content: string; isError?: boolean };\n\n/** One message in a {@link ChatRequest}: a role plus its content parts. */\nexport interface ChatMessage {\n role: ChatRole;\n content: ContentPart[];\n}\n\n/** A tool the model may call — honored only when `features.tools`. */\nexport interface ToolDef {\n name: string;\n description?: string;\n /** JSON-Schema for the tool's arguments. */\n inputSchema: Record<string, unknown>;\n}\n\n/** A host-brokered chat completion request: the messages plus optional tools,\n * response format, and model hint (each honored per the provider's features). */\nexport interface ChatRequest {\n messages: ChatMessage[];\n /** Honored only when the resolved provider advertises `features.tools`. */\n tools?: ToolDef[];\n /** `'json'` honored only when `features.jsonMode`. Defaults to `'text'`. */\n responseFormat?: 'text' | 'json';\n maxTokens?: number;\n /** An ABSTRACT tier hint, never a vendor model id — the host maps it to a concrete\n * model on the resolved provider. Omit to take the provider's default. */\n modelHint?: 'fast' | 'smart';\n /** Abort the completion mid-stream. When it fires, the SDK sends the host a cancel\n * frame so the host aborts the upstream provider request and STOPS BILLING the\n * user's key — not merely stops the app-side iterator (LLM_AND_AGENTS_SPEC §3.3\n * \"abort the in-flight LLM request\", R3-224). Not sent over the wire (an\n * `AbortSignal` isn't serializable); handled SDK-side. */\n signal?: AbortSignal;\n}\n\n/** One streamed chunk. Consumers typically accumulate `text-delta`s. */\nexport type ChatDelta =\n | { type: 'text-delta'; text: string }\n | { type: 'tool-call'; id: string; name: string; input: unknown }\n // R3-335 — the model's reasoning as it streams. `reasoning-delta` carries the text\n // incrementally (render it live); the terminal `reasoning` carries the WHOLE block\n // plus the `signature` the provider may require on the echo, and is what a caller\n // should put back into the conversation. A provider without reasoning emits neither.\n | { type: 'reasoning-delta'; text: string }\n | { type: 'reasoning'; text: string; signature?: string }\n | { type: 'reasoning-redacted'; data: string }\n // Token accounting for the turn. `cacheReadTokens`/`cacheWriteTokens` are present\n // only on providers that report prompt caching (R3-336) — they are what makes a\n // caching claim verifiable rather than believed, and their ABSENCE is meaningful:\n // it says this provider reports nothing, not that nothing was cached.\n | {\n type: 'usage';\n inputTokens: number;\n outputTokens: number;\n cacheReadTokens?: number;\n cacheWriteTokens?: number;\n };\n\n/** Why generation stopped: natural `end`, `length` cap, a `tool` call, or content `filtered`. */\nexport type ChatStopReason = 'end' | 'length' | 'tool' | 'filtered';\n\n/** The terminal value of the {@link chat} stream. */\nexport interface ChatResult {\n stopReason: ChatStopReason;\n}\n\n/**\n * Stream a chat completion from whichever provider the user has configured.\n *\n * ```ts\n * let summary = '';\n * for await (const d of chat({ messages: [{ role: 'user', content: [{ type: 'text', text }] }] })) {\n * if (d.type === 'text-delta') summary += d.text;\n * }\n * ```\n *\n * Requires the `llm:chat` capability. If no provider is bound, the host first\n * draws the SP-7 connect-me gate itself (R3-456: the app never draws a\n * credential prompt — that is host chrome, SECRETS_SPEC S3):\n * - the user connects a key → the call retries once and streams normally;\n * - the user declines → the generator throws `code: 'cancelled'` (the same code\n * a declined powerbox produces — a working degraded state: catch it and\n * degrade, e.g. skip the AI feature);\n * - an older host without the gate throws `code: 'provider-not-configured'`.\n * A signed-out user throws `code: 'auth-required'`; an un-granted call throws\n * `forbidden`.\n */\nexport function chat(req: ChatRequest): AsyncGenerator<ChatDelta, ChatResult, void> {\n // Peel `signal` out of the request before it becomes wire params — an AbortSignal\n // can't cross the postMessage boundary as data; it drives the SDK-side cancel frame.\n const { signal, ...params } = req;\n return invokeStream<ChatDelta, ChatResult>('llm:chat', params as unknown as Record<string, unknown>, signal);\n}\n\n/** The resolved provider's advertised abilities (SERVICE_PROVIDERS_SPEC §2.5) — read\n * to branch/degrade (offer image upload only when `vision`). */\nexport interface ChatFeatures {\n vision: boolean;\n tools: boolean;\n jsonMode: boolean;\n /** R3-335: the provider emits reasoning blocks. Read it to decide whether to render\n * a thinking surface at all — an empty affordance on a provider that never thinks\n * is worse than none. Normalized to `false` by the channel when a host predating\n * R3-335 omits it, so this is never `undefined` in practice. */\n reasoning: boolean;\n maxContextTokens: number;\n}\n\n/** How the resolved provider's requests physically leave the browser. Read it to\n * DESCRIBE routing where that matters to the person (\"runs in your browser\" vs\n * \"routed through immediately.run\") — never to draw host chrome or a consent prompt,\n * which remain the host's (UI_AS_APPS §8 T15). */\nexport type ChatExecutor = 'browser-direct' | 'backend-proxied';\n\n/** The concrete model each abstract tier resolves to right now. */\nexport interface ChatTierModels {\n fast: string;\n smart: string;\n}\n\n/** Info about the provider the host resolved for this app. `null` when no provider\n * is bound (SP-7: prompt the user to add a key before calling {@link chat}). */\nexport interface ChatProviderInfo {\n /** Opaque provider id, e.g. `llm.chat.anthropic` — never a vendor secret or model id. */\n providerId: string;\n /** True for Host-proxied providers (host-vouched, SP-9); false for app-level ones,\n * whose `features` are an untrusted claim. */\n hostVouched: boolean;\n features: ChatFeatures;\n /** The provider's human name, e.g. `OpenRouter` — what to put in front of a person.\n * Absent on a host that predates the field: fall back to your own copy rather than\n * rendering the id, which is a platform identifier and not a name. */\n displayName?: string;\n /** How this provider's requests leave the browser. Absent on a host that predates the\n * field, which is NOT the same as `browser-direct` — say nothing about routing rather\n * than guess at it. */\n executor?: ChatExecutor;\n /**\n * The concrete model each {@link ChatRequest.modelHint} tier resolves to — what a\n * `smart` request would actually run, after the user's own preference.\n *\n * Read-only, and it does not weaken `LLM_AND_AGENTS_SPEC §0`: an app still names no\n * model, and {@link ChatRequest} still carries only the abstract hint. It is here so an\n * app can be HONEST about what answered — a transcript that says which model wrote a\n * reply, a warning that names the model about to be spent on — instead of showing a\n * blank where the platform knows the answer. The user picks the model in host settings;\n * the app reports it.\n *\n * Absent on a host that predates the field. It changes when the user changes their\n * preference, so read it through {@link onChatProviderChange} rather than caching it.\n */\n models?: ChatTierModels;\n}\n\n/**\n * Whether the host has told us about a provider yet, and if so whether one is bound.\n *\n * THREE states, because two is the bug (R3-300). `describeChat()` returns `null` both\n * when no provider is configured AND when the channel has not answered — so an app\n * cannot tell \"you need a key\" from \"ask again in a moment\", and consuming apps\n * rendered a misleading \"connect a key\" banner at users who had one. `unknown` is the\n * state before the host answers; it is not an error and not a prompt to act.\n *\n * **`unknown` is TRANSIENT — the host answers every frame** (R3-419;\n * `LLM_AND_AGENTS_SPEC §4.1` R-LLM-1..3). An app that does not hold `llm:chat` is not\n * met with silence: it is answered `not-configured`, the same terminal state as a user\n * with no key, because from the app's side those are the same fact — do not render a\n * provider, do offer the connect path. So it is correct to treat a `unknown` that\n * persists as a host bug rather than as a state to design around, and WRONG to render a\n * spinner with no timeout on it. (Before R3-419 the host withheld the channel entirely\n * from an ungranted frame, and `unknown` stood forever — that is the failure this note\n * exists to keep from being re-created on the app side.)\n */\nexport type ChatProviderState =\n | { status: 'unknown' }\n | { status: 'not-configured' }\n | { status: 'configured'; provider: ChatProviderInfo };\n\n// The `llm-provider` describe channel (Recipe A): the host pushes the resolved\n// provider info on change and replays it on register-frame, gated by `llm:chat`.\n// A message with no `provider` key is ignored; an explicit `null` means \"no provider\n// bound\", which is now REPRESENTABLE as distinct from \"not yet answered\".\n// The channel's VALUE stays exactly what the wire carries — `ChatProviderInfo | null` —\n// because the wire did not change here and the protocol snapshot gate reads this type as\n// the channel's shape. The three-state lives BESIDE it: `answered` records whether the host\n// has ever spoken on this channel, which is the one bit `null` cannot carry. Deriving the\n// state rather than widening the channel keeps the wire contract byte-identical, which it\n// is (SDK_PACKAGING_SPEC §9: the wire is additive-only, and this is not a wire change).\n/**\n * Reconcile what the host actually sent with what this SDK declares.\n *\n * `features.reasoning` arrived after `ChatFeatures` shipped, so a host predating it\n * omits the key. `undefined` reads as falsy everywhere EXCEPT a `'reasoning' in\n * features` check, which is exactly the kind of difference that produces one wrong\n * branch a year later — so it is normalized here, once, rather than left to every\n * caller. Absent means \"does not reason\": the fail-closed reading.\n *\n * `displayName`, `executor` and `models` arrived later still, and for them absence is a\n * REAL answer an app is told to handle (\"this host does not say\"), so they are left\n * absent rather than filled in. What is dropped is a value that is present but not\n * usable — an `executor` outside the union, a `models` missing a tier — because a\n * half-answer rendered as fact is worse than the honest blank the app already handles.\n *\n * Exported for its own test; not part of the public surface (`index.ts` re-exports\n * this module wholesale, so it is reachable — it is documented as internal rather\n * than hidden behind a lie).\n * @internal\n */\nconst EXECUTORS: readonly ChatExecutor[] = ['browser-direct', 'backend-proxied'];\n\nconst usableModels = (raw: unknown): ChatTierModels | undefined => {\n if (!raw || typeof raw !== 'object') return undefined;\n const { fast, smart } = raw as Partial<ChatTierModels>;\n return typeof fast === 'string' && fast && typeof smart === 'string' && smart ? { fast, smart } : undefined;\n};\n\nexport function normalizeProviderInfo(provider: ChatProviderInfo | null): ChatProviderInfo | null {\n if (!provider) return null;\n // The three later fields are taken OFF the value and put back only if usable — spreading\n // and then overwriting would leave an unusable key present, and `key in provider` is\n // exactly how an app is told to ask whether the host said anything.\n const { displayName: rawName, executor: rawExecutor, models: rawModels, ...rest } = provider;\n // The wire value is whatever the host sent, which may predate any of these fields — so\n // read it as partial rather than trusting the declared type, and decide each explicitly.\n const wire = provider.features as Partial<ChatFeatures>;\n const executor = EXECUTORS.includes(rawExecutor as ChatExecutor) ? (rawExecutor as ChatExecutor) : undefined;\n const displayName = typeof rawName === 'string' && rawName ? rawName : undefined;\n const models = usableModels(rawModels);\n return {\n ...rest,\n features: { ...wire, reasoning: wire.reasoning === true } as ChatFeatures,\n ...(displayName ? { displayName } : {}),\n ...(executor ? { executor } : {}),\n ...(models ? { models } : {}),\n };\n}\n\nlet answered = false;\nconst channel = createPushChannel<ChatProviderInfo | null>({\n pushType: LLM_PROVIDER,\n requestType: REQUEST_LLM_PROVIDER,\n initial: null,\n parse: (msg) => {\n if (!('provider' in msg)) return undefined;\n answered = true;\n return normalizeProviderInfo((msg.provider as ChatProviderInfo | null) ?? null);\n },\n});\n\n/** Derive the three-state from the wire value plus whether the host has answered. */\nconst stateOf = (provider: ChatProviderInfo | null): ChatProviderState =>\n !answered ? { status: 'unknown' } : provider ? { status: 'configured', provider } : { status: 'not-configured' };\n\n/**\n * The provider the host resolved for this app, or `null`.\n *\n * Kept for compatibility (`ways_of_working §6`, additive-only): it collapses `unknown`\n * and `not-configured` to `null`. Prefer {@link describeChatState} when the difference\n * matters — which is any time you would render \"connect a key\", because doing that in\n * the `unknown` state is exactly the false banner R3-300 fixes.\n */\nexport const describeChat = (): ChatProviderInfo | null => channel.get();\n\n/** The three-state read: `unknown` before the host answers, then configured or not. */\nexport const describeChatState = (): ChatProviderState => stateOf(channel.get());\n\n/** Subscribe to provider changes (key added/revoked, preference changed). Invoked\n * immediately with the current value, then on every change. Returns unsubscribe. */\nexport const onChatProviderChange = (listener: (provider: ChatProviderInfo | null) => void): (() => void) =>\n channel.onChange(listener);\n\n/** Subscribe to the three-state provider description. */\nexport const onChatProviderStateChange = (listener: (state: ChatProviderState) => void): (() => void) =>\n channel.onChange((p) => listener(stateOf(p)));\n\n/** React hook returning the resolved chat provider (or `null`), re-rendering on\n * change — gate the summarize affordance on `provider !== null`. */\nexport const useChatProvider = (): ChatProviderInfo | null => channel.use();\n\n/**\n * React hook returning the three-state description.\n *\n * Use this to render provider state honestly: show nothing (or a neutral placeholder)\n * while `unknown`, the connect affordance only on `not-configured`, and the provider's\n * name on `configured`.\n */\nexport const useChatProviderState = (): ChatProviderState => stateOf(channel.use());\n"],"mappings":";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAeA,qBAA6B;AAC7B,yBAAkC;AAClC,sBAAmD;AAqH5C,SAAS,KAAK,KAA+D;AAGlF,QAAM,EAAE,QAAQ,GAAG,OAAO,IAAI;AAC9B,aAAO,6BAAoC,YAAY,QAA8C,MAAM;AAC7G;AAoHA,MAAM,YAAqC,CAAC,kBAAkB,iBAAiB;AAE/E,MAAM,eAAe,CAAC,QAA6C;AACjE,MAAI,CAAC,OAAO,OAAO,QAAQ,SAAU,QAAO;AAC5C,QAAM,EAAE,MAAM,MAAM,IAAI;AACxB,SAAO,OAAO,SAAS,YAAY,QAAQ,OAAO,UAAU,YAAY,QAAQ,EAAE,MAAM,MAAM,IAAI;AACpG;AAEO,SAAS,sBAAsB,UAA4D;AAChG,MAAI,CAAC,SAAU,QAAO;AAItB,QAAM,EAAE,aAAa,SAAS,UAAU,aAAa,QAAQ,WAAW,GAAG,KAAK,IAAI;AAGpF,QAAM,OAAO,SAAS;AACtB,QAAM,WAAW,UAAU,SAAS,WAA2B,IAAK,cAA+B;AACnG,QAAM,cAAc,OAAO,YAAY,YAAY,UAAU,UAAU;AACvE,QAAM,SAAS,aAAa,SAAS;AACrC,SAAO;AAAA,IACL,GAAG;AAAA,IACH,UAAU,EAAE,GAAG,MAAM,WAAW,KAAK,cAAc,KAAK;AAAA,IACxD,GAAI,cAAc,EAAE,YAAY,IAAI,CAAC;AAAA,IACrC,GAAI,WAAW,EAAE,SAAS,IAAI,CAAC;AAAA,IAC/B,GAAI,SAAS,EAAE,OAAO,IAAI,CAAC;AAAA,EAC7B;AACF;AAEA,IAAI,WAAW;AACf,MAAM,cAAU,sCAA2C;AAAA,EACzD,UAAU;AAAA,EACV,aAAa;AAAA,EACb,SAAS;AAAA,EACT,OAAO,CAAC,QAAQ;AACd,QAAI,EAAE,cAAc,KAAM,QAAO;AACjC,eAAW;AACX,WAAO,sBAAuB,IAAI,YAAwC,IAAI;AAAA,EAChF;AACF,CAAC;AAGD,MAAM,UAAU,CAAC,aACf,CAAC,WAAW,EAAE,QAAQ,UAAU,IAAI,WAAW,EAAE,QAAQ,cAAc,SAAS,IAAI,EAAE,QAAQ,iBAAiB;AAU1G,MAAM,eAAe,MAA+B,QAAQ,IAAI;AAGhE,MAAM,oBAAoB,MAAyB,QAAQ,QAAQ,IAAI,CAAC;AAIxE,MAAM,uBAAuB,CAAC,aACnC,QAAQ,SAAS,QAAQ;AAGpB,MAAM,4BAA4B,CAAC,aACxC,QAAQ,SAAS,CAAC,MAAM,SAAS,QAAQ,CAAC,CAAC,CAAC;AAIvC,MAAM,kBAAkB,MAA+B,QAAQ,IAAI;AASnE,MAAM,uBAAuB,MAAyB,QAAQ,QAAQ,IAAI,CAAC;","names":[]}
|
package/dist/llm.d.cts
CHANGED
|
@@ -126,6 +126,16 @@ interface ChatFeatures {
|
|
|
126
126
|
reasoning: boolean;
|
|
127
127
|
maxContextTokens: number;
|
|
128
128
|
}
|
|
129
|
+
/** How the resolved provider's requests physically leave the browser. Read it to
|
|
130
|
+
* DESCRIBE routing where that matters to the person ("runs in your browser" vs
|
|
131
|
+
* "routed through immediately.run") — never to draw host chrome or a consent prompt,
|
|
132
|
+
* which remain the host's (UI_AS_APPS §8 T15). */
|
|
133
|
+
type ChatExecutor = 'browser-direct' | 'backend-proxied';
|
|
134
|
+
/** The concrete model each abstract tier resolves to right now. */
|
|
135
|
+
interface ChatTierModels {
|
|
136
|
+
fast: string;
|
|
137
|
+
smart: string;
|
|
138
|
+
}
|
|
129
139
|
/** Info about the provider the host resolved for this app. `null` when no provider
|
|
130
140
|
* is bound (SP-7: prompt the user to add a key before calling {@link chat}). */
|
|
131
141
|
interface ChatProviderInfo {
|
|
@@ -135,6 +145,29 @@ interface ChatProviderInfo {
|
|
|
135
145
|
* whose `features` are an untrusted claim. */
|
|
136
146
|
hostVouched: boolean;
|
|
137
147
|
features: ChatFeatures;
|
|
148
|
+
/** The provider's human name, e.g. `OpenRouter` — what to put in front of a person.
|
|
149
|
+
* Absent on a host that predates the field: fall back to your own copy rather than
|
|
150
|
+
* rendering the id, which is a platform identifier and not a name. */
|
|
151
|
+
displayName?: string;
|
|
152
|
+
/** How this provider's requests leave the browser. Absent on a host that predates the
|
|
153
|
+
* field, which is NOT the same as `browser-direct` — say nothing about routing rather
|
|
154
|
+
* than guess at it. */
|
|
155
|
+
executor?: ChatExecutor;
|
|
156
|
+
/**
|
|
157
|
+
* The concrete model each {@link ChatRequest.modelHint} tier resolves to — what a
|
|
158
|
+
* `smart` request would actually run, after the user's own preference.
|
|
159
|
+
*
|
|
160
|
+
* Read-only, and it does not weaken `LLM_AND_AGENTS_SPEC §0`: an app still names no
|
|
161
|
+
* model, and {@link ChatRequest} still carries only the abstract hint. It is here so an
|
|
162
|
+
* app can be HONEST about what answered — a transcript that says which model wrote a
|
|
163
|
+
* reply, a warning that names the model about to be spent on — instead of showing a
|
|
164
|
+
* blank where the platform knows the answer. The user picks the model in host settings;
|
|
165
|
+
* the app reports it.
|
|
166
|
+
*
|
|
167
|
+
* Absent on a host that predates the field. It changes when the user changes their
|
|
168
|
+
* preference, so read it through {@link onChatProviderChange} rather than caching it.
|
|
169
|
+
*/
|
|
170
|
+
models?: ChatTierModels;
|
|
138
171
|
}
|
|
139
172
|
/**
|
|
140
173
|
* Whether the host has told us about a provider yet, and if so whether one is bound.
|
|
@@ -163,20 +196,6 @@ type ChatProviderState = {
|
|
|
163
196
|
status: 'configured';
|
|
164
197
|
provider: ChatProviderInfo;
|
|
165
198
|
};
|
|
166
|
-
/**
|
|
167
|
-
* Fill in feature flags a host older than the field does not send (R3-335).
|
|
168
|
-
*
|
|
169
|
-
* `features.reasoning` arrived after `ChatFeatures` shipped, so a host predating it
|
|
170
|
-
* omits the key. `undefined` reads as falsy everywhere EXCEPT a `'reasoning' in
|
|
171
|
-
* features` check, which is exactly the kind of difference that produces one wrong
|
|
172
|
-
* branch a year later — so it is normalized here, once, rather than left to every
|
|
173
|
-
* caller. Absent means "does not reason": the fail-closed reading.
|
|
174
|
-
*
|
|
175
|
-
* Exported for its own test; not part of the public surface (`index.ts` re-exports
|
|
176
|
-
* this module wholesale, so it is reachable — it is documented as internal rather
|
|
177
|
-
* than hidden behind a lie).
|
|
178
|
-
* @internal
|
|
179
|
-
*/
|
|
180
199
|
declare function normalizeProviderInfo(provider: ChatProviderInfo | null): ChatProviderInfo | null;
|
|
181
200
|
/**
|
|
182
201
|
* The provider the host resolved for this app, or `null`.
|
|
@@ -206,4 +225,4 @@ declare const useChatProvider: () => ChatProviderInfo | null;
|
|
|
206
225
|
*/
|
|
207
226
|
declare const useChatProviderState: () => ChatProviderState;
|
|
208
227
|
|
|
209
|
-
export { type ChatDelta, type ChatFeatures, type ChatMessage, type ChatProviderInfo, type ChatProviderState, type ChatRequest, type ChatResult, type ChatRole, type ChatStopReason, type ContentPart, type ToolDef, chat, describeChat, describeChatState, normalizeProviderInfo, onChatProviderChange, onChatProviderStateChange, useChatProvider, useChatProviderState };
|
|
228
|
+
export { type ChatDelta, type ChatExecutor, type ChatFeatures, type ChatMessage, type ChatProviderInfo, type ChatProviderState, type ChatRequest, type ChatResult, type ChatRole, type ChatStopReason, type ChatTierModels, type ContentPart, type ToolDef, chat, describeChat, describeChatState, normalizeProviderInfo, onChatProviderChange, onChatProviderStateChange, useChatProvider, useChatProviderState };
|
package/dist/llm.d.ts
CHANGED
|
@@ -126,6 +126,16 @@ interface ChatFeatures {
|
|
|
126
126
|
reasoning: boolean;
|
|
127
127
|
maxContextTokens: number;
|
|
128
128
|
}
|
|
129
|
+
/** How the resolved provider's requests physically leave the browser. Read it to
|
|
130
|
+
* DESCRIBE routing where that matters to the person ("runs in your browser" vs
|
|
131
|
+
* "routed through immediately.run") — never to draw host chrome or a consent prompt,
|
|
132
|
+
* which remain the host's (UI_AS_APPS §8 T15). */
|
|
133
|
+
type ChatExecutor = 'browser-direct' | 'backend-proxied';
|
|
134
|
+
/** The concrete model each abstract tier resolves to right now. */
|
|
135
|
+
interface ChatTierModels {
|
|
136
|
+
fast: string;
|
|
137
|
+
smart: string;
|
|
138
|
+
}
|
|
129
139
|
/** Info about the provider the host resolved for this app. `null` when no provider
|
|
130
140
|
* is bound (SP-7: prompt the user to add a key before calling {@link chat}). */
|
|
131
141
|
interface ChatProviderInfo {
|
|
@@ -135,6 +145,29 @@ interface ChatProviderInfo {
|
|
|
135
145
|
* whose `features` are an untrusted claim. */
|
|
136
146
|
hostVouched: boolean;
|
|
137
147
|
features: ChatFeatures;
|
|
148
|
+
/** The provider's human name, e.g. `OpenRouter` — what to put in front of a person.
|
|
149
|
+
* Absent on a host that predates the field: fall back to your own copy rather than
|
|
150
|
+
* rendering the id, which is a platform identifier and not a name. */
|
|
151
|
+
displayName?: string;
|
|
152
|
+
/** How this provider's requests leave the browser. Absent on a host that predates the
|
|
153
|
+
* field, which is NOT the same as `browser-direct` — say nothing about routing rather
|
|
154
|
+
* than guess at it. */
|
|
155
|
+
executor?: ChatExecutor;
|
|
156
|
+
/**
|
|
157
|
+
* The concrete model each {@link ChatRequest.modelHint} tier resolves to — what a
|
|
158
|
+
* `smart` request would actually run, after the user's own preference.
|
|
159
|
+
*
|
|
160
|
+
* Read-only, and it does not weaken `LLM_AND_AGENTS_SPEC §0`: an app still names no
|
|
161
|
+
* model, and {@link ChatRequest} still carries only the abstract hint. It is here so an
|
|
162
|
+
* app can be HONEST about what answered — a transcript that says which model wrote a
|
|
163
|
+
* reply, a warning that names the model about to be spent on — instead of showing a
|
|
164
|
+
* blank where the platform knows the answer. The user picks the model in host settings;
|
|
165
|
+
* the app reports it.
|
|
166
|
+
*
|
|
167
|
+
* Absent on a host that predates the field. It changes when the user changes their
|
|
168
|
+
* preference, so read it through {@link onChatProviderChange} rather than caching it.
|
|
169
|
+
*/
|
|
170
|
+
models?: ChatTierModels;
|
|
138
171
|
}
|
|
139
172
|
/**
|
|
140
173
|
* Whether the host has told us about a provider yet, and if so whether one is bound.
|
|
@@ -163,20 +196,6 @@ type ChatProviderState = {
|
|
|
163
196
|
status: 'configured';
|
|
164
197
|
provider: ChatProviderInfo;
|
|
165
198
|
};
|
|
166
|
-
/**
|
|
167
|
-
* Fill in feature flags a host older than the field does not send (R3-335).
|
|
168
|
-
*
|
|
169
|
-
* `features.reasoning` arrived after `ChatFeatures` shipped, so a host predating it
|
|
170
|
-
* omits the key. `undefined` reads as falsy everywhere EXCEPT a `'reasoning' in
|
|
171
|
-
* features` check, which is exactly the kind of difference that produces one wrong
|
|
172
|
-
* branch a year later — so it is normalized here, once, rather than left to every
|
|
173
|
-
* caller. Absent means "does not reason": the fail-closed reading.
|
|
174
|
-
*
|
|
175
|
-
* Exported for its own test; not part of the public surface (`index.ts` re-exports
|
|
176
|
-
* this module wholesale, so it is reachable — it is documented as internal rather
|
|
177
|
-
* than hidden behind a lie).
|
|
178
|
-
* @internal
|
|
179
|
-
*/
|
|
180
199
|
declare function normalizeProviderInfo(provider: ChatProviderInfo | null): ChatProviderInfo | null;
|
|
181
200
|
/**
|
|
182
201
|
* The provider the host resolved for this app, or `null`.
|
|
@@ -206,4 +225,4 @@ declare const useChatProvider: () => ChatProviderInfo | null;
|
|
|
206
225
|
*/
|
|
207
226
|
declare const useChatProviderState: () => ChatProviderState;
|
|
208
227
|
|
|
209
|
-
export { type ChatDelta, type ChatFeatures, type ChatMessage, type ChatProviderInfo, type ChatProviderState, type ChatRequest, type ChatResult, type ChatRole, type ChatStopReason, type ContentPart, type ToolDef, chat, describeChat, describeChatState, normalizeProviderInfo, onChatProviderChange, onChatProviderStateChange, useChatProvider, useChatProviderState };
|
|
228
|
+
export { type ChatDelta, type ChatExecutor, type ChatFeatures, type ChatMessage, type ChatProviderInfo, type ChatProviderState, type ChatRequest, type ChatResult, type ChatRole, type ChatStopReason, type ChatTierModels, type ContentPart, type ToolDef, chat, describeChat, describeChatState, normalizeProviderInfo, onChatProviderChange, onChatProviderStateChange, useChatProvider, useChatProviderState };
|
package/dist/llm.js
CHANGED
|
@@ -6,10 +6,26 @@ function chat(req) {
|
|
|
6
6
|
const { signal, ...params } = req;
|
|
7
7
|
return invokeStream("llm:chat", params, signal);
|
|
8
8
|
}
|
|
9
|
+
const EXECUTORS = ["browser-direct", "backend-proxied"];
|
|
10
|
+
const usableModels = (raw) => {
|
|
11
|
+
if (!raw || typeof raw !== "object") return void 0;
|
|
12
|
+
const { fast, smart } = raw;
|
|
13
|
+
return typeof fast === "string" && fast && typeof smart === "string" && smart ? { fast, smart } : void 0;
|
|
14
|
+
};
|
|
9
15
|
function normalizeProviderInfo(provider) {
|
|
10
16
|
if (!provider) return null;
|
|
17
|
+
const { displayName: rawName, executor: rawExecutor, models: rawModels, ...rest } = provider;
|
|
11
18
|
const wire = provider.features;
|
|
12
|
-
|
|
19
|
+
const executor = EXECUTORS.includes(rawExecutor) ? rawExecutor : void 0;
|
|
20
|
+
const displayName = typeof rawName === "string" && rawName ? rawName : void 0;
|
|
21
|
+
const models = usableModels(rawModels);
|
|
22
|
+
return {
|
|
23
|
+
...rest,
|
|
24
|
+
features: { ...wire, reasoning: wire.reasoning === true },
|
|
25
|
+
...displayName ? { displayName } : {},
|
|
26
|
+
...executor ? { executor } : {},
|
|
27
|
+
...models ? { models } : {}
|
|
28
|
+
};
|
|
13
29
|
}
|
|
14
30
|
let answered = false;
|
|
15
31
|
const channel = createPushChannel({
|
package/dist/llm.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/llm.ts"],"sourcesContent":["// Provider-agnostic LLM chat — the `llm.chat@1` slot (SERVICE_PROVIDERS_SPEC;\n// LLM_AND_AGENTS_SPEC §8 D5).\n//\n// An app calls ONE chat slot and never worries about which provider the user has a\n// key for: the HOST resolves which vendor answers from the key the user holds\n// (`SecretView.boundOrigin`) plus their `preferredImplementation` choice, normalizes\n// the wire format, injects the key host-side at the §6 net:fetch point (the\n// look-at-nothing proxy), and streams normalized deltas back. The app never names a\n// vendor, never sees the key, and needs NO `net:fetch`/`secrets` grant of its own —\n// only the `llm:chat` capability (elevated, app-scoped: a fork earns it by consent).\n//\n// Inert until the host implements `protocol-llm` (the `chat` stream) + the\n// `llm-provider` describe channel; the contract ships here so apps (the file-explorer\n// summarize fork) can be written against it — exactly how `secrets.ts` shipped ahead\n// of `protocol-secrets`.\nimport { invokeStream } from './catalog';\nimport { createPushChannel } from './pushChannel';\nimport { LLM_PROVIDER, REQUEST_LLM_PROVIDER } from './generated/protocol';\n\n/** Who authored a {@link ChatMessage}. */\nexport type ChatRole = 'system' | 'user' | 'assistant' | 'tool';\n\n/** A part of a message. `image` is only honored when the resolved provider\n * advertises `features.vision` (§2.5); `tool-use`/`tool-result` only when it\n * advertises `features.tools` — branch on {@link describeChat} first. */\nexport type ContentPart =\n | { type: 'text'; text: string }\n | { type: 'image'; mimeType: string; data: string } // data: base64, no data: URL prefix\n // A tool call the model emitted on a prior `assistant` turn — replay it in the\n // conversation so a follow-up request carries the agentic history. Pairs with the\n // streamed `tool-call` {@link ChatDelta} that first surfaced it.\n | { type: 'tool-use'; id: string; name: string; input: Record<string, unknown> }\n // A block of the model's own REASONING from a prior `assistant` turn (R3-335).\n // Honored only when the resolved provider advertises `features.reasoning`.\n //\n // Echo these back. On some providers a reasoning block must be replayed — with its\n // `signature` intact and BEFORE the turn's text/tool-use — for the following turn to\n // be accepted at all; a loop that drops them is quietly lossy across turns in a way\n // that shows up as degraded output rather than an error. Pairs with the streamed\n // `reasoning` {@link ChatDelta}.\n | { type: 'reasoning'; text: string; signature?: string }\n // Reasoning the provider REDACTED: opaque bytes with no readable text, which still\n // have to be echoed back in place to keep the chain valid. Never render it.\n | { type: 'reasoning-redacted'; data: string }\n // The result of executing a `tool-use`, fed back so the model can continue. Carried\n // on a `user`/`tool`-role message; `toolCallId` matches the `tool-use` `id`.\n | { type: 'tool-result'; toolCallId: string; content: string; isError?: boolean };\n\n/** One message in a {@link ChatRequest}: a role plus its content parts. */\nexport interface ChatMessage {\n role: ChatRole;\n content: ContentPart[];\n}\n\n/** A tool the model may call — honored only when `features.tools`. */\nexport interface ToolDef {\n name: string;\n description?: string;\n /** JSON-Schema for the tool's arguments. */\n inputSchema: Record<string, unknown>;\n}\n\n/** A host-brokered chat completion request: the messages plus optional tools,\n * response format, and model hint (each honored per the provider's features). */\nexport interface ChatRequest {\n messages: ChatMessage[];\n /** Honored only when the resolved provider advertises `features.tools`. */\n tools?: ToolDef[];\n /** `'json'` honored only when `features.jsonMode`. Defaults to `'text'`. */\n responseFormat?: 'text' | 'json';\n maxTokens?: number;\n /** An ABSTRACT tier hint, never a vendor model id — the host maps it to a concrete\n * model on the resolved provider. Omit to take the provider's default. */\n modelHint?: 'fast' | 'smart';\n /** Abort the completion mid-stream. When it fires, the SDK sends the host a cancel\n * frame so the host aborts the upstream provider request and STOPS BILLING the\n * user's key — not merely stops the app-side iterator (LLM_AND_AGENTS_SPEC §3.3\n * \"abort the in-flight LLM request\", R3-224). Not sent over the wire (an\n * `AbortSignal` isn't serializable); handled SDK-side. */\n signal?: AbortSignal;\n}\n\n/** One streamed chunk. Consumers typically accumulate `text-delta`s. */\nexport type ChatDelta =\n | { type: 'text-delta'; text: string }\n | { type: 'tool-call'; id: string; name: string; input: unknown }\n // R3-335 — the model's reasoning as it streams. `reasoning-delta` carries the text\n // incrementally (render it live); the terminal `reasoning` carries the WHOLE block\n // plus the `signature` the provider may require on the echo, and is what a caller\n // should put back into the conversation. A provider without reasoning emits neither.\n | { type: 'reasoning-delta'; text: string }\n | { type: 'reasoning'; text: string; signature?: string }\n | { type: 'reasoning-redacted'; data: string }\n // Token accounting for the turn. `cacheReadTokens`/`cacheWriteTokens` are present\n // only on providers that report prompt caching (R3-336) — they are what makes a\n // caching claim verifiable rather than believed, and their ABSENCE is meaningful:\n // it says this provider reports nothing, not that nothing was cached.\n | {\n type: 'usage';\n inputTokens: number;\n outputTokens: number;\n cacheReadTokens?: number;\n cacheWriteTokens?: number;\n };\n\n/** Why generation stopped: natural `end`, `length` cap, a `tool` call, or content `filtered`. */\nexport type ChatStopReason = 'end' | 'length' | 'tool' | 'filtered';\n\n/** The terminal value of the {@link chat} stream. */\nexport interface ChatResult {\n stopReason: ChatStopReason;\n}\n\n/**\n * Stream a chat completion from whichever provider the user has configured.\n *\n * ```ts\n * let summary = '';\n * for await (const d of chat({ messages: [{ role: 'user', content: [{ type: 'text', text }] }] })) {\n * if (d.type === 'text-delta') summary += d.text;\n * }\n * ```\n *\n * Requires the `llm:chat` capability. If no provider is bound, the host first\n * draws the SP-7 connect-me gate itself (R3-456: the app never draws a\n * credential prompt — that is host chrome, SECRETS_SPEC S3):\n * - the user connects a key → the call retries once and streams normally;\n * - the user declines → the generator throws `code: 'cancelled'` (the same code\n * a declined powerbox produces — a working degraded state: catch it and\n * degrade, e.g. skip the AI feature);\n * - an older host without the gate throws `code: 'provider-not-configured'`.\n * A signed-out user throws `code: 'auth-required'`; an un-granted call throws\n * `forbidden`.\n */\nexport function chat(req: ChatRequest): AsyncGenerator<ChatDelta, ChatResult, void> {\n // Peel `signal` out of the request before it becomes wire params — an AbortSignal\n // can't cross the postMessage boundary as data; it drives the SDK-side cancel frame.\n const { signal, ...params } = req;\n return invokeStream<ChatDelta, ChatResult>('llm:chat', params as unknown as Record<string, unknown>, signal);\n}\n\n/** The resolved provider's advertised abilities (SERVICE_PROVIDERS_SPEC §2.5) — read\n * to branch/degrade (offer image upload only when `vision`). */\nexport interface ChatFeatures {\n vision: boolean;\n tools: boolean;\n jsonMode: boolean;\n /** R3-335: the provider emits reasoning blocks. Read it to decide whether to render\n * a thinking surface at all — an empty affordance on a provider that never thinks\n * is worse than none. Normalized to `false` by the channel when a host predating\n * R3-335 omits it, so this is never `undefined` in practice. */\n reasoning: boolean;\n maxContextTokens: number;\n}\n\n/** Info about the provider the host resolved for this app. `null` when no provider\n * is bound (SP-7: prompt the user to add a key before calling {@link chat}). */\nexport interface ChatProviderInfo {\n /** Opaque provider id, e.g. `llm.chat.anthropic` — never a vendor secret or model id. */\n providerId: string;\n /** True for Host-proxied providers (host-vouched, SP-9); false for app-level ones,\n * whose `features` are an untrusted claim. */\n hostVouched: boolean;\n features: ChatFeatures;\n // NOTE (R3-300): `displayName`, `executor` and the resolved per-tier `models` belong\n // here — an app rendering provider state wants all three. They are NOT added yet,\n // deliberately: this interface IS the `llm-provider` channel's declared value, so\n // adding a field is a WIRE change, and the wire is owned by\n // `@immediately-run/sandbox-protocol` (descriptor edit → publish → pin bump on both\n // sides). The protocol snapshot gate enforces exactly that, and it is right to. The\n // enrichment rides R3-307's publish, which already has to touch those descriptors —\n // one publish for two additions rather than two.\n}\n\n/**\n * Whether the host has told us about a provider yet, and if so whether one is bound.\n *\n * THREE states, because two is the bug (R3-300). `describeChat()` returns `null` both\n * when no provider is configured AND when the channel has not answered — so an app\n * cannot tell \"you need a key\" from \"ask again in a moment\", and consuming apps\n * rendered a misleading \"connect a key\" banner at users who had one. `unknown` is the\n * state before the host answers; it is not an error and not a prompt to act.\n *\n * **`unknown` is TRANSIENT — the host answers every frame** (R3-419;\n * `LLM_AND_AGENTS_SPEC §4.1` R-LLM-1..3). An app that does not hold `llm:chat` is not\n * met with silence: it is answered `not-configured`, the same terminal state as a user\n * with no key, because from the app's side those are the same fact — do not render a\n * provider, do offer the connect path. So it is correct to treat a `unknown` that\n * persists as a host bug rather than as a state to design around, and WRONG to render a\n * spinner with no timeout on it. (Before R3-419 the host withheld the channel entirely\n * from an ungranted frame, and `unknown` stood forever — that is the failure this note\n * exists to keep from being re-created on the app side.)\n */\nexport type ChatProviderState =\n | { status: 'unknown' }\n | { status: 'not-configured' }\n | { status: 'configured'; provider: ChatProviderInfo };\n\n// The `llm-provider` describe channel (Recipe A): the host pushes the resolved\n// provider info on change and replays it on register-frame, gated by `llm:chat`.\n// A message with no `provider` key is ignored; an explicit `null` means \"no provider\n// bound\", which is now REPRESENTABLE as distinct from \"not yet answered\".\n// The channel's VALUE stays exactly what the wire carries — `ChatProviderInfo | null` —\n// because the wire did not change here and the protocol snapshot gate reads this type as\n// the channel's shape. The three-state lives BESIDE it: `answered` records whether the host\n// has ever spoken on this channel, which is the one bit `null` cannot carry. Deriving the\n// state rather than widening the channel keeps the wire contract byte-identical, which it\n// is (SDK_PACKAGING_SPEC §9: the wire is additive-only, and this is not a wire change).\n/**\n * Fill in feature flags a host older than the field does not send (R3-335).\n *\n * `features.reasoning` arrived after `ChatFeatures` shipped, so a host predating it\n * omits the key. `undefined` reads as falsy everywhere EXCEPT a `'reasoning' in\n * features` check, which is exactly the kind of difference that produces one wrong\n * branch a year later — so it is normalized here, once, rather than left to every\n * caller. Absent means \"does not reason\": the fail-closed reading.\n *\n * Exported for its own test; not part of the public surface (`index.ts` re-exports\n * this module wholesale, so it is reachable — it is documented as internal rather\n * than hidden behind a lie).\n * @internal\n */\nexport function normalizeProviderInfo(provider: ChatProviderInfo | null): ChatProviderInfo | null {\n if (!provider) return null;\n // The wire value is whatever the host sent, which may predate `reasoning` — so read\n // it as partial rather than trusting the declared type, and decide the flag explicitly.\n const wire = provider.features as Partial<ChatFeatures>;\n return { ...provider, features: { ...wire, reasoning: wire.reasoning === true } as ChatFeatures };\n}\n\nlet answered = false;\nconst channel = createPushChannel<ChatProviderInfo | null>({\n pushType: LLM_PROVIDER,\n requestType: REQUEST_LLM_PROVIDER,\n initial: null,\n parse: (msg) => {\n if (!('provider' in msg)) return undefined;\n answered = true;\n return normalizeProviderInfo((msg.provider as ChatProviderInfo | null) ?? null);\n },\n});\n\n/** Derive the three-state from the wire value plus whether the host has answered. */\nconst stateOf = (provider: ChatProviderInfo | null): ChatProviderState =>\n !answered ? { status: 'unknown' } : provider ? { status: 'configured', provider } : { status: 'not-configured' };\n\n/**\n * The provider the host resolved for this app, or `null`.\n *\n * Kept for compatibility (`ways_of_working §6`, additive-only): it collapses `unknown`\n * and `not-configured` to `null`. Prefer {@link describeChatState} when the difference\n * matters — which is any time you would render \"connect a key\", because doing that in\n * the `unknown` state is exactly the false banner R3-300 fixes.\n */\nexport const describeChat = (): ChatProviderInfo | null => channel.get();\n\n/** The three-state read: `unknown` before the host answers, then configured or not. */\nexport const describeChatState = (): ChatProviderState => stateOf(channel.get());\n\n/** Subscribe to provider changes (key added/revoked, preference changed). Invoked\n * immediately with the current value, then on every change. Returns unsubscribe. */\nexport const onChatProviderChange = (listener: (provider: ChatProviderInfo | null) => void): (() => void) =>\n channel.onChange(listener);\n\n/** Subscribe to the three-state provider description. */\nexport const onChatProviderStateChange = (listener: (state: ChatProviderState) => void): (() => void) =>\n channel.onChange((p) => listener(stateOf(p)));\n\n/** React hook returning the resolved chat provider (or `null`), re-rendering on\n * change — gate the summarize affordance on `provider !== null`. */\nexport const useChatProvider = (): ChatProviderInfo | null => channel.use();\n\n/**\n * React hook returning the three-state description.\n *\n * Use this to render provider state honestly: show nothing (or a neutral placeholder)\n * while `unknown`, the connect affordance only on `not-configured`, and the provider's\n * name on `configured`.\n */\nexport const useChatProviderState = (): ChatProviderState => stateOf(channel.use());\n"],"mappings":";AAeA,SAAS,oBAAoB;AAC7B,SAAS,yBAAyB;AAClC,SAAS,cAAc,4BAA4B;AAqH5C,SAAS,KAAK,KAA+D;AAGlF,QAAM,EAAE,QAAQ,GAAG,OAAO,IAAI;AAC9B,SAAO,aAAoC,YAAY,QAA8C,MAAM;AAC7G;AAmFO,SAAS,sBAAsB,UAA4D;AAChG,MAAI,CAAC,SAAU,QAAO;AAGtB,QAAM,OAAO,SAAS;AACtB,SAAO,EAAE,GAAG,UAAU,UAAU,EAAE,GAAG,MAAM,WAAW,KAAK,cAAc,KAAK,EAAkB;AAClG;AAEA,IAAI,WAAW;AACf,MAAM,UAAU,kBAA2C;AAAA,EACzD,UAAU;AAAA,EACV,aAAa;AAAA,EACb,SAAS;AAAA,EACT,OAAO,CAAC,QAAQ;AACd,QAAI,EAAE,cAAc,KAAM,QAAO;AACjC,eAAW;AACX,WAAO,sBAAuB,IAAI,YAAwC,IAAI;AAAA,EAChF;AACF,CAAC;AAGD,MAAM,UAAU,CAAC,aACf,CAAC,WAAW,EAAE,QAAQ,UAAU,IAAI,WAAW,EAAE,QAAQ,cAAc,SAAS,IAAI,EAAE,QAAQ,iBAAiB;AAU1G,MAAM,eAAe,MAA+B,QAAQ,IAAI;AAGhE,MAAM,oBAAoB,MAAyB,QAAQ,QAAQ,IAAI,CAAC;AAIxE,MAAM,uBAAuB,CAAC,aACnC,QAAQ,SAAS,QAAQ;AAGpB,MAAM,4BAA4B,CAAC,aACxC,QAAQ,SAAS,CAAC,MAAM,SAAS,QAAQ,CAAC,CAAC,CAAC;AAIvC,MAAM,kBAAkB,MAA+B,QAAQ,IAAI;AASnE,MAAM,uBAAuB,MAAyB,QAAQ,QAAQ,IAAI,CAAC;","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/llm.ts"],"sourcesContent":["// Provider-agnostic LLM chat — the `llm.chat@1` slot (SERVICE_PROVIDERS_SPEC;\n// LLM_AND_AGENTS_SPEC §8 D5).\n//\n// An app calls ONE chat slot and never worries about which provider the user has a\n// key for: the HOST resolves which vendor answers from the key the user holds\n// (`SecretView.boundOrigin`) plus their `preferredImplementation` choice, normalizes\n// the wire format, injects the key host-side at the §6 net:fetch point (the\n// look-at-nothing proxy), and streams normalized deltas back. The app never names a\n// vendor, never sees the key, and needs NO `net:fetch`/`secrets` grant of its own —\n// only the `llm:chat` capability (elevated, app-scoped: a fork earns it by consent).\n//\n// Inert until the host implements `protocol-llm` (the `chat` stream) + the\n// `llm-provider` describe channel; the contract ships here so apps (the file-explorer\n// summarize fork) can be written against it — exactly how `secrets.ts` shipped ahead\n// of `protocol-secrets`.\nimport { invokeStream } from './catalog';\nimport { createPushChannel } from './pushChannel';\nimport { LLM_PROVIDER, REQUEST_LLM_PROVIDER } from './generated/protocol';\n\n/** Who authored a {@link ChatMessage}. */\nexport type ChatRole = 'system' | 'user' | 'assistant' | 'tool';\n\n/** A part of a message. `image` is only honored when the resolved provider\n * advertises `features.vision` (§2.5); `tool-use`/`tool-result` only when it\n * advertises `features.tools` — branch on {@link describeChat} first. */\nexport type ContentPart =\n | { type: 'text'; text: string }\n | { type: 'image'; mimeType: string; data: string } // data: base64, no data: URL prefix\n // A tool call the model emitted on a prior `assistant` turn — replay it in the\n // conversation so a follow-up request carries the agentic history. Pairs with the\n // streamed `tool-call` {@link ChatDelta} that first surfaced it.\n | { type: 'tool-use'; id: string; name: string; input: Record<string, unknown> }\n // A block of the model's own REASONING from a prior `assistant` turn (R3-335).\n // Honored only when the resolved provider advertises `features.reasoning`.\n //\n // Echo these back. On some providers a reasoning block must be replayed — with its\n // `signature` intact and BEFORE the turn's text/tool-use — for the following turn to\n // be accepted at all; a loop that drops them is quietly lossy across turns in a way\n // that shows up as degraded output rather than an error. Pairs with the streamed\n // `reasoning` {@link ChatDelta}.\n | { type: 'reasoning'; text: string; signature?: string }\n // Reasoning the provider REDACTED: opaque bytes with no readable text, which still\n // have to be echoed back in place to keep the chain valid. Never render it.\n | { type: 'reasoning-redacted'; data: string }\n // The result of executing a `tool-use`, fed back so the model can continue. Carried\n // on a `user`/`tool`-role message; `toolCallId` matches the `tool-use` `id`.\n | { type: 'tool-result'; toolCallId: string; content: string; isError?: boolean };\n\n/** One message in a {@link ChatRequest}: a role plus its content parts. */\nexport interface ChatMessage {\n role: ChatRole;\n content: ContentPart[];\n}\n\n/** A tool the model may call — honored only when `features.tools`. */\nexport interface ToolDef {\n name: string;\n description?: string;\n /** JSON-Schema for the tool's arguments. */\n inputSchema: Record<string, unknown>;\n}\n\n/** A host-brokered chat completion request: the messages plus optional tools,\n * response format, and model hint (each honored per the provider's features). */\nexport interface ChatRequest {\n messages: ChatMessage[];\n /** Honored only when the resolved provider advertises `features.tools`. */\n tools?: ToolDef[];\n /** `'json'` honored only when `features.jsonMode`. Defaults to `'text'`. */\n responseFormat?: 'text' | 'json';\n maxTokens?: number;\n /** An ABSTRACT tier hint, never a vendor model id — the host maps it to a concrete\n * model on the resolved provider. Omit to take the provider's default. */\n modelHint?: 'fast' | 'smart';\n /** Abort the completion mid-stream. When it fires, the SDK sends the host a cancel\n * frame so the host aborts the upstream provider request and STOPS BILLING the\n * user's key — not merely stops the app-side iterator (LLM_AND_AGENTS_SPEC §3.3\n * \"abort the in-flight LLM request\", R3-224). Not sent over the wire (an\n * `AbortSignal` isn't serializable); handled SDK-side. */\n signal?: AbortSignal;\n}\n\n/** One streamed chunk. Consumers typically accumulate `text-delta`s. */\nexport type ChatDelta =\n | { type: 'text-delta'; text: string }\n | { type: 'tool-call'; id: string; name: string; input: unknown }\n // R3-335 — the model's reasoning as it streams. `reasoning-delta` carries the text\n // incrementally (render it live); the terminal `reasoning` carries the WHOLE block\n // plus the `signature` the provider may require on the echo, and is what a caller\n // should put back into the conversation. A provider without reasoning emits neither.\n | { type: 'reasoning-delta'; text: string }\n | { type: 'reasoning'; text: string; signature?: string }\n | { type: 'reasoning-redacted'; data: string }\n // Token accounting for the turn. `cacheReadTokens`/`cacheWriteTokens` are present\n // only on providers that report prompt caching (R3-336) — they are what makes a\n // caching claim verifiable rather than believed, and their ABSENCE is meaningful:\n // it says this provider reports nothing, not that nothing was cached.\n | {\n type: 'usage';\n inputTokens: number;\n outputTokens: number;\n cacheReadTokens?: number;\n cacheWriteTokens?: number;\n };\n\n/** Why generation stopped: natural `end`, `length` cap, a `tool` call, or content `filtered`. */\nexport type ChatStopReason = 'end' | 'length' | 'tool' | 'filtered';\n\n/** The terminal value of the {@link chat} stream. */\nexport interface ChatResult {\n stopReason: ChatStopReason;\n}\n\n/**\n * Stream a chat completion from whichever provider the user has configured.\n *\n * ```ts\n * let summary = '';\n * for await (const d of chat({ messages: [{ role: 'user', content: [{ type: 'text', text }] }] })) {\n * if (d.type === 'text-delta') summary += d.text;\n * }\n * ```\n *\n * Requires the `llm:chat` capability. If no provider is bound, the host first\n * draws the SP-7 connect-me gate itself (R3-456: the app never draws a\n * credential prompt — that is host chrome, SECRETS_SPEC S3):\n * - the user connects a key → the call retries once and streams normally;\n * - the user declines → the generator throws `code: 'cancelled'` (the same code\n * a declined powerbox produces — a working degraded state: catch it and\n * degrade, e.g. skip the AI feature);\n * - an older host without the gate throws `code: 'provider-not-configured'`.\n * A signed-out user throws `code: 'auth-required'`; an un-granted call throws\n * `forbidden`.\n */\nexport function chat(req: ChatRequest): AsyncGenerator<ChatDelta, ChatResult, void> {\n // Peel `signal` out of the request before it becomes wire params — an AbortSignal\n // can't cross the postMessage boundary as data; it drives the SDK-side cancel frame.\n const { signal, ...params } = req;\n return invokeStream<ChatDelta, ChatResult>('llm:chat', params as unknown as Record<string, unknown>, signal);\n}\n\n/** The resolved provider's advertised abilities (SERVICE_PROVIDERS_SPEC §2.5) — read\n * to branch/degrade (offer image upload only when `vision`). */\nexport interface ChatFeatures {\n vision: boolean;\n tools: boolean;\n jsonMode: boolean;\n /** R3-335: the provider emits reasoning blocks. Read it to decide whether to render\n * a thinking surface at all — an empty affordance on a provider that never thinks\n * is worse than none. Normalized to `false` by the channel when a host predating\n * R3-335 omits it, so this is never `undefined` in practice. */\n reasoning: boolean;\n maxContextTokens: number;\n}\n\n/** How the resolved provider's requests physically leave the browser. Read it to\n * DESCRIBE routing where that matters to the person (\"runs in your browser\" vs\n * \"routed through immediately.run\") — never to draw host chrome or a consent prompt,\n * which remain the host's (UI_AS_APPS §8 T15). */\nexport type ChatExecutor = 'browser-direct' | 'backend-proxied';\n\n/** The concrete model each abstract tier resolves to right now. */\nexport interface ChatTierModels {\n fast: string;\n smart: string;\n}\n\n/** Info about the provider the host resolved for this app. `null` when no provider\n * is bound (SP-7: prompt the user to add a key before calling {@link chat}). */\nexport interface ChatProviderInfo {\n /** Opaque provider id, e.g. `llm.chat.anthropic` — never a vendor secret or model id. */\n providerId: string;\n /** True for Host-proxied providers (host-vouched, SP-9); false for app-level ones,\n * whose `features` are an untrusted claim. */\n hostVouched: boolean;\n features: ChatFeatures;\n /** The provider's human name, e.g. `OpenRouter` — what to put in front of a person.\n * Absent on a host that predates the field: fall back to your own copy rather than\n * rendering the id, which is a platform identifier and not a name. */\n displayName?: string;\n /** How this provider's requests leave the browser. Absent on a host that predates the\n * field, which is NOT the same as `browser-direct` — say nothing about routing rather\n * than guess at it. */\n executor?: ChatExecutor;\n /**\n * The concrete model each {@link ChatRequest.modelHint} tier resolves to — what a\n * `smart` request would actually run, after the user's own preference.\n *\n * Read-only, and it does not weaken `LLM_AND_AGENTS_SPEC §0`: an app still names no\n * model, and {@link ChatRequest} still carries only the abstract hint. It is here so an\n * app can be HONEST about what answered — a transcript that says which model wrote a\n * reply, a warning that names the model about to be spent on — instead of showing a\n * blank where the platform knows the answer. The user picks the model in host settings;\n * the app reports it.\n *\n * Absent on a host that predates the field. It changes when the user changes their\n * preference, so read it through {@link onChatProviderChange} rather than caching it.\n */\n models?: ChatTierModels;\n}\n\n/**\n * Whether the host has told us about a provider yet, and if so whether one is bound.\n *\n * THREE states, because two is the bug (R3-300). `describeChat()` returns `null` both\n * when no provider is configured AND when the channel has not answered — so an app\n * cannot tell \"you need a key\" from \"ask again in a moment\", and consuming apps\n * rendered a misleading \"connect a key\" banner at users who had one. `unknown` is the\n * state before the host answers; it is not an error and not a prompt to act.\n *\n * **`unknown` is TRANSIENT — the host answers every frame** (R3-419;\n * `LLM_AND_AGENTS_SPEC §4.1` R-LLM-1..3). An app that does not hold `llm:chat` is not\n * met with silence: it is answered `not-configured`, the same terminal state as a user\n * with no key, because from the app's side those are the same fact — do not render a\n * provider, do offer the connect path. So it is correct to treat a `unknown` that\n * persists as a host bug rather than as a state to design around, and WRONG to render a\n * spinner with no timeout on it. (Before R3-419 the host withheld the channel entirely\n * from an ungranted frame, and `unknown` stood forever — that is the failure this note\n * exists to keep from being re-created on the app side.)\n */\nexport type ChatProviderState =\n | { status: 'unknown' }\n | { status: 'not-configured' }\n | { status: 'configured'; provider: ChatProviderInfo };\n\n// The `llm-provider` describe channel (Recipe A): the host pushes the resolved\n// provider info on change and replays it on register-frame, gated by `llm:chat`.\n// A message with no `provider` key is ignored; an explicit `null` means \"no provider\n// bound\", which is now REPRESENTABLE as distinct from \"not yet answered\".\n// The channel's VALUE stays exactly what the wire carries — `ChatProviderInfo | null` —\n// because the wire did not change here and the protocol snapshot gate reads this type as\n// the channel's shape. The three-state lives BESIDE it: `answered` records whether the host\n// has ever spoken on this channel, which is the one bit `null` cannot carry. Deriving the\n// state rather than widening the channel keeps the wire contract byte-identical, which it\n// is (SDK_PACKAGING_SPEC §9: the wire is additive-only, and this is not a wire change).\n/**\n * Reconcile what the host actually sent with what this SDK declares.\n *\n * `features.reasoning` arrived after `ChatFeatures` shipped, so a host predating it\n * omits the key. `undefined` reads as falsy everywhere EXCEPT a `'reasoning' in\n * features` check, which is exactly the kind of difference that produces one wrong\n * branch a year later — so it is normalized here, once, rather than left to every\n * caller. Absent means \"does not reason\": the fail-closed reading.\n *\n * `displayName`, `executor` and `models` arrived later still, and for them absence is a\n * REAL answer an app is told to handle (\"this host does not say\"), so they are left\n * absent rather than filled in. What is dropped is a value that is present but not\n * usable — an `executor` outside the union, a `models` missing a tier — because a\n * half-answer rendered as fact is worse than the honest blank the app already handles.\n *\n * Exported for its own test; not part of the public surface (`index.ts` re-exports\n * this module wholesale, so it is reachable — it is documented as internal rather\n * than hidden behind a lie).\n * @internal\n */\nconst EXECUTORS: readonly ChatExecutor[] = ['browser-direct', 'backend-proxied'];\n\nconst usableModels = (raw: unknown): ChatTierModels | undefined => {\n if (!raw || typeof raw !== 'object') return undefined;\n const { fast, smart } = raw as Partial<ChatTierModels>;\n return typeof fast === 'string' && fast && typeof smart === 'string' && smart ? { fast, smart } : undefined;\n};\n\nexport function normalizeProviderInfo(provider: ChatProviderInfo | null): ChatProviderInfo | null {\n if (!provider) return null;\n // The three later fields are taken OFF the value and put back only if usable — spreading\n // and then overwriting would leave an unusable key present, and `key in provider` is\n // exactly how an app is told to ask whether the host said anything.\n const { displayName: rawName, executor: rawExecutor, models: rawModels, ...rest } = provider;\n // The wire value is whatever the host sent, which may predate any of these fields — so\n // read it as partial rather than trusting the declared type, and decide each explicitly.\n const wire = provider.features as Partial<ChatFeatures>;\n const executor = EXECUTORS.includes(rawExecutor as ChatExecutor) ? (rawExecutor as ChatExecutor) : undefined;\n const displayName = typeof rawName === 'string' && rawName ? rawName : undefined;\n const models = usableModels(rawModels);\n return {\n ...rest,\n features: { ...wire, reasoning: wire.reasoning === true } as ChatFeatures,\n ...(displayName ? { displayName } : {}),\n ...(executor ? { executor } : {}),\n ...(models ? { models } : {}),\n };\n}\n\nlet answered = false;\nconst channel = createPushChannel<ChatProviderInfo | null>({\n pushType: LLM_PROVIDER,\n requestType: REQUEST_LLM_PROVIDER,\n initial: null,\n parse: (msg) => {\n if (!('provider' in msg)) return undefined;\n answered = true;\n return normalizeProviderInfo((msg.provider as ChatProviderInfo | null) ?? null);\n },\n});\n\n/** Derive the three-state from the wire value plus whether the host has answered. */\nconst stateOf = (provider: ChatProviderInfo | null): ChatProviderState =>\n !answered ? { status: 'unknown' } : provider ? { status: 'configured', provider } : { status: 'not-configured' };\n\n/**\n * The provider the host resolved for this app, or `null`.\n *\n * Kept for compatibility (`ways_of_working §6`, additive-only): it collapses `unknown`\n * and `not-configured` to `null`. Prefer {@link describeChatState} when the difference\n * matters — which is any time you would render \"connect a key\", because doing that in\n * the `unknown` state is exactly the false banner R3-300 fixes.\n */\nexport const describeChat = (): ChatProviderInfo | null => channel.get();\n\n/** The three-state read: `unknown` before the host answers, then configured or not. */\nexport const describeChatState = (): ChatProviderState => stateOf(channel.get());\n\n/** Subscribe to provider changes (key added/revoked, preference changed). Invoked\n * immediately with the current value, then on every change. Returns unsubscribe. */\nexport const onChatProviderChange = (listener: (provider: ChatProviderInfo | null) => void): (() => void) =>\n channel.onChange(listener);\n\n/** Subscribe to the three-state provider description. */\nexport const onChatProviderStateChange = (listener: (state: ChatProviderState) => void): (() => void) =>\n channel.onChange((p) => listener(stateOf(p)));\n\n/** React hook returning the resolved chat provider (or `null`), re-rendering on\n * change — gate the summarize affordance on `provider !== null`. */\nexport const useChatProvider = (): ChatProviderInfo | null => channel.use();\n\n/**\n * React hook returning the three-state description.\n *\n * Use this to render provider state honestly: show nothing (or a neutral placeholder)\n * while `unknown`, the connect affordance only on `not-configured`, and the provider's\n * name on `configured`.\n */\nexport const useChatProviderState = (): ChatProviderState => stateOf(channel.use());\n"],"mappings":";AAeA,SAAS,oBAAoB;AAC7B,SAAS,yBAAyB;AAClC,SAAS,cAAc,4BAA4B;AAqH5C,SAAS,KAAK,KAA+D;AAGlF,QAAM,EAAE,QAAQ,GAAG,OAAO,IAAI;AAC9B,SAAO,aAAoC,YAAY,QAA8C,MAAM;AAC7G;AAoHA,MAAM,YAAqC,CAAC,kBAAkB,iBAAiB;AAE/E,MAAM,eAAe,CAAC,QAA6C;AACjE,MAAI,CAAC,OAAO,OAAO,QAAQ,SAAU,QAAO;AAC5C,QAAM,EAAE,MAAM,MAAM,IAAI;AACxB,SAAO,OAAO,SAAS,YAAY,QAAQ,OAAO,UAAU,YAAY,QAAQ,EAAE,MAAM,MAAM,IAAI;AACpG;AAEO,SAAS,sBAAsB,UAA4D;AAChG,MAAI,CAAC,SAAU,QAAO;AAItB,QAAM,EAAE,aAAa,SAAS,UAAU,aAAa,QAAQ,WAAW,GAAG,KAAK,IAAI;AAGpF,QAAM,OAAO,SAAS;AACtB,QAAM,WAAW,UAAU,SAAS,WAA2B,IAAK,cAA+B;AACnG,QAAM,cAAc,OAAO,YAAY,YAAY,UAAU,UAAU;AACvE,QAAM,SAAS,aAAa,SAAS;AACrC,SAAO;AAAA,IACL,GAAG;AAAA,IACH,UAAU,EAAE,GAAG,MAAM,WAAW,KAAK,cAAc,KAAK;AAAA,IACxD,GAAI,cAAc,EAAE,YAAY,IAAI,CAAC;AAAA,IACrC,GAAI,WAAW,EAAE,SAAS,IAAI,CAAC;AAAA,IAC/B,GAAI,SAAS,EAAE,OAAO,IAAI,CAAC;AAAA,EAC7B;AACF;AAEA,IAAI,WAAW;AACf,MAAM,UAAU,kBAA2C;AAAA,EACzD,UAAU;AAAA,EACV,aAAa;AAAA,EACb,SAAS;AAAA,EACT,OAAO,CAAC,QAAQ;AACd,QAAI,EAAE,cAAc,KAAM,QAAO;AACjC,eAAW;AACX,WAAO,sBAAuB,IAAI,YAAwC,IAAI;AAAA,EAChF;AACF,CAAC;AAGD,MAAM,UAAU,CAAC,aACf,CAAC,WAAW,EAAE,QAAQ,UAAU,IAAI,WAAW,EAAE,QAAQ,cAAc,SAAS,IAAI,EAAE,QAAQ,iBAAiB;AAU1G,MAAM,eAAe,MAA+B,QAAQ,IAAI;AAGhE,MAAM,oBAAoB,MAAyB,QAAQ,QAAQ,IAAI,CAAC;AAIxE,MAAM,uBAAuB,CAAC,aACnC,QAAQ,SAAS,QAAQ;AAGpB,MAAM,4BAA4B,CAAC,aACxC,QAAQ,SAAS,CAAC,MAAM,SAAS,QAAQ,CAAC,CAAC,CAAC;AAIvC,MAAM,kBAAkB,MAA+B,QAAQ,IAAI;AASnE,MAAM,uBAAuB,MAAyB,QAAQ,QAAQ,IAAI,CAAC;","names":[]}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
var platformLink_exports = {};
|
|
20
|
+
__export(platformLink_exports, {
|
|
21
|
+
PlatformLink: () => PlatformLink,
|
|
22
|
+
usePlatformHref: () => usePlatformHref
|
|
23
|
+
});
|
|
24
|
+
module.exports = __toCommonJS(platformLink_exports);
|
|
25
|
+
var import_jsx_runtime = require("react/jsx-runtime");
|
|
26
|
+
var import_react = require("react");
|
|
27
|
+
var import_TinkerableContext = require("./TinkerableContext");
|
|
28
|
+
var import_urlUtils = require("./urlUtils");
|
|
29
|
+
const usePlatformHref = () => {
|
|
30
|
+
const { outerHref } = (0, import_react.use)(import_TinkerableContext.TinkerableContext);
|
|
31
|
+
return (path) => (0, import_urlUtils.platformHref)(outerHref, path);
|
|
32
|
+
};
|
|
33
|
+
function PlatformLink({ path, children, ...rest }) {
|
|
34
|
+
const platform = usePlatformHref();
|
|
35
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("a", { ...rest, href: platform(path), target: "_top", children });
|
|
36
|
+
}
|
|
37
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
38
|
+
0 && (module.exports = {
|
|
39
|
+
PlatformLink,
|
|
40
|
+
usePlatformHref
|
|
41
|
+
});
|
|
42
|
+
//# sourceMappingURL=platformLink.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/platformLink.tsx"],"sourcesContent":["import type { AnchorHTMLAttributes, ReactNode } from 'react';\nimport { use } from 'react';\n\nimport { TinkerableContext } from './TinkerableContext';\nimport { platformHref } from './urlUtils';\n\n/**\n * Build a PLATFORM-space href (`/present/…`, `/edit/github/…`, `/home`) in the host's URL\n * space, reading `outerHref` from {@link TinkerableContext} the way `useTinkerableLink` does.\n * The returned closure is fresh each render (its output is pure, so identity churn is\n * harmless); an empty context (no host, `vite dev`) yields the path unchanged.\n *\n * Render the result through {@link PlatformLink}, which also carries `target=\"_top\"` —\n * a root-relative href inside the sandboxed frame would resolve against the SANDBOX origin\n * and land nowhere, and an anchor without `target=\"_top\"` navigates the frame instead of\n * the page.\n */\nexport const usePlatformHref = (): ((path: string) => string) => {\n const { outerHref } = use(TinkerableContext);\n return (path: string) => platformHref(outerHref, path);\n};\n\nexport interface PlatformLinkProps extends Omit<AnchorHTMLAttributes<HTMLAnchorElement>, 'href'> {\n /** A root-relative platform path, e.g. `/present/github/acme/todo`. */\n path: string;\n children?: ReactNode;\n}\n\n/**\n * The ONE way to render an anchor to a PLATFORM route: it builds the href with\n * {@link platformHref} (resolving against the host's outer origin) and always carries\n * `target=\"_top\"`, because an anchor inside the sandboxed frame otherwise navigates the\n * frame instead of the host document. External URLs (`https://…`) are not platform routes\n * and should stay plain `<a target=\"_blank\">` anchors.\n */\nexport function PlatformLink({ path, children, ...rest }: PlatformLinkProps) {\n const platform = usePlatformHref();\n return (\n <a {...rest} href={platform(path)} target=\"_top\">\n {children}\n </a>\n );\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAsCI;AArCJ,mBAAoB;AAEpB,+BAAkC;AAClC,sBAA6B;AAatB,MAAM,kBAAkB,MAAkC;AAC/D,QAAM,EAAE,UAAU,QAAI,kBAAI,0CAAiB;AAC3C,SAAO,CAAC,aAAiB,8BAAa,WAAW,IAAI;AACvD;AAeO,SAAS,aAAa,EAAE,MAAM,UAAU,GAAG,KAAK,GAAsB;AAC3E,QAAM,WAAW,gBAAgB;AACjC,SACE,4CAAC,OAAG,GAAG,MAAM,MAAM,SAAS,IAAI,GAAG,QAAO,QACvC,UACH;AAEJ;","names":[]}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import * as react from 'react';
|
|
2
|
+
import { AnchorHTMLAttributes, ReactNode } from 'react';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Build a PLATFORM-space href (`/present/…`, `/edit/github/…`, `/home`) in the host's URL
|
|
6
|
+
* space, reading `outerHref` from {@link TinkerableContext} the way `useTinkerableLink` does.
|
|
7
|
+
* The returned closure is fresh each render (its output is pure, so identity churn is
|
|
8
|
+
* harmless); an empty context (no host, `vite dev`) yields the path unchanged.
|
|
9
|
+
*
|
|
10
|
+
* Render the result through {@link PlatformLink}, which also carries `target="_top"` —
|
|
11
|
+
* a root-relative href inside the sandboxed frame would resolve against the SANDBOX origin
|
|
12
|
+
* and land nowhere, and an anchor without `target="_top"` navigates the frame instead of
|
|
13
|
+
* the page.
|
|
14
|
+
*/
|
|
15
|
+
declare const usePlatformHref: () => ((path: string) => string);
|
|
16
|
+
interface PlatformLinkProps extends Omit<AnchorHTMLAttributes<HTMLAnchorElement>, 'href'> {
|
|
17
|
+
/** A root-relative platform path, e.g. `/present/github/acme/todo`. */
|
|
18
|
+
path: string;
|
|
19
|
+
children?: ReactNode;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* The ONE way to render an anchor to a PLATFORM route: it builds the href with
|
|
23
|
+
* {@link platformHref} (resolving against the host's outer origin) and always carries
|
|
24
|
+
* `target="_top"`, because an anchor inside the sandboxed frame otherwise navigates the
|
|
25
|
+
* frame instead of the host document. External URLs (`https://…`) are not platform routes
|
|
26
|
+
* and should stay plain `<a target="_blank">` anchors.
|
|
27
|
+
*/
|
|
28
|
+
declare function PlatformLink({ path, children, ...rest }: PlatformLinkProps): react.JSX.Element;
|
|
29
|
+
|
|
30
|
+
export { PlatformLink, type PlatformLinkProps, usePlatformHref };
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import * as react from 'react';
|
|
2
|
+
import { AnchorHTMLAttributes, ReactNode } from 'react';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Build a PLATFORM-space href (`/present/…`, `/edit/github/…`, `/home`) in the host's URL
|
|
6
|
+
* space, reading `outerHref` from {@link TinkerableContext} the way `useTinkerableLink` does.
|
|
7
|
+
* The returned closure is fresh each render (its output is pure, so identity churn is
|
|
8
|
+
* harmless); an empty context (no host, `vite dev`) yields the path unchanged.
|
|
9
|
+
*
|
|
10
|
+
* Render the result through {@link PlatformLink}, which also carries `target="_top"` —
|
|
11
|
+
* a root-relative href inside the sandboxed frame would resolve against the SANDBOX origin
|
|
12
|
+
* and land nowhere, and an anchor without `target="_top"` navigates the frame instead of
|
|
13
|
+
* the page.
|
|
14
|
+
*/
|
|
15
|
+
declare const usePlatformHref: () => ((path: string) => string);
|
|
16
|
+
interface PlatformLinkProps extends Omit<AnchorHTMLAttributes<HTMLAnchorElement>, 'href'> {
|
|
17
|
+
/** A root-relative platform path, e.g. `/present/github/acme/todo`. */
|
|
18
|
+
path: string;
|
|
19
|
+
children?: ReactNode;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* The ONE way to render an anchor to a PLATFORM route: it builds the href with
|
|
23
|
+
* {@link platformHref} (resolving against the host's outer origin) and always carries
|
|
24
|
+
* `target="_top"`, because an anchor inside the sandboxed frame otherwise navigates the
|
|
25
|
+
* frame instead of the host document. External URLs (`https://…`) are not platform routes
|
|
26
|
+
* and should stay plain `<a target="_blank">` anchors.
|
|
27
|
+
*/
|
|
28
|
+
declare function PlatformLink({ path, children, ...rest }: PlatformLinkProps): react.JSX.Element;
|
|
29
|
+
|
|
30
|
+
export { PlatformLink, type PlatformLinkProps, usePlatformHref };
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import "./chunk-VHAA22YE.js";
|
|
2
|
+
import { jsx } from "react/jsx-runtime";
|
|
3
|
+
import { use } from "react";
|
|
4
|
+
import { TinkerableContext } from "./TinkerableContext";
|
|
5
|
+
import { platformHref } from "./urlUtils";
|
|
6
|
+
const usePlatformHref = () => {
|
|
7
|
+
const { outerHref } = use(TinkerableContext);
|
|
8
|
+
return (path) => platformHref(outerHref, path);
|
|
9
|
+
};
|
|
10
|
+
function PlatformLink({ path, children, ...rest }) {
|
|
11
|
+
const platform = usePlatformHref();
|
|
12
|
+
return /* @__PURE__ */ jsx("a", { ...rest, href: platform(path), target: "_top", children });
|
|
13
|
+
}
|
|
14
|
+
export {
|
|
15
|
+
PlatformLink,
|
|
16
|
+
usePlatformHref
|
|
17
|
+
};
|
|
18
|
+
//# sourceMappingURL=platformLink.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/platformLink.tsx"],"sourcesContent":["import type { AnchorHTMLAttributes, ReactNode } from 'react';\nimport { use } from 'react';\n\nimport { TinkerableContext } from './TinkerableContext';\nimport { platformHref } from './urlUtils';\n\n/**\n * Build a PLATFORM-space href (`/present/…`, `/edit/github/…`, `/home`) in the host's URL\n * space, reading `outerHref` from {@link TinkerableContext} the way `useTinkerableLink` does.\n * The returned closure is fresh each render (its output is pure, so identity churn is\n * harmless); an empty context (no host, `vite dev`) yields the path unchanged.\n *\n * Render the result through {@link PlatformLink}, which also carries `target=\"_top\"` —\n * a root-relative href inside the sandboxed frame would resolve against the SANDBOX origin\n * and land nowhere, and an anchor without `target=\"_top\"` navigates the frame instead of\n * the page.\n */\nexport const usePlatformHref = (): ((path: string) => string) => {\n const { outerHref } = use(TinkerableContext);\n return (path: string) => platformHref(outerHref, path);\n};\n\nexport interface PlatformLinkProps extends Omit<AnchorHTMLAttributes<HTMLAnchorElement>, 'href'> {\n /** A root-relative platform path, e.g. `/present/github/acme/todo`. */\n path: string;\n children?: ReactNode;\n}\n\n/**\n * The ONE way to render an anchor to a PLATFORM route: it builds the href with\n * {@link platformHref} (resolving against the host's outer origin) and always carries\n * `target=\"_top\"`, because an anchor inside the sandboxed frame otherwise navigates the\n * frame instead of the host document. External URLs (`https://…`) are not platform routes\n * and should stay plain `<a target=\"_blank\">` anchors.\n */\nexport function PlatformLink({ path, children, ...rest }: PlatformLinkProps) {\n const platform = usePlatformHref();\n return (\n <a {...rest} href={platform(path)} target=\"_top\">\n {children}\n </a>\n );\n}\n"],"mappings":";AAsCI;AArCJ,SAAS,WAAW;AAEpB,SAAS,yBAAyB;AAClC,SAAS,oBAAoB;AAatB,MAAM,kBAAkB,MAAkC;AAC/D,QAAM,EAAE,UAAU,IAAI,IAAI,iBAAiB;AAC3C,SAAO,CAAC,SAAiB,aAAa,WAAW,IAAI;AACvD;AAeO,SAAS,aAAa,EAAE,MAAM,UAAU,GAAG,KAAK,GAAsB;AAC3E,QAAM,WAAW,gBAAgB;AACjC,SACE,oBAAC,OAAG,GAAG,MAAM,MAAM,SAAS,IAAI,GAAG,QAAO,QACvC,UACH;AAEJ;","names":[]}
|
package/dist/recents.cjs
CHANGED
|
@@ -25,7 +25,15 @@ module.exports = __toCommonJS(recents_exports);
|
|
|
25
25
|
var import_sandboxUtils = require("./sandboxUtils");
|
|
26
26
|
var import_protocol = require("./generated/protocol");
|
|
27
27
|
var import_protocolSchemes = require("./protocolSchemes");
|
|
28
|
-
const recentsRequest = (params) =>
|
|
28
|
+
const recentsRequest = async (params) => {
|
|
29
|
+
const res = await (0, import_sandboxUtils.protocolRequest)(import_protocolSchemes.SCHEMES[import_protocol.PROTOCOL_RECENTS], "list", [params]);
|
|
30
|
+
if (!res || res.ok !== true) {
|
|
31
|
+
const err = new Error(res?.message ?? "recents request failed");
|
|
32
|
+
err.code = res?.code ?? "unknown";
|
|
33
|
+
throw err;
|
|
34
|
+
}
|
|
35
|
+
return res.data;
|
|
36
|
+
};
|
|
29
37
|
async function listRecentProjects() {
|
|
30
38
|
const res = await recentsRequest({});
|
|
31
39
|
return res.projects ?? null;
|
package/dist/recents.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/recents.ts"],"sourcesContent":["// The recent-projects record — app-facing surface (R3-485, OSO §4.3).\n//\n// `listRecentProjects()` reads the HOST-OWNED record through the one elevated,\n// app-scoped `recents:read` capability. Two gates stand between an app and the\n// record, and both must pass: the §8.4 capability grant (declared as\n// `requests: { 'recents:read': {} }` and consented per (app, principal)), and the\n// handler's structural binding — the read is granted only to the app bound at\n// `page.home`. For every other app the call resolves to a refusal, whatever it\n// declared.\n//\n// A project entry is COORDINATES (provider/namespace/repository/ref + when) — a\n// location the user already navigated to under their own authority, never an\n// in-repo path. Opening one runs the ordinary load path with the ordinary\n// consent: the record confers nothing (R-OSO-22).\nimport { protocolRequest } from './sandboxUtils';\nimport { PROTOCOL_RECENTS } from './generated/protocol';\nimport { SCHEMES } from './protocolSchemes';\n\n/** One recently opened project — repository coordinates, never an in-repo path. */\nexport interface RecentProject {\n provider: string;\n namespace: string;\n repository: string;\n /** The ref the user opened (`main`, a sha). */\n ref: string;\n /** When the user last opened it (epoch ms). */\n ts: number;\n}\n\ninterface RecentsReply {\n /** Newest-first; `null` when the record is empty or the user cleared it. */\n projects: RecentProject[] | null;\n}\n\n/**\n * The one call shape the SDK speaks under `protocol-recents` — read, or clear\n * (same gated surface). A single typed call site keeps the wire contract exact:\n * the snapshot gate reads THIS shape, and the host handler accepts the same.\n */\nconst recentsRequest = (params: { clear?: boolean }): Promise<RecentsReply>
|
|
1
|
+
{"version":3,"sources":["../src/recents.ts"],"sourcesContent":["// The recent-projects record — app-facing surface (R3-485, OSO §4.3).\n//\n// `listRecentProjects()` reads the HOST-OWNED record through the one elevated,\n// app-scoped `recents:read` capability. Two gates stand between an app and the\n// record, and both must pass: the §8.4 capability grant (declared as\n// `requests: { 'recents:read': {} }` and consented per (app, principal)), and the\n// handler's structural binding — the read is granted only to the app bound at\n// `page.home`. For every other app the call resolves to a refusal, whatever it\n// declared.\n//\n// A project entry is COORDINATES (provider/namespace/repository/ref + when) — a\n// location the user already navigated to under their own authority, never an\n// in-repo path. Opening one runs the ordinary load path with the ordinary\n// consent: the record confers nothing (R-OSO-22).\nimport { protocolRequest } from './sandboxUtils';\nimport { PROTOCOL_RECENTS } from './generated/protocol';\nimport { SCHEMES } from './protocolSchemes';\n\n/** One recently opened project — repository coordinates, never an in-repo path. */\nexport interface RecentProject {\n provider: string;\n namespace: string;\n repository: string;\n /** The ref the user opened (`main`, a sha). */\n ref: string;\n /** When the user last opened it (epoch ms). */\n ts: number;\n}\n\ninterface RecentsReply {\n /** Newest-first; `null` when the record is empty or the user cleared it. */\n projects: RecentProject[] | null;\n}\n\n/**\n * The host wraps every action result as `{ ok: true, data }` and every refusal\n * as `{ ok: false, code, message }` INSIDE the reply's `result` — refusals\n * resolve rather than reject, because the sandbox's protocolRequest rejects\n * with a bare `Error` and would drop the `code` (requestDispatcher's reply\n * contract). Unwrap here, like every other gated module, so callers see the\n * coded refusal and never a silent `null`.\n */\ntype RecentsResult = { ok: true; data: RecentsReply } | { ok: false; code: string; message?: string };\n\n/**\n * The one call shape the SDK speaks under `protocol-recents` — read, or clear\n * (same gated surface). A single typed call site keeps the wire contract exact:\n * the snapshot gate reads THIS shape, and the host handler accepts the same.\n */\nconst recentsRequest = async (params: { clear?: boolean }): Promise<RecentsReply> => {\n const res = (await protocolRequest(SCHEMES[PROTOCOL_RECENTS], 'list', [params])) as RecentsResult;\n if (!res || res.ok !== true) {\n const err = new Error(res?.message ?? 'recents request failed') as Error & { code: string };\n err.code = res?.code ?? 'unknown';\n throw err;\n }\n return res.data;\n};\n\n/**\n * The user's recently opened projects, newest-first, or `null` when the record is\n * absent (R-OSO-22: cleared is absent, never an empty list). Refuses for any app\n * that is not the `page.home` binding.\n *\n * @throws a coded error — `forbidden` without the grant or the binding, or the\n * host's own code; never a silent `null` for a refusal.\n */\nexport async function listRecentProjects(): Promise<RecentProject[] | null> {\n const res = await recentsRequest({});\n return res.projects ?? null;\n}\n\n/**\n * Clear the user's recent-projects record (the surface afterwards is absent, not\n * empty). Rides the same page.home binding gate as the read.\n *\n * @throws a coded error when the clear is refused — the caller must not treat\n * the record as cleared.\n */\nexport async function clearRecentProjects(): Promise<void> {\n await recentsRequest({ clear: true });\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAcA,0BAAgC;AAChC,sBAAiC;AACjC,6BAAwB;AAiCxB,MAAM,iBAAiB,OAAO,WAAuD;AACnF,QAAM,MAAO,UAAM,qCAAgB,+BAAQ,gCAAgB,GAAG,QAAQ,CAAC,MAAM,CAAC;AAC9E,MAAI,CAAC,OAAO,IAAI,OAAO,MAAM;AAC3B,UAAM,MAAM,IAAI,MAAM,KAAK,WAAW,wBAAwB;AAC9D,QAAI,OAAO,KAAK,QAAQ;AACxB,UAAM;AAAA,EACR;AACA,SAAO,IAAI;AACb;AAUA,eAAsB,qBAAsD;AAC1E,QAAM,MAAM,MAAM,eAAe,CAAC,CAAC;AACnC,SAAO,IAAI,YAAY;AACzB;AASA,eAAsB,sBAAqC;AACzD,QAAM,eAAe,EAAE,OAAO,KAAK,CAAC;AACtC;","names":[]}
|
package/dist/recents.d.cts
CHANGED
|
@@ -12,11 +12,17 @@ interface RecentProject {
|
|
|
12
12
|
* The user's recently opened projects, newest-first, or `null` when the record is
|
|
13
13
|
* absent (R-OSO-22: cleared is absent, never an empty list). Refuses for any app
|
|
14
14
|
* that is not the `page.home` binding.
|
|
15
|
+
*
|
|
16
|
+
* @throws a coded error — `forbidden` without the grant or the binding, or the
|
|
17
|
+
* host's own code; never a silent `null` for a refusal.
|
|
15
18
|
*/
|
|
16
19
|
declare function listRecentProjects(): Promise<RecentProject[] | null>;
|
|
17
20
|
/**
|
|
18
21
|
* Clear the user's recent-projects record (the surface afterwards is absent, not
|
|
19
22
|
* empty). Rides the same page.home binding gate as the read.
|
|
23
|
+
*
|
|
24
|
+
* @throws a coded error when the clear is refused — the caller must not treat
|
|
25
|
+
* the record as cleared.
|
|
20
26
|
*/
|
|
21
27
|
declare function clearRecentProjects(): Promise<void>;
|
|
22
28
|
|
package/dist/recents.d.ts
CHANGED
|
@@ -12,11 +12,17 @@ interface RecentProject {
|
|
|
12
12
|
* The user's recently opened projects, newest-first, or `null` when the record is
|
|
13
13
|
* absent (R-OSO-22: cleared is absent, never an empty list). Refuses for any app
|
|
14
14
|
* that is not the `page.home` binding.
|
|
15
|
+
*
|
|
16
|
+
* @throws a coded error — `forbidden` without the grant or the binding, or the
|
|
17
|
+
* host's own code; never a silent `null` for a refusal.
|
|
15
18
|
*/
|
|
16
19
|
declare function listRecentProjects(): Promise<RecentProject[] | null>;
|
|
17
20
|
/**
|
|
18
21
|
* Clear the user's recent-projects record (the surface afterwards is absent, not
|
|
19
22
|
* empty). Rides the same page.home binding gate as the read.
|
|
23
|
+
*
|
|
24
|
+
* @throws a coded error when the clear is refused — the caller must not treat
|
|
25
|
+
* the record as cleared.
|
|
20
26
|
*/
|
|
21
27
|
declare function clearRecentProjects(): Promise<void>;
|
|
22
28
|
|
package/dist/recents.js
CHANGED
|
@@ -2,7 +2,15 @@ import "./chunk-VHAA22YE.js";
|
|
|
2
2
|
import { protocolRequest } from "./sandboxUtils";
|
|
3
3
|
import { PROTOCOL_RECENTS } from "./generated/protocol";
|
|
4
4
|
import { SCHEMES } from "./protocolSchemes";
|
|
5
|
-
const recentsRequest = (params) =>
|
|
5
|
+
const recentsRequest = async (params) => {
|
|
6
|
+
const res = await protocolRequest(SCHEMES[PROTOCOL_RECENTS], "list", [params]);
|
|
7
|
+
if (!res || res.ok !== true) {
|
|
8
|
+
const err = new Error(res?.message ?? "recents request failed");
|
|
9
|
+
err.code = res?.code ?? "unknown";
|
|
10
|
+
throw err;
|
|
11
|
+
}
|
|
12
|
+
return res.data;
|
|
13
|
+
};
|
|
6
14
|
async function listRecentProjects() {
|
|
7
15
|
const res = await recentsRequest({});
|
|
8
16
|
return res.projects ?? null;
|
package/dist/recents.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/recents.ts"],"sourcesContent":["// The recent-projects record — app-facing surface (R3-485, OSO §4.3).\n//\n// `listRecentProjects()` reads the HOST-OWNED record through the one elevated,\n// app-scoped `recents:read` capability. Two gates stand between an app and the\n// record, and both must pass: the §8.4 capability grant (declared as\n// `requests: { 'recents:read': {} }` and consented per (app, principal)), and the\n// handler's structural binding — the read is granted only to the app bound at\n// `page.home`. For every other app the call resolves to a refusal, whatever it\n// declared.\n//\n// A project entry is COORDINATES (provider/namespace/repository/ref + when) — a\n// location the user already navigated to under their own authority, never an\n// in-repo path. Opening one runs the ordinary load path with the ordinary\n// consent: the record confers nothing (R-OSO-22).\nimport { protocolRequest } from './sandboxUtils';\nimport { PROTOCOL_RECENTS } from './generated/protocol';\nimport { SCHEMES } from './protocolSchemes';\n\n/** One recently opened project — repository coordinates, never an in-repo path. */\nexport interface RecentProject {\n provider: string;\n namespace: string;\n repository: string;\n /** The ref the user opened (`main`, a sha). */\n ref: string;\n /** When the user last opened it (epoch ms). */\n ts: number;\n}\n\ninterface RecentsReply {\n /** Newest-first; `null` when the record is empty or the user cleared it. */\n projects: RecentProject[] | null;\n}\n\n/**\n * The one call shape the SDK speaks under `protocol-recents` — read, or clear\n * (same gated surface). A single typed call site keeps the wire contract exact:\n * the snapshot gate reads THIS shape, and the host handler accepts the same.\n */\nconst recentsRequest = (params: { clear?: boolean }): Promise<RecentsReply>
|
|
1
|
+
{"version":3,"sources":["../src/recents.ts"],"sourcesContent":["// The recent-projects record — app-facing surface (R3-485, OSO §4.3).\n//\n// `listRecentProjects()` reads the HOST-OWNED record through the one elevated,\n// app-scoped `recents:read` capability. Two gates stand between an app and the\n// record, and both must pass: the §8.4 capability grant (declared as\n// `requests: { 'recents:read': {} }` and consented per (app, principal)), and the\n// handler's structural binding — the read is granted only to the app bound at\n// `page.home`. For every other app the call resolves to a refusal, whatever it\n// declared.\n//\n// A project entry is COORDINATES (provider/namespace/repository/ref + when) — a\n// location the user already navigated to under their own authority, never an\n// in-repo path. Opening one runs the ordinary load path with the ordinary\n// consent: the record confers nothing (R-OSO-22).\nimport { protocolRequest } from './sandboxUtils';\nimport { PROTOCOL_RECENTS } from './generated/protocol';\nimport { SCHEMES } from './protocolSchemes';\n\n/** One recently opened project — repository coordinates, never an in-repo path. */\nexport interface RecentProject {\n provider: string;\n namespace: string;\n repository: string;\n /** The ref the user opened (`main`, a sha). */\n ref: string;\n /** When the user last opened it (epoch ms). */\n ts: number;\n}\n\ninterface RecentsReply {\n /** Newest-first; `null` when the record is empty or the user cleared it. */\n projects: RecentProject[] | null;\n}\n\n/**\n * The host wraps every action result as `{ ok: true, data }` and every refusal\n * as `{ ok: false, code, message }` INSIDE the reply's `result` — refusals\n * resolve rather than reject, because the sandbox's protocolRequest rejects\n * with a bare `Error` and would drop the `code` (requestDispatcher's reply\n * contract). Unwrap here, like every other gated module, so callers see the\n * coded refusal and never a silent `null`.\n */\ntype RecentsResult = { ok: true; data: RecentsReply } | { ok: false; code: string; message?: string };\n\n/**\n * The one call shape the SDK speaks under `protocol-recents` — read, or clear\n * (same gated surface). A single typed call site keeps the wire contract exact:\n * the snapshot gate reads THIS shape, and the host handler accepts the same.\n */\nconst recentsRequest = async (params: { clear?: boolean }): Promise<RecentsReply> => {\n const res = (await protocolRequest(SCHEMES[PROTOCOL_RECENTS], 'list', [params])) as RecentsResult;\n if (!res || res.ok !== true) {\n const err = new Error(res?.message ?? 'recents request failed') as Error & { code: string };\n err.code = res?.code ?? 'unknown';\n throw err;\n }\n return res.data;\n};\n\n/**\n * The user's recently opened projects, newest-first, or `null` when the record is\n * absent (R-OSO-22: cleared is absent, never an empty list). Refuses for any app\n * that is not the `page.home` binding.\n *\n * @throws a coded error — `forbidden` without the grant or the binding, or the\n * host's own code; never a silent `null` for a refusal.\n */\nexport async function listRecentProjects(): Promise<RecentProject[] | null> {\n const res = await recentsRequest({});\n return res.projects ?? null;\n}\n\n/**\n * Clear the user's recent-projects record (the surface afterwards is absent, not\n * empty). Rides the same page.home binding gate as the read.\n *\n * @throws a coded error when the clear is refused — the caller must not treat\n * the record as cleared.\n */\nexport async function clearRecentProjects(): Promise<void> {\n await recentsRequest({ clear: true });\n}\n"],"mappings":";AAcA,SAAS,uBAAuB;AAChC,SAAS,wBAAwB;AACjC,SAAS,eAAe;AAiCxB,MAAM,iBAAiB,OAAO,WAAuD;AACnF,QAAM,MAAO,MAAM,gBAAgB,QAAQ,gBAAgB,GAAG,QAAQ,CAAC,MAAM,CAAC;AAC9E,MAAI,CAAC,OAAO,IAAI,OAAO,MAAM;AAC3B,UAAM,MAAM,IAAI,MAAM,KAAK,WAAW,wBAAwB;AAC9D,QAAI,OAAO,KAAK,QAAQ;AACxB,UAAM;AAAA,EACR;AACA,SAAO,IAAI;AACb;AAUA,eAAsB,qBAAsD;AAC1E,QAAM,MAAM,MAAM,eAAe,CAAC,CAAC;AACnC,SAAO,IAAI,YAAY;AACzB;AASA,eAAsB,sBAAqC;AACzD,QAAM,eAAe,EAAE,OAAO,KAAK,CAAC;AACtC;","names":[]}
|
package/dist/urlUtils.cjs
CHANGED
|
@@ -30,6 +30,7 @@ __export(urlUtils_exports, {
|
|
|
30
30
|
parseHref: () => parseHref,
|
|
31
31
|
parsePath: () => parsePath,
|
|
32
32
|
parseTarget: () => parseTarget,
|
|
33
|
+
platformHref: () => platformHref,
|
|
33
34
|
repositoryPrefixURL: () => repositoryPrefixURL,
|
|
34
35
|
splitHash: () => splitHash,
|
|
35
36
|
underAppRoot: () => import_platform_constants.underAppRoot
|
|
@@ -92,6 +93,17 @@ const isInternalHref = (outerHref, target, navigationState) => {
|
|
|
92
93
|
}
|
|
93
94
|
return true;
|
|
94
95
|
};
|
|
96
|
+
const platformHref = (outerHref, path) => {
|
|
97
|
+
if (typeof path !== "string") {
|
|
98
|
+
throw new TypeError(`platformHref: path must be a string, got ${typeof path}`);
|
|
99
|
+
}
|
|
100
|
+
if (!outerHref) return path;
|
|
101
|
+
try {
|
|
102
|
+
return new URL(path, outerHref).toString();
|
|
103
|
+
} catch {
|
|
104
|
+
return path;
|
|
105
|
+
}
|
|
106
|
+
};
|
|
95
107
|
const decodeSegment = (value) => {
|
|
96
108
|
try {
|
|
97
109
|
return decodeURIComponent(value);
|
|
@@ -170,6 +182,7 @@ const constructUrl = (outerHref, navigationState) => {
|
|
|
170
182
|
parseHref,
|
|
171
183
|
parsePath,
|
|
172
184
|
parseTarget,
|
|
185
|
+
platformHref,
|
|
173
186
|
repositoryPrefixURL,
|
|
174
187
|
splitHash,
|
|
175
188
|
underAppRoot
|
package/dist/urlUtils.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/urlUtils.ts"],"sourcesContent":["import { APP_ROOT, underAppRoot } from '@immediately-run/platform-constants';\n\nimport { joinPaths } from './pathUtils';\nimport { NavigationState, PathState } from './TinkerableContext';\n\nexport const FILES_PREFIX = '/files';\n\n/**\n * Mount point of the Git repository inside the sandbox filesystem, and the join\n * into it — both now defined ONCE in `@immediately-run/platform-constants` (R3-275)\n * and re-exported here so every import site is unchanged.\n *\n * They were declared here AND in `sandbox/src/fsLayout.ts`, with two different\n * implementations of the join (this one over `joinPaths`, the sandbox's hand-rolled).\n * The two agreed; nothing made them agree — and the metadata key space is derived\n * from this constant on both sides, so a drift would have split that key space\n * silently. The package's tests replicate BOTH implementations, quirks included.\n *\n * The sandbox fs is rooted at `/` (so apps can reach dynamic mounts like\n * `/firestore`), with the repo mounted at `APP_ROOT`. URL subpaths are\n * repo-relative, so the file router resolves them under it.\n */\nexport { APP_ROOT, underAppRoot };\n\n/**\n * The origin every outer URL is rebuilt on — `protocol//host`, **port included**.\n *\n * `url.hostname` drops the port; `url.host` keeps it (and still omits it for the default\n * 80/443, so a production URL is byte-identical either way). Using `hostname` here was\n * invisible on `https://immediately.run` and broke every routed link under local dev on any\n * port but 80: `http://localhost:3100/edit/…` was rebuilt as `http://localhost/edit/…`.\n *\n * Worse than a wrong link, it silently changed link BEHAVIOUR. `repositoryPrefixURL` is\n * built from this, and `isInternalHref` decides by prefix-matching against it — so an\n * absolute in-app URL failed the match, was classified EXTERNAL, and rendered as a plain\n * `<a>`. Clicking it performed a real navigation out of the sandboxed frame instead of\n * routing: the app appeared to \"reload\" on an ordinary internal link.\n */\nexport const getOuterHostname = (outerHref: string) => {\n const url = new URL(outerHref);\n return `${url.protocol}//${url.host}`;\n};\n\nexport const getSearchParams = (search?: string): Record<string, string> =>\n Object.fromEntries([...new URLSearchParams(search ?? window.location.search).entries()]);\n\n/**\n * Split a link target into its path part and its `#fragment` (the `#` dropped),\n * e.g. `\"FOO.mdx#sec-8-9\"` → `[\"FOO.mdx\", \"sec-8-9\"]`, `\"#sec-3\"` → `[\"\", \"sec-3\"]`,\n * `\"FOO.mdx\"` → `[\"FOO.mdx\", \"\"]`. Only the **first** `#` splits — a fragment never\n * contains another `#`. The seam between wiki-links (§13) and heading ids (§15):\n * existence is resolved on the path part, the fragment is threaded to navigation.\n * MARKDOWN_SYNTAX_SPEC §13.5.\n */\nexport const splitHash = (target: string): [string, string] => {\n const i = target.indexOf('#');\n return i === -1 ? [target, ''] : [target.slice(0, i), target.slice(i + 1)];\n};\n\nexport const parseTarget = (target: string, navigation: NavigationState): NavigationState => {\n const newNavigation = { ...navigation };\n let [prehash, hash] = target.split('#');\n if (prehash) {\n let [path, search] = prehash.split('?');\n if (path) {\n newNavigation.sandboxPath = path;\n }\n newNavigation.search = search ? search : '';\n }\n newNavigation.hash = hash ? hash : '';\n return newNavigation;\n};\n\nexport const maybeParseUrl = (str: string): URL | null => {\n try {\n return new URL(str);\n } catch (_) {\n return null;\n }\n};\n\nexport const isAbsolutePath = (sandboxPath: string) => sandboxPath.startsWith('/');\n\n/**\n * The origin+repo prefix every same-app URL starts with — used by {@link isInternalHref} to\n * decide whether an absolute href is \"this app\" or somewhere else.\n *\n * `hash` and `search` are cleared, not just `sandboxPath`. They belong to the CURRENT page,\n * and leaving them in put them on the end of a *prefix*: from a page carrying `#sec-8-9`,\n * the prefix became `…/main/#sec-8-9`, no same-app URL could start with it, every absolute\n * in-app link was classified EXTERNAL, and clicking one navigated the sandboxed frame away\n * instead of routing. A page with a fragment quietly broke its own links.\n */\nexport const repositoryPrefixURL = (outerHref: string, navigationState: NavigationState) =>\n constructUrl(outerHref, {\n ...navigationState,\n sandboxPath: '',\n hash: '',\n search: '',\n });\n\nexport const constructOuterUrl = (\n previousOuterHref: string,\n sandboxTarget: string,\n navigationState: NavigationState,\n addFilesPrefix = true,\n): string => {\n if (isAbsolutePath(sandboxTarget)) {\n // Split a trailing `#fragment` off before the target is folded into the\n // sandboxPath, and carry it in `hash` instead — a fragment addresses a section,\n // not a file, so it must not leak into the path (MARKDOWN_SYNTAX_SPEC §13.5). An\n // absolute target with no fragment clears any stale hash from the prior state.\n const [pathPart, hash] = splitHash(sandboxTarget);\n return constructUrl(previousOuterHref, {\n ...navigationState,\n sandboxPath: addFilesPrefix ? joinPaths(FILES_PREFIX, pathPart) : pathPart,\n hash,\n });\n }\n return new URL(sandboxTarget, constructUrl(previousOuterHref, navigationState)).toString();\n};\n\nexport const isInternalHref = (outerHref: string, target: string, navigationState: NavigationState) => {\n const parsedUrl = maybeParseUrl(target);\n if (parsedUrl) {\n return target.startsWith(repositoryPrefixURL(outerHref, navigationState));\n }\n // if target is not a valid URL, then assume it's relative.\n return true;\n};\n\nexport type PathSegment = {\n name: string;\n pattern: string;\n transform?: (pathSegment: string) => string;\n // When true, the leading slash that delimits this segment is optional, so the\n // whole `/segment` group can be absent. Used for the trailing sandboxPath:\n // an outer href of `/mode/provider/namespace/repository/ref` (no trailing\n // slash, no sub-path) must still parse, otherwise the regex matches nothing\n // and every segment comes back empty.\n optionalLeadingSlash?: boolean;\n // Inverse of `transform`, applied when BUILDING a url (`constructUrl`). Without it a\n // decoded ref would be written back raw and split into two segments again.\n encode?: (value: string) => string;\n};\n\n// One URL path segment is \"anything but a slash\". The previous patterns instead\n// ENUMERATED the allowed characters (`[a-zA-Z0-9-_]+`), which silently rejected three\n// real shapes — and rejection here is not an error, it is a whole-match failure that\n// makes `parsePath` return every field EMPTY, so the app's navigation state just goes\n// blank with nothing logged:\n//\n// /…/repo/v1.2.3/… a semver TAG (dot)\n// /…/foo.js/main/… a dotted REPOSITORY name (dot — very common)\n// /…/repo/claude%2Fx/… a `/`-containing REF, percent-encoded as one segment\n//\n// The third is the one that motivated this (a ref may contain `/`; the host now encodes\n// it — see site-main `encodeRef`), but the first two were already broken and are the\n// same mistake. `[^/]+` is both the fix and the accurate description of a segment.\n/** Percent-decode one path segment, tolerating a malformed escape: this is untrusted\n * URL input, and `decodeURIComponent('%zz')` throws. A bad ref must not blank out the\n * whole navigation state. */\nconst decodeSegment = (value: string): string => {\n try {\n return decodeURIComponent(value);\n } catch {\n return value;\n }\n};\n\nconst PATH_SEGMENTS: PathSegment[] = [\n { name: 'mode', pattern: '\\\\w+' },\n { name: 'provider', pattern: '[^/]+' },\n { name: 'namespace', pattern: '[^/]+' },\n { name: 'repository', pattern: '[^/]+' },\n // The ref is percent-encoded by the host so a `/` inside it stays ONE segment; decode\n // on the way in and re-encode on the way out (`encode`), so app code always sees the\n // real ref (`claude/x`) and never the wire form.\n { name: 'ref', pattern: '[^/]+', transform: decodeSegment, encode: encodeURIComponent },\n { name: 'sandboxPath', pattern: '.*', transform: (s) => `/${s}`, optionalLeadingSlash: true },\n];\n\nconst OUTER_HREF_REGEXP = new RegExp(\n '^' +\n PATH_SEGMENTS.map(({ name, pattern, optionalLeadingSlash }) =>\n optionalLeadingSlash ? `(?:\\/(?<${name}>${pattern}))?` : `\\/(?<${name}>${pattern})`,\n ).join('') +\n '$',\n);\n\nexport const parsePath = (pathname: string): PathState => {\n const matchResults = pathname.match(OUTER_HREF_REGEXP)?.groups ?? {};\n return PATH_SEGMENTS.reduce((acc: Partial<PathState>, { name, transform }: PathSegment) => {\n let value: string | undefined = undefined;\n if (name in matchResults) {\n value = matchResults[name];\n }\n if (!value) {\n // fall back to default value if var not present in\n value = '';\n }\n if (typeof value === 'string') {\n acc[name] = transform ? transform(value) : value;\n }\n return acc;\n }, {}) as PathState;\n};\n\nexport const parseHref = (href: string): NavigationState => {\n const parsedUrl = new URL(href);\n const pathnameState = parsePath(parsedUrl.pathname);\n return {\n ...pathnameState,\n search: parsedUrl.search.substring(1),\n hash: parsedUrl.hash.substring(1),\n } as NavigationState;\n};\n\nconst stripSlashPrefix = (s: string): string => (s.startsWith('/') ? s.substring(1) : s);\n\nexport const constructUrl = (outerHref: string, navigationState: NavigationState): string => {\n const path = PATH_SEGMENTS.map(({ name, encode }) => {\n const value = stripSlashPrefix(navigationState[name] ?? '');\n // Encode AFTER stripping the delimiter slash: `encode` is per-segment, and the\n // sandboxPath (which has no `encode`) keeps its own internal slashes.\n return encode ? encode(value) : value;\n }).join('/');\n const host = getOuterHostname(outerHref);\n let url = `${host}/${path}`;\n if (navigationState.search) {\n url += '?' + navigationState.search;\n }\n if (navigationState.hash) {\n url += '#' + navigationState.hash;\n }\n return url;\n};\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/urlUtils.ts"],"sourcesContent":["import { APP_ROOT, underAppRoot } from '@immediately-run/platform-constants';\n\nimport { joinPaths } from './pathUtils';\nimport { NavigationState, PathState } from './TinkerableContext';\n\nexport const FILES_PREFIX = '/files';\n\n/**\n * Mount point of the Git repository inside the sandbox filesystem, and the join\n * into it — both now defined ONCE in `@immediately-run/platform-constants` (R3-275)\n * and re-exported here so every import site is unchanged.\n *\n * They were declared here AND in `sandbox/src/fsLayout.ts`, with two different\n * implementations of the join (this one over `joinPaths`, the sandbox's hand-rolled).\n * The two agreed; nothing made them agree — and the metadata key space is derived\n * from this constant on both sides, so a drift would have split that key space\n * silently. The package's tests replicate BOTH implementations, quirks included.\n *\n * The sandbox fs is rooted at `/` (so apps can reach dynamic mounts like\n * `/firestore`), with the repo mounted at `APP_ROOT`. URL subpaths are\n * repo-relative, so the file router resolves them under it.\n */\nexport { APP_ROOT, underAppRoot };\n\n/**\n * The origin every outer URL is rebuilt on — `protocol//host`, **port included**.\n *\n * `url.hostname` drops the port; `url.host` keeps it (and still omits it for the default\n * 80/443, so a production URL is byte-identical either way). Using `hostname` here was\n * invisible on `https://immediately.run` and broke every routed link under local dev on any\n * port but 80: `http://localhost:3100/edit/…` was rebuilt as `http://localhost/edit/…`.\n *\n * Worse than a wrong link, it silently changed link BEHAVIOUR. `repositoryPrefixURL` is\n * built from this, and `isInternalHref` decides by prefix-matching against it — so an\n * absolute in-app URL failed the match, was classified EXTERNAL, and rendered as a plain\n * `<a>`. Clicking it performed a real navigation out of the sandboxed frame instead of\n * routing: the app appeared to \"reload\" on an ordinary internal link.\n */\nexport const getOuterHostname = (outerHref: string) => {\n const url = new URL(outerHref);\n return `${url.protocol}//${url.host}`;\n};\n\nexport const getSearchParams = (search?: string): Record<string, string> =>\n Object.fromEntries([...new URLSearchParams(search ?? window.location.search).entries()]);\n\n/**\n * Split a link target into its path part and its `#fragment` (the `#` dropped),\n * e.g. `\"FOO.mdx#sec-8-9\"` → `[\"FOO.mdx\", \"sec-8-9\"]`, `\"#sec-3\"` → `[\"\", \"sec-3\"]`,\n * `\"FOO.mdx\"` → `[\"FOO.mdx\", \"\"]`. Only the **first** `#` splits — a fragment never\n * contains another `#`. The seam between wiki-links (§13) and heading ids (§15):\n * existence is resolved on the path part, the fragment is threaded to navigation.\n * MARKDOWN_SYNTAX_SPEC §13.5.\n */\nexport const splitHash = (target: string): [string, string] => {\n const i = target.indexOf('#');\n return i === -1 ? [target, ''] : [target.slice(0, i), target.slice(i + 1)];\n};\n\nexport const parseTarget = (target: string, navigation: NavigationState): NavigationState => {\n const newNavigation = { ...navigation };\n let [prehash, hash] = target.split('#');\n if (prehash) {\n let [path, search] = prehash.split('?');\n if (path) {\n newNavigation.sandboxPath = path;\n }\n newNavigation.search = search ? search : '';\n }\n newNavigation.hash = hash ? hash : '';\n return newNavigation;\n};\n\nexport const maybeParseUrl = (str: string): URL | null => {\n try {\n return new URL(str);\n } catch (_) {\n return null;\n }\n};\n\nexport const isAbsolutePath = (sandboxPath: string) => sandboxPath.startsWith('/');\n\n/**\n * The origin+repo prefix every same-app URL starts with — used by {@link isInternalHref} to\n * decide whether an absolute href is \"this app\" or somewhere else.\n *\n * `hash` and `search` are cleared, not just `sandboxPath`. They belong to the CURRENT page,\n * and leaving them in put them on the end of a *prefix*: from a page carrying `#sec-8-9`,\n * the prefix became `…/main/#sec-8-9`, no same-app URL could start with it, every absolute\n * in-app link was classified EXTERNAL, and clicking one navigated the sandboxed frame away\n * instead of routing. A page with a fragment quietly broke its own links.\n */\nexport const repositoryPrefixURL = (outerHref: string, navigationState: NavigationState) =>\n constructUrl(outerHref, {\n ...navigationState,\n sandboxPath: '',\n hash: '',\n search: '',\n });\n\nexport const constructOuterUrl = (\n previousOuterHref: string,\n sandboxTarget: string,\n navigationState: NavigationState,\n addFilesPrefix = true,\n): string => {\n if (isAbsolutePath(sandboxTarget)) {\n // Split a trailing `#fragment` off before the target is folded into the\n // sandboxPath, and carry it in `hash` instead — a fragment addresses a section,\n // not a file, so it must not leak into the path (MARKDOWN_SYNTAX_SPEC §13.5). An\n // absolute target with no fragment clears any stale hash from the prior state.\n const [pathPart, hash] = splitHash(sandboxTarget);\n return constructUrl(previousOuterHref, {\n ...navigationState,\n sandboxPath: addFilesPrefix ? joinPaths(FILES_PREFIX, pathPart) : pathPart,\n hash,\n });\n }\n return new URL(sandboxTarget, constructUrl(previousOuterHref, navigationState)).toString();\n};\n\nexport const isInternalHref = (outerHref: string, target: string, navigationState: NavigationState) => {\n const parsedUrl = maybeParseUrl(target);\n if (parsedUrl) {\n return target.startsWith(repositoryPrefixURL(outerHref, navigationState));\n }\n // if target is not a valid URL, then assume it's relative.\n return true;\n};\n\n/**\n * The href to render for a PLATFORM-space path — `/present/github/…`, `/edit/github/…`,\n * `/home`, `/settings/language-model`. These are the HOST's URLs, not this app's: inside the\n * sandboxed frame a root-relative `href` resolves against the SANDBOX origin, which serves no\n * such page, so the path is resolved against the OUTER origin (`outerHref`) instead. An\n * absolute `path` (`https://…`) is returned as-is, which `new URL` already does. With no host\n * (`vite dev`, an empty `outerHref`) or an unresolvable pair, `path` is returned unchanged so\n * the local dev server keeps working. A non-string `path` throws: a caller passing one has a\n * bug worth surfacing, not silently rendering.\n *\n * Render the result through `PlatformLink` (`./platformLink`), which also carries\n * `target=\"_top\"` — an anchor inside the sandboxed frame otherwise navigates the frame.\n */\nexport const platformHref = (outerHref: string, path: string): string => {\n if (typeof path !== 'string') {\n throw new TypeError(`platformHref: path must be a string, got ${typeof path}`);\n }\n if (!outerHref) return path;\n try {\n return new URL(path, outerHref).toString();\n } catch {\n return path;\n }\n};\n\nexport type PathSegment = {\n name: string;\n pattern: string;\n transform?: (pathSegment: string) => string;\n // When true, the leading slash that delimits this segment is optional, so the\n // whole `/segment` group can be absent. Used for the trailing sandboxPath:\n // an outer href of `/mode/provider/namespace/repository/ref` (no trailing\n // slash, no sub-path) must still parse, otherwise the regex matches nothing\n // and every segment comes back empty.\n optionalLeadingSlash?: boolean;\n // Inverse of `transform`, applied when BUILDING a url (`constructUrl`). Without it a\n // decoded ref would be written back raw and split into two segments again.\n encode?: (value: string) => string;\n};\n\n// One URL path segment is \"anything but a slash\". The previous patterns instead\n// ENUMERATED the allowed characters (`[a-zA-Z0-9-_]+`), which silently rejected three\n// real shapes — and rejection here is not an error, it is a whole-match failure that\n// makes `parsePath` return every field EMPTY, so the app's navigation state just goes\n// blank with nothing logged:\n//\n// /…/repo/v1.2.3/… a semver TAG (dot)\n// /…/foo.js/main/… a dotted REPOSITORY name (dot — very common)\n// /…/repo/claude%2Fx/… a `/`-containing REF, percent-encoded as one segment\n//\n// The third is the one that motivated this (a ref may contain `/`; the host now encodes\n// it — see site-main `encodeRef`), but the first two were already broken and are the\n// same mistake. `[^/]+` is both the fix and the accurate description of a segment.\n/** Percent-decode one path segment, tolerating a malformed escape: this is untrusted\n * URL input, and `decodeURIComponent('%zz')` throws. A bad ref must not blank out the\n * whole navigation state. */\nconst decodeSegment = (value: string): string => {\n try {\n return decodeURIComponent(value);\n } catch {\n return value;\n }\n};\n\nconst PATH_SEGMENTS: PathSegment[] = [\n { name: 'mode', pattern: '\\\\w+' },\n { name: 'provider', pattern: '[^/]+' },\n { name: 'namespace', pattern: '[^/]+' },\n { name: 'repository', pattern: '[^/]+' },\n // The ref is percent-encoded by the host so a `/` inside it stays ONE segment; decode\n // on the way in and re-encode on the way out (`encode`), so app code always sees the\n // real ref (`claude/x`) and never the wire form.\n { name: 'ref', pattern: '[^/]+', transform: decodeSegment, encode: encodeURIComponent },\n { name: 'sandboxPath', pattern: '.*', transform: (s) => `/${s}`, optionalLeadingSlash: true },\n];\n\nconst OUTER_HREF_REGEXP = new RegExp(\n '^' +\n PATH_SEGMENTS.map(({ name, pattern, optionalLeadingSlash }) =>\n optionalLeadingSlash ? `(?:\\/(?<${name}>${pattern}))?` : `\\/(?<${name}>${pattern})`,\n ).join('') +\n '$',\n);\n\nexport const parsePath = (pathname: string): PathState => {\n const matchResults = pathname.match(OUTER_HREF_REGEXP)?.groups ?? {};\n return PATH_SEGMENTS.reduce((acc: Partial<PathState>, { name, transform }: PathSegment) => {\n let value: string | undefined = undefined;\n if (name in matchResults) {\n value = matchResults[name];\n }\n if (!value) {\n // fall back to default value if var not present in\n value = '';\n }\n if (typeof value === 'string') {\n acc[name] = transform ? transform(value) : value;\n }\n return acc;\n }, {}) as PathState;\n};\n\nexport const parseHref = (href: string): NavigationState => {\n const parsedUrl = new URL(href);\n const pathnameState = parsePath(parsedUrl.pathname);\n return {\n ...pathnameState,\n search: parsedUrl.search.substring(1),\n hash: parsedUrl.hash.substring(1),\n } as NavigationState;\n};\n\nconst stripSlashPrefix = (s: string): string => (s.startsWith('/') ? s.substring(1) : s);\n\nexport const constructUrl = (outerHref: string, navigationState: NavigationState): string => {\n const path = PATH_SEGMENTS.map(({ name, encode }) => {\n const value = stripSlashPrefix(navigationState[name] ?? '');\n // Encode AFTER stripping the delimiter slash: `encode` is per-segment, and the\n // sandboxPath (which has no `encode`) keeps its own internal slashes.\n return encode ? encode(value) : value;\n }).join('/');\n const host = getOuterHostname(outerHref);\n let url = `${host}/${path}`;\n if (navigationState.search) {\n url += '?' + navigationState.search;\n }\n if (navigationState.hash) {\n url += '#' + navigationState.hash;\n }\n return url;\n};\n"],"mappings":"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","names":[]}
|
package/dist/urlUtils.d.cts
CHANGED
|
@@ -47,6 +47,20 @@ declare const isAbsolutePath: (sandboxPath: string) => boolean;
|
|
|
47
47
|
declare const repositoryPrefixURL: (outerHref: string, navigationState: NavigationState) => string;
|
|
48
48
|
declare const constructOuterUrl: (previousOuterHref: string, sandboxTarget: string, navigationState: NavigationState, addFilesPrefix?: boolean) => string;
|
|
49
49
|
declare const isInternalHref: (outerHref: string, target: string, navigationState: NavigationState) => boolean;
|
|
50
|
+
/**
|
|
51
|
+
* The href to render for a PLATFORM-space path — `/present/github/…`, `/edit/github/…`,
|
|
52
|
+
* `/home`, `/settings/language-model`. These are the HOST's URLs, not this app's: inside the
|
|
53
|
+
* sandboxed frame a root-relative `href` resolves against the SANDBOX origin, which serves no
|
|
54
|
+
* such page, so the path is resolved against the OUTER origin (`outerHref`) instead. An
|
|
55
|
+
* absolute `path` (`https://…`) is returned as-is, which `new URL` already does. With no host
|
|
56
|
+
* (`vite dev`, an empty `outerHref`) or an unresolvable pair, `path` is returned unchanged so
|
|
57
|
+
* the local dev server keeps working. A non-string `path` throws: a caller passing one has a
|
|
58
|
+
* bug worth surfacing, not silently rendering.
|
|
59
|
+
*
|
|
60
|
+
* Render the result through `PlatformLink` (`./platformLink`), which also carries
|
|
61
|
+
* `target="_top"` — an anchor inside the sandboxed frame otherwise navigates the frame.
|
|
62
|
+
*/
|
|
63
|
+
declare const platformHref: (outerHref: string, path: string) => string;
|
|
50
64
|
type PathSegment = {
|
|
51
65
|
name: string;
|
|
52
66
|
pattern: string;
|
|
@@ -58,4 +72,4 @@ declare const parsePath: (pathname: string) => PathState;
|
|
|
58
72
|
declare const parseHref: (href: string) => NavigationState;
|
|
59
73
|
declare const constructUrl: (outerHref: string, navigationState: NavigationState) => string;
|
|
60
74
|
|
|
61
|
-
export { FILES_PREFIX, type PathSegment, constructOuterUrl, constructUrl, getOuterHostname, getSearchParams, isAbsolutePath, isInternalHref, maybeParseUrl, parseHref, parsePath, parseTarget, repositoryPrefixURL, splitHash };
|
|
75
|
+
export { FILES_PREFIX, type PathSegment, constructOuterUrl, constructUrl, getOuterHostname, getSearchParams, isAbsolutePath, isInternalHref, maybeParseUrl, parseHref, parsePath, parseTarget, platformHref, repositoryPrefixURL, splitHash };
|
package/dist/urlUtils.d.ts
CHANGED
|
@@ -47,6 +47,20 @@ declare const isAbsolutePath: (sandboxPath: string) => boolean;
|
|
|
47
47
|
declare const repositoryPrefixURL: (outerHref: string, navigationState: NavigationState) => string;
|
|
48
48
|
declare const constructOuterUrl: (previousOuterHref: string, sandboxTarget: string, navigationState: NavigationState, addFilesPrefix?: boolean) => string;
|
|
49
49
|
declare const isInternalHref: (outerHref: string, target: string, navigationState: NavigationState) => boolean;
|
|
50
|
+
/**
|
|
51
|
+
* The href to render for a PLATFORM-space path — `/present/github/…`, `/edit/github/…`,
|
|
52
|
+
* `/home`, `/settings/language-model`. These are the HOST's URLs, not this app's: inside the
|
|
53
|
+
* sandboxed frame a root-relative `href` resolves against the SANDBOX origin, which serves no
|
|
54
|
+
* such page, so the path is resolved against the OUTER origin (`outerHref`) instead. An
|
|
55
|
+
* absolute `path` (`https://…`) is returned as-is, which `new URL` already does. With no host
|
|
56
|
+
* (`vite dev`, an empty `outerHref`) or an unresolvable pair, `path` is returned unchanged so
|
|
57
|
+
* the local dev server keeps working. A non-string `path` throws: a caller passing one has a
|
|
58
|
+
* bug worth surfacing, not silently rendering.
|
|
59
|
+
*
|
|
60
|
+
* Render the result through `PlatformLink` (`./platformLink`), which also carries
|
|
61
|
+
* `target="_top"` — an anchor inside the sandboxed frame otherwise navigates the frame.
|
|
62
|
+
*/
|
|
63
|
+
declare const platformHref: (outerHref: string, path: string) => string;
|
|
50
64
|
type PathSegment = {
|
|
51
65
|
name: string;
|
|
52
66
|
pattern: string;
|
|
@@ -58,4 +72,4 @@ declare const parsePath: (pathname: string) => PathState;
|
|
|
58
72
|
declare const parseHref: (href: string) => NavigationState;
|
|
59
73
|
declare const constructUrl: (outerHref: string, navigationState: NavigationState) => string;
|
|
60
74
|
|
|
61
|
-
export { FILES_PREFIX, type PathSegment, constructOuterUrl, constructUrl, getOuterHostname, getSearchParams, isAbsolutePath, isInternalHref, maybeParseUrl, parseHref, parsePath, parseTarget, repositoryPrefixURL, splitHash };
|
|
75
|
+
export { FILES_PREFIX, type PathSegment, constructOuterUrl, constructUrl, getOuterHostname, getSearchParams, isAbsolutePath, isInternalHref, maybeParseUrl, parseHref, parsePath, parseTarget, platformHref, repositoryPrefixURL, splitHash };
|
package/dist/urlUtils.js
CHANGED
|
@@ -56,6 +56,17 @@ const isInternalHref = (outerHref, target, navigationState) => {
|
|
|
56
56
|
}
|
|
57
57
|
return true;
|
|
58
58
|
};
|
|
59
|
+
const platformHref = (outerHref, path) => {
|
|
60
|
+
if (typeof path !== "string") {
|
|
61
|
+
throw new TypeError(`platformHref: path must be a string, got ${typeof path}`);
|
|
62
|
+
}
|
|
63
|
+
if (!outerHref) return path;
|
|
64
|
+
try {
|
|
65
|
+
return new URL(path, outerHref).toString();
|
|
66
|
+
} catch {
|
|
67
|
+
return path;
|
|
68
|
+
}
|
|
69
|
+
};
|
|
59
70
|
const decodeSegment = (value) => {
|
|
60
71
|
try {
|
|
61
72
|
return decodeURIComponent(value);
|
|
@@ -133,6 +144,7 @@ export {
|
|
|
133
144
|
parseHref,
|
|
134
145
|
parsePath,
|
|
135
146
|
parseTarget,
|
|
147
|
+
platformHref,
|
|
136
148
|
repositoryPrefixURL,
|
|
137
149
|
splitHash,
|
|
138
150
|
underAppRoot
|
package/dist/urlUtils.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/urlUtils.ts"],"sourcesContent":["import { APP_ROOT, underAppRoot } from '@immediately-run/platform-constants';\n\nimport { joinPaths } from './pathUtils';\nimport { NavigationState, PathState } from './TinkerableContext';\n\nexport const FILES_PREFIX = '/files';\n\n/**\n * Mount point of the Git repository inside the sandbox filesystem, and the join\n * into it — both now defined ONCE in `@immediately-run/platform-constants` (R3-275)\n * and re-exported here so every import site is unchanged.\n *\n * They were declared here AND in `sandbox/src/fsLayout.ts`, with two different\n * implementations of the join (this one over `joinPaths`, the sandbox's hand-rolled).\n * The two agreed; nothing made them agree — and the metadata key space is derived\n * from this constant on both sides, so a drift would have split that key space\n * silently. The package's tests replicate BOTH implementations, quirks included.\n *\n * The sandbox fs is rooted at `/` (so apps can reach dynamic mounts like\n * `/firestore`), with the repo mounted at `APP_ROOT`. URL subpaths are\n * repo-relative, so the file router resolves them under it.\n */\nexport { APP_ROOT, underAppRoot };\n\n/**\n * The origin every outer URL is rebuilt on — `protocol//host`, **port included**.\n *\n * `url.hostname` drops the port; `url.host` keeps it (and still omits it for the default\n * 80/443, so a production URL is byte-identical either way). Using `hostname` here was\n * invisible on `https://immediately.run` and broke every routed link under local dev on any\n * port but 80: `http://localhost:3100/edit/…` was rebuilt as `http://localhost/edit/…`.\n *\n * Worse than a wrong link, it silently changed link BEHAVIOUR. `repositoryPrefixURL` is\n * built from this, and `isInternalHref` decides by prefix-matching against it — so an\n * absolute in-app URL failed the match, was classified EXTERNAL, and rendered as a plain\n * `<a>`. Clicking it performed a real navigation out of the sandboxed frame instead of\n * routing: the app appeared to \"reload\" on an ordinary internal link.\n */\nexport const getOuterHostname = (outerHref: string) => {\n const url = new URL(outerHref);\n return `${url.protocol}//${url.host}`;\n};\n\nexport const getSearchParams = (search?: string): Record<string, string> =>\n Object.fromEntries([...new URLSearchParams(search ?? window.location.search).entries()]);\n\n/**\n * Split a link target into its path part and its `#fragment` (the `#` dropped),\n * e.g. `\"FOO.mdx#sec-8-9\"` → `[\"FOO.mdx\", \"sec-8-9\"]`, `\"#sec-3\"` → `[\"\", \"sec-3\"]`,\n * `\"FOO.mdx\"` → `[\"FOO.mdx\", \"\"]`. Only the **first** `#` splits — a fragment never\n * contains another `#`. The seam between wiki-links (§13) and heading ids (§15):\n * existence is resolved on the path part, the fragment is threaded to navigation.\n * MARKDOWN_SYNTAX_SPEC §13.5.\n */\nexport const splitHash = (target: string): [string, string] => {\n const i = target.indexOf('#');\n return i === -1 ? [target, ''] : [target.slice(0, i), target.slice(i + 1)];\n};\n\nexport const parseTarget = (target: string, navigation: NavigationState): NavigationState => {\n const newNavigation = { ...navigation };\n let [prehash, hash] = target.split('#');\n if (prehash) {\n let [path, search] = prehash.split('?');\n if (path) {\n newNavigation.sandboxPath = path;\n }\n newNavigation.search = search ? search : '';\n }\n newNavigation.hash = hash ? hash : '';\n return newNavigation;\n};\n\nexport const maybeParseUrl = (str: string): URL | null => {\n try {\n return new URL(str);\n } catch (_) {\n return null;\n }\n};\n\nexport const isAbsolutePath = (sandboxPath: string) => sandboxPath.startsWith('/');\n\n/**\n * The origin+repo prefix every same-app URL starts with — used by {@link isInternalHref} to\n * decide whether an absolute href is \"this app\" or somewhere else.\n *\n * `hash` and `search` are cleared, not just `sandboxPath`. They belong to the CURRENT page,\n * and leaving them in put them on the end of a *prefix*: from a page carrying `#sec-8-9`,\n * the prefix became `…/main/#sec-8-9`, no same-app URL could start with it, every absolute\n * in-app link was classified EXTERNAL, and clicking one navigated the sandboxed frame away\n * instead of routing. A page with a fragment quietly broke its own links.\n */\nexport const repositoryPrefixURL = (outerHref: string, navigationState: NavigationState) =>\n constructUrl(outerHref, {\n ...navigationState,\n sandboxPath: '',\n hash: '',\n search: '',\n });\n\nexport const constructOuterUrl = (\n previousOuterHref: string,\n sandboxTarget: string,\n navigationState: NavigationState,\n addFilesPrefix = true,\n): string => {\n if (isAbsolutePath(sandboxTarget)) {\n // Split a trailing `#fragment` off before the target is folded into the\n // sandboxPath, and carry it in `hash` instead — a fragment addresses a section,\n // not a file, so it must not leak into the path (MARKDOWN_SYNTAX_SPEC §13.5). An\n // absolute target with no fragment clears any stale hash from the prior state.\n const [pathPart, hash] = splitHash(sandboxTarget);\n return constructUrl(previousOuterHref, {\n ...navigationState,\n sandboxPath: addFilesPrefix ? joinPaths(FILES_PREFIX, pathPart) : pathPart,\n hash,\n });\n }\n return new URL(sandboxTarget, constructUrl(previousOuterHref, navigationState)).toString();\n};\n\nexport const isInternalHref = (outerHref: string, target: string, navigationState: NavigationState) => {\n const parsedUrl = maybeParseUrl(target);\n if (parsedUrl) {\n return target.startsWith(repositoryPrefixURL(outerHref, navigationState));\n }\n // if target is not a valid URL, then assume it's relative.\n return true;\n};\n\nexport type PathSegment = {\n name: string;\n pattern: string;\n transform?: (pathSegment: string) => string;\n // When true, the leading slash that delimits this segment is optional, so the\n // whole `/segment` group can be absent. Used for the trailing sandboxPath:\n // an outer href of `/mode/provider/namespace/repository/ref` (no trailing\n // slash, no sub-path) must still parse, otherwise the regex matches nothing\n // and every segment comes back empty.\n optionalLeadingSlash?: boolean;\n // Inverse of `transform`, applied when BUILDING a url (`constructUrl`). Without it a\n // decoded ref would be written back raw and split into two segments again.\n encode?: (value: string) => string;\n};\n\n// One URL path segment is \"anything but a slash\". The previous patterns instead\n// ENUMERATED the allowed characters (`[a-zA-Z0-9-_]+`), which silently rejected three\n// real shapes — and rejection here is not an error, it is a whole-match failure that\n// makes `parsePath` return every field EMPTY, so the app's navigation state just goes\n// blank with nothing logged:\n//\n// /…/repo/v1.2.3/… a semver TAG (dot)\n// /…/foo.js/main/… a dotted REPOSITORY name (dot — very common)\n// /…/repo/claude%2Fx/… a `/`-containing REF, percent-encoded as one segment\n//\n// The third is the one that motivated this (a ref may contain `/`; the host now encodes\n// it — see site-main `encodeRef`), but the first two were already broken and are the\n// same mistake. `[^/]+` is both the fix and the accurate description of a segment.\n/** Percent-decode one path segment, tolerating a malformed escape: this is untrusted\n * URL input, and `decodeURIComponent('%zz')` throws. A bad ref must not blank out the\n * whole navigation state. */\nconst decodeSegment = (value: string): string => {\n try {\n return decodeURIComponent(value);\n } catch {\n return value;\n }\n};\n\nconst PATH_SEGMENTS: PathSegment[] = [\n { name: 'mode', pattern: '\\\\w+' },\n { name: 'provider', pattern: '[^/]+' },\n { name: 'namespace', pattern: '[^/]+' },\n { name: 'repository', pattern: '[^/]+' },\n // The ref is percent-encoded by the host so a `/` inside it stays ONE segment; decode\n // on the way in and re-encode on the way out (`encode`), so app code always sees the\n // real ref (`claude/x`) and never the wire form.\n { name: 'ref', pattern: '[^/]+', transform: decodeSegment, encode: encodeURIComponent },\n { name: 'sandboxPath', pattern: '.*', transform: (s) => `/${s}`, optionalLeadingSlash: true },\n];\n\nconst OUTER_HREF_REGEXP = new RegExp(\n '^' +\n PATH_SEGMENTS.map(({ name, pattern, optionalLeadingSlash }) =>\n optionalLeadingSlash ? `(?:\\/(?<${name}>${pattern}))?` : `\\/(?<${name}>${pattern})`,\n ).join('') +\n '$',\n);\n\nexport const parsePath = (pathname: string): PathState => {\n const matchResults = pathname.match(OUTER_HREF_REGEXP)?.groups ?? {};\n return PATH_SEGMENTS.reduce((acc: Partial<PathState>, { name, transform }: PathSegment) => {\n let value: string | undefined = undefined;\n if (name in matchResults) {\n value = matchResults[name];\n }\n if (!value) {\n // fall back to default value if var not present in\n value = '';\n }\n if (typeof value === 'string') {\n acc[name] = transform ? transform(value) : value;\n }\n return acc;\n }, {}) as PathState;\n};\n\nexport const parseHref = (href: string): NavigationState => {\n const parsedUrl = new URL(href);\n const pathnameState = parsePath(parsedUrl.pathname);\n return {\n ...pathnameState,\n search: parsedUrl.search.substring(1),\n hash: parsedUrl.hash.substring(1),\n } as NavigationState;\n};\n\nconst stripSlashPrefix = (s: string): string => (s.startsWith('/') ? s.substring(1) : s);\n\nexport const constructUrl = (outerHref: string, navigationState: NavigationState): string => {\n const path = PATH_SEGMENTS.map(({ name, encode }) => {\n const value = stripSlashPrefix(navigationState[name] ?? '');\n // Encode AFTER stripping the delimiter slash: `encode` is per-segment, and the\n // sandboxPath (which has no `encode`) keeps its own internal slashes.\n return encode ? encode(value) : value;\n }).join('/');\n const host = getOuterHostname(outerHref);\n let url = `${host}/${path}`;\n if (navigationState.search) {\n url += '?' + navigationState.search;\n }\n if (navigationState.hash) {\n url += '#' + navigationState.hash;\n }\n return url;\n};\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/urlUtils.ts"],"sourcesContent":["import { APP_ROOT, underAppRoot } from '@immediately-run/platform-constants';\n\nimport { joinPaths } from './pathUtils';\nimport { NavigationState, PathState } from './TinkerableContext';\n\nexport const FILES_PREFIX = '/files';\n\n/**\n * Mount point of the Git repository inside the sandbox filesystem, and the join\n * into it — both now defined ONCE in `@immediately-run/platform-constants` (R3-275)\n * and re-exported here so every import site is unchanged.\n *\n * They were declared here AND in `sandbox/src/fsLayout.ts`, with two different\n * implementations of the join (this one over `joinPaths`, the sandbox's hand-rolled).\n * The two agreed; nothing made them agree — and the metadata key space is derived\n * from this constant on both sides, so a drift would have split that key space\n * silently. The package's tests replicate BOTH implementations, quirks included.\n *\n * The sandbox fs is rooted at `/` (so apps can reach dynamic mounts like\n * `/firestore`), with the repo mounted at `APP_ROOT`. URL subpaths are\n * repo-relative, so the file router resolves them under it.\n */\nexport { APP_ROOT, underAppRoot };\n\n/**\n * The origin every outer URL is rebuilt on — `protocol//host`, **port included**.\n *\n * `url.hostname` drops the port; `url.host` keeps it (and still omits it for the default\n * 80/443, so a production URL is byte-identical either way). Using `hostname` here was\n * invisible on `https://immediately.run` and broke every routed link under local dev on any\n * port but 80: `http://localhost:3100/edit/…` was rebuilt as `http://localhost/edit/…`.\n *\n * Worse than a wrong link, it silently changed link BEHAVIOUR. `repositoryPrefixURL` is\n * built from this, and `isInternalHref` decides by prefix-matching against it — so an\n * absolute in-app URL failed the match, was classified EXTERNAL, and rendered as a plain\n * `<a>`. Clicking it performed a real navigation out of the sandboxed frame instead of\n * routing: the app appeared to \"reload\" on an ordinary internal link.\n */\nexport const getOuterHostname = (outerHref: string) => {\n const url = new URL(outerHref);\n return `${url.protocol}//${url.host}`;\n};\n\nexport const getSearchParams = (search?: string): Record<string, string> =>\n Object.fromEntries([...new URLSearchParams(search ?? window.location.search).entries()]);\n\n/**\n * Split a link target into its path part and its `#fragment` (the `#` dropped),\n * e.g. `\"FOO.mdx#sec-8-9\"` → `[\"FOO.mdx\", \"sec-8-9\"]`, `\"#sec-3\"` → `[\"\", \"sec-3\"]`,\n * `\"FOO.mdx\"` → `[\"FOO.mdx\", \"\"]`. Only the **first** `#` splits — a fragment never\n * contains another `#`. The seam between wiki-links (§13) and heading ids (§15):\n * existence is resolved on the path part, the fragment is threaded to navigation.\n * MARKDOWN_SYNTAX_SPEC §13.5.\n */\nexport const splitHash = (target: string): [string, string] => {\n const i = target.indexOf('#');\n return i === -1 ? [target, ''] : [target.slice(0, i), target.slice(i + 1)];\n};\n\nexport const parseTarget = (target: string, navigation: NavigationState): NavigationState => {\n const newNavigation = { ...navigation };\n let [prehash, hash] = target.split('#');\n if (prehash) {\n let [path, search] = prehash.split('?');\n if (path) {\n newNavigation.sandboxPath = path;\n }\n newNavigation.search = search ? search : '';\n }\n newNavigation.hash = hash ? hash : '';\n return newNavigation;\n};\n\nexport const maybeParseUrl = (str: string): URL | null => {\n try {\n return new URL(str);\n } catch (_) {\n return null;\n }\n};\n\nexport const isAbsolutePath = (sandboxPath: string) => sandboxPath.startsWith('/');\n\n/**\n * The origin+repo prefix every same-app URL starts with — used by {@link isInternalHref} to\n * decide whether an absolute href is \"this app\" or somewhere else.\n *\n * `hash` and `search` are cleared, not just `sandboxPath`. They belong to the CURRENT page,\n * and leaving them in put them on the end of a *prefix*: from a page carrying `#sec-8-9`,\n * the prefix became `…/main/#sec-8-9`, no same-app URL could start with it, every absolute\n * in-app link was classified EXTERNAL, and clicking one navigated the sandboxed frame away\n * instead of routing. A page with a fragment quietly broke its own links.\n */\nexport const repositoryPrefixURL = (outerHref: string, navigationState: NavigationState) =>\n constructUrl(outerHref, {\n ...navigationState,\n sandboxPath: '',\n hash: '',\n search: '',\n });\n\nexport const constructOuterUrl = (\n previousOuterHref: string,\n sandboxTarget: string,\n navigationState: NavigationState,\n addFilesPrefix = true,\n): string => {\n if (isAbsolutePath(sandboxTarget)) {\n // Split a trailing `#fragment` off before the target is folded into the\n // sandboxPath, and carry it in `hash` instead — a fragment addresses a section,\n // not a file, so it must not leak into the path (MARKDOWN_SYNTAX_SPEC §13.5). An\n // absolute target with no fragment clears any stale hash from the prior state.\n const [pathPart, hash] = splitHash(sandboxTarget);\n return constructUrl(previousOuterHref, {\n ...navigationState,\n sandboxPath: addFilesPrefix ? joinPaths(FILES_PREFIX, pathPart) : pathPart,\n hash,\n });\n }\n return new URL(sandboxTarget, constructUrl(previousOuterHref, navigationState)).toString();\n};\n\nexport const isInternalHref = (outerHref: string, target: string, navigationState: NavigationState) => {\n const parsedUrl = maybeParseUrl(target);\n if (parsedUrl) {\n return target.startsWith(repositoryPrefixURL(outerHref, navigationState));\n }\n // if target is not a valid URL, then assume it's relative.\n return true;\n};\n\n/**\n * The href to render for a PLATFORM-space path — `/present/github/…`, `/edit/github/…`,\n * `/home`, `/settings/language-model`. These are the HOST's URLs, not this app's: inside the\n * sandboxed frame a root-relative `href` resolves against the SANDBOX origin, which serves no\n * such page, so the path is resolved against the OUTER origin (`outerHref`) instead. An\n * absolute `path` (`https://…`) is returned as-is, which `new URL` already does. With no host\n * (`vite dev`, an empty `outerHref`) or an unresolvable pair, `path` is returned unchanged so\n * the local dev server keeps working. A non-string `path` throws: a caller passing one has a\n * bug worth surfacing, not silently rendering.\n *\n * Render the result through `PlatformLink` (`./platformLink`), which also carries\n * `target=\"_top\"` — an anchor inside the sandboxed frame otherwise navigates the frame.\n */\nexport const platformHref = (outerHref: string, path: string): string => {\n if (typeof path !== 'string') {\n throw new TypeError(`platformHref: path must be a string, got ${typeof path}`);\n }\n if (!outerHref) return path;\n try {\n return new URL(path, outerHref).toString();\n } catch {\n return path;\n }\n};\n\nexport type PathSegment = {\n name: string;\n pattern: string;\n transform?: (pathSegment: string) => string;\n // When true, the leading slash that delimits this segment is optional, so the\n // whole `/segment` group can be absent. Used for the trailing sandboxPath:\n // an outer href of `/mode/provider/namespace/repository/ref` (no trailing\n // slash, no sub-path) must still parse, otherwise the regex matches nothing\n // and every segment comes back empty.\n optionalLeadingSlash?: boolean;\n // Inverse of `transform`, applied when BUILDING a url (`constructUrl`). Without it a\n // decoded ref would be written back raw and split into two segments again.\n encode?: (value: string) => string;\n};\n\n// One URL path segment is \"anything but a slash\". The previous patterns instead\n// ENUMERATED the allowed characters (`[a-zA-Z0-9-_]+`), which silently rejected three\n// real shapes — and rejection here is not an error, it is a whole-match failure that\n// makes `parsePath` return every field EMPTY, so the app's navigation state just goes\n// blank with nothing logged:\n//\n// /…/repo/v1.2.3/… a semver TAG (dot)\n// /…/foo.js/main/… a dotted REPOSITORY name (dot — very common)\n// /…/repo/claude%2Fx/… a `/`-containing REF, percent-encoded as one segment\n//\n// The third is the one that motivated this (a ref may contain `/`; the host now encodes\n// it — see site-main `encodeRef`), but the first two were already broken and are the\n// same mistake. `[^/]+` is both the fix and the accurate description of a segment.\n/** Percent-decode one path segment, tolerating a malformed escape: this is untrusted\n * URL input, and `decodeURIComponent('%zz')` throws. A bad ref must not blank out the\n * whole navigation state. */\nconst decodeSegment = (value: string): string => {\n try {\n return decodeURIComponent(value);\n } catch {\n return value;\n }\n};\n\nconst PATH_SEGMENTS: PathSegment[] = [\n { name: 'mode', pattern: '\\\\w+' },\n { name: 'provider', pattern: '[^/]+' },\n { name: 'namespace', pattern: '[^/]+' },\n { name: 'repository', pattern: '[^/]+' },\n // The ref is percent-encoded by the host so a `/` inside it stays ONE segment; decode\n // on the way in and re-encode on the way out (`encode`), so app code always sees the\n // real ref (`claude/x`) and never the wire form.\n { name: 'ref', pattern: '[^/]+', transform: decodeSegment, encode: encodeURIComponent },\n { name: 'sandboxPath', pattern: '.*', transform: (s) => `/${s}`, optionalLeadingSlash: true },\n];\n\nconst OUTER_HREF_REGEXP = new RegExp(\n '^' +\n PATH_SEGMENTS.map(({ name, pattern, optionalLeadingSlash }) =>\n optionalLeadingSlash ? `(?:\\/(?<${name}>${pattern}))?` : `\\/(?<${name}>${pattern})`,\n ).join('') +\n '$',\n);\n\nexport const parsePath = (pathname: string): PathState => {\n const matchResults = pathname.match(OUTER_HREF_REGEXP)?.groups ?? {};\n return PATH_SEGMENTS.reduce((acc: Partial<PathState>, { name, transform }: PathSegment) => {\n let value: string | undefined = undefined;\n if (name in matchResults) {\n value = matchResults[name];\n }\n if (!value) {\n // fall back to default value if var not present in\n value = '';\n }\n if (typeof value === 'string') {\n acc[name] = transform ? transform(value) : value;\n }\n return acc;\n }, {}) as PathState;\n};\n\nexport const parseHref = (href: string): NavigationState => {\n const parsedUrl = new URL(href);\n const pathnameState = parsePath(parsedUrl.pathname);\n return {\n ...pathnameState,\n search: parsedUrl.search.substring(1),\n hash: parsedUrl.hash.substring(1),\n } as NavigationState;\n};\n\nconst stripSlashPrefix = (s: string): string => (s.startsWith('/') ? s.substring(1) : s);\n\nexport const constructUrl = (outerHref: string, navigationState: NavigationState): string => {\n const path = PATH_SEGMENTS.map(({ name, encode }) => {\n const value = stripSlashPrefix(navigationState[name] ?? '');\n // Encode AFTER stripping the delimiter slash: `encode` is per-segment, and the\n // sandboxPath (which has no `encode`) keeps its own internal slashes.\n return encode ? encode(value) : value;\n }).join('/');\n const host = getOuterHostname(outerHref);\n let url = `${host}/${path}`;\n if (navigationState.search) {\n url += '?' + navigationState.search;\n }\n if (navigationState.hash) {\n url += '#' + navigationState.hash;\n }\n return url;\n};\n"],"mappings":"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","names":[]}
|
package/dist/version.cjs
CHANGED
|
@@ -21,7 +21,7 @@ __export(version_exports, {
|
|
|
21
21
|
SDK_VERSION: () => SDK_VERSION
|
|
22
22
|
});
|
|
23
23
|
module.exports = __toCommonJS(version_exports);
|
|
24
|
-
const SDK_VERSION = "0.
|
|
24
|
+
const SDK_VERSION = "0.61.0";
|
|
25
25
|
// Annotate the CommonJS export names for ESM import in node:
|
|
26
26
|
0 && (module.exports = {
|
|
27
27
|
SDK_VERSION
|
package/dist/version.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/version.ts"],"sourcesContent":["// GENERATED by scripts/gen-version.mjs from package.json — do not edit by hand.\n// Regenerated on every build (prebuild); kept honest by version.test.ts.\n\n/** This SDK's package version, baked from package.json at build (SP2-6). */\nexport const SDK_VERSION = '0.
|
|
1
|
+
{"version":3,"sources":["../src/version.ts"],"sourcesContent":["// GENERATED by scripts/gen-version.mjs from package.json — do not edit by hand.\n// Regenerated on every build (prebuild); kept honest by version.test.ts.\n\n/** This SDK's package version, baked from package.json at build (SP2-6). */\nexport const SDK_VERSION = '0.61.0';\n"],"mappings":";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAIO,MAAM,cAAc;","names":[]}
|
package/dist/version.d.cts
CHANGED
package/dist/version.d.ts
CHANGED
package/dist/version.js
CHANGED
package/dist/version.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/version.ts"],"sourcesContent":["// GENERATED by scripts/gen-version.mjs from package.json — do not edit by hand.\n// Regenerated on every build (prebuild); kept honest by version.test.ts.\n\n/** This SDK's package version, baked from package.json at build (SP2-6). */\nexport const SDK_VERSION = '0.
|
|
1
|
+
{"version":3,"sources":["../src/version.ts"],"sourcesContent":["// GENERATED by scripts/gen-version.mjs from package.json — do not edit by hand.\n// Regenerated on every build (prebuild); kept honest by version.test.ts.\n\n/** This SDK's package version, baked from package.json at build (SP2-6). */\nexport const SDK_VERSION = '0.61.0';\n"],"mappings":";AAIO,MAAM,cAAc;","names":[]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@immediately-run/sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.61.0",
|
|
4
4
|
"description": "Runtime SDK for code executing inside an immediately.run sandbox.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": "github:immediately-run/immediately-run-sdk",
|
|
@@ -39,7 +39,10 @@
|
|
|
39
39
|
"api:update": "node scripts/check-api-stability.mjs --update",
|
|
40
40
|
"check:pins": "node scripts/check-dependency-pins.mjs --self-test && node scripts/check-dependency-pins.mjs",
|
|
41
41
|
"check:publish-version": "node scripts/check-publish-version.mjs --self-test && node scripts/check-publish-version.mjs",
|
|
42
|
-
"
|
|
42
|
+
"check:clones": "node scripts/check-clones.mjs",
|
|
43
|
+
"check:unused": "node scripts/check-unused.mjs",
|
|
44
|
+
"check:untested": "node scripts/check-untested.mjs",
|
|
45
|
+
"verify": "npm run check:pins && npm run check:publish-version && npm run format:check && npm run check:circular && npm run check:clones && npm run check:unused && npm run check:untested && npm run check:bundler:selftest && npm run check:bundler && npm run build && npm test && npm run test:safe-content && npm run test:metadata-e2e && npm run test:subpath-imports && npm run api:selftest && npm run api:check && npm run compat:selftest && npm run compat:previous && npm run protocol:check && npm run protocol:selftest && npm run check:ambient:selftest && npm run check:ambient && npm run check:selfhost:selftest && npm run check:selfhost && npm run verify:codegen-parity",
|
|
43
46
|
"docs": "typedoc --json docs/api.json && node scripts/gen-llms.mjs",
|
|
44
47
|
"prepublishOnly": "npm run check:circular && npm run build && npm run api:selftest && npm run api:check",
|
|
45
48
|
"test:safe-content": "node scripts/build-safecontent-e2e.mjs && node --test test/safeContent.e2e.mjs",
|
|
@@ -64,11 +67,12 @@
|
|
|
64
67
|
"@immediately-run/mdx-plugins": "0.5.0",
|
|
65
68
|
"@immediately-run/platform-constants": "0.2.0",
|
|
66
69
|
"@immediately-run/safe-content": "0.1.0",
|
|
67
|
-
"@immediately-run/sandbox-protocol": "0.
|
|
70
|
+
"@immediately-run/sandbox-protocol": "0.9.0",
|
|
68
71
|
"react-error-boundary": "^6.0.0"
|
|
69
72
|
},
|
|
70
73
|
"devDependencies": {
|
|
71
74
|
"@immediately-run/prettier-config": "0.1.0",
|
|
75
|
+
"@immediately-run/verify-checks": "0.1.1",
|
|
72
76
|
"@testing-library/dom": "^10.4.1",
|
|
73
77
|
"@testing-library/react": "^16.3.2",
|
|
74
78
|
"@types/jest": "^29.5.0",
|