Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
14 commits
Select commit Hold shift + click to select a range
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 .github/PULL_REQUEST_TEMPLATE.md
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,7 @@ even when the underlying access was authorized.
- States and viewports shown:
- Source data: <!-- choose one: none | synthetic | public_fixture -->
- Attention review: <!-- Per docs/development/design.md: what earns its place through high-value information, essential interaction, or expressive visual presentation? Name consolidation/removal across the whole viewport, and how failures, uncertainty, and one-step controls remain available. -->
- Craft review: <!-- Per docs/development/design.md#craft-review, judged on showcase-workspace screenshots: one hero, no grouping-only cards, designed loading/unknown/failure states, motion by token, status glyph family, no new raw hex. -->

## Type of Change

Expand Down
4 changes: 2 additions & 2 deletions apps/presentation/dashboard/src/data/status.ts
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@ export const todoItemSchema = z.object({
export const todoGroupSchema = z.object({
source_section: z.string().optional().nullable(),
total_count: z.number().optional().default(0),
open_count: z.number().optional().default(0),
open_count: z.number().optional(),
done_count: z.number().optional().default(0),
advancement_done_count: z.number().optional(),
items: z.array(todoItemSchema).optional().default([]),
Expand Down Expand Up @@ -257,7 +257,7 @@ export const goalChannelNotificationProjectionSchema = z.object({

export const projectAssetTodoSummarySchema = z.object({
source_section: z.string().optional().nullable(),
open: z.number().optional().default(0),
open: z.number().optional(),
done: z.number().optional().default(0),
total: z.number().optional().default(0),
advancement_done_count: z.number().optional(),
Expand Down
Original file line number Diff line number Diff line change
@@ -1,28 +1,32 @@
import { Bot, Eye, FolderOpen, Menu, RefreshCw, SlidersHorizontal } from "lucide-react";
import { GoalActivityChip } from "./goal-activity-view";
import { GoalLifecycleControl, GoalMastheadIdentity } from "./goal-masthead";

import { localizedGoalState, useWorkspaceI18n } from "./i18n";
import { useWorkspaceI18n } from "./i18n";
import type { ManagerChannelBinding, ManagerRuntimeSessionReadback } from "../../data/chat";
import type { WorkspaceAgentOption, WorkspaceGoal, WorkspaceGoalTab } from "./personal-workspace-model";
import { WorkspaceSelect } from "./workspace-select";

export function ChannelHeader({
agents,
conversationScope,
goalLifecycleBusy,
managerChannelBinding,
managerChatOpen,
managerRuntime,
mobileNavigationOpen,
onOpenGoalCapabilities,
onOpenManagerChat,
onOpenManagerSettings,
onOpenWorkspace,
onRefresh,
onRequestGoalLifecycle,
onOpenNavigation,
onSelectGoalTab,
onSelectAgent,
onReturnManagerHome,
refreshState,
readOnlySourceLabel,
resumeReviewed,
selectedAgentId,
selectedGoal,
selectedGoalTab,
Expand All @@ -31,6 +35,7 @@ export function ChannelHeader({
agents: WorkspaceAgentOption[];
/** Steward conversation scope; absent when the host grants no workspace. */
conversationScope?: { options: { disabled?: boolean; label: string; value: string }[]; value: string; onChange: (value: string) => void } | null;
goalLifecycleBusy?: boolean;
/** Set while the steward conversation is scoped to a workspace. */
workspaceGrantLabel?: string | null;
managerChannelBinding?: ManagerChannelBinding | null;
Expand All @@ -40,13 +45,18 @@ export function ChannelHeader({
onOpenGoalCapabilities?: () => void;
onOpenManagerChat?: () => void;
onOpenManagerSettings?: () => void;
onOpenWorkspace?: () => void;
onRefresh?: () => void;
/** Absent when the selected source does not authorize Goal lifecycle changes. */
onRequestGoalLifecycle?: (operation: "stop" | "resume") => void;
onOpenNavigation?: () => void;
onSelectGoalTab: (tab: WorkspaceGoalTab) => void;
onSelectAgent: (agentId: string) => void;
onReturnManagerHome?: () => void;
refreshState?: "idle" | "loading" | "done" | "error";
readOnlySourceLabel?: string;
/** Resume opens the owner's reviewed preview instead of a local confirmation. */
resumeReviewed?: boolean;
selectedAgentId: string;
selectedGoal: WorkspaceGoal | null;
selectedGoalTab: WorkspaceGoalTab;
Expand Down Expand Up @@ -169,8 +179,7 @@ export function ChannelHeader({
<header className="personal-channel-header" data-goal-selected={Boolean(selectedGoal)} data-workspace-scope={workspaceGrantLabel ? "true" : undefined}>
<button aria-expanded={mobileNavigationOpen ?? false} aria-label={t("header.openGoalNavigation")} className="personal-icon-button personal-mobile-menu" onClick={onOpenNavigation} type="button"><Menu size={18} /></button>
<div className="personal-channel-title">
<h1>{selectedGoal?.title ?? t("header.manager")}</h1>
{selectedGoal && !selectedGoal.loadState ? <p className="personal-channel-activity"><GoalActivityChip goal={selectedGoal} /></p> : null}
{selectedGoal ? <GoalMastheadIdentity goal={selectedGoal} onOpenWorkspace={onOpenWorkspace ?? (() => undefined)} /> : <h1>{t("header.manager")}</h1>}
{workspaceGrantLabel ? <p className="personal-channel-activity">{t("workspace.conversation", { grant: workspaceGrantLabel })}</p> : null}
<div className="personal-manager-context">
{stewardBindingVisible && managerChannelBinding ? (
Expand Down Expand Up @@ -232,6 +241,9 @@ export function ChannelHeader({
</nav>
)}
<div className="personal-channel-actions">
{selectedGoal && !selectedGoal.loadState && onRequestGoalLifecycle ? (
<GoalLifecycleControl busy={goalLifecycleBusy ?? false} goal={selectedGoal} onRequest={onRequestGoalLifecycle} resumeReviewed={resumeReviewed ?? true} />
) : null}
{selectedGoal && onOpenGoalCapabilities ? (
<button aria-label={t("header.goalSettings")} title={t("header.goalSettingsDescription")} className="personal-icon-button personal-goal-settings-action" onClick={onOpenGoalCapabilities} type="button"><SlidersHorizontal aria-hidden size={17} /></button>
) : null}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,21 @@
.delivery-review-toolbar > div, .delivery-chain-toolbar > div, .delivery-source-actions { display: flex; gap: 8px; flex-wrap: wrap; }
.delivery-review-toolbar button, .delivery-source-actions button { display: inline-flex; gap: 8px; align-items: center; }
.delivery-snapshot-time, .delivery-boundary, .delivery-chain-toolbar > span { color: var(--pw-muted); font-size: var(--text-caption); line-height: var(--leading-caption); }
.delivery-notice { border: 1px solid var(--pw-line); border-left: 3px solid var(--pw-amber, #a96500); border-radius: 6px; padding: 12px 16px; font-size: var(--text-body); line-height: var(--leading-body); }
.delivery-notice { border-radius: 6px; padding: 10px 14px; background: var(--pw-hover, var(--pw-card)); color: var(--pw-muted); font-size: var(--text-body); line-height: var(--leading-body); }
/* A region failure stays visible and announced, sized to one line rather than a banner. */
.delivery-notice.is-error { display: flex; align-items: center; gap: 8px; padding: 0; background: none; }
.delivery-notice.is-error svg { flex: none; color: var(--pw-amber, #a96500); }
.delivery-loading { display: grid; gap: 10px; padding: 4px 0; }
.delivery-loading i { display: block; height: 12px; border-radius: 4px; background: linear-gradient(90deg, var(--pw-line) 0%, var(--pw-hover, var(--pw-card)) 50%, var(--pw-line) 100%) 0 0 / 200% 100%; animation: delivery-skeleton 1.4s ease-in-out infinite; }
.delivery-loading i:nth-child(1) { width: 42%; }
.delivery-loading i:nth-child(2) { width: 68%; }
.delivery-loading i:nth-child(3) { width: 54%; }
.goal-overview .delivery-review { padding-top: 24px; border-top: 1px solid var(--pw-line); }
.goal-overview .delivery-review-toolbar h2 { font-size: var(--text-lead); line-height: var(--leading-lead); }
.goal-overview .delivery-review-toolbar button { border-color: transparent; background: transparent; color: var(--pw-muted); transition: background-color var(--motion-fast) var(--ease-standard), color var(--motion-fast) var(--ease-standard); }
.goal-overview .delivery-review-toolbar button:hover:not(:disabled) { background: var(--pw-hover, var(--pw-card)); color: var(--pw-text); }
@keyframes delivery-skeleton { to { background-position: -200% 0; } }
@media (prefers-reduced-motion: reduce) { .delivery-loading i { animation: none; } }
.delivery-notice dl { display: flex; gap: 8px 24px; flex-wrap: wrap; margin: 12px 0 0; }
.delivery-notice dl > div { display: flex; gap: 8px; }
.delivery-notice dd { margin: 0; font-weight: 600; }
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { useEffect, useId, useMemo, useRef, useState } from "react";
import { ArrowRight, Download, ExternalLink, RefreshCw, Search } from "lucide-react";
import { ArrowRight, CircleAlert, Download, ExternalLink, RefreshCw, Search } from "lucide-react";
import { deliveryReviewMarkdown, fetchDeliveryReview, filterReviewNodes, reviewCoverageIncomplete, reviewNodeColumn, type DeliveryReviewSnapshot, type ReviewFocus, type ReviewGraph, type ReviewNode } from "../../data/delivery-review";
import type { WorkspaceDrawerSelection, WorkspaceGoal, WorkspaceModel, WorkspaceTimelineItem } from "./personal-workspace-model";
import { GoalAcceptanceObservationCard } from "./goal-acceptance-observation-card";
Expand Down Expand Up @@ -149,11 +149,12 @@ export function DeliveryReview({ goal, items, userTodos, onSelect, active }: Del
<div><button type="button" disabled={state.kind === "loading"} onClick={() => setRefresh(value => value + 1)}><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}
{!snapshot ? <p role={state.kind === "error" ? "alert" : "status"} className="delivery-notice">{state.kind === "error" ? copy.error : copy.loading}</p> : <>
{state.kind === "error" ? <p role="alert" className="delivery-notice">{copy.refreshError}</p> : null}
{!snapshot ? state.kind === "error" ? <p role="alert" className="delivery-notice is-error"><CircleAlert aria-hidden size={15} />{copy.error}</p>
: <div role="status" className="delivery-loading" aria-label={copy.loading}><i /><i /><i /></div> : <>
{state.kind === "error" ? <p role="alert" className="delivery-notice is-error"><CircleAlert aria-hidden size={15} />{copy.refreshError}</p> : null}
<p className="delivery-snapshot-time">{copy.observed} · <time dateTime={snapshot.observed_at}>{new Date(snapshot.observed_at).toLocaleString(locale)}</time>
{state.kind === "loading" || (stale && state.kind !== "error") ? <span role="status"> · {copy.updating}</span> : null}</p>
{stale && state.kind === "error" ? <p role="alert" className="delivery-notice">{copy.changed}</p> : null}
{stale && state.kind === "error" ? <p role="alert" className="delivery-notice is-error"><CircleAlert aria-hidden size={15} />{copy.changed}</p> : null}
{snapshot.goal_map ? <GoalWorkMapView map={snapshot.goal_map} copy={copy.workMap} selectedId={mapSelectedId} onSelect={setMapSelectedId}
canOpen={node => usable && mapSource(node) !== null} onOpen={node => { const source = mapSource(node); if (source) onSelect(source); }} />
: snapshot.goal_map === null ? <p className="delivery-notice">{copy.workMap.unavailable}</p> : null}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
.goal-masthead-breadcrumb { display: flex; align-items: center; gap: 4px; min-width: 0; margin: 0 0 2px; color: var(--pw-muted); font-size: var(--text-caption); line-height: var(--leading-caption); }
.goal-masthead-breadcrumb svg { flex: none; color: var(--pw-faint); }
.goal-masthead-breadcrumb button { padding: 0; border: 0; background: none; color: inherit; font: inherit; cursor: pointer; transition: color var(--motion-fast) var(--ease-standard); }
.goal-masthead-breadcrumb button:hover { color: var(--pw-text); }
.goal-masthead-breadcrumb [aria-current] { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

.goal-lifecycle-control { position: relative; flex: none; }
.goal-lifecycle-trigger { display: inline-flex; align-items: center; gap: 6px; min-height: 36px; padding: 0 12px; border: 1px solid var(--pw-line); border-radius: 6px; background: var(--pw-card); color: var(--pw-text); cursor: pointer; font-family: inherit; font-size: var(--text-body); font-weight: 500; line-height: var(--leading-body); white-space: nowrap; transition: background-color var(--motion-fast) var(--ease-standard), border-color var(--motion-fast) var(--ease-standard); }
.goal-lifecycle-trigger:hover:not(:disabled) { border-color: var(--pw-line-strong); background: var(--pw-hover); }
.goal-lifecycle-trigger:disabled { color: var(--pw-muted); cursor: progress; }
.goal-lifecycle-trigger:focus-visible, .goal-lifecycle-confirm button:focus-visible, .goal-vital:focus-visible, .goal-masthead-breadcrumb button:focus-visible { outline: 2px solid var(--pw-blue); outline-offset: 2px; }
.goal-lifecycle-confirm { position: absolute; top: calc(100% + 8px); right: 0; z-index: 30; width: min(340px, calc(100vw - 32px)); padding: 16px; border: 1px solid var(--pw-line); border-radius: 12px; background: var(--pw-card); box-shadow: 0 12px 32px rgb(0 0 0 / 8%), 0 1px 2px rgb(0 0 0 / 6%); transform-origin: top right; animation: goal-popover-enter var(--motion-base) var(--ease-out); }
.goal-lifecycle-confirm strong { display: block; color: var(--pw-text); font-size: var(--text-body); font-weight: 600; line-height: var(--leading-body); }
.goal-lifecycle-confirm p { margin: 6px 0 14px; color: var(--pw-muted); font-size: var(--text-caption); line-height: var(--leading-caption); white-space: normal; }
.goal-lifecycle-confirm div { display: flex; justify-content: flex-end; gap: 8px; }
.goal-lifecycle-confirm button { min-height: 34px; padding: 0 12px; border: 1px solid var(--pw-line); border-radius: 6px; background: var(--pw-card); color: var(--pw-text); cursor: pointer; font-family: inherit; font-size: var(--text-caption); font-weight: 500; }
.goal-lifecycle-confirm button.is-primary { border-color: var(--pw-text); background: var(--pw-text); color: var(--pw-card); }

/* A stat row, not a table: values carry the weight, labels recede beneath them. */
.goal-vitals { display: grid; grid-template-columns: repeat(6, minmax(0, 1fr)) auto; column-gap: 8px; border-bottom: 1px solid var(--pw-line); padding: 4px 16px 8px; }
.goal-vital { position: relative; display: flex; flex-direction: column; align-items: flex-start; justify-content: center; gap: 2px; min-width: 0; min-height: 60px; padding: 8px 12px; border: 0; border-radius: 8px; background: transparent; color: var(--pw-text); cursor: pointer; font-family: inherit; text-align: left; transition: background-color var(--motion-fast) var(--ease-standard); }
.goal-vital:hover { background: var(--pw-hover); }
.goal-vital-value { display: inline-flex; align-items: center; gap: 6px; max-width: 100%; overflow: hidden; font-size: var(--text-heading); font-variant-numeric: tabular-nums; font-weight: 600; letter-spacing: -0.02em; line-height: var(--leading-heading); text-overflow: ellipsis; white-space: nowrap; }
.goal-vital-value small { color: var(--pw-faint); font-size: var(--text-body); font-weight: 500; letter-spacing: 0; }
.goal-vital-meta { display: flex; align-items: baseline; gap: 0; width: 100%; min-width: 0; overflow: hidden; color: var(--pw-muted); font-size: var(--text-caption); line-height: var(--leading-caption); white-space: nowrap; }
.goal-vital-label { flex: none; font-weight: 500; }
.goal-vital-sub { min-width: 0; overflow: hidden; color: var(--pw-faint); text-overflow: ellipsis; }
.goal-vital-sub::before { content: "·"; padding: 0 5px; }
.goal-vital.is-danger .goal-vital-value { color: var(--pw-red); }
.goal-vital.is-recede .goal-vital-value { color: var(--pw-faint); font-weight: 500; }
.goal-vital-partial { color: var(--pw-amber); }
.goal-vital-swap { display: inline-block; min-width: 0; overflow: hidden; text-overflow: ellipsis; animation: goal-value-swap var(--motion-base) var(--ease-out); }
.goal-vitals-scope { align-self: center; padding-left: 12px; color: var(--pw-faint); font-size: var(--text-caption); line-height: var(--leading-caption); white-space: nowrap; }

/* One glyph per state: filled dot (settled), hollow ring (waiting or stopped), spinning arc (observed live). */
.goal-vital-dot { --goal-dot: var(--pw-track); position: relative; flex: none; width: 8px; height: 8px; box-sizing: border-box; border-radius: 50%; background: var(--goal-dot); }
.goal-vital.is-running .goal-vital-dot { --goal-dot: var(--pw-blue); }
.goal-vital.is-attention .goal-vital-dot { --goal-dot: var(--pw-caution); }
.goal-vital.is-danger .goal-vital-dot { --goal-dot: var(--pw-red); }
.goal-vital.is-waiting .goal-vital-dot, .goal-vital.is-queued .goal-vital-dot { border: 1.5px solid var(--pw-muted); background: transparent; }
.goal-vital.is-stopped .goal-vital-dot { border: 1.5px solid var(--pw-faint); background: transparent; }
.goal-vital-dot.is-live { width: 14px; height: 14px; border: 1.5px solid color-mix(in srgb, var(--goal-dot) 28%, transparent); background: transparent; }
.goal-vital-dot.is-live::before { position: absolute; inset: 2.5px; border-radius: 50%; background: var(--goal-dot); content: ""; }
.goal-vital-dot.is-live::after { position: absolute; inset: -1.5px; border: 1.5px solid transparent; border-top-color: var(--goal-dot); border-radius: 50%; content: ""; animation: pw-spin 1s linear infinite; }

.goal-work-ribbon { display: flex; gap: 2px; width: 100%; height: 3px; margin-top: 4px; }
.goal-work-ribbon i { display: block; min-width: 3px; border-radius: 2px; background: var(--pw-track); transition: flex-grow var(--motion-section) var(--ease-out); }
.goal-work-ribbon .is-done { background: var(--pw-text); }
.goal-work-ribbon .is-running_observed { background: var(--pw-blue); }
.goal-work-ribbon .is-waiting_dependency { background: repeating-linear-gradient(90deg, var(--pw-faint) 0 3px, transparent 3px 5px); }
.goal-work-ribbon .is-blocked { background: var(--pw-amber); }
.goal-work-ribbon .is-deferred { background: var(--pw-line); }

.goal-vital-skeleton { cursor: default; gap: 8px; }
.goal-vital-skeleton i { display: block; height: 12px; border-radius: 4px; background: linear-gradient(90deg, var(--pw-line) 0%, var(--pw-hover) 50%, var(--pw-line) 100%) 0 0 / 200% 100%; animation: goal-skeleton 1.4s ease-in-out infinite; }
.goal-vital-skeleton i:first-child { width: 64%; height: 18px; }
.goal-vital-skeleton i:last-child { width: 40%; }

@keyframes goal-popover-enter { from { opacity: 0; transform: translateY(-4px) scale(.98); } }
@keyframes goal-value-swap { from { opacity: 0; transform: translateY(3px); } }
@keyframes goal-skeleton { to { background-position: -200% 0; } }

@media (prefers-reduced-motion: reduce) {
.goal-lifecycle-confirm, .goal-vital-swap, .goal-vital-skeleton i, .goal-vital-dot.is-live::after { animation: none; }
}

@media (max-width: 1199px) {
.goal-vitals { grid-template-columns: repeat(6, minmax(0, 1fr)); }
.goal-vitals-scope { display: none; }
}

@media (max-width: 900px) {
.goal-vitals { grid-template-columns: repeat(3, minmax(0, 1fr)); padding: 4px 8px 8px; }
.goal-vital { min-height: 56px; padding: 6px 10px; }
}

@media (max-width: 640px) {
.goal-masthead-breadcrumb { display: none; }
.goal-lifecycle-trigger { width: 36px; padding: 0; justify-content: center; }
.goal-lifecycle-trigger span { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; }
.goal-vitals { column-gap: 0; padding: 2px 4px 6px; }
.goal-vital { min-height: 48px; padding: 6px 8px; }
.goal-vital-value { font-size: var(--text-lead); line-height: var(--leading-lead); }
.personal-channel:has(.personal-channel-scroll:not([data-active-goal-view="overview"])) .goal-vital-sub { display: none; }
}
Loading
Loading