Language picker on the onboarding welcome step; agents work in the app language

This commit is contained in:
ZacharyZhang-NY committed 2026-07-07 15:59:16 -04:00
1 parent ea9418a6e0
commit 155c08b16d
4 files changed
+66 -5

No files matched your search

+7 -1
View File
@@ -131,6 +131,7 @@ export class TaskExecutionService {
const connectorNames = new ConnectorGrantsService(this.db) const connectorNames = new ConnectorGrantsService(this.db)
.toolkitsForAgent(orgId, agent.id) .toolkitsForAgent(orgId, agent.id)
.map(connectorDisplayName); .map(connectorDisplayName);
const language = this.appLanguage();
const externalTools = await loadComposioTools(this.db, orgId, agent.id); const externalTools = await loadComposioTools(this.db, orgId, agent.id);
const result = await runAgentLoop( const result = await runAgentLoop(
{ {
@@ -142,7 +143,7 @@ export class TaskExecutionService {
orgId, orgId,
providerId: provider.id, providerId: provider.id,
workspaceId: task.workspace_id, workspaceId: task.workspace_id,
instructions: buildAgentInstructions({ agent, companyName: this.orgName(orgId), isCeo, hasReports, connectorNames }), instructions: buildAgentInstructions({ agent, companyName: this.orgName(orgId), isCeo, hasReports, connectorNames, language }),
initialUserTurn: buildTaskPrompt({ initialUserTurn: buildTaskPrompt({
task, task,
agent, agent,
@@ -418,6 +419,11 @@ export class TaskExecutionService {
return row.name; return row.name;
} }
private appLanguage(): "en" | "zh" {
const row = this.db.prepare("select language from app_state where id = 1").get() as { language: string } | undefined;
return row?.language === "zh" ? "zh" : "en";
}
private completeRun(orgId: string, runId: string, providerRequestId: string): void { private completeRun(orgId: string, runId: string, providerRequestId: string): void {
this.db.prepare(` this.db.prepare(`
update task_runs update task_runs
+5
View File
@@ -78,6 +78,8 @@ export interface AgentInstructionContext {
hasReports: boolean; hasReports: boolean;
/** Display names of the connected toolkits this agent carries in this run. */ /** Display names of the connected toolkits this agent carries in this run. */
connectorNames: string[]; connectorNames: string[];
/** The operator's app language; agents speak it back. */
language: "en" | "zh";
} }
/** /**
@@ -93,6 +95,9 @@ export function buildAgentInstructions(input: AgentInstructionContext): string {
roleVoice(input), roleVoice(input),
"Work like a sharp person: take in what is actually in front of you, decide the smallest real next step, do it, then tell whoever needs to know. Do not narrate busywork.", "Work like a sharp person: take in what is actually in front of you, decide the smallest real next step, do it, then tell whoever needs to know. Do not narrate busywork.",
`Your tools are how you act: hire_agent builds your team, create_task delegates and assigns work by name, update_task_status tracks it, write_deliverable produces real files, create_document writes versioned company documents (PRDs, specs, memos, reports), read_messages catches you up on what teammates said, and send_message is how you talk to everyone.`, `Your tools are how you act: hire_agent builds your team, create_task delegates and assigns work by name, update_task_status tracks it, write_deliverable produces real files, create_document writes versioned company documents (PRDs, specs, memos, reports), read_messages catches you up on what teammates said, and send_message is how you talk to everyone.`,
...(input.language === "zh"
? ["公司的工作语言是简体中文:你写的每一条消息、任务标题与描述、文档和交付物都用中文表达(代码、命令、API 名称等技术标识保持原样)。工具名称和工具参数的字段名仍按定义使用英文。"]
: []),
capabilityBriefing(input), capabilityBriefing(input),
"Talk like a person on a team, not a status bot: address teammates by @name, say what you genuinely think, ask when you are unsure, and reply when someone asks you something. Before starting work, check your messages with read_messages to see if anyone needs you. Post where the work lives — 'engineering', 'product', or otherwise 'general' — and keep a back-and-forth in one thread by passing the thread_id a previous send_message returned. Hand work to a teammate with /handoff @Name and the file_id write_deliverable gives you.", "Talk like a person on a team, not a status bot: address teammates by @name, say what you genuinely think, ask when you are unsure, and reply when someone asks you something. Before starting work, check your messages with read_messages to see if anyone needs you. Post where the work lives — 'engineering', 'product', or otherwise 'general' — and keep a back-and-forth in one thread by passing the thread_id a previous send_message returned. Hand work to a teammate with /handoff @Name and the file_id write_deliverable gives you.",
].join("\n"); ].join("\n");
+24 -4
View File
@@ -3,8 +3,9 @@ import {
PROVIDER_AUTH_TYPE, PROVIDER_AUTH_TYPE,
type AppSnapshot, type AppSnapshot,
type ProviderCatalogEntry, type ProviderCatalogEntry,
type UiLanguage,
} from "../../../shared/domain"; } from "../../../shared/domain";
import { Button, Field, Icon, IconButton, Pill } from "../shared/ui"; import { Button, Field, Icon, IconButton, Pill, Segmented } from "../shared/ui";
import type { SnapshotActions } from "./useSnapshot"; import type { SnapshotActions } from "./useSnapshot";
import { ProviderStep } from "./OnboardingProviderStep"; import { ProviderStep } from "./OnboardingProviderStep";
import { ConnectorsStep } from "./OnboardingConnectorsStep"; import { ConnectorsStep } from "./OnboardingConnectorsStep";
@@ -178,7 +179,11 @@ export function Onboarding({
<form className="onboarding-panel" onSubmit={submit} noValidate> <form className="onboarding-panel" onSubmit={submit} noValidate>
<div key={step} className="step-view"> <div key={step} className="step-view">
{step === "welcome" && ( {step === "welcome" && (
<WelcomeStep onNext={() => setStep("provider")} /> <WelcomeStep
language={snapshot.uiPreferences.language}
onLanguage={(language) => void actions.run(() => window.agentCompany.setUiPreferences({ language }))}
onNext={() => setStep("provider")}
/>
)} )}
{step === "provider" && ( {step === "provider" && (
<ProviderStep <ProviderStep
@@ -231,10 +236,25 @@ interface StepProps {
onNext: () => void; onNext: () => void;
} }
function WelcomeStep({ onNext }: { onNext: () => void }) { function WelcomeStep({
language,
onLanguage,
onNext,
}: {
language: UiLanguage;
onLanguage: (language: UiLanguage) => void;
onNext: () => void;
}) {
return ( return (
<div className="form-grid"> <div className="form-grid">
<h2 className="page-h1">{t("Welcome")}</h2> <div className="row">
<h2 className="page-h1" style={{ margin: 0, flex: 1 }}>{t("Welcome")}</h2>
<Segmented
value={language}
onChange={(next) => { if (next !== language) onLanguage(next); }}
options={[{ id: "en", label: "English" }, { id: "zh", label: "中文" }]}
/>
</div>
<p style={{ color: "var(--text-2)", fontSize: 14, lineHeight: 1.55 }}> <p style={{ color: "var(--text-2)", fontSize: 14, lineHeight: 1.55 }}>
{t("AgentCompany sets up your operating loop in six steps. Bring an API key, pick a runtime for the founding CEO, and we'll wire up the first goal, project, and task.")} {t("AgentCompany sets up your operating loop in six steps. Bring an API key, pick a runtime for the founding CEO, and we'll wire up the first goal, project, and task.")}
</p> </p>
+30
View File
@@ -35,6 +35,7 @@ describe("agent instructions", () => {
isCeo: true, isCeo: true,
hasReports: false, hasReports: false,
connectorNames: ["GitHub", "Slack"], connectorNames: ["GitHub", "Slack"],
language: "en",
}); });
expect(text).toContain("every connected service and every company skill"); expect(text).toContain("every connected service and every company skill");
expect(text).toContain("grant_connector"); expect(text).toContain("grant_connector");
@@ -51,6 +52,7 @@ describe("agent instructions", () => {
isCeo: false, isCeo: false,
hasReports: false, hasReports: false,
connectorNames: ["Stripe"], connectorNames: ["Stripe"],
language: "en",
}); });
expect(text).toContain("Connected external services you carry in this run: Stripe"); expect(text).toContain("Connected external services you carry in this run: Stripe");
expect(text).toContain("composio_ prefix"); expect(text).toContain("composio_ prefix");
@@ -66,11 +68,33 @@ describe("agent instructions", () => {
isCeo: false, isCeo: false,
hasReports: false, hasReports: false,
connectorNames: [], connectorNames: [],
language: "en",
}); });
expect(text).toContain("You currently hold no connector grants"); expect(text).toContain("You currently hold no connector grants");
expect(text).toContain("message the CEO"); expect(text).toContain("message the CEO");
expect(text).not.toContain("grant before you assign"); expect(text).not.toContain("grant before you assign");
}); });
test("Chinese app language directs agents to work in Chinese", () => {
const zh = buildAgentInstructions({
agent: agentRow({}),
companyName: "OrbitOps",
isCeo: false,
hasReports: false,
connectorNames: [],
language: "zh",
});
expect(zh).toContain("公司的工作语言是简体中文");
const en = buildAgentInstructions({
agent: agentRow({}),
companyName: "OrbitOps",
isCeo: false,
hasReports: false,
connectorNames: [],
language: "en",
});
expect(en).not.toContain("简体中文");
});
}); });
describe("agent instruction wiring", () => { describe("agent instruction wiring", () => {
@@ -123,5 +147,11 @@ describe("agent instruction wiring", () => {
expect(capturedInstructions).not.toContain("You run this company"); expect(capturedInstructions).not.toContain("You run this company");
expect(capturedInstructions).toContain("Connected external services you carry in this run: GitHub"); expect(capturedInstructions).toContain("Connected external services you carry in this run: GitHub");
expect(capturedInstructions).toContain("list_connectors"); expect(capturedInstructions).toContain("list_connectors");
// Switching the app language redirects the whole company to Chinese.
organization.updateUiPreferences({ language: "zh" });
connection.db.prepare("update task_runs set state = 'failed' where org_id = ?").run(orgId);
await execution.runTask({ orgId, taskId });
expect(capturedInstructions).toContain("公司的工作语言是简体中文");
}); });
}); });