cortena-ui 1.4.2 → 1.5.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.
Files changed (133) hide show
  1. package/LICENSE +7 -0
  2. package/README.md +149 -1
  3. package/dist/a2ui/views.js +2 -2
  4. package/dist/agent-chat/a2ui-block.d.ts +60 -0
  5. package/dist/agent-chat/a2ui-block.js +69 -0
  6. package/dist/agent-chat/a2ui-block.js.map +1 -0
  7. package/dist/agent-chat/agui-client.d.ts +40 -0
  8. package/dist/agent-chat/agui-client.js +251 -0
  9. package/dist/agent-chat/agui-client.js.map +1 -0
  10. package/dist/agent-chat/bridge.d.ts +109 -0
  11. package/dist/agent-chat/bridge.js +349 -0
  12. package/dist/agent-chat/bridge.js.map +1 -0
  13. package/dist/agent-chat/session.d.ts +58 -0
  14. package/dist/agent-chat/session.js +249 -0
  15. package/dist/agent-chat/session.js.map +1 -0
  16. package/dist/agent-chat/store.d.ts +67 -0
  17. package/dist/agent-chat/store.js +548 -0
  18. package/dist/agent-chat/store.js.map +1 -0
  19. package/dist/agent-chat/types.d.ts +187 -0
  20. package/dist/agent-chat/types.js +17 -0
  21. package/dist/agent-chat/types.js.map +1 -0
  22. package/dist/agent-chat.d.ts +10 -0
  23. package/dist/agent-chat.js +10 -0
  24. package/dist/components/admin-permissions/admin-permissions.d.ts +66 -0
  25. package/dist/components/admin-permissions/admin-permissions.js +101 -0
  26. package/dist/components/admin-permissions/admin-permissions.js.map +1 -0
  27. package/dist/components/admin-permissions/context.d.ts +70 -0
  28. package/dist/components/admin-permissions/context.js +258 -0
  29. package/dist/components/admin-permissions/context.js.map +1 -0
  30. package/dist/components/admin-permissions/index.d.ts +10 -0
  31. package/dist/components/admin-permissions/licence.d.ts +15 -0
  32. package/dist/components/admin-permissions/licence.js +78 -0
  33. package/dist/components/admin-permissions/licence.js.map +1 -0
  34. package/dist/components/admin-permissions/matrix.d.ts +20 -0
  35. package/dist/components/admin-permissions/matrix.js +191 -0
  36. package/dist/components/admin-permissions/matrix.js.map +1 -0
  37. package/dist/components/admin-permissions/members.d.ts +18 -0
  38. package/dist/components/admin-permissions/members.js +185 -0
  39. package/dist/components/admin-permissions/members.js.map +1 -0
  40. package/dist/components/admin-permissions/role-assignment.d.ts +35 -0
  41. package/dist/components/admin-permissions/role-assignment.js +174 -0
  42. package/dist/components/admin-permissions/role-assignment.js.map +1 -0
  43. package/dist/components/admin-permissions/roles.d.ts +25 -0
  44. package/dist/components/admin-permissions/roles.js +168 -0
  45. package/dist/components/admin-permissions/roles.js.map +1 -0
  46. package/dist/components/admin-permissions/types.d.ts +152 -0
  47. package/dist/components/admin-permissions/types.js +63 -0
  48. package/dist/components/admin-permissions/types.js.map +1 -0
  49. package/dist/components/agent-chat-popup.d.ts +29 -0
  50. package/dist/components/agent-chat-popup.js +188 -0
  51. package/dist/components/agent-chat-popup.js.map +1 -0
  52. package/dist/components/agent-chat.d.ts +143 -0
  53. package/dist/components/agent-chat.js +578 -0
  54. package/dist/components/agent-chat.js.map +1 -0
  55. package/dist/components/app-shell.d.ts +126 -0
  56. package/dist/components/app-shell.js +297 -0
  57. package/dist/components/app-shell.js.map +1 -0
  58. package/dist/components/badge.d.ts +1 -1
  59. package/dist/components/button-link.js +1 -1
  60. package/dist/components/button.d.ts +2 -2
  61. package/dist/components/checkbox.d.ts +1 -1
  62. package/dist/components/combobox.d.ts +1 -1
  63. package/dist/components/combobox.js +1 -1
  64. package/dist/components/consent-screen.d.ts +65 -0
  65. package/dist/components/consent-screen.js +123 -0
  66. package/dist/components/consent-screen.js.map +1 -0
  67. package/dist/components/data-table/data-table.d.ts +15 -1
  68. package/dist/components/data-table/data-table.js +18 -4
  69. package/dist/components/data-table/data-table.js.map +1 -1
  70. package/dist/components/data-table/index.d.ts +4 -4
  71. package/dist/components/data-table/parts.d.ts +27 -3
  72. package/dist/components/data-table/parts.js +175 -55
  73. package/dist/components/data-table/parts.js.map +1 -1
  74. package/dist/components/data-table/types.d.ts +61 -0
  75. package/dist/components/data-table/use-data-table.js +91 -6
  76. package/dist/components/data-table/use-data-table.js.map +1 -1
  77. package/dist/components/data-table/use-server-source.js +119 -28
  78. package/dist/components/data-table/use-server-source.js.map +1 -1
  79. package/dist/components/help-panel.d.ts +131 -0
  80. package/dist/components/help-panel.js +545 -0
  81. package/dist/components/help-panel.js.map +1 -0
  82. package/dist/components/login-screen.d.ts +127 -0
  83. package/dist/components/login-screen.js +339 -0
  84. package/dist/components/login-screen.js.map +1 -0
  85. package/dist/components/session-guard.d.ts +268 -0
  86. package/dist/components/session-guard.js +632 -0
  87. package/dist/components/session-guard.js.map +1 -0
  88. package/dist/components/toast.d.ts +1 -1
  89. package/dist/core.d.ts +5 -1
  90. package/dist/core.js +11 -7
  91. package/dist/data-table.d.ts +13 -4
  92. package/dist/data-table.js +10 -2
  93. package/dist/hooks/use-cortena-theme.js +49 -3
  94. package/dist/hooks/use-cortena-theme.js.map +1 -1
  95. package/dist/index.d.ts +17 -4
  96. package/dist/index.js +21 -8
  97. package/dist/markdown.d.ts +2 -1
  98. package/dist/markdown.js +2 -1
  99. package/package.json +16 -4
  100. package/src/agent-chat/a2ui-block.ts +118 -0
  101. package/src/agent-chat/agui-client.ts +405 -0
  102. package/src/agent-chat/bridge.ts +433 -0
  103. package/src/agent-chat/session.ts +392 -0
  104. package/src/agent-chat/store.ts +738 -0
  105. package/src/agent-chat/types.ts +213 -0
  106. package/src/components/admin-permissions/admin-permissions.tsx +130 -0
  107. package/src/components/admin-permissions/context.tsx +376 -0
  108. package/src/components/admin-permissions/index.tsx +32 -0
  109. package/src/components/admin-permissions/licence.tsx +84 -0
  110. package/src/components/admin-permissions/matrix.tsx +257 -0
  111. package/src/components/admin-permissions/members.tsx +204 -0
  112. package/src/components/admin-permissions/role-assignment.tsx +239 -0
  113. package/src/components/admin-permissions/roles.tsx +169 -0
  114. package/src/components/admin-permissions/types.ts +231 -0
  115. package/src/components/agent-chat-popup.tsx +289 -0
  116. package/src/components/agent-chat.tsx +843 -0
  117. package/src/components/app-shell.tsx +502 -0
  118. package/src/components/consent-screen.tsx +239 -0
  119. package/src/components/data-table/data-table.tsx +36 -0
  120. package/src/components/data-table/index.tsx +6 -1
  121. package/src/components/data-table/parts.tsx +223 -47
  122. package/src/components/data-table/types.ts +68 -0
  123. package/src/components/data-table/use-data-table.ts +152 -4
  124. package/src/components/data-table/use-server-source.ts +150 -12
  125. package/src/components/help-panel.tsx +765 -0
  126. package/src/components/login-screen.tsx +479 -0
  127. package/src/components/session-guard.tsx +1071 -0
  128. package/src/entries/agent-chat.ts +113 -0
  129. package/src/entries/core.ts +8 -0
  130. package/src/entries/data-table.ts +41 -0
  131. package/src/entries/markdown.ts +25 -0
  132. package/src/hooks/use-cortena-theme.ts +63 -4
  133. package/src/index.ts +6 -0
@@ -0,0 +1,249 @@
1
+ "use client";
2
+ //#region src/agent-chat/session.ts
3
+ /**
4
+ * A new session key, minted client-side.
5
+ *
6
+ * `agent:<agentId>:new-<Date.now()>-<random36>`. cortenacore reads the
7
+ * `agent:<id>:` prefix to bind the session to that agent's template, and the
8
+ * key is the AG-UI `threadId` verbatim, prefix included. Minting it rather than
9
+ * asking for one is what lets the first message go out immediately.
10
+ */
11
+ function mintSessionKey(agentId) {
12
+ return `agent:${agentId}:new-${Date.now()}-${Math.random().toString(36).slice(2, 7)}`;
13
+ }
14
+ /** Was this key minted locally for a chat that had no session yet? */
15
+ function isProvisionalSessionKey(key) {
16
+ return /(^|:)new-\d+-[a-z0-9]+$/.test(key);
17
+ }
18
+ /** Does this session belong to this agent? `sessions.list` is filtered by it. */
19
+ function isSessionOfAgent(key, agentId) {
20
+ return key.startsWith(`agent:${agentId}:`);
21
+ }
22
+ /** The `sessionStorage` name the current key is kept under, per extension. */
23
+ function sessionStorageKey(scope) {
24
+ return `cortena.agent-chat.${scope}.session`;
25
+ }
26
+ /** The `sessionStorage` name the open/collapsed state is kept under. */
27
+ function viewStorageKey(scope) {
28
+ return `cortena.agent-chat.${scope}.view`;
29
+ }
30
+ /**
31
+ * `sessionStorage`, or nothing.
32
+ *
33
+ * Absent during SSR and refused outright when a browser blocks storage for the
34
+ * origin, and neither is a reason for the chat not to work: without it a window
35
+ * simply forgets its session on reload.
36
+ */
37
+ function windowSessionStorage() {
38
+ try {
39
+ return typeof window === "undefined" ? null : window.sessionStorage;
40
+ } catch {
41
+ return null;
42
+ }
43
+ }
44
+ function readStoredSessionKey(scope) {
45
+ try {
46
+ return windowSessionStorage()?.getItem(sessionStorageKey(scope)) ?? null;
47
+ } catch {
48
+ return null;
49
+ }
50
+ }
51
+ function writeStoredSessionKey(scope, key) {
52
+ try {
53
+ const storage = windowSessionStorage();
54
+ if (!storage) return;
55
+ if (key === null) storage.removeItem(sessionStorageKey(scope));
56
+ else storage.setItem(sessionStorageKey(scope), key);
57
+ } catch {}
58
+ }
59
+ function generateId() {
60
+ return `msg-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`;
61
+ }
62
+ function isToolRelatedMessage(msg) {
63
+ const role = typeof msg.role === "string" ? msg.role.toLowerCase() : "";
64
+ if (role === "tool" || role === "toolresult" || role === "tool_result") return true;
65
+ const content = msg.content;
66
+ if (Array.isArray(content) && content.length > 0) {
67
+ const toolTypes = /* @__PURE__ */ new Set([
68
+ "tool_use",
69
+ "tool_call",
70
+ "tool_result",
71
+ "toolresult",
72
+ "server_tool_use"
73
+ ]);
74
+ const hasText = content.some((c) => c.type === "text" || c.type === "thinking");
75
+ if (content.some((c) => toolTypes.has(c.type)) && !hasText) return true;
76
+ }
77
+ return false;
78
+ }
79
+ /**
80
+ * Strip `System: [timestamp] …` lines and `[System Message]` prefixes the
81
+ * runtime prepends to a user turn as agent context. They are not user text.
82
+ */
83
+ function stripSystemPrefixes(text) {
84
+ return text.split("\n").filter((line) => !/^System:\s*\[/.test(line) && !line.startsWith("[System Message]")).join("\n").replace(/^\n+/, "");
85
+ }
86
+ /** Lift `<thinking>…</thinking>` out of plain text. */
87
+ function separateThinking(text) {
88
+ const match = text.match(/<thinking>([\s\S]*?)<\/thinking>/);
89
+ if (!match?.[1]) return { mainText: text };
90
+ const thinkingText = match[1].trim();
91
+ const mainText = text.replace(/<thinking>[\s\S]*?<\/thinking>\s*/, "").trim();
92
+ return {
93
+ ...thinkingText ? { thinkingText } : {},
94
+ mainText
95
+ };
96
+ }
97
+ function parseOneHistoryMessage(raw) {
98
+ const msg = raw;
99
+ const content = msg.content;
100
+ 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 : "";
101
+ const isToolMessage = isToolRelatedMessage(msg);
102
+ const role = msg.role === "user" ? "user" : "assistant";
103
+ if (role === "user") text = stripSystemPrefixes(text);
104
+ let thinkingText = "";
105
+ if (typeof msg.thinking === "string" && msg.thinking.trim()) thinkingText = msg.thinking.trim();
106
+ else if (Array.isArray(content)) thinkingText = content.filter((c) => c.type === "thinking").map((c) => c.thinking ?? c.text ?? "").filter(Boolean).join("\n\n");
107
+ if (!thinkingText) {
108
+ const { thinkingText: tagThinking, mainText } = separateThinking(text);
109
+ if (tagThinking) return {
110
+ id: typeof msg.id === "string" ? msg.id : generateId(),
111
+ role,
112
+ text: mainText,
113
+ timestamp: msg.timestamp ?? Date.now(),
114
+ thinkingText: tagThinking,
115
+ ...isToolMessage ? { isToolMessage } : {}
116
+ };
117
+ }
118
+ return {
119
+ id: typeof msg.id === "string" ? msg.id : generateId(),
120
+ role,
121
+ text,
122
+ timestamp: msg.timestamp ?? Date.now(),
123
+ ...thinkingText ? { thinkingText } : {},
124
+ ...isToolMessage ? { isToolMessage } : {}
125
+ };
126
+ }
127
+ /**
128
+ * Merge an agent turn — assistant messages plus tool call and result messages —
129
+ * into one visible message. Everything except the final substantive response is
130
+ * folded into the chain of thought. Only a genuine user message breaks the turn.
131
+ */
132
+ function mergeConsecutiveAssistantMessages(messages) {
133
+ const result = [];
134
+ let run = [];
135
+ const flush = () => {
136
+ const first = run[0];
137
+ if (!first) return;
138
+ if (run.length === 1) {
139
+ result.push(first);
140
+ run = [];
141
+ return;
142
+ }
143
+ let visibleIdx = run.length - 1;
144
+ while (visibleIdx > 0 && !(run[visibleIdx]?.text ?? "").trim()) visibleIdx--;
145
+ const visible = run[visibleIdx] ?? first;
146
+ const cot = [];
147
+ for (let i = 0; i < run.length; i++) {
148
+ const entry = run[i];
149
+ if (!entry) continue;
150
+ if (i === visibleIdx) {
151
+ if (visible.thinkingText) cot.push(visible.thinkingText);
152
+ continue;
153
+ }
154
+ if (entry.thinkingText) cot.push(entry.thinkingText);
155
+ if (entry.text.trim()) cot.push(entry.text.trim());
156
+ }
157
+ const combined = cot.filter(Boolean).join("\n\n");
158
+ result.push({
159
+ ...visible,
160
+ ...combined ? { thinkingText: combined } : {}
161
+ });
162
+ run = [];
163
+ };
164
+ for (const msg of messages) if (msg.role === "user" && !msg.isToolMessage && msg.text.trim().length > 0) {
165
+ flush();
166
+ result.push(msg);
167
+ } else run.push(msg);
168
+ flush();
169
+ return result;
170
+ }
171
+ /** Raw transcript records to the messages the list renders. */
172
+ function parseHistoryMessages(raw) {
173
+ return mergeConsecutiveAssistantMessages(raw.map(parseOneHistoryMessage));
174
+ }
175
+ /**
176
+ * The history shrink guard.
177
+ *
178
+ * A history load must not shrink the session already on screen. The first
179
+ * message of a new chat loads history while the turn is still in flight and the
180
+ * user's message has not been persisted yet; replacing wholesale threw that
181
+ * message away, and the reply survived, so the symptom was a conversation with
182
+ * the answer but not the question.
183
+ *
184
+ * Only the same session, and only against losing messages: a longer or equal
185
+ * history still replaces, so an edit, a deletion made elsewhere and a compaction
186
+ * all land normally.
187
+ */
188
+ function wouldShrinkHistory(params) {
189
+ return params.currentSessionKey === params.loadedSessionKey && params.loadedCount < params.currentCount;
190
+ }
191
+ /** A JSON blob a tool or an extension API returned, echoed as a message. */
192
+ function isToolResult(text) {
193
+ const trimmed = text.trim();
194
+ if (!(trimmed.startsWith("[") && trimmed.endsWith("]") || trimmed.startsWith("{") && trimmed.endsWith("}"))) return false;
195
+ try {
196
+ const parsed = JSON.parse(trimmed);
197
+ return typeof parsed === "object" && parsed !== null;
198
+ } catch {
199
+ return false;
200
+ }
201
+ }
202
+ /** Raw skill-file content the model echoed instead of acting on. */
203
+ function isSkillContent(text) {
204
+ const trimmed = text.trim();
205
+ return /^name:\s*\S+.*\bdescription:.*\btype:\s*skill\b/i.test(trimmed) || /^---\s*\n[\s\S]*?\btype:\s*skill\b[\s\S]*?\n---/m.test(trimmed);
206
+ }
207
+ /** Runtime diagnostics: token counters, a leaked api-key hint. */
208
+ function isAgentDiagnostics(text) {
209
+ const trimmed = text.trim();
210
+ return /Tokens:\s*\d+.*Cache:.*Context:/s.test(trimmed) || /\bapi-key\s+sk-/.test(trimmed);
211
+ }
212
+ /** Tool execution output that leaked into history as its own message. */
213
+ function isToolOutput(text) {
214
+ const trimmed = text.trim();
215
+ if (!trimmed) return false;
216
+ if (/^Tool\s+\S+\s+not found$/i.test(trimmed)) return true;
217
+ if (trimmed === "(no output)") return true;
218
+ if (/^total\s+\d+\s+[d-][rwx-]{9}/.test(trimmed)) return true;
219
+ if (/^[d-][rwx-]{9}[@+]?\s+\d+\s+\S+\s+\S+/.test(trimmed)) return true;
220
+ if (/(?:^|\n)[A-Z_]{2,}=\S/m.test(trimmed)) {
221
+ if (/\b(?:PATH|PWD|HOME|USER|INIT_CWD|PNPM_|NODE_|npm_|CORTENA_|CORTENACORE_|CORTENABOT_)/m.test(trimmed)) return true;
222
+ if ((trimmed.match(/(?:^|\n)[A-Z_]{3,}=\S/gm) ?? []).length >= 3) return true;
223
+ }
224
+ return false;
225
+ }
226
+ /**
227
+ * Should this message be hidden?
228
+ *
229
+ * The same chain cortenaweb's bubble applies, in the same order. It exists
230
+ * because tool plumbing leaks into a transcript in several recognisable shapes,
231
+ * and a chat that renders them reads as broken. Tool detail is not lost: it is
232
+ * in the tool strip and in the chain of thought.
233
+ *
234
+ * A reply that is only UI the agent drew has no text at all, so an A2UI block
235
+ * counts as content — without that, the surface the user asked for is filtered
236
+ * away and nothing appears.
237
+ */
238
+ function isHiddenMessage(message) {
239
+ if (message.isError) return false;
240
+ if (message.isToolMessage) return true;
241
+ if (message.role === "assistant" && !message.text.trim() && !message.thinkingText && !message.a2ui?.length) return true;
242
+ if (message.role === "user") return !message.text.trim() && !message.attachments?.length;
243
+ const text = message.text;
244
+ return isToolResult(text) || isSkillContent(text) || isAgentDiagnostics(text) || isToolOutput(text);
245
+ }
246
+ //#endregion
247
+ export { isHiddenMessage, isProvisionalSessionKey, isSessionOfAgent, mintSessionKey, parseHistoryMessages, readStoredSessionKey, separateThinking, sessionStorageKey, viewStorageKey, windowSessionStorage, wouldShrinkHistory, writeStoredSessionKey };
248
+
249
+ //# sourceMappingURL=session.js.map
@@ -0,0 +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 type { AgentChatMessage } 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/**\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/* ── 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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"}
@@ -0,0 +1,67 @@
1
+ "use client";
2
+ import { A2UIChatBlock } from "./a2ui-block.js";
3
+ import { AgentApprovalDecision, AgentApprovalRequest, AgentChatClient, AgentChatMessage, AgentSession } from "./types.js";
4
+ //#region src/agent-chat/store.d.ts
5
+ export interface AgentToolEntry {
6
+ id: string;
7
+ name: string;
8
+ status: "running" | "complete" | "error";
9
+ input?: unknown;
10
+ output?: unknown;
11
+ startedAt: number;
12
+ completedAt?: number;
13
+ }
14
+ export interface AgentChatState {
15
+ sessionKey: string | null;
16
+ messages: AgentChatMessage[];
17
+ streaming: boolean;
18
+ streamText: string;
19
+ streamThinkingText: string;
20
+ streamA2ui: A2UIChatBlock[];
21
+ runId: string | null;
22
+ hasMoreHistory: boolean;
23
+ loadingHistory: boolean;
24
+ toolOrder: string[];
25
+ tools: Record<string, AgentToolEntry>;
26
+ approvals: AgentApprovalRequest[];
27
+ /**
28
+ * Transcript RECORDS loaded, which is not `messages.length`.
29
+ *
30
+ * `parseHistoryMessages` folds a turn — an assistant preamble, the tool call,
31
+ * the tool result, the answer — into one visible message, so the two numbers
32
+ * diverge on the very first agent turn. `offset` is an offset into the
33
+ * server's records, and paging with the folded count asked for records the
34
+ * screen already had and skipped the ones it did not: "load older" silently
35
+ * lost messages the further back a user read.
36
+ */
37
+ historyCount: number;
38
+ sessions: AgentSession[];
39
+ loadingSessions: boolean;
40
+ /** A failure the user has to be told about, e.g. no template for the slug. */
41
+ error: string | null;
42
+ }
43
+ export interface UseAgentChatOptions {
44
+ client: AgentChatClient;
45
+ /**
46
+ * Names the `sessionStorage` slot the current session key lives in. Two
47
+ * pop-ups with different scopes never share a key, which is what keeps one
48
+ * extension's chat out of another's.
49
+ */
50
+ storageScope: string;
51
+ /** Skip reading and writing `sessionStorage`; for a transient mount. */
52
+ ephemeral?: boolean;
53
+ thinking?: "low" | "medium" | "high";
54
+ }
55
+ export interface UseAgentChatResult extends AgentChatState {
56
+ send: (text: string, attachments?: unknown[]) => void;
57
+ sendAction: (action: NonNullable<Parameters<AgentChatClient["send"]>[0]["a2uiAction"]>) => void;
58
+ abort: () => void;
59
+ resolveApproval: (id: string, decision: AgentApprovalDecision) => void;
60
+ newSession: () => void;
61
+ resumeSession: (key: string) => void;
62
+ loadOlder: () => void;
63
+ refreshSessions: () => void;
64
+ }
65
+ export declare function useAgentChat({ client, storageScope, ephemeral, thinking }: UseAgentChatOptions): UseAgentChatResult;
66
+ //#endregion
67
+ //# sourceMappingURL=store.d.ts.map