feat: implement Wiki Compose P2 orchestrator graph and UI (#950)#959
Conversation
Backend (server/api/src/agents/graphs/wikiCompose/): - wikiComposeGraph orchestrator that walks Brief → Research → Structure → Draft → Completed in one LangGraph. State extends ResearchLoopState as a strict superset so the P1 research loop composes inline; interrupts at the three HITL points (brief / research / outline) halt the same thread_id. - new nodes: briefDialogue (0..7 structured questions), humanReviewBrief, structureDialogue (3..10 section outline), humanReviewOutline, draftSections (sequential per-section LLM streaming via model.stream), completed (markdown assembly + citation collation). - resume payload validators (briefResumeSchema, outlineResumeSchema) reject malformed payloads at the node boundary. - new SSE events compose_phase / compose_section so the frontend can drive the phase stepper + per-section streaming without inspecting state. - composeSessions route bumps recursion limit to 120 for the orchestrator (Brief + Research up to 5x6 nodes + Structure + up to 10 draft sections). Frontend (src/): - /notes/:noteId/:pageId/compose and /compose/:sessionId routes mount the new WikiComposePage (split-screen: left EditorPane with live section preview, right ComposePanel with PhaseStepper + Brief/Research/Outline sections + Activity timeline). Mobile uses vertical split. - useWikiComposeSession hook owns the SSE wiring and state machine. - composeService provides REST + SSE clients with a spec-compliant SSE parser (handles multi-chunk records, comments, multi-line data, aborts). - WikiGeneratorButton gains a composeHref mode that navigates to /compose and stays visible on pages that already have content (Compose supports append). Legacy inline-generation path unchanged when composeHref is absent. Tests: - vitest: orchestrator wiring + 3 interrupt points pinned with MemorySaver; SSE custom-event mapper extensions; SSE parser edge cases; hook state reductions for Brief / Draft / submitBrief flows; PhaseStepper a11y. - playwright (e2e/wiki-compose.spec.ts): full happy-path with mocked SSE routes — Compose entry → Brief submit → research approval → outline approval → completed Draft → back to /notes. Issue: #950
Qodo reviews are paused for this user.Troubleshooting steps vary by plan Learn more → On a Teams plan? Using GitHub Enterprise Server, GitLab Self-Managed, or Bitbucket Data Center? |
📝 WalkthroughWalkthroughThis PR implements the complete Wiki Compose P2 orchestration system—a multi-phase LangGraph workflow that guides users through question-and-answer briefs, research source approval, outline editing, and streamed content drafting. The feature includes server-side orchestration graph nodes with human interrupts, frontend session management via React hooks, a split-pane UI for preview and controls, and full E2E test coverage. ChangesWiki Compose P2: Backend Graph & Frontend Integration
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related issues
Possibly related PRs
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
|
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 7de1b5c085
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| const result = await resumeSession({ pageId, sessionId: session.id, resume: input }); | ||
| update({ status: result.status }); | ||
| if (result.status === "interrupted" || result.status === "running") { | ||
| await streamRun(session); |
There was a problem hiding this comment.
Consume completed resume output after outline approval
When submitOutline resumes the orchestrator, the backend can legitimately return status: "completed" (there are no further interrupts after human_review_outline). This branch only updates status and skips both streamRun and result.output handling unless status is interrupted/running, so the UI never receives draft/completion data in the real API path and ends with an empty final preview/state. Handle the completed case by projecting result.output (or by explicitly reopening a stream when required) so completion data is surfaced.
Useful? React with 👍 / 👎.
| const [appendToExisting, setAppendToExisting] = useState<boolean>( | ||
| Boolean(pageSnapshot?.hasContent), | ||
| ); |
There was a problem hiding this comment.
Re-sync append mode after snapshot loads
appendToExisting is initialized from pageSnapshot only once at mount, but pageSnapshot is populated asynchronously after the first Brief interrupt. On pages that already have content, this leaves the radio default stuck at false (replace) instead of append, so users can unintentionally submit replace mode unless they manually toggle it. Sync this state when pageSnapshot.hasContent becomes available.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Code Review
This pull request introduces the Wiki Compose P2 orchestrator, establishing a multi-phase workflow for AI-assisted wiki authoring. It includes a LangGraph backend, a split-screen React UI with real-time SSE streaming, and associated tests. Review feedback highlights the need to update the frontend state with user-edited outlines to ensure correct display order during drafting. Other suggestions include refining heading levels in the Markdown preview for consistency, improving error reporting during section generation, and cleaning up redundant logic in the UI preview.
| update({ status: result.status }); | ||
| if (result.status === "interrupted" || result.status === "running") { | ||
| await streamRun(session); | ||
| } | ||
| }, |
There was a problem hiding this comment.
submitOutline でユーザーが編集・確定したアウトラインを outlineProposal ステートに反映していません。これにより、執筆フェーズ(Draft)中の EditorPane での表示順序が古いままになったり、ユーザーが追加したセクションが表示されなかったりする問題が発生します。また、完了時の completedMarkdown の計算でも誤った順序が使用されます。
update({ outlineProposal: input.sections });
const result = await resumeSession({ pageId, sessionId: session.id, resume: input });
update({ status: result.status });
if (result.status === "interrupted" || result.status === "running") {
await streamRun(session);
}| // what happened without losing earlier sections. | ||
| // セクション 1 件の失敗で Draft 全体を止めない。エラーは本文に追記。 | ||
| const message = err instanceof Error ? err.message : String(err); | ||
| body = body || `*(Section draft failed: ${message})*`; |
| const sections = state.outlineProposal | ||
| .map((s) => state.draftedSections[s.id]) | ||
| .filter((s): s is DraftedSection => Boolean(s)); | ||
| if (sections.length === 0) return null; | ||
| return sections.map((s) => `## ${s.heading}\n\n${s.body}`).join("\n\n"); |
There was a problem hiding this comment.
completedMarkdown の組み立てにおいて、見出しレベルが ## に固定されています。バックエンドの completed.ts や EditorPane.tsx のプレビュー実装と合わせるため、section.depth に応じて見出しレベル(H2 または H3)を切り替えるべきです。また、outlineProposal を直接走査することで、最新の構成順序を確実に反映できます。
const lines = state.outlineProposal
.map((section) => {
const drafted = state.draftedSections[section.id];
if (!drafted) return null;
const level = section.depth === 1 ? "##" : "###";
return `${level} ${drafted.heading}\n\n${drafted.body}`;
})
.filter((t): t is string => t !== null);
if (lines.length === 0) return null;
return lines.join("\n\n");| const outlineForPreview = | ||
| session.phase === "completed" || session.phase === "draft" | ||
| ? session.outlineProposal | ||
| : session.outlineProposal; |
There was a problem hiding this comment.
outlineForPreview の決定ロジックが冗長で、どちらの条件でも session.outlineProposal を返しています。意図した実装が漏れているか、単なるタイポである可能性があるため、簡略化を検討してください。
| const outlineForPreview = | |
| session.phase === "completed" || session.phase === "draft" | |
| ? session.outlineProposal | |
| : session.outlineProposal; | |
| const outlineForPreview = session.outlineProposal; |
There was a problem hiding this comment.
Actionable comments posted: 19
Note
Due to the large number of review comments, Critical, Major severity comments were prioritized as inline comments.
🟡 Minor comments (4)
server/api/src/agents/graphs/wikiCompose/nodes/humanReviewOutline.ts-19-23 (1)
19-23:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winAdd TSDoc comment for the exported function.
The
humanReviewOutlinefunction is exported but lacks a TSDoc comment block. As per coding guidelines, all exported functions must include TSDoc/JSDoc documentation.📝 Add TSDoc comment
+/** + * Human review outline interrupt node. + * アウトライン承認のための human-in-the-loop 中断ノード。 + * + * Packages the proposed outline and approved research sources into an interrupt + * payload, halts execution until the user resumes with validated outline edits, + * then projects the approved outline into state. + * + * `@param` state - Current Wiki Compose state / 現在の Wiki Compose 状態 + * `@param` _config - LangGraph runnable config (unused) / LangGraph 実行設定(未使用) + * `@returns` State update with approved outline / 承認済みアウトラインを含む状態更新 + */ export async function humanReviewOutline( state: WikiComposeStateType, _config: LangGraphRunnableConfig, ): Promise<WikiComposeStateUpdate> {As per coding guidelines: "Include TSDoc/JSDoc comments for all exported functions, types, and interfaces."
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@server/api/src/agents/graphs/wikiCompose/nodes/humanReviewOutline.ts` around lines 19 - 23, The exported function humanReviewOutline lacks a TSDoc block; add a concise TSDoc comment above the function describing its purpose, parameters, and return type—mention that it handles the "human_review_outline" node behavior (interrupt + resume projection), document the parameters state: WikiComposeStateType and _config: LangGraphRunnableConfig, and the returned Promise<WikiComposeStateUpdate>; include any relevant tags like `@param` and `@returns` for clarity.server/api/src/agents/graphs/wikiCompose/nodes/shared/dispatch.ts-12-41 (1)
12-41:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winAdd TSDoc comments for exported interfaces and functions.
The exported
ComposePhasePayloadandComposeSectionPayloadinterfaces, along with thedispatchComposePhaseanddispatchComposeSectionfunctions, lack TSDoc comments. As per coding guidelines, all exported types and functions should include TSDoc/JSDoc documentation.📝 Add TSDoc comments
+/** + * Payload shape for `compose_phase` custom events. + * `compose_phase` カスタムイベント用ペイロード形状。 + */ export interface ComposePhasePayload { phase: "brief" | "research" | "structure" | "draft" | "completed"; status: "entered" | "completed"; } +/** + * Payload shape for `compose_section` custom events. + * `compose_section` カスタムイベント用ペイロード形状。 + */ export interface ComposeSectionPayload { sectionId: string; heading: string; status: "started" | "completed"; index: number; total: number; } -/** Dispatch a `compose_phase` SSE custom event. */ +/** + * Dispatch a `compose_phase` SSE custom event. + * `compose_phase` SSE カスタムイベントを発火する。 + * + * `@param` payload - Phase transition payload / フェーズ遷移ペイロード + * `@param` config - LangGraph runnable config / LangGraph 実行設定 + */ export async function dispatchComposePhase( payload: ComposePhasePayload, config: LangGraphRunnableConfig, ): Promise<void> { await dispatchCustomEvent("compose_phase", payload, config); } -/** Dispatch a `compose_section` SSE custom event. */ +/** + * Dispatch a `compose_section` SSE custom event. + * `compose_section` SSE カスタムイベントを発火する。 + * + * `@param` payload - Section drafting payload / セクション執筆ペイロード + * `@param` config - LangGraph runnable config / LangGraph 実行設定 + */ export async function dispatchComposeSection( payload: ComposeSectionPayload, config: LangGraphRunnableConfig, ): Promise<void> { await dispatchCustomEvent("compose_section", payload, config); }As per coding guidelines: "Include TSDoc/JSDoc comments for all exported functions, types, and interfaces."
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@server/api/src/agents/graphs/wikiCompose/nodes/shared/dispatch.ts` around lines 12 - 41, Add TSDoc comments for the exported interfaces and functions: document ComposePhasePayload and ComposeSectionPayload with a short description for each field (phase/status, sectionId, heading, index, total), and add TSDoc for dispatchComposePhase and dispatchComposeSection describing their purpose, parameters (payload: ComposePhasePayload | ComposeSectionPayload and config: LangGraphRunnableConfig) and return type Promise<void>; place the comments immediately above the respective declarations (ComposePhasePayload, ComposeSectionPayload, dispatchComposePhase, dispatchComposeSection) following project TSDoc style.src/pages/WikiComposePage.tsx-128-131 (1)
128-131:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winClose action is disabled in the completed state.
When status is completed, the label becomes “Close” but the button is disabled, so users can’t use that action.
🐛 Proposed fix
- disabled={session.status === "completed" || session.status === "cancelled"} + disabled={session.status === "cancelled"}🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/pages/WikiComposePage.tsx` around lines 128 - 131, The button rendering the X icon currently sets disabled when session.status === "completed" || session.status === "cancelled", which hides the intended "Close" action; change the condition so the button is only disabled for the "cancelled" state (e.g., disabled={session.status === "cancelled"}) so that when session.status === "completed" the "Close" label/button remains enabled; ensure any click handler (the function that handles cancel/close near this button) correctly differentiates session.status === "completed" to perform the close flow.server/api/src/agents/runner/sseMapper.ts-261-277 (1)
261-277:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winHarden
compose_sectionnumeric validation before emitting SSE.
typeof === "number"currently allowsNaN, fractional, negative, and inconsistent values (index > total), which can corrupt section progress UI state.Suggested fix
function mapComposeSection(data: Record<string, unknown>): SseComposeSectionEvent[] { const sectionId = typeof data.sectionId === "string" ? data.sectionId : null; const heading = typeof data.heading === "string" ? data.heading : null; const status = data.status === "started" || data.status === "completed" ? data.status : null; - const index = typeof data.index === "number" ? data.index : null; - const total = typeof data.total === "number" ? data.total : null; + const index = typeof data.index === "number" ? data.index : null; + const total = typeof data.total === "number" ? data.total : null; if ( sectionId === null || heading === null || status === null || index === null || total === null ) { return []; } + if (!Number.isInteger(index) || !Number.isInteger(total)) return []; + if (index < 1 || total < 1 || index > total) return []; return [{ type: "compose_section", sectionId, heading, status, index, total }]; }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@server/api/src/agents/runner/sseMapper.ts` around lines 261 - 277, mapComposeSection currently accepts any typeof number (including NaN, fractions, negatives, infinities) for index and total; update the validation in mapComposeSection so index and total are finite integers (use Number.isInteger and isFinite/!Number.isNaN), non-negative, and that index <= total (and total >= 0, or total > 0 if you require at least one item) before emitting the SseComposeSectionEvent; keep the existing checks for sectionId, heading, and status, and return [] if any of these numeric checks fail to avoid corrupting the compose progress UI.
🧹 Nitpick comments (4)
server/api/src/agents/graphs/wikiCompose/nodes/completed.ts (1)
31-46: ⚡ Quick winConsider merging the two outline loops.
The code iterates
outlinetwice: once to buildordered(lines 31-35) and again to buildlines(lines 37-46). This results in duplicatedraftById.get(section.id)lookups and unnecessary passes over the same data.♻️ Merge loops to reduce redundant lookups
const ordered: DraftedSection[] = []; + const lines: string[] = []; for (const section of outline) { const drafted = draftById.get(section.id); - if (drafted) ordered.push(drafted); - } - - const lines: string[] = []; - for (const section of outline) { - const drafted = draftById.get(section.id); if (!drafted) continue; + ordered.push(drafted); const prefix = "#".repeat(Math.min(3, Math.max(2, section.depth + 1))); lines.push(`${prefix} ${section.heading}`); lines.push(""); lines.push(drafted.body); lines.push(""); }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@server/api/src/agents/graphs/wikiCompose/nodes/completed.ts` around lines 31 - 46, The two loops over outline (which build ordered and then lines) perform duplicate draftById.get lookups; merge them by removing the separate ordered array and, in a single pass over outline, retrieve drafted via draftById.get(section.id) once, skip if missing, then push to ordered if needed and append the heading/prefix and drafted.body to lines (use the existing prefix calculation using section.depth and section.heading). Update references to ordered accordingly or eliminate ordered if it's only used here.server/api/src/agents/graphs/wikiCompose/nodes/shared/loadPageSnapshot.ts (1)
45-49: 💤 Low valueConsider logging errors in the catch block for observability.
The catch block silently swallows all errors and returns an empty snapshot. While this is intentionally defensive (per the comment), it makes debugging DB issues harder. Adding a log statement would improve observability without changing behavior.
🔍 Add error logging
- } catch { + } catch (err) { // Defence in depth: a transient DB error must not crash the whole graph. // The Brief node tolerates an empty snapshot (it just asks broader questions). + console.error("loadPageSnapshot: failed to fetch page snapshot", { pageId, error: err }); return emptySnapshot(pageId); }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@server/api/src/agents/graphs/wikiCompose/nodes/shared/loadPageSnapshot.ts` around lines 45 - 49, Update the catch block inside loadPageSnapshot to capture the thrown error (change catch to catch (err)) and log it before returning emptySnapshot(pageId); for example call a logger if available (e.g., logger.error or processLogger.error) or console.error with a short contextual message plus the err object so DB/transient errors are recorded while preserving the current behavior of returning emptySnapshot(pageId).src/pages/WikiComposePage.tsx (1)
62-65: ⚡ Quick winRemove the no-op phase switch (or wire the intended outline source).
This ternary always returns
session.outlineProposal, so it currently doesn’t switch anything and makes the phase intent ambiguous.♻️ Proposed cleanup
- const outlineForPreview = - session.phase === "completed" || session.phase === "draft" - ? session.outlineProposal - : session.outlineProposal; + const outlineForPreview = session.outlineProposal;🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/pages/WikiComposePage.tsx` around lines 62 - 65, The ternary assigned to outlineForPreview is a no-op (always returns session.outlineProposal); replace it with a single direct assignment or wire in the intended source for the "completed" phase. Either change the line to const outlineForPreview = session.outlineProposal; or, if completion should use a different field, pick that field (e.g., session.finalOutline or session.outline) when session.phase === "completed" and keep session.outlineProposal for draft/other phases—update the outlineForPreview declaration accordingly.src/components/wikiCompose/BriefQuestionCard.tsx (1)
16-20: ⚡ Quick winAdd JSDoc for exported
BriefQuestionCardProps.This exported interface should have a dedicated TSDoc/JSDoc block to satisfy the exported-symbol documentation rule.
📝 Proposed fix
+/** + * `BriefQuestionCard` の入力 props。 + * Props consumed by `BriefQuestionCard`. + */ export interface BriefQuestionCardProps { question: BriefQuestion; answer: BriefAnswer | null; onChange: (next: BriefAnswer) => void; }As per coding guidelines "
**/*.{ts,tsx,js,jsx}: Include TSDoc/JSDoc comments for all exported functions, types, and interfaces".🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/components/wikiCompose/BriefQuestionCard.tsx` around lines 16 - 20, Add a TSDoc/JSDoc comment block for the exported interface BriefQuestionCardProps that describes the purpose of the interface and documents each property (question: BriefQuestion, answer: BriefAnswer | null, onChange: (next: BriefAnswer) => void); place the comment directly above the interface declaration and include short descriptions for the interface and each field (use `@property` or plain TSDoc lines), referencing the types BriefQuestion and BriefAnswer so the exported-symbol documentation rule is satisfied.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@e2e/wiki-compose.spec.ts`:
- Around line 101-109: The test mock for the "human_review_research" interrupt
currently uses payload.batch (an object with id, iteration, queries, etc.) but
should match the orchestrator contract which expects payload to include batchId
(string) and pendingSources (array); update the mock "payload" object to replace
the nested "batch" with batchId: "batch-1" and pendingSources: [] and
remove/translate any fields (iteration/queries/sources/evaluation) to conform to
the contract; do the same for the other occurrence of this mock in the spec so
both fixtures use the canonical shape for human_review_research.
In `@server/api/src/agents/graphs/wikiCompose/nodes/humanReviewBrief.ts`:
- Around line 61-73: The node currently trusts briefResumeSchema.parse and
builds answers without enforcing that all required questions in
state.briefQuestions are present, so fix by validating after parsing: iterate
state.briefQuestions (use questionMap and symbols state.briefQuestions,
questionMap, parsed, answers, BriefAnswer) to collect required question ids,
ensure parsed.answers includes an entry for each required id and that each
required answer contains the necessary data (selectedOptionIds non-empty or
freeText present depending on the question type), and if any required question
is missing or empty throw/return a validation error before continuing; update
the creation of answers to only proceed after this required-question check.
In `@server/api/src/agents/graphs/wikiCompose/resumeSchemas.ts`:
- Around line 56-60: The schema currently uses heading: z.string().min(1) which
permits whitespace-only strings; change the heading validator to reject strings
that trim to empty by replacing it with a refinement such as heading:
z.string().min(1).refine(s => s.trim().length > 0, { message: "Heading cannot be
empty or whitespace" }) (or the equivalent z.string().refine(...)). Update the
heading entry in the resume schema (the object containing id, heading, depth,
intent, sourceIds) so whitespace-only headings are rejected at the schema
boundary.
In `@src/components/wikiCompose/ActivitySection.tsx`:
- Around line 17-20: Add a JSDoc/TSDoc comment for the exported interface
ActivitySectionProps describing the purpose of the interface and its properties:
document that ActivitySectionProps contains an array of ComposeActivity (explain
what ComposeActivity represents if not obvious) under the `activity` field and a
boolean `isStreaming` indicating streaming state; place the JSDoc immediately
above the ActivitySectionProps declaration so tools and consumers pick it up.
- Around line 35-36: Add a proper TSDoc/JSDoc block above the exported
ActivitySection component describing the component purpose, its props
(ActivitySectionProps) and each prop (activity, isStreaming), the React.FC
return type, and any important behavior (e.g., streaming UI rendering). Mention
prop types and whether they are optional, include a short example usage if
helpful, and ensure the comment sits immediately above the ActivitySection
export.
In `@src/components/wikiCompose/ComposePanel.tsx`:
- Around line 27-53: The exported interface ComposePanelProps is missing a
TSDoc/JSDoc comment; add a JSDoc block above the export that briefly describes
the purpose of ComposePanelProps and documents each property (phase,
isStreaming, briefQuestions, pageSnapshot, latestBatch, pendingSources,
approvedSources, outlineProposal, activity) and the three callback signatures
(onSubmitBrief, onSubmitResearchApproval, onSubmitOutline) including parameter
shapes and return type; ensure to note nullable fields (e.g., pageSnapshot,
latestBatch) and any optional callback parameters (appendToExisting,
researchMaxIterations, rejectedSourceIds, note) so consumers and IDE tooltips
have clear type/usage info for ComposePanelProps.
- Around line 55-70: Add a proper TSDoc/JSDoc block above the exported
ComposePanel component describing its purpose, the ComposePanelProps type, and
summarizing key props/behavior (e.g., phase, isStreaming, briefQuestions,
pageSnapshot, latestBatch, pendingSources, approvedSources, outlineProposal,
activity and the callback props onSubmitBrief, onSubmitResearchApproval,
onSubmitOutline). Include `@param` annotations for the props object or for
important props, and an `@returns` annotation describing it returns a React.FC
element; mention any side effects like streaming UI or submission handlers if
relevant.
In `@src/components/wikiCompose/DialogueSection.tsx`:
- Around line 68-70: The append/replace mode is initialized from pageSnapshot
only once, so appendToExisting can be stale if pageSnapshot changes; update the
logic to sync state whenever pageSnapshot updates by adding an effect that sets
setAppendToExisting(Boolean(pageSnapshot?.hasContent)) whenever pageSnapshot
changes (and do the same fix for the other similar state hooks referenced around
the 110-138 and 169-176 sections), ensuring the component derives its
append/replace mode from the latest pageSnapshot rather than only on mount.
In `@src/components/wikiCompose/EditorPane.tsx`:
- Around line 17-25: Add a TSDoc/JSDoc comment for the exported interface
EditorPaneProps: above the EditorPaneProps declaration write a concise /** ...
*/ block describing the purpose of the interface and document each exported
property (title, outline, draftedSections, sectionBuffers, streamingSectionId,
completedMarkdown) with `@property` or inline param descriptions and mention
nullable/optional semantics where relevant so the interface follows the
project's exported type documentation guidelines.
- Around line 27-35: Add a JSDoc/TSDoc block for the exported React component
EditorPane describing its purpose and parameters/props; update the comment
immediately above the `export const EditorPane: React.FC<EditorPaneProps> = ({
... }) => {` declaration to include a short description and `@param` entries (or
`@props`) for `title`, `outline`, `draftedSections`, `sectionBuffers`,
`streamingSectionId`, and `completedMarkdown`, plus an `@returns` describing the
rendered JSX, matching project doc style.
In `@src/components/wikiCompose/OutlineEditor.tsx`:
- Around line 39-41: The effect in OutlineEditor that runs React.useEffect(() =>
setSections(initialSections), [initialSections]) can overwrite in-progress
edits; change it to only sync props into state when the editor is first mounted
or when the user has not started editing by adding a local "hasUserEdited" (or
"isDirty") flag/ref that you set on the first local edit and then guard the
effect to call setSections(initialSections) only if hasUserEdited is false (or
only on initial mount using a "didInit" ref); update handlers that mutate
sections to mark the flag true so subsequent prop changes won't clobber edits.
In `@src/components/wikiCompose/PhaseStepper.tsx`:
- Around line 27-30: Add a JSDoc/TSDoc block above the exported interface
PhaseStepperProps that documents the purpose of the interface and its properties
(at minimum describe the interface and the `phase: ComposePhase` property),
following the project's TSDoc style for exported types so tooling and consumers
can see the description.
- Around line 32-34: The exported React component PhaseStepper lacks proper
TSDoc/JSDoc; add a JSDoc block above the declaration of PhaseStepper describing
the component's purpose, its props (PhaseStepperProps), the expected
type/meaning of the phase prop, and any important behavior (e.g. how
currentIndex is derived from PHASE_ORDER and that it clamps to 0); keep the
summary, `@param` for phase, and `@returns` detailing it renders the 5-step phase
stepper so tooling and readers get typed documentation.
In `@src/components/wikiCompose/ResearchSection.tsx`:
- Around line 21-32: Add a bilingual (English and Japanese) TSDoc block above
the exported ResearchSectionProps interface describing each prop and the overall
purpose; document batch (ResearchBatch|null), pendingSources/approvedSources
(arrays of ResearchSource), isReadOnly/isStreaming (booleans), and onSubmit
(shape of the submitted object and Promise<void> behavior) in both EN and JP,
following the repo's TSDoc style and including parameter descriptions and return
details for discoverability.
- Around line 61-69: Remove the effect that seeds decisions via useEffect and
stop calling setDecisions inside that effect; instead rely on the existing
lookup pattern (decisions[s.id] ?? "approved") so missing entries default to
"approved" without an extra render. Delete the useEffect block that references
setDecisions and pendingSources, keep decisions as the single source of user
overrides, and remove any unused "pending" variant handling if present to avoid
dead code.
In `@src/hooks/useWikiComposeSession.ts`:
- Around line 222-249: The compose_section "completed" branch doesn't persist
the finished buffer into draftedSections, so content in
sectionBuffers[event.sectionId] is lost; modify the "compose_section" case in
the reducer (the branch where event.status !== "started") to copy the buffer at
prev.sectionBuffers[event.sectionId] into draftedSections (e.g., merge into
prev.draftedSections with key event.sectionId) and also clear streamingSectionId
appropriately while preserving the activity update via appendActivity; ensure
the reducer updates both draftedSections and sectionBuffers (or removes the
flushed buffer) so completedMarkdown can be computed later.
- Around line 355-377: The finally block in useWikiComposeSession may clear a
newer stream because it unconditionally sets isStreaming:false and
abortRef.current = null; modify the finally to only clear state for the same
run's controller: capture the created controller (const controller = new
AbortController()) and in finally check that abortRef.current === controller
before calling update({ isStreaming: false }) and before nulling
abortRef.current; this ensures runSession/abortRef mutations from later calls
are not overwritten and preserves cancel() and spinner state for newer streams.
- Around line 36-134: Add bilingual (English + Japanese) TSDoc comments to all
newly exported types in this block—at minimum add docs for ComposePhase and
ensure ComposeActivity, WikiComposeSessionState, UseWikiComposeSessionArgs, and
UseWikiComposeSessionReturn have consistent bilingual summaries and field
descriptions; use JSDoc/TSDoc style (/** ... */) placed immediately above each
exported symbol (ComposePhase, ComposeActivity, WikiComposeSessionState,
UseWikiComposeSessionArgs, UseWikiComposeSessionReturn) and include concise
English then Japanese lines for each property/comment to satisfy the project's
documentation guidelines.
In `@src/lib/wikiCompose/types.ts`:
- Around line 15-188: Add bilingual (EN/JP) TSDoc/JSDoc comments to every
exported type/interface/const in this file (e.g., PageSnapshot, BriefOption,
BriefQuestion, BriefAnswer, BriefResult, ResearchSource, ResearchBatch,
OutlineSection, DraftedSection, ComposeSessionStatus, ComposeSession,
ComposeInterruptPayload, ComposeSseEvent, and WIKI_COMPOSE_GRAPH_ID) describing
purpose, key fields, and any discriminants/enum meanings; for unions
(ComposeInterruptPayload, ComposeSseEvent) document each variant and its
discriminant (kind/type) and for complex fields (evaluation, sources, metadata,
status) document semantics and units (e.g., timestamps are ISO strings), keeping
both English and Japanese brief sentences for each exported declaration.
---
Minor comments:
In `@server/api/src/agents/graphs/wikiCompose/nodes/humanReviewOutline.ts`:
- Around line 19-23: The exported function humanReviewOutline lacks a TSDoc
block; add a concise TSDoc comment above the function describing its purpose,
parameters, and return type—mention that it handles the "human_review_outline"
node behavior (interrupt + resume projection), document the parameters state:
WikiComposeStateType and _config: LangGraphRunnableConfig, and the returned
Promise<WikiComposeStateUpdate>; include any relevant tags like `@param` and
`@returns` for clarity.
In `@server/api/src/agents/graphs/wikiCompose/nodes/shared/dispatch.ts`:
- Around line 12-41: Add TSDoc comments for the exported interfaces and
functions: document ComposePhasePayload and ComposeSectionPayload with a short
description for each field (phase/status, sectionId, heading, index, total), and
add TSDoc for dispatchComposePhase and dispatchComposeSection describing their
purpose, parameters (payload: ComposePhasePayload | ComposeSectionPayload and
config: LangGraphRunnableConfig) and return type Promise<void>; place the
comments immediately above the respective declarations (ComposePhasePayload,
ComposeSectionPayload, dispatchComposePhase, dispatchComposeSection) following
project TSDoc style.
In `@server/api/src/agents/runner/sseMapper.ts`:
- Around line 261-277: mapComposeSection currently accepts any typeof number
(including NaN, fractions, negatives, infinities) for index and total; update
the validation in mapComposeSection so index and total are finite integers (use
Number.isInteger and isFinite/!Number.isNaN), non-negative, and that index <=
total (and total >= 0, or total > 0 if you require at least one item) before
emitting the SseComposeSectionEvent; keep the existing checks for sectionId,
heading, and status, and return [] if any of these numeric checks fail to avoid
corrupting the compose progress UI.
In `@src/pages/WikiComposePage.tsx`:
- Around line 128-131: The button rendering the X icon currently sets disabled
when session.status === "completed" || session.status === "cancelled", which
hides the intended "Close" action; change the condition so the button is only
disabled for the "cancelled" state (e.g., disabled={session.status ===
"cancelled"}) so that when session.status === "completed" the "Close"
label/button remains enabled; ensure any click handler (the function that
handles cancel/close near this button) correctly differentiates session.status
=== "completed" to perform the close flow.
---
Nitpick comments:
In `@server/api/src/agents/graphs/wikiCompose/nodes/completed.ts`:
- Around line 31-46: The two loops over outline (which build ordered and then
lines) perform duplicate draftById.get lookups; merge them by removing the
separate ordered array and, in a single pass over outline, retrieve drafted via
draftById.get(section.id) once, skip if missing, then push to ordered if needed
and append the heading/prefix and drafted.body to lines (use the existing prefix
calculation using section.depth and section.heading). Update references to
ordered accordingly or eliminate ordered if it's only used here.
In `@server/api/src/agents/graphs/wikiCompose/nodes/shared/loadPageSnapshot.ts`:
- Around line 45-49: Update the catch block inside loadPageSnapshot to capture
the thrown error (change catch to catch (err)) and log it before returning
emptySnapshot(pageId); for example call a logger if available (e.g.,
logger.error or processLogger.error) or console.error with a short contextual
message plus the err object so DB/transient errors are recorded while preserving
the current behavior of returning emptySnapshot(pageId).
In `@src/components/wikiCompose/BriefQuestionCard.tsx`:
- Around line 16-20: Add a TSDoc/JSDoc comment block for the exported interface
BriefQuestionCardProps that describes the purpose of the interface and documents
each property (question: BriefQuestion, answer: BriefAnswer | null, onChange:
(next: BriefAnswer) => void); place the comment directly above the interface
declaration and include short descriptions for the interface and each field (use
`@property` or plain TSDoc lines), referencing the types BriefQuestion and
BriefAnswer so the exported-symbol documentation rule is satisfied.
In `@src/pages/WikiComposePage.tsx`:
- Around line 62-65: The ternary assigned to outlineForPreview is a no-op
(always returns session.outlineProposal); replace it with a single direct
assignment or wire in the intended source for the "completed" phase. Either
change the line to const outlineForPreview = session.outlineProposal; or, if
completion should use a different field, pick that field (e.g.,
session.finalOutline or session.outline) when session.phase === "completed" and
keep session.outlineProposal for draft/other phases—update the outlineForPreview
declaration accordingly.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 81fff795-861f-45e4-a705-3a65d7187227
📒 Files selected for processing (41)
e2e/wiki-compose.spec.tsserver/api/src/__tests__/agents/graphs/wikiCompose/wikiComposeGraph.test.tsserver/api/src/__tests__/agents/runner/sseMapper.test.tsserver/api/src/agents/core/types/sseEvents.tsserver/api/src/agents/graphs/wikiCompose/index.tsserver/api/src/agents/graphs/wikiCompose/nodes/briefDialogue.tsserver/api/src/agents/graphs/wikiCompose/nodes/completed.tsserver/api/src/agents/graphs/wikiCompose/nodes/draftSections.tsserver/api/src/agents/graphs/wikiCompose/nodes/humanReviewBrief.tsserver/api/src/agents/graphs/wikiCompose/nodes/humanReviewOutline.tsserver/api/src/agents/graphs/wikiCompose/nodes/index.tsserver/api/src/agents/graphs/wikiCompose/nodes/shared/dispatch.tsserver/api/src/agents/graphs/wikiCompose/nodes/shared/loadPageSnapshot.tsserver/api/src/agents/graphs/wikiCompose/nodes/structureDialogue.tsserver/api/src/agents/graphs/wikiCompose/resumeSchemas.tsserver/api/src/agents/graphs/wikiCompose/state.tsserver/api/src/agents/graphs/wikiCompose/types.tsserver/api/src/agents/graphs/wikiCompose/wikiComposeGraph.tsserver/api/src/agents/index.tsserver/api/src/agents/runner/sseMapper.tsserver/api/src/app.tsserver/api/src/routes/composeSessions.tssrc/App.tsxsrc/components/editor/WikiGeneratorButton.tsxsrc/components/note/PageEditorContent.tsxsrc/components/wikiCompose/ActivitySection.tsxsrc/components/wikiCompose/BriefQuestionCard.tsxsrc/components/wikiCompose/ComposePanel.tsxsrc/components/wikiCompose/DialogueSection.tsxsrc/components/wikiCompose/EditorPane.tsxsrc/components/wikiCompose/OutlineEditor.tsxsrc/components/wikiCompose/PhaseStepper.test.tsxsrc/components/wikiCompose/PhaseStepper.tsxsrc/components/wikiCompose/ResearchSection.tsxsrc/hooks/useWikiComposeSession.test.tssrc/hooks/useWikiComposeSession.tssrc/lib/wikiCompose/composeService.test.tssrc/lib/wikiCompose/composeService.tssrc/lib/wikiCompose/types.tssrc/pages/NotePageView.tsxsrc/pages/WikiComposePage.tsx
| kind: "human_review_research", | ||
| batch: { | ||
| id: "batch-1", | ||
| iteration: 0, | ||
| queries: [], | ||
| sources: [], | ||
| evaluation: null, | ||
| createdAt: new Date().toISOString(), | ||
| }, |
There was a problem hiding this comment.
Research interrupt mock payload shape appears drifted from orchestrator contract.
The E2E fixture uses payload.batch, while the shared interrupt type defines human_review_research as batchId + pendingSources. This can give false confidence by testing a non-canonical wire shape.
Suggested fix
payload: {
kind: "human_review_research",
- batch: {
- id: "batch-1",
- iteration: 0,
- queries: [],
- sources: [],
- evaluation: null,
- createdAt: new Date().toISOString(),
- },
+ batchId: "batch-1",
pendingSources: [Also applies to: 190-193
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@e2e/wiki-compose.spec.ts` around lines 101 - 109, The test mock for the
"human_review_research" interrupt currently uses payload.batch (an object with
id, iteration, queries, etc.) but should match the orchestrator contract which
expects payload to include batchId (string) and pendingSources (array); update
the mock "payload" object to replace the nested "batch" with batchId: "batch-1"
and pendingSources: [] and remove/translate any fields
(iteration/queries/sources/evaluation) to conform to the contract; do the same
for the other occurrence of this mock in the spec so both fixtures use the
canonical shape for human_review_research.
| const parsed = briefResumeSchema.parse(resumeValue); | ||
|
|
||
| // Index questions by id so we can produce a stable, readable summary. | ||
| // 質問テキストを id → text で引けるよう、ループの外で 1 度だけ Map 化する。 | ||
| const questionMap = new Map<string, string>(); | ||
| for (const q of state.briefQuestions) questionMap.set(q.id, q.question); | ||
|
|
||
| const answers: BriefAnswer[] = parsed.answers.map((a) => ({ | ||
| questionId: a.questionId, | ||
| selectedOptionIds: a.selectedOptionIds, | ||
| ...(a.freeText !== undefined ? { freeText: a.freeText } : {}), | ||
| })); | ||
|
|
There was a problem hiding this comment.
Required-question enforcement is missing on resume.
This node trusts client-side validation and accepts answers even when required questions are omitted, allowing workflow bypass through crafted resume payloads.
Suggested fix
const parsed = briefResumeSchema.parse(resumeValue);
+ const requiredIds = new Set(
+ state.briefQuestions.filter((q) => q.required).map((q) => q.id),
+ );
+ const answered = new Set(
+ parsed.answers
+ .filter(
+ (a) =>
+ a.selectedOptionIds.length > 0 ||
+ (typeof a.freeText === "string" && a.freeText.trim().length > 0),
+ )
+ .map((a) => a.questionId),
+ );
+ for (const id of requiredIds) {
+ if (!answered.has(id)) {
+ throw new Error(`Missing required brief answer: ${id}`);
+ }
+ }📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const parsed = briefResumeSchema.parse(resumeValue); | |
| // Index questions by id so we can produce a stable, readable summary. | |
| // 質問テキストを id → text で引けるよう、ループの外で 1 度だけ Map 化する。 | |
| const questionMap = new Map<string, string>(); | |
| for (const q of state.briefQuestions) questionMap.set(q.id, q.question); | |
| const answers: BriefAnswer[] = parsed.answers.map((a) => ({ | |
| questionId: a.questionId, | |
| selectedOptionIds: a.selectedOptionIds, | |
| ...(a.freeText !== undefined ? { freeText: a.freeText } : {}), | |
| })); | |
| const parsed = briefResumeSchema.parse(resumeValue); | |
| const requiredIds = new Set( | |
| state.briefQuestions.filter((q) => q.required).map((q) => q.id), | |
| ); | |
| const answered = new Set( | |
| parsed.answers | |
| .filter( | |
| (a) => | |
| a.selectedOptionIds.length > 0 || | |
| (typeof a.freeText === "string" && a.freeText.trim().length > 0), | |
| ) | |
| .map((a) => a.questionId), | |
| ); | |
| for (const id of requiredIds) { | |
| if (!answered.has(id)) { | |
| throw new Error(`Missing required brief answer: ${id}`); | |
| } | |
| } | |
| // Index questions by id so we can produce a stable, readable summary. | |
| // 質問テキストを id → text で引けるよう、ループの外で 1 度だけ Map 化する。 | |
| const questionMap = new Map<string, string>(); | |
| for (const q of state.briefQuestions) questionMap.set(q.id, q.question); | |
| const answers: BriefAnswer[] = parsed.answers.map((a) => ({ | |
| questionId: a.questionId, | |
| selectedOptionIds: a.selectedOptionIds, | |
| ...(a.freeText !== undefined ? { freeText: a.freeText } : {}), | |
| })); |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@server/api/src/agents/graphs/wikiCompose/nodes/humanReviewBrief.ts` around
lines 61 - 73, The node currently trusts briefResumeSchema.parse and builds
answers without enforcing that all required questions in state.briefQuestions
are present, so fix by validating after parsing: iterate state.briefQuestions
(use questionMap and symbols state.briefQuestions, questionMap, parsed, answers,
BriefAnswer) to collect required question ids, ensure parsed.answers includes an
entry for each required id and that each required answer contains the necessary
data (selectedOptionIds non-empty or freeText present depending on the question
type), and if any required question is missing or empty throw/return a
validation error before continuing; update the creation of answers to only
proceed after this required-question check.
| id: z.string().min(1), | ||
| heading: z.string().min(1), | ||
| depth: z.number().int().min(1).max(3), | ||
| intent: z.string().default(""), | ||
| sourceIds: z.array(z.string().min(1)).optional(), |
There was a problem hiding this comment.
Reject whitespace-only outline headings at schema boundary.
z.string().min(1) accepts " ", so invalid section headings can enter state and draft generation.
Suggested fix
z.object({
id: z.string().min(1),
- heading: z.string().min(1),
+ heading: z.string().trim().min(1),
depth: z.number().int().min(1).max(3),
- intent: z.string().default(""),
+ intent: z.string().trim().default(""),
sourceIds: z.array(z.string().min(1)).optional(),
}),📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| id: z.string().min(1), | |
| heading: z.string().min(1), | |
| depth: z.number().int().min(1).max(3), | |
| intent: z.string().default(""), | |
| sourceIds: z.array(z.string().min(1)).optional(), | |
| id: z.string().min(1), | |
| heading: z.string().trim().min(1), | |
| depth: z.number().int().min(1).max(3), | |
| intent: z.string().trim().default(""), | |
| sourceIds: z.array(z.string().min(1)).optional(), |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@server/api/src/agents/graphs/wikiCompose/resumeSchemas.ts` around lines 56 -
60, The schema currently uses heading: z.string().min(1) which permits
whitespace-only strings; change the heading validator to reject strings that
trim to empty by replacing it with a refinement such as heading:
z.string().min(1).refine(s => s.trim().length > 0, { message: "Heading cannot be
empty or whitespace" }) (or the equivalent z.string().refine(...)). Update the
heading entry in the resume schema (the object containing id, heading, depth,
intent, sourceIds) so whitespace-only headings are rejected at the schema
boundary.
| export interface ActivitySectionProps { | ||
| activity: ComposeActivity[]; | ||
| isStreaming: boolean; | ||
| } |
There was a problem hiding this comment.
🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win
Add TSDoc/JSDoc for exported interface.
The exported ActivitySectionProps interface lacks a JSDoc comment. As per coding guidelines, all exported types and interfaces should include TSDoc/JSDoc documentation.
📝 Add JSDoc comment
+/**
+ * Props for the ActivitySection component.
+ * ActivitySection コンポーネントのプロップス。
+ */
export interface ActivitySectionProps {
activity: ComposeActivity[];
isStreaming: boolean;
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| export interface ActivitySectionProps { | |
| activity: ComposeActivity[]; | |
| isStreaming: boolean; | |
| } | |
| /** | |
| * Props for the ActivitySection component. | |
| * ActivitySection コンポーネントのプロップス。 | |
| */ | |
| export interface ActivitySectionProps { | |
| activity: ComposeActivity[]; | |
| isStreaming: boolean; | |
| } |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/components/wikiCompose/ActivitySection.tsx` around lines 17 - 20, Add a
JSDoc/TSDoc comment for the exported interface ActivitySectionProps describing
the purpose of the interface and its properties: document that
ActivitySectionProps contains an array of ComposeActivity (explain what
ComposeActivity represents if not obvious) under the `activity` field and a
boolean `isStreaming` indicating streaming state; place the JSDoc immediately
above the ActivitySectionProps declaration so tools and consumers pick it up.
| /** Compact activity timeline. */ | ||
| export const ActivitySection: React.FC<ActivitySectionProps> = ({ activity, isStreaming }) => { |
There was a problem hiding this comment.
🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win
Add TSDoc/JSDoc for exported component.
The exported ActivitySection component has only a brief single-line comment. As per coding guidelines, all exported functions should include proper TSDoc/JSDoc documentation.
📝 Add JSDoc comment
-/** Compact activity timeline. */
+/**
+ * Compact activity timeline showing compose session events.
+ * Compose セッションのイベントを表示するコンパクトなアクティビティタイムライン。
+ */
export const ActivitySection: React.FC<ActivitySectionProps> = ({ activity, isStreaming }) => {📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| /** Compact activity timeline. */ | |
| export const ActivitySection: React.FC<ActivitySectionProps> = ({ activity, isStreaming }) => { | |
| /** | |
| * Compact activity timeline showing compose session events. | |
| * Compose セッションのイベントを表示するコンパクトなアクティビティタイムライン。 | |
| */ | |
| export const ActivitySection: React.FC<ActivitySectionProps> = ({ activity, isStreaming }) => { |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/components/wikiCompose/ActivitySection.tsx` around lines 35 - 36, Add a
proper TSDoc/JSDoc block above the exported ActivitySection component describing
the component purpose, its props (ActivitySectionProps) and each prop (activity,
isStreaming), the React.FC return type, and any important behavior (e.g.,
streaming UI rendering). Mention prop types and whether they are optional,
include a short example usage if helpful, and ensure the comment sits
immediately above the ActivitySection export.
| useEffect(() => { | ||
| setDecisions((prev) => { | ||
| const next: Record<string, Decision> = { ...prev }; | ||
| for (const s of pendingSources) { | ||
| if (next[s.id] === undefined) next[s.id] = "approved"; | ||
| } | ||
| return next; | ||
| }); | ||
| }, [pendingSources]); |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# 1) Confirm repo config references the rule/plugin
fd -HI 'eslint.config.*' | xargs -r rg -n 'react-hooks|set-state-in-effect'
# 2) Inspect the concrete pattern in this file
rg -n -C3 'useEffect|setDecisions\(' src/components/wikiCompose/ResearchSection.tsx
# 3) Validate with project lint (run locally with repo toolchain)
bunx eslint src/components/wikiCompose/ResearchSection.tsxRepository: otomatty/zedi
Length of output: 1244
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Find ESLint config files
configs=$(fd -HI 'eslint.config.*' . || true)
echo "$configs" | head -n 50
# Search for the specific rule name across configs
rg -n "set-state-in-effect|react-hooks/set-state-in-effect" -S . || true
# Inspect the specific ESLint config file(s) that load eslint-plugin-react-hooks
for f in $configs; do
echo "---- $f ----"
# Print only relevant parts around rule definitions / plugin registration
rg -n "eslint-plugin-react-hooks|reactHooks|plugins|rules" "$f" -n || true
# Print the whole config only if it's small; otherwise just the relevant range
lines=$(wc -l < "$f")
if [ "$lines" -le 200 ]; then
cat -n "$f"
else
# print first ~120 lines which usually include plugin/rule setup
head -n 140 "$f" | cat -n
fi
done
# Show the full effect block and adjacent code in the target file (lines around the useEffect)
rg -n -C5 "useEffect\\s*\\(|setDecisions\\(" src/components/wikiCompose/ResearchSection.tsx
# Check if there are any eslint-disable comments near the effect
rg -n "eslint-disable|eslint-disable-next-line|eslint-disable-line" src/components/wikiCompose/ResearchSection.tsx || trueRepository: otomatty/zedi
Length of output: 9178
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# 1) Determine actual severity for react-hooks/set-state-in-effect in this repo's eslint-plugin-react-hooks
node - <<'NODE'
try {
const plugin = require('eslint-plugin-react-hooks');
const rule = plugin?.configs?.recommended?.rules?.['react-hooks/set-state-in-effect'];
console.log(JSON.stringify(rule, null, 2));
} catch (e) {
console.error("Failed to load eslint-plugin-react-hooks from repo environment:", e?.message || e);
process.exitCode = 1;
}
NODE
# 2) Inspect ResearchSection.tsx around Decision/decisions/useEffect for context
sed -n '1,120p' src/components/wikiCompose/ResearchSection.tsx | nl -ba
# 3) Search within file for decisions/pendingSources usage to ensure refactor direction aligns
rg -n "pendingSources|decisions|setDecisions|Decision" src/components/wikiCompose/ResearchSection.tsxRepository: otomatty/zedi
Length of output: 212
🏁 Script executed:
#!/bin/bash
set -euo pipefail
FILE="src/components/wikiCompose/ResearchSection.tsx"
# Show type definitions and initial state region (early in file)
sed -n '1,120p' "$FILE" | nl -ba
# Show where Decision is declared/used
rg -n --context 3 "type Decision|Decision\\b|\\\"pending\\\"|setDecision\\(|decisions\\[|decisionFor\\b" "$FILE"
# Show the useEffect block and nearby render logic (broader context)
rg -n -C10 "useEffect\\(|setDecisions\\(" "$FILE"Repository: otomatty/zedi
Length of output: 100
🏁 Script executed:
#!/bin/bash
set -euo pipefail
FILE="src/components/wikiCompose/ResearchSection.tsx"
wc -l "$FILE"
sed -n '1,140p' "$FILE" | cat -n
echo "---- Decision/pending references ----"
rg -n --context 3 "type Decision|\\bDecision\\b|\"pending\"|setDecisions\\(|setDecision\\(|decisions\\b" "$FILE"
echo "---- useEffect context ----"
rg -n -C8 "useEffect\\s*\\(|setDecisions\\(" "$FILE"Repository: otomatty/zedi
Length of output: 9458
🏁 Script executed:
#!/bin/bash
set -euo pipefail
FILE="src/components/wikiCompose/ResearchSection.tsx"
rg -n "\"pending\"|\\bpending\\b" "$FILE"
rg -n "decisions\\[|decisions\\.|\\bsetDecisions\\b" "$FILE"
sed -n '130,234p' "$FILE" | cat -nRepository: otomatty/zedi
Length of output: 5027
Avoid setDecisions inside the useEffect seeding block (Line 61).
This file writes state synchronously in an effect (useEffect → setDecisions(...)) while react-hooks/set-state-in-effect is enabled via eslint-plugin-react-hooks recommended rules—this should be avoided and the extra render is unnecessary. Since the component already treats missing entries as "approved" (decisions[s.id] ?? "approved"), keep decisions as user overrides and remove the effect-driven initialization (optionally drop the unused "pending" variant).
♻️ Refactor direction (remove effect-driven reset)
-import React, { useEffect, useState } from "react";
+import React, { useState } from "react";
@@
-type Decision = "approved" | "rejected" | "pending";
+type Decision = "approved" | "rejected";
@@
- useEffect(() => {
- setDecisions((prev) => {
- const next: Record<string, Decision> = { ...prev };
- for (const s of pendingSources) {
- if (next[s.id] === undefined) next[s.id] = "approved";
- }
- return next;
- });
- }, [pendingSources]);
-
const setDecision = (id: string, d: Decision) => setDecisions((prev) => ({ ...prev, [id]: d }));
+ const decisionFor = (id: string): Decision => decisions[id] ?? "approved";
@@
- const d = decisions[s.id] ?? "approved";
+ const d = decisionFor(s.id);
@@
- (s) => (decisions[s.id] ?? "approved") === "approved",
+ (s) => decisionFor(s.id) === "approved",
@@
- const d = decisions[s.id] ?? "approved";
+ const d = decisionFor(s.id);🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/components/wikiCompose/ResearchSection.tsx` around lines 61 - 69, Remove
the effect that seeds decisions via useEffect and stop calling setDecisions
inside that effect; instead rely on the existing lookup pattern (decisions[s.id]
?? "approved") so missing entries default to "approved" without an extra render.
Delete the useEffect block that references setDecisions and pendingSources, keep
decisions as the single source of user overrides, and remove any unused
"pending" variant handling if present to avoid dead code.
| export type ComposePhase = "brief" | "research" | "structure" | "draft" | "completed"; | ||
|
|
||
| /** Activity log entry surfaced in the right pane's ActivitySection. */ | ||
| export interface ComposeActivity { | ||
| id: string; | ||
| /** ISO timestamp. */ | ||
| at: string; | ||
| /** Human-readable label for the activity row. */ | ||
| label: string; | ||
| /** Optional secondary line (status, tool name, etc.). */ | ||
| detail?: string; | ||
| /** Lifecycle hint so the UI can render spinners / checkmarks. */ | ||
| status?: "started" | "completed" | "info" | "error"; | ||
| } | ||
|
|
||
| /** Aggregate state surfaced to the UI. */ | ||
| export interface WikiComposeSessionState { | ||
| session: ComposeSession | null; | ||
| status: ComposeSessionStatus | "idle"; | ||
| phase: ComposePhase; | ||
| /** Brief phase question cards (from interrupt). */ | ||
| briefQuestions: BriefQuestion[]; | ||
| /** Page snapshot (loaded at session start). */ | ||
| pageSnapshot: PageSnapshot | null; | ||
| /** Latest research batch from the human-review interrupt. */ | ||
| latestBatch: ResearchBatch | null; | ||
| /** Pending sources at the research interrupt. */ | ||
| pendingSources: ResearchSource[]; | ||
| /** Approved sources after research resume. */ | ||
| approvedSources: ResearchSource[]; | ||
| /** Proposed outline from the structure interrupt. */ | ||
| outlineProposal: OutlineSection[]; | ||
| /** Drafted section bodies — keyed by sectionId. */ | ||
| draftedSections: Record<string, DraftedSection>; | ||
| /** While streaming a section, this id is set; null between sections. */ | ||
| streamingSectionId: string | null; | ||
| /** Per-section running token buffer while the section is mid-stream. */ | ||
| sectionBuffers: Record<string, string>; | ||
| /** Activity timeline (newest last). */ | ||
| activity: ComposeActivity[]; | ||
| /** Final markdown if the session completed. */ | ||
| completedMarkdown: string | null; | ||
| /** Last error message (set on failure). */ | ||
| error: string | null; | ||
| /** True while an SSE stream is open. */ | ||
| isStreaming: boolean; | ||
| } | ||
|
|
||
| const INITIAL_STATE: WikiComposeSessionState = { | ||
| session: null, | ||
| status: "idle", | ||
| phase: "brief", | ||
| briefQuestions: [], | ||
| pageSnapshot: null, | ||
| latestBatch: null, | ||
| pendingSources: [], | ||
| approvedSources: [], | ||
| outlineProposal: [], | ||
| draftedSections: {}, | ||
| streamingSectionId: null, | ||
| sectionBuffers: {}, | ||
| activity: [], | ||
| completedMarkdown: null, | ||
| error: null, | ||
| isStreaming: false, | ||
| }; | ||
|
|
||
| /** Args accepted by the hook. */ | ||
| export interface UseWikiComposeSessionArgs { | ||
| pageId: string; | ||
| /** Existing session to resume; pass `null` to create a fresh session on start. */ | ||
| sessionId: string | null; | ||
| /** Optional initial body for the first run (e.g. seed messages). */ | ||
| initialInput?: Record<string, unknown>; | ||
| /** Auto-start the first `run` when the session is created. Default `true`. */ | ||
| autoStart?: boolean; | ||
| } | ||
|
|
||
| /** Hook return shape. */ | ||
| export interface UseWikiComposeSessionReturn extends WikiComposeSessionState { | ||
| /** Start a new session (or resume the existing one) and begin streaming. */ | ||
| start: () => Promise<void>; | ||
| /** Submit Brief answers and continue streaming. */ | ||
| submitBrief: (input: { | ||
| answers: BriefAnswer[]; | ||
| appendToExisting?: boolean; | ||
| researchMaxIterations?: number; | ||
| }) => Promise<void>; | ||
| /** Submit research source approval (Approve/Reject) and continue streaming. */ | ||
| submitResearchApproval: (input: { | ||
| approvedSourceIds: string[]; | ||
| rejectedSourceIds?: string[]; | ||
| note?: string; | ||
| }) => Promise<void>; | ||
| /** Submit outline approval and continue streaming. */ | ||
| submitOutline: (input: { sections: OutlineSection[] }) => Promise<void>; | ||
| /** Cancel the session (DELETE). */ | ||
| cancel: () => Promise<void>; | ||
| } |
There was a problem hiding this comment.
🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win
Add bilingual TSDoc to the new exported API block.
ComposePhase is exported without TSDoc, and the new public docs in this block are not consistently bilingual.
📝 Example
+/**
+ * Top-level compose phase exposed to the UI.
+ * UI に公開する Compose のトップレベルフェーズ。
+ */
export type ComposePhase = "brief" | "research" | "structure" | "draft" | "completed";As per coding guidelines, **/*.{ts,tsx,js,jsx}: Include TSDoc/JSDoc comments for all exported functions, types, and interfacesand**/*.{ts,tsx,js,jsx,md}: Include both Japanese and English comments/documentation in code and documentation files to maintain project tone consistency.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/hooks/useWikiComposeSession.ts` around lines 36 - 134, Add bilingual
(English + Japanese) TSDoc comments to all newly exported types in this block—at
minimum add docs for ComposePhase and ensure ComposeActivity,
WikiComposeSessionState, UseWikiComposeSessionArgs, and
UseWikiComposeSessionReturn have consistent bilingual summaries and field
descriptions; use JSDoc/TSDoc style (/** ... */) placed immediately above each
exported symbol (ComposePhase, ComposeActivity, WikiComposeSessionState,
UseWikiComposeSessionArgs, UseWikiComposeSessionReturn) and include concise
English then Japanese lines for each property/comment to satisfy the project's
documentation guidelines.
| case "compose_section": | ||
| if (event.status === "started") { | ||
| return { | ||
| streamingSectionId: event.sectionId, | ||
| sectionBuffers: { ...prev.sectionBuffers, [event.sectionId]: "" }, | ||
| activity: appendActivity(prev.activity, { | ||
| label: `Drafting: ${event.heading}`, | ||
| detail: `${event.index} / ${event.total}`, | ||
| status: "started", | ||
| }), | ||
| }; | ||
| } | ||
| return { | ||
| streamingSectionId: | ||
| prev.streamingSectionId === event.sectionId ? null : prev.streamingSectionId, | ||
| activity: appendActivity(prev.activity, { | ||
| label: `Drafted: ${event.heading}`, | ||
| detail: `${event.index} / ${event.total}`, | ||
| status: "completed", | ||
| }), | ||
| }; | ||
| case "token": { | ||
| const id = prev.streamingSectionId; | ||
| if (!id) return {}; | ||
| const prior = prev.sectionBuffers[id] ?? ""; | ||
| return { | ||
| sectionBuffers: { ...prev.sectionBuffers, [id]: prior + event.content }, | ||
| }; |
There was a problem hiding this comment.
Persist finished section buffers into draftedSections.
The reducer never commits sectionBuffers[event.sectionId] when a "compose_section" finishes, so completed section content is dropped from long-lived state and completedMarkdown at Lines 475-482 stays null after a successful run.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/hooks/useWikiComposeSession.ts` around lines 222 - 249, The
compose_section "completed" branch doesn't persist the finished buffer into
draftedSections, so content in sectionBuffers[event.sectionId] is lost; modify
the "compose_section" case in the reducer (the branch where event.status !==
"started") to copy the buffer at prev.sectionBuffers[event.sectionId] into
draftedSections (e.g., merge into prev.draftedSections with key event.sectionId)
and also clear streamingSectionId appropriately while preserving the activity
update via appendActivity; ensure the reducer updates both draftedSections and
sectionBuffers (or removes the flushed buffer) so completedMarkdown can be
computed later.
| abortRef.current?.abort(); | ||
| const controller = new AbortController(); | ||
| abortRef.current = controller; | ||
| update({ isStreaming: true, error: null }); | ||
| try { | ||
| await runSession({ | ||
| pageId, | ||
| sessionId: session.id, | ||
| body, | ||
| onEvent, | ||
| signal: controller.signal, | ||
| }); | ||
| } catch (err) { | ||
| if ((err as { name?: string }).name === "AbortError") { | ||
| // Caller aborted; do not surface as an error. | ||
| // ユーザー操作による abort は error として扱わない。 | ||
| return; | ||
| } | ||
| const message = err instanceof Error ? err.message : String(err); | ||
| update({ error: message, isStreaming: false }); | ||
| } finally { | ||
| update({ isStreaming: false }); | ||
| abortRef.current = null; |
There was a problem hiding this comment.
Guard finally from clearing a newer stream.
If streamRun() is called again while the previous runSession() is unwinding, the older finally block can null out abortRef.current and flip isStreaming to false for the newer stream. That breaks cancel() for the live SSE and can hide the spinner while streaming is still active.
🛠️ Proposed fix
} finally {
- update({ isStreaming: false });
- abortRef.current = null;
+ if (abortRef.current === controller) {
+ update({ isStreaming: false });
+ abortRef.current = null;
+ }
}🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/hooks/useWikiComposeSession.ts` around lines 355 - 377, The finally block
in useWikiComposeSession may clear a newer stream because it unconditionally
sets isStreaming:false and abortRef.current = null; modify the finally to only
clear state for the same run's controller: capture the created controller (const
controller = new AbortController()) and in finally check that abortRef.current
=== controller before calling update({ isStreaming: false }) and before nulling
abortRef.current; this ensures runSession/abortRef mutations from later calls
are not overwritten and preserves cancel() and spinner state for newer streams.
| export interface PageSnapshot { | ||
| pageId: string; | ||
| title: string; | ||
| body: string; | ||
| hasContent: boolean; | ||
| } | ||
|
|
||
| // ── Brief phase ──────────────────────────────────────────────────────────── | ||
| export interface BriefOption { | ||
| id: string; | ||
| label: string; | ||
| hint?: string; | ||
| } | ||
|
|
||
| export interface BriefQuestion { | ||
| id: string; | ||
| question: string; | ||
| rationale?: string; | ||
| options: BriefOption[]; | ||
| required: boolean; | ||
| } | ||
|
|
||
| export interface BriefAnswer { | ||
| questionId: string; | ||
| selectedOptionIds: string[]; | ||
| freeText?: string; | ||
| } | ||
|
|
||
| export interface BriefResult { | ||
| answers: BriefAnswer[]; | ||
| summary: string; | ||
| appendToExisting: boolean; | ||
| } | ||
|
|
||
| // ── Research sources (subset of backend `Source`) ────────────────────────── | ||
| export interface ResearchSource { | ||
| id: string; | ||
| kind: "web" | "wiki" | "fetched"; | ||
| title: string; | ||
| url?: string; | ||
| finalUrl?: string; | ||
| snippet?: string; | ||
| excerpt?: string; | ||
| pageId?: string; | ||
| noteId?: string; | ||
| } | ||
|
|
||
| export interface ResearchBatch { | ||
| id: string; | ||
| iteration: number; | ||
| sources: ResearchSource[]; | ||
| // The full evaluation lives in state; the wire event carries only summary | ||
| // fields, but the interrupt payload may include it. | ||
| evaluation?: { | ||
| score: number; | ||
| rationale: string; | ||
| missingAspects: string[]; | ||
| } | null; | ||
| createdAt: string; | ||
| } | ||
|
|
||
| // ── Structure / draft ────────────────────────────────────────────────────── | ||
| export interface OutlineSection { | ||
| id: string; | ||
| heading: string; | ||
| depth: number; | ||
| intent: string; | ||
| sourceIds?: string[]; | ||
| } | ||
|
|
||
| export interface DraftedSection { | ||
| sectionId: string; | ||
| heading: string; | ||
| body: string; | ||
| citedSourceIds: string[]; | ||
| completedAt: string; | ||
| } | ||
|
|
||
| // ── Compose session row (REST shape from POST/GET) ───────────────────────── | ||
| export type ComposeSessionStatus = | ||
| | "pending" | ||
| | "running" | ||
| | "interrupted" | ||
| | "completed" | ||
| | "cancelled" | ||
| | "failed"; | ||
|
|
||
| export interface ComposeSession { | ||
| id: string; | ||
| pageId: string; | ||
| userId: string; | ||
| graphId: string; | ||
| backend: string; | ||
| phase: string; | ||
| status: ComposeSessionStatus; | ||
| metadata?: Record<string, unknown> | null; | ||
| lastError?: string | null; | ||
| closedAt?: string | null; | ||
| createdAt: string; | ||
| updatedAt: string; | ||
| } | ||
|
|
||
| // ── Interrupt payloads (discriminated union) ─────────────────────────────── | ||
| export type ComposeInterruptPayload = | ||
| | { | ||
| kind: "human_review_brief"; | ||
| questions: BriefQuestion[]; | ||
| pageSnapshot: PageSnapshot; | ||
| } | ||
| | { | ||
| kind: "human_review_research"; | ||
| batch: ResearchBatch | null; | ||
| pendingSources: ResearchSource[]; | ||
| } | ||
| | { | ||
| kind: "human_review_outline"; | ||
| outline: OutlineSection[]; | ||
| approvedSources: ResearchSource[]; | ||
| }; | ||
|
|
||
| // ── SSE event union (mirrors backend `SseEvent`) ─────────────────────────── | ||
| export type ComposeSseEvent = | ||
| | { type: "started"; sessionId: string; graphId: string; phase?: string } | ||
| | { type: "status"; phase: string; message?: string } | ||
| | { type: "token"; node?: string; content: string } | ||
| | { type: "tool_start"; tool: string; input?: Record<string, unknown> } | ||
| | { type: "tool_end"; tool: string; outputLength?: number; error?: string } | ||
| | { | ||
| type: "usage"; | ||
| inputTokens: number; | ||
| outputTokens: number; | ||
| costUnits: number; | ||
| usagePercent: number; | ||
| } | ||
| | { type: "interrupt"; payload?: ComposeInterruptPayload } | ||
| | { type: "done"; status: "completed" | "interrupted" | "failed" } | ||
| | { type: "error"; message: string; retryable?: boolean } | ||
| | { | ||
| type: "research_iteration"; | ||
| iteration: number; | ||
| status: "planned" | "refined"; | ||
| queryCount: number; | ||
| } | ||
| | { | ||
| type: "research_evaluation"; | ||
| iteration: number; | ||
| score: number; | ||
| rationale: string; | ||
| missingAspectsCount: number; | ||
| } | ||
| | { | ||
| type: "research_batch"; | ||
| batchId: string; | ||
| iteration: number; | ||
| sourceCount: number; | ||
| score: number | null; | ||
| exitReason: "score_threshold" | "max_iterations"; | ||
| } | ||
| | { | ||
| type: "compose_phase"; | ||
| phase: "brief" | "research" | "structure" | "draft" | "completed"; | ||
| status: "entered" | "completed"; | ||
| } | ||
| | { | ||
| type: "compose_section"; | ||
| sectionId: string; | ||
| heading: string; | ||
| status: "started" | "completed"; | ||
| index: number; | ||
| total: number; | ||
| }; | ||
|
|
||
| /** Convenience tag for the orchestrator graph id used by the frontend. */ | ||
| export const WIKI_COMPOSE_GRAPH_ID = "wiki-compose"; |
There was a problem hiding this comment.
🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win
Document exported wire contracts with bilingual TSDoc.
This file defines the frontend/backend contract, but exported interfaces/types are mostly undocumented. Please add JP/EN TSDoc on each exported declaration to make the wire spec self-describing and maintainable.
🧩 Example pattern
-export interface PageSnapshot {
+/**
+ * Snapshot of the target wiki page used during compose.
+ * Compose 中に参照する対象ページのスナップショット。
+ */
+export interface PageSnapshot {
@@
-export type ComposeSessionStatus =
+/**
+ * Lifecycle status of a compose session row.
+ * Compose セッション行のライフサイクル状態。
+ */
+export type ComposeSessionStatus =As per coding guidelines "Include TSDoc/JSDoc comments for all exported functions, types, and interfaces".
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/lib/wikiCompose/types.ts` around lines 15 - 188, Add bilingual (EN/JP)
TSDoc/JSDoc comments to every exported type/interface/const in this file (e.g.,
PageSnapshot, BriefOption, BriefQuestion, BriefAnswer, BriefResult,
ResearchSource, ResearchBatch, OutlineSection, DraftedSection,
ComposeSessionStatus, ComposeSession, ComposeInterruptPayload, ComposeSseEvent,
and WIKI_COMPOSE_GRAPH_ID) describing purpose, key fields, and any
discriminants/enum meanings; for unions (ComposeInterruptPayload,
ComposeSseEvent) document each variant and its discriminant (kind/type) and for
complex fields (evaluation, sources, metadata, status) document semantics and
units (e.g., timestamps are ISO strings), keeping both English and Japanese
brief sentences for each exported declaration.
概要
Wiki Compose P2 の完全実装。Brief → Research → Structure → Draft → Completed の全体フローを担う LangGraph オーケストレータグラフと、分割画面 UI を追加します。Research subgraph (#949) を subgraph as node として組み込み、各フェーズの interrupt を統一スレッドで管理します。
変更点
バックエンド
wikiComposeGraph.ts: 5 フェーズ(Brief → Research → Structure → Draft → Completed)を統合したオーケストレータグラフbriefDialogue.ts: ページスナップショット読み込み、構造化質問生成humanReviewBrief.ts: Brief interrupt、ユーザー回答の検証・確定structureDialogue.ts: アウトライン提案生成humanReviewOutline.ts: Outline interrupt、ユーザー編集の確定draftSections.ts: セクション本文のストリーミング生成(SSE token イベント)completed.ts: 最終 Markdown 組み立てstate.ts:WikiComposeState— BaseState + ResearchLoopState channels を superset として保持し、research subgraph との state 自動共有を実現types.ts: Brief / Research / Outline / Draft 共通の値型resumeSchemas.ts: 各 interrupt の resume payload バリデーションnodes/shared/:dispatch.ts(compose_phase / compose_section custom event)、loadPageSnapshot.ts(ページ読み込み)sseMapper.tsにcompose_phase/compose_sectionイベント型を追加、sseEvents.tsに型定義composeSessions.tsに Wiki Compose セッション REST + SSE エンドポイント、app.tsに graph 登録フロントエンド
useWikiComposeSession.ts: React state machine フックstart()/submitBrief()/submitResearchApproval()/submitOutline()/cancel()mutatorsWikiComposeSessionState/UseWikiComposeSessionReturn)WikiComposePage.tsx: ルートページ(/notes/:noteId/:pageId/compose[/:sessionId])EditorPane(タイトル + Markdown プレビュー)ComposePanel(PhaseStepper + Dialogue + Research + Activity)UI コンポーネント:
PhaseStepper.tsx: Brief → Research → Structure → Draft → Completed の進捗表示DialogueSection.tsx: Brief 質問カード / Outline エディタ / Draft 進捗の出し分けBriefQuestionCard.tsx: 構造化質問カード(チップ選択 + フリーテキスト)OutlineEditor.tsx: アウトライン編集(並び替え / リネーム / depth 変更 / 削除)ResearchSection.tsx: ソース個別採用 UIActivitySection.tsx: エージェント活動タイムラインhttps://claude.ai/code/session_014HYEdJ4Q1EnuUZbQ2zHoTj
Summary by CodeRabbit
Release Notes
New Features
Tests