fix: speed up opening OpenCode in the terminal
Every 'Open in terminal' launch for OpenCode copied the bundled plugin dependency tree (about 3,600 files, 52 MB) into a temporary directory, adding 2.2-2.4 s on Windows before the TUI started. The tree is now copied once into terminal/opencode-config under user data and reused until its bundled version marker changes. Skills, Ask-mode plugins and model configuration are still generated per launch and cleaned up on close. OpenCode's own TUI startup (about 3 s) is unchanged; the remote Agent path is unaffected. Release note: 在终端中打开 OpenCode 时不再每次复制插件依赖,第二次及以后的打开速度明显加快。
This commit is contained in:
@@ -1,5 +1,17 @@
|
|||||||
# 工作栏实现与验证进度
|
# 工作栏实现与验证进度
|
||||||
|
|
||||||
|
## 2026-09-30:OpenCode 终端启动准备
|
||||||
|
|
||||||
|
原先每次“在终端中打开” OpenCode 都会把随包插件依赖目录(约 3,600 个文件、52 MB)
|
||||||
|
复制到临时目录,Windows 本机每次约 2.2–2.4 秒。现在该目录只复制一次到用户数据下的
|
||||||
|
`terminal/opencode-config`,按 `.goodbuddy-ready.json` 判断是否需要刷新;Skills、Ask
|
||||||
|
插件和模型配置仍按每次启动单独生成,关闭时清理。
|
||||||
|
|
||||||
|
Windows 本机实测启动准备:首次 2.9 秒,之后 19–44 ms。OpenCode 自身 TUI 从进程启动
|
||||||
|
到显示输入框约 2.9–3.3 秒,由 OpenCode 程序决定,本次未改变。验证:原生终端单元测试
|
||||||
|
5 项通过;已安装 OpenCode 的 Ask/Execute 真实 PTY 测试 2 项通过(合成模型,真实模型
|
||||||
|
调用 0 次)。远程 Agent 使用独立的 OpenCode 启动路径,不受本次修改影响。
|
||||||
|
|
||||||
## 2026-09-28:消息取消与失败展示
|
## 2026-09-28:消息取消与失败展示
|
||||||
|
|
||||||
消息终态按[Runtime 交互边界](./runtime-interactions.md)保存和展示。取消使用结构化
|
消息终态按[Runtime 交互边界](./runtime-interactions.md)保存和展示。取消使用结构化
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// @vitest-environment node
|
// @vitest-environment node
|
||||||
import { mkdtemp, readFile, readdir, rm } from 'node:fs/promises'
|
import { mkdir, mkdtemp, readFile, readdir, rm, stat, writeFile } from 'node:fs/promises'
|
||||||
import { tmpdir } from 'node:os'
|
import { tmpdir } from 'node:os'
|
||||||
import { join } from 'node:path'
|
import { join } from 'node:path'
|
||||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||||
@@ -82,6 +82,26 @@ describe('native terminal client', () => {
|
|||||||
await expect(client.open(1, input)).resolves.toMatchObject({ state: 'running' })
|
await expect(client.open(1, input)).resolves.toMatchObject({ state: 'running' })
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('copies the bundled OpenCode config once and reuses it for later launches', async () => {
|
||||||
|
const { root, create, fetcher, input } = await fixture()
|
||||||
|
const bundled = join(root, 'bundled-config')
|
||||||
|
await mkdir(join(bundled, 'node_modules', 'plugin'), { recursive: true })
|
||||||
|
await writeFile(join(bundled, '.goodbuddy-ready.json'), '{"version":"1"}')
|
||||||
|
await writeFile(join(bundled, 'node_modules', 'plugin', 'index.js'), 'export {}')
|
||||||
|
const client = new NativeTerminalClient({
|
||||||
|
terminalManager: { create }, rootDirectory: root, fetcher,
|
||||||
|
bundledRuntimePaths: { opencode: process.execPath, opencodeConfig: bundled, continue: process.execPath, ripgrep: '', deepseekHarness: '' }
|
||||||
|
})
|
||||||
|
await client.open(1, input)
|
||||||
|
const configDirectory = launches[0]!.spawnSpec.env.OPENCODE_CONFIG_DIR!
|
||||||
|
expect(configDirectory).toBe(join(root, 'opencode-config'))
|
||||||
|
const copied = (await stat(join(configDirectory, 'node_modules', 'plugin', 'index.js'))).mtimeMs
|
||||||
|
await launches[0]!.dispose()
|
||||||
|
await client.open(1, { ...input, workMode: 'execute' })
|
||||||
|
expect(launches[1]!.spawnSpec.env.OPENCODE_CONFIG_DIR).toBe(configDirectory)
|
||||||
|
expect((await stat(join(configDirectory, 'node_modules', 'plugin', 'index.js'))).mtimeMs).toBe(copied)
|
||||||
|
})
|
||||||
|
|
||||||
it('uses Execute permissions and explicit session MCP endpoints', async () => {
|
it('uses Execute permissions and explicit session MCP endpoints', async () => {
|
||||||
const { client, input } = await fixture()
|
const { client, input } = await fixture()
|
||||||
await client.open(1, { ...input, workMode: 'execute', mcpServers: [{ name: 'tools', url: 'http://127.0.0.1:12345/mcp', headers: { Authorization: 'Bearer local-token' } }] })
|
await client.open(1, { ...input, workMode: 'execute', mcpServers: [{ name: 'tools', url: 'http://127.0.0.1:12345/mcp', headers: { Authorization: 'Bearer local-token' } }] })
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { randomUUID } from 'node:crypto'
|
import { randomUUID } from 'node:crypto'
|
||||||
import { execFile } from 'node:child_process'
|
import { execFile } from 'node:child_process'
|
||||||
import { cp, mkdir, mkdtemp, readFile, rm, stat, writeFile } from 'node:fs/promises'
|
import { cp, mkdir, mkdtemp, readFile, rename, rm, stat, writeFile } from 'node:fs/promises'
|
||||||
import { dirname, join } from 'node:path'
|
import { dirname, join } from 'node:path'
|
||||||
import { pathToFileURL } from 'node:url'
|
import { pathToFileURL } from 'node:url'
|
||||||
import { promisify } from 'node:util'
|
import { promisify } from 'node:util'
|
||||||
@@ -49,6 +49,29 @@ export type NativeTerminalClientOptions = {
|
|||||||
// Same native read permissions as the existing remote OpenCode adapter.
|
// Same native read permissions as the existing remote OpenCode adapter.
|
||||||
const openCodeReadTools = ['read', 'glob', 'grep', 'list', 'lsp', 'webfetch', 'websearch', 'codesearch', 'question', 'external_directory']
|
const openCodeReadTools = ['read', 'glob', 'grep', 'list', 'lsp', 'webfetch', 'websearch', 'codesearch', 'question', 'external_directory']
|
||||||
const continueReadTools = ['Read', 'List', 'Search', 'Fetch', 'Diff', 'AskQuestion', 'CheckBackgroundJob', 'Skills']
|
const continueReadTools = ['Read', 'List', 'Search', 'Fetch', 'Diff', 'AskQuestion', 'CheckBackgroundJob', 'Skills']
|
||||||
|
const sharedOpenCodeConfigs = new Map<string, Promise<string>>()
|
||||||
|
|
||||||
|
/** Copies the bundled OpenCode plugin tree once instead of on every terminal launch. */
|
||||||
|
function prepareSharedOpenCodeConfig(source: string, target: string): Promise<string> {
|
||||||
|
const existing = sharedOpenCodeConfigs.get(target)
|
||||||
|
if (existing) return existing
|
||||||
|
const operation = (async () => {
|
||||||
|
const marker = '.goodbuddy-ready.json'
|
||||||
|
const expected = await readFile(join(source, marker), 'utf8').catch(() => undefined)
|
||||||
|
if (expected !== undefined && await readFile(join(target, marker), 'utf8').catch(() => undefined) === expected) return target
|
||||||
|
const staging = `${target}.staging-${randomUUID()}`
|
||||||
|
try {
|
||||||
|
await cp(source, staging, { recursive: true })
|
||||||
|
await rm(target, { recursive: true, force: true })
|
||||||
|
await rename(staging, target)
|
||||||
|
} finally {
|
||||||
|
await rm(staging, { recursive: true, force: true })
|
||||||
|
}
|
||||||
|
return target
|
||||||
|
})().catch(error => { sharedOpenCodeConfigs.delete(target); throw error })
|
||||||
|
sharedOpenCodeConfigs.set(target, operation)
|
||||||
|
return operation
|
||||||
|
}
|
||||||
|
|
||||||
export class NativeTerminalClient {
|
export class NativeTerminalClient {
|
||||||
private readonly pending = new Map<string, Promise<TerminalSnapshot>>()
|
private readonly pending = new Map<string, Promise<TerminalSnapshot>>()
|
||||||
@@ -176,11 +199,12 @@ export class NativeTerminalClient {
|
|||||||
if (input.workMode === 'execute') args.push('--auto')
|
if (input.workMode === 'execute') args.push('--auto')
|
||||||
else args.push('--readonly', ...allowedContinueTools.flatMap(name => ['--allow', name]), '--exclude', '*')
|
else args.push('--readonly', ...allowedContinueTools.flatMap(name => ['--allow', name]), '--exclude', '*')
|
||||||
} else {
|
} else {
|
||||||
const configDirectory = join(temporary, 'config')
|
const bundledConfig = this.options.bundledRuntimePaths.opencodeConfig
|
||||||
if (this.options.bundledRuntimePaths.opencodeConfig) {
|
const configDirectory = bundledConfig
|
||||||
await cp(this.options.bundledRuntimePaths.opencodeConfig, configDirectory, { recursive: true })
|
? await prepareSharedOpenCodeConfig(bundledConfig, join(root, 'opencode-config'))
|
||||||
}
|
: join(temporary, 'config')
|
||||||
const skillsRoot = await stageRuntimeSkillPackages(configDirectory, skills, 'OpenCode')
|
await mkdir(configDirectory, { recursive: true })
|
||||||
|
const skillsRoot = await stageRuntimeSkillPackages(join(temporary, 'opencode'), skills, 'OpenCode')
|
||||||
const config = createOpenCodeModelBridgeProviderConfig({
|
const config = createOpenCodeModelBridgeProviderConfig({
|
||||||
protocol: managed.profile.protocol, model: selected.modelName, name: selected.name,
|
protocol: managed.profile.protocol, model: selected.modelName, name: selected.name,
|
||||||
loopbackOrigin: origin, supportsImageInput: selected.supportsImageInput, workMode: input.workMode
|
loopbackOrigin: origin, supportsImageInput: selected.supportsImageInput, workMode: input.workMode
|
||||||
|
|||||||
Reference in New Issue
Block a user