Skip to content
Open
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 @@ -98,3 +98,9 @@
.goal-masthead-title > button:focus-visible { outline: 2px solid var(--pw-blue); outline-offset: 2px; }
.personal-channel-title .goal-masthead-context { white-space: nowrap; }
}

/* Unmeasured vitals are readable facts, not actionable values: muted text,
no hover affordance, and the en dash never replaces the explanation. */
.goal-vital.is-empty { cursor: default; }
.goal-vital.is-empty:hover { background: transparent; }
.goal-vital.is-empty .goal-vital-value { color: var(--pw-faint); font-size: var(--text-body); font-weight: 500; letter-spacing: 0; }
Original file line number Diff line number Diff line change
Expand Up @@ -140,11 +140,11 @@ export type GoalVitalRegion = "overview" | "tasks" | "attention" | "usage";

const ribbonOrder = ["done", "running_observed", "needs_you", "open", "waiting_dependency", "blocked", "deferred"] as const;

function Vital({ children, extra, help, label, onClick, sub, tone, valueKey }: {
children: ReactNode; extra?: ReactNode; help?: ReactNode; label: string; onClick: () => void; sub?: ReactNode; tone?: string; valueKey?: string;
function Vital({ children, empty, extra, help, label, onClick, sub, tone, valueKey }: {
children: ReactNode; empty?: boolean; extra?: ReactNode; help?: ReactNode; label: string; onClick: () => void; sub?: ReactNode; tone?: string; valueKey?: string;
}) {
return (
<div className="goal-vital-cell"><button className={`goal-vital${tone ? ` is-${tone}` : ""}`} onClick={onClick} type="button">
<div className="goal-vital-cell"><button aria-disabled={empty} className={`goal-vital${empty ? " is-empty" : ""}${tone ? ` is-${tone}` : ""}`} onClick={empty ? undefined : onClick} tabIndex={empty ? -1 : undefined} type="button">
<span className="goal-vital-value goal-vital-swap" key={valueKey}>{children}</span>
<span className="goal-vital-meta">
<span className="goal-vital-label">{label}</span>
Expand Down Expand Up @@ -190,6 +190,8 @@ export function GoalVitals({ goal, items, onFocusRegion, userTodos, digest: shar

const windowLabel = digest.usage.window === "7d" ? t("vitals.window7d") : digest.usage.window === "24h" ? t("vitals.window24h") : t("vitals.notMeasured");
const needs = digest.needsYou;
const needsEmpty = needs.availability === "ok" && needs.coverage === "complete"
&& needs.blocking === 0 && needs.optional === 0;
const last = digest.lastActivity;
const lastTime = last ? relativeTime(last.at, locale) : null;
const stateLabel = finishing ? t("vitals.stopping") : t(activity.labelKey as WorkspaceMessageKey);
Expand All @@ -215,30 +217,33 @@ export function GoalVitals({ goal, items, onFocusRegion, userTodos, digest: shar
</>}>
{work.total === null ? work.open : <span>{work.done}<small>/{work.total}</small></span>}
</Vital>
<Vital label={t("vitals.needsYou")} onClick={() => onFocusRegion("attention")} valueKey={`${goal.goalId}:${needs.availability}:${needs.availability === "ok" ? needs.blocking : ""}`}
<Vital label={t("vitals.needsYou")} empty={needsEmpty} onClick={() => onFocusRegion("attention")} valueKey={`${goal.goalId}:${needs.availability}:${needs.availability === "ok" ? needs.blocking : ""}`}
tone={needs.availability === "ok" && needs.blocking !== null && needs.blocking > 0 ? "danger" : needs.availability === "ok" ? "recede" : undefined}
help={needs.availability !== "ok" || needs.coverage !== "complete" ? <ContextHelp icon={CircleHelp} label={`${t("vitals.needsYou")} · ${needs.availability !== "ok" ? t("vitals.unavailable") : (locale === "zh-CN" ? "部分记录" : "Partial records")}`}><p>{needs.availability === "ok" ? t("vitals.needsYouPartial", {count: needs.listed}) : t("vitals.unavailable")}</p></ContextHelp> : undefined}
sub={needs.availability !== "ok" ? null : [
needs.oldestWaitingSince ? t("vitals.needsYouOldest", { time: relativeTime(needs.oldestWaitingSince, locale) ?? "" }) : null,
needs.optional ? t("vitals.needsYouOptional", { count: needs.optional }) : null,
].filter(Boolean).join(" · ")}>
{needs.availability !== "ok" ? t("vitals.unavailable") : needs.blocking === null ? "—"
{needs.availability !== "ok" ? t("vitals.unavailable") : needs.blocking === null ? "—" : needsEmpty ? t("vitals.needsYouNone")
: <>{needs.blocking > 0 ? <AlertCircle aria-hidden size={15} /> : null}{needs.blocking}</>}
</Vital>
<Vital label={t("vitals.agentTime")} onClick={() => onFocusRegion("usage")} sub={digest.usage.agentTimeMs === null ? undefined : windowLabel}
help={digest.usage.agentTimeMs === null ? <ContextHelp icon={Clock3} label={`${t("vitals.agentTime")} · ${t("vitals.notMeasured")}`}><p>{t("vitals.notMeasured")}</p></ContextHelp> : undefined}
tone={digest.usage.agentTimeMs === null ? "recede" : undefined} valueKey={`${goal.goalId}:${digest.usage.window}:${digest.usage.agentTimeMs}`}>
{digest.usage.agentTimeMs === null ? "—" : formatDurationMs(digest.usage.agentTimeMs)}
empty={digest.usage.agentTimeMs === null}
valueKey={`${goal.goalId}:${digest.usage.window}:${digest.usage.agentTimeMs}`}>
{digest.usage.agentTimeMs === null ? t("vitals.notMeasured") : formatDurationMs(digest.usage.agentTimeMs)}
</Vital>
<Vital label={t("vitals.cost")} onClick={() => onFocusRegion("usage")} sub={digest.usage.costUsd === null ? undefined : windowLabel}
help={digest.usage.costUsd === null ? <ContextHelp icon={CircleHelp} label={`${t("vitals.cost")} · ${t("vitals.notMeasured")}`}><p>{t("vitals.notMeasured")}</p></ContextHelp> : undefined}
tone={digest.usage.costUsd === null ? "recede" : undefined} valueKey={`${goal.goalId}:${digest.usage.window}:${digest.usage.costUsd}`}>
{digest.usage.costUsd === null ? "—" : formatCostUsd(digest.usage.costUsd)}
empty={digest.usage.costUsd === null}
valueKey={`${goal.goalId}:${digest.usage.window}:${digest.usage.costUsd}`}>
{digest.usage.costUsd === null ? t("vitals.notMeasured") : formatCostUsd(digest.usage.costUsd)}
</Vital>
<Vital label={t("vitals.lastActivity")} onClick={() => onFocusRegion("overview")}
sub={last ? last.actor || t("vitals.turn") : undefined}
help={!last ? <ContextHelp icon={Clock3} label={t("vitals.noActivity")}><p>{t("vitals.noActivity")}</p></ContextHelp> : undefined} tone={last ? undefined : "recede"}>
{last && lastTime ? lastTime : "—"}
help={!last ? <ContextHelp icon={Clock3} label={t("vitals.noActivity")}><p>{t("vitals.noActivity")}</p></ContextHelp> : undefined} empty={!last}
tone={last ? undefined : "recede"}>
{last && lastTime ? lastTime : t("vitals.noActivity")}
</Vital>
</section>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@ export function GoalResearchResults({goalId, api, zh, hideEmpty = false}: {goalI
void (async () => {
try {
const response = await fetch(api.indexUrl, {cache: "no-store"});
// This optional index may be absent. Keep the normal empty-state path;
// other HTTP and decoding failures must retain their error and retry.
if (response.status === 404 || response.status === 410) return;
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const collection = parsePresentationSurfaceCollectionResponse(await response.json());
const surfaces = collection.items.filter(surface => surface.goal_id === goalId
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ export function GoalResearchTimeline({timeline, zh, selectedIds, selectedEventId
span: "记录跨度", start: "起点", end: "末端", replan: "转向", continue: "继续", stop: "停止", wait: "等待", no_change: "无变化", ask_human: "请人决定",
...findingCopy.labels, unlabeled: "无结论", phaseNote: "按记录时间三等分,仅为阅读分组,不是预声明的评估窗口。",
noTime: "没有可用的记录时间,无法放上时间轴;下方记录仍可完整阅读。",
whole: "全程",
} : {
early: "Early", middle: "Middle", late: "Late", items: "Research items", findings: "Findings",
decisions: "Explicit decisions", none: "No explicit decision recorded", changed: "Changed", stopped: "Stopped", retained: "Retained",
Expand All @@ -54,6 +55,7 @@ export function GoalResearchTimeline({timeline, zh, selectedIds, selectedEventId
span: "Recorded span", start: "Start", end: "End", replan: "Replan", continue: "Continue", stop: "Stop", wait: "Wait", no_change: "No change", ask_human: "Ask human",
...findingCopy.labels, unlabeled: "No finding", phaseNote: "Equal thirds of record time, for reading only; not a pre-declared evaluation window.",
noTime: "No usable record time; the history cannot be placed on a time axis. All records remain readable below.",
whole: "Whole run",
};
const {start, end} = timeline;
if (start === null || end === null) return <ContextHelp label={zh ? "记录时间未知" : "Record time unknown"} icon={CircleHelp}><p className="research-history-note">{copy.noTime}</p></ContextHelp>;
Expand Down Expand Up @@ -89,7 +91,7 @@ export function GoalResearchTimeline({timeline, zh, selectedIds, selectedEventId
const groupedIds = new Set(dayGroups.flatMap(group => group.nodes.map(node => node.id)));
const swarm = timeline.singles.filter(id => !groupedIds.has(id)).map(id => timeline.nodes.get(id)!).filter(item => item.first !== null);
const unknownTime = [...timeline.nodes.values()].filter(item => item.first === null).length;
const phaseName = (item: TimelinePhase) => [copy.early, copy.middle, copy.late][item.index];
const phaseName = (item: TimelinePhase) => timeline.phases.length === 1 ? copy.whole : [copy.early, copy.middle, copy.late][item.index];
const phaseDescription = (item: TimelinePhase) => {
const counts = Object.entries(item.decisions);
return [
Expand All @@ -111,7 +113,7 @@ export function GoalResearchTimeline({timeline, zh, selectedIds, selectedEventId
};
return <div className="research-timeline">
<div className="research-timeline-guide"><span>{zh ? "记录时间" : "Record time"}</span><ContextHelp label={zh ? "时间线读法" : "Reading the timeline"}>
<p>{copy.phaseNote}</p><p>{copy.densityHint}{zh ? "。" : ". "}{zh ? "无记录空档不代表研究停止。" : "A recording gap does not establish that research stopped."}</p>
<p>{timeline.phases.length === 1 ? (zh ? "全部记录归入同一阅读分组,仅为阅读组织,不是预声明的评估窗口。" : "All records share one reading group, for reading only; not a pre-declared evaluation window.") : copy.phaseNote}</p><p>{copy.densityHint}{zh ? "。" : ". "}{zh ? "无记录空档不代表研究停止。" : "A recording gap does not establish that research stopped."}</p>
{!timeline.structuredCount ? <p>{copy.unstructuredHint}</p> : null}
<p>{copy.routeHint}{zh ? "。" : ". "}{zh ? "相邻点合拢,数字为研究项数,一次展开原始时间与依据。" : "Nearby points group; the number counts research items. Select once for original times and sources."}</p>
<p>{zh ? "未关联问题按首次记录 UTC 日期收纳,不推断路线或因果。决策轨迹按研究问题归组,组内保留各轮显式决策,最近记录在前;同名问题不会自动合并。" : "Unlinked questions group by first-record UTC date, without inferring a route or cause. Decision history groups explicit decisions by question, newest record first; matching titles do not merge questions."}</p>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1535,6 +1535,7 @@ const en = {
"brief.runningReading": "Reading run state…",
"brief.runningModeUnrecognized": "No verified run · {count} Goal(s) with unknown execution mode",
"brief.alsoQueued": "{count} more queued",
"brief.queuedOnly": "{count} queued",
"brief.claimed": "Claimed by host",
"brief.claimedEmpty": "No work is with a host",
"brief.completed": "Recently completed",
Expand Down Expand Up @@ -3186,6 +3187,7 @@ const zhCN: Record<WorkspaceMessageKey, string> = {
"brief.runningReading": "正在读取执行状态…",
"brief.runningModeUnrecognized": "没有可确认的执行 · {count} 个 Goal 执行方式未知",
"brief.alsoQueued": "另有 {count} 个已安排",
"brief.queuedOnly": "{count} 个已安排 · 由宿主 App 线程负责",
"brief.claimed": "宿主已领取",
"brief.claimedEmpty": "没有交给宿主的工作",
"brief.completed": "最近完成",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ export function ManagerBrief({ goals, onSelectGoal, operations = [], onSelectOpe
title={t("brief.operationDeliveryPending")} /> : null}
<BriefTile count={running.length} empty={runningEmpty} kind="running" live={running.some((goal) => presentGoalActivity(goal).live)} onSelectGoal={onSelectGoal}
rows={running.map((goal) => ({ goal, key: goal.goalId, meta: <RunningMeta goal={goal} />, text: goal.title }))}
title={t("brief.running")} total={running.length && queued ? t("brief.alsoQueued", { count: queued }) : null} />
title={t("brief.running")} total={queued ? (running.length ? t("brief.alsoQueued", { count: queued }) : t("brief.queuedOnly", { count: queued })) : null} />
{claimed.length ? <BriefTile count={claimed.length} empty={t("brief.claimedEmpty")} kind="claimed" onSelectGoal={onSelectGoal}
rows={claimed.map((goal) => ({ goal, key: goal.goalId, meta: <RunningMeta goal={goal} />, text: goal.title }))}
title={t("brief.claimed")} /> : null}
Expand Down
18 changes: 10 additions & 8 deletions apps/presentation/dashboard/vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,11 @@ import tailwindcss from "@tailwindcss/vite";
import react from "@vitejs/plugin-react";
import { defineConfig, type ProxyOptions } from "vite";

const statusTarget = "http://127.0.0.1:8766";
const statusProxy: ProxyOptions = {
target: statusTarget,
changeOrigin: false,
};
const chatTarget = "http://127.0.0.1:8767";
const chatProxy: ProxyOptions = {
target: chatTarget,
Expand All @@ -23,14 +28,11 @@ export default defineConfig({
host: "127.0.0.1",
port: 5173,
proxy: {
"/periodic-report-workspace": {
target: "http://127.0.0.1:8766",
changeOrigin: false,
},
"/status.json": {
target: "http://127.0.0.1:8766",
changeOrigin: false,
},
"/periodic-report-workspace": statusProxy,
"/periodic-report-workspace-projection": statusProxy,
"/extension-presentation-surfaces": statusProxy,
"/extension-projection": statusProxy,
"/status.json": statusProxy,
"/ssh-hosts": chatProxy,
"/api/chat": chatProxy,
"/api/actions": chatProxy,
Expand Down
8 changes: 5 additions & 3 deletions examples/personal-workspace-browser/goal-masthead.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -186,7 +186,7 @@ export const goalMastheadScenario = {
await page.getByTestId("goal-vitals").waitFor({ state: "visible", timeout: 15_000 });
assert.equal(await page.locator(".personal-channel-title h1").innerText(), title, "The stopped Goal stays selected across reload");
assert.equal((await vital(page, "状态")).value, "已停止", "Without an observed turn the Goal reads stopped");
assert.equal((await vital(page, "等你决定")).value, "0", "A stopped Goal leaves active attention");
assert.equal((await vital(page, "等你决定")).value, "暂无等待", "A stopped Goal with complete empty attention names the empty state");

await page.locator(".goal-lifecycle-trigger", { hasText: "恢复" }).click();
assert.equal(await page.getByRole("dialog", { name: "恢复这个 Goal?" }).count(), 0, "Resume uses the owner's reviewed preview, not a second confirmation");
Expand Down Expand Up @@ -220,9 +220,11 @@ export const goalMastheadScenario = {
await openGoal(quiet.page, title);
const agentTime = await vital(quiet.page, "Agent 用时");
const cost = await vital(quiet.page, "成本");
assert.deepEqual([agentTime.value, cost.value, cost.sub], ["—", "—", ""], "Absent usage is unknown, never $0.00");
assert.deepEqual([agentTime.value, cost.value, cost.sub], ["未采集", "未采集", ""], "Absent usage names the unmeasured state, never $0.00");
assert.deepEqual(await Promise.all([agentTime.cell, cost.cell].map(cell => cell.getAttribute("aria-disabled"))), ["true", "true"], "Unmeasured usage has no navigation affordance");
const last = await vital(quiet.page, "最近活动");
assert.deepEqual([last.value, last.sub], ["—", ""], "A claim or Todo write is not activity");
assert.deepEqual([last.value, last.sub], ["暂无观测到的活动", ""], "A claim or Todo write is not activity");
assert.equal(await last.cell.getAttribute("aria-disabled"), "true", "Missing activity has no navigation affordance");
assert.match(await readVitalHelp(quiet.page, cost.cell), /未采集/);
assert.match(await readVitalHelp(quiet.page, last.cell), /暂无观测到的活动/);
assert.notEqual((await vital(quiet.page, "状态")).value, "执行中");
Expand Down
4 changes: 2 additions & 2 deletions examples/personal-workspace-browser/navigation-sorting.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -179,8 +179,8 @@ export const navigationSortingScenario = {
const usageVitals = page.getByTestId("goal-vitals").locator(".goal-vital-cell")
.filter({ has: page.locator(".goal-vital-label", { hasText: /^(Agent 用时|成本)$/ }) });
const unknownUsage = await usageVitals.locator(".goal-vital-value").allTextContents();
if (unknownUsage.length !== 2 || unknownUsage.some(value => value !== "—")) {
throw new Error(`Unknown usage became a zero or disappeared: ${unknownUsage}`);
if (unknownUsage.length !== 2 || unknownUsage.some(value => value !== "未采集")) {
throw new Error(`Unknown usage lost its explicit unmeasured state: ${unknownUsage}`);
}
for (const label of ["Agent 用时", "成本"]) {
const cell = usageVitals.filter({ has: page.getByText(label, { exact: true }) });
Expand Down
Loading
Loading