From c4fe6428a96ff594959fa96eb67ee976ac067d27 Mon Sep 17 00:00:00 2001 From: YD-233 <146694019+YD-233@users.noreply.github.com> Date: Fri, 30 Oct 2026 16:04:10 +0800 Subject: [PATCH] fix(tui): surface invalid additional directory errors --- .../surface-invalid-additional-dir-errors.md | 5 +++ .../skill/workspace/rootFileSkillSource.ts | 1 + .../workspace/workspaceSkillCatalogService.ts | 1 + .../node-fs/projectLocalConfigService.ts | 9 ++--- .../workspaceDirs/workspaceDirsService.ts | 7 +++- .../skill/workspace/skillCatalog.test.ts | 22 +++++++++- .../node-fs/projectLocalConfigService.test.ts | 40 +++++++++++++++++++ .../workspaceDirsService.test.ts | 14 +++++++ 8 files changed, 91 insertions(+), 8 deletions(-) create mode 100644 .changeset/surface-invalid-additional-dir-errors.md diff --git a/.changeset/surface-invalid-additional-dir-errors.md b/.changeset/surface-invalid-additional-dir-errors.md new file mode 100644 index 00000000000..5189d547d4e --- /dev/null +++ b/.changeset/surface-invalid-additional-dir-errors.md @@ -0,0 +1,5 @@ +--- +"@moonshot-ai/kimi-code": patch +--- + +Show the invalid workspace directory path instead of silently exiting during TUI startup. diff --git a/packages/agent-core-v2/src/features/skill/workspace/rootFileSkillSource.ts b/packages/agent-core-v2/src/features/skill/workspace/rootFileSkillSource.ts index 5555543b938..0ecfbf65fef 100644 --- a/packages/agent-core-v2/src/features/skill/workspace/rootFileSkillSource.ts +++ b/packages/agent-core-v2/src/features/skill/workspace/rootFileSkillSource.ts @@ -73,6 +73,7 @@ export class WorkspaceRootSkillSource extends Disposable implements IWorkspaceRo this.watchReady = this.workspaceDirs.ready .then(() => this.updateProjectSkillRootWatches([])) .then(() => undefined); + void this.watchReady.catch(() => undefined); } private get workDirs(): readonly string[] { diff --git a/packages/agent-core-v2/src/features/skill/workspace/workspaceSkillCatalogService.ts b/packages/agent-core-v2/src/features/skill/workspace/workspaceSkillCatalogService.ts index a1ce0f5a56f..758b96415ce 100644 --- a/packages/agent-core-v2/src/features/skill/workspace/workspaceSkillCatalogService.ts +++ b/packages/agent-core-v2/src/features/skill/workspace/workspaceSkillCatalogService.ts @@ -56,6 +56,7 @@ export class WorkspaceSkillCatalogService extends Disposable implements IWorkspa ); } this.ready = this.loadAll(); + void this.ready.catch(() => undefined); } private get contributions(): Map< diff --git a/packages/agent-core-v2/src/persistence/backends/node-fs/projectLocalConfigService.ts b/packages/agent-core-v2/src/persistence/backends/node-fs/projectLocalConfigService.ts index a3c10cd4703..78e2dd60e7c 100644 --- a/packages/agent-core-v2/src/persistence/backends/node-fs/projectLocalConfigService.ts +++ b/packages/agent-core-v2/src/persistence/backends/node-fs/projectLocalConfigService.ts @@ -153,7 +153,7 @@ export class FileProjectLocalConfigService implements IProjectLocalConfigService if (isPathMissing(error)) { throw new Error2( ErrorCodes.CONFIG_INVALID, - 'workspace.additional_dir must exist and be a directory', + `workspace.additional_dir must exist and be a directory: ${filePath}`, ); } throw toStorageIoError(error, { path: filePath, op: 'stat' }); @@ -162,7 +162,7 @@ export class FileProjectLocalConfigService implements IProjectLocalConfigService if (!stat.isDirectory) { throw new Error2( ErrorCodes.CONFIG_INVALID, - 'workspace.additional_dir must exist and be a directory', + `workspace.additional_dir must exist and be a directory: ${filePath}`, ); } } @@ -200,10 +200,7 @@ function normalizeAdditionalDirInput(additionalDir: string): string { } const trimmed = additionalDir.trim(); if (trimmed.length === 0) { - throw new Error2( - ErrorCodes.CONFIG_INVALID, - 'workspace.additional_dir must exist and be a directory', - ); + throw new Error2(ErrorCodes.CONFIG_INVALID, 'workspace.additional_dir must not be empty'); } return normalize(trimmed); } diff --git a/packages/agent-core-v2/src/workspace/workspaceDirs/workspaceDirsService.ts b/packages/agent-core-v2/src/workspace/workspaceDirs/workspaceDirsService.ts index 573c79abf3f..bd038bc3d20 100644 --- a/packages/agent-core-v2/src/workspace/workspaceDirs/workspaceDirsService.ts +++ b/packages/agent-core-v2/src/workspace/workspaceDirs/workspaceDirsService.ts @@ -74,7 +74,12 @@ export class WorkspaceDirsService implements IWorkspaceDirs { await this.reloadFromRegistry(); }), ); - if (this.legacyProjectConfig) void this.ready.then(() => this.watchLocalToml()); + if (this.legacyProjectConfig) { + void this.ready.then( + () => this.watchLocalToml(), + () => undefined, + ); + } this._register( this.workspaceStore.onDidChange(() => { void this.enqueue(() => this.reloadFromRegistry()).catch((error) => { diff --git a/packages/agent-core-v2/test/features/skill/workspace/skillCatalog.test.ts b/packages/agent-core-v2/test/features/skill/workspace/skillCatalog.test.ts index e389085d32a..8634057ebfb 100644 --- a/packages/agent-core-v2/test/features/skill/workspace/skillCatalog.test.ts +++ b/packages/agent-core-v2/test/features/skill/workspace/skillCatalog.test.ts @@ -174,10 +174,11 @@ function workspaceContextStub(workDir: string): IWorkspaceContext { function workspaceDirsStub( sharedAdditionalDirs: readonly string[] = [], additionalDirs: readonly string[] = sharedAdditionalDirs, + ready: Promise = Promise.resolve(), ): IWorkspaceDirs { return { _serviceBrand: undefined, - ready: Promise.resolve(), + ready, additionalDirs, sharedAdditionalDirs, primaryByWorkspace: {}, @@ -296,6 +297,25 @@ describe('WorkspaceSkillCatalogService', () => { ); } + it('keeps initial source failures observable for a late ready consumer', async () => { + const { host, workspace } = makeHost( + new FileSkillDiscovery(stubLog()), + workspaceContextStub('/work'), + [], + undefined, + undefined, + workspaceDirsStub([], [], Promise.reject(new Error('invalid workspace directories'))), + ); + + try { + const catalog = workspace.accessor.get(IWorkspaceSkillCatalog); + await new Promise((resolve) => setTimeout(resolve, 0)); + await expect(catalog.ready).rejects.toThrow('invalid workspace directories'); + } finally { + host.dispose(); + } + }); + it('discovers all workspace directories with primary and earlier dirs winning collisions', async () => { const base = await mkdtemp(join(tmpdir(), 'skill-catalog-multi-root-')); const primary = join(base, 'primary'); diff --git a/packages/agent-core-v2/test/persistence/backends/node-fs/projectLocalConfigService.test.ts b/packages/agent-core-v2/test/persistence/backends/node-fs/projectLocalConfigService.test.ts index a973c19ca75..db547330be8 100644 --- a/packages/agent-core-v2/test/persistence/backends/node-fs/projectLocalConfigService.test.ts +++ b/packages/agent-core-v2/test/persistence/backends/node-fs/projectLocalConfigService.test.ts @@ -62,4 +62,44 @@ describe('FileProjectLocalConfigService.resolveAdditionalDirs', () => { await expect(remote.resolveAdditionalDirs('/work', ['~/shared'])).resolves.toEqual(['/work/~/shared']); expect(stated).toEqual(['/work/~/shared']); }); + + it('includes the resolved path when an additional directory is missing', async () => { + const fs = { + _serviceBrand: undefined, + stat: async () => { + throw new HostFsError(OsFsErrors.codes.OS_FS_NOT_FOUND, 'path does not exist'); + }, + } as unknown as IHostFileSystem; + const local = new FileProjectLocalConfigService( + { _serviceBrand: undefined, osHomeDir: '/client/home' } as never, + fs, + ); + + await expect(local.resolveAdditionalDirs('/work', ['../missing'])).rejects.toThrow( + 'workspace.additional_dir must exist and be a directory: /missing', + ); + }); + + it('includes the resolved path when an additional directory is a file', async () => { + const fs = { + _serviceBrand: undefined, + stat: async () => ({ isDirectory: false }), + } as unknown as IHostFileSystem; + const local = new FileProjectLocalConfigService( + { _serviceBrand: undefined, osHomeDir: '/client/home' } as never, + fs, + ); + + await expect(local.resolveAdditionalDirs('/work', ['file'])).rejects.toThrow( + 'workspace.additional_dir must exist and be a directory: /work/file', + ); + }); + + it('reports an empty additional directory value', async () => { + const { service: local } = service('/client/home'); + + await expect(local.resolveAdditionalDirs('/work', [' '])).rejects.toThrow( + 'workspace.additional_dir must not be empty', + ); + }); }); diff --git a/packages/agent-core-v2/test/workspace/workspaceDirs/workspaceDirsService.test.ts b/packages/agent-core-v2/test/workspace/workspaceDirs/workspaceDirsService.test.ts index c00e5f1702d..a76fc741511 100644 --- a/packages/agent-core-v2/test/workspace/workspaceDirs/workspaceDirsService.test.ts +++ b/packages/agent-core-v2/test/workspace/workspaceDirs/workspaceDirsService.test.ts @@ -432,4 +432,18 @@ describe('WorkspaceDirsService (registry source)', () => { expect(dirs.sessionInfo().primaryByWorkspace).toEqual({}); }); + + it('keeps an initial local config failure on ready without an unhandled watcher rejection', async () => { + await mkdir(join(dirA, '.kimi-code'), { recursive: true }); + await writeFile( + join(dirA, '.kimi-code', 'local.toml'), + '[workspace]\nadditional_dir = ["missing"]\n', + 'utf8', + ); + const dirs = createDirs(dirA, encodeWorkDirKey(dirA)); + + await expect(dirs.ready).rejects.toThrow( + `workspace.additional_dir must exist and be a directory: ${join(dirA, 'missing')}`, + ); + }); });