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:
1 parent
f79e3d4d7c
commit
1cc67c244e
28 files changed
+158
-52
No files matched your search
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
},
|
||||
|
||||
@@ -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. */
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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}`;
|
||||
}
|
||||
@@ -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": "工作区访问",
|
||||
};
|
||||
@@ -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" }}>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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} />
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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) => (
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in new issue
Block a user