style(web): fix prettier config, format entire web/ tree
.prettierrc.json was missing "semi": false, so prettier wanted to add semicolons to a codebase written without them (763 semicolon-free statements vs. 150 with, in hand-written .ts; zero hand-written .svelte files use them at all). That's why prettier --check failed on 249 files — not because the code was unformatted, but because the config didn't match the actual house style. Added "semi": false; left printWidth/etc as configured (printWidth barely moves the failure count: 218/213/212 files at 100/120/140). Ran `prettier --write .` with the corrected config. Verified semantics-preserving before and after: - eslint: 142 problems both before and after, byte-identical - build passes, 38/38 tests pass - token-stream diff (whitespace/semicolons/quotes normalized) on all 218 changed files: only 52 had any remaining token change, all either trailing-comma removal (matching trailingComma: "none") or import/ ternary reflow — no semantic changes - live smoke test: Knowledge, Tasks, Fleet map, and a chat window (AgentTrace, markdown, Scope graph, activity rail) all render correctly, no console errors Most of the diff is shadcn/ui vendor files (lib/components/ui/) moving from the CLI's own style (double quotes, tabs, semicolons) to house style; re-running `shadcn-svelte add` on a component will need a follow-up format pass. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -60,10 +60,16 @@
|
||||
let wasStreaming = $state(false)
|
||||
|
||||
$effect(() => {
|
||||
if (streaming) { indicatorDone = false; wasStreaming = true }
|
||||
if (streaming) {
|
||||
indicatorDone = false
|
||||
wasStreaming = true
|
||||
}
|
||||
if (!streaming && wasStreaming) {
|
||||
indicatorDone = true
|
||||
const t = setTimeout(() => { indicatorDone = false; wasStreaming = false }, 3000)
|
||||
const t = setTimeout(() => {
|
||||
indicatorDone = false
|
||||
wasStreaming = false
|
||||
}, 3000)
|
||||
return () => clearTimeout(t)
|
||||
}
|
||||
})
|
||||
@@ -91,7 +97,10 @@
|
||||
const lineHeight = parseFloat(taCs.lineHeight)
|
||||
if (!Number.isFinite(lineHeight)) return
|
||||
const taBoxY =
|
||||
parseFloat(taCs.paddingTop) + parseFloat(taCs.paddingBottom) + parseFloat(taCs.borderTopWidth) + parseFloat(taCs.borderBottomWidth)
|
||||
parseFloat(taCs.paddingTop) +
|
||||
parseFloat(taCs.paddingBottom) +
|
||||
parseFloat(taCs.borderTopWidth) +
|
||||
parseFloat(taCs.borderBottomWidth)
|
||||
// The wrapper's own padding/border (space around the textarea, not part
|
||||
// of it) also has to fit inside the minimum, or the textarea gets
|
||||
// squeezed below one line once the pane is dragged down to it.
|
||||
@@ -144,7 +153,9 @@
|
||||
}
|
||||
renderer.table = function (token) {
|
||||
const header = token.header.map((c: { text: string }) => `<th>${c.text}</th>`).join('')
|
||||
const body = token.rows.map((r: { text: string }[]) => `<tr>${r.map((c) => `<td>${c.text}</td>`).join('')}</tr>`).join('')
|
||||
const body = token.rows
|
||||
.map((r: { text: string }[]) => `<tr>${r.map((c) => `<td>${c.text}</td>`).join('')}</tr>`)
|
||||
.join('')
|
||||
return `<div class="table-wrapper"><table><thead><tr>${header}</tr></thead><tbody>${body}</tbody></table></div>`
|
||||
}
|
||||
return DOMPurify.sanitize(marked.parse(text, { async: false, renderer }) as string)
|
||||
@@ -181,126 +192,172 @@
|
||||
</script>
|
||||
|
||||
<div class="flex h-full min-h-0 min-w-0 flex-col" bind:clientHeight={threadHeight}>
|
||||
<Splitpanes horizontal theme="oikos-theme" dblClickSplitter={false} class="min-h-0 flex-1" on:resize={() => (userResizedInput = true)}>
|
||||
<Splitpanes
|
||||
horizontal
|
||||
theme="oikos-theme"
|
||||
dblClickSplitter={false}
|
||||
class="min-h-0 flex-1"
|
||||
on:resize={() => (userResizedInput = true)}
|
||||
>
|
||||
<Pane class="flex flex-col">
|
||||
<div class="min-h-0 flex-1 overflow-y-auto" bind:this={container} onscroll={onScroll}>
|
||||
<div class="mx-auto flex min-h-full max-w-3xl flex-col gap-5 p-4">
|
||||
{#if messages.length === 0}
|
||||
<div class="flex flex-1 flex-col items-center justify-center gap-6 text-center">
|
||||
<div>
|
||||
<h2 class="text-xl font-semibold">Nomos</h2>
|
||||
<p class="mt-1 text-sm text-muted-foreground">Your resident operator. Ask about the fleet, or tell it to act.</p>
|
||||
</div>
|
||||
{#if suggestions.length}
|
||||
<div class="grid w-full max-w-md grid-cols-1 gap-2 sm:grid-cols-2">
|
||||
{#each suggestions as q}
|
||||
<Button variant="outline" size="sm" class="h-auto justify-start whitespace-normal py-2 text-left text-xs" onclick={() => ask(q)}>
|
||||
{q}
|
||||
</Button>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#each messages as msg, idx (msg.id)}
|
||||
<div class="flex flex-col gap-1.5 {msg.role === 'user' ? 'items-end' : 'items-start'}">
|
||||
{#if msg.role === 'user'}
|
||||
<div class="flex items-baseline gap-2 px-1">
|
||||
<span class="text-[10px] font-medium text-muted-foreground/70">You</span>
|
||||
{#if msg.created_at}
|
||||
<span class="text-[9px] text-muted-foreground/50">{formatTime(msg.created_at)}</span>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="max-w-[85%] rounded-2xl rounded-br-sm bg-primary px-4 py-2.5 text-sm text-primary-foreground whitespace-pre-wrap user-msg">{msg.text}</div>
|
||||
{:else}
|
||||
{@const isLast = idx === messages.length - 1}
|
||||
{@const traceStatus = !isLast
|
||||
? 'idle'
|
||||
: error
|
||||
? 'error'
|
||||
: streaming
|
||||
? 'running'
|
||||
: indicatorDone
|
||||
? 'done'
|
||||
: 'idle'}
|
||||
<div class="flex w-full flex-col gap-2">
|
||||
<div class="flex items-baseline gap-2 px-1">
|
||||
<span class="text-[10px] font-medium text-muted-foreground/70">Nomos</span>
|
||||
{#if msg.created_at}
|
||||
<span class="text-[9px] text-muted-foreground/50">{formatTime(msg.created_at)}</span>
|
||||
{/if}
|
||||
<div class="min-h-0 flex-1 overflow-y-auto" bind:this={container} onscroll={onScroll}>
|
||||
<div class="mx-auto flex min-h-full max-w-3xl flex-col gap-5 p-4">
|
||||
{#if messages.length === 0}
|
||||
<div class="flex flex-1 flex-col items-center justify-center gap-6 text-center">
|
||||
<div>
|
||||
<h2 class="text-xl font-semibold">Nomos</h2>
|
||||
<p class="mt-1 text-sm text-muted-foreground">
|
||||
Your resident operator. Ask about the fleet, or tell it to act.
|
||||
</p>
|
||||
</div>
|
||||
<!-- The working trace sits above the answer: it's what happened
|
||||
first, and collapsed it keeps a long tool run from burying
|
||||
the text below it. -->
|
||||
{#if msg.tools.length > 0 || traceStatus !== 'idle'}
|
||||
<AgentTrace
|
||||
tools={msg.tools}
|
||||
status={traceStatus}
|
||||
label={traceStatus === 'idle' ? null : indicatorLabel}
|
||||
/>
|
||||
{/if}
|
||||
{#if msg.text}
|
||||
<div class="prose-chat max-w-none text-sm leading-relaxed assistant-msg">
|
||||
<!-- eslint-disable-next-line svelte/no-at-html-tags — sanitized via DOMPurify -->
|
||||
{@html render(msg.text)}
|
||||
{#if isLast && streaming}
|
||||
<span class="stream-cursor" aria-hidden="true"></span>
|
||||
{/if}
|
||||
{#if suggestions.length}
|
||||
<div class="grid w-full max-w-md grid-cols-1 gap-2 sm:grid-cols-2">
|
||||
{#each suggestions as q}
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
class="h-auto justify-start whitespace-normal py-2 text-left text-xs"
|
||||
onclick={() => ask(q)}
|
||||
>
|
||||
{q}
|
||||
</Button>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#each messages as msg, idx (msg.id)}
|
||||
<div class="flex flex-col gap-1.5 {msg.role === 'user' ? 'items-end' : 'items-start'}">
|
||||
{#if msg.role === 'user'}
|
||||
<div class="flex items-baseline gap-2 px-1">
|
||||
<span class="text-[10px] font-medium text-muted-foreground/70">You</span>
|
||||
{#if msg.created_at}
|
||||
<span class="text-[9px] text-muted-foreground/50"
|
||||
>{formatTime(msg.created_at)}</span
|
||||
>
|
||||
{/if}
|
||||
</div>
|
||||
<div
|
||||
class="max-w-[85%] rounded-2xl rounded-br-sm bg-primary px-4 py-2.5 text-sm text-primary-foreground whitespace-pre-wrap user-msg"
|
||||
>
|
||||
{msg.text}
|
||||
</div>
|
||||
{:else}
|
||||
{@const isLast = idx === messages.length - 1}
|
||||
{@const traceStatus = !isLast
|
||||
? 'idle'
|
||||
: error
|
||||
? 'error'
|
||||
: streaming
|
||||
? 'running'
|
||||
: indicatorDone
|
||||
? 'done'
|
||||
: 'idle'}
|
||||
<div class="flex w-full flex-col gap-2">
|
||||
<div class="flex items-baseline gap-2 px-1">
|
||||
<span class="text-[10px] font-medium text-muted-foreground/70">Nomos</span>
|
||||
{#if msg.created_at}
|
||||
<span class="text-[9px] text-muted-foreground/50"
|
||||
>{formatTime(msg.created_at)}</span
|
||||
>
|
||||
{/if}
|
||||
</div>
|
||||
<!-- The working trace sits above the answer: it's what happened
|
||||
first, and collapsed it keeps a long tool run from burying
|
||||
the text below it. -->
|
||||
{#if msg.tools.length > 0 || traceStatus !== 'idle'}
|
||||
<AgentTrace
|
||||
tools={msg.tools}
|
||||
status={traceStatus}
|
||||
label={traceStatus === 'idle' ? null : indicatorLabel}
|
||||
/>
|
||||
{/if}
|
||||
{#if msg.text}
|
||||
<div class="prose-chat max-w-none text-sm leading-relaxed assistant-msg">
|
||||
<!-- eslint-disable-next-line svelte/no-at-html-tags — sanitized via DOMPurify -->
|
||||
{@html render(msg.text)}
|
||||
{#if isLast && streaming}
|
||||
<span class="stream-cursor" aria-hidden="true"></span>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/each}
|
||||
{#if question}
|
||||
<OperatorQuestion {sessionId} {question} />
|
||||
{/if}
|
||||
<div bind:this={messagesEnd}></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if connectionState === 'disconnected'}
|
||||
<div class="mx-auto w-full max-w-3xl px-4">
|
||||
<div
|
||||
class="mb-2 flex items-center gap-2 rounded-md border border-warning/50 bg-warning/10 px-3 py-2 text-xs"
|
||||
>
|
||||
<RefreshCwIcon class="size-3 shrink-0" aria-hidden="true" />
|
||||
<span class="text-warning-foreground flex-1"
|
||||
>Agent connection lost. The task may still be running.</span
|
||||
>
|
||||
<Button size="xs" variant="outline" class="h-6 text-[11px]" onclick={onReconnect}
|
||||
>Reconnect</Button
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
{:else if connectionState === 'reconnecting'}
|
||||
<div class="mx-auto w-full max-w-3xl px-4">
|
||||
<div class="mb-2 flex items-center gap-2 rounded-md border bg-muted/50 px-3 py-2 text-xs">
|
||||
<RefreshCwIcon
|
||||
class="size-3 shrink-0 animate-spin text-muted-foreground"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<span class="text-muted-foreground flex-1">Reconnecting to agent…</span>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if error}
|
||||
<div class="mx-auto w-full max-w-3xl px-4">
|
||||
<div
|
||||
class="mb-2 rounded-md border border-destructive/50 bg-destructive/10 px-3 py-2 text-xs text-destructive"
|
||||
>
|
||||
{error}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#each chatErrors as err (err.id)}
|
||||
<div class="mx-auto w-full max-w-3xl px-4">
|
||||
<div
|
||||
class="mb-2 flex items-center gap-2 rounded-md border border-destructive/30 bg-destructive/5 px-3 py-2 text-xs text-destructive"
|
||||
>
|
||||
<span class="flex-1">{err.message}</span>
|
||||
{#if err.action}
|
||||
<Button
|
||||
size="xs"
|
||||
variant="ghost"
|
||||
class="h-6 text-[11px]"
|
||||
onclick={() => onDismissError(err.id)}>{err.action}</Button
|
||||
>
|
||||
{/if}
|
||||
<button
|
||||
class="ml-1 text-muted-foreground hover:text-foreground"
|
||||
onclick={() => onDismissError(err.id)}
|
||||
aria-label="Dismiss">×</button
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
{#if question}
|
||||
<OperatorQuestion {sessionId} {question} />
|
||||
{/if}
|
||||
<div bind:this={messagesEnd}></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if connectionState === 'disconnected'}
|
||||
<div class="mx-auto w-full max-w-3xl px-4">
|
||||
<div class="mb-2 flex items-center gap-2 rounded-md border border-warning/50 bg-warning/10 px-3 py-2 text-xs">
|
||||
<RefreshCwIcon class="size-3 shrink-0" aria-hidden="true" />
|
||||
<span class="text-warning-foreground flex-1">Agent connection lost. The task may still be running.</span>
|
||||
<Button size="xs" variant="outline" class="h-6 text-[11px]" onclick={onReconnect}>Reconnect</Button>
|
||||
</div>
|
||||
</div>
|
||||
{:else if connectionState === 'reconnecting'}
|
||||
<div class="mx-auto w-full max-w-3xl px-4">
|
||||
<div class="mb-2 flex items-center gap-2 rounded-md border bg-muted/50 px-3 py-2 text-xs">
|
||||
<RefreshCwIcon class="size-3 shrink-0 animate-spin text-muted-foreground" aria-hidden="true" />
|
||||
<span class="text-muted-foreground flex-1">Reconnecting to agent…</span>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if error}
|
||||
<div class="mx-auto w-full max-w-3xl px-4">
|
||||
<div class="mb-2 rounded-md border border-destructive/50 bg-destructive/10 px-3 py-2 text-xs text-destructive">
|
||||
{error}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#each chatErrors as err (err.id)}
|
||||
<div class="mx-auto w-full max-w-3xl px-4">
|
||||
<div class="mb-2 flex items-center gap-2 rounded-md border border-destructive/30 bg-destructive/5 px-3 py-2 text-xs text-destructive">
|
||||
<span class="flex-1">{err.message}</span>
|
||||
{#if err.action}
|
||||
<Button size="xs" variant="ghost" class="h-6 text-[11px]" onclick={() => onDismissError(err.id)}>{err.action}</Button>
|
||||
{/if}
|
||||
<button class="ml-1 text-muted-foreground hover:text-foreground" onclick={() => onDismissError(err.id)} aria-label="Dismiss">×</button>
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
</Pane>
|
||||
|
||||
<Pane bind:size={inputSize} minSize={inputMinSize} maxSize={45} class="flex flex-col">
|
||||
<div class="flex h-full min-h-0 flex-col border-t bg-card/50 p-3 input-ornament relative" bind:this={inputWrapperRef}>
|
||||
<div
|
||||
class="flex h-full min-h-0 flex-col border-t bg-card/50 p-3 input-ornament relative"
|
||||
bind:this={inputWrapperRef}
|
||||
>
|
||||
<form
|
||||
class="relative mx-auto flex h-full w-full max-w-3xl"
|
||||
onsubmit={(e) => {
|
||||
@@ -514,7 +571,13 @@
|
||||
border: none;
|
||||
height: 1px;
|
||||
margin: 0.75rem 0;
|
||||
background: linear-gradient(to right, transparent, var(--border) 20%, var(--border) 80%, transparent);
|
||||
background: linear-gradient(
|
||||
to right,
|
||||
transparent,
|
||||
var(--border) 20%,
|
||||
var(--border) 80%,
|
||||
transparent
|
||||
);
|
||||
}
|
||||
|
||||
/* Bold is emphasis, not color — dark weight reads cleanly and lets the
|
||||
@@ -559,7 +622,9 @@
|
||||
border-radius: 0.375rem;
|
||||
color: var(--muted-foreground);
|
||||
opacity: 0;
|
||||
transition: opacity 0.15s, color 0.15s;
|
||||
transition:
|
||||
opacity 0.15s,
|
||||
color 0.15s;
|
||||
cursor: pointer;
|
||||
border: none;
|
||||
background: transparent;
|
||||
@@ -586,7 +651,12 @@
|
||||
}
|
||||
|
||||
@keyframes cursor-blink {
|
||||
0%, 100% { opacity: 0.75; }
|
||||
50% { opacity: 0; }
|
||||
0%,
|
||||
100% {
|
||||
opacity: 0.75;
|
||||
}
|
||||
50% {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user