@pixelbyte-software/pixcode 1.53.0 → 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/index.html CHANGED
@@ -35,11 +35,11 @@
35
35
 
36
36
  <!-- Prevent zoom on iOS -->
37
37
  <meta name="format-detection" content="telephone=no" />
38
- <script type="module" crossorigin src="/assets/index-yE2qL74i.js"></script>
38
+ <script type="module" crossorigin src="/assets/index-CP2HfVKD.js"></script>
39
39
  <link rel="modulepreload" crossorigin href="/assets/vendor-react-DB6V5Fl1.js">
40
40
  <link rel="modulepreload" crossorigin href="/assets/vendor-codemirror-CIYNS698.js">
41
41
  <link rel="modulepreload" crossorigin href="/assets/vendor-xterm-C7tpxJl7.js">
42
- <link rel="stylesheet" crossorigin href="/assets/index-Cz9BCbXv.css">
42
+ <link rel="stylesheet" crossorigin href="/assets/index-FhOsv2Yy.css">
43
43
  </head>
44
44
  <body>
45
45
  <div id="root"></div>
@@ -624,7 +624,94 @@ async function setupProjectsWatcher() {
624
624
  // `project_files_updated` events to subscribed clients only, letting the
625
625
  // explorer refresh automatically without HTTP polling.
626
626
  const WORKSPACE_WATCHER_DEBOUNCE_MS = 800;
627
- const workspaceWatchers = new Map(); // projectName -> { watcher, subscribers, debounceTimer, pendingEvent, rootPath }
627
+ const WORKSPACE_DIFF_SNAPSHOT_MAX_BYTES = 512 * 1024;
628
+ const WORKSPACE_DIFF_SNAPSHOT_MAX_FILES = 800;
629
+ const workspaceWatchers = new Map(); // projectName -> { watcher, subscribers, debounceTimer, pendingEvent, rootPath, fileSnapshots }
630
+ function runWorkspaceCommand(command, args, cwd) {
631
+ return new Promise((resolve, reject) => {
632
+ const child = spawn(command, args, {
633
+ cwd,
634
+ shell: false,
635
+ stdio: ['ignore', 'pipe', 'pipe'],
636
+ });
637
+ let stdout = '';
638
+ let stderr = '';
639
+ child.stdout?.on('data', (chunk) => {
640
+ stdout += chunk.toString();
641
+ });
642
+ child.stderr?.on('data', (chunk) => {
643
+ stderr += chunk.toString();
644
+ });
645
+ child.on('error', reject);
646
+ child.on('close', (code) => {
647
+ if (code === 0) {
648
+ resolve({ stdout, stderr });
649
+ return;
650
+ }
651
+ reject(new Error(stderr || `${command} exited with code ${code}`));
652
+ });
653
+ });
654
+ }
655
+ function parseGitPorcelainZ(output) {
656
+ const entries = [];
657
+ const parts = String(output || '').split('\0').filter(Boolean);
658
+ for (let index = 0; index < parts.length; index += 1) {
659
+ const entry = parts[index];
660
+ if (entry.length < 4) {
661
+ continue;
662
+ }
663
+ const status = entry.slice(0, 2);
664
+ const filePath = entry.slice(3).replace(/\\/g, '/');
665
+ if (filePath) {
666
+ entries.push(filePath);
667
+ }
668
+ if (status[0] === 'R' || status[0] === 'C') {
669
+ index += 1;
670
+ }
671
+ }
672
+ return entries;
673
+ }
674
+ function normalizeWorkspaceRelativePath(rootPath, filePath) {
675
+ const relativePath = path.relative(rootPath, filePath).replace(/\\/g, '/');
676
+ if (!relativePath || relativePath.startsWith('../') || relativePath === '..' || path.isAbsolute(relativePath)) {
677
+ return null;
678
+ }
679
+ return relativePath;
680
+ }
681
+ async function readWorkspaceTextSnapshot(filePath) {
682
+ try {
683
+ const stats = await fsPromises.stat(filePath);
684
+ if (!stats.isFile() || stats.size > WORKSPACE_DIFF_SNAPSHOT_MAX_BYTES) {
685
+ return null;
686
+ }
687
+ const buffer = await fsPromises.readFile(filePath);
688
+ if (buffer.includes(0)) {
689
+ return null;
690
+ }
691
+ return buffer.toString('utf8');
692
+ }
693
+ catch (error) {
694
+ if (error?.code === 'ENOENT') {
695
+ return '';
696
+ }
697
+ return null;
698
+ }
699
+ }
700
+ async function initializeWorkspaceDirtySnapshots(rootPath, fileSnapshots) {
701
+ try {
702
+ const { stdout } = await runWorkspaceCommand('git', ['status', '--porcelain', '-z'], rootPath);
703
+ const changedPaths = parseGitPorcelainZ(stdout).slice(0, WORKSPACE_DIFF_SNAPSHOT_MAX_FILES);
704
+ await Promise.all(changedPaths.map(async (relativePath) => {
705
+ const content = await readWorkspaceTextSnapshot(path.join(rootPath, relativePath));
706
+ if (typeof content === 'string') {
707
+ fileSnapshots.set(relativePath, content);
708
+ }
709
+ }));
710
+ }
711
+ catch {
712
+ // Non-git workspaces still get event-time snapshots after the first change.
713
+ }
714
+ }
628
715
  async function subscribeToWorkspace(ws, projectName) {
629
716
  if (!projectName || typeof projectName !== 'string')
630
717
  return;
@@ -650,10 +737,44 @@ async function subscribeToWorkspace(ws, projectName) {
650
737
  debounceTimer: null,
651
738
  pendingEvent: null,
652
739
  rootPath,
740
+ fileSnapshots: new Map(),
653
741
  };
654
742
  workspaceWatchers.set(projectName, entry);
655
- const broadcastFileUpdate = (eventType, filePath) => {
656
- entry.pendingEvent = { eventType, filePath };
743
+ await initializeWorkspaceDirtySnapshots(rootPath, entry.fileSnapshots);
744
+ const broadcastFileUpdate = async (eventType, filePath) => {
745
+ const relativePath = filePath ? normalizeWorkspaceRelativePath(rootPath, filePath) : null;
746
+ const previousContent = relativePath ? entry.fileSnapshots.get(relativePath) : undefined;
747
+ let currentContent;
748
+ if (relativePath && !eventType.endsWith('Dir')) {
749
+ if (eventType === 'unlink') {
750
+ currentContent = '';
751
+ entry.fileSnapshots.delete(relativePath);
752
+ }
753
+ else {
754
+ const snapshot = await readWorkspaceTextSnapshot(filePath);
755
+ if (typeof snapshot === 'string') {
756
+ currentContent = snapshot;
757
+ entry.fileSnapshots.set(relativePath, snapshot);
758
+ }
759
+ }
760
+ }
761
+ const nextEvent = {
762
+ eventType,
763
+ filePath,
764
+ changedFile: relativePath,
765
+ oldContent: typeof previousContent === 'string'
766
+ ? previousContent
767
+ : eventType === 'add'
768
+ ? ''
769
+ : undefined,
770
+ currentContent,
771
+ };
772
+ entry.pendingEvent = entry.pendingEvent?.changedFile === relativePath
773
+ ? {
774
+ ...nextEvent,
775
+ oldContent: entry.pendingEvent.oldContent ?? nextEvent.oldContent,
776
+ }
777
+ : nextEvent;
657
778
  if (entry.debounceTimer) {
658
779
  clearTimeout(entry.debounceTimer);
659
780
  }
@@ -661,11 +782,21 @@ async function subscribeToWorkspace(ws, projectName) {
661
782
  entry.debounceTimer = null;
662
783
  const pending = entry.pendingEvent || {};
663
784
  entry.pendingEvent = null;
785
+ const hasSnapshotDiff = (typeof pending.oldContent === 'string'
786
+ && typeof pending.currentContent === 'string'
787
+ && pending.oldContent !== pending.currentContent);
664
788
  const message = JSON.stringify({
665
789
  type: 'project_files_updated',
666
790
  projectName,
667
791
  changeType: pending.eventType || 'change',
668
- changedFile: pending.filePath ? path.relative(rootPath, pending.filePath) : null,
792
+ changedFile: pending.changedFile ?? (pending.filePath ? path.relative(rootPath, pending.filePath) : null),
793
+ ...(hasSnapshotDiff
794
+ ? {
795
+ oldContent: pending.oldContent,
796
+ currentContent: pending.currentContent,
797
+ diffSource: 'workspace-snapshot',
798
+ }
799
+ : {}),
669
800
  timestamp: new Date().toISOString(),
670
801
  });
671
802
  entry.subscribers.forEach((client) => {
@@ -689,11 +820,11 @@ async function subscribeToWorkspace(ws, projectName) {
689
820
  }
690
821
  });
691
822
  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))
823
+ .on('add', (filePath) => void broadcastFileUpdate('add', filePath))
824
+ .on('change', (filePath) => void broadcastFileUpdate('change', filePath))
825
+ .on('unlink', (filePath) => void broadcastFileUpdate('unlink', filePath))
826
+ .on('addDir', (dirPath) => void broadcastFileUpdate('addDir', dirPath))
827
+ .on('unlinkDir', (dirPath) => void broadcastFileUpdate('unlinkDir', dirPath))
697
828
  .on('error', (error) => {
698
829
  console.error(`[ERROR] Workspace watcher error for ${projectName}:`, error);
699
830
  });
@@ -3443,13 +3574,6 @@ function handleShellConnection(ws, request) {
3443
3574
  session.ws = null;
3444
3575
  return;
3445
3576
  }
3446
- // Plain shells are cheap to recreate, so terminate them immediately
3447
- // instead of keeping a 30-minute zombie PTY around.
3448
- if (session.isPlainShell) {
3449
- console.log('🧹 Terminating plain shell PTY on disconnect:', ptySessionKey);
3450
- terminatePtySession(ptySessionKey, session, 'plain shell disconnect');
3451
- return;
3452
- }
3453
3577
  console.log('⏳ PTY session kept alive, will timeout in 30 minutes:', ptySessionKey);
3454
3578
  session.ws = null;
3455
3579
  session.timeoutId = setTimeout(() => {