pneuma-skills 2.4.1 → 2.4.3

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
@@ -54,6 +54,7 @@ Download the latest release for your platform:
54
54
  |----------|----------|
55
55
  | macOS (Apple Silicon + Intel) | [`.dmg`](https://github.com/pandazki/pneuma-skills/releases/latest) |
56
56
  | Windows x64 | [`.exe` installer](https://github.com/pandazki/pneuma-skills/releases/latest) |
57
+ | Windows ARM64 | [`.exe` installer](https://github.com/pandazki/pneuma-skills/releases/latest) |
57
58
  | Linux x64 | [`.AppImage`](https://github.com/pandazki/pneuma-skills/releases/latest) / [`.deb`](https://github.com/pandazki/pneuma-skills/releases/latest) |
58
59
 
59
60
  The desktop app bundles Bun — no runtime install needed. Just install [Claude Code CLI](https://docs.anthropic.com/en/docs/claude-code) and you're ready to go.
@@ -2,7 +2,15 @@
2
2
  "name": "{{modeName}}",
3
3
  "private": true,
4
4
  "dependencies": {
5
- "react-markdown": "^9.0.0",
6
- "remark-gfm": "^4.0.0"
5
+ "@tailwindcss/vite": "^4.0.0",
6
+ "@vitejs/plugin-react": "^4.4.0",
7
+ "react": "^19.0.0",
8
+ "react-dom": "^19.0.0",
9
+ "react-markdown": "^10.1.0",
10
+ "remark-gfm": "^4.0.1",
11
+ "rehype-raw": "^7.0.0",
12
+ "tailwindcss": "^4.0.0",
13
+ "vite": "^6.3.0",
14
+ "zustand": "^5.0.0"
7
15
  }
8
16
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pneuma-skills",
3
- "version": "2.4.1",
3
+ "version": "2.4.3",
4
4
  "type": "module",
5
5
  "description": "Co-creation infrastructure for humans and code agents — visual environment, skills, continuous learning, and distribution.",
6
6
  "license": "MIT",
@@ -23,6 +23,8 @@
23
23
  "modes/",
24
24
  "snapshot/",
25
25
  "dist/",
26
+ "src/",
27
+ "vite.config.ts",
26
28
  "index.html",
27
29
  "README.md"
28
30
  ],
@@ -37,33 +39,20 @@
37
39
  },
38
40
  "dependencies": {
39
41
  "@clack/prompts": "^1.0.1",
40
- "@xyflow/react": "^12.10.1",
41
- "@zumer/snapdom": "^2.0.2",
42
- "chokidar": "^4.0.0",
43
- "diff": "^8.0.3",
44
- "hono": "^4.7.0"
45
- },
46
- "devDependencies": {
47
42
  "@codemirror/lang-css": "^6.3.1",
48
43
  "@codemirror/lang-html": "^6.4.11",
49
44
  "@codemirror/lang-javascript": "^6.2.4",
50
45
  "@codemirror/lang-json": "^6.0.2",
51
46
  "@codemirror/lang-markdown": "^6.5.0",
52
47
  "@codemirror/lang-python": "^6.2.1",
53
- "@dnd-kit/core": "^6.3.1",
54
- "@dnd-kit/sortable": "^10.0.0",
55
- "@dnd-kit/utilities": "^3.2.2",
56
- "@excalidraw/excalidraw": "^0.18.0",
57
- "@tailwindcss/typography": "^0.5.19",
58
48
  "@tailwindcss/vite": "^4.0.0",
59
- "@types/bun": "^1.2.5",
60
- "@types/diff": "^8.0.0",
61
- "@types/react": "^19.0.0",
62
- "@types/react-dom": "^19.0.0",
63
49
  "@uiw/react-codemirror": "^4.25.5",
64
50
  "@vitejs/plugin-react": "^4.4.0",
65
51
  "@xterm/addon-fit": "^0.11.0",
66
52
  "@xterm/xterm": "^6.0.0",
53
+ "chokidar": "^4.0.0",
54
+ "diff": "^8.0.3",
55
+ "hono": "^4.7.0",
67
56
  "react": "^19.0.0",
68
57
  "react-dom": "^19.0.0",
69
58
  "react-markdown": "^10.1.0",
@@ -71,10 +60,23 @@
71
60
  "rehype-raw": "^7.0.0",
72
61
  "remark-gfm": "^4.0.1",
73
62
  "tailwindcss": "^4.0.0",
74
- "typescript": "^5.9.3",
75
63
  "vite": "^6.3.0",
76
64
  "zustand": "^5.0.0"
77
65
  },
66
+ "devDependencies": {
67
+ "@dnd-kit/core": "^6.3.1",
68
+ "@dnd-kit/sortable": "^10.0.0",
69
+ "@dnd-kit/utilities": "^3.2.2",
70
+ "@excalidraw/excalidraw": "^0.18.0",
71
+ "@tailwindcss/typography": "^0.5.19",
72
+ "@types/bun": "^1.2.5",
73
+ "@types/diff": "^8.0.0",
74
+ "@types/react": "^19.0.0",
75
+ "@types/react-dom": "^19.0.0",
76
+ "@xyflow/react": "^12.10.1",
77
+ "@zumer/snapdom": "^2.0.2",
78
+ "typescript": "^5.9.3"
79
+ },
78
80
  "overrides": {
79
81
  "@radix-ui/react-tabs": "^1.1.13",
80
82
  "@radix-ui/react-collection": "^1.1.7",
package/src/App.tsx ADDED
@@ -0,0 +1,337 @@
1
+ import { useEffect, useState, useMemo, lazy, Suspense } from "react";
2
+ import { Panel, Group, Separator } from "react-resizable-panels";
3
+ import TopBar from "./components/TopBar.js";
4
+ import ChatPanel from "./components/ChatPanel.js";
5
+ import DiffPanel from "./components/DiffPanel.js";
6
+ import ProcessPanel from "./components/ProcessPanel.js";
7
+ import ContextPanel from "./components/ContextPanel.js";
8
+ import SchedulePanel from "./components/SchedulePanel.js";
9
+
10
+ import { useStore, nextId } from "./store.js";
11
+ import type { SelectionType } from "./types.js";
12
+ import { connect, sendViewerNotification } from "./ws.js";
13
+ import { loadMode, registerExternalMode } from "../core/mode-loader.js";
14
+ import { useSystemPreferences } from "./hooks/useSystemPreferences.js";
15
+ import { selectBestContentSet } from "../core/utils/content-set-matcher.js";
16
+ import type { ViewerPreviewProps } from "../core/types/viewer-contract.js";
17
+
18
+ const EditorPanel = lazy(() => import("./components/EditorPanel.js"));
19
+ const TerminalPanel = lazy(() => import("./components/TerminalPanel.js"));
20
+ const Launcher = lazy(() => import("./components/Launcher.js"));
21
+
22
+ function LazyFallback() {
23
+ return (
24
+ <div className="flex items-center justify-center h-full text-cc-muted text-sm">
25
+ Loading...
26
+ </div>
27
+ );
28
+ }
29
+
30
+ function RightPanel() {
31
+ const activeTab = useStore((s) => s.activeTab);
32
+ const [terminalMounted, setTerminalMounted] = useState(false);
33
+
34
+ useEffect(() => {
35
+ if (activeTab === "terminal") setTerminalMounted(true);
36
+ }, [activeTab]);
37
+
38
+ return (
39
+ <div className="flex flex-col h-full">
40
+ {activeTab === "chat" && <ChatPanel />}
41
+ {activeTab === "editor" && (
42
+ <Suspense fallback={<LazyFallback />}>
43
+ <EditorPanel />
44
+ </Suspense>
45
+ )}
46
+ {activeTab === "diff" && <DiffPanel />}
47
+ {/* Terminal stays mounted once visited to preserve PTY connection */}
48
+ {terminalMounted && (
49
+ <Suspense fallback={activeTab === "terminal" ? <LazyFallback /> : null}>
50
+ <div className={activeTab === "terminal" ? "flex flex-col h-full" : "hidden"}>
51
+ <TerminalPanel />
52
+ </div>
53
+ </Suspense>
54
+ )}
55
+ {activeTab === "processes" && <ProcessPanel />}
56
+ {activeTab === "context" && <ContextPanel />}
57
+ {activeTab === "schedules" && <SchedulePanel />}
58
+ </div>
59
+ );
60
+ }
61
+
62
+ /** Build the ViewerPreviewProps from store state. */
63
+ function useViewerProps(): ViewerPreviewProps {
64
+ const rawFiles = useStore((s) => s.files);
65
+ const activeContentSet = useStore((s) => s.activeContentSet);
66
+ const selection = useStore((s) => s.selection);
67
+ const setSelection = useStore((s) => s.setSelection);
68
+ const previewMode = useStore((s) => s.previewMode);
69
+ const imageTick = useStore((s) => s.imageTick);
70
+ const initParams = useStore((s) => s.initParams);
71
+ const activeFile = useStore((s) => s.activeFile);
72
+ const setActiveFile = useStore((s) => s.setActiveFile);
73
+ const setViewportRange = useStore((s) => s.setViewportRange);
74
+ const workspaceItems = useStore((s) => s.workspaceItems);
75
+ const actionRequest = useStore((s) => s.actionRequest);
76
+ const setActionRequest = useStore((s) => s.setActionRequest);
77
+ const navigateRequest = useStore((s) => s.navigateRequest);
78
+ const setNavigateRequest = useStore((s) => s.setNavigateRequest);
79
+ const contentSets = useStore((s) => s.contentSets);
80
+
81
+ // Filter and remap files based on active content set
82
+ const files = useMemo(() => {
83
+ if (!activeContentSet) return rawFiles.map((f) => ({ path: f.path, content: f.content }));
84
+ const pfx = activeContentSet + "/";
85
+ return rawFiles
86
+ .filter((f) => f.path.startsWith(pfx))
87
+ .map((f) => ({ path: f.path.slice(pfx.length), content: f.content }));
88
+ }, [rawFiles, activeContentSet]);
89
+
90
+ return {
91
+ files,
92
+ activeFile,
93
+ selection: selection
94
+ ? {
95
+ type: selection.type,
96
+ content: selection.content,
97
+ level: selection.level,
98
+ file: selection.file,
99
+ tag: selection.tag,
100
+ classes: selection.classes,
101
+ selector: selection.selector,
102
+ thumbnail: selection.thumbnail,
103
+ label: selection.label,
104
+ nearbyText: selection.nearbyText,
105
+ accessibility: selection.accessibility,
106
+ }
107
+ : null,
108
+ onSelect: (sel) => {
109
+ if (!sel) {
110
+ setSelection(null);
111
+ return;
112
+ }
113
+ // Use file from the viewer component (e.g. current slide), fallback to first file
114
+ const file = sel.file || files[0]?.path || "";
115
+ setSelection({
116
+ type: sel.type as SelectionType,
117
+ content: sel.content,
118
+ level: sel.level,
119
+ file,
120
+ tag: sel.tag,
121
+ classes: sel.classes,
122
+ selector: sel.selector,
123
+ thumbnail: sel.thumbnail,
124
+ label: sel.label,
125
+ nearbyText: sel.nearbyText,
126
+ accessibility: sel.accessibility,
127
+ });
128
+ },
129
+ mode: previewMode,
130
+ imageVersion: imageTick,
131
+ initParams,
132
+ onActiveFileChange: setActiveFile,
133
+ onViewportChange: setViewportRange,
134
+ workspaceItems,
135
+ actionRequest,
136
+ onActionResult: (requestId, result) => {
137
+ import("./ws.js").then(({ sendViewerActionResponse }) => {
138
+ sendViewerActionResponse(requestId, result);
139
+ });
140
+ setActionRequest(null);
141
+ },
142
+ onNotifyAgent: (notification) => {
143
+ // Queue — will be flushed when CC goes idle (see useFlushViewerNotification)
144
+ useStore.getState().setPendingViewerNotification(notification);
145
+ },
146
+ navigateRequest,
147
+ onNavigateComplete: () => setNavigateRequest(null),
148
+ };
149
+ }
150
+
151
+ function getApiBase(): string {
152
+ if (import.meta.env.DEV) {
153
+ return `http://${location.hostname}:${import.meta.env.VITE_API_PORT || "17007"}`;
154
+ }
155
+ return "";
156
+ }
157
+
158
+ export default function App() {
159
+ // Launcher mode — lightweight marketplace UI
160
+ const [isLauncher] = useState(() => {
161
+ const params = new URLSearchParams(location.search);
162
+ // Launcher if explicitly requested OR no session/mode params (bare URL)
163
+ return params.has("launcher") || (!params.has("session") && !params.has("mode"));
164
+ });
165
+ if (isLauncher) {
166
+ return (
167
+ <Suspense fallback={<LazyFallback />}>
168
+ <Launcher />
169
+ </Suspense>
170
+ );
171
+ }
172
+
173
+ const PreviewComponent = useStore((s) => s.modeViewer?.PreviewComponent);
174
+
175
+ useEffect(() => {
176
+ const params = new URLSearchParams(location.search);
177
+ const explicitSession = params.get("session");
178
+ const modeName = params.get("mode") || "doc";
179
+ if (params.get("debug") === "1") {
180
+ useStore.getState().setDebugMode(true);
181
+ }
182
+
183
+ // Check if this is an external mode — fetch mode info from server first
184
+ const loadModeAsync = async () => {
185
+ try {
186
+ const modeInfoRes = await fetch(`${getApiBase()}/api/mode-info`);
187
+ const modeInfo = await modeInfoRes.json();
188
+
189
+ if (modeInfo.external && modeInfo.name === modeName) {
190
+ // Register external mode so mode-loader knows where to import from
191
+ registerExternalMode(modeInfo.name, modeInfo.path);
192
+ console.log(`[app] Registered external mode "${modeInfo.name}" from ${modeInfo.path}`);
193
+ }
194
+ } catch {
195
+ // Server not available yet or no external mode — continue with builtin
196
+ }
197
+
198
+ const def = await loadMode(modeName);
199
+ useStore.getState().setModeViewer(def.viewer);
200
+ useStore.getState().setModeDisplayName(def.manifest.displayName);
201
+ };
202
+
203
+ loadModeAsync().catch((err) => {
204
+ console.error(`[app] Failed to load mode "${modeName}":`, err);
205
+ });
206
+
207
+ // Connect to session
208
+ if (explicitSession) {
209
+ connect(explicitSession);
210
+ } else {
211
+ fetch(`${getApiBase()}/api/session`)
212
+ .then((r) => r.json())
213
+ .then((d) => {
214
+ connect(d.sessionId || "default");
215
+ })
216
+ .catch(() => connect("default"));
217
+ }
218
+
219
+ // Fetch initial file contents, then restore last viewer state
220
+ fetch(`${getApiBase()}/api/files`)
221
+ .then((r) => r.json())
222
+ .then(async (d) => {
223
+ if (d.files?.length) useStore.getState().setFiles(d.files);
224
+ // Restore persisted viewer position (content set + active file)
225
+ try {
226
+ const vs = await fetch(`${getApiBase()}/api/viewer-state`).then((r) => r.json());
227
+ const store = useStore.getState();
228
+ if (vs.contentSet && store.contentSets.some((cs: { prefix: string }) => cs.prefix === vs.contentSet)) {
229
+ store.setActiveContentSet(vs.contentSet);
230
+ }
231
+ if (vs.file) {
232
+ // For content-set modes, the file path is relative within the set
233
+ const items = useStore.getState().workspaceItems;
234
+ if (items.some((item: { path: string }) => item.path === vs.file)) {
235
+ useStore.getState().setActiveFile(vs.file);
236
+ }
237
+ }
238
+ } catch { /* no saved state — auto-selection will handle it */ }
239
+ })
240
+ .catch(() => { });
241
+
242
+ // Fetch mode init params
243
+ fetch(`${getApiBase()}/api/config`)
244
+ .then((r) => r.json())
245
+ .then((d) => {
246
+ if (d.initParams) useStore.getState().setInitParams(d.initParams);
247
+ })
248
+ .catch(() => { });
249
+
250
+ // Check git availability
251
+ fetch(`${getApiBase()}/api/git/available`)
252
+ .then((r) => r.json())
253
+ .then((d) => useStore.getState().setGitAvailable(d.available))
254
+ .catch(() => useStore.getState().setGitAvailable(false));
255
+ }, []);
256
+
257
+ // Flush queued viewer notification when CC goes idle
258
+ const sessionStatus = useStore((s) => s.sessionStatus);
259
+ const pendingNotification = useStore((s) => s.pendingViewerNotification);
260
+ useEffect(() => {
261
+ if (sessionStatus !== "idle" || !pendingNotification) return;
262
+ const store = useStore.getState();
263
+ store.setPendingViewerNotification(null);
264
+
265
+ // Send to server
266
+ sendViewerNotification(pendingNotification);
267
+
268
+ // Parse affected files from notification message
269
+ const fileMatches = [...pendingNotification.message.matchAll(/\(([^)]+\.html)\)/g)];
270
+ const affectedFiles = fileMatches.map((m) => m[1]);
271
+
272
+ // Show as user-side bubble with context card
273
+ const msg: import("./types.js").ChatMessage = {
274
+ id: nextId(),
275
+ role: "user",
276
+ content: "",
277
+ timestamp: Date.now(),
278
+ viewerNotification: {
279
+ type: pendingNotification.type,
280
+ summary: pendingNotification.summary || pendingNotification.type,
281
+ files: affectedFiles.length > 0 ? affectedFiles : undefined,
282
+ },
283
+ };
284
+ if (store.debugMode) {
285
+ msg.debugPayload = { enrichedContent: pendingNotification.message };
286
+ }
287
+ store.appendMessage(msg);
288
+ }, [sessionStatus, pendingNotification]);
289
+
290
+ // Workspace item auto-selection (topBarNavigation modes)
291
+ const topBarNav = useStore((s) => s.modeViewer?.workspace?.topBarNavigation);
292
+ const workspaceItemsForAutoSelect = useStore((s) => s.workspaceItems);
293
+ const activeFileForAutoSelect = useStore((s) => s.activeFile);
294
+ useEffect(() => {
295
+ if (topBarNav && workspaceItemsForAutoSelect.length > 0 && !activeFileForAutoSelect) {
296
+ useStore.getState().setActiveFile(workspaceItemsForAutoSelect[0].path);
297
+ }
298
+ }, [topBarNav, workspaceItemsForAutoSelect, activeFileForAutoSelect]);
299
+
300
+ // Content set auto-selection based on system preferences
301
+ const contentSets = useStore((s) => s.contentSets);
302
+ const activeContentSet = useStore((s) => s.activeContentSet);
303
+ const systemPrefs = useSystemPreferences();
304
+ useEffect(() => {
305
+ if (contentSets.length > 1 && !activeContentSet) {
306
+ const best = selectBestContentSet(contentSets, systemPrefs);
307
+ if (best) useStore.getState().setActiveContentSet(best.prefix);
308
+ }
309
+ }, [contentSets, systemPrefs]); // activeContentSet intentionally excluded
310
+
311
+ const viewerProps = useViewerProps();
312
+
313
+ return (
314
+ <div className="flex flex-col h-screen bg-cc-bg text-cc-fg relative overflow-hidden p-4 sm:p-6 md:p-8">
315
+ {/* Immersive mesh gradient background element */}
316
+ <div className="absolute top-[-10%] left-[-10%] w-[60%] h-[50%] bg-cc-primary/10 blur-[120px] rounded-full pointer-events-none animate-[pulse-dot_8s_ease-in-out_infinite]" />
317
+ <div className="absolute top-[20%] right-[-10%] w-[50%] h-[60%] bg-purple-500/10 blur-[100px] rounded-full pointer-events-none animate-[pulse-dot_10s_ease-in-out_infinite_reverse]" />
318
+
319
+ <div className="relative z-10 flex flex-col flex-1 border border-cc-primary/20 rounded-2xl overflow-hidden shadow-[0_0_40px_rgba(249,115,22,0.15)] ring-1 ring-white/5 before:absolute before:inset-0 before:bg-cc-surface/40 before:backdrop-blur-3xl before:-z-10">
320
+ <TopBar />
321
+ <Group orientation="horizontal" className="flex-1">
322
+ <Panel defaultSize={65} minSize={30}>
323
+ {PreviewComponent ? (
324
+ <PreviewComponent {...viewerProps} />
325
+ ) : (
326
+ <LazyFallback />
327
+ )}
328
+ </Panel>
329
+ <Separator className="w-[1px] bg-cc-border/40 hover:w-1 hover:bg-cc-primary/40 transition-all duration-300 cursor-col-resize z-10" />
330
+ <Panel defaultSize={35} minSize={20}>
331
+ <RightPanel />
332
+ </Panel>
333
+ </Group>
334
+ </div>
335
+ </div>
336
+ );
337
+ }