Skip to content

Commit b26e21f

Browse files
yiliang114buaoyezz
andcommitted
feat(vscode-ide-companion): cherry-pick ACP compatibility improvements from PR #2195
- Add extractSessionListItems() utility for robust ACP response parsing - Refactor getSessionList() and getSessionListPaged() to use the new utility - Add openNewChatTabCommand to create new session when opening chat tab - Add comprehensive test coverage for session list extraction Co-authored-by: ZZAoYe <zzbuaoye@gmail.com>
1 parent 2334386 commit b26e21f

File tree

5 files changed

+198
-24
lines changed

5 files changed

+198
-24
lines changed

packages/vscode-ide-companion/package.json

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -28,14 +28,7 @@
2828
"ide companion"
2929
],
3030
"activationEvents": [
31-
"onStartupFinished",
32-
"onView:qwenCode.chatView.panel",
33-
"onView:qwenCode.chatView.secondary",
34-
"onView:qwenCode.chatView.sidebar",
35-
"onCommand:qwen-code.openChat",
36-
"onCommand:qwen-code.focusChat",
37-
"onCommand:qwen-code.newConversation",
38-
"onCommand:qwen-code.showLogs"
31+
"onStartupFinished"
3932
],
4033
"contributes": {
4134
"jsonValidation": [
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
/**
2+
* @license
3+
* Copyright 2025 Qwen Team
4+
* SPDX-License-Identifier: Apache-2.0
5+
*/
6+
7+
import { beforeEach, describe, expect, it, vi } from 'vitest';
8+
import * as vscode from 'vscode';
9+
import { openNewChatTabCommand, registerNewCommands } from './index.js';
10+
11+
vi.mock('vscode', () => ({
12+
commands: {
13+
registerCommand: vi.fn(
14+
(_command: string, _handler: (...args: unknown[]) => unknown) => ({
15+
dispose: vi.fn(),
16+
}),
17+
),
18+
},
19+
workspace: {
20+
workspaceFolders: [],
21+
},
22+
window: {
23+
showErrorMessage: vi.fn(),
24+
showInformationMessage: vi.fn(),
25+
showWarningMessage: vi.fn(),
26+
},
27+
Uri: {
28+
joinPath: vi.fn(),
29+
},
30+
}));
31+
32+
describe('registerNewCommands', () => {
33+
beforeEach(() => {
34+
vi.clearAllMocks();
35+
});
36+
37+
it('creates a fresh session when opening a new chat tab', async () => {
38+
const fakeProvider = {
39+
show: vi.fn().mockResolvedValue(undefined),
40+
createNewSession: vi.fn().mockResolvedValue(undefined),
41+
forceReLogin: vi.fn().mockResolvedValue(undefined),
42+
};
43+
44+
registerNewCommands(
45+
{ subscriptions: [] } as unknown as vscode.ExtensionContext,
46+
vi.fn(),
47+
{} as never,
48+
() => [],
49+
() => fakeProvider as never,
50+
);
51+
52+
const commandCall = vi
53+
.mocked(vscode.commands.registerCommand)
54+
.mock.calls.find(([command]) => command === openNewChatTabCommand);
55+
56+
expect(commandCall).toBeDefined();
57+
58+
const handler = commandCall?.[1] as (() => Promise<void>) | undefined;
59+
expect(handler).toBeDefined();
60+
61+
await handler?.();
62+
63+
expect(fakeProvider.show).toHaveBeenCalledTimes(1);
64+
expect(fakeProvider.createNewSession).toHaveBeenCalledTimes(1);
65+
});
66+
});

packages/vscode-ide-companion/src/commands/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,7 @@ export function registerNewCommands(
8787
vscode.commands.registerCommand(openNewChatTabCommand, async () => {
8888
const provider = createWebViewProvider();
8989
await provider.show();
90+
await provider.createNewSession();
9091
}),
9192
);
9293

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
/**
2+
* @license
3+
* Copyright 2025 Qwen Team
4+
* SPDX-License-Identifier: Apache-2.0
5+
*/
6+
7+
import { describe, expect, it, vi } from 'vitest';
8+
import {
9+
extractSessionListItems,
10+
QwenAgentManager,
11+
} from './qwenAgentManager.js';
12+
13+
vi.mock('vscode', () => ({
14+
window: {
15+
showInformationMessage: vi.fn(),
16+
showWarningMessage: vi.fn(),
17+
showErrorMessage: vi.fn(),
18+
},
19+
}));
20+
21+
describe('extractSessionListItems', () => {
22+
it('reads ACP session arrays from the sessions field', () => {
23+
const items = extractSessionListItems({
24+
sessions: [{ sessionId: 'session-1' }],
25+
});
26+
27+
expect(items).toEqual([{ sessionId: 'session-1' }]);
28+
});
29+
30+
it('reads ACP session arrays from the legacy items field', () => {
31+
const items = extractSessionListItems({
32+
items: [{ sessionId: 'session-2' }],
33+
});
34+
35+
expect(items).toEqual([{ sessionId: 'session-2' }]);
36+
});
37+
38+
it('returns empty array for invalid responses', () => {
39+
expect(extractSessionListItems(null)).toEqual([]);
40+
expect(extractSessionListItems(undefined)).toEqual([]);
41+
expect(extractSessionListItems('string')).toEqual([]);
42+
expect(extractSessionListItems({})).toEqual([]);
43+
expect(extractSessionListItems({ sessions: 'not-array' })).toEqual([]);
44+
});
45+
46+
it('prefers sessions over items when both exist', () => {
47+
const items = extractSessionListItems({
48+
sessions: [{ sessionId: 'from-sessions' }],
49+
items: [{ sessionId: 'from-items' }],
50+
});
51+
52+
expect(items).toEqual([{ sessionId: 'from-sessions' }]);
53+
});
54+
});
55+
56+
describe('QwenAgentManager session list compatibility', () => {
57+
it('maps paged ACP session lists returned via items', async () => {
58+
const manager = new QwenAgentManager();
59+
const listSessions = vi.fn().mockResolvedValue({
60+
items: [
61+
{
62+
sessionId: 'session-3',
63+
prompt: 'Fix sidebar history',
64+
mtime: 1772114825468.5825,
65+
cwd: '/workspace/qwen-code',
66+
},
67+
],
68+
});
69+
70+
(
71+
manager as unknown as {
72+
connection: { listSessions: typeof listSessions };
73+
}
74+
).connection = { listSessions };
75+
76+
const page = await manager.getSessionListPaged({ size: 20 });
77+
78+
expect(listSessions).toHaveBeenCalledWith({ size: 20 });
79+
expect(page).toEqual({
80+
sessions: [
81+
{
82+
id: 'session-3',
83+
sessionId: 'session-3',
84+
title: 'Fix sidebar history',
85+
name: 'Fix sidebar history',
86+
startTime: undefined,
87+
lastUpdated: 1772114825468.5825,
88+
messageCount: 0,
89+
projectHash: undefined,
90+
filePath: undefined,
91+
cwd: '/workspace/qwen-code',
92+
},
93+
],
94+
nextCursor: undefined,
95+
hasMore: false,
96+
});
97+
});
98+
});

packages/vscode-ide-companion/src/services/qwenAgentManager.ts

Lines changed: 32 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,36 @@ import { handleAuthenticateUpdate } from '../utils/authNotificationHandler.js';
4040

4141
export type { ChatMessage, PlanEntry, ToolCallUpdateData };
4242

43+
/**
44+
* Extract session list items from ACP response.
45+
* Handles both 'sessions' (new) and 'items' (legacy) response shapes.
46+
* @param response - The ACP session/list response
47+
* @returns Array of session items, or empty array if invalid
48+
*/
49+
export function extractSessionListItems(
50+
response: unknown,
51+
): Array<Record<string, unknown>> {
52+
if (!response || typeof response !== 'object') {
53+
return [];
54+
}
55+
56+
const payload = response as {
57+
sessions?: unknown;
58+
items?: unknown;
59+
};
60+
61+
// Prefer 'sessions' field, fall back to 'items' for backwards compatibility
62+
if (Array.isArray(payload.sessions)) {
63+
return payload.sessions as Array<Record<string, unknown>>;
64+
}
65+
66+
if (Array.isArray(payload.items)) {
67+
return payload.items as Array<Record<string, unknown>>;
68+
}
69+
70+
return [];
71+
}
72+
4373
/**
4474
* Qwen Agent Manager
4575
*
@@ -413,14 +443,7 @@ export class QwenAgentManager {
413443
console.log('[QwenAgentManager] ACP session list response:', response);
414444

415445
const res: unknown = response;
416-
let items: Array<Record<string, unknown>> = [];
417-
418-
if (res && typeof res === 'object' && 'sessions' in res) {
419-
const sessionsValue = (res as { sessions?: unknown }).sessions;
420-
items = Array.isArray(sessionsValue)
421-
? (sessionsValue as Array<Record<string, unknown>>)
422-
: [];
423-
}
446+
const items = extractSessionListItems(res);
424447

425448
console.log(
426449
'[QwenAgentManager] Sessions retrieved via ACP:',
@@ -514,14 +537,7 @@ export class QwenAgentManager {
514537
...(cursor !== undefined ? { cursor } : {}),
515538
});
516539
const res: unknown = response;
517-
let items: Array<Record<string, unknown>> = [];
518-
519-
if (res && typeof res === 'object' && 'sessions' in res) {
520-
const sessionsValue = (res as { sessions?: unknown }).sessions;
521-
items = Array.isArray(sessionsValue)
522-
? (sessionsValue as Array<Record<string, unknown>>)
523-
: [];
524-
}
540+
const items = extractSessionListItems(res);
525541

526542
const mapped = items.map((item) => ({
527543
id: item.sessionId || item.id,

0 commit comments

Comments
 (0)