Expand dark theme coverage across client UI
This commit is contained in:
@@ -10,6 +10,7 @@ import MessageStream from "./components/message-stream"
|
|||||||
import PromptInput from "./components/prompt-input"
|
import PromptInput from "./components/prompt-input"
|
||||||
import InfoView from "./components/info-view"
|
import InfoView from "./components/info-view"
|
||||||
import { initMarkdown } from "./lib/markdown"
|
import { initMarkdown } from "./lib/markdown"
|
||||||
|
import { useTheme } from "./lib/theme"
|
||||||
import { createCommandRegistry } from "./lib/commands"
|
import { createCommandRegistry } from "./lib/commands"
|
||||||
import type { Command } from "./lib/commands"
|
import type { Command } from "./lib/commands"
|
||||||
import {
|
import {
|
||||||
@@ -160,9 +161,14 @@ const SessionView: Component<{
|
|||||||
}
|
}
|
||||||
|
|
||||||
const App: Component = () => {
|
const App: Component = () => {
|
||||||
|
const { isDark } = useTheme()
|
||||||
const commandRegistry = createCommandRegistry()
|
const commandRegistry = createCommandRegistry()
|
||||||
const [escapeInDebounce, setEscapeInDebounce] = createSignal(false)
|
const [escapeInDebounce, setEscapeInDebounce] = createSignal(false)
|
||||||
|
|
||||||
|
createEffect(() => {
|
||||||
|
void initMarkdown(isDark()).catch(console.error)
|
||||||
|
})
|
||||||
|
|
||||||
const activeInstance = createMemo(() => getActiveInstance())
|
const activeInstance = createMemo(() => getActiveInstance())
|
||||||
|
|
||||||
const activeSessions = createMemo(() => {
|
const activeSessions = createMemo(() => {
|
||||||
@@ -634,8 +640,6 @@ const App: Component = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
initMarkdown(false).catch(console.error)
|
|
||||||
|
|
||||||
setEscapeStateChangeHandler(setEscapeInDebounce)
|
setEscapeStateChangeHandler(setEscapeInDebounce)
|
||||||
|
|
||||||
setupCommands()
|
setupCommands()
|
||||||
|
|||||||
@@ -64,17 +64,19 @@ export default function AgentSelector(props: AgentSelectorProps) {
|
|||||||
itemComponent={(itemProps) => (
|
itemComponent={(itemProps) => (
|
||||||
<Select.Item
|
<Select.Item
|
||||||
item={itemProps.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">
|
<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>
|
<span>{itemProps.item.rawValue.name}</span>
|
||||||
<Show when={itemProps.item.rawValue.mode === "subagent"}>
|
<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>
|
</Show>
|
||||||
</Select.ItemLabel>
|
</Select.ItemLabel>
|
||||||
<Show when={itemProps.item.rawValue.description}>
|
<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.length > 50
|
||||||
? itemProps.item.rawValue.description.slice(0, 50) + "..."
|
? itemProps.item.rawValue.description.slice(0, 50) + "..."
|
||||||
: itemProps.item.rawValue.description}
|
: itemProps.item.rawValue.description}
|
||||||
@@ -86,23 +88,25 @@ export default function AgentSelector(props: AgentSelectorProps) {
|
|||||||
>
|
>
|
||||||
<Select.Trigger
|
<Select.Trigger
|
||||||
data-agent-selector
|
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>>
|
<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.Value>
|
||||||
<Select.Icon>
|
<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.Icon>
|
||||||
</Select.Trigger>
|
</Select.Trigger>
|
||||||
|
|
||||||
<Select.Portal>
|
<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.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 />
|
<Select.Listbox class="bg-white dark:bg-gray-800" />
|
||||||
</Select.Content>
|
</Select.Content>
|
||||||
</Select.Portal>
|
</Select.Portal>
|
||||||
</Select>
|
</Select>
|
||||||
<span class="text-xs text-gray-400">
|
<span class="text-xs text-gray-400 dark:text-gray-500">
|
||||||
<Kbd shortcut="cmd+shift+a" />
|
<Kbd shortcut="cmd+shift+a" />
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -30,7 +30,9 @@ const InstanceTab: Component<InstanceTabProps> = (props) => {
|
|||||||
<div class="instance-tab-container group">
|
<div class="instance-tab-container group">
|
||||||
<button
|
<button
|
||||||
class={`instance-tab inline-flex items-center gap-2 px-3 py-2 rounded-t-md max-w-[200px] transition-colors ${
|
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}
|
onClick={props.onSelect}
|
||||||
title={props.instance.folder}
|
title={props.instance.folder}
|
||||||
@@ -42,7 +44,7 @@ const InstanceTab: Component<InstanceTabProps> = (props) => {
|
|||||||
{props.instance.folder.split("/").pop() || props.instance.folder}
|
{props.instance.folder.split("/").pop() || props.instance.folder}
|
||||||
</span>
|
</span>
|
||||||
<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) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation()
|
e.stopPropagation()
|
||||||
props.onClose()
|
props.onClose()
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ interface InstanceTabsProps {
|
|||||||
|
|
||||||
const InstanceTabs: Component<InstanceTabsProps> = (props) => {
|
const InstanceTabs: Component<InstanceTabsProps> = (props) => {
|
||||||
return (
|
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="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">
|
<div class="flex items-center gap-1 overflow-x-auto">
|
||||||
<For each={Array.from(props.instances.entries())}>
|
<For each={Array.from(props.instances.entries())}>
|
||||||
@@ -29,7 +29,7 @@ const InstanceTabs: Component<InstanceTabsProps> = (props) => {
|
|||||||
)}
|
)}
|
||||||
</For>
|
</For>
|
||||||
<button
|
<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}
|
onClick={props.onNew}
|
||||||
title="New instance (Cmd/Ctrl+N)"
|
title="New instance (Cmd/Ctrl+N)"
|
||||||
aria-label="New instance"
|
aria-label="New instance"
|
||||||
|
|||||||
@@ -146,14 +146,14 @@ const InstanceWelcomeView: Component<InstanceWelcomeViewProps> = (props) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
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 lg:flex-row gap-4 p-4 overflow-auto">
|
||||||
<div class="flex-1 flex flex-col gap-4 min-h-0">
|
<div class="flex-1 flex flex-col gap-4 min-h-0">
|
||||||
<Show
|
<Show
|
||||||
when={parentSessions().length > 0}
|
when={parentSessions().length > 0}
|
||||||
fallback={
|
fallback={
|
||||||
<div class="bg-white rounded-lg shadow-sm border border-gray-200 p-6 text-center flex-shrink-0">
|
<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 mb-2">
|
<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">
|
<svg class="w-12 h-12 mx-auto" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
<path
|
<path
|
||||||
stroke-linecap="round"
|
stroke-linecap="round"
|
||||||
@@ -163,15 +163,15 @@ const InstanceWelcomeView: Component<InstanceWelcomeViewProps> = (props) => {
|
|||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
<p class="text-gray-600 font-medium text-sm">No Previous Sessions</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">Create a new session below to get started</p>
|
<p class="text-xs text-gray-500 dark:text-gray-400">Create a new session below to get started</p>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<div class="bg-white rounded-lg shadow-sm border border-gray-200 overflow-hidden flex-shrink-0">
|
<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 bg-gray-50">
|
<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">Resume Session</h2>
|
<h2 class="text-base font-semibold text-gray-900 dark:text-gray-100">Resume Session</h2>
|
||||||
<p class="text-xs text-gray-500 mt-0.5">
|
<p class="text-xs text-gray-500 dark:text-gray-400 mt-0.5">
|
||||||
{parentSessions().length} {parentSessions().length === 1 ? "session" : "sessions"} available
|
{parentSessions().length} {parentSessions().length === 1 ? "session" : "sessions"} available
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -180,11 +180,10 @@ const InstanceWelcomeView: Component<InstanceWelcomeViewProps> = (props) => {
|
|||||||
{(session, index) => (
|
{(session, index) => (
|
||||||
<button
|
<button
|
||||||
data-session-index={index()}
|
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={{
|
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(),
|
focusMode() === "sessions" && selectedIndex() === index(),
|
||||||
"hover:bg-blue-50": focusMode() !== "sessions" || selectedIndex() !== index(),
|
|
||||||
}}
|
}}
|
||||||
onClick={() => handleSessionSelect(session.id)}
|
onClick={() => handleSessionSelect(session.id)}
|
||||||
onMouseEnter={() => {
|
onMouseEnter={() => {
|
||||||
@@ -196,22 +195,23 @@ const InstanceWelcomeView: Component<InstanceWelcomeViewProps> = (props) => {
|
|||||||
<div class="flex-1 min-w-0">
|
<div class="flex-1 min-w-0">
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<span
|
<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={{
|
classList={{
|
||||||
"text-blue-700": focusMode() === "sessions" && selectedIndex() === index(),
|
"text-blue-700 dark:text-blue-300":
|
||||||
|
focusMode() === "sessions" && selectedIndex() === index(),
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{session.title || "Untitled Session"}
|
{session.title || "Untitled Session"}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</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>{session.agent}</span>
|
||||||
<span>•</span>
|
<span>•</span>
|
||||||
<span>{formatRelativeTime(session.time.updated)}</span>
|
<span>{formatRelativeTime(session.time.updated)}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Show when={focusMode() === "sessions" && selectedIndex() === index()}>
|
<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>
|
</kbd>
|
||||||
</Show>
|
</Show>
|
||||||
@@ -223,18 +223,23 @@ const InstanceWelcomeView: Component<InstanceWelcomeViewProps> = (props) => {
|
|||||||
</div>
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
|
|
||||||
<div class="bg-white rounded-lg shadow-sm border border-gray-200 overflow-hidden flex-shrink-0">
|
<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 bg-gray-50">
|
<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">Start New Session</h2>
|
<h2 class="text-base font-semibold text-gray-900 dark:text-gray-100">Start New Session</h2>
|
||||||
<p class="text-xs text-gray-500 mt-0.5">Create a fresh conversation with your chosen agent</p>
|
<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>
|
||||||
<div class="p-4">
|
<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 class="space-y-3">
|
||||||
<div>
|
<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
|
<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()}
|
value={selectedAgent()}
|
||||||
onChange={(e) => setSelectedAgent(e.currentTarget.value)}
|
onChange={(e) => setSelectedAgent(e.currentTarget.value)}
|
||||||
>
|
>
|
||||||
@@ -250,7 +255,7 @@ const InstanceWelcomeView: Component<InstanceWelcomeViewProps> = (props) => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button
|
<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}
|
onClick={handleNewSession}
|
||||||
disabled={isCreating() || agentList().length === 0}
|
disabled={isCreating() || agentList().length === 0}
|
||||||
>
|
>
|
||||||
@@ -276,7 +281,7 @@ const InstanceWelcomeView: Component<InstanceWelcomeViewProps> = (props) => {
|
|||||||
</svg>
|
</svg>
|
||||||
<span>Create Session</span>
|
<span>Create Session</span>
|
||||||
</div>
|
</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
|
Cmd+Enter
|
||||||
</kbd>
|
</kbd>
|
||||||
</Show>
|
</Show>
|
||||||
@@ -294,29 +299,45 @@ const InstanceWelcomeView: Component<InstanceWelcomeViewProps> = (props) => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="px-4 py-2 bg-white border-t border-gray-200 flex-shrink-0">
|
<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">
|
<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">
|
<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 dark:bg-gray-800 border border-gray-300 dark:border-gray-600 rounded font-mono text-xs">
|
||||||
<kbd class="px-1.5 py-0.5 bg-gray-100 border border-gray-300 rounded font-mono text-xs">↓</kbd>
|
↑
|
||||||
|
</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>
|
<span>Navigate</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center gap-1.5">
|
<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 dark:bg-gray-800 border border-gray-300 dark:border-gray-600 rounded font-mono text-xs">
|
||||||
<kbd class="px-1.5 py-0.5 bg-gray-100 border border-gray-300 rounded font-mono text-xs">PgDn</kbd>
|
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>
|
<span>Jump</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center gap-1.5">
|
<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 dark:bg-gray-800 border border-gray-300 dark:border-gray-600 rounded font-mono text-xs">
|
||||||
<kbd class="px-1.5 py-0.5 bg-gray-100 border border-gray-300 rounded font-mono text-xs">End</kbd>
|
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>
|
<span>First/Last</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center gap-1.5">
|
<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>
|
<span>Resume</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center gap-1.5">
|
<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>
|
<span>New Session</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { createEffect, createSignal, onMount, onCleanup } from "solid-js"
|
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"
|
import type { TextPart } from "../types/message"
|
||||||
|
|
||||||
interface MarkdownProps {
|
interface MarkdownProps {
|
||||||
@@ -15,11 +15,16 @@ export function Markdown(props: MarkdownProps) {
|
|||||||
createEffect(async () => {
|
createEffect(async () => {
|
||||||
const part = props.part
|
const part = props.part
|
||||||
const text = part.text || ""
|
const text = part.text || ""
|
||||||
|
const dark = Boolean(props.isDark)
|
||||||
|
const themeKey = dark ? "dark" : "light"
|
||||||
|
|
||||||
latestRequestedText = text
|
latestRequestedText = text
|
||||||
|
|
||||||
if (part.renderCache && part.renderCache.text === text) {
|
await initMarkdown(dark)
|
||||||
setHtml(part.renderCache.html)
|
|
||||||
|
const cache = part.renderCache
|
||||||
|
if (cache && cache.text === text && cache.theme === themeKey) {
|
||||||
|
setHtml(cache.html)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -28,12 +33,13 @@ export function Markdown(props: MarkdownProps) {
|
|||||||
|
|
||||||
if (latestRequestedText === text) {
|
if (latestRequestedText === text) {
|
||||||
setHtml(rendered)
|
setHtml(rendered)
|
||||||
part.renderCache = { text, html: rendered }
|
part.renderCache = { text, html: rendered, theme: themeKey }
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to render markdown:", error)
|
console.error("Failed to render markdown:", error)
|
||||||
if (latestRequestedText === text) {
|
if (latestRequestedText === text) {
|
||||||
setHtml(text)
|
setHtml(text)
|
||||||
|
part.renderCache = { text, html: text, theme: themeKey }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -323,11 +323,11 @@ export default function MessageStream(props: MessageStreamProps) {
|
|||||||
return (
|
return (
|
||||||
<div class="message-stream-container">
|
<div class="message-stream-container">
|
||||||
<div class="connection-status">
|
<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>
|
<span>{formattedSessionInfo()}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex-1" />
|
<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>
|
<span>Command Palette</span>
|
||||||
<Kbd shortcut="cmd+shift+p" />
|
<Kbd shortcut="cmd+shift+p" />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -78,19 +78,19 @@ export default function ModelSelector(props: ModelSelectorProps) {
|
|||||||
itemComponent={(itemProps) => (
|
itemComponent={(itemProps) => (
|
||||||
<Combobox.Item
|
<Combobox.Item
|
||||||
item={itemProps.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">
|
<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}
|
{itemProps.item.rawValue.name}
|
||||||
</Combobox.ItemLabel>
|
</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.providerName} • {itemProps.item.rawValue.providerId}/
|
||||||
{itemProps.item.rawValue.id}
|
{itemProps.item.rawValue.id}
|
||||||
</Combobox.ItemDescription>
|
</Combobox.ItemDescription>
|
||||||
</div>
|
</div>
|
||||||
<Combobox.ItemIndicator class="flex-shrink-0 mt-0.5">
|
<Combobox.ItemIndicator class="flex-shrink-0 mt-0.5 text-blue-600 dark:text-blue-400">
|
||||||
<svg class="w-4 h-4 text-blue-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
<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" />
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" />
|
||||||
</svg>
|
</svg>
|
||||||
</Combobox.ItemIndicator>
|
</Combobox.ItemIndicator>
|
||||||
@@ -101,36 +101,38 @@ export default function ModelSelector(props: ModelSelectorProps) {
|
|||||||
<Combobox.Input class="sr-only" data-model-selector />
|
<Combobox.Input class="sr-only" data-model-selector />
|
||||||
<Combobox.Trigger
|
<Combobox.Trigger
|
||||||
ref={triggerRef}
|
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">
|
<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() && (
|
{currentModelValue() && (
|
||||||
<span class="text-gray-500 text-[10px]">
|
<span class="text-gray-500 dark:text-gray-400 text-[10px]">
|
||||||
{currentModelValue()!.providerId}/{currentModelValue()!.id}
|
{currentModelValue()!.providerId}/{currentModelValue()!.id}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<Combobox.Icon class="flex-shrink-0">
|
<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.Icon>
|
||||||
</Combobox.Trigger>
|
</Combobox.Trigger>
|
||||||
</Combobox.Control>
|
</Combobox.Control>
|
||||||
|
|
||||||
<Combobox.Portal>
|
<Combobox.Portal>
|
||||||
<Combobox.Content class="bg-white border border-gray-300 rounded-md shadow-lg overflow-hidden z-50 min-w-[300px]">
|
<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">
|
<div class="p-2 border-b border-gray-200 dark:border-gray-700">
|
||||||
<Combobox.Input
|
<Combobox.Input
|
||||||
ref={searchInputRef}
|
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..."
|
placeholder="Search models..."
|
||||||
/>
|
/>
|
||||||
</div>
|
</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.Content>
|
||||||
</Combobox.Portal>
|
</Combobox.Portal>
|
||||||
</Combobox>
|
</Combobox>
|
||||||
<span class="text-xs text-gray-400">
|
<span class="text-xs text-gray-400 dark:text-gray-500">
|
||||||
<Kbd shortcut="cmd+shift+m" />
|
<Kbd shortcut="cmd+shift+m" />
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -66,28 +66,34 @@ const SessionPicker: Component<SessionPickerProps> = (props) => {
|
|||||||
<Dialog.Portal>
|
<Dialog.Portal>
|
||||||
<Dialog.Overlay class="fixed inset-0 bg-black/50 z-50" />
|
<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">
|
<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.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 mb-4">
|
<Dialog.Title class="text-xl font-semibold text-gray-900 dark:text-gray-100 mb-4">
|
||||||
OpenCode • {instance()?.folder.split("/").pop()}
|
OpenCode • {instance()?.folder.split("/").pop()}
|
||||||
</Dialog.Title>
|
</Dialog.Title>
|
||||||
|
|
||||||
<div class="space-y-6">
|
<div class="space-y-6">
|
||||||
<Show
|
<Show
|
||||||
when={parentSessions().length > 0}
|
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>
|
<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">
|
<div class="space-y-1 max-h-[400px] overflow-y-auto">
|
||||||
<For each={parentSessions()}>
|
<For each={parentSessions()}>
|
||||||
{(session) => (
|
{(session) => (
|
||||||
<button
|
<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)}
|
onClick={() => handleSessionSelect(session.id)}
|
||||||
>
|
>
|
||||||
<div class="flex justify-between items-start">
|
<div class="flex justify-between items-start">
|
||||||
<span class="text-sm text-gray-900 truncate flex-1">{session.title || "Untitled"}</span>
|
<span class="text-sm text-gray-900 dark:text-gray-100 truncate flex-1">
|
||||||
<span class="text-xs text-gray-500 ml-2 flex-shrink-0">
|
{session.title || "Untitled"}
|
||||||
|
</span>
|
||||||
|
<span class="text-xs text-gray-500 dark:text-gray-400 ml-2 flex-shrink-0">
|
||||||
{formatRelativeTime(session.time.updated)}
|
{formatRelativeTime(session.time.updated)}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -100,22 +106,22 @@ const SessionPicker: Component<SessionPickerProps> = (props) => {
|
|||||||
|
|
||||||
<div class="relative">
|
<div class="relative">
|
||||||
<div class="absolute inset-0 flex items-center">
|
<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>
|
||||||
<div class="relative flex justify-center text-sm">
|
<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>
|
</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">
|
<div class="space-y-3">
|
||||||
<Show
|
<Show
|
||||||
when={agentList().length > 0}
|
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
|
<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()}
|
value={selectedAgent()}
|
||||||
onChange={(e) => setSelectedAgent(e.currentTarget.value)}
|
onChange={(e) => setSelectedAgent(e.currentTarget.value)}
|
||||||
>
|
>
|
||||||
@@ -124,7 +130,7 @@ const SessionPicker: Component<SessionPickerProps> = (props) => {
|
|||||||
</Show>
|
</Show>
|
||||||
|
|
||||||
<button
|
<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}
|
onClick={handleNewSession}
|
||||||
disabled={isCreating() || agentList().length === 0}
|
disabled={isCreating() || agentList().length === 0}
|
||||||
>
|
>
|
||||||
@@ -135,7 +141,10 @@ const SessionPicker: Component<SessionPickerProps> = (props) => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mt-6 flex justify-end">
|
<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
|
Cancel
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ const SessionTabs: Component<SessionTabsProps> = (props) => {
|
|||||||
const totalTabs = () => sessionsList().length + 1
|
const totalTabs = () => sessionsList().length + 1
|
||||||
|
|
||||||
return (
|
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="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">
|
<div class="flex items-center gap-1 overflow-x-auto">
|
||||||
<For each={sessionsList()}>
|
<For each={sessionsList()}>
|
||||||
@@ -39,7 +39,7 @@ const SessionTabs: Component<SessionTabsProps> = (props) => {
|
|||||||
onSelect={() => props.onSelect("info")}
|
onSelect={() => props.onSelect("info")}
|
||||||
/>
|
/>
|
||||||
<button
|
<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}
|
onClick={props.onNew}
|
||||||
title="New parent session (Cmd/Ctrl+T)"
|
title="New parent session (Cmd/Ctrl+T)"
|
||||||
aria-label="New parent session"
|
aria-label="New parent session"
|
||||||
|
|||||||
@@ -43,11 +43,18 @@ body {
|
|||||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
||||||
-webkit-font-smoothing: antialiased;
|
-webkit-font-smoothing: antialiased;
|
||||||
-moz-osx-font-smoothing: grayscale;
|
-moz-osx-font-smoothing: grayscale;
|
||||||
|
background-color: var(--background);
|
||||||
|
color: #1f2933;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme="dark"] body {
|
||||||
|
color: #f3f4f6;
|
||||||
}
|
}
|
||||||
|
|
||||||
#root {
|
#root {
|
||||||
width: 100vw;
|
width: 100vw;
|
||||||
height: 100vh;
|
height: 100vh;
|
||||||
|
background-color: var(--background);
|
||||||
}
|
}
|
||||||
|
|
||||||
.message-stream-container {
|
.message-stream-container {
|
||||||
@@ -68,6 +75,10 @@ body {
|
|||||||
gap: 16px;
|
gap: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.connection-status-text {
|
||||||
|
color: var(--text-muted);
|
||||||
|
}
|
||||||
|
|
||||||
.status-indicator {
|
.status-indicator {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -102,6 +113,8 @@ body {
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 16px;
|
gap: 16px;
|
||||||
|
background-color: var(--background);
|
||||||
|
color: inherit;
|
||||||
}
|
}
|
||||||
|
|
||||||
.message-item {
|
.message-item {
|
||||||
@@ -111,6 +124,7 @@ body {
|
|||||||
padding: 12px 16px;
|
padding: 12px 16px;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
color: inherit;
|
||||||
}
|
}
|
||||||
|
|
||||||
.message-item.user {
|
.message-item.user {
|
||||||
@@ -132,6 +146,7 @@ body {
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
background-color: #f8f9fa;
|
background-color: #f8f9fa;
|
||||||
border-left: 4px solid #6c757d;
|
border-left: 4px solid #6c757d;
|
||||||
|
color: inherit;
|
||||||
}
|
}
|
||||||
|
|
||||||
[data-theme="dark"] .tool-call-message {
|
[data-theme="dark"] .tool-call-message {
|
||||||
@@ -145,32 +160,24 @@ body {
|
|||||||
gap: 8px;
|
gap: 8px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
color: #495057;
|
color: var(--text-muted);
|
||||||
margin-bottom: 4px;
|
margin-bottom: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
[data-theme="dark"] .tool-call-header-label {
|
|
||||||
color: #adb5bd;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tool-call-header-label .tool-call-icon {
|
.tool-call-header-label .tool-call-icon {
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tool-call-header-label .tool-name {
|
.tool-call-header-label .tool-name {
|
||||||
font-family: monospace;
|
font-family: monospace;
|
||||||
color: #212529;
|
color: inherit;
|
||||||
background-color: #e9ecef;
|
background-color: var(--secondary-bg);
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
padding: 2px 6px;
|
padding: 2px 6px;
|
||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
}
|
}
|
||||||
|
|
||||||
[data-theme="dark"] .tool-call-header-label .tool-name {
|
|
||||||
color: #f8f9fa;
|
|
||||||
background-color: #343a40;
|
|
||||||
}
|
|
||||||
|
|
||||||
.message-header {
|
.message-header {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
@@ -181,6 +188,7 @@ body {
|
|||||||
.message-sender {
|
.message-sender {
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
|
color: var(--text-muted);
|
||||||
}
|
}
|
||||||
|
|
||||||
.message-timestamp {
|
.message-timestamp {
|
||||||
@@ -220,6 +228,7 @@ body {
|
|||||||
line-height: 1.6;
|
line-height: 1.6;
|
||||||
white-space: pre-wrap;
|
white-space: pre-wrap;
|
||||||
word-break: break-word;
|
word-break: break-word;
|
||||||
|
color: inherit;
|
||||||
}
|
}
|
||||||
|
|
||||||
.message-queued-badge {
|
.message-queued-badge {
|
||||||
@@ -239,6 +248,7 @@ body {
|
|||||||
line-height: 1.5;
|
line-height: 1.5;
|
||||||
word-wrap: break-word;
|
word-wrap: break-word;
|
||||||
overflow-wrap: break-word;
|
overflow-wrap: break-word;
|
||||||
|
color: inherit;
|
||||||
}
|
}
|
||||||
|
|
||||||
.message-text pre {
|
.message-text pre {
|
||||||
@@ -262,6 +272,10 @@ body {
|
|||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[data-theme="dark"] .message-error-part {
|
||||||
|
background-color: rgba(244, 67, 54, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
.message-error-block {
|
.message-error-block {
|
||||||
color: var(--error-color);
|
color: var(--error-color);
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
@@ -272,6 +286,10 @@ body {
|
|||||||
margin: 8px 0;
|
margin: 8px 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[data-theme="dark"] .message-error-block {
|
||||||
|
background-color: rgba(244, 67, 54, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
.message-generating {
|
.message-generating {
|
||||||
color: var(--text-muted);
|
color: var(--text-muted);
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
@@ -299,6 +317,7 @@ body {
|
|||||||
border: 1px solid var(--border-color);
|
border: 1px solid var(--border-color);
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
background-color: var(--secondary-bg);
|
background-color: var(--secondary-bg);
|
||||||
|
color: inherit;
|
||||||
}
|
}
|
||||||
|
|
||||||
.reasoning-container {
|
.reasoning-container {
|
||||||
@@ -332,6 +351,7 @@ body {
|
|||||||
border: 1px solid var(--border-color);
|
border: 1px solid var(--border-color);
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
color: inherit;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tool-call-message .tool-call {
|
.tool-call-message .tool-call {
|
||||||
@@ -538,10 +558,15 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.tool-call-todo-item code {
|
.tool-call-todo-item code {
|
||||||
background-color: rgba(0, 100, 255, 0.1);
|
background-color: rgba(0, 102, 255, 0.12);
|
||||||
padding: 2px 4px;
|
padding: 2px 4px;
|
||||||
border-radius: 2px;
|
border-radius: 2px;
|
||||||
font-family: monospace;
|
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 {
|
.tool-call-task-summary {
|
||||||
@@ -573,6 +598,10 @@ body {
|
|||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[data-theme="dark"] .tool-call-error-content {
|
||||||
|
background-color: rgba(244, 67, 54, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
.tool-call-error-content strong {
|
.tool-call-error-content strong {
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
@@ -785,6 +814,8 @@ body {
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
background-color: var(--background);
|
||||||
|
color: inherit;
|
||||||
}
|
}
|
||||||
|
|
||||||
.message-sending {
|
.message-sending {
|
||||||
|
|||||||
@@ -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"
|
import { storage } from "./storage"
|
||||||
|
|
||||||
interface ThemeContextValue {
|
interface ThemeContextValue {
|
||||||
@@ -9,16 +9,28 @@ interface ThemeContextValue {
|
|||||||
|
|
||||||
const ThemeContext = createContext<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 }) {
|
export function ThemeProvider(props: { children: JSX.Element }) {
|
||||||
const prefersDark = window.matchMedia("(prefers-color-scheme: dark)").matches
|
const prefersDark = window.matchMedia("(prefers-color-scheme: dark)").matches
|
||||||
const [isDark, setIsDarkSignal] = createSignal(prefersDark)
|
const [isDark, setIsDarkSignal] = createSignal(prefersDark)
|
||||||
|
|
||||||
|
applyTheme(prefersDark)
|
||||||
|
|
||||||
async function loadTheme() {
|
async function loadTheme() {
|
||||||
try {
|
try {
|
||||||
const config = await storage.loadConfig()
|
const config = await storage.loadConfig()
|
||||||
const savedTheme = (config as any).theme
|
const savedTheme = (config as any).theme
|
||||||
const initialDark = savedTheme ? savedTheme === "dark" : prefersDark
|
const initialDark = savedTheme ? savedTheme === "dark" : prefersDark
|
||||||
setIsDarkSignal(initialDark)
|
setIsDarkSignal(initialDark)
|
||||||
|
applyTheme(initialDark)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn("Failed to load theme from config:", error)
|
console.warn("Failed to load theme from config:", error)
|
||||||
}
|
}
|
||||||
@@ -37,7 +49,6 @@ export function ThemeProvider(props: { children: JSX.Element }) {
|
|||||||
onMount(() => {
|
onMount(() => {
|
||||||
loadTheme()
|
loadTheme()
|
||||||
|
|
||||||
// Listen for config changes from other instances
|
|
||||||
const unsubscribe = storage.onConfigChanged(() => {
|
const unsubscribe = storage.onConfigChanged(() => {
|
||||||
loadTheme()
|
loadTheme()
|
||||||
})
|
})
|
||||||
@@ -45,14 +56,14 @@ export function ThemeProvider(props: { children: JSX.Element }) {
|
|||||||
return unsubscribe
|
return unsubscribe
|
||||||
})
|
})
|
||||||
|
|
||||||
|
createEffect(() => {
|
||||||
|
applyTheme(isDark())
|
||||||
|
})
|
||||||
|
|
||||||
const setTheme = (dark: boolean) => {
|
const setTheme = (dark: boolean) => {
|
||||||
setIsDarkSignal(dark)
|
setIsDarkSignal(dark)
|
||||||
|
applyTheme(dark)
|
||||||
saveTheme(dark)
|
saveTheme(dark)
|
||||||
if (dark) {
|
|
||||||
document.documentElement.setAttribute("data-theme", "dark")
|
|
||||||
} else {
|
|
||||||
document.documentElement.removeAttribute("data-theme")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const toggleTheme = () => {
|
const toggleTheme = () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user