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/fd-missing-non-git-warning.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@moonshot-ai/kimi-code": patch
---

Warn when @ file completion may be unavailable because fd is missing outside a git repository.
9 changes: 9 additions & 0 deletions apps/kimi-code/src/tui/kimi-tui.ts
Original file line number Diff line number Diff line change
Expand Up @@ -393,13 +393,22 @@ 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);
this.state.ui.setFocus(this.state.editor);
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);
Expand Down
82 changes: 75 additions & 7 deletions apps/kimi-code/test/tui/kimi-tui-startup.test.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -12,11 +11,6 @@ import {
promptPlatformSelection,
promptLogoutProviderSelection,
} from "#/tui/commands/prompts";

vi.mock("#/tui/commands/prompts", async (importOriginal) => {
const actual = await importOriginal<typeof import("#/tui/commands/prompts")>();
return { ...actual, promptPlatformSelection: vi.fn(), promptLogoutProviderSelection: vi.fn() };
});
import {
DISABLE_TERMINAL_THEME_REPORTING,
ENABLE_TERMINAL_THEME_REPORTING,
Expand All @@ -25,6 +19,32 @@ import {
TERMINAL_THEME_LIGHT,
} from "#/tui/utils/terminal-theme";

vi.mock("#/tui/commands/prompts", async (importOriginal) => {
const actual = await importOriginal<typeof import("#/tui/commands/prompts")>();
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<boolean>;
Expand Down Expand Up @@ -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 () => ({
Expand Down Expand Up @@ -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 {
Expand Down