feat: 品牌替换 + 启动优化 + AGENTS.md 模板定制

- 品牌替换:OpenCode/opencode → AirCoding/aircoding(16+ 文件)
- Logo ASCII art:修复 left/right 行数不匹配导致的启动崩溃
- 启动诊断:添加 OPENCODE_PRINT_TIMING 计时探针
- dev 模式默认 --pure 跳过外部插件加载
- AGENTS.md 模板:追加 AirCoding 多 Agent 专项段落
- architect prompt + plugin:强化 AGENTS.md 产出验证
This commit is contained in:
airlongdian
2026-06-14 09:31:29 +08:00
commit e2fd375a1c
5757 changed files with 1170016 additions and 0 deletions

View File

@@ -0,0 +1,13 @@
import { mkdtemp, realpath, rm } from "node:fs/promises"
import path from "node:path"
import os from "node:os"
export async function tmpdir() {
const directory = await realpath(await mkdtemp(path.join(os.tmpdir(), "opencode-tui-test-")))
return {
path: directory,
async [Symbol.asyncDispose]() {
await rm(directory, { recursive: true, force: true })
},
}
}

View File

@@ -0,0 +1,32 @@
/** @jsxImportSource @opentui/solid */
import {
TuiPathsProvider,
TuiStartupProvider,
TuiTerminalEnvironmentProvider,
type TuiPaths,
} from "../../src/context/runtime"
import type { ParentProps } from "solid-js"
export function TestTuiContexts(
props: ParentProps<{
cwd?: string
directory?: string
paths?: Partial<TuiPaths>
}>,
) {
return (
<TuiPathsProvider
value={{
cwd: props.cwd ?? props.directory ?? "/tmp/opencode/packages/tui",
home: "/tmp/opencode/home",
state: "/tmp/opencode/state",
worktree: "/tmp/opencode",
...props.paths,
}}
>
<TuiTerminalEnvironmentProvider value={{ platform: "linux" }}>
<TuiStartupProvider value={{ skipInitialLoading: false }}>{props.children}</TuiStartupProvider>
</TuiTerminalEnvironmentProvider>
</TuiPathsProvider>
)
}

View File

@@ -0,0 +1,36 @@
import type { TuiPluginApi } from "@opencode-ai/plugin/tui"
import { RGBA } from "@opentui/core"
import { createTuiResolvedConfig } from "./tui-runtime"
type Opts = {
client?: TuiPluginApi["client"]
keymap?: TuiPluginApi["keymap"]
attention?: Partial<TuiPluginApi["attention"]>
event?: TuiPluginApi["event"]
state?: { session?: Partial<TuiPluginApi["state"]["session"]> }
}
export function createTuiPluginApi(opts: Opts = {}) {
const values = new Map<string, unknown>()
const color = RGBA.fromInts(200, 200, 200)
const dialog = { clear() {}, replace() {}, setSize() {}, size: "medium" as const, depth: 0, open: false }
return {
attention: { notify: async () => ({ ok: false, notification: false, sound: false }), ...opts.attention },
client: opts.client,
event: opts.event,
keymap: opts.keymap,
kv: {
get(name: string, fallback?: unknown) {
return values.has(name) ? values.get(name) : fallback
},
set(name: string, value: unknown) {
values.set(name, value)
},
ready: true,
},
state: { session: { get: () => undefined, ...opts.state?.session } },
theme: { current: new Proxy({}, { get: () => color }) },
tuiConfig: createTuiResolvedConfig(),
ui: { dialog },
} as unknown as TuiPluginApi
}

View File

@@ -0,0 +1,12 @@
import { resolve, type Info, type Resolved } from "../../src/config"
import { TuiKeybind } from "../../src/config/keybind"
type ResolvedInput = Omit<Info, "attention" | "keybinds" | "leader_timeout"> & {
attention?: Partial<Resolved["attention"]>
keybinds?: Partial<TuiKeybind.Keybinds>
leader_timeout?: number
}
export function createTuiResolvedConfig(input: ResolvedInput = {}) {
return resolve(input, { terminalSuspend: process.platform !== "win32" })
}

View File

@@ -0,0 +1,81 @@
import type { GlobalEvent } from "@opencode-ai/sdk/v2"
import type { EventSource } from "../../src/context/sdk"
export const worktree = "/tmp/opencode"
export const directory = `${worktree}/packages/tui`
export function json(data: unknown, init?: ResponseInit) {
return new Response(JSON.stringify(data), {
...init,
headers: { "content-type": "application/json", ...(init?.headers ?? {}) },
})
}
export function eventSource(): EventSource {
return { subscribe: async () => () => {} }
}
export function createEventSource() {
let fn: ((event: GlobalEvent) => void) | undefined
return {
source: {
subscribe: async (handler: (event: GlobalEvent) => void) => {
fn = handler
return () => {
if (fn === handler) fn = undefined
}
},
} satisfies EventSource,
emit(event: GlobalEvent) {
if (!fn) throw new Error("event source not ready")
fn(event)
},
}
}
export type FetchHandler = (url: URL) => Response | Promise<Response> | undefined
export function createFetch(override?: FetchHandler) {
const session = [] as URL[]
const fetch = (async (input: RequestInfo | URL) => {
const url = new URL(input instanceof Request ? input.url : String(input))
if (url.pathname === "/session") session.push(url)
const overridden = await override?.(url)
if (overridden) return overridden
if (
[
"/agent",
"/command",
"/experimental/workspace",
"/experimental/workspace/status",
"/formatter",
"/lsp",
].includes(url.pathname)
)
return json([])
if (["/config", "/experimental/resource", "/mcp", "/provider/auth", "/session/status"].includes(url.pathname))
return json({})
if (url.pathname === "/config/providers") return json({ providers: {}, default: {} })
if (url.pathname === "/experimental/console") return json({ consoleManagedProviders: [], switchableOrgCount: 0 })
if (url.pathname === "/path") return json({ home: "", state: "", config: "", worktree, directory })
if (url.pathname === "/api/location") return json({ directory, project: { id: "proj_test", directory: worktree } })
if (
["/api/agent", "/api/model", "/api/provider", "/api/connector", "/api/command", "/api/skill"].includes(
url.pathname,
)
)
return json({
location: { directory, project: { id: "proj_test", directory: worktree } },
data: [],
})
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" })
throw new Error(`unexpected request: ${url.pathname}`)
}) as typeof globalThis.fetch
return { fetch, session }
}