chore(app): createStore over signals
This commit is contained in:
@@ -3,7 +3,7 @@ import { useDialog } from "@opencode-ai/ui/context/dialog"
|
|||||||
import { Dialog } from "@opencode-ai/ui/dialog"
|
import { Dialog } from "@opencode-ai/ui/dialog"
|
||||||
import { TextField } from "@opencode-ai/ui/text-field"
|
import { TextField } from "@opencode-ai/ui/text-field"
|
||||||
import { Icon } from "@opencode-ai/ui/icon"
|
import { Icon } from "@opencode-ai/ui/icon"
|
||||||
import { createMemo, createSignal, For, Show } from "solid-js"
|
import { createMemo, For, Show } from "solid-js"
|
||||||
import { createStore } from "solid-js/store"
|
import { createStore } from "solid-js/store"
|
||||||
import { useGlobalSDK } from "@/context/global-sdk"
|
import { useGlobalSDK } from "@/context/global-sdk"
|
||||||
import { useGlobalSync } from "@/context/global-sync"
|
import { useGlobalSync } from "@/context/global-sync"
|
||||||
@@ -29,35 +29,34 @@ export function DialogEditProject(props: { project: LocalProject }) {
|
|||||||
iconUrl: props.project.icon?.override || "",
|
iconUrl: props.project.icon?.override || "",
|
||||||
startup: props.project.commands?.start ?? "",
|
startup: props.project.commands?.start ?? "",
|
||||||
saving: false,
|
saving: false,
|
||||||
|
dragOver: false,
|
||||||
|
iconHover: false,
|
||||||
})
|
})
|
||||||
|
|
||||||
const [dragOver, setDragOver] = createSignal(false)
|
|
||||||
const [iconHover, setIconHover] = createSignal(false)
|
|
||||||
|
|
||||||
function handleFileSelect(file: File) {
|
function handleFileSelect(file: File) {
|
||||||
if (!file.type.startsWith("image/")) return
|
if (!file.type.startsWith("image/")) return
|
||||||
const reader = new FileReader()
|
const reader = new FileReader()
|
||||||
reader.onload = (e) => {
|
reader.onload = (e) => {
|
||||||
setStore("iconUrl", e.target?.result as string)
|
setStore("iconUrl", e.target?.result as string)
|
||||||
setIconHover(false)
|
setStore("iconHover", false)
|
||||||
}
|
}
|
||||||
reader.readAsDataURL(file)
|
reader.readAsDataURL(file)
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleDrop(e: DragEvent) {
|
function handleDrop(e: DragEvent) {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
setDragOver(false)
|
setStore("dragOver", false)
|
||||||
const file = e.dataTransfer?.files[0]
|
const file = e.dataTransfer?.files[0]
|
||||||
if (file) handleFileSelect(file)
|
if (file) handleFileSelect(file)
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleDragOver(e: DragEvent) {
|
function handleDragOver(e: DragEvent) {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
setDragOver(true)
|
setStore("dragOver", true)
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleDragLeave() {
|
function handleDragLeave() {
|
||||||
setDragOver(false)
|
setStore("dragOver", false)
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleInputChange(e: Event) {
|
function handleInputChange(e: Event) {
|
||||||
@@ -116,19 +115,23 @@ export function DialogEditProject(props: { project: LocalProject }) {
|
|||||||
<div class="flex flex-col gap-2">
|
<div class="flex flex-col gap-2">
|
||||||
<label class="text-12-medium text-text-weak">{language.t("dialog.project.edit.icon")}</label>
|
<label class="text-12-medium text-text-weak">{language.t("dialog.project.edit.icon")}</label>
|
||||||
<div class="flex gap-3 items-start">
|
<div class="flex gap-3 items-start">
|
||||||
<div class="relative" onMouseEnter={() => setIconHover(true)} onMouseLeave={() => setIconHover(false)}>
|
<div
|
||||||
|
class="relative"
|
||||||
|
onMouseEnter={() => setStore("iconHover", true)}
|
||||||
|
onMouseLeave={() => setStore("iconHover", false)}
|
||||||
|
>
|
||||||
<div
|
<div
|
||||||
class="relative size-16 rounded-md transition-colors cursor-pointer"
|
class="relative size-16 rounded-md transition-colors cursor-pointer"
|
||||||
classList={{
|
classList={{
|
||||||
"border-text-interactive-base bg-surface-info-base/20": dragOver(),
|
"border-text-interactive-base bg-surface-info-base/20": store.dragOver,
|
||||||
"border-border-base hover:border-border-strong": !dragOver(),
|
"border-border-base hover:border-border-strong": !store.dragOver,
|
||||||
"overflow-hidden": !!store.iconUrl,
|
"overflow-hidden": !!store.iconUrl,
|
||||||
}}
|
}}
|
||||||
onDrop={handleDrop}
|
onDrop={handleDrop}
|
||||||
onDragOver={handleDragOver}
|
onDragOver={handleDragOver}
|
||||||
onDragLeave={handleDragLeave}
|
onDragLeave={handleDragLeave}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (store.iconUrl && iconHover()) {
|
if (store.iconUrl && store.iconHover) {
|
||||||
clearIcon()
|
clearIcon()
|
||||||
} else {
|
} else {
|
||||||
document.getElementById("icon-upload")?.click()
|
document.getElementById("icon-upload")?.click()
|
||||||
@@ -166,7 +169,7 @@ export function DialogEditProject(props: { project: LocalProject }) {
|
|||||||
"border-radius": "6px",
|
"border-radius": "6px",
|
||||||
"z-index": 10,
|
"z-index": 10,
|
||||||
"pointer-events": "none",
|
"pointer-events": "none",
|
||||||
opacity: iconHover() && !store.iconUrl ? 1 : 0,
|
opacity: store.iconHover && !store.iconUrl ? 1 : 0,
|
||||||
display: "flex",
|
display: "flex",
|
||||||
"align-items": "center",
|
"align-items": "center",
|
||||||
"justify-content": "center",
|
"justify-content": "center",
|
||||||
@@ -185,7 +188,7 @@ export function DialogEditProject(props: { project: LocalProject }) {
|
|||||||
"border-radius": "6px",
|
"border-radius": "6px",
|
||||||
"z-index": 10,
|
"z-index": 10,
|
||||||
"pointer-events": "none",
|
"pointer-events": "none",
|
||||||
opacity: iconHover() && store.iconUrl ? 1 : 0,
|
opacity: store.iconHover && store.iconUrl ? 1 : 0,
|
||||||
display: "flex",
|
display: "flex",
|
||||||
"align-items": "center",
|
"align-items": "center",
|
||||||
"justify-content": "center",
|
"justify-content": "center",
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import type { JSX } from "solid-js"
|
import type { JSX } from "solid-js"
|
||||||
import { createSignal, Show } from "solid-js"
|
import { Show } from "solid-js"
|
||||||
|
import { createStore } from "solid-js/store"
|
||||||
import { createSortable } from "@thisbeyond/solid-dnd"
|
import { createSortable } from "@thisbeyond/solid-dnd"
|
||||||
import { IconButton } from "@opencode-ai/ui/icon-button"
|
import { IconButton } from "@opencode-ai/ui/icon-button"
|
||||||
import { Tabs } from "@opencode-ai/ui/tabs"
|
import { Tabs } from "@opencode-ai/ui/tabs"
|
||||||
@@ -12,11 +13,13 @@ export function SortableTerminalTab(props: { terminal: LocalPTY; onClose?: () =>
|
|||||||
const terminal = useTerminal()
|
const terminal = useTerminal()
|
||||||
const language = useLanguage()
|
const language = useLanguage()
|
||||||
const sortable = createSortable(props.terminal.id)
|
const sortable = createSortable(props.terminal.id)
|
||||||
const [editing, setEditing] = createSignal(false)
|
const [store, setStore] = createStore({
|
||||||
const [title, setTitle] = createSignal(props.terminal.title)
|
editing: false,
|
||||||
const [menuOpen, setMenuOpen] = createSignal(false)
|
title: props.terminal.title,
|
||||||
const [menuPosition, setMenuPosition] = createSignal({ x: 0, y: 0 })
|
menuOpen: false,
|
||||||
const [blurEnabled, setBlurEnabled] = createSignal(false)
|
menuPosition: { x: 0, y: 0 },
|
||||||
|
blurEnabled: false,
|
||||||
|
})
|
||||||
|
|
||||||
const isDefaultTitle = () => {
|
const isDefaultTitle = () => {
|
||||||
const number = props.terminal.titleNumber
|
const number = props.terminal.titleNumber
|
||||||
@@ -47,7 +50,7 @@ export function SortableTerminalTab(props: { terminal: LocalPTY; onClose?: () =>
|
|||||||
}
|
}
|
||||||
|
|
||||||
const focus = () => {
|
const focus = () => {
|
||||||
if (editing()) return
|
if (store.editing) return
|
||||||
|
|
||||||
if (document.activeElement instanceof HTMLElement) {
|
if (document.activeElement instanceof HTMLElement) {
|
||||||
document.activeElement.blur()
|
document.activeElement.blur()
|
||||||
@@ -71,26 +74,26 @@ export function SortableTerminalTab(props: { terminal: LocalPTY; onClose?: () =>
|
|||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
}
|
}
|
||||||
|
|
||||||
setBlurEnabled(false)
|
setStore("blurEnabled", false)
|
||||||
setTitle(props.terminal.title)
|
setStore("title", props.terminal.title)
|
||||||
setEditing(true)
|
setStore("editing", true)
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
const input = document.getElementById(`terminal-title-input-${props.terminal.id}`) as HTMLInputElement
|
const input = document.getElementById(`terminal-title-input-${props.terminal.id}`) as HTMLInputElement
|
||||||
if (!input) return
|
if (!input) return
|
||||||
input.focus()
|
input.focus()
|
||||||
input.select()
|
input.select()
|
||||||
setTimeout(() => setBlurEnabled(true), 100)
|
setTimeout(() => setStore("blurEnabled", true), 100)
|
||||||
}, 10)
|
}, 10)
|
||||||
}
|
}
|
||||||
|
|
||||||
const save = () => {
|
const save = () => {
|
||||||
if (!blurEnabled()) return
|
if (!store.blurEnabled) return
|
||||||
|
|
||||||
const value = title().trim()
|
const value = store.title.trim()
|
||||||
if (value && value !== props.terminal.title) {
|
if (value && value !== props.terminal.title) {
|
||||||
terminal.update({ id: props.terminal.id, title: value })
|
terminal.update({ id: props.terminal.id, title: value })
|
||||||
}
|
}
|
||||||
setEditing(false)
|
setStore("editing", false)
|
||||||
}
|
}
|
||||||
|
|
||||||
const keydown = (e: KeyboardEvent) => {
|
const keydown = (e: KeyboardEvent) => {
|
||||||
@@ -101,14 +104,14 @@ export function SortableTerminalTab(props: { terminal: LocalPTY; onClose?: () =>
|
|||||||
}
|
}
|
||||||
if (e.key === "Escape") {
|
if (e.key === "Escape") {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
setEditing(false)
|
setStore("editing", false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const menu = (e: MouseEvent) => {
|
const menu = (e: MouseEvent) => {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
setMenuPosition({ x: e.clientX, y: e.clientY })
|
setStore("menuPosition", { x: e.clientX, y: e.clientY })
|
||||||
setMenuOpen(true)
|
setStore("menuOpen", true)
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -143,17 +146,17 @@ export function SortableTerminalTab(props: { terminal: LocalPTY; onClose?: () =>
|
|||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<span onDblClick={edit} style={{ visibility: editing() ? "hidden" : "visible" }}>
|
<span onDblClick={edit} style={{ visibility: store.editing ? "hidden" : "visible" }}>
|
||||||
{label()}
|
{label()}
|
||||||
</span>
|
</span>
|
||||||
</Tabs.Trigger>
|
</Tabs.Trigger>
|
||||||
<Show when={editing()}>
|
<Show when={store.editing}>
|
||||||
<div class="absolute inset-0 flex items-center px-3 bg-muted z-10 pointer-events-auto">
|
<div class="absolute inset-0 flex items-center px-3 bg-muted z-10 pointer-events-auto">
|
||||||
<input
|
<input
|
||||||
id={`terminal-title-input-${props.terminal.id}`}
|
id={`terminal-title-input-${props.terminal.id}`}
|
||||||
type="text"
|
type="text"
|
||||||
value={title()}
|
value={store.title}
|
||||||
onInput={(e) => setTitle(e.currentTarget.value)}
|
onInput={(e) => setStore("title", e.currentTarget.value)}
|
||||||
onBlur={save}
|
onBlur={save}
|
||||||
onKeyDown={keydown}
|
onKeyDown={keydown}
|
||||||
onMouseDown={(e) => e.stopPropagation()}
|
onMouseDown={(e) => e.stopPropagation()}
|
||||||
@@ -161,13 +164,13 @@ export function SortableTerminalTab(props: { terminal: LocalPTY; onClose?: () =>
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
<DropdownMenu open={menuOpen()} onOpenChange={setMenuOpen}>
|
<DropdownMenu open={store.menuOpen} onOpenChange={(open) => setStore("menuOpen", open)}>
|
||||||
<DropdownMenu.Portal>
|
<DropdownMenu.Portal>
|
||||||
<DropdownMenu.Content
|
<DropdownMenu.Content
|
||||||
style={{
|
style={{
|
||||||
position: "fixed",
|
position: "fixed",
|
||||||
left: `${menuPosition().x}px`,
|
left: `${store.menuPosition.x}px`,
|
||||||
top: `${menuPosition().y}px`,
|
top: `${store.menuPosition.y}px`,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<DropdownMenu.Item onSelect={edit}>
|
<DropdownMenu.Item onSelect={edit}>
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { Component, For, Show, createMemo, createSignal, onCleanup, onMount } from "solid-js"
|
import { Component, For, Show, createMemo, onCleanup, onMount } from "solid-js"
|
||||||
|
import { createStore } from "solid-js/store"
|
||||||
import { Button } from "@opencode-ai/ui/button"
|
import { Button } from "@opencode-ai/ui/button"
|
||||||
import { Icon } from "@opencode-ai/ui/icon"
|
import { Icon } from "@opencode-ai/ui/icon"
|
||||||
import { IconButton } from "@opencode-ai/ui/icon-button"
|
import { IconButton } from "@opencode-ai/ui/icon-button"
|
||||||
@@ -111,24 +112,26 @@ export const SettingsKeybinds: Component = () => {
|
|||||||
const language = useLanguage()
|
const language = useLanguage()
|
||||||
const settings = useSettings()
|
const settings = useSettings()
|
||||||
|
|
||||||
const [active, setActive] = createSignal<string | null>(null)
|
const [store, setStore] = createStore({
|
||||||
const [filter, setFilter] = createSignal("")
|
active: null as string | null,
|
||||||
|
filter: "",
|
||||||
|
})
|
||||||
|
|
||||||
const stop = () => {
|
const stop = () => {
|
||||||
if (!active()) return
|
if (!store.active) return
|
||||||
setActive(null)
|
setStore("active", null)
|
||||||
command.keybinds(true)
|
command.keybinds(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
const start = (id: string) => {
|
const start = (id: string) => {
|
||||||
if (active() === id) {
|
if (store.active === id) {
|
||||||
stop()
|
stop()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (active()) stop()
|
if (store.active) stop()
|
||||||
|
|
||||||
setActive(id)
|
setStore("active", id)
|
||||||
command.keybinds(false)
|
command.keybinds(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -203,7 +206,7 @@ export const SettingsKeybinds: Component = () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const filtered = createMemo(() => {
|
const filtered = createMemo(() => {
|
||||||
const query = filter().toLowerCase().trim()
|
const query = store.filter.toLowerCase().trim()
|
||||||
if (!query) return grouped()
|
if (!query) return grouped()
|
||||||
|
|
||||||
const map = list()
|
const map = list()
|
||||||
@@ -285,7 +288,7 @@ export const SettingsKeybinds: Component = () => {
|
|||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
const handle = (event: KeyboardEvent) => {
|
const handle = (event: KeyboardEvent) => {
|
||||||
const id = active()
|
const id = store.active
|
||||||
if (!id) return
|
if (!id) return
|
||||||
|
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
@@ -345,7 +348,7 @@ export const SettingsKeybinds: Component = () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
onCleanup(() => {
|
onCleanup(() => {
|
||||||
if (active()) command.keybinds(true)
|
if (store.active) command.keybinds(true)
|
||||||
})
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -370,8 +373,8 @@ export const SettingsKeybinds: Component = () => {
|
|||||||
<TextField
|
<TextField
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
type="text"
|
type="text"
|
||||||
value={filter()}
|
value={store.filter}
|
||||||
onChange={setFilter}
|
onChange={(v) => setStore("filter", v)}
|
||||||
placeholder={language.t("settings.shortcuts.search.placeholder")}
|
placeholder={language.t("settings.shortcuts.search.placeholder")}
|
||||||
spellcheck={false}
|
spellcheck={false}
|
||||||
autocorrect="off"
|
autocorrect="off"
|
||||||
@@ -379,8 +382,8 @@ export const SettingsKeybinds: Component = () => {
|
|||||||
autocapitalize="off"
|
autocapitalize="off"
|
||||||
class="flex-1"
|
class="flex-1"
|
||||||
/>
|
/>
|
||||||
<Show when={filter()}>
|
<Show when={store.filter}>
|
||||||
<IconButton icon="circle-x" variant="ghost" onClick={() => setFilter("")} />
|
<IconButton icon="circle-x" variant="ghost" onClick={() => setStore("filter", "")} />
|
||||||
</Show>
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -402,13 +405,13 @@ export const SettingsKeybinds: Component = () => {
|
|||||||
classList={{
|
classList={{
|
||||||
"h-8 px-3 rounded-md text-12-regular": true,
|
"h-8 px-3 rounded-md text-12-regular": true,
|
||||||
"bg-surface-base text-text-subtle hover:bg-surface-raised-base-hover active:bg-surface-raised-base-active":
|
"bg-surface-base text-text-subtle hover:bg-surface-raised-base-hover active:bg-surface-raised-base-active":
|
||||||
active() !== id,
|
store.active !== id,
|
||||||
"border border-border-weak-base bg-surface-inset-base text-text-weak": active() === id,
|
"border border-border-weak-base bg-surface-inset-base text-text-weak": store.active === id,
|
||||||
}}
|
}}
|
||||||
onClick={() => start(id)}
|
onClick={() => start(id)}
|
||||||
>
|
>
|
||||||
<Show
|
<Show
|
||||||
when={active() === id}
|
when={store.active === id}
|
||||||
fallback={command.keybind(id) || language.t("settings.shortcuts.unassigned")}
|
fallback={command.keybind(id) || language.t("settings.shortcuts.unassigned")}
|
||||||
>
|
>
|
||||||
{language.t("settings.shortcuts.pressKeys")}
|
{language.t("settings.shortcuts.pressKeys")}
|
||||||
@@ -423,11 +426,11 @@ export const SettingsKeybinds: Component = () => {
|
|||||||
)}
|
)}
|
||||||
</For>
|
</For>
|
||||||
|
|
||||||
<Show when={filter() && !hasResults()}>
|
<Show when={store.filter && !hasResults()}>
|
||||||
<div class="flex flex-col items-center justify-center py-12 text-center">
|
<div class="flex flex-col items-center justify-center py-12 text-center">
|
||||||
<span class="text-14-regular text-text-weak">{language.t("settings.shortcuts.search.empty")}</span>
|
<span class="text-14-regular text-text-weak">{language.t("settings.shortcuts.search.empty")}</span>
|
||||||
<Show when={filter()}>
|
<Show when={store.filter}>
|
||||||
<span class="text-14-regular text-text-strong mt-1">"{filter()}"</span>
|
<span class="text-14-regular text-text-strong mt-1">"{store.filter}"</span>
|
||||||
</Show>
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
|
|||||||
@@ -39,9 +39,10 @@ export function StatusPopover() {
|
|||||||
const language = useLanguage()
|
const language = useLanguage()
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
|
|
||||||
const [loading, setLoading] = createSignal<string | null>(null)
|
|
||||||
const [store, setStore] = createStore({
|
const [store, setStore] = createStore({
|
||||||
status: {} as Record<string, ServerStatus | undefined>,
|
status: {} as Record<string, ServerStatus | undefined>,
|
||||||
|
loading: null as string | null,
|
||||||
|
defaultServerUrl: undefined as string | undefined,
|
||||||
})
|
})
|
||||||
|
|
||||||
const servers = createMemo(() => {
|
const servers = createMemo(() => {
|
||||||
@@ -97,8 +98,8 @@ export function StatusPopover() {
|
|||||||
const mcpConnected = createMemo(() => mcpItems().filter((i) => i.status === "connected").length)
|
const mcpConnected = createMemo(() => mcpItems().filter((i) => i.status === "connected").length)
|
||||||
|
|
||||||
const toggleMcp = async (name: string) => {
|
const toggleMcp = async (name: string) => {
|
||||||
if (loading()) return
|
if (store.loading) return
|
||||||
setLoading(name)
|
setStore("loading", name)
|
||||||
const status = sync.data.mcp[name]
|
const status = sync.data.mcp[name]
|
||||||
if (status?.status === "connected") {
|
if (status?.status === "connected") {
|
||||||
await sdk.client.mcp.disconnect({ name })
|
await sdk.client.mcp.disconnect({ name })
|
||||||
@@ -107,7 +108,7 @@ export function StatusPopover() {
|
|||||||
}
|
}
|
||||||
const result = await sdk.client.mcp.status()
|
const result = await sdk.client.mcp.status()
|
||||||
if (result.data) sync.set("mcp", result.data)
|
if (result.data) sync.set("mcp", result.data)
|
||||||
setLoading(null)
|
setStore("loading", null)
|
||||||
}
|
}
|
||||||
|
|
||||||
const lspItems = createMemo(() => sync.data.lsp ?? [])
|
const lspItems = createMemo(() => sync.data.lsp ?? [])
|
||||||
@@ -123,19 +124,17 @@ export function StatusPopover() {
|
|||||||
|
|
||||||
const serverCount = createMemo(() => sortedServers().length)
|
const serverCount = createMemo(() => sortedServers().length)
|
||||||
|
|
||||||
const [defaultServerUrl, setDefaultServerUrl] = createSignal<string | undefined>()
|
|
||||||
|
|
||||||
const refreshDefaultServerUrl = () => {
|
const refreshDefaultServerUrl = () => {
|
||||||
const result = platform.getDefaultServerUrl?.()
|
const result = platform.getDefaultServerUrl?.()
|
||||||
if (!result) {
|
if (!result) {
|
||||||
setDefaultServerUrl(undefined)
|
setStore("defaultServerUrl", undefined)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (result instanceof Promise) {
|
if (result instanceof Promise) {
|
||||||
result.then((url) => setDefaultServerUrl(url ? normalizeServerUrl(url) : undefined))
|
result.then((url) => setStore("defaultServerUrl", url ? normalizeServerUrl(url) : undefined))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
setDefaultServerUrl(normalizeServerUrl(result))
|
setStore("defaultServerUrl", normalizeServerUrl(result))
|
||||||
}
|
}
|
||||||
|
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
@@ -220,7 +219,7 @@ export function StatusPopover() {
|
|||||||
<For each={sortedServers()}>
|
<For each={sortedServers()}>
|
||||||
{(url) => {
|
{(url) => {
|
||||||
const isActive = () => url === server.url
|
const isActive = () => url === server.url
|
||||||
const isDefault = () => url === defaultServerUrl()
|
const isDefault = () => url === store.defaultServerUrl
|
||||||
const status = () => store.status[url]
|
const status = () => store.status[url]
|
||||||
const isBlocked = () => status()?.healthy === false
|
const isBlocked = () => status()?.healthy === false
|
||||||
const [truncated, setTruncated] = createSignal(false)
|
const [truncated, setTruncated] = createSignal(false)
|
||||||
@@ -329,7 +328,7 @@ export function StatusPopover() {
|
|||||||
type="button"
|
type="button"
|
||||||
class="flex items-center gap-2 w-full h-8 pl-3 pr-2 py-1 rounded-md hover:bg-surface-raised-base-hover transition-colors text-left"
|
class="flex items-center gap-2 w-full h-8 pl-3 pr-2 py-1 rounded-md hover:bg-surface-raised-base-hover transition-colors text-left"
|
||||||
onClick={() => toggleMcp(item.name)}
|
onClick={() => toggleMcp(item.name)}
|
||||||
disabled={loading() === item.name}
|
disabled={store.loading === item.name}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
classList={{
|
classList={{
|
||||||
@@ -345,7 +344,7 @@ export function StatusPopover() {
|
|||||||
<div onClick={(event) => event.stopPropagation()}>
|
<div onClick={(event) => event.stopPropagation()}>
|
||||||
<Switch
|
<Switch
|
||||||
checked={enabled()}
|
checked={enabled()}
|
||||||
disabled={loading() === item.name}
|
disabled={store.loading === item.name}
|
||||||
onChange={() => toggleMcp(item.name)}
|
onChange={() => toggleMcp(item.name)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { createEffect, createMemo, createSignal, onCleanup, onMount, type Accessor } from "solid-js"
|
import { createEffect, createMemo, onCleanup, onMount, type Accessor } from "solid-js"
|
||||||
import { createStore } from "solid-js/store"
|
import { createStore } from "solid-js/store"
|
||||||
import { createSimpleContext } from "@opencode-ai/ui/context"
|
import { createSimpleContext } from "@opencode-ai/ui/context"
|
||||||
import { useDialog } from "@opencode-ai/ui/context/dialog"
|
import { useDialog } from "@opencode-ai/ui/context/dialog"
|
||||||
@@ -165,8 +165,10 @@ export const { use: useCommand, provider: CommandProvider } = createSimpleContex
|
|||||||
const dialog = useDialog()
|
const dialog = useDialog()
|
||||||
const settings = useSettings()
|
const settings = useSettings()
|
||||||
const language = useLanguage()
|
const language = useLanguage()
|
||||||
const [registrations, setRegistrations] = createSignal<Accessor<CommandOption[]>[]>([])
|
const [store, setStore] = createStore({
|
||||||
const [suspendCount, setSuspendCount] = createSignal(0)
|
registrations: [] as Accessor<CommandOption[]>[],
|
||||||
|
suspendCount: 0,
|
||||||
|
})
|
||||||
|
|
||||||
const [catalog, setCatalog, _, catalogReady] = persisted(
|
const [catalog, setCatalog, _, catalogReady] = persisted(
|
||||||
Persist.global("command.catalog.v1"),
|
Persist.global("command.catalog.v1"),
|
||||||
@@ -184,7 +186,7 @@ export const { use: useCommand, provider: CommandProvider } = createSimpleContex
|
|||||||
const seen = new Set<string>()
|
const seen = new Set<string>()
|
||||||
const all: CommandOption[] = []
|
const all: CommandOption[] = []
|
||||||
|
|
||||||
for (const reg of registrations()) {
|
for (const reg of store.registrations) {
|
||||||
for (const opt of reg()) {
|
for (const opt of reg()) {
|
||||||
if (seen.has(opt.id)) continue
|
if (seen.has(opt.id)) continue
|
||||||
seen.add(opt.id)
|
seen.add(opt.id)
|
||||||
@@ -230,7 +232,7 @@ export const { use: useCommand, provider: CommandProvider } = createSimpleContex
|
|||||||
]
|
]
|
||||||
})
|
})
|
||||||
|
|
||||||
const suspended = () => suspendCount() > 0
|
const suspended = () => store.suspendCount > 0
|
||||||
|
|
||||||
const palette = createMemo(() => {
|
const palette = createMemo(() => {
|
||||||
const config = settings.keybinds.get(PALETTE_ID) ?? DEFAULT_PALETTE_KEYBIND
|
const config = settings.keybinds.get(PALETTE_ID) ?? DEFAULT_PALETTE_KEYBIND
|
||||||
@@ -297,9 +299,9 @@ export const { use: useCommand, provider: CommandProvider } = createSimpleContex
|
|||||||
return {
|
return {
|
||||||
register(cb: () => CommandOption[]) {
|
register(cb: () => CommandOption[]) {
|
||||||
const results = createMemo(cb)
|
const results = createMemo(cb)
|
||||||
setRegistrations((arr) => [results, ...arr])
|
setStore("registrations", (arr) => [results, ...arr])
|
||||||
onCleanup(() => {
|
onCleanup(() => {
|
||||||
setRegistrations((arr) => arr.filter((x) => x !== results))
|
setStore("registrations", (arr) => arr.filter((x) => x !== results))
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
trigger(id: string, source?: "palette" | "keybind" | "slash") {
|
trigger(id: string, source?: "palette" | "keybind" | "slash") {
|
||||||
@@ -321,7 +323,7 @@ export const { use: useCommand, provider: CommandProvider } = createSimpleContex
|
|||||||
},
|
},
|
||||||
show: showPalette,
|
show: showPalette,
|
||||||
keybinds(enabled: boolean) {
|
keybinds(enabled: boolean) {
|
||||||
setSuspendCount((count) => count + (enabled ? -1 : 1))
|
setStore("suspendCount", (count) => count + (enabled ? -1 : 1))
|
||||||
},
|
},
|
||||||
suspended,
|
suspended,
|
||||||
get catalog() {
|
get catalog() {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { batch, createMemo, createRoot, createSignal, onCleanup } from "solid-js"
|
import { batch, createMemo, createRoot, onCleanup } from "solid-js"
|
||||||
import { createStore } from "solid-js/store"
|
import { createStore } from "solid-js/store"
|
||||||
import { createSimpleContext } from "@opencode-ai/ui/context"
|
import { createSimpleContext } from "@opencode-ai/ui/context"
|
||||||
import { useParams } from "@solidjs/router"
|
import { useParams } from "@solidjs/router"
|
||||||
@@ -37,8 +37,16 @@ function createCommentSession(dir: string, id: string | undefined) {
|
|||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
const [focus, setFocus] = createSignal<CommentFocus | null>(null)
|
const [state, setState] = createStore({
|
||||||
const [active, setActive] = createSignal<CommentFocus | null>(null)
|
focus: null as CommentFocus | null,
|
||||||
|
active: null as CommentFocus | null,
|
||||||
|
})
|
||||||
|
|
||||||
|
const setFocus = (value: CommentFocus | null | ((value: CommentFocus | null) => CommentFocus | null)) =>
|
||||||
|
setState("focus", value)
|
||||||
|
|
||||||
|
const setActive = (value: CommentFocus | null | ((value: CommentFocus | null) => CommentFocus | null)) =>
|
||||||
|
setState("active", value)
|
||||||
|
|
||||||
const list = (file: string) => store.comments[file] ?? []
|
const list = (file: string) => store.comments[file] ?? []
|
||||||
|
|
||||||
@@ -74,10 +82,10 @@ function createCommentSession(dir: string, id: string | undefined) {
|
|||||||
all,
|
all,
|
||||||
add,
|
add,
|
||||||
remove,
|
remove,
|
||||||
focus: createMemo(() => focus()),
|
focus: createMemo(() => state.focus),
|
||||||
setFocus,
|
setFocus,
|
||||||
clearFocus: () => setFocus(null),
|
clearFocus: () => setFocus(null),
|
||||||
active: createMemo(() => active()),
|
active: createMemo(() => state.active),
|
||||||
setActive,
|
setActive,
|
||||||
clearActive: () => setActive(null),
|
clearActive: () => setActive(null),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { createOpencodeClient } from "@opencode-ai/sdk/v2/client"
|
import { createOpencodeClient } from "@opencode-ai/sdk/v2/client"
|
||||||
import { createSimpleContext } from "@opencode-ai/ui/context"
|
import { createSimpleContext } from "@opencode-ai/ui/context"
|
||||||
import { batch, createEffect, createMemo, createSignal, onCleanup } from "solid-js"
|
import { batch, createEffect, createMemo, onCleanup } from "solid-js"
|
||||||
import { createStore } from "solid-js/store"
|
import { createStore } from "solid-js/store"
|
||||||
import { usePlatform } from "@/context/platform"
|
import { usePlatform } from "@/context/platform"
|
||||||
import { Persist, persisted } from "@/utils/persist"
|
import { Persist, persisted } from "@/utils/persist"
|
||||||
@@ -40,12 +40,17 @@ export const { use: useServer, provider: ServerProvider } = createSimpleContext(
|
|||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
const [active, setActiveRaw] = createSignal("")
|
const [state, setState] = createStore({
|
||||||
|
active: "",
|
||||||
|
healthy: undefined as boolean | undefined,
|
||||||
|
})
|
||||||
|
|
||||||
|
const healthy = () => state.healthy
|
||||||
|
|
||||||
function setActive(input: string) {
|
function setActive(input: string) {
|
||||||
const url = normalizeServerUrl(input)
|
const url = normalizeServerUrl(input)
|
||||||
if (!url) return
|
if (!url) return
|
||||||
setActiveRaw(url)
|
setState("active", url)
|
||||||
}
|
}
|
||||||
|
|
||||||
function add(input: string) {
|
function add(input: string) {
|
||||||
@@ -54,7 +59,7 @@ export const { use: useServer, provider: ServerProvider } = createSimpleContext(
|
|||||||
|
|
||||||
const fallback = normalizeServerUrl(props.defaultUrl)
|
const fallback = normalizeServerUrl(props.defaultUrl)
|
||||||
if (fallback && url === fallback) {
|
if (fallback && url === fallback) {
|
||||||
setActiveRaw(url)
|
setState("active", url)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -62,7 +67,7 @@ export const { use: useServer, provider: ServerProvider } = createSimpleContext(
|
|||||||
if (!store.list.includes(url)) {
|
if (!store.list.includes(url)) {
|
||||||
setStore("list", store.list.length, url)
|
setStore("list", store.list.length, url)
|
||||||
}
|
}
|
||||||
setActiveRaw(url)
|
setState("active", url)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -71,25 +76,23 @@ export const { use: useServer, provider: ServerProvider } = createSimpleContext(
|
|||||||
if (!url) return
|
if (!url) return
|
||||||
|
|
||||||
const list = store.list.filter((x) => x !== url)
|
const list = store.list.filter((x) => x !== url)
|
||||||
const next = active() === url ? (list[0] ?? normalizeServerUrl(props.defaultUrl) ?? "") : active()
|
const next = state.active === url ? (list[0] ?? normalizeServerUrl(props.defaultUrl) ?? "") : state.active
|
||||||
|
|
||||||
batch(() => {
|
batch(() => {
|
||||||
setStore("list", list)
|
setStore("list", list)
|
||||||
setActiveRaw(next)
|
setState("active", next)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
if (!ready()) return
|
if (!ready()) return
|
||||||
if (active()) return
|
if (state.active) return
|
||||||
const url = normalizeServerUrl(props.defaultUrl)
|
const url = normalizeServerUrl(props.defaultUrl)
|
||||||
if (!url) return
|
if (!url) return
|
||||||
setActiveRaw(url)
|
setState("active", url)
|
||||||
})
|
})
|
||||||
|
|
||||||
const isReady = createMemo(() => ready() && !!active())
|
const isReady = createMemo(() => ready() && !!state.active)
|
||||||
|
|
||||||
const [healthy, setHealthy] = createSignal<boolean | undefined>(undefined)
|
|
||||||
|
|
||||||
const check = (url: string) => {
|
const check = (url: string) => {
|
||||||
const sdk = createOpencodeClient({
|
const sdk = createOpencodeClient({
|
||||||
@@ -104,10 +107,10 @@ export const { use: useServer, provider: ServerProvider } = createSimpleContext(
|
|||||||
}
|
}
|
||||||
|
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
const url = active()
|
const url = state.active
|
||||||
if (!url) return
|
if (!url) return
|
||||||
|
|
||||||
setHealthy(undefined)
|
setState("healthy", undefined)
|
||||||
|
|
||||||
let alive = true
|
let alive = true
|
||||||
let busy = false
|
let busy = false
|
||||||
@@ -118,7 +121,7 @@ export const { use: useServer, provider: ServerProvider } = createSimpleContext(
|
|||||||
void check(url)
|
void check(url)
|
||||||
.then((next) => {
|
.then((next) => {
|
||||||
if (!alive) return
|
if (!alive) return
|
||||||
setHealthy(next)
|
setState("healthy", next)
|
||||||
})
|
})
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
busy = false
|
busy = false
|
||||||
@@ -134,7 +137,7 @@ export const { use: useServer, provider: ServerProvider } = createSimpleContext(
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
const origin = createMemo(() => projectsKey(active()))
|
const origin = createMemo(() => projectsKey(state.active))
|
||||||
const projectsList = createMemo(() => store.projects[origin()] ?? [])
|
const projectsList = createMemo(() => store.projects[origin()] ?? [])
|
||||||
const isLocal = createMemo(() => origin() === "local")
|
const isLocal = createMemo(() => origin() === "local")
|
||||||
|
|
||||||
@@ -143,10 +146,10 @@ export const { use: useServer, provider: ServerProvider } = createSimpleContext(
|
|||||||
healthy,
|
healthy,
|
||||||
isLocal,
|
isLocal,
|
||||||
get url() {
|
get url() {
|
||||||
return active()
|
return state.active
|
||||||
},
|
},
|
||||||
get name() {
|
get name() {
|
||||||
return serverDisplayName(active())
|
return serverDisplayName(state.active)
|
||||||
},
|
},
|
||||||
get list() {
|
get list() {
|
||||||
return store.list
|
return store.list
|
||||||
|
|||||||
@@ -91,7 +91,6 @@ export default function Layout(props: ParentProps) {
|
|||||||
let scrollContainerRef: HTMLDivElement | undefined
|
let scrollContainerRef: HTMLDivElement | undefined
|
||||||
|
|
||||||
const params = useParams()
|
const params = useParams()
|
||||||
const [autoselect, setAutoselect] = createSignal(!params.dir)
|
|
||||||
const globalSDK = useGlobalSDK()
|
const globalSDK = useGlobalSDK()
|
||||||
const globalSync = useGlobalSync()
|
const globalSync = useGlobalSync()
|
||||||
const layout = useLayout()
|
const layout = useLayout()
|
||||||
@@ -117,27 +116,31 @@ export default function Layout(props: ParentProps) {
|
|||||||
}
|
}
|
||||||
const colorSchemeLabel = (scheme: ColorScheme) => language.t(colorSchemeKey[scheme])
|
const colorSchemeLabel = (scheme: ColorScheme) => language.t(colorSchemeKey[scheme])
|
||||||
|
|
||||||
|
const [state, setState] = createStore({
|
||||||
|
autoselect: !params.dir,
|
||||||
|
busyWorkspaces: new Set<string>(),
|
||||||
|
hoverSession: undefined as string | undefined,
|
||||||
|
hoverProject: undefined as string | undefined,
|
||||||
|
scrollSessionKey: undefined as string | undefined,
|
||||||
|
nav: undefined as HTMLElement | undefined,
|
||||||
|
})
|
||||||
|
|
||||||
const [editor, setEditor] = createStore({
|
const [editor, setEditor] = createStore({
|
||||||
active: "" as string,
|
active: "" as string,
|
||||||
value: "",
|
value: "",
|
||||||
})
|
})
|
||||||
const [busyWorkspaces, setBusyWorkspaces] = createSignal<Set<string>>(new Set())
|
|
||||||
const setBusy = (directory: string, value: boolean) => {
|
const setBusy = (directory: string, value: boolean) => {
|
||||||
const key = workspaceKey(directory)
|
const key = workspaceKey(directory)
|
||||||
setBusyWorkspaces((prev) => {
|
setState("busyWorkspaces", (prev) => {
|
||||||
const next = new Set(prev)
|
const next = new Set(prev)
|
||||||
if (value) next.add(key)
|
if (value) next.add(key)
|
||||||
else next.delete(key)
|
else next.delete(key)
|
||||||
return next
|
return next
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
const isBusy = (directory: string) => busyWorkspaces().has(workspaceKey(directory))
|
const isBusy = (directory: string) => state.busyWorkspaces.has(workspaceKey(directory))
|
||||||
const editorRef = { current: undefined as HTMLInputElement | undefined }
|
const editorRef = { current: undefined as HTMLInputElement | undefined }
|
||||||
|
|
||||||
const [hoverSession, setHoverSession] = createSignal<string | undefined>()
|
|
||||||
const [hoverProject, setHoverProject] = createSignal<string | undefined>()
|
|
||||||
|
|
||||||
const [nav, setNav] = createSignal<HTMLElement | undefined>(undefined)
|
|
||||||
const navLeave = { current: undefined as number | undefined }
|
const navLeave = { current: undefined as number | undefined }
|
||||||
|
|
||||||
onCleanup(() => {
|
onCleanup(() => {
|
||||||
@@ -145,18 +148,18 @@ export default function Layout(props: ParentProps) {
|
|||||||
clearTimeout(navLeave.current)
|
clearTimeout(navLeave.current)
|
||||||
})
|
})
|
||||||
|
|
||||||
const sidebarHovering = createMemo(() => !layout.sidebar.opened() && hoverProject() !== undefined)
|
const sidebarHovering = createMemo(() => !layout.sidebar.opened() && state.hoverProject !== undefined)
|
||||||
const sidebarExpanded = createMemo(() => layout.sidebar.opened() || sidebarHovering())
|
const sidebarExpanded = createMemo(() => layout.sidebar.opened() || sidebarHovering())
|
||||||
|
|
||||||
const hoverProjectData = createMemo(() => {
|
const hoverProjectData = createMemo(() => {
|
||||||
const id = hoverProject()
|
const id = state.hoverProject
|
||||||
if (!id) return
|
if (!id) return
|
||||||
return layout.projects.list().find((project) => project.worktree === id)
|
return layout.projects.list().find((project) => project.worktree === id)
|
||||||
})
|
})
|
||||||
|
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
if (!layout.sidebar.opened()) return
|
if (!layout.sidebar.opened()) return
|
||||||
setHoverProject(undefined)
|
setState("hoverProject", undefined)
|
||||||
})
|
})
|
||||||
|
|
||||||
createEffect(
|
createEffect(
|
||||||
@@ -164,9 +167,9 @@ export default function Layout(props: ParentProps) {
|
|||||||
() => ({ dir: params.dir, id: params.id }),
|
() => ({ dir: params.dir, id: params.id }),
|
||||||
() => {
|
() => {
|
||||||
if (layout.sidebar.opened()) return
|
if (layout.sidebar.opened()) return
|
||||||
if (!hoverProject()) return
|
if (!state.hoverProject) return
|
||||||
setHoverSession(undefined)
|
setState("hoverSession", undefined)
|
||||||
setHoverProject(undefined)
|
setState("hoverProject", undefined)
|
||||||
},
|
},
|
||||||
{ defer: true },
|
{ defer: true },
|
||||||
),
|
),
|
||||||
@@ -175,7 +178,7 @@ export default function Layout(props: ParentProps) {
|
|||||||
const autoselecting = createMemo(() => {
|
const autoselecting = createMemo(() => {
|
||||||
if (params.dir) return false
|
if (params.dir) return false
|
||||||
if (initialDir) return false
|
if (initialDir) return false
|
||||||
if (!autoselect()) return false
|
if (!state.autoselect) return false
|
||||||
if (!pageReady()) return true
|
if (!pageReady()) return true
|
||||||
if (!layoutReady()) return true
|
if (!layoutReady()) return true
|
||||||
const list = layout.projects.list()
|
const list = layout.projects.list()
|
||||||
@@ -483,20 +486,18 @@ export default function Layout(props: ParentProps) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const [scrollSessionKey, setScrollSessionKey] = createSignal<string | undefined>(undefined)
|
|
||||||
|
|
||||||
function scrollToSession(sessionId: string, sessionKey: string) {
|
function scrollToSession(sessionId: string, sessionKey: string) {
|
||||||
if (!scrollContainerRef) return
|
if (!scrollContainerRef) return
|
||||||
if (scrollSessionKey() === sessionKey) return
|
if (state.scrollSessionKey === sessionKey) return
|
||||||
const element = scrollContainerRef.querySelector(`[data-session-id="${sessionId}"]`)
|
const element = scrollContainerRef.querySelector(`[data-session-id="${sessionId}"]`)
|
||||||
if (!element) return
|
if (!element) return
|
||||||
const containerRect = scrollContainerRef.getBoundingClientRect()
|
const containerRect = scrollContainerRef.getBoundingClientRect()
|
||||||
const elementRect = element.getBoundingClientRect()
|
const elementRect = element.getBoundingClientRect()
|
||||||
if (elementRect.top >= containerRect.top && elementRect.bottom <= containerRect.bottom) {
|
if (elementRect.top >= containerRect.top && elementRect.bottom <= containerRect.bottom) {
|
||||||
setScrollSessionKey(sessionKey)
|
setState("scrollSessionKey", sessionKey)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
setScrollSessionKey(sessionKey)
|
setState("scrollSessionKey", sessionKey)
|
||||||
element.scrollIntoView({ block: "nearest", behavior: "smooth" })
|
element.scrollIntoView({ block: "nearest", behavior: "smooth" })
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -544,7 +545,7 @@ export default function Layout(props: ParentProps) {
|
|||||||
(value) => {
|
(value) => {
|
||||||
if (!value.ready) return
|
if (!value.ready) return
|
||||||
if (!value.layoutReady) return
|
if (!value.layoutReady) return
|
||||||
if (!autoselect()) return
|
if (!state.autoselect) return
|
||||||
if (initialDir) return
|
if (initialDir) return
|
||||||
if (value.dir) return
|
if (value.dir) return
|
||||||
if (value.list.length === 0) return
|
if (value.list.length === 0) return
|
||||||
@@ -552,7 +553,7 @@ export default function Layout(props: ParentProps) {
|
|||||||
const last = server.projects.last()
|
const last = server.projects.last()
|
||||||
const next = value.list.find((project) => project.worktree === last) ?? value.list[0]
|
const next = value.list.find((project) => project.worktree === last) ?? value.list[0]
|
||||||
if (!next) return
|
if (!next) return
|
||||||
setAutoselect(false)
|
setState("autoselect", false)
|
||||||
openProject(next.worktree, false)
|
openProject(next.worktree, false)
|
||||||
navigateToProject(next.worktree)
|
navigateToProject(next.worktree)
|
||||||
},
|
},
|
||||||
@@ -1066,8 +1067,8 @@ export default function Layout(props: ParentProps) {
|
|||||||
function navigateToProject(directory: string | undefined) {
|
function navigateToProject(directory: string | undefined) {
|
||||||
if (!directory) return
|
if (!directory) return
|
||||||
if (!layout.sidebar.opened()) {
|
if (!layout.sidebar.opened()) {
|
||||||
setHoverSession(undefined)
|
setState("hoverSession", undefined)
|
||||||
setHoverProject(undefined)
|
setState("hoverProject", undefined)
|
||||||
}
|
}
|
||||||
server.projects.touch(directory)
|
server.projects.touch(directory)
|
||||||
const lastSession = store.lastSession[directory]
|
const lastSession = store.lastSession[directory]
|
||||||
@@ -1078,8 +1079,8 @@ export default function Layout(props: ParentProps) {
|
|||||||
function navigateToSession(session: Session | undefined) {
|
function navigateToSession(session: Session | undefined) {
|
||||||
if (!session) return
|
if (!session) return
|
||||||
if (!layout.sidebar.opened()) {
|
if (!layout.sidebar.opened()) {
|
||||||
setHoverSession(undefined)
|
setState("hoverSession", undefined)
|
||||||
setHoverProject(undefined)
|
setState("hoverProject", undefined)
|
||||||
}
|
}
|
||||||
navigate(`/${base64Encode(session.directory)}/session/${session.id}`)
|
navigate(`/${base64Encode(session.directory)}/session/${session.id}`)
|
||||||
layout.mobileSidebar.hide()
|
layout.mobileSidebar.hide()
|
||||||
@@ -1472,7 +1473,7 @@ export default function Layout(props: ParentProps) {
|
|||||||
function handleDragStart(event: unknown) {
|
function handleDragStart(event: unknown) {
|
||||||
const id = getDraggableId(event)
|
const id = getDraggableId(event)
|
||||||
if (!id) return
|
if (!id) return
|
||||||
setHoverProject(undefined)
|
setState("hoverProject", undefined)
|
||||||
setStore("activeProject", id)
|
setStore("activeProject", id)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1632,8 +1633,10 @@ export default function Layout(props: ParentProps) {
|
|||||||
const hoverAllowed = createMemo(() => !props.mobile && sidebarExpanded())
|
const hoverAllowed = createMemo(() => !props.mobile && sidebarExpanded())
|
||||||
const hoverEnabled = createMemo(() => (props.popover ?? true) && hoverAllowed())
|
const hoverEnabled = createMemo(() => (props.popover ?? true) && hoverAllowed())
|
||||||
const isActive = createMemo(() => props.session.id === params.id)
|
const isActive = createMemo(() => props.session.id === params.id)
|
||||||
const [menuOpen, setMenuOpen] = createSignal(false)
|
const [menu, setMenu] = createStore({
|
||||||
const [pendingRename, setPendingRename] = createSignal(false)
|
open: false,
|
||||||
|
pendingRename: false,
|
||||||
|
})
|
||||||
|
|
||||||
const messageLabel = (message: Message) => {
|
const messageLabel = (message: Message) => {
|
||||||
const parts = sessionStore.part[message.id] ?? []
|
const parts = sessionStore.part[message.id] ?? []
|
||||||
@@ -1644,13 +1647,13 @@ export default function Layout(props: ParentProps) {
|
|||||||
const item = (
|
const item = (
|
||||||
<A
|
<A
|
||||||
href={`${props.slug}/session/${props.session.id}`}
|
href={`${props.slug}/session/${props.session.id}`}
|
||||||
class={`flex items-center justify-between gap-3 min-w-0 text-left w-full focus:outline-none transition-[padding] ${menuOpen() ? "pr-7" : ""} group-hover/session:pr-7 group-focus-within/session:pr-7 group-active/session:pr-7 ${props.dense ? "py-0.5" : "py-1"}`}
|
class={`flex items-center justify-between gap-3 min-w-0 text-left w-full focus:outline-none transition-[padding] ${menu.open ? "pr-7" : ""} group-hover/session:pr-7 group-focus-within/session:pr-7 group-active/session:pr-7 ${props.dense ? "py-0.5" : "py-1"}`}
|
||||||
onMouseEnter={() => prefetchSession(props.session, "high")}
|
onMouseEnter={() => prefetchSession(props.session, "high")}
|
||||||
onFocus={() => prefetchSession(props.session, "high")}
|
onFocus={() => prefetchSession(props.session, "high")}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setHoverSession(undefined)
|
setState("hoverSession", undefined)
|
||||||
if (layout.sidebar.opened()) return
|
if (layout.sidebar.opened()) return
|
||||||
queueMicrotask(() => setHoverProject(undefined))
|
queueMicrotask(() => setState("hoverProject", undefined))
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div class="flex items-center gap-1 w-full">
|
<div class="flex items-center gap-1 w-full">
|
||||||
@@ -1713,9 +1716,9 @@ export default function Layout(props: ParentProps) {
|
|||||||
gutter={16}
|
gutter={16}
|
||||||
shift={-2}
|
shift={-2}
|
||||||
trigger={item}
|
trigger={item}
|
||||||
mount={!props.mobile ? nav() : undefined}
|
mount={!props.mobile ? state.nav : undefined}
|
||||||
open={hoverSession() === props.session.id}
|
open={state.hoverSession === props.session.id}
|
||||||
onOpenChange={(open) => setHoverSession(open ? props.session.id : undefined)}
|
onOpenChange={(open) => setState("hoverSession", open ? props.session.id : undefined)}
|
||||||
>
|
>
|
||||||
<Show
|
<Show
|
||||||
when={hoverReady()}
|
when={hoverReady()}
|
||||||
@@ -1745,13 +1748,13 @@ export default function Layout(props: ParentProps) {
|
|||||||
<div
|
<div
|
||||||
class={`absolute ${props.dense ? "top-0.5 right-0.5" : "top-1 right-1"} flex items-center gap-0.5 transition-opacity`}
|
class={`absolute ${props.dense ? "top-0.5 right-0.5" : "top-1 right-1"} flex items-center gap-0.5 transition-opacity`}
|
||||||
classList={{
|
classList={{
|
||||||
"opacity-100 pointer-events-auto": menuOpen(),
|
"opacity-100 pointer-events-auto": menu.open,
|
||||||
"opacity-0 pointer-events-none": !menuOpen(),
|
"opacity-0 pointer-events-none": !menu.open,
|
||||||
"group-hover/session:opacity-100 group-hover/session:pointer-events-auto": true,
|
"group-hover/session:opacity-100 group-hover/session:pointer-events-auto": true,
|
||||||
"group-focus-within/session:opacity-100 group-focus-within/session:pointer-events-auto": true,
|
"group-focus-within/session:opacity-100 group-focus-within/session:pointer-events-auto": true,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<DropdownMenu modal={!sidebarHovering()} open={menuOpen()} onOpenChange={setMenuOpen}>
|
<DropdownMenu modal={!sidebarHovering()} open={menu.open} onOpenChange={(open) => setMenu("open", open)}>
|
||||||
<Tooltip value={language.t("common.moreOptions")} placement="top">
|
<Tooltip value={language.t("common.moreOptions")} placement="top">
|
||||||
<DropdownMenu.Trigger
|
<DropdownMenu.Trigger
|
||||||
as={IconButton}
|
as={IconButton}
|
||||||
@@ -1761,19 +1764,19 @@ export default function Layout(props: ParentProps) {
|
|||||||
aria-label={language.t("common.moreOptions")}
|
aria-label={language.t("common.moreOptions")}
|
||||||
/>
|
/>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<DropdownMenu.Portal mount={!props.mobile ? nav() : undefined}>
|
<DropdownMenu.Portal mount={!props.mobile ? state.nav : undefined}>
|
||||||
<DropdownMenu.Content
|
<DropdownMenu.Content
|
||||||
onCloseAutoFocus={(event) => {
|
onCloseAutoFocus={(event) => {
|
||||||
if (!pendingRename()) return
|
if (!menu.pendingRename) return
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
setPendingRename(false)
|
setMenu("pendingRename", false)
|
||||||
openEditor(`session:${props.session.id}`, props.session.title)
|
openEditor(`session:${props.session.id}`, props.session.title)
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<DropdownMenu.Item
|
<DropdownMenu.Item
|
||||||
onSelect={() => {
|
onSelect={() => {
|
||||||
setPendingRename(true)
|
setMenu("pendingRename", true)
|
||||||
setMenuOpen(false)
|
setMenu("open", false)
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<DropdownMenu.ItemLabel>{language.t("common.rename")}</DropdownMenu.ItemLabel>
|
<DropdownMenu.ItemLabel>{language.t("common.rename")}</DropdownMenu.ItemLabel>
|
||||||
@@ -1802,9 +1805,9 @@ export default function Layout(props: ParentProps) {
|
|||||||
end
|
end
|
||||||
class={`flex items-center justify-between gap-3 min-w-0 text-left w-full focus:outline-none ${props.dense ? "py-0.5" : "py-1"}`}
|
class={`flex items-center justify-between gap-3 min-w-0 text-left w-full focus:outline-none ${props.dense ? "py-0.5" : "py-1"}`}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setHoverSession(undefined)
|
setState("hoverSession", undefined)
|
||||||
if (layout.sidebar.opened()) return
|
if (layout.sidebar.opened()) return
|
||||||
queueMicrotask(() => setHoverProject(undefined))
|
queueMicrotask(() => setState("hoverProject", undefined))
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div class="flex items-center gap-1 w-full">
|
<div class="flex items-center gap-1 w-full">
|
||||||
@@ -1884,8 +1887,10 @@ export default function Layout(props: ParentProps) {
|
|||||||
const SortableWorkspace = (props: { directory: string; project: LocalProject; mobile?: boolean }): JSX.Element => {
|
const SortableWorkspace = (props: { directory: string; project: LocalProject; mobile?: boolean }): JSX.Element => {
|
||||||
const sortable = createSortable(props.directory)
|
const sortable = createSortable(props.directory)
|
||||||
const [workspaceStore, setWorkspaceStore] = globalSync.child(props.directory, { bootstrap: false })
|
const [workspaceStore, setWorkspaceStore] = globalSync.child(props.directory, { bootstrap: false })
|
||||||
const [menuOpen, setMenuOpen] = createSignal(false)
|
const [menu, setMenu] = createStore({
|
||||||
const [pendingRename, setPendingRename] = createSignal(false)
|
open: false,
|
||||||
|
pendingRename: false,
|
||||||
|
})
|
||||||
const slug = createMemo(() => base64Encode(props.directory))
|
const slug = createMemo(() => base64Encode(props.directory))
|
||||||
const sessions = createMemo(() =>
|
const sessions = createMemo(() =>
|
||||||
workspaceStore.session
|
workspaceStore.session
|
||||||
@@ -1995,13 +2000,17 @@ export default function Layout(props: ParentProps) {
|
|||||||
<div
|
<div
|
||||||
class="absolute right-1 top-1/2 -translate-y-1/2 flex items-center gap-0.5 transition-opacity"
|
class="absolute right-1 top-1/2 -translate-y-1/2 flex items-center gap-0.5 transition-opacity"
|
||||||
classList={{
|
classList={{
|
||||||
"opacity-100 pointer-events-auto": menuOpen(),
|
"opacity-100 pointer-events-auto": menu.open,
|
||||||
"opacity-0 pointer-events-none": !menuOpen(),
|
"opacity-0 pointer-events-none": !menu.open,
|
||||||
"group-hover/workspace:opacity-100 group-hover/workspace:pointer-events-auto": true,
|
"group-hover/workspace:opacity-100 group-hover/workspace:pointer-events-auto": true,
|
||||||
"group-focus-within/workspace:opacity-100 group-focus-within/workspace:pointer-events-auto": true,
|
"group-focus-within/workspace:opacity-100 group-focus-within/workspace:pointer-events-auto": true,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<DropdownMenu modal={!sidebarHovering()} open={menuOpen()} onOpenChange={setMenuOpen}>
|
<DropdownMenu
|
||||||
|
modal={!sidebarHovering()}
|
||||||
|
open={menu.open}
|
||||||
|
onOpenChange={(open) => setMenu("open", open)}
|
||||||
|
>
|
||||||
<Tooltip value={language.t("common.moreOptions")} placement="top">
|
<Tooltip value={language.t("common.moreOptions")} placement="top">
|
||||||
<DropdownMenu.Trigger
|
<DropdownMenu.Trigger
|
||||||
as={IconButton}
|
as={IconButton}
|
||||||
@@ -2011,20 +2020,20 @@ export default function Layout(props: ParentProps) {
|
|||||||
aria-label={language.t("common.moreOptions")}
|
aria-label={language.t("common.moreOptions")}
|
||||||
/>
|
/>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<DropdownMenu.Portal mount={!props.mobile ? nav() : undefined}>
|
<DropdownMenu.Portal mount={!props.mobile ? state.nav : undefined}>
|
||||||
<DropdownMenu.Content
|
<DropdownMenu.Content
|
||||||
onCloseAutoFocus={(event) => {
|
onCloseAutoFocus={(event) => {
|
||||||
if (!pendingRename()) return
|
if (!menu.pendingRename) return
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
setPendingRename(false)
|
setMenu("pendingRename", false)
|
||||||
openEditor(`workspace:${props.directory}`, workspaceValue())
|
openEditor(`workspace:${props.directory}`, workspaceValue())
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<DropdownMenu.Item
|
<DropdownMenu.Item
|
||||||
disabled={local()}
|
disabled={local()}
|
||||||
onSelect={() => {
|
onSelect={() => {
|
||||||
setPendingRename(true)
|
setMenu("pendingRename", true)
|
||||||
setMenuOpen(false)
|
setMenu("open", false)
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<DropdownMenu.ItemLabel>{language.t("common.rename")}</DropdownMenu.ItemLabel>
|
<DropdownMenu.ItemLabel>{language.t("common.rename")}</DropdownMenu.ItemLabel>
|
||||||
@@ -2103,7 +2112,7 @@ export default function Layout(props: ParentProps) {
|
|||||||
|
|
||||||
const preview = createMemo(() => !props.mobile && layout.sidebar.opened())
|
const preview = createMemo(() => !props.mobile && layout.sidebar.opened())
|
||||||
const overlay = createMemo(() => !props.mobile && !layout.sidebar.opened())
|
const overlay = createMemo(() => !props.mobile && !layout.sidebar.opened())
|
||||||
const active = createMemo(() => (preview() ? open() : overlay() && hoverProject() === props.project.worktree))
|
const active = createMemo(() => (preview() ? open() : overlay() && state.hoverProject === props.project.worktree))
|
||||||
|
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
if (preview()) return
|
if (preview()) return
|
||||||
@@ -2155,14 +2164,14 @@ export default function Layout(props: ParentProps) {
|
|||||||
onMouseEnter={() => {
|
onMouseEnter={() => {
|
||||||
if (!overlay()) return
|
if (!overlay()) return
|
||||||
globalSync.child(props.project.worktree)
|
globalSync.child(props.project.worktree)
|
||||||
setHoverProject(props.project.worktree)
|
setState("hoverProject", props.project.worktree)
|
||||||
setHoverSession(undefined)
|
setState("hoverSession", undefined)
|
||||||
}}
|
}}
|
||||||
onFocus={() => {
|
onFocus={() => {
|
||||||
if (!overlay()) return
|
if (!overlay()) return
|
||||||
globalSync.child(props.project.worktree)
|
globalSync.child(props.project.worktree)
|
||||||
setHoverProject(props.project.worktree)
|
setState("hoverProject", props.project.worktree)
|
||||||
setHoverSession(undefined)
|
setState("hoverSession", undefined)
|
||||||
}}
|
}}
|
||||||
onClick={() => navigateToProject(props.project.worktree)}
|
onClick={() => navigateToProject(props.project.worktree)}
|
||||||
onBlur={() => setOpen(false)}
|
onBlur={() => setOpen(false)}
|
||||||
@@ -2184,7 +2193,7 @@ export default function Layout(props: ParentProps) {
|
|||||||
trigger={trigger}
|
trigger={trigger}
|
||||||
onOpenChange={(value) => {
|
onOpenChange={(value) => {
|
||||||
setOpen(value)
|
setOpen(value)
|
||||||
if (value) setHoverSession(undefined)
|
if (value) setState("hoverSession", undefined)
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div class="-m-3 p-2 flex flex-col w-72">
|
<div class="-m-3 p-2 flex flex-col w-72">
|
||||||
@@ -2323,8 +2332,8 @@ export default function Layout(props: ParentProps) {
|
|||||||
|
|
||||||
const createWorkspace = async (project: LocalProject) => {
|
const createWorkspace = async (project: LocalProject) => {
|
||||||
if (!layout.sidebar.opened()) {
|
if (!layout.sidebar.opened()) {
|
||||||
setHoverSession(undefined)
|
setState("hoverSession", undefined)
|
||||||
setHoverProject(undefined)
|
setState("hoverProject", undefined)
|
||||||
}
|
}
|
||||||
const created = await globalSDK.client.worktree
|
const created = await globalSDK.client.worktree
|
||||||
.create({ directory: project.worktree })
|
.create({ directory: project.worktree })
|
||||||
@@ -2427,7 +2436,7 @@ export default function Layout(props: ParentProps) {
|
|||||||
class="shrink-0 size-6 rounded-md opacity-0 group-hover/project:opacity-100 data-[expanded]:opacity-100 data-[expanded]:bg-surface-base-active"
|
class="shrink-0 size-6 rounded-md opacity-0 group-hover/project:opacity-100 data-[expanded]:opacity-100 data-[expanded]:bg-surface-base-active"
|
||||||
aria-label={language.t("common.moreOptions")}
|
aria-label={language.t("common.moreOptions")}
|
||||||
/>
|
/>
|
||||||
<DropdownMenu.Portal mount={!panelProps.mobile ? nav() : undefined}>
|
<DropdownMenu.Portal mount={!panelProps.mobile ? state.nav : undefined}>
|
||||||
<DropdownMenu.Content class="mt-1">
|
<DropdownMenu.Content class="mt-1">
|
||||||
<DropdownMenu.Item onSelect={() => dialog.show(() => <DialogEditProject project={p} />)}>
|
<DropdownMenu.Item onSelect={() => dialog.show(() => <DialogEditProject project={p} />)}>
|
||||||
<DropdownMenu.ItemLabel>{language.t("common.edit")}</DropdownMenu.ItemLabel>
|
<DropdownMenu.ItemLabel>{language.t("common.edit")}</DropdownMenu.ItemLabel>
|
||||||
@@ -2476,8 +2485,8 @@ export default function Layout(props: ParentProps) {
|
|||||||
class="w-full"
|
class="w-full"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (!layout.sidebar.opened()) {
|
if (!layout.sidebar.opened()) {
|
||||||
setHoverSession(undefined)
|
setState("hoverSession", undefined)
|
||||||
setHoverProject(undefined)
|
setState("hoverProject", undefined)
|
||||||
}
|
}
|
||||||
navigate(`/${base64Encode(p.worktree)}/session`)
|
navigate(`/${base64Encode(p.worktree)}/session`)
|
||||||
layout.mobileSidebar.hide()
|
layout.mobileSidebar.hide()
|
||||||
@@ -2668,7 +2677,7 @@ export default function Layout(props: ParentProps) {
|
|||||||
}}
|
}}
|
||||||
style={{ width: layout.sidebar.opened() ? `${Math.max(layout.sidebar.width(), 244)}px` : "64px" }}
|
style={{ width: layout.sidebar.opened() ? `${Math.max(layout.sidebar.width(), 244)}px` : "64px" }}
|
||||||
ref={(el) => {
|
ref={(el) => {
|
||||||
setNav(el)
|
setState("nav", el)
|
||||||
}}
|
}}
|
||||||
onMouseEnter={() => {
|
onMouseEnter={() => {
|
||||||
if (navLeave.current === undefined) return
|
if (navLeave.current === undefined) return
|
||||||
@@ -2681,8 +2690,8 @@ export default function Layout(props: ParentProps) {
|
|||||||
if (navLeave.current !== undefined) clearTimeout(navLeave.current)
|
if (navLeave.current !== undefined) clearTimeout(navLeave.current)
|
||||||
navLeave.current = window.setTimeout(() => {
|
navLeave.current = window.setTimeout(() => {
|
||||||
navLeave.current = undefined
|
navLeave.current = undefined
|
||||||
setHoverProject(undefined)
|
setState("hoverProject", undefined)
|
||||||
setHoverSession(undefined)
|
setState("hoverSession", undefined)
|
||||||
}, 300)
|
}, 300)
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { For, onCleanup, onMount, Show, Match, Switch, createMemo, createEffect, on, createSignal } from "solid-js"
|
import { For, onCleanup, onMount, Show, Match, Switch, createMemo, createEffect, on } from "solid-js"
|
||||||
import { createMediaQuery } from "@solid-primitives/media"
|
import { createMediaQuery } from "@solid-primitives/media"
|
||||||
import { createResizeObserver } from "@solid-primitives/resize-observer"
|
import { createResizeObserver } from "@solid-primitives/resize-observer"
|
||||||
import { Dynamic } from "solid-js/web"
|
import { Dynamic } from "solid-js/web"
|
||||||
@@ -198,12 +198,17 @@ export default function Page() {
|
|||||||
return next
|
return next
|
||||||
})
|
})
|
||||||
|
|
||||||
const [responding, setResponding] = createSignal(false)
|
const [ui, setUi] = createStore({
|
||||||
|
responding: false,
|
||||||
|
pendingMessage: undefined as string | undefined,
|
||||||
|
scrollGesture: 0,
|
||||||
|
autoCreated: false,
|
||||||
|
})
|
||||||
|
|
||||||
createEffect(
|
createEffect(
|
||||||
on(
|
on(
|
||||||
() => request()?.id,
|
() => request()?.id,
|
||||||
() => setResponding(false),
|
() => setUi("responding", false),
|
||||||
{ defer: true },
|
{ defer: true },
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
@@ -211,18 +216,17 @@ export default function Page() {
|
|||||||
const decide = (response: "once" | "always" | "reject") => {
|
const decide = (response: "once" | "always" | "reject") => {
|
||||||
const perm = request()
|
const perm = request()
|
||||||
if (!perm) return
|
if (!perm) return
|
||||||
if (responding()) return
|
if (ui.responding) return
|
||||||
|
|
||||||
setResponding(true)
|
setUi("responding", true)
|
||||||
sdk.client.permission
|
sdk.client.permission
|
||||||
.respond({ sessionID: perm.sessionID, permissionID: perm.id, response })
|
.respond({ sessionID: perm.sessionID, permissionID: perm.id, response })
|
||||||
.catch((err: unknown) => {
|
.catch((err: unknown) => {
|
||||||
const message = err instanceof Error ? err.message : String(err)
|
const message = err instanceof Error ? err.message : String(err)
|
||||||
showToast({ title: language.t("common.requestFailed"), description: message })
|
showToast({ title: language.t("common.requestFailed"), description: message })
|
||||||
})
|
})
|
||||||
.finally(() => setResponding(false))
|
.finally(() => setUi("responding", false))
|
||||||
}
|
}
|
||||||
const [pendingMessage, setPendingMessage] = createSignal<string | undefined>(undefined)
|
|
||||||
const sessionKey = createMemo(() => `${params.dir}${params.id ? "/" + params.id : ""}`)
|
const sessionKey = createMemo(() => `${params.dir}${params.id ? "/" + params.id : ""}`)
|
||||||
const tabs = createMemo(() => layout.tabs(sessionKey))
|
const tabs = createMemo(() => layout.tabs(sessionKey))
|
||||||
const view = createMemo(() => layout.view(sessionKey))
|
const view = createMemo(() => layout.view(sessionKey))
|
||||||
@@ -439,7 +443,6 @@ export default function Page() {
|
|||||||
let promptDock: HTMLDivElement | undefined
|
let promptDock: HTMLDivElement | undefined
|
||||||
let scroller: HTMLDivElement | undefined
|
let scroller: HTMLDivElement | undefined
|
||||||
|
|
||||||
const [scrollGesture, setScrollGesture] = createSignal(0)
|
|
||||||
const scrollGestureWindowMs = 250
|
const scrollGestureWindowMs = 250
|
||||||
|
|
||||||
const markScrollGesture = (target?: EventTarget | null) => {
|
const markScrollGesture = (target?: EventTarget | null) => {
|
||||||
@@ -450,26 +453,24 @@ export default function Page() {
|
|||||||
const nested = el?.closest("[data-scrollable]")
|
const nested = el?.closest("[data-scrollable]")
|
||||||
if (nested && nested !== root) return
|
if (nested && nested !== root) return
|
||||||
|
|
||||||
setScrollGesture(Date.now())
|
setUi("scrollGesture", Date.now())
|
||||||
}
|
}
|
||||||
|
|
||||||
const hasScrollGesture = () => Date.now() - scrollGesture() < scrollGestureWindowMs
|
const hasScrollGesture = () => Date.now() - ui.scrollGesture < scrollGestureWindowMs
|
||||||
|
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
if (!params.id) return
|
if (!params.id) return
|
||||||
sync.session.sync(params.id)
|
sync.session.sync(params.id)
|
||||||
})
|
})
|
||||||
|
|
||||||
const [autoCreated, setAutoCreated] = createSignal(false)
|
|
||||||
|
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
if (!view().terminal.opened()) {
|
if (!view().terminal.opened()) {
|
||||||
setAutoCreated(false)
|
setUi("autoCreated", false)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (!terminal.ready() || terminal.all().length !== 0 || autoCreated()) return
|
if (!terminal.ready() || terminal.all().length !== 0 || ui.autoCreated) return
|
||||||
terminal.new()
|
terminal.new()
|
||||||
setAutoCreated(true)
|
setUi("autoCreated", true)
|
||||||
})
|
})
|
||||||
|
|
||||||
createEffect(
|
createEffect(
|
||||||
@@ -1019,9 +1020,18 @@ export default function Page() {
|
|||||||
|
|
||||||
const showTabs = createMemo(() => view().reviewPanel.opened())
|
const showTabs = createMemo(() => view().reviewPanel.opened())
|
||||||
|
|
||||||
const [fileTreeTab, setFileTreeTab] = createSignal<"changes" | "all">("changes")
|
const [tree, setTree] = createStore({
|
||||||
const [reviewScroll, setReviewScroll] = createSignal<HTMLDivElement | undefined>(undefined)
|
fileTreeTab: "changes" as "changes" | "all",
|
||||||
const [pendingDiff, setPendingDiff] = createSignal<string | undefined>(undefined)
|
reviewScroll: undefined as HTMLDivElement | undefined,
|
||||||
|
pendingDiff: undefined as string | undefined,
|
||||||
|
})
|
||||||
|
|
||||||
|
const fileTreeTab = () => tree.fileTreeTab
|
||||||
|
const setFileTreeTab = (value: "changes" | "all") => setTree("fileTreeTab", value)
|
||||||
|
const reviewScroll = () => tree.reviewScroll
|
||||||
|
const setReviewScroll = (value: HTMLDivElement | undefined) => setTree("reviewScroll", value)
|
||||||
|
const pendingDiff = () => tree.pendingDiff
|
||||||
|
const setPendingDiff = (value: string | undefined) => setTree("pendingDiff", value)
|
||||||
|
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
if (!layout.fileTree.opened()) return
|
if (!layout.fileTree.opened()) return
|
||||||
@@ -1316,7 +1326,7 @@ export default function Page() {
|
|||||||
if (pendingSessionID !== sessionID) return
|
if (pendingSessionID !== sessionID) return
|
||||||
|
|
||||||
sessionStorage.removeItem("opencode.pendingMessage")
|
sessionStorage.removeItem("opencode.pendingMessage")
|
||||||
setPendingMessage(messageID)
|
setUi("pendingMessage", messageID)
|
||||||
})
|
})
|
||||||
|
|
||||||
const scrollToElement = (el: HTMLElement, behavior: ScrollBehavior) => {
|
const scrollToElement = (el: HTMLElement, behavior: ScrollBehavior) => {
|
||||||
@@ -1484,7 +1494,7 @@ export default function Page() {
|
|||||||
store.turnStart
|
store.turnStart
|
||||||
|
|
||||||
const targetId =
|
const targetId =
|
||||||
pendingMessage() ??
|
ui.pendingMessage ??
|
||||||
(() => {
|
(() => {
|
||||||
const hash = window.location.hash.slice(1)
|
const hash = window.location.hash.slice(1)
|
||||||
const match = hash.match(/^message-(.+)$/)
|
const match = hash.match(/^message-(.+)$/)
|
||||||
@@ -1496,7 +1506,7 @@ export default function Page() {
|
|||||||
|
|
||||||
const msg = visibleUserMessages().find((m) => m.id === targetId)
|
const msg = visibleUserMessages().find((m) => m.id === targetId)
|
||||||
if (!msg) return
|
if (!msg) return
|
||||||
if (pendingMessage() === targetId) setPendingMessage(undefined)
|
if (ui.pendingMessage === targetId) setUi("pendingMessage", undefined)
|
||||||
requestAnimationFrame(() => scrollToMessage(msg, "auto"))
|
requestAnimationFrame(() => scrollToMessage(msg, "auto"))
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -1877,18 +1887,18 @@ export default function Page() {
|
|||||||
</BasicTool>
|
</BasicTool>
|
||||||
<div data-component="permission-prompt">
|
<div data-component="permission-prompt">
|
||||||
<div data-slot="permission-actions">
|
<div data-slot="permission-actions">
|
||||||
<Button variant="ghost" size="small" onClick={() => decide("reject")} disabled={responding()}>
|
<Button variant="ghost" size="small" onClick={() => decide("reject")} disabled={ui.responding}>
|
||||||
{language.t("ui.permission.deny")}
|
{language.t("ui.permission.deny")}
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
size="small"
|
size="small"
|
||||||
onClick={() => decide("always")}
|
onClick={() => decide("always")}
|
||||||
disabled={responding()}
|
disabled={ui.responding}
|
||||||
>
|
>
|
||||||
{language.t("ui.permission.allowAlways")}
|
{language.t("ui.permission.allowAlways")}
|
||||||
</Button>
|
</Button>
|
||||||
<Button variant="primary" size="small" onClick={() => decide("once")} disabled={responding()}>
|
<Button variant="primary" size="small" onClick={() => decide("once")} disabled={ui.responding}>
|
||||||
{language.t("ui.permission.allowOnce")}
|
{language.t("ui.permission.allowOnce")}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
@@ -2144,11 +2154,40 @@ export default function Page() {
|
|||||||
|
|
||||||
const commentedLines = createMemo(() => fileComments().map((comment) => comment.selection))
|
const commentedLines = createMemo(() => fileComments().map((comment) => comment.selection))
|
||||||
|
|
||||||
const [openedComment, setOpenedComment] = createSignal<string | null>(null)
|
const [note, setNote] = createStore({
|
||||||
const [commenting, setCommenting] = createSignal<SelectedLineRange | null>(null)
|
openedComment: null as string | null,
|
||||||
const [draft, setDraft] = createSignal("")
|
commenting: null as SelectedLineRange | null,
|
||||||
const [positions, setPositions] = createSignal<Record<string, number>>({})
|
draft: "",
|
||||||
const [draftTop, setDraftTop] = createSignal<number | undefined>(undefined)
|
positions: {} as Record<string, number>,
|
||||||
|
draftTop: undefined as number | undefined,
|
||||||
|
})
|
||||||
|
|
||||||
|
const openedComment = () => note.openedComment
|
||||||
|
const setOpenedComment = (
|
||||||
|
value:
|
||||||
|
| typeof note.openedComment
|
||||||
|
| ((value: typeof note.openedComment) => typeof note.openedComment),
|
||||||
|
) => setNote("openedComment", value)
|
||||||
|
|
||||||
|
const commenting = () => note.commenting
|
||||||
|
const setCommenting = (
|
||||||
|
value: typeof note.commenting | ((value: typeof note.commenting) => typeof note.commenting),
|
||||||
|
) => setNote("commenting", value)
|
||||||
|
|
||||||
|
const draft = () => note.draft
|
||||||
|
const setDraft = (
|
||||||
|
value: typeof note.draft | ((value: typeof note.draft) => typeof note.draft),
|
||||||
|
) => setNote("draft", value)
|
||||||
|
|
||||||
|
const positions = () => note.positions
|
||||||
|
const setPositions = (
|
||||||
|
value: typeof note.positions | ((value: typeof note.positions) => typeof note.positions),
|
||||||
|
) => setNote("positions", value)
|
||||||
|
|
||||||
|
const draftTop = () => note.draftTop
|
||||||
|
const setDraftTop = (
|
||||||
|
value: typeof note.draftTop | ((value: typeof note.draftTop) => typeof note.draftTop),
|
||||||
|
) => setNote("draftTop", value)
|
||||||
|
|
||||||
const commentLabel = (range: SelectedLineRange) => {
|
const commentLabel = (range: SelectedLineRange) => {
|
||||||
const start = Math.min(range.start, range.end)
|
const start = Math.min(range.start, range.end)
|
||||||
@@ -2695,7 +2734,7 @@ export default function Page() {
|
|||||||
terminal={pty}
|
terminal={pty}
|
||||||
onClose={() => {
|
onClose={() => {
|
||||||
view().terminal.close()
|
view().terminal.close()
|
||||||
setAutoCreated(false)
|
setUi("autoCreated", false)
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { createSignal, onCleanup } from "solid-js"
|
import { onCleanup } from "solid-js"
|
||||||
|
import { createStore } from "solid-js/store"
|
||||||
|
|
||||||
// Minimal types to avoid relying on non-standard DOM typings
|
// Minimal types to avoid relying on non-standard DOM typings
|
||||||
type RecognitionResult = {
|
type RecognitionResult = {
|
||||||
@@ -59,9 +60,15 @@ export function createSpeechRecognition(opts?: {
|
|||||||
typeof window !== "undefined" &&
|
typeof window !== "undefined" &&
|
||||||
Boolean((window as any).webkitSpeechRecognition || (window as any).SpeechRecognition)
|
Boolean((window as any).webkitSpeechRecognition || (window as any).SpeechRecognition)
|
||||||
|
|
||||||
const [isRecording, setIsRecording] = createSignal(false)
|
const [store, setStore] = createStore({
|
||||||
const [committed, setCommitted] = createSignal("")
|
isRecording: false,
|
||||||
const [interim, setInterim] = createSignal("")
|
committed: "",
|
||||||
|
interim: "",
|
||||||
|
})
|
||||||
|
|
||||||
|
const isRecording = () => store.isRecording
|
||||||
|
const committed = () => store.committed
|
||||||
|
const interim = () => store.interim
|
||||||
|
|
||||||
let recognition: Recognition | undefined
|
let recognition: Recognition | undefined
|
||||||
let shouldContinue = false
|
let shouldContinue = false
|
||||||
@@ -82,7 +89,7 @@ export function createSpeechRecognition(opts?: {
|
|||||||
const nextCommitted = appendSegment(committedText, segment)
|
const nextCommitted = appendSegment(committedText, segment)
|
||||||
if (nextCommitted === committedText) return
|
if (nextCommitted === committedText) return
|
||||||
committedText = nextCommitted
|
committedText = nextCommitted
|
||||||
setCommitted(committedText)
|
setStore("committed", committedText)
|
||||||
if (opts?.onFinal) opts.onFinal(segment.trim())
|
if (opts?.onFinal) opts.onFinal(segment.trim())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -98,7 +105,7 @@ export function createSpeechRecognition(opts?: {
|
|||||||
pendingHypothesis = ""
|
pendingHypothesis = ""
|
||||||
lastInterimSuffix = ""
|
lastInterimSuffix = ""
|
||||||
shrinkCandidate = undefined
|
shrinkCandidate = undefined
|
||||||
setInterim("")
|
setStore("interim", "")
|
||||||
if (opts?.onInterim) opts.onInterim("")
|
if (opts?.onInterim) opts.onInterim("")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -107,7 +114,7 @@ export function createSpeechRecognition(opts?: {
|
|||||||
pendingHypothesis = hypothesis
|
pendingHypothesis = hypothesis
|
||||||
lastInterimSuffix = suffix
|
lastInterimSuffix = suffix
|
||||||
shrinkCandidate = undefined
|
shrinkCandidate = undefined
|
||||||
setInterim(suffix)
|
setStore("interim", suffix)
|
||||||
if (opts?.onInterim) {
|
if (opts?.onInterim) {
|
||||||
opts.onInterim(suffix ? appendSegment(committedText, suffix) : "")
|
opts.onInterim(suffix ? appendSegment(committedText, suffix) : "")
|
||||||
}
|
}
|
||||||
@@ -122,7 +129,7 @@ export function createSpeechRecognition(opts?: {
|
|||||||
pendingHypothesis = ""
|
pendingHypothesis = ""
|
||||||
lastInterimSuffix = ""
|
lastInterimSuffix = ""
|
||||||
shrinkCandidate = undefined
|
shrinkCandidate = undefined
|
||||||
setInterim("")
|
setStore("interim", "")
|
||||||
if (opts?.onInterim) opts.onInterim("")
|
if (opts?.onInterim) opts.onInterim("")
|
||||||
}, COMMIT_DELAY)
|
}, COMMIT_DELAY)
|
||||||
}
|
}
|
||||||
@@ -162,7 +169,7 @@ export function createSpeechRecognition(opts?: {
|
|||||||
pendingHypothesis = ""
|
pendingHypothesis = ""
|
||||||
lastInterimSuffix = ""
|
lastInterimSuffix = ""
|
||||||
shrinkCandidate = undefined
|
shrinkCandidate = undefined
|
||||||
setInterim("")
|
setStore("interim", "")
|
||||||
if (opts?.onInterim) opts.onInterim("")
|
if (opts?.onInterim) opts.onInterim("")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -211,7 +218,7 @@ export function createSpeechRecognition(opts?: {
|
|||||||
lastInterimSuffix = ""
|
lastInterimSuffix = ""
|
||||||
shrinkCandidate = undefined
|
shrinkCandidate = undefined
|
||||||
if (e.error === "no-speech" && shouldContinue) {
|
if (e.error === "no-speech" && shouldContinue) {
|
||||||
setInterim("")
|
setStore("interim", "")
|
||||||
if (opts?.onInterim) opts.onInterim("")
|
if (opts?.onInterim) opts.onInterim("")
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
try {
|
try {
|
||||||
@@ -221,7 +228,7 @@ export function createSpeechRecognition(opts?: {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
shouldContinue = false
|
shouldContinue = false
|
||||||
setIsRecording(false)
|
setStore("isRecording", false)
|
||||||
}
|
}
|
||||||
|
|
||||||
recognition.onstart = () => {
|
recognition.onstart = () => {
|
||||||
@@ -230,16 +237,16 @@ export function createSpeechRecognition(opts?: {
|
|||||||
cancelPendingCommit()
|
cancelPendingCommit()
|
||||||
lastInterimSuffix = ""
|
lastInterimSuffix = ""
|
||||||
shrinkCandidate = undefined
|
shrinkCandidate = undefined
|
||||||
setInterim("")
|
setStore("interim", "")
|
||||||
if (opts?.onInterim) opts.onInterim("")
|
if (opts?.onInterim) opts.onInterim("")
|
||||||
setIsRecording(true)
|
setStore("isRecording", true)
|
||||||
}
|
}
|
||||||
|
|
||||||
recognition.onend = () => {
|
recognition.onend = () => {
|
||||||
cancelPendingCommit()
|
cancelPendingCommit()
|
||||||
lastInterimSuffix = ""
|
lastInterimSuffix = ""
|
||||||
shrinkCandidate = undefined
|
shrinkCandidate = undefined
|
||||||
setIsRecording(false)
|
setStore("isRecording", false)
|
||||||
if (shouldContinue) {
|
if (shouldContinue) {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
try {
|
try {
|
||||||
@@ -258,7 +265,7 @@ export function createSpeechRecognition(opts?: {
|
|||||||
cancelPendingCommit()
|
cancelPendingCommit()
|
||||||
lastInterimSuffix = ""
|
lastInterimSuffix = ""
|
||||||
shrinkCandidate = undefined
|
shrinkCandidate = undefined
|
||||||
setInterim("")
|
setStore("interim", "")
|
||||||
try {
|
try {
|
||||||
recognition.start()
|
recognition.start()
|
||||||
} catch {}
|
} catch {}
|
||||||
@@ -271,7 +278,7 @@ export function createSpeechRecognition(opts?: {
|
|||||||
cancelPendingCommit()
|
cancelPendingCommit()
|
||||||
lastInterimSuffix = ""
|
lastInterimSuffix = ""
|
||||||
shrinkCandidate = undefined
|
shrinkCandidate = undefined
|
||||||
setInterim("")
|
setStore("interim", "")
|
||||||
if (opts?.onInterim) opts.onInterim("")
|
if (opts?.onInterim) opts.onInterim("")
|
||||||
try {
|
try {
|
||||||
recognition.stop()
|
recognition.stop()
|
||||||
@@ -284,7 +291,7 @@ export function createSpeechRecognition(opts?: {
|
|||||||
cancelPendingCommit()
|
cancelPendingCommit()
|
||||||
lastInterimSuffix = ""
|
lastInterimSuffix = ""
|
||||||
shrinkCandidate = undefined
|
shrinkCandidate = undefined
|
||||||
setInterim("")
|
setStore("interim", "")
|
||||||
if (opts?.onInterim) opts.onInterim("")
|
if (opts?.onInterim) opts.onInterim("")
|
||||||
try {
|
try {
|
||||||
recognition?.stop()
|
recognition?.stop()
|
||||||
|
|||||||
Reference in New Issue
Block a user