@k2b/cloud 0.28.0 → 0.29.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 (101) hide show
  1. package/package.json +7 -5
  2. package/src/_internal/cli-plugins.ts +18 -0
  3. package/src/_internal/define-app.ts +21 -5
  4. package/src/_internal/registry-validation.ts +30 -1
  5. package/src/_internal/runtime-context.ts +1 -0
  6. package/src/access/PermissionEditor.tsx +11 -4
  7. package/src/ai/assistant-models.ts +25 -4
  8. package/src/ai/audio-tool.ts +3 -10
  9. package/src/ai/capabilities.ts +35 -9
  10. package/src/ai/chat/blocks.tsx +97 -17
  11. package/src/ai/chat/builtin-tools.tsx +3 -1
  12. package/src/ai/chat/composer-adapter.ts +8 -0
  13. package/src/ai/chat/live-turn.browser-harness.tsx +12 -3
  14. package/src/ai/chat/message-actions.tsx +1 -1
  15. package/src/ai/chat/message-utils.ts +28 -6
  16. package/src/ai/chat/messages.ts +16 -0
  17. package/src/ai/chat/presentation.tsx +1 -8
  18. package/src/ai/chat/tool-names.ts +157 -0
  19. package/src/ai/chat/turn-layout.ts +12 -0
  20. package/src/ai/chat/turn-view.tsx +15 -7
  21. package/src/ai/chat-tasks.ts +16 -3
  22. package/src/ai/code-mode-skill.ts +4 -4
  23. package/src/ai/compaction.ts +22 -3
  24. package/src/ai/default-tools.ts +9 -2
  25. package/src/ai/executor.ts +127 -40
  26. package/src/ai/file-media-type.ts +4 -0
  27. package/src/ai/files-store.ts +17 -10
  28. package/src/ai/http.ts +11 -10
  29. package/src/ai/index.ts +1 -0
  30. package/src/ai/memories.ts +2 -0
  31. package/src/ai/memory-learning.ts +59 -5
  32. package/src/ai/memory-workflow-evidence.ts +35 -10
  33. package/src/ai/projects-routes.ts +5 -1
  34. package/src/ai/projects.ts +5 -1
  35. package/src/ai/protocol.ts +34 -1
  36. package/src/ai/provider.ts +16 -1
  37. package/src/ai/quota-provider.ts +4 -1
  38. package/src/ai/routes.ts +20 -10
  39. package/src/ai/settings.ts +54 -17
  40. package/src/ai/skill-seeds.ts +11 -3
  41. package/src/ai/store.ts +56 -20
  42. package/src/ai/structured.ts +2 -1
  43. package/src/ai/system-prompt.ts +5 -1
  44. package/src/ai/transcription.ts +58 -13
  45. package/src/ai/turn-failure.ts +5 -1
  46. package/src/ai/turn-guidance.ts +505 -0
  47. package/src/ai/types.ts +1 -0
  48. package/src/ai/ui.tsx +1 -0
  49. package/src/api/admin-core-settings.ts +16 -1
  50. package/src/api/me.ts +42 -1
  51. package/src/browser/FileChooser.tsx +71 -295
  52. package/src/browser/FileSaver.tsx +407 -0
  53. package/src/browser/app-badges.ts +16 -0
  54. package/src/browser/choose-files.browser-harness.tsx +22 -6
  55. package/src/browser/choose-files.tsx +23 -36
  56. package/src/browser/file-chooser-messages.ts +64 -0
  57. package/src/browser/file-providers.ts +258 -8
  58. package/src/browser/files.tsx +26 -3
  59. package/src/browser/notifications.ts +37 -1
  60. package/src/browser/provider-folder.tsx +301 -0
  61. package/src/browser/provider-list.ts +39 -0
  62. package/src/browser/save-files.browser-harness.tsx +35 -0
  63. package/src/browser/save-files.tsx +107 -0
  64. package/src/contracts/app.ts +10 -0
  65. package/src/contracts/commands.ts +3 -1
  66. package/src/contracts/notification-group.ts +6 -0
  67. package/src/contracts/notification-types.ts +6 -0
  68. package/src/contracts/registry.ts +1 -0
  69. package/src/contracts/user-notifications.ts +74 -0
  70. package/src/server/middleware/request-logger.ts +11 -2
  71. package/src/services/accounts/avatar.ts +5 -2
  72. package/src/services/image-metadata.ts +438 -0
  73. package/src/services/index.ts +1 -0
  74. package/src/services/logging/redaction.ts +50 -0
  75. package/src/services/mandates/index.ts +4 -2
  76. package/src/services/mandates/policy.ts +20 -0
  77. package/src/services/notifications/browser.ts +48 -15
  78. package/src/services/notifications/dispatcher.ts +31 -1
  79. package/src/services/notifications/platform.ts +32 -4
  80. package/src/services/notifications/preview.ts +23 -0
  81. package/src/services/notifications/quiet.ts +153 -0
  82. package/src/services/notifications/user.ts +6 -0
  83. package/src/services/pdf/template-preview.ts +2 -1
  84. package/src/services/settings/app.ts +3 -0
  85. package/src/services/settings/store.ts +4 -1
  86. package/src/services/weather/geo.ts +6 -2
  87. package/src/shared/ai-platform-prompt.ts +2 -2
  88. package/src/shared/redirect.ts +2 -1
  89. package/src/shared/template-rendering.ts +120 -7
  90. package/src/ssr/AppBadge.tsx +30 -0
  91. package/src/ssr/AppLaunchpadPanel.tsx +47 -26
  92. package/src/ssr/IslandError.tsx +33 -0
  93. package/src/ssr/Layout.tsx +3 -0
  94. package/src/ssr/LayoutHelp.tsx +4 -2
  95. package/src/ssr/RailApps.island.tsx +9 -2
  96. package/src/ssr/app-badges.ts +93 -0
  97. package/src/ssr/rail-messages.ts +4 -0
  98. package/src/ssr/rail-navigation.ts +2 -0
  99. package/src/styles/effects.css +23 -0
  100. package/src/styles/file-chooser.css +25 -0
  101. package/src/styles/utilities-navigation.css +41 -0
@@ -3,6 +3,7 @@ import { IconButton, ScrollArea, TextInput, useLocale } from "@k2b/ui";
3
3
  import { createSignal, For, type JSX, Show } from "solid-js";
4
4
  import { Dynamic } from "solid-js/web";
5
5
  import type { CloudTheme } from "../shared/theme";
6
+ import { AppBadge, useAppBadgeDescription } from "./AppBadge";
6
7
  import { MobileProfileActions } from "./MobileProfileActions";
7
8
  import { platformMessages } from "./platform-messages";
8
9
  import { openRailEditor } from "./RailEditor";
@@ -17,6 +18,8 @@ export type AppLaunchpadApp = {
17
18
  href: string;
18
19
  description?: string;
19
20
  accent?: string;
21
+ /** The app's `nav.badge` endpoint. */
22
+ badge?: string;
20
23
  };
21
24
 
22
25
  export type AppLaunchpadLegalLink = {
@@ -119,39 +122,57 @@ export const AppLaunchpadPanel = (
119
122
  <section class="mb-5" aria-label={t().shortcuts}>
120
123
  <div class="flex flex-wrap justify-center gap-3">
121
124
  <For each={shortcuts()}>
122
- {(shortcut) => (
123
- <a
124
- onClick={follow}
125
- href={shortcut.href}
126
- class="group flex w-16 min-w-0 flex-col items-center gap-1 rounded-lg p-1 text-center focus-ui"
127
- >
128
- <span class="grid h-9 w-9 place-items-center rounded-xl bg-[var(--ui-hover)] text-lg transition-colors group-hover:bg-[var(--ui-active)]">
129
- <i class={shortcut.iconClass} aria-hidden="true" />
130
- </span>
131
- <span class="max-w-full truncate text-[11px] font-medium text-primary dark:text-white">{shortcut.label}</span>
132
- </a>
133
- )}
125
+ {(shortcut) => {
126
+ const badge = useAppBadgeDescription(() => shortcut.badge);
127
+ return (
128
+ <a
129
+ onClick={follow}
130
+ href={shortcut.href}
131
+ class="group flex w-16 min-w-0 flex-col items-center gap-1 rounded-lg p-1 text-center focus-ui"
132
+ >
133
+ <span class="relative grid h-9 w-9 place-items-center rounded-xl bg-[var(--ui-hover)] text-lg transition-colors group-hover:bg-[var(--ui-active)]">
134
+ <i class={shortcut.iconClass} aria-hidden="true" />
135
+ <AppBadge endpoint={shortcut.badge} />
136
+ </span>
137
+ <span class="max-w-full truncate text-[11px] font-medium text-primary dark:text-white">
138
+ {shortcut.label}
139
+ <Show when={badge()}>
140
+ <span class="sr-only">, {badge()}</span>
141
+ </Show>
142
+ </span>
143
+ </a>
144
+ );
145
+ }}
134
146
  </For>
135
147
  </div>
136
148
  </section>
137
149
  </Show>
138
150
  <div class="launchpad-apps-grid flex flex-wrap justify-center gap-x-4 gap-y-4 sm:gap-x-7 sm:gap-y-6">
139
151
  <For each={apps()}>
140
- {(app) => (
141
- <a
142
- onClick={follow}
143
- href={app.href}
144
- class="launchpad-app group flex w-[4.75rem] min-w-0 flex-col items-center gap-1.5 rounded-2xl p-1 text-center focus-ui sm:w-[6.25rem] sm:gap-2 sm:p-2"
145
- >
146
- <span
147
- class="app-icon grid h-12 w-12 place-items-center rounded-[0.95rem] text-[1.25rem] sm:h-16 sm:w-16 sm:rounded-[1.25rem] sm:text-[1.7rem]"
148
- style={appIconStyle(app)}
152
+ {(app) => {
153
+ const badge = useAppBadgeDescription(() => app.badge);
154
+ return (
155
+ <a
156
+ onClick={follow}
157
+ href={app.href}
158
+ class="launchpad-app group flex w-[4.75rem] min-w-0 flex-col items-center gap-1.5 rounded-2xl p-1 text-center focus-ui sm:w-[6.25rem] sm:gap-2 sm:p-2"
149
159
  >
150
- <i class={app.iconClass} />
151
- </span>
152
- <span class="max-w-full truncate text-[11px] font-medium text-primary sm:text-xs dark:text-white">{app.label}</span>
153
- </a>
154
- )}
160
+ <span
161
+ class="app-icon relative grid h-12 w-12 place-items-center rounded-[0.95rem] text-[1.25rem] sm:h-16 sm:w-16 sm:rounded-[1.25rem] sm:text-[1.7rem]"
162
+ style={appIconStyle(app)}
163
+ >
164
+ <i class={app.iconClass} />
165
+ <AppBadge endpoint={app.badge} />
166
+ </span>
167
+ <span class="max-w-full truncate text-[11px] font-medium text-primary sm:text-xs dark:text-white">
168
+ {app.label}
169
+ <Show when={badge()}>
170
+ <span class="sr-only">, {badge()}</span>
171
+ </Show>
172
+ </span>
173
+ </a>
174
+ );
175
+ }}
155
176
  </For>
156
177
  </div>
157
178
  <Show when={!apps().length && !shortcuts().length}>
@@ -0,0 +1,33 @@
1
+ import type { IslandErrorProps } from "@k2b/ssr";
2
+ import { i18n } from "@k2b/stdlib";
3
+ import { Button, Placeholder, useLocale } from "@k2b/ui";
4
+
5
+ const messages = i18n.define({
6
+ baseLocale: "en",
7
+ messages: {
8
+ en: { title: "This section could not be displayed.", retry: "Try again" },
9
+ de: { title: "Dieser Bereich konnte nicht angezeigt werden.", retry: "Erneut versuchen" },
10
+ },
11
+ });
12
+
13
+ /**
14
+ * Takes the place of an island or client component whose rendering failed.
15
+ * `defineApp` hands it to @k2b/ssr as `errorFallback`, so every island of every
16
+ * app gets it; the locale comes from `<html lang>`. "Try again" remounts the
17
+ * island with its original props.
18
+ */
19
+ export default function IslandError(props: IslandErrorProps) {
20
+ const locale = useLocale();
21
+ const t = () => messages.resolve([locale()]).t;
22
+ return (
23
+ <Placeholder
24
+ state="error"
25
+ title={t().title}
26
+ action={
27
+ <Button variant="secondary" onClick={() => props.reset()}>
28
+ {t().retry}
29
+ </Button>
30
+ }
31
+ />
32
+ );
33
+ }
@@ -61,6 +61,7 @@ function buildNavLinks(
61
61
  match: resolveNavMatch(app) ?? app.nav.href.split("?")[0] ?? app.nav.href,
62
62
  description: app.description,
63
63
  accent: app.appearance?.accent,
64
+ badge: app.nav.badge,
64
65
  } satisfies LayoutAppLink,
65
66
  }));
66
67
  const primary = links.filter((entry) => entry.section === "primary").map((entry) => entry.link);
@@ -75,6 +76,7 @@ function buildNavLinks(
75
76
  match: "/admin",
76
77
  description: t.platformAdministration,
77
78
  accent: undefined,
79
+ badge: undefined,
78
80
  });
79
81
  }
80
82
  return { primary, more };
@@ -166,6 +168,7 @@ export default function Layout(props: LayoutProps) {
166
168
  href: app.href,
167
169
  description: app.description,
168
170
  accent: app.accent,
171
+ badge: app.badge,
169
172
  }));
170
173
  const searchLinks = navigationSearchItems(
171
174
  runtime.apps,
@@ -95,11 +95,13 @@ const registerTopic = (topic: HelpTopic) => {
95
95
  /** Register an app's server-prepared Markdown manifest with the shared help UI. */
96
96
  export function LayoutHelpDocuments(props: LayoutHelpDocumentsProps) {
97
97
  onMount(() => {
98
- window.__cloudLayoutHelpPageBase = props.pageBase;
98
+ // Cleanup runs while this unmounts, when a parent `<Show>` may already have dropped the props' source.
99
+ const pageBase = props.pageBase;
100
+ window.__cloudLayoutHelpPageBase = pageBase;
99
101
  const disposers = props.documents.map((document) => registerTopic({ ...document, kind: "document" }));
100
102
  onCleanup(() => {
101
103
  disposers.forEach((dispose) => dispose());
102
- if (window.__cloudLayoutHelpPageBase === props.pageBase) delete window.__cloudLayoutHelpPageBase;
104
+ if (window.__cloudLayoutHelpPageBase === pageBase) delete window.__cloudLayoutHelpPageBase;
103
105
  });
104
106
  });
105
107
  return null;
@@ -1,25 +1,29 @@
1
1
  import { ScrollArea, Tooltip, useLocale } from "@k2b/ui";
2
2
  import { createMemo, createSignal, For, onCleanup, onMount, Show } from "solid-js";
3
3
  import type { RailSnapshot } from "../contracts/rail-preferences";
4
+ import { AppBadge, useAppBadgeDescription } from "./AppBadge";
4
5
  import { appAccentStyle } from "./app-appearance";
6
+ import { watchAppBadges } from "./app-badges";
5
7
  import { RAIL_PREFERENCES_EVENT, readRailContext } from "./rail-context";
6
8
  import { projectRailNavigation, type RailApp, type RailLink, railLinkActive } from "./rail-navigation";
7
9
 
8
10
  function RailIcon(props: { link: RailLink; currentPath: string }) {
9
11
  let target: HTMLAnchorElement | undefined;
10
12
  const active = () => railLinkActive(props.link, props.currentPath);
13
+ const badge = useAppBadgeDescription(() => props.link.badge);
11
14
  return (
12
15
  <>
13
16
  <a
14
17
  ref={target}
15
18
  href={props.link.href}
16
- class={`rail-item shrink-0 ${active() ? "rail-item-active" : ""}`}
17
- aria-label={props.link.label}
19
+ class={`rail-item relative shrink-0 ${active() ? "rail-item-active" : ""}`}
20
+ aria-label={badge() ? `${props.link.label}, ${badge()}` : props.link.label}
18
21
  aria-current={active() ? "page" : undefined}
19
22
  style={appAccentStyle(props.link.accent)}
20
23
  >
21
24
  <i class={`${props.link.iconClass} text-base`} aria-hidden="true" />
22
25
  <span class="sr-only">{props.link.label}</span>
26
+ <AppBadge endpoint={props.link.badge} />
23
27
  </a>
24
28
  <Tooltip target={() => target} content={props.link.label} placement="right" delay={0} />
25
29
  </>
@@ -36,10 +40,13 @@ export default function RailApps(props: { apps: RailApp[]; settings: RailSnapsho
36
40
  const syncLocation = () => setCurrentPath(`${window.location.pathname}${window.location.search}${window.location.hash}`);
37
41
  sync();
38
42
  syncLocation();
43
+ // Every visible app, not only the rail's: the app grid shows the same counts.
44
+ const stopBadges = watchAppBadges(props.apps.flatMap((app) => (app.badge ? [app.badge] : [])));
39
45
  window.addEventListener(RAIL_PREFERENCES_EVENT, sync);
40
46
  window.addEventListener("popstate", syncLocation);
41
47
  window.addEventListener("hashchange", syncLocation);
42
48
  onCleanup(() => {
49
+ stopBadges();
43
50
  window.removeEventListener(RAIL_PREFERENCES_EVENT, sync);
44
51
  window.removeEventListener("popstate", syncLocation);
45
52
  window.removeEventListener("hashchange", syncLocation);
@@ -0,0 +1,93 @@
1
+ import { createSignal } from "solid-js";
2
+ import { APP_BADGES_REFRESH_EVENT } from "../browser/app-badges";
3
+
4
+ /**
5
+ * Shortest time between two scheduled reads in one tab, and the longest one
6
+ * read may take. Badges are a hint to open an app, not a live view, so one
7
+ * read per minute keeps them current while someone works elsewhere without a
8
+ * steady stream of requests per tab.
9
+ */
10
+ export const APP_BADGE_INTERVAL_MS = 60_000;
11
+
12
+ // Keyed by endpoint, so a pinned app shortcut and the app's own entry share one
13
+ // count. Only the browser ever writes it; server renders always see no badge.
14
+ const [counts, setCounts] = createSignal<ReadonlyMap<string, number>>(new Map());
15
+
16
+ /** The count for a declared `nav.badge` endpoint; 0 until the browser has read it. */
17
+ export const appBadgeCount = (endpoint: string | undefined): number => (endpoint ? (counts().get(endpoint) ?? 0) : 0);
18
+
19
+ /** A positive safe integer from an `AppNavBadge` body, else 0. */
20
+ export const parseAppBadge = (body: unknown): number => {
21
+ if (typeof body !== "object" || body === null || !("count" in body)) return 0;
22
+ const count = body.count;
23
+ return typeof count === "number" && Number.isSafeInteger(count) && count > 0 ? count : 0;
24
+ };
25
+
26
+ const readBadge = async (endpoint: string, signal: AbortSignal): Promise<number> => {
27
+ try {
28
+ const response = await fetch(endpoint, { credentials: "same-origin", headers: { accept: "application/json" }, signal });
29
+ return response.status === 200 ? parseAppBadge(await response.json()) : 0;
30
+ } catch {
31
+ // An unreachable or failing app shows no badge rather than a stale one.
32
+ return 0;
33
+ }
34
+ };
35
+
36
+ let watching = false;
37
+
38
+ /**
39
+ * Reads the badge endpoints after hydration, every interval while the tab is
40
+ * visible, when a hidden tab becomes visible again after the interval, and on
41
+ * `refreshAppBadges()`. A hidden tab sends nothing and keeps its schedule.
42
+ * Each endpoint has at most one read in flight, and its count shows as soon as
43
+ * that read settles, so a slow app delays only its own badge. A refresh while
44
+ * a read runs queues one more read after it instead of another request, and a
45
+ * read still running after the interval gives up. Returns a disposer.
46
+ */
47
+ export const watchAppBadges = (endpoints: readonly string[]): (() => void) => {
48
+ const unique = [...new Set(endpoints)];
49
+ if (typeof window === "undefined" || unique.length === 0 || watching) return () => {};
50
+ watching = true;
51
+ const stopped = new AbortController();
52
+ // Endpoints with a read in flight, mapped to whether one more read waits for it.
53
+ const running = new Map<string, boolean>();
54
+ let timer: ReturnType<typeof setTimeout> | undefined;
55
+ let last = Number.NEGATIVE_INFINITY;
56
+
57
+ const read = (endpoint: string) => {
58
+ if (running.has(endpoint)) {
59
+ running.set(endpoint, true);
60
+ return;
61
+ }
62
+ running.set(endpoint, false);
63
+ void readBadge(endpoint, AbortSignal.any([stopped.signal, AbortSignal.timeout(APP_BADGE_INTERVAL_MS)])).then((count) => {
64
+ if (stopped.signal.aborted) return;
65
+ setCounts((previous) => new Map(previous).set(endpoint, count));
66
+ const queued = running.get(endpoint);
67
+ running.delete(endpoint);
68
+ if (queued && document.visibilityState !== "hidden") read(endpoint);
69
+ });
70
+ };
71
+ const refresh = () => {
72
+ // Leave the pending read in place, so the tab still reads on time once it is visible again.
73
+ if (document.visibilityState === "hidden") return;
74
+ clearTimeout(timer);
75
+ last = Date.now();
76
+ timer = setTimeout(refresh, APP_BADGE_INTERVAL_MS);
77
+ for (const endpoint of unique) read(endpoint);
78
+ };
79
+ const resume = () => {
80
+ if (document.visibilityState !== "hidden" && Date.now() - last >= APP_BADGE_INTERVAL_MS) refresh();
81
+ };
82
+
83
+ document.addEventListener("visibilitychange", resume);
84
+ window.addEventListener(APP_BADGES_REFRESH_EVENT, refresh);
85
+ refresh();
86
+ return () => {
87
+ watching = false;
88
+ clearTimeout(timer);
89
+ stopped.abort();
90
+ document.removeEventListener("visibilitychange", resume);
91
+ window.removeEventListener(APP_BADGES_REFRESH_EVENT, refresh);
92
+ };
93
+ };
@@ -32,6 +32,8 @@ export const railMessages = i18n.define({
32
32
  saveFailed: "Could not save app bar settings. Your edits are still here.",
33
33
  conflict: "The app bar changed in another editor. Cancel and reopen this editor to load the latest settings.",
34
34
  invalid: "Check shortcut titles, links and icons. Each app can be pinned once; settings must fit within 16 KiB.",
35
+ badge: ({ count }: { count: number }) => `${count} new`,
36
+ badgeMany: "more than 99 new",
35
37
  },
36
38
  de: {
37
39
  managed: "Von der Administration bereitgestellt",
@@ -63,6 +65,8 @@ export const railMessages = i18n.define({
63
65
  conflict:
64
66
  "Die App-Leiste wurde in einem anderen Editor geändert. Brich ab und öffne den Editor erneut, um den aktuellen Stand zu laden.",
65
67
  invalid: "Prüfe Titel, Links und Icons. Jede App kann einmal angeheftet werden; die Einstellungen dürfen höchstens 16 KiB belegen.",
68
+ badge: ({ count }) => `${count} neu`,
69
+ badgeMany: "mehr als 99 neu",
66
70
  },
67
71
  },
68
72
  });
@@ -9,6 +9,8 @@ export type RailApp = {
9
9
  defaultVisible: boolean;
10
10
  description?: string;
11
11
  accent?: string;
12
+ /** The app's `nav.badge` endpoint. */
13
+ badge?: string;
12
14
  };
13
15
  export type RailLink = Omit<RailApp, "defaultVisible"> & { exact?: boolean };
14
16
 
@@ -892,3 +892,26 @@ dialog:has(.launchpad-panel)::backdrop {
892
892
  .k2b-ui .ai-response-info__rows dd { min-width: 0; margin: 0; overflow: hidden; color: var(--k2b-text); text-align: end; text-overflow: ellipsis; white-space: nowrap; font-variant-numeric: tabular-nums; }
893
893
  .k2b-ui .ai-response-info__tools { display: flex; flex-wrap: wrap; gap: 0.25rem 1rem; margin: 0; padding: 0; list-style: none; font-size: 0.8125rem; color: var(--k2b-text-secondary); }
894
894
  .k2b-ui .ai-response-info__tools > li { display: inline-flex; align-items: center; gap: 0.375rem; }
895
+
896
+ /* The image a step looked at: a small inset thumbnail whose box is fixed before the image loads, so nothing moves. */
897
+ .k2b-ui .k2b-chat-activity .ai-step-image {
898
+ display: flex;
899
+ width: 12rem;
900
+ max-width: 100%;
901
+ height: 8rem;
902
+ flex-shrink: 0;
903
+ align-items: center;
904
+ justify-content: center;
905
+ overflow: hidden;
906
+ padding: 0;
907
+ border-radius: var(--k2b-radius-control);
908
+ background: var(--ui-surface-muted);
909
+ box-shadow: var(--ui-control-recess);
910
+ }
911
+
912
+ .k2b-ui .k2b-chat-activity .ai-step-image img {
913
+ width: 100%;
914
+ height: 100%;
915
+ object-fit: cover;
916
+ object-position: top;
917
+ }
@@ -167,3 +167,28 @@
167
167
  gap: 0.5rem;
168
168
  margin-left: auto;
169
169
  }
170
+
171
+ /* A saved file's progress, or the field for another name when its name is taken. Every row keeps the field's
172
+ height, so a conflict never moves the rows below. */
173
+ .cloud-file-saver__slot {
174
+ display: flex;
175
+ align-items: center;
176
+ min-height: 2.25rem;
177
+ }
178
+
179
+ .cloud-file-saver__slot > .k2b-progress {
180
+ flex: 1 1 auto;
181
+ }
182
+
183
+ .cloud-file-saver__rename {
184
+ display: flex;
185
+ flex: 1 1 auto;
186
+ align-items: center;
187
+ gap: 0.5rem;
188
+ min-width: 0;
189
+ }
190
+
191
+ .cloud-file-saver__rename > :first-child {
192
+ flex: 1 1 auto;
193
+ min-width: 0;
194
+ }
@@ -771,6 +771,47 @@
771
771
  box-shadow: none;
772
772
  }
773
773
 
774
+ /* An app's count on its icon in the rail and the app grid. It is laid over the
775
+ icon's corner and never takes layout space, so a count appearing, growing to
776
+ "99+", or clearing moves nothing. The fill is the solid action colour, the
777
+ same as the sidebar's mention count. */
778
+ .cloud-app-badge {
779
+ position: absolute;
780
+ top: 0;
781
+ right: -0.25rem;
782
+ box-sizing: border-box;
783
+ min-width: 0.875rem;
784
+ height: 0.875rem;
785
+ padding-inline: 0.1875rem;
786
+ border-radius: 999px;
787
+ color: #ffffff;
788
+ background: var(--k2b-action-solid);
789
+ font-size: 0.625rem;
790
+ font-variant-numeric: tabular-nums;
791
+ font-weight: 600;
792
+ line-height: 0.875rem;
793
+ text-align: center;
794
+ white-space: nowrap;
795
+ pointer-events: none;
796
+ }
797
+
798
+ .app-icon > .cloud-app-badge {
799
+ top: -0.375rem;
800
+ right: -0.375rem;
801
+ min-width: 1.25rem;
802
+ height: 1.25rem;
803
+ padding-inline: 0.3125rem;
804
+ font-size: 0.6875rem;
805
+ line-height: 1.25rem;
806
+ }
807
+
808
+ @media (forced-colors: active) {
809
+ .cloud-app-badge {
810
+ outline: 1px solid CanvasText;
811
+ outline-offset: -1px;
812
+ }
813
+ }
814
+
774
815
  .rail-item-active:hover {
775
816
  color: color-mix(in srgb, var(--app-accent, var(--color-blue-500)) 88%, black);
776
817
  background: transparent;