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
@@ -1,7 +1,7 @@
import type { WorkspaceGoal } from "./personal-workspace-model";
import { useWorkspaceI18n } from "./i18n";

export function GoalAcceptanceObservationCard({ goal }: { goal: WorkspaceGoal }) {
export function GoalAcceptanceObservationCard({ goal, overview = false }: { goal: WorkspaceGoal; overview?: boolean }) {
const { t, locale } = useWorkspaceI18n();
const checkLabels = locale === "zh-CN" ? {
checkpoint_satisfied: "检查点满足", checkpoint_fresh: "检查点有效",
Expand All @@ -22,8 +22,8 @@ export function GoalAcceptanceObservationCard({ goal }: { goal: WorkspaceGoal })
const label = (key: string) => labels[key as keyof typeof labels] ?? t("acceptance.unknown");
const projection = goal.acceptanceObservation;
const unavailable = goal.loadState || !projection || projection.goal_id !== goal.goalId || projection.coverage === "unavailable";
return <section className="personal-detail-card personal-goal-acceptance" aria-label={t("acceptance.title")}>
<div className="personal-detail-card-title"><h3>{t("acceptance.title")}</h3><em>{t("common.readOnly")}</em></div>
const content = <section className="personal-detail-card personal-goal-acceptance" aria-label={t("acceptance.title")}>
{!overview ? <div className="personal-detail-card-title"><h3>{t("acceptance.title")}</h3><em>{t("common.readOnly")}</em></div> : null}
<p role="status">{t(unavailable ? "acceptance.unavailable" : "acceptance.partial")}</p>
{!unavailable && projection ? <>
<h4>{t("acceptance.gaps")}</h4>
Expand Down Expand Up @@ -55,4 +55,11 @@ export function GoalAcceptanceObservationCard({ goal }: { goal: WorkspaceGoal })
{projection.truncated ? <p>{t("acceptance.truncated")}</p> : null}
</> : null}
</section>;
if (!overview) return content;
const zh = locale === "zh-CN";
return <details className="goal-overview-evidence goal-observation-details" key={goal.goalId}>
<summary><span>{t("acceptance.title")}</span><small>{unavailable ? t("acceptance.unavailable")
: `${projection!.acceptance_gaps.length} ${zh ? "项缺口" : "gaps"} · ${projection!.guards.length} ${zh ? "项限制" : "guards"} · ${zh ? "仅部分观测" : "Partial observations"}`}</small></summary>
{content}
</details>;
}
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ export function GoalOverview({ active, goal, items, userTodos, readOnly, onOpenD
<GoalWorkspaceWork onOpenAgentConversation={onOpenAgentConversation} goal={goal} digest={digest} items={items} userTodos={userTodos} selectedTodoId={selectedTodoId} onSelect={onSelect}
readOnly={readOnly} active={active} railOpen={railOpen} onCloseRail={onCloseRail} historyRefreshRevision={historyRefreshRevision} proposals={proposals} onPreview={onPreview} board={board}
map={readOnly ? <p className="goal-work-scope">{copy.remote}</p> : <DeliveryWorkMap goal={goal} userTodos={userTodos} review={review} onSelect={onSelect} selectedTodoId={selectedTodoId}/>}/>
{readOnly ? <><p className="goal-overview-source-note">{copy.remote}</p><GoalAcceptanceObservationCard goal={goal} /></>
{readOnly ? <GoalAcceptanceObservationCard goal={goal} overview />
: <OverviewDeliveryEvidence key={`delivery:${goal.goalId}`} review={review} zh={locale === "zh-CN"}>
<DeliveryReviewContent review={review} hideWorkMap hideAcceptanceContract active={active} goal={goal} items={items} userTodos={userTodos} onSelect={onSelect} />
</OverviewDeliveryEvidence>}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
.goal-workspace-composition { display: grid; grid-template-columns: minmax(0, 1fr) 280px; gap: 32px; align-items: start; }
.goal-workspace-composition { display: grid; grid-template-columns: minmax(0, 1fr); }
.goal-workspace-document { display: grid; gap: 32px; min-width: 0; }
.goal-now, .goal-workspace-results { min-width: 0; display: grid; gap: 16px; }
.goal-now h2, .goal-workspace-results > h2 { font-size: var(--text-heading); line-height: var(--leading-heading); font-weight: 600; }
Expand All @@ -11,10 +11,12 @@
.goal-owner-group > header { display: flex; flex-wrap: wrap; gap: 12px; align-items: baseline; padding: 8px 0; border-bottom: 1px solid var(--pw-line-strong); }
.goal-owner-group h3 { font-size: var(--text-body); line-height: var(--leading-body); font-weight: 600; }
.goal-owner-group > header > span { color: var(--pw-muted); font-size: var(--text-caption); }
.goal-work-more > summary { width: fit-content; padding: 12px 0; color: var(--pw-muted); font-size: var(--text-body); cursor: pointer; }
.goal-overview .goal-work-row { width: 100%; display: grid; grid-template-columns: 16px minmax(0, 1fr) auto 36px; gap: 10px; border: 0; border-bottom: 1px solid var(--pw-line); border-radius: 0; background: transparent; padding: 12px 0; }
.goal-overview .goal-work-row[aria-pressed="true"] { background: var(--pw-hover); box-shadow: inset 2px 0 var(--pw-blue); }
.goal-work-row-text { display: grid; gap: 4px; min-width: 0; }
.goal-work-row strong { font-weight: 500; font-size: var(--text-body); line-height: var(--leading-body); overflow-wrap: anywhere; }
.goal-owner-group > .goal-work-row strong { display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; }
.goal-work-row small { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 100%; color: var(--pw-muted); font-size: var(--text-caption); }
.goal-work-row time { color: var(--pw-faint); font-size: var(--text-caption); text-align: right; }
.goal-work-badge { font-size: var(--text-caption); color: var(--pw-muted); white-space: nowrap; }
Expand Down Expand Up @@ -62,24 +64,21 @@
.goal-all-work { border-left: 0; border-top: 1px solid var(--pw-line); padding: 20px 0 0; }
.goal-overview-summary.has-attention { grid-template-columns: minmax(0, 1fr); }
}
.goal-overview .goal-direction-toggle, .personal-channel-header .personal-channel-actions > .goal-all-work-trigger { display: none; }
.goal-overview .goal-direction-toggle { display: none; }
.personal-channel-header .personal-channel-actions > .goal-all-work-trigger { display: inline-flex; }
.goal-direction-content { display: grid; gap: 12px; }
.goal-direction-content[hidden] { display: none; }
.goal-work-rail-dialog { margin: 12px 12px 12px auto; width: min(400px, calc(100vw - 24px)); max-height: calc(100dvh - 24px); border: 1px solid var(--pw-line); border-radius: 12px; background: var(--pw-bg); color: var(--pw-text); padding: 20px; }
.goal-work-rail-dialog { margin: 12px 12px 12px auto; width: min(480px, calc(100vw - 24px)); max-height: calc(100dvh - 24px); border: 1px solid var(--pw-line); border-radius: 12px; background: var(--pw-bg); color: var(--pw-text); padding: 20px; }
.goal-work-rail-dialog::backdrop { background: rgb(0 0 0 / 30%); }
.goal-work-rail-dialog .goal-all-work { border: 0; padding: 0; }
.goal-work-rail-close { margin-bottom: 16px; }
@media (max-width: 1199px) {
.personal-channel-header .personal-channel-actions > .goal-all-work-trigger { display: inline-flex; }
}
@media (max-width: 640px) {
.goal-overview .goal-direction-toggle { display: inline-flex; justify-self: start; min-height: 32px; border: 0; padding: 4px 0; background: transparent; color: var(--pw-muted); font-size: var(--text-caption); }
}

.goal-owner-group > header .goal-owner-conversation { margin-left: auto; padding: 6px 8px; min-height: 44px; border: 0; background: transparent; color: var(--pw-text); font: inherit; font-size: var(--text-caption); text-decoration: underline; text-underline-offset: 3px; cursor: pointer; }

/* Known empty regions yield space; loading, partial coverage and errors still mount. */
.goal-workspace-composition.is-quiet { grid-template-columns: minmax(0, 1fr); }
.goal-workspace-results:has(> h2:only-child) { display: none; }
.goal-workspace-results .personal-files-list.is-empty > header { display: none; }
.goal-workspace-results .personal-files-list.is-empty:has(> header:only-child) { display: none; }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,21 +72,14 @@ export function GoalWorkspaceWork({goal, digest, items, userTodos, selectedTodoI
}) {
const {locale, t} = useWorkspaceI18n();
const zh = locale === "zh-CN";
const [compactRail, setCompactRail] = useState(() => typeof window !== "undefined" && typeof window.matchMedia === "function" && window.matchMedia("(max-width: 1199px)").matches);
const dialog = useRef<HTMLDialogElement>(null);
useEffect(() => {
const media = window.matchMedia("(max-width: 1199px)");
const update = () => setCompactRail(media.matches);
update(); media.addEventListener("change", update);
return () => media.removeEventListener("change", update);
}, []);
useEffect(() => {
const node = dialog.current;
if (compactRail && railOpen && active) node?.showModal(); else node?.close();
if (railOpen && active) node?.showModal(); else node?.close();
return () => node?.close();
}, [compactRail, railOpen, active]);
}, [railOpen, active]);
const select = (selection: WorkspaceDrawerSelection) => {
if (compactRail && railOpen) {dialog.current?.close(); onCloseRail();}
if (railOpen) {dialog.current?.close(); onCloseRail();}
onSelect(selection);
};
const [lens, setLens] = useState<"owner" | "board" | "map">("owner");
Expand Down Expand Up @@ -141,13 +134,13 @@ export function GoalWorkspaceWork({goal, digest, items, userTodos, selectedTodoI
<h3>{sectionLabels[section]}</h3>{digest.work.rail[section].filter(matches).map(id => task(id, true))}
</section>)}
{filter === "all" || filter === "done" ? <CompletedTaskLane key={`${goal.goalId}:${readOnly}`} goal={goal} agentId="all" seed={digest.work.rail.done.flatMap(id => todos.get(id) ?? [])}
enabled={!readOnly && active && (!compactRail || railOpen)} refreshRevision={historyRefreshRevision} listView onSelect={select} selectedTodoId={selectedTodoId} query={query}/> : null}
enabled={!readOnly && active && railOpen} refreshRevision={historyRefreshRevision} listView onSelect={select} selectedTodoId={selectedTodoId} query={query}/> : null}
</> : <div className="goal-material-activity">{activity.map(item => <button type="button" key={item.id} onClick={() => item.kind === "run" ? select({kind: "run", item: item.run}) : item.kind === "output" ? select({kind: "output", item: item.output}) : item.kind === "proposal" ? select({kind: "proposal", item: item.proposal}) : undefined}>
{item.kind === "run" ? <>{item.run.status === "completed" ? <CircleCheck size={14} aria-hidden/> : <CircleAlert size={14} aria-hidden/>}<span>{item.run.title}<small>{localizedSessionStatus(item.run.status, t)}</small></span></> : item.kind === "output" ? <><CircleCheck size={14} aria-hidden/><span>{item.output.title}<small>{zh ? "成果返回" : "Result returned"}</small></span></> : item.kind === "proposal" ? <><Hand size={14} aria-hidden/><span>{item.proposal.title}<small>{zh ? "决定已记录" : "Decision recorded"}</small></span></> : null}
</button>)}{!activity.length ? <p>{zh ? "当前时间线尚无已完成或返回的记录。" : "No completed work or returned results in this timeline."}</p> : null}</div>}
<footer className="goal-work-scope">{zh ? "统计仅覆盖此 Goal;任务与历史可能分批载入。" : "Counts cover this Goal; tasks and history may be loaded in bounded pages."}<time dateTime={digest.observedAt}>{new Date(digest.observedAt).toLocaleTimeString(locale)}</time></footer>
</aside>;
return <><div className={`goal-workspace-composition${quietWork && !railOpen ? " is-quiet" : ""}`}>
return <><div className="goal-workspace-composition">
<div className="goal-workspace-document">
<section className="goal-now" aria-label={zh ? "当前工作" : "Now"}>
<header className="goal-now-header"><h2>{zh ? "当前工作" : "Now"}</h2>
Expand All @@ -163,18 +156,28 @@ export function GoalWorkspaceWork({goal, digest, items, userTodos, selectedTodoI
<header><h3>{ownerName(group.ownerId)}</h3><span>{group.todoIds.length} {zh ? "项任务" : "tasks"} · {group.todoIds.filter(id => rows.get(id)?.displayClass === group.displayClass).length} {goalWorkClassLabel(group.displayClass, zh)}</span>
{!readOnly && group.ownerId && onOpenAgentConversation ? <button type="button" className="goal-owner-conversation" data-agent-id={group.ownerId} aria-label={zh ? `与 ${ownerName(group.ownerId)} 对话` : `Talk to ${ownerName(group.ownerId)}`} onClick={() => onOpenAgentConversation(group.ownerId!)}>{zh ? "工作记录" : "Work conversation"}</button> : null}
</header>
{group.todoIds.map(id => task(id))}
{group.todoIds.slice(0, 3).map(id => task(id))}
{group.todoIds.length > 3 ? <RemainingWork key={group.ownerId ?? ""} selectedTodoId={group.todoIds.slice(3).includes(selectedTodoId ?? "") ? selectedTodoId : null}
label={zh ? `其余 ${group.todoIds.length - 3} 项任务` : `${group.todoIds.length - 3} more tasks`}>
{group.todoIds.slice(3).map(id => task(id))}
</RemainingWork> : null}
</section>)}
{!digest.work.groups.length ? <p className="goal-work-scope">{digest.work.coverage === "complete" ? (zh ? "当前没有可列出的进行中任务。延后与完成记录在全部工作中。" : "No current work to list. Deferred and completed work remain in All work.") : (zh ? "当前快照没有可列出的任务。" : "No work is listed in this snapshot.")}</p> : null}
</div>
{visited.has("board") ? <div hidden={lens !== "board"}>{board}</div> : null}
{visited.has("map") ? <div hidden={lens !== "map"}>{map}</div> : null}
</section>
</div>
{!quietWork || railOpen ? compactRail ? <dialog className="goal-work-rail-dialog" aria-label={zh ? "全部工作" : "All work"} ref={dialog} onCancel={event => {event.preventDefault(); onCloseRail();}}>
<dialog className="goal-work-rail-dialog" aria-label={zh ? "全部工作" : "All work"} ref={dialog} onCancel={event => {event.preventDefault(); onCloseRail();}}>

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Escape should dismiss the topmost All work modal. On desktop, open a task drawer, then All work, then press Escape once: the underlying task drawer disappears while this dialog remains open and modal. A second Escape is required. The existing ContextDrawer document keydown handler calls preventDefault before the native dialog cancel, so this all-width modal path needs topmost keyboard arbitration. Preserve the underlying selection and add a regression asserting one Escape closes this modal, retains the original task drawer and restores trigger focus. Reproduced twice with the exact built head and an isolated canonical Todo backend; the current main retains both handlers unchanged.

<button type="button" className="goal-work-rail-close" onClick={onCloseRail}>{zh ? "关闭全部工作" : "Close all work"}</button>
{rail}
</dialog> : rail : null}
</dialog>
</div>
</>;
}

function RemainingWork({selectedTodoId, label, children}: {selectedTodoId: string | null; label: string; children: ReactNode}) {
const disclosure = useRef<HTMLDetailsElement>(null);
useEffect(() => {if (selectedTodoId && disclosure.current) disclosure.current.open = true;}, [selectedTodoId]);
return <details className="goal-work-more" ref={disclosure}><summary>{label}</summary>{children}</details>;
}
12 changes: 12 additions & 0 deletions docs/development/design.md
Original file line number Diff line number Diff line change
Expand Up @@ -595,6 +595,18 @@ From top to bottom, one reading column with an optional right rail:
4. **Owner rail.** What needs the owner sits beside the hero as a hairline
list: glyph, the decision in owner language, relative age, an arrow that
opens it in place. A quiet Goal shows no empty rail.
Do not repeat the same work in a permanently visible task rail. Overview
shows up to three compact tasks per responsible owner; the group summary
retains its task count and significant state, including blocked work.
Expand the remaining tasks in place or open All work for search, filters,
completed history and activity. Direct task links reveal their selected row.
Read-only acceptance observations begin as one disclosure with gap/guard
counts and an explicit partial or unknown state; full reasons, requirements
and evidence remain inside. Zero listed gaps never means Goal acceptance.
不要在常驻任务侧栏重复展示主栏的工作。概览每位负责人默认最多展示三条紧凑任务,
组摘要保留任务数和受阻等重要状态;其余任务就地展开,搜索、筛选、已完成历史与活动
从“全部工作”打开。任务直链自动展开选中行。只读验收观察默认折叠,摘要显示缺口、
限制数量及“部分观测”或“未知”;完整原因、要求与证据保留在正文,零缺口不等于验收通过。
5. **Folded composer.** Reading views fold the composer to one line that
expands on request; conversation views keep it open.

Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Loading