feat(sessions): masquer les sessions externes + terminal plein écran
Masquage persistant des sessions Claude découvertes (lancées hors Arboretum) qui polluent la liste — calqué sur repos.hidden : - table hidden_sessions (#9) clé claudeSessionId ; helpers DB - DiscoveryService pose le champ additif SessionSummary.hidden (resume/fork restent possibles, survit à un re-scan) - routes POST/DELETE /sessions/:id/hide, POST /sessions/hide-discovered (masse, un clic), GET /sessions?includeHidden=true - UI : bouton Masquer par ligne, « Masquer l'historique externe », toggle « Afficher les masquées », badge masquée ; store + i18n EN/FR + audit Terminal plein écran (corrige le sizing) : - SessionView root s'étire (flex-1) → xterm calcule les bonnes dimensions - refit robuste (requestAnimationFrame + après resync), h-full sur le wrapper - bouton plein écran (API Fullscreen), tabbar mobile masquée en fullbleed DialogPrompt : options en liste verticale pleine largeur (plus de troncature ni de chevauchement sur les libellés longs / multi-lignes). Doc : centre d'aide (sessions), README EN/FR, site (showcase), CLAUDE.md. Tests : discovery (hidden) + route hide/unhide/hide-discovered.
This commit is contained in:
@@ -148,6 +148,20 @@ describe('app e2e — auth, origin et sessions', () => {
|
||||
expect(res.json()).toEqual({ sessions: [] });
|
||||
});
|
||||
|
||||
it('masquage des sessions : hide-discovered (vide), includeHidden, hide d’un id inconnu → 404', async () => {
|
||||
const headers = { authorization: `Bearer ${t.token}` };
|
||||
const mass = await t.bundle.app.inject({ method: 'POST', url: '/api/v1/sessions/hide-discovered', headers });
|
||||
expect(mass.statusCode).toBe(200);
|
||||
expect(mass.json()).toEqual({ hidden: 0 });
|
||||
|
||||
const withHidden = await t.bundle.app.inject({ method: 'GET', url: '/api/v1/sessions?includeHidden=true', headers });
|
||||
expect(withHidden.statusCode).toBe(200);
|
||||
expect(withHidden.json()).toEqual({ sessions: [] });
|
||||
|
||||
const unknown = await t.bundle.app.inject({ method: 'POST', url: '/api/v1/sessions/nope/hide', headers });
|
||||
expect(unknown.statusCode).toBe(404);
|
||||
});
|
||||
|
||||
it('Origin interdite → 403 même avec un Bearer valide, et même sur la route publique de login', async () => {
|
||||
const res = await t.bundle.app.inject({
|
||||
method: 'GET',
|
||||
|
||||
@@ -7,7 +7,7 @@ import { DiscoveryService, mergeSessions } from '../src/core/discovery-service.j
|
||||
import { munge } from '../src/core/jsonl-discovery.js';
|
||||
import { readProcStart } from '../src/core/session-registry.js';
|
||||
import { PtyManager } from '../src/core/pty-manager.js';
|
||||
import { openDb, type Db } from '../src/db/index.js';
|
||||
import { openDb, hideSession, unhideSession, listHiddenSessionIds, type Db } from '../src/db/index.js';
|
||||
|
||||
function writeJsonl(projectsDir: string, cwd: string, sid: string): void {
|
||||
const dir = join(projectsDir, munge(cwd));
|
||||
@@ -30,7 +30,7 @@ describe('DiscoveryService', () => {
|
||||
sessionsDir = mkdtempSync(join(tmpdir(), 'arb-sess-'));
|
||||
db = openDb(':memory:');
|
||||
manager = new PtyManager(db, sessionsDir);
|
||||
svc = new DiscoveryService({ ptyManager: manager, projectsDir, sessionsDir });
|
||||
svc = new DiscoveryService({ db, ptyManager: manager, projectsDir, sessionsDir });
|
||||
});
|
||||
afterEach(() => {
|
||||
svc.stop();
|
||||
@@ -71,6 +71,29 @@ describe('DiscoveryService', () => {
|
||||
expect(svc.list().find((x) => x.id === 'managed-sid')).toBeUndefined();
|
||||
});
|
||||
|
||||
it('marque hidden une session masquée mais la garde résoluble (resume/fork)', async () => {
|
||||
writeJsonl(projectsDir, '/home/u/old', 'old-sid');
|
||||
hideSession(db, 'old-sid');
|
||||
await svc.refresh();
|
||||
const s = svc.list().find((x) => x.id === 'old-sid');
|
||||
expect(s?.hidden).toBe(true);
|
||||
// toujours connue : la reprise/le fork doivent rester possibles
|
||||
expect(svc.getDiscovered('old-sid')?.cwd).toBe('/home/u/old');
|
||||
// ré-affichage
|
||||
unhideSession(db, 'old-sid');
|
||||
await svc.refresh();
|
||||
expect(svc.list().find((x) => x.id === 'old-sid')?.hidden).toBe(false);
|
||||
});
|
||||
|
||||
it('hideSession est idempotent et listHiddenSessionIds reflète l’état', () => {
|
||||
hideSession(db, 'a');
|
||||
hideSession(db, 'a');
|
||||
hideSession(db, 'b');
|
||||
expect(listHiddenSessionIds(db)).toEqual(new Set(['a', 'b']));
|
||||
unhideSession(db, 'a');
|
||||
expect(listHiddenSessionIds(db)).toEqual(new Set(['b']));
|
||||
});
|
||||
|
||||
it('émet discovery_update sur changement uniquement', async () => {
|
||||
writeJsonl(projectsDir, '/home/u/x', 'sid-x');
|
||||
const seen: SessionSummary[] = [];
|
||||
|
||||
@@ -69,7 +69,7 @@ describe('WorktreeManager', () => {
|
||||
claudeHome = mkdtempSync(join(tmpdir(), 'arb-ch-'));
|
||||
dirs.push(claudeHome);
|
||||
pty = new PtyManager(db, join(claudeHome, 'sessions'));
|
||||
discovery = new DiscoveryService({ ptyManager: pty, projectsDir: join(claudeHome, 'projects'), sessionsDir: join(claudeHome, 'sessions') });
|
||||
discovery = new DiscoveryService({ db, ptyManager: pty, projectsDir: join(claudeHome, 'projects'), sessionsDir: join(claudeHome, 'sessions') });
|
||||
wt = new WorktreeManager(db, pty, discovery);
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user