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
8 changes: 3 additions & 5 deletions .ade/ade.yaml
Original file line number Diff line number Diff line change
@@ -1,12 +1,10 @@
version: 1
processes:
- id: hiwo8mbf
name: dogfood.sh local model fixes
name: dogfood.sh droid
command:
- ./scripts/dogfood.sh
- local
- model
- fixes
- scripts/dogfood.sh
- droid-chat
cwd: ./
stackButtons: []
testSuites: []
Expand Down
19 changes: 8 additions & 11 deletions .ade/cto/identity.yaml
Original file line number Diff line number Diff line change
@@ -1,14 +1,7 @@
name: CTO
version: 1
persona: >-
You are the CTO for this project inside ADE.

You are the persistent technical lead who owns architecture, execution
quality, engineering continuity, and team direction.

Use ADE's tools and project context to help the team move forward with clear,
concrete decisions.
personality: strategic
version: 3
persona: Persistent project CTO with collaborative personality.
personality: casual
modelPreferences:
provider: claude
model: sonnet
Expand All @@ -28,4 +21,8 @@ openclawContextPolicy:
- secret
- token
- system_prompt
updatedAt: 1970-01-01T00:00:00.000Z
onboardingState:
completedSteps:
- identity
completedAt: 2026-04-02T14:20:19.124Z
updatedAt: 2026-04-02T14:20:19.127Z
20 changes: 20 additions & 0 deletions apps/desktop/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions apps/desktop/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@
"electron-updater": "^6.8.3",
"framer-motion": "^12.34.2",
"geist": "^1.7.0",
"lottie-react": "^2.4.1",
"lucide-react": "^0.563.0",
"monaco-editor": "^0.55.1",
"motion": "^12.34.2",
Expand Down
33 changes: 14 additions & 19 deletions apps/desktop/src/main/main.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { app, BrowserWindow, nativeImage, protocol, shell } from "electron";
import path from "node:path";
type NodePtyType = typeof import("node-pty");

Check warning on line 3 in apps/desktop/src/main/main.ts

View workflow job for this annotation

GitHub Actions / lint-desktop

`import()` type annotations are forbidden
import { registerIpc } from "./services/ipc/registerIpc";
import { createFileLogger } from "./services/logging/logger";
import { openKvDb } from "./services/state/kvDb";
Expand Down Expand Up @@ -3204,25 +3204,20 @@
globalStatePath,
});

await createWindow(getActiveContext().logger);

// Initial project context: load AFTER the window is visible so the main
// thread isn't blocked (DB load + service init) before anything renders.
// Dogfood and other explicit ADE_PROJECT_ROOT launches need the project
// context ready before the renderer boots, otherwise the window can paint
// the welcome state and swallow project selection into a confusing no-op.
if (startupUserSelected) {
const startupRoot = normalizeProjectRoot(initialCandidate);
void (async () => {
try {
await switchProjectFromDialog(initialCandidate);
} catch {
if (!activeProjectRoot || activeProjectRoot === startupRoot) {
setActiveProject(null);
dormantContext = createDormantProjectContext();
emitProjectChanged(null);
}
}
})();
try {
await switchProjectFromDialog(initialCandidate);
} catch {
setActiveProject(null);
dormantContext = createDormantProjectContext();
}
}

await createWindow(getActiveContext().logger);

app.on("activate", async () => {
if (BrowserWindow.getAllWindows().length === 0) {
await createWindow(getActiveContext().logger);
Expand All @@ -3237,10 +3232,10 @@
const current = getActiveContext();
const previousRoot = current.project?.rootPath;
current.logger.info("app.before_quit");
// Kill the shared OpenCode inventory server before quitting
// Kill any remaining OpenCode servers before quitting.
try {
const { shutdownInventoryServer } = require("./services/opencode/openCodeInventory");
shutdownInventoryServer();
const { shutdownOpenCodeServers } = require("./services/opencode/openCodeServerManager");
shutdownOpenCodeServers();
} catch { /* ignore if module not loaded */ }
setActiveProject(null);
dormantContext = createDormantProjectContext(previousRoot);
Expand Down
136 changes: 134 additions & 2 deletions apps/desktop/src/main/services/chat/agentChatService.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,20 @@ import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { unstable_v2_createSession, unstable_v2_resumeSession } from "@anthropic-ai/claude-agent-sdk";
import { buildOpenCodePromptParts } from "../opencode/openCodeRuntime";
import { buildOpenCodePromptParts, startOpenCodeSession } from "../opencode/openCodeRuntime";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";

const generateText = vi.fn();
const streamText = vi.fn();

vi.mock("@opencode-ai/sdk", () => ({
createOpencodeServer: vi.fn(async () => ({
url: "http://mock-opencode-server",
close: vi.fn(),
})),
createOpencodeClient: vi.fn(() => ({})),
}));

// ---------------------------------------------------------------------------
// vi.hoisted mock state
// ---------------------------------------------------------------------------
Expand Down Expand Up @@ -303,6 +311,9 @@ vi.mock("../opencode/openCodeRuntime", () => ({
close: vi.fn(),
},
close: vi.fn(),
touch: vi.fn(),
setBusy: vi.fn(),
setEvictionHandler: vi.fn(),
client,
};
}),
Expand Down Expand Up @@ -1601,7 +1612,20 @@ describe("createAgentChatService", () => {
const promptCalls = vi.mocked(buildOpenCodePromptParts).mock.calls;
const firstUserContent = String(promptCalls[0]?.[0]?.prompt ?? "");
const secondUserContent = String(promptCalls[1]?.[0]?.prompt ?? "");

const resolvedTmpRoot = fs.realpathSync(tmpRoot);
const openCodeStartCalls = vi.mocked(startOpenCodeSession).mock.calls;

expect(vi.mocked(resolveAdeMcpServerLaunch)).toHaveBeenCalledWith(expect.objectContaining({
projectRoot: tmpRoot,
workspaceRoot: resolvedTmpRoot,
workspaceBinding: "project_root",
chatSessionId: session.id,
}));
expect(openCodeStartCalls.length).toBeGreaterThan(0);
expect(openCodeStartCalls[0]?.[0]).toEqual(expect.objectContaining({
leaseKind: "shared",
dynamicMcpLaunch: expect.any(Object),
}));
expect(firstUserContent).toContain("[ADE launch directive]");
expect(firstUserContent).toContain(tmpRoot);
expect(firstUserContent).toContain("only inside that worktree");
Expand Down Expand Up @@ -3383,6 +3407,114 @@ describe("createAgentChatService", () => {
expect(setPermissionMode.mock.invocationCallOrder[0]).toBeLessThan(send.mock.invocationCallOrder[1]);
});

it("preserves Claude access overrides when entering and exiting plan mode", async () => {
const events: AgentChatEventEnvelope[] = [];
const setPermissionMode = vi.fn().mockResolvedValue(undefined);
const send = vi.fn().mockResolvedValue(undefined);
let streamCall = 0;
let service: ReturnType<typeof createService>["service"];
let sessionId = "";

const stream = vi.fn(() => (async function* () {
streamCall += 1;
if (streamCall === 1) {
yield {
type: "system",
subtype: "init",
session_id: "sdk-session-plan-preserve",
slash_commands: [],
};
return;
}

const sessionOpts = vi.mocked(unstable_v2_createSession).mock.calls.at(-1)?.[0] as any;
const enterResult = await sessionOpts.canUseTool("EnterPlanMode", {}, {
signal: new AbortController().signal,
toolUseID: "tool-enter-plan",
});
expect(enterResult).toEqual({ behavior: "allow" });

const entered = await service.getSessionSummary(sessionId);
expect(entered?.permissionMode).toBe("plan");
expect(entered?.claudePermissionMode).toBe("acceptEdits");

const exitPromise = sessionOpts.canUseTool("ExitPlanMode", {
planDescription: "Ship the approved Claude changes.",
}, {
signal: new AbortController().signal,
toolUseID: "tool-exit-plan",
});

const approvalEvent = await waitForEvent(
events,
(event): event is AgentChatEventEnvelope & {
event: Extract<AgentChatEventEnvelope["event"], { type: "approval_request" }>;
} =>
event.event.type === "approval_request"
&& typeof ((event.event.detail as { request?: { kind?: string } } | undefined)?.request?.kind) === "string"
&& ((event.event.detail as { request?: { kind?: string } } | undefined)?.request?.kind === "plan_approval"),
);

await service.approveToolUse({
sessionId,
itemId: approvalEvent.event.itemId,
decision: "accept",
});

const exitResult = await exitPromise;
expect(exitResult).toMatchObject({
behavior: "deny",
message: expect.stringContaining("exited plan mode"),
});

yield {
type: "assistant",
message: {
content: [{ type: "text", text: "Plan approved and preserved." }],
usage: { input_tokens: 1, output_tokens: 1 },
},
};
yield {
type: "result",
usage: { input_tokens: 1, output_tokens: 1 },
};
})());

vi.mocked(unstable_v2_createSession).mockReturnValue({
send,
stream,
close: vi.fn(),
sessionId: "sdk-session-plan-preserve",
setPermissionMode,
} as any);

({ service } = createService({
onEvent: (event: AgentChatEventEnvelope) => events.push(event),
}));

const session = await service.createSession({
laneId: "lane-1",
provider: "claude",
model: "sonnet",
modelId: "anthropic/claude-sonnet-4-6",
permissionMode: "edit",
claudePermissionMode: "acceptEdits",
});
sessionId = session.id;

const result = await service.runSessionTurn({
sessionId: session.id,
text: "Enter plan mode, then exit it after approval.",
});

expect(result.outputText).toContain("Plan approved and preserved.");
expect(setPermissionMode).toHaveBeenCalledWith("acceptEdits");

const summary = await service.getSessionSummary(session.id);
expect(summary?.permissionMode).toBe("edit");
expect(summary?.claudePermissionMode).toBe("acceptEdits");
});

it("emits todo_update events for Claude TodoWrite tool uses", async () => {
const events: AgentChatEventEnvelope[] = [];
const setPermissionMode = vi.fn().mockResolvedValue(undefined);
Expand Down
Loading
Loading