chore: migrate remaining ui to token utilities

This commit is contained in:
Shantur Rathore
2025-10-28 21:22:19 +00:00
parent ad86154d47
commit 311c1bcd76
9 changed files with 274 additions and 162 deletions

View File

@@ -64,19 +64,17 @@ 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 rounded outline-none transition-colors hover:bg-gray-100 focus:bg-gray-100 dark:hover:bg-gray-800 dark:focus:bg-gray-800" class="selector-option"
> >
<div class="flex flex-col"> <div class="flex flex-col flex-1 min-w-0">
<Select.ItemLabel class="font-medium text-sm text-gray-900 dark:text-gray-100 flex items-center gap-2"> <Select.ItemLabel class="selector-option-label 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="neutral-badge"> <span class="neutral-badge">subagent</span>
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 dark:text-gray-300"> <Select.ItemDescription class="selector-option-description">
{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}
@@ -92,11 +90,15 @@ export default function AgentSelector(props: AgentSelectorProps) {
> >
<Select.Value<Agent>> <Select.Value<Agent>>
{(state) => ( {(state) => (
<span class="text-gray-700 dark:text-gray-200">Agent: {state.selectedOption()?.name ?? "None"}</span> <div class="selector-trigger-label">
<span class="selector-trigger-primary">
Agent: {state.selectedOption()?.name ?? "None"}
</span>
</div>
)} )}
</Select.Value> </Select.Value>
<Select.Icon> <Select.Icon class="selector-trigger-icon">
<ChevronDown class="w-3 h-3 text-gray-500 dark:text-gray-300" /> <ChevronDown class="w-3 h-3" />
</Select.Icon> </Select.Icon>
</Select.Trigger> </Select.Trigger>
@@ -106,7 +108,7 @@ export default function AgentSelector(props: AgentSelectorProps) {
</Select.Content> </Select.Content>
</Select.Portal> </Select.Portal>
</Select> </Select>
<span class="text-xs text-gray-400 dark:text-gray-500"> <span class="hint">
<Kbd shortcut="cmd+shift+a" /> <Kbd shortcut="cmd+shift+a" />
</span> </span>
</div> </div>

View File

@@ -8,21 +8,20 @@ interface EmptyStateProps {
const EmptyState: Component<EmptyStateProps> = (props) => { const EmptyState: Component<EmptyStateProps> = (props) => {
return ( return (
<div class="flex h-full w-full items-center justify-center" style="background-color: var(--surface-secondary);"> <div class="flex h-full w-full items-center justify-center bg-surface-secondary">
<div class="max-w-[500px] px-8 py-12 text-center"> <div class="max-w-[500px] px-8 py-12 text-center">
<div class="mb-8 flex justify-center"> <div class="mb-8 flex justify-center">
<Folder class="h-16 w-16 text-gray-400 dark:text-gray-600" /> <Folder class="h-16 w-16 icon-muted" />
</div> </div>
<h1 class="mb-4 text-2xl font-semibold text-gray-900 dark:text-gray-100">Welcome to OpenCode Client</h1> <h1 class="mb-4 text-2xl font-semibold text-primary">Welcome to OpenCode Client</h1>
<p class="mb-8 text-base text-gray-600 dark:text-gray-400">Select a folder to start coding with AI</p> <p class="mb-8 text-base text-secondary">Select a folder to start coding with AI</p>
<button <button
onClick={props.onSelectFolder} onClick={props.onSelectFolder}
disabled={props.isLoading} disabled={props.isLoading}
class="mb-4 inline-flex items-center gap-2 rounded-lg px-6 py-3 text-base font-medium transition-colors disabled:cursor-not-allowed disabled:opacity-50 focus:outline-none focus:ring-2 focus:ring-offset-2 hover:opacity-90" class="mb-4 button-primary"
style="background-color: var(--accent-primary); color: var(--text-inverted); ring-color: var(--accent-primary); ring-offset-color: var(--surface-base);"
> >
{props.isLoading ? ( {props.isLoading ? (
<> <>
@@ -34,11 +33,11 @@ const EmptyState: Component<EmptyStateProps> = (props) => {
)} )}
</button> </button>
<p class="text-sm text-gray-500 dark:text-gray-500"> <p class="text-sm text-muted">
Keyboard shortcut: {navigator.platform.includes("Mac") ? "Cmd" : "Ctrl"}+N Keyboard shortcut: {navigator.platform.includes("Mac") ? "Cmd" : "Ctrl"}+N
</p> </p>
<div class="mt-6 space-y-1 text-sm text-gray-400 dark:text-gray-600"> <div class="mt-6 space-y-1 text-sm text-muted">
<p>Examples: ~/projects/my-app</p> <p>Examples: ~/projects/my-app</p>
<p>You can have multiple instances of the same folder</p> <p>You can have multiple instances of the same folder</p>
</div> </div>

View File

@@ -52,9 +52,9 @@ const EnvironmentVariablesEditor: Component<EnvironmentVariablesEditorProps> = (
return ( return (
<div class="space-y-3"> <div class="space-y-3">
<div class="flex items-center gap-2 mb-3"> <div class="flex items-center gap-2 mb-3">
<Globe class="w-4 h-4 text-gray-500 dark:text-gray-400" /> <Globe class="w-4 h-4 icon-muted" />
<span class="text-sm font-medium text-gray-700 dark:text-gray-300">Environment Variables</span> <span class="text-sm font-medium text-secondary">Environment Variables</span>
<span class="text-xs text-gray-500 dark:text-gray-400"> <span class="text-xs text-muted">
({entries().length} variable{entries().length !== 1 ? "s" : ""}) ({entries().length} variable{entries().length !== 1 ? "s" : ""})
</span> </span>
</div> </div>
@@ -66,12 +66,12 @@ const EnvironmentVariablesEditor: Component<EnvironmentVariablesEditorProps> = (
{([key, value]) => ( {([key, value]) => (
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<div class="flex-1 flex items-center gap-2"> <div class="flex-1 flex items-center gap-2">
<Key class="w-3.5 h-3.5 text-gray-400 dark:text-gray-500 flex-shrink-0" /> <Key class="w-3.5 h-3.5 icon-muted flex-shrink-0" />
<input <input
type="text" type="text"
value={key} value={key}
disabled={props.disabled} disabled={props.disabled}
class="flex-1 px-2.5 py-1.5 text-sm bg-gray-50 dark:bg-gray-800 border border-gray-200 dark:border-gray-600 rounded text-gray-500 dark:text-gray-400 cursor-not-allowed" class="flex-1 px-2.5 py-1.5 text-sm bg-surface-secondary border border-base rounded text-muted cursor-not-allowed"
placeholder="Variable name" placeholder="Variable name"
title="Variable name (read-only)" title="Variable name (read-only)"
/> />
@@ -80,14 +80,14 @@ const EnvironmentVariablesEditor: Component<EnvironmentVariablesEditorProps> = (
value={value} value={value}
disabled={props.disabled} disabled={props.disabled}
onInput={(e) => handleUpdateVariable(key, e.currentTarget.value)} onInput={(e) => handleUpdateVariable(key, e.currentTarget.value)}
class="flex-1 px-2.5 py-1.5 text-sm bg-white dark:bg-gray-700 border border-gray-200 dark:border-gray-600 rounded text-gray-900 dark:text-gray-100 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent disabled:opacity-50 disabled:cursor-not-allowed" class="flex-1 px-2.5 py-1.5 text-sm bg-surface-base border border-base rounded text-primary focus-ring-accent disabled:opacity-50 disabled:cursor-not-allowed"
placeholder="Variable value" placeholder="Variable value"
/> />
</div> </div>
<button <button
onClick={() => handleRemoveVariable(key)} onClick={() => handleRemoveVariable(key)}
disabled={props.disabled} disabled={props.disabled}
class="p-1.5 text-gray-400 dark:text-gray-500 hover:text-red-600 dark:hover:text-red-400 disabled:opacity-50 disabled:cursor-not-allowed transition-colors" class="p-1.5 icon-muted icon-danger-hover disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
title="Remove variable" title="Remove variable"
> >
<Trash2 class="w-3.5 h-3.5" /> <Trash2 class="w-3.5 h-3.5" />
@@ -99,16 +99,16 @@ const EnvironmentVariablesEditor: Component<EnvironmentVariablesEditorProps> = (
</Show> </Show>
{/* Add new variable */} {/* Add new variable */}
<div class="flex items-center gap-2 pt-2 border-t border-gray-200 dark:border-gray-600"> <div class="flex items-center gap-2 pt-2 border-t border-base">
<div class="flex-1 flex items-center gap-2"> <div class="flex-1 flex items-center gap-2">
<Key class="w-3.5 h-3.5 text-gray-400 dark:text-gray-500 flex-shrink-0" /> <Key class="w-3.5 h-3.5 icon-muted flex-shrink-0" />
<input <input
type="text" type="text"
value={newKey()} value={newKey()}
onInput={(e) => setNewKey(e.currentTarget.value)} onInput={(e) => setNewKey(e.currentTarget.value)}
onKeyPress={handleKeyPress} onKeyPress={handleKeyPress}
disabled={props.disabled} disabled={props.disabled}
class="flex-1 px-2.5 py-1.5 text-sm bg-white dark:bg-gray-700 border border-gray-200 dark:border-gray-600 rounded text-gray-900 dark:text-gray-100 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent disabled:opacity-50 disabled:cursor-not-allowed" class="flex-1 px-2.5 py-1.5 text-sm bg-surface-base border border-base rounded text-primary focus-ring-accent disabled:opacity-50 disabled:cursor-not-allowed"
placeholder="Variable name" placeholder="Variable name"
/> />
<input <input
@@ -117,14 +117,14 @@ const EnvironmentVariablesEditor: Component<EnvironmentVariablesEditorProps> = (
onInput={(e) => setNewValue(e.currentTarget.value)} onInput={(e) => setNewValue(e.currentTarget.value)}
onKeyPress={handleKeyPress} onKeyPress={handleKeyPress}
disabled={props.disabled} disabled={props.disabled}
class="flex-1 px-2.5 py-1.5 text-sm bg-white dark:bg-gray-700 border border-gray-200 dark:border-gray-600 rounded text-gray-900 dark:text-gray-100 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent disabled:opacity-50 disabled:cursor-not-allowed" class="flex-1 px-2.5 py-1.5 text-sm bg-surface-base border border-base rounded text-primary focus-ring-accent disabled:opacity-50 disabled:cursor-not-allowed"
placeholder="Variable value" placeholder="Variable value"
/> />
</div> </div>
<button <button
onClick={handleAddVariable} onClick={handleAddVariable}
disabled={props.disabled || !newKey().trim()} disabled={props.disabled || !newKey().trim()}
class="p-1.5 text-gray-400 dark:text-gray-500 hover:text-blue-600 dark:hover:text-blue-400 disabled:opacity-50 disabled:cursor-not-allowed transition-colors" class="p-1.5 icon-muted icon-accent-hover disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
title="Add variable" title="Add variable"
> >
<Plus class="w-3.5 h-3.5" /> <Plus class="w-3.5 h-3.5" />
@@ -132,12 +132,12 @@ const EnvironmentVariablesEditor: Component<EnvironmentVariablesEditorProps> = (
</div> </div>
<Show when={entries().length === 0}> <Show when={entries().length === 0}>
<div class="text-xs text-gray-500 dark:text-gray-400 text-center py-2"> <div class="text-xs text-muted text-center py-2">
No environment variables configured. Add variables above to customize the OpenCode environment. No environment variables configured. Add variables above to customize the OpenCode environment.
</div> </div>
</Show> </Show>
<div class="text-xs text-gray-500 dark:text-gray-400 mt-2"> <div class="text-xs text-muted mt-2">
These variables will be available in the OpenCode environment when starting instances. These variables will be available in the OpenCode environment when starting instances.
</div> </div>
</div> </div>

View File

@@ -6,7 +6,7 @@ interface HintRowProps {
} }
const HintRow: Component<HintRowProps> = (props) => { const HintRow: Component<HintRowProps> = (props) => {
return <span class={`text-xs text-gray-500 dark:text-gray-400 ${props.class || ""}`}>{props.children}</span> return <span class={`text-xs text-muted ${props.class || ""}`}>{props.children}</span>
} }
export default HintRow export default HintRow

View File

@@ -81,8 +81,8 @@ const InstanceInfo: Component<InstanceInfoProps> = (props) => {
</div> </div>
<div class="panel-body space-y-3"> <div class="panel-body space-y-3">
<div> <div>
<div class="text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wide mb-1">Folder</div> <div class="text-xs font-medium text-muted uppercase tracking-wide mb-1">Folder</div>
<div class="text-xs text-gray-900 dark:text-gray-100 font-mono break-all px-2 py-1.5 rounded border" style="background-color: var(--surface-secondary); border-color: var(--border-base); color: var(--text-primary);"> <div class="text-xs text-primary font-mono break-all px-2 py-1.5 rounded border bg-surface-secondary border-base">
{props.instance.folder} {props.instance.folder}
</div> </div>
</div> </div>
@@ -91,22 +91,23 @@ const InstanceInfo: Component<InstanceInfoProps> = (props) => {
{(project) => ( {(project) => (
<> <>
<div> <div>
<div class="text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wide mb-1"> <div class="text-xs font-medium text-muted uppercase tracking-wide mb-1">
Project Project
</div> </div>
<div class="text-xs font-mono px-2 py-1.5 rounded border truncate" style="background-color: var(--surface-secondary); border-color: var(--border-base); color: var(--text-primary);"> <div class="text-xs font-mono px-2 py-1.5 rounded border truncate bg-surface-secondary border-base text-primary">
{project().id} {project().id}
</div> </div>
</div> </div>
<Show when={project().vcs}> <Show when={project().vcs}>
<div> <div>
<div class="text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wide mb-1"> <div class="text-xs font-medium text-muted uppercase tracking-wide mb-1">
Version Control Version Control
</div> </div>
<div class="flex items-center gap-2 text-xs text-gray-900 dark:text-gray-100"> <div class="flex items-center gap-2 text-xs text-primary">
<svg <svg
class="w-3.5 h-3.5 text-orange-600 dark:text-orange-500" class="w-3.5 h-3.5"
style="color: var(--status-warning);"
fill="currentColor" fill="currentColor"
viewBox="0 0 24 24" viewBox="0 0 24 24"
> >
@@ -122,10 +123,10 @@ const InstanceInfo: Component<InstanceInfoProps> = (props) => {
<Show when={metadata()?.version}> <Show when={metadata()?.version}>
<div> <div>
<div class="text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wide mb-1"> <div class="text-xs font-medium text-muted uppercase tracking-wide mb-1">
OpenCode Version OpenCode Version
</div> </div>
<div class="text-xs px-2 py-1.5 rounded border" style="background-color: var(--surface-secondary); border-color: var(--border-base); color: var(--text-primary);"> <div class="text-xs px-2 py-1.5 rounded border bg-surface-secondary border-base text-primary">
v{metadata()?.version} v{metadata()?.version}
</div> </div>
</div> </div>
@@ -133,10 +134,10 @@ const InstanceInfo: Component<InstanceInfoProps> = (props) => {
<Show when={props.instance.binaryPath}> <Show when={props.instance.binaryPath}>
<div> <div>
<div class="text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wide mb-1"> <div class="text-xs font-medium text-muted uppercase tracking-wide mb-1">
Binary Path Binary Path
</div> </div>
<div class="text-xs font-mono break-all px-2 py-1.5 rounded border" style="background-color: var(--surface-secondary); border-color: var(--border-base); color: var(--text-primary);"> <div class="text-xs font-mono break-all px-2 py-1.5 rounded border bg-surface-secondary border-base text-primary">
{props.instance.binaryPath} {props.instance.binaryPath}
</div> </div>
</div> </div>
@@ -144,17 +145,17 @@ const InstanceInfo: Component<InstanceInfoProps> = (props) => {
<Show when={props.instance.environmentVariables && Object.keys(props.instance.environmentVariables).length > 0}> <Show when={props.instance.environmentVariables && Object.keys(props.instance.environmentVariables).length > 0}>
<div> <div>
<div class="text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wide mb-1.5"> <div class="text-xs font-medium text-muted uppercase tracking-wide mb-1.5">
Environment Variables ({Object.keys(props.instance.environmentVariables!).length}) Environment Variables ({Object.keys(props.instance.environmentVariables!).length})
</div> </div>
<div class="space-y-1"> <div class="space-y-1">
<For each={Object.entries(props.instance.environmentVariables!)}> <For each={Object.entries(props.instance.environmentVariables!)}>
{([key, value]) => ( {([key, value]) => (
<div class="flex items-center gap-2 px-2 py-1.5 rounded border" style="background-color: var(--surface-secondary); border-color: var(--border-base);"> <div class="flex items-center gap-2 px-2 py-1.5 rounded border bg-surface-secondary border-base">
<span class="text-xs font-mono font-medium flex-1" title={key} style="color: var(--text-primary);"> <span class="text-xs font-mono font-medium flex-1 text-primary" title={key}>
{key} {key}
</span> </span>
<span class="text-xs font-mono flex-1" title={value} style="color: var(--text-secondary);"> <span class="text-xs font-mono flex-1 text-secondary" title={value}>
{value} {value}
</span> </span>
</div> </div>
@@ -166,27 +167,23 @@ const InstanceInfo: Component<InstanceInfoProps> = (props) => {
<Show when={!isLoadingMetadata() && mcpServers().length > 0}> <Show when={!isLoadingMetadata() && mcpServers().length > 0}>
<div> <div>
<div class="text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wide mb-1.5"> <div class="text-xs font-medium text-muted uppercase tracking-wide mb-1.5">
MCP Servers MCP Servers
</div> </div>
<div class="space-y-1.5"> <div class="space-y-1.5">
<For each={mcpServers()}> <For each={mcpServers()}>
{(server) => ( {(server) => (
<div class="flex items-center justify-between px-2 py-1.5 rounded border" style="background-color: var(--surface-secondary); border-color: var(--border-base);"> <div class="flex items-center justify-between px-2 py-1.5 rounded border bg-surface-secondary border-base">
<span class="text-xs text-gray-900 dark:text-gray-100 font-medium truncate">{server.name}</span> <span class="text-xs text-primary font-medium truncate">{server.name}</span>
<div class="flex items-center gap-1.5 flex-shrink-0"> <div class="flex items-center gap-1.5 flex-shrink-0">
<Show <Show when={server.status === "running"}>
when={server.status === "running"} <div class="status-dot ready animate-pulse" />
fallback={ </Show>
<Show <Show when={server.status === "error"}>
when={server.status === "error"} <div class="status-dot error" />
fallback={<div class="w-1.5 h-1.5 rounded-full bg-gray-400" />} </Show>
> <Show when={server.status === "stopped"}>
<div class="w-1.5 h-1.5 rounded-full bg-red-500" /> <div class="status-dot stopped" />
</Show>
}
>
<div class="w-1.5 h-1.5 rounded-full bg-green-500 animate-pulse" />
</Show> </Show>
</div> </div>
</div> </div>
@@ -197,9 +194,9 @@ const InstanceInfo: Component<InstanceInfoProps> = (props) => {
</Show> </Show>
<Show when={isLoadingMetadata()}> <Show when={isLoadingMetadata()}>
<div class="text-xs text-gray-500 dark:text-gray-400 py-1"> <div class="text-xs text-muted py-1">
<div class="flex items-center gap-1.5"> <div class="flex items-center gap-1.5">
<svg class="animate-spin h-3 w-3" fill="none" viewBox="0 0 24 24"> <svg class="animate-spin h-3 w-3 icon-muted" fill="none" viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4" /> <circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4" />
<path <path
class="opacity-75" class="opacity-75"
@@ -213,23 +210,23 @@ const InstanceInfo: Component<InstanceInfoProps> = (props) => {
</Show> </Show>
<div> <div>
<div class="text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wide mb-1.5">Server</div> <div class="text-xs font-medium text-muted uppercase tracking-wide mb-1.5">Server</div>
<div class="space-y-1 text-xs"> <div class="space-y-1 text-xs">
<div class="flex justify-between items-center"> <div class="flex justify-between items-center">
<span class="text-gray-600 dark:text-gray-400">Port:</span> <span class="text-secondary">Port:</span>
<span class="text-gray-900 dark:text-gray-100 font-mono">{props.instance.port}</span> <span class="text-primary font-mono">{props.instance.port}</span>
</div> </div>
<div class="flex justify-between items-center"> <div class="flex justify-between items-center">
<span class="text-gray-600 dark:text-gray-400">PID:</span> <span class="text-secondary">PID:</span>
<span class="text-gray-900 dark:text-gray-100 font-mono">{props.instance.pid}</span> <span class="text-primary font-mono">{props.instance.pid}</span>
</div> </div>
<div class="flex justify-between items-center"> <div class="flex justify-between items-center">
<span class="text-gray-600 dark:text-gray-400">Status:</span> <span class="text-secondary">Status:</span>
<span <span
class={`status-badge ${props.instance.status}`} class={`status-badge ${props.instance.status}`}
> >
<div <div
class={`status-dot ${props.instance.status} ${props.instance.status === "ready" || props.instance.status === "starting" ? "animate-pulse" : ""}`} class={`status-dot ${props.instance.status === "ready" ? "ready" : props.instance.status === "starting" ? "starting" : props.instance.status === "error" ? "error" : "stopped"} ${props.instance.status === "ready" || props.instance.status === "starting" ? "animate-pulse" : ""}`}
/> />
{props.instance.status} {props.instance.status}
</span> </span>

View File

@@ -3,6 +3,7 @@ import type { Instance } from "../types/instance"
import { getParentSessions, createSession, setActiveParentSession, agents } from "../stores/sessions" import { getParentSessions, createSession, setActiveParentSession, agents } from "../stores/sessions"
import InstanceInfo from "./instance-info" import InstanceInfo from "./instance-info"
interface InstanceWelcomeViewProps { interface InstanceWelcomeViewProps {
instance: Instance instance: Instance
} }
@@ -146,14 +147,14 @@ const InstanceWelcomeView: Component<InstanceWelcomeViewProps> = (props) => {
} }
return ( return (
<div class="flex-1 flex flex-col overflow-hidden bg-gray-50 dark:bg-gray-950"> <div class="flex-1 flex flex-col overflow-hidden bg-surface-secondary">
<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 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="panel panel-empty-state flex-shrink-0">
<div class="text-gray-400 dark:text-gray-500 mb-2"> <div class="panel-empty-state-icon">
<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,83 +164,81 @@ const InstanceWelcomeView: Component<InstanceWelcomeViewProps> = (props) => {
/> />
</svg> </svg>
</div> </div>
<p class="text-gray-600 dark:text-gray-200 font-medium text-sm">No Previous Sessions</p> <p class="panel-empty-state-title">No Previous Sessions</p>
<p class="text-xs text-gray-500 dark:text-gray-400">Create a new session below to get started</p> <p class="panel-empty-state-description">Create a new session below to get started</p>
</div> </div>
} }
> >
<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="panel 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"> <div class="panel-header">
<h2 class="text-base font-semibold text-gray-900 dark:text-gray-100">Resume Session</h2> <h2 class="panel-title">Resume Session</h2>
<p class="text-xs text-gray-500 dark:text-gray-400 mt-0.5"> <p class="panel-subtitle">
{parentSessions().length} {parentSessions().length === 1 ? "session" : "sessions"} available {parentSessions().length} {parentSessions().length === 1 ? "session" : "sessions"} available
</p> </p>
</div> </div>
<div class="max-h-[400px] overflow-y-auto"> <div class="panel-list">
<For each={parentSessions()}> <For each={parentSessions()}>
{(session, index) => ( {(session, index) => (
<button <div class="panel-list-item">
data-session-index={index()} <button
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" data-session-index={index()}
classList={{ class="panel-list-item-content group"
"bg-blue-100 dark:bg-blue-900/50 ring-2 ring-blue-500 dark:ring-blue-400 ring-inset": classList={{
focusMode() === "sessions" && selectedIndex() === index(), "panel-list-item-highlight ring-accent-inset":
}} focusMode() === "sessions" && selectedIndex() === index(),
onClick={() => handleSessionSelect(session.id)} }}
onMouseEnter={() => { onClick={() => handleSessionSelect(session.id)}
setFocusMode("sessions") onMouseEnter={() => {
setSelectedIndex(index()) setFocusMode("sessions")
}} setSelectedIndex(index())
> }}
<div class="flex items-center justify-between gap-3"> >
<div class="flex-1 min-w-0"> <div class="flex items-center justify-between gap-3 w-full">
<div class="flex items-center gap-2"> <div class="flex-1 min-w-0">
<span <div class="flex items-center gap-2">
class="text-sm font-medium text-gray-900 dark:text-gray-100 group-hover:text-blue-700 dark:group-hover:text-blue-300 truncate" <span
classList={{ class="text-sm font-medium text-primary truncate transition-colors"
"text-blue-700 dark:text-blue-300": classList={{
focusMode() === "sessions" && selectedIndex() === index(), "text-accent":
}} focusMode() === "sessions" && selectedIndex() === index(),
> }}
{session.title || "Untitled Session"} >
</span> {session.title || "Untitled Session"}
</div> </span>
<div class="flex items-center gap-3 text-xs text-gray-500 dark:text-gray-400 mt-0.5"> </div>
<span>{session.agent}</span> <div class="flex items-center gap-3 text-xs text-muted mt-0.5">
<span></span> <span>{session.agent}</span>
<span>{formatRelativeTime(session.time.updated)}</span> <span></span>
<span>{formatRelativeTime(session.time.updated)}</span>
</div>
</div> </div>
<Show when={focusMode() === "sessions" && selectedIndex() === index()}>
<kbd class="kbd flex-shrink-0"></kbd>
</Show>
</div> </div>
<Show when={focusMode() === "sessions" && selectedIndex() === index()}> </button>
<kbd class="kbd flex-shrink-0"> </div>
</kbd>
</Show>
</div>
</button>
)} )}
</For> </For>
</div> </div>
</div> </div>
</Show> </Show>
<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="panel 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"> <div class="panel-header">
<h2 class="text-base font-semibold text-gray-900 dark:text-gray-100">Start New Session</h2> <h2 class="panel-title">Start New Session</h2>
<p class="text-xs text-gray-500 dark:text-gray-400 mt-0.5"> <p class="panel-subtitle">Create a fresh conversation with your chosen agent</p>
Create a fresh conversation with your chosen agent
</p>
</div> </div>
<div class="p-4"> <div class="panel-body">
<Show <Show
when={agentList().length > 0} when={agentList().length > 0}
fallback={<div class="text-sm text-gray-500 dark:text-gray-400">Loading agents...</div>} fallback={<div class="text-sm text-muted">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 dark:text-gray-300 mb-1.5">Agent</label> <label class="block text-xs font-medium text-secondary mb-1.5">Agent</label>
<select <select
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" class="selector-input w-full"
value={selectedAgent()} value={selectedAgent()}
onChange={(e) => setSelectedAgent(e.currentTarget.value)} onChange={(e) => setSelectedAgent(e.currentTarget.value)}
> >
@@ -255,14 +254,15 @@ 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 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" type="button"
class="selector-button selector-button-primary w-full flex items-center justify-between gap-2 font-medium"
onClick={handleNewSession} onClick={handleNewSession}
disabled={isCreating() || agentList().length === 0} disabled={isCreating() || agentList().length === 0}
> >
<Show <Show
when={!isCreating()} when={!isCreating()}
fallback={ fallback={
<> <div class="flex items-center gap-2 w-full justify-center text-sm">
<svg class="animate-spin h-4 w-4" fill="none" viewBox="0 0 24 24"> <svg class="animate-spin h-4 w-4" fill="none" viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4" /> <circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4" />
<path <path
@@ -272,18 +272,16 @@ const InstanceWelcomeView: Component<InstanceWelcomeViewProps> = (props) => {
/> />
</svg> </svg>
Creating... Creating...
</> </div>
} }
> >
<div class="flex items-center gap-2 flex-1 justify-center"> <div class="flex items-center gap-2 flex-1 justify-center text-sm">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4" /> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4" />
</svg> </svg>
<span>Create Session</span> <span>Create Session</span>
</div> </div>
<kbd class="kbd flex-shrink-0"> <kbd class="kbd flex-shrink-0">Cmd+Enter</kbd>
Cmd+Enter
</kbd>
</Show> </Show>
</button> </button>
</div> </div>
@@ -299,8 +297,8 @@ const InstanceWelcomeView: Component<InstanceWelcomeViewProps> = (props) => {
</div> </div>
</div> </div>
<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="panel-footer">
<div class="flex items-center justify-center flex-wrap gap-3 text-xs text-gray-500 dark:text-gray-400"> <div class="panel-footer-hints">
<div class="flex items-center gap-1.5"> <div class="flex items-center gap-1.5">
<kbd class="kbd"></kbd> <kbd class="kbd"></kbd>
<kbd class="kbd"></kbd> <kbd class="kbd"></kbd>
@@ -331,3 +329,4 @@ const InstanceWelcomeView: Component<InstanceWelcomeViewProps> = (props) => {
} }
export default InstanceWelcomeView export default InstanceWelcomeView

View File

@@ -354,10 +354,10 @@ const OpenCodeBinarySelector: Component<OpenCodeBinarySelectorProps> = (props) =
</div> </div>
<button <button
onClick={(e) => handleRemoveBinary(binary.path, e)} onClick={(e) => handleRemoveBinary(binary.path, e)}
class="opacity-0 group-hover:opacity-100 p-1 hover:bg-red-100 dark:hover:bg-red-900/30 rounded transition-all" class="remove-binary-button"
title="Remove binary" title="Remove binary"
> >
<Trash2 class="w-3.5 h-3.5 text-gray-400 dark:text-gray-500 hover:text-red-600 dark:hover:text-red-400" /> <Trash2 class="w-3.5 h-3.5" />
</button> </button>
</div> </div>
</div> </div>

View File

@@ -64,39 +64,38 @@ const SessionPicker: Component<SessionPickerProps> = (props) => {
return ( return (
<Dialog open={props.open} onOpenChange={(open) => !open && handleCancel()}> <Dialog open={props.open} onOpenChange={(open) => !open && handleCancel()}>
<Dialog.Portal> <Dialog.Portal>
<Dialog.Overlay class="fixed inset-0 bg-black/50 z-50" /> <Dialog.Overlay class="modal-overlay" />
<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="modal-surface w-full max-w-lg p-6"> <Dialog.Content class="modal-surface w-full max-w-lg p-6">
<Dialog.Title class="text-xl font-semibold mb-4" style="color: var(--text-primary);"> <Dialog.Title class="text-xl font-semibold text-primary 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={ fallback={<div class="text-center py-4 text-sm text-muted">No previous sessions</div>}
<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 dark:text-gray-300 mb-2"> <h3 class="text-sm font-medium text-secondary mb-2">
Resume a session ({parentSessions().length}): Resume a session ({parentSessions().length}):
</h3> </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 transition-colors group hover:bg-gray-100 dark:hover:bg-gray-800" type="button"
class="selector-option w-full text-left"
onClick={() => handleSessionSelect(session.id)} onClick={() => handleSessionSelect(session.id)}
> >
<div class="flex justify-between items-start"> <div class="selector-option-content">
<span class="text-sm text-gray-900 dark:text-gray-100 truncate flex-1"> <span class="selector-option-label truncate">
{session.title || "Untitled"} {session.title || "Untitled"}
</span> </span>
<span class="text-xs text-gray-500 dark:text-gray-400 ml-2 flex-shrink-0">
{formatRelativeTime(session.time.updated)}
</span>
</div> </div>
<span class="selector-badge-time flex-shrink-0">
{formatRelativeTime(session.time.updated)}
</span>
</button> </button>
)} )}
</For> </For>
@@ -106,22 +105,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 dark:border-gray-700" /> <div class="w-full border-t border-base" />
</div> </div>
<div class="relative flex justify-center text-sm"> <div class="relative flex justify-center text-sm">
<span class="px-2" style="background-color: var(--surface-base); color: var(--text-muted);">or</span> <span class="px-2 bg-surface-base text-muted">or</span>
</div> </div>
</div> </div>
<div> <div>
<h3 class="text-sm font-medium mb-2" style="color: var(--text-secondary);">Start new session:</h3> <h3 class="text-sm font-medium text-secondary 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 dark:text-gray-400">Loading agents...</div>} fallback={<div class="text-sm text-muted">Loading agents...</div>}
> >
<select <select
class="selector-input" class="selector-input w-full"
value={selectedAgent()} value={selectedAgent()}
onChange={(e) => setSelectedAgent(e.currentTarget.value)} onChange={(e) => setSelectedAgent(e.currentTarget.value)}
> >
@@ -130,7 +129,7 @@ const SessionPicker: Component<SessionPickerProps> = (props) => {
</Show> </Show>
<button <button
class="selector-button-primary w-full" class="selector-button selector-button-primary w-full"
onClick={handleNewSession} onClick={handleNewSession}
disabled={isCreating() || agentList().length === 0} disabled={isCreating() || agentList().length === 0}
> >
@@ -142,7 +141,8 @@ const SessionPicker: Component<SessionPickerProps> = (props) => {
<div class="mt-6 flex justify-end"> <div class="mt-6 flex justify-end">
<button <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" type="button"
class="selector-button selector-button-secondary"
onClick={handleCancel} onClick={handleCancel}
> >
Cancel Cancel

View File

@@ -1,6 +1,83 @@
/* Reusable component utilities using tokens */ /* Reusable component utilities using tokens */
/* Base token utility helpers */
.text-primary {
color: var(--text-primary);
}
.text-secondary {
color: var(--text-secondary);
}
.text-muted {
color: var(--text-muted);
}
.text-inverted {
color: var(--text-inverted);
}
.text-accent {
color: var(--accent-primary);
}
.bg-surface-base {
background-color: var(--surface-base);
}
.bg-surface-secondary {
background-color: var(--surface-secondary);
}
.bg-surface-muted {
background-color: var(--surface-muted);
}
.border-base {
border-color: var(--border-base);
}
.icon-muted {
color: var(--text-muted);
}
.icon-accent {
color: var(--accent-primary);
}
.icon-danger-hover:hover {
color: var(--status-error);
}
.icon-accent-hover:hover {
color: var(--accent-primary);
}
.ring-accent-inset {
box-shadow: inset 0 0 0 2px var(--accent-primary);
}
.button-primary {
@apply inline-flex items-center justify-center gap-2 rounded-lg px-6 py-3 text-base font-medium transition-colors;
background-color: var(--accent-primary);
color: var(--text-inverted);
}
.button-primary:hover:not(:disabled) {
opacity: 0.9;
}
.button-primary:focus-visible {
outline: none;
box-shadow: 0 0 0 2px var(--surface-base), 0 0 0 4px var(--accent-primary);
}
.button-primary:disabled {
@apply cursor-not-allowed opacity-50;
}
/* Message item base styles */ /* Message item base styles */
.message-item-base { .message-item-base {
@apply flex flex-col gap-2 p-3 rounded-lg w-full; @apply flex flex-col gap-2 p-3 rounded-lg w-full;
} }
@@ -960,18 +1037,28 @@
background-color: var(--surface-hover); background-color: var(--surface-hover);
} }
.selector-option-highlighted { .selector-option[data-highlighted],
.selector-option[data-focused] {
background-color: rgba(0, 102, 255, 0.1); background-color: rgba(0, 102, 255, 0.1);
} }
[data-theme="dark"] .selector-option-highlighted { [data-theme="dark"] .selector-option[data-highlighted],
[data-theme="dark"] .selector-option[data-focused] {
background-color: rgba(0, 128, 255, 0.2); background-color: rgba(0, 128, 255, 0.2);
} }
.selector-option[data-selected] {
background-color: rgba(0, 102, 255, 0.15);
}
.selector-option-selected { .selector-option-selected {
background-color: rgba(0, 102, 255, 0.15); background-color: rgba(0, 102, 255, 0.15);
} }
[data-theme="dark"] .selector-option[data-selected] {
background-color: rgba(0, 128, 255, 0.25);
}
[data-theme="dark"] .selector-option-selected { [data-theme="dark"] .selector-option-selected {
background-color: rgba(0, 128, 255, 0.25); background-color: rgba(0, 128, 255, 0.25);
} }
@@ -990,6 +1077,28 @@
color: var(--text-muted); color: var(--text-muted);
} }
.selector-option .remove-binary-button {
opacity: 0;
}
.selector-option:hover .remove-binary-button {
opacity: 1;
}
.remove-binary-button {
@apply p-1 rounded transition-all;
color: var(--text-muted);
}
.remove-binary-button:hover {
background-color: rgba(239, 68, 68, 0.1);
color: var(--status-error);
}
[data-theme="dark"] .remove-binary-button:hover {
background-color: rgba(239, 68, 68, 0.2);
}
.selector-option-indicator { .selector-option-indicator {
@apply flex-shrink-0 mt-0.5; @apply flex-shrink-0 mt-0.5;
color: var(--accent-primary); color: var(--accent-primary);
@@ -1106,6 +1215,12 @@
@apply opacity-50 cursor-not-allowed; @apply opacity-50 cursor-not-allowed;
} }
.focus-ring-accent:focus {
outline: none;
border-color: transparent;
box-shadow: 0 0 0 2px var(--accent-primary);
}
.selector-empty-state { .selector-empty-state {
@apply p-4 text-center text-sm; @apply p-4 text-center text-sm;
color: var(--text-muted); color: var(--text-muted);