cortena-ui 1.6.0 → 1.8.0
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/CHANGELOG.md +122 -0
- package/README.md +104 -0
- package/dist/agent-chat/session.js +21 -7
- package/dist/agent-chat/session.js.map +1 -1
- package/dist/agent-chat/store.d.ts +55 -4
- package/dist/agent-chat/store.js +87 -22
- package/dist/agent-chat/store.js.map +1 -1
- package/dist/agent-chat/types.d.ts +11 -0
- package/dist/agent-chat/types.js.map +1 -1
- package/dist/agent-chat.d.ts +3 -3
- package/dist/components/agent-chat-popup.d.ts +2 -2
- package/dist/components/agent-chat-popup.js.map +1 -1
- package/dist/components/agent-chat.d.ts +169 -13
- package/dist/components/agent-chat.js +200 -27
- package/dist/components/agent-chat.js.map +1 -1
- package/dist/components/data-table/data-table.js +32 -4
- package/dist/components/data-table/data-table.js.map +1 -1
- package/dist/components/data-table/system-columns.js +22 -3
- package/dist/components/data-table/system-columns.js.map +1 -1
- package/dist/components/data-table/types.d.ts +42 -1
- package/dist/components/data-table/use-data-table.js +51 -11
- package/dist/components/data-table/use-data-table.js.map +1 -1
- package/package.json +1 -1
- package/src/agent-chat/session.ts +35 -5
- package/src/agent-chat/store.ts +193 -21
- package/src/agent-chat/types.ts +11 -0
- package/src/components/agent-chat-popup.tsx +3 -3
- package/src/components/agent-chat.tsx +576 -149
- package/src/components/data-table/data-table.tsx +62 -6
- package/src/components/data-table/system-columns.tsx +24 -1
- package/src/components/data-table/types.ts +42 -1
- package/src/components/data-table/use-data-table.ts +88 -10
- package/src/entries/agent-chat.ts +12 -0
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,128 @@
|
|
|
3
3
|
Notable changes per release. Versions before 1.6.0 are recorded in the git log
|
|
4
4
|
and in `../../CONSUMING.md`; this file starts where the changelog does.
|
|
5
5
|
|
|
6
|
+
## 1.8.0
|
|
7
|
+
|
|
8
|
+
### Added
|
|
9
|
+
|
|
10
|
+
- **A `DataTable` column can take the width the others leave** (DESIGN-77).
|
|
11
|
+
The grid is `table-fixed`, so a column that declared no `size` sat at
|
|
12
|
+
TanStack's 150 px default: on a list of one wide column and a few narrow
|
|
13
|
+
ones, the column people came to read was clipped while empty space sat to
|
|
14
|
+
its right. See "Column widths" in `README.md`.
|
|
15
|
+
- `meta: { fill: true }` marks that column. It is the one column drawn with
|
|
16
|
+
no fixed width, so the remainder lands on it instead of on the filler
|
|
17
|
+
cell; every other column keeps its `size`. One per table — the first
|
|
18
|
+
visible one wins, and a pinned column is not eligible: a pinned column is
|
|
19
|
+
placed at an offset measured off its neighbours' widths, so one with no
|
|
20
|
+
width of its own would misplace every pinned column beside it. Its own
|
|
21
|
+
`size` stays as a floor, since the table's `min-width` still counts it,
|
|
22
|
+
and it is not itself resizable: a column with no width of its own has no
|
|
23
|
+
edge to drag.
|
|
24
|
+
- A table with a fill column defaults `enableColumnResizing` to `true`. The
|
|
25
|
+
drag now has somewhere to go — the dragged column takes the width and the
|
|
26
|
+
fill column gives it up. An explicit `false` still wins. The default
|
|
27
|
+
commits the new width on release (`columnResizeMode: "onEnd"`), because a
|
|
28
|
+
table that did not ask to be resizable should not re-render every row on
|
|
29
|
+
every mouse move; setting `enableColumnResizing` yourself keeps the live
|
|
30
|
+
`"onChange"` drag.
|
|
31
|
+
- Cells carry their full text in the native `title`, so text the row
|
|
32
|
+
truncates is still reachable — on every row, independent of anything else
|
|
33
|
+
on it. A column that renders its own value gets it from that value; a
|
|
34
|
+
column with its own `cell` renderer draws a node the table will not guess
|
|
35
|
+
at, and says what the hover text is with
|
|
36
|
+
`meta: { hoverText: (row) => string | undefined }`. Headers get a title
|
|
37
|
+
only from `meta: { label }`; repeating a one-word header in a tooltip on
|
|
38
|
+
every `<th>` is noise, and the select and expand columns have no sentence
|
|
39
|
+
to repeat.
|
|
40
|
+
- In dev, a table with an unsized column and no fill column warns once,
|
|
41
|
+
naming the column.
|
|
42
|
+
|
|
43
|
+
### Fixed
|
|
44
|
+
|
|
45
|
+
- **The expander and the selection checkbox no longer fire the row's own
|
|
46
|
+
click** (DESIGN-79). On a table with both `onRowClick` and
|
|
47
|
+
`renderSubComponent`, opening a row's detail panel also navigated away from
|
|
48
|
+
it, and ticking the checkbox selected the row and then left the page. Both
|
|
49
|
+
controls now stop the event, by pointer and by keyboard, so a consumer no
|
|
50
|
+
longer has to recognise the click target itself.
|
|
51
|
+
|
|
52
|
+
## 1.7.0
|
|
53
|
+
|
|
54
|
+
### Added
|
|
55
|
+
|
|
56
|
+
- **`AgentChat` is a host-able surface** (PLATFORM-64). cortenaweb adopts the
|
|
57
|
+
shared component instead of running a second chat, and the pieces its chat
|
|
58
|
+
has that an extension's does not — a Shiki/KaTeX bubble with feedback
|
|
59
|
+
controls, a composer with attachments and agent/model selectors, a reading
|
|
60
|
+
column, the MCP App host — go in through props rather than a fork:
|
|
61
|
+
- `chat: UseAgentChatResult` — the host holds the state and the surface
|
|
62
|
+
draws it. A sidebar, a shortcut or a page that routes on the session key
|
|
63
|
+
can then read the same state the surface does. `AgentChatProps` is now
|
|
64
|
+
`AgentChatOwnedProps | AgentChatHostedProps`: with `chat` there is no
|
|
65
|
+
`client` and no `storageScope`, because the host owns both. The result
|
|
66
|
+
may come from `useAgentChat` or from a store of the host's projected onto
|
|
67
|
+
the same shape.
|
|
68
|
+
- `components?: Partial<AgentChatComponents>` — `Message`, `Streaming`,
|
|
69
|
+
`Empty`, `LoadOlder`, `Tools`, `Composer`; each replaces its region whole.
|
|
70
|
+
`Tools` receives the host's `renderToolCall` as passed, so a strip of the
|
|
71
|
+
host's own still mounts an MCP App through the one seam.
|
|
72
|
+
- `classNames?: AgentChatClassNames` for the wrappers (`header`, `scroll`,
|
|
73
|
+
`transcript`, `sentinel`, `tools`, `composer`); `transcript` replaces the
|
|
74
|
+
default spacing rather than merging with it. `hideHeader` for a host with
|
|
75
|
+
its own session list.
|
|
76
|
+
- `useAgentChat` gains `sendMcpAppAction`, `clearSession` (leave without
|
|
77
|
+
minting; the next send mints), `regenerate(messageId)`, `send(text,
|
|
78
|
+
attachments, { model, thinking })`, and options `mintSessionKey`,
|
|
79
|
+
`restoreSession` and `listSessions`. `AgentChatMessage.attachmentItems`
|
|
80
|
+
(`unknown[]`, the host's own shape) is set on the user bubble a send adds;
|
|
81
|
+
`attachments` keeps its `string[]` shape.
|
|
82
|
+
- `autoLoadOlder`: the transcript pages older history from a sentinel at the
|
|
83
|
+
top as the reader reaches it, as well as from the button. Off by default —
|
|
84
|
+
`AgentChatPopup` pages on a click — and armed only once the scroller
|
|
85
|
+
overflows, so a short transcript does not pull every page the server has.
|
|
86
|
+
Whatever asked for the page, the reader's place is held across the prepend
|
|
87
|
+
(`overflow-anchor: none` on the scroller; the surface owns the rule in every
|
|
88
|
+
browser), and a new message is followed smoothly when the reader was at the
|
|
89
|
+
bottom.
|
|
90
|
+
- `useAgentChat().loadOlder` returns `true` when a load was started and
|
|
91
|
+
`false` when it declined (no session, one in flight, no more), so a caller
|
|
92
|
+
saving scroll position knows whether a prepend is coming.
|
|
93
|
+
|
|
94
|
+
### Changed (type-level)
|
|
95
|
+
|
|
96
|
+
- `AgentChatProps` is now the union `AgentChatOwnedProps | AgentChatHostedProps`.
|
|
97
|
+
A wrapper that did `interface MyProps extends AgentChatProps` no longer
|
|
98
|
+
compiles — an interface cannot extend a union. Extend `AgentChatOwnedProps`
|
|
99
|
+
(you pass `client` and `storageScope`) or `AgentChatHostedProps` (you pass
|
|
100
|
+
`chat`) instead, as `AgentChatPopupProps` now does.
|
|
101
|
+
- `UseAgentChatResult.loadOlder` is `() => boolean`, was `() => void`. A store
|
|
102
|
+
projected onto the shape returns whether it started a load.
|
|
103
|
+
- The transcript wrapper (`role="log"`) gains `relative`, and a new first child
|
|
104
|
+
`data-slot="agent-chat-sentinel"` (`absolute inset-x-0 top-0 h-px` by
|
|
105
|
+
default, or `classNames.sentinel`). A selector on the transcript's first
|
|
106
|
+
child, or a test counting its children, sees one more element.
|
|
107
|
+
- `AgentChatEmptySlotProps` is unchanged, but a host's `Empty` no longer holds
|
|
108
|
+
the region while `chat.error` is set: the transcript mounts with the error
|
|
109
|
+
banner in it instead.
|
|
110
|
+
|
|
111
|
+
### Fixed
|
|
112
|
+
|
|
113
|
+
- **`regenerate` asks the question once.** The user turn being asked again is
|
|
114
|
+
moved onto the new run rather than drawn a second time, so the transcript
|
|
115
|
+
reads Q then A2, not Q, Q, A2.
|
|
116
|
+
- **A stale older page cannot land under another session.** `history/older`
|
|
117
|
+
is dropped for a key that is no longer the current one, the same rule
|
|
118
|
+
`history/loaded` follows.
|
|
119
|
+
- **A reopened chat brings back the surfaces the agent drew.**
|
|
120
|
+
`parseHistoryMessages` reads `a2ui` content parts, folds them across a turn,
|
|
121
|
+
and strips the injected `[Dow YYYY-MM-DD HH:MM TZ]` stamp from a user turn —
|
|
122
|
+
the same three rules `cortena-shared` applies, ported.
|
|
123
|
+
- **A stale history reply cannot change the session.** `history/loaded` for a
|
|
124
|
+
key that is no longer the current one — including `null`, after
|
|
125
|
+
`clearSession` — is dropped whole. Two quick resumes used to land the first
|
|
126
|
+
transcript under the second key.
|
|
127
|
+
|
|
6
128
|
## 1.6.0
|
|
7
129
|
|
|
8
130
|
### Added
|
package/README.md
CHANGED
|
@@ -318,6 +318,110 @@ returns replaces the default card.
|
|
|
318
318
|
/>
|
|
319
319
|
```
|
|
320
320
|
|
|
321
|
+
### Hosting the surface
|
|
322
|
+
|
|
323
|
+
`AgentChatPopup` owns its state. A host that already has state of its own —
|
|
324
|
+
cortenaweb's shell reads the current session from a store the sidebar, the
|
|
325
|
+
keyboard shortcuts and the Canvas panel all share — mounts `AgentChat` with
|
|
326
|
+
`chat` instead of `client`, and no `storageScope`:
|
|
327
|
+
|
|
328
|
+
```tsx
|
|
329
|
+
<AgentChat
|
|
330
|
+
chat={chat} // a UseAgentChatResult: from useAgentChat, or a store projected onto it
|
|
331
|
+
agentName="Cortena"
|
|
332
|
+
hideHeader // the host has its own session list
|
|
333
|
+
components={{ Message, Streaming, Empty, LoadOlder, Tools, Composer }}
|
|
334
|
+
classNames={{ scroll: "chat-scroll", transcript: "chat-column gap-6 py-6" }}
|
|
335
|
+
renderToolCall={renderMcpApp} // the host's MCP App frame, through the one seam
|
|
336
|
+
/>
|
|
337
|
+
```
|
|
338
|
+
|
|
339
|
+
What stays shared is the surface: the scroller that keeps to the bottom, the
|
|
340
|
+
pager that holds the reader's place across a prepend (and, with
|
|
341
|
+
`autoLoadOlder`, asks for the next page from a sentinel at the top once the
|
|
342
|
+
transcript overflows), the order of the regions, the progress steps and the
|
|
343
|
+
tool-card seam. What each
|
|
344
|
+
region looks like is the host's to replace, one component per region; a
|
|
345
|
+
component given in `components` replaces its region whole, wrapper included.
|
|
346
|
+
`Tools` is handed `renderToolCall` as it was passed, so a strip of the host's
|
|
347
|
+
own still mounts an MCP App through the seam. `Streaming` is handed `steps`
|
|
348
|
+
because the default draws them inside the live bubble; a host's bubble draws
|
|
349
|
+
them itself. `classNames.transcript` replaces the default gutter rather than
|
|
350
|
+
merging with it, so a reading column owns its own spacing.
|
|
351
|
+
|
|
352
|
+
The hook has the matching options: `restoreSession: false` for a host whose URL
|
|
353
|
+
names the session (the key is still written, so one window stays on one
|
|
354
|
+
session), `listSessions: false` for a host with its own session list, and
|
|
355
|
+
`mintSessionKey` for a host that binds sessions some other way.
|
|
356
|
+
|
|
357
|
+
## Column widths
|
|
358
|
+
|
|
359
|
+
`DataTable` lays the grid out `table-fixed`: every column is drawn at its
|
|
360
|
+
`size`, and a column that never declared one sits at TanStack's 150 px
|
|
361
|
+
default. A list of one wide column and a few narrow ones — a title beside an
|
|
362
|
+
id, a status, an assignee — then clips the column people came to read while
|
|
363
|
+
empty space sits to its right. Three rules keep a list readable.
|
|
364
|
+
|
|
365
|
+
**One fill column: the column people scan the list by.** Mark it
|
|
366
|
+
`meta: { fill: true }` and give every other column a `size`. The fill column
|
|
367
|
+
is the one drawn with no fixed width, so the remainder lands on it. Its own
|
|
368
|
+
`size` stays as a floor — the table's `min-width` still counts it, so a narrow
|
|
369
|
+
window scrolls rather than squeezing the column to nothing — and it is not
|
|
370
|
+
itself resizable, because a column with no width has no edge to drag. **A fill
|
|
371
|
+
column cannot be pinned**: a pinned column is placed at an offset measured off
|
|
372
|
+
the widths of the columns beside it, so one with no width of its own would put
|
|
373
|
+
every pinned neighbour in the wrong place. Pin it and the table skips it and
|
|
374
|
+
falls back to the filler cell.
|
|
375
|
+
|
|
376
|
+
A table with a fill column turns `enableColumnResizing` on by default:
|
|
377
|
+
dragging any other column's edge now has somewhere to go, and the fill column
|
|
378
|
+
absorbs the difference. That default resizes on release (`onEnd`), since a
|
|
379
|
+
table that never asked to be resizable should not re-render every row on every
|
|
380
|
+
mouse move; set `enableColumnResizing` yourself and the drag is live
|
|
381
|
+
(`onChange`). Pass `enableColumnResizing={false}` to turn it off.
|
|
382
|
+
|
|
383
|
+
```tsx
|
|
384
|
+
const columns = helper.columns([
|
|
385
|
+
helper.accessor("id", { header: "ID", size: 110 }),
|
|
386
|
+
helper.accessor("title", { header: "Title", size: 240, meta: { fill: true } }),
|
|
387
|
+
helper.accessor("status", { header: "Status", size: 130, meta: { filter: "faceted" } }),
|
|
388
|
+
helper.accessor("assignee", { header: "Assignee", size: 200 }),
|
|
389
|
+
]);
|
|
390
|
+
```
|
|
391
|
+
|
|
392
|
+
In dev, a table with an unsized column and no fill column warns once, naming
|
|
393
|
+
the column. It is the one layout mistake that produces no error and no
|
|
394
|
+
warning from anything else — just clipped text.
|
|
395
|
+
|
|
396
|
+
**Cells truncate, never wrap, and carry their full text on hover.** Rows keep
|
|
397
|
+
one height: a list whose rows grow to fit their longest cell is no longer
|
|
398
|
+
scannable, and there is no wrapping option. Truncated text has to stay
|
|
399
|
+
reachable, so a column that renders its own value puts that value in the
|
|
400
|
+
cell's `title` automatically — every row of it, whatever else is or is not on
|
|
401
|
+
that row. A column with its own `cell` renderer draws a node the table cannot
|
|
402
|
+
read; say what the hover text is with
|
|
403
|
+
`meta: { hoverText: (row) => row.title }`, or leave it with none. It is the
|
|
404
|
+
native `title`, not a tooltip component — a tooltip on every cell of a
|
|
405
|
+
hundred-row grid is a hundred listeners and a lot of noise.
|
|
406
|
+
|
|
407
|
+
Headers are not cells and get no automatic title: a header is one short phrase
|
|
408
|
+
the consumer chose, and repeating it in a tooltip on every `<th>` is noise. A
|
|
409
|
+
column that wants one sets `meta: { label: "…" }`, which is the same label the
|
|
410
|
+
view menu and the exports use.
|
|
411
|
+
|
|
412
|
+
**Long text belongs in an expandable row.** A description, an error body, a
|
|
413
|
+
payload: none of them fit on a row, and widening the column does not make them
|
|
414
|
+
fit. Pass `renderSubComponent` and the table adds an expander column; the
|
|
415
|
+
detail panel opens beneath the row, full width.
|
|
416
|
+
|
|
417
|
+
```tsx
|
|
418
|
+
<DataTable
|
|
419
|
+
columns={columns}
|
|
420
|
+
dataSource={{ kind: "client", rows }}
|
|
421
|
+
renderSubComponent={(row) => <Markdown>{row.original.description}</Markdown>}
|
|
422
|
+
/>
|
|
423
|
+
```
|
|
424
|
+
|
|
321
425
|
## Charts in tests
|
|
322
426
|
|
|
323
427
|
Both chart engines size themselves from the parent box, and under jsdom every
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
"use client";
|
|
2
|
+
import { extractA2UIBlocks, foldA2UIBlocks } from "./a2ui-block.js";
|
|
2
3
|
import { truncate } from "./step-label.js";
|
|
3
4
|
//#region src/agent-chat/session.ts
|
|
4
5
|
/**
|
|
@@ -219,11 +220,19 @@ function isToolRelatedMessage(msg) {
|
|
|
219
220
|
return false;
|
|
220
221
|
}
|
|
221
222
|
/**
|
|
222
|
-
*
|
|
223
|
-
*
|
|
223
|
+
* A leading `[Dow YYYY-MM-DD HH:MM TZ] ` stamp, as the runtime injects it
|
|
224
|
+
* ahead of every message the agent is given. The date is what makes it safe
|
|
225
|
+
* to remove: a user who opens with "[draft] ship it" keeps their bracket.
|
|
226
|
+
* Same expression as `cortena-shared/src/stores/use-chat.ts`; keep in step.
|
|
227
|
+
*/
|
|
228
|
+
const INJECTED_TIMESTAMP_PREFIX = /^\[[^\]]*\d{4}-\d{2}-\d{2} \d{2}:\d{2}[^\]]*\]\s*/;
|
|
229
|
+
/**
|
|
230
|
+
* Strip the envelope the runtime prepends to a user turn: `System: [timestamp] …`
|
|
231
|
+
* lines and `[System Message]` prefixes carrying agent context, and the
|
|
232
|
+
* injected timestamp ahead of the words themselves. None of it is user text.
|
|
224
233
|
*/
|
|
225
234
|
function stripSystemPrefixes(text) {
|
|
226
|
-
return text.split("\n").filter((line) => !/^System:\s*\[/.test(line) && !line.startsWith("[System Message]")).join("\n").replace(/^\n+/, "");
|
|
235
|
+
return text.split("\n").filter((line) => !/^System:\s*\[/.test(line) && !line.startsWith("[System Message]")).join("\n").replace(/^\n+/, "").replace(INJECTED_TIMESTAMP_PREFIX, "");
|
|
227
236
|
}
|
|
228
237
|
/** Lift `<thinking>…</thinking>` out of plain text. */
|
|
229
238
|
function separateThinking(text) {
|
|
@@ -239,6 +248,7 @@ function separateThinking(text) {
|
|
|
239
248
|
function parseOneHistoryMessage(raw) {
|
|
240
249
|
const msg = raw;
|
|
241
250
|
const content = msg.content;
|
|
251
|
+
const a2ui = Array.isArray(content) ? extractA2UIBlocks({ content }) : [];
|
|
242
252
|
let text = Array.isArray(content) ? content.filter((c) => c.type === "text" && c.text).map((c) => c.text).join("") : typeof msg.content === "string" ? msg.content : typeof msg.text === "string" ? msg.text : "";
|
|
243
253
|
const isToolMessage = isToolRelatedMessage(msg);
|
|
244
254
|
const role = msg.role === "user" ? "user" : "assistant";
|
|
@@ -254,7 +264,8 @@ function parseOneHistoryMessage(raw) {
|
|
|
254
264
|
text: mainText,
|
|
255
265
|
timestamp: msg.timestamp ?? Date.now(),
|
|
256
266
|
thinkingText: tagThinking,
|
|
257
|
-
...isToolMessage ? { isToolMessage } : {}
|
|
267
|
+
...isToolMessage ? { isToolMessage } : {},
|
|
268
|
+
...a2ui.length > 0 ? { a2ui } : {}
|
|
258
269
|
};
|
|
259
270
|
}
|
|
260
271
|
return {
|
|
@@ -263,7 +274,8 @@ function parseOneHistoryMessage(raw) {
|
|
|
263
274
|
text,
|
|
264
275
|
timestamp: msg.timestamp ?? Date.now(),
|
|
265
276
|
...thinkingText ? { thinkingText } : {},
|
|
266
|
-
...isToolMessage ? { isToolMessage } : {}
|
|
277
|
+
...isToolMessage ? { isToolMessage } : {},
|
|
278
|
+
...a2ui.length > 0 ? { a2ui } : {}
|
|
267
279
|
};
|
|
268
280
|
}
|
|
269
281
|
/**
|
|
@@ -297,9 +309,11 @@ function mergeConsecutiveAssistantMessages(messages) {
|
|
|
297
309
|
if (entry.text.trim()) cot.push(entry.text.trim());
|
|
298
310
|
}
|
|
299
311
|
const combined = cot.filter(Boolean).join("\n\n");
|
|
312
|
+
const blocks = foldA2UIBlocks(run.flatMap((entry) => entry.a2ui ?? []));
|
|
300
313
|
result.push({
|
|
301
314
|
...visible,
|
|
302
|
-
...combined ? { thinkingText: combined } : {}
|
|
315
|
+
...combined ? { thinkingText: combined } : {},
|
|
316
|
+
...blocks.length > 0 ? { a2ui: blocks } : {}
|
|
303
317
|
});
|
|
304
318
|
run = [];
|
|
305
319
|
};
|
|
@@ -381,7 +395,7 @@ function isHiddenMessage(message) {
|
|
|
381
395
|
if (message.isError) return false;
|
|
382
396
|
if (message.isToolMessage) return true;
|
|
383
397
|
if (message.role === "assistant" && !message.text.trim() && !message.thinkingText && !message.a2ui?.length) return true;
|
|
384
|
-
if (message.role === "user") return !message.text.trim() && !message.attachments?.length;
|
|
398
|
+
if (message.role === "user") return !message.text.trim() && !message.attachments?.length && !message.attachmentItems?.length;
|
|
385
399
|
const text = message.text;
|
|
386
400
|
return isToolResult(text) || isSkillContent(text) || isAgentDiagnostics(text) || isToolOutput(text);
|
|
387
401
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"session.js","names":[],"sources":["../../src/agent-chat/session.ts"],"sourcesContent":["/**\n * Sessions, history and the filter chain.\n *\n * Ported from `cortena-shared/src/stores/use-chat.ts` and\n * `cortenaweb/components/chat/message-bubble.tsx` (Cortena monorepo).\n * `cortena-shared` is not published, so the rules the two transports share\n * live here; keep them in step.\n *\n * The one deliberate difference is where the current session key is kept.\n * cortenaweb puts it in `localStorage`, which is shared by every tab of an\n * origin, so a second window adopted the first window's session: it inherited\n * a stream that was not its own and both windows then showed the same content.\n * Sessions here are **per window** — the key lives in `sessionStorage`, under a\n * name scoped to the extension — so opening the extension twice starts two\n * sessions. Both stay listed and either window can resume either one; two\n * windows on one session may both send, and cortenacore serialises the runs.\n */\n\nimport { truncate } from \"./step-label\";\nimport type { AgentChatMessage, AgentChatStep } from \"./types\";\n\n/* ── minting and remembering a key ───────────────────────────────────────── */\n\n/**\n * A new session key, minted client-side.\n *\n * `agent:<agentId>:new-<Date.now()>-<random36>`. cortenacore reads the\n * `agent:<id>:` prefix to bind the session to that agent's template, and the\n * key is the AG-UI `threadId` verbatim, prefix included. Minting it rather than\n * asking for one is what lets the first message go out immediately.\n */\nexport function mintSessionKey(agentId: string): string {\n return `agent:${agentId}:new-${Date.now()}-${Math.random().toString(36).slice(2, 7)}`;\n}\n\n/** Was this key minted locally for a chat that had no session yet? */\nexport function isProvisionalSessionKey(key: string): boolean {\n return /(^|:)new-\\d+-[a-z0-9]+$/.test(key);\n}\n\n/** Does this session belong to this agent? `sessions.list` is filtered by it. */\nexport function isSessionOfAgent(key: string, agentId: string): boolean {\n return key.startsWith(`agent:${agentId}:`);\n}\n\n/** The `sessionStorage` name the current key is kept under, per extension. */\nexport function sessionStorageKey(scope: string): string {\n return `cortena.agent-chat.${scope}.session`;\n}\n\n/** The `sessionStorage` name the open/collapsed state is kept under. */\nexport function viewStorageKey(scope: string): string {\n return `cortena.agent-chat.${scope}.view`;\n}\n\n/** The `sessionStorage` name this window's progress steps are kept under. */\nexport function stepsStorageKey(scope: string): string {\n return `cortena.agent-chat.${scope}.steps`;\n}\n\n/**\n * `sessionStorage`, or nothing.\n *\n * Absent during SSR and refused outright when a browser blocks storage for the\n * origin, and neither is a reason for the chat not to work: without it a window\n * simply forgets its session on reload.\n */\nexport function windowSessionStorage(): Storage | null {\n try {\n return typeof window === \"undefined\" ? null : window.sessionStorage;\n } catch {\n return null;\n }\n}\n\nexport function readStoredSessionKey(scope: string): string | null {\n try {\n return windowSessionStorage()?.getItem(sessionStorageKey(scope)) ?? null;\n } catch {\n return null;\n }\n}\n\nexport function writeStoredSessionKey(scope: string, key: string | null): void {\n try {\n const storage = windowSessionStorage();\n if (!storage) {\n return;\n }\n if (key === null) {\n storage.removeItem(sessionStorageKey(scope));\n } else {\n storage.setItem(sessionStorageKey(scope), key);\n }\n } catch {\n // A blocked storage is not a reason to lose the run in flight.\n }\n}\n\n/* ── the progress steps of the turn ──────────────────────────────────────── */\n\n/** Most steps kept, in memory and in `sessionStorage`. A turn that long is a runaway loop. */\nexport const STEP_LIMIT = 50;\n\n/**\n * Most bytes the steps slot may occupy.\n *\n * `sessionStorage` is a few megabytes for the whole origin, shared with\n * everything else the host keeps there, and a step count alone does not bound\n * the size: fifty steps whose labels are all near the limit, with a failure\n * message on each, is a slot nobody budgeted for. Over the cap the OLDEST are\n * dropped, one at a time, because the recent ones are the ones a reload has to\n * bring back.\n */\nconst STEP_BYTE_LIMIT = 64 * 1024;\n\n/** Longest tool call id or tool name kept, in storage and on `data-tool`. */\nexport const STORED_TEXT_LIMIT = 80;\n\n/**\n * The steps of the current turn, and the session they belong to.\n *\n * They go in the per-window store beside the session key, for the same reason\n * the key does: a tab is reloaded mid-run, or a host remounts the surface on a\n * route change, and both threw the whole strip away. (Collapsing the pop-up is\n * NOT one of them — the panel is `hidden`, not unmounted, and the run streams\n * into it either way.) Messages come back from `chat.history`; steps do not\n * exist on the server, so this is the only place they can come back from.\n *\n * Scoped to a session key, and checked on read: resuming a different session\n * must not inherit the last one's steps.\n */\nexport function readStoredSteps(scope: string, sessionKey: string): AgentChatStep[] {\n try {\n const raw = windowSessionStorage()?.getItem(stepsStorageKey(scope));\n if (!raw) {\n return [];\n }\n const parsed = JSON.parse(raw) as { sessionKey?: unknown; steps?: unknown };\n if (parsed?.sessionKey !== sessionKey || !Array.isArray(parsed.steps)) {\n return [];\n }\n return (parsed.steps as AgentChatStep[])\n .filter((step) => typeof step?.id === \"string\" && typeof step?.label === \"string\")\n .map(restoreStep)\n .slice(-STEP_LIMIT);\n } catch {\n // Storage refused, or somebody else wrote the slot. Neither is worth a\n // broken chat; the strip simply starts empty.\n return [];\n }\n}\n\n/** Every status a step may legitimately come back with. */\nconst STEP_STATUSES: ReadonlySet<string> = new Set([\n \"running\",\n \"done\",\n \"error\",\n \"stopped\",\n \"paused\",\n]);\n\n/**\n * A step as it comes back from storage.\n *\n * The important cases are `running` and `paused`. Neither is true any more:\n * the run that owned the call ended when the surface went away — a reload, a\n * remount on a route change; a collapsed pop-up only HIDES the strip and never\n * reaches this path — and no result will arrive for it now, so a restored\n * spinner spins until the user gives up and reloads again, which restores it.\n *\n * It comes back `stopped`, with no message. Neutral is the honest reading:\n * nothing failed, the work simply did not continue, and \"Interrupted.\" drawn in\n * the failure colour told the user their call had broken when the page had.\n *\n * An unrecognised status is treated the same way. Nothing else writes this\n * slot today, but it is per-origin `sessionStorage` and this is the only place\n * that decides what a strip is allowed to render.\n */\nfunction restoreStep(step: AgentChatStep): AgentChatStep {\n const known = STEP_STATUSES.has(step.status);\n if (known && step.status !== \"running\" && step.status !== \"paused\") {\n return step;\n }\n const restored: AgentChatStep = {\n ...step,\n status: \"stopped\",\n endedAt: step.endedAt ?? Date.now(),\n };\n delete restored.errorMessage;\n return restored;\n}\n\n/**\n * The fields of a step that are safe to keep, and the only ones kept.\n *\n * `args` is deliberately absent. A tool call's arguments are the user's data —\n * what they searched for, whose record they opened, the body of what they\n * wrote — and `sessionStorage` is readable by every script on the origin,\n * survives the run, and is the sort of thing that ends up in a support bundle.\n * Nothing on screen needs them after the run: the LABEL is already derived\n * from them, and the label is what a restored strip shows.\n */\nfunction storedStep(step: AgentChatStep): AgentChatStep {\n return {\n // Bounded, both of them. Neither is written by this package: the id is the\n // producer's `toolCallId` and the name is whatever the model called, so a\n // single call carrying 9 kB of either filled the byte cap on its own and\n // pushed every real step out of the slot.\n id: truncate(step.id, STORED_TEXT_LIMIT),\n toolName: truncate(step.toolName, STORED_TEXT_LIMIT),\n label: step.label,\n status: step.status,\n startedAt: step.startedAt,\n ...(step.endedAt === undefined ? {} : { endedAt: step.endedAt }),\n ...(step.errorMessage === undefined ? {} : { errorMessage: step.errorMessage }),\n };\n}\n\n/** The slot's contents, projected and trimmed to fit the byte cap. */\nexport function serialiseSteps(sessionKey: string, steps: readonly AgentChatStep[]): string {\n let kept = steps.slice(-STEP_LIMIT).map(storedStep);\n let json = JSON.stringify({ sessionKey, steps: kept });\n while (kept.length > 1 && json.length > STEP_BYTE_LIMIT) {\n kept = kept.slice(1);\n json = JSON.stringify({ sessionKey, steps: kept });\n }\n return json;\n}\n\nexport function writeStoredSteps(\n scope: string,\n sessionKey: string | null,\n steps: readonly AgentChatStep[],\n): void {\n try {\n const storage = windowSessionStorage();\n if (!storage) {\n return;\n }\n if (!sessionKey || steps.length === 0) {\n storage.removeItem(stepsStorageKey(scope));\n return;\n }\n storage.setItem(stepsStorageKey(scope), serialiseSteps(sessionKey, steps));\n } catch {\n // Blocked, or over quota. The run in flight is not worth losing over it.\n }\n}\n\n/* ── history ─────────────────────────────────────────────────────────────── */\n\nfunction generateId(): string {\n return `msg-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`;\n}\n\nfunction isToolRelatedMessage(msg: Record<string, unknown>): boolean {\n const role = typeof msg.role === \"string\" ? msg.role.toLowerCase() : \"\";\n if (role === \"tool\" || role === \"toolresult\" || role === \"tool_result\") {\n return true;\n }\n const content = msg.content as Array<{ type: string }> | undefined;\n if (Array.isArray(content) && content.length > 0) {\n const toolTypes = new Set([\n \"tool_use\",\n \"tool_call\",\n \"tool_result\",\n \"toolresult\",\n \"server_tool_use\",\n ]);\n const hasText = content.some((c) => c.type === \"text\" || c.type === \"thinking\");\n const hasToolBlocks = content.some((c) => toolTypes.has(c.type));\n if (hasToolBlocks && !hasText) {\n return true;\n }\n }\n return false;\n}\n\n/**\n * Strip `System: [timestamp] …` lines and `[System Message]` prefixes the\n * runtime prepends to a user turn as agent context. They are not user text.\n */\nfunction stripSystemPrefixes(text: string): string {\n return text\n .split(\"\\n\")\n .filter((line) => !/^System:\\s*\\[/.test(line) && !line.startsWith(\"[System Message]\"))\n .join(\"\\n\")\n .replace(/^\\n+/, \"\");\n}\n\n/** Lift `<thinking>…</thinking>` out of plain text. */\nexport function separateThinking(text: string): { thinkingText?: string; mainText: string } {\n const match = text.match(/<thinking>([\\s\\S]*?)<\\/thinking>/);\n if (!match?.[1]) {\n return { mainText: text };\n }\n const thinkingText = match[1].trim();\n const mainText = text.replace(/<thinking>[\\s\\S]*?<\\/thinking>\\s*/, \"\").trim();\n return { ...(thinkingText ? { thinkingText } : {}), mainText };\n}\n\nfunction parseOneHistoryMessage(raw: unknown): AgentChatMessage {\n const msg = raw as Record<string, unknown>;\n const content = msg.content as\n | Array<{ type: string; text?: string; thinking?: string; a2ui?: unknown }>\n | undefined;\n let text = Array.isArray(content)\n ? content\n .filter((c) => c.type === \"text\" && c.text)\n .map((c) => c.text!)\n .join(\"\")\n : typeof msg.content === \"string\"\n ? msg.content\n : typeof msg.text === \"string\"\n ? msg.text\n : \"\";\n\n const isToolMessage = isToolRelatedMessage(msg);\n const role = ((msg.role as string) === \"user\" ? \"user\" : \"assistant\") as \"user\" | \"assistant\";\n if (role === \"user\") {\n text = stripSystemPrefixes(text);\n }\n\n let thinkingText = \"\";\n if (typeof msg.thinking === \"string\" && msg.thinking.trim()) {\n thinkingText = msg.thinking.trim();\n } else if (Array.isArray(content)) {\n thinkingText = content\n .filter((c) => c.type === \"thinking\")\n .map((c) => c.thinking ?? c.text ?? \"\")\n .filter(Boolean)\n .join(\"\\n\\n\");\n }\n if (!thinkingText) {\n const { thinkingText: tagThinking, mainText } = separateThinking(text);\n if (tagThinking) {\n return {\n id: typeof msg.id === \"string\" ? msg.id : generateId(),\n role,\n text: mainText,\n timestamp: (msg.timestamp as number) ?? Date.now(),\n thinkingText: tagThinking,\n ...(isToolMessage ? { isToolMessage } : {}),\n };\n }\n }\n\n return {\n id: typeof msg.id === \"string\" ? msg.id : generateId(),\n role,\n text,\n timestamp: (msg.timestamp as number) ?? Date.now(),\n ...(thinkingText ? { thinkingText } : {}),\n ...(isToolMessage ? { isToolMessage } : {}),\n };\n}\n\n/**\n * Merge an agent turn — assistant messages plus tool call and result messages —\n * into one visible message. Everything except the final substantive response is\n * folded into the chain of thought. Only a genuine user message breaks the turn.\n */\nfunction mergeConsecutiveAssistantMessages(messages: AgentChatMessage[]): AgentChatMessage[] {\n const result: AgentChatMessage[] = [];\n let run: AgentChatMessage[] = [];\n\n const flush = () => {\n const first = run[0];\n if (!first) {\n return;\n }\n if (run.length === 1) {\n result.push(first);\n run = [];\n return;\n }\n let visibleIdx = run.length - 1;\n while (visibleIdx > 0 && !(run[visibleIdx]?.text ?? \"\").trim()) {\n visibleIdx--;\n }\n const visible = run[visibleIdx] ?? first;\n const cot: string[] = [];\n for (let i = 0; i < run.length; i++) {\n const entry = run[i];\n if (!entry) {\n continue;\n }\n if (i === visibleIdx) {\n if (visible.thinkingText) {\n cot.push(visible.thinkingText);\n }\n continue;\n }\n if (entry.thinkingText) {\n cot.push(entry.thinkingText);\n }\n if (entry.text.trim()) {\n cot.push(entry.text.trim());\n }\n }\n const combined = cot.filter(Boolean).join(\"\\n\\n\");\n result.push({ ...visible, ...(combined ? { thinkingText: combined } : {}) });\n run = [];\n };\n\n for (const msg of messages) {\n const isRealUser = msg.role === \"user\" && !msg.isToolMessage && msg.text.trim().length > 0;\n if (isRealUser) {\n flush();\n result.push(msg);\n } else {\n run.push(msg);\n }\n }\n flush();\n return result;\n}\n\n/** Raw transcript records to the messages the list renders. */\nexport function parseHistoryMessages(raw: readonly unknown[]): AgentChatMessage[] {\n return mergeConsecutiveAssistantMessages(raw.map(parseOneHistoryMessage));\n}\n\n/**\n * The history shrink guard.\n *\n * A history load must not shrink the session already on screen. The first\n * message of a new chat loads history while the turn is still in flight and the\n * user's message has not been persisted yet; replacing wholesale threw that\n * message away, and the reply survived, so the symptom was a conversation with\n * the answer but not the question.\n *\n * Only the same session, and only against losing messages: a longer or equal\n * history still replaces, so an edit, a deletion made elsewhere and a compaction\n * all land normally.\n */\nexport function wouldShrinkHistory(params: {\n currentSessionKey: string | null;\n loadedSessionKey: string;\n currentCount: number;\n loadedCount: number;\n}): boolean {\n return (\n params.currentSessionKey === params.loadedSessionKey &&\n params.loadedCount < params.currentCount\n );\n}\n\n/* ── the filter chain ────────────────────────────────────────────────────── */\n\n/** A JSON blob a tool or an extension API returned, echoed as a message. */\nfunction isToolResult(text: string): boolean {\n const trimmed = text.trim();\n const wrapped =\n (trimmed.startsWith(\"[\") && trimmed.endsWith(\"]\")) ||\n (trimmed.startsWith(\"{\") && trimmed.endsWith(\"}\"));\n if (!wrapped) {\n return false;\n }\n try {\n const parsed = JSON.parse(trimmed);\n return typeof parsed === \"object\" && parsed !== null;\n } catch {\n return false;\n }\n}\n\n/** Raw skill-file content the model echoed instead of acting on. */\nfunction isSkillContent(text: string): boolean {\n const trimmed = text.trim();\n return (\n /^name:\\s*\\S+.*\\bdescription:.*\\btype:\\s*skill\\b/i.test(trimmed) ||\n /^---\\s*\\n[\\s\\S]*?\\btype:\\s*skill\\b[\\s\\S]*?\\n---/m.test(trimmed)\n );\n}\n\n/** Runtime diagnostics: token counters, a leaked api-key hint. */\nfunction isAgentDiagnostics(text: string): boolean {\n const trimmed = text.trim();\n return /Tokens:\\s*\\d+.*Cache:.*Context:/s.test(trimmed) || /\\bapi-key\\s+sk-/.test(trimmed);\n}\n\n/** Tool execution output that leaked into history as its own message. */\nfunction isToolOutput(text: string): boolean {\n const trimmed = text.trim();\n if (!trimmed) {\n return false;\n }\n if (/^Tool\\s+\\S+\\s+not found$/i.test(trimmed)) {\n return true;\n }\n if (trimmed === \"(no output)\") {\n return true;\n }\n if (/^total\\s+\\d+\\s+[d-][rwx-]{9}/.test(trimmed)) {\n return true;\n }\n if (/^[d-][rwx-]{9}[@+]?\\s+\\d+\\s+\\S+\\s+\\S+/.test(trimmed)) {\n return true;\n }\n if (/(?:^|\\n)[A-Z_]{2,}=\\S/m.test(trimmed)) {\n if (\n /\\b(?:PATH|PWD|HOME|USER|INIT_CWD|PNPM_|NODE_|npm_|CORTENA_|CORTENACORE_|CORTENABOT_)/m.test(\n trimmed,\n )\n ) {\n return true;\n }\n if ((trimmed.match(/(?:^|\\n)[A-Z_]{3,}=\\S/gm) ?? []).length >= 3) {\n return true;\n }\n }\n return false;\n}\n\n/**\n * Should this message be hidden?\n *\n * The same chain cortenaweb's bubble applies, in the same order. It exists\n * because tool plumbing leaks into a transcript in several recognisable shapes,\n * and a chat that renders them reads as broken. Tool detail is not lost: it is\n * in the tool strip and in the chain of thought.\n *\n * A reply that is only UI the agent drew has no text at all, so an A2UI block\n * counts as content — without that, the surface the user asked for is filtered\n * away and nothing appears.\n */\nexport function isHiddenMessage(message: AgentChatMessage): boolean {\n if (message.isError) {\n return false;\n }\n if (message.isToolMessage) {\n return true;\n }\n if (\n message.role === \"assistant\" &&\n !message.text.trim() &&\n !message.thinkingText &&\n !message.a2ui?.length\n ) {\n return true;\n }\n if (message.role === \"user\") {\n return !message.text.trim() && !message.attachments?.length;\n }\n const text = message.text;\n return isToolResult(text) || isSkillContent(text) || isAgentDiagnostics(text) || isToolOutput(text);\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"session.js","names":[],"sources":["../../src/agent-chat/session.ts"],"sourcesContent":["/**\n * Sessions, history and the filter chain.\n *\n * Ported from `cortena-shared/src/stores/use-chat.ts` and\n * `cortenaweb/components/chat/message-bubble.tsx` (Cortena monorepo).\n * `cortena-shared` is not published, so the rules the two transports share\n * live here; keep them in step.\n *\n * The one deliberate difference is where the current session key is kept.\n * cortenaweb puts it in `localStorage`, which is shared by every tab of an\n * origin, so a second window adopted the first window's session: it inherited\n * a stream that was not its own and both windows then showed the same content.\n * Sessions here are **per window** — the key lives in `sessionStorage`, under a\n * name scoped to the extension — so opening the extension twice starts two\n * sessions. Both stay listed and either window can resume either one; two\n * windows on one session may both send, and cortenacore serialises the runs.\n */\n\nimport { extractA2UIBlocks, foldA2UIBlocks } from \"./a2ui-block\";\nimport { truncate } from \"./step-label\";\nimport type { AgentChatMessage, AgentChatStep } from \"./types\";\n\n/* ── minting and remembering a key ───────────────────────────────────────── */\n\n/**\n * A new session key, minted client-side.\n *\n * `agent:<agentId>:new-<Date.now()>-<random36>`. cortenacore reads the\n * `agent:<id>:` prefix to bind the session to that agent's template, and the\n * key is the AG-UI `threadId` verbatim, prefix included. Minting it rather than\n * asking for one is what lets the first message go out immediately.\n */\nexport function mintSessionKey(agentId: string): string {\n return `agent:${agentId}:new-${Date.now()}-${Math.random().toString(36).slice(2, 7)}`;\n}\n\n/** Was this key minted locally for a chat that had no session yet? */\nexport function isProvisionalSessionKey(key: string): boolean {\n return /(^|:)new-\\d+-[a-z0-9]+$/.test(key);\n}\n\n/** Does this session belong to this agent? `sessions.list` is filtered by it. */\nexport function isSessionOfAgent(key: string, agentId: string): boolean {\n return key.startsWith(`agent:${agentId}:`);\n}\n\n/** The `sessionStorage` name the current key is kept under, per extension. */\nexport function sessionStorageKey(scope: string): string {\n return `cortena.agent-chat.${scope}.session`;\n}\n\n/** The `sessionStorage` name the open/collapsed state is kept under. */\nexport function viewStorageKey(scope: string): string {\n return `cortena.agent-chat.${scope}.view`;\n}\n\n/** The `sessionStorage` name this window's progress steps are kept under. */\nexport function stepsStorageKey(scope: string): string {\n return `cortena.agent-chat.${scope}.steps`;\n}\n\n/**\n * `sessionStorage`, or nothing.\n *\n * Absent during SSR and refused outright when a browser blocks storage for the\n * origin, and neither is a reason for the chat not to work: without it a window\n * simply forgets its session on reload.\n */\nexport function windowSessionStorage(): Storage | null {\n try {\n return typeof window === \"undefined\" ? null : window.sessionStorage;\n } catch {\n return null;\n }\n}\n\nexport function readStoredSessionKey(scope: string): string | null {\n try {\n return windowSessionStorage()?.getItem(sessionStorageKey(scope)) ?? null;\n } catch {\n return null;\n }\n}\n\nexport function writeStoredSessionKey(scope: string, key: string | null): void {\n try {\n const storage = windowSessionStorage();\n if (!storage) {\n return;\n }\n if (key === null) {\n storage.removeItem(sessionStorageKey(scope));\n } else {\n storage.setItem(sessionStorageKey(scope), key);\n }\n } catch {\n // A blocked storage is not a reason to lose the run in flight.\n }\n}\n\n/* ── the progress steps of the turn ──────────────────────────────────────── */\n\n/** Most steps kept, in memory and in `sessionStorage`. A turn that long is a runaway loop. */\nexport const STEP_LIMIT = 50;\n\n/**\n * Most bytes the steps slot may occupy.\n *\n * `sessionStorage` is a few megabytes for the whole origin, shared with\n * everything else the host keeps there, and a step count alone does not bound\n * the size: fifty steps whose labels are all near the limit, with a failure\n * message on each, is a slot nobody budgeted for. Over the cap the OLDEST are\n * dropped, one at a time, because the recent ones are the ones a reload has to\n * bring back.\n */\nconst STEP_BYTE_LIMIT = 64 * 1024;\n\n/** Longest tool call id or tool name kept, in storage and on `data-tool`. */\nexport const STORED_TEXT_LIMIT = 80;\n\n/**\n * The steps of the current turn, and the session they belong to.\n *\n * They go in the per-window store beside the session key, for the same reason\n * the key does: a tab is reloaded mid-run, or a host remounts the surface on a\n * route change, and both threw the whole strip away. (Collapsing the pop-up is\n * NOT one of them — the panel is `hidden`, not unmounted, and the run streams\n * into it either way.) Messages come back from `chat.history`; steps do not\n * exist on the server, so this is the only place they can come back from.\n *\n * Scoped to a session key, and checked on read: resuming a different session\n * must not inherit the last one's steps.\n */\nexport function readStoredSteps(scope: string, sessionKey: string): AgentChatStep[] {\n try {\n const raw = windowSessionStorage()?.getItem(stepsStorageKey(scope));\n if (!raw) {\n return [];\n }\n const parsed = JSON.parse(raw) as { sessionKey?: unknown; steps?: unknown };\n if (parsed?.sessionKey !== sessionKey || !Array.isArray(parsed.steps)) {\n return [];\n }\n return (parsed.steps as AgentChatStep[])\n .filter((step) => typeof step?.id === \"string\" && typeof step?.label === \"string\")\n .map(restoreStep)\n .slice(-STEP_LIMIT);\n } catch {\n // Storage refused, or somebody else wrote the slot. Neither is worth a\n // broken chat; the strip simply starts empty.\n return [];\n }\n}\n\n/** Every status a step may legitimately come back with. */\nconst STEP_STATUSES: ReadonlySet<string> = new Set([\n \"running\",\n \"done\",\n \"error\",\n \"stopped\",\n \"paused\",\n]);\n\n/**\n * A step as it comes back from storage.\n *\n * The important cases are `running` and `paused`. Neither is true any more:\n * the run that owned the call ended when the surface went away — a reload, a\n * remount on a route change; a collapsed pop-up only HIDES the strip and never\n * reaches this path — and no result will arrive for it now, so a restored\n * spinner spins until the user gives up and reloads again, which restores it.\n *\n * It comes back `stopped`, with no message. Neutral is the honest reading:\n * nothing failed, the work simply did not continue, and \"Interrupted.\" drawn in\n * the failure colour told the user their call had broken when the page had.\n *\n * An unrecognised status is treated the same way. Nothing else writes this\n * slot today, but it is per-origin `sessionStorage` and this is the only place\n * that decides what a strip is allowed to render.\n */\nfunction restoreStep(step: AgentChatStep): AgentChatStep {\n const known = STEP_STATUSES.has(step.status);\n if (known && step.status !== \"running\" && step.status !== \"paused\") {\n return step;\n }\n const restored: AgentChatStep = {\n ...step,\n status: \"stopped\",\n endedAt: step.endedAt ?? Date.now(),\n };\n delete restored.errorMessage;\n return restored;\n}\n\n/**\n * The fields of a step that are safe to keep, and the only ones kept.\n *\n * `args` is deliberately absent. A tool call's arguments are the user's data —\n * what they searched for, whose record they opened, the body of what they\n * wrote — and `sessionStorage` is readable by every script on the origin,\n * survives the run, and is the sort of thing that ends up in a support bundle.\n * Nothing on screen needs them after the run: the LABEL is already derived\n * from them, and the label is what a restored strip shows.\n */\nfunction storedStep(step: AgentChatStep): AgentChatStep {\n return {\n // Bounded, both of them. Neither is written by this package: the id is the\n // producer's `toolCallId` and the name is whatever the model called, so a\n // single call carrying 9 kB of either filled the byte cap on its own and\n // pushed every real step out of the slot.\n id: truncate(step.id, STORED_TEXT_LIMIT),\n toolName: truncate(step.toolName, STORED_TEXT_LIMIT),\n label: step.label,\n status: step.status,\n startedAt: step.startedAt,\n ...(step.endedAt === undefined ? {} : { endedAt: step.endedAt }),\n ...(step.errorMessage === undefined ? {} : { errorMessage: step.errorMessage }),\n };\n}\n\n/** The slot's contents, projected and trimmed to fit the byte cap. */\nexport function serialiseSteps(sessionKey: string, steps: readonly AgentChatStep[]): string {\n let kept = steps.slice(-STEP_LIMIT).map(storedStep);\n let json = JSON.stringify({ sessionKey, steps: kept });\n while (kept.length > 1 && json.length > STEP_BYTE_LIMIT) {\n kept = kept.slice(1);\n json = JSON.stringify({ sessionKey, steps: kept });\n }\n return json;\n}\n\nexport function writeStoredSteps(\n scope: string,\n sessionKey: string | null,\n steps: readonly AgentChatStep[],\n): void {\n try {\n const storage = windowSessionStorage();\n if (!storage) {\n return;\n }\n if (!sessionKey || steps.length === 0) {\n storage.removeItem(stepsStorageKey(scope));\n return;\n }\n storage.setItem(stepsStorageKey(scope), serialiseSteps(sessionKey, steps));\n } catch {\n // Blocked, or over quota. The run in flight is not worth losing over it.\n }\n}\n\n/* ── history ─────────────────────────────────────────────────────────────── */\n\nfunction generateId(): string {\n return `msg-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`;\n}\n\nfunction isToolRelatedMessage(msg: Record<string, unknown>): boolean {\n const role = typeof msg.role === \"string\" ? msg.role.toLowerCase() : \"\";\n if (role === \"tool\" || role === \"toolresult\" || role === \"tool_result\") {\n return true;\n }\n const content = msg.content as Array<{ type: string }> | undefined;\n if (Array.isArray(content) && content.length > 0) {\n const toolTypes = new Set([\n \"tool_use\",\n \"tool_call\",\n \"tool_result\",\n \"toolresult\",\n \"server_tool_use\",\n ]);\n const hasText = content.some((c) => c.type === \"text\" || c.type === \"thinking\");\n const hasToolBlocks = content.some((c) => toolTypes.has(c.type));\n if (hasToolBlocks && !hasText) {\n return true;\n }\n }\n return false;\n}\n\n/**\n * A leading `[Dow YYYY-MM-DD HH:MM TZ] ` stamp, as the runtime injects it\n * ahead of every message the agent is given. The date is what makes it safe\n * to remove: a user who opens with \"[draft] ship it\" keeps their bracket.\n * Same expression as `cortena-shared/src/stores/use-chat.ts`; keep in step.\n */\nconst INJECTED_TIMESTAMP_PREFIX = /^\\[[^\\]]*\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}[^\\]]*\\]\\s*/;\n\n/**\n * Strip the envelope the runtime prepends to a user turn: `System: [timestamp] …`\n * lines and `[System Message]` prefixes carrying agent context, and the\n * injected timestamp ahead of the words themselves. None of it is user text.\n */\nfunction stripSystemPrefixes(text: string): string {\n return text\n .split(\"\\n\")\n .filter((line) => !/^System:\\s*\\[/.test(line) && !line.startsWith(\"[System Message]\"))\n .join(\"\\n\")\n .replace(/^\\n+/, \"\")\n .replace(INJECTED_TIMESTAMP_PREFIX, \"\");\n}\n\n/** Lift `<thinking>…</thinking>` out of plain text. */\nexport function separateThinking(text: string): { thinkingText?: string; mainText: string } {\n const match = text.match(/<thinking>([\\s\\S]*?)<\\/thinking>/);\n if (!match?.[1]) {\n return { mainText: text };\n }\n const thinkingText = match[1].trim();\n const mainText = text.replace(/<thinking>[\\s\\S]*?<\\/thinking>\\s*/, \"\").trim();\n return { ...(thinkingText ? { thinkingText } : {}), mainText };\n}\n\nfunction parseOneHistoryMessage(raw: unknown): AgentChatMessage {\n const msg = raw as Record<string, unknown>;\n const content = msg.content as\n | Array<{ type: string; text?: string; thinking?: string; a2ui?: unknown }>\n | undefined;\n // The UI the agent drew, back from the transcript. `chat.history` lifts the\n // ```a2ui fences out of the prose and leaves them as `a2ui` content parts —\n // the same parts a live delta carries — so a reload has a block to render\n // rather than a fence to parse. Without this every drawn surface vanished\n // on reopen. Ported from cortena-shared (CORTENA-44).\n const a2ui = Array.isArray(content)\n ? extractA2UIBlocks({ content: content as Array<Record<string, unknown>> })\n : [];\n let text = Array.isArray(content)\n ? content\n .filter((c) => c.type === \"text\" && c.text)\n .map((c) => c.text!)\n .join(\"\")\n : typeof msg.content === \"string\"\n ? msg.content\n : typeof msg.text === \"string\"\n ? msg.text\n : \"\";\n\n const isToolMessage = isToolRelatedMessage(msg);\n const role = ((msg.role as string) === \"user\" ? \"user\" : \"assistant\") as \"user\" | \"assistant\";\n if (role === \"user\") {\n text = stripSystemPrefixes(text);\n }\n\n let thinkingText = \"\";\n if (typeof msg.thinking === \"string\" && msg.thinking.trim()) {\n thinkingText = msg.thinking.trim();\n } else if (Array.isArray(content)) {\n thinkingText = content\n .filter((c) => c.type === \"thinking\")\n .map((c) => c.thinking ?? c.text ?? \"\")\n .filter(Boolean)\n .join(\"\\n\\n\");\n }\n if (!thinkingText) {\n const { thinkingText: tagThinking, mainText } = separateThinking(text);\n if (tagThinking) {\n return {\n id: typeof msg.id === \"string\" ? msg.id : generateId(),\n role,\n text: mainText,\n timestamp: (msg.timestamp as number) ?? Date.now(),\n thinkingText: tagThinking,\n ...(isToolMessage ? { isToolMessage } : {}),\n ...(a2ui.length > 0 ? { a2ui } : {}),\n };\n }\n }\n\n return {\n id: typeof msg.id === \"string\" ? msg.id : generateId(),\n role,\n text,\n timestamp: (msg.timestamp as number) ?? Date.now(),\n ...(thinkingText ? { thinkingText } : {}),\n ...(isToolMessage ? { isToolMessage } : {}),\n ...(a2ui.length > 0 ? { a2ui } : {}),\n };\n}\n\n/**\n * Merge an agent turn — assistant messages plus tool call and result messages —\n * into one visible message. Everything except the final substantive response is\n * folded into the chain of thought. Only a genuine user message breaks the turn.\n */\nfunction mergeConsecutiveAssistantMessages(messages: AgentChatMessage[]): AgentChatMessage[] {\n const result: AgentChatMessage[] = [];\n let run: AgentChatMessage[] = [];\n\n const flush = () => {\n const first = run[0];\n if (!first) {\n return;\n }\n if (run.length === 1) {\n result.push(first);\n run = [];\n return;\n }\n let visibleIdx = run.length - 1;\n while (visibleIdx > 0 && !(run[visibleIdx]?.text ?? \"\").trim()) {\n visibleIdx--;\n }\n const visible = run[visibleIdx] ?? first;\n const cot: string[] = [];\n for (let i = 0; i < run.length; i++) {\n const entry = run[i];\n if (!entry) {\n continue;\n }\n if (i === visibleIdx) {\n if (visible.thinkingText) {\n cot.push(visible.thinkingText);\n }\n continue;\n }\n if (entry.thinkingText) {\n cot.push(entry.thinkingText);\n }\n if (entry.text.trim()) {\n cot.push(entry.text.trim());\n }\n }\n const combined = cot.filter(Boolean).join(\"\\n\\n\");\n // A surface the agent drew in an earlier message of the same turn belongs\n // to the answer the turn produced; folding the run must not drop it.\n const blocks = foldA2UIBlocks(run.flatMap((entry) => entry.a2ui ?? []));\n result.push({\n ...visible,\n ...(combined ? { thinkingText: combined } : {}),\n ...(blocks.length > 0 ? { a2ui: blocks } : {}),\n });\n run = [];\n };\n\n for (const msg of messages) {\n const isRealUser = msg.role === \"user\" && !msg.isToolMessage && msg.text.trim().length > 0;\n if (isRealUser) {\n flush();\n result.push(msg);\n } else {\n run.push(msg);\n }\n }\n flush();\n return result;\n}\n\n/** Raw transcript records to the messages the list renders. */\nexport function parseHistoryMessages(raw: readonly unknown[]): AgentChatMessage[] {\n return mergeConsecutiveAssistantMessages(raw.map(parseOneHistoryMessage));\n}\n\n/**\n * The history shrink guard.\n *\n * A history load must not shrink the session already on screen. The first\n * message of a new chat loads history while the turn is still in flight and the\n * user's message has not been persisted yet; replacing wholesale threw that\n * message away, and the reply survived, so the symptom was a conversation with\n * the answer but not the question.\n *\n * Only the same session, and only against losing messages: a longer or equal\n * history still replaces, so an edit, a deletion made elsewhere and a compaction\n * all land normally.\n */\nexport function wouldShrinkHistory(params: {\n currentSessionKey: string | null;\n loadedSessionKey: string;\n currentCount: number;\n loadedCount: number;\n}): boolean {\n return (\n params.currentSessionKey === params.loadedSessionKey &&\n params.loadedCount < params.currentCount\n );\n}\n\n/* ── the filter chain ────────────────────────────────────────────────────── */\n\n/** A JSON blob a tool or an extension API returned, echoed as a message. */\nfunction isToolResult(text: string): boolean {\n const trimmed = text.trim();\n const wrapped =\n (trimmed.startsWith(\"[\") && trimmed.endsWith(\"]\")) ||\n (trimmed.startsWith(\"{\") && trimmed.endsWith(\"}\"));\n if (!wrapped) {\n return false;\n }\n try {\n const parsed = JSON.parse(trimmed);\n return typeof parsed === \"object\" && parsed !== null;\n } catch {\n return false;\n }\n}\n\n/** Raw skill-file content the model echoed instead of acting on. */\nfunction isSkillContent(text: string): boolean {\n const trimmed = text.trim();\n return (\n /^name:\\s*\\S+.*\\bdescription:.*\\btype:\\s*skill\\b/i.test(trimmed) ||\n /^---\\s*\\n[\\s\\S]*?\\btype:\\s*skill\\b[\\s\\S]*?\\n---/m.test(trimmed)\n );\n}\n\n/** Runtime diagnostics: token counters, a leaked api-key hint. */\nfunction isAgentDiagnostics(text: string): boolean {\n const trimmed = text.trim();\n return /Tokens:\\s*\\d+.*Cache:.*Context:/s.test(trimmed) || /\\bapi-key\\s+sk-/.test(trimmed);\n}\n\n/** Tool execution output that leaked into history as its own message. */\nfunction isToolOutput(text: string): boolean {\n const trimmed = text.trim();\n if (!trimmed) {\n return false;\n }\n if (/^Tool\\s+\\S+\\s+not found$/i.test(trimmed)) {\n return true;\n }\n if (trimmed === \"(no output)\") {\n return true;\n }\n if (/^total\\s+\\d+\\s+[d-][rwx-]{9}/.test(trimmed)) {\n return true;\n }\n if (/^[d-][rwx-]{9}[@+]?\\s+\\d+\\s+\\S+\\s+\\S+/.test(trimmed)) {\n return true;\n }\n if (/(?:^|\\n)[A-Z_]{2,}=\\S/m.test(trimmed)) {\n if (\n /\\b(?:PATH|PWD|HOME|USER|INIT_CWD|PNPM_|NODE_|npm_|CORTENA_|CORTENACORE_|CORTENABOT_)/m.test(\n trimmed,\n )\n ) {\n return true;\n }\n if ((trimmed.match(/(?:^|\\n)[A-Z_]{3,}=\\S/gm) ?? []).length >= 3) {\n return true;\n }\n }\n return false;\n}\n\n/**\n * Should this message be hidden?\n *\n * The same chain cortenaweb's bubble applies, in the same order. It exists\n * because tool plumbing leaks into a transcript in several recognisable shapes,\n * and a chat that renders them reads as broken. Tool detail is not lost: it is\n * in the tool strip and in the chain of thought.\n *\n * A reply that is only UI the agent drew has no text at all, so an A2UI block\n * counts as content — without that, the surface the user asked for is filtered\n * away and nothing appears.\n */\nexport function isHiddenMessage(message: AgentChatMessage): boolean {\n if (message.isError) {\n return false;\n }\n if (message.isToolMessage) {\n return true;\n }\n if (\n message.role === \"assistant\" &&\n !message.text.trim() &&\n !message.thinkingText &&\n !message.a2ui?.length\n ) {\n return true;\n }\n if (message.role === \"user\") {\n return (\n !message.text.trim() && !message.attachments?.length && !message.attachmentItems?.length\n );\n }\n const text = message.text;\n return isToolResult(text) || isSkillContent(text) || isAgentDiagnostics(text) || isToolOutput(text);\n}\n"],"mappings":"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"}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { A2UIChatBlock } from "./a2ui-block.js";
|
|
3
|
-
import { AgentApprovalDecision, AgentApprovalRequest, AgentChatClient, AgentChatMessage, AgentChatStep, AgentSession, AgentToolResultMetadata } from "./types.js";
|
|
3
|
+
import { AgentApprovalDecision, AgentApprovalRequest, AgentChatClient, AgentChatMessage, AgentChatSendInput, AgentChatStep, AgentSession, AgentToolResultMetadata } from "./types.js";
|
|
4
4
|
//#region src/agent-chat/store.d.ts
|
|
5
5
|
export interface AgentToolEntry {
|
|
6
6
|
id: string;
|
|
@@ -86,17 +86,68 @@ export interface UseAgentChatOptions {
|
|
|
86
86
|
/** Skip reading and writing `sessionStorage`; for a transient mount. */
|
|
87
87
|
ephemeral?: boolean;
|
|
88
88
|
thinking?: "low" | "medium" | "high";
|
|
89
|
+
/**
|
|
90
|
+
* Mint the key for a brand-new session. Defaults to `mintSessionKey(client.agentId)`,
|
|
91
|
+
* which binds the session to this client's agent template. A host whose
|
|
92
|
+
* sessions are bound some other way — cortenaweb lets the user pick an agent
|
|
93
|
+
* per chat, and a chat with no pick is bound server-side to the default —
|
|
94
|
+
* supplies its own.
|
|
95
|
+
*/
|
|
96
|
+
mintSessionKey?: () => string;
|
|
97
|
+
/**
|
|
98
|
+
* Read the stored session key back on mount. Default `true`. A host whose
|
|
99
|
+
* URL names the session — `/chat/<key>` — is the source of truth itself and
|
|
100
|
+
* passes `false`: the key is still WRITTEN, so the rule "one window, one
|
|
101
|
+
* session" holds, but nothing is restored under the URL's feet and no
|
|
102
|
+
* history is fetched for a session the page is about to leave.
|
|
103
|
+
*/
|
|
104
|
+
restoreSession?: boolean;
|
|
105
|
+
/**
|
|
106
|
+
* Fetch `sessions.list` on mount and after each send. Default `true`. A host
|
|
107
|
+
* with its own session list — cortenaweb's sidebar — passes `false` and the
|
|
108
|
+
* hook makes no session calls at all; `sessions` stays empty.
|
|
109
|
+
*/
|
|
110
|
+
listSessions?: boolean;
|
|
111
|
+
}
|
|
112
|
+
/** What `send` accepts beside the text and the attachments. */
|
|
113
|
+
export interface AgentChatSendOptions {
|
|
114
|
+
/** The model for this run; the composer's selector. */
|
|
115
|
+
model?: AgentChatSendInput["model"];
|
|
116
|
+
/** Overrides the hook-level `thinking` for this run. */
|
|
117
|
+
thinking?: AgentChatSendInput["thinking"];
|
|
89
118
|
}
|
|
90
119
|
export interface UseAgentChatResult extends AgentChatState {
|
|
91
|
-
send: (text: string, attachments?: unknown[]) => void;
|
|
120
|
+
send: (text: string, attachments?: unknown[], options?: AgentChatSendOptions) => void;
|
|
92
121
|
sendAction: (action: NonNullable<Parameters<AgentChatClient["send"]>[0]["a2uiAction"]>) => void;
|
|
122
|
+
/** A click inside a rendered MCP App, as the next run's input. No user bubble. */
|
|
123
|
+
sendMcpAppAction: (action: NonNullable<AgentChatSendInput["mcpAppAction"]>) => void;
|
|
93
124
|
abort: () => void;
|
|
94
125
|
resolveApproval: (id: string, decision: AgentApprovalDecision) => void;
|
|
95
126
|
newSession: () => void;
|
|
96
127
|
resumeSession: (key: string) => void;
|
|
97
|
-
|
|
128
|
+
/**
|
|
129
|
+
* Leave the session without starting another. The transcript empties, the
|
|
130
|
+
* stored key is removed and the next `send` mints a fresh key — which is
|
|
131
|
+
* what a host's "New chat" wants when the URL should not change until the
|
|
132
|
+
* first message goes out. `newSession` mints immediately; this does not.
|
|
133
|
+
*/
|
|
134
|
+
clearSession: () => void;
|
|
135
|
+
/**
|
|
136
|
+
* Ask again for the message this one answered: the bubble is dropped and the
|
|
137
|
+
* user turn before it is sent again, so the new answer takes its place. Also
|
|
138
|
+
* the Retry on a failed turn. A no-op while a run is live or when nothing
|
|
139
|
+
* precedes the message.
|
|
140
|
+
*/
|
|
141
|
+
regenerate: (messageId: string) => void;
|
|
142
|
+
/**
|
|
143
|
+
* Ask for the next page of older history. `true` when a load was started;
|
|
144
|
+
* `false` when there is nothing to do — no session, a load already in
|
|
145
|
+
* flight, or no more history — so a caller that saves scroll position
|
|
146
|
+
* before a prepend knows whether one is coming.
|
|
147
|
+
*/
|
|
148
|
+
loadOlder: () => boolean;
|
|
98
149
|
refreshSessions: () => void;
|
|
99
150
|
}
|
|
100
|
-
export declare function useAgentChat({ client, storageScope, ephemeral, thinking }: UseAgentChatOptions): UseAgentChatResult;
|
|
151
|
+
export declare function useAgentChat({ client, storageScope, ephemeral, thinking, mintSessionKey: mintKey, restoreSession, listSessions }: UseAgentChatOptions): UseAgentChatResult;
|
|
101
152
|
//#endregion
|
|
102
153
|
//# sourceMappingURL=store.d.ts.map
|