From 3ff0eb3065e477af8f9c7c50c37c2b04e919e689 Mon Sep 17 00:00:00 2001 From: Aiden Cline Date: Wed, 26 Nov 2025 14:55:39 -0600 Subject: [PATCH 01/15] Revert "fix: disable virtual extmarks for file/agent mentions (#4731)" This reverts commit 673dbeee09d30c32bf4a6c93a96cfa7bb147dfa1. --- .../opencode/src/cli/cmd/tui/component/prompt/autocomplete.tsx | 1 + packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx | 1 + 2 files changed, 2 insertions(+) diff --git a/packages/opencode/src/cli/cmd/tui/component/prompt/autocomplete.tsx b/packages/opencode/src/cli/cmd/tui/component/prompt/autocomplete.tsx index 5780be4e9..8371c395f 100644 --- a/packages/opencode/src/cli/cmd/tui/component/prompt/autocomplete.tsx +++ b/packages/opencode/src/cli/cmd/tui/component/prompt/autocomplete.tsx @@ -81,6 +81,7 @@ export function Autocomplete(props: { const extmarkId = input.extmarks.create({ start: extmarkStart, end: extmarkEnd, + virtual: true, styleId, typeId: props.promptPartTypeId(), }) diff --git a/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx b/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx index 19986f8b6..90fb4c982 100644 --- a/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx +++ b/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx @@ -310,6 +310,7 @@ export function Prompt(props: PromptProps) { const extmarkId = input.extmarks.create({ start, end, + virtual: true, styleId, typeId: promptPartTypeId, }) From 99d7ff47c409a80a9c3b8217e8ca09383f4e70ac Mon Sep 17 00:00:00 2001 From: Aiden Cline <63023139+rekram1-node@users.noreply.github.com> Date: Wed, 26 Nov 2025 15:33:43 -0800 Subject: [PATCH 02/15] enable parcel file watcher, expand parcel ignore patterns, replace fs watcher for git branches with parcel (#4805) --- packages/opencode/src/file/ignore.ts | 10 +++++ packages/opencode/src/file/watcher.ts | 52 +++++++++++++++--------- packages/opencode/src/project/project.ts | 8 ++++ packages/opencode/src/project/vcs.ts | 50 ++++++++--------------- packages/sdk/js/src/gen/types.gen.ts | 19 +++++---- 5 files changed, 77 insertions(+), 62 deletions(-) diff --git a/packages/opencode/src/file/ignore.ts b/packages/opencode/src/file/ignore.ts index 2e1d1428f..7230f67af 100644 --- a/packages/opencode/src/file/ignore.ts +++ b/packages/opencode/src/file/ignore.ts @@ -6,6 +6,7 @@ export namespace FileIgnore { "bower_components", ".pnpm-store", "vendor", + ".npm", "dist", "build", "out", @@ -22,12 +23,21 @@ export namespace FileIgnore { ".output", "desktop", ".sst", + ".cache", + ".webkit-cache", + "__pycache__", + ".pytest_cache", + "mypy_cache", + ".history", + ".gradle", ]) const FILES = [ "**/*.swp", "**/*.swo", + "**/*.pyc", + // OS "**/.DS_Store", "**/Thumbs.db", diff --git a/packages/opencode/src/file/watcher.ts b/packages/opencode/src/file/watcher.ts index d5985b582..4459d20e9 100644 --- a/packages/opencode/src/file/watcher.ts +++ b/packages/opencode/src/file/watcher.ts @@ -1,6 +1,5 @@ import z from "zod" import { Bus } from "../bus" -import { Flag } from "../flag/flag" import { Instance } from "../project/instance" import { Log } from "../util/log" import { FileIgnore } from "./ignore" @@ -8,6 +7,7 @@ import { Config } from "../config/config" // @ts-ignore import { createWrapper } from "@parcel/watcher/wrapper" import { lazy } from "@/util/lazy" +import type ParcelWatcher from "@parcel/watcher" export namespace FileWatcher { const log = Log.create({ service: "file.watcher" }) @@ -44,32 +44,46 @@ export namespace FileWatcher { return {} } log.info("watcher backend", { platform: process.platform, backend }) - const sub = await watcher().subscribe( - Instance.directory, - (err, evts) => { - if (err) return - for (const evt of evts) { - log.info("event", evt) - if (evt.type === "create") Bus.publish(Event.Updated, { file: evt.path, event: "add" }) - if (evt.type === "update") Bus.publish(Event.Updated, { file: evt.path, event: "change" }) - if (evt.type === "delete") Bus.publish(Event.Updated, { file: evt.path, event: "unlink" }) - } - }, - { - ignore: [...FileIgnore.PATTERNS, ...(cfg.watcher?.ignore ?? [])], + const subscribe: ParcelWatcher.SubscribeCallback = (err, evts) => { + if (err) return + for (const evt of evts) { + log.info("event", evt) + if (evt.type === "create") Bus.publish(Event.Updated, { file: evt.path, event: "add" }) + if (evt.type === "update") Bus.publish(Event.Updated, { file: evt.path, event: "change" }) + if (evt.type === "delete") Bus.publish(Event.Updated, { file: evt.path, event: "unlink" }) + } + } + + const subs = [] + const cfgIgnores = cfg.watcher?.ignore ?? [] + + subs.push( + await watcher().subscribe(Instance.directory, subscribe, { + ignore: [...FileIgnore.PATTERNS, ...cfgIgnores], backend, - }, + }), ) - return { sub } + + const vcsDir = Instance.project.vcsDir + if (vcsDir && !cfgIgnores.includes(".git") && !cfgIgnores.includes(vcsDir)) { + subs.push( + await watcher().subscribe(vcsDir, subscribe, { + ignore: ["hooks", "info", "logs", "objects", "refs", "worktrees", "modules", "lfs"], + backend, + }), + ) + } + + return { subs } }, async (state) => { - if (!state.sub) return - await state.sub?.unsubscribe() + if (!state.subs) return + await Promise.all(state.subs.map((sub) => sub?.unsubscribe())) }, ) export function init() { - if (!Flag.OPENCODE_EXPERIMENTAL_WATCHER) return + // if (!Flag.OPENCODE_EXPERIMENTAL_WATCHER) return state() } } diff --git a/packages/opencode/src/project/project.ts b/packages/opencode/src/project/project.ts index 381559e80..74e969145 100644 --- a/packages/opencode/src/project/project.ts +++ b/packages/opencode/src/project/project.ts @@ -12,6 +12,7 @@ export namespace Project { .object({ id: z.string(), worktree: z.string(), + vcsDir: z.string().optional(), vcs: z.literal("git").optional(), time: z.object({ created: z.number(), @@ -80,9 +81,16 @@ export namespace Project { .cwd(worktree) .text() .then((x) => x.trim()) + const vcsDir = await $`git rev-parse --path-format=absolute --git-dir` + .quiet() + .nothrow() + .cwd(worktree) + .text() + .then((x) => x.trim()) const project: Info = { id, worktree, + vcsDir, vcs: "git", time: { created: Date.now(), diff --git a/packages/opencode/src/project/vcs.ts b/packages/opencode/src/project/vcs.ts index c3b21f3be..a8d5e91b3 100644 --- a/packages/opencode/src/project/vcs.ts +++ b/packages/opencode/src/project/vcs.ts @@ -1,10 +1,10 @@ import { $ } from "bun" -import { watch, type FSWatcher } from "fs" import path from "path" import z from "zod" import { Log } from "@/util/log" import { Bus } from "@/bus" import { Instance } from "./instance" +import { FileWatcher } from "@/file/watcher" const log = Log.create({ service: "vcs" }) @@ -39,49 +39,31 @@ export namespace Vcs { const state = Instance.state( async () => { - if (Instance.project.vcs !== "git") { - return { branch: async () => undefined, watcher: undefined } + const vcsDir = Instance.project.vcsDir + if (Instance.project.vcs !== "git" || !vcsDir) { + return { branch: async () => undefined, unsubscribe: undefined } } let current = await currentBranch() log.info("initialized", { branch: current }) - const gitDir = await $`git rev-parse --git-dir` - .quiet() - .nothrow() - .cwd(Instance.worktree) - .text() - .then((x) => x.trim()) - .catch(() => undefined) - if (!gitDir) { - log.warn("failed to resolve git directory") - return { branch: async () => current, watcher: undefined } - } - - const gitHead = path.join(gitDir, "HEAD") - let watcher: FSWatcher | undefined - // we should probably centralize file watching (see watcher.ts) - // but parcel still marked experimental rn - try { - watcher = watch(gitHead, async () => { - const next = await currentBranch() - if (next !== current) { - log.info("branch changed", { from: current, to: next }) - current = next - Bus.publish(Event.BranchUpdated, { branch: next }) - } - }) - log.info("watching", { path: gitHead }) - } catch (e) { - log.warn("failed to watch git HEAD", { error: e }) - } + const head = path.join(vcsDir, "HEAD") + const unsubscribe = Bus.subscribe(FileWatcher.Event.Updated, async (evt) => { + if (evt.properties.file !== head) return + const next = await currentBranch() + if (next !== current) { + log.info("branch changed", { from: current, to: next }) + current = next + Bus.publish(Event.BranchUpdated, { branch: next }) + } + }) return { branch: async () => current, - watcher, + unsubscribe, } }, async (state) => { - state.watcher?.close() + state.unsubscribe?.() }, ) diff --git a/packages/sdk/js/src/gen/types.gen.ts b/packages/sdk/js/src/gen/types.gen.ts index efa5c9946..bf23f77ec 100644 --- a/packages/sdk/js/src/gen/types.gen.ts +++ b/packages/sdk/js/src/gen/types.gen.ts @@ -589,6 +589,14 @@ export type EventSessionError = { } } +export type EventFileWatcherUpdated = { + type: "file.watcher.updated" + properties: { + file: string + event: "add" | "change" | "unlink" + } +} + export type EventVcsBranchUpdated = { type: "vcs.branch.updated" properties: { @@ -647,14 +655,6 @@ export type EventServerConnected = { } } -export type EventFileWatcherUpdated = { - type: "file.watcher.updated" - properties: { - file: string - event: "add" | "change" | "unlink" - } -} - export type Event = | EventInstallationUpdated | EventInstallationUpdateAvailable @@ -677,12 +677,12 @@ export type Event = | EventSessionDeleted | EventSessionDiff | EventSessionError + | EventFileWatcherUpdated | EventVcsBranchUpdated | EventTuiPromptAppend | EventTuiCommandExecute | EventTuiToastShow | EventServerConnected - | EventFileWatcherUpdated export type GlobalEvent = { directory: string @@ -692,6 +692,7 @@ export type GlobalEvent = { export type Project = { id: string worktree: string + vcsDir?: string vcs?: "git" time: { created: number From 63bfe767200f4caf9a6c808af085e293f9816e99 Mon Sep 17 00:00:00 2001 From: Dax Date: Wed, 26 Nov 2025 20:11:39 -0500 Subject: [PATCH 03/15] tui design refinement (#4809) --- .opencode/opencode.jsonc | 8 +- packages/opencode/src/cli/cmd/tui/app.tsx | 53 +-- .../cli/cmd/tui/component/dialog-provider.tsx | 13 + .../cmd/tui/component/prompt/autocomplete.tsx | 5 + .../cli/cmd/tui/component/prompt/index.tsx | 13 +- .../src/cli/cmd/tui/context/directory.ts | 12 + .../opencode/src/cli/cmd/tui/routes/home.tsx | 52 +-- .../src/cli/cmd/tui/routes/session/footer.tsx | 37 ++ .../src/cli/cmd/tui/routes/session/header.tsx | 93 +++-- .../src/cli/cmd/tui/routes/session/index.tsx | 37 +- .../cli/cmd/tui/routes/session/sidebar.tsx | 333 +++++++++++------- 11 files changed, 389 insertions(+), 267 deletions(-) create mode 100644 packages/opencode/src/cli/cmd/tui/context/directory.ts create mode 100644 packages/opencode/src/cli/cmd/tui/routes/session/footer.tsx diff --git a/.opencode/opencode.jsonc b/.opencode/opencode.jsonc index dd5a4c750..369832f9f 100644 --- a/.opencode/opencode.jsonc +++ b/.opencode/opencode.jsonc @@ -2,7 +2,7 @@ "$schema": "https://opencode.ai/config.json", "plugin": ["opencode-openai-codex-auth"], // "enterprise": { - // "url": "http://localhost:3000", + // "url": "https://enterprise.dev.opencode.ai", // }, "provider": { "opencode": { @@ -11,4 +11,10 @@ }, }, }, + "mcp": { + "exa": { + "type": "remote", + "url": "https://mcp.exa.ai/mcp", + }, + }, } diff --git a/packages/opencode/src/cli/cmd/tui/app.tsx b/packages/opencode/src/cli/cmd/tui/app.tsx index 7c72274ad..5ec737256 100644 --- a/packages/opencode/src/cli/cmd/tui/app.tsx +++ b/packages/opencode/src/cli/cmd/tui/app.tsx @@ -452,51 +452,14 @@ function App() { } }} > - - - - - - - - - - - - - - open - - code{" "} - - v{Installation.VERSION} - - - - {process.cwd().replace(Global.Path.home, "~")} - {sync.data.vcs?.branch ? `:${sync.data.vcs.branch}` : ""} - - - - - - - tab - - {""} - - {local.agent.current().name.toUpperCase()} - AGENT - - - - + + + + + + + + ) } diff --git a/packages/opencode/src/cli/cmd/tui/component/dialog-provider.tsx b/packages/opencode/src/cli/cmd/tui/component/dialog-provider.tsx index 109d4d25a..30a8bb2fc 100644 --- a/packages/opencode/src/cli/cmd/tui/component/dialog-provider.tsx +++ b/packages/opencode/src/cli/cmd/tui/component/dialog-provider.tsx @@ -197,11 +197,24 @@ function ApiMethod(props: ApiMethodProps) { const dialog = useDialog() const sdk = useSDK() const sync = useSync() + const { theme } = useTheme() return ( + + OpenCode Zen gives you access to all the best coding models at the cheapest prices with a single API key. + + + Go to https://opencode.ai/zen to get a key + + + ) : undefined + } onConfirm={async (value) => { if (!value) return sdk.client.auth.set({ diff --git a/packages/opencode/src/cli/cmd/tui/component/prompt/autocomplete.tsx b/packages/opencode/src/cli/cmd/tui/component/prompt/autocomplete.tsx index 8371c395f..4232f3ae8 100644 --- a/packages/opencode/src/cli/cmd/tui/component/prompt/autocomplete.tsx +++ b/packages/opencode/src/cli/cmd/tui/component/prompt/autocomplete.tsx @@ -292,6 +292,11 @@ export function Autocomplete(props: { description: "open editor", onSelect: () => command.trigger("prompt.editor", "prompt"), }, + { + display: "/connect", + description: "connect to a provider", + onSelect: () => command.trigger("provider.connect"), + }, { display: "/help", description: "show help", diff --git a/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx b/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx index 90fb4c982..06e9a49e6 100644 --- a/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx +++ b/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx @@ -637,11 +637,7 @@ export function Prompt(props: PromptProps) { flexGrow={1} >