app: better session id handling
parent
fb6bf0b35e
commit
cf3df010ce
|
|
@ -8,10 +8,11 @@ import { Font } from "@opencode-ai/ui/font"
|
|||
import { Splash } from "@opencode-ai/ui/logo"
|
||||
import { ThemeProvider } from "@opencode-ai/ui/theme"
|
||||
import { MetaProvider } from "@solidjs/meta"
|
||||
import { type BaseRouterProps, Navigate, Route, Router } from "@solidjs/router"
|
||||
import { type BaseRouterProps, Navigate, Route, Router, useLocation } from "@solidjs/router"
|
||||
import { type Duration, Effect } from "effect"
|
||||
import {
|
||||
type Component,
|
||||
createEffect,
|
||||
createMemo,
|
||||
createResource,
|
||||
createSignal,
|
||||
|
|
@ -114,6 +115,10 @@ function SessionProviders(props: ParentProps) {
|
|||
}
|
||||
|
||||
function RouterRoot(props: ParentProps<{ appChildren?: JSX.Element }>) {
|
||||
const l = useLocation()
|
||||
createEffect(() => {
|
||||
console.log("pathname", l.pathname)
|
||||
})
|
||||
return (
|
||||
<AppShellProviders>
|
||||
<Suspense fallback={<Loading />}>
|
||||
|
|
|
|||
|
|
@ -1,61 +1,62 @@
|
|||
import { useFilteredList } from "@opencode-ai/ui/hooks"
|
||||
import { useSpring } from "@opencode-ai/ui/motion-spring"
|
||||
import { createEffect, on, Component, Show, onCleanup, createMemo, createSignal } from "solid-js"
|
||||
import { createStore } from "solid-js/store"
|
||||
import { useLocal } from "@/context/local"
|
||||
import { selectionFromLines, type SelectedLineRange, useFile } from "@/context/file"
|
||||
import {
|
||||
ContentPart,
|
||||
DEFAULT_PROMPT,
|
||||
isPromptEqual,
|
||||
Prompt,
|
||||
usePrompt,
|
||||
ImageAttachmentPart,
|
||||
AgentPart,
|
||||
FileAttachmentPart,
|
||||
} from "@/context/prompt"
|
||||
import { useLayout } from "@/context/layout"
|
||||
import { useSDK } from "@/context/sdk"
|
||||
import { useSync } from "@/context/sync"
|
||||
import { useComments } from "@/context/comments"
|
||||
import { Button } from "@opencode-ai/ui/button"
|
||||
import { DockShellForm, DockTray } from "@opencode-ai/ui/dock-surface"
|
||||
import { Icon } from "@opencode-ai/ui/icon"
|
||||
import { ProviderIcon } from "@opencode-ai/ui/provider-icon"
|
||||
import { Tooltip, TooltipKeybind } from "@opencode-ai/ui/tooltip"
|
||||
import { IconButton } from "@opencode-ai/ui/icon-button"
|
||||
import { Select } from "@opencode-ai/ui/select"
|
||||
import { useDialog } from "@opencode-ai/ui/context/dialog"
|
||||
import { DockShellForm, DockTray } from "@opencode-ai/ui/dock-surface"
|
||||
import { useFilteredList } from "@opencode-ai/ui/hooks"
|
||||
import { Icon } from "@opencode-ai/ui/icon"
|
||||
import { IconButton } from "@opencode-ai/ui/icon-button"
|
||||
import { ImagePreview } from "@opencode-ai/ui/image-preview"
|
||||
import { useSpring } from "@opencode-ai/ui/motion-spring"
|
||||
import { ProviderIcon } from "@opencode-ai/ui/provider-icon"
|
||||
import { Select } from "@opencode-ai/ui/select"
|
||||
import { Tooltip, TooltipKeybind } from "@opencode-ai/ui/tooltip"
|
||||
import { type Component, createEffect, createMemo, createSignal, on, onCleanup, Show } from "solid-js"
|
||||
import { createStore } from "solid-js/store"
|
||||
import { ModelSelectorPopover } from "@/components/dialog-select-model"
|
||||
import { DialogSelectModelUnpaid } from "@/components/dialog-select-model-unpaid"
|
||||
import { useProviders } from "@/hooks/use-providers"
|
||||
import { useCommand } from "@/context/command"
|
||||
import { Persist, persisted } from "@/utils/persist"
|
||||
import { usePermission } from "@/context/permission"
|
||||
import { useComments } from "@/context/comments"
|
||||
import { type SelectedLineRange, selectionFromLines, useFile } from "@/context/file"
|
||||
import { useLanguage } from "@/context/language"
|
||||
import { useLayout } from "@/context/layout"
|
||||
import { useLocal } from "@/context/local"
|
||||
import { usePermission } from "@/context/permission"
|
||||
import { usePlatform } from "@/context/platform"
|
||||
import { useSessionLayout } from "@/pages/session/session-layout"
|
||||
import {
|
||||
type AgentPart,
|
||||
type ContentPart,
|
||||
DEFAULT_PROMPT,
|
||||
type FileAttachmentPart,
|
||||
type ImageAttachmentPart,
|
||||
isPromptEqual,
|
||||
type Prompt,
|
||||
usePrompt,
|
||||
} from "@/context/prompt"
|
||||
import { useSDK } from "@/context/sdk"
|
||||
import { useSync } from "@/context/sync"
|
||||
import { useProviders } from "@/hooks/use-providers"
|
||||
import { createSessionTabs } from "@/pages/session/helpers"
|
||||
import { useSessionLayout } from "@/pages/session/session-layout"
|
||||
import { promptEnabled, promptProbe } from "@/testing/prompt"
|
||||
import { createTextFragment, getCursorPosition, setCursorPosition, setRangeEdge } from "./prompt-input/editor-dom"
|
||||
import { Optional } from "@/utils/optional"
|
||||
import { Persist, persisted } from "@/utils/persist"
|
||||
import { createPromptAttachments } from "./prompt-input/attachments"
|
||||
import { PromptContextItems } from "./prompt-input/context-items"
|
||||
import { PromptDragOverlay } from "./prompt-input/drag-overlay"
|
||||
import { createTextFragment, getCursorPosition, setCursorPosition, setRangeEdge } from "./prompt-input/editor-dom"
|
||||
import { ACCEPTED_FILE_TYPES } from "./prompt-input/files"
|
||||
import {
|
||||
canNavigateHistoryAtCursor,
|
||||
navigatePromptHistory,
|
||||
prependHistoryEntry,
|
||||
type PromptHistoryComment,
|
||||
type PromptHistoryEntry,
|
||||
type PromptHistoryStoredEntry,
|
||||
prependHistoryEntry,
|
||||
promptLength,
|
||||
} from "./prompt-input/history"
|
||||
import { createPromptSubmit, type FollowupDraft } from "./prompt-input/submit"
|
||||
import { PromptPopover, type AtOption, type SlashCommand } from "./prompt-input/slash-popover"
|
||||
import { PromptContextItems } from "./prompt-input/context-items"
|
||||
import { PromptImageAttachments } from "./prompt-input/image-attachments"
|
||||
import { PromptDragOverlay } from "./prompt-input/drag-overlay"
|
||||
import { promptPlaceholder } from "./prompt-input/placeholder"
|
||||
import { ImagePreview } from "@opencode-ai/ui/image-preview"
|
||||
import { type AtOption, PromptPopover, type SlashCommand } from "./prompt-input/slash-popover"
|
||||
import { createPromptSubmit, type FollowupDraft } from "./prompt-input/submit"
|
||||
|
||||
interface PromptInputProps {
|
||||
class?: string
|
||||
|
|
@ -171,10 +172,10 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|||
}).activeFileTab
|
||||
|
||||
const commentInReview = (path: string) => {
|
||||
const sessionID = params.id
|
||||
if (!sessionID) return false
|
||||
const id = params.id
|
||||
if (!id) return false
|
||||
|
||||
const diffs = sync.data.session_diff[sessionID]
|
||||
const diffs = sync.data.session_diff[id]
|
||||
if (!diffs) return false
|
||||
return diffs.some((diff) => diff.file === path)
|
||||
}
|
||||
|
|
@ -236,10 +237,10 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|||
|
||||
return paths
|
||||
})
|
||||
const info = createMemo(() => (params.id ? sync.session.get(params.id) : undefined))
|
||||
const info = createMemo(() => Optional.map(params.id, (s) => sync.session.get(s)))
|
||||
const status = createMemo(
|
||||
() =>
|
||||
sync.data.session_status[params.id ?? ""] ?? {
|
||||
Optional.map(params.id, (id) => sync.data.session_status[id]) ?? {
|
||||
type: "idle",
|
||||
},
|
||||
)
|
||||
|
|
@ -283,7 +284,10 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|||
applyingHistory: false,
|
||||
})
|
||||
|
||||
const buttonsSpring = useSpring(() => (store.mode === "normal" ? 1 : 0), { visualDuration: 0.2, bounce: 0 })
|
||||
const buttonsSpring = useSpring(() => (store.mode === "normal" ? 1 : 0), {
|
||||
visualDuration: 0.2,
|
||||
bounce: 0,
|
||||
})
|
||||
const motion = (value: number) => ({
|
||||
opacity: value,
|
||||
transform: `scale(${0.95 + value * 0.05})`,
|
||||
|
|
@ -306,9 +310,9 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|||
})
|
||||
|
||||
const hasUserPrompt = createMemo(() => {
|
||||
const sessionID = params.id
|
||||
if (!sessionID) return false
|
||||
const messages = sync.data.message[sessionID]
|
||||
const id = params.id
|
||||
if (!id) return false
|
||||
const messages = sync.data.message[id]
|
||||
if (!messages) return false
|
||||
return messages.some((m) => m.role === "user")
|
||||
})
|
||||
|
|
@ -510,9 +514,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|||
}
|
||||
|
||||
createEffect(() => {
|
||||
params.id
|
||||
if (params.id) return
|
||||
if (!suggest()) return
|
||||
if (params.id || !suggest()) return
|
||||
const interval = setInterval(() => {
|
||||
setStore("placeholder", (prev) => (prev + 1) % EXAMPLES.length)
|
||||
}, 6500)
|
||||
|
|
@ -542,16 +544,34 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|||
const agentList = createMemo(() =>
|
||||
sync.data.agent
|
||||
.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) => {
|
||||
if (!option) return
|
||||
if (option.type === "agent") {
|
||||
addPart({ type: "agent", name: option.name, content: "@" + option.name, start: 0, end: 0 })
|
||||
addPart({
|
||||
type: "agent",
|
||||
name: option.name,
|
||||
content: "@" + option.name,
|
||||
start: 0,
|
||||
end: 0,
|
||||
})
|
||||
} else {
|
||||
addPart({ type: "file", path: option.path, content: "@" + option.path, start: 0, end: 0 })
|
||||
addPart({
|
||||
type: "file",
|
||||
path: option.path,
|
||||
content: "@" + option.path,
|
||||
start: 0,
|
||||
end: 0,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -571,7 +591,12 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|||
const agents = agentList()
|
||||
const open = recent()
|
||||
const seen = new Set(open)
|
||||
const pinned: AtOption[] = open.map((path) => ({ type: "file", path, display: path, recent: true }))
|
||||
const pinned: AtOption[] = open.map((path) => ({
|
||||
type: "file",
|
||||
path,
|
||||
display: path,
|
||||
recent: true,
|
||||
}))
|
||||
const paths = await files.searchFilesAndDirectories(query)
|
||||
const fileOptions: AtOption[] = paths
|
||||
.filter((path) => !seen.has(path))
|
||||
|
|
@ -780,7 +805,12 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|||
if (content.includes("\u200B")) content = content.replace(/\u200B/g, "")
|
||||
buffer = ""
|
||||
if (!content) return
|
||||
parts.push({ type: "text", content, start: position, end: position + content.length })
|
||||
parts.push({
|
||||
type: "text",
|
||||
content,
|
||||
start: position,
|
||||
end: position + content.length,
|
||||
})
|
||||
position += content.length
|
||||
}
|
||||
|
||||
|
|
@ -1057,20 +1087,16 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|||
|
||||
const variants = createMemo(() => ["default", ...local.model.variant.list()])
|
||||
const accepting = createMemo(() => {
|
||||
const id = params.id
|
||||
if (!id) return permission.isAutoAcceptingDirectory(sdk.directory)
|
||||
return permission.isAutoAccepting(id, sdk.directory)
|
||||
if (!params.id) return permission.isAutoAcceptingDirectory(sdk.directory)
|
||||
return permission.isAutoAccepting(params.id, sdk.directory)
|
||||
})
|
||||
const acceptLabel = createMemo(() =>
|
||||
language.t(accepting() ? "command.permissions.autoaccept.disable" : "command.permissions.autoaccept.enable"),
|
||||
)
|
||||
const toggleAccept = () => {
|
||||
if (!params.id) {
|
||||
permission.toggleAutoAcceptDirectory(sdk.directory)
|
||||
return
|
||||
}
|
||||
|
||||
permission.toggleAutoAccept(params.id, sdk.directory)
|
||||
const id = params.id
|
||||
if (!id) permission.toggleAutoAcceptDirectory(sdk.directory)
|
||||
else permission.toggleAutoAccept(id, sdk.directory)
|
||||
}
|
||||
|
||||
const { abort, handleSubmit } = createPromptSubmit({
|
||||
|
|
@ -1503,7 +1529,10 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|||
<ProviderIcon
|
||||
id={local.model.current()!.provider.id}
|
||||
class="size-4 shrink-0 opacity-40 group-hover:opacity-100 transition-opacity duration-150"
|
||||
style={{ "will-change": "opacity", transform: "translateZ(0)" }}
|
||||
style={{
|
||||
"will-change": "opacity",
|
||||
transform: "translateZ(0)",
|
||||
}}
|
||||
/>
|
||||
</Show>
|
||||
<span class="truncate">
|
||||
|
|
@ -1535,7 +1564,10 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|||
<ProviderIcon
|
||||
id={local.model.current()!.provider.id}
|
||||
class="size-4 shrink-0 opacity-40 group-hover:opacity-100 transition-opacity duration-150"
|
||||
style={{ "will-change": "opacity", transform: "translateZ(0)" }}
|
||||
style={{
|
||||
"will-change": "opacity",
|
||||
transform: "translateZ(0)",
|
||||
}}
|
||||
/>
|
||||
</Show>
|
||||
<span class="truncate">
|
||||
|
|
|
|||
|
|
@ -1,21 +1,22 @@
|
|||
import { createMemo, createEffect, on, onCleanup, For, Show } from "solid-js"
|
||||
import type { JSX } from "solid-js"
|
||||
import { useSync } from "@/context/sync"
|
||||
import { checksum } from "@opencode-ai/util/encode"
|
||||
import { findLast } from "@opencode-ai/util/array"
|
||||
import { same } from "@/utils/same"
|
||||
import { Icon } from "@opencode-ai/ui/icon"
|
||||
import type { Message, Part, UserMessage } from "@opencode-ai/sdk/v2/client"
|
||||
import { Accordion } from "@opencode-ai/ui/accordion"
|
||||
import { StickyAccordionHeader } from "@opencode-ai/ui/sticky-accordion-header"
|
||||
import { File } from "@opencode-ai/ui/file"
|
||||
import { Icon } from "@opencode-ai/ui/icon"
|
||||
import { Markdown } from "@opencode-ai/ui/markdown"
|
||||
import { ScrollView } from "@opencode-ai/ui/scroll-view"
|
||||
import type { Message, Part, UserMessage } from "@opencode-ai/sdk/v2/client"
|
||||
import { StickyAccordionHeader } from "@opencode-ai/ui/sticky-accordion-header"
|
||||
import { findLast } from "@opencode-ai/util/array"
|
||||
import { checksum } from "@opencode-ai/util/encode"
|
||||
import type { JSX } from "solid-js"
|
||||
import { createEffect, createMemo, For, on, onCleanup, Show } from "solid-js"
|
||||
import { useLanguage } from "@/context/language"
|
||||
import { useSync } from "@/context/sync"
|
||||
import { useSessionLayout } from "@/pages/session/session-layout"
|
||||
import { getSessionContextMetrics } from "./session-context-metrics"
|
||||
import { Optional } from "@/utils/optional"
|
||||
import { same } from "@/utils/same"
|
||||
import { estimateSessionContextBreakdown, type SessionContextBreakdownKey } from "./session-context-breakdown"
|
||||
import { createSessionContextFormatter } from "./session-context-format"
|
||||
import { getSessionContextMetrics } from "./session-context-metrics"
|
||||
|
||||
const BREAKDOWN_COLOR: Record<SessionContextBreakdownKey, string> = {
|
||||
system: "var(--syntax-info)",
|
||||
|
|
@ -94,7 +95,7 @@ export function SessionContextTab() {
|
|||
const language = useLanguage()
|
||||
const { params, view } = useSessionLayout()
|
||||
|
||||
const info = createMemo(() => (params.id ? sync.session.get(params.id) : undefined))
|
||||
const info = createMemo(() => Optional.map(params.id, (s) => sync.session.get(s)))
|
||||
|
||||
const messages = createMemo(
|
||||
() => {
|
||||
|
|
|
|||
|
|
@ -1,44 +1,44 @@
|
|||
import type { Project, UserMessage } from "@opencode-ai/sdk/v2"
|
||||
import { Button } from "@opencode-ai/ui/button"
|
||||
import { useDialog } from "@opencode-ai/ui/context/dialog"
|
||||
import {
|
||||
batch,
|
||||
onCleanup,
|
||||
Show,
|
||||
Match,
|
||||
Switch,
|
||||
createMemo,
|
||||
createEffect,
|
||||
createComputed,
|
||||
on,
|
||||
onMount,
|
||||
untrack,
|
||||
} from "solid-js"
|
||||
import { createMediaQuery } from "@solid-primitives/media"
|
||||
import { createResizeObserver } from "@solid-primitives/resize-observer"
|
||||
import { useLocal } from "@/context/local"
|
||||
import { selectionFromLines, useFile, type FileSelection, type SelectedLineRange } from "@/context/file"
|
||||
import { createStore } from "solid-js/store"
|
||||
import { createAutoScroll } from "@opencode-ai/ui/hooks"
|
||||
import { previewSelectedLines } from "@opencode-ai/ui/pierre/selection-bridge"
|
||||
import { ResizeHandle } from "@opencode-ai/ui/resize-handle"
|
||||
import { Select } from "@opencode-ai/ui/select"
|
||||
import { Tabs } from "@opencode-ai/ui/tabs"
|
||||
import { createAutoScroll } from "@opencode-ai/ui/hooks"
|
||||
import { previewSelectedLines } from "@opencode-ai/ui/pierre/selection-bridge"
|
||||
import { Button } from "@opencode-ai/ui/button"
|
||||
import { showToast } from "@opencode-ai/ui/toast"
|
||||
import { base64Encode, checksum } from "@opencode-ai/util/encode"
|
||||
import { createMediaQuery } from "@solid-primitives/media"
|
||||
import { createResizeObserver } from "@solid-primitives/resize-observer"
|
||||
import { useNavigate, useSearchParams } from "@solidjs/router"
|
||||
import {
|
||||
batch,
|
||||
createComputed,
|
||||
createEffect,
|
||||
createMemo,
|
||||
Match,
|
||||
on,
|
||||
onCleanup,
|
||||
onMount,
|
||||
Show,
|
||||
Switch,
|
||||
untrack,
|
||||
} from "solid-js"
|
||||
import { createStore } from "solid-js/store"
|
||||
import { type FollowupDraft, sendFollowupDraft } from "@/components/prompt-input/submit"
|
||||
import { NewSessionView, SessionHeader } from "@/components/session"
|
||||
import { useComments } from "@/context/comments"
|
||||
import { getSessionPrefetch, SESSION_PREFETCH_TTL } from "@/context/global-sync/session-prefetch"
|
||||
import { type FileSelection, type SelectedLineRange, selectionFromLines, useFile } from "@/context/file"
|
||||
import { useGlobalSync } from "@/context/global-sync"
|
||||
import { getSessionPrefetch, SESSION_PREFETCH_TTL } from "@/context/global-sync/session-prefetch"
|
||||
import { useLanguage } from "@/context/language"
|
||||
import { useLayout } from "@/context/layout"
|
||||
import { useLocal } from "@/context/local"
|
||||
import { usePrompt } from "@/context/prompt"
|
||||
import { useSDK } from "@/context/sdk"
|
||||
import { useSettings } from "@/context/settings"
|
||||
import { useSync } from "@/context/sync"
|
||||
import { useTerminal } from "@/context/terminal"
|
||||
import { type FollowupDraft, sendFollowupDraft } from "@/components/prompt-input/submit"
|
||||
import { createSessionComposerState, SessionComposerRegion } from "@/pages/session/composer"
|
||||
import { createOpenReviewFile, createSessionTabs, createSizing, focusTerminalById } from "@/pages/session/helpers"
|
||||
import { MessageTimeline } from "@/pages/session/message-timeline"
|
||||
|
|
@ -76,6 +76,8 @@ type SessionHistoryWindowInput = {
|
|||
* small batches while scrolling upward, and prefetches older history near top.
|
||||
*/
|
||||
function createSessionHistoryWindow(input: SessionHistoryWindowInput) {
|
||||
const { params } = useSessionLayout()
|
||||
|
||||
const turnInit = 10
|
||||
const turnBatch = 8
|
||||
const turnScrollThreshold = 200
|
||||
|
|
@ -93,7 +95,7 @@ function createSessionHistoryWindow(input: SessionHistoryWindowInput) {
|
|||
const initialTurnStart = (len: number) => (len > turnInit ? len - turnInit : 0)
|
||||
|
||||
const turnStart = createMemo(() => {
|
||||
const id = input.sessionID()
|
||||
const id = params.id
|
||||
const len = input.visibleUserMessages().length
|
||||
if (!id || len <= 0) return 0
|
||||
if (state.turnID !== id) return initialTurnStart(len)
|
||||
|
|
@ -103,7 +105,7 @@ function createSessionHistoryWindow(input: SessionHistoryWindowInput) {
|
|||
})
|
||||
|
||||
const setTurnStart = (start: number) => {
|
||||
const id = input.sessionID()
|
||||
const id = params.id
|
||||
const next = start > 0 ? start : 0
|
||||
if (!id) {
|
||||
setState({ turnID: undefined, turnStart: next })
|
||||
|
|
@ -153,7 +155,7 @@ function createSessionHistoryWindow(input: SessionHistoryWindowInput) {
|
|||
|
||||
/** Button path: reveal all cached turns, fetch older history, reveal one batch. */
|
||||
const loadAndReveal = async () => {
|
||||
const id = input.sessionID()
|
||||
const id = params.id
|
||||
if (!id) return
|
||||
|
||||
const start = turnStart()
|
||||
|
|
@ -169,7 +171,7 @@ function createSessionHistoryWindow(input: SessionHistoryWindowInput) {
|
|||
|
||||
while (true) {
|
||||
await input.loadMore(id)
|
||||
if (input.sessionID() !== id) return
|
||||
if (params.id !== id) return
|
||||
|
||||
afterVisible = input.visibleUserMessages().length
|
||||
const nextLoaded = input.loaded()
|
||||
|
|
@ -195,7 +197,7 @@ function createSessionHistoryWindow(input: SessionHistoryWindowInput) {
|
|||
|
||||
/** Scroll/prefetch path: fetch older history from server. */
|
||||
const fetchOlderMessages = async (opts?: { prefetch?: boolean }) => {
|
||||
const id = input.sessionID()
|
||||
const id = params.id
|
||||
if (!id) return
|
||||
if (!input.historyMore() || input.historyLoading()) return
|
||||
|
||||
|
|
@ -215,7 +217,7 @@ function createSessionHistoryWindow(input: SessionHistoryWindowInput) {
|
|||
|
||||
while (true) {
|
||||
await input.loadMore(id)
|
||||
if (input.sessionID() !== id) return
|
||||
if (params.id !== id) return
|
||||
|
||||
const nextLoaded = input.loaded()
|
||||
const raw = nextLoaded - loaded
|
||||
|
|
@ -279,7 +281,7 @@ function createSessionHistoryWindow(input: SessionHistoryWindowInput) {
|
|||
|
||||
createEffect(
|
||||
on(
|
||||
() => [input.sessionID(), input.messagesReady()] as const,
|
||||
() => [params.id, input.messagesReady()] as const,
|
||||
([id, ready]) => {
|
||||
if (!id || !ready) return
|
||||
setTurnStart(initialTurnStart(input.visibleUserMessages().length))
|
||||
|
|
|
|||
|
|
@ -1,18 +1,19 @@
|
|||
import { Show, createEffect, createMemo, onCleanup } from "solid-js"
|
||||
import { createStore } from "solid-js/store"
|
||||
import { useSpring } from "@opencode-ai/ui/motion-spring"
|
||||
import { createEffect, createMemo, onCleanup, Show } from "solid-js"
|
||||
import { createStore } from "solid-js/store"
|
||||
import { PromptInput } from "@/components/prompt-input"
|
||||
import type { FollowupDraft } from "@/components/prompt-input/submit"
|
||||
import { useLanguage } from "@/context/language"
|
||||
import { usePrompt } from "@/context/prompt"
|
||||
import { getSessionHandoff, setSessionHandoff } from "@/pages/session/handoff"
|
||||
import { useSessionKey } from "@/pages/session/session-layout"
|
||||
import type { SessionComposerState } from "@/pages/session/composer/session-composer-state"
|
||||
import { SessionFollowupDock } from "@/pages/session/composer/session-followup-dock"
|
||||
import { SessionPermissionDock } from "@/pages/session/composer/session-permission-dock"
|
||||
import { SessionQuestionDock } from "@/pages/session/composer/session-question-dock"
|
||||
import { SessionFollowupDock } from "@/pages/session/composer/session-followup-dock"
|
||||
import { SessionRevertDock } from "@/pages/session/composer/session-revert-dock"
|
||||
import type { SessionComposerState } from "@/pages/session/composer/session-composer-state"
|
||||
import { SessionTodoDock } from "@/pages/session/composer/session-todo-dock"
|
||||
import type { FollowupDraft } from "@/components/prompt-input/submit"
|
||||
import { getSessionHandoff, setSessionHandoff } from "@/pages/session/handoff"
|
||||
|
||||
import { useSessionKey } from "@/pages/session/session-layout"
|
||||
|
||||
export function SessionComposerRegion(props: {
|
||||
state: SessionComposerState
|
||||
|
|
@ -194,7 +195,6 @@ export function SessionComposerRegion(props: {
|
|||
>
|
||||
<div ref={(el) => setStore("body", el)}>
|
||||
<SessionTodoDock
|
||||
sessionID={route.params.id}
|
||||
todos={props.state.todos()}
|
||||
collapseLabel={language.t("session.todo.collapse")}
|
||||
expandLabel={language.t("session.todo.expand")}
|
||||
|
|
|
|||
|
|
@ -1,14 +1,15 @@
|
|||
import type { PermissionRequest, QuestionRequest, Todo } from "@opencode-ai/sdk/v2"
|
||||
import { showToast } from "@opencode-ai/ui/toast"
|
||||
import { createEffect, createMemo, on, onCleanup, onMount } from "solid-js"
|
||||
import { createStore } from "solid-js/store"
|
||||
import type { PermissionRequest, QuestionRequest, Todo } from "@opencode-ai/sdk/v2"
|
||||
import { useParams } from "@solidjs/router"
|
||||
import { showToast } from "@opencode-ai/ui/toast"
|
||||
import { useGlobalSync } from "@/context/global-sync"
|
||||
import { useLanguage } from "@/context/language"
|
||||
import { usePermission } from "@/context/permission"
|
||||
import { useSDK } from "@/context/sdk"
|
||||
import { useSync } from "@/context/sync"
|
||||
import { composerDriver, composerEnabled, composerEvent } from "@/testing/session-composer"
|
||||
import { Optional } from "@/utils/optional"
|
||||
import { useSessionLayout } from "../session-layout"
|
||||
import { sessionPermissionRequest, sessionQuestionRequest } from "./session-request-tree"
|
||||
|
||||
export const todoState = (input: {
|
||||
|
|
@ -25,12 +26,12 @@ export const todoState = (input: {
|
|||
const idle = { type: "idle" as const }
|
||||
|
||||
export function createSessionComposerState(options?: { closeMs?: number | (() => number) }) {
|
||||
const params = useParams()
|
||||
const sdk = useSDK()
|
||||
const sync = useSync()
|
||||
const globalSync = useGlobalSync()
|
||||
const language = useLanguage()
|
||||
const permission = usePermission()
|
||||
const { params } = useSessionLayout()
|
||||
|
||||
const questionRequest = createMemo((): QuestionRequest | undefined => {
|
||||
return sessionQuestionRequest(sync.data.session, sync.data.question, params.id)
|
||||
|
|
@ -43,8 +44,7 @@ export function createSessionComposerState(options?: { closeMs?: number | (() =>
|
|||
})
|
||||
|
||||
const blocked = createMemo(() => {
|
||||
const id = params.id
|
||||
if (!id) return false
|
||||
if (!params.id) return false
|
||||
return !!permissionRequest() || !!questionRequest()
|
||||
})
|
||||
|
||||
|
|
@ -101,11 +101,7 @@ export function createSessionComposerState(options?: { closeMs?: number | (() =>
|
|||
() => todos().length > 0 && todos().every((todo) => todo.status === "completed" || todo.status === "cancelled"),
|
||||
)
|
||||
|
||||
const status = createMemo(() => {
|
||||
const id = params.id
|
||||
if (!id) return idle
|
||||
return sync.data.session_status[id] ?? idle
|
||||
})
|
||||
const status = createMemo(() => Optional.map(params.id, (id) => sync.data.session_status[id]) ?? idle)
|
||||
|
||||
const busy = createMemo(() => status().type !== "idle")
|
||||
const live = createMemo(() => {
|
||||
|
|
|
|||
|
|
@ -6,10 +6,11 @@ import { IconButton } from "@opencode-ai/ui/icon-button"
|
|||
import { useSpring } from "@opencode-ai/ui/motion-spring"
|
||||
import { TextReveal } from "@opencode-ai/ui/text-reveal"
|
||||
import { TextStrikethrough } from "@opencode-ai/ui/text-strikethrough"
|
||||
import { Index, createEffect, createMemo, on, onCleanup } from "solid-js"
|
||||
import { createEffect, createMemo, Index, on, onCleanup } from "solid-js"
|
||||
import { createStore } from "solid-js/store"
|
||||
import { composerEnabled, composerProbe } from "@/testing/session-composer"
|
||||
import { useLanguage } from "@/context/language"
|
||||
import { composerEnabled, composerProbe } from "@/testing/session-composer"
|
||||
import { useSessionLayout } from "../session-layout"
|
||||
|
||||
const doneToken = "\u0000done\u0000"
|
||||
const totalToken = "\u0000total\u0000"
|
||||
|
|
@ -40,7 +41,6 @@ function dot(status: Todo["status"]) {
|
|||
}
|
||||
|
||||
export function SessionTodoDock(props: {
|
||||
sessionID?: string
|
||||
todos: Todo[]
|
||||
collapseLabel: string
|
||||
expandLabel: string
|
||||
|
|
@ -51,6 +51,7 @@ export function SessionTodoDock(props: {
|
|||
collapsed: false,
|
||||
height: 320,
|
||||
})
|
||||
const { params } = useSessionLayout()
|
||||
|
||||
const toggle = () => setStore("collapsed", (value) => !value)
|
||||
|
||||
|
|
@ -81,7 +82,7 @@ export function SessionTodoDock(props: {
|
|||
const turn = createMemo(() => Math.max(0, Math.min(1, value())))
|
||||
const full = createMemo(() => Math.max(78, store.height))
|
||||
const e2e = composerEnabled()
|
||||
const probe = composerProbe(props.sessionID)
|
||||
const probe = composerProbe(params.id)
|
||||
let contentRef: HTMLDivElement | undefined
|
||||
|
||||
createEffect(() => {
|
||||
|
|
|
|||
|
|
@ -1,34 +1,35 @@
|
|||
import { For, createEffect, createMemo, on, onCleanup, Show, Index, type JSX } from "solid-js"
|
||||
import { createStore, produce } from "solid-js/store"
|
||||
import { useNavigate } from "@solidjs/router"
|
||||
import { Popover as KobaltePopover } from "@kobalte/core/popover"
|
||||
import type { AssistantMessage, Message as MessageType, Part, TextPart, UserMessage } from "@opencode-ai/sdk/v2"
|
||||
import { Button } from "@opencode-ai/ui/button"
|
||||
import { useDialog } from "@opencode-ai/ui/context/dialog"
|
||||
import { Dialog } from "@opencode-ai/ui/dialog"
|
||||
import { DropdownMenu } from "@opencode-ai/ui/dropdown-menu"
|
||||
import { FileIcon } from "@opencode-ai/ui/file-icon"
|
||||
import { Icon } from "@opencode-ai/ui/icon"
|
||||
import { IconButton } from "@opencode-ai/ui/icon-button"
|
||||
import { DropdownMenu } from "@opencode-ai/ui/dropdown-menu"
|
||||
import { Dialog } from "@opencode-ai/ui/dialog"
|
||||
import { InlineInput } from "@opencode-ai/ui/inline-input"
|
||||
import { Spinner } from "@opencode-ai/ui/spinner"
|
||||
import { SessionTurn } from "@opencode-ai/ui/session-turn"
|
||||
import { ScrollView } from "@opencode-ai/ui/scroll-view"
|
||||
import { SessionTurn } from "@opencode-ai/ui/session-turn"
|
||||
import { Spinner } from "@opencode-ai/ui/spinner"
|
||||
import { TextField } from "@opencode-ai/ui/text-field"
|
||||
import type { AssistantMessage, Message as MessageType, Part, TextPart, UserMessage } from "@opencode-ai/sdk/v2"
|
||||
import { showToast } from "@opencode-ai/ui/toast"
|
||||
import { Binary } from "@opencode-ai/util/binary"
|
||||
import { getFilename } from "@opencode-ai/util/path"
|
||||
import { Popover as KobaltePopover } from "@kobalte/core/popover"
|
||||
import { shouldMarkBoundaryGesture, normalizeWheelDelta } from "@/pages/session/message-gesture"
|
||||
import { useNavigate } from "@solidjs/router"
|
||||
import { createEffect, createMemo, For, Index, type JSX, on, onCleanup, Show } from "solid-js"
|
||||
import { createStore, produce } from "solid-js/store"
|
||||
import { SessionContextUsage } from "@/components/session-context-usage"
|
||||
import { useDialog } from "@opencode-ai/ui/context/dialog"
|
||||
import { useLanguage } from "@/context/language"
|
||||
import { useSessionKey } from "@/pages/session/session-layout"
|
||||
import { useGlobalSDK } from "@/context/global-sdk"
|
||||
import { useLanguage } from "@/context/language"
|
||||
import { usePlatform } from "@/context/platform"
|
||||
import { useSettings } from "@/context/settings"
|
||||
import { useSDK } from "@/context/sdk"
|
||||
import { useSettings } from "@/context/settings"
|
||||
import { useSync } from "@/context/sync"
|
||||
import { normalizeWheelDelta, shouldMarkBoundaryGesture } from "@/pages/session/message-gesture"
|
||||
import { useSessionKey } from "@/pages/session/session-layout"
|
||||
import { messageAgentColor } from "@/utils/agent"
|
||||
import { parseCommentNote, readCommentMetadata } from "@/utils/comment-note"
|
||||
import { Optional } from "@/utils/optional"
|
||||
|
||||
type MessageComment = {
|
||||
path: string
|
||||
|
|
@ -230,22 +231,13 @@ export function MessageTimeline(props: {
|
|||
const platform = usePlatform()
|
||||
|
||||
const rendered = createMemo(() => props.renderedUserMessages.map((message) => message.id))
|
||||
const sessionID = createMemo(() => params.id)
|
||||
const sessionMessages = createMemo(() => {
|
||||
const id = sessionID()
|
||||
if (!id) return emptyMessages
|
||||
return sync.data.message[id] ?? emptyMessages
|
||||
})
|
||||
const sessionMessages = createMemo(() => Optional.map(params.id, (id) => sync.data.message[id]) ?? emptyMessages)
|
||||
const pending = createMemo(() =>
|
||||
sessionMessages().findLast(
|
||||
(item): item is AssistantMessage => item.role === "assistant" && typeof item.time.completed !== "number",
|
||||
),
|
||||
)
|
||||
const sessionStatus = createMemo(() => {
|
||||
const id = sessionID()
|
||||
if (!id) return idle
|
||||
return sync.data.session_status[id] ?? idle
|
||||
})
|
||||
const sessionStatus = createMemo(() => Optional.map(params.id, (id) => sync.data.session_status[id]) ?? idle)
|
||||
const working = createMemo(() => !!pending() || sessionStatus().type !== "idle")
|
||||
const tint = createMemo(() => messageAgentColor(sessionMessages(), sync.data.agent))
|
||||
|
||||
|
|
@ -300,11 +292,7 @@ export function MessageTimeline(props: {
|
|||
|
||||
return undefined
|
||||
})
|
||||
const info = createMemo(() => {
|
||||
const id = sessionID()
|
||||
if (!id) return
|
||||
return sync.session.get(id)
|
||||
})
|
||||
const info = createMemo(() => Optional.map(params.id, (id) => sync.session.get(id)))
|
||||
const titleValue = createMemo(() => info()?.title)
|
||||
const shareUrl = createMemo(() => info()?.share?.url)
|
||||
const shareEnabled = createMemo(() => sync.data.config.share !== "disabled")
|
||||
|
|
@ -338,7 +326,7 @@ export function MessageTimeline(props: {
|
|||
const [req, setReq] = createStore({ share: false, unshare: false })
|
||||
|
||||
const shareSession = () => {
|
||||
const id = sessionID()
|
||||
const id = params.id
|
||||
if (!id || req.share) return
|
||||
if (!shareEnabled()) return
|
||||
setReq("share", true)
|
||||
|
|
@ -353,7 +341,7 @@ export function MessageTimeline(props: {
|
|||
}
|
||||
|
||||
const unshareSession = () => {
|
||||
const id = sessionID()
|
||||
const id = params.id
|
||||
if (!id || req.unshare) return
|
||||
if (!shareEnabled()) return
|
||||
setReq("unshare", true)
|
||||
|
|
@ -399,7 +387,7 @@ export function MessageTimeline(props: {
|
|||
)
|
||||
|
||||
const openTitleEditor = () => {
|
||||
if (!sessionID()) return
|
||||
if (!params.id) return
|
||||
setTitle({ editing: true, draft: titleValue() ?? "" })
|
||||
requestAnimationFrame(() => {
|
||||
titleRef?.focus()
|
||||
|
|
@ -413,7 +401,7 @@ export function MessageTimeline(props: {
|
|||
}
|
||||
|
||||
const saveTitleEditor = async () => {
|
||||
const id = sessionID()
|
||||
const id = params.id
|
||||
if (!id) return
|
||||
if (title.saving) return
|
||||
|
||||
|
|
@ -444,17 +432,17 @@ export function MessageTimeline(props: {
|
|||
})
|
||||
}
|
||||
|
||||
const navigateAfterSessionRemoval = (sessionID: string, parentID?: string, nextSessionID?: string) => {
|
||||
if (params.id !== sessionID) return
|
||||
const navigateAfterSessionRemoval = (id: string, parentID?: string, nextSessionID?: string) => {
|
||||
if (params.id !== id) return
|
||||
if (parentID) {
|
||||
navigate(`/${params.dir}/session/${parentID}`)
|
||||
navigate(`../${parentID}`)
|
||||
return
|
||||
}
|
||||
if (nextSessionID) {
|
||||
navigate(`/${params.dir}/session/${nextSessionID}`)
|
||||
navigate(`../${nextSessionID}`)
|
||||
return
|
||||
}
|
||||
navigate(`/${params.dir}/session`)
|
||||
navigate("../")
|
||||
}
|
||||
|
||||
const archiveSession = async (sessionID: string) => {
|
||||
|
|
@ -547,7 +535,7 @@ export function MessageTimeline(props: {
|
|||
const navigateParent = () => {
|
||||
const id = parentID()
|
||||
if (!id) return
|
||||
navigate(`/${params.dir}/session/${id}`)
|
||||
navigate(`../${id}`)
|
||||
}
|
||||
|
||||
function DialogDeleteSession(props: { sessionID: string }) {
|
||||
|
|
@ -734,7 +722,7 @@ export function MessageTimeline(props: {
|
|||
</Show>
|
||||
</div>
|
||||
</div>
|
||||
<Show when={sessionID()}>
|
||||
<Show when={params.id}>
|
||||
{(id) => (
|
||||
<div class="shrink-0 flex items-center gap-3">
|
||||
<SessionContextUsage placement="bottom" />
|
||||
|
|
@ -999,7 +987,7 @@ export function MessageTimeline(props: {
|
|||
</div>
|
||||
</Show>
|
||||
<SessionTurn
|
||||
sessionID={sessionID() ?? ""}
|
||||
sessionID={params.id ?? ""}
|
||||
messageID={messageID}
|
||||
actions={props.actions}
|
||||
active={active()}
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import { createMemo } from "solid-js"
|
|||
import { useLayout } from "@/context/layout"
|
||||
|
||||
export const useSessionKey = () => {
|
||||
const params = useParams()
|
||||
const params = useParams<{ dir: string; id: string & { __brand: "SessionID" } }>()
|
||||
const sessionKey = createMemo(() => `${params.dir}${params.id ? "/" + params.id : ""}`)
|
||||
return { params, sessionKey }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,30 +1,31 @@
|
|||
import { For, Match, Show, Switch, createEffect, createMemo, onCleanup, type JSX } from "solid-js"
|
||||
import { createStore } from "solid-js/store"
|
||||
import { createMediaQuery } from "@solid-primitives/media"
|
||||
import { Tabs } from "@opencode-ai/ui/tabs"
|
||||
import { IconButton } from "@opencode-ai/ui/icon-button"
|
||||
import { TooltipKeybind } from "@opencode-ai/ui/tooltip"
|
||||
import { ResizeHandle } from "@opencode-ai/ui/resize-handle"
|
||||
import { Mark } from "@opencode-ai/ui/logo"
|
||||
import { DragDropProvider, DragDropSensors, DragOverlay, SortableProvider, closestCenter } from "@thisbeyond/solid-dnd"
|
||||
import type { DragEvent } from "@thisbeyond/solid-dnd"
|
||||
import { ConstrainDragYAxis, getDraggableId } from "@/utils/solid-dnd"
|
||||
import { useDialog } from "@opencode-ai/ui/context/dialog"
|
||||
import { IconButton } from "@opencode-ai/ui/icon-button"
|
||||
import { Mark } from "@opencode-ai/ui/logo"
|
||||
import { ResizeHandle } from "@opencode-ai/ui/resize-handle"
|
||||
import { Tabs } from "@opencode-ai/ui/tabs"
|
||||
import { TooltipKeybind } from "@opencode-ai/ui/tooltip"
|
||||
import { createMediaQuery } from "@solid-primitives/media"
|
||||
import type { DragEvent } from "@thisbeyond/solid-dnd"
|
||||
import { closestCenter, DragDropProvider, DragDropSensors, DragOverlay, SortableProvider } from "@thisbeyond/solid-dnd"
|
||||
import { createEffect, createMemo, For, type JSX, Match, onCleanup, Show, Switch } from "solid-js"
|
||||
import { createStore } from "solid-js/store"
|
||||
import { DialogSelectFile } from "@/components/dialog-select-file"
|
||||
|
||||
import FileTree from "@/components/file-tree"
|
||||
import { FileVisual, SessionContextTab, SortableTab } from "@/components/session"
|
||||
import { SessionContextUsage } from "@/components/session-context-usage"
|
||||
import { DialogSelectFile } from "@/components/dialog-select-file"
|
||||
import { SessionContextTab, SortableTab, FileVisual } from "@/components/session"
|
||||
import { useCommand } from "@/context/command"
|
||||
import { useFile, type SelectedLineRange } from "@/context/file"
|
||||
import { type SelectedLineRange, useFile } from "@/context/file"
|
||||
import { useLanguage } from "@/context/language"
|
||||
import { useLayout } from "@/context/layout"
|
||||
import { useSync } from "@/context/sync"
|
||||
import { createFileTabListSync } from "@/pages/session/file-tab-scroll"
|
||||
import { FileTabContent } from "@/pages/session/file-tabs"
|
||||
import { createOpenSessionFileTab, createSessionTabs, getTabReorderIndex, type Sizing } from "@/pages/session/helpers"
|
||||
import { setSessionHandoff } from "@/pages/session/handoff"
|
||||
import { createOpenSessionFileTab, createSessionTabs, getTabReorderIndex, type Sizing } from "@/pages/session/helpers"
|
||||
import { useSessionLayout } from "@/pages/session/session-layout"
|
||||
import { Optional } from "@/utils/optional"
|
||||
import { ConstrainDragYAxis, getDraggableId } from "@/utils/solid-dnd"
|
||||
|
||||
export function SessionSidePanel(props: {
|
||||
reviewPanel: () => JSX.Element
|
||||
|
|
@ -54,14 +55,13 @@ export function SessionSidePanel(props: {
|
|||
})
|
||||
const treeWidth = createMemo(() => (fileOpen() ? `${layout.fileTree.width()}px` : "0px"))
|
||||
|
||||
const info = createMemo(() => (params.id ? sync.session.get(params.id) : undefined))
|
||||
const diffs = createMemo(() => (params.id ? (sync.data.session_diff[params.id] ?? []) : []))
|
||||
const info = createMemo(() => Optional.map(params.id, (id) => sync.session.get(id)))
|
||||
const diffs = createMemo(() => Optional.map(params.id, (id) => sync.data.session_diff[id]) ?? [])
|
||||
const reviewCount = createMemo(() => Math.max(info()?.summary?.files ?? 0, diffs().length))
|
||||
const hasReview = createMemo(() => reviewCount() > 0)
|
||||
const diffsReady = createMemo(() => {
|
||||
const id = params.id
|
||||
if (!id) return true
|
||||
if (!hasReview()) return true
|
||||
if (!id || !hasReview()) return true
|
||||
return sync.data.session_diff[id] !== undefined
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -1,8 +1,15 @@
|
|||
import { useNavigate } from "@solidjs/router"
|
||||
import { useCommand, type CommandOption } from "@/context/command"
|
||||
import type { UserMessage } from "@opencode-ai/sdk/v2"
|
||||
import { useDialog } from "@opencode-ai/ui/context/dialog"
|
||||
import { previewSelectedLines } from "@opencode-ai/ui/pierre/selection-bridge"
|
||||
import { useFile, selectionFromLines, type FileSelection, type SelectedLineRange } from "@/context/file"
|
||||
import { showToast } from "@opencode-ai/ui/toast"
|
||||
import { findLast } from "@opencode-ai/util/array"
|
||||
import { useNavigate } from "@solidjs/router"
|
||||
import { DialogFork } from "@/components/dialog-fork"
|
||||
import { DialogSelectFile } from "@/components/dialog-select-file"
|
||||
import { DialogSelectMcp } from "@/components/dialog-select-mcp"
|
||||
import { DialogSelectModel } from "@/components/dialog-select-model"
|
||||
import { type CommandOption, useCommand } from "@/context/command"
|
||||
import { type FileSelection, type SelectedLineRange, selectionFromLines, useFile } from "@/context/file"
|
||||
import { useLanguage } from "@/context/language"
|
||||
import { useLayout } from "@/context/layout"
|
||||
import { useLocal } from "@/context/local"
|
||||
|
|
@ -11,16 +18,10 @@ import { usePrompt } from "@/context/prompt"
|
|||
import { useSDK } from "@/context/sdk"
|
||||
import { useSync } from "@/context/sync"
|
||||
import { useTerminal } from "@/context/terminal"
|
||||
import { DialogSelectFile } from "@/components/dialog-select-file"
|
||||
import { DialogSelectModel } from "@/components/dialog-select-model"
|
||||
import { DialogSelectMcp } from "@/components/dialog-select-mcp"
|
||||
import { DialogFork } from "@/components/dialog-fork"
|
||||
import { showToast } from "@opencode-ai/ui/toast"
|
||||
import { findLast } from "@opencode-ai/util/array"
|
||||
import { createSessionTabs } from "@/pages/session/helpers"
|
||||
import { extractPromptFromParts } from "@/utils/prompt"
|
||||
import { UserMessage } from "@opencode-ai/sdk/v2"
|
||||
import { useSessionLayout } from "@/pages/session/session-layout"
|
||||
import { Optional } from "@/utils/optional"
|
||||
import { extractPromptFromParts } from "@/utils/prompt"
|
||||
|
||||
export type SessionCommandContext = {
|
||||
navigateMessageByOffset: (offset: number) => void
|
||||
|
|
@ -51,11 +52,8 @@ export const useSessionCommands = (actions: SessionCommandContext) => {
|
|||
const navigate = useNavigate()
|
||||
const { params, tabs, view } = useSessionLayout()
|
||||
|
||||
const info = () => {
|
||||
const id = params.id
|
||||
if (!id) return
|
||||
return sync.session.get(id)
|
||||
}
|
||||
const info = () => Optional.map(params.id, (id) => sync.session.get(id))
|
||||
|
||||
const hasReview = () => {
|
||||
const id = params.id
|
||||
if (!id) return false
|
||||
|
|
@ -76,12 +74,9 @@ export const useSessionCommands = (actions: SessionCommandContext) => {
|
|||
const closableTab = tabState.closableTab
|
||||
|
||||
const idle = { type: "idle" as const }
|
||||
const status = () => sync.data.session_status[params.id ?? ""] ?? idle
|
||||
const messages = () => {
|
||||
const id = params.id
|
||||
if (!id) return []
|
||||
return sync.data.message[id] ?? []
|
||||
}
|
||||
const status = () => Optional.map(params.id, (id) => sync.data.session_status[id]) ?? idle
|
||||
const messages = () => Optional.map(params.id, (id) => sync.data.message[id]) ?? []
|
||||
|
||||
const userMessages = () => messages().filter((m) => m.role === "user") as UserMessage[]
|
||||
const visibleUserMessages = () => {
|
||||
const revert = info()?.revert?.messageID
|
||||
|
|
@ -97,7 +92,10 @@ export const useSessionCommands = (actions: SessionCommandContext) => {
|
|||
const selectionPreview = (path: string, selection: FileSelection) => {
|
||||
const content = file.get(path)?.content?.content
|
||||
if (!content) return undefined
|
||||
return previewSelectedLines(content, { start: selection.startLine, end: selection.endLine })
|
||||
return previewSelectedLines(content, {
|
||||
start: selection.startLine,
|
||||
end: selection.endLine,
|
||||
})
|
||||
}
|
||||
|
||||
const addSelectionToContext = (path: string, selection: FileSelection) => {
|
||||
|
|
@ -128,8 +126,8 @@ export const useSessionCommands = (actions: SessionCommandContext) => {
|
|||
const permissionsCommand = withCategory(language.t("command.category.permissions"))
|
||||
|
||||
const isAutoAcceptActive = () => {
|
||||
const sessionID = params.id
|
||||
if (sessionID) return permission.isAutoAccepting(sessionID, sdk.directory)
|
||||
const id = params.id
|
||||
if (id) return permission.isAutoAccepting(id, sdk.directory)
|
||||
return permission.isAutoAcceptingDirectory(sdk.directory)
|
||||
}
|
||||
command.register("session", () => {
|
||||
|
|
@ -148,7 +146,8 @@ export const useSessionCommands = (actions: SessionCommandContext) => {
|
|||
slash: "share",
|
||||
disabled: !params.id,
|
||||
onSelect: async () => {
|
||||
if (!params.id) return
|
||||
const id = params.id
|
||||
if (!id) return
|
||||
|
||||
const write = (value: string) => {
|
||||
const body = typeof document === "undefined" ? undefined : document.body
|
||||
|
|
@ -200,7 +199,7 @@ export const useSessionCommands = (actions: SessionCommandContext) => {
|
|||
}
|
||||
|
||||
const url = await sdk.client.session
|
||||
.share({ sessionID: params.id })
|
||||
.share({ sessionID: id })
|
||||
.then((res) => res.data?.share?.url)
|
||||
.catch(() => undefined)
|
||||
if (!url) {
|
||||
|
|
@ -222,9 +221,10 @@ export const useSessionCommands = (actions: SessionCommandContext) => {
|
|||
slash: "unshare",
|
||||
disabled: !params.id || !info()?.share?.url,
|
||||
onSelect: async () => {
|
||||
if (!params.id) return
|
||||
const id = params.id
|
||||
if (!id) return
|
||||
await sdk.client.session
|
||||
.unshare({ sessionID: params.id })
|
||||
.unshare({ sessionID: id })
|
||||
.then(() =>
|
||||
showToast({
|
||||
title: language.t("toast.session.unshare.success.title"),
|
||||
|
|
@ -391,12 +391,12 @@ export const useSessionCommands = (actions: SessionCommandContext) => {
|
|||
keybind: "mod+shift+a",
|
||||
disabled: false,
|
||||
onSelect: () => {
|
||||
const sessionID = params.id
|
||||
if (sessionID) permission.toggleAutoAccept(sessionID, sdk.directory)
|
||||
const id = params.id
|
||||
if (id) permission.toggleAutoAccept(id, sdk.directory)
|
||||
else permission.toggleAutoAcceptDirectory(sdk.directory)
|
||||
|
||||
const active = sessionID
|
||||
? permission.isAutoAccepting(sessionID, sdk.directory)
|
||||
const active = id
|
||||
? permission.isAutoAccepting(id, sdk.directory)
|
||||
: permission.isAutoAcceptingDirectory(sdk.directory)
|
||||
showToast({
|
||||
title: active
|
||||
|
|
@ -415,18 +415,23 @@ export const useSessionCommands = (actions: SessionCommandContext) => {
|
|||
slash: "undo",
|
||||
disabled: !params.id || visibleUserMessages().length === 0,
|
||||
onSelect: async () => {
|
||||
const sessionID = params.id
|
||||
if (!sessionID) return
|
||||
const id = params.id
|
||||
if (!id) return
|
||||
if (status().type !== "idle") {
|
||||
await sdk.client.session.abort({ sessionID }).catch(() => {})
|
||||
await sdk.client.session.abort({ sessionID: id }).catch(() => {})
|
||||
}
|
||||
const revert = info()?.revert?.messageID
|
||||
const message = findLast(userMessages(), (x) => !revert || x.id < revert)
|
||||
if (!message) return
|
||||
await sdk.client.session.revert({ sessionID, messageID: message.id })
|
||||
await sdk.client.session.revert({
|
||||
sessionID: id,
|
||||
messageID: message.id,
|
||||
})
|
||||
const parts = sync.data.part[message.id]
|
||||
if (parts) {
|
||||
const restored = extractPromptFromParts(parts, { directory: sdk.directory })
|
||||
const restored = extractPromptFromParts(parts, {
|
||||
directory: sdk.directory,
|
||||
})
|
||||
prompt.set(restored)
|
||||
}
|
||||
const priorMessage = findLast(userMessages(), (x) => x.id < message.id)
|
||||
|
|
@ -440,19 +445,22 @@ export const useSessionCommands = (actions: SessionCommandContext) => {
|
|||
slash: "redo",
|
||||
disabled: !params.id || !info()?.revert?.messageID,
|
||||
onSelect: async () => {
|
||||
const sessionID = params.id
|
||||
if (!sessionID) return
|
||||
const id = params.id
|
||||
if (!id) return
|
||||
const revertMessageID = info()?.revert?.messageID
|
||||
if (!revertMessageID) return
|
||||
const nextMessage = userMessages().find((x) => x.id > revertMessageID)
|
||||
if (!nextMessage) {
|
||||
await sdk.client.session.unrevert({ sessionID })
|
||||
await sdk.client.session.unrevert({ sessionID: id })
|
||||
prompt.reset()
|
||||
const lastMsg = findLast(userMessages(), (x) => x.id >= revertMessageID)
|
||||
setActiveMessage(lastMsg)
|
||||
return
|
||||
}
|
||||
await sdk.client.session.revert({ sessionID, messageID: nextMessage.id })
|
||||
await sdk.client.session.revert({
|
||||
sessionID: id,
|
||||
messageID: nextMessage.id,
|
||||
})
|
||||
const priorMsg = findLast(userMessages(), (x) => x.id < nextMessage.id)
|
||||
setActiveMessage(priorMsg)
|
||||
},
|
||||
|
|
@ -464,8 +472,8 @@ export const useSessionCommands = (actions: SessionCommandContext) => {
|
|||
slash: "compact",
|
||||
disabled: !params.id || visibleUserMessages().length === 0,
|
||||
onSelect: async () => {
|
||||
const sessionID = params.id
|
||||
if (!sessionID) return
|
||||
const id = params.id
|
||||
if (!id) return
|
||||
const model = local.model.current()
|
||||
if (!model) {
|
||||
showToast({
|
||||
|
|
@ -475,7 +483,7 @@ export const useSessionCommands = (actions: SessionCommandContext) => {
|
|||
return
|
||||
}
|
||||
await sdk.client.session.summarize({
|
||||
sessionID,
|
||||
sessionID: id,
|
||||
modelID: model.id,
|
||||
providerID: model.provider.id,
|
||||
})
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import type { UserMessage } from "@opencode-ai/sdk/v2"
|
|||
import { useLocation, useNavigate } from "@solidjs/router"
|
||||
import { createEffect, createMemo, onCleanup, onMount } from "solid-js"
|
||||
import { messageIdFromHash } from "./message-id-from-hash"
|
||||
import { useSessionLayout } from "./session-layout"
|
||||
|
||||
export const useSessionHashScroll = (input: {
|
||||
sessionKey: () => string
|
||||
|
|
@ -28,6 +29,7 @@ export const useSessionHashScroll = (input: {
|
|||
|
||||
const location = useLocation()
|
||||
const navigate = useNavigate()
|
||||
const { params } = useSessionLayout()
|
||||
|
||||
const frames = new Set<number>()
|
||||
const queue = (fn: () => void) => {
|
||||
|
|
@ -142,13 +144,13 @@ export const useSessionHashScroll = (input: {
|
|||
createEffect(() => {
|
||||
const hash = location.hash
|
||||
if (!hash) clearing = false
|
||||
if (!input.sessionID() || !input.messagesReady()) return
|
||||
if (!params.id || !input.messagesReady()) return
|
||||
cancel()
|
||||
queue(() => applyHash("auto"))
|
||||
})
|
||||
|
||||
createEffect(() => {
|
||||
if (!input.sessionID() || !input.messagesReady()) return
|
||||
if (!params.id || !input.messagesReady()) return
|
||||
|
||||
visibleUserMessages()
|
||||
input.turnStart()
|
||||
|
|
|
|||
|
|
@ -0,0 +1,11 @@
|
|||
import { dual } from "effect/Function"
|
||||
|
||||
export namespace Optional {
|
||||
export const map = dual<
|
||||
<I, O>(f: (value: I) => O) => (opt: I | undefined) => O | undefined,
|
||||
<I, O>(opt: I | undefined, f: (value: I) => O) => O | undefined
|
||||
>(2, (opt, f) => {
|
||||
if (opt === undefined) return undefined
|
||||
return f(opt)
|
||||
})
|
||||
}
|
||||
|
|
@ -1,9 +1,9 @@
|
|||
import { Hono } from "hono"
|
||||
import { DurableObject } from "cloudflare:workers"
|
||||
import { randomUUID } from "node:crypto"
|
||||
import { jwtVerify, createRemoteJWKSet } from "jose"
|
||||
import { createAppAuth } from "@octokit/auth-app"
|
||||
import { Octokit } from "@octokit/rest"
|
||||
import { Hono } from "hono"
|
||||
import { createRemoteJWKSet, jwtVerify } from "jose"
|
||||
import { Resource } from "sst"
|
||||
|
||||
type Env = {
|
||||
|
|
|
|||
|
|
@ -1,15 +1,15 @@
|
|||
import { useDialog } from "@tui/ui/dialog"
|
||||
import { DialogSelect } from "@tui/ui/dialog-select"
|
||||
import { useRoute } from "@tui/context/route"
|
||||
import { useSync } from "@tui/context/sync"
|
||||
import { createMemo, createSignal, createResource, onMount, Show } from "solid-js"
|
||||
import { useDialog } from "@tui/ui/dialog"
|
||||
import { DialogSelect } from "@tui/ui/dialog-select"
|
||||
import { createMemo, createResource, createSignal, onMount } from "solid-js"
|
||||
import { Locale } from "@/util/locale"
|
||||
import { useKeybind } from "../context/keybind"
|
||||
import { useTheme } from "../context/theme"
|
||||
import { useSDK } from "../context/sdk"
|
||||
import { DialogSessionRename } from "./dialog-session-rename"
|
||||
import { useKV } from "../context/kv"
|
||||
import { useSDK } from "../context/sdk"
|
||||
import { useTheme } from "../context/theme"
|
||||
import { createDebouncedSignal } from "../util/signal"
|
||||
import { DialogSessionRename } from "./dialog-session-rename"
|
||||
import { Spinner } from "./spinner"
|
||||
|
||||
export function DialogSessionList() {
|
||||
|
|
|
|||
|
|
@ -1,17 +1,17 @@
|
|||
import { useDialog } from "@tui/ui/dialog"
|
||||
import { DialogSelect } from "@tui/ui/dialog-select"
|
||||
import { useRoute } from "@tui/context/route"
|
||||
import { useSync } from "@tui/context/sync"
|
||||
import { createMemo, createSignal, createResource, onMount, Show } from "solid-js"
|
||||
import { useDialog } from "@tui/ui/dialog"
|
||||
import { DialogSelect } from "@tui/ui/dialog-select"
|
||||
import { createMemo, createResource, createSignal, onMount } from "solid-js"
|
||||
import { Locale } from "@/util/locale"
|
||||
import { useKeybind } from "../../context/keybind"
|
||||
import { useTheme } from "../../context/theme"
|
||||
import { useSDK } from "../../context/sdk"
|
||||
import { DialogSessionRename } from "../dialog-session-rename"
|
||||
import { useKV } from "../../context/kv"
|
||||
import { createDebouncedSignal } from "../../util/signal"
|
||||
import { Spinner } from "../spinner"
|
||||
import { useSDK } from "../../context/sdk"
|
||||
import { useTheme } from "../../context/theme"
|
||||
import { useToast } from "../../ui/toast"
|
||||
import { createDebouncedSignal } from "../../util/signal"
|
||||
import { DialogSessionRename } from "../dialog-session-rename"
|
||||
import { Spinner } from "../spinner"
|
||||
|
||||
export function DialogSessionList(props: { workspaceID?: string; localOnly?: boolean } = {}) {
|
||||
const dialog = useDialog()
|
||||
|
|
|
|||
|
|
@ -1,60 +1,59 @@
|
|||
import {
|
||||
Component,
|
||||
createEffect,
|
||||
createMemo,
|
||||
createSignal,
|
||||
For,
|
||||
Match,
|
||||
onMount,
|
||||
Show,
|
||||
Switch,
|
||||
onCleanup,
|
||||
Index,
|
||||
type JSX,
|
||||
} from "solid-js"
|
||||
import { createStore } from "solid-js/store"
|
||||
import stripAnsi from "strip-ansi"
|
||||
import { Dynamic } from "solid-js/web"
|
||||
import {
|
||||
import type {
|
||||
AgentPart,
|
||||
AssistantMessage,
|
||||
FilePart,
|
||||
Message as MessageType,
|
||||
Part as PartType,
|
||||
ReasoningPart,
|
||||
TextPart,
|
||||
ToolPart,
|
||||
UserMessage,
|
||||
Todo,
|
||||
QuestionAnswer,
|
||||
QuestionInfo,
|
||||
ReasoningPart,
|
||||
TextPart,
|
||||
Todo,
|
||||
ToolPart,
|
||||
UserMessage,
|
||||
} from "@opencode-ai/sdk/v2"
|
||||
import { checksum } from "@opencode-ai/util/encode"
|
||||
import { getDirectory as _getDirectory, getFilename } from "@opencode-ai/util/path"
|
||||
import { useLocation } from "@solidjs/router"
|
||||
import { animate } from "motion"
|
||||
import {
|
||||
type Component,
|
||||
createEffect,
|
||||
createMemo,
|
||||
createSignal,
|
||||
For,
|
||||
Index,
|
||||
type JSX,
|
||||
Match,
|
||||
onCleanup,
|
||||
onMount,
|
||||
Show,
|
||||
Switch,
|
||||
} from "solid-js"
|
||||
import { createStore } from "solid-js/store"
|
||||
import { Dynamic } from "solid-js/web"
|
||||
import stripAnsi from "strip-ansi"
|
||||
import { useData } from "../context"
|
||||
import { useFileComponent } from "../context/file"
|
||||
import { useDialog } from "../context/dialog"
|
||||
import { useFileComponent } from "../context/file"
|
||||
import { type UiI18n, useI18n } from "../context/i18n"
|
||||
import { BasicTool, GenericTool } from "./basic-tool"
|
||||
import { Accordion } from "./accordion"
|
||||
import { StickyAccordionHeader } from "./sticky-accordion-header"
|
||||
import { Card } from "./card"
|
||||
import { BasicTool, GenericTool } from "./basic-tool"
|
||||
import { Checkbox } from "./checkbox"
|
||||
import { Collapsible } from "./collapsible"
|
||||
import { DiffChanges } from "./diff-changes"
|
||||
import { FileIcon } from "./file-icon"
|
||||
import { Icon } from "./icon"
|
||||
import { ToolErrorCard } from "./tool-error-card"
|
||||
import { Checkbox } from "./checkbox"
|
||||
import { DiffChanges } from "./diff-changes"
|
||||
import { Markdown } from "./markdown"
|
||||
import { ImagePreview } from "./image-preview"
|
||||
import { getDirectory as _getDirectory, getFilename } from "@opencode-ai/util/path"
|
||||
import { checksum } from "@opencode-ai/util/encode"
|
||||
import { Tooltip } from "./tooltip"
|
||||
import { IconButton } from "./icon-button"
|
||||
import { ImagePreview } from "./image-preview"
|
||||
import { Markdown } from "./markdown"
|
||||
import { attached, inline, kind } from "./message-file"
|
||||
import { StickyAccordionHeader } from "./sticky-accordion-header"
|
||||
import { TextShimmer } from "./text-shimmer"
|
||||
import { AnimatedCountList } from "./tool-count-summary"
|
||||
import { ToolErrorCard } from "./tool-error-card"
|
||||
import { ToolStatusTitle } from "./tool-status-title"
|
||||
import { animate } from "motion"
|
||||
import { useLocation } from "@solidjs/router"
|
||||
import { attached, inline, kind } from "./message-file"
|
||||
import { Tooltip } from "./tooltip"
|
||||
|
||||
function ShellSubmessage(props: { text: string; animate?: boolean }) {
|
||||
let widthRef: HTMLSpanElement | undefined
|
||||
|
|
@ -1087,10 +1086,18 @@ function HighlightedText(props: { text: string; references: FilePart[]; agents:
|
|||
const allRefs: { start: number; end: number; type: "file" | "agent" }[] = [
|
||||
...props.references
|
||||
.filter((r) => r.source?.text?.start !== undefined && r.source?.text?.end !== undefined)
|
||||
.map((r) => ({ start: r.source!.text!.start, end: r.source!.text!.end, type: "file" as const })),
|
||||
.map((r) => ({
|
||||
start: r.source!.text!.start,
|
||||
end: r.source!.text!.end,
|
||||
type: "file" as const,
|
||||
})),
|
||||
...props.agents
|
||||
.filter((a) => a.source?.start !== undefined && a.source?.end !== undefined)
|
||||
.map((a) => ({ start: a.source!.start, end: a.source!.end, type: "agent" as const })),
|
||||
.map((a) => ({
|
||||
start: a.source!.start,
|
||||
end: a.source!.end,
|
||||
type: "agent" as const,
|
||||
})),
|
||||
].sort((a, b) => a.start - b.start)
|
||||
|
||||
const result: HighlightSegment[] = []
|
||||
|
|
@ -1334,7 +1341,10 @@ PART_MAPPING["text"] = function TextPartDisplay(props) {
|
|||
: -1
|
||||
if (!(ms >= 0)) return ""
|
||||
const total = Math.round(ms / 1000)
|
||||
if (total < 60) return i18n.t("ui.message.duration.seconds", { count: numfmt().format(total) })
|
||||
if (total < 60)
|
||||
return i18n.t("ui.message.duration.seconds", {
|
||||
count: numfmt().format(total),
|
||||
})
|
||||
const minutes = Math.floor(total / 60)
|
||||
const seconds = total % 60
|
||||
return i18n.t("ui.message.duration.minutesSeconds", {
|
||||
|
|
@ -1475,7 +1485,10 @@ ToolRegistry.register({
|
|||
<BasicTool
|
||||
{...props}
|
||||
icon="bullet-list"
|
||||
trigger={{ title: i18n.t("ui.tool.list"), subtitle: getDirectory(props.input.path || "/") }}
|
||||
trigger={{
|
||||
title: i18n.t("ui.tool.list"),
|
||||
subtitle: getDirectory(props.input.path || "/"),
|
||||
}}
|
||||
>
|
||||
<Show when={props.output}>
|
||||
<div data-component="tool-output" data-scrollable>
|
||||
|
|
@ -1974,7 +1987,12 @@ ToolRegistry.register({
|
|||
<Accordion.Trigger>
|
||||
<div data-slot="apply-patch-trigger-content">
|
||||
<div data-slot="apply-patch-file-info">
|
||||
<FileIcon node={{ path: file.relativePath, type: "file" }} />
|
||||
<FileIcon
|
||||
node={{
|
||||
path: file.relativePath,
|
||||
type: "file",
|
||||
}}
|
||||
/>
|
||||
<div data-slot="apply-patch-file-name-container">
|
||||
<Show when={file.relativePath.includes("/")}>
|
||||
<span data-slot="apply-patch-directory">{`\u202A${getDirectory(file.relativePath)}\u202C`}</span>
|
||||
|
|
@ -2000,7 +2018,12 @@ ToolRegistry.register({
|
|||
</span>
|
||||
</Match>
|
||||
<Match when={true}>
|
||||
<DiffChanges changes={{ additions: file.additions, deletions: file.deletions }} />
|
||||
<DiffChanges
|
||||
changes={{
|
||||
additions: file.additions,
|
||||
deletions: file.deletions,
|
||||
}}
|
||||
/>
|
||||
</Match>
|
||||
</Switch>
|
||||
<Icon name="chevron-grabber-vertical" size="small" />
|
||||
|
|
@ -2014,8 +2037,14 @@ ToolRegistry.register({
|
|||
<Dynamic
|
||||
component={fileComponent}
|
||||
mode="diff"
|
||||
before={{ name: file.filePath, contents: file.before }}
|
||||
after={{ name: file.movePath ?? file.filePath, contents: file.after }}
|
||||
before={{
|
||||
name: file.filePath,
|
||||
contents: file.before,
|
||||
}}
|
||||
after={{
|
||||
name: file.movePath ?? file.filePath,
|
||||
contents: file.after,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</Show>
|
||||
|
|
@ -2054,7 +2083,12 @@ ToolRegistry.register({
|
|||
</div>
|
||||
<div data-slot="message-part-actions">
|
||||
<Show when={!pending()}>
|
||||
<DiffChanges changes={{ additions: single()!.additions, deletions: single()!.deletions }} />
|
||||
<DiffChanges
|
||||
changes={{
|
||||
additions: single()!.additions,
|
||||
deletions: single()!.deletions,
|
||||
}}
|
||||
/>
|
||||
</Show>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -2080,7 +2114,12 @@ ToolRegistry.register({
|
|||
</span>
|
||||
</Match>
|
||||
<Match when={true}>
|
||||
<DiffChanges changes={{ additions: single()!.additions, deletions: single()!.deletions }} />
|
||||
<DiffChanges
|
||||
changes={{
|
||||
additions: single()!.additions,
|
||||
deletions: single()!.deletions,
|
||||
}}
|
||||
/>
|
||||
</Match>
|
||||
</Switch>
|
||||
}
|
||||
|
|
@ -2089,8 +2128,14 @@ ToolRegistry.register({
|
|||
<Dynamic
|
||||
component={fileComponent}
|
||||
mode="diff"
|
||||
before={{ name: single()!.filePath, contents: single()!.before }}
|
||||
after={{ name: single()!.movePath ?? single()!.filePath, contents: single()!.after }}
|
||||
before={{
|
||||
name: single()!.filePath,
|
||||
contents: single()!.before,
|
||||
}}
|
||||
after={{
|
||||
name: single()!.movePath ?? single()!.filePath,
|
||||
contents: single()!.after,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</ToolFileAccordion>
|
||||
|
|
|
|||
Loading…
Reference in New Issue