Expand dark theme coverage across client UI

This commit is contained in:
Shantur Rathore
2025-10-28 18:19:17 +00:00
parent 8858fe052b
commit 1ce5b125a9
12 changed files with 197 additions and 107 deletions

View File

@@ -10,6 +10,7 @@ import MessageStream from "./components/message-stream"
import PromptInput from "./components/prompt-input"
import InfoView from "./components/info-view"
import { initMarkdown } from "./lib/markdown"
import { useTheme } from "./lib/theme"
import { createCommandRegistry } from "./lib/commands"
import type { Command } from "./lib/commands"
import {
@@ -160,9 +161,14 @@ const SessionView: Component<{
}
const App: Component = () => {
const { isDark } = useTheme()
const commandRegistry = createCommandRegistry()
const [escapeInDebounce, setEscapeInDebounce] = createSignal(false)
createEffect(() => {
void initMarkdown(isDark()).catch(console.error)
})
const activeInstance = createMemo(() => getActiveInstance())
const activeSessions = createMemo(() => {
@@ -634,8 +640,6 @@ const App: Component = () => {
}
onMount(() => {
initMarkdown(false).catch(console.error)
setEscapeStateChangeHandler(setEscapeInDebounce)
setupCommands()

View File

@@ -64,17 +64,19 @@ export default function AgentSelector(props: AgentSelectorProps) {
itemComponent={(itemProps) => (
<Select.Item
item={itemProps.item}
class="px-3 py-2 cursor-pointer hover:bg-gray-100 rounded outline-none focus:bg-gray-100"
class="px-3 py-2 cursor-pointer rounded outline-none transition-colors hover:bg-gray-100 focus:bg-gray-100 dark:hover:bg-gray-800 dark:focus:bg-gray-800"
>
<div class="flex flex-col">
<Select.ItemLabel class="font-medium text-sm text-gray-900 flex items-center gap-2">
<Select.ItemLabel class="font-medium text-sm text-gray-900 dark:text-gray-100 flex items-center gap-2">
<span>{itemProps.item.rawValue.name}</span>
<Show when={itemProps.item.rawValue.mode === "subagent"}>
<span class="text-xs font-normal text-blue-600 bg-blue-50 px-1.5 py-0.5 rounded">subagent</span>
<span class="text-xs font-normal text-blue-600 dark:text-blue-300 bg-blue-50 dark:bg-blue-900/40 px-1.5 py-0.5 rounded">
subagent
</span>
</Show>
</Select.ItemLabel>
<Show when={itemProps.item.rawValue.description}>
<Select.ItemDescription class="text-xs text-gray-600">
<Select.ItemDescription class="text-xs text-gray-600 dark:text-gray-300">
{itemProps.item.rawValue.description.length > 50
? itemProps.item.rawValue.description.slice(0, 50) + "..."
: itemProps.item.rawValue.description}
@@ -86,23 +88,25 @@ export default function AgentSelector(props: AgentSelectorProps) {
>
<Select.Trigger
data-agent-selector
class="inline-flex items-center justify-between gap-2 px-2 py-1 bg-white border border-gray-300 rounded hover:bg-gray-50 outline-none focus:ring-2 focus:ring-blue-500 text-xs min-w-[100px]"
class="inline-flex items-center justify-between gap-2 px-2 py-1 bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-600 rounded hover:bg-gray-50 dark:hover:bg-gray-700 outline-none focus:ring-2 focus:ring-blue-500 text-xs min-w-[100px] transition-colors"
>
<Select.Value<Agent>>
{(state) => <span class="text-gray-700">Agent: {state.selectedOption()?.name ?? "None"}</span>}
{(state) => (
<span class="text-gray-700 dark:text-gray-200">Agent: {state.selectedOption()?.name ?? "None"}</span>
)}
</Select.Value>
<Select.Icon>
<ChevronDown class="w-3 h-3 text-gray-500" />
<ChevronDown class="w-3 h-3 text-gray-500 dark:text-gray-300" />
</Select.Icon>
</Select.Trigger>
<Select.Portal>
<Select.Content class="bg-white border border-gray-300 rounded-md shadow-lg max-h-80 overflow-auto p-1 z-50">
<Select.Listbox />
<Select.Content class="bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-600 rounded-md shadow-lg max-h-80 overflow-auto p-1 z-50">
<Select.Listbox class="bg-white dark:bg-gray-800" />
</Select.Content>
</Select.Portal>
</Select>
<span class="text-xs text-gray-400">
<span class="text-xs text-gray-400 dark:text-gray-500">
<Kbd shortcut="cmd+shift+a" />
</span>
</div>

View File

@@ -30,7 +30,9 @@ const InstanceTab: Component<InstanceTabProps> = (props) => {
<div class="instance-tab-container group">
<button
class={`instance-tab inline-flex items-center gap-2 px-3 py-2 rounded-t-md max-w-[200px] transition-colors ${
props.active ? "bg-blue-500 text-white" : "bg-gray-100 text-gray-700 hover:bg-gray-200"
props.active
? "bg-blue-500 text-white"
: "bg-gray-100 dark:bg-gray-800 text-gray-700 dark:text-gray-300 hover:bg-gray-200 dark:hover:bg-gray-700"
}`}
onClick={props.onSelect}
title={props.instance.folder}
@@ -42,7 +44,7 @@ const InstanceTab: Component<InstanceTabProps> = (props) => {
{props.instance.folder.split("/").pop() || props.instance.folder}
</span>
<span
class="tab-close opacity-0 group-hover:opacity-100 hover:bg-red-500 hover:text-white rounded p-0.5 transition-opacity ml-auto cursor-pointer"
class="tab-close opacity-0 group-hover:opacity-100 hover:bg-red-500 dark:hover:bg-red-600 hover:text-white rounded p-0.5 transition-opacity ml-auto cursor-pointer"
onClick={(e) => {
e.stopPropagation()
props.onClose()

View File

@@ -15,7 +15,7 @@ interface InstanceTabsProps {
const InstanceTabs: Component<InstanceTabsProps> = (props) => {
return (
<div class="instance-tabs bg-gray-50 border-b border-gray-200">
<div class="instance-tabs bg-gray-50 dark:bg-gray-900 border-b border-gray-200 dark:border-gray-700">
<div class="tabs-container flex items-center justify-between gap-1 px-2 py-1 overflow-x-auto" role="tablist">
<div class="flex items-center gap-1 overflow-x-auto">
<For each={Array.from(props.instances.entries())}>
@@ -29,7 +29,7 @@ const InstanceTabs: Component<InstanceTabsProps> = (props) => {
)}
</For>
<button
class="new-tab-button inline-flex items-center justify-center w-8 h-8 rounded-md text-gray-600 hover:bg-gray-200 transition-colors"
class="new-tab-button inline-flex items-center justify-center w-8 h-8 rounded-md text-gray-600 dark:text-gray-300 hover:bg-gray-200 dark:hover:bg-gray-800 transition-colors"
onClick={props.onNew}
title="New instance (Cmd/Ctrl+N)"
aria-label="New instance"

View File

@@ -146,14 +146,14 @@ const InstanceWelcomeView: Component<InstanceWelcomeViewProps> = (props) => {
}
return (
<div class="flex-1 flex flex-col overflow-hidden bg-gray-50">
<div class="flex-1 flex flex-col overflow-hidden bg-gray-50 dark:bg-gray-950">
<div class="flex-1 flex flex-col lg:flex-row gap-4 p-4 overflow-auto">
<div class="flex-1 flex flex-col gap-4 min-h-0">
<Show
when={parentSessions().length > 0}
fallback={
<div class="bg-white rounded-lg shadow-sm border border-gray-200 p-6 text-center flex-shrink-0">
<div class="text-gray-400 mb-2">
<div class="bg-white dark:bg-gray-900 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700 p-6 text-center flex-shrink-0">
<div class="text-gray-400 dark:text-gray-500 mb-2">
<svg class="w-12 h-12 mx-auto" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
stroke-linecap="round"
@@ -163,15 +163,15 @@ const InstanceWelcomeView: Component<InstanceWelcomeViewProps> = (props) => {
/>
</svg>
</div>
<p class="text-gray-600 font-medium text-sm">No Previous Sessions</p>
<p class="text-xs text-gray-500">Create a new session below to get started</p>
<p class="text-gray-600 dark:text-gray-200 font-medium text-sm">No Previous Sessions</p>
<p class="text-xs text-gray-500 dark:text-gray-400">Create a new session below to get started</p>
</div>
}
>
<div class="bg-white rounded-lg shadow-sm border border-gray-200 overflow-hidden flex-shrink-0">
<div class="px-4 py-3 border-b border-gray-200 bg-gray-50">
<h2 class="text-base font-semibold text-gray-900">Resume Session</h2>
<p class="text-xs text-gray-500 mt-0.5">
<div class="bg-white dark:bg-gray-900 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700 overflow-hidden flex-shrink-0">
<div class="px-4 py-3 border-b border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-900">
<h2 class="text-base font-semibold text-gray-900 dark:text-gray-100">Resume Session</h2>
<p class="text-xs text-gray-500 dark:text-gray-400 mt-0.5">
{parentSessions().length} {parentSessions().length === 1 ? "session" : "sessions"} available
</p>
</div>
@@ -180,11 +180,10 @@ const InstanceWelcomeView: Component<InstanceWelcomeViewProps> = (props) => {
{(session, index) => (
<button
data-session-index={index()}
class="w-full text-left px-4 py-2.5 border-b border-gray-100 hover:bg-blue-50 transition-all group focus:outline-none"
class="w-full text-left px-4 py-2.5 border-b border-gray-100 dark:border-gray-800 transition-all group focus:outline-none hover:bg-blue-50 dark:hover:bg-blue-900/30"
classList={{
"bg-blue-100 ring-2 ring-blue-500 ring-inset":
"bg-blue-100 dark:bg-blue-900/50 ring-2 ring-blue-500 dark:ring-blue-400 ring-inset":
focusMode() === "sessions" && selectedIndex() === index(),
"hover:bg-blue-50": focusMode() !== "sessions" || selectedIndex() !== index(),
}}
onClick={() => handleSessionSelect(session.id)}
onMouseEnter={() => {
@@ -196,22 +195,23 @@ const InstanceWelcomeView: Component<InstanceWelcomeViewProps> = (props) => {
<div class="flex-1 min-w-0">
<div class="flex items-center gap-2">
<span
class="text-sm font-medium text-gray-900 group-hover:text-blue-700 truncate"
class="text-sm font-medium text-gray-900 dark:text-gray-100 group-hover:text-blue-700 dark:group-hover:text-blue-300 truncate"
classList={{
"text-blue-700": focusMode() === "sessions" && selectedIndex() === index(),
"text-blue-700 dark:text-blue-300":
focusMode() === "sessions" && selectedIndex() === index(),
}}
>
{session.title || "Untitled Session"}
</span>
</div>
<div class="flex items-center gap-3 text-xs text-gray-500 mt-0.5">
<div class="flex items-center gap-3 text-xs text-gray-500 dark:text-gray-400 mt-0.5">
<span>{session.agent}</span>
<span></span>
<span>{formatRelativeTime(session.time.updated)}</span>
</div>
</div>
<Show when={focusMode() === "sessions" && selectedIndex() === index()}>
<kbd class="px-1.5 py-0.5 text-xs font-semibold text-gray-700 bg-white border border-gray-300 rounded flex-shrink-0">
<kbd class="px-1.5 py-0.5 text-xs font-semibold text-gray-700 dark:text-gray-200 bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-600 rounded flex-shrink-0">
</kbd>
</Show>
@@ -223,18 +223,23 @@ const InstanceWelcomeView: Component<InstanceWelcomeViewProps> = (props) => {
</div>
</Show>
<div class="bg-white rounded-lg shadow-sm border border-gray-200 overflow-hidden flex-shrink-0">
<div class="px-4 py-3 border-b border-gray-200 bg-gray-50">
<h2 class="text-base font-semibold text-gray-900">Start New Session</h2>
<p class="text-xs text-gray-500 mt-0.5">Create a fresh conversation with your chosen agent</p>
<div class="bg-white dark:bg-gray-900 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700 overflow-hidden flex-shrink-0">
<div class="px-4 py-3 border-b border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-900">
<h2 class="text-base font-semibold text-gray-900 dark:text-gray-100">Start New Session</h2>
<p class="text-xs text-gray-500 dark:text-gray-400 mt-0.5">
Create a fresh conversation with your chosen agent
</p>
</div>
<div class="p-4">
<Show when={agentList().length > 0} fallback={<div class="text-sm text-gray-500">Loading agents...</div>}>
<Show
when={agentList().length > 0}
fallback={<div class="text-sm text-gray-500 dark:text-gray-400">Loading agents...</div>}
>
<div class="space-y-3">
<div>
<label class="block text-xs font-medium text-gray-700 mb-1.5">Agent</label>
<label class="block text-xs font-medium text-gray-700 dark:text-gray-300 mb-1.5">Agent</label>
<select
class="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm bg-white hover:border-gray-400 focus:border-blue-500 focus:ring-2 focus:ring-blue-500/20 transition-all"
class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg text-sm bg-white dark:bg-gray-800 dark:text-gray-100 hover:border-gray-400 dark:hover:border-gray-500 focus:border-blue-500 focus:ring-2 focus:ring-blue-500/20 transition-all"
value={selectedAgent()}
onChange={(e) => setSelectedAgent(e.currentTarget.value)}
>
@@ -250,7 +255,7 @@ const InstanceWelcomeView: Component<InstanceWelcomeViewProps> = (props) => {
</div>
<button
class="w-full px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 disabled:bg-gray-300 disabled:cursor-not-allowed transition-all font-medium flex items-center justify-between text-sm relative group"
class="w-full px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 dark:hover:bg-blue-500 disabled:bg-gray-300 dark:disabled:bg-gray-600 disabled:cursor-not-allowed transition-all font-medium flex items-center justify-between text-sm relative group focus:outline-none focus:ring-2 focus:ring-blue-500/40"
onClick={handleNewSession}
disabled={isCreating() || agentList().length === 0}
>
@@ -276,7 +281,7 @@ const InstanceWelcomeView: Component<InstanceWelcomeViewProps> = (props) => {
</svg>
<span>Create Session</span>
</div>
<kbd class="px-1.5 py-0.5 text-xs font-semibold bg-blue-700 border border-blue-500 rounded flex-shrink-0">
<kbd class="px-1.5 py-0.5 text-xs font-semibold bg-blue-700 border border-blue-500 rounded flex-shrink-0 dark:bg-blue-600 dark:border-blue-400">
Cmd+Enter
</kbd>
</Show>
@@ -294,29 +299,45 @@ const InstanceWelcomeView: Component<InstanceWelcomeViewProps> = (props) => {
</div>
</div>
<div class="px-4 py-2 bg-white border-t border-gray-200 flex-shrink-0">
<div class="flex items-center justify-center flex-wrap gap-3 text-xs text-gray-500">
<div class="px-4 py-2 bg-white dark:bg-gray-900 border-t border-gray-200 dark:border-gray-700 flex-shrink-0">
<div class="flex items-center justify-center flex-wrap gap-3 text-xs text-gray-500 dark:text-gray-400">
<div class="flex items-center gap-1.5">
<kbd class="px-1.5 py-0.5 bg-gray-100 border border-gray-300 rounded font-mono text-xs"></kbd>
<kbd class="px-1.5 py-0.5 bg-gray-100 border border-gray-300 rounded font-mono text-xs"></kbd>
<kbd class="px-1.5 py-0.5 bg-gray-100 dark:bg-gray-800 border border-gray-300 dark:border-gray-600 rounded font-mono text-xs">
</kbd>
<kbd class="px-1.5 py-0.5 bg-gray-100 dark:bg-gray-800 border border-gray-300 dark:border-gray-600 rounded font-mono text-xs">
</kbd>
<span>Navigate</span>
</div>
<div class="flex items-center gap-1.5">
<kbd class="px-1.5 py-0.5 bg-gray-100 border border-gray-300 rounded font-mono text-xs">PgUp</kbd>
<kbd class="px-1.5 py-0.5 bg-gray-100 border border-gray-300 rounded font-mono text-xs">PgDn</kbd>
<kbd class="px-1.5 py-0.5 bg-gray-100 dark:bg-gray-800 border border-gray-300 dark:border-gray-600 rounded font-mono text-xs">
PgUp
</kbd>
<kbd class="px-1.5 py-0.5 bg-gray-100 dark:bg-gray-800 border border-gray-300 dark:border-gray-600 rounded font-mono text-xs">
PgDn
</kbd>
<span>Jump</span>
</div>
<div class="flex items-center gap-1.5">
<kbd class="px-1.5 py-0.5 bg-gray-100 border border-gray-300 rounded font-mono text-xs">Home</kbd>
<kbd class="px-1.5 py-0.5 bg-gray-100 border border-gray-300 rounded font-mono text-xs">End</kbd>
<kbd class="px-1.5 py-0.5 bg-gray-100 dark:bg-gray-800 border border-gray-300 dark:border-gray-600 rounded font-mono text-xs">
Home
</kbd>
<kbd class="px-1.5 py-0.5 bg-gray-100 dark:bg-gray-800 border border-gray-300 dark:border-gray-600 rounded font-mono text-xs">
End
</kbd>
<span>First/Last</span>
</div>
<div class="flex items-center gap-1.5">
<kbd class="px-1.5 py-0.5 bg-gray-100 border border-gray-300 rounded font-mono text-xs">Enter</kbd>
<kbd class="px-1.5 py-0.5 bg-gray-100 dark:bg-gray-800 border border-gray-300 dark:border-gray-600 rounded font-mono text-xs">
Enter
</kbd>
<span>Resume</span>
</div>
<div class="flex items-center gap-1.5">
<kbd class="px-1.5 py-0.5 bg-gray-100 border border-gray-300 rounded text-xs">Cmd+Enter</kbd>
<kbd class="px-1.5 py-0.5 bg-gray-100 dark:bg-gray-800 border border-gray-300 dark:border-gray-600 rounded text-xs">
Cmd+Enter
</kbd>
<span>New Session</span>
</div>
</div>

View File

@@ -1,5 +1,5 @@
import { createEffect, createSignal, onMount, onCleanup } from "solid-js"
import { renderMarkdown, onLanguagesLoaded } from "../lib/markdown"
import { renderMarkdown, onLanguagesLoaded, initMarkdown } from "../lib/markdown"
import type { TextPart } from "../types/message"
interface MarkdownProps {
@@ -15,11 +15,16 @@ export function Markdown(props: MarkdownProps) {
createEffect(async () => {
const part = props.part
const text = part.text || ""
const dark = Boolean(props.isDark)
const themeKey = dark ? "dark" : "light"
latestRequestedText = text
if (part.renderCache && part.renderCache.text === text) {
setHtml(part.renderCache.html)
await initMarkdown(dark)
const cache = part.renderCache
if (cache && cache.text === text && cache.theme === themeKey) {
setHtml(cache.html)
return
}
@@ -28,12 +33,13 @@ export function Markdown(props: MarkdownProps) {
if (latestRequestedText === text) {
setHtml(rendered)
part.renderCache = { text, html: rendered }
part.renderCache = { text, html: rendered, theme: themeKey }
}
} catch (error) {
console.error("Failed to render markdown:", error)
if (latestRequestedText === text) {
setHtml(text)
part.renderCache = { text, html: text, theme: themeKey }
}
}
})

View File

@@ -323,11 +323,11 @@ export default function MessageStream(props: MessageStreamProps) {
return (
<div class="message-stream-container">
<div class="connection-status">
<div class="flex items-center gap-2 text-sm font-medium text-gray-700">
<div class="connection-status-text flex items-center gap-2 text-sm font-medium">
<span>{formattedSessionInfo()}</span>
</div>
<div class="flex-1" />
<div class="flex items-center gap-2 text-sm font-medium text-gray-700">
<div class="connection-status-text flex items-center gap-2 text-sm font-medium">
<span>Command Palette</span>
<Kbd shortcut="cmd+shift+p" />
</div>

View File

@@ -78,19 +78,19 @@ export default function ModelSelector(props: ModelSelectorProps) {
itemComponent={(itemProps) => (
<Combobox.Item
item={itemProps.item}
class="px-3 py-2 cursor-pointer hover:bg-blue-50 rounded outline-none data-[highlighted]:bg-blue-100 flex items-start gap-2"
class="px-3 py-2 cursor-pointer rounded outline-none transition-colors hover:bg-blue-50 dark:hover:bg-blue-900/40 data-[highlighted]:bg-blue-100 dark:data-[highlighted]:bg-blue-900/60 flex items-start gap-2"
>
<div class="flex flex-col flex-1 min-w-0">
<Combobox.ItemLabel class="font-medium text-sm text-gray-900">
<Combobox.ItemLabel class="font-medium text-sm text-gray-900 dark:text-gray-100">
{itemProps.item.rawValue.name}
</Combobox.ItemLabel>
<Combobox.ItemDescription class="text-xs text-gray-600">
<Combobox.ItemDescription class="text-xs text-gray-600 dark:text-gray-300">
{itemProps.item.rawValue.providerName} {itemProps.item.rawValue.providerId}/
{itemProps.item.rawValue.id}
</Combobox.ItemDescription>
</div>
<Combobox.ItemIndicator class="flex-shrink-0 mt-0.5">
<svg class="w-4 h-4 text-blue-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<Combobox.ItemIndicator class="flex-shrink-0 mt-0.5 text-blue-600 dark:text-blue-400">
<svg class="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" />
</svg>
</Combobox.ItemIndicator>
@@ -101,36 +101,38 @@ export default function ModelSelector(props: ModelSelectorProps) {
<Combobox.Input class="sr-only" data-model-selector />
<Combobox.Trigger
ref={triggerRef}
class="inline-flex items-center justify-between gap-2 px-2 py-1 bg-white border border-gray-300 rounded hover:bg-gray-50 outline-none focus:ring-2 focus:ring-blue-500 text-xs min-w-[180px]"
class="inline-flex items-center justify-between gap-2 px-2 py-1 bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-600 rounded hover:bg-gray-50 dark:hover:bg-gray-700 outline-none focus:ring-2 focus:ring-blue-500 text-xs min-w-[180px] transition-colors"
>
<div class="flex flex-col items-start min-w-0">
<span class="text-gray-700 font-medium">Model: {currentModelValue()?.name ?? "None"}</span>
<span class="text-gray-700 dark:text-gray-200 font-medium">
Model: {currentModelValue()?.name ?? "None"}
</span>
{currentModelValue() && (
<span class="text-gray-500 text-[10px]">
<span class="text-gray-500 dark:text-gray-400 text-[10px]">
{currentModelValue()!.providerId}/{currentModelValue()!.id}
</span>
)}
</div>
<Combobox.Icon class="flex-shrink-0">
<ChevronDown class="w-3 h-3 text-gray-500" />
<ChevronDown class="w-3 h-3 text-gray-500 dark:text-gray-300" />
</Combobox.Icon>
</Combobox.Trigger>
</Combobox.Control>
<Combobox.Portal>
<Combobox.Content class="bg-white border border-gray-300 rounded-md shadow-lg overflow-hidden z-50 min-w-[300px]">
<div class="p-2 border-b border-gray-200">
<Combobox.Content class="bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-600 rounded-md shadow-lg overflow-hidden z-50 min-w-[300px]">
<div class="p-2 border-b border-gray-200 dark:border-gray-700">
<Combobox.Input
ref={searchInputRef}
class="w-full px-3 py-1.5 text-xs border border-gray-300 rounded outline-none focus:ring-2 focus:ring-blue-500"
class="w-full px-3 py-1.5 text-xs border border-gray-300 dark:border-gray-600 rounded outline-none focus:ring-2 focus:ring-blue-500 bg-white dark:bg-gray-900 text-gray-900 dark:text-gray-100"
placeholder="Search models..."
/>
</div>
<Combobox.Listbox class="max-h-64 overflow-auto p-1" />
<Combobox.Listbox class="max-h-64 overflow-auto p-1 bg-white dark:bg-gray-800" />
</Combobox.Content>
</Combobox.Portal>
</Combobox>
<span class="text-xs text-gray-400">
<span class="text-xs text-gray-400 dark:text-gray-500">
<Kbd shortcut="cmd+shift+m" />
</span>
</div>

View File

@@ -66,28 +66,34 @@ const SessionPicker: Component<SessionPickerProps> = (props) => {
<Dialog.Portal>
<Dialog.Overlay class="fixed inset-0 bg-black/50 z-50" />
<div class="fixed inset-0 z-50 flex items-center justify-center p-4">
<Dialog.Content class="bg-white rounded-lg shadow-2xl w-full max-w-lg p-6">
<Dialog.Title class="text-xl font-semibold text-gray-900 mb-4">
<Dialog.Content class="bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-700 rounded-lg shadow-2xl w-full max-w-lg p-6">
<Dialog.Title class="text-xl font-semibold text-gray-900 dark:text-gray-100 mb-4">
OpenCode {instance()?.folder.split("/").pop()}
</Dialog.Title>
<div class="space-y-6">
<Show
when={parentSessions().length > 0}
fallback={<div class="text-center py-4 text-gray-500 text-sm">No previous sessions</div>}
fallback={
<div class="text-center py-4 text-gray-500 dark:text-gray-400 text-sm">No previous sessions</div>
}
>
<div>
<h3 class="text-sm font-medium text-gray-700 mb-2">Resume a session ({parentSessions().length}):</h3>
<h3 class="text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
Resume a session ({parentSessions().length}):
</h3>
<div class="space-y-1 max-h-[400px] overflow-y-auto">
<For each={parentSessions()}>
{(session) => (
<button
class="w-full text-left px-3 py-2 rounded hover:bg-gray-100 transition-colors group"
class="w-full text-left px-3 py-2 rounded transition-colors group hover:bg-gray-100 dark:hover:bg-gray-800"
onClick={() => handleSessionSelect(session.id)}
>
<div class="flex justify-between items-start">
<span class="text-sm text-gray-900 truncate flex-1">{session.title || "Untitled"}</span>
<span class="text-xs text-gray-500 ml-2 flex-shrink-0">
<span class="text-sm text-gray-900 dark:text-gray-100 truncate flex-1">
{session.title || "Untitled"}
</span>
<span class="text-xs text-gray-500 dark:text-gray-400 ml-2 flex-shrink-0">
{formatRelativeTime(session.time.updated)}
</span>
</div>
@@ -100,22 +106,22 @@ const SessionPicker: Component<SessionPickerProps> = (props) => {
<div class="relative">
<div class="absolute inset-0 flex items-center">
<div class="w-full border-t border-gray-300" />
<div class="w-full border-t border-gray-300 dark:border-gray-700" />
</div>
<div class="relative flex justify-center text-sm">
<span class="px-2 bg-white text-gray-500">or</span>
<span class="px-2 bg-white dark:bg-gray-900 text-gray-500 dark:text-gray-400">or</span>
</div>
</div>
<div>
<h3 class="text-sm font-medium text-gray-700 mb-2">Start new session:</h3>
<h3 class="text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Start new session:</h3>
<div class="space-y-3">
<Show
when={agentList().length > 0}
fallback={<div class="text-sm text-gray-500">Loading agents...</div>}
fallback={<div class="text-sm text-gray-500 dark:text-gray-400">Loading agents...</div>}
>
<select
class="w-full px-3 py-2 border border-gray-300 rounded text-sm bg-white hover:border-gray-400 transition-colors"
class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded text-sm bg-white dark:bg-gray-800 dark:text-gray-100 hover:border-gray-400 dark:hover:border-gray-500 transition-colors"
value={selectedAgent()}
onChange={(e) => setSelectedAgent(e.currentTarget.value)}
>
@@ -124,7 +130,7 @@ const SessionPicker: Component<SessionPickerProps> = (props) => {
</Show>
<button
class="w-full px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700 disabled:bg-gray-300 disabled:cursor-not-allowed transition-colors"
class="w-full px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700 dark:hover:bg-blue-500 disabled:bg-gray-300 dark:disabled:bg-gray-600 disabled:cursor-not-allowed transition-colors focus:outline-none focus:ring-2 focus:ring-blue-500/40"
onClick={handleNewSession}
disabled={isCreating() || agentList().length === 0}
>
@@ -135,7 +141,10 @@ const SessionPicker: Component<SessionPickerProps> = (props) => {
</div>
<div class="mt-6 flex justify-end">
<button class="px-4 py-2 text-sm text-gray-700 hover:text-gray-900" onClick={handleCancel}>
<button
class="px-4 py-2 text-sm text-gray-700 dark:text-gray-300 hover:text-gray-900 dark:hover:text-gray-100 transition-colors"
onClick={handleCancel}
>
Cancel
</button>
</div>

View File

@@ -19,7 +19,7 @@ const SessionTabs: Component<SessionTabsProps> = (props) => {
const totalTabs = () => sessionsList().length + 1
return (
<div class="session-tabs bg-white border-b border-gray-200">
<div class="session-tabs bg-white dark:bg-gray-900 border-b border-gray-200 dark:border-gray-700">
<div class="tabs-container flex items-center justify-between gap-1 px-2 py-1 overflow-x-auto" role="tablist">
<div class="flex items-center gap-1 overflow-x-auto">
<For each={sessionsList()}>
@@ -39,7 +39,7 @@ const SessionTabs: Component<SessionTabsProps> = (props) => {
onSelect={() => props.onSelect("info")}
/>
<button
class="new-tab-button inline-flex items-center justify-center w-8 h-8 rounded-md text-gray-600 hover:bg-gray-100 transition-colors"
class="new-tab-button inline-flex items-center justify-center w-8 h-8 rounded-md text-gray-600 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors"
onClick={props.onNew}
title="New parent session (Cmd/Ctrl+T)"
aria-label="New parent session"

View File

@@ -43,11 +43,18 @@ body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
background-color: var(--background);
color: #1f2933;
}
[data-theme="dark"] body {
color: #f3f4f6;
}
#root {
width: 100vw;
height: 100vh;
background-color: var(--background);
}
.message-stream-container {
@@ -68,6 +75,10 @@ body {
gap: 16px;
}
.connection-status-text {
color: var(--text-muted);
}
.status-indicator {
display: flex;
align-items: center;
@@ -102,6 +113,8 @@ body {
display: flex;
flex-direction: column;
gap: 16px;
background-color: var(--background);
color: inherit;
}
.message-item {
@@ -111,6 +124,7 @@ body {
padding: 12px 16px;
border-radius: 8px;
width: 100%;
color: inherit;
}
.message-item.user {
@@ -132,6 +146,7 @@ body {
width: 100%;
background-color: #f8f9fa;
border-left: 4px solid #6c757d;
color: inherit;
}
[data-theme="dark"] .tool-call-message {
@@ -145,32 +160,24 @@ body {
gap: 8px;
font-weight: 600;
font-size: 14px;
color: #495057;
color: var(--text-muted);
margin-bottom: 4px;
}
[data-theme="dark"] .tool-call-header-label {
color: #adb5bd;
}
.tool-call-header-label .tool-call-icon {
font-size: 16px;
}
.tool-call-header-label .tool-name {
font-family: monospace;
color: #212529;
background-color: #e9ecef;
color: inherit;
background-color: var(--secondary-bg);
border: 1px solid var(--border-color);
padding: 2px 6px;
border-radius: 3px;
font-size: 13px;
}
[data-theme="dark"] .tool-call-header-label .tool-name {
color: #f8f9fa;
background-color: #343a40;
}
.message-header {
display: flex;
justify-content: space-between;
@@ -181,6 +188,7 @@ body {
.message-sender {
font-weight: 600;
font-size: 14px;
color: var(--text-muted);
}
.message-timestamp {
@@ -220,6 +228,7 @@ body {
line-height: 1.6;
white-space: pre-wrap;
word-break: break-word;
color: inherit;
}
.message-queued-badge {
@@ -239,6 +248,7 @@ body {
line-height: 1.5;
word-wrap: break-word;
overflow-wrap: break-word;
color: inherit;
}
.message-text pre {
@@ -262,6 +272,10 @@ body {
border-radius: 4px;
}
[data-theme="dark"] .message-error-part {
background-color: rgba(244, 67, 54, 0.15);
}
.message-error-block {
color: var(--error-color);
font-size: 14px;
@@ -272,6 +286,10 @@ body {
margin: 8px 0;
}
[data-theme="dark"] .message-error-block {
background-color: rgba(244, 67, 54, 0.15);
}
.message-generating {
color: var(--text-muted);
font-size: 14px;
@@ -299,6 +317,7 @@ body {
border: 1px solid var(--border-color);
border-radius: 4px;
background-color: var(--secondary-bg);
color: inherit;
}
.reasoning-container {
@@ -332,6 +351,7 @@ body {
border: 1px solid var(--border-color);
border-radius: 6px;
overflow: hidden;
color: inherit;
}
.tool-call-message .tool-call {
@@ -538,10 +558,15 @@ body {
}
.tool-call-todo-item code {
background-color: rgba(0, 100, 255, 0.1);
background-color: rgba(0, 102, 255, 0.12);
padding: 2px 4px;
border-radius: 2px;
font-family: monospace;
color: inherit;
}
[data-theme="dark"] .tool-call-todo-item code {
background-color: rgba(0, 128, 255, 0.22);
}
.tool-call-task-summary {
@@ -573,6 +598,10 @@ body {
font-size: 12px;
}
[data-theme="dark"] .tool-call-error-content {
background-color: rgba(244, 67, 54, 0.2);
}
.tool-call-error-content strong {
font-weight: 600;
}
@@ -785,6 +814,8 @@ body {
display: flex;
flex-direction: column;
height: 100%;
background-color: var(--background);
color: inherit;
}
.message-sending {

View File

@@ -1,4 +1,4 @@
import { createContext, createSignal, useContext, onMount, type JSX } from "solid-js"
import { createContext, createSignal, useContext, onMount, createEffect, type JSX } from "solid-js"
import { storage } from "./storage"
interface ThemeContextValue {
@@ -9,16 +9,28 @@ interface ThemeContextValue {
const ThemeContext = createContext<ThemeContextValue>()
function applyTheme(dark: boolean) {
if (dark) {
document.documentElement.setAttribute("data-theme", "dark")
return
}
document.documentElement.removeAttribute("data-theme")
}
export function ThemeProvider(props: { children: JSX.Element }) {
const prefersDark = window.matchMedia("(prefers-color-scheme: dark)").matches
const [isDark, setIsDarkSignal] = createSignal(prefersDark)
applyTheme(prefersDark)
async function loadTheme() {
try {
const config = await storage.loadConfig()
const savedTheme = (config as any).theme
const initialDark = savedTheme ? savedTheme === "dark" : prefersDark
setIsDarkSignal(initialDark)
applyTheme(initialDark)
} catch (error) {
console.warn("Failed to load theme from config:", error)
}
@@ -37,7 +49,6 @@ export function ThemeProvider(props: { children: JSX.Element }) {
onMount(() => {
loadTheme()
// Listen for config changes from other instances
const unsubscribe = storage.onConfigChanged(() => {
loadTheme()
})
@@ -45,14 +56,14 @@ export function ThemeProvider(props: { children: JSX.Element }) {
return unsubscribe
})
createEffect(() => {
applyTheme(isDark())
})
const setTheme = (dark: boolean) => {
setIsDarkSignal(dark)
applyTheme(dark)
saveTheme(dark)
if (dark) {
document.documentElement.setAttribute("data-theme", "dark")
} else {
document.documentElement.removeAttribute("data-theme")
}
}
const toggleTheme = () => {