diff --git a/apps/presentation/dashboard/src/features/personal-workspace/attached-agent-conversations.css b/apps/presentation/dashboard/src/features/personal-workspace/attached-agent-conversations.css
index 69f81040e2..8bb9a1ccfa 100644
--- a/apps/presentation/dashboard/src/features/personal-workspace/attached-agent-conversations.css
+++ b/apps/presentation/dashboard/src/features/personal-workspace/attached-agent-conversations.css
@@ -10,7 +10,7 @@
.attached-conversation-content textarea {min-height: 88px; width: 100%; resize: vertical;}
.attached-conversation-content button {font: inherit; padding: 8px 12px; border: 1px solid var(--pw-line); border-radius: 6px; background: var(--pw-card); color: var(--pw-text); cursor: pointer;}
.attached-conversation-content button:disabled, .attached-conversation-content button[aria-disabled="true"] {opacity: .5; cursor: default;}
-.attached-conversation-content .attached-conversation-open {border: 0; padding: 0; border-radius: 0; background: transparent; text-decoration: underline; text-underline-offset: 3px;}
+.attached-conversation-content .attached-conversation-open {border: 0; padding: 0; border-radius: 0; background: transparent;}
.attached-session-selection {display: flex; align-items: center; gap: 12px; margin-block: 12px 4px; min-width: 0;}
.attached-session-selection > span {flex-shrink: 0;}
.attached-session-selection select {min-width: 0; max-width: 100%; min-height: 44px; font: inherit; padding: 8px; border: 1px solid var(--pw-line); border-radius: 6px; background: var(--pw-card); color: var(--pw-text);}
@@ -59,9 +59,10 @@
.goal-agent-work-page form button[type="submit"] {background: var(--pw-text); color: var(--pw-card); min-height: 44px;}
@media (max-width: 640px) {.agent-work-navigation {width: 100%;} .agent-work-navigation select {flex: 1; min-width: 0;} .goal-agent-work-page .attached-conversation-messages {min-height: 120px;}}
-.attached-connection-status {display: flex; flex-wrap: wrap; align-items: baseline; justify-content: space-between; gap: 8px 16px; padding-block: 8px;}
+.attached-connection-status {display: grid; grid-template-columns: minmax(0, 1fr) auto; align-items: center; gap: 8px 16px; padding-block: 8px; padding-inline-end: 4px;}
.attached-connection-status p {margin: 0;}
-.attached-connection-status .attached-conversation-open {min-height: 44px; flex-shrink: 0;}
+.attached-connection-status .attached-conversation-open {display: inline-flex; align-items: center; justify-content: center; min-width: 44px; min-height: 44px; border-radius: 6px; color: var(--pw-muted);}
+.attached-connection-status .attached-conversation-open:hover {background: var(--pw-hover); color: var(--pw-text);}
@media (max-width: 640px) {.goal-agent-work-page .attached-conversation-heading h2 {font-size: var(--text-lead);} .agent-work-navigation {width: auto; flex: 1;} .agent-work-navigation select {max-width: 140px;} .attached-conversation-heading button {padding-inline: 8px;}}
.attached-conversation-messages summary > strong {font-weight: 500;}
diff --git a/apps/presentation/dashboard/src/features/personal-workspace/attached-agent-conversations.tsx b/apps/presentation/dashboard/src/features/personal-workspace/attached-agent-conversations.tsx
index 21b3c0d726..c79f83af37 100644
--- a/apps/presentation/dashboard/src/features/personal-workspace/attached-agent-conversations.tsx
+++ b/apps/presentation/dashboard/src/features/personal-workspace/attached-agent-conversations.tsx
@@ -1,5 +1,5 @@
import {useEffect, useId, useLayoutEffect, useRef, useState} from "react";
-import {Keyboard} from "lucide-react";
+import {ExternalLink, Keyboard} from "lucide-react";
import {AttachedConversationTimeline, type ConversationRecord, type ContentPage} from "./attached-conversation-record";
import {composerKeyAction} from "./composer-keyboard";
import {ContextHelp} from "./context-help";
@@ -411,7 +411,8 @@ export function AttachedConversation({goalId, binding, zh, reconnect}: {
{recoveryError ?
{recoveryError}
: null}
{sendNotice ? {sendNotice}
: null}
Codex App
{zh ? "已保存消息与工具记录自动更新;实时内容来自原宿主。" : "Saved messages and tools update automatically; live content comes from the original host."}
{!snapshot ? (zh ? "正在连接或连接不可用" : "Connecting or unavailable") : snapshot.can_reply ? (zh ? "原会话可回复" : "Original conversation accepts replies") : snapshot.activity.state === "turn_open" ? (zh ? "原会话有未结束执行;可在原 App 中追加输入" : "The original conversation has an unfinished turn; add input in the original App") : (zh ? "历史可读;回复连接或执行状态尚未确认" : "History available; reply connection or execution state unconfirmed")}
-
+
{openRequested ? {zh ? "已请求打开原会话,请在 Codex App 中查看。" : "Requested opening the original conversation; check Codex App."}
: null}
{openError ? {openError}
: null}
{loadingOlder ? (zh ? "正在加载消息…" : "Loading messages…") : readingHistory ? {zh ? "正在回看历史" : "Reading history"}{unread ? (zh ? " · 有新活动" : " · new activity") : ""} : null}
diff --git a/examples/personal-workspace-browser/codex-attached.mjs b/examples/personal-workspace-browser/codex-attached.mjs
index 96fadec0c8..e7fed54250 100644
--- a/examples/personal-workspace-browser/codex-attached.mjs
+++ b/examples/personal-workspace-browser/codex-attached.mjs
@@ -3,6 +3,7 @@ import {spawn} from 'node:child_process';
import {once} from 'node:events';
import {resolve} from 'node:path';
import {createInterface} from 'node:readline';
+import {readFile} from 'node:fs/promises';
import {repoRoot, outputDir} from './fixture.mjs';
import {openWorkspacePage} from './scenario-context.mjs';
import {testPythonCommand} from '../../scripts/test-python.mjs';
@@ -556,20 +557,52 @@ async function runConversation(browser, uncertain) {
assert.equal(new Set(sentOperations).size,6);
assert.equal(await direct.getByRole('textbox',{name:'回复 worker',exact:true}).inputValue(),'');
await page.screenshot({path:resolve(outputDir,'attached-codex-owner-conversation.png'),animations:'disabled'});
+ // Keep the real display state intact through the production attached
+ // reader, HTTP detail pages and rendered tool record. Host execution is a
+ // disposable protocol peer, not the original benchmark/model conversation.
+ const benchmarkReference = JSON.parse(await readFile(resolve(repoRoot,
+ 'apps/presentation/dashboard/src/data/fixtures/edgebench-grid-context-on-20261103-history.json'), 'utf8'));
await page.request.post(new URL('/fixture/work-records',url).href);
await direct.getByText('工作结果已返回。',{exact:true}).waitFor();
const toolResult = direct.locator('article[data-role="tool"]').filter({hasText:'工具结果'});
await expandActivity(toolResult);
await toolResult.locator('summary').click();
- assert.equal(await toolResult.getByText('COMPLETE TOOL RESULT',{exact:false}).count(),0);
- await toolResult.getByRole('button',{name:'继续读取完整内容',exact:true}).click();
- await toolResult.getByText('COMPLETE TOOL RESULT',{exact:false}).waitFor();
+ const recordId = await toolResult.getAttribute('data-record-id');
+ let detailPages = 0;
+ while (await toolResult.getByRole('button',{name:'继续读取完整内容',exact:true}).count()) {
+ const before = await toolResult.locator('pre').textContent();
+ await toolResult.getByRole('button',{name:'继续读取完整内容',exact:true}).click();
+ await page.waitForFunction(({id, previous}) => {
+ const text = document.querySelector(`article[data-record-id="${CSS.escape(id)}"] pre`)?.textContent;
+ return text != null && text !== previous;
+ }, {id: recordId, previous: before});
+ await toolResult.getByRole('button',{name:'正在读取…',exact:true}).waitFor({state:'hidden'});
+ assert.ok(++detailPages <= 32, 'The bounded reference must finish reading');
+ }
+ assert.ok(detailPages > 0, 'The genuine reference crosses a detail-page boundary');
assert.equal(await toolResult.getByRole('button',{name:'继续读取完整内容',exact:true}).count(),0);
// Unicode detail offsets must count source characters, not JS UTF-16 units.
- assert.equal(await toolResult.locator('pre').textContent(),'Validation output\n' + '🧭验证结果\n'.repeat(6000) + 'COMPLETE TOOL RESULT');
+ const body = await toolResult.locator('pre').textContent();
+ assert.ok(body.startsWith('🧭\n'));
+ assert.deepEqual(JSON.parse(body.slice('🧭\n'.length)), benchmarkReference,
+ 'Every real observation, original wording, negative/unknown result and provenance survives native paging');
+ await page.screenshot({path:resolve(outputDir,'attached-codex-benchmark-detail-desktop.png'),animations:'disabled'});
+ await page.setViewportSize({width:390,height:844});
+ assert.ok(await page.locator('body').evaluate(node => node.scrollWidth <= innerWidth + 1));
+ await page.screenshot({path:resolve(outputDir,'attached-codex-benchmark-detail-narrow.png'),animations:'disabled'});
+ await page.setViewportSize({width:1512,height:982});
await toolResult.locator('summary').click();
await toolResult.locator('xpath=ancestor::details[contains(@class,"attached-record-group")]').locator(':scope > summary').click();
await page.screenshot({path:resolve(outputDir,'attached-codex-work-timeline.png'),animations:'disabled'});
+ await page.setViewportSize({width:390,height:844});
+ const connection = direct.locator('.attached-connection-status');
+ assert.ok(await connection.getByRole('status').isVisible(), 'The native reply or unknown state stays visible');
+ await connection.getByRole('button',{name:'在 Codex App 中打开',exact:true}).focus();
+ assert.ok(await connection.getByRole('button',{name:'在 Codex App 中打开',exact:true})
+ .evaluate(node => document.activeElement === node), 'The compact original-App action remains keyboard reachable');
+ assert.ok(await page.locator('body').evaluate(node => node.scrollWidth <= innerWidth + 1));
+ await page.screenshot({path:resolve(outputDir,'attached-codex-work-timeline-narrow.png'),animations:'disabled'});
+ await page.setViewportSize({width:1512,height:982});
await scrollHistory(page, direct, 1);
await direct.getByText('工作结果已返回。',{exact:true}).waitFor();
assert.equal(await direct.getByRole('button',{name:/^(读取更早的消息|较新记录|回到最新)$/}).count(),0,'History needs no pagination controls');
diff --git a/examples/personal-workspace-browser/codex_attached_fixture.py b/examples/personal-workspace-browser/codex_attached_fixture.py
index 07d782ab99..d6c934fa0b 100644
--- a/examples/personal-workspace-browser/codex_attached_fixture.py
+++ b/examples/personal-workspace-browser/codex_attached_fixture.py
@@ -118,12 +118,20 @@ def do_POST(self):
self._send_json({"ok": True})
return
if self.path == "/fixture/work-records":
+ # Genuine, already public-safe display data; only the host
+ # protocol and carrier messages are synthetic. Keep one
+ # non-BMP prefix to exercise character-based detail offsets.
+ reference = (
+ Path(__file__).resolve().parents[2]
+ / "apps/presentation/dashboard/src/data/fixtures"
+ / "edgebench-grid-context-on-20261103-history.json"
+ )
records = [
{
"type": "function_call",
- "name": "validate",
+ "name": "read_file",
"call_id": "work-1",
- "arguments": "Check the release artifact",
+ "arguments": json.dumps({"path": reference.name}),
},
{
"type": "function_call_output",
@@ -131,9 +139,7 @@ def do_POST(self):
"output": [
{
"type": "text",
- "text": "Validation output\n"
- + "🧭验证结果\n" * 6000
- + "COMPLETE TOOL RESULT",
+ "text": "🧭\n" + reference.read_text().strip(),
}
],
},
diff --git a/examples/personal-workspace-browser/codex_attached_peer.py b/examples/personal-workspace-browser/codex_attached_peer.py
index 2b9a4ff1b9..c4e76edf0d 100644
--- a/examples/personal-workspace-browser/codex_attached_peer.py
+++ b/examples/personal-workspace-browser/codex_attached_peer.py
@@ -124,6 +124,7 @@ def fixture(tmp_path, *, uncertain=False, goal_id="sample-goal"):
registry.write_text(
json.dumps(
{
+ "common_runtime_root": str(tmp_path / "runtime"),
"goals": [
{
"id": goal_id,
diff --git a/examples/personal-workspace-browser/research-history.md b/examples/personal-workspace-browser/research-history.md
index 00b77d207e..acab3de700 100644
--- a/examples/personal-workspace-browser/research-history.md
+++ b/examples/personal-workspace-browser/research-history.md
@@ -47,6 +47,23 @@ original JSONL digests and sizes. The two source byte totals are 80,992 and
20,575; their exact original serialization can be reconstructed as described
below. No model or solver is called by this scenario.
+The attached work-conversation scenario also uses the complete Grid context-on
+fixture as a tool result, replacing repeated invented validation lines. The
+production attached reader and HTTP detail pages must deliver the entire JSON
+object into the packaged conversation, including every original observation and
+its diagnostic/unknown boundaries, at desktop and 390px widths. A single synthetic
+Unicode prefix retains the character-offset regression. The disposable host peer,
+tool call and carrier messages are synthetic; this does not reconstruct a real
+benchmark conversation or qualify model, installed App or original-host adoption.
+The peer registry pins its disposable runtime instead of consulting the operator's
+implicit default roots; conflicting defaults are covered by the HTTP regression.
+The separate artificial paging, transport-failure and heavy-tool stress cases
+remain boundary tests.
+The narrow work view keeps the native source, reply/unknown state and original-App
+action in one status band. Its icon retains the full accessible action name,
+keyboard activation and native failure/recovery path; the original-App action is
+never a substitute for reading the complete record in LoopX.
+
The same core scenario now includes two record-rich histories from different
tasks, with all **349 original events** retained:
diff --git a/tests/presentation/test_codex_attached_conversation.py b/tests/presentation/test_codex_attached_conversation.py
index 11ecac4734..42f2e2f82f 100644
--- a/tests/presentation/test_codex_attached_conversation.py
+++ b/tests/presentation/test_codex_attached_conversation.py
@@ -45,6 +45,31 @@ def body(binding, text="Owner follow-up"):
}
+def test_protocol_fixture_ignores_conflicting_ambient_runtime_roots(tmp_path, monkeypatch):
+ from loopx import paths
+
+ current, legacy = tmp_path / "current", tmp_path / "legacy"
+ monkeypatch.setattr(paths, "DEFAULT_RUNTIME_ROOT", current)
+ monkeypatch.setattr(paths, "LEGACY_RUNTIME_ROOT", legacy)
+ ambient = []
+ for root in (current, legacy):
+ root.mkdir()
+ registry = paths.global_registry_path(root)
+ registry.write_text(json.dumps({"goals": []}))
+ ambient.append(registry)
+ assert paths.default_runtime_route()["status"] == "conflict"
+ before = {path: path.read_bytes() for path in ambient}
+ workspace = tmp_path / "isolated"
+ workspace.mkdir()
+ with fixture(workspace) as (request, _, _, inputs, _, _):
+ _, query = target(request)
+ status, result = request(query=query)
+ assert status == 200
+ assert any(row["text"] == "Original result" for row in result["messages"])
+ assert not inputs
+ assert {path: path.read_bytes() for path in ambient} == before
+
+
def test_managed_directory_keeps_registered_identity_and_original_sessions(tmp_path):
with fixture(tmp_path) as (request, _, methods, inputs, registry, _):
store = request.server.chat_store