Skip to content
Draft
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
2 changes: 1 addition & 1 deletion README.ja.md
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ CodeMux はチャットにとどまりません — 開発ワークフローを

- **LAN**: IPアドレスの自動検出 + QRコードで、数秒で準備完了
- **パブリックインターネット**: ワンクリックで [Cloudflare Tunnel](https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/) — ポート転送、VPN、ファイアウォール変更は一切不要。**クイックトンネル**(ランダムな一時URL、設定不要)と**ネームドトンネル**(`~/.cloudflared/` 認証情報による永続カスタムドメイン)の両方をサポート
- **セキュリティ内蔵**: デバイス認証、JWT トークン、Cloudflare 経由のHTTPS; クイックトンネルURLは再起動ごとにローテーション、ネームドトンネルはカスタムホスト名を維持
- **セキュリティ内蔵**: デバイス認証、JWT トークン、Cloudflare 経由のHTTPS; クイックトンネルURLはトンネル自体を作り直したときにローテーションし、ネームドトンネルはカスタムホスト名を維持

#### IM ボットチャネル

Expand Down
2 changes: 1 addition & 1 deletion README.ko.md
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ CodeMux는 채팅을 넘어 — 개발 워크플로를 인터페이스에서 직

- **LAN**: 자동 감지된 IP + QR 코드, 수초 내 준비 완료
- **공용 인터넷**: 원클릭 [Cloudflare Tunnel](https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/) — 포트 포워딩, VPN, 방화벽 변경 불필요. **퀵 터널**(랜덤 임시 URL, 제로 설정)과 **네임드 터널**(`~/.cloudflared/` 인증 정보를 통한 영구 커스텀 도메인) 모두 지원
- **내장 보안**: 기기 인증, JWT 토큰, Cloudflare를 통한 HTTPS; 퀵 터널 URL은 재시작마다 변경, 네임드 터널은 커스텀 호스트명 유지
- **내장 보안**: 기기 인증, JWT 토큰, Cloudflare를 통한 HTTPS; 퀵 터널 URL은 터널 자체를 다시 만들 때 변경되며, 네임드 터널은 커스텀 호스트명을 유지합니다

#### IM 봇 채널

Expand Down
6 changes: 5 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ Access your coding agents from any device — phone, tablet, or another machine

- **LAN**: Auto-detected IP + QR code, ready in seconds
- **Public Internet**: One-click [Cloudflare Tunnel](https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/) — no port forwarding, no VPN, no firewall changes. Supports both **quick tunnels** (random ephemeral URL, zero config) and **named tunnels** (persistent custom domain via `~/.cloudflared/` credentials)
- **Security built-in**: Device authorization, JWT tokens, HTTPS via Cloudflare; quick tunnel URLs rotate on every restart, named tunnels preserve your custom hostname
- **Security built-in**: Device authorization, JWT tokens, HTTPS via Cloudflare; quick tunnel URLs rotate whenever the tunnel itself is recreated, while named tunnels preserve your custom hostname

#### IM Bot Channels

Expand Down Expand Up @@ -201,6 +201,7 @@ bun run server:dev
# Run the same headless dev stack in the background
bun run server:up
bun run server:status
bun run server:restart
bun run server:down

# Run the headless dev stack and start a Cloudflare quick tunnel
Expand All @@ -215,6 +216,8 @@ bun run server:access-requests

`bun run start` is still the lightest option for a web-only standalone server. The desktop app's "Public Access" toggle manages Cloudflare inside the packaged app; on a headless dev server, `bun run server:tunnel` provides the equivalent quick-tunnel workflow from the shell.

If you want to restart CodeMux itself without rotating the current quick-tunnel URL, use `bun run server:restart`. It restarts the managed app process and keeps the existing `cloudflared` process alive whenever possible, so remote browsers can usually stay on the same public origin.


`bun run server:tunnel` now prints the access code after startup. When a remote browser submits that code, you can stay entirely in SSH and run `bun run server:access-requests` to review and interactively approve or deny pending requests. If you started CodeMux with `bun run server:dev`, open a second SSH session and run `bun run server:access-code` / `bun run server:access-requests`.

Expand Down Expand Up @@ -301,6 +304,7 @@ bun run dev # Electron + Vite HMR
bun run server:dev # Foreground headless Electron dev
bun run server:up # Background headless Electron dev
bun run server:tunnel # Background headless Electron dev + quick tunnel
bun run server:restart # Restart app only; preserve managed quick tunnel when possible
bun run server:access-code # Print the current 6-digit access code
bun run server:access-requests # Interactively review pending remote access requests
bun run server:down # Stop background headless Electron dev
Expand Down
2 changes: 1 addition & 1 deletion README.ru.md
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ CodeMux выходит за рамки чата — предоставляет

- **LAN**: Автоматически определённый IP + QR-код, готово за секунды
- **Публичный интернет**: Одним кликом [Cloudflare Tunnel](https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/) — без проброса портов, VPN и изменений файрвола. Поддерживаются как **быстрые туннели** (случайный временный URL, без настройки), так и **именованные туннели** (постоянный пользовательский домен через учётные данные `~/.cloudflared/`)
- **Встроенная безопасность**: Авторизация устройств, JWT-токены, HTTPS через Cloudflare; URL быстрых туннелей меняются при каждом перезапуске, именованные туннели сохраняют ваш домен
- **Встроенная безопасность**: Авторизация устройств, JWT-токены, HTTPS через Cloudflare; URL быстрых туннелей меняются при пересоздании самого туннеля, а именованные туннели сохраняют ваш домен

#### Каналы IM-ботов

Expand Down
2 changes: 1 addition & 1 deletion README.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ CodeMux 不只是聊天 —— 它提供集成工具,让你直接在界面中

- **局域网**:自动检测 IP + 二维码,几秒内即可就绪
- **公网**:一键 [Cloudflare Tunnel](https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/) —— 无需端口转发、无需 VPN、无需防火墙更改。支持**快速隧道**(随机临时 URL,零配置)和**命名隧道**(通过 `~/.cloudflared/` 凭证持久化自定义域名)
- **内置安全机制**:设备授权、JWT 令牌、通过 Cloudflare 的 HTTPS;快速隧道 URL 每次重启时轮换,命名隧道保留你的自定义主机名
- **内置安全机制**:设备授权、JWT 令牌、通过 Cloudflare 的 HTTPS;快速隧道 URL 会在隧道本身被重建时轮换,命名隧道保留你的自定义主机名

#### IM 机器人渠道

Expand Down
12 changes: 11 additions & 1 deletion electron/main/engines/claude/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,8 @@ export class ClaudeCodeAdapter extends EngineAdapter {
private sessionDirectories = new Map<string, string>();
/** Persisted ccSessionId per session, for SDK session resumption across restarts */
private sessionCcIds = new Map<string, string>();
/** Custom system prompts per session (e.g. orchestration instructions for agent team) */
private sessionSystemPrompts = new Map<string, string>();
/** Sessions that were just resumed after a dead process — emit notice on next message */
private pendingResumeNotice = new Set<string>();

Expand Down Expand Up @@ -562,6 +564,9 @@ export class ClaudeCodeAdapter extends EngineAdapter {
if (meta?.ccSessionId && typeof meta.ccSessionId === "string") {
this.sessionCcIds.set(sessionId, meta.ccSessionId);
}
if (meta?.systemPrompt && typeof meta.systemPrompt === "string") {
this.sessionSystemPrompts.set(sessionId, meta.systemPrompt);
}
this.emit("session.created", { session });

// Warm up in background — store the promise so listCommands() can await it.
Expand Down Expand Up @@ -626,6 +631,7 @@ export class ClaudeCodeAdapter extends EngineAdapter {
}

this.sessionDirectories.delete(sessionId);
this.sessionSystemPrompts.delete(sessionId);
this.messageHistory.delete(sessionId);
this.messageBuffers.delete(sessionId);
this.sessionModes.delete(sessionId);
Expand Down Expand Up @@ -1813,11 +1819,15 @@ export class ClaudeCodeAdapter extends EngineAdapter {
// narrower than the internal Options type. The SDK internally passes these
// through to ProcessTransport which accepts all Options fields.

// Build system prompt append: identity + cached user skills
// Build system prompt append: identity + cached user skills + optional custom system prompt
let promptAppend = CODEMUX_IDENTITY_PROMPT;
if (this.cachedSkillNames.length > 0) {
promptAppend += `\n\nThe user has installed the following additional skills (invokable via the Skill tool): ${this.cachedSkillNames.join(", ")}. When the user's request matches one of these skills, use the Skill tool to invoke it.`;
}
const customSystemPrompt = this.sessionSystemPrompts.get(sessionId);
if (customSystemPrompt) {
promptAppend += "\n\n" + customSystemPrompt;
}

const sdkOptions: any = {
model: opts.model ?? this.currentModelId ?? "claude-sonnet-4-20250514",
Expand Down
34 changes: 27 additions & 7 deletions electron/main/engines/codex/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,8 @@ export class CodexAdapter extends EngineAdapter {
private sessionReasoningEfforts = new Map<string, ReasoningEffort>();
private sessionServiceTiers = new Map<string, CodexServiceTier>();
private sessionDirectories = new Map<string, string>();
/** Custom system prompts per session (e.g. orchestration instructions for agent team) */
private sessionSystemPrompts = new Map<string, string>();

private currentModelId: string = CODEX_FALLBACK_MODEL;
private currentMode: string = DEFAULT_MODE_ID;
Expand Down Expand Up @@ -362,17 +364,24 @@ export class CodexAdapter extends EngineAdapter {
await this.start();

const normalizedDirectory = normalizeDirectory(directory);
const customSystemPrompt = (meta?.systemPrompt && typeof meta.systemPrompt === "string") ? meta.systemPrompt : undefined;
const existingThreadId = resolveThreadId(undefined, meta);
const startThread = () =>
customSystemPrompt
? this.startThread(normalizedDirectory, customSystemPrompt)
: this.startThread(normalizedDirectory);
let threadResponse: ThreadResponse;
if (existingThreadId) {
try {
threadResponse = await this.resumeThread(existingThreadId, normalizedDirectory);
threadResponse = customSystemPrompt
? await this.resumeThread(existingThreadId, normalizedDirectory, customSystemPrompt)
: await this.resumeThread(existingThreadId, normalizedDirectory);
} catch (error) {
codexLog.warn(`Failed to resume Codex thread ${existingThreadId}, starting a new one instead:`, error);
threadResponse = await this.startThread(normalizedDirectory);
threadResponse = await startThread();
}
} else {
threadResponse = await this.startThread(normalizedDirectory);
threadResponse = await startThread();
}

const threadId = threadResponse.thread?.id;
Expand All @@ -389,6 +398,9 @@ export class CodexAdapter extends EngineAdapter {
if (!this.sessionDirectories.has(sessionId)) {
this.sessionDirectories.set(sessionId, normalizedDirectory);
}
if (customSystemPrompt) {
this.sessionSystemPrompts.set(sessionId, customSystemPrompt);
}
if (threadResponse.model) {
this.sessionModels.set(sessionId, threadResponse.model);
this.currentModelId = threadResponse.model;
Expand Down Expand Up @@ -1893,16 +1905,19 @@ export class CodexAdapter extends EngineAdapter {
return undefined;
}

private async startThread(directory: string): Promise<ThreadResponse> {
private async startThread(directory: string, customSystemPrompt?: string): Promise<ThreadResponse> {
const modeId = this.currentMode;
const approvalPolicy = clampApprovalPolicy(modeToApprovalPolicy(modeId), this.configRequirements);
const sandboxMode = clampSandboxMode(modeToSandboxMode(modeId), this.configRequirements);
const baseInstructions = customSystemPrompt
? CODEMUX_IDENTITY_PROMPT + "\n\n" + customSystemPrompt
: CODEMUX_IDENTITY_PROMPT;
const response = asRecord(await this.client!.request("thread/start", {
cwd: directory,
model: this.currentModelId,
approvalPolicy,
sandbox: sandboxMode,
baseInstructions: CODEMUX_IDENTITY_PROMPT,
baseInstructions,
serviceName: "codemux",
experimentalRawEvents: false,
persistExtendedHistory: true,
Expand All @@ -1911,21 +1926,25 @@ export class CodexAdapter extends EngineAdapter {
return response;
}

private async resumeThread(threadId: string, directory: string): Promise<ThreadResponse> {
private async resumeThread(threadId: string, directory: string, customSystemPrompt?: string): Promise<ThreadResponse> {
const sessionId = toEngineSessionId(threadId);
const modeId = this.sessionModes.get(sessionId) ?? this.currentMode;
const approvalPolicy = clampApprovalPolicy(modeToApprovalPolicy(modeId), this.configRequirements);
const sandboxMode = clampSandboxMode(modeToSandboxMode(modeId), this.configRequirements);
const modelId = this.sessionModels.get(sessionId) ?? this.currentModelId;
const serviceTier = this.sessionServiceTiers.get(sessionId);
const systemPrompt = customSystemPrompt ?? this.sessionSystemPrompts.get(sessionId);
const baseInstructions = systemPrompt
? CODEMUX_IDENTITY_PROMPT + "\n\n" + systemPrompt
: CODEMUX_IDENTITY_PROMPT;

const response = asRecord(await this.client!.request("thread/resume", {
threadId,
cwd: directory,
model: modelId,
approvalPolicy,
sandbox: sandboxMode,
baseInstructions: CODEMUX_IDENTITY_PROMPT,
baseInstructions,
persistExtendedHistory: true,
...(serviceTier ? { serviceTier } : {}),
})) as ThreadResponse;
Expand Down Expand Up @@ -2171,6 +2190,7 @@ export class CodexAdapter extends EngineAdapter {
this.sessionReasoningEfforts.delete(sessionId);
this.sessionServiceTiers.delete(sessionId);
this.sessionDirectories.delete(sessionId);
this.sessionSystemPrompts.delete(sessionId);
this.rejectQueuedMessagesForSession(sessionId, "Session deleted");
this.rejectPendingForSession(sessionId, "Session deleted");

Expand Down
24 changes: 20 additions & 4 deletions electron/main/engines/copilot/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,8 @@ export class CopilotSdkAdapter extends EngineAdapter {
private sessionTodos = new Map<string, Map<string, { id: string; title: string; status: string }>>();
private allowedAlwaysKinds = new Set<string>();
private cachedCommands: EngineCommand[] = [];
/** Custom system prompt per session (e.g. orchestration instructions for agent team) */
private sessionSystemPrompts = new Map<string, string>();

private messageBuffers = new Map<string, MessageBuffer>();
private messageHistory = new Map<string, UnifiedMessage[]>();
Expand Down Expand Up @@ -297,18 +299,24 @@ export class CopilotSdkAdapter extends EngineAdapter {
}
}

async createSession(directory: string): Promise<UnifiedSession> {
async createSession(directory: string, meta?: Record<string, unknown>): Promise<UnifiedSession> {
this.ensureClient();
const normalizedDir = directory.replaceAll("\\", "/");
const mode = "autopilot";

// Build system message: identity prompt + optional custom system prompt (e.g. orchestration instructions)
let systemContent = CODEMUX_IDENTITY_PROMPT;
if (meta?.systemPrompt && typeof meta.systemPrompt === "string") {
systemContent += "\n\n" + meta.systemPrompt;
}

const config: SessionConfig = {
workingDirectory: directory,
streaming: true,
model: this.currentModelId ?? undefined,
onPermissionRequest: (req, ctx) => this.handlePermissionRequest(req as any, ctx),
onUserInputRequest: (req, ctx) => this.handleUserInputRequest(req as any, ctx),
systemMessage: { mode: "append" as const, content: CODEMUX_IDENTITY_PROMPT },
systemMessage: { mode: "append" as const, content: systemContent },
};

const sdkSession = await this.client!.createSession(config);
Expand All @@ -318,6 +326,9 @@ export class CopilotSdkAdapter extends EngineAdapter {
this.activeSessions.set(sessionId, sdkSession);
this.sessionModes.set(sessionId, mode);
this.sessionDirectories.set(sessionId, directory);
if (meta?.systemPrompt && typeof meta.systemPrompt === "string") {
this.sessionSystemPrompts.set(sessionId, meta.systemPrompt);
}

const now = Date.now();
const session: UnifiedSession = {
Expand Down Expand Up @@ -375,6 +386,7 @@ export class CopilotSdkAdapter extends EngineAdapter {
this.sessionModes.delete(sessionId);
this.sessionDirectories.delete(sessionId);
this.sessionTodos.delete(sessionId);
this.sessionSystemPrompts.delete(sessionId);
}

async sendMessage(
Expand Down Expand Up @@ -990,12 +1002,16 @@ export class CopilotSdkAdapter extends EngineAdapter {

const workingDirectory = directory || this.sessionDirectories.get(sessionId);
const sdkReasoningEffort = this.getSdkReasoningEffort(sessionId);
const customSystemPrompt = this.sessionSystemPrompts.get(sessionId);
const systemContent = customSystemPrompt
? CODEMUX_IDENTITY_PROMPT + "\n\n" + customSystemPrompt
: CODEMUX_IDENTITY_PROMPT;
const config: ResumeSessionConfig = {
streaming: true,
workingDirectory,
model: this.currentModelId ?? undefined,
...(sdkReasoningEffort ? { reasoningEffort: sdkReasoningEffort } : {}),
systemMessage: { mode: "append" as const, content: CODEMUX_IDENTITY_PROMPT },
systemMessage: { mode: "append" as const, content: systemContent },
onPermissionRequest: (req, ctx) => this.handlePermissionRequest(req as any, ctx),
onUserInputRequest: (req, ctx) => this.handleUserInputRequest(req as any, ctx),
};
Expand All @@ -1017,7 +1033,7 @@ export class CopilotSdkAdapter extends EngineAdapter {
workingDirectory,
model: this.currentModelId ?? undefined,
...(sdkReasoningEffort ? { reasoningEffort: sdkReasoningEffort } : {}),
systemMessage: { mode: "append" as const, content: CODEMUX_IDENTITY_PROMPT },
systemMessage: { mode: "append" as const, content: systemContent },
onPermissionRequest: (req, ctx) => this.handlePermissionRequest(req as any, ctx),
onUserInputRequest: (req, ctx) => this.handleUserInputRequest(req as any, ctx),
};
Expand Down
Loading
Loading