+ Ghost buttons are the primary style — invisible until interacted with. On hover, text shifts to Interaction Blue (#1863dc) with 0.8 opacity. Focus: 2px solid Interaction Blue outline.
+
+
+
+
+
+
+
+
04 / Card Examples
+
Cards & Containers
+
+
+
+
STANDARD CARD
+
Signature 22px Radius
+
The distinctive Cohere card with 22px border-radius and #333 border on dark surfaces. Creates cloud-like, organic softness on the dark canvas.
+
+
+
DIALOG CARD
+
Dialog 8px Radius
+
Modal and dialog containers use a tighter 8px radius with #333 border. Functional, secondary containment for transient UI.
+
+
+
GENEROUS CARD
+
Featured 16px Radius
+
Medium-sized containers for featured content. Uses 16px radius as a middle step in the radius scale with subtle border.
+
+
+
+
+
+
+
+
+
05 / Spacing Scale
+
Spacing System
+
Base unit: 8px. Scale from 2px to 60px.
+
+
+
2px
+
6px
+
8px
+
10px
+
12px
+
16px
+
20px
+
22px
+
24px
+
28px
+
32px
+
40px
+
56px
+
60px
+
+
+
+
+
+
+
+
06 / Border Radius Scale
+
Border Radius
+
+
+
4px
Sharp — nav, tags
+
8px
Comfortable — dialogs
+
16px
Generous — featured
+
20px
Large — feature cards
+
22px
Signature — THE radius
+
9999px
Pill — buttons, tags
+
+
+
+
+
+
+
+
07 / Elevation & Depth
+
Depth & Elevation
+
+
+
+
Flat
No shadow, no border. Page background and text blocks.
+
Level 0
+
+
+
Bordered (Subtle)
1px solid #2a2a2f. Subtle dark containment lines.
+
Level 1 — Subtle
+
+
+
Bordered (Standard)
1px solid #333. Standard dark-mode card borders.
+
Level 1 — Standard
+
+
+
Purple Band
Full-width dark purple background. Hero sections, showcases.
+
Level 2 — Dramatic
+
+
+
+
+ Cohere is nearly shadow-free. Depth is communicated through background color contrast (elevated cards on dark canvas), border containment, and dramatic light-to-dark section alternation.
+
+ Ghost buttons are the primary style — invisible until interacted with. On hover, text shifts to Interaction Blue (#1863dc) with 0.8 opacity. Focus: 2px solid Interaction Blue outline.
+
+
+
+
+
+
+
+
04 / Card Examples
+
Cards & Containers
+
+
+
+
STANDARD CARD
+
Signature 22px Radius
+
The distinctive Cohere card with 22px border-radius and 1px solid #f2f2f2 border. Creates cloud-like, organic softness on the white canvas.
+
+
+
DIALOG CARD
+
Dialog 8px Radius
+
Modal and dialog containers use a tighter 8px radius with #d9d9dd border. Functional, secondary containment for transient UI.
+
+
+
GENEROUS CARD
+
Featured 16px Radius
+
Medium-sized containers for featured content. Uses 16px radius as a middle step in the radius scale with subtle #f2f2f2 border.
+
+
+
+
+
+
+
+
+
05 / Spacing Scale
+
Spacing System
+
Base unit: 8px. Scale from 2px to 60px.
+
+
+
2px
+
6px
+
8px
+
10px
+
12px
+
16px
+
20px
+
22px
+
24px
+
28px
+
32px
+
40px
+
56px
+
60px
+
+
+
+
+
+
+
+
06 / Border Radius Scale
+
Border Radius
+
+
+
4px
Sharp — nav, tags
+
8px
Comfortable — dialogs
+
16px
Generous — featured
+
20px
Large — feature cards
+
22px
Signature — THE radius
+
9999px
Pill — buttons, tags
+
+
+
+
+
+
+
+
07 / Elevation & Depth
+
Depth & Elevation
+
+
+
+
Flat
No shadow, no border. Page background and text blocks.
+
Level 0
+
+
+
Bordered (Light)
1px solid #f2f2f2. Standard cards, subtle containment.
+
Level 1 — Lightest Gray
+
+
+
Bordered (Cool)
1px solid #d9d9dd. Emphasized cards, list separators.
+
Level 1 — Border Cool
+
+
+
Purple Band
Full-width dark purple background. Hero sections, showcases.
+
Level 2 — Dramatic
+
+
+
+
+ Cohere is nearly shadow-free. Depth is communicated through background color contrast (white cards on purple bands), border containment (cool gray borders), and dramatic light-to-dark section alternation.
+
+
+
+
+
+
+
diff --git a/package.json b/package.json
index 2200c8cf..338660a9 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "agent-company",
- "version": "0.4.156",
+ "version": "0.4.165",
"private": true,
"type": "module",
"description": "Cross-platform Electron desktop control plane for local-first AI agent companies.",
diff --git a/src/main/automation/automation-actions.ts b/src/main/automation/automation-actions.ts
index 5dbd4d58..f6ac23e3 100644
--- a/src/main/automation/automation-actions.ts
+++ b/src/main/automation/automation-actions.ts
@@ -247,25 +247,9 @@ export function executeAutomationAction(
}
} else {
// No manager found — agent is likely the CEO or top-level.
- // Post as company-wide message so the board and other leaders see it.
- const escalationResult = dispatchAgentMessage(
- { db, emitEvent, publishDomainChanged, wakeAgentIfPossible },
- {
- companyId: ctx.companyId,
- fromAgentId: targetAgentId,
- toAgentId: null,
- channel: "company",
- subject: `Escalation: ${taskTitle}`,
- body: escalationBody,
- priority: "urgent",
- },
- wakeTrigger,
- );
- if (!escalationResult.ok) {
- logger.warn(
- `[automation] Failed to escalate "${taskTitle}" to company channel: ${escalationResult.error.code} ${escalationResult.error.message}`,
- );
- }
+ // Skip posting to company channel to avoid flooding it with system noise.
+ // The task status change is already visible in the task list and activity log.
+ logger.info(`[automation] Skipping escalation for top-level agent "${agentName}" — no manager to escalate to.`);
}
break;
}
diff --git a/src/main/database-base.ts b/src/main/database-base.ts
index 8e9ecdb9..036b690a 100644
--- a/src/main/database-base.ts
+++ b/src/main/database-base.ts
@@ -168,6 +168,7 @@ export class AppDatabaseBase {
canOpenPath(targetPath: string) { return this.settings.canOpenPath(targetPath); }
getDisabledModels(connectorId: string) { return this.settings.getDisabledModels(connectorId); }
setDisabledModels(connectorId: string, models: Array<{ id: string; label: string }>) { this.settings.setDisabledModels(connectorId, models); }
+ hasModelPoolSettings(connectorId: string) { return this.settings.hasModelPoolSettings(connectorId); }
getLastBudgetResetMonth(companyId: string) { return this.settings.getLastBudgetResetMonth(companyId); }
setLastBudgetResetMonth(companyId: string, month: string) { this.settings.setLastBudgetResetMonth(companyId, month); }
diff --git a/src/main/database/connectors-crud.ts b/src/main/database/connectors-crud.ts
index f27a4ad6..9a5ec51f 100644
--- a/src/main/database/connectors-crud.ts
+++ b/src/main/database/connectors-crud.ts
@@ -90,6 +90,53 @@ export function buildConnectorMethods(
const currentRows = readAll>(
"select id, label, description, status, command, version, auth_state as authState, last_checked_at as lastCheckedAt, last_error as lastError, model, env_binding_text as envBindingText, notes from connectors",
);
+ // Migrate first compat slot if its definition defaults have changed (edition upgrade).
+ // When the definition ships with preset notes that differ from the DB, relocate the
+ // old user config to the next empty compat slot so definition defaults take effect.
+ const firstCompat = currentRows.find((r) => r.id === "claude_compatible");
+ if (firstCompat?.envBindingText && firstCompat.notes) {
+ let firstDef: { notes: string } | undefined;
+ try { firstDef = getConnectorDefinition("claude_compatible"); } catch { /* skip */ }
+ if (firstDef?.notes && firstCompat.notes !== firstDef.notes) {
+ const migrationTargets: ConnectorId[] = [
+ "claude_compat_2", "claude_compat_3", "claude_compat_4", "claude_compat_5",
+ "claude_compat_6", "claude_compat_7", "claude_compat_8", "claude_compat_9", "claude_compat_10",
+ ];
+ const occupied = new Set(
+ currentRows
+ .filter((r) => r.id !== "claude_compatible" && r.id?.startsWith("claude_compat") && r.envBindingText)
+ .map((r) => r.id),
+ );
+ const targetId = migrationTargets.find((id) => !occupied.has(id));
+ if (targetId) {
+ const migrated = {
+ envBindingText: firstCompat.envBindingText,
+ notes: firstCompat.notes,
+ model: firstCompat.model,
+ status: firstCompat.status,
+ authState: firstCompat.authState,
+ version: firstCompat.version,
+ lastCheckedAt: firstCompat.lastCheckedAt,
+ lastError: firstCompat.lastError,
+ };
+ const targetRow = currentRows.find((r) => r.id === targetId);
+ if (targetRow) {
+ Object.assign(targetRow, migrated);
+ } else {
+ currentRows.push({ id: targetId as ConnectorId, ...migrated });
+ }
+ firstCompat.envBindingText = undefined;
+ firstCompat.notes = undefined;
+ firstCompat.model = undefined;
+ firstCompat.status = undefined;
+ firstCompat.authState = undefined;
+ firstCompat.version = undefined;
+ firstCompat.lastCheckedAt = undefined;
+ firstCompat.lastError = undefined;
+ }
+ }
+ }
+
const merged = buildDefaultConnectorRows(currentRows);
const statement = getDb().prepare(
"insert or replace into connectors (id, label, description, status, command, version, auth_state, last_checked_at, last_error, model, env_binding_text, notes, updated_at) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
@@ -102,6 +149,35 @@ export function buildConnectorMethods(
row.lastError, row.model, row.envBindingText, row.notes, updatedAt,
);
}
+
+ // One-time migration: mark native connectors that are actively used by
+ // agents as model-pool-managed so they remain visible to the CEO after
+ // the opt-in policy change. Connectors not used by any agent stay
+ // unmanaged and will not appear in CEO prompts until the user explicitly
+ // enables models for them in Settings > Model Management.
+ const migrationKey = "migration:model_pool_opt_in_v1";
+ const migrated = readAll<{ value: string }>("SELECT value FROM settings WHERE key = ?", migrationKey);
+ if (migrated.length === 0) {
+ const usedIds = new Set(
+ readAll<{ connector_id: string }>(
+ "SELECT DISTINCT connector_id FROM agents WHERE status NOT IN ('terminated')",
+ ).map((r) => r.connector_id),
+ );
+ const nativeIds = ["codex_local", "claude_local", "gemini_local"];
+ const upsert = getDb().prepare(
+ "INSERT INTO settings (key, value) VALUES (?, ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value",
+ );
+ for (const id of nativeIds) {
+ const hasSettings = readAll<{ key: string }>(
+ "SELECT key FROM settings WHERE key = ?",
+ `disabled_models:${id}`,
+ );
+ if (hasSettings.length === 0 && usedIds.has(id)) {
+ upsert.run(`disabled_models:${id}`, "[]");
+ }
+ }
+ upsert.run(migrationKey, "1");
+ }
},
};
}
diff --git a/src/main/database/settings.ts b/src/main/database/settings.ts
index 54f68810..001d9d0b 100644
--- a/src/main/database/settings.ts
+++ b/src/main/database/settings.ts
@@ -84,11 +84,11 @@ export function buildSettingsMethods(
},
setDisabledModels(connectorId: string, models: Array<{ id: string; label: string }>) {
- if (models.length === 0) {
- getDb().prepare("delete from settings where key = ?").run(`disabled_models:${connectorId}`);
- } else {
- setSetting(`disabled_models:${connectorId}`, JSON.stringify(models));
- }
+ setSetting(`disabled_models:${connectorId}`, JSON.stringify(models));
+ },
+
+ hasModelPoolSettings(connectorId: string): boolean {
+ return getSetting(`disabled_models:${connectorId}`) !== null;
},
getLastBudgetResetMonth(companyId: string): string | null {
diff --git a/src/main/orchestration/turn-context.ts b/src/main/orchestration/turn-context.ts
index 2f690e6a..14b8314e 100644
--- a/src/main/orchestration/turn-context.ts
+++ b/src/main/orchestration/turn-context.ts
@@ -237,12 +237,16 @@ export function buildRunPayload(params: RunDispatchParams, deps: TurnContextDeps
recentDocuments: snapshot.documents
.filter((document) => document.companyId === companyId && (document.authorAgentId === agent.id || document.reviewerAgentId === agent.id || document.status === "in_review"))
.slice(0, 5),
- allConnectors: snapshot.connectors.map((c) => ({
- id: c.id,
- label: c.label,
- status: c.status,
- enabledModels: resolveEnabledConnectorModels(db, c),
- })),
+ allConnectors: snapshot.connectors
+ .filter((c) => c.status === "ready")
+ .filter((c) => c.id.startsWith("claude_compat") || db.hasModelPoolSettings(c.id))
+ .map((c) => ({
+ id: c.id,
+ label: c.label,
+ status: c.status,
+ enabledModels: resolveEnabledConnectorModels(db, c),
+ }))
+ .filter((c) => c.enabledModels.length > 0),
sessionHandoff,
skillNames,
skillEntries: skills.map((s) => ({
diff --git a/src/main/prompt-builder/system-prompt.ts b/src/main/prompt-builder/system-prompt.ts
index 0632b528..f0400cbf 100644
--- a/src/main/prompt-builder/system-prompt.ts
+++ b/src/main/prompt-builder/system-prompt.ts
@@ -381,7 +381,11 @@ Always verify API responses before reporting — only report successful actions.
// Reinforce locale at the end of prompt (recency bias helps LLMs follow this)
if (localeInstruction) {
- sections.push(`# 语言要求提醒\n${localeInstruction}\n请记住:你的所有输出必须使用中文。\n`);
+ if ((ctx.locale ?? "en") === "zh") {
+ sections.push(`# 语言要求提醒\n${localeInstruction}\n请记住:你的所有输出必须使用中文。\n`);
+ } else {
+ sections.push(`# Language Requirement Reminder\n${localeInstruction}\nRemember: ALL your output must be in English.\n`);
+ }
}
return sections.join("\n");
diff --git a/src/shared/localization.ts b/src/shared/localization.ts
index a366ed50..28c59c4d 100644
--- a/src/shared/localization.ts
+++ b/src/shared/localization.ts
@@ -3,7 +3,7 @@ import type { AppLocale } from "./locale";
/**
* Returns a locale-appropriate system prompt instruction for AI agents.
* When locale is "zh", the instruction tells the agent to respond entirely in Chinese.
- * For "en" (or any other locale), returns an empty string (no additional instruction needed).
+ * When locale is "en", the instruction tells the agent to respond entirely in English.
*/
export function getLocalePromptInstruction(locale: AppLocale): string {
if (locale === "zh") {
@@ -14,5 +14,10 @@ export function getLocalePromptInstruction(locale: AppLocale): string {
"不要使用英文回复,除非是代码、变量名、API 端点等技术术语。",
].join("\n");
}
- return "";
+ return [
+ "Important language requirement: You MUST communicate entirely in English.",
+ "This includes: task descriptions, comments, messages, reports, documents, approvals, notifications — ALL output must be in English.",
+ "Even if some existing content or context is in another language, all your responses and created content must be in English.",
+ "Do not respond in other languages, except for code, variable names, API endpoints, and other technical terms.",
+ ].join("\n");
}