refactor: restructure session sidebar layout
This commit is contained in:
128
src/App.tsx
128
src/App.tsx
@@ -9,6 +9,9 @@ import SessionList from "./components/session-list"
|
||||
import MessageStream from "./components/message-stream"
|
||||
import PromptInput from "./components/prompt-input"
|
||||
import InfoView from "./components/info-view"
|
||||
import AgentSelector from "./components/agent-selector"
|
||||
import ModelSelector from "./components/model-selector"
|
||||
import KeyboardHint from "./components/keyboard-hint"
|
||||
import { initMarkdown } from "./lib/markdown"
|
||||
import { useTheme } from "./lib/theme"
|
||||
import { createCommandRegistry } from "./lib/commands"
|
||||
@@ -78,14 +81,6 @@ const SessionView: Component<{
|
||||
await sendMessage(props.instanceId, props.sessionId, prompt, attachments)
|
||||
}
|
||||
|
||||
async function handleAgentChange(agent: string) {
|
||||
await updateSessionAgent(props.instanceId, props.sessionId, agent)
|
||||
}
|
||||
|
||||
async function handleModelChange(model: { providerId: string; modelId: string }) {
|
||||
await updateSessionModel(props.instanceId, props.sessionId, model)
|
||||
}
|
||||
|
||||
async function handleRevert(messageId: string) {
|
||||
const instance = instances().get(props.instanceId)
|
||||
if (!instance || !instance.client) return
|
||||
@@ -148,10 +143,6 @@ const SessionView: Component<{
|
||||
instanceFolder={props.instanceFolder}
|
||||
sessionId={s().id}
|
||||
onSend={handleSendMessage}
|
||||
agent={s().agent}
|
||||
model={s().model}
|
||||
onAgentChange={handleAgentChange}
|
||||
onModelChange={handleModelChange}
|
||||
escapeInDebounce={props.escapeInDebounce}
|
||||
/>
|
||||
</div>
|
||||
@@ -189,6 +180,29 @@ const App: Component = () => {
|
||||
return activeSessionId().get(instance.id) || null
|
||||
})
|
||||
|
||||
const activeSessionForInstance = createMemo(() => {
|
||||
const sessionId = activeSessionIdForInstance()
|
||||
if (!sessionId || sessionId === "info") return null
|
||||
return activeSessions().get(sessionId) ?? null
|
||||
})
|
||||
|
||||
const handleSidebarAgentChange = async (agent: string) => {
|
||||
const instance = activeInstance()
|
||||
const sessionId = activeSessionIdForInstance()
|
||||
if (!instance || !sessionId || sessionId === "info") return
|
||||
await updateSessionAgent(instance.id, sessionId, agent)
|
||||
}
|
||||
|
||||
const handleSidebarModelChange = async (model: { providerId: string; modelId: string }) => {
|
||||
const instance = activeInstance()
|
||||
const sessionId = activeSessionIdForInstance()
|
||||
if (!instance || !sessionId || sessionId === "info") return
|
||||
await updateSessionModel(instance.id, sessionId, model)
|
||||
}
|
||||
|
||||
const DEFAULT_SESSION_SIDEBAR_WIDTH = 280
|
||||
const [sessionSidebarWidth, setSessionSidebarWidth] = createSignal(DEFAULT_SESSION_SIDEBAR_WIDTH)
|
||||
|
||||
async function handleSelectFolder(folderPath?: string, binaryPath?: string) {
|
||||
setIsSelectingFolder(true)
|
||||
try {
|
||||
@@ -813,24 +827,76 @@ const App: Component = () => {
|
||||
{(instance) => (
|
||||
<>
|
||||
<Show when={activeSessions().size > 0} fallback={<InstanceWelcomeView instance={instance()} />}>
|
||||
<div class="flex h-full">
|
||||
{/* Session List Sidebar */}
|
||||
<SessionList
|
||||
instanceId={instance().id}
|
||||
sessions={activeSessions()}
|
||||
activeSessionId={activeSessionIdForInstance()}
|
||||
onSelect={(id) => setActiveSession(instance().id, id)}
|
||||
onClose={(id) => handleCloseSession(instance().id, id)}
|
||||
onNew={() => handleNewSession(instance().id)}
|
||||
/>
|
||||
<div class="flex flex-1 min-h-0">
|
||||
{/* Session Sidebar */}
|
||||
<div
|
||||
class="session-sidebar flex flex-col bg-surface-secondary"
|
||||
style={{ width: `${sessionSidebarWidth()}px` }}
|
||||
>
|
||||
<SessionList
|
||||
instanceId={instance().id}
|
||||
sessions={activeSessions()}
|
||||
activeSessionId={activeSessionIdForInstance()}
|
||||
onSelect={(id) => setActiveSession(instance().id, id)}
|
||||
onClose={(id) => handleCloseSession(instance().id, id)}
|
||||
onNew={() => handleNewSession(instance().id)}
|
||||
showHeader
|
||||
showFooter={false}
|
||||
headerContent={
|
||||
<div class="session-sidebar-header">
|
||||
<span class="session-sidebar-title text-sm font-semibold text-primary">Sessions</span>
|
||||
<div class="session-sidebar-shortcuts">
|
||||
{(() => {
|
||||
const shortcut = keyboardRegistry.get("session-prev")
|
||||
return shortcut ? <KeyboardHint shortcuts={[shortcut]} separator="" /> : null
|
||||
})()}
|
||||
{(() => {
|
||||
const shortcut = keyboardRegistry.get("session-next")
|
||||
return shortcut ? <KeyboardHint shortcuts={[shortcut]} separator="" /> : null
|
||||
})()}
|
||||
</div>
|
||||
<button
|
||||
class="session-sidebar-new inline-flex items-center justify-center gap-1.5 rounded-md border border-base px-3 py-2 text-xs font-semibold transition-colors hover:border-accent-primary hover:text-accent-primary"
|
||||
onClick={() => handleNewSession(instance().id)}
|
||||
type="button"
|
||||
aria-label="Create new session"
|
||||
title="New session (Cmd/Ctrl+Shift+N)"
|
||||
>
|
||||
<span class="leading-none">New Session</span>
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
|
||||
onWidthChange={setSessionSidebarWidth}
|
||||
/>
|
||||
<div class="session-sidebar-separator border-t border-base" />
|
||||
<Show when={activeSessionForInstance()}>
|
||||
{(activeSession) => (
|
||||
<div class="session-sidebar-controls px-3 py-3 border-r border-base flex flex-col gap-3">
|
||||
<AgentSelector
|
||||
instanceId={instance().id}
|
||||
sessionId={activeSession().id}
|
||||
currentAgent={activeSession().agent}
|
||||
onAgentChange={handleSidebarAgentChange}
|
||||
/>
|
||||
<ModelSelector
|
||||
instanceId={instance().id}
|
||||
sessionId={activeSession().id}
|
||||
currentModel={activeSession().model}
|
||||
onModelChange={handleSidebarModelChange}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</Show>
|
||||
</div>
|
||||
{/* Main Content Area */}
|
||||
<div class="content-area flex-1 overflow-hidden flex flex-col">
|
||||
<div class="content-area flex-1 min-h-0 overflow-hidden flex flex-col">
|
||||
<Show
|
||||
when={activeSessionIdForInstance() === "info"}
|
||||
fallback={
|
||||
<Show
|
||||
when={activeSessionIdForInstance()}
|
||||
keyed
|
||||
fallback={
|
||||
<div class="flex items-center justify-center h-full">
|
||||
<div class="text-center text-gray-500 dark:text-gray-400">
|
||||
@@ -840,13 +906,15 @@ const App: Component = () => {
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<SessionView
|
||||
sessionId={activeSessionIdForInstance()!}
|
||||
activeSessions={activeSessions()}
|
||||
instanceId={activeInstance()!.id}
|
||||
instanceFolder={activeInstance()!.folder}
|
||||
escapeInDebounce={escapeInDebounce()}
|
||||
/>
|
||||
{(sessionId) => (
|
||||
<SessionView
|
||||
sessionId={sessionId}
|
||||
activeSessions={activeSessions()}
|
||||
instanceId={activeInstance()!.id}
|
||||
instanceFolder={activeInstance()!.folder}
|
||||
escapeInDebounce={escapeInDebounce()}
|
||||
/>
|
||||
)}
|
||||
</Show>
|
||||
}
|
||||
>
|
||||
|
||||
@@ -53,7 +53,7 @@ export default function AgentSelector(props: AgentSelectorProps) {
|
||||
}
|
||||
|
||||
return (
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="sidebar-selector">
|
||||
<Select
|
||||
value={availableAgents().find((a) => a.name === props.currentAgent)}
|
||||
onChange={handleChange}
|
||||
@@ -108,7 +108,7 @@ export default function AgentSelector(props: AgentSelectorProps) {
|
||||
</Select.Content>
|
||||
</Select.Portal>
|
||||
</Select>
|
||||
<span class="hint">
|
||||
<span class="hint sidebar-selector-hint">
|
||||
<Kbd shortcut="cmd+shift+a" />
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -63,7 +63,7 @@ export default function ModelSelector(props: ModelSelectorProps) {
|
||||
})
|
||||
|
||||
return (
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="sidebar-selector">
|
||||
<Combobox<FlatModel>
|
||||
value={currentModelValue()}
|
||||
onChange={handleChange}
|
||||
@@ -97,14 +97,14 @@ export default function ModelSelector(props: ModelSelectorProps) {
|
||||
</Combobox.Item>
|
||||
)}
|
||||
>
|
||||
<Combobox.Control class="relative" data-model-selector-control>
|
||||
<Combobox.Control class="relative w-full" data-model-selector-control>
|
||||
<Combobox.Input class="sr-only" data-model-selector />
|
||||
<Combobox.Trigger
|
||||
ref={triggerRef}
|
||||
class="selector-trigger"
|
||||
>
|
||||
<div class="selector-trigger-label">
|
||||
<span class="selector-trigger-primary">
|
||||
<div class="selector-trigger-label selector-trigger-label--stacked">
|
||||
<span class="selector-trigger-primary selector-trigger-primary--align-left">
|
||||
Model: {currentModelValue()?.name ?? "None"}
|
||||
</span>
|
||||
{currentModelValue() && (
|
||||
@@ -132,7 +132,7 @@ export default function ModelSelector(props: ModelSelectorProps) {
|
||||
</Combobox.Content>
|
||||
</Combobox.Portal>
|
||||
</Combobox>
|
||||
<span class="hint">
|
||||
<span class="hint sidebar-selector-hint">
|
||||
<Kbd shortcut="cmd+shift+m" />
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
import { createSignal, Show, onMount, For, onCleanup } from "solid-js"
|
||||
import AgentSelector from "./agent-selector"
|
||||
import ModelSelector from "./model-selector"
|
||||
import UnifiedPicker from "./unified-picker"
|
||||
import { addToHistory, getHistory } from "../stores/message-history"
|
||||
import { getAttachments, addAttachment, clearAttachments, removeAttachment } from "../stores/attachments"
|
||||
@@ -17,10 +15,6 @@ interface PromptInputProps {
|
||||
sessionId: string
|
||||
onSend: (prompt: string, attachments: Attachment[]) => Promise<void>
|
||||
disabled?: boolean
|
||||
agent: string
|
||||
model: { providerId: string; modelId: string }
|
||||
onAgentChange: (agent: string) => Promise<void>
|
||||
onModelChange: (model: { providerId: string; modelId: string }) => Promise<void>
|
||||
escapeInDebounce?: boolean
|
||||
}
|
||||
|
||||
@@ -730,37 +724,25 @@ export default function PromptInput(props: PromptInputProps) {
|
||||
</button>
|
||||
</div>
|
||||
<div class="prompt-input-hints">
|
||||
<HintRow>
|
||||
<Show
|
||||
when={props.escapeInDebounce}
|
||||
fallback={
|
||||
<>
|
||||
<Kbd>Enter</Kbd> to send • <Kbd>Shift+Enter</Kbd> for new line • <Kbd>@</Kbd> for files/agents •{" "}
|
||||
<Kbd>↑↓</Kbd> for history
|
||||
<Show when={attachments().length > 0}>
|
||||
<span class="ml-2 text-xs" style="color: var(--text-muted);">• {attachments().length} file(s) attached</span>
|
||||
</Show>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<span class="font-medium" style="color: var(--status-warning);">
|
||||
Press <Kbd>Esc</Kbd> again to abort session
|
||||
</span>
|
||||
</Show>
|
||||
</HintRow>
|
||||
<div class="flex items-center gap-2">
|
||||
<AgentSelector
|
||||
instanceId={props.instanceId}
|
||||
sessionId={props.sessionId}
|
||||
currentAgent={props.agent}
|
||||
onAgentChange={props.onAgentChange}
|
||||
/>
|
||||
<ModelSelector
|
||||
instanceId={props.instanceId}
|
||||
sessionId={props.sessionId}
|
||||
currentModel={props.model}
|
||||
onModelChange={props.onModelChange}
|
||||
/>
|
||||
<div class="flex justify-end">
|
||||
<HintRow>
|
||||
<Show
|
||||
when={props.escapeInDebounce}
|
||||
fallback={
|
||||
<>
|
||||
<Kbd>Enter</Kbd> to send • <Kbd>Shift+Enter</Kbd> for new line • <Kbd>@</Kbd> for files/agents •{" "}
|
||||
<Kbd>↑↓</Kbd> for history
|
||||
<Show when={attachments().length > 0}>
|
||||
<span class="ml-2 text-xs" style="color: var(--text-muted);">• {attachments().length} file(s) attached</span>
|
||||
</Show>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<span class="font-medium" style="color: var(--status-warning);">
|
||||
Press <Kbd>Esc</Kbd> again to abort session
|
||||
</span>
|
||||
</Show>
|
||||
</HintRow>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Component, For, Show, createSignal, createEffect, onCleanup, onMount, createMemo } from "solid-js"
|
||||
import { Component, For, Show, createSignal, createEffect, onCleanup, onMount, createMemo, JSX } from "solid-js"
|
||||
import type { Session } from "../types/session"
|
||||
import { MessageSquare, Info, Plus, X } from "lucide-solid"
|
||||
import KeyboardHint from "./keyboard-hint"
|
||||
@@ -21,6 +21,11 @@ interface SessionListProps {
|
||||
onSelect: (sessionId: string) => void
|
||||
onClose: (sessionId: string) => void
|
||||
onNew: () => void
|
||||
showHeader?: boolean
|
||||
showFooter?: boolean
|
||||
headerContent?: JSX.Element
|
||||
footerContent?: JSX.Element
|
||||
onWidthChange?: (width: number) => void
|
||||
}
|
||||
|
||||
const MIN_WIDTH = 200
|
||||
@@ -57,6 +62,10 @@ const SessionList: Component<SessionListProps> = (props) => {
|
||||
window.localStorage.setItem(STORAGE_KEY, width.toString())
|
||||
})
|
||||
|
||||
createEffect(() => {
|
||||
props.onWidthChange?.(sidebarWidth())
|
||||
})
|
||||
|
||||
const clampWidth = (width: number) => Math.max(MIN_WIDTH, Math.min(MAX_WIDTH, width))
|
||||
|
||||
const removeMouseListeners = () => {
|
||||
@@ -194,14 +203,18 @@ const SessionList: Component<SessionListProps> = (props) => {
|
||||
aria-hidden="true"
|
||||
/>
|
||||
|
||||
<div class="session-list-header p-3 border-b border-base">
|
||||
<div class="flex items-center justify-between gap-3">
|
||||
<h3 class="text-sm font-semibold text-primary">Sessions</h3>
|
||||
<KeyboardHint
|
||||
shortcuts={[keyboardRegistry.get("session-prev")!, keyboardRegistry.get("session-next")!].filter(Boolean)}
|
||||
/>
|
||||
<Show when={props.showHeader !== false}>
|
||||
<div class="session-list-header p-3 border-b border-base">
|
||||
{props.headerContent ?? (
|
||||
<div class="flex items-center justify-between gap-3">
|
||||
<h3 class="text-sm font-semibold text-primary">Sessions</h3>
|
||||
<KeyboardHint
|
||||
shortcuts={[keyboardRegistry.get("session-prev")!, keyboardRegistry.get("session-next")!].filter(Boolean)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</Show>
|
||||
|
||||
<div class="session-list flex-1 overflow-y-auto">
|
||||
<div class="session-section">
|
||||
@@ -289,17 +302,21 @@ const SessionList: Component<SessionListProps> = (props) => {
|
||||
</Show>
|
||||
</div>
|
||||
|
||||
<div class="session-list-footer p-3 border-t border-base">
|
||||
<button
|
||||
class="session-new-button w-full flex items-center gap-2 px-3 py-2 rounded-md transition-colors text-sm font-medium"
|
||||
onClick={props.onNew}
|
||||
title="New session (Cmd/Ctrl+T)"
|
||||
aria-label="New session"
|
||||
>
|
||||
<Plus class="w-4 h-4" />
|
||||
<span>New Session</span>
|
||||
</button>
|
||||
</div>
|
||||
<Show when={props.showFooter !== false}>
|
||||
<div class="session-list-footer p-3 border-t border-base">
|
||||
{props.footerContent ?? (
|
||||
<button
|
||||
class="session-new-button w-full flex items-center gap-2 px-3 py-2 rounded-md transition-colors text-sm font-medium"
|
||||
onClick={props.onNew}
|
||||
title="New session (Cmd/Ctrl+T)"
|
||||
aria-label="New session"
|
||||
>
|
||||
<Plus class="w-4 h-4" />
|
||||
<span>New Session</span>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -491,7 +491,7 @@ button.button-primary {
|
||||
|
||||
/* Message stream component utilities */
|
||||
.message-stream-container {
|
||||
@apply relative flex-1 flex flex-col overflow-hidden;
|
||||
@apply relative flex-1 min-h-0 flex flex-col overflow-hidden;
|
||||
}
|
||||
|
||||
.connection-status {
|
||||
@@ -527,7 +527,7 @@ button.button-primary {
|
||||
}
|
||||
|
||||
.message-stream {
|
||||
@apply flex-1 overflow-y-auto p-4 flex flex-col gap-4;
|
||||
@apply flex-1 min-h-0 overflow-y-auto p-4 flex flex-col gap-4;
|
||||
background-color: var(--surface-base);
|
||||
color: inherit;
|
||||
}
|
||||
@@ -1060,17 +1060,25 @@ button.button-primary {
|
||||
}
|
||||
|
||||
.selector-trigger-label {
|
||||
@apply flex flex-col items-start min-w-0;
|
||||
@apply flex flex-col min-w-0;
|
||||
}
|
||||
|
||||
.selector-trigger-label--stacked {
|
||||
@apply items-start;
|
||||
}
|
||||
|
||||
.selector-trigger-primary {
|
||||
@apply text-sm font-medium truncate;
|
||||
color: var(--text-primary);
|
||||
font-weight: var(--font-weight-medium);
|
||||
}
|
||||
|
||||
.selector-trigger-primary--align-left {
|
||||
@apply text-left w-full;
|
||||
}
|
||||
|
||||
.selector-trigger-secondary {
|
||||
@apply text-xs text-left truncate;
|
||||
color: var(--text-muted);
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
.selector-trigger-icon {
|
||||
@@ -1622,19 +1630,71 @@ button.button-primary {
|
||||
|
||||
/* Session view utility */
|
||||
.session-view {
|
||||
@apply flex flex-col h-full;
|
||||
@apply flex flex-1 min-h-0 flex-col;
|
||||
background-color: var(--surface-base);
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
/* Session list component */
|
||||
.session-list-container {
|
||||
@apply flex flex-col h-full relative;
|
||||
@apply flex flex-col flex-1 min-h-0 relative;
|
||||
background-color: var(--surface-secondary);
|
||||
min-width: 200px;
|
||||
max-width: 500px;
|
||||
}
|
||||
|
||||
.session-sidebar {
|
||||
@apply flex flex-col min-h-0;
|
||||
background-color: var(--surface-secondary);
|
||||
}
|
||||
|
||||
.session-sidebar-header {
|
||||
@apply flex flex-col gap-2 w-full;
|
||||
}
|
||||
|
||||
.session-sidebar-title {
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.session-sidebar-shortcuts {
|
||||
@apply flex flex-col gap-1;
|
||||
}
|
||||
|
||||
.session-sidebar-new {
|
||||
@apply w-full;
|
||||
}
|
||||
|
||||
.session-sidebar-controls {
|
||||
@apply flex flex-col gap-3;
|
||||
background-color: var(--surface-secondary);
|
||||
}
|
||||
|
||||
.session-sidebar-controls > * {
|
||||
@apply w-full;
|
||||
}
|
||||
|
||||
.session-sidebar-controls .selector-trigger,
|
||||
.session-sidebar-controls [data-model-selector-control],
|
||||
.session-sidebar-controls .selector-trigger-label,
|
||||
.session-sidebar-controls .selector-trigger-primary {
|
||||
@apply w-full;
|
||||
}
|
||||
|
||||
.sidebar-selector {
|
||||
@apply flex flex-col gap-1 w-full;
|
||||
}
|
||||
|
||||
.sidebar-selector-hint {
|
||||
@apply flex justify-center text-xs w-full;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.session-sidebar-separator {
|
||||
background-color: var(--border-base);
|
||||
height: 1px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.session-resize-handle {
|
||||
@apply absolute top-0 right-0 w-1 h-full cursor-col-resize bg-transparent transition-colors;
|
||||
z-index: 10;
|
||||
|
||||
Reference in New Issue
Block a user