@sero-ai/ui 0.3.2 → 0.4.1

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 (169) hide show
  1. package/.turbo/turbo-build.log +615 -0
  2. package/.turbo/turbo-typecheck.log +1 -1
  3. package/CHANGELOG.md +58 -0
  4. package/dist/components/context-editor/CapabilitySection.cjs +57 -0
  5. package/dist/components/context-editor/CapabilitySection.d.cts +20 -0
  6. package/dist/components/context-editor/CapabilitySection.d.ts +20 -0
  7. package/dist/components/context-editor/CapabilitySection.js +57 -0
  8. package/dist/components/context-editor/ContextEditor.cjs +108 -0
  9. package/dist/components/context-editor/ContextEditor.d.cts +26 -0
  10. package/dist/components/context-editor/ContextEditor.d.ts +26 -0
  11. package/dist/components/context-editor/ContextEditor.js +108 -0
  12. package/dist/components/context-editor/PresetBar.cjs +71 -0
  13. package/dist/components/context-editor/PresetBar.d.cts +19 -0
  14. package/dist/components/context-editor/PresetBar.d.ts +19 -0
  15. package/dist/components/context-editor/PresetBar.js +71 -0
  16. package/dist/components/context-editor/SkillsSection.cjs +31 -0
  17. package/dist/components/context-editor/SkillsSection.d.cts +14 -0
  18. package/dist/components/context-editor/SkillsSection.d.ts +14 -0
  19. package/dist/components/context-editor/SkillsSection.js +31 -0
  20. package/dist/components/context-editor/SystemPromptSection.cjs +58 -0
  21. package/dist/components/context-editor/SystemPromptSection.d.cts +25 -0
  22. package/dist/components/context-editor/SystemPromptSection.d.ts +25 -0
  23. package/dist/components/context-editor/SystemPromptSection.js +58 -0
  24. package/dist/components/context-editor/ToolsSection.cjs +31 -0
  25. package/dist/components/context-editor/ToolsSection.d.cts +14 -0
  26. package/dist/components/context-editor/ToolsSection.d.ts +14 -0
  27. package/dist/components/context-editor/ToolsSection.js +31 -0
  28. package/dist/components/context-editor/index.cjs +8 -0
  29. package/dist/components/context-editor/index.d.cts +5 -0
  30. package/dist/components/context-editor/index.d.ts +5 -0
  31. package/dist/components/context-editor/index.js +8 -0
  32. package/dist/components/context-editor/parts.cjs +184 -0
  33. package/dist/components/context-editor/parts.d.cts +32 -0
  34. package/dist/components/context-editor/parts.d.ts +32 -0
  35. package/dist/components/context-editor/parts.js +184 -0
  36. package/dist/components/context-editor/use-context-editor-model.cjs +194 -0
  37. package/dist/components/context-editor/use-context-editor-model.d.cts +57 -0
  38. package/dist/components/context-editor/use-context-editor-model.d.ts +57 -0
  39. package/dist/components/context-editor/use-context-editor-model.js +194 -0
  40. package/dist/components/dashboard/activity-list.cjs +59 -0
  41. package/dist/components/dashboard/activity-list.d.cts +27 -0
  42. package/dist/components/dashboard/activity-list.d.ts +27 -0
  43. package/dist/components/dashboard/activity-list.js +59 -0
  44. package/dist/components/dashboard/catalog.cjs +1 -0
  45. package/dist/components/dashboard/catalog.d.cts +13 -0
  46. package/dist/components/dashboard/catalog.d.ts +13 -0
  47. package/dist/components/dashboard/catalog.js +0 -0
  48. package/dist/components/dashboard/catalog.json +312 -0
  49. package/dist/components/dashboard/data-boundary.cjs +29 -0
  50. package/dist/components/dashboard/data-boundary.d.cts +21 -0
  51. package/dist/components/dashboard/data-boundary.d.ts +21 -0
  52. package/dist/components/dashboard/data-boundary.js +29 -0
  53. package/dist/components/dashboard/empty-state.cjs +49 -0
  54. package/dist/components/dashboard/empty-state.d.cts +20 -0
  55. package/dist/components/dashboard/empty-state.d.ts +20 -0
  56. package/dist/components/dashboard/empty-state.js +49 -0
  57. package/dist/components/dashboard/icon-button.cjs +40 -0
  58. package/dist/components/dashboard/icon-button.d.cts +24 -0
  59. package/dist/components/dashboard/icon-button.d.ts +24 -0
  60. package/dist/components/dashboard/icon-button.js +40 -0
  61. package/dist/components/dashboard/index.cjs +15 -0
  62. package/dist/components/dashboard/index.d.cts +24 -0
  63. package/dist/components/dashboard/index.d.ts +24 -0
  64. package/dist/components/dashboard/index.js +15 -0
  65. package/dist/components/dashboard/item-list.cjs +71 -0
  66. package/dist/components/dashboard/item-list.d.cts +28 -0
  67. package/dist/components/dashboard/item-list.d.ts +28 -0
  68. package/dist/components/dashboard/item-list.js +71 -0
  69. package/dist/components/dashboard/key-value.cjs +39 -0
  70. package/dist/components/dashboard/key-value.d.cts +17 -0
  71. package/dist/components/dashboard/key-value.d.ts +17 -0
  72. package/dist/components/dashboard/key-value.js +39 -0
  73. package/dist/components/dashboard/layout.cjs +193 -0
  74. package/dist/components/dashboard/layout.d.cts +91 -0
  75. package/dist/components/dashboard/layout.d.ts +91 -0
  76. package/dist/components/dashboard/layout.js +193 -0
  77. package/dist/components/dashboard/metric.cjs +98 -0
  78. package/dist/components/dashboard/metric.d.cts +44 -0
  79. package/dist/components/dashboard/metric.d.ts +44 -0
  80. package/dist/components/dashboard/metric.js +98 -0
  81. package/dist/components/dashboard/progress-ring.cjs +77 -0
  82. package/dist/components/dashboard/progress-ring.d.cts +25 -0
  83. package/dist/components/dashboard/progress-ring.d.ts +25 -0
  84. package/dist/components/dashboard/progress-ring.js +77 -0
  85. package/dist/components/dashboard/skeletons.cjs +54 -0
  86. package/dist/components/dashboard/skeletons.d.cts +15 -0
  87. package/dist/components/dashboard/skeletons.d.ts +15 -0
  88. package/dist/components/dashboard/skeletons.js +54 -0
  89. package/dist/components/dashboard/spacing.cjs +10 -0
  90. package/dist/components/dashboard/spacing.d.cts +4 -0
  91. package/dist/components/dashboard/spacing.d.ts +4 -0
  92. package/dist/components/dashboard/spacing.js +10 -0
  93. package/dist/components/dashboard/stale-indicator.cjs +42 -0
  94. package/dist/components/dashboard/stale-indicator.d.cts +15 -0
  95. package/dist/components/dashboard/stale-indicator.d.ts +15 -0
  96. package/dist/components/dashboard/stale-indicator.js +42 -0
  97. package/dist/components/dashboard/status.cjs +66 -0
  98. package/dist/components/dashboard/status.d.cts +15 -0
  99. package/dist/components/dashboard/status.d.ts +15 -0
  100. package/dist/components/dashboard/status.js +66 -0
  101. package/dist/components/dashboard/tone.cjs +34 -0
  102. package/dist/components/dashboard/tone.d.cts +15 -0
  103. package/dist/components/dashboard/tone.d.ts +15 -0
  104. package/dist/components/dashboard/tone.js +34 -0
  105. package/dist/components/dashboard/typography.cjs +99 -0
  106. package/dist/components/dashboard/typography.d.cts +57 -0
  107. package/dist/components/dashboard/typography.d.ts +57 -0
  108. package/dist/components/dashboard/typography.js +99 -0
  109. package/dist/components/ui/badge.cjs +5 -1
  110. package/dist/components/ui/badge.d.cts +1 -1
  111. package/dist/components/ui/badge.d.ts +1 -1
  112. package/dist/components/ui/badge.js +5 -1
  113. package/dist/index.cjs +1 -0
  114. package/dist/index.d.cts +16 -0
  115. package/dist/index.d.ts +16 -0
  116. package/dist/index.js +1 -0
  117. package/dist/reference.cjs +1260 -0
  118. package/dist/reference.d.cts +11 -0
  119. package/dist/reference.d.ts +11 -0
  120. package/dist/reference.js +1230 -0
  121. package/dist/styles/glass-board.css +43 -0
  122. package/dist/styles/globals.css +59 -0
  123. package/package.json +16 -4
  124. package/scripts/ensure-pnpm-publish.mjs +23 -0
  125. package/scripts/smoke-dist.mjs +68 -0
  126. package/src/components/context-editor/CapabilitySection.tsx +85 -0
  127. package/src/components/context-editor/ContextEditor.tsx +138 -0
  128. package/src/components/context-editor/PresetBar.tsx +94 -0
  129. package/src/components/context-editor/SkillsSection.tsx +37 -0
  130. package/src/components/context-editor/SystemPromptSection.tsx +88 -0
  131. package/src/components/context-editor/ToolsSection.tsx +37 -0
  132. package/src/components/context-editor/index.ts +7 -0
  133. package/src/components/context-editor/parts.tsx +217 -0
  134. package/src/components/context-editor/use-context-editor-model.ts +296 -0
  135. package/src/components/dashboard/__tests__/catalog.test.ts +62 -0
  136. package/src/components/dashboard/__tests__/glass.test.tsx +42 -0
  137. package/src/components/dashboard/__tests__/state.test.tsx +127 -0
  138. package/src/components/dashboard/__tests__/variants.test.tsx +95 -0
  139. package/src/components/dashboard/activity-list.tsx +99 -0
  140. package/src/components/dashboard/catalog.json +312 -0
  141. package/src/components/dashboard/catalog.ts +29 -0
  142. package/src/components/dashboard/data-boundary.tsx +54 -0
  143. package/src/components/dashboard/empty-state.tsx +70 -0
  144. package/src/components/dashboard/icon-button.tsx +58 -0
  145. package/src/components/dashboard/index.ts +21 -0
  146. package/src/components/dashboard/item-list.tsx +106 -0
  147. package/src/components/dashboard/key-value.tsx +54 -0
  148. package/src/components/dashboard/layout.tsx +286 -0
  149. package/src/components/dashboard/metric.tsx +139 -0
  150. package/src/components/dashboard/progress-ring.tsx +100 -0
  151. package/src/components/dashboard/skeletons.tsx +73 -0
  152. package/src/components/dashboard/spacing.ts +14 -0
  153. package/src/components/dashboard/stale-indicator.tsx +55 -0
  154. package/src/components/dashboard/status.tsx +88 -0
  155. package/src/components/dashboard/tone.ts +48 -0
  156. package/src/components/dashboard/typography.tsx +151 -0
  157. package/src/components/reference/ActivityExample.tsx +68 -0
  158. package/src/components/reference/ResourceExample.tsx +78 -0
  159. package/src/components/reference/SchedulerExample.tsx +88 -0
  160. package/src/components/reference/StarterExample.tsx +48 -0
  161. package/src/components/reference/index.ts +10 -0
  162. package/src/components/ui/badge.tsx +6 -0
  163. package/src/index.ts +7 -0
  164. package/src/reference.ts +15 -0
  165. package/src/styles/glass-board.css +43 -0
  166. package/src/styles/globals.css +59 -0
  167. package/tsup.config.ts +18 -4
  168. package/tsup.reference.config.ts +15 -0
  169. package/vitest.config.ts +8 -0
@@ -0,0 +1,43 @@
1
+ /**
2
+ * Shared glass board — the frosted-board look, framework-agnostic.
3
+ *
4
+ * `.glass-canvas` paints the ambient accent-glow backdrop; `.glass-tile`
5
+ * is a translucent, backdrop-blurred surface with a luminous hairline rim.
6
+ * The desktop dashboard and the styleguide preview both consume this file
7
+ * so widgets render on an identical board.
8
+ *
9
+ * Depends on the canonical `--glass-*` tokens in globals.css. Dark is the
10
+ * primary target; light mirrors the same relationships (frosted white
11
+ * glass over a soft tinted canvas).
12
+ */
13
+
14
+ /* ── Canvas ──────────────────────────────────────────────────────
15
+ Ambient accent glow the glass tiles pick up. */
16
+ .glass-canvas {
17
+ --glass-canvas-base: #e9eaef;
18
+ --glass-glow-a: color-mix(in srgb, var(--accent-primary) 16%, transparent);
19
+ --glass-glow-b: color-mix(in srgb, var(--brand-primary) 12%, transparent);
20
+ background:
21
+ radial-gradient(90% 70% at 85% -10%, var(--glass-glow-a), transparent 60%),
22
+ radial-gradient(80% 60% at -10% 110%, var(--glass-glow-b), transparent 60%),
23
+ var(--glass-canvas-base);
24
+ }
25
+
26
+ .dark .glass-canvas {
27
+ --glass-canvas-base: #060608;
28
+ --glass-glow-a: color-mix(in srgb, var(--accent-primary) 48%, transparent);
29
+ --glass-glow-b: color-mix(in srgb, var(--brand-primary) 30%, transparent);
30
+ }
31
+
32
+ /* ── Glass tile surface ──────────────────────────────────────────
33
+ Translucent, blurred surface floating over the canvas. The surface is
34
+ static on hover — the tile is content, not a button, so pointing at it
35
+ must not change it. (`--glass-hover` is still used for row hover *inside*
36
+ widgets.) The transition covers the drag/resize state changes only. */
37
+ .glass-tile {
38
+ background: var(--glass-tile);
39
+ border: 1px solid var(--glass-border);
40
+ backdrop-filter: blur(24px) saturate(140%);
41
+ box-shadow: inset 0 1px 0 var(--glass-rim);
42
+ transition: background-color 150ms ease, border-color 150ms ease;
43
+ }
@@ -179,6 +179,27 @@
179
179
  --sidebar-accent-foreground: var(--text-primary);
180
180
  --sidebar-border: var(--border-subtle);
181
181
  --sidebar-ring: var(--border-focus);
182
+
183
+ /* ── Glass (dashboard) ────────────────────────────────────
184
+ Canonical translucent surface language for the glass
185
+ dashboard. The host `dashboard.css` derives its `--dash-*`
186
+ tokens from these, and the `.glass` scope below remaps the
187
+ component-facing `--surface-*` tokens to them. Light =
188
+ frosted white; dark values live in the `.dark` block. */
189
+ --glass-tile: rgba(255, 255, 255, 0.52); /* base tile (host) */
190
+ --glass-surface: rgba(255, 255, 255, 0.62); /* raised card */
191
+ --glass-surface-flat: rgba(255, 255, 255, 0.40); /* flat row */
192
+ --glass-border: rgba(15, 17, 23, 0.10);
193
+ --glass-rim: rgba(255, 255, 255, 0.60);
194
+ --glass-hover: rgba(255, 255, 255, 0.72);
195
+
196
+ /* Component-facing surface tokens. Default to the solid design
197
+ tokens so components look unchanged outside the `.glass`
198
+ scope; the scope flips them to the glass values above. */
199
+ --surface-raised: var(--bg-surface);
200
+ --surface-flat: var(--bg-elevated);
201
+ --surface-line: var(--border-subtle);
202
+ --surface-rim: transparent;
182
203
  }
183
204
 
184
205
  /* ── Scrollbars ─────────────────────────────────────────────── */
@@ -421,6 +442,44 @@
421
442
  --sidebar-accent-foreground: var(--text-primary);
422
443
  --sidebar-border: var(--border-subtle);
423
444
  --sidebar-ring: var(--border-focus);
445
+
446
+ /* ── Glass (dashboard) — dark: low-opacity white over the dark
447
+ canvas glow. `--surface-*` defaults stay solid; the `.glass`
448
+ scope flips them (values shared across themes via the vars). */
449
+ --glass-tile: rgba(255, 255, 255, 0.06);
450
+ --glass-surface: rgba(255, 255, 255, 0.09);
451
+ --glass-surface-flat: rgba(255, 255, 255, 0.05);
452
+ --glass-border: rgba(255, 255, 255, 0.12);
453
+ --glass-rim: rgba(255, 255, 255, 0.08);
454
+ --glass-hover: rgba(255, 255, 255, 0.13);
455
+ }
456
+
457
+ /* ── Glass scope ─────────────────────────────────────────────────
458
+ Subtree-local class applied by `WidgetContent` (default on). It
459
+ remaps the component-facing surface tokens and the container/border
460
+ shadcn bridge tokens to the translucent glass values, so dashboard
461
+ components read as part of the frosted tile. Interactive controls
462
+ (`--input`) and portalled surfaces (`--popover`, rendered at
463
+ document.body outside this scope) stay solid for legibility. No
464
+ `backdrop-filter` here — the host tile provides the single blur. */
465
+ .glass {
466
+ /* component surface tokens */
467
+ --surface-raised: var(--glass-surface);
468
+ --surface-flat: var(--glass-surface-flat);
469
+ --surface-line: var(--glass-border);
470
+ --surface-rim: var(--glass-rim);
471
+
472
+ /* containers + borders for primitives used inside widgets */
473
+ --card: var(--glass-surface);
474
+ --secondary: var(--glass-surface-flat);
475
+ --muted: var(--glass-surface-flat);
476
+ --accent: var(--glass-surface-flat);
477
+ --border: var(--glass-border);
478
+
479
+ /* keep interactive controls + portalled surfaces solid */
480
+ --input: var(--border-subtle);
481
+ --popover: var(--bg-elevated);
482
+ --ring: var(--border-focus);
424
483
  }
425
484
 
426
485
  html {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sero-ai/ui",
3
- "version": "0.3.2",
3
+ "version": "0.4.1",
4
4
  "description": "Shared UI components, AI elements, and design tokens for the Sero platform",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -11,6 +11,12 @@
11
11
  "import": "./dist/index.js",
12
12
  "require": "./dist/index.cjs"
13
13
  },
14
+ "./dashboard-catalog.json": "./dist/components/dashboard/catalog.json",
15
+ "./reference": {
16
+ "types": "./dist/reference.d.ts",
17
+ "import": "./dist/reference.js",
18
+ "require": "./dist/reference.cjs"
19
+ },
14
20
  "./theme": {
15
21
  "types": "./dist/theme/index.d.ts",
16
22
  "import": "./dist/theme/index.js",
@@ -79,15 +85,21 @@
79
85
  "@types/react": "^19.2.13",
80
86
  "@types/react-dom": "^19.2.3",
81
87
  "@types/react-syntax-highlighter": "^15.5.13",
88
+ "jsdom": "^26.1.0",
89
+ "react": "^19.2.4",
90
+ "react-dom": "^19.2.4",
82
91
  "shadcn": "^3.8.5",
83
92
  "tailwindcss": "4.1.18",
84
93
  "tsup": "^8.5.1",
85
94
  "tw-animate-css": "^1.4.0",
86
- "typescript": "^5.9.3"
95
+ "typescript": "^5.9.3",
96
+ "vitest": "^4.1.7"
87
97
  },
88
98
  "scripts": {
89
- "build": "tsup",
90
- "typecheck": "tsc --noEmit"
99
+ "build": "tsup && tsup --config tsup.reference.config.ts",
100
+ "typecheck": "tsc --noEmit",
101
+ "test": "vitest run",
102
+ "smoke": "node scripts/smoke-dist.mjs"
91
103
  },
92
104
  "module": "./dist/index.js"
93
105
  }
@@ -0,0 +1,23 @@
1
+ // Publish guard — this package MUST be published with `pnpm publish`.
2
+ //
3
+ // The `publishConfig` block swaps main/types/exports over to the built `dist/`
4
+ // at publish time. That override is a pnpm (and yarn) feature; plain npm
5
+ // IGNORES it and publishes the top-level fields, which point at `src/`. So
6
+ // `npm publish` here silently ships raw TypeScript source instead of the
7
+ // compiled package — exactly how 0.4.0 went out wrong.
8
+ //
9
+ // This runs from `prepublishOnly`, so it fires on every publish. It aborts
10
+ // unless the invoking package manager is pnpm.
11
+
12
+ const ua = process.env.npm_config_user_agent ?? "";
13
+
14
+ if (!ua.startsWith("pnpm")) {
15
+ const tool = ua.split("/")[0] || "an unknown tool";
16
+ console.error(
17
+ `\n✖ @sero-ai/ui must be published with pnpm, but this ran under ${tool}.\n\n` +
18
+ " Plain npm ignores the publishConfig dist overrides and would ship raw\n" +
19
+ " TypeScript source instead of the built dist/. Use:\n\n" +
20
+ " pnpm publish (from packages/ui)\n",
21
+ );
22
+ process.exit(1);
23
+ }
@@ -0,0 +1,68 @@
1
+ // Smoke test for the published artifact.
2
+ //
3
+ // The package is bundle:false and bundler-resolved, so its public entrypoints
4
+ // are easy to break under raw Node ESM (extensionless / directory imports).
5
+ // This runs the built dist the way a published consumer would: it imports the
6
+ // reference entry (ESM + CJS) and reads the catalogue JSON, failing loudly if
7
+ // any entrypoint no longer resolves. Run after `pnpm build`.
8
+
9
+ import { createRequire } from "node:module";
10
+ import { existsSync, readFileSync } from "node:fs";
11
+ import { fileURLToPath } from "node:url";
12
+ import { dirname, resolve } from "node:path";
13
+
14
+ const require = createRequire(import.meta.url);
15
+ const here = dirname(fileURLToPath(import.meta.url));
16
+ const dist = resolve(here, "..", "dist");
17
+
18
+ const fail = (msg) => {
19
+ console.error(`✗ ${msg}`);
20
+ process.exitCode = 1;
21
+ };
22
+
23
+ if (!existsSync(dist)) {
24
+ fail("dist/ is missing — run `pnpm build` first");
25
+ process.exit(1);
26
+ }
27
+
28
+ // 1. Reference entrypoint resolves as ESM and exposes the example widgets.
29
+ const EXPECTED = [
30
+ "StarterExample",
31
+ "SchedulerExample",
32
+ "ResourceExample",
33
+ "ActivityExample",
34
+ ];
35
+
36
+ const esm = await import(resolve(dist, "reference.js"));
37
+ for (const name of EXPECTED) {
38
+ if (typeof esm[name] !== "function") fail(`reference.js missing export ${name}`);
39
+ }
40
+
41
+ // 2. Reference entrypoint resolves as CJS too.
42
+ const cjs = require(resolve(dist, "reference.cjs"));
43
+ for (const name of EXPECTED) {
44
+ if (typeof cjs[name] !== "function") fail(`reference.cjs missing export ${name}`);
45
+ }
46
+
47
+ // The publishConfig `types` map points at these; a clean-race once dropped them.
48
+ for (const f of ["reference.d.ts", "reference.d.cts"]) {
49
+ if (!existsSync(resolve(dist, f))) fail(`${f} not emitted to dist`);
50
+ }
51
+
52
+ // 3. Catalogue ships as plain JSON with the expected shape.
53
+ const catalogPath = resolve(dist, "components", "dashboard", "catalog.json");
54
+ if (!existsSync(catalogPath)) fail("dashboard-catalog.json not emitted to dist");
55
+ else {
56
+ const catalog = JSON.parse(readFileSync(catalogPath, "utf8"));
57
+ if (!Array.isArray(catalog) || catalog.length === 0) {
58
+ fail("dashboard-catalog.json is not a non-empty array");
59
+ } else if (!catalog.every((e) => e.name && e.category && e.kind)) {
60
+ fail("dashboard-catalog.json has malformed entries");
61
+ }
62
+ }
63
+
64
+ if (process.exitCode) {
65
+ console.error("Smoke test failed.");
66
+ } else {
67
+ console.log("✓ dist entrypoints resolve (reference esm+cjs, catalog json)");
68
+ }
@@ -0,0 +1,85 @@
1
+ import type { LucideIcon } from 'lucide-react';
2
+ import type { ContextSkillInfo, ContextToolInfo } from '@sero-ai/common';
3
+ import { Switch } from '../ui/switch';
4
+ import { ContextSection, ToggleRow } from './parts';
5
+
6
+ interface CapabilitySectionProps<TItem extends ContextToolInfo | ContextSkillInfo> {
7
+ icon: LucideIcon;
8
+ title: string;
9
+ tint: 'amber' | 'violet';
10
+ includeAllLabel: string;
11
+ emptyLabel: string;
12
+ items: TItem[];
13
+ allDisabled: boolean;
14
+ enabledCount: number;
15
+ isEnabled: (name: string) => boolean;
16
+ onToggle: (name: string) => void;
17
+ onToggleAll: (disabled: boolean) => void;
18
+ }
19
+
20
+ export function CapabilitySection<TItem extends ContextToolInfo | ContextSkillInfo>({
21
+ icon,
22
+ title,
23
+ tint,
24
+ includeAllLabel,
25
+ emptyLabel,
26
+ items,
27
+ allDisabled,
28
+ enabledCount,
29
+ isEnabled,
30
+ onToggle,
31
+ onToggleAll,
32
+ }: CapabilitySectionProps<TItem>) {
33
+ const badgeVariant = allDisabled
34
+ ? 'disabled' as const
35
+ : enabledCount < items.length
36
+ ? 'partial' as const
37
+ : 'default' as const;
38
+
39
+ return (
40
+ <ContextSection
41
+ icon={icon}
42
+ title={title}
43
+ tint={tint}
44
+ count={items.length}
45
+ badge={
46
+ items.length > 0 && enabledCount < items.length
47
+ ? `${enabledCount}/${items.length} included`
48
+ : undefined
49
+ }
50
+ badgeVariant={badgeVariant}
51
+ defaultOpen={false}
52
+ >
53
+ <div className="space-y-1">
54
+ {items.length > 0 && (
55
+ <div className="mb-1 flex items-center justify-between rounded-md border-b border-border/20 px-2 pb-2">
56
+ <span className="text-[11px] font-medium text-[var(--text-secondary)]">
57
+ {includeAllLabel}
58
+ </span>
59
+ <Switch
60
+ size="sm"
61
+ checked={!allDisabled}
62
+ onCheckedChange={(checked) => onToggleAll(!checked)}
63
+ />
64
+ </div>
65
+ )}
66
+
67
+ {items.map((item) => (
68
+ <ToggleRow
69
+ key={item.name}
70
+ name={item.name}
71
+ description={item.description}
72
+ enabled={isEnabled(item.name)}
73
+ onToggle={() => onToggle(item.name)}
74
+ />
75
+ ))}
76
+
77
+ {items.length === 0 && (
78
+ <span className="text-[11px] italic text-[var(--text-muted)]">
79
+ {emptyLabel}
80
+ </span>
81
+ )}
82
+ </div>
83
+ </ContextSection>
84
+ );
85
+ }
@@ -0,0 +1,138 @@
1
+ /**
2
+ * ContextEditor — shared, controlled context editor dialog.
3
+ *
4
+ * Edits a system prompt plus tool/skill enablement and saved presets, against
5
+ * an `available` context. Host-agnostic: the consumer supplies the available
6
+ * context, the current overrides, the preset list, and the persistence
7
+ * callbacks. Used by the desktop chat session editor and by app modules (e.g.
8
+ * the Orchestrator loop context override).
9
+ */
10
+
11
+ import { useState } from 'react';
12
+ import { AlertCircle, Loader2, Settings2 } from 'lucide-react';
13
+ import type { AvailableContext, ContextOverrides, ContextPreset } from '@sero-ai/common';
14
+ import {
15
+ Dialog,
16
+ DialogContent,
17
+ DialogDescription,
18
+ DialogFooter,
19
+ DialogHeader,
20
+ DialogTitle,
21
+ } from '../ui/dialog';
22
+ import { ScrollArea } from '../ui/scroll-area';
23
+ import { PresetBar } from './PresetBar';
24
+ import { SkillsSection } from './SkillsSection';
25
+ import { SystemPromptSection, type SystemPromptCopy } from './SystemPromptSection';
26
+ import { ToolsSection } from './ToolsSection';
27
+ import { useContextEditorModel, type ContextPresetBody } from './use-context-editor-model';
28
+
29
+ export interface ContextEditorProps {
30
+ open: boolean;
31
+ onOpenChange: (open: boolean) => void;
32
+ available: AvailableContext | null;
33
+ initialOverrides: ContextOverrides | null;
34
+ presets: ContextPreset[];
35
+ /** Persist the chosen overrides. Return false to keep the dialog open (error). */
36
+ onApply: (overrides: ContextOverrides | null) => Promise<boolean> | boolean;
37
+ onSavePreset: (name: string, preset: ContextPresetBody) => void;
38
+ onDeletePreset: (id: string) => void;
39
+ applyError?: string | null;
40
+ title?: string;
41
+ description?: string;
42
+ systemPromptCopy?: SystemPromptCopy;
43
+ applyLabel?: string;
44
+ /** Hide the Tools section (e.g. the Orchestrator picks tools per step, not per loop). */
45
+ hideTools?: boolean;
46
+ }
47
+
48
+ export function ContextEditor({
49
+ open,
50
+ onOpenChange,
51
+ available,
52
+ initialOverrides,
53
+ presets,
54
+ onApply,
55
+ onSavePreset,
56
+ onDeletePreset,
57
+ applyError,
58
+ title = 'Context Editor',
59
+ description = 'Configure what is included in the LLM context.',
60
+ systemPromptCopy,
61
+ applyLabel = 'Apply',
62
+ hideTools = false,
63
+ }: ContextEditorProps) {
64
+ const model = useContextEditorModel({ open, available, initialOverrides, presets, onSavePreset, onDeletePreset });
65
+ const [applying, setApplying] = useState(false);
66
+
67
+ const handleApply = async () => {
68
+ setApplying(true);
69
+ try {
70
+ const ok = await onApply(model.buildOverrides());
71
+ if (ok !== false) onOpenChange(false);
72
+ } finally {
73
+ setApplying(false);
74
+ }
75
+ };
76
+
77
+ return (
78
+ <Dialog open={open} onOpenChange={onOpenChange}>
79
+ <DialogContent
80
+ className="max-h-[85vh] overflow-hidden bg-[var(--bg-surface)] sm:max-w-[58rem]"
81
+ showCloseButton
82
+ >
83
+ <DialogHeader>
84
+ <DialogTitle className="flex items-center gap-2 text-sm">
85
+ <Settings2 className="size-4 text-[var(--text-muted)]" />
86
+ {title}
87
+ </DialogTitle>
88
+ <DialogDescription className="text-xs">{description}</DialogDescription>
89
+ </DialogHeader>
90
+
91
+ <PresetBar {...model.preset} />
92
+
93
+ {!available ? (
94
+ <div className="flex items-center justify-center gap-2 py-8">
95
+ <Loader2 className="size-4 animate-spin text-[var(--text-muted)]" />
96
+ <span className="text-xs text-[var(--text-muted)]">Loading context…</span>
97
+ </div>
98
+ ) : (
99
+ <ScrollArea className="max-h-[65vh] overflow-hidden">
100
+ <div className="min-w-0 space-y-2 pr-2">
101
+ <SystemPromptSection
102
+ displayedPrompt={model.displayedPrompt}
103
+ systemPrompt={model.systemPrompt}
104
+ onSystemPromptChange={model.setSystemPrompt}
105
+ copy={systemPromptCopy}
106
+ />
107
+ {!hideTools && <ToolsSection {...model.tools} />}
108
+ <SkillsSection {...model.skills} />
109
+ </div>
110
+ </ScrollArea>
111
+ )}
112
+
113
+ {applyError && (
114
+ <div className="flex items-start gap-2 rounded-md border border-status-error-border bg-status-error-muted px-3 py-2">
115
+ <AlertCircle className="mt-0.5 size-3.5 shrink-0 text-status-error" />
116
+ <span className="text-[11px] text-status-error">{applyError}</span>
117
+ </div>
118
+ )}
119
+
120
+ <DialogFooter>
121
+ <button type="button"
122
+ onClick={() => onOpenChange(false)}
123
+ className="rounded-md border border-border/50 px-3 py-1.5 text-xs text-[var(--text-secondary)] transition-colors hover:bg-[var(--bg-elevated)]"
124
+ >
125
+ Cancel
126
+ </button>
127
+ <button type="button"
128
+ onClick={() => { void handleApply(); }}
129
+ disabled={!available || applying}
130
+ className="rounded-md bg-[var(--accent-primary)] px-3 py-1.5 text-xs font-medium text-white transition-colors hover:bg-[var(--accent-hover)] disabled:opacity-50"
131
+ >
132
+ {applyLabel}
133
+ </button>
134
+ </DialogFooter>
135
+ </DialogContent>
136
+ </Dialog>
137
+ );
138
+ }
@@ -0,0 +1,94 @@
1
+ import { RotateCcw, Save, Trash2 } from 'lucide-react';
2
+ import type { ContextPreset } from '@sero-ai/common';
3
+ import {
4
+ Select,
5
+ SelectContent,
6
+ SelectItem,
7
+ SelectTrigger,
8
+ SelectValue,
9
+ } from '../ui/select';
10
+ import { SavePresetInput } from './parts';
11
+
12
+ interface PresetBarProps {
13
+ allPresets: ContextPreset[];
14
+ activePresetId: string | null;
15
+ activeUserPreset: ContextPreset | null;
16
+ hasOverrides: boolean;
17
+ showSaveInput: boolean;
18
+ onPresetChange: (id: string) => void;
19
+ onDelete: (id: string) => void;
20
+ onReset: () => void;
21
+ onShowSave: () => void;
22
+ onSave: (name: string) => void;
23
+ onCancelSave: () => void;
24
+ }
25
+
26
+ export function PresetBar({
27
+ allPresets,
28
+ activePresetId,
29
+ activeUserPreset,
30
+ hasOverrides,
31
+ showSaveInput,
32
+ onPresetChange,
33
+ onDelete,
34
+ onReset,
35
+ onShowSave,
36
+ onSave,
37
+ onCancelSave,
38
+ }: PresetBarProps) {
39
+ return (
40
+ <>
41
+ <div className="flex items-center gap-2">
42
+ <span className="text-[11px] font-medium text-[var(--text-muted)]">
43
+ Preset:
44
+ </span>
45
+ <Select value={activePresetId ?? ''} onValueChange={onPresetChange}>
46
+ <SelectTrigger size="sm" className="h-7 w-40 text-xs">
47
+ <SelectValue placeholder="Custom" />
48
+ </SelectTrigger>
49
+ <SelectContent>
50
+ {allPresets.map((preset) => (
51
+ <SelectItem key={preset.id} value={preset.id}>
52
+ {preset.name}
53
+ </SelectItem>
54
+ ))}
55
+ </SelectContent>
56
+ </Select>
57
+
58
+ {activeUserPreset && (
59
+ <button type="button"
60
+ onClick={() => onDelete(activeUserPreset.id)}
61
+ className="flex items-center gap-1 rounded-md px-1.5 py-1 text-[var(--text-muted)] transition-colors hover:bg-status-error-muted hover:text-status-error"
62
+ title={`Delete "${activeUserPreset.name}"`}
63
+ >
64
+ <Trash2 className="size-3" />
65
+ </button>
66
+ )}
67
+
68
+ {!showSaveInput && (
69
+ <button type="button"
70
+ onClick={onShowSave}
71
+ className="flex items-center gap-1 rounded-md px-2 py-1 text-[11px] text-[var(--text-muted)] transition-colors hover:bg-[var(--bg-elevated)] hover:text-[var(--text-secondary)]"
72
+ >
73
+ <Save className="size-3" />
74
+ Save as
75
+ </button>
76
+ )}
77
+
78
+ {hasOverrides && (
79
+ <button type="button"
80
+ onClick={onReset}
81
+ className="ml-auto flex items-center gap-1 rounded-md px-2 py-1 text-[11px] text-[var(--text-muted)] transition-colors hover:bg-[var(--bg-elevated)] hover:text-[var(--text-secondary)]"
82
+ >
83
+ <RotateCcw className="size-3" />
84
+ Reset
85
+ </button>
86
+ )}
87
+ </div>
88
+
89
+ {showSaveInput && (
90
+ <SavePresetInput onSave={onSave} onCancel={onCancelSave} />
91
+ )}
92
+ </>
93
+ );
94
+ }
@@ -0,0 +1,37 @@
1
+ import { Sparkles } from 'lucide-react';
2
+ import type { ContextSkillInfo } from '@sero-ai/common';
3
+ import { CapabilitySection } from './CapabilitySection';
4
+
5
+ interface SkillsSectionProps {
6
+ items: ContextSkillInfo[];
7
+ allDisabled: boolean;
8
+ enabledCount: number;
9
+ isEnabled: (name: string) => boolean;
10
+ onToggle: (name: string) => void;
11
+ onToggleAll: (disabled: boolean) => void;
12
+ }
13
+
14
+ export function SkillsSection({
15
+ items,
16
+ allDisabled,
17
+ enabledCount,
18
+ isEnabled,
19
+ onToggle,
20
+ onToggleAll,
21
+ }: SkillsSectionProps) {
22
+ return (
23
+ <CapabilitySection
24
+ icon={Sparkles}
25
+ title="Skills"
26
+ tint="violet"
27
+ includeAllLabel="Include all skills"
28
+ emptyLabel="No skills available"
29
+ items={items}
30
+ allDisabled={allDisabled}
31
+ enabledCount={enabledCount}
32
+ isEnabled={isEnabled}
33
+ onToggle={onToggle}
34
+ onToggleAll={onToggleAll}
35
+ />
36
+ );
37
+ }