@k2b/cloud 0.24.0 → 0.26.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 -2
- package/src/_internal/capabilities.ts +12 -0
- package/src/_internal/registry.ts +1 -0
- package/src/access/GroupCoverage.tsx +175 -0
- package/src/access/PermissionEditor.tsx +134 -93
- package/src/access/managers.ts +32 -0
- package/src/access/messages.ts +36 -0
- package/src/ai/approval-routes.ts +5 -5
- package/src/ai/capabilities.ts +34 -12
- package/src/ai/chat/blocks.tsx +86 -179
- package/src/ai/chat/builtin-tools.tsx +57 -30
- package/src/ai/chat/live-turn.browser-harness.tsx +37 -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 +324 -2
- package/src/ai/chat/presentation.tsx +257 -103
- 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 +609 -0
- package/src/ai/client/projection.ts +37 -6
- package/src/ai/executor.ts +38 -5
- package/src/ai/memory-learning.ts +7 -12
- package/src/ai/memory-workflow-evidence.ts +10 -2
- package/src/ai/migrate.ts +45 -2
- package/src/ai/model-access.ts +4 -2
- package/src/ai/prefs.ts +19 -5
- package/src/ai/projects.ts +5 -2
- package/src/ai/protocol.ts +20 -4
- package/src/ai/provider-fetch.ts +67 -15
- package/src/ai/provider-retry.ts +105 -0
- package/src/ai/quota-provider.ts +14 -5
- package/src/ai/skills.ts +5 -2
- package/src/ai/store.ts +108 -3
- package/src/ai/stream.ts +2 -0
- package/src/ai/timeline.ts +9 -11
- package/src/ai/turn-timing.ts +31 -3
- package/src/ai/types.ts +12 -2
- package/src/browser/FileChooser.tsx +600 -0
- package/src/browser/choose-files.browser-harness.tsx +32 -0
- package/src/browser/choose-files.tsx +70 -0
- package/src/browser/file-chooser-messages.ts +86 -0
- package/src/browser/file-providers.ts +217 -0
- package/src/browser/files.tsx +23 -0
- package/src/cli/admin/account-administration.ts +2 -2
- package/src/contracts/registry.ts +2 -0
- package/src/server/index.ts +1 -0
- package/src/server/services/access.ts +44 -11
- package/src/server/services/index.ts +1 -0
- package/src/shared/app-presentation.ts +10 -2
- package/src/ssr/platform-messages.ts +2 -2
- package/src/styles/effects.css +69 -0
- package/src/styles/file-chooser.css +169 -0
- package/src/styles/global.css +1 -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>
|
|
@@ -145,13 +151,14 @@ function LoadToolsView(props: { block: ToolBlock }) {
|
|
|
145
151
|
const value = result().titles;
|
|
146
152
|
return isRecord(value) ? value : {};
|
|
147
153
|
};
|
|
154
|
+
const t = useMessages();
|
|
148
155
|
const groups = () =>
|
|
149
156
|
(
|
|
150
157
|
[
|
|
151
|
-
[
|
|
152
|
-
[
|
|
153
|
-
[
|
|
154
|
-
[
|
|
158
|
+
[t().toolsLoaded, result().loaded],
|
|
159
|
+
[t().toolsAlreadyLoaded, result().alreadyLoaded],
|
|
160
|
+
[t().toolsMissing, result().missing],
|
|
161
|
+
[t().toolsEvicted, result().evicted],
|
|
155
162
|
] as const
|
|
156
163
|
).flatMap(([label, value]) =>
|
|
157
164
|
Array.isArray(value) && value.length > 0 ? [{ label, names: value.filter((name): name is string => typeof name === "string") }] : [],
|
|
@@ -161,7 +168,7 @@ function LoadToolsView(props: { block: ToolBlock }) {
|
|
|
161
168
|
return Array.isArray(value) ? value.length : 0;
|
|
162
169
|
};
|
|
163
170
|
return (
|
|
164
|
-
<CompletedActivity block={props.block} label=
|
|
171
|
+
<CompletedActivity block={props.block} label={t().loadTools} description={t().loadedCount({ count: loadedCount() })}>
|
|
165
172
|
<Show when={groups().length > 0}>
|
|
166
173
|
<ResultList>
|
|
167
174
|
<For each={groups()}>
|
|
@@ -179,8 +186,9 @@ function LoadToolsView(props: { block: ToolBlock }) {
|
|
|
179
186
|
|
|
180
187
|
function LoadSkillView(props: { block: ToolBlock }) {
|
|
181
188
|
const result = () => (isRecord(props.block.result) ? props.block.result : {});
|
|
189
|
+
const t = useMessages();
|
|
182
190
|
return (
|
|
183
|
-
<CompletedActivity block={props.block} label={
|
|
191
|
+
<CompletedActivity block={props.block} label={t().loadedSkill({ name: text(result().name) })}>
|
|
184
192
|
<DetailSurface>
|
|
185
193
|
<p class="whitespace-pre-wrap px-2 py-1.5 leading-5 text-secondary">{text(result().description)}</p>
|
|
186
194
|
</DetailSurface>
|
|
@@ -192,11 +200,16 @@ function SearchHelpView(props: { block: ToolBlock }) {
|
|
|
192
200
|
const args = () => (isRecord(props.block.args) ? props.block.args : {});
|
|
193
201
|
const result = () => (isRecord(props.block.result) ? props.block.result : {});
|
|
194
202
|
const documents = () => records(result().documents);
|
|
195
|
-
const
|
|
203
|
+
const t = useMessages();
|
|
204
|
+
const query = () => text(args().query) || t().help;
|
|
196
205
|
return (
|
|
197
|
-
<CompletedActivity
|
|
206
|
+
<CompletedActivity
|
|
207
|
+
block={props.block}
|
|
208
|
+
label={t().searchHelp({ query: query() })}
|
|
209
|
+
description={t().found({ count: documents().length })}
|
|
210
|
+
>
|
|
198
211
|
<ResultList>
|
|
199
|
-
<Show when={documents().length > 0} fallback={<EmptyRow>
|
|
212
|
+
<Show when={documents().length > 0} fallback={<EmptyRow>{t().noHelp}</EmptyRow>}>
|
|
200
213
|
<For each={documents()}>
|
|
201
214
|
{(document) => (
|
|
202
215
|
<ResultRow
|
|
@@ -219,11 +232,12 @@ function ReadHelpView(props: { block: ToolBlock }) {
|
|
|
219
232
|
const value = result().document;
|
|
220
233
|
return isRecord(value) ? value : null;
|
|
221
234
|
};
|
|
235
|
+
const t = useMessages();
|
|
222
236
|
return (
|
|
223
237
|
<CompletedActivity
|
|
224
238
|
block={props.block}
|
|
225
|
-
label={document() ?
|
|
226
|
-
description={document() ? text(document()!.appName) || text(document()!.appId) :
|
|
239
|
+
label={document() ? t().readHelpArticle({ title: text(document()!.title) }) : t().readHelp}
|
|
240
|
+
description={document() ? text(document()!.appName) || text(document()!.appId) : t().articleNotFound}
|
|
227
241
|
/>
|
|
228
242
|
);
|
|
229
243
|
}
|
|
@@ -233,14 +247,15 @@ function SearchProjectView(props: { block: ToolBlock }) {
|
|
|
233
247
|
const result = () => (isRecord(props.block.result) ? props.block.result : {});
|
|
234
248
|
const items = () => records(result().items);
|
|
235
249
|
const query = () => text(args().query);
|
|
250
|
+
const t = useMessages();
|
|
236
251
|
return (
|
|
237
252
|
<CompletedActivity
|
|
238
253
|
block={props.block}
|
|
239
|
-
label={query() ?
|
|
240
|
-
description={
|
|
254
|
+
label={query() ? t().searchProject({ query: query() }) : t().listProjectSources}
|
|
255
|
+
description={t().found({ count: items().length, more: result().truncated === true })}
|
|
241
256
|
>
|
|
242
257
|
<ResultList>
|
|
243
|
-
<Show when={items().length > 0} fallback={<EmptyRow>
|
|
258
|
+
<Show when={items().length > 0} fallback={<EmptyRow>{t().noProjectSources}</EmptyRow>}>
|
|
244
259
|
<For each={items()}>
|
|
245
260
|
{(item) => {
|
|
246
261
|
const kind = text(item.kind);
|
|
@@ -270,21 +285,23 @@ function SearchProjectView(props: { block: ToolBlock }) {
|
|
|
270
285
|
|
|
271
286
|
function ReadProjectKnowledgeView(props: { block: ToolBlock }) {
|
|
272
287
|
const result = () => (isRecord(props.block.result) ? props.block.result : {});
|
|
273
|
-
|
|
288
|
+
const t = useMessages();
|
|
289
|
+
return <CompletedActivity block={props.block} label={t().readProjectKnowledge({ title: text(result().title) || t().projectEntry })} />;
|
|
274
290
|
}
|
|
275
291
|
|
|
276
292
|
function ListFilesView(props: { block: ToolBlock }) {
|
|
277
293
|
const args = () => (isRecord(props.block.args) ? props.block.args : {});
|
|
278
294
|
const result = () => (isRecord(props.block.result) ? props.block.result : {});
|
|
279
295
|
const files = () => records(result().files);
|
|
296
|
+
const t = useMessages();
|
|
280
297
|
return (
|
|
281
298
|
<CompletedActivity
|
|
282
299
|
block={props.block}
|
|
283
|
-
label={
|
|
284
|
-
description={
|
|
300
|
+
label={t().listFiles({ path: text(args().path) || "/" })}
|
|
301
|
+
description={t().found({ count: files().length, more: result().truncated === true })}
|
|
285
302
|
>
|
|
286
303
|
<ResultList>
|
|
287
|
-
<Show when={files().length > 0} fallback={<EmptyRow>
|
|
304
|
+
<Show when={files().length > 0} fallback={<EmptyRow>{t().noFiles}</EmptyRow>}>
|
|
288
305
|
<For each={files()}>
|
|
289
306
|
{(file) => {
|
|
290
307
|
const path = text(file.path);
|
|
@@ -309,10 +326,11 @@ function FileOperationView(props: { block: ToolBlock; verb: string; resultPath?:
|
|
|
309
326
|
const result = () => (isRecord(props.block.result) ? props.block.result : {});
|
|
310
327
|
const path = () => props.resultPath || text(result().path) || text(args().path);
|
|
311
328
|
const size = () => number(result().size);
|
|
329
|
+
const t = useMessages();
|
|
312
330
|
return (
|
|
313
331
|
<CompletedActivity
|
|
314
332
|
block={props.block}
|
|
315
|
-
label={`${props.verb}: ${basename(path()) ||
|
|
333
|
+
label={`${props.verb}: ${basename(path()) || t().file}`}
|
|
316
334
|
description={[path(), size() > 0 ? formatAiFileSize(size()) : ""].filter(Boolean).join(" · ")}
|
|
317
335
|
/>
|
|
318
336
|
);
|
|
@@ -334,11 +352,12 @@ function ReadFileView(props: { block: ToolBlock }) {
|
|
|
334
352
|
const t = aiChatMessages(locale());
|
|
335
353
|
return end > start ? t.byteRange({ start: start.toLocaleString(locale()), end: end.toLocaleString(locale()) }) : t.read;
|
|
336
354
|
};
|
|
355
|
+
const t = () => aiChatMessages(locale());
|
|
337
356
|
return (
|
|
338
357
|
<CompletedActivity
|
|
339
358
|
block={props.block}
|
|
340
|
-
label={
|
|
341
|
-
description={`${range()}${result().eof === true ?
|
|
359
|
+
label={t().readFile({ name: basename(path()) })}
|
|
360
|
+
description={`${range()} · ${result().eof === true ? t().fileComplete : t().fileMoreAvailable}`}
|
|
342
361
|
/>
|
|
343
362
|
);
|
|
344
363
|
}
|
|
@@ -346,15 +365,17 @@ function ReadFileView(props: { block: ToolBlock }) {
|
|
|
346
365
|
function CalculateView(props: { block: ToolBlock }) {
|
|
347
366
|
const args = () => (isRecord(props.block.args) ? props.block.args : {});
|
|
348
367
|
const result = () => (isRecord(props.block.result) ? props.block.result : {});
|
|
349
|
-
|
|
368
|
+
const t = useMessages();
|
|
369
|
+
return <CompletedActivity block={props.block} label={text(args().expression) || t().calculate} description={text(result().result)} />;
|
|
350
370
|
}
|
|
351
371
|
|
|
352
372
|
function ViewImageView(props: { block: ToolBlock }) {
|
|
353
373
|
const args = () => (isRecord(props.block.args) ? props.block.args : {});
|
|
354
374
|
const result = () => (isRecord(props.block.result) ? props.block.result : {});
|
|
355
375
|
const path = () => text(result().path) || text(args().path);
|
|
376
|
+
const t = useMessages();
|
|
356
377
|
return (
|
|
357
|
-
<CompletedActivity block={props.block} label={
|
|
378
|
+
<CompletedActivity block={props.block} label={t().inspectImage({ name: basename(path()) })} defaultOpen>
|
|
358
379
|
<DetailSurface>
|
|
359
380
|
<p class="whitespace-pre-wrap px-2 py-1.5 leading-5 text-secondary">{text(result().description)}</p>
|
|
360
381
|
</DetailSurface>
|
|
@@ -382,15 +403,21 @@ function CloudResourceView(props: { block: ToolBlock }) {
|
|
|
382
403
|
const args = () => (isRecord(props.block.args) ? props.block.args : {});
|
|
383
404
|
const result = () => (isRecord(props.block.result) ? props.block.result : {});
|
|
384
405
|
const summary = () => text(result().summary);
|
|
406
|
+
const t = useMessages();
|
|
385
407
|
return (
|
|
386
408
|
<CompletedActivity
|
|
387
409
|
block={props.block}
|
|
388
|
-
label={
|
|
410
|
+
label={t().readResource({ ref: `${text(args().type)}:${text(args().id)}` })}
|
|
389
411
|
description={summary() || undefined}
|
|
390
412
|
/>
|
|
391
413
|
);
|
|
392
414
|
}
|
|
393
415
|
|
|
416
|
+
function WriteFileView(props: { block: ToolBlock }) {
|
|
417
|
+
const t = useMessages();
|
|
418
|
+
return <FileOperationView block={props.block} verb={t().wroteFile} />;
|
|
419
|
+
}
|
|
420
|
+
|
|
394
421
|
export function SpecializedBuiltinToolBlock(props: { block: ToolBlock }) {
|
|
395
422
|
switch (props.block.name) {
|
|
396
423
|
case "search_tools":
|
|
@@ -414,7 +441,7 @@ export function SpecializedBuiltinToolBlock(props: { block: ToolBlock }) {
|
|
|
414
441
|
case "read_file":
|
|
415
442
|
return <ReadFileView block={props.block} />;
|
|
416
443
|
case "write_file":
|
|
417
|
-
return <
|
|
444
|
+
return <WriteFileView block={props.block} />;
|
|
418
445
|
case "calculate":
|
|
419
446
|
return <CalculateView block={props.block} />;
|
|
420
447
|
case "view_image":
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { Chat } from "@k2b/ui";
|
|
2
|
+
import { createMemo } 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, and
|
|
12
|
+
* `window.steer(text)` appends a pending steer as the controller does.
|
|
13
|
+
*/
|
|
14
|
+
const [state, setState] = createStore(emptyProjection());
|
|
15
|
+
Object.assign(window, {
|
|
16
|
+
emit: (event: AiWireEvent) => setState(reconcile(reduceProjection(state, event), { key: "id", merge: true })),
|
|
17
|
+
steer: (text: string) =>
|
|
18
|
+
setState("activeTurn", "blocks", (blocks): AiTurnBlock[] => [
|
|
19
|
+
...blocks,
|
|
20
|
+
{ id: `steer-request-${blocks.length}`, kind: "steer_message", steerId: String(blocks.length), text, status: "pending" },
|
|
21
|
+
]),
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
const root = document.createElement("main");
|
|
25
|
+
root.style.cssText = "display: flex; flex-direction: column; height: 100dvh; max-width: 48rem; margin: 0 auto;";
|
|
26
|
+
document.body.append(root);
|
|
27
|
+
render(
|
|
28
|
+
() => (
|
|
29
|
+
<AiChatActionsProvider actions={{}}>
|
|
30
|
+
{(() => {
|
|
31
|
+
const items = createAiChatTimeline({ messages: createMemo(() => visibleMessages(state)), activeTurn: () => state.activeTurn });
|
|
32
|
+
return <Chat.Timeline items={items()} />;
|
|
33
|
+
})()}
|
|
34
|
+
</AiChatActionsProvider>
|
|
35
|
+
),
|
|
36
|
+
root,
|
|
37
|
+
);
|
|
@@ -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 => {
|