mirror of
https://github.com/anomalyco/opencode.git
synced 2026-03-09 16:14:07 +00:00
Compare commits
8 Commits
jlongster/
...
dev
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f20ee2fad2 | ||
|
|
8b9710e56c | ||
|
|
c6262f9d40 | ||
|
|
b749fa90f2 | ||
|
|
8a51cbd253 | ||
|
|
399b8f0701 | ||
|
|
3742e42fdf | ||
|
|
0388ec6862 |
38
.github/workflows/storybook.yml
vendored
Normal file
38
.github/workflows/storybook.yml
vendored
Normal file
@@ -0,0 +1,38 @@
|
||||
name: storybook
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [dev]
|
||||
paths:
|
||||
- ".github/workflows/storybook.yml"
|
||||
- "package.json"
|
||||
- "bun.lock"
|
||||
- "packages/storybook/**"
|
||||
- "packages/ui/**"
|
||||
pull_request:
|
||||
branches: [dev]
|
||||
paths:
|
||||
- ".github/workflows/storybook.yml"
|
||||
- "package.json"
|
||||
- "bun.lock"
|
||||
- "packages/storybook/**"
|
||||
- "packages/ui/**"
|
||||
workflow_dispatch:
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: storybook build
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Bun
|
||||
uses: ./.github/actions/setup-bun
|
||||
|
||||
- name: Build Storybook
|
||||
run: bun --cwd packages/storybook build
|
||||
@@ -5,6 +5,11 @@
|
||||
"options": {},
|
||||
},
|
||||
},
|
||||
"permission": {
|
||||
"edit": {
|
||||
"packages/opencode/migration/*": "deny",
|
||||
},
|
||||
},
|
||||
"mcp": {},
|
||||
"tools": {
|
||||
"github-triage": false,
|
||||
|
||||
32
bun.lock
32
bun.lock
@@ -26,7 +26,7 @@
|
||||
},
|
||||
"packages/app": {
|
||||
"name": "@opencode-ai/app",
|
||||
"version": "1.2.23",
|
||||
"version": "1.2.24",
|
||||
"dependencies": {
|
||||
"@kobalte/core": "catalog:",
|
||||
"@opencode-ai/sdk": "workspace:*",
|
||||
@@ -76,7 +76,7 @@
|
||||
},
|
||||
"packages/console/app": {
|
||||
"name": "@opencode-ai/console-app",
|
||||
"version": "1.2.23",
|
||||
"version": "1.2.24",
|
||||
"dependencies": {
|
||||
"@cloudflare/vite-plugin": "1.15.2",
|
||||
"@ibm/plex": "6.4.1",
|
||||
@@ -110,7 +110,7 @@
|
||||
},
|
||||
"packages/console/core": {
|
||||
"name": "@opencode-ai/console-core",
|
||||
"version": "1.2.23",
|
||||
"version": "1.2.24",
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-sts": "3.782.0",
|
||||
"@jsx-email/render": "1.1.1",
|
||||
@@ -137,7 +137,7 @@
|
||||
},
|
||||
"packages/console/function": {
|
||||
"name": "@opencode-ai/console-function",
|
||||
"version": "1.2.23",
|
||||
"version": "1.2.24",
|
||||
"dependencies": {
|
||||
"@ai-sdk/anthropic": "2.0.0",
|
||||
"@ai-sdk/openai": "2.0.2",
|
||||
@@ -161,7 +161,7 @@
|
||||
},
|
||||
"packages/console/mail": {
|
||||
"name": "@opencode-ai/console-mail",
|
||||
"version": "1.2.23",
|
||||
"version": "1.2.24",
|
||||
"dependencies": {
|
||||
"@jsx-email/all": "2.2.3",
|
||||
"@jsx-email/cli": "1.4.3",
|
||||
@@ -185,7 +185,7 @@
|
||||
},
|
||||
"packages/desktop": {
|
||||
"name": "@opencode-ai/desktop",
|
||||
"version": "1.2.23",
|
||||
"version": "1.2.24",
|
||||
"dependencies": {
|
||||
"@opencode-ai/app": "workspace:*",
|
||||
"@opencode-ai/ui": "workspace:*",
|
||||
@@ -218,7 +218,7 @@
|
||||
},
|
||||
"packages/desktop-electron": {
|
||||
"name": "@opencode-ai/desktop-electron",
|
||||
"version": "1.2.23",
|
||||
"version": "1.2.24",
|
||||
"dependencies": {
|
||||
"@opencode-ai/app": "workspace:*",
|
||||
"@opencode-ai/ui": "workspace:*",
|
||||
@@ -248,7 +248,7 @@
|
||||
},
|
||||
"packages/enterprise": {
|
||||
"name": "@opencode-ai/enterprise",
|
||||
"version": "1.2.23",
|
||||
"version": "1.2.24",
|
||||
"dependencies": {
|
||||
"@opencode-ai/ui": "workspace:*",
|
||||
"@opencode-ai/util": "workspace:*",
|
||||
@@ -277,7 +277,7 @@
|
||||
},
|
||||
"packages/function": {
|
||||
"name": "@opencode-ai/function",
|
||||
"version": "1.2.23",
|
||||
"version": "1.2.24",
|
||||
"dependencies": {
|
||||
"@octokit/auth-app": "8.0.1",
|
||||
"@octokit/rest": "catalog:",
|
||||
@@ -293,7 +293,7 @@
|
||||
},
|
||||
"packages/opencode": {
|
||||
"name": "opencode",
|
||||
"version": "1.2.23",
|
||||
"version": "1.2.24",
|
||||
"bin": {
|
||||
"opencode": "./bin/opencode",
|
||||
},
|
||||
@@ -409,7 +409,7 @@
|
||||
},
|
||||
"packages/plugin": {
|
||||
"name": "@opencode-ai/plugin",
|
||||
"version": "1.2.23",
|
||||
"version": "1.2.24",
|
||||
"dependencies": {
|
||||
"@opencode-ai/sdk": "workspace:*",
|
||||
"zod": "catalog:",
|
||||
@@ -429,7 +429,7 @@
|
||||
},
|
||||
"packages/sdk/js": {
|
||||
"name": "@opencode-ai/sdk",
|
||||
"version": "1.2.23",
|
||||
"version": "1.2.24",
|
||||
"devDependencies": {
|
||||
"@hey-api/openapi-ts": "0.90.10",
|
||||
"@tsconfig/node22": "catalog:",
|
||||
@@ -440,7 +440,7 @@
|
||||
},
|
||||
"packages/slack": {
|
||||
"name": "@opencode-ai/slack",
|
||||
"version": "1.2.23",
|
||||
"version": "1.2.24",
|
||||
"dependencies": {
|
||||
"@opencode-ai/sdk": "workspace:*",
|
||||
"@slack/bolt": "^3.17.1",
|
||||
@@ -475,7 +475,7 @@
|
||||
},
|
||||
"packages/ui": {
|
||||
"name": "@opencode-ai/ui",
|
||||
"version": "1.2.23",
|
||||
"version": "1.2.24",
|
||||
"dependencies": {
|
||||
"@kobalte/core": "catalog:",
|
||||
"@opencode-ai/sdk": "workspace:*",
|
||||
@@ -521,7 +521,7 @@
|
||||
},
|
||||
"packages/util": {
|
||||
"name": "@opencode-ai/util",
|
||||
"version": "1.2.23",
|
||||
"version": "1.2.24",
|
||||
"dependencies": {
|
||||
"zod": "catalog:",
|
||||
},
|
||||
@@ -532,7 +532,7 @@
|
||||
},
|
||||
"packages/web": {
|
||||
"name": "@opencode-ai/web",
|
||||
"version": "1.2.23",
|
||||
"version": "1.2.24",
|
||||
"dependencies": {
|
||||
"@astrojs/cloudflare": "12.6.3",
|
||||
"@astrojs/markdown-remark": "6.3.1",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/app",
|
||||
"version": "1.2.23",
|
||||
"version": "1.2.24",
|
||||
"description": "",
|
||||
"type": "module",
|
||||
"exports": {
|
||||
|
||||
@@ -424,6 +424,17 @@ export default function Layout(props: ParentProps) {
|
||||
return
|
||||
}
|
||||
|
||||
if (
|
||||
e.details?.type === "question.replied" ||
|
||||
e.details?.type === "question.rejected" ||
|
||||
e.details?.type === "permission.replied"
|
||||
) {
|
||||
const props = e.details.properties as { sessionID: string }
|
||||
const sessionKey = `${e.name}:${props.sessionID}`
|
||||
dismissSessionAlert(sessionKey)
|
||||
return
|
||||
}
|
||||
|
||||
if (e.details?.type !== "permission.asked" && e.details?.type !== "question.asked") return
|
||||
const title =
|
||||
e.details.type === "permission.asked"
|
||||
|
||||
@@ -37,7 +37,6 @@ import { createOpenReviewFile, createSizing } from "@/pages/session/helpers"
|
||||
import { MessageTimeline } from "@/pages/session/message-timeline"
|
||||
import { type DiffStyle, SessionReviewTab, type SessionReviewTabProps } from "@/pages/session/review-tab"
|
||||
import { resetSessionModel, syncSessionModel } from "@/pages/session/session-model-helpers"
|
||||
import { createScrollSpy } from "@/pages/session/scroll-spy"
|
||||
import { SessionMobileTabs } from "@/pages/session/session-mobile-tabs"
|
||||
import { SessionSidePanel } from "@/pages/session/session-side-panel"
|
||||
import { TerminalPanel } from "@/pages/session/terminal-panel"
|
||||
@@ -486,20 +485,49 @@ export default function Page() {
|
||||
return "main"
|
||||
})
|
||||
|
||||
const activeMessage = createMemo(() => {
|
||||
if (!store.messageId) return lastUserMessage()
|
||||
const found = visibleUserMessages()?.find((m) => m.id === store.messageId)
|
||||
return found ?? lastUserMessage()
|
||||
})
|
||||
const setActiveMessage = (message: UserMessage | undefined) => {
|
||||
messageMark = scrollMark
|
||||
setStore("messageId", message?.id)
|
||||
}
|
||||
|
||||
const anchor = (id: string) => `message-${id}`
|
||||
|
||||
const cursor = () => {
|
||||
const root = scroller
|
||||
if (!root) return store.messageId
|
||||
|
||||
const box = root.getBoundingClientRect()
|
||||
const line = box.top + 100
|
||||
const list = [...root.querySelectorAll<HTMLElement>("[data-message-id]")]
|
||||
.map((el) => {
|
||||
const id = el.dataset.messageId
|
||||
if (!id) return
|
||||
|
||||
const rect = el.getBoundingClientRect()
|
||||
return { id, top: rect.top, bottom: rect.bottom }
|
||||
})
|
||||
.filter((item): item is { id: string; top: number; bottom: number } => !!item)
|
||||
|
||||
const shown = list.filter((item) => item.bottom > box.top && item.top < box.bottom)
|
||||
const hit = shown.find((item) => item.top <= line && item.bottom >= line)
|
||||
if (hit) return hit.id
|
||||
|
||||
const near = [...shown].sort((a, b) => {
|
||||
const da = Math.abs(a.top - line)
|
||||
const db = Math.abs(b.top - line)
|
||||
if (da !== db) return da - db
|
||||
return a.top - b.top
|
||||
})[0]
|
||||
if (near) return near.id
|
||||
|
||||
return list.filter((item) => item.top <= line).at(-1)?.id ?? list[0]?.id ?? store.messageId
|
||||
}
|
||||
|
||||
function navigateMessageByOffset(offset: number) {
|
||||
const msgs = visibleUserMessages()
|
||||
if (msgs.length === 0) return
|
||||
|
||||
const current = store.messageId
|
||||
const current = store.messageId && messageMark === scrollMark ? store.messageId : cursor()
|
||||
const base = current ? msgs.findIndex((m) => m.id === current) : msgs.length
|
||||
const currentIndex = base === -1 ? msgs.length : base
|
||||
const targetIndex = currentIndex + offset
|
||||
@@ -572,6 +600,8 @@ export default function Page() {
|
||||
let dockHeight = 0
|
||||
let scroller: HTMLDivElement | undefined
|
||||
let content: HTMLDivElement | undefined
|
||||
let scrollMark = 0
|
||||
let messageMark = 0
|
||||
|
||||
const scrollGestureWindowMs = 250
|
||||
|
||||
@@ -616,6 +646,7 @@ export default function Page() {
|
||||
() => {
|
||||
setStore("messageId", undefined)
|
||||
setStore("changes", "session")
|
||||
setUi("pendingMessage", undefined)
|
||||
},
|
||||
{ defer: true },
|
||||
),
|
||||
@@ -1110,12 +1141,6 @@ export default function Page() {
|
||||
|
||||
let scrollStateFrame: number | undefined
|
||||
let scrollStateTarget: HTMLDivElement | undefined
|
||||
const scrollSpy = createScrollSpy({
|
||||
onActive: (id) => {
|
||||
if (id === store.messageId) return
|
||||
setStore("messageId", id)
|
||||
},
|
||||
})
|
||||
|
||||
const updateScrollState = (el: HTMLDivElement) => {
|
||||
const max = el.scrollHeight - el.clientHeight
|
||||
@@ -1163,31 +1188,21 @@ export default function Page() {
|
||||
),
|
||||
)
|
||||
|
||||
createEffect(
|
||||
on(
|
||||
sessionKey,
|
||||
() => {
|
||||
scrollSpy.clear()
|
||||
},
|
||||
{ defer: true },
|
||||
),
|
||||
)
|
||||
|
||||
const anchor = (id: string) => `message-${id}`
|
||||
|
||||
const setScrollRef = (el: HTMLDivElement | undefined) => {
|
||||
scroller = el
|
||||
autoScroll.scrollRef(el)
|
||||
scrollSpy.setContainer(el)
|
||||
if (el) scheduleScrollState(el)
|
||||
}
|
||||
|
||||
const markUserScroll = () => {
|
||||
scrollMark += 1
|
||||
}
|
||||
|
||||
createResizeObserver(
|
||||
() => content,
|
||||
() => {
|
||||
const el = scroller
|
||||
if (el) scheduleScrollState(el)
|
||||
scrollSpy.markDirty()
|
||||
},
|
||||
)
|
||||
|
||||
@@ -1220,7 +1235,6 @@ export default function Page() {
|
||||
if (stick) autoScroll.forceScrollToBottom()
|
||||
|
||||
if (el) scheduleScrollState(el)
|
||||
scrollSpy.markDirty()
|
||||
},
|
||||
)
|
||||
|
||||
@@ -1248,7 +1262,6 @@ export default function Page() {
|
||||
|
||||
onCleanup(() => {
|
||||
document.removeEventListener("keydown", handleKeyDown)
|
||||
scrollSpy.destroy()
|
||||
if (reviewFrame !== undefined) cancelAnimationFrame(reviewFrame)
|
||||
if (scrollStateFrame !== undefined) cancelAnimationFrame(scrollStateFrame)
|
||||
})
|
||||
@@ -1280,7 +1293,7 @@ export default function Page() {
|
||||
<div class="flex-1 min-h-0 overflow-hidden">
|
||||
<Switch>
|
||||
<Match when={params.id}>
|
||||
<Show when={activeMessage()}>
|
||||
<Show when={lastUserMessage()}>
|
||||
<MessageTimeline
|
||||
mobileChanges={mobileChanges()}
|
||||
mobileFallback={reviewContent({
|
||||
@@ -1300,8 +1313,7 @@ export default function Page() {
|
||||
onAutoScrollHandleScroll={autoScroll.handleScroll}
|
||||
onMarkScrollGesture={markScrollGesture}
|
||||
hasScrollGesture={hasScrollGesture}
|
||||
isDesktop={isDesktop()}
|
||||
onScrollSpyScroll={scrollSpy.onScroll}
|
||||
onUserScroll={markUserScroll}
|
||||
onTurnBackfillScroll={historyWindow.onScrollerScroll}
|
||||
onAutoScrollInteraction={autoScroll.handleInteraction}
|
||||
centered={centered()}
|
||||
@@ -1320,8 +1332,6 @@ export default function Page() {
|
||||
}}
|
||||
renderedUserMessages={historyWindow.renderedUserMessages()}
|
||||
anchor={anchor}
|
||||
onRegisterMessage={scrollSpy.register}
|
||||
onUnregisterMessage={scrollSpy.unregister}
|
||||
/>
|
||||
</Show>
|
||||
</Match>
|
||||
|
||||
@@ -8,6 +8,7 @@ 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 type { AssistantMessage, Message as MessageType, Part, TextPart, UserMessage } from "@opencode-ai/sdk/v2"
|
||||
@@ -192,8 +193,7 @@ export function MessageTimeline(props: {
|
||||
onAutoScrollHandleScroll: () => void
|
||||
onMarkScrollGesture: (target?: EventTarget | null) => void
|
||||
hasScrollGesture: () => boolean
|
||||
isDesktop: boolean
|
||||
onScrollSpyScroll: () => void
|
||||
onUserScroll: () => void
|
||||
onTurnBackfillScroll: () => void
|
||||
onAutoScrollInteraction: (event: MouseEvent) => void
|
||||
centered: boolean
|
||||
@@ -204,8 +204,6 @@ export function MessageTimeline(props: {
|
||||
onLoadEarlier: () => void
|
||||
renderedUserMessages: UserMessage[]
|
||||
anchor: (id: string) => string
|
||||
onRegisterMessage: (el: HTMLDivElement, id: string) => void
|
||||
onUnregisterMessage: (id: string) => void
|
||||
}) {
|
||||
let touchGesture: number | undefined
|
||||
|
||||
@@ -235,6 +233,40 @@ export function MessageTimeline(props: {
|
||||
if (!id) return idle
|
||||
return sync.data.session_status[id] ?? idle
|
||||
})
|
||||
const working = createMemo(() => !!pending() || sessionStatus().type !== "idle")
|
||||
|
||||
const [slot, setSlot] = createStore({
|
||||
open: false,
|
||||
show: false,
|
||||
fade: false,
|
||||
})
|
||||
|
||||
let f: number | undefined
|
||||
const clear = () => {
|
||||
if (f !== undefined) window.clearTimeout(f)
|
||||
f = undefined
|
||||
}
|
||||
|
||||
onCleanup(clear)
|
||||
createEffect(
|
||||
on(
|
||||
working,
|
||||
(on, prev) => {
|
||||
clear()
|
||||
if (on) {
|
||||
setSlot({ open: true, show: true, fade: false })
|
||||
return
|
||||
}
|
||||
if (prev) {
|
||||
setSlot({ open: false, show: true, fade: true })
|
||||
f = window.setTimeout(() => setSlot({ show: false, fade: false }), 260)
|
||||
return
|
||||
}
|
||||
setSlot({ open: false, show: false, fade: false })
|
||||
},
|
||||
{ defer: true },
|
||||
),
|
||||
)
|
||||
const activeMessageID = createMemo(() => {
|
||||
const parentID = pending()?.parentID
|
||||
if (parentID) {
|
||||
@@ -539,9 +571,9 @@ export function MessageTimeline(props: {
|
||||
props.onScheduleScrollState(e.currentTarget)
|
||||
props.onTurnBackfillScroll()
|
||||
if (!props.hasScrollGesture()) return
|
||||
props.onUserScroll()
|
||||
props.onAutoScrollHandleScroll()
|
||||
props.onMarkScrollGesture(e.currentTarget)
|
||||
if (props.isDesktop) props.onScrollSpyScroll()
|
||||
}}
|
||||
onClick={props.onAutoScrollInteraction}
|
||||
class="relative min-w-0 w-full h-full"
|
||||
@@ -573,43 +605,64 @@ export function MessageTimeline(props: {
|
||||
aria-label={language.t("common.goBack")}
|
||||
/>
|
||||
</Show>
|
||||
<Show when={titleValue() || title.editing}>
|
||||
<Show
|
||||
when={title.editing}
|
||||
fallback={
|
||||
<h1
|
||||
class="text-14-medium text-text-strong truncate grow-1 min-w-0 pl-2"
|
||||
onDblClick={openTitleEditor}
|
||||
>
|
||||
{titleValue()}
|
||||
</h1>
|
||||
}
|
||||
<div class="flex items-center min-w-0 grow-1">
|
||||
<div
|
||||
class="shrink-0 flex items-center justify-center overflow-hidden transition-[width,margin] duration-300 ease-[cubic-bezier(0.22,1,0.36,1)]"
|
||||
style={{
|
||||
width: slot.open ? "16px" : "0px",
|
||||
"margin-right": slot.open ? "8px" : "0px",
|
||||
}}
|
||||
aria-hidden="true"
|
||||
>
|
||||
<InlineInput
|
||||
ref={(el) => {
|
||||
titleRef = el
|
||||
}}
|
||||
value={title.draft}
|
||||
disabled={title.saving}
|
||||
class="text-14-medium text-text-strong grow-1 min-w-0 pl-2 rounded-[6px]"
|
||||
style={{ "--inline-input-shadow": "var(--shadow-xs-border-select)" }}
|
||||
onInput={(event) => setTitle("draft", event.currentTarget.value)}
|
||||
onKeyDown={(event) => {
|
||||
event.stopPropagation()
|
||||
if (event.key === "Enter") {
|
||||
event.preventDefault()
|
||||
void saveTitleEditor()
|
||||
return
|
||||
}
|
||||
if (event.key === "Escape") {
|
||||
event.preventDefault()
|
||||
closeTitleEditor()
|
||||
}
|
||||
}}
|
||||
onBlur={closeTitleEditor}
|
||||
/>
|
||||
<Show when={slot.show}>
|
||||
<div
|
||||
class="transition-opacity duration-200 ease-out"
|
||||
classList={{
|
||||
"opacity-0": slot.fade,
|
||||
}}
|
||||
>
|
||||
<Spinner class="size-4" style={{ color: "var(--icon-interactive-base)" }} />
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
<Show when={titleValue() || title.editing}>
|
||||
<Show
|
||||
when={title.editing}
|
||||
fallback={
|
||||
<h1
|
||||
class="text-14-medium text-text-strong truncate grow-1 min-w-0"
|
||||
onDblClick={openTitleEditor}
|
||||
>
|
||||
{titleValue()}
|
||||
</h1>
|
||||
}
|
||||
>
|
||||
<InlineInput
|
||||
ref={(el) => {
|
||||
titleRef = el
|
||||
}}
|
||||
value={title.draft}
|
||||
disabled={title.saving}
|
||||
class="text-14-medium text-text-strong grow-1 min-w-0 rounded-[6px]"
|
||||
style={{ "--inline-input-shadow": "var(--shadow-xs-border-select)" }}
|
||||
onInput={(event) => setTitle("draft", event.currentTarget.value)}
|
||||
onKeyDown={(event) => {
|
||||
event.stopPropagation()
|
||||
if (event.key === "Enter") {
|
||||
event.preventDefault()
|
||||
void saveTitleEditor()
|
||||
return
|
||||
}
|
||||
if (event.key === "Escape") {
|
||||
event.preventDefault()
|
||||
closeTitleEditor()
|
||||
}
|
||||
}}
|
||||
onBlur={closeTitleEditor}
|
||||
/>
|
||||
</Show>
|
||||
</Show>
|
||||
</Show>
|
||||
</div>
|
||||
</div>
|
||||
<Show when={sessionID()}>
|
||||
{(id) => (
|
||||
@@ -707,10 +760,6 @@ export function MessageTimeline(props: {
|
||||
<div
|
||||
id={props.anchor(messageID)}
|
||||
data-message-id={messageID}
|
||||
ref={(el) => {
|
||||
props.onRegisterMessage(el, messageID)
|
||||
onCleanup(() => props.onUnregisterMessage(messageID))
|
||||
}}
|
||||
classList={{
|
||||
"min-w-0 w-full max-w-full": true,
|
||||
"md:max-w-200 2xl:max-w-[1000px]": props.centered,
|
||||
|
||||
@@ -1,127 +0,0 @@
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import { createScrollSpy, pickOffsetId, pickVisibleId } from "./scroll-spy"
|
||||
|
||||
const rect = (top: number, height = 80): DOMRect =>
|
||||
({
|
||||
x: 0,
|
||||
y: top,
|
||||
top,
|
||||
left: 0,
|
||||
right: 800,
|
||||
bottom: top + height,
|
||||
width: 800,
|
||||
height,
|
||||
toJSON: () => ({}),
|
||||
}) as DOMRect
|
||||
|
||||
const setRect = (el: Element, top: number, height = 80) => {
|
||||
Object.defineProperty(el, "getBoundingClientRect", {
|
||||
configurable: true,
|
||||
value: () => rect(top, height),
|
||||
})
|
||||
}
|
||||
|
||||
describe("pickVisibleId", () => {
|
||||
test("prefers higher intersection ratio", () => {
|
||||
const id = pickVisibleId(
|
||||
[
|
||||
{ id: "a", ratio: 0.2, top: 100 },
|
||||
{ id: "b", ratio: 0.8, top: 300 },
|
||||
],
|
||||
120,
|
||||
)
|
||||
|
||||
expect(id).toBe("b")
|
||||
})
|
||||
|
||||
test("breaks ratio ties by nearest line", () => {
|
||||
const id = pickVisibleId(
|
||||
[
|
||||
{ id: "a", ratio: 0.5, top: 90 },
|
||||
{ id: "b", ratio: 0.5, top: 140 },
|
||||
],
|
||||
130,
|
||||
)
|
||||
|
||||
expect(id).toBe("b")
|
||||
})
|
||||
})
|
||||
|
||||
describe("pickOffsetId", () => {
|
||||
test("uses binary search cutoff", () => {
|
||||
const id = pickOffsetId(
|
||||
[
|
||||
{ id: "a", top: 0 },
|
||||
{ id: "b", top: 200 },
|
||||
{ id: "c", top: 400 },
|
||||
],
|
||||
350,
|
||||
)
|
||||
|
||||
expect(id).toBe("b")
|
||||
})
|
||||
})
|
||||
|
||||
describe("createScrollSpy fallback", () => {
|
||||
test("tracks active id from offsets and dirty refresh", () => {
|
||||
const active: string[] = []
|
||||
const root = document.createElement("div") as HTMLDivElement
|
||||
const one = document.createElement("div")
|
||||
const two = document.createElement("div")
|
||||
const three = document.createElement("div")
|
||||
|
||||
root.append(one, two, three)
|
||||
document.body.append(root)
|
||||
|
||||
Object.defineProperty(root, "scrollTop", { configurable: true, writable: true, value: 250 })
|
||||
setRect(root, 0, 800)
|
||||
setRect(one, -250)
|
||||
setRect(two, -50)
|
||||
setRect(three, 150)
|
||||
|
||||
const queue: FrameRequestCallback[] = []
|
||||
const flush = () => {
|
||||
const run = [...queue]
|
||||
queue.length = 0
|
||||
for (const cb of run) cb(0)
|
||||
}
|
||||
|
||||
const spy = createScrollSpy({
|
||||
onActive: (id) => active.push(id),
|
||||
raf: (cb) => (queue.push(cb), queue.length),
|
||||
caf: () => {},
|
||||
IntersectionObserver: undefined,
|
||||
ResizeObserver: undefined,
|
||||
MutationObserver: undefined,
|
||||
})
|
||||
|
||||
spy.setContainer(root)
|
||||
spy.register(one, "a")
|
||||
spy.register(two, "b")
|
||||
spy.register(three, "c")
|
||||
spy.onScroll()
|
||||
flush()
|
||||
|
||||
expect(spy.getActiveId()).toBe("b")
|
||||
expect(active.at(-1)).toBe("b")
|
||||
|
||||
root.scrollTop = 450
|
||||
setRect(one, -450)
|
||||
setRect(two, -250)
|
||||
setRect(three, -50)
|
||||
spy.onScroll()
|
||||
flush()
|
||||
expect(spy.getActiveId()).toBe("c")
|
||||
|
||||
root.scrollTop = 250
|
||||
setRect(one, -250)
|
||||
setRect(two, 250)
|
||||
setRect(three, 150)
|
||||
spy.markDirty()
|
||||
spy.onScroll()
|
||||
flush()
|
||||
expect(spy.getActiveId()).toBe("a")
|
||||
|
||||
spy.destroy()
|
||||
})
|
||||
})
|
||||
@@ -1,275 +0,0 @@
|
||||
type Visible = {
|
||||
id: string
|
||||
ratio: number
|
||||
top: number
|
||||
}
|
||||
|
||||
type Offset = {
|
||||
id: string
|
||||
top: number
|
||||
}
|
||||
|
||||
type Input = {
|
||||
onActive: (id: string) => void
|
||||
raf?: (cb: FrameRequestCallback) => number
|
||||
caf?: (id: number) => void
|
||||
IntersectionObserver?: typeof globalThis.IntersectionObserver
|
||||
ResizeObserver?: typeof globalThis.ResizeObserver
|
||||
MutationObserver?: typeof globalThis.MutationObserver
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
const da = Math.abs(a.top - line)
|
||||
const db = Math.abs(b.top - line)
|
||||
if (da !== db) return da - db
|
||||
|
||||
return a.top - b.top
|
||||
})
|
||||
|
||||
return sorted[0]?.id
|
||||
}
|
||||
|
||||
export const pickOffsetId = (list: Offset[], cutoff: number) => {
|
||||
if (list.length === 0) return
|
||||
|
||||
let lo = 0
|
||||
let hi = list.length - 1
|
||||
let out = 0
|
||||
|
||||
while (lo <= hi) {
|
||||
const mid = (lo + hi) >> 1
|
||||
const top = list[mid]?.top
|
||||
if (top === undefined) break
|
||||
|
||||
if (top <= cutoff) {
|
||||
out = mid
|
||||
lo = mid + 1
|
||||
continue
|
||||
}
|
||||
|
||||
hi = mid - 1
|
||||
}
|
||||
|
||||
return list[out]?.id
|
||||
}
|
||||
|
||||
export const createScrollSpy = (input: Input) => {
|
||||
const raf = input.raf ?? requestAnimationFrame
|
||||
const caf = input.caf ?? cancelAnimationFrame
|
||||
const CtorIO = input.IntersectionObserver ?? globalThis.IntersectionObserver
|
||||
const CtorRO = input.ResizeObserver ?? globalThis.ResizeObserver
|
||||
const CtorMO = input.MutationObserver ?? globalThis.MutationObserver
|
||||
|
||||
let root: HTMLDivElement | undefined
|
||||
let io: IntersectionObserver | undefined
|
||||
let ro: ResizeObserver | undefined
|
||||
let mo: MutationObserver | undefined
|
||||
let frame: number | undefined
|
||||
let active: string | undefined
|
||||
let dirty = true
|
||||
|
||||
const node = new Map<string, HTMLElement>()
|
||||
const id = new WeakMap<HTMLElement, string>()
|
||||
const visible = new Map<string, { ratio: number; top: number }>()
|
||||
let offset: Offset[] = []
|
||||
|
||||
const schedule = () => {
|
||||
if (frame !== undefined) return
|
||||
frame = raf(() => {
|
||||
frame = undefined
|
||||
update()
|
||||
})
|
||||
}
|
||||
|
||||
const refreshOffset = () => {
|
||||
const el = root
|
||||
if (!el) {
|
||||
offset = []
|
||||
dirty = false
|
||||
return
|
||||
}
|
||||
|
||||
const base = el.getBoundingClientRect().top
|
||||
offset = [...node].map(([next, item]) => ({
|
||||
id: next,
|
||||
top: item.getBoundingClientRect().top - base + el.scrollTop,
|
||||
}))
|
||||
offset.sort((a, b) => a.top - b.top)
|
||||
dirty = false
|
||||
}
|
||||
|
||||
const update = () => {
|
||||
const el = root
|
||||
if (!el) return
|
||||
|
||||
const line = el.getBoundingClientRect().top + 100
|
||||
const next =
|
||||
pickVisibleId(
|
||||
[...visible].map(([k, v]) => ({
|
||||
id: k,
|
||||
ratio: v.ratio,
|
||||
top: v.top,
|
||||
})),
|
||||
line,
|
||||
) ??
|
||||
(() => {
|
||||
if (dirty) refreshOffset()
|
||||
return pickOffsetId(offset, el.scrollTop + 100)
|
||||
})()
|
||||
|
||||
if (!next || next === active) return
|
||||
active = next
|
||||
input.onActive(next)
|
||||
}
|
||||
|
||||
const observe = () => {
|
||||
const el = root
|
||||
if (!el) return
|
||||
|
||||
io?.disconnect()
|
||||
io = undefined
|
||||
if (CtorIO) {
|
||||
try {
|
||||
io = new CtorIO(
|
||||
(entries) => {
|
||||
for (const entry of entries) {
|
||||
const item = entry.target
|
||||
if (!(item instanceof HTMLElement)) continue
|
||||
const key = id.get(item)
|
||||
if (!key) continue
|
||||
|
||||
if (!entry.isIntersecting || entry.intersectionRatio <= 0) {
|
||||
visible.delete(key)
|
||||
continue
|
||||
}
|
||||
|
||||
visible.set(key, {
|
||||
ratio: entry.intersectionRatio,
|
||||
top: entry.boundingClientRect.top,
|
||||
})
|
||||
}
|
||||
|
||||
schedule()
|
||||
},
|
||||
{
|
||||
root: el,
|
||||
threshold: [0, 0.25, 0.5, 0.75, 1],
|
||||
},
|
||||
)
|
||||
} catch {
|
||||
io = undefined
|
||||
}
|
||||
}
|
||||
|
||||
if (io) {
|
||||
for (const item of node.values()) io.observe(item)
|
||||
}
|
||||
|
||||
ro?.disconnect()
|
||||
ro = undefined
|
||||
if (CtorRO) {
|
||||
ro = new CtorRO(() => {
|
||||
dirty = true
|
||||
schedule()
|
||||
})
|
||||
ro.observe(el)
|
||||
for (const item of node.values()) ro.observe(item)
|
||||
}
|
||||
|
||||
mo?.disconnect()
|
||||
mo = undefined
|
||||
if (CtorMO) {
|
||||
mo = new CtorMO(() => {
|
||||
dirty = true
|
||||
schedule()
|
||||
})
|
||||
mo.observe(el, { subtree: true, childList: true, characterData: true })
|
||||
}
|
||||
|
||||
dirty = true
|
||||
schedule()
|
||||
}
|
||||
|
||||
const setContainer = (el?: HTMLDivElement) => {
|
||||
if (root === el) return
|
||||
|
||||
root = el
|
||||
visible.clear()
|
||||
active = undefined
|
||||
observe()
|
||||
}
|
||||
|
||||
const register = (el: HTMLElement, key: string) => {
|
||||
const prev = node.get(key)
|
||||
if (prev && prev !== el) {
|
||||
io?.unobserve(prev)
|
||||
ro?.unobserve(prev)
|
||||
}
|
||||
|
||||
node.set(key, el)
|
||||
id.set(el, key)
|
||||
if (io) io.observe(el)
|
||||
if (ro) ro.observe(el)
|
||||
dirty = true
|
||||
schedule()
|
||||
}
|
||||
|
||||
const unregister = (key: string) => {
|
||||
const item = node.get(key)
|
||||
if (!item) return
|
||||
|
||||
io?.unobserve(item)
|
||||
ro?.unobserve(item)
|
||||
node.delete(key)
|
||||
visible.delete(key)
|
||||
dirty = true
|
||||
schedule()
|
||||
}
|
||||
|
||||
const markDirty = () => {
|
||||
dirty = true
|
||||
schedule()
|
||||
}
|
||||
|
||||
const clear = () => {
|
||||
for (const item of node.values()) {
|
||||
io?.unobserve(item)
|
||||
ro?.unobserve(item)
|
||||
}
|
||||
|
||||
node.clear()
|
||||
visible.clear()
|
||||
offset = []
|
||||
active = undefined
|
||||
dirty = true
|
||||
}
|
||||
|
||||
const destroy = () => {
|
||||
if (frame !== undefined) caf(frame)
|
||||
frame = undefined
|
||||
clear()
|
||||
io?.disconnect()
|
||||
ro?.disconnect()
|
||||
mo?.disconnect()
|
||||
io = undefined
|
||||
ro = undefined
|
||||
mo = undefined
|
||||
root = undefined
|
||||
}
|
||||
|
||||
return {
|
||||
setContainer,
|
||||
register,
|
||||
unregister,
|
||||
onScroll: schedule,
|
||||
markDirty,
|
||||
clear,
|
||||
destroy,
|
||||
getActiveId: () => active,
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { UserMessage } from "@opencode-ai/sdk/v2"
|
||||
import { useLocation, useNavigate } from "@solidjs/router"
|
||||
import { createEffect, createMemo, onMount } from "solid-js"
|
||||
import { createEffect, createMemo, onCleanup, onMount } from "solid-js"
|
||||
import { messageIdFromHash } from "./message-id-from-hash"
|
||||
|
||||
export { messageIdFromHash } from "./message-id-from-hash"
|
||||
@@ -26,17 +26,38 @@ export const useSessionHashScroll = (input: {
|
||||
const messageById = createMemo(() => new Map(visibleUserMessages().map((m) => [m.id, m])))
|
||||
const messageIndex = createMemo(() => new Map(visibleUserMessages().map((m, i) => [m.id, i])))
|
||||
let pendingKey = ""
|
||||
let clearing = false
|
||||
|
||||
const location = useLocation()
|
||||
const navigate = useNavigate()
|
||||
|
||||
const frames = new Set<number>()
|
||||
const queue = (fn: () => void) => {
|
||||
const id = requestAnimationFrame(() => {
|
||||
frames.delete(id)
|
||||
fn()
|
||||
})
|
||||
frames.add(id)
|
||||
}
|
||||
const cancel = () => {
|
||||
for (const id of frames) cancelAnimationFrame(id)
|
||||
frames.clear()
|
||||
}
|
||||
|
||||
const clearMessageHash = () => {
|
||||
cancel()
|
||||
input.consumePendingMessage(input.sessionKey())
|
||||
if (input.pendingMessage()) input.setPendingMessage(undefined)
|
||||
if (!location.hash) return
|
||||
clearing = true
|
||||
navigate(location.pathname + location.search, { replace: true })
|
||||
}
|
||||
|
||||
const updateHash = (id: string) => {
|
||||
navigate(location.pathname + location.search + `#${input.anchor(id)}`, {
|
||||
const hash = `#${input.anchor(id)}`
|
||||
if (location.hash === hash) return
|
||||
clearing = false
|
||||
navigate(location.pathname + location.search + hash, {
|
||||
replace: true,
|
||||
})
|
||||
}
|
||||
@@ -54,51 +75,37 @@ export const useSessionHashScroll = (input: {
|
||||
return true
|
||||
}
|
||||
|
||||
const seek = (id: string, behavior: ScrollBehavior, left = 4): boolean => {
|
||||
const el = document.getElementById(input.anchor(id))
|
||||
if (el) return scrollToElement(el, behavior)
|
||||
if (left <= 0) return false
|
||||
queue(() => {
|
||||
seek(id, behavior, left - 1)
|
||||
})
|
||||
return false
|
||||
}
|
||||
|
||||
const scrollToMessage = (message: UserMessage, behavior: ScrollBehavior = "smooth") => {
|
||||
console.log({ message, behavior })
|
||||
cancel()
|
||||
if (input.currentMessageId() !== message.id) input.setActiveMessage(message)
|
||||
|
||||
const index = messageIndex().get(message.id) ?? -1
|
||||
if (index !== -1 && index < input.turnStart()) {
|
||||
input.setTurnStart(index)
|
||||
|
||||
requestAnimationFrame(() => {
|
||||
const el = document.getElementById(input.anchor(message.id))
|
||||
if (!el) {
|
||||
requestAnimationFrame(() => {
|
||||
const next = document.getElementById(input.anchor(message.id))
|
||||
if (!next) return
|
||||
scrollToElement(next, behavior)
|
||||
})
|
||||
return
|
||||
}
|
||||
scrollToElement(el, behavior)
|
||||
queue(() => {
|
||||
seek(message.id, behavior)
|
||||
})
|
||||
|
||||
updateHash(message.id)
|
||||
return
|
||||
}
|
||||
|
||||
const el = document.getElementById(input.anchor(message.id))
|
||||
if (!el) {
|
||||
updateHash(message.id)
|
||||
requestAnimationFrame(() => {
|
||||
const next = document.getElementById(input.anchor(message.id))
|
||||
if (!next) return
|
||||
if (!scrollToElement(next, behavior)) return
|
||||
})
|
||||
return
|
||||
}
|
||||
if (scrollToElement(el, behavior)) {
|
||||
if (seek(message.id, behavior)) {
|
||||
updateHash(message.id)
|
||||
return
|
||||
}
|
||||
|
||||
requestAnimationFrame(() => {
|
||||
const next = document.getElementById(input.anchor(message.id))
|
||||
if (!next) return
|
||||
if (!scrollToElement(next, behavior)) return
|
||||
})
|
||||
updateHash(message.id)
|
||||
}
|
||||
|
||||
@@ -135,9 +142,11 @@ export const useSessionHashScroll = (input: {
|
||||
}
|
||||
|
||||
createEffect(() => {
|
||||
location.hash
|
||||
const hash = location.hash
|
||||
if (!hash) clearing = false
|
||||
if (!input.sessionID() || !input.messagesReady()) return
|
||||
requestAnimationFrame(() => applyHash("auto"))
|
||||
cancel()
|
||||
queue(() => applyHash("auto"))
|
||||
})
|
||||
|
||||
createEffect(() => {
|
||||
@@ -159,16 +168,19 @@ export const useSessionHashScroll = (input: {
|
||||
}
|
||||
}
|
||||
|
||||
if (!targetId) targetId = messageIdFromHash(location.hash)
|
||||
if (!targetId && !clearing) targetId = messageIdFromHash(location.hash)
|
||||
if (!targetId) return
|
||||
if (input.currentMessageId() === targetId) return
|
||||
|
||||
const pending = input.pendingMessage() === targetId
|
||||
const msg = messageById().get(targetId)
|
||||
if (!msg) return
|
||||
|
||||
if (input.pendingMessage() === targetId) input.setPendingMessage(undefined)
|
||||
if (pending) input.setPendingMessage(undefined)
|
||||
if (input.currentMessageId() === targetId && !pending) return
|
||||
|
||||
input.autoScroll.pause()
|
||||
requestAnimationFrame(() => scrollToMessage(msg, "auto"))
|
||||
cancel()
|
||||
queue(() => scrollToMessage(msg, "auto"))
|
||||
})
|
||||
|
||||
onMount(() => {
|
||||
@@ -177,6 +189,8 @@ export const useSessionHashScroll = (input: {
|
||||
}
|
||||
})
|
||||
|
||||
onCleanup(cancel)
|
||||
|
||||
return {
|
||||
clearMessageHash,
|
||||
scrollToMessage,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/console-app",
|
||||
"version": "1.2.23",
|
||||
"version": "1.2.24",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "@opencode-ai/console-core",
|
||||
"version": "1.2.23",
|
||||
"version": "1.2.24",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/console-function",
|
||||
"version": "1.2.23",
|
||||
"version": "1.2.24",
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/console-mail",
|
||||
"version": "1.2.23",
|
||||
"version": "1.2.24",
|
||||
"dependencies": {
|
||||
"@jsx-email/all": "2.2.3",
|
||||
"@jsx-email/cli": "1.4.3",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@opencode-ai/desktop-electron",
|
||||
"private": true,
|
||||
"version": "1.2.23",
|
||||
"version": "1.2.24",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"homepage": "https://opencode.ai",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@opencode-ai/desktop",
|
||||
"private": true,
|
||||
"version": "1.2.23",
|
||||
"version": "1.2.24",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/enterprise",
|
||||
"version": "1.2.23",
|
||||
"version": "1.2.24",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
id = "opencode"
|
||||
name = "OpenCode"
|
||||
description = "The open source coding agent."
|
||||
version = "1.2.23"
|
||||
version = "1.2.24"
|
||||
schema_version = 1
|
||||
authors = ["Anomaly"]
|
||||
repository = "https://github.com/anomalyco/opencode"
|
||||
@@ -11,26 +11,26 @@ name = "OpenCode"
|
||||
icon = "./icons/opencode.svg"
|
||||
|
||||
[agent_servers.opencode.targets.darwin-aarch64]
|
||||
archive = "https://github.com/anomalyco/opencode/releases/download/v1.2.23/opencode-darwin-arm64.zip"
|
||||
archive = "https://github.com/anomalyco/opencode/releases/download/v1.2.24/opencode-darwin-arm64.zip"
|
||||
cmd = "./opencode"
|
||||
args = ["acp"]
|
||||
|
||||
[agent_servers.opencode.targets.darwin-x86_64]
|
||||
archive = "https://github.com/anomalyco/opencode/releases/download/v1.2.23/opencode-darwin-x64.zip"
|
||||
archive = "https://github.com/anomalyco/opencode/releases/download/v1.2.24/opencode-darwin-x64.zip"
|
||||
cmd = "./opencode"
|
||||
args = ["acp"]
|
||||
|
||||
[agent_servers.opencode.targets.linux-aarch64]
|
||||
archive = "https://github.com/anomalyco/opencode/releases/download/v1.2.23/opencode-linux-arm64.tar.gz"
|
||||
archive = "https://github.com/anomalyco/opencode/releases/download/v1.2.24/opencode-linux-arm64.tar.gz"
|
||||
cmd = "./opencode"
|
||||
args = ["acp"]
|
||||
|
||||
[agent_servers.opencode.targets.linux-x86_64]
|
||||
archive = "https://github.com/anomalyco/opencode/releases/download/v1.2.23/opencode-linux-x64.tar.gz"
|
||||
archive = "https://github.com/anomalyco/opencode/releases/download/v1.2.24/opencode-linux-x64.tar.gz"
|
||||
cmd = "./opencode"
|
||||
args = ["acp"]
|
||||
|
||||
[agent_servers.opencode.targets.windows-x86_64]
|
||||
archive = "https://github.com/anomalyco/opencode/releases/download/v1.2.23/opencode-windows-x64.zip"
|
||||
archive = "https://github.com/anomalyco/opencode/releases/download/v1.2.24/opencode-windows-x64.zip"
|
||||
cmd = "./opencode.exe"
|
||||
args = ["acp"]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/function",
|
||||
"version": "1.2.23",
|
||||
"version": "1.2.24",
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"version": "1.2.23",
|
||||
"version": "1.2.24",
|
||||
"name": "opencode",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -539,12 +539,25 @@ export function Prompt(props: PromptProps) {
|
||||
promptModelWarning()
|
||||
return
|
||||
}
|
||||
const sessionID = props.sessionID
|
||||
? props.sessionID
|
||||
: await (async () => {
|
||||
const sessionID = await sdk.client.session.create({}).then((x) => x.data!.id)
|
||||
return sessionID
|
||||
})()
|
||||
|
||||
let sessionID = props.sessionID
|
||||
if (sessionID == null) {
|
||||
const res = await sdk.client.session.create({})
|
||||
|
||||
if (res.error) {
|
||||
console.log("Creating a session failed:", res.error)
|
||||
|
||||
toast.show({
|
||||
message: "Creating a session failed. Open console for more details.",
|
||||
variant: "error",
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
sessionID = res.data.id
|
||||
}
|
||||
|
||||
const messageID = Identifier.ascending("message")
|
||||
let inputText = store.prompt.input
|
||||
|
||||
|
||||
@@ -1130,7 +1130,30 @@ export namespace LSPServer {
|
||||
|
||||
export const JDTLS: Info = {
|
||||
id: "jdtls",
|
||||
root: NearestRoot(["pom.xml", "build.gradle", "build.gradle.kts", ".project", ".classpath"]),
|
||||
root: async (file) => {
|
||||
// Without exclusions, NearestRoot defaults to instance directory so we can't
|
||||
// distinguish between a) no project found and b) project found at instance dir.
|
||||
// So we can't choose the root from (potential) monorepo markers first.
|
||||
// Look for potential subproject markers first while excluding potential monorepo markers.
|
||||
const settingsMarkers = ["settings.gradle", "settings.gradle.kts"]
|
||||
const gradleMarkers = ["gradlew", "gradlew.bat"]
|
||||
const exclusionsForMonorepos = gradleMarkers.concat(settingsMarkers)
|
||||
|
||||
const [projectRoot, wrapperRoot, settingsRoot] = await Promise.all([
|
||||
NearestRoot(
|
||||
["pom.xml", "build.gradle", "build.gradle.kts", ".project", ".classpath"],
|
||||
exclusionsForMonorepos,
|
||||
)(file),
|
||||
NearestRoot(gradleMarkers, settingsMarkers)(file),
|
||||
NearestRoot(settingsMarkers)(file),
|
||||
])
|
||||
|
||||
// If projectRoot is undefined we know we are in a monorepo or no project at all.
|
||||
// So can safely fall through to the other roots
|
||||
if (projectRoot) return projectRoot
|
||||
if (wrapperRoot) return wrapperRoot
|
||||
if (settingsRoot) return settingsRoot
|
||||
},
|
||||
extensions: [".java"],
|
||||
async spawn(root) {
|
||||
const java = which("java")
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "@opencode-ai/plugin",
|
||||
"version": "1.2.23",
|
||||
"version": "1.2.24",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "@opencode-ai/sdk",
|
||||
"version": "1.2.23",
|
||||
"version": "1.2.24",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/slack",
|
||||
"version": "1.2.23",
|
||||
"version": "1.2.24",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/ui",
|
||||
"version": "1.2.23",
|
||||
"version": "1.2.24",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"exports": {
|
||||
|
||||
19
packages/ui/src/components/scroll-view.test.ts
Normal file
19
packages/ui/src/components/scroll-view.test.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import { scrollKey } from "./scroll-view"
|
||||
|
||||
describe("scrollKey", () => {
|
||||
test("maps plain navigation keys", () => {
|
||||
expect(scrollKey({ key: "PageDown", altKey: false, ctrlKey: false, metaKey: false, shiftKey: false })).toBe(
|
||||
"page-down",
|
||||
)
|
||||
expect(scrollKey({ key: "ArrowUp", altKey: false, ctrlKey: false, metaKey: false, shiftKey: false })).toBe("up")
|
||||
})
|
||||
|
||||
test("ignores modified keybinds", () => {
|
||||
expect(
|
||||
scrollKey({ key: "ArrowDown", altKey: false, ctrlKey: false, metaKey: true, shiftKey: false }),
|
||||
).toBeUndefined()
|
||||
expect(scrollKey({ key: "PageUp", altKey: false, ctrlKey: true, metaKey: false, shiftKey: false })).toBeUndefined()
|
||||
expect(scrollKey({ key: "End", altKey: false, ctrlKey: false, metaKey: false, shiftKey: true })).toBeUndefined()
|
||||
})
|
||||
})
|
||||
@@ -6,6 +6,25 @@ export interface ScrollViewProps extends ComponentProps<"div"> {
|
||||
orientation?: "vertical" | "horizontal" // currently only vertical is fully implemented for thumb
|
||||
}
|
||||
|
||||
export const scrollKey = (event: Pick<KeyboardEvent, "key" | "altKey" | "ctrlKey" | "metaKey" | "shiftKey">) => {
|
||||
if (event.altKey || event.ctrlKey || event.metaKey || event.shiftKey) return
|
||||
|
||||
switch (event.key) {
|
||||
case "PageDown":
|
||||
return "page-down"
|
||||
case "PageUp":
|
||||
return "page-up"
|
||||
case "Home":
|
||||
return "home"
|
||||
case "End":
|
||||
return "end"
|
||||
case "ArrowUp":
|
||||
return "up"
|
||||
case "ArrowDown":
|
||||
return "down"
|
||||
}
|
||||
}
|
||||
|
||||
export function ScrollView(props: ScrollViewProps) {
|
||||
const i18n = useI18n()
|
||||
const merged = mergeProps({ orientation: "vertical" }, props)
|
||||
@@ -133,31 +152,34 @@ export function ScrollView(props: ScrollViewProps) {
|
||||
return
|
||||
}
|
||||
|
||||
const next = scrollKey(e)
|
||||
if (!next) return
|
||||
|
||||
const scrollAmount = viewportRef.clientHeight * 0.8
|
||||
const lineAmount = 40
|
||||
|
||||
switch (e.key) {
|
||||
case "PageDown":
|
||||
switch (next) {
|
||||
case "page-down":
|
||||
e.preventDefault()
|
||||
viewportRef.scrollBy({ top: scrollAmount, behavior: "smooth" })
|
||||
break
|
||||
case "PageUp":
|
||||
case "page-up":
|
||||
e.preventDefault()
|
||||
viewportRef.scrollBy({ top: -scrollAmount, behavior: "smooth" })
|
||||
break
|
||||
case "Home":
|
||||
case "home":
|
||||
e.preventDefault()
|
||||
viewportRef.scrollTo({ top: 0, behavior: "smooth" })
|
||||
break
|
||||
case "End":
|
||||
case "end":
|
||||
e.preventDefault()
|
||||
viewportRef.scrollTo({ top: viewportRef.scrollHeight, behavior: "smooth" })
|
||||
break
|
||||
case "ArrowUp":
|
||||
case "up":
|
||||
e.preventDefault()
|
||||
viewportRef.scrollBy({ top: -lineAmount, behavior: "smooth" })
|
||||
break
|
||||
case "ArrowDown":
|
||||
case "down":
|
||||
e.preventDefault()
|
||||
viewportRef.scrollBy({ top: lineAmount, behavior: "smooth" })
|
||||
break
|
||||
|
||||
@@ -41,6 +41,7 @@ export function Spinner(props: {
|
||||
animation: square.corner
|
||||
? undefined
|
||||
: `${square.outer ? "pulse-opacity-dim" : "pulse-opacity"} ${square.duration}s ease-in-out infinite`,
|
||||
"animation-fill-mode": square.corner ? undefined : "both",
|
||||
"animation-delay": square.corner ? undefined : `${square.delay}s`,
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -26,10 +26,10 @@
|
||||
@keyframes pulse-opacity-dim {
|
||||
0%,
|
||||
100% {
|
||||
opacity: 0;
|
||||
opacity: 0.15;
|
||||
}
|
||||
50% {
|
||||
opacity: 0.2;
|
||||
opacity: 0.35;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/util",
|
||||
"version": "1.2.23",
|
||||
"version": "1.2.24",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"name": "@opencode-ai/web",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"version": "1.2.23",
|
||||
"version": "1.2.24",
|
||||
"scripts": {
|
||||
"dev": "astro dev",
|
||||
"dev:remote": "VITE_API_URL=https://api.opencode.ai astro dev",
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"name": "opencode",
|
||||
"displayName": "opencode",
|
||||
"description": "opencode for VS Code",
|
||||
"version": "1.2.23",
|
||||
"version": "1.2.24",
|
||||
"publisher": "sst-dev",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
||||
Reference in New Issue
Block a user