Arboretum découvre désormais toutes les sessions Claude de la machine (scan ~/.claude/projects + registre ~/.claude/sessions), distingue vivantes/mortes par pid+procStart, et permet de reprendre une morte (--resume dans son cwd d'origine) ou forker une vivante sans la corrompre. - shared: SessionSummary enrichi (source, claudeSessionId, pid, resumable, attachable, registryStatus) — additif, PROTOCOL_VERSION inchangé ; types REST resume/fork. - db: migration id:2 (claude_session_id, resumed_from). - core: jsonl-discovery (parseur tolérant, scan asynchrone non bloquant), session-registry (vivacité pid+procStart), discovery-service (cache + refresh périodique + diff/broadcast), pty-manager (resume/fork + capture du claudeSessionId via le registre). - routes: /sessions/:id/resume (garde-fou 409 anti-corruption sur session vivante) et /fork ; GET fusionné managées + découvertes ; relais WS. - web: badges managed/discovered + busy/idle/waiting, actions conditionnelles (Open/Observe/Kill vs Fork/View vs Resume/Fork), vue read-only des sessions externes, i18n EN/FR. - tests: jsonl-discovery, session-registry, discovery-service + resume/fork (130 verts) ; acceptation E2E acceptance-p2.mjs (sans quota) ALL GREEN. Conforme aux verdicts S1 (resume dans cwd d'origine, vivacité pid+procStart) et S4 (munge cwd, parseur tête+queue, priorité de titre). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
102 lines
3.2 KiB
TypeScript
102 lines
3.2 KiB
TypeScript
import { defineStore } from 'pinia';
|
|
import { ref } from 'vue';
|
|
import type { CreateSessionRequest, SessionResponse, SessionSummary, SessionsListResponse } from '@arboretum/shared';
|
|
import { api } from '../lib/api';
|
|
import { wsClient, type SessionEvent } from '../lib/ws-client';
|
|
|
|
export const useSessionsStore = defineStore('sessions', () => {
|
|
const sessions = ref<SessionSummary[]>([]);
|
|
const loading = ref(false);
|
|
const loadError = ref<string | null>(null);
|
|
let unsubscribe: (() => void) | null = null;
|
|
|
|
// vivantes d'abord, puis par date de création décroissante (même ordre que le serveur)
|
|
function sorted(list: SessionSummary[]): SessionSummary[] {
|
|
return [...list].sort((a, b) => Number(b.live) - Number(a.live) || b.createdAt.localeCompare(a.createdAt));
|
|
}
|
|
|
|
function upsert(session: SessionSummary): void {
|
|
const idx = sessions.value.findIndex((s) => s.id === session.id);
|
|
if (idx >= 0) sessions.value.splice(idx, 1, session);
|
|
else sessions.value.push(session);
|
|
sessions.value = sorted(sessions.value);
|
|
}
|
|
|
|
function onEvent(e: SessionEvent): void {
|
|
if (e.type === 'session_update') {
|
|
upsert(e.session);
|
|
return;
|
|
}
|
|
const current = sessions.value.find((s) => s.id === e.sessionId);
|
|
if (!current) return;
|
|
upsert({
|
|
...current,
|
|
status: 'exited',
|
|
live: false,
|
|
exitCode: e.exitCode,
|
|
endedAt: new Date().toISOString(),
|
|
clients: 0,
|
|
});
|
|
}
|
|
|
|
async function fetchSessions(): Promise<void> {
|
|
loading.value = true;
|
|
loadError.value = null;
|
|
try {
|
|
const res = await api.get<SessionsListResponse>('/api/v1/sessions');
|
|
sessions.value = sorted(res.sessions);
|
|
} catch (err) {
|
|
loadError.value = err instanceof Error ? err.message : String(err);
|
|
} finally {
|
|
loading.value = false;
|
|
}
|
|
}
|
|
|
|
function startRealtime(): void {
|
|
unsubscribe ??= wsClient.subscribeSessions(onEvent);
|
|
}
|
|
|
|
function stopRealtime(): void {
|
|
unsubscribe?.();
|
|
unsubscribe = null;
|
|
}
|
|
|
|
async function createSession(cwd: string, command: NonNullable<CreateSessionRequest['command']>): Promise<SessionSummary> {
|
|
const body: CreateSessionRequest = { cwd, command };
|
|
const res = await api.post<SessionResponse>('/api/v1/sessions', body);
|
|
upsert(res.session);
|
|
return res.session;
|
|
}
|
|
|
|
async function killSession(id: string): Promise<void> {
|
|
await api.delete<{ ok: true }>(`/api/v1/sessions/${id}`);
|
|
}
|
|
|
|
// Reprise d'une session morte (nouveau PTY managé dans son cwd d'origine, côté serveur).
|
|
async function resumeSession(id: string): Promise<SessionSummary> {
|
|
const res = await api.post<SessionResponse>(`/api/v1/sessions/${id}/resume`);
|
|
upsert(res.session);
|
|
return res.session;
|
|
}
|
|
|
|
// Fork d'une session (vivante ou morte) — ne corrompt jamais l'originale.
|
|
async function forkSession(id: string): Promise<SessionSummary> {
|
|
const res = await api.post<SessionResponse>(`/api/v1/sessions/${id}/fork`);
|
|
upsert(res.session);
|
|
return res.session;
|
|
}
|
|
|
|
return {
|
|
sessions,
|
|
loading,
|
|
loadError,
|
|
fetchSessions,
|
|
startRealtime,
|
|
stopRealtime,
|
|
createSession,
|
|
killSession,
|
|
resumeSession,
|
|
forkSession,
|
|
};
|
|
});
|