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:
@@ -1,13 +1,24 @@
|
||||
import type { FastifyInstance } from 'fastify';
|
||||
import type { CreateSessionRequest, SessionResponse, SessionsListResponse } from '@arboretum/shared';
|
||||
import type { CreateSessionRequest, HideDiscoveredResponse, SessionResponse, SessionsListResponse } from '@arboretum/shared';
|
||||
import type { PtyManager } from '../core/pty-manager.js';
|
||||
import { mergeSessions, type DiscoveryService } from '../core/discovery-service.js';
|
||||
import { hideSession, unhideSession, type Db } from '../db/index.js';
|
||||
import { recordAudit } from '../core/audit-log.js';
|
||||
|
||||
export function registerSessionRoutes(app: FastifyInstance, manager: PtyManager, discovery: DiscoveryService): void {
|
||||
app.get('/api/v1/sessions', async (): Promise<SessionsListResponse> => {
|
||||
return { sessions: mergeSessions(manager.list(), discovery.list()) };
|
||||
export function registerSessionRoutes(app: FastifyInstance, manager: PtyManager, discovery: DiscoveryService, db: Db): void {
|
||||
app.get('/api/v1/sessions', async (req): Promise<SessionsListResponse> => {
|
||||
const includeHidden = (req.query as { includeHidden?: string }).includeHidden === 'true';
|
||||
const all = mergeSessions(manager.list(), discovery.list());
|
||||
return { sessions: includeHidden ? all : all.filter((s) => !s.hidden) };
|
||||
});
|
||||
|
||||
// Résout le claudeSessionId d'un :id de session (managée par UUID, ou découverte dont l'id EST le sid).
|
||||
const resolveClaudeSid = (id: string): string | null => {
|
||||
const managed = manager.resumeTargetById(id);
|
||||
if (managed?.claudeSessionId) return managed.claudeSessionId;
|
||||
return discovery.getDiscovered(id) ? id : null;
|
||||
};
|
||||
|
||||
app.post('/api/v1/sessions', async (req, reply) => {
|
||||
const body = req.body as Partial<CreateSessionRequest> | null;
|
||||
if (!body || typeof body.cwd !== 'string' || !body.cwd.startsWith('/')) {
|
||||
@@ -81,6 +92,49 @@ export function registerSessionRoutes(app: FastifyInstance, manager: PtyManager,
|
||||
}
|
||||
});
|
||||
|
||||
// Masque une session découverte (ou managée morte) : exclue de la liste sauf ?includeHidden=true.
|
||||
// Reste reprenable/forkable — c'est un filtre d'affichage, pas une suppression.
|
||||
app.post('/api/v1/sessions/:id/hide', async (req, reply) => {
|
||||
const { id } = req.params as { id: string };
|
||||
const sid = resolveClaudeSid(id);
|
||||
if (!sid) {
|
||||
return reply.status(404).send({ error: { code: 'NOT_FOUND', message: 'No session with this id to hide' } });
|
||||
}
|
||||
hideSession(db, sid);
|
||||
await discovery.refresh(); // rediffuse l'état (hidden) aux clients
|
||||
recordAudit(db, { actor: req.authContext?.tokenId ?? 'unknown', action: 'session.hide', resourceId: sid });
|
||||
return reply.send({ ok: true });
|
||||
});
|
||||
|
||||
// Ré-affiche une session masquée.
|
||||
app.delete('/api/v1/sessions/:id/hide', async (req, reply) => {
|
||||
const { id } = req.params as { id: string };
|
||||
const sid = resolveClaudeSid(id);
|
||||
if (!sid) {
|
||||
return reply.status(404).send({ error: { code: 'NOT_FOUND', message: 'No session with this id to unhide' } });
|
||||
}
|
||||
unhideSession(db, sid);
|
||||
await discovery.refresh();
|
||||
recordAudit(db, { actor: req.authContext?.tokenId ?? 'unknown', action: 'session.unhide', resourceId: sid });
|
||||
return reply.send({ ok: true });
|
||||
});
|
||||
|
||||
// Masquage de masse : nettoie d'un coup tout l'historique externe actuellement visible. Les futures
|
||||
// sessions externes réapparaîtront (sinon --no-discover). Renvoie le nombre de sessions masquées.
|
||||
app.post('/api/v1/sessions/hide-discovered', async (req, reply) => {
|
||||
const sids = discovery
|
||||
.list()
|
||||
.filter((s) => !s.hidden && s.claudeSessionId)
|
||||
.map((s) => s.claudeSessionId as string);
|
||||
for (const sid of sids) hideSession(db, sid);
|
||||
await discovery.refresh();
|
||||
if (sids.length > 0) {
|
||||
recordAudit(db, { actor: req.authContext?.tokenId ?? 'unknown', action: 'session.hideDiscovered', resourceId: null, details: { count: sids.length } });
|
||||
}
|
||||
const res: HideDiscoveredResponse = { hidden: sids.length };
|
||||
return reply.send(res);
|
||||
});
|
||||
|
||||
app.delete('/api/v1/sessions/:id', async (req, reply) => {
|
||||
const { id } = req.params as { id: string };
|
||||
if (!manager.kill(id)) {
|
||||
|
||||
Reference in New Issue
Block a user