Skip to content
Open
Show file tree
Hide file tree
Changes from 94 commits
Commits
Show all changes
97 commits
Select commit Hold shift + click to select a range
11c457e
feat: add comment
lulusir Mar 11, 2026
cf4927b
feat: support session
lulusir Mar 16, 2026
ecffc7e
feat: rm useless coment
lulusir Mar 16, 2026
49eea95
fix: compile error
lulusir Mar 16, 2026
09cf8d0
fix: fix the di error,ant the chat pannel hide unnecessary buttons in…
lulusir Mar 17, 2026
6f3f604
feat: support image content
lulusir Mar 17, 2026
759ba33
fix: ci problem
lulusir Mar 17, 2026
c7bce9e
feat: refactor the AcpCliBackService and remove the dependency on Acp…
lulusir Mar 17, 2026
7617e89
feat: use pty instead of terminal。fix history restoration errors
lulusir Mar 18, 2026
d1ab00f
chore: update yarn lock
lulusir Mar 18, 2026
8ba3bf0
feat: add node path
lulusir Mar 18, 2026
6a2d65a
fix: dep problem of ci
lulusir Mar 19, 2026
8a280e7
fix: ci
lulusir Mar 19, 2026
d5487bd
feat: adjust the startup logic of the chat panel
lulusir Mar 22, 2026
0df4932
fix: init fail
lulusir Mar 22, 2026
1e34e4f
feat: disable cache of session list
lulusir Mar 23, 2026
91bd610
feat: change agentConfig to be passed from the front end
lulusir Mar 23, 2026
4981036
fix: not connected error when initalizing acp
lulusir Mar 25, 2026
7baecb9
fix: chat history error
lulusir Mar 25, 2026
2c9846f
feat: add inline chat
lulusir Mar 25, 2026
c7325e0
feat: refactor ACP components with contribution point pattern
lulusir Apr 9, 2026
6fe13e2
style: add disabled styles for chat history list and new button
lulusir Apr 9, 2026
efc73f2
feat: add disabled prop to AcpChatHistory component
lulusir Apr 9, 2026
416b190
feat: subscribe to session loading event in AcpChatViewHeader
lulusir Apr 9, 2026
8622e66
feat: disable chat input during session loading
lulusir Apr 9, 2026
373e731
fix: code review problem
lulusir Apr 10, 2026
6ab3b85
fix: ci
lulusir Apr 10, 2026
b042e42
feat: reserve space for existing active sessions to avoid being elimi…
lulusir Apr 10, 2026
dd01c17
feat(acp): scope @file and @folder search to agent cwd
lulusir Apr 13, 2026
59be0c3
feat(acp): pass agentCwd to ChatInputWrapperRender from appConfig
lulusir Apr 13, 2026
a6b25fb
fix(acp): use agentCwd in folder getHighestLevelItems root comparison
lulusir Apr 13, 2026
54c3365
feat(acp): support configuring enabled mention types via IChatRenderR…
lulusir Apr 13, 2026
c571a20
feat(acp): disable @code mention in ai-native contribution
lulusir Apr 13, 2026
b430df8
feat: throw error when acp not ready
lulusir Apr 13, 2026
55fdb22
feat(acp): support providerDefaultInput for slash command shortcuts
lulusir Apr 14, 2026
b3817a7
feat(acp): add providerDefaultInput example for Explain slash command
lulusir Apr 14, 2026
66aa709
fix: compile error
lulusir Apr 14, 2026
bee35d4
fix(acp): auto-create session when loading fails with Resource not found
lulusir Apr 15, 2026
1cae0af
docs: add ACP chat multi-workspace support design spec
lulusir Apr 15, 2026
03e5434
docs: add ACP chat multi-workspace implementation plan
lulusir Apr 15, 2026
b8b6436
feat(acp): add i18n strings for multi-workspace directory selection
lulusir Apr 15, 2026
0799d36
feat(acp): add pickWorkspaceDir utility for multi-workspace selection
lulusir Apr 15, 2026
2def541
feat(acp): use pickWorkspaceDir in ACPSessionProvider for multi-works…
lulusir Apr 15, 2026
77e0654
feat(acp): use pickWorkspaceDir in AcpChatAgent for multi-workspace s…
lulusir Apr 15, 2026
d50630f
fix(acp): return empty string instead of undefined from pickWorkspaceDir
lulusir Apr 15, 2026
931e2bf
feat(acp): cache workspace dir selection and add switch button in header
lulusir Apr 15, 2026
8c449d7
fix(acp): move workspace switch button to AcpChatViewHeader
lulusir Apr 15, 2026
00eab8c
fix(acp): read cached workspace dir on each render instead of stale s…
lulusir Apr 15, 2026
f490bb4
feat(acp): auto-create new session after switching workspace directory
lulusir Apr 15, 2026
e9fe42f
fix(acp): fix workspace dir tooltip not updating and improve hover text
lulusir Apr 15, 2026
1d90cb2
fix(acp): force Popover remount on workspace dir change via key prop
lulusir Apr 15, 2026
28c7ede
feat: rm doc
lulusir Apr 15, 2026
2e1f25b
feat(acp): rewrite ACPChatAgentPromptProvider with XML-free prompt fo…
lulusir Apr 15, 2026
0c8a594
feat(acp): add 30s timeout with retry button for ACP initialization
lulusir Apr 16, 2026
312b181
feat(ai-native): add ChatWelcomePageRender extension point
lulusir May 9, 2026
a31cd3b
feat(acp): fallback to default agent when ACP initialization fails
lulusir May 12, 2026
84d5ade
feat(ai-native): add chat input contribution point and @ mention trig…
lulusir May 13, 2026
935ae25
feat(ai-native): add ChatViewRegistry and ChatHistoryRegistry contrib…
lulusir May 13, 2026
166637b
feat(acp): wire chat view and history contribution points
lulusir May 13, 2026
b53cad8
feat(ai-native): consume ChatHistoryRegistry in ACP view and unify DI…
lulusir May 13, 2026
399a2b9
revert: remove comment-only changes in chat module files
lulusir May 13, 2026
9d89e96
revert: remove top-level JSDoc comments and restore chat.view.tsx to …
lulusir May 13, 2026
abf81d2
feat: add search.followSymlinks preference to control symlink followi…
May 13, 2026
7c5a83b
fix: add onPreferenceChanged mock to search test fixtures
May 13, 2026
5f72a3c
refactor(ai-native): restore core files to main and use .acp.ts subcl…
lulusir May 13, 2026
81b81ba
feat: add acp mode
lulusir May 13, 2026
9509d3f
feat(ai-native): add AcpChatInput component and register in ACP mode
lulusir May 14, 2026
6e2d5db
refactor(ai-native): extract ACP-specific components into browser/com…
lulusir May 14, 2026
48a9672
feat(ai-native): inject slash command text into editor in ACP mode
lulusir May 15, 2026
b8745b5
feat(ai-native): add ACP footer buttons (MCP, Rules, slash command)
lulusir May 15, 2026
d6a360e
refactor(ai-native): move footer registration from useEffect to Contr…
lulusir May 15, 2026
0b90553
feat(ai-native): insert slash commands as tags at cursor position
lulusir May 15, 2026
7cd5cba
fix(ai-native): strip space from slash command display and use data-a…
lulusir May 15, 2026
3d6d0e8
chore: delete the incorrect tests
lulusir May 16, 2026
51ebbc1
fix(ai-native): deduplicate availableCommands by name and remove debu…
lulusir May 17, 2026
08706b9
test(ai-native): restore deleted test files from main
lulusir May 18, 2026
4e5f7d2
fix(ai-native): sync command state when selecting slash command from …
lulusir May 18, 2026
be2664c
chore(ai-native): remove unnecessary debug logs from ACP module
lulusir May 18, 2026
1651b75
fix(ai-native): resolve TypeScript errors in ACP session provider and…
lulusir May 18, 2026
0d6fd34
chore(ai-native): comment out verbose debug log in CLI client
lulusir May 18, 2026
262bd11
test(ai-native): fix slash command name assertion to not expect trail…
lulusir May 18, 2026
8442d87
fix(ai-native): prevent slash command duplication in input and chat list
lulusir May 18, 2026
5f56c83
chore(ai-native): remove unnecessary debug logs from ACP CLI client s…
lulusir May 18, 2026
2db958c
test(ai-native): add unit tests for ACP CLI back service
lulusir May 18, 2026
e5ab9be
test(ai-native): fix ACP browser unit tests and make initStorage lazy
lulusir May 18, 2026
9f7128c
chore(ai-native): remove unused import and empty CSS comments
lulusir May 18, 2026
9d87caf
fix(ai-native): fix MentionItem[] type mismatch in folder search
lulusir May 18, 2026
a65d4a3
fix(ai-native): fix button visibility logic and add missing test import
lulusir May 18, 2026
131aba3
test(ai-native): add unit tests for ACP node service files
lulusir May 18, 2026
e9768a7
fix(ai-native): pass messageId to deferred chat component in ChatReply
lulusir May 18, 2026
438d387
test(ai-native): add unit tests for CliAgentProcessManager
lulusir May 18, 2026
be0610c
test(ai-native): fix lint errors in acp-cli-back test and add test cases
lulusir May 18, 2026
c16663e
Merge branch 'main' into feat/acp
May 19, 2026
bc60220
Merge remote-tracking branch 'origin/feat/support-follow-symlinks' in…
May 19, 2026
9e75eb8
fix(search): make search.followSymlinks setting actually take effect
May 20, 2026
b010e3b
chore: mark @opensumi/ide-dev-tool as private to skip lerna publish
May 20, 2026
5f5844e
Revert "chore: mark @opensumi/ide-dev-tool as private to skip lerna p…
May 20, 2026
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
5 changes: 3 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -49,8 +49,9 @@
"rebuild:node": "sumi rebuild",
"start": "yarn run rebuild:node && cross-env HOST=127.0.0.1 WS_PATH=ws://127.0.0.1:8000 NODE_ENV=development tsx ./scripts/start",
"start:e2e": "yarn start --script=start:e2e",
"start:electron": "cross-env NODE_ENV=development tsx ./scripts/start-electron",
"start:lite": "cross-env NODE_ENV=development tsx ./scripts/start --script=start:lite",
"start:electron": "cross-env NODE_ENV=development tsx ./scripts/start-electron",
"start:lite": "cross-env NODE_ENV=development tsx ./scripts/start --script=start:lite",
"start:client": "cross-env HOST=127.0.0.1 WS_PATH=ws://127.0.0.1:7001 NODE_ENV=development tsx ./scripts/start --script=start:client",
"start:pty-service": "KTLOG_SHOW_DEBUG=1 npx tsx packages/terminal-next/src/node/pty.proxy.remote.exec.ts",
"start:remote": "yarn run rebuild:node && cross-env NODE_ENV=development tsx ./scripts/start",
"test": "node --expose-gc ./node_modules/.bin/jest --forceExit --detectOpenHandles",
Expand Down
1 change: 1 addition & 0 deletions packages/addons/src/browser/file-search.contribution.ts
Original file line number Diff line number Diff line change
Expand Up @@ -366,6 +366,7 @@ export class FileSearchQuickCommandHandler {
useGitIgnore: true,
noIgnoreParent: true,
excludePatterns: this.getPreferenceSearchExcludes(),
followSymlinks: this.preferenceService.get<boolean>('search.followSymlinks') ?? true,
},
token,
);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import { AcpPermissionRpcService } from '../../../lib/browser/acp/acp-permission-rpc.service';
import { AcpPermissionBridgeService } from '../../../lib/browser/acp/permission-bridge.service';

// Mock dependencies
const mockBridgeService = {
showPermissionDialog: jest.fn(),
handleUserDecision: jest.fn(),
handleDialogClose: jest.fn(),
cancelRequest: jest.fn(),
onDidRequestPermission: jest.fn(),
onDidReceivePermissionResult: jest.fn(),
getActiveDialogCount: jest.fn(),
getActiveDialogs: jest.fn(),
};

const mockLogger = {
log: jest.fn(),
error: jest.fn(),
debug: jest.fn(),
verbose: jest.fn(),
warn: jest.fn(),
};

describe('AcpPermissionRpcService', () => {
let service: AcpPermissionRpcService;

beforeEach(() => {
jest.clearAllMocks();

service = new AcpPermissionRpcService();
Object.defineProperty(service, 'permissionBridgeService', { value: mockBridgeService, writable: true });
Object.defineProperty(service, 'logger', { value: mockLogger, writable: true });
});

describe('$showPermissionDialog()', () => {
it('should forward params to bridge service and return decision', async () => {
const params = {
requestId: 'req-001',
title: 'Test title',
kind: 'write',
content: 'Test content',
locations: [{ path: '/workspace/file.txt', line: 10 }],
command: undefined,
options: [{ optionId: 'opt-1', name: 'Allow', kind: 'allow_once' }],
timeout: 30000,
};

mockBridgeService.showPermissionDialog.mockResolvedValue({
type: 'allow',
optionId: 'opt-1',
always: false,
});

const result = await service.$showPermissionDialog(params);

expect(mockBridgeService.showPermissionDialog).toHaveBeenCalledWith({
requestId: 'req-001',
title: 'Test title',
kind: 'write',
content: 'Test content',
locations: [{ path: '/workspace/file.txt', line: 10 }],
command: undefined,
options: [{ optionId: 'opt-1', name: 'Allow', kind: 'allow_once' }],
timeout: 30000,
});
expect(result).toEqual({ type: 'allow', optionId: 'opt-1', always: false });
});

it('should return cancelled on error', async () => {
const params = {
requestId: 'req-002',
title: 'Test title',
kind: 'write',
content: 'Test content',
options: [],
timeout: 30000,
};

mockBridgeService.showPermissionDialog.mockRejectedValue(new Error('Bridge error'));

const result = await service.$showPermissionDialog(params);

expect(result).toEqual({ type: 'cancelled' });
});
});

describe('$cancelRequest()', () => {
it('should forward cancel request to bridge service', async () => {
await service.$cancelRequest('req-001');

expect(mockBridgeService.cancelRequest).toHaveBeenCalledWith('req-001');
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,196 @@
import { Emitter } from '@opensumi/ide-core-common';

import {
AcpPermissionBridgeService,
ShowPermissionDialogParams,
} from '../../../lib/browser/acp/permission-bridge.service';

// Mock @opensumi/di to make decorators no-ops
jest.mock('@opensumi/di', () => {
const actual = jest.requireActual('@opensumi/di');
const noopDecorator = () => () => {};
return {
...actual,
Injectable: () => (cls: any) => cls,
Autowired: noopDecorator,
Inject: noopDecorator,
Optional: noopDecorator,
};
});

// Mock dependencies
const mockLogger = {
log: jest.fn(),
error: jest.fn(),
debug: jest.fn(),
verbose: jest.fn(),
warn: jest.fn(),
};

const mockMainLayoutService = {};

describe('AcpPermissionBridgeService', () => {
let service: AcpPermissionBridgeService;

const mockParams: ShowPermissionDialogParams = {
requestId: 'req-001',
title: 'Test permission',
kind: 'write',
content: 'Edit file.txt',
locations: [{ path: '/workspace/file.txt' }],
options: [
{ optionId: 'allow_once', name: 'Allow Once', kind: 'allow_once' },
{ optionId: 'reject_once', name: 'Reject', kind: 'reject_once' },
],
timeout: 5000,
};

beforeEach(() => {
jest.useFakeTimers();
jest.clearAllMocks();

service = new AcpPermissionBridgeService();
Object.defineProperty(service, 'logger', { value: mockLogger, writable: true });
Object.defineProperty(service, 'mainLayoutService', { value: mockMainLayoutService, writable: true });
});

afterEach(() => {
jest.useRealTimers();
});

describe('showPermissionDialog()', () => {
it('should return cancelled if dialog already exists for requestId', async () => {
const promise1 = service.showPermissionDialog(mockParams);
const promise2 = service.showPermissionDialog(mockParams);

expect(await promise2).toEqual({ type: 'cancelled' });
});

it('should fire onDidRequestPermission event', async () => {
const receivedParams: ShowPermissionDialogParams[] = [];
service.onDidRequestPermission((params) => receivedParams.push(params));

service.showPermissionDialog(mockParams);

expect(receivedParams).toHaveLength(1);
expect(receivedParams[0].requestId).toBe('req-001');
});

it('should resolve with allow when user decides allow_once', async () => {
const promise = service.showPermissionDialog(mockParams);

service.handleUserDecision('req-001', 'allow_once', 'allow_once');

const result = await promise;
expect(result).toEqual({
type: 'allow',
optionId: 'allow_once',
always: false,
});
});

it('should resolve with reject when user decides reject_once', async () => {
const promise = service.showPermissionDialog(mockParams);

service.handleUserDecision('req-001', 'reject_once', 'reject_once');

const result = await promise;
expect(result).toEqual({
type: 'reject',
optionId: 'reject_once',
always: false,
});
});

it('should resolve with allow and always=true for allow_always', async () => {
const promise = service.showPermissionDialog(mockParams);

service.handleUserDecision('req-001', 'allow_always', 'allow_always');

const result = await promise;
expect(result.type).toBe('allow');
expect(result.always).toBe(true);
});

it('should fire onDidReceivePermissionResult on user decision', async () => {
const results: any[] = [];
service.onDidReceivePermissionResult((result) => results.push(result));

const promise = service.showPermissionDialog(mockParams);
service.handleUserDecision('req-001', 'allow_once', 'allow_once');
await promise;

expect(results).toHaveLength(1);
expect(results[0].requestId).toBe('req-001');
expect(results[0].decision.type).toBe('allow');
});
});

describe('handleDialogClose()', () => {
it('should resolve with timeout when dialog closes', async () => {
const promise = service.showPermissionDialog(mockParams);

service.handleDialogClose('req-001');

const result = await promise;
expect(result).toEqual({ type: 'timeout' });
});

it('should do nothing when no pending decision', () => {
// Should not throw
service.handleDialogClose('non-existent-id');
});

it('should fire onDidReceivePermissionResult with timeout decision', async () => {
const results: any[] = [];
service.onDidReceivePermissionResult((result) => results.push(result));

const promise = service.showPermissionDialog(mockParams);
service.handleDialogClose('req-001');
await promise;

expect(results).toHaveLength(1);
expect(results[0].decision.type).toBe('timeout');
});
});

describe('cancelRequest()', () => {
it('should resolve with timeout (same as handleDialogClose)', async () => {
const promise = service.showPermissionDialog(mockParams);

service.cancelRequest('req-001');

const result = await promise;
expect(result).toEqual({ type: 'timeout' });
});
});

describe('getActiveDialogCount()', () => {
it('should return 0 initially', () => {
expect(service.getActiveDialogCount()).toBe(0);
});

it('should return correct count with active dialogs', () => {
service.showPermissionDialog(mockParams);
expect(service.getActiveDialogCount()).toBe(1);

service.handleUserDecision('req-001', 'allow_once', 'allow_once');
expect(service.getActiveDialogCount()).toBe(0);
});
});

describe('getActiveDialogs()', () => {
it('should return empty array initially', () => {
expect(service.getActiveDialogs()).toEqual([]);
});

it('should return active dialog props', () => {
service.showPermissionDialog(mockParams);

const dialogs = service.getActiveDialogs();
expect(dialogs).toHaveLength(1);
expect(dialogs[0].requestId).toBe('req-001');
expect(dialogs[0].visible).toBe(true);
});
});
});
Loading
Loading