@cyber-dash-tech/revela 0.7.9 → 0.8.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  **English** | [中文](README.zh-CN.md)
4
4
 
5
- [![npm version](https://img.shields.io/npm/v/@cyber-dash-tech/revela)](https://www.npmjs.com/package/@cyber-dash-tech/revela) [![license](https://img.shields.io/npm/l/@cyber-dash-tech/revela)](LICENSE) [![tests](https://img.shields.io/badge/tests-171%20passing-brightgreen)](tests/) [![OpenCode plugin](https://img.shields.io/badge/OpenCode-plugin-blue)](https://opencode.ai) [![Bun](https://img.shields.io/badge/Bun-%E2%89%A51.0-orange)](https://bun.sh)
5
+ [![npm version](https://img.shields.io/npm/v/@cyber-dash-tech/revela)](https://www.npmjs.com/package/@cyber-dash-tech/revela) [![license](https://img.shields.io/npm/l/@cyber-dash-tech/revela)](LICENSE) [![tests](https://img.shields.io/badge/tests-178%20passing-brightgreen)](tests/) [![OpenCode plugin](https://img.shields.io/badge/OpenCode-plugin-blue)](https://opencode.ai) [![Bun](https://img.shields.io/badge/Bun-%E2%89%A51.0-orange)](https://bun.sh)
6
6
 
7
7
  <p align="center">
8
8
  <img src="assets/img/logo.png" alt="Revela" width="800" />
@@ -112,7 +112,7 @@ Create a 6-slide HTML deck on humanoid robotics supply chains. Cite the main mar
112
112
  Before the agent writes `decks/humanoid-robotics.html`, it must update `DECKS.json` through the `revela-decks` tool with the active deck, confirmed slide specs, layouts, components, and computed `writeReadiness.status: ready`. You can ask for an explicit readiness check at any time:
113
113
 
114
114
  ```text
115
- /revela review humanoid-robotics
115
+ /revela review
116
116
  ```
117
117
 
118
118
  Export when needed, either manually or by asking the agent to export:
@@ -138,9 +138,9 @@ Disable presentation mode when done:
138
138
  /revela disable disable presentation mode
139
139
 
140
140
  /revela init initialize or refresh workspace DECKS.json
141
- /revela review [slug] review active deck readiness before writing HTML
141
+ /revela review review current deck readiness before writing HTML
142
142
  /revela remember <text> save an explicit user/workflow preference
143
- /revela edit [target] open visual editor for active deck or a specific target
143
+ /revela edit open visual editor for the only deck in decks/
144
144
 
145
145
  /revela designs list installed designs
146
146
  /revela designs <name> activate a design
@@ -185,7 +185,7 @@ Revela uses a workspace-root `DECKS.json` file for cross-session continuity and
185
185
  It has two jobs:
186
186
 
187
187
  - workspace memory: stable project context, source materials, explicit user preferences, deck history, and open questions
188
- - active deck spec: current deck slug, output path, prerequisites, research plan, per-slide content, layouts, components, evidence, visuals, blockers, and write readiness
188
+ - active deck spec: current deck output path, prerequisites, research plan, per-slide content, layouts, components, evidence, visuals, blockers, and write readiness
189
189
 
190
190
  `DECKS.json` is the source of truth for workspace memory and deck readiness.
191
191
 
@@ -198,7 +198,7 @@ Create or refresh it with:
198
198
  Review the current deck state with:
199
199
 
200
200
  ```text
201
- /revela review [slug]
201
+ /revela review
202
202
  ```
203
203
 
204
204
  `/revela review` does not write the final HTML deck. It reads and updates `DECKS.json` through the `revela-decks` tool, checks what is missing, and sets `writeReadiness.status` to `ready` only when the deck is ready to generate.
@@ -593,19 +593,17 @@ A custom domain is a folder containing `INDUSTRY.md`.
593
593
 
594
594
  ## Visual Editing
595
595
 
596
- Open the visual editor for the active deck, or pass a slug / workspace-relative HTML path:
596
+ Open the visual editor for the only HTML deck in `decks/`:
597
597
 
598
598
  ```text
599
599
  /revela edit
600
- /revela edit my-deck
601
- /revela edit decks/my-deck.html
602
600
  ```
603
601
 
604
- Without a target, `/revela edit` opens `DECKS.json.activeDeck`. If no active deck is set and there is exactly one deck in `DECKS.json`, it opens that deck.
602
+ `/revela edit` does not accept a target in Revela 0.8. If `decks/` contains exactly one `.html` file, Revela opens it. If `decks/` contains zero or multiple `.html` files, Revela asks you to generate a deck first or move extra decks to separate workspaces.
605
603
 
606
604
  The editor opens in your browser. Use `Ctrl`/`Cmd` + click to reference deck elements, write a natural-language comment, then send it back to OpenCode. Revela sends a structured edit prompt that includes the deck file, slide context, selected element metadata, and your comment.
607
605
 
608
- LLM tool equivalent: `revela-edit` with `{ "target": "decks/my-deck.html" }`. This lets the agent open the same editor when you say things like “I want to edit @decks/my-deck.html”.
606
+ LLM tool equivalent: `revela-edit` with no target. This lets the agent open the same editor when you say things like “I want to edit the deck”.
609
607
 
610
608
  `/revela edit` prepares minimal `DECKS.json` state for the existing HTML deck if needed, so the normal deck write gate can still protect `decks/*.html` while allowing targeted edits.
611
609
 
package/README.zh-CN.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  [English](README.md) | **中文**
4
4
 
5
- [![npm version](https://img.shields.io/npm/v/@cyber-dash-tech/revela)](https://www.npmjs.com/package/@cyber-dash-tech/revela) [![license](https://img.shields.io/npm/l/@cyber-dash-tech/revela)](LICENSE) [![tests](https://img.shields.io/badge/tests-171%20passing-brightgreen)](tests/) [![OpenCode plugin](https://img.shields.io/badge/OpenCode-plugin-blue)](https://opencode.ai) [![Bun](https://img.shields.io/badge/Bun-%E2%89%A51.0-orange)](https://bun.sh)
5
+ [![npm version](https://img.shields.io/npm/v/@cyber-dash-tech/revela)](https://www.npmjs.com/package/@cyber-dash-tech/revela) [![license](https://img.shields.io/npm/l/@cyber-dash-tech/revela)](LICENSE) [![tests](https://img.shields.io/badge/tests-178%20passing-brightgreen)](tests/) [![OpenCode plugin](https://img.shields.io/badge/OpenCode-plugin-blue)](https://opencode.ai) [![Bun](https://img.shields.io/badge/Bun-%E2%89%A51.0-orange)](https://bun.sh)
6
6
 
7
7
  <p align="center">
8
8
  <img src="assets/img/logo.png" alt="Revela" width="800" />
@@ -111,7 +111,7 @@ Create a 6-slide HTML deck on humanoid robotics supply chains. Cite the main mar
111
111
  在 agent 写入 `decks/humanoid-robotics.html` 之前,它必须通过 `revela-decks` 工具更新 `DECKS.json`:记录 active deck、已确认的逐页规格、layout、component,并由工具计算出 `writeReadiness.status: ready`。你也可以随时显式触发 readiness 检查:
112
112
 
113
113
  ```text
114
- /revela review humanoid-robotics
114
+ /revela review
115
115
  ```
116
116
 
117
117
  需要导出时,可以手动调用,也可以让 agent 直接导出:
@@ -137,9 +137,9 @@ Create a 6-slide HTML deck on humanoid robotics supply chains. Cite the main mar
137
137
  /revela disable 关闭演示文稿模式
138
138
 
139
139
  /revela init 初始化或刷新工作区 DECKS.json
140
- /revela review [slug] 写 HTML 前检查 active deck readiness
140
+ /revela review 写 HTML 前检查当前 deck readiness
141
141
  /revela remember <text> 保存明确的用户/工作流偏好
142
- /revela edit [target] 打开 active deck 或指定 target 的可视化编辑器
142
+ /revela edit 打开 decks/ 下唯一 deck 的可视化编辑器
143
143
 
144
144
  /revela designs 列出已安装 design
145
145
  /revela designs <name> 激活某个 design
@@ -184,7 +184,7 @@ Revela 使用工作区根目录的 `DECKS.json` 做跨会话记忆和 deck 生
184
184
  它有两个职责:
185
185
 
186
186
  - 工作区记忆:稳定项目背景、源材料、明确用户偏好、历史 deck 和开放问题
187
- - active deck 规格:当前 deck slug、输出路径、前置条件、research plan、逐页内容、layout、component、证据、视觉需求、blocker 和 write readiness
187
+ - active deck 规格:当前 deck 输出路径、前置条件、research plan、逐页内容、layout、component、证据、视觉需求、blocker 和 write readiness
188
188
 
189
189
  `DECKS.json` 是工作区记忆和 deck readiness 的 source of truth。
190
190
 
@@ -197,7 +197,7 @@ Revela 使用工作区根目录的 `DECKS.json` 做跨会话记忆和 deck 生
197
197
  检查当前 deck 状态:
198
198
 
199
199
  ```text
200
- /revela review [slug]
200
+ /revela review
201
201
  ```
202
202
 
203
203
  `/revela review` 不会写最终 HTML deck。它通过 `revela-decks` 工具读取并更新 `DECKS.json`,检查缺失项,并且只有在 deck 真的可以生成时才把 `writeReadiness.status` 设为 `ready`。
@@ -558,19 +558,17 @@ Prompt 注入规则:
558
558
 
559
559
  ## 可视化编辑
560
560
 
561
- 可以直接打开 active deck,也可以传入 deck slug 或工作区相对 HTML 路径:
561
+ 打开 `decks/` 下唯一 HTML deck 的可视化编辑器:
562
562
 
563
563
  ```text
564
564
  /revela edit
565
- /revela edit my-deck
566
- /revela edit decks/my-deck.html
567
565
  ```
568
566
 
569
- 不传 target 时,`/revela edit` 会打开 `DECKS.json.activeDeck`。如果没有 active deck,但 `DECKS.json` 里只有一个 deck,则打开这个唯一 deck。
567
+ Revela 0.8 中 `/revela edit` 不接受 target。如果 `decks/` 下正好有一个 `.html` 文件,Revela 会打开它。如果 `decks/` 下没有 HTML 或有多个 HTML,Revela 会提示先生成 deck,或把多余 deck 移到独立 workspace
570
568
 
571
569
  编辑器会在浏览器中打开。使用 `Ctrl`/`Cmd` + 点击 deck 元素来引用它们,写一段自然语言评论,然后发送回 OpenCode。Revela 会把 deck 文件、slide 上下文、选中元素 metadata 和你的评论整理成结构化 edit prompt。
572
570
 
573
- 对应的 LLM tool:`revela-edit`,参数为 `{ "target": "decks/my-deck.html" }`。因此当你说“我要编辑 @decks/my-deck.html”时,agent 也可以主动打开同一个编辑器。
571
+ 对应的 LLM tool:`revela-edit`,不需要 target。因此当你说“我要编辑这个 deck”时,agent 也可以主动打开同一个编辑器。
574
572
 
575
573
  如果已有 HTML deck 缺少 `DECKS.json` 状态,`/revela edit` 会自动准备最小 deck state,让正常的 `decks/*.html` 写入门禁仍然生效,同时允许后续精准修改。
576
574
 
@@ -29,7 +29,7 @@ Given a research brief specifying your topic and axis, you will:
29
29
  2. Use \`DECKS.json\` through \`revela-decks\` as the workspace material index when it exists
30
30
  3. Run a lightweight workspace freshness check when needed
31
31
  4. Search the web for current data, reports, and case studies when the brief requires it
32
- 5. Write all findings to ONE structured file: \`researches/{topic-slug}/{axis-name}.md\`
32
+ 5. Write all findings to ONE structured file: \`researches/{topic-key}/{axis-name}.md\`
33
33
  6. Return a brief summary of what you found
34
34
 
35
35
  ---
@@ -37,7 +37,7 @@ Given a research brief specifying your topic and axis, you will:
37
37
  ## Step 1 — Research brief and workspace memory
38
38
 
39
39
  Start from the research brief supplied by the primary agent. It should include:
40
- - shared topic slug
40
+ - shared topic key
41
41
  - your axis filename
42
42
  - the specific question for this axis
43
43
  - time period, geography, and evidence standard
@@ -100,7 +100,7 @@ Search strategy:
100
100
 
101
101
  Use **\`revela-research-save\`** to write ONE file with all your findings.
102
102
 
103
- - \`topic\`: kebab-case slug shared across all agents for this presentation
103
+ - \`topic\`: kebab-case topic key shared across all agents for this presentation
104
104
  - \`filename\`: your axis name (e.g. \`market-data\`, \`catl-profile\`, \`tech-trends\`)
105
105
  - \`content\`: structured findings using the four sections below
106
106
  - \`sources\`: list of all URLs and filenames used
@@ -140,7 +140,7 @@ Content rules:
140
140
  After writing the file, return this summary (do NOT include the raw data):
141
141
 
142
142
  \`\`\`
143
- Research complete: {axis-name} → researches/{topic-slug}/{axis-name}.md
143
+ Research complete: {axis-name} → researches/{topic-key}/{axis-name}.md
144
144
 
145
145
  Key findings (3–5, most argument-worthy only):
146
146
  - {1–2 sentence highlight with source}
@@ -1,22 +1,19 @@
1
1
  import { openEditableDeck } from "../edit/open"
2
2
 
3
3
  export async function handleEdit(
4
- input: string,
5
4
  options: { client: any; sessionID: string; workspaceRoot: string },
6
5
  send: (text: string) => Promise<void>,
7
6
  ): Promise<void> {
8
- const target = input.trim()
9
-
10
7
  try {
11
- const result = openEditableDeck(target, {
8
+ const result = openEditableDeck("", {
12
9
  client: options.client,
13
10
  sessionID: options.sessionID,
14
11
  workspaceRoot: options.workspaceRoot,
15
12
  })
16
13
 
17
14
  await send(
18
- `Opened visual editor for deck \`${result.deck.slug}\`.\n` +
19
- `File: \`${result.deck.file}\` (${result.source})\n` +
15
+ `Opened visual editor for the only deck in \`decks/\`.\n` +
16
+ `File: \`${result.deck.file}\`\n` +
20
17
  `${result.stateNote}\n` +
21
18
  `URL: ${result.url}\n\n` +
22
19
  `Use Ctrl/Cmd + click in the browser to reference elements, write a comment, then send comments. Revela mode has been enabled for the edit prompt.`
@@ -27,8 +27,8 @@ export async function handleHelp(
27
27
  `\`/revela enable\` — enable slide generation mode\n` +
28
28
  `\`/revela disable\` — disable slide generation mode\n` +
29
29
  `\`/revela init\` — initialize or refresh workspace DECKS.json\n` +
30
- `\`/revela review [slug]\` — review active deck readiness before writing HTML\n` +
31
- `\`/revela edit [target]\` — open visual editor for active deck or a specific target\n` +
30
+ `\`/revela review\` — review current deck readiness before writing HTML\n` +
31
+ `\`/revela edit\` — open visual editor for the only deck in decks/\n` +
32
32
  `\`/revela remember <text>\` — save an explicit preference to DECKS.json\n` +
33
33
  `\`/revela designs\` — list installed designs\n` +
34
34
  `\`/revela designs <name>\` — activate a design\n` +
@@ -16,9 +16,9 @@ export function buildInitPrompt({
16
16
  Goal:
17
17
  - Build or update ${DECKS_STATE_FILE}, the workspace-level machine-readable state file for slide deck work.
18
18
  - Use the \`revela-decks\` tool for state updates. Do not write or patch ${DECKS_STATE_FILE} directly.
19
- - Capture stable project context, available source materials, deck history, active deck specs, slide plans, and open questions for future sessions.
20
- - Do not treat initialization as permission to write a slide deck; each deck must pass a later readiness review.
21
- - ${DECKS_STATE_FILE} is the source of truth for Revela workspace state.
19
+ - Capture stable project context, available source materials, the current deck spec, slide plan, and open questions for future sessions.
20
+ - Do not treat initialization as permission to write a slide deck; the current deck must pass a later readiness review.
21
+ - ${DECKS_STATE_FILE} is the source of truth for the single current workspace deck.
22
22
 
23
23
  Current state:
24
24
  - ${mode}
@@ -40,12 +40,12 @@ Workflow:
40
40
  - \`presentations/**/*.html\`
41
41
  - \`decks/**/*.pdf\`
42
42
  - \`slides/**/*.pdf\`
43
- Run these searches only inside the current workspace root. These are generated/output decks, not necessarily source materials. Record stable history, themes, filenames, and obvious reuse opportunities without treating them as authoritative source data.
43
+ Run these searches only inside the current workspace root. These are generated/output decks, not necessarily source materials. If \`decks/\` contains exactly one HTML file, treat it as the current deck artifact. If \`decks/\` contains multiple HTML files, stop and ask the user to move extra decks to separate workspaces before adopting one.
44
44
  3. Select the files that look most relevant for future slide decks. Prioritize source decks, PDFs, Word docs, spreadsheets, CSVs, Markdown, text notes, and relevant existing generated decks.
45
45
  4. For selected PDF/PPTX/DOCX/XLSX files, call \`revela-extract-document-materials\` before deciding what to summarize.
46
46
  5. Read only the materials needed to form a conservative workspace memory. Do not exhaustively read every file if the workspace is large.
47
47
  6. Call \`revela-decks\` with action \`init\` to create ${DECKS_STATE_FILE} if needed.
48
- 7. If this conversation already contains a concrete deck task, call \`revela-decks\` with action \`upsertDeck\` and later \`upsertSlides\` only for explicit deck spec information. Do not mark readiness ready during init.
48
+ 7. If this conversation already contains a concrete deck task, call \`revela-decks\` with action \`upsertDeck\` and later \`upsertSlides\` only for explicit deck spec information. Do not pass or ask for a deck key; the tool uses the workspace folder name internally. Do not mark readiness ready during init.
49
49
  8. Report what was initialized or updated and list any open questions.
50
50
 
51
51
  Memory rules:
@@ -54,6 +54,7 @@ Memory rules:
54
54
  - Do not infer personal preferences from one-off requests.
55
55
  - Do not store secrets, credentials, API keys, tokens, account details, or sensitive personal information.
56
56
  - Do not mark writeReadiness as ready during init unless the current deck has already passed an explicit \`revela-decks\` review.
57
+ - Treat this workspace as a single deck project. If the user wants another deck, guide them to create another workspace/folder rather than adding a second deck record.
57
58
  - If new evidence conflicts with existing memory, preserve both briefly and add an Open Question instead of silently overwriting.
58
59
 
59
60
  Start now by scanning the workspace.`
@@ -1,16 +1,12 @@
1
1
  import { DECKS_STATE_FILE } from "../decks-state"
2
2
 
3
3
  export function buildReviewPrompt({
4
- slug,
5
4
  exists,
6
5
  workspaceRoot,
7
6
  }: {
8
- slug?: string
9
7
  exists: boolean
10
8
  workspaceRoot?: string
11
9
  }): string {
12
- const target = slug?.trim()
13
- const deckTarget = target ? `the deck slug or output path matching \`${target}\`` : "the current active deck"
14
10
  const state = exists
15
11
  ? `${DECKS_STATE_FILE} exists. Read it through the revela-decks tool.`
16
12
  : `${DECKS_STATE_FILE} does not exist yet. Create it through the revela-decks tool if there is enough deck context.`
@@ -18,7 +14,7 @@ export function buildReviewPrompt({
18
14
  return `Review Revela deck write readiness.
19
15
 
20
16
  Goal:
21
- - Use ${DECKS_STATE_FILE} as the source of truth for whether ${deckTarget} is ready to be written to \`decks/*.html\`.
17
+ - Use ${DECKS_STATE_FILE} as the source of truth for whether the current workspace deck is ready to be written to \`decks/*.html\`.
22
18
  - Preserve the deck spec for future sessions: every slide's content, layout, components, evidence, visuals, and production status.
23
19
  - Do not write, patch, or directly edit ${DECKS_STATE_FILE}. Use the \`revela-decks\` tool for all state changes.
24
20
  - Let \`revela-decks\` action \`review\` compute writeReadiness; do not manually set readiness to ready.
@@ -34,11 +30,11 @@ Workspace boundary rules:
34
30
  - For Glob/file searches, use the current workspace as the search root. Do not set the search root to a parent directory or home directory.
35
31
 
36
32
  Workflow:
37
- 1. Call \`revela-decks\` with action \`read\` for ${deckTarget}.
38
- 2. If no matching deck exists but the conversation contains enough deck context, call \`revela-decks\` action \`upsertDeck\` with slug, goal, outputPath, theme, requiredInputs, and researchPlan.
33
+ 1. Call \`revela-decks\` with action \`read\` for the current workspace deck.
34
+ 2. If no current deck exists but the conversation contains enough deck context, call \`revela-decks\` action \`upsertDeck\` with goal, outputPath, theme, requiredInputs, and researchPlan. Do not invent or ask for a deck key; the tool uses the workspace folder name internally.
39
35
  3. If a user-confirmed slide plan is available, call \`revela-decks\` action \`upsertSlides\` with every slide's title, purpose, layout, components, structured content, evidence, visuals, and status.
40
36
  4. Only set requiredInputs fields true when explicit conversation state, files read, research findings read, selected design, fetched layouts/components, or user confirmation supports them. Do not infer completion.
41
- 5. Call \`revela-decks\` action \`review\` for the slug. The tool computes and writes \`writeReadiness\`.
37
+ 5. Call \`revela-decks\` action \`review\`. The tool computes and writes \`writeReadiness\` for the current workspace deck.
42
38
  6. Briefly report whether the deck is ready. If blocked, list the exact blockers returned by the tool.
43
39
 
44
40
  Minimum conditions for \`ready\`:
@@ -53,6 +49,7 @@ Minimum conditions for \`ready\`:
53
49
 
54
50
  Rules:
55
51
  - Do not write or overwrite \`decks/*.html\` during review.
52
+ - Treat the workspace as one deck project. If the user wants another deck, tell them to use a separate workspace/folder.
56
53
  - Do not write, patch, or directly edit ${DECKS_STATE_FILE}; use \`revela-decks\`.
57
54
  - Do not store secrets, credentials, tokens, or sensitive personal information.
58
55
  - Do not add inferred user preferences to long-term preference state.
@@ -1,5 +1,5 @@
1
1
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs"
2
- import { basename, dirname, join } from "path"
2
+ import { basename, dirname, join, resolve } from "path"
3
3
 
4
4
  export const DECKS_STATE_FILE = "DECKS.json"
5
5
 
@@ -154,6 +154,29 @@ export function createEmptyDecksState(): DecksState {
154
154
  }
155
155
  }
156
156
 
157
+ export function workspaceDeckSlug(workspaceRoot: string): string {
158
+ return normalizeSlug(basename(resolve(workspaceRoot)) || "deck") || "deck"
159
+ }
160
+
161
+ export function normalizeWorkspaceDeckState(state: DecksState, workspaceRoot: string): DecksState {
162
+ const normalized = normalizeDecksState(state)
163
+ const keys = Object.keys(normalized.decks)
164
+ if (keys.length !== 1) return normalized
165
+
166
+ const slug = workspaceDeckSlug(workspaceRoot)
167
+ const existingKey = keys[0]
168
+ if (existingKey === slug) {
169
+ normalized.activeDeck = slug
170
+ return normalized
171
+ }
172
+
173
+ const deck = createDeckSpec({ ...normalized.decks[existingKey], slug })
174
+ delete normalized.decks[existingKey]
175
+ normalized.decks[slug] = deck
176
+ normalized.activeDeck = slug
177
+ return normalized
178
+ }
179
+
157
180
  export function defaultRequiredInputs(overrides?: Partial<RequiredInputs>): RequiredInputs {
158
181
  return {
159
182
  topicClarified: false,
@@ -210,6 +233,10 @@ export function readOrCreateDecksState(workspaceRoot: string): DecksState {
210
233
  export function upsertDeck(state: DecksState, input: Partial<DeckSpec> & { slug: string }): DecksState {
211
234
  const normalized = normalizeDecksState(state)
212
235
  const slug = normalizeSlug(input.slug)
236
+ const existingKey = currentDeckKey(normalized)
237
+ if (existingKey && slug !== existingKey && !normalized.decks[slug]) {
238
+ throw new Error(`${DECKS_STATE_FILE} already has a current deck (${existingKey}). Use a separate workspace for another deck.`)
239
+ }
213
240
  const existing = normalized.decks[slug]
214
241
  const next = createDeckSpec({ ...existing, ...input, slug })
215
242
  normalized.decks[slug] = next
@@ -220,6 +247,10 @@ export function upsertDeck(state: DecksState, input: Partial<DeckSpec> & { slug:
220
247
  export function upsertSlides(state: DecksState, slug: string, slides: SlideSpec[]): DecksState {
221
248
  const normalized = normalizeDecksState(state)
222
249
  const key = normalizeSlug(slug)
250
+ const existingKey = currentDeckKey(normalized)
251
+ if (existingKey && key !== existingKey && !normalized.decks[key]) {
252
+ throw new Error(`${DECKS_STATE_FILE} already has a current deck (${existingKey}). Use a separate workspace for another deck.`)
253
+ }
223
254
  const deck = normalized.decks[key] ?? createDeckSpec({ slug: key })
224
255
  const byIndex = new Map(deck.slides.map((slide) => [slide.index, slide]))
225
256
  for (const slide of normalizeSlides(slides)) byIndex.set(slide.index, slide)
@@ -231,7 +262,7 @@ export function upsertSlides(state: DecksState, slug: string, slides: SlideSpec[
231
262
 
232
263
  export function reviewDeckState(state: DecksState, slug?: string): { state: DecksState; result: DeckStateReadinessResult } {
233
264
  const normalized = normalizeDecksState(state)
234
- const key = normalizeSlug(slug || normalized.activeDeck || "")
265
+ const key = normalizeSlug(slug || currentDeckKey(normalized) || "")
235
266
  const deck = key ? normalized.decks[key] : undefined
236
267
  if (!deck) {
237
268
  const missing = key || "active deck"
@@ -276,13 +307,14 @@ export function evaluateDeckStateWriteReadiness(state: DecksState, filePath: str
276
307
  const targetPath = normalizeDeckPath(filePath)
277
308
  const targetSlug = deckSlugFromPath(targetPath)
278
309
  const normalized = normalizeDecksState(state)
279
- const deck = findDeckForTarget(normalized, targetPath, targetSlug)
310
+ const key = currentDeckKey(normalized)
311
+ const deck = key ? normalized.decks[key] : undefined
280
312
  if (!deck) {
281
313
  return {
282
314
  ready: false,
283
315
  slug: targetSlug,
284
- blocker: `No deck in ${DECKS_STATE_FILE} matches ${targetPath}. Use revela-decks upsertDeck/upsertSlides, then review before writing.`,
285
- blockers: [`No deck in ${DECKS_STATE_FILE} matches ${targetPath}.`],
316
+ blocker: currentDeckBlocker(normalized),
317
+ blockers: [currentDeckBlocker(normalized)],
286
318
  }
287
319
  }
288
320
 
@@ -324,16 +356,17 @@ export function extractDecksStateTargetsFromPatch(patchText: string): string[] {
324
356
  export function buildDecksStatePromptLayer(workspaceRoot: string, maxChars = 14000): string {
325
357
  if (!hasDecksState(workspaceRoot)) return ""
326
358
  const state = readDecksState(workspaceRoot)
327
- const active = state.activeDeck ? state.decks[state.activeDeck] : undefined
359
+ const activeKey = currentDeckKey(state)
360
+ const active = activeKey ? state.decks[activeKey] : undefined
328
361
  const compact = {
329
362
  sourceOfTruth: DECKS_STATE_FILE,
330
- activeDeck: state.activeDeck,
363
+ activeDeck: activeKey,
331
364
  workspace: state.workspace,
332
365
  deck: active,
333
366
  }
334
367
  let text = JSON.stringify(compact, null, 2)
335
368
  if (text.length > maxChars) text = text.slice(0, maxChars).trimEnd() + "\n[DECKS.json state truncated for prompt size.]"
336
- return `---\n\n# Revela Workspace State From ${DECKS_STATE_FILE}\n\n\`\`\`json\n${text}\n\`\`\`\n\nRules for this state layer:\n- Treat ${DECKS_STATE_FILE} as the source of truth for deck specs, slide plans, and write readiness.\n- Do not edit ${DECKS_STATE_FILE} directly; use the revela-decks tool.\n- Before writing decks/*.html, the matching deck must have writeReadiness.status=ready and a complete slide spec.`
369
+ return `---\n\n# Revela Workspace State From ${DECKS_STATE_FILE}\n\n\`\`\`json\n${text}\n\`\`\`\n\nRules for this state layer:\n- Treat ${DECKS_STATE_FILE} as the source of truth for the single current deck's specs, slide plan, and write readiness.\n- The decks map is compatibility storage; operate only on the current workspace deck.\n- Do not edit ${DECKS_STATE_FILE} directly; use the revela-decks tool.\n- Before writing decks/*.html, the current deck must have writeReadiness.status=ready and a complete slide spec, and its outputPath must match the target file.`
337
370
  }
338
371
 
339
372
  function normalizeDecksState(input: DecksState): DecksState {
@@ -357,9 +390,26 @@ function normalizeDecksState(input: DecksState): DecksState {
357
390
  state.decks[normalizedSlug] = createDeckSpec({ ...deck, slug: normalizedSlug })
358
391
  }
359
392
  if (state.activeDeck && !state.decks[state.activeDeck]) state.activeDeck = undefined
393
+ if (!state.activeDeck) {
394
+ const keys = Object.keys(state.decks)
395
+ if (keys.length === 1) state.activeDeck = keys[0]
396
+ }
360
397
  return state
361
398
  }
362
399
 
400
+ function currentDeckKey(state: DecksState): string | undefined {
401
+ if (state.activeDeck && state.decks[state.activeDeck]) return state.activeDeck
402
+ const keys = Object.keys(state.decks)
403
+ if (keys.length === 1) return keys[0]
404
+ return undefined
405
+ }
406
+
407
+ function currentDeckBlocker(state: DecksState): string {
408
+ const count = Object.keys(state.decks).length
409
+ if (count === 0) return `No current deck exists in ${DECKS_STATE_FILE}. Use revela-decks upsertDeck/upsertSlides/review before writing deck HTML.`
410
+ return `${DECKS_STATE_FILE} contains multiple deck records and no activeDeck. Select one current deck explicitly or move extra decks to separate workspaces.`
411
+ }
412
+
363
413
  function computeDeckBlockers(deck: DeckSpec): string[] {
364
414
  const blockers: string[] = []
365
415
  if (!deck.goal.trim()) blockers.push("Deck goal is missing")
@@ -402,10 +452,6 @@ function normalizeSlides(slides: SlideSpec[]): SlideSpec[] {
402
452
  .sort((a, b) => a.index - b.index)
403
453
  }
404
454
 
405
- function findDeckForTarget(state: DecksState, targetPath: string, targetSlug: string): DeckSpec | undefined {
406
- return Object.values(state.decks).find((deck) => normalizeDeckPath(deck.outputPath) === targetPath) ?? state.decks[normalizeSlug(targetSlug)]
407
- }
408
-
409
455
  function hasSlideContent(slide: SlideSpec): boolean {
410
456
  const content = slide.content ?? {}
411
457
  return Boolean(
@@ -3,25 +3,27 @@ import { activeDesign } from "../design/designs"
3
3
  import { activeDomain } from "../domain/domains"
4
4
  import {
5
5
  defaultRequiredInputs,
6
+ DECKS_STATE_FILE,
7
+ normalizeWorkspaceDeckState,
6
8
  readOrCreateDecksState,
7
- reviewDeckState,
8
9
  upsertDeck,
9
10
  upsertSlides,
10
11
  writeDecksState,
11
- type DeckStateReadinessResult,
12
12
  type SlideSpec,
13
13
  } from "../decks-state"
14
14
  import type { EditableDeck } from "./resolve-deck"
15
15
 
16
16
  export interface EditDeckStatePreflightResult {
17
17
  changed: boolean
18
- readiness: DeckStateReadinessResult
19
18
  }
20
19
 
21
20
  export function ensureEditableDeckState(workspaceRoot: string, deck: EditableDeck): EditDeckStatePreflightResult {
22
- let state = readOrCreateDecksState(workspaceRoot)
21
+ let state = normalizeWorkspaceDeckState(readOrCreateDecksState(workspaceRoot), workspaceRoot)
22
+ const active = state.activeDeck ? state.decks[state.activeDeck] : undefined
23
+ if (active && active.slug !== deck.slug && active.outputPath !== deck.file) {
24
+ throw new Error(`${DECKS_STATE_FILE} already points to ${active.outputPath}. Revela 0.8 expects one deck per workspace; move extra decks to a separate workspace.`)
25
+ }
23
26
  const existing = state.decks[deck.slug]
24
- const existingReady = existing?.writeReadiness?.status === "ready" && existing.writeReadiness.blockers.length === 0
25
27
  let changed = !existing || existing.outputPath !== deck.file
26
28
 
27
29
  state = upsertDeck(state, {
@@ -58,12 +60,10 @@ export function ensureEditableDeckState(workspaceRoot: string, deck: EditableDec
58
60
  changed = true
59
61
  }
60
62
 
61
- const reviewed = reviewDeckState(state, deck.slug)
62
- writeDecksState(workspaceRoot, reviewed.state)
63
+ writeDecksState(workspaceRoot, state)
63
64
 
64
65
  return {
65
- changed: changed || !existingReady,
66
- readiness: reviewed.result,
66
+ changed,
67
67
  }
68
68
  }
69
69
 
@@ -112,7 +112,7 @@ function safeActiveDesign(): string {
112
112
  try {
113
113
  return activeDesign()
114
114
  } catch {
115
- return "summit"
115
+ return "aurora"
116
116
  }
117
117
  }
118
118
 
package/lib/edit/open.ts CHANGED
@@ -14,6 +14,9 @@ export interface OpenEditableDeckResult {
14
14
  source: string
15
15
  stateNote: string
16
16
  preflightChanged: boolean
17
+ reusedSession: boolean
18
+ liveSession: boolean
19
+ openedBrowser: boolean
17
20
  }
18
21
 
19
22
  export interface OpenEditableDeckOptions {
@@ -21,6 +24,11 @@ export interface OpenEditableDeckOptions {
21
24
  sessionID: string
22
25
  workspaceRoot: string
23
26
  openBrowser?: boolean
27
+ openUrl?: (url: string) => void
28
+ }
29
+
30
+ export interface EnsureEditableDeckOpenResult extends OpenEditableDeckResult {
31
+ skippedReason?: "live-session"
24
32
  }
25
33
 
26
34
  export function openUrl(url: string): void {
@@ -41,11 +49,23 @@ export function openUrl(url: string): void {
41
49
  }
42
50
 
43
51
  export function openEditableDeck(target: string, options: OpenEditableDeckOptions): OpenEditableDeckResult {
52
+ return openEditableDeckInternal(target, options, { skipLiveSession: false })
53
+ }
54
+
55
+ export function ensureEditableDeckOpenForChange(
56
+ target: string,
57
+ options: OpenEditableDeckOptions,
58
+ ): EnsureEditableDeckOpenResult {
59
+ return openEditableDeckInternal(target, options, { skipLiveSession: true })
60
+ }
61
+
62
+ function openEditableDeckInternal(
63
+ target: string,
64
+ options: OpenEditableDeckOptions,
65
+ behavior: { skipLiveSession: boolean },
66
+ ): EnsureEditableDeckOpenResult {
44
67
  const deck = resolveEditableDeck(options.workspaceRoot, target)
45
68
  const preflight = ensureEditableDeckState(options.workspaceRoot, deck)
46
- if (!preflight.readiness.ready) {
47
- throw new Error(preflight.readiness.blocker || "Deck is not ready for HTML edits.")
48
- }
49
69
 
50
70
  ctx.enabled = true
51
71
  if (!existsSync(ACTIVE_PROMPT_FILE)) {
@@ -55,16 +75,17 @@ export function openEditableDeck(target: string, options: OpenEditableDeckOption
55
75
  }
56
76
 
57
77
  const editServer = startEditServer()
58
- const token = editServer.createSession({
78
+ const session = editServer.getOrCreateSession({
59
79
  client: options.client,
60
80
  sessionID: options.sessionID,
61
81
  deck,
62
82
  })
63
- const url = `${editServer.baseUrl}/edit?token=${encodeURIComponent(token)}`
64
- if (options.openBrowser !== false) openUrl(url)
83
+ const url = `${editServer.baseUrl}/edit?token=${encodeURIComponent(session.token)}`
84
+ const shouldOpen = options.openBrowser !== false && !(behavior.skipLiveSession && session.live)
85
+ if (shouldOpen) (options.openUrl ?? openUrl)(url)
65
86
 
66
87
  const source = deck.source === "decks-state" ? "DECKS.json" : deck.source === "file-path" ? "file path" : "fallback path"
67
- const stateNote = preflight.changed ? "Deck state was prepared in DECKS.json before opening the editor." : "Deck state is ready in DECKS.json."
88
+ const stateNote = preflight.changed ? "Deck state was prepared in DECKS.json for visual editing." : "Deck state already points to this visual edit target."
68
89
 
69
90
  return {
70
91
  deck,
@@ -72,5 +93,9 @@ export function openEditableDeck(target: string, options: OpenEditableDeckOption
72
93
  source,
73
94
  stateNote,
74
95
  preflightChanged: preflight.changed,
96
+ reusedSession: session.reused,
97
+ liveSession: session.live,
98
+ openedBrowser: shouldOpen,
99
+ skippedReason: behavior.skipLiveSession && session.live ? "live-session" : undefined,
75
100
  }
76
101
  }