refactor(core): consolidate references

This commit is contained in:
Dax Raad 2026-06-09 11:38:35 -04:00
commit f76ca58a54
63 changed files with 683 additions and 2722 deletions

View file

@ -9,6 +9,7 @@ import { useEditorContext } from "../../context/editor"
import { useProject } from "../../context/project"
import { useSDK } from "../../context/sdk"
import { useSync } from "../../context/sync"
import { useSyncV2 } from "../../context/sync-v2"
import { getScrollAcceleration } from "../../util/scroll"
import { useTuiPaths } from "../../context/runtime"
import { useTuiConfig } from "../../config"
@ -19,7 +20,6 @@ import { Locale } from "../../util/locale"
import type { PromptInfo } from "../../prompt/history"
import { useFrecency } from "../../prompt/frecency"
import { useBindings, useCommandSlashes, useOpencodeModeStack } from "../../keymap"
import type { ReferenceDescriptor } from "@opencode-ai/sdk/v2"
import { displayCharAt, mentionTriggerIndex } from "../../prompt/display"
function removeLineRange(input: string) {
@ -85,6 +85,7 @@ export function Autocomplete(props: {
const editor = useEditorContext()
const sdk = useSDK()
const sync = useSync()
const syncV2 = useSyncV2()
const project = useProject()
const slashes = useCommandSlashes()
const modeStack = useOpencodeModeStack()
@ -272,37 +273,12 @@ export function Autocomplete(props: {
}
}
function referencePromptText(reference: ReferenceDescriptor) {
const problem = reference.kind === "invalid" ? reference.message : undefined
return [
`Referenced configured reference @${reference.name}.`,
...(reference.kind === "local" ? ["Kind: local directory"] : []),
...(reference.kind === "git" ? ["Kind: git repository"] : []),
...(reference.kind === "invalid" && reference.repository ? [`Repository: ${reference.repository}`] : []),
...(reference.kind === "git" ? [`Repository: ${reference.repository}`] : []),
...(reference.kind === "git" && reference.branch ? [`Branch/ref: ${reference.branch}`] : []),
...(reference.kind === "invalid" ? [] : [`Reference root: ${reference.path}`]),
...(problem
? [`Problem: ${problem}`]
: ["Inspect the configured reference with Read, Glob, and Grep when useful."]),
].join("\n")
}
const [references] = createResource(
() => project.workspace.current(),
async (workspace) => {
const result = await sdk.client.reference.list({ workspace })
return result.data ?? []
},
{ initialValue: [] },
)
const referenceMatch = createMemo(() => {
if (!store.visible || store.visible === "/") return
const { baseQuery } = extractLineRange(search())
const slash = baseQuery.indexOf("/")
const alias = slash === -1 ? baseQuery : baseQuery.slice(0, slash)
return references().find((item) => item.name === alias)
return syncV2.data.reference.find((item) => item.name === alias)
})
function normalizeMentionPath(filePath: string) {
@ -435,29 +411,21 @@ export function Autocomplete(props: {
})
const referenceAliases = createMemo(() =>
references().map(
syncV2.data.reference.map(
(reference): AutocompleteOption => ({
display: "@" + reference.name,
description: reference.kind === "invalid" ? reference.message : " dir",
description: " dir",
onSelect: () => {
if (reference.kind !== "invalid") {
insertPart(reference.name, {
type: "file",
mime: "application/x-directory",
filename: reference.name,
url: pathToFileURL(reference.path).href,
source: {
type: "file",
text: { start: 0, end: 0, value: "" },
path: reference.name,
},
})
return
}
insertPart(reference.name, {
type: "text",
text: referencePromptText(reference),
synthetic: true,
type: "file",
mime: "application/x-directory",
filename: reference.name,
url: pathToFileURL(reference.path).href,
source: {
type: "file",
text: { start: 0, end: 0, value: "" },
path: reference.name,
},
})
},
}),

View file

@ -1,6 +1,7 @@
import { useEvent } from "./event"
import type {
Event,
ReferenceInfo,
SessionMessage,
SessionMessageAssistant,
SessionMessageAssistantReasoning,
@ -10,6 +11,8 @@ import type {
import { createStore, produce, reconcile } from "solid-js/store"
import { createSimpleContext } from "./helper"
import { useSDK } from "./sdk"
import { useProject } from "./project"
import { createEffect } from "solid-js"
function activeAssistant(messages: SessionMessage[]) {
const index = messages.findIndex((message) => message.type === "assistant" && !message.time.completed)
@ -60,12 +63,15 @@ export const { use: useSyncV2, provider: SyncProviderV2 } = createSimpleContext(
messages: {
[sessionID: string]: SessionMessage[]
}
reference: ReferenceInfo[]
}>({
messages: {},
reference: [],
})
const event = useEvent()
const sdk = useSDK()
const project = useProject()
const applied = new Set<string>()
const buffering = new Map<string, Event[]>()
const syncing = new Map<string, Promise<void>>()
@ -117,6 +123,17 @@ export const { use: useSyncV2, provider: SyncProviderV2 } = createSimpleContext(
return result
}
async function syncReferences(workspace = project.workspace.current()) {
const result = await sdk.client.v2.reference.list({ location: { workspace } })
if (workspace !== project.workspace.current()) return
setStore("reference", reconcile(result.data?.data ?? []))
}
createEffect(() => {
project.workspace.current()
void syncReferences()
})
function apply(event: Event) {
switch (event.type) {
case "session.next.agent.switched":
@ -147,7 +164,6 @@ export const { use: useSyncV2, provider: SyncProviderV2 } = createSimpleContext(
text: event.properties.prompt.text,
files: event.properties.prompt.files,
agents: event.properties.prompt.agents,
references: event.properties.prompt.references,
time: { created: event.properties.timestamp },
})
})
@ -163,7 +179,6 @@ export const { use: useSyncV2, provider: SyncProviderV2 } = createSimpleContext(
text: event.properties.prompt.text,
files: event.properties.prompt.files,
agents: event.properties.prompt.agents,
references: event.properties.prompt.references,
time: { created: event.properties.timeCreated },
})
})
@ -418,6 +433,9 @@ export const { use: useSyncV2, provider: SyncProviderV2 } = createSimpleContext(
})
})
break
case "reference.updated":
void syncReferences()
break
}
}

View file

@ -27,6 +27,52 @@ function emitTwice(events: ReturnType<typeof createEventSource>, payload: Event)
events.emit(event)
}
test("sync v2 refreshes references after updates", async () => {
const events = createEventSource()
let requests = 0
const calls = createFetch((url) => {
if (url.pathname !== "/api/reference") return
requests++
return json({
location: { directory, project: { id: "proj_test", directory } },
data: requests === 1 ? [] : [{ name: "docs", path: "/docs", source: { type: "local", path: "/docs" } }],
})
})
let sync!: ReturnType<typeof useSyncV2>
let ready!: () => void
const mounted = new Promise<void>((resolve) => {
ready = resolve
})
function Probe() {
sync = useSyncV2()
onMount(ready)
return <box />
}
const app = await testRender(() => (
<TestTuiContexts>
<SDKProvider url="http://test" directory={directory} events={events.source} fetch={calls.fetch}>
<ProjectProvider>
<SyncProviderV2>
<Probe />
</SyncProviderV2>
</ProjectProvider>
</SDKProvider>
</TestTuiContexts>
))
try {
await mounted
await wait(() => requests === 1)
events.emit(global({ id: "evt_reference_1", type: "reference.updated", properties: {} }))
await wait(() => sync.data.reference.length === 1)
expect(sync.data.reference[0]?.name).toBe("docs")
} finally {
app.renderer.destroy()
}
})
test("sync v2 settles pending tools when a live failure arrives", async () => {
const events = createEventSource()
const calls = createFetch()

View file

@ -60,6 +60,7 @@ export function createFetch(override?: FetchHandler) {
if (url.pathname === "/experimental/console") return json({ consoleManagedProviders: [], switchableOrgCount: 0 })
if (url.pathname === "/path") return json({ home: "", state: "", config: "", worktree, directory })
if (url.pathname === "/project/current") return json({ id: "proj_test" })
if (url.pathname === "/api/reference") return json({ location: { directory, project: { id: "proj_test", directory } }, data: [] })
if (url.pathname === "/provider") return json({ all: [], default: {}, connected: [] })
if (url.pathname === "/session") return json([])
if (url.pathname === "/vcs") return json({ branch: "main" })