Q174: choosing a language no longer restarts the app; the restart waits for the user
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
3f14e3f706
commit
dc3127c26f
9 files changed
+95
-27
No files matched your search
@@ -13,6 +13,8 @@ const noInput = z.undefined();
|
|||||||
/** The runtime contract of each IPC call; a method without a schema, or a schema that disagrees with its type, fails the typecheck. */
|
/** The runtime contract of each IPC call; a method without a schema, or a schema that disagrees with its type, fails the typecheck. */
|
||||||
export const IPC_CONTRACTS: { [K in InvokedMethod]: z.ZodType<InputOf<K>> } = {
|
export const IPC_CONTRACTS: { [K in InvokedMethod]: z.ZodType<InputOf<K>> } = {
|
||||||
loadSnapshot: noInput,
|
loadSnapshot: noInput,
|
||||||
|
relaunchApp: noInput,
|
||||||
|
runningSessionCount: noInput,
|
||||||
pickDirectory: noInput,
|
pickDirectory: noInput,
|
||||||
listConnectorCatalog: noInput,
|
listConnectorCatalog: noInput,
|
||||||
windowMinimize: noInput,
|
windowMinimize: noInput,
|
||||||
|
|||||||
@@ -140,18 +140,16 @@ export function registerIpcHandlers(deps: IpcDeps): void {
|
|||||||
|
|
||||||
handle("createOrganization", (input) => mutate(() => organization.createOrganization(input)));
|
handle("createOrganization", (input) => mutate(() => organization.createOrganization(input)));
|
||||||
handle("setActiveOrganization", (input) => mutate(() => organization.setActiveOrganization(input.orgId)));
|
handle("setActiveOrganization", (input) => mutate(() => organization.setActiveOrganization(input.orgId)));
|
||||||
handle("setUiPreferences", (input) => {
|
handle("setUiPreferences", (input) => mutate(() => organization.updateUiPreferences(input)));
|
||||||
const before = snapshot.load().uiPreferences.language;
|
handle("runningSessionCount", () => ok((deps.connection.db.prepare("select count(*) as n from task_runs where state = 'running'").get() as { n: number }).n));
|
||||||
const result = mutate(() => organization.updateUiPreferences(input));
|
// Chromium's locale is fixed at launch; the reply goes out before the process exits.
|
||||||
if (result.ok && result.data.uiPreferences.language !== before) {
|
handle("relaunchApp", () => {
|
||||||
// Chromium's locale is fixed at launch; a language change needs a
|
console.info("[app] relaunch requested by the user");
|
||||||
// fresh process to take effect.
|
setTimeout(() => {
|
||||||
setTimeout(() => {
|
app.relaunch();
|
||||||
app.relaunch();
|
app.quit();
|
||||||
app.quit();
|
}, 150);
|
||||||
}, 150);
|
return ok(null);
|
||||||
}
|
|
||||||
return result;
|
|
||||||
});
|
});
|
||||||
handle("deleteOrganization", (input) => mutate(() => organization.deleteOrganization(input.orgId)));
|
handle("deleteOrganization", (input) => mutate(() => organization.deleteOrganization(input.orgId)));
|
||||||
handle("createAgent", (input) => mutate(() => organization.createAgent(input)));
|
handle("createAgent", (input) => mutate(() => organization.createAgent(input)));
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ function invoke<T>(channel: string, payload?: unknown): Promise<T> {
|
|||||||
|
|
||||||
const api: DesktopApi = {
|
const api: DesktopApi = {
|
||||||
loadSnapshot: () => invoke(IPC_CHANNELS.loadSnapshot),
|
loadSnapshot: () => invoke(IPC_CHANNELS.loadSnapshot),
|
||||||
|
relaunchApp: () => invoke(IPC_CHANNELS.relaunchApp),
|
||||||
|
runningSessionCount: () => invoke(IPC_CHANNELS.runningSessionCount),
|
||||||
bootstrapOnboarding: (input) => invoke(IPC_CHANNELS.bootstrapOnboarding, input),
|
bootstrapOnboarding: (input) => invoke(IPC_CHANNELS.bootstrapOnboarding, input),
|
||||||
createOrganization: (input) => invoke(IPC_CHANNELS.createOrganization, input),
|
createOrganization: (input) => invoke(IPC_CHANNELS.createOrganization, input),
|
||||||
setActiveOrganization: (input) => invoke(IPC_CHANNELS.setActiveOrganization, input),
|
setActiveOrganization: (input) => invoke(IPC_CHANNELS.setActiveOrganization, input),
|
||||||
|
|||||||
@@ -696,6 +696,12 @@ export const zh: Record<string, string> = {
|
|||||||
"Resets {date} (UTC)": "{date} 重置(UTC)",
|
"Resets {date} (UTC)": "{date} 重置(UTC)",
|
||||||
"Unsaved": "未保存",
|
"Unsaved": "未保存",
|
||||||
"Working language": "工作语言",
|
"Working language": "工作语言",
|
||||||
|
"{language} applies after a restart.": "{language} 将在重启后生效。",
|
||||||
|
"Restart now": "立即重启",
|
||||||
|
"Restart to switch language?": "重启以切换语言?",
|
||||||
|
"Text you typed but have not sent or saved is lost.": "已输入但尚未发送或保存的文字会丢失。",
|
||||||
|
"Running agent sessions: {count}. They stop now; their tasks stay open and start a new session when next queued, by heartbeat or Run, and a chat reply in progress is not sent.": "运行中的 Agent 会话:{count}。它们会立即停止;其任务保持未完成,并在下次通过心跳或“运行”排队时开始新会话,进行中的聊天回复不会发出。",
|
||||||
|
"Counting running agent sessions…": "正在统计运行中的 Agent 会话…",
|
||||||
"Company default ({language})": "公司默认({language})",
|
"Company default ({language})": "公司默认({language})",
|
||||||
"Agents' working language": "Agent 工作语言",
|
"Agents' working language": "Agent 工作语言",
|
||||||
"Agents write messages, tasks, documents and deliverables in it. The display language does not change it, and a task can set its own.": "Agent 用它撰写消息、任务、文档和交付物。显示语言不会改变它,单个任务也可以单独设置。",
|
"Agents write messages, tasks, documents and deliverables in it. The display language does not change it, and a task can set its own.": "Agent 用它撰写消息、任务、文档和交付物。显示语言不会改变它,单个任务也可以单独设置。",
|
||||||
|
|||||||
@@ -0,0 +1,58 @@
|
|||||||
|
import { useState } from "react";
|
||||||
|
import type { UiLanguage, UiPreferences } from "../../../../shared/domain";
|
||||||
|
import type { SnapshotActions } from "../../app/useSnapshot";
|
||||||
|
import { Dialog } from "../../shared/Dialog";
|
||||||
|
import { Button, Field } from "../../shared/ui";
|
||||||
|
import { t } from "../../i18n/i18n";
|
||||||
|
|
||||||
|
const NAMES: Record<UiLanguage, string> = { en: "English", zh: "中文" };
|
||||||
|
|
||||||
|
/** The display language is fixed per launch, so a choice waits for a restart the user starts after seeing what it interrupts. */
|
||||||
|
export function LanguageField({ uiPreferences, actions }: { uiPreferences: UiPreferences; actions: SnapshotActions }) {
|
||||||
|
const [confirming, setConfirming] = useState(false);
|
||||||
|
const [running, setRunning] = useState<number | null>(null);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
const confirm = () => {
|
||||||
|
setRunning(null);
|
||||||
|
setError(null);
|
||||||
|
setConfirming(true);
|
||||||
|
void window.agentCompany.runningSessionCount().then((result) => {
|
||||||
|
if (result.ok) setRunning(result.data);
|
||||||
|
else setError(result.error.message);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
const pending = uiPreferences.language !== window.agentCompany.language;
|
||||||
|
const restart = () => {
|
||||||
|
setError(null);
|
||||||
|
void window.agentCompany.relaunchApp().then((result) => { if (!result.ok) setError(result.error.message); });
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<Field label={t("Language")}>
|
||||||
|
<select
|
||||||
|
className="input"
|
||||||
|
aria-label={t("Language")}
|
||||||
|
value={uiPreferences.language}
|
||||||
|
onChange={(event) => void actions.run(() => window.agentCompany.setUiPreferences({ language: event.target.value as UiLanguage }))}
|
||||||
|
>
|
||||||
|
<option value="en">{NAMES.en}</option>
|
||||||
|
<option value="zh">{NAMES.zh}</option>
|
||||||
|
</select>
|
||||||
|
<span style={{ fontSize: 12, color: "var(--text-3)" }}>{t("Changes only what you see; agents keep the company's working language.")}</span>
|
||||||
|
{pending && (
|
||||||
|
<span className="row" style={{ gap: 8, fontSize: 12 }} role="status">
|
||||||
|
{t("{language} applies after a restart.", { language: NAMES[uiPreferences.language] })}
|
||||||
|
<Button size="small" onClick={confirm}>{t("Restart now")}</Button>
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
<Dialog title={t("Restart to switch language?")} open={confirming} submitLabel={t("Restart now")} onClose={() => setConfirming(false)} onSubmit={(event) => { event.preventDefault(); restart(); }}>
|
||||||
|
<ul style={{ margin: 0, paddingLeft: 18, lineHeight: 1.6, fontSize: 13 }}>
|
||||||
|
<li>{t("Text you typed but have not sent or saved is lost.")}</li>
|
||||||
|
<li>{running === null
|
||||||
|
? t("Counting running agent sessions…")
|
||||||
|
: t("Running agent sessions: {count}. They stop now; their tasks stay open and start a new session when next queued, by heartbeat or Run, and a chat reply in progress is not sent.", { count: running })}</li>
|
||||||
|
</ul>
|
||||||
|
{error && <div className="form-error" role="alert">{error}</div>}
|
||||||
|
</Dialog>
|
||||||
|
</Field>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { LanguageField } from "./LanguageField";
|
||||||
import { WorkspaceRow } from "./WorkspaceRow";
|
import { WorkspaceRow } from "./WorkspaceRow";
|
||||||
import { CompanyDialog } from "./CompanyDialog";
|
import { CompanyDialog } from "./CompanyDialog";
|
||||||
import { ScopeNote } from "./ScopeNote";
|
import { ScopeNote } from "./ScopeNote";
|
||||||
@@ -5,7 +6,6 @@ import { useState, type FormEvent } from "react";
|
|||||||
import type {
|
import type {
|
||||||
OrganizationRecord,
|
OrganizationRecord,
|
||||||
ProjectRecord,
|
ProjectRecord,
|
||||||
UiLanguage,
|
|
||||||
UiPreferences,
|
UiPreferences,
|
||||||
UiTheme,
|
UiTheme,
|
||||||
WorkspaceRecord,
|
WorkspaceRecord,
|
||||||
@@ -102,20 +102,7 @@ export function WorkspacePanel({
|
|||||||
<option value="dark">{t("Dark")}</option>
|
<option value="dark">{t("Dark")}</option>
|
||||||
</select>
|
</select>
|
||||||
</Field>
|
</Field>
|
||||||
<Field label={t("Language")}>
|
<LanguageField uiPreferences={uiPreferences} actions={actions} />
|
||||||
<select
|
|
||||||
className="input"
|
|
||||||
aria-label={t("Language")}
|
|
||||||
value={uiPreferences.language}
|
|
||||||
onChange={(event) =>
|
|
||||||
void actions.run(() => window.agentCompany.setUiPreferences({ language: event.target.value as UiLanguage }))
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<option value="en">English</option>
|
|
||||||
<option value="zh">中文</option>
|
|
||||||
</select>
|
|
||||||
<span style={{ fontSize: 12, color: "var(--text-3)" }}>{t("Changes only what you see; agents keep the company's working language.")}</span>
|
|
||||||
</Field>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="row reveal-row" style={{ marginBottom: 8 }}>
|
<div className="row reveal-row" style={{ marginBottom: 8 }}>
|
||||||
|
|||||||
@@ -120,6 +120,10 @@ export type ModelCheckResult = { passed: true } | { passed: false; detail: strin
|
|||||||
|
|
||||||
export interface DesktopApi {
|
export interface DesktopApi {
|
||||||
loadSnapshot(): Promise<DesktopResult<AppSnapshot>>;
|
loadSnapshot(): Promise<DesktopResult<AppSnapshot>>;
|
||||||
|
/** Restarts the app, which a display-language change needs; the user asks for it after seeing what it interrupts. */
|
||||||
|
relaunchApp(): Promise<DesktopResult<null>>;
|
||||||
|
/** Every agent session running now, in every company: what a restart interrupts. */
|
||||||
|
runningSessionCount(): Promise<DesktopResult<number>>;
|
||||||
bootstrapOnboarding(input: BootstrapOnboardingInput): Promise<DesktopResult<BootstrapOnboardingSummary>>;
|
bootstrapOnboarding(input: BootstrapOnboardingInput): Promise<DesktopResult<BootstrapOnboardingSummary>>;
|
||||||
setActiveOrganization(input: SetActiveOrganizationInput): Promise<DesktopResult<AppSnapshot>>;
|
setActiveOrganization(input: SetActiveOrganizationInput): Promise<DesktopResult<AppSnapshot>>;
|
||||||
setUiPreferences(input: SetUiPreferencesInput): Promise<DesktopResult<AppSnapshot>>;
|
setUiPreferences(input: SetUiPreferencesInput): Promise<DesktopResult<AppSnapshot>>;
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
export const IPC_CHANNELS = {
|
export const IPC_CHANNELS = {
|
||||||
loadSnapshot: "agentcompany:load-snapshot",
|
loadSnapshot: "agentcompany:load-snapshot",
|
||||||
|
relaunchApp: "agentcompany:relaunch-app",
|
||||||
|
runningSessionCount: "agentcompany:running-session-count",
|
||||||
bootstrapOnboarding: "agentcompany:bootstrap-onboarding",
|
bootstrapOnboarding: "agentcompany:bootstrap-onboarding",
|
||||||
setActiveOrganization: "agentcompany:set-active-organization",
|
setActiveOrganization: "agentcompany:set-active-organization",
|
||||||
setUiPreferences: "agentcompany:set-ui-preferences",
|
setUiPreferences: "agentcompany:set-ui-preferences",
|
||||||
|
|||||||
@@ -76,6 +76,15 @@ test("the pinned CEO conversation is the Messages front door", async () => {
|
|||||||
await page.getByRole("button", { name: "Settings" }).click();
|
await page.getByRole("button", { name: "Settings" }).click();
|
||||||
await expect(page.getByRole("note").filter({ hasText: "Applies to FlowPilot only." })).toBeVisible();
|
await expect(page.getByRole("note").filter({ hasText: "Applies to FlowPilot only." })).toBeVisible();
|
||||||
await expect(page.getByRole("note").filter({ hasText: "Applies to every company in this app." })).toBeVisible();
|
await expect(page.getByRole("note").filter({ hasText: "Applies to every company in this app." })).toBeVisible();
|
||||||
|
// Picking a language does not restart the app; the restart waits for the user, after saying what it interrupts.
|
||||||
|
await page.getByLabel("Language", { exact: true }).selectOption("zh");
|
||||||
|
await expect(page.getByRole("status").filter({ hasText: "中文 applies after a restart." })).toBeVisible();
|
||||||
|
await page.getByRole("button", { name: "Restart now" }).click();
|
||||||
|
await expect(page.locator(".modal")).toContainText("Text you typed but have not sent or saved is lost.");
|
||||||
|
await expect(page.locator(".modal")).toContainText("Running agent sessions: 0. They stop now");
|
||||||
|
await page.locator(".modal").getByRole("button", { name: "Cancel" }).click();
|
||||||
|
await page.getByLabel("Language", { exact: true }).selectOption("en");
|
||||||
|
await expect(page.getByRole("status").filter({ hasText: "applies after a restart." })).toHaveCount(0);
|
||||||
// How the company runs can be changed after onboarding.
|
// How the company runs can be changed after onboarding.
|
||||||
const editCompany = async (mode: string, autoHires: boolean) => {
|
const editCompany = async (mode: string, autoHires: boolean) => {
|
||||||
await page.getByRole("button", { name: "Edit company" }).click();
|
await page.getByRole("button", { name: "Edit company" }).click();
|
||||||
|
|||||||
Reference in new issue
Block a user