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
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,13 @@
.goal-report-reviews :is([data-result-catalog], [data-result-reader]):focus { outline: 2px solid var(--pw-blue-ink); outline-offset: 4px; }
.report-review-tools, .report-review-tools > button, .goal-report-reviews .result-catalog-return { display: inline-flex; align-items: center; gap: 8px; }
.report-review-reader { min-width: 0; margin-top: 8px; }
.report-review-masthead { padding: 8px 0; border-bottom: 1px solid var(--pw-border); }
.report-review-masthead { padding: 8px 0; border-bottom: 1px solid var(--pw-line); }
.report-review-masthead h2 { margin: 8px 0; font-size: var(--text-title); line-height: var(--leading-title); overflow-wrap: anywhere; }
.report-review-masthead time, .report-review-masthead span { font-size: var(--text-caption); line-height: var(--leading-caption); color: var(--pw-muted); }
.report-review-actions { display: flex; flex-wrap: wrap; align-items: start; gap: 8px; padding: 8px 0 16px; }
.report-review-actions .personal-decision-bar { flex: 1; }
.report-review-actions .personal-compact-menu { flex: 0 0 auto; }
.report-review-reader iframe { display: block; width: 100%; height: min(1000px, 80vh); min-height: 560px; border: 1px solid var(--pw-border); border-radius: 6px; background: var(--pw-surface); }
.report-review-reader iframe { display: block; width: 100%; height: min(1000px, 80vh); min-height: 560px; border: 1px solid var(--pw-line); border-radius: 6px; background: var(--pw-card); }
.report-review-prose { font-size: var(--text-lead); line-height: var(--leading-prose); }
.report-review-reader > details { padding: 16px 0; overflow-wrap: anywhere; }
@media (max-width: 640px) { .report-review-masthead > div { flex-wrap: wrap; } .report-review-reader iframe { min-height: 640px; } .report-review-actions .personal-decision-bar { flex-basis: 100%; } }
10 changes: 8 additions & 2 deletions examples/personal-workspace-browser/conversation-activity.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -216,7 +216,10 @@ export const conversationActivityScenario = {
const answer = "检查完成,下一步已列出。";
const runtime = page.__loopxRuntime;
runtime.sessions.set(next.sessionId, { ...runtime.sessions.get(next.sessionId), active_turn_id: null, status: "ready" });
runtime.messages.get(next.sessionId).push({ message_id: `${next.turnId}-assistant`, turn_id: next.turnId, role: "assistant", text: answer });
runtime.messages.get(next.sessionId).push({
message_id: `${next.turnId}-assistant`, turn_id: next.turnId, role: "assistant", text: answer,
created_at: await page.evaluate(() => new Date().toISOString()),
});
response.end(event(5, "turn.completed", { response: { schema_version: "loopx_chat_agent_response_v0", message: answer, proposals: [], gate: null } }));
await route.fulfill({ json: { ok: true, session_id: next.sessionId, turn_id: next.turnId, status: "completed" } });
});
Expand Down Expand Up @@ -246,7 +249,10 @@ export const conversationActivityScenario = {
const runtime = page.__loopxRuntime;
runtime.turnMessages.set(turnId, body.message);
runtime.sessions.set(sessionId, {...runtime.sessions.get(sessionId), active_turn_id: turnId, status: "busy"});
runtime.messages.get(sessionId).push({message_id: `${turnId}-user`, turn_id: turnId, role: "user", text: body.message});
runtime.messages.get(sessionId).push({
message_id: `${turnId}-user`, turn_id: turnId, role: "user", text: body.message,
created_at: await page.evaluate(() => new Date().toISOString()),
});
api.turnRequests.push({message: body.message, sessionId, turnId});
let holdFirstRead = true;
await page.route(`**/api/chat/sessions/${sessionId}`, snapshot => {
Expand Down
21 changes: 12 additions & 9 deletions examples/personal-workspace-browser/goal-masthead.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -245,15 +245,12 @@ export const goalMastheadScenario = {
// and adapter, rather than manufacturing a complete three-item queue.
const fixtureEnv = { ...process.env, LOOPX_USAGE_PING: "0" };
if (!["LOOPX_TEST_PYTHON", "LOOPX_PYTHON_BIN", "LOOPX_PYTHON"].some(key => process.env[key])) fixtureEnv.PYTHONPATH = repoRoot;
const native = JSON.parse(execFileSync(resolveTestPython(), [
resolve(repoRoot, "examples/personal-workspace-browser/goal-attention-fixture.py"),
], { cwd: repoRoot, env: fixtureEnv, encoding: "utf8", timeout: 30_000 }));
const nativeCase = (...args) => JSON.parse(execFileSync(resolveTestPython(), [
resolve(repoRoot, "examples/personal-workspace-browser/goal-attention-fixture.py"), ...args,
], { cwd: repoRoot, env: fixtureEnv, encoding: "utf8", timeout: 30_000 }));
const native = nativeCase();
const mixed = nativeCase("--mixed");
const complete = nativeCase("--full");
const mixedComplete = nativeCase("--mixed", "--full");
const optionalComplete = nativeCase("--optional", "--full");
// Exercise source -> parser -> adapter -> digest -> packaged rendering.
// Counts are removed from a real producer payload, not a prebuilt UI model.
Expand Down Expand Up @@ -307,6 +304,7 @@ export const goalMastheadScenario = {
} finally { coverageEntries.push(...await ui.close()); }
}
for (const locale of ["zh-CN", "en"]) for (const mode of ["mixed", "bounded", "optional", "complete", "mixed-complete", "empty", "unknown"]) {
const partial = !["complete", "mixed-complete", "optional"].includes(mode);
const ui = await openWorkspacePage(browser, url, {
collectCoverage,
viewport: { width: locale === "en" ? 390 : 1512, height: 982 },
Expand All @@ -317,9 +315,12 @@ export const goalMastheadScenario = {
api.transformStatus = (fixture) => {
previous(fixture);
const queue = fixture.attention_queue.items.find(item => item.goal_id === goalId);
const source = mode === "mixed" ? mixed : mode === "complete" ? complete
: mode === "mixed-complete" ? mixedComplete : mode === "optional" ? optionalComplete : native;
const source = mode === "mixed" || mode === "mixed-complete" ? mixed : mode === "complete" ? complete
: mode === "optional" ? optionalComplete : native;
Object.assign(queue, structuredClone(source));
// Workspace prefers the owner-facing group. Bound its rows as well
// for partial cases while retaining the producer's total.
if (partial) queue.user_todos.items = queue.project_asset.user_todos.items;
if (mode === "empty") {
queue.user_todos.items = [];
queue.project_asset.user_todos.items = [];
Expand All @@ -336,13 +337,15 @@ export const goalMastheadScenario = {
await openGoal(ui.page, title);
const needs = await vital(ui.page, locale === "en" ? "Needs you" : "等你决定");
const expected = mode === "complete" ? "21" : mode === "mixed-complete" ? "1" : mode === "optional" ? "0" : "—";
assert.equal(needs.value, expected, "Only complete classified rows qualify a blocking total; open User Todo totals do not classify hidden rows");
const partial = !["complete", "mixed-complete", "optional"].includes(mode);
assert.equal(needs.value, expected, `${locale} ${mode}: only complete classified rows qualify a blocking total`);
const coverageHelp = partial ? await readVitalHelp(ui.page, needs.cell) : "";
if (partial) assert.match(coverageHelp, locale === "en" ? /oldest unknown/ : /最早时间未知/);
const listed = ["complete", "mixed-complete", "optional"].includes(mode) ? 21 : mode === "empty" ? 0 : 3;
if (partial) assert.match(coverageHelp, new RegExp(`${listed} ${locale === "en" ? "listed" : "项"}`));
if (mode === "optional") assert.match(needs.sub, locale === "en" ? /21 optional/ : /21 项可选/);
if (mode === "optional" || mode === "mixed-complete") {
const optional = mode === "optional" ? 21 : 20;
assert.match(needs.sub, new RegExp(`${optional} ${locale === "en" ? "optional" : "项可选"}`));
}
if (mode === "unknown") assert.doesNotMatch(needs.sub, /Nothing waiting|暂无等待/);
await ui.page.getByRole("navigation", { name: /^(Goal 视图|Goal view)$/ }).getByRole("button", { name: /^(概览|Overview)$/ }).click();
const rail = ui.page.locator(".goal-overview-attention");
Expand Down
18 changes: 12 additions & 6 deletions loopx/doctor.py
Original file line number Diff line number Diff line change
Expand Up @@ -147,12 +147,18 @@ def python_distribution_install(module_path: Path) -> dict[str, Any]:
return {"available": False}
installer = (installed.read_text("INSTALLER") or "").strip() or "unknown"
expected = module_path.resolve()
owns_module = any(
item.as_posix().endswith("loopx/doctor.py")
and Path(item.locate()).resolve() == expected
for item in installed.files or ()
)
if not owns_module:
module_suffix = os.path.join("loopx", "doctor.py")
for item in installed.files or ():
entry = item.as_posix()
if not os.path.normcase(entry).endswith(module_suffix):
continue
# A PackagePath uses POSIX components, so locating a drive-qualified
# RECORD entry can lose its Windows root on Python 3.11.
record_path = Path(entry)
located = record_path if record_path.is_absolute() else Path(item.locate())
if located.resolve() == expected:
break
else:
return {"available": False}
owner = resolve_python_install_owner(default_installer=installer, prefix=Path(sys.prefix))
return {
Expand Down
Loading