@k2b/cloud 0.37.0 → 0.39.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 (149) hide show
  1. package/package.json +8 -3
  2. package/scripts/build-canvas-workers.ts +109 -29
  3. package/scripts/build.ts +3 -5
  4. package/src/_internal/app-path.ts +25 -0
  5. package/src/_internal/app-presentation.ts +4 -0
  6. package/src/_internal/canvas-worker.ts +5 -3
  7. package/src/_internal/define-app.ts +28 -2
  8. package/src/_internal/help-catalog.ts +123 -4
  9. package/src/_internal/help.ts +27 -6
  10. package/src/_internal/panel-cookie.ts +39 -0
  11. package/src/_internal/registry-validation.ts +21 -26
  12. package/src/_internal/runtime-context.ts +1 -0
  13. package/src/access/PermissionEditor.browser-harness.tsx +57 -38
  14. package/src/access/PermissionEditor.tsx +30 -5
  15. package/src/access/ResourceApiKeys.tsx +5 -1
  16. package/src/account/Avatar.tsx +20 -2
  17. package/src/account/ui.ts +1 -1
  18. package/src/ai/approval-routes.ts +5 -2
  19. package/src/ai/approvals.ts +139 -28
  20. package/src/ai/browser.ts +11 -0
  21. package/src/ai/chart-block.ts +44 -13
  22. package/src/ai/chat/action-sentences.ts +1 -44
  23. package/src/ai/chat/chart-block.tsx +2 -11
  24. package/src/ai/chat/messages.ts +10 -0
  25. package/src/ai/chat/turn-view.tsx +35 -3
  26. package/src/ai/chat/view-block.tsx +7 -2
  27. package/src/ai/chat/web-tools.tsx +23 -6
  28. package/src/ai/chat-task-contracts.ts +28 -3
  29. package/src/ai/chat-task-reasons-migrate.ts +68 -0
  30. package/src/ai/chat-task-reasons.ts +92 -0
  31. package/src/ai/chat-tasks.ts +460 -66
  32. package/src/ai/default-tool-contracts.ts +30 -13
  33. package/src/ai/fetch-file-tool.ts +37 -3
  34. package/src/ai/index.ts +12 -0
  35. package/src/ai/migrate.ts +46 -3
  36. package/src/ai/readable-html.ts +1057 -0
  37. package/src/ai/request-context.ts +7 -1
  38. package/src/ai/routes.ts +27 -1
  39. package/src/ai/runtime.ts +12 -27
  40. package/src/ai/store.ts +5 -3
  41. package/src/ai/timeline.ts +9 -1
  42. package/src/ai/tool-wording.ts +45 -0
  43. package/src/ai/turn-abort.ts +33 -0
  44. package/src/ai/types.ts +12 -1
  45. package/src/api/admin-core-settings.ts +190 -139
  46. package/src/api/admin-outgoing-mail.ts +4 -2
  47. package/src/api/app-approval.ts +9 -2
  48. package/src/api/approvals.ts +1 -1
  49. package/src/api/help.ts +23 -4
  50. package/src/api/index.ts +2 -1
  51. package/src/api/mcp.ts +8 -2
  52. package/src/browser/CloudResourceSearch.tsx +10 -6
  53. package/src/browser/FileChooser.tsx +11 -3
  54. package/src/browser/app-approval.ts +7 -2
  55. package/src/browser/choose-files.browser-harness.tsx +4 -1
  56. package/src/browser/composer-catalog.ts +103 -0
  57. package/src/browser/composer-insert-messages.ts +31 -0
  58. package/src/browser/composer-insert.ts +150 -0
  59. package/src/browser/file-chooser-messages.ts +4 -0
  60. package/src/browser/file-providers.ts +14 -1
  61. package/src/browser/panel-messages.ts +24 -0
  62. package/src/browser/panel-slot.ts +354 -0
  63. package/src/browser/panel.ts +28 -0
  64. package/src/browser/provider-folder.tsx +58 -5
  65. package/src/browser/resource-picker.tsx +17 -2
  66. package/src/cli/admin/workflows.ts +1 -0
  67. package/src/config/env.ts +14 -0
  68. package/src/contracts/app-approval.ts +49 -9
  69. package/src/contracts/app.ts +14 -0
  70. package/src/contracts/file-provider.ts +48 -1
  71. package/src/contracts/outgoing-mail.ts +8 -1
  72. package/src/contracts/registry.ts +3 -1
  73. package/src/contracts/shared.ts +2 -0
  74. package/src/server/help.ts +136 -10
  75. package/src/server/services/access.ts +5 -1
  76. package/src/server/services/geo.ts +21 -4
  77. package/src/services/accounts/posix.ts +2 -1
  78. package/src/services/allowed-network-fetch.ts +189 -0
  79. package/src/services/app-approval.ts +158 -38
  80. package/src/services/approvals/device.ts +2 -0
  81. package/src/services/help/index.ts +46 -5
  82. package/src/services/help/store.ts +26 -5
  83. package/src/services/help/types.ts +10 -2
  84. package/src/services/index.ts +22 -3
  85. package/src/services/notifications/browser.ts +16 -5
  86. package/src/services/outgoing-mail/dispatcher.ts +7 -2
  87. package/src/services/outgoing-mail/resolve-host.ts +39 -0
  88. package/src/services/outgoing-mail/test-send.ts +18 -3
  89. package/src/services/outgoing-mail/transport.ts +24 -13
  90. package/src/services/pdf/image-protocol.ts +135 -0
  91. package/src/services/pdf/image-variants.ts +124 -0
  92. package/src/services/pdf/image-worker.ts +31 -0
  93. package/src/services/pdf/image.ts +109 -0
  94. package/src/services/pdf/index.ts +9 -0
  95. package/src/services/settings/api.ts +11 -6
  96. package/src/services/settings/app.ts +5 -4
  97. package/src/services/settings/core-settings.ts +4 -3
  98. package/src/services/settings/index.ts +67 -43
  99. package/src/services/settings/namespace.ts +2 -1
  100. package/src/services/settings/store.ts +83 -11
  101. package/src/shared/ai-platform-prompt.ts +1 -1
  102. package/src/shared/app-presentation.ts +1 -0
  103. package/src/shared/help.ts +41 -0
  104. package/src/shared/index.ts +9 -1
  105. package/src/shared/markdown/extensions/guided-help.ts +208 -64
  106. package/src/shared/markdown/formula.ts +8 -8
  107. package/src/shared/markdown/index.ts +72 -11
  108. package/src/shared/redirect.ts +12 -0
  109. package/src/ssr/CloudShell.island.tsx +86 -9
  110. package/src/ssr/Layout.tsx +62 -11
  111. package/src/ssr/LayoutHeader.tsx +15 -9
  112. package/src/ssr/LayoutHelp.browser-harness.tsx +130 -27
  113. package/src/ssr/LayoutHelp.tsx +569 -410
  114. package/src/ssr/LayoutRail.tsx +2 -2
  115. package/src/ssr/LayoutTabBar.tsx +29 -0
  116. package/src/ssr/PanelLayout.tsx +27 -0
  117. package/src/ssr/PanelRuntime.island.tsx +87 -0
  118. package/src/ssr/PanelSlot.tsx +108 -0
  119. package/src/ssr/PanelTrigger.tsx +49 -0
  120. package/src/ssr/ShellRuntime.island.tsx +72 -25
  121. package/src/ssr/app-appearance.ts +3 -2
  122. package/src/ssr/app-color.ts +100 -0
  123. package/src/ssr/app-launchpad.tsx +18 -1
  124. package/src/ssr/app-navigation.ts +15 -0
  125. package/src/ssr/compact-shell.baseline.json +5270 -0
  126. package/src/ssr/compact-shell.browser-harness.ts +431 -0
  127. package/src/ssr/help-messages.ts +60 -38
  128. package/src/ssr/help.ts +3 -0
  129. package/src/ssr/index.ts +3 -0
  130. package/src/ssr/layout-help-article.ts +41 -0
  131. package/src/ssr/layout-help-navigation.ts +0 -12
  132. package/src/ssr/layout-help-registry.tsx +4 -11
  133. package/src/ssr/layout-help-search.ts +9 -12
  134. package/src/ssr/layout-help-start.ts +68 -0
  135. package/src/ssr/panel-slot.browser-harness.tsx +110 -0
  136. package/src/ssr/platform-messages.ts +34 -0
  137. package/src/ssr/rail-navigation.ts +44 -0
  138. package/src/ssr/shell-data.tsx +11 -0
  139. package/src/ssr/shell-shortcuts.ts +19 -0
  140. package/src/styles/effects.css +689 -185
  141. package/src/styles/global.css +269 -55
  142. package/src/styles/panel.css +373 -0
  143. package/src/styles/resource-search.css +25 -0
  144. package/src/styles/tokens.css +23 -6
  145. package/src/styles/utilities-navigation.css +5 -21
  146. package/src/workflows/store/events.ts +24 -0
  147. package/src/workflows/store/index.ts +8 -0
  148. package/src/workflows/store/observability.ts +6 -1
  149. package/src/workflows/store/retention.ts +157 -0
@@ -0,0 +1,373 @@
1
+ /* Panel slot: one provider page in a same-origin iframe beside the app.
2
+ From 1024 px it is a column after the app card and the app card narrows;
3
+ from 768 to 1023 px a non-modal drawer over the right edge; below 768 px a
4
+ sheet from the bottom. The canvas carries the state from the cloud_panel
5
+ cookie, so the server draws the first frame. Loading, failure, the resize
6
+ shield and the closing hint are stacked in the body and only change their
7
+ visibility, so nothing ever moves inside the reserved box. */
8
+ .cloud-panel {
9
+ --cloud-panel-width: var(--cloud-panel-width-set, 26rem);
10
+ position: relative;
11
+ display: none;
12
+ min-height: 0;
13
+ color: var(--k2b-text);
14
+ }
15
+
16
+ @media not all and (min-width: 1280px) {
17
+ .cloud-panel {
18
+ --cloud-panel-width: var(--cloud-panel-width-set, 21.5rem);
19
+ }
20
+ }
21
+
22
+ /* The same surface, radius, frame and dark border as the app card. */
23
+ .cloud-panel__card {
24
+ display: flex;
25
+ flex-direction: column;
26
+ width: 100%;
27
+ height: 100%;
28
+ min-height: 0;
29
+ overflow: hidden;
30
+ border: 1px solid transparent;
31
+ border-radius: var(--cloud-card-radius, var(--k2b-radius-dialog));
32
+ background: var(--k2b-surface);
33
+ box-shadow: var(--k2b-shadow-frame);
34
+ }
35
+
36
+ .dark .cloud-panel__card {
37
+ border-color: var(--k2b-border);
38
+ }
39
+
40
+ /* One 46 px row, no line below; the provider's page starts right under it. */
41
+ .cloud-panel__head {
42
+ display: flex;
43
+ flex: none;
44
+ align-items: center;
45
+ gap: 0.25rem;
46
+ min-height: 2.875rem;
47
+ padding: 0.375rem 0.375rem 0.375rem 0.5rem;
48
+ }
49
+
50
+ .cloud-panel__head .k2b-icon-button .ti {
51
+ font-size: 1.125rem;
52
+ }
53
+
54
+ .cloud-panel__title {
55
+ display: flex;
56
+ flex: 1;
57
+ min-width: 0;
58
+ align-items: center;
59
+ gap: 0.5rem;
60
+ padding-inline-start: 0.5rem;
61
+ font-size: 0.875rem;
62
+ font-weight: 600;
63
+ }
64
+
65
+ .cloud-panel__title > .ti {
66
+ flex: none;
67
+ font-size: 1rem;
68
+ color: var(--k2b-text-muted);
69
+ }
70
+
71
+ .cloud-panel__title > span,
72
+ .cloud-panel__tab-label {
73
+ overflow: hidden;
74
+ text-overflow: ellipsis;
75
+ white-space: nowrap;
76
+ }
77
+
78
+ .cloud-panel__tabs {
79
+ display: flex;
80
+ flex: 1;
81
+ min-width: 0;
82
+ gap: 0.125rem;
83
+ overflow-x: auto;
84
+ scrollbar-width: none;
85
+ }
86
+
87
+ .cloud-panel__tab {
88
+ display: inline-flex;
89
+ flex: none;
90
+ max-width: 12rem;
91
+ height: 2rem;
92
+ align-items: center;
93
+ gap: 0.375rem;
94
+ padding: 0 0.625rem;
95
+ border: 0;
96
+ border-radius: var(--k2b-radius-control);
97
+ background: transparent;
98
+ color: var(--k2b-text-muted);
99
+ font-size: 0.875rem;
100
+ font-weight: 500;
101
+ cursor: pointer;
102
+ }
103
+
104
+ .cloud-panel__tab > .ti {
105
+ flex: none;
106
+ font-size: 1rem;
107
+ }
108
+
109
+ .cloud-panel__tab:hover {
110
+ background: var(--k2b-hover);
111
+ color: var(--k2b-text);
112
+ }
113
+
114
+ .cloud-panel__tab[aria-selected="true"] {
115
+ background: var(--k2b-surface-muted);
116
+ color: var(--k2b-text);
117
+ }
118
+
119
+ .cloud-panel__tab:focus-visible,
120
+ .cloud-panel__grabber:focus-visible {
121
+ outline: 2px solid var(--k2b-focus-ring);
122
+ outline-offset: -2px;
123
+ }
124
+
125
+ .cloud-panel__body {
126
+ position: relative;
127
+ flex: 1;
128
+ min-height: 0;
129
+ }
130
+
131
+ .cloud-panel__body > :is(.cloud-panel__skeleton, .cloud-panel__frame, .cloud-panel__fallback, .cloud-panel__shield, .cloud-panel__closing) {
132
+ position: absolute;
133
+ inset: 0;
134
+ width: 100%;
135
+ height: 100%;
136
+ visibility: hidden;
137
+ opacity: 0;
138
+ }
139
+
140
+ .cloud-panel[data-state="ready"] .cloud-panel__frame,
141
+ .cloud-panel[data-state="failed"] .cloud-panel__fallback,
142
+ .cloud-panel[data-resizing="true"] .cloud-panel__shield,
143
+ .cloud-panel[data-closing="true"] .cloud-panel__closing {
144
+ visibility: visible;
145
+ opacity: 1;
146
+ }
147
+
148
+ /* A loading page already takes focus and keys; the skeleton covers it until it reports ready. */
149
+ .cloud-panel[data-state="loading"] .cloud-panel__frame {
150
+ visibility: visible;
151
+ }
152
+
153
+ .cloud-panel[data-state="loading"] .cloud-panel__skeleton {
154
+ visibility: visible;
155
+ opacity: 1;
156
+ }
157
+
158
+ .cloud-panel__frame {
159
+ display: block;
160
+ border: 0;
161
+ background: var(--k2b-surface);
162
+ }
163
+
164
+ .cloud-panel__skeleton {
165
+ z-index: 1;
166
+ display: flex;
167
+ flex-direction: column;
168
+ gap: 0.75rem;
169
+ padding: 0.5rem 1rem;
170
+ background: var(--k2b-surface);
171
+ pointer-events: none;
172
+ }
173
+
174
+ .cloud-panel__skeleton > span {
175
+ display: block;
176
+ height: 0.75rem;
177
+ border-radius: 0.375rem;
178
+ background: var(--k2b-surface-muted);
179
+ }
180
+
181
+ .cloud-panel__skeleton > span:nth-child(2) {
182
+ width: 85%;
183
+ }
184
+
185
+ .cloud-panel__skeleton > span:last-child {
186
+ width: 60%;
187
+ }
188
+
189
+ @media (prefers-reduced-motion: no-preference) {
190
+ html:not([data-k2b-motion="reduced"]) .cloud-panel[data-state="loading"] .cloud-panel__skeleton > span {
191
+ animation: cloud-panel-pulse 1.4s ease-in-out infinite;
192
+ }
193
+ }
194
+
195
+ @keyframes cloud-panel-pulse {
196
+ 50% {
197
+ opacity: 0.55;
198
+ }
199
+ }
200
+
201
+ .cloud-panel__fallback {
202
+ z-index: 1;
203
+ display: flex;
204
+ align-items: center;
205
+ justify-content: center;
206
+ padding: 1.5rem;
207
+ background: var(--k2b-surface);
208
+ }
209
+
210
+ /* Takes the pointer from the iframe while the separator drags. */
211
+ .cloud-panel__shield {
212
+ z-index: 2;
213
+ cursor: col-resize;
214
+ }
215
+
216
+ /* Dragging clearly below 20 rem: the panel fades and says what release does. */
217
+ .cloud-panel__closing {
218
+ z-index: 3;
219
+ display: flex;
220
+ align-items: center;
221
+ justify-content: center;
222
+ padding: 1rem;
223
+ background: color-mix(in srgb, var(--k2b-surface) 82%, transparent);
224
+ pointer-events: none;
225
+ }
226
+
227
+ .cloud-panel__closing > span {
228
+ display: inline-flex;
229
+ align-items: center;
230
+ gap: 0.5rem;
231
+ padding: 0.5rem 0.75rem;
232
+ border-radius: var(--k2b-radius-control);
233
+ background: var(--k2b-surface-muted);
234
+ color: var(--k2b-text);
235
+ font-size: 0.875rem;
236
+ font-weight: 500;
237
+ }
238
+
239
+ .cloud-panel__closing .ti {
240
+ font-size: 1rem;
241
+ color: var(--k2b-text-muted);
242
+ }
243
+
244
+ .cloud-panel__grabber {
245
+ display: none;
246
+ }
247
+
248
+ /* The separator sits in the gap at the panel's start edge. */
249
+ .cloud-panel__resize {
250
+ position: absolute !important;
251
+ inset-block: 0;
252
+ inset-inline-start: calc(-6px - var(--cloud-card-inset, 0.5rem) / 2);
253
+ z-index: 4;
254
+ }
255
+
256
+ /* Column: the app area is the AppWorkspace host, so the app lays out by its narrowed width. */
257
+ @media (min-width: 1024px) {
258
+ .cloud-app-canvas[data-panel="open"] > .cloud-panel {
259
+ display: block;
260
+ flex: none;
261
+ align-self: stretch;
262
+ width: var(--cloud-panel-width);
263
+ margin-inline-start: var(--cloud-card-inset, 0.5rem);
264
+ }
265
+
266
+ .cloud-app-canvas[data-panel="open"] > [data-cloud-app-area] {
267
+ container: k2b-app-area / inline-size;
268
+ }
269
+ }
270
+
271
+ /* Drawer: over the app's right edge; the app keeps its width. Not modal. */
272
+ @media (min-width: 768px) {
273
+ @media not all and (min-width: 1024px) {
274
+ .cloud-app-canvas[data-panel="open"] > .cloud-panel {
275
+ position: absolute;
276
+ z-index: 20;
277
+ inset-block: calc(0.5rem + env(safe-area-inset-top, 0px)) calc(0.5rem + env(safe-area-inset-bottom, 0px));
278
+ inset-inline-end: calc(0.5rem + env(safe-area-inset-right, 0px));
279
+ display: block;
280
+ width: min(var(--cloud-panel-width), calc(100% - 1rem));
281
+ }
282
+
283
+ .cloud-panel__card {
284
+ border-radius: var(--k2b-radius-dialog);
285
+ box-shadow: var(--k2b-shadow-float);
286
+ }
287
+
288
+ .cloud-panel__resize {
289
+ display: none !important;
290
+ }
291
+ }
292
+ }
293
+
294
+ /* Sheet: from the bottom, a little over half the height; the grabber makes it full. */
295
+ @media not all and (min-width: 768px) {
296
+ .cloud-app-canvas[data-panel-sheet="open"] > .cloud-panel {
297
+ position: fixed;
298
+ z-index: 30;
299
+ inset-inline: 0;
300
+ bottom: 0;
301
+ display: block;
302
+ height: 58dvh;
303
+ }
304
+
305
+ /* The page's last row scrolls up above the sheet. Only the end of the page
306
+ grows, so nothing above it moves; full pages own their own scrollers. */
307
+ .cloud-app-canvas[data-layout-authenticated][data-panel-sheet="open"]:not([data-layout-full-page]) .layout-content-main {
308
+ padding-bottom: max(58dvh, var(--cloud-tab-bar-space, 0px));
309
+ scroll-padding-bottom: max(58dvh, var(--cloud-tab-bar-space, 0px));
310
+ }
311
+
312
+ .cloud-app-canvas[data-panel-sheet="open"] > .cloud-panel[data-panel-size="full"] {
313
+ height: calc(100dvh - 2.875rem - env(safe-area-inset-top, 0px));
314
+ }
315
+
316
+ .cloud-panel__card {
317
+ padding-bottom: env(safe-area-inset-bottom, 0px);
318
+ border-radius: 1.125rem 1.125rem 0 0;
319
+ box-shadow: 0 -10px 36px rgb(9 9 11 / 0.18);
320
+ }
321
+
322
+ .dark .cloud-panel__card {
323
+ border-bottom-color: transparent;
324
+ box-shadow: 0 -10px 36px rgb(0 0 0 / 0.5);
325
+ }
326
+
327
+ .cloud-panel__grabber {
328
+ display: flex;
329
+ flex: none;
330
+ align-items: center;
331
+ justify-content: center;
332
+ width: 100%;
333
+ height: 1.25rem;
334
+ padding: 0;
335
+ border: 0;
336
+ background: transparent;
337
+ }
338
+
339
+ .cloud-panel__grabber > span {
340
+ width: 2.375rem;
341
+ height: 0.3125rem;
342
+ border-radius: 999px;
343
+ background: var(--k2b-border-strong);
344
+ }
345
+
346
+ .cloud-panel__head {
347
+ padding-top: 0;
348
+ }
349
+
350
+ .cloud-panel__resize {
351
+ display: none !important;
352
+ }
353
+ }
354
+
355
+ @media print {
356
+ .cloud-panel {
357
+ display: none !important;
358
+ }
359
+ }
360
+
361
+ /* The provider's page inside the iframe: the panel surface from the first paint. */
362
+ .cloud-panel-page {
363
+ display: flex;
364
+ height: 100dvh;
365
+ min-height: 0;
366
+ flex-direction: column;
367
+ background: var(--k2b-surface);
368
+ }
369
+
370
+ html:has([data-cloud-panel-page]),
371
+ html:has([data-cloud-panel-page]) body {
372
+ background: var(--k2b-surface);
373
+ }
@@ -428,6 +428,31 @@
428
428
  }
429
429
  }
430
430
 
431
+ /* On a phone the resource picker is a full-screen sheet: the whole width, from
432
+ below the status bar to the bottom edge, rounded at the top. Its results
433
+ take the room the search field and footer leave. Global search keeps its
434
+ floating card. */
435
+ @media (max-width: 47.999rem) {
436
+ .cloud-search-dialog.cloud-search-dialog--picker {
437
+ --search-top: max(0.5rem, env(safe-area-inset-top, 0px));
438
+ --search-max-height: calc(100dvh - var(--search-top));
439
+ inset: var(--search-top) 0 auto;
440
+ transform: none;
441
+ width: 100%;
442
+ height: var(--search-max-height);
443
+ border-width: 1px 0 0;
444
+ border-radius: var(--ui-radius-frame) var(--ui-radius-frame) 0 0;
445
+ }
446
+ .cloud-search-dialog--picker > .cloud-search-dialog__content,
447
+ .cloud-search-dialog--picker .cloud-resource-search {
448
+ height: 100%;
449
+ }
450
+ .cloud-search-dialog--picker .cloud-resource-search {
451
+ --search-body-height: max(0px, calc(var(--search-max-height) - 12rem));
452
+ padding-bottom: max(0.5rem, env(safe-area-inset-bottom, 0px));
453
+ }
454
+ }
455
+
431
456
  /* Border grips belong only to desktop Spotlight, never the resource picker. */
432
457
  .cloud-search-dialog:has(.cloud-global-search) {
433
458
  overflow: visible;
@@ -15,7 +15,16 @@
15
15
  --ui-hover: rgb(239 240 239 / 0.9);
16
16
  --ui-active: rgb(229 231 230 / 0.94);
17
17
  --ui-selected: color-mix(in srgb, var(--color-blue-500) 7%, var(--ui-surface));
18
- --ui-app-accent-text: color-mix(in srgb, var(--app-accent, var(--color-blue-500)) 78%, black);
18
+ /* app-color.ts computes these defaults from --color-blue-500; app-color.test.ts pins them. */
19
+ --app-ink-light: #2264c9;
20
+ --app-ink-dark: #589aff;
21
+ --app-selected-light: #f0f6ff;
22
+ --app-selected-dark: #15243b;
23
+ --app-selected-nav-light: #e0e8f6;
24
+ --app-selected-nav-dark: #22334e;
25
+ --ui-app-accent-text: var(--app-ink-light);
26
+ --ui-app-selected: var(--app-selected-light);
27
+ --ui-app-selected-nav: var(--app-selected-nav-light);
19
28
  --ui-app-accent-border: var(--app-accent, var(--color-blue-500));
20
29
  --ui-focus: inset 0 0 0 2px color-mix(in srgb, var(--color-blue-500) 45%, transparent);
21
30
  --ui-action-primary-surface: var(--color-zinc-900);
@@ -143,7 +152,9 @@
143
152
  --ui-hover: #252c35;
144
153
  --ui-active: color-mix(in srgb, var(--ui-hover) 72%, var(--ui-border-strong));
145
154
  --ui-selected: color-mix(in srgb, var(--color-blue-400) 12%, var(--ui-surface));
146
- --ui-app-accent-text: color-mix(in srgb, var(--app-accent, var(--color-blue-400)) 58%, white);
155
+ --ui-app-accent-text: var(--app-ink-dark);
156
+ --ui-app-selected: var(--app-selected-dark);
157
+ --ui-app-selected-nav: var(--app-selected-nav-dark);
147
158
  --ui-app-accent-border: color-mix(in srgb, var(--app-accent, var(--color-blue-400)) 58%, white);
148
159
  --ui-focus: inset 0 0 0 2px color-mix(in srgb, var(--color-blue-400) 55%, transparent);
149
160
  --ui-action-primary-surface: var(--color-zinc-100);
@@ -186,23 +197,29 @@
186
197
  --ui-shadow-workspace-edge: -3px 0 6px -5px rgb(0 0 0 / 0.28), 3px 0 6px -5px rgb(0 0 0 / 0.28);
187
198
  }
188
199
 
189
- /* App appearance is applied on the canvas rather than :root. Recompute the
190
- derived accent roles here so an app never inherits the platform fallback. */
200
+ /* Custom properties resolve where declared. Each app scope must re-read its
201
+ inline per-app values instead of inheriting the resolved platform defaults. */
191
202
  .cloud-app-canvas,
192
203
  .app-accent-scope {
193
- --ui-app-accent-text: color-mix(in srgb, var(--app-accent, var(--color-blue-500)) 78%, black);
204
+ --ui-app-accent-text: var(--app-ink-light);
205
+ --ui-app-selected: var(--app-selected-light);
206
+ --ui-app-selected-nav: var(--app-selected-nav-light);
194
207
  --ui-app-accent-border: var(--app-accent, var(--color-blue-500));
195
208
  /* Portable UI components consume their own optional hook; Cloud alone maps
196
209
  it to the active app appearance. */
197
210
  --k2b-app-workspace-active: var(--ui-app-accent-text);
211
+ --k2b-app-workspace-selected: var(--ui-app-selected-nav);
198
212
  --k2b-detail-panel-accent: var(--ui-app-accent-text);
199
213
  --k2b-link-accent: var(--ui-app-accent-text);
200
214
  }
201
215
 
202
216
  .dark :is(.cloud-app-canvas, .app-accent-scope) {
203
- --ui-app-accent-text: color-mix(in srgb, var(--app-accent, var(--color-blue-400)) 58%, white);
217
+ --ui-app-accent-text: var(--app-ink-dark);
218
+ --ui-app-selected: var(--app-selected-dark);
219
+ --ui-app-selected-nav: var(--app-selected-nav-dark);
204
220
  --ui-app-accent-border: color-mix(in srgb, var(--app-accent, var(--color-blue-400)) 58%, white);
205
221
  --k2b-app-workspace-active: var(--ui-app-accent-text);
222
+ --k2b-app-workspace-selected: var(--ui-app-selected-nav);
206
223
  --k2b-detail-panel-accent: var(--ui-app-accent-text);
207
224
  --k2b-link-accent: var(--ui-app-accent-text);
208
225
  }
@@ -226,12 +226,12 @@
226
226
  @utility sidebar-item-active {
227
227
  position: relative;
228
228
  background: var(--ui-selected);
229
- color: color-mix(in srgb, var(--app-accent, var(--color-blue-500)) 78%, black);
229
+ color: var(--ui-app-accent-text);
230
230
  @apply font-medium;
231
231
  box-shadow: var(--ui-shadow-surface);
232
232
 
233
233
  &:hover {
234
- color: color-mix(in srgb, var(--app-accent, var(--color-blue-500)) 88%, black);
234
+ color: var(--ui-app-accent-text);
235
235
  }
236
236
 
237
237
  &::before {
@@ -241,14 +241,6 @@
241
241
  }
242
242
  }
243
243
 
244
- .dark .sidebar-item-active {
245
- color: color-mix(in srgb, var(--app-accent, var(--color-blue-400)) 58%, white);
246
- }
247
-
248
- .dark .sidebar-item-active:hover {
249
- color: color-mix(in srgb, var(--app-accent, var(--color-blue-400)) 45%, white);
250
- }
251
-
252
244
  @utility sidebar-item-meta {
253
245
  @apply mt-0.5 text-left text-xs leading-tight text-dimmed;
254
246
  }
@@ -723,12 +715,12 @@
723
715
  border-left: 0;
724
716
  position: relative;
725
717
  background: var(--ui-selected);
726
- color: color-mix(in srgb, var(--app-accent, var(--color-blue-500)) 78%, black);
718
+ color: var(--ui-app-accent-text);
727
719
  @apply rounded-lg font-medium;
728
720
  box-shadow: var(--ui-shadow-surface);
729
721
 
730
722
  &:hover {
731
- color: color-mix(in srgb, var(--app-accent, var(--color-blue-500)) 88%, black);
723
+ color: var(--ui-app-accent-text);
732
724
  }
733
725
 
734
726
  &::before {
@@ -738,14 +730,6 @@
738
730
  }
739
731
  }
740
732
 
741
- .dark .list-item-active {
742
- color: color-mix(in srgb, var(--app-accent, var(--color-blue-400)) 58%, white);
743
- }
744
-
745
- .dark .list-item-active:hover {
746
- color: color-mix(in srgb, var(--app-accent, var(--color-blue-400)) 45%, white);
747
- }
748
-
749
733
  @utility section-label {
750
734
  @apply text-[0.6875rem] font-medium normal-case tracking-normal mb-5 text-dimmed select-none;
751
735
  }
@@ -1115,7 +1099,7 @@
1115
1099
  }
1116
1100
 
1117
1101
  .sidebar-icon-action-active {
1118
- color: color-mix(in srgb, var(--app-accent, var(--color-blue-500)) 78%, black);
1102
+ color: var(--ui-app-accent-text);
1119
1103
  background: var(--ui-selected);
1120
1104
  box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--app-accent, var(--color-blue-500)) 20%, transparent);
1121
1105
  }
@@ -10,11 +10,28 @@
10
10
  * A schedule tick is an event. A button press is an event. An inbound message
11
11
  * is an event. Nothing needs its own registration path.
12
12
  */
13
+ import { Buffer } from "node:buffer";
13
14
  import { type SQL, sql } from "bun";
14
15
  import type { WorkflowJsonValue } from "../contracts";
15
16
  import { createWorkflowRun } from "./runs";
16
17
  import { withTransaction } from "./transaction";
17
18
 
19
+ /** UTF-8 bytes of serialized JSON, independently for data and context. */
20
+ export const WORKFLOW_EVENT_MAX_PAYLOAD_BYTES = 16 * 1024 * 1024;
21
+
22
+ export class WorkflowEventTooLargeError extends Error {
23
+ readonly code = "WORKFLOW_EVENT_TOO_LARGE";
24
+ readonly limitBytes = WORKFLOW_EVENT_MAX_PAYLOAD_BYTES;
25
+
26
+ constructor(
27
+ readonly field: "data" | "context",
28
+ readonly bytes: number,
29
+ ) {
30
+ super(`Workflow event ${field} is ${bytes} bytes; the limit is ${WORKFLOW_EVENT_MAX_PAYLOAD_BYTES} bytes (16 MiB).`);
31
+ this.name = "WorkflowEventTooLargeError";
32
+ }
33
+ }
34
+
18
35
  export type WorkflowEventInput = {
19
36
  appId: string;
20
37
  scopeId: string;
@@ -173,6 +190,13 @@ export const emitWorkflowEvent = async (
173
190
  const context = event.context ?? {};
174
191
  const authorization = event.authorization ?? {};
175
192
 
193
+ // Validate before even opening a savepoint: callers may catch this inside
194
+ // their own transaction and continue writing domain state.
195
+ for (const field of ["data", "context"] as const) {
196
+ const bytes = Buffer.byteLength(JSON.stringify(field === "data" ? data : context), "utf8");
197
+ if (bytes > WORKFLOW_EVENT_MAX_PAYLOAD_BYTES) throw new WorkflowEventTooLargeError(field, bytes);
198
+ }
199
+
176
200
  return withTransaction(options.db, async (tx) => {
177
201
  const inserted = await tx<{ id: string }[]>`
178
202
  INSERT INTO workflows.event (
@@ -45,8 +45,10 @@ export {
45
45
  emitWorkflowEvent,
46
46
  listUndispatchedWorkflowEvents,
47
47
  type UndispatchedWorkflowEvent,
48
+ WORKFLOW_EVENT_MAX_PAYLOAD_BYTES,
48
49
  type WorkflowEmission,
49
50
  type WorkflowEventInput,
51
+ WorkflowEventTooLargeError,
50
52
  } from "./events";
51
53
  export {
52
54
  getWorkflowRun,
@@ -66,6 +68,12 @@ export {
66
68
  type WorkflowStepSummary,
67
69
  workflowHealth,
68
70
  } from "./observability";
71
+ export {
72
+ expireWorkflowEventPayloads,
73
+ expireWorkflowEventPayloadsBatch,
74
+ startWorkflowEventRetention,
75
+ WORKFLOW_EVENT_RETENTION_DAYS,
76
+ } from "./retention";
69
77
  export {
70
78
  beginWorkflowEffect,
71
79
  claimWorkflowRun,
@@ -387,6 +387,7 @@ export type WorkflowRunDetail = WorkflowRunSummary & {
387
387
  source: string;
388
388
  eventId: string | null;
389
389
  eventData: Record<string, WorkflowJsonValue> | null;
390
+ eventPayloadExpired: boolean;
390
391
  steps: WorkflowStepSummary[];
391
392
  children: Record<WorkflowRunState, number>;
392
393
  };
@@ -412,10 +413,13 @@ export const getWorkflowRun = async (runId: string, options: { db?: SQL } = {}):
412
413
  source: string;
413
414
  event_id: string | null;
414
415
  event_data: Record<string, WorkflowJsonValue> | null;
416
+ event_payload_expired: boolean;
415
417
  })[]
416
418
  >`
417
419
  SELECT ${db.unsafe(RUN_SELECT)},
418
- r.inputs, r.result, r.effects_used, v.effect_budget, v.source, r.event_id, e.data AS event_data
420
+ r.inputs, r.result, r.effects_used, v.effect_budget, v.source, r.event_id,
421
+ CASE WHEN e.payload_expired_at IS NULL THEN e.data END AS event_data,
422
+ (e.payload_expired_at IS NOT NULL) AS event_payload_expired
419
423
  ${db.unsafe(RUN_FROM)}
420
424
  WHERE r.id = ${runId}::uuid
421
425
  `;
@@ -438,6 +442,7 @@ export const getWorkflowRun = async (runId: string, options: { db?: SQL } = {}):
438
442
  source: row.source,
439
443
  eventId: row.event_id,
440
444
  eventData: row.event_data,
445
+ eventPayloadExpired: row.event_payload_expired,
441
446
  children,
442
447
  steps,
443
448
  };