@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.
Files changed (53) hide show
  1. package/package.json +3 -2
  2. package/src/_internal/capabilities.ts +12 -0
  3. package/src/_internal/registry.ts +1 -0
  4. package/src/access/GroupCoverage.tsx +175 -0
  5. package/src/access/PermissionEditor.tsx +134 -93
  6. package/src/access/managers.ts +32 -0
  7. package/src/access/messages.ts +36 -0
  8. package/src/ai/approval-routes.ts +5 -5
  9. package/src/ai/capabilities.ts +34 -12
  10. package/src/ai/chat/blocks.tsx +86 -179
  11. package/src/ai/chat/builtin-tools.tsx +57 -30
  12. package/src/ai/chat/live-turn.browser-harness.tsx +37 -0
  13. package/src/ai/chat/message-actions.tsx +6 -2
  14. package/src/ai/chat/message-utils.ts +17 -14
  15. package/src/ai/chat/messages.ts +324 -2
  16. package/src/ai/chat/presentation.tsx +257 -103
  17. package/src/ai/chat/tool-groups.ts +55 -35
  18. package/src/ai/chat/turn-layout.ts +141 -0
  19. package/src/ai/chat/turn-view.tsx +609 -0
  20. package/src/ai/client/projection.ts +37 -6
  21. package/src/ai/executor.ts +38 -5
  22. package/src/ai/memory-learning.ts +7 -12
  23. package/src/ai/memory-workflow-evidence.ts +10 -2
  24. package/src/ai/migrate.ts +45 -2
  25. package/src/ai/model-access.ts +4 -2
  26. package/src/ai/prefs.ts +19 -5
  27. package/src/ai/projects.ts +5 -2
  28. package/src/ai/protocol.ts +20 -4
  29. package/src/ai/provider-fetch.ts +67 -15
  30. package/src/ai/provider-retry.ts +105 -0
  31. package/src/ai/quota-provider.ts +14 -5
  32. package/src/ai/skills.ts +5 -2
  33. package/src/ai/store.ts +108 -3
  34. package/src/ai/stream.ts +2 -0
  35. package/src/ai/timeline.ts +9 -11
  36. package/src/ai/turn-timing.ts +31 -3
  37. package/src/ai/types.ts +12 -2
  38. package/src/browser/FileChooser.tsx +600 -0
  39. package/src/browser/choose-files.browser-harness.tsx +32 -0
  40. package/src/browser/choose-files.tsx +70 -0
  41. package/src/browser/file-chooser-messages.ts +86 -0
  42. package/src/browser/file-providers.ts +217 -0
  43. package/src/browser/files.tsx +23 -0
  44. package/src/cli/admin/account-administration.ts +2 -2
  45. package/src/contracts/registry.ts +2 -0
  46. package/src/server/index.ts +1 -0
  47. package/src/server/services/access.ts +44 -11
  48. package/src/server/services/index.ts +1 -0
  49. package/src/shared/app-presentation.ts +10 -2
  50. package/src/ssr/platform-messages.ts +2 -2
  51. package/src/styles/effects.css +69 -0
  52. package/src/styles/file-chooser.css +169 -0
  53. 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 query = () => text(args().query) || "Tools";
108
+ const t = useMessages();
109
+ const query = () => text(args().query) || t().tools;
105
110
  return (
106
- <CompletedActivity block={props.block} label={`Search tools: ${query()}`} description={`${tools().length} found`}>
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>No tools found.</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="List apps" description={`${apps().length} available`}>
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>No capability apps available.</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
- ["Loaded", result().loaded],
152
- ["Already loaded", result().alreadyLoaded],
153
- ["Missing", result().missing],
154
- ["Evicted", result().evicted],
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="Load tools" description={`${loadedCount()} loaded`}>
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={`Loaded skill ${text(result().name)}`}>
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 query = () => text(args().query) || "Help";
203
+ const t = useMessages();
204
+ const query = () => text(args().query) || t().help;
196
205
  return (
197
- <CompletedActivity block={props.block} label={`Search help: ${query()}`} description={`${documents().length} found`}>
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>No Help articles found.</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() ? `Read help: ${text(document()!.title)}` : "Read help"}
226
- description={document() ? text(document()!.appName) || text(document()!.appId) : "Article not found"}
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() ? `Search Project: ${query()}` : "List Project sources"}
240
- description={`${items().length} found${result().truncated === true ? "+" : ""}`}
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>No Project sources found.</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
- return <CompletedActivity block={props.block} label={`Read Project knowledge: ${text(result().title) || "Entry"}`} />;
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={`List files: ${text(args().path) || "/"}`}
284
- description={`${files().length} found${result().truncated === true ? "+" : ""}`}
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>No files found.</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()) || "File"}`}
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={`Read file: ${basename(path())}`}
341
- description={`${range()}${result().eof === true ? " · complete" : " · more available"}`}
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
- return <CompletedActivity block={props.block} label={text(args().expression) || "Calculate"} description={text(result().result)} />;
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={`Inspect image: ${basename(path())}`} defaultOpen>
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={`Read resource: ${text(args().type)}:${text(args().id)}`}
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 <FileOperationView block={props.block} verb="Wrote file" />;
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
- /** Host-owned visualizations, persisted independently of the execution session. */
32
- renderCodePresentation?: (result: unknown) => JSX.Element;
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
- /** Canonical capability failure text for the compact chat row. Raw input and output remain in the audit trail. */
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) || "The action could not be completed.";
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 "The action could not be completed.";
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
- return {
429
- label: "File download failed",
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: "Memory not updated", description: result.message, failed: true };
442
+ if (!result.ok) return { label: t.memoryNotUpdated, description: result.message, failed: true };
440
443
  if (args.action === "add")
441
- return { label: "Remembered", description: typeof args.content === "string" ? args.content.trim() : result.message, failed: false };
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: "Updated memory", description: result.message.replace(/^Updated memory:\s*/, ""), failed: false };
446
+ return { label: t.memoryUpdated, description: result.message.replace(/^Updated memory:\s*/, ""), failed: false };
444
447
  if (args.action === "delete")
445
- return { label: "Forgot memory", description: result.message.replace(/^Forgot memory:\s*/, ""), failed: false };
446
- return { label: args.action === "search" ? "Searched memories" : "Listed memories", description: result.message, failed: false };
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 => {