feat(vscode): statut git détaillé, sessions archivées, lien IDE web, fetch/pull (0.2.0)

Reflète la refonte IDE en supervision légère, sans dupliquer l'éditeur :
- statut git fin dans l'arbre (staged/unstaged/conflits + dernier commit) ;
- commande « Open Worktree IDE » → deep-link /workspace/:repoId/:wt ;
- commandes fetch / pull (ff-only|rebase) sur les worktrees ;
- event WS session_archived + réglage showArchivedSessions (badge + filtre).
README/CHANGELOG + tests (config/rest-client/ws-client).
This commit is contained in:
2026-06-27 16:04:28 +02:00
parent 8a8fac75e6
commit d4e3ab47cd
13 changed files with 198 additions and 10 deletions

View File

@@ -1,5 +1,5 @@
import { describe, expect, it } from 'vitest';
import { normalizeBaseUrl, wsUrlFromBase } from '../src/config.js';
import { normalizeBaseUrl, workspaceUrl, wsUrlFromBase } from '../src/config.js';
describe('config', () => {
it('normalise la base URL (slash final, défaut)', () => {
@@ -12,4 +12,11 @@ describe('config', () => {
expect(wsUrlFromBase('http://127.0.0.1:7317')).toBe('ws://127.0.0.1:7317/ws');
expect(wsUrlFromBase('https://box.ts.net')).toBe('wss://box.ts.net/ws');
});
it('construit lURL de la vue IDE web (workspaceUrl, encodage par segment)', () => {
expect(workspaceUrl('http://127.0.0.1:7317', 'repo id', '/home/user/wt')).toBe(
'http://127.0.0.1:7317/workspace/repo%20id/%2Fhome%2Fuser%2Fwt',
);
expect(workspaceUrl('https://box.ts.net/', 'r', '/a/b')).toBe('https://box.ts.net/workspace/r/%2Fa%2Fb');
});
});

View File

@@ -48,4 +48,31 @@ describe('RestClient', () => {
expect(captured[0]?.init.method).toBe('POST');
expect(JSON.parse(String(captured[0]?.init.body))).toEqual({ path: '/w', message: 'msg' });
});
it('fetch/pull worktree : chemins et corps', async () => {
const captured: Captured[] = [];
const rest = new RestClient(
{ baseUrl: 'http://h:1', token: 't' },
fakeFetch({ ok: true, status: 200, body: { worktree: {} } }, captured),
);
await rest.fetchWorktree('r', { path: '/w' });
await rest.pullWorktree('r', { path: '/w', mode: 'rebase' });
expect(captured[0]?.url).toBe('http://h:1/api/v1/repos/r/worktrees/fetch');
expect(captured[1]?.url).toBe('http://h:1/api/v1/repos/r/worktrees/pull');
expect(JSON.parse(String(captured[1]?.init.body))).toEqual({ path: '/w', mode: 'rebase' });
});
it('listSessions construit la query includeHidden/includeArchived', async () => {
const captured: Captured[] = [];
const rest = new RestClient(
{ baseUrl: 'http://h:1', token: 't' },
fakeFetch({ ok: true, status: 200, body: { sessions: [] } }, captured),
);
await rest.listSessions();
await rest.listSessions({ includeArchived: true });
await rest.listSessions({ includeHidden: true, includeArchived: true });
expect(captured[0]?.url).toBe('http://h:1/api/v1/sessions');
expect(captured[1]?.url).toBe('http://h:1/api/v1/sessions?includeArchived=true');
expect(captured[2]?.url).toBe('http://h:1/api/v1/sessions?includeHidden=true&includeArchived=true');
});
});

View File

@@ -124,6 +124,18 @@ describe('ArbWsClient', () => {
expect(acks[acks.length - 1]).toMatchObject({ bytes: FLOW.ACK_EVERY_BYTES });
});
it('route session_archived vers onSessionArchived', () => {
const archived: string[] = [];
const f = new FakeSocket();
const c = new ArbWsClient({ socketFactory: () => f, onSessionArchived: (id) => archived.push(id) });
c.configure('http://127.0.0.1:7317', 'tok');
c.connect();
f.fireOpen();
f.fireText({ type: 'hello_ok', protocol: 1, serverVersion: '2.0.0' });
f.fireText({ type: 'session_archived', sessionId: 'sess-42' });
expect(archived).toEqual(['sess-42']);
});
it('envoie stdin/resize sur le bon canal', async () => {
const p = client.attach({ sessionId: 'sess', mode: 'interactive', cols: 80, rows: 24, sink: makeNoopSink() });
fake.fireText({ type: 'attached', channel: 5, sessionId: 'sess', mode: 'interactive', controlling: true });