From 72a8cfb0fe5e42af907b0059f66c6b120230e5ab Mon Sep 17 00:00:00 2001 From: song Date: Wed, 28 Oct 2026 09:48:42 +0800 Subject: [PATCH 1/7] feat(workspace): integrate attention, task and reading continuity Keep native request identity and source scope through home actions, shared task browsing, exact report returns and lazy result readers. Align Unicode admission with the existing owner and preserve drafts during settings readback recovery. Signed-off-by: song --- .../personal-workspace/answer-report-page.css | 42 +++-- .../personal-workspace/answer-report-page.tsx | 146 ++++++++++++++---- .../personal-workspace/attention-actions.tsx | 22 ++- .../personal-workspace/channel-timeline.tsx | 73 +++++++-- .../completed-task-lane.tsx | 25 ++- .../personal-workspace/context-drawer.tsx | 9 +- .../goal-capability-settings.tsx | 64 +++++--- .../personal-workspace/goal-create-request.ts | 19 ++- .../personal-workspace/goal-draft-card.tsx | 3 +- .../goal-managed-results.tsx | 91 +++++++---- .../personal-workspace/goal-overview.tsx | 2 +- .../goal-research-results.tsx | 142 +++++++++++++---- .../personal-workspace/goal-tasks-view.css | 10 ++ .../personal-workspace/goal-tasks-view.tsx | 87 ++++++++--- .../personal-workspace/goal-team-results.tsx | 68 +++++--- .../personal-workspace/goal-work-browse.tsx | 53 +++++++ .../goal-workspace-work.css | 3 + .../goal-workspace-work.tsx | 94 ++++++++--- .../src/features/personal-workspace/i18n.tsx | 36 +++-- .../machine-configuration-settings.tsx | 125 +++++++++++---- .../personal-workspace/manager-brief.tsx | 65 +++++--- .../personal-workspace/markdown-blocks.ts | 124 +++++++++++++++ .../features/personal-workspace/markdown.css | 9 ++ .../features/personal-workspace/markdown.tsx | 111 ++----------- .../personal-workspace/message-reading.css | 9 ++ .../personal-workspace/message-reading.tsx | 40 +++++ .../personal-workspace-page.tsx | 111 +++++++++---- .../personal-workspace/personal-workspace.css | 50 +++--- .../research-record-comparison.tsx | 37 ++++- .../personal-workspace/result-catalog.css | 12 ++ .../personal-workspace/result-catalog.tsx | 54 +++++++ .../personal-workspace/task-request.tsx | 19 ++- .../workspace-action-form.tsx | 25 +-- apps/presentation/dashboard/src/router.tsx | 10 ++ .../dashboard/src/views/dashboard-page.tsx | 59 +++++-- .../coordination/python_whitespace.ts | 31 ++++ .../control_plane/coordination/todo_agents.ts | 33 +--- 37 files changed, 1413 insertions(+), 500 deletions(-) create mode 100644 apps/presentation/dashboard/src/features/personal-workspace/goal-tasks-view.css create mode 100644 apps/presentation/dashboard/src/features/personal-workspace/goal-work-browse.tsx create mode 100644 apps/presentation/dashboard/src/features/personal-workspace/markdown-blocks.ts create mode 100644 apps/presentation/dashboard/src/features/personal-workspace/markdown.css create mode 100644 apps/presentation/dashboard/src/features/personal-workspace/message-reading.css create mode 100644 apps/presentation/dashboard/src/features/personal-workspace/message-reading.tsx create mode 100644 apps/presentation/dashboard/src/features/personal-workspace/result-catalog.css create mode 100644 apps/presentation/dashboard/src/features/personal-workspace/result-catalog.tsx create mode 100644 loopx/control_plane/coordination/python_whitespace.ts diff --git a/apps/presentation/dashboard/src/features/personal-workspace/answer-report-page.css b/apps/presentation/dashboard/src/features/personal-workspace/answer-report-page.css index 2998e4b26a..eabfbb7e91 100644 --- a/apps/presentation/dashboard/src/features/personal-workspace/answer-report-page.css +++ b/apps/presentation/dashboard/src/features/personal-workspace/answer-report-page.css @@ -1,29 +1,39 @@ -.answer-report-page { min-height: 100dvh; background: #fafafa; color: #171717; padding: 0 20px 64px; } +.answer-report-page.personal-workspace-shell { display: block; min-height: 100dvh; background: var(--pw-bg); color: var(--pw-text); padding: 0 20px 64px; } .answer-report-shell { max-width: 920px; margin: 0 auto; } -.answer-report-header { display: flex; justify-content: space-between; align-items: center; gap: 20px; min-height: 84px; border-bottom: 1px solid #e7e7e7; } +.answer-report-header { display: flex; justify-content: space-between; align-items: center; gap: 20px; min-height: 84px; border-bottom: 1px solid var(--pw-line); } .answer-report-header > div { display: flex; align-items: baseline; flex-wrap: wrap; gap: 12px; } .answer-report-brand { font-size: var(--text-heading); line-height: var(--leading-heading); font-weight: 600; letter-spacing: -.04em; } -.answer-report-context { color: #777; font-size: var(--text-body); line-height: var(--leading-body); overflow-wrap: anywhere; } -.answer-report-header button, .answer-report-heading button { min-height: 44px; border: 1px solid #d7d7d7; border-radius: 8px; background: #fff; color: #171717; padding: 8px 14px; font: inherit; font-size: var(--text-body); cursor: pointer; } -.answer-report-header button:hover, .answer-report-heading button:hover { border-color: #8f8f8f; } -.answer-report-page button:focus-visible, .answer-report-page summary:focus-visible { outline: 2px solid #0070f3; outline-offset: 3px; } -.answer-report-body { padding: 48px min(5vw, 56px) 56px; margin: 28px 0 0; background: #fff; border: 1px solid #ebebeb; border-radius: 14px; min-width: 0; } -.answer-report-heading { display: flex; justify-content: space-between; align-items: center; gap: 16px; flex-wrap: wrap; border-bottom: 1px solid #ebebeb; padding-bottom: 24px; margin-bottom: 28px; } -.answer-report-heading small { color: #666; font-size: var(--text-caption); line-height: var(--leading-caption); } +.answer-report-context { color: var(--pw-muted); font-size: var(--text-body); line-height: var(--leading-body); overflow-wrap: anywhere; } +.answer-report-header button, .answer-report-heading button { min-height: 44px; border: 1px solid var(--pw-line-strong); border-radius: 8px; background: var(--pw-card); color: var(--pw-text); padding: 8px 14px; font: inherit; font-size: var(--text-body); cursor: pointer; } +.answer-report-header button:hover, .answer-report-heading button:hover { border-color: var(--pw-faint); } +.answer-report-page button:focus-visible, .answer-report-page summary:focus-visible, .answer-report-page a:focus-visible, .answer-report-page textarea:focus-visible { outline: 2px solid var(--pw-blue-ink); outline-offset: 3px; } +.answer-report-body { padding: 48px min(5vw, 56px) 56px; margin: 28px 0 0; background: var(--pw-card); border: 1px solid var(--pw-line); border-radius: 14px; min-width: 0; } +.answer-report-heading { display: flex; justify-content: space-between; align-items: center; gap: 16px; flex-wrap: wrap; border-bottom: 1px solid var(--pw-line); padding-bottom: 24px; margin-bottom: 28px; } +.answer-report-heading small { color: var(--pw-muted); font-size: var(--text-caption); line-height: var(--leading-caption); } .answer-report-heading h1 { font-size: clamp(var(--text-title), 3vw, var(--text-display)); font-weight: 600; letter-spacing: -.035em; line-height: 1.25; margin: 6px 0 8px; } -.answer-report-heading p { color: #777; margin: 0; font-size: var(--text-caption); line-height: var(--leading-caption); } +.answer-report-heading p { color: var(--pw-muted); margin: 0; font-size: var(--text-caption); line-height: var(--leading-caption); } .answer-report-content { min-width: 0; overflow-wrap: anywhere; } -.answer-report-content .personal-md { display: grid; gap: 18px; font-size: var(--text-lead); line-height: var(--leading-prose); } +.answer-report-page .answer-report-content .personal-md { display: grid; gap: 18px; font-size: var(--text-lead); line-height: var(--leading-prose); } .answer-report-content .personal-md p { margin: 0; font-size: inherit; line-height: inherit; } .answer-report-content .personal-md-heading.is-h1 { font-size: var(--text-title); line-height: var(--leading-prose); font-weight: 600; } .answer-report-content .personal-md-heading.is-h2 { font-size: var(--text-heading); line-height: var(--leading-prose); font-weight: 600; } .answer-report-content .personal-md-table-scroll { max-width: 100%; overflow-x: auto; } -.answer-report-body footer { border-top: 1px solid #ebebeb; margin-top: 40px; padding-top: 16px; color: #666; font-size: var(--text-caption); line-height: var(--leading-caption); } +.answer-report-body footer { border-top: 1px solid var(--pw-line); margin-top: 40px; padding-top: 16px; color: var(--pw-muted); font-size: var(--text-body); line-height: var(--leading-prose); } .answer-report-body footer summary { cursor: pointer; min-height: 36px; align-content: center; } .answer-report-body footer dl { display: grid; grid-template-columns: 72px minmax(0, 1fr); gap: 8px; } .answer-report-body footer dd { margin: 0; overflow-wrap: anywhere; } -.answer-report-body footer code { font-size: var(--text-caption); line-height: var(--leading-caption); } -.personal-answer-link { display: inline-block; margin-top: 8px; font-size: var(--text-caption); line-height: var(--leading-caption); color: var(--pw-blue-ink, #0070f3); text-decoration: none; } +.answer-report-body footer code { font-size: var(--text-body); line-height: var(--leading-body); } +.personal-answer-link { display: inline-block; margin-top: 8px; font-size: var(--text-caption); line-height: var(--leading-caption); color: var(--pw-blue-ink); text-decoration: none; } .personal-answer-link:hover { text-decoration: underline; } -.personal-answer-link:focus-visible { outline: 2px solid #0070f3; outline-offset: 3px; } -@media (max-width: 640px) { .answer-report-page { padding: 0 12px 28px; } .answer-report-header { min-height: 68px; } .answer-report-body { margin-top: 12px; padding: 28px 16px 36px; } } +.personal-answer-link:focus-visible { outline: 2px solid var(--pw-blue-ink); outline-offset: 3px; } +@media (max-width: 640px) { .answer-report-page.personal-workspace-shell { padding: 0 12px 28px; } .answer-report-header { min-height: 68px; } .answer-report-body { margin-top: 12px; padding: 28px 16px 36px; } } + +.answer-report-outline { margin-bottom: 28px; border-bottom: 1px solid var(--pw-line); padding-bottom: 16px; } +.answer-report-outline > summary { cursor: pointer; min-height: 44px; align-content: center; font-weight: 500; } +.answer-report-outline ol { display: grid; gap: 4px; margin: 8px 0 0; padding: 0; list-style: none; } +.answer-report-outline li { min-width: 0; overflow-wrap: anywhere; } +.answer-report-outline li[data-level="3"] { padding-inline-start: 16px; } +.answer-report-outline li[data-level="4"], .answer-report-outline li[data-level="5"], .answer-report-outline li[data-level="6"] { padding-inline-start: 32px; } +.answer-report-outline a { display: inline-block; padding-block: 8px; color: var(--pw-blue-ink); text-decoration: underline; text-underline-offset: 3px; } +.answer-report-copy-fallback { margin-bottom: 24px; } +.answer-report-copy-fallback textarea { display: block; width: 100%; resize: vertical; padding: 12px; border: 1px solid var(--pw-line-strong); border-radius: 6px; background: var(--pw-card); color: var(--pw-text); font: var(--text-body) / var(--leading-prose) var(--font-mono); } diff --git a/apps/presentation/dashboard/src/features/personal-workspace/answer-report-page.tsx b/apps/presentation/dashboard/src/features/personal-workspace/answer-report-page.tsx index d57a288fe2..e0ef26a4e9 100644 --- a/apps/presentation/dashboard/src/features/personal-workspace/answer-report-page.tsx +++ b/apps/presentation/dashboard/src/features/personal-workspace/answer-report-page.tsx @@ -1,4 +1,4 @@ -import {useEffect, useState} from "react"; +import {useEffect, useMemo, useRef, useState} from "react"; import {fetchChatSession, type ChatSessionSnapshot, type ChatVisibleMessage} from "../../data/chat"; import {visibleAgentMessage} from "./answer-text"; @@ -6,6 +6,8 @@ import {readWorkspaceLocale} from "./i18n"; import {resolveConversationScope} from "../../../../../../loopx/control_plane/collaboration/conversation_scope"; import {ReturnDeliveryStatus} from "./return-delivery-status"; import {MarkdownText} from "./markdown"; +import {markdownOutline, parseMarkdownBlocks} from "./markdown-blocks"; +import {readWorkspaceTheme} from "./workspace-theme"; import "./personal-workspace.css"; import "./goal-loopx-mode.css"; import "./answer-report-page.css"; @@ -13,7 +15,7 @@ import "./answer-report-page.css"; type ReportState = | {kind: "loading"} | {kind: "ready"; snapshot: ChatSessionSnapshot; message: ChatVisibleMessage} - | {kind: "missing"} + | {kind: "missing"; snapshot: ChatSessionSnapshot} | {kind: "error"}; export function AnswerReportPage({sessionId, messageId, statusUrl}: { @@ -21,27 +23,39 @@ export function AnswerReportPage({sessionId, messageId, statusUrl}: { messageId: string; statusUrl: string; }) { - const [state, setState] = useState({kind: "loading"}); + const scope = JSON.stringify([statusUrl || "/status.json", sessionId, messageId]); + const [loaded, setLoaded] = useState<{scope: string; value: ReportState} | null>(null); + // A late read from the preceding source never appears in the new reader. + const state: ReportState = loaded?.scope === scope ? loaded.value : {kind: "loading"}; + const [readRevision, setReadRevision] = useState(0); + const manualCopy = useRef(null); + const copyAttempt = useRef(0); const [copied, setCopied] = useState(false); const [copyFailed, setCopyFailed] = useState(false); const zh = readWorkspaceLocale() === "zh-CN"; useEffect(() => { let live = true; + const controller = new AbortController(); + copyAttempt.current += 1; const previousTitle = document.title; document.title = zh ? "答复记录 · LoopX" : "Answer record · LoopX"; - setState({kind: "loading"}); - void fetchChatSession(sessionId).then((snapshot) => { + setLoaded({scope, value: {kind: "loading"}}); + setCopied(false); + setCopyFailed(false); + void fetchChatSession(sessionId, controller.signal).then((snapshot) => { if (!live) return; + if (snapshot.ok !== true || snapshot.schema_version !== "loopx_chat_store_v1" + || snapshot.session.session_id !== sessionId) throw new Error("The saved Session changed during read."); const message = snapshot.messages.find((item) => item.message_id === messageId && ["agent", "assistant"].includes(item.role)); - setState(message ? {kind: "ready", snapshot, message} : {kind: "missing"}); - }).catch(() => { if (live) setState({kind: "error"}); }); - return () => { live = false; document.title = previousTitle; }; - }, [sessionId, messageId, zh]); + setLoaded({scope, value: message ? {kind: "ready", snapshot, message} : {kind: "missing", snapshot}}); + }).catch(() => { if (live) setLoaded({scope, value: {kind: "error"}}); }); + return () => {live = false; controller.abort(); copyAttempt.current += 1; document.title = previousTitle;}; + }, [scope, sessionId, messageId, zh, readRevision]); const navigation = answerReportNavigation( - state.kind === "ready" ? state.snapshot.session : null, window.location.href, statusUrl, zh, + (state.kind === "ready" || state.kind === "missing") ? state.snapshot.session : null, window.location.href, statusUrl, zh, ); const closeOrReturn = () => { if (window.opener) window.close(); @@ -50,7 +64,36 @@ export function AnswerReportPage({sessionId, messageId, statusUrl}: { const answer = state.kind === "ready" ? state.message : null; const answerText = answer ? visibleAgentMessage(answer.text) : ""; - const sourceQuestion = state.kind === "ready" + const headingPrefix = `answer-${sessionId}-${messageId}`; + const headings = useMemo(() => markdownOutline(parseMarkdownBlocks(answerText), headingPrefix), [answerText, headingPrefix]); + useEffect(() => { + if (!copyFailed) return; + manualCopy.current?.focus(); manualCopy.current?.select(); + }, [copyFailed]); + useEffect(() => { + if (state.kind !== "ready") return; + let frame = window.requestAnimationFrame(() => { + frame = window.requestAnimationFrame(() => { + const anchor = headings.find(heading => `#${encodeURIComponent(heading.id)}` === window.location.hash + || `#${heading.id}` === window.location.hash); + if (anchor) document.getElementById(anchor.id)?.scrollIntoView({block: "start"}); + else window.scrollTo({top: readAnswerPosition(scope), behavior: "instant"}); + }); + }); + const save = () => saveAnswerPosition(scope, window.scrollY); + window.addEventListener("pagehide", save); + return () => {window.cancelAnimationFrame(frame); window.removeEventListener("pagehide", save); save();}; + }, [scope, state.kind, headings]); + async function copyAnswer() { + const attempt = ++copyAttempt.current; + setCopyFailed(false); + try { + if (!navigator.clipboard?.writeText) throw new Error("Clipboard unavailable"); + await navigator.clipboard.writeText(answerText); + if (attempt === copyAttempt.current) setCopied(true); + } catch {if (attempt === copyAttempt.current) {setCopied(false); setCopyFailed(true);}} + } + const sourceQuestion = state.kind === "ready" && answer?.turn_id ? state.snapshot.messages.find((item) => item.turn_id === answer?.turn_id && item.role === "user") : null; const created = answer?.created_at ? new Date(answer.created_at) : null; @@ -58,7 +101,7 @@ export function AnswerReportPage({sessionId, messageId, statusUrl}: { ? new Intl.DateTimeFormat(zh ? "zh-CN" : "en", {dateStyle: "medium", timeStyle: "short"}).format(created) : null; - return
+ return
LoopX @@ -70,24 +113,23 @@ export function AnswerReportPage({sessionId, messageId, statusUrl}: {
{zh ? "已保存的答复" : "Saved answer"} {dateLabel ?

: null} -
{answer ? : null}
- {copyFailed ?

{zh ? "复制失败。请从正文中选中并复制。" : "Copy failed. Select and copy the answer text instead."}

: null} +
{answer ? : null}
+ {copyFailed ?

{zh ? "复制失败。可从下面的 Markdown 原文中选中并复制。" : "Copy failed. Select and copy the Markdown source below."}

+