@signal9/era-ui 34.2.9 → 34.4.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/CHANGELOG.md +12 -0
- package/dist/ai/code-mode/code-mode.svelte +201 -0
- package/dist/ai/code-mode/code-mode.svelte.d.ts +15 -0
- package/dist/ai/code-mode/index.d.ts +2 -0
- package/dist/ai/code-mode/index.js +1 -0
- package/dist/ai/code-mode/types.d.ts +26 -0
- package/dist/ai/code-mode/types.js +1 -0
- package/dist/ai/confirmation/batch.d.ts +21 -0
- package/dist/ai/confirmation/batch.js +1 -0
- package/dist/ai/confirmation/confirmation-batch.svelte +190 -0
- package/dist/ai/confirmation/confirmation-batch.svelte.d.ts +17 -0
- package/dist/ai/confirmation/index.d.ts +2 -0
- package/dist/ai/confirmation/index.js +1 -0
- package/dist/ai/conversation/conversation-scroll-button.svelte +2 -2
- package/dist/ai/conversations/conversations-item.svelte +39 -0
- package/dist/ai/conversations/conversations-item.svelte.d.ts +12 -0
- package/dist/ai/conversations/conversations.svelte.d.ts +6 -0
- package/dist/ai/index.d.ts +3 -1
- package/dist/ai/index.js +1 -0
- package/dist/ai/model-selector/index.d.ts +1 -0
- package/dist/ai/model-selector/model-selector.svelte +65 -31
- package/dist/ai/model-selector/model-selector.svelte.d.ts +24 -7
- package/dist/ai/queue/index.d.ts +1 -1
- package/dist/ai/queue/queue-item.svelte +63 -6
- package/dist/ai/queue/queue-item.svelte.d.ts +12 -3
- package/dist/ai/queue/queue.svelte +33 -9
- package/dist/ai/queue/queue.svelte.d.ts +6 -2
- package/dist/ai/tool/index.d.ts +1 -1
- package/dist/ai/tool/index.js +1 -1
- package/dist/ai/tool/tool-chip.svelte +2 -12
- package/dist/ai/tool/tool-chip.svelte.d.ts +1 -1
- package/dist/ai/tool/tool.svelte +46 -16
- package/dist/ai/tool/tool.svelte.d.ts +11 -2
- package/dist/era-ui.css +1 -1
- package/dist/ui/file-diff/file-diff.svelte +243 -0
- package/dist/ui/file-diff/file-diff.svelte.d.ts +16 -0
- package/dist/ui/file-diff/fixture.svelte +76 -0
- package/dist/ui/file-diff/fixture.svelte.d.ts +3 -0
- package/dist/ui/file-diff/index.d.ts +2 -0
- package/dist/ui/file-diff/index.js +1 -0
- package/dist/ui/file-diff/types.d.ts +31 -0
- package/dist/ui/file-diff/types.js +1 -0
- package/dist/ui/tabs/tabs.svelte +7 -2
- package/dist/ui/tabs/tabs.svelte.d.ts +1 -1
- package/dist/ui/terminal/fixture.svelte +57 -0
- package/dist/ui/terminal/fixture.svelte.d.ts +3 -0
- package/dist/ui/terminal/index.d.ts +2 -0
- package/dist/ui/terminal/index.js +1 -0
- package/dist/ui/terminal/terminal.svelte +91 -0
- package/dist/ui/terminal/terminal.svelte.d.ts +18 -0
- package/dist/ui/terminal/types.d.ts +26 -0
- package/dist/ui/terminal/types.js +1 -0
- package/package.json +16 -1
- package/skills/era-ui/references/releases/34.md +12 -0
- package/skills/era-ui/references/releases/index.md +1 -1
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { HTMLAttributes } from 'svelte/elements';
|
|
3
|
+
import { cn } from '../../utils/index.js';
|
|
4
|
+
import type { FileDiffFile, FileDiffRow, FileDiffStatus } from './types.js';
|
|
5
|
+
|
|
6
|
+
interface Props extends HTMLAttributes<HTMLDivElement> {
|
|
7
|
+
ref?: HTMLDivElement | null;
|
|
8
|
+
files?: readonly FileDiffFile[];
|
|
9
|
+
selectedFileId?: string | null;
|
|
10
|
+
activeRowId?: string | null;
|
|
11
|
+
label?: string;
|
|
12
|
+
virtualize?: boolean;
|
|
13
|
+
overscan?: number;
|
|
14
|
+
onselectedfilechange?: (file: FileDiffFile) => void;
|
|
15
|
+
onactiverowchange?: (row: FileDiffRow) => void;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
let {
|
|
19
|
+
ref = $bindable(null),
|
|
20
|
+
files = [],
|
|
21
|
+
selectedFileId = $bindable(null),
|
|
22
|
+
activeRowId = $bindable(null),
|
|
23
|
+
label = 'File changes',
|
|
24
|
+
virtualize = true,
|
|
25
|
+
overscan = 6,
|
|
26
|
+
onselectedfilechange,
|
|
27
|
+
onactiverowchange,
|
|
28
|
+
class: className,
|
|
29
|
+
...restProps
|
|
30
|
+
}: Props = $props();
|
|
31
|
+
|
|
32
|
+
let viewport: HTMLDivElement | null = $state(null);
|
|
33
|
+
let scrollTop = $state(0);
|
|
34
|
+
let viewportHeight = $state(0);
|
|
35
|
+
let rowHeight = $state(30);
|
|
36
|
+
|
|
37
|
+
const activeFile = $derived(files.find((file) => file.id === selectedFileId) ?? files[0]);
|
|
38
|
+
const rows = $derived.by((): FileDiffRow[] => {
|
|
39
|
+
if (!activeFile) return [];
|
|
40
|
+
return activeFile.hunks.flatMap((hunk) => [
|
|
41
|
+
{ id: `hunk:${hunk.id}`, type: 'hunk' as const, header: hunk.header },
|
|
42
|
+
...hunk.lines.map((line): FileDiffRow => ({
|
|
43
|
+
id: `line:${hunk.id}:${line.id}`,
|
|
44
|
+
type: 'line',
|
|
45
|
+
line
|
|
46
|
+
}))
|
|
47
|
+
]);
|
|
48
|
+
});
|
|
49
|
+
const effectiveActiveId = $derived(
|
|
50
|
+
rows.some((row) => row.id === activeRowId) ? activeRowId : (rows[0]?.id ?? null)
|
|
51
|
+
);
|
|
52
|
+
const visibleCount = $derived(
|
|
53
|
+
Math.max(1, Math.ceil((viewportHeight || rowHeight * 10) / Math.max(rowHeight, 1)))
|
|
54
|
+
);
|
|
55
|
+
const startIndex = $derived(
|
|
56
|
+
virtualize ? Math.max(0, Math.floor(scrollTop / Math.max(rowHeight, 1)) - overscan) : 0
|
|
57
|
+
);
|
|
58
|
+
const endIndex = $derived(
|
|
59
|
+
virtualize ? Math.min(rows.length, startIndex + visibleCount + overscan * 2) : rows.length
|
|
60
|
+
);
|
|
61
|
+
const visibleRows = $derived(rows.slice(startIndex, endIndex));
|
|
62
|
+
const topSpace = $derived(startIndex * rowHeight);
|
|
63
|
+
const bottomSpace = $derived(Math.max(0, (rows.length - endIndex) * rowHeight));
|
|
64
|
+
|
|
65
|
+
const statusLabels: Record<FileDiffStatus, string> = {
|
|
66
|
+
added: 'Added',
|
|
67
|
+
modified: 'Modified',
|
|
68
|
+
deleted: 'Deleted',
|
|
69
|
+
renamed: 'Renamed'
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
$effect(() => {
|
|
73
|
+
const element = viewport;
|
|
74
|
+
if (!element) return;
|
|
75
|
+
const measure = () => {
|
|
76
|
+
viewportHeight = element.clientHeight;
|
|
77
|
+
const measured = Number.parseFloat(
|
|
78
|
+
getComputedStyle(element).getPropertyValue('--era-h-control')
|
|
79
|
+
);
|
|
80
|
+
if (Number.isFinite(measured) && measured > 0) rowHeight = measured;
|
|
81
|
+
};
|
|
82
|
+
measure();
|
|
83
|
+
const observer = new ResizeObserver(measure);
|
|
84
|
+
observer.observe(element);
|
|
85
|
+
return () => observer.disconnect();
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
function rowDomId(id: string): string {
|
|
89
|
+
return `era-file-diff-${id.replace(/[^a-zA-Z0-9_-]/g, '-')}`;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function chooseFile(file: FileDiffFile): void {
|
|
93
|
+
selectedFileId = file.id;
|
|
94
|
+
const next = file.hunks[0];
|
|
95
|
+
activeRowId = next ? `hunk:${next.id}` : null;
|
|
96
|
+
if (viewport) viewport.scrollTop = 0;
|
|
97
|
+
onselectedfilechange?.(file);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function chooseRow(row: FileDiffRow): void {
|
|
101
|
+
activeRowId = row.id;
|
|
102
|
+
onactiverowchange?.(row);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function moveActive(event: KeyboardEvent): void {
|
|
106
|
+
if (!rows.length) return;
|
|
107
|
+
const current = Math.max(
|
|
108
|
+
0,
|
|
109
|
+
rows.findIndex((row) => row.id === effectiveActiveId)
|
|
110
|
+
);
|
|
111
|
+
let next: number;
|
|
112
|
+
if (event.key === 'ArrowDown') next = Math.min(rows.length - 1, current + 1);
|
|
113
|
+
else if (event.key === 'ArrowUp') next = Math.max(0, current - 1);
|
|
114
|
+
else if (event.key === 'Home') next = 0;
|
|
115
|
+
else if (event.key === 'End') next = rows.length - 1;
|
|
116
|
+
else if (event.key === 'PageDown') next = Math.min(rows.length - 1, current + visibleCount);
|
|
117
|
+
else if (event.key === 'PageUp') next = Math.max(0, current - visibleCount);
|
|
118
|
+
else return;
|
|
119
|
+
|
|
120
|
+
event.preventDefault();
|
|
121
|
+
const row = rows[next];
|
|
122
|
+
chooseRow(row);
|
|
123
|
+
if (!viewport) return;
|
|
124
|
+
const top = next * rowHeight;
|
|
125
|
+
const bottom = top + rowHeight;
|
|
126
|
+
if (top < viewport.scrollTop) viewport.scrollTop = top;
|
|
127
|
+
else if (bottom > viewport.scrollTop + viewport.clientHeight) {
|
|
128
|
+
viewport.scrollTop = bottom - viewport.clientHeight;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function activateRow(event: KeyboardEvent, row: FileDiffRow): void {
|
|
133
|
+
if (event.key !== 'Enter' && event.key !== ' ') return;
|
|
134
|
+
event.preventDefault();
|
|
135
|
+
chooseRow(row);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function marker(kind: 'context' | 'addition' | 'deletion'): string {
|
|
139
|
+
return kind === 'addition' ? '+' : kind === 'deletion' ? '-' : ' ';
|
|
140
|
+
}
|
|
141
|
+
</script>
|
|
142
|
+
|
|
143
|
+
<div
|
|
144
|
+
bind:this={ref}
|
|
145
|
+
class={cn(
|
|
146
|
+
'flex min-h-0 min-w-0 flex-col overflow-hidden rounded-bar bg-well text-body text-fg shadow-well',
|
|
147
|
+
className
|
|
148
|
+
)}
|
|
149
|
+
{...restProps}
|
|
150
|
+
>
|
|
151
|
+
{#if files.length > 1}
|
|
152
|
+
<div
|
|
153
|
+
class="flex shrink-0 overflow-x-auto border-b border-divider-faded"
|
|
154
|
+
aria-label="Changed files"
|
|
155
|
+
>
|
|
156
|
+
{#each files as file (file.id)}
|
|
157
|
+
<button
|
|
158
|
+
type="button"
|
|
159
|
+
class={cn(
|
|
160
|
+
'flex h-control shrink-0 items-center gap-gutter px-inset-control text-left hover:bg-highlight focus-visible:bg-highlight focus-visible:outline-none',
|
|
161
|
+
file.id === activeFile?.id ? 'text-bright' : 'text-muted'
|
|
162
|
+
)}
|
|
163
|
+
aria-pressed={file.id === activeFile?.id}
|
|
164
|
+
onclick={() => chooseFile(file)}
|
|
165
|
+
>
|
|
166
|
+
<span class="max-w-[calc(var(--era-h-control)*8)] truncate font-mono">{file.path}</span>
|
|
167
|
+
{#if file.status}<span class="text-muted">{statusLabels[file.status]}</span>{/if}
|
|
168
|
+
</button>
|
|
169
|
+
{/each}
|
|
170
|
+
</div>
|
|
171
|
+
{:else if activeFile}
|
|
172
|
+
<div
|
|
173
|
+
class="flex h-control shrink-0 items-center gap-gutter border-b border-divider-faded px-inset-control"
|
|
174
|
+
>
|
|
175
|
+
<span class="min-w-0 flex-1 truncate font-mono text-bright">{activeFile.path}</span>
|
|
176
|
+
{#if activeFile.status}<span class="text-muted">{statusLabels[activeFile.status]}</span>{/if}
|
|
177
|
+
</div>
|
|
178
|
+
{/if}
|
|
179
|
+
|
|
180
|
+
<div
|
|
181
|
+
bind:this={viewport}
|
|
182
|
+
role="grid"
|
|
183
|
+
tabindex="0"
|
|
184
|
+
aria-label={label}
|
|
185
|
+
aria-rowcount={rows.length}
|
|
186
|
+
aria-colcount="3"
|
|
187
|
+
aria-activedescendant={effectiveActiveId ? rowDomId(effectiveActiveId) : undefined}
|
|
188
|
+
class="h-[calc(var(--era-h-control)*12)] min-h-0 overflow-auto overscroll-contain font-mono outline-none focus-visible:bg-highlight"
|
|
189
|
+
onscroll={(event) => (scrollTop = event.currentTarget.scrollTop)}
|
|
190
|
+
onkeydown={moveActive}
|
|
191
|
+
>
|
|
192
|
+
{#if topSpace > 0}<div aria-hidden="true" style:height={`${topSpace}px`}></div>{/if}
|
|
193
|
+
{#each visibleRows as row, localIndex (row.id)}
|
|
194
|
+
{@const absoluteIndex = startIndex + localIndex}
|
|
195
|
+
<div
|
|
196
|
+
id={rowDomId(row.id)}
|
|
197
|
+
role="row"
|
|
198
|
+
tabindex="-1"
|
|
199
|
+
aria-rowindex={absoluteIndex + 1}
|
|
200
|
+
aria-selected={row.id === effectiveActiveId}
|
|
201
|
+
data-kind={row.type === 'line' ? row.line.kind : 'hunk'}
|
|
202
|
+
class={cn(
|
|
203
|
+
'grid h-control grid-cols-[var(--era-h-control)_var(--era-h-control)_minmax(0,1fr)] items-center border-b border-divider-faded',
|
|
204
|
+
row.id === effectiveActiveId && 'bg-highlight'
|
|
205
|
+
)}
|
|
206
|
+
onclick={() => chooseRow(row)}
|
|
207
|
+
onkeydown={(event) => activateRow(event, row)}
|
|
208
|
+
>
|
|
209
|
+
{#if row.type === 'hunk'}
|
|
210
|
+
<div
|
|
211
|
+
role="gridcell"
|
|
212
|
+
aria-colspan="3"
|
|
213
|
+
class="col-span-3 truncate px-inset-control text-muted"
|
|
214
|
+
>
|
|
215
|
+
{row.header}
|
|
216
|
+
</div>
|
|
217
|
+
{:else}
|
|
218
|
+
<div role="gridcell" aria-label="Old line" class="px-gutter text-right text-muted">
|
|
219
|
+
{row.line.oldLineNumber ?? ''}
|
|
220
|
+
</div>
|
|
221
|
+
<div role="gridcell" aria-label="New line" class="px-gutter text-right text-muted">
|
|
222
|
+
{row.line.newLineNumber ?? ''}
|
|
223
|
+
</div>
|
|
224
|
+
<div
|
|
225
|
+
role="gridcell"
|
|
226
|
+
aria-label={`${row.line.kind}: ${row.line.text}`}
|
|
227
|
+
class={cn(
|
|
228
|
+
'min-w-0 overflow-hidden px-gutter whitespace-pre',
|
|
229
|
+
row.line.kind === 'addition' && 'bg-success/10 text-success',
|
|
230
|
+
row.line.kind === 'deletion' && 'bg-destructive/10 text-destructive'
|
|
231
|
+
)}
|
|
232
|
+
>
|
|
233
|
+
<span aria-hidden="true">{marker(row.line.kind)}</span>{row.line.text}
|
|
234
|
+
</div>
|
|
235
|
+
{/if}
|
|
236
|
+
</div>
|
|
237
|
+
{/each}
|
|
238
|
+
{#if bottomSpace > 0}<div aria-hidden="true" style:height={`${bottomSpace}px`}></div>{/if}
|
|
239
|
+
{#if !rows.length}
|
|
240
|
+
<p class="flex h-control items-center px-inset-control text-muted">No changes</p>
|
|
241
|
+
{/if}
|
|
242
|
+
</div>
|
|
243
|
+
</div>
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { HTMLAttributes } from 'svelte/elements';
|
|
2
|
+
import type { FileDiffFile, FileDiffRow } from './types.js';
|
|
3
|
+
interface Props extends HTMLAttributes<HTMLDivElement> {
|
|
4
|
+
ref?: HTMLDivElement | null;
|
|
5
|
+
files?: readonly FileDiffFile[];
|
|
6
|
+
selectedFileId?: string | null;
|
|
7
|
+
activeRowId?: string | null;
|
|
8
|
+
label?: string;
|
|
9
|
+
virtualize?: boolean;
|
|
10
|
+
overscan?: number;
|
|
11
|
+
onselectedfilechange?: (file: FileDiffFile) => void;
|
|
12
|
+
onactiverowchange?: (row: FileDiffRow) => void;
|
|
13
|
+
}
|
|
14
|
+
declare const FileDiff: import("svelte").Component<Props, {}, "ref" | "selectedFileId" | "activeRowId">;
|
|
15
|
+
type FileDiff = ReturnType<typeof FileDiff>;
|
|
16
|
+
export default FileDiff;
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { FileDiff } from './index.js';
|
|
3
|
+
import type { FileDiffFile } from './types.js';
|
|
4
|
+
|
|
5
|
+
const files: FileDiffFile[] = [
|
|
6
|
+
{
|
|
7
|
+
id: 'runtime',
|
|
8
|
+
path: 'src/runtime.ts',
|
|
9
|
+
status: 'modified',
|
|
10
|
+
hunks: [
|
|
11
|
+
{
|
|
12
|
+
id: 'imports',
|
|
13
|
+
header: '@@ -1,3 +1,4 @@',
|
|
14
|
+
lines: [
|
|
15
|
+
{
|
|
16
|
+
id: '1',
|
|
17
|
+
kind: 'context',
|
|
18
|
+
oldLineNumber: 1,
|
|
19
|
+
newLineNumber: 1,
|
|
20
|
+
text: "import { run } from './run';"
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
id: '2',
|
|
24
|
+
kind: 'addition',
|
|
25
|
+
oldLineNumber: null,
|
|
26
|
+
newLineNumber: 2,
|
|
27
|
+
text: "import { resume } from './resume';"
|
|
28
|
+
},
|
|
29
|
+
{ id: '3', kind: 'context', oldLineNumber: 2, newLineNumber: 3, text: '' }
|
|
30
|
+
]
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
id: 'body',
|
|
34
|
+
header: '@@ -14,2 +15,2 @@ export async function execute()',
|
|
35
|
+
lines: Array.from({ length: 80 }, (_, index) => ({
|
|
36
|
+
id: `body-${index}`,
|
|
37
|
+
kind:
|
|
38
|
+
index % 9 === 0
|
|
39
|
+
? ('deletion' as const)
|
|
40
|
+
: index % 7 === 0
|
|
41
|
+
? ('addition' as const)
|
|
42
|
+
: ('context' as const),
|
|
43
|
+
oldLineNumber: index % 7 === 0 ? null : index + 14,
|
|
44
|
+
newLineNumber: index % 9 === 0 ? null : index + 15,
|
|
45
|
+
text: `const event${index} = reduce(chunks[${index}]);`
|
|
46
|
+
}))
|
|
47
|
+
}
|
|
48
|
+
]
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
id: 'cursor',
|
|
52
|
+
path: 'src/cursor.ts',
|
|
53
|
+
status: 'added',
|
|
54
|
+
hunks: [
|
|
55
|
+
{
|
|
56
|
+
id: 'cursor-body',
|
|
57
|
+
header: '@@ -0,0 +1,2 @@',
|
|
58
|
+
lines: [
|
|
59
|
+
{ id: '1', kind: 'addition', newLineNumber: 1, text: 'export type Cursor = string;' },
|
|
60
|
+
{ id: '2', kind: 'addition', newLineNumber: 2, text: 'export const initial = "";' }
|
|
61
|
+
]
|
|
62
|
+
}
|
|
63
|
+
]
|
|
64
|
+
}
|
|
65
|
+
];
|
|
66
|
+
|
|
67
|
+
let selectedFileId = $state<string | null>('runtime');
|
|
68
|
+
let activeRowId = $state<string | null>(null);
|
|
69
|
+
</script>
|
|
70
|
+
|
|
71
|
+
<div class="p-card">
|
|
72
|
+
<FileDiff {files} bind:selectedFileId bind:activeRowId label="Runtime changes" />
|
|
73
|
+
<p class="mt-gutter text-body text-muted">
|
|
74
|
+
Selected: {selectedFileId}; row: {activeRowId ?? 'none'}
|
|
75
|
+
</p>
|
|
76
|
+
</div>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default as FileDiff } from './file-diff.svelte';
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
export type FileDiffStatus = 'added' | 'modified' | 'deleted' | 'renamed';
|
|
2
|
+
export type FileDiffLineKind = 'context' | 'addition' | 'deletion';
|
|
3
|
+
/** One source line. Text excludes the visual `+`, `-`, or space marker. */
|
|
4
|
+
export interface FileDiffLine {
|
|
5
|
+
id: string;
|
|
6
|
+
kind: FileDiffLineKind;
|
|
7
|
+
oldLineNumber?: number | null;
|
|
8
|
+
newLineNumber?: number | null;
|
|
9
|
+
text: string;
|
|
10
|
+
}
|
|
11
|
+
export interface FileDiffHunk {
|
|
12
|
+
id: string;
|
|
13
|
+
header: string;
|
|
14
|
+
lines: readonly FileDiffLine[];
|
|
15
|
+
}
|
|
16
|
+
export interface FileDiffFile {
|
|
17
|
+
id: string;
|
|
18
|
+
path: string;
|
|
19
|
+
oldPath?: string;
|
|
20
|
+
status?: FileDiffStatus;
|
|
21
|
+
hunks: readonly FileDiffHunk[];
|
|
22
|
+
}
|
|
23
|
+
export type FileDiffRow = {
|
|
24
|
+
id: string;
|
|
25
|
+
type: 'hunk';
|
|
26
|
+
header: string;
|
|
27
|
+
} | {
|
|
28
|
+
id: string;
|
|
29
|
+
type: 'line';
|
|
30
|
+
line: FileDiffLine;
|
|
31
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/ui/tabs/tabs.svelte
CHANGED
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import { Tabs } from 'bits-ui';
|
|
3
3
|
|
|
4
|
-
let {
|
|
4
|
+
let {
|
|
5
|
+
ref = $bindable(null),
|
|
6
|
+
value = $bindable(),
|
|
7
|
+
class: className,
|
|
8
|
+
...restProps
|
|
9
|
+
}: Tabs.RootProps = $props();
|
|
5
10
|
</script>
|
|
6
11
|
|
|
7
|
-
<Tabs.Root bind:ref class={className} {...restProps} />
|
|
12
|
+
<Tabs.Root bind:ref bind:value class={className} {...restProps} />
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { Terminal, type TerminalRenderer } from './index.js';
|
|
3
|
+
|
|
4
|
+
const encoder = new TextEncoder();
|
|
5
|
+
let inputBytes = $state<number[]>([]);
|
|
6
|
+
|
|
7
|
+
const renderer: TerminalRenderer = {
|
|
8
|
+
attach(host, options) {
|
|
9
|
+
const decoder = new TextDecoder();
|
|
10
|
+
const output = document.createElement('pre');
|
|
11
|
+
output.dataset.terminalOutput = '';
|
|
12
|
+
output.className = 'min-h-bar overflow-auto p-panel leading-body whitespace-pre-wrap';
|
|
13
|
+
|
|
14
|
+
const input = document.createElement('textarea');
|
|
15
|
+
input.dataset.terminalInput = '';
|
|
16
|
+
input.setAttribute('aria-label', `${options.label} input`);
|
|
17
|
+
input.className = 'w-full resize-none bg-field p-inset-control text-fg shadow-well';
|
|
18
|
+
input.readOnly = options.readOnly;
|
|
19
|
+
input.addEventListener('input', () => {
|
|
20
|
+
options.onInput(encoder.encode(input.value));
|
|
21
|
+
input.value = '';
|
|
22
|
+
});
|
|
23
|
+
host.append(output, input);
|
|
24
|
+
|
|
25
|
+
return {
|
|
26
|
+
write(data) {
|
|
27
|
+
output.textContent += decoder.decode(data, { stream: true });
|
|
28
|
+
},
|
|
29
|
+
focus() {
|
|
30
|
+
input.focus();
|
|
31
|
+
},
|
|
32
|
+
dispose() {
|
|
33
|
+
output.remove();
|
|
34
|
+
input.remove();
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
const chunks = [
|
|
41
|
+
{ key: 1, data: encoder.encode('$ printf "hello\\n"\r\n') },
|
|
42
|
+
{ key: 2, data: new Uint8Array([104, 101, 108, 108, 111, 13, 10]) }
|
|
43
|
+
];
|
|
44
|
+
</script>
|
|
45
|
+
|
|
46
|
+
<div class="flex min-h-48 flex-col gap-gutter">
|
|
47
|
+
<Terminal
|
|
48
|
+
{renderer}
|
|
49
|
+
{chunks}
|
|
50
|
+
label="Investigation terminal"
|
|
51
|
+
oninputbytes={(data) => {
|
|
52
|
+
inputBytes = [...data];
|
|
53
|
+
}}
|
|
54
|
+
class="min-h-40"
|
|
55
|
+
/>
|
|
56
|
+
<output data-input-bytes class="font-mono text-body text-muted">{inputBytes.join(',')}</output>
|
|
57
|
+
</div>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default as Terminal } from './terminal.svelte';
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { HTMLAttributes } from 'svelte/elements';
|
|
3
|
+
import { SvelteSet } from 'svelte/reactivity';
|
|
4
|
+
import { cn } from '../../utils/index.js';
|
|
5
|
+
import type { TerminalOutputChunk, TerminalRenderer, TerminalSession } from './types.js';
|
|
6
|
+
|
|
7
|
+
interface Props extends HTMLAttributes<HTMLDivElement> {
|
|
8
|
+
ref?: HTMLDivElement | null;
|
|
9
|
+
/** Renderer adapter (for example an xterm.js adapter). Era owns no transport. */
|
|
10
|
+
renderer: TerminalRenderer;
|
|
11
|
+
/** Immutable, stably keyed PTY output chunks. Bytes are passed through unchanged. */
|
|
12
|
+
chunks?: readonly TerminalOutputChunk[];
|
|
13
|
+
/** The attached renderer session, exposed for focus/clear/imperative writes. */
|
|
14
|
+
session?: TerminalSession | null;
|
|
15
|
+
label?: string;
|
|
16
|
+
readOnly?: boolean;
|
|
17
|
+
oninputbytes?: (data: Uint8Array) => void | Promise<void>;
|
|
18
|
+
onterminalresize?: (columns: number, rows: number) => void | Promise<void>;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
let {
|
|
22
|
+
ref = $bindable(null),
|
|
23
|
+
renderer,
|
|
24
|
+
chunks = [],
|
|
25
|
+
session = $bindable(null),
|
|
26
|
+
label = 'Terminal',
|
|
27
|
+
readOnly = false,
|
|
28
|
+
oninputbytes,
|
|
29
|
+
onterminalresize,
|
|
30
|
+
class: className,
|
|
31
|
+
...restProps
|
|
32
|
+
}: Props = $props();
|
|
33
|
+
|
|
34
|
+
let writtenSession: TerminalSession | null = null;
|
|
35
|
+
const written = new SvelteSet<string | number>();
|
|
36
|
+
|
|
37
|
+
// Renderer attachment is the only side effect. The renderer owns its DOM and
|
|
38
|
+
// cleanup; this component owns lifecycle, byte plumbing, semantics, and axes.
|
|
39
|
+
$effect(() => {
|
|
40
|
+
const host = ref;
|
|
41
|
+
if (!host) return;
|
|
42
|
+
|
|
43
|
+
const attached = renderer.attach(host, {
|
|
44
|
+
label,
|
|
45
|
+
readOnly,
|
|
46
|
+
onInput(data) {
|
|
47
|
+
void oninputbytes?.(data);
|
|
48
|
+
},
|
|
49
|
+
onResize(columns, rows) {
|
|
50
|
+
void onterminalresize?.(columns, rows);
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
session = attached;
|
|
54
|
+
|
|
55
|
+
return () => {
|
|
56
|
+
if (session === attached) session = null;
|
|
57
|
+
attached.dispose();
|
|
58
|
+
};
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
// Each stable chunk is reduced once per renderer session. Uint8Array goes
|
|
62
|
+
// straight to the adapter: no decode/re-encode boundary can corrupt PTY bytes.
|
|
63
|
+
$effect(() => {
|
|
64
|
+
const attached = session;
|
|
65
|
+
const output = chunks;
|
|
66
|
+
if (!attached) return;
|
|
67
|
+
|
|
68
|
+
if (writtenSession !== attached) {
|
|
69
|
+
writtenSession = attached;
|
|
70
|
+
written.clear();
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
for (const chunk of output) {
|
|
74
|
+
if (written.has(chunk.key)) continue;
|
|
75
|
+
written.add(chunk.key);
|
|
76
|
+
attached.write(chunk.data);
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
</script>
|
|
80
|
+
|
|
81
|
+
<div
|
|
82
|
+
bind:this={ref}
|
|
83
|
+
role="region"
|
|
84
|
+
aria-label={label}
|
|
85
|
+
data-readonly={readOnly || undefined}
|
|
86
|
+
class={cn(
|
|
87
|
+
'relative min-h-control min-w-0 overflow-hidden rounded-bar bg-field font-mono text-body text-fg shadow-well focus-within:bg-highlight focus-within:outline-none',
|
|
88
|
+
className
|
|
89
|
+
)}
|
|
90
|
+
{...restProps}
|
|
91
|
+
></div>
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { HTMLAttributes } from 'svelte/elements';
|
|
2
|
+
import type { TerminalOutputChunk, TerminalRenderer, TerminalSession } from './types.js';
|
|
3
|
+
interface Props extends HTMLAttributes<HTMLDivElement> {
|
|
4
|
+
ref?: HTMLDivElement | null;
|
|
5
|
+
/** Renderer adapter (for example an xterm.js adapter). Era owns no transport. */
|
|
6
|
+
renderer: TerminalRenderer;
|
|
7
|
+
/** Immutable, stably keyed PTY output chunks. Bytes are passed through unchanged. */
|
|
8
|
+
chunks?: readonly TerminalOutputChunk[];
|
|
9
|
+
/** The attached renderer session, exposed for focus/clear/imperative writes. */
|
|
10
|
+
session?: TerminalSession | null;
|
|
11
|
+
label?: string;
|
|
12
|
+
readOnly?: boolean;
|
|
13
|
+
oninputbytes?: (data: Uint8Array) => void | Promise<void>;
|
|
14
|
+
onterminalresize?: (columns: number, rows: number) => void | Promise<void>;
|
|
15
|
+
}
|
|
16
|
+
declare const Terminal: import("svelte").Component<Props, {}, "ref" | "session">;
|
|
17
|
+
type Terminal = ReturnType<typeof Terminal>;
|
|
18
|
+
export default Terminal;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/** A stable output record. Reusing a key means the bytes were already rendered. */
|
|
2
|
+
export interface TerminalOutputChunk {
|
|
3
|
+
key: string | number;
|
|
4
|
+
data: Uint8Array;
|
|
5
|
+
}
|
|
6
|
+
/** Callbacks a renderer uses to return raw PTY input and geometry. */
|
|
7
|
+
export interface TerminalRendererOptions {
|
|
8
|
+
label: string;
|
|
9
|
+
readOnly: boolean;
|
|
10
|
+
onInput(data: Uint8Array): void;
|
|
11
|
+
onResize(columns: number, rows: number): void;
|
|
12
|
+
}
|
|
13
|
+
/** Attached renderer surface. `write` must consume the supplied bytes as-is. */
|
|
14
|
+
export interface TerminalSession {
|
|
15
|
+
write(data: Uint8Array): void;
|
|
16
|
+
focus?(): void;
|
|
17
|
+
clear?(): void;
|
|
18
|
+
dispose(): void;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* UI-renderer seam, deliberately independent of WebSocket, SSH, MCP, or any
|
|
22
|
+
* other backend. Consumers can adapt xterm.js or another terminal engine.
|
|
23
|
+
*/
|
|
24
|
+
export interface TerminalRenderer {
|
|
25
|
+
attach(host: HTMLElement, options: TerminalRendererOptions): TerminalSession;
|
|
26
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@signal9/era-ui",
|
|
3
|
-
"version": "34.
|
|
3
|
+
"version": "34.4.0",
|
|
4
4
|
"packageManager": "npm@11.19.0",
|
|
5
5
|
"engines": {
|
|
6
6
|
"node": ">=24 <27",
|
|
@@ -234,6 +234,11 @@
|
|
|
234
234
|
"svelte": "./dist/ui/dropdown-menu/index.js",
|
|
235
235
|
"default": "./dist/ui/dropdown-menu/index.js"
|
|
236
236
|
},
|
|
237
|
+
"./file-diff": {
|
|
238
|
+
"types": "./dist/ui/file-diff/index.d.ts",
|
|
239
|
+
"svelte": "./dist/ui/file-diff/index.js",
|
|
240
|
+
"default": "./dist/ui/file-diff/index.js"
|
|
241
|
+
},
|
|
237
242
|
"./file-upload": {
|
|
238
243
|
"types": "./dist/ui/file-upload/index.d.ts",
|
|
239
244
|
"svelte": "./dist/ui/file-upload/index.js",
|
|
@@ -389,6 +394,11 @@
|
|
|
389
394
|
"svelte": "./dist/ui/tabs/index.js",
|
|
390
395
|
"default": "./dist/ui/tabs/index.js"
|
|
391
396
|
},
|
|
397
|
+
"./terminal": {
|
|
398
|
+
"types": "./dist/ui/terminal/index.d.ts",
|
|
399
|
+
"svelte": "./dist/ui/terminal/index.js",
|
|
400
|
+
"default": "./dist/ui/terminal/index.js"
|
|
401
|
+
},
|
|
392
402
|
"./time-field": {
|
|
393
403
|
"types": "./dist/ui/time-field/index.d.ts",
|
|
394
404
|
"svelte": "./dist/ui/time-field/index.js",
|
|
@@ -454,6 +464,11 @@
|
|
|
454
464
|
"svelte": "./dist/ai/chain-of-thought/index.js",
|
|
455
465
|
"default": "./dist/ai/chain-of-thought/index.js"
|
|
456
466
|
},
|
|
467
|
+
"./ai/code-mode": {
|
|
468
|
+
"types": "./dist/ai/code-mode/index.d.ts",
|
|
469
|
+
"svelte": "./dist/ai/code-mode/index.js",
|
|
470
|
+
"default": "./dist/ai/code-mode/index.js"
|
|
471
|
+
},
|
|
457
472
|
"./ai/confirmation": {
|
|
458
473
|
"types": "./dist/ai/confirmation/index.d.ts",
|
|
459
474
|
"svelte": "./dist/ai/confirmation/index.js",
|
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# Era UI v34 release history
|
|
2
2
|
|
|
3
|
+
## [34.4.0](https://github.com/sig-nine/era-ui/compare/v34.3.0...v34.4.0) (2026-08-25)
|
|
4
|
+
|
|
5
|
+
### Features
|
|
6
|
+
|
|
7
|
+
* add Navi execution primitives ([540c97b](https://github.com/sig-nine/era-ui/commit/540c97bc0a4f45b106dd7bb4228810d2f17c9db6))
|
|
8
|
+
|
|
9
|
+
## [34.3.0](https://github.com/sig-nine/era-ui/compare/v34.2.9...v34.3.0) (2026-08-25)
|
|
10
|
+
|
|
11
|
+
### Features
|
|
12
|
+
|
|
13
|
+
* **ai:** generalize durable provider primitives ([808f9e3](https://github.com/sig-nine/era-ui/commit/808f9e3ff5d0446cc80d9e020c9fdf1259ce934e))
|
|
14
|
+
|
|
3
15
|
## [34.2.9](https://github.com/sig-nine/era-ui/compare/v34.2.8...v34.2.9) (2026-08-25)
|
|
4
16
|
|
|
5
17
|
### Bug Fixes
|