Q178: the Chinese UI labels stored values and sign-in prompts, and shortcuts follow the platform

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
ZacharyZhang-NYandClaude Opus 5.5 committed 2026-10-09 05:42:45 -04:00
1 parent f79e3d4d7c
commit 1cc67c244e
28 files changed
+158 -52

No files matched your search

+2 -2
View File
@@ -294,10 +294,10 @@ export function registerIpcHandlers(deps: IpcDeps): void {
const secret = await providerOAuth.run(
{ providerKey: input.providerKey, enterpriseDomain: input.enterpriseDomain ?? null },
{
onAuth: ({ url, instructions }) => {
onAuth: ({ url, userCode }) => {
const browserUrl = normalizeExternalBrowserUrl(url);
void shell.openExternal(browserUrl);
emit({ phase: "authorize", url: browserUrl, instructions });
emit({ phase: "authorize", url: browserUrl, userCode });
},
onProgress: (message) => emit({ phase: "progress", message }),
},
@@ -75,7 +75,7 @@ export const anthropicClaudeFlow: OAuthFlow = {
const { verifier, challenge } = generatePkce();
const server = await startOAuthCallbackServer({ port: CALLBACK_PORT, path: CALLBACK_PATH, signal: callbacks.signal });
try {
callbacks.onAuth({ url: buildAuthorizeUrl(challenge, verifier), instructions: "Complete the Claude sign-in in your browser." });
callbacks.onAuth({ url: buildAuthorizeUrl(challenge, verifier), userCode: null });
const result = await server.waitForCode();
if (result.state !== verifier) throw new Error("OAuth state mismatch.");
return await exchangeCode(result.code, result.state, verifier, callbacks.signal);
@@ -216,7 +216,7 @@ export const githubCopilotFlow: OAuthFlow = {
const device = await startDeviceFlow(domain, callbacks.signal);
callbacks.onAuth({
url: device.verification_uri,
instructions: `Enter code ${device.user_code} at ${device.verification_uri}`,
userCode: device.user_code,
});
const githubToken = await pollForGitHubAccessToken(domain, device, callbacks.signal);
const credentials = await refreshGitHubCopilotToken(githubToken, enterpriseDomain ?? undefined, callbacks.signal);
@@ -140,7 +140,7 @@ export const antigravityFlow: OAuthFlow = {
const { verifier, challenge } = generatePkce();
const server = await startOAuthCallbackServer({ port: CALLBACK_PORT, path: CALLBACK_PATH, signal: callbacks.signal });
try {
callbacks.onAuth({ url: buildAuthorizeUrl(challenge, verifier), instructions: "Complete the Google sign-in in your browser." });
callbacks.onAuth({ url: buildAuthorizeUrl(challenge, verifier), userCode: null });
const result = await server.waitForCode();
if (result.state !== verifier) throw new Error("OAuth state mismatch.");
const tokens = await exchangeCode(result.code, verifier, clientSecret, callbacks.signal);
+1 -1
View File
@@ -183,7 +183,7 @@ export const kimiCodeFlow: OAuthFlow = {
const device = await requestDeviceAuthorization(callbacks.signal);
callbacks.onAuth({
url: device.verification_uri_complete,
instructions: `Approve code ${device.user_code} in your browser to link your Kimi plan.`,
userCode: device.user_code,
});
return pollForToken(device, callbacks.signal);
},
+2 -3
View File
@@ -15,9 +15,8 @@ export interface OAuthCredentials {
}
export interface OAuthLoginCallbacks {
/** Called once the authorization URL is ready. `instructions` carries the
* device user-code (device flow) or a short note (authorization-code flow). */
onAuth: (info: { url: string; instructions?: string }) => void;
/** Called once the authorization URL is ready; `userCode` is the device-flow code the user enters, null for a browser redirect. */
onAuth: (info: { url: string; userCode: string | null }) => void;
onProgress?: (message: string) => void;
signal?: AbortSignal;
/** GitHub Enterprise domain for Copilot; null/empty means github.com. */
+1 -1
View File
@@ -99,7 +99,7 @@ export const openaiCodexFlow: OAuthFlow = {
const state = generatePkce().verifier;
const server = await startOAuthCallbackServer({ port: CALLBACK_PORT, path: CALLBACK_PATH, signal: callbacks.signal });
try {
callbacks.onAuth({ url: buildAuthorizeUrl(challenge, state), instructions: "Complete the ChatGPT sign-in in your browser." });
callbacks.onAuth({ url: buildAuthorizeUrl(challenge, state), userCode: null });
const result = await server.waitForCode();
if (result.state !== state) throw new Error("OAuth state mismatch.");
const tokens = await exchangeCode(result.code, verifier, callbacks.signal);
+1 -1
View File
@@ -2,7 +2,7 @@ import { getOAuthFlow } from "./oauth/oauth-flows";
import type { IntegrationKeyStore } from "./integration-key-store";
export interface ProviderOAuthHooks {
onAuth: (info: { url: string; instructions?: string }) => void;
onAuth: (info: { url: string; userCode: string | null }) => void;
onProgress?: (message: string) => void;
}
+59
View File
@@ -0,0 +1,59 @@
import { t } from "./i18n";
/** A stored value with no label yet shows as stored and is logged, so the gap is visible instead of hidden. */
function labelOf(kind: string, labels: Record<string, string>, value: string): string {
const label = labels[value];
if (label !== undefined) return label;
console.warn(`[labels] no ${kind} label for "${value}"`);
return value;
}
const APPROVAL_KINDS: Record<string, string> = {
task_risk: t("High-risk task"),
task_execution: t("Workspace access"),
agent_hire: t("Hire"),
deliverable_review: t("Deliverable review"),
goal_review: t("Goal review"),
chat_escalation: t("Escalation"),
board_force_approval: t("Board override"),
social_publish: t("Social post"),
};
export const approvalKindLabel = (kind: string) => labelOf("approval kind", APPROVAL_KINDS, kind);
const APPROVAL_STATES: Record<string, string> = { pending: t("Pending"), approved: t("Approved"), declined: t("Declined"), superseded: t("Superseded") };
export const approvalStateLabel = (state: string) => labelOf("approval state", APPROVAL_STATES, state);
const RUN_TRIGGERS: Record<string, string> = { heartbeat: t("Heartbeat"), manual: t("Manual"), chat_mention: t("Chat mention"), bootstrap: t("Company launch") };
export const runTriggerLabel = (trigger: string) => labelOf("run trigger", RUN_TRIGGERS, trigger);
const REQUEST_STATES: Record<string, string> = { pending: t("Pending"), streaming: t("Streaming"), succeeded: t("Succeeded"), failed: t("Failed"), cancelled: t("Cancelled") };
export const requestStateLabel = (state: string) => labelOf("request state", REQUEST_STATES, state);
const RISKS: Record<string, string> = { low: t("Low"), medium: t("Medium"), high: t("High") };
export const riskLabel = (risk: string) => labelOf("risk", RISKS, risk);
const ROLE_LEVELS: Record<string, string> = { staff: t("Staff"), lead: t("Lead"), director: t("Director"), c_suite: t("C-suite") };
export const roleLevelLabel = (level: string) => labelOf("rank", ROLE_LEVELS, level);
const DOCUMENT_STATUSES: Record<string, string> = { draft: t("Draft"), review: t("In review"), approved: t("Approved"), archived: t("Archived") };
export const DOCUMENT_STATUS_VALUES = Object.keys(DOCUMENT_STATUSES);
export const documentStatusLabel = (status: string) => labelOf("document status", DOCUMENT_STATUSES, status);
const CREDENTIAL_SOURCES: Record<string, string> = { manual_secret: t("Key entered here"), provider_oauth_login: t("Signed in") };
export const credentialSourceLabel = (source: string) => labelOf("credential source", CREDENTIAL_SOURCES, source);
const TASK_EVENTS: Record<string, string> = {
created: t("Created"), updated: t("Edited"), deleted: t("Deleted"), status_changed: t("Status changed"), stalled: t("Stalled"),
review_requested: t("Review requested"), review_closed: t("Review closed"), blocked: t("Blocked"), unblocked: t("Unblocked"),
};
export const taskEventLabel = (action: string) => labelOf("task event", TASK_EVENTS, action);
const FILE_KINDS: Record<string, string> = { md: "Markdown", pdf: "PDF", img: t("Image"), sheet: t("Spreadsheet"), doc: t("Document"), file: t("File") };
/** Other kinds are file extensions, shown as the format name. */
export const fileKindLabel = (kind: string) => FILE_KINDS[kind] ?? kind.toUpperCase();
/** The platform's own notation, matching the keys the handlers accept (Cmd on macOS, Ctrl elsewhere). */
export function shortcutLabel(key: "K" | "Enter"): string {
if (window.agentCompany.platform === "darwin") return key === "Enter" ? "⌘↩" : "⌘K";
return `Ctrl+${key}`;
}
+33
View File
@@ -731,4 +731,37 @@ export const zh: Record<string, string> = {
"Queued sessions that will fail when they start: {count}.": "开始时将失败的排队会话:{count}。",
"Disconnecting…": "正在断开…",
"{label} is disconnected.": "{label} 已断开。",
"Archived": "已归档",
"Board override": "董事会强制批准",
"C-suite": "高管",
"Cancelled": "已取消",
"Chat mention": "聊天提及",
"Company launch": "公司启动",
"Deleted": "已删除",
"Deliverable review": "交付物审核",
"Draft": "草稿",
"Edited": "已编辑",
"Enter code {code} at {url}": "在 {url} 输入代码 {code}",
"Escalation": "升级",
"Finish signing in in the browser window that opened.": "请在打开的浏览器窗口中完成登录。",
"Goal review": "目标审查",
"Heartbeat": "心跳",
"High-risk task": "高风险任务",
"Image": "图片",
"Key entered here": "在此输入的密钥",
"Review closed": "审核已结束",
"Review requested": "已请求审核",
"Sign in again": "重新登录",
"Sign in with {label}": "使用 {label} 登录",
"Signed in": "已登录",
"Signed in to {label}": "已登录 {label}",
"Social post": "社交发布",
"Spreadsheet": "表格",
"Stalled": "已停滞",
"Status changed": "状态已变更",
"Streaming": "传输中",
"Succeeded": "成功",
"Unblocked": "已解除阻塞",
"Waiting for {label} sign-in": "等待 {label} 登录",
"Workspace access": "工作区访问",
};
+2 -1
View File
@@ -1,3 +1,4 @@
import { approvalKindLabel } from "../i18n/labels";
import { decideEach, type BatchResult } from "./approvals/decide-each";
import { DECIDED_PAGE, decidedWindow } from "./approvals/decided-window";
import { ApprovalDetailDialog, ApprovalRecordDialog } from "./approvals/ApprovalDialogs";
@@ -278,7 +279,7 @@ function ApprovalRow({
<div className="row" style={{ marginBottom: 4 }}>
<span style={{ fontSize: 12, color: tone.color, fontWeight: 540, letterSpacing: "0.02em" }}>{tone.label}</span>
<span style={{ color: "var(--text-4)" }}>·</span>
<Pill>{approval.kind}</Pill>
<Pill>{approvalKindLabel(approval.kind)}</Pill>
</div>
<div style={{ fontSize: 15, fontWeight: 540, letterSpacing: "-0.01em", marginBottom: 4 }}>{approval.title}</div>
<div style={{ fontSize: 14, color: "var(--text-2)", lineHeight: 1.5, marginBottom: 12, whiteSpace: "pre-wrap" }}>
+2 -1
View File
@@ -1,3 +1,4 @@
import { runTriggerLabel } from "../i18n/labels";
import { goalProgress, type GoalPace } from "./home/goal-progress";
import { newestRuns } from "./runs/newest-runs";
import { isBusinessTask } from "./work/task-helpers";
@@ -241,7 +242,7 @@ function RunActivityCard({
{agent && <Avatar name={agent.name} size="sm" bot />}
<div style={{ flex: 1, minWidth: 0 }}>
<div style={{ fontSize: 14, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>{task?.title ?? run.taskId}</div>
<div style={{ fontSize: 12, color: "var(--text-3)" }}>{agent?.name ?? t("Agent")} · {run.trigger}</div>
<div style={{ fontSize: 12, color: "var(--text-3)" }}>{agent?.name ?? t("Agent")} · {runTriggerLabel(run.trigger)}</div>
</div>
<Pill tone={runStateTone(run.state)}>{runStateLabel(run.state)}</Pill>
<span className="mono" style={{ fontSize: 12, color: "var(--text-3)", flexShrink: 0 }}>{formatRelative(run.createdAt)}</span>
+4 -3
View File
@@ -1,3 +1,4 @@
import { requestStateLabel, runTriggerLabel } from "../i18n/labels";
import { RunRequestList } from "./runs/RunRequestList";
import { useRunRequests } from "./runs/use-run-requests";
import { useCallback, useEffect, useMemo, useState } from "react";
@@ -117,7 +118,7 @@ export function RunsPage({ snapshot, actions, navigate, focus }: PageProps) {
{task?.title ?? run.taskId}
</div>
<div style={{ fontSize: 12, color: "var(--text-3)", overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>
{agent?.name ?? t("Agent")} · {run.trigger}
{agent?.name ?? t("Agent")} · {runTriggerLabel(run.trigger)}
</div>
</div>
<Pill tone={runTone(run.state)}>{runLabel(run.state)}</Pill>
@@ -206,7 +207,7 @@ function RunDetail({ run, tasks, agents, providers, actions, onOpenTask }: {
<MetaLabel>{t("Agent")}</MetaLabel><div>{agent?.name ?? "—"} ({agent?.role ?? "—"})</div>
<MetaLabel>{t("Provider")}</MetaLabel><div>{provider?.label ?? "—"}</div>
<MetaLabel>{t("Model")}</MetaLabel><div>{run.runModel ?? t("Chosen when the session starts")}</div>
<MetaLabel>{t("Trigger")}</MetaLabel><div>{run.trigger}</div>
<MetaLabel>{t("Trigger")}</MetaLabel><div>{runTriggerLabel(run.trigger)}</div>
<MetaLabel>{t("Created")}</MetaLabel><div>{formatRelative(run.createdAt)}</div>
{run.startedAt && (<><MetaLabel>{t("Started")}</MetaLabel><div>{formatRelative(run.startedAt)}</div></>)}
{run.completedAt && (<><MetaLabel>{t("Completed")}</MetaLabel><div>{formatRelative(run.completedAt)}</div></>)}
@@ -242,7 +243,7 @@ function RunDetail({ run, tasks, agents, providers, actions, onOpenTask }: {
<div style={{ display: "grid", gridTemplateColumns: "120px 1fr", gap: "8px 16px", fontSize: 14 }}>
<MetaLabel>{t("Request ID")}</MetaLabel><div className="mono" style={{ fontSize: 12, overflow: "hidden", textOverflow: "ellipsis" }}>{request.id}</div>
<MetaLabel>{t("Model")}</MetaLabel><div>{request.model}</div>
<MetaLabel>{t("State")}</MetaLabel><div>{request.state}</div>
<MetaLabel>{t("State")}</MetaLabel><div>{requestStateLabel(request.state)}</div>
<MetaLabel>{t("Input chars")}</MetaLabel><div className="num">{request.inputCharCount.toLocaleString()}</div>
<MetaLabel>{t("Output chars")}</MetaLabel><div className="num">{request.outputCharCount.toLocaleString()}</div>
<MetaLabel>{t("Tokens")}</MetaLabel><div className="num">{request.inputTokens === null ? t("not reported") : t("{input} in · {output} out", { input: request.inputTokens.toLocaleString(), output: (request.outputTokens ?? 0).toLocaleString() })}</div>
@@ -1,3 +1,4 @@
import { approvalKindLabel } from "../../i18n/labels";
import { useState } from "react";
import { Button, IconButton, Pill } from "../../shared/ui";
import type { ApprovalRecord } from "../../../../shared/domain";
@@ -45,7 +46,7 @@ export function ApprovalDetailDialog({
<div className="row" style={{ flexWrap: "wrap", gap: 6, marginBottom: 14 }}>
<span style={{ fontSize: 12, color: tone.color, fontWeight: 540, letterSpacing: "0.02em" }}>{tone.label}</span>
<span style={{ color: "var(--text-4)" }}>·</span>
<Pill>{approval.kind}</Pill>
<Pill>{approvalKindLabel(approval.kind)}</Pill>
</div>
<p style={{ margin: "0 0 16px", color: "var(--text-2)", fontSize: 14, lineHeight: 1.55, whiteSpace: "pre-wrap" }}>
{approval.details}
@@ -112,7 +113,7 @@ export function ApprovalRecordDialog({ approval, requester, decider, drift, onCl
</div>
<div className="row" style={{ flexWrap: "wrap", gap: 6, marginBottom: 14 }}>
<Pill>{outcome}</Pill>
<Pill>{approval.kind}</Pill>
<Pill>{approvalKindLabel(approval.kind)}</Pill>
{drift && <Pill tone="amber">{drift === "changed" ? t("Changed since approval") : t("Removed since approval")}</Pill>}
</div>
<p style={{ margin: "0 0 16px", color: "var(--text-2)", fontSize: 14, lineHeight: 1.55, whiteSpace: "pre-wrap" }}>{approval.details}</p>
+2 -1
View File
@@ -1,3 +1,4 @@
import { shortcutLabel } from "../../i18n/labels";
import { useEffect, useRef, useState, type FormEvent, type MutableRefObject } from "react";
import { Button, Icon, IconButton } from "../../shared/ui";
import type { AgentRecord, EmployeeRecord, FileObjectRecord } from "../../../../shared/domain";
@@ -108,7 +109,7 @@ export function ChatComposer({
</span>
))}
<div style={{ marginLeft: "auto", display: "flex", alignItems: "center", gap: 6 }}>
<span className="mono" style={{ fontSize: 12, color: "var(--text-3)" }}>Ctrl+Enter</span>
<span className="mono" style={{ fontSize: 12, color: "var(--text-3)" }}>{shortcutLabel("Enter")}</span>
<Button tone="primary" size="small" type="submit" disabled={locked}><Icon name="send" size={12} /> {t("Send")}</Button>
</div>
</div>
@@ -1,3 +1,4 @@
import { fileKindLabel } from "../../i18n/labels";
import { useState, type FormEvent, type ReactNode } from "react";
import { Avatar, Button, Icon, IconButton } from "../../shared/ui";
import type {
@@ -260,7 +261,7 @@ function AttachmentCard({ file, handoff = false, onOpen }: { file: FileObjectRec
</span>
)}
</div>
<div style={{ fontSize: 12, color: "var(--text-3)" }}>{file.kind.toUpperCase()}</div>
<div style={{ fontSize: 12, color: "var(--text-3)" }}>{fileKindLabel(file.kind)}</div>
</div>
{canOpen && <IconButton icon="arrowUpRight" label={t("Open {name}", { name: file.name })} onClick={onOpen ?? undefined} />}
</div>
+2 -1
View File
@@ -1,3 +1,4 @@
import { riskLabel } from "../../i18n/labels";
import { useState, type ReactNode } from "react";
import { Button, Icon, Pill } from "../../shared/ui";
import type {
@@ -149,7 +150,7 @@ function ApprovalInline({ approval, onApprove, onDecline }: { approval: Approval
<div className="row" style={{ marginBottom: 8 }}>
<Icon name="approve" size={14} color="var(--amber)" />
<span style={{ fontSize: 12, color: "var(--amber)", fontWeight: 540 }}>{t("Approval needed")}</span>
<Pill tone={approval.risk === "high" ? "red" : approval.risk === "medium" ? "amber" : undefined}>{approval.risk}</Pill>
<Pill tone={approval.risk === "high" ? "red" : approval.risk === "medium" ? "amber" : undefined}>{riskLabel(approval.risk)}</Pill>
</div>
<div style={{ fontSize: 14, fontWeight: 540, marginBottom: 4 }}>{approval.title}</div>
<div style={{ fontSize: 12, color: "var(--text-3)", marginBottom: 12, lineHeight: 1.5 }}>{approval.details}</div>
@@ -1,3 +1,4 @@
import { DOCUMENT_STATUS_VALUES, documentStatusLabel } from "../../i18n/labels";
import { useEffect, useState, type FormEvent } from "react";
import type { DocumentRecord, TaskRecord } from "../../../../shared/domain";
import type { DocumentVersionRecord } from "../../../../shared/commands";
@@ -98,7 +99,7 @@ export function DocumentsPanel({
<div className="row" style={{ gap: 8, marginBottom: 8, flexWrap: "wrap" }}>
<h2 style={{ margin: 0, fontSize: 18 }}>{shown?.title ?? selected.title}</h2>
<Pill>{selected.docType}</Pill>
<Pill tone={selected.status === "approved" ? "green" : undefined}>{shown?.status ?? selected.status}</Pill>
<Pill tone={selected.status === "approved" ? "green" : undefined}>{documentStatusLabel(shown?.status ?? selected.status)}</Pill>
</div>
{producedBy && <p style={{ color: "var(--text-2)", margin: "0 0 8px" }}>{t("Produced by task: {title}", { title: producedBy })}</p>}
<div className="row" style={{ gap: 8, marginBottom: 12, flexWrap: "wrap" }}>
@@ -124,7 +125,7 @@ export function DocumentsPanel({
<Field label={t("Title")}><input className="input" name="title" defaultValue={selected.title} required /></Field>
<Field label={t("Status")}>
<select className="input" name="status" defaultValue={selected.status}>
{["draft", "review", "approved", "archived"].map((status) => <option key={status} value={status}>{status}</option>)}
{DOCUMENT_STATUS_VALUES.map((status) => <option key={status} value={status}>{documentStatusLabel(status)}</option>)}
</select>
</Field>
<Field label={t("Content")}><textarea className="input" name="content" defaultValue={selected.content} rows={14} required /></Field>
@@ -1,5 +1,5 @@
import { Avatar, Icon, IconButton, Pill } from "../../shared/ui";
import { humanizeToken } from "../types";
import { roleLevelLabel } from "../../i18n/labels";
import { t } from "../../i18n/i18n";
import type { AgentActivity, OfficeAgentInput } from "./office-types";
@@ -45,7 +45,7 @@ export function AgentDetailCard({ agent, onClose }: { agent: OfficeAgentInput; o
</div>
<div className="row" style={{ gap: 6, marginTop: 10, flexWrap: "wrap" }}>
<Pill>{agent.department}</Pill>
<Pill>{humanizeToken(agent.roleLevel)}</Pill>
<Pill>{roleLevelLabel(agent.roleLevel)}</Pill>
<Pill tone={ACTIVITY_TONE[agent.activity]} dot>{ACTIVITY_LABEL[agent.activity]}</Pill>
</div>
{agent.taskTitle && (
@@ -1,3 +1,4 @@
import { requestStateLabel } from "../../i18n/labels";
import type { LoadedRequests } from "./use-run-requests";
import { Pill } from "../../shared/ui";
import { t } from "../../i18n/i18n";
@@ -24,7 +25,7 @@ export function RunRequestList({ loaded }: { loaded: LoadedRequests | null }) {
<span className="mono" style={{ color: "var(--text-3)", width: 24 }}>{index + 1}</span>
<span style={{ width: 96 }}>{purposeLabel(request.purpose)}</span>
<span className="mono" style={{ flex: 1, minWidth: 0, overflow: "hidden", textOverflow: "ellipsis" }}>{request.model}</span>
<Pill tone={request.state === "failed" ? "red" : request.state === "succeeded" ? "green" : "amber"}>{request.state}</Pill>
<Pill tone={request.state === "failed" ? "red" : request.state === "succeeded" ? "green" : "amber"}>{requestStateLabel(request.state)}</Pill>
<span className="mono" style={{ color: "var(--text-3)", width: 120, textAlign: "right" }}>
{request.inputTokens ?? "—"} / {request.outputTokens ?? "—"}
</span>
@@ -1,3 +1,4 @@
import { requestStateLabel } from "../../i18n/labels";
import { useState, type FormEvent } from "react";
import type { ProviderCatalogEntry, ProviderRecord, ProviderRequestRecord, WorkspaceRecord } from "../../../../shared/domain";
import { PROVIDER_DATA_POLICY, PROVIDER_EXECUTION_MODE, PROVIDER_REQUEST_STATE, type ProviderExecutionMode } from "../../../../shared/domain";
@@ -174,7 +175,7 @@ function ProviderRequestRow({
</div>
<div className="mono" style={{ color: "var(--text-2)", overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>{workspace?.name ?? t("Organization")}</div>
<div className="mono" style={{ color: "var(--text-2)" }}>{request.executionMode}</div>
<div><Pill tone={stateTone(request.state)}>{request.state}</Pill></div>
<div><Pill tone={stateTone(request.state)}>{requestStateLabel(request.state)}</Pill></div>
<div className="mono" style={{ color: "var(--text-2)" }}>{t("{count} chars", { count: request.outputCharCount })}</div>
<Button size="small" tone="ghost" onClick={cancel} disabled={!canCancel}>
<span className="row" style={{ gap: 5 }}>
@@ -1,3 +1,4 @@
import { credentialSourceLabel } from "../../i18n/labels";
import { defaultModelReach } from "../../../../shared/agent-model";
import { ScopeNote } from "./ScopeNote";
import { useState, type FormEvent } from "react";
@@ -261,7 +262,7 @@ function ProviderRow({
<div style={{ flex: 1, minWidth: 0 }}>
<div style={{ fontSize: 13.5, fontWeight: 540, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>{provider.label}</div>
<div style={{ fontSize: 12, color: "var(--text-3)", overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>
{provider.model ? `${provider.model} · ` : ""}{readableAuth(provider.authType)}{authProfile ? ` · ${authProfile.sourceType}` : ""}
{provider.model ? `${provider.model} · ` : ""}{readableAuth(provider.authType)}{authProfile ? ` · ${credentialSourceLabel(authProfile.sourceType)}` : ""}
</div>
</div>
<ProviderHealth provider={provider} />
-9
View File
@@ -55,15 +55,6 @@ export function formatTime(iso: string): string {
return date.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit", hour12: false });
}
export function humanizeToken(value: string): string {
const spaced = value.replace(/_/g, " ").trim();
if (!spaced) return spaced;
return spaced.charAt(0).toUpperCase() + spaced.slice(1);
}
export function auditEventLabel(entityType: string, action: string): string {
return `${humanizeToken(entityType)} ${action.replace(/_/g, " ")}`.trim();
}
export function formatBytes(bytes: number): string {
if (bytes < 1024) return `${bytes} B`;
@@ -1,3 +1,4 @@
import { approvalStateLabel, runTriggerLabel, taskEventLabel } from "../../i18n/labels";
import { TaskHoldNotice } from "./TaskHoldNotice";
import { useState, type FormEvent } from "react";
import { Avatar, Button, Field, Icon, IconButton, Pill, Segmented } from "../../shared/ui";
@@ -13,7 +14,7 @@ import type {
WorkspaceRecord,
} from "../../../../shared/domain";
import { TASK_STATUS, type TaskDeleteImpact, type TaskStatus, type WorkLanguage, type WritingMode } from "../../../../shared/domain";
import { auditEventLabel, formatRelative } from "../types";
import { formatRelative } from "../types";
import { WorkLanguageField } from "./WorkLanguageField";
import { WRITING_OPTIONS, readWorkLanguage, descendantIds, nextStatus, prettyRunState, prettyStatus, providerFor, runBlocker, runsForTask, runTone } from "./task-helpers";
import type { SnapshotActions } from "../../app/useSnapshot";
@@ -255,7 +256,7 @@ function OverviewTab({ task, assignee, project, goal, workspace, provider, runs,
<button type="button" className="link-button" aria-label={t("Open run {id}", { id: run.id })} onClick={() => navigate("runs", { kind: "run", id: run.id })}>
<Pill tone={runTone(run.state)}>{prettyRunState(run.state)}</Pill>
</button>
<span style={{ color: "var(--text-3)", fontSize: 12 }}>{run.trigger}</span>
<span style={{ color: "var(--text-3)", fontSize: 12 }}>{runTriggerLabel(run.trigger)}</span>
<span className="num" style={{ marginLeft: "auto", color: "var(--text-3)", fontSize: 12 }}>{formatRelative(run.createdAt)}</span>
</div>
{run.state === "failed" && run.errorMessage && (
@@ -300,7 +301,7 @@ function ActivityTab({ audit, approvals, onOpenApproval }: { audit: AuditEventRe
<div className="section-label">{t("Approvals")}</div>
{approvals.map((a) => (
<div key={a.id} className="row" style={{ gap: 8, padding: "6px 0" }}>
<Pill tone={a.state === "approved" ? "green" : a.state === "pending" ? "amber" : "red"}>{a.state}</Pill>
<Pill tone={a.state === "approved" ? "green" : a.state === "pending" ? "amber" : "red"}>{approvalStateLabel(a.state)}</Pill>
<button type="button" className="link-button" style={{ fontSize: 12 }} aria-label={t("Open approval {title}", { title: a.title })} onClick={() => onOpenApproval(a.id)}>{a.title}</button>
<span className="mono" style={{ marginLeft: "auto", fontSize: 12, color: "var(--text-3)" }}>{formatRelative(a.createdAt)}</span>
</div>
@@ -312,7 +313,7 @@ function ActivityTab({ audit, approvals, onOpenApproval }: { audit: AuditEventRe
{audit.map((e) => (
<div key={e.id} className="row" style={{ gap: 8, padding: "5px 0" }}>
<span className="mono" style={{ fontSize: 12, color: "var(--text-3)", flexShrink: 0 }}>{formatRelative(e.createdAt)}</span>
<span style={{ fontSize: 12, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>{auditEventLabel(e.entityType, e.action)}</span>
<span style={{ fontSize: 12, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>{taskEventLabel(e.action)}</span>
</div>
))}
</div>
@@ -1,6 +1,7 @@
import { useEffect, useState } from "react";
import type { StartProviderOAuthInput } from "../../../shared/commands";
import { Button, Icon } from "./ui";
import { t } from "../i18n/i18n";
type Phase = "idle" | "running" | "done" | "error";
@@ -21,21 +22,21 @@ export function ProviderOAuthConnect({
onComplete: (secret: string | null) => void;
}) {
const [phase, setPhase] = useState<Phase>("idle");
const [instruction, setInstruction] = useState<string | null>(null);
const [prompt, setPrompt] = useState<{ url: string; userCode: string | null } | null>(null);
const [error, setError] = useState<string | null>(null);
useEffect(
() =>
window.agentCompany.subscribe((event) => {
if (event.type !== "provider-oauth" || event.providerKey !== providerKey) return;
if (event.phase === "authorize" && event.instructions) setInstruction(event.instructions);
if (event.phase === "authorize" && event.url) setPrompt({ url: event.url, userCode: event.userCode ?? null });
}),
[providerKey],
);
const run = () => {
setPhase("running");
setInstruction(null);
setPrompt(null);
setError(null);
void window.agentCompany.startProviderOAuth(buildInput()).then((result) => {
if (result.ok) {
@@ -55,10 +56,16 @@ export function ProviderOAuthConnect({
<div className="oauth-connect">
<div className="oauth-connect-row">
<span className="spinner" aria-hidden />
<span>Waiting for {label} sign-in</span>
<button type="button" className="link-button" onClick={cancel}>Cancel</button>
<span>{t("Waiting for {label} sign-in", { label })}</span>
<button type="button" className="link-button" onClick={cancel}>{t("Cancel")}</button>
</div>
{instruction && <div className="oauth-connect-code">{instruction}</div>}
{prompt && (
<div className="oauth-connect-code">
{prompt.userCode
? t("Enter code {code} at {url}", { code: prompt.userCode, url: prompt.url })
: t("Finish signing in in the browser window that opened.")}
</div>
)}
</div>
);
}
@@ -67,15 +74,15 @@ export function ProviderOAuthConnect({
return (
<div className="oauth-connect-row done">
<Icon name="check" size={14} color="var(--green)" />
<span>Signed in to {label}</span>
<button type="button" className="link-button" onClick={run}>Redo</button>
<span>{t("Signed in to {label}", { label })}</span>
<button type="button" className="link-button" onClick={run}>{t("Sign in again")}</button>
</div>
);
}
return (
<div className="oauth-connect">
<Button tone="primary" onClick={run}>Sign in with {label}</Button>
<Button tone="primary" onClick={run}>{t("Sign in with {label}", { label })}</Button>
{error && <div className="oauth-connect-error">{error}</div>}
</div>
);
+2 -1
View File
@@ -1,3 +1,4 @@
import { shortcutLabel } from "../i18n/labels";
import { isBusinessTask } from "../pages/work/task-helpers";
import { unreadBySpace } from "../pages/chat/chat-helpers";
import { useEffect, useState, type FormEvent, type ReactNode } from "react";
@@ -99,7 +100,7 @@ export function Shell({
<button type="button" className="search-bar" aria-label={t("Search")} onClick={() => setSearchOpen(true)}>
<Icon name="search" size={13} />
<span>{t("Search")}</span>
<span className="search-kbd">⌘K</span>
<span className="search-kbd">{shortcutLabel("K")}</span>
</button>
<nav className="nav">
{allNav.map((item) => (
+2 -1
View File
@@ -107,7 +107,8 @@ export interface ProviderOAuthEvent {
providerKey: string;
phase: ProviderOAuthPhase;
url?: string;
instructions?: string;
/** The device-flow code the user enters at `url`; null when the browser redirect completes the sign-in. */
userCode?: string | null;
message?: string;
}
+3
View File
@@ -138,6 +138,9 @@ test("the pinned CEO conversation is the Messages front door", async () => {
await expect(page.getByText("Goal: Ship the FlowPilot MVP.", { exact: true })).toBeVisible();
await page.getByRole("button", { name: /Operationalize goal: Ship the FlowPilot MVP\./ }).first().click();
await taskDialog.getByRole("button", { name: /^Open run/ }).first().click();
// Stored values read as words, and shortcuts use this platform's notation.
await expect(page.getByText("Company launch").first()).toBeVisible();
await expect(page.locator(".search-kbd")).toHaveText(process.platform === "darwin" ? "⌘K" : "Ctrl+K");
// A queued run can be cancelled before it starts; then it offers to resume.
await page.getByRole("button", { name: "Cancel run" }).click();
await expect(page.getByRole("button", { name: "Resume" })).toBeVisible();