Merge branch 'main' into merge/main-into-dev

# Conflicts:
#	packages/app/src/components/combined-model-selector.tsx
#	packages/app/src/components/message-input.tsx
#	packages/app/src/components/sidebar-workspace-list.tsx
#	packages/app/src/screens/agent/draft-agent-screen.tsx
#	packages/app/src/screens/settings-screen.tsx
#	packages/desktop/src/main.ts
#	packages/server/src/server/agent/provider-launch-config.ts
#	packages/server/src/server/session.ts
#	packages/server/src/server/session.workspaces.test.ts
#	packages/server/src/shared/messages.ts
#	packages/server/src/utils/checkout-git.ts
This commit is contained in:
Mohamed Boudra
2026-04-10 10:15:49 +07:00
53 changed files with 1187 additions and 237 deletions
@@ -1047,6 +1047,8 @@ export function readEventIdentifiers(message: SDKMessage): EventIdentifiers {
};
}
const claudeDebug = process.env.PASEO_CLAUDE_DEBUG === "1";
export class ClaudeAgentClient implements AgentClient {
readonly provider: "claude" = "claude";
readonly capabilities = CLAUDE_CAPABILITIES;
@@ -2453,15 +2455,17 @@ class ClaudeAgentSession implements AgentSession {
while (!this.closed && this.query === activeQuery) {
try {
for await (const message of activeQuery) {
this.logger.trace(
{
claudeSessionId: this.claudeSessionId,
messageType: message.type,
messageSubtype: "subtype" in message ? message.subtype : undefined,
messageUuid: "uuid" in message ? message.uuid : undefined,
},
"Claude query pump: raw SDK message",
);
if (claudeDebug) {
this.logger.trace(
{
claudeSessionId: this.claudeSessionId,
messageType: message.type,
messageSubtype: "subtype" in message ? message.subtype : undefined,
messageUuid: "uuid" in message ? message.uuid : undefined,
},
"Claude query pump: raw SDK message",
);
}
consecutiveInterruptAbortRecoveries = 0;
if (await this.handleMissingResumedConversation(message, activeQuery)) {
return;
@@ -2537,14 +2541,16 @@ class ClaudeAgentSession implements AgentSession {
const turnId = this.activeForegroundTurnId ?? this.autonomousTurn?.id ?? null;
const identifiers = readEventIdentifiers(message);
this.logger.trace(
{
claudeSessionId: this.claudeSessionId,
messageType: message.type,
turnId,
},
"Claude query pump: SDK message",
);
if (claudeDebug) {
this.logger.trace(
{
claudeSessionId: this.claudeSessionId,
messageType: message.type,
turnId,
},
"Claude query pump: SDK message",
);
}
const messageEvents = this.translateMessageToEvents(message, {
suppressAssistantText: true,
@@ -14,6 +14,14 @@ import {
} from "./test-utils/index.js";
import { getFullAccessConfig, getAskModeConfig } from "./daemon-e2e/agent-configs.js";
import { chunkPcm16, parsePcm16MonoWav, wordSimilarity } from "./test-utils/dictation-e2e.js";
import type {
AgentClient,
AgentPersistenceHandle,
AgentRunResult,
AgentSession,
AgentSessionConfig,
AgentStreamEvent,
} from "./agent/agent-sdk-types.js";
const openaiApiKey = process.env.OPENAI_API_KEY ?? null;
@@ -94,6 +102,116 @@ function waitForSignal<T>(
});
}
class NonPersistentReloadSession implements AgentSession {
readonly provider = "claude" as const;
readonly id = null;
readonly capabilities = {
supportsStreaming: false,
supportsSessionPersistence: true,
supportsDynamicModes: false,
supportsMcpServers: false,
supportsReasoningStream: false,
supportsToolInvocations: false,
} as const;
constructor(private readonly onClose: () => void) {}
async run(): Promise<AgentRunResult> {
return {
sessionId: "non-persistent",
finalText: "",
timeline: [],
};
}
async startTurn(): Promise<{ turnId: string }> {
return { turnId: "non-persistent-turn" };
}
subscribe(_callback: (event: AgentStreamEvent) => void): () => void {
return () => undefined;
}
async *streamHistory(): AsyncGenerator<AgentStreamEvent> {
return;
}
async getRuntimeInfo() {
return {
provider: "claude" as const,
sessionId: null,
model: null,
modeId: null,
};
}
async getAvailableModes(): Promise<[]> {
return [];
}
async getCurrentMode(): Promise<string | null> {
return null;
}
async setMode(_modeId: string): Promise<void> {}
getPendingPermissions() {
return [];
}
async respondToPermission(): Promise<void> {}
describePersistence(): AgentPersistenceHandle | null {
return null;
}
async interrupt(): Promise<void> {}
async close(): Promise<void> {
this.onClose();
}
}
class NonPersistentReloadClient implements AgentClient {
readonly provider = "claude" as const;
readonly capabilities = {
supportsStreaming: false,
supportsSessionPersistence: true,
supportsDynamicModes: false,
supportsMcpServers: false,
supportsReasoningStream: false,
supportsToolInvocations: false,
} as const;
createSessionCalls = 0;
resumeSessionCalls = 0;
closeCalls = 0;
async isAvailable(): Promise<boolean> {
return true;
}
async createSession(_config: AgentSessionConfig): Promise<AgentSession> {
this.createSessionCalls += 1;
return new NonPersistentReloadSession(() => {
this.closeCalls += 1;
});
}
async resumeSession(
_handle: AgentPersistenceHandle,
_overrides?: Partial<AgentSessionConfig>,
): Promise<AgentSession> {
this.resumeSessionCalls += 1;
return new NonPersistentReloadSession(() => {
this.closeCalls += 1;
});
}
async listModels() {
return [];
}
}
describe("daemon client E2E", () => {
let ctx: DaemonTestContext;
@@ -281,6 +399,38 @@ describe("daemon client E2E", () => {
}
}, 120000);
test("refresh_agent rebuilds a live agent even when it has no persistence handle", async () => {
const cwd = tmpCwd();
const client = new NonPersistentReloadClient();
const localCtx = await createDaemonTestContext({
agentClients: {
claude: client,
},
});
try {
const created = await localCtx.client.createAgent({
config: {
provider: "claude",
cwd,
},
});
expect(client.createSessionCalls).toBe(1);
expect(client.resumeSessionCalls).toBe(0);
expect(client.closeCalls).toBe(0);
await localCtx.client.refreshAgent(created.id);
expect(client.createSessionCalls).toBe(2);
expect(client.resumeSessionCalls).toBe(0);
expect(client.closeCalls).toBe(1);
} finally {
await localCtx.cleanup();
rmSync(cwd, { recursive: true, force: true });
}
});
test("resume_agent auto-unarchives archived agents", async () => {
const cwd = tmpCwd();
try {
+1 -1
View File
@@ -50,7 +50,7 @@ const LOG_LEVEL_PRIORITIES: Record<LogLevel, number> = {
const DEFAULT_CONSOLE_LEVEL: LogLevel = "info";
const DEFAULT_CONSOLE_FORMAT: LogFormat = "pretty";
const DEFAULT_FILE_LEVEL: LogLevel = "trace";
const DEFAULT_FILE_LEVEL: LogLevel = "debug";
const DEFAULT_FILE_ROTATE_SIZE = "10m";
const DEFAULT_FILE_ROTATE_MAX_FILES = 2;
const DEFAULT_DAEMON_LOG_FILENAME = "daemon.log";
+1
View File
@@ -217,6 +217,7 @@ function clientSupportsAllProviders(appVersion: string | null): boolean {
function clientSupportsFlexibleEditorIds(appVersion: string | null): boolean {
return isAppVersionAtLeast(appVersion, MIN_VERSION_FLEXIBLE_EDITOR_IDS);
}
const WORKSPACE_GIT_WATCH_DEBOUNCE_MS = 500;
const WORKSPACE_GIT_WATCH_REMOVED_FINGERPRINT = "__removed__";
const TERMINAL_STREAM_HIGH_WATER_BYTES = 256 * 1024;
+1
View File
@@ -1130,6 +1130,7 @@ export const WorkspaceSetupStatusRequestSchema = z.object({
requestId: z.string(),
});
// TODO(2026-07): Remove once most clients are on >=0.1.50 and support arbitrary editor ids.
export const LEGACY_EDITOR_TARGET_IDS = [
"cursor",
@@ -1946,6 +1946,7 @@ const ReviewDecisionSchema = z
type CheckRunNode = z.infer<typeof CheckRunNodeSchema>;
type StatusContextNode = z.infer<typeof StatusContextNodeSchema>;
async function resolveGhPath(): Promise<string> {
if (cachedGhPath === undefined) {
cachedGhPath = await findExecutable("gh");