chore(app): refactor for better solidjs hygiene (#13344)
This commit is contained in:
@@ -345,6 +345,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|||||||
.filter((agent) => !agent.hidden && agent.mode !== "primary")
|
.filter((agent) => !agent.hidden && agent.mode !== "primary")
|
||||||
.map((agent): AtOption => ({ type: "agent", name: agent.name, display: agent.name })),
|
.map((agent): AtOption => ({ type: "agent", name: agent.name, display: agent.name })),
|
||||||
)
|
)
|
||||||
|
const agentNames = createMemo(() => local.agent.list().map((agent) => agent.name))
|
||||||
|
|
||||||
const handleAtSelect = (option: AtOption | undefined) => {
|
const handleAtSelect = (option: AtOption | undefined) => {
|
||||||
if (!option) return
|
if (!option) return
|
||||||
@@ -1038,7 +1039,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|||||||
keybind={command.keybind("agent.cycle")}
|
keybind={command.keybind("agent.cycle")}
|
||||||
>
|
>
|
||||||
<Select
|
<Select
|
||||||
options={local.agent.list().map((agent) => agent.name)}
|
options={agentNames()}
|
||||||
current={local.agent.current()?.name ?? ""}
|
current={local.agent.current()?.name ?? ""}
|
||||||
onSelect={local.agent.set}
|
onSelect={local.agent.set}
|
||||||
class={`capitalize ${local.model.variant.list().length > 0 ? "max-w-full" : "max-w-[120px]"}`}
|
class={`capitalize ${local.model.variant.list().length > 0 ? "max-w-full" : "max-w-[120px]"}`}
|
||||||
|
|||||||
@@ -7,32 +7,6 @@ import type { QuestionAnswer, QuestionRequest } from "@opencode-ai/sdk/v2"
|
|||||||
import { useLanguage } from "@/context/language"
|
import { useLanguage } from "@/context/language"
|
||||||
import { useSDK } from "@/context/sdk"
|
import { useSDK } from "@/context/sdk"
|
||||||
|
|
||||||
const writeAt = <T,>(list: T[], index: number, value: T) => {
|
|
||||||
const next = [...list]
|
|
||||||
next[index] = value
|
|
||||||
return next
|
|
||||||
}
|
|
||||||
|
|
||||||
const pickAnswer = (list: QuestionAnswer[], index: number, value: string) => {
|
|
||||||
return writeAt(list, index, [value])
|
|
||||||
}
|
|
||||||
|
|
||||||
const toggleAnswer = (list: QuestionAnswer[], index: number, value: string) => {
|
|
||||||
const current = list[index] ?? []
|
|
||||||
const next = current.includes(value) ? current.filter((item) => item !== value) : [...current, value]
|
|
||||||
return writeAt(list, index, next)
|
|
||||||
}
|
|
||||||
|
|
||||||
const appendAnswer = (list: QuestionAnswer[], index: number, value: string) => {
|
|
||||||
const current = list[index] ?? []
|
|
||||||
if (current.includes(value)) return list
|
|
||||||
return writeAt(list, index, [...current, value])
|
|
||||||
}
|
|
||||||
|
|
||||||
const writeCustom = (list: string[], index: number, value: string) => {
|
|
||||||
return writeAt(list, index, value)
|
|
||||||
}
|
|
||||||
|
|
||||||
export const QuestionDock: Component<{ request: QuestionRequest }> = (props) => {
|
export const QuestionDock: Component<{ request: QuestionRequest }> = (props) => {
|
||||||
const sdk = useSDK()
|
const sdk = useSDK()
|
||||||
const language = useLanguage()
|
const language = useLanguage()
|
||||||
@@ -95,10 +69,10 @@ export const QuestionDock: Component<{ request: QuestionRequest }> = (props) =>
|
|||||||
}
|
}
|
||||||
|
|
||||||
const pick = (answer: string, custom: boolean = false) => {
|
const pick = (answer: string, custom: boolean = false) => {
|
||||||
setStore("answers", pickAnswer(store.answers, store.tab, answer))
|
setStore("answers", store.tab, [answer])
|
||||||
|
|
||||||
if (custom) {
|
if (custom) {
|
||||||
setStore("custom", writeCustom(store.custom, store.tab, answer))
|
setStore("custom", store.tab, answer)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (single()) {
|
if (single()) {
|
||||||
@@ -110,7 +84,10 @@ export const QuestionDock: Component<{ request: QuestionRequest }> = (props) =>
|
|||||||
}
|
}
|
||||||
|
|
||||||
const toggle = (answer: string) => {
|
const toggle = (answer: string) => {
|
||||||
setStore("answers", toggleAnswer(store.answers, store.tab, answer))
|
setStore("answers", store.tab, (current = []) => {
|
||||||
|
if (current.includes(answer)) return current.filter((item) => item !== answer)
|
||||||
|
return [...current, answer]
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const selectTab = (index: number) => {
|
const selectTab = (index: number) => {
|
||||||
@@ -146,7 +123,10 @@ export const QuestionDock: Component<{ request: QuestionRequest }> = (props) =>
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (multi()) {
|
if (multi()) {
|
||||||
setStore("answers", appendAnswer(store.answers, store.tab, value))
|
setStore("answers", store.tab, (current = []) => {
|
||||||
|
if (current.includes(value)) return current
|
||||||
|
return [...current, value]
|
||||||
|
})
|
||||||
setStore("editing", false)
|
setStore("editing", false)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -239,7 +219,7 @@ export const QuestionDock: Component<{ request: QuestionRequest }> = (props) =>
|
|||||||
value={input()}
|
value={input()}
|
||||||
disabled={store.sending}
|
disabled={store.sending}
|
||||||
onInput={(e) => {
|
onInput={(e) => {
|
||||||
setStore("custom", writeCustom(store.custom, store.tab, e.currentTarget.value))
|
setStore("custom", store.tab, e.currentTarget.value)
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<Button type="submit" variant="primary" size="small" disabled={store.sending}>
|
<Button type="submit" variant="primary" size="small" disabled={store.sending}>
|
||||||
|
|||||||
@@ -168,34 +168,27 @@ export function SessionContextTab(props: SessionContextTabProps) {
|
|||||||
return language.t("context.breakdown.other")
|
return language.t("context.breakdown.other")
|
||||||
}
|
}
|
||||||
|
|
||||||
const stats = createMemo(() => {
|
const stats = [
|
||||||
const c = ctx()
|
{ label: "context.stats.session", value: () => props.info()?.title ?? params.id ?? "—" },
|
||||||
const count = counts()
|
{ label: "context.stats.messages", value: () => counts().all.toLocaleString(language.locale()) },
|
||||||
return [
|
{ label: "context.stats.provider", value: providerLabel },
|
||||||
{ label: language.t("context.stats.session"), value: props.info()?.title ?? params.id ?? "—" },
|
{ label: "context.stats.model", value: modelLabel },
|
||||||
{ label: language.t("context.stats.messages"), value: count.all.toLocaleString(language.locale()) },
|
{ label: "context.stats.limit", value: () => formatter().number(ctx()?.limit) },
|
||||||
{ label: language.t("context.stats.provider"), value: providerLabel() },
|
{ label: "context.stats.totalTokens", value: () => formatter().number(ctx()?.total) },
|
||||||
{ label: language.t("context.stats.model"), value: modelLabel() },
|
{ label: "context.stats.usage", value: () => formatter().percent(ctx()?.usage) },
|
||||||
{ label: language.t("context.stats.limit"), value: formatter().number(c?.limit) },
|
{ label: "context.stats.inputTokens", value: () => formatter().number(ctx()?.input) },
|
||||||
{ label: language.t("context.stats.totalTokens"), value: formatter().number(c?.total) },
|
{ label: "context.stats.outputTokens", value: () => formatter().number(ctx()?.output) },
|
||||||
{ label: language.t("context.stats.usage"), value: formatter().percent(c?.usage) },
|
{ label: "context.stats.reasoningTokens", value: () => formatter().number(ctx()?.reasoning) },
|
||||||
{ label: language.t("context.stats.inputTokens"), value: formatter().number(c?.input) },
|
{
|
||||||
{ label: language.t("context.stats.outputTokens"), value: formatter().number(c?.output) },
|
label: "context.stats.cacheTokens",
|
||||||
{ label: language.t("context.stats.reasoningTokens"), value: formatter().number(c?.reasoning) },
|
value: () => `${formatter().number(ctx()?.cacheRead)} / ${formatter().number(ctx()?.cacheWrite)}`,
|
||||||
{
|
},
|
||||||
label: language.t("context.stats.cacheTokens"),
|
{ label: "context.stats.userMessages", value: () => counts().user.toLocaleString(language.locale()) },
|
||||||
value: `${formatter().number(c?.cacheRead)} / ${formatter().number(c?.cacheWrite)}`,
|
{ label: "context.stats.assistantMessages", value: () => counts().assistant.toLocaleString(language.locale()) },
|
||||||
},
|
{ label: "context.stats.totalCost", value: cost },
|
||||||
{ label: language.t("context.stats.userMessages"), value: count.user.toLocaleString(language.locale()) },
|
{ label: "context.stats.sessionCreated", value: () => formatter().time(props.info()?.time.created) },
|
||||||
{
|
{ label: "context.stats.lastActivity", value: () => formatter().time(ctx()?.message.time.created) },
|
||||||
label: language.t("context.stats.assistantMessages"),
|
] satisfies { label: string; value: () => JSX.Element }[]
|
||||||
value: count.assistant.toLocaleString(language.locale()),
|
|
||||||
},
|
|
||||||
{ label: language.t("context.stats.totalCost"), value: cost() },
|
|
||||||
{ label: language.t("context.stats.sessionCreated"), value: formatter().time(props.info()?.time.created) },
|
|
||||||
{ label: language.t("context.stats.lastActivity"), value: formatter().time(c?.message.time.created) },
|
|
||||||
] satisfies { label: string; value: JSX.Element }[]
|
|
||||||
})
|
|
||||||
|
|
||||||
let scroll: HTMLDivElement | undefined
|
let scroll: HTMLDivElement | undefined
|
||||||
let frame: number | undefined
|
let frame: number | undefined
|
||||||
@@ -257,7 +250,9 @@ export function SessionContextTab(props: SessionContextTabProps) {
|
|||||||
>
|
>
|
||||||
<div class="px-6 pt-4 flex flex-col gap-10">
|
<div class="px-6 pt-4 flex flex-col gap-10">
|
||||||
<div class="grid grid-cols-1 @[32rem]:grid-cols-2 gap-4">
|
<div class="grid grid-cols-1 @[32rem]:grid-cols-2 gap-4">
|
||||||
<For each={stats()}>{(stat) => <Stat label={stat.label} value={stat.value} />}</For>
|
<For each={stats}>
|
||||||
|
{(stat) => <Stat label={language.t(stat.label as Parameters<typeof language.t>[0])} value={stat.value()} />}
|
||||||
|
</For>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Show when={breakdown().length > 0}>
|
<Show when={breakdown().length > 0}>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { createEffect, createMemo, onCleanup, Show } from "solid-js"
|
import { createEffect, createMemo, For, onCleanup, Show } from "solid-js"
|
||||||
import { createStore } from "solid-js/store"
|
import { createStore } from "solid-js/store"
|
||||||
import { Portal } from "solid-js/web"
|
import { Portal } from "solid-js/web"
|
||||||
import { useParams } from "@solidjs/router"
|
import { useParams } from "@solidjs/router"
|
||||||
@@ -404,23 +404,25 @@ export function SessionHeader() {
|
|||||||
setPrefs("app", value as OpenApp)
|
setPrefs("app", value as OpenApp)
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{options().map((o) => (
|
<For each={options()}>
|
||||||
<DropdownMenu.RadioItem
|
{(o) => (
|
||||||
value={o.id}
|
<DropdownMenu.RadioItem
|
||||||
onSelect={() => {
|
value={o.id}
|
||||||
setMenu("open", false)
|
onSelect={() => {
|
||||||
openDir(o.id)
|
setMenu("open", false)
|
||||||
}}
|
openDir(o.id)
|
||||||
>
|
}}
|
||||||
<div class="flex size-5 shrink-0 items-center justify-center">
|
>
|
||||||
<AppIcon id={o.icon} class={openIconSize(o.icon)} />
|
<div class="flex size-5 shrink-0 items-center justify-center">
|
||||||
</div>
|
<AppIcon id={o.icon} class={openIconSize(o.icon)} />
|
||||||
<DropdownMenu.ItemLabel>{o.label}</DropdownMenu.ItemLabel>
|
</div>
|
||||||
<DropdownMenu.ItemIndicator>
|
<DropdownMenu.ItemLabel>{o.label}</DropdownMenu.ItemLabel>
|
||||||
<Icon name="check-small" size="small" class="text-icon-weak" />
|
<DropdownMenu.ItemIndicator>
|
||||||
</DropdownMenu.ItemIndicator>
|
<Icon name="check-small" size="small" class="text-icon-weak" />
|
||||||
</DropdownMenu.RadioItem>
|
</DropdownMenu.ItemIndicator>
|
||||||
))}
|
</DropdownMenu.RadioItem>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
</DropdownMenu.RadioGroup>
|
</DropdownMenu.RadioGroup>
|
||||||
</DropdownMenu.Group>
|
</DropdownMenu.Group>
|
||||||
<DropdownMenu.Separator />
|
<DropdownMenu.Separator />
|
||||||
|
|||||||
@@ -173,12 +173,9 @@ export function StatusPopover() {
|
|||||||
const sortedServers = createMemo(() => listServersByHealth(servers(), server.url, health))
|
const sortedServers = createMemo(() => listServersByHealth(servers(), server.url, health))
|
||||||
const mcp = useMcpToggle({ sync, sdk, language })
|
const mcp = useMcpToggle({ sync, sdk, language })
|
||||||
const defaultServer = useDefaultServerUrl(platform.getDefaultServerUrl)
|
const defaultServer = useDefaultServerUrl(platform.getDefaultServerUrl)
|
||||||
const mcpItems = createMemo(() =>
|
const mcpNames = createMemo(() => Object.keys(sync.data.mcp ?? {}).sort((a, b) => a.localeCompare(b)))
|
||||||
Object.entries(sync.data.mcp ?? {})
|
const mcpStatus = (name: string) => sync.data.mcp?.[name]?.status
|
||||||
.map(([name, status]) => ({ name, status: status.status }))
|
const mcpConnected = createMemo(() => mcpNames().filter((name) => mcpStatus(name) === "connected").length)
|
||||||
.sort((a, b) => a.name.localeCompare(b.name)),
|
|
||||||
)
|
|
||||||
const mcpConnected = createMemo(() => mcpItems().filter((item) => item.status === "connected").length)
|
|
||||||
const lspItems = createMemo(() => sync.data.lsp ?? [])
|
const lspItems = createMemo(() => sync.data.lsp ?? [])
|
||||||
const lspCount = createMemo(() => lspItems().length)
|
const lspCount = createMemo(() => lspItems().length)
|
||||||
const plugins = createMemo(() => sync.data.config.plugin ?? [])
|
const plugins = createMemo(() => sync.data.config.plugin ?? [])
|
||||||
@@ -186,7 +183,10 @@ export function StatusPopover() {
|
|||||||
const pluginEmpty = createMemo(() => pluginEmptyMessage(language.t("dialog.plugins.empty"), "opencode.json"))
|
const pluginEmpty = createMemo(() => pluginEmptyMessage(language.t("dialog.plugins.empty"), "opencode.json"))
|
||||||
const overallHealthy = createMemo(() => {
|
const overallHealthy = createMemo(() => {
|
||||||
const serverHealthy = server.healthy() === true
|
const serverHealthy = server.healthy() === true
|
||||||
const anyMcpIssue = mcpItems().some((item) => item.status !== "connected" && item.status !== "disabled")
|
const anyMcpIssue = mcpNames().some((name) => {
|
||||||
|
const status = mcpStatus(name)
|
||||||
|
return status !== "connected" && status !== "disabled"
|
||||||
|
})
|
||||||
return serverHealthy && !anyMcpIssue
|
return serverHealthy && !anyMcpIssue
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -306,39 +306,40 @@ export function StatusPopover() {
|
|||||||
<div class="flex flex-col px-2 pb-2">
|
<div class="flex flex-col px-2 pb-2">
|
||||||
<div class="flex flex-col p-3 bg-background-base rounded-sm min-h-14">
|
<div class="flex flex-col p-3 bg-background-base rounded-sm min-h-14">
|
||||||
<Show
|
<Show
|
||||||
when={mcpItems().length > 0}
|
when={mcpNames().length > 0}
|
||||||
fallback={
|
fallback={
|
||||||
<div class="text-14-regular text-text-base text-center my-auto">
|
<div class="text-14-regular text-text-base text-center my-auto">
|
||||||
{language.t("dialog.mcp.empty")}
|
{language.t("dialog.mcp.empty")}
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<For each={mcpItems()}>
|
<For each={mcpNames()}>
|
||||||
{(item) => {
|
{(name) => {
|
||||||
const enabled = () => item.status === "connected"
|
const status = () => mcpStatus(name)
|
||||||
|
const enabled = () => status() === "connected"
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
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={() => mcp.toggle(item.name)}
|
onClick={() => mcp.toggle(name)}
|
||||||
disabled={mcp.loading() === item.name}
|
disabled={mcp.loading() === name}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
classList={{
|
classList={{
|
||||||
"size-1.5 rounded-full shrink-0": true,
|
"size-1.5 rounded-full shrink-0": true,
|
||||||
"bg-icon-success-base": item.status === "connected",
|
"bg-icon-success-base": status() === "connected",
|
||||||
"bg-icon-critical-base": item.status === "failed",
|
"bg-icon-critical-base": status() === "failed",
|
||||||
"bg-border-weak-base": item.status === "disabled",
|
"bg-border-weak-base": status() === "disabled",
|
||||||
"bg-icon-warning-base":
|
"bg-icon-warning-base":
|
||||||
item.status === "needs_auth" || item.status === "needs_client_registration",
|
status() === "needs_auth" || status() === "needs_client_registration",
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<span class="text-14-regular text-text-base truncate flex-1">{item.name}</span>
|
<span class="text-14-regular text-text-base truncate flex-1">{name}</span>
|
||||||
<div onClick={(event) => event.stopPropagation()}>
|
<div onClick={(event) => event.stopPropagation()}>
|
||||||
<Switch
|
<Switch
|
||||||
checked={enabled()}
|
checked={enabled()}
|
||||||
disabled={mcp.loading() === item.name}
|
disabled={mcp.loading() === name}
|
||||||
onChange={() => mcp.toggle(item.name)}
|
onChange={() => mcp.toggle(name)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -23,6 +23,16 @@ function normalizeSelectedLines(range: SelectedLineRange): SelectedLineRange {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function equalSelectedLines(a: SelectedLineRange | null | undefined, b: SelectedLineRange | null | undefined) {
|
||||||
|
if (!a && !b) return true
|
||||||
|
if (!a || !b) return false
|
||||||
|
const left = normalizeSelectedLines(a)
|
||||||
|
const right = normalizeSelectedLines(b)
|
||||||
|
return (
|
||||||
|
left.start === right.start && left.end === right.end && left.side === right.side && left.endSide === right.endSide
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
function createViewSession(dir: string, id: string | undefined) {
|
function createViewSession(dir: string, id: string | undefined) {
|
||||||
const legacyViewKey = `${dir}/file${id ? "/" + id : ""}.v1`
|
const legacyViewKey = `${dir}/file${id ? "/" + id : ""}.v1`
|
||||||
|
|
||||||
@@ -65,36 +75,36 @@ function createViewSession(dir: string, id: string | undefined) {
|
|||||||
const selectedLines = (path: string) => view.file[path]?.selectedLines
|
const selectedLines = (path: string) => view.file[path]?.selectedLines
|
||||||
|
|
||||||
const setScrollTop = (path: string, top: number) => {
|
const setScrollTop = (path: string, top: number) => {
|
||||||
setView("file", path, (current) => {
|
setView(
|
||||||
if (current?.scrollTop === top) return current
|
produce((draft) => {
|
||||||
return {
|
const file = draft.file[path] ?? (draft.file[path] = {})
|
||||||
...(current ?? {}),
|
if (file.scrollTop === top) return
|
||||||
scrollTop: top,
|
file.scrollTop = top
|
||||||
}
|
}),
|
||||||
})
|
)
|
||||||
pruneView(path)
|
pruneView(path)
|
||||||
}
|
}
|
||||||
|
|
||||||
const setScrollLeft = (path: string, left: number) => {
|
const setScrollLeft = (path: string, left: number) => {
|
||||||
setView("file", path, (current) => {
|
setView(
|
||||||
if (current?.scrollLeft === left) return current
|
produce((draft) => {
|
||||||
return {
|
const file = draft.file[path] ?? (draft.file[path] = {})
|
||||||
...(current ?? {}),
|
if (file.scrollLeft === left) return
|
||||||
scrollLeft: left,
|
file.scrollLeft = left
|
||||||
}
|
}),
|
||||||
})
|
)
|
||||||
pruneView(path)
|
pruneView(path)
|
||||||
}
|
}
|
||||||
|
|
||||||
const setSelectedLines = (path: string, range: SelectedLineRange | null) => {
|
const setSelectedLines = (path: string, range: SelectedLineRange | null) => {
|
||||||
const next = range ? normalizeSelectedLines(range) : null
|
const next = range ? normalizeSelectedLines(range) : null
|
||||||
setView("file", path, (current) => {
|
setView(
|
||||||
if (current?.selectedLines === next) return current
|
produce((draft) => {
|
||||||
return {
|
const file = draft.file[path] ?? (draft.file[path] = {})
|
||||||
...(current ?? {}),
|
if (equalSelectedLines(file.selectedLines, next)) return
|
||||||
selectedLines: next,
|
file.selectedLines = next
|
||||||
}
|
}),
|
||||||
})
|
)
|
||||||
pruneView(path)
|
pruneView(path)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -233,6 +233,7 @@ export function applyDirectoryEvent(input: {
|
|||||||
}
|
}
|
||||||
case "vcs.branch.updated": {
|
case "vcs.branch.updated": {
|
||||||
const props = event.properties as { branch: string }
|
const props = event.properties as { branch: string }
|
||||||
|
if (input.store.vcs?.branch === props.branch) break
|
||||||
const next = { branch: props.branch }
|
const next = { branch: props.branch }
|
||||||
input.setStore("vcs", next)
|
input.setStore("vcs", next)
|
||||||
if (input.vcsCache) input.vcsCache.setStore("value", next)
|
if (input.vcsCache) input.vcsCache.setStore("value", next)
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { createStore } from "solid-js/store"
|
import { createStore, reconcile } from "solid-js/store"
|
||||||
import { createEffect, createMemo, onCleanup } from "solid-js"
|
import { batch, createEffect, createMemo, onCleanup } from "solid-js"
|
||||||
import { useParams } from "@solidjs/router"
|
import { useParams } from "@solidjs/router"
|
||||||
import { createSimpleContext } from "@opencode-ai/ui/context"
|
import { createSimpleContext } from "@opencode-ai/ui/context"
|
||||||
import { useGlobalSDK } from "./global-sdk"
|
import { useGlobalSDK } from "./global-sdk"
|
||||||
@@ -13,7 +13,6 @@ import { decode64 } from "@/utils/base64"
|
|||||||
import { EventSessionError } from "@opencode-ai/sdk/v2"
|
import { EventSessionError } from "@opencode-ai/sdk/v2"
|
||||||
import { Persist, persisted } from "@/utils/persist"
|
import { Persist, persisted } from "@/utils/persist"
|
||||||
import { playSound, soundSrc } from "@/utils/sound"
|
import { playSound, soundSrc } from "@/utils/sound"
|
||||||
import { buildNotificationIndex } from "./notification-index"
|
|
||||||
|
|
||||||
type NotificationBase = {
|
type NotificationBase = {
|
||||||
directory?: string
|
directory?: string
|
||||||
@@ -34,6 +33,21 @@ type ErrorNotification = NotificationBase & {
|
|||||||
|
|
||||||
export type Notification = TurnCompleteNotification | ErrorNotification
|
export type Notification = TurnCompleteNotification | ErrorNotification
|
||||||
|
|
||||||
|
type NotificationIndex = {
|
||||||
|
session: {
|
||||||
|
all: Record<string, Notification[]>
|
||||||
|
unseen: Record<string, Notification[]>
|
||||||
|
unseenCount: Record<string, number>
|
||||||
|
unseenHasError: Record<string, boolean>
|
||||||
|
}
|
||||||
|
project: {
|
||||||
|
all: Record<string, Notification[]>
|
||||||
|
unseen: Record<string, Notification[]>
|
||||||
|
unseenCount: Record<string, number>
|
||||||
|
unseenHasError: Record<string, boolean>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const MAX_NOTIFICATIONS = 500
|
const MAX_NOTIFICATIONS = 500
|
||||||
const NOTIFICATION_TTL_MS = 1000 * 60 * 60 * 24 * 30
|
const NOTIFICATION_TTL_MS = 1000 * 60 * 60 * 24 * 30
|
||||||
|
|
||||||
@@ -44,6 +58,53 @@ function pruneNotifications(list: Notification[]) {
|
|||||||
return pruned.slice(pruned.length - MAX_NOTIFICATIONS)
|
return pruned.slice(pruned.length - MAX_NOTIFICATIONS)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function createNotificationIndex(): NotificationIndex {
|
||||||
|
return {
|
||||||
|
session: {
|
||||||
|
all: {},
|
||||||
|
unseen: {},
|
||||||
|
unseenCount: {},
|
||||||
|
unseenHasError: {},
|
||||||
|
},
|
||||||
|
project: {
|
||||||
|
all: {},
|
||||||
|
unseen: {},
|
||||||
|
unseenCount: {},
|
||||||
|
unseenHasError: {},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildNotificationIndex(list: Notification[]) {
|
||||||
|
const index = createNotificationIndex()
|
||||||
|
|
||||||
|
list.forEach((notification) => {
|
||||||
|
if (notification.session) {
|
||||||
|
const all = index.session.all[notification.session] ?? []
|
||||||
|
index.session.all[notification.session] = [...all, notification]
|
||||||
|
if (!notification.viewed) {
|
||||||
|
const unseen = index.session.unseen[notification.session] ?? []
|
||||||
|
index.session.unseen[notification.session] = [...unseen, notification]
|
||||||
|
index.session.unseenCount[notification.session] = unseen.length + 1
|
||||||
|
if (notification.type === "error") index.session.unseenHasError[notification.session] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (notification.directory) {
|
||||||
|
const all = index.project.all[notification.directory] ?? []
|
||||||
|
index.project.all[notification.directory] = [...all, notification]
|
||||||
|
if (!notification.viewed) {
|
||||||
|
const unseen = index.project.unseen[notification.directory] ?? []
|
||||||
|
index.project.unseen[notification.directory] = [...unseen, notification]
|
||||||
|
index.project.unseenCount[notification.directory] = unseen.length + 1
|
||||||
|
if (notification.type === "error") index.project.unseenHasError[notification.directory] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return index
|
||||||
|
}
|
||||||
|
|
||||||
export const { use: useNotification, provider: NotificationProvider } = createSimpleContext({
|
export const { use: useNotification, provider: NotificationProvider } = createSimpleContext({
|
||||||
name: "Notification",
|
name: "Notification",
|
||||||
init: () => {
|
init: () => {
|
||||||
@@ -68,21 +129,81 @@ export const { use: useNotification, provider: NotificationProvider } = createSi
|
|||||||
list: [] as Notification[],
|
list: [] as Notification[],
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
const [index, setIndex] = createStore<NotificationIndex>(buildNotificationIndex(store.list))
|
||||||
|
|
||||||
const meta = { pruned: false, disposed: false }
|
const meta = { pruned: false, disposed: false }
|
||||||
|
|
||||||
|
const updateUnseen = (scope: "session" | "project", key: string, unseen: Notification[]) => {
|
||||||
|
setIndex(scope, "unseen", key, unseen)
|
||||||
|
setIndex(scope, "unseenCount", key, unseen.length)
|
||||||
|
setIndex(
|
||||||
|
scope,
|
||||||
|
"unseenHasError",
|
||||||
|
key,
|
||||||
|
unseen.some((notification) => notification.type === "error"),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const appendToIndex = (notification: Notification) => {
|
||||||
|
if (notification.session) {
|
||||||
|
setIndex("session", "all", notification.session, (all = []) => [...all, notification])
|
||||||
|
if (!notification.viewed) {
|
||||||
|
setIndex("session", "unseen", notification.session, (unseen = []) => [...unseen, notification])
|
||||||
|
setIndex("session", "unseenCount", notification.session, (count = 0) => count + 1)
|
||||||
|
if (notification.type === "error") setIndex("session", "unseenHasError", notification.session, true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (notification.directory) {
|
||||||
|
setIndex("project", "all", notification.directory, (all = []) => [...all, notification])
|
||||||
|
if (!notification.viewed) {
|
||||||
|
setIndex("project", "unseen", notification.directory, (unseen = []) => [...unseen, notification])
|
||||||
|
setIndex("project", "unseenCount", notification.directory, (count = 0) => count + 1)
|
||||||
|
if (notification.type === "error") setIndex("project", "unseenHasError", notification.directory, true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const removeFromIndex = (notification: Notification) => {
|
||||||
|
if (notification.session) {
|
||||||
|
setIndex("session", "all", notification.session, (all = []) => all.filter((n) => n !== notification))
|
||||||
|
if (!notification.viewed) {
|
||||||
|
const unseen = (index.session.unseen[notification.session] ?? empty).filter((n) => n !== notification)
|
||||||
|
updateUnseen("session", notification.session, unseen)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (notification.directory) {
|
||||||
|
setIndex("project", "all", notification.directory, (all = []) => all.filter((n) => n !== notification))
|
||||||
|
if (!notification.viewed) {
|
||||||
|
const unseen = (index.project.unseen[notification.directory] ?? empty).filter((n) => n !== notification)
|
||||||
|
updateUnseen("project", notification.directory, unseen)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
if (!ready()) return
|
if (!ready()) return
|
||||||
if (meta.pruned) return
|
if (meta.pruned) return
|
||||||
meta.pruned = true
|
meta.pruned = true
|
||||||
setStore("list", pruneNotifications(store.list))
|
const list = pruneNotifications(store.list)
|
||||||
|
batch(() => {
|
||||||
|
setStore("list", list)
|
||||||
|
setIndex(reconcile(buildNotificationIndex(list), { merge: false }))
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
const append = (notification: Notification) => {
|
const append = (notification: Notification) => {
|
||||||
setStore("list", (list) => pruneNotifications([...list, notification]))
|
const list = pruneNotifications([...store.list, notification])
|
||||||
}
|
const keep = new Set(list)
|
||||||
|
const removed = store.list.filter((n) => !keep.has(n))
|
||||||
|
|
||||||
const index = createMemo(() => buildNotificationIndex(store.list))
|
batch(() => {
|
||||||
|
if (keep.has(notification)) appendToIndex(notification)
|
||||||
|
removed.forEach((n) => removeFromIndex(n))
|
||||||
|
setStore("list", list)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
const lookup = async (directory: string, sessionID?: string) => {
|
const lookup = async (directory: string, sessionID?: string) => {
|
||||||
if (!sessionID) return undefined
|
if (!sessionID) return undefined
|
||||||
@@ -181,36 +302,66 @@ export const { use: useNotification, provider: NotificationProvider } = createSi
|
|||||||
ready,
|
ready,
|
||||||
session: {
|
session: {
|
||||||
all(session: string) {
|
all(session: string) {
|
||||||
return index().session.all.get(session) ?? empty
|
return index.session.all[session] ?? empty
|
||||||
},
|
},
|
||||||
unseen(session: string) {
|
unseen(session: string) {
|
||||||
return index().session.unseen.get(session) ?? empty
|
return index.session.unseen[session] ?? empty
|
||||||
},
|
},
|
||||||
unseenCount(session: string) {
|
unseenCount(session: string) {
|
||||||
return index().session.unseenCount.get(session) ?? 0
|
return index.session.unseenCount[session] ?? 0
|
||||||
},
|
},
|
||||||
unseenHasError(session: string) {
|
unseenHasError(session: string) {
|
||||||
return index().session.unseenHasError.get(session) ?? false
|
return index.session.unseenHasError[session] ?? false
|
||||||
},
|
},
|
||||||
markViewed(session: string) {
|
markViewed(session: string) {
|
||||||
setStore("list", (n) => n.session === session, "viewed", true)
|
const unseen = index.session.unseen[session] ?? empty
|
||||||
|
if (!unseen.length) return
|
||||||
|
|
||||||
|
const projects = [
|
||||||
|
...new Set(unseen.flatMap((notification) => (notification.directory ? [notification.directory] : []))),
|
||||||
|
]
|
||||||
|
batch(() => {
|
||||||
|
setStore("list", (n) => n.session === session && !n.viewed, "viewed", true)
|
||||||
|
updateUnseen("session", session, [])
|
||||||
|
projects.forEach((directory) => {
|
||||||
|
const next = (index.project.unseen[directory] ?? empty).filter(
|
||||||
|
(notification) => notification.session !== session,
|
||||||
|
)
|
||||||
|
updateUnseen("project", directory, next)
|
||||||
|
})
|
||||||
|
})
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
project: {
|
project: {
|
||||||
all(directory: string) {
|
all(directory: string) {
|
||||||
return index().project.all.get(directory) ?? empty
|
return index.project.all[directory] ?? empty
|
||||||
},
|
},
|
||||||
unseen(directory: string) {
|
unseen(directory: string) {
|
||||||
return index().project.unseen.get(directory) ?? empty
|
return index.project.unseen[directory] ?? empty
|
||||||
},
|
},
|
||||||
unseenCount(directory: string) {
|
unseenCount(directory: string) {
|
||||||
return index().project.unseenCount.get(directory) ?? 0
|
return index.project.unseenCount[directory] ?? 0
|
||||||
},
|
},
|
||||||
unseenHasError(directory: string) {
|
unseenHasError(directory: string) {
|
||||||
return index().project.unseenHasError.get(directory) ?? false
|
return index.project.unseenHasError[directory] ?? false
|
||||||
},
|
},
|
||||||
markViewed(directory: string) {
|
markViewed(directory: string) {
|
||||||
setStore("list", (n) => n.directory === directory, "viewed", true)
|
const unseen = index.project.unseen[directory] ?? empty
|
||||||
|
if (!unseen.length) return
|
||||||
|
|
||||||
|
const sessions = [
|
||||||
|
...new Set(unseen.flatMap((notification) => (notification.session ? [notification.session] : []))),
|
||||||
|
]
|
||||||
|
batch(() => {
|
||||||
|
setStore("list", (n) => n.directory === directory && !n.viewed, "viewed", true)
|
||||||
|
updateUnseen("project", directory, [])
|
||||||
|
sessions.forEach((session) => {
|
||||||
|
const next = (index.session.unseen[session] ?? empty).filter(
|
||||||
|
(notification) => notification.directory !== directory,
|
||||||
|
)
|
||||||
|
updateUnseen("session", session, next)
|
||||||
|
})
|
||||||
|
})
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -101,11 +101,15 @@ function createWorkspaceTerminalSession(sdk: ReturnType<typeof useSDK>, dir: str
|
|||||||
const all = store.all
|
const all = store.all
|
||||||
const index = all.findIndex((x) => x.id === id)
|
const index = all.findIndex((x) => x.id === id)
|
||||||
if (index === -1) return
|
if (index === -1) return
|
||||||
const filtered = all.filter((x) => x.id !== id)
|
const active = store.active === id ? (index === 0 ? all[1]?.id : all[0]?.id) : store.active
|
||||||
const active = store.active === id ? filtered[0]?.id : store.active
|
|
||||||
batch(() => {
|
batch(() => {
|
||||||
setStore("all", filtered)
|
|
||||||
setStore("active", active)
|
setStore("active", active)
|
||||||
|
setStore(
|
||||||
|
"all",
|
||||||
|
produce((draft) => {
|
||||||
|
draft.splice(index, 1)
|
||||||
|
}),
|
||||||
|
)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -157,10 +161,7 @@ function createWorkspaceTerminalSession(sdk: ReturnType<typeof useSDK>, dir: str
|
|||||||
title: pty.data?.title ?? "Terminal",
|
title: pty.data?.title ?? "Terminal",
|
||||||
titleNumber: nextNumber,
|
titleNumber: nextNumber,
|
||||||
}
|
}
|
||||||
setStore("all", (all) => {
|
setStore("all", store.all.length, newTerminal)
|
||||||
const newAll = [...all, newTerminal]
|
|
||||||
return newAll
|
|
||||||
})
|
|
||||||
setStore("active", id)
|
setStore("active", id)
|
||||||
})
|
})
|
||||||
.catch((error: unknown) => {
|
.catch((error: unknown) => {
|
||||||
@@ -168,8 +169,11 @@ function createWorkspaceTerminalSession(sdk: ReturnType<typeof useSDK>, dir: str
|
|||||||
})
|
})
|
||||||
},
|
},
|
||||||
update(pty: Partial<LocalPTY> & { id: string }) {
|
update(pty: Partial<LocalPTY> & { id: string }) {
|
||||||
const previous = store.all.find((x) => x.id === pty.id)
|
const index = store.all.findIndex((x) => x.id === pty.id)
|
||||||
if (previous) setStore("all", (all) => all.map((item) => (item.id === pty.id ? { ...item, ...pty } : item)))
|
const previous = index >= 0 ? store.all[index] : undefined
|
||||||
|
if (index >= 0) {
|
||||||
|
setStore("all", index, (item) => ({ ...item, ...pty }))
|
||||||
|
}
|
||||||
sdk.client.pty
|
sdk.client.pty
|
||||||
.update({
|
.update({
|
||||||
ptyID: pty.id,
|
ptyID: pty.id,
|
||||||
@@ -178,7 +182,8 @@ function createWorkspaceTerminalSession(sdk: ReturnType<typeof useSDK>, dir: str
|
|||||||
})
|
})
|
||||||
.catch((error: unknown) => {
|
.catch((error: unknown) => {
|
||||||
if (previous) {
|
if (previous) {
|
||||||
setStore("all", (all) => all.map((item) => (item.id === pty.id ? previous : item)))
|
const currentIndex = store.all.findIndex((item) => item.id === pty.id)
|
||||||
|
if (currentIndex >= 0) setStore("all", currentIndex, previous)
|
||||||
}
|
}
|
||||||
console.error("Failed to update terminal", error)
|
console.error("Failed to update terminal", error)
|
||||||
})
|
})
|
||||||
@@ -232,15 +237,21 @@ function createWorkspaceTerminalSession(sdk: ReturnType<typeof useSDK>, dir: str
|
|||||||
setStore("active", store.all[prevIndex]?.id)
|
setStore("active", store.all[prevIndex]?.id)
|
||||||
},
|
},
|
||||||
async close(id: string) {
|
async close(id: string) {
|
||||||
batch(() => {
|
const index = store.all.findIndex((f) => f.id === id)
|
||||||
const filtered = store.all.filter((x) => x.id !== id)
|
if (index !== -1) {
|
||||||
if (store.active === id) {
|
batch(() => {
|
||||||
const index = store.all.findIndex((f) => f.id === id)
|
if (store.active === id) {
|
||||||
const next = index > 0 ? index - 1 : 0
|
const next = index > 0 ? store.all[index - 1]?.id : store.all[1]?.id
|
||||||
setStore("active", filtered[next]?.id)
|
setStore("active", next)
|
||||||
}
|
}
|
||||||
setStore("all", filtered)
|
setStore(
|
||||||
})
|
"all",
|
||||||
|
produce((all) => {
|
||||||
|
all.splice(index, 1)
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
await sdk.client.pty.remove({ ptyID: id }).catch((error: unknown) => {
|
await sdk.client.pty.remove({ ptyID: id }).catch((error: unknown) => {
|
||||||
console.error("Failed to close terminal", error)
|
console.error("Failed to close terminal", error)
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { useParams } from "@solidjs/router"
|
|||||||
import { createMemo } from "solid-js"
|
import { createMemo } from "solid-js"
|
||||||
|
|
||||||
export const popularProviders = ["opencode", "anthropic", "github-copilot", "openai", "google", "openrouter", "vercel"]
|
export const popularProviders = ["opencode", "anthropic", "github-copilot", "openai", "google", "openrouter", "vercel"]
|
||||||
|
const popularProviderSet = new Set(popularProviders)
|
||||||
|
|
||||||
export function useProviders() {
|
export function useProviders() {
|
||||||
const globalSync = useGlobalSync()
|
const globalSync = useGlobalSync()
|
||||||
@@ -16,11 +17,12 @@ export function useProviders() {
|
|||||||
}
|
}
|
||||||
return globalSync.data.provider
|
return globalSync.data.provider
|
||||||
})
|
})
|
||||||
const connected = createMemo(() => providers().all.filter((p) => providers().connected.includes(p.id)))
|
const connectedIDs = createMemo(() => new Set(providers().connected))
|
||||||
|
const connected = createMemo(() => providers().all.filter((p) => connectedIDs().has(p.id)))
|
||||||
const paid = createMemo(() =>
|
const paid = createMemo(() =>
|
||||||
connected().filter((p) => p.id !== "opencode" || Object.values(p.models).find((m) => m.cost?.input)),
|
connected().filter((p) => p.id !== "opencode" || Object.values(p.models).find((m) => m.cost?.input)),
|
||||||
)
|
)
|
||||||
const popular = createMemo(() => providers().all.filter((p) => popularProviders.includes(p.id)))
|
const popular = createMemo(() => providers().all.filter((p) => popularProviderSet.has(p.id)))
|
||||||
return {
|
return {
|
||||||
all: createMemo(() => providers().all),
|
all: createMemo(() => providers().all),
|
||||||
default: createMemo(() => providers().default),
|
default: createMemo(() => providers().default),
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import {
|
|||||||
batch,
|
batch,
|
||||||
createEffect,
|
createEffect,
|
||||||
createMemo,
|
createMemo,
|
||||||
|
createSignal,
|
||||||
For,
|
For,
|
||||||
on,
|
on,
|
||||||
onCleanup,
|
onCleanup,
|
||||||
@@ -124,7 +125,7 @@ export default function Layout(props: ParentProps) {
|
|||||||
|
|
||||||
const [state, setState] = createStore({
|
const [state, setState] = createStore({
|
||||||
autoselect: !initialDirectory,
|
autoselect: !initialDirectory,
|
||||||
busyWorkspaces: new Set<string>(),
|
busyWorkspaces: {} as Record<string, boolean>,
|
||||||
hoverSession: undefined as string | undefined,
|
hoverSession: undefined as string | undefined,
|
||||||
hoverProject: undefined as string | undefined,
|
hoverProject: undefined as string | undefined,
|
||||||
scrollSessionKey: undefined as string | undefined,
|
scrollSessionKey: undefined as string | undefined,
|
||||||
@@ -134,15 +135,28 @@ export default function Layout(props: ParentProps) {
|
|||||||
const editor = createInlineEditorController()
|
const editor = createInlineEditorController()
|
||||||
const setBusy = (directory: string, value: boolean) => {
|
const setBusy = (directory: string, value: boolean) => {
|
||||||
const key = workspaceKey(directory)
|
const key = workspaceKey(directory)
|
||||||
setState("busyWorkspaces", (prev) => {
|
if (value) {
|
||||||
const next = new Set(prev)
|
setState("busyWorkspaces", key, true)
|
||||||
if (value) next.add(key)
|
return
|
||||||
else next.delete(key)
|
}
|
||||||
return next
|
setState(
|
||||||
})
|
"busyWorkspaces",
|
||||||
|
produce((draft) => {
|
||||||
|
delete draft[key]
|
||||||
|
}),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
const isBusy = (directory: string) => state.busyWorkspaces.has(workspaceKey(directory))
|
const isBusy = (directory: string) => !!state.busyWorkspaces[workspaceKey(directory)]
|
||||||
const navLeave = { current: undefined as number | undefined }
|
const navLeave = { current: undefined as number | undefined }
|
||||||
|
const [sortNow, setSortNow] = createSignal(Date.now())
|
||||||
|
let sortNowInterval: ReturnType<typeof setInterval> | undefined
|
||||||
|
const sortNowTimeout = setTimeout(
|
||||||
|
() => {
|
||||||
|
setSortNow(Date.now())
|
||||||
|
sortNowInterval = setInterval(() => setSortNow(Date.now()), 60_000)
|
||||||
|
},
|
||||||
|
60_000 - (Date.now() % 60_000),
|
||||||
|
)
|
||||||
|
|
||||||
const aim = createAim({
|
const aim = createAim({
|
||||||
enabled: () => !layout.sidebar.opened(),
|
enabled: () => !layout.sidebar.opened(),
|
||||||
@@ -157,6 +171,8 @@ export default function Layout(props: ParentProps) {
|
|||||||
|
|
||||||
onCleanup(() => {
|
onCleanup(() => {
|
||||||
if (navLeave.current !== undefined) clearTimeout(navLeave.current)
|
if (navLeave.current !== undefined) clearTimeout(navLeave.current)
|
||||||
|
clearTimeout(sortNowTimeout)
|
||||||
|
if (sortNowInterval) clearInterval(sortNowInterval)
|
||||||
aim.reset()
|
aim.reset()
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -518,10 +534,13 @@ export default function Layout(props: ParentProps) {
|
|||||||
|
|
||||||
const setWorkspaceName = (directory: string, next: string, projectId?: string, branch?: string) => {
|
const setWorkspaceName = (directory: string, next: string, projectId?: string, branch?: string) => {
|
||||||
const key = workspaceKey(directory)
|
const key = workspaceKey(directory)
|
||||||
setStore("workspaceName", (prev) => ({ ...(prev ?? {}), [key]: next }))
|
setStore("workspaceName", key, next)
|
||||||
if (!projectId) return
|
if (!projectId) return
|
||||||
if (!branch) return
|
if (!branch) return
|
||||||
setStore("workspaceBranchName", projectId, (prev) => ({ ...(prev ?? {}), [branch]: next }))
|
if (!store.workspaceBranchName[projectId]) {
|
||||||
|
setStore("workspaceBranchName", projectId, {})
|
||||||
|
}
|
||||||
|
setStore("workspaceBranchName", projectId, branch, next)
|
||||||
}
|
}
|
||||||
|
|
||||||
const workspaceLabel = (directory: string, branch?: string, projectId?: string) =>
|
const workspaceLabel = (directory: string, branch?: string, projectId?: string) =>
|
||||||
@@ -1447,23 +1466,41 @@ export default function Layout(props: ParentProps) {
|
|||||||
document.documentElement.style.setProperty("--dialog-left-margin", `${sidebarWidth}px`)
|
document.documentElement.style.setProperty("--dialog-left-margin", `${sidebarWidth}px`)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const loadedSessionDirs = new Set<string>()
|
||||||
|
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
const project = currentProject()
|
const project = currentProject()
|
||||||
if (!project) return
|
const workspaces = workspaceSetting()
|
||||||
|
const next = new Set<string>()
|
||||||
|
if (!project) {
|
||||||
|
loadedSessionDirs.clear()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if (workspaceSetting()) {
|
if (workspaces) {
|
||||||
const activeDir = currentDir()
|
const activeDir = currentDir()
|
||||||
const dirs = [project.worktree, ...(project.sandboxes ?? [])]
|
const dirs = [project.worktree, ...(project.sandboxes ?? [])]
|
||||||
for (const directory of dirs) {
|
for (const directory of dirs) {
|
||||||
const expanded = store.workspaceExpanded[directory] ?? directory === project.worktree
|
const expanded = store.workspaceExpanded[directory] ?? directory === project.worktree
|
||||||
const active = directory === activeDir
|
const active = directory === activeDir
|
||||||
if (!expanded && !active) continue
|
if (!expanded && !active) continue
|
||||||
globalSync.project.loadSessions(directory)
|
next.add(directory)
|
||||||
}
|
}
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
globalSync.project.loadSessions(project.worktree)
|
if (!workspaces) {
|
||||||
|
next.add(project.worktree)
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const directory of next) {
|
||||||
|
if (loadedSessionDirs.has(directory)) continue
|
||||||
|
globalSync.project.loadSessions(directory)
|
||||||
|
}
|
||||||
|
|
||||||
|
loadedSessionDirs.clear()
|
||||||
|
for (const directory of next) {
|
||||||
|
loadedSessionDirs.add(directory)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
function handleDragStart(event: unknown) {
|
function handleDragStart(event: unknown) {
|
||||||
@@ -1766,7 +1803,12 @@ export default function Layout(props: ParentProps) {
|
|||||||
</TooltipKeybind>
|
</TooltipKeybind>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex-1 min-h-0">
|
<div class="flex-1 min-h-0">
|
||||||
<LocalWorkspace ctx={workspaceSidebarCtx} project={p()} mobile={panelProps.mobile} />
|
<LocalWorkspace
|
||||||
|
ctx={workspaceSidebarCtx}
|
||||||
|
project={p()}
|
||||||
|
sortNow={sortNow}
|
||||||
|
mobile={panelProps.mobile}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
@@ -1805,6 +1847,7 @@ export default function Layout(props: ParentProps) {
|
|||||||
ctx={workspaceSidebarCtx}
|
ctx={workspaceSidebarCtx}
|
||||||
directory={directory}
|
directory={directory}
|
||||||
project={p()}
|
project={p()}
|
||||||
|
sortNow={sortNow}
|
||||||
mobile={panelProps.mobile}
|
mobile={panelProps.mobile}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
@@ -1890,7 +1933,9 @@ export default function Layout(props: ParentProps) {
|
|||||||
opened={() => layout.sidebar.opened()}
|
opened={() => layout.sidebar.opened()}
|
||||||
aimMove={aim.move}
|
aimMove={aim.move}
|
||||||
projects={() => layout.projects.list()}
|
projects={() => layout.projects.list()}
|
||||||
renderProject={(project) => <SortableProject ctx={projectSidebarCtx} project={project} />}
|
renderProject={(project) => (
|
||||||
|
<SortableProject ctx={projectSidebarCtx} project={project} sortNow={sortNow} />
|
||||||
|
)}
|
||||||
handleDragStart={handleDragStart}
|
handleDragStart={handleDragStart}
|
||||||
handleDragEnd={handleDragEnd}
|
handleDragEnd={handleDragEnd}
|
||||||
handleDragOver={handleDragOver}
|
handleDragOver={handleDragOver}
|
||||||
@@ -1953,7 +1998,9 @@ export default function Layout(props: ParentProps) {
|
|||||||
opened={() => layout.sidebar.opened()}
|
opened={() => layout.sidebar.opened()}
|
||||||
aimMove={aim.move}
|
aimMove={aim.move}
|
||||||
projects={() => layout.projects.list()}
|
projects={() => layout.projects.list()}
|
||||||
renderProject={(project) => <SortableProject ctx={projectSidebarCtx} project={project} mobile />}
|
renderProject={(project) => (
|
||||||
|
<SortableProject ctx={projectSidebarCtx} project={project} sortNow={sortNow} mobile />
|
||||||
|
)}
|
||||||
handleDragStart={handleDragStart}
|
handleDragStart={handleDragStart}
|
||||||
handleDragEnd={handleDragEnd}
|
handleDragEnd={handleDragEnd}
|
||||||
handleDragOver={handleDragOver}
|
handleDragOver={handleDragOver}
|
||||||
|
|||||||
@@ -244,6 +244,7 @@ export const SortableProject = (props: {
|
|||||||
project: LocalProject
|
project: LocalProject
|
||||||
mobile?: boolean
|
mobile?: boolean
|
||||||
ctx: ProjectSidebarContext
|
ctx: ProjectSidebarContext
|
||||||
|
sortNow: Accessor<number>
|
||||||
}): JSX.Element => {
|
}): JSX.Element => {
|
||||||
const globalSync = useGlobalSync()
|
const globalSync = useGlobalSync()
|
||||||
const language = useLanguage()
|
const language = useLanguage()
|
||||||
@@ -284,11 +285,11 @@ export const SortableProject = (props: {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const projectStore = createMemo(() => globalSync.child(props.project.worktree, { bootstrap: false })[0])
|
const projectStore = createMemo(() => globalSync.child(props.project.worktree, { bootstrap: false })[0])
|
||||||
const projectSessions = createMemo(() => sortedRootSessions(projectStore(), Date.now()).slice(0, 2))
|
const projectSessions = createMemo(() => sortedRootSessions(projectStore(), props.sortNow()).slice(0, 2))
|
||||||
const projectChildren = createMemo(() => childMapByParent(projectStore().session))
|
const projectChildren = createMemo(() => childMapByParent(projectStore().session))
|
||||||
const workspaceSessions = (directory: string) => {
|
const workspaceSessions = (directory: string) => {
|
||||||
const [data] = globalSync.child(directory, { bootstrap: false })
|
const [data] = globalSync.child(directory, { bootstrap: false })
|
||||||
return sortedRootSessions(data, Date.now()).slice(0, 2)
|
return sortedRootSessions(data, props.sortNow()).slice(0, 2)
|
||||||
}
|
}
|
||||||
const workspaceChildren = (directory: string) => {
|
const workspaceChildren = (directory: string) => {
|
||||||
const [data] = globalSync.child(directory, { bootstrap: false })
|
const [data] = globalSync.child(directory, { bootstrap: false })
|
||||||
|
|||||||
@@ -302,6 +302,7 @@ export const SortableWorkspace = (props: {
|
|||||||
ctx: WorkspaceSidebarContext
|
ctx: WorkspaceSidebarContext
|
||||||
directory: string
|
directory: string
|
||||||
project: LocalProject
|
project: LocalProject
|
||||||
|
sortNow: Accessor<number>
|
||||||
mobile?: boolean
|
mobile?: boolean
|
||||||
}): JSX.Element => {
|
}): JSX.Element => {
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
@@ -315,7 +316,7 @@ export const SortableWorkspace = (props: {
|
|||||||
pendingRename: false,
|
pendingRename: false,
|
||||||
})
|
})
|
||||||
const slug = createMemo(() => base64Encode(props.directory))
|
const slug = createMemo(() => base64Encode(props.directory))
|
||||||
const sessions = createMemo(() => sortedRootSessions(workspaceStore, Date.now()))
|
const sessions = createMemo(() => sortedRootSessions(workspaceStore, props.sortNow()))
|
||||||
const children = createMemo(() => childMapByParent(workspaceStore.session))
|
const children = createMemo(() => childMapByParent(workspaceStore.session))
|
||||||
const local = createMemo(() => props.directory === props.project.worktree)
|
const local = createMemo(() => props.directory === props.project.worktree)
|
||||||
const active = createMemo(() => props.ctx.currentDir() === props.directory)
|
const active = createMemo(() => props.ctx.currentDir() === props.directory)
|
||||||
@@ -464,6 +465,7 @@ export const SortableWorkspace = (props: {
|
|||||||
export const LocalWorkspace = (props: {
|
export const LocalWorkspace = (props: {
|
||||||
ctx: WorkspaceSidebarContext
|
ctx: WorkspaceSidebarContext
|
||||||
project: LocalProject
|
project: LocalProject
|
||||||
|
sortNow: Accessor<number>
|
||||||
mobile?: boolean
|
mobile?: boolean
|
||||||
}): JSX.Element => {
|
}): JSX.Element => {
|
||||||
const globalSync = useGlobalSync()
|
const globalSync = useGlobalSync()
|
||||||
@@ -473,7 +475,7 @@ export const LocalWorkspace = (props: {
|
|||||||
return { store, setStore }
|
return { store, setStore }
|
||||||
})
|
})
|
||||||
const slug = createMemo(() => base64Encode(props.project.worktree))
|
const slug = createMemo(() => base64Encode(props.project.worktree))
|
||||||
const sessions = createMemo(() => sortedRootSessions(workspace().store, Date.now()))
|
const sessions = createMemo(() => sortedRootSessions(workspace().store, props.sortNow()))
|
||||||
const children = createMemo(() => childMapByParent(workspace().store.session))
|
const children = createMemo(() => childMapByParent(workspace().store.session))
|
||||||
const booted = createMemo((prev) => prev || workspace().store.status === "complete", false)
|
const booted = createMemo((prev) => prev || workspace().store.status === "complete", false)
|
||||||
const loading = createMemo(() => !booted() && sessions().length === 0)
|
const loading = createMemo(() => !booted() && sessions().length === 0)
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { type ValidComponent, createEffect, createMemo, For, Match, on, onCleanup, Show, Switch } from "solid-js"
|
import { type ValidComponent, createEffect, createMemo, For, Match, on, onCleanup, Show, Switch } from "solid-js"
|
||||||
import { createStore } from "solid-js/store"
|
import { createStore, produce } from "solid-js/store"
|
||||||
import { Dynamic } from "solid-js/web"
|
import { Dynamic } from "solid-js/web"
|
||||||
import { checksum } from "@opencode-ai/util/encode"
|
import { checksum } from "@opencode-ai/util/encode"
|
||||||
import { decode64 } from "@/utils/base64"
|
import { decode64 } from "@/utils/base64"
|
||||||
@@ -112,6 +112,12 @@ export function FileTabContent(props: {
|
|||||||
return props.comments.list(p)
|
return props.comments.list(p)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const commentLayout = createMemo(() => {
|
||||||
|
return fileComments()
|
||||||
|
.map((comment) => `${comment.id}:${comment.selection.start}:${comment.selection.end}`)
|
||||||
|
.join("|")
|
||||||
|
})
|
||||||
|
|
||||||
const commentedLines = createMemo(() => fileComments().map((comment) => comment.selection))
|
const commentedLines = createMemo(() => fileComments().map((comment) => comment.selection))
|
||||||
|
|
||||||
const [note, setNote] = createStore({
|
const [note, setNote] = createStore({
|
||||||
@@ -164,7 +170,22 @@ export function FileTabContent(props: {
|
|||||||
next[comment.id] = markerTop(el, marker)
|
next[comment.id] = markerTop(el, marker)
|
||||||
}
|
}
|
||||||
|
|
||||||
setNote("positions", next)
|
const removed = Object.keys(note.positions).filter((id) => next[id] === undefined)
|
||||||
|
const changed = Object.entries(next).filter(([id, top]) => note.positions[id] !== top)
|
||||||
|
if (removed.length > 0 || changed.length > 0) {
|
||||||
|
setNote(
|
||||||
|
"positions",
|
||||||
|
produce((draft) => {
|
||||||
|
for (const id of removed) {
|
||||||
|
delete draft[id]
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const [id, top] of changed) {
|
||||||
|
draft[id] = top
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
const range = note.commenting
|
const range = note.commenting
|
||||||
if (!range) {
|
if (!range) {
|
||||||
@@ -186,7 +207,7 @@ export function FileTabContent(props: {
|
|||||||
}
|
}
|
||||||
|
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
fileComments()
|
commentLayout()
|
||||||
scheduleComments()
|
scheduleComments()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import type { CommandOption } from "@/context/command"
|
import type { CommandOption } from "@/context/command"
|
||||||
|
import { batch } from "solid-js"
|
||||||
|
|
||||||
export const focusTerminalById = (id: string) => {
|
export const focusTerminalById = (id: string) => {
|
||||||
const wrapper = document.getElementById(`terminal-wrapper-${id}`)
|
const wrapper = document.getElementById(`terminal-wrapper-${id}`)
|
||||||
@@ -27,9 +28,11 @@ export const createOpenReviewFile = (input: {
|
|||||||
loadFile: (path: string) => void
|
loadFile: (path: string) => void
|
||||||
}) => {
|
}) => {
|
||||||
return (path: string) => {
|
return (path: string) => {
|
||||||
input.showAllFiles()
|
batch(() => {
|
||||||
input.openTab(input.tabForPath(path))
|
input.showAllFiles()
|
||||||
input.loadFile(path)
|
input.openTab(input.tabForPath(path))
|
||||||
|
input.loadFile(path)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -72,6 +72,8 @@ export function SessionSidePanel(props: {
|
|||||||
activeDiff?: string
|
activeDiff?: string
|
||||||
focusReviewDiff: (path: string) => void
|
focusReviewDiff: (path: string) => void
|
||||||
}) {
|
}) {
|
||||||
|
const openedTabs = createMemo(() => props.openedTabs())
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Show when={props.open}>
|
<Show when={props.open}>
|
||||||
<aside
|
<aside
|
||||||
@@ -140,8 +142,8 @@ export function SessionSidePanel(props: {
|
|||||||
</div>
|
</div>
|
||||||
</Tabs.Trigger>
|
</Tabs.Trigger>
|
||||||
</Show>
|
</Show>
|
||||||
<SortableProvider ids={props.openedTabs()}>
|
<SortableProvider ids={openedTabs()}>
|
||||||
<For each={props.openedTabs()}>
|
<For each={openedTabs()}>
|
||||||
{(tab) => <SortableTab tab={tab} onTabClose={props.tabs().close} />}
|
{(tab) => <SortableTab tab={tab} onTabClose={props.tabs().close} />}
|
||||||
</For>
|
</For>
|
||||||
</SortableProvider>
|
</SortableProvider>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { For, Show } from "solid-js"
|
import { For, Show, createMemo } from "solid-js"
|
||||||
import { Tabs } from "@opencode-ai/ui/tabs"
|
import { Tabs } from "@opencode-ai/ui/tabs"
|
||||||
import { ResizeHandle } from "@opencode-ai/ui/resize-handle"
|
import { ResizeHandle } from "@opencode-ai/ui/resize-handle"
|
||||||
import { IconButton } from "@opencode-ai/ui/icon-button"
|
import { IconButton } from "@opencode-ai/ui/icon-button"
|
||||||
@@ -8,7 +8,7 @@ import type { DragEvent } from "@thisbeyond/solid-dnd"
|
|||||||
import { ConstrainDragYAxis } from "@/utils/solid-dnd"
|
import { ConstrainDragYAxis } from "@/utils/solid-dnd"
|
||||||
import { SortableTerminalTab } from "@/components/session"
|
import { SortableTerminalTab } from "@/components/session"
|
||||||
import { Terminal } from "@/components/terminal"
|
import { Terminal } from "@/components/terminal"
|
||||||
import { useTerminal, type LocalPTY } from "@/context/terminal"
|
import { useTerminal } from "@/context/terminal"
|
||||||
import { useLanguage } from "@/context/language"
|
import { useLanguage } from "@/context/language"
|
||||||
import { useCommand } from "@/context/command"
|
import { useCommand } from "@/context/command"
|
||||||
import { terminalTabLabel } from "@/pages/session/terminal-label"
|
import { terminalTabLabel } from "@/pages/session/terminal-label"
|
||||||
@@ -28,6 +28,10 @@ export function TerminalPanel(props: {
|
|||||||
handleTerminalDragEnd: () => void
|
handleTerminalDragEnd: () => void
|
||||||
onCloseTab: () => void
|
onCloseTab: () => void
|
||||||
}) {
|
}) {
|
||||||
|
const all = createMemo(() => props.terminal.all())
|
||||||
|
const ids = createMemo(() => all().map((pty) => pty.id))
|
||||||
|
const byId = createMemo(() => new Map(all().map((pty) => [pty.id, pty])))
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Show when={props.open}>
|
<Show when={props.open}>
|
||||||
<div
|
<div
|
||||||
@@ -86,8 +90,8 @@ export function TerminalPanel(props: {
|
|||||||
class="!h-auto !flex-none"
|
class="!h-auto !flex-none"
|
||||||
>
|
>
|
||||||
<Tabs.List class="h-10">
|
<Tabs.List class="h-10">
|
||||||
<SortableProvider ids={props.terminal.all().map((t: LocalPTY) => t.id)}>
|
<SortableProvider ids={ids()}>
|
||||||
<For each={props.terminal.all()}>
|
<For each={all()}>
|
||||||
{(pty) => (
|
{(pty) => (
|
||||||
<SortableTerminalTab
|
<SortableTerminalTab
|
||||||
terminal={pty}
|
terminal={pty}
|
||||||
@@ -117,7 +121,7 @@ export function TerminalPanel(props: {
|
|||||||
</Tabs.List>
|
</Tabs.List>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
<div class="flex-1 min-h-0 relative">
|
<div class="flex-1 min-h-0 relative">
|
||||||
<For each={props.terminal.all()}>
|
<For each={all()}>
|
||||||
{(pty) => (
|
{(pty) => (
|
||||||
<div
|
<div
|
||||||
id={`terminal-wrapper-${pty.id}`}
|
id={`terminal-wrapper-${pty.id}`}
|
||||||
@@ -142,7 +146,7 @@ export function TerminalPanel(props: {
|
|||||||
<Show when={props.activeTerminalDraggable()}>
|
<Show when={props.activeTerminalDraggable()}>
|
||||||
{(draggedId) => {
|
{(draggedId) => {
|
||||||
return (
|
return (
|
||||||
<Show when={props.terminal.all().find((t: LocalPTY) => t.id === draggedId())}>
|
<Show when={byId().get(draggedId())}>
|
||||||
{(t) => (
|
{(t) => (
|
||||||
<div class="relative p-1 h-10 flex items-center bg-background-stronger text-14-regular">
|
<div class="relative p-1 h-10 flex items-center bg-background-stronger text-14-regular">
|
||||||
{terminalTabLabel({
|
{terminalTabLabel({
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { createEffect, on, onCleanup } from "solid-js"
|
import { createEffect, createMemo, on, onCleanup } from "solid-js"
|
||||||
import { UserMessage } from "@opencode-ai/sdk/v2"
|
import { UserMessage } from "@opencode-ai/sdk/v2"
|
||||||
|
|
||||||
export const messageIdFromHash = (hash: string) => {
|
export const messageIdFromHash = (hash: string) => {
|
||||||
@@ -26,6 +26,10 @@ export const useSessionHashScroll = (input: {
|
|||||||
scheduleScrollState: (el: HTMLDivElement) => void
|
scheduleScrollState: (el: HTMLDivElement) => void
|
||||||
consumePendingMessage: (key: string) => string | undefined
|
consumePendingMessage: (key: string) => string | undefined
|
||||||
}) => {
|
}) => {
|
||||||
|
const visibleUserMessages = createMemo(() => input.visibleUserMessages())
|
||||||
|
const messageById = createMemo(() => new Map(visibleUserMessages().map((m) => [m.id, m])))
|
||||||
|
const messageIndex = createMemo(() => new Map(visibleUserMessages().map((m, i) => [m.id, i])))
|
||||||
|
|
||||||
const clearMessageHash = () => {
|
const clearMessageHash = () => {
|
||||||
if (!window.location.hash) return
|
if (!window.location.hash) return
|
||||||
window.history.replaceState(null, "", window.location.href.replace(/#.*$/, ""))
|
window.history.replaceState(null, "", window.location.href.replace(/#.*$/, ""))
|
||||||
@@ -47,10 +51,9 @@ export const useSessionHashScroll = (input: {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const scrollToMessage = (message: UserMessage, behavior: ScrollBehavior = "smooth") => {
|
const scrollToMessage = (message: UserMessage, behavior: ScrollBehavior = "smooth") => {
|
||||||
input.setActiveMessage(message)
|
if (input.currentMessageId() !== message.id) input.setActiveMessage(message)
|
||||||
|
|
||||||
const msgs = input.visibleUserMessages()
|
const index = messageIndex().get(message.id) ?? -1
|
||||||
const index = msgs.findIndex((m) => m.id === message.id)
|
|
||||||
if (index !== -1 && index < input.turnStart()) {
|
if (index !== -1 && index < input.turnStart()) {
|
||||||
input.setTurnStart(index)
|
input.setTurnStart(index)
|
||||||
input.scheduleTurnBackfill()
|
input.scheduleTurnBackfill()
|
||||||
@@ -107,7 +110,7 @@ export const useSessionHashScroll = (input: {
|
|||||||
const messageId = messageIdFromHash(hash)
|
const messageId = messageIdFromHash(hash)
|
||||||
if (messageId) {
|
if (messageId) {
|
||||||
input.autoScroll.pause()
|
input.autoScroll.pause()
|
||||||
const msg = input.visibleUserMessages().find((m) => m.id === messageId)
|
const msg = messageById().get(messageId)
|
||||||
if (msg) {
|
if (msg) {
|
||||||
scrollToMessage(msg, behavior)
|
scrollToMessage(msg, behavior)
|
||||||
return
|
return
|
||||||
@@ -144,14 +147,14 @@ export const useSessionHashScroll = (input: {
|
|||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
if (!input.sessionID() || !input.messagesReady()) return
|
if (!input.sessionID() || !input.messagesReady()) return
|
||||||
|
|
||||||
input.visibleUserMessages().length
|
visibleUserMessages()
|
||||||
input.turnStart()
|
input.turnStart()
|
||||||
|
|
||||||
const targetId = input.pendingMessage() ?? messageIdFromHash(window.location.hash)
|
const targetId = input.pendingMessage() ?? messageIdFromHash(window.location.hash)
|
||||||
if (!targetId) return
|
if (!targetId) return
|
||||||
if (input.currentMessageId() === targetId) return
|
if (input.currentMessageId() === targetId) return
|
||||||
|
|
||||||
const msg = input.visibleUserMessages().find((m) => m.id === targetId)
|
const msg = messageById().get(targetId)
|
||||||
if (!msg) return
|
if (!msg) return
|
||||||
|
|
||||||
if (input.pendingMessage() === targetId) input.setPendingMessage(undefined)
|
if (input.pendingMessage() === targetId) input.setPendingMessage(undefined)
|
||||||
|
|||||||
Reference in New Issue
Block a user