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:
@@ -1,5 +1,17 @@
|
||||
# Changelog
|
||||
|
||||
## 0.2.0
|
||||
|
||||
Follows the daemon's "worktree IDE" milestone (P7→P12), while keeping the extension a lightweight
|
||||
**visual worktree manager** — no editor/diff duplicated here, that lives in the web IDE:
|
||||
|
||||
- **Detailed git status** in the worktree tree: staged / unstaged / conflict counts and the last commit
|
||||
subject (from the daemon's enriched `WorktreeGitStatus`).
|
||||
- **Open Worktree IDE** command — deep-links to the web `/workspace/:repoId/:wt` view.
|
||||
- **Fetch** and **Pull** (fast-forward only or rebase) commands on worktrees, alongside commit/push.
|
||||
- **Archived sessions**: the `session_archived` event is handled live; finished, auto-archived sessions
|
||||
are badged and hidden by default, with a new `arboretum.showArchivedSessions` setting to reveal them.
|
||||
|
||||
## 0.1.0
|
||||
|
||||
Initial release — native VS Code integration for the Arboretum daemon:
|
||||
|
||||
@@ -9,7 +9,11 @@ integration on top of the [Arboretum](https://git.lidge.fr/johanleroy/arboretum)
|
||||
daemon's PTY) — you get VS Code's own rendering, scrollback, copy/paste and links for free.
|
||||
- **Waiting alerts**: a status-bar counter and native notifications when a Claude session is waiting for
|
||||
input — answer **Yes/No** without even opening the terminal (uses the daemon's `answer` command).
|
||||
- **Git mutations** from the tree: create worktree, commit, push, promote to main.
|
||||
- **Git mutations** from the tree: create worktree, commit, push, fetch, pull, promote to main — with a
|
||||
**detailed git status** on each worktree (staged / unstaged / conflicts and the last commit subject).
|
||||
- **Open Worktree IDE**: jump from any worktree to its full `/workspace` view (file tree, Monaco editor,
|
||||
inline diffs, integrated terminal) in the browser. The extension stays a lightweight visual worktree
|
||||
manager — the heavy editing lives in the web IDE.
|
||||
- **Workspace-aware**: the worktree matching your open folder is highlighted; start a session or create a
|
||||
worktree for the current folder in one command.
|
||||
|
||||
@@ -33,6 +37,7 @@ integration on top of the [Arboretum](https://git.lidge.fr/johanleroy/arboretum)
|
||||
| --- | --- | --- |
|
||||
| `arboretum.url` | `http://127.0.0.1:7317` | Base URL of the daemon (REST + WebSocket). |
|
||||
| `arboretum.showExternalSessions` | `false` | Also show Claude sessions started outside Arboretum (CLI). |
|
||||
| `arboretum.showArchivedSessions` | `false` | Also show finished sessions auto-archived after their retention window. |
|
||||
| `arboretum.notifyOnWaiting` | `true` | Native notification when a session starts waiting for input. |
|
||||
|
||||
The extension authenticates with `Authorization: Bearer <token>` on both REST and the WebSocket upgrade.
|
||||
@@ -48,13 +53,13 @@ The extension is bundled with esbuild (`@arboretum/shared` is inlined → the VS
|
||||
npm install
|
||||
npm run build:vscode # builds @arboretum/shared then bundles the extension
|
||||
cd packages/vscode && npx @vscode/vsce package --no-dependencies
|
||||
# → git-arboretum-0.1.0.vsix
|
||||
# → git-arboretum-0.2.0.vsix
|
||||
```
|
||||
|
||||
Install it with **Extensions: Install from VSIX…** in the Command Palette, or:
|
||||
|
||||
```bash
|
||||
code --install-extension git-arboretum-0.1.0.vsix # also: codium / cursor
|
||||
code --install-extension git-arboretum-0.2.0.vsix # also: codium / cursor
|
||||
```
|
||||
|
||||
### Other distribution channels (optional)
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"name": "git-arboretum",
|
||||
"displayName": "Arboretum",
|
||||
"description": "Pilot your git worktrees and Claude Code sessions from VS Code — native terminals, live tree, waiting alerts.",
|
||||
"version": "0.1.1",
|
||||
"version": "0.2.0",
|
||||
"private": true,
|
||||
"publisher": "johanleroy",
|
||||
"license": "MIT",
|
||||
@@ -42,6 +42,11 @@
|
||||
"default": false,
|
||||
"markdownDescription": "Show Claude sessions discovered outside Arboretum (started from the CLI) in the trees. Off by default to avoid clutter."
|
||||
},
|
||||
"arboretum.showArchivedSessions": {
|
||||
"type": "boolean",
|
||||
"default": false,
|
||||
"markdownDescription": "Show finished sessions that Arboretum auto-archived after their retention window. Off by default to keep the trees focused on live work."
|
||||
},
|
||||
"arboretum.notifyOnWaiting": {
|
||||
"type": "boolean",
|
||||
"default": true,
|
||||
@@ -94,7 +99,10 @@
|
||||
{ "command": "arboretum.createWorktree", "title": "Create Worktree…", "category": "Arboretum", "icon": "$(add)" },
|
||||
{ "command": "arboretum.commitWorktree", "title": "Commit…", "category": "Arboretum", "icon": "$(git-commit)" },
|
||||
{ "command": "arboretum.pushWorktree", "title": "Push", "category": "Arboretum", "icon": "$(repo-push)" },
|
||||
{ "command": "arboretum.fetchWorktree", "title": "Fetch", "category": "Arboretum", "icon": "$(cloud-download)" },
|
||||
{ "command": "arboretum.pullWorktree", "title": "Pull…", "category": "Arboretum", "icon": "$(arrow-down)" },
|
||||
{ "command": "arboretum.promoteWorktree", "title": "Promote to Main", "category": "Arboretum", "icon": "$(arrow-up)" },
|
||||
{ "command": "arboretum.openWorktreeIde", "title": "Open Worktree IDE", "category": "Arboretum", "icon": "$(link-external)" },
|
||||
{ "command": "arboretum.startSession", "title": "Start Claude Session", "category": "Arboretum", "icon": "$(play)" },
|
||||
{ "command": "arboretum.startGroupSession", "title": "Start Group Session…", "category": "Arboretum", "icon": "$(play)" },
|
||||
{ "command": "arboretum.startSessionHere", "title": "Start Session in Current Folder", "category": "Arboretum" },
|
||||
@@ -111,9 +119,13 @@
|
||||
"view/item/context": [
|
||||
{ "command": "arboretum.createWorktree", "when": "viewItem == arboretum:repo", "group": "inline" },
|
||||
{ "command": "arboretum.startSession", "when": "viewItem == arboretum:repo", "group": "1_session" },
|
||||
{ "command": "arboretum.openWorktreeIde", "when": "viewItem == arboretum:worktree", "group": "inline" },
|
||||
{ "command": "arboretum.commitWorktree", "when": "viewItem == arboretum:worktree", "group": "1_git" },
|
||||
{ "command": "arboretum.pushWorktree", "when": "viewItem == arboretum:worktree", "group": "1_git" },
|
||||
{ "command": "arboretum.fetchWorktree", "when": "viewItem == arboretum:worktree", "group": "1_git" },
|
||||
{ "command": "arboretum.pullWorktree", "when": "viewItem == arboretum:worktree", "group": "1_git" },
|
||||
{ "command": "arboretum.promoteWorktree", "when": "viewItem == arboretum:worktree", "group": "2_git" },
|
||||
{ "command": "arboretum.openWorktreeIde", "when": "viewItem == arboretum:worktree", "group": "2_git" },
|
||||
{ "command": "arboretum.startSession", "when": "viewItem == arboretum:worktree", "group": "3_session" },
|
||||
{ "command": "arboretum.attachSession", "when": "viewItem =~ /arboretum:session:live/", "group": "inline" },
|
||||
{ "command": "arboretum.observeSession", "when": "viewItem =~ /arboretum:session:live/", "group": "1_term" },
|
||||
@@ -137,7 +149,10 @@
|
||||
{ "command": "arboretum.createWorktree", "when": "false" },
|
||||
{ "command": "arboretum.commitWorktree", "when": "false" },
|
||||
{ "command": "arboretum.pushWorktree", "when": "false" },
|
||||
{ "command": "arboretum.fetchWorktree", "when": "false" },
|
||||
{ "command": "arboretum.pullWorktree", "when": "false" },
|
||||
{ "command": "arboretum.promoteWorktree", "when": "false" },
|
||||
{ "command": "arboretum.openWorktreeIde", "when": "false" },
|
||||
{ "command": "arboretum.startSession", "when": "false" },
|
||||
{ "command": "arboretum.startGroupSession", "when": "false" },
|
||||
{ "command": "arboretum.revealWorktree", "when": "false" }
|
||||
|
||||
@@ -9,10 +9,12 @@ import type {
|
||||
CreateSessionRequest,
|
||||
CreateWorktreeRequest,
|
||||
CreateWorktreeResponse,
|
||||
FetchWorktreeRequest,
|
||||
GroupSessionResponse,
|
||||
GroupsListResponse,
|
||||
MeResponse,
|
||||
PromoteWorktreeRequest,
|
||||
PullWorktreeRequest,
|
||||
PushWorktreeRequest,
|
||||
RepoBranchesResponse,
|
||||
ReposListResponse,
|
||||
@@ -93,6 +95,14 @@ export class RestClient {
|
||||
return this.request<WorktreeResponse>('POST', `/api/v1/repos/${encodeURIComponent(repoId)}/worktrees/push`, body);
|
||||
}
|
||||
|
||||
fetchWorktree(repoId: string, body: FetchWorktreeRequest): Promise<WorktreeResponse> {
|
||||
return this.request<WorktreeResponse>('POST', `/api/v1/repos/${encodeURIComponent(repoId)}/worktrees/fetch`, body);
|
||||
}
|
||||
|
||||
pullWorktree(repoId: string, body: PullWorktreeRequest): Promise<WorktreeResponse> {
|
||||
return this.request<WorktreeResponse>('POST', `/api/v1/repos/${encodeURIComponent(repoId)}/worktrees/pull`, body);
|
||||
}
|
||||
|
||||
promoteWorktree(repoId: string, body: PromoteWorktreeRequest): Promise<WorktreeResponse> {
|
||||
return this.request<WorktreeResponse>('POST', `/api/v1/repos/${encodeURIComponent(repoId)}/worktrees/promote`, body);
|
||||
}
|
||||
@@ -106,9 +116,12 @@ export class RestClient {
|
||||
return this.request<SessionResponse>('POST', '/api/v1/sessions', body);
|
||||
}
|
||||
|
||||
listSessions(includeHidden = false): Promise<SessionsListResponse> {
|
||||
const q = includeHidden ? '?includeHidden=true' : '';
|
||||
return this.request<SessionsListResponse>('GET', `/api/v1/sessions${q}`);
|
||||
listSessions(opts: { includeHidden?: boolean; includeArchived?: boolean } = {}): Promise<SessionsListResponse> {
|
||||
const params = new URLSearchParams();
|
||||
if (opts.includeHidden) params.set('includeHidden', 'true');
|
||||
if (opts.includeArchived) params.set('includeArchived', 'true');
|
||||
const q = params.toString();
|
||||
return this.request<SessionsListResponse>('GET', `/api/v1/sessions${q ? `?${q}` : ''}`);
|
||||
}
|
||||
|
||||
resumeSession(id: string): Promise<SessionResponse> {
|
||||
|
||||
@@ -115,6 +115,8 @@ export interface ArbWsClientOptions {
|
||||
onSession?: (e: SessionEvent) => void;
|
||||
onWorktree?: (e: WorktreeEvent) => void;
|
||||
onGroup?: (e: GroupEvent) => void;
|
||||
/** P10 : une session managée terminée vient d'être auto-archivée. */
|
||||
onSessionArchived?: (sessionId: string) => void;
|
||||
/** appelé au premier hello_ok après (re)connexion → re-seed du store via REST. */
|
||||
onReady?: () => void;
|
||||
log?: (msg: string) => void;
|
||||
@@ -387,6 +389,9 @@ export class ArbWsClient {
|
||||
case 'session_exit':
|
||||
this.opts.onSession?.(msg);
|
||||
return;
|
||||
case 'session_archived':
|
||||
this.opts.onSessionArchived?.(msg.sessionId);
|
||||
return;
|
||||
case 'repo_update':
|
||||
case 'repo_removed':
|
||||
case 'worktree_update':
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
import * as vscode from 'vscode';
|
||||
import type { GroupSummary, RepoSummary, SessionSummary, WorktreeSummary } from '@arboretum/shared';
|
||||
import { RestError, type RestClient } from './api/rest-client.js';
|
||||
import { workspaceUrl } from './config.js';
|
||||
import type { ArbWsClient, AttachmentSink } from './api/ws-client.js';
|
||||
import type { Store } from './state/store.js';
|
||||
import type { SessionTerminalManager } from './terminal/session-pty.js';
|
||||
@@ -174,10 +175,43 @@ export function registerCommands(context: vscode.ExtensionContext, deps: Command
|
||||
}),
|
||||
);
|
||||
});
|
||||
cmd('arboretum.fetchWorktree', (node) => {
|
||||
const wt = asWorktree(node as AnyNode);
|
||||
if (!wt) return;
|
||||
void vscode.window.withProgress({ location: vscode.ProgressLocation.Notification, title: 'Arboretum: fetching…' }, () =>
|
||||
run('fetch', async () => {
|
||||
await rest.fetchWorktree(wt.repoId, { path: wt.worktree.path });
|
||||
void vscode.window.showInformationMessage('Arboretum: fetched.');
|
||||
}),
|
||||
);
|
||||
});
|
||||
cmd('arboretum.pullWorktree', (node) => {
|
||||
const wt = asWorktree(node as AnyNode);
|
||||
if (!wt) return;
|
||||
void run('pull', async () => {
|
||||
const mode = await vscode.window.showQuickPick(
|
||||
[
|
||||
{ label: 'Fast-forward only', mode: 'ff-only' as const },
|
||||
{ label: 'Rebase', mode: 'rebase' as const },
|
||||
],
|
||||
{ placeHolder: 'Pull strategy' },
|
||||
);
|
||||
if (!mode) return;
|
||||
await vscode.window.withProgress({ location: vscode.ProgressLocation.Notification, title: 'Arboretum: pulling…' }, async () => {
|
||||
await rest.pullWorktree(wt.repoId, { path: wt.worktree.path, mode: mode.mode });
|
||||
void vscode.window.showInformationMessage('Arboretum: pulled.');
|
||||
});
|
||||
});
|
||||
});
|
||||
cmd('arboretum.promoteWorktree', (node) => {
|
||||
const wt = asWorktree(node as AnyNode);
|
||||
if (wt) void promoteWorktreeFlow(deps, wt.repoId, wt.worktree);
|
||||
});
|
||||
cmd('arboretum.openWorktreeIde', (node) => {
|
||||
const wt = asWorktree(node as AnyNode);
|
||||
if (!wt) return;
|
||||
void vscode.env.openExternal(vscode.Uri.parse(workspaceUrl(rest.url, wt.repoId, wt.worktree.path)));
|
||||
});
|
||||
|
||||
// ---- démarrage de session ----
|
||||
cmd('arboretum.startSession', (node) => {
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
export interface ConfigSnapshot {
|
||||
url: string;
|
||||
showExternalSessions: boolean;
|
||||
showArchivedSessions: boolean;
|
||||
notifyOnWaiting: boolean;
|
||||
}
|
||||
|
||||
@@ -19,3 +20,12 @@ export function wsUrlFromBase(baseUrl: string): string {
|
||||
const base = normalizeBaseUrl(baseUrl);
|
||||
return `${base.replace(/^http/, 'ws')}/ws`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Construit l'URL de la vue IDE web d'un worktree — deep-link vers la route `/workspace/:repoId/:wt`
|
||||
* (le routeur web est en history mode). `repoId` et le chemin du worktree sont encodés par segment.
|
||||
*/
|
||||
export function workspaceUrl(baseUrl: string, repoId: string, worktreePath: string): string {
|
||||
const base = normalizeBaseUrl(baseUrl);
|
||||
return `${base}/workspace/${encodeURIComponent(repoId)}/${encodeURIComponent(worktreePath)}`;
|
||||
}
|
||||
|
||||
@@ -21,6 +21,9 @@ function readUrl(): string {
|
||||
function readShowExternal(): boolean {
|
||||
return vscode.workspace.getConfiguration('arboretum').get<boolean>('showExternalSessions', false);
|
||||
}
|
||||
function readShowArchived(): boolean {
|
||||
return vscode.workspace.getConfiguration('arboretum').get<boolean>('showArchivedSessions', false);
|
||||
}
|
||||
function readNotify(): boolean {
|
||||
return vscode.workspace.getConfiguration('arboretum').get<boolean>('notifyOnWaiting', true);
|
||||
}
|
||||
@@ -36,6 +39,7 @@ export async function activate(context: vscode.ExtensionContext): Promise<void>
|
||||
const auth = new Auth(context.secrets);
|
||||
const store = new Store();
|
||||
store.showExternalSessions = readShowExternal();
|
||||
store.showArchivedSessions = readShowArchived();
|
||||
const rest = new RestClient({ baseUrl: readUrl() });
|
||||
|
||||
const reseed = async (): Promise<void> => {
|
||||
@@ -54,6 +58,7 @@ export async function activate(context: vscode.ExtensionContext): Promise<void>
|
||||
onSession: (e) => store.applySession(e),
|
||||
onWorktree: (e) => store.applyWorktree(e),
|
||||
onGroup: (e) => store.applyGroup(e),
|
||||
onSessionArchived: (id) => store.archiveSession(id),
|
||||
onReady: () => void reseed(),
|
||||
onStatus: (s) => {
|
||||
const connected = s === 'open';
|
||||
@@ -132,6 +137,10 @@ export async function activate(context: vscode.ExtensionContext): Promise<void>
|
||||
if (e.affectsConfiguration('arboretum.showExternalSessions')) {
|
||||
store.setShowExternalSessions(readShowExternal());
|
||||
}
|
||||
if (e.affectsConfiguration('arboretum.showArchivedSessions')) {
|
||||
store.setShowArchivedSessions(readShowArchived());
|
||||
await reseed(); // recharge la liste avec/sans les sessions archivées (includeArchived)
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
||||
|
||||
@@ -27,6 +27,8 @@ export class Store {
|
||||
|
||||
/** affiche aussi les sessions externes (découvertes hors Arboretum). */
|
||||
showExternalSessions = false;
|
||||
/** affiche aussi les sessions managées auto-archivées par ancienneté (P10). */
|
||||
showArchivedSessions = false;
|
||||
|
||||
onDidChange(cb: () => void): () => void {
|
||||
this.listeners.add(cb);
|
||||
@@ -40,6 +42,13 @@ export class Store {
|
||||
this.emit();
|
||||
}
|
||||
|
||||
/** Bascule l'affichage des sessions archivées (le re-seed REST est piloté par l'extension). */
|
||||
setShowArchivedSessions(value: boolean): void {
|
||||
if (this.showArchivedSessions === value) return;
|
||||
this.showArchivedSessions = value;
|
||||
this.emit();
|
||||
}
|
||||
|
||||
private emit(): void {
|
||||
for (const cb of this.listeners) cb();
|
||||
}
|
||||
@@ -49,7 +58,7 @@ export class Store {
|
||||
const [repos, worktrees, sessions, groups] = await Promise.all([
|
||||
rest.listRepos(),
|
||||
rest.listWorktrees(),
|
||||
rest.listSessions(false),
|
||||
rest.listSessions({ includeArchived: this.showArchivedSessions }),
|
||||
rest.listGroups(),
|
||||
]);
|
||||
this.repos.clear();
|
||||
@@ -95,6 +104,14 @@ export class Store {
|
||||
this.emit();
|
||||
}
|
||||
|
||||
/** P10 : une session managée terminée vient d'être auto-archivée (message WS `session_archived`). */
|
||||
archiveSession(sessionId: string): void {
|
||||
const prev = this.sessions.get(sessionId);
|
||||
if (!prev) return;
|
||||
this.sessions.set(sessionId, { ...prev, archived: true });
|
||||
this.emit();
|
||||
}
|
||||
|
||||
applyWorktree(e: WorktreeEvent): void {
|
||||
switch (e.type) {
|
||||
case 'repo_update':
|
||||
@@ -152,6 +169,7 @@ export class Store {
|
||||
return [...this.sessions.values()]
|
||||
.filter((s) => normPath(s.cwd) === wp)
|
||||
.filter((s) => this.showExternalSessions || s.source === 'managed')
|
||||
.filter((s) => this.showArchivedSessions || !s.archived)
|
||||
.sort((a, b) => b.createdAt.localeCompare(a.createdAt));
|
||||
}
|
||||
|
||||
@@ -172,6 +190,7 @@ export class Store {
|
||||
return [...this.sessions.values()]
|
||||
.filter((s) => s.groupId === groupId)
|
||||
.filter((s) => this.showExternalSessions || s.source === 'managed')
|
||||
.filter((s) => this.showArchivedSessions || !s.archived)
|
||||
.sort((a, b) => b.createdAt.localeCompare(a.createdAt));
|
||||
}
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@ export function worktreeTreeItem(worktree: WorktreeSummary): vscode.TreeItem {
|
||||
item.contextValue = 'arboretum:worktree';
|
||||
item.iconPath = new vscode.ThemeIcon(worktree.isMain ? 'star-full' : 'git-branch');
|
||||
item.description = worktreeStatus(worktree);
|
||||
item.tooltip = worktree.path;
|
||||
item.tooltip = worktreeTooltip(worktree);
|
||||
item.resourceUri = vscode.Uri.file(worktree.path);
|
||||
return item;
|
||||
}
|
||||
@@ -32,9 +32,28 @@ function worktreeStatus(w: WorktreeSummary): string {
|
||||
if (w.git.ahead > 0) parts.push(`↑${w.git.ahead}`);
|
||||
if (w.git.behind > 0) parts.push(`↓${w.git.behind}`);
|
||||
if (w.git.dirtyCount > 0) parts.push(`●${w.git.dirtyCount}`);
|
||||
// Compteurs fins (P7, additifs) : staged/conflits surfacés directement dans l'arbre.
|
||||
if (w.git.stagedCount && w.git.stagedCount > 0) parts.push(`+${w.git.stagedCount}`);
|
||||
if (w.git.conflictCount && w.git.conflictCount > 0) parts.push(`!${w.git.conflictCount}`);
|
||||
return parts.join(' ');
|
||||
}
|
||||
|
||||
/** Tooltip détaillé : chemin + statut git fin (P7) + dernier commit. */
|
||||
function worktreeTooltip(w: WorktreeSummary): vscode.MarkdownString {
|
||||
const md = new vscode.MarkdownString();
|
||||
md.appendMarkdown(`\`${w.path}\`\n\n`);
|
||||
const g = w.git;
|
||||
const status: string[] = [];
|
||||
if (g.stagedCount && g.stagedCount > 0) status.push(`${g.stagedCount} staged`);
|
||||
if (g.unstagedCount && g.unstagedCount > 0) status.push(`${g.unstagedCount} unstaged`);
|
||||
if (g.conflictCount && g.conflictCount > 0) status.push(`${g.conflictCount} conflicts`);
|
||||
if (status.length === 0 && g.dirtyCount > 0) status.push(`${g.dirtyCount} changed`);
|
||||
md.appendMarkdown(status.length > 0 ? status.join(' · ') : 'clean');
|
||||
if (g.ahead > 0 || g.behind > 0) md.appendMarkdown(` (↑${g.ahead} ↓${g.behind})`);
|
||||
if (g.lastCommitSubject) md.appendMarkdown(`\n\n_${g.lastCommitSubject}_`);
|
||||
return md;
|
||||
}
|
||||
|
||||
export function sessionContextValue(s: SessionSummary): string {
|
||||
let v = `arboretum:session:${s.live ? 'live' : 'dead'}`;
|
||||
if (s.source === 'discovered') v += ':discovered';
|
||||
@@ -72,6 +91,7 @@ function sessionDescription(s: SessionSummary): string {
|
||||
// Affiche 'available' pour idle (cohérent avec le libellé web) ; l'enum reste 'idle' côté protocole.
|
||||
if (s.live && s.activity) parts.push(s.activity === 'idle' ? 'available' : s.activity);
|
||||
else if (!s.live) parts.push('exited');
|
||||
if (s.archived) parts.push('archived');
|
||||
if (s.source === 'discovered') parts.push('external');
|
||||
if (s.groupId) parts.push('group');
|
||||
if (s.waitingFor) parts.push(`— ${s.waitingFor}`);
|
||||
|
||||
@@ -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 l’URL 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');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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 });
|
||||
|
||||
Reference in New Issue
Block a user