diff --git a/packages/cli/src/tests/context-provider.test.ts b/packages/cli/src/tests/context-provider.test.ts new file mode 100644 index 00000000..2542b059 --- /dev/null +++ b/packages/cli/src/tests/context-provider.test.ts @@ -0,0 +1,34 @@ +import { test } from "node:test"; +import assert from "node:assert/strict"; +import { createContextStatusProvider } from "../ui/statusline/context-provider"; + +test("context statusline provider reports context usage percentage (#234)", async () => { + const provider = createContextStatusProvider("ctx", undefined, false, undefined); + const text = await provider.fetch({ + projectRoot: "/tmp", + signal: new AbortController().signal, + getSessionInfo: () => ({ + activeSessionId: "s1", + messageCount: 10, + requestCount: 2, + totalTokens: 100000, + activeTokens: 131072, + maxContextTokens: 1024 * 1024, + model: "deepseek-v4-flash", + thinkingEnabled: false, + reasoningEffort: "high", + toolUsage: {}, + }), + }); + assert.equal(text, "ctx 13%"); +}); + +test("context statusline provider returns empty without an active session (#234)", async () => { + const provider = createContextStatusProvider("ctx"); + const text = await provider.fetch({ + projectRoot: "/tmp", + signal: new AbortController().signal, + getSessionInfo: () => null, + }); + assert.equal(text, ""); +}); diff --git a/packages/cli/src/ui/statusline/context-provider.ts b/packages/cli/src/ui/statusline/context-provider.ts new file mode 100644 index 00000000..8d4b0d13 --- /dev/null +++ b/packages/cli/src/ui/statusline/context-provider.ts @@ -0,0 +1,27 @@ +import type { StatusProvider } from "./types"; + +/** + * Built-in statusline provider that shows the current session's context-window + * usage as a percentage, e.g. `ctx 12%`. (#234) + */ +export function createContextStatusProvider( + id: string, + color?: string, + newLine?: boolean, + maxLength?: number +): StatusProvider { + return { + id, + color, + newLine, + maxLength, + fetch: async (ctx) => { + const info = ctx.getSessionInfo?.(); + if (!info || !info.activeSessionId || info.maxContextTokens <= 0) { + return ""; + } + const percent = Math.min(100, Math.round((info.activeTokens / info.maxContextTokens) * 100)); + return `ctx ${percent}%`; + }, + }; +} diff --git a/packages/cli/src/ui/statusline/manager.ts b/packages/cli/src/ui/statusline/manager.ts index 9f4015ff..78ed1d3b 100644 --- a/packages/cli/src/ui/statusline/manager.ts +++ b/packages/cli/src/ui/statusline/manager.ts @@ -1,6 +1,7 @@ import type { ResolvedStatusLineSettings, StatusLineProviderConfig } from "@vegamo/deepcode-core"; import { sanitizeStatusText } from "./sanitize"; import { createCommandStatusProvider } from "./command-provider"; +import { createContextStatusProvider } from "./context-provider"; import { loadModuleProvider, validateModulePath } from "./module-provider"; import type { SessionInfo, StatusProvider, StatusSegment } from "./types"; @@ -147,6 +148,9 @@ export class StatusLineManager { } return provider; } + if (config.type === "context") { + return createContextStatusProvider(providerId, config.color, config.newLine, config.maxLength); + } return null; } diff --git a/packages/core/src/settings.ts b/packages/core/src/settings.ts index da0e2d94..be853c4c 100644 --- a/packages/core/src/settings.ts +++ b/packages/core/src/settings.ts @@ -64,6 +64,13 @@ export type StatusLineProviderConfig = color?: string; newLine?: boolean; maxLength?: number; + } + | { + type: "context"; + id?: string; + color?: string; + newLine?: boolean; + maxLength?: number; }; export type StatusLineSettings = { @@ -357,6 +364,15 @@ function normalizeStatusLineProvider(value: unknown): StatusLineProviderConfig | maxLength, }; } + if (type === "context") { + return { + type: "context", + id, + color, + newLine, + maxLength, + }; + } return null; } diff --git a/packages/core/src/tests/settings-and-notify.test.ts b/packages/core/src/tests/settings-and-notify.test.ts index 93e8dc29..fbe1410c 100644 --- a/packages/core/src/tests/settings-and-notify.test.ts +++ b/packages/core/src/tests/settings-and-notify.test.ts @@ -11,6 +11,20 @@ import { applyModelConfigSelection, resolveSettings, resolveSettingsSources } fr const TEST_PROCESS_ENV = {}; +test("resolveSettingsSources normalizes the statusline context provider (#234)", () => { + const resolved = resolveSettingsSources( + { + statusline: { enabled: true, providers: [{ type: "context" }] }, + env: { API_KEY: "sk-test" }, + }, + null, + { model: "default-model", baseURL: "https://default.example.com" }, + TEST_PROCESS_ENV + ); + assert.equal(resolved.statusline.providers.length, 1); + assert.equal(resolved.statusline.providers[0]?.type, "context"); +}); + test("resolveSettings reads top-level thinkingEnabled, notify, and webSearchTool", () => { const resolved = resolveSettings( {