@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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pixelbyte-software/pixcode",
3
- "version": "1.53.1",
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 workspaceWatchers = new Map(); // projectName -> { watcher, subscribers, debounceTimer, pendingEvent, rootPath }
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
  });