fix(desktop, web): pont presse-papier Electron, sans quoi copier ne faisait rien dans l'app
CI / No em/en dashes (push) Successful in 3s
CI / Build & test (Node 24) (push) Successful in 10m48s
Release / Publish to Gitea npm registry (push) Successful in 10m52s
CI / Build & test (Node 22) (push) Successful in 11m6s
Desktop Release / Build Linux (AppImage + deb) (push) Successful in 13m38s
Desktop Release / Publish floating desktop-latest release (push) Successful in 11s
CI / Pack & boot smoke (Node 22) (push) Successful in 9m56s
Desktop Release / Build Windows (NSIS + portable) (push) Canceled after 0s

git-arboretum 3.5.1, desktop 0.2.2.

Le copier/coller livre en 3.5.0 ne fonctionnait que dans un navigateur : verifie dans l'app packagee,
`navigator.clipboard` y rejette en NotAllowedError A LA LECTURE COMME A L'ECRITURE. La copie echouait
donc silencieusement dans le contexte meme ou le probleme avait ete signale.

- feat(desktop): `arboretumDesktop.clipboard` expose par le preload, relaye en IPC vers le module
  `clipboard` d'Electron (lecture + ecriture, ecriture bornee a 1 M de caracteres). Le module
  `clipboard` n'etant pas accessible a un preload sandboxe, le passage par le main est obligatoire.
  Constantes de canaux isolees dans src/shared/ipc.ts pour que le bundle du preload ne tire pas
  `ipcMain` avec lui.
- fix(web): acces au presse-papier en cascade, du plus fiable au plus degrade : pont de l'app de
  bureau, puis `navigator.clipboard`, puis `document.execCommand('copy')` pour l'ecriture. Ce dernier
  recours couvre aussi l'acces LAN en clair (http://<ip>:7317), ou l'API Clipboard est indisponible.
- test: quatre cas de plus sur l'ordre de la cascade et le retour a null quand tout est refuse
  (501 tests). Pont IPC verifie dans un vrai Electron : writeText/readText repondent OK, alors que
  `navigator.clipboard` y refuse. Le chemin complet frappe -> copie -> collage -> SIGINT reste
  couvert par verify-clipboard.mjs (Chromium CDP, ALL GREEN).
This commit is contained in:
2026-08-04 21:11:46 +02:00
parent bde5358ea8
commit 114fbc8ba0
13 changed files with 197 additions and 23 deletions
+23
View File
@@ -0,0 +1,23 @@
import { clipboard, ipcMain } from 'electron';
import { CLIPBOARD_READ, CLIPBOARD_WRITE } from '../shared/ipc';
// Pont presse-papier pour le renderer.
//
// Pourquoi il est nécessaire : dans Electron, `navigator.clipboard.writeText` ET `readText`
// échouent en `NotAllowedError` (vérifié dans l'app packagée). La SPA ne pouvait donc PAS copier
// la sélection d'un terminal, alors que la même page y arrive dans un navigateur. Le module
// `clipboard` n'étant pas exposé aux preloads sandboxés, on passe par IPC.
//
// Portée : l'app charge exclusivement sa propre SPA locale servie par son daemon, et un terminal
// web est déjà de l'exécution de code par conception : le presse-papier n'élargit pas la surface.
// On borne quand même la taille écrite pour qu'une boucle accidentelle ne remplisse pas la mémoire.
const MAX_WRITE_CHARS = 1_000_000;
export function registerClipboardBridge(): void {
ipcMain.handle(CLIPBOARD_READ, () => clipboard.readText());
ipcMain.handle(CLIPBOARD_WRITE, (_event, text: unknown) => {
if (typeof text !== 'string' || text.length === 0) return false;
clipboard.writeText(text.slice(0, MAX_WRITE_CHARS));
return true;
});
}
+2
View File
@@ -5,6 +5,7 @@ import { seedSessionCookie } from './auth';
import { loadWindowState, saveWindowState } from './window-state';
import { createTray } from './tray';
import { installAppMenu } from './app-menu';
import { registerClipboardBridge } from './clipboard';
import { initUpdater } from './updater';
import { resolveIconPath } from './paths';
@@ -39,6 +40,7 @@ async function bootstrap(): Promise<void> {
const dataDir = join(app.getPath('userData'), 'daemon');
daemon = await startDaemon({ dataDir, port: PORT, onLog: (l) => process.stdout.write(l) });
await seedSessionCookie(PARTITION, daemon.url, daemon.token);
registerClipboardBridge();
createWindow(daemon.url);
installAppMenu({ url: daemon.url, onQuit: quitApp });
tray = createTray({ show: showWindow, quit: quitApp });
+12 -3
View File
@@ -1,7 +1,16 @@
import { contextBridge } from 'electron';
import { contextBridge, ipcRenderer } from 'electron';
import { CLIPBOARD_READ, CLIPBOARD_WRITE } from '../shared/ipc';
// Preload minimal (sandbox activé) : expose seulement un marqueur permettant à la SPA de détecter
// qu'elle tourne dans l'app de bureau. Aucun accès Node/fs exposé au renderer.
// Preload minimal (sandbox activé) : un marqueur permettant à la SPA de détecter qu'elle tourne dans
// l'app de bureau, plus un pont presse-papier. Aucun accès Node/fs exposé au renderer.
//
// Le pont existe parce que `navigator.clipboard` est refusé (NotAllowedError) dans le renderer
// Electron : sans lui, impossible de copier la sélection d'un terminal depuis l'app. Le module
// `clipboard` n'étant pas disponible dans un preload sandboxé, on relaie par IPC vers le main.
contextBridge.exposeInMainWorld('arboretumDesktop', {
isDesktop: true,
clipboard: {
readText: (): Promise<string> => ipcRenderer.invoke(CLIPBOARD_READ) as Promise<string>,
writeText: (text: string): Promise<boolean> => ipcRenderer.invoke(CLIPBOARD_WRITE, text) as Promise<boolean>,
},
});
+4
View File
@@ -0,0 +1,4 @@
// Noms des canaux IPC, partagés entre le process principal et le preload. Isolés ici pour que le
// bundle du preload n'ait pas à importer un module du main (qui tire `ipcMain` avec lui).
export const CLIPBOARD_READ = 'arboretum:clipboard-read';
export const CLIPBOARD_WRITE = 'arboretum:clipboard-write';