@signal9/era-ui 4.7.2 → 4.9.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/ai/swarm/index.d.ts +2 -1
- package/dist/ai/swarm/index.js +2 -1
- package/dist/ai/swarm/swarm-actions.svelte +86 -0
- package/dist/ai/swarm/swarm-actions.svelte.d.ts +31 -0
- package/dist/ai/swarm/swarm-agent.svelte +9 -43
- package/dist/ai/swarm/swarm-artifact.svelte +2 -6
- package/dist/ai/swarm/swarm-artifacts.svelte +1 -10
- package/dist/ai/swarm/swarm-artifacts.svelte.d.ts +0 -2
- package/dist/ai/swarm/swarm-barriers.svelte +39 -0
- package/dist/ai/swarm/swarm-barriers.svelte.d.ts +13 -0
- package/dist/ai/swarm/swarm-counts.svelte +1 -4
- package/dist/ai/swarm/swarm-counts.svelte.d.ts +0 -2
- package/dist/ai/swarm/swarm-footer.svelte +20 -13
- package/dist/ai/swarm/swarm-header.svelte +3 -5
- package/dist/ai/swarm/swarm-node.svelte +8 -64
- package/dist/ai/swarm/swarm-role.svelte +2 -9
- package/dist/ai/swarm/swarm-sandbox.svelte +45 -0
- package/dist/ai/swarm/swarm-sandbox.svelte.d.ts +10 -0
- package/dist/ai/swarm/swarm-tree.svelte +22 -8
- package/dist/ai/swarm/swarm.d.ts +8 -0
- package/dist/ai/swarm/swarm.js +11 -2
- package/dist/ai/swarm/swarm.svelte.d.ts +0 -1
- package/dist/ai/swarm/swarm.svelte.js +10 -12
- package/dist/apps/llm-shell/llm-shell.svelte +2 -36
- package/dist/apps/llm-shell/llm-swarm.svelte +64 -0
- package/dist/apps/llm-shell/llm-swarm.svelte.d.ts +23 -0
- package/dist/apps/llm-shell/types.d.ts +2 -0
- package/dist/apps/notes/editor/bubble-toolbar.svelte +2 -1
- package/dist/apps/notes/notes.svelte +13 -10
- package/dist/dev/audit/audits/clipped-glyphs.js +33 -11
- package/dist/docs/llms-txt.js +1 -1
- package/dist/era-ui.css +1 -1
- package/dist/generated-docs/bar.md +3 -0
- package/dist/generated-docs/input.md +3 -1
- package/dist/generated-docs/llms-full.txt +15 -6
- package/dist/generated-docs/llms.txt +1 -1
- package/dist/generated-docs/manifest.json +5 -5
- package/dist/generated-docs/utilities.json +2 -2
- package/dist/generated-docs/utilities.md +8 -4
- package/dist/styles/density.css +42 -9
- package/dist/styles/index.css +8 -4
- package/dist/ui/bar/bar.svelte +37 -3
- package/dist/ui/bar/bar.svelte.d.ts +61 -1
- package/dist/ui/input/input.svelte +24 -4
- package/dist/ui/input/input.svelte.d.ts +6 -1
- package/dist/ui/input/variants.d.ts +99 -30
- package/dist/ui/input/variants.js +45 -19
- package/package.json +1 -1
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
<script lang="ts" module>
|
|
2
|
+
import type { SwarmSandbox } from './swarm.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* VM health → tint. The one place it is spelled: the row showed the glyph and
|
|
6
|
+
* the card showed glyph + word, and each had its own copy of this mapping —
|
|
7
|
+
* which disagreed about `terminated`.
|
|
8
|
+
*/
|
|
9
|
+
const SANDBOX_TONE: Record<SwarmSandbox['status'], string> = {
|
|
10
|
+
running: 'text-success',
|
|
11
|
+
paused: 'text-warning',
|
|
12
|
+
terminated: 'text-muted'
|
|
13
|
+
};
|
|
14
|
+
</script>
|
|
15
|
+
|
|
16
|
+
<script lang="ts">
|
|
17
|
+
import Box from '@lucide/svelte/icons/box';
|
|
18
|
+
import { cn } from '../../utils/index.js';
|
|
19
|
+
|
|
20
|
+
let {
|
|
21
|
+
sandbox,
|
|
22
|
+
label = false,
|
|
23
|
+
class: className
|
|
24
|
+
}: {
|
|
25
|
+
sandbox: SwarmSandbox;
|
|
26
|
+
/** Spell the status out beside the glyph — for a card, where there is room. */
|
|
27
|
+
label?: boolean;
|
|
28
|
+
class?: string;
|
|
29
|
+
} = $props();
|
|
30
|
+
</script>
|
|
31
|
+
|
|
32
|
+
<!-- Deliberately a signal of its own, never folded into the status dot: an agent
|
|
33
|
+
can be running with a terminated VM (reaped, or never created), and one
|
|
34
|
+
indicator for both hides exactly the state you opened a console to find. -->
|
|
35
|
+
<span
|
|
36
|
+
class={cn(
|
|
37
|
+
'flex min-w-0 shrink-0 items-center gap-(--era-gap) font-mono',
|
|
38
|
+
SANDBOX_TONE[sandbox.status],
|
|
39
|
+
className
|
|
40
|
+
)}
|
|
41
|
+
title="sandbox {sandbox.id ?? ''} ({sandbox.status})"
|
|
42
|
+
>
|
|
43
|
+
<Box class="size-(--era-h-xs) shrink-0" aria-hidden="true" />
|
|
44
|
+
{#if label}<span class="truncate">{sandbox.status}</span>{/if}
|
|
45
|
+
</span>
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { SwarmSandbox } from './swarm.js';
|
|
2
|
+
type $$ComponentProps = {
|
|
3
|
+
sandbox: SwarmSandbox;
|
|
4
|
+
/** Spell the status out beside the glyph — for a card, where there is room. */
|
|
5
|
+
label?: boolean;
|
|
6
|
+
class?: string;
|
|
7
|
+
};
|
|
8
|
+
declare const SwarmSandbox: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
9
|
+
type SwarmSandbox = ReturnType<typeof SwarmSandbox>;
|
|
10
|
+
export default SwarmSandbox;
|
|
@@ -49,7 +49,11 @@
|
|
|
49
49
|
const isOpen = (id: string) => !closed[id];
|
|
50
50
|
|
|
51
51
|
function setOpen(agent: SwarmAgent, open: boolean) {
|
|
52
|
-
|
|
52
|
+
// Mutated in place, not replaced: assigning a fresh object changes the
|
|
53
|
+
// `closed` signal itself, which invalidates EVERY node's props (each reads it
|
|
54
|
+
// through isOpen) — so collapsing one row rebuilt the whole forest's props.
|
|
55
|
+
// The deep proxy notifies only this key's readers.
|
|
56
|
+
closed[agent.id] = !open;
|
|
53
57
|
onToggle?.(agent, open);
|
|
54
58
|
}
|
|
55
59
|
|
|
@@ -58,14 +62,23 @@
|
|
|
58
62
|
* order the arrow keys have to move through, so navigation is derived from
|
|
59
63
|
* the same walk that renders rather than from the DOM.
|
|
60
64
|
*/
|
|
61
|
-
|
|
65
|
+
// Carries the agent, not just its id: every arrow key that opens or closes a
|
|
66
|
+
// row needs the record, and looking it up again meant an O(n) find plus a
|
|
67
|
+
// null-guard in each branch of the key handler.
|
|
68
|
+
type Row = {
|
|
69
|
+
id: string;
|
|
70
|
+
agent: SwarmAgent;
|
|
71
|
+
parentId: string | null;
|
|
72
|
+
expandable: boolean;
|
|
73
|
+
open: boolean;
|
|
74
|
+
};
|
|
62
75
|
const rows = $derived.by(() => {
|
|
63
76
|
const out: Row[] = [];
|
|
64
77
|
const walk = (nodes: SwarmNode[], parentId: string | null) => {
|
|
65
78
|
for (const node of nodes) {
|
|
66
79
|
const canExpand = nodeExpandable(node);
|
|
67
80
|
const open = isOpen(node.agent.id);
|
|
68
|
-
out.push({ id: node.agent.id, parentId, expandable: canExpand, open });
|
|
81
|
+
out.push({ id: node.agent.id, agent: node.agent, parentId, expandable: canExpand, open });
|
|
69
82
|
if (canExpand && open) walk(node.children, node.agent.id);
|
|
70
83
|
}
|
|
71
84
|
};
|
|
@@ -117,8 +130,7 @@
|
|
|
117
130
|
if (!current) break;
|
|
118
131
|
event.preventDefault();
|
|
119
132
|
if (current.expandable && !current.open) {
|
|
120
|
-
|
|
121
|
-
if (agent) setOpen(agent, true);
|
|
133
|
+
setOpen(current.agent, true);
|
|
122
134
|
} else if (rows[index + 1]?.parentId === current.id) {
|
|
123
135
|
focusRow(rows[index + 1].id);
|
|
124
136
|
}
|
|
@@ -128,8 +140,7 @@
|
|
|
128
140
|
if (!current) break;
|
|
129
141
|
event.preventDefault();
|
|
130
142
|
if (current.expandable && current.open) {
|
|
131
|
-
|
|
132
|
-
if (agent) setOpen(agent, false);
|
|
143
|
+
setOpen(current.agent, false);
|
|
133
144
|
} else {
|
|
134
145
|
focusRow(current.parentId);
|
|
135
146
|
}
|
|
@@ -185,8 +196,11 @@
|
|
|
185
196
|
detail,
|
|
186
197
|
meta,
|
|
187
198
|
open: isOpen(node.agent.id),
|
|
199
|
+
// `active` is the whole roving-focus story: the node derives its own
|
|
200
|
+
// tabindex from it. Passing tabindex separately meant the same fact twice,
|
|
201
|
+
// and it only worked because the node happened to spread restProps AFTER
|
|
202
|
+
// its own tabindex — reorder those two lines and Tab access dies silently.
|
|
188
203
|
active: focusedId === node.agent.id,
|
|
189
|
-
tabindex: focusedId === node.agent.id ? 0 : -1,
|
|
190
204
|
onToggle: (open: boolean) => setOpen(node.agent, open)
|
|
191
205
|
}}
|
|
192
206
|
{#if node.children.length > 0}
|
package/dist/ai/swarm/swarm.d.ts
CHANGED
|
@@ -175,6 +175,14 @@ export declare function summarizeBarrier(barrier: SwarmBarrier, agents: SwarmAge
|
|
|
175
175
|
total: number;
|
|
176
176
|
status: SwarmBarrierStatus;
|
|
177
177
|
};
|
|
178
|
+
/**
|
|
179
|
+
* Work that has not landed yet — blocked, queued or running.
|
|
180
|
+
*
|
|
181
|
+
* The predicate a cancel affordance is gated on, and the definition of `active`
|
|
182
|
+
* in `summarizeSwarm`. One function so the two can never disagree about whether
|
|
183
|
+
* a blocked agent is still cancellable.
|
|
184
|
+
*/
|
|
185
|
+
export declare function isSwarmAgentActive(agent: SwarmAgent): boolean;
|
|
178
186
|
/**
|
|
179
187
|
* How long an agent has been working, in ms — `null` when it never started.
|
|
180
188
|
*
|
package/dist/ai/swarm/swarm.js
CHANGED
|
@@ -100,9 +100,8 @@ export function summarizeSwarm(agents) {
|
|
|
100
100
|
};
|
|
101
101
|
for (const agent of agents) {
|
|
102
102
|
counts[agent.status]++;
|
|
103
|
-
if (agent
|
|
103
|
+
if (isSwarmAgentActive(agent))
|
|
104
104
|
counts.active++;
|
|
105
|
-
}
|
|
106
105
|
if (agent.sandbox && agent.sandbox.status !== 'terminated')
|
|
107
106
|
counts.sandboxes++;
|
|
108
107
|
}
|
|
@@ -145,6 +144,16 @@ export function summarizeBarrier(barrier, agents) {
|
|
|
145
144
|
: 'done';
|
|
146
145
|
return { done, failed, total, status: barrier.status ?? (total === 0 ? 'open' : derived) };
|
|
147
146
|
}
|
|
147
|
+
/**
|
|
148
|
+
* Work that has not landed yet — blocked, queued or running.
|
|
149
|
+
*
|
|
150
|
+
* The predicate a cancel affordance is gated on, and the definition of `active`
|
|
151
|
+
* in `summarizeSwarm`. One function so the two can never disagree about whether
|
|
152
|
+
* a blocked agent is still cancellable.
|
|
153
|
+
*/
|
|
154
|
+
export function isSwarmAgentActive(agent) {
|
|
155
|
+
return agent.status === 'blocked' || agent.status === 'queued' || agent.status === 'running';
|
|
156
|
+
}
|
|
148
157
|
/**
|
|
149
158
|
* How long an agent has been working, in ms — `null` when it never started.
|
|
150
159
|
*
|
|
@@ -38,7 +38,6 @@ export declare class SwarmController {
|
|
|
38
38
|
/** The watermark: the last APPLIED event seq, and the resume point. */
|
|
39
39
|
seq: number;
|
|
40
40
|
constructor(snapshot?: SwarmSnapshot);
|
|
41
|
-
readonly tree: import("./swarm.js").SwarmNode[];
|
|
42
41
|
readonly counts: import("./swarm.js").SwarmCounts;
|
|
43
42
|
/**
|
|
44
43
|
* Merge a snapshot in.
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* the only one a host has to construct). See that module for the model itself
|
|
7
7
|
* and for why the sequence number is the whole resumption contract.
|
|
8
8
|
*/
|
|
9
|
-
import {
|
|
9
|
+
import { summarizeSwarm } from './swarm.js';
|
|
10
10
|
/**
|
|
11
11
|
* Merge an agent patch over what is already known.
|
|
12
12
|
*
|
|
@@ -21,12 +21,18 @@ function mergeAgent(prev, next) {
|
|
|
21
21
|
const patch = Object.fromEntries(Object.entries(next).filter(([, value]) => value !== undefined));
|
|
22
22
|
return { ...prev, ...patch };
|
|
23
23
|
}
|
|
24
|
-
|
|
24
|
+
/**
|
|
25
|
+
* Replace-or-append by id, immutably. `merge` is how the two kinds of record
|
|
26
|
+
* differ: barriers and artifacts arrive whole, agents arrive as patches (see
|
|
27
|
+
* `mergeAgent`), and that is the only difference — so it is a parameter rather
|
|
28
|
+
* than a second copy of the find-index/clone/splice dance.
|
|
29
|
+
*/
|
|
30
|
+
function upsert(list, item, merge = (prev, next) => ({ ...prev, ...next })) {
|
|
25
31
|
const index = list.findIndex((existing) => existing.id === item.id);
|
|
26
32
|
if (index < 0)
|
|
27
33
|
return [...list, item];
|
|
28
34
|
const next = [...list];
|
|
29
|
-
next[index] =
|
|
35
|
+
next[index] = merge(next[index], item);
|
|
30
36
|
return next;
|
|
31
37
|
}
|
|
32
38
|
/**
|
|
@@ -62,7 +68,6 @@ export class SwarmController {
|
|
|
62
68
|
if (snapshot)
|
|
63
69
|
this.snapshot(snapshot);
|
|
64
70
|
}
|
|
65
|
-
tree = $derived(buildSwarmTree(this.agents));
|
|
66
71
|
counts = $derived(summarizeSwarm(this.agents));
|
|
67
72
|
/**
|
|
68
73
|
* Merge a snapshot in.
|
|
@@ -113,13 +118,6 @@ export class SwarmController {
|
|
|
113
118
|
this.seq = -1;
|
|
114
119
|
}
|
|
115
120
|
#mergeAgent(agent) {
|
|
116
|
-
|
|
117
|
-
if (index < 0) {
|
|
118
|
-
this.agents = [...this.agents, agent];
|
|
119
|
-
return;
|
|
120
|
-
}
|
|
121
|
-
const next = [...this.agents];
|
|
122
|
-
next[index] = mergeAgent(next[index], agent);
|
|
123
|
-
this.agents = next;
|
|
121
|
+
this.agents = upsert(this.agents, agent, mergeAgent);
|
|
124
122
|
}
|
|
125
123
|
}
|
|
@@ -27,6 +27,7 @@
|
|
|
27
27
|
import Sparkles from '@lucide/svelte/icons/sparkles';
|
|
28
28
|
import Plus from '@lucide/svelte/icons/plus';
|
|
29
29
|
import { cn } from '../../utils/index.js';
|
|
30
|
+
import LlmSwarmBlock from './llm-swarm.svelte';
|
|
30
31
|
import type { ChatStatus, LlmBlock, LlmConversation, LlmMessage, RunState } from './types.js';
|
|
31
32
|
|
|
32
33
|
let {
|
|
@@ -277,42 +278,7 @@
|
|
|
277
278
|
</AI.Tool.Content>
|
|
278
279
|
</AI.Tool.Root>
|
|
279
280
|
{:else if block.kind === 'swarm'}
|
|
280
|
-
|
|
281
|
-
delegation shape (who asked whom, what is still open); `view="grid"`
|
|
282
|
-
turns the same agents into the console layout for a wide pane.
|
|
283
|
-
Everything reads off block.swarm, so a live SwarmController streams
|
|
284
|
-
straight into it — the app never copies swarm state around. -->
|
|
285
|
-
{@const swarm = block.swarm}
|
|
286
|
-
{@const artifactsFor = (agent: AI.Swarm.SwarmAgent) =>
|
|
287
|
-
(swarm.artifacts ?? []).filter((a) => a.agentId === agent.id)}
|
|
288
|
-
<AI.Swarm.Root {now} onOpen={onOpenAgent} onCancel={onCancelAgent}>
|
|
289
|
-
<AI.Swarm.Header title={swarm.plan?.title} status={swarm.plan?.status}>
|
|
290
|
-
<AI.Swarm.Counts agents={swarm.agents} />
|
|
291
|
-
</AI.Swarm.Header>
|
|
292
|
-
|
|
293
|
-
{#if block.view === 'grid'}
|
|
294
|
-
<AI.Swarm.Grid agents={swarm.agents} />
|
|
295
|
-
{:else}
|
|
296
|
-
<AI.Swarm.Tree agents={swarm.agents} expandable={(agent) => artifactsFor(agent).length > 0}>
|
|
297
|
-
{#snippet detail(agent)}
|
|
298
|
-
<AI.Swarm.Artifacts artifacts={artifactsFor(agent)} />
|
|
299
|
-
{/snippet}
|
|
300
|
-
</AI.Swarm.Tree>
|
|
301
|
-
{/if}
|
|
302
|
-
|
|
303
|
-
{#if (swarm.barriers ?? []).length > 0}
|
|
304
|
-
<!-- Fan-in gates last: they are what the branches above are waiting on. -->
|
|
305
|
-
<div
|
|
306
|
-
class="flex flex-col gap-(--era-pad-sm) border-t border-divider-faded p-(--era-pad-sm)"
|
|
307
|
-
>
|
|
308
|
-
{#each swarm.barriers ?? [] as barrier (barrier.id)}
|
|
309
|
-
<AI.Swarm.Barrier {barrier} agents={swarm.agents} />
|
|
310
|
-
{/each}
|
|
311
|
-
</div>
|
|
312
|
-
{/if}
|
|
313
|
-
|
|
314
|
-
<AI.Swarm.Footer connection={swarm.connection} seq={swarm.seq} />
|
|
315
|
-
</AI.Swarm.Root>
|
|
281
|
+
<LlmSwarmBlock swarm={block.swarm} view={block.view} {now} {onOpenAgent} {onCancelAgent} />
|
|
316
282
|
{:else if block.kind === 'approval'}
|
|
317
283
|
<AI.Confirmation.Root state={approvalState}>
|
|
318
284
|
Approve <span class="font-mono text-fg">{block.toolName}</span>? Review the arguments before
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* The `swarm` block: a delegated run, inline in the turn that started it.
|
|
4
|
+
*
|
|
5
|
+
* Its own component rather than a branch of llm-shell's blockView, because it
|
|
6
|
+
* needs one thing a snippet cannot hold — a $derived index of artifacts by
|
|
7
|
+
* agent. Inline, the lookup was a `{@const}` arrow re-created on every render
|
|
8
|
+
* and called once per node by the tree's expandable predicate, again by the
|
|
9
|
+
* keyboard walk, and again by the detail body: O(agents x artifacts) per event,
|
|
10
|
+
* three times over. Here it is one pass per artifact change.
|
|
11
|
+
*/
|
|
12
|
+
import * as AI from '../../ai/index.js';
|
|
13
|
+
import type { LlmSwarm } from './types.js';
|
|
14
|
+
|
|
15
|
+
let {
|
|
16
|
+
swarm,
|
|
17
|
+
view = 'tree',
|
|
18
|
+
now = Date.now(),
|
|
19
|
+
onOpenAgent,
|
|
20
|
+
onCancelAgent
|
|
21
|
+
}: {
|
|
22
|
+
swarm: LlmSwarm;
|
|
23
|
+
/** `tree` keeps the delegation shape; `grid` is the console layout. */
|
|
24
|
+
view?: 'tree' | 'grid';
|
|
25
|
+
now?: number;
|
|
26
|
+
onOpenAgent?: (agent: AI.Swarm.SwarmAgent) => void;
|
|
27
|
+
onCancelAgent?: (agent: AI.Swarm.SwarmAgent) => void;
|
|
28
|
+
} = $props();
|
|
29
|
+
|
|
30
|
+
// A plain record, not a Map: it is rebuilt whole by the derived, so it never
|
|
31
|
+
// needs reactivity of its own (and SvelteMap here would only add proxy cost).
|
|
32
|
+
// NONE is shared so a miss doesn't allocate a fresh array per lookup per render.
|
|
33
|
+
const NONE: AI.Swarm.SwarmArtifact[] = [];
|
|
34
|
+
const byAgent = $derived.by(() => {
|
|
35
|
+
const index: Record<string, AI.Swarm.SwarmArtifact[]> = {};
|
|
36
|
+
for (const artifact of swarm.artifacts ?? []) {
|
|
37
|
+
if (artifact.agentId == null) continue;
|
|
38
|
+
(index[artifact.agentId] ??= []).push(artifact);
|
|
39
|
+
}
|
|
40
|
+
return index;
|
|
41
|
+
});
|
|
42
|
+
const artifactsFor = (agent: AI.Swarm.SwarmAgent) => byAgent[agent.id] ?? NONE;
|
|
43
|
+
</script>
|
|
44
|
+
|
|
45
|
+
<AI.Swarm.Root {now} onOpen={onOpenAgent} onCancel={onCancelAgent}>
|
|
46
|
+
<AI.Swarm.Header title={swarm.plan?.title} status={swarm.plan?.status}>
|
|
47
|
+
<AI.Swarm.Counts agents={swarm.agents} />
|
|
48
|
+
</AI.Swarm.Header>
|
|
49
|
+
|
|
50
|
+
{#if view === 'grid'}
|
|
51
|
+
<AI.Swarm.Grid agents={swarm.agents} />
|
|
52
|
+
{:else}
|
|
53
|
+
<AI.Swarm.Tree agents={swarm.agents} expandable={(agent) => artifactsFor(agent).length > 0}>
|
|
54
|
+
{#snippet detail(agent)}
|
|
55
|
+
<AI.Swarm.Artifacts artifacts={artifactsFor(agent)} />
|
|
56
|
+
{/snippet}
|
|
57
|
+
</AI.Swarm.Tree>
|
|
58
|
+
{/if}
|
|
59
|
+
|
|
60
|
+
<!-- Fan-in gates last: they are what the branches above are waiting on. -->
|
|
61
|
+
<AI.Swarm.Barriers barriers={swarm.barriers ?? []} agents={swarm.agents} />
|
|
62
|
+
|
|
63
|
+
<AI.Swarm.Footer connection={swarm.connection} seq={swarm.seq} error={swarm.error} />
|
|
64
|
+
</AI.Swarm.Root>
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `swarm` block: a delegated run, inline in the turn that started it.
|
|
3
|
+
*
|
|
4
|
+
* Its own component rather than a branch of llm-shell's blockView, because it
|
|
5
|
+
* needs one thing a snippet cannot hold — a $derived index of artifacts by
|
|
6
|
+
* agent. Inline, the lookup was a `{@const}` arrow re-created on every render
|
|
7
|
+
* and called once per node by the tree's expandable predicate, again by the
|
|
8
|
+
* keyboard walk, and again by the detail body: O(agents x artifacts) per event,
|
|
9
|
+
* three times over. Here it is one pass per artifact change.
|
|
10
|
+
*/
|
|
11
|
+
import * as AI from '../../ai/index.js';
|
|
12
|
+
import type { LlmSwarm } from './types.js';
|
|
13
|
+
type $$ComponentProps = {
|
|
14
|
+
swarm: LlmSwarm;
|
|
15
|
+
/** `tree` keeps the delegation shape; `grid` is the console layout. */
|
|
16
|
+
view?: 'tree' | 'grid';
|
|
17
|
+
now?: number;
|
|
18
|
+
onOpenAgent?: (agent: AI.Swarm.SwarmAgent) => void;
|
|
19
|
+
onCancelAgent?: (agent: AI.Swarm.SwarmAgent) => void;
|
|
20
|
+
};
|
|
21
|
+
declare const LlmSwarm: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
22
|
+
type LlmSwarm = ReturnType<typeof LlmSwarm>;
|
|
23
|
+
export default LlmSwarm;
|
|
@@ -36,6 +36,8 @@ export type LlmSwarm = {
|
|
|
36
36
|
artifacts?: SwarmArtifact[];
|
|
37
37
|
connection?: SwarmConnection;
|
|
38
38
|
seq?: number;
|
|
39
|
+
/** Last transport error. A `SwarmController` exposes this under the same name. */
|
|
40
|
+
error?: string | null;
|
|
39
41
|
};
|
|
40
42
|
/**
|
|
41
43
|
* Everything an assistant turn can contain.
|
|
@@ -32,7 +32,8 @@
|
|
|
32
32
|
it still tracks the surface rather than being a drawn-on line. -->
|
|
33
33
|
<Bar
|
|
34
34
|
size="md"
|
|
35
|
-
|
|
35
|
+
content="xxs"
|
|
36
|
+
class="w-max border border-divider-faded bg-elevated shadow-lg glass-blur"
|
|
36
37
|
>
|
|
37
38
|
{#each items as item (item.mark)}
|
|
38
39
|
{@const Icon = item.icon}
|
|
@@ -151,22 +151,24 @@
|
|
|
151
151
|
>
|
|
152
152
|
<!-- Sidebar: filter + note list -->
|
|
153
153
|
<div class="flex w-56 shrink-0 flex-col border-r border-divider-faded">
|
|
154
|
-
<!--
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
154
|
+
<!-- content="xxs": 24/30/36/48 of chrome around an 18/20/22/26 field and
|
|
155
|
+
button, with Bar deriving the two-tier concentric inset. A notes sidebar
|
|
156
|
+
is chrome around a list; at the lg/md pairing it was a 32px bar over
|
|
157
|
+
24px rows and the chrome outweighed the content. This is the tier pair
|
|
158
|
+
term's notes app uses — its hardcoded 3px is what Bar now derives at
|
|
159
|
+
dense. The field keeps its fill: xxs is the smallest tier a real field
|
|
160
|
+
fits on. -->
|
|
161
|
+
<Bar size="md" content="xxs" class="shrink-0 rounded-none border-b border-divider-faded">
|
|
161
162
|
<Input
|
|
162
163
|
icon={Search}
|
|
164
|
+
size="xxs"
|
|
163
165
|
class="min-w-0 flex-1"
|
|
164
166
|
type="text"
|
|
165
167
|
placeholder="Filter…"
|
|
166
168
|
aria-label="Filter notes"
|
|
167
169
|
bind:value={query}
|
|
168
170
|
/>
|
|
169
|
-
<Button icon aria-label="New note" title="New note" onclick={createNote}>
|
|
171
|
+
<Button icon size="xxs" aria-label="New note" title="New note" onclick={createNote}>
|
|
170
172
|
<Plus />
|
|
171
173
|
</Button>
|
|
172
174
|
</Bar>
|
|
@@ -312,9 +314,9 @@
|
|
|
312
314
|
{#if selected}
|
|
313
315
|
<!-- Matches the sidebar's filter bar exactly — the two headers sit side by
|
|
314
316
|
side across the top of the app, so they must be the same tier. -->
|
|
315
|
-
<Bar class="shrink-0 rounded-none border-b border-divider-faded">
|
|
317
|
+
<Bar size="md" content="xxs" class="shrink-0 rounded-none border-b border-divider-faded">
|
|
316
318
|
<Popover.Root bind:open={iconPickerOpen}>
|
|
317
|
-
<Popover.Trigger icon aria-label="Change icon" title="Change icon">
|
|
319
|
+
<Popover.Trigger icon size="xxs" aria-label="Change icon" title="Change icon">
|
|
318
320
|
{#if selected.icon}
|
|
319
321
|
<span aria-hidden="true">{selected.icon}</span>
|
|
320
322
|
{:else}
|
|
@@ -359,6 +361,7 @@
|
|
|
359
361
|
|
|
360
362
|
<Input
|
|
361
363
|
reveal
|
|
364
|
+
size="xxs"
|
|
362
365
|
class="min-w-0 flex-1"
|
|
363
366
|
aria-label="Note title"
|
|
364
367
|
placeholder="Untitled"
|
|
@@ -23,7 +23,7 @@ const INK_RATIO = 1.15;
|
|
|
23
23
|
export const clippedGlyphs = {
|
|
24
24
|
id: 'typography/clipped-glyphs',
|
|
25
25
|
name: 'Clipping text box is taller than its ink',
|
|
26
|
-
description: 'An element that clips its overflow must be at least as tall as
|
|
26
|
+
description: 'An element that clips its overflow must be at least as tall as the ink it holds — ~1.15em under an authored line-height, or its own line box when the height is constrained — or the clip slices the ascenders and descenders off its text.',
|
|
27
27
|
category: 'layout',
|
|
28
28
|
severity: 'error',
|
|
29
29
|
selector: '*',
|
|
@@ -36,24 +36,46 @@ export const clippedGlyphs = {
|
|
|
36
36
|
const s = getComputedStyle(el);
|
|
37
37
|
if (!/hidden|clip|auto|scroll/.test(s.overflowX + s.overflowY))
|
|
38
38
|
return null;
|
|
39
|
-
// `normal` IS the ink box: the UA sizes the line from the font's own
|
|
40
|
-
// ascent + descent, so the box cannot be shorter than what the font paints,
|
|
41
|
-
// whatever ratio that works out to (a serif at 14px lands at 16px = 1.14em,
|
|
42
|
-
// under the ratio below and perfectly safe). Only an AUTHORED line-height
|
|
43
|
-
// can starve the box, so only an authored one is measured.
|
|
44
|
-
if (s.lineHeight === 'normal')
|
|
45
|
-
return null;
|
|
46
39
|
const fontSize = parseFloat(s.fontSize) || 14;
|
|
47
40
|
const h = el.getBoundingClientRect().height;
|
|
48
41
|
if (h === 0)
|
|
49
42
|
return null;
|
|
50
|
-
|
|
43
|
+
/*
|
|
44
|
+
* Two ways a clipping box can be too short, and they need different tests.
|
|
45
|
+
*
|
|
46
|
+
* An AUTHORED line-height sets the box, so the box is measurable against the
|
|
47
|
+
* ink: shorter than ~1.15em and the glyphs lose their tails.
|
|
48
|
+
*
|
|
49
|
+
* `normal` sets the box from the font's own ascent + descent, so the LINE box
|
|
50
|
+
* is the ink box by construction and the ratio says nothing (a serif at 14px
|
|
51
|
+
* lands at 16px = 1.14em, under the ratio and perfectly safe). But the
|
|
52
|
+
* ELEMENT can still be shorter than that line box — an explicit height, a
|
|
53
|
+
* flex or grid constraint, a max-height — and then the clip bites exactly as
|
|
54
|
+
* hard. That case is what scrollHeight reports: content taller than the box
|
|
55
|
+
* it is being clipped into. An earlier version of this audit bailed on
|
|
56
|
+
* `normal` outright and so could never fire on era's own `truncate`, which
|
|
57
|
+
* sets line-height: normal — the exact construct the description names.
|
|
58
|
+
*/
|
|
59
|
+
const authored = s.lineHeight !== 'normal';
|
|
60
|
+
const starved = authored ? h < fontSize * INK_RATIO : el.scrollHeight - el.clientHeight > 1;
|
|
61
|
+
if (!starved)
|
|
51
62
|
return null;
|
|
63
|
+
const need = authored
|
|
64
|
+
? `~${(fontSize * INK_RATIO).toFixed(1)}px`
|
|
65
|
+
: `${el.scrollHeight}px (its own line box)`;
|
|
52
66
|
const issue = {
|
|
53
67
|
auditId: 'typography/clipped-glyphs',
|
|
54
68
|
element: el,
|
|
55
|
-
message: `clipping box is ${h.toFixed(1)}px tall for ${fontSize}px text — it needs
|
|
56
|
-
|
|
69
|
+
message: `clipping box is ${h.toFixed(1)}px tall for ${fontSize}px text — it needs ${need}, so the ascenders and descenders are sliced off ("g" loses its hook). line-height is ${s.lineHeight}; ${authored
|
|
70
|
+
? "give the clipping element line-height: normal (era's truncate does)"
|
|
71
|
+
: 'the box is constrained shorter than its line box — drop the height'} or move the clip to a taller row.`,
|
|
72
|
+
details: {
|
|
73
|
+
height: h,
|
|
74
|
+
fontSize,
|
|
75
|
+
lineHeight: s.lineHeight,
|
|
76
|
+
lineBox: el.scrollHeight,
|
|
77
|
+
text: text.slice(0, 40)
|
|
78
|
+
}
|
|
57
79
|
};
|
|
58
80
|
return issue;
|
|
59
81
|
}
|
package/dist/docs/llms-txt.js
CHANGED
|
@@ -40,7 +40,7 @@ function orientation(origin) {
|
|
|
40
40
|
'lg = 4·sp bars, top-level containers 32 / 40 / 48 px',
|
|
41
41
|
'```',
|
|
42
42
|
'',
|
|
43
|
-
'Heights (`--era-h-*`), insets (`--era-inset-*`), radii (`--era-rd-*`), and concentric gaps (`--era-xs-inset-sm`, `--era-xxs-inset-md`, …) are all `calc()` chains off `--era-sp`. Never hard-code pixel values — setting `data-mode` on any ancestor re-derives the whole subtree.
|
|
43
|
+
'Heights (`--era-h-*`), insets (`--era-inset-*`), radii (`--era-rd-*`), and concentric gaps (`--era-xs-inset-sm`, `--era-xxs-inset-md`, …) are all `calc()` chains off `--era-sp`. Never hard-code pixel values — setting `data-mode` on any ancestor re-derives the whole subtree. Type is the second density input (12 px at dense, 14 px above); the spacing atom moves with it.',
|
|
44
44
|
'',
|
|
45
45
|
'## Per-component docs',
|
|
46
46
|
'',
|