fix: recherche worktrees, sessions externes masquées, rendu/historique terminal & plein écran grille
- Recherche : le watcher débouncé du singleton useWorktreeView est créé dans un effectScope détaché → ne gèle plus après une navigation ; recherche aussi par label de dépôt ; accès à la palette depuis le MoreSheet mobile (sidebar/⌘K indisponibles sur mobile). - Sessions : sessionsForCwd exclut désormais toujours les sessions masquées ; les sessions externes (discovered) sont masquées par défaut côté UI, avec puce de révélation par carte et case globale dans le dashboard. - Terminal : ACK découplé du callback de rendu (armé à la réception + flush au retour de visibilité) → plus de pause PTY quand l'onglet passe en arrière-plan ; repaint forcé sur perte de contexte WebGL / resize / retour en avant-plan. Historique élargi : REPLAY_TAIL_BYTES 1 Mo, RING_CAPACITY 4 Mo, scrollback xterm 20k. - Grille de groupe : bouton « plein écran » sur chaque cellule ouvrant /sessions/:id.
This commit is contained in:
@@ -12,7 +12,9 @@ import { SessionActivityTracker } from './claude-adapter.js';
|
||||
import type { PushService } from './push-service.js';
|
||||
import type { Db } from '../db/index.js';
|
||||
|
||||
const RING_CAPACITY = 2 * 1024 * 1024;
|
||||
// 4 Mo : conserve assez d'historique pour que le replay (REPLAY_TAIL_BYTES = 1 Mo) reste largement
|
||||
// dans le ring et qu'on puisse remonter une conversation Claude après ré-attache.
|
||||
const RING_CAPACITY = 4 * 1024 * 1024;
|
||||
const KILL_GRACE_MS = 5000;
|
||||
/** Délai avant envoi d'une notif push sur passage en `waiting` : annulé si la session repart (faux positif). */
|
||||
const NOTIFY_DEBOUNCE_MS = 1500;
|
||||
|
||||
@@ -250,10 +250,18 @@ export class WorktreeManager extends EventEmitter<WorktreeManagerEvents> {
|
||||
|
||||
// ---- worktrees ----
|
||||
|
||||
/** Sessions (managées + découvertes) dont le cwd correspond à ce chemin de worktree. */
|
||||
private sessionsForCwd(path: string): SessionSummary[] {
|
||||
/**
|
||||
* Sessions (managées + découvertes) dont le cwd correspond à ce chemin de worktree.
|
||||
* Les sessions explicitement masquées (`hidden`) sont exclues — cohérent avec `/api/v1/sessions`
|
||||
* (sans quoi le masquage était ignoré dans les fiches worktree). Le tri managées/externes est laissé
|
||||
* au client (interrupteur « afficher les externes »), qui dispose du champ `source`. La garde de
|
||||
* suppression réclame en revanche TOUTES les sessions vivantes (`includeHidden`) pour rester sûre.
|
||||
*/
|
||||
private sessionsForCwd(path: string, opts?: { includeHidden?: boolean }): SessionSummary[] {
|
||||
const rp = resolve(path);
|
||||
return mergeSessions(this.ptyManager.list(), this.discovery.list()).filter((s) => resolve(s.cwd) === rp);
|
||||
return mergeSessions(this.ptyManager.list(), this.discovery.list())
|
||||
.filter((s) => resolve(s.cwd) === rp)
|
||||
.filter((s) => opts?.includeHidden || !s.hidden);
|
||||
}
|
||||
|
||||
private toSummary(repoId: string, repoPath: string, w: ParsedWorktree, status: WorktreeGitStatus): WorktreeSummary {
|
||||
@@ -507,7 +515,7 @@ export class WorktreeManager extends EventEmitter<WorktreeManagerEvents> {
|
||||
if (!w) throw httpError(404, 'NOT_FOUND', 'No such worktree under this repo');
|
||||
if (resolve(w.path) === resolve(row.path)) throw httpError(400, 'IS_MAIN_WORKTREE', 'Cannot remove the main worktree');
|
||||
// garde-fou : une session vivante tourne dans ce worktree → exiger une confirmation explicite.
|
||||
if (!force && this.sessionsForCwd(w.path).some((s) => s.live)) {
|
||||
if (!force && this.sessionsForCwd(w.path, { includeHidden: true }).some((s) => s.live)) {
|
||||
throw httpError(409, 'SESSION_LIVE_IN_WORKTREE', 'A live session runs in this worktree — pass force to delete anyway');
|
||||
}
|
||||
return this.withLock(repoId, async () => {
|
||||
|
||||
@@ -243,7 +243,11 @@ describe('PtyManager (pty mocké)', () => {
|
||||
|
||||
it('resync à l’attach = queue du ring (REPLAY_TAIL_BYTES max)', () => {
|
||||
const { summary, pty } = spawnBash();
|
||||
const chunks = ['A', 'B', 'C'].map((c) => c.repeat(100 * 1024));
|
||||
// on écrit volontairement PLUS que REPLAY_TAIL_BYTES (mais < RING_CAPACITY) pour vérifier
|
||||
// le plafonnement de la queue rejouée. Tailles dérivées de la constante → robuste aux bumps.
|
||||
const chunkSize = 256 * 1024;
|
||||
const chunkCount = Math.ceil(REPLAY_TAIL_BYTES / chunkSize) + 2;
|
||||
const chunks = Array.from({ length: chunkCount }, (_, i) => String.fromCharCode(65 + (i % 26)).repeat(chunkSize));
|
||||
for (const c of chunks) pty.emitData(c);
|
||||
|
||||
const b = makeBinding('interactive');
|
||||
|
||||
Reference in New Issue
Block a user