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,4 +1,5 @@
import { useWorkspaceI18n } from "./i18n";
import { localizedAttentionAge, useWorkspaceI18n } from "./i18n";
import { MarkdownText } from "./markdown";
import type { WorkspaceAttention } from "./personal-workspace-model";

export function AttentionDetailCard({ item, successor, onSelect }: {
Expand All @@ -8,23 +9,34 @@ export function AttentionDetailCard({ item, successor, onSelect }: {
}) {
const { t } = useWorkspaceI18n();
const detail = item.details;
return <section className="personal-detail-card" aria-label={t("attentionDetail.title")}>
<h3>{t("attentionDetail.title")}</h3>
{detail?.requestText && detail.requestText !== item.text ? <p style={{ whiteSpace: "pre-wrap", overflowWrap: "anywhere" }}>{detail.requestText}</p> : null}
<dl>
<div><dt>{t("attentionDetail.request")}</dt><dd>{t(detail?.interaction === "decision" ? "attentionDetail.decision" : "attentionDetail.unknownRequest")}</dd></div>
<div><dt>{t("common.status")}</dt><dd>{t(`attentionDetail.${detail?.lifecycle ?? "unknown"}`)}</dd></div>
<div><dt>{t("drawer.reason")}</dt><dd>{detail?.reason ?? item.explanation ?? t("attentionDetail.unknownReason")}</dd></div>
<div><dt>Todo</dt><dd>{item.todoId}</dd></div>
<div><dt>{t("attentionDetail.targetTodo")}</dt><dd>{detail?.unblocksTodoId ?? t("attentionDetail.notProvided")}</dd></div>
<div><dt>{t("attentionDetail.targetAgent")}</dt><dd>{detail?.blocksAgent ?? t("attentionDetail.notProvided")}</dd></div>
<div><dt>{t("attentionDetail.scope")}</dt><dd>{detail?.decisionScope
? `${detail.decisionScope.kind} · ${detail.decisionScope.granularity} · ${detail.decisionScope.scopeKey}`
: t("attentionDetail.notProvided")}</dd></div>
<div><dt>{t("drawer.evidence")}</dt><dd>{detail?.evidence ?? item.evidence ?? t("drawer.decisionDefaultEvidence")}</dd></div>
</dl>
{detail?.supersededBy ? <p>{t("attentionDetail.replacement")}: {detail.supersededBy}</p> : null}
const age = localizedAttentionAge(item.updatedAt, t);
const lifecycle = detail?.lifecycle ?? "unknown";
return <section className="personal-detail-card personal-attention-brief" aria-label={t("attentionDetail.title")}>
<div className="personal-attention-meta">
<span>{t(`attentionDetail.${lifecycle}`)}</span>
{item.priority ? <span>{item.priority}</span> : null}
{age ? <span>{t("tasks.waitingAge", { age })}</span> : null}
</div>
<h3>{t(detail?.requestText ? "attentionDetail.request" : "attentionDetail.summary")}</h3>
{detail?.requestText ? <MarkdownText text={detail.requestText} /> : <>
<p className="personal-attention-missing" role="status">{t("attentionDetail.summaryOnly")}</p>
<MarkdownText text={item.text} />
</>}
<h4>{t("drawer.reason")}</h4>
<MarkdownText text={detail?.reason ?? item.explanation ?? t("attentionDetail.unknownReason")} />
<h4>{t("drawer.evidence")}</h4>
<MarkdownText text={detail?.evidence ?? item.evidence ?? t("drawer.decisionDefaultEvidence")} />
{successor && onSelect ? <button className="personal-secondary-action" onClick={() => onSelect(successor)} type="button">{t("attentionDetail.openReplacement")}</button> : null}
<details className="personal-attention-identifiers">
<summary>{t("drawer.advancedDiagnostics")}</summary>
<dl>
<div><dt>Todo</dt><dd>{item.todoId}</dd></div>
{detail?.unblocksTodoId ? <div><dt>{t("attentionDetail.targetTodo")}</dt><dd>{detail.unblocksTodoId}</dd></div> : null}
{detail?.blocksAgent ? <div><dt>{t("attentionDetail.targetAgent")}</dt><dd>{detail.blocksAgent}</dd></div> : null}
{detail?.decisionScope ? <div><dt>{t("attentionDetail.scope")}</dt><dd>{detail.decisionScope.kind} · {detail.decisionScope.granularity} · {detail.decisionScope.scopeKey}</dd></div> : null}
{detail?.supersededBy ? <div><dt>{t("attentionDetail.replacement")}</dt><dd>{detail.supersededBy}</dd></div> : null}
</dl>
</details>
<p>{t("attentionDetail.boundary")}</p>
</section>;
}
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ import type {
WorkspaceTodo,
} from "./personal-workspace-model";
import type { LarkGoalConnection } from "../../data/chat";
import { localizedAttentionAge, localizedGoalState, localizedSessionStatus, useWorkspaceI18n } from "./i18n";
import { localizedGoalState, localizedSessionStatus, useWorkspaceI18n } from "./i18n";
import { formatCostUsd, formatDurationMs, formatTokenCount, formatUsageValue } from "./personal-workspace-model";
import { TeamPlanResult } from "./team-plan-result";
import { parseTodoResumeCondition } from "./todo-resume-condition";
Expand All @@ -57,6 +57,7 @@ const focusableSelector = [
"textarea:not([disabled])",
"select:not([disabled])",
"input:not([disabled])",
"summary",
"[tabindex]:not([tabindex='-1'])",
].join(",");

Expand Down Expand Up @@ -264,7 +265,7 @@ export function ContextDrawer({ agents, attentionHistory = [], onSelectAttention
}
if (event.key === "Tab" && selection.kind !== "todo") {
const focusable = Array.from(drawerRef.current?.querySelectorAll<HTMLElement>(focusableSelector) ?? [])
.filter((element) => !element.hasAttribute("disabled") && element.getAttribute("aria-hidden") !== "true");
.filter((element) => !element.hasAttribute("disabled") && element.getAttribute("aria-hidden") !== "true" && element.checkVisibility({ visibilityProperty: true }));
if (focusable.length === 0) return;
const first = focusable[0];
const last = focusable[focusable.length - 1];
Expand Down Expand Up @@ -316,7 +317,6 @@ export function ContextDrawer({ agents, attentionHistory = [], onSelectAttention
|| Boolean(selection.item.latestActivity)
|| Boolean(selection.item.outputs?.length)
);
const attentionAge = selection.kind === "attention" ? localizedAttentionAge(selection.item.updatedAt, t) : null;
const normalizedTodoResumeWhen = parseTodoResumeCondition(todoResumeWhen);

const selectedRunId = selection.kind === "run" ? selection.item.runId : null;
Expand Down Expand Up @@ -602,16 +602,6 @@ export function ContextDrawer({ agents, attentionHistory = [], onSelectAttention
<div className="personal-drawer-body">
{selection.kind === "attention" ? (
<>
<section className="personal-detail-card is-attention">
<small>{selection.item.blocking ? t("drawer.attentionBlocking") : t("drawer.attentionWaiting")}</small>
<h3>{selection.item.text}</h3>
<dl>
<div><dt>Goal</dt><dd>{selection.item.goalTitle ?? selection.item.goalId}</dd></div>
<div><dt>{t("drawer.priority")}</dt><dd>{selection.item.priority ?? "medium"}</dd></div>
{attentionAge ? <div><dt>{t("common.waiting")}</dt><dd>{t("tasks.waitingAge", { age: attentionAge })}</dd></div> : null}

</dl>
</section>
<AttentionDetailCard item={selection.item} onSelect={onSelectAttention} successor={attentionSuccessor(selection.item, attentionHistory)} />
{!readOnly && canReviewAttention(selection.item) ? <>
<button className="personal-primary-action" onClick={() => void previewDecision(selection.item, "approve", t("common.confirm"))} type="button"><Check size={17} />{t("drawer.decisionReview")}</button>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -155,9 +155,9 @@ const en = {
"drawer.usageNotMeasured": "Not measured",
"drawer.currentGoal": "Current Goal",
"attentionDetail.title": "Request details",
"attentionDetail.summary": "Available summary",
"attentionDetail.summaryOnly": "Only a summary is available. The source has not supplied the complete request.",
"attentionDetail.request": "What is requested",
"attentionDetail.decision": "Decision requested",
"attentionDetail.unknownRequest": "Not specified; review the source before deciding",
"attentionDetail.open": "Open in current projection",
"attentionDetail.closed": "Closed",
"attentionDetail.deferred": "Deferred",
Expand All @@ -168,7 +168,6 @@ const en = {
"attentionDetail.targetTodo": "Linked Todo to unblock",
"attentionDetail.targetAgent": "Agent named by the request",
"attentionDetail.scope": "Declared decision scope",
"attentionDetail.notProvided": "Not provided",
"attentionDetail.replacement": "Replacement Todo",
"attentionDetail.openReplacement": "Open replacement",
"attentionDetail.boundary": "Reading this detail does not resolve a gate or grant authority. Available decisions require a fresh preview.",
Expand Down Expand Up @@ -1347,9 +1346,9 @@ const zhCN: Record<WorkspaceMessageKey, string> = {
"drawer.usageNotMeasured": "未采集",
"drawer.currentGoal": "当前 Goal",
"attentionDetail.title": "事项说明",
"attentionDetail.summary": "现有摘要",
"attentionDetail.summaryOnly": "目前只有摘要,来源尚未提供完整请求。",
"attentionDetail.request": "需要你做什么",
"attentionDetail.decision": "需要作出决定",
"attentionDetail.unknownRequest": "来源未明确,请先查看来源再判断",
"attentionDetail.open": "当前投影中待处理",
"attentionDetail.closed": "已关闭",
"attentionDetail.deferred": "已推迟",
Expand All @@ -1360,7 +1359,6 @@ const zhCN: Record<WorkspaceMessageKey, string> = {
"attentionDetail.targetTodo": "关联的待解锁 Todo",
"attentionDetail.targetAgent": "事项指定的 Agent",
"attentionDetail.scope": "声明的决策范围",
"attentionDetail.notProvided": "来源未提供",
"attentionDetail.replacement": "替代 Todo",
"attentionDetail.openReplacement": "打开替代事项",
"attentionDetail.boundary": "阅读详情不会关闭 gate 或授予权限;作出决定前仍需新的操作预览。",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1174,6 +1174,18 @@ button.personal-execution-chip:focus-visible { outline: 2px solid #0070f3; outli
.personal-detail-card dl div { display: grid; grid-template-columns: 74px minmax(0, 1fr); gap: 10px; font-size: 12px; }
.personal-detail-card dt { color: var(--pw-faint); }
.personal-detail-card dd { margin: 0; overflow-wrap: anywhere; }
.personal-attention-meta { display: flex; flex-wrap: wrap; gap: 8px 16px; color: var(--pw-muted); font-size: 12px; }
.personal-attention-brief h3 { margin: 20px 0 12px; font-size: 16px; }
.personal-attention-brief h4 { margin: 24px 0 8px; font-size: 13px; font-weight: 600; }
.personal-attention-brief .personal-md { font-size: 14px; line-height: 1.6; overflow-wrap: anywhere; }
.personal-attention-brief .personal-md p { color: var(--pw-text); font-size: inherit; line-height: inherit; }
.personal-attention-brief .personal-attention-missing { margin: 0 0 12px; padding: 10px 12px; border-radius: 8px; background: var(--pw-amber-bg); color: var(--pw-amber); }
.personal-attention-identifiers { margin-top: 24px; border-top: 1px solid var(--pw-line); padding-top: 16px; }
.personal-attention-identifiers summary { color: var(--pw-muted); cursor: pointer; font-size: 12px; min-height: 44px; display: flex; align-items: center; gap: 8px; list-style: none; }
.personal-attention-identifiers summary::-webkit-details-marker { display: none; }
.personal-attention-identifiers summary::before { content: "▸"; }
.personal-attention-identifiers[open] summary::before { content: "▾"; }
.personal-attention-identifiers dl { margin: 0 0 16px; }
.personal-run-drawer-tabs { display: grid; grid-template-columns: 1fr 1fr; gap: 4px; padding: 4px; border-radius: 11px; background: #efefec; }
.personal-run-drawer-tabs button { min-height: 36px; padding: 0 8px; border: 0; border-radius: 8px; background: transparent; color: var(--pw-muted); cursor: pointer; font-size: 12px; font-weight: 650; }
.personal-run-drawer-tabs button[aria-selected="true"] { background: #fff; color: var(--pw-text); box-shadow: 0 1px 4px rgb(30 28 20 / 12%); }
Expand Down
17 changes: 15 additions & 2 deletions docs/architecture/rfcs/app-conversation-and-async-inbox-v0.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,8 +74,13 @@ Transfer the hierarchy, not the artwork or untested claims:
routine activity; preserve missing authority, stale information and failures.
Decision notices use the request body, object and evidence rather than a short
scheduling label. Keep distinct request identities and label bounded previews;
users inspect the current request before deciding. App detail drawers retain
the request separately from the compact card label. Provider notices use the
users inspect the current request before deciding. Steward and Goal details
share one readable request brief: full body, reason/recommendation and linked
evidence first; identifiers and declared scope remain in a keyboard-accessible
disclosure. The compact list label is not repeated as another detail card.
A summary-only source is labelled as such rather than presented as a complete
request. Existing fresh-preview, read-only and lifecycle fences still govern
actions; Markdown presentation cannot infer authority. Provider notices use the
same content distinction: missing request bodies are explicitly unavailable,
never reconstructed from legacy action labels or free-form gate prompts.
Retire obsolete presentation branches rather than preserving old data shapes
Expand All @@ -95,6 +100,14 @@ return context. First-screen changes still require the repository's preview gate
No external screenshot, private incident transcript or proprietary asset is
redistributed by this proposal.

The decision-detail slice qualifies the packaged renderer with synthetic
desktop Chinese, narrow English and read-only sources: readable Markdown,
retained full text, evidence links, disclosure/return focus, one correctly
scoped preview, failed/missing/replaced source fences and inert source HTML.
This is a proposed App presentation improvement, not installed readback or
full GQ10 prioritization. Cross-project selection, at most two recommended
priorities and actual scoped adoption remain in the existing P1 attention work.

### Waiting is part of the conversation

One shared TypeScript activity surface serves manager and Goal conversations,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,13 +56,21 @@ LoopX 还必须核验综合结果返回原请求。这些是文档陈述,非
- 活跃会话和结果获得最大的有效阅读空间。简洁的委派记录说明谁负责什么并链接实际工作;
返回记录指向当前结果。不要让用户到另一会话寻找已请求的答案。
- 把一个有用的下一步放在对应失败或决定旁。折叠常规活动,保留缺失授权、过期信息和失败。
管家与 Goal 详情复用一份可读事项说明:正文、原因/建议和证据链接优先,标识与声明范围放入键盘可达的折叠区;
不把列表摘要重复成另一张详情卡。来源只有摘要时明确标注,不能冒充完整请求。
操作继续由既有新预览、只读和生命周期约束决定,Markdown 展示不能推断授权。
- 使用现有设计系统的字体、间距和克制的状态强调。动效解释已核验的转换,不虚构忙碌 worker。
- 保持创建、连接、直接与 owner 对话以及团队工作可发现。更简洁的界面不能隐藏未解决工作或减少授权范围内的 owner 发现。

预览使用公开或合成数据。检查有内容、安静、阻塞、不可用的桌面和窄屏视图。
保留键盘访问、阅读位置和返回上下文。首屏改动仍须遵循仓库预览门禁。
本设计不重新分发外部截图、私有事故记录或专有素材。

事项详情这一切片验证打包渲染器及合成的桌面中文、窄屏英文和只读来源:可读 Markdown、
完整正文、证据链接、折叠区/返回焦点、一次准确作用域的预览、失败/缺失/被替代来源约束,以及不可执行的来源 HTML。
这是待交付的 App 展示改进,不代表已安装回读或完整 GQ10 排序能力。
跨项目选择、最多两项建议与真实的范围内采用,仍归既有 P1 注意力工作。

### 等待也是对话的一部分

管家、Goal 对话和总览里的紧凑回执共用一个 TypeScript 活动组件。
Expand Down
Loading
Loading