Improve session defaults and onboarding UI
This commit is contained in:
@@ -40,6 +40,14 @@ export default function AgentSelector(props: AgentSelectorProps) {
|
|||||||
return filtered
|
return filtered
|
||||||
})
|
})
|
||||||
|
|
||||||
|
createEffect(() => {
|
||||||
|
const list = availableAgents()
|
||||||
|
if (list.length === 0) return
|
||||||
|
if (!list.some((agent) => agent.name === props.currentAgent)) {
|
||||||
|
void props.onAgentChange(list[0].name)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
if (instanceAgents().length === 0) {
|
if (instanceAgents().length === 0) {
|
||||||
fetchAgents(props.instanceId).catch(console.error)
|
fetchAgents(props.instanceId).catch(console.error)
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { Folder, Clock, Trash2, FolderPlus, Settings, ChevronDown, ChevronUp } f
|
|||||||
import { recentFolders, removeRecentFolder, preferences, updateLastUsedBinary } from "../stores/preferences"
|
import { recentFolders, removeRecentFolder, preferences, updateLastUsedBinary } from "../stores/preferences"
|
||||||
import OpenCodeBinarySelector from "./opencode-binary-selector"
|
import OpenCodeBinarySelector from "./opencode-binary-selector"
|
||||||
import EnvironmentVariablesEditor from "./environment-variables-editor"
|
import EnvironmentVariablesEditor from "./environment-variables-editor"
|
||||||
|
import Kbd from "./kbd"
|
||||||
|
|
||||||
interface FolderSelectionViewProps {
|
interface FolderSelectionViewProps {
|
||||||
onSelectFolder: (folder?: string, binaryPath?: string) => void
|
onSelectFolder: (folder?: string, binaryPath?: string) => void
|
||||||
@@ -14,28 +15,30 @@ const FolderSelectionView: Component<FolderSelectionViewProps> = (props) => {
|
|||||||
const [focusMode, setFocusMode] = createSignal<"recent" | "new" | null>("recent")
|
const [focusMode, setFocusMode] = createSignal<"recent" | "new" | null>("recent")
|
||||||
const [showAdvanced, setShowAdvanced] = createSignal(false)
|
const [showAdvanced, setShowAdvanced] = createSignal(false)
|
||||||
const [selectedBinary, setSelectedBinary] = createSignal(preferences().lastUsedBinary || "opencode")
|
const [selectedBinary, setSelectedBinary] = createSignal(preferences().lastUsedBinary || "opencode")
|
||||||
|
let recentListRef: HTMLDivElement | undefined
|
||||||
|
|
||||||
const folders = () => recentFolders()
|
const folders = () => recentFolders()
|
||||||
|
|
||||||
// Update selected binary when preferences change
|
// Update selected binary when preferences change
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
const lastUsed = preferences().lastUsedBinary
|
const lastUsed = preferences().lastUsedBinary
|
||||||
if (lastUsed && lastUsed !== selectedBinary()) {
|
if (lastUsed && lastUsed !== selectedBinary()) {
|
||||||
setSelectedBinary(lastUsed)
|
setSelectedBinary(lastUsed)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
function scrollToIndex(index: number) {
|
function scrollToIndex(index: number) {
|
||||||
const element = document.querySelector(`[data-folder-index="${index}"]`)
|
const element = recentListRef?.querySelector(`[data-folder-index="${index}"]`)
|
||||||
if (element) {
|
if (element) {
|
||||||
element.scrollIntoView({ block: "nearest", behavior: "auto" })
|
element.scrollIntoView({ block: "nearest", behavior: "auto" })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function handleKeyDown(e: KeyboardEvent) {
|
function handleKeyDown(e: KeyboardEvent) {
|
||||||
const folderList = folders()
|
const folderList = folders()
|
||||||
|
|
||||||
if (e.key === "Enter" && (e.metaKey || e.ctrlKey)) {
|
if ((e.metaKey || e.ctrlKey) && !e.shiftKey && e.key.toLowerCase() === "n") {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
handleBrowse()
|
handleBrowse()
|
||||||
return
|
return
|
||||||
@@ -43,6 +46,7 @@ const FolderSelectionView: Component<FolderSelectionViewProps> = (props) => {
|
|||||||
|
|
||||||
if (folderList.length === 0) return
|
if (folderList.length === 0) return
|
||||||
|
|
||||||
|
|
||||||
if (e.key === "ArrowDown") {
|
if (e.key === "ArrowDown") {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
const newIndex = Math.min(selectedIndex() + 1, folderList.length - 1)
|
const newIndex = Math.min(selectedIndex() + 1, folderList.length - 1)
|
||||||
@@ -154,21 +158,22 @@ const FolderSelectionView: Component<FolderSelectionViewProps> = (props) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class="flex h-full w-full items-center justify-center" style="background-color: var(--surface-secondary)">
|
<div class="flex h-screen w-full items-start justify-center overflow-hidden py-6" style="background-color: var(--surface-secondary)">
|
||||||
<div class="w-full max-w-3xl px-8 py-12">
|
<div class="w-full max-w-3xl h-full max-h-[90vh] px-8 flex flex-col overflow-hidden">
|
||||||
<div class="mb-8 text-center">
|
<div class="mb-6 text-center shrink-0">
|
||||||
<div class="mb-4 flex justify-center">
|
<div class="mb-3 flex justify-center">
|
||||||
<Folder class="h-16 w-16 icon-muted" />
|
<Folder class="h-16 w-16 icon-muted" />
|
||||||
</div>
|
</div>
|
||||||
<h1 class="mb-2 text-2xl font-semibold text-primary">Welcome to OpenCode</h1>
|
<h1 class="mb-2 text-2xl font-semibold text-primary">Welcome to OpenCode</h1>
|
||||||
<p class="text-base text-secondary">Select a folder to start coding with AI</p>
|
<p class="text-base text-secondary">Select a folder to start coding with AI</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="space-y-4 overflow-visible">
|
<div class="space-y-4 flex-1 min-h-0 overflow-hidden flex flex-col">
|
||||||
|
|
||||||
<Show
|
<Show
|
||||||
when={folders().length > 0}
|
when={folders().length > 0}
|
||||||
fallback={
|
fallback={
|
||||||
<div class="panel panel-empty-state">
|
<div class="panel panel-empty-state flex-1">
|
||||||
<div class="panel-empty-state-icon">
|
<div class="panel-empty-state-icon">
|
||||||
<Clock class="w-12 h-12 mx-auto" />
|
<Clock class="w-12 h-12 mx-auto" />
|
||||||
</div>
|
</div>
|
||||||
@@ -177,22 +182,27 @@ const FolderSelectionView: Component<FolderSelectionViewProps> = (props) => {
|
|||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<div class="panel">
|
<div class="panel flex-1 min-h-0 overflow-hidden">
|
||||||
<div class="panel-header">
|
<div class="panel-header">
|
||||||
<h2 class="panel-title">Recent Folders</h2>
|
<h2 class="panel-title">Recent Folders</h2>
|
||||||
<p class="panel-subtitle">
|
<p class="panel-subtitle">
|
||||||
{folders().length} {folders().length === 1 ? "folder" : "folders"} available
|
{folders().length} {folders().length === 1 ? "folder" : "folders"} available
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="panel-list">
|
<div
|
||||||
<For each={folders()}>
|
class="panel-list max-h-[50vh] overflow-y-auto pr-1"
|
||||||
{(folder, index) => (
|
ref={(el) => (recentListRef = el)}
|
||||||
<div
|
>
|
||||||
class="panel-list-item"
|
<For each={folders()}>
|
||||||
classList={{
|
{(folder, index) => (
|
||||||
"panel-list-item-highlight": focusMode() === "recent" && selectedIndex() === index(),
|
<div
|
||||||
}}
|
data-folder-index={index()}
|
||||||
>
|
class="panel-list-item"
|
||||||
|
classList={{
|
||||||
|
"panel-list-item-highlight": focusMode() === "recent" && selectedIndex() === index(),
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
|
||||||
<div class="flex items-center w-full">
|
<div class="flex items-center w-full">
|
||||||
<button
|
<button
|
||||||
class="panel-list-item-content w-full"
|
class="panel-list-item-content w-full"
|
||||||
@@ -237,7 +247,7 @@ const FolderSelectionView: Component<FolderSelectionViewProps> = (props) => {
|
|||||||
</div>
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
|
|
||||||
<div class="panel">
|
<div class="panel shrink-0">
|
||||||
<div class="panel-header">
|
<div class="panel-header">
|
||||||
<h2 class="panel-title">Browse for Folder</h2>
|
<h2 class="panel-title">Browse for Folder</h2>
|
||||||
<p class="panel-subtitle">Select any folder on your computer</p>
|
<p class="panel-subtitle">Select any folder on your computer</p>
|
||||||
@@ -254,9 +264,7 @@ const FolderSelectionView: Component<FolderSelectionViewProps> = (props) => {
|
|||||||
<FolderPlus class="w-4 h-4" />
|
<FolderPlus class="w-4 h-4" />
|
||||||
<span>{props.isLoading ? "Opening..." : "Browse Folders"}</span>
|
<span>{props.isLoading ? "Opening..." : "Browse Folders"}</span>
|
||||||
</div>
|
</div>
|
||||||
<kbd class="kbd ml-2">
|
<Kbd shortcut="cmd+n" class="ml-2" />
|
||||||
Cmd+Enter
|
|
||||||
</kbd>
|
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -297,7 +305,7 @@ const FolderSelectionView: Component<FolderSelectionViewProps> = (props) => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mt-6 panel panel-footer">
|
<div class="mt-4 panel panel-footer shrink-0">
|
||||||
<div class="panel-footer-hints">
|
<div class="panel-footer-hints">
|
||||||
<Show when={folders().length > 0}>
|
<Show when={folders().length > 0}>
|
||||||
<div class="flex items-center gap-1.5">
|
<div class="flex items-center gap-1.5">
|
||||||
@@ -315,7 +323,7 @@ const FolderSelectionView: Component<FolderSelectionViewProps> = (props) => {
|
|||||||
</div>
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
<div class="flex items-center gap-1.5">
|
<div class="flex items-center gap-1.5">
|
||||||
<kbd class="kbd">Cmd+Enter</kbd>
|
<Kbd shortcut="cmd+n" />
|
||||||
<span>Browse</span>
|
<span>Browse</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,7 +1,11 @@
|
|||||||
import { Component, createSignal, Show, For, createEffect, onMount, onCleanup } from "solid-js"
|
import { Component, createSignal, Show, For, createEffect, onMount, onCleanup, createMemo } from "solid-js"
|
||||||
import type { Instance } from "../types/instance"
|
import type { Instance } from "../types/instance"
|
||||||
import { getParentSessions, createSession, setActiveParentSession, agents } from "../stores/sessions"
|
import { getParentSessions, createSession, setActiveParentSession } from "../stores/sessions"
|
||||||
import InstanceInfo from "./instance-info"
|
import InstanceInfo from "./instance-info"
|
||||||
|
import KeyboardHint from "./keyboard-hint"
|
||||||
|
import Kbd from "./kbd"
|
||||||
|
import { keyboardRegistry, type KeyboardShortcut } from "../lib/keyboard-registry"
|
||||||
|
import { isMac } from "../lib/keyboard-utils"
|
||||||
|
|
||||||
|
|
||||||
interface InstanceWelcomeViewProps {
|
interface InstanceWelcomeViewProps {
|
||||||
@@ -9,20 +13,28 @@ interface InstanceWelcomeViewProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const InstanceWelcomeView: Component<InstanceWelcomeViewProps> = (props) => {
|
const InstanceWelcomeView: Component<InstanceWelcomeViewProps> = (props) => {
|
||||||
const [selectedAgent, setSelectedAgent] = createSignal<string>("")
|
|
||||||
const [isCreating, setIsCreating] = createSignal(false)
|
const [isCreating, setIsCreating] = createSignal(false)
|
||||||
const [selectedIndex, setSelectedIndex] = createSignal(0)
|
const [selectedIndex, setSelectedIndex] = createSignal(0)
|
||||||
const [focusMode, setFocusMode] = createSignal<"sessions" | "new-session" | null>("sessions")
|
const [focusMode, setFocusMode] = createSignal<"sessions" | "new-session" | null>("sessions")
|
||||||
|
|
||||||
const parentSessions = () => getParentSessions(props.instance.id)
|
const parentSessions = () => getParentSessions(props.instance.id)
|
||||||
const agentList = () => agents().get(props.instance.id) || []
|
const newSessionShortcut = createMemo<KeyboardShortcut>(() => {
|
||||||
|
const registered = keyboardRegistry.get("session-new")
|
||||||
createEffect(() => {
|
if (registered) return registered
|
||||||
const list = agentList()
|
return {
|
||||||
if (list.length > 0 && !selectedAgent()) {
|
id: "session-new-display",
|
||||||
setSelectedAgent(list[0].name)
|
key: "n",
|
||||||
|
modifiers: {
|
||||||
|
shift: true,
|
||||||
|
meta: isMac(),
|
||||||
|
ctrl: !isMac(),
|
||||||
|
},
|
||||||
|
handler: () => {},
|
||||||
|
description: "New Session",
|
||||||
|
context: "global",
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
const newSessionShortcutString = createMemo(() => (isMac() ? "cmd+shift+n" : "ctrl+shift+n"))
|
||||||
|
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
const sessions = parentSessions()
|
const sessions = parentSessions()
|
||||||
@@ -45,7 +57,7 @@ const InstanceWelcomeView: Component<InstanceWelcomeViewProps> = (props) => {
|
|||||||
function handleKeyDown(e: KeyboardEvent) {
|
function handleKeyDown(e: KeyboardEvent) {
|
||||||
const sessions = parentSessions()
|
const sessions = parentSessions()
|
||||||
|
|
||||||
if (e.key === "Enter" && (e.metaKey || e.ctrlKey)) {
|
if ((e.metaKey || e.ctrlKey) && e.shiftKey && e.key.toLowerCase() === "n") {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
handleNewSession()
|
handleNewSession()
|
||||||
return
|
return
|
||||||
@@ -133,11 +145,11 @@ const InstanceWelcomeView: Component<InstanceWelcomeViewProps> = (props) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function handleNewSession() {
|
async function handleNewSession() {
|
||||||
if (isCreating() || agentList().length === 0) return
|
if (isCreating()) return
|
||||||
|
|
||||||
setIsCreating(true)
|
setIsCreating(true)
|
||||||
try {
|
try {
|
||||||
const session = await createSession(props.instance.id, selectedAgent())
|
const session = await createSession(props.instance.id)
|
||||||
setActiveParentSession(props.instance.id, session.id)
|
setActiveParentSession(props.instance.id, session.id)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to create session:", error)
|
console.error("Failed to create session:", error)
|
||||||
@@ -153,7 +165,7 @@ const InstanceWelcomeView: Component<InstanceWelcomeViewProps> = (props) => {
|
|||||||
<Show
|
<Show
|
||||||
when={parentSessions().length > 0}
|
when={parentSessions().length > 0}
|
||||||
fallback={
|
fallback={
|
||||||
<div class="panel panel-empty-state flex-shrink-0">
|
<div class="panel panel-empty-state flex-1 flex flex-col justify-center">
|
||||||
<div class="panel-empty-state-icon">
|
<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
|
||||||
@@ -169,14 +181,14 @@ const InstanceWelcomeView: Component<InstanceWelcomeViewProps> = (props) => {
|
|||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<div class="panel flex-shrink-0">
|
<div class="panel flex flex-col flex-1 min-h-0">
|
||||||
<div class="panel-header">
|
<div class="panel-header">
|
||||||
<h2 class="panel-title">Resume Session</h2>
|
<h2 class="panel-title">Resume Session</h2>
|
||||||
<p class="panel-subtitle">
|
<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="panel-list">
|
<div class="panel-list panel-list--fill flex-1 min-h-0 overflow-auto">
|
||||||
<For each={parentSessions()}>
|
<For each={parentSessions()}>
|
||||||
{(session, index) => (
|
{(session, index) => (
|
||||||
<div
|
<div
|
||||||
@@ -228,35 +240,15 @@ const InstanceWelcomeView: Component<InstanceWelcomeViewProps> = (props) => {
|
|||||||
<div class="panel flex-shrink-0">
|
<div class="panel flex-shrink-0">
|
||||||
<div class="panel-header">
|
<div class="panel-header">
|
||||||
<h2 class="panel-title">Start New Session</h2>
|
<h2 class="panel-title">Start New Session</h2>
|
||||||
<p class="panel-subtitle">Create a fresh conversation with your chosen agent</p>
|
<p class="panel-subtitle">We’ll reuse your last agent/model automatically</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="panel-body">
|
<div class="panel-body">
|
||||||
<div class="space-y-3">
|
<div class="space-y-3">
|
||||||
<Show when={agentList().length > 0}>
|
|
||||||
<div>
|
|
||||||
<label class="block text-xs font-medium text-secondary mb-1.5">Agent</label>
|
|
||||||
<select
|
|
||||||
class="selector-input w-full"
|
|
||||||
value={selectedAgent()}
|
|
||||||
onChange={(e) => setSelectedAgent(e.currentTarget.value)}
|
|
||||||
>
|
|
||||||
<For each={agentList()}>
|
|
||||||
{(agent) => (
|
|
||||||
<option value={agent.name}>
|
|
||||||
{agent.name}
|
|
||||||
{agent.description ? ` - ${agent.description}` : ""}
|
|
||||||
</option>
|
|
||||||
)}
|
|
||||||
</For>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
</Show>
|
|
||||||
|
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="button-primary w-full flex items-center justify-center text-sm disabled:cursor-not-allowed"
|
class="button-primary w-full flex items-center justify-center text-sm disabled:cursor-not-allowed"
|
||||||
onClick={handleNewSession}
|
onClick={handleNewSession}
|
||||||
disabled={isCreating() || agentList().length === 0}
|
disabled={isCreating()}
|
||||||
>
|
>
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
{isCreating() ? (
|
{isCreating() ? (
|
||||||
@@ -273,11 +265,9 @@ const InstanceWelcomeView: Component<InstanceWelcomeViewProps> = (props) => {
|
|||||||
<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>{agentList().length === 0 ? "Loading agents..." : "Create Session"}</span>
|
<span>Create Session</span>
|
||||||
</div>
|
</div>
|
||||||
<kbd class="kbd ml-2">
|
<Kbd shortcut={newSessionShortcutString()} class="ml-2" />
|
||||||
Cmd+Enter
|
|
||||||
</kbd>
|
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -312,10 +302,7 @@ const InstanceWelcomeView: Component<InstanceWelcomeViewProps> = (props) => {
|
|||||||
<kbd class="kbd">Enter</kbd>
|
<kbd class="kbd">Enter</kbd>
|
||||||
<span>Resume</span>
|
<span>Resume</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center gap-1.5">
|
<KeyboardHint shortcuts={[newSessionShortcut()]} separator="" />
|
||||||
<kbd class="kbd">Cmd+Enter</kbd>
|
|
||||||
<span>New Session</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -577,10 +577,11 @@ export default function MessageStream(props: MessageStreamProps) {
|
|||||||
<div class="empty-state">
|
<div class="empty-state">
|
||||||
<div class="empty-state-content">
|
<div class="empty-state-content">
|
||||||
<h3>Start a conversation</h3>
|
<h3>Start a conversation</h3>
|
||||||
<p>Type a message below or try:</p>
|
<p>Type a message below or open the Command Palette:</p>
|
||||||
<ul>
|
<ul>
|
||||||
<li>
|
<li>
|
||||||
<code>/init-project</code>
|
<span>Command Palette</span>
|
||||||
|
<Kbd shortcut="cmd+shift+p" class="ml-2" />
|
||||||
</li>
|
</li>
|
||||||
<li>Ask about your codebase</li>
|
<li>Ask about your codebase</li>
|
||||||
<li>
|
<li>
|
||||||
|
|||||||
@@ -21,7 +21,12 @@ const SessionPicker: Component<SessionPickerProps> = (props) => {
|
|||||||
|
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
const list = agentList()
|
const list = agentList()
|
||||||
if (list.length > 0 && !selectedAgent()) {
|
if (list.length === 0) {
|
||||||
|
setSelectedAgent("")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const current = selectedAgent()
|
||||||
|
if (!current || !list.some((agent) => agent.name === current)) {
|
||||||
setSelectedAgent(list[0].name)
|
setSelectedAgent(list[0].name)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
import { instances, activeInstanceId, setActiveInstanceId } from "../stores/instances"
|
import { instances, activeInstanceId, setActiveInstanceId } from "../stores/instances"
|
||||||
import { activeSessionId, setActiveSession, getSessions, activeParentSessionId } from "../stores/sessions"
|
import { activeSessionId, setActiveSession, getSessions, activeParentSessionId } from "../stores/sessions"
|
||||||
|
import { keyboardRegistry } from "./keyboard-registry"
|
||||||
|
import { isMac } from "./keyboard-utils"
|
||||||
|
|
||||||
export function setupTabKeyboardShortcuts(
|
export function setupTabKeyboardShortcuts(
|
||||||
handleNewInstance: () => void,
|
handleNewInstance: () => void,
|
||||||
@@ -8,6 +10,22 @@ export function setupTabKeyboardShortcuts(
|
|||||||
handleCloseSession: (instanceId: string, sessionId: string) => void,
|
handleCloseSession: (instanceId: string, sessionId: string) => void,
|
||||||
handleCommandPalette: () => void,
|
handleCommandPalette: () => void,
|
||||||
) {
|
) {
|
||||||
|
keyboardRegistry.register({
|
||||||
|
id: "session-new",
|
||||||
|
key: "n",
|
||||||
|
modifiers: {
|
||||||
|
shift: true,
|
||||||
|
meta: isMac(),
|
||||||
|
ctrl: !isMac(),
|
||||||
|
},
|
||||||
|
handler: () => {
|
||||||
|
const instanceId = activeInstanceId()
|
||||||
|
if (instanceId) void handleNewSession(instanceId)
|
||||||
|
},
|
||||||
|
description: "New Session",
|
||||||
|
context: "global",
|
||||||
|
})
|
||||||
|
|
||||||
window.addEventListener("keydown", (e) => {
|
window.addEventListener("keydown", (e) => {
|
||||||
if ((e.metaKey || e.ctrlKey) && e.shiftKey && e.key.toLowerCase() === "p") {
|
if ((e.metaKey || e.ctrlKey) && e.shiftKey && e.key.toLowerCase() === "p") {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
@@ -47,14 +65,6 @@ export function setupTabKeyboardShortcuts(
|
|||||||
handleNewInstance()
|
handleNewInstance()
|
||||||
}
|
}
|
||||||
|
|
||||||
if ((e.metaKey || e.ctrlKey) && e.shiftKey && e.key.toLowerCase() === "n") {
|
|
||||||
e.preventDefault()
|
|
||||||
const instanceId = activeInstanceId()
|
|
||||||
if (instanceId) {
|
|
||||||
handleNewSession(instanceId)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if ((e.metaKey || e.ctrlKey) && !e.shiftKey && e.key.toLowerCase() === "w") {
|
if ((e.metaKey || e.ctrlKey) && !e.shiftKey && e.key.toLowerCase() === "w") {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
const instanceId = activeInstanceId()
|
const instanceId = activeInstanceId()
|
||||||
|
|||||||
@@ -52,6 +52,8 @@ export class FileStorage {
|
|||||||
preferences: {
|
preferences: {
|
||||||
showThinkingBlocks: false,
|
showThinkingBlocks: false,
|
||||||
environmentVariables: {},
|
environmentVariables: {},
|
||||||
|
modelRecents: [],
|
||||||
|
agentModelSelections: {},
|
||||||
},
|
},
|
||||||
recentFolders: [],
|
recentFolders: [],
|
||||||
opencodeBinaries: [],
|
opencodeBinaries: [],
|
||||||
|
|||||||
@@ -1,10 +1,21 @@
|
|||||||
import { createSignal, onMount } from "solid-js"
|
import { createSignal, onMount } from "solid-js"
|
||||||
import { storage, type ConfigData } from "../lib/storage"
|
import { storage, type ConfigData } from "../lib/storage"
|
||||||
|
|
||||||
|
export interface ModelPreference {
|
||||||
|
providerId: string
|
||||||
|
modelId: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AgentModelSelections {
|
||||||
|
[instanceId: string]: Record<string, ModelPreference>
|
||||||
|
}
|
||||||
|
|
||||||
export interface Preferences {
|
export interface Preferences {
|
||||||
showThinkingBlocks: boolean
|
showThinkingBlocks: boolean
|
||||||
lastUsedBinary?: string
|
lastUsedBinary?: string
|
||||||
environmentVariables?: Record<string, string>
|
environmentVariables?: Record<string, string>
|
||||||
|
modelRecents?: ModelPreference[]
|
||||||
|
agentModelSelections?: AgentModelSelections
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface OpenCodeBinary {
|
export interface OpenCodeBinary {
|
||||||
@@ -19,9 +30,12 @@ export interface RecentFolder {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const MAX_RECENT_FOLDERS = 10
|
const MAX_RECENT_FOLDERS = 10
|
||||||
|
const MAX_RECENT_MODELS = 5
|
||||||
|
|
||||||
const defaultPreferences: Preferences = {
|
const defaultPreferences: Preferences = {
|
||||||
showThinkingBlocks: false,
|
showThinkingBlocks: false,
|
||||||
|
modelRecents: [],
|
||||||
|
agentModelSelections: {},
|
||||||
}
|
}
|
||||||
|
|
||||||
const [preferences, setPreferences] = createSignal<Preferences>(defaultPreferences)
|
const [preferences, setPreferences] = createSignal<Preferences>(defaultPreferences)
|
||||||
@@ -127,6 +141,37 @@ function removeEnvironmentVariable(key: string): void {
|
|||||||
updateEnvironmentVariables(rest)
|
updateEnvironmentVariables(rest)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function addRecentModelPreference(model: ModelPreference): void {
|
||||||
|
if (!model.providerId || !model.modelId) return
|
||||||
|
const recents = preferences().modelRecents ?? []
|
||||||
|
const filtered = recents.filter((item) => item.providerId !== model.providerId || item.modelId !== model.modelId)
|
||||||
|
const updated = [model, ...filtered].slice(0, MAX_RECENT_MODELS)
|
||||||
|
updatePreferences({ modelRecents: updated })
|
||||||
|
}
|
||||||
|
|
||||||
|
function setAgentModelPreference(instanceId: string, agent: string, model: ModelPreference): void {
|
||||||
|
if (!instanceId || !agent || !model.providerId || !model.modelId) return
|
||||||
|
const selections = preferences().agentModelSelections ?? {}
|
||||||
|
const instanceSelections = selections[instanceId] ?? {}
|
||||||
|
const existing = instanceSelections[agent]
|
||||||
|
if (existing && existing.providerId === model.providerId && existing.modelId === model.modelId) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
updatePreferences({
|
||||||
|
agentModelSelections: {
|
||||||
|
...selections,
|
||||||
|
[instanceId]: {
|
||||||
|
...instanceSelections,
|
||||||
|
[agent]: model,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function getAgentModelPreference(instanceId: string, agent: string): ModelPreference | undefined {
|
||||||
|
return preferences().agentModelSelections?.[instanceId]?.[agent]
|
||||||
|
}
|
||||||
|
|
||||||
// Load config on mount and listen for changes from other instances
|
// Load config on mount and listen for changes from other instances
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
loadConfig()
|
loadConfig()
|
||||||
@@ -154,4 +199,7 @@ export {
|
|||||||
updateEnvironmentVariables,
|
updateEnvironmentVariables,
|
||||||
addEnvironmentVariable,
|
addEnvironmentVariable,
|
||||||
removeEnvironmentVariable,
|
removeEnvironmentVariable,
|
||||||
|
addRecentModelPreference,
|
||||||
|
setAgentModelPreference,
|
||||||
|
getAgentModelPreference,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { instances } from "./instances"
|
|||||||
|
|
||||||
import { sseManager } from "../lib/sse-manager"
|
import { sseManager } from "../lib/sse-manager"
|
||||||
import { decodeHtmlEntities } from "../lib/markdown"
|
import { decodeHtmlEntities } from "../lib/markdown"
|
||||||
import { preferences } from "./preferences"
|
import { preferences, addRecentModelPreference, getAgentModelPreference, setAgentModelPreference } from "./preferences"
|
||||||
|
|
||||||
interface SessionInfo {
|
interface SessionInfo {
|
||||||
tokens: number
|
tokens: number
|
||||||
@@ -330,6 +330,28 @@ async function fetchSessions(instanceId: string): Promise<void> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isModelValid(
|
||||||
|
instanceId: string,
|
||||||
|
model?: { providerId: string; modelId: string } | null,
|
||||||
|
): model is { providerId: string; modelId: string } {
|
||||||
|
if (!model?.providerId || !model.modelId) return false
|
||||||
|
const instanceProviders = providers().get(instanceId) || []
|
||||||
|
const provider = instanceProviders.find((p) => p.id === model.providerId)
|
||||||
|
if (!provider) return false
|
||||||
|
return provider.models.some((item) => item.id === model.modelId)
|
||||||
|
}
|
||||||
|
|
||||||
|
function getRecentModelPreferenceForInstance(
|
||||||
|
instanceId: string,
|
||||||
|
): { providerId: string; modelId: string } | undefined {
|
||||||
|
const recents = preferences().modelRecents ?? []
|
||||||
|
for (const item of recents) {
|
||||||
|
if (isModelValid(instanceId, item)) {
|
||||||
|
return item
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function getDefaultModel(
|
async function getDefaultModel(
|
||||||
instanceId: string,
|
instanceId: string,
|
||||||
agentName?: string,
|
agentName?: string,
|
||||||
@@ -337,9 +359,16 @@ async function getDefaultModel(
|
|||||||
const instanceProviders = providers().get(instanceId) || []
|
const instanceProviders = providers().get(instanceId) || []
|
||||||
const instanceAgents = agents().get(instanceId) || []
|
const instanceAgents = agents().get(instanceId) || []
|
||||||
|
|
||||||
|
if (agentName) {
|
||||||
|
const stored = getAgentModelPreference(instanceId, agentName)
|
||||||
|
if (isModelValid(instanceId, stored)) {
|
||||||
|
return stored
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (agentName) {
|
if (agentName) {
|
||||||
const agent = instanceAgents.find((a) => a.name === agentName)
|
const agent = instanceAgents.find((a) => a.name === agentName)
|
||||||
if (agent?.model?.providerId && agent.model.modelId) {
|
if (agent && agent.model && isModelValid(instanceId, agent.model)) {
|
||||||
return {
|
return {
|
||||||
providerId: agent.model.providerId,
|
providerId: agent.model.providerId,
|
||||||
modelId: agent.model.modelId,
|
modelId: agent.model.modelId,
|
||||||
@@ -347,25 +376,30 @@ async function getDefaultModel(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const anthropicProvider = instanceProviders.find((p) => p.id === "anthropic")
|
const recent = getRecentModelPreferenceForInstance(instanceId)
|
||||||
if (anthropicProvider) {
|
if (recent) {
|
||||||
const defaultModelId = anthropicProvider.defaultModelId || anthropicProvider.models[0]?.id
|
return recent
|
||||||
if (defaultModelId) {
|
}
|
||||||
return {
|
|
||||||
providerId: "anthropic",
|
for (const provider of instanceProviders) {
|
||||||
modelId: defaultModelId,
|
if (provider.defaultModelId) {
|
||||||
|
const model = provider.models.find((m) => m.id === provider.defaultModelId)
|
||||||
|
if (model) {
|
||||||
|
return {
|
||||||
|
providerId: provider.id,
|
||||||
|
modelId: model.id,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (instanceProviders.length > 0) {
|
if (instanceProviders.length > 0) {
|
||||||
const firstProvider = instanceProviders[0]
|
const firstProvider = instanceProviders[0]
|
||||||
const defaultModelId = firstProvider.defaultModelId || firstProvider.models[0]?.id
|
const firstModel = firstProvider.models[0]
|
||||||
|
if (firstModel) {
|
||||||
if (defaultModelId) {
|
|
||||||
return {
|
return {
|
||||||
providerId: firstProvider.id,
|
providerId: firstProvider.id,
|
||||||
modelId: defaultModelId,
|
modelId: firstModel.id,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -469,6 +503,10 @@ async function createSession(instanceId: string, agent?: string): Promise<Sessio
|
|||||||
|
|
||||||
const defaultModel = await getDefaultModel(instanceId, selectedAgent)
|
const defaultModel = await getDefaultModel(instanceId, selectedAgent)
|
||||||
|
|
||||||
|
if (selectedAgent && isModelValid(instanceId, defaultModel)) {
|
||||||
|
setAgentModelPreference(instanceId, selectedAgent, defaultModel)
|
||||||
|
}
|
||||||
|
|
||||||
setLoading((prev) => {
|
setLoading((prev) => {
|
||||||
const next = { ...prev }
|
const next = { ...prev }
|
||||||
next.creatingSession.set(instanceId, true)
|
next.creatingSession.set(instanceId, true)
|
||||||
@@ -1457,16 +1495,27 @@ async function updateSessionAgent(instanceId: string, sessionId: string, agent:
|
|||||||
throw new Error("Session not found")
|
throw new Error("Session not found")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const nextModel = await getDefaultModel(instanceId, agent)
|
||||||
|
const shouldApplyModel = isModelValid(instanceId, nextModel)
|
||||||
|
|
||||||
setSessions((prev) => {
|
setSessions((prev) => {
|
||||||
const next = new Map(prev)
|
const next = new Map(prev)
|
||||||
const instanceSessions = new Map(prev.get(instanceId))
|
const map = new Map(prev.get(instanceId))
|
||||||
const session = instanceSessions.get(sessionId)
|
const current = map.get(sessionId)
|
||||||
if (session) {
|
if (current) {
|
||||||
instanceSessions.set(sessionId, { ...session, agent })
|
map.set(sessionId, {
|
||||||
next.set(instanceId, instanceSessions)
|
...current,
|
||||||
|
agent,
|
||||||
|
model: shouldApplyModel ? nextModel : current.model,
|
||||||
|
})
|
||||||
|
next.set(instanceId, map)
|
||||||
}
|
}
|
||||||
return next
|
return next
|
||||||
})
|
})
|
||||||
|
|
||||||
|
if (agent && shouldApplyModel) {
|
||||||
|
setAgentModelPreference(instanceId, agent, nextModel)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function updateSessionModel(
|
async function updateSessionModel(
|
||||||
@@ -1480,16 +1529,28 @@ async function updateSessionModel(
|
|||||||
throw new Error("Session not found")
|
throw new Error("Session not found")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!isModelValid(instanceId, model)) {
|
||||||
|
console.warn("Invalid model selection", model)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentAgent = session.agent
|
||||||
|
|
||||||
setSessions((prev) => {
|
setSessions((prev) => {
|
||||||
const next = new Map(prev)
|
const next = new Map(prev)
|
||||||
const instanceSessions = new Map(prev.get(instanceId))
|
const map = new Map(prev.get(instanceId))
|
||||||
const session = instanceSessions.get(sessionId)
|
const existing = map.get(sessionId)
|
||||||
if (session) {
|
if (existing) {
|
||||||
instanceSessions.set(sessionId, { ...session, model })
|
map.set(sessionId, { ...existing, model })
|
||||||
next.set(instanceId, instanceSessions)
|
next.set(instanceId, map)
|
||||||
}
|
}
|
||||||
return next
|
return next
|
||||||
})
|
})
|
||||||
|
|
||||||
|
if (currentAgent) {
|
||||||
|
setAgentModelPreference(instanceId, currentAgent, model)
|
||||||
|
}
|
||||||
|
addRecentModelPreference(model)
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleSessionCompacted(instanceId: string, event: any): void {
|
function handleSessionCompacted(instanceId: string, event: any): void {
|
||||||
|
|||||||
@@ -1526,6 +1526,11 @@ button.button-primary {
|
|||||||
@apply max-h-[400px] overflow-y-auto;
|
@apply max-h-[400px] overflow-y-auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.panel-list--fill {
|
||||||
|
max-height: none;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
.panel-list-item {
|
.panel-list-item {
|
||||||
@apply border-b last:border-b-0 transition-colors w-full;
|
@apply border-b last:border-b-0 transition-colors w-full;
|
||||||
border-color: var(--border-base);
|
border-color: var(--border-base);
|
||||||
|
|||||||
Reference in New Issue
Block a user