diff --git a/apps/presentation/dashboard/smoke/conversation-history-smoke.ts b/apps/presentation/dashboard/smoke/conversation-history-smoke.ts index bcf72d5d46..8ae67ab3fb 100644 --- a/apps/presentation/dashboard/smoke/conversation-history-smoke.ts +++ b/apps/presentation/dashboard/smoke/conversation-history-smoke.ts @@ -41,7 +41,8 @@ try { assert.equal((await next()).recovered, true); requests.length = 0; const restored = await fetchChatHistory(options, partial); - assert.deepEqual(requests, ["/api/chat/sessions/old"], "Recovery reads only the missing session"); + assert.deepEqual(requests.map(request => new URL(request, origin).pathname), + ["/api/chat/sessions/old"], "Recovery reads only the missing session"); assert.deepEqual(restored.unavailableSessionIds, []); assert.deepEqual(restored.messages.map(row => row.text), ["Earlier public report", "Current public report"]); assert.deepEqual(restored.messages.map(row => row.session_id), ["old", "current"], "Colliding IDs preserve both sessions"); diff --git a/apps/presentation/dashboard/smoke/conversation-return-browser-smoke.mjs b/apps/presentation/dashboard/smoke/conversation-return-browser-smoke.mjs index 1456c0b7f6..882c016770 100644 --- a/apps/presentation/dashboard/smoke/conversation-return-browser-smoke.mjs +++ b/apps/presentation/dashboard/smoke/conversation-return-browser-smoke.mjs @@ -112,6 +112,8 @@ try { assert.ok((await (await recoveredRead).text()).includes("Revised checked result"), "A non-current conversation recovers before navigation back"); await page.locator(".personal-manager-link").click(); await chat(); + assert.equal(await composer.inputValue(), "Keep this draft while original history recovers.", + "Goal navigation and background recovery must preserve the steward draft"); await page.getByText("Revised checked result", { exact: true }).waitFor({ state: "visible" }); assert.equal(await page.getByText("First checked result", { exact: true }).count(), 1); assert.equal(await page.getByText("Revised checked result", { exact: true }).count(), 1); diff --git a/apps/presentation/dashboard/src/features/personal-workspace/app-steward-entry.tsx b/apps/presentation/dashboard/src/features/personal-workspace/app-steward-entry.tsx index efdc57bd1e..1a23420147 100644 --- a/apps/presentation/dashboard/src/features/personal-workspace/app-steward-entry.tsx +++ b/apps/presentation/dashboard/src/features/personal-workspace/app-steward-entry.tsx @@ -37,26 +37,36 @@ export function useAppStewardTarget(enabled: boolean) { unavailable: ready && read.unavailable, retry}; } -export function AppStewardEntry({children, onOpen}: {children: ReactNode; onOpen: (target: BoundConversationTarget) => void}) { +export function AppStewardEntry({children, enabled, intercept, onOpen}: { + children: (entry: ReturnType) => ReactNode; + enabled: boolean; + intercept: boolean; + onOpen: (target: BoundConversationTarget) => void; +}) { const {locale} = useWorkspaceI18n(); const zh = locale === "zh-CN"; - const entry = useAppStewardTarget(true); + const entry = useAppStewardTarget(enabled); const open = useRef(onOpen); open.current = onOpen; - useEffect(() => {if (entry.target) open.current(entry.target);}, [entry.target]); - if (!entry.loading && !entry.unavailable && !entry.target) return children; + useEffect(() => {if (intercept && entry.target) open.current(entry.target);}, [intercept, entry.target]); + const blocked = intercept && (entry.loading || entry.unavailable || Boolean(entry.target)); const settings = new URL(window.location.href); settings.searchParams.set("view", "overview"); settings.searchParams.set("settings_scope", "machine"); settings.searchParams.set("settings_tab", "lark"); - return
-

{zh ? "LoopX 管家" : "LoopX steward"}

- {!entry.unavailable ?

{zh ? "正在打开管家对话…" : "Opening the steward conversation…"}

: <> -

{zh ? "原管家会话暂不可用。请检查所选连接后重试。" : "The original steward conversation is unavailable. Check the selected connection and retry."}

-
- - {zh ? "打开会话设置" : "Open conversation settings"} -
- } -
; + return <> + {/* Keep the workspace and its readers mounted across entry/navigation. + The same readback gates its queue while discovery is unresolved. */} +
{children(entry)}
+ {blocked ?
+

{zh ? "LoopX 管家" : "LoopX steward"}

+ {!entry.unavailable ?

{zh ? "正在打开管家对话…" : "Opening the steward conversation…"}

: <> +

{zh ? "原管家会话暂不可用。请检查所选连接后重试。" : "The original steward conversation is unavailable. Check the selected connection and retry."}

+
+ + {zh ? "打开会话设置" : "Open conversation settings"} +
+ } +
: null} + ; } diff --git a/apps/presentation/dashboard/src/views/dashboard-page.tsx b/apps/presentation/dashboard/src/views/dashboard-page.tsx index 36753836e6..676eb21129 100644 --- a/apps/presentation/dashboard/src/views/dashboard-page.tsx +++ b/apps/presentation/dashboard/src/views/dashboard-page.tsx @@ -1229,6 +1229,7 @@ function buildPersonalHomeModel( }; } function PersonalGoalHome({ + appEntry, goalArchiveLoadState, sourceError, selectedView, @@ -1259,6 +1260,7 @@ function PersonalGoalHome({ statusSourceControl, theme, }: { + appEntry: ReturnType; goalArchiveLoadState: WorkspaceGoalArchiveLoadState; /** The current source's latest read failed; the retained payload is stale. */ sourceError: string | null; @@ -1379,10 +1381,6 @@ function PersonalGoalHome({ }, }; }, [sourceError, scopedModel, t]); - const appEntryEnabled = !readOnly && statusSourceControl.activeSource.kind === "local" - && !requestedGoalId && !rootGoalId && !requestedWorkspaceRef && !requestedBoundConversation - && selectedWorkAgentId == null && !selectedWorkSessionId && selectedView !== "chat"; - const appEntry = useAppStewardTarget(appEntryEnabled); const appEntryBlocked = appEntry.loading || appEntry.unavailable; const boundConversation = requestedBoundConversation ?? appEntry.target; const selectedGoalId = rootGoalId && !model.goals.some(goal => goal.goalId === requestedGoalId) @@ -3787,8 +3785,9 @@ export function DashboardPage() { ); } - const home = ( + const home = (appEntry: ReturnType) => ( void navigate({search: current => ({...current, goalId: "", workspace: undefined, @@ -3859,15 +3858,16 @@ export function DashboardPage() { theme={theme} /> ); - const defaultStewardEntry = search.view === "conversation" && !search.goalId && !search.rootGoalId - && search.workspace === undefined && search.workAgent === undefined && search.workSession === undefined + const stewardEntryEnabled = !search.goalId && !search.rootGoalId + && !search.workspace && search.workAgent === undefined && !search.workSession && search.conversationBinding === undefined && search.conversationSession === undefined && activeStatusSource.kind === "local" && !activeStatusSource.readOnly; - return defaultStewardEntry ? void navigate({replace: true, + return void navigate({replace: true, search: current => "goalId" in current && current.statusUrl === search.statusUrl && current.view === "conversation" - && !current.goalId && !current.rootGoalId && current.workspace === undefined && current.workAgent === undefined - && current.workSession === undefined && current.conversationBinding === undefined && current.conversationSession === undefined + && !current.goalId && !current.rootGoalId && !current.workspace && current.workAgent === undefined + && !current.workSession && current.conversationBinding === undefined && current.conversationSession === undefined ? {...current, conversationBinding: target.bindingId, conversationSession: target.sessionId} : current})}> {home} - : home; + ; } diff --git a/docs/architecture/rfcs/app-conversation-and-async-inbox-v0.md b/docs/architecture/rfcs/app-conversation-and-async-inbox-v0.md index 93a2c67bed..b933314867 100644 --- a/docs/architecture/rfcs/app-conversation-and-async-inbox-v0.md +++ b/docs/architecture/rfcs/app-conversation-and-async-inbox-v0.md @@ -1315,9 +1315,13 @@ state or grant. Response row count is bounded; server-side journal reads and prefix hashing still scale with full history and no latency improvement is claimed. HTTP/store and source/packaged browser checks qualify this bounded history path; installed native input, reply threading and live App/Lark media -remain separate acceptance. The scoped packaged `--pagination-only` journey -does not certify the default return scenario's background-read wait, which also -times out with the unchanged baseline production code; that gap remains open. +remain separate acceptance. Earlier baseline and `--pagination-only` runs did +not qualify the default return scenario's background wait. That historical +timeout is retained; the default return journey now reads background revisions +across steward/Goal navigation. The App entry keeps one workspace and its +readers mounted, sharing its source readback. Pending or failed discovery still +gates the ordinary queue. Disposable HTTP/store checks qualify this continuation +path, not installed native or live-provider acceptance. Following the newest reply also survives a resized viewport or timeline, including image layout and expanded replies, without waiting for another message. Scrolling diff --git a/docs/architecture/rfcs/app-conversation-and-async-inbox-v0.zh-CN.md b/docs/architecture/rfcs/app-conversation-and-async-inbox-v0.zh-CN.md index 40379c32b4..da776f1b79 100644 --- a/docs/architecture/rfcs/app-conversation-and-async-inbox-v0.zh-CN.md +++ b/docs/architecture/rfcs/app-conversation-and-async-inbox-v0.zh-CN.md @@ -977,8 +977,11 @@ TS 历史 owner 校验并读完全部页面,随后复核 读取提示,不增加 store、consumer、执行状态或 grant。响应记录数有界,但服务端 journal 读取和前缀摘要仍随完整历史增长,不宣称延迟改善。HTTP/store 与源码、 打包浏览器检查验收该历史路径;实际原生输入、回复线程和真实 App/飞书媒体仍需 -分别验收。打包 `--pagination-only` 有界旅程不代表默认回传场景的后台读取等待已通过; -该等待在未修改的基线生产代码上同样超时,缺口继续保留。 +分别验收。此前基线及 `--pagination-only` 运行未验收默认回传场景的后台读取等待, +该历史超时保留;当前默认回传旅程已覆盖管家/Goal 切换期间的后台修订读取。 +App 入口保持同一个工作区及其读取器挂载,共享来源读回;来源发现尚未完成或失败时 +仍阻止普通队列准入。隔离 HTTP/store 检查只验收该接续路径,不扩大实际原生或 +真实 provider 的验收范围。 同一对话界面在用户停留底部时跟随输出,上翻阅读后保持位置,并提供回到最新消息的入口。 窗口或时间线尺寸变化(包括图片布局与展开回复)后仍应跟随最新回复,不必等待下一条消息。 diff --git a/examples/personal-workspace-browser/evidence/steward-navigation/bound-entry-denied-phone.png b/examples/personal-workspace-browser/evidence/steward-navigation/bound-entry-denied-phone.png new file mode 100644 index 0000000000..5aae72d42a Binary files /dev/null and b/examples/personal-workspace-browser/evidence/steward-navigation/bound-entry-denied-phone.png differ diff --git a/examples/personal-workspace-browser/evidence/steward-navigation/bound-entry-desktop.png b/examples/personal-workspace-browser/evidence/steward-navigation/bound-entry-desktop.png new file mode 100644 index 0000000000..8b0265d28e Binary files /dev/null and b/examples/personal-workspace-browser/evidence/steward-navigation/bound-entry-desktop.png differ diff --git a/examples/personal-workspace-browser/evidence/steward-navigation/bound-entry-long-phone.png b/examples/personal-workspace-browser/evidence/steward-navigation/bound-entry-long-phone.png new file mode 100644 index 0000000000..66181c9da6 Binary files /dev/null and b/examples/personal-workspace-browser/evidence/steward-navigation/bound-entry-long-phone.png differ