diff --git a/CHANGELOG.md b/CHANGELOG.md index 0dd373e3..8e0a4e35 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,10 @@ ### Added +- #617:接力任务支持可选本步要求,以及与模型输入一致的交接快照;可查看前序结果来源、裁剪和过滤提示,共享现有上下文预算并去除重复来源。新增本地记录字段,降级旧版本前请备份工作区。 + +### Added + - #615: Show per-task execution progress in project cards, schedule rows, and the task editor. Reuse live stage events and exact persisted snapshots, recover on reconnect, and keep completed execution separate from delivery acceptance. - #466:审批队列和详情展示可选 Jev 老化建议(`nudge` / `wait` / `discuss-close`),仅为文案。出站载荷只有 `{ createdAt, now, decision.kind, risk }`。overlay 不得隐藏 pending high,不得 POST decision,点击只打开事项。过期仍走现有 clock helpers。`ROLEWEAVE_LAYA_ENABLED` 默认关。https://github.com/bytefolk/roleweave/issues/422 diff --git a/apps/desktop/packaging/runtime-layout.cjs b/apps/desktop/packaging/runtime-layout.cjs index d3e7c4fc..7dc8e709 100644 --- a/apps/desktop/packaging/runtime-layout.cjs +++ b/apps/desktop/packaging/runtime-layout.cjs @@ -166,6 +166,7 @@ const SERVER_RUNTIME_FILES = [ "dist/src/turns/delta-forwarder.js", "dist/src/turns/envelope.js", "dist/src/turns/progress.js", + "dist/src/turns/relay-handoff.js", "dist/src/turns/response-format.js", "dist/src/turns/running.js", "dist/src/turns/store.js", @@ -194,6 +195,7 @@ const SHARED_RUNTIME_FILES = [ "pending-approval.cjs", "position-id.cjs", "turn-engines.cjs", + "relay-steps.cjs", "dist/acceptance.js", "dist/agent-binding.js", "dist/api.js", @@ -223,6 +225,7 @@ const SHARED_RUNTIME_FILES = [ "dist/position-id.js", "dist/position-profile.js", "dist/relationship-graph.js", + "dist/relay-steps.js", "dist/semantic-runtime.js", "dist/services.js", "dist/sessions.js", @@ -325,6 +328,9 @@ const APP_RESOURCE_REQUIRED_ENTRIES = [ "node_modules/@roleweave/shared/position-id.cjs", "node_modules/@roleweave/shared/pending-approval.cjs", "node_modules/@roleweave/shared/turn-engines.cjs", + "node_modules/@roleweave/shared/relay-steps.cjs", + "node_modules/@roleweave/shared/dist/relay-steps.js", + "apps/server/dist/src/turns/relay-handoff.js", "examples/oss-maintainer/workspace.json", "examples/oss-maintainer/organization.v1alpha1.json", ]; diff --git a/apps/desktop/renderer/src/app.css b/apps/desktop/renderer/src/app.css index 96145328..b200f03a 100644 --- a/apps/desktop/renderer/src/app.css +++ b/apps/desktop/renderer/src/app.css @@ -9110,3 +9110,17 @@ body, @media (prefers-reduced-motion: reduce) { .owb-turn-panel *, .owb-org-module__splitter::before { animation: none !important; transition: none !important; } } .owb-turn-panel .owb-tc-head__eng { max-width: 100%; white-space: normal; overflow-wrap: anywhere; } + +/* Bounded relay notes and optional per-step requirements. */ +.owb-relay-handoff, .owb-relay-instructions { min-width: 0; margin-block: 8px; } +.owb-relay-handoff summary, .owb-relay-instructions summary { cursor: pointer; overflow-wrap: anywhere; } +.owb-relay-handoff__body { max-height: 420px; overflow: auto; padding: 8px 0; } +.owb-relay-handoff dl { margin: 8px 0; } +.owb-relay-handoff dt { font-weight: 600; } +.owb-relay-handoff dd { margin: 4px 0 12px; white-space: pre-wrap; overflow-wrap: anywhere; } +.owb-relay-handoff section { border-top: 1px solid var(--ui-border); padding-block: 10px; } +.owb-relay-handoff pre { white-space: pre-wrap; overflow-wrap: anywhere; font: inherit; } +.owb-relay-handoff code { overflow-wrap: anywhere; } +.owb-relay-instructions__fields { display: grid; gap: 10px; max-height: 240px; overflow: auto; padding: 8px 0; } +.owb-relay-instructions__fields label { display: grid; gap: 4px; min-width: 0; } +.owb-relay-instructions small { color: var(--ui-foreground-muted); } diff --git a/apps/desktop/renderer/src/groups/GroupsPanel.tsx b/apps/desktop/renderer/src/groups/GroupsPanel.tsx index 5d7381a6..408cc149 100644 --- a/apps/desktop/renderer/src/groups/GroupsPanel.tsx +++ b/apps/desktop/renderer/src/groups/GroupsPanel.tsx @@ -3,6 +3,8 @@ import { createPortal } from "react-dom"; import { Button as AntButton, Input, Select as AntSelect } from "antd"; import { ArrowUp, Plus, Search, Trash2, UserRound, UserRoundPlus, UsersRound } from "lucide-react"; import { useOwbLocale, useT } from "@roleweave/ui"; +import { RelayHandoffDetails } from "./RelayHandoffDetails"; +import { instructionBytes, selectedRelaySteps, relayStepsTooLarge, RELAY_INSTRUCTION_BYTES } from "./relay-instructions"; import { PositionAvatar } from "../PositionAvatar"; import { DiagnosticNotice, type AvailabilityCheck } from "../DiagnosticNotice"; import { ProgressTrail } from "../turns/TurnThread"; @@ -208,8 +210,8 @@ export function GroupsPanel({ const [createOpen, setCreateOpen] = useState(false); const [draftMembers, setDraftMembers] = useState>(new Set()); const draftKey = selectedRef ?? ""; - const [drafts, setDrafts] = useState>({}); - const emptyDraft = { input: "", mentions: [] as string[], mode: "parallel" as const }; + const [drafts, setDrafts] = useState }>>({}); + const emptyDraft = { input: "", mentions: [] as string[], mode: "parallel" as const, instructions: {} as Record }; const draft = drafts[draftKey] ?? emptyDraft; const input = draft.input; const mentions = useMemo(() => new Set(draft.mentions), [draft.mentions]); @@ -230,6 +232,12 @@ export function GroupsPanel({ ); const mentionsReady = unavailableMention === undefined; const dispatchMode = draft.mode; + const relaySteps = selectedRelaySteps(mentionedPositionIds, draft.instructions); + const invalidRelaySteps = dispatchMode === "relay" && relayStepsTooLarge(relaySteps); + const setStepInstruction = (positionId: string, instruction: string) => setDrafts(current => { + const previous = current[draftKey] ?? emptyDraft; + return { ...current, [draftKey]: { ...previous, instructions: { ...previous.instructions, [positionId]: instruction } } }; + }); const setInput = (input: string) => setDrafts((current) => ({ ...current, [draftKey]: { ...(current[draftKey] ?? emptyDraft), input } })); const setMentions = (mentions: ReadonlySet) => setDrafts((current) => ({ ...current, [draftKey]: { ...(current[draftKey] ?? emptyDraft), mentions: [...mentions] } })); const setDispatchMode = (mode: "parallel" | "relay") => setDrafts((current) => ({ ...current, [draftKey]: { ...(current[draftKey] ?? emptyDraft), mode } })); @@ -543,7 +551,7 @@ export function GroupsPanel({ const ref = selectedRefRef.current; const trimmed = input.trim(); if ( - ref === null || trimmed.length === 0 || mentions.size === 0 || !mentionsReady || + ref === null || trimmed.length === 0 || mentions.size === 0 || !mentionsReady || invalidRelaySteps || sendingGroupsRef.current.has(ref) ) return; const requestEngine = mentionEngines[mentionedPositionIds[0]!] ?? engine; @@ -560,6 +568,7 @@ export function GroupsPanel({ engines: mentionEngines, mentions: mentionedPositionIds, mode: dispatchMode, + ...(dispatchMode === "relay" && relaySteps.length ? { relaySteps } : {}), }); if (!isCurrent()) return; if (res.status !== 202) { @@ -572,7 +581,7 @@ export function GroupsPanel({ spawns: Array<{ turnId: string; positionId: string; engine?: TurnEngine }>; }; onSpawnRuns(ref, body.messageId, body.spawns, trimmed, requestEngine); - setDrafts((current) => current[ref]?.input === input ? { ...current, [ref]: { ...current[ref]!, input: "", mentions: [] } } : current); + setDrafts((current) => current[ref] === draft ? { ...current, [ref]: { ...current[ref]!, input: "", mentions: [], instructions: {} } } : current); void loadTimeline(ref); } catch { if (isCurrent() && selectedRefRef.current === ref) setPanelError(t("grp.turnFailOffline")); @@ -585,6 +594,9 @@ export function GroupsPanel({ }, [ captureScope, dispatchMode, + draft, + invalidRelaySteps, + relaySteps, engine, input, loadTimeline, @@ -946,6 +958,7 @@ export function GroupsPanel({ )} {isLive || !["group_relay_blocked", "group_relay_stopped"].includes(turn.errorCode ?? "") ? : null} + {"relayHandoff" in turn && turn.relayHandoff ? : null} {turn.output ? ( {dispatchMode === "relay" ?

{t("grp.relayOrder", { order: [...mentions].map(displayPositionName).join(" → ") })}

: null} + {dispatchMode === "relay" && mentions.size > 0 ?
+ {t("grp.relayInstructions")} +
+ {mentionedPositionIds.map((positionId, index) => )} +
+
: null} + {invalidRelaySteps ?

{t("grp.relayInstructionLimit")}

: null}