@k2b/cloud 0.39.0 → 0.41.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 (237) hide show
  1. package/README.md +8 -0
  2. package/package.json +11 -4
  3. package/src/_internal/app-favicon.ts +4 -2
  4. package/src/_internal/capabilities.ts +72 -48
  5. package/src/_internal/capability-reviews.ts +81 -0
  6. package/src/_internal/capability-schemas.ts +29 -0
  7. package/src/_internal/capability-sentences.ts +217 -1
  8. package/src/_internal/capability-streams.ts +7 -0
  9. package/src/_internal/define-app.ts +49 -18
  10. package/src/_internal/font-preloads.ts +2 -1
  11. package/src/_internal/heartbeat.ts +203 -14
  12. package/src/_internal/registry.ts +28 -1
  13. package/src/_internal/runtime-watcher.ts +6 -0
  14. package/src/_internal/setup-lock.ts +103 -0
  15. package/src/_internal/static-assets.ts +52 -3
  16. package/src/_internal/status-preserving-ssr.ts +2 -0
  17. package/src/access/PermissionEditor.tsx +2 -2
  18. package/src/account/EntitySearch.tsx +1 -1
  19. package/src/ai/action-review.ts +101 -0
  20. package/src/ai/background-review.ts +72 -0
  21. package/src/ai/capabilities.ts +29 -11
  22. package/src/ai/capability-execution.ts +76 -43
  23. package/src/ai/capability-result.ts +95 -0
  24. package/src/ai/chat/action-card.ts +464 -0
  25. package/src/ai/chat/action-messages.ts +129 -0
  26. package/src/ai/chat/action-question.browser-harness.tsx +245 -0
  27. package/src/ai/chat/action-question.fixtures.ts +733 -0
  28. package/src/ai/chat/action-question.tsx +680 -0
  29. package/src/ai/chat/action-text.ts +288 -0
  30. package/src/ai/chat/blocks.tsx +19 -25
  31. package/src/ai/chat/message-actions.tsx +6 -0
  32. package/src/ai/chat/tool-action-words.ts +31 -0
  33. package/src/ai/chat/tool-action.tsx +456 -0
  34. package/src/ai/chat/turn-view.tsx +10 -160
  35. package/src/ai/chat/view-block.tsx +15 -5
  36. package/src/ai/chat-task-contracts.ts +4 -1
  37. package/src/ai/chat-task-reasons-migrate.ts +7 -0
  38. package/src/ai/chat-task-reasons.ts +1 -1
  39. package/src/ai/chat-tasks.ts +55 -25
  40. package/src/ai/client/controller.ts +59 -0
  41. package/src/ai/default-tools.ts +9 -1
  42. package/src/ai/executor.ts +198 -39
  43. package/src/ai/firecrawl-tools.ts +15 -2
  44. package/src/ai/index.ts +3 -0
  45. package/src/ai/migrate.ts +26 -2
  46. package/src/ai/open-questions.ts +172 -0
  47. package/src/ai/projects-routes.ts +3 -0
  48. package/src/ai/protocol.ts +14 -2
  49. package/src/ai/question-text.ts +8 -0
  50. package/src/ai/recent-resources.ts +240 -0
  51. package/src/ai/recent-sent-refs.ts +38 -0
  52. package/src/ai/routes.ts +195 -5
  53. package/src/ai/runtime.ts +1 -0
  54. package/src/ai/skills-routes.ts +3 -0
  55. package/src/ai/skills.ts +10 -1
  56. package/src/ai/store.ts +245 -24
  57. package/src/ai/testing.ts +36 -0
  58. package/src/ai/tool-audit.ts +8 -0
  59. package/src/ai/tool-sentences.ts +152 -34
  60. package/src/ai/tool-wording.ts +36 -8
  61. package/src/ai/tools.ts +8 -3
  62. package/src/ai/types.ts +43 -4
  63. package/src/api/accounts-entities.ts +2 -1
  64. package/src/api/admin-ai-projects.ts +2 -0
  65. package/src/api/admin-ai-skills.ts +2 -0
  66. package/src/api/admin-ai-usage.ts +2 -0
  67. package/src/api/admin-geoip.ts +80 -0
  68. package/src/api/admin-identity.ts +3 -0
  69. package/src/api/admin-lifecycle.ts +3 -0
  70. package/src/api/admin-outgoing-mail.ts +2 -0
  71. package/src/api/app-approval-actions.ts +31 -7
  72. package/src/api/app-approval.ts +46 -5
  73. package/src/api/approvals-loader.ts +23 -1
  74. package/src/api/approvals.ts +2 -0
  75. package/src/api/auth/schemas.ts +3 -0
  76. package/src/api/auth.ts +67 -12
  77. package/src/api/capabilities.ts +44 -11
  78. package/src/api/capability-streams.ts +12 -0
  79. package/src/api/help.ts +4 -28
  80. package/src/api/index.ts +6 -1
  81. package/src/api/lasting-access-messages.ts +17 -0
  82. package/src/api/mcp.ts +30 -3
  83. package/src/api/me-panel-consent.ts +71 -0
  84. package/src/api/me.ts +159 -36
  85. package/src/api/search.ts +3 -1
  86. package/src/api/sync-ops.ts +2 -0
  87. package/src/api/widgets.ts +3 -0
  88. package/src/approvals/Approvals.tsx +166 -262
  89. package/src/approvals/approval-card.ts +68 -0
  90. package/src/approvals/messages.ts +12 -24
  91. package/src/browser/CloudResourceSearch.browser-harness.tsx +10 -2
  92. package/src/browser/app-approval.ts +54 -5
  93. package/src/browser/commands.ts +22 -6
  94. package/src/browser/composer-catalog.ts +45 -3
  95. package/src/browser/composer-insert.ts +10 -0
  96. package/src/browser/composer-open-messages.ts +17 -0
  97. package/src/browser/composer-open.ts +110 -0
  98. package/src/browser/discoverable-command.ts +6 -0
  99. package/src/browser/hints.ts +6 -0
  100. package/src/browser/live-connection.ts +26 -2
  101. package/src/browser/navigation-apps.ts +7 -0
  102. package/src/browser/panel-context-bridge.ts +30 -0
  103. package/src/browser/panel-context-receiver.ts +53 -0
  104. package/src/browser/panel-menu.tsx +56 -0
  105. package/src/browser/panel-messages.ts +11 -0
  106. package/src/browser/panel-shortcuts.ts +31 -0
  107. package/src/browser/panel-slot.ts +412 -10
  108. package/src/browser/panel.ts +37 -0
  109. package/src/browser/search-commands.ts +6 -3
  110. package/src/capabilities/client.ts +8 -2
  111. package/src/capabilities/reviews.ts +205 -0
  112. package/src/capabilities/server.ts +10 -3
  113. package/src/capabilities/testing.ts +47 -0
  114. package/src/capabilities/types.ts +2 -2
  115. package/src/cli/account.ts +13 -3
  116. package/src/cli/admin/geoip.ts +115 -0
  117. package/src/cli/admin/index.ts +3 -0
  118. package/src/contracts/app-approval.ts +123 -16
  119. package/src/contracts/approvals.ts +2 -2
  120. package/src/contracts/capabilities.ts +410 -18
  121. package/src/contracts/geoip.ts +42 -0
  122. package/src/contracts/index.ts +3 -0
  123. package/src/contracts/panel-context-plain.ts +99 -0
  124. package/src/contracts/panel-context.ts +93 -0
  125. package/src/contracts/seen-hints.ts +35 -0
  126. package/src/contracts/shared.ts +3 -0
  127. package/src/server/client-ip.ts +5 -0
  128. package/src/server/help-payload.ts +38 -0
  129. package/src/server/middleware/auth.ts +13 -1
  130. package/src/server/middleware/middleware.ts +9 -0
  131. package/src/server/middleware/no-transform.ts +19 -0
  132. package/src/server/middleware/rate-limit.ts +4 -6
  133. package/src/server/request-context/address.ts +45 -0
  134. package/src/server/request-context/geoip.ts +380 -0
  135. package/src/server/request-context/index.ts +33 -0
  136. package/src/server/request-context/user-agent.ts +95 -0
  137. package/src/services/accounts/app.ts +23 -3
  138. package/src/services/accounts/users.ts +12 -1
  139. package/src/services/app-approval.ts +382 -59
  140. package/src/services/approvals/device.ts +49 -31
  141. package/src/services/approvals/index.ts +7 -5
  142. package/src/services/approvals/list.ts +27 -6
  143. package/src/services/approvals/question.ts +68 -0
  144. package/src/services/audit/index.ts +1 -0
  145. package/src/services/auth-flows/ipa.ts +39 -2
  146. package/src/services/auth-flows/notification-sender.ts +3 -0
  147. package/src/services/auth-flows/password-reset.ts +32 -6
  148. package/src/services/geoip/index.ts +164 -0
  149. package/src/services/geoip/store.ts +30 -0
  150. package/src/services/geoip/upload.ts +150 -0
  151. package/src/services/identity/routes.ts +5 -1
  152. package/src/services/index.ts +13 -0
  153. package/src/services/known-browsers.ts +312 -0
  154. package/src/services/login-address-store.ts +51 -0
  155. package/src/services/login-context.ts +118 -0
  156. package/src/services/mandates/index.ts +3 -0
  157. package/src/services/mandates/policy.ts +15 -3
  158. package/src/services/notifications/email-frame.ts +1 -1
  159. package/src/services/oauth-tokens.ts +16 -2
  160. package/src/services/panel-context-consent.ts +58 -0
  161. package/src/services/pdf/markdown.ts +160 -2
  162. package/src/services/service-account-credentials.ts +21 -0
  163. package/src/services/session/index.ts +296 -41
  164. package/src/services/session/recent.ts +13 -0
  165. package/src/services/webauthn.ts +12 -0
  166. package/src/shared/email-html.ts +2 -0
  167. package/src/shared/help.ts +3 -0
  168. package/src/shared-text/cursor.ts +22 -0
  169. package/src/shared-text/documents.ts +142 -0
  170. package/src/shared-text/events.ts +43 -0
  171. package/src/shared-text/index.ts +12 -0
  172. package/src/shared-text/log.ts +48 -0
  173. package/src/shared-text/text-diff.ts +23 -0
  174. package/src/shared-text/types.ts +41 -0
  175. package/src/ssr/AdminLayout.tsx +2 -0
  176. package/src/ssr/AppLaunchpadPanel.tsx +2 -2
  177. package/src/ssr/CloudShell.island.tsx +10 -2
  178. package/src/ssr/HelpPanelArticle.island.tsx +308 -0
  179. package/src/ssr/HelpPanelBack.island.tsx +32 -0
  180. package/src/ssr/HelpPanelMenu.island.tsx +133 -0
  181. package/src/ssr/HelpPanelSearch.island.tsx +123 -0
  182. package/src/ssr/HelpPanelShortcuts.island.tsx +22 -0
  183. package/src/ssr/HelpPanelView.island.tsx +27 -0
  184. package/src/ssr/Layout.tsx +25 -2
  185. package/src/ssr/LayoutHelp.browser-harness.tsx +2 -0
  186. package/src/ssr/LayoutHelp.tsx +222 -419
  187. package/src/ssr/LayoutHelpPanel.browser-harness.tsx +135 -0
  188. package/src/ssr/LayoutHelpPanel.server.browser-harness.tsx +131 -0
  189. package/src/ssr/LayoutHelpPanel.server.tsx +115 -0
  190. package/src/ssr/LayoutHelpTrigger.tsx +27 -2
  191. package/src/ssr/MobileNavigation.tsx +2 -2
  192. package/src/ssr/PanelLayout.tsx +9 -1
  193. package/src/ssr/PanelRuntime.island.tsx +7 -0
  194. package/src/ssr/PanelSlot.tsx +53 -3
  195. package/src/ssr/ShellRuntime.island.tsx +11 -2
  196. package/src/ssr/__snapshots__/LayoutHelp.ssr.render.test.tsx.snap +29 -0
  197. package/src/ssr/app-color.ts +14 -9
  198. package/src/ssr/app-navigation.ts +16 -3
  199. package/src/ssr/compact-shell.browser-harness.ts +30 -22
  200. package/src/ssr/dialog-app-colors.browser-harness.tsx +43 -0
  201. package/src/ssr/help-messages.ts +4 -0
  202. package/src/ssr/help.ts +21 -0
  203. package/src/ssr/hints.ts +25 -0
  204. package/src/ssr/index.ts +3 -0
  205. package/src/ssr/layout-context.ts +2 -0
  206. package/src/ssr/layout-help-article.server.ts +50 -0
  207. package/src/ssr/layout-help-panel-controls.tsx +43 -0
  208. package/src/ssr/layout-help-panel-document.ts +67 -0
  209. package/src/ssr/layout-help-panel-lifecycle.ts +32 -0
  210. package/src/ssr/layout-help-panel-menu.tsx +113 -0
  211. package/src/ssr/layout-help-panel-modules.ts +7 -0
  212. package/src/ssr/layout-help-panel-reader.tsx +159 -0
  213. package/src/ssr/layout-help-panel-search.tsx +69 -0
  214. package/src/ssr/layout-help-panel-shortcuts.ts +7 -0
  215. package/src/ssr/layout-help-panel-state.ts +141 -0
  216. package/src/ssr/layout-help-panel-types.ts +19 -0
  217. package/src/ssr/layout-help-start.ts +33 -7
  218. package/src/ssr/layout-help-views.tsx +484 -0
  219. package/src/ssr/panel-consent.ts +29 -0
  220. package/src/ssr/panel-context.ts +10 -0
  221. package/src/ssr/panel-idle.ts +9 -0
  222. package/src/ssr/panel-slot.browser-harness.tsx +53 -4
  223. package/src/ssr/platform-messages.ts +18 -0
  224. package/src/ssr/shell-data.tsx +2 -1
  225. package/src/styles/approvals.css +6 -0
  226. package/src/styles/effects.css +213 -9
  227. package/src/styles/file-chooser.css +2 -1
  228. package/src/styles/global.css +49 -12
  229. package/src/styles/panel.css +47 -4
  230. package/src/styles/resource-search.css +6 -2
  231. package/src/styles/tokens.css +17 -11
  232. package/src/styles/utilities-buttons.css +3 -2
  233. package/src/styles/utilities-data.css +2 -1
  234. package/src/styles/utilities-feedback.css +2 -2
  235. package/src/styles/utilities-layout.css +3 -13
  236. package/src/styles/utilities-markdown-editor.css +6 -6
  237. package/src/styles/utilities-navigation.css +17 -22
@@ -0,0 +1,288 @@
1
+ import { dates, i18n } from "@k2b/stdlib";
2
+ import { capabilityPersonText, capabilitySentenceText } from "../../_internal/capability-sentences";
3
+ import type { CapabilityAskReason, CapabilityPerson, CapabilitySentence } from "../../contracts/capabilities";
4
+ import { formatBytes } from "../../shared/format";
5
+ import type { ActionCard } from "./action-card";
6
+ import { actionMessages } from "./action-messages";
7
+
8
+ export type ActionTextRow = { label: string; value: string; format?: "date" | "date-time"; block?: true };
9
+ export type ActionQuestionText = {
10
+ locale: string;
11
+ question: string;
12
+ reason: string;
13
+ reasonCode: CapabilityAskReason;
14
+ rows: ActionTextRow[];
15
+ readMore?: string;
16
+ confirm: string;
17
+ decline: string;
18
+ incomplete: boolean;
19
+ };
20
+
21
+ const messages = i18n.define({
22
+ baseLocale: "en",
23
+ messages: {
24
+ en: {
25
+ from: "From",
26
+ to: "To",
27
+ cc: "Cc",
28
+ bcc: "Bcc",
29
+ subject: "Subject",
30
+ sendAt: "Send at",
31
+ warning: "Warning",
32
+ attachments: "Attachments",
33
+ text: "Text",
34
+ when: "When",
35
+ until: "Until",
36
+ where: "Where",
37
+ with: "With",
38
+ repeats: "Repeats",
39
+ due: "Due",
40
+ assignees: "Assignees",
41
+ status: "Status",
42
+ checklist: "Checklist",
43
+ tags: "Tags",
44
+ folder: "Folder",
45
+ size: "Size",
46
+ target: "To",
47
+ place: "Place",
48
+ added: "Added",
49
+ removed: "Removed",
50
+ starts: "Starts",
51
+ may: "May",
52
+ mayAdded: "May (added)",
53
+ mayRemoved: "May (removed)",
54
+ also: "Also",
55
+ readAll: "Read all",
56
+ title: "Title",
57
+ name: "Name",
58
+ note: "Note",
59
+ before: ({ label }: { label: string }) => `${label} (before)`,
60
+ more: ({ count }: { count: number }) => `${count.toLocaleString("en")} more`,
61
+ },
62
+ de: {
63
+ from: "Von",
64
+ to: "An",
65
+ cc: "Cc",
66
+ bcc: "Bcc",
67
+ subject: "Betreff",
68
+ sendAt: "Senden am",
69
+ warning: "Warnung",
70
+ attachments: "Anhänge",
71
+ text: "Text",
72
+ when: "Wann",
73
+ until: "Bis",
74
+ where: "Wo",
75
+ with: "Mit",
76
+ repeats: "Wiederholung",
77
+ due: "Fällig",
78
+ assignees: "Zuständig",
79
+ status: "Status",
80
+ checklist: "Checkliste",
81
+ tags: "Tags",
82
+ folder: "Ordner",
83
+ size: "Größe",
84
+ target: "Ziel",
85
+ place: "Ort",
86
+ added: "Neu",
87
+ removed: "Entfernt",
88
+ starts: "Startet",
89
+ may: "Darf",
90
+ mayAdded: "Darf (neu)",
91
+ mayRemoved: "Darf (entfernt)",
92
+ also: "Folge",
93
+ readAll: "Ganz lesen",
94
+ title: "Titel",
95
+ name: "Name",
96
+ note: "Hinweis",
97
+ before: ({ label }) => `${label} (vorher)`,
98
+ more: ({ count }) => `${count.toLocaleString("de")} weitere`,
99
+ },
100
+ },
101
+ });
102
+ export const checkActionTextMessages = () => messages.check();
103
+ /** The label for a row an old review left without one, where a transport requires labels. */
104
+ export const actionNoteLabel = (locale: string): string => messages.resolve([locale]).t.note;
105
+
106
+ /** Flatten the question card once. Transport limits and terminal escaping belong to its callers. */
107
+ export const actionCardText = (card: ActionCard): ActionQuestionText => {
108
+ const t = messages.resolve([card.locale]).t;
109
+ const rows: ActionTextRow[] = [];
110
+ let incomplete = false;
111
+ const row = (label: string, value: string | undefined, extra: Pick<ActionTextRow, "block" | "format"> = {}) => {
112
+ if (value !== undefined && value !== "") rows.push({ label, value, ...extra });
113
+ };
114
+ const list = (values: string[], more = 0) => {
115
+ if (more > 0) {
116
+ incomplete = true;
117
+ values.push(t.more({ count: more }));
118
+ }
119
+ return new Intl.ListFormat(card.locale, { type: "conjunction" }).format(values);
120
+ };
121
+ const people = (values: CapabilityPerson[] | undefined, more = 0) => list((values ?? []).map(capabilityPersonText), more);
122
+ // The thing's own name, unless the question already says it.
123
+ const named = (label: string, value: string) => {
124
+ if (!card.questionText.includes(value)) row(label, value);
125
+ };
126
+ const model = card.preview;
127
+ if (model?.kind === "legacy") {
128
+ rows.push(...model.rows);
129
+ row(t.text, model.markdown, { block: true });
130
+ } else if (model?.kind === "review") {
131
+ const context = { locale: card.locale, timeZone: model.timeZone };
132
+ const sentence = (value: CapabilitySentence) => capabilitySentenceText(value.text, value.slots ?? model.slots ?? {}, context);
133
+ const preview = model.preview;
134
+ if (preview && "truncated" in preview && preview.truncated) incomplete = true;
135
+ switch (preview?.kind) {
136
+ case "message":
137
+ row(t.from, preview.from && capabilityPersonText(preview.from));
138
+ row(t.to, people(preview.to, preview.more?.to));
139
+ row(t.cc, people(preview.cc, preview.more?.cc));
140
+ row(t.bcc, people(preview.bcc, preview.more?.bcc));
141
+ row(t.subject, preview.subject);
142
+ row(t.sendAt, preview.sendAt, { format: "date-time" });
143
+ row(t.warning, preview.warnings?.join(" · "));
144
+ row(
145
+ t.attachments,
146
+ list(
147
+ (preview.attachments ?? []).map((file) =>
148
+ file.size === undefined ? file.name : `${file.name} (${formatBytes(file.size, { locale: card.locale })})`,
149
+ ),
150
+ preview.moreAttachments,
151
+ ),
152
+ );
153
+ row(t.text, preview.body, { block: true });
154
+ break;
155
+ case "event": {
156
+ named(t.title, preview.title);
157
+ const dateOnly = (value: string) => /^\d{4}-\d{2}-\d{2}$/.test(value);
158
+ const allDay = preview.allDay === true || dateOnly(preview.start);
159
+ // Both ends stay typed, so a device shows them in its own zone. An all-day instant becomes its calendar day
160
+ // in the card's zone first, so no reader's zone moves it to another day.
161
+ const day = (value: string) =>
162
+ dateOnly(value)
163
+ ? value
164
+ : new Intl.DateTimeFormat("en-CA", { timeZone: model.timeZone, year: "numeric", month: "2-digit", day: "2-digit" }).format(
165
+ new Date(value),
166
+ );
167
+ const format = allDay ? "date" : "date-time";
168
+ row(t.when, allDay ? day(preview.start) : preview.start, { format });
169
+ if (preview.end) row(t.until, allDay ? day(preview.end) : preview.end, { format });
170
+ row(t.where, [preview.place, preview.location].filter(Boolean).join(" · "));
171
+ row(t.with, people(preview.attendees));
172
+ row(t.repeats, preview.recurrence);
173
+ row(t.text, preview.description, { block: true });
174
+ break;
175
+ }
176
+ case "task":
177
+ named(t.title, preview.title);
178
+ row(t.place, preview.place);
179
+ row(t.due, preview.due, { format: /^\d{4}-\d{2}-\d{2}$/.test(preview.due ?? "") ? "date" : "date-time" });
180
+ row(t.assignees, people(preview.assignees));
181
+ row(t.status, preview.status);
182
+ row(t.checklist, preview.checklist?.map((item) => `${item.done ? "☑" : "☐"} ${item.label}`).join(" · "));
183
+ row(t.tags, preview.tags?.join(", "));
184
+ row(t.text, preview.description, { block: true });
185
+ break;
186
+ case "file":
187
+ named(t.name, preview.name);
188
+ row(t.folder, preview.place ?? preview.path);
189
+ row(t.size, preview.size === undefined ? undefined : formatBytes(preview.size, { locale: card.locale }));
190
+ row(t.target, preview.to);
191
+ break;
192
+ case "page":
193
+ named(t.title, preview.title);
194
+ row(t.place, preview.place);
195
+ row(t.text, preview.body, { block: true });
196
+ for (const change of preview.changes ?? []) row(change.change === "added" ? t.added : t.removed, change.text);
197
+ break;
198
+ case "rule":
199
+ row(t.starts, sentence(preview.when));
200
+ row(t.text, preview.does, { block: true });
201
+ for (const may of preview.may)
202
+ row(may.mark === "added" ? t.mayAdded : may.mark === "removed" ? t.mayRemoved : t.may, sentence(may));
203
+ // The card's fixed line: a routine knows only its instruction, never this chat.
204
+ if (!preview.seesChat) row(t.note, actionMessages(card.locale).ruleBlindToChat);
205
+ break;
206
+ case "fields":
207
+ for (const field of preview.rows) {
208
+ rows.push({ label: field.label, value: field.value, ...(field.format ? { format: field.format } : {}) });
209
+ if (field.before !== undefined)
210
+ rows.push({ label: t.before({ label: field.label }), value: field.before, ...(field.format ? { format: field.format } : {}) });
211
+ }
212
+ break;
213
+ }
214
+ for (const consequence of model.consequences)
215
+ row(consequence.mark === "added" ? t.added : consequence.mark === "removed" ? t.removed : t.also, consequence.plain);
216
+ }
217
+ // A long question shortens slot values and names at most three people of a slot, then "N more". The text is
218
+ // complete only when the question or a row shows each value whole; people the app left out are never shown.
219
+ const shown = [card.questionText, ...rows.map((entry) => entry.value)];
220
+ const whole = (value: string) => shown.some((text) => text.includes(value));
221
+ const typed = (value: string, format: "date" | "date-time") =>
222
+ rows.some((entry) => entry.value === value && (entry.format === "date-time" || entry.format === format));
223
+ if (
224
+ card.slotValues?.unlisted ||
225
+ card.slotValues?.values.some((value) => !whole(value)) ||
226
+ card.slotValues?.times.some(
227
+ (time) => !whole(time.text) && !(typed(time.start, time.format) && (!time.end || typed(time.end, time.format))),
228
+ )
229
+ )
230
+ incomplete = true;
231
+ return {
232
+ locale: card.locale,
233
+ question: card.questionText,
234
+ reason: card.reason.text,
235
+ reasonCode: card.reason.code,
236
+ rows,
237
+ ...(card.readMore ? { readMore: card.readMore } : {}),
238
+ confirm: card.confirm.label,
239
+ decline: card.decline,
240
+ incomplete,
241
+ };
242
+ };
243
+
244
+ /** The CLI's plain question block, before terminal-safe sanitizing at the CLI boundary. */
245
+ export const actionQuestionLines = (
246
+ text: ActionQuestionText,
247
+ extra: { origin?: string; commands?: { approve: string; reject: string }; timeZone: string },
248
+ ): string[] => {
249
+ const t = messages.resolve([text.locale]).t;
250
+ const lines = [`? ${text.question}`, ` ${text.reason}`];
251
+ if (extra.origin) lines.push(` ${extra.origin}`);
252
+ const width = Math.max(0, ...text.rows.filter((row) => !row.block && row.label).map((row) => Array.from(row.label).length + 1));
253
+ let markedIncomplete = false;
254
+ for (const row of text.rows) {
255
+ if (row.block) {
256
+ const body = row.value.split(/\r?\n/);
257
+ // Cut a long text only when "Read all" can open the whole thing; otherwise the terminal shows all of it.
258
+ const shown = text.readMore ? body.slice(0, 8) : body;
259
+ lines.push(...shown.map((line) => ` ${line}`));
260
+ if (shown.length < body.length || (text.incomplete && !markedIncomplete)) {
261
+ lines.push(" …");
262
+ markedIncomplete = true;
263
+ }
264
+ continue;
265
+ }
266
+ const options = { locale: text.locale, timeZone: extra.timeZone };
267
+ const value =
268
+ row.format === "date-time"
269
+ ? dates.formatDateTime(row.value, options)
270
+ : row.format === "date"
271
+ ? dates.formatDate(/^\d{4}-\d{2}-\d{2}$/.test(row.value) ? `${row.value}T12:00:00.000Z` : row.value, {
272
+ ...options,
273
+ ...(/^\d{4}-\d{2}-\d{2}$/.test(row.value) ? { timeZone: "UTC" } : {}),
274
+ })
275
+ : row.value;
276
+ lines.push(row.label ? ` ${(row.label + ":").padEnd(width)} ${value}` : ` ${value}`);
277
+ }
278
+ if (text.incomplete && !markedIncomplete) lines.push(" …");
279
+ if (text.readMore) lines.push(` ${t.readAll}: ${text.readMore}`);
280
+ if (extra.commands) {
281
+ const width = Math.max(Array.from(text.confirm).length, Array.from(text.decline).length) + 1;
282
+ lines.push(
283
+ ` ${(text.confirm + ":").padEnd(width)} ${extra.commands.approve}`,
284
+ ` ${(text.decline + ":").padEnd(width)} ${extra.commands.reject}`,
285
+ );
286
+ }
287
+ return lines;
288
+ };
@@ -3,6 +3,7 @@ import { mutation } from "@k2b/stdlib/solid";
3
3
  import { Button, ButtonLink, Chat, isStructuredDataValue, MarkdownView, SplitButton, StructuredDataPreview, useLocale } from "@k2b/ui";
4
4
  import { createSignal, createUniqueId, For, type JSX, Match, Show, Switch, untrack } from "solid-js";
5
5
  import { capabilityApprovalReason, capabilityApprovalReasonLabel } from "../../_internal/capability-sentences";
6
+ import { capabilityReviewView } from "../../capabilities/reviews";
6
7
  import type { CapabilityActionReview } from "../../contracts/capabilities";
7
8
  import { markdown } from "../../shared";
8
9
  import type { AiTurnBlock } from "../protocol";
@@ -31,8 +32,10 @@ import {
31
32
  } from "./message-utils";
32
33
  import { aiChatMessages } from "./messages";
33
34
  import { AssistantMarkdownBlock } from "./primitives";
35
+ import { ToolActionView } from "./tool-action";
34
36
  import { AiToolActivity } from "./tool-disclosure";
35
37
  import { isFailedTool } from "./tool-groups";
38
+ import type { AiTurnActionState } from "./turn-layout";
36
39
  import { CloudCardBlock, CloudSurveyBlock, CloudSurveyResultBlock, CloudTextEditorBlock, CloudTextEditorResultBlock } from "./visual-tools";
37
40
  import { FetchFileToolBlock, WebExtractToolBlock, WebSearchToolBlock, WebsiteReceipts, websiteReceipts } from "./web-tools";
38
41
 
@@ -199,6 +202,7 @@ export function ApprovalBlockView(props: { turnId: string; block: ToolBlock; onD
199
202
  const locale = useLocale();
200
203
  const timeZone = useReaderTimeZone();
201
204
  const t = () => aiChatMessages(locale());
205
+ const review = () => (props.block.approval?.review ? capabilityReviewView(props.block.approval.review, { timeZone }) : undefined);
202
206
  const request = () => ({ turnId: props.turnId, callId: props.block.callId, name: props.block.name });
203
207
  const pending = () => props.block.status === "awaiting_approval";
204
208
  const actionDisabled = () => actions.actionDisabled?.() ?? false;
@@ -229,7 +233,7 @@ export function ApprovalBlockView(props: { turnId: string; block: ToolBlock; onD
229
233
  // The model's own reason, labelled as such, only where Cloud offered it; it adds to the app's sentence and never replaces it.
230
234
  const reason = () => (props.block.presentation?.approvalReason ? capabilityApprovalReason(props.block.args) : null);
231
235
  const description = () => {
232
- const reviewMessage = props.block.approval?.review?.message.trim();
236
+ const reviewMessage = review()?.message.trim();
233
237
  if (reviewMessage) return reviewMessage;
234
238
  // An app Action's approval text repeats its heading and reason for text-only readers; the card shows them itself.
235
239
  if (capability()) return null;
@@ -252,10 +256,9 @@ export function ApprovalBlockView(props: { turnId: string; block: ToolBlock; onD
252
256
  const match = /^([^:]{1,40}):\s+(.+)$/.exec(line);
253
257
  return match ? { label: match[1], value: match[2] } : { value: line };
254
258
  });
255
- const inlineReviewDetails = () =>
256
- (props.block.approval?.review?.details ?? []).filter((detail) => (detail.display ?? "inline") === "inline");
257
- const blockReviewDetails = () => (props.block.approval?.review?.details ?? []).filter((detail) => detail.display === "block");
258
- const reviewLinks = () => props.block.approval?.review?.links ?? [];
259
+ const inlineReviewDetails = () => (review()?.details ?? []).filter((detail) => (detail.display ?? "inline") === "inline");
260
+ const blockReviewDetails = () => (review()?.details ?? []).filter((detail) => detail.display === "block");
261
+ const reviewLinks = () => review()?.links ?? [];
259
262
  const reviewLinkTitle = (rel: "open" | "edit" | "status" | "preview" | "download") =>
260
263
  ({
261
264
  open: t().openIn({ app: ownerName() }),
@@ -545,20 +548,6 @@ function CapabilityToolView(props: { block: ToolBlock }) {
545
548
  );
546
549
  }
547
550
 
548
- function RejectedToolView(props: { block: ToolBlock }) {
549
- const locale = useLocale();
550
- const timeZone = useReaderTimeZone();
551
- const presentation = () => props.block.presentation;
552
- const title = () => toolSubject(props.block, locale(), timeZone) ?? presentation()?.title ?? displayToolName(props.block.name, locale());
553
- return (
554
- <Chat.Activity
555
- icon={aiToolIcon(props.block.name, presentation()?.appIcon)}
556
- label={toolOutcome(props.block, "rejected", locale(), timeZone) ?? aiChatMessages(locale()).toolRejected({ title: title() })}
557
- accent={presentation()?.appAccent}
558
- />
559
- );
560
- }
561
-
562
551
  export function SurveyToolView(props: { turnId: string; block: ToolBlock; active?: boolean }) {
563
552
  const actions = useAiChatActions();
564
553
  const locale = useLocale();
@@ -670,6 +659,15 @@ function ToolBlockView(props: { turnId: string; block: ToolBlock; active?: boole
670
659
  const actions = useAiChatActions();
671
660
  const locale = useLocale();
672
661
  const status = () => props.block.status;
662
+ // The same states as place 4 of a turn: open, then running, done, rejected, failed, or not run.
663
+ const actionState = (): AiTurnActionState | "open" | undefined => {
664
+ if (status() === "awaiting_approval") return "open";
665
+ if (status() === "rejected") return "rejected";
666
+ const action = props.block.presentation?.kind === "capability" && props.block.presentation.capabilityKind === "action";
667
+ if (!action) return undefined;
668
+ if (status() === "running" || status() === "awaiting_client") return props.active === false ? "not_run" : "running";
669
+ return isFailedTool(props.block) ? "failed" : "done";
670
+ };
673
671
  const fetchError = () =>
674
672
  props.block.name === "fetch_file" && props.block.isError ? fetchFileErrorPresentation(props.block.result, locale()) : undefined;
675
673
  return (
@@ -688,12 +686,8 @@ function ToolBlockView(props: { turnId: string; block: ToolBlock; active?: boole
688
686
  />
689
687
  }
690
688
  >
691
- <Match when={status() === "awaiting_approval"}>
692
- <ApprovalBlockView turnId={props.turnId} block={props.block} />
693
- </Match>
694
- <Match when={status() === "rejected"}>
695
- <RejectedToolView block={props.block} />
696
- </Match>
689
+ {/* A question, and every outcome of an app Action, is one card v4 element. */}
690
+ <Match when={actionState()}>{(state) => <ToolActionView block={props.block} turnId={props.turnId} state={state()} />}</Match>
697
691
  <Match when={props.block.presentation?.kind === "capability"}>
698
692
  <CapabilityToolView block={props.block} />
699
693
  </Match>
@@ -12,6 +12,7 @@ import {
12
12
  } from "@k2b/ui";
13
13
  import { createContext, createSignal, For, type JSX, Show, useContext } from "solid-js";
14
14
  import type { CloudResourceRef } from "../../contracts/capabilities";
15
+ import type { AiActionCheck } from "../action-review";
15
16
  import type { AiTurnBlock } from "../protocol";
16
17
  import type { AiMessageFeedback, AiMessageFeedbackReason, AiStoredMessage } from "../types";
17
18
  import {
@@ -46,6 +47,11 @@ export type AiChatActions = {
46
47
  /** Revoke the approval that lets this chat read `origin` without asking; the next request asks again. */
47
48
  onRevokeWebsite?: (origin: string) => Promise<void>;
48
49
  onFrontendToolResult?: FrontendToolResultHandler;
50
+ /**
51
+ * Ask the owning app once more whether a call whose outcome stayed unknown happened, through its `check` Query. Never
52
+ * repeats the call itself. Resolves with the stored check; without it, an unknown outcome offers no new check.
53
+ */
54
+ onCheckAction?: (request: AiTurnActionRequest) => Promise<AiActionCheck>;
49
55
  onForkMessage?: ForkMessageHandler;
50
56
  onRetryMessage?: RetryMessageHandler;
51
57
  onRetrySteer?: RetrySteerHandler;
@@ -0,0 +1,31 @@
1
+ import { capabilityUnknownText } from "../../_internal/capability-sentences";
2
+ import type { AiTurnBlock } from "../protocol";
3
+ import { toolOutcome } from "../tool-wording";
4
+ import { isRecord } from "./message-utils";
5
+ import { aiChatMessages } from "./messages";
6
+
7
+ type ToolBlock = Extract<AiTurnBlock, { kind: "tool" }>;
8
+
9
+ /** The application's own short summary of a result, if it fits on one line. */
10
+ const resultSummary = (block: ToolBlock): string | undefined => {
11
+ const value = isRecord(block.result) ? block.result.summary : undefined;
12
+ return typeof value === "string" && value.trim() && value.trim().length <= 500 ? value.trim() : undefined;
13
+ };
14
+
15
+ /** The words of a call without an app sentence: Cloud's frames around its catalog sentence or title. */
16
+ export const legacyOutcomeWords = (block: ToolBlock, locale: string, timeZone: string) => (subject: string) => {
17
+ const t = aiChatMessages(locale);
18
+ const capability = block.presentation?.kind === "capability";
19
+ return {
20
+ running: t.receiptRunning({ title: subject }),
21
+ // The app's done sentence, else its own summary of the call, else Cloud's frame.
22
+ done:
23
+ toolOutcome(block, "done", locale, timeZone) ??
24
+ resultSummary(block) ??
25
+ (capability ? t.receiptDone({ title: subject }) : t.receiptApproved({ title: subject })),
26
+ rejected: toolOutcome(block, "rejected", locale, timeZone) ?? t.receiptRejected({ title: subject }),
27
+ notRun: toolOutcome(block, "notRun", locale, timeZone) ?? t.receiptNotRun({ title: subject, stopped: false }),
28
+ unknown: capabilityUnknownText(subject, locale),
29
+ unknownStill: capabilityUnknownText(subject, locale, true),
30
+ };
31
+ };