@pixelbyte-software/pixcode 1.53.1 → 1.53.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/dist/assets/{index-BO0lz8ZD.js → index-CP2HfVKD.js} +150 -132
- package/dist/assets/index-FhOsv2Yy.css +32 -0
- package/dist/index.html +2 -2
- package/dist-server/server/index.js +140 -9
- package/dist-server/server/index.js.map +1 -1
- package/package.json +1 -1
- package/server/index.js +159 -9
- package/dist/assets/index-DnjqWG8B.css +0 -32
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pixelbyte-software/pixcode",
|
|
3
|
-
"version": "1.53.
|
|
3
|
+
"version": "1.53.2",
|
|
4
4
|
"description": "Self-hosted AI coding agent control room for Claude Code, Cursor CLI, OpenAI Codex, Gemini CLI, Qwen Code, and OpenCode with chat, files, shell, Git, orchestration, API keys, Telegram, MCP, plugins, themes, and desktop/server deployment.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist-server/server/index.js",
|
package/server/index.js
CHANGED
|
@@ -695,7 +695,108 @@ async function setupProjectsWatcher() {
|
|
|
695
695
|
// `project_files_updated` events to subscribed clients only, letting the
|
|
696
696
|
// explorer refresh automatically without HTTP polling.
|
|
697
697
|
const WORKSPACE_WATCHER_DEBOUNCE_MS = 800;
|
|
698
|
-
const
|
|
698
|
+
const WORKSPACE_DIFF_SNAPSHOT_MAX_BYTES = 512 * 1024;
|
|
699
|
+
const WORKSPACE_DIFF_SNAPSHOT_MAX_FILES = 800;
|
|
700
|
+
const workspaceWatchers = new Map(); // projectName -> { watcher, subscribers, debounceTimer, pendingEvent, rootPath, fileSnapshots }
|
|
701
|
+
|
|
702
|
+
function runWorkspaceCommand(command, args, cwd) {
|
|
703
|
+
return new Promise((resolve, reject) => {
|
|
704
|
+
const child = spawn(command, args, {
|
|
705
|
+
cwd,
|
|
706
|
+
shell: false,
|
|
707
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
708
|
+
});
|
|
709
|
+
let stdout = '';
|
|
710
|
+
let stderr = '';
|
|
711
|
+
|
|
712
|
+
child.stdout?.on('data', (chunk) => {
|
|
713
|
+
stdout += chunk.toString();
|
|
714
|
+
});
|
|
715
|
+
child.stderr?.on('data', (chunk) => {
|
|
716
|
+
stderr += chunk.toString();
|
|
717
|
+
});
|
|
718
|
+
child.on('error', reject);
|
|
719
|
+
child.on('close', (code) => {
|
|
720
|
+
if (code === 0) {
|
|
721
|
+
resolve({ stdout, stderr });
|
|
722
|
+
return;
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
reject(new Error(stderr || `${command} exited with code ${code}`));
|
|
726
|
+
});
|
|
727
|
+
});
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
function parseGitPorcelainZ(output) {
|
|
731
|
+
const entries = [];
|
|
732
|
+
const parts = String(output || '').split('\0').filter(Boolean);
|
|
733
|
+
|
|
734
|
+
for (let index = 0; index < parts.length; index += 1) {
|
|
735
|
+
const entry = parts[index];
|
|
736
|
+
if (entry.length < 4) {
|
|
737
|
+
continue;
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
const status = entry.slice(0, 2);
|
|
741
|
+
const filePath = entry.slice(3).replace(/\\/g, '/');
|
|
742
|
+
if (filePath) {
|
|
743
|
+
entries.push(filePath);
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
if (status[0] === 'R' || status[0] === 'C') {
|
|
747
|
+
index += 1;
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
return entries;
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
function normalizeWorkspaceRelativePath(rootPath, filePath) {
|
|
755
|
+
const relativePath = path.relative(rootPath, filePath).replace(/\\/g, '/');
|
|
756
|
+
if (!relativePath || relativePath.startsWith('../') || relativePath === '..' || path.isAbsolute(relativePath)) {
|
|
757
|
+
return null;
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
return relativePath;
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
async function readWorkspaceTextSnapshot(filePath) {
|
|
764
|
+
try {
|
|
765
|
+
const stats = await fsPromises.stat(filePath);
|
|
766
|
+
if (!stats.isFile() || stats.size > WORKSPACE_DIFF_SNAPSHOT_MAX_BYTES) {
|
|
767
|
+
return null;
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
const buffer = await fsPromises.readFile(filePath);
|
|
771
|
+
if (buffer.includes(0)) {
|
|
772
|
+
return null;
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
return buffer.toString('utf8');
|
|
776
|
+
} catch (error) {
|
|
777
|
+
if (error?.code === 'ENOENT') {
|
|
778
|
+
return '';
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
return null;
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
async function initializeWorkspaceDirtySnapshots(rootPath, fileSnapshots) {
|
|
786
|
+
try {
|
|
787
|
+
const { stdout } = await runWorkspaceCommand('git', ['status', '--porcelain', '-z'], rootPath);
|
|
788
|
+
const changedPaths = parseGitPorcelainZ(stdout).slice(0, WORKSPACE_DIFF_SNAPSHOT_MAX_FILES);
|
|
789
|
+
|
|
790
|
+
await Promise.all(changedPaths.map(async (relativePath) => {
|
|
791
|
+
const content = await readWorkspaceTextSnapshot(path.join(rootPath, relativePath));
|
|
792
|
+
if (typeof content === 'string') {
|
|
793
|
+
fileSnapshots.set(relativePath, content);
|
|
794
|
+
}
|
|
795
|
+
}));
|
|
796
|
+
} catch {
|
|
797
|
+
// Non-git workspaces still get event-time snapshots after the first change.
|
|
798
|
+
}
|
|
799
|
+
}
|
|
699
800
|
|
|
700
801
|
async function subscribeToWorkspace(ws, projectName) {
|
|
701
802
|
if (!projectName || typeof projectName !== 'string') return;
|
|
@@ -722,11 +823,48 @@ async function subscribeToWorkspace(ws, projectName) {
|
|
|
722
823
|
debounceTimer: null,
|
|
723
824
|
pendingEvent: null,
|
|
724
825
|
rootPath,
|
|
826
|
+
fileSnapshots: new Map(),
|
|
725
827
|
};
|
|
726
828
|
workspaceWatchers.set(projectName, entry);
|
|
829
|
+
await initializeWorkspaceDirtySnapshots(rootPath, entry.fileSnapshots);
|
|
830
|
+
|
|
831
|
+
const broadcastFileUpdate = async (eventType, filePath) => {
|
|
832
|
+
const relativePath = filePath ? normalizeWorkspaceRelativePath(rootPath, filePath) : null;
|
|
833
|
+
const previousContent = relativePath ? entry.fileSnapshots.get(relativePath) : undefined;
|
|
834
|
+
let currentContent;
|
|
835
|
+
|
|
836
|
+
if (relativePath && !eventType.endsWith('Dir')) {
|
|
837
|
+
if (eventType === 'unlink') {
|
|
838
|
+
currentContent = '';
|
|
839
|
+
entry.fileSnapshots.delete(relativePath);
|
|
840
|
+
} else {
|
|
841
|
+
const snapshot = await readWorkspaceTextSnapshot(filePath);
|
|
842
|
+
if (typeof snapshot === 'string') {
|
|
843
|
+
currentContent = snapshot;
|
|
844
|
+
entry.fileSnapshots.set(relativePath, snapshot);
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
const nextEvent = {
|
|
850
|
+
eventType,
|
|
851
|
+
filePath,
|
|
852
|
+
changedFile: relativePath,
|
|
853
|
+
oldContent: typeof previousContent === 'string'
|
|
854
|
+
? previousContent
|
|
855
|
+
: eventType === 'add'
|
|
856
|
+
? ''
|
|
857
|
+
: undefined,
|
|
858
|
+
currentContent,
|
|
859
|
+
};
|
|
860
|
+
|
|
861
|
+
entry.pendingEvent = entry.pendingEvent?.changedFile === relativePath
|
|
862
|
+
? {
|
|
863
|
+
...nextEvent,
|
|
864
|
+
oldContent: entry.pendingEvent.oldContent ?? nextEvent.oldContent,
|
|
865
|
+
}
|
|
866
|
+
: nextEvent;
|
|
727
867
|
|
|
728
|
-
const broadcastFileUpdate = (eventType, filePath) => {
|
|
729
|
-
entry.pendingEvent = { eventType, filePath };
|
|
730
868
|
if (entry.debounceTimer) {
|
|
731
869
|
clearTimeout(entry.debounceTimer);
|
|
732
870
|
}
|
|
@@ -734,11 +872,23 @@ async function subscribeToWorkspace(ws, projectName) {
|
|
|
734
872
|
entry.debounceTimer = null;
|
|
735
873
|
const pending = entry.pendingEvent || {};
|
|
736
874
|
entry.pendingEvent = null;
|
|
875
|
+
const hasSnapshotDiff = (
|
|
876
|
+
typeof pending.oldContent === 'string'
|
|
877
|
+
&& typeof pending.currentContent === 'string'
|
|
878
|
+
&& pending.oldContent !== pending.currentContent
|
|
879
|
+
);
|
|
737
880
|
const message = JSON.stringify({
|
|
738
881
|
type: 'project_files_updated',
|
|
739
882
|
projectName,
|
|
740
883
|
changeType: pending.eventType || 'change',
|
|
741
|
-
changedFile: pending.filePath ? path.relative(rootPath, pending.filePath) : null,
|
|
884
|
+
changedFile: pending.changedFile ?? (pending.filePath ? path.relative(rootPath, pending.filePath) : null),
|
|
885
|
+
...(hasSnapshotDiff
|
|
886
|
+
? {
|
|
887
|
+
oldContent: pending.oldContent,
|
|
888
|
+
currentContent: pending.currentContent,
|
|
889
|
+
diffSource: 'workspace-snapshot',
|
|
890
|
+
}
|
|
891
|
+
: {}),
|
|
742
892
|
timestamp: new Date().toISOString(),
|
|
743
893
|
});
|
|
744
894
|
entry.subscribers.forEach((client) => {
|
|
@@ -764,11 +914,11 @@ async function subscribeToWorkspace(ws, projectName) {
|
|
|
764
914
|
});
|
|
765
915
|
|
|
766
916
|
watcher
|
|
767
|
-
.on('add', (filePath) => broadcastFileUpdate('add', filePath))
|
|
768
|
-
.on('change', (filePath) => broadcastFileUpdate('change', filePath))
|
|
769
|
-
.on('unlink', (filePath) => broadcastFileUpdate('unlink', filePath))
|
|
770
|
-
.on('addDir', (dirPath) => broadcastFileUpdate('addDir', dirPath))
|
|
771
|
-
.on('unlinkDir', (dirPath) => broadcastFileUpdate('unlinkDir', dirPath))
|
|
917
|
+
.on('add', (filePath) => void broadcastFileUpdate('add', filePath))
|
|
918
|
+
.on('change', (filePath) => void broadcastFileUpdate('change', filePath))
|
|
919
|
+
.on('unlink', (filePath) => void broadcastFileUpdate('unlink', filePath))
|
|
920
|
+
.on('addDir', (dirPath) => void broadcastFileUpdate('addDir', dirPath))
|
|
921
|
+
.on('unlinkDir', (dirPath) => void broadcastFileUpdate('unlinkDir', dirPath))
|
|
772
922
|
.on('error', (error) => {
|
|
773
923
|
console.error(`[ERROR] Workspace watcher error for ${projectName}:`, error);
|
|
774
924
|
});
|