Compare commits
3 Commits
dev
...
brendan/be
| Author | SHA1 | Date |
|---|---|---|
|
|
56c5cc86c2 | |
|
|
32d542d1c6 | |
|
|
cf3df010ce |
|
|
@ -8,10 +8,11 @@ import { Font } from "@opencode-ai/ui/font"
|
||||||
import { Splash } from "@opencode-ai/ui/logo"
|
import { Splash } from "@opencode-ai/ui/logo"
|
||||||
import { ThemeProvider } from "@opencode-ai/ui/theme"
|
import { ThemeProvider } from "@opencode-ai/ui/theme"
|
||||||
import { MetaProvider } from "@solidjs/meta"
|
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 Duration, Effect } from "effect"
|
||||||
import {
|
import {
|
||||||
type Component,
|
type Component,
|
||||||
|
createEffect,
|
||||||
createMemo,
|
createMemo,
|
||||||
createResource,
|
createResource,
|
||||||
createSignal,
|
createSignal,
|
||||||
|
|
@ -114,6 +115,10 @@ function SessionProviders(props: ParentProps) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function RouterRoot(props: ParentProps<{ appChildren?: JSX.Element }>) {
|
function RouterRoot(props: ParentProps<{ appChildren?: JSX.Element }>) {
|
||||||
|
const l = useLocation()
|
||||||
|
createEffect(() => {
|
||||||
|
console.log("pathname", l.pathname)
|
||||||
|
})
|
||||||
return (
|
return (
|
||||||
<AppShellProviders>
|
<AppShellProviders>
|
||||||
<Suspense fallback={<Loading />}>
|
<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 { 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 { 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 { ModelSelectorPopover } from "@/components/dialog-select-model"
|
||||||
import { DialogSelectModelUnpaid } from "@/components/dialog-select-model-unpaid"
|
import { DialogSelectModelUnpaid } from "@/components/dialog-select-model-unpaid"
|
||||||
import { useProviders } from "@/hooks/use-providers"
|
|
||||||
import { useCommand } from "@/context/command"
|
import { useCommand } from "@/context/command"
|
||||||
import { Persist, persisted } from "@/utils/persist"
|
import { useComments } from "@/context/comments"
|
||||||
import { usePermission } from "@/context/permission"
|
import { type SelectedLineRange, selectionFromLines, useFile } from "@/context/file"
|
||||||
import { useLanguage } from "@/context/language"
|
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 { 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 { createSessionTabs } from "@/pages/session/helpers"
|
||||||
|
import { useSessionLayout } from "@/pages/session/session-layout"
|
||||||
import { promptEnabled, promptProbe } from "@/testing/prompt"
|
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 { 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 { ACCEPTED_FILE_TYPES } from "./prompt-input/files"
|
||||||
import {
|
import {
|
||||||
canNavigateHistoryAtCursor,
|
canNavigateHistoryAtCursor,
|
||||||
navigatePromptHistory,
|
navigatePromptHistory,
|
||||||
prependHistoryEntry,
|
|
||||||
type PromptHistoryComment,
|
type PromptHistoryComment,
|
||||||
type PromptHistoryEntry,
|
type PromptHistoryEntry,
|
||||||
type PromptHistoryStoredEntry,
|
type PromptHistoryStoredEntry,
|
||||||
|
prependHistoryEntry,
|
||||||
promptLength,
|
promptLength,
|
||||||
} from "./prompt-input/history"
|
} 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 { PromptImageAttachments } from "./prompt-input/image-attachments"
|
||||||
import { PromptDragOverlay } from "./prompt-input/drag-overlay"
|
|
||||||
import { promptPlaceholder } from "./prompt-input/placeholder"
|
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 {
|
interface PromptInputProps {
|
||||||
class?: string
|
class?: string
|
||||||
|
|
@ -171,10 +172,10 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
||||||
}).activeFileTab
|
}).activeFileTab
|
||||||
|
|
||||||
const commentInReview = (path: string) => {
|
const commentInReview = (path: string) => {
|
||||||
const sessionID = params.id
|
const id = params.id
|
||||||
if (!sessionID) return false
|
if (!id) return false
|
||||||
|
|
||||||
const diffs = sync.data.session_diff[sessionID]
|
const diffs = sync.data.session_diff[id]
|
||||||
if (!diffs) return false
|
if (!diffs) return false
|
||||||
return diffs.some((diff) => diff.file === path)
|
return diffs.some((diff) => diff.file === path)
|
||||||
}
|
}
|
||||||
|
|
@ -236,10 +237,10 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
||||||
|
|
||||||
return paths
|
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(
|
const status = createMemo(
|
||||||
() =>
|
() =>
|
||||||
sync.data.session_status[params.id ?? ""] ?? {
|
Optional.map(params.id, (id) => sync.data.session_status[id]) ?? {
|
||||||
type: "idle",
|
type: "idle",
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
@ -283,7 +284,10 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
||||||
applyingHistory: false,
|
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) => ({
|
const motion = (value: number) => ({
|
||||||
opacity: value,
|
opacity: value,
|
||||||
transform: `scale(${0.95 + value * 0.05})`,
|
transform: `scale(${0.95 + value * 0.05})`,
|
||||||
|
|
@ -510,9 +514,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
params.id
|
if (params.id || !suggest()) return
|
||||||
if (params.id) return
|
|
||||||
if (!suggest()) return
|
|
||||||
const interval = setInterval(() => {
|
const interval = setInterval(() => {
|
||||||
setStore("placeholder", (prev) => (prev + 1) % EXAMPLES.length)
|
setStore("placeholder", (prev) => (prev + 1) % EXAMPLES.length)
|
||||||
}, 6500)
|
}, 6500)
|
||||||
|
|
@ -542,16 +544,34 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
||||||
const agentList = createMemo(() =>
|
const agentList = createMemo(() =>
|
||||||
sync.data.agent
|
sync.data.agent
|
||||||
.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 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
|
||||||
if (option.type === "agent") {
|
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 {
|
} 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 agents = agentList()
|
||||||
const open = recent()
|
const open = recent()
|
||||||
const seen = new Set(open)
|
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 paths = await files.searchFilesAndDirectories(query)
|
||||||
const fileOptions: AtOption[] = paths
|
const fileOptions: AtOption[] = paths
|
||||||
.filter((path) => !seen.has(path))
|
.filter((path) => !seen.has(path))
|
||||||
|
|
@ -780,7 +805,12 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
||||||
if (content.includes("\u200B")) content = content.replace(/\u200B/g, "")
|
if (content.includes("\u200B")) content = content.replace(/\u200B/g, "")
|
||||||
buffer = ""
|
buffer = ""
|
||||||
if (!content) return
|
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
|
position += content.length
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1057,20 +1087,16 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
||||||
|
|
||||||
const variants = createMemo(() => ["default", ...local.model.variant.list()])
|
const variants = createMemo(() => ["default", ...local.model.variant.list()])
|
||||||
const accepting = createMemo(() => {
|
const accepting = createMemo(() => {
|
||||||
const id = params.id
|
if (!params.id) return permission.isAutoAcceptingDirectory(sdk.directory)
|
||||||
if (!id) return permission.isAutoAcceptingDirectory(sdk.directory)
|
return permission.isAutoAccepting(params.id, sdk.directory)
|
||||||
return permission.isAutoAccepting(id, sdk.directory)
|
|
||||||
})
|
})
|
||||||
const acceptLabel = createMemo(() =>
|
const acceptLabel = createMemo(() =>
|
||||||
language.t(accepting() ? "command.permissions.autoaccept.disable" : "command.permissions.autoaccept.enable"),
|
language.t(accepting() ? "command.permissions.autoaccept.disable" : "command.permissions.autoaccept.enable"),
|
||||||
)
|
)
|
||||||
const toggleAccept = () => {
|
const toggleAccept = () => {
|
||||||
if (!params.id) {
|
const id = params.id
|
||||||
permission.toggleAutoAcceptDirectory(sdk.directory)
|
if (!id) permission.toggleAutoAcceptDirectory(sdk.directory)
|
||||||
return
|
else permission.toggleAutoAccept(id, sdk.directory)
|
||||||
}
|
|
||||||
|
|
||||||
permission.toggleAutoAccept(params.id, sdk.directory)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const { abort, handleSubmit } = createPromptSubmit({
|
const { abort, handleSubmit } = createPromptSubmit({
|
||||||
|
|
@ -1503,7 +1529,10 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
||||||
<ProviderIcon
|
<ProviderIcon
|
||||||
id={local.model.current()!.provider.id}
|
id={local.model.current()!.provider.id}
|
||||||
class="size-4 shrink-0 opacity-40 group-hover:opacity-100 transition-opacity duration-150"
|
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>
|
</Show>
|
||||||
<span class="truncate">
|
<span class="truncate">
|
||||||
|
|
@ -1535,7 +1564,10 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
||||||
<ProviderIcon
|
<ProviderIcon
|
||||||
id={local.model.current()!.provider.id}
|
id={local.model.current()!.provider.id}
|
||||||
class="size-4 shrink-0 opacity-40 group-hover:opacity-100 transition-opacity duration-150"
|
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>
|
</Show>
|
||||||
<span class="truncate">
|
<span class="truncate">
|
||||||
|
|
|
||||||
|
|
@ -1,21 +1,22 @@
|
||||||
import { createMemo, createEffect, on, onCleanup, For, Show } from "solid-js"
|
import type { Message, Part, UserMessage } from "@opencode-ai/sdk/v2/client"
|
||||||
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 { Accordion } from "@opencode-ai/ui/accordion"
|
import { Accordion } from "@opencode-ai/ui/accordion"
|
||||||
import { StickyAccordionHeader } from "@opencode-ai/ui/sticky-accordion-header"
|
|
||||||
import { File } from "@opencode-ai/ui/file"
|
import { File } from "@opencode-ai/ui/file"
|
||||||
|
import { Icon } from "@opencode-ai/ui/icon"
|
||||||
import { Markdown } from "@opencode-ai/ui/markdown"
|
import { Markdown } from "@opencode-ai/ui/markdown"
|
||||||
import { ScrollView } from "@opencode-ai/ui/scroll-view"
|
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 { useLanguage } from "@/context/language"
|
||||||
|
import { useSync } from "@/context/sync"
|
||||||
import { useSessionLayout } from "@/pages/session/session-layout"
|
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 { estimateSessionContextBreakdown, type SessionContextBreakdownKey } from "./session-context-breakdown"
|
||||||
import { createSessionContextFormatter } from "./session-context-format"
|
import { createSessionContextFormatter } from "./session-context-format"
|
||||||
|
import { getSessionContextMetrics } from "./session-context-metrics"
|
||||||
|
|
||||||
const BREAKDOWN_COLOR: Record<SessionContextBreakdownKey, string> = {
|
const BREAKDOWN_COLOR: Record<SessionContextBreakdownKey, string> = {
|
||||||
system: "var(--syntax-info)",
|
system: "var(--syntax-info)",
|
||||||
|
|
@ -94,7 +95,7 @@ export function SessionContextTab() {
|
||||||
const language = useLanguage()
|
const language = useLanguage()
|
||||||
const { params, view } = useSessionLayout()
|
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(
|
const messages = createMemo(
|
||||||
() => {
|
() => {
|
||||||
|
|
|
||||||
|
|
@ -1,44 +1,44 @@
|
||||||
import type { Project, UserMessage } from "@opencode-ai/sdk/v2"
|
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 { useDialog } from "@opencode-ai/ui/context/dialog"
|
||||||
import {
|
import { createAutoScroll } from "@opencode-ai/ui/hooks"
|
||||||
batch,
|
import { previewSelectedLines } from "@opencode-ai/ui/pierre/selection-bridge"
|
||||||
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 { ResizeHandle } from "@opencode-ai/ui/resize-handle"
|
import { ResizeHandle } from "@opencode-ai/ui/resize-handle"
|
||||||
import { Select } from "@opencode-ai/ui/select"
|
import { Select } from "@opencode-ai/ui/select"
|
||||||
import { Tabs } from "@opencode-ai/ui/tabs"
|
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 { showToast } from "@opencode-ai/ui/toast"
|
||||||
import { base64Encode, checksum } from "@opencode-ai/util/encode"
|
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 { 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 { NewSessionView, SessionHeader } from "@/components/session"
|
||||||
import { useComments } from "@/context/comments"
|
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 { useGlobalSync } from "@/context/global-sync"
|
||||||
|
import { getSessionPrefetch, SESSION_PREFETCH_TTL } from "@/context/global-sync/session-prefetch"
|
||||||
import { useLanguage } from "@/context/language"
|
import { useLanguage } from "@/context/language"
|
||||||
import { useLayout } from "@/context/layout"
|
import { useLayout } from "@/context/layout"
|
||||||
|
import { useLocal } from "@/context/local"
|
||||||
import { usePrompt } from "@/context/prompt"
|
import { usePrompt } from "@/context/prompt"
|
||||||
import { useSDK } from "@/context/sdk"
|
import { useSDK } from "@/context/sdk"
|
||||||
import { useSettings } from "@/context/settings"
|
import { useSettings } from "@/context/settings"
|
||||||
import { useSync } from "@/context/sync"
|
import { useSync } from "@/context/sync"
|
||||||
import { useTerminal } from "@/context/terminal"
|
import { useTerminal } from "@/context/terminal"
|
||||||
import { type FollowupDraft, sendFollowupDraft } from "@/components/prompt-input/submit"
|
|
||||||
import { createSessionComposerState, SessionComposerRegion } from "@/pages/session/composer"
|
import { createSessionComposerState, SessionComposerRegion } from "@/pages/session/composer"
|
||||||
import { createOpenReviewFile, createSessionTabs, createSizing, focusTerminalById } from "@/pages/session/helpers"
|
import { createOpenReviewFile, createSessionTabs, createSizing, focusTerminalById } from "@/pages/session/helpers"
|
||||||
import { MessageTimeline } from "@/pages/session/message-timeline"
|
import { MessageTimeline } from "@/pages/session/message-timeline"
|
||||||
|
|
@ -76,6 +76,8 @@ type SessionHistoryWindowInput = {
|
||||||
* small batches while scrolling upward, and prefetches older history near top.
|
* small batches while scrolling upward, and prefetches older history near top.
|
||||||
*/
|
*/
|
||||||
function createSessionHistoryWindow(input: SessionHistoryWindowInput) {
|
function createSessionHistoryWindow(input: SessionHistoryWindowInput) {
|
||||||
|
const { params } = useSessionLayout()
|
||||||
|
|
||||||
const turnInit = 10
|
const turnInit = 10
|
||||||
const turnBatch = 8
|
const turnBatch = 8
|
||||||
const turnScrollThreshold = 200
|
const turnScrollThreshold = 200
|
||||||
|
|
@ -93,7 +95,7 @@ function createSessionHistoryWindow(input: SessionHistoryWindowInput) {
|
||||||
const initialTurnStart = (len: number) => (len > turnInit ? len - turnInit : 0)
|
const initialTurnStart = (len: number) => (len > turnInit ? len - turnInit : 0)
|
||||||
|
|
||||||
const turnStart = createMemo(() => {
|
const turnStart = createMemo(() => {
|
||||||
const id = input.sessionID()
|
const id = params.id
|
||||||
const len = input.visibleUserMessages().length
|
const len = input.visibleUserMessages().length
|
||||||
if (!id || len <= 0) return 0
|
if (!id || len <= 0) return 0
|
||||||
if (state.turnID !== id) return initialTurnStart(len)
|
if (state.turnID !== id) return initialTurnStart(len)
|
||||||
|
|
@ -103,7 +105,7 @@ function createSessionHistoryWindow(input: SessionHistoryWindowInput) {
|
||||||
})
|
})
|
||||||
|
|
||||||
const setTurnStart = (start: number) => {
|
const setTurnStart = (start: number) => {
|
||||||
const id = input.sessionID()
|
const id = params.id
|
||||||
const next = start > 0 ? start : 0
|
const next = start > 0 ? start : 0
|
||||||
if (!id) {
|
if (!id) {
|
||||||
setState({ turnID: undefined, turnStart: next })
|
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. */
|
/** Button path: reveal all cached turns, fetch older history, reveal one batch. */
|
||||||
const loadAndReveal = async () => {
|
const loadAndReveal = async () => {
|
||||||
const id = input.sessionID()
|
const id = params.id
|
||||||
if (!id) return
|
if (!id) return
|
||||||
|
|
||||||
const start = turnStart()
|
const start = turnStart()
|
||||||
|
|
@ -169,7 +171,7 @@ function createSessionHistoryWindow(input: SessionHistoryWindowInput) {
|
||||||
|
|
||||||
while (true) {
|
while (true) {
|
||||||
await input.loadMore(id)
|
await input.loadMore(id)
|
||||||
if (input.sessionID() !== id) return
|
if (params.id !== id) return
|
||||||
|
|
||||||
afterVisible = input.visibleUserMessages().length
|
afterVisible = input.visibleUserMessages().length
|
||||||
const nextLoaded = input.loaded()
|
const nextLoaded = input.loaded()
|
||||||
|
|
@ -195,7 +197,7 @@ function createSessionHistoryWindow(input: SessionHistoryWindowInput) {
|
||||||
|
|
||||||
/** Scroll/prefetch path: fetch older history from server. */
|
/** Scroll/prefetch path: fetch older history from server. */
|
||||||
const fetchOlderMessages = async (opts?: { prefetch?: boolean }) => {
|
const fetchOlderMessages = async (opts?: { prefetch?: boolean }) => {
|
||||||
const id = input.sessionID()
|
const id = params.id
|
||||||
if (!id) return
|
if (!id) return
|
||||||
if (!input.historyMore() || input.historyLoading()) return
|
if (!input.historyMore() || input.historyLoading()) return
|
||||||
|
|
||||||
|
|
@ -215,7 +217,7 @@ function createSessionHistoryWindow(input: SessionHistoryWindowInput) {
|
||||||
|
|
||||||
while (true) {
|
while (true) {
|
||||||
await input.loadMore(id)
|
await input.loadMore(id)
|
||||||
if (input.sessionID() !== id) return
|
if (params.id !== id) return
|
||||||
|
|
||||||
const nextLoaded = input.loaded()
|
const nextLoaded = input.loaded()
|
||||||
const raw = nextLoaded - loaded
|
const raw = nextLoaded - loaded
|
||||||
|
|
@ -279,7 +281,7 @@ function createSessionHistoryWindow(input: SessionHistoryWindowInput) {
|
||||||
|
|
||||||
createEffect(
|
createEffect(
|
||||||
on(
|
on(
|
||||||
() => [input.sessionID(), input.messagesReady()] as const,
|
() => [params.id, input.messagesReady()] as const,
|
||||||
([id, ready]) => {
|
([id, ready]) => {
|
||||||
if (!id || !ready) return
|
if (!id || !ready) return
|
||||||
setTurnStart(initialTurnStart(input.visibleUserMessages().length))
|
setTurnStart(initialTurnStart(input.visibleUserMessages().length))
|
||||||
|
|
@ -1618,7 +1620,6 @@ export default function Page() {
|
||||||
|
|
||||||
const { clearMessageHash, scrollToMessage } = useSessionHashScroll({
|
const { clearMessageHash, scrollToMessage } = useSessionHashScroll({
|
||||||
sessionKey,
|
sessionKey,
|
||||||
sessionID: () => params.id,
|
|
||||||
messagesReady,
|
messagesReady,
|
||||||
visibleUserMessages,
|
visibleUserMessages,
|
||||||
turnStart: historyWindow.turnStart,
|
turnStart: historyWindow.turnStart,
|
||||||
|
|
@ -1692,48 +1693,51 @@ export default function Page() {
|
||||||
<div class="flex-1 min-h-0 overflow-hidden">
|
<div class="flex-1 min-h-0 overflow-hidden">
|
||||||
<Switch>
|
<Switch>
|
||||||
<Match when={params.id}>
|
<Match when={params.id}>
|
||||||
<Show when={lastUserMessage()}>
|
{(sessionID) => (
|
||||||
<MessageTimeline
|
<Show when={lastUserMessage()}>
|
||||||
mobileChanges={mobileChanges()}
|
<MessageTimeline
|
||||||
mobileFallback={reviewContent({
|
sessionID={sessionID()}
|
||||||
diffStyle: "unified",
|
mobileChanges={mobileChanges()}
|
||||||
classes: {
|
mobileFallback={reviewContent({
|
||||||
root: "pb-8",
|
diffStyle: "unified",
|
||||||
header: "px-4",
|
classes: {
|
||||||
container: "px-4",
|
root: "pb-8",
|
||||||
},
|
header: "px-4",
|
||||||
loadingClass: "px-4 py-4 text-text-weak",
|
container: "px-4",
|
||||||
emptyClass: "h-full pb-64 -mt-4 flex flex-col items-center justify-center text-center gap-6",
|
},
|
||||||
})}
|
loadingClass: "px-4 py-4 text-text-weak",
|
||||||
actions={actions}
|
emptyClass: "h-full pb-64 -mt-4 flex flex-col items-center justify-center text-center gap-6",
|
||||||
scroll={ui.scroll}
|
})}
|
||||||
onResumeScroll={resumeScroll}
|
actions={actions}
|
||||||
setScrollRef={setScrollRef}
|
scroll={ui.scroll}
|
||||||
onScheduleScrollState={scheduleScrollState}
|
onResumeScroll={resumeScroll}
|
||||||
onAutoScrollHandleScroll={autoScroll.handleScroll}
|
setScrollRef={setScrollRef}
|
||||||
onMarkScrollGesture={markScrollGesture}
|
onScheduleScrollState={scheduleScrollState}
|
||||||
hasScrollGesture={hasScrollGesture}
|
onAutoScrollHandleScroll={autoScroll.handleScroll}
|
||||||
onUserScroll={markUserScroll}
|
onMarkScrollGesture={markScrollGesture}
|
||||||
onTurnBackfillScroll={historyWindow.onScrollerScroll}
|
hasScrollGesture={hasScrollGesture}
|
||||||
onAutoScrollInteraction={autoScroll.handleInteraction}
|
onUserScroll={markUserScroll}
|
||||||
centered={centered()}
|
onTurnBackfillScroll={historyWindow.onScrollerScroll}
|
||||||
setContentRef={(el) => {
|
onAutoScrollInteraction={autoScroll.handleInteraction}
|
||||||
content = el
|
centered={centered()}
|
||||||
autoScroll.contentRef(el)
|
setContentRef={(el) => {
|
||||||
|
content = el
|
||||||
|
autoScroll.contentRef(el)
|
||||||
|
|
||||||
const root = scroller
|
const root = scroller
|
||||||
if (root) scheduleScrollState(root)
|
if (root) scheduleScrollState(root)
|
||||||
}}
|
}}
|
||||||
turnStart={historyWindow.turnStart()}
|
turnStart={historyWindow.turnStart()}
|
||||||
historyMore={historyMore()}
|
historyMore={historyMore()}
|
||||||
historyLoading={historyLoading()}
|
historyLoading={historyLoading()}
|
||||||
onLoadEarlier={() => {
|
onLoadEarlier={() => {
|
||||||
void historyWindow.loadAndReveal()
|
void historyWindow.loadAndReveal()
|
||||||
}}
|
}}
|
||||||
renderedUserMessages={historyWindow.renderedUserMessages()}
|
renderedUserMessages={historyWindow.renderedUserMessages()}
|
||||||
anchor={anchor}
|
anchor={anchor}
|
||||||
/>
|
/>
|
||||||
</Show>
|
</Show>
|
||||||
|
)}
|
||||||
</Match>
|
</Match>
|
||||||
<Match when={true}>
|
<Match when={true}>
|
||||||
<NewSessionView worktree={newSessionWorktree()} />
|
<NewSessionView worktree={newSessionWorktree()} />
|
||||||
|
|
|
||||||
|
|
@ -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 { 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 { PromptInput } from "@/components/prompt-input"
|
||||||
|
import type { FollowupDraft } from "@/components/prompt-input/submit"
|
||||||
import { useLanguage } from "@/context/language"
|
import { useLanguage } from "@/context/language"
|
||||||
import { usePrompt } from "@/context/prompt"
|
import { usePrompt } from "@/context/prompt"
|
||||||
import { getSessionHandoff, setSessionHandoff } from "@/pages/session/handoff"
|
import type { SessionComposerState } from "@/pages/session/composer/session-composer-state"
|
||||||
import { useSessionKey } from "@/pages/session/session-layout"
|
import { SessionFollowupDock } from "@/pages/session/composer/session-followup-dock"
|
||||||
import { SessionPermissionDock } from "@/pages/session/composer/session-permission-dock"
|
import { SessionPermissionDock } from "@/pages/session/composer/session-permission-dock"
|
||||||
import { SessionQuestionDock } from "@/pages/session/composer/session-question-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 { 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 { 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: {
|
export function SessionComposerRegion(props: {
|
||||||
state: SessionComposerState
|
state: SessionComposerState
|
||||||
|
|
@ -194,7 +195,6 @@ export function SessionComposerRegion(props: {
|
||||||
>
|
>
|
||||||
<div ref={(el) => setStore("body", el)}>
|
<div ref={(el) => setStore("body", el)}>
|
||||||
<SessionTodoDock
|
<SessionTodoDock
|
||||||
sessionID={route.params.id}
|
|
||||||
todos={props.state.todos()}
|
todos={props.state.todos()}
|
||||||
collapseLabel={language.t("session.todo.collapse")}
|
collapseLabel={language.t("session.todo.collapse")}
|
||||||
expandLabel={language.t("session.todo.expand")}
|
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 { createEffect, createMemo, on, onCleanup, onMount } from "solid-js"
|
||||||
import { createStore } from "solid-js/store"
|
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 { useGlobalSync } from "@/context/global-sync"
|
||||||
import { useLanguage } from "@/context/language"
|
import { useLanguage } from "@/context/language"
|
||||||
import { usePermission } from "@/context/permission"
|
import { usePermission } from "@/context/permission"
|
||||||
import { useSDK } from "@/context/sdk"
|
import { useSDK } from "@/context/sdk"
|
||||||
import { useSync } from "@/context/sync"
|
import { useSync } from "@/context/sync"
|
||||||
import { composerDriver, composerEnabled, composerEvent } from "@/testing/session-composer"
|
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"
|
import { sessionPermissionRequest, sessionQuestionRequest } from "./session-request-tree"
|
||||||
|
|
||||||
export const todoState = (input: {
|
export const todoState = (input: {
|
||||||
|
|
@ -25,12 +26,12 @@ export const todoState = (input: {
|
||||||
const idle = { type: "idle" as const }
|
const idle = { type: "idle" as const }
|
||||||
|
|
||||||
export function createSessionComposerState(options?: { closeMs?: number | (() => number) }) {
|
export function createSessionComposerState(options?: { closeMs?: number | (() => number) }) {
|
||||||
const params = useParams()
|
|
||||||
const sdk = useSDK()
|
const sdk = useSDK()
|
||||||
const sync = useSync()
|
const sync = useSync()
|
||||||
const globalSync = useGlobalSync()
|
const globalSync = useGlobalSync()
|
||||||
const language = useLanguage()
|
const language = useLanguage()
|
||||||
const permission = usePermission()
|
const permission = usePermission()
|
||||||
|
const { params } = useSessionLayout()
|
||||||
|
|
||||||
const questionRequest = createMemo((): QuestionRequest | undefined => {
|
const questionRequest = createMemo((): QuestionRequest | undefined => {
|
||||||
return sessionQuestionRequest(sync.data.session, sync.data.question, params.id)
|
return sessionQuestionRequest(sync.data.session, sync.data.question, params.id)
|
||||||
|
|
@ -43,8 +44,7 @@ export function createSessionComposerState(options?: { closeMs?: number | (() =>
|
||||||
})
|
})
|
||||||
|
|
||||||
const blocked = createMemo(() => {
|
const blocked = createMemo(() => {
|
||||||
const id = params.id
|
if (!params.id) return false
|
||||||
if (!id) return false
|
|
||||||
return !!permissionRequest() || !!questionRequest()
|
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"),
|
() => todos().length > 0 && todos().every((todo) => todo.status === "completed" || todo.status === "cancelled"),
|
||||||
)
|
)
|
||||||
|
|
||||||
const status = createMemo(() => {
|
const status = createMemo(() => Optional.map(params.id, (id) => sync.data.session_status[id]) ?? idle)
|
||||||
const id = params.id
|
|
||||||
if (!id) return idle
|
|
||||||
return sync.data.session_status[id] ?? idle
|
|
||||||
})
|
|
||||||
|
|
||||||
const busy = createMemo(() => status().type !== "idle")
|
const busy = createMemo(() => status().type !== "idle")
|
||||||
const live = createMemo(() => {
|
const live = createMemo(() => {
|
||||||
|
|
|
||||||
|
|
@ -6,10 +6,11 @@ import { IconButton } from "@opencode-ai/ui/icon-button"
|
||||||
import { useSpring } from "@opencode-ai/ui/motion-spring"
|
import { useSpring } from "@opencode-ai/ui/motion-spring"
|
||||||
import { TextReveal } from "@opencode-ai/ui/text-reveal"
|
import { TextReveal } from "@opencode-ai/ui/text-reveal"
|
||||||
import { TextStrikethrough } from "@opencode-ai/ui/text-strikethrough"
|
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 { createStore } from "solid-js/store"
|
||||||
import { composerEnabled, composerProbe } from "@/testing/session-composer"
|
|
||||||
import { useLanguage } from "@/context/language"
|
import { useLanguage } from "@/context/language"
|
||||||
|
import { composerEnabled, composerProbe } from "@/testing/session-composer"
|
||||||
|
import { useSessionLayout } from "../session-layout"
|
||||||
|
|
||||||
const doneToken = "\u0000done\u0000"
|
const doneToken = "\u0000done\u0000"
|
||||||
const totalToken = "\u0000total\u0000"
|
const totalToken = "\u0000total\u0000"
|
||||||
|
|
@ -40,7 +41,6 @@ function dot(status: Todo["status"]) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function SessionTodoDock(props: {
|
export function SessionTodoDock(props: {
|
||||||
sessionID?: string
|
|
||||||
todos: Todo[]
|
todos: Todo[]
|
||||||
collapseLabel: string
|
collapseLabel: string
|
||||||
expandLabel: string
|
expandLabel: string
|
||||||
|
|
@ -51,6 +51,7 @@ export function SessionTodoDock(props: {
|
||||||
collapsed: false,
|
collapsed: false,
|
||||||
height: 320,
|
height: 320,
|
||||||
})
|
})
|
||||||
|
const { params } = useSessionLayout()
|
||||||
|
|
||||||
const toggle = () => setStore("collapsed", (value) => !value)
|
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 turn = createMemo(() => Math.max(0, Math.min(1, value())))
|
||||||
const full = createMemo(() => Math.max(78, store.height))
|
const full = createMemo(() => Math.max(78, store.height))
|
||||||
const e2e = composerEnabled()
|
const e2e = composerEnabled()
|
||||||
const probe = composerProbe(props.sessionID)
|
const probe = composerProbe(params.id)
|
||||||
let contentRef: HTMLDivElement | undefined
|
let contentRef: HTMLDivElement | undefined
|
||||||
|
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
|
|
|
||||||
|
|
@ -1,32 +1,32 @@
|
||||||
import { For, createEffect, createMemo, on, onCleanup, Show, Index, type JSX } from "solid-js"
|
import { Popover as KobaltePopover } from "@kobalte/core/popover"
|
||||||
import { createStore, produce } from "solid-js/store"
|
import type { AssistantMessage, Message as MessageType, Part, TextPart, UserMessage } from "@opencode-ai/sdk/v2"
|
||||||
import { useNavigate } from "@solidjs/router"
|
|
||||||
import { Button } from "@opencode-ai/ui/button"
|
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 { FileIcon } from "@opencode-ai/ui/file-icon"
|
||||||
import { Icon } from "@opencode-ai/ui/icon"
|
import { Icon } from "@opencode-ai/ui/icon"
|
||||||
import { IconButton } from "@opencode-ai/ui/icon-button"
|
import { IconButton } from "@opencode-ai/ui/icon-button"
|
||||||
import { DropdownMenu } from "@opencode-ai/ui/dropdown-menu"
|
|
||||||
import { Dialog } from "@opencode-ai/ui/dialog"
|
|
||||||
import { InlineInput } from "@opencode-ai/ui/inline-input"
|
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 { 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 { 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 { showToast } from "@opencode-ai/ui/toast"
|
||||||
import { Binary } from "@opencode-ai/util/binary"
|
import { Binary } from "@opencode-ai/util/binary"
|
||||||
import { getFilename } from "@opencode-ai/util/path"
|
import { getFilename } from "@opencode-ai/util/path"
|
||||||
import { Popover as KobaltePopover } from "@kobalte/core/popover"
|
import { useNavigate } from "@solidjs/router"
|
||||||
import { shouldMarkBoundaryGesture, normalizeWheelDelta } from "@/pages/session/message-gesture"
|
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 { 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 { useGlobalSDK } from "@/context/global-sdk"
|
||||||
|
import { useLanguage } from "@/context/language"
|
||||||
import { usePlatform } from "@/context/platform"
|
import { usePlatform } from "@/context/platform"
|
||||||
import { useSettings } from "@/context/settings"
|
|
||||||
import { useSDK } from "@/context/sdk"
|
import { useSDK } from "@/context/sdk"
|
||||||
|
import { useSettings } from "@/context/settings"
|
||||||
import { useSync } from "@/context/sync"
|
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 { messageAgentColor } from "@/utils/agent"
|
||||||
import { parseCommentNote, readCommentMetadata } from "@/utils/comment-note"
|
import { parseCommentNote, readCommentMetadata } from "@/utils/comment-note"
|
||||||
|
|
||||||
|
|
@ -195,6 +195,7 @@ function createTimelineStaging(input: TimelineStageInput) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function MessageTimeline(props: {
|
export function MessageTimeline(props: {
|
||||||
|
sessionID: string
|
||||||
mobileChanges: boolean
|
mobileChanges: boolean
|
||||||
mobileFallback: JSX.Element
|
mobileFallback: JSX.Element
|
||||||
actions?: UserActions
|
actions?: UserActions
|
||||||
|
|
@ -226,26 +227,17 @@ export function MessageTimeline(props: {
|
||||||
const settings = useSettings()
|
const settings = useSettings()
|
||||||
const dialog = useDialog()
|
const dialog = useDialog()
|
||||||
const language = useLanguage()
|
const language = useLanguage()
|
||||||
const { params, sessionKey } = useSessionKey()
|
const { sessionKey } = useSessionKey()
|
||||||
const platform = usePlatform()
|
const platform = usePlatform()
|
||||||
|
|
||||||
const rendered = createMemo(() => props.renderedUserMessages.map((message) => message.id))
|
const rendered = createMemo(() => props.renderedUserMessages.map((message) => message.id))
|
||||||
const sessionID = createMemo(() => params.id)
|
const sessionMessages = createMemo(() => sync.data.message[props.sessionID] ?? emptyMessages)
|
||||||
const sessionMessages = createMemo(() => {
|
|
||||||
const id = sessionID()
|
|
||||||
if (!id) return emptyMessages
|
|
||||||
return sync.data.message[id] ?? emptyMessages
|
|
||||||
})
|
|
||||||
const pending = createMemo(() =>
|
const pending = createMemo(() =>
|
||||||
sessionMessages().findLast(
|
sessionMessages().findLast(
|
||||||
(item): item is AssistantMessage => item.role === "assistant" && typeof item.time.completed !== "number",
|
(item): item is AssistantMessage => item.role === "assistant" && typeof item.time.completed !== "number",
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
const sessionStatus = createMemo(() => {
|
const sessionStatus = createMemo(() => sync.data.session_status[props.sessionID] ?? idle)
|
||||||
const id = sessionID()
|
|
||||||
if (!id) return idle
|
|
||||||
return sync.data.session_status[id] ?? idle
|
|
||||||
})
|
|
||||||
const working = createMemo(() => !!pending() || sessionStatus().type !== "idle")
|
const working = createMemo(() => !!pending() || sessionStatus().type !== "idle")
|
||||||
const tint = createMemo(() => messageAgentColor(sessionMessages(), sync.data.agent))
|
const tint = createMemo(() => messageAgentColor(sessionMessages(), sync.data.agent))
|
||||||
|
|
||||||
|
|
@ -300,11 +292,7 @@ export function MessageTimeline(props: {
|
||||||
|
|
||||||
return undefined
|
return undefined
|
||||||
})
|
})
|
||||||
const info = createMemo(() => {
|
const info = createMemo(() => sync.session.get(props.sessionID))
|
||||||
const id = sessionID()
|
|
||||||
if (!id) return
|
|
||||||
return sync.session.get(id)
|
|
||||||
})
|
|
||||||
const titleValue = createMemo(() => info()?.title)
|
const titleValue = createMemo(() => info()?.title)
|
||||||
const shareUrl = createMemo(() => info()?.share?.url)
|
const shareUrl = createMemo(() => info()?.share?.url)
|
||||||
const shareEnabled = createMemo(() => sync.data.config.share !== "disabled")
|
const shareEnabled = createMemo(() => sync.data.config.share !== "disabled")
|
||||||
|
|
@ -338,12 +326,11 @@ export function MessageTimeline(props: {
|
||||||
const [req, setReq] = createStore({ share: false, unshare: false })
|
const [req, setReq] = createStore({ share: false, unshare: false })
|
||||||
|
|
||||||
const shareSession = () => {
|
const shareSession = () => {
|
||||||
const id = sessionID()
|
if (req.share) return
|
||||||
if (!id || req.share) return
|
|
||||||
if (!shareEnabled()) return
|
if (!shareEnabled()) return
|
||||||
setReq("share", true)
|
setReq("share", true)
|
||||||
globalSDK.client.session
|
globalSDK.client.session
|
||||||
.share({ sessionID: id, directory: sdk.directory })
|
.share({ sessionID: props.sessionID, directory: sdk.directory })
|
||||||
.catch((err: unknown) => {
|
.catch((err: unknown) => {
|
||||||
console.error("Failed to share session", err)
|
console.error("Failed to share session", err)
|
||||||
})
|
})
|
||||||
|
|
@ -353,12 +340,11 @@ export function MessageTimeline(props: {
|
||||||
}
|
}
|
||||||
|
|
||||||
const unshareSession = () => {
|
const unshareSession = () => {
|
||||||
const id = sessionID()
|
if (req.unshare) return
|
||||||
if (!id || req.unshare) return
|
|
||||||
if (!shareEnabled()) return
|
if (!shareEnabled()) return
|
||||||
setReq("unshare", true)
|
setReq("unshare", true)
|
||||||
globalSDK.client.session
|
globalSDK.client.session
|
||||||
.unshare({ sessionID: id, directory: sdk.directory })
|
.unshare({ sessionID: props.sessionID, directory: sdk.directory })
|
||||||
.catch((err: unknown) => {
|
.catch((err: unknown) => {
|
||||||
console.error("Failed to unshare session", err)
|
console.error("Failed to unshare session", err)
|
||||||
})
|
})
|
||||||
|
|
@ -399,7 +385,6 @@ export function MessageTimeline(props: {
|
||||||
)
|
)
|
||||||
|
|
||||||
const openTitleEditor = () => {
|
const openTitleEditor = () => {
|
||||||
if (!sessionID()) return
|
|
||||||
setTitle({ editing: true, draft: titleValue() ?? "" })
|
setTitle({ editing: true, draft: titleValue() ?? "" })
|
||||||
requestAnimationFrame(() => {
|
requestAnimationFrame(() => {
|
||||||
titleRef?.focus()
|
titleRef?.focus()
|
||||||
|
|
@ -413,8 +398,6 @@ export function MessageTimeline(props: {
|
||||||
}
|
}
|
||||||
|
|
||||||
const saveTitleEditor = async () => {
|
const saveTitleEditor = async () => {
|
||||||
const id = sessionID()
|
|
||||||
if (!id) return
|
|
||||||
if (title.saving) return
|
if (title.saving) return
|
||||||
|
|
||||||
const next = title.draft.trim()
|
const next = title.draft.trim()
|
||||||
|
|
@ -425,11 +408,11 @@ export function MessageTimeline(props: {
|
||||||
|
|
||||||
setTitle("saving", true)
|
setTitle("saving", true)
|
||||||
await sdk.client.session
|
await sdk.client.session
|
||||||
.update({ sessionID: id, title: next })
|
.update({ sessionID: props.sessionID, title: next })
|
||||||
.then(() => {
|
.then(() => {
|
||||||
sync.set(
|
sync.set(
|
||||||
produce((draft) => {
|
produce((draft) => {
|
||||||
const index = draft.session.findIndex((s) => s.id === id)
|
const index = draft.session.findIndex((s) => s.id === props.sessionID)
|
||||||
if (index !== -1) draft.session[index].title = next
|
if (index !== -1) draft.session[index].title = next
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
@ -444,17 +427,17 @@ export function MessageTimeline(props: {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const navigateAfterSessionRemoval = (sessionID: string, parentID?: string, nextSessionID?: string) => {
|
const navigateAfterSessionRemoval = (id: string, parentID?: string, nextSessionID?: string) => {
|
||||||
if (params.id !== sessionID) return
|
if (props.sessionID !== id) return
|
||||||
if (parentID) {
|
if (parentID) {
|
||||||
navigate(`/${params.dir}/session/${parentID}`)
|
navigate(`../${parentID}`)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (nextSessionID) {
|
if (nextSessionID) {
|
||||||
navigate(`/${params.dir}/session/${nextSessionID}`)
|
navigate(`../${nextSessionID}`)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
navigate(`/${params.dir}/session`)
|
navigate("../")
|
||||||
}
|
}
|
||||||
|
|
||||||
const archiveSession = async (sessionID: string) => {
|
const archiveSession = async (sessionID: string) => {
|
||||||
|
|
@ -547,7 +530,7 @@ export function MessageTimeline(props: {
|
||||||
const navigateParent = () => {
|
const navigateParent = () => {
|
||||||
const id = parentID()
|
const id = parentID()
|
||||||
if (!id) return
|
if (!id) return
|
||||||
navigate(`/${params.dir}/session/${id}`)
|
navigate(`../${id}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
function DialogDeleteSession(props: { sessionID: string }) {
|
function DialogDeleteSession(props: { sessionID: string }) {
|
||||||
|
|
@ -734,184 +717,180 @@ export function MessageTimeline(props: {
|
||||||
</Show>
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Show when={sessionID()}>
|
<div class="shrink-0 flex items-center gap-3">
|
||||||
{(id) => (
|
<SessionContextUsage placement="bottom" />
|
||||||
<div class="shrink-0 flex items-center gap-3">
|
<DropdownMenu
|
||||||
<SessionContextUsage placement="bottom" />
|
gutter={4}
|
||||||
<DropdownMenu
|
placement="bottom-end"
|
||||||
gutter={4}
|
open={title.menuOpen}
|
||||||
placement="bottom-end"
|
onOpenChange={(open) => {
|
||||||
open={title.menuOpen}
|
setTitle("menuOpen", open)
|
||||||
onOpenChange={(open) => {
|
if (open) return
|
||||||
setTitle("menuOpen", open)
|
}}
|
||||||
if (open) return
|
>
|
||||||
|
<DropdownMenu.Trigger
|
||||||
|
as={IconButton}
|
||||||
|
icon="dot-grid"
|
||||||
|
variant="ghost"
|
||||||
|
class="size-6 rounded-md data-[expanded]:bg-surface-base-active"
|
||||||
|
classList={{
|
||||||
|
"bg-surface-base-active": share.open || title.pendingShare,
|
||||||
|
}}
|
||||||
|
aria-label={language.t("common.moreOptions")}
|
||||||
|
aria-expanded={title.menuOpen || share.open || title.pendingShare}
|
||||||
|
ref={(el: HTMLButtonElement) => {
|
||||||
|
more = el
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<DropdownMenu.Portal>
|
||||||
|
<DropdownMenu.Content
|
||||||
|
style={{ "min-width": "104px" }}
|
||||||
|
onCloseAutoFocus={(event) => {
|
||||||
|
if (title.pendingRename) {
|
||||||
|
event.preventDefault()
|
||||||
|
setTitle("pendingRename", false)
|
||||||
|
openTitleEditor()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (title.pendingShare) {
|
||||||
|
event.preventDefault()
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
setShare({ open: true, dismiss: null })
|
||||||
|
setTitle("pendingShare", false)
|
||||||
|
})
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<DropdownMenu.Trigger
|
<DropdownMenu.Item
|
||||||
as={IconButton}
|
onSelect={() => {
|
||||||
icon="dot-grid"
|
setTitle("pendingRename", true)
|
||||||
variant="ghost"
|
setTitle("menuOpen", false)
|
||||||
class="size-6 rounded-md data-[expanded]:bg-surface-base-active"
|
|
||||||
classList={{
|
|
||||||
"bg-surface-base-active": share.open || title.pendingShare,
|
|
||||||
}}
|
}}
|
||||||
aria-label={language.t("common.moreOptions")}
|
>
|
||||||
aria-expanded={title.menuOpen || share.open || title.pendingShare}
|
<DropdownMenu.ItemLabel>{language.t("common.rename")}</DropdownMenu.ItemLabel>
|
||||||
ref={(el: HTMLButtonElement) => {
|
</DropdownMenu.Item>
|
||||||
more = el
|
<Show when={shareEnabled()}>
|
||||||
}}
|
<DropdownMenu.Item
|
||||||
/>
|
onSelect={() => {
|
||||||
<DropdownMenu.Portal>
|
setTitle({ pendingShare: true, menuOpen: false })
|
||||||
<DropdownMenu.Content
|
|
||||||
style={{ "min-width": "104px" }}
|
|
||||||
onCloseAutoFocus={(event) => {
|
|
||||||
if (title.pendingRename) {
|
|
||||||
event.preventDefault()
|
|
||||||
setTitle("pendingRename", false)
|
|
||||||
openTitleEditor()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (title.pendingShare) {
|
|
||||||
event.preventDefault()
|
|
||||||
requestAnimationFrame(() => {
|
|
||||||
setShare({ open: true, dismiss: null })
|
|
||||||
setTitle("pendingShare", false)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<DropdownMenu.Item
|
<DropdownMenu.ItemLabel>
|
||||||
onSelect={() => {
|
{language.t("session.share.action.share")}
|
||||||
setTitle("pendingRename", true)
|
</DropdownMenu.ItemLabel>
|
||||||
setTitle("menuOpen", false)
|
</DropdownMenu.Item>
|
||||||
}}
|
</Show>
|
||||||
>
|
<DropdownMenu.Item onSelect={() => void archiveSession(props.sessionID)}>
|
||||||
<DropdownMenu.ItemLabel>{language.t("common.rename")}</DropdownMenu.ItemLabel>
|
<DropdownMenu.ItemLabel>{language.t("common.archive")}</DropdownMenu.ItemLabel>
|
||||||
</DropdownMenu.Item>
|
</DropdownMenu.Item>
|
||||||
<Show when={shareEnabled()}>
|
<DropdownMenu.Separator />
|
||||||
<DropdownMenu.Item
|
<DropdownMenu.Item
|
||||||
onSelect={() => {
|
onSelect={() => dialog.show(() => <DialogDeleteSession sessionID={props.sessionID} />)}
|
||||||
setTitle({ pendingShare: true, menuOpen: false })
|
>
|
||||||
}}
|
<DropdownMenu.ItemLabel>{language.t("common.delete")}</DropdownMenu.ItemLabel>
|
||||||
>
|
</DropdownMenu.Item>
|
||||||
<DropdownMenu.ItemLabel>
|
</DropdownMenu.Content>
|
||||||
{language.t("session.share.action.share")}
|
</DropdownMenu.Portal>
|
||||||
</DropdownMenu.ItemLabel>
|
</DropdownMenu>
|
||||||
</DropdownMenu.Item>
|
|
||||||
</Show>
|
|
||||||
<DropdownMenu.Item onSelect={() => void archiveSession(id())}>
|
|
||||||
<DropdownMenu.ItemLabel>{language.t("common.archive")}</DropdownMenu.ItemLabel>
|
|
||||||
</DropdownMenu.Item>
|
|
||||||
<DropdownMenu.Separator />
|
|
||||||
<DropdownMenu.Item
|
|
||||||
onSelect={() => dialog.show(() => <DialogDeleteSession sessionID={id()} />)}
|
|
||||||
>
|
|
||||||
<DropdownMenu.ItemLabel>{language.t("common.delete")}</DropdownMenu.ItemLabel>
|
|
||||||
</DropdownMenu.Item>
|
|
||||||
</DropdownMenu.Content>
|
|
||||||
</DropdownMenu.Portal>
|
|
||||||
</DropdownMenu>
|
|
||||||
|
|
||||||
<KobaltePopover
|
<KobaltePopover
|
||||||
open={share.open}
|
open={share.open}
|
||||||
anchorRef={() => more}
|
anchorRef={() => more}
|
||||||
placement="bottom-end"
|
placement="bottom-end"
|
||||||
gutter={4}
|
gutter={4}
|
||||||
modal={false}
|
modal={false}
|
||||||
onOpenChange={(open) => {
|
onOpenChange={(open) => {
|
||||||
if (open) setShare("dismiss", null)
|
if (open) setShare("dismiss", null)
|
||||||
setShare("open", open)
|
setShare("open", open)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<KobaltePopover.Portal>
|
||||||
|
<KobaltePopover.Content
|
||||||
|
data-component="popover-content"
|
||||||
|
style={{ "min-width": "320px" }}
|
||||||
|
onEscapeKeyDown={(event) => {
|
||||||
|
setShare({ dismiss: "escape", open: false })
|
||||||
|
event.preventDefault()
|
||||||
|
event.stopPropagation()
|
||||||
|
}}
|
||||||
|
onPointerDownOutside={() => {
|
||||||
|
setShare({ dismiss: "outside", open: false })
|
||||||
|
}}
|
||||||
|
onFocusOutside={() => {
|
||||||
|
setShare({ dismiss: "outside", open: false })
|
||||||
|
}}
|
||||||
|
onCloseAutoFocus={(event) => {
|
||||||
|
if (share.dismiss === "outside") event.preventDefault()
|
||||||
|
setShare("dismiss", null)
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<KobaltePopover.Portal>
|
<div class="flex flex-col p-3">
|
||||||
<KobaltePopover.Content
|
<div class="flex flex-col gap-1">
|
||||||
data-component="popover-content"
|
<div class="text-13-medium text-text-strong">
|
||||||
style={{ "min-width": "320px" }}
|
{language.t("session.share.popover.title")}
|
||||||
onEscapeKeyDown={(event) => {
|
|
||||||
setShare({ dismiss: "escape", open: false })
|
|
||||||
event.preventDefault()
|
|
||||||
event.stopPropagation()
|
|
||||||
}}
|
|
||||||
onPointerDownOutside={() => {
|
|
||||||
setShare({ dismiss: "outside", open: false })
|
|
||||||
}}
|
|
||||||
onFocusOutside={() => {
|
|
||||||
setShare({ dismiss: "outside", open: false })
|
|
||||||
}}
|
|
||||||
onCloseAutoFocus={(event) => {
|
|
||||||
if (share.dismiss === "outside") event.preventDefault()
|
|
||||||
setShare("dismiss", null)
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div class="flex flex-col p-3">
|
|
||||||
<div class="flex flex-col gap-1">
|
|
||||||
<div class="text-13-medium text-text-strong">
|
|
||||||
{language.t("session.share.popover.title")}
|
|
||||||
</div>
|
|
||||||
<div class="text-12-regular text-text-weak">
|
|
||||||
{shareUrl()
|
|
||||||
? language.t("session.share.popover.description.shared")
|
|
||||||
: language.t("session.share.popover.description.unshared")}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="mt-3 flex flex-col gap-2">
|
|
||||||
<Show
|
|
||||||
when={shareUrl()}
|
|
||||||
fallback={
|
|
||||||
<Button
|
|
||||||
size="large"
|
|
||||||
variant="primary"
|
|
||||||
class="w-full"
|
|
||||||
onClick={shareSession}
|
|
||||||
disabled={req.share}
|
|
||||||
>
|
|
||||||
{req.share
|
|
||||||
? language.t("session.share.action.publishing")
|
|
||||||
: language.t("session.share.action.publish")}
|
|
||||||
</Button>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<div class="flex flex-col gap-2">
|
|
||||||
<TextField
|
|
||||||
value={shareUrl() ?? ""}
|
|
||||||
readOnly
|
|
||||||
copyable
|
|
||||||
copyKind="link"
|
|
||||||
tabIndex={-1}
|
|
||||||
class="w-full"
|
|
||||||
/>
|
|
||||||
<div class="grid grid-cols-2 gap-2">
|
|
||||||
<Button
|
|
||||||
size="large"
|
|
||||||
variant="secondary"
|
|
||||||
class="w-full shadow-none border border-border-weak-base"
|
|
||||||
onClick={unshareSession}
|
|
||||||
disabled={req.unshare}
|
|
||||||
>
|
|
||||||
{req.unshare
|
|
||||||
? language.t("session.share.action.unpublishing")
|
|
||||||
: language.t("session.share.action.unpublish")}
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
size="large"
|
|
||||||
variant="primary"
|
|
||||||
class="w-full"
|
|
||||||
onClick={viewShare}
|
|
||||||
disabled={req.unshare}
|
|
||||||
>
|
|
||||||
{language.t("session.share.action.view")}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Show>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</KobaltePopover.Content>
|
<div class="text-12-regular text-text-weak">
|
||||||
</KobaltePopover.Portal>
|
{shareUrl()
|
||||||
</KobaltePopover>
|
? language.t("session.share.popover.description.shared")
|
||||||
</div>
|
: language.t("session.share.popover.description.unshared")}
|
||||||
)}
|
</div>
|
||||||
</Show>
|
</div>
|
||||||
|
<div class="mt-3 flex flex-col gap-2">
|
||||||
|
<Show
|
||||||
|
when={shareUrl()}
|
||||||
|
fallback={
|
||||||
|
<Button
|
||||||
|
size="large"
|
||||||
|
variant="primary"
|
||||||
|
class="w-full"
|
||||||
|
onClick={shareSession}
|
||||||
|
disabled={req.share}
|
||||||
|
>
|
||||||
|
{req.share
|
||||||
|
? language.t("session.share.action.publishing")
|
||||||
|
: language.t("session.share.action.publish")}
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<div class="flex flex-col gap-2">
|
||||||
|
<TextField
|
||||||
|
value={shareUrl() ?? ""}
|
||||||
|
readOnly
|
||||||
|
copyable
|
||||||
|
copyKind="link"
|
||||||
|
tabIndex={-1}
|
||||||
|
class="w-full"
|
||||||
|
/>
|
||||||
|
<div class="grid grid-cols-2 gap-2">
|
||||||
|
<Button
|
||||||
|
size="large"
|
||||||
|
variant="secondary"
|
||||||
|
class="w-full shadow-none border border-border-weak-base"
|
||||||
|
onClick={unshareSession}
|
||||||
|
disabled={req.unshare}
|
||||||
|
>
|
||||||
|
{req.unshare
|
||||||
|
? language.t("session.share.action.unpublishing")
|
||||||
|
: language.t("session.share.action.unpublish")}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
size="large"
|
||||||
|
variant="primary"
|
||||||
|
class="w-full"
|
||||||
|
onClick={viewShare}
|
||||||
|
disabled={req.unshare}
|
||||||
|
>
|
||||||
|
{language.t("session.share.action.view")}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</KobaltePopover.Content>
|
||||||
|
</KobaltePopover.Portal>
|
||||||
|
</KobaltePopover>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
|
|
@ -999,7 +978,7 @@ export function MessageTimeline(props: {
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
<SessionTurn
|
<SessionTurn
|
||||||
sessionID={sessionID() ?? ""}
|
sessionID={props.sessionID}
|
||||||
messageID={messageID}
|
messageID={messageID}
|
||||||
actions={props.actions}
|
actions={props.actions}
|
||||||
active={active()}
|
active={active()}
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ import { createMemo } from "solid-js"
|
||||||
import { useLayout } from "@/context/layout"
|
import { useLayout } from "@/context/layout"
|
||||||
|
|
||||||
export const useSessionKey = () => {
|
export const useSessionKey = () => {
|
||||||
const params = useParams()
|
const params = useParams<{ dir: string; id: string & { __brand: "SessionID" } }>()
|
||||||
const sessionKey = createMemo(() => `${params.dir}${params.id ? "/" + params.id : ""}`)
|
const sessionKey = createMemo(() => `${params.dir}${params.id ? "/" + params.id : ""}`)
|
||||||
return { params, sessionKey }
|
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 { 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 FileTree from "@/components/file-tree"
|
||||||
|
import { FileVisual, SessionContextTab, SortableTab } from "@/components/session"
|
||||||
import { SessionContextUsage } from "@/components/session-context-usage"
|
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 { useCommand } from "@/context/command"
|
||||||
import { useFile, type SelectedLineRange } from "@/context/file"
|
import { type SelectedLineRange, useFile } from "@/context/file"
|
||||||
import { useLanguage } from "@/context/language"
|
import { useLanguage } from "@/context/language"
|
||||||
import { useLayout } from "@/context/layout"
|
import { useLayout } from "@/context/layout"
|
||||||
import { useSync } from "@/context/sync"
|
import { useSync } from "@/context/sync"
|
||||||
import { createFileTabListSync } from "@/pages/session/file-tab-scroll"
|
import { createFileTabListSync } from "@/pages/session/file-tab-scroll"
|
||||||
import { FileTabContent } from "@/pages/session/file-tabs"
|
import { FileTabContent } from "@/pages/session/file-tabs"
|
||||||
import { createOpenSessionFileTab, createSessionTabs, getTabReorderIndex, type Sizing } from "@/pages/session/helpers"
|
|
||||||
import { setSessionHandoff } from "@/pages/session/handoff"
|
import { setSessionHandoff } from "@/pages/session/handoff"
|
||||||
|
import { createOpenSessionFileTab, createSessionTabs, getTabReorderIndex, type Sizing } from "@/pages/session/helpers"
|
||||||
import { useSessionLayout } from "@/pages/session/session-layout"
|
import { useSessionLayout } from "@/pages/session/session-layout"
|
||||||
|
import { Optional } from "@/utils/optional"
|
||||||
|
import { ConstrainDragYAxis, getDraggableId } from "@/utils/solid-dnd"
|
||||||
|
|
||||||
export function SessionSidePanel(props: {
|
export function SessionSidePanel(props: {
|
||||||
reviewPanel: () => JSX.Element
|
reviewPanel: () => JSX.Element
|
||||||
|
|
@ -54,14 +55,13 @@ export function SessionSidePanel(props: {
|
||||||
})
|
})
|
||||||
const treeWidth = createMemo(() => (fileOpen() ? `${layout.fileTree.width()}px` : "0px"))
|
const treeWidth = createMemo(() => (fileOpen() ? `${layout.fileTree.width()}px` : "0px"))
|
||||||
|
|
||||||
const info = createMemo(() => (params.id ? sync.session.get(params.id) : undefined))
|
const info = createMemo(() => Optional.map(params.id, (id) => sync.session.get(id)))
|
||||||
const diffs = createMemo(() => (params.id ? (sync.data.session_diff[params.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 reviewCount = createMemo(() => Math.max(info()?.summary?.files ?? 0, diffs().length))
|
||||||
const hasReview = createMemo(() => reviewCount() > 0)
|
const hasReview = createMemo(() => reviewCount() > 0)
|
||||||
const diffsReady = createMemo(() => {
|
const diffsReady = createMemo(() => {
|
||||||
const id = params.id
|
const id = params.id
|
||||||
if (!id) return true
|
if (!id || !hasReview()) return true
|
||||||
if (!hasReview()) return true
|
|
||||||
return sync.data.session_diff[id] !== undefined
|
return sync.data.session_diff[id] !== undefined
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,15 @@
|
||||||
import { useNavigate } from "@solidjs/router"
|
import type { UserMessage } from "@opencode-ai/sdk/v2"
|
||||||
import { useCommand, type CommandOption } from "@/context/command"
|
|
||||||
import { useDialog } from "@opencode-ai/ui/context/dialog"
|
import { useDialog } from "@opencode-ai/ui/context/dialog"
|
||||||
import { previewSelectedLines } from "@opencode-ai/ui/pierre/selection-bridge"
|
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 { useLanguage } from "@/context/language"
|
||||||
import { useLayout } from "@/context/layout"
|
import { useLayout } from "@/context/layout"
|
||||||
import { useLocal } from "@/context/local"
|
import { useLocal } from "@/context/local"
|
||||||
|
|
@ -11,16 +18,10 @@ import { usePrompt } from "@/context/prompt"
|
||||||
import { useSDK } from "@/context/sdk"
|
import { useSDK } from "@/context/sdk"
|
||||||
import { useSync } from "@/context/sync"
|
import { useSync } from "@/context/sync"
|
||||||
import { useTerminal } from "@/context/terminal"
|
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 { 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 { useSessionLayout } from "@/pages/session/session-layout"
|
||||||
|
import { Optional } from "@/utils/optional"
|
||||||
|
import { extractPromptFromParts } from "@/utils/prompt"
|
||||||
|
|
||||||
export type SessionCommandContext = {
|
export type SessionCommandContext = {
|
||||||
navigateMessageByOffset: (offset: number) => void
|
navigateMessageByOffset: (offset: number) => void
|
||||||
|
|
@ -51,11 +52,8 @@ export const useSessionCommands = (actions: SessionCommandContext) => {
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
const { params, tabs, view } = useSessionLayout()
|
const { params, tabs, view } = useSessionLayout()
|
||||||
|
|
||||||
const info = () => {
|
const info = () => Optional.map(params.id, (id) => sync.session.get(id))
|
||||||
const id = params.id
|
|
||||||
if (!id) return
|
|
||||||
return sync.session.get(id)
|
|
||||||
}
|
|
||||||
const hasReview = () => {
|
const hasReview = () => {
|
||||||
const id = params.id
|
const id = params.id
|
||||||
if (!id) return false
|
if (!id) return false
|
||||||
|
|
@ -76,12 +74,9 @@ export const useSessionCommands = (actions: SessionCommandContext) => {
|
||||||
const closableTab = tabState.closableTab
|
const closableTab = tabState.closableTab
|
||||||
|
|
||||||
const idle = { type: "idle" as const }
|
const idle = { type: "idle" as const }
|
||||||
const status = () => sync.data.session_status[params.id ?? ""] ?? idle
|
const status = () => Optional.map(params.id, (id) => sync.data.session_status[id]) ?? idle
|
||||||
const messages = () => {
|
const messages = () => Optional.map(params.id, (id) => sync.data.message[id]) ?? []
|
||||||
const id = params.id
|
|
||||||
if (!id) return []
|
|
||||||
return sync.data.message[id] ?? []
|
|
||||||
}
|
|
||||||
const userMessages = () => messages().filter((m) => m.role === "user") as UserMessage[]
|
const userMessages = () => messages().filter((m) => m.role === "user") as UserMessage[]
|
||||||
const visibleUserMessages = () => {
|
const visibleUserMessages = () => {
|
||||||
const revert = info()?.revert?.messageID
|
const revert = info()?.revert?.messageID
|
||||||
|
|
@ -97,7 +92,10 @@ export const useSessionCommands = (actions: SessionCommandContext) => {
|
||||||
const selectionPreview = (path: string, selection: FileSelection) => {
|
const selectionPreview = (path: string, selection: FileSelection) => {
|
||||||
const content = file.get(path)?.content?.content
|
const content = file.get(path)?.content?.content
|
||||||
if (!content) return undefined
|
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) => {
|
const addSelectionToContext = (path: string, selection: FileSelection) => {
|
||||||
|
|
@ -222,9 +220,10 @@ export const useSessionCommands = (actions: SessionCommandContext) => {
|
||||||
slash: "unshare",
|
slash: "unshare",
|
||||||
disabled: !params.id || !info()?.share?.url,
|
disabled: !params.id || !info()?.share?.url,
|
||||||
onSelect: async () => {
|
onSelect: async () => {
|
||||||
if (!params.id) return
|
const id = params.id
|
||||||
|
if (!id) return
|
||||||
await sdk.client.session
|
await sdk.client.session
|
||||||
.unshare({ sessionID: params.id })
|
.unshare({ sessionID: id })
|
||||||
.then(() =>
|
.then(() =>
|
||||||
showToast({
|
showToast({
|
||||||
title: language.t("toast.session.unshare.success.title"),
|
title: language.t("toast.session.unshare.success.title"),
|
||||||
|
|
@ -423,10 +422,15 @@ export const useSessionCommands = (actions: SessionCommandContext) => {
|
||||||
const revert = info()?.revert?.messageID
|
const revert = info()?.revert?.messageID
|
||||||
const message = findLast(userMessages(), (x) => !revert || x.id < revert)
|
const message = findLast(userMessages(), (x) => !revert || x.id < revert)
|
||||||
if (!message) return
|
if (!message) return
|
||||||
await sdk.client.session.revert({ sessionID, messageID: message.id })
|
await sdk.client.session.revert({
|
||||||
|
sessionID,
|
||||||
|
messageID: message.id,
|
||||||
|
})
|
||||||
const parts = sync.data.part[message.id]
|
const parts = sync.data.part[message.id]
|
||||||
if (parts) {
|
if (parts) {
|
||||||
const restored = extractPromptFromParts(parts, { directory: sdk.directory })
|
const restored = extractPromptFromParts(parts, {
|
||||||
|
directory: sdk.directory,
|
||||||
|
})
|
||||||
prompt.set(restored)
|
prompt.set(restored)
|
||||||
}
|
}
|
||||||
const priorMessage = findLast(userMessages(), (x) => x.id < message.id)
|
const priorMessage = findLast(userMessages(), (x) => x.id < message.id)
|
||||||
|
|
@ -452,7 +456,10 @@ export const useSessionCommands = (actions: SessionCommandContext) => {
|
||||||
setActiveMessage(lastMsg)
|
setActiveMessage(lastMsg)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
await sdk.client.session.revert({ sessionID, messageID: nextMessage.id })
|
await sdk.client.session.revert({
|
||||||
|
sessionID,
|
||||||
|
messageID: nextMessage.id,
|
||||||
|
})
|
||||||
const priorMsg = findLast(userMessages(), (x) => x.id < nextMessage.id)
|
const priorMsg = findLast(userMessages(), (x) => x.id < nextMessage.id)
|
||||||
setActiveMessage(priorMsg)
|
setActiveMessage(priorMsg)
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -2,10 +2,11 @@ import type { UserMessage } from "@opencode-ai/sdk/v2"
|
||||||
import { useLocation, useNavigate } from "@solidjs/router"
|
import { useLocation, useNavigate } from "@solidjs/router"
|
||||||
import { createEffect, createMemo, onCleanup, onMount } from "solid-js"
|
import { createEffect, createMemo, onCleanup, onMount } from "solid-js"
|
||||||
import { messageIdFromHash } from "./message-id-from-hash"
|
import { messageIdFromHash } from "./message-id-from-hash"
|
||||||
|
import { useSessionLayout } from "./session-layout"
|
||||||
|
|
||||||
export const useSessionHashScroll = (input: {
|
export const useSessionHashScroll = (input: {
|
||||||
sessionKey: () => string
|
sessionKey: () => string
|
||||||
sessionID: () => string | undefined
|
|
||||||
messagesReady: () => boolean
|
messagesReady: () => boolean
|
||||||
visibleUserMessages: () => UserMessage[]
|
visibleUserMessages: () => UserMessage[]
|
||||||
turnStart: () => number
|
turnStart: () => number
|
||||||
|
|
@ -28,6 +29,7 @@ export const useSessionHashScroll = (input: {
|
||||||
|
|
||||||
const location = useLocation()
|
const location = useLocation()
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
|
const { params } = useSessionLayout()
|
||||||
|
|
||||||
const frames = new Set<number>()
|
const frames = new Set<number>()
|
||||||
const queue = (fn: () => void) => {
|
const queue = (fn: () => void) => {
|
||||||
|
|
@ -142,13 +144,13 @@ export const useSessionHashScroll = (input: {
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
const hash = location.hash
|
const hash = location.hash
|
||||||
if (!hash) clearing = false
|
if (!hash) clearing = false
|
||||||
if (!input.sessionID() || !input.messagesReady()) return
|
if (!params.id || !input.messagesReady()) return
|
||||||
cancel()
|
cancel()
|
||||||
queue(() => applyHash("auto"))
|
queue(() => applyHash("auto"))
|
||||||
})
|
})
|
||||||
|
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
if (!input.sessionID() || !input.messagesReady()) return
|
if (!params.id || !input.messagesReady()) return
|
||||||
|
|
||||||
visibleUserMessages()
|
visibleUserMessages()
|
||||||
input.turnStart()
|
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 { DurableObject } from "cloudflare:workers"
|
||||||
import { randomUUID } from "node:crypto"
|
import { randomUUID } from "node:crypto"
|
||||||
import { jwtVerify, createRemoteJWKSet } from "jose"
|
|
||||||
import { createAppAuth } from "@octokit/auth-app"
|
import { createAppAuth } from "@octokit/auth-app"
|
||||||
import { Octokit } from "@octokit/rest"
|
import { Octokit } from "@octokit/rest"
|
||||||
|
import { Hono } from "hono"
|
||||||
|
import { createRemoteJWKSet, jwtVerify } from "jose"
|
||||||
import { Resource } from "sst"
|
import { Resource } from "sst"
|
||||||
|
|
||||||
type Env = {
|
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 { useRoute } from "@tui/context/route"
|
||||||
import { useSync } from "@tui/context/sync"
|
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 { Locale } from "@/util/locale"
|
||||||
import { useKeybind } from "../context/keybind"
|
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 { useKV } from "../context/kv"
|
||||||
|
import { useSDK } from "../context/sdk"
|
||||||
|
import { useTheme } from "../context/theme"
|
||||||
import { createDebouncedSignal } from "../util/signal"
|
import { createDebouncedSignal } from "../util/signal"
|
||||||
|
import { DialogSessionRename } from "./dialog-session-rename"
|
||||||
import { Spinner } from "./spinner"
|
import { Spinner } from "./spinner"
|
||||||
|
|
||||||
export function DialogSessionList() {
|
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 { useRoute } from "@tui/context/route"
|
||||||
import { useSync } from "@tui/context/sync"
|
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 { Locale } from "@/util/locale"
|
||||||
import { useKeybind } from "../../context/keybind"
|
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 { useKV } from "../../context/kv"
|
||||||
import { createDebouncedSignal } from "../../util/signal"
|
import { useSDK } from "../../context/sdk"
|
||||||
import { Spinner } from "../spinner"
|
import { useTheme } from "../../context/theme"
|
||||||
import { useToast } from "../../ui/toast"
|
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 } = {}) {
|
export function DialogSessionList(props: { workspaceID?: string; localOnly?: boolean } = {}) {
|
||||||
const dialog = useDialog()
|
const dialog = useDialog()
|
||||||
|
|
|
||||||
|
|
@ -1,60 +1,59 @@
|
||||||
import {
|
import type {
|
||||||
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 {
|
|
||||||
AgentPart,
|
AgentPart,
|
||||||
AssistantMessage,
|
AssistantMessage,
|
||||||
FilePart,
|
FilePart,
|
||||||
Message as MessageType,
|
Message as MessageType,
|
||||||
Part as PartType,
|
Part as PartType,
|
||||||
ReasoningPart,
|
|
||||||
TextPart,
|
|
||||||
ToolPart,
|
|
||||||
UserMessage,
|
|
||||||
Todo,
|
|
||||||
QuestionAnswer,
|
QuestionAnswer,
|
||||||
QuestionInfo,
|
QuestionInfo,
|
||||||
|
ReasoningPart,
|
||||||
|
TextPart,
|
||||||
|
Todo,
|
||||||
|
ToolPart,
|
||||||
|
UserMessage,
|
||||||
} from "@opencode-ai/sdk/v2"
|
} 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 { useData } from "../context"
|
||||||
import { useFileComponent } from "../context/file"
|
|
||||||
import { useDialog } from "../context/dialog"
|
import { useDialog } from "../context/dialog"
|
||||||
|
import { useFileComponent } from "../context/file"
|
||||||
import { type UiI18n, useI18n } from "../context/i18n"
|
import { type UiI18n, useI18n } from "../context/i18n"
|
||||||
import { BasicTool, GenericTool } from "./basic-tool"
|
|
||||||
import { Accordion } from "./accordion"
|
import { Accordion } from "./accordion"
|
||||||
import { StickyAccordionHeader } from "./sticky-accordion-header"
|
import { BasicTool, GenericTool } from "./basic-tool"
|
||||||
import { Card } from "./card"
|
import { Checkbox } from "./checkbox"
|
||||||
import { Collapsible } from "./collapsible"
|
import { Collapsible } from "./collapsible"
|
||||||
|
import { DiffChanges } from "./diff-changes"
|
||||||
import { FileIcon } from "./file-icon"
|
import { FileIcon } from "./file-icon"
|
||||||
import { Icon } from "./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 { 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 { TextShimmer } from "./text-shimmer"
|
||||||
import { AnimatedCountList } from "./tool-count-summary"
|
import { AnimatedCountList } from "./tool-count-summary"
|
||||||
|
import { ToolErrorCard } from "./tool-error-card"
|
||||||
import { ToolStatusTitle } from "./tool-status-title"
|
import { ToolStatusTitle } from "./tool-status-title"
|
||||||
import { animate } from "motion"
|
import { Tooltip } from "./tooltip"
|
||||||
import { useLocation } from "@solidjs/router"
|
|
||||||
import { attached, inline, kind } from "./message-file"
|
|
||||||
|
|
||||||
function ShellSubmessage(props: { text: string; animate?: boolean }) {
|
function ShellSubmessage(props: { text: string; animate?: boolean }) {
|
||||||
let widthRef: HTMLSpanElement | undefined
|
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" }[] = [
|
const allRefs: { start: number; end: number; type: "file" | "agent" }[] = [
|
||||||
...props.references
|
...props.references
|
||||||
.filter((r) => r.source?.text?.start !== undefined && r.source?.text?.end !== undefined)
|
.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
|
...props.agents
|
||||||
.filter((a) => a.source?.start !== undefined && a.source?.end !== undefined)
|
.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)
|
].sort((a, b) => a.start - b.start)
|
||||||
|
|
||||||
const result: HighlightSegment[] = []
|
const result: HighlightSegment[] = []
|
||||||
|
|
@ -1334,7 +1341,10 @@ PART_MAPPING["text"] = function TextPartDisplay(props) {
|
||||||
: -1
|
: -1
|
||||||
if (!(ms >= 0)) return ""
|
if (!(ms >= 0)) return ""
|
||||||
const total = Math.round(ms / 1000)
|
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 minutes = Math.floor(total / 60)
|
||||||
const seconds = total % 60
|
const seconds = total % 60
|
||||||
return i18n.t("ui.message.duration.minutesSeconds", {
|
return i18n.t("ui.message.duration.minutesSeconds", {
|
||||||
|
|
@ -1475,7 +1485,10 @@ ToolRegistry.register({
|
||||||
<BasicTool
|
<BasicTool
|
||||||
{...props}
|
{...props}
|
||||||
icon="bullet-list"
|
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}>
|
<Show when={props.output}>
|
||||||
<div data-component="tool-output" data-scrollable>
|
<div data-component="tool-output" data-scrollable>
|
||||||
|
|
@ -1974,7 +1987,12 @@ ToolRegistry.register({
|
||||||
<Accordion.Trigger>
|
<Accordion.Trigger>
|
||||||
<div data-slot="apply-patch-trigger-content">
|
<div data-slot="apply-patch-trigger-content">
|
||||||
<div data-slot="apply-patch-file-info">
|
<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">
|
<div data-slot="apply-patch-file-name-container">
|
||||||
<Show when={file.relativePath.includes("/")}>
|
<Show when={file.relativePath.includes("/")}>
|
||||||
<span data-slot="apply-patch-directory">{`\u202A${getDirectory(file.relativePath)}\u202C`}</span>
|
<span data-slot="apply-patch-directory">{`\u202A${getDirectory(file.relativePath)}\u202C`}</span>
|
||||||
|
|
@ -2000,7 +2018,12 @@ ToolRegistry.register({
|
||||||
</span>
|
</span>
|
||||||
</Match>
|
</Match>
|
||||||
<Match when={true}>
|
<Match when={true}>
|
||||||
<DiffChanges changes={{ additions: file.additions, deletions: file.deletions }} />
|
<DiffChanges
|
||||||
|
changes={{
|
||||||
|
additions: file.additions,
|
||||||
|
deletions: file.deletions,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</Match>
|
</Match>
|
||||||
</Switch>
|
</Switch>
|
||||||
<Icon name="chevron-grabber-vertical" size="small" />
|
<Icon name="chevron-grabber-vertical" size="small" />
|
||||||
|
|
@ -2014,8 +2037,14 @@ ToolRegistry.register({
|
||||||
<Dynamic
|
<Dynamic
|
||||||
component={fileComponent}
|
component={fileComponent}
|
||||||
mode="diff"
|
mode="diff"
|
||||||
before={{ name: file.filePath, contents: file.before }}
|
before={{
|
||||||
after={{ name: file.movePath ?? file.filePath, contents: file.after }}
|
name: file.filePath,
|
||||||
|
contents: file.before,
|
||||||
|
}}
|
||||||
|
after={{
|
||||||
|
name: file.movePath ?? file.filePath,
|
||||||
|
contents: file.after,
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
|
|
@ -2054,7 +2083,12 @@ ToolRegistry.register({
|
||||||
</div>
|
</div>
|
||||||
<div data-slot="message-part-actions">
|
<div data-slot="message-part-actions">
|
||||||
<Show when={!pending()}>
|
<Show when={!pending()}>
|
||||||
<DiffChanges changes={{ additions: single()!.additions, deletions: single()!.deletions }} />
|
<DiffChanges
|
||||||
|
changes={{
|
||||||
|
additions: single()!.additions,
|
||||||
|
deletions: single()!.deletions,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</Show>
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -2080,7 +2114,12 @@ ToolRegistry.register({
|
||||||
</span>
|
</span>
|
||||||
</Match>
|
</Match>
|
||||||
<Match when={true}>
|
<Match when={true}>
|
||||||
<DiffChanges changes={{ additions: single()!.additions, deletions: single()!.deletions }} />
|
<DiffChanges
|
||||||
|
changes={{
|
||||||
|
additions: single()!.additions,
|
||||||
|
deletions: single()!.deletions,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</Match>
|
</Match>
|
||||||
</Switch>
|
</Switch>
|
||||||
}
|
}
|
||||||
|
|
@ -2089,8 +2128,14 @@ ToolRegistry.register({
|
||||||
<Dynamic
|
<Dynamic
|
||||||
component={fileComponent}
|
component={fileComponent}
|
||||||
mode="diff"
|
mode="diff"
|
||||||
before={{ name: single()!.filePath, contents: single()!.before }}
|
before={{
|
||||||
after={{ name: single()!.movePath ?? single()!.filePath, contents: single()!.after }}
|
name: single()!.filePath,
|
||||||
|
contents: single()!.before,
|
||||||
|
}}
|
||||||
|
after={{
|
||||||
|
name: single()!.movePath ?? single()!.filePath,
|
||||||
|
contents: single()!.after,
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</ToolFileAccordion>
|
</ToolFileAccordion>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue