@jini-ai/chat 0.5.6 → 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 (127) 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/durable-projection.d.ts +3 -1
  5. package/dist/core/durable-projection.js +40 -7
  6. package/dist/core/events.d.ts +3 -0
  7. package/dist/core/history-tool-metadata.d.ts +23 -0
  8. package/dist/core/history-tool-metadata.js +13 -0
  9. package/dist/core/index.d.ts +4 -0
  10. package/dist/core/index.js +4 -0
  11. package/dist/core/persistence/title.d.ts +24 -0
  12. package/dist/core/persistence/title.js +60 -9
  13. package/dist/core/run-activity.d.ts +15 -2
  14. package/dist/core/run-activity.js +6 -4
  15. package/dist/core/run-events/entry.d.ts +12 -1
  16. package/dist/core/run-events/entry.js +19 -6
  17. package/dist/core/tool-call-detail.d.ts +51 -0
  18. package/dist/core/tool-call-detail.js +67 -0
  19. package/dist/core/turn-usage.d.ts +50 -0
  20. package/dist/core/turn-usage.js +114 -0
  21. package/dist/core/usage-format.d.ts +10 -0
  22. package/dist/core/usage-format.js +15 -0
  23. package/dist/embed-widget/browser/lazy-loader.d.ts +34 -0
  24. package/dist/embed-widget/browser/lazy-loader.js +116 -0
  25. package/dist/embed-widget/react/dialog.hooks.d.ts +28 -0
  26. package/dist/embed-widget/react/dialog.hooks.js +54 -0
  27. package/dist/embed-widget/react/embed.d.ts +1 -0
  28. package/dist/embed-widget/react/embed.js +5 -3
  29. package/dist/embed-widget/react/header.d.ts +3 -1
  30. package/dist/embed-widget/react/header.hooks.d.ts +13 -0
  31. package/dist/embed-widget/react/header.hooks.js +32 -0
  32. package/dist/embed-widget/react/header.js +4 -14
  33. package/dist/embed-widget/vanilla/index.d.ts +63 -0
  34. package/dist/embed-widget/vanilla/index.js +319 -0
  35. package/dist/embed-widget/vanilla/state.d.ts +48 -0
  36. package/dist/embed-widget/vanilla/state.js +120 -0
  37. package/dist/embed-widget/vanilla/style.css +29 -0
  38. package/dist/react/chat-pane.d.ts +2 -2
  39. package/dist/react/chat-pane.js +1 -1
  40. package/dist/react/components/A2uiSurfaceCard.d.ts +5 -1
  41. package/dist/react/components/A2uiSurfaceCard.js +9 -9
  42. package/dist/react/components/AttachmentPreviewModal.hooks.d.ts +7 -0
  43. package/dist/react/components/AttachmentPreviewModal.hooks.js +1 -1
  44. package/dist/react/components/ChatFab.d.ts +4 -1
  45. package/dist/react/components/ChatFab.js +2 -2
  46. package/dist/react/components/Composer.d.ts +2 -0
  47. package/dist/react/components/Composer.js +5 -2
  48. package/dist/react/components/ConversationList.d.ts +8 -1
  49. package/dist/react/components/ConversationList.hooks.d.ts +18 -0
  50. package/dist/react/components/ConversationList.hooks.js +25 -0
  51. package/dist/react/components/ConversationList.js +12 -5
  52. package/dist/react/components/ConversationList.rules.d.ts +25 -0
  53. package/dist/react/components/ConversationList.rules.js +19 -0
  54. package/dist/react/components/Markdown.js +6 -6
  55. package/dist/react/components/McpUiSurfaceCard.d.ts +1 -1
  56. package/dist/react/components/McpUiSurfaceCard.js +8 -4
  57. package/dist/react/components/MessageList.d.ts +8 -13
  58. package/dist/react/components/MessageList.hooks.d.ts +4 -0
  59. package/dist/react/components/MessageList.hooks.js +6 -1
  60. package/dist/react/components/MessageList.js +23 -4
  61. package/dist/react/components/MessageRow.d.ts +23 -1
  62. package/dist/react/components/MessageRow.js +61 -36
  63. package/dist/react/components/MessageRow.usage.d.ts +48 -0
  64. package/dist/react/components/MessageRow.usage.js +47 -0
  65. package/dist/react/components/MidRunUserMessage.d.ts +17 -0
  66. package/dist/react/components/MidRunUserMessage.js +41 -0
  67. package/dist/react/components/SentAttachment.d.ts +19 -0
  68. package/dist/react/components/SentAttachment.hooks.d.ts +44 -0
  69. package/dist/react/components/SentAttachment.hooks.js +56 -0
  70. package/dist/react/components/SentAttachment.js +14 -0
  71. package/dist/react/components/ToolCallGroup.d.ts +12 -0
  72. package/dist/react/components/ToolCallGroup.hooks.d.ts +20 -0
  73. package/dist/react/components/ToolCallGroup.hooks.js +23 -0
  74. package/dist/react/components/ToolCallGroup.js +13 -0
  75. package/dist/react/components/ToolCard.js +41 -22
  76. package/dist/react/components/mcp-ui-surface-state.d.ts +21 -0
  77. package/dist/react/components/mcp-ui-surface-state.js +26 -0
  78. package/dist/react/ext-event-renderer-registry.d.ts +7 -0
  79. package/dist/react/features/chat-pane/components/ChatPane.d.ts +1 -1
  80. package/dist/react/features/chat-pane/components/ChatPane.js +20 -7
  81. package/dist/react/features/chat-pane/create-mid-run-message-poster.d.ts +44 -0
  82. package/dist/react/features/chat-pane/create-mid-run-message-poster.js +69 -0
  83. package/dist/react/features/chat-pane/hooks/useChatPane.hooks.d.ts +4 -1
  84. package/dist/react/features/chat-pane/hooks/useChatPane.hooks.js +65 -10
  85. package/dist/react/features/chat-pane/hooks/useChatPaneNewTabLinks.hooks.d.ts +25 -0
  86. package/dist/react/features/chat-pane/hooks/useChatPaneNewTabLinks.hooks.js +70 -0
  87. package/dist/react/features/chat-pane/hooks/useChatPaneRuntimeInventory.hooks.js +4 -0
  88. package/dist/react/features/chat-pane/hooks/useChatPaneSuggestion.hooks.d.ts +9 -0
  89. package/dist/react/features/chat-pane/hooks/useChatPaneSuggestion.hooks.js +11 -0
  90. package/dist/react/features/chat-pane/hooks/useChatPaneSurface.hooks.d.ts +17 -0
  91. package/dist/react/features/chat-pane/hooks/useChatPaneSurface.hooks.js +15 -0
  92. package/dist/react/features/chat-pane/index.d.ts +2 -1
  93. package/dist/react/features/chat-pane/index.js +1 -0
  94. package/dist/react/features/chat-pane/rules.d.ts +27 -0
  95. package/dist/react/features/chat-pane/rules.js +32 -0
  96. package/dist/react/features/chat-pane/styles.d.ts +1 -1
  97. package/dist/react/features/chat-pane/styles.js +92 -1
  98. package/dist/react/features/chat-pane/types.d.ts +39 -1
  99. package/dist/react/hooks/composer-draft-cache.d.ts +15 -0
  100. package/dist/react/hooks/composer-draft-cache.js +20 -0
  101. package/dist/react/hooks/useAnsweredCards.d.ts +20 -0
  102. package/dist/react/hooks/useAnsweredCards.js +23 -0
  103. package/dist/react/hooks/useComposer.d.ts +14 -0
  104. package/dist/react/hooks/useComposer.js +78 -6
  105. package/dist/react/hooks/useConversation.d.ts +8 -0
  106. package/dist/react/hooks/useConversation.js +34 -0
  107. package/dist/react/hooks/useRunActivity.d.ts +5 -3
  108. package/dist/react/hooks/useRunActivity.js +4 -3
  109. package/dist/react/index.d.ts +8 -2
  110. package/dist/react/index.js +6 -1
  111. package/dist/react/message-blocks.d.ts +22 -3
  112. package/dist/react/message-blocks.js +25 -3
  113. package/dist/react/tool-call-display.d.ts +90 -0
  114. package/dist/react/tool-call-display.js +97 -0
  115. package/dist/server/durable-runs/recover.js +19 -11
  116. package/dist/server/run-finalizer/entry.js +2 -1
  117. package/dist/store/ports.d.ts +25 -2
  118. package/dist/store/runtime/durable-run-store.js +5 -2
  119. package/dist/store/runtime/entry.d.ts +1 -0
  120. package/dist/store/runtime/entry.js +1 -0
  121. package/dist/store/runtime/history-tools.d.ts +27 -0
  122. package/dist/store/runtime/history-tools.js +169 -0
  123. package/dist/store/runtime/run-ledger.js +5 -2
  124. package/dist/store/sql/store.js +52 -0
  125. package/dist/testing/index.d.ts +7 -0
  126. package/dist/testing/index.js +22 -0
  127. 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) {
@@ -24,5 +24,7 @@ export declare function terminalMessageNotice({ message }: {
24
24
  * visible text can justify a Continued divider; the recovery marker still records the attempt. */
25
25
  export declare function recoveredRunEvents({ saved }: {
26
26
  saved: readonly AgentEvent[];
27
- }, _optional?: {}): AgentEvent[];
27
+ }, { notice }?: {
28
+ notice?: 'visible' | 'silent';
29
+ }): AgentEvent[];
28
30
  //# sourceMappingURL=durable-projection.d.ts.map
@@ -1,4 +1,4 @@
1
- function identity(event) {
1
+ function identity(event, usageOccurrences) {
2
2
  const wire = event;
3
3
  if (wire.eventId !== undefined)
4
4
  return `event:${wire.eventId}`;
@@ -8,7 +8,15 @@ function identity(event) {
8
8
  return `use:${event.id}`;
9
9
  if (event.kind === 'tool_result')
10
10
  return `result:${event.toolUseId}`;
11
- return JSON.stringify(event);
11
+ const snapshot = JSON.stringify(event);
12
+ if (event.kind === 'usage') {
13
+ // Two segments can have identical counters. Match occurrence-for-occurrence during replay,
14
+ // rather than treating equal charges as the same event and silently dropping one segment.
15
+ const occurrence = (usageOccurrences.get(snapshot) ?? 0) + 1;
16
+ usageOccurrences.set(snapshot, occurrence);
17
+ return `usage:${snapshot}:${occurrence}`;
18
+ }
19
+ return snapshot;
12
20
  }
13
21
  function textOf(events) {
14
22
  return events.filter((event) => event.kind === 'text').map((event) => event.text).join('');
@@ -18,7 +26,9 @@ function textOf(events) {
18
26
  export function mergeRunEvents({ saved, incoming }, _optional = {}) {
19
27
  const previous = saved ?? [];
20
28
  const result = [...previous];
21
- const keys = new Set(previous.map(identity));
29
+ const savedUsage = new Map();
30
+ const incomingUsage = new Map();
31
+ const keys = new Set(previous.map(event => identity(event, savedUsage)));
22
32
  const before = textOf(previous);
23
33
  const after = textOf(incoming);
24
34
  let skip = after.startsWith(before) ? before.length : after.length;
@@ -29,9 +39,12 @@ export function mergeRunEvents({ saved, incoming }, _optional = {}) {
29
39
  if (text)
30
40
  result.push({ ...event, text });
31
41
  }
32
- else if (!keys.has(identity(event))) {
33
- keys.add(identity(event));
34
- result.push(event);
42
+ else {
43
+ const key = identity(event, incomingUsage);
44
+ if (!keys.has(key)) {
45
+ keys.add(key);
46
+ result.push(event);
47
+ }
35
48
  }
36
49
  }
37
50
  return result;
@@ -44,6 +57,9 @@ export function continuingRunNotice({ events, active }, _optional = {}) {
44
57
  const marker = list.findLastIndex((event) => event.kind === 'status' && event.code === 'run_recovering');
45
58
  if (marker < 0)
46
59
  return null;
60
+ const markerEvent = list[marker];
61
+ if (markerEvent?.kind === 'status' && markerEvent.label === '')
62
+ return null;
47
63
  const progressed = list.slice(marker + 1).some((event) => event.kind === 'text' || event.kind === 'tool_use' || event.kind === 'tool_result');
48
64
  return progressed ? null : 'Continuing…';
49
65
  }
@@ -63,7 +79,24 @@ export function terminalMessageNotice({ message }, _optional = {}) {
63
79
  }
64
80
  /** The first answer has no earlier segment to divide, even if startup was retried. Only saved
65
81
  * visible text can justify a Continued divider; the recovery marker still records the attempt. */
66
- export function recoveredRunEvents({ saved }, _optional = {}) {
82
+ export function recoveredRunEvents({ saved }, { notice = 'visible' } = {}) {
83
+ if (notice === 'silent') {
84
+ // A missing rollout is an expected reconstruction fallback, not a failed user turn. Keep an
85
+ // invisible attempt marker for fencing/diagnostic offsets, and preserve all unrelated work.
86
+ const attemptStart = saved.findLastIndex(event => event.kind === 'status' && event.code === 'run_recovering') + 1;
87
+ const diagnostic = saved.findIndex((event, index) => index >= attemptStart && ((event.kind === 'status' && /no rollout found for thread id/.test(`${event.label}\n${event.detail ?? ''}`)) ||
88
+ (event.kind === 'raw' && /no rollout found for thread id/.test(event.line))));
89
+ const events = saved.filter((event, index) => {
90
+ if (diagnostic < 0 || index < attemptStart)
91
+ return true;
92
+ if (event.kind === 'raw')
93
+ return !/no rollout found for thread id/.test(event.line);
94
+ if (event.kind === 'status')
95
+ return event.label !== 'Run failed — the agent process exited without answering' && !/no rollout found for thread id/.test(`${event.label}\n${event.detail ?? ''}`);
96
+ return true;
97
+ });
98
+ return [...events, { kind: 'status', code: 'run_recovering', label: '' }];
99
+ }
67
100
  const hasAnswer = saved.some(event => event.kind === 'text' && event.text.trim().length > 0);
68
101
  return [...saved, ...(hasAnswer ? [{ kind: 'text', text: '\n\n---\n\nContinued\n\n' }] : []),
69
102
  { kind: 'status', code: 'run_recovering', label: 'Continuing…' }];
@@ -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
@@ -8,6 +8,7 @@ export * from './messages.js';
8
8
  export * from './partial-json.js';
9
9
  export * from './tool-events.js';
10
10
  export * from './tool-output.js';
11
+ export * from './tool-call-detail.js';
11
12
  export * from './transport.js';
12
13
  export * from './tools.js';
13
14
  export * from './todos.js';
@@ -32,4 +33,7 @@ export { recoveredRunEvents } from './durable-projection.js';
32
33
  export { composerHistoryKeyAction, createComposerHistoryState, mergeComposerHistory, normalizeComposerHistory, transitionComposerHistory, } from './composer-history.js';
33
34
  export type { ComposerHistoryKeyInput, ComposerHistoryState, ComposerHistoryStoragePort } from './composer-history.js';
34
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';
35
39
  //# sourceMappingURL=index.d.ts.map
@@ -8,6 +8,7 @@ export * from './messages.js';
8
8
  export * from './partial-json.js';
9
9
  export * from './tool-events.js';
10
10
  export * from './tool-output.js';
11
+ export * from './tool-call-detail.js';
11
12
  export * from './transport.js';
12
13
  export * from './tools.js';
13
14
  export * from './todos.js';
@@ -31,4 +32,7 @@ export * from './persistence/index.js';
31
32
  export { recoveredRunEvents } from './durable-projection.js';
32
33
  export { composerHistoryKeyAction, createComposerHistoryState, mergeComposerHistory, normalizeComposerHistory, transitionComposerHistory, } from './composer-history.js';
33
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';
34
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 }) {
@@ -293,6 +293,15 @@ export interface RunFrameOutcome {
293
293
  readonly error?: Error;
294
294
  readonly terminal?: Extract<ChatRunStatus, "succeeded" | "failed" | "canceled">;
295
295
  }
296
+ /**
297
+ * Per-connection memory for {@link translateRunFrame}: which channels are mid-way through an echo
298
+ * line. Only a line's first chunk carries the {@link ECHO_LINE} prefix, so without it the rest of a
299
+ * split `user` echo stays as `raw` — tool-result prose that durable recovery then reads as executor
300
+ * diagnostics. Pass one fresh `{}` per daemon connection (each connection replays from event 0).
301
+ */
302
+ export interface RunFrameCarry {
303
+ openEchoChannels?: Set<string>;
304
+ }
296
305
  /**
297
306
  * Translates one named daemon SSE frame (`event:` name plus raw `data:` text) into its effect on the
298
307
  * turn. The browser's `subscribeToRun` dispatches the result to `RunHandlers`; the server finalizer
@@ -309,5 +318,7 @@ export declare function translateRunFrame({ kind, raw, notices }: {
309
318
  kind: string;
310
319
  raw: string | undefined;
311
320
  notices: RunNotices;
312
- }, _options?: Record<string, never>): RunFrameOutcome;
321
+ }, { carry }?: {
322
+ carry?: RunFrameCarry;
323
+ }): RunFrameOutcome;
313
324
  //# sourceMappingURL=entry.d.ts.map
@@ -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
  }
@@ -439,19 +441,30 @@ function heartbeatStatus(line) {
439
441
  const ECHO_LINE = /^\{"type":"(?:stream_event|assistant|user)"/;
440
442
  /**
441
443
  * One CLI stdout/stderr chunk → events. Every JSON line that is a heartbeat also yields a typed
442
- * `status` event; echo lines ({@link ECHO_LINE}) are dropped; everything else stays in ONE `raw`
443
- * event, verbatim. A chunk with no JSON lines at all (a plain-format CLI) is one `raw` event, as before.
444
+ * `status` event; echo lines ({@link ECHO_LINE}) are dropped — with a `carry`, including the later
445
+ * chunks of a split one; everything else stays in ONE `raw` event, verbatim. A chunk with no JSON
446
+ * lines at all (a plain-format CLI) is one `raw` event, as before.
444
447
  */
445
- function chunkEvent(raw) {
448
+ function chunkEvent(raw, channel, carry) {
446
449
  const frame = parseWire(raw);
447
450
  if (!frame)
448
451
  return [];
449
452
  const chunk = wireString(frame.payload?.chunk);
453
+ const open = carry ? (carry.openEchoChannels ??= new Set()) : undefined;
450
454
  const statuses = [];
451
455
  let kept = "";
452
456
  let droppedAny = false;
457
+ let first = true;
453
458
  for (const piece of chunk.split(/(?<=\n)/)) {
454
- if (ECHO_LINE.test(piece.trimStart())) {
459
+ // Only a chunk's first piece can continue the previous chunk's line; splitting on "\n" starts
460
+ // every later piece on a fresh line. Channels are tracked apart: stderr can interleave mid-line.
461
+ const echo = (first && open?.has(channel) === true) || ECHO_LINE.test(piece.trimStart());
462
+ first = false;
463
+ if (echo && !piece.endsWith("\n"))
464
+ open?.add(channel);
465
+ else
466
+ open?.delete(channel);
467
+ if (echo) {
455
468
  droppedAny = true;
456
469
  continue;
457
470
  }
@@ -497,7 +510,7 @@ function endOutcome(raw, notices) {
497
510
  // where the daemon's plain pre-spawn reason ("The assistant could not start: ...") and the
498
511
  // host's start refusals reach the user.
499
512
  /** Fold one named daemon frame into events, a failure and an optional terminal status. */
500
- export function translateRunFrame({ kind, raw, notices }, _options = {}) {
513
+ export function translateRunFrame({ kind, raw, notices }, { carry } = {}) {
501
514
  switch (kind) {
502
515
  case "agent": {
503
516
  const frame = parseWire(raw);
@@ -506,7 +519,7 @@ export function translateRunFrame({ kind, raw, notices }, _options = {}) {
506
519
  }
507
520
  case "stdout":
508
521
  case "stderr":
509
- return { events: chunkEvent(raw) };
522
+ return { events: chunkEvent(raw, kind, carry) };
510
523
  case "error": {
511
524
  const frame = parseWire(raw);
512
525
  const message = wireString(frame?.payload?.message) || "agent run failed";
@@ -0,0 +1,51 @@
1
+ import { type RedactUserText } from './user-text-redaction.js';
2
+ /** The note keys a row shows instead of (or beside) a result. English text doubles as the i18n key, as everywhere else in this package. */
3
+ export type ToolCallDetailNote = 'Waiting for the result or answer…' | 'No result was recorded for this call.' | 'The run ended before this call returned a result.' | 'Finished with no output.' | 'Failed without an error message.';
4
+ export interface ToolCallDetail {
5
+ /** The call's input, pretty-printed and redacted; `null` when there is none or it cannot be serialized. */
6
+ readonly inputText: string | null;
7
+ /** The result's text, re-indented when JSON and redacted; `null` when there is no result text. */
8
+ readonly outputText: string | null;
9
+ /** Why there is no output, when there is none; `null` when the output says it all. */
10
+ readonly noteKey: ToolCallDetailNote | null;
11
+ }
12
+ export interface ToolCallDetailOptions {
13
+ readonly runStreaming?: boolean;
14
+ readonly runSucceeded?: boolean;
15
+ /** Per-block display cap; a longer input or output is cut with an ellipsis. Defaults to 10 000. */
16
+ readonly maxChars?: number;
17
+ /** Defaults to the chat text policy, so a tool call never shows a key the rest of the chat would hide. */
18
+ readonly redact?: RedactUserText;
19
+ }
20
+ /**
21
+ * Builds the readable detail for one tool call.
22
+ *
23
+ * @param required.input - The `tool_use` event's input, any shape.
24
+ * @param required.result - The paired `tool_result`, when one arrived.
25
+ * @param options - Run state that explains a missing result, the display cap, and the redactor.
26
+ * @returns Display-ready input and output text plus a note when the output is absent.
27
+ * @complexity O(n) in the serialized input plus result length.
28
+ */
29
+ export declare function describeToolCallDetail({ input, result }: {
30
+ readonly input: unknown;
31
+ readonly result?: {
32
+ readonly content: string;
33
+ readonly isError?: boolean;
34
+ } | undefined;
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;
51
+ //# sourceMappingURL=tool-call-detail.d.ts.map