@k2b/cloud 0.25.0 → 0.27.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/package.json +3 -3
- package/src/_internal/capabilities.ts +12 -0
- package/src/_internal/define-app.ts +8 -1
- package/src/_internal/process-identity.ts +7 -1
- package/src/_internal/registry-validation.ts +3 -0
- package/src/_internal/registry.ts +1 -0
- package/src/_internal/runtime-context.ts +1 -0
- package/src/access/GroupCoverage.tsx +175 -0
- package/src/access/PermissionEditor.tsx +119 -99
- package/src/access/messages.ts +30 -0
- package/src/ai/admin.ts +1 -0
- package/src/ai/approval-routes.ts +5 -5
- package/src/ai/browser-code-contracts.ts +14 -2
- package/src/ai/browser.ts +8 -1
- package/src/ai/capabilities.ts +115 -33
- package/src/ai/chat/blocks.tsx +86 -179
- package/src/ai/chat/builtin-tools.tsx +83 -48
- package/src/ai/chat/file-tools.tsx +4 -1
- package/src/ai/chat/live-turn.browser-harness.tsx +44 -0
- package/src/ai/chat/message-actions.tsx +6 -2
- package/src/ai/chat/message-utils.ts +17 -14
- package/src/ai/chat/messages.ts +330 -2
- package/src/ai/chat/presentation.tsx +278 -104
- package/src/ai/chat/tool-groups.ts +55 -35
- package/src/ai/chat/turn-layout.ts +141 -0
- package/src/ai/chat/turn-view.tsx +644 -0
- package/src/ai/client/controller.ts +60 -31
- package/src/ai/client/file-source.ts +20 -3
- package/src/ai/client/projection.ts +42 -6
- package/src/ai/code-mode-skill.ts +27 -27
- package/src/ai/code-runtime-tools.ts +10 -1
- package/src/ai/code-source-contracts.ts +54 -4
- package/src/ai/code-source-tools.ts +10 -3
- package/src/ai/credentials.ts +17 -3
- package/src/ai/data-analysis-skill.ts +2 -2
- package/src/ai/default-tools.ts +2 -2
- package/src/ai/executor.ts +177 -80
- package/src/ai/file-context.ts +14 -2
- package/src/ai/file-tools.ts +17 -3
- package/src/ai/files-store.ts +134 -11
- package/src/ai/grids-skill.ts +2 -2
- package/src/ai/index.ts +7 -0
- package/src/ai/memories.ts +14 -0
- package/src/ai/migrate.ts +125 -0
- package/src/ai/model-request-settings.ts +98 -0
- package/src/ai/protocol.ts +26 -4
- package/src/ai/provider-fetch.ts +67 -15
- package/src/ai/provider-retry.ts +105 -0
- package/src/ai/provider.ts +7 -1
- package/src/ai/quota-provider.ts +16 -7
- package/src/ai/request-headers.ts +117 -0
- package/src/ai/routes.ts +34 -6
- package/src/ai/runtime.ts +1 -1
- package/src/ai/settings.ts +19 -2
- package/src/ai/skill-seeds.ts +31 -3
- package/src/ai/skills.ts +26 -0
- package/src/ai/solid.ts +1 -1
- package/src/ai/store.ts +202 -59
- package/src/ai/stream.ts +182 -37
- package/src/ai/structured.ts +20 -5
- package/src/ai/system-prompt.ts +25 -0
- package/src/ai/timeline.ts +9 -11
- package/src/ai/tool-call-names.ts +45 -0
- package/src/ai/turn-policy.ts +247 -0
- package/src/ai/turn-timing.ts +31 -3
- package/src/ai/types.ts +36 -5
- package/src/api/admin-ai-quotas.ts +36 -1
- package/src/api/admin-core-settings.ts +16 -23
- package/src/api/admin-outgoing-mail.ts +62 -0
- package/src/api/index.ts +2 -0
- package/src/cli/admin/ai-quotas.ts +70 -1
- package/src/cli/admin/index.ts +6 -0
- package/src/cli/admin/outgoing-mail.ts +118 -0
- package/src/contracts/app.ts +2 -0
- package/src/contracts/index.ts +1 -0
- package/src/contracts/outgoing-mail.ts +77 -0
- package/src/contracts/registry.ts +4 -0
- package/src/services/index.ts +3 -0
- package/src/services/notifications/email.ts +16 -26
- package/src/services/outgoing-mail/index.ts +19 -0
- package/src/services/outgoing-mail/store.ts +286 -0
- package/src/services/outgoing-mail/test-send.ts +40 -0
- package/src/services/outgoing-mail/transport.ts +13 -0
- package/src/services/settings/core-settings.ts +1 -38
- package/src/services/settings/store.ts +5 -1
- package/src/shared/ai-model-request-settings.ts +21 -0
- package/src/shared/ai-platform-prompt.ts +1 -1
- package/src/shared/ai-request-options.ts +185 -0
- package/src/shared/app-presentation.ts +10 -2
- package/src/ssr/admin-navigation.ts +1 -1
- package/src/ssr/platform-messages.ts +2 -0
- package/src/ssr/workspace-navigation.ts +7 -1
- package/src/styles/effects.css +69 -0
|
@@ -38,6 +38,10 @@ const text = (value: unknown): string => (typeof value === "string" ? value : ""
|
|
|
38
38
|
const number = (value: unknown): number => (typeof value === "number" && Number.isFinite(value) ? value : 0);
|
|
39
39
|
const records = (value: unknown): Record<string, unknown>[] => (Array.isArray(value) ? value.filter(isRecord) : []);
|
|
40
40
|
const basename = (path: string): string => path.slice(path.lastIndexOf("/") + 1) || path;
|
|
41
|
+
const useMessages = () => {
|
|
42
|
+
const locale = useLocale();
|
|
43
|
+
return () => aiChatMessages(locale());
|
|
44
|
+
};
|
|
41
45
|
|
|
42
46
|
function DetailSurface(props: { children: JSX.Element }) {
|
|
43
47
|
return (
|
|
@@ -101,11 +105,12 @@ function SearchToolsView(props: { block: ToolBlock }) {
|
|
|
101
105
|
const args = () => (isRecord(props.block.args) ? props.block.args : {});
|
|
102
106
|
const result = () => (isRecord(props.block.result) ? props.block.result : {});
|
|
103
107
|
const tools = () => records(result().tools);
|
|
104
|
-
const
|
|
108
|
+
const t = useMessages();
|
|
109
|
+
const query = () => text(args().query) || t().tools;
|
|
105
110
|
return (
|
|
106
|
-
<CompletedActivity block={props.block} label={
|
|
111
|
+
<CompletedActivity block={props.block} label={t().searchTools({ query: query() })} description={t().found({ count: tools().length })}>
|
|
107
112
|
<ResultList>
|
|
108
|
-
<Show when={tools().length > 0} fallback={<EmptyRow>
|
|
113
|
+
<Show when={tools().length > 0} fallback={<EmptyRow>{t().noTools}</EmptyRow>}>
|
|
109
114
|
<For each={tools()}>
|
|
110
115
|
{(tool) => (
|
|
111
116
|
<ResultRow
|
|
@@ -128,10 +133,11 @@ function ListAppsView(props: { block: ToolBlock }) {
|
|
|
128
133
|
const value = result().apps;
|
|
129
134
|
return isRecord(value) ? Object.entries(value) : [];
|
|
130
135
|
};
|
|
136
|
+
const t = useMessages();
|
|
131
137
|
return (
|
|
132
|
-
<CompletedActivity block={props.block} label=
|
|
138
|
+
<CompletedActivity block={props.block} label={t().listApps} description={t().available({ count: apps().length })}>
|
|
133
139
|
<ResultList>
|
|
134
|
-
<Show when={apps().length > 0} fallback={<EmptyRow>
|
|
140
|
+
<Show when={apps().length > 0} fallback={<EmptyRow>{t().noApps}</EmptyRow>}>
|
|
135
141
|
<For each={apps()}>{([appId, description]) => <ResultRow icon="ti-apps" title={appId} description={text(description)} />}</For>
|
|
136
142
|
</Show>
|
|
137
143
|
</ResultList>
|
|
@@ -139,37 +145,46 @@ function ListAppsView(props: { block: ToolBlock }) {
|
|
|
139
145
|
);
|
|
140
146
|
}
|
|
141
147
|
|
|
148
|
+
/** Entries are `{name}` records; turns stored before #528 hold plain names and a `missing` list. */
|
|
149
|
+
const toolNames = (value: unknown): string[] =>
|
|
150
|
+
Array.isArray(value)
|
|
151
|
+
? value.flatMap((item) => (typeof item === "string" ? [item] : isRecord(item) && typeof item.name === "string" ? [item.name] : []))
|
|
152
|
+
: [];
|
|
153
|
+
|
|
142
154
|
function LoadToolsView(props: { block: ToolBlock }) {
|
|
143
155
|
const result = () => (isRecord(props.block.result) ? props.block.result : {});
|
|
144
156
|
const titles = (): Record<string, unknown> => {
|
|
145
157
|
const value = result().titles;
|
|
146
158
|
return isRecord(value) ? value : {};
|
|
147
159
|
};
|
|
148
|
-
const
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
)
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
160
|
+
const t = useMessages();
|
|
161
|
+
const unavailableLabel = (reason: unknown) =>
|
|
162
|
+
reason === "not_offered_in_turn"
|
|
163
|
+
? t().toolNotOffered
|
|
164
|
+
: reason === "not_allowed"
|
|
165
|
+
? t().toolNotAllowed
|
|
166
|
+
: reason === "app_offline"
|
|
167
|
+
? t().toolAppOffline
|
|
168
|
+
: t().toolUnknown;
|
|
169
|
+
const rows = () => [
|
|
170
|
+
...toolNames(result().loaded).map((name) => ({ name, label: t().toolsLoaded })),
|
|
171
|
+
...toolNames(result().alreadyLoaded).map((name) => ({ name, label: t().toolsAlreadyLoaded })),
|
|
172
|
+
...records(result().unavailable).flatMap((item) =>
|
|
173
|
+
typeof item.name === "string" ? [{ name: item.name, label: unavailableLabel(item.reason) }] : [],
|
|
174
|
+
),
|
|
175
|
+
...toolNames(result().missing).map((name) => ({ name, label: t().toolUnknown })),
|
|
176
|
+
...toolNames(result().evicted).map((name) => ({ name, label: t().toolsEvicted })),
|
|
177
|
+
];
|
|
163
178
|
return (
|
|
164
|
-
<CompletedActivity
|
|
165
|
-
|
|
179
|
+
<CompletedActivity
|
|
180
|
+
block={props.block}
|
|
181
|
+
label={t().loadTools}
|
|
182
|
+
description={t().loadedCount({ count: toolNames(result().loaded).length })}
|
|
183
|
+
>
|
|
184
|
+
<Show when={rows().length > 0}>
|
|
166
185
|
<ResultList>
|
|
167
|
-
<For each={
|
|
168
|
-
{(
|
|
169
|
-
<For each={group.names}>
|
|
170
|
-
{(name) => <ResultRow icon={aiToolIcon(name)} title={text(titles()[name]) || name} meta={group.label} />}
|
|
171
|
-
</For>
|
|
172
|
-
)}
|
|
186
|
+
<For each={rows()}>
|
|
187
|
+
{(row) => <ResultRow icon={aiToolIcon(row.name)} title={text(titles()[row.name]) || row.name} meta={row.label} />}
|
|
173
188
|
</For>
|
|
174
189
|
</ResultList>
|
|
175
190
|
</Show>
|
|
@@ -179,8 +194,9 @@ function LoadToolsView(props: { block: ToolBlock }) {
|
|
|
179
194
|
|
|
180
195
|
function LoadSkillView(props: { block: ToolBlock }) {
|
|
181
196
|
const result = () => (isRecord(props.block.result) ? props.block.result : {});
|
|
197
|
+
const t = useMessages();
|
|
182
198
|
return (
|
|
183
|
-
<CompletedActivity block={props.block} label={
|
|
199
|
+
<CompletedActivity block={props.block} label={t().loadedSkill({ name: text(result().name) })}>
|
|
184
200
|
<DetailSurface>
|
|
185
201
|
<p class="whitespace-pre-wrap px-2 py-1.5 leading-5 text-secondary">{text(result().description)}</p>
|
|
186
202
|
</DetailSurface>
|
|
@@ -192,11 +208,16 @@ function SearchHelpView(props: { block: ToolBlock }) {
|
|
|
192
208
|
const args = () => (isRecord(props.block.args) ? props.block.args : {});
|
|
193
209
|
const result = () => (isRecord(props.block.result) ? props.block.result : {});
|
|
194
210
|
const documents = () => records(result().documents);
|
|
195
|
-
const
|
|
211
|
+
const t = useMessages();
|
|
212
|
+
const query = () => text(args().query) || t().help;
|
|
196
213
|
return (
|
|
197
|
-
<CompletedActivity
|
|
214
|
+
<CompletedActivity
|
|
215
|
+
block={props.block}
|
|
216
|
+
label={t().searchHelp({ query: query() })}
|
|
217
|
+
description={t().found({ count: documents().length })}
|
|
218
|
+
>
|
|
198
219
|
<ResultList>
|
|
199
|
-
<Show when={documents().length > 0} fallback={<EmptyRow>
|
|
220
|
+
<Show when={documents().length > 0} fallback={<EmptyRow>{t().noHelp}</EmptyRow>}>
|
|
200
221
|
<For each={documents()}>
|
|
201
222
|
{(document) => (
|
|
202
223
|
<ResultRow
|
|
@@ -219,11 +240,12 @@ function ReadHelpView(props: { block: ToolBlock }) {
|
|
|
219
240
|
const value = result().document;
|
|
220
241
|
return isRecord(value) ? value : null;
|
|
221
242
|
};
|
|
243
|
+
const t = useMessages();
|
|
222
244
|
return (
|
|
223
245
|
<CompletedActivity
|
|
224
246
|
block={props.block}
|
|
225
|
-
label={document() ?
|
|
226
|
-
description={document() ? text(document()!.appName) || text(document()!.appId) :
|
|
247
|
+
label={document() ? t().readHelpArticle({ title: text(document()!.title) }) : t().readHelp}
|
|
248
|
+
description={document() ? text(document()!.appName) || text(document()!.appId) : t().articleNotFound}
|
|
227
249
|
/>
|
|
228
250
|
);
|
|
229
251
|
}
|
|
@@ -233,14 +255,15 @@ function SearchProjectView(props: { block: ToolBlock }) {
|
|
|
233
255
|
const result = () => (isRecord(props.block.result) ? props.block.result : {});
|
|
234
256
|
const items = () => records(result().items);
|
|
235
257
|
const query = () => text(args().query);
|
|
258
|
+
const t = useMessages();
|
|
236
259
|
return (
|
|
237
260
|
<CompletedActivity
|
|
238
261
|
block={props.block}
|
|
239
|
-
label={query() ?
|
|
240
|
-
description={
|
|
262
|
+
label={query() ? t().searchProject({ query: query() }) : t().listProjectSources}
|
|
263
|
+
description={t().found({ count: items().length, more: result().truncated === true })}
|
|
241
264
|
>
|
|
242
265
|
<ResultList>
|
|
243
|
-
<Show when={items().length > 0} fallback={<EmptyRow>
|
|
266
|
+
<Show when={items().length > 0} fallback={<EmptyRow>{t().noProjectSources}</EmptyRow>}>
|
|
244
267
|
<For each={items()}>
|
|
245
268
|
{(item) => {
|
|
246
269
|
const kind = text(item.kind);
|
|
@@ -270,21 +293,23 @@ function SearchProjectView(props: { block: ToolBlock }) {
|
|
|
270
293
|
|
|
271
294
|
function ReadProjectKnowledgeView(props: { block: ToolBlock }) {
|
|
272
295
|
const result = () => (isRecord(props.block.result) ? props.block.result : {});
|
|
273
|
-
|
|
296
|
+
const t = useMessages();
|
|
297
|
+
return <CompletedActivity block={props.block} label={t().readProjectKnowledge({ title: text(result().title) || t().projectEntry })} />;
|
|
274
298
|
}
|
|
275
299
|
|
|
276
300
|
function ListFilesView(props: { block: ToolBlock }) {
|
|
277
301
|
const args = () => (isRecord(props.block.args) ? props.block.args : {});
|
|
278
302
|
const result = () => (isRecord(props.block.result) ? props.block.result : {});
|
|
279
303
|
const files = () => records(result().files);
|
|
304
|
+
const t = useMessages();
|
|
280
305
|
return (
|
|
281
306
|
<CompletedActivity
|
|
282
307
|
block={props.block}
|
|
283
|
-
label={
|
|
284
|
-
description={
|
|
308
|
+
label={t().listFiles({ path: text(args().path) || "/" })}
|
|
309
|
+
description={t().found({ count: files().length, more: result().truncated === true })}
|
|
285
310
|
>
|
|
286
311
|
<ResultList>
|
|
287
|
-
<Show when={files().length > 0} fallback={<EmptyRow>
|
|
312
|
+
<Show when={files().length > 0} fallback={<EmptyRow>{t().noFiles}</EmptyRow>}>
|
|
288
313
|
<For each={files()}>
|
|
289
314
|
{(file) => {
|
|
290
315
|
const path = text(file.path);
|
|
@@ -309,10 +334,11 @@ function FileOperationView(props: { block: ToolBlock; verb: string; resultPath?:
|
|
|
309
334
|
const result = () => (isRecord(props.block.result) ? props.block.result : {});
|
|
310
335
|
const path = () => props.resultPath || text(result().path) || text(args().path);
|
|
311
336
|
const size = () => number(result().size);
|
|
337
|
+
const t = useMessages();
|
|
312
338
|
return (
|
|
313
339
|
<CompletedActivity
|
|
314
340
|
block={props.block}
|
|
315
|
-
label={`${props.verb}: ${basename(path()) ||
|
|
341
|
+
label={`${props.verb}: ${basename(path()) || t().file}`}
|
|
316
342
|
description={[path(), size() > 0 ? formatAiFileSize(size()) : ""].filter(Boolean).join(" · ")}
|
|
317
343
|
/>
|
|
318
344
|
);
|
|
@@ -334,11 +360,12 @@ function ReadFileView(props: { block: ToolBlock }) {
|
|
|
334
360
|
const t = aiChatMessages(locale());
|
|
335
361
|
return end > start ? t.byteRange({ start: start.toLocaleString(locale()), end: end.toLocaleString(locale()) }) : t.read;
|
|
336
362
|
};
|
|
363
|
+
const t = () => aiChatMessages(locale());
|
|
337
364
|
return (
|
|
338
365
|
<CompletedActivity
|
|
339
366
|
block={props.block}
|
|
340
|
-
label={
|
|
341
|
-
description={`${range()}${result().eof === true ?
|
|
367
|
+
label={t().readFile({ name: basename(path()) })}
|
|
368
|
+
description={`${range()} · ${result().eof === true ? t().fileComplete : t().fileMoreAvailable}`}
|
|
342
369
|
/>
|
|
343
370
|
);
|
|
344
371
|
}
|
|
@@ -346,15 +373,17 @@ function ReadFileView(props: { block: ToolBlock }) {
|
|
|
346
373
|
function CalculateView(props: { block: ToolBlock }) {
|
|
347
374
|
const args = () => (isRecord(props.block.args) ? props.block.args : {});
|
|
348
375
|
const result = () => (isRecord(props.block.result) ? props.block.result : {});
|
|
349
|
-
|
|
376
|
+
const t = useMessages();
|
|
377
|
+
return <CompletedActivity block={props.block} label={text(args().expression) || t().calculate} description={text(result().result)} />;
|
|
350
378
|
}
|
|
351
379
|
|
|
352
380
|
function ViewImageView(props: { block: ToolBlock }) {
|
|
353
381
|
const args = () => (isRecord(props.block.args) ? props.block.args : {});
|
|
354
382
|
const result = () => (isRecord(props.block.result) ? props.block.result : {});
|
|
355
383
|
const path = () => text(result().path) || text(args().path);
|
|
384
|
+
const t = useMessages();
|
|
356
385
|
return (
|
|
357
|
-
<CompletedActivity block={props.block} label={
|
|
386
|
+
<CompletedActivity block={props.block} label={t().inspectImage({ name: basename(path()) })} defaultOpen>
|
|
358
387
|
<DetailSurface>
|
|
359
388
|
<p class="whitespace-pre-wrap px-2 py-1.5 leading-5 text-secondary">{text(result().description)}</p>
|
|
360
389
|
</DetailSurface>
|
|
@@ -382,15 +411,21 @@ function CloudResourceView(props: { block: ToolBlock }) {
|
|
|
382
411
|
const args = () => (isRecord(props.block.args) ? props.block.args : {});
|
|
383
412
|
const result = () => (isRecord(props.block.result) ? props.block.result : {});
|
|
384
413
|
const summary = () => text(result().summary);
|
|
414
|
+
const t = useMessages();
|
|
385
415
|
return (
|
|
386
416
|
<CompletedActivity
|
|
387
417
|
block={props.block}
|
|
388
|
-
label={
|
|
418
|
+
label={t().readResource({ ref: `${text(args().type)}:${text(args().id)}` })}
|
|
389
419
|
description={summary() || undefined}
|
|
390
420
|
/>
|
|
391
421
|
);
|
|
392
422
|
}
|
|
393
423
|
|
|
424
|
+
function WriteFileView(props: { block: ToolBlock }) {
|
|
425
|
+
const t = useMessages();
|
|
426
|
+
return <FileOperationView block={props.block} verb={t().wroteFile} />;
|
|
427
|
+
}
|
|
428
|
+
|
|
394
429
|
export function SpecializedBuiltinToolBlock(props: { block: ToolBlock }) {
|
|
395
430
|
switch (props.block.name) {
|
|
396
431
|
case "search_tools":
|
|
@@ -414,7 +449,7 @@ export function SpecializedBuiltinToolBlock(props: { block: ToolBlock }) {
|
|
|
414
449
|
case "read_file":
|
|
415
450
|
return <ReadFileView block={props.block} />;
|
|
416
451
|
case "write_file":
|
|
417
|
-
return <
|
|
452
|
+
return <WriteFileView block={props.block} />;
|
|
418
453
|
case "calculate":
|
|
419
454
|
return <CalculateView block={props.block} />;
|
|
420
455
|
case "view_image":
|
|
@@ -40,7 +40,10 @@ export function PresentToolBlock(props: { block: ToolBlock }) {
|
|
|
40
40
|
</Show>
|
|
41
41
|
}
|
|
42
42
|
>
|
|
43
|
-
<div
|
|
43
|
+
<div
|
|
44
|
+
data-call-id={props.block.callId}
|
|
45
|
+
class="inline-flex min-h-7 max-w-full items-center gap-1.5 py-1 text-xs leading-none text-dimmed"
|
|
46
|
+
>
|
|
44
47
|
<i class={`ti ${icon()} shrink-0 text-base leading-none`} aria-hidden="true" />
|
|
45
48
|
<Show
|
|
46
49
|
when={actions.onOpenFile}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { Chat } from "@k2b/ui";
|
|
2
|
+
import { createMemo, createSignal } from "solid-js";
|
|
3
|
+
import { createStore, reconcile } from "solid-js/store";
|
|
4
|
+
import { render } from "solid-js/web";
|
|
5
|
+
import { emptyProjection, reduceProjection, visibleMessages } from "../client/projection";
|
|
6
|
+
import type { AiTurnBlock, AiWireEvent } from "../protocol";
|
|
7
|
+
import { AiChatActionsProvider, createAiChatTimeline } from "./presentation";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* The live chat timeline for a browser test, folded from wire events like the
|
|
11
|
+
* controller does. `window.emit(event)` applies one event,
|
|
12
|
+
* `window.steer(text)` appends a pending steer as the controller does, and
|
|
13
|
+
* `window.reconnect(on)` sets whether the stream reconnects.
|
|
14
|
+
*/
|
|
15
|
+
const [state, setState] = createStore(emptyProjection());
|
|
16
|
+
const [reconnecting, setReconnecting] = createSignal(false);
|
|
17
|
+
Object.assign(window, {
|
|
18
|
+
reconnect: (on: boolean) => setReconnecting(on),
|
|
19
|
+
emit: (event: AiWireEvent) => setState(reconcile(reduceProjection(state, event), { key: "id", merge: true })),
|
|
20
|
+
steer: (text: string) =>
|
|
21
|
+
setState("activeTurn", "blocks", (blocks): AiTurnBlock[] => [
|
|
22
|
+
...blocks,
|
|
23
|
+
{ id: `steer-request-${blocks.length}`, kind: "steer_message", steerId: String(blocks.length), text, status: "pending" },
|
|
24
|
+
]),
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
const root = document.createElement("main");
|
|
28
|
+
root.style.cssText = "display: flex; flex-direction: column; height: 100dvh; max-width: 48rem; margin: 0 auto;";
|
|
29
|
+
document.body.append(root);
|
|
30
|
+
render(
|
|
31
|
+
() => (
|
|
32
|
+
<AiChatActionsProvider actions={{}}>
|
|
33
|
+
{(() => {
|
|
34
|
+
const items = createAiChatTimeline({
|
|
35
|
+
messages: createMemo(() => visibleMessages(state)),
|
|
36
|
+
activeTurn: () => state.activeTurn,
|
|
37
|
+
reconnecting,
|
|
38
|
+
});
|
|
39
|
+
return <Chat.Timeline items={items()} />;
|
|
40
|
+
})()}
|
|
41
|
+
</AiChatActionsProvider>
|
|
42
|
+
),
|
|
43
|
+
root,
|
|
44
|
+
);
|
|
@@ -28,8 +28,12 @@ type RetryMessageHandler = (entry: AiStoredMessage, input?: AiRetryMessageInput)
|
|
|
28
28
|
type RetrySteerHandler = (block: Extract<AiTurnBlock, { kind: "steer_message" }>) => void | Promise<void>;
|
|
29
29
|
|
|
30
30
|
export type AiChatActions = {
|
|
31
|
-
/**
|
|
32
|
-
|
|
31
|
+
/**
|
|
32
|
+
* Host-owned visualizations, persisted independently of the execution session. Called once per `code_present`
|
|
33
|
+
* call, from its first event: `result()` is undefined while the call runs and the saved result afterwards, so
|
|
34
|
+
* the host reserves the frame early and keeps its state when the turn ends.
|
|
35
|
+
*/
|
|
36
|
+
renderCodePresentation?: (result: () => unknown) => JSX.Element;
|
|
33
37
|
/** Prevents turn-continuation actions while the current turn is stopping. */
|
|
34
38
|
actionDisabled?: () => boolean;
|
|
35
39
|
onApproval?: ApprovalHandler;
|
|
@@ -8,6 +8,7 @@ import { type AiResourceMarker, parseAiResourceMarker } from "../resource-marker
|
|
|
8
8
|
import { assistantVisibleTextFromMessage } from "../timeline";
|
|
9
9
|
import type { AiStoredMessage, AiUserContentPart } from "../types";
|
|
10
10
|
import { AI_IMAGE_MEDIA_TYPES, isAiImageMediaType } from "../types";
|
|
11
|
+
import { aiChatMessages } from "./messages";
|
|
11
12
|
|
|
12
13
|
type AssistantToolResultMessage = Extract<Message, { role: "tool_result" }>;
|
|
13
14
|
|
|
@@ -401,20 +402,23 @@ const boundedErrorText = (value: string): string => {
|
|
|
401
402
|
return compact.length > 500 ? `${compact.slice(0, 497)}...` : compact;
|
|
402
403
|
};
|
|
403
404
|
|
|
404
|
-
/**
|
|
405
|
+
/**
|
|
406
|
+
* Canonical capability failure text for the compact chat row, or "" when the result carries none and the row
|
|
407
|
+
* shows its localized fallback. Raw input and output remain in the audit trail.
|
|
408
|
+
*/
|
|
405
409
|
export const capabilityErrorDescription = (result: unknown): string => {
|
|
406
|
-
if (typeof result === "string") return boundedErrorText(result)
|
|
410
|
+
if (typeof result === "string") return boundedErrorText(result);
|
|
407
411
|
if (isRecord(result)) {
|
|
408
412
|
const message = typeof result.message === "string" ? result.message : typeof result.error === "string" ? result.error : "";
|
|
409
413
|
const code = typeof result.code === "string" ? result.code : "";
|
|
410
414
|
const text = code && message && !message.startsWith(code) ? `${code}: ${message}` : message || code;
|
|
411
415
|
if (text) return boundedErrorText(text);
|
|
412
416
|
}
|
|
413
|
-
return "
|
|
417
|
+
return "";
|
|
414
418
|
};
|
|
415
419
|
|
|
416
420
|
/** Compact user-facing copy for fetch_file failures; the full response remains in the disclosure and audit trail. */
|
|
417
|
-
export const fetchFileErrorPresentation = (result: unknown): FetchFileErrorPresentation => {
|
|
421
|
+
export const fetchFileErrorPresentation = (result: unknown, locale = "en"): FetchFileErrorPresentation => {
|
|
418
422
|
const message =
|
|
419
423
|
typeof result === "string"
|
|
420
424
|
? result.trim()
|
|
@@ -425,25 +429,24 @@ export const fetchFileErrorPresentation = (result: unknown): FetchFileErrorPrese
|
|
|
425
429
|
if (separator > 0) {
|
|
426
430
|
return { label: message.slice(0, separator), description: message.slice(separator + 3) };
|
|
427
431
|
}
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
description: message || "The linked file could not be imported into this chat.",
|
|
431
|
-
};
|
|
432
|
+
const t = aiChatMessages(locale);
|
|
433
|
+
return { label: t.fileDownloadFailed, description: message || t.fileNotImported };
|
|
432
434
|
};
|
|
433
435
|
|
|
434
436
|
/** End-user copy for memory updates; full tool input/result remains available in persisted audit data. */
|
|
435
|
-
export const memoryToolPresentation = (args: unknown, result: unknown): MemoryToolPresentation | null => {
|
|
437
|
+
export const memoryToolPresentation = (args: unknown, result: unknown, locale = "en"): MemoryToolPresentation | null => {
|
|
436
438
|
if (!isRecord(args) || !["list", "search", "add", "update", "delete"].includes(String(args.action))) return null;
|
|
437
439
|
if (!isRecord(result) || typeof result.ok !== "boolean" || typeof result.message !== "string") return null;
|
|
440
|
+
const t = aiChatMessages(locale);
|
|
438
441
|
|
|
439
|
-
if (!result.ok) return { label:
|
|
442
|
+
if (!result.ok) return { label: t.memoryNotUpdated, description: result.message, failed: true };
|
|
440
443
|
if (args.action === "add")
|
|
441
|
-
return { label:
|
|
444
|
+
return { label: t.remembered, description: typeof args.content === "string" ? args.content.trim() : result.message, failed: false };
|
|
442
445
|
if (args.action === "update")
|
|
443
|
-
return { label:
|
|
446
|
+
return { label: t.memoryUpdated, description: result.message.replace(/^Updated memory:\s*/, ""), failed: false };
|
|
444
447
|
if (args.action === "delete")
|
|
445
|
-
return { label:
|
|
446
|
-
return { label: args.action === "search" ?
|
|
448
|
+
return { label: t.memoryForgotten, description: result.message.replace(/^Forgot memory:\s*/, ""), failed: false };
|
|
449
|
+
return { label: args.action === "search" ? t.memoriesSearched : t.memoriesListed, description: result.message, failed: false };
|
|
447
450
|
};
|
|
448
451
|
|
|
449
452
|
export const toolResultSummary = (message: AssistantToolResultMessage | null | undefined): string => {
|