pneuma-skills 0.3.0 → 1.0.0

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
@@ -1,18 +1,18 @@
1
1
  # Pneuma Skills
2
2
 
3
- **Let Code Agents do WYSIWYG editing on HTML-based content.**
3
+ **An extensible delivery platform for filesystem-based Agent capabilities.**
4
4
 
5
5
  > **"pneuma"** — Greek *pneuma*, meaning soul, breath, life force.
6
6
 
7
- Pneuma Skills is a framework that connects a Code Agent (like Claude Code) to a browser-based editor, giving users a real-time WYSIWYG experience for AI-assisted content editing. The agent edits files on disk; Pneuma watches for changes and streams a live preview to the browser alongside a chat interface.
7
+ Pneuma fills the last mile between Code Agents and users: agents edit files on disk, Pneuma watches for changes and streams a live WYSIWYG preview alongside a full chat interface. Everything is driven by three pluggable contracts bring your own Mode, Viewer, or Agent backend.
8
8
 
9
9
  ```
10
- Pneuma Skills = Content Mode x Code Agent Backend x Editor Shell
10
+ ModeManifest(skill + viewer + agent_config) × AgentBackend × RuntimeShell
11
11
  ```
12
12
 
13
13
  ## Demo
14
14
 
15
- Currently ships with **Doc Mode** — a markdown editing environment where Claude Code edits `.md` files and you see the rendered result in real-time.
15
+ Ships with **Doc Mode** — a markdown editing environment where Claude Code edits `.md` files and you see the rendered result in real-time.
16
16
 
17
17
  ```
18
18
  ┌─────────────────────────────┬──────────────────────────┐
@@ -25,6 +25,8 @@ Currently ships with **Doc Mode** — a markdown editing environment where Claud
25
25
  │ - GFM support │ ✎ Edit README.md │
26
26
  │ - Image rendering │ ✎ Write hero.png │
27
27
  │ │ │
28
+ ├─────────────────────────────┼──────────────────────────┤
29
+ │ view / edit / select │ Chat │ Context │ Term │
28
30
  ├─────────────────────────────┴──────────────────────────┤
29
31
  │ ● Connected session:abc123 $0.02 3 turns │
30
32
  └────────────────────────────────────────────────────────┘
@@ -32,7 +34,7 @@ Currently ships with **Doc Mode** — a markdown editing environment where Claud
32
34
 
33
35
  ## Prerequisites
34
36
 
35
- - [Bun](https://bun.sh) >= 1.0
37
+ - [Bun](https://bun.sh) >= 1.3.5 (required for PTY terminal support)
36
38
  - [Claude Code CLI](https://docs.anthropic.com/en/docs/claude-code) installed and authenticated (`claude` command available in PATH)
37
39
 
38
40
  ## Quick Start
@@ -56,7 +58,7 @@ bun run dev doc --workspace ~/my-notes
56
58
 
57
59
  This will:
58
60
 
59
- 1. Install a skill prompt into `<workspace>/.claude/skills/`
61
+ 1. Load the Doc Mode manifest and install its skill prompt into `<workspace>/.claude/skills/`
60
62
  2. Start the Pneuma server on `http://localhost:17996`
61
63
  3. Spawn a Claude Code CLI session connected via WebSocket
62
64
  4. Open your browser with the editor UI
@@ -77,21 +79,26 @@ Options:
77
79
 
78
80
  ## Architecture
79
81
 
82
+ Pneuma is organized in four layers, each with a clear contract boundary:
83
+
80
84
  ```
81
- Browser (React) Pneuma Server (Bun + Hono) Claude Code CLI
82
- ┌──────────────┐ JSON/WS ┌─────────────────────┐ NDJSON/WS ┌──────────────┐
83
- Chat Panel │◄──────────────►│ │◄───────────────►│
84
- Live Preview │ │ WebSocket Bridge │ claude │
85
- │ Permissions │ │ File Watcher │ │ --sdk-url │
86
- Status Bar │ │ Content Server │ │
87
- └──────────────┘ └─────────────────────┘ └──────────────┘
88
-
89
- watches disk
90
-
91
- ┌─────────────┐
92
- Workspace
93
- │ *.md files │
94
- └─────────────┘
85
+ ┌─────────────────────────────────────────────────────────┐
86
+ │ Layer 4: Mode Protocol │
87
+ ModeManifest "what capability, what config, what UI"
88
+ modes/doc/pneuma-mode.ts
89
+ ├─────────────────────────────────────────────────────────┤
90
+ Layer 3: Content Viewer
91
+ │ ViewerContract — "how to render, select, update" │
92
+ modes/doc/components/DocPreview.tsx │
93
+ ├─────────────────────────────────────────────────────────┤
94
+ Layer 2: Agent Bridge │
95
+ │ AgentBackend — "how to launch, communicate, lifecycle" │
96
+ backends/claude-code/
97
+ ├─────────────────────────────────────────────────────────┤
98
+ │ Layer 1: Runtime Shell │
99
+ │ WS Bridge, HTTP, File Watcher, Session, Frontend │
100
+ │ server/ + src/ │
101
+ └─────────────────────────────────────────────────────────┘
95
102
  ```
96
103
 
97
104
  The server maintains dual WebSocket channels:
@@ -100,83 +107,104 @@ The server maintains dual WebSocket channels:
100
107
 
101
108
  When Claude Code edits files, chokidar detects the changes and pushes updated content to the browser for live preview.
102
109
 
110
+ ## Three Core Contracts
111
+
112
+ | Contract | Responsibility | Extend to... |
113
+ |----------|---------------|-------------|
114
+ | **ModeManifest** | Declares skill, viewer config, agent preferences, init seeds | Add new modes (slide, mindmap, canvas) |
115
+ | **ViewerContract** | Preview component, context extraction, update strategy | Custom renderers (iframe, D3, Monaco) |
116
+ | **AgentBackend** | Launch, resume, kill, capability declaration | Other agents (Codex, Aider) |
117
+
118
+ Contracts are defined in `core/types/` with 42 tests in `core/__tests__/`.
119
+
103
120
  ## Project Structure
104
121
 
105
122
  ```
106
123
  pneuma-skills/
107
- ├── bin/pneuma.ts # CLI entry point
124
+ ├── bin/pneuma.ts # CLI entry — orchestrates mode + agent + server
125
+ ├── core/
126
+ │ ├── types/ # Contract definitions
127
+ │ │ ├── mode-manifest.ts # ModeManifest, SkillConfig, ViewerConfig
128
+ │ │ ├── viewer-contract.ts # ViewerContract, ViewerPreviewProps
129
+ │ │ ├── agent-backend.ts # AgentBackend, AgentCapabilities
130
+ │ │ ├── mode-definition.ts # ModeDefinition (manifest + viewer)
131
+ │ │ └── index.ts # Re-exports
132
+ │ ├── mode-loader.ts # Dynamic mode discovery and loading
133
+ │ └── __tests__/ # 42 contract tests
134
+ ├── modes/
135
+ │ └── doc/
136
+ │ ├── pneuma-mode.ts # Doc Mode definition (manifest + viewer)
137
+ │ ├── skill/SKILL.md # Skill prompt for Claude Code
138
+ │ └── components/
139
+ │ └── DocPreview.tsx # Markdown preview with select/edit modes
140
+ ├── backends/
141
+ │ └── claude-code/
142
+ │ ├── index.ts # ClaudeCodeBackend implements AgentBackend
143
+ │ └── cli-launcher.ts # Process spawner (Bun.spawn + --sdk-url)
108
144
  ├── server/
109
145
  │ ├── index.ts # Hono HTTP server + content API
110
- │ ├── ws-bridge.ts # Dual WebSocket bridge (browser <-> CLI)
111
- │ ├── cli-launcher.ts # Claude Code process spawner
112
- │ ├── file-watcher.ts # chokidar-based file watcher
113
- └── skill-installer.ts # Copies skill prompts to workspace
146
+ │ ├── ws-bridge.ts # Dual WebSocket bridge (browser CLI)
147
+ │ ├── ws-bridge-*.ts # Controls, replay, browser handlers, types
148
+ │ ├── file-watcher.ts # chokidar watcher (manifest-driven patterns)
149
+ ├── skill-installer.ts # Copies skill prompts (manifest-driven)
150
+ │ └── terminal-manager.ts # PTY terminal sessions
114
151
  ├── src/
115
- │ ├── App.tsx # Root layout (resizable panels)
116
- │ ├── main.tsx # React entry
117
- │ ├── store.ts # Zustand state management
152
+ │ ├── App.tsx # Root layout (dynamic viewer from store)
153
+ │ ├── store.ts # Zustand state (session, messages, viewer)
118
154
  │ ├── ws.ts # Browser WebSocket client
119
- │ ├── types.ts # Shared TypeScript types
120
155
  │ └── components/
121
156
  │ ├── ChatPanel.tsx # Chat message feed
122
- │ ├── ChatInput.tsx # Message input box
123
- │ ├── MarkdownPreview.tsx # Live markdown renderer
124
- │ ├── MessageBubble.tsx # Rich message rendering (text, tools, thinking)
157
+ │ ├── ChatInput.tsx # Message composer with image upload
158
+ │ ├── MessageBubble.tsx # Rich messages (markdown, tools, thinking, context card)
159
+ │ ├── ContextPanel.tsx # Session stats, tasks, MCP servers, tools
160
+ │ ├── TerminalPanel.tsx # Integrated xterm.js terminal
125
161
  │ ├── ToolBlock.tsx # Expandable tool call cards
126
- │ ├── StreamingText.tsx # Streaming response display
127
- ├── ActivityIndicator.tsx # Thinking/tool progress indicator
128
- │ ├── PermissionBanner.tsx # Tool permission approval UI
129
- │ └── StatusBar.tsx # Connection status + session info
130
- ├── skill/
131
- └── doc/SKILL.md # Doc Mode skill prompt for Claude Code
132
- ├── docs/adr/ # Architecture Decision Records
133
- └── draft.md # Full requirements document (Chinese)
162
+ │ ├── PermissionBanner.tsx # Tool permission approval UI
163
+ └── TopBar.tsx # Tabs (Chat/Context/Terminal) + status
164
+ └── docs/
165
+ ├── architecture-review-v1.md # Architecture review & v1.0 blueprint
166
+ ├── design/phase1-internal-decoupling.md # Detailed design doc
167
+ └── adr/ # Architecture Decision Records (1-11)
134
168
  ```
135
169
 
136
170
  ## Tech Stack
137
171
 
138
172
  | Layer | Technology |
139
173
  |-------|-----------|
140
- | Runtime | [Bun](https://bun.sh) |
174
+ | Runtime | [Bun](https://bun.sh) >= 1.3.5 |
141
175
  | Server | [Hono](https://hono.dev) |
142
176
  | Frontend | React 19 + [Vite](https://vite.dev) 6 |
143
- | Styling | [Tailwind CSS](https://tailwindcss.com) 4 + Typography plugin |
177
+ | Styling | [Tailwind CSS](https://tailwindcss.com) 4 |
144
178
  | State | [Zustand](https://zustand.docs.pmnd.rs) 5 |
145
179
  | Markdown | [react-markdown](https://github.com/remarkjs/react-markdown) + remark-gfm |
180
+ | Terminal | [xterm.js](https://xtermjs.org) + Bun native PTY |
146
181
  | File Watching | [chokidar](https://github.com/paulmillr/chokidar) 4 |
147
182
  | Agent | [Claude Code](https://docs.anthropic.com/en/docs/claude-code) via `--sdk-url` |
148
183
 
149
- ## How It Works
150
-
151
- 1. **Skill Installation** — Pneuma copies a mode-specific skill prompt (e.g. `skill/doc/SKILL.md`) into the workspace's `.claude/skills/` directory. Claude Code natively discovers and loads skills from this location.
152
-
153
- 2. **Agent Spawning** — The CLI launcher spawns `claude --sdk-url ws://localhost:17007/ws/cli/<sessionId>` which connects Claude Code's streaming output to the Pneuma server.
154
-
155
- 3. **Message Bridge** — The WebSocket bridge translates between the browser's JSON protocol and Claude Code's NDJSON protocol, handling message routing, permission flows, and event replay.
156
-
157
- 4. **Live Preview** — When Claude Code writes or edits files, chokidar detects changes and pushes updated content to all connected browsers via WebSocket.
158
-
159
- 5. **Rich Chat UI** — The browser renders Claude Code's full output: streaming text with markdown, expandable tool call cards, collapsible thinking blocks, and an activity indicator.
160
-
161
- ## Content Mode System
162
-
163
- Pneuma is designed to be extensible with different content modes. Each mode defines:
164
-
165
- - **Renderer** — How to render the content in the browser
166
- - **Skill** — Domain-specific prompt for the Code Agent
167
- - **File Convention** — How content is organized on disk
168
- - **Navigator** — Structural navigation (outline, page list, etc.)
184
+ ## Features
169
185
 
170
- Currently implemented: **Doc Mode** (markdown). Future modes could include slides, mindmaps, canvas, and more.
186
+ - **Live WYSIWYG preview** Agent edits files, you see rendered results instantly
187
+ - **Element selection** — Click any block to select it, then instruct changes on that specific element
188
+ - **Inline editing** — Edit content directly in the preview (edit mode)
189
+ - **Rich chat UI** — Streaming text, expandable tool calls, collapsible thinking, context visualization
190
+ - **Integrated terminal** — Full PTY terminal with xterm.js
191
+ - **Session management** — Persist and resume sessions across restarts
192
+ - **Permission control** — Review and approve/deny tool use requests
193
+ - **Task tracking** — Visualize Claude's TodoWrite/TaskCreate progress
194
+ - **Background processes** — Monitor long-running background commands
195
+ - **Context visualization** — Rich `/context` card with category breakdown and stacked bar
196
+ - **Image upload** — Drag & drop or paste images into chat
171
197
 
172
198
  ## Roadmap
173
199
 
174
200
  - [x] Doc Mode MVP — Markdown WYSIWYG editing
175
- - [x] Element selection Click to select and instruct edits on specific elements
176
- - [ ] Slide Mode Presentation editing with page navigation
177
- - [ ] Session persistence Resume previous editing sessions
178
- - [ ] Multiple agent backends Codex CLI, custom agents
179
- - [x] Production build`bunx pneuma-skills` distribution
201
+ - [x] Element selection & inline editing
202
+ - [x] Session persistence & resume
203
+ - [x] Terminal, tasks, context panel
204
+ - [x] v1.0 contract architecture (ModeManifest, ViewerContract, AgentBackend)
205
+ - [ ] Slide ModePresentation editing with iframe preview (v1.1)
206
+ - [ ] Remote mode loading — `pneuma --mode github:user/repo` (v1.x)
207
+ - [ ] Additional agent backends — Codex CLI, custom agents (v1.x)
180
208
 
181
209
  ## Acknowledgements
182
210
 
package/bin/pneuma.ts CHANGED
@@ -3,16 +3,20 @@
3
3
  * Pneuma Skills CLI entry point.
4
4
  *
5
5
  * Usage:
6
- * pneuma doc --workspace /path/to/project [--port 17996] [--no-open]
6
+ * pneuma <mode> --workspace /path/to/project [--port 17996] [--no-open]
7
+ *
8
+ * Driven by ModeManifest + AgentBackend — no hardcoded mode knowledge.
7
9
  */
8
10
 
9
11
  import { resolve, dirname, join } from "node:path";
10
12
  import { existsSync, copyFileSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
11
13
  import * as readline from "node:readline";
12
14
  import { startServer } from "../server/index.js";
13
- import { CliLauncher } from "../server/cli-launcher.js";
15
+ import { ClaudeCodeBackend } from "../backends/claude-code/index.js";
14
16
  import { installSkill } from "../server/skill-installer.js";
15
17
  import { startFileWatcher } from "../server/file-watcher.js";
18
+ import { loadModeManifest, listModes } from "../core/mode-loader.js";
19
+ import type { ModeManifest } from "../core/types/mode-manifest.js";
16
20
 
17
21
  const PROJECT_ROOT = resolve(dirname(import.meta.path), "..");
18
22
 
@@ -20,7 +24,8 @@ const PROJECT_ROOT = resolve(dirname(import.meta.path), "..");
20
24
 
21
25
  interface PersistedSession {
22
26
  sessionId: string;
23
- cliSessionId?: string;
27
+ /** Agent's internal session ID (e.g. Claude Code's --resume ID) */
28
+ agentSessionId?: string;
24
29
  mode: string;
25
30
  createdAt: number;
26
31
  }
@@ -29,7 +34,13 @@ function loadSession(workspace: string): PersistedSession | null {
29
34
  const filePath = join(workspace, ".pneuma", "session.json");
30
35
  try {
31
36
  const content = readFileSync(filePath, "utf-8");
32
- return JSON.parse(content);
37
+ const data = JSON.parse(content);
38
+ // Backward compat: rename cliSessionId → agentSessionId
39
+ if (data.cliSessionId && !data.agentSessionId) {
40
+ data.agentSessionId = data.cliSessionId;
41
+ delete data.cliSessionId;
42
+ }
43
+ return data;
33
44
  } catch {
34
45
  return null;
35
46
  }
@@ -41,6 +52,22 @@ function saveSession(workspace: string, session: PersistedSession): void {
41
52
  writeFileSync(join(dir, "session.json"), JSON.stringify(session, null, 2));
42
53
  }
43
54
 
55
+ function loadHistory(workspace: string): unknown[] {
56
+ try {
57
+ const content = readFileSync(join(workspace, ".pneuma", "history.json"), "utf-8");
58
+ const data = JSON.parse(content);
59
+ return Array.isArray(data) ? data : [];
60
+ } catch {
61
+ return [];
62
+ }
63
+ }
64
+
65
+ function saveHistory(workspace: string, history: unknown[]): void {
66
+ const dir = join(workspace, ".pneuma");
67
+ mkdirSync(dir, { recursive: true });
68
+ writeFileSync(join(dir, "history.json"), JSON.stringify(history));
69
+ }
70
+
44
71
  // ── CLI arg parsing ──────────────────────────────────────────────────────────
45
72
 
46
73
  function parseArgs(argv: string[]) {
@@ -78,11 +105,45 @@ function ask(question: string): Promise<string> {
78
105
 
79
106
  // ── Main ─────────────────────────────────────────────────────────────────────
80
107
 
108
+ function checkBunVersion() {
109
+ const MIN_BUN = "1.3.5"; // Required for Bun.spawn terminal (PTY) support
110
+ const current = typeof Bun !== "undefined" ? Bun.version : null;
111
+ if (!current) {
112
+ console.warn("[pneuma] Warning: Not running under Bun. Pneuma requires Bun >= " + MIN_BUN);
113
+ return;
114
+ }
115
+ const [curMajor, curMinor, curPatch] = current.split(".").map(Number);
116
+ const [minMajor, minMinor, minPatch] = MIN_BUN.split(".").map(Number);
117
+ const ok =
118
+ curMajor > minMajor ||
119
+ (curMajor === minMajor && curMinor > minMinor) ||
120
+ (curMajor === minMajor && curMinor === minMinor && curPatch >= minPatch);
121
+ if (!ok) {
122
+ console.warn(
123
+ `[pneuma] Warning: Bun ${current} detected, but >= ${MIN_BUN} is required.` +
124
+ ` Terminal features may not work. Run \`bun upgrade\` to update.`
125
+ );
126
+ }
127
+ }
128
+
81
129
  async function main() {
130
+ checkBunVersion();
82
131
  const { mode, workspace, port, noOpen } = parseArgs(process.argv);
83
132
 
84
- if (!mode || mode !== "doc") {
85
- console.log("Usage: pneuma doc --workspace /path/to/project [--port 17996] [--no-open]");
133
+ // Validate mode
134
+ const availableModes = listModes();
135
+ if (!mode || !availableModes.includes(mode)) {
136
+ const modeList = availableModes.join(" | ");
137
+ console.log(`Usage: pneuma <${modeList}> --workspace /path/to/project [--port 17996] [--no-open]`);
138
+ process.exit(1);
139
+ }
140
+
141
+ // Load mode manifest (no React deps — backend safe)
142
+ let manifest: ModeManifest;
143
+ try {
144
+ manifest = await loadModeManifest(mode);
145
+ } catch (err) {
146
+ console.error(`[pneuma] Failed to load mode "${mode}":`, err);
86
147
  process.exit(1);
87
148
  }
88
149
 
@@ -96,29 +157,35 @@ async function main() {
96
157
  console.log(`[pneuma] Created workspace: ${workspace}`);
97
158
  }
98
159
 
99
- console.log(`[pneuma] Mode: ${mode}`);
160
+ console.log(`[pneuma] Mode: ${manifest.displayName} (${mode})`);
100
161
  console.log(`[pneuma] Workspace: ${workspace}`);
101
162
 
102
- // 1. Install skill + inject CLAUDE.md
163
+ // 1. Install skill + inject CLAUDE.md (driven by manifest)
103
164
  console.log("[pneuma] Installing skill and preparing environment...");
104
- installSkill(workspace);
105
-
106
- // 1.5 Seed default content if workspace has no meaningful markdown files
107
- const contentFiles = Array.from(
108
- new Bun.Glob("**/*.md").scanSync({ cwd: workspace, absolute: false })
109
- ).filter((f) => f !== "CLAUDE.md" && !f.startsWith(".claude/"));
110
-
111
- const hasContent = contentFiles.some((f) => {
112
- try {
113
- return readFileSync(join(workspace, f), "utf-8").trim().length > 0;
114
- } catch { return false; }
115
- });
165
+ const modeSourceDir = resolve(PROJECT_ROOT, "modes", mode);
166
+ installSkill(workspace, manifest.skill, modeSourceDir);
167
+
168
+ // 1.5 Seed default content if workspace has no meaningful files
169
+ if (manifest.init && manifest.init.contentCheckPattern) {
170
+ const checkPattern = manifest.init.contentCheckPattern;
171
+ const contentFiles = Array.from(
172
+ new Bun.Glob(checkPattern).scanSync({ cwd: workspace, absolute: false })
173
+ ).filter((f) => f !== "CLAUDE.md" && !f.startsWith(".claude/"));
174
+
175
+ const hasContent = contentFiles.some((f) => {
176
+ try {
177
+ return readFileSync(join(workspace, f), "utf-8").trim().length > 0;
178
+ } catch { return false; }
179
+ });
116
180
 
117
- if (!hasContent) {
118
- const readmeSrc = join(PROJECT_ROOT, "README.md");
119
- if (existsSync(readmeSrc)) {
120
- copyFileSync(readmeSrc, join(workspace, "README.md"));
121
- console.log("[pneuma] Seeded workspace with default README.md");
181
+ if (!hasContent && manifest.init.seedFiles) {
182
+ for (const [src, dst] of Object.entries(manifest.init.seedFiles)) {
183
+ const srcPath = join(PROJECT_ROOT, src);
184
+ if (existsSync(srcPath)) {
185
+ copyFileSync(srcPath, join(workspace, dst));
186
+ console.log(`[pneuma] Seeded workspace with ${dst}`);
187
+ }
188
+ }
122
189
  }
123
190
  }
124
191
 
@@ -142,18 +209,18 @@ async function main() {
142
209
  ...(isDev ? {} : { distDir }),
143
210
  });
144
211
 
145
- // 4. Launch CLI (with session resume if available)
146
- const launcher = new CliLauncher(actualPort);
212
+ // 4. Launch Agent backend (driven by manifest)
213
+ const backend = new ClaudeCodeBackend(actualPort);
147
214
 
148
215
  // When the CLI reports its internal session_id, persist it
149
- wsBridge.onCLISessionIdReceived((sessionId, cliSessionId) => {
150
- launcher.setCLISessionId(sessionId, cliSessionId);
216
+ wsBridge.onCLISessionIdReceived((sessionId, agentSessionId) => {
217
+ backend.setAgentSessionId(sessionId, agentSessionId);
151
218
  // Persist to .pneuma/session.json
152
219
  const persisted = loadSession(workspace);
153
220
  if (persisted && persisted.sessionId === sessionId) {
154
- persisted.cliSessionId = cliSessionId;
221
+ persisted.agentSessionId = agentSessionId;
155
222
  saveSession(workspace, persisted);
156
- console.log(`[pneuma] Saved cliSessionId for resume: ${cliSessionId}`);
223
+ console.log(`[pneuma] Saved agentSessionId for resume: ${agentSessionId}`);
157
224
  }
158
225
  });
159
226
 
@@ -161,49 +228,82 @@ async function main() {
161
228
  const existing = loadSession(workspace);
162
229
  let resuming = false;
163
230
 
164
- const session = launcher.launch({
231
+ const permissionMode = manifest.agent?.permissionMode;
232
+ const session = backend.launch({
165
233
  cwd: workspace,
166
- permissionMode: "bypassPermissions",
234
+ permissionMode,
167
235
  // Reuse sessionId for stable WS routing
168
- ...(existing?.cliSessionId ? {
236
+ ...(existing?.agentSessionId ? {
169
237
  sessionId: existing.sessionId,
170
- resumeSessionId: existing.cliSessionId,
238
+ resumeSessionId: existing.agentSessionId,
171
239
  } : {}),
172
240
  });
173
241
 
174
- if (existing?.cliSessionId) {
242
+ if (existing?.agentSessionId) {
175
243
  resuming = true;
176
- console.log(`[pneuma] Resuming session: ${existing.cliSessionId}`);
244
+ console.log(`[pneuma] Resuming session: ${existing.agentSessionId}`);
177
245
  }
178
246
 
179
247
  // Persist session info
180
248
  saveSession(workspace, {
181
249
  sessionId: session.sessionId,
182
- cliSessionId: existing?.cliSessionId,
250
+ agentSessionId: existing?.agentSessionId,
183
251
  mode,
184
252
  createdAt: existing?.createdAt || Date.now(),
185
253
  });
186
254
 
187
- console.log(`[pneuma] CLI session started: ${session.sessionId}`);
255
+ console.log(`[pneuma] Agent session started: ${session.sessionId}`);
188
256
 
189
- // If resume fails (CLI exits quickly), clear cliSessionId from persistence
190
- launcher.onSessionExited((exitedId, exitCode) => {
257
+ // Auto-greeting for fresh sessions (driven by manifest)
258
+ if (!resuming && manifest.agent?.greeting) {
259
+ wsBridge.injectGreeting(session.sessionId, manifest.agent.greeting);
260
+ console.log("[pneuma] Sent auto-greeting for fresh session");
261
+ }
262
+
263
+ // Load persisted message history into WsBridge
264
+ const savedHistory = loadHistory(workspace);
265
+ if (savedHistory.length > 0) {
266
+ wsBridge.loadMessageHistory(session.sessionId, savedHistory as any);
267
+ console.log(`[pneuma] Restored ${savedHistory.length} messages from history`);
268
+ }
269
+
270
+ // Periodically persist message history (debounced — every 5s)
271
+ const historyInterval = setInterval(() => {
272
+ const history = wsBridge.getMessageHistory(session.sessionId);
273
+ if (history.length > 0) {
274
+ saveHistory(workspace, history);
275
+ }
276
+ }, 5_000);
277
+
278
+ // Handle Agent exit: surface errors + clear stale resume state
279
+ backend.onSessionExited((exitedId, exitCode) => {
280
+ // Broadcast Agent errors to browser
281
+ if (exitCode !== 0 && exitCode !== 143 /* SIGTERM = normal shutdown */) {
282
+ let errorMsg: string;
283
+ if (exitCode === 127) {
284
+ errorMsg = "Claude Code CLI not found. Please install it: https://docs.anthropic.com/claude-code";
285
+ } else {
286
+ errorMsg = `Claude Code exited unexpectedly (code ${exitCode}). Check CLI installation and subscription status.`;
287
+ }
288
+ wsBridge.broadcastToSession(exitedId, { type: "error", message: errorMsg });
289
+ }
290
+
291
+ // If resume fails (Agent exits quickly), clear agentSessionId from persistence
191
292
  if (exitedId === session.sessionId && resuming) {
192
- const info = launcher.getSession(exitedId);
193
- if (info && !info.cliSessionId) {
194
- // Resume failed, cliSessionId was cleared by launcher
293
+ const info = backend.getSession(exitedId);
294
+ if (info && !info.agentSessionId) {
195
295
  const persisted = loadSession(workspace);
196
296
  if (persisted) {
197
- persisted.cliSessionId = undefined;
297
+ persisted.agentSessionId = undefined;
198
298
  saveSession(workspace, persisted);
199
- console.log("[pneuma] Resume failed, cleared cliSessionId. Restart for fresh session.");
299
+ console.log("[pneuma] Resume failed, cleared agentSessionId. Restart for fresh session.");
200
300
  }
201
301
  }
202
302
  }
203
303
  });
204
304
 
205
- // 5. Start file watcher
206
- startFileWatcher(workspace, (files) => {
305
+ // 5. Start file watcher (driven by manifest)
306
+ startFileWatcher(workspace, manifest.viewer, (files) => {
207
307
  wsBridge.broadcastToSession(session.sessionId, {
208
308
  type: "content_update",
209
309
  files,
@@ -246,9 +346,9 @@ async function main() {
246
346
  await new Promise((r) => setTimeout(r, 2000));
247
347
  }
248
348
 
249
- // 7. Open browser
349
+ // 7. Open browser (include mode in URL for frontend)
250
350
  if (!noOpen) {
251
- const url = `http://localhost:${browserPort}?session=${session.sessionId}`;
351
+ const url = `http://localhost:${browserPort}?session=${session.sessionId}&mode=${mode}`;
252
352
  console.log(`[pneuma] Opening browser: ${url}`);
253
353
  try {
254
354
  const opener = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
@@ -261,8 +361,15 @@ async function main() {
261
361
  // Graceful shutdown
262
362
  const shutdown = async () => {
263
363
  console.log("\n[pneuma] Shutting down...");
364
+ clearInterval(historyInterval);
365
+ // Final history save
366
+ const history = wsBridge.getMessageHistory(session.sessionId);
367
+ if (history.length > 0) {
368
+ saveHistory(workspace, history);
369
+ console.log(`[pneuma] Saved ${history.length} messages to history`);
370
+ }
264
371
  viteProc?.kill();
265
- await launcher.killAll();
372
+ await backend.killAll();
266
373
  server.stop(true);
267
374
  process.exit(0);
268
375
  };