Skip to content
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
15 changes: 15 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,21 @@

## [Unreleased]

## 0.2.3 — 2026-05-14

### Fixed

- Playground saved-replay sidebar: deleting a fixture file out of band (Explorer, terminal, git pull) now refreshes the sidebar within ~1 s via a Node `fs.watch` on `.copilotkit/fixtures/`. The previous `vscode.workspace.createFileSystemWatcher` was unreliable for dot-prefixed paths on Windows; the entry would stick around until the next reload.
- Playground ▶ on a saved replay no longer drops the recorded conversation. The previous race posted `play-fixture` before the new bundle's `PlaygroundChat` had registered its replay listener; the App shell now unmounts the stale bundle synchronously on `bundle-ready`, queues the replay messages, and dispatches them after the new chat mounts (React runs child effects before parent effects on the same commit, so the listener is guaranteed to be attached).
- Playground recovery: clicking ▶ on a fixture whose file has been deleted no longer permanently bricks the panel ("Preparing chat surface…" + ENOENT loop). `load-fixture` now reads the file before committing `replayFixturePath`, refreshes the sidebar, and surfaces a one-shot warning. The `runBundle` pass also recovers when the active fixture vanishes mid-session by falling back to record mode instead of crashing.
- Playground Refresh button: always reconciles the saved-replays sidebar with disk at the start of every rebundle. Refresh is now a guaranteed manual recovery path even if the fs.watch misses an event.
- Playground chat with many `vscode.lm` tools: some models (notably Claude through Copilot Chat with ~80+ tools forwarded) silently reject requests by returning 200 OK with an empty stream. The chat now auto-retries once without the `vscode.lm` tools so the user gets a real response on the same query. If both attempts return empty, an actionable in-chat message explains the cause and points at the `copilotkit.playground.enableVscodeLmTools` setting.
- Playground stream translation: text content streamed as `LanguageModelDataPart` (text/plain or application/json mimes) is surfaced as `TEXT_MESSAGE_CONTENT` instead of being silently dropped. Newer Claude builds routed through Copilot Chat had been producing empty chats this way.

### Added

- Playground output channel now logs unknown `vscode.lm` stream-part types (constructor name + mime when applicable) so future model-side changes leave a breadcrumb instead of an empty chat.

## 0.2.2 — 2026-05-05

### Fixed
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "copilotkit-vscode-extension",
"displayName": "CopilotKit",
"version": "0.2.2",
"version": "0.2.3",
"description": "Preview generative-UI components, explore CopilotKit hooks, and inspect AG-UI agent runs — all without leaving your editor.",
"categories": [
"Other",
Expand Down
21 changes: 21 additions & 0 deletions src/extension/activate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import {
} from "./playground/view-provider";
import { scanPlayground } from "./playground/scanner";
import { PlaygroundFileWatcher } from "./playground/file-watcher";
import { FixturesDirWatcher } from "./playground/fixtures-dir-watcher";

let activePlaygroundProvider: PlaygroundViewProvider | null = null;

Expand Down Expand Up @@ -343,6 +344,26 @@ export function activate(context: vscode.ExtensionContext): void {
runPlaygroundScan();
});
context.subscriptions.push(playgroundFileWatcher);

// Reflect manual fixture changes (Explorer delete, git pull, etc.)
// in the saved-replays sidebar. We use a Node fs.watch on the
// fixtures directory rather than vscode.createFileSystemWatcher —
// the latter is unreliable for dot-prefixed paths like
// `.copilotkit/` on Windows. The webview-initiated save/delete
// round-trip already pushes a fresh list, so this watcher is for
// out-of-band edits only.
const fixturesDir = path.join(workspaceRoot, ".copilotkit", "fixtures");
const fixturesDirWatcher = new FixturesDirWatcher(
fixturesDir,
() => {
playgroundOutputChannel.appendLine(
"[playground] fixtures dir changed — refreshing sidebar",
);
playgroundProvider.refreshFixturesList();
},
(line) => playgroundOutputChannel.appendLine(line),
);
context.subscriptions.push(fixturesDirWatcher);
}

runPlaygroundScan();
Expand Down
92 changes: 92 additions & 0 deletions src/extension/playground/__tests__/fixtures-dir-watcher.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import * as fs from "node:fs";
import * as os from "node:os";
import * as path from "node:path";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";

vi.mock("vscode", () => ({}));

import { FixturesDirWatcher } from "../fixtures-dir-watcher";

describe("FixturesDirWatcher", () => {
let tmpRoot: string;
let watcher: FixturesDirWatcher | null = null;

beforeEach(() => {
tmpRoot = fs.mkdtempSync(path.join(os.tmpdir(), "fixtures-watcher-"));
});

afterEach(() => {
watcher?.dispose();
watcher = null;
fs.rmSync(tmpRoot, { recursive: true, force: true });
});

it("fires onChange when a file inside the watched dir is deleted", async () => {
const fixturesDir = path.join(tmpRoot, ".copilotkit", "fixtures");
fs.mkdirSync(fixturesDir, { recursive: true });
const filePath = path.join(fixturesDir, "x.json");
fs.writeFileSync(filePath, "{}");

const onChange = vi.fn();
watcher = new FixturesDirWatcher(fixturesDir, onChange);

fs.unlinkSync(filePath);

await waitFor(() => onChange.mock.calls.length > 0);
expect(onChange).toHaveBeenCalled();
});

it("fires onChange when a file is created in the watched dir", async () => {
const fixturesDir = path.join(tmpRoot, ".copilotkit", "fixtures");
fs.mkdirSync(fixturesDir, { recursive: true });

const onChange = vi.fn();
watcher = new FixturesDirWatcher(fixturesDir, onChange);

fs.writeFileSync(path.join(fixturesDir, "new.json"), "{}");

await waitFor(() => onChange.mock.calls.length > 0);
expect(onChange).toHaveBeenCalled();
});

it("is a no-op when the directory does not exist yet", () => {
const fixturesDir = path.join(tmpRoot, "absent", "fixtures");
const log = vi.fn();
const onChange = vi.fn();
watcher = new FixturesDirWatcher(fixturesDir, onChange, log);

// Nothing thrown, no attachment log line.
expect(log).not.toHaveBeenCalledWith(
expect.stringMatching(/attached/),
);
expect(onChange).not.toHaveBeenCalled();
});

it("dispose stops further events", async () => {
const fixturesDir = path.join(tmpRoot, ".copilotkit", "fixtures");
fs.mkdirSync(fixturesDir, { recursive: true });

const onChange = vi.fn();
watcher = new FixturesDirWatcher(fixturesDir, onChange);
watcher.dispose();
watcher = null;

fs.writeFileSync(path.join(fixturesDir, "after-dispose.json"), "{}");
// Give the OS a moment to deliver any stray events.
await new Promise((r) => setTimeout(r, 100));
expect(onChange).not.toHaveBeenCalled();
});
});

async function waitFor(
predicate: () => boolean,
{ timeoutMs = 2000, intervalMs = 20 } = {},
): Promise<void> {
const start = Date.now();
while (!predicate()) {
if (Date.now() - start > timeoutMs) {
throw new Error("waitFor timeout");
}
await new Promise((r) => setTimeout(r, intervalMs));
}
}
Loading
Loading