Răsfoiți Sursa

feat(interaction): add shared book-draft ideation flow

Ma 7 luni în urmă
părinte
comite
2437df01c0

+ 118 - 0
docs/plans/2026-04-13-book-draft-conversation-design.md

@@ -0,0 +1,118 @@
+# Book Draft Conversation Design
+
+## Goal
+
+Fill the missing front-half interaction layer inside `interaction-control-layer` by adding a shared book-draft conversation that works in both `TUI` and `Studio Chat`.
+
+This is not the full “creative operating system” yet. It is the first complete loop for:
+- vague idea input
+- iterative draft refinement
+- shared draft state across shells
+- explicit conversion from draft to book creation
+
+## Scope
+
+### Included
+- shared `creationDraft` state inside the project interaction session
+- natural-language ideation when no active book is bound
+- `/create` to create a book from the current draft
+- `/discard` to drop the current draft
+- Studio Chat / TUI synchronization through the same session file
+
+### Excluded
+- full title/blurb/outline negotiation UI
+- foundation diff / outline revision workflow
+- automatic truth/sqlite rebuild after manual chapter edits
+
+## Product Behavior
+
+### No active book
+
+When the project has no active book and the user enters freeform text, the interaction layer treats it as book ideation instead of generic chat.
+
+Example:
+
+```text
+我想写个港风商战悬疑,主角从灰产洗白。
+```
+
+The system responds with:
+- a tightened shared draft
+- one focused next question
+
+### Shared draft
+
+The draft lives in project session state and can be resumed from:
+- TUI
+- Studio Chat
+- external agent entrypoints
+
+### Explicit creation
+
+Users finalize the draft through:
+
+```text
+/create
+```
+
+This converts the shared draft into the existing structured `create_book` path.
+
+### Explicit discard
+
+Users can drop the draft through:
+
+```text
+/discard
+```
+
+## Architecture
+
+### Session
+
+`InteractionSession` gains `creationDraft`.
+
+The draft is the single shared source of truth for:
+- concept
+- optional title / genre / platform / language
+- optional chapter scale fields
+- blurb / author intent / current focus seeds
+- next question
+- missing fields
+- ready-to-create flag
+
+### Routing
+
+`routeNaturalLanguageIntent()` gets one new branch:
+- if no active book is bound, unmatched freeform input routes to `develop_book`
+
+Explicit commands:
+- `/new <idea>` -> `develop_book`
+- `/create` -> `create_book`
+- `/discard` -> `discard_book_draft`
+
+### Runtime
+
+New runtime intents:
+- `develop_book`
+- `discard_book_draft`
+
+`create_book` now fills missing fields from the current draft before calling the existing creation tool.
+
+### Tools
+
+Shared interaction tools gain `developBookDraft`.
+
+It uses the active model to:
+- merge the latest user turn into the current draft
+- return a compact assistant reply
+- return updated structured draft JSON
+
+## Why This Is The Right Cut
+
+This design gives us:
+- a real front-half conversation loop
+- zero duplicate shell logic
+- no new standalone wizard system
+- no collision with the already-working write/revise/export control paths
+
+It is intentionally smaller than the final vision, but it closes the biggest product gap: “I can control a book, but I still can’t converse a new book into existence.”

+ 58 - 0
docs/plans/2026-04-13-book-draft-conversation-implementation.md

@@ -0,0 +1,58 @@
+# Book Draft Conversation Implementation Plan
+
+> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
+
+**Goal:** Add a shared book-draft ideation loop so TUI and Studio Chat can converge a vague story idea into a structured create-ready draft.
+
+**Architecture:** Extend `InteractionSession` with `creationDraft`, route no-active-book freeform input into a new `develop_book` runtime intent, and reuse the existing `create_book` runtime/tool path for finalization.
+
+**Tech Stack:** TypeScript, Vitest, Zod, existing interaction runtime/session/tooling.
+
+---
+
+### Task 1: Add shared draft state and intents
+
+**Files:**
+- Modify: `packages/core/src/interaction/session.ts`
+- Modify: `packages/core/src/interaction/intents.ts`
+- Modify: `packages/core/src/index.ts`
+- Test: `packages/core/src/__tests__/interaction-models.test.ts`
+
+### Task 2: Route ideation input
+
+**Files:**
+- Modify: `packages/core/src/interaction/nl-router.ts`
+- Modify: `packages/core/src/interaction/project-control.ts`
+- Test: `packages/core/src/__tests__/interaction-nl-router.test.ts`
+
+### Task 3: Implement runtime + tools
+
+**Files:**
+- Modify: `packages/core/src/interaction/runtime.ts`
+- Modify: `packages/core/src/interaction/project-tools.ts`
+- Test: `packages/core/src/__tests__/interaction-runtime.test.ts`
+- Test: `packages/core/src/__tests__/project-interaction.test.ts`
+
+### Task 4: Surface shared draft in Studio Chat
+
+**Files:**
+- Modify: `packages/studio/src/components/ChatBar.tsx`
+- Test: `packages/studio/src/components/chatbar-state.test.ts`
+
+### Task 5: Verify
+
+Run:
+
+```bash
+pnpm --dir packages/core exec vitest run \
+  src/__tests__/interaction-models.test.ts \
+  src/__tests__/interaction-nl-router.test.ts \
+  src/__tests__/interaction-runtime.test.ts \
+  src/__tests__/project-interaction.test.ts
+pnpm --dir packages/studio exec vitest run \
+  src/components/chatbar-state.test.ts \
+  src/api/server.test.ts
+pnpm --dir packages/core run build
+pnpm --dir packages/cli run build
+pnpm --dir packages/studio run build
+```

+ 42 - 0
packages/cli/src/__tests__/tui-dashboard.test.tsx

@@ -177,4 +177,46 @@ describe("ink dashboard", () => {
     expect(frame).toContain("告诉 InkOS 要写什么、修改什么,或解释什么");
     expect(frame).toContain("告诉 InkOS 要写什么、修改什么,或解释什么");
     expect(frame).toContain("回车发送");
     expect(frame).toContain("回车发送");
   });
   });
+
+  it("surfaces the shared creation draft when no active book exists yet", async () => {
+    const mod = await import("../tui/dashboard.js");
+
+    const draftSession: InteractionSession = {
+      ...createSession(),
+      activeBookId: undefined,
+      activeChapterNumber: undefined,
+      currentExecution: {
+        status: "planning",
+        stageLabel: "developing book draft",
+      },
+      pendingDecision: undefined,
+      creationDraft: {
+        concept: "港风商战悬疑,主角从灰产洗白。",
+        title: "夜港账本",
+        nextQuestion: "你更想写长篇连载,还是十来章能收住?",
+        missingFields: ["targetChapters"],
+        readyToCreate: false,
+      },
+      messages: [
+        { role: "user", content: "我想写个港风商战悬疑。", timestamp: 1 },
+        { role: "assistant", content: "先把这本书的大概方向收住。", timestamp: 2 },
+      ],
+      events: [],
+    };
+
+    const { lastFrame } = render(
+      <mod.InkTuiDashboard
+        locale="zh-CN"
+        projectName="inkos-demo"
+        modelLabel="gpt-5.4 (openai)"
+        session={draftSession}
+        inputValue=""
+        isSubmitting={false}
+      />,
+    );
+
+    const frame = lastFrame() ?? "";
+    expect(frame).toContain("草稿 夜港账本");
+    expect(frame).toContain("你更想写长篇连载,还是十来章能收住?");
+  });
 });
 });

+ 16 - 3
packages/cli/src/tui/dashboard-model.ts

@@ -56,6 +56,8 @@ export function buildDashboardViewModel(params: BuildDashboardViewModelParams):
   const executionLabel = normalizeStageLabel(params.session.currentExecution?.stageLabel ?? status, params.copy);
   const executionLabel = normalizeStageLabel(params.session.currentExecution?.stageLabel ?? status, params.copy);
   const modeLabel = formatModeLabel(params.session.automationMode, params.copy);
   const modeLabel = formatModeLabel(params.session.automationMode, params.copy);
   const bookLabel = params.activeBookTitle ?? params.session.activeBookId ?? params.copy.labels.none;
   const bookLabel = params.activeBookTitle ?? params.session.activeBookId ?? params.copy.labels.none;
+  const draftTitle = params.session.creationDraft?.title;
+  const draftQuestion = params.session.creationDraft?.nextQuestion;
   const sinceTimestamp = params.sinceTimestamp ?? 0;
   const sinceTimestamp = params.sinceTimestamp ?? 0;
   const terminalRows = params.terminalRows ?? process.stdout.rows ?? 24;
   const terminalRows = params.terminalRows ?? process.stdout.rows ?? 24;
   const conversationLimit = Math.max(4, terminalRows - 10);
   const conversationLimit = Math.max(4, terminalRows - 10);
@@ -88,15 +90,26 @@ export function buildDashboardViewModel(params: BuildDashboardViewModelParams):
     modeLabel,
     modeLabel,
     executionStatus: status,
     executionStatus: status,
     executionLabel,
     executionLabel,
-    headerLine: `${params.copy.labels.project} ${params.projectName} · ${params.copy.labels.book} ${bookLabel} · ${params.copy.labels.depth} ${params.depthLabel ?? params.copy.depthLabels.normal} · ${params.copy.labels.session} ${params.session.sessionId.slice(-4)} · ${params.copy.labels.messageCount(params.session.messages.length)}`,
+    headerLine: [
+      `${params.copy.labels.project} ${params.projectName}`,
+      `${params.copy.labels.book} ${bookLabel}`,
+      draftTitle ? `${params.copy.labels.draft} ${draftTitle}` : undefined,
+      `${params.copy.labels.depth} ${params.depthLabel ?? params.copy.depthLabels.normal}`,
+      `${params.copy.labels.session} ${params.session.sessionId.slice(-4)}`,
+      params.copy.labels.messageCount(params.session.messages.length),
+    ].filter(Boolean).join(" · "),
     statusPrimaryLine: `${params.copy.labels.stage} ${executionLabel} · ${params.copy.labels.mode} ${modeLabel} · ${params.copy.labels.model} ${params.modelLabel}`,
     statusPrimaryLine: `${params.copy.labels.stage} ${executionLabel} · ${params.copy.labels.mode} ${modeLabel} · ${params.copy.labels.model} ${params.modelLabel}`,
     statusSecondaryLine: params.lastError
     statusSecondaryLine: params.lastError
       ? `${params.copy.labels.error} · ${compactInline(params.lastError)}`
       ? `${params.copy.labels.error} · ${compactInline(params.lastError)}`
       : params.isSubmitting && latestEventSummary
       : params.isSubmitting && latestEventSummary
         ? `${params.copy.labels.recent} · ${latestEventSummary}`
         ? `${params.copy.labels.recent} · ${latestEventSummary}`
-        : params.session.pendingDecision?.summary
+      : params.session.pendingDecision?.summary
           ? `${params.copy.labels.pending} · ${compactInline(params.session.pendingDecision.summary)}`
           ? `${params.copy.labels.pending} · ${compactInline(params.session.pendingDecision.summary)}`
-          : latestEventSummary
+        : draftQuestion
+          ? `${params.copy.labels.draft} · ${compactInline(draftQuestion)}`
+        : draftTitle
+          ? `${params.copy.labels.draft} · ${draftTitle}`
+        : latestEventSummary
             ? `${params.copy.labels.recent} · ${latestEventSummary}`
             ? `${params.copy.labels.recent} · ${latestEventSummary}`
             : `${params.copy.labels.ready} · ${bookLabel}`,
             : `${params.copy.labels.ready} · ${bookLabel}`,
     messageRows,
     messageRows,

+ 4 - 1
packages/cli/src/tui/dashboard.tsx

@@ -335,7 +335,10 @@ export function InkTuiApp(props: InkTuiAppProps): React.JSX.Element {
       }
       }
 
 
       const activeBookId = await resolveSessionActiveBook(props.projectRoot, session);
       const activeBookId = await resolveSessionActiveBook(props.projectRoot, session);
-      const routed = routeNaturalLanguageIntent(input, { activeBookId });
+      const routed = routeNaturalLanguageIntent(input, {
+        activeBookId,
+        hasCreationDraft: Boolean(session.creationDraft),
+      });
       const userTimestamp = Date.now();
       const userTimestamp = Date.now();
       const assistantDraftTimestamp = routed.intent === "chat" ? userTimestamp + 1 : null;
       const assistantDraftTimestamp = routed.intent === "chat" ? userTimestamp + 1 : null;
       assistantDraftTimestampRef.current = assistantDraftTimestamp;
       assistantDraftTimestampRef.current = assistantDraftTimestamp;

+ 3 - 0
packages/cli/src/tui/i18n.ts

@@ -16,6 +16,7 @@ export interface TuiCopy {
     readonly error: string;
     readonly error: string;
     readonly recent: string;
     readonly recent: string;
     readonly pending: string;
     readonly pending: string;
+    readonly draft: string;
     readonly ready: string;
     readonly ready: string;
     readonly none: string;
     readonly none: string;
     readonly notConfigured: string;
     readonly notConfigured: string;
@@ -70,6 +71,7 @@ const ZH_CN: TuiCopy = {
     error: "错误",
     error: "错误",
     recent: "最近",
     recent: "最近",
     pending: "待确认",
     pending: "待确认",
+    draft: "草稿",
     ready: "就绪",
     ready: "就绪",
     none: "无",
     none: "无",
     notConfigured: "未配置",
     notConfigured: "未配置",
@@ -149,6 +151,7 @@ const EN: TuiCopy = {
     error: "Error",
     error: "Error",
     recent: "Recent",
     recent: "Recent",
     pending: "Pending",
     pending: "Pending",
+    draft: "Draft",
     ready: "Ready",
     ready: "Ready",
     none: "none",
     none: "none",
     notConfigured: "not configured",
     notConfigured: "not configured",

+ 26 - 0
packages/core/src/__tests__/interaction-models.test.ts

@@ -1,6 +1,7 @@
 import { describe, expect, it } from "vitest";
 import { describe, expect, it } from "vitest";
 import {
 import {
   AutomationModeSchema,
   AutomationModeSchema,
+  BookCreationDraftSchema,
   InteractionIntentTypeSchema,
   InteractionIntentTypeSchema,
   ExecutionStatusSchema,
   ExecutionStatusSchema,
   InteractionSessionSchema,
   InteractionSessionSchema,
@@ -9,6 +10,8 @@ import {
   isTerminalExecutionStatus,
   isTerminalExecutionStatus,
   appendInteractionMessage,
   appendInteractionMessage,
   appendInteractionEvent,
   appendInteractionEvent,
+  updateCreationDraft,
+  clearCreationDraft,
 } from "../index.js";
 } from "../index.js";
 
 
 describe("interaction models", () => {
 describe("interaction models", () => {
@@ -19,7 +22,9 @@ describe("interaction models", () => {
   });
   });
 
 
   it("parses supported interaction intents", () => {
   it("parses supported interaction intents", () => {
+    expect(InteractionIntentTypeSchema.parse("develop_book")).toBe("develop_book");
     expect(InteractionIntentTypeSchema.parse("create_book")).toBe("create_book");
     expect(InteractionIntentTypeSchema.parse("create_book")).toBe("create_book");
+    expect(InteractionIntentTypeSchema.parse("discard_book_draft")).toBe("discard_book_draft");
     expect(InteractionIntentTypeSchema.parse("chat")).toBe("chat");
     expect(InteractionIntentTypeSchema.parse("chat")).toBe("chat");
     expect(InteractionIntentTypeSchema.parse("write_next")).toBe("write_next");
     expect(InteractionIntentTypeSchema.parse("write_next")).toBe("write_next");
     expect(InteractionIntentTypeSchema.parse("rewrite_chapter")).toBe("rewrite_chapter");
     expect(InteractionIntentTypeSchema.parse("rewrite_chapter")).toBe("rewrite_chapter");
@@ -126,4 +131,25 @@ describe("interaction models", () => {
       detail: "Completed write_next for harbor.",
       detail: "Completed write_next for harbor.",
     }]);
     }]);
   });
   });
+
+  it("stores and clears a creation draft inside the shared session", () => {
+    const draft = BookCreationDraftSchema.parse({
+      concept: "港风商战悬疑,主角从灰产洗白。",
+      title: "夜港账本",
+      genre: "urban",
+      readyToCreate: false,
+    });
+
+    const session = InteractionSessionSchema.parse({
+      sessionId: "session-5",
+      projectRoot: "/tmp/project",
+      automationMode: "semi",
+      messages: [],
+      events: [],
+    });
+
+    const withDraft = updateCreationDraft(session, draft);
+    expect(withDraft.creationDraft?.title).toBe("夜港账本");
+    expect(clearCreationDraft(withDraft).creationDraft).toBeUndefined();
+  });
 });
 });

+ 27 - 1
packages/core/src/__tests__/interaction-nl-router.test.ts

@@ -89,6 +89,26 @@ describe("interaction natural-language router", () => {
     });
     });
   });
   });
 
 
+  it("routes freeform input into book-development when no active book is bound", () => {
+    expect(routeNaturalLanguageIntent("我想写个港风商战悬疑")).toEqual({
+      intent: "develop_book",
+      instruction: "我想写个港风商战悬疑",
+    });
+    expect(routeNaturalLanguageIntent("先不要开书,我想想名字", {
+      hasCreationDraft: true,
+    })).toEqual({
+      intent: "develop_book",
+      instruction: "先不要开书,我想想名字",
+    });
+    expect(routeNaturalLanguageIntent("名字再狠一点", {
+      activeBookId: "harbor",
+      hasCreationDraft: true,
+    })).toEqual({
+      intent: "develop_book",
+      instruction: "名字再狠一点",
+    });
+  });
+
   it("maps mode switch requests to switch_mode", () => {
   it("maps mode switch requests to switch_mode", () => {
     expect(routeNaturalLanguageIntent("切换到全自动", { activeBookId: "harbor" })).toEqual({
     expect(routeNaturalLanguageIntent("切换到全自动", { activeBookId: "harbor" })).toEqual({
       intent: "switch_mode",
       intent: "switch_mode",
@@ -109,8 +129,14 @@ describe("interaction natural-language router", () => {
       intent: "list_books",
       intent: "list_books",
     });
     });
     expect(routeNaturalLanguageIntent("/new Night Harbor", { activeBookId: "harbor" })).toEqual({
     expect(routeNaturalLanguageIntent("/new Night Harbor", { activeBookId: "harbor" })).toEqual({
+      intent: "develop_book",
+      instruction: "Night Harbor",
+    });
+    expect(routeNaturalLanguageIntent("/create", { hasCreationDraft: true })).toEqual({
       intent: "create_book",
       intent: "create_book",
-      title: "Night Harbor",
+    });
+    expect(routeNaturalLanguageIntent("/discard", { hasCreationDraft: true })).toEqual({
+      intent: "discard_book_draft",
     });
     });
     expect(routeNaturalLanguageIntent("/open beta", { activeBookId: "harbor" })).toEqual({
     expect(routeNaturalLanguageIntent("/open beta", { activeBookId: "harbor" })).toEqual({
       intent: "select_book",
       intent: "select_book",

+ 81 - 7
packages/core/src/__tests__/interaction-runtime.test.ts

@@ -5,6 +5,7 @@ import { runInteractionRequest } from "../interaction/runtime.js";
 function makeTools(overrides: Partial<Parameters<typeof runInteractionRequest>[0]["tools"]> = {}) {
 function makeTools(overrides: Partial<Parameters<typeof runInteractionRequest>[0]["tools"]> = {}) {
   return {
   return {
     listBooks: vi.fn(async () => ["harbor"]),
     listBooks: vi.fn(async () => ["harbor"]),
+    developBookDraft: vi.fn(),
     createBook: vi.fn(),
     createBook: vi.fn(),
     exportBook: vi.fn(),
     exportBook: vi.fn(),
     writeNextChapter: vi.fn(),
     writeNextChapter: vi.fn(),
@@ -19,6 +20,48 @@ function makeTools(overrides: Partial<Parameters<typeof runInteractionRequest>[0
 }
 }
 
 
 describe("interaction runtime", () => {
 describe("interaction runtime", () => {
+  it("routes develop_book through the shared draft tool and updates the creation draft", async () => {
+    const developBookDraft = vi.fn(async () => ({
+      __interaction: {
+        responseText: "我先按港风商战悬疑收着。你更想写长篇连载,还是十来章能收住?",
+        details: {
+          creationDraft: {
+            concept: "港风商战悬疑,主角从灰产洗白。",
+            title: "夜港账本",
+            genre: "urban",
+            nextQuestion: "更想写长篇连载,还是十来章能收住?",
+            missingFields: ["targetChapters"],
+            readyToCreate: false,
+          },
+        },
+      },
+    }));
+
+    const result = await runInteractionRequest({
+      session: InteractionSessionSchema.parse({
+        sessionId: "session-draft",
+        projectRoot: "/tmp/project",
+        automationMode: "semi",
+        messages: [],
+        events: [],
+      }),
+      request: {
+        intent: "develop_book",
+        instruction: "我想写个港风商战悬疑,主角从灰产洗白。",
+      },
+      tools: makeTools({
+        developBookDraft,
+      }),
+    });
+
+    expect(developBookDraft).toHaveBeenCalledWith("我想写个港风商战悬疑,主角从灰产洗白。", undefined);
+    expect(result.session.creationDraft).toEqual(expect.objectContaining({
+      title: "夜港账本",
+      genre: "urban",
+    }));
+    expect(result.responseText).toContain("港风商战悬疑");
+  });
+
   it("routes create_book through the shared create tool and binds the created book", async () => {
   it("routes create_book through the shared create tool and binds the created book", async () => {
     const createBook = vi.fn(async () => ({
     const createBook = vi.fn(async () => ({
       bookId: "night-harbor",
       bookId: "night-harbor",
@@ -33,16 +76,21 @@ describe("interaction runtime", () => {
         sessionId: "session-create",
         sessionId: "session-create",
         projectRoot: "/tmp/project",
         projectRoot: "/tmp/project",
         automationMode: "semi",
         automationMode: "semi",
+        creationDraft: {
+          concept: "港风商战悬疑,主角从灰产洗白。",
+          title: "Night Harbor",
+          genre: "urban",
+          platform: "tomato",
+          targetChapters: 120,
+          chapterWordCount: 2800,
+          readyToCreate: true,
+          missingFields: [],
+        },
         messages: [],
         messages: [],
         events: [],
         events: [],
       }),
       }),
       request: {
       request: {
         intent: "create_book",
         intent: "create_book",
-        title: "Night Harbor",
-        genre: "urban",
-        platform: "tomato",
-        targetChapters: 120,
-        chapterWordCount: 2800,
       },
       },
       tools: makeTools({
       tools: makeTools({
         createBook,
         createBook,
@@ -57,9 +105,35 @@ describe("interaction runtime", () => {
       chapterWordCount: 2800,
       chapterWordCount: 2800,
     });
     });
     expect(result.session.activeBookId).toBe("night-harbor");
     expect(result.session.activeBookId).toBe("night-harbor");
+    expect(result.session.creationDraft).toBeUndefined();
     expect(result.responseText).toContain("Created Night Harbor.");
     expect(result.responseText).toContain("Created Night Harbor.");
   });
   });
 
 
+  it("clears the creation draft when discard_book_draft is requested", async () => {
+    const result = await runInteractionRequest({
+      session: InteractionSessionSchema.parse({
+        sessionId: "session-discard",
+        projectRoot: "/tmp/project",
+        automationMode: "semi",
+        creationDraft: {
+          concept: "港风商战悬疑,主角从灰产洗白。",
+          title: "夜港账本",
+          readyToCreate: false,
+          missingFields: ["genre"],
+        },
+        messages: [],
+        events: [],
+      }),
+      request: {
+        intent: "discard_book_draft",
+      },
+      tools: makeTools(),
+    });
+
+    expect(result.session.creationDraft).toBeUndefined();
+    expect(result.responseText).toContain("已丢弃");
+  });
+
   it("routes export_book through the shared export tool", async () => {
   it("routes export_book through the shared export tool", async () => {
     const exportBook = vi.fn(async () => ({
     const exportBook = vi.fn(async () => ({
       outputPath: "/tmp/project/exports/harbor.md",
       outputPath: "/tmp/project/exports/harbor.md",
@@ -187,7 +261,7 @@ describe("interaction runtime", () => {
       "stage.changed",
       "stage.changed",
       "task.completed",
       "task.completed",
     ]);
     ]);
-    expect(result.responseText).toContain("waiting for your next decision");
+    expect(result.responseText).toContain("等待你的下一步决定");
   });
   });
 
 
   it("routes revise_chapter to reviseDraft with local-fix", async () => {
   it("routes revise_chapter to reviseDraft with local-fix", async () => {
@@ -495,7 +569,7 @@ describe("interaction runtime", () => {
     });
     });
 
 
     expect(result.session.currentExecution?.status).toBe("blocked");
     expect(result.session.currentExecution?.status).toBe("blocked");
-    expect(result.responseText).toContain("Paused");
+    expect(result.responseText).toContain("已暂停");
     expect(result.session.events.map((event) => event.kind)).toEqual([
     expect(result.session.events.map((event) => event.kind)).toEqual([
       "task.started",
       "task.started",
       "task.completed",
       "task.completed",

+ 56 - 1
packages/core/src/__tests__/project-interaction.test.ts

@@ -1,4 +1,4 @@
-import { mkdtemp, mkdir, writeFile } from "node:fs/promises";
+import { mkdtemp, mkdir, rm, writeFile } from "node:fs/promises";
 import { tmpdir } from "node:os";
 import { tmpdir } from "node:os";
 import { join } from "node:path";
 import { join } from "node:path";
 import { afterAll, beforeAll, describe, expect, it, vi } from "vitest";
 import { afterAll, beforeAll, describe, expect, it, vi } from "vitest";
@@ -183,4 +183,59 @@ describe("project interaction control", () => {
     const persisted = await loadProjectSession(projectRoot);
     const persisted = await loadProjectSession(projectRoot);
     expect(persisted.activeBookId).toBe("night-harbor");
     expect(persisted.activeBookId).toBe("night-harbor");
   });
   });
+
+  it("persists a creation draft across freeform ideation turns", async () => {
+    const ideationRoot = await mkdtemp(join(tmpdir(), "inkos-project-ideation-"));
+    await writeFile(join(ideationRoot, "inkos.json"), JSON.stringify({ language: "zh" }), "utf-8");
+    await persistProjectSession(ideationRoot, createProjectSession(ideationRoot));
+
+    const tools = {
+      listBooks: vi.fn(async () => ["harbor"]),
+      developBookDraft: vi.fn(async () => ({
+        __interaction: {
+          responseText: "我先按港风商战悬疑收着。你更想写长篇连载,还是十来章能收住?",
+          details: {
+            creationDraft: {
+              concept: "港风商战悬疑,主角从灰产洗白。",
+              title: "夜港账本",
+              genre: "urban",
+              nextQuestion: "更想写长篇连载,还是十来章能收住?",
+              missingFields: ["targetChapters"],
+              readyToCreate: false,
+            },
+          },
+        },
+      })),
+      createBook: vi.fn(async () => ({ ok: true })),
+      exportBook: vi.fn(async () => ({ ok: true })),
+      writeNextChapter: vi.fn(async () => ({ ok: true })),
+      reviseDraft: vi.fn(async () => ({ ok: true })),
+      patchChapterText: vi.fn(async () => ({ ok: true })),
+      renameEntity: vi.fn(async () => ({ ok: true })),
+      updateCurrentFocus: vi.fn(async () => ({ ok: true })),
+      updateAuthorIntent: vi.fn(async () => ({ ok: true })),
+      writeTruthFile: vi.fn(async () => ({ ok: true })),
+    };
+
+    try {
+      const result = await processProjectInteractionInput({
+        projectRoot: ideationRoot,
+        input: "我想写个港风商战悬疑,主角从灰产洗白。",
+        tools,
+      });
+
+      expect(result.request.intent).toBe("develop_book");
+      expect(result.session.creationDraft).toEqual(expect.objectContaining({
+        title: "夜港账本",
+        genre: "urban",
+      }));
+
+      const persisted = await loadProjectSession(ideationRoot);
+      expect(persisted.creationDraft).toEqual(expect.objectContaining({
+        title: "夜港账本",
+      }));
+    } finally {
+      await rm(ideationRoot, { recursive: true, force: true });
+    }
+  });
 });
 });

+ 4 - 0
packages/core/src/index.ts

@@ -89,15 +89,19 @@ export {
   isTerminalExecutionStatus,
   isTerminalExecutionStatus,
 } from "./interaction/events.js";
 } from "./interaction/events.js";
 export {
 export {
+  BookCreationDraftSchema,
   PendingDecisionSchema,
   PendingDecisionSchema,
   InteractionMessageSchema,
   InteractionMessageSchema,
   InteractionSessionSchema,
   InteractionSessionSchema,
+  type BookCreationDraft,
   type PendingDecision,
   type PendingDecision,
   type InteractionMessage,
   type InteractionMessage,
   type InteractionSession,
   type InteractionSession,
   bindActiveBook,
   bindActiveBook,
+  clearCreationDraft,
   clearPendingDecision,
   clearPendingDecision,
   updateAutomationMode,
   updateAutomationMode,
+  updateCreationDraft,
   appendInteractionMessage,
   appendInteractionMessage,
   appendInteractionEvent,
   appendInteractionEvent,
 } from "./interaction/session.js";
 } from "./interaction/session.js";

+ 2 - 0
packages/core/src/interaction/intents.ts

@@ -2,7 +2,9 @@ import { z } from "zod";
 import { AutomationModeSchema } from "./modes.js";
 import { AutomationModeSchema } from "./modes.js";
 
 
 export const InteractionIntentTypeSchema = z.enum([
 export const InteractionIntentTypeSchema = z.enum([
+  "develop_book",
   "create_book",
   "create_book",
+  "discard_book_draft",
   "list_books",
   "list_books",
   "select_book",
   "select_book",
   "continue_book",
   "continue_book",

+ 22 - 2
packages/core/src/interaction/nl-router.ts

@@ -2,6 +2,7 @@ import type { InteractionRequest } from "./intents.js";
 
 
 export interface NaturalLanguageRoutingContext {
 export interface NaturalLanguageRoutingContext {
   readonly activeBookId?: string;
   readonly activeBookId?: string;
+  readonly hasCreationDraft?: boolean;
 }
 }
 
 
 export function routeNaturalLanguageIntent(
 export function routeNaturalLanguageIntent(
@@ -9,7 +10,6 @@ export function routeNaturalLanguageIntent(
   context: NaturalLanguageRoutingContext = {},
   context: NaturalLanguageRoutingContext = {},
 ): InteractionRequest {
 ): InteractionRequest {
   const trimmed = input.trim();
   const trimmed = input.trim();
-  const lower = trimmed.toLowerCase();
   const bookId = context.activeBookId;
   const bookId = context.activeBookId;
 
 
   if (/^(hi|hello|hey|你好|嗨|哈喽)$/i.test(trimmed)) {
   if (/^(hi|hello|hey|你好|嗨|哈喽)$/i.test(trimmed)) {
@@ -42,9 +42,22 @@ export function routeNaturalLanguageIntent(
 
 
   const newCommand = trimmed.match(/^\/new\s+(.+)$/i);
   const newCommand = trimmed.match(/^\/new\s+(.+)$/i);
   if (newCommand) {
   if (newCommand) {
+    return {
+      intent: "develop_book",
+      instruction: newCommand[1]!.trim(),
+    };
+  }
+
+  if (/^\/create$/i.test(trimmed)) {
     return {
     return {
       intent: "create_book",
       intent: "create_book",
-      title: newCommand[1]!.trim(),
+      ...(bookId ? { bookId } : {}),
+    };
+  }
+
+  if (/^\/discard$/i.test(trimmed)) {
+    return {
+      intent: "discard_book_draft",
     };
     };
   }
   }
 
 
@@ -223,6 +236,13 @@ export function routeNaturalLanguageIntent(
     };
     };
   }
   }
 
 
+  if (!bookId || context.hasCreationDraft) {
+    return {
+      intent: "develop_book",
+      instruction: trimmed,
+    };
+  }
+
   return {
   return {
     intent: "chat",
     intent: "chat",
     ...(bookId ? { bookId } : {}),
     ...(bookId ? { bookId } : {}),

+ 1 - 0
packages/core/src/interaction/project-control.ts

@@ -80,6 +80,7 @@ export async function processProjectInteractionInput(params: {
   });
   });
   const request = attachRequestLanguage(routeNaturalLanguageIntent(params.input, {
   const request = attachRequestLanguage(routeNaturalLanguageIntent(params.input, {
     activeBookId: userSession.activeBookId,
     activeBookId: userSession.activeBookId,
+    hasCreationDraft: Boolean(userSession.creationDraft),
   }), requestLanguage);
   }), requestLanguage);
   try {
   try {
     const result = await runInteractionRequest({
     const result = await runInteractionRequest({

+ 147 - 0
packages/core/src/interaction/project-tools.ts

@@ -13,6 +13,7 @@ import type {
 import { chatCompletion } from "../index.js";
 import { chatCompletion } from "../index.js";
 import { executeEditTransaction } from "./edit-controller.js";
 import { executeEditTransaction } from "./edit-controller.js";
 import type { InteractionRuntimeTools } from "./runtime.js";
 import type { InteractionRuntimeTools } from "./runtime.js";
+import type { BookCreationDraft } from "./session.js";
 
 
 type PipelineLike = Pick<PipelineRunner, "writeNextChapter" | "reviseDraft"> & {
 type PipelineLike = Pick<PipelineRunner, "writeNextChapter" | "reviseDraft"> & {
   readonly initBook?: PipelineRunner["initBook"];
   readonly initBook?: PipelineRunner["initBook"];
@@ -37,6 +38,84 @@ function normalizePlatform(platform?: string): Platform {
   }
   }
 }
 }
 
 
+function extractBalancedJsonObject(text: string): string | null {
+  const start = text.indexOf("{");
+  if (start < 0) {
+    return null;
+  }
+
+  let depth = 0;
+  let inString = false;
+  let escaped = false;
+
+  for (let index = start; index < text.length; index += 1) {
+    const char = text[index]!;
+
+    if (inString) {
+      if (escaped) {
+        escaped = false;
+        continue;
+      }
+      if (char === "\\") {
+        escaped = true;
+        continue;
+      }
+      if (char === "\"") {
+        inString = false;
+      }
+      continue;
+    }
+
+    if (char === "\"") {
+      inString = true;
+      continue;
+    }
+
+    if (char === "{") {
+      depth += 1;
+      continue;
+    }
+
+    if (char === "}") {
+      depth -= 1;
+      if (depth === 0) {
+        return text.slice(start, index + 1);
+      }
+      if (depth < 0) {
+        return null;
+      }
+    }
+  }
+
+  return null;
+}
+
+function parseCreationDraftResult(text: string): {
+  readonly assistantReply: string;
+  readonly draft: BookCreationDraft;
+} | null {
+  const candidate = extractBalancedJsonObject(text);
+  if (!candidate) {
+    return null;
+  }
+
+  try {
+    const parsed = JSON.parse(candidate) as {
+      assistantReply?: string;
+      draft?: BookCreationDraft;
+    };
+    if (!parsed.assistantReply || !parsed.draft) {
+      return null;
+    }
+    return {
+      assistantReply: parsed.assistantReply,
+      draft: parsed.draft,
+    };
+  } catch {
+    return null;
+  }
+}
+
 function deriveBookId(title: string): string {
 function deriveBookId(title: string): string {
   return title
   return title
     .toLowerCase()
     .toLowerCase()
@@ -327,6 +406,74 @@ export function createInteractionToolsFromDeps(
 
 
   return {
   return {
     listBooks: () => state.listBooks(),
     listBooks: () => state.listBooks(),
+    developBookDraft: async (input, existingDraft) => {
+      if (!instrumentedPipeline.config?.client || !instrumentedPipeline.config?.model) {
+        const concept = existingDraft?.concept ?? input;
+        return {
+          __interaction: {
+            responseText: "先把这本书的大概方向收住。你更想写长篇连载,还是十来章能收住的版本?",
+            details: {
+              creationDraft: {
+                concept,
+                title: existingDraft?.title,
+                genre: existingDraft?.genre,
+                platform: existingDraft?.platform,
+                language: existingDraft?.language,
+                targetChapters: existingDraft?.targetChapters,
+                chapterWordCount: existingDraft?.chapterWordCount,
+                blurb: existingDraft?.blurb,
+                authorIntent: existingDraft?.authorIntent,
+                currentFocus: existingDraft?.currentFocus,
+                nextQuestion: "你更想写长篇连载,还是十来章能收住的版本?",
+                missingFields: existingDraft?.missingFields ?? ["title", "genre", "targetChapters"],
+                readyToCreate: existingDraft?.readyToCreate ?? false,
+              } satisfies BookCreationDraft,
+            },
+          },
+        };
+      }
+
+      const response = await chatCompletion(
+        instrumentedPipeline.config.client,
+        instrumentedPipeline.config.model,
+        [
+          {
+            role: "system",
+            content: [
+              "You are InkOS book ideation assistant.",
+              "Turn the user's latest message and the current draft into a tighter book creation draft.",
+              "Ask at most one sharp next question.",
+              "Default to concise Chinese unless the draft language is clearly English.",
+              "Return JSON only with keys assistantReply and draft.",
+              "draft must include concept and may include title, genre, platform, language, targetChapters, chapterWordCount, blurb, authorIntent, currentFocus, nextQuestion, missingFields, readyToCreate.",
+              "Be conservative: only mark readyToCreate=true when the draft already has a workable title, genre, targetChapters, and chapterWordCount.",
+            ].join(" "),
+          },
+          {
+            role: "user",
+            content: JSON.stringify({
+              currentDraft: existingDraft ?? null,
+              latestMessage: input,
+            }, null, 2),
+          },
+        ],
+        { temperature: 0.4, maxTokens: 700 },
+      );
+
+      const parsed = parseCreationDraftResult(response.content);
+      if (!parsed) {
+        throw new Error("Book draft assistant returned invalid JSON.");
+      }
+
+      return {
+        __interaction: {
+          responseText: parsed.assistantReply,
+          details: {
+            creationDraft: parsed.draft,
+          },
+        },
+      };
+    },
     createBook: async (input) => {
     createBook: async (input) => {
       const book = buildBookConfig(input);
       const book = buildBookConfig(input);
       if (!pipeline.initBook) {
       if (!pipeline.initBook) {

+ 79 - 8
packages/core/src/interaction/runtime.ts

@@ -6,7 +6,9 @@ import type { PendingDecision, InteractionSession } from "./session.js";
 import {
 import {
   appendInteractionEvent,
   appendInteractionEvent,
   bindActiveBook,
   bindActiveBook,
+  clearCreationDraft,
   clearPendingDecision,
   clearPendingDecision,
+  updateCreationDraft,
   updateAutomationMode,
   updateAutomationMode,
 } from "./session.js";
 } from "./session.js";
 
 
@@ -15,6 +17,10 @@ type RuntimeLanguage = "zh" | "en";
 
 
 export interface InteractionRuntimeTools {
 export interface InteractionRuntimeTools {
   readonly listBooks: () => Promise<ReadonlyArray<string>>;
   readonly listBooks: () => Promise<ReadonlyArray<string>>;
+  readonly developBookDraft?: (
+    input: string,
+    existingDraft?: InteractionSession["creationDraft"],
+  ) => Promise<unknown>;
   readonly createBook?: (input: {
   readonly createBook?: (input: {
     readonly title: string;
     readonly title: string;
     readonly genre?: string;
     readonly genre?: string;
@@ -129,6 +135,15 @@ function buildTaskStartedState(
           en: "preparing chapter inputs",
           en: "preparing chapter inputs",
         }),
         }),
       };
       };
+    case "develop_book":
+      return {
+        status: "planning",
+        bookId: request.bookId ?? session.activeBookId,
+        stageLabel: localize(language, {
+          zh: "收敛创作草案",
+          en: "developing book draft",
+        }),
+      };
     case "create_book":
     case "create_book":
       return {
       return {
         status: "planning",
         status: "planning",
@@ -171,6 +186,7 @@ function buildTaskStartedState(
         }),
         }),
       };
       };
     case "pause_book":
     case "pause_book":
+    case "discard_book_draft":
       return {
       return {
         status: "blocked",
         status: "blocked",
         bookId: request.bookId ?? session.activeBookId,
         bookId: request.bookId ?? session.activeBookId,
@@ -328,6 +344,46 @@ export async function runInteractionRequest(params: {
   });
   });
 
 
   switch (request.intent) {
   switch (request.intent) {
+    case "develop_book": {
+      if (!params.tools.developBookDraft) {
+        throw new Error(localize(language, {
+          zh: "创作草案会话暂未实现。",
+          en: "Book-draft ideation is not implemented yet.",
+        }));
+      }
+      if (!request.instruction) {
+        throw new Error(localize(language, {
+          zh: "创作草案需要一条用户输入。",
+          en: "Book-draft ideation requires user input.",
+        }));
+      }
+      const toolResult = await params.tools.developBookDraft(request.instruction, session.creationDraft);
+      const metadata = extractToolMetadata(toolResult);
+      const draft = metadata.details?.creationDraft as InteractionSession["creationDraft"] | undefined;
+      if (!draft) {
+        throw new Error(localize(language, {
+          zh: "创作草案工具没有返回草案数据。",
+          en: "Book-draft tool did not return draft data.",
+        }));
+      }
+      session = updateCreationDraft(session, draft);
+      session = appendToolEvents(session, metadata.events);
+      const completed = {
+        ...markCompleted(session),
+        currentExecution: metadata.currentExecution ?? markCompleted(session).currentExecution,
+      };
+      return {
+        session: addEvent(completed, "task.completed", "completed", localize(language, {
+          zh: "已更新创作草案。",
+          en: "Updated the book draft.",
+        })),
+        responseText: metadata.responseText ?? localize(language, {
+          zh: "已更新创作草案。",
+          en: "Updated the book draft.",
+        }),
+        details: metadata.details,
+      };
+    }
     case "create_book": {
     case "create_book": {
       if (!params.tools.createBook) {
       if (!params.tools.createBook) {
         throw new Error(localize(language, {
         throw new Error(localize(language, {
@@ -335,19 +391,21 @@ export async function runInteractionRequest(params: {
           en: "Book creation is not implemented in the interaction runtime yet.",
           en: "Book creation is not implemented in the interaction runtime yet.",
         }));
         }));
       }
       }
-      if (!request.title) {
+      const effectiveDraft = session.creationDraft;
+      const title = request.title ?? effectiveDraft?.title;
+      if (!title) {
         throw new Error(localize(language, {
         throw new Error(localize(language, {
           zh: "创建作品需要标题。",
           zh: "创建作品需要标题。",
           en: "Book creation requires a title.",
           en: "Book creation requires a title.",
         }));
         }));
       }
       }
       const toolResult = await params.tools.createBook({
       const toolResult = await params.tools.createBook({
-        title: request.title,
-        genre: request.genre,
-        platform: request.platform,
-        language: request.language,
-        chapterWordCount: request.chapterWordCount,
-        targetChapters: request.targetChapters,
+        title,
+        genre: request.genre ?? effectiveDraft?.genre,
+        platform: request.platform ?? effectiveDraft?.platform,
+        language: request.language ?? effectiveDraft?.language,
+        chapterWordCount: request.chapterWordCount ?? effectiveDraft?.chapterWordCount,
+        targetChapters: request.targetChapters ?? effectiveDraft?.targetChapters,
       });
       });
       const metadata = extractToolMetadata(toolResult);
       const metadata = extractToolMetadata(toolResult);
       const createdBookId = typeof toolResult === "object" && toolResult !== null && "bookId" in toolResult
       const createdBookId = typeof toolResult === "object" && toolResult !== null && "bookId" in toolResult
@@ -360,7 +418,7 @@ export async function runInteractionRequest(params: {
           en: "Create-book tool did not return a book id.",
           en: "Create-book tool did not return a book id.",
         }));
         }));
       }
       }
-      session = bindActiveBook(session, createdBookId);
+      session = clearCreationDraft(bindActiveBook(session, createdBookId));
       session = appendToolEvents(session, metadata.events);
       session = appendToolEvents(session, metadata.events);
       const completed = {
       const completed = {
         ...markCompleted(session),
         ...markCompleted(session),
@@ -378,6 +436,19 @@ export async function runInteractionRequest(params: {
         details: metadata.details,
         details: metadata.details,
       };
       };
     }
     }
+    case "discard_book_draft": {
+      const completed = markCompleted(clearCreationDraft(session));
+      return {
+        session: addEvent(completed, "task.completed", "completed", localize(language, {
+          zh: "已丢弃当前创作草案。",
+          en: "Discarded the current book draft.",
+        })),
+        responseText: localize(language, {
+          zh: "已丢弃当前创作草案。",
+          en: "Discarded the current book draft.",
+        }),
+      };
+    }
     case "write_next":
     case "write_next":
     case "continue_book": {
     case "continue_book": {
       const bookId = request.bookId ?? session.activeBookId;
       const bookId = request.bookId ?? session.activeBookId;

+ 40 - 0
packages/core/src/interaction/session.ts

@@ -19,11 +19,30 @@ export const InteractionMessageSchema = z.object({
 
 
 export type InteractionMessage = z.infer<typeof InteractionMessageSchema>;
 export type InteractionMessage = z.infer<typeof InteractionMessageSchema>;
 
 
+export const BookCreationDraftSchema = z.object({
+  concept: z.string().min(1),
+  title: z.string().min(1).optional(),
+  genre: z.string().min(1).optional(),
+  platform: z.string().min(1).optional(),
+  language: z.enum(["zh", "en"]).optional(),
+  targetChapters: z.number().int().min(1).optional(),
+  chapterWordCount: z.number().int().min(1).optional(),
+  blurb: z.string().min(1).optional(),
+  authorIntent: z.string().min(1).optional(),
+  currentFocus: z.string().min(1).optional(),
+  nextQuestion: z.string().min(1).optional(),
+  missingFields: z.array(z.string().min(1)).default([]),
+  readyToCreate: z.boolean().default(false),
+});
+
+export type BookCreationDraft = z.infer<typeof BookCreationDraftSchema>;
+
 export const InteractionSessionSchema = z.object({
 export const InteractionSessionSchema = z.object({
   sessionId: z.string().min(1),
   sessionId: z.string().min(1),
   projectRoot: z.string().min(1),
   projectRoot: z.string().min(1),
   activeBookId: z.string().min(1).optional(),
   activeBookId: z.string().min(1).optional(),
   activeChapterNumber: z.number().int().min(1).optional(),
   activeChapterNumber: z.number().int().min(1).optional(),
+  creationDraft: BookCreationDraftSchema.optional(),
   automationMode: AutomationModeSchema.default("semi"),
   automationMode: AutomationModeSchema.default("semi"),
   messages: z.array(InteractionMessageSchema).default([]),
   messages: z.array(InteractionMessageSchema).default([]),
   events: z.array(InteractionEventSchema).default([]),
   events: z.array(InteractionEventSchema).default([]),
@@ -56,6 +75,27 @@ export function clearPendingDecision(session: InteractionSession): InteractionSe
   };
   };
 }
 }
 
 
+export function updateCreationDraft(
+  session: InteractionSession,
+  draft: BookCreationDraft,
+): InteractionSession {
+  return {
+    ...session,
+    creationDraft: draft,
+  };
+}
+
+export function clearCreationDraft(session: InteractionSession): InteractionSession {
+  if (!session.creationDraft) {
+    return session;
+  }
+
+  return {
+    ...session,
+    creationDraft: undefined,
+  };
+}
+
 export function updateAutomationMode(
 export function updateAutomationMode(
   session: InteractionSession,
   session: InteractionSession,
   automationMode: AutomationMode,
   automationMode: AutomationMode,

+ 32 - 0
packages/studio/src/api/server.test.ts

@@ -675,4 +675,36 @@ describe("createStudioServer daemon lifecycle", () => {
       activeBookId: "demo-book",
       activeBookId: "demo-book",
     });
     });
   });
   });
+
+  it("returns creation-draft state through the shared interaction session endpoint", async () => {
+    loadProjectSessionMock.mockResolvedValue({
+      sessionId: "session-3",
+      projectRoot: root,
+      automationMode: "semi",
+      creationDraft: {
+        concept: "港风商战悬疑,主角从灰产洗白。",
+        title: "夜港账本",
+        nextQuestion: "你更想写长篇连载,还是十来章能收住?",
+        missingFields: ["targetChapters"],
+        readyToCreate: false,
+      },
+      messages: [],
+    });
+    resolveSessionActiveBookMock.mockResolvedValue(undefined);
+
+    const { createStudioServer } = await import("./server.js");
+    const app = createStudioServer(cloneProjectConfig() as never, root);
+
+    const response = await app.request("http://localhost/api/interaction/session");
+
+    expect(response.status).toBe(200);
+    await expect(response.json()).resolves.toMatchObject({
+      session: expect.objectContaining({
+        creationDraft: expect.objectContaining({
+          title: "夜港账本",
+          nextQuestion: "你更想写长篇连载,还是十来章能收住?",
+        }),
+      }),
+    });
+  });
 });
 });

+ 10 - 0
packages/studio/src/components/ChatBar.tsx

@@ -46,6 +46,7 @@ interface SharedSessionMeta {
   readonly automationMode?: string;
   readonly automationMode?: string;
   readonly currentStage?: string;
   readonly currentStage?: string;
   readonly pendingSummary?: string;
   readonly pendingSummary?: string;
+  readonly draftTitle?: string;
 }
 }
 
 
 interface BookRef {
 interface BookRef {
@@ -83,6 +84,7 @@ export function resolveDirectWriteTarget(
 export function formatSharedSessionContext(meta: SharedSessionMeta): string {
 export function formatSharedSessionContext(meta: SharedSessionMeta): string {
   return [
   return [
     meta.activeBookId ?? "no-book",
     meta.activeBookId ?? "no-book",
+    meta.draftTitle ? `draft:${meta.draftTitle}` : undefined,
     meta.automationMode ?? "semi",
     meta.automationMode ?? "semi",
     meta.currentStage,
     meta.currentStage,
   ].filter(Boolean).join(" · ");
   ].filter(Boolean).join(" · ");
@@ -255,6 +257,9 @@ export function ChatPanel({ open, onClose, t, sse, activeBookId }: {
       session?: {
       session?: {
         activeBookId?: string;
         activeBookId?: string;
         automationMode?: string;
         automationMode?: string;
+        creationDraft?: {
+          title?: string;
+        };
         currentExecution?: {
         currentExecution?: {
           status?: string;
           status?: string;
           stageLabel?: string;
           stageLabel?: string;
@@ -269,6 +274,7 @@ export function ChatPanel({ open, onClose, t, sse, activeBookId }: {
       if (cancelled) return;
       if (cancelled) return;
       setSessionMeta({
       setSessionMeta({
         activeBookId: data.activeBookId ?? data.session?.activeBookId,
         activeBookId: data.activeBookId ?? data.session?.activeBookId,
+        draftTitle: data.session?.creationDraft?.title,
         automationMode: data.session?.automationMode,
         automationMode: data.session?.automationMode,
         currentStage: data.session?.currentExecution?.stageLabel ?? data.session?.currentExecution?.status,
         currentStage: data.session?.currentExecution?.stageLabel ?? data.session?.currentExecution?.status,
         pendingSummary: data.session?.pendingDecision?.summary,
         pendingSummary: data.session?.pendingDecision?.summary,
@@ -373,6 +379,9 @@ export function ChatPanel({ open, onClose, t, sse, activeBookId }: {
         session?: {
         session?: {
           activeBookId?: string;
           activeBookId?: string;
           automationMode?: string;
           automationMode?: string;
+          creationDraft?: {
+            title?: string;
+          };
           currentExecution?: {
           currentExecution?: {
             status?: string;
             status?: string;
             stageLabel?: string;
             stageLabel?: string;
@@ -391,6 +400,7 @@ export function ChatPanel({ open, onClose, t, sse, activeBookId }: {
       if (data.session) {
       if (data.session) {
         setSessionMeta({
         setSessionMeta({
           activeBookId: data.session.activeBookId ?? activeBookId,
           activeBookId: data.session.activeBookId ?? activeBookId,
+          draftTitle: data.session.creationDraft?.title,
           automationMode: data.session.automationMode,
           automationMode: data.session.automationMode,
           currentStage: data.session.currentExecution?.stageLabel ?? data.session.currentExecution?.status,
           currentStage: data.session.currentExecution?.stageLabel ?? data.session.currentExecution?.status,
           pendingSummary: data.session.pendingDecision?.summary,
           pendingSummary: data.session.pendingDecision?.summary,

+ 8 - 0
packages/studio/src/components/chatbar-state.test.ts

@@ -58,4 +58,12 @@ describe("resolveDirectWriteTarget", () => {
       currentStage: "waiting for your next decision",
       currentStage: "waiting for your next decision",
     })).toBe("harbor · semi · waiting for your next decision");
     })).toBe("harbor · semi · waiting for your next decision");
   });
   });
+
+  it("surfaces creation-draft context when no active book is bound yet", () => {
+    expect(formatSharedSessionContext({
+      draftTitle: "夜港账本",
+      automationMode: "semi",
+      currentStage: "developing book draft",
+    })).toBe("no-book · draft:夜港账本 · semi · developing book draft");
+  });
 });
 });