@jini-ai/chat 0.5.7 → 0.5.8

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 (105) hide show
  1. package/dist/core/ag-ui/entry.js +2 -0
  2. package/dist/core/assistant-client/durable-run-subscription.d.ts +4 -1
  3. package/dist/core/assistant-client/durable-run-subscription.js +13 -4
  4. package/dist/core/events.d.ts +3 -0
  5. package/dist/core/history-tool-metadata.d.ts +23 -0
  6. package/dist/core/history-tool-metadata.js +13 -0
  7. package/dist/core/index.d.ts +3 -0
  8. package/dist/core/index.js +3 -0
  9. package/dist/core/persistence/title.d.ts +24 -0
  10. package/dist/core/persistence/title.js +60 -9
  11. package/dist/core/run-activity.d.ts +15 -2
  12. package/dist/core/run-activity.js +6 -4
  13. package/dist/core/run-events/entry.js +2 -0
  14. package/dist/core/tool-call-detail.d.ts +15 -0
  15. package/dist/core/tool-call-detail.js +18 -0
  16. package/dist/core/turn-usage.d.ts +42 -3
  17. package/dist/core/turn-usage.js +101 -4
  18. package/dist/core/usage-format.d.ts +10 -0
  19. package/dist/core/usage-format.js +15 -0
  20. package/dist/embed-widget/browser/lazy-loader.d.ts +34 -0
  21. package/dist/embed-widget/browser/lazy-loader.js +116 -0
  22. package/dist/embed-widget/react/dialog.hooks.d.ts +28 -0
  23. package/dist/embed-widget/react/dialog.hooks.js +54 -0
  24. package/dist/embed-widget/react/embed.d.ts +1 -0
  25. package/dist/embed-widget/react/embed.js +5 -3
  26. package/dist/embed-widget/react/header.d.ts +3 -1
  27. package/dist/embed-widget/react/header.hooks.d.ts +13 -0
  28. package/dist/embed-widget/react/header.hooks.js +32 -0
  29. package/dist/embed-widget/react/header.js +4 -14
  30. package/dist/embed-widget/vanilla/index.d.ts +63 -0
  31. package/dist/embed-widget/vanilla/index.js +319 -0
  32. package/dist/embed-widget/vanilla/state.d.ts +48 -0
  33. package/dist/embed-widget/vanilla/state.js +120 -0
  34. package/dist/embed-widget/vanilla/style.css +29 -0
  35. package/dist/react/components/A2uiSurfaceCard.d.ts +5 -1
  36. package/dist/react/components/A2uiSurfaceCard.js +9 -9
  37. package/dist/react/components/AttachmentPreviewModal.hooks.d.ts +7 -0
  38. package/dist/react/components/AttachmentPreviewModal.hooks.js +1 -1
  39. package/dist/react/components/ChatFab.d.ts +4 -1
  40. package/dist/react/components/ChatFab.js +2 -2
  41. package/dist/react/components/ConversationList.d.ts +8 -1
  42. package/dist/react/components/ConversationList.hooks.d.ts +18 -0
  43. package/dist/react/components/ConversationList.hooks.js +25 -0
  44. package/dist/react/components/ConversationList.js +12 -5
  45. package/dist/react/components/ConversationList.rules.d.ts +25 -0
  46. package/dist/react/components/ConversationList.rules.js +19 -0
  47. package/dist/react/components/McpUiSurfaceCard.d.ts +1 -1
  48. package/dist/react/components/McpUiSurfaceCard.js +8 -4
  49. package/dist/react/components/MessageList.d.ts +1 -1
  50. package/dist/react/components/MessageList.hooks.d.ts +4 -0
  51. package/dist/react/components/MessageList.hooks.js +6 -1
  52. package/dist/react/components/MessageList.js +4 -3
  53. package/dist/react/components/MessageRow.d.ts +17 -0
  54. package/dist/react/components/MessageRow.js +54 -35
  55. package/dist/react/components/MessageRow.usage.d.ts +48 -0
  56. package/dist/react/components/MessageRow.usage.js +47 -0
  57. package/dist/react/components/MidRunUserMessage.d.ts +2 -12
  58. package/dist/react/components/MidRunUserMessage.js +4 -2
  59. package/dist/react/components/SentAttachment.d.ts +19 -0
  60. package/dist/react/components/SentAttachment.hooks.d.ts +44 -0
  61. package/dist/react/components/SentAttachment.hooks.js +56 -0
  62. package/dist/react/components/SentAttachment.js +14 -0
  63. package/dist/react/components/ToolCallGroup.d.ts +12 -0
  64. package/dist/react/components/ToolCallGroup.hooks.d.ts +20 -0
  65. package/dist/react/components/ToolCallGroup.hooks.js +23 -0
  66. package/dist/react/components/ToolCallGroup.js +13 -0
  67. package/dist/react/components/ToolCard.js +25 -17
  68. package/dist/react/components/mcp-ui-surface-state.d.ts +21 -0
  69. package/dist/react/components/mcp-ui-surface-state.js +26 -0
  70. package/dist/react/ext-event-renderer-registry.d.ts +7 -0
  71. package/dist/react/features/chat-pane/components/ChatPane.d.ts +1 -1
  72. package/dist/react/features/chat-pane/components/ChatPane.js +11 -6
  73. package/dist/react/features/chat-pane/hooks/useChatPane.hooks.d.ts +1 -0
  74. package/dist/react/features/chat-pane/hooks/useChatPane.hooks.js +16 -10
  75. package/dist/react/features/chat-pane/hooks/useChatPaneRuntimeInventory.hooks.js +4 -0
  76. package/dist/react/features/chat-pane/hooks/useChatPaneSuggestion.hooks.d.ts +9 -0
  77. package/dist/react/features/chat-pane/hooks/useChatPaneSuggestion.hooks.js +11 -0
  78. package/dist/react/features/chat-pane/hooks/useChatPaneSurface.hooks.d.ts +17 -0
  79. package/dist/react/features/chat-pane/hooks/useChatPaneSurface.hooks.js +15 -0
  80. package/dist/react/features/chat-pane/rules.d.ts +14 -0
  81. package/dist/react/features/chat-pane/rules.js +18 -0
  82. package/dist/react/features/chat-pane/styles.d.ts +1 -1
  83. package/dist/react/features/chat-pane/styles.js +85 -1
  84. package/dist/react/features/chat-pane/types.d.ts +16 -1
  85. package/dist/react/hooks/useAnsweredCards.d.ts +20 -0
  86. package/dist/react/hooks/useAnsweredCards.js +23 -0
  87. package/dist/react/hooks/useConversation.d.ts +8 -0
  88. package/dist/react/hooks/useConversation.js +34 -0
  89. package/dist/react/hooks/useRunActivity.d.ts +5 -3
  90. package/dist/react/hooks/useRunActivity.js +4 -3
  91. package/dist/react/index.d.ts +4 -0
  92. package/dist/react/index.js +3 -0
  93. package/dist/react/message-blocks.d.ts +22 -3
  94. package/dist/react/message-blocks.js +25 -3
  95. package/dist/react/tool-call-display.d.ts +90 -0
  96. package/dist/react/tool-call-display.js +97 -0
  97. package/dist/store/ports.d.ts +25 -2
  98. package/dist/store/runtime/entry.d.ts +1 -0
  99. package/dist/store/runtime/entry.js +1 -0
  100. package/dist/store/runtime/history-tools.d.ts +27 -0
  101. package/dist/store/runtime/history-tools.js +169 -0
  102. package/dist/store/sql/store.js +52 -0
  103. package/dist/testing/index.d.ts +7 -0
  104. package/dist/testing/index.js +22 -0
  105. package/package.json +48 -18
@@ -15,6 +15,8 @@ function reduceUsagePayload(payload) {
15
15
  ...(typeof usage.input_tokens === "number" ? { inputTokens: usage.input_tokens } : {}),
16
16
  ...(typeof usage.output_tokens === "number" ? { outputTokens: usage.output_tokens } : {}),
17
17
  ...(typeof payload.costUsd === "number" ? { costUsd: payload.costUsd } : {}),
18
+ ...(typeof payload.cumulativeCostUsd === "number" ? { cumulativeCostUsd: payload.cumulativeCostUsd } : {}),
19
+ ...(typeof payload.costSessionId === "string" ? { costSessionId: payload.costSessionId } : {}),
18
20
  ...(typeof payload.durationMs === "number" ? { durationMs: payload.durationMs } : {}),
19
21
  };
20
22
  }
@@ -6,10 +6,13 @@ export interface DurableRunBinding {
6
6
  readonly conversationId: string;
7
7
  }
8
8
  export interface DurableSubscriptionPorts {
9
+ /** Absent recover intent preserves legacy recovery; false requests only the saved checkpoint. */
9
10
  readonly read: (required: {
10
11
  runId: string;
11
12
  messageId?: string;
12
- }, optional: {}) => Promise<ChatMessage | null | "gone">;
13
+ }, optional: {
14
+ recover?: boolean;
15
+ }) => Promise<ChatMessage | null | "gone">;
13
16
  readonly stream: (required: {
14
17
  runId: string;
15
18
  cursor: string;
@@ -6,6 +6,7 @@ export function followDurableRun({ runId: initialRunId, handlers, signal, bindin
6
6
  let cursor = "";
7
7
  let stopped = false;
8
8
  let reading = false;
9
+ let recoveryPending = true;
9
10
  let dropped = false;
10
11
  let stopStream;
11
12
  let stopTimer;
@@ -26,7 +27,7 @@ export function followDurableRun({ runId: initialRunId, handlers, signal, bindin
26
27
  function open() {
27
28
  dropped = false;
28
29
  stopStream?.();
29
- stopStream = ports.stream({ runId, cursor, frame, dropped: () => { dropped = true; void read().catch(() => undefined); } }, {});
30
+ stopStream = ports.stream({ runId, cursor, frame, dropped: () => { dropped = true; recoveryPending = true; void read().catch(() => undefined); } }, {});
30
31
  }
31
32
  function frame(kind, data, nextCursor) {
32
33
  if (stopped)
@@ -36,6 +37,7 @@ export function followDurableRun({ runId: initialRunId, handlers, signal, bindin
36
37
  const outcome = translateRunFrame({ kind, raw: data, notices }, {});
37
38
  // An attempt's error/end may be followed by a continuation. Only the row can finalize it.
38
39
  if (outcome.terminal || outcome.error) {
40
+ recoveryPending = true;
39
41
  void read().catch(() => undefined);
40
42
  return;
41
43
  }
@@ -52,8 +54,11 @@ export function followDurableRun({ runId: initialRunId, handlers, signal, bindin
52
54
  reading = true;
53
55
  stopTimer?.();
54
56
  stopTimer = undefined;
57
+ const recover = recoveryPending;
58
+ // Consume only when a read starts: faults during its await latch one follow-up recovery.
59
+ recoveryPending = false;
55
60
  try {
56
- const message = await ports.read({ runId, ...(messageId ? { messageId } : {}) }, {});
61
+ const message = await ports.read({ runId, ...(messageId ? { messageId } : {}) }, { recover });
57
62
  if (stopped || !message)
58
63
  return;
59
64
  if (message === "gone") {
@@ -64,8 +69,12 @@ export function followDurableRun({ runId: initialRunId, handlers, signal, bindin
64
69
  }
65
70
  finally {
66
71
  reading = false;
67
- if (!stopped)
68
- stopTimer = ports.schedule({ work: () => void read().catch(() => undefined), delayMs: 1_000 }, {});
72
+ if (!stopped) {
73
+ if (recoveryPending)
74
+ void read().catch(() => undefined);
75
+ else
76
+ stopTimer = ports.schedule({ work: () => void read().catch(() => undefined), delayMs: 1_000 }, {});
77
+ }
69
78
  }
70
79
  }
71
80
  function applySavedMessage(message) {
@@ -65,6 +65,9 @@ export type AgentEvent = {
65
65
  inputTokens?: number;
66
66
  outputTokens?: number;
67
67
  costUsd?: number;
68
+ /** CLI session snapshot, persisted in events_json; reconciled across turns before display. */
69
+ cumulativeCostUsd?: number;
70
+ costSessionId?: string;
68
71
  durationMs?: number;
69
72
  stopReason?: string;
70
73
  } | {
@@ -0,0 +1,23 @@
1
+ /** Discovery and read approval for the generic history tools, shared with host projections. */
2
+ export declare const assistantHistoryToolMetadata: {
3
+ readonly assistant_chats_list: {
4
+ readonly search: {
5
+ readonly keywords: "previous earlier interrupted chat conversation history search resume continue what we were doing";
6
+ readonly queries: readonly ["I had to start a new chat. Can you find what we were doing?", "Find my previous conversation."];
7
+ };
8
+ readonly approval: {
9
+ readonly class: "read";
10
+ readonly confirmation: "direct";
11
+ };
12
+ };
13
+ readonly assistant_chat_read: {
14
+ readonly search: {
15
+ readonly keywords: "read previous chat transcript history tail messages resume interrupted continue conversation";
16
+ };
17
+ readonly approval: {
18
+ readonly class: "read";
19
+ readonly confirmation: "direct";
20
+ };
21
+ };
22
+ };
23
+ //# sourceMappingURL=history-tool-metadata.d.ts.map
@@ -0,0 +1,13 @@
1
+ /** Discovery and read approval for the generic history tools, shared with host projections. */
2
+ export const assistantHistoryToolMetadata = {
3
+ assistant_chats_list: {
4
+ search: { keywords: 'previous earlier interrupted chat conversation history search resume continue what we were doing',
5
+ queries: ['I had to start a new chat. Can you find what we were doing?', 'Find my previous conversation.'] },
6
+ approval: { class: 'read', confirmation: 'direct' },
7
+ },
8
+ assistant_chat_read: {
9
+ search: { keywords: 'read previous chat transcript history tail messages resume interrupted continue conversation' },
10
+ approval: { class: 'read', confirmation: 'direct' },
11
+ },
12
+ };
13
+ //# sourceMappingURL=history-tool-metadata.js.map
@@ -33,4 +33,7 @@ export { recoveredRunEvents } from './durable-projection.js';
33
33
  export { composerHistoryKeyAction, createComposerHistoryState, mergeComposerHistory, normalizeComposerHistory, transitionComposerHistory, } from './composer-history.js';
34
34
  export type { ComposerHistoryKeyInput, ComposerHistoryState, ComposerHistoryStoragePort } from './composer-history.js';
35
35
  export * from './user-text-redaction.js';
36
+ export * from './history-tool-metadata.js';
37
+ export { turnUsage, conversationUsage, replySegments, isReplyBoundary, MID_RUN_USER_MESSAGE_EVENT_NAME, type ChatUsageSummary } from './turn-usage.js';
38
+ export { formatUsageCost } from './usage-format.js';
36
39
  //# sourceMappingURL=index.d.ts.map
@@ -32,4 +32,7 @@ export * from './persistence/index.js';
32
32
  export { recoveredRunEvents } from './durable-projection.js';
33
33
  export { composerHistoryKeyAction, createComposerHistoryState, mergeComposerHistory, normalizeComposerHistory, transitionComposerHistory, } from './composer-history.js';
34
34
  export * from './user-text-redaction.js';
35
+ export * from './history-tool-metadata.js';
36
+ export { turnUsage, conversationUsage, replySegments, isReplyBoundary, MID_RUN_USER_MESSAGE_EVENT_NAME } from './turn-usage.js';
37
+ export { formatUsageCost } from './usage-format.js';
35
38
  //# sourceMappingURL=index.js.map
@@ -23,6 +23,18 @@
23
23
  * 2. `LATIN_NEVER_TRAILING` trims function words off the end. The original's word cap lands
24
24
  * mid-clause on question-shaped prompts and strands them ("...Posts Do I").
25
25
  */
26
+ /** Remove presentation markup from title text while preserving link labels and inline code.
27
+ * Shared by prompt derivation, stored-title display and product naming adapters; no DOM or I/O.
28
+ * Fenced programs and reference definitions are not titles. Only paired emphasis is stripped:
29
+ * glob stars and identifier underscores are meaningful text. Older saved titles may already have
30
+ * been cut through a markdown span; cut links keep their partial label, unpaired emphasis stays.
31
+ * @param input.text - A prompt or a previously saved title.
32
+ * @returns Plain text with whitespace collapsed; identifiers keep their internal underscores.
33
+ * @complexity O(n²) worst-case regex time for unmatched spans; O(n) space in title/prompt length.
34
+ */
35
+ export declare function stripConversationTitleMarkdown({ text }: {
36
+ text: string;
37
+ }, _options?: {}): string;
26
38
  /**
27
39
  * Returns a short display title for a conversation, or `''` when the prompt yields nothing
28
40
  * usable (a bare URL, a lone code fence, pure punctuation). Callers should treat `''` as "leave
@@ -33,5 +45,17 @@
33
45
  */
34
46
  export declare function deriveConversationTitle({ prompt }: {
35
47
  prompt: string;
48
+ }, { casing }?: {
49
+ casing?: 'title' | 'sentence';
36
50
  }): string;
51
+ /** Sentence-case only a known heuristic title using the original prompt's proper names/acronyms.
52
+ * Manual names, model-authored names and unloaded transcripts keep their saved wording, with
53
+ * presentation markup removed and the display ceiling applied; no writes.
54
+ * @complexity O(n) time/space in the first prompt's length, using the existing bounded heuristic.
55
+ */
56
+ export declare function conversationTitleForDisplay({ title, titleSource, firstPrompt }: {
57
+ title: string | null;
58
+ titleSource: 'fallback' | 'generated' | 'manual';
59
+ firstPrompt?: string | undefined;
60
+ }, _options?: {}): string | null;
37
61
  //# sourceMappingURL=title.d.ts.map
@@ -76,23 +76,60 @@ const LATIN_NEVER_TRAILING = new Set([
76
76
  'from', 'into', 'about', 'so', 'just', 'also', 'too', 'up', 'out',
77
77
  ]);
78
78
  /**
79
- * Strips the parts of a prompt that are never a good title: fenced and inline code, URLs, and
79
+ * Strips the parts of a prompt that are never a good title: fenced code, URLs, and
80
80
  * `@handle`/`#tag` tokens. Each of these is high-entropy and low-meaning in a six-word summary.
81
+ * Inline code keeps its label with delimiters removed, so a filename can still name the task.
81
82
  */
82
83
  function cleanPrompt(prompt) {
83
- return prompt
84
- .replace(/```[\s\S]*?```/g, ' ')
85
- .replace(/`[^`]*`/g, ' ')
84
+ return stripConversationTitleMarkdown({ text: prompt })
86
85
  .replace(/https?:\/\/\S+/g, ' ')
87
86
  .replace(/[@#][\w.-]+/g, ' ')
88
87
  .replace(/[“”"']/g, '')
89
88
  .replace(/\s+/g, ' ')
90
89
  .trim();
91
90
  }
91
+ /** Remove presentation markup from title text while preserving link labels and inline code.
92
+ * Shared by prompt derivation, stored-title display and product naming adapters; no DOM or I/O.
93
+ * Fenced programs and reference definitions are not titles. Only paired emphasis is stripped:
94
+ * glob stars and identifier underscores are meaningful text. Older saved titles may already have
95
+ * been cut through a markdown span; cut links keep their partial label, unpaired emphasis stays.
96
+ * @param input.text - A prompt or a previously saved title.
97
+ * @returns Plain text with whitespace collapsed; identifiers keep their internal underscores.
98
+ * @complexity O(n²) worst-case regex time for unmatched spans; O(n) space in title/prompt length.
99
+ */
100
+ export function stripConversationTitleMarkdown({ text }, _options = {}) {
101
+ return text
102
+ .replace(/```[\s\S]*?(?:```|$)/g, ' ')
103
+ .replace(/^\s{0,3}\[[^\]\n]+\]:[^\n]*(?:\n|$)/gm, ' ')
104
+ .replace(/!?\[([^\[\]\n]+)\]\((?:[^()[\]\n]|\([^()[\]\n]*\))*(?:\)|$)/g, '$1')
105
+ .replace(/\[([^\[\]\n]+)\]\[[^\[\]\n]*(?:\]|$)/g, '$1')
106
+ .replace(/^\s{0,3}#{1,6}\s+|[ \t]+#+[ \t]*$/gm, '')
107
+ .replace(/(?<![\w\\*])(\*\*|\*)(?=[^\s*])([^*\n]*?[^\s*\\])\1(?![\w*])/g, '$2')
108
+ .replace(/(?<![\w\\_])(__|_)(?=[^\s_])([^\n]*?[^\s\\])\1(?![\w_])/g, '$2')
109
+ .replace(/`|~~/g, '')
110
+ .replace(/!?\[([^\]\n]*)(?=\n|$)/g, '$1')
111
+ // Unescape after markup removal so an escaped literal marker cannot become emphasis.
112
+ .replace(/\\([\\`*_[\]~])/g, '$1')
113
+ .replace(/\s+/g, ' ')
114
+ .trim();
115
+ }
116
+ /** Keep complete words at the display ceiling. A single oversized token still gets a bounded
117
+ * prefix: pasted blobs must never become megabyte-sized stored/listed titles.
118
+ * @complexity O(n) time/space, bounded by the title ceiling after the initial length check.
119
+ */
120
+ function truncateTitle(title) {
121
+ if (title.length <= MAX_TITLE_CHARS)
122
+ return title;
123
+ const prefix = title.slice(0, MAX_TITLE_CHARS);
124
+ if (/\s/.test(title.charAt(MAX_TITLE_CHARS)))
125
+ return prefix.trimEnd();
126
+ const lastSpace = prefix.lastIndexOf(' ');
127
+ return (lastSpace > 0 ? prefix.slice(0, lastSpace) : prefix).trimEnd();
128
+ }
92
129
  function toTitleCase(word) {
93
130
  return word ? word.charAt(0).toUpperCase() + word.slice(1).toLowerCase() : word;
94
131
  }
95
- function trimLatinTitle(input) {
132
+ function trimLatinTitle(input, { casing }) {
96
133
  const words = input
97
134
  .replace(LEADING_LATIN_FILLER, '')
98
135
  .replace(/[^\p{L}\p{N}\s-]/gu, ' ')
@@ -104,7 +141,8 @@ function trimLatinTitle(input) {
104
141
  while (words.length > 0 && LATIN_NEVER_TRAILING.has(words[words.length - 1].toLowerCase())) {
105
142
  words.pop();
106
143
  }
107
- return words.map(toTitleCase).join(' ');
144
+ const title = casing === 'title' ? words.map(toTitleCase).join(' ') : words.join(' ');
145
+ return title ? title.charAt(0).toUpperCase() + title.slice(1) : title;
108
146
  }
109
147
  function trimCjkTitle(input) {
110
148
  let title = input.trim();
@@ -122,16 +160,29 @@ function trimCjkTitle(input) {
122
160
  *
123
161
  * @complexity O(n) in prompt length; no allocation beyond the intermediate strings.
124
162
  */
125
- export function deriveConversationTitle({ prompt }) {
163
+ export function deriveConversationTitle({ prompt }, { casing = 'title' } = {}) {
126
164
  const cleaned = cleanPrompt(prompt);
127
165
  if (!cleaned)
128
166
  return '';
129
167
  const firstClause = cleaned.split(/[\n\r。!?!?]/)[0]?.trim() ?? cleaned;
130
168
  if (!firstClause)
131
169
  return '';
132
- const title = CJK_PATTERN.test(firstClause) ? trimCjkTitle(firstClause) : trimLatinTitle(firstClause);
170
+ const title = CJK_PATTERN.test(firstClause) ? trimCjkTitle(firstClause) : trimLatinTitle(firstClause, { casing });
133
171
  // Truncated without an ellipsis: this is a working title, not prose, and a `…` would end up stored
134
172
  // and shown as if the author wrote it.
135
- return title.length > MAX_TITLE_CHARS ? title.slice(0, MAX_TITLE_CHARS).trimEnd() : title;
173
+ return truncateTitle(title);
174
+ }
175
+ /** Sentence-case only a known heuristic title using the original prompt's proper names/acronyms.
176
+ * Manual names, model-authored names and unloaded transcripts keep their saved wording, with
177
+ * presentation markup removed and the display ceiling applied; no writes.
178
+ * @complexity O(n) time/space in the first prompt's length, using the existing bounded heuristic.
179
+ */
180
+ export function conversationTitleForDisplay({ title, titleSource, firstPrompt }, _options = {}) {
181
+ if (!title)
182
+ return title;
183
+ if (titleSource === 'generated' && firstPrompt && title === deriveConversationTitle({ prompt: firstPrompt })) {
184
+ return deriveConversationTitle({ prompt: firstPrompt }, { casing: 'sentence' });
185
+ }
186
+ return truncateTitle(stripConversationTitleMarkdown({ text: title })) || null;
136
187
  }
137
188
  //# sourceMappingURL=title.js.map
@@ -34,6 +34,19 @@ export declare function humanizeToolName({ name }: {
34
34
  }, { input }?: {
35
35
  input?: (unknown) | undefined;
36
36
  }): string;
37
+ /** Whether the person already answered a card, before its tool call returned. */
38
+ export type IsAnsweredCard = (card: {
39
+ name: string;
40
+ data: unknown;
41
+ }) => boolean;
42
+ export interface RunActivityOptions {
43
+ /**
44
+ * A card the person answered no longer waits on them, even though its tool call stays open while
45
+ * the confirmed work runs (a publish can take minutes). Without this the line kept saying
46
+ * "Waiting for your answer above" under a card that already read "Done." (owner, 2026-10-11).
47
+ */
48
+ isAnsweredCard?: IsAnsweredCard | undefined;
49
+ }
37
50
  /**
38
51
  * The latest signal in a running turn's events.
39
52
  *
@@ -46,7 +59,7 @@ export declare function humanizeToolName({ name }: {
46
59
  */
47
60
  export declare function deriveRunActivity({ events }: {
48
61
  events: readonly AgentEvent[] | undefined;
49
- }): RunActivityState;
62
+ }, { isAnsweredCard }?: RunActivityOptions): RunActivityState;
50
63
  /**
51
64
  * Whether the run is waiting on the person: a card (form, confirm, sign-in) arrived while its tool
52
65
  * call is still open. Hosts use it to hide "still working" notices, which read as a hung run while
@@ -56,7 +69,7 @@ export declare function deriveRunActivity({ events }: {
56
69
  */
57
70
  export declare function isAwaitingAnswer({ events }: {
58
71
  events: readonly AgentEvent[] | undefined;
59
- }): boolean;
72
+ }, options?: RunActivityOptions): boolean;
60
73
  /** `4` → `"4 s"`, `80` → `"1 m 20 s"`, `120` → `"2 m 0 s"`. */
61
74
  export declare function formatActivityClock({ totalSeconds }: {
62
75
  totalSeconds: number;
@@ -67,6 +67,8 @@ function isVisible(event) {
67
67
  function isAnswerCard(scan, name, data) {
68
68
  if (!CARD_EXT_NAMES.has(name))
69
69
  return false;
70
+ if (scan.isAnsweredCard?.({ name, data }) === true)
71
+ return false;
70
72
  if (name !== A2UI_EXT_NAME)
71
73
  return true;
72
74
  const displayOnlyId = displayOnlySurfaceIdOf({ message: data });
@@ -109,8 +111,8 @@ function scanEvent(scan, event, index) {
109
111
  *
110
112
  * @complexity O(n) in events, O(open tool calls) extra space.
111
113
  */
112
- export function deriveRunActivity({ events }) {
113
- const scan = { open: [], lastVisible: null, lastCardIndex: -1, retry: null, progress: null, visibleCount: 0, displayOnlySurfaces: new Set() };
114
+ export function deriveRunActivity({ events }, { isAnsweredCard } = {}) {
115
+ const scan = { open: [], lastVisible: null, lastCardIndex: -1, retry: null, progress: null, visibleCount: 0, displayOnlySurfaces: new Set(), isAnsweredCard };
114
116
  (events ?? []).forEach((event, index) => scanEvent(scan, event, index));
115
117
  const visibleCount = scan.visibleCount;
116
118
  const lastVisibleIndex = scan.lastVisible?.index ?? -1;
@@ -161,8 +163,8 @@ export function deriveRunActivity({ events }) {
161
163
  *
162
164
  * @complexity O(n) in events.
163
165
  */
164
- export function isAwaitingAnswer({ events }) {
165
- return deriveRunActivity({ events }).activity.kind === 'awaiting-answer';
166
+ export function isAwaitingAnswer({ events }, options = {}) {
167
+ return deriveRunActivity({ events }, options).activity.kind === 'awaiting-answer';
166
168
  }
167
169
  /** `4` → `"4 s"`, `80` → `"1 m 20 s"`, `120` → `"2 m 0 s"`. */
168
170
  export function formatActivityClock({ totalSeconds }) {
@@ -32,6 +32,8 @@ export function parseUsageEvent({ payload }, _options = {}) {
32
32
  ...(typeof usage.input_tokens === "number" ? { inputTokens: usage.input_tokens } : {}),
33
33
  ...(typeof usage.output_tokens === "number" ? { outputTokens: usage.output_tokens } : {}),
34
34
  ...(typeof payload.costUsd === "number" ? { costUsd: payload.costUsd } : {}),
35
+ ...(typeof payload.cumulativeCostUsd === "number" ? { cumulativeCostUsd: payload.cumulativeCostUsd } : {}),
36
+ ...(typeof payload.costSessionId === "string" ? { costSessionId: payload.costSessionId } : {}),
35
37
  ...(typeof payload.durationMs === "number" ? { durationMs: payload.durationMs } : {}),
36
38
  };
37
39
  }
@@ -33,4 +33,19 @@ export declare function describeToolCallDetail({ input, result }: {
33
33
  readonly isError?: boolean;
34
34
  } | undefined;
35
35
  }, { runStreaming, runSucceeded, maxChars, redact }?: ToolCallDetailOptions): ToolCallDetail;
36
+ /** Per-block cap for the text a collapsed row keeps mounted — see {@link describeCollapsedToolCall}. */
37
+ export declare const COLLAPSED_DETAIL_MAX_CHARS = 400;
38
+ /**
39
+ * The short, single-node text a collapsed row keeps in the DOM in place of its full detail. A long
40
+ * chat mounts hundreds of collapsed rows (713 in a 38-message chat, 3.45 MB of hidden text at the
41
+ * full 10 000-char cap), so the full panel is mounted only while open; this keeps the gist —
42
+ * input, then output or the note explaining its absence — readable through the row's
43
+ * `textContent` for a `page.*` DOM reader that never clicks.
44
+ *
45
+ * @param required - Same as {@link describeToolCallDetail}.
46
+ * @param options - Same as {@link describeToolCallDetail}; `maxChars` defaults to {@link COLLAPSED_DETAIL_MAX_CHARS}.
47
+ * @returns Input (when it has a JSON form) and output or note, joined by a newline — never empty, since a missing output always carries a note.
48
+ * @complexity O(n) in the serialized input plus result length (redaction runs before the cap).
49
+ */
50
+ export declare function describeCollapsedToolCall(required: Parameters<typeof describeToolCallDetail>[0], options?: ToolCallDetailOptions): string;
36
51
  //# sourceMappingURL=tool-call-detail.d.ts.map
@@ -25,6 +25,24 @@ export function describeToolCallDetail({ input, result }, { runStreaming = false
25
25
  return { inputText, outputText: null, noteKey: result.isError ? 'Failed without an error message.' : 'Finished with no output.' };
26
26
  return { inputText, outputText: show(formatToolOutputForDisplay({ text: result.content })), noteKey: null };
27
27
  }
28
+ /** Per-block cap for the text a collapsed row keeps mounted — see {@link describeCollapsedToolCall}. */
29
+ export const COLLAPSED_DETAIL_MAX_CHARS = 400;
30
+ /**
31
+ * The short, single-node text a collapsed row keeps in the DOM in place of its full detail. A long
32
+ * chat mounts hundreds of collapsed rows (713 in a 38-message chat, 3.45 MB of hidden text at the
33
+ * full 10 000-char cap), so the full panel is mounted only while open; this keeps the gist —
34
+ * input, then output or the note explaining its absence — readable through the row's
35
+ * `textContent` for a `page.*` DOM reader that never clicks.
36
+ *
37
+ * @param required - Same as {@link describeToolCallDetail}.
38
+ * @param options - Same as {@link describeToolCallDetail}; `maxChars` defaults to {@link COLLAPSED_DETAIL_MAX_CHARS}.
39
+ * @returns Input (when it has a JSON form) and output or note, joined by a newline — never empty, since a missing output always carries a note.
40
+ * @complexity O(n) in the serialized input plus result length (redaction runs before the cap).
41
+ */
42
+ export function describeCollapsedToolCall(required, options = {}) {
43
+ const detail = describeToolCallDetail(required, { maxChars: COLLAPSED_DETAIL_MAX_CHARS, ...options });
44
+ return [detail.inputText, detail.outputText ?? detail.noteKey].filter((part) => part !== null).join('\n');
45
+ }
28
46
  function missingResultNote({ runStreaming, runSucceeded }) {
29
47
  if (runStreaming)
30
48
  return 'Waiting for the result or answer…';
@@ -1,11 +1,50 @@
1
1
  import type { AgentEvent } from './events.js';
2
+ import type { ChatMessage } from './messages.js';
2
3
  type UsageEvent = Extract<AgentEvent, {
3
4
  kind: 'usage';
4
5
  }>;
5
- /** Sum per-segment usage for the whole turn, including aborted segments and recovered attempts.
6
- * Missing fields stay absent; monetary charges are never inferred. O(n) time, O(1) space. */
6
+ /** A turn's reported usage plus the conversation's prefix total. `segments` is present only when the
7
+ * message holds more than one reply (a mid-run message started a new one): one entry per reply, in
8
+ * order, each with its own prefix total, so every reply can show its own "Done" line. */
9
+ export type ChatUsageSummary = UsageEvent & {
10
+ totalCostUsd?: number;
11
+ segments?: readonly (ChatUsageSummary | undefined)[];
12
+ };
13
+ type SessionCost = {
14
+ sessionId: string;
15
+ chargedUsd: number;
16
+ };
17
+ /** The ext event a message sent mid-run arrives as (the daemon's `user_message` run event). The agent
18
+ * answers it as a new reply inside the same run, so it ends the reply before it. */
19
+ export declare const MID_RUN_USER_MESSAGE_EVENT_NAME = "user_message";
20
+ /** Whether this event is a mid-run user message, i.e. the boundary between two replies of one run. */
21
+ export declare function isReplyBoundary({ event }: {
22
+ event: AgentEvent;
23
+ }, _optional?: {}): boolean;
24
+ /** Split one assistant message's events into replies at each mid-run user message. A usage event
25
+ * that lands after a boundary but before the next reply produced anything is the interrupted
26
+ * reply's own result (the daemon notes it can arrive after the interrupt ack), so it stays with
27
+ * the reply before. Event order is otherwise preserved. O(n) for n events.
28
+ * @example replySegments({ events }).length // 1 + number of mid-run user messages */
29
+ export declare function replySegments({ events }: {
30
+ events: readonly AgentEvent[] | undefined;
31
+ }, _optional?: {}): AgentEvent[][];
32
+ /** Sum reported segment usage for a turn, including aborted segments and recovered attempts.
33
+ * Optional projection state is updated to reconcile session snapshots across turns.
34
+ * Missing fields stay absent; money is never inferred. O(n) time for n events.
35
+ * @example turnUsage({ events: message.events }) */
7
36
  export declare function turnUsage({ events }: {
8
37
  events: readonly AgentEvent[] | undefined;
9
- }, _optional?: {}): UsageEvent | undefined;
38
+ }, { sessions, agentId }?: {
39
+ sessions?: Map<string, SessionCost>;
40
+ agentId?: string;
41
+ }): UsageEvent | undefined;
42
+ /** Derive per-turn charges and prefix totals from persisted events_json, for live and reloaded
43
+ * chats alike. A message with mid-run replies also gets per-reply `segments`. O(e + m) for e events
44
+ * and m messages; no per-row transcript scans or I/O.
45
+ * @example conversationUsage({ messages: savedMessages }).get('assistant-2') */
46
+ export declare function conversationUsage({ messages }: {
47
+ messages: readonly ChatMessage[];
48
+ }, _optional?: {}): Map<string, ChatUsageSummary>;
10
49
  export {};
11
50
  //# sourceMappingURL=turn-usage.d.ts.map
@@ -1,17 +1,114 @@
1
- /** Sum per-segment usage for the whole turn, including aborted segments and recovered attempts.
2
- * Missing fields stay absent; monetary charges are never inferred. O(n) time, O(1) space. */
3
- export function turnUsage({ events }, _optional = {}) {
1
+ /** The ext event a message sent mid-run arrives as (the daemon's `user_message` run event). The agent
2
+ * answers it as a new reply inside the same run, so it ends the reply before it. */
3
+ export const MID_RUN_USER_MESSAGE_EVENT_NAME = 'user_message';
4
+ /** Whether this event is a mid-run user message, i.e. the boundary between two replies of one run. */
5
+ export function isReplyBoundary({ event }, _optional = {}) {
6
+ return event.kind === 'ext' && event.name === MID_RUN_USER_MESSAGE_EVENT_NAME;
7
+ }
8
+ /** Split one assistant message's events into replies at each mid-run user message. A usage event
9
+ * that lands after a boundary but before the next reply produced anything is the interrupted
10
+ * reply's own result (the daemon notes it can arrive after the interrupt ack), so it stays with
11
+ * the reply before. Event order is otherwise preserved. O(n) for n events.
12
+ * @example replySegments({ events }).length // 1 + number of mid-run user messages */
13
+ export function replySegments({ events }, _optional = {}) {
14
+ const segments = [[]];
15
+ let started = false;
16
+ for (const event of events ?? []) {
17
+ if (isReplyBoundary({ event })) {
18
+ segments.push([]);
19
+ started = false;
20
+ continue;
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);
26
+ }
27
+ return segments;
28
+ }
29
+ /** Resolve one charge against this agent/session's last snapshot. State is local to an
30
+ * ordered transcript projection; revisiting a session keeps its baseline. A lower snapshot
31
+ * starts a new counter epoch (some CLIs reset their counter on resume). O(1) map access. */
32
+ function usageCost({ event, agentId, sessions }, _optional = {}) {
33
+ const sessionId = event.costSessionId;
34
+ if (!sessionId)
35
+ return validCost(event.costUsd);
36
+ const key = JSON.stringify([agentId, sessionId]);
37
+ const chargedUsd = sessions.get(key)?.chargedUsd ?? 0;
38
+ const cumulative = validCost(event.cumulativeCostUsd);
39
+ const cost = cumulative === undefined ? validCost(event.costUsd)
40
+ : cumulative < chargedUsd ? cumulative : cumulative - chargedUsd;
41
+ // Interim interrupted-segment charges are already paid; later snapshots subtract them.
42
+ // After a counter reset the new snapshot, rather than the old epoch's total, is the baseline.
43
+ sessions.set(key, { sessionId, chargedUsd: cumulative ?? chargedUsd + (cost ?? 0) });
44
+ return cost;
45
+ }
46
+ /** Missing or malformed costs stay unknown; never infer charges from tokens. */
47
+ function validCost(value) {
48
+ return typeof value === 'number' && Number.isFinite(value) && value >= 0 ? value : undefined;
49
+ }
50
+ /** Sum reported segment usage for a turn, including aborted segments and recovered attempts.
51
+ * Optional projection state is updated to reconcile session snapshots across turns.
52
+ * Missing fields stay absent; money is never inferred. O(n) time for n events.
53
+ * @example turnUsage({ events: message.events }) */
54
+ export function turnUsage({ events }, { sessions = new Map(), agentId = '' } = {}) {
4
55
  let total;
5
56
  for (const event of events ?? []) {
6
57
  if (event.kind !== 'usage')
7
58
  continue;
8
59
  total ??= { kind: 'usage' };
9
- for (const field of ['inputTokens', 'outputTokens', 'costUsd', 'durationMs']) {
60
+ for (const field of ['inputTokens', 'outputTokens', 'durationMs']) {
10
61
  const value = event[field];
11
62
  if (typeof value === 'number' && Number.isFinite(value) && value >= 0)
12
63
  total[field] = (total[field] ?? 0) + value;
13
64
  }
65
+ const cost = usageCost({ event, agentId, sessions });
66
+ if (cost !== undefined)
67
+ total.costUsd = (total.costUsd ?? 0) + cost;
14
68
  }
15
69
  return total;
16
70
  }
71
+ /** Add one reply's usage into a message total; fields absent from both stay absent. O(1). */
72
+ function addUsage({ total, usage }, _optional = {}) {
73
+ if (!usage)
74
+ return total;
75
+ const next = { ...(total ?? { kind: 'usage' }) };
76
+ for (const field of ['inputTokens', 'outputTokens', 'durationMs', 'costUsd']) {
77
+ const value = usage[field];
78
+ if (value !== undefined)
79
+ next[field] = (next[field] ?? 0) + value;
80
+ }
81
+ return next;
82
+ }
83
+ /** Derive per-turn charges and prefix totals from persisted events_json, for live and reloaded
84
+ * chats alike. A message with mid-run replies also gets per-reply `segments`. O(e + m) for e events
85
+ * and m messages; no per-row transcript scans or I/O.
86
+ * @example conversationUsage({ messages: savedMessages }).get('assistant-2') */
87
+ export function conversationUsage({ messages }, _optional = {}) {
88
+ const sessions = new Map();
89
+ const summaries = new Map();
90
+ let totalCostUsd;
91
+ for (const message of messages) {
92
+ if (message.role !== 'assistant')
93
+ continue;
94
+ // Replies run in event order through one session map, so their sum equals the whole-message charge.
95
+ const segments = [];
96
+ let usage;
97
+ for (const events of replySegments({ events: message.events })) {
98
+ const segment = turnUsage({ events }, { sessions, agentId: message.agentId ?? '' });
99
+ usage = addUsage({ total: usage, usage: segment });
100
+ if (segment?.costUsd !== undefined)
101
+ totalCostUsd = (totalCostUsd ?? 0) + segment.costUsd;
102
+ segments.push(segment && { ...segment, ...(totalCostUsd === undefined ? {} : { totalCostUsd }) });
103
+ }
104
+ if (!usage)
105
+ continue;
106
+ summaries.set(message.id, {
107
+ ...usage,
108
+ ...(totalCostUsd === undefined ? {} : { totalCostUsd }),
109
+ ...(segments.length > 1 ? { segments } : {}),
110
+ });
111
+ }
112
+ return summaries;
113
+ }
17
114
  //# sourceMappingURL=turn-usage.js.map
@@ -0,0 +1,10 @@
1
+ /** Format a known turn charge, and its conversation prefix total only when one is passed; round only
2
+ * for display. The total is the chat's running total as of that turn, so an earlier turn's line never
3
+ * changes when later turns run (owner, 2026-10-10: "Done · 16s · $0.39 · total $3.95").
4
+ * @example formatUsageCost({ costUsd: 0.445, totalCostUsd: 2.5 }) // '$0.45 · total $2.50'
5
+ * @example formatUsageCost({ costUsd: 0.445 }) // '$0.45' */
6
+ export declare function formatUsageCost({ costUsd, totalCostUsd }: {
7
+ costUsd: number;
8
+ totalCostUsd?: number | undefined;
9
+ }, _optional?: {}): string;
10
+ //# sourceMappingURL=usage-format.d.ts.map
@@ -0,0 +1,15 @@
1
+ /** Round reported USD upward to cents after clearing arithmetic noise at 1e-8 USD precision.
2
+ * Keep the minimum cent for any positive reported charge, even below the noise precision. */
3
+ function dollars(usd) {
4
+ const cents = usd > 0 ? Math.max(1, Math.ceil(Math.round(usd * 1e8) / 1e6)) : 0;
5
+ return `$${(cents / 100).toFixed(2)}`;
6
+ }
7
+ /** Format a known turn charge, and its conversation prefix total only when one is passed; round only
8
+ * for display. The total is the chat's running total as of that turn, so an earlier turn's line never
9
+ * changes when later turns run (owner, 2026-10-10: "Done · 16s · $0.39 · total $3.95").
10
+ * @example formatUsageCost({ costUsd: 0.445, totalCostUsd: 2.5 }) // '$0.45 · total $2.50'
11
+ * @example formatUsageCost({ costUsd: 0.445 }) // '$0.45' */
12
+ export function formatUsageCost({ costUsd, totalCostUsd }, _optional = {}) {
13
+ return totalCostUsd === undefined ? dollars(costUsd) : `${dollars(costUsd)} · total ${dollars(totalCostUsd)}`;
14
+ }
15
+ //# sourceMappingURL=usage-format.js.map