@pixelbyte-software/pixcode 1.53.1 → 1.53.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/dist/assets/{index-BO0lz8ZD.js → index-CZZiJpxs.js} +196 -172
- package/dist/assets/index-FhOsv2Yy.css +32 -0
- package/dist/index.html +2 -2
- package/dist-server/server/index.js +201 -9
- package/dist-server/server/index.js.map +1 -1
- package/package.json +1 -1
- package/server/index.js +225 -9
- package/dist/assets/index-DnjqWG8B.css +0 -32
|
@@ -231,6 +231,52 @@ function getActiveUpdateJob() {
|
|
|
231
231
|
}
|
|
232
232
|
return null;
|
|
233
233
|
}
|
|
234
|
+
function countByProvider(items) {
|
|
235
|
+
return items.reduce((counts, item) => {
|
|
236
|
+
const provider = item?.provider || 'unknown';
|
|
237
|
+
counts[provider] = (counts[provider] || 0) + 1;
|
|
238
|
+
return counts;
|
|
239
|
+
}, {});
|
|
240
|
+
}
|
|
241
|
+
function getActiveProviderWorkSummary() {
|
|
242
|
+
const sessions = [
|
|
243
|
+
...getActiveClaudeSDKSessions().map((id) => ({ id, provider: 'claude' })),
|
|
244
|
+
...getActiveCursorSessions().map((session) => ({ ...session, provider: 'cursor' })),
|
|
245
|
+
...getActiveCodexSessions().map((session) => ({ ...session, provider: 'codex' })),
|
|
246
|
+
...getActiveGeminiSessions().map((session) => ({ ...session, provider: 'gemini' })),
|
|
247
|
+
...getActiveQwenSessions().map((session) => ({ ...session, provider: 'qwen' })),
|
|
248
|
+
...getActiveOpencodeSessions().map((session) => ({ ...session, provider: 'opencode' })),
|
|
249
|
+
];
|
|
250
|
+
return {
|
|
251
|
+
total: sessions.length,
|
|
252
|
+
byProvider: countByProvider(sessions),
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
function getActivePtyWorkSummary() {
|
|
256
|
+
const activePtys = Array.from(ptySessionsMap.values())
|
|
257
|
+
.filter((session) => session?.pty && session.lifecycleState === 'running')
|
|
258
|
+
.map((session) => ({
|
|
259
|
+
provider: session.isPlainShell ? 'plain-shell' : (session.provider || 'unknown'),
|
|
260
|
+
connected: Boolean(session.ws && session.ws.readyState === WebSocket.OPEN),
|
|
261
|
+
}));
|
|
262
|
+
return {
|
|
263
|
+
total: activePtys.length,
|
|
264
|
+
connected: activePtys.filter((session) => session.connected).length,
|
|
265
|
+
detached: activePtys.filter((session) => !session.connected).length,
|
|
266
|
+
byProvider: countByProvider(activePtys),
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
function getActiveWorkSummary() {
|
|
270
|
+
const pty = getActivePtyWorkSummary();
|
|
271
|
+
const agents = getActiveProviderWorkSummary();
|
|
272
|
+
const total = pty.total + agents.total;
|
|
273
|
+
return {
|
|
274
|
+
hasActiveWork: total > 0,
|
|
275
|
+
total,
|
|
276
|
+
pty,
|
|
277
|
+
agents,
|
|
278
|
+
};
|
|
279
|
+
}
|
|
234
280
|
async function readLatestPixcodePackageMetadata() {
|
|
235
281
|
const registryRes = await fetch('https://registry.npmjs.org/@pixelbyte-software/pixcode');
|
|
236
282
|
if (!registryRes.ok)
|
|
@@ -329,8 +375,13 @@ async function runCommandUpdateJob(job, updateCommand, updateCwd) {
|
|
|
329
375
|
cwd: updateCwd,
|
|
330
376
|
env: process.env,
|
|
331
377
|
shell: true,
|
|
378
|
+
detached: true,
|
|
332
379
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
333
380
|
});
|
|
381
|
+
try {
|
|
382
|
+
child.unref();
|
|
383
|
+
}
|
|
384
|
+
catch { /* noop */ }
|
|
334
385
|
child.stdout?.on('data', (data) => appendUpdateJobLog(job, 'stdout', data.toString()));
|
|
335
386
|
child.stderr?.on('data', (data) => appendUpdateJobLog(job, 'stderr', data.toString()));
|
|
336
387
|
child.on('error', reject);
|
|
@@ -624,7 +675,94 @@ async function setupProjectsWatcher() {
|
|
|
624
675
|
// `project_files_updated` events to subscribed clients only, letting the
|
|
625
676
|
// explorer refresh automatically without HTTP polling.
|
|
626
677
|
const WORKSPACE_WATCHER_DEBOUNCE_MS = 800;
|
|
627
|
-
const
|
|
678
|
+
const WORKSPACE_DIFF_SNAPSHOT_MAX_BYTES = 512 * 1024;
|
|
679
|
+
const WORKSPACE_DIFF_SNAPSHOT_MAX_FILES = 800;
|
|
680
|
+
const workspaceWatchers = new Map(); // projectName -> { watcher, subscribers, debounceTimer, pendingEvent, rootPath, fileSnapshots }
|
|
681
|
+
function runWorkspaceCommand(command, args, cwd) {
|
|
682
|
+
return new Promise((resolve, reject) => {
|
|
683
|
+
const child = spawn(command, args, {
|
|
684
|
+
cwd,
|
|
685
|
+
shell: false,
|
|
686
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
687
|
+
});
|
|
688
|
+
let stdout = '';
|
|
689
|
+
let stderr = '';
|
|
690
|
+
child.stdout?.on('data', (chunk) => {
|
|
691
|
+
stdout += chunk.toString();
|
|
692
|
+
});
|
|
693
|
+
child.stderr?.on('data', (chunk) => {
|
|
694
|
+
stderr += chunk.toString();
|
|
695
|
+
});
|
|
696
|
+
child.on('error', reject);
|
|
697
|
+
child.on('close', (code) => {
|
|
698
|
+
if (code === 0) {
|
|
699
|
+
resolve({ stdout, stderr });
|
|
700
|
+
return;
|
|
701
|
+
}
|
|
702
|
+
reject(new Error(stderr || `${command} exited with code ${code}`));
|
|
703
|
+
});
|
|
704
|
+
});
|
|
705
|
+
}
|
|
706
|
+
function parseGitPorcelainZ(output) {
|
|
707
|
+
const entries = [];
|
|
708
|
+
const parts = String(output || '').split('\0').filter(Boolean);
|
|
709
|
+
for (let index = 0; index < parts.length; index += 1) {
|
|
710
|
+
const entry = parts[index];
|
|
711
|
+
if (entry.length < 4) {
|
|
712
|
+
continue;
|
|
713
|
+
}
|
|
714
|
+
const status = entry.slice(0, 2);
|
|
715
|
+
const filePath = entry.slice(3).replace(/\\/g, '/');
|
|
716
|
+
if (filePath) {
|
|
717
|
+
entries.push(filePath);
|
|
718
|
+
}
|
|
719
|
+
if (status[0] === 'R' || status[0] === 'C') {
|
|
720
|
+
index += 1;
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
return entries;
|
|
724
|
+
}
|
|
725
|
+
function normalizeWorkspaceRelativePath(rootPath, filePath) {
|
|
726
|
+
const relativePath = path.relative(rootPath, filePath).replace(/\\/g, '/');
|
|
727
|
+
if (!relativePath || relativePath.startsWith('../') || relativePath === '..' || path.isAbsolute(relativePath)) {
|
|
728
|
+
return null;
|
|
729
|
+
}
|
|
730
|
+
return relativePath;
|
|
731
|
+
}
|
|
732
|
+
async function readWorkspaceTextSnapshot(filePath) {
|
|
733
|
+
try {
|
|
734
|
+
const stats = await fsPromises.stat(filePath);
|
|
735
|
+
if (!stats.isFile() || stats.size > WORKSPACE_DIFF_SNAPSHOT_MAX_BYTES) {
|
|
736
|
+
return null;
|
|
737
|
+
}
|
|
738
|
+
const buffer = await fsPromises.readFile(filePath);
|
|
739
|
+
if (buffer.includes(0)) {
|
|
740
|
+
return null;
|
|
741
|
+
}
|
|
742
|
+
return buffer.toString('utf8');
|
|
743
|
+
}
|
|
744
|
+
catch (error) {
|
|
745
|
+
if (error?.code === 'ENOENT') {
|
|
746
|
+
return '';
|
|
747
|
+
}
|
|
748
|
+
return null;
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
async function initializeWorkspaceDirtySnapshots(rootPath, fileSnapshots) {
|
|
752
|
+
try {
|
|
753
|
+
const { stdout } = await runWorkspaceCommand('git', ['status', '--porcelain', '-z'], rootPath);
|
|
754
|
+
const changedPaths = parseGitPorcelainZ(stdout).slice(0, WORKSPACE_DIFF_SNAPSHOT_MAX_FILES);
|
|
755
|
+
await Promise.all(changedPaths.map(async (relativePath) => {
|
|
756
|
+
const content = await readWorkspaceTextSnapshot(path.join(rootPath, relativePath));
|
|
757
|
+
if (typeof content === 'string') {
|
|
758
|
+
fileSnapshots.set(relativePath, content);
|
|
759
|
+
}
|
|
760
|
+
}));
|
|
761
|
+
}
|
|
762
|
+
catch {
|
|
763
|
+
// Non-git workspaces still get event-time snapshots after the first change.
|
|
764
|
+
}
|
|
765
|
+
}
|
|
628
766
|
async function subscribeToWorkspace(ws, projectName) {
|
|
629
767
|
if (!projectName || typeof projectName !== 'string')
|
|
630
768
|
return;
|
|
@@ -650,10 +788,44 @@ async function subscribeToWorkspace(ws, projectName) {
|
|
|
650
788
|
debounceTimer: null,
|
|
651
789
|
pendingEvent: null,
|
|
652
790
|
rootPath,
|
|
791
|
+
fileSnapshots: new Map(),
|
|
653
792
|
};
|
|
654
793
|
workspaceWatchers.set(projectName, entry);
|
|
655
|
-
|
|
656
|
-
|
|
794
|
+
await initializeWorkspaceDirtySnapshots(rootPath, entry.fileSnapshots);
|
|
795
|
+
const broadcastFileUpdate = async (eventType, filePath) => {
|
|
796
|
+
const relativePath = filePath ? normalizeWorkspaceRelativePath(rootPath, filePath) : null;
|
|
797
|
+
const previousContent = relativePath ? entry.fileSnapshots.get(relativePath) : undefined;
|
|
798
|
+
let currentContent;
|
|
799
|
+
if (relativePath && !eventType.endsWith('Dir')) {
|
|
800
|
+
if (eventType === 'unlink') {
|
|
801
|
+
currentContent = '';
|
|
802
|
+
entry.fileSnapshots.delete(relativePath);
|
|
803
|
+
}
|
|
804
|
+
else {
|
|
805
|
+
const snapshot = await readWorkspaceTextSnapshot(filePath);
|
|
806
|
+
if (typeof snapshot === 'string') {
|
|
807
|
+
currentContent = snapshot;
|
|
808
|
+
entry.fileSnapshots.set(relativePath, snapshot);
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
const nextEvent = {
|
|
813
|
+
eventType,
|
|
814
|
+
filePath,
|
|
815
|
+
changedFile: relativePath,
|
|
816
|
+
oldContent: typeof previousContent === 'string'
|
|
817
|
+
? previousContent
|
|
818
|
+
: eventType === 'add'
|
|
819
|
+
? ''
|
|
820
|
+
: undefined,
|
|
821
|
+
currentContent,
|
|
822
|
+
};
|
|
823
|
+
entry.pendingEvent = entry.pendingEvent?.changedFile === relativePath
|
|
824
|
+
? {
|
|
825
|
+
...nextEvent,
|
|
826
|
+
oldContent: entry.pendingEvent.oldContent ?? nextEvent.oldContent,
|
|
827
|
+
}
|
|
828
|
+
: nextEvent;
|
|
657
829
|
if (entry.debounceTimer) {
|
|
658
830
|
clearTimeout(entry.debounceTimer);
|
|
659
831
|
}
|
|
@@ -661,11 +833,21 @@ async function subscribeToWorkspace(ws, projectName) {
|
|
|
661
833
|
entry.debounceTimer = null;
|
|
662
834
|
const pending = entry.pendingEvent || {};
|
|
663
835
|
entry.pendingEvent = null;
|
|
836
|
+
const hasSnapshotDiff = (typeof pending.oldContent === 'string'
|
|
837
|
+
&& typeof pending.currentContent === 'string'
|
|
838
|
+
&& pending.oldContent !== pending.currentContent);
|
|
664
839
|
const message = JSON.stringify({
|
|
665
840
|
type: 'project_files_updated',
|
|
666
841
|
projectName,
|
|
667
842
|
changeType: pending.eventType || 'change',
|
|
668
|
-
changedFile: pending.filePath ? path.relative(rootPath, pending.filePath) : null,
|
|
843
|
+
changedFile: pending.changedFile ?? (pending.filePath ? path.relative(rootPath, pending.filePath) : null),
|
|
844
|
+
...(hasSnapshotDiff
|
|
845
|
+
? {
|
|
846
|
+
oldContent: pending.oldContent,
|
|
847
|
+
currentContent: pending.currentContent,
|
|
848
|
+
diffSource: 'workspace-snapshot',
|
|
849
|
+
}
|
|
850
|
+
: {}),
|
|
669
851
|
timestamp: new Date().toISOString(),
|
|
670
852
|
});
|
|
671
853
|
entry.subscribers.forEach((client) => {
|
|
@@ -689,11 +871,11 @@ async function subscribeToWorkspace(ws, projectName) {
|
|
|
689
871
|
}
|
|
690
872
|
});
|
|
691
873
|
watcher
|
|
692
|
-
.on('add', (filePath) => broadcastFileUpdate('add', filePath))
|
|
693
|
-
.on('change', (filePath) => broadcastFileUpdate('change', filePath))
|
|
694
|
-
.on('unlink', (filePath) => broadcastFileUpdate('unlink', filePath))
|
|
695
|
-
.on('addDir', (dirPath) => broadcastFileUpdate('addDir', dirPath))
|
|
696
|
-
.on('unlinkDir', (dirPath) => broadcastFileUpdate('unlinkDir', dirPath))
|
|
874
|
+
.on('add', (filePath) => void broadcastFileUpdate('add', filePath))
|
|
875
|
+
.on('change', (filePath) => void broadcastFileUpdate('change', filePath))
|
|
876
|
+
.on('unlink', (filePath) => void broadcastFileUpdate('unlink', filePath))
|
|
877
|
+
.on('addDir', (dirPath) => void broadcastFileUpdate('addDir', dirPath))
|
|
878
|
+
.on('unlinkDir', (dirPath) => void broadcastFileUpdate('unlinkDir', dirPath))
|
|
697
879
|
.on('error', (error) => {
|
|
698
880
|
console.error(`[ERROR] Workspace watcher error for ${projectName}:`, error);
|
|
699
881
|
});
|
|
@@ -1296,6 +1478,7 @@ app.get('/api/system/update-state', authenticateToken, (req, res) => {
|
|
|
1296
1478
|
success: true,
|
|
1297
1479
|
state: readSystemUpdateState(),
|
|
1298
1480
|
activeJob: snapshotUpdateJob(getActiveUpdateJob()),
|
|
1481
|
+
activeWork: getActiveWorkSummary(),
|
|
1299
1482
|
currentVersion: SERVER_VERSION,
|
|
1300
1483
|
installMode,
|
|
1301
1484
|
capabilities: {
|
|
@@ -1684,6 +1867,15 @@ app.post('/api/system/update', authenticateToken, requireAdmin, requireApiScope(
|
|
|
1684
1867
|
// (systemd/pm2/daemon manager) can bring the server back on the new code.
|
|
1685
1868
|
// Foreground installs without a wrapper will simply stop; the UI reports this.
|
|
1686
1869
|
app.post('/api/system/restart', authenticateToken, requireAdmin, requireApiScope('system:restart'), (req, res) => {
|
|
1870
|
+
const forceRestart = req.body?.force === true || req.query.force === 'true';
|
|
1871
|
+
const activeWork = getActiveWorkSummary();
|
|
1872
|
+
if (!forceRestart && activeWork.hasActiveWork) {
|
|
1873
|
+
return res.status(409).json({
|
|
1874
|
+
success: false,
|
|
1875
|
+
error: 'Active terminal or agent sessions are running. Confirm restart to interrupt them.',
|
|
1876
|
+
activeWork,
|
|
1877
|
+
});
|
|
1878
|
+
}
|
|
1687
1879
|
res.json({
|
|
1688
1880
|
success: true,
|
|
1689
1881
|
version: SERVER_VERSION,
|