@ouraihub/aether-ui 0.0.0-stage → 0.1.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/LICENSE +789 -0
- package/fixtures/model-classify.json +42 -0
- package/fixtures/pricing-cost.json +56 -0
- package/fixtures/reasoning-infer.json +44 -0
- package/fixtures/sse/anthropic-messages/chinese.sse +24 -0
- package/fixtures/sse/anthropic-messages/multi-tool.sse +33 -0
- package/fixtures/sse/anthropic-messages/single-tool.sse +39 -0
- package/fixtures/sse/anthropic-messages/text.sse +27 -0
- package/fixtures/sse/anthropic-messages/truncated.sse +21 -0
- package/fixtures/sse/anthropic-messages/upstream-error.sse +12 -0
- package/fixtures/sse/expect.json +20 -0
- package/fixtures/sse/openai-chat/chinese.sse +14 -0
- package/fixtures/sse/openai-chat/multi-tool.sse +18 -0
- package/fixtures/sse/openai-chat/single-tool.sse +22 -0
- package/fixtures/sse/openai-chat/text.sse +14 -0
- package/fixtures/sse/openai-chat/truncated.sse +12 -0
- package/fixtures/sse/openai-chat/upstream-error.sse +6 -0
- package/fixtures/sse/openai-responses/chinese.sse +33 -0
- package/fixtures/sse/openai-responses/multi-tool.sse +36 -0
- package/fixtures/sse/openai-responses/single-tool.sse +27 -0
- package/fixtures/sse/openai-responses/text.sse +33 -0
- package/fixtures/sse/openai-responses/truncated.sse +30 -0
- package/fixtures/sse/openai-responses/upstream-error.sse +9 -0
- package/package.json +71 -3
- package/src/CopyInput.svelte +33 -0
- package/src/KitNotifier.svelte +33 -0
- package/src/chat/ApprovalCard.svelte +23 -0
- package/src/chat/ChatPanel.svelte +2097 -0
- package/src/chat/MessageItem.svelte +583 -0
- package/src/chat/SkillPicker.svelte +126 -0
- package/src/chat/ToolCallItem.svelte +23 -0
- package/src/chat/UsageDetails.svelte +138 -0
- package/src/chat/index.ts +2 -0
- package/src/chat/run-session.svelte.ts +556 -0
- package/src/components/ui/alert/alert-action.svelte +20 -0
- package/src/components/ui/alert/alert-description.svelte +23 -0
- package/src/components/ui/alert/alert-title.svelte +23 -0
- package/src/components/ui/alert/alert.svelte +43 -0
- package/src/components/ui/alert/index.ts +17 -0
- package/src/components/ui/alert-dialog/alert-dialog-action.svelte +27 -0
- package/src/components/ui/alert-dialog/alert-dialog-cancel.svelte +27 -0
- package/src/components/ui/alert-dialog/alert-dialog-content.svelte +32 -0
- package/src/components/ui/alert-dialog/alert-dialog-description.svelte +17 -0
- package/src/components/ui/alert-dialog/alert-dialog-footer.svelte +23 -0
- package/src/components/ui/alert-dialog/alert-dialog-header.svelte +20 -0
- package/src/components/ui/alert-dialog/alert-dialog-media.svelte +20 -0
- package/src/components/ui/alert-dialog/alert-dialog-overlay.svelte +17 -0
- package/src/components/ui/alert-dialog/alert-dialog-portal.svelte +7 -0
- package/src/components/ui/alert-dialog/alert-dialog-title.svelte +17 -0
- package/src/components/ui/alert-dialog/alert-dialog-trigger.svelte +7 -0
- package/src/components/ui/alert-dialog/alert-dialog.svelte +7 -0
- package/src/components/ui/alert-dialog/index.ts +40 -0
- package/src/components/ui/button/button.svelte +81 -0
- package/src/components/ui/button/index.ts +17 -0
- package/src/components/ui/dialog/dialog-close.svelte +11 -0
- package/src/components/ui/dialog/dialog-content.svelte +48 -0
- package/src/components/ui/dialog/dialog-description.svelte +17 -0
- package/src/components/ui/dialog/dialog-footer.svelte +32 -0
- package/src/components/ui/dialog/dialog-header.svelte +20 -0
- package/src/components/ui/dialog/dialog-overlay.svelte +17 -0
- package/src/components/ui/dialog/dialog-portal.svelte +7 -0
- package/src/components/ui/dialog/dialog-title.svelte +17 -0
- package/src/components/ui/dialog/dialog-trigger.svelte +11 -0
- package/src/components/ui/dialog/dialog.svelte +7 -0
- package/src/components/ui/dialog/index.ts +34 -0
- package/src/components/ui/dropdown-menu/dropdown-menu-checkbox-group.svelte +16 -0
- package/src/components/ui/dropdown-menu/dropdown-menu-checkbox-item.svelte +46 -0
- package/src/components/ui/dropdown-menu/dropdown-menu-content.svelte +31 -0
- package/src/components/ui/dropdown-menu/dropdown-menu-group-heading.svelte +22 -0
- package/src/components/ui/dropdown-menu/dropdown-menu-group.svelte +7 -0
- package/src/components/ui/dropdown-menu/dropdown-menu-item.svelte +27 -0
- package/src/components/ui/dropdown-menu/dropdown-menu-label.svelte +24 -0
- package/src/components/ui/dropdown-menu/dropdown-menu-portal.svelte +7 -0
- package/src/components/ui/dropdown-menu/dropdown-menu-radio-group.svelte +17 -0
- package/src/components/ui/dropdown-menu/dropdown-menu-radio-item.svelte +36 -0
- package/src/components/ui/dropdown-menu/dropdown-menu-separator.svelte +17 -0
- package/src/components/ui/dropdown-menu/dropdown-menu-shortcut.svelte +20 -0
- package/src/components/ui/dropdown-menu/dropdown-menu-sub-content.svelte +31 -0
- package/src/components/ui/dropdown-menu/dropdown-menu-sub-trigger.svelte +29 -0
- package/src/components/ui/dropdown-menu/dropdown-menu-sub.svelte +7 -0
- package/src/components/ui/dropdown-menu/dropdown-menu-trigger.svelte +7 -0
- package/src/components/ui/dropdown-menu/dropdown-menu.svelte +7 -0
- package/src/components/ui/dropdown-menu/index.ts +54 -0
- package/src/components/ui/empty/empty-content.svelte +23 -0
- package/src/components/ui/empty/empty-description.svelte +23 -0
- package/src/components/ui/empty/empty-header.svelte +20 -0
- package/src/components/ui/empty/empty-media.svelte +41 -0
- package/src/components/ui/empty/empty-title.svelte +20 -0
- package/src/components/ui/empty/empty.svelte +23 -0
- package/src/components/ui/empty/index.ts +22 -0
- package/src/components/ui/field/field-content.svelte +20 -0
- package/src/components/ui/field/field-description.svelte +25 -0
- package/src/components/ui/field/field-error.svelte +58 -0
- package/src/components/ui/field/field-group.svelte +23 -0
- package/src/components/ui/field/field-label.svelte +25 -0
- package/src/components/ui/field/field-legend.svelte +24 -0
- package/src/components/ui/field/field-separator.svelte +35 -0
- package/src/components/ui/field/field-set.svelte +20 -0
- package/src/components/ui/field/field-title.svelte +20 -0
- package/src/components/ui/field/field.svelte +47 -0
- package/src/components/ui/field/index.ts +33 -0
- package/src/components/ui/input/index.ts +7 -0
- package/src/components/ui/input/input.svelte +48 -0
- package/src/components/ui/label/index.ts +7 -0
- package/src/components/ui/label/label.svelte +20 -0
- package/src/components/ui/select/index.ts +40 -0
- package/src/components/ui/select/select-content.svelte +45 -0
- package/src/components/ui/select/select-group-heading.svelte +21 -0
- package/src/components/ui/select/select-group.svelte +17 -0
- package/src/components/ui/select/select-item.svelte +43 -0
- package/src/components/ui/select/select-label.svelte +20 -0
- package/src/components/ui/select/select-portal.svelte +7 -0
- package/src/components/ui/select/select-scroll-down-button.svelte +20 -0
- package/src/components/ui/select/select-scroll-up-button.svelte +20 -0
- package/src/components/ui/select/select-separator.svelte +18 -0
- package/src/components/ui/select/select-trigger.svelte +29 -0
- package/src/components/ui/select/select-value.svelte +7 -0
- package/src/components/ui/select/select.svelte +11 -0
- package/src/components/ui/separator/index.ts +7 -0
- package/src/components/ui/separator/separator.svelte +21 -0
- package/src/components/ui/sonner/index.ts +1 -0
- package/src/components/ui/sonner/sonner.svelte +40 -0
- package/src/components/ui/switch/index.ts +7 -0
- package/src/components/ui/switch/switch.svelte +34 -0
- package/src/components/ui/textarea/index.ts +7 -0
- package/src/components/ui/textarea/textarea.svelte +22 -0
- package/src/context.ts +34 -0
- package/src/dev/Demo.svelte +45 -0
- package/src/dev/main.ts +7 -0
- package/src/i18n/en.ts +617 -0
- package/src/i18n/index.ts +23 -0
- package/src/i18n/zh-CN.ts +599 -0
- package/src/index.ts +10 -0
- package/src/logs/LogPanel.svelte +97 -0
- package/src/logs/LogSettings.svelte +56 -0
- package/src/logs/index.ts +5 -0
- package/src/logs/logger.ts +96 -0
- package/src/markdown.ts +8 -0
- package/src/mcp/McpClientSettings.svelte +491 -0
- package/src/mcp/McpServerSettings.svelte +327 -0
- package/src/mcp/config.ts +26 -0
- package/src/mcp/index.ts +3 -0
- package/src/mcp/input.ts +73 -0
- package/src/notify.svelte.ts +43 -0
- package/src/ports/contract.ts +165 -0
- package/src/ports/fake.ts +484 -0
- package/src/ports/index.ts +7 -0
- package/src/ports/search.ts +47 -0
- package/src/ports/skill-sources.ts +45 -0
- package/src/providers/EndpointKeysDialog.svelte +343 -0
- package/src/providers/EndpointPanel.svelte +491 -0
- package/src/providers/ModelPickerDialog.svelte +187 -0
- package/src/providers/ModelSelect.svelte +434 -0
- package/src/providers/ModelSettingsDialog.svelte +453 -0
- package/src/providers/ProvidersSettings.svelte +631 -0
- package/src/providers/RequestConfigDialog.svelte +250 -0
- package/src/providers/SecretInput.svelte +276 -0
- package/src/providers/draft.ts +329 -0
- package/src/providers/edit-session.ts +146 -0
- package/src/providers/index.ts +5 -0
- package/src/providers/reasoning.ts +41 -0
- package/src/providers/revision.svelte.ts +4 -0
- package/src/search/SearchSettings.svelte +703 -0
- package/src/search/index.ts +13 -0
- package/src/search/settings-flow.ts +62 -0
- package/src/session.ts +20 -0
- package/src/skills/SkillAdd.svelte +347 -0
- package/src/skills/SkillAuthoring.svelte +381 -0
- package/src/skills/SkillEditor.svelte +94 -0
- package/src/skills/SkillPreviewDialog.svelte +122 -0
- package/src/skills/SkillsSettings.svelte +140 -0
- package/src/skills/error.ts +32 -0
- package/src/skills/index.ts +4 -0
- package/src/styles/kit.css +137 -0
- package/src/styles/tokens.css +22 -0
- package/src/tauri/index.ts +282 -0
- package/src/utils.ts +22 -0
- package/src/web/index.ts +376 -0
- package/README.md +0 -3
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
// Log viewer (docs/16 §4.5): tail(300) every 2 s while the page is visible; problems-only filter; open folder.
|
|
3
|
+
import { Alert, AlertDescription } from '#lib/components/ui/alert/index.js'
|
|
4
|
+
import { Button } from '#lib/components/ui/button/index.js'
|
|
5
|
+
import * as Empty from '#lib/components/ui/empty/index.js'
|
|
6
|
+
import { Switch } from '#lib/components/ui/switch/index.js'
|
|
7
|
+
import { getKitContext } from '../context'
|
|
8
|
+
import type { LogEntry } from '../ports'
|
|
9
|
+
import { cn } from '../utils'
|
|
10
|
+
|
|
11
|
+
const POLL_MS = 2000
|
|
12
|
+
const LIMIT = 300
|
|
13
|
+
|
|
14
|
+
const { ports, i18n, notify } = getKitContext()
|
|
15
|
+
const t = i18n.t
|
|
16
|
+
const logs = ports.logs
|
|
17
|
+
|
|
18
|
+
let entries = $state<LogEntry[]>([])
|
|
19
|
+
let failed = $state(false)
|
|
20
|
+
let problemsOnly = $state(false)
|
|
21
|
+
const shown = $derived(problemsOnly ? entries.filter((e) => e.level === 'ERROR' || e.level === 'WARN') : entries)
|
|
22
|
+
|
|
23
|
+
async function refresh() {
|
|
24
|
+
if (!logs) return
|
|
25
|
+
try {
|
|
26
|
+
entries = await logs.tail(LIMIT)
|
|
27
|
+
failed = false
|
|
28
|
+
} catch {
|
|
29
|
+
failed = true
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
$effect(() => {
|
|
34
|
+
if (!logs) return
|
|
35
|
+
let timer: number | undefined
|
|
36
|
+
const sync = () => {
|
|
37
|
+
window.clearInterval(timer)
|
|
38
|
+
timer = undefined
|
|
39
|
+
if (document.visibilityState !== 'visible') return
|
|
40
|
+
void refresh()
|
|
41
|
+
timer = window.setInterval(() => void refresh(), POLL_MS)
|
|
42
|
+
}
|
|
43
|
+
sync()
|
|
44
|
+
document.addEventListener('visibilitychange', sync)
|
|
45
|
+
return () => {
|
|
46
|
+
window.clearInterval(timer)
|
|
47
|
+
document.removeEventListener('visibilitychange', sync)
|
|
48
|
+
}
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
async function openDir() {
|
|
52
|
+
try {
|
|
53
|
+
await ports.desktop?.openLogsDirectory()
|
|
54
|
+
} catch {
|
|
55
|
+
notify.toast('error', t('errors.internal'))
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
</script>
|
|
59
|
+
|
|
60
|
+
{#if logs}
|
|
61
|
+
<section class="flex min-h-0 flex-1 flex-col gap-3" aria-label={t('logs.title')}>
|
|
62
|
+
<header class="flex items-center gap-3">
|
|
63
|
+
<h2 class="text-base font-medium">{t('logs.title')}</h2>
|
|
64
|
+
<label class="ml-auto flex items-center gap-2 text-sm">
|
|
65
|
+
<Switch bind:checked={problemsOnly} />
|
|
66
|
+
{t('logs.problemsOnly')}
|
|
67
|
+
</label>
|
|
68
|
+
{#if ports.desktop}
|
|
69
|
+
<Button variant="outline" size="sm" onclick={() => void openDir()}>{t('logs.openDir')}</Button>
|
|
70
|
+
{/if}
|
|
71
|
+
</header>
|
|
72
|
+
{#if failed}
|
|
73
|
+
<Alert variant="destructive"><AlertDescription>{t('logs.loadFailed')}</AlertDescription></Alert>
|
|
74
|
+
{/if}
|
|
75
|
+
{#if shown.length === 0}
|
|
76
|
+
<Empty.Root>
|
|
77
|
+
<Empty.Header><Empty.Title>{t('logs.empty')}</Empty.Title></Empty.Header>
|
|
78
|
+
</Empty.Root>
|
|
79
|
+
{:else}
|
|
80
|
+
<ol class="bg-muted min-h-0 flex-1 overflow-auto rounded-md p-2 font-mono text-xs">
|
|
81
|
+
{#each shown as entry (entry.seq)}
|
|
82
|
+
<li
|
|
83
|
+
class={cn(
|
|
84
|
+
'break-all whitespace-pre-wrap',
|
|
85
|
+
entry.level === 'ERROR' && 'text-destructive',
|
|
86
|
+
(entry.level === 'DEBUG' || entry.level === 'TRACE') && 'text-muted-foreground',
|
|
87
|
+
)}
|
|
88
|
+
>
|
|
89
|
+
{entry.time}
|
|
90
|
+
{entry.level}
|
|
91
|
+
{entry.target}: {entry.message}
|
|
92
|
+
</li>
|
|
93
|
+
{/each}
|
|
94
|
+
</ol>
|
|
95
|
+
{/if}
|
|
96
|
+
</section>
|
|
97
|
+
{/if}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
// Log level (docs/16 §4.5): applies immediately; warns when the log file cannot be written.
|
|
3
|
+
import { Alert, AlertDescription } from '#lib/components/ui/alert/index.js'
|
|
4
|
+
import * as Field from '#lib/components/ui/field/index.js'
|
|
5
|
+
import * as Select from '#lib/components/ui/select/index.js'
|
|
6
|
+
import { getKitContext } from '../context'
|
|
7
|
+
import { isKitClientError } from '../ports'
|
|
8
|
+
import type { LogLevel, LogStatus } from '../ports'
|
|
9
|
+
|
|
10
|
+
const LEVELS: readonly LogLevel[] = ['ERROR', 'WARN', 'INFO', 'DEBUG', 'TRACE']
|
|
11
|
+
|
|
12
|
+
const { ports, i18n, notify } = getKitContext()
|
|
13
|
+
const t = i18n.t
|
|
14
|
+
const logs = ports.logs
|
|
15
|
+
|
|
16
|
+
let status = $state<LogStatus | null>(null)
|
|
17
|
+
|
|
18
|
+
async function run(call: () => Promise<LogStatus>) {
|
|
19
|
+
try {
|
|
20
|
+
status = await call()
|
|
21
|
+
} catch (error) {
|
|
22
|
+
notify.toast('error', t(`errors.${isKitClientError(error) ? error.code : 'internal'}`))
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
$effect(() => {
|
|
27
|
+
if (logs) void run(() => logs.getLevel())
|
|
28
|
+
})
|
|
29
|
+
</script>
|
|
30
|
+
|
|
31
|
+
{#if logs}
|
|
32
|
+
<Field.Group>
|
|
33
|
+
<Field.Field>
|
|
34
|
+
<Field.Label for="kit-log-level">{t('logs.level')}</Field.Label>
|
|
35
|
+
<Select.Root
|
|
36
|
+
type="single"
|
|
37
|
+
value={status?.level ?? ''}
|
|
38
|
+
disabled={!status}
|
|
39
|
+
onValueChange={(value) => void run(() => logs.setLevel(value as LogLevel))}
|
|
40
|
+
>
|
|
41
|
+
<Select.Trigger id="kit-log-level" class="w-40">{status?.level ?? '—'}</Select.Trigger>
|
|
42
|
+
<Select.Content>
|
|
43
|
+
<Select.Group>
|
|
44
|
+
{#each LEVELS as level (level)}
|
|
45
|
+
<Select.Item value={level} label={level} />
|
|
46
|
+
{/each}
|
|
47
|
+
</Select.Group>
|
|
48
|
+
</Select.Content>
|
|
49
|
+
</Select.Root>
|
|
50
|
+
<Field.Description>{t('logs.levelHint')}</Field.Description>
|
|
51
|
+
</Field.Field>
|
|
52
|
+
{#if status && !status.available}
|
|
53
|
+
<Alert variant="destructive"><AlertDescription>{t('logs.unavailable')}</AlertDescription></Alert>
|
|
54
|
+
{/if}
|
|
55
|
+
</Field.Group>
|
|
56
|
+
{/if}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
// createLogger (docs/16 §4.5): same whitelist as the backend (docs/10 §10), batched into LogsPort.ingest.
|
|
2
|
+
// ponytail: secret detection stays in the backend (it rejects and counts); mirror mask.rs here
|
|
3
|
+
// if a secret-looking value must never leave the page.
|
|
4
|
+
import type { KitLogger, LogFields } from '../context'
|
|
5
|
+
import type { IngestEntry, IngestLevel, LogsPort } from '../ports'
|
|
6
|
+
|
|
7
|
+
const MAX_QUEUE = 200
|
|
8
|
+
const BATCH = 20
|
|
9
|
+
const MAX_BATCH = 50
|
|
10
|
+
const DELAY_MS = 250
|
|
11
|
+
const MAX_FIELDS = 16
|
|
12
|
+
const MAX_STRING = 200
|
|
13
|
+
const EVENT = /^[a-z][a-z0-9_]*(\.[a-z0-9_]+){1,5}$/
|
|
14
|
+
const KEY = /^[a-z][a-z0-9_]*(\.[a-z0-9_]+){0,5}$/
|
|
15
|
+
|
|
16
|
+
export interface BatchingLogger extends KitLogger {
|
|
17
|
+
/** Sends everything queued now (also done on `pagehide`). */
|
|
18
|
+
flush(): Promise<void>
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function isValid(event: string, fields: LogFields | undefined): boolean {
|
|
22
|
+
if (!EVENT.test(event)) return false
|
|
23
|
+
const entries = Object.entries(fields ?? {})
|
|
24
|
+
return (
|
|
25
|
+
entries.length <= MAX_FIELDS &&
|
|
26
|
+
entries.every(([key, value]) => {
|
|
27
|
+
if (!KEY.test(key)) return false
|
|
28
|
+
if (typeof value === 'string') return [...value].length <= MAX_STRING
|
|
29
|
+
return typeof value === 'boolean' || Number.isFinite(value)
|
|
30
|
+
})
|
|
31
|
+
)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function createLogger(port: LogsPort): BatchingLogger {
|
|
35
|
+
const queue: IngestEntry[] = []
|
|
36
|
+
let dropped = 0
|
|
37
|
+
let timer: number | undefined
|
|
38
|
+
let inFlight: Promise<void> | null = null
|
|
39
|
+
|
|
40
|
+
// One request at a time; entries queue meanwhile, bounded by MAX_QUEUE.
|
|
41
|
+
async function drain(): Promise<void> {
|
|
42
|
+
while (queue.length > 0 || dropped > 0) {
|
|
43
|
+
const entries = queue.splice(0, MAX_BATCH)
|
|
44
|
+
const droppedCount = dropped
|
|
45
|
+
dropped = 0
|
|
46
|
+
try {
|
|
47
|
+
await port.ingest({ entries, droppedCount })
|
|
48
|
+
} catch {
|
|
49
|
+
dropped += droppedCount + entries.length
|
|
50
|
+
return
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function schedule(): void {
|
|
56
|
+
timer ??= window.setTimeout(() => void flush(), DELAY_MS)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function flush(): Promise<void> {
|
|
60
|
+
window.clearTimeout(timer)
|
|
61
|
+
timer = undefined
|
|
62
|
+
inFlight ??= drain().finally(() => {
|
|
63
|
+
inFlight = null
|
|
64
|
+
if (queue.length > 0) schedule()
|
|
65
|
+
})
|
|
66
|
+
return inFlight
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function push(level: IngestLevel, event: string, fields?: LogFields): void {
|
|
70
|
+
if (!isValid(event, fields)) {
|
|
71
|
+
dropped++
|
|
72
|
+
return
|
|
73
|
+
}
|
|
74
|
+
if (queue.length >= MAX_QUEUE) {
|
|
75
|
+
// Full: drop the oldest debug/info first; a new debug/info entry is dropped itself.
|
|
76
|
+
dropped++
|
|
77
|
+
const low = queue.findIndex((entry) => entry.level === 'debug' || entry.level === 'info')
|
|
78
|
+
if (low >= 0) queue.splice(low, 1)
|
|
79
|
+
else if (level === 'debug' || level === 'info') return
|
|
80
|
+
else queue.shift()
|
|
81
|
+
}
|
|
82
|
+
queue.push(fields ? { level, event, fields } : { level, event })
|
|
83
|
+
if (queue.length >= BATCH) void flush()
|
|
84
|
+
else schedule()
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
window.addEventListener('pagehide', () => void flush())
|
|
88
|
+
|
|
89
|
+
return {
|
|
90
|
+
debug: (event, fields) => push('debug', event, fields),
|
|
91
|
+
info: (event, fields) => push('info', event, fields),
|
|
92
|
+
warn: (event, fields) => push('warn', event, fields),
|
|
93
|
+
error: (event, fields) => push('error', event, fields),
|
|
94
|
+
flush,
|
|
95
|
+
}
|
|
96
|
+
}
|
package/src/markdown.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
// Markdown → sanitized HTML. Render it with `{@attach (node) => { node.innerHTML = html }}`, never `{@html}` (docs/04 §5).
|
|
2
|
+
import DOMPurify from 'dompurify'
|
|
3
|
+
import { marked } from 'marked'
|
|
4
|
+
|
|
5
|
+
/** Model and skill text is untrusted: DOMPurify drops scripts, event handlers and `javascript:` URLs. */
|
|
6
|
+
export function renderMarkdown(source: string): string {
|
|
7
|
+
return DOMPurify.sanitize(marked.parse(source, { async: false, gfm: true }))
|
|
8
|
+
}
|