@jini-ai/chat 0.5.6 → 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.
- package/dist/core/durable-projection.d.ts +3 -1
- package/dist/core/durable-projection.js +40 -7
- package/dist/core/index.d.ts +1 -0
- package/dist/core/index.js +1 -0
- package/dist/core/run-events/entry.d.ts +12 -1
- package/dist/core/run-events/entry.js +17 -6
- package/dist/core/tool-call-detail.d.ts +36 -0
- package/dist/core/tool-call-detail.js +49 -0
- package/dist/core/turn-usage.d.ts +11 -0
- package/dist/core/turn-usage.js +17 -0
- package/dist/react/chat-pane.d.ts +2 -2
- package/dist/react/chat-pane.js +1 -1
- package/dist/react/components/Composer.d.ts +2 -0
- package/dist/react/components/Composer.js +5 -2
- package/dist/react/components/Markdown.js +6 -6
- package/dist/react/components/MessageList.d.ts +7 -12
- package/dist/react/components/MessageList.js +20 -2
- package/dist/react/components/MessageRow.d.ts +6 -1
- package/dist/react/components/MessageRow.js +10 -4
- package/dist/react/components/MidRunUserMessage.d.ts +27 -0
- package/dist/react/components/MidRunUserMessage.js +39 -0
- package/dist/react/components/ToolCard.js +33 -22
- package/dist/react/features/chat-pane/components/ChatPane.d.ts +1 -1
- package/dist/react/features/chat-pane/components/ChatPane.js +11 -3
- package/dist/react/features/chat-pane/create-mid-run-message-poster.d.ts +44 -0
- package/dist/react/features/chat-pane/create-mid-run-message-poster.js +69 -0
- package/dist/react/features/chat-pane/hooks/useChatPane.hooks.d.ts +3 -1
- package/dist/react/features/chat-pane/hooks/useChatPane.hooks.js +55 -6
- package/dist/react/features/chat-pane/hooks/useChatPaneNewTabLinks.hooks.d.ts +25 -0
- package/dist/react/features/chat-pane/hooks/useChatPaneNewTabLinks.hooks.js +70 -0
- package/dist/react/features/chat-pane/index.d.ts +2 -1
- package/dist/react/features/chat-pane/index.js +1 -0
- package/dist/react/features/chat-pane/rules.d.ts +13 -0
- package/dist/react/features/chat-pane/rules.js +14 -0
- package/dist/react/features/chat-pane/styles.d.ts +1 -1
- package/dist/react/features/chat-pane/styles.js +7 -0
- package/dist/react/features/chat-pane/types.d.ts +23 -0
- package/dist/react/hooks/composer-draft-cache.d.ts +15 -0
- package/dist/react/hooks/composer-draft-cache.js +20 -0
- package/dist/react/hooks/useComposer.d.ts +14 -0
- package/dist/react/hooks/useComposer.js +78 -6
- package/dist/react/index.d.ts +4 -2
- package/dist/react/index.js +3 -1
- package/dist/server/durable-runs/recover.js +19 -11
- package/dist/server/run-finalizer/entry.js +2 -1
- package/dist/store/runtime/durable-run-store.js +5 -2
- package/dist/store/runtime/run-ledger.js +5 -2
- package/package.json +8 -8
|
@@ -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
|
-
},
|
|
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
|
-
|
|
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
|
|
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
|
|
33
|
-
|
|
34
|
-
|
|
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 },
|
|
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…' }];
|
package/dist/core/index.d.ts
CHANGED
|
@@ -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';
|
package/dist/core/index.js
CHANGED
|
@@ -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
|
-
},
|
|
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
|
|
443
|
-
*
|
|
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
|
-
|
|
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 },
|
|
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
|
package/dist/react/chat-pane.js
CHANGED
|
@@ -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,
|
|
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
|
|
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
|
-
//
|
|
393
|
-
//
|
|
394
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|