mirror of
https://github.com/anomalyco/opencode.git
synced 2026-02-10 02:44:21 +00:00
Compare commits
1 Commits
beta
...
opencode/s
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3ab23c45c3 |
@@ -762,6 +762,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
||||
editor: () => editorRef,
|
||||
isFocused,
|
||||
isDialogActive: () => !!dialog.active,
|
||||
draggingType: () => store.draggingType,
|
||||
setDraggingType: (type) => setStore("draggingType", type),
|
||||
focusEditor: () => {
|
||||
editorRef.focus()
|
||||
|
||||
@@ -11,6 +11,7 @@ type PromptAttachmentsInput = {
|
||||
editor: () => HTMLDivElement | undefined
|
||||
isFocused: () => boolean
|
||||
isDialogActive: () => boolean
|
||||
draggingType: () => "image" | "@mention" | null
|
||||
setDraggingType: (type: "image" | "@mention" | null) => void
|
||||
focusEditor: () => void
|
||||
addPart: (part: ContentPart) => void
|
||||
@@ -21,6 +22,11 @@ export function createPromptAttachments(input: PromptAttachmentsInput) {
|
||||
const prompt = usePrompt()
|
||||
const language = useLanguage()
|
||||
|
||||
const setDraggingType = (next: "image" | "@mention" | null) => {
|
||||
if (input.draggingType() === next) return
|
||||
input.setDraggingType(next)
|
||||
}
|
||||
|
||||
const addImageAttachment = async (file: File) => {
|
||||
if (!ACCEPTED_FILE_TYPES.includes(file.type)) return
|
||||
|
||||
@@ -98,16 +104,20 @@ export function createPromptAttachments(input: PromptAttachmentsInput) {
|
||||
const hasFiles = event.dataTransfer?.types.includes("Files")
|
||||
const hasText = event.dataTransfer?.types.includes("text/plain")
|
||||
if (hasFiles) {
|
||||
input.setDraggingType("image")
|
||||
} else if (hasText) {
|
||||
input.setDraggingType("@mention")
|
||||
setDraggingType("image")
|
||||
return
|
||||
}
|
||||
if (hasText) {
|
||||
setDraggingType("@mention")
|
||||
return
|
||||
}
|
||||
setDraggingType(null)
|
||||
}
|
||||
|
||||
const handleGlobalDragLeave = (event: DragEvent) => {
|
||||
if (input.isDialogActive()) return
|
||||
if (!event.relatedTarget) {
|
||||
input.setDraggingType(null)
|
||||
setDraggingType(null)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -115,7 +125,7 @@ export function createPromptAttachments(input: PromptAttachmentsInput) {
|
||||
if (input.isDialogActive()) return
|
||||
|
||||
event.preventDefault()
|
||||
input.setDraggingType(null)
|
||||
setDraggingType(null)
|
||||
|
||||
const plainText = event.dataTransfer?.getData("text/plain")
|
||||
const filePrefix = "file:"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { TextField } from "@opencode-ai/ui/text-field"
|
||||
import { Logo } from "@opencode-ai/ui/logo"
|
||||
import { Button } from "@opencode-ai/ui/button"
|
||||
import { Component, Show } from "solid-js"
|
||||
import { Component, Show, createMemo } from "solid-js"
|
||||
import { createStore } from "solid-js/store"
|
||||
import { usePlatform } from "@/context/platform"
|
||||
import { useLanguage } from "@/context/language"
|
||||
@@ -209,6 +209,7 @@ interface ErrorPageProps {
|
||||
export const ErrorPage: Component<ErrorPageProps> = (props) => {
|
||||
const platform = usePlatform()
|
||||
const language = useLanguage()
|
||||
const details = createMemo(() => formatError(props.error, language.t))
|
||||
const [store, setStore] = createStore({
|
||||
checking: false,
|
||||
version: undefined as string | undefined,
|
||||
@@ -237,7 +238,7 @@ export const ErrorPage: Component<ErrorPageProps> = (props) => {
|
||||
<p class="text-sm text-text-weak">{language.t("error.page.description")}</p>
|
||||
</div>
|
||||
<TextField
|
||||
value={formatError(props.error, language.t)}
|
||||
value={details()}
|
||||
readOnly
|
||||
copyable
|
||||
multiline
|
||||
|
||||
@@ -24,14 +24,23 @@ export default function Home() {
|
||||
const language = useLanguage()
|
||||
const homedir = createMemo(() => sync.data.path.home)
|
||||
const recent = createMemo(() => {
|
||||
return sync.data.project
|
||||
.toSorted((a, b) => (b.time.updated ?? b.time.created) - (a.time.updated ?? a.time.created))
|
||||
.slice(0, 5)
|
||||
return sync.data.project.reduce(
|
||||
(list, project) => {
|
||||
const time = project.time.updated ?? project.time.created
|
||||
const index = list.findIndex((item) => (item.time.updated ?? item.time.created) < time)
|
||||
if (index === -1) list.push(project)
|
||||
else list.splice(index, 0, project)
|
||||
if (list.length > 5) list.pop()
|
||||
return list
|
||||
},
|
||||
sync.data.project.slice(0, 0),
|
||||
)
|
||||
})
|
||||
|
||||
function openProject(directory: string) {
|
||||
function openProject(directory: string, doNavigate = true) {
|
||||
layout.projects.open(directory)
|
||||
server.projects.touch(directory)
|
||||
if (!doNavigate) return
|
||||
navigate(`/${base64Encode(directory)}`)
|
||||
}
|
||||
|
||||
@@ -39,9 +48,14 @@ export default function Home() {
|
||||
function resolve(result: string | string[] | null) {
|
||||
if (Array.isArray(result)) {
|
||||
for (const directory of result) {
|
||||
openProject(directory)
|
||||
openProject(directory, false)
|
||||
}
|
||||
} else if (result) {
|
||||
const first = result[0]
|
||||
if (!first) return
|
||||
navigate(`/${base64Encode(first)}`)
|
||||
return
|
||||
}
|
||||
if (result) {
|
||||
openProject(result)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -563,9 +563,17 @@ export default function Layout(props: ParentProps) {
|
||||
if (!pageReady()) return
|
||||
if (!layoutReady()) return
|
||||
const projects = layout.projects.list()
|
||||
const byDirectory = new Map<string, LocalProject>()
|
||||
for (const project of projects) {
|
||||
byDirectory.set(project.worktree, project)
|
||||
for (const directory of project.sandboxes ?? []) {
|
||||
byDirectory.set(directory, project)
|
||||
}
|
||||
}
|
||||
|
||||
for (const [directory, expanded] of Object.entries(store.workspaceExpanded)) {
|
||||
if (!expanded) continue
|
||||
const project = projects.find((item) => item.worktree === directory || item.sandboxes?.includes(directory))
|
||||
const project = byDirectory.get(directory)
|
||||
if (!project) continue
|
||||
if (project.vcs === "git" && layout.sidebar.workspaces(project.worktree)()) continue
|
||||
setStore("workspaceExpanded", directory, false)
|
||||
@@ -1473,6 +1481,16 @@ export default function Layout(props: ParentProps) {
|
||||
globalSync.project.loadSessions(project.worktree)
|
||||
})
|
||||
|
||||
const projectIndex = createMemo(() => {
|
||||
const map = new Map<string, number>()
|
||||
const projects = layout.projects.list()
|
||||
for (let i = 0; i < projects.length; i++) {
|
||||
const project = projects[i]
|
||||
map.set(project.worktree, i)
|
||||
}
|
||||
return map
|
||||
})
|
||||
|
||||
function handleDragStart(event: unknown) {
|
||||
const id = getDraggableId(event)
|
||||
if (!id) return
|
||||
@@ -1483,10 +1501,10 @@ export default function Layout(props: ParentProps) {
|
||||
function handleDragOver(event: DragEvent) {
|
||||
const { draggable, droppable } = event
|
||||
if (draggable && droppable) {
|
||||
const projects = layout.projects.list()
|
||||
const fromIndex = projects.findIndex((p) => p.worktree === draggable.id.toString())
|
||||
const toIndex = projects.findIndex((p) => p.worktree === droppable.id.toString())
|
||||
if (fromIndex !== toIndex && toIndex !== -1) {
|
||||
const index = projectIndex()
|
||||
const fromIndex = index.get(draggable.id.toString())
|
||||
const toIndex = index.get(droppable.id.toString())
|
||||
if (fromIndex !== undefined && toIndex !== undefined && fromIndex !== toIndex) {
|
||||
layout.projects.move(draggable.id.toString(), toIndex)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -113,11 +113,16 @@ export const SessionItem = (props: SessionItemProps): JSX.Element => {
|
||||
})
|
||||
|
||||
const hoverMessages = createMemo(() =>
|
||||
sessionStore.message[props.session.id]?.filter((message) => message.role === "user"),
|
||||
props.hoverSession() === props.session.id
|
||||
? (sessionStore.message[props.session.id] ?? []).filter((message) => message.role === "user")
|
||||
: [],
|
||||
)
|
||||
const hoverReady = createMemo(() =>
|
||||
props.hoverSession() === props.session.id ? sessionStore.message[props.session.id] !== undefined : true,
|
||||
)
|
||||
const hoverReady = createMemo(() => sessionStore.message[props.session.id] !== undefined)
|
||||
const hoverAllowed = createMemo(() => !props.mobile && props.sidebarExpanded())
|
||||
const hoverEnabled = createMemo(() => (props.popover ?? true) && hoverAllowed())
|
||||
const hoverOpen = createMemo(() => props.hoverSession() === props.session.id)
|
||||
const isActive = createMemo(() => props.session.id === params.id)
|
||||
|
||||
const hoverPrefetch = { current: undefined as ReturnType<typeof setTimeout> | undefined }
|
||||
@@ -148,8 +153,6 @@ export const SessionItem = (props: SessionItemProps): JSX.Element => {
|
||||
class={`flex items-center justify-between gap-3 min-w-0 text-left w-full focus:outline-none transition-[padding] ${props.mobile ? "pr-7" : ""} group-hover/session:pr-7 group-focus-within/session:pr-7 group-active/session:pr-7 ${props.dense ? "py-0.5" : "py-1"}`}
|
||||
onPointerEnter={scheduleHoverPrefetch}
|
||||
onPointerLeave={cancelHoverPrefetch}
|
||||
onMouseEnter={scheduleHoverPrefetch}
|
||||
onMouseLeave={cancelHoverPrefetch}
|
||||
onFocus={() => props.prefetchSession(props.session, "high")}
|
||||
onClick={() => {
|
||||
props.setHoverSession(undefined)
|
||||
@@ -213,7 +216,7 @@ export const SessionItem = (props: SessionItemProps): JSX.Element => {
|
||||
shift={-2}
|
||||
trigger={item}
|
||||
mount={!props.mobile ? props.nav() : undefined}
|
||||
open={props.hoverSession() === props.session.id}
|
||||
open={hoverOpen()}
|
||||
onOpenChange={(open) => props.setHoverSession(open ? props.session.id : undefined)}
|
||||
>
|
||||
<Show
|
||||
|
||||
@@ -21,17 +21,66 @@ type Input = {
|
||||
export const pickVisibleId = (list: Visible[], line: number) => {
|
||||
if (list.length === 0) return
|
||||
|
||||
const sorted = [...list].sort((a, b) => {
|
||||
if (b.ratio !== a.ratio) return b.ratio - a.ratio
|
||||
let best = list[0]
|
||||
for (let i = 1; i < list.length; i++) {
|
||||
const next = list[i]
|
||||
if (next.ratio > best.ratio) {
|
||||
best = next
|
||||
continue
|
||||
}
|
||||
if (next.ratio < best.ratio) continue
|
||||
|
||||
const da = Math.abs(a.top - line)
|
||||
const db = Math.abs(b.top - line)
|
||||
if (da !== db) return da - db
|
||||
const bestDistance = Math.abs(best.top - line)
|
||||
const nextDistance = Math.abs(next.top - line)
|
||||
if (nextDistance < bestDistance) {
|
||||
best = next
|
||||
continue
|
||||
}
|
||||
if (nextDistance > bestDistance) continue
|
||||
|
||||
return a.top - b.top
|
||||
})
|
||||
if (next.top < best.top) best = next
|
||||
}
|
||||
|
||||
return sorted[0]?.id
|
||||
return best.id
|
||||
}
|
||||
|
||||
const pickVisibleMapId = (list: Map<string, { ratio: number; top: number }>, line: number) => {
|
||||
let bestID: string | undefined
|
||||
let bestRatio = 0
|
||||
let bestTop = 0
|
||||
|
||||
for (const [id, item] of list) {
|
||||
if (!bestID) {
|
||||
bestID = id
|
||||
bestRatio = item.ratio
|
||||
bestTop = item.top
|
||||
continue
|
||||
}
|
||||
|
||||
if (item.ratio > bestRatio) {
|
||||
bestID = id
|
||||
bestRatio = item.ratio
|
||||
bestTop = item.top
|
||||
continue
|
||||
}
|
||||
if (item.ratio < bestRatio) continue
|
||||
|
||||
const bestDistance = Math.abs(bestTop - line)
|
||||
const nextDistance = Math.abs(item.top - line)
|
||||
if (nextDistance < bestDistance) {
|
||||
bestID = id
|
||||
bestTop = item.top
|
||||
continue
|
||||
}
|
||||
if (nextDistance > bestDistance) continue
|
||||
|
||||
if (item.top < bestTop) {
|
||||
bestID = id
|
||||
bestTop = item.top
|
||||
}
|
||||
}
|
||||
|
||||
return bestID
|
||||
}
|
||||
|
||||
export const pickOffsetId = (list: Offset[], cutoff: number) => {
|
||||
@@ -107,16 +156,9 @@ export const createScrollSpy = (input: Input) => {
|
||||
const el = root
|
||||
if (!el) return
|
||||
|
||||
const line = el.getBoundingClientRect().top + 100
|
||||
const line = el.scrollTop + 100
|
||||
const next =
|
||||
pickVisibleId(
|
||||
[...visible].map(([k, v]) => ({
|
||||
id: k,
|
||||
ratio: v.ratio,
|
||||
top: v.top,
|
||||
})),
|
||||
line,
|
||||
) ??
|
||||
pickVisibleMapId(visible, line) ??
|
||||
(() => {
|
||||
if (dirty) refreshOffset()
|
||||
return pickOffsetId(offset, el.scrollTop + 100)
|
||||
@@ -148,9 +190,13 @@ export const createScrollSpy = (input: Input) => {
|
||||
continue
|
||||
}
|
||||
|
||||
const rootTop = entry.rootBounds?.top
|
||||
visible.set(key, {
|
||||
ratio: entry.intersectionRatio,
|
||||
top: entry.boundingClientRect.top,
|
||||
top:
|
||||
rootTop === undefined
|
||||
? entry.boundingClientRect.top - el.getBoundingClientRect().top + el.scrollTop
|
||||
: entry.boundingClientRect.top - rootTop + el.scrollTop,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -188,7 +234,7 @@ export const createScrollSpy = (input: Input) => {
|
||||
dirty = true
|
||||
schedule()
|
||||
})
|
||||
mo.observe(el, { subtree: true, childList: true, characterData: true })
|
||||
mo.observe(el, { subtree: true, childList: true })
|
||||
}
|
||||
|
||||
dirty = true
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { createEffect, on, onCleanup } from "solid-js"
|
||||
import { createEffect, createMemo, on, onCleanup } from "solid-js"
|
||||
import { UserMessage } from "@opencode-ai/sdk/v2"
|
||||
|
||||
export const messageIdFromHash = (hash: string) => {
|
||||
@@ -35,6 +35,16 @@ export const useSessionHashScroll = (input: {
|
||||
window.history.replaceState(null, "", `#${input.anchor(id)}`)
|
||||
}
|
||||
|
||||
const visible = createMemo(() => {
|
||||
const map = new Map<string, { message: UserMessage; index: number }>()
|
||||
const list = input.visibleUserMessages()
|
||||
for (let i = 0; i < list.length; i++) {
|
||||
const message = list[i]
|
||||
map.set(message.id, { message, index: i })
|
||||
}
|
||||
return map
|
||||
})
|
||||
|
||||
const scrollToElement = (el: HTMLElement, behavior: ScrollBehavior) => {
|
||||
const root = input.scroller()
|
||||
if (!root) return false
|
||||
@@ -49,9 +59,8 @@ export const useSessionHashScroll = (input: {
|
||||
const scrollToMessage = (message: UserMessage, behavior: ScrollBehavior = "smooth") => {
|
||||
input.setActiveMessage(message)
|
||||
|
||||
const msgs = input.visibleUserMessages()
|
||||
const index = msgs.findIndex((m) => m.id === message.id)
|
||||
if (index !== -1 && index < input.turnStart()) {
|
||||
const index = visible().get(message.id)?.index
|
||||
if (index !== undefined && index < input.turnStart()) {
|
||||
input.setTurnStart(index)
|
||||
input.scheduleTurnBackfill()
|
||||
|
||||
@@ -107,7 +116,7 @@ export const useSessionHashScroll = (input: {
|
||||
const messageId = messageIdFromHash(hash)
|
||||
if (messageId) {
|
||||
input.autoScroll.pause()
|
||||
const msg = input.visibleUserMessages().find((m) => m.id === messageId)
|
||||
const msg = visible().get(messageId)?.message
|
||||
if (msg) {
|
||||
scrollToMessage(msg, behavior)
|
||||
return
|
||||
@@ -144,14 +153,14 @@ export const useSessionHashScroll = (input: {
|
||||
createEffect(() => {
|
||||
if (!input.sessionID() || !input.messagesReady()) return
|
||||
|
||||
input.visibleUserMessages().length
|
||||
const lookup = visible()
|
||||
input.turnStart()
|
||||
|
||||
const targetId = input.pendingMessage() ?? messageIdFromHash(window.location.hash)
|
||||
if (!targetId) return
|
||||
if (input.currentMessageId() === targetId) return
|
||||
|
||||
const msg = input.visibleUserMessages().find((m) => m.id === targetId)
|
||||
const msg = lookup.get(targetId)?.message
|
||||
if (!msg) return
|
||||
|
||||
if (input.pendingMessage() === targetId) input.setPendingMessage(undefined)
|
||||
|
||||
Reference in New Issue
Block a user