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
4 changes: 3 additions & 1 deletion apps/presentation/dashboard/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,9 @@
"smoke:usage-progress": "rm -rf /tmp/loopx-usage-progress-smoke && tsc --ignoreConfig --target ES2022 --module CommonJS --moduleResolution Node --ignoreDeprecations 6.0 --resolveJsonModule --esModuleInterop --jsx react-jsx --skipLibCheck --strict --outDir /tmp/loopx-usage-progress-smoke smoke/usage-progress-smoke.ts src/features/personal-workspace/personal-workspace-model.ts && node /tmp/loopx-usage-progress-smoke/apps/presentation/dashboard/smoke/usage-progress-smoke.js",
"smoke:action-review-plan": "rm -rf .local/action-review-plan-smoke && tsc --ignoreConfig --target ES2022 --module ES2022 --moduleResolution Bundler --skipLibCheck --strict --rootDir ../../.. --outDir .local/action-review-plan-smoke smoke/action-review-plan-smoke.ts ../../../loopx/control_plane/presentation/action_review_plan.ts src/vite-env.d.ts && node .local/action-review-plan-smoke/apps/presentation/dashboard/smoke/action-review-plan-smoke.js",
"smoke:attention-details": "tsc --ignoreConfig --target ES2022 --module CommonJS --moduleResolution Node --ignoreDeprecations 6.0 --resolveJsonModule --esModuleInterop --skipLibCheck --strict --outDir /tmp/loopx-attention-details-smoke smoke/attention-details-smoke.ts && NODE_PATH=\"$PWD/node_modules\" node /tmp/loopx-attention-details-smoke/apps/presentation/dashboard/smoke/attention-details-smoke.js",
"smoke:delivery-review": "tsc --ignoreConfig --target ES2022 --module NodeNext --moduleResolution NodeNext --skipLibCheck --strict --rootDir src --outDir node_modules/.cache/delivery-review src/data/delivery-review.ts src/data/goal-acceptance-observation.ts src/features/personal-workspace/delivery-review-copy.ts && node smoke/delivery-review-smoke.mjs"
"smoke:delivery-review": "tsc --ignoreConfig --target ES2022 --module NodeNext --moduleResolution NodeNext --skipLibCheck --strict --rootDir src --outDir node_modules/.cache/delivery-review src/data/delivery-review.ts src/data/goal-acceptance-observation.ts src/features/personal-workspace/delivery-review-copy.ts && node smoke/delivery-review-smoke.mjs",
"smoke:team-artifact-comparison": "tsc --ignoreConfig --target ES2022 --module ES2022 --moduleResolution Bundler --skipLibCheck --strict --rootDir src --outDir node_modules/.cache/team-comparison src/features/personal-workspace/team-artifact-comparison.ts src/vite-env.d.ts && node smoke/team-artifact-comparison-smoke.mjs",
"smoke:team-report": "tsc --ignoreConfig --target ES2022 --module ES2022 --moduleResolution Bundler --jsx react-jsx --skipLibCheck --strict --rootDir src --outDir node_modules/.cache/team-report src/features/personal-workspace/team-artifact-content.tsx src/vite-env.d.ts && node smoke/team-report-smoke.mjs"
},
"dependencies": {
"@fontsource-variable/geist": "^5.3.0",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import assert from "node:assert/strict";
import {comparisonSource, changedRange, preferredComparisonIndex} from "../node_modules/.cache/team-comparison/features/personal-workspace/team-artifact-comparison.js";

const link = {operation_id: "original", ref: "report.txt", sha256: "a".repeat(64),
input_ref: "input.txt", relation: "revises", state: "current"};
const source = {operation_id: "original", request_id: "request", agent_id: "author", todo_id: "work",
status: "accepted", worker_active: false, recovery_required: false,
artifacts: [{ref: link.ref, sha256: link.sha256, text: "unchanged\nold\nend"}]};
assert.equal(comparisonSource(link, source)?.text, "unchanged\nold\nend");
for (const patch of [{operation_id: "other"}, {status: "rejected"}, {recovery_required: true}, {error: "unavailable"},
{artifacts: [{ref: link.ref, sha256: "b".repeat(64), text: "new"}]},
{artifacts: [{ref: "other.txt", sha256: link.sha256, text: "new"}]},
{artifacts: [...source.artifacts, ...source.artifacts]}]) {
assert.equal(comparisonSource(link, {...source, ...patch}), null);
}
assert.equal(comparisonSource({...link, state: "unavailable"}, source), null);
assert.deepEqual(changedRange("unchanged\nold\nend", "unchanged\nnew\nend"), {
left: ["unchanged", "old", "end"], right: ["unchanged", "new", "end"], start: 1, leftEnd: 2, rightEnd: 2,
});
const equal = changedRange("one\ntwo", "one\ntwo");
assert.equal(equal.start, equal.leftEnd);
assert.equal(equal.start, equal.rightEnd);
const inserted = changedRange("one\nend", "one\nadded\nend");
assert.equal(inserted.leftEnd, 1); assert.equal(inserted.rightEnd, 2);
const removed = changedRange("one\nremoved\nend", "one\nend");
assert.equal(removed.leftEnd, 2); assert.equal(removed.rightEnd, 1);
const outputs = [{ref: "output.json"}, {ref: "review.md"}, {ref: "report.md"}];
assert.equal(preferredComparisonIndex("report.md", outputs), 2);
assert.equal(preferredComparisonIndex("analysis.MD", outputs), 1);
assert.equal(preferredComparisonIndex("source.json", outputs), 0);
assert.equal(preferredComparisonIndex("no-extension", outputs), 0);
assert.equal(preferredComparisonIndex("source.txt", outputs), 0);
console.log("team artifact comparison: exact-version rejection and changed-range cases passed");
22 changes: 22 additions & 0 deletions apps/presentation/dashboard/smoke/team-report-smoke.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import assert from "node:assert/strict";
import {createElement} from "react";
import {renderToStaticMarkup} from "react-dom/server";
import {MarkdownText} from "../node_modules/.cache/team-report/features/personal-workspace/markdown.js";
import {TeamArtifactContent} from "../node_modules/.cache/team-report/features/personal-workspace/team-artifact-content.js";

const text = '# Research\n\n| Measure | Value |\n|---|---:|\n| **Cash** | 75 |\n| Escaped \\| label | 25 |\n\n[Source](https://example.org/report)\n<script>window.pwned=true</script>\n[Unsafe](javascript:alert(1))\n![Tracking](https://example.org/pixel)';
const artifact = {ref: "report.md", sha256: "a".repeat(64), text};
const html = renderToStaticMarkup(createElement(TeamArtifactContent, {artifact, label: "Report"}));
assert.match(html, /<table>/); assert.match(html, /scope="col"/); assert.match(html, /<strong>Cash<\/strong>/);
assert.match(html, /Escaped \| label/); assert.match(html, /href="https:\/\/example.org\/report"/);
assert.doesNotMatch(html, /<script|<img|href="javascript:/);
assert.match(html, /&lt;script&gt;/);
const raw = renderToStaticMarkup(createElement(TeamArtifactContent, {artifact, label: "Report", raw: true}));
assert.doesNotMatch(raw, /<table>|<a /); assert.match(raw, /\| Measure \| Value \|/);
const chat = renderToStaticMarkup(createElement(MarkdownText, {text}));
assert.doesNotMatch(chat, /<table>/, "Existing chat rendering stays unchanged");
const malformed = renderToStaticMarkup(createElement(MarkdownText, {text: "| One | Two |\n|---|\n| kept | intact |", report: true}));
assert.doesNotMatch(malformed, /<table>/); assert.match(malformed, /kept/);
const json = renderToStaticMarkup(createElement(TeamArtifactContent, {artifact: {...artifact, ref: "report.json"}, label: "JSON"}));
assert.doesNotMatch(json, /<table>/, "File suffix does not turn JSON evidence into a report");
console.log("report rendering: readable table, raw fidelity, safe links and inert HTML, chat parity passed");
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { useEffect, useId, useRef, useState } from "react";
import { z } from "zod";
import {compactWorkspaceText} from "./personal-workspace-model";
import type { WorkspaceAgentTodo, WorkspaceDrawerSelection, WorkspaceGoal } from "./personal-workspace-model";
import { useWorkspaceI18n } from "./i18n";

Expand Down Expand Up @@ -75,7 +76,7 @@ export function CompletedTaskLane({ goal, agentId, seed, enabled, listView = fal
const todo = rows[index]!;
return <div className="personal-task-card personal-completed-row" key={todo.todoId} style={{ top: index * rowHeight, height: rowHeight }}>
<button type="button" onFocus={() => setFocused(index)} onBlur={() => setFocused(null)} onClick={() => onSelect({ kind: "todo", item: { ...todo, goalId: goal.goalId, goalTitle: goal.title, ownerLabel: todo.claimedBy ?? goal.agentLabel ?? goal.agentId } })}>
<span className="is-done">✓</span><strong>{todo.text}</strong><small>{todo.claimedBy ?? goal.agentLabel ?? goal.agentId}</small>
<span className="is-done">✓</span><strong>{compactWorkspaceText(todo.text, 112)}</strong><small>{todo.claimedBy ?? goal.agentLabel ?? goal.agentId}</small>
</button>
</div>;
})}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,3 +53,59 @@
.goal-team-lineage ul { list-style: none; padding: 0; margin: 12px 0; }
.goal-team-lineage li { display: grid; gap: 8px; padding: 0 0 16px 16px; border-left: 2px solid var(--pw-line, #ebebeb); overflow-wrap: anywhere; }
.goal-team-lineage button { justify-self: start; max-width: 100%; overflow-wrap: anywhere; text-align: start; }
.goal-loopx-dialog:has(.goal-team-comparison) { width: min(1040px, calc(100vw - 32px)); }
.goal-team-comparison { margin-block: 24px; }
.goal-team-comparison-heading { display: flex; align-items: baseline; justify-content: space-between; gap: 16px; }
.goal-team-comparison-heading h4 { font-size: 20px; letter-spacing: -.02em; }
.goal-team-comparison-heading > span { font-size: 12px; color: var(--pw-muted, #666); }
.goal-team-comparison-sources { display: flex; gap: 8px; flex-wrap: wrap; margin-block: 16px; }
.goal-team-comparison-sources button { flex-direction: column; align-items: flex-start; max-width: 100%; overflow-wrap: anywhere; text-align: left; }
.goal-team-comparison-sources button > span { color: var(--pw-muted, #666); font-size: 12px; }
.goal-team-comparison-sources button[aria-pressed="true"] { border-color: var(--pw-text, #171717); }
.goal-team-comparison-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 16px; }
.goal-team-comparison-grid article { margin: 0; min-width: 0; border: 1px solid var(--pw-line, #ebebeb); border-radius: 12px; overflow: hidden; }
.goal-team-comparison-grid header { display: grid; gap: 4px; padding: 16px; border-bottom: 1px solid var(--pw-line, #ebebeb); overflow-wrap: anywhere; }
.goal-team-comparison-grid header span { color: var(--pw-muted, #666); font-size: 12px; }
.goal-team-comparison-grid pre { margin: 0; max-height: 400px; border: 0; border-radius: 0; padding: 16px; font-size: 13px; line-height: 1.8; }
.goal-team-comparison-grid pre span[data-changed="true"] { background: color-mix(in srgb, var(--pw-text, #171717) 7%, transparent); text-decoration: underline; text-decoration-color: var(--pw-muted, #666); text-underline-offset: 4px; }
.goal-team-comparison select { min-height: 44px; padding: 8px; background: var(--pw-surface, #fff); color: inherit; border: 1px solid var(--pw-line, #ebebeb); border-radius: 6px; }
@media (max-width: 640px) { .goal-team-comparison-grid { grid-template-columns: 1fr; } }

.goal-team-artifact > header { display: flex; justify-content: space-between; align-items: center; gap: 16px; margin-bottom: 16px; }
.goal-team-artifact > header h4 { font-size: 12px; color: var(--pw-muted); font-weight: 500; }
.goal-team-report { padding: 24px; background: var(--pw-surface, #fff); color: var(--pw-text, #171717); overflow-wrap: anywhere; }
.goal-team-report .personal-md { font-size: 15px; line-height: 1.8; gap: 20px; }
.goal-team-report .personal-md p { font-size: inherit; line-height: inherit; color: inherit; margin: 0; }
.goal-team-report .personal-md .is-h1 { font-size: 26px; line-height: 1.3; font-weight: 600; letter-spacing: -.03em; }
.goal-team-report .personal-md .is-h2 { font-size: 18px; font-weight: 600; margin-top: 8px; }
.goal-team-report .personal-md-list { list-style: revert; margin: 0; padding-left: 24px; }
.goal-team-report .personal-md-list li { border: 0; padding: 0; }
.personal-md-table-scroll { min-width: 0; max-width: 100%; overflow-x: auto; }
.personal-md-table-scroll table { width: 100%; border-collapse: collapse; font-size: 14px; line-height: 1.6; }
.personal-md-table-scroll th, .personal-md-table-scroll td { padding: 12px; border-bottom: 1px solid var(--pw-line, #ebebeb); text-align: left; vertical-align: top; }
.personal-md-table-scroll th { font-weight: 500; background: var(--pw-bg, #fafafa); }
.goal-team-comparison-grid .goal-team-report { max-height: 480px; overflow: auto; }
.goal-team-comparison-grid .goal-team-report .personal-md { font-size: 14px; }
.goal-team-results { width: min(960px, 100%); margin: 16px auto 24px; padding: 24px; border: 1px solid var(--pw-line, #ebebeb); border-radius: 12px; background: var(--pw-surface, #fff); color: var(--pw-text, #171717); }
.goal-team-results > header { display: flex; align-items: center; justify-content: space-between; gap: 16px; }
.goal-team-results h3 { font-size: 16px; font-weight: 600; margin: 0; }
.goal-team-results p { font-size: 12px; line-height: 1.7; color: var(--pw-muted); }
.goal-team-results [role="alert"] { color: var(--pw-red, #b42318); }
.goal-team-results button, .goal-team-results select { min-height: 44px; padding: 8px 12px; border: 1px solid var(--pw-line, #ebebeb); border-radius: 6px; background: var(--pw-surface, #fff); color: inherit; font: inherit; cursor: pointer; }
.goal-team-results button[aria-pressed="true"] { border-color: var(--pw-text); }
.goal-team-results button:disabled { opacity: .5; cursor: default; }
.goal-team-results summary { cursor: pointer; padding: 12px 0; font-size: 12px; min-height: 44px; }
.goal-team-results code { overflow-wrap: anywhere; }
.goal-team-report table code { white-space: pre; overflow-wrap: normal; word-break: normal; }
.goal-team-results pre { padding: 16px; max-height: 400px; overflow: auto; white-space: pre-wrap; overflow-wrap: anywhere; font: 13px/1.7 var(--font-mono, monospace); background: var(--pw-bg); }
.goal-team-result-list { display: flex; gap: 8px; flex-wrap: wrap; margin-top: 16px; }
.goal-team-result-list button { max-width: 100%; overflow-wrap: anywhere; font-size: 13px; text-align: start; }
.goal-team-result-reader { border-top: 1px solid var(--pw-line); margin-top: 16px; padding-top: 16px; }
.goal-team-results .goal-team-artifact > header { margin-bottom: 0; }
.goal-team-results :focus-visible, .goal-team-report:focus-visible, .personal-md-table-scroll:focus-visible { outline: 2px solid var(--pw-blue-ink, #0070f3); outline-offset: 2px; }
@media (max-width: 640px) {
.goal-team-results { padding: 16px; }
.goal-team-report { padding: 16px 0; }
.goal-team-comparison-grid .goal-team-report { padding: 16px; }
.goal-team-report .personal-md .is-h1 { font-size: 22px; }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import {useEffect, useRef, useState} from "react";
import {readLoopXTeamWork, type DelegationDependency, type DelegationReadback} from "../../data/chat";
import {changedRange, comparisonSource, preferredComparisonIndex} from "./team-artifact-comparison";
import {TeamArtifactContent} from "./team-artifact-content";

export function GoalTeamComparison({sessionId, result, zh}: {
sessionId: string; result: DelegationReadback; zh: boolean;
}) {
const [selection, setSelection] = useState<{link: DelegationDependency; source: DelegationReadback} | null>(null);
const [outputIndex, setOutputIndex] = useState(0);
const [busy, setBusy] = useState(false);
const [error, setError] = useState("");
const [raw, setRaw] = useState(false);
const generation = useRef(0);
useEffect(() => {
generation.current++; setSelection(null); setError(""); setBusy(false); setOutputIndex(0);
return () => {generation.current++;};
}, [result, sessionId]);
const artifacts = result.status === "accepted" && !result.error && !result.recovery_required ? result.artifacts ?? [] : [];
const links = result.dependencies ?? [];
async function compare(link: DelegationDependency) {
const current = ++generation.current;
setSelection(null); setError(""); setBusy(true); setOutputIndex(preferredComparisonIndex(link.ref, artifacts)); setRaw(false);
try {
const source = await readLoopXTeamWork(sessionId, link.operation_id);
if (current !== generation.current) return;
if (!comparisonSource(link, source)) {
setError(zh ? "指定来源版本无法核验,未展示对照。" : "The referenced source version cannot be verified. Comparison withheld.");
} else setSelection({link, source});
} catch {
if (current === generation.current) setError(zh ? "来源读取失败,已清除上次对照。可重试或查看执行详情。" : "Source read failed; previous comparison cleared. Retry or inspect the execution.");
} finally {if (current === generation.current) setBusy(false);}
}
if (!links.length || !artifacts.length) return null;
const before = selection ? comparisonSource(selection.link, selection.source) : null;
const after = artifacts[outputIndex];
const range = before && after ? changedRange(before.text, after.text) : null;
const labels = zh ? {revises: "修订依据", responds_to: "回应依据", uses: "使用依据"}
: {revises: "Revision source", responds_to: "Response source", uses: "Input source"};
return <section className="goal-team-comparison" aria-label={zh ? "依据与结果对照" : "Source and result comparison"} aria-busy={busy}>
<div className="goal-team-comparison-heading"><h4>{zh ? "看清这次变化" : "See what changed"}</h4>
<span>{zh ? "按需核验" : "On-demand verification"}</span></div>
<div className="goal-team-comparison-sources">{links.map((link, index) => <button type="button"
key={`${link.operation_id}:${link.input_ref}:${index}`} disabled={busy || link.state !== "current"}
aria-pressed={selection?.link === link} onClick={() => void compare(link)}>
<span>{labels[link.relation]}</span><strong>{link.ref}</strong>
{links.filter(row => row.ref === link.ref).length > 1 ? <span>{link.operation_id}</span> : null}
{link.state !== "current" ? <span>{zh ? "无法核验" : "Unavailable"}</span> : null}
</button>)}</div>
{busy ? <p role="status">{zh ? "正在读取指定来源版本…" : "Reading the referenced source version…"}</p> : null}
{error ? <p role="alert">{error}</p> : null}
{before && after && range ? <>
<button type="button" aria-pressed={raw} onClick={() => setRaw(value => !value)}>{raw ? (zh ? "阅读对照" : "Read comparison") : (zh ? "查看原文差异" : "View source changes")}</button>
{artifacts.length > 1 ? <label>{zh ? "对照产物" : "Compare output"}<select value={outputIndex} onChange={e => setOutputIndex(Number(e.target.value))}>
{artifacts.map((row, index) => <option key={`${row.ref}:${index}`} value={index}>{row.ref}</option>)}
</select></label> : null}
<div className="goal-team-comparison-grid">
{([{kind: "source", title: zh ? "指定来源" : "Referenced source", artifact: before, lines: range.left, end: range.leftEnd},
{kind: "result", title: zh ? "本次产物" : "This output", artifact: after, lines: range.right, end: range.rightEnd}]).map(pane => <article key={pane.kind}>
<header><span>{pane.title}</span><strong>{pane.artifact.ref}</strong></header>
{raw ? <pre tabIndex={0} aria-label={`${pane.title}: ${pane.artifact.ref}`}>{pane.lines.map((line, i) => <span key={i}
data-changed={i >= range.start && i < pane.end}>{line}{i < pane.lines.length - 1 ? "\n" : ""}</span>)}</pre>
: <TeamArtifactContent artifact={pane.artifact} label={`${pane.title}: ${pane.artifact.ref}`}/>}
</article>)}
</div>
<p>{before.text === after.text ? (zh ? "两个版本的文本相同。" : "Both versions contain the same text.")
: raw ? (zh ? "标记文本变化所在范围;不代表结论正确或已被采用。" : "Marks the range containing text changes; does not establish correctness or adoption.")
: (zh ? "正文对照;验收与采用见版本关系。" : "Read the reports; see lineage for acceptance and adoption.")}</p>
<details><summary>{zh ? "核验的版本" : "Verified versions"}</summary>
<code>{before.ref} · sha256:{before.sha256}</code><code>{after.ref} · sha256:{after.sha256}</code>
</details>
</> : <p>{zh ? "选择一份依据,与本次产物并排阅读。" : "Choose a source to read beside this output."}</p>}
</section>;
}
Loading
Loading