Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
6f01e74
fix(services): keep unsaved connection drafts across background refre…
PeterGuy326 Sep 25, 2026
4c88d54
Merge branch 'main' into fix/services-unsaved-draft-refresh
PeterGuy326 Sep 29, 2026
0d26632
Merge branch 'main' into fix/services-unsaved-draft-refresh
PeterGuy326 Sep 29, 2026
8071c7b
Merge branch 'main' into fix/services-unsaved-draft-refresh
PeterGuy326 Sep 29, 2026
98d673c
Merge branch 'main' into fix/services-unsaved-draft-refresh
PeterGuy326 Sep 29, 2026
d109395
Merge branch 'main' into fix/services-unsaved-draft-refresh
PeterGuy326 Sep 29, 2026
b798429
feat(space): add acceptance.v1 contract for human acceptance verdicts
huyz Oct 1, 2026
c269325
feat(space): document-driven collaboration space UI with acceptance gate
PeterGuy326 Oct 1, 2026
cc748ff
fix(space): type-check server tests under noUncheckedIndexedAccess
PeterGuy326 Oct 1, 2026
b0fa620
fix(space): narrow verdict index access for noUncheckedIndexedAccess
PeterGuy326 Oct 1, 2026
4ea2434
fix(packaging): ship the new shared modules in the runtime allowlist
PeterGuy326 Oct 1, 2026
1a19cd5
fix(packaging): ship acceptance.js in the runtime allowlist
PeterGuy326 Oct 1, 2026
f849fb8
fix(space): move the spine filename into the catalogs and register th…
PeterGuy326 Oct 1, 2026
373110a
fix(space): import shared values by subpath so the renderer can bundle
PeterGuy326 Oct 1, 2026
f649147
fix(acceptance): sync main and require complete criterion coverage
PeterGuy326 Oct 4, 2026
2a700a6
fix(services): sync main while preserving unsaved connection drafts
PeterGuy326 Oct 4, 2026
fe6fd5d
fix(space): sync acceptance contract and preserve document writes
PeterGuy326 Oct 4, 2026
97df0f6
fix: resolve current review and dependency audit blockers
PeterGuy326 Oct 5, 2026
6b7819c
fix: resolve current review and dependency audit blockers
PeterGuy326 Oct 5, 2026
b1a8f84
fix(space): sync contract, connection drafts and secure build depende…
PeterGuy326 Oct 5, 2026
6e26eaa
Merge current main after dependency security fix
PeterGuy326 Oct 5, 2026
9f70372
Sync draft with merged acceptance contract on main
PeterGuy326 Oct 5, 2026
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: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@

### Added

- #560:新增文档与结构双视图的协作区预览;结构写回保留空行与文件末尾换行,缺少结构章节时插入新章节。持久化验收仍待接入,不作为正式验收入口发布。

- #559:新增协作区人工验收契约,验收通过必须完整覆盖计划中的全部验收项;限制标识符、时间戳和产物路径,拒绝遗漏或越界的裁决。

- #469:可选 Laya 接力停止建议。仅在 `ROLEWEAVE_LAYA_ENABLED` 开启、已完成步骤得到正向建议且仍有未执行步骤时暂停;桌面端由 owner 显式选择“停止剩余步骤”或“继续接力”。30 秒超时、SSE 断连、无效或失败建议均继续原 mention order;Laya 只接收 `status`、`errorCode`、`hasOutput`,不接收输入、输出或 handoff 文本。已接受的消息与 spawn 清单保持不可变。Refs #422。
Expand Down
1 change: 1 addition & 0 deletions apps/desktop/packaging/runtime-layout.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,7 @@ const SHARED_RUNTIME_FILES = [
"dist/semantic-runtime.js",
"dist/services.js",
"dist/sessions.js",
"dist/space-doc.js",
"dist/task-board.js",
"dist/turn-progress.js",
"dist/turns.js",
Expand Down
17 changes: 15 additions & 2 deletions apps/desktop/renderer/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ import type {
WorkspaceCreateResponse,
WorkspaceInfoResponse,
} from "@roleweave/shared";
import { ChevronsRight, FolderKanban, FolderOpen, Inbox, MessagesSquare, MoreHorizontal, Network, PanelLeft, PencilLine, Plus, Settings, Undo2, X } from "lucide-react";
import { Boxes, ChevronsRight, FolderKanban, FolderOpen, Inbox, MessagesSquare, MoreHorizontal, Network, PanelLeft, PencilLine, Plus, Settings, Undo2, X } from "lucide-react";
import { useThemeMode, useThemeProfile } from "./theme-toggle";
import { useTheme, ThemeProvider } from "./theme-context";
import { themeToAntdSeed } from "./theme-resolution";
Expand Down Expand Up @@ -84,6 +84,7 @@ import { decodeEscapedUnicode } from "./display-text";
import { SettingsModule, type ConfigurationCategory } from "./settings/SettingsModule";
import { GoalsModule } from "./goals/GoalsModule";
import { ProjectManagementModule } from "./projects/ProjectManagementModule";
import { SpaceHost } from "./space/SpaceHost.js";
import { ProjectSwitcher } from "./project/ProjectSwitcher";
import { ProjectWorkspaceDialog } from "./project/ProjectWorkspaceDialog";
import { assignDefaultAvatars, avatarSrcFor, readAvatarPreferences, type AvatarValue } from "./PositionAvatar";
Expand Down Expand Up @@ -135,7 +136,7 @@ function AppInner({
}) {
const themeContext = useTheme();
const [activeModule, setActiveModuleRaw] = useState<
"conversation" | "profile" | "org" | "groups" | "reports" | "approvals" | "docs" | "goals" | "projects" | "settings" | "progress"
"conversation" | "profile" | "org" | "groups" | "reports" | "approvals" | "docs" | "goals" | "projects" | "settings" | "progress" | "space"
>("conversation");
const [settingsInitialCategory, setSettingsInitialCategory] = useState<ConfigurationCategory | undefined>();
const [contactsOpen, setContactsOpen] = useState(false);
Expand Down Expand Up @@ -1971,6 +1972,7 @@ function AppInner({
{ id: "collaboration", label: t("rail.collaboration"), icon: <MessagesSquare aria-hidden="true" size={16} />, active: collaborationActive, onSelect: () => setActiveModule("conversation") },
{ id: "org", label: t("rail.org"), icon: <Network aria-hidden="true" size={16} />, active: activeModule === "org", onSelect: () => setActiveModule("org") },
{ id: "projects", label: t("rail.projects"), icon: <FolderKanban aria-hidden="true" size={16} />, active: projectsActive, onSelect: () => setActiveModule("projects") },
{ id: "space", label: t("rail.space"), icon: <Boxes aria-hidden="true" size={16} />, active: activeModule === "space", onSelect: () => setActiveModule("space") },
{ id: "inbox", label: t("rail.inbox"), icon: <Badge count={pendingApprovals} size="small" showZero={false} offset={[6, -2]} color="var(--ui-primary)"><Inbox aria-hidden="true" size={16} /></Badge>, active: inboxActive, onSelect: () => { setActiveModule("approvals"); void approvalState.refresh(); } },
]}
footer={<>
Expand Down Expand Up @@ -2209,6 +2211,17 @@ function AppInner({
setActiveModule("conversation");
}}
/>
) : activeModule === "space" ? (
<SpaceHost
workspaceOpen={workspaceInfo?.open === true}
spacePositionId={selectedId}
positionNames={positionNames}
ownerPositionId={snapshot?.owner}
onOpenBoundSession={(positionId) => {
selectPosition(positionId);
setActiveModule("conversation");
}}
/>
) : activeModule === "goals" ? (
<GoalsModule
workspaceOpen={workspaceInfo?.open === true}
Expand Down
134 changes: 134 additions & 0 deletions apps/desktop/renderer/src/space/AcceptanceGate.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
import { useMemo, useState } from "react";
import { Button as AntButton, Checkbox, Input } from "antd";
import { CheckCircle2, XCircle } from "lucide-react";
import { useT } from "@roleweave/ui";
import type { AcceptanceDecision, SpaceDocCriterion } from "@roleweave/shared";
import {
buildAcceptanceRecord,
canAccept,
canReject,
untickedCriteria,
type GateDraft,
type GateEvidence,
type GateTicks,
} from "./acceptance-gate-model.js";
import "./space-gate.css";

export interface AcceptanceGateProps {
draft: GateDraft;
criteria: SpaceDocCriterion[];
/** Called with a record that has already passed the model's invariants. */
onDecide: (decision: AcceptanceDecision, record: ReturnType<typeof buildAcceptanceRecord>) => void | Promise<void>;
busy?: boolean;
}

/**
* The single place the human signs off. Accept stays disabled until every
* criterion is ticked, and reject demands a reason — the same invariants
* `acceptance.v1` enforces, surfaced before anything is submitted.
*/
export function AcceptanceGate({ draft, criteria, onDecide, busy = false }: AcceptanceGateProps) {
const t = useT();
const [ticks, setTicks] = useState<GateTicks>({});
const [evidence, setEvidence] = useState<GateEvidence>({});
const [note, setNote] = useState("");
const [pendingDecision, setPendingDecision] = useState<AcceptanceDecision | null>(null);

const remaining = useMemo(() => untickedCriteria(criteria, ticks), [criteria, ticks]);
const acceptReady = canAccept(criteria, ticks);
const rejectReady = canReject(note);

const submit = (decision: AcceptanceDecision) => {
const record = buildAcceptanceRecord(draft, criteria, ticks, decision, note, evidence);
setPendingDecision(null);
void onDecide(decision, record);
};

if (criteria.length === 0) {
return (
<div className="owb-space-gate owb-space-gate--empty">
<p className="owb-space-gate__empty">{t("space.gate.empty")}</p>
</div>
);
}

return (
<div className="owb-space-gate" data-ticked={criteria.length - remaining.length} data-total={criteria.length}>
<div className="owb-space-gate__head">
<h3 className="owb-space-gate__title">{t("space.gate.title")}</h3>
<span className="owb-space-gate__progress">
{t("space.gate.progress", { done: criteria.length - remaining.length, total: criteria.length })}
</span>
</div>

<ul className="owb-space-gate__list">
{criteria.map((criterion) => (
<li key={criterion.criteriaIndex} className="owb-space-gate__item">
<Checkbox
checked={ticks[criterion.criteriaIndex] === true}
onChange={(event) =>
setTicks((prev) => ({ ...prev, [criterion.criteriaIndex]: event.target.checked }))
}
>
{criterion.text}
</Checkbox>
<Input
className="owb-space-gate__evidence"
size="small"
value={evidence[criterion.criteriaIndex] ?? ""}
placeholder={t("space.gate.evidencePlaceholder")}
onChange={(event) =>
setEvidence((prev) => ({ ...prev, [criterion.criteriaIndex]: event.target.value }))
}
/>
</li>
))}
</ul>

{remaining.length > 0 ? (
<p className="owb-space-gate__hint">{t("space.gate.remaining", { count: remaining.length })}</p>
) : null}

<Input.TextArea
className="owb-space-gate__note"
rows={2}
value={note}
placeholder={t("space.gate.notePlaceholder")}
onChange={(event) => setNote(event.target.value)}
/>

<div className="owb-space-gate__actions">
{pendingDecision === null ? (
<>
<AntButton
type="primary"
icon={<CheckCircle2 size={14} aria-hidden="true" />}
disabled={!acceptReady || busy}
onClick={() => setPendingDecision("accepted")}
>
{t("space.gate.accept")}
</AntButton>
<AntButton
danger
icon={<XCircle size={14} aria-hidden="true" />}
disabled={!rejectReady || busy}
onClick={() => setPendingDecision("rejected")}
>
{t("space.gate.reject")}
</AntButton>
</>
) : (
<>
<span className="owb-space-gate__confirm">
{pendingDecision === "accepted" ? t("space.gate.confirmAccept") : t("space.gate.confirmReject")}
</span>
<AntButton type="primary" loading={busy} onClick={() => submit(pendingDecision)}>
{t("space.gate.confirm")}
</AntButton>
<AntButton onClick={() => setPendingDecision(null)}>{t("space.gate.cancel")}</AntButton>
</>
)}
</div>
</div>
);
}
60 changes: 60 additions & 0 deletions apps/desktop/renderer/src/space/SpaceHost.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { useMemo } from "react";
import { useT } from "@roleweave/ui";
import type { AcceptanceRecord } from "@roleweave/shared";
import { SpaceModule, type SpaceAgent } from "./SpaceModule.js";
import { useSpaceFiles } from "./useSpaceFiles.js";

export interface SpaceHostProps {
workspaceOpen: boolean;
/** The position whose package directory backs the current space, if any. */
spacePositionId: string | null;
positionNames: Record<string, string>;
ownerPositionId?: string;
onOpenBoundSession?: (positionId: string) => void;
onAccept?: (record: AcceptanceRecord) => void | Promise<void>;
}

/**
* Bridges the space module to the shell: resolves the current space's files
* through the existing position-docs bridge and derives the participant list
* from the org, so the module itself stays a pure view.
*/
export function SpaceHost({
workspaceOpen,
spacePositionId,
positionNames,
ownerPositionId,
onOpenBoundSession,
onAccept,
}: SpaceHostProps) {
const t = useT();
const state = useSpaceFiles(spacePositionId, workspaceOpen);

const participants = useMemo<SpaceAgent[]>(() => {
const ids = Object.keys(positionNames);
// The owner always comes first and is marked; it is an org-level fact.
const ordered = ownerPositionId && ids.includes(ownerPositionId)
? [ownerPositionId, ...ids.filter((id) => id !== ownerPositionId)]
: ids;
return ordered.map((positionId) => ({
positionId,
name: positionNames[positionId] ?? positionId,
owner: positionId === ownerPositionId,
}));
}, [positionNames, ownerPositionId]);

return (
<SpaceModule
key={spacePositionId ?? "none"}
workspaceOpen={workspaceOpen}
spaceId={spacePositionId ?? undefined}
spaceName={spacePositionId ? (positionNames[spacePositionId] ?? spacePositionId) : t("space.untitled")}
files={state.files}
readFile={state.readFile}
writeFile={state.writeFile}
participants={participants}
onOpenBoundSession={onOpenBoundSession}
onAccept={onAccept}
/>
);
}
Loading
Loading