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
2 changes: 1 addition & 1 deletion apps/presentation/dashboard/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@
"smoke:task-inspector-route": "LOOPX_PLAYWRIGHT_PACKAGE=\"$PWD/node_modules/playwright\" node smoke/task-inspector-route-smoke.mjs",
"smoke:bound-conversation": "vite build --ssr smoke/bound-conversation-smoke.ts --outDir node_modules/.cache/loopx-bound-conversation --emptyOutDir && node node_modules/.cache/loopx-bound-conversation/bound-conversation-smoke.js",
"smoke:delegation-search": "LOOPX_PLAYWRIGHT_PACKAGE=\"$PWD/node_modules/playwright\" node smoke/delegation-search-browser-smoke.mjs",
"smoke:workspace-command-palette": "node --experimental-strip-types --test src/features/personal-workspace/workspace-command-entries.test.mjs && node --experimental-strip-types smoke/workspace-command-palette-browser-smoke.mjs"
"smoke:workspace-command-palette": "node --experimental-strip-types --test src/features/personal-workspace/workspace-command-entries.test.mjs && node --experimental-strip-types smoke/workspace-command-palette-browser-smoke.mjs && node --experimental-strip-types smoke/workspace-navigation-browser-smoke.mjs"
},
"dependencies": {
"@fontsource-variable/geist": "^5.3.0",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# Workspace navigation reading evidence

Synthetic populated workspace, including 30 Goals and a linked read-only archive.
The packaged Chat bundle is served by the existing production Chat entrypoint;
API discovery/permission descriptors are synthetic. These captures contain no
private operating state and do not establish an installed App result.

The baseline is `c10d8c2b4a20747e6eb77a188fd8e870b3be19de`. It reproduces the
missing host Settings entry while viewing the archived Goal. The after views
use the accompanying source and the same fixture. Desktop is 1440×900, low
window 1280×540, and narrow 390×640. Narrow captures show the opened sidebar.
The default footer now has Settings and a compact search icon on one row.
The independent Goal list scrolls above it; update status retains its own row.
Restriction views show an explicit Goal target and its original read-only
boundary, without local management forms. Repeated matching target/source
labels are shown once. Scope and source remain available.

Reproduce after a Chat build with `npm run smoke:workspace-command-palette` in
the dashboard package and `LOOPX_PERSONAL_WORKSPACE_PACKAGED=1`. The navigation
scenario covers EN/ZH, low/narrow windows, real browser touch events, keyboard,
IME-shaped events, focus, outside dismissal, internal selection/drag/scroll,
no click-through and zero write requests. It does not certify native macOS IME
or touch hardware. `LOOPX_NAVIGATION_CAPTURE_BASE=1` is only the baseline
capture/reproduction mode; it does not run the new behavior assertions.

Whole-page judgment: the stable lower row makes Settings discoverable while
reading the archive and removes a competing navigation card. Actual values,
required permissions, action outcomes and recovery remain direct. Coverage,
missing-observation and methodology explanations use contextual glyphs, with
complete hover, focus and tap readback. The empty/partial Overview no longer
stacks duplicate disclaimers. Help uses body text and stays within the viewport.
Research captures use the real read-only HTTP owner with synthetic measurements
and long analysis; judgments, unknown values and original citations are retained.
The unanalysed-scope glyph sits beside the result heading. The expanded phone
view shows the original full rationale and evidence. This is whole-page visual
inspection and interaction evidence, not a human comprehension study or a
qualification of every setting, platform or native input method.

The glyph and research phone captures are 390×844. The research desktop is
1280×900. Reproduce research views with the packaged `research-history` browser
scenario. Both before and after captures are synthetic; archived source names
are fixture labels, not a claim that benchmark work was executed.
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
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
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
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,10 @@ try {
await page.keyboard.press('Tab');
assert.equal(await dialogFor(page).evaluate(element => element.contains(document.activeElement)), true);
}
if (await page.locator('.context-help-popover:popover-open').count()) {
await page.keyboard.press('Escape');
assert.equal(await dialogFor(page).isVisible(), true, 'Tab-focused help closes before the palette');
}
await page.keyboard.press('Escape');
await page.locator('.personal-mobile-menu').click();
await launcher.click();
Expand Down Expand Up @@ -147,8 +151,13 @@ try {
}});
try {
await remote.page.locator('.personal-goal-row').first().locator('.personal-goal-link').click();
assert.equal(await remote.page.getByRole('button', {name:/^(Settings|设置)$/}).count(), 0, 'a read-only host does not expose local management');
await openPalette(remote.page);
assert.match(await dialogFor(remote.page).innerText(), /Read-only source|只读来源/);
const remoteHelp = dialogFor(remote.page).getByRole('button', {name:/Navigation help|导航说明/});
await remoteHelp.focus();
assert.match(await remote.page.locator('.context-help-popover:popover-open').innerText(), /Read-only source|只读来源/);
await remote.page.keyboard.press('Escape');
assert.equal(await dialogFor(remote.page).isVisible(), true, 'Help closes before the surrounding palette');
assert.equal(await dialogFor(remote.page).getByRole('option', {name: /Review preview|查看预览/}).count(), 0);
} finally {await remote.close();}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
// Packaged workspace navigation with synthetic source/host descriptors.
// No model, source mutation, setting apply or session start.
import assert from 'node:assert/strict';
import {mkdir} from 'node:fs/promises';
import {resolve} from 'node:path';
import {startServer, port, outputDir, packaged} from '../../../../examples/personal-workspace-browser/fixture.mjs';
import {launchBrowser, loadPlaywright, waitForHttp} from '../../../../examples/dashboard-browser-smoke-support.mjs';
import {openWorkspacePage} from '../../../../examples/personal-workspace-browser/scenario-context.mjs';
import {installShowcaseWorkspace} from '../../../../examples/personal-workspace-browser/showcase-workspace.mjs';

const baseline = process.env.LOOPX_NAVIGATION_CAPTURE_BASE === '1';
const stage = baseline ? 'before' : 'after';
const server = await startServer();
let browser;
const url = `http://127.0.0.1:${port}/${packaged ? 'chat/' : ''}?statusUrl=/status.json`;
const dialog = page => page.locator('dialog.workspace-command-palette');
try {
await waitForHttp(url); await mkdir(outputDir, {recursive:true});
browser = await launchBrowser(loadPlaywright().chromium);
for (const locale of ['zh-CN', 'en']) {
const session = await openWorkspacePage({newPage: options => browser.newPage({...options, hasTouch:true})}, url, {beforeGoto: async (api, page) => {
await page.addInitScript(value => localStorage.setItem('loopx-pw-locale', value), locale);
await installShowcaseWorkspace(api, page, {done:23, cost:24.8});
const transform = api.transformStatus;
api.transformStatus = data => {
const result = transform(data) ?? data;
const goals = result.run_history.goals, original = goals.find(goal => goal.id === 'progress-projection');
goals.push({...structuredClone(original), id:'readonly-archive', display_name:'Graph · archived experiment',
source_access:{namespace:'a'.repeat(64),label:'Graph · archived experiment',read_only:true}});
for (let i=0; i<24; i++) goals.push({...structuredClone(original), id:`navigation-goal-${i}`,
display_name:`${i+1}. 核验长程结果与完整来源 Verify the complete export history`});
return result;
};
}});
const {page} = session;
try {
await page.locator('.personal-goal-link', {hasText:'Graph · archived experiment'}).click();
await page.getByRole('heading', {name:'Graph · archived experiment', exact:true}).waitFor();
const launch = page.getByRole('button', {name:locale==='en'?'Quick navigation':'快速导航',exact:true});
const settings = page.getByRole('button', {name:locale==='en'?'Settings':'设置',exact:true});
const writes=[]; page.on('request', r => {if (!['GET','HEAD','OPTIONS'].includes(r.method())) writes.push(new URL(r.url()).pathname);});
for (const [name,width,height] of [['desktop',1440,900],['low',1280,540],['narrow',390,640]]) {
await page.setViewportSize({width,height});
if (width===390) await page.locator('.personal-mobile-menu').click();
await page.locator('.personal-sidebar-nav').evaluate(e => {e.scrollTop=0;});
await page.mouse.move(width-3,3);
await page.screenshot({path:resolve(outputDir,`navigation-${stage}-${name}-${locale}.png`),animations:'disabled'});
if (!baseline) {
assert.equal(await settings.isVisible(),true);
const bounds=await settings.boundingBox(); assert.ok(bounds.y>=0 && bounds.y+bounds.height<=height);
const searchBounds=await launch.boundingBox(); assert.equal(searchBounds.width,44); assert.equal(searchBounds.height,44);
assert.equal(await launch.innerText(),'','no visible replacement label');
await page.locator('.personal-sidebar-nav').evaluate(e => {e.scrollTop=e.scrollHeight;});
assert.deepEqual(await settings.boundingBox(),bounds,'scrolling a long directory leaves settings in place');
await settings.click();
const panel=page.locator('.personal-settings-page');
await panel.waitFor();
assert.equal(await panel.getByRole('radio',{name:locale==='en'?'This device':'本机设置',exact:true}).isChecked(),true);
assert.equal(await panel.getByRole('combobox',{name:locale==='en'?'Target Goal':'目标 Goal',exact:true}).count(),0);
await panel.getByRole('radio',{name:locale==='en'?'One Goal':'Goal 设置',exact:true}).check();
await panel.getByRole('combobox',{name:locale==='en'?'Target Goal':'目标 Goal',exact:true}).selectOption('readonly-archive');
await panel.locator('.personal-settings-unavailable').waitFor();
assert.equal(await panel.locator('.personal-capability-apply').count(),0);
await page.screenshot({path:resolve(outputDir,`navigation-${stage}-restriction-${name}-${locale}.png`),animations:'disabled'});
await panel.locator('.personal-settings-back').click();
await panel.waitFor({state:'hidden'});
} else assert.equal(await settings.count(),0,'baseline reproduces host settings hidden by archived Goal');
if (width===390 && await page.locator('.personal-workspace-shell.mobile-sidebar-open').count()) await page.keyboard.press('Escape');
}
if (baseline) continue;
// Empty/partial snapshots keep their truth in contextual glyphs. Reading
// them cannot expand a parent disclosure, grant access or start work.
for (const [name,width,height] of [['desktop',1440,900],['narrow',390,844]]) {
await page.setViewportSize({width,height});
const now = page.locator('.goal-now');
const workHelp = now.getByRole('button',{name:locale==='en'?'Work coverage unconfirmed':'工作覆盖未确认',exact:true});
const acceptance = page.locator('.goal-observation-details');
const acceptanceHelp = acceptance.getByRole('button',{name:locale==='en'?'Acceptance observations unavailable':'验收观察未获取',exact:true});
await workHelp.waitFor();
assert.doesNotMatch(await now.innerText(), /当前快照|coverage is incomplete|没有可列出/,'No duplicate scope or empty-list paragraphs in the default view');
assert.equal(await acceptance.evaluate(e=>e.open),false);
await page.mouse.move(width-2,2);
await page.screenshot({path:resolve(outputDir,`glyph-default-${name}-${locale}.png`),animations:'disabled'});
await workHelp.hover();
const bubble=page.locator('.context-help-popover:popover-open');
await bubble.waitFor();
assert.match(await bubble.innerText(),locale==='en'?/coverage is incomplete/:/覆盖范围不完整/);
assert.ok(await bubble.evaluate(e=>parseFloat(getComputedStyle(e).fontSize)>=14),'Optional help uses reading text');
await page.keyboard.press('Escape'); await bubble.waitFor({state:'hidden'});
await acceptanceHelp.focus(); await bubble.waitFor();
assert.match(await bubble.innerText(),locale==='en'?/unknown/:/仍未知/,'Missing observations never become successful acceptance');
await page.keyboard.press('Escape'); await bubble.waitFor({state:'hidden'});
assert.equal(await acceptanceHelp.evaluate(e=>document.activeElement===e),true,'Escape keeps the help trigger focused');
await acceptanceHelp.tap(); await bubble.waitFor();
assert.equal(await acceptance.evaluate(e=>e.open),false,'Tapping help does not toggle the surrounding evidence disclosure');
const bounds=await bubble.boundingBox();
assert.ok(bounds.x>=0 && bounds.x+bounds.width<=width && bounds.y>=0 && bounds.y+bounds.height<=height,'Complete help fits desktop and phone');
await page.screenshot({path:resolve(outputDir,`glyph-help-${name}-${locale}.png`),animations:'disabled'});
await page.locator('.personal-channel-title').click(); await bubble.waitFor({state:'hidden'});
}
await page.setViewportSize({width:1440,height:900});
await launch.focus();
await page.evaluate(() => {
window.navigationBackgroundClicks = 0;
document.addEventListener('click', event => {if (!event.target.closest('dialog.workspace-command-palette')) window.navigationBackgroundClicks++;});
});
for (const side of ['left','right','top','bottom']) {
await launch.click(); await dialog(page).waitFor();
const count=await page.evaluate(()=>window.navigationBackgroundClicks);
const b=await dialog(page).boundingBox();
const point={left:[b.x-5,b.y+30],right:[b.x+b.width+5,b.y+30],top:[b.x+30,b.y-5],bottom:[b.x+30,b.y+b.height+5]}[side];
await page.mouse.click(...point); await dialog(page).waitFor({state:'hidden'});
assert.equal(await page.evaluate(()=>window.navigationBackgroundClicks),count,'dismissal never clicks the underlying workspace');
assert.equal(await launch.evaluate(e=>e===document.activeElement),true);
}
await launch.click(); await dialog(page).waitFor();
const b=await dialog(page).boundingBox();
await page.mouse.click(b.x+4,b.y+4); assert.equal(await dialog(page).isVisible(),true,'internal whitespace stays open');
await page.mouse.move(b.x+4,b.y+4); await page.mouse.down(); await page.mouse.move(b.x-5,b.y+30); await page.mouse.up();
assert.equal(await dialog(page).isVisible(),true,'drag from inside does not activate backdrop');
const heading=await dialog(page).getByRole('heading').boundingBox();
await page.mouse.move(heading.x,heading.y+10); await page.mouse.down();
await page.mouse.move(heading.x+heading.width,heading.y+10); await page.mouse.up();
await dialog(page).getByRole('listbox').hover(); await page.mouse.wheel(0,180);
assert.equal(await dialog(page).isVisible(),true,'text selection and list scrolling stay inside');
const search=dialog(page).getByRole('combobox'); await search.fill('Graph');
await search.dispatchEvent('compositionstart'); await search.dispatchEvent('keydown',{key:'Enter',keyCode:229,isComposing:true});
assert.equal(await dialog(page).isVisible(),true); await search.dispatchEvent('compositionend');
await search.press('Escape'); await dialog(page).waitFor({state:'hidden'});
await launch.focus(); await page.keyboard.press('Enter'); await dialog(page).waitFor();
await page.keyboard.press('Control+k'); await dialog(page).waitFor({state:'hidden'});
// Touch uses the same completed outside press; the sidebar closes first.
await page.setViewportSize({width:390,height:640});
await page.locator('.personal-mobile-menu').click(); await launch.click(); await dialog(page).waitFor();
const mobile=await dialog(page).boundingBox();
await page.touchscreen.tap(mobile.x-5,mobile.y+20);
await dialog(page).waitFor({state:'hidden'});
assert.equal(await page.locator('.personal-mobile-menu').evaluate(e=>e===document.activeElement),true,'mobile dismissal returns to the visible menu');
assert.deepEqual(writes,[],'discovery, dismissal and readonly settings never mutate or start work');
assert.deepEqual(session.errors,[]);
} finally {await session.close();}
}
console.log(baseline?'Baseline captures reproduce missing settings.':'Workspace navigation: long directory, host/Goal isolation, desktop/low/390, EN/ZH, backdrop and focus passed.');
} finally {await browser?.close(); server.kill();}
Original file line number Diff line number Diff line change
Expand Up @@ -83,8 +83,8 @@ export function AnswerReportPage({sessionId, messageId, statusUrl}: {
{state.kind === "missing" ? <p role="alert">{zh ? "找不到这份答复。它可能已被清理,或链接有误。" : "This answer was not found. It may have been removed, or the link may be incorrect."}</p> : null}
{answer ? <>
<div className="answer-report-content"><MarkdownText text={answerText}/></div>
<footer><p>{zh ? "来自本机保存的对话消息;打开此页不会重新运行 Agent。" : "From a locally saved conversation message. Opening this page does not rerun the Agent."}</p>
<details><summary>{zh ? "来源与版本" : "Source and version"}</summary>
<footer>
<details><summary>{zh ? "来源与版本" : "Source and version"}</summary><p>{zh ? "来自本机保存的对话消息;打开此页不会重新运行 Agent。" : "From a locally saved conversation message. Opening this page does not rerun the Agent."}</p>
<dl><dt>{zh ? "消息" : "Message"}</dt><dd><code>{answer.message_id}</code></dd>
<dt>Session</dt><dd><code>{sessionId}</code></dd></dl>
{sourceQuestion?.text ? <p><strong>{zh ? "原问题:" : "Original question: "}</strong>{sourceQuestion.text}</p> : null}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import {ContextHelp} from "./context-help";
import { AlertTriangle, ShieldCheck, SlidersHorizontal } from "lucide-react";

import type { CapabilityConfigurationCatalog } from "../../data/chat";
Expand Down Expand Up @@ -45,8 +46,7 @@ export function CapabilityEditorStatus({ available, description, t }: Readonly<{
}>) {
if (available) return null;
return <section className="personal-capability-editor-status is-read-only">
<AlertTriangle aria-hidden size={18} />
<div><strong>{t("capabilities.readOnly")}</strong><p>{description}</p></div>
<div><strong>{t("capabilities.readOnly")}</strong><ContextHelp icon={AlertTriangle} label={t("capabilities.readOnly")}><p>{description}</p></ContextHelp></div>
</section>;
}

Expand Down
Loading
Loading