@signal9/era-ui 3.12.4 → 3.13.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.
- package/dist/apps/index.d.ts +1 -0
- package/dist/apps/index.js +1 -0
- package/dist/apps/llm-shell/index.d.ts +2 -0
- package/dist/apps/llm-shell/index.js +1 -0
- package/dist/apps/llm-shell/llm-shell.svelte +291 -0
- package/dist/apps/llm-shell/llm-shell.svelte.d.ts +44 -0
- package/dist/apps/llm-shell/types.d.ts +79 -0
- package/dist/apps/llm-shell/types.js +1 -0
- package/dist/apps/notes/note-editor.svelte +13 -3
- package/dist/apps/notes/notes.svelte +16 -5
- package/dist/generated-docs/llms-full.txt +8 -8
- package/dist/generated-docs/llms.txt +1 -1
- package/dist/generated-docs/manifest.json +8 -8
- package/package.json +6 -1
package/dist/apps/index.d.ts
CHANGED
package/dist/apps/index.js
CHANGED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default as LlmShell } from './llm-shell.svelte';
|
|
@@ -0,0 +1,291 @@
|
|
|
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 Copy from '@lucide/svelte/icons/copy';
|
|
25
|
+
import RotateCcw from '@lucide/svelte/icons/rotate-ccw';
|
|
26
|
+
import Sparkles from '@lucide/svelte/icons/sparkles';
|
|
27
|
+
import Plus from '@lucide/svelte/icons/plus';
|
|
28
|
+
import { cn } from '../../utils/index.js';
|
|
29
|
+
import type { ChatStatus, LlmBlock, LlmConversation, LlmMessage, RunState } from './types.js';
|
|
30
|
+
|
|
31
|
+
let {
|
|
32
|
+
conversations = $bindable([]),
|
|
33
|
+
activeId = $bindable(''),
|
|
34
|
+
status = 'ready',
|
|
35
|
+
runStatus = 'idle',
|
|
36
|
+
suggestions = [],
|
|
37
|
+
/** Consumer owns the clock — the time bucketer takes it as an argument. */
|
|
38
|
+
now = Date.now(),
|
|
39
|
+
emptyTitle = 'LLM Shell',
|
|
40
|
+
emptyDescription = 'Ask something to get started.',
|
|
41
|
+
approvalState = $bindable('requested'),
|
|
42
|
+
placeholder = 'Send a message…',
|
|
43
|
+
onSend,
|
|
44
|
+
onNewChat,
|
|
45
|
+
onDelete,
|
|
46
|
+
onStop,
|
|
47
|
+
empty,
|
|
48
|
+
class: className
|
|
49
|
+
}: {
|
|
50
|
+
conversations?: LlmConversation[];
|
|
51
|
+
activeId?: string;
|
|
52
|
+
status?: ChatStatus;
|
|
53
|
+
runStatus?: RunState;
|
|
54
|
+
suggestions?: string[];
|
|
55
|
+
now?: number;
|
|
56
|
+
emptyTitle?: string;
|
|
57
|
+
emptyDescription?: string;
|
|
58
|
+
approvalState?: AI.Confirmation.ApprovalState;
|
|
59
|
+
placeholder?: string;
|
|
60
|
+
onSend?: (text: string) => void;
|
|
61
|
+
onNewChat?: () => void;
|
|
62
|
+
onDelete?: (id: string) => void;
|
|
63
|
+
onStop?: () => void;
|
|
64
|
+
/** Replaces the built-in empty state for a conversation with no messages. */
|
|
65
|
+
empty?: Snippet;
|
|
66
|
+
class?: string;
|
|
67
|
+
} = $props();
|
|
68
|
+
|
|
69
|
+
const active = $derived(conversations.find((c) => c.id === activeId) ?? conversations[0]);
|
|
70
|
+
const groups = $derived(AI.Conversations.bucketConversations(conversations, now));
|
|
71
|
+
|
|
72
|
+
// One open chip per row is plenty — the expansion area is shared, so tracking
|
|
73
|
+
// more than the current index would render two bodies into one slot.
|
|
74
|
+
let openChip = $state<string | null>(null);
|
|
75
|
+
|
|
76
|
+
function copyText(text: string) {
|
|
77
|
+
void navigator.clipboard?.writeText(text);
|
|
78
|
+
}
|
|
79
|
+
</script>
|
|
80
|
+
|
|
81
|
+
<div class={cn('flex h-full min-h-0', className)}>
|
|
82
|
+
<!-- Conversation rail. The gutter (p-gutter) matches the inter-row gap so the
|
|
83
|
+
rows sit an even gap from every edge — the same treatment the docs Library
|
|
84
|
+
window gets via its pane bodyClass. -->
|
|
85
|
+
<aside class="hidden w-64 shrink-0 flex-col border-r border-divider p-gutter md:flex">
|
|
86
|
+
<AI.Conversations.Root>
|
|
87
|
+
{#snippet header()}
|
|
88
|
+
<Button class="w-full justify-start" onclick={() => onNewChat?.()}>
|
|
89
|
+
<Plus class="size-xs" aria-hidden="true" />
|
|
90
|
+
New chat
|
|
91
|
+
</Button>
|
|
92
|
+
{/snippet}
|
|
93
|
+
{#each groups as group (group.bucket)}
|
|
94
|
+
<AI.Conversations.Group label={group.label}>
|
|
95
|
+
{#each group.items as entry (entry.id)}
|
|
96
|
+
{@const convo = conversations.find((c) => c.id === entry.id)}
|
|
97
|
+
{#if convo}
|
|
98
|
+
<AI.Conversations.Item
|
|
99
|
+
bind:title={convo.title}
|
|
100
|
+
bind:starred={convo.starred}
|
|
101
|
+
status={convo.status}
|
|
102
|
+
active={convo.id === active?.id}
|
|
103
|
+
onSelect={() => (activeId = convo.id)}
|
|
104
|
+
onDelete={() => onDelete?.(convo.id)}
|
|
105
|
+
/>
|
|
106
|
+
{/if}
|
|
107
|
+
{/each}
|
|
108
|
+
</AI.Conversations.Group>
|
|
109
|
+
{/each}
|
|
110
|
+
</AI.Conversations.Root>
|
|
111
|
+
</aside>
|
|
112
|
+
|
|
113
|
+
<!-- Thread -->
|
|
114
|
+
<div class="flex min-w-0 flex-1 flex-col">
|
|
115
|
+
{#if runStatus !== 'idle'}
|
|
116
|
+
<div class="shrink-0 border-b border-divider px-(--era-pad-md) py-gutter">
|
|
117
|
+
<AI.RunStatus.Root
|
|
118
|
+
status={runStatus}
|
|
119
|
+
tools={{
|
|
120
|
+
complete: runStatus === 'running' ? 1 : 0,
|
|
121
|
+
total: 1,
|
|
122
|
+
running: runStatus === 'running' ? 0 : 1
|
|
123
|
+
}}
|
|
124
|
+
latest={{
|
|
125
|
+
tone: 'muted',
|
|
126
|
+
text: runStatus === 'running' ? 'streaming response…' : 'thinking…'
|
|
127
|
+
}}
|
|
128
|
+
oncancel={() => onStop?.()}
|
|
129
|
+
/>
|
|
130
|
+
</div>
|
|
131
|
+
{/if}
|
|
132
|
+
|
|
133
|
+
<AI.Conversation.Root>
|
|
134
|
+
<AI.Conversation.Content>
|
|
135
|
+
{#if !active || active.messages.length === 0}
|
|
136
|
+
{#if empty}
|
|
137
|
+
{@render empty()}
|
|
138
|
+
{:else}
|
|
139
|
+
<AI.Conversation.EmptyState title={emptyTitle} description={emptyDescription}>
|
|
140
|
+
{#snippet icon()}
|
|
141
|
+
<Sparkles class="size-md text-muted" />
|
|
142
|
+
{/snippet}
|
|
143
|
+
</AI.Conversation.EmptyState>
|
|
144
|
+
{/if}
|
|
145
|
+
{:else}
|
|
146
|
+
{#each active.messages as message (message.id)}
|
|
147
|
+
{@render messageView(message)}
|
|
148
|
+
{/each}
|
|
149
|
+
{/if}
|
|
150
|
+
</AI.Conversation.Content>
|
|
151
|
+
<AI.Conversation.ScrollButton />
|
|
152
|
+
</AI.Conversation.Root>
|
|
153
|
+
|
|
154
|
+
<div class="mx-auto w-full max-w-2xl shrink-0 p-(--era-pad-md) pt-0">
|
|
155
|
+
{#if active && active.messages.length === 0 && suggestions.length > 0}
|
|
156
|
+
<div class="pb-(--era-gap)">
|
|
157
|
+
<AI.Suggestions.Root>
|
|
158
|
+
{#each suggestions as s (s)}
|
|
159
|
+
<AI.Suggestions.Suggestion suggestion={s} onSelect={(t) => onSend?.(t)} />
|
|
160
|
+
{/each}
|
|
161
|
+
</AI.Suggestions.Root>
|
|
162
|
+
</div>
|
|
163
|
+
{/if}
|
|
164
|
+
<AI.PromptInput.Root accept="image/*" onSubmit={(m) => onSend?.(m.text)}>
|
|
165
|
+
<AI.PromptInput.Attachments />
|
|
166
|
+
<AI.PromptInput.Toolbar>
|
|
167
|
+
<AI.PromptInput.AddAttachments />
|
|
168
|
+
<AI.PromptInput.Textarea {placeholder} />
|
|
169
|
+
<AI.PromptInput.Submit {status} onStop={() => onStop?.()} />
|
|
170
|
+
</AI.PromptInput.Toolbar>
|
|
171
|
+
</AI.PromptInput.Root>
|
|
172
|
+
</div>
|
|
173
|
+
</div>
|
|
174
|
+
</div>
|
|
175
|
+
|
|
176
|
+
<!-- ── Render snippets ─────────────────────────────────────────────────── -->
|
|
177
|
+
|
|
178
|
+
{#snippet messageView(message: LlmMessage)}
|
|
179
|
+
{#if message.role === 'user'}
|
|
180
|
+
<AI.Message.Root from="user">
|
|
181
|
+
<AI.Message.Content>{message.text}</AI.Message.Content>
|
|
182
|
+
</AI.Message.Root>
|
|
183
|
+
{:else}
|
|
184
|
+
<AI.Message.Root from="assistant">
|
|
185
|
+
{#each message.blocks ?? [] as block, i (i)}
|
|
186
|
+
{@render blockView(block)}
|
|
187
|
+
{/each}
|
|
188
|
+
</AI.Message.Root>
|
|
189
|
+
{/if}
|
|
190
|
+
{/snippet}
|
|
191
|
+
|
|
192
|
+
{#snippet blockView(block: LlmBlock)}
|
|
193
|
+
{#if block.kind === 'reasoning'}
|
|
194
|
+
<AI.Reasoning.Root streaming={block.streaming} bind:duration={block.duration}>
|
|
195
|
+
<AI.Reasoning.Trigger streaming={block.streaming} duration={block.duration} />
|
|
196
|
+
<AI.Reasoning.Content>
|
|
197
|
+
<p class="text-body leading-relaxed text-muted">{block.text}</p>
|
|
198
|
+
</AI.Reasoning.Content>
|
|
199
|
+
</AI.Reasoning.Root>
|
|
200
|
+
{:else if block.kind === 'tool'}
|
|
201
|
+
<AI.Tool.Root state={block.state}>
|
|
202
|
+
<AI.Tool.Header type={block.name} />
|
|
203
|
+
<AI.Tool.Content>
|
|
204
|
+
<AI.Tool.Input input={block.input} />
|
|
205
|
+
{#if block.feed}
|
|
206
|
+
<AI.Tool.Section label="trace">
|
|
207
|
+
<AI.RuntimeFeed.Root items={block.feed} />
|
|
208
|
+
</AI.Tool.Section>
|
|
209
|
+
{/if}
|
|
210
|
+
{#if block.output !== undefined}
|
|
211
|
+
<AI.Tool.Section label="output">
|
|
212
|
+
<AI.StructuredValue.Root value={block.output} />
|
|
213
|
+
</AI.Tool.Section>
|
|
214
|
+
{/if}
|
|
215
|
+
</AI.Tool.Content>
|
|
216
|
+
</AI.Tool.Root>
|
|
217
|
+
{:else if block.kind === 'tool-row'}
|
|
218
|
+
<AI.Tool.Chips>
|
|
219
|
+
{#each block.tools as tool (tool.id)}
|
|
220
|
+
<AI.Tool.Chip
|
|
221
|
+
state={tool.state}
|
|
222
|
+
active={openChip === tool.id}
|
|
223
|
+
icon={tool.icon}
|
|
224
|
+
onclick={() => (openChip = openChip === tool.id ? null : tool.id)}
|
|
225
|
+
>
|
|
226
|
+
{tool.name}
|
|
227
|
+
</AI.Tool.Chip>
|
|
228
|
+
{/each}
|
|
229
|
+
{#snippet expanded()}
|
|
230
|
+
{@const tool = block.tools.find((t) => t.id === openChip)}
|
|
231
|
+
{#if tool}
|
|
232
|
+
<div
|
|
233
|
+
class="flex min-w-0 flex-col gap-(--era-pad-sm) rounded-md bg-well p-(--era-pad-sm) shadow-well"
|
|
234
|
+
>
|
|
235
|
+
<AI.Tool.Input input={tool.input} />
|
|
236
|
+
<AI.Tool.Output output={tool.output} />
|
|
237
|
+
</div>
|
|
238
|
+
{/if}
|
|
239
|
+
{/snippet}
|
|
240
|
+
</AI.Tool.Chips>
|
|
241
|
+
{:else if block.kind === 'exec'}
|
|
242
|
+
<AI.Tool.Root state={block.exitCode ? 'output-error' : 'output-available'}>
|
|
243
|
+
<AI.Tool.Header type={block.command} />
|
|
244
|
+
<AI.Tool.Content>
|
|
245
|
+
<AI.Tool.Exec
|
|
246
|
+
command={block.command}
|
|
247
|
+
stdout={block.stdout}
|
|
248
|
+
stderr={block.stderr}
|
|
249
|
+
exitCode={block.exitCode}
|
|
250
|
+
/>
|
|
251
|
+
</AI.Tool.Content>
|
|
252
|
+
</AI.Tool.Root>
|
|
253
|
+
{:else if block.kind === 'approval'}
|
|
254
|
+
<AI.Confirmation.Root state={approvalState}>
|
|
255
|
+
Approve <span class="font-mono text-fg">{block.toolName}</span>? Review the arguments before
|
|
256
|
+
it runs.
|
|
257
|
+
<AI.Confirmation.Args controller={block.controller} />
|
|
258
|
+
<AI.Confirmation.Actions>
|
|
259
|
+
<Button variant="link" onclick={() => (approvalState = 'rejected')}>Reject</Button>
|
|
260
|
+
<Button tone="accent" onclick={() => (approvalState = 'approved')}>Approve</Button>
|
|
261
|
+
</AI.Confirmation.Actions>
|
|
262
|
+
</AI.Confirmation.Root>
|
|
263
|
+
{:else if block.kind === 'error'}
|
|
264
|
+
<AI.ErrorPanel.Root error={block.error} />
|
|
265
|
+
{:else if block.kind === 'answer'}
|
|
266
|
+
{#if block.text || block.streaming}
|
|
267
|
+
<AI.Message.Content>
|
|
268
|
+
<AI.Response content={block.text} streaming={block.streaming} />
|
|
269
|
+
</AI.Message.Content>
|
|
270
|
+
{/if}
|
|
271
|
+
{#if !block.streaming && block.text}
|
|
272
|
+
<AI.Message.Toolbar>
|
|
273
|
+
<AI.Actions.Action tooltip="Copy" onclick={() => copyText(block.text)}>
|
|
274
|
+
<Copy class="size-xs" />
|
|
275
|
+
</AI.Actions.Action>
|
|
276
|
+
<AI.Actions.Action tooltip="Regenerate" onclick={() => copyText(block.text)}>
|
|
277
|
+
<RotateCcw class="size-xs" />
|
|
278
|
+
</AI.Actions.Action>
|
|
279
|
+
</AI.Message.Toolbar>
|
|
280
|
+
{#if block.usage}
|
|
281
|
+
<AI.Message.Usage
|
|
282
|
+
tokensIn={block.usage.tokensIn}
|
|
283
|
+
tokensOut={block.usage.tokensOut}
|
|
284
|
+
model={block.usage.model}
|
|
285
|
+
cost={block.usage.cost}
|
|
286
|
+
breakdown={block.usage.breakdown}
|
|
287
|
+
/>
|
|
288
|
+
{/if}
|
|
289
|
+
{/if}
|
|
290
|
+
{/if}
|
|
291
|
+
{/snippet}
|
|
@@ -0,0 +1,44 @@
|
|
|
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
|
+
/** Replaces the built-in empty state for a conversation with no messages. */
|
|
39
|
+
empty?: Snippet;
|
|
40
|
+
class?: string;
|
|
41
|
+
};
|
|
42
|
+
declare const LlmShell: import("svelte").Component<$$ComponentProps, {}, "conversations" | "activeId" | "approvalState">;
|
|
43
|
+
type LlmShell = ReturnType<typeof LlmShell>;
|
|
44
|
+
export default LlmShell;
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import type { Component } from 'svelte';
|
|
2
|
+
import type { IconProps } from '@lucide/svelte';
|
|
3
|
+
import type { ChatStatus, ConversationStatus, RunState, ToolState, UsageBreakdown } from '../../ai/index.js';
|
|
4
|
+
import type { ApprovalController } from '../../ai/confirmation/index.js';
|
|
5
|
+
import type { RuntimeFeedItemData } from '../../ai/runtime-feed/index.js';
|
|
6
|
+
/** Per-turn cost footer under a finished answer. */
|
|
7
|
+
export type LlmUsage = {
|
|
8
|
+
tokensIn: number;
|
|
9
|
+
tokensOut: number;
|
|
10
|
+
model: string;
|
|
11
|
+
cost: number;
|
|
12
|
+
breakdown?: UsageBreakdown;
|
|
13
|
+
};
|
|
14
|
+
/** One tool in a compact chip row — the projectMessages "tool burst" shape. */
|
|
15
|
+
export type LlmToolChip = {
|
|
16
|
+
id: string;
|
|
17
|
+
name: string;
|
|
18
|
+
state: ToolState;
|
|
19
|
+
icon?: Component<IconProps>;
|
|
20
|
+
input?: unknown;
|
|
21
|
+
output?: unknown;
|
|
22
|
+
};
|
|
23
|
+
/**
|
|
24
|
+
* Everything an assistant turn can contain.
|
|
25
|
+
*
|
|
26
|
+
* The union is the app's contract: it covers every presentation the AI suite
|
|
27
|
+
* has, so a host maps its own stream into these and never renders AI parts by
|
|
28
|
+
* hand. `tool-row` is the compact chips view (one shared expansion area);
|
|
29
|
+
* `tool` is the full expanded step with input, trace and output.
|
|
30
|
+
*/
|
|
31
|
+
export type LlmBlock = {
|
|
32
|
+
kind: 'reasoning';
|
|
33
|
+
text: string;
|
|
34
|
+
duration: number;
|
|
35
|
+
streaming: boolean;
|
|
36
|
+
} | {
|
|
37
|
+
kind: 'tool';
|
|
38
|
+
name: string;
|
|
39
|
+
input: unknown;
|
|
40
|
+
output?: unknown;
|
|
41
|
+
state: ToolState;
|
|
42
|
+
feed?: RuntimeFeedItemData[];
|
|
43
|
+
} | {
|
|
44
|
+
kind: 'tool-row';
|
|
45
|
+
tools: LlmToolChip[];
|
|
46
|
+
} | {
|
|
47
|
+
kind: 'exec';
|
|
48
|
+
command: string;
|
|
49
|
+
stdout?: string;
|
|
50
|
+
stderr?: string;
|
|
51
|
+
exitCode?: number;
|
|
52
|
+
} | {
|
|
53
|
+
kind: 'approval';
|
|
54
|
+
toolName: string;
|
|
55
|
+
controller: ApprovalController;
|
|
56
|
+
} | {
|
|
57
|
+
kind: 'error';
|
|
58
|
+
error: unknown;
|
|
59
|
+
} | {
|
|
60
|
+
kind: 'answer';
|
|
61
|
+
text: string;
|
|
62
|
+
streaming: boolean;
|
|
63
|
+
usage?: LlmUsage;
|
|
64
|
+
};
|
|
65
|
+
export type LlmMessage = {
|
|
66
|
+
id: number | string;
|
|
67
|
+
role: 'user' | 'assistant';
|
|
68
|
+
text?: string;
|
|
69
|
+
blocks?: LlmBlock[];
|
|
70
|
+
};
|
|
71
|
+
export type LlmConversation = {
|
|
72
|
+
id: string;
|
|
73
|
+
title: string;
|
|
74
|
+
updatedAt: number;
|
|
75
|
+
starred?: boolean;
|
|
76
|
+
status?: ConversationStatus;
|
|
77
|
+
messages: LlmMessage[];
|
|
78
|
+
};
|
|
79
|
+
export type { ChatStatus, RunState };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -457,7 +457,17 @@
|
|
|
457
457
|
.era-notes-prose :global(.tiptap .column-resize-handle) {
|
|
458
458
|
background-color: var(--color-primary);
|
|
459
459
|
}
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
460
|
+
/* NO ::selection rule here, deliberately.
|
|
461
|
+
*
|
|
462
|
+
* era owns text selection as a surface token contract — --era-selection-bg /
|
|
463
|
+
* -fg / -shadow (surfaces/base.css), which is how flat gets inverse video,
|
|
464
|
+
* glass a glowing wash, and bevel an opaque accent bar. This file used to
|
|
465
|
+
* override it with `background: var(--color-4)` and no `color`, which threw
|
|
466
|
+
* all of that away AND left body text on a mid-scale grey: the band barely
|
|
467
|
+
* separated from the page and the glyphs sat on top of it at whatever
|
|
468
|
+
* contrast happened to fall out. Dropping the override restores both the
|
|
469
|
+
* contrast (bright fill, --color-1 glyphs) and the per-surface treatment.
|
|
470
|
+
*
|
|
471
|
+
* The ProseMirror overrides above are still needed — those replace hardcoded
|
|
472
|
+
* library chrome that no era token would otherwise reach. */
|
|
463
473
|
</style>
|
|
@@ -223,7 +223,7 @@
|
|
|
223
223
|
{#if note.icon}
|
|
224
224
|
{note.icon}
|
|
225
225
|
{:else}
|
|
226
|
-
<FileText class="size-
|
|
226
|
+
<FileText class="size-(--era-icon-xxs) text-muted" />
|
|
227
227
|
{/if}
|
|
228
228
|
</span>
|
|
229
229
|
|
|
@@ -232,7 +232,9 @@
|
|
|
232
232
|
<!-- One cell, three occupants: the age at rest, the actions on
|
|
233
233
|
hover. Swapping them in place keeps the row from reflowing. -->
|
|
234
234
|
<span class="shrink-0 text-muted group-hover:hidden">
|
|
235
|
-
{#if note.pinned}<Pin class="size-
|
|
235
|
+
{#if note.pinned}<Pin class="size-(--era-icon-xxs)" />{:else}{formatAge(
|
|
236
|
+
note.updatedAt
|
|
237
|
+
)}{/if}
|
|
236
238
|
</span>
|
|
237
239
|
<span class="hidden shrink-0 items-center gap-(--era-xs-inset-md) group-hover:flex">
|
|
238
240
|
<Button
|
|
@@ -276,9 +278,15 @@
|
|
|
276
278
|
<!-- Document pane -->
|
|
277
279
|
<div class="flex min-w-0 flex-1 flex-col">
|
|
278
280
|
{#if selected}
|
|
279
|
-
|
|
281
|
+
<!-- md and xxs-padded, exactly like the sidebar's filter bar: the two
|
|
282
|
+
headers sit side by side across the top of the app, so an lg bar
|
|
283
|
+
here left the document header 8px taller than the list header. -->
|
|
284
|
+
<Bar
|
|
285
|
+
size="md"
|
|
286
|
+
class="shrink-0 gap-(--era-xxs-inset-md) rounded-none border-b border-divider-faded px-(--era-xxs-inset-md)"
|
|
287
|
+
>
|
|
280
288
|
<Popover.Root bind:open={iconPickerOpen}>
|
|
281
|
-
<Popover.Trigger icon aria-label="Change icon" title="Change icon">
|
|
289
|
+
<Popover.Trigger icon size="xxs" aria-label="Change icon" title="Change icon">
|
|
282
290
|
{#if selected.icon}
|
|
283
291
|
<span aria-hidden="true">{selected.icon}</span>
|
|
284
292
|
{:else}
|
|
@@ -321,8 +329,11 @@
|
|
|
321
329
|
</Popover.Content>
|
|
322
330
|
</Popover.Root>
|
|
323
331
|
|
|
332
|
+
<!-- Chromeless, like the filter input: an h-md Input inside an h-md bar
|
|
333
|
+
fills it edge to edge, so the well would draw a box with no room
|
|
334
|
+
around it. The bar is the container. -->
|
|
324
335
|
<Input
|
|
325
|
-
class="min-w-0 flex-1 bg-transparent shadow-none"
|
|
336
|
+
class="h-full min-w-0 flex-1 bg-transparent px-0 shadow-none"
|
|
326
337
|
aria-label="Note title"
|
|
327
338
|
placeholder="Untitled"
|
|
328
339
|
value={selected.title}
|
|
@@ -566,14 +566,6 @@ Make any element draggable with @neodrag/svelte v3.
|
|
|
566
566
|
|
|
567
567
|
<!-- end: draggable -->
|
|
568
568
|
|
|
569
|
-
<!-- begin: llm-shell -->
|
|
570
|
-
|
|
571
|
-
# LLM Shell
|
|
572
|
-
|
|
573
|
-
A full chat console — conversation rail, streaming, tools, approvals.
|
|
574
|
-
|
|
575
|
-
<!-- end: llm-shell -->
|
|
576
|
-
|
|
577
569
|
<!-- begin: os -->
|
|
578
570
|
|
|
579
571
|
# OS Shell
|
|
@@ -590,6 +582,14 @@ Release history — every version, grouped by change type.
|
|
|
590
582
|
|
|
591
583
|
<!-- end: changelog -->
|
|
592
584
|
|
|
585
|
+
<!-- begin: llm-shell -->
|
|
586
|
+
|
|
587
|
+
# LLM Shell
|
|
588
|
+
|
|
589
|
+
A full chat console — conversation rail, streaming, tools, approvals.
|
|
590
|
+
|
|
591
|
+
<!-- end: llm-shell -->
|
|
592
|
+
|
|
593
593
|
<!-- begin: notes -->
|
|
594
594
|
|
|
595
595
|
# Notes
|
|
@@ -31,7 +31,7 @@ Full concatenated reference: `{{ORIGIN}}/llms-full.txt`.
|
|
|
31
31
|
|
|
32
32
|
## Components
|
|
33
33
|
|
|
34
|
-
getting-started, spacing, surfaces, measurements, text, utilities, draggable,
|
|
34
|
+
getting-started, spacing, surfaces, measurements, text, utilities, draggable, os, changelog, llm-shell, notes, aspect-ratio, badge, avatar, bar, button, button-group, pane, card, chip, code-block, copy-button, cycle, kv, separator, sheet, skeleton, switch, toggle, accordion, alert-dialog, calendar, checkbox, collapsible, combobox, command, command-bar, step, context-menu, date-field, date-picker, date-range-field, date-range-picker, dialog, dropdown-menu, file-upload, scroll-area, input, link-preview, label, logo, menu, menubar, meter, navigation-menu, mode, pagination, pin-input, popover, progress, range-calendar, radio-group, rating-group, select, slider, table, tabs, time-field, time-range-field, toggle-group, toolbar, video-player, tooltip
|
|
35
35
|
|
|
36
36
|
## CSS utilities
|
|
37
37
|
|
|
@@ -76,14 +76,6 @@
|
|
|
76
76
|
"sections": [],
|
|
77
77
|
"file": "draggable.md"
|
|
78
78
|
},
|
|
79
|
-
{
|
|
80
|
-
"slug": "llm-shell",
|
|
81
|
-
"title": "LLM Shell",
|
|
82
|
-
"summary": "A full chat console — conversation rail, streaming, tools, approvals.",
|
|
83
|
-
"tokenEstimate": 21,
|
|
84
|
-
"sections": [],
|
|
85
|
-
"file": "llm-shell.md"
|
|
86
|
-
},
|
|
87
79
|
{
|
|
88
80
|
"slug": "os",
|
|
89
81
|
"title": "OS Shell",
|
|
@@ -100,6 +92,14 @@
|
|
|
100
92
|
"sections": [],
|
|
101
93
|
"file": "changelog.md"
|
|
102
94
|
},
|
|
95
|
+
{
|
|
96
|
+
"slug": "llm-shell",
|
|
97
|
+
"title": "LLM Shell",
|
|
98
|
+
"summary": "A full chat console — conversation rail, streaming, tools, approvals.",
|
|
99
|
+
"tokenEstimate": 21,
|
|
100
|
+
"sections": [],
|
|
101
|
+
"file": "llm-shell.md"
|
|
102
|
+
},
|
|
103
103
|
{
|
|
104
104
|
"slug": "notes",
|
|
105
105
|
"title": "Notes",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@signal9/era-ui",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.13.1",
|
|
4
4
|
"scripts": {
|
|
5
5
|
"dev": "vite dev --host",
|
|
6
6
|
"build": "vite build && npm run prepack",
|
|
@@ -68,6 +68,11 @@
|
|
|
68
68
|
"svelte": "./dist/apps/index.js",
|
|
69
69
|
"default": "./dist/apps/index.js"
|
|
70
70
|
},
|
|
71
|
+
"./apps/llm-shell": {
|
|
72
|
+
"types": "./dist/apps/llm-shell/index.d.ts",
|
|
73
|
+
"svelte": "./dist/apps/llm-shell/index.js",
|
|
74
|
+
"default": "./dist/apps/llm-shell/index.js"
|
|
75
|
+
},
|
|
71
76
|
"./apps/notes": {
|
|
72
77
|
"types": "./dist/apps/notes/index.d.ts",
|
|
73
78
|
"svelte": "./dist/apps/notes/index.js",
|