Skip to content
Open
Show file tree
Hide file tree
Changes from 3 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
3 changes: 3 additions & 0 deletions packages/acp-bridge/src/status.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1049,6 +1049,9 @@ export const IDLE_HOOK_EVENTS: Record<HookEventName, ServeHookEventMeta> = {
description: 'When a new session is started',
matcherKind: 'sessionTrigger',
},
CwdChanged: {
description: 'After the session changes its working directory',
},
MessageDisplay: {
description: 'Repeatedly, as the assistant reply streams',
},
Expand Down
12 changes: 11 additions & 1 deletion packages/cli/src/acp-integration/acpAgent.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2075,6 +2075,7 @@ describe('QwenAgent MCP SSE/HTTP support', () => {
installPendingManagedConversationBinding: ReturnType<typeof vi.fn>;
commitManagedConversationBinding: ReturnType<typeof vi.fn>;
releaseManagedConversationBinding: ReturnType<typeof vi.fn>;
startCronScheduler: ReturnType<typeof vi.fn>;
appendLiveConversationTranscript: ReturnType<typeof vi.fn>;
collectActiveWorkHolds: ReturnType<typeof vi.fn>;
hasStandaloneRelocationBlockers: ReturnType<typeof vi.fn>;
Expand Down Expand Up @@ -4360,6 +4361,7 @@ describe('QwenAgent MCP SSE/HTTP support', () => {
getCreatedAt: vi.fn().mockReturnValue(1_700_000_000_000),
getTurnCount: vi.fn().mockReturnValue(3),
prompt: vi.fn().mockResolvedValue({ stopReason: 'end_turn' }),
refreshSkillsFromSettings: vi.fn().mockResolvedValue(undefined),
};
lastSessionMock = sessionMock;
return sessionMock as unknown as InstanceType<typeof Session>;
Expand Down Expand Up @@ -5078,6 +5080,7 @@ describe('QwenAgent MCP SSE/HTTP support', () => {
expect(
lastSessionMock?.hardSuspendTodoStopGuard.mock.invocationCallOrder[0],
).toBeLessThan(relocateWorkingDirectory.mock.invocationCallOrder[0]!);
expect(lastSessionMock?.startCronScheduler).toHaveBeenCalledTimes(2);
} finally {
await fs.rm(targetDir, { recursive: true, force: true });
}
Expand Down Expand Up @@ -5361,7 +5364,11 @@ describe('QwenAgent MCP SSE/HTTP support', () => {
expect(innerConfig.relocateWorkingDirectory).toHaveBeenCalledWith(
expectation.child.canonicalPath,
expectation.child.canonicalPath,
{ skipProcessChdir: true, skipArtifactMigration: true },
{
skipProcessChdir: true,
skipArtifactMigration: true,
trustedFolder: true,
},
);
expect(
lastSessionMock?.installPendingManagedConversationBinding,
Expand Down Expand Up @@ -10899,6 +10906,7 @@ describe('QwenAgent MCP SSE/HTTP support', () => {
expect.objectContaining({
toolInvocationGuard: expect.any(Function),
}),
expect.anything(),
);

mockConnectionState.resolve();
Expand Down Expand Up @@ -19662,8 +19670,10 @@ describe('QwenAgent loadSession / unstable_resumeSession', () => {
}

const sessionSettings = vi.mocked(loadCliConfig).mock.calls[0]?.[0];
const hostPolicy = vi.mocked(loadCliConfig).mock.calls[0]?.[9];
expect(sessionSettings?.experimental?.cron).toBe(false);
expect(requestSettings.merged.experimental?.cron).toBe(true);
expect(hostPolicy?.projectRuntimeCronEnabled).toBe(false);

mockConnectionState.resolve();
await agentPromise;
Expand Down
36 changes: 34 additions & 2 deletions packages/cli/src/acp-integration/acpAgent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10198,7 +10198,11 @@ class QwenAgent implements Agent {
const relocation = await config.relocateWorkingDirectory(
canonicalPath,
canonicalPath,
{ skipProcessChdir: true, skipArtifactMigration: true },
{
skipProcessChdir: true,
skipArtifactMigration: true,
trustedFolder: true,
},
);
if (conversationDirectoryExpectation !== undefined) {
await assertManagedConversationDirectoryIdentity(
Expand Down Expand Up @@ -10230,6 +10234,27 @@ class QwenAgent implements Agent {
}`,
);
}
for (const error of relocation.projectRuntimeRefreshErrors ?? []) {
warnings.push(
`Project runtime refresh failed: ${
error instanceof Error ? error.message : String(error)
}`,
);
}

try {
await session.refreshSkillsFromSettings({
reloadSettings: false,
notifyConfigChanged: false,
});
} catch (error) {
warnings.push(
`Available commands refresh failed: ${
error instanceof Error ? error.message : String(error)
}`,
);
}
session.startCronScheduler();

try {
await config
Expand Down Expand Up @@ -12498,14 +12523,20 @@ class QwenAgent implements Agent {
// not process.exit(1) the shared ACP child and every session on its
// channel. newSessionConfig maps the throw to a RequestError.
true,
this.managedToolInvocationGuard || restoreOptions || provisionalWorkspace
this.managedToolInvocationGuard ||
restoreOptions ||
provisionalWorkspace ||
sessionSource?.sourceType === 'channel'
? {
...(provisionalWorkspace
? { provisionalWorkspace: true as const }
: {}),
...(this.managedToolInvocationGuard
? { toolInvocationGuard: this.managedToolInvocationGuard }
: {}),
...(sessionSource?.sourceType === 'channel'
? { projectRuntimeCronEnabled: false }
: {}),
...(restoreOptions && sessionId
? {
sessionRestore: {
Expand All @@ -12521,6 +12552,7 @@ class QwenAgent implements Agent {
: {}),
}
: undefined,
settings,
);
if (sessionSource) {
config.setSessionSource(sessionSource.sourceType, sessionSource.sourceId);
Expand Down
12 changes: 12 additions & 0 deletions packages/cli/src/acp-integration/session/Session.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -491,7 +491,9 @@ describe('Session', () => {
getTool: ReturnType<typeof vi.fn>;
ensureTool: ReturnType<typeof vi.fn>;
registerTool: ReturnType<typeof vi.fn>;
registerSessionTool: ReturnType<typeof vi.fn>;
registerPermissionDeferredFactory: ReturnType<typeof vi.fn>;
registerSessionPermissionDeferredFactory: ReturnType<typeof vi.fn>;
revealDeferredTool: ReturnType<typeof vi.fn>;
pinDeferredToolReveal: ReturnType<typeof vi.fn>;
warmAll: ReturnType<typeof vi.fn>;
Expand Down Expand Up @@ -782,14 +784,23 @@ describe('Session', () => {
getTool: vi.fn(),
ensureTool: vi.fn().mockResolvedValue(true),
registerTool: vi.fn(),
registerSessionTool: vi.fn(),
registerPermissionDeferredFactory: vi.fn(),
registerSessionPermissionDeferredFactory: vi.fn(),
revealDeferredTool: vi.fn(),
pinDeferredToolReveal: vi.fn(),
warmAll: vi.fn().mockResolvedValue(undefined),
getFunctionDeclarationsFiltered: vi.fn((names: string[]) =>
names.map((name) => ({ name })),
),
};
mockToolRegistry.registerSessionTool = vi.fn((tool) =>
mockToolRegistry.registerTool(tool),
);
mockToolRegistry.registerSessionPermissionDeferredFactory = vi.fn(
(name, factory) =>
mockToolRegistry.registerPermissionDeferredFactory(name, factory),
);
const fileService = {
shouldGitIgnoreFile: vi.fn().mockReturnValue(false),
shouldIgnoreFile: vi.fn().mockReturnValue(false),
Expand Down Expand Up @@ -823,6 +834,7 @@ describe('Session', () => {
assertCanStartTurn: vi.fn().mockResolvedValue(undefined),
getWorkingDir: vi.fn().mockReturnValue(process.cwd()),
getProjectRoot: vi.fn().mockReturnValue('/repo'),
getContextFileNames: vi.fn().mockReturnValue(['QWEN.md', 'AGENTS.md']),
// Folder trust gates the project `.qwen/loop.md`; default trusted (the
// production default). Untrusted-folder tests override to false.
isTrustedFolder: vi.fn().mockReturnValue(true),
Expand Down
17 changes: 11 additions & 6 deletions packages/cli/src/acp-integration/session/Session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1691,14 +1691,14 @@ export async function registerCreateSubSessionTool(
}
const toolRegistry = config.getToolRegistry();
if (registrationStatus === 'deferred') {
toolRegistry.registerPermissionDeferredFactory(
toolRegistry.registerSessionPermissionDeferredFactory(
ToolNames.CREATE_SUB_SESSION,
Comment thread
qqqys marked this conversation as resolved.
async () => new CreateSubSessionTool(config),
);
await config.getGeminiClient().setTools();
return;
}
toolRegistry.registerTool(new CreateSubSessionTool(config));
toolRegistry.registerSessionTool(new CreateSubSessionTool(config));
Comment thread
qqqys marked this conversation as resolved.
// The registration lands after `config.initialize()` → `startChat()` already
// snapshotted the chat's tool declarations, and the tool is deferred — so it
// stays filtered out of the declarations until revealed. Reveal it and
Expand Down Expand Up @@ -3207,7 +3207,7 @@ export class Session implements SessionContext {
screenshotPath,
};
});
registry.registerTool(tool);
registry.registerSessionTool(tool);
if (registry.getTool(CAPTURE_SCREEN_CONTEXT_TOOL_NAME) !== tool) {
Comment thread
qqqys marked this conversation as resolved.
throw new Error(
'capture_screen_context is required for Live Voice but is disabled.',
Expand All @@ -3231,7 +3231,7 @@ export class Session implements SessionContext {
);
}
}
for (const tool of tools) registry.registerTool(tool);
for (const tool of tools) registry.registerSessionTool(tool);
for (const tool of tools) {
if (registry.getTool(tool.name) !== tool) {
throw new Error(
Expand All @@ -3258,7 +3258,7 @@ export class Session implements SessionContext {
message,
});
});
registry.registerTool(tool);
registry.registerSessionTool(tool);
if (registry.getTool(SPEAK_TO_USER_TOOL_NAME) !== tool) {
throw new Error(
'speak_to_user is required for Live Voice but is disabled.',
Expand Down Expand Up @@ -10097,6 +10097,7 @@ export class Session implements SessionContext {
const matchedContextFileWrite = didWriteProjectContextFile(
memoryWriteCandidates,
this.config.getProjectRoot(),
this.config.getContextFileNames(),
);
debugLogger.debug(
`ACP session ${this.sessionId} checked marked context-file memory tool batch; matched=${matchedContextFileWrite}`,
Expand Down Expand Up @@ -10969,7 +10970,11 @@ export class Session implements SessionContext {
// prompt right after an allow-rule call just worked.
const forceAutoReviewForAllow =
approvalMode === ApprovalMode.AUTO &&
(shouldForceAutoModeReviewForAllow(pmCtx, this.config.getCwd()) ||
(shouldForceAutoModeReviewForAllow(
pmCtx,
this.config.getCwd(),
this.config.getContextFileNames(),
) ||
shouldClassifyAllShellForAutoMode(policyToolName, this.config));
const confirmationPermission = getEffectivePermissionForConfirmation(
finalPermission,
Expand Down
7 changes: 6 additions & 1 deletion packages/cli/src/config/config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5337,13 +5337,18 @@ describe('loadCliConfig skills.directories', () => {
42 as unknown as string,
null as unknown as string,
'/abs/skills',
'relative-skills',
],
},
};

const config = await loadCliConfig(settings, argv);

expect(config.getCustomSkillDirs()).toEqual(['~/my-skills', '/abs/skills']);
expect(config.getCustomSkillDirs()).toEqual([
'~/my-skills',
'/abs/skills',
path.resolve(process.cwd(), 'relative-skills'),
]);
});

it('should return empty array when skills.directories is not set', async () => {
Expand Down
Loading
Loading