diff --git a/.changeset/fd-missing-non-git-warning.md b/.changeset/fd-missing-non-git-warning.md new file mode 100644 index 00000000..02dbd70b --- /dev/null +++ b/.changeset/fd-missing-non-git-warning.md @@ -0,0 +1,5 @@ +--- +"@moonshot-ai/kimi-code": patch +--- + +Warn when @ file completion may be unavailable because fd is missing outside a git repository. diff --git a/apps/kimi-code/src/tui/kimi-tui.ts b/apps/kimi-code/src/tui/kimi-tui.ts index c3b3ae41..c49cd57e 100644 --- a/apps/kimi-code/src/tui/kimi-tui.ts +++ b/apps/kimi-code/src/tui/kimi-tui.ts @@ -393,6 +393,7 @@ export class KimiTUI { this.renderWelcome(); setExperimentalFlags(await this.harness.getExperimentalFlags()); this.setupAutocomplete(); + this.showFileCompletionWarningIfNeeded(); void this.loadPersistedInputHistory(); this.state.editorContainer.clear(); this.state.editorContainer.addChild(this.state.editor); @@ -400,6 +401,14 @@ export class KimiTUI { return shouldReplayHistory; } + private showFileCompletionWarningIfNeeded(): void { + if (this.fdPath !== null || this.gitLsFilesCache.isGitRepo()) return; + this.showStatus( + 'Warning: fd not found and this directory is not a git repository. @ file completion may be unavailable. Install fd for full file search.', + this.state.theme.colors.warning, + ); + } + private startEventLoop(): void { this.state.ui.start(); this.terminalFocusTrackingDispose = installTerminalFocusTracking(this.state); diff --git a/apps/kimi-code/test/tui/kimi-tui-startup.test.ts b/apps/kimi-code/test/tui/kimi-tui-startup.test.ts index de33c6ab..311343d9 100644 --- a/apps/kimi-code/test/tui/kimi-tui-startup.test.ts +++ b/apps/kimi-code/test/tui/kimi-tui-startup.test.ts @@ -1,8 +1,7 @@ -import { describe, expect, it, vi } from "vitest"; +import { beforeEach, describe, expect, it, vi } from "vitest"; import type { MigrationPlan } from "@moonshot-ai/migration-legacy"; import { log } from "@moonshot-ai/kimi-code-sdk"; - import { KimiTUI, type KimiTUIStartupInput, type TUIState } from "#/tui/kimi-tui"; import { handleLoginCommand, @@ -12,11 +11,6 @@ import { promptPlatformSelection, promptLogoutProviderSelection, } from "#/tui/commands/prompts"; - -vi.mock("#/tui/commands/prompts", async (importOriginal) => { - const actual = await importOriginal(); - return { ...actual, promptPlatformSelection: vi.fn(), promptLogoutProviderSelection: vi.fn() }; -}); import { DISABLE_TERMINAL_THEME_REPORTING, ENABLE_TERMINAL_THEME_REPORTING, @@ -25,6 +19,32 @@ import { TERMINAL_THEME_LIGHT, } from "#/tui/utils/terminal-theme"; +vi.mock("#/tui/commands/prompts", async (importOriginal) => { + const actual = await importOriginal(); + return { ...actual, promptPlatformSelection: vi.fn(), promptLogoutProviderSelection: vi.fn() }; +}); + +const moduleMocks = vi.hoisted(() => ({ + detectFdPath: vi.fn(() => "fd" as string | null), + createGitLsFilesCache: vi.fn(), +})); + +function makeGitCache(isGitRepo: boolean) { + return { + isGitRepo: () => isGitRepo, + getSnapshot: () => null, + list: () => null, + }; +} + +vi.mock("#/utils/process/fd-detect", () => ({ + detectFdPath: moduleMocks.detectFdPath, +})); + +vi.mock("#/utils/git/git-ls-files", () => ({ + createGitLsFilesCache: moduleMocks.createGitLsFilesCache, +})); + interface StartupDriver { state: TUIState; init(): Promise; @@ -169,6 +189,11 @@ function captureInputListeners(driver: StartupDriver) { } describe("KimiTUI startup", () => { + beforeEach(() => { + moduleMocks.detectFdPath.mockReturnValue("fd"); + moduleMocks.createGitLsFilesCache.mockReturnValue(makeGitCache(true)); + }); + it("creates a fresh session from startup flags and syncs runtime state", async () => { const session = makeSession({ getStatus: vi.fn(async () => ({ @@ -814,6 +839,49 @@ describe("KimiTUI startup", () => { expect(uiContainsFooter(driver)).toBe(true); }); + + it("warns when fd is missing outside a git repository", async () => { + moduleMocks.detectFdPath.mockReturnValue(null); + moduleMocks.createGitLsFilesCache.mockReturnValue(makeGitCache(false)); + const harness = makeHarness(); + const driver = makeDriver(harness, makeStartupInput()) as unknown as MigrateExitDriver; + const showStatus = vi.spyOn(driver as unknown as KimiTUI, "showStatus").mockImplementation(() => {}); + + await driver.initMainTui(); + + expect(showStatus).toHaveBeenCalledWith( + expect.stringContaining("fd not found and this directory is not a git repository"), + driver.state.theme.colors.warning, + ); + }); + + it("does not warn when fd is missing inside a git repository", async () => { + moduleMocks.detectFdPath.mockReturnValue(null); + moduleMocks.createGitLsFilesCache.mockReturnValue(makeGitCache(true)); + const harness = makeHarness(); + const driver = makeDriver(harness, makeStartupInput()) as unknown as MigrateExitDriver; + const showStatus = vi.spyOn(driver as unknown as KimiTUI, "showStatus").mockImplementation(() => {}); + + await driver.initMainTui(); + + expect(showStatus.mock.calls.filter(([message]) => message.includes("fd not found"))).toEqual( + [], + ); + }); + + it("does not warn when fd is available outside a git repository", async () => { + moduleMocks.detectFdPath.mockReturnValue("fd"); + moduleMocks.createGitLsFilesCache.mockReturnValue(makeGitCache(false)); + const harness = makeHarness(); + const driver = makeDriver(harness, makeStartupInput()) as unknown as MigrateExitDriver; + const showStatus = vi.spyOn(driver as unknown as KimiTUI, "showStatus").mockImplementation(() => {}); + + await driver.initMainTui(); + + expect(showStatus.mock.calls.filter(([message]) => message.includes("fd not found"))).toEqual( + [], + ); + }); }); function uiContainsFooter(driver: StartupDriver): boolean {