@signal9/era-ui 12.1.2 → 13.0.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.
- package/dist/dev/audit-overlay.svelte +1 -1
- package/dist/docs/css-utilities.js +6 -1
- package/dist/docs/notes.md +12 -11
- package/dist/era-ui.css +1 -1
- package/dist/generated-docs/badge.md +3 -0
- package/dist/generated-docs/button.md +4 -0
- package/dist/generated-docs/chip.md +3 -0
- package/dist/generated-docs/llms-full.txt +53 -31
- package/dist/generated-docs/llms.txt +1 -1
- package/dist/generated-docs/manifest.json +17 -17
- package/dist/generated-docs/notes.md +12 -11
- package/dist/generated-docs/utilities.json +10 -10
- package/dist/generated-docs/utilities.md +30 -19
- package/dist/os/index.d.ts +0 -1
- package/dist/os/index.js +0 -1
- package/dist/os/notification-center.svelte +1 -2
- package/dist/os/taskbar.svelte +2 -3
- package/dist/os/toaster.svelte +2 -3
- package/dist/styles/index.css +77 -12
- package/dist/styles/layers.css +48 -0
- package/dist/styles/surfaces/base.css +4 -2
- package/dist/styles/surfaces/glass.css +0 -13
- package/dist/theme/index.d.ts +87 -0
- package/dist/theme/index.js +86 -0
- package/dist/ui/alert-dialog/alert-dialog-content.svelte +2 -2
- package/dist/ui/alert-dialog/alert-dialog-overlay.svelte +1 -1
- package/dist/ui/badge/badge.svelte +14 -1
- package/dist/ui/badge/badge.svelte.d.ts +4 -0
- package/dist/ui/button/button.svelte +37 -7
- package/dist/ui/button/button.svelte.d.ts +6 -0
- package/dist/ui/chip/chip.svelte +14 -1
- package/dist/ui/chip/chip.svelte.d.ts +4 -0
- package/dist/ui/combobox/combobox-content.svelte +1 -1
- package/dist/ui/command-bar/command-bar.svelte +1 -1
- package/dist/ui/context-menu/context-menu-content.svelte +1 -1
- package/dist/ui/context-menu/context-menu-sub-content.svelte +1 -1
- package/dist/ui/date-picker/date-picker-content.svelte +1 -1
- package/dist/ui/date-range-picker/date-range-picker-content.svelte +1 -1
- package/dist/ui/dialog/dialog-content.svelte +1 -1
- package/dist/ui/dialog/dialog-overlay.svelte +5 -1
- package/dist/ui/dropdown-menu/dropdown-menu-content.svelte +1 -1
- package/dist/ui/dropdown-menu/dropdown-menu-sub-content.svelte +1 -1
- package/dist/ui/link-preview/link-preview-content.svelte +1 -1
- package/dist/ui/menubar/menubar-content.svelte +1 -1
- package/dist/ui/menubar/menubar-sub-content.svelte +1 -1
- package/dist/ui/popover/popover-content.svelte +1 -1
- package/dist/ui/select/select-content.svelte +1 -1
- package/dist/ui/sheet/sheet-content.svelte +1 -1
- package/dist/ui/sheet/sheet-content.svelte.d.ts +1 -1
- package/dist/ui/tooltip/tooltip-content.svelte +1 -1
- package/package.json +9 -12
- package/dist/apps/index.d.ts +0 -20
- package/dist/apps/index.js +0 -20
- package/dist/apps/llm-shell/index.d.ts +0 -2
- package/dist/apps/llm-shell/index.js +0 -1
- package/dist/apps/llm-shell/llm-shell.svelte +0 -334
- package/dist/apps/llm-shell/llm-shell.svelte.d.ts +0 -57
- package/dist/apps/llm-shell/llm-swarm.svelte +0 -79
- package/dist/apps/llm-shell/llm-swarm.svelte.d.ts +0 -25
- package/dist/apps/llm-shell/types.d.ts +0 -103
- package/dist/apps/llm-shell/types.js +0 -1
- package/dist/apps/notes/editor/bubble-menu.svelte.d.ts +0 -11
- package/dist/apps/notes/editor/bubble-menu.svelte.js +0 -165
- package/dist/apps/notes/editor/bubble-toolbar.svelte +0 -55
- package/dist/apps/notes/editor/bubble-toolbar.svelte.d.ts +0 -22
- package/dist/apps/notes/editor/extensions.d.ts +0 -18
- package/dist/apps/notes/editor/extensions.js +0 -88
- package/dist/apps/notes/editor/floating.d.ts +0 -25
- package/dist/apps/notes/editor/floating.js +0 -54
- package/dist/apps/notes/editor/link.d.ts +0 -9
- package/dist/apps/notes/editor/link.js +0 -22
- package/dist/apps/notes/editor/list-cleanup-rule.d.ts +0 -14
- package/dist/apps/notes/editor/list-cleanup-rule.js +0 -47
- package/dist/apps/notes/editor/slash-command.svelte.d.ts +0 -22
- package/dist/apps/notes/editor/slash-command.svelte.js +0 -215
- package/dist/apps/notes/editor/slash-menu.svelte +0 -82
- package/dist/apps/notes/editor/slash-menu.svelte.d.ts +0 -23
- package/dist/apps/notes/index.d.ts +0 -11
- package/dist/apps/notes/index.js +0 -13
- package/dist/apps/notes/markdown.d.ts +0 -12
- package/dist/apps/notes/markdown.js +0 -165
- package/dist/apps/notes/note-editor.svelte +0 -176
- package/dist/apps/notes/note-editor.svelte.d.ts +0 -36
- package/dist/apps/notes/notes-store.svelte.d.ts +0 -64
- package/dist/apps/notes/notes-store.svelte.js +0 -234
- package/dist/apps/notes/notes.svelte +0 -501
- package/dist/apps/notes/notes.svelte.d.ts +0 -53
- package/dist/apps/notes/tree.d.ts +0 -70
- package/dist/apps/notes/tree.js +0 -185
- package/dist/apps/notes/types.d.ts +0 -49
- package/dist/apps/notes/types.js +0 -11
- package/dist/os/layers.d.ts +0 -23
- package/dist/os/layers.js +0 -23
|
@@ -5,4 +5,8 @@
|
|
|
5
5
|
let { ref = $bindable(null), class: className, ...restProps }: Dialog.OverlayProps = $props();
|
|
6
6
|
</script>
|
|
7
7
|
|
|
8
|
-
<Dialog.Overlay
|
|
8
|
+
<Dialog.Overlay
|
|
9
|
+
bind:ref
|
|
10
|
+
class={cn('fixed inset-0 z-overlay bg-overlay', className)}
|
|
11
|
+
{...restProps}
|
|
12
|
+
/>
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
// floating menu, like the context menu it should match: full radius, a
|
|
30
30
|
// gutter of air, shadow doing the separating.
|
|
31
31
|
class={cn(
|
|
32
|
-
'z-
|
|
32
|
+
'z-overlay w-max min-w-(--bits-dropdown-menu-anchor-width) overflow-hidden rounded-control bg-elevated p-menu shadow-lg glass-blur',
|
|
33
33
|
className
|
|
34
34
|
)}
|
|
35
35
|
{sideOffset}
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
<DropdownMenu.SubContent
|
|
16
16
|
bind:ref
|
|
17
17
|
class={cn(
|
|
18
|
-
'z-
|
|
18
|
+
'z-overlay w-max overflow-hidden rounded-control bg-elevated p-menu shadow-lg glass-blur',
|
|
19
19
|
className
|
|
20
20
|
)}
|
|
21
21
|
{sideOffset}
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
<LinkPreview.Content
|
|
16
16
|
bind:ref
|
|
17
17
|
class={cn(
|
|
18
|
-
'z-
|
|
18
|
+
'z-overlay max-w-xs rounded-control bg-elevated p-inset-control text-body shadow-lg glass-blur',
|
|
19
19
|
className
|
|
20
20
|
)}
|
|
21
21
|
{sideOffset}
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
// floating menu, like the context menu it should match: full radius, a
|
|
30
30
|
// gutter of air, shadow doing the separating.
|
|
31
31
|
class={cn(
|
|
32
|
-
'z-
|
|
32
|
+
'z-overlay w-max min-w-(--bits-menubar-menu-anchor-width) overflow-hidden rounded-control bg-elevated p-menu shadow-lg glass-blur',
|
|
33
33
|
className
|
|
34
34
|
)}
|
|
35
35
|
{sideOffset}
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
<Menubar.SubContent
|
|
16
16
|
bind:ref
|
|
17
17
|
class={cn(
|
|
18
|
-
'z-
|
|
18
|
+
'z-overlay w-max overflow-hidden rounded-control bg-elevated p-menu shadow-lg glass-blur',
|
|
19
19
|
className
|
|
20
20
|
)}
|
|
21
21
|
{sideOffset}
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
// floating menu, like the context menu it should match: full radius, a
|
|
23
23
|
// gutter of air, shadow doing the separating.
|
|
24
24
|
class={cn(
|
|
25
|
-
'z-
|
|
25
|
+
'z-overlay w-max min-w-(--bits-popover-anchor-width) overflow-hidden rounded-control bg-elevated p-card shadow-lg glass-blur',
|
|
26
26
|
className
|
|
27
27
|
)}
|
|
28
28
|
{sideOffset}
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
// 2px frame that stops a full-width highlight from breaking their chrome.
|
|
30
30
|
// A fixed gutter was tried here and rejected: it padded flat menus too.
|
|
31
31
|
class={cn(
|
|
32
|
-
'z-
|
|
32
|
+
'z-overlay w-max min-w-(--bits-select-anchor-width) overflow-hidden rounded-control bg-elevated p-menu shadow-lg glass-blur data-[side=bottom]:rounded-t-none data-[side=top]:rounded-b-none',
|
|
33
33
|
className
|
|
34
34
|
)}
|
|
35
35
|
{sideOffset}
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
// border-divider on every side: the pane's hairline-edge law — on flat,
|
|
8
8
|
// shadow-lg collapses to none and the sheet's edge was a bare tone step.
|
|
9
9
|
export const sheetVariants = tv({
|
|
10
|
-
base: 'fixed z-
|
|
10
|
+
base: 'fixed z-overlay flex flex-col border border-divider bg-well shadow-lg glass-blur',
|
|
11
11
|
variants: {
|
|
12
12
|
// The two axes meet at the corners: the SIDE picks which corners are
|
|
13
13
|
// detached from the screen edge, the HEADER picks their radius. Stated
|
|
@@ -17,7 +17,7 @@ export declare const sheetVariants: import("tailwind-variants").TVReturnType<{
|
|
|
17
17
|
true: "[--sheet-rd:var(--era-rd-control)]";
|
|
18
18
|
false: "[--sheet-rd:var(--era-rd-bar)] gap-gutter overflow-y-auto p-card";
|
|
19
19
|
};
|
|
20
|
-
}, undefined, "fixed z-
|
|
20
|
+
}, undefined, "fixed z-overlay flex flex-col border border-divider bg-well shadow-lg glass-blur", {
|
|
21
21
|
side: {
|
|
22
22
|
top: "inset-x-0 top-0 max-h-[85dvh] rounded-b-(--sheet-rd)";
|
|
23
23
|
bottom: "inset-x-0 bottom-0 max-h-[85dvh] rounded-t-(--sheet-rd)";
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
<Tooltip.Content
|
|
16
16
|
bind:ref
|
|
17
17
|
class={cn(
|
|
18
|
-
'z-
|
|
18
|
+
'z-overlay inline-flex h-pill items-center gap-gutter rounded-pill bg-elevated px-inset-pill text-body leading-none whitespace-nowrap text-fg shadow-lg glass-blur has-[svg]:gap-inset-pill',
|
|
19
19
|
className
|
|
20
20
|
)}
|
|
21
21
|
{sideOffset}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@signal9/era-ui",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "13.0.0",
|
|
4
4
|
"scripts": {
|
|
5
5
|
"dev": "vite dev --host",
|
|
6
6
|
"build": "vite build && npm run prepack",
|
|
@@ -20,9 +20,11 @@
|
|
|
20
20
|
"check:tokens": "node --experimental-strip-types scripts/check-tokens.ts",
|
|
21
21
|
"check:routes": "node --experimental-strip-types scripts/check-routes.ts",
|
|
22
22
|
"check:canonical": "node --experimental-strip-types scripts/check-canonical.ts",
|
|
23
|
+
"check:layers": "node --experimental-strip-types scripts/check-layers.ts",
|
|
24
|
+
"check:themes": "node --experimental-strip-types scripts/check-themes.ts",
|
|
23
25
|
"build:llm-docs": "node --experimental-strip-types scripts/build-llm-docs.ts",
|
|
24
|
-
"prebuild": "npm run check:surfaces && npm run check:tokens && npm run check:routes && npm run check:canonical && npm run build:llm-docs",
|
|
25
|
-
"validate": "npm run check && npm run lint && npm run format:check && npm run check:surfaces && npm run check:tokens && npm run check:routes && npm run check:canonical",
|
|
26
|
+
"prebuild": "npm run check:surfaces && npm run check:tokens && npm run check:routes && npm run check:canonical && npm run check:layers && npm run check:themes && npm run build:llm-docs",
|
|
27
|
+
"validate": "npm run check && npm run lint && npm run format:check && npm run check:surfaces && npm run check:tokens && npm run check:routes && npm run check:canonical && npm run check:layers && npm run check:themes",
|
|
26
28
|
"test": "npm run test:visual"
|
|
27
29
|
},
|
|
28
30
|
"files": [
|
|
@@ -58,15 +60,10 @@
|
|
|
58
60
|
"svelte": "./dist/ai/*/index.js",
|
|
59
61
|
"default": "./dist/ai/*/index.js"
|
|
60
62
|
},
|
|
61
|
-
"./
|
|
62
|
-
"types": "./dist/
|
|
63
|
-
"svelte": "./dist/
|
|
64
|
-
"default": "./dist/
|
|
65
|
-
},
|
|
66
|
-
"./apps/*": {
|
|
67
|
-
"types": "./dist/apps/*/index.d.ts",
|
|
68
|
-
"svelte": "./dist/apps/*/index.js",
|
|
69
|
-
"default": "./dist/apps/*/index.js"
|
|
63
|
+
"./theme": {
|
|
64
|
+
"types": "./dist/theme/index.d.ts",
|
|
65
|
+
"svelte": "./dist/theme/index.js",
|
|
66
|
+
"default": "./dist/theme/index.js"
|
|
70
67
|
},
|
|
71
68
|
"./os": {
|
|
72
69
|
"types": "./dist/os/index.d.ts",
|
package/dist/apps/index.d.ts
DELETED
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The apps layer — whole, opinionated applications built out of era primitives.
|
|
3
|
-
*
|
|
4
|
-
* `ui/` ships unopinionated pieces and `ai/` ships chat/agent composites; an app
|
|
5
|
-
* here is the finished thing: its own data model, its own persistence, its own
|
|
6
|
-
* keyboard map. You drop it into a window and it works. That also means an app
|
|
7
|
-
* makes product decisions a primitive never would, which is exactly why these
|
|
8
|
-
* live behind their own subpath instead of the root barrel.
|
|
9
|
-
*
|
|
10
|
-
* Apps are NOT re-exported from `@signal9/era-ui`. Each carries heavy,
|
|
11
|
-
* app-specific dependencies (Notes brings TipTap), and the same reasoning that
|
|
12
|
-
* isolates `ai/workflow` on its own subpath applies here: a consumer who never
|
|
13
|
-
* imports an app never pays for it. Import from `@signal9/era-ui/apps` (or a
|
|
14
|
-
* single app, e.g. `@signal9/era-ui/apps/notes`) and install that app's peers.
|
|
15
|
-
*
|
|
16
|
-
* Exports are flat and prefixed by app (`Notes`, `NotesStore`, `NoteEditor`) so
|
|
17
|
-
* a second app can land without a namespace churn.
|
|
18
|
-
*/
|
|
19
|
-
export * from './llm-shell/index.js';
|
|
20
|
-
export * from './notes/index.js';
|
package/dist/apps/index.js
DELETED
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The apps layer — whole, opinionated applications built out of era primitives.
|
|
3
|
-
*
|
|
4
|
-
* `ui/` ships unopinionated pieces and `ai/` ships chat/agent composites; an app
|
|
5
|
-
* here is the finished thing: its own data model, its own persistence, its own
|
|
6
|
-
* keyboard map. You drop it into a window and it works. That also means an app
|
|
7
|
-
* makes product decisions a primitive never would, which is exactly why these
|
|
8
|
-
* live behind their own subpath instead of the root barrel.
|
|
9
|
-
*
|
|
10
|
-
* Apps are NOT re-exported from `@signal9/era-ui`. Each carries heavy,
|
|
11
|
-
* app-specific dependencies (Notes brings TipTap), and the same reasoning that
|
|
12
|
-
* isolates `ai/workflow` on its own subpath applies here: a consumer who never
|
|
13
|
-
* imports an app never pays for it. Import from `@signal9/era-ui/apps` (or a
|
|
14
|
-
* single app, e.g. `@signal9/era-ui/apps/notes`) and install that app's peers.
|
|
15
|
-
*
|
|
16
|
-
* Exports are flat and prefixed by app (`Notes`, `NotesStore`, `NoteEditor`) so
|
|
17
|
-
* a second app can land without a namespace churn.
|
|
18
|
-
*/
|
|
19
|
-
export * from './llm-shell/index.js';
|
|
20
|
-
export * from './notes/index.js';
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export { default as LlmShell } from './llm-shell.svelte';
|
|
@@ -1,334 +0,0 @@
|
|
|
1
|
-
<script lang="ts">
|
|
2
|
-
/**
|
|
3
|
-
* LLM Shell — a streaming chat console, assembled entirely from the AI suite.
|
|
4
|
-
*
|
|
5
|
-
* A conversation rail (rename / star / delete / time buckets), a run-status
|
|
6
|
-
* strip, reasoning and tool steps with structured output and a runtime feed,
|
|
7
|
-
* an editable approval gate, an error panel, a compact chip row, a markdown
|
|
8
|
-
* response with a stream cursor, and a per-turn usage footer.
|
|
9
|
-
*
|
|
10
|
-
* The app owns PRESENTATION and interaction wiring only — it is deliberately
|
|
11
|
-
* backend-free. The host owns the model: it passes conversations in, and gets
|
|
12
|
-
* `onSend` / `onNewChat` / `onDelete` / `onStop` back. That is the same split
|
|
13
|
-
* Notes draws with its adapter, and it is why the docs demo can drive this
|
|
14
|
-
* with a canned stream while a real app drives it from an SDK without
|
|
15
|
-
* touching the component.
|
|
16
|
-
*
|
|
17
|
-
* Every presentation the suite offers is reachable through the LlmBlock union
|
|
18
|
-
* (see types.ts), so a host maps its stream into blocks rather than composing
|
|
19
|
-
* AI parts by hand.
|
|
20
|
-
*/
|
|
21
|
-
import type { Snippet } from 'svelte';
|
|
22
|
-
import * as AI from '../../ai/index.js';
|
|
23
|
-
import { Button } from '../../ui/button/index.js';
|
|
24
|
-
import { Card } from '../../ui/card/index.js';
|
|
25
|
-
import Copy from '@lucide/svelte/icons/copy';
|
|
26
|
-
import RotateCcw from '@lucide/svelte/icons/rotate-ccw';
|
|
27
|
-
import Sparkles from '@lucide/svelte/icons/sparkles';
|
|
28
|
-
import Plus from '@lucide/svelte/icons/plus';
|
|
29
|
-
import { cn } from '../../utils/index.js';
|
|
30
|
-
import LlmSwarmBlock from './llm-swarm.svelte';
|
|
31
|
-
import type { ChatStatus, LlmBlock, LlmConversation, LlmMessage, RunState } from './types.js';
|
|
32
|
-
|
|
33
|
-
let {
|
|
34
|
-
conversations = $bindable([]),
|
|
35
|
-
activeId = $bindable(''),
|
|
36
|
-
status = 'ready',
|
|
37
|
-
runStatus = 'idle',
|
|
38
|
-
suggestions = [],
|
|
39
|
-
/** Consumer owns the clock — the time bucketer takes it as an argument. */
|
|
40
|
-
now = Date.now(),
|
|
41
|
-
emptyTitle = 'LLM Shell',
|
|
42
|
-
emptyDescription = 'Ask something to get started.',
|
|
43
|
-
approvalState = $bindable('requested'),
|
|
44
|
-
placeholder = 'Send a message…',
|
|
45
|
-
onSend,
|
|
46
|
-
onNewChat,
|
|
47
|
-
onDelete,
|
|
48
|
-
onStop,
|
|
49
|
-
onOpenAgent,
|
|
50
|
-
onCancelAgent,
|
|
51
|
-
swarmRenderers,
|
|
52
|
-
empty,
|
|
53
|
-
class: className
|
|
54
|
-
}: {
|
|
55
|
-
conversations?: LlmConversation[];
|
|
56
|
-
activeId?: string;
|
|
57
|
-
status?: ChatStatus;
|
|
58
|
-
runStatus?: RunState;
|
|
59
|
-
suggestions?: string[];
|
|
60
|
-
now?: number;
|
|
61
|
-
emptyTitle?: string;
|
|
62
|
-
emptyDescription?: string;
|
|
63
|
-
approvalState?: AI.Confirmation.ApprovalState;
|
|
64
|
-
placeholder?: string;
|
|
65
|
-
onSend?: (text: string) => void;
|
|
66
|
-
onNewChat?: () => void;
|
|
67
|
-
onDelete?: (id: string) => void;
|
|
68
|
-
onStop?: () => void;
|
|
69
|
-
/**
|
|
70
|
-
* Open a swarm agent's own transcript. Without it a swarm renders read-only
|
|
71
|
-
* — the button appears on agents that carry a `conversationId`.
|
|
72
|
-
*/
|
|
73
|
-
onOpenAgent?: (agent: AI.Swarm.SwarmAgent) => void;
|
|
74
|
-
/** Cancel a swarm agent. Offered only while the agent has work left to stop. */
|
|
75
|
-
onCancelAgent?: (agent: AI.Swarm.SwarmAgent) => void;
|
|
76
|
-
/**
|
|
77
|
-
* Inline renderers for swarm artifact payloads, one per content kind
|
|
78
|
-
* (chart, exec, component, …) — see `SwarmContentRenderers`. The host
|
|
79
|
-
* registers them once here; every swarm block resolves the same registry.
|
|
80
|
-
*/
|
|
81
|
-
swarmRenderers?: AI.Swarm.SwarmContentRenderers;
|
|
82
|
-
/** Replaces the built-in empty state for a conversation with no messages. */
|
|
83
|
-
empty?: Snippet;
|
|
84
|
-
class?: string;
|
|
85
|
-
} = $props();
|
|
86
|
-
|
|
87
|
-
const active = $derived(conversations.find((c) => c.id === activeId) ?? conversations[0]);
|
|
88
|
-
const groups = $derived(AI.Conversations.bucketConversations(conversations, now));
|
|
89
|
-
|
|
90
|
-
// Swarm agents still in flight, across every swarm block in the open thread.
|
|
91
|
-
// The run strip is about THIS conversation's work, and a delegated run is the
|
|
92
|
-
// part of it least visible in the transcript — it happens inside one block.
|
|
93
|
-
const swarmActive = $derived(
|
|
94
|
-
AI.Swarm.summarizeSwarm(
|
|
95
|
-
(active?.messages ?? [])
|
|
96
|
-
.flatMap((message) => message.blocks ?? [])
|
|
97
|
-
.filter((block) => block.kind === 'swarm')
|
|
98
|
-
.flatMap((block) => block.swarm.agents)
|
|
99
|
-
).active
|
|
100
|
-
);
|
|
101
|
-
|
|
102
|
-
// One open chip per row is plenty — the expansion area is shared, so tracking
|
|
103
|
-
// more than the current index would render two bodies into one slot.
|
|
104
|
-
let openChip = $state<string | null>(null);
|
|
105
|
-
|
|
106
|
-
function copyText(text: string) {
|
|
107
|
-
void navigator.clipboard?.writeText(text);
|
|
108
|
-
}
|
|
109
|
-
</script>
|
|
110
|
-
|
|
111
|
-
<div class={cn('flex h-full min-h-0', className)}>
|
|
112
|
-
<!-- Conversation rail. The gutter (p-gutter) matches the inter-row gap so the
|
|
113
|
-
rows sit an even gap from every edge — the same treatment the docs Library
|
|
114
|
-
window gets via its pane bodyClass. -->
|
|
115
|
-
<aside class="hidden w-64 shrink-0 flex-col border-r border-divider p-gutter md:flex">
|
|
116
|
-
<AI.Conversations.Root>
|
|
117
|
-
{#snippet header()}
|
|
118
|
-
<Button class="w-full justify-start" onclick={() => onNewChat?.()}>
|
|
119
|
-
<Plus class="size-icon" aria-hidden="true" />
|
|
120
|
-
New chat
|
|
121
|
-
</Button>
|
|
122
|
-
{/snippet}
|
|
123
|
-
{#each groups as group (group.bucket)}
|
|
124
|
-
<AI.Conversations.Group label={group.label}>
|
|
125
|
-
{#each group.items as entry (entry.id)}
|
|
126
|
-
{@const convo = conversations.find((c) => c.id === entry.id)}
|
|
127
|
-
{#if convo}
|
|
128
|
-
<AI.Conversations.Item
|
|
129
|
-
bind:title={convo.title}
|
|
130
|
-
bind:starred={convo.starred}
|
|
131
|
-
status={convo.status}
|
|
132
|
-
active={convo.id === active?.id}
|
|
133
|
-
onSelect={() => (activeId = convo.id)}
|
|
134
|
-
onDelete={() => onDelete?.(convo.id)}
|
|
135
|
-
/>
|
|
136
|
-
{/if}
|
|
137
|
-
{/each}
|
|
138
|
-
</AI.Conversations.Group>
|
|
139
|
-
{/each}
|
|
140
|
-
</AI.Conversations.Root>
|
|
141
|
-
</aside>
|
|
142
|
-
|
|
143
|
-
<!-- Thread -->
|
|
144
|
-
<div class="flex min-w-0 flex-1 flex-col">
|
|
145
|
-
{#if runStatus !== 'idle'}
|
|
146
|
-
<div class="shrink-0 border-b border-divider px-card py-gutter">
|
|
147
|
-
<AI.RunStatus.Root
|
|
148
|
-
status={runStatus}
|
|
149
|
-
tools={{
|
|
150
|
-
complete: runStatus === 'running' ? 1 : 0,
|
|
151
|
-
total: 1,
|
|
152
|
-
running: runStatus === 'running' ? 0 : 1
|
|
153
|
-
}}
|
|
154
|
-
agents={swarmActive || undefined}
|
|
155
|
-
latest={{
|
|
156
|
-
tone: 'muted',
|
|
157
|
-
text:
|
|
158
|
-
swarmActive > 0
|
|
159
|
-
? `${swarmActive} agents working…`
|
|
160
|
-
: runStatus === 'running'
|
|
161
|
-
? 'streaming response…'
|
|
162
|
-
: 'thinking…'
|
|
163
|
-
}}
|
|
164
|
-
oncancel={() => onStop?.()}
|
|
165
|
-
/>
|
|
166
|
-
</div>
|
|
167
|
-
{/if}
|
|
168
|
-
|
|
169
|
-
<AI.Conversation.Root>
|
|
170
|
-
<AI.Conversation.Content>
|
|
171
|
-
{#if !active || active.messages.length === 0}
|
|
172
|
-
{#if empty}
|
|
173
|
-
{@render empty()}
|
|
174
|
-
{:else}
|
|
175
|
-
<AI.Conversation.EmptyState title={emptyTitle} description={emptyDescription}>
|
|
176
|
-
{#snippet icon()}
|
|
177
|
-
<Sparkles class="size-control text-muted" />
|
|
178
|
-
{/snippet}
|
|
179
|
-
</AI.Conversation.EmptyState>
|
|
180
|
-
{/if}
|
|
181
|
-
{:else}
|
|
182
|
-
{#each active.messages as message (message.id)}
|
|
183
|
-
{@render messageView(message)}
|
|
184
|
-
{/each}
|
|
185
|
-
{/if}
|
|
186
|
-
</AI.Conversation.Content>
|
|
187
|
-
<AI.Conversation.ScrollButton />
|
|
188
|
-
</AI.Conversation.Root>
|
|
189
|
-
|
|
190
|
-
<div class="mx-auto w-full max-w-2xl shrink-0 p-card pt-0">
|
|
191
|
-
{#if active && active.messages.length === 0 && suggestions.length > 0}
|
|
192
|
-
<div class="pb-gutter">
|
|
193
|
-
<AI.Suggestions.Root>
|
|
194
|
-
{#each suggestions as s (s)}
|
|
195
|
-
<AI.Suggestions.Suggestion suggestion={s} onSelect={(t) => onSend?.(t)} />
|
|
196
|
-
{/each}
|
|
197
|
-
</AI.Suggestions.Root>
|
|
198
|
-
</div>
|
|
199
|
-
{/if}
|
|
200
|
-
<AI.PromptInput.Root accept="image/*" onSubmit={(m) => onSend?.(m.text)}>
|
|
201
|
-
<AI.PromptInput.Attachments />
|
|
202
|
-
<AI.PromptInput.Toolbar>
|
|
203
|
-
<AI.PromptInput.AddAttachments />
|
|
204
|
-
<AI.PromptInput.Textarea {placeholder} />
|
|
205
|
-
<AI.PromptInput.Submit {status} onStop={() => onStop?.()} />
|
|
206
|
-
</AI.PromptInput.Toolbar>
|
|
207
|
-
</AI.PromptInput.Root>
|
|
208
|
-
</div>
|
|
209
|
-
</div>
|
|
210
|
-
</div>
|
|
211
|
-
|
|
212
|
-
<!-- ── Render snippets ─────────────────────────────────────────────────── -->
|
|
213
|
-
|
|
214
|
-
{#snippet messageView(message: LlmMessage)}
|
|
215
|
-
{#if message.role === 'user'}
|
|
216
|
-
<AI.Message.Root from="user">
|
|
217
|
-
<AI.Message.Content>{message.text}</AI.Message.Content>
|
|
218
|
-
</AI.Message.Root>
|
|
219
|
-
{:else}
|
|
220
|
-
<AI.Message.Root from="assistant">
|
|
221
|
-
{#each message.blocks ?? [] as block, i (i)}
|
|
222
|
-
{@render blockView(block)}
|
|
223
|
-
{/each}
|
|
224
|
-
</AI.Message.Root>
|
|
225
|
-
{/if}
|
|
226
|
-
{/snippet}
|
|
227
|
-
|
|
228
|
-
{#snippet blockView(block: LlmBlock)}
|
|
229
|
-
{#if block.kind === 'reasoning'}
|
|
230
|
-
<AI.Reasoning.Root streaming={block.streaming} bind:duration={block.duration}>
|
|
231
|
-
<AI.Reasoning.Trigger streaming={block.streaming} duration={block.duration} />
|
|
232
|
-
<AI.Reasoning.Content>
|
|
233
|
-
<p class="text-body leading-body text-muted">{block.text}</p>
|
|
234
|
-
</AI.Reasoning.Content>
|
|
235
|
-
</AI.Reasoning.Root>
|
|
236
|
-
{:else if block.kind === 'tool'}
|
|
237
|
-
<AI.Tool.Root state={block.state}>
|
|
238
|
-
<AI.Tool.Header type={block.name} />
|
|
239
|
-
<AI.Tool.Content>
|
|
240
|
-
<AI.Tool.Input input={block.input} />
|
|
241
|
-
{#if block.feed}
|
|
242
|
-
<AI.Tool.Section label="trace">
|
|
243
|
-
<AI.RuntimeFeed.Root items={block.feed} />
|
|
244
|
-
</AI.Tool.Section>
|
|
245
|
-
{/if}
|
|
246
|
-
{#if block.output !== undefined}
|
|
247
|
-
<AI.Tool.Section label="output">
|
|
248
|
-
<AI.StructuredValue.Root value={block.output} />
|
|
249
|
-
</AI.Tool.Section>
|
|
250
|
-
{/if}
|
|
251
|
-
</AI.Tool.Content>
|
|
252
|
-
</AI.Tool.Root>
|
|
253
|
-
{:else if block.kind === 'tool-row'}
|
|
254
|
-
<AI.Tool.Chips>
|
|
255
|
-
{#each block.tools as tool (tool.id)}
|
|
256
|
-
<AI.Tool.Chip
|
|
257
|
-
state={tool.state}
|
|
258
|
-
active={openChip === tool.id}
|
|
259
|
-
icon={tool.icon}
|
|
260
|
-
onclick={() => (openChip = openChip === tool.id ? null : tool.id)}
|
|
261
|
-
>
|
|
262
|
-
{tool.name}
|
|
263
|
-
</AI.Tool.Chip>
|
|
264
|
-
{/each}
|
|
265
|
-
{#snippet expanded()}
|
|
266
|
-
{@const tool = block.tools.find((t) => t.id === openChip)}
|
|
267
|
-
{#if tool}
|
|
268
|
-
<Card tone="well" class="flex min-w-0 flex-col gap-panel p-panel">
|
|
269
|
-
<AI.Tool.Input input={tool.input} />
|
|
270
|
-
<AI.Tool.Output output={tool.output} />
|
|
271
|
-
</Card>
|
|
272
|
-
{/if}
|
|
273
|
-
{/snippet}
|
|
274
|
-
</AI.Tool.Chips>
|
|
275
|
-
{:else if block.kind === 'exec'}
|
|
276
|
-
<AI.Tool.Root state={block.exitCode ? 'output-error' : 'output-available'}>
|
|
277
|
-
<AI.Tool.Header type={block.command} />
|
|
278
|
-
<AI.Tool.Content>
|
|
279
|
-
<AI.Tool.Exec
|
|
280
|
-
command={block.command}
|
|
281
|
-
stdout={block.stdout}
|
|
282
|
-
stderr={block.stderr}
|
|
283
|
-
exitCode={block.exitCode}
|
|
284
|
-
/>
|
|
285
|
-
</AI.Tool.Content>
|
|
286
|
-
</AI.Tool.Root>
|
|
287
|
-
{:else if block.kind === 'swarm'}
|
|
288
|
-
<LlmSwarmBlock
|
|
289
|
-
swarm={block.swarm}
|
|
290
|
-
view={block.view}
|
|
291
|
-
{now}
|
|
292
|
-
{onOpenAgent}
|
|
293
|
-
{onCancelAgent}
|
|
294
|
-
renderers={swarmRenderers}
|
|
295
|
-
/>
|
|
296
|
-
{:else if block.kind === 'approval'}
|
|
297
|
-
<AI.Confirmation.Root state={approvalState}>
|
|
298
|
-
Approve <span class="font-mono text-fg">{block.toolName}</span>? Review the arguments before
|
|
299
|
-
it runs.
|
|
300
|
-
<AI.Confirmation.Args controller={block.controller} />
|
|
301
|
-
<AI.Confirmation.Actions>
|
|
302
|
-
<Button variant="link" onclick={() => (approvalState = 'rejected')}>Reject</Button>
|
|
303
|
-
<Button tone="accent" onclick={() => (approvalState = 'approved')}>Approve</Button>
|
|
304
|
-
</AI.Confirmation.Actions>
|
|
305
|
-
</AI.Confirmation.Root>
|
|
306
|
-
{:else if block.kind === 'error'}
|
|
307
|
-
<AI.ErrorPanel.Root error={block.error} />
|
|
308
|
-
{:else if block.kind === 'answer'}
|
|
309
|
-
{#if block.text || block.streaming}
|
|
310
|
-
<AI.Message.Content>
|
|
311
|
-
<AI.Response content={block.text} streaming={block.streaming} />
|
|
312
|
-
</AI.Message.Content>
|
|
313
|
-
{/if}
|
|
314
|
-
{#if !block.streaming && block.text}
|
|
315
|
-
<AI.Message.Toolbar>
|
|
316
|
-
<AI.Actions.Action tooltip="Copy" onclick={() => copyText(block.text)}>
|
|
317
|
-
<Copy class="size-icon" />
|
|
318
|
-
</AI.Actions.Action>
|
|
319
|
-
<AI.Actions.Action tooltip="Regenerate" onclick={() => copyText(block.text)}>
|
|
320
|
-
<RotateCcw class="size-icon" />
|
|
321
|
-
</AI.Actions.Action>
|
|
322
|
-
</AI.Message.Toolbar>
|
|
323
|
-
{#if block.usage}
|
|
324
|
-
<AI.Message.Usage
|
|
325
|
-
tokensIn={block.usage.tokensIn}
|
|
326
|
-
tokensOut={block.usage.tokensOut}
|
|
327
|
-
model={block.usage.model}
|
|
328
|
-
cost={block.usage.cost}
|
|
329
|
-
breakdown={block.usage.breakdown}
|
|
330
|
-
/>
|
|
331
|
-
{/if}
|
|
332
|
-
{/if}
|
|
333
|
-
{/if}
|
|
334
|
-
{/snippet}
|
|
@@ -1,57 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* LLM Shell — a streaming chat console, assembled entirely from the AI suite.
|
|
3
|
-
*
|
|
4
|
-
* A conversation rail (rename / star / delete / time buckets), a run-status
|
|
5
|
-
* strip, reasoning and tool steps with structured output and a runtime feed,
|
|
6
|
-
* an editable approval gate, an error panel, a compact chip row, a markdown
|
|
7
|
-
* response with a stream cursor, and a per-turn usage footer.
|
|
8
|
-
*
|
|
9
|
-
* The app owns PRESENTATION and interaction wiring only — it is deliberately
|
|
10
|
-
* backend-free. The host owns the model: it passes conversations in, and gets
|
|
11
|
-
* `onSend` / `onNewChat` / `onDelete` / `onStop` back. That is the same split
|
|
12
|
-
* Notes draws with its adapter, and it is why the docs demo can drive this
|
|
13
|
-
* with a canned stream while a real app drives it from an SDK without
|
|
14
|
-
* touching the component.
|
|
15
|
-
*
|
|
16
|
-
* Every presentation the suite offers is reachable through the LlmBlock union
|
|
17
|
-
* (see types.ts), so a host maps its stream into blocks rather than composing
|
|
18
|
-
* AI parts by hand.
|
|
19
|
-
*/
|
|
20
|
-
import type { Snippet } from 'svelte';
|
|
21
|
-
import * as AI from '../../ai/index.js';
|
|
22
|
-
import type { ChatStatus, LlmConversation, RunState } from './types.js';
|
|
23
|
-
type $$ComponentProps = {
|
|
24
|
-
conversations?: LlmConversation[];
|
|
25
|
-
activeId?: string;
|
|
26
|
-
status?: ChatStatus;
|
|
27
|
-
runStatus?: RunState;
|
|
28
|
-
suggestions?: string[];
|
|
29
|
-
now?: number;
|
|
30
|
-
emptyTitle?: string;
|
|
31
|
-
emptyDescription?: string;
|
|
32
|
-
approvalState?: AI.Confirmation.ApprovalState;
|
|
33
|
-
placeholder?: string;
|
|
34
|
-
onSend?: (text: string) => void;
|
|
35
|
-
onNewChat?: () => void;
|
|
36
|
-
onDelete?: (id: string) => void;
|
|
37
|
-
onStop?: () => void;
|
|
38
|
-
/**
|
|
39
|
-
* Open a swarm agent's own transcript. Without it a swarm renders read-only
|
|
40
|
-
* — the button appears on agents that carry a `conversationId`.
|
|
41
|
-
*/
|
|
42
|
-
onOpenAgent?: (agent: AI.Swarm.SwarmAgent) => void;
|
|
43
|
-
/** Cancel a swarm agent. Offered only while the agent has work left to stop. */
|
|
44
|
-
onCancelAgent?: (agent: AI.Swarm.SwarmAgent) => void;
|
|
45
|
-
/**
|
|
46
|
-
* Inline renderers for swarm artifact payloads, one per content kind
|
|
47
|
-
* (chart, exec, component, …) — see `SwarmContentRenderers`. The host
|
|
48
|
-
* registers them once here; every swarm block resolves the same registry.
|
|
49
|
-
*/
|
|
50
|
-
swarmRenderers?: AI.Swarm.SwarmContentRenderers;
|
|
51
|
-
/** Replaces the built-in empty state for a conversation with no messages. */
|
|
52
|
-
empty?: Snippet;
|
|
53
|
-
class?: string;
|
|
54
|
-
};
|
|
55
|
-
declare const LlmShell: import("svelte").Component<$$ComponentProps, {}, "conversations" | "activeId" | "approvalState">;
|
|
56
|
-
type LlmShell = ReturnType<typeof LlmShell>;
|
|
57
|
-
export default LlmShell;
|