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:
67
packages/opencode/specs/v2/api.ts
Normal file
67
packages/opencode/specs/v2/api.ts
Normal file
@@ -0,0 +1,67 @@
|
||||
// @ts-nocheck
|
||||
|
||||
import { OpenCode } from "@opencode-ai/core"
|
||||
import { ReadTool } from "@opencode-ai/core/tools"
|
||||
|
||||
const opencode = OpenCode.make({})
|
||||
|
||||
opencode.tool.add(ReadTool)
|
||||
|
||||
opencode.tool.add({
|
||||
name: "bash",
|
||||
schema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
command: {
|
||||
type: "string",
|
||||
description: "The command to run.",
|
||||
},
|
||||
},
|
||||
required: ["command"],
|
||||
},
|
||||
execute(input, ctx) {},
|
||||
})
|
||||
|
||||
opencode.auth.add({
|
||||
provider: "openai",
|
||||
type: "api",
|
||||
value: process.env.OPENAI_API_KEY,
|
||||
})
|
||||
|
||||
opencode.agent.add({
|
||||
name: "build",
|
||||
permissions: [],
|
||||
model: {
|
||||
id: "gpt-5-5",
|
||||
provider: "openai",
|
||||
variant: "xhigh",
|
||||
},
|
||||
})
|
||||
|
||||
const sessionID = await opencode.session.create({
|
||||
agent: "build",
|
||||
})
|
||||
|
||||
opencode.subscribe((event) => {
|
||||
console.log(event)
|
||||
})
|
||||
|
||||
await opencode.session.prompt({
|
||||
sessionID,
|
||||
text: "hey what is up",
|
||||
})
|
||||
|
||||
await opencode.session.prompt({
|
||||
sessionID,
|
||||
text: "what is up with this",
|
||||
files: [
|
||||
{
|
||||
mime: "image/png",
|
||||
uri: "data:image/png;base64,xxxx",
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
await opencode.session.wait()
|
||||
|
||||
console.log(await opencode.session.messages(sessionID))
|
||||
136
packages/opencode/specs/v2/message-shape.md
Normal file
136
packages/opencode/specs/v2/message-shape.md
Normal file
@@ -0,0 +1,136 @@
|
||||
# Message Shape
|
||||
|
||||
Problem:
|
||||
|
||||
- stored messages need enough data to replay and resume a session later
|
||||
- prompt hooks often just want to append a synthetic user/assistant message
|
||||
- today that means faking ids, timestamps, and request metadata
|
||||
|
||||
## Option 1: Two Message Shapes
|
||||
|
||||
Keep `User` / `Assistant` for stored history, but clean them up.
|
||||
|
||||
```ts
|
||||
type User = {
|
||||
role: "user"
|
||||
time: { created: number }
|
||||
request: {
|
||||
agent: string
|
||||
model: ModelRef
|
||||
variant?: string
|
||||
format?: OutputFormat
|
||||
system?: string
|
||||
tools?: Record<string, boolean>
|
||||
}
|
||||
}
|
||||
|
||||
type Assistant = {
|
||||
role: "assistant"
|
||||
run: { agent: string; model: ModelRef; path: { cwd: string; root: string } }
|
||||
usage: { cost: number; tokens: Tokens }
|
||||
result: { finish?: string; error?: Error; structured?: unknown; kind: "reply" | "summary" }
|
||||
}
|
||||
```
|
||||
|
||||
Add a separate transient `PromptMessage` for prompt surgery.
|
||||
|
||||
```ts
|
||||
type PromptMessage = {
|
||||
role: "user" | "assistant"
|
||||
parts: PromptPart[]
|
||||
}
|
||||
```
|
||||
|
||||
Plugin hook example:
|
||||
|
||||
```ts
|
||||
prompt.push({
|
||||
role: "user",
|
||||
parts: [{ type: "text", text: "Summarize the tool output above and continue." }],
|
||||
})
|
||||
```
|
||||
|
||||
Tradeoff: prompt hooks get easy lightweight messages, but there are now two message shapes.
|
||||
|
||||
## Option 2: Prompt Mutators
|
||||
|
||||
Keep `User` / `Assistant` as the stored history model.
|
||||
|
||||
Prompt hooks do not build messages directly. The runtime gives them prompt mutators.
|
||||
|
||||
```ts
|
||||
type PromptEditor = {
|
||||
append(input: { role: "user" | "assistant"; parts: PromptPart[] }): void
|
||||
prepend(input: { role: "user" | "assistant"; parts: PromptPart[] }): void
|
||||
appendTo(target: "last-user" | "last-assistant", parts: PromptPart[]): void
|
||||
insertAfter(messageID: string, input: { role: "user" | "assistant"; parts: PromptPart[] }): void
|
||||
insertBefore(messageID: string, input: { role: "user" | "assistant"; parts: PromptPart[] }): void
|
||||
}
|
||||
```
|
||||
|
||||
Plugin hook examples:
|
||||
|
||||
```ts
|
||||
prompt.append({
|
||||
role: "user",
|
||||
parts: [{ type: "text", text: "Summarize the tool output above and continue." }],
|
||||
})
|
||||
```
|
||||
|
||||
```ts
|
||||
prompt.appendTo("last-user", [{ type: "text", text: BUILD_SWITCH }])
|
||||
```
|
||||
|
||||
Tradeoff: avoids a second full message type and avoids fake ids/timestamps, but moves more magic into the hook API.
|
||||
|
||||
## Option 3: Separate Turn State
|
||||
|
||||
Move execution settings out of `User` and into a separate turn/request object.
|
||||
|
||||
```ts
|
||||
type Turn = {
|
||||
id: string
|
||||
request: {
|
||||
agent: string
|
||||
model: ModelRef
|
||||
variant?: string
|
||||
format?: OutputFormat
|
||||
system?: string
|
||||
tools?: Record<string, boolean>
|
||||
}
|
||||
}
|
||||
|
||||
type User = {
|
||||
role: "user"
|
||||
turnID: string
|
||||
time: { created: number }
|
||||
}
|
||||
|
||||
type Assistant = {
|
||||
role: "assistant"
|
||||
turnID: string
|
||||
usage: { cost: number; tokens: Tokens }
|
||||
result: { finish?: string; error?: Error; structured?: unknown; kind: "reply" | "summary" }
|
||||
}
|
||||
```
|
||||
|
||||
Examples:
|
||||
|
||||
```ts
|
||||
const turn = {
|
||||
request: {
|
||||
agent: "build",
|
||||
model: { providerID: "openai", modelID: "gpt-5" },
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
```ts
|
||||
const msg = {
|
||||
role: "user",
|
||||
turnID: turn.id,
|
||||
parts: [{ type: "text", text: "Summarize the tool output above and continue." }],
|
||||
}
|
||||
```
|
||||
|
||||
Tradeoff: stored messages get much smaller and cleaner, but replay now has to join messages with turn state and prompt hooks still need a way to pick which turn they belong to.
|
||||
13
packages/opencode/specs/v2/notifications.md
Normal file
13
packages/opencode/specs/v2/notifications.md
Normal file
@@ -0,0 +1,13 @@
|
||||
# TUI Notifications Default
|
||||
|
||||
Problem:
|
||||
|
||||
- v1 defaults `attention.enabled` to `false`
|
||||
- users can opt in with `attention.enabled = true`
|
||||
- v2 should make core TUI notifications a default behavior
|
||||
|
||||
## v2 Target
|
||||
|
||||
Flip `attention.enabled` to `true` by default in v2.
|
||||
|
||||
Keep `attention.enabled = false` as the explicit opt-out.
|
||||
67
packages/opencode/specs/v2/tui-command-shim.md
Normal file
67
packages/opencode/specs/v2/tui-command-shim.md
Normal file
@@ -0,0 +1,67 @@
|
||||
# TUI Command Shim Removal
|
||||
|
||||
Problem:
|
||||
|
||||
- v1 keeps a deprecated `api.command` TUI plugin shim so older plugins do not fail during initialization
|
||||
- v2 should expose only the keymap command API
|
||||
- tests and fixtures should not encode legacy command behavior as expected behavior
|
||||
|
||||
## Remove Public Types
|
||||
|
||||
In `packages/plugin/src/tui.ts`, remove:
|
||||
|
||||
- `TuiCommand`
|
||||
- `TuiCommandApi`
|
||||
- `TuiPluginApi.command`
|
||||
|
||||
Keep `api.keymap` as the only TUI command registration and execution surface.
|
||||
|
||||
## Remove Runtime Shim
|
||||
|
||||
Delete `packages/opencode/src/cli/cmd/tui/plugin/command-shim.ts`.
|
||||
|
||||
In `packages/opencode/src/cli/cmd/tui/plugin/api.tsx`, remove:
|
||||
|
||||
- the `createCommandShim` import
|
||||
- the `command: createCommandShim(...)` field from `createTuiApi(...)`
|
||||
|
||||
In `packages/opencode/src/cli/cmd/tui/plugin/runtime.ts`, remove:
|
||||
|
||||
- the `createCommandShim` import
|
||||
- the `command: createCommandShim(...)` field from `pluginApi(...)`
|
||||
|
||||
## Migration Target
|
||||
|
||||
Plugin authors should replace old calls with keymap calls:
|
||||
|
||||
```ts
|
||||
api.keymap.registerLayer({
|
||||
commands: [
|
||||
{
|
||||
name: "plugin.command",
|
||||
title: "Plugin Command",
|
||||
namespace: "palette",
|
||||
slashName: "plugin",
|
||||
run() {
|
||||
api.ui.dialog.clear()
|
||||
},
|
||||
},
|
||||
],
|
||||
bindings: [{ key: "ctrl+shift+p", cmd: "plugin.command" }],
|
||||
})
|
||||
```
|
||||
|
||||
Direct replacements:
|
||||
|
||||
- `api.command.register(cb)` -> `api.keymap.registerLayer({ commands, bindings })`
|
||||
- `api.command.trigger(name)` -> `api.keymap.dispatchCommand(name)`
|
||||
- `api.command.show()` -> `api.keymap.dispatchCommand("command.palette.show")`
|
||||
- `onSelect(dialog)` -> use `api.ui.dialog` from the plugin API closure
|
||||
|
||||
## Verification
|
||||
|
||||
After removal, run from package directories:
|
||||
|
||||
- `bun typecheck` in `packages/plugin`
|
||||
- `bun typecheck` in `packages/opencode`
|
||||
- TUI plugin loader tests in `packages/opencode` if runtime plugin API wiring changed
|
||||
Reference in New Issue
Block a user