Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -1397,6 +1397,28 @@ const en = {
"brief.completedEmpty": "No completed work yet",
"brief.completedTotal": "{count} completed in total",
"home.allGoals": "All Goals",
"home.firstGoal.title": "No Goals yet",
"home.firstGoal.lead": "A Goal is one outcome you want; the Agent keeps working toward it until you stop it. Describe the outcome in your own words. The LoopX Manager drafts the Goal and nothing is created until you confirm.",
"home.firstGoal.greeting": "Let’s start with your first Goal.",
"home.firstGoal.create": "Create a Goal",
"home.firstGoal.examplesTitle": "Or start from an example. It only fills the message box; nothing is sent until you press send.",
"home.firstGoal.example.research": "Help me shape a goal to research Microsoft’s cash flow over the last three fiscal years from its official reports.",
"home.firstGoal.example.issue": "Help me shape a goal to turn one open issue in a repository I choose into a reviewed patch.",
"home.firstGoal.example.monitor": "Help me shape a goal to watch a public web page every weekday and report only when its content changes.",
"home.firstGoal.diagnostics": "Status source details",
"home.health.payloadNotOk": "The status source reports that it is not healthy.",
"home.health.contractFailed": "Control-plane contract checks failed: {errors} error(s), {warnings} warning(s).",
"home.health.contractFailedDetail": "Control-plane contract checks failed: {detail}",
"home.health.contractFailedUnknown": "Control-plane contract checks failed.",
"home.health.registryHigh": "Registry checks found {count} high-severity finding(s).",
"home.health.diagnostics": "Diagnostics",
"home.health.sourceUnreadable": "The status source could not be read. What is shown is the last successful read.",
"home.health.sourceUnreadableDetail": "Latest read failed: {error}",
"home.health.retry": "Retry",
"home.health.allClear": "All control-plane contract and registry checks passed.",
"home.health.checked": "Status checks completed.",
"home.health.concerns": "{count} concern(s) found.",
"home.health.freshness": "{count} decision state(s) are stale.",
"state.stopped": "Stopped",
"state.waiting": "Waiting",
"tasks.blocked": "Blocked",
Expand Down Expand Up @@ -2881,6 +2903,28 @@ const zhCN: Record<WorkspaceMessageKey, string> = {
"brief.completedEmpty": "还没有完成的工作",
"brief.completedTotal": "累计完成 {count} 项",
"home.allGoals": "全部 Goal",
"home.firstGoal.title": "还没有 Goal",
"home.firstGoal.lead": "一个 Goal 就是你想要的一个结果,Agent 会持续推进它直到你叫停。用你自己的话描述想要的结果,LoopX 管家会先整理成 Goal 草稿,你确认之前不会创建任何东西。",
"home.firstGoal.greeting": "我们从第一个 Goal 开始。",
"home.firstGoal.create": "创建 Goal",
"home.firstGoal.examplesTitle": "也可以从一个例子开始。点选只会填入输入框,按发送前不会发出任何内容。",
"home.firstGoal.example.research": "帮我整理一个目标:依据官方财报研究微软最近三个财年的现金流。",
"home.firstGoal.example.issue": "帮我整理一个目标:把我指定仓库里的一个未关闭 issue 做成一个经过评审的补丁。",
"home.firstGoal.example.monitor": "帮我整理一个目标:每个工作日查看一个公开网页,只有内容变化时才向我汇报。",
"home.firstGoal.diagnostics": "状态来源详情",
"home.health.payloadNotOk": "状态来源报告自身不健康。",
"home.health.contractFailed": "控制面契约检查未通过:{errors} 项错误,{warnings} 项警告。",
"home.health.contractFailedDetail": "控制面契约检查未通过:{detail}",
"home.health.contractFailedUnknown": "控制面契约检查未通过。",
"home.health.registryHigh": "注册表检查发现 {count} 项高危问题。",
"home.health.diagnostics": "诊断信息",
"home.health.sourceUnreadable": "状态来源读取失败,当前显示的是上一次成功读取的内容。",
"home.health.sourceUnreadableDetail": "最近一次读取失败:{error}",
"home.health.retry": "重试",
"home.health.allClear": "所有控制面契约与注册表检查均正常。",
"home.health.checked": "状态检查已完成。",
"home.health.concerns": "发现 {count} 项需要关注。",
"home.health.freshness": "{count} 项决策状态已过期。",
"state.stopped": "已停止",
"state.waiting": "等待条件",
"tasks.blocked": "受阻",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -585,4 +585,19 @@ assert.match(
"The Goal header prefers the current projected Todo over stale recommended_action strings",
);

// First-Goal home: a healthy source with no registered Goal is a designed
// state, not empty lanes, and health issues are owner-language sentences.
assert.match(model, /kind: "no_goals"/, "The home carries a typed first-Goal classification");
assert.match(dashboard, /const onboarding: WorkspaceHomeOnboarding \| undefined = isHealthy/, "Only a healthy source classifies as a first-Goal workspace");
assert.match(dashboard, /if \(!sourceError\) return loadedModel;[\s\S]*?onboarding: undefined,[\s\S]*?retryable: true,/, "A failed current read withdraws the first-Goal state and shows a retryable failure");
assert.match(dashboard, /onRefresh=\{async \(readScope = "all"\) => \{[\s\S]*?rejectOnError: true[\s\S]*?return \{ ok: false, error: formatStatusError\(error\) \}/, "A refresh reports its failure to the page instead of swallowing it");
assert.match(page, /if \(result && result\.ok === false\) \{\n\s*setRefreshState\("error"\);/, "The header reports a failed refresh and never an update");
assert.doesNotMatch(dashboard, /payload\.ok === false|契约检查未通过|系统健康关注点/, "Health issues are localized owner language, not protocol fragments");
assert.match(page, /data-testid="personal-first-goal"/, "The home renders the first-Goal state");
assert.match(page, /onSuggestRequest\(t\(`home\.firstGoal\.example\./, "Example requests only fill the composer");
assert.match(page, /personal-first-goal-diagnostics/, "Status source details stay in a collapsed disclosure");
for (const key of ["home.firstGoal.title", "home.firstGoal.lead", "home.firstGoal.create", "home.health.contractFailed", "home.health.concerns"]) {
assert.equal((i18n.match(new RegExp(`"${key.replace(/\./g, "\\.")}":`, "g")) ?? []).length, 2, `${key} exists in both locales`);
}

console.log("personal workspace drawer contract smoke passed");
Original file line number Diff line number Diff line change
Expand Up @@ -434,12 +434,31 @@ export type WorkspaceGoalNotification = {
};

export type WorkspaceSystemHealth = {
/** Status-source contract lines for a collapsed disclosure; never primary copy. */
diagnostics?: string[];
freshnessWarning?: string | null;
issues: string[];
ok: boolean;
/** The current read of the source failed; a retry can clear it. */
retryable?: boolean;
summary: string;
};

/** Outcome of a workspace refresh; a failed read never rejects, it reports. */
export type WorkspaceRefreshResult = { ok: true } | { ok: false; error: string };

/**
* Typed first-Goal classification of the manager home. `no_goals` means the
* status source reports zero registered Goals and no execution history, so
* the home shows its first-Goal state instead of empty lanes. `diagnostics`
* carries the status source's own contract lines for a collapsed disclosure;
* it is never primary copy.
*/
export type WorkspaceHomeOnboarding = {
kind: "no_goals";
diagnostics: string[];
};

export type WorkspaceModel = {
blockingTodoCount: number;
goalNotifications?: WorkspaceGoalNotification[];
Expand All @@ -449,6 +468,7 @@ export type WorkspaceModel = {
error?: string | null;
loading: boolean;
};
onboarding?: WorkspaceHomeOnboarding;
systemHealth?: WorkspaceSystemHealth;
timeline?: WorkspaceTimelineItem[];
attentionHistory?: WorkspaceAttention[];
Expand Down Expand Up @@ -490,6 +510,7 @@ export type PersonalHomeCompatibleModel = {
goalNotifications?: WorkspaceGoalNotification[];
goals: WorkspaceGoal[];
openUserTodoCount: number;
onboarding?: WorkspaceHomeOnboarding;
systemHealth?: WorkspaceSystemHealth;
attentionHistory?: WorkspaceAttention[];
userTodos: WorkspaceAttention[];
Expand Down Expand Up @@ -545,7 +566,7 @@ export type PersonalWorkspaceCallbacks = {
* names the Goals the action touched, so a peer's snapshot is not re-read for it. */
onReconcileStatus?: (options?: { invalidateGoalIds?: string[] }) => void | Promise<void>;
/** Full refresh by default; error recovery can explicitly read missing Goals only. */
onRefresh?: (scope?: WorkspaceReadScope) => void | Promise<void>;
onRefresh?: (scope?: WorkspaceReadScope) => void | WorkspaceRefreshResult | Promise<void | WorkspaceRefreshResult>;
onRetryGoalArchive?: () => void | Promise<void>;
onPreviewAction?: (request: WorkspaceActionPreviewRequest) => WorkspaceActionPreview | Promise<WorkspaceActionPreview>;
onRequestGoalCreate?: () => WorkspaceActionPreview | Promise<WorkspaceActionPreview | void> | void;
Expand Down Expand Up @@ -596,6 +617,7 @@ export function normalizePersonalHomeModel(model: PersonalHomeCompatibleModel):
goalNotifications: model.goalNotifications,
goals: model.goals,
openUserTodoCount: model.openUserTodoCount,
onboarding: model.onboarding,
systemHealth: model.systemHealth,
attentionHistory: model.attentionHistory,
userTodos: model.userTodos,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ import type {
WorkspaceImageAttachment,
WorkspaceModel,
WorkspaceRun,
WorkspaceHomeOnboarding,
WorkspaceSystemHealth,
WorkspaceConversationDirectory,
WorkspaceTimelineItem,
Expand Down Expand Up @@ -120,6 +121,10 @@ function ManagerHomeBoard({
goals,
onSelectGoal,
onRetry,
onRetrySource,
onboarding,
onRequestGoalCreate,
onSuggestRequest,
systemHealth,
operations,
onSelectOperation,
Expand All @@ -128,6 +133,11 @@ function ManagerHomeBoard({
goals: WorkspaceGoal[];
onSelectGoal: (goalId: string) => void;
onRetry?: () => void;
/** Re-read the whole source after its latest read failed. */
onRetrySource?: () => void;
onboarding?: WorkspaceHomeOnboarding;
onRequestGoalCreate?: () => void;
onSuggestRequest?: (text: string) => void;
systemHealth?: WorkspaceSystemHealth;
operations: WorkspaceActionPreview[];
onSelectOperation: (proposal: WorkspaceActionPreview) => void;
Expand Down Expand Up @@ -176,8 +186,44 @@ function ManagerHomeBoard({
))}
</ul>
) : null}
{systemHealth.diagnostics?.length ? (
<details className="personal-system-health-diagnostics">
<summary>{t("home.health.diagnostics")}</summary>
<ul>{systemHealth.diagnostics.map((line, idx) => <li key={idx}>{line}</li>)}</ul>
</details>
) : null}
{systemHealth.retryable && onRetrySource ? (
<button className="personal-system-health-retry" data-testid="personal-source-retry" onClick={onRetrySource} type="button">{t("home.health.retry")}</button>
) : null}
</div>
) : null}
{onboarding?.kind === "no_goals" && goals.length === 0 ? (
<section aria-labelledby="personal-first-goal-title" className="personal-first-goal" data-testid="personal-first-goal">
<h2 id="personal-first-goal-title">{t("home.firstGoal.title")}</h2>
<p>{t("home.firstGoal.lead")}</p>
{onRequestGoalCreate ? (
<div className="personal-first-goal-actions">
<button className="personal-first-goal-create" onClick={onRequestGoalCreate} type="button"><Plus size={16} /><span>{t("home.firstGoal.create")}</span></button>
</div>
) : null}
{onSuggestRequest ? (
<ul className="personal-first-goal-examples">
<li>{t("home.firstGoal.examplesTitle")}</li>
{(["research", "issue", "monitor"] as const).map((example) => (
<li key={example}>
<button onClick={() => onSuggestRequest(t(`home.firstGoal.example.${example}`))} type="button">{t(`home.firstGoal.example.${example}`)}</button>
</li>
))}
</ul>
) : null}
{onboarding.diagnostics.length ? (
<details className="personal-first-goal-diagnostics">
<summary>{t("home.firstGoal.diagnostics")}</summary>
<ul>{onboarding.diagnostics.map((line, idx) => <li key={idx}>{line}</li>)}</ul>
</details>
) : null}
</section>
) : null}
{currentGoals.some((goal) => goal.loadState) ? <section className="personal-home-lane" aria-live="polite">
<header>{t("startup.progress", { loaded: currentGoals.filter((goal) => !goal.loadState).length, total: currentGoals.length })}</header>
{failedCount ? <div className="personal-stopped-goal-error" role="status"><span>{t("startup.failedCount", { count: failedCount })}</span>
Expand All @@ -186,7 +232,7 @@ function ManagerHomeBoard({
</section> : null}
{currentGoals.some((goal) => !goal.loadState) ? <ManagerBrief goals={goals} onSelectGoal={onSelectGoal}
operations={operations} onSelectOperation={onSelectOperation} onViewAllOperations={onViewAllOperations} /> : null}
<h2 className="personal-home-section-title">{t("home.allGoals")}</h2>
{onboarding?.kind === "no_goals" && goals.length === 0 ? null : <h2 className="personal-home-section-title">{t("home.allGoals")}</h2>}
<div className="personal-home-lanes">
{activeHomeLanes.filter((lane) => active[lane.key].length > 0).map((lane) => (
<section className={`personal-home-lane is-${lane.key}`} data-testid={`personal-home-lane-${lane.key}`} key={lane.key}>
Expand Down Expand Up @@ -2010,9 +2056,13 @@ export function PersonalWorkspacePage({
}
setRefreshState("loading");
try {
await callbacks.onRefresh();
setHistoryRefreshRevision((revision) => revision + 1);
setRefreshState("done");
const result = await callbacks.onRefresh();
if (result && result.ok === false) {
setRefreshState("error");
} else {
setHistoryRefreshRevision((revision) => revision + 1);
setRefreshState("done");
}
} catch {
setRefreshState("error");
}
Expand Down Expand Up @@ -2210,7 +2260,7 @@ export function PersonalWorkspacePage({
{!selectedGoal && !managerChatOpen ? (
<section className="personal-manager-greeting">
<span><Bot size={20} /></span>
<div><small className="personal-brief-date">{t("brief.title")} · {new Intl.DateTimeFormat(locale, { month: "long", day: "numeric", weekday: "short" }).format(new Date())}</small><strong>{t("home.greeting")}</strong><p>{model.goals.some((goal) => goal.activationState === "active" && goal.loadState) ? t("startup.partial") : <>{t("home.waitingCount", { count: managerNeedsYouCount })} {managerBlockingCount > 0 ? t("home.blockingSummary", { count: managerBlockingCount }) : null} {!actionReadback.isError && homeOperations.length ? t("home.operationSummary", {count: homeOperations.length}) : null}</>}</p></div>
<div><small className="personal-brief-date">{t("brief.title")} · {new Intl.DateTimeFormat(locale, { month: "long", day: "numeric", weekday: "short" }).format(new Date())}</small><strong>{t("home.greeting")}</strong><p>{model.goals.some((goal) => goal.activationState === "active" && goal.loadState) ? t("startup.partial") : model.onboarding?.kind === "no_goals" && model.goals.length === 0 ? t("home.firstGoal.greeting") : <>{t("home.waitingCount", { count: managerNeedsYouCount })} {managerBlockingCount > 0 ? t("home.blockingSummary", { count: managerBlockingCount }) : null} {!actionReadback.isError && homeOperations.length ? t("home.operationSummary", {count: homeOperations.length}) : null}</>}</p></div>
</section>
) : null}
{selectedGoal && !selectedGoal.loadState && readOnly && routedTodoId
Expand Down Expand Up @@ -2253,7 +2303,8 @@ export function PersonalWorkspacePage({
</>),
}} />
) : !managerChatOpen ? (
<ManagerHomeBoard goals={workspaceGoals} onRetry={() => void callbacks.onRefresh?.("missing")} onSelectGoal={selectGoal} systemHealth={model.systemHealth}
<ManagerHomeBoard goals={workspaceGoals} onRetry={() => void callbacks.onRefresh?.("missing")} onRetrySource={() => void refreshWorkspace()} onSelectGoal={selectGoal} systemHealth={model.systemHealth}
onboarding={model.onboarding} onRequestGoalCreate={readOnly ? undefined : requestGoalCreate} onSuggestRequest={readOnly ? undefined : suggestReply}
operations={actionReadback.isError ? [] : homeOperations} onSelectOperation={proposal => setSelection({kind: "proposal", item: proposal})}
onViewAllOperations={() => setSelectedGoalTab("chat")} />
) : (
Expand Down
Loading
Loading