Skip to content

feat: implement Wiki Compose P2 orchestrator graph and UI (#950)#959

Merged
cursor[bot] merged 1 commit into
developfrom
claude/quirky-ptolemy-7vT3B
May 24, 2026
Merged

feat: implement Wiki Compose P2 orchestrator graph and UI (#950)#959
cursor[bot] merged 1 commit into
developfrom
claude/quirky-ptolemy-7vT3B

Conversation

@otomatty

@otomatty otomatty commented May 24, 2026

Copy link
Copy Markdown
Owner

概要

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(ページ読み込み)
  • SSE 統合: sseMapper.tscompose_phase / compose_section イベント型を追加、sseEvents.ts に型定義
  • ルート統合: composeSessions.ts に Wiki Compose セッション REST + SSE エンドポイント、app.ts に graph 登録

フロントエンド

  • useWikiComposeSession.ts: React state machine フック

    • SSE イベント pattern matching と state reduction
    • start() / submitBrief() / submitResearchApproval() / submitOutline() / cancel() mutators
    • Activity timeline 管理
    • 完全な型安全性(WikiComposeSessionState / 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: ソース個別採用 UI
    • ActivitySection.tsx: エージェント活動タイムライン

https://claude.ai/code/session_014HYEdJ4Q1EnuUZbQ2zHoTj

Summary by CodeRabbit

Release Notes

  • New Features

    • Added "Wiki Compose" orchestration workflow for guided wiki page creation, featuring a split-screen UI with live content preview and phase-tracked compose panel.
    • Implemented multi-phase guided workflow: brief questions → research source review → outline structure → real-time draft generation with completion.
    • Added support for interrupting and resuming compose sessions at any phase.
    • Integrated compose button into page editor for seamless workflow initiation.
  • Tests

    • Added comprehensive E2E and unit test coverage for the Wiki Compose orchestration and service layers.

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-code-review

Copy link
Copy Markdown

Qodo reviews are paused for this user.

Troubleshooting steps vary by plan Learn more →

On a Teams plan?
Reviews resume once this user has a paid seat and their Git account is linked in Qodo.
Link Git account →

Using GitHub Enterprise Server, GitLab Self-Managed, or Bitbucket Data Center?
These require an Enterprise plan - Contact us
Contact us →

@coderabbitai

coderabbitai Bot commented May 24, 2026

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

This 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.

Changes

Wiki Compose P2: Backend Graph & Frontend Integration

Layer / File(s) Summary
SSE Event Contracts
server/api/src/agents/core/types/sseEvents.ts
Extended SseEvent union to include compose_phase (for phase lifecycle) and compose_section (for per-section streaming) discriminated event types, with updated SSE_EVENT_NAMES constants.
Backend Data Models & Validation
server/api/src/agents/graphs/wikiCompose/types.ts, state.ts, resumeSchemas.ts
Defined domain contracts for brief/outline/research/completion data, LangGraph state annotation with custom dedup/merge reducers for sources and sections, and Zod validators for resume payloads at Brief and Outline interrupt points.
Brief Phase: Question Generation & User Review
server/api/src/agents/graphs/wikiCompose/nodes/briefDialogue.ts, humanReviewBrief.ts, shared/loadPageSnapshot.ts, shared/dispatch.ts
Implemented brief dialogue node invoking structured LLM to generate contextual questions, human-review interrupt node parsing user answers, and shared helpers for page snapshots and SSE phase/section event dispatching.
Structure & Draft Phases
server/api/src/agents/graphs/wikiCompose/nodes/structureDialogue.ts, humanReviewOutline.ts, draftSections.ts, completed.ts
Implemented outline proposal generation, human outline review/approval, streamed section drafting with per-section error recovery, and final markdown assembly with citation filtering.
Graph Assembly & Registration
server/api/src/agents/graphs/wikiCompose/wikiComposeGraph.ts, nodes/index.ts, index.ts, server/api/src/agents/index.ts, app.ts, routes/composeSessions.ts
Assembled LangGraph orchestrator with interrupt points and edge routing, exported nodes/state/types via barrel modules, registered graph in Hono app, and configured recursion limits per graph.
SSE Mapping & Backend Tests
server/api/src/agents/runner/sseMapper.ts, __tests__/agents/graphs/wikiCompose/wikiComposeGraph.test.ts, __tests__/agents/runner/sseMapper.test.ts
Mapped compose-phase/compose-section custom events to typed SSE payloads; included comprehensive unit tests for interrupt/resume behavior and event mapping.
Frontend Types & Service Client
src/lib/wikiCompose/types.ts, composeService.ts, composeService.test.ts
Mirrored backend wire types; implemented REST/SSE client with fetch-based streaming, SSE record parsing, and abort signal support; included unit tests for stream handling and error paths.
Session Hook & State Management
src/hooks/useWikiComposeSession.ts, useWikiComposeSession.test.ts
Implemented React hook managing session lifecycle via SSE event reduction, activity logging, and async submit/cancel handlers; included unit tests validating event→state transforms and resume flows.
UI Components
src/components/wikiCompose/ActivitySection.tsx, BriefQuestionCard.tsx, PhaseStepper.tsx, PhaseStepper.test.tsx, ResearchSection.tsx, DialogueSection.tsx, OutlineEditor.tsx, EditorPane.tsx, ComposePanel.tsx
Implemented reusable components for activity timeline, brief question cards with options/free-text, phase progress stepper, research source review, phase-aware dialogue routing, outline editor with add/remove/reorder, and left preview pane with streaming buffers.
Page Layout & Routing
src/pages/WikiComposePage.tsx, src/App.tsx, src/pages/NotePageView.tsx, src/components/editor/WikiGeneratorButton.tsx, src/components/note/PageEditorContent.tsx
Assembled split-pane WikiComposePage with header/editor/panel layout; wired /notes/:noteId/:pageId/compose[/:sessionId] routes; updated wiki button to support compose navigation via composeHref prop while preserving legacy inline generation.
End-to-End Test
e2e/wiki-compose.spec.ts
Defined Playwright E2E test mocking Compose API and SSE streams, exercising full Brief→Research→Outline→Draft→Completed workflow with user interactions at each interrupt point and final navigation validation.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related issues

  • #950 — This PR directly implements the Wiki Compose P2 feature scope including all server graph nodes, frontend components, hooks, types, and E2E test described in the issue.

Possibly related PRs

  • otomatty/zedi#956 — Both PRs extend SSE event mapping in sseMapper.ts/sseEvents.ts to handle LangGraph custom events (main: compose_phase/compose_section; PR#956: research_iteration/research_evaluation/research_batch).
  • otomatty/zedi#719 — Main PR's new compose routes (/notes/:noteId/:pageId/compose) directly depend on PR#719's earlier note-native URL reshaping from /notes/:noteId/pages/:pageId.
  • otomatty/zedi#894 — Main PR extends the same note-scoped /notes/:noteId/:pageId routing architecture introduced in PR#894, which also updated NotePageView and App.tsx.

🐰 A rabbit composes a wiki in phases so bright,
Brief questions, research, outlines outlined just right,
Stream drafts flow like prose in the dark of night,
From chaos to order, from questions to light.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch claude/quirky-ptolemy-7vT3B

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment on lines +437 to +440
const result = await resumeSession({ pageId, sessionId: session.id, resume: input });
update({ status: result.status });
if (result.status === "interrupted" || result.status === "running") {
await streamRun(session);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge 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 👍 / 👎.

Comment on lines +68 to +70
const [appendToExisting, setAppendToExisting] = useState<boolean>(
Boolean(pageSnapshot?.hasContent),
);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge 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 👍 / 👎.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +438 to +442
update({ status: result.status });
if (result.status === "interrupted" || result.status === "running") {
await streamRun(session);
}
},

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

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})*`;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

セクションの生成中にエラーが発生した場合、既に一部のテキストが生成されている(body が空でない)と、エラーメッセージが本文に追記されません。ユーザーが生成の失敗に気づけるよう、常にエラーメッセージを追記するように変更することを推奨します。

Suggested change
body = body || `*(Section draft failed: ${message})*`;
body += `\n\n*(Section draft failed: ${message})*`;

Comment on lines +477 to +481
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");

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

completedMarkdown の組み立てにおいて、見出しレベルが ## に固定されています。バックエンドの completed.tsEditorPane.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");

Comment on lines +62 to +65
const outlineForPreview =
session.phase === "completed" || session.phase === "draft"
? session.outlineProposal
: session.outlineProposal;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

outlineForPreview の決定ロジックが冗長で、どちらの条件でも session.outlineProposal を返しています。意図した実装が漏れているか、単なるタイポである可能性があるため、簡略化を検討してください。

Suggested change
const outlineForPreview =
session.phase === "completed" || session.phase === "draft"
? session.outlineProposal
: session.outlineProposal;
const outlineForPreview = session.outlineProposal;

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 win

Add TSDoc comment for the exported function.

The humanReviewOutline function 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 win

Add TSDoc comments for exported interfaces and functions.

The exported ComposePhasePayload and ComposeSectionPayload interfaces, along with the dispatchComposePhase and dispatchComposeSection functions, 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 win

Close 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 win

Harden compose_section numeric validation before emitting SSE.

typeof === "number" currently allows NaN, 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 win

Consider merging the two outline loops.

The code iterates outline twice: once to build ordered (lines 31-35) and again to build lines (lines 37-46). This results in duplicate draftById.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 value

Consider 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 win

Remove 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 win

Add 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

📥 Commits

Reviewing files that changed from the base of the PR and between 779c1f5 and 7de1b5c.

📒 Files selected for processing (41)
  • e2e/wiki-compose.spec.ts
  • server/api/src/__tests__/agents/graphs/wikiCompose/wikiComposeGraph.test.ts
  • server/api/src/__tests__/agents/runner/sseMapper.test.ts
  • server/api/src/agents/core/types/sseEvents.ts
  • server/api/src/agents/graphs/wikiCompose/index.ts
  • server/api/src/agents/graphs/wikiCompose/nodes/briefDialogue.ts
  • server/api/src/agents/graphs/wikiCompose/nodes/completed.ts
  • server/api/src/agents/graphs/wikiCompose/nodes/draftSections.ts
  • server/api/src/agents/graphs/wikiCompose/nodes/humanReviewBrief.ts
  • server/api/src/agents/graphs/wikiCompose/nodes/humanReviewOutline.ts
  • server/api/src/agents/graphs/wikiCompose/nodes/index.ts
  • server/api/src/agents/graphs/wikiCompose/nodes/shared/dispatch.ts
  • server/api/src/agents/graphs/wikiCompose/nodes/shared/loadPageSnapshot.ts
  • server/api/src/agents/graphs/wikiCompose/nodes/structureDialogue.ts
  • server/api/src/agents/graphs/wikiCompose/resumeSchemas.ts
  • server/api/src/agents/graphs/wikiCompose/state.ts
  • server/api/src/agents/graphs/wikiCompose/types.ts
  • server/api/src/agents/graphs/wikiCompose/wikiComposeGraph.ts
  • server/api/src/agents/index.ts
  • server/api/src/agents/runner/sseMapper.ts
  • server/api/src/app.ts
  • server/api/src/routes/composeSessions.ts
  • src/App.tsx
  • src/components/editor/WikiGeneratorButton.tsx
  • src/components/note/PageEditorContent.tsx
  • src/components/wikiCompose/ActivitySection.tsx
  • src/components/wikiCompose/BriefQuestionCard.tsx
  • src/components/wikiCompose/ComposePanel.tsx
  • src/components/wikiCompose/DialogueSection.tsx
  • src/components/wikiCompose/EditorPane.tsx
  • src/components/wikiCompose/OutlineEditor.tsx
  • src/components/wikiCompose/PhaseStepper.test.tsx
  • src/components/wikiCompose/PhaseStepper.tsx
  • src/components/wikiCompose/ResearchSection.tsx
  • src/hooks/useWikiComposeSession.test.ts
  • src/hooks/useWikiComposeSession.ts
  • src/lib/wikiCompose/composeService.test.ts
  • src/lib/wikiCompose/composeService.ts
  • src/lib/wikiCompose/types.ts
  • src/pages/NotePageView.tsx
  • src/pages/WikiComposePage.tsx

Comment thread e2e/wiki-compose.spec.ts
Comment on lines +101 to +109
kind: "human_review_research",
batch: {
id: "batch-1",
iteration: 0,
queries: [],
sources: [],
evaluation: null,
createdAt: new Date().toISOString(),
},

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

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: [
As per coding guidelines, "`**/*.{test,spec}.{ts,tsx,js,jsx}`: Tests serve as a source of truth for specifications alongside implementation code TSDoc/JSDoc".

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.

Comment on lines +61 to +73
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 } : {}),
}));

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

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.

Suggested change
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.

Comment on lines +56 to +60
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(),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

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.

Suggested change
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.

Comment on lines +17 to +20
export interface ActivitySectionProps {
activity: ComposeActivity[];
isStreaming: boolean;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ 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.

Suggested change
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.

Comment on lines +35 to +36
/** Compact activity timeline. */
export const ActivitySection: React.FC<ActivitySectionProps> = ({ activity, isStreaming }) => {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ 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.

Suggested change
/** 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.

Comment on lines +61 to +69
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]);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 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.tsx

Repository: 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 || true

Repository: 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.tsx

Repository: 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 -n

Repository: otomatty/zedi

Length of output: 5027


Avoid setDecisions inside the useEffect seeding block (Line 61).

This file writes state synchronously in an effect (useEffectsetDecisions(...)) 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.

Comment on lines +36 to +134
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>;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ 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.

Comment on lines +222 to +249
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 },
};

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

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.

Comment on lines +355 to +377
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;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

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.

Comment on lines +15 to +188
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";

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ 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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants