Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
681d0b9
feat(dashboard): denser workspace with stage bar and run band
xiaoan17 Oct 30, 2026
104e23c
refactor(dashboard): radius and spacing tokens across the workspace
xiaoan17 Oct 30, 2026
fac949d
refactor(dashboard): linear goal flow and hole-free home tiles
xiaoan17 Oct 30, 2026
7dd3246
refactor(dashboard): quiet empty research and result sections
xiaoan17 Oct 30, 2026
a60a5e8
fix(developers): wrap commands at word boundaries, size diff columns
xiaoan17 Oct 30, 2026
0b73dad
refactor(developers): render projection diff rows as cards
xiaoan17 Oct 30, 2026
e96faa8
refactor(dashboard): all work becomes the last row of the goal document
xiaoan17 Oct 30, 2026
3c7c83c
refactor(dashboard): board cards start with the title
xiaoan17 Oct 30, 2026
24f1756
style(dashboard): align board card metrics with the design board
xiaoan17 Oct 30, 2026
b46c8f1
docs(design): add the dashboard design system
xiaoan17 Oct 30, 2026
b3456e1
fix(dashboard): keep lane scrollers and the goal card reason line
xiaoan17 Oct 30, 2026
06fdf26
fix(dashboard): keep the Goal row activity word and the card's second…
xiaoan17 Oct 31, 2026
c22d56f
feat(dashboard): keep cost and Agent time in the stage bar
xiaoan17 Oct 31, 2026
f2546f5
fix(dashboard): read typed outcomes, owner-declared empty results and…
xiaoan17 Oct 31, 2026
b782421
test(dashboard): read the stage bar, home lanes and drawer owners
xiaoan17 Oct 31, 2026
6a936ea
docs(design): describe the stage bar the Goal page ships
xiaoan17 Oct 31, 2026
624d70d
fix(dashboard): keep the health failure reason in the open
xiaoan17 Oct 31, 2026
715887d
test(dashboard): read the unreadable-run and notification owners
xiaoan17 Oct 31, 2026
9bd1d97
docs(workspace): describe the notification bell, not the removed sett…
xiaoan17 Oct 31, 2026
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
1 change: 1 addition & 0 deletions apps/presentation/dashboard/public/board-preview.css

Large diffs are not rendered by default.

31 changes: 31 additions & 0 deletions apps/presentation/dashboard/public/board-preview.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<!doctype html>
<html lang="en"><head><meta charset="utf-8"><title>LoopX board preview</title>
<link rel="stylesheet" href="/board-preview.css">
<style>body{margin:0;background:#fbfaf7} .personal-workspace-shell{display:block;min-height:auto} .wrap{width:1384px;margin:0 auto;padding:24px 28px} .personal-task-kanban{grid-template-columns:repeat(4,minmax(0,1fr))} .personal-task-board{padding:0}</style>
</head><body><div class="personal-workspace-shell" data-pw-theme="loopx"><main class="personal-workspace-main"><div class="wrap">
<div class="personal-task-board">
<header class="personal-task-view-toolbar">
<div><strong>Tasks</strong></div>
<label class="personal-task-search"><input type="search" placeholder="Search tasks" aria-label="Search tasks"></label>
<label class="personal-task-filter"><select aria-label="Task state filters"><option>All</option><option>Needs a decision</option><option>Open</option><option>Blocked and deferred</option><option>Done</option></select></label>
<div class="personal-task-view-switch" role="group"><button type="button">List</button><button type="button" aria-pressed="true">Board</button></div>
</header>
<div class="personal-task-kanban">
<section class="personal-object-list personal-task-lane">
<header><strong><i aria-hidden="true" class="personal-kanban-dot tone-attention"></i>Needs your confirmation</strong><span>0</span></header>
<div class="personal-task-lane-scroll"><p class="personal-task-empty">No needs your confirmation.</p></div>
</section><section class="personal-object-list personal-task-lane">
<header><strong><i aria-hidden="true" class="personal-kanban-dot tone-progress"></i>Queued / Running</strong><span>2</span></header>
<div class="personal-task-lane-scroll"><button type="button"><span aria-hidden="true">!</span><strong>确认第 5 批清理清单(3 个零引用辅助函数)</strong><small><span class="personal-row-status is-blocking">待确认</span><span class="personal-task-age">18 天</span></small></button>
<button type="button"><span aria-hidden="true">!</span><strong>是否退役 TR-C 目标:源注册表缺失且状态文件不存在</strong><small><span class="personal-row-status is-blocking">待确认</span><span class="personal-task-age">18 天</span></small></button></div>
</section><section class="personal-object-list personal-task-lane">
<header><strong><i aria-hidden="true" class="personal-kanban-dot tone-schedule"></i>Scheduled & continuous</strong><span>0</span></header>
<div class="personal-task-lane-scroll"><p class="personal-task-empty">No scheduled & continuous.</p></div>
</section><section class="personal-object-list personal-task-lane">
<header><strong><i aria-hidden="true" class="personal-kanban-dot tone-done"></i>Completed</strong><span>2</span></header>
<div class="personal-task-lane-scroll"><button type="button"><span aria-hidden="true">!</span><strong>删除 docker-compose.yml 中仅回滚保留的 cache 卷配置</strong><small><span class="personal-row-status is-pending">进行中</span><span class="personal-task-age">12 分钟</span></small></button>
<button type="button"><span aria-hidden="true">!</span><strong>统一数据版本表述:sstp110 / 本地缓存 / 论文正文</strong><small><span class="personal-row-status is-open">未开始</span><span class="personal-task-age">2 天</span></small></button></div>
</section>
</div>
</div>
</div></main></div></body></html>
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ try {
const readsBeforeHiddenRefresh = historyReads;
state.done++;
await page.getByRole('button', { name: '刷新状态', exact: true }).click();
await page.waitForFunction(() => document.querySelectorAll('[data-testid="goal-vitals"] .goal-vital')[1]?.querySelector('.goal-vital-value')?.textContent === '24/29');
await page.waitForFunction(() => document.querySelector('[data-testid="goal-vitals"] .goal-stagebar-work')?.textContent === '24/29');
assert.equal(historyReads, readsBeforeHiddenRefresh, 'Refreshing a hidden inventory does not read completed history');
await overview.evaluate(element => element.style.removeProperty('max-width'));
await inlineRail.locator('.goal-work-filters').waitFor({ state: 'visible' });
Expand Down Expand Up @@ -178,7 +178,7 @@ try {
state.quiet = true;
await page.setViewportSize({ width: 1440, height: 1000 });
await page.getByRole('button', { name: '刷新状态', exact: true }).click();
await page.waitForFunction(() => document.querySelectorAll('[data-testid="goal-vitals"] .goal-vital')[1]?.querySelector('.goal-vital-value')?.textContent === '0/0');
await page.waitForFunction(() => document.querySelector('[data-testid="goal-vitals"] .goal-stagebar-work')?.textContent === '0/0');
await inlineRail.waitFor({ state: 'detached' });
assert.ok(await page.locator('.goal-workspace-composition').evaluate(element => {
const frame = element.getBoundingClientRect(), document = element.querySelector('.goal-workspace-document').getBoundingClientRect();
Expand Down
4 changes: 4 additions & 0 deletions apps/presentation/dashboard/smoke/home-route-smoke.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,10 @@ const designSource = readFileSync("design.md", "utf8");

excludes(routerSource, 'view: z.enum(["ops", "share"])', "legacy dual-view routing removed");
excludes(routerSource, 'chatRoute', "legacy standalone chat route removed");
includes(routerSource, 'path: "/design/local-dashboard"', "temporary local dashboard design route");
includes(routerSource, "<DashboardPage search={search} navigate={navigate}/>", "shared dashboard route adapter");
excludes(dashboardSource, "dashboardRoute.useSearch()", "dashboard search is provided by its route adapter");
excludes(dashboardSource, "dashboardRoute.useNavigate()", "dashboard navigation is provided by its route adapter");
includes(dashboardSource, 'data-testid="personal-goal-home"', "personal workspace route");
includes(dashboardSource, "<PersonalWorkspacePage", "personal workspace rendering");
includes(dashboardSource, "buildPersonalHomeModel(payload, rows,", "public-safe workspace projection");
Expand Down
14 changes: 14 additions & 0 deletions apps/presentation/dashboard/src/data/status.ts
Original file line number Diff line number Diff line change
Expand Up @@ -518,6 +518,15 @@ export const projectMapSchema = z.object({
files_checked: z.number().optional().nullable(),
});

/** Closed outcome vocabularies a run writer already validates. They are the only
* facts that may say a run advanced or failed; `classification` stays a free
* display string. Owners: loopx/control_plane/work_items/delivery_outcome.py
* (DeliveryOutcome) and loopx/control_plane/work_items/progress_result.py
* (ProgressResultClass). Both are exposed on every compact run record by
* loopx/control_plane/runtime/run_compaction.py:96,99-100. */
export const deliveryOutcomeSchema = z.enum(["surface_only", "outcome_gap", "outcome_progress", "primary_goal_outcome"]);
export const progressResultClassSchema = z.enum(["advanced", "unchanged", "blocked", "exploration_exhausted", "no_followup"]);

export const runRecordSchema = z.object({
generated_at: z.string(),
goal_id: z.string(),
Expand All @@ -536,6 +545,11 @@ export const runRecordSchema = z.object({
operator_gate_resume_contract: operatorGateResumeContractSchema.optional().nullable(),
controller_readiness: controllerReadinessSchema.optional().nullable(),
project_map: projectMapSchema.optional().nullable(),
/** An unrecognized token degrades to `null` (unknown), never to a guess. */
delivery_outcome: deliveryOutcomeSchema.optional().nullable().catch(null),
progress_observation: z.object({
result_class: progressResultClassSchema.optional().nullable().catch(null),
}).optional().nullable().catch(null),
});

export const runGoalSchema = z.object({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -254,6 +254,7 @@ export function ChannelHeader({
{selectedGoal?.loadState ? <p role="status">{t(selectedGoal.loadState === "error" ? "startup.goalError" : "startup.goalLoading")}</p> : null}
</div>
{selectedGoal ? (
<>
<div className="personal-goal-navigation">
<nav aria-label={t("header.goalView")} className="personal-goal-tabs">
<button aria-current={selectedGoalTab === "overview" ? "page" : undefined} onClick={() => onSelectGoalTab("overview")} type="button">{t("header.overview")}</button>
Expand All @@ -262,9 +263,10 @@ export function ChannelHeader({
<button aria-current={selectedGoalTab === "chat" ? "page" : undefined} onClick={() => onSelectGoalTab("chat")} type="button">{t("header.chat")}</button>
<button aria-current={selectedGoalTab === "files" ? "page" : undefined} onClick={() => onSelectGoalTab("files")} type="button">{t("header.files")}</button>
</nav>
{researchControl}
{runtimeControl}
</div>
{researchControl ? <div className="personal-goal-subnav">{researchControl}</div> : null}
</>
) : !scopeUnavailable ? (
<nav aria-label={t("header.managerView")} className="personal-goal-tabs">
<button aria-current={!managerChatOpen ? "page" : undefined} onClick={onReturnManagerHome} type="button">{t("header.managerOverview")}</button>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import {ContextHelp} from "./context-help";
import { ZCodeGoalControl } from "./zcode-goal-control";
import { GoalAcceptanceObservationCard } from "./goal-acceptance-observation-card";
import { goalShortLabel } from "./goal-display";
import { AttentionActions } from "./attention-actions";
import { AttentionDetailCard, AttentionProvenance } from "./attention-detail-card";
import { attentionSuccessor } from "./attention-details";
Expand Down Expand Up @@ -703,7 +704,7 @@ export function ContextDrawer({ agents, attentionHistory = [], onSelectAttention
<section aria-label={t("drawer.taskInfo")} className="personal-task-inspector-fields">
<h4>{t("drawer.taskInfo")}</h4>
<dl>
<div><dt>Goal</dt><dd>{selection.item.goalTitle}</dd></div>
<div><dt>Goal</dt><dd><span className="drawer-goal-name" title={selection.item.goalTitle}>{selection.item.goalTitle}</span></dd></div>
<div><dt>{t("common.owner")}</dt><dd>{selection.item.claimedBy ? selection.item.ownerLabel ?? selection.item.claimedBy : t("drawer.notAssigned")}</dd></div>
<div><dt>{t("common.status")}</dt><dd>{todoCompleted ? t("drawer.taskStatusCompleted") : selection.item.retryReadback ? t("drawer.resumePending") : selection.item.status === "deferred" ? t("drawer.taskStatusDeferred") : selection.item.status === "blocked" ? t("drawer.taskStatusBlocked") : t("drawer.taskStatusOpen")}</dd></div>
<div><dt>{t("drawer.priority")}</dt><dd>{selection.item.priority ?? t("drawer.notSet")}</dd></div>
Expand Down Expand Up @@ -801,8 +802,8 @@ export function ContextDrawer({ agents, attentionHistory = [], onSelectAttention
<>
<section className="personal-detail-card personal-goal-detail-summary">
<small>{localizedGoalState(selection.item.state, locale)}</small>
<h3>{selection.item.title}</h3>
<p>{selection.item.agentSentence}</p>
<h3 title={selection.item.title}>{goalShortLabel(selection.item)}</h3>
<p className="personal-goal-detail-sentence" title={selection.item.agentSentence}>{selection.item.agentSentence}</p>
</section>
<GoalAcceptanceObservationCard goal={selection.item} />
<section className="personal-detail-card personal-goal-detail-usage">
Expand Down Expand Up @@ -1065,7 +1066,7 @@ export function ContextDrawer({ agents, attentionHistory = [], onSelectAttention
<h3>{selection.item.title}</h3>
<p>{selection.item.latestActivity}</p>
<dl>
<div><dt>Goal</dt><dd>{selection.item.goalTitle}</dd></div>
<div><dt>Goal</dt><dd><span className="drawer-goal-name" title={selection.item.goalTitle}>{selection.item.goalTitle}</span></dd></div>
<div><dt>{t("drawer.progress")}</dt><dd>{selection.item.completedSteps}/{selection.item.totalSteps}</dd></div>
</dl>
</section>
Expand Down Expand Up @@ -1106,7 +1107,7 @@ export function ContextDrawer({ agents, attentionHistory = [], onSelectAttention
<h3>{selection.item.title}</h3>
<p>{selection.item.latestActivity}</p>
<dl>
<div><dt>Goal</dt><dd>{selection.item.goalTitle}</dd></div>
<div><dt>Goal</dt><dd><span className="drawer-goal-name" title={selection.item.goalTitle}>{selection.item.goalTitle}</span></dd></div>
<div><dt>{t("drawer.progress")}</dt><dd>{selection.item.completedSteps}/{selection.item.totalSteps}</dd></div>
<div><dt>{t("drawer.sessionStatus")}</dt><dd>{localizedSessionStatus(selection.item.sessionStatus ?? selection.item.status, t)}</dd></div>
<div><dt>{t("drawer.sessionRecoverable")}</dt><dd>{selection.item.resumable === false ? t("drawer.resumeNo") : t("drawer.resumeYes")}</dd></div>
Expand Down Expand Up @@ -1163,7 +1164,7 @@ export function ContextDrawer({ agents, attentionHistory = [], onSelectAttention
<h3>{selection.item.title}</h3>
<p>{selection.item.summary ?? t("drawer.outputRecorded")}</p>
<dl>
<div><dt>Goal</dt><dd>{selection.item.goalTitle ?? selection.item.goalId}</dd></div>
<div><dt>Goal</dt><dd><span className="drawer-goal-name" title={selection.item.goalTitle}>{selection.item.goalTitle ?? selection.item.goalId}</span></dd></div>
<div><dt>{t("drawer.outputTodo")}</dt><dd>{selection.item.todoId ?? t("drawer.notLinked")}</dd></div>
<div><dt>{t("drawer.outputRun")}</dt><dd>{selection.item.runId ?? t("drawer.notLinked")}</dd></div>
<div><dt>Agent</dt><dd>{selection.item.agentLabel ?? selection.item.agentId ?? "LoopX"}</dd></div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,8 +71,8 @@ export function DeliveryReview(props: DeliveryReviewProps) {
return <DeliveryReviewContent {...props} review={review} />;
}

export function DeliveryReviewContent({ goal, items, userTodos, onSelect, review, active, hideWorkMap = false, hideAcceptanceContract = false }: DeliveryReviewProps & {
review: DeliveryReviewRead; hideWorkMap?: boolean; hideAcceptanceContract?: boolean;
export function DeliveryReviewContent({ goal, items, userTodos, onSelect, review, active, hideWorkMap = false, hideAcceptanceContract = false, hideTitle = false }: DeliveryReviewProps & {
review: DeliveryReviewRead; hideWorkMap?: boolean; hideAcceptanceContract?: boolean; hideTitle?: boolean;
}) {
const { locale } = useWorkspaceI18n();
const copy = deliveryReviewCopy[locale];
Expand Down Expand Up @@ -123,7 +123,7 @@ export function DeliveryReviewContent({ goal, items, userTodos, onSelect, review
}
return <section className="delivery-review" aria-label={copy.title}>
<header className="delivery-review-toolbar">
<h2>{copy.title}</h2>{snapshot ? <SourceNotice review={review} copy={copy} includeBoundary /> : null}
{hideTitle ? null : <h2>{copy.title}</h2>}{snapshot ? <SourceNotice review={review} copy={copy} includeBoundary /> : null}
<div><button type="button" disabled={state.kind === "loading"} onClick={() => review.refresh()}><RefreshCw size={15} />{copy.refresh}</button><button type="button" disabled={!usable} onClick={download}><Download size={15} />{copy.export}</button></div>
</header>
{feedback ? <p role="status">{feedback}</p> : null}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -110,14 +110,3 @@ export function GoalIdentityMark({ goal, size = "md" }: { goal: GoalActivitySubj
</span>
);
}

export function GoalActivityChip({ goal }: { goal: GoalActivitySubject }) {
const activity = useGoalActivity(goal);
return (
<span className={`personal-goal-activity-chip is-${activity.tone}${activity.live ? " is-live" : ""}`} title={activity.text}>
<span aria-hidden="true" className="personal-goal-activity-dot" />
{activity.windowBadge ? <span className={`personal-goal-window-badge is-${activity.windowBadge.state}`}>{activity.windowBadge.text}</span> : null}
<span>{activity.text}</span>
</span>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
const LABEL_SEPARATORS = /[::,,((… ]/;
const MAX_LABEL = 24;

/**
* One-line label for dense rows. Goal titles are whole objectives, so a row
* takes the clause before the first separator when that clause is short
* enough to read, and otherwise keeps the title for CSS to ellipsize. The full
* title stays in the tooltip and in the Goal's own header.
*/
export function goalShortLabel(goal: { title: string }): string {
const raw = goal.title.trim();
if (raw.length <= MAX_LABEL) return raw;
const head = raw.split(LABEL_SEPARATORS)[0]?.trim() ?? "";
if (head.length >= 4 && head.length <= MAX_LABEL) return head;
return raw;
}

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading