Skip to content

Commit a2c74df

Browse files
fix: statusline provider id dedup, newLine support, and CI type errors
- Project-level providers override user-level by id instead of appending - Add newLine option to break statusline into multiple rows - Remove external deepcode-core from esbuild (bundle inline) - Fix permissions.test.ts readonly array type errors
1 parent dc068a6 commit a2c74df

8 files changed

Lines changed: 100 additions & 26 deletions

File tree

packages/cli/src/tests/statusline.test.ts

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import { sanitizeStatusText, STATUS_SEGMENT_MAX_LENGTH } from "../ui/statusline/
77
import { validateModulePath, loadModuleProvider } from "../ui/statusline/module-provider";
88
import { createCommandStatusProvider } from "../ui/statusline/command-provider";
99
import { StatusLineManager } from "../ui/statusline/manager";
10-
import { resolveSettings } from "@vegamo/deepcode-core";
10+
import { resolveSettings, resolveSettingsSources } from "@vegamo/deepcode-core";
1111
import type { ResolvedStatusLineSettings } from "@vegamo/deepcode-core";
1212

1313
test("sanitizeStatusText returns empty for null/undefined/empty", () => {
@@ -171,6 +171,33 @@ test("loadModuleProvider succeeds for a well-formed module", async () => {
171171
}
172172
});
173173

174+
test("resolveSettingsSources lets project-level providers override user-level by id", () => {
175+
const resolved = resolveSettingsSources(
176+
{
177+
statusline: {
178+
enabled: true,
179+
providers: [
180+
{ type: "command", id: "model", command: "echo user-model" },
181+
{ type: "command", id: "branch", command: "echo user-branch" },
182+
],
183+
},
184+
},
185+
{
186+
statusline: {
187+
providers: [
188+
{ type: "command", id: "model", command: "echo project-model" },
189+
{ type: "command", id: "cwd", command: "echo project-cwd" },
190+
],
191+
},
192+
},
193+
{ model: "default-model", baseURL: "https://default.example.com" }
194+
);
195+
const ids = resolved.statusline.providers.map((p) => p.id);
196+
assert.deepEqual(ids, ["branch", "model", "cwd"]);
197+
const modelProvider = resolved.statusline.providers.find((p) => p.id === "model");
198+
assert.equal(modelProvider?.type === "command" && modelProvider.command, "echo project-model");
199+
});
200+
174201
test("StatusLineManager emits segments after fetch and stops cleanly", async () => {
175202
const config: ResolvedStatusLineSettings = {
176203
enabled: true,

packages/cli/src/ui/statusline/command-provider.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ export function createCommandStatusProvider(
3232
return {
3333
id,
3434
color: config.color,
35+
newLine: config.newLine,
3536
maxLength: config.maxLength,
3637
fetch: ({ signal }: StatusProviderContext) =>
3738
new Promise<string>((resolve) => {

packages/cli/src/ui/statusline/manager.ts

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,12 @@ function segmentsEqual(a: StatusSegment[], b: StatusSegment[]): boolean {
1111
return false;
1212
}
1313
for (let i = 0; i < a.length; i++) {
14-
if (a[i]?.id !== b[i]?.id || a[i]?.text !== b[i]?.text || a[i]?.color !== b[i]?.color) {
14+
if (
15+
a[i]?.id !== b[i]?.id ||
16+
a[i]?.text !== b[i]?.text ||
17+
a[i]?.color !== b[i]?.color ||
18+
a[i]?.newLine !== b[i]?.newLine
19+
) {
1520
return false;
1621
}
1722
}
@@ -130,7 +135,17 @@ export class StatusLineManager {
130135
if (!resolvedPath) {
131136
return null;
132137
}
133-
return loadModuleProvider(resolvedPath, config.color, providerId, config.timeoutMs, config.maxLength);
138+
const provider = await loadModuleProvider(
139+
resolvedPath,
140+
config.color,
141+
providerId,
142+
config.timeoutMs,
143+
config.maxLength
144+
);
145+
if (provider && config.newLine) {
146+
provider.newLine = true;
147+
}
148+
return provider;
134149
}
135150
return null;
136151
}
@@ -156,6 +171,9 @@ export class StatusLineManager {
156171
if (provider.color) {
157172
segment.color = provider.color;
158173
}
174+
if (provider.newLine) {
175+
segment.newLine = true;
176+
}
159177
return segment;
160178
} catch {
161179
return null;

packages/cli/src/ui/statusline/types.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ export type StatusSegment = {
44
id: string;
55
text: string;
66
color?: string;
7+
newLine?: boolean;
78
};
89

910
export type SessionInfo = {
@@ -29,6 +30,7 @@ export type StatusProvider = {
2930
id: string;
3031
color?: string;
3132
maxLength?: number;
33+
newLine?: boolean;
3234
fetch: (ctx: StatusProviderContext) => Promise<string>;
3335
dispose?: () => void;
3436
};

packages/cli/src/ui/views/PromptInput.tsx

Lines changed: 27 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -845,15 +845,33 @@ export const PromptInput = React.memo(function PromptInput({
845845
</Box>
846846
)}
847847
{statusLineSegments && statusLineSegments.length > 0 && (
848-
<Box>
849-
{statusLineSegments.map((segment, index) => (
850-
<React.Fragment key={segment.id}>
851-
{index > 0 && <Text dimColor>{statusLineSeparator ?? " · "}</Text>}
852-
<Text color={segment.color} dimColor={!segment.color}>
853-
{segment.text}
854-
</Text>
855-
</React.Fragment>
856-
))}
848+
<Box flexDirection="column">
849+
{(() => {
850+
const lines: StatusSegment[][] = [];
851+
let currentLine: StatusSegment[] = [];
852+
for (const segment of statusLineSegments) {
853+
if (segment.newLine && currentLine.length > 0) {
854+
lines.push(currentLine);
855+
currentLine = [];
856+
}
857+
currentLine.push(segment);
858+
}
859+
if (currentLine.length > 0) {
860+
lines.push(currentLine);
861+
}
862+
return lines.map((line, lineIndex) => (
863+
<Box key={lineIndex}>
864+
{line.map((segment, index) => (
865+
<React.Fragment key={segment.id}>
866+
{index > 0 && <Text dimColor>{statusLineSeparator ?? " · "}</Text>}
867+
<Text color={segment.color} dimColor={!segment.color}>
868+
{segment.text}
869+
</Text>
870+
</React.Fragment>
871+
))}
872+
</Box>
873+
));
874+
})()}
857875
</Box>
858876
)}
859877
</Box>

packages/core/src/settings.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ export type StatusLineProviderConfig =
5353
cwd?: string;
5454
timeoutMs?: number;
5555
color?: string;
56+
newLine?: boolean;
5657
maxLength?: number;
5758
}
5859
| {
@@ -61,6 +62,7 @@ export type StatusLineProviderConfig =
6162
path: string;
6263
timeoutMs?: number;
6364
color?: string;
65+
newLine?: boolean;
6466
maxLength?: number;
6567
};
6668

@@ -278,6 +280,7 @@ function normalizeStatusLineProvider(value: unknown): StatusLineProviderConfig |
278280
typeof maxLengthRaw === "number" && Number.isFinite(maxLengthRaw) && maxLengthRaw > 0
279281
? Math.floor(maxLengthRaw)
280282
: undefined;
283+
const newLine = value["newLine"] === true ? true : undefined;
281284

282285
if (type === "command") {
283286
const command = trimString(value["command"]);
@@ -292,6 +295,7 @@ function normalizeStatusLineProvider(value: unknown): StatusLineProviderConfig |
292295
cwd: cwdRaw || undefined,
293296
timeoutMs,
294297
color,
298+
newLine,
295299
maxLength,
296300
};
297301
}
@@ -306,6 +310,7 @@ function normalizeStatusLineProvider(value: unknown): StatusLineProviderConfig |
306310
path: modulePath,
307311
timeoutMs,
308312
color,
313+
newLine,
309314
maxLength,
310315
};
311316
}
@@ -349,7 +354,10 @@ function mergeStatusLine(
349354
): ResolvedStatusLineSettings {
350355
const userConfig = normalizeStatusLine(userSettings?.statusline) ?? {};
351356
const projectConfig = normalizeStatusLine(projectSettings?.statusline) ?? {};
352-
const providers = [...(userConfig.providers ?? []), ...(projectConfig.providers ?? [])];
357+
const userProviders = userConfig.providers ?? [];
358+
const projectProviders = projectConfig.providers ?? [];
359+
const projectIds = new Set(projectProviders.map((p) => p.id));
360+
const providers = [...userProviders.filter((p) => !projectIds.has(p.id)), ...projectProviders];
353361
const enabled = projectConfig.enabled ?? userConfig.enabled ?? providers.length > 0;
354362
const refreshMs = projectConfig.refreshMs ?? userConfig.refreshMs ?? DEFAULT_STATUSLINE_REFRESH_MS;
355363
const separator = projectConfig.separator ?? userConfig.separator ?? DEFAULT_STATUSLINE_SEPARATOR;

packages/core/src/tests/permissions.test.ts

Lines changed: 13 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import {
1111
isPathInAnyDirectory,
1212
parseBashSideEffects,
1313
} from "../common/permissions";
14+
import type { PermissionScope } from "../settings";
1415

1516
const tempDirs: string[] = [];
1617

@@ -52,10 +53,10 @@ test("computeToolCallPermissions maps tool calls to permission requests", () =>
5253
sessionId: "session-1",
5354
projectRoot,
5455
settings: {
55-
allow: [],
56-
deny: [],
57-
ask: ["write-out-cwd", "network"],
58-
defaultMode: "allowAll",
56+
allow: [] as PermissionScope[],
57+
deny: [] as PermissionScope[],
58+
ask: ["write-out-cwd", "network"] as PermissionScope[],
59+
defaultMode: "allowAll" as const,
5960
},
6061
resolveSnippetPath: () => path.join(projectRoot, "src", "file.ts"),
6162
toolCalls: [
@@ -100,10 +101,10 @@ test("computeToolCallPermissions only asks for scopes not already allowed", () =
100101
sessionId: "session-1",
101102
projectRoot,
102103
settings: {
103-
allow: ["read-in-cwd"],
104-
deny: [],
105-
ask: [],
106-
defaultMode: "askAll",
104+
allow: ["read-in-cwd"] as PermissionScope[],
105+
deny: [] as PermissionScope[],
106+
ask: [] as PermissionScope[],
107+
defaultMode: "askAll" as const,
107108
},
108109
toolCalls: [
109110
{
@@ -138,10 +139,10 @@ test("computeToolCallPermissions allows read tool calls under skill scan paths",
138139
projectRoot,
139140
readPermissionExemptPaths: [skillRoot],
140141
settings: {
141-
allow: [],
142-
deny: [],
143-
ask: [],
144-
defaultMode: "askAll",
142+
allow: [] as PermissionScope[],
143+
deny: [] as PermissionScope[],
144+
ask: [] as PermissionScope[],
145+
defaultMode: "askAll" as const,
145146
},
146147
toolCalls: [
147148
{

scripts/esbuild.config.js

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,6 @@ await build({
2020
jsx: "automatic",
2121
jsxImportSource: "react",
2222
packages: "external",
23-
external: ["@vegamo/deepcode-core"],
2423
logOverride: {
2524
"empty-import-meta": "silent",
2625
},

0 commit comments

Comments
 (0)