Files
qwen-code/packages/web-shell/client/daemon/workspace/actions.ts
T
1853b1505f fix(subscription): isolate activation and restore sign-in recovery (#16453)
* fix(web-shell): wait for session before checking subscription commands

* fix(cli): keep signed-out subscription startup in the composer

* fix(subscription): preserve model selection across sign-in

* fix(subscription): align sign-in with model selection scope

* fix(subscription): apply reasoning defaults across models

* fix(subscription): tag Qwen web links by client

* fix(subscription): prefix Qwen web client tags

* fix(subscription): attribute Qwen web links by paywall scene

* fix(subscription): recover historical sessions with saved API models

* fix(cli): remove the signed-out subscription startup notice

* fix(subscription): support standalone desktop activation and expose errors

* fix(subscription): isolate activation and session model selection

* fix(subscription): enforce auth policy and restore home login

* test(cli): preserve the default initializer mock on restore

---------

Co-authored-by: luoyun <[email protected]>
Co-authored-by: 易良 <[email protected]>
2026-11-06 20:11:52 +08:00

1479 lines
46 KiB
TypeScript

/**
* @license
* Copyright 2025 Qwen Team
* SPDX-License-Identifier: Apache-2.0
*/
import {
EXTENSION_ARCHIVE_UPLOAD_TIMEOUT_MS,
DaemonClient,
type GoalControlRequest,
type GoalSnapshotV2,
type GoalStateResponse,
} from '@qwen-code/sdk/daemon';
import { withActionTimeout } from '../timing.js';
import { createDaemonRestRequest } from '../rest-request.js';
import type {
DaemonAutofixPrWatchList,
DaemonDirectoryListing,
DaemonFileStat,
DaemonGoal,
DaemonScheduledTask,
DaemonWorkspaceActions,
DaemonWorkspaceDirectoryPickerResult,
DaemonWorkspacePathSuggestions,
} from './types.js';
const AGENT_GENERATE_TIMEOUT_MS = 330_000;
function hasGoalSnapshot(value: unknown): value is DaemonGoal {
if (!value || typeof value !== 'object') return false;
const snapshot = (value as { snapshot?: unknown }).snapshot;
if (!snapshot || typeof snapshot !== 'object') return false;
const candidate = snapshot as Partial<GoalSnapshotV2>;
if (
candidate.v !== 2 ||
(candidate.activity !== 'idle' &&
candidate.activity !== 'running' &&
candidate.activity !== 'verifying')
) {
return false;
}
return candidate.goal === null || typeof candidate.goal === 'object';
}
export interface CreateDaemonWorkspaceActionsArgs {
getClient: () => DaemonClient | undefined;
getWorkspaceCwd: () => string | undefined;
baseUrl: string;
token?: string;
}
export function createDaemonWorkspaceActions({
getClient,
getWorkspaceCwd,
baseUrl,
token,
}: CreateDaemonWorkspaceActionsArgs): DaemonWorkspaceActions {
// Login cleanup must outlive disposal of the workspace transport.
const subscriptionRequest = async <T>(
run: (client: DaemonClient) => Promise<T>,
): Promise<T> => {
const client = new DaemonClient({ baseUrl, token });
try {
return await run(client);
} finally {
client.dispose();
}
};
const requestRest = (url: URL, init: RequestInit) =>
createDaemonRestRequest(
baseUrl,
token,
getClient(),
)(daemonRoutePath(url, baseUrl), init);
return {
async setUiLanguage(language, scope, workspaceCwd) {
const client = requireClient(getClient, 'Change UI language failed');
if (scope === 'workspace' && !workspaceCwd) {
throw new Error('The selected workspace is unavailable');
}
return withActionTimeout(
scope === 'workspace' && workspaceCwd
? client.workspaceByCwd(workspaceCwd).setUiLanguage(language)
: client.setUserLanguage(language),
'Change UI language timed out',
);
},
async listSessions(options) {
const client = requireClient(getClient, 'List sessions failed');
const cwd = getWorkspaceCwd();
if (!cwd) return [];
return withActionTimeout(
client
.listWorkspaceSessionsPage(cwd, options)
.then((page) => page.sessions),
'List sessions timed out',
);
},
async listSessionsPage(options) {
const client = requireClient(getClient, 'List sessions failed');
const cwd = getWorkspaceCwd();
if (!cwd) return { sessions: [] };
return withActionTimeout(
client.listWorkspaceSessionsPage(cwd, options),
'List sessions timed out',
);
},
async listSessionGroups(workspaceCwd) {
const client = requireClient(getClient, 'List session groups failed');
const currentCwd = getWorkspaceCwd();
const targetCwd = workspaceCwd ?? currentCwd;
if (!targetCwd) return { groups: [], colorOptions: [] };
return withActionTimeout(
workspaceCwd && workspaceCwd !== currentCwd
? client.workspaceByCwd(workspaceCwd).listSessionGroups()
: client.listSessionGroups(targetCwd),
'List session groups timed out',
);
},
async createSessionGroup(input, workspaceCwd) {
const client = requireClient(getClient, 'Create session group failed');
const currentCwd = getWorkspaceCwd();
const targetCwd = workspaceCwd ?? requireWorkspaceCwd(getWorkspaceCwd);
return withActionTimeout(
workspaceCwd && workspaceCwd !== currentCwd
? client.workspaceByCwd(workspaceCwd).createSessionGroup(input)
: client.createSessionGroup(targetCwd, input),
'Create session group timed out',
);
},
async updateSessionGroup(groupId, update) {
const client = requireClient(getClient, 'Update session group failed');
const cwd = requireWorkspaceCwd(getWorkspaceCwd);
return withActionTimeout(
client.updateSessionGroup(cwd, groupId, update),
'Update session group timed out',
);
},
async deleteSessionGroup(groupId) {
const client = requireClient(getClient, 'Delete session group failed');
const cwd = requireWorkspaceCwd(getWorkspaceCwd);
return withActionTimeout(
client.deleteSessionGroup(cwd, groupId),
'Delete session group timed out',
);
},
async updateSessionOrganization(sessionId, update) {
const client = requireClient(
getClient,
'Update session organization failed',
);
return withActionTimeout(
client.updateSessionOrganization(sessionId, update),
'Update session organization timed out',
);
},
async deleteSession(sessionId: string) {
const client = requireClient(getClient, 'Delete session failed');
const result = await withActionTimeout(
client.deleteSessionsData([sessionId]),
'Delete session timed out',
);
if (result.errors.length > 0) {
throw new Error(result.errors[0].error);
}
return result.removed.length > 0 || result.notFound.length > 0;
},
async deleteSessions(sessionIds: string[]) {
const client = requireClient(getClient, 'Delete sessions failed');
return withActionTimeout(
client.deleteSessionsData(sessionIds),
'Delete sessions timed out',
);
},
async exportSession(sessionId, format = 'html') {
const client = requireClient(getClient, 'Export session failed');
return withActionTimeout(
client.exportSession(sessionId, { format }),
'Export session timed out',
);
},
async archiveSession(sessionId: string) {
const client = requireClient(getClient, 'Archive session failed');
const result = await withActionTimeout(
client.archiveSessionsData([sessionId]),
'Archive session timed out',
);
if (result.errors.length > 0) {
throw new Error(result.errors[0].error);
}
return result.archived.length > 0 || result.alreadyArchived.length > 0;
},
async unarchiveSession(sessionId: string) {
const client = requireClient(getClient, 'Unarchive session failed');
const result = await withActionTimeout(
client.unarchiveSessionsData([sessionId]),
'Unarchive session timed out',
);
if (result.errors.length > 0) {
throw new Error(result.errors[0].error);
}
return result.unarchived.length > 0 || result.alreadyActive.length > 0;
},
async loadChannels() {
const workspace = requireWorkspaceClient(
getClient,
getWorkspaceCwd,
'Load channels failed',
);
const [catalog, snapshot] = await withActionTimeout(
Promise.all([
workspace.workspaceChannelTypes(),
workspace.workspaceChannels(),
]),
'Load channels timed out',
);
return { catalog, snapshot };
},
async listChannelEvents(query) {
const workspace = requireWorkspaceClient(
getClient,
getWorkspaceCwd,
'Load channel events failed',
);
return withActionTimeout(
workspace.workspaceChannelEvents(query),
'Load channel events timed out',
);
},
async upsertChannel(name, request) {
const workspace = requireWorkspaceClient(
getClient,
getWorkspaceCwd,
'Update channel failed',
);
return workspace.upsertWorkspaceChannel(name, request);
},
async removeChannel(name, request) {
const workspace = requireWorkspaceClient(
getClient,
getWorkspaceCwd,
'Remove channel failed',
);
return workspace.deleteWorkspaceChannel(name, request);
},
async setChannelStartup(name, request) {
const workspace = requireWorkspaceClient(
getClient,
getWorkspaceCwd,
'Update channel startup failed',
);
return workspace.setWorkspaceChannelStartup(name, request);
},
async startChannel(name) {
const workspace = requireWorkspaceClient(
getClient,
getWorkspaceCwd,
'Start channel failed',
);
return workspace.startWorkspaceChannel(name);
},
async stopChannel(name) {
const workspace = requireWorkspaceClient(
getClient,
getWorkspaceCwd,
'Stop channel failed',
);
return workspace.stopWorkspaceChannel(name);
},
async restartChannel(name) {
const workspace = requireWorkspaceClient(
getClient,
getWorkspaceCwd,
'Restart channel failed',
);
return workspace.restartWorkspaceChannel(name);
},
channelPairing: {
async list(name) {
const workspace = requireWorkspaceClient(
getClient,
getWorkspaceCwd,
'Load channel pairing requests failed',
);
return withActionTimeout(
workspace.workspaceChannelPairingRequests(name),
'Load channel pairing requests timed out',
);
},
async approve(name, code) {
const workspace = requireWorkspaceClient(
getClient,
getWorkspaceCwd,
'Approve channel pairing failed',
);
return workspace.approveWorkspaceChannelPairing(name, { code });
},
async approvals(name) {
const workspace = requireWorkspaceClient(
getClient,
getWorkspaceCwd,
'Load channel pairing approvals failed',
);
return withActionTimeout(
workspace.workspaceChannelPairingApprovals(name),
'Load channel pairing approvals timed out',
);
},
async revoke(name, request) {
const workspace = requireWorkspaceClient(
getClient,
getWorkspaceCwd,
'Revoke channel pairing approval failed',
);
return workspace.revokeWorkspaceChannelPairingApproval(name, request);
},
},
async ensureRuntime() {
const workspace = requireWorkspaceClient(
getClient,
getWorkspaceCwd,
'Initialize workspace runtime failed',
);
return withActionTimeout(
workspace.ensureRuntime(),
'Initialize workspace runtime timed out',
62_000,
);
},
async loadMcpConfig() {
const workspace = requireWorkspaceClient(
getClient,
getWorkspaceCwd,
'Load MCP configuration failed',
);
return withActionTimeout(
workspace.mcpConfig(),
'Load MCP configuration timed out',
);
},
async loadMcpStatus() {
const workspace = requireWorkspaceClient(
getClient,
getWorkspaceCwd,
'Load MCP status failed',
);
return withActionTimeout(
workspace.runtimeMcp(),
'Load MCP status timed out',
);
},
async initializeMcp() {
const workspace = requireWorkspaceClient(
getClient,
getWorkspaceCwd,
'Initialize MCP failed',
);
return withActionTimeout(
workspace.ensureRuntime().then(() => ({ accepted: true })),
'Initialize MCP timed out',
62_000,
);
},
async reloadMcp() {
const workspace = requireWorkspaceClient(
getClient,
getWorkspaceCwd,
'Reload MCP failed',
);
return withActionTimeout(
workspace.reloadRuntimeMcp(),
'Reload MCP timed out',
5 * 60_000,
);
},
async loadMcpTools(serverName) {
const workspace = requireWorkspaceClient(
getClient,
getWorkspaceCwd,
'Load MCP tools failed',
);
return withActionTimeout(
workspace.runtimeMcpTools(serverName),
'Load MCP tools timed out',
);
},
async loadMcpResources(serverName) {
const workspace = requireWorkspaceClient(
getClient,
getWorkspaceCwd,
'Load MCP resources failed',
);
return withActionTimeout(
workspace.runtimeMcpResources(serverName),
'Load MCP resources timed out',
);
},
async restartMcpServer(serverName) {
const workspace = requireWorkspaceClient(
getClient,
getWorkspaceCwd,
'Restart MCP server failed',
);
return withActionTimeout(
workspace.restartRuntimeMcpServer(serverName),
'Restart MCP server timed out',
5 * 60_000,
);
},
async manageMcpServer(serverName, action) {
const client = requireClient(getClient, 'Manage MCP server failed');
const workspace = requireWorkspaceClient(
getClient,
getWorkspaceCwd,
'Manage MCP server failed',
);
const timeoutMs = action === 'authenticate' ? 10 * 60_000 : 5 * 60_000;
if (action === 'enable' || action === 'disable') {
const config =
action === 'disable' ? await workspace.mcpConfig() : undefined;
const scopes =
action === 'enable'
? (['user', 'workspace'] as const)
: [
Object.hasOwn(config!.user, serverName) &&
!Object.hasOwn(config!.workspace, serverName)
? ('user' as const)
: ('workspace' as const),
];
let changed: boolean | undefined;
for (const scope of scopes) {
const result = await (scope === 'workspace'
? workspace.setMcpServerEnabled(serverName, action === 'enable')
: client.setUserMcpServerEnabled(serverName, action === 'enable'));
if (result.changed !== undefined) {
changed = (changed ?? false) || result.changed;
}
}
return {
serverName,
action,
ok: true,
...(changed === undefined ? {} : { changed }),
};
}
return withActionTimeout(
workspace.manageRuntimeMcpServer(serverName, action),
'Manage MCP server timed out',
timeoutMs,
);
},
async addRuntimeMcpServer(request) {
const client = requireClient(getClient, 'Add MCP server failed');
return withActionTimeout(
client.addRuntimeMcpServer(request),
'Add MCP server timed out',
5 * 60_000,
);
},
async removeRuntimeMcpServer(name) {
const client = requireClient(getClient, 'Remove MCP server failed');
return withActionTimeout(
client.removeRuntimeMcpServer(name),
'Remove MCP server timed out',
5 * 60_000,
);
},
async setMcpServer(name, scope, config) {
const client = requireClient(getClient, 'Save MCP server failed');
const workspace = requireWorkspaceClient(
getClient,
getWorkspaceCwd,
'Save MCP server failed',
);
return scope === 'user'
? client.setUserMcpServer(name, config)
: workspace.setMcpServer(name, config);
},
async removeMcpServer(name, scope) {
const client = requireClient(getClient, 'Remove MCP server failed');
const workspace = requireWorkspaceClient(
getClient,
getWorkspaceCwd,
'Remove MCP server failed',
);
return scope === 'user'
? client.removeUserMcpServer(name)
: workspace.removeMcpServer(name);
},
async setMcpServerEnabled(name, scope, enabled) {
const client = requireClient(getClient, 'Update MCP server failed');
const workspace = requireWorkspaceClient(
getClient,
getWorkspaceCwd,
'Update MCP server failed',
);
return scope === 'user'
? client.setUserMcpServerEnabled(name, enabled)
: workspace.setMcpServerEnabled(name, enabled);
},
async loadDaemonStatus(detail) {
const client = requireClient(getClient, 'Load daemon status failed');
return withActionTimeout(
client.daemonStatus(detail),
'Load daemon status timed out',
);
},
async loadUsageDashboard(opts) {
const client = requireClient(getClient, 'Load usage dashboard failed');
return withActionTimeout(
client.usageDashboard(opts),
'Load usage dashboard timed out',
);
},
async loadSkillsStatus() {
const client = requireClient(getClient, 'Load skills failed');
return withActionTimeout(
client.workspaceSkills(),
'Load skills timed out',
);
},
async setWorkspaceSkillEnabled(skillName, enabled) {
const client = requireClient(getClient, 'Set skill enabled failed');
return withActionTimeout(
client.setWorkspaceSkillEnabled(skillName, enabled),
'Set skill enabled timed out',
);
},
async installWorkspaceSkill(request) {
const client = requireClient(getClient, 'Install skill failed');
return withActionTimeout(
client.installWorkspaceSkill(request),
'Install skill timed out',
);
},
async deleteWorkspaceSkill(skillName, scope) {
const client = requireClient(getClient, 'Delete skill failed');
return withActionTimeout(
client.deleteWorkspaceSkill(skillName, scope),
'Delete skill timed out',
);
},
async loadExtensionsStatus() {
const client = requireClient(getClient, 'Load extensions failed');
return withActionTimeout(
client.workspaceExtensions(),
'Load extensions timed out',
);
},
async loadExtensionSummaries() {
const client = requireClient(getClient, 'Load extensions failed');
return withActionTimeout(
client.workspaceExtensionSummaries(),
'Load extensions timed out',
);
},
async loadExtensionDetails(name) {
const client = requireClient(getClient, 'Load extension details failed');
return withActionTimeout(
client.workspaceExtensionDetails(name),
'Load extension details timed out',
);
},
async loadToolsStatus() {
const client = requireClient(getClient, 'Load tools failed');
return withActionTimeout(client.workspaceTools(), 'Load tools timed out');
},
async preheatAcp(timeoutMs) {
const client = requireClient(getClient, 'Preheat ACP failed');
return withActionTimeout(
client.workspaceAcpPreheat(timeoutMs),
'Preheat ACP timed out',
timeoutMs === undefined ? undefined : timeoutMs + 2_000,
);
},
async setWorkspaceToolEnabled(toolName, enabled) {
const client = requireClient(getClient, 'Set tool enabled failed');
return withActionTimeout(
client.setWorkspaceToolEnabled(toolName, enabled),
'Set tool enabled timed out',
);
},
async loadSettingsStatus() {
const client = requireClient(getClient, 'Load settings failed');
return withActionTimeout(
client.workspaceSettings(),
'Load settings timed out',
);
},
async setWorkspaceSetting(
scope: 'workspace' | 'user',
key: string,
value: unknown,
options?: {
mcpServerMutation?: { operation: 'set' | 'remove'; name: string };
},
) {
const client = requireClient(getClient, 'Set setting failed');
return withActionTimeout(
client.setWorkspaceSetting(scope, key, value, options),
'Set setting timed out',
);
},
async loadMemoryStatus(options) {
const client = requireClient(getClient, 'Load memory failed');
return withActionTimeout(
client.workspaceMemory(options),
'Load memory timed out',
);
},
async readWorkspaceFile(filePath, opts) {
const client = requireClient(getClient, 'Read workspace file failed');
return withActionTimeout(
client.readWorkspaceFile(filePath, opts),
'Read workspace file timed out',
);
},
async writeMemory(req) {
const client = requireClient(getClient, 'Write memory failed');
return withActionTimeout(
client.writeWorkspaceMemory(req),
'Write memory timed out',
);
},
async *generateContent(prompt, opts) {
const client = requireClient(getClient, 'Generate content failed');
yield* client.generateWorkspaceContent(prompt, {
signal: opts?.signal,
});
},
async listAgents() {
const client = requireClient(getClient, 'List agents failed');
return withActionTimeout(
client.listWorkspaceAgents(),
'List agents timed out',
);
},
async getAgent(agentType, scope) {
const client = requireClient(getClient, 'Get agent failed');
return withActionTimeout(
client.getWorkspaceAgent(agentType, scope ? { scope } : {}),
'Get agent timed out',
);
},
async createAgent(req) {
const client = requireClient(getClient, 'Create agent failed');
return withActionTimeout(
client.createWorkspaceAgent(req),
'Create agent timed out',
);
},
async generateAgent(description) {
const client = requireClient(getClient, 'Generate agent failed');
return withActionTimeout(
client.generateWorkspaceAgent(description),
'Generate agent timed out',
AGENT_GENERATE_TIMEOUT_MS,
);
},
async deleteAgent(agentType, scope) {
const client = requireClient(getClient, 'Delete agent failed');
return withActionTimeout(
client.deleteWorkspaceAgent(agentType, scope ? { scope } : {}),
'Delete agent timed out',
);
},
// These routes stay native REST until the ACP route table supports them.
async globWorkspace(pattern, opts) {
requireClient(getClient, 'Glob workspace failed');
const url = createDaemonRequestUrl(baseUrl, '/glob');
url.searchParams.set('pattern', pattern);
if (opts?.maxResults !== undefined) {
url.searchParams.set('maxResults', String(opts.maxResults));
}
if (opts?.includeIgnored !== undefined) {
url.searchParams.set('includeIgnored', opts.includeIgnored ? '1' : '0');
}
if (opts?.cwd !== undefined) {
url.searchParams.set('cwd', opts.cwd);
}
const res = await withActionTimeout(
requestRest(url, {
headers: createDaemonHeaders(token),
}),
'Glob workspace timed out',
);
if (!res.ok) {
throw new Error(await readDaemonError(res, 'GET /glob'));
}
const data = (await res.json()) as { matches?: unknown[] };
return {
matches: Array.isArray(data.matches)
? data.matches.filter(
(match): match is string => typeof match === 'string',
)
: [],
};
},
async loadProviders(workspaceCwd) {
const client = requireClient(getClient, 'Load providers failed');
const cwd = getWorkspaceCwd();
return withActionTimeout(
workspaceCwd && workspaceCwd !== cwd
? client.workspaceByCwd(workspaceCwd).workspaceProviders()
: client.workspaceProviders(),
'Load providers timed out',
);
},
async readFileBytes(filePath, opts) {
const client = requireClient(getClient, 'Read file bytes failed');
return withActionTimeout(
client.readWorkspaceFileBytes(filePath, opts ?? {}),
'Read file bytes timed out',
);
},
async writeFile(req) {
const client = requireClient(getClient, 'Write file failed');
return withActionTimeout(
client.writeWorkspaceFile(req),
'Write file timed out',
);
},
async editFile(req) {
const client = requireClient(getClient, 'Edit file failed');
return withActionTimeout(
client.editWorkspaceFile(req),
'Edit file timed out',
);
},
async stat(filePath) {
requireClient(getClient, 'Stat file failed');
const url = createDaemonRequestUrl(baseUrl, '/stat');
url.searchParams.set('path', filePath);
const res = await withActionTimeout(
requestRest(url, {
headers: createDaemonHeaders(token),
}),
'Stat file timed out',
);
if (!res.ok) {
throw new Error(await readDaemonError(res, 'GET /stat'));
}
return (await res.json()) as DaemonFileStat;
},
async listDirectory(dirPath) {
requireClient(getClient, 'List directory failed');
const url = createDaemonRequestUrl(baseUrl, '/list');
url.searchParams.set('path', dirPath);
const res = await withActionTimeout(
requestRest(url, {
headers: createDaemonHeaders(token),
}),
'List directory timed out',
);
if (!res.ok) {
throw new Error(await readDaemonError(res, 'GET /list'));
}
return (await res.json()) as DaemonDirectoryListing;
},
// A workspaceId selects that workspace's cron file; omitting it selects
// the primary workspace. These routes require native REST.
async listScheduledTasks(workspaceId) {
requireClient(getClient, 'List scheduled tasks failed');
const path = scheduledTasksPath(workspaceId);
const url = createDaemonRequestUrl(baseUrl, path);
const res = await withActionTimeout(
requestRest(url, {
headers: createDaemonHeaders(token),
}),
'List scheduled tasks timed out',
);
if (!res.ok) {
throw new Error(await readDaemonError(res, `GET ${path}`));
}
const data = (await res.json()) as { tasks?: DaemonScheduledTask[] };
return Array.isArray(data.tasks) ? data.tasks : [];
},
async createScheduledTask(req, workspaceId) {
requireClient(getClient, 'Create scheduled task failed');
const path = scheduledTasksPath(workspaceId);
const url = createDaemonRequestUrl(baseUrl, path);
const res = await withActionTimeout(
requestRest(url, {
method: 'POST',
headers: createDaemonJsonHeaders(token),
body: JSON.stringify(req),
}),
'Create scheduled task timed out',
);
if (!res.ok) {
throw new Error(await readDaemonError(res, `POST ${path}`));
}
return (await res.json()) as DaemonScheduledTask;
},
async updateScheduledTask(id, patch, workspaceId) {
requireClient(getClient, 'Update scheduled task failed');
const path = scheduledTasksPath(
workspaceId,
`/${encodeURIComponent(id)}`,
);
const url = createDaemonRequestUrl(baseUrl, path);
const res = await withActionTimeout(
requestRest(url, {
method: 'PATCH',
headers: createDaemonJsonHeaders(token),
body: JSON.stringify(patch),
}),
'Update scheduled task timed out',
);
if (!res.ok) {
throw new Error(await readDaemonError(res, `PATCH ${path}`));
}
return (await res.json()) as DaemonScheduledTask;
},
async runScheduledTask(id, workspaceId) {
requireClient(getClient, 'Run scheduled task failed');
const path = scheduledTasksPath(
workspaceId,
`/${encodeURIComponent(id)}/run`,
);
const url = createDaemonRequestUrl(baseUrl, path);
const res = await withActionTimeout(
requestRest(url, {
method: 'POST',
headers: createDaemonJsonHeaders(token),
}),
'Run scheduled task timed out',
);
if (!res.ok) {
throw new Error(await readDaemonError(res, `POST ${path}`));
}
return (await res.json()) as DaemonScheduledTask;
},
async deleteScheduledTask(id, workspaceId) {
requireClient(getClient, 'Delete scheduled task failed');
const path = scheduledTasksPath(
workspaceId,
`/${encodeURIComponent(id)}`,
);
const url = createDaemonRequestUrl(baseUrl, path);
const res = await withActionTimeout(
requestRest(url, {
method: 'DELETE',
headers: createDaemonHeaders(token),
}),
'Delete scheduled task timed out',
);
if (!res.ok) {
throw new Error(await readDaemonError(res, `DELETE ${path}`));
}
},
// `/autofix-pr` watches: REST-only and read-only, routed like the
// scheduled tasks (primary unqualified, others by workspace id).
async listAutofixPrWatches(workspaceId) {
requireClient(getClient, 'List autofix-pr watches failed');
const path = workspaceId
? `/workspaces/${encodeURIComponent(workspaceId)}/autofix-pr/watches`
: '/autofix-pr/watches';
const url = createDaemonRequestUrl(baseUrl, path);
const res = await withActionTimeout(
requestRest(url, {
headers: createDaemonHeaders(token),
}),
'List autofix-pr watches timed out',
);
if (!res.ok) {
throw new Error(await readDaemonError(res, `GET ${path}`));
}
const data = (await res.json()) as Partial<DaemonAutofixPrWatchList>;
return {
available: data.available === true,
...(typeof data.root === 'string' ? { root: data.root } : {}),
...(typeof data.now === 'string' ? { now: data.now } : {}),
watches: Array.isArray(data.watches) ? data.watches : [],
...(Array.isArray(data.unreadable)
? { unreadable: data.unreadable }
: {}),
};
},
// Goals. `GET /goals` is REST-only (like /scheduled-tasks) and not on the
// DaemonClient transport; the clear path reuses the existing per-session
// route so a page-level clear and a `/goal clear` in chat take the same
// code path in the daemon.
async listGoals() {
requireClient(getClient, 'List goals failed');
const url = createDaemonRequestUrl(baseUrl, '/goals');
const res = await withActionTimeout(
requestRest(url, {
headers: createDaemonHeaders(token),
}),
'List goals timed out',
);
if (!res.ok) {
throw new Error(await readDaemonError(res, 'GET /goals'));
}
const data = (await res.json()) as {
goals?: unknown[];
droppedCount?: number;
};
const rawGoals = Array.isArray(data.goals) ? data.goals : [];
const goals = rawGoals.filter(hasGoalSnapshot);
const droppedCount =
typeof data.droppedCount === 'number' && data.droppedCount > 0
? data.droppedCount
: 0;
return {
goals,
droppedCount: droppedCount + rawGoals.length - goals.length,
};
},
async clearGoal(sessionId) {
requireClient(getClient, 'Clear goal failed');
const url = createDaemonRequestUrl(
baseUrl,
`/session/${encodeURIComponent(sessionId)}/goal/clear`,
);
const res = await withActionTimeout(
requestRest(url, {
method: 'POST',
headers: createDaemonJsonHeaders(token),
body: '{}',
}),
'Clear goal timed out',
);
if (!res.ok) {
throw new Error(
await readDaemonError(res, `POST /session/${sessionId}/goal/clear`),
);
}
return (await res.json()) as { cleared: boolean };
},
async controlGoal(sessionId: string, request: GoalControlRequest) {
requireClient(getClient, 'Control goal failed');
const path = `/session/${encodeURIComponent(sessionId)}/goal`;
const url = createDaemonRequestUrl(baseUrl, path);
const res = await withActionTimeout(
requestRest(url, {
method: 'POST',
headers: createDaemonJsonHeaders(token),
body: JSON.stringify(request),
}),
'Control goal timed out',
);
if (!res.ok) {
throw new Error(await readDaemonError(res, `POST ${path}`));
}
return (await res.json()) as GoalStateResponse;
},
async loadEnv() {
const client = requireClient(getClient, 'Load env failed');
return withActionTimeout(client.workspaceEnv(), 'Load env timed out');
},
async loadPreflight() {
const client = requireClient(getClient, 'Load preflight failed');
return withActionTimeout(
client.workspacePreflight(),
'Load preflight timed out',
);
},
async initWorkspace(opts) {
const client = requireClient(getClient, 'Init workspace failed');
return withActionTimeout(
client.initWorkspace(opts),
'Init workspace timed out',
);
},
async updateAgent(agentType, req, scope) {
const client = requireClient(getClient, 'Update agent failed');
return withActionTimeout(
client.updateWorkspaceAgent(agentType, req, scope ? { scope } : {}),
'Update agent timed out',
);
},
async installExtension(params, clientId) {
const client = requireClient(getClient, 'Install extension failed');
return withActionTimeout(
client.installExtension(params, clientId),
'Install extension timed out',
);
},
async installExtensionArchive(params, clientId) {
const client = requireClient(getClient, 'Install extension failed');
return withActionTimeout(
client.installExtensionArchive(params, clientId),
'Install extension timed out',
EXTENSION_ARCHIVE_UPLOAD_TIMEOUT_MS + 10_000,
);
},
async extensionOperationStatus(operationId) {
const client = requireClient(
getClient,
'Load extension operation failed',
);
return withActionTimeout(
client.extensionOperationStatus(operationId),
'Load extension operation timed out',
);
},
async activeExtensionOperations() {
const client = requireClient(
getClient,
'Load active extension operations failed',
);
return withActionTimeout(
client.activeExtensionOperations(),
'Load active extension operations timed out',
);
},
async respondToExtensionInteraction(
operationId,
interactionId,
response,
clientId,
) {
const client = requireClient(
getClient,
'Respond to extension interaction failed',
);
return withActionTimeout(
client.respondToExtensionInteraction(
operationId,
interactionId,
response,
clientId,
),
'Respond to extension interaction timed out',
);
},
async checkExtensionUpdates(clientId) {
const client = requireClient(getClient, 'Check extension updates failed');
return withActionTimeout(
client.checkExtensionUpdates(clientId),
'Check extension updates timed out',
);
},
async refreshExtensions(clientId) {
const client = requireClient(getClient, 'Refresh extensions failed');
return withActionTimeout(
client.refreshExtensions(clientId),
'Refresh extensions timed out',
);
},
async enableExtension(name, params, clientId) {
const client = requireClient(getClient, 'Enable extension failed');
return withActionTimeout(
client.enableExtension(name, params, clientId),
'Enable extension timed out',
);
},
async disableExtension(name, params, clientId) {
const client = requireClient(getClient, 'Disable extension failed');
return withActionTimeout(
client.disableExtension(name, params, clientId),
'Disable extension timed out',
);
},
async updateExtension(name, clientId) {
const client = requireClient(getClient, 'Update extension failed');
return withActionTimeout(
client.updateExtension(name, clientId),
'Update extension timed out',
);
},
async uninstallExtension(name, clientId) {
const client = requireClient(getClient, 'Uninstall extension failed');
return withActionTimeout(
client.uninstallExtension(name, clientId),
'Uninstall extension timed out',
);
},
async startDeviceFlow(providerId) {
const client = requireClient(getClient, 'Start device flow failed');
return withActionTimeout(
client.startDeviceFlow({ providerId }),
'Start device flow timed out',
);
},
async getDeviceFlow(deviceFlowId, opts) {
const client = requireClient(getClient, 'Get device flow failed');
return withActionTimeout(
client.getDeviceFlow(deviceFlowId, opts),
'Get device flow timed out',
);
},
async cancelDeviceFlow(deviceFlowId) {
const client = requireClient(getClient, 'Cancel device flow failed');
return withActionTimeout(
client.cancelDeviceFlow(deviceFlowId),
'Cancel device flow timed out',
);
},
activateSubscription(options) {
const { workspaceCwd, ...request } = options ?? {};
const workspace = requireWorkspaceClient(
getClient,
() => workspaceCwd ?? getWorkspaceCwd(),
'Connect Qwen Subscription failed',
);
return workspace.activateSubscription(request);
},
getSubscriptionConfig(options) {
return subscriptionRequest((client) =>
client.getSubscriptionConfig(options),
);
},
startSubscriptionLogin() {
return subscriptionRequest((client) => client.startSubscriptionLogin());
},
getSubscriptionLogin(id, options) {
return subscriptionRequest((client) =>
client.getSubscriptionLogin(id, options),
);
},
cancelSubscriptionLogin(id) {
return subscriptionRequest((client) =>
client.cancelSubscriptionLogin(id),
);
},
getSubscriptionUserInfo(options) {
return subscriptionRequest((client) =>
client.getSubscriptionUserInfo(options),
);
},
getSubscriptionDefaultModel(options) {
return subscriptionRequest((client) =>
client.getSubscriptionDefaultModel(options),
);
},
signOutSubscription() {
return subscriptionRequest((client) => client.signOutSubscription());
},
async getAuthStatus() {
const client = requireClient(getClient, 'Get auth status failed');
return withActionTimeout(
client.getAuthStatus(),
'Get auth status timed out',
);
},
async getAuthProviders() {
const client = requireClient(getClient, 'Get auth providers failed');
return withActionTimeout(
client.getAuthProviders(),
'Get auth providers timed out',
);
},
async installAuthProvider(req) {
const client = requireClient(getClient, 'Install auth provider failed');
return client.installAuthProvider(req);
},
async deleteModel(target) {
const client = requireClient(getClient, 'Delete model failed');
return client.deleteModel(target);
},
async addWorkspace(cwd, options) {
const client = requireClient(getClient, 'Add workspace failed');
return withActionTimeout(
client.addWorkspace(cwd, options),
'Add workspace timed out',
);
},
/** Applies the standard mutation timeout without retrying the POST. */
async addScratchWorkspace() {
const client = requireClient(getClient, 'Add scratch workspace failed');
return withActionTimeout(
client.addScratchWorkspace(),
'Add scratch workspace timed out',
);
},
async startGitWorkspaceClone(input) {
const client = requireClient(
getClient,
'Start git workspace clone failed',
);
return withActionTimeout(
client.startGitWorkspaceClone(input),
'Start git workspace clone timed out',
);
},
async getGitWorkspaceCloneJob(jobId) {
const client = requireClient(
getClient,
'Read git workspace clone failed',
);
return withActionTimeout(
client.getGitWorkspaceCloneJob(jobId),
'Read git workspace clone timed out',
);
},
async cancelGitWorkspaceClone(jobId) {
const client = requireClient(
getClient,
'Cancel git workspace clone failed',
);
return withActionTimeout(
client.cancelGitWorkspaceClone(jobId),
'Cancel git workspace clone timed out',
);
},
async suggestWorkspacePaths(prefix) {
const client = requireClient(getClient, 'Suggest workspace paths failed');
const result = await withActionTimeout(
client.workspacePathSuggestions(prefix),
'Suggest workspace paths timed out',
);
return result as DaemonWorkspacePathSuggestions;
},
async pickWorkspaceDirectory() {
const client = requireClient(getClient, 'Open directory picker failed');
const result = await withActionTimeout(
client.workspaceDirectoryPicker(),
'Open directory picker timed out',
320_000,
);
return result as DaemonWorkspaceDirectoryPickerResult;
},
async listWorkspaceDirectories(workspaceSelector) {
const client = requireClient(
getClient,
'List workspace directories failed',
);
return withActionTimeout(
client.workspaceByCwd(workspaceSelector).directories(),
'List workspace directories timed out',
);
},
async addWorkspaceDirectory(workspaceSelector, path) {
const client = requireClient(getClient, 'Add workspace directory failed');
return withActionTimeout(
client.workspaceByCwd(workspaceSelector).addDirectory(path),
'Add workspace directory timed out',
);
},
async removeWorkspaceDirectory(workspaceSelector, path) {
const client = requireClient(
getClient,
'Remove workspace directory failed',
);
return withActionTimeout(
client.workspaceByCwd(workspaceSelector).removeDirectory(path),
'Remove workspace directory timed out',
);
},
async updateWorkspace(workspaceSelector, update) {
const client = requireClient(getClient, 'Update workspace failed');
return withActionTimeout(
client.updateWorkspace(workspaceSelector, update),
'Update workspace timed out',
);
},
async removeWorkspace(workspaceId, options) {
const client = requireClient(getClient, 'Remove workspace failed');
const removal = client.workspaceById(workspaceId).remove(options);
if (options?.timeoutMs === 0) return removal;
return withActionTimeout(
removal,
'Remove workspace timed out',
options?.timeoutMs,
);
},
};
}
// ── Helpers ─────────────────────────────────────────────────────────
function requireClient(
getClient: () => DaemonClient | undefined,
action: string,
): DaemonClient {
const client = getClient();
if (!client) {
throw new Error(`${action}: DaemonClient is not connected`);
}
return client;
}
function requireWorkspaceCwd(
getWorkspaceCwd: () => string | undefined,
): string {
const cwd = getWorkspaceCwd();
if (!cwd) {
throw new Error('Daemon workspace is not connected');
}
return cwd;
}
function requireWorkspaceClient(
getClient: () => DaemonClient | undefined,
getWorkspaceCwd: () => string | undefined,
action: string,
) {
const client = requireClient(getClient, action);
return client.workspaceByCwd(requireWorkspaceCwd(getWorkspaceCwd));
}
// Builds a scheduled-tasks REST path. With a `workspaceId` it targets that
// workspace's own cron file via the qualified route; without one it hits the
// primary surface. `suffix` appends the task id (and `/run`) for item routes.
// The aggregated view passes the primary workspace as `undefined`, so the
// primary keeps its trust-free unqualified surface while secondaries use the
// (trust-checked) qualified one.
function scheduledTasksPath(workspaceId?: string, suffix = ''): string {
return workspaceId
? `/workspaces/${encodeURIComponent(workspaceId)}/scheduled-tasks${suffix}`
: `/scheduled-tasks${suffix}`;
}
function createDaemonHeaders(token: string | undefined): HeadersInit {
const headers: Record<string, string> = {};
if (token) headers['Authorization'] = `Bearer ${token}`;
return headers;
}
// Same as createDaemonHeaders but with a JSON content-type, for the
// POST/PATCH scheduled-task writes that carry a body.
function createDaemonJsonHeaders(token: string | undefined): HeadersInit {
const headers: Record<string, string> = {
'Content-Type': 'application/json',
};
if (token) headers['Authorization'] = `Bearer ${token}`;
return headers;
}
function createDaemonRequestUrl(baseUrl: string, path: string): URL {
const normalizedBaseUrl = stripTrailingSlashes(baseUrl);
const fallbackBase =
typeof window === 'undefined' ? 'http://localhost' : window.location.origin;
return new URL(`${normalizedBaseUrl}${path}`, fallbackBase);
}
function daemonRoutePath(url: URL, baseUrl: string): string {
const fallbackBase =
typeof window === 'undefined' ? 'http://localhost' : window.location.origin;
const prefix = baseUrl
? new URL(stripTrailingSlashes(baseUrl), fallbackBase).pathname.replace(
/\/+$/,
'',
)
: '';
return `${url.pathname.slice(prefix.length)}${url.search}`;
}
function stripTrailingSlashes(value: string): string {
let end = value.length;
while (end > 0 && value.charCodeAt(end - 1) === 0x2f) end--;
return end === value.length ? value : value.slice(0, end);
}
async function readDaemonError(
res: Response,
fallback: string,
): Promise<string> {
try {
const data = (await res.json()) as { error?: unknown; message?: unknown };
const message =
typeof data.error === 'string'
? data.error
: typeof data.message === 'string'
? data.message
: undefined;
return message
? `${fallback}: ${message}`
: `${fallback}: HTTP ${res.status}`;
} catch {
return `${fallback}: HTTP ${res.status}`;
}
}