Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
77 commits
Select commit Hold shift + click to select a range
233427f
test(core): cover skill directory output (#31263)
rekram1-node Jun 7, 2026
aacdb34
fix(opencode): avoid duplicate skill catalog (#31269)
rekram1-node Jun 7, 2026
8ff4013
chore(opencode): update MCP SDK to 1.29.0 (#31268)
rekram1-node Jun 7, 2026
c495635
chore: generate
opencode-agent[bot] Jun 7, 2026
914a643
chore: update nix node_modules hashes
opencode-agent[bot] Jun 7, 2026
07808be
run: make minimal mode more minimal (#31227)
simonklee Jun 7, 2026
3867fa2
chore: generate
opencode-agent[bot] Jun 7, 2026
b1d14ac
chore: update web and desktop code owners (#31289)
Hona Jun 8, 2026
65a3f7f
fix(desktop): few WSL bugs (#31095)
neriousy Jun 8, 2026
4d09a71
chore: generate
opencode-agent[bot] Jun 8, 2026
b5cb9aa
fix(opencode): respect MCP server capabilities (#31271)
rekram1-node Jun 8, 2026
4863aed
console: update email
fwang Jun 8, 2026
79ea379
zen: fix
fwang Jun 8, 2026
685a894
fix(opencode): include acp pending tool input (#31321)
nexxeln Jun 8, 2026
bea56fe
fix(lsp): resolve JDTLS root to topmost pom.xml in Java Maven multi-m…
areyouok Jun 8, 2026
d46af9c
chore: generate
opencode-agent[bot] Jun 8, 2026
0050134
fix(session): merge per-call tool rules into session permission (#30529)
remorses Jun 8, 2026
1772e8e
docs(go): update MiniMax M3 pricing (#31350)
MrMushrooooom Jun 8, 2026
b1a6c40
fix: speed up fff file search (#31366)
nexxeln Jun 8, 2026
89e371c
chore: generate
opencode-agent[bot] Jun 8, 2026
f116a55
fix(stats): show new for leaderboard deltas
adamdotdevin Jun 8, 2026
4119051
fix: stabilize fff file results (#31369)
nexxeln Jun 8, 2026
ea5f976
chore: generate
opencode-agent[bot] Jun 8, 2026
89e2a23
fix(core): restore npm proxy agent patch (#31373)
rekram1-node Jun 8, 2026
f3f59db
chore: update nix node_modules hashes
opencode-agent[bot] Jun 8, 2026
31d2fec
chore: upgrade OpenTUI to v0.3.4 (#31326)
simonklee Jun 8, 2026
9654412
chore: update nix node_modules hashes
opencode-agent[bot] Jun 8, 2026
b34d924
fix(core): disable fff trace logs (#31380)
nexxeln Jun 8, 2026
f43209b
fix(session): avoid sticky prompt tool overrides (#31394)
nexxeln Jun 8, 2026
0a7cb20
fix(opencode): await run event loop (#31389)
rekram1-node Jun 8, 2026
c06ad7c
refactor(core): replace legacy logger with Effect logging (#31310)
thdxr Jun 8, 2026
5376661
chore: generate
opencode-agent[bot] Jun 8, 2026
fc52c5a
fix(tui): trim select footer action highlight (#31411)
jlongster Jun 8, 2026
6e84142
fix(opencode): support MiniMax M3 thinking toggle (#31426)
rekram1-node Jun 8, 2026
a86ecf3
fix: adjust item id stripping to happen prior to request signing (#31…
rekram1-node Jun 8, 2026
161247c
fix(opencode): generate reasoning variants for all OpenRouter models.…
AnthonyMLau Jun 9, 2026
f565ff3
feat(app): add draft tab support to tabs store (#31343)
Brendonovich Jun 9, 2026
277ecc5
chore: generate
opencode-agent[bot] Jun 9, 2026
0efc334
fix(opencode): paginate MCP catalogs (#31442)
rekram1-node Jun 9, 2026
79cff28
fix(opencode): pass abort signal to MCP tool calls (#31455)
rekram1-node Jun 9, 2026
1a08ee7
feat(app): draft prompt state (#31452)
Brendonovich Jun 9, 2026
50c9d52
chore: generate
opencode-agent[bot] Jun 9, 2026
384a8f5
feat(app): tabs help button (#31454)
Brendonovich Jun 9, 2026
ab701d2
feat: add "reasoning" as interleaved field option for vLLM providers …
delta9000 Jun 9, 2026
5372c63
fix(app): clip rounded session panels (#31462)
Hona Jun 9, 2026
2ababc8
core: fix idle CPU use in file logger (#31478)
simonklee Jun 9, 2026
671d193
docs: add uninstall troubleshooting steps (#31424)
opencode-agent[bot] Jun 9, 2026
7c1e61a
leave a breadcrumb comment about batchWindow zero (#31508)
kommander Jun 9, 2026
b4a6419
chore: stats -> data
adamdotdevin Jun 9, 2026
960eace
test(core): avoid Windows worker close race (#31532)
rekram1-node Jun 9, 2026
3752218
refactor(tui): centralize application exit (#31524)
thdxr Jun 9, 2026
8415ff5
chore: generate
opencode-agent[bot] Jun 9, 2026
0bb677c
feat(opencode): configure Cohere North model (#31536)
rekram1-node Jun 9, 2026
6566ede
refactor(core): consolidate references (#31539)
thdxr Jun 9, 2026
ffcb45d
feat(tui): show project copy in session list (#31421)
jlongster Jun 9, 2026
ada5b31
chore: generate
opencode-agent[bot] Jun 9, 2026
f1c31f4
fix(stats): use data branding assets
adamdotdevin Jun 9, 2026
db9391e
drop citation_options from cohere (#31543)
rekram1-node Jun 9, 2026
531eebb
zen: add north mini code model
fwang Jun 9, 2026
d68397b
fix(data): timestamp formatting
adamdotdevin Jun 9, 2026
c4bc902
fix(opencode): support Claude Fable reasoning (#31546)
rekram1-node Jun 9, 2026
0777cf1
fix(mcp): log actionable connection statuses (#31544)
rekram1-node Jun 9, 2026
132ef57
refactor(core): simplify location filesystem (#31545)
thdxr Jun 9, 2026
cc52dc3
chore: generate
opencode-agent[bot] Jun 9, 2026
1daf535
chore: update nix node_modules hashes
opencode-agent[bot] Jun 9, 2026
600e405
fix(opencode): restore effect error logging (#31551)
thdxr Jun 9, 2026
7a54a2c
chore: generate
opencode-agent[bot] Jun 9, 2026
07e5ea9
feat(opencode): add typed application layer graph (#31531)
jlongster Jun 9, 2026
9b8e356
zen: add claude fable 5
fwang Jun 9, 2026
c939aa0
chore: generate
opencode-agent[bot] Jun 9, 2026
381eabb
refactor(mcp): simplify service helpers (#31549)
rekram1-node Jun 9, 2026
80c0b06
feat: add X-Session-Id header for proxy cache routing affinity (#31511)
songchaow Jun 9, 2026
bc42187
zen: update email
fwang Jun 9, 2026
1cc94bb
fix(desktop): update Electron stack and panel layout (#31571)
Hona Jun 9, 2026
215fb39
chore: generate
opencode-agent[bot] Jun 9, 2026
ce4e658
chore: update nix node_modules hashes
opencode-agent[bot] Jun 9, 2026
0c6cf3e
chore: sync fork with upstream anomalyco/opencode (ce4e658e3)
shoootyou Jun 10, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
refactor(core): consolidate references (anomalyco#31539)
  • Loading branch information
thdxr authored Jun 9, 2026
commit 6566ede935069d9771e051ddbf60e087092d4395
63 changes: 63 additions & 0 deletions packages/core/src/config/plugin/reference.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
export * as ConfigReferencePlugin from "./reference"

import path from "path"
import { Effect } from "effect"
import { Config } from "../../config"
import { ConfigReference } from "../reference"
import { Global } from "../../global"
import { Location } from "../../location"
import { PluginV2 } from "../../plugin"
import { Reference } from "../../reference"
import { AbsolutePath } from "../../schema"

export const Plugin = {
id: PluginV2.ID.make("core/config-reference"),
effect: Effect.gen(function* () {
const config = yield* Config.Service
const global = yield* Global.Service
const location = yield* Location.Service
const references = yield* Reference.Service
const update = yield* references.transform()
const entries = new Map<string, Reference.Source>()
for (const doc of (yield* config.entries()).filter(
(entry): entry is Config.Document => entry.type === "document",
)) {
const directory = doc.path ? path.dirname(doc.path) : location.directory
for (const [name, entry] of Object.entries(doc.info.references ?? {})) {
if (!validAlias(name)) continue
entries.set(
name,
local(entry)
? new Reference.LocalSource({
type: "local",
path: AbsolutePath.make(localPath(directory, global.home, typeof entry === "string" ? entry : entry.path)),
})
: new Reference.GitSource({
type: "git",
repository: typeof entry === "string" ? entry : entry.repository,
branch: typeof entry === "string" ? undefined : entry.branch,
}),
)
}
}

yield* update((editor) => {
for (const [name, source] of entries) editor.add(name, source)
})
}),
}

function validAlias(name: string) {
return name.length > 0 && !/[\/\s`,]/.test(name)
}

function local(entry: ConfigReference.Entry): entry is string | ConfigReference.Local {
return typeof entry === "string"
? entry.startsWith(".") || entry.startsWith("/") || entry.startsWith("~")
: "path" in entry
}

function localPath(directory: string, home: string, value: string) {
if (value.startsWith("~/")) return path.join(home, value.slice(2))
return path.isAbsolute(value) ? value : path.resolve(directory, value)
}
30 changes: 0 additions & 30 deletions packages/core/src/config/reference.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,33 +16,3 @@ export type Entry = typeof Entry.Type

export const Info = Schema.Record(Schema.String, Entry)
export type Info = typeof Info.Type

export type NormalizedEntry =
| { readonly kind: "local"; readonly path: string }
| { readonly kind: "git"; readonly repository: string; readonly branch?: string }
| { readonly kind: "invalid"; readonly message: string }

export type NormalizedInfo = Record<string, NormalizedEntry>

export function validateAlias(name: string) {
if (name.length === 0) return "Reference alias must not be empty"
if (/[\/\s`,]/.test(name)) return "Reference alias must not contain /, whitespace, comma, or backtick"
}

export function normalizeEntry(entry: Entry): NormalizedEntry {
if (typeof entry === "string") {
if (entry.startsWith(".") || entry.startsWith("/") || entry.startsWith("~")) return { kind: "local", path: entry }
return { kind: "git", repository: entry }
}
if ("path" in entry) return { kind: "local", path: entry.path }
return { kind: "git", repository: entry.repository, branch: entry.branch }
}

export function normalize(info: Info): NormalizedInfo {
return Object.fromEntries(
Object.entries(info).map(([name, entry]) => {
const message = validateAlias(name)
return [name, message ? { kind: "invalid" as const, message } : normalizeEntry(entry)]
}),
)
}
40 changes: 12 additions & 28 deletions packages/core/src/filesystem.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,13 @@ import { EventV2 } from "./event"
import { FSUtil } from "./fs-util"
import { Global } from "./global"
import { Location } from "./location"
import { ProjectReference } from "./project-reference"
import { NonNegativeInt, PositiveInt, RelativePath } from "./schema"
import { Protected } from "./filesystem/protected"
import { Ripgrep } from "./filesystem/ripgrep"
import { ToolOutputStore } from "./tool-output-store"

export const ReadInput = Schema.Struct({
path: Schema.String,
reference: Schema.NonEmptyString.pipe(Schema.optional),
})
export type ReadInput = typeof ReadInput.Type

Expand Down Expand Up @@ -134,7 +132,6 @@ export class ReadPath extends Schema.Class<ReadPath>("FileSystem.ReadPath")({

export const ListInput = Schema.Struct({
path: Schema.String.pipe(Schema.optional),
reference: Schema.NonEmptyString.pipe(Schema.optional),
})
export type ListInput = typeof ListInput.Type

Expand All @@ -158,7 +155,6 @@ export class RootTarget extends Schema.Class<RootTarget>("FileSystem.RootTarget"
real: Schema.String,
root: Schema.String,
resource: Schema.String,
reference: Schema.NonEmptyString.pipe(Schema.optional),
type: Schema.Literals(["file", "directory"]),
}) {}

Expand Down Expand Up @@ -239,7 +235,6 @@ export const layer = Layer.effect(
const fs = yield* FSUtil.Service
const location = yield* Location.Service
const global = yield* Effect.serviceOption(Global.Service)
const references = yield* ProjectReference.Service
const ripgrep = yield* Ripgrep.Service
const root = yield* fs.realPath(location.directory).pipe(Effect.orDie)
const ignored = ignore()
Expand All @@ -251,21 +246,12 @@ export const layer = Layer.effect(
.readFileString(path.join(location.project.directory, ".ignore"))
.pipe(Effect.catch(() => Effect.succeed("")))
if (ignorefile) ignored.add(ignorefile)
const select = Effect.fnUntraced(function* (reference?: string) {
if (!reference) return { directory: location.directory, root }
const resolved = yield* references.get(reference)
if (!resolved) return yield* Effect.die(new Error(`Unknown project reference: ${reference}`))
if (resolved.kind === "invalid") return yield* Effect.die(new Error(resolved.message))
if (resolved.kind === "git") yield* references.ensurePath(resolved.path).pipe(Effect.orDie)
return { directory: resolved.path, root: yield* fs.realPath(resolved.path).pipe(Effect.orDie) }
})
const resolve = Effect.fnUntraced(function* (input?: string, reference?: string) {
const resolve = Effect.fnUntraced(function* (input?: string) {
const managed = path.join(
Option.match(global, { onNone: () => Global.Path.data, onSome: (value) => value.data }),
ToolOutputStore.MANAGED_DIRECTORY,
)
if (input && path.isAbsolute(input)) {
if (reference) return yield* Effect.die(new Error("Absolute paths cannot use a project reference"))
if (path.dirname(input) !== managed || !path.basename(input).startsWith("tool_"))
return yield* Effect.die(new Error("Absolute path is not managed tool output"))
const real = yield* fs.realPath(input).pipe(Effect.orDie)
Expand All @@ -274,9 +260,9 @@ export const layer = Layer.effect(
return yield* Effect.die(new Error("Path escapes managed tool output"))
return { absolute: input, real, directory: managed, root: managedRoot }
}
const selected = yield* select(reference)
const absolute = path.resolve(selected.directory, input ?? ".")
if (!FSUtil.contains(selected.directory, absolute))
const selected = { directory: location.directory, root }
const absolute = path.resolve(location.directory, input ?? ".")
if (!FSUtil.contains(location.directory, absolute))
return yield* Effect.die(new Error("Path escapes the location"))
const real = yield* fs.realPath(absolute).pipe(Effect.orDie)
if (!FSUtil.contains(selected.root, real)) return yield* Effect.die(new Error("Path escapes the location"))
Expand Down Expand Up @@ -335,24 +321,24 @@ export const layer = Layer.effect(
})

const resolveReadPath = Effect.fn("FileSystem.resolveReadPath")(function* (input: ReadInput) {
const target = yield* resolve(input.path, input.reference)
const target = yield* resolve(input.path)
const info = yield* fs.stat(target.real).pipe(Effect.orDie)
const type = info.type === "File" ? "file" : info.type === "Directory" ? "directory" : undefined
if (!type) return yield* Effect.die(new Error("Path is not a file or directory"))
const relative = path.relative(target.root, target.real).replaceAll("\\", "/") || "."
return new ReadPath({
type,
resource: input.reference === undefined ? relative : `${input.reference}:${relative}`,
resource: relative,
})
})
const resolveFile = Effect.fnUntraced(function* (input: ReadInput) {
const target = yield* resolve(input.path, input.reference)
const target = yield* resolve(input.path)
const info = yield* fs.stat(target.real).pipe(Effect.orDie)
if (info.type !== "File") return yield* Effect.die(new Error("Path is not a file"))
const relative = path.relative(target.root, target.real).replaceAll("\\", "/") || "."
return {
real: target.real,
resource: input.reference === undefined ? relative : `${input.reference}:${relative}`,
resource: relative,
}
})
const content = (target: { readonly real: string }, bytes: Uint8Array) =>
Expand Down Expand Up @@ -510,25 +496,24 @@ export const layer = Layer.effect(
)
})
const resolveList = Effect.fn("FileSystem.resolveList")(function* (input: ListInput = {}) {
const directory = yield* resolve(input.path, input.reference)
const directory = yield* resolve(input.path)
const info = yield* fs.stat(directory.real).pipe(Effect.orDie)
if (info.type !== "Directory") return yield* Effect.die(new Error("Path is not a directory"))
const relative = path.relative(directory.root, directory.real).replaceAll("\\", "/") || "."
return new ListTarget({
...directory,
resource: input.reference === undefined ? relative : `${input.reference}:${relative}`,
resource: relative,
})
})
const resolveRoot = Effect.fn("FileSystem.resolveRoot")(function* (input: ListInput = {}) {
const target = yield* resolve(input.path, input.reference)
const target = yield* resolve(input.path)
const info = yield* fs.stat(target.real).pipe(Effect.orDie)
const type = info.type === "File" ? "file" : info.type === "Directory" ? "directory" : undefined
if (!type) return yield* Effect.die(new Error("Path is not a file or directory"))
const relative = path.relative(target.root, target.real).replaceAll("\\", "/") || "."
return new RootTarget({
...target,
resource: input.reference === undefined ? relative : `${input.reference}:${relative}`,
reference: input.reference,
resource: relative,
type,
})
})
Expand Down Expand Up @@ -644,5 +629,4 @@ export const layer = Layer.effect(

export const locationLayer = layer.pipe(
Layer.provide(Ripgrep.defaultLayer),
Layer.provideMerge(ProjectReference.locationLayer),
)
4 changes: 2 additions & 2 deletions packages/core/src/location-layer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import { Watcher } from "./filesystem/watcher"
import { LocationMutation } from "./location-mutation"
import { LocationSearch } from "./location-search"
import { FileMutation } from "./file-mutation"
import { ProjectReference } from "./project-reference"
import { Reference } from "./reference"
import { RepositoryCache } from "./repository-cache"
import { Pty } from "./pty"
import { SkillV2 } from "./skill"
Expand Down Expand Up @@ -52,7 +52,7 @@ export class LocationServiceMap extends LayerMap.Service<LocationServiceMap>()("
location,
Policy.locationLayer,
Config.locationLayer,
ProjectReference.locationLayer,
Reference.locationLayer,
PluginV2.locationLayer,
Catalog.locationLayer,
CommandV2.locationLayer,
Expand Down
5 changes: 2 additions & 3 deletions packages/core/src/location-search.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,7 @@ import { NonNegativeInt, PositiveInt, RelativePath } from "./schema"

/**
* Location-scoped raw search substrate. Search authority is selected only by
* FileSystem, preserving Location-relative paths and named read
* references. Model formatting, leaf-tool permissions, and HTTP transport stay
* FileSystem, preserving Location-relative paths. Model formatting, leaf-tool permissions, and HTTP transport stay
* outside this service so future GlobTool, GrepTool, and HTTP consumers can
* share the same bounded filesystem behavior.
*
Expand Down Expand Up @@ -106,7 +105,7 @@ export const layer = Layer.effect(
return {
path: RelativePath.make(relative),
canonical,
resource: root.reference === undefined ? relative : `${root.reference}:${relative}`,
resource: relative,
mtime: info.mtime.pipe(
Option.map((date) => date.getTime()),
Option.getOrElse(() => 0),
Expand Down
7 changes: 7 additions & 0 deletions packages/core/src/plugin/boot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { Config } from "../config"
import { ConfigAgentPlugin } from "../config/plugin/agent"
import { ConfigCommandPlugin } from "../config/plugin/command"
import { ConfigSkillPlugin } from "../config/plugin/skill"
import { ConfigReferencePlugin } from "../config/plugin/reference"
import { EventV2 } from "../event"
import { FSUtil } from "../fs-util"
import { Global } from "../global"
Expand All @@ -25,6 +26,7 @@ import { EnvPlugin } from "./env"
import { ModelsDevPlugin } from "./models-dev"
import { ProviderPlugins } from "./provider"
import { SkillV2 } from "../skill"
import { Reference } from "../reference"

type Plugin = {
id: PluginV2.ID
Expand All @@ -42,6 +44,7 @@ type Plugin = {
| Config.Service
| ModelsDev.Service
| SkillV2.Service
| Reference.Service
>
}

Expand All @@ -67,6 +70,7 @@ export const layer = Layer.effect(
const fs = yield* FSUtil.Service
const global = yield* Global.Service
const skill = yield* SkillV2.Service
const references = yield* Reference.Service
const done = yield* Deferred.make<void>()

const add = Effect.fn("PluginBoot.add")(function* (input: Plugin) {
Expand All @@ -85,6 +89,7 @@ export const layer = Layer.effect(
Effect.provideService(FSUtil.Service, fs),
Effect.provideService(Global.Service, global),
Effect.provideService(SkillV2.Service, skill),
Effect.provideService(Reference.Service, references),
Effect.provideService(PluginV2.Service, plugin),
),
})
Expand All @@ -104,6 +109,7 @@ export const layer = Layer.effect(
yield* add(ConfigAgentPlugin.Plugin)
yield* add(ConfigCommandPlugin.Plugin)
yield* add(ConfigSkillPlugin.Plugin)
yield* add(ConfigReferencePlugin.Plugin)
}).pipe(Effect.withSpan("PluginBoot.boot"))

yield* boot.pipe(
Expand All @@ -124,4 +130,5 @@ export const locationLayer = layer.pipe(
Layer.provideMerge(Config.locationLayer),
Layer.provideMerge(AgentV2.locationLayer),
Layer.provideMerge(SkillV2.locationLayer),
Layer.provideMerge(Reference.locationLayer),
)
Loading