@jini-ai/chat 0.5.5 → 0.5.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 (51) hide show
  1. package/CHANGELOG.md +4 -0
  2. package/dist/core/durable-projection.d.ts +3 -1
  3. package/dist/core/durable-projection.js +40 -7
  4. package/dist/core/index.d.ts +1 -0
  5. package/dist/core/index.js +1 -0
  6. package/dist/core/run-events/entry.d.ts +12 -1
  7. package/dist/core/run-events/entry.js +17 -6
  8. package/dist/core/tool-call-detail.d.ts +36 -0
  9. package/dist/core/tool-call-detail.js +49 -0
  10. package/dist/core/turn-usage.d.ts +11 -0
  11. package/dist/core/turn-usage.js +17 -0
  12. package/dist/react/chat-pane.d.ts +2 -2
  13. package/dist/react/chat-pane.js +1 -1
  14. package/dist/react/components/Composer.d.ts +2 -0
  15. package/dist/react/components/Composer.js +5 -2
  16. package/dist/react/components/Markdown.js +6 -6
  17. package/dist/react/components/MessageList.d.ts +7 -12
  18. package/dist/react/components/MessageList.js +20 -2
  19. package/dist/react/components/MessageRow.d.ts +6 -1
  20. package/dist/react/components/MessageRow.js +10 -4
  21. package/dist/react/components/MidRunUserMessage.d.ts +27 -0
  22. package/dist/react/components/MidRunUserMessage.js +39 -0
  23. package/dist/react/components/ToolCard.js +33 -22
  24. package/dist/react/features/chat-pane/components/ChatPane.d.ts +1 -1
  25. package/dist/react/features/chat-pane/components/ChatPane.js +11 -3
  26. package/dist/react/features/chat-pane/create-mid-run-message-poster.d.ts +44 -0
  27. package/dist/react/features/chat-pane/create-mid-run-message-poster.js +69 -0
  28. package/dist/react/features/chat-pane/hooks/useChatPane.hooks.d.ts +3 -1
  29. package/dist/react/features/chat-pane/hooks/useChatPane.hooks.js +69 -11
  30. package/dist/react/features/chat-pane/hooks/useChatPaneNewTabLinks.hooks.d.ts +25 -0
  31. package/dist/react/features/chat-pane/hooks/useChatPaneNewTabLinks.hooks.js +70 -0
  32. package/dist/react/features/chat-pane/index.d.ts +2 -1
  33. package/dist/react/features/chat-pane/index.js +1 -0
  34. package/dist/react/features/chat-pane/model-options.d.ts +3 -1
  35. package/dist/react/features/chat-pane/model-options.js +6 -2
  36. package/dist/react/features/chat-pane/rules.d.ts +13 -0
  37. package/dist/react/features/chat-pane/rules.js +14 -0
  38. package/dist/react/features/chat-pane/styles.d.ts +1 -1
  39. package/dist/react/features/chat-pane/styles.js +7 -0
  40. package/dist/react/features/chat-pane/types.d.ts +23 -0
  41. package/dist/react/hooks/composer-draft-cache.d.ts +15 -0
  42. package/dist/react/hooks/composer-draft-cache.js +20 -0
  43. package/dist/react/hooks/useComposer.d.ts +14 -0
  44. package/dist/react/hooks/useComposer.js +78 -6
  45. package/dist/react/index.d.ts +4 -2
  46. package/dist/react/index.js +3 -1
  47. package/dist/server/durable-runs/recover.js +19 -11
  48. package/dist/server/run-finalizer/entry.js +2 -1
  49. package/dist/store/runtime/durable-run-store.js +5 -2
  50. package/dist/store/runtime/run-ledger.js +5 -2
  51. package/package.json +9 -9
package/CHANGELOG.md CHANGED
@@ -1,3 +1,7 @@
1
+ ## 0.5.6 — 2026-10-08
2
+
3
+ - Desktop 0.1.13 release fixes: see commits a2659d24 (admin), 5695a7c3 (chat, agent-runtime), 8ad4ac5c (devops).
4
+
1
5
  ## 0.5.5 — 2026-10-08
2
6
 
3
7
  - Tovu clean-up release: code Tovu moved into Jini, plus the Jini clean-up (see the commit log).
@@ -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…' }];
@@ -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';
@@ -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';
@@ -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
@@ -439,19 +439,30 @@ function heartbeatStatus(line) {
439
439
  const ECHO_LINE = /^\{"type":"(?:stream_event|assistant|user)"/;
440
440
  /**
441
441
  * 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.
442
+ * `status` event; echo lines ({@link ECHO_LINE}) are dropped — with a `carry`, including the later
443
+ * chunks of a split one; everything else stays in ONE `raw` event, verbatim. A chunk with no JSON
444
+ * lines at all (a plain-format CLI) is one `raw` event, as before.
444
445
  */
445
- function chunkEvent(raw) {
446
+ function chunkEvent(raw, channel, carry) {
446
447
  const frame = parseWire(raw);
447
448
  if (!frame)
448
449
  return [];
449
450
  const chunk = wireString(frame.payload?.chunk);
451
+ const open = carry ? (carry.openEchoChannels ??= new Set()) : undefined;
450
452
  const statuses = [];
451
453
  let kept = "";
452
454
  let droppedAny = false;
455
+ let first = true;
453
456
  for (const piece of chunk.split(/(?<=\n)/)) {
454
- if (ECHO_LINE.test(piece.trimStart())) {
457
+ // Only a chunk's first piece can continue the previous chunk's line; splitting on "\n" starts
458
+ // every later piece on a fresh line. Channels are tracked apart: stderr can interleave mid-line.
459
+ const echo = (first && open?.has(channel) === true) || ECHO_LINE.test(piece.trimStart());
460
+ first = false;
461
+ if (echo && !piece.endsWith("\n"))
462
+ open?.add(channel);
463
+ else
464
+ open?.delete(channel);
465
+ if (echo) {
455
466
  droppedAny = true;
456
467
  continue;
457
468
  }
@@ -497,7 +508,7 @@ function endOutcome(raw, notices) {
497
508
  // where the daemon's plain pre-spawn reason ("The assistant could not start: ...") and the
498
509
  // host's start refusals reach the user.
499
510
  /** Fold one named daemon frame into events, a failure and an optional terminal status. */
500
- export function translateRunFrame({ kind, raw, notices }, _options = {}) {
511
+ export function translateRunFrame({ kind, raw, notices }, { carry } = {}) {
501
512
  switch (kind) {
502
513
  case "agent": {
503
514
  const frame = parseWire(raw);
@@ -506,7 +517,7 @@ export function translateRunFrame({ kind, raw, notices }, _options = {}) {
506
517
  }
507
518
  case "stdout":
508
519
  case "stderr":
509
- return { events: chunkEvent(raw) };
520
+ return { events: chunkEvent(raw, kind, carry) };
510
521
  case "error": {
511
522
  const frame = parseWire(raw);
512
523
  const message = wireString(frame?.payload?.message) || "agent run failed";
@@ -0,0 +1,36 @@
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
+ //# sourceMappingURL=tool-call-detail.d.ts.map
@@ -0,0 +1,49 @@
1
+ /**
2
+ * The expanded body of a tool-call row: what the agent sent, what came back, and — when nothing
3
+ * came back — why. Shared by every card in `react/components/ToolCard.tsx` so no row kind (a CLI
4
+ * built-in like WebFetch, an unknown MCP tool, a card tool such as `assistant_ask_choice` still
5
+ * waiting on the person's answer) is left without a readable call.
6
+ */
7
+ import { formatToolOutputForDisplay } from './tool-output.js';
8
+ import { redactUserText } from './user-text-redaction.js';
9
+ const DEFAULT_MAX_CHARS = 10_000;
10
+ /**
11
+ * Builds the readable detail for one tool call.
12
+ *
13
+ * @param required.input - The `tool_use` event's input, any shape.
14
+ * @param required.result - The paired `tool_result`, when one arrived.
15
+ * @param options - Run state that explains a missing result, the display cap, and the redactor.
16
+ * @returns Display-ready input and output text plus a note when the output is absent.
17
+ * @complexity O(n) in the serialized input plus result length.
18
+ */
19
+ export function describeToolCallDetail({ input, result }, { runStreaming = false, runSucceeded = false, maxChars = DEFAULT_MAX_CHARS, redact = redactUserText } = {}) {
20
+ const show = (text) => (text === null ? null : truncate(redact({ text: text }).text, maxChars));
21
+ const inputText = show(serializeInput(input));
22
+ if (!result)
23
+ return { inputText, outputText: null, noteKey: missingResultNote({ runStreaming, runSucceeded }) };
24
+ if (!result.content.trim())
25
+ return { inputText, outputText: null, noteKey: result.isError ? 'Failed without an error message.' : 'Finished with no output.' };
26
+ return { inputText, outputText: show(formatToolOutputForDisplay({ text: result.content })), noteKey: null };
27
+ }
28
+ function missingResultNote({ runStreaming, runSucceeded }) {
29
+ if (runStreaming)
30
+ return 'Waiting for the result or answer…';
31
+ return runSucceeded ? 'No result was recorded for this call.' : 'The run ended before this call returned a result.';
32
+ }
33
+ /** A string input is shown as given (re-indented only when it is itself a JSON object/array); anything else is pretty JSON, or `null` when it has no JSON form (circular, a function). */
34
+ function serializeInput(input) {
35
+ if (input === null || input === undefined)
36
+ return null;
37
+ if (typeof input === 'string')
38
+ return formatToolOutputForDisplay({ text: input });
39
+ try {
40
+ return JSON.stringify(input, null, 2) ?? null;
41
+ }
42
+ catch {
43
+ return null;
44
+ }
45
+ }
46
+ function truncate(text, max) {
47
+ return text.length <= max ? text : text.slice(0, max - 1) + '…';
48
+ }
49
+ //# sourceMappingURL=tool-call-detail.js.map
@@ -0,0 +1,11 @@
1
+ import type { AgentEvent } from './events.js';
2
+ type UsageEvent = Extract<AgentEvent, {
3
+ kind: 'usage';
4
+ }>;
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. */
7
+ export declare function turnUsage({ events }: {
8
+ events: readonly AgentEvent[] | undefined;
9
+ }, _optional?: {}): UsageEvent | undefined;
10
+ export {};
11
+ //# sourceMappingURL=turn-usage.d.ts.map
@@ -0,0 +1,17 @@
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 = {}) {
4
+ let total;
5
+ for (const event of events ?? []) {
6
+ if (event.kind !== 'usage')
7
+ continue;
8
+ total ??= { kind: 'usage' };
9
+ for (const field of ['inputTokens', 'outputTokens', 'costUsd', 'durationMs']) {
10
+ const value = event[field];
11
+ if (typeof value === 'number' && Number.isFinite(value) && value >= 0)
12
+ total[field] = (total[field] ?? 0) + value;
13
+ }
14
+ }
15
+ return total;
16
+ }
17
+ //# sourceMappingURL=turn-usage.js.map
@@ -16,6 +16,6 @@
16
16
  * `@deprecated`, so every existing `import { ChatPane } from '@jini-ai/chat/react'` keeps working
17
17
  * unchanged. New code should import from this subpath directly.
18
18
  */
19
- export { AgentRuntimePicker, CHAT_PANE_AGENT_TOOLS, ChatPane, createDaemonAttachmentUploader, createMcpUiToolCaller, createTypedAnswerPoster, defaultChatPaneSelection, orderChatPaneAgents, resolveChatPaneSelection, useChatPane, useChatPaneAgentControl, useChatPaneRuntimeInventory, useChatPaneWorkingDirectory, } from './features/chat-pane/index.js';
20
- export type { AgentRuntimePickerProps, ByokRuntimeSummary, ChatPaneActivity, ChatPaneAgent, ChatPaneAgentBridgeAccess, ChatPaneAgentControlOptions, ChatPaneAgentOption, ChatPaneAgentSelection, ChatPaneAgentToolAction, ChatPaneAgentToolDef, ChatPaneAgentToolInputSchema, ChatPaneAgentToolRisk, ChatPaneAttachmentUploadOptions, ChatPaneComposerHandle, ChatPaneProps, ChatPaneRunContext, ChatPaneRunContextInput, ChatPaneRuntimeAccess, ChatPaneVariant, ChatPaneWorkingDirectoryAccess, CreateDaemonAttachmentUploaderOptions, CreateMcpUiToolCallerOptions, CreateTypedAnswerPosterOptions, DeliverTypedAnswer, McpUiToolCallRequest, RuntimePickerPlacement, TypedAnswerDelivery, TypedAnswerNotice, UseChatPaneAgentControlOptions, UseChatPaneOptions, UseChatPaneResult, UseChatPaneRuntimeInventoryOptions, UseChatPaneRuntimeInventoryResult, UseChatPaneWorkingDirectoryOptions, UseChatPaneWorkingDirectoryResult, } from './features/chat-pane/index.js';
19
+ export { AgentRuntimePicker, CHAT_PANE_AGENT_TOOLS, ChatPane, createDaemonAttachmentUploader, createMcpUiToolCaller, createMidRunMessagePoster, createTypedAnswerPoster, defaultChatPaneSelection, orderChatPaneAgents, resolveChatPaneSelection, useChatPane, useChatPaneAgentControl, useChatPaneRuntimeInventory, useChatPaneWorkingDirectory, } from './features/chat-pane/index.js';
20
+ export type { AgentRuntimePickerProps, ByokRuntimeSummary, ChatPaneActivity, ChatPaneAgent, ChatPaneAgentBridgeAccess, ChatPaneAgentControlOptions, ChatPaneAgentOption, ChatPaneAgentSelection, ChatPaneAgentToolAction, ChatPaneAgentToolDef, ChatPaneAgentToolInputSchema, ChatPaneAgentToolRisk, ChatPaneAttachmentUploadOptions, ChatPaneComposerHandle, ChatPaneProps, ChatPaneRunContext, ChatPaneRunContextInput, ChatPaneRuntimeAccess, ChatPaneVariant, ChatPaneWorkingDirectoryAccess, CreateDaemonAttachmentUploaderOptions, CreateMcpUiToolCallerOptions, CreateMidRunMessagePosterOptions, CreateTypedAnswerPosterOptions, DeliverMidRunMessage, DeliverTypedAnswer, McpUiToolCallRequest, MidRunMessageDelivery, RuntimePickerPlacement, TypedAnswerDelivery, TypedAnswerNotice, UseChatPaneAgentControlOptions, UseChatPaneOptions, UseChatPaneResult, UseChatPaneRuntimeInventoryOptions, UseChatPaneRuntimeInventoryResult, UseChatPaneWorkingDirectoryOptions, UseChatPaneWorkingDirectoryResult, } from './features/chat-pane/index.js';
21
21
  //# sourceMappingURL=chat-pane.d.ts.map
@@ -16,5 +16,5 @@
16
16
  * `@deprecated`, so every existing `import { ChatPane } from '@jini-ai/chat/react'` keeps working
17
17
  * unchanged. New code should import from this subpath directly.
18
18
  */
19
- export { AgentRuntimePicker, CHAT_PANE_AGENT_TOOLS, ChatPane, createDaemonAttachmentUploader, createMcpUiToolCaller, createTypedAnswerPoster, defaultChatPaneSelection, orderChatPaneAgents, resolveChatPaneSelection, useChatPane, useChatPaneAgentControl, useChatPaneRuntimeInventory, useChatPaneWorkingDirectory, } from './features/chat-pane/index.js';
19
+ export { AgentRuntimePicker, CHAT_PANE_AGENT_TOOLS, ChatPane, createDaemonAttachmentUploader, createMcpUiToolCaller, createMidRunMessagePoster, createTypedAnswerPoster, defaultChatPaneSelection, orderChatPaneAgents, resolveChatPaneSelection, useChatPane, useChatPaneAgentControl, useChatPaneRuntimeInventory, useChatPaneWorkingDirectory, } from './features/chat-pane/index.js';
20
20
  //# sourceMappingURL=chat-pane.js.map
@@ -19,6 +19,8 @@ export interface ComposerProps {
19
19
  * separately placed cancel affordance elsewhere in the pane. Always clickable when true,
20
20
  * regardless of `disabled`/`sendDisabled`/`composer.canSubmit`: those all gate *submitting a new
21
21
  * draft*, which has nothing to do with whether a caller may cancel the run already in flight.
22
+ * Once the draft is submittable (and neither gate refuses it), Send comes back beside Stop, so a
23
+ * message can go out mid-run without stopping anything.
22
24
  */
23
25
  running?: boolean;
24
26
  /** Cancels the in-flight run. Required when `running` is true; ignored otherwise. */
@@ -338,7 +338,10 @@ export function Composer({ composer, onSend, disabled = false, sendDisabled = fa
338
338
  composer.setDraft(e.target.value);
339
339
  setSlashActiveIndex(0);
340
340
  setDismissedSlashDraft(null);
341
- }, onKeyDown: handleKeyDown, onPaste: handleAttachmentPaste, "aria-controls": slashOpen ? 'jini-composer-slash-menu' : undefined, "aria-expanded": slashOpen, "aria-activedescendant": slashOpen
341
+ }, onKeyDown: handleKeyDown,
342
+ // Draft persistence is debounced (see `DRAFT_PERSIST_DEBOUNCE_MS`); leaving the field is a
343
+ // natural point to make it durable now.
344
+ onBlur: () => composer.flushDraft?.(), onPaste: handleAttachmentPaste, "aria-controls": slashOpen ? 'jini-composer-slash-menu' : undefined, "aria-expanded": slashOpen, "aria-activedescendant": slashOpen
342
345
  ? `jini-composer-slash-option-${Math.min(slashActiveIndex, slashMatches.length - 1)}`
343
346
  : undefined, rows: 3 }), slashOpen ? (_jsx(ComposerSlashMenu, { matches: slashMatches, activeIndex: Math.min(slashActiveIndex, slashMatches.length - 1), onSelect: (item) => selectSlashItem(slashMatches.findIndex((match) => match.item === item)), showFilterHint: showSlashFilterHint, style: slashMenuPosition, t: t })) : null, _jsxs("div", { className: "jini-composer-footer", children: [attachmentPicker && !hasDiscoveryItems ? (_jsxs("div", { className: "jini-composer-attachment-picker", children: [_jsx("input", { ref: attachmentInputRef, className: "jini-composer-file-input", "data-testid": "composer-attachment-input", type: "file", multiple: true, "aria-label": t('Attach files'), accept: attachmentPicker.accept, disabled: disabled || attachmentPicker.uploading, onChange: handleAttachmentChange }), _jsx("button", { type: "button", className: "jini-composer-attach", disabled: disabled || attachmentPicker.uploading, onClick: () => attachmentInputRef.current?.click(), title: t(attachmentPicker.uploading ? 'Attaching files…' : 'Attach files'), "aria-label": t(attachmentPicker.uploading ? 'Attaching files…' : 'Attach files'), children: _jsx(RemixIcon, { name: attachmentPicker.uploading ? 'refresh-line' : 'add-line', size: 20, ...(attachmentPicker.uploading ? { className: 'jini-composer-spinner' } : {}) }) })] })) : null, hasDiscoveryItems ? (_jsx(ComposerDiscoveryMenu, { groups: discoveryGroups, open: discoveryMenuOpen, disabled: disabled, ...(attachmentPicker ? { attachmentPicker } : {}), attachmentInputRef: attachmentInputRef, onToggle: () => setDiscoveryMenuOpen((open) => !open), onClose: () => setDiscoveryMenuOpen(false), onSelect: selectPlusItem, onAttachmentChange: handleAttachmentChange, style: discoveryMenuPosition, t: t })) : null, slots?.footerLeadingAccessory ? (_jsx("div", { className: "jini-composer-footer-leading", children: slots.footerLeadingAccessory })) : null, onChangeWorkingDirectory || onPickWorkingDirectory ? (_jsxs("div", { className: "jini-composer-workdir", "data-testid": "composer-workdir", children: [_jsx("button", { type: "button", className: "jini-composer-attach", "data-testid": "composer-workdir-trigger", disabled: disabled, onClick: onPickWorkingDirectory ? () => onPickWorkingDirectory() : toggleWorkdir, ...(onPickWorkingDirectory ? {} : { 'aria-haspopup': 'dialog', 'aria-expanded': workdirOpen }), title: workingDirectory ?? t('Set working directory'), "aria-label": workingDirectory
344
347
  ? t('Working directory: {dir}', { dir: workingDirectory })
@@ -351,6 +354,6 @@ export function Composer({ composer, onSend, disabled = false, sendDisabled = fa
351
354
  e.preventDefault();
352
355
  setWorkdirOpen(false);
353
356
  }
354
- }, placeholder: t('Working directory path'), "aria-label": t('Working directory path'), autoFocus: true }), _jsx("button", { type: "button", className: "jini-composer-workdir-confirm", "data-testid": "composer-workdir-confirm", onClick: confirmWorkdir, children: t('Set') })] })) : null] })) : null, slots?.footerAccessories ? (_jsx("div", { className: "jini-composer-footer-accessories", children: slots.footerAccessories })) : null, slots?.plusMenuItems && slots.plusMenuItems.length > 0 ? (_jsx("div", { className: "jini-composer-plus-menu", children: slots.plusMenuItems.map((item) => (_jsxs("button", { type: "button", className: "jini-composer-plus-item", onClick: () => runComposerHostEffect('plusMenuItems.onSelect', () => item.onSelect()), title: t(item.label), children: [item.icon ?? null, _jsx("span", { children: t(item.label) })] }, item.id))) })) : null, running ? (_jsx("button", { type: "button", className: "jini-composer-send jini-composer-send--stop", onClick: onCancel, title: t('Stop run'), "aria-label": t('Stop run'), children: _jsx(RemixIcon, { name: "stop-fill", size: 14 }) })) : (_jsx("button", { type: "button", className: "jini-composer-send", disabled: disabled || sendDisabled || !composer.canSubmit, onClick: onSend, title: t('Send'), "aria-label": t('Send'), children: _jsx(RemixIcon, { name: "send-plane-2-line", size: 16 }) }))] })] }));
357
+ }, placeholder: t('Working directory path'), "aria-label": t('Working directory path'), autoFocus: true }), _jsx("button", { type: "button", className: "jini-composer-workdir-confirm", "data-testid": "composer-workdir-confirm", onClick: confirmWorkdir, children: t('Set') })] })) : null] })) : null, slots?.footerAccessories ? (_jsx("div", { className: "jini-composer-footer-accessories", children: slots.footerAccessories })) : null, slots?.plusMenuItems && slots.plusMenuItems.length > 0 ? (_jsx("div", { className: "jini-composer-plus-menu", children: slots.plusMenuItems.map((item) => (_jsxs("button", { type: "button", className: "jini-composer-plus-item", onClick: () => runComposerHostEffect('plusMenuItems.onSelect', () => item.onSelect()), title: t(item.label), children: [item.icon ?? null, _jsx("span", { children: t(item.label) })] }, item.id))) })) : null, _jsxs("span", { className: "jini-composer-send-group", children: [!running || (!disabled && !sendDisabled && composer.canSubmit) ? (_jsx("button", { type: "button", className: "jini-composer-send", disabled: disabled || sendDisabled || !composer.canSubmit, onClick: onSend, title: t('Send'), "aria-label": t('Send'), children: _jsx(RemixIcon, { name: "send-plane-2-line", size: 16 }) })) : null, running ? (_jsx("button", { type: "button", className: "jini-composer-send jini-composer-send--stop", onClick: onCancel, title: t('Stop run'), "aria-label": t('Stop run'), children: _jsx(RemixIcon, { name: "stop-fill", size: 14 }) })) : null] })] })] }));
355
358
  }
356
359
  //# sourceMappingURL=Composer.js.map
@@ -389,16 +389,16 @@ const INLINE_RE = new RegExp(String.raw `(\x60[^\x60]+\x60)|(${MD_LINK})|(\*\*[^
389
389
  // Only these targets become anchors; anything else (`javascript:`, `data:`, protocol-relative
390
390
  // `//host`) stays literal text, since the text is untrusted model output.
391
391
  const SAFE_LINK_HREF = /^(?:https?:\/\/|mailto:|\/(?!\/)|#)/iu;
392
- // Off-site links open in a new tab; a same-origin path stays in the tab so the host's in-app link
393
- // interceptor (an SPA router) can take it without a reload.
394
- const OFFSITE_LINK_HREF = /^(?:https?:|mailto:)/iu;
392
+ // Every link opens in a new tab — an in-app path too, so following one never navigates the tab the
393
+ // chat lives in (owner 2026-10-08). Only a `#` same-document anchor stays in place. ChatPane's
394
+ // click guard (`useChatPaneNewTabLinks`) covers links this renderer does not draw.
395
+ const NEW_TAB = { target: '_blank', rel: 'noopener noreferrer' };
395
396
  function renderMarkdownLink(source, key) {
396
397
  // `MD_LINK` already matched `source`, so these two groups always capture.
397
398
  const [, label, href] = MD_LINK_PARTS.exec(source);
398
399
  if (!SAFE_LINK_HREF.test(href))
399
400
  return source;
400
- const offsite = OFFSITE_LINK_HREF.test(href);
401
- return (_jsx("a", { href: href, ...(offsite ? { target: '_blank', rel: 'noreferrer' } : {}), children: renderInline(label) }, key));
401
+ return (_jsx("a", { href: href, ...(href.startsWith('#') ? {} : NEW_TAB), children: renderInline(label) }, key));
402
402
  }
403
403
  function renderInline(text) {
404
404
  const nodes = [];
@@ -421,7 +421,7 @@ function renderInline(text) {
421
421
  else if (italic)
422
422
  nodes.push(_jsx("em", { children: italic.slice(1, -1) }, key++));
423
423
  else if (link)
424
- nodes.push(_jsx("a", { href: link, target: "_blank", rel: "noreferrer", children: link }, key++));
424
+ nodes.push(_jsx("a", { href: link, ...NEW_TAB, children: link }, key++));
425
425
  // `whole` is `match[0]` — a successful `RegExpExecArray` always has a
426
426
  // defined (if possibly empty) full-match string at index 0, so this
427
427
  // fallback is unreachable (same dead-fallback reasoning as above).
@@ -1,14 +1,3 @@
1
- /**
2
- * @module MessageList
3
- *
4
- * Renders a conversation's messages via `<MessageRow>`, and — matching
5
- * `useConversation`'s `scrollIntent` flag (see
6
- * `ADS-memory/reports/jini-port/recon/r4b-webui-design.md` §4: "message array, optimistic
7
- * user message, scroll-intent flag") — auto-scrolls to the newest content
8
- * whenever `scrollIntent` is true, calling `onScrolled` once it has. The
9
- * scroll-anchoring state lives in `useMessageListAutoScroll`
10
- * (`MessageList.hooks.ts`); see that file for why it is presentational.
11
- */
12
1
  import type { ChatMessage } from '../../core/index.js';
13
2
  import { type MessageRowProps } from './MessageRow.js';
14
3
  export interface MessageListProps extends Pick<MessageRowProps, 'projectFileNames' | 'onRequestOpenFile' | 'renderAttachment'> {
@@ -35,5 +24,11 @@ export interface MessageListProps extends Pick<MessageRowProps, 'projectFileName
35
24
  onCancel: () => void;
36
25
  } | null;
37
26
  }
38
- export declare function MessageList({ messages, isStreaming, scrollIntent, onScrolled, activeQuestionFormMessageId, questionFormSubmittedAnswersByMessageId, onQuestionFormSubmit, projectFileNames, onRequestOpenFile, renderAttachment, pendingPrompt, }: MessageListProps): import("react").JSX.Element;
27
+ /**
28
+ * Memoized: the pane re-renders on every composer keystroke, and re-rendering a long transcript
29
+ * (hundreds of tool rows, MCP-UI cards, markdown) per key made typing lag (owner report
30
+ * 2026-10-08). Callers must pass stable props — `ChatPane` memoizes `pendingPrompt` and
31
+ * `projectFileNames` for exactly this.
32
+ */
33
+ export declare const MessageList: import("react").NamedExoticComponent<MessageListProps>;
39
34
  //# sourceMappingURL=MessageList.d.ts.map
@@ -1,9 +1,27 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ /**
3
+ * @module MessageList
4
+ *
5
+ * Renders a conversation's messages via `<MessageRow>`, and — matching
6
+ * `useConversation`'s `scrollIntent` flag (see
7
+ * `ADS-memory/reports/jini-port/recon/r4b-webui-design.md` §4: "message array, optimistic
8
+ * user message, scroll-intent flag") — auto-scrolls to the newest content
9
+ * whenever `scrollIntent` is true, calling `onScrolled` once it has. The
10
+ * scroll-anchoring state lives in `useMessageListAutoScroll`
11
+ * (`MessageList.hooks.ts`); see that file for why it is presentational.
12
+ */
13
+ import { memo } from 'react';
2
14
  import { isTerminalRunStatus } from '../../core/index.js';
3
15
  import { useT } from '../hooks/context.js';
4
16
  import { useMessageListAutoScroll } from './MessageList.hooks.js';
5
17
  import { MessageRow } from './MessageRow.js';
6
- export function MessageList({ messages, isStreaming = false, scrollIntent = false, onScrolled, activeQuestionFormMessageId = null, questionFormSubmittedAnswersByMessageId, onQuestionFormSubmit, projectFileNames, onRequestOpenFile, renderAttachment, pendingPrompt = null, }) {
18
+ /**
19
+ * Memoized: the pane re-renders on every composer keystroke, and re-rendering a long transcript
20
+ * (hundreds of tool rows, MCP-UI cards, markdown) per key made typing lag (owner report
21
+ * 2026-10-08). Callers must pass stable props — `ChatPane` memoizes `pendingPrompt` and
22
+ * `projectFileNames` for exactly this.
23
+ */
24
+ export const MessageList = memo(function MessageList({ messages, isStreaming = false, scrollIntent = false, onScrolled, activeQuestionFormMessageId = null, questionFormSubmittedAnswersByMessageId, onQuestionFormSubmit, projectFileNames, onRequestOpenFile, renderAttachment, pendingPrompt = null, }) {
7
25
  const t = useT();
8
26
  const { containerRef, onScroll } = useMessageListAutoScroll({ messages, scrollIntent }, onScrolled !== undefined ? { onScrolled } : {});
9
27
  return (_jsxs("div", { className: "jini-message-list", ref: containerRef, onScroll: onScroll, "data-agent-element": "chat-transcript", "data-agent-role": "list", "data-agent-label": "The conversation transcript, oldest message first", children: [messages.map((message) => {
@@ -11,5 +29,5 @@ export function MessageList({ messages, isStreaming = false, scrollIntent = fals
11
29
  const runStreaming = isLast && isStreaming && !isTerminalRunStatus({ status: message.runStatus });
12
30
  return (_jsx(MessageRow, { message: message, runStreaming: runStreaming, runSucceeded: message.runStatus === 'succeeded', questionFormInteractive: message.id === activeQuestionFormMessageId, ...(questionFormSubmittedAnswersByMessageId?.[message.id] !== undefined ? { questionFormSubmittedAnswers: questionFormSubmittedAnswersByMessageId[message.id] } : {}), ...(onQuestionFormSubmit !== undefined ? { onQuestionFormSubmit: (text, answers) => onQuestionFormSubmit(message.id, text, answers) } : {}), ...(projectFileNames !== undefined ? { projectFileNames } : {}), ...(onRequestOpenFile !== undefined ? { onRequestOpenFile } : {}), ...(renderAttachment !== undefined ? { renderAttachment } : {}) }, message.id));
13
31
  }), pendingPrompt ? (_jsxs("div", { className: "jini-chat-pane__queued", role: "status", "aria-label": t('Message queued — not yet sent, will send once the current run finishes'), "data-testid": "chat-pane-queued", "data-agent-element": "chat-message-pending", "data-agent-role": "region", "data-agent-label": "A message queued to send once the current run finishes", children: [_jsx("div", { className: "jini-chat-pane__queued-text", children: pendingPrompt.text }), _jsx("div", { className: "jini-chat-pane__queued-actions", children: _jsx("button", { type: "button", className: "jini-chat-pane__queued-cancel", onClick: pendingPrompt.onCancel, title: t('Cancel queued message'), children: t('Cancel') }) })] })) : null] }));
14
- }
32
+ });
15
33
  //# sourceMappingURL=MessageList.js.map
@@ -64,5 +64,10 @@ export interface MessageRowProps {
64
64
  /** Host-supplied renderer for a `ChatAttachment` chip. Falls back to a plain filename chip. */
65
65
  renderAttachment?: (attachment: ChatAttachment) => ReactNode;
66
66
  }
67
- export declare function MessageRow({ message, runStreaming, runSucceeded, questionFormInteractive, questionFormSubmittedAnswers, onQuestionFormSubmit, projectFileNames, onRequestOpenFile, renderAttachment, }: MessageRowProps): React.JSX.Element;
67
+ /**
68
+ * Memoized so a transcript re-render (a streaming update to the newest message, or a parent that
69
+ * re-renders per composer keystroke) only re-renders rows whose props actually changed. Messages
70
+ * are replaced, never mutated, so identity is a sound change signal.
71
+ */
72
+ export declare const MessageRow: React.NamedExoticComponent<MessageRowProps>;
68
73
  //# sourceMappingURL=MessageRow.d.ts.map
@@ -1,5 +1,6 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
2
  import { terminalMessageNotice, continuingRunNotice } from '../../core/durable-projection.js';
3
+ import { turnUsage } from '../../core/turn-usage.js';
3
4
  /**
4
5
  * @module MessageRow
5
6
  *
@@ -50,7 +51,7 @@ import { terminalMessageNotice, continuingRunNotice } from '../../core/durable-p
50
51
  * not appear until the whole message is done, not merely until its current
51
52
  * chunk stopped arriving.
52
53
  */
53
- import React, { useState } from 'react';
54
+ import React, { memo, useState } from 'react';
54
55
  import { isAwaitingAnswer, isTerminalRunStatus, splitOnQuestionForms, stripArtifact } from '../../core/index.js';
55
56
  import { useToolTimeline } from '../hooks/useToolTimeline.js';
56
57
  import { useExtEventGroups } from '../hooks/useExtEventGroups.js';
@@ -154,7 +155,12 @@ function CopyMessageButton({ text, label }) {
154
155
  void copy(text);
155
156
  }, children: _jsx(Icon, { name: copied ? 'check' : 'copy' }) }), _jsx("span", { className: "jini-message-copy-status", "aria-live": "polite", children: copied ? t('Copied to clipboard') : '' })] }));
156
157
  }
157
- export function MessageRow({ message, runStreaming = false, runSucceeded = false, questionFormInteractive = false, questionFormSubmittedAnswers, onQuestionFormSubmit, projectFileNames, onRequestOpenFile, renderAttachment, }) {
158
+ /**
159
+ * Memoized so a transcript re-render (a streaming update to the newest message, or a parent that
160
+ * re-renders per composer keystroke) only re-renders rows whose props actually changed. Messages
161
+ * are replaced, never mutated, so identity is a sound change signal.
162
+ */
163
+ export const MessageRow = memo(function MessageRow({ message, runStreaming = false, runSucceeded = false, questionFormInteractive = false, questionFormSubmittedAnswers, onQuestionFormSubmit, projectFileNames, onRequestOpenFile, renderAttachment, }) {
158
164
  const t = useT();
159
165
  const timeline = useToolTimeline({ events: message.events }, { runStreaming, runSucceeded });
160
166
  const extGroups = useExtEventGroups({ events: message.events }, { slotOf: (name, data) => extEventSlot({ name, data }) });
@@ -168,7 +174,7 @@ export function MessageRow({ message, runStreaming = false, runSucceeded = false
168
174
  const terminalNotice = terminalMessageNotice({ message }, {});
169
175
  const visibleContent = stripArtifact({ content: message.content });
170
176
  const segments = splitOnQuestionForms({ input: visibleContent });
171
- const usageEvent = message.events?.filter((ev) => ev.kind === 'usage').pop();
177
+ const usageEvent = turnUsage({ events: message.events }, {});
172
178
  const blocks = messageBlocks({ visibleContent, message, rows: timeline.rows }, {});
173
179
  const pending = isPendingWithNoContent(message, visibleContent, timeline.rows.length);
174
180
  const runInProgress = isRunInProgress(message.runStatus);
@@ -210,7 +216,7 @@ export function MessageRow({ message, runStreaming = false, runSucceeded = false
210
216
  return (_jsx("div", { className: "jini-message-ext-events", children: renderExtGroup(group) }, block.key));
211
217
  })
212
218
  : (_jsxs(_Fragment, { children: [segments.map((segment, i) => segment.kind === 'text' ? (segment.text.trim() ? (_jsx("div", { className: "jini-message-content", children: _jsx(Markdown, { children: segment.text }) }, i)) : null) : (_jsx(QuestionForm, { form: segment.form, interactive: questionFormInteractive, ...(questionFormSubmittedAnswers !== undefined ? { submittedAnswers: questionFormSubmittedAnswers } : {}), ...(onQuestionFormSubmit !== undefined ? { onSubmit: onQuestionFormSubmit } : {}) }, i))), timeline.rows.length > 0 ? (_jsx("div", { className: "jini-message-tools", children: timeline.rows.map(renderToolCard) })) : null, extGroups.length > 0 ? _jsx("div", { className: "jini-message-ext-events", children: extGroups.map(renderExtGroup) }) : null] })), _jsx(AssistantMessageFooter, { message, usageEvent, terminalNotice, pending, runInProgress, runStreaming, visibleContent })] }));
213
- }
219
+ });
214
220
  /**
215
221
  * The live "what is it doing" line under a running reply (see `core/run-activity.ts`). Replaces the
216
222
  * old "Thinking…" placeholder, which vanished for good as soon as the first tool row appeared.
@@ -0,0 +1,27 @@
1
+ /**
2
+ * @module MidRunUserMessage
3
+ *
4
+ * Renders a message the human sent while a run was going (`ChatPane`'s `deliverMidRunMessage`) as a
5
+ * user bubble inside the running turn, at the point in the stream where the live agent received it.
6
+ * The daemon records each one as a `user_message` run event (`AgentExecutor.sendUserMessage`), which
7
+ * the run-event translator surfaces as `kind: 'ext'` with that name; this claims it.
8
+ *
9
+ * It is an event of the assistant turn, not a `ChatMessage` of its own, because the agent folded it
10
+ * into the turn it was already running — there is no separate reply to hang off it.
11
+ */
12
+ import { type ExtEventRenderProps } from '../ext-event-renderer-registry.js';
13
+ /** The ext-event name the daemon's mid-run `user_message` run event arrives under. */
14
+ export declare const MID_RUN_USER_MESSAGE_EXT_EVENT_NAME = "user_message";
15
+ /** One slot per message id, so each mid-run message renders where it arrived rather than folding into the first. */
16
+ export declare function midRunUserMessageSlotKey({ data }: {
17
+ data: unknown;
18
+ }): string | undefined;
19
+ /** The bubble for one mid-run message — the same classes as a sent user message, so it reads as one. */
20
+ export declare function MidRunUserMessage({ events }: Pick<ExtEventRenderProps, 'events'>): import("react").JSX.Element | null;
21
+ /**
22
+ * Registers {@link MidRunUserMessage} against the ext-event registry.
23
+ *
24
+ * @returns An unregister handle, so a test or a hot reload can dispose cleanly.
25
+ */
26
+ export declare function registerMidRunUserMessageRenderer(): () => void;
27
+ //# sourceMappingURL=MidRunUserMessage.d.ts.map
@@ -0,0 +1,39 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ /**
3
+ * @module MidRunUserMessage
4
+ *
5
+ * Renders a message the human sent while a run was going (`ChatPane`'s `deliverMidRunMessage`) as a
6
+ * user bubble inside the running turn, at the point in the stream where the live agent received it.
7
+ * The daemon records each one as a `user_message` run event (`AgentExecutor.sendUserMessage`), which
8
+ * the run-event translator surfaces as `kind: 'ext'` with that name; this claims it.
9
+ *
10
+ * It is an event of the assistant turn, not a `ChatMessage` of its own, because the agent folded it
11
+ * into the turn it was already running — there is no separate reply to hang off it.
12
+ */
13
+ import { registerExtEventRenderer } from '../ext-event-renderer-registry.js';
14
+ /** The ext-event name the daemon's mid-run `user_message` run event arrives under. */
15
+ export const MID_RUN_USER_MESSAGE_EXT_EVENT_NAME = 'user_message';
16
+ function field(data, key) {
17
+ const value = typeof data === 'object' && data !== null ? data[key] : undefined;
18
+ return typeof value === 'string' && value !== '' ? value : undefined;
19
+ }
20
+ /** One slot per message id, so each mid-run message renders where it arrived rather than folding into the first. */
21
+ export function midRunUserMessageSlotKey({ data }) {
22
+ return field(data, 'id');
23
+ }
24
+ /** The bubble for one mid-run message — the same classes as a sent user message, so it reads as one. */
25
+ export function MidRunUserMessage({ events }) {
26
+ const text = field(events.at(-1), 'text');
27
+ if (text === undefined)
28
+ return null;
29
+ return (_jsx("div", { className: "jini-message jini-message-user jini-message-user--mid-run", "data-agent-role": "region", "data-agent-label": "A message from the user, sent while the agent was working", children: _jsx("div", { className: "jini-message-content", children: text }) }));
30
+ }
31
+ /**
32
+ * Registers {@link MidRunUserMessage} against the ext-event registry.
33
+ *
34
+ * @returns An unregister handle, so a test or a hot reload can dispose cleanly.
35
+ */
36
+ export function registerMidRunUserMessageRenderer() {
37
+ return registerExtEventRenderer({ name: MID_RUN_USER_MESSAGE_EXT_EVENT_NAME, renderer: (props) => _jsx(MidRunUserMessage, { events: props.events }) }, { slotKey: midRunUserMessageSlotKey });
38
+ }
39
+ //# sourceMappingURL=MidRunUserMessage.js.map