From 7fb23d9b6b3e808183f31e54857690b335fae4ae Mon Sep 17 00:00:00 2001 From: mesalogo Date: Wed, 30 Sep 2026 16:59:13 +0800 Subject: [PATCH] fix: speed up opening OpenCode in the terminal MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 时不再每次复制插件依赖,第二次及以后的打开速度明显加快。 --- docs/features/assistant-workbar/progress.md | 12 +++++++ src/main/agent/native-terminal-client.test.ts | 22 +++++++++++- src/main/agent/native-terminal-client.ts | 36 +++++++++++++++---- 3 files changed, 63 insertions(+), 7 deletions(-) diff --git a/docs/features/assistant-workbar/progress.md b/docs/features/assistant-workbar/progress.md index e0c5cd34..34f74412 100644 --- a/docs/features/assistant-workbar/progress.md +++ b/docs/features/assistant-workbar/progress.md @@ -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:消息取消与失败展示 消息终态按[Runtime 交互边界](./runtime-interactions.md)保存和展示。取消使用结构化 diff --git a/src/main/agent/native-terminal-client.test.ts b/src/main/agent/native-terminal-client.test.ts index 6fb9151f..740b3ec7 100644 --- a/src/main/agent/native-terminal-client.test.ts +++ b/src/main/agent/native-terminal-client.test.ts @@ -1,5 +1,5 @@ // @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 { join } from 'node:path' 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' }) }) + 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 () => { 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' } }] }) diff --git a/src/main/agent/native-terminal-client.ts b/src/main/agent/native-terminal-client.ts index 7455eb06..ee8b4268 100644 --- a/src/main/agent/native-terminal-client.ts +++ b/src/main/agent/native-terminal-client.ts @@ -1,6 +1,6 @@ import { randomUUID } from 'node:crypto' 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 { pathToFileURL } from 'node:url' import { promisify } from 'node:util' @@ -49,6 +49,29 @@ export type NativeTerminalClientOptions = { // Same native read permissions as the existing remote OpenCode adapter. const openCodeReadTools = ['read', 'glob', 'grep', 'list', 'lsp', 'webfetch', 'websearch', 'codesearch', 'question', 'external_directory'] const continueReadTools = ['Read', 'List', 'Search', 'Fetch', 'Diff', 'AskQuestion', 'CheckBackgroundJob', 'Skills'] +const sharedOpenCodeConfigs = new Map>() + +/** Copies the bundled OpenCode plugin tree once instead of on every terminal launch. */ +function prepareSharedOpenCodeConfig(source: string, target: string): Promise { + 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 { private readonly pending = new Map>() @@ -176,11 +199,12 @@ export class NativeTerminalClient { if (input.workMode === 'execute') args.push('--auto') else args.push('--readonly', ...allowedContinueTools.flatMap(name => ['--allow', name]), '--exclude', '*') } else { - const configDirectory = join(temporary, 'config') - if (this.options.bundledRuntimePaths.opencodeConfig) { - await cp(this.options.bundledRuntimePaths.opencodeConfig, configDirectory, { recursive: true }) - } - const skillsRoot = await stageRuntimeSkillPackages(configDirectory, skills, 'OpenCode') + const bundledConfig = this.options.bundledRuntimePaths.opencodeConfig + const configDirectory = bundledConfig + ? await prepareSharedOpenCodeConfig(bundledConfig, join(root, 'opencode-config')) + : join(temporary, 'config') + await mkdir(configDirectory, { recursive: true }) + const skillsRoot = await stageRuntimeSkillPackages(join(temporary, 'opencode'), skills, 'OpenCode') const config = createOpenCodeModelBridgeProviderConfig({ protocol: managed.profile.protocol, model: selected.modelName, name: selected.name, loopbackOrigin: origin, supportsImageInput: selected.supportsImageInput, workMode: input.workMode