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
6 changes: 3 additions & 3 deletions apps/presentation/dashboard/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,10 +38,10 @@
"smoke:home-route": "rm -rf /tmp/loopx-home-route-smoke && tsc --ignoreConfig --target ES2022 --module CommonJS --moduleResolution Node --ignoreDeprecations 6.0 --skipLibCheck --strict --outDir /tmp/loopx-home-route-smoke smoke/home-route-smoke.ts && node /tmp/loopx-home-route-smoke/home-route-smoke.js",
"smoke:delegation-preflight": "rm -rf node_modules/.cache/loopx-delegation-preflight-smoke && tsc --ignoreConfig --target ES2022 --module NodeNext --moduleResolution NodeNext --jsx react-jsx --skipLibCheck --strict --rootDir . --outDir node_modules/.cache/loopx-delegation-preflight-smoke smoke/delegation-preflight-smoke.tsx src/data/delegation-preflight.ts src/features/personal-workspace/delegation-preflight-status.tsx && node node_modules/.cache/loopx-delegation-preflight-smoke/smoke/delegation-preflight-smoke.js",
"smoke:delegation-preflight-browser": "node --experimental-strip-types smoke/delegation-preflight-browser-smoke.mjs",
"smoke:personal-workspace": "npm run smoke:conversation-history-order && npm run smoke:goal-order && npm run smoke:goal-activity && npm run smoke:proposal-recency && node src/features/personal-workspace/conversation-order.test.mjs && node --no-warnings --experimental-strip-types --test src/features/personal-workspace/steering-recovery.test.mjs && node --no-warnings --experimental-strip-types --test src/features/personal-workspace/goal-suggestions.test.mjs && npm run smoke:delegation-preflight && node src/features/personal-workspace/workspace-theme.test.mjs && node src/features/personal-workspace/font-token.test.mjs && node src/features/personal-workspace/personal-workspace-contract.test.mjs && node ../../../examples/personal-workspace-browser-smoke.mjs",
"smoke:personal-workspace": "npm run smoke:conversation-history-order && npm run smoke:goal-order && npm run smoke:goal-activity && npm run smoke:proposal-recency && node src/features/personal-workspace/conversation-order.test.mjs && node --no-warnings --experimental-strip-types --test src/features/personal-workspace/steering-recovery.test.mjs && node --no-warnings --experimental-strip-types --test src/features/personal-workspace/since-you-looked.test.mjs && node --no-warnings --experimental-strip-types --test src/features/personal-workspace/goal-suggestions.test.mjs && npm run smoke:delegation-preflight && node src/features/personal-workspace/workspace-theme.test.mjs && node src/features/personal-workspace/font-token.test.mjs && node src/features/personal-workspace/personal-workspace-contract.test.mjs && node ../../../examples/personal-workspace-browser-smoke.mjs",
"smoke:workspace-locale": "LOOPX_PERSONAL_WORKSPACE_SCENARIO=workspace-locale node ../../../examples/personal-workspace-browser-smoke.mjs",
"smoke:personal-workspace-packaged": "npm run smoke:conversation-history-order && npm run smoke:goal-order && npm run smoke:goal-activity && npm run smoke:proposal-recency && node src/features/personal-workspace/conversation-order.test.mjs && node --no-warnings --experimental-strip-types --test src/features/personal-workspace/steering-recovery.test.mjs && node --no-warnings --experimental-strip-types --test src/features/personal-workspace/goal-suggestions.test.mjs && npm run smoke:delegation-preflight && node src/features/personal-workspace/workspace-theme.test.mjs && node src/features/personal-workspace/font-token.test.mjs && node src/features/personal-workspace/personal-workspace-contract.test.mjs && node ../../../examples/personal-workspace-browser/server-startup.test.mjs && LOOPX_PERSONAL_WORKSPACE_PACKAGED=1 LOOPX_PLAYWRIGHT_PACKAGE=\"$PWD/node_modules/playwright\" node ../../../examples/personal-workspace-browser-smoke.mjs && LOOPX_PLAYWRIGHT_PACKAGE=\"$PWD/node_modules/playwright\" node smoke/team-evidence-return-smoke.mjs && LOOPX_PLAYWRIGHT_PACKAGE=\"$PWD/node_modules/playwright\" node smoke/team-feedback-recovery-smoke.mjs && LOOPX_PERSONAL_WORKSPACE_PACKAGED=1 LOOPX_PLAYWRIGHT_PACKAGE=\"$PWD/node_modules/playwright\" node smoke/goal-workspace-craft-browser-smoke.mjs",
"smoke:personal-workspace-packaged:catalog": "npm run smoke:conversation-history-order && npm run smoke:goal-order && npm run smoke:goal-activity && npm run smoke:proposal-recency && node src/features/personal-workspace/conversation-order.test.mjs && node --no-warnings --experimental-strip-types --test src/features/personal-workspace/steering-recovery.test.mjs && node --no-warnings --experimental-strip-types --test src/features/personal-workspace/goal-suggestions.test.mjs && npm run smoke:delegation-preflight && node src/features/personal-workspace/workspace-theme.test.mjs && node src/features/personal-workspace/font-token.test.mjs && node src/features/personal-workspace/personal-workspace-contract.test.mjs && node ../../../examples/personal-workspace-browser/server-startup.test.mjs && LOOPX_PERSONAL_WORKSPACE_PACKAGED=1 LOOPX_PLAYWRIGHT_PACKAGE=\"$PWD/node_modules/playwright\" node ../../../examples/personal-workspace-browser-smoke.mjs",
"smoke:personal-workspace-packaged": "npm run smoke:conversation-history-order && npm run smoke:goal-order && npm run smoke:goal-activity && npm run smoke:proposal-recency && node src/features/personal-workspace/conversation-order.test.mjs && node --no-warnings --experimental-strip-types --test src/features/personal-workspace/steering-recovery.test.mjs && node --no-warnings --experimental-strip-types --test src/features/personal-workspace/since-you-looked.test.mjs && node --no-warnings --experimental-strip-types --test src/features/personal-workspace/goal-suggestions.test.mjs && npm run smoke:delegation-preflight && node src/features/personal-workspace/workspace-theme.test.mjs && node src/features/personal-workspace/font-token.test.mjs && node src/features/personal-workspace/personal-workspace-contract.test.mjs && node ../../../examples/personal-workspace-browser/server-startup.test.mjs && LOOPX_PERSONAL_WORKSPACE_PACKAGED=1 LOOPX_PLAYWRIGHT_PACKAGE=\"$PWD/node_modules/playwright\" node ../../../examples/personal-workspace-browser-smoke.mjs && LOOPX_PLAYWRIGHT_PACKAGE=\"$PWD/node_modules/playwright\" node smoke/team-evidence-return-smoke.mjs && LOOPX_PLAYWRIGHT_PACKAGE=\"$PWD/node_modules/playwright\" node smoke/team-feedback-recovery-smoke.mjs && LOOPX_PERSONAL_WORKSPACE_PACKAGED=1 LOOPX_PLAYWRIGHT_PACKAGE=\"$PWD/node_modules/playwright\" node smoke/goal-workspace-craft-browser-smoke.mjs",
"smoke:personal-workspace-packaged:catalog": "npm run smoke:conversation-history-order && npm run smoke:goal-order && npm run smoke:goal-activity && npm run smoke:proposal-recency && node src/features/personal-workspace/conversation-order.test.mjs && node --no-warnings --experimental-strip-types --test src/features/personal-workspace/steering-recovery.test.mjs && node --no-warnings --experimental-strip-types --test src/features/personal-workspace/since-you-looked.test.mjs && node --no-warnings --experimental-strip-types --test src/features/personal-workspace/goal-suggestions.test.mjs && npm run smoke:delegation-preflight && node src/features/personal-workspace/workspace-theme.test.mjs && node src/features/personal-workspace/font-token.test.mjs && node src/features/personal-workspace/personal-workspace-contract.test.mjs && node ../../../examples/personal-workspace-browser/server-startup.test.mjs && LOOPX_PERSONAL_WORKSPACE_PACKAGED=1 LOOPX_PLAYWRIGHT_PACKAGE=\"$PWD/node_modules/playwright\" node ../../../examples/personal-workspace-browser-smoke.mjs",
"smoke:personal-workspace-packaged:team": "LOOPX_PLAYWRIGHT_PACKAGE=\"$PWD/node_modules/playwright\" node smoke/team-evidence-return-smoke.mjs && LOOPX_PLAYWRIGHT_PACKAGE=\"$PWD/node_modules/playwright\" node smoke/team-feedback-recovery-smoke.mjs",
"smoke:personal-workspace-packaged:craft": "LOOPX_PERSONAL_WORKSPACE_PACKAGED=1 LOOPX_PLAYWRIGHT_PACKAGE=\"$PWD/node_modules/playwright\" node smoke/goal-workspace-craft-browser-smoke.mjs",
"smoke:todo-resume-condition": "rm -rf /tmp/loopx-todo-resume-condition-smoke && tsc --ignoreConfig --target ES2022 --module commonjs --moduleResolution node --ignoreDeprecations 6.0 --skipLibCheck --strict --outDir /tmp/loopx-todo-resume-condition-smoke smoke/todo-resume-condition-smoke.ts src/features/personal-workspace/todo-resume-condition.ts && node /tmp/loopx-todo-resume-condition-smoke/smoke/todo-resume-condition-smoke.js",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,11 @@
.goal-overview .goal-overview-heading button:hover { border-color: transparent; background: var(--pw-hover); color: var(--pw-text); }

.goal-overview-objective { max-width: 44em; color: var(--pw-text); font-size: var(--text-heading); font-weight: 600; letter-spacing: -0.015em; line-height: var(--leading-heading); text-wrap: pretty; }
/* One quiet line above the hero: what changed since the owner's last look. Idle content stays still. */
.goal-overview-since { display: flex; flex-wrap: wrap; align-items: center; gap: 6px 14px; margin: 0 0 16px; padding-bottom: 12px; border-bottom: 1px solid var(--pw-line); color: var(--pw-muted); font-size: var(--text-caption); line-height: var(--leading-caption); font-variant-numeric: tabular-nums; }
.goal-overview-since button { min-height: 28px; padding: 0 10px; border: 1px solid var(--pw-line-strong); border-radius: 6px; background: transparent; color: var(--pw-text); font-size: var(--text-caption); cursor: pointer; }
.goal-overview-since button:hover { border-color: var(--pw-blue); color: var(--pw-blue-ink); }
.goal-overview-since button:focus-visible { outline: 2px solid var(--pw-blue); outline-offset: 2px; }
.goal-overview-note { max-width: 56em; color: var(--pw-muted); font-size: var(--text-body); line-height: var(--leading-body); }
.goal-overview-note small { color: var(--pw-faint); }

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,28 @@ import type {GoalWorkspaceDigest} from "../../../../../../loopx/control_plane/pr
import { GoalAcceptanceObservationCard } from "./goal-acceptance-observation-card";
import type { WorkspaceActionPreview, WorkspaceActionPreviewRequest, WorkspaceDrawerSelection, WorkspaceGoal, WorkspaceGoalTab, WorkspaceModel, WorkspaceTimelineItem } from "./personal-workspace-model";
import { useWorkspaceI18n } from "./i18n";
import type { SinceYouLookedDelta } from "./since-you-looked";
import {goalAcceptanceDisplayStatus, projectGoalAcceptanceCriteria} from "../../data/goal-acceptance-observation";
import "./goal-overview.css";

export const goalOverviewAttentionId = "goal-overview-attention";

export function GoalOverview({ active, goal, items, userTodos, readOnly, onOpenDetails, onSelect, onView, selectedTodoId, board, results, proposals, onPreview, historyRefreshRevision, review, digest, railOpen, onCloseRail, onOpenAgentConversation }: {
/** "3 hours ago" for the owner's last look; falls back to the date for older marks. */
function relativeLookLabel(iso: string, locale: string): string {
const then = new Date(iso).getTime();
if (Number.isNaN(then)) return iso;
const minutes = Math.max(1, Math.round((Date.now() - then) / 60_000));
const format = new Intl.RelativeTimeFormat(locale === "zh-CN" ? "zh-CN" : "en", { numeric: "always" });
if (minutes < 60) return format.format(-minutes, "minute");
if (minutes < 60 * 24) return format.format(-Math.round(minutes / 60), "hour");
if (minutes < 60 * 24 * 14) return format.format(-Math.round(minutes / (60 * 24)), "day");
return new Intl.DateTimeFormat(locale === "zh-CN" ? "zh-CN" : "en", { month: "short", day: "numeric" }).format(then);
}

export function GoalOverview({ active, goal, items, userTodos, readOnly, onOpenDetails, onSelect, onView, selectedTodoId, board, results, proposals, onPreview, historyRefreshRevision, review, digest, railOpen, onCloseRail, onOpenAgentConversation, sinceYouLooked, onMarkSeen }: {
onOpenAgentConversation?: (agentId: string) => void;
/** Material changes since the owner's last acknowledged look; null when none. */
sinceYouLooked?: SinceYouLookedDelta | null; onMarkSeen?: () => void;
railOpen: boolean; onCloseRail: () => void;
review: DeliveryReviewRead; digest: GoalWorkspaceDigest;
active: boolean; goal: WorkspaceGoal; items: WorkspaceTimelineItem[]; userTodos: WorkspaceModel["userTodos"];
Expand Down Expand Up @@ -58,7 +73,19 @@ export function GoalOverview({ active, goal, items, userTodos, readOnly, onOpenD
const criteria = contract ? projectGoalAcceptanceCriteria(contract) : [];
const passed = criteria.filter(criterion => criterion.state === "accepted").length;
const acceptanceCopy = goalAcceptanceContractCopy[locale === "zh-CN" ? "zh-CN" : "en"];
const sinceParts = sinceYouLooked ? [
sinceYouLooked.newlyDone ? t("since.newlyDone", { count: sinceYouLooked.newlyDone }) : null,
sinceYouLooked.newDecisions ? t("since.newDecisions", { count: sinceYouLooked.newDecisions }) : null,
sinceYouLooked.newlyBlocked ? t("since.newlyBlocked", { count: sinceYouLooked.newlyBlocked }) : null,
sinceYouLooked.resultsReturned ? t("since.resultsReturned", { count: sinceYouLooked.resultsReturned }) : null,
].filter((part): part is string => part !== null) : [];
return <section className="goal-overview" aria-label={t("header.overview")}>
{sinceYouLooked && sinceParts.length ? (
<p className="goal-overview-since" data-testid="goal-since-you-looked" role="status">
<span>{t("since.lead", { when: relativeLookLabel(sinceYouLooked.since, locale) })} {sinceParts.join(locale === "zh-CN" ? "、" : ", ")}</span>
{onMarkSeen ? <button onClick={onMarkSeen} type="button">{t("since.markSeen")}</button> : null}
</p>
) : null}
<div className={`goal-overview-summary${showAttention ? " has-attention" : ""}`}>
<section className="goal-overview-direction" aria-label={copy.direction}>
<header className="goal-overview-heading">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -589,6 +589,12 @@ const en = {
"header.chatRuntime": "Chat",
"header.workAgentCount": "{count} work Agents",
"header.overview": "Overview",
"since.lead": "Since you looked ({when}):",
"since.newlyDone": "{count} newly done",
"since.newDecisions": "{count} new decision(s) for you",
"since.newlyBlocked": "{count} newly blocked",
"since.resultsReturned": "{count} result(s) returned",
"since.markSeen": "Mark seen",
"header.chat": "Chat",
"header.files": "Files",
"header.goalCapabilities": "Capability settings",
Expand Down Expand Up @@ -2106,6 +2112,12 @@ const zhCN: Record<WorkspaceMessageKey, string> = {
"header.chatRuntime": "Chat",
"header.workAgentCount": "{count} 个工作 Agent",
"header.overview": "概览",
"since.lead": "自你上次查看({when}):",
"since.newlyDone": "新完成 {count} 项",
"since.newDecisions": "新增 {count} 项等你决定",
"since.newlyBlocked": "新受阻 {count} 项",
"since.resultsReturned": "新成果 {count} 项",
"since.markSeen": "标记已读",
"header.chat": "对话",
"header.files": "成果",
"header.goalCapabilities": "能力配置",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -600,6 +600,34 @@ for (const key of ["home.firstGoal.title", "home.firstGoal.lead", "home.firstGoa
assert.equal((i18n.match(new RegExp(`"${key.replace(/\./g, "\\.")}":`, "g")) ?? []).length, 2, `${key} exists in both locales`);
}

// Since you looked (RFC goal-workspace-surface §5.7): a browser-local marker
// per source and Goal, typed counts and ids only, no cost, no service write;
// the line appears only for a material delta. Mark seen acknowledges what is
// on screen; leaving acknowledges the look that was compared on that visit.
// Decisions reuse the typed attention classification, the comparison waits
// for an owner refresh still in flight, and denied storage only loses memory.
const sinceYouLooked = source("./since-you-looked.ts");
const seenMarkerType = sinceYouLooked.slice(sinceYouLooked.indexOf("export type SeenMarker = {"), sinceYouLooked.indexOf("};", sinceYouLooked.indexOf("export type SeenMarker = {")));
assert.doesNotMatch(seenMarkerType, /cost|title|text|usd/i, "The marker carries no titles, text or cost");
assert.match(sinceYouLooked, /export const seenMarkerIdLimit = 200;/, "Done and result ids are bounded");
assert.match(page, /seenMarkerStorageKey\(seenStatusSource, selectedGoal\.goalId\)/, "Markers are scoped per status source and Goal");
assert.match(page, /if \(delta\.material\) setSinceYouLooked\(/, "Only a material delta shows the line");
assert.match(page, /advanceSeenMarker\(previous\.goalId, previous\.source, "compared"\);[\s\S]*?setSinceYouLooked\(\(shown\) => shown && shown\.goalId === previous\.goalId/, "Leaving the Goal records the compared look and acknowledges a shown line");
assert.match(page, /advanceSeenMarker\(sinceYouLooked\.goalId, sinceYouLooked\.source, "latest"\)/, "Mark seen acknowledges the current look");
assert.match(page, /if \(stored && stored\.observedAt > input\.observedAt\) return;/, "No write moves the marker back behind a newer acknowledgment");
assert.match(page, /if \(look === "latest"\) comparedSeenInputsRef\.current\.set\(scope, input\);/, "Mark seen becomes the look the visit stands on, so leaving cannot undo it");
assert.match(sinceYouLooked, /function coversWholeSet\(/, "Capped or partial id sets report count growth, never unretained ids as new");
assert.match(sinceYouLooked, /newlyDone = coversWholeSet\(previous\.doneTodoIds, null\) \? doneById : 0;/, "With no done total a capped done history proves no completion");
assert.match(page, /decisionIds: model\.userTodos\.filter\(\(item\) => item\.goalId === selectedGoal\.goalId && item\.blocking\)/, "Only items that wait on the owner are decisions; optional user actions keep the typed classification");
assert.match(page, /if \(refreshState === "loading"\) return;[\s\S]*?seenComparedRef\.current = scope;/, "The comparison waits for an owner refresh still in flight instead of latching on the snapshot it replaces");
const seenBlock = page.slice(page.indexOf("const seenStatusSource"), page.indexOf("function markSeen"));
assert.doesNotMatch(seenBlock, /window\.localStorage/, "Storage is acquired through the guarded helper, never read as a bare property");
assert.match(sinceYouLooked, /export function seenMarkerStorage\([\s\S]*?try \{[\s\S]*?scope\.localStorage/, "Acquiring storage sits inside the failure boundary");
assert.doesNotMatch(sinceYouLooked + page.slice(page.indexOf("const seenStatusSource"), page.indexOf("function markSeen")), /fetch\(|requestJson|\/api\//, "No service write or read belongs to the marker");
for (const key of ["since.lead", "since.newlyDone", "since.newDecisions", "since.newlyBlocked", "since.resultsReturned", "since.markSeen"]) {
assert.equal((i18n.match(new RegExp(`"${key.replace(/\./g, "\\.")}":`, "g")) ?? []).length, 2, `${key} exists in both locales`);
}

// State-aware suggestions cite digest, Todo, execution and acceptance facts;
// they never replace the shipped steward prompt row, and a stopped Goal or an
// unknown execution state produces none.
Expand Down
Loading
Loading