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
3 changes: 2 additions & 1 deletion apps/desktop/src/main/__tests__/ipc-surface-contract.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,8 @@ describe('IPC surface contract', () => {
// The ai-sdk backend wiring moved into session-stream.ts (arch R5); these two
// pins now target the combined main-process source instead of main.ts itself.
assert.match(combinedMainProcess, /const memoryPromptSnapshot = await systemPromptService\.buildLocalMemoryPromptFragment\(\)/);
assert.match(combinedMainProcess, /systemPrompt: \(\{ cwd \}\) => systemPromptService\.buildBackendSystemPrompt\(ctx\.header, cwd, \{[\s\S]*childInstruction: ctx\.systemPrompt/);
assert.match(combinedMainProcess, /systemPrompt: async \(\{ cwd \}\) => \{/);
assert.match(combinedMainProcess, /const base = await systemPromptService\.buildBackendSystemPrompt\([\s\S]*ctx\.header,[\s\S]*cwd,[\s\S]*childInstruction: ctx\.systemPrompt/);
assert.match(combinedMainProcess, /async function buildBackendSystemPrompt/);
assert.match(combinedMainProcess, /childInstruction[\s\S]*memoryFragment: null, includePersonalization: false/);
assert.match(combinedMainProcess, /子代理必须继承当前会话的权限、隐私、工作区和技能约束/);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { strict as assert } from 'node:assert';
import { describe, it } from 'node:test';
import { readRendererShellSources } from './renderer-shell-source-helpers.js';
import { readMainProcessCombinedSource } from './main-process-contract-source-helpers.js';

describe('new-chat Plan Mode contract', () => {
it('keeps the Plan switch available before a session exists', async () => {
const source = await readRendererShellSources(['app-shell.tsx']);

assert.match(source, /const \[newChatPlanModeActive, setNewChatPlanModeActive\] = useState\(false\);/);
assert.match(source, /if \(!sessionId\) \{\s*setNewChatPlanModeActive\(active\);\s*return;\s*\}/);
assert.match(source, /onPlanModeChange=\{setPlanMode\}/);
assert.doesNotMatch(source, /onPlanModeChange=\{activeId \? setPlanMode : undefined\}/);
});

it('carries the selected collaboration mode into first-message session creation', async () => {
const renderer = await readRendererShellSources(['app-shell.tsx', 'app-shell-chat-actions.ts']);
const main = await readMainProcessCombinedSource();

assert.match(renderer, /newChatCollaborationMode: newChatPlanModeActive \? 'plan' : 'agent'/);
assert.match(renderer, /collaborationMode: newChatCollaborationMode/);
assert.match(main, /const collaborationMode = input\?\.collaborationMode \?\? 'agent';/);
assert.ok(
(main.match(/\s+collaborationMode,\s/g) ?? []).length >= 2,
'both fake and ai-sdk session creation must persist the selected collaboration mode',
);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import { strict as assert } from 'node:assert';
import { readFile } from 'node:fs/promises';
import { resolve } from 'node:path';
import { describe, it } from 'node:test';

const repoRoot = resolve(import.meta.dirname, '../../../../..');
const rendererRoot = resolve(repoRoot, 'apps', 'desktop', 'src', 'renderer');

describe('Plan Mode proposal presentation', () => {
it('renders proposal steps and risks as individual items', async () => {
const source = await readFile(resolve(rendererRoot, 'plan-mode-panel.tsx'), 'utf8');

assert.match(source, /className="plan-proposal-steps"/);
assert.match(source, /proposal\.steps\.map\(\(step, index\)/);
assert.match(source, /className="plan-proposal-step-number"/);
assert.match(source, /className="plan-proposal-step-content"/);
assert.match(source, /<strong>\{step\.title\}<\/strong>/);
assert.match(source, /<p>\{step\.description\}<\/p>/);
assert.match(source, /proposal\.risks\.map\(\(risk, index\)/);
assert.match(source, /reviewable && \(/);
assert.match(source, /proposalStatusLabel\(proposal\.status\)/);
});

it('uses Maka theme tokens instead of fixed light-mode colors', async () => {
const styles = await readFile(resolve(rendererRoot, 'styles/plan-mode.css'), 'utf8');

assert.match(styles, /background: var\(--background-elevated\)/);
assert.match(styles, /color: var\(--foreground\)/);
assert.match(styles, /solid var\(--border\)/);
assert.doesNotMatch(styles, /#[\da-f]{3,8}\b/i);
});

it('anchors every proposal to its historical turn and keeps execution above the composer', async () => {
const appShell = await readFile(resolve(rendererRoot, 'app-shell.tsx'), 'utf8');
const chatView = await readFile(
resolve(repoRoot, 'packages', 'ui', 'src', 'chat-view.tsx'),
'utf8',
);

assert.match(appShell, /afterTurnId: proposal\.turnId/);
assert.match(appShell, /conversationItems=\{planConversationItems\}/);
assert.match(appShell, /<PlanExecutionPanel planMode=\{planMode\} \/>[\s\S]*?<ChatComposerRegion/);
assert.match(
chatView,
/conversationItemsByTurn\.get\(turn\.turnId\)/,
);
});

it('refreshes execution state after a durable Plan tool result', async () => {
const source = await readFile(resolve(rendererRoot, 'plan-mode-panel.tsx'), 'utf8');

assert.match(source, /isPlanToolResult\(event\)/);
assert.match(source, /kind === 'plan_progress_updated'/);
assert.match(source, /className="plan-execution-steps"/);
assert.match(source, /executionStepStatusLabel\(step\.status\)/);
assert.match(source, /<span>\{step\.title\}<\/span>/);
});

it('keeps execution details collapsed by default and offers interrupted actions', async () => {
const [source, preload, bridge, main] = await Promise.all([
readFile(resolve(rendererRoot, 'plan-mode-panel.tsx'), 'utf8'),
readFile(resolve(repoRoot, 'apps', 'desktop', 'src', 'preload', 'preload.ts'), 'utf8'),
readFile(
resolve(repoRoot, 'apps', 'desktop', 'src', 'preload', 'bridge-contract.d.ts'),
'utf8',
),
readFile(resolve(repoRoot, 'apps', 'desktop', 'src', 'main', 'sessions-ipc-main.ts'), 'utf8'),
]);

assert.match(source, /const \[expanded, setExpanded\] = useState\(false\)/);
assert.match(source, /aria-expanded=\{expanded\}/);
assert.match(source, /\{expanded && \(/);
assert.match(source, />\s*恢复执行\s*</);
assert.match(source, />\s*放弃计划\s*</);
assert.match(source, /toastApi\.confirm\(\{/);
assert.match(source, /window\.maka\.sessions\.abandonPlanExecution/);
assert.match(preload, /abandonPlanExecution\(sessionId: string, executionId: string\)/);
assert.match(preload, /ipcRenderer\.invoke\('plan-mode:abandonExecution', sessionId, executionId\)/);
assert.match(bridge, /abandonPlanExecution\(sessionId: string, executionId: string\)/);
assert.match(main, /ipcMain\.handle\('plan-mode:abandonExecution'/);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,8 @@ describe('session environment prompt', () => {
it('is injected as a current-turn tail instead of durable system prefix', async () => {
const source = await readMainProcessCombinedSource();

assert.match(source, /turnTailPrompt:\s*\(\{ cwd, sessionId \}\) => systemPromptService\.buildTurnTailPrompt\(cwd, sessionId\)/);
assert.match(source, /turnTailPrompt:\s*async \(\{ cwd, sessionId \}\) => \{/);
assert.match(source, /const base = await systemPromptService\.buildTurnTailPrompt\(cwd, sessionId\)/);
assert.match(source, /async function buildTurnTailPrompt\(cwd\?: string, sessionId\?: string\)/);
assert.match(source, /projectGit:\s*await resolveProjectGitInfo\(cwd\)/);
assert.doesNotMatch(source, /personalization\.text,\n\s*environment,\n\s*deepResearch/);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ describe('PR-SESSION-STICKY-MODEL-0 contract', () => {
assert.match(coreReadiness, /normalizeRequestedModelForReadiness\(\s*connection: LlmConnection,\s*requestedModel: string \| undefined,\s*\)/);
assert.match(coreReadiness, /CODEX_SUBSCRIPTION_UNSUPPORTED_CHATGPT_MODELS\.has\(requestedModel\)/);
assert.match(main, /const \{ connection, apiKey, model \} = await getReadyConnection\(ctx\.header\.llmConnectionSlug, ctx\.header\.model\)/);
assert.match(main, /header: \{ \.\.\.ctx\.header, model \}/);
assert.match(main, /header: \{ \.\.\.ctx\.header, model, permissionMode: effectivePermissionMode \}/);
assert.match(main, /modelId: model/);
// #1038: the locked/sticky guarantee lives in the core projection —
// the desktop send gate delegates the decision to it.
Expand Down
2 changes: 1 addition & 1 deletion apps/desktop/src/main/__tests__/skills.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ Use the Office tools.`);
assert.match(mainProcess, /\[\.\.\.builtinTools, \.\.\.buildMcpTools\(mcpManager\)\]/);
assert.match(mainProcess, /const backendTools = computerUseToolsForModel\(/);
assert.match(mainProcess, /const backendToolNames = new Set\(/);
assert.match(mainProcess, /backendTools\.map\(\(tool\) => tool\.name\)/);
assert.match(mainProcess, /selectedTools\.map\(\(tool\) => tool\.name\)/);
assert.match(mainProcess, /if \(!ctx\.tools\) desktopSessionSkillHosts\.set\(ctx\.sessionId, backendSkillHost\)/);
assert.match(mainProcess, /host: backendSkillHost/);
});
Expand Down
8 changes: 8 additions & 0 deletions apps/desktop/src/main/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ import {
createReadImageSnapshotter,
createConnectionStore,
createPlanReminderStore,
createPlanStore,
openRuntimeEventPersistence,
createSessionStore,
createSettingsStore,
Expand Down Expand Up @@ -195,6 +196,7 @@ const workspaceRoot = join(app.getPath('userData'), 'workspaces', visualSmokeFix
// process, so quit needs no special teardown.
const keepSystemAwake = createKeepSystemAwakeController(powerSaveBlocker);
const store = createSessionStore(workspaceRoot);
const planStore = createPlanStore(workspaceRoot);
const runStore = createAgentRunStore(workspaceRoot);
const runtimePersistence = await openRuntimeEventPersistence({
workspaceRoot,
Expand Down Expand Up @@ -566,6 +568,7 @@ const {
builtinTools,
toolAvailability,
childAgentTools,
sandboxDiagnosticsProvider,
} = assembleDesktopTools({
isComputerUseRealModelE2e,
workspaceRoot,
Expand Down Expand Up @@ -683,10 +686,12 @@ backends.register('ai-sdk', createAiSdkBackendFactory({
agentTeamLeadTools,
builtinTools,
toolAvailability,
sandboxDiagnosticsProvider,
persistToolArtifacts,
persistArchivedToolResult,
readArchivedToolResult,
runtimeCommitStore: runtimePersistence.runtimeCommitStore,
planStore,
safeSendToRenderer,
getRuntime: () => runtime,
getLookupPricing: () => lookupPricing,
Expand All @@ -709,6 +714,7 @@ if (isE2e) {

const runtime = new SessionManager({
store,
planStore,
runStore,
runtimeEventStore,
...(runtimePersistence.runtimeCommitStore
Expand Down Expand Up @@ -987,6 +993,8 @@ const streamEvents = createSessionStreamer({
computerUseTools,
safeSendToRenderer,
emitSessionsChanged,
interruptActivePlanExecution: (sessionId, reason) =>
runtime.interruptActivePlanExecution(sessionId, reason),
});

async function ensureSessionCanSend(sessionId: string): Promise<void> {
Expand Down
Loading
Loading