@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
@@ -1,11 +1,10 @@
1
1
  import { dates } from "@k2b/stdlib";
2
2
  import { mutation } from "@k2b/stdlib/solid";
3
3
  import { Button, ButtonLink, Chat, isStructuredDataValue, MarkdownView, SplitButton, StructuredDataPreview, useLocale } from "@k2b/ui";
4
- import { createMemo, createSignal, For, type JSX, Match, Show, Switch } from "solid-js";
4
+ import { createSignal, For, type JSX, Match, Show, Switch, untrack } from "solid-js";
5
5
  import type { CapabilityActionReview } from "../../contracts/capabilities";
6
6
  import { markdown } from "../../shared";
7
7
  import type { AiTurnBlock } from "../protocol";
8
- import { isRenderableTurnBlock } from "../protocol";
9
8
  import { hasSpecializedBuiltinToolView, SpecializedBuiltinToolBlock } from "./builtin-tools";
10
9
  import { hasCapabilityTable } from "./capability-result";
11
10
  import { CapabilityTablePreview } from "./capability-table";
@@ -26,8 +25,8 @@ import {
26
25
  } from "./message-utils";
27
26
  import { aiChatMessages } from "./messages";
28
27
  import { AssistantMarkdownBlock } from "./primitives";
29
- import { AiToolActivity, AiToolDisclosureProvider, type AiToolDisclosureState, createAiToolDisclosureState } from "./tool-disclosure";
30
- import { groupToolBlocks, isFailedTool, summarizeToolGroup } from "./tool-groups";
28
+ import { AiToolActivity } from "./tool-disclosure";
29
+ import { isFailedTool } from "./tool-groups";
31
30
  import { CloudCardBlock, CloudSurveyBlock, CloudSurveyResultBlock, CloudTextEditorBlock, CloudTextEditorResultBlock } from "./visual-tools";
32
31
  import { FetchFileToolBlock, WebExtractToolBlock, WebSearchToolBlock } from "./web-tools";
33
32
 
@@ -78,35 +77,36 @@ function ThinkingBlockView(props: { text: string; streaming?: boolean }) {
78
77
  );
79
78
  }
80
79
 
81
- function CompactionBlockView(props: { block: Extract<AiTurnBlock, { kind: "compaction" }> }) {
80
+ export function CompactionBlockView(props: { block: Extract<AiTurnBlock, { kind: "compaction" }> }) {
82
81
  const locale = useLocale();
82
+ const t = () => aiChatMessages(locale());
83
83
  const status = () => props.block.status;
84
84
  const description = () => {
85
- if (status() === "completed") return "Context compacted";
86
- if (status() === "skipped") return "No-op";
87
- if (status() === "failed") return "Compaction failed";
88
- return "Compacting context";
85
+ if (status() === "completed") return t().compacted;
86
+ if (status() === "skipped") return t().compactionSkipped;
87
+ if (status() === "failed") return t().compactionFailed;
88
+ return t().compacting;
89
89
  };
90
90
 
91
91
  return (
92
- <Show when={status() !== "running"} fallback={<Chat.Activity label="Compacting context" icon="ti ti-brain" tone="ai" busy />}>
92
+ <Show when={status() !== "running"} fallback={<Chat.Activity label={t().compacting} icon="ti ti-brain" tone="ai" busy />}>
93
93
  <Chat.Activity
94
- label="Show compaction"
94
+ label={t().showCompaction}
95
95
  description={description()}
96
96
  icon="ti ti-brain"
97
97
  tone={status() === "failed" ? "danger" : "ai"}
98
98
  bodyInset={false}
99
99
  >
100
100
  <div class="w-full min-w-0 rounded-md bg-zinc-100/70 p-2 text-[11px] leading-5 text-secondary [box-shadow:var(--ui-control-recess)] dark:bg-zinc-950/70">
101
- <Show when={props.block.result} fallback={<p>Older chat context was summarized into compact conversation memory.</p>}>
101
+ <Show when={props.block.result} fallback={<p>{t().compactionSummary}</p>}>
102
102
  {(compactResult) => (
103
103
  <dl class="grid grid-cols-2 gap-2">
104
104
  <div class="rounded-md bg-white/65 px-2 py-1 dark:bg-white/5">
105
- <dt class="uppercase tracking-wide text-dimmed">Before</dt>
105
+ <dt class="uppercase tracking-wide text-dimmed">{t().before}</dt>
106
106
  <dd class="font-medium text-primary">{compactResult().entriesBefore.toLocaleString(locale())}</dd>
107
107
  </div>
108
108
  <div class="rounded-md bg-white/65 px-2 py-1 dark:bg-white/5">
109
- <dt class="uppercase tracking-wide text-dimmed">After</dt>
109
+ <dt class="uppercase tracking-wide text-dimmed">{t().after}</dt>
110
110
  <dd class="font-medium text-primary">{compactResult().entriesAfter.toLocaleString(locale())}</dd>
111
111
  </div>
112
112
  </dl>
@@ -158,11 +158,13 @@ function ToolResultDisclosure(props: {
158
158
  errorLabel?: string;
159
159
  errorDescription?: string;
160
160
  }) {
161
+ const locale = useLocale();
162
+ const t = () => aiChatMessages(locale());
161
163
  return (
162
164
  <AiToolActivity
163
165
  blockId={props.blockId}
164
166
  icon={props.icon ?? (props.isError ? "ti ti-alert-circle" : aiToolIcon(props.toolName))}
165
- label={props.isError ? (props.errorLabel ?? `${props.labelOnError ?? props.name} failed`) : props.name}
167
+ label={props.isError ? (props.errorLabel ?? t().toolFailed({ title: props.labelOnError ?? props.name })) : props.name}
166
168
  description={props.isError ? props.errorDescription : undefined}
167
169
  tone={props.isError ? "danger" : "neutral"}
168
170
  accent={props.accent}
@@ -171,16 +173,18 @@ function ToolResultDisclosure(props: {
171
173
  >
172
174
  <div class="flex w-full min-w-0 flex-col gap-2">
173
175
  <Show when={props.args !== undefined}>
174
- <ToolDetail title="Input" toolName={props.toolName} value={props.args} />
176
+ <ToolDetail title={t().input} toolName={props.toolName} value={props.args} />
175
177
  </Show>
176
- <ToolDetail title="Response" toolName={props.toolName} value={props.result} />
178
+ <ToolDetail title={t().response} toolName={props.toolName} value={props.result} />
177
179
  </div>
178
180
  </AiToolActivity>
179
181
  );
180
182
  }
181
183
 
182
- function ApprovalBlockView(props: { turnId: string; block: ToolBlock }) {
184
+ export function ApprovalBlockView(props: { turnId: string; block: ToolBlock }) {
183
185
  const actions = useAiChatActions();
186
+ const locale = useLocale();
187
+ const t = () => aiChatMessages(locale());
184
188
  const request = () => ({ turnId: props.turnId, callId: props.block.callId, name: props.block.name });
185
189
  const pending = () => props.block.status === "awaiting_approval";
186
190
  const actionDisabled = () => actions.actionDisabled?.() ?? false;
@@ -197,14 +201,17 @@ function ApprovalBlockView(props: { turnId: string; block: ToolBlock }) {
197
201
  if (!actionDisabled() && !approval.loading()) void approval.mutate(input);
198
202
  };
199
203
  const title = () => props.block.presentation?.title ?? displayToolName(props.block.name);
200
- const ownerName = () => props.block.presentation?.appName ?? "Assistant";
204
+ const ownerName = () => props.block.presentation?.appName ?? t().assistant;
201
205
  const description = () => {
202
206
  const reviewMessage = props.block.approval?.review?.message.trim();
203
207
  if (reviewMessage) return reviewMessage;
204
208
  const message = props.block.approval?.message?.trim();
205
209
  if (!message) return null;
206
- const duplicateTitle = props.block.presentation ? `${props.block.presentation.appName}: ${props.block.presentation.title}\n` : "";
207
- const withoutDuplicateTitle = message.startsWith(duplicateTitle) ? message.slice(duplicateTitle.length).trim() : message;
210
+ const duplicateTitle = props.block.presentation ? `${props.block.presentation.appName}: ${props.block.presentation.title}` : "";
211
+ const withoutDuplicateTitle =
212
+ duplicateTitle && (message === duplicateTitle || message.startsWith(`${duplicateTitle}\n`))
213
+ ? message.slice(duplicateTitle.length).trim()
214
+ : message;
208
215
  if (/^Review the validated arguments below before running this action\.$/i.test(withoutDuplicateTitle)) return null;
209
216
  return withoutDuplicateTitle || null;
210
217
  };
@@ -223,11 +230,11 @@ function ApprovalBlockView(props: { turnId: string; block: ToolBlock }) {
223
230
  const reviewLinks = () => props.block.approval?.review?.links ?? [];
224
231
  const reviewLinkTitle = (rel: "open" | "edit" | "status" | "preview" | "download") =>
225
232
  ({
226
- open: `Open in ${ownerName()}`,
227
- edit: `Edit in ${ownerName()}`,
228
- status: `View status in ${ownerName()}`,
229
- preview: "Preview",
230
- download: "Download",
233
+ open: t().openIn({ app: ownerName() }),
234
+ edit: t().editIn({ app: ownerName() }),
235
+ status: t().statusIn({ app: ownerName() }),
236
+ preview: t().preview,
237
+ download: t().download,
231
238
  })[rel];
232
239
  const detailData = () => (isStructuredDataValue(props.block.args) ? props.block.args : undefined);
233
240
  const appAccent = () => props.block.presentation?.appAccent;
@@ -237,7 +244,7 @@ function ApprovalBlockView(props: { turnId: string; block: ToolBlock }) {
237
244
  <section
238
245
  class={`w-full overflow-hidden rounded-xl border border-[var(--k2b-border)] bg-[var(--k2b-surface)] text-sm text-primary ${appAccent() ? "app-accent-scope" : ""}`}
239
246
  style={{ "--app-accent": appAccent() }}
240
- aria-label={`Approval required: ${title()}`}
247
+ aria-label={t().approvalRequired({ title: title() })}
241
248
  >
242
249
  <div class="p-4">
243
250
  <div class="flex min-w-0 items-center gap-3">
@@ -257,7 +264,7 @@ function ApprovalBlockView(props: { turnId: string; block: ToolBlock }) {
257
264
  <h3 class="truncate text-sm font-semibold leading-5 text-primary">
258
265
  {ownerName()} · {title()}
259
266
  </h3>
260
- <p class="text-xs text-dimmed">Action</p>
267
+ <p class="text-xs text-dimmed">{t().action}</p>
261
268
  </div>
262
269
  </div>
263
270
  <Show when={reviewLines().length > 0}>
@@ -300,7 +307,7 @@ function ApprovalBlockView(props: { turnId: string; block: ToolBlock }) {
300
307
  class="max-h-72 overflow-auto whitespace-pre-wrap break-words pr-2 font-sans text-xs leading-5 text-secondary"
301
308
  role="region"
302
309
  tabIndex={0}
303
- aria-label={`${detail.label} content`}
310
+ aria-label={t().contentOf({ label: detail.label })}
304
311
  >
305
312
  <ReviewDetailValue detail={detail} />
306
313
  </pre>
@@ -321,7 +328,7 @@ function ApprovalBlockView(props: { turnId: string; block: ToolBlock }) {
321
328
  data-ai-approval-footer
322
329
  >
323
330
  <Show when={reviewLinks().length > 0}>
324
- <nav class="flex flex-wrap gap-1" aria-label={`${title()} links`}>
331
+ <nav class="flex flex-wrap gap-1" aria-label={t().linksFor({ title: title() })}>
325
332
  <For each={reviewLinks()}>
326
333
  {(link) => (
327
334
  <ButtonLink href={link.href} target="_blank" rel="noopener noreferrer" size="xs" variant="ghost">
@@ -332,24 +339,24 @@ function ApprovalBlockView(props: { turnId: string; block: ToolBlock }) {
332
339
  </nav>
333
340
  </Show>
334
341
  <Show when={approval.error()}>
335
- <p class="text-xs text-red-700 dark:text-red-300">Could not submit. Try again.</p>
342
+ <p class="text-xs text-red-700 dark:text-red-300">{t().approvalFailed}</p>
336
343
  </Show>
337
344
  <div class="ml-auto shrink-0">
338
345
  <Show
339
346
  when={pending()}
340
347
  fallback={
341
348
  <span class="text-xs font-medium text-secondary">
342
- {title()} · {props.block.status === "rejected" ? "Rejected" : "Approved"}
349
+ {title()} · {props.block.status === "rejected" ? t().rejected : t().approved}
343
350
  </span>
344
351
  }
345
352
  >
346
- <Show when={actions.onApproval} fallback={<span class="text-xs font-medium text-secondary">Approval unavailable</span>}>
353
+ <Show when={actions.onApproval} fallback={<span class="text-xs font-medium text-secondary">{t().approvalUnavailable}</span>}>
347
354
  <Show
348
355
  when={!actionDisabled()}
349
356
  fallback={
350
357
  <span class="inline-flex items-center gap-1 text-xs font-medium text-secondary">
351
358
  <i class="ti ti-player-stop" aria-hidden="true" />
352
- Stopping response
359
+ {t().stoppingResponse}
353
360
  </span>
354
361
  }
355
362
  >
@@ -358,30 +365,30 @@ function ApprovalBlockView(props: { turnId: string; block: ToolBlock }) {
358
365
  fallback={
359
366
  <span class="inline-flex items-center gap-1 text-xs font-medium text-secondary">
360
367
  <i class={`ti ${submitted() ? "ti-check" : "ti-loader-2 animate-spin"}`} aria-hidden="true" />
361
- {submitted() ? "Submitted" : "Submitting"}
368
+ {submitted() ? t().submitted : t().submitting}
362
369
  </span>
363
370
  }
364
371
  >
365
372
  <div class="flex flex-wrap justify-end gap-1">
366
373
  <Button size="xs" variant="ghost" onClick={() => submit({ approved: false })}>
367
- Reject
374
+ {t().reject}
368
375
  </Button>
369
376
  <SplitButton
370
377
  size="xs"
371
378
  variant="ai"
372
379
  onClick={() => submit({ approved: true })}
373
- menuLabel={`More options for ${title()}`}
380
+ menuLabel={t().moreOptions({ title: title() })}
374
381
  menuPosition="bottom-right"
375
382
  items={[
376
383
  {
377
- label: detailsOpen() ? "Hide details" : "Details",
384
+ label: detailsOpen() ? t().hideDetails : t().details,
378
385
  icon: detailsOpen() ? "ti ti-eye-off" : "ti ti-eye",
379
386
  action: () => setDetailsOpen((open) => !open),
380
387
  },
381
388
  ...(props.block.approval?.allowAlways
382
389
  ? [
383
390
  {
384
- label: "Always approve",
391
+ label: t().alwaysApprove,
385
392
  icon: "ti ti-shield-check",
386
393
  action: () => submit({ approved: true, remember: "always" }),
387
394
  },
@@ -400,7 +407,7 @@ function ApprovalBlockView(props: { turnId: string; block: ToolBlock }) {
400
407
  </footer>
401
408
  </section>
402
409
  <Show when={detailsOpen()}>
403
- <div id={detailsId} class="mt-2 w-full" role="region" aria-label={`${title()} details`}>
410
+ <div id={detailsId} class="mt-2 w-full" role="region" aria-label={t().detailsFor({ title: title() })}>
404
411
  <Show
405
412
  when={detailData()}
406
413
  fallback={
@@ -418,6 +425,8 @@ function ApprovalBlockView(props: { turnId: string; block: ToolBlock }) {
418
425
  }
419
426
 
420
427
  function CapabilityToolView(props: { block: ToolBlock }) {
428
+ const locale = useLocale();
429
+ const t = () => aiChatMessages(locale());
421
430
  const presentation = () => props.block.presentation!;
422
431
  const label = () => presentation().title;
423
432
  const result = () => (isRecord(props.block.result) ? props.block.result : null);
@@ -453,8 +462,8 @@ function CapabilityToolView(props: { block: ToolBlock }) {
453
462
  fallback={
454
463
  <Chat.Activity
455
464
  icon={aiToolIcon(props.block.name, presentation().appIcon)}
456
- label={`${label()} failed`}
457
- description={capabilityErrorDescription(props.block.result)}
465
+ label={t().toolFailed({ title: label() })}
466
+ description={capabilityErrorDescription(props.block.result) || t().actionNotCompleted}
458
467
  tone="danger"
459
468
  accent={presentation().appAccent}
460
469
  />
@@ -496,10 +505,10 @@ function CapabilityToolView(props: { block: ToolBlock }) {
496
505
  {typeof link.title === "string"
497
506
  ? link.title
498
507
  : link.rel === "edit"
499
- ? "Edit"
508
+ ? t().edit
500
509
  : link.rel === "download"
501
- ? "Download"
502
- : "Open"}
510
+ ? t().download
511
+ : t().open}
503
512
  </ButtonLink>
504
513
  )}
505
514
  </For>
@@ -515,19 +524,21 @@ function CapabilityToolView(props: { block: ToolBlock }) {
515
524
  }
516
525
 
517
526
  function RejectedToolView(props: { block: ToolBlock }) {
527
+ const locale = useLocale();
518
528
  const presentation = () => props.block.presentation;
519
529
  const title = () => presentation()?.title ?? displayToolName(props.block.name);
520
530
  return (
521
531
  <Chat.Activity
522
532
  icon={aiToolIcon(props.block.name, presentation()?.appIcon)}
523
- label={`${title()} was rejected`}
533
+ label={aiChatMessages(locale()).toolRejected({ title: title() })}
524
534
  accent={presentation()?.appAccent}
525
535
  />
526
536
  );
527
537
  }
528
538
 
529
- function SurveyToolView(props: { turnId: string; block: ToolBlock; active?: boolean }) {
539
+ export function SurveyToolView(props: { turnId: string; block: ToolBlock; active?: boolean }) {
530
540
  const actions = useAiChatActions();
541
+ const locale = useLocale();
531
542
  const request = () => ({ turnId: props.turnId, callId: props.block.callId, name: props.block.name });
532
543
  const submit = actions.onFrontendToolResult;
533
544
  const submittedResult = () =>
@@ -538,7 +549,7 @@ function SurveyToolView(props: { turnId: string; block: ToolBlock; active?: bool
538
549
  <CloudSurveyBlock
539
550
  args={props.block.args}
540
551
  disabled={!submit || actions.actionDisabled?.()}
541
- disabledLabel={actions.actionDisabled?.() ? "Stopping response." : undefined}
552
+ disabledLabel={actions.actionDisabled?.() ? aiChatMessages(locale()).stoppingResponse : undefined}
542
553
  onSubmit={submit ? (result) => submit(request(), result) : undefined}
543
554
  />
544
555
  </Match>
@@ -554,8 +565,10 @@ function SurveyToolView(props: { turnId: string; block: ToolBlock; active?: bool
554
565
  );
555
566
  }
556
567
 
557
- function TextEditorToolView(props: { turnId: string; block: ToolBlock; active?: boolean }) {
568
+ export function TextEditorToolView(props: { turnId: string; block: ToolBlock; active?: boolean }) {
558
569
  const actions = useAiChatActions();
570
+ const locale = useLocale();
571
+ const t = () => aiChatMessages(locale());
559
572
  const request = () => ({ turnId: props.turnId, callId: props.block.callId, name: props.block.name });
560
573
  const submit = actions.onFrontendToolResult;
561
574
  const completedResult = () =>
@@ -570,7 +583,7 @@ function TextEditorToolView(props: { turnId: string; block: ToolBlock; active?:
570
583
  fallback={
571
584
  <ToolResultDisclosure
572
585
  blockId={props.block.id}
573
- name="text editor"
586
+ name={t().textEditor}
574
587
  toolName={props.block.name}
575
588
  args={props.block.args}
576
589
  result={props.block.result}
@@ -579,13 +592,13 @@ function TextEditorToolView(props: { turnId: string; block: ToolBlock; active?:
579
592
  }
580
593
  >
581
594
  <Match when={props.block.status === "running"}>
582
- <Chat.Activity label="Preparing text editor" icon="ti ti-edit" tone="ai" busy />
595
+ <Chat.Activity label={t().preparingTextEditor} icon="ti ti-edit" tone="ai" busy />
583
596
  </Match>
584
597
  <Match when={props.block.status === "awaiting_client"}>
585
598
  <CloudTextEditorBlock
586
599
  args={props.block.args}
587
600
  disabled={!submit || actions.actionDisabled?.()}
588
- disabledLabel={actions.actionDisabled?.() ? "Stopping response." : undefined}
601
+ disabledLabel={actions.actionDisabled?.() ? t().stoppingResponse : undefined}
589
602
  onSubmit={submit ? (result) => submit(request(), result) : undefined}
590
603
  />
591
604
  </Match>
@@ -599,15 +612,17 @@ function TextEditorToolView(props: { turnId: string; block: ToolBlock; active?:
599
612
  }
600
613
 
601
614
  function MemoryToolView(props: { block: ToolBlock }) {
602
- const presentation = () => memoryToolPresentation(props.block.args, props.block.result);
615
+ const locale = useLocale();
616
+ const t = () => aiChatMessages(locale());
617
+ const presentation = () => memoryToolPresentation(props.block.args, props.block.result, locale());
603
618
  return (
604
- <Show when={props.block.status !== "running"} fallback={<Chat.Activity label="Using memory" icon="ti ti-brain" tone="ai" busy />}>
619
+ <Show when={props.block.status !== "running"} fallback={<Chat.Activity label={t().usingMemory} icon="ti ti-brain" tone="ai" busy />}>
605
620
  <Show
606
621
  when={presentation()}
607
622
  fallback={
608
623
  <ToolResultDisclosure
609
624
  blockId={props.block.id}
610
- name="Memory"
625
+ name={t().memory}
611
626
  toolName={props.block.name}
612
627
  args={props.block.args}
613
628
  result={props.block.result}
@@ -630,9 +645,10 @@ function MemoryToolView(props: { block: ToolBlock }) {
630
645
 
631
646
  function ToolBlockView(props: { turnId: string; block: ToolBlock; active?: boolean }) {
632
647
  const actions = useAiChatActions();
648
+ const locale = useLocale();
633
649
  const status = () => props.block.status;
634
650
  const fetchError = () =>
635
- props.block.name === "fetch_file" && props.block.isError ? fetchFileErrorPresentation(props.block.result) : undefined;
651
+ props.block.name === "fetch_file" && props.block.isError ? fetchFileErrorPresentation(props.block.result, locale()) : undefined;
636
652
  return (
637
653
  <>
638
654
  <Switch
@@ -661,7 +677,7 @@ function ToolBlockView(props: { turnId: string; block: ToolBlock; active?: boole
661
677
  <Match
662
678
  when={actions.renderCodePresentation && props.block.name === "code_present" && status() === "completed" && !props.block.isError}
663
679
  >
664
- {actions.renderCodePresentation?.(props.block.result)}
680
+ {untrack(() => actions.renderCodePresentation?.(() => props.block.result))}
665
681
  </Match>
666
682
  <Match when={props.block.name === "present" && !props.block.isError}>
667
683
  <PresentToolBlock block={props.block} />
@@ -716,7 +732,7 @@ export function AiTurnBlockView(props: { block: AiTurnBlock; turnId: string; str
716
732
  {(block) => <ThinkingBlockView text={block().text} streaming={props.streaming} />}
717
733
  </Match>
718
734
  <Match when={props.block.kind === "steer_applied"}>
719
- <Chat.Activity label="Conversation steered" icon="ti ti-route" tone="ai" />
735
+ <Chat.Activity label={aiChatMessages(locale()).conversationSteered} icon="ti ti-route" tone="ai" />
720
736
  </Match>
721
737
  <Match when={props.block.kind === "tool" && props.block}>
722
738
  {(block) => <ToolBlockView turnId={props.turnId} block={block()} active={props.active} />}
@@ -726,12 +742,15 @@ export function AiTurnBlockView(props: { block: AiTurnBlock; turnId: string; str
726
742
  );
727
743
  }
728
744
 
729
- function CompactToolRow(props: { block: ToolBlock; busy?: boolean }) {
745
+ /** One step in the expanded work list: its title, target, and state, with input and output on demand. */
746
+ export function CompactToolRow(props: { block: ToolBlock; busy?: boolean }) {
730
747
  const locale = useLocale();
731
- const de = () => locale().startsWith("de");
748
+ const t = () => aiChatMessages(locale());
732
749
  const args = () => (isRecord(props.block.args) ? props.block.args : {});
733
- const detail = () => [args().path, args().name, args().query].find((value) => typeof value === "string");
734
- const waiting = () => props.block.status === "awaiting_client";
750
+ const detail = () => [args().path, args().name, args().query, args().title].find((value) => typeof value === "string");
751
+ const waiting = () => props.block.status === "awaiting_client" || props.block.status === "awaiting_approval";
752
+ const state = () =>
753
+ waiting() ? t().stepWaiting : props.block.status === "rejected" ? t().stepRejected : isFailedTool(props.block) ? t().stepFailed : "";
735
754
  return (
736
755
  <AiToolActivity
737
756
  blockId={props.block.id}
@@ -739,135 +758,23 @@ function CompactToolRow(props: { block: ToolBlock; busy?: boolean }) {
739
758
  description={[
740
759
  props.block.status === "running" ? (props.block.progress ?? "") : "",
741
760
  typeof detail() === "string" ? String(detail()) : "",
742
- waiting() ? (de() ? "Wartet" : "Waiting") : isFailedTool(props.block) ? (de() ? "Fehlgeschlagen" : "Failed") : "",
761
+ state(),
743
762
  ]
744
763
  .filter(Boolean)
745
764
  .join(" · ")}
746
765
  icon={waiting() ? "ti ti-clock" : aiToolIcon(props.block.name, props.block.presentation?.appIcon)}
747
- tone={isFailedTool(props.block) ? "danger" : "neutral"}
748
- busy={props.busy && !waiting()}
749
- bodyInset={false}
766
+ busy={props.busy && props.block.status === "running"}
750
767
  renderBody={() => (
751
768
  <div
752
769
  class="max-h-72 overflow-auto overscroll-contain rounded-md bg-zinc-100 p-3 dark:bg-zinc-950"
753
770
  tabIndex={0}
754
771
  role="region"
755
- aria-label={de() ? "Werkzeug-Eingabe und Ausgabe" : "Tool input and output"}
772
+ aria-label={t().toolInputOutput}
756
773
  >
757
- <ToolDetail title={de() ? "Eingabe" : "Input"} toolName={props.block.name} value={props.block.args} />
758
- <ToolDetail title={de() ? "Ausgabe" : "Output"} toolName={props.block.name} value={props.block.result} />
774
+ <ToolDetail title={t().input} toolName={props.block.name} value={props.block.args} />
775
+ <ToolDetail title={t().output} toolName={props.block.name} value={props.block.result} />
759
776
  </div>
760
777
  )}
761
778
  />
762
779
  );
763
780
  }
764
-
765
- function ToolGroupView(props: { blocks: ToolBlock[]; active: boolean }) {
766
- const locale = useLocale();
767
- const last = () => props.blocks.at(-1)!;
768
- const waiting = () => last().status === "awaiting_client";
769
- const multiple = () => props.blocks.length > 1;
770
- const detail = () => {
771
- const args = last().args;
772
- return isRecord(args) ? [args.path, args.name, args.query].find((value) => typeof value === "string") : undefined;
773
- };
774
- return (
775
- <AiToolActivity
776
- blockId={`group:${props.blocks[0]!.id}`}
777
- bodyInset={false}
778
- label={
779
- props.active || !multiple()
780
- ? (last().presentation?.title ?? displayToolName(last().name))
781
- : summarizeToolGroup(props.blocks, locale())
782
- }
783
- description={
784
- waiting()
785
- ? locale().startsWith("de")
786
- ? "Wartet"
787
- : "Waiting"
788
- : last().status === "running"
789
- ? last().progress
790
- : !multiple() && typeof detail() === "string"
791
- ? String(detail())
792
- : undefined
793
- }
794
- icon={props.active || !multiple() ? (waiting() ? "ti ti-clock" : aiToolIcon(last().name)) : "ti ti-stack"}
795
- tone={!multiple() && isFailedTool(last()) ? "danger" : "neutral"}
796
- busy={props.active && !waiting()}
797
- renderBody={() => (
798
- <Show
799
- when={multiple()}
800
- fallback={
801
- <div
802
- class="max-h-72 overflow-auto overscroll-contain rounded-md bg-zinc-100 p-3 dark:bg-zinc-950"
803
- tabIndex={0}
804
- role="region"
805
- aria-label={locale().startsWith("de") ? "Werkzeug-Eingabe und Ausgabe" : "Tool input and output"}
806
- >
807
- <ToolDetail title={locale().startsWith("de") ? "Eingabe" : "Input"} toolName={last().name} value={last().args} />
808
- <ToolDetail title={locale().startsWith("de") ? "Ausgabe" : "Output"} toolName={last().name} value={last().result} />
809
- </div>
810
- }
811
- >
812
- <For each={props.blocks}>{(block) => <CompactToolRow block={block} />}</For>
813
- </Show>
814
- )}
815
- />
816
- );
817
- }
818
-
819
- export function AiTurnBlockList(props: {
820
- blocks: AiTurnBlock[];
821
- turnId: string;
822
- streaming?: boolean;
823
- compact?: boolean;
824
- active?: boolean;
825
- disclosureState?: AiToolDisclosureState;
826
- }) {
827
- const groups = createMemo(() => groupToolBlocks(props.blocks.filter(isRenderableTurnBlock)));
828
- const keyedGroups = createMemo(
829
- () => new Map(groups().map((group) => [group.kind === "tools" ? `tools:${group.blocks[0]!.id}` : `block:${group.block.id}`, group])),
830
- );
831
- const keys = createMemo(() => [...keyedGroups().keys()]);
832
- const disclosureState = props.disclosureState ?? createAiToolDisclosureState();
833
- return (
834
- <Show when={groups().length > 0}>
835
- <AiToolDisclosureProvider state={disclosureState}>
836
- <div class={`flex flex-col ${props.compact ? "gap-1" : "gap-2"}`}>
837
- <For each={keys()}>
838
- {(key, index) => {
839
- const group = () => keyedGroups().get(key)!;
840
- return (
841
- <Switch>
842
- <Match
843
- when={(() => {
844
- const value = group();
845
- return value.kind === "tools" ? value : undefined;
846
- })()}
847
- >
848
- {(tools) => <ToolGroupView blocks={tools().blocks} active={Boolean(props.active && index() === keys().length - 1)} />}
849
- </Match>
850
- <Match
851
- when={(() => {
852
- const value = group();
853
- return value.kind === "block" ? value : undefined;
854
- })()}
855
- >
856
- {(item) => (
857
- <AiTurnBlockView
858
- block={item().block}
859
- turnId={props.turnId}
860
- streaming={props.streaming && index() === keys().length - 1}
861
- active={props.active}
862
- />
863
- )}
864
- </Match>
865
- </Switch>
866
- );
867
- }}
868
- </For>
869
- </div>
870
- </AiToolDisclosureProvider>
871
- </Show>
872
- );
873
- }