diff --git a/apps/presentation/dashboard/src/features/personal-workspace/lark-settings-page.tsx b/apps/presentation/dashboard/src/features/personal-workspace/lark-settings-page.tsx index 1f9470e3e8..7460516cbe 100644 --- a/apps/presentation/dashboard/src/features/personal-workspace/lark-settings-page.tsx +++ b/apps/presentation/dashboard/src/features/personal-workspace/lark-settings-page.tsx @@ -582,14 +582,15 @@ export function LarkSettingsPage({ ) : null}
+ {filteredConnections.length === 0 ?

{t("lark.noConnections")}

: null}
-
-
{t("lark.connection")}{t("common.goal")}{t("lark.capture")}{t("lark.processing")}{t("common.actions")}
+ {filteredConnections.length > 0 ?
+
{t("lark.connection")}{t("common.goal")}{t("lark.capture")}{t("lark.processing")}{t("common.actions")}
{filteredConnections.map((connection) => { const health = larkConnectionHealth(connection, t); return
- + {connection.chat_name} {connection.app_label} · {health.label} {health.detail} @@ -600,17 +601,16 @@ export function LarkSettingsPage({ {t("lark.historyPermission")} ) : null} - {connection.goal_title}# {connection.topic_name}{connection.conversation_kind === "manager" ? {t(connection.read_goal_scope === "all_registered" ? "lark.readAllGoals" : connection.read_goal_scope === null ? "lark.readScopeUnavailable" : "lark.readSelectedGoals")} : null} - {connection.conversation_kind === "manager" ? t(connection.turn_trigger === "human_messages" ? "lark.triggerHuman" : "lark.triggerAddressed") : connection.capture_scope === "addressed_only" ? t("lark.mentionsOnly") : t("lark.allTopicMessages")} - {connection.conversation_kind === "manager" ? t("lark.managerConversation") : ingressPresentation(connection.ingress_mode, t).label}{connection.conversation_kind === "manager" ? t("lark.managerConversationDescription") : connection.agent_id ?? ingressPresentation(connection.ingress_mode, t).detail} - + {connection.goal_title}# {connection.topic_name}{connection.conversation_kind === "manager" ? {t(connection.read_goal_scope === "all_registered" ? "lark.readAllGoals" : connection.read_goal_scope === null ? "lark.readScopeUnavailable" : "lark.readSelectedGoals")} : null} + {connection.conversation_kind === "manager" ? t(connection.turn_trigger === "human_messages" ? "lark.triggerHuman" : "lark.triggerAddressed") : connection.capture_scope === "addressed_only" ? t("lark.mentionsOnly") : t("lark.allTopicMessages")} + {connection.conversation_kind === "manager" ? t("lark.managerConversation") : ingressPresentation(connection.ingress_mode, t).label}{connection.conversation_kind === "manager" ? t("lark.managerConversationDescription") : connection.agent_id ?? ingressPresentation(connection.ingress_mode, t).detail} +
; })} - {filteredConnections.length === 0 ?

{t("lark.noConnections")}

: null} -
+
: null} ) : null} diff --git a/apps/presentation/dashboard/src/features/personal-workspace/private-conversation-panel.tsx b/apps/presentation/dashboard/src/features/personal-workspace/private-conversation-panel.tsx index ff97f46b34..2657f92f07 100644 --- a/apps/presentation/dashboard/src/features/personal-workspace/private-conversation-panel.tsx +++ b/apps/presentation/dashboard/src/features/personal-workspace/private-conversation-panel.tsx @@ -1,3 +1,4 @@ +import {MessageSquareText, RefreshCw} from "lucide-react"; import {ContextHelp} from "./context-help"; import {useEffect, useRef, useState} from "react"; import {connectPrivateConversation, disconnectPrivateConversation, fetchPrivateConversations, @@ -116,13 +117,22 @@ export function PrivateConversationPanel({apps, onOpenOriginal}: {apps: LarkApp[ } else {setProjectGrant("workspace_write"); setAudience("owner");} } return
-

{zh ? "飞书 · 项目助手与管家" : "Lark · Project assistants and steward"}

-

{zh ? "每个 App 单独核验本人。项目助手默认支持工作区读写,按项目规则与 skills 执行编辑;可选只读。普通聊天不创建隐藏 Goal。本人管家默认读取本机已注册的 Goal 与 Agent;没有注册工作时显示空态。旧连接保留原范围,需明确升级授权。交办不提升执行或发布权限。" : "Verify the owner independently for each App. Project Chat defaults to workspace writes under project rules and skills; read-only remains available without hidden Goals. A personal steward defaults to the registered local Goals and Agents, including an honest empty state. Existing connections retain their scope until explicitly upgraded. Handoff does not elevate execution or publishing permissions."}

- {rows.map(row =>
+
+

{zh ? "飞书 · 项目助手与管家" : "Lark · Project assistants and steward"}

+

{zh ? "每个 App 单独核验本人。项目助手默认支持工作区读写,按项目规则与 skills 执行编辑;可选只读。普通聊天不创建隐藏 Goal。本人管家默认读取本机已注册的 Goal 与 Agent;没有注册工作时显示空态。旧连接保留原范围,需明确升级授权。交办不提升执行或发布权限。" : "Verify the owner independently for each App. Project Chat defaults to workspace writes under project rules and skills; read-only remains available without hidden Goals. A personal steward defaults to the registered local Goals and Agents, including an honest empty state. Existing connections retain their scope until explicitly upgraded. Handoff does not elevate execution or publishing permissions."}

+ +

{zh ? "发送文字、图片或图文消息开始;后续消息进入原会话队列。/status 查看工作区、角色与持久排队状态,/help 查看用法与解绑入口,/stop 停止当前聊天执行,/new 开启新会话。文件、音视频、附在控制命令或已选择 Agent 上的图片会明确提示暂不支持。" : "Send text, images or image/text posts to begin; follow-ups queue in the same Session. /status shows the workspace, role and durable queue, /help explains commands and where to unbind, /stop stops the current Chat Turn, /new starts a new conversation. Files, audio/video, and images sent with control commands or to a selected attached Agent receive an explicit unsupported response."}

+ {audience === "owner" ?

{zh ? "管家新委托:/delegate --tokens N 具体目标。先读预览,再用原私聊的完整 /confirm 命令确认;15 分钟过期。原生执行保持只读,总 token 上限可能被运行中的请求超过;没有默认定时调度。回执提供 /stop-commission 停止和 /resume-commission 恢复命令;恢复保留原线程及累计用量。" : "Steward commission: /delegate --tokens N objective. Read the preview, then use its full /confirm command in the original private Chat within 15 minutes. Native execution remains read-only; in-flight requests can exceed the total token allowance. No default schedule. Receipts provide /stop-commission and /resume-commission commands; recovery retains the original thread and cumulative usage."}

: null} +
+ +
+ {rows.map(row =>
+
{row.app_ref} · {row.context_available ? row.project_title : (zh ? "工作区不可用" : "Workspace unavailable")} {onOpenOriginal && row.audience === "owner" ? : null} +

{row.context_kind === "steward" ? (row.goal_scope === "all_registered" ? (zh ? `LoopX 管家 · 全部已注册工作 · ${row.goal_count} 个 Goal` : `LoopX steward · All registered work · ${row.goal_count} Goals`) : (zh ? `LoopX 管家 · 已选范围 · ${row.goal_count} 个 Goal` : `LoopX steward · Selected scope · ${row.goal_count} Goals`)) : (row.grant === "workspace_write" ? (zh ? "普通项目助手 · 已授权工作区读写" : "Project assistant · Workspace writes authorized") : (zh ? "普通项目助手 · 只读对话" : "Project assistant · Read-only Chat"))}

@@ -168,8 +178,10 @@ export function PrivateConversationPanel({apps, onOpenOriginal}: {apps: LarkApp[ onPrepare={() => void act(() => prepareManagedSteward(row.binding_id))} onMonitor={(operation, cadence) => void act(() => configureStewardMonitor(row.binding_id, operation, cadence))} onReport={(operation, schedule) => void act(() => configureStewardReport(row.binding_id, operation, schedule))}/> : null} +
{row.context_kind === "steward" && row.goal_scope !== "all_registered" ? : null} +
)} {recoveries.map(row =>
{row.project_title} · {zh ? "旧授权" : "Previous grant"} {row.binding_id.slice(-6)} @@ -179,47 +191,52 @@ export function PrivateConversationPanel({apps, onOpenOriginal}: {apps: LarkApp[ {rows.length === 0 ?

{connectionsLoading ? (zh ? "正在读取已有连接…" : "Reading existing connections…") : error ? (zh ? "连接读取暂不可用,请刷新重试。" : "Connections unavailable. Refresh to retry.") : (zh ? "尚未连接助手。" : "No assistant connected.")}

: null} - - - {audience === "group" ?
- {zh ? "允许使用的群(最多 16 个)" : "Authorized groups (up to 16)"} - {loadingGroups ?

{zh ? "正在读取此 App 的群" : "Reading this App's groups"}

: groups.map(chat => )} - {!loadingGroups && !groups.length && !groupError ?

{zh ? "此 App 尚无可见群。先将 Bot 加入调试群,再刷新。" : "This App has no visible groups. Add the Bot to a trial group, then refresh."}

: null} - {groupError ?

{groupError}

: null} - -

{zh ? "新话题需 @此 Bot;回复留在原话题。工作区、历史和工具保持隔离,模型认证可复用可信宿主账号。连接成功不代表已通过公开群准出。" : "Mention this Bot to start; replies stay in the original topic. Workspace, history and tools remain isolated; model authentication can use the trusted host account. Connection is not public release qualification."}

-
: null} - - - - {role === "project" ? : null} - {role === "project" && effectiveProjectGrant === "workspace_write" ?

{zh ? "此 App 可按你的指令编辑选定工作区;不提高已直连 Agent 的原宿主权限。更改权限会建立新绑定和新会话,旧会话不会自动提升权限,已有 Agent 直连需重新授权。" : "This App may edit the selected workspace on your instruction; attached Agents retain their original host permissions. Changing access creates a new binding and Session. Existing Chat does not gain access and Agent targets require new authorization."}

: null} -
-
-

{zh ? "发送文字、图片或图文消息开始;后续消息进入原会话队列。/status 查看工作区、角色与持久排队状态,/help 查看用法与解绑入口,/stop 停止当前聊天执行,/new 开启新会话。文件、音视频、附在控制命令或已选择 Agent 上的图片会明确提示暂不支持。" : "Send text, images or image/text posts to begin; follow-ups queue in the same Session. /status shows the workspace, role and durable queue, /help explains commands and where to unbind, /stop stops the current Chat Turn, /new starts a new conversation. Files, audio/video, and images sent with control commands or to a selected attached Agent receive an explicit unsupported response."}

- {audience === "owner" ?

{zh ? "管家新委托:/delegate --tokens N 具体目标。先读预览,再用原私聊的完整 /confirm 命令确认;15 分钟过期。原生执行保持只读,总 token 上限可能被运行中的请求超过;没有默认定时调度。回执提供 /stop-commission 停止和 /resume-commission 恢复命令;恢复保留原线程及累计用量。" : "Steward commission: /delegate --tokens N objective. Read the preview, then use its full /confirm command in the original private Chat within 15 minutes. Native execution remains read-only; in-flight requests can exceed the total token allowance. No default schedule. Receipts provide /stop-commission and /resume-commission commands; recovery retains the original thread and cumulative usage."}

: null} +
+ {zh ? "连接助手" : "Connect an assistant"} +
+
+ + + {audience === "group" ?
+ {zh ? "允许使用的群(最多 16 个)" : "Authorized groups (up to 16)"} + {loadingGroups ?

{zh ? "正在读取此 App 的群" : "Reading this App's groups"}

: groups.map(chat => )} + {!loadingGroups && !groups.length && !groupError ?

{zh ? "此 App 尚无可见群。先将 Bot 加入调试群,再刷新。" : "This App has no visible groups. Add the Bot to a trial group, then refresh."}

: null} + {groupError ?

{groupError}

: null} + +

{zh ? "新话题需 @此 Bot;回复留在原话题。工作区、历史和工具保持隔离,模型认证可复用可信宿主账号。连接成功不代表已通过公开群准出。" : "Mention this Bot to start; replies stay in the original topic. Workspace, history and tools remain isolated; model authentication can use the trusted host account. Connection is not public release qualification."}

+
: null} + + + + {role === "project" ? : null} +
+ {role === "project" && effectiveProjectGrant === "workspace_write" ?

{zh ? "此 App 可按你的指令编辑选定工作区;不提高已直连 Agent 的原宿主权限。更改权限会建立新绑定和新会话,旧会话不会自动提升权限,已有 Agent 直连需重新授权。" : "This App may edit the selected workspace on your instruction; attached Agents retain their original host permissions. Changing access creates a new binding and Session. Existing Chat does not gain access and Agent targets require new authorization."}

: null} +
+
+
+
{error ?

{error}

: null}
; } diff --git a/apps/presentation/dashboard/src/features/personal-workspace/private-conversation.css b/apps/presentation/dashboard/src/features/personal-workspace/private-conversation.css index 9d5fa81613..fee906aa1e 100644 --- a/apps/presentation/dashboard/src/features/personal-workspace/private-conversation.css +++ b/apps/presentation/dashboard/src/features/personal-workspace/private-conversation.css @@ -1,5 +1,19 @@ .personal-private-conversation { display: grid; gap: 18px; } -.personal-private-conversation > p { margin: 0; } +.personal-private-conversation.personal-detail-card p { font-size: var(--text-body); line-height: var(--leading-body); } +.personal-private-conversation > p, .personal-private-connection p, .personal-private-setup-body > p { margin: 0; } +.personal-private-heading { display: flex; align-items: center; flex-wrap: wrap; gap: 8px; } +.personal-private-heading h3 { flex: 1; margin: 0; } +.personal-private-heading strong { flex: 1; min-width: 0; overflow-wrap: anywhere; } +.personal-private-connection { display: grid; gap: 10px; padding-bottom: 18px; border-bottom: 1px solid var(--pw-line); } +.personal-private-setup > summary { min-height: 44px; align-content: center; cursor: pointer; font-size: var(--text-body); font-weight: 600; } +.personal-private-setup > summary:focus-visible { outline: 2px solid var(--pw-blue); outline-offset: 2px; } +.personal-private-setup-body { display: grid; gap: 16px; margin-top: 12px; } +.personal-private-fields { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 18px; } +.personal-private-groups { display: grid; grid-column: 1 / -1; gap: 12px; margin: 0; padding: 16px; border: 1px solid var(--pw-line-strong); border-radius: 10px; } +@media (max-width: 640px) { + .personal-private-fields { grid-template-columns: minmax(0, 1fr); } + .personal-private-connection > .personal-private-heading { display: grid; grid-template-columns: minmax(0, 1fr); justify-items: start; } +} .personal-private-conversation label { display: grid; gap: 8px; font-size: var(--text-body); line-height: var(--leading-body); } .personal-private-conversation select { width: 100%; min-height: 40px; padding: 10px 12px; @@ -18,3 +32,5 @@ .personal-private-conversation form button[type="submit"] { background: var(--pw-text); color: var(--pw-card); } .personal-private-conversation [role="alert"] { color: var(--pw-red); } .personal-private-conversation [role="status"]:empty { display: none; } +.personal-private-conversation .personal-private-refresh { display: inline-flex; align-items: center; justify-content: center; width: 32px; min-height: 32px; padding: 0; border: 0; background: transparent; color: var(--pw-muted); } +@media (pointer: coarse), (max-width: 640px) { .personal-private-conversation .personal-private-refresh { width: 44px; min-height: 44px; } } diff --git a/examples/personal-workspace-browser/evidence/lark-conversation-layout/after-desktop.png b/examples/personal-workspace-browser/evidence/lark-conversation-layout/after-desktop.png new file mode 100644 index 0000000000..ac45c9ea3d Binary files /dev/null and b/examples/personal-workspace-browser/evidence/lark-conversation-layout/after-desktop.png differ diff --git a/examples/personal-workspace-browser/evidence/lark-conversation-layout/after-narrow.png b/examples/personal-workspace-browser/evidence/lark-conversation-layout/after-narrow.png new file mode 100644 index 0000000000..00108c01ac Binary files /dev/null and b/examples/personal-workspace-browser/evidence/lark-conversation-layout/after-narrow.png differ diff --git a/examples/personal-workspace-browser/evidence/lark-conversation-layout/before-desktop.png b/examples/personal-workspace-browser/evidence/lark-conversation-layout/before-desktop.png new file mode 100644 index 0000000000..7d31e587f2 Binary files /dev/null and b/examples/personal-workspace-browser/evidence/lark-conversation-layout/before-desktop.png differ diff --git a/examples/personal-workspace-browser/evidence/lark-conversation-layout/before-narrow.png b/examples/personal-workspace-browser/evidence/lark-conversation-layout/before-narrow.png new file mode 100644 index 0000000000..69ede5216f Binary files /dev/null and b/examples/personal-workspace-browser/evidence/lark-conversation-layout/before-narrow.png differ diff --git a/examples/personal-workspace-browser/evidence/lark-conversation-layout/commands-narrow.png b/examples/personal-workspace-browser/evidence/lark-conversation-layout/commands-narrow.png new file mode 100644 index 0000000000..c465a7dfd0 Binary files /dev/null and b/examples/personal-workspace-browser/evidence/lark-conversation-layout/commands-narrow.png differ diff --git a/examples/personal-workspace-browser/evidence/lark-conversation-layout/gap-desktop.png b/examples/personal-workspace-browser/evidence/lark-conversation-layout/gap-desktop.png new file mode 100644 index 0000000000..01589869dc Binary files /dev/null and b/examples/personal-workspace-browser/evidence/lark-conversation-layout/gap-desktop.png differ diff --git a/examples/personal-workspace-browser/evidence/lark-conversation-layout/mixed-desktop.png b/examples/personal-workspace-browser/evidence/lark-conversation-layout/mixed-desktop.png new file mode 100644 index 0000000000..581a10ff14 Binary files /dev/null and b/examples/personal-workspace-browser/evidence/lark-conversation-layout/mixed-desktop.png differ diff --git a/examples/personal-workspace-browser/evidence/lark-conversation-layout/mixed-narrow.png b/examples/personal-workspace-browser/evidence/lark-conversation-layout/mixed-narrow.png new file mode 100644 index 0000000000..699186c60f Binary files /dev/null and b/examples/personal-workspace-browser/evidence/lark-conversation-layout/mixed-narrow.png differ diff --git a/examples/personal-workspace-browser/evidence/lark-conversation-layout/setup-desktop.png b/examples/personal-workspace-browser/evidence/lark-conversation-layout/setup-desktop.png new file mode 100644 index 0000000000..2cb1aa4c39 Binary files /dev/null and b/examples/personal-workspace-browser/evidence/lark-conversation-layout/setup-desktop.png differ diff --git a/examples/personal-workspace-browser/evidence/lark-conversation-layout/setup-narrow.png b/examples/personal-workspace-browser/evidence/lark-conversation-layout/setup-narrow.png new file mode 100644 index 0000000000..3df09e8aea Binary files /dev/null and b/examples/personal-workspace-browser/evidence/lark-conversation-layout/setup-narrow.png differ diff --git a/examples/personal-workspace-browser/private-steward-scope.mjs b/examples/personal-workspace-browser/private-steward-scope.mjs index 43dc328f5c..fe5a85e44b 100644 --- a/examples/personal-workspace-browser/private-steward-scope.mjs +++ b/examples/personal-workspace-browser/private-steward-scope.mjs @@ -6,7 +6,7 @@ export const privateStewardScopeScenario = { id: "private-steward-scope", async run({browser, collectCoverage, url}) { const writes = []; - let projectsAvailable = true; + let projectsAvailable = true, hasConnection = true, extraRows = []; const row = {binding_id: "synthetic-steward", app_ref: "mew", context_kind: "steward", project_ref: "synthetic-workspace", project_title: "Personal workspace", context_available: true, executor_endpoint_id: "codex", grant: "portfolio_read", goal_count: 1, goal_scope: "selected", @@ -29,7 +29,7 @@ export const privateStewardScopeScenario = { } Object.assign(row, {goal_scope: "all_registered", goal_count: 150}); } - await route.fulfill({json: {ok: true, revision: writes.length + 1, connections: [row]}}); + await route.fulfill({json: {ok: true, revision: writes.length + 1, connections: hasConnection ? [row, ...extraRows] : []}}); }); }, }); @@ -49,6 +49,41 @@ export const privateStewardScopeScenario = { await settings(); const panel = page.getByRole("region", {name: "飞书项目助手与管家"}); await panel.getByText("LoopX 管家 · 已选范围 · 1 个 Goal", {exact: true}).waitFor(); + await page.screenshot({path: resolve(outputDir, "lark-connections-default-desktop.png"), animations: "disabled"}); + await page.setViewportSize({width: 390, height: 844}); + await page.screenshot({path: resolve(outputDir, "lark-connections-default-narrow.png"), animations: "disabled"}); + await page.setViewportSize({width: 1512, height: 982}); + const setup = panel.locator(".personal-private-setup"); + const appSelect = panel.getByRole("combobox", {name: "助手 App", exact: true}); + const commands = panel.getByRole("button", {name: "聊天与委托命令", exact: true}); + const commandHelp = panel.getByRole("tooltip").filter({hasText: "聊天与委托命令"}); + if (await appSelect.isVisible() || await commandHelp.isVisible()) throw new Error("Existing connections are buried under optional setup or command instructions"); + await commands.focus(); + await commandHelp.getByText(/后续消息进入原会话队列/u).waitFor(); + await commandHelp.getByText(/15 分钟过期/u).waitFor(); + await page.screenshot({path: resolve(outputDir, "lark-conversation-help-desktop.png"), animations: "disabled"}); + await page.keyboard.press("Escape"); + if (!await commands.evaluate(node => node === document.activeElement) || await commandHelp.isVisible() || writes.length) throw new Error("Closing command help lost focus or performed a write"); + await setup.locator("summary").focus(); + await page.keyboard.press("Enter"); + await appSelect.waitFor(); + const audienceSelect = panel.getByRole("combobox", {name: "使用位置", exact: true}); + const appBounds = await appSelect.boundingBox(), audienceBounds = await audienceSelect.boundingBox(); + if (!appBounds || !audienceBounds || Math.abs(appBounds.y - audienceBounds.y) > 2) throw new Error("Desktop setup did not group related fields into columns"); + await panel.getByRole("status").filter({hasText: "更改权限会建立新绑定和新会话"}).waitFor(); + await page.screenshot({path: resolve(outputDir, "lark-conversation-setup-desktop.png"), fullPage: true, animations: "disabled"}); + await page.setViewportSize({width: 390, height: 844}); + await appSelect.scrollIntoViewIfNeeded(); + const narrowApp = await appSelect.boundingBox(), narrowAudience = await audienceSelect.boundingBox(); + if (!narrowApp || !narrowAudience || narrowAudience.y <= narrowApp.y || await page.evaluate(() => document.documentElement.scrollWidth > innerWidth)) throw new Error("Narrow setup did not remain in a readable single column"); + await panel.getByRole("status").filter({hasText: "更改权限会建立新绑定和新会话"}).scrollIntoViewIfNeeded(); + await page.screenshot({path: resolve(outputDir, "lark-conversation-setup-narrow.png"), fullPage: true, animations: "disabled"}); + await commands.click(); + await commandHelp.getByText(/恢复保留原线程及累计用量/u).waitFor(); + await page.screenshot({path: resolve(outputDir, "lark-conversation-help-narrow.png"), animations: "disabled"}); + await page.keyboard.press("Escape"); + await setup.locator("summary").click(); + await page.setViewportSize({width: 1512, height: 982}); const upgrade = panel.getByRole("button", {name: "授权全部已注册工作", exact: true}); await upgrade.scrollIntoViewIfNeeded(); await page.screenshot({path: resolve(outputDir, "private-steward-scope-before.png"), animations: "disabled"}); @@ -57,6 +92,10 @@ export const privateStewardScopeScenario = { await panel.getByRole("alert").filter({hasText: "Scope publication failed; retry the upgrade."}).waitFor(); await panel.getByText("LoopX 管家 · 已选范围 · 1 个 Goal", {exact: true}).waitFor(); if (await upgrade.isDisabled()) throw new Error("Failed upgrade must allow an explicit retry"); + await panel.getByRole("button", {name: "刷新状态", exact: true}).click(); + await panel.getByText("LoopX 管家 · 已选范围 · 1 个 Goal", {exact: true}).waitFor(); + if (await appSelect.isVisible() || writes.length !== 1) throw new Error("Refreshing a failed operation requires hidden setup or replays the write"); + await upgrade.click({trial: true}); await upgrade.focus(); await page.keyboard.press("Enter"); await panel.getByText("LoopX 管家 · 全部已注册工作 · 150 个 Goal", {exact: true}).waitFor(); @@ -102,9 +141,34 @@ export const privateStewardScopeScenario = { await reloadSettings(); await panel.getByRole("option", {name: "Personal workspace", exact: true}).waitFor({state: "attached"}); if (await panel.getByRole("alert").count()) throw new Error("Recovered workspace catalog retained a stale failure"); + // Long titles and multiple connections expose composition failures that + // a quiet single-row fixture cannot. Readback never retries their work. + row.owner_attention = {available: true, enabled: true, revision: 1, last_checked_at: Date.now() / 1000, + error_code: "source_incomplete", source_coverage: {goal_count: 150, read_goal_count: 147, unavailable_goal_count: 3}, + pending_count: 2, unconfirmed_count: 1, failed_count: 0}; + extraRows = [{...row, binding_id: "synthetic-long-steward", app_ref: "synthetic-other-app", + project_title: "长期研究工作区 · Extended research workspace · 原始证据与跨会话接续记录 ".repeat(4), + context_available: true, pending_count: 3, recovery_count: 1, listener_status: "retrying"}, + {...row, binding_id: "synthetic-unavailable-steward", app_ref: "synthetic-unavailable-app", + context_available: false, owner_attention: {available: false, error_code: "context_unavailable"}}]; + await page.setViewportSize({width: 1512, height: 982}); + await reloadSettings(); + await panel.getByText("3 个 Goal 暂时不可读;其余 147 个继续自动检查。", {exact: true}).first().waitFor(); + await panel.getByRole("alert").filter({hasText: "工作区授权已失效"}).waitFor(); + await page.screenshot({path: resolve(outputDir, "lark-conversation-mixed-desktop.png"), animations: "disabled"}); + await page.setViewportSize({width: 390, height: 844}); + await panel.getByText(`${extraRows[0].app_ref} · ${extraRows[0].project_title}`, {exact: true}).scrollIntoViewIfNeeded(); + await page.screenshot({path: resolve(outputDir, "lark-conversation-mixed-narrow.png"), animations: "disabled"}); + if (await page.evaluate(() => document.documentElement.scrollWidth > innerWidth) || writes.length !== 2 || await appSelect.isVisible()) throw new Error("Long mixed connections overflowed, replayed a write, or expanded optional setup"); + hasConnection = false; + await reloadSettings(); + await panel.getByText("尚未连接助手。", {exact: true}).waitFor(); + await appSelect.waitFor(); + if (!await setup.evaluate(node => node.open) || writes.length !== 2) throw new Error("An empty connection list must expose setup without creating a connection"); + await page.screenshot({path: resolve(outputDir, "lark-conversation-empty-narrow.png"), fullPage: true, animations: "disabled"}); const expectedErrors = context.errors.filter(error => error === "Failed to load resource: the server responded with a status of 500 (Internal Server Error)"); if (expectedErrors.length !== 1 || context.errors.length !== 1) throw new Error(context.errors.join(" | ")); - return {coverageEntries: await context.close(), note: "Failed scope publication retains selected scope and allows keyboard retry; success clears the failure and survives reload; desktop/mobile, synthetic API paired with native backend fault regressions"}; + return {coverageEntries: await context.close(), note: "Failed scope publication retains selected scope and allows keyboard retry; success clears the failure and survives reload; existing connections remain primary, setup is keyboard-disclosed and opens for an empty list, full command help works with focus/tap/Escape; desktop/mobile, synthetic API paired with native backend fault regressions"}; } catch (error) {await context.close(); throw error;} }, }; diff --git a/examples/personal-workspace-browser/progressive-loading.mjs b/examples/personal-workspace-browser/progressive-loading.mjs index 0ec62c74b5..b8adfe7d83 100644 --- a/examples/personal-workspace-browser/progressive-loading.mjs +++ b/examples/personal-workspace-browser/progressive-loading.mjs @@ -104,7 +104,13 @@ async function verifyLarkLoading(browser, url, collectCoverage) { await verifyCompactHelp(emptyExplanation); await page.keyboard.press("Escape"); await page.locator(".personal-lark-tabs").getByRole("button", {name: /^连接/u}).click(); - await page.getByRole("button", {name: "暂无 Goal 连接", exact: true}).click(); + const emptyConnections = page.getByRole("button", {name: "暂无 Goal 连接", exact: true}); + if (await emptyConnections.evaluate(node => Boolean(node.closest('[role="table"]')))) throw new Error("Empty connection help has no table-cell accessibility owner"); + if (await page.getByRole("table").count()) throw new Error("An empty directory must not expose an empty data table"); + await emptyConnections.focus(); + await page.getByRole("tooltip").filter({hasText: "暂无 Goal 连接"}).waitFor(); + await page.keyboard.press("Escape"); + await emptyConnections.click(); const connectionExplanation = page.getByRole("tooltip").filter({hasText: "暂无 Goal 连接"}); await connectionExplanation.waitFor(); await verifyCompactHelp(connectionExplanation); diff --git a/examples/personal-workspace-browser/steward-group-trigger.mjs b/examples/personal-workspace-browser/steward-group-trigger.mjs index 573d956ec3..7d654227af 100644 --- a/examples/personal-workspace-browser/steward-group-trigger.mjs +++ b/examples/personal-workspace-browser/steward-group-trigger.mjs @@ -23,6 +23,7 @@ export const stewardGroupTriggerScenario = { await dialog.getByRole("button", {name: "连接", exact: true}).click(); await dialog.waitFor({state: "hidden"}); const row = page.locator(".personal-lark-table-row", {hasText: "Product group"}); + if (await page.getByRole("columnheader").count() !== 5 || await row.getByRole("cell").count() !== 5) throw new Error("Populated Goal connections lack accessible table cells and headers"); await row.getByText("群成员直接发消息,无需 @", {exact: true}).waitFor(); await row.getByText("监听已连接,尚无新消息验证这条连接。请在群里直接发一条任务,无需 @;若没有收到事件,再检查 im.message.receive_v1 和接收群内所有消息的权限。", {exact: true}).waitFor(); if (api.larkWrites[0]?.read_goal_scope !== "all_registered") throw new Error("Evidence scope not saved");