Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/surface-invalid-additional-dir-errors.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@moonshot-ai/kimi-code": patch
---

Show the invalid workspace directory path instead of silently exiting during TUI startup.
Original file line number Diff line number Diff line change
Expand Up @@ -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[] {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ export class WorkspaceSkillCatalogService extends Disposable implements IWorkspa
);
}
this.ready = this.loadAll();
void this.ready.catch(() => undefined);
}

private get contributions(): Map<
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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' });
Expand All @@ -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}`,
);
}
}
Expand Down Expand Up @@ -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);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -174,10 +174,11 @@ function workspaceContextStub(workDir: string): IWorkspaceContext {
function workspaceDirsStub(
sharedAdditionalDirs: readonly string[] = [],
additionalDirs: readonly string[] = sharedAdditionalDirs,
ready: Promise<void> = Promise.resolve(),
): IWorkspaceDirs {
return {
_serviceBrand: undefined,
ready: Promise.resolve(),
ready,
additionalDirs,
sharedAdditionalDirs,
primaryByWorkspace: {},
Expand Down Expand Up @@ -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');
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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',
);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -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')}`,
);
});
});
Loading