From dd5e4d1d7fcd65bbc02e8ce07933de107241cf14 Mon Sep 17 00:00:00 2001
From: LoopX Agent <337587101+loopx-agent@users.noreply.github.com>
Date: Sun, 18 Oct 2026 19:59:08 +0800
Subject: [PATCH 1/3] feat(workspace): read pinned runtime sources in the
existing Goal view
Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com>
---
apps/presentation/dashboard/package.json | 1 +
.../smoke/status-source-catalog-smoke.ts | 18 ++
.../src/data/status-source-catalog.ts | 20 +-
.../presentation/dashboard/src/data/status.ts | 6 +
.../personal-workspace/goal-overview.tsx | 5 +-
.../goal-research-analysis.tsx | 13 +-
.../goal-research-history.tsx | 8 +-
.../personal-workspace/goal-sidebar.tsx | 2 +-
.../src/features/personal-workspace/i18n.tsx | 2 +
.../personal-workspace-page.tsx | 2 +-
.../status-source-switcher.tsx | 10 +-
.../personal-workspace/use-goal-order.ts | 12 +-
.../dashboard/src/views/dashboard-page.tsx | 14 +-
.../before-desktop.png | Bin 0 -> 132641 bytes
.../runtime-source-workspace/desktop.png | Bin 0 -> 127426 bytes
.../assets/runtime-source-workspace/phone.png | Bin 0 -> 62853 bytes
docs/development/frontend-delivery.md | 18 ++
...runtime-source-workspace-browser-smoke.mjs | 104 +++++++
loopx/presentation/explore_results_api.py | 11 +-
loopx/presentation/runtime_workspace.py | 264 ++++++++++++++++++
tests/presentation/test_runtime_workspace.py | 127 +++++++++
21 files changed, 610 insertions(+), 27 deletions(-)
create mode 100644 docs/assets/runtime-source-workspace/before-desktop.png
create mode 100644 docs/assets/runtime-source-workspace/desktop.png
create mode 100644 docs/assets/runtime-source-workspace/phone.png
create mode 100644 examples/runtime-source-workspace-browser-smoke.mjs
create mode 100644 loopx/presentation/runtime_workspace.py
create mode 100644 tests/presentation/test_runtime_workspace.py
diff --git a/apps/presentation/dashboard/package.json b/apps/presentation/dashboard/package.json
index c21a9ccd98..3d1d8b045b 100644
--- a/apps/presentation/dashboard/package.json
+++ b/apps/presentation/dashboard/package.json
@@ -58,6 +58,7 @@
"smoke:team-plan-proposal": "tsc --ignoreConfig --target ES2022 --module ES2022 --moduleResolution Bundler --ignoreDeprecations 6.0 --jsx react-jsx --types node --skipLibCheck --strict --rootDir ../../.. --outDir node_modules/.cache/loopx-team-plan-smoke smoke/team-plan-proposal-smoke.ts src/data/chat.ts src/features/personal-workspace/team-plan-preview.ts src/vite-env.d.ts && node node_modules/.cache/loopx-team-plan-smoke/apps/presentation/dashboard/smoke/team-plan-proposal-smoke.js",
"smoke:status-source-switch-browser": "node ../../../examples/status-source-switch-browser-smoke.mjs",
"smoke:status-source-switch-packaged": "LOOPX_STATUS_SOURCE_SWITCH_PACKAGED=1 LOOPX_STATUS_SOURCE_SWITCH_PORT=5198 node ../../../examples/status-source-switch-browser-smoke.mjs",
+ "smoke:runtime-source-workspace": "node ../../../examples/runtime-source-workspace-browser-smoke.mjs",
"smoke:status-sources": "rm -rf /tmp/loopx-status-source-smoke && tsc --ignoreConfig --target ES2022 --module CommonJS --moduleResolution Node --ignoreDeprecations 6.0 --skipLibCheck --strict --resolveJsonModule --esModuleInterop --outDir /tmp/loopx-status-source-smoke smoke/status-source-catalog-smoke.ts src/data/ssh-host-catalog.ts src/data/status-source-catalog.ts src/data/local-status-query.ts src/data/status.ts && NODE_PATH=\"$PWD/node_modules\" node /tmp/loopx-status-source-smoke/apps/presentation/dashboard/smoke/status-source-catalog-smoke.js",
"smoke:task-board-scroll": "node smoke/task-board-scroll-smoke.mjs",
"smoke:usage-progress": "tsc --ignoreConfig --target ES2022 --module ESNext --moduleResolution Bundler --allowImportingTsExtensions --types vite/client --resolveJsonModule --esModuleInterop --jsx react-jsx --skipLibCheck --strict --noEmit smoke/usage-progress-smoke.ts src/features/personal-workspace/personal-workspace-model.ts && vite build --ssr smoke/usage-progress-smoke.ts --outDir node_modules/.cache/loopx-usage-progress-smoke --emptyOutDir && node node_modules/.cache/loopx-usage-progress-smoke/usage-progress-smoke.js",
diff --git a/apps/presentation/dashboard/smoke/status-source-catalog-smoke.ts b/apps/presentation/dashboard/smoke/status-source-catalog-smoke.ts
index 3952f50db2..1d5e7f6edf 100644
--- a/apps/presentation/dashboard/smoke/status-source-catalog-smoke.ts
+++ b/apps/presentation/dashboard/smoke/status-source-catalog-smoke.ts
@@ -8,6 +8,7 @@ import {
localStatusSource,
projectedStatusSourceForUrl,
removeStatusSource,
+ runtimeStatusSourceCatalog,
saveStatusSourceCatalog,
statusSourceCatalogStorageKey,
statusSourceForUrl,
@@ -124,4 +125,21 @@ equal(configuredDraft.command, "ssh -N -L 8876:127.0.0.1:8766 remote-lab", "the
equal(configuredDraft.statusUrl, "http://127.0.0.1:8876/status.json", "the selected Host maps to the loopback-only status source");
assert("error" in configuredSshTunnelDraft("remote-lab", "22"), "privileged local ports fail closed");
+const runtimeUrl = "http://127.0.0.1:19801/status.json";
+const runtimeEntries = [
+ {id: "retained", label: "Retained", status_url: runtimeUrl},
+ {id: "fresh", label: "Fresh", status_url: "http://127.0.0.1:19802/status.json"},
+];
+const runtimes = runtimeStatusSourceCatalog(runtimeEntries, runtimeUrl, baseHref);
+assert(runtimes, "an owner-prepared catalog recognizes its current source");
+assert(runtimes.sources.every((s) => s.readOnly && s.kind === "runtime" && !s.hostAlias), "runtime readers never acquire SSH or execution authority");
+equal(activeStatusSourceForUrl(runtimes, runtimeEntries[1].status_url, runtimeUrl, baseHref).id, "fresh", "a failed or pending same-Goal source selection does not bounce to the loaded runtime");
+equal(runtimeStatusSourceCatalog(undefined, runtimeUrl, baseHref), null, "existing services need no runtime catalog");
+equal(runtimeStatusSourceCatalog(runtimeEntries, "http://127.0.0.1:19803/status.json", baseHref), null, "an unrelated service cannot replace source choices");
+equal(runtimeStatusSourceCatalog([...runtimeEntries, runtimeEntries[0]], runtimeUrl, baseHref), null, "duplicate runtime ids or URLs fail closed");
+equal(runtimeStatusSourceCatalog([{...runtimeEntries[0], status_url: "https://external.example/status.json"}], runtimeUrl, baseHref), null, "runtime readers remain loopback-only");
+const runtimeStorage = new MemoryStorage();
+saveStatusSourceCatalog(runtimeStorage, runtimes);
+equal(loadStatusSourceCatalog(runtimeStorage, baseHref).sources.length, 1, "ephemeral runtime choices are not persisted as execution or SSH configuration");
+
console.log("status source catalog smoke: ok");
diff --git a/apps/presentation/dashboard/src/data/status-source-catalog.ts b/apps/presentation/dashboard/src/data/status-source-catalog.ts
index 9c54843616..ae8acced2e 100644
--- a/apps/presentation/dashboard/src/data/status-source-catalog.ts
+++ b/apps/presentation/dashboard/src/data/status-source-catalog.ts
@@ -7,7 +7,7 @@ export const statusSourceCatalogStorageKey = "loopx-status-source-catalog-v1";
export type StatusSource = {
hostAlias?: string;
id: string;
- kind: "local" | "ssh_tunnel";
+ kind: "local" | "ssh_tunnel" | "runtime";
label: string;
readOnly: boolean;
statusUrl: string;
@@ -20,6 +20,24 @@ export type StatusSourceCatalog = {
export type StatusSourceStorage = Pick
{a.zh ? "此运行尚未接入深度分析。" : "Deep analysis is not connected for this runtime."}
; if (!p) return{a.loading ? (a.zh ? "正在核对可分析来源;浏览不调用 Agent。" : "Checking analysis sources. Browsing never invokes an Agent.") : reasonCopy(a.snapshot?.catalog.reason_code, a.zh)}
; return !p.result && p.availability !== "available" ?{reasonCopy(p.reason_code, a.zh)}
: null; } diff --git a/apps/presentation/dashboard/src/features/personal-workspace/goal-research-history.tsx b/apps/presentation/dashboard/src/features/personal-workspace/goal-research-history.tsx index c28e8f1492..09031d9779 100644 --- a/apps/presentation/dashboard/src/features/personal-workspace/goal-research-history.tsx +++ b/apps/presentation/dashboard/src/features/personal-workspace/goal-research-history.tsx @@ -26,8 +26,8 @@ const selectedFromUrl = (goal: string): Selection => { /** An effect-free history consumer. Its source is Explore, not raw sessions or * a judge. All recorded updates survive; unsupported analysis stays unavailable. */ -export function GoalResearchHistory({goalId, active, zh, refreshRevision = 0}: {goalId: string; active: boolean; zh: boolean; refreshRevision?: number}) { - const analysis = useResearchAnalysis(goalId, active, zh); +export function GoalResearchHistory({goalId, active, zh, refreshRevision = 0, readSourceUrl}: {goalId: string; active: boolean; zh: boolean; refreshRevision?: number; readSourceUrl?: string}) { + const analysis = useResearchAnalysis(goalId, active, zh, readSourceUrl); const [read, setRead] = useState7W^`%k
z1Lyenl7j9~Al-bIF+PzQ|G~1G
?_%k9qcoNEl(&<0M3Wmazrc{W2qd}{K}{}0>8aqjU7Fb`~NU?mQhu<
zTeueKlx~pj?rtgRP(Va-(XF%r(p>_JQo0)n38g_`NvR+qjR=w|AjX+%@3YT1e;niU
zhaar