@k2b/cloud 0.29.0 → 0.30.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 (92) hide show
  1. package/package.json +5 -3
  2. package/src/_internal/app-roles.ts +14 -0
  3. package/src/_internal/capabilities.ts +111 -3
  4. package/src/_internal/capability-sentences.ts +413 -0
  5. package/src/_internal/define-app.ts +10 -1
  6. package/src/_internal/registry-validation.ts +5 -0
  7. package/src/access/PermissionEditor.tsx +43 -24
  8. package/src/ai/app-skill-store.ts +298 -0
  9. package/src/ai/app-skills.ts +105 -0
  10. package/src/ai/browser.ts +10 -0
  11. package/src/ai/capabilities.ts +68 -9
  12. package/src/ai/chart-block.ts +380 -0
  13. package/src/ai/chat/action-sentences.ts +49 -0
  14. package/src/ai/chat/blocks.tsx +203 -185
  15. package/src/ai/chat/chart-block.tsx +79 -0
  16. package/src/ai/chat/live-turn.browser-harness.tsx +5 -1
  17. package/src/ai/chat/message-utils.ts +3 -0
  18. package/src/ai/chat/messages.ts +12 -26
  19. package/src/ai/chat/tool-disclosure.tsx +21 -14
  20. package/src/ai/chat/tool-groups.ts +64 -37
  21. package/src/ai/chat/tool-names.ts +2 -0
  22. package/src/ai/chat/turn-layout.ts +4 -4
  23. package/src/ai/chat/turn-view.tsx +141 -69
  24. package/src/ai/code-runtime-tools.ts +59 -44
  25. package/src/ai/default-tool-contracts.ts +198 -0
  26. package/src/ai/default-tools.ts +25 -1
  27. package/src/ai/executor.ts +19 -21
  28. package/src/ai/index.ts +9 -1
  29. package/src/ai/migrate.ts +59 -12
  30. package/src/ai/protocol.ts +9 -4
  31. package/src/ai/runtime-tools.ts +1 -0
  32. package/src/ai/skill-content.ts +54 -0
  33. package/src/ai/skills-routes.ts +22 -13
  34. package/src/ai/skills.ts +177 -231
  35. package/src/ai/store.ts +20 -13
  36. package/src/ai/tool-sentences.ts +38 -0
  37. package/src/ai/types.ts +10 -2
  38. package/src/api/admin-ai-skills.ts +35 -16
  39. package/src/api/admin-outgoing-mail.ts +18 -0
  40. package/src/api/fanout.ts +128 -0
  41. package/src/api/help.ts +8 -3
  42. package/src/api/mcp.ts +4 -1
  43. package/src/api/search.ts +31 -106
  44. package/src/api/widgets.ts +259 -100
  45. package/src/browser/CloudResourceSearch.browser-harness.tsx +1 -1
  46. package/src/browser/ndjson.ts +34 -0
  47. package/src/browser/reload.ts +46 -0
  48. package/src/browser/search-stream.ts +4 -26
  49. package/src/browser/widgets.ts +28 -0
  50. package/src/capabilities/server.ts +20 -2
  51. package/src/cli/access.ts +48 -14
  52. package/src/cli/admin/ai-skills.ts +59 -39
  53. package/src/cli/admin/outgoing-mail.ts +30 -2
  54. package/src/contracts/announcement-cookie.ts +62 -0
  55. package/src/contracts/announcements.ts +14 -52
  56. package/src/contracts/capabilities.ts +24 -0
  57. package/src/contracts/outgoing-mail.ts +56 -11
  58. package/src/contracts/pwa-paths.ts +7 -2
  59. package/src/contracts/pwa.ts +2 -7
  60. package/src/contracts/registry.ts +1 -0
  61. package/src/contracts/shared.ts +5 -0
  62. package/src/contracts/widgets.ts +20 -0
  63. package/src/index.ts +1 -0
  64. package/src/server/help.ts +8 -1
  65. package/src/services/accounts/users.ts +15 -0
  66. package/src/services/help/index.ts +18 -5
  67. package/src/services/help/types.ts +3 -2
  68. package/src/services/identity/constants.ts +2 -0
  69. package/src/services/identity/key-ring.ts +123 -42
  70. package/src/services/identity/runtime-config.ts +13 -2
  71. package/src/services/outbox.ts +24 -9
  72. package/src/services/outgoing-mail/index.ts +33 -5
  73. package/src/services/outgoing-mail/messages.ts +60 -19
  74. package/src/services/outgoing-mail/store.ts +63 -4
  75. package/src/shared/network-address.ts +1 -0
  76. package/src/ssr/AppLaunchpadPanel.tsx +12 -3
  77. package/src/ssr/GlobalAnnouncements.island.tsx +2 -2
  78. package/src/ssr/GlobalSearchDialog.tsx +1 -43
  79. package/src/ssr/GlobalSearchTrigger.island.tsx +12 -3
  80. package/src/ssr/LayoutHelpTrigger.island.tsx +1 -1
  81. package/src/ssr/PwaRuntime.island.tsx +1 -1
  82. package/src/ssr/app-navigation.ts +5 -7
  83. package/src/ssr/global-search-host.tsx +55 -0
  84. package/src/ssr/help.ts +3 -3
  85. package/src/ssr/rail-context.ts +4 -3
  86. package/src/ssr/runtime.ts +14 -1
  87. package/src/styles/effects.css +142 -0
  88. package/src/styles/utilities-markdown-table.css +5 -1
  89. package/src/ai/code-mode-skill.ts +0 -114
  90. package/src/ai/data-analysis-skill.ts +0 -10
  91. package/src/ai/grids-skill.ts +0 -158
  92. package/src/ai/skill-seeds.ts +0 -560
@@ -9,6 +9,7 @@ const messages = i18n.define({
9
9
  tableLinks: "Links",
10
10
  tableOpen: "Open",
11
11
  tableEmpty: "No matching rows.",
12
+ chartUnavailable: "This chart cannot be shown.",
12
13
  tableCapped: "Showing the first 100 returned rows. The full tool result remains available in details.",
13
14
  tableMore: "More rows are available. Ask for the next page or open the result in the application.",
14
15
  assistantTurns: "Assistant turns",
@@ -84,6 +85,7 @@ const messages = i18n.define({
84
85
  stepDeliveringFile: "Delivering a file",
85
86
  stepPreparingView: "Preparing a view",
86
87
  stepPreparingCard: "Preparing a card",
88
+ stepPreparingChart: "Preparing a chart",
87
89
  stepCompacting: "Summarizing the conversation",
88
90
  stepTool: "Using a tool",
89
91
  stepFailed: "failed",
@@ -95,33 +97,24 @@ const messages = i18n.define({
95
97
  checkWarnings: ({ count }: { count: number }) => (count === 1 ? "1 warning" : `${count} warnings`),
96
98
  openImage: ({ name }: { name: string }) => `Open image ${name}`,
97
99
  steered: "Conversation steered",
98
- reasoning: "Reasoning",
99
- groupPlan: "Updated working plan",
100
- groupCode: "Worked with code",
101
- groupCloud: "Used Cloud integration",
102
- groupReadFiles: "Read files",
103
- groupWriteFiles: "Wrote files",
104
- groupPdf: "Created PDFs",
105
100
  groupLoad: "Loaded tools and guidance",
106
- groupWeb: "Searched the web",
107
- groupMemory: "Used memory",
108
- groupDelivered: "Delivered results",
109
- groupTools: "Used tools",
110
101
  groupFailed: ({ count }: { count: number }) => `${count} failed`,
111
102
  groupRejected: ({ count }: { count: number }) => `${count} rejected`,
112
- receiptDone: ({ title }: { title: string }) => `${title} done`,
103
+ receiptDone: ({ title }: { title: string }) => `Done: ${title}`,
113
104
  receiptApproved: ({ title }: { title: string }) => `Approved: ${title}`,
105
+ receiptRunning: ({ title }: { title: string }) => `Running: ${title}`,
114
106
  approvalNeeded: ({ title }: { title: string }) => `Approval needed: ${title}`,
115
107
  answerReady: "Answer ready",
116
108
  receiptRejected: ({ title }: { title: string }) => `Rejected: ${title}`,
117
109
  receiptFailed: ({ title }: { title: string }) => `Failed: ${title}`,
118
110
  receiptNotRun: ({ title, stopped }: { title: string; stopped: boolean }) => `Not run: ${title}${stopped ? " · stopped" : ""}`,
111
+ receiptNotRunSentence: ({ sentence, stopped }: { sentence: string; stopped: boolean }) => `${sentence}${stopped ? " · stopped" : ""}`,
119
112
  workSteps: "Work steps",
120
113
  streamLoginRequired: "Your session has ended. Sign in again to continue this chat.",
121
114
  streamAccessDenied: "You no longer have access to this chat.",
122
115
  streamNotFound: "This chat is no longer available.",
123
116
  assistant: "Assistant",
124
- action: "Action",
117
+ approvalRunsAfter: "Runs only after you approve it",
125
118
  approvalRequired: ({ title }: { title: string }) => `Approval required: ${title}`,
126
119
  reject: "Reject",
127
120
  approved: "Approved",
@@ -300,6 +293,7 @@ const messages = i18n.define({
300
293
  tableLinks: "Links",
301
294
  tableOpen: "Öffnen",
302
295
  tableEmpty: "Keine passenden Zeilen.",
296
+ chartUnavailable: "Dieses Diagramm kann nicht angezeigt werden.",
303
297
  tableCapped:
304
298
  "Die ersten 100 zurückgegebenen Zeilen werden angezeigt. Das vollständige Werkzeugergebnis bleibt in den Details verfügbar.",
305
299
  tableMore: "Weitere Zeilen sind verfügbar. Frage nach der nächsten Seite oder öffne das Ergebnis in der Anwendung.",
@@ -382,6 +376,7 @@ const messages = i18n.define({
382
376
  stepDeliveringFile: "Stellt eine Datei bereit",
383
377
  stepPreparingView: "Bereitet eine Ansicht vor",
384
378
  stepPreparingCard: "Bereitet eine Karte vor",
379
+ stepPreparingChart: "Bereitet ein Diagramm vor",
385
380
  stepCompacting: "Fasst das Gespräch zusammen",
386
381
  stepTool: "Nutzt ein Werkzeug",
387
382
  stepFailed: "fehlgeschlagen",
@@ -393,33 +388,24 @@ const messages = i18n.define({
393
388
  checkWarnings: ({ count }) => (count === 1 ? "1 Warnung" : `${count} Warnungen`),
394
389
  openImage: ({ name }) => `Bild ${name} öffnen`,
395
390
  steered: "Richtung geändert",
396
- reasoning: "Überlegungen",
397
- groupPlan: "Arbeitsplan aktualisiert",
398
- groupCode: "Mit Code gearbeitet",
399
- groupCloud: "Cloud-Integration verwendet",
400
- groupReadFiles: "Dateien gelesen",
401
- groupWriteFiles: "Dateien geschrieben",
402
- groupPdf: "PDFs erstellt",
403
391
  groupLoad: "Werkzeuge und Wissen geladen",
404
- groupWeb: "Im Web recherchiert",
405
- groupMemory: "Gedächtnis genutzt",
406
- groupDelivered: "Ergebnisse bereitgestellt",
407
- groupTools: "Werkzeuge verwendet",
408
392
  groupFailed: ({ count }) => `${count} fehlgeschlagen`,
409
393
  groupRejected: ({ count }) => `${count} abgelehnt`,
410
- receiptDone: ({ title }) => `${title} erledigt`,
394
+ receiptDone: ({ title }) => `Erledigt: ${title}`,
411
395
  receiptApproved: ({ title }) => `Freigegeben: ${title}`,
396
+ receiptRunning: ({ title }) => `Wird ausgeführt: ${title}`,
412
397
  approvalNeeded: ({ title }) => `Freigabe nötig: ${title}`,
413
398
  answerReady: "Antwort fertig",
414
399
  receiptRejected: ({ title }) => `Abgelehnt: ${title}`,
415
400
  receiptFailed: ({ title }) => `Fehlgeschlagen: ${title}`,
416
401
  receiptNotRun: ({ title, stopped }) => `Nicht ausgeführt: ${title}${stopped ? " · gestoppt" : ""}`,
402
+ receiptNotRunSentence: ({ sentence, stopped }) => `${sentence}${stopped ? " · gestoppt" : ""}`,
417
403
  workSteps: "Arbeitsschritte",
418
404
  streamLoginRequired: "Deine Sitzung ist abgelaufen. Melde dich erneut an, um diesen Chat fortzusetzen.",
419
405
  streamAccessDenied: "Du hast keinen Zugriff mehr auf diesen Chat.",
420
406
  streamNotFound: "Dieser Chat ist nicht mehr verfügbar.",
421
407
  assistant: "Assistent",
422
- action: "Aktion",
408
+ approvalRunsAfter: "Wird erst nach deiner Freigabe ausgeführt",
423
409
  approvalRequired: ({ title }) => `Freigabe erforderlich: ${title}`,
424
410
  reject: "Ablehnen",
425
411
  approved: "Freigegeben",
@@ -1,28 +1,35 @@
1
1
  import { Chat, type ChatActivityProps } from "@k2b/ui";
2
- import { createContext, type JSX, splitProps, useContext } from "solid-js";
2
+ import { createContext, createSignal, type JSX, type Signal, splitProps, useContext } from "solid-js";
3
3
 
4
4
  export type AiToolDisclosureState = {
5
5
  get: (blockId: string) => boolean | undefined;
6
6
  set: (blockId: string, open: boolean) => void;
7
7
  };
8
8
 
9
+ /**
10
+ * Disclosure choices by block id. Reactive, so a disclosure that its reader opens renders its body at once, also when
11
+ * nothing else in the turn changes; choices survive a remount and, in session storage, a reload.
12
+ */
9
13
  export const createAiToolDisclosureState = (): AiToolDisclosureState => {
10
- const openByBlockId = new Map<string, boolean>();
14
+ const signals = new Map<string, Signal<boolean | undefined>>();
11
15
  const storageKey = (blockId: string) => `cloud.ai.tool-disclosure:${blockId}`;
16
+ const signal = (blockId: string): Signal<boolean | undefined> => {
17
+ const existing = signals.get(blockId);
18
+ if (existing) return existing;
19
+ let saved: boolean | undefined;
20
+ try {
21
+ if (window.sessionStorage.getItem(storageKey(blockId)) === "open") saved = true;
22
+ } catch {
23
+ /* SSR and disabled storage keep the in-memory state. */
24
+ }
25
+ const created = createSignal(saved);
26
+ signals.set(blockId, created);
27
+ return created;
28
+ };
12
29
  return {
13
- get: (blockId) => {
14
- if (!openByBlockId.has(blockId)) {
15
- try {
16
- const saved = window.sessionStorage.getItem(storageKey(blockId));
17
- if (saved === "open") openByBlockId.set(blockId, true);
18
- } catch {
19
- /* SSR and disabled storage keep the in-memory state. */
20
- }
21
- }
22
- return openByBlockId.get(blockId);
23
- },
30
+ get: (blockId) => signal(blockId)[0](),
24
31
  set: (blockId, open) => {
25
- openByBlockId.set(blockId, open);
32
+ signal(blockId)[1](open);
26
33
  try {
27
34
  if (open) window.sessionStorage.setItem(storageKey(blockId), "open");
28
35
  else window.sessionStorage.removeItem(storageKey(blockId));
@@ -1,10 +1,18 @@
1
+ import { aiSkillFilePathFromMount } from "../file-mount";
1
2
  import type { AiTurnBlock } from "../protocol";
2
3
  import { aiChatMessages } from "./messages";
3
4
 
4
5
  type Tool = Extract<AiTurnBlock, { kind: "tool" }>;
5
6
  type Text = Extract<AiTurnBlock, { kind: "text" }>;
6
7
  export type AiWorkEntry = Exclude<AiTurnBlock, { kind: "text" } | { kind: "steer_message" }>;
7
- export type AiWorkGroup = { kind: "text"; block: Text } | { kind: "steps"; id: string; entries: AiWorkEntry[] };
8
+ /**
9
+ * One row of the expanded work, all at one indent: an intermediate text, a step, or a group that folds housekeeping
10
+ * steps and opens in place.
11
+ */
12
+ export type AiWorkItem =
13
+ | { kind: "text"; block: Text }
14
+ | { kind: "step"; entry: AiWorkEntry }
15
+ | { kind: "housekeeping"; id: string; entries: AiWorkEntry[] };
8
16
 
9
17
  /** A failed call, including a code run that reported an error. A rejected approval is a decision, not a failure. */
10
18
  export function isFailedTool(tool: Tool): boolean {
@@ -20,46 +28,65 @@ export function isFailedTool(tool: Tool): boolean {
20
28
  );
21
29
  }
22
30
 
31
+ const HOUSEKEEPING_TOOLS = new Set([
32
+ "load_skill",
33
+ "search_skills",
34
+ "load_tools",
35
+ "search_tools",
36
+ "list_apps",
37
+ "search_help",
38
+ "read_help",
39
+ "search_project",
40
+ "read_project_knowledge",
41
+ ]);
42
+
43
+ /** Preparation the reader rarely needs: loading tools, skills, and knowledge, and reading a loaded skill's files. */
44
+ export const isHousekeepingTool = (tool: Tool): boolean => {
45
+ if (HOUSEKEEPING_TOOLS.has(tool.name)) return true;
46
+ if (tool.name !== "read_file" && tool.name !== "list_files") return false;
47
+ const path = tool.args !== null && typeof tool.args === "object" && "path" in tool.args ? tool.args.path : undefined;
48
+ return typeof path === "string" && aiSkillFilePathFromMount(path.trim()) !== null;
49
+ };
50
+
51
+ const isStep = (entry: AiWorkEntry) => entry.kind === "tool" || entry.kind === "compaction";
52
+
23
53
  /**
24
- * Split folded work into intermediate texts and the step groups between them. Reasoning stays inside its group so it
25
- * does not break up the tools around it. A group is keyed by its first tool, whose id is the same live and in history.
54
+ * The rows of the expanded work, in the original order. A run of at least two housekeeping steps, with the reasoning
55
+ * between them, folds into one group, unless it would hold all but one of the work's steps: then the steps show
56
+ * directly, so the work line never opens to a summary of itself. Entries listed in `shown` already show as rows of
57
+ * their own and never fold into a group, so nothing a reader sees folds away under them. A group is keyed by its first
58
+ * tool, whose id is the same live and in history.
26
59
  */
27
- export function groupWorkBlocks(blocks: readonly AiTurnBlock[]): AiWorkGroup[] {
28
- const groups: AiWorkGroup[] = [];
29
- for (const block of blocks) {
30
- if (block.kind === "steer_message") continue;
31
- if (block.kind === "text") {
32
- groups.push({ kind: "text", block });
33
- continue;
60
+ export function groupWorkBlocks(blocks: readonly AiTurnBlock[], shown: ReadonlySet<string> = new Set()): AiWorkItem[] {
61
+ const entries = blocks.filter((block): block is AiWorkEntry | Text => block.kind !== "steer_message");
62
+ const steps = entries.filter((entry) => entry.kind !== "text" && isStep(entry)).length;
63
+ const items: AiWorkItem[] = [];
64
+ let run: AiWorkEntry[] = [];
65
+ let trailing: AiWorkEntry[] = [];
66
+ const flush = () => {
67
+ if (run.length > 0) {
68
+ const runSteps = run.filter(isStep).length;
69
+ if (runSteps >= 2 && steps - runSteps >= 2) items.push({ kind: "housekeeping", id: run[0]!.id, entries: run });
70
+ else for (const entry of run) items.push({ kind: "step", entry });
71
+ }
72
+ // Reasoning after the last housekeeping step belongs to what comes next.
73
+ for (const entry of trailing) items.push({ kind: "step", entry });
74
+ run = [];
75
+ trailing = [];
76
+ };
77
+ for (const entry of entries) {
78
+ const foldable = !shown.has(entry.id);
79
+ if (foldable && entry.kind === "tool" && isHousekeepingTool(entry)) {
80
+ run.push(...trailing, entry);
81
+ trailing = [];
82
+ } else if (foldable && entry.kind === "thinking" && run.length > 0) trailing.push(entry);
83
+ else {
84
+ flush();
85
+ items.push(entry.kind === "text" ? { kind: "text", block: entry } : { kind: "step", entry });
34
86
  }
35
- const last = groups.at(-1);
36
- if (last?.kind === "steps") last.entries.push(block);
37
- else groups.push({ kind: "steps", id: block.id, entries: [block] });
38
- }
39
- for (const group of groups) {
40
- if (group.kind === "steps") group.id = (group.entries.find((entry) => entry.kind === "tool") ?? group.entries[0]!).id;
41
87
  }
42
- return groups;
43
- }
44
-
45
- /** What a group of tools did, in categories; failures and rejections are counted separately. */
46
- export function summarizeToolGroup(tools: readonly Tool[], locale: string): string {
47
- const t = aiChatMessages(locale);
48
- const categories = tools.map((tool) => {
49
- if (tool.name === "todo_write") return t.groupPlan;
50
- if (tool.name === "present" || tool.name === "code_present" || tool.name === "card" || tool.name === "cloud_card")
51
- return t.groupDelivered;
52
- if (tool.name.startsWith("code_")) return t.groupCode;
53
- if (tool.presentation?.kind === "capability" || tool.name === "read_cloud_resource") return t.groupCloud;
54
- if (["read_file", "list_files", "view_image"].includes(tool.name)) return t.groupReadFiles;
55
- if (tool.name === "write_file") return t.groupWriteFiles;
56
- if (["markdown_to_pdf", "html_to_pdf"].includes(tool.name)) return t.groupPdf;
57
- if (["load_skill", "load_tools", "search_tools", "list_apps"].includes(tool.name)) return t.groupLoad;
58
- if (tool.name.startsWith("web_") || tool.name === "fetch_file") return t.groupWeb;
59
- if (tool.name === "memory") return t.groupMemory;
60
- return t.groupTools;
61
- });
62
- return [...new Set(categories)].join(", ");
88
+ flush();
89
+ return items;
63
90
  }
64
91
 
65
92
  /** "9 steps · 1 failed · 1 rejected" for a group of steps. */
@@ -10,6 +10,7 @@ const toolNames = i18n.define({
10
10
  en: {
11
11
  calculate: "Calculate",
12
12
  card: "Card",
13
+ chart: "Chart",
13
14
  survey: "Survey",
14
15
  text_editor: "Text editor",
15
16
  local_bash: "Local Bash",
@@ -77,6 +78,7 @@ const toolNames = i18n.define({
77
78
  de: {
78
79
  calculate: "Rechnen",
79
80
  card: "Karte",
81
+ chart: "Diagramm",
80
82
  survey: "Umfrage",
81
83
  text_editor: "Texteditor",
82
84
  local_bash: "Lokale Bash",
@@ -1,7 +1,7 @@
1
1
  import { type AiTurnBlock, isRenderableTurnBlock } from "../protocol";
2
2
  import type { AiStoredMessage } from "../types";
3
3
  import { hasCapabilityTable } from "./capability-result";
4
- import { isCardToolName, isRecord, isSurveyToolName, isTextEditorToolName } from "./message-utils";
4
+ import { isCardToolName, isChartToolName, isRecord, isSurveyToolName, isTextEditorToolName } from "./message-utils";
5
5
  import { isFailedTool } from "./tool-groups";
6
6
 
7
7
  type ToolBlock = Extract<AiTurnBlock, { kind: "tool" }>;
@@ -69,7 +69,7 @@ const isResult = (block: ToolBlock, phase: AiTurnPhase, codePresentations: boole
69
69
  // A running delivery reserves its place once its arguments arrived: a new version of an earlier result must find the
70
70
  // earlier place before it takes one of its own.
71
71
  const delivered = block.status === "completed" || (live(phase) && block.status === "running" && block.args !== undefined);
72
- if (block.name === "present" || isCardToolName(block.name)) return delivered;
72
+ if (block.name === "present" || isCardToolName(block.name) || isChartToolName(block.name)) return delivered;
73
73
  if (block.name === "code_present") return codePresentations && delivered;
74
74
  return hasCapabilityTable(block);
75
75
  };
@@ -79,9 +79,9 @@ const actionState = (block: ToolBlock, phase: AiTurnPhase): AiTurnActionState |
79
79
  if (block.status === "rejected") return "rejected";
80
80
  if (isSurveyToolName(block.name) || isTextEditorToolName(block.name)) return isFailedTool(block) ? null : "interaction";
81
81
  if (block.name === "code_secret") return block.status === "awaiting_client" ? "interaction" : null;
82
- // Effects in Cloud and decided approvals stay visible as receipts, whether or not the model mentions them.
82
+ // Effects in Cloud and approvals, decided or expired, stay visible as receipts, whether or not the model mentions them.
83
83
  const capabilityAction = block.presentation?.kind === "capability" && block.presentation.capabilityKind === "action";
84
- if (!capabilityAction && !block.approved) return null;
84
+ if (!capabilityAction && block.approved === undefined) return null;
85
85
  if (block.status === "running" || block.status === "awaiting_client") return live(phase) ? "running" : "not_run";
86
86
  return isFailedTool(block) ? "failed" : "done";
87
87
  };