Skip to content
This repository was archived by the owner on Jun 28, 2026. It is now read-only.
Merged
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
2 changes: 1 addition & 1 deletion src/llms/local/service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ export class LocalModelService {
private static instance: LocalModelService;

constructor() {
this.logger = Logger.initialize(LocalModelService.name, {
this.logger = Logger.initialize("LocalModelService", {
minLevel: LogLevel.DEBUG,
enableConsole: true,
enableFile: true,
Expand Down
2 changes: 1 addition & 1 deletion src/services/chat-history-worker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ export class ChatHistoryWorker {

constructor() {
this.chatHistoryRepo = ChatHistoryRepository.getInstance();
this.logger = Logger.initialize(ChatHistoryWorker.name, {
this.logger = Logger.initialize("ChatHistoryWorker", {
minLevel: LogLevel.DEBUG,
enableConsole: true,
enableFile: true,
Expand Down
2 changes: 1 addition & 1 deletion src/services/docker/DockerModelService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ export class DockerModelService implements vscode.Disposable {
private static readonly DAEMON_CHECK_COOLDOWN_MS = 15_000; // Re-check at most once per 15 seconds

constructor() {
this.logger = Logger.initialize(DockerModelService.name, {
this.logger = Logger.initialize("DockerModelService", {
minLevel: LogLevel.INFO,
enableConsole: true,
enableFile: true,
Expand Down
2 changes: 1 addition & 1 deletion src/services/file-storage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ export class FileStorage implements IStorage {

constructor() {
this.createCodeBuddyFolder();
this.logger = Logger.initialize(FileStorage.name, {
this.logger = Logger.initialize("FileStorage", {
minLevel: LogLevel.DEBUG,
enableConsole: true,
enableFile: true,
Expand Down
2 changes: 1 addition & 1 deletion src/services/git-actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ export class GitActions {
baseDir: this.rootPath,
};
this.git = simpleGit(options);
this.logger = Logger.initialize(GitActions.name, {
this.logger = Logger.initialize("GitActions", {
minLevel: LogLevel.DEBUG,
enableConsole: true,
enableFile: true,
Expand Down
2 changes: 1 addition & 1 deletion src/services/project-rules.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ export class ProjectRulesService implements vscode.Disposable {
private statusCallback: ((status: IProjectRulesStatus) => void) | undefined;

private constructor() {
this.logger = Logger.initialize(ProjectRulesService.name, {
this.logger = Logger.initialize("ProjectRulesService", {
minLevel: LogLevel.DEBUG,
enableConsole: true,
enableFile: true,
Expand Down
36 changes: 24 additions & 12 deletions src/services/test-runner.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -121,18 +121,30 @@ class JestVitestStrategy implements TestFrameworkStrategy {
}

parseCounts(output: string) {
// "Tests: 3 passed, 1 failed, 4 total"
const m = output.match(
/Tests:\s*(?:(\d+)\s*failed,?\s*)?(?:(\d+)\s*skipped,?\s*)?(?:(\d+)\s*passed,?\s*)?(\d+)\s*total/i,
);
if (!m) return null;
return {
failed: parseInt(m[1] || "0"),
skipped: parseInt(m[2] || "0"),
passed: parseInt(m[3] || "0"),
total: parseInt(m[4] || "0"),
duration: "unknown",
};
// Match the Jest/Vitest summary line: "Tests: N passed, N failed, N total"
// Fields can appear in any order, so extract each independently.
const summaryMatch = output.match(/Tests:\s*(.+?)\s*total/i);
if (!summaryMatch) return null;

const line = summaryMatch[0];
const passedM = line.match(/(\d+)\s*passed/i);
const failedM = line.match(/(\d+)\s*failed/i);
const skippedM = line.match(/(\d+)\s*(?:skipped|pending|todo)/i);
const totalM = line.match(/(\d+)\s*total/i);

if (!totalM) return null;

const passed = passedM ? parseInt(passedM[1]) : 0;
const failed = failedM ? parseInt(failedM[1]) : 0;
const skipped = skippedM ? parseInt(skippedM[1]) : 0;
const total = parseInt(totalM[1]);

// Duration: "Time: 20.56 s" or "Time: 1.234s"
let duration = "unknown";
const durMatch = output.match(/Time:\s*([\d.]+\s*m?s)/i);
if (durMatch) duration = durMatch[1].trim();

return { failed, skipped, passed, total, duration };
}

parseFailures(output: string): TestFailure[] {
Expand Down
2 changes: 1 addition & 1 deletion src/utils/terminal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ export class Terminal {
> = new Map();

constructor() {
this.logger = Logger.initialize(Terminal.name, {
this.logger = Logger.initialize("Terminal", {
minLevel: LogLevel.DEBUG,
enableConsole: true,
enableFile: true,
Expand Down
4 changes: 4 additions & 0 deletions src/webview-providers/base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,8 @@ import {
TeamGraphHandler,
DoctorHandler,
OnboardingHandler,
GitStatusHandler,
TestRunnerHandler,
} from "./handlers";
import { HandlerContext, MessageHandlerRegistry } from "./handlers/types";
import { AccessControlService } from "../services/access-control.service";
Expand Down Expand Up @@ -274,6 +276,8 @@ export abstract class BaseWebViewProvider implements vscode.Disposable {
this.handlerRegistry.register(new TerminalViewerHandler());
this.handlerRegistry.register(new DoctorHandler());
this.handlerRegistry.register(new OnboardingHandler());
this.handlerRegistry.register(new GitStatusHandler());
this.handlerRegistry.register(new TestRunnerHandler());
this.handlerRegistry.register(
new PerformanceHandler(
() => this.performanceProfiler,
Expand Down
66 changes: 66 additions & 0 deletions src/webview-providers/handlers/git-status-handler.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import { WebviewMessageHandler, HandlerContext } from "./types";
import { GitActions, BranchInfo } from "../../services/git-actions";

const GIT_COMMANDS = ["git-status-request"] as const;

function isGitMessage(
msg: unknown,
): msg is { command: (typeof GIT_COMMANDS)[number] } {
return (
typeof msg === "object" &&
msg !== null &&
"command" in msg &&
typeof (msg as Record<string, unknown>).command === "string" &&
GIT_COMMANDS.includes(
(msg as Record<string, unknown>).command as (typeof GIT_COMMANDS)[number],
)
);
}

export class GitStatusHandler implements WebviewMessageHandler {
readonly commands = [...GIT_COMMANDS];

async handle(
message: Record<string, unknown>,
ctx: HandlerContext,
): Promise<void> {
if (!isGitMessage(message)) return;

try {
const git = new GitActions();
const [branchInfo, status] = await Promise.all([
git.getCurrentBranchInfo(),
git.getRepositoryStatus(),
]);

const changedFiles: number =
(status.modified?.length ?? 0) +
(status.not_added?.length ?? 0) +
(status.created?.length ?? 0) +
(status.deleted?.length ?? 0) +
(status.renamed?.length ?? 0);

const staged: number = status.staged?.length ?? 0;

await ctx.webview.webview.postMessage({
type: "git-status-result",
branch: branchInfo.current,
upstream: branchInfo.upstream ?? null,
changedFiles,
staged,
ahead: status.ahead ?? 0,
behind: status.behind ?? 0,
});
} catch {
await ctx.webview.webview.postMessage({
type: "git-status-result",
branch: null,
upstream: null,
changedFiles: 0,
staged: 0,
ahead: 0,
behind: 0,
});
}
}
}
2 changes: 2 additions & 0 deletions src/webview-providers/handlers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,3 +23,5 @@ export { CostTrackingHandler } from "./cost-tracking-handler";
export { TerminalViewerHandler } from "./terminal-viewer-handler";
export { DoctorHandler } from "./doctor-handler";
export { OnboardingHandler } from "./onboarding-handler";
export { GitStatusHandler } from "./git-status-handler";
export { TestRunnerHandler } from "./test-runner-handler";
105 changes: 105 additions & 0 deletions src/webview-providers/handlers/test-runner-handler.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
import { WebviewMessageHandler, HandlerContext } from "./types";
import {
TestRunnerService,
TestResult,
} from "../../services/test-runner.service";

const TEST_COMMANDS = ["test-run"] as const;

function isTestMessage(msg: unknown): msg is {
command: (typeof TEST_COMMANDS)[number];
testPath?: string;
testName?: string;
} {
return (
typeof msg === "object" &&
msg !== null &&
"command" in msg &&
typeof (msg as Record<string, unknown>).command === "string" &&
TEST_COMMANDS.includes(
(msg as Record<string, unknown>)
.command as (typeof TEST_COMMANDS)[number],
)
);
}

export class TestRunnerHandler implements WebviewMessageHandler {
readonly commands = [...TEST_COMMANDS];

async handle(
message: Record<string, unknown>,
ctx: HandlerContext,
): Promise<void> {
if (!isTestMessage(message)) return;

const service = TestRunnerService.getInstance();

// Let the webview know tests are starting
await ctx.webview.webview.postMessage({
type: "test-run-started",
});

try {
const result: TestResult = await service.runTests(
typeof message.testPath === "string" ? message.testPath : undefined,
typeof message.testName === "string" ? message.testName : undefined,
);

await ctx.webview.webview.postMessage({
type: "test-run-result",
result: {
framework: result.framework,
command: result.command,
passed: result.passed,
failed: result.failed,
skipped: result.skipped,
total: result.total,
duration: result.duration,
success: result.success,
failures: result.failures,
parseWarning: result.parseWarning ?? null,
},
});
} catch (err) {
const errMsg = err instanceof Error ? err.message : "Test run failed";

// Attempt partial parsing from timeout output
const timeoutMatch = errMsg.match(
/^Test command timed out after [\d.]+s\. Output so far:\n([\s\S]+)$/,
);
if (timeoutMatch) {
const partialOutput = timeoutMatch[1];
const partial = service.parseOutput(
partialOutput,
"unknown",
"timed-out",
);

if (partial.total > 0 || partial.failures.length > 0) {
await ctx.webview.webview.postMessage({
type: "test-run-result",
result: {
framework: partial.framework,
command: partial.command,
passed: partial.passed,
failed: partial.failed,
skipped: partial.skipped,
total: partial.total,
duration: partial.duration,
success: false,
failures: partial.failures,
parseWarning:
"Test command timed out. Results shown are partial.",
},
});
return;
}
}

await ctx.webview.webview.postMessage({
type: "test-run-error",
error: errMsg,
});
}
}
}
95 changes: 95 additions & 0 deletions webviewUi/src/components/GitStatusBar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import { useEffect } from "react";
import styled from "styled-components";
import { useGitStore } from "../stores/git.store";

const Bar = styled.div`
display: flex;
align-items: center;
gap: 6px;
font-size: 11px;
color: var(--vscode-descriptionForeground, #888);
white-space: nowrap;
overflow: hidden;
`;

const BranchName = styled.span`
display: inline-flex;
align-items: center;
gap: 4px;
color: var(--vscode-foreground, #ccc);
font-weight: 500;
max-width: 120px;
overflow: hidden;
text-overflow: ellipsis;
`;

const Badge = styled.span<{ $color?: string }>`
display: inline-flex;
align-items: center;
gap: 2px;
padding: 1px 5px;
border-radius: 8px;
font-size: 10px;
font-weight: 500;
background: ${(p) => p.$color ?? "rgba(255,255,255,0.08)"};
color: var(--vscode-foreground, #ccc);
`;

const BranchIcon = () => (
<svg
width="12"
height="12"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
>
<line x1="6" y1="3" x2="6" y2="15" />
<circle cx="18" cy="6" r="3" />
<circle cx="6" cy="18" r="3" />
<path d="M18 9a9 9 0 0 1-9 9" />
</svg>
);

export function GitStatusBar() {
const { branch, changedFiles, staged, ahead, behind, startPolling, stopPolling } =
useGitStore();

useEffect(() => {
startPolling();
return stopPolling;
}, [startPolling, stopPolling]);

if (!branch) return null;

return (
<Bar title={`Branch: ${branch}`}>
<BranchName>
<BranchIcon />
{branch}
</BranchName>
{changedFiles > 0 && (
<Badge $color="rgba(224,175,104,0.15)" title={`${changedFiles} changed file${changedFiles !== 1 ? "s" : ""}`}>
{changedFiles}M
</Badge>
)}
{staged > 0 && (
<Badge $color="rgba(122,162,247,0.15)" title={`${staged} staged file${staged !== 1 ? "s" : ""}`}>
{staged}S
</Badge>
)}
{ahead > 0 && (
<Badge $color="rgba(137,209,133,0.15)" title={`${ahead} commit${ahead !== 1 ? "s" : ""} ahead`}>
↑{ahead}
</Badge>
)}
{behind > 0 && (
<Badge $color="rgba(247,118,142,0.15)" title={`${behind} commit${behind !== 1 ? "s" : ""} behind`}>
↓{behind}
</Badge>
)}
</Bar>
);
}
Loading
Loading