mirror of
https://github.com/anomalyco/opencode.git
synced 2026-08-13 23:09:50 -04:00
Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| cfc82d6dcb | |||
| d63db95c07 |
@@ -48,6 +48,7 @@ export const Flag = {
|
|||||||
|
|
||||||
OPENCODE_WORKSPACE_ID: process.env["OPENCODE_WORKSPACE_ID"],
|
OPENCODE_WORKSPACE_ID: process.env["OPENCODE_WORKSPACE_ID"],
|
||||||
OPENCODE_EXPERIMENTAL_WORKSPACES: enabledByExperimental("OPENCODE_EXPERIMENTAL_WORKSPACES"),
|
OPENCODE_EXPERIMENTAL_WORKSPACES: enabledByExperimental("OPENCODE_EXPERIMENTAL_WORKSPACES"),
|
||||||
|
OPENCODE_EXPERIMENTAL_TAB_SCROLL: enabledByExperimental("OPENCODE_EXPERIMENTAL_TAB_SCROLL"),
|
||||||
|
|
||||||
// Evaluated at access time (not module load) because tests, the CLI, and
|
// Evaluated at access time (not module load) because tests, the CLI, and
|
||||||
// external tooling set these env vars at runtime.
|
// external tooling set these env vars at runtime.
|
||||||
|
|||||||
@@ -417,6 +417,7 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
|
|||||||
const filePath = path.join(paths.state, "session.json")
|
const filePath = path.join(paths.state, "session.json")
|
||||||
const state = {
|
const state = {
|
||||||
pending: false,
|
pending: false,
|
||||||
|
scroll: new Map<string, number>(),
|
||||||
}
|
}
|
||||||
|
|
||||||
function save() {
|
function save() {
|
||||||
@@ -455,6 +456,7 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
|
|||||||
|
|
||||||
function prune(sessionID: string) {
|
function prune(sessionID: string) {
|
||||||
batch(() => {
|
batch(() => {
|
||||||
|
state.scroll.delete(sessionID)
|
||||||
if (sessionStore.pinned.includes(sessionID)) {
|
if (sessionStore.pinned.includes(sessionID)) {
|
||||||
setSessionStore(
|
setSessionStore(
|
||||||
"pinned",
|
"pinned",
|
||||||
@@ -487,6 +489,7 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
|
|||||||
? sessionStore.pinned.filter((x) => x !== sessionID)
|
? sessionStore.pinned.filter((x) => x !== sessionID)
|
||||||
: [...sessionStore.pinned, sessionID]
|
: [...sessionStore.pinned, sessionID]
|
||||||
setSessionStore("pinned", next)
|
setSessionStore("pinned", next)
|
||||||
|
if (exists) state.scroll.delete(sessionID)
|
||||||
save()
|
save()
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
@@ -496,6 +499,17 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
|
|||||||
if (route.data.type === "session" && route.data.sessionID === target) return
|
if (route.data.type === "session" && route.data.sessionID === target) return
|
||||||
route.navigate({ type: "session", sessionID: target })
|
route.navigate({ type: "session", sessionID: target })
|
||||||
},
|
},
|
||||||
|
scrollPosition(sessionID: string) {
|
||||||
|
if (!slots().includes(sessionID)) return
|
||||||
|
return state.scroll.get(sessionID)
|
||||||
|
},
|
||||||
|
setScrollPosition(sessionID: string, position: number | undefined) {
|
||||||
|
if (position === undefined || !slots().includes(sessionID)) {
|
||||||
|
state.scroll.delete(sessionID)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
state.scroll.set(sessionID, position)
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import {
|
|||||||
createSignal,
|
createSignal,
|
||||||
For,
|
For,
|
||||||
Match,
|
Match,
|
||||||
on,
|
|
||||||
onCleanup,
|
onCleanup,
|
||||||
onMount,
|
onMount,
|
||||||
Show,
|
Show,
|
||||||
@@ -82,6 +81,7 @@ import { getRevertDiffFiles } from "../../util/revert-diff"
|
|||||||
import { OPENCODE_BASE_MODE, useBindings, useCommandShortcut, useOpencodeKeymap } from "../../keymap"
|
import { OPENCODE_BASE_MODE, useBindings, useCommandShortcut, useOpencodeKeymap } from "../../keymap"
|
||||||
import { usePathFormatter } from "../../context/path-format"
|
import { usePathFormatter } from "../../context/path-format"
|
||||||
import { LocationProvider } from "../../context/location"
|
import { LocationProvider } from "../../context/location"
|
||||||
|
import { Flag } from "@opencode-ai/core/flag/flag"
|
||||||
|
|
||||||
addDefaultParsers(parsers.parsers)
|
addDefaultParsers(parsers.parsers)
|
||||||
|
|
||||||
@@ -259,6 +259,11 @@ export function Session() {
|
|||||||
const [diffWrapMode] = kv.signal<"word" | "none">("diff_wrap_mode", "word")
|
const [diffWrapMode] = kv.signal<"word" | "none">("diff_wrap_mode", "word")
|
||||||
const [_animationsEnabled, _setAnimationsEnabled] = kv.signal("animations_enabled", true)
|
const [_animationsEnabled, _setAnimationsEnabled] = kv.signal("animations_enabled", true)
|
||||||
const [showGenericToolOutput, setShowGenericToolOutput] = kv.signal("generic_tool_output_visibility", false)
|
const [showGenericToolOutput, setShowGenericToolOutput] = kv.signal("generic_tool_output_visibility", false)
|
||||||
|
const [jumpBottomPosition, setJumpBottomPosition] = kv.signal<"center" | "right">(
|
||||||
|
"experimental_jump_bottom_position",
|
||||||
|
"center",
|
||||||
|
)
|
||||||
|
const [awayFromBottom, setAwayFromBottom] = createSignal(false)
|
||||||
|
|
||||||
const wide = createMemo(() => dimensions().width > 120)
|
const wide = createMemo(() => dimensions().width > 120)
|
||||||
const sidebarVisible = createMemo(() => {
|
const sidebarVisible = createMemo(() => {
|
||||||
@@ -275,6 +280,7 @@ export function Session() {
|
|||||||
const toast = useToast()
|
const toast = useToast()
|
||||||
const sdk = useSDK()
|
const sdk = useSDK()
|
||||||
const editor = useEditorContext()
|
const editor = useEditorContext()
|
||||||
|
const local = useLocal()
|
||||||
|
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
const sessionID = route.sessionID
|
const sessionID = route.sessionID
|
||||||
@@ -304,7 +310,15 @@ export function Session() {
|
|||||||
}
|
}
|
||||||
editor.reconnect(result.data.directory)
|
editor.reconnect(result.data.directory)
|
||||||
await sync.session.sync(sessionID)
|
await sync.session.sync(sessionID)
|
||||||
if (route.sessionID === sessionID && scroll) scroll.scrollBy(100_000)
|
setTimeout(() => {
|
||||||
|
if (route.sessionID !== sessionID || !scroll || scroll.isDestroyed) return
|
||||||
|
scroll.scrollTo(
|
||||||
|
Flag.OPENCODE_EXPERIMENTAL_TAB_SCROLL
|
||||||
|
? (local.session.scrollPosition(sessionID) ?? scroll.scrollHeight)
|
||||||
|
: scroll.scrollHeight,
|
||||||
|
)
|
||||||
|
updateAwayFromBottom()
|
||||||
|
}, 50)
|
||||||
})().catch((error) => {
|
})().catch((error) => {
|
||||||
if (route.sessionID !== sessionID) return
|
if (route.sessionID !== sessionID) return
|
||||||
toast.show({
|
toast.show({
|
||||||
@@ -335,6 +349,13 @@ export function Session() {
|
|||||||
|
|
||||||
let seeded = false
|
let seeded = false
|
||||||
let scroll: ScrollBoxRenderable
|
let scroll: ScrollBoxRenderable
|
||||||
|
onCleanup(() => {
|
||||||
|
if (!scroll || scroll.isDestroyed) return
|
||||||
|
local.session.setScrollPosition(
|
||||||
|
route.sessionID,
|
||||||
|
Flag.OPENCODE_EXPERIMENTAL_TAB_SCROLL && isAwayFromBottom() ? scroll.scrollTop : undefined,
|
||||||
|
)
|
||||||
|
})
|
||||||
let prompt: PromptRef | undefined
|
let prompt: PromptRef | undefined
|
||||||
const bind = (r: PromptRef | undefined) => {
|
const bind = (r: PromptRef | undefined) => {
|
||||||
prompt = r
|
prompt = r
|
||||||
@@ -404,24 +425,40 @@ export function Session() {
|
|||||||
|
|
||||||
if (!targetID) {
|
if (!targetID) {
|
||||||
scroll.scrollBy(direction === "next" ? scroll.height : -scroll.height)
|
scroll.scrollBy(direction === "next" ? scroll.height : -scroll.height)
|
||||||
|
updateAwayFromBottom()
|
||||||
dialog.clear()
|
dialog.clear()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const child = scroll.getChildren().find((c) => c.id === targetID)
|
const child = scroll.getChildren().find((c) => c.id === targetID)
|
||||||
if (child) scroll.scrollBy(child.y - scroll.y - 1)
|
if (child) scroll.scrollBy(child.y - scroll.y - 1)
|
||||||
|
updateAwayFromBottom()
|
||||||
dialog.clear()
|
dialog.clear()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isAwayFromBottom() {
|
||||||
|
return scroll.scrollTop < Math.max(0, scroll.scrollHeight - scroll.viewport.height) - 1
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateAwayFromBottom() {
|
||||||
|
if (!Flag.OPENCODE_EXPERIMENTAL_TAB_SCROLL) return
|
||||||
|
setTimeout(() => {
|
||||||
|
if (!scroll || scroll.isDestroyed) return
|
||||||
|
const away = isAwayFromBottom()
|
||||||
|
setAwayFromBottom(away)
|
||||||
|
if (!away) local.session.setScrollPosition(route.sessionID, undefined)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
function toBottom() {
|
function toBottom() {
|
||||||
|
setAwayFromBottom(false)
|
||||||
|
local.session.setScrollPosition(route.sessionID, undefined)
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
if (!scroll || scroll.isDestroyed) return
|
if (!scroll || scroll.isDestroyed) return
|
||||||
scroll.scrollTo(scroll.scrollHeight)
|
scroll.scrollTo(scroll.scrollHeight)
|
||||||
}, 50)
|
}, 50)
|
||||||
}
|
}
|
||||||
|
|
||||||
const local = useLocal()
|
|
||||||
|
|
||||||
function enterChild(sessionID: string) {
|
function enterChild(sessionID: string) {
|
||||||
navigate({
|
navigate({
|
||||||
type: "session",
|
type: "session",
|
||||||
@@ -522,6 +559,7 @@ export function Session() {
|
|||||||
return child.id === messageID
|
return child.id === messageID
|
||||||
})
|
})
|
||||||
if (child) scroll.scrollBy(child.y - scroll.y - 1)
|
if (child) scroll.scrollBy(child.y - scroll.y - 1)
|
||||||
|
updateAwayFromBottom()
|
||||||
}}
|
}}
|
||||||
sessionID={route.sessionID}
|
sessionID={route.sessionID}
|
||||||
setPrompt={(promptInfo) => prompt?.set(promptInfo)}
|
setPrompt={(promptInfo) => prompt?.set(promptInfo)}
|
||||||
@@ -545,6 +583,7 @@ export function Session() {
|
|||||||
return child.id === messageID
|
return child.id === messageID
|
||||||
})
|
})
|
||||||
if (child) scroll.scrollBy(child.y - scroll.y - 1)
|
if (child) scroll.scrollBy(child.y - scroll.y - 1)
|
||||||
|
updateAwayFromBottom()
|
||||||
}}
|
}}
|
||||||
sessionID={route.sessionID}
|
sessionID={route.sessionID}
|
||||||
/>
|
/>
|
||||||
@@ -742,6 +781,19 @@ export function Session() {
|
|||||||
dialog.clear()
|
dialog.clear()
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
title: `Move jump-to-bottom button ${jumpBottomPosition() === "center" ? "right" : "to center"}`,
|
||||||
|
value: "session.jump_bottom.position",
|
||||||
|
category: "Session",
|
||||||
|
hidden: !Flag.OPENCODE_EXPERIMENTAL_TAB_SCROLL,
|
||||||
|
slash: {
|
||||||
|
name: "jump-bottom-position",
|
||||||
|
},
|
||||||
|
run: () => {
|
||||||
|
setJumpBottomPosition((position) => (position === "center" ? "right" : "center"))
|
||||||
|
dialog.clear()
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
title: "Page up",
|
title: "Page up",
|
||||||
value: "session.page.up",
|
value: "session.page.up",
|
||||||
@@ -749,6 +801,7 @@ export function Session() {
|
|||||||
hidden: true,
|
hidden: true,
|
||||||
run: () => {
|
run: () => {
|
||||||
scroll.scrollBy(-scroll.height / 2)
|
scroll.scrollBy(-scroll.height / 2)
|
||||||
|
updateAwayFromBottom()
|
||||||
dialog.clear()
|
dialog.clear()
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -759,6 +812,7 @@ export function Session() {
|
|||||||
hidden: true,
|
hidden: true,
|
||||||
run: () => {
|
run: () => {
|
||||||
scroll.scrollBy(scroll.height / 2)
|
scroll.scrollBy(scroll.height / 2)
|
||||||
|
updateAwayFromBottom()
|
||||||
dialog.clear()
|
dialog.clear()
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -769,6 +823,7 @@ export function Session() {
|
|||||||
hidden: true,
|
hidden: true,
|
||||||
run: () => {
|
run: () => {
|
||||||
scroll.scrollBy(-1)
|
scroll.scrollBy(-1)
|
||||||
|
updateAwayFromBottom()
|
||||||
dialog.clear()
|
dialog.clear()
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -779,6 +834,7 @@ export function Session() {
|
|||||||
hidden: true,
|
hidden: true,
|
||||||
run: () => {
|
run: () => {
|
||||||
scroll.scrollBy(1)
|
scroll.scrollBy(1)
|
||||||
|
updateAwayFromBottom()
|
||||||
dialog.clear()
|
dialog.clear()
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -789,6 +845,7 @@ export function Session() {
|
|||||||
hidden: true,
|
hidden: true,
|
||||||
run: () => {
|
run: () => {
|
||||||
scroll.scrollBy(-scroll.height / 4)
|
scroll.scrollBy(-scroll.height / 4)
|
||||||
|
updateAwayFromBottom()
|
||||||
dialog.clear()
|
dialog.clear()
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -799,6 +856,7 @@ export function Session() {
|
|||||||
hidden: true,
|
hidden: true,
|
||||||
run: () => {
|
run: () => {
|
||||||
scroll.scrollBy(scroll.height / 4)
|
scroll.scrollBy(scroll.height / 4)
|
||||||
|
updateAwayFromBottom()
|
||||||
dialog.clear()
|
dialog.clear()
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -809,6 +867,7 @@ export function Session() {
|
|||||||
hidden: true,
|
hidden: true,
|
||||||
run: () => {
|
run: () => {
|
||||||
scroll.scrollTo(0)
|
scroll.scrollTo(0)
|
||||||
|
updateAwayFromBottom()
|
||||||
dialog.clear()
|
dialog.clear()
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -818,7 +877,7 @@ export function Session() {
|
|||||||
category: "Session",
|
category: "Session",
|
||||||
hidden: true,
|
hidden: true,
|
||||||
run: () => {
|
run: () => {
|
||||||
scroll.scrollTo(scroll.scrollHeight)
|
toBottom()
|
||||||
dialog.clear()
|
dialog.clear()
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -848,6 +907,7 @@ export function Session() {
|
|||||||
return child.id === message.id
|
return child.id === message.id
|
||||||
})
|
})
|
||||||
if (child) scroll.scrollBy(child.y - scroll.y - 1)
|
if (child) scroll.scrollBy(child.y - scroll.y - 1)
|
||||||
|
updateAwayFromBottom()
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1139,9 +1199,6 @@ export function Session() {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
// snap to bottom when session changes
|
|
||||||
createEffect(on(() => route.sessionID, toBottom))
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<LocationProvider location={location()}>
|
<LocationProvider location={location()}>
|
||||||
<context.Provider
|
<context.Provider
|
||||||
@@ -1182,6 +1239,7 @@ export function Session() {
|
|||||||
stickyStart="bottom"
|
stickyStart="bottom"
|
||||||
flexGrow={1}
|
flexGrow={1}
|
||||||
scrollAcceleration={scrollAcceleration()}
|
scrollAcceleration={scrollAcceleration()}
|
||||||
|
onMouseScroll={updateAwayFromBottom}
|
||||||
>
|
>
|
||||||
<box height={1} />
|
<box height={1} />
|
||||||
<For each={messages()}>
|
<For each={messages()}>
|
||||||
@@ -1280,6 +1338,18 @@ export function Session() {
|
|||||||
</For>
|
</For>
|
||||||
</scrollbox>
|
</scrollbox>
|
||||||
<box flexShrink={0}>
|
<box flexShrink={0}>
|
||||||
|
<Show when={Flag.OPENCODE_EXPERIMENTAL_TAB_SCROLL && awayFromBottom()}>
|
||||||
|
<box
|
||||||
|
height={1}
|
||||||
|
flexDirection="row"
|
||||||
|
justifyContent={jumpBottomPosition() === "center" ? "center" : "flex-end"}
|
||||||
|
paddingRight={jumpBottomPosition() === "right" ? 1 : 0}
|
||||||
|
>
|
||||||
|
<text fg={theme.textMuted} onMouseUp={toBottom}>
|
||||||
|
↓ Bottom
|
||||||
|
</text>
|
||||||
|
</box>
|
||||||
|
</Show>
|
||||||
<Show when={permissions().length > 0}>
|
<Show when={permissions().length > 0}>
|
||||||
<PermissionPrompt
|
<PermissionPrompt
|
||||||
request={permissions()[0]}
|
request={permissions()[0]}
|
||||||
|
|||||||
Reference in New Issue
Block a user