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