163 lines
4.2 KiB
TypeScript
163 lines
4.2 KiB
TypeScript
import * as fs from "node:fs";
|
|
import * as path from "node:path";
|
|
import * as vscode from "vscode";
|
|
import type { SfClient } from "./sf-client.js";
|
|
|
|
export interface SessionItem {
|
|
label: string;
|
|
sessionFile: string;
|
|
timestamp: Date;
|
|
sessionId: string;
|
|
isCurrent: boolean;
|
|
}
|
|
|
|
/**
|
|
* Tree view provider that lists SF session files from the same directory
|
|
* as the currently active session.
|
|
*/
|
|
export class SfSessionTreeProvider
|
|
implements vscode.TreeDataProvider<SessionItem>, vscode.Disposable
|
|
{
|
|
public static readonly viewId = "sf-sessions";
|
|
|
|
private readonly _onDidChangeTreeData = new vscode.EventEmitter<void>();
|
|
readonly onDidChangeTreeData = this._onDidChangeTreeData.event;
|
|
|
|
private sessions: SessionItem[] = [];
|
|
private disposables: vscode.Disposable[] = [];
|
|
|
|
constructor(private readonly client: SfClient) {
|
|
this.disposables.push(
|
|
this._onDidChangeTreeData,
|
|
client.onConnectionChange(() => this.refresh()),
|
|
);
|
|
}
|
|
|
|
async refresh(): Promise<void> {
|
|
this.sessions = await this.loadSessions();
|
|
this._onDidChangeTreeData.fire();
|
|
}
|
|
|
|
private async loadSessions(): Promise<SessionItem[]> {
|
|
if (!this.client.isConnected) {
|
|
return [];
|
|
}
|
|
try {
|
|
const state = await this.client.getState();
|
|
this.currentSessionFile = state.sessionFile;
|
|
if (!state.sessionFile) {
|
|
return [];
|
|
}
|
|
|
|
const sessionDir = path.dirname(state.sessionFile);
|
|
const files = fs
|
|
.readdirSync(sessionDir)
|
|
.filter((f) => f.endsWith(".jsonl"))
|
|
.sort()
|
|
.reverse(); // newest first
|
|
|
|
const items: SessionItem[] = [];
|
|
for (const file of files) {
|
|
const sessionFile = path.join(sessionDir, file);
|
|
|
|
// Try two filename formats:
|
|
// 1. ISO timestamp: 2026-03-23T17-49-05-784Z_<sessionId>.jsonl
|
|
// 2. Unix timestamp: <unixTimestampMs>_<sessionId>.jsonl
|
|
const isoMatch = file.match(
|
|
/^(\d{4}-\d{2}-\d{2}T[\d-]+Z)_(.+)\.jsonl$/,
|
|
);
|
|
const unixMatch = file.match(/^(\d{10,})_(.+)\.jsonl$/);
|
|
|
|
let timestamp: Date;
|
|
let sessionId: string;
|
|
|
|
if (isoMatch) {
|
|
// Convert ISO-like format (dashes instead of colons) back to parseable ISO
|
|
const isoStr = isoMatch[1].replace(
|
|
/(\d{4}-\d{2}-\d{2}T\d{2})-(\d{2})-(\d{2})-(\d+)Z/,
|
|
"$1:$2:$3.$4Z",
|
|
);
|
|
timestamp = new Date(isoStr);
|
|
sessionId = isoMatch[2];
|
|
} else if (unixMatch) {
|
|
timestamp = new Date(parseInt(unixMatch[1], 10));
|
|
sessionId = unixMatch[2];
|
|
} else {
|
|
continue;
|
|
}
|
|
|
|
if (Number.isNaN(timestamp.getTime())) continue;
|
|
|
|
items.push({
|
|
label: formatDate(timestamp),
|
|
sessionFile,
|
|
timestamp,
|
|
sessionId,
|
|
isCurrent: sessionFile === state.sessionFile,
|
|
});
|
|
}
|
|
return items;
|
|
} catch {
|
|
return [];
|
|
}
|
|
}
|
|
|
|
getTreeItem(element: SessionItem): vscode.TreeItem {
|
|
const item = new vscode.TreeItem(
|
|
element.label,
|
|
vscode.TreeItemCollapsibleState.None,
|
|
);
|
|
item.description = element.sessionId.slice(0, 8);
|
|
item.tooltip = new vscode.MarkdownString(
|
|
`**${element.label}**\n\nID: \`${element.sessionId}\`\n\nFile: \`${element.sessionFile}\``,
|
|
);
|
|
item.iconPath = new vscode.ThemeIcon(
|
|
element.isCurrent ? "comment-discussion" : "history",
|
|
element.isCurrent
|
|
? new vscode.ThemeColor("terminal.ansiGreen")
|
|
: undefined,
|
|
);
|
|
if (!element.isCurrent) {
|
|
item.command = {
|
|
command: "sf.switchSession",
|
|
title: "Switch to Session",
|
|
arguments: [element.sessionFile],
|
|
};
|
|
}
|
|
item.contextValue = element.isCurrent ? "currentSession" : "session";
|
|
return item;
|
|
}
|
|
|
|
getChildren(): SessionItem[] {
|
|
return this.sessions;
|
|
}
|
|
|
|
dispose(): void {
|
|
for (const d of this.disposables) {
|
|
d.dispose();
|
|
}
|
|
}
|
|
}
|
|
|
|
function formatDate(d: Date): string {
|
|
const now = new Date();
|
|
const diffMs = now.getTime() - d.getTime();
|
|
const diffDays = Math.floor(diffMs / 86_400_000);
|
|
|
|
if (diffDays === 0) {
|
|
return `Today ${d.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" })}`;
|
|
} else if (diffDays === 1) {
|
|
return `Yesterday ${d.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" })}`;
|
|
} else if (diffDays < 7) {
|
|
return d.toLocaleDateString([], {
|
|
weekday: "short",
|
|
hour: "2-digit",
|
|
minute: "2-digit",
|
|
});
|
|
}
|
|
return d.toLocaleDateString([], {
|
|
month: "short",
|
|
day: "numeric",
|
|
year: "numeric",
|
|
});
|
|
}
|