@jini-ai/chat 0.6.6 → 0.6.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (156) hide show
  1. package/CHANGELOG.md +4 -0
  2. package/dist/core/accepted-input.d.ts +54 -0
  3. package/dist/core/accepted-input.js +165 -0
  4. package/dist/core/agent-plugin-chat-state.d.ts +43 -0
  5. package/dist/core/agent-plugin-chat-state.js +48 -0
  6. package/dist/core/assistant-client/a2ui-action-poster.js +32 -28
  7. package/dist/core/assistant-client/messages.d.ts +1 -1
  8. package/dist/core/assistant-client/messages.js +2 -2
  9. package/dist/core/assistant-client/transport.d.ts +2 -2
  10. package/dist/core/assistant-client/transport.js +184 -97
  11. package/dist/core/assistant-content.d.ts +2 -1
  12. package/dist/core/assistant-content.js +9 -11
  13. package/dist/core/browser/page-actions.js +4 -2
  14. package/dist/core/composer-history.d.ts +16 -9
  15. package/dist/core/composer-history.js +28 -16
  16. package/dist/core/conversation-title.d.ts +3 -0
  17. package/dist/core/conversation-title.js +3 -0
  18. package/dist/core/durable-projection.js +20 -13
  19. package/dist/core/durable-runs/continuation.d.ts +4 -0
  20. package/dist/core/durable-runs/continuation.js +31 -6
  21. package/dist/core/durable-runs/ledger-contracts.d.ts +2 -1
  22. package/dist/core/durable-runs/ports.d.ts +6 -2
  23. package/dist/core/index.d.ts +2 -0
  24. package/dist/core/index.js +2 -0
  25. package/dist/core/messages.d.ts +2 -0
  26. package/dist/core/partial-json.d.ts +6 -7
  27. package/dist/core/partial-json.js +81 -47
  28. package/dist/core/persistence/title.js +4 -2
  29. package/dist/core/question-answer.d.ts +23 -0
  30. package/dist/core/question-answer.js +66 -0
  31. package/dist/core/question-form/format.d.ts +6 -6
  32. package/dist/core/question-form/format.js +16 -17
  33. package/dist/core/question-form/normalize.d.ts +18 -0
  34. package/dist/core/question-form/normalize.js +112 -122
  35. package/dist/core/question-form/scan.js +141 -203
  36. package/dist/core/run-activity.js +62 -49
  37. package/dist/core/run-events/entry.d.ts +10 -1
  38. package/dist/core/run-events/entry.js +117 -87
  39. package/dist/core/todos.d.ts +1 -1
  40. package/dist/core/todos.js +60 -85
  41. package/dist/core/tool-events.js +14 -12
  42. package/dist/core/transcript.js +62 -16
  43. package/dist/core/turn-usage.js +54 -32
  44. package/dist/core/user-text-redaction.js +3 -0
  45. package/dist/core/util/manifest.js +77 -82
  46. package/dist/core/util/markdown-context.js +51 -53
  47. package/dist/core/util/parser.js +136 -144
  48. package/dist/core/util/pointer.js +16 -16
  49. package/dist/core/util/strip.js +57 -67
  50. package/dist/core/util/validate.js +15 -17
  51. package/dist/embed-widget/browser/session-store.js +21 -12
  52. package/dist/embed-widget/transports/fetch-sse.js +108 -95
  53. package/dist/embed-widget/vanilla/index.js +81 -70
  54. package/dist/react/agent-bridge/frontend-session-bridge.js +80 -52
  55. package/dist/react/components/AttachmentPreviewModal.js +25 -1
  56. package/dist/react/components/ChatSettingsDialog.d.ts +13 -0
  57. package/dist/react/components/ChatSettingsDialog.hooks.d.ts +115 -0
  58. package/dist/react/components/ChatSettingsDialog.hooks.js +40 -0
  59. package/dist/react/components/ChatSettingsDialog.js +72 -0
  60. package/dist/react/components/ChatSettingsSegmented.d.ts +18 -0
  61. package/dist/react/components/ChatSettingsSegmented.js +11 -0
  62. package/dist/react/components/Composer.d.ts +5 -1
  63. package/dist/react/components/Composer.js +126 -82
  64. package/dist/react/components/ConversationList.js +25 -39
  65. package/dist/react/components/ConversationTitleEditor.d.ts +27 -0
  66. package/dist/react/components/ConversationTitleEditor.js +54 -0
  67. package/dist/react/components/Icon.d.ts +1 -1
  68. package/dist/react/components/Icon.js +25 -45
  69. package/dist/react/components/Markdown.js +134 -123
  70. package/dist/react/components/McpUiSurfaceCard.d.ts +2 -2
  71. package/dist/react/components/McpUiSurfaceCard.js +30 -9
  72. package/dist/react/components/MessageList.d.ts +13 -13
  73. package/dist/react/components/MessageList.hooks.d.ts +6 -1
  74. package/dist/react/components/MessageList.hooks.js +19 -3
  75. package/dist/react/components/MessageList.js +34 -10
  76. package/dist/react/components/MessageRow.js +47 -31
  77. package/dist/react/components/MessageRow.usage.js +16 -11
  78. package/dist/react/components/MidRunUserMessage.js +16 -3
  79. package/dist/react/components/QuestionForm.js +148 -76
  80. package/dist/react/components/QuestionsPanel.js +1 -1
  81. package/dist/react/components/TodoCard.js +12 -6
  82. package/dist/react/components/ToolCard.js +47 -40
  83. package/dist/react/components/composer-discovery.js +6 -1
  84. package/dist/react/components/mcp-ui-surface-state.d.ts +6 -11
  85. package/dist/react/components/mcp-ui-surface-state.js +23 -8
  86. package/dist/react/ext-event-renderer-registry.d.ts +8 -0
  87. package/dist/react/ext-event-renderer-registry.js +5 -2
  88. package/dist/react/features/chat-pane/components/AgentPluginUsage.d.ts +7 -0
  89. package/dist/react/features/chat-pane/components/AgentPluginUsage.js +45 -0
  90. package/dist/react/features/chat-pane/components/AgentRuntimePicker.js +55 -19
  91. package/dist/react/features/chat-pane/components/ChatPane.d.ts +1 -1
  92. package/dist/react/features/chat-pane/components/ChatPane.js +192 -74
  93. package/dist/react/features/chat-pane/components/RuntimeDropUp.d.ts +29 -0
  94. package/dist/react/features/chat-pane/components/RuntimeDropUp.js +45 -0
  95. package/dist/react/features/chat-pane/create-daemon-attachment-uploader.js +80 -47
  96. package/dist/react/features/chat-pane/create-mcp-ui-tool-caller.js +44 -40
  97. package/dist/react/features/chat-pane/create-mid-run-message-poster.d.ts +6 -6
  98. package/dist/react/features/chat-pane/create-mid-run-message-poster.js +8 -8
  99. package/dist/react/features/chat-pane/create-typed-answer-poster.d.ts +3 -3
  100. package/dist/react/features/chat-pane/create-typed-answer-poster.js +3 -3
  101. package/dist/react/features/chat-pane/hooks/useAgentRuntimePicker.hooks.d.ts +6 -1
  102. package/dist/react/features/chat-pane/hooks/useAgentRuntimePicker.hooks.js +11 -5
  103. package/dist/react/features/chat-pane/hooks/useChatPane.hooks.d.ts +12 -7
  104. package/dist/react/features/chat-pane/hooks/useChatPane.hooks.js +340 -149
  105. package/dist/react/features/chat-pane/hooks/useChatPaneControlsHeight.hooks.js +7 -6
  106. package/dist/react/features/chat-pane/hooks/useChatPaneNewTabLinks.hooks.js +28 -10
  107. package/dist/react/features/chat-pane/hooks/useChatPaneRuntimeInventory.hooks.d.ts +3 -3
  108. package/dist/react/features/chat-pane/hooks/useChatPaneRuntimeInventory.hooks.js +60 -12
  109. package/dist/react/features/chat-pane/hooks/useRuntimeDropUp.hooks.d.ts +49 -0
  110. package/dist/react/features/chat-pane/hooks/useRuntimeDropUp.hooks.js +143 -0
  111. package/dist/react/features/chat-pane/model-options.d.ts +14 -0
  112. package/dist/react/features/chat-pane/model-options.js +45 -25
  113. package/dist/react/features/chat-pane/rules.d.ts +8 -9
  114. package/dist/react/features/chat-pane/rules.js +84 -46
  115. package/dist/react/features/chat-pane/styles.d.ts +1 -1
  116. package/dist/react/features/chat-pane/styles.js +152 -36
  117. package/dist/react/features/chat-pane/types.d.ts +17 -8
  118. package/dist/react/hooks/assistant-chats.js +12 -5
  119. package/dist/react/hooks/composer-draft-cache.js +116 -118
  120. package/dist/react/hooks/composer-history-storage.js +5 -5
  121. package/dist/react/hooks/fab-position.js +7 -4
  122. package/dist/react/hooks/useAnsweredCards.d.ts +21 -4
  123. package/dist/react/hooks/useAnsweredCards.js +38 -11
  124. package/dist/react/hooks/useArtifactStream.js +28 -22
  125. package/dist/react/hooks/useComposer.d.ts +20 -0
  126. package/dist/react/hooks/useComposer.js +22 -9
  127. package/dist/react/hooks/useComposerHistory.hooks.d.ts +6 -1
  128. package/dist/react/hooks/useComposerHistory.hooks.js +39 -19
  129. package/dist/react/hooks/useComposerHistoryKeys.hooks.js +24 -14
  130. package/dist/react/hooks/useConversation.d.ts +7 -0
  131. package/dist/react/hooks/useConversation.js +23 -10
  132. package/dist/react/hooks/useExtEventGroups.js +39 -39
  133. package/dist/react/hooks/useQuestionForms.js +21 -26
  134. package/dist/react/hooks/useRunStream.js +10 -2
  135. package/dist/react/index.d.ts +7 -1
  136. package/dist/react/index.js +9 -0
  137. package/dist/react/message-blocks.d.ts +6 -4
  138. package/dist/react/message-blocks.js +94 -72
  139. package/dist/react/slots.d.ts +2 -2
  140. package/dist/react/tool-call-display.js +19 -17
  141. package/dist/server/durable-runs/recover.js +132 -80
  142. package/dist/server/run-finalizer/entry.js +25 -15
  143. package/dist/store/legacy/sqlite/conversations.d.ts +11 -11
  144. package/dist/store/legacy/sqlite/conversations.js +69 -98
  145. package/dist/store/legacy/sqlite/messages.d.ts +10 -2
  146. package/dist/store/legacy/sqlite/messages.js +113 -71
  147. package/dist/store/runtime/chat-history-store.js +9 -3
  148. package/dist/store/runtime/durable-run-store.d.ts +2 -2
  149. package/dist/store/runtime/durable-run-store.js +165 -117
  150. package/dist/store/runtime/history-tools.js +118 -82
  151. package/dist/store/runtime/run-ledger.d.ts +4 -1
  152. package/dist/store/runtime/run-ledger.js +56 -16
  153. package/dist/store/sql/cursor.js +38 -18
  154. package/dist/store/sql/store.d.ts +18 -0
  155. package/dist/store/sql/store.js +46 -15
  156. package/package.json +12 -10
@@ -19,13 +19,16 @@ export function replySegments({ events }, _optional = {}) {
19
19
  started = false;
20
20
  continue;
21
21
  }
22
- const target = event.kind === 'usage' && !started && segments.length > 1 ? segments.length - 2 : segments.length - 1;
23
- if (event.kind !== 'usage' && event.kind !== 'status' && event.kind !== 'raw')
24
- started = true;
25
- segments[target].push(event);
22
+ const lateResult = event.kind === 'usage' && !started && segments.length > 1;
23
+ started ||= producesReplyOutput({ event });
24
+ segments[segments.length - (lateResult ? 2 : 1)].push(event);
26
25
  }
27
26
  return segments;
28
27
  }
28
+ /** Usage, status and raw lines are bookkeeping; anything else means the reply has produced output. */
29
+ function producesReplyOutput({ event }, _optional = {}) {
30
+ return event.kind !== 'usage' && event.kind !== 'status' && event.kind !== 'raw';
31
+ }
29
32
  /** Resolve one charge against this agent/session's last snapshot. State is local to an
30
33
  * ordered transcript projection; revisiting a session keeps its baseline. A lower snapshot
31
34
  * starts a new counter epoch (some CLIs reset their counter on resume). O(1) map access. */
@@ -57,17 +60,21 @@ export function turnUsage({ events }, { sessions = new Map(), agentId = '' } = {
57
60
  if (event.kind !== 'usage')
58
61
  continue;
59
62
  total ??= { kind: 'usage' };
60
- for (const field of ['inputTokens', 'outputTokens', 'durationMs']) {
61
- const value = event[field];
62
- if (typeof value === 'number' && Number.isFinite(value) && value >= 0)
63
- total[field] = (total[field] ?? 0) + value;
64
- }
63
+ addReportedCounts({ total, event });
65
64
  const cost = usageCost({ event, agentId, sessions });
66
65
  if (cost !== undefined)
67
66
  total.costUsd = (total.costUsd ?? 0) + cost;
68
67
  }
69
68
  return total;
70
69
  }
70
+ /** Add an event's well-formed token and duration counts into `total`, in place. */
71
+ function addReportedCounts({ total, event }, _optional = {}) {
72
+ for (const field of ['inputTokens', 'outputTokens', 'durationMs']) {
73
+ const value = event[field];
74
+ if (typeof value === 'number' && Number.isFinite(value) && value >= 0)
75
+ total[field] = (total[field] ?? 0) + value;
76
+ }
77
+ }
71
78
  /** Add one reply's usage into a message total; fields absent from both stay absent. O(1). */
72
79
  function addUsage({ total, usage }, _optional = {}) {
73
80
  if (!usage)
@@ -92,30 +99,45 @@ export function conversationUsage({ messages }, _optional = {}) {
92
99
  for (const message of messages) {
93
100
  if (message.role !== 'assistant')
94
101
  continue;
95
- // Replies run in event order through one session map, so their sum equals the whole-message charge.
96
- const segments = [];
97
- let usage;
98
- for (const events of replySegments({ events: message.events })) {
99
- const segment = turnUsage({ events }, { sessions, agentId: message.agentId ?? '' });
100
- usage = addUsage({ total: usage, usage: segment });
101
- if (segment?.costUsd !== undefined)
102
- totalCostUsd = (totalCostUsd ?? 0) + segment.costUsd;
103
- segments.push(segment && { ...segment, ...(totalCostUsd === undefined ? {} : { totalCostUsd }) });
104
- }
105
- if (!usage)
106
- continue;
107
- // Tokens-only runtimes still have run timestamps; those cannot time individual mid-run replies.
108
- if (segments.length === 1 && usage.durationMs === undefined && message.startedAt !== undefined && message.endedAt !== undefined) {
109
- const durationMs = message.endedAt - message.startedAt;
110
- if (Number.isFinite(durationMs) && durationMs >= 0)
111
- usage.durationMs = durationMs;
112
- }
113
- summaries.set(message.id, {
114
- ...usage,
115
- ...(totalCostUsd === undefined ? {} : { totalCostUsd }),
116
- ...(segments.length > 1 ? { segments } : {}),
117
- });
102
+ const result = messageUsage({ message, sessions, totalCostUsd });
103
+ totalCostUsd = result.totalCostUsd;
104
+ if (result.usage)
105
+ summaries.set(message.id, messageSummary({ ...result, usage: result.usage, message }));
118
106
  }
119
107
  return summaries;
120
108
  }
109
+ /** A message's summary: its usage, the prefix total, and per-reply segments when it holds several replies. */
110
+ function messageSummary({ usage, segments, totalCostUsd, message }, _optional = {}) {
111
+ // Tokens-only runtimes still have run timestamps; those cannot time individual mid-run replies.
112
+ if (segments.length === 1)
113
+ fillRunDuration({ usage, message });
114
+ return {
115
+ ...usage,
116
+ ...(totalCostUsd === undefined ? {} : { totalCostUsd }),
117
+ ...(segments.length > 1 ? { segments } : {}),
118
+ };
119
+ }
120
+ /** One assistant message's summed usage and per-reply summaries, advancing the conversation's prefix total. */
121
+ function messageUsage({ message, sessions, totalCostUsd }, _optional = {}) {
122
+ // Replies run in event order through one session map, so their sum equals the whole-message charge.
123
+ const segments = [];
124
+ let usage;
125
+ let prefixTotal = totalCostUsd;
126
+ for (const events of replySegments({ events: message.events })) {
127
+ const segment = turnUsage({ events }, { sessions, agentId: message.agentId ?? '' });
128
+ usage = addUsage({ total: usage, usage: segment });
129
+ if (segment?.costUsd !== undefined)
130
+ prefixTotal = (prefixTotal ?? 0) + segment.costUsd;
131
+ segments.push(segment && { ...segment, ...(prefixTotal === undefined ? {} : { totalCostUsd: prefixTotal }) });
132
+ }
133
+ return { usage, segments, totalCostUsd: prefixTotal };
134
+ }
135
+ /** Use the message's elapsed run time when no reply reported a duration; invalid timestamps stay unknown. */
136
+ function fillRunDuration({ usage, message }, _optional = {}) {
137
+ if (usage.durationMs !== undefined || message.startedAt === undefined || message.endedAt === undefined)
138
+ return;
139
+ const durationMs = message.endedAt - message.startedAt;
140
+ if (Number.isFinite(durationMs) && durationMs >= 0)
141
+ usage.durationMs = durationMs;
142
+ }
121
143
  //# sourceMappingURL=turn-usage.js.map
@@ -7,6 +7,9 @@ const INTERNAL_REMOVED = '[REDACTED:chat_secret]';
7
7
  const REDACTION_MARKER = /\[REDACTED:[^\]\r\n]+\]/g;
8
8
  const CHAT_RULES = [
9
9
  { kind: 'chat_key', pattern: /(?<![A-Za-z0-9_-])sk-[A-Za-z0-9_-]{16,}/g, replacement: () => INTERNAL_REMOVED },
10
+ // Kept on top of the shared Slack rule: that rule refuses a token preceded by `-` (e.g. a URL path
11
+ // segment `/-xoxb-…`) and the entropy fallback below exempts URLs, so without this rule such a token
12
+ // would reach storage and model history. Only drop it once the shared rule covers that case.
10
13
  { kind: 'chat_key', pattern: /(?<![A-Za-z0-9_])xox[baprs]-[A-Za-z0-9-]{16,}/g, replacement: () => INTERNAL_REMOVED },
11
14
  // Explicit authorization syntax catches even short values. Exempt scheme descriptions in prose.
12
15
  { kind: 'chat_bearer', pattern: /\bBearer[ \t]+(?!\[REDACTED)(?!(?:token|tokens|authentication|authorization|auth|scheme|credentials?)\b)[A-Za-z0-9._~+\/=-]+/gi, replacement: () => 'Bearer ' + INTERNAL_REMOVED },
@@ -27,33 +27,27 @@ function normalizeExt(name) {
27
27
  const i = name.lastIndexOf('.');
28
28
  return i >= 0 ? name.slice(i).toLowerCase() : '';
29
29
  }
30
- function inferKindFromEntry(entry) {
31
- const ext = normalizeExt(entry);
32
- if (['.html', '.htm'].includes(ext))
33
- return 'html';
34
- if (ext === '.svg')
35
- return 'svg';
36
- if (ext === '.md')
37
- return 'markdown-document';
38
- if (['.jsx', '.tsx'].includes(ext))
39
- return 'react-component';
40
- if (['.js', '.ts', '.json', '.css'].includes(ext))
41
- return 'code-snippet';
42
- return null;
43
- }
44
- function exportsForKind(kind) {
45
- if (kind === 'deck')
46
- return ['html', 'pdf', 'zip'];
47
- if (kind === 'react-component')
48
- return ['jsx', 'html', 'zip'];
49
- if (kind === 'markdown-document')
50
- return ['md', 'html', 'pdf', 'zip'];
51
- if (kind === 'svg' || kind === 'diagram')
52
- return ['svg', 'zip'];
53
- if (kind === 'code-snippet')
54
- return ['txt', 'zip'];
55
- return ['html', 'pdf', 'zip'];
56
- }
30
+ const LEGACY_KIND_BY_EXT = new Map([
31
+ ['.html', 'html'],
32
+ ['.htm', 'html'],
33
+ ['.svg', 'svg'],
34
+ ['.md', 'markdown-document'],
35
+ ['.jsx', 'react-component'],
36
+ ['.tsx', 'react-component'],
37
+ ['.js', 'code-snippet'],
38
+ ['.ts', 'code-snippet'],
39
+ ['.json', 'code-snippet'],
40
+ ['.css', 'code-snippet'],
41
+ ]);
42
+ /** Renderer and export set for every kind `inferLegacyManifest` can resolve to (the inferred kinds plus the deck-name heuristic's `deck`). */
43
+ const LEGACY_KIND_INFO = {
44
+ html: { renderer: 'html', exports: ['html', 'pdf', 'zip'] },
45
+ deck: { renderer: 'deck-html', exports: ['html', 'pdf', 'zip'] },
46
+ 'markdown-document': { renderer: 'markdown', exports: ['md', 'html', 'pdf', 'zip'] },
47
+ 'react-component': { renderer: 'react-component', exports: ['jsx', 'html', 'zip'] },
48
+ 'code-snippet': { renderer: 'code', exports: ['txt', 'zip'] },
49
+ svg: { renderer: 'svg', exports: ['svg', 'zip'] }, // 'svg' is a valid ArtifactRendererId as-is
50
+ };
57
51
  /** The manifest sidecar file name convention for a given artifact entry file name. */
58
52
  export function artifactManifestNameFor({ entry }) {
59
53
  return `${entry}.artifact.json`;
@@ -90,50 +84,62 @@ export function serializeArtifactManifest({ manifest }) {
90
84
  * @complexity O(k) in the manifest's own field/array count — no I/O.
91
85
  */
92
86
  export function parseArtifactManifest({ raw }) {
87
+ let parsed;
93
88
  try {
94
- const parsed = JSON.parse(raw);
95
- if (parsed?.version !== MANIFEST_VERSION)
96
- return null;
97
- if (typeof parsed.entry !== 'string' || !parsed.entry)
98
- return null;
99
- if (typeof parsed.title !== 'string' || !parsed.title)
100
- return null;
101
- if (!Array.isArray(parsed.exports))
102
- return null;
103
- if (typeof parsed.kind !== 'string' || typeof parsed.renderer !== 'string') {
104
- return null;
105
- }
106
- if (!ALLOWED_KINDS.has(parsed.kind))
107
- return null;
108
- if (!ALLOWED_RENDERERS.has(parsed.renderer))
109
- return null;
110
- if (parsed.status !== undefined && !ALLOWED_STATUS.has(parsed.status)) {
111
- return null;
112
- }
113
- if (parsed.exports.length === 0)
114
- return null;
115
- if (parsed.exports.some((value) => !ALLOWED_EXPORTS.has(value)))
116
- return null;
117
- return {
118
- version: MANIFEST_VERSION,
119
- kind: parsed.kind,
120
- title: parsed.title,
121
- entry: parsed.entry,
122
- renderer: parsed.renderer,
123
- status: ALLOWED_STATUS.has(parsed.status) ? parsed.status : 'complete',
124
- exports: parsed.exports,
125
- primary: parsed.primary === true || typeof parsed.primary === 'string' ? parsed.primary : undefined,
126
- supportingFiles: Array.isArray(parsed.supportingFiles) ? parsed.supportingFiles.filter((x) => typeof x === 'string') : undefined,
127
- createdAt: typeof parsed.createdAt === 'string' ? parsed.createdAt : undefined,
128
- updatedAt: typeof parsed.updatedAt === 'string' ? parsed.updatedAt : undefined,
129
- sourceSkillId: typeof parsed.sourceSkillId === 'string' ? parsed.sourceSkillId : undefined,
130
- designSystemId: typeof parsed.designSystemId === 'string' || parsed.designSystemId === null ? parsed.designSystemId : undefined,
131
- metadata: parsed.metadata && typeof parsed.metadata === 'object' && !Array.isArray(parsed.metadata) ? parsed.metadata : undefined,
132
- };
89
+ parsed = JSON.parse(raw);
133
90
  }
134
91
  catch {
135
92
  return null;
136
93
  }
94
+ if (!hasRequiredManifestFields(parsed) || !hasKnownManifestValues(parsed))
95
+ return null;
96
+ return {
97
+ version: MANIFEST_VERSION,
98
+ kind: parsed.kind,
99
+ title: parsed.title,
100
+ entry: parsed.entry,
101
+ renderer: parsed.renderer,
102
+ status: parsed.status ?? 'complete',
103
+ exports: parsed.exports,
104
+ ...optionalManifestFields(parsed),
105
+ };
106
+ }
107
+ /** The optional manifest fields, each kept only when it has its documented type (anything else is dropped, not rejected). */
108
+ function optionalManifestFields(parsed) {
109
+ return {
110
+ primary: parsed.primary === true || typeof parsed.primary === 'string' ? parsed.primary : undefined,
111
+ supportingFiles: Array.isArray(parsed.supportingFiles) ? parsed.supportingFiles.filter(isString) : undefined,
112
+ createdAt: stringOrUndefined(parsed.createdAt),
113
+ updatedAt: stringOrUndefined(parsed.updatedAt),
114
+ sourceSkillId: stringOrUndefined(parsed.sourceSkillId),
115
+ designSystemId: parsed.designSystemId === null ? null : stringOrUndefined(parsed.designSystemId),
116
+ metadata: parsed.metadata && typeof parsed.metadata === 'object' && !Array.isArray(parsed.metadata) ? parsed.metadata : undefined,
117
+ };
118
+ }
119
+ function hasRequiredManifestFields(parsed) {
120
+ if (parsed?.version !== MANIFEST_VERSION)
121
+ return false;
122
+ if (!isNonEmptyString(parsed.entry) || !isNonEmptyString(parsed.title))
123
+ return false;
124
+ return Array.isArray(parsed.exports) && isString(parsed.kind) && isString(parsed.renderer);
125
+ }
126
+ function hasKnownManifestValues(parsed) {
127
+ if (!ALLOWED_KINDS.has(parsed.kind))
128
+ return false;
129
+ if (!ALLOWED_RENDERERS.has(parsed.renderer))
130
+ return false;
131
+ if (parsed.status !== undefined && !ALLOWED_STATUS.has(parsed.status))
132
+ return false;
133
+ return parsed.exports.length > 0 && parsed.exports.every((value) => ALLOWED_EXPORTS.has(value));
134
+ }
135
+ function isString(value) {
136
+ return typeof value === 'string';
137
+ }
138
+ function isNonEmptyString(value) {
139
+ return isString(value) && value !== '';
140
+ }
141
+ function stringOrUndefined(value) {
142
+ return isString(value) ? value : undefined;
137
143
  }
138
144
  /**
139
145
  * Infer a manifest for a legacy artifact file that predates manifest
@@ -141,31 +147,20 @@ export function parseArtifactManifest({ raw }) {
141
147
  * @returns `null` when the extension isn't a recognized artifact kind.
142
148
  */
143
149
  export function inferLegacyManifest(input) {
144
- const kind = inferKindFromEntry(input.entry);
150
+ const kind = LEGACY_KIND_BY_EXT.get(normalizeExt(input.entry));
145
151
  if (!kind)
146
152
  return null;
147
- const lowerEntry = input.entry.toLowerCase();
148
- const isDeck = kind === 'html' && (lowerEntry.includes('deck') || lowerEntry.includes('slides') || lowerEntry.includes('pitch'));
149
- const renderer = isDeck
150
- ? 'deck-html'
151
- : kind === 'html'
152
- ? 'html'
153
- : kind === 'markdown-document'
154
- ? 'markdown'
155
- : kind === 'react-component'
156
- ? 'react-component'
157
- : kind === 'code-snippet'
158
- ? 'code'
159
- : kind; // only 'svg' remains — a valid ArtifactRendererId as-is
153
+ const isDeck = kind === 'html' && /deck|slides|pitch/.test(input.entry.toLowerCase());
160
154
  const resolvedKind = isDeck ? 'deck' : kind;
155
+ const info = LEGACY_KIND_INFO[resolvedKind];
161
156
  return {
162
157
  version: MANIFEST_VERSION,
163
158
  kind: resolvedKind,
164
159
  title: input.title || input.entry,
165
160
  entry: input.entry,
166
- renderer,
161
+ renderer: info.renderer,
167
162
  status: 'complete',
168
- exports: exportsForKind(resolvedKind),
163
+ exports: [...info.exports],
169
164
  primary: resolvedKind === 'html' || resolvedKind === 'deck' ? true : undefined,
170
165
  metadata: input.metadata,
171
166
  };
@@ -53,66 +53,64 @@ export function isRealArtifactOpenAt({ content, idx }) {
53
53
  * backtick scan per accumulated paragraph block.
54
54
  */
55
55
  export function computeSkipRanges({ buffer }) {
56
- const ranges = [];
57
- // Paragraph-block regions are contiguous spans of paragraph lines outside
58
- // any fenced code block; inline-code scanning is restricted to one block at
59
- // a time so backticks never pair across a block boundary.
60
- const blockRegions = [];
56
+ const scan = { ranges: [], blockRegions: [], fenceStart: -1, blockStart: -1 };
57
+ for (const line of linesOf(buffer)) {
58
+ if (scan.fenceStart === -1)
59
+ visitLineOutsideFence(scan, line);
60
+ else
61
+ visitLineInsideFence(scan, line);
62
+ }
63
+ const unclosedFenceStart = scan.fenceStart === -1 ? null : scan.fenceStart;
64
+ if (unclosedFenceStart === null)
65
+ closeBlockBefore(scan, buffer.length); // flush an open paragraph region at end-of-buffer
66
+ for (const [s, e] of scan.blockRegions) {
67
+ for (const m of buffer.slice(s, e).matchAll(INLINE_CODE_RE)) {
68
+ scan.ranges.push([s + m.index, s + m.index + m[0].length]);
69
+ }
70
+ }
71
+ return { ranges: scan.ranges, unclosedFenceStart };
72
+ }
73
+ function* linesOf(buffer) {
61
74
  let pos = 0;
62
- let inFence = false;
63
- let fenceStart = -1;
64
- let blockStart = -1;
65
- const closeBlockBefore = (idx) => {
66
- if (blockStart !== -1 && idx > blockStart)
67
- blockRegions.push([blockStart, idx]);
68
- blockStart = -1;
69
- };
70
75
  while (pos < buffer.length) {
71
76
  const eol = buffer.indexOf('\n', pos);
72
- const lineEnd = eol === -1 ? buffer.length : eol;
73
- const line = buffer.slice(pos, lineEnd);
74
- const lineHasNewline = eol !== -1;
75
- if (!inFence) {
76
- if (lineHasNewline && FENCE_OPEN_RE.test(line)) {
77
- closeBlockBefore(pos);
78
- inFence = true;
79
- fenceStart = pos;
80
- }
81
- else if (line.trim() === '') {
82
- closeBlockBefore(pos);
83
- }
84
- else if (HEADING_RE.test(line) || UL_ITEM_RE.test(line) || OL_ITEM_RE.test(line)) {
85
- // Each heading/list-item line is its own inline-scan region rather
86
- // than joining adjacent paragraphs.
87
- closeBlockBefore(pos);
88
- blockRegions.push([pos, lineEnd]);
89
- }
90
- else {
91
- if (blockStart === -1)
92
- blockStart = pos;
93
- }
94
- }
95
- else if (lineHasNewline && FENCE_CLOSE_RE.test(line)) {
96
- inFence = false;
97
- ranges.push([fenceStart, eol + 1]);
98
- fenceStart = -1;
77
+ if (eol === -1) {
78
+ // trailing partial line — classified like any other, but never as a fence delimiter
79
+ yield { start: pos, end: buffer.length, text: buffer.slice(pos), hasNewline: false };
80
+ return;
99
81
  }
100
- if (!lineHasNewline)
101
- break; // trailing partial line — already accounted for above
82
+ yield { start: pos, end: eol, text: buffer.slice(pos, eol), hasNewline: true };
102
83
  pos = eol + 1;
103
84
  }
104
- if (!inFence)
105
- closeBlockBefore(buffer.length); // flush an open paragraph region at end-of-buffer
106
- for (const [s, e] of blockRegions) {
107
- INLINE_CODE_RE.lastIndex = 0;
108
- const segment = buffer.slice(s, e);
109
- let m = INLINE_CODE_RE.exec(segment);
110
- while (m !== null) {
111
- ranges.push([s + m.index, s + m.index + m[0].length]);
112
- m = INLINE_CODE_RE.exec(segment);
113
- }
85
+ }
86
+ function closeBlockBefore(scan, idx) {
87
+ if (scan.blockStart !== -1 && idx > scan.blockStart)
88
+ scan.blockRegions.push([scan.blockStart, idx]);
89
+ scan.blockStart = -1;
90
+ }
91
+ function visitLineInsideFence(scan, line) {
92
+ if (!line.hasNewline || !FENCE_CLOSE_RE.test(line.text))
93
+ return;
94
+ scan.ranges.push([scan.fenceStart, line.end + 1]);
95
+ scan.fenceStart = -1;
96
+ }
97
+ function visitLineOutsideFence(scan, line) {
98
+ if (line.hasNewline && FENCE_OPEN_RE.test(line.text)) {
99
+ closeBlockBefore(scan, line.start);
100
+ scan.fenceStart = line.start;
101
+ }
102
+ else if (line.text.trim() === '') {
103
+ closeBlockBefore(scan, line.start);
104
+ }
105
+ else if (HEADING_RE.test(line.text) || UL_ITEM_RE.test(line.text) || OL_ITEM_RE.test(line.text)) {
106
+ // Each heading/list-item line is its own inline-scan region rather
107
+ // than joining adjacent paragraphs.
108
+ closeBlockBefore(scan, line.start);
109
+ scan.blockRegions.push([line.start, line.end]);
110
+ }
111
+ else if (scan.blockStart === -1) {
112
+ scan.blockStart = line.start;
114
113
  }
115
- return { ranges, unclosedFenceStart: inFence ? fenceStart : null };
116
114
  }
117
115
  /** `true` when position `p` falls inside one of `ranges` (half-open intervals). */
118
116
  export function rangeContains({ ranges, p }) {