# การบันทึกในตัวแก้ไขระยะไกลและการตรวจสอบเวลาแฝง ## สาเหตุที่แท้จริง ตัวแก้ไขเดิมอัปโหลดไฟล์ทำงานไปยังไดเรกทอรีหลักของไฟล์เป้าหมาย ซึ่งบัญชี SSH มักไม่มีสิทธิ์เขียนภายใต้ `/etc` หรือ `/var` การเปลี่ยนชื่อผ่าน SFTP ไม่มีเส้นทาง sudo ที่ได้รับอนุญาต การตรวจสอบข้อขัดแย้งเทียบกับสถานะตั้งต้น การตรวจสอบเมทาดาทาด้านความปลอดภัย หรือความหมายของการเขียนทับที่เชื่อถือได้ ตัวจัดการข้อผิดพลาดของตัวแก้ไขเดิมลบสำเนาชั่วคราว ทำให้ข้อมูลสำหรับกู้คืนหายไปแม้ในกรณีที่ไม่แน่ใจว่าการบันทึกเสร็จสมบูรณ์หรือไม่ สถานะเซสชันก่อนหน้านี้แสดงเพียงสถานะการเชื่อมต่อ ไม่มีการวัด ICMP หรือการตอบกลับ SSH ที่ยืนยันตัวตนแล้วซึ่งส่งข้อมูลให้ตัวบ่งชี้ของเทอร์มินัล ## โปรโตคอลการบันทึก 1. Helper SSH แบบคงที่จะเปิดไฟล์ปกติที่มีลิงก์เดียวโดยใช้การดำเนินการแบบอ้างอิงตาม descriptor และ `O_NOFOLLOW` สำหรับทุกส่วนของพาธ Helper จะเก็บเนื้อหา แฮช SHA-256 อุปกรณ์/inode, UID, GID บิตสิทธิ์ทั้งหมด เวลาแก้ไข/เปลี่ยนแปลงระดับนาโนวินาที และแอตทริบิวต์ขยายทั้งหมดที่อ่านได้ Core จะตรวจสอบแฮชและถอดรหัส UTF-8 อย่างเข้มงวด โดยคง BOM ไว้ เอกสารแต่ละฉบับจะได้รับ `editId` แบบทึบ ซึ่งจำกัดขอบเขตไว้ที่ปลั๊กอิน เซสชันเทอร์มินัล และตัวตนของเซิร์ฟเวอร์ 2. การดำเนินการเตรียมการที่ไม่มีสิทธิ์พิเศษจะหาโฮมของบัญชีที่ยืนยันตัวตนแล้วผ่าน `pwd.getpwuid(os.getuid())` สร้าง `$HOME/ch-j-sm/` ด้วยโหมด `0700` และสร้างบันทึกการกู้คืน (journal) ส่วนตัวแบบเอกสิทธิ์ โฮมที่ไม่ปลอดภัย พื้นที่ทำงานที่เป็นลิงก์สัญลักษณ์ หรือพื้นที่ทำงานที่เป็นของบัญชีอื่นจะถูกปฏิเสธ 3. Core อัปโหลดไฟล์ทำงาน `.tmp` ที่ซ่อนอยู่และมีชื่อสุ่ม 128 บิตผ่าน SFTP ด้วยการสร้างแบบเอกสิทธิ์ (`wx`) โหมด `0600` และการหมดเวลาของช่องสัญญาณ/การอัปโหลดหกสิบวินาที Journal มีไฟล์เป้าหมาย สถานะตั้งต้นดั้งเดิม และแฮชที่คาดไว้ของไฟล์ทำงาน โดยไม่มีข้อมูลรับรอง 4. **การเรียกใช้ SSH ครั้งเดียวจะทำขั้นตอนสรุปผลทั้งหมด** โดยอาจผ่านตัวดำเนินการ sudo ที่มีอยู่ Helper จะเปิดและคำนวณแฮชของสำเนาทำงานส่วนตัวอย่างปลอดภัย แล้วเขียนลงดิสก์ (flush) จากนั้นล็อก inode ดั้งเดิม ตรวจสอบสถานะตั้งต้น สร้างไฟล์ปลายทางแบบเอกสิทธิ์ข้างไฟล์เป้าหมาย เขียนเนื้อหาทั้งหมด ใช้และตรวจสอบเมทาดาทา ตรวจสอบไฟล์ดั้งเดิมอีกครั้ง และเขียนไฟล์ทดแทนลงดิสก์ 5. `os.replace` ในไดเรกทอรีเดียวกันจะแทนที่ไฟล์เป้าหมายแบบอะตอมมิก ไดเรกทอรีหลักจะถูกเขียนลงดิสก์ แล้วจึงตรวจสอบพาธสุดท้าย แฮช inode และเมทาดาทา พาธดั้งเดิมยังคงใช้ได้จนกว่าจะแทนที่ ไฟล์ทำงานจะไม่ถูกย้ายข้ามระบบไฟล์ และจะไม่ถูกลบโดยขั้นตอนสรุปผล 6. หลังจาก Core ได้รับผลสำเร็จที่ตรวจสอบแล้ว ขั้นตอนล้างข้อมูลที่แยกต่างหากจะตรวจสอบเนื้อหาสุดท้ายและเมทาดาทาด้านความปลอดภัยอีกครั้งก่อนลบไฟล์ทำงานและ journal หากการล้างข้อมูลล้มเหลว ยังคงรายงานว่าการบันทึกได้รับการยืนยันพร้อม `recoveryAvailable: true` การอัปโหลดที่ล้มเหลว sudo ที่ล้มเหลว ข้อขัดแย้ง ข้อผิดพลาดของเมทาดาทา หรือการแทนที่ที่ล้มเหลว จะคงไฟล์ดั้งเดิมและข้อมูลที่เตรียมไว้ทั้งหมด การอัปโหลดที่ไม่สมบูรณ์จะถูกระบุเช่นนั้นและไม่สามารถกู้คืนเป็นเอกสารที่แก้ไขฉบับสมบูรณ์ได้ การสื่อสาร SSH ที่ขาดหาย สถานะการออกที่หายไป การหมดเวลา และผลลัพธ์การสรุปผลที่ไม่คาดคิดจะถูกรายงานเป็น **ไม่ทราบ** ไม่ใช่บันทึกแล้ว Core จะบล็อกการบันทึกครั้งต่อไปสำหรับเอกสารที่ผลลัพธ์ไม่แน่นอน และไม่มีการลองแทนที่ซ้ำโดยอัตโนมัติ ## เมทาดาทา สิทธิ์พิเศษ และขอบเขตด้านความปลอดภัย Helper จะคงไว้และตรวจสอบ UID, GID และบิตสิทธิ์ POSIX ทั้งหมด รวมถึงบิต executable, setuid, setgid และ sticky และจะจำลองแอตทริบิวต์ขยายของ Linux ทีละไบต์ รวมถึง POSIX ACL, capabilities และป้ายกำกับ SELinux ความเป็นเจ้าของและเนื้อหาไฟล์จะถูกตั้งค่าก่อนแอตทริบิวต์ด้านความปลอดภัย เนื่องจากการเขียน/chown อาจล้าง capabilities หรือบิตสิทธิ์พิเศษ แอตทริบิวต์ที่สืบทอดมาที่ปลายทางแต่ไม่มีในไฟล์ดั้งเดิมจะถูกลบ เวลาแก้ไขสะท้อนการเขียนครั้งใหม่และไม่ได้คัดลอกมาจากไฟล์ดั้งเดิม แอตทริบิวต์ที่อ่านไม่ได้หรือแสดงแทนไม่ได้จะทำให้การดำเนินการถูกยกเลิก เมทาดาทาจะไม่ถูกทิ้งไปอย่างเงียบ ๆ ลายเซ็น IMA/EVM จะถูกปฏิเสธอย่างชัดแจ้ง เพราะการคัดลอกลายเซ็นความสมบูรณ์ที่ผูกกับเนื้อหา/inode จะทำให้ลายเซ็นใช้ไม่ได้ ลิงก์สัญลักษณ์ในพาธเป้าหมายหรือโฮม ไฟล์ที่มีฮาร์ดลิงก์ และไฟล์ที่ไม่ใช่ไฟล์ปกติจะถูกปฏิเสธ ไม่มีการถอยกลับไปใช้การเขียนทับในที่เดิมหรือการเขียนทับ SFTP ที่ปลอดภัยน้อยกว่า การบันทึกปกติที่มีสิทธิ์เขียนจะทำงานในนามบัญชี SSH การบันทึกที่ได้รับการป้องกันต้องระบุ `{ sudo: true }` อย่างชัดแจ้ง รหัสผ่าน sudo ไม่บังคับสำหรับ sudo แบบไม่ใช้รหัสผ่าน รหัสผ่านจะได้รับการตรวจสอบและส่งผ่าน stdin ของช่องสัญญาณเท่านั้น ไม่เคยอยู่ในคำสั่ง journal ไฟล์ทำงาน ตัวบันทึก (logger) หรือข้อผิดพลาดระยะไกล เซสชัน SSH ของ root ใช้ตัวดำเนินการ root โดยตรงที่มีอยู่ การอนุญาตที่ล้มเหลวจะคงไฟล์ดั้งเดิมและสำเนาทำงานไว้ครบถ้วน การบันทึกทั้งหมดที่ทำงานในนาม root (SSH ของ root โดยตรงหรือ sudo) จะปฏิเสธไดเรกทอรีเป้าหมาย/ไดเรกทอรีบรรพบุรุษที่เป็นของหรือเขียนได้โดยบัญชีอื่น หรือมี ACL การเข้าถึงด้วย แทนที่จะเชื่อถือชื่อพาธที่มีสิทธิ์พิเศษซึ่งบัญชีอื่นควบคุมได้ Helper แบบคงที่ทำงานด้วย `python3 -I -B` ซึ่งจะไม่สนใจพาธ Python ของผู้ใช้ ไดเรกทอรีทำงาน และ site packages ของผู้ใช้ และไม่เขียน bytecode พาธและอาร์กิวเมนต์ JSON จะถูกใส่เครื่องหมายคำพูดสำหรับเชลล์ใน Core ปลั๊กอินไม่เคยได้รับการเรียกใช้ SSH แบบอิสระหรือ handle ของ SFTP แซนด์บ็อกซ์ CSP สิทธิ์ในแมนิเฟสต์ การตรวจสอบคีย์โฮสต์ และการตรวจสอบการอัปเดตที่มีอยู่ยังคงใช้งานอยู่ ## Plugin API และการผสานรวม Monaco ที่เหลืออยู่ **UI ของ Monaco/File Manager ที่เผยแพร่แยกต่างหากไม่มีอยู่ใน checkout นี้ การผสานรวม UI ข้อความแจ้ง sudo การจัดการบัฟเฟอร์ที่ยังไม่บันทึก และคำแปลของ UI ดังกล่าวจึงไม่สามารถนำไปใช้หรือทดสอบได้ที่นี่** ซอร์สปลั๊กอินที่รวมมาคือปลั๊กอิน Hash & Checksum ไม่มีการสร้างซอร์ส File Manager ทดแทนขึ้นมาเอง เมธอดที่มีอยู่ยังคงใช้ได้ เมธอด/ตัวเลือกเพิ่มเติมเปิดให้ใช้ผ่าน preload ในแซนด์บ็อกซ์และรันไทม์ที่ตรวจสอบสิทธิ์: ```js const opened = await chjPlugin.files.readText(sessionId, path); // For a protected read, explicitly pass { sudo: true, sudoPassword }. const result = await chjPlugin.files.saveText(sessionId, path, editor.getValue(), { editId: opened.editId, sudo: true, // Include only after the user authorizes elevation. sudoPassword // Omit for passwordless sudo; never persist this value. }); if (result.ok && result.value.status === "saved") { // Mark this exact submitted editor version as saved. Later edits stay dirty. } else { // Keep the Monaco model dirty. Translate result.error.code/status. // result.error.recoveryId/recoveryPath identify retained working material. } await chjPlugin.files.closeText(opened.editId); const recovery = await chjPlugin.files.listRecovery(sessionId); const copy = await chjPlugin.files.readRecovery(sessionId, recoveryId); // Review copy.status and copy.text; open a fresh document before another save. await chjPlugin.files.cleanupRecovery(sessionId, recoveryId); // Cleanup rejects anything whose target is not currently confirmed. ``` เมธอดการกู้คืนรับอาร์กิวเมนต์สุดท้ายแบบไม่บังคับ `{ sudo: true, sudoPassword }` เมื่อการตรวจสอบไฟล์เป้าหมายต้องใช้สิทธิ์ที่สูงขึ้น เมธอดเหล่านี้ยังคงใช้พื้นที่ทำงานของผู้ใช้ที่ยืนยันตัวตนแล้วแม้จะทำงานผ่าน sudo Journal ยังคงอยู่หลังการรีสตาร์ตแอปและการเชื่อมต่อ SSH ใหม่ การกู้คืนจะไม่ทำขั้นตอนสรุปผลซ้ำโดยอัตโนมัติ ให้ตรวจทานบัฟเฟอร์ที่กู้คืนเทียบกับไฟล์เป้าหมายที่เพิ่งเปิดใหม่ก่อนบันทึกอย่างชัดแจ้ง `files.read` ควบคุมการอ่าน การตรวจสอบ และการปล่อย handle ส่วน `files.write` ควบคุมการบันทึกและการล้างข้อมูลการกู้คืน `writeText` ยังคงปฏิเสธ (reject) เมื่อล้มเหลวสำหรับปลั๊กอินที่มีอยู่ ส่วน `saveText` ส่งคืนซองข้อมูลที่มีโครงสร้าง `{ ok, value/error }` เนื่องจาก Electron ทิ้งพร็อพเพอร์ตี้ที่กำหนดเองบนอ็อบเจ็กต์ Error ที่ถูกปฏิเสธ `writeText` แบบเดิมที่มีสามอาร์กิวเมนต์สามารถใช้สถานะตั้งต้นของเอกสารที่เปิดอยู่เพียงฉบับเดียวได้ เอกสารที่เปิดอยู่หลายฉบับสำหรับพาธเดียวกันต้องใช้ `editId` การเรียกแบบเดิมที่กำกวมจะล้มเหลวอย่างปลอดภัยแทนที่จะเลือกสถานะตั้งต้นของเอกสารอื่น ปลั๊กอินควรปล่อย handle ที่ไม่ได้ใช้ UI ภายนอกต้องแสดงสถานะการบันทึกทันที คงบัฟเฟอร์ไว้เป็นสถานะยังไม่บันทึกเมื่อล้มเหลว เสนอ sudo หลังจากความล้มเหลวด้านสิทธิ์/การยืนยันตัวตนเท่านั้น แสดงข้อขัดแย้งโดยไม่เขียนทับ และเสนอการกู้คืนหลังการเชื่อมต่อขาดหายหรือผลลัพธ์ที่ไม่ทราบ UI ต้องแปลสถานะเหล่านี้เป็นภาษาเช็ก เยอรมัน และอังกฤษ ข้อความสถานะที่แนะนำ: | สถานะ | ภาษาเช็ก | ภาษาเยอรมัน | ภาษาอังกฤษ | | --- | --- | --- | --- | | กำลังบันทึก | Ukládání… | Wird gespeichert… | Saving… | | ยืนยันแล้ว | Uloženo | Gespeichert | Saved | | ล้มเหลว | Uložení selhalo | Speichern fehlgeschlagen | Save failed | | ต้องใช้ sudo | Vyžadováno oprávnění sudo | sudo-Autorisierung erforderlich | Sudo authorization required | | ข้อขัดแย้ง | Původní soubor byl změněn | Originaldatei wurde geändert | Original file changed | | ขาดการเชื่อมต่อ | Spojení bylo přerušeno | Verbindung unterbrochen | Connection lost | | ไม่ทราบ | Výsledek uložení není znám | Speicherergebnis unbekannt | Save result unknown | | การกู้คืน | Obnova je dostupná | Wiederherstellung verfügbar | Recovery available | ## นิยามของเวลาแฝงและวงจรชีวิต **Ping** คือเวลาแฝงที่รายงานโดยการตอบกลับ ICMP echo จริงฝั่งไคลเอนต์ไปยัง IP ที่แปลงชื่อแล้วซึ่ง SSH ใช้ Core เรียกไฟล์ปฏิบัติการ ping ของแพลตฟอร์มแบบอะซิงโครนัสด้วยอาร์เรย์อาร์กิวเมนต์คงที่ โดยไม่ใช้เชลล์และไม่ต้องเริ่มด้วยสิทธิ์ผู้ดูแลระบบ อาร์กิวเมนต์สำหรับ Linux, macOS (รวมถึง `ping6`) และ Windows ถูกเลือกแยกกัน ICMP ที่ถูกบล็อก ไฟล์ปฏิบัติการที่ไม่มี การหมดเวลา เอาต์พุตที่ไม่ถูกต้อง หรือการตอบกลับของ Windows ที่มีเพียงขอบเขตบน เช่น `<1ms` จะให้ค่าที่ไม่พร้อมใช้งาน ไม่ใช่ตัวเลขที่แต่งขึ้น **SSH RTT** คือระยะเวลาแบบ monotonic ของ `performance.now()` สำหรับคำขอ exec `true` แบบคงที่ผ่านไคลเอนต์ SSH ที่ยืนยันตัวตนแล้วซึ่งมีอยู่ ค่านี้รวมการตั้งค่าช่องสัญญาณ การประมวลผลของเซิร์ฟเวอร์ และการส่งการตอบกลับ ไม่ใช่ RTT ของเครือข่ายล้วน ๆ หรือระยะเวลาเข้าสู่ระบบ SSH คำอธิบายเครื่องมือ (tooltip) ที่แปลแล้วของตัวบ่งชี้จะอธิบายความแตกต่างนี้ การตรวจสอบจะเริ่มทันทีที่เทอร์มินัลเชื่อมต่อ การตรวจวัดทำงานพร้อมกันแบบอะซิงโครนัส โดยมีการหมดเวลาของคำขอ/กระบวนการห้าวินาที ตัวอย่างถัดไปจะถูกกำหนดเวลาสิบสองวินาทีหลังจากตัวอย่างก่อนหน้าเสร็จสิ้น เพื่อป้องกันการทับซ้อน การตัดการเชื่อมต่อจะยกเลิก ICMP ล้างตัวจับเวลา ยกเลิกงาน SSH ที่ค้างอยู่ และล้างค่า การตรวจสอบตัวตนของระเบียนจะป้องกันไม่ให้การเชื่อมต่อก่อนหน้าเผยแพร่ค่าหลังจากเชื่อมต่อใหม่ ความล้มเหลวของ ICMP ไม่เคยเปลี่ยนสถานะการเชื่อมต่อ SSH เวลาแฝงถูกส่งภายในเพย์โหลด IPC ของเหตุการณ์ `state` / `ssh:state` ที่มีอยู่: ```js latency: { pingMs: 24.1, sshRttMs: 31.25, measuredAt: "…" } ``` ตัวเลขที่ไม่พร้อมใช้งานมีค่าเป็น `null` ตัวบ่งชี้เทอร์มินัลแบบกะทัดรัดใช้หน่วยมิลลิวินาที ข้อความไม่พร้อมใช้งานที่แปลแล้ว และป้ายกำกับภาษาเช็ก/เยอรมัน/อังกฤษ และจะซ่อนเมื่อตัดการเชื่อมต่อ ไม่จำเป็นต้องมีความสามารถในการเรียกใช้ของ renderer หรือคำขอ IPC เพิ่มเติม ## การตรวจสอบและข้อจำกัดในการดำเนินงาน - เรียกใช้จาก `app/`: `npm ci` แล้วตามด้วย `npm test` - ผลลัพธ์สุดท้ายในเครื่อง: **179 การทดสอบ ผ่าน 163 ล้มเหลว 0 ข้าม 16** รายการที่ข้ามทั้งหมดเป็นกรณีระบบไฟล์ของ Linux บนเครื่อง macOS นี้ `npm ci` การคอมไพล์ไวยากรณ์ Python และ `git diff --check` เสร็จสมบูรณ์โดยไม่มีข้อผิดพลาด - การทดสอบครอบคลุมการบันทึก/การกู้คืนของ Core โปรโตคอล sudo และการปกปิดรหัสผ่าน การใส่เครื่องหมายคำพูดเชลล์ POSIX จริงสำหรับพาธที่เป็นอันตราย ข้อขัดแย้ง การอัปโหลด การล้างข้อมูลที่ล้มเหลว ผลลัพธ์ที่ไม่แน่นอน เอกสารพร้อมกันหลายฉบับ สิทธิ์ IPC และวงจรชีวิตของเวลาแฝง - การทดสอบระบบไฟล์ของ Linux เรียกใช้จุดเข้า Python ที่จัดส่งจริง โดยใช้การค้นหาโฮมของบัญชีทดสอบส่วนตัว ได้แก่ ความเป็นเจ้าของ/โหมด/xattrs, POSIX ACL, ลิงก์สัญลักษณ์/ฮาร์ดลิงก์, FIFO, ความล้มเหลวของการอัปโหลดและเมทาดาทา ความล้มเหลวของการแทนที่ การแข่งขันบนไฟล์เป้าหมาย ผลลัพธ์ที่ไม่ทราบหลังการแทนที่ และการกู้คืน การเตรียมข้ามระบบไฟล์ใช้ `/dev/shm` และระบบไฟล์อื่นเมื่อมี การทดสอบเหล่านี้จะถูกข้ามบนเครื่องที่ไม่ใช่ Linux ส่วนสิ่งอำนวยความสะดวกด้าน ACL/ระบบไฟล์แยกที่ไม่มีจะถูกข้ามอย่างชัดแจ้ง - งาน GitHub Actions สำหรับ Linux ที่เพิ่มเข้ามาจะเรียกใช้ชุดทดสอบทั้งหมดและทำกรณีระบบไฟล์ซ้ำในนาม root เวิร์กโฟลว์นี้ยังไม่ได้ถูกเรียกใช้จาก working tree นี้ - การตรวจสอบในเครื่องทำบน macOS ไวยากรณ์ของซอร์ส Python ได้รับการตรวจสอบด้วยไฟล์ปฏิบัติการ Python ของ Command Line Tools โดยตรง การทดสอบเบื้องต้น ICMP loopback จริงบน macOS ได้ค่า `0.071 ms` ไม่มีเซิร์ฟเวอร์ SSH ระยะไกลที่ทำงานจริง รันไทม์ Windows รันไทม์ Linux การใช้งาน SELinux หรือ UI ของ Monaco ภายนอกที่ได้รับการทดสอบในเครื่อง - ขณะนี้การแก้ไขข้อความระยะไกลต้องใช้ Linux และ Python 3 ที่รองรับระบบไฟล์/xattr แบบอ้างอิงตาม descriptor หากไม่มีตัวแปลภาษา จะล้มเหลวอย่างชัดแจ้ง Core นี้ตั้งใจปฏิเสธเมทาดาทา ลิงก์ และการกำหนดค่าไดเรกทอรีที่ไม่ปลอดภัย - การล็อกเป็นแบบให้คำแนะนำ (advisory) กระบวนการที่ไม่ให้ความร่วมมือยังคงแข่งขันได้ในช่วงเวลาสั้น ๆ ระหว่างการตรวจสอบสถานะตั้งต้นครั้งสุดท้ายกับการเปลี่ยนชื่อแบบอะตอมมิก Linux ไม่มีการดำเนินการทั่วไปแบบอะตอมมิกที่เปรียบเทียบแฮชของเนื้อหาแล้วแทนที่ การตรวจสอบจะตรวจพบการเปลี่ยนแปลงก่อนช่วงเวลานั้น และสำเนาทำงานที่เก็บไว้จะช่วยการกู้คืนในภายหลัง - ข้อมูลการกู้คืนจะยังอยู่ในบัญชีระยะไกลจนกว่าจะมีการล้างข้อมูลที่ได้รับการยืนยัน Journal ที่ไม่สมบูรณ์ มีข้อขัดแย้ง หรือตรวจสอบไม่ได้จะไม่ถูกทิ้งไปอย่างเงียบ ๆ ไม่มีการหมดอายุอัตโนมัติของไฟล์กู้คืนและไม่มีตัวเลือกเขียนทับแบบบังคับ - `npm ci` รายงานผลการตรวจสอบการพึ่งพา 17 รายการ (ปานกลาง 9 รายการ สูง 8 รายการ) การเปลี่ยนแปลงนี้ไม่ได้เปลี่ยนการพึ่งพาหรือโมเดลความเชื่อถือในการอัปเดตของแอปพลิเคชัน ผู้ใช้ขอให้เก็บการเปลี่ยนแปลงไว้ใน working tree ที่มีอยู่ ซึ่งขณะนี้คือ `feature/hash-checksum-plugin` ไม่มีการสลับแบรนช์หรือการล้างข้อมูลที่ไม่เกี่ยวข้อง ## ไฟล์ที่เปลี่ยนแปลงสำหรับงานนี้ - `src/main/files/remoteFileService.js` - `src/main/files/remoteEditor.py` (ใหม่) - `src/main/sessions/sessionManager.js` - `src/main/sessions/latencyMonitor.js` (ใหม่) - `src/main/plugins/pluginRuntime.js` - `src/preload/pluginPreload.js` - `src/renderer/app.js` - `src/renderer/index.html` - `src/renderer/styles.css` - `src/renderer/i18n.js` - `test/remoteFileService.test.js` - `test/sessionManager.test.js` - `test/pluginRuntime.test.js` - `test/remoteEditor.test.js` (ใหม่) - `test/remoteEditorProtocol.test.js` (ใหม่) - `test/remoteEditorLinux.test.js` (ใหม่) - `test/fixtures/remoteEditorLinux.py` (ใหม่) - `test/latencyMonitor.test.js` (ใหม่) - `README.md` - `docs/remote-editor-latency.md` (ไฟล์นี้) - `../.github/workflows/remote-editor-tests.yml` (ใหม่)