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 +1 -0
- package/modes/mode-maker/seed/package.json +10 -2
- package/package.json +20 -18
- package/src/App.tsx +337 -0
- package/src/__tests__/task-extraction.test.ts +350 -0
- package/src/components/ActivityIndicator.tsx +65 -0
- package/src/components/ChatInput.tsx +483 -0
- package/src/components/ChatPanel.tsx +129 -0
- package/src/components/ContentSetSelector.tsx +96 -0
- package/src/components/ContextPanel.tsx +261 -0
- package/src/components/DiffPanel.tsx +123 -0
- package/src/components/DiffViewer.tsx +141 -0
- package/src/components/EditorPanel.tsx +341 -0
- package/src/components/Launcher.tsx +1322 -0
- package/src/components/MessageBubble.tsx +1194 -0
- package/src/components/ModelSwitcher.tsx +82 -0
- package/src/components/PermissionBanner.tsx +53 -0
- package/src/components/ProcessPanel.tsx +192 -0
- package/src/components/ScaffoldConfirm.tsx +84 -0
- package/src/components/SchedulePanel.tsx +124 -0
- package/src/components/SlashMenu.tsx +56 -0
- package/src/components/StreamingText.tsx +21 -0
- package/src/components/TerminalPanel.tsx +214 -0
- package/src/components/ToolBlock.tsx +289 -0
- package/src/components/TopBar.tsx +151 -0
- package/src/components/__tests__/context-parser.test.ts +185 -0
- package/src/hooks/useSystemPreferences.ts +43 -0
- package/src/index.css +253 -0
- package/src/main.tsx +16 -0
- package/src/store.ts +648 -0
- package/src/types.ts +84 -0
- package/src/ws.ts +1065 -0
- package/vite.config.ts +172 -0
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
|
-
"
|
|
6
|
-
"
|
|
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.
|
|
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
|
+
}
|