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
42 changes: 24 additions & 18 deletions apps/desktop/e2e/skill-draft-lifecycle.spec.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -8,11 +8,17 @@ import { expect, test, COMPOSER_INPUT } from './fixtures';
* here — begin edit, prepare the branch, fail the send, cancel back — moves it
* by moving the text. The point of these journeys is that nothing has to carry
* the Skill separately for that to hold.
*
* The Skill itself comes from the real catalog (the invocable-skills fixture
* plus the Skills module page), not a Desktop-only starter IPC.
*/
async function createStarterSkill(page: Page): Promise<void> {
const result = await page.evaluate(() => window.maka.skills.createStarter());
expect(result.ok).toBe(true);
await page.reload();
async function openInstalledWorkspaceSkill(page: Page): Promise<void> {
await page.getByRole('button', { name: '展开侧边栏' }).click();
const sidebar = page.getByRole('navigation', { name: '任务列表' });
await sidebar.getByRole('button', { name: '扩展' }).click();
await expect(page.locator('[data-module="skills"]')).toBeVisible();
await expect(page.getByText('Workspace Only', { exact: true })).toBeVisible();
await sidebar.getByRole('button', { name: '新任务', exact: true }).click();
await expect(page.locator(COMPOSER_INPUT)).toBeVisible();
}

Expand DownExpand Up@@ -41,9 +47,9 @@ async function beginRevision(page: Page): Promise<void> {
await expect(page.locator('[data-revision-notice="true"]')).toBeVisible();
}

async function failStarterSkillRevision(page: Page): Promise<void> {
async function failWorkspaceSkillRevision(page: Page): Promise<void> {
const disabled = await page.evaluate(() =>
window.maka.skills.setEnabled('starter-skill', false),
window.maka.skills.setEnabled('workspace-only', false),
);
expect(disabled.ok).toBe(true);

Expand All@@ -55,20 +61,20 @@ async function failStarterSkillRevision(page: Page): Promise<void> {
// composer draws chips from. A chip here would promise a Skill that no longer
// resolves — the text is the honest rendering, and re-enabling it below sends.
await expect(composer).toContainText('edited with skill');
await expect(composer).toContainText('/skill:starter-skill');
await expect(composer).toContainText('/skill:workspace-only');
}

test('a successful revision retry clears both child and source drafts', async ({
window: page,
invocableSkillsWindow: page,
}) => {
await createStarterSkill(page);
await openInstalledWorkspaceSkill(page);
await seedEditableTurn(page);
await beginRevision(page);
await composeWithSkill(page, 'edited with skill', /示例技能/);
await failStarterSkillRevision(page);
await composeWithSkill(page, 'edited with skill', /Workspace Only/);
await failWorkspaceSkillRevision(page);

const enabled = await page.evaluate(() =>
window.maka.skills.setEnabled('starter-skill', true),
window.maka.skills.setEnabled('workspace-only', true),
);
expect(enabled.ok).toBe(true);
await page.locator(COMPOSER_INPUT).press('Enter');
Expand All@@ -85,14 +91,14 @@ test('a successful revision retry clears both child and source drafts', async ({
test('cancelling a failed revision restores the complete pre-edit draft', async ({
invocableSkillsWindow: page,
}) => {
await createStarterSkill(page);
await openInstalledWorkspaceSkill(page);
await seedEditableTurn(page);

const composer = page.locator(COMPOSER_INPUT);
await composeWithSkill(page, 'previous unsent draft', /Workspace Only/);
await composeWithSkill(page, 'previous unsent draft', /Project Only/);
await beginRevision(page);
await composeWithSkill(page, 'edited with skill', /示例技能/);
await failStarterSkillRevision(page);
await composeWithSkill(page, 'edited with skill', /Workspace Only/);
await failWorkspaceSkillRevision(page);

await page.getByRole('button', { name: '取消' }).click();

Expand All@@ -102,6 +108,6 @@ test('cancelling a failed revision restores the complete pre-edit draft', async
// redraws it from that text, not because anything carried it separately.
await expect(composer).toContainText('previous unsent draft');
await expect(
page.locator('[data-astryx-token-value="/skill:workspace-only"]'),
).toContainText('Workspace Only');
page.locator('[data-astryx-token-value="/skill:project-only"]'),
).toContainText('Project Only');
});
10 changes: 8 additions & 2 deletions apps/desktop/e2e/slash-command-menu.spec.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -123,11 +123,17 @@ test('dispatches a staged slash command instead of steering it into a running tu
await expect(page.getByRole('button', { name: '插入消息' })).toBeVisible();
await composer.press('Enter');

await composer.fill('/');
// After the steering send the composer remounts. `fill('/')` can land
// before the contentEditable is focused, so the `/` trigger never
// populates. An empty command group then makes `/compact` look absent
// and the `/side` click waits out the timeout.
await composer.click();
await composer.pressSequentially('/');
const menu = page.getByRole('listbox', { name: '命令和技能' });
const commands = menu.getByRole('group', { name: '命令' });
await expect(commands.getByRole('option', { name: /\/compact/ })).toHaveCount(0);
const side = commands.getByRole('option', { name: /打开侧聊.*\/side/ });
await expect(side).toBeVisible();
await expect(commands.getByRole('option', { name: /\/compact/ })).toHaveCount(0);
await side.click();
await expect.poll(() => composer.textContent()).toBe('/side ');
await expect(page.locator('.maka-quote-workbar-panel')).toHaveCount(0);
Expand Down
3 changes: 0 additions & 3 deletions apps/desktop/src/main/__tests__/mcp-ipc-main.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -31,7 +31,6 @@ test('MCP IPC commits config before publishing capabilities and emitting status'
cancelConnect: () => { calls.push('cancel'); return true; },
sync: async () => { calls.push('sync'); },
statuses: () => [connected],
reconnect: async () => connected,
test: async () => ({ ok: true, status: connected, latencyMs: 1 }),
},
ensureReady: async () => { calls.push('ready'); },
Expand DownExpand Up@@ -106,7 +105,6 @@ test('MCP market cancellation waits for an in-flight config write before rolling
cancelConnect: () => { calls.push('cancel'); return true; },
sync: async () => { calls.push('sync'); },
statuses: () => [],
reconnect: async () => { throw new Error('not used'); },
test: async () => { throw new Error('not used'); },
},
ensureReady: async () => {},
Expand DownExpand Up@@ -157,7 +155,6 @@ test('MCP config commit is not rolled back by a capability publication failure',
cancelConnect: () => false,
sync: async () => {},
statuses: () => [],
reconnect: async () => { throw new Error('not used'); },
test: async () => { throw new Error('not used'); },
},
ensureReady: async () => {},
Expand Down
8 changes: 1 addition & 7 deletions apps/desktop/src/main/mcp-ipc-main.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -6,7 +6,7 @@ import type { McpConfigStore } from '@maka/storage';
export interface McpIpcMainDeps {
ipcMain: Pick<IpcMain, 'handle'>;
store: McpConfigStore;
manager: Pick<McpClientManager, 'sync' | 'statuses' | 'test' | 'reconnect' | 'cancelConnect'>;
manager: Pick<McpClientManager, 'sync' | 'statuses' | 'test' | 'cancelConnect'>;
ensureReady(): Promise<void>;
publishCapabilities(): Promise<void>;
onPublicationError(error: unknown): void;
Expand DownExpand Up@@ -81,12 +81,6 @@ export function registerMcpIpcMain(deps: McpIpcMainDeps): void {
deps.emitChanged(deps.manager.statuses());
return result;
});
deps.ipcMain.handle('mcp:reconnect', async (_event, serverId: string) => {
await deps.ensureReady();
const result = await deps.manager.reconnect(serverId);
changed(deps);
return result;
});
}

function changed(deps: McpIpcMainDeps): void {
Expand Down
3 changes: 0 additions & 3 deletions apps/desktop/src/main/onboarding-ipc-main.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -25,7 +25,4 @@ export function registerOnboardingIpc(deps: OnboardingIpcDeps): void {
// it as a typed reject rather than silently swallowing.
return deps.onboardingService.setMilestone(id, status);
});
target.handle('onboarding:clearMilestone', async (_event, id: unknown) => {
return deps.onboardingService.clearMilestone(id);
});
}
32 changes: 0 additions & 32 deletions apps/desktop/src/main/onboarding-service.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -68,7 +68,6 @@ export interface OnboardingServiceDeps {
id: OnboardingMilestoneId,
status: 'completed' | 'skipped',
): Promise<OnboardingMilestone[]>;
clearMilestone(id: OnboardingMilestoneId): Promise<OnboardingMilestone[]>;
/**
* Whether `connection` has a usable credential — an API key OR (for
* OAuth-subscription providers) a stored OAuth token. MUST be
Expand All@@ -84,7 +83,6 @@ export interface OnboardingService {
id: unknown,
status: unknown,
): Promise<OnboardingSnapshot>;
clearMilestone(id: unknown): Promise<OnboardingSnapshot>;
}

/**
Expand DownExpand Up@@ -173,36 +171,6 @@ export function createOnboardingService(deps: OnboardingServiceDeps): Onboarding
});
return buildSnapshot(state, milestones, sessions, connections, defaultSlug, secrets);
},

async clearMilestone(id: unknown): Promise<OnboardingSnapshot> {
if (typeof id !== 'string' || !isOnboardingMilestoneId(id)) {
throw new Error('INVALID_MILESTONE_ID');
}
const milestones = await deps.clearMilestone(id);
const [connections, defaultSlug, sessions] = await Promise.all([
deps.listConnections(),
deps.getDefaultSlug(),
deps.listSessions(),
]);
const secretEntries = await Promise.all(
connections.map(async (connection) => {
try {
return [connection.slug, await deps.hasCredential(connection)] as const;
} catch {
return [connection.slug, false] as const;
}
}),
);
const secrets: Record<string, boolean> = Object.fromEntries(secretEntries);
const logicalSessions = collapseSessionRevisions(sessions);
const state = deriveOnboardingState({
connections,
defaultSlug: defaultSlug ?? undefined,
sessions: logicalSessions,
secrets,
});
return buildSnapshot(state, milestones, sessions, connections, defaultSlug, secrets);
},
};
}

Expand Down
6 changes: 0 additions & 6 deletions apps/desktop/src/main/runtime-host-artifacts-ipc-main.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -42,12 +42,6 @@ export function registerRuntimeHostArtifactsIpc(
: artifacts.filter(({ status }) => status !== "deleted");
},
);
handleReconnectableRead(
deps.ipcMain,
"artifacts:get",
(_event, sessionId: string, artifactId: string) =>
deps.client.getArtifact(sessionId, artifactId),
);
handleReconnectableRead(
deps.ipcMain,
"artifacts:readText",
Expand Down
1 change: 0 additions & 1 deletion apps/desktop/src/main/runtime-host-boot.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -987,7 +987,6 @@ function registerHostClientIpc(
(await settingsStore.get()).onboarding.milestones,
upsertMilestone: (id, status) =>
settingsStore.upsertOnboardingMilestone(id, status),
clearMilestone: (id) => settingsStore.clearOnboardingMilestone(id),
hasCredential: (connection) =>
readWithFallback(async () => {
if (!providerAuthRequiresSecret(connection.providerType)) return true;
Expand Down
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Add copy buttons to all
 blocks
(function() {
function addCopyButtons() {
document.querySelectorAll('pre code').forEach(function(codeBlock) {
if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;
codeBlock.parentElement.setAttribute('data-copy-added', 'true');
var btn = document.createElement('button');
btn.textContent = 'Copy';
btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';
btn.onmouseover = function() { this.style.opacity = '1'; };
btn.onmouseout = function() { this.style.opacity = '0.7'; };
btn.onclick = function() {
navigator.clipboard.writeText(codeBlock.textContent).then(function() {
btn.textContent = 'Copied!';
setTimeout(function() { btn.textContent = 'Copy'; }, 1500);
});
};
codeBlock.parentElement.style.position = 'relative';
codeBlock.parentElement.appendChild(btn);
});
}
addCopyButtons();
// Re-run on dynamic content
var observer = new MutationObserver(addCopyButtons);
observer.observe(document.body, { childList: true, subtree: true });
})();
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
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
42 changes: 24 additions & 18 deletions apps/desktop/e2e/skill-draft-lifecycle.spec.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -8,11 +8,17 @@ import { expect, test, COMPOSER_INPUT } from './fixtures';
* here — begin edit, prepare the branch, fail the send, cancel back — moves it
* by moving the text. The point of these journeys is that nothing has to carry
* the Skill separately for that to hold.
*
* The Skill itself comes from the real catalog (the invocable-skills fixture
* plus the Skills module page), not a Desktop-only starter IPC.
*/
async function createStarterSkill(page: Page): Promise<void> {
const result = await page.evaluate(() => window.maka.skills.createStarter());
expect(result.ok).toBe(true);
await page.reload();
async function openInstalledWorkspaceSkill(page: Page): Promise<void> {
await page.getByRole('button', { name: '展开侧边栏' }).click();
const sidebar = page.getByRole('navigation', { name: '任务列表' });
await sidebar.getByRole('button', { name: '扩展' }).click();
await expect(page.locator('[data-module="skills"]')).toBeVisible();
await expect(page.getByText('Workspace Only', { exact: true })).toBeVisible();
await sidebar.getByRole('button', { name: '新任务', exact: true }).click();
await expect(page.locator(COMPOSER_INPUT)).toBeVisible();
}

Expand DownExpand Up@@ -41,9 +47,9 @@ async function beginRevision(page: Page): Promise<void> {
await expect(page.locator('[data-revision-notice="true"]')).toBeVisible();
}

async function failStarterSkillRevision(page: Page): Promise<void> {
async function failWorkspaceSkillRevision(page: Page): Promise<void> {
const disabled = await page.evaluate(() =>
window.maka.skills.setEnabled('starter-skill', false),
window.maka.skills.setEnabled('workspace-only', false),
);
expect(disabled.ok).toBe(true);

Expand All@@ -55,20 +61,20 @@ async function failStarterSkillRevision(page: Page): Promise<void> {
// composer draws chips from. A chip here would promise a Skill that no longer
// resolves — the text is the honest rendering, and re-enabling it below sends.
await expect(composer).toContainText('edited with skill');
await expect(composer).toContainText('/skill:starter-skill');
await expect(composer).toContainText('/skill:workspace-only');
}

test('a successful revision retry clears both child and source drafts', async ({
window: page,
invocableSkillsWindow: page,
}) => {
await createStarterSkill(page);
await openInstalledWorkspaceSkill(page);
await seedEditableTurn(page);
await beginRevision(page);
await composeWithSkill(page, 'edited with skill', /示例技能/);
await failStarterSkillRevision(page);
await composeWithSkill(page, 'edited with skill', /Workspace Only/);
await failWorkspaceSkillRevision(page);

const enabled = await page.evaluate(() =>
window.maka.skills.setEnabled('starter-skill', true),
window.maka.skills.setEnabled('workspace-only', true),
);
expect(enabled.ok).toBe(true);
await page.locator(COMPOSER_INPUT).press('Enter');
Expand All@@ -85,14 +91,14 @@ test('a successful revision retry clears both child and source drafts', async ({
test('cancelling a failed revision restores the complete pre-edit draft', async ({
invocableSkillsWindow: page,
}) => {
await createStarterSkill(page);
await openInstalledWorkspaceSkill(page);
await seedEditableTurn(page);

const composer = page.locator(COMPOSER_INPUT);
await composeWithSkill(page, 'previous unsent draft', /Workspace Only/);
await composeWithSkill(page, 'previous unsent draft', /Project Only/);
await beginRevision(page);
await composeWithSkill(page, 'edited with skill', /示例技能/);
await failStarterSkillRevision(page);
await composeWithSkill(page, 'edited with skill', /Workspace Only/);
await failWorkspaceSkillRevision(page);

await page.getByRole('button', { name: '取消' }).click();

Expand All@@ -102,6 +108,6 @@ test('cancelling a failed revision restores the complete pre-edit draft', async
// redraws it from that text, not because anything carried it separately.
await expect(composer).toContainText('previous unsent draft');
await expect(
page.locator('[data-astryx-token-value="/skill:workspace-only"]'),
).toContainText('Workspace Only');
page.locator('[data-astryx-token-value="/skill:project-only"]'),
).toContainText('Project Only');
});
10 changes: 8 additions & 2 deletions apps/desktop/e2e/slash-command-menu.spec.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -123,11 +123,17 @@ test('dispatches a staged slash command instead of steering it into a running tu
await expect(page.getByRole('button', { name: '插入消息' })).toBeVisible();
await composer.press('Enter');

await composer.fill('/');
// After the steering send the composer remounts. `fill('/')` can land
// before the contentEditable is focused, so the `/` trigger never
// populates. An empty command group then makes `/compact` look absent
// and the `/side` click waits out the timeout.
await composer.click();
await composer.pressSequentially('/');
const menu = page.getByRole('listbox', { name: '命令和技能' });
const commands = menu.getByRole('group', { name: '命令' });
await expect(commands.getByRole('option', { name: /\/compact/ })).toHaveCount(0);
const side = commands.getByRole('option', { name: /打开侧聊.*\/side/ });
await expect(side).toBeVisible();
await expect(commands.getByRole('option', { name: /\/compact/ })).toHaveCount(0);
await side.click();
await expect.poll(() => composer.textContent()).toBe('/side ');
await expect(page.locator('.maka-quote-workbar-panel')).toHaveCount(0);
Expand Down
3 changes: 0 additions & 3 deletions apps/desktop/src/main/__tests__/mcp-ipc-main.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -31,7 +31,6 @@ test('MCP IPC commits config before publishing capabilities and emitting status'
cancelConnect: () => { calls.push('cancel'); return true; },
sync: async () => { calls.push('sync'); },
statuses: () => [connected],
reconnect: async () => connected,
test: async () => ({ ok: true, status: connected, latencyMs: 1 }),
},
ensureReady: async () => { calls.push('ready'); },
Expand DownExpand Up@@ -106,7 +105,6 @@ test('MCP market cancellation waits for an in-flight config write before rolling
cancelConnect: () => { calls.push('cancel'); return true; },
sync: async () => { calls.push('sync'); },
statuses: () => [],
reconnect: async () => { throw new Error('not used'); },
test: async () => { throw new Error('not used'); },
},
ensureReady: async () => {},
Expand DownExpand Up@@ -157,7 +155,6 @@ test('MCP config commit is not rolled back by a capability publication failure',
cancelConnect: () => false,
sync: async () => {},
statuses: () => [],
reconnect: async () => { throw new Error('not used'); },
test: async () => { throw new Error('not used'); },
},
ensureReady: async () => {},
Expand Down
8 changes: 1 addition & 7 deletions apps/desktop/src/main/mcp-ipc-main.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -6,7 +6,7 @@ import type { McpConfigStore } from '@maka/storage';
export interface McpIpcMainDeps {
ipcMain: Pick<IpcMain, 'handle'>;
store: McpConfigStore;
manager: Pick<McpClientManager, 'sync' | 'statuses' | 'test' | 'reconnect' | 'cancelConnect'>;
manager: Pick<McpClientManager, 'sync' | 'statuses' | 'test' | 'cancelConnect'>;
ensureReady(): Promise<void>;
publishCapabilities(): Promise<void>;
onPublicationError(error: unknown): void;
Expand DownExpand Up@@ -81,12 +81,6 @@ export function registerMcpIpcMain(deps: McpIpcMainDeps): void {
deps.emitChanged(deps.manager.statuses());
return result;
});
deps.ipcMain.handle('mcp:reconnect', async (_event, serverId: string) => {
await deps.ensureReady();
const result = await deps.manager.reconnect(serverId);
changed(deps);
return result;
});
}

function changed(deps: McpIpcMainDeps): void {
Expand Down
3 changes: 0 additions & 3 deletions apps/desktop/src/main/onboarding-ipc-main.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -25,7 +25,4 @@ export function registerOnboardingIpc(deps: OnboardingIpcDeps): void {
// it as a typed reject rather than silently swallowing.
return deps.onboardingService.setMilestone(id, status);
});
target.handle('onboarding:clearMilestone', async (_event, id: unknown) => {
return deps.onboardingService.clearMilestone(id);
});
}
32 changes: 0 additions & 32 deletions apps/desktop/src/main/onboarding-service.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -68,7 +68,6 @@ export interface OnboardingServiceDeps {
id: OnboardingMilestoneId,
status: 'completed' | 'skipped',
): Promise<OnboardingMilestone[]>;
clearMilestone(id: OnboardingMilestoneId): Promise<OnboardingMilestone[]>;
/**
* Whether `connection` has a usable credential — an API key OR (for
* OAuth-subscription providers) a stored OAuth token. MUST be
Expand All@@ -84,7 +83,6 @@ export interface OnboardingService {
id: unknown,
status: unknown,
): Promise<OnboardingSnapshot>;
clearMilestone(id: unknown): Promise<OnboardingSnapshot>;
}

/**
Expand DownExpand Up@@ -173,36 +171,6 @@ export function createOnboardingService(deps: OnboardingServiceDeps): Onboarding
});
return buildSnapshot(state, milestones, sessions, connections, defaultSlug, secrets);
},

async clearMilestone(id: unknown): Promise<OnboardingSnapshot> {
if (typeof id !== 'string' || !isOnboardingMilestoneId(id)) {
throw new Error('INVALID_MILESTONE_ID');
}
const milestones = await deps.clearMilestone(id);
const [connections, defaultSlug, sessions] = await Promise.all([
deps.listConnections(),
deps.getDefaultSlug(),
deps.listSessions(),
]);
const secretEntries = await Promise.all(
connections.map(async (connection) => {
try {
return [connection.slug, await deps.hasCredential(connection)] as const;
} catch {
return [connection.slug, false] as const;
}
}),
);
const secrets: Record<string, boolean> = Object.fromEntries(secretEntries);
const logicalSessions = collapseSessionRevisions(sessions);
const state = deriveOnboardingState({
connections,
defaultSlug: defaultSlug ?? undefined,
sessions: logicalSessions,
secrets,
});
return buildSnapshot(state, milestones, sessions, connections, defaultSlug, secrets);
},
};
}

Expand Down
6 changes: 0 additions & 6 deletions apps/desktop/src/main/runtime-host-artifacts-ipc-main.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -42,12 +42,6 @@ export function registerRuntimeHostArtifactsIpc(
: artifacts.filter(({ status }) => status !== "deleted");
},
);
handleReconnectableRead(
deps.ipcMain,
"artifacts:get",
(_event, sessionId: string, artifactId: string) =>
deps.client.getArtifact(sessionId, artifactId),
);
handleReconnectableRead(
deps.ipcMain,
"artifacts:readText",
Expand Down
1 change: 0 additions & 1 deletion apps/desktop/src/main/runtime-host-boot.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -987,7 +987,6 @@ function registerHostClientIpc(
(await settingsStore.get()).onboarding.milestones,
upsertMilestone: (id, status) =>
settingsStore.upsertOnboardingMilestone(id, status),
clearMilestone: (id) => settingsStore.clearOnboardingMilestone(id),
hasCredential: (connection) =>
readWithFallback(async () => {
if (!providerAuthRequiresSecret(connection.providerType)) return true;
Expand Down
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Force GitHub README to respect dark mode (function() { var style = document.createElement('style'); style.textContent = ' .markdown-body { color-scheme: dark light; } .markdown-body pre { background: #161b22 !important; } .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; } .markdown-body table th, .markdown-body table td { border-color: #30363d !important; } .markdown-body img { background: #0d1117; } .markdown-body blockquote { border-left-color: #8b949e; } .markdown-body hr { border-color: #30363d; } '; document.head.appendChild(style); })(); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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
42 changes: 24 additions & 18 deletions apps/desktop/e2e/skill-draft-lifecycle.spec.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -8,11 +8,17 @@ import { expect, test, COMPOSER_INPUT } from './fixtures';
* here — begin edit, prepare the branch, fail the send, cancel back — moves it
* by moving the text. The point of these journeys is that nothing has to carry
* the Skill separately for that to hold.
*
* The Skill itself comes from the real catalog (the invocable-skills fixture
* plus the Skills module page), not a Desktop-only starter IPC.
*/
async function createStarterSkill(page: Page): Promise<void> {
const result = await page.evaluate(() => window.maka.skills.createStarter());
expect(result.ok).toBe(true);
await page.reload();
async function openInstalledWorkspaceSkill(page: Page): Promise<void> {
await page.getByRole('button', { name: '展开侧边栏' }).click();
const sidebar = page.getByRole('navigation', { name: '任务列表' });
await sidebar.getByRole('button', { name: '扩展' }).click();
await expect(page.locator('[data-module="skills"]')).toBeVisible();
await expect(page.getByText('Workspace Only', { exact: true })).toBeVisible();
await sidebar.getByRole('button', { name: '新任务', exact: true }).click();
await expect(page.locator(COMPOSER_INPUT)).toBeVisible();
}

Expand DownExpand Up@@ -41,9 +47,9 @@ async function beginRevision(page: Page): Promise<void> {
await expect(page.locator('[data-revision-notice="true"]')).toBeVisible();
}

async function failStarterSkillRevision(page: Page): Promise<void> {
async function failWorkspaceSkillRevision(page: Page): Promise<void> {
const disabled = await page.evaluate(() =>
window.maka.skills.setEnabled('starter-skill', false),
window.maka.skills.setEnabled('workspace-only', false),
);
expect(disabled.ok).toBe(true);

Expand All@@ -55,20 +61,20 @@ async function failStarterSkillRevision(page: Page): Promise<void> {
// composer draws chips from. A chip here would promise a Skill that no longer
// resolves — the text is the honest rendering, and re-enabling it below sends.
await expect(composer).toContainText('edited with skill');
await expect(composer).toContainText('/skill:starter-skill');
await expect(composer).toContainText('/skill:workspace-only');
}

test('a successful revision retry clears both child and source drafts', async ({
window: page,
invocableSkillsWindow: page,
}) => {
await createStarterSkill(page);
await openInstalledWorkspaceSkill(page);
await seedEditableTurn(page);
await beginRevision(page);
await composeWithSkill(page, 'edited with skill', /示例技能/);
await failStarterSkillRevision(page);
await composeWithSkill(page, 'edited with skill', /Workspace Only/);
await failWorkspaceSkillRevision(page);

const enabled = await page.evaluate(() =>
window.maka.skills.setEnabled('starter-skill', true),
window.maka.skills.setEnabled('workspace-only', true),
);
expect(enabled.ok).toBe(true);
await page.locator(COMPOSER_INPUT).press('Enter');
Expand All@@ -85,14 +91,14 @@ test('a successful revision retry clears both child and source drafts', async ({
test('cancelling a failed revision restores the complete pre-edit draft', async ({
invocableSkillsWindow: page,
}) => {
await createStarterSkill(page);
await openInstalledWorkspaceSkill(page);
await seedEditableTurn(page);

const composer = page.locator(COMPOSER_INPUT);
await composeWithSkill(page, 'previous unsent draft', /Workspace Only/);
await composeWithSkill(page, 'previous unsent draft', /Project Only/);
await beginRevision(page);
await composeWithSkill(page, 'edited with skill', /示例技能/);
await failStarterSkillRevision(page);
await composeWithSkill(page, 'edited with skill', /Workspace Only/);
await failWorkspaceSkillRevision(page);

await page.getByRole('button', { name: '取消' }).click();

Expand All@@ -102,6 +108,6 @@ test('cancelling a failed revision restores the complete pre-edit draft', async
// redraws it from that text, not because anything carried it separately.
await expect(composer).toContainText('previous unsent draft');
await expect(
page.locator('[data-astryx-token-value="/skill:workspace-only"]'),
).toContainText('Workspace Only');
page.locator('[data-astryx-token-value="/skill:project-only"]'),
).toContainText('Project Only');
});
10 changes: 8 additions & 2 deletions apps/desktop/e2e/slash-command-menu.spec.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -123,11 +123,17 @@ test('dispatches a staged slash command instead of steering it into a running tu
await expect(page.getByRole('button', { name: '插入消息' })).toBeVisible();
await composer.press('Enter');

await composer.fill('/');
// After the steering send the composer remounts. `fill('/')` can land
// before the contentEditable is focused, so the `/` trigger never
// populates. An empty command group then makes `/compact` look absent
// and the `/side` click waits out the timeout.
await composer.click();
await composer.pressSequentially('/');
const menu = page.getByRole('listbox', { name: '命令和技能' });
const commands = menu.getByRole('group', { name: '命令' });
await expect(commands.getByRole('option', { name: /\/compact/ })).toHaveCount(0);
const side = commands.getByRole('option', { name: /打开侧聊.*\/side/ });
await expect(side).toBeVisible();
await expect(commands.getByRole('option', { name: /\/compact/ })).toHaveCount(0);
await side.click();
await expect.poll(() => composer.textContent()).toBe('/side ');
await expect(page.locator('.maka-quote-workbar-panel')).toHaveCount(0);
Expand Down
3 changes: 0 additions & 3 deletions apps/desktop/src/main/__tests__/mcp-ipc-main.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -31,7 +31,6 @@ test('MCP IPC commits config before publishing capabilities and emitting status'
cancelConnect: () => { calls.push('cancel'); return true; },
sync: async () => { calls.push('sync'); },
statuses: () => [connected],
reconnect: async () => connected,
test: async () => ({ ok: true, status: connected, latencyMs: 1 }),
},
ensureReady: async () => { calls.push('ready'); },
Expand DownExpand Up@@ -106,7 +105,6 @@ test('MCP market cancellation waits for an in-flight config write before rolling
cancelConnect: () => { calls.push('cancel'); return true; },
sync: async () => { calls.push('sync'); },
statuses: () => [],
reconnect: async () => { throw new Error('not used'); },
test: async () => { throw new Error('not used'); },
},
ensureReady: async () => {},
Expand DownExpand Up@@ -157,7 +155,6 @@ test('MCP config commit is not rolled back by a capability publication failure',
cancelConnect: () => false,
sync: async () => {},
statuses: () => [],
reconnect: async () => { throw new Error('not used'); },
test: async () => { throw new Error('not used'); },
},
ensureReady: async () => {},
Expand Down
8 changes: 1 addition & 7 deletions apps/desktop/src/main/mcp-ipc-main.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -6,7 +6,7 @@ import type { McpConfigStore } from '@maka/storage';
export interface McpIpcMainDeps {
ipcMain: Pick<IpcMain, 'handle'>;
store: McpConfigStore;
manager: Pick<McpClientManager, 'sync' | 'statuses' | 'test' | 'reconnect' | 'cancelConnect'>;
manager: Pick<McpClientManager, 'sync' | 'statuses' | 'test' | 'cancelConnect'>;
ensureReady(): Promise<void>;
publishCapabilities(): Promise<void>;
onPublicationError(error: unknown): void;
Expand DownExpand Up@@ -81,12 +81,6 @@ export function registerMcpIpcMain(deps: McpIpcMainDeps): void {
deps.emitChanged(deps.manager.statuses());
return result;
});
deps.ipcMain.handle('mcp:reconnect', async (_event, serverId: string) => {
await deps.ensureReady();
const result = await deps.manager.reconnect(serverId);
changed(deps);
return result;
});
}

function changed(deps: McpIpcMainDeps): void {
Expand Down
3 changes: 0 additions & 3 deletions apps/desktop/src/main/onboarding-ipc-main.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -25,7 +25,4 @@ export function registerOnboardingIpc(deps: OnboardingIpcDeps): void {
// it as a typed reject rather than silently swallowing.
return deps.onboardingService.setMilestone(id, status);
});
target.handle('onboarding:clearMilestone', async (_event, id: unknown) => {
return deps.onboardingService.clearMilestone(id);
});
}
32 changes: 0 additions & 32 deletions apps/desktop/src/main/onboarding-service.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -68,7 +68,6 @@ export interface OnboardingServiceDeps {
id: OnboardingMilestoneId,
status: 'completed' | 'skipped',
): Promise<OnboardingMilestone[]>;
clearMilestone(id: OnboardingMilestoneId): Promise<OnboardingMilestone[]>;
/**
* Whether `connection` has a usable credential — an API key OR (for
* OAuth-subscription providers) a stored OAuth token. MUST be
Expand All@@ -84,7 +83,6 @@ export interface OnboardingService {
id: unknown,
status: unknown,
): Promise<OnboardingSnapshot>;
clearMilestone(id: unknown): Promise<OnboardingSnapshot>;
}

/**
Expand DownExpand Up@@ -173,36 +171,6 @@ export function createOnboardingService(deps: OnboardingServiceDeps): Onboarding
});
return buildSnapshot(state, milestones, sessions, connections, defaultSlug, secrets);
},

async clearMilestone(id: unknown): Promise<OnboardingSnapshot> {
if (typeof id !== 'string' || !isOnboardingMilestoneId(id)) {
throw new Error('INVALID_MILESTONE_ID');
}
const milestones = await deps.clearMilestone(id);
const [connections, defaultSlug, sessions] = await Promise.all([
deps.listConnections(),
deps.getDefaultSlug(),
deps.listSessions(),
]);
const secretEntries = await Promise.all(
connections.map(async (connection) => {
try {
return [connection.slug, await deps.hasCredential(connection)] as const;
} catch {
return [connection.slug, false] as const;
}
}),
);
const secrets: Record<string, boolean> = Object.fromEntries(secretEntries);
const logicalSessions = collapseSessionRevisions(sessions);
const state = deriveOnboardingState({
connections,
defaultSlug: defaultSlug ?? undefined,
sessions: logicalSessions,
secrets,
});
return buildSnapshot(state, milestones, sessions, connections, defaultSlug, secrets);
},
};
}

Expand Down
6 changes: 0 additions & 6 deletions apps/desktop/src/main/runtime-host-artifacts-ipc-main.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -42,12 +42,6 @@ export function registerRuntimeHostArtifactsIpc(
: artifacts.filter(({ status }) => status !== "deleted");
},
);
handleReconnectableRead(
deps.ipcMain,
"artifacts:get",
(_event, sessionId: string, artifactId: string) =>
deps.client.getArtifact(sessionId, artifactId),
);
handleReconnectableRead(
deps.ipcMain,
"artifacts:readText",
Expand Down
1 change: 0 additions & 1 deletion apps/desktop/src/main/runtime-host-boot.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -987,7 +987,6 @@ function registerHostClientIpc(
(await settingsStore.get()).onboarding.milestones,
upsertMilestone: (id, status) =>
settingsStore.upsertOnboardingMilestone(id, status),
clearMilestone: (id) => settingsStore.clearOnboardingMilestone(id),
hasCredential: (connection) =>
readWithFallback(async () => {
if (!providerAuthRequiresSecret(connection.providerType)) return true;
Expand Down
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Highlight search terms from Google/DuckDuckGo/Bing referrer (function() { var ref = document.referrer; var terms = []; if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) { var url = new URL(ref); var q = url.searchParams.get('q') || url.searchParams.get('p'); if (q) { terms = q.split(/\s+/).filter(function(t) { return t.length > 2; }); } } if (terms.length === 0) return; var style = document.createElement('style'); style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }'; document.head.appendChild(style); function highlight(node) { if (node.nodeType === 3) { // text node var text = node.textContent; var found = false; terms.forEach(function(term) { var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\]\\]/g, '\\') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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
42 changes: 24 additions & 18 deletions apps/desktop/e2e/skill-draft-lifecycle.spec.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -8,11 +8,17 @@ import { expect, test, COMPOSER_INPUT } from './fixtures';
* here — begin edit, prepare the branch, fail the send, cancel back — moves it
* by moving the text. The point of these journeys is that nothing has to carry
* the Skill separately for that to hold.
*
* The Skill itself comes from the real catalog (the invocable-skills fixture
* plus the Skills module page), not a Desktop-only starter IPC.
*/
async function createStarterSkill(page: Page): Promise<void> {
const result = await page.evaluate(() => window.maka.skills.createStarter());
expect(result.ok).toBe(true);
await page.reload();
async function openInstalledWorkspaceSkill(page: Page): Promise<void> {
await page.getByRole('button', { name: '展开侧边栏' }).click();
const sidebar = page.getByRole('navigation', { name: '任务列表' });
await sidebar.getByRole('button', { name: '扩展' }).click();
await expect(page.locator('[data-module="skills"]')).toBeVisible();
await expect(page.getByText('Workspace Only', { exact: true })).toBeVisible();
await sidebar.getByRole('button', { name: '新任务', exact: true }).click();
await expect(page.locator(COMPOSER_INPUT)).toBeVisible();
}

Expand DownExpand Up@@ -41,9 +47,9 @@ async function beginRevision(page: Page): Promise<void> {
await expect(page.locator('[data-revision-notice="true"]')).toBeVisible();
}

async function failStarterSkillRevision(page: Page): Promise<void> {
async function failWorkspaceSkillRevision(page: Page): Promise<void> {
const disabled = await page.evaluate(() =>
window.maka.skills.setEnabled('starter-skill', false),
window.maka.skills.setEnabled('workspace-only', false),
);
expect(disabled.ok).toBe(true);

Expand All@@ -55,20 +61,20 @@ async function failStarterSkillRevision(page: Page): Promise<void> {
// composer draws chips from. A chip here would promise a Skill that no longer
// resolves — the text is the honest rendering, and re-enabling it below sends.
await expect(composer).toContainText('edited with skill');
await expect(composer).toContainText('/skill:starter-skill');
await expect(composer).toContainText('/skill:workspace-only');
}

test('a successful revision retry clears both child and source drafts', async ({
window: page,
invocableSkillsWindow: page,
}) => {
await createStarterSkill(page);
await openInstalledWorkspaceSkill(page);
await seedEditableTurn(page);
await beginRevision(page);
await composeWithSkill(page, 'edited with skill', /示例技能/);
await failStarterSkillRevision(page);
await composeWithSkill(page, 'edited with skill', /Workspace Only/);
await failWorkspaceSkillRevision(page);

const enabled = await page.evaluate(() =>
window.maka.skills.setEnabled('starter-skill', true),
window.maka.skills.setEnabled('workspace-only', true),
);
expect(enabled.ok).toBe(true);
await page.locator(COMPOSER_INPUT).press('Enter');
Expand All@@ -85,14 +91,14 @@ test('a successful revision retry clears both child and source drafts', async ({
test('cancelling a failed revision restores the complete pre-edit draft', async ({
invocableSkillsWindow: page,
}) => {
await createStarterSkill(page);
await openInstalledWorkspaceSkill(page);
await seedEditableTurn(page);

const composer = page.locator(COMPOSER_INPUT);
await composeWithSkill(page, 'previous unsent draft', /Workspace Only/);
await composeWithSkill(page, 'previous unsent draft', /Project Only/);
await beginRevision(page);
await composeWithSkill(page, 'edited with skill', /示例技能/);
await failStarterSkillRevision(page);
await composeWithSkill(page, 'edited with skill', /Workspace Only/);
await failWorkspaceSkillRevision(page);

await page.getByRole('button', { name: '取消' }).click();

Expand All@@ -102,6 +108,6 @@ test('cancelling a failed revision restores the complete pre-edit draft', async
// redraws it from that text, not because anything carried it separately.
await expect(composer).toContainText('previous unsent draft');
await expect(
page.locator('[data-astryx-token-value="/skill:workspace-only"]'),
).toContainText('Workspace Only');
page.locator('[data-astryx-token-value="/skill:project-only"]'),
).toContainText('Project Only');
});
10 changes: 8 additions & 2 deletions apps/desktop/e2e/slash-command-menu.spec.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -123,11 +123,17 @@ test('dispatches a staged slash command instead of steering it into a running tu
await expect(page.getByRole('button', { name: '插入消息' })).toBeVisible();
await composer.press('Enter');

await composer.fill('/');
// After the steering send the composer remounts. `fill('/')` can land
// before the contentEditable is focused, so the `/` trigger never
// populates. An empty command group then makes `/compact` look absent
// and the `/side` click waits out the timeout.
await composer.click();
await composer.pressSequentially('/');
const menu = page.getByRole('listbox', { name: '命令和技能' });
const commands = menu.getByRole('group', { name: '命令' });
await expect(commands.getByRole('option', { name: /\/compact/ })).toHaveCount(0);
const side = commands.getByRole('option', { name: /打开侧聊.*\/side/ });
await expect(side).toBeVisible();
await expect(commands.getByRole('option', { name: /\/compact/ })).toHaveCount(0);
await side.click();
await expect.poll(() => composer.textContent()).toBe('/side ');
await expect(page.locator('.maka-quote-workbar-panel')).toHaveCount(0);
Expand Down
3 changes: 0 additions & 3 deletions apps/desktop/src/main/__tests__/mcp-ipc-main.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -31,7 +31,6 @@ test('MCP IPC commits config before publishing capabilities and emitting status'
cancelConnect: () => { calls.push('cancel'); return true; },
sync: async () => { calls.push('sync'); },
statuses: () => [connected],
reconnect: async () => connected,
test: async () => ({ ok: true, status: connected, latencyMs: 1 }),
},
ensureReady: async () => { calls.push('ready'); },
Expand DownExpand Up@@ -106,7 +105,6 @@ test('MCP market cancellation waits for an in-flight config write before rolling
cancelConnect: () => { calls.push('cancel'); return true; },
sync: async () => { calls.push('sync'); },
statuses: () => [],
reconnect: async () => { throw new Error('not used'); },
test: async () => { throw new Error('not used'); },
},
ensureReady: async () => {},
Expand DownExpand Up@@ -157,7 +155,6 @@ test('MCP config commit is not rolled back by a capability publication failure',
cancelConnect: () => false,
sync: async () => {},
statuses: () => [],
reconnect: async () => { throw new Error('not used'); },
test: async () => { throw new Error('not used'); },
},
ensureReady: async () => {},
Expand Down
8 changes: 1 addition & 7 deletions apps/desktop/src/main/mcp-ipc-main.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -6,7 +6,7 @@ import type { McpConfigStore } from '@maka/storage';
export interface McpIpcMainDeps {
ipcMain: Pick<IpcMain, 'handle'>;
store: McpConfigStore;
manager: Pick<McpClientManager, 'sync' | 'statuses' | 'test' | 'reconnect' | 'cancelConnect'>;
manager: Pick<McpClientManager, 'sync' | 'statuses' | 'test' | 'cancelConnect'>;
ensureReady(): Promise<void>;
publishCapabilities(): Promise<void>;
onPublicationError(error: unknown): void;
Expand DownExpand Up@@ -81,12 +81,6 @@ export function registerMcpIpcMain(deps: McpIpcMainDeps): void {
deps.emitChanged(deps.manager.statuses());
return result;
});
deps.ipcMain.handle('mcp:reconnect', async (_event, serverId: string) => {
await deps.ensureReady();
const result = await deps.manager.reconnect(serverId);
changed(deps);
return result;
});
}

function changed(deps: McpIpcMainDeps): void {
Expand Down
3 changes: 0 additions & 3 deletions apps/desktop/src/main/onboarding-ipc-main.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -25,7 +25,4 @@ export function registerOnboardingIpc(deps: OnboardingIpcDeps): void {
// it as a typed reject rather than silently swallowing.
return deps.onboardingService.setMilestone(id, status);
});
target.handle('onboarding:clearMilestone', async (_event, id: unknown) => {
return deps.onboardingService.clearMilestone(id);
});
}
32 changes: 0 additions & 32 deletions apps/desktop/src/main/onboarding-service.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -68,7 +68,6 @@ export interface OnboardingServiceDeps {
id: OnboardingMilestoneId,
status: 'completed' | 'skipped',
): Promise<OnboardingMilestone[]>;
clearMilestone(id: OnboardingMilestoneId): Promise<OnboardingMilestone[]>;
/**
* Whether `connection` has a usable credential — an API key OR (for
* OAuth-subscription providers) a stored OAuth token. MUST be
Expand All@@ -84,7 +83,6 @@ export interface OnboardingService {
id: unknown,
status: unknown,
): Promise<OnboardingSnapshot>;
clearMilestone(id: unknown): Promise<OnboardingSnapshot>;
}

/**
Expand DownExpand Up@@ -173,36 +171,6 @@ export function createOnboardingService(deps: OnboardingServiceDeps): Onboarding
});
return buildSnapshot(state, milestones, sessions, connections, defaultSlug, secrets);
},

async clearMilestone(id: unknown): Promise<OnboardingSnapshot> {
if (typeof id !== 'string' || !isOnboardingMilestoneId(id)) {
throw new Error('INVALID_MILESTONE_ID');
}
const milestones = await deps.clearMilestone(id);
const [connections, defaultSlug, sessions] = await Promise.all([
deps.listConnections(),
deps.getDefaultSlug(),
deps.listSessions(),
]);
const secretEntries = await Promise.all(
connections.map(async (connection) => {
try {
return [connection.slug, await deps.hasCredential(connection)] as const;
} catch {
return [connection.slug, false] as const;
}
}),
);
const secrets: Record<string, boolean> = Object.fromEntries(secretEntries);
const logicalSessions = collapseSessionRevisions(sessions);
const state = deriveOnboardingState({
connections,
defaultSlug: defaultSlug ?? undefined,
sessions: logicalSessions,
secrets,
});
return buildSnapshot(state, milestones, sessions, connections, defaultSlug, secrets);
},
};
}

Expand Down
6 changes: 0 additions & 6 deletions apps/desktop/src/main/runtime-host-artifacts-ipc-main.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -42,12 +42,6 @@ export function registerRuntimeHostArtifactsIpc(
: artifacts.filter(({ status }) => status !== "deleted");
},
);
handleReconnectableRead(
deps.ipcMain,
"artifacts:get",
(_event, sessionId: string, artifactId: string) =>
deps.client.getArtifact(sessionId, artifactId),
);
handleReconnectableRead(
deps.ipcMain,
"artifacts:readText",
Expand Down
1 change: 0 additions & 1 deletion apps/desktop/src/main/runtime-host-boot.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -987,7 +987,6 @@ function registerHostClientIpc(
(await settingsStore.get()).onboarding.milestones,
upsertMilestone: (id, status) =>
settingsStore.upsertOnboardingMilestone(id, status),
clearMilestone: (id) => settingsStore.clearOnboardingMilestone(id),
hasCredential: (connection) =>
readWithFallback(async () => {
if (!providerAuthRequiresSecret(connection.providerType)) return true;
Expand Down
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
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
42 changes: 24 additions & 18 deletions apps/desktop/e2e/skill-draft-lifecycle.spec.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -8,11 +8,17 @@ import { expect, test, COMPOSER_INPUT } from './fixtures';
* here — begin edit, prepare the branch, fail the send, cancel back — moves it
* by moving the text. The point of these journeys is that nothing has to carry
* the Skill separately for that to hold.
*
* The Skill itself comes from the real catalog (the invocable-skills fixture
* plus the Skills module page), not a Desktop-only starter IPC.
*/
async function createStarterSkill(page: Page): Promise<void> {
const result = await page.evaluate(() => window.maka.skills.createStarter());
expect(result.ok).toBe(true);
await page.reload();
async function openInstalledWorkspaceSkill(page: Page): Promise<void> {
await page.getByRole('button', { name: '展开侧边栏' }).click();
const sidebar = page.getByRole('navigation', { name: '任务列表' });
await sidebar.getByRole('button', { name: '扩展' }).click();
await expect(page.locator('[data-module="skills"]')).toBeVisible();
await expect(page.getByText('Workspace Only', { exact: true })).toBeVisible();
await sidebar.getByRole('button', { name: '新任务', exact: true }).click();
await expect(page.locator(COMPOSER_INPUT)).toBeVisible();
}

Expand DownExpand Up@@ -41,9 +47,9 @@ async function beginRevision(page: Page): Promise<void> {
await expect(page.locator('[data-revision-notice="true"]')).toBeVisible();
}

async function failStarterSkillRevision(page: Page): Promise<void> {
async function failWorkspaceSkillRevision(page: Page): Promise<void> {
const disabled = await page.evaluate(() =>
window.maka.skills.setEnabled('starter-skill', false),
window.maka.skills.setEnabled('workspace-only', false),
);
expect(disabled.ok).toBe(true);

Expand All@@ -55,20 +61,20 @@ async function failStarterSkillRevision(page: Page): Promise<void> {
// composer draws chips from. A chip here would promise a Skill that no longer
// resolves — the text is the honest rendering, and re-enabling it below sends.
await expect(composer).toContainText('edited with skill');
await expect(composer).toContainText('/skill:starter-skill');
await expect(composer).toContainText('/skill:workspace-only');
}

test('a successful revision retry clears both child and source drafts', async ({
window: page,
invocableSkillsWindow: page,
}) => {
await createStarterSkill(page);
await openInstalledWorkspaceSkill(page);
await seedEditableTurn(page);
await beginRevision(page);
await composeWithSkill(page, 'edited with skill', /示例技能/);
await failStarterSkillRevision(page);
await composeWithSkill(page, 'edited with skill', /Workspace Only/);
await failWorkspaceSkillRevision(page);

const enabled = await page.evaluate(() =>
window.maka.skills.setEnabled('starter-skill', true),
window.maka.skills.setEnabled('workspace-only', true),
);
expect(enabled.ok).toBe(true);
await page.locator(COMPOSER_INPUT).press('Enter');
Expand All@@ -85,14 +91,14 @@ test('a successful revision retry clears both child and source drafts', async ({
test('cancelling a failed revision restores the complete pre-edit draft', async ({
invocableSkillsWindow: page,
}) => {
await createStarterSkill(page);
await openInstalledWorkspaceSkill(page);
await seedEditableTurn(page);

const composer = page.locator(COMPOSER_INPUT);
await composeWithSkill(page, 'previous unsent draft', /Workspace Only/);
await composeWithSkill(page, 'previous unsent draft', /Project Only/);
await beginRevision(page);
await composeWithSkill(page, 'edited with skill', /示例技能/);
await failStarterSkillRevision(page);
await composeWithSkill(page, 'edited with skill', /Workspace Only/);
await failWorkspaceSkillRevision(page);

await page.getByRole('button', { name: '取消' }).click();

Expand All@@ -102,6 +108,6 @@ test('cancelling a failed revision restores the complete pre-edit draft', async
// redraws it from that text, not because anything carried it separately.
await expect(composer).toContainText('previous unsent draft');
await expect(
page.locator('[data-astryx-token-value="/skill:workspace-only"]'),
).toContainText('Workspace Only');
page.locator('[data-astryx-token-value="/skill:project-only"]'),
).toContainText('Project Only');
});
10 changes: 8 additions & 2 deletions apps/desktop/e2e/slash-command-menu.spec.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -123,11 +123,17 @@ test('dispatches a staged slash command instead of steering it into a running tu
await expect(page.getByRole('button', { name: '插入消息' })).toBeVisible();
await composer.press('Enter');

await composer.fill('/');
// After the steering send the composer remounts. `fill('/')` can land
// before the contentEditable is focused, so the `/` trigger never
// populates. An empty command group then makes `/compact` look absent
// and the `/side` click waits out the timeout.
await composer.click();
await composer.pressSequentially('/');
const menu = page.getByRole('listbox', { name: '命令和技能' });
const commands = menu.getByRole('group', { name: '命令' });
await expect(commands.getByRole('option', { name: /\/compact/ })).toHaveCount(0);
const side = commands.getByRole('option', { name: /打开侧聊.*\/side/ });
await expect(side).toBeVisible();
await expect(commands.getByRole('option', { name: /\/compact/ })).toHaveCount(0);
await side.click();
await expect.poll(() => composer.textContent()).toBe('/side ');
await expect(page.locator('.maka-quote-workbar-panel')).toHaveCount(0);
Expand Down
3 changes: 0 additions & 3 deletions apps/desktop/src/main/__tests__/mcp-ipc-main.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -31,7 +31,6 @@ test('MCP IPC commits config before publishing capabilities and emitting status'
cancelConnect: () => { calls.push('cancel'); return true; },
sync: async () => { calls.push('sync'); },
statuses: () => [connected],
reconnect: async () => connected,
test: async () => ({ ok: true, status: connected, latencyMs: 1 }),
},
ensureReady: async () => { calls.push('ready'); },
Expand DownExpand Up@@ -106,7 +105,6 @@ test('MCP market cancellation waits for an in-flight config write before rolling
cancelConnect: () => { calls.push('cancel'); return true; },
sync: async () => { calls.push('sync'); },
statuses: () => [],
reconnect: async () => { throw new Error('not used'); },
test: async () => { throw new Error('not used'); },
},
ensureReady: async () => {},
Expand DownExpand Up@@ -157,7 +155,6 @@ test('MCP config commit is not rolled back by a capability publication failure',
cancelConnect: () => false,
sync: async () => {},
statuses: () => [],
reconnect: async () => { throw new Error('not used'); },
test: async () => { throw new Error('not used'); },
},
ensureReady: async () => {},
Expand Down
8 changes: 1 addition & 7 deletions apps/desktop/src/main/mcp-ipc-main.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -6,7 +6,7 @@ import type { McpConfigStore } from '@maka/storage';
export interface McpIpcMainDeps {
ipcMain: Pick<IpcMain, 'handle'>;
store: McpConfigStore;
manager: Pick<McpClientManager, 'sync' | 'statuses' | 'test' | 'reconnect' | 'cancelConnect'>;
manager: Pick<McpClientManager, 'sync' | 'statuses' | 'test' | 'cancelConnect'>;
ensureReady(): Promise<void>;
publishCapabilities(): Promise<void>;
onPublicationError(error: unknown): void;
Expand DownExpand Up@@ -81,12 +81,6 @@ export function registerMcpIpcMain(deps: McpIpcMainDeps): void {
deps.emitChanged(deps.manager.statuses());
return result;
});
deps.ipcMain.handle('mcp:reconnect', async (_event, serverId: string) => {
await deps.ensureReady();
const result = await deps.manager.reconnect(serverId);
changed(deps);
return result;
});
}

function changed(deps: McpIpcMainDeps): void {
Expand Down
3 changes: 0 additions & 3 deletions apps/desktop/src/main/onboarding-ipc-main.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -25,7 +25,4 @@ export function registerOnboardingIpc(deps: OnboardingIpcDeps): void {
// it as a typed reject rather than silently swallowing.
return deps.onboardingService.setMilestone(id, status);
});
target.handle('onboarding:clearMilestone', async (_event, id: unknown) => {
return deps.onboardingService.clearMilestone(id);
});
}
32 changes: 0 additions & 32 deletions apps/desktop/src/main/onboarding-service.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -68,7 +68,6 @@ export interface OnboardingServiceDeps {
id: OnboardingMilestoneId,
status: 'completed' | 'skipped',
): Promise<OnboardingMilestone[]>;
clearMilestone(id: OnboardingMilestoneId): Promise<OnboardingMilestone[]>;
/**
* Whether `connection` has a usable credential — an API key OR (for
* OAuth-subscription providers) a stored OAuth token. MUST be
Expand All@@ -84,7 +83,6 @@ export interface OnboardingService {
id: unknown,
status: unknown,
): Promise<OnboardingSnapshot>;
clearMilestone(id: unknown): Promise<OnboardingSnapshot>;
}

/**
Expand DownExpand Up@@ -173,36 +171,6 @@ export function createOnboardingService(deps: OnboardingServiceDeps): Onboarding
});
return buildSnapshot(state, milestones, sessions, connections, defaultSlug, secrets);
},

async clearMilestone(id: unknown): Promise<OnboardingSnapshot> {
if (typeof id !== 'string' || !isOnboardingMilestoneId(id)) {
throw new Error('INVALID_MILESTONE_ID');
}
const milestones = await deps.clearMilestone(id);
const [connections, defaultSlug, sessions] = await Promise.all([
deps.listConnections(),
deps.getDefaultSlug(),
deps.listSessions(),
]);
const secretEntries = await Promise.all(
connections.map(async (connection) => {
try {
return [connection.slug, await deps.hasCredential(connection)] as const;
} catch {
return [connection.slug, false] as const;
}
}),
);
const secrets: Record<string, boolean> = Object.fromEntries(secretEntries);
const logicalSessions = collapseSessionRevisions(sessions);
const state = deriveOnboardingState({
connections,
defaultSlug: defaultSlug ?? undefined,
sessions: logicalSessions,
secrets,
});
return buildSnapshot(state, milestones, sessions, connections, defaultSlug, secrets);
},
};
}

Expand Down
6 changes: 0 additions & 6 deletions apps/desktop/src/main/runtime-host-artifacts-ipc-main.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -42,12 +42,6 @@ export function registerRuntimeHostArtifactsIpc(
: artifacts.filter(({ status }) => status !== "deleted");
},
);
handleReconnectableRead(
deps.ipcMain,
"artifacts:get",
(_event, sessionId: string, artifactId: string) =>
deps.client.getArtifact(sessionId, artifactId),
);
handleReconnectableRead(
deps.ipcMain,
"artifacts:readText",
Expand Down
1 change: 0 additions & 1 deletion apps/desktop/src/main/runtime-host-boot.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -987,7 +987,6 @@ function registerHostClientIpc(
(await settingsStore.get()).onboarding.milestones,
upsertMilestone: (id, status) =>
settingsStore.upsertOnboardingMilestone(id, status),
clearMilestone: (id) => settingsStore.clearOnboardingMilestone(id),
hasCredential: (connection) =>
readWithFallback(async () => {
if (!providerAuthRequiresSecret(connection.providerType)) return true;
Expand Down
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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
42 changes: 24 additions & 18 deletions apps/desktop/e2e/skill-draft-lifecycle.spec.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -8,11 +8,17 @@ import { expect, test, COMPOSER_INPUT } from './fixtures';
* here — begin edit, prepare the branch, fail the send, cancel back — moves it
* by moving the text. The point of these journeys is that nothing has to carry
* the Skill separately for that to hold.
*
* The Skill itself comes from the real catalog (the invocable-skills fixture
* plus the Skills module page), not a Desktop-only starter IPC.
*/
async function createStarterSkill(page: Page): Promise<void> {
const result = await page.evaluate(() => window.maka.skills.createStarter());
expect(result.ok).toBe(true);
await page.reload();
async function openInstalledWorkspaceSkill(page: Page): Promise<void> {
await page.getByRole('button', { name: '展开侧边栏' }).click();
const sidebar = page.getByRole('navigation', { name: '任务列表' });
await sidebar.getByRole('button', { name: '扩展' }).click();
await expect(page.locator('[data-module="skills"]')).toBeVisible();
await expect(page.getByText('Workspace Only', { exact: true })).toBeVisible();
await sidebar.getByRole('button', { name: '新任务', exact: true }).click();
await expect(page.locator(COMPOSER_INPUT)).toBeVisible();
}

Expand DownExpand Up@@ -41,9 +47,9 @@ async function beginRevision(page: Page): Promise<void> {
await expect(page.locator('[data-revision-notice="true"]')).toBeVisible();
}

async function failStarterSkillRevision(page: Page): Promise<void> {
async function failWorkspaceSkillRevision(page: Page): Promise<void> {
const disabled = await page.evaluate(() =>
window.maka.skills.setEnabled('starter-skill', false),
window.maka.skills.setEnabled('workspace-only', false),
);
expect(disabled.ok).toBe(true);

Expand All@@ -55,20 +61,20 @@ async function failStarterSkillRevision(page: Page): Promise<void> {
// composer draws chips from. A chip here would promise a Skill that no longer
// resolves — the text is the honest rendering, and re-enabling it below sends.
await expect(composer).toContainText('edited with skill');
await expect(composer).toContainText('/skill:starter-skill');
await expect(composer).toContainText('/skill:workspace-only');
}

test('a successful revision retry clears both child and source drafts', async ({
window: page,
invocableSkillsWindow: page,
}) => {
await createStarterSkill(page);
await openInstalledWorkspaceSkill(page);
await seedEditableTurn(page);
await beginRevision(page);
await composeWithSkill(page, 'edited with skill', /示例技能/);
await failStarterSkillRevision(page);
await composeWithSkill(page, 'edited with skill', /Workspace Only/);
await failWorkspaceSkillRevision(page);

const enabled = await page.evaluate(() =>
window.maka.skills.setEnabled('starter-skill', true),
window.maka.skills.setEnabled('workspace-only', true),
);
expect(enabled.ok).toBe(true);
await page.locator(COMPOSER_INPUT).press('Enter');
Expand All@@ -85,14 +91,14 @@ test('a successful revision retry clears both child and source drafts', async ({
test('cancelling a failed revision restores the complete pre-edit draft', async ({
invocableSkillsWindow: page,
}) => {
await createStarterSkill(page);
await openInstalledWorkspaceSkill(page);
await seedEditableTurn(page);

const composer = page.locator(COMPOSER_INPUT);
await composeWithSkill(page, 'previous unsent draft', /Workspace Only/);
await composeWithSkill(page, 'previous unsent draft', /Project Only/);
await beginRevision(page);
await composeWithSkill(page, 'edited with skill', /示例技能/);
await failStarterSkillRevision(page);
await composeWithSkill(page, 'edited with skill', /Workspace Only/);
await failWorkspaceSkillRevision(page);

await page.getByRole('button', { name: '取消' }).click();

Expand All@@ -102,6 +108,6 @@ test('cancelling a failed revision restores the complete pre-edit draft', async
// redraws it from that text, not because anything carried it separately.
await expect(composer).toContainText('previous unsent draft');
await expect(
page.locator('[data-astryx-token-value="/skill:workspace-only"]'),
).toContainText('Workspace Only');
page.locator('[data-astryx-token-value="/skill:project-only"]'),
).toContainText('Project Only');
});
10 changes: 8 additions & 2 deletions apps/desktop/e2e/slash-command-menu.spec.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -123,11 +123,17 @@ test('dispatches a staged slash command instead of steering it into a running tu
await expect(page.getByRole('button', { name: '插入消息' })).toBeVisible();
await composer.press('Enter');

await composer.fill('/');
// After the steering send the composer remounts. `fill('/')` can land
// before the contentEditable is focused, so the `/` trigger never
// populates. An empty command group then makes `/compact` look absent
// and the `/side` click waits out the timeout.
await composer.click();
await composer.pressSequentially('/');
const menu = page.getByRole('listbox', { name: '命令和技能' });
const commands = menu.getByRole('group', { name: '命令' });
await expect(commands.getByRole('option', { name: /\/compact/ })).toHaveCount(0);
const side = commands.getByRole('option', { name: /打开侧聊.*\/side/ });
await expect(side).toBeVisible();
await expect(commands.getByRole('option', { name: /\/compact/ })).toHaveCount(0);
await side.click();
await expect.poll(() => composer.textContent()).toBe('/side ');
await expect(page.locator('.maka-quote-workbar-panel')).toHaveCount(0);
Expand Down
3 changes: 0 additions & 3 deletions apps/desktop/src/main/__tests__/mcp-ipc-main.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -31,7 +31,6 @@ test('MCP IPC commits config before publishing capabilities and emitting status'
cancelConnect: () => { calls.push('cancel'); return true; },
sync: async () => { calls.push('sync'); },
statuses: () => [connected],
reconnect: async () => connected,
test: async () => ({ ok: true, status: connected, latencyMs: 1 }),
},
ensureReady: async () => { calls.push('ready'); },
Expand DownExpand Up@@ -106,7 +105,6 @@ test('MCP market cancellation waits for an in-flight config write before rolling
cancelConnect: () => { calls.push('cancel'); return true; },
sync: async () => { calls.push('sync'); },
statuses: () => [],
reconnect: async () => { throw new Error('not used'); },
test: async () => { throw new Error('not used'); },
},
ensureReady: async () => {},
Expand DownExpand Up@@ -157,7 +155,6 @@ test('MCP config commit is not rolled back by a capability publication failure',
cancelConnect: () => false,
sync: async () => {},
statuses: () => [],
reconnect: async () => { throw new Error('not used'); },
test: async () => { throw new Error('not used'); },
},
ensureReady: async () => {},
Expand Down
8 changes: 1 addition & 7 deletions apps/desktop/src/main/mcp-ipc-main.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -6,7 +6,7 @@ import type { McpConfigStore } from '@maka/storage';
export interface McpIpcMainDeps {
ipcMain: Pick<IpcMain, 'handle'>;
store: McpConfigStore;
manager: Pick<McpClientManager, 'sync' | 'statuses' | 'test' | 'reconnect' | 'cancelConnect'>;
manager: Pick<McpClientManager, 'sync' | 'statuses' | 'test' | 'cancelConnect'>;
ensureReady(): Promise<void>;
publishCapabilities(): Promise<void>;
onPublicationError(error: unknown): void;
Expand DownExpand Up@@ -81,12 +81,6 @@ export function registerMcpIpcMain(deps: McpIpcMainDeps): void {
deps.emitChanged(deps.manager.statuses());
return result;
});
deps.ipcMain.handle('mcp:reconnect', async (_event, serverId: string) => {
await deps.ensureReady();
const result = await deps.manager.reconnect(serverId);
changed(deps);
return result;
});
}

function changed(deps: McpIpcMainDeps): void {
Expand Down
3 changes: 0 additions & 3 deletions apps/desktop/src/main/onboarding-ipc-main.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -25,7 +25,4 @@ export function registerOnboardingIpc(deps: OnboardingIpcDeps): void {
// it as a typed reject rather than silently swallowing.
return deps.onboardingService.setMilestone(id, status);
});
target.handle('onboarding:clearMilestone', async (_event, id: unknown) => {
return deps.onboardingService.clearMilestone(id);
});
}
32 changes: 0 additions & 32 deletions apps/desktop/src/main/onboarding-service.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -68,7 +68,6 @@ export interface OnboardingServiceDeps {
id: OnboardingMilestoneId,
status: 'completed' | 'skipped',
): Promise<OnboardingMilestone[]>;
clearMilestone(id: OnboardingMilestoneId): Promise<OnboardingMilestone[]>;
/**
* Whether `connection` has a usable credential — an API key OR (for
* OAuth-subscription providers) a stored OAuth token. MUST be
Expand All@@ -84,7 +83,6 @@ export interface OnboardingService {
id: unknown,
status: unknown,
): Promise<OnboardingSnapshot>;
clearMilestone(id: unknown): Promise<OnboardingSnapshot>;
}

/**
Expand DownExpand Up@@ -173,36 +171,6 @@ export function createOnboardingService(deps: OnboardingServiceDeps): Onboarding
});
return buildSnapshot(state, milestones, sessions, connections, defaultSlug, secrets);
},

async clearMilestone(id: unknown): Promise<OnboardingSnapshot> {
if (typeof id !== 'string' || !isOnboardingMilestoneId(id)) {
throw new Error('INVALID_MILESTONE_ID');
}
const milestones = await deps.clearMilestone(id);
const [connections, defaultSlug, sessions] = await Promise.all([
deps.listConnections(),
deps.getDefaultSlug(),
deps.listSessions(),
]);
const secretEntries = await Promise.all(
connections.map(async (connection) => {
try {
return [connection.slug, await deps.hasCredential(connection)] as const;
} catch {
return [connection.slug, false] as const;
}
}),
);
const secrets: Record<string, boolean> = Object.fromEntries(secretEntries);
const logicalSessions = collapseSessionRevisions(sessions);
const state = deriveOnboardingState({
connections,
defaultSlug: defaultSlug ?? undefined,
sessions: logicalSessions,
secrets,
});
return buildSnapshot(state, milestones, sessions, connections, defaultSlug, secrets);
},
};
}

Expand Down
6 changes: 0 additions & 6 deletions apps/desktop/src/main/runtime-host-artifacts-ipc-main.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -42,12 +42,6 @@ export function registerRuntimeHostArtifactsIpc(
: artifacts.filter(({ status }) => status !== "deleted");
},
);
handleReconnectableRead(
deps.ipcMain,
"artifacts:get",
(_event, sessionId: string, artifactId: string) =>
deps.client.getArtifact(sessionId, artifactId),
);
handleReconnectableRead(
deps.ipcMain,
"artifacts:readText",
Expand Down
1 change: 0 additions & 1 deletion apps/desktop/src/main/runtime-host-boot.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -987,7 +987,6 @@ function registerHostClientIpc(
(await settingsStore.get()).onboarding.milestones,
upsertMilestone: (id, status) =>
settingsStore.upsertOnboardingMilestone(id, status),
clearMilestone: (id) => settingsStore.clearOnboardingMilestone(id),
hasCredential: (connection) =>
readWithFallback(async () => {
if (!providerAuthRequiresSecret(connection.providerType)) return true;
Expand Down
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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
42 changes: 24 additions & 18 deletions apps/desktop/e2e/skill-draft-lifecycle.spec.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -8,11 +8,17 @@ import { expect, test, COMPOSER_INPUT } from './fixtures';
* here — begin edit, prepare the branch, fail the send, cancel back — moves it
* by moving the text. The point of these journeys is that nothing has to carry
* the Skill separately for that to hold.
*
* The Skill itself comes from the real catalog (the invocable-skills fixture
* plus the Skills module page), not a Desktop-only starter IPC.
*/
async function createStarterSkill(page: Page): Promise<void> {
const result = await page.evaluate(() => window.maka.skills.createStarter());
expect(result.ok).toBe(true);
await page.reload();
async function openInstalledWorkspaceSkill(page: Page): Promise<void> {
await page.getByRole('button', { name: '展开侧边栏' }).click();
const sidebar = page.getByRole('navigation', { name: '任务列表' });
await sidebar.getByRole('button', { name: '扩展' }).click();
await expect(page.locator('[data-module="skills"]')).toBeVisible();
await expect(page.getByText('Workspace Only', { exact: true })).toBeVisible();
await sidebar.getByRole('button', { name: '新任务', exact: true }).click();
await expect(page.locator(COMPOSER_INPUT)).toBeVisible();
}

Expand DownExpand Up@@ -41,9 +47,9 @@ async function beginRevision(page: Page): Promise<void> {
await expect(page.locator('[data-revision-notice="true"]')).toBeVisible();
}

async function failStarterSkillRevision(page: Page): Promise<void> {
async function failWorkspaceSkillRevision(page: Page): Promise<void> {
const disabled = await page.evaluate(() =>
window.maka.skills.setEnabled('starter-skill', false),
window.maka.skills.setEnabled('workspace-only', false),
);
expect(disabled.ok).toBe(true);

Expand All@@ -55,20 +61,20 @@ async function failStarterSkillRevision(page: Page): Promise<void> {
// composer draws chips from. A chip here would promise a Skill that no longer
// resolves — the text is the honest rendering, and re-enabling it below sends.
await expect(composer).toContainText('edited with skill');
await expect(composer).toContainText('/skill:starter-skill');
await expect(composer).toContainText('/skill:workspace-only');
}

test('a successful revision retry clears both child and source drafts', async ({
window: page,
invocableSkillsWindow: page,
}) => {
await createStarterSkill(page);
await openInstalledWorkspaceSkill(page);
await seedEditableTurn(page);
await beginRevision(page);
await composeWithSkill(page, 'edited with skill', /示例技能/);
await failStarterSkillRevision(page);
await composeWithSkill(page, 'edited with skill', /Workspace Only/);
await failWorkspaceSkillRevision(page);

const enabled = await page.evaluate(() =>
window.maka.skills.setEnabled('starter-skill', true),
window.maka.skills.setEnabled('workspace-only', true),
);
expect(enabled.ok).toBe(true);
await page.locator(COMPOSER_INPUT).press('Enter');
Expand All@@ -85,14 +91,14 @@ test('a successful revision retry clears both child and source drafts', async ({
test('cancelling a failed revision restores the complete pre-edit draft', async ({
invocableSkillsWindow: page,
}) => {
await createStarterSkill(page);
await openInstalledWorkspaceSkill(page);
await seedEditableTurn(page);

const composer = page.locator(COMPOSER_INPUT);
await composeWithSkill(page, 'previous unsent draft', /Workspace Only/);
await composeWithSkill(page, 'previous unsent draft', /Project Only/);
await beginRevision(page);
await composeWithSkill(page, 'edited with skill', /示例技能/);
await failStarterSkillRevision(page);
await composeWithSkill(page, 'edited with skill', /Workspace Only/);
await failWorkspaceSkillRevision(page);

await page.getByRole('button', { name: '取消' }).click();

Expand All@@ -102,6 +108,6 @@ test('cancelling a failed revision restores the complete pre-edit draft', async
// redraws it from that text, not because anything carried it separately.
await expect(composer).toContainText('previous unsent draft');
await expect(
page.locator('[data-astryx-token-value="/skill:workspace-only"]'),
).toContainText('Workspace Only');
page.locator('[data-astryx-token-value="/skill:project-only"]'),
).toContainText('Project Only');
});
10 changes: 8 additions & 2 deletions apps/desktop/e2e/slash-command-menu.spec.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -123,11 +123,17 @@ test('dispatches a staged slash command instead of steering it into a running tu
await expect(page.getByRole('button', { name: '插入消息' })).toBeVisible();
await composer.press('Enter');

await composer.fill('/');
// After the steering send the composer remounts. `fill('/')` can land
// before the contentEditable is focused, so the `/` trigger never
// populates. An empty command group then makes `/compact` look absent
// and the `/side` click waits out the timeout.
await composer.click();
await composer.pressSequentially('/');
const menu = page.getByRole('listbox', { name: '命令和技能' });
const commands = menu.getByRole('group', { name: '命令' });
await expect(commands.getByRole('option', { name: /\/compact/ })).toHaveCount(0);
const side = commands.getByRole('option', { name: /打开侧聊.*\/side/ });
await expect(side).toBeVisible();
await expect(commands.getByRole('option', { name: /\/compact/ })).toHaveCount(0);
await side.click();
await expect.poll(() => composer.textContent()).toBe('/side ');
await expect(page.locator('.maka-quote-workbar-panel')).toHaveCount(0);
Expand Down
3 changes: 0 additions & 3 deletions apps/desktop/src/main/__tests__/mcp-ipc-main.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -31,7 +31,6 @@ test('MCP IPC commits config before publishing capabilities and emitting status'
cancelConnect: () => { calls.push('cancel'); return true; },
sync: async () => { calls.push('sync'); },
statuses: () => [connected],
reconnect: async () => connected,
test: async () => ({ ok: true, status: connected, latencyMs: 1 }),
},
ensureReady: async () => { calls.push('ready'); },
Expand DownExpand Up@@ -106,7 +105,6 @@ test('MCP market cancellation waits for an in-flight config write before rolling
cancelConnect: () => { calls.push('cancel'); return true; },
sync: async () => { calls.push('sync'); },
statuses: () => [],
reconnect: async () => { throw new Error('not used'); },
test: async () => { throw new Error('not used'); },
},
ensureReady: async () => {},
Expand DownExpand Up@@ -157,7 +155,6 @@ test('MCP config commit is not rolled back by a capability publication failure',
cancelConnect: () => false,
sync: async () => {},
statuses: () => [],
reconnect: async () => { throw new Error('not used'); },
test: async () => { throw new Error('not used'); },
},
ensureReady: async () => {},
Expand Down
8 changes: 1 addition & 7 deletions apps/desktop/src/main/mcp-ipc-main.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -6,7 +6,7 @@ import type { McpConfigStore } from '@maka/storage';
export interface McpIpcMainDeps {
ipcMain: Pick<IpcMain, 'handle'>;
store: McpConfigStore;
manager: Pick<McpClientManager, 'sync' | 'statuses' | 'test' | 'reconnect' | 'cancelConnect'>;
manager: Pick<McpClientManager, 'sync' | 'statuses' | 'test' | 'cancelConnect'>;
ensureReady(): Promise<void>;
publishCapabilities(): Promise<void>;
onPublicationError(error: unknown): void;
Expand DownExpand Up@@ -81,12 +81,6 @@ export function registerMcpIpcMain(deps: McpIpcMainDeps): void {
deps.emitChanged(deps.manager.statuses());
return result;
});
deps.ipcMain.handle('mcp:reconnect', async (_event, serverId: string) => {
await deps.ensureReady();
const result = await deps.manager.reconnect(serverId);
changed(deps);
return result;
});
}

function changed(deps: McpIpcMainDeps): void {
Expand Down
3 changes: 0 additions & 3 deletions apps/desktop/src/main/onboarding-ipc-main.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -25,7 +25,4 @@ export function registerOnboardingIpc(deps: OnboardingIpcDeps): void {
// it as a typed reject rather than silently swallowing.
return deps.onboardingService.setMilestone(id, status);
});
target.handle('onboarding:clearMilestone', async (_event, id: unknown) => {
return deps.onboardingService.clearMilestone(id);
});
}
32 changes: 0 additions & 32 deletions apps/desktop/src/main/onboarding-service.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -68,7 +68,6 @@ export interface OnboardingServiceDeps {
id: OnboardingMilestoneId,
status: 'completed' | 'skipped',
): Promise<OnboardingMilestone[]>;
clearMilestone(id: OnboardingMilestoneId): Promise<OnboardingMilestone[]>;
/**
* Whether `connection` has a usable credential — an API key OR (for
* OAuth-subscription providers) a stored OAuth token. MUST be
Expand All@@ -84,7 +83,6 @@ export interface OnboardingService {
id: unknown,
status: unknown,
): Promise<OnboardingSnapshot>;
clearMilestone(id: unknown): Promise<OnboardingSnapshot>;
}

/**
Expand DownExpand Up@@ -173,36 +171,6 @@ export function createOnboardingService(deps: OnboardingServiceDeps): Onboarding
});
return buildSnapshot(state, milestones, sessions, connections, defaultSlug, secrets);
},

async clearMilestone(id: unknown): Promise<OnboardingSnapshot> {
if (typeof id !== 'string' || !isOnboardingMilestoneId(id)) {
throw new Error('INVALID_MILESTONE_ID');
}
const milestones = await deps.clearMilestone(id);
const [connections, defaultSlug, sessions] = await Promise.all([
deps.listConnections(),
deps.getDefaultSlug(),
deps.listSessions(),
]);
const secretEntries = await Promise.all(
connections.map(async (connection) => {
try {
return [connection.slug, await deps.hasCredential(connection)] as const;
} catch {
return [connection.slug, false] as const;
}
}),
);
const secrets: Record<string, boolean> = Object.fromEntries(secretEntries);
const logicalSessions = collapseSessionRevisions(sessions);
const state = deriveOnboardingState({
connections,
defaultSlug: defaultSlug ?? undefined,
sessions: logicalSessions,
secrets,
});
return buildSnapshot(state, milestones, sessions, connections, defaultSlug, secrets);
},
};
}

Expand Down
6 changes: 0 additions & 6 deletions apps/desktop/src/main/runtime-host-artifacts-ipc-main.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -42,12 +42,6 @@ export function registerRuntimeHostArtifactsIpc(
: artifacts.filter(({ status }) => status !== "deleted");
},
);
handleReconnectableRead(
deps.ipcMain,
"artifacts:get",
(_event, sessionId: string, artifactId: string) =>
deps.client.getArtifact(sessionId, artifactId),
);
handleReconnectableRead(
deps.ipcMain,
"artifacts:readText",
Expand Down
1 change: 0 additions & 1 deletion apps/desktop/src/main/runtime-host-boot.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -987,7 +987,6 @@ function registerHostClientIpc(
(await settingsStore.get()).onboarding.milestones,
upsertMilestone: (id, status) =>
settingsStore.upsertOnboardingMilestone(id, status),
clearMilestone: (id) => settingsStore.clearOnboardingMilestone(id),
hasCredential: (connection) =>
readWithFallback(async () => {
if (!providerAuthRequiresSecret(connection.providerType)) return true;
Expand Down
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Universal Dark Mode - works on any site (function() { var enabled = true; function applyDarkMode() { if (!enabled) return; // Create style element if it doesn't exist var style = document.getElementById('universal-dark-mode-style'); if (!style) { style = document.createElement('style'); style.id = 'universal-dark-mode-style'; document.head.appendChild(style); } // Dark mode CSS - inverts colors but preserves images/video style.textContent = ' /* Invert everything except media */ html { filter: invert(1) hue-rotate(180deg) !important; background: #1a1a2e !important; } /* Restore images, videos, iframes, canvas */ img, video, iframe, canvas, svg, picture, [style*="background-image"] { filter: invert(1) hue-rotate(180deg) !important; } /* Preserve specific elements that should not be inverted */ .no-dark-mode, .no-dark-mode *, [data-theme="light"], [data-theme="light"], .ace_editor, .ace_editor *, .CodeMirror, .CodeMirror *, .monaco-editor, .monaco-editor *, .markdown-body pre, .markdown-body pre *, .highlight, .highlight *, pre code, pre code * { filter: none !important; } /* Fix common UI elements */ .modal, .popup, .dropdown-menu, .tooltip, .popover { filter: invert(1) hue-rotate(180deg) !important; background: #2d2d44 !important; border-color: #444 !important; } /* Scrollbars */ ::-webkit-scrollbar { background: #1a1a2e !important; } ::-webkit-scrollbar-thumb { background: #444 !important; } ::-webkit-scrollbar-thumb:hover { background: #555 !important; } /* Selection */ ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; } ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; } '; } function removeDarkMode() { var style = document.getElementById('universal-dark-mode-style'); if (style) style.remove(); } // Toggle with Alt+Shift+D document.addEventListener('keydown', function(e) { if (e.altKey && e.shiftKey && e.key === 'D') { e.preventDefault(); enabled = !enabled; if (enabled) { applyDarkMode(); console.log('[Universal Dark Mode] Enabled'); } else { removeDarkMode(); console.log('[Universal Dark Mode] Disabled'); } } }); // Apply on load applyDarkMode(); // Re-apply on dynamic content var observer = new MutationObserver(function(mutations) { if (enabled && !document.getElementById('universal-dark-mode-style')) { applyDarkMode(); } }); observer.observe(document.head, { childList: true }); console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle'); })(); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
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
42 changes: 24 additions & 18 deletions apps/desktop/e2e/skill-draft-lifecycle.spec.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -8,11 +8,17 @@ import { expect, test, COMPOSER_INPUT } from './fixtures';
* here — begin edit, prepare the branch, fail the send, cancel back — moves it
* by moving the text. The point of these journeys is that nothing has to carry
* the Skill separately for that to hold.
*
* The Skill itself comes from the real catalog (the invocable-skills fixture
* plus the Skills module page), not a Desktop-only starter IPC.
*/
async function createStarterSkill(page: Page): Promise<void> {
const result = await page.evaluate(() => window.maka.skills.createStarter());
expect(result.ok).toBe(true);
await page.reload();
async function openInstalledWorkspaceSkill(page: Page): Promise<void> {
await page.getByRole('button', { name: '展开侧边栏' }).click();
const sidebar = page.getByRole('navigation', { name: '任务列表' });
await sidebar.getByRole('button', { name: '扩展' }).click();
await expect(page.locator('[data-module="skills"]')).toBeVisible();
await expect(page.getByText('Workspace Only', { exact: true })).toBeVisible();
await sidebar.getByRole('button', { name: '新任务', exact: true }).click();
await expect(page.locator(COMPOSER_INPUT)).toBeVisible();
}

Expand DownExpand Up@@ -41,9 +47,9 @@ async function beginRevision(page: Page): Promise<void> {
await expect(page.locator('[data-revision-notice="true"]')).toBeVisible();
}

async function failStarterSkillRevision(page: Page): Promise<void> {
async function failWorkspaceSkillRevision(page: Page): Promise<void> {
const disabled = await page.evaluate(() =>
window.maka.skills.setEnabled('starter-skill', false),
window.maka.skills.setEnabled('workspace-only', false),
);
expect(disabled.ok).toBe(true);

Expand All@@ -55,20 +61,20 @@ async function failStarterSkillRevision(page: Page): Promise<void> {
// composer draws chips from. A chip here would promise a Skill that no longer
// resolves — the text is the honest rendering, and re-enabling it below sends.
await expect(composer).toContainText('edited with skill');
await expect(composer).toContainText('/skill:starter-skill');
await expect(composer).toContainText('/skill:workspace-only');
}

test('a successful revision retry clears both child and source drafts', async ({
window: page,
invocableSkillsWindow: page,
}) => {
await createStarterSkill(page);
await openInstalledWorkspaceSkill(page);
await seedEditableTurn(page);
await beginRevision(page);
await composeWithSkill(page, 'edited with skill', /示例技能/);
await failStarterSkillRevision(page);
await composeWithSkill(page, 'edited with skill', /Workspace Only/);
await failWorkspaceSkillRevision(page);

const enabled = await page.evaluate(() =>
window.maka.skills.setEnabled('starter-skill', true),
window.maka.skills.setEnabled('workspace-only', true),
);
expect(enabled.ok).toBe(true);
await page.locator(COMPOSER_INPUT).press('Enter');
Expand All@@ -85,14 +91,14 @@ test('a successful revision retry clears both child and source drafts', async ({
test('cancelling a failed revision restores the complete pre-edit draft', async ({
invocableSkillsWindow: page,
}) => {
await createStarterSkill(page);
await openInstalledWorkspaceSkill(page);
await seedEditableTurn(page);

const composer = page.locator(COMPOSER_INPUT);
await composeWithSkill(page, 'previous unsent draft', /Workspace Only/);
await composeWithSkill(page, 'previous unsent draft', /Project Only/);
await beginRevision(page);
await composeWithSkill(page, 'edited with skill', /示例技能/);
await failStarterSkillRevision(page);
await composeWithSkill(page, 'edited with skill', /Workspace Only/);
await failWorkspaceSkillRevision(page);

await page.getByRole('button', { name: '取消' }).click();

Expand All@@ -102,6 +108,6 @@ test('cancelling a failed revision restores the complete pre-edit draft', async
// redraws it from that text, not because anything carried it separately.
await expect(composer).toContainText('previous unsent draft');
await expect(
page.locator('[data-astryx-token-value="/skill:workspace-only"]'),
).toContainText('Workspace Only');
page.locator('[data-astryx-token-value="/skill:project-only"]'),
).toContainText('Project Only');
});
10 changes: 8 additions & 2 deletions apps/desktop/e2e/slash-command-menu.spec.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -123,11 +123,17 @@ test('dispatches a staged slash command instead of steering it into a running tu
await expect(page.getByRole('button', { name: '插入消息' })).toBeVisible();
await composer.press('Enter');

await composer.fill('/');
// After the steering send the composer remounts. `fill('/')` can land
// before the contentEditable is focused, so the `/` trigger never
// populates. An empty command group then makes `/compact` look absent
// and the `/side` click waits out the timeout.
await composer.click();
await composer.pressSequentially('/');
const menu = page.getByRole('listbox', { name: '命令和技能' });
const commands = menu.getByRole('group', { name: '命令' });
await expect(commands.getByRole('option', { name: /\/compact/ })).toHaveCount(0);
const side = commands.getByRole('option', { name: /打开侧聊.*\/side/ });
await expect(side).toBeVisible();
await expect(commands.getByRole('option', { name: /\/compact/ })).toHaveCount(0);
await side.click();
await expect.poll(() => composer.textContent()).toBe('/side ');
await expect(page.locator('.maka-quote-workbar-panel')).toHaveCount(0);
Expand Down
3 changes: 0 additions & 3 deletions apps/desktop/src/main/__tests__/mcp-ipc-main.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -31,7 +31,6 @@ test('MCP IPC commits config before publishing capabilities and emitting status'
cancelConnect: () => { calls.push('cancel'); return true; },
sync: async () => { calls.push('sync'); },
statuses: () => [connected],
reconnect: async () => connected,
test: async () => ({ ok: true, status: connected, latencyMs: 1 }),
},
ensureReady: async () => { calls.push('ready'); },
Expand DownExpand Up@@ -106,7 +105,6 @@ test('MCP market cancellation waits for an in-flight config write before rolling
cancelConnect: () => { calls.push('cancel'); return true; },
sync: async () => { calls.push('sync'); },
statuses: () => [],
reconnect: async () => { throw new Error('not used'); },
test: async () => { throw new Error('not used'); },
},
ensureReady: async () => {},
Expand DownExpand Up@@ -157,7 +155,6 @@ test('MCP config commit is not rolled back by a capability publication failure',
cancelConnect: () => false,
sync: async () => {},
statuses: () => [],
reconnect: async () => { throw new Error('not used'); },
test: async () => { throw new Error('not used'); },
},
ensureReady: async () => {},
Expand Down
8 changes: 1 addition & 7 deletions apps/desktop/src/main/mcp-ipc-main.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -6,7 +6,7 @@ import type { McpConfigStore } from '@maka/storage';
export interface McpIpcMainDeps {
ipcMain: Pick<IpcMain, 'handle'>;
store: McpConfigStore;
manager: Pick<McpClientManager, 'sync' | 'statuses' | 'test' | 'reconnect' | 'cancelConnect'>;
manager: Pick<McpClientManager, 'sync' | 'statuses' | 'test' | 'cancelConnect'>;
ensureReady(): Promise<void>;
publishCapabilities(): Promise<void>;
onPublicationError(error: unknown): void;
Expand DownExpand Up@@ -81,12 +81,6 @@ export function registerMcpIpcMain(deps: McpIpcMainDeps): void {
deps.emitChanged(deps.manager.statuses());
return result;
});
deps.ipcMain.handle('mcp:reconnect', async (_event, serverId: string) => {
await deps.ensureReady();
const result = await deps.manager.reconnect(serverId);
changed(deps);
return result;
});
}

function changed(deps: McpIpcMainDeps): void {
Expand Down
3 changes: 0 additions & 3 deletions apps/desktop/src/main/onboarding-ipc-main.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -25,7 +25,4 @@ export function registerOnboardingIpc(deps: OnboardingIpcDeps): void {
// it as a typed reject rather than silently swallowing.
return deps.onboardingService.setMilestone(id, status);
});
target.handle('onboarding:clearMilestone', async (_event, id: unknown) => {
return deps.onboardingService.clearMilestone(id);
});
}
32 changes: 0 additions & 32 deletions apps/desktop/src/main/onboarding-service.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -68,7 +68,6 @@ export interface OnboardingServiceDeps {
id: OnboardingMilestoneId,
status: 'completed' | 'skipped',
): Promise<OnboardingMilestone[]>;
clearMilestone(id: OnboardingMilestoneId): Promise<OnboardingMilestone[]>;
/**
* Whether `connection` has a usable credential — an API key OR (for
* OAuth-subscription providers) a stored OAuth token. MUST be
Expand All@@ -84,7 +83,6 @@ export interface OnboardingService {
id: unknown,
status: unknown,
): Promise<OnboardingSnapshot>;
clearMilestone(id: unknown): Promise<OnboardingSnapshot>;
}

/**
Expand DownExpand Up@@ -173,36 +171,6 @@ export function createOnboardingService(deps: OnboardingServiceDeps): Onboarding
});
return buildSnapshot(state, milestones, sessions, connections, defaultSlug, secrets);
},

async clearMilestone(id: unknown): Promise<OnboardingSnapshot> {
if (typeof id !== 'string' || !isOnboardingMilestoneId(id)) {
throw new Error('INVALID_MILESTONE_ID');
}
const milestones = await deps.clearMilestone(id);
const [connections, defaultSlug, sessions] = await Promise.all([
deps.listConnections(),
deps.getDefaultSlug(),
deps.listSessions(),
]);
const secretEntries = await Promise.all(
connections.map(async (connection) => {
try {
return [connection.slug, await deps.hasCredential(connection)] as const;
} catch {
return [connection.slug, false] as const;
}
}),
);
const secrets: Record<string, boolean> = Object.fromEntries(secretEntries);
const logicalSessions = collapseSessionRevisions(sessions);
const state = deriveOnboardingState({
connections,
defaultSlug: defaultSlug ?? undefined,
sessions: logicalSessions,
secrets,
});
return buildSnapshot(state, milestones, sessions, connections, defaultSlug, secrets);
},
};
}

Expand Down
6 changes: 0 additions & 6 deletions apps/desktop/src/main/runtime-host-artifacts-ipc-main.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -42,12 +42,6 @@ export function registerRuntimeHostArtifactsIpc(
: artifacts.filter(({ status }) => status !== "deleted");
},
);
handleReconnectableRead(
deps.ipcMain,
"artifacts:get",
(_event, sessionId: string, artifactId: string) =>
deps.client.getArtifact(sessionId, artifactId),
);
handleReconnectableRead(
deps.ipcMain,
"artifacts:readText",
Expand Down
1 change: 0 additions & 1 deletion apps/desktop/src/main/runtime-host-boot.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -987,7 +987,6 @@ function registerHostClientIpc(
(await settingsStore.get()).onboarding.milestones,
upsertMilestone: (id, status) =>
settingsStore.upsertOnboardingMilestone(id, status),
clearMilestone: (id) => settingsStore.clearOnboardingMilestone(id),
hasCredential: (connection) =>
readWithFallback(async () => {
if (!providerAuthRequiresSecret(connection.providerType)) return true;
Expand Down
Loading
Loading