mirror of
https://github.com/anomalyco/opencode.git
synced 2026-03-26 08:34:35 +00:00
Compare commits
1 Commits
brendan-cl
...
opencode/q
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
871a0e11b9 |
@@ -133,7 +133,7 @@ function RouterRoot(props: ParentProps<{ appChildren?: JSX.Element }>) {
|
||||
export function AppBaseProviders(props: ParentProps<{ locale?: Locale }>) {
|
||||
return (
|
||||
<MetaProvider>
|
||||
<Font />
|
||||
<Font preloadMono={false} />
|
||||
<ThemeProvider
|
||||
onThemeApplied={(_, mode) => {
|
||||
void window.api?.setTitlebar?.({ mode })
|
||||
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
type JSXElement,
|
||||
type ParentProps,
|
||||
} from "solid-js"
|
||||
import { createStore, type SetStoreFunction } from "solid-js/store"
|
||||
import { Dynamic } from "solid-js/web"
|
||||
import type { FileNode } from "@opencode-ai/sdk/v2"
|
||||
|
||||
@@ -201,6 +202,7 @@ export default function FileTree(props: {
|
||||
modified?: readonly string[]
|
||||
kinds?: ReadonlyMap<string, Kind>
|
||||
draggable?: boolean
|
||||
synthetic?: boolean
|
||||
onFileClick?: (file: FileNode) => void
|
||||
|
||||
_filter?: Filter
|
||||
@@ -208,10 +210,15 @@ export default function FileTree(props: {
|
||||
_deeps?: Map<string, number>
|
||||
_kinds?: ReadonlyMap<string, Kind>
|
||||
_chain?: readonly string[]
|
||||
_open?: Record<string, boolean>
|
||||
_setOpen?: SetStoreFunction<Record<string, boolean>>
|
||||
}) {
|
||||
const file = useFile()
|
||||
const level = props.level ?? 0
|
||||
const draggable = () => props.draggable ?? true
|
||||
const local = createStore<Record<string, boolean>>({})
|
||||
const open = props._open ?? local[0]
|
||||
const setOpen = props._setOpen ?? local[1]
|
||||
|
||||
const key = (p: string) =>
|
||||
file
|
||||
@@ -258,6 +265,7 @@ export default function FileTree(props: {
|
||||
|
||||
const deeps = createMemo(() => {
|
||||
if (props._deeps) return props._deeps
|
||||
if (props.synthetic) return new Map<string, number>()
|
||||
|
||||
const out = new Map<string, number>()
|
||||
|
||||
@@ -304,6 +312,7 @@ export default function FileTree(props: {
|
||||
})
|
||||
|
||||
createEffect(() => {
|
||||
if (props.synthetic) return
|
||||
const current = filter()
|
||||
const dirs = dirsToExpand({
|
||||
level,
|
||||
@@ -317,6 +326,7 @@ export default function FileTree(props: {
|
||||
on(
|
||||
() => props.path,
|
||||
(path) => {
|
||||
if (props.synthetic) return
|
||||
const dir = untrack(() => file.tree.state(path))
|
||||
if (!shouldListRoot({ level, dir })) return
|
||||
void file.tree.list(path)
|
||||
@@ -388,7 +398,8 @@ export default function FileTree(props: {
|
||||
<div data-component="filetree" class={`flex flex-col gap-0.5 ${props.class ?? ""}`}>
|
||||
<For each={nodes()}>
|
||||
{(node) => {
|
||||
const expanded = () => file.tree.state(node.path)?.expanded ?? false
|
||||
const expanded = () =>
|
||||
props.synthetic ? (open[node.path] ?? true) : (file.tree.state(node.path)?.expanded ?? false)
|
||||
const deep = () => deeps().get(node.path) ?? -1
|
||||
const kind = () => visibleKind(node, kinds(), marks())
|
||||
const active = () => !!kind() && !node.ignored
|
||||
@@ -402,7 +413,13 @@ export default function FileTree(props: {
|
||||
data-scope="filetree"
|
||||
forceMount={false}
|
||||
open={expanded()}
|
||||
onOpenChange={(open) => (open ? file.tree.expand(node.path) : file.tree.collapse(node.path))}
|
||||
onOpenChange={(open) => {
|
||||
if (props.synthetic) {
|
||||
setOpen(node.path, open)
|
||||
return
|
||||
}
|
||||
open ? file.tree.expand(node.path) : file.tree.collapse(node.path)
|
||||
}}
|
||||
>
|
||||
<Collapsible.Trigger>
|
||||
<FileTreeNode
|
||||
@@ -435,6 +452,7 @@ export default function FileTree(props: {
|
||||
<FileTree
|
||||
path={node.path}
|
||||
level={level + 1}
|
||||
synthetic={props.synthetic}
|
||||
allowed={props.allowed}
|
||||
modified={props.modified}
|
||||
kinds={props.kinds}
|
||||
@@ -446,6 +464,8 @@ export default function FileTree(props: {
|
||||
_deeps={deeps()}
|
||||
_kinds={kinds()}
|
||||
_chain={chain}
|
||||
_open={open}
|
||||
_setOpen={setOpen}
|
||||
/>
|
||||
</Show>
|
||||
</Collapsible.Content>
|
||||
|
||||
@@ -27,7 +27,6 @@ import { IconButton } from "@opencode-ai/ui/icon-button"
|
||||
import { Select } from "@opencode-ai/ui/select"
|
||||
import { useDialog } from "@opencode-ai/ui/context/dialog"
|
||||
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"
|
||||
@@ -1494,7 +1493,11 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
||||
size="normal"
|
||||
class="min-w-0 max-w-[320px] text-13-regular text-text-base group"
|
||||
style={control()}
|
||||
onClick={() => dialog.show(() => <DialogSelectModelUnpaid model={local.model} />)}
|
||||
onClick={() => {
|
||||
void import("@/components/dialog-select-model-unpaid").then((x) =>
|
||||
dialog.show(() => <x.DialogSelectModelUnpaid model={local.model} />),
|
||||
)
|
||||
}}
|
||||
>
|
||||
<Show when={local.model.current()?.provider?.id}>
|
||||
<ProviderIcon
|
||||
|
||||
@@ -243,12 +243,6 @@ export async function bootstrapDirectory(input: {
|
||||
]
|
||||
|
||||
const slow = [
|
||||
() =>
|
||||
retry(() =>
|
||||
input.sdk.provider.list().then((x) => {
|
||||
input.setStore("provider", normalizeProviderList(x.data!))
|
||||
}),
|
||||
),
|
||||
() => Promise.resolve(input.loadSessions(input.directory)),
|
||||
() => retry(() => input.sdk.mcp.status().then((x) => input.setStore("mcp", x.data!))),
|
||||
() => retry(() => input.sdk.lsp.status().then((x) => input.setStore("lsp", x.data!))),
|
||||
|
||||
@@ -56,6 +56,22 @@ function cleanupSessionCaches(
|
||||
)
|
||||
}
|
||||
|
||||
function keep(next: Session, prev?: Session) {
|
||||
const diffs = prev?.summary?.diffs
|
||||
const files = prev?.summary?.files
|
||||
if (!diffs?.length) return next
|
||||
if (!next.summary || next.summary.diffs?.length) return next
|
||||
if (next.summary.files <= 0) return next
|
||||
if (next.summary.files !== files) return next
|
||||
return {
|
||||
...next,
|
||||
summary: {
|
||||
...next.summary,
|
||||
diffs,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export function cleanupDroppedSessionCaches(
|
||||
store: Store<State>,
|
||||
setStore: SetStoreFunction<State>,
|
||||
@@ -105,7 +121,7 @@ export function applyDirectoryEvent(input: {
|
||||
const info = (event.properties as { info: Session }).info
|
||||
const result = Binary.search(input.store.session, info.id, (s) => s.id)
|
||||
if (result.found) {
|
||||
input.setStore("session", result.index, reconcile(info))
|
||||
input.setStore("session", result.index, reconcile(keep(info, input.store.session[result.index])))
|
||||
break
|
||||
}
|
||||
const next = input.store.session.slice()
|
||||
@@ -134,7 +150,7 @@ export function applyDirectoryEvent(input: {
|
||||
break
|
||||
}
|
||||
if (result.found) {
|
||||
input.setStore("session", result.index, reconcile(info))
|
||||
input.setStore("session", result.index, reconcile(keep(info, input.store.session[result.index])))
|
||||
break
|
||||
}
|
||||
const next = input.store.session.slice()
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { createStore, reconcile } from "solid-js/store"
|
||||
import { createEffect, createMemo } from "solid-js"
|
||||
import { createEffect, createMemo, onCleanup } from "solid-js"
|
||||
import { createSimpleContext } from "@opencode-ai/ui/context"
|
||||
import { persisted } from "@/utils/persist"
|
||||
|
||||
@@ -120,7 +120,16 @@ export const { use: useSettings, provider: SettingsProvider } = createSimpleCont
|
||||
if (typeof document === "undefined") return
|
||||
const id = store.appearance?.font ?? defaultSettings.appearance.font
|
||||
if (id !== defaultSettings.appearance.font) {
|
||||
void loadFont().then((x) => x.ensureMonoFont(id))
|
||||
const run = () => {
|
||||
void loadFont().then((x) => x.ensureMonoFont(id))
|
||||
}
|
||||
if (typeof requestIdleCallback === "function") {
|
||||
const idle = requestIdleCallback(run, { timeout: 2000 })
|
||||
onCleanup(() => cancelIdleCallback(idle))
|
||||
} else {
|
||||
const timeout = window.setTimeout(run, 2000)
|
||||
onCleanup(() => window.clearTimeout(timeout))
|
||||
}
|
||||
}
|
||||
document.documentElement.style.setProperty("--font-family-mono", monoFontFamily(id))
|
||||
})
|
||||
|
||||
@@ -180,8 +180,8 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
|
||||
return globalSync.child(directory)
|
||||
}
|
||||
const absolute = (path: string) => (current()[0].path.directory + "/" + path).replace("//", "/")
|
||||
const initialMessagePageSize = 80
|
||||
const historyMessagePageSize = 200
|
||||
const initialMessagePageSize = 40
|
||||
const historyMessagePageSize = 80
|
||||
const inflight = new Map<string, Promise<void>>()
|
||||
const inflightDiff = new Map<string, Promise<void>>()
|
||||
const inflightTodo = new Map<string, Promise<void>>()
|
||||
@@ -460,13 +460,16 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
|
||||
}
|
||||
}
|
||||
|
||||
const hasSession = Binary.search(store.session, sessionID, (s) => s.id).found
|
||||
const hit = Binary.search(store.session, sessionID, (s) => s.id)
|
||||
const session = hit.found ? store.session[hit.index] : undefined
|
||||
const hasSession = hit.found
|
||||
const cached = store.message[sessionID] !== undefined && meta.limit[key] !== undefined
|
||||
if (cached && hasSession && !opts?.force) return
|
||||
const needs = !!session?.summary?.files && !session.summary?.diffs
|
||||
if (cached && hasSession && !opts?.force && !needs) return
|
||||
|
||||
const limit = meta.limit[key] ?? initialMessagePageSize
|
||||
const sessionReq =
|
||||
hasSession && !opts?.force
|
||||
hasSession && !opts?.force && !needs
|
||||
? Promise.resolve()
|
||||
: retry(() => client.session.get({ sessionID })).then((session) => {
|
||||
if (!tracked(directory, sessionID)) return
|
||||
|
||||
@@ -21,7 +21,7 @@ export function useProviders() {
|
||||
const dir = createMemo(() => decode64(params.dir) ?? "")
|
||||
const providers = () => {
|
||||
if (dir()) {
|
||||
const [projectStore] = globalSync.child(dir())
|
||||
const [projectStore] = globalSync.peek(dir(), { bootstrap: false })
|
||||
if (projectStore.provider.all.length > 0) return projectStore.provider
|
||||
}
|
||||
return globalSync.data.provider
|
||||
|
||||
@@ -158,6 +158,13 @@ function createSessionHistoryWindow(input: SessionHistoryWindowInput) {
|
||||
preserveScroll(() => setTurnStart(nextStart))
|
||||
}
|
||||
|
||||
const reveal = () => {
|
||||
const start = turnStart()
|
||||
if (start <= 0) return false
|
||||
backfillTurns()
|
||||
return true
|
||||
}
|
||||
|
||||
/** Button path: reveal all cached turns, fetch older history, reveal one batch. */
|
||||
const loadAndReveal = async () => {
|
||||
const id = input.sessionID()
|
||||
@@ -303,6 +310,7 @@ function createSessionHistoryWindow(input: SessionHistoryWindowInput) {
|
||||
return {
|
||||
turnStart,
|
||||
setTurnStart,
|
||||
reveal,
|
||||
renderedUserMessages,
|
||||
loadAndReveal,
|
||||
onScrollerScroll,
|
||||
@@ -877,6 +885,7 @@ export default function Page() {
|
||||
}
|
||||
|
||||
const mobileChanges = createMemo(() => !isDesktop() && store.mobileTab === "changes")
|
||||
const wantsDiff = createMemo(() => (isDesktop() ? desktopReviewOpen() && activeTab() === "review" : mobileChanges()))
|
||||
|
||||
const fileTreeTab = () => layout.fileTree.tab()
|
||||
const setFileTreeTab = (value: "changes" | "all") => layout.fileTree.setTab(value)
|
||||
@@ -1074,6 +1083,7 @@ export default function Page() {
|
||||
}
|
||||
|
||||
const focusReviewDiff = (path: string) => {
|
||||
void tabs().open("review")
|
||||
openReviewPanel()
|
||||
view().review.openPath(path)
|
||||
setTree({ activeDiff: path, pendingDiff: path })
|
||||
@@ -1124,10 +1134,7 @@ export default function Page() {
|
||||
const id = params.id
|
||||
if (!id) return
|
||||
|
||||
const wants = isDesktop()
|
||||
? desktopFileTreeOpen() || (desktopReviewOpen() && activeTab() === "review")
|
||||
: store.mobileTab === "changes"
|
||||
if (!wants) return
|
||||
if (!wantsDiff()) return
|
||||
if (sync.data.session_diff[id] !== undefined) return
|
||||
if (sync.status === "loading") return
|
||||
|
||||
@@ -1136,13 +1143,7 @@ export default function Page() {
|
||||
|
||||
createEffect(
|
||||
on(
|
||||
() =>
|
||||
[
|
||||
sessionKey(),
|
||||
isDesktop()
|
||||
? desktopFileTreeOpen() || (desktopReviewOpen() && activeTab() === "review")
|
||||
: store.mobileTab === "changes",
|
||||
] as const,
|
||||
() => [sessionKey(), wantsDiff()] as const,
|
||||
([key, wants]) => {
|
||||
if (diffFrame !== undefined) cancelAnimationFrame(diffFrame)
|
||||
if (diffTimer !== undefined) window.clearTimeout(diffTimer)
|
||||
@@ -1167,19 +1168,6 @@ export default function Page() {
|
||||
),
|
||||
)
|
||||
|
||||
let treeDir: string | undefined
|
||||
createEffect(() => {
|
||||
const dir = sdk.directory
|
||||
if (!isDesktop()) return
|
||||
if (!layout.fileTree.opened()) return
|
||||
if (sync.status === "loading") return
|
||||
|
||||
fileTreeTab()
|
||||
const refresh = treeDir !== dir
|
||||
treeDir = dir
|
||||
void (refresh ? file.tree.refresh("") : file.tree.list(""))
|
||||
})
|
||||
|
||||
createEffect(
|
||||
on(
|
||||
() => sdk.directory,
|
||||
@@ -1296,9 +1284,9 @@ export default function Page() {
|
||||
const el = scroller
|
||||
if (!el) return
|
||||
if (el.scrollHeight > el.clientHeight + 1) return
|
||||
if (historyWindow.turnStart() <= 0 && !historyMore()) return
|
||||
if (historyWindow.turnStart() <= 0) return
|
||||
|
||||
void historyWindow.loadAndReveal()
|
||||
historyWindow.reveal()
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1309,14 +1297,13 @@ export default function Page() {
|
||||
params.id,
|
||||
messagesReady(),
|
||||
historyWindow.turnStart(),
|
||||
historyMore(),
|
||||
historyLoading(),
|
||||
autoScroll.userScrolled(),
|
||||
visibleUserMessages().length,
|
||||
] as const,
|
||||
([id, ready, start, more, loading, scrolled]) => {
|
||||
([id, ready, start, loading, scrolled]) => {
|
||||
if (!id || !ready || loading || scrolled) return
|
||||
if (start <= 0 && !more) return
|
||||
if (start <= 0) return
|
||||
fill()
|
||||
},
|
||||
{ defer: true },
|
||||
|
||||
@@ -49,7 +49,6 @@ export const createSessionTabs = (input: TabsInput) => {
|
||||
const first = openedTabs()[0]
|
||||
if (first) return first
|
||||
if (contextOpen()) return "context"
|
||||
if (review() && hasReview()) return "review"
|
||||
return "empty"
|
||||
})
|
||||
const activeFileTab = createMemo(() => {
|
||||
|
||||
@@ -13,7 +13,6 @@ import { useDialog } from "@opencode-ai/ui/context/dialog"
|
||||
|
||||
import FileTree from "@/components/file-tree"
|
||||
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"
|
||||
@@ -56,14 +55,15 @@ export function SessionSidePanel(props: {
|
||||
|
||||
const info = createMemo(() => (params.id ? sync.session.get(params.id) : undefined))
|
||||
const diffs = createMemo(() => (params.id ? (sync.data.session_diff[params.id] ?? []) : []))
|
||||
const changes = createMemo(() => {
|
||||
const id = params.id
|
||||
if (!id) return []
|
||||
const full = sync.data.session_diff[id]
|
||||
if (full !== undefined) return full
|
||||
return info()?.summary?.diffs ?? []
|
||||
})
|
||||
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
|
||||
return sync.data.session_diff[id] !== undefined
|
||||
})
|
||||
|
||||
const reviewEmptyKey = createMemo(() => {
|
||||
if (sync.project && !sync.project.vcs) return "session.review.noVcs"
|
||||
@@ -71,7 +71,7 @@ export function SessionSidePanel(props: {
|
||||
return "session.review.noChanges"
|
||||
})
|
||||
|
||||
const diffFiles = createMemo(() => diffs().map((d) => d.file))
|
||||
const diffFiles = createMemo(() => changes().map((d) => d.file))
|
||||
const kinds = createMemo(() => {
|
||||
const merge = (a: "add" | "del" | "mix" | undefined, b: "add" | "del" | "mix") => {
|
||||
if (!a) return b
|
||||
@@ -82,7 +82,7 @@ export function SessionSidePanel(props: {
|
||||
const normalize = (p: string) => p.replaceAll("\\\\", "/").replace(/\/+$/, "")
|
||||
|
||||
const out = new Map<string, "add" | "del" | "mix">()
|
||||
for (const diff of diffs()) {
|
||||
for (const diff of changes()) {
|
||||
const file = normalize(diff.file)
|
||||
const kind = diff.status === "added" ? "add" : diff.status === "deleted" ? "del" : "mix"
|
||||
|
||||
@@ -293,9 +293,11 @@ export function SessionSidePanel(props: {
|
||||
variant="ghost"
|
||||
iconSize="large"
|
||||
class="!rounded-md"
|
||||
onClick={() =>
|
||||
dialog.show(() => <DialogSelectFile mode="files" onOpenFile={showAllFiles} />)
|
||||
}
|
||||
onClick={() => {
|
||||
void import("@/components/dialog-select-file").then((x) =>
|
||||
dialog.show(() => <x.DialogSelectFile mode="files" onOpenFile={showAllFiles} />),
|
||||
)
|
||||
}}
|
||||
aria-label={language.t("command.file.open")}
|
||||
/>
|
||||
</TooltipKeybind>
|
||||
@@ -386,26 +388,17 @@ export function SessionSidePanel(props: {
|
||||
</Tabs.List>
|
||||
<Tabs.Content value="changes" class="bg-background-stronger px-3 py-0">
|
||||
<Switch>
|
||||
<Match when={hasReview()}>
|
||||
<Show
|
||||
when={diffsReady()}
|
||||
fallback={
|
||||
<div class="px-2 py-2 text-12-regular text-text-weak">
|
||||
{language.t("common.loading")}
|
||||
{language.t("common.loading.ellipsis")}
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<FileTree
|
||||
path=""
|
||||
class="pt-3"
|
||||
allowed={diffFiles()}
|
||||
kinds={kinds()}
|
||||
draggable={false}
|
||||
active={props.activeDiff}
|
||||
onFileClick={(node) => props.focusReviewDiff(node.path)}
|
||||
/>
|
||||
</Show>
|
||||
<Match when={hasReview() && diffFiles().length > 0}>
|
||||
<FileTree
|
||||
synthetic
|
||||
path=""
|
||||
class="pt-3"
|
||||
allowed={diffFiles()}
|
||||
kinds={kinds()}
|
||||
draggable={false}
|
||||
active={props.activeDiff}
|
||||
onFileClick={(node) => props.focusReviewDiff(node.path)}
|
||||
/>
|
||||
</Match>
|
||||
<Match when={true}>
|
||||
{empty(
|
||||
|
||||
@@ -11,10 +11,6 @@ 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"
|
||||
@@ -257,7 +253,11 @@ export const useSessionCommands = (actions: SessionCommandContext) => {
|
||||
description: language.t("palette.search.placeholder"),
|
||||
keybind: "mod+k,mod+p",
|
||||
slash: "open",
|
||||
onSelect: () => dialog.show(() => <DialogSelectFile onOpenFile={showAllFiles} />),
|
||||
onSelect: () => {
|
||||
void import("@/components/dialog-select-file").then((x) =>
|
||||
dialog.show(() => <x.DialogSelectFile onOpenFile={showAllFiles} />),
|
||||
)
|
||||
},
|
||||
}),
|
||||
fileCommand({
|
||||
id: "tab.close",
|
||||
@@ -351,7 +351,11 @@ export const useSessionCommands = (actions: SessionCommandContext) => {
|
||||
description: language.t("command.model.choose.description"),
|
||||
keybind: "mod+'",
|
||||
slash: "model",
|
||||
onSelect: () => dialog.show(() => <DialogSelectModel model={local.model} />),
|
||||
onSelect: () => {
|
||||
void import("@/components/dialog-select-model").then((x) =>
|
||||
dialog.show(() => <x.DialogSelectModel model={local.model} />),
|
||||
)
|
||||
},
|
||||
}),
|
||||
mcpCommand({
|
||||
id: "mcp.toggle",
|
||||
@@ -359,7 +363,9 @@ export const useSessionCommands = (actions: SessionCommandContext) => {
|
||||
description: language.t("command.mcp.toggle.description"),
|
||||
keybind: "mod+;",
|
||||
slash: "mcp",
|
||||
onSelect: () => dialog.show(() => <DialogSelectMcp />),
|
||||
onSelect: () => {
|
||||
void import("@/components/dialog-select-mcp").then((x) => dialog.show(() => <x.DialogSelectMcp />))
|
||||
},
|
||||
}),
|
||||
agentCommand({
|
||||
id: "agent.cycle",
|
||||
@@ -487,7 +493,9 @@ export const useSessionCommands = (actions: SessionCommandContext) => {
|
||||
description: language.t("command.session.fork.description"),
|
||||
slash: "fork",
|
||||
disabled: !params.id || visibleUserMessages().length === 0,
|
||||
onSelect: () => dialog.show(() => <DialogFork />),
|
||||
onSelect: () => {
|
||||
void import("@/components/dialog-fork").then((x) => dialog.show(() => <x.DialogFork />))
|
||||
},
|
||||
}),
|
||||
...share,
|
||||
]
|
||||
|
||||
@@ -123,6 +123,15 @@ export const SessionRoutes = lazy(() =>
|
||||
const sessionID = c.req.valid("param").sessionID
|
||||
log.info("SEARCH", { url: c.req.url })
|
||||
const session = await Session.get(sessionID)
|
||||
if (session.summary?.files) {
|
||||
const diffs = await SessionSummary.list(sessionID)
|
||||
if (diffs.length > 0) {
|
||||
session.summary = {
|
||||
...session.summary,
|
||||
diffs,
|
||||
}
|
||||
}
|
||||
}
|
||||
return c.json(session)
|
||||
},
|
||||
)
|
||||
|
||||
@@ -51,6 +51,58 @@ globalThis.AI_SDK_LOG_WARNINGS = false
|
||||
const csp = (hash = "") =>
|
||||
`default-src 'self'; script-src 'self' 'wasm-unsafe-eval'${hash ? ` 'sha256-${hash}'` : ""}; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self' data:; media-src 'self' data:; connect-src 'self' data:`
|
||||
|
||||
const api = (path: string) =>
|
||||
path === "/agent" ||
|
||||
path === "/command" ||
|
||||
path === "/formatter" ||
|
||||
path === "/log" ||
|
||||
path === "/lsp" ||
|
||||
path === "/path" ||
|
||||
path === "/skill" ||
|
||||
path === "/vcs" ||
|
||||
path.startsWith("/auth/") ||
|
||||
path.startsWith("/config") ||
|
||||
path.startsWith("/experimental") ||
|
||||
path.startsWith("/global") ||
|
||||
path.startsWith("/mcp") ||
|
||||
path.startsWith("/permission") ||
|
||||
path.startsWith("/project") ||
|
||||
path.startsWith("/provider") ||
|
||||
path.startsWith("/question") ||
|
||||
path.startsWith("/session")
|
||||
|
||||
const json = (value: string | null) => {
|
||||
const type = value?.split(";")[0]?.trim()
|
||||
return type === "application/json" || type?.endsWith("+json")
|
||||
}
|
||||
|
||||
const gzip = (value?: string) => {
|
||||
if (!value) return false
|
||||
|
||||
let star = false
|
||||
for (const item of value.split(",")) {
|
||||
const [name, ...params] = item.trim().toLowerCase().split(";")
|
||||
const q = params.find((part) => part.trim().startsWith("q="))
|
||||
const score = q ? Number(q.trim().slice(2)) : 1
|
||||
const ok = !Number.isNaN(score) && score > 0
|
||||
|
||||
if (name === "gzip") return ok
|
||||
if (name === "*") star = ok
|
||||
}
|
||||
|
||||
return star
|
||||
}
|
||||
|
||||
const vary = (headers: Headers, value: string) => {
|
||||
const current = headers.get("Vary")
|
||||
if (!current) {
|
||||
headers.set("Vary", value)
|
||||
return
|
||||
}
|
||||
if (current.split(",").some((item) => item.trim().toLowerCase() === value.toLowerCase())) return
|
||||
headers.set("Vary", `${current}, ${value}`)
|
||||
}
|
||||
|
||||
export namespace Server {
|
||||
const log = Log.create({ service: "server" })
|
||||
|
||||
@@ -104,6 +156,26 @@ export namespace Server {
|
||||
timer.stop()
|
||||
}
|
||||
})
|
||||
.use(async (c, next) => {
|
||||
await next()
|
||||
|
||||
if (!api(c.req.path)) return
|
||||
if (c.req.method === "HEAD") return
|
||||
if (c.res.headers.has("Content-Encoding")) return
|
||||
if (c.res.headers.has("Transfer-Encoding")) return
|
||||
if (!json(c.res.headers.get("Content-Type"))) return
|
||||
|
||||
if (!gzip(c.req.header("Accept-Encoding"))) return
|
||||
|
||||
const size = Number(c.res.headers.get("Content-Length") ?? "")
|
||||
if (Number.isFinite(size) && size > 0 && size < 1024) return
|
||||
if (!c.res.body) return
|
||||
|
||||
c.res = new Response(c.res.body.pipeThrough(new CompressionStream("gzip")), c.res)
|
||||
c.res.headers.delete("Content-Length")
|
||||
c.res.headers.set("Content-Encoding", "gzip")
|
||||
vary(c.res.headers, "Accept-Encoding")
|
||||
})
|
||||
.use(
|
||||
cors({
|
||||
origin(input) {
|
||||
|
||||
@@ -12,6 +12,14 @@ import { Bus } from "@/bus"
|
||||
import { NotFoundError } from "@/storage/db"
|
||||
|
||||
export namespace SessionSummary {
|
||||
function shape(diffs: Snapshot.FileDiff[]) {
|
||||
return diffs.map((item) => ({
|
||||
...item,
|
||||
before: "",
|
||||
after: "",
|
||||
}))
|
||||
}
|
||||
|
||||
function unquoteGitPath(input: string) {
|
||||
if (!input.startsWith('"')) return input
|
||||
if (!input.endsWith('"')) return input
|
||||
@@ -141,6 +149,8 @@ export namespace SessionSummary {
|
||||
},
|
||||
)
|
||||
|
||||
export const list = fn(SessionID.zod, async (sessionID) => shape(await diff({ sessionID })))
|
||||
|
||||
export async function computeDiff(input: { messages: MessageV2.WithParts[] }) {
|
||||
let from: string | undefined
|
||||
let to: string | undefined
|
||||
|
||||
@@ -5,6 +5,31 @@ import { type Config } from "./gen/client/types.gen.js"
|
||||
import { OpencodeClient } from "./gen/sdk.gen.js"
|
||||
export { type Config as OpencodeClientConfig, OpencodeClient }
|
||||
|
||||
const keys = [
|
||||
["x-opencode-directory", "directory"],
|
||||
["x-opencode-workspace", "workspace"],
|
||||
] as const
|
||||
|
||||
function move(req: Request) {
|
||||
if (req.method !== "GET" && req.method !== "HEAD") return req
|
||||
|
||||
let url: URL | undefined
|
||||
|
||||
for (const [header, key] of keys) {
|
||||
const value = req.headers.get(header)
|
||||
if (!value) continue
|
||||
url ??= new URL(req.url)
|
||||
if (!url.searchParams.has(key)) url.searchParams.set(key, value)
|
||||
}
|
||||
|
||||
if (!url) return req
|
||||
const next = new Request(url, req)
|
||||
for (const [header] of keys) {
|
||||
next.headers.delete(header)
|
||||
}
|
||||
return next
|
||||
}
|
||||
|
||||
export function createOpencodeClient(config?: Config & { directory?: string }) {
|
||||
if (!config?.fetch) {
|
||||
const customFetch: any = (req: any) => {
|
||||
@@ -26,5 +51,8 @@ export function createOpencodeClient(config?: Config & { directory?: string }) {
|
||||
}
|
||||
|
||||
const client = createClient(config)
|
||||
if (typeof window === "object" && typeof document === "object") {
|
||||
client.interceptors.request.use(move)
|
||||
}
|
||||
return new OpencodeClient({ client })
|
||||
}
|
||||
|
||||
@@ -5,6 +5,31 @@ import { type Config } from "./gen/client/types.gen.js"
|
||||
import { OpencodeClient } from "./gen/sdk.gen.js"
|
||||
export { type Config as OpencodeClientConfig, OpencodeClient }
|
||||
|
||||
const keys = [
|
||||
["x-opencode-directory", "directory"],
|
||||
["x-opencode-workspace", "workspace"],
|
||||
] as const
|
||||
|
||||
function move(req: Request) {
|
||||
if (req.method !== "GET" && req.method !== "HEAD") return req
|
||||
|
||||
let url: URL | undefined
|
||||
|
||||
for (const [header, key] of keys) {
|
||||
const value = req.headers.get(header)
|
||||
if (!value) continue
|
||||
url ??= new URL(req.url)
|
||||
if (!url.searchParams.has(key)) url.searchParams.set(key, value)
|
||||
}
|
||||
|
||||
if (!url) return req
|
||||
const next = new Request(url, req)
|
||||
for (const [header] of keys) {
|
||||
next.headers.delete(header)
|
||||
}
|
||||
return next
|
||||
}
|
||||
|
||||
export function createOpencodeClient(config?: Config & { directory?: string; experimental_workspaceID?: string }) {
|
||||
if (!config?.fetch) {
|
||||
const customFetch: any = (req: any) => {
|
||||
@@ -35,5 +60,8 @@ export function createOpencodeClient(config?: Config & { directory?: string; exp
|
||||
}
|
||||
|
||||
const client = createClient(config)
|
||||
if (typeof window === "object" && typeof document === "object") {
|
||||
client.interceptors.request.use(move)
|
||||
}
|
||||
return new OpencodeClient({ client })
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ import ibmPlexMonoBold from "../assets/fonts/ibm-plex-mono-bold.woff2"
|
||||
import ibmPlexMonoMedium from "../assets/fonts/ibm-plex-mono-medium.woff2"
|
||||
import ibmPlexMonoRegular from "../assets/fonts/ibm-plex-mono.woff2"
|
||||
|
||||
export const Font = () => {
|
||||
export const Font = (props: { preloadMono?: boolean }) => {
|
||||
return (
|
||||
<>
|
||||
<Style>{`
|
||||
@@ -56,7 +56,9 @@ export const Font = () => {
|
||||
`}</Style>
|
||||
<Show when={typeof location === "undefined" || location.protocol !== "file:"}>
|
||||
<Link rel="preload" href={inter} as="font" type="font/woff2" crossorigin="anonymous" />
|
||||
<Link rel="preload" href={ibmPlexMonoRegular} as="font" type="font/woff2" crossorigin="anonymous" />
|
||||
<Show when={props.preloadMono !== false}>
|
||||
<Link rel="preload" href={ibmPlexMonoRegular} as="font" type="font/woff2" crossorigin="anonymous" />
|
||||
</Show>
|
||||
</Show>
|
||||
</>
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user