Skip to content

Instantly share code, notes, and snippets.

@Keboo
Last active June 17, 2026 18:55
Show Gist options
  • Select an option

  • Save Keboo/08fc73fd0bec49e58da14d6a6ad7eb96 to your computer and use it in GitHub Desktop.

Select an option

Save Keboo/08fc73fd0bec49e58da14d6a6ad7eb96 to your computer and use it in GitHub Desktop.
Copilot extension: chronicle-sessions
{
"name": "chronicle",
"version": 1
}
import { createServer } from "node:http";
import { joinSession, createCanvas, CanvasError } from "@github/copilot-sdk/extension";
import { homedir } from "node:os";
import { join as joinPath } from "node:path";
import { readdir, readFile, stat, rm } from "node:fs/promises";
const servers = new Map();
const copilotHome = process.env.COPILOT_HOME || joinPath(homedir(), ".copilot");
const sessionStateRoot = joinPath(copilotHome, "session-state");
function escapeHtml(value) {
return String(value ?? "")
.replaceAll("&", "&")
.replaceAll("<", "&lt;")
.replaceAll(">", "&gt;")
.replaceAll('"', "&quot;")
.replaceAll("'", "&#39;");
}
function sendJson(res, statusCode, payload) {
res.statusCode = statusCode;
res.setHeader("Content-Type", "application/json; charset=utf-8");
res.end(JSON.stringify(payload));
}
async function readJsonBody(req) {
const chunks = [];
for await (const chunk of req) {
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
}
const raw = Buffer.concat(chunks).toString("utf8").trim();
if (!raw) {
return {};
}
return JSON.parse(raw);
}
function errorMessage(error) {
return error instanceof Error ? error.message : String(error);
}
function parseYamlScalar(rawValue) {
const value = rawValue.trim();
if (value === "") return "";
if (value === "true") return true;
if (value === "false") return false;
if (value === "null") return "";
if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) {
return value.slice(1, -1);
}
return value;
}
function parseWorkspaceYaml(content) {
const data = {};
const lines = content.split(/\r?\n/);
for (const line of lines) {
const trimmed = line.trim();
if (!trimmed || trimmed.startsWith("#")) continue;
const index = line.indexOf(":");
if (index <= 0) continue;
const key = line.slice(0, index).trim();
const rawValue = line.slice(index + 1);
data[key] = parseYamlScalar(rawValue);
}
return data;
}
function coerceIso(value) {
if (typeof value !== "string" || !value) return "";
const date = new Date(value);
if (Number.isNaN(date.getTime())) return "";
return date.toISOString();
}
function isAlivePid(pid) {
try {
process.kill(pid, 0);
return true;
} catch {
return false;
}
}
async function hasLiveInUseLock(sessionDir) {
try {
const entries = await readdir(sessionDir, { withFileTypes: true });
for (const entry of entries) {
if (!entry.isFile()) continue;
const match = /^inuse\.(\d+)\.lock$/i.exec(entry.name);
if (!match) continue;
const pid = Number(match[1]);
if (Number.isFinite(pid) && pid > 0 && isAlivePid(pid)) {
return true;
}
}
} catch {
// Ignore listing failures and treat as no lock evidence.
}
return false;
}
async function getActiveSessionSet(session, sessionIds) {
const active = new Set();
try {
const chunkSize = 200;
for (let index = 0; index < sessionIds.length; index += chunkSize) {
const chunk = sessionIds.slice(index, index + chunkSize);
const inUse = await session.rpc.sessions.checkInUse({ sessionIds: chunk });
for (const sessionId of inUse?.inUse ?? []) {
active.add(sessionId);
}
}
return active;
} catch {
// Fall back to lock-file scanning when RPC availability is limited.
await Promise.all(
sessionIds.map(async (sessionId) => {
const sessionDir = joinPath(sessionStateRoot, sessionId);
if (await hasLiveInUseLock(sessionDir)) {
active.add(sessionId);
}
}),
);
return active;
}
}
async function listSessionsFromSessionState(session) {
const currentSessionId = session.sessionId;
const entries = await readdir(sessionStateRoot, { withFileTypes: true });
const candidates = entries.filter((entry) => entry.isDirectory()).map((entry) => entry.name);
const activeSessions = await getActiveSessionSet(session, candidates);
const sessions = await Promise.all(
candidates.map(async (sessionId) => {
const sessionDir = joinPath(sessionStateRoot, sessionId);
const yamlPath = joinPath(sessionDir, "workspace.yaml");
let metadata = {};
try {
const yaml = await readFile(yamlPath, "utf8");
metadata = parseWorkspaceYaml(yaml);
} catch {
// Ignore missing/invalid workspace metadata for this session.
}
let folderTimes = null;
try {
folderTimes = await stat(sessionDir);
} catch {
// Ignore stat failures; we'll still return what we have.
}
const startTime =
coerceIso(metadata.created_at) ||
coerceIso(metadata.start_time) ||
(folderTimes ? folderTimes.birthtime.toISOString() : "");
const modifiedTime =
coerceIso(metadata.updated_at) ||
coerceIso(metadata.modified_time) ||
(folderTimes ? folderTimes.mtime.toISOString() : "");
return {
sessionId,
name: typeof metadata.name === "string" ? metadata.name : "",
summary: typeof metadata.summary === "string" ? metadata.summary : "",
startTime,
modifiedTime,
isRemote: Boolean(metadata.remote_steerable),
isDetached: false,
cwd: typeof metadata.cwd === "string" ? metadata.cwd : "",
gitRoot: typeof metadata.git_root === "string" ? metadata.git_root : "",
branch: typeof metadata.branch === "string" ? metadata.branch : "",
repository: typeof metadata.repository === "string" ? metadata.repository : "",
isCurrent: sessionId === currentSessionId,
isActive: activeSessions.has(sessionId) || sessionId === currentSessionId,
};
}),
);
sessions.sort((a, b) => {
const aTime = Date.parse(a.modifiedTime || a.startTime || "") || 0;
const bTime = Date.parse(b.modifiedTime || b.startTime || "") || 0;
return bTime - aTime;
});
return {
sessions,
source: "session-state",
};
}
async function listChronicleSessions(session) {
try {
return await listSessionsFromSessionState(session);
} catch (error) {
throw new CanvasError("session_state_read_failed", `Unable to read local sessions: ${errorMessage(error)}`);
}
}
async function countEntries(path) {
try {
const entries = await readdir(path, { withFileTypes: true });
return entries.length;
} catch {
return 0;
}
}
async function readSize(path) {
try {
const fileStat = await stat(path);
return fileStat.size;
} catch {
return null;
}
}
async function getSessionDetailsFromSessionState(session, sessionId) {
if (!sessionId || typeof sessionId !== "string") {
throw new CanvasError("invalid_session_id", "sessionId is required.");
}
const sessionDir = joinPath(sessionStateRoot, sessionId);
const dirExists = await stat(sessionDir).then(
() => true,
() => false,
);
if (!dirExists) {
throw new CanvasError("session_not_found", `Session not found: ${sessionId}`);
}
const yamlPath = joinPath(sessionDir, "workspace.yaml");
let metadata = {};
try {
const yaml = await readFile(yamlPath, "utf8");
metadata = parseWorkspaceYaml(yaml);
} catch {
// Keep metadata empty when unavailable.
}
const folderStats = await stat(sessionDir);
const checkpointsDir = joinPath(sessionDir, "checkpoints");
const filesDir = joinPath(sessionDir, "files");
const researchDir = joinPath(sessionDir, "research");
const eventsPath = joinPath(sessionDir, "events.jsonl");
const dbPath = joinPath(sessionDir, "session.db");
let active = false;
try {
const inUse = await session.rpc.sessions.checkInUse({ sessionIds: [sessionId] });
active = Array.isArray(inUse?.inUse) && inUse.inUse.includes(sessionId);
} catch {
active = await hasLiveInUseLock(sessionDir);
}
return {
sessionId,
name: typeof metadata.name === "string" ? metadata.name : "",
summary: typeof metadata.summary === "string" ? metadata.summary : "",
cwd: typeof metadata.cwd === "string" ? metadata.cwd : "",
gitRoot: typeof metadata.git_root === "string" ? metadata.git_root : "",
repository: typeof metadata.repository === "string" ? metadata.repository : "",
branch: typeof metadata.branch === "string" ? metadata.branch : "",
hostType: typeof metadata.host_type === "string" ? metadata.host_type : "",
clientName: typeof metadata.client_name === "string" ? metadata.client_name : "",
mcTaskId: typeof metadata.mc_task_id === "string" ? metadata.mc_task_id : "",
mcSessionId: typeof metadata.mc_session_id === "string" ? metadata.mc_session_id : "",
startTime:
coerceIso(metadata.created_at) ||
coerceIso(metadata.start_time) ||
folderStats.birthtime.toISOString(),
modifiedTime:
coerceIso(metadata.updated_at) ||
coerceIso(metadata.modified_time) ||
folderStats.mtime.toISOString(),
path: sessionDir,
checkpointsCount: await countEntries(checkpointsDir),
filesCount: await countEntries(filesDir),
researchCount: await countEntries(researchDir),
eventsBytes: await readSize(eventsPath),
dbBytes: await readSize(dbPath),
isCurrent: sessionId === session.sessionId,
isActive: active || sessionId === session.sessionId,
};
}
function truncateText(value, maxLength = 12000) {
if (typeof value !== "string") return "";
if (value.length <= maxLength) return value;
return `${value.slice(0, maxLength)}\n…(truncated)…`;
}
function previewText(value, maxLength = 220) {
if (typeof value !== "string") return "";
const singleLine = value.replace(/\s+/g, " ").trim();
if (singleLine.length <= maxLength) return singleLine;
return `${singleLine.slice(0, maxLength)}…`;
}
function titleCaseFromType(type) {
if (!type || typeof type !== "string") return "Event";
const normalized = type.replace(/[._-]+/g, " ");
return normalized.replace(/\b\w/g, (char) => char.toUpperCase());
}
function formatEventBody(type, data, fallback) {
if (type === "user.message" || type === "assistant.message") {
const message = data?.content;
if (typeof message === "string" && message.trim()) {
return message;
}
}
if (type === "tool.execution_start") {
const toolName = data?.toolName ? `Tool: ${data.toolName}` : "Tool execution started";
const args = data?.arguments ? `Arguments:\n${JSON.stringify(data.arguments, null, 2)}` : "";
return `${toolName}${args ? `\n${args}` : ""}`;
}
if (type === "tool.execution_complete") {
const toolName = data?.toolName ? `Tool: ${data.toolName}` : "Tool execution completed";
const success = typeof data?.success === "boolean" ? `Success: ${data.success}` : "";
const result =
data?.result !== undefined
? `Result:\n${typeof data.result === "string" ? data.result : JSON.stringify(data.result, null, 2)}`
: "";
const error = data?.error ? `Error:\n${data.error}` : "";
return [toolName, success, result, error].filter(Boolean).join("\n");
}
if (data !== undefined) {
try {
return JSON.stringify(data, null, 2);
} catch {
return String(data);
}
}
return fallback;
}
function formatDebugLogName(type, data) {
if (type === "session.info") {
return data?.infoType ? `${titleCaseFromType(String(data.infoType))} Info` : "Session Info";
}
if (type === "user.message") return "User Message";
if (type === "assistant.message") return data?.model ? String(data.model) : "Agent Response";
if (type === "tool.execution_start") return data?.toolName ? String(data.toolName) : "Tool Start";
if (type === "tool.execution_complete") return data?.toolName ? String(data.toolName) : "Tool Complete";
if (type === "hook.start" || type === "hook.end") {
return data?.hookType ? `${String(data.hookType)} Hook` : titleCaseFromType(type);
}
if (type === "permission.requested") return "Permission Requested";
if (type === "permission.completed") return "Permission Completed";
if (type === "external_tool.requested") return "External Tool Requested";
if (type === "external_tool.completed") return "External Tool Completed";
if (type === "skill.invoked") return "Skill Invoked";
if (type === "session.start") return "Session Start";
if (type === "session.resume") return "Session Resume";
if (type === "session.shutdown") return "Session Shutdown";
if (type === "session.error") return "Session Error";
return titleCaseFromType(type);
}
function formatDebugLogDetails(type, data, eventBody) {
if (type === "session.start") {
const repository = data?.context?.repository ? String(data.context.repository) : "";
const branch = data?.context?.branch ? String(data.context.branch) : "";
const cwd = data?.context?.cwd ? String(data.context.cwd) : "";
return [repository, branch, cwd].filter(Boolean).join(" • ");
}
if (type === "session.resume") return previewText(data?.message ?? data?.reason ?? "");
if (type === "session.shutdown") return previewText(data?.reason ?? "");
if (type === "session.info") return previewText(data?.message ?? "");
if (type === "user.message") return previewText(data?.message ?? "");
if (type === "assistant.message") return previewText(data?.content ?? "");
if (type === "tool.execution_start") {
const args =
data?.arguments && typeof data.arguments === "object" ? previewText(JSON.stringify(data.arguments)) : "";
return args ? `started ${args}` : "started";
}
if (type === "tool.execution_complete") {
const parts = [];
if (typeof data?.success === "boolean") parts.push(data.success ? "success" : "failed");
if (data?.error) parts.push(previewText(String(data.error)));
if (data?.result !== undefined) {
const resultText =
typeof data.result === "string" ? previewText(data.result) : previewText(JSON.stringify(data.result));
if (resultText) parts.push(resultText);
}
return parts.join(" • ");
}
if (type === "hook.start") return "start";
if (type === "hook.end") return "end";
if (type === "external_tool.requested" || type === "external_tool.completed") {
const toolName = typeof data?.toolName === "string" ? data.toolName : "";
const status = type.endsWith("completed") ? "completed" : "requested";
return [toolName, status].filter(Boolean).join(" • ");
}
if (type === "permission.requested" || type === "permission.completed") {
const status = type.endsWith("completed") ? "granted" : "requested";
const target = typeof data?.resource === "string" ? data.resource : typeof data?.toolName === "string" ? data.toolName : "";
return [target, status].filter(Boolean).join(" • ");
}
if (type === "skill.invoked") {
if (typeof data?.name === "string" && data.name) return data.name;
if (typeof data?.skill === "string" && data.skill) return data.skill;
}
if (type === "session.error") return previewText(data?.message ?? data?.error ?? "");
return previewText(eventBody ?? "");
}
async function getSessionContentFromSessionState(session, sessionId) {
const details = await getSessionDetailsFromSessionState(session, sessionId);
const eventsPath = joinPath(sessionStateRoot, sessionId, "events.jsonl");
let eventsRaw = "";
try {
eventsRaw = await readFile(eventsPath, "utf8");
} catch {
eventsRaw = "";
}
const lines = eventsRaw
.split(/\r?\n/)
.map((line) => line.trim())
.filter(Boolean);
const maxEvents = 5000;
const start = Math.max(0, lines.length - maxEvents);
const selectedLines = lines.slice(start);
const events = selectedLines.map((line, index) => {
let parsed = null;
try {
parsed = JSON.parse(line);
} catch {
parsed = null;
}
const type = typeof parsed?.type === "string" ? parsed.type : "event";
const data = parsed && typeof parsed === "object" ? parsed.data : undefined;
const timestampCandidate =
parsed?.timestamp ?? parsed?.time ?? parsed?.createdAt ?? data?.timestamp ?? data?.time;
const timestamp = typeof timestampCandidate === "string" ? timestampCandidate : "";
const fallbackBody = parsed ? JSON.stringify(parsed, null, 2) : line;
const body = truncateText(
formatEventBody(type, data, fallbackBody),
);
const rawEvent = truncateText(fallbackBody, 12000);
return {
sequence: start + index + 1,
type,
timestamp,
body,
rawEvent,
data,
};
});
const debugLogs = events.map((event) => ({
sequence: event.sequence,
created: event.timestamp,
name: formatDebugLogName(event.type, event.data),
details: truncateText(formatDebugLogDetails(event.type, event.data, event.body), 1200),
type: event.type,
rawEvent: event.rawEvent,
}));
return {
session: details,
events,
debugLogs,
totalEventLines: lines.length,
truncated: lines.length > maxEvents,
};
}
async function deleteSessionViaChronicle(session, sessionId) {
if (!sessionId || typeof sessionId !== "string") {
throw new CanvasError("invalid_session_id", "sessionId is required.");
}
if (sessionId === session.sessionId) {
throw new CanvasError("cannot_delete_current_session", "Refusing to delete the current active session.");
}
const sessionDir = joinPath(sessionStateRoot, sessionId);
const existsBeforeDelete = await stat(sessionDir).then(
() => true,
() => false,
);
if (!existsBeforeDelete) {
throw new CanvasError("session_not_found", `Session not found: ${sessionId}`);
}
try {
const inUseResult = await session.rpc.sessions.checkInUse({ sessionIds: [sessionId] });
if (Array.isArray(inUseResult.inUse) && inUseResult.inUse.includes(sessionId)) {
throw new CanvasError(
"session_in_use",
"That session is currently in use by another running process and cannot be deleted yet.",
);
}
} catch (error) {
if (error instanceof CanvasError) throw error;
// If in-use detection is unavailable, continue to deletion attempt.
}
try {
const result = await session.rpc.sessions.bulkDelete({ sessionIds: [sessionId] });
const freed = result?.freedBytes ?? {};
if (Object.prototype.hasOwnProperty.call(freed, sessionId)) {
return { deleted: true };
}
} catch (error) {
if (error instanceof CanvasError) throw error;
// Fall through and verify by filesystem check below.
}
const existsAfterDelete = await stat(sessionDir).then(
() => true,
() => false,
);
if (!existsAfterDelete) {
return { deleted: true };
}
if (await hasLiveInUseLock(sessionDir)) {
throw new CanvasError(
"session_in_use",
"That session is currently in use by another running process and cannot be deleted yet.",
);
}
try {
await rm(sessionDir, { recursive: true, force: true });
const stillExists = await stat(sessionDir).then(
() => true,
() => false,
);
if (!stillExists) {
return { deleted: true };
}
} catch {
// Fall through to user-facing error below.
}
throw new CanvasError("chronicle_delete_failed", "Unable to delete that session.");
}
function renderHtml(instanceId) {
return `<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Copilot Session Manager</title>
<style>
:root {
color-scheme: light dark;
}
body {
margin: 0;
background: var(--background-color-default, #fff);
color: var(--text-color-default, #1f2328);
font-family: var(--font-sans, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif);
font-size: var(--text-body-medium, 14px);
line-height: var(--leading-body-medium, 20px);
}
.wrap {
padding: 12px;
display: grid;
gap: 12px;
}
.toolbar {
display: flex;
gap: 8px;
align-items: center;
flex-wrap: wrap;
}
input[type="search"] {
min-width: 280px;
max-width: 520px;
width: 100%;
flex: 1 1 280px;
border: 1px solid var(--border-color-default, #d1d9e0);
border-radius: 6px;
padding: 6px 10px;
font: inherit;
background: var(--background-color-default, #fff);
color: var(--text-color-default, #1f2328);
}
button {
border: 1px solid var(--border-color-default, #d1d9e0);
border-radius: 6px;
padding: 6px 10px;
background: var(--background-color-default, #fff);
color: var(--text-color-default, #1f2328);
font: inherit;
cursor: pointer;
}
button:disabled {
cursor: default;
opacity: 0.6;
}
table {
width: 100%;
border-collapse: collapse;
}
th, td {
border-bottom: 1px solid var(--border-color-default, #d1d9e0);
text-align: left;
vertical-align: top;
padding: 8px 6px;
}
th {
color: var(--text-color-muted, #59636e);
font-weight: var(--font-weight-semibold, 600);
}
code {
font-family: var(--font-mono, ui-monospace, SFMono-Regular, Menlo, Consolas, monospace);
font-size: var(--text-code-inline, 12px);
}
.meta {
color: var(--text-color-muted, #59636e);
}
.error {
color: var(--true-color-red, #d1242f);
}
.empty {
padding: 12px 0;
color: var(--text-color-muted, #59636e);
}
.name {
font-weight: var(--font-weight-semibold, 600);
}
.session-open {
all: unset;
cursor: pointer;
color: var(--text-color-default, #1f2328);
}
.session-open:hover {
text-decoration: underline;
}
.badges {
display: flex;
gap: 6px;
margin-top: 4px;
flex-wrap: wrap;
}
.badge {
border: 1px solid var(--border-color-default, #d1d9e0);
border-radius: 999px;
padding: 1px 8px;
font-size: 12px;
line-height: 16px;
color: var(--text-color-muted, #59636e);
}
.badge-active {
color: var(--true-color-blue, #0969da);
border-color: var(--true-color-blue-muted, #54aeff66);
}
.badge-current {
color: var(--true-color-red, #d1242f);
border-color: var(--true-color-red-muted, #ff818266);
}
tr.active-session > td {
background: color-mix(in srgb, var(--true-color-blue-muted, #54aeff66) 14%, transparent);
}
.actions {
display: inline-flex;
gap: 4px;
flex-wrap: nowrap;
white-space: nowrap;
align-items: center;
}
.icon-button {
width: 30px;
height: 30px;
padding: 0;
display: inline-flex;
align-items: center;
justify-content: center;
font-size: 16px;
line-height: 1;
}
.icon-delete {
color: var(--true-color-red, #d1242f);
}
.details-panel {
border: 1px solid var(--border-color-default, #d1d9e0);
border-radius: 8px;
padding: 10px 12px;
background: color-mix(in srgb, var(--background-color-default, #fff) 92%, var(--true-color-blue-muted, #54aeff66) 8%);
}
.details-header {
display: flex;
justify-content: space-between;
align-items: center;
gap: 8px;
margin-bottom: 8px;
}
.details-title {
font-weight: var(--font-weight-semibold, 600);
}
.details-close {
width: 24px;
height: 24px;
padding: 0;
line-height: 1;
}
.details-grid {
display: grid;
grid-template-columns: 160px 1fr;
gap: 4px 10px;
}
.details-meta-grid {
margin-top: 8px;
}
.details-key {
color: var(--text-color-muted, #59636e);
}
.details-value {
word-break: break-word;
}
.details-code {
font-family: var(--font-mono, ui-monospace, SFMono-Regular, Menlo, Consolas, monospace);
font-size: 12px;
}
.session-view {
display: grid;
gap: 10px;
}
.session-tabs {
display: inline-flex;
gap: 6px;
}
.tab-button {
padding: 4px 10px;
}
.tab-button.active {
border-color: var(--true-color-blue, #0969da);
color: var(--true-color-blue, #0969da);
background: color-mix(in srgb, var(--true-color-blue-muted, #54aeff66) 22%, transparent);
}
.session-events {
display: grid;
gap: 8px;
max-height: 68vh;
overflow: auto;
padding-right: 4px;
}
.debug-tools {
display: flex;
gap: 8px;
align-items: center;
flex-wrap: wrap;
}
.debug-filter {
min-width: 220px;
max-width: 520px;
width: 100%;
flex: 1 1 220px;
}
.debug-list-wrap {
max-height: 44vh;
overflow: auto;
}
.debug-row {
cursor: pointer;
}
.debug-row.selected > td {
background: color-mix(in srgb, var(--true-color-blue-muted, #54aeff66) 25%, transparent);
}
.event-card {
border: 1px solid var(--border-color-default, #d1d9e0);
border-radius: 8px;
padding: 8px;
background: color-mix(in srgb, var(--background-color-default, #fff) 95%, var(--true-color-blue-muted, #54aeff66) 5%);
}
.event-head {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
margin-bottom: 6px;
}
.event-type {
border: 1px solid var(--border-color-default, #d1d9e0);
border-radius: 999px;
padding: 1px 8px;
font-size: 12px;
line-height: 16px;
color: var(--text-color-muted, #59636e);
}
.event-content {
margin: 0;
white-space: pre-wrap;
word-break: break-word;
font-family: var(--font-mono, ui-monospace, SFMono-Regular, Menlo, Consolas, monospace);
font-size: 12px;
line-height: 18px;
}
</style>
</head>
<body>
<div class="wrap">
<div class="toolbar" id="toolbar">
<input id="search" type="search" placeholder="Search old sessions by name, summary, path, branch, id..." />
<button id="refresh" type="button">Refresh</button>
</div>
<div id="status" class="meta"></div>
<div id="error" class="error"></div>
<div id="details"></div>
<div id="table"></div>
</div>
<script>
const searchInput = document.getElementById("search");
const refreshButton = document.getElementById("refresh");
const toolbarEl = document.getElementById("toolbar");
const statusEl = document.getElementById("status");
const errorEl = document.getElementById("error");
const detailsEl = document.getElementById("details");
const tableEl = document.getElementById("table");
const state = {
sessions: [],
source: "",
loading: false,
retryTimer: null,
viewMode: "list",
selectedSessionId: null,
sessionContentById: {},
sessionViewLoading: false,
sessionViewTab: "timeline",
timelineFilter: "",
timelineFilterDraft: "",
timelineFilterDebounceTimer: null,
debugFilter: "",
debugFilterDraft: "",
debugFilterDebounceTimer: null,
selectedDebugSequence: null,
};
function escapeHtml(value) {
return String(value ?? "")
.replaceAll("&", "&amp;")
.replaceAll("<", "&lt;")
.replaceAll(">", "&gt;")
.replaceAll('"', "&quot;")
.replaceAll("'", "&#39;");
}
function formatTime(value) {
if (!value) return "";
const date = new Date(value);
if (Number.isNaN(date.getTime())) return value;
return date.toLocaleString();
}
function formatBytes(value) {
if (typeof value !== "number" || !Number.isFinite(value) || value < 0) return "";
if (value < 1024) return value + " B";
if (value < 1024 * 1024) return (value / 1024).toFixed(1) + " KB";
return (value / (1024 * 1024)).toFixed(1) + " MB";
}
function searchSnippet(value, maxLength = 800) {
if (typeof value !== "string") return "";
if (value.length <= maxLength) return value;
return value.slice(0, maxLength);
}
function getFilteredSessions() {
const q = searchInput.value.trim().toLowerCase();
if (!q) return state.sessions;
return state.sessions.filter((s) => {
const haystack = [
s.sessionId,
s.name,
s.summary,
s.cwd,
s.gitRoot,
s.branch,
s.repository,
s.modifiedTime,
s.startTime,
]
.filter(Boolean)
.join(" ")
.toLowerCase();
return haystack.includes(q);
});
}
function setLoading(loading) {
state.loading = loading;
refreshButton.disabled = loading;
statusEl.textContent = loading ? "Loading sessions..." : statusEl.textContent;
}
function clearDebugFilterDebounce() {
if (state.debugFilterDebounceTimer != null) {
window.clearTimeout(state.debugFilterDebounceTimer);
state.debugFilterDebounceTimer = null;
}
}
function clearTimelineFilterDebounce() {
if (state.timelineFilterDebounceTimer != null) {
window.clearTimeout(state.timelineFilterDebounceTimer);
state.timelineFilterDebounceTimer = null;
}
}
function restoreFocusedFilterInput(focusState) {
if (!focusState || !focusState.id) return;
const input = tableEl.querySelector("#" + focusState.id);
if (!input) return;
input.focus();
if (typeof focusState.start === "number" && typeof focusState.end === "number") {
const start = Math.max(0, Math.min(focusState.start, input.value.length));
const end = Math.max(0, Math.min(focusState.end, input.value.length));
try {
input.setSelectionRange(start, end);
} catch {}
}
}
function renderSessionView() {
const sessionId = state.selectedSessionId;
if (!sessionId) {
state.viewMode = "list";
render();
return;
}
if (state.sessionViewLoading && !state.sessionContentById[sessionId]) {
tableEl.innerHTML = '<div class="details-panel">Loading session content...</div>';
return;
}
const content = state.sessionContentById[sessionId];
if (!content || !content.session) {
tableEl.innerHTML = '<div class="details-panel">Session content unavailable.</div>';
return;
}
const activeElement = document.activeElement;
const focusedFilterInput =
activeElement && (activeElement.id === "timeline-filter" || activeElement.id === "debug-filter")
? {
id: activeElement.id,
start: activeElement.selectionStart,
end: activeElement.selectionEnd,
}
: null;
const sessionDetails = content.session;
const displayName = sessionDetails.name || sessionDetails.summary || "(unnamed session)";
const summary = sessionDetails.summary ? '<div class="meta">' + escapeHtml(sessionDetails.summary) + "</div>" : "";
const context = [sessionDetails.repository, sessionDetails.branch, sessionDetails.cwd].filter(Boolean).join(" • ");
const events = Array.isArray(content.events) ? content.events : [];
const timelineQuery = (state.timelineFilter || "").trim().toLowerCase();
const filteredEvents = !timelineQuery
? events
: events.filter((event) => {
const haystack = [event.type, searchSnippet(event.body, 1200), event.timestamp, String(event.sequence || "")]
.filter(Boolean)
.join(" ")
.toLowerCase();
return haystack.includes(timelineQuery);
});
const debugLogs = Array.isArray(content.debugLogs) ? content.debugLogs : [];
const debugQuery = (state.debugFilter || "").trim().toLowerCase();
const filteredDebugLogs = !debugQuery
? debugLogs
: debugLogs.filter((log) => {
const haystack = [log.name, searchSnippet(log.details, 800), log.type]
.filter(Boolean)
.join(" ")
.toLowerCase();
return haystack.includes(debugQuery);
});
if (state.sessionViewTab === "debug" && !filteredDebugLogs.some((log) => log.sequence === state.selectedDebugSequence)) {
state.selectedDebugSequence = filteredDebugLogs.length ? filteredDebugLogs[0].sequence : null;
}
const selectedDebugLog =
state.sessionViewTab === "debug"
? filteredDebugLogs.find((log) => log.sequence === state.selectedDebugSequence) || null
: null;
const eventsMarkup = filteredEvents.length
? filteredEvents
.map((event) => {
return \`
<article class="event-card">
<div class="event-head">
<span class="event-type">\${escapeHtml(event.type || "event")}</span>
<span class="meta">\${escapeHtml(event.timestamp ? formatTime(event.timestamp) : "#" + String(event.sequence || ""))}</span>
</div>
<pre class="event-content">\${escapeHtml(event.body || "")}</pre>
</article>
\`;
})
.join("")
: '<div class="empty">No timeline events match the current filter.</div>';
const timelineFilterValue = state.timelineFilterDraft != null ? state.timelineFilterDraft : state.timelineFilter;
const timelineViewMarkup = \`
<div class="debug-tools">
<input
id="timeline-filter"
class="debug-filter"
type="search"
placeholder="Filter timeline events (type, content, timestamp...)"
value="\${escapeHtml(timelineFilterValue || "")}"
/>
<div class="meta">Showing \${String(filteredEvents.length)} of \${String(events.length)} events</div>
</div>
<div class="session-events">\${eventsMarkup}</div>
\`;
const debugRowsMarkup = filteredDebugLogs.length
? filteredDebugLogs
.map((log) => {
const isSelected = log.sequence === state.selectedDebugSequence;
return \`
<tr class="debug-row \${isSelected ? "selected" : ""}" data-debug-seq="\${String(log.sequence)}">
<td>\${escapeHtml(log.created ? formatTime(log.created) : "")}</td>
<td>\${escapeHtml(log.name || "")}</td>
<td>\${escapeHtml(log.details || "")}</td>
</tr>
\`;
})
.join("")
: '<tr><td colspan="3" class="meta">No debug logs match the current filter.</td></tr>';
const debugFilterValue = state.debugFilterDraft != null ? state.debugFilterDraft : state.debugFilter;
const debugViewMarkup = \`
<div class="debug-tools">
<input
id="debug-filter"
class="debug-filter"
type="search"
placeholder="Filter debug logs (name, details, type...)"
value="\${escapeHtml(debugFilterValue || "")}"
/>
<div class="meta">Showing \${String(filteredDebugLogs.length)} of \${String(debugLogs.length)} logs</div>
</div>
<div class="debug-list-wrap">
<table class="debug-table">
<thead>
<tr>
<th>Created</th>
<th>Name</th>
<th>Details</th>
</tr>
</thead>
<tbody>
\${debugRowsMarkup}
</tbody>
</table>
</div>
\${selectedDebugLog ? \`
<div class="details-panel">
<div class="details-title">Debug event details</div>
<div class="meta" style="margin-bottom: 6px;">\${escapeHtml(selectedDebugLog.type || "")}</div>
<pre class="event-content">\${escapeHtml(selectedDebugLog.rawEvent || "")}</pre>
</div>
\` : ""}
\`;
statusEl.textContent = "Viewing session " + (sessionDetails.sessionId || sessionId);
tableEl.innerHTML = \`
<div class="session-view">
<div class="details-header">
<button type="button" class="details-close" data-back-to-list aria-label="Back to sessions" title="Back">←</button>
<div class="details-title">Session Viewer</div>
<div class="session-tabs">
<button type="button" class="tab-button \${state.sessionViewTab === "timeline" ? "active" : ""}" data-session-tab="timeline">Timeline</button>
<button type="button" class="tab-button \${state.sessionViewTab === "debug" ? "active" : ""}" data-session-tab="debug">Debug Logs</button>
</div>
</div>
<div class="details-panel">
<div class="name">\${escapeHtml(displayName)}</div>
\${summary}
<div class="meta">\${escapeHtml(context)}</div>
<div class="details-grid details-meta-grid">
<div class="details-key">Session ID</div><div class="details-value details-code">\${escapeHtml(sessionDetails.sessionId || "")}</div>
<div class="details-key">Modified</div><div class="details-value">\${escapeHtml(formatTime(sessionDetails.modifiedTime || sessionDetails.startTime))}</div>
<div class="details-key">Path</div><div class="details-value details-code">\${escapeHtml(sessionDetails.path || "")}</div>
<div class="details-key">Events</div><div class="details-value">\${escapeHtml(String(content.totalEventLines ?? events.length))}\${content.truncated ? " (showing recent 5000)" : ""}</div>
</div>
</div>
\${state.sessionViewTab === "debug" ? debugViewMarkup : timelineViewMarkup}
</div>
\`;
const backButton = tableEl.querySelector("button[data-back-to-list]");
if (backButton) {
backButton.addEventListener("click", () => {
clearDebugFilterDebounce();
clearTimelineFilterDebounce();
state.viewMode = "list";
state.selectedSessionId = null;
state.sessionViewTab = "timeline";
state.timelineFilter = "";
state.timelineFilterDraft = "";
state.debugFilter = "";
state.debugFilterDraft = "";
state.selectedDebugSequence = null;
render();
});
}
for (const button of tableEl.querySelectorAll("button[data-session-tab]")) {
button.addEventListener("click", () => {
clearDebugFilterDebounce();
clearTimelineFilterDebounce();
const tab = button.getAttribute("data-session-tab");
if (tab !== "timeline" && tab !== "debug") return;
state.sessionViewTab = tab;
if (tab === "debug" && state.selectedDebugSequence == null && filteredDebugLogs.length) {
state.selectedDebugSequence = filteredDebugLogs[0].sequence;
}
renderSessionView();
});
}
const debugFilterInput = tableEl.querySelector("#debug-filter");
if (debugFilterInput) {
debugFilterInput.addEventListener("input", () => {
state.debugFilterDraft = debugFilterInput.value || "";
clearDebugFilterDebounce();
state.debugFilterDebounceTimer = window.setTimeout(() => {
state.debugFilterDebounceTimer = null;
state.debugFilter = state.debugFilterDraft || "";
renderSessionView();
}, 350);
});
}
const timelineFilterInput = tableEl.querySelector("#timeline-filter");
if (timelineFilterInput) {
timelineFilterInput.addEventListener("input", () => {
state.timelineFilterDraft = timelineFilterInput.value || "";
clearTimelineFilterDebounce();
state.timelineFilterDebounceTimer = window.setTimeout(() => {
state.timelineFilterDebounceTimer = null;
state.timelineFilter = state.timelineFilterDraft || "";
renderSessionView();
}, 350);
});
}
for (const row of tableEl.querySelectorAll("tr[data-debug-seq]")) {
row.addEventListener("click", () => {
const rawSeq = row.getAttribute("data-debug-seq");
const seq = rawSeq ? Number.parseInt(rawSeq, 10) : NaN;
if (!Number.isFinite(seq)) return;
state.selectedDebugSequence = seq;
renderSessionView();
});
}
restoreFocusedFilterInput(focusedFilterInput);
}
async function openSessionViewer(sessionId) {
if (!sessionId) return;
clearDebugFilterDebounce();
clearTimelineFilterDebounce();
state.viewMode = "session";
state.selectedSessionId = sessionId;
state.sessionViewLoading = true;
state.sessionViewTab = "timeline";
state.timelineFilter = "";
state.timelineFilterDraft = "";
state.debugFilter = "";
state.debugFilterDraft = "";
state.selectedDebugSequence = null;
render();
try {
const response = await fetch("/api/session/content?sessionId=" + encodeURIComponent(sessionId));
const data = await response.json();
if (!response.ok) {
throw new Error((data && data.error) || ("HTTP " + response.status));
}
state.sessionContentById[sessionId] = data || null;
} catch (error) {
errorEl.textContent = error instanceof Error ? error.message : "Unable to load session content.";
} finally {
state.sessionViewLoading = false;
render();
}
}
function scheduleRetry() {
if (state.retryTimer) return;
state.retryTimer = window.setTimeout(() => {
state.retryTimer = null;
loadSessions();
}, 1200);
}
function render() {
detailsEl.innerHTML = "";
if (state.viewMode === "session" && state.selectedSessionId) {
if (toolbarEl) toolbarEl.style.display = "none";
renderSessionView();
return;
}
if (toolbarEl) toolbarEl.style.display = "";
const rows = getFilteredSessions();
const filteredCount = rows.length;
const totalCount = state.sessions.length;
statusEl.textContent =
"Showing " + filteredCount + " of " + totalCount + " sessions" + (state.source ? " via " + state.source : "");
if (rows.length === 0) {
tableEl.innerHTML = '<div class="empty">No sessions match your search.</div>';
return;
}
tableEl.innerHTML = \`
<table>
<thead>
<tr>
<th>Name / Summary</th>
<th>Modified</th>
<th>Context</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
\${rows
.map((s) => {
const displayName = s.name || s.summary || "(unnamed session)";
const summary = s.summary && s.summary !== s.name ? \`<div class="meta">\${escapeHtml(s.summary)}</div>\` : "";
const badges = [
s.isCurrent ? '<span class="badge badge-current">Current</span>' : "",
s.isActive ? '<span class="badge badge-active">Active</span>' : "",
].filter(Boolean).join("");
const badgeMarkup = badges ? \`<div class="badges">\${badges}</div>\` : "";
const context = [s.repository, s.branch, s.cwd].filter(Boolean).join(" • ");
const remote = s.isRemote ? " (remote)" : "";
return \`
<tr class="\${s.isActive ? "active-session" : ""}">
<td>
<button
type="button"
class="name session-open"
data-open-id="\${escapeHtml(s.sessionId)}"
title="Open session viewer"
>\${escapeHtml(displayName)}\${remote}</button>
\${summary}
\${badgeMarkup}
</td>
<td>\${escapeHtml(formatTime(s.modifiedTime || s.startTime))}</td>
<td>\${escapeHtml(context)}</td>
<td>
<div class="actions">
<button
type="button"
class="icon-button"
data-copy-id="\${escapeHtml(s.sessionId)}"
aria-label="Copy session ID"
title="Copy session ID"
>⧉</button>
<button
type="button"
class="icon-button icon-delete"
data-delete-id="\${escapeHtml(s.sessionId)}"
aria-label="Delete session"
title="Delete session"
>🗑</button>
</div>
</td>
</tr>
\`;
})
.join("")}
</tbody>
</table>
\`;
for (const button of tableEl.querySelectorAll("button[data-copy-id]")) {
button.addEventListener("click", async () => {
const sessionId = button.getAttribute("data-copy-id");
if (!sessionId) return;
await copySessionId(sessionId, button);
});
}
for (const button of tableEl.querySelectorAll("button[data-open-id]")) {
button.addEventListener("click", async () => {
const sessionId = button.getAttribute("data-open-id");
if (!sessionId) return;
await openSessionViewer(sessionId);
});
}
for (const button of tableEl.querySelectorAll("button[data-delete-id]")) {
button.addEventListener("click", async () => {
const sessionId = button.getAttribute("data-delete-id");
if (!sessionId) return;
const ok = window.confirm("Delete session " + sessionId + "? This cannot be undone.");
if (!ok) return;
await deleteSession(sessionId, button);
});
}
}
async function copySessionId(sessionId, button) {
errorEl.textContent = "";
const prior = button.textContent;
try {
if (navigator.clipboard && navigator.clipboard.writeText) {
await navigator.clipboard.writeText(sessionId);
} else {
const textArea = document.createElement("textarea");
textArea.value = sessionId;
textArea.setAttribute("readonly", "");
textArea.style.position = "absolute";
textArea.style.left = "-9999px";
document.body.appendChild(textArea);
textArea.select();
document.execCommand("copy");
document.body.removeChild(textArea);
}
button.textContent = "✓";
window.setTimeout(() => {
button.textContent = prior;
}, 900);
} catch {
errorEl.textContent = "Unable to copy session ID.";
}
}
async function loadSessions() {
errorEl.textContent = "";
setLoading(true);
if (state.retryTimer) {
window.clearTimeout(state.retryTimer);
state.retryTimer = null;
}
try {
const response = await fetch("/api/sessions");
const data = await response.json();
if (!response.ok) {
if (data && data.retryable) {
statusEl.textContent = data.error || "Waiting for Copilot to become idle...";
if (!state.sessions.length) {
tableEl.innerHTML = '<div class="empty">Waiting to load sessions...</div>';
}
scheduleRetry();
return;
}
throw new Error((data && data.error) || ("HTTP " + response.status));
}
state.sessions = Array.isArray(data.sessions) ? data.sessions : [];
state.source = data.source || "";
if (state.selectedSessionId && !state.sessions.some((s) => s.sessionId === state.selectedSessionId)) {
state.selectedSessionId = null;
state.viewMode = "list";
}
render();
} catch (error) {
errorEl.textContent = "Unable to load sessions right now.";
statusEl.textContent = "Failed to load sessions.";
if (!state.sessions.length) {
tableEl.innerHTML = '<div class="empty">No sessions available yet.</div>';
}
} finally {
setLoading(false);
}
}
async function deleteSession(sessionId, button) {
errorEl.textContent = "";
const prior = button.textContent;
button.disabled = true;
button.textContent = "⋯";
try {
const response = await fetch("/api/delete", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ sessionId }),
});
const data = await response.json();
if (!response.ok) {
throw new Error((data && data.error) || ("HTTP " + response.status));
}
state.sessions = Array.isArray(data.sessions) ? data.sessions : state.sessions;
state.source = data.source || state.source;
delete state.sessionContentById[sessionId];
if (state.selectedSessionId === sessionId) {
state.selectedSessionId = null;
state.viewMode = "list";
}
render();
} catch (error) {
errorEl.textContent = error instanceof Error ? error.message : "Unable to delete session.";
button.disabled = false;
button.textContent = prior;
}
}
refreshButton.addEventListener("click", () => loadSessions());
searchInput.addEventListener("input", () => render());
loadSessions();
</script>
</body>
</html>`;
}
async function startServer(instanceId, session) {
const server = createServer(async (req, res) => {
try {
const method = req.method ?? "GET";
const url = new URL(req.url ?? "/", "http://127.0.0.1");
if (method === "GET" && url.pathname === "/") {
res.statusCode = 200;
res.setHeader("Content-Type", "text/html; charset=utf-8");
res.end(renderHtml(instanceId));
return;
}
if (method === "GET" && url.pathname === "/api/sessions") {
try {
const result = await listChronicleSessions(session);
sendJson(res, 200, result);
} catch (error) {
if (error instanceof CanvasError && error.code === "session_busy") {
sendJson(res, 409, { error: error.message, retryable: true });
} else {
throw error;
}
}
return;
}
if (method === "GET" && url.pathname === "/api/session") {
const sessionId = url.searchParams.get("sessionId") ?? "";
try {
const details = await getSessionDetailsFromSessionState(session, sessionId);
sendJson(res, 200, { session: details });
} catch (error) {
if (error instanceof CanvasError) {
if (error.code === "invalid_session_id") {
sendJson(res, 400, { error: "A valid session ID is required." });
} else if (error.code === "session_not_found") {
sendJson(res, 404, { error: error.message });
} else {
sendJson(res, 500, { error: "Unable to load session details." });
}
} else {
throw error;
}
}
return;
}
if (method === "GET" && url.pathname === "/api/session/content") {
const sessionId = url.searchParams.get("sessionId") ?? "";
try {
const content = await getSessionContentFromSessionState(session, sessionId);
sendJson(res, 200, content);
} catch (error) {
if (error instanceof CanvasError) {
if (error.code === "invalid_session_id") {
sendJson(res, 400, { error: "A valid session ID is required." });
} else if (error.code === "session_not_found") {
sendJson(res, 404, { error: error.message });
} else {
sendJson(res, 500, { error: "Unable to load session content." });
}
} else {
throw error;
}
}
return;
}
if (method === "POST" && url.pathname === "/api/delete") {
const body = await readJsonBody(req);
const sessionId = typeof body.sessionId === "string" ? body.sessionId : "";
try {
await deleteSessionViaChronicle(session, sessionId);
const updated = await listChronicleSessions(session);
sendJson(res, 200, updated);
} catch (error) {
if (error instanceof CanvasError) {
if (error.code === "session_busy") {
sendJson(res, 409, { error: error.message, retryable: true });
} else if (error.code === "invalid_session_id") {
sendJson(res, 400, { error: "A valid session ID is required." });
} else if (error.code === "cannot_delete_current_session") {
sendJson(res, 400, { error: "You can't delete the currently active session." });
} else if (error.code === "session_in_use") {
sendJson(res, 409, { error: error.message, retryable: true });
} else if (error.code === "session_not_found") {
sendJson(res, 404, { error: error.message });
} else {
sendJson(res, 500, { error: "Unable to delete that session." });
}
} else {
throw error;
}
}
return;
}
sendJson(res, 404, { error: `Not found: ${escapeHtml(url.pathname)}` });
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
sendJson(res, 500, { error: message });
}
});
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
const address = server.address();
const port = typeof address === "object" && address ? address.port : 0;
return { server, url: `http://127.0.0.1:${port}/` };
}
const session = await joinSession({
canvases: [
createCanvas({
id: "chronicle-sessions",
displayName: "Copilot Session Manager",
description: "View, search, and delete old sessions from chronicle session listings.",
actions: [
{
name: "refresh_sessions",
description: "Refresh and return sessions from chronicle.",
handler: async () => {
try {
const result = await listChronicleSessions(session);
return { ok: true, ...result };
} catch (error) {
return {
ok: false,
error: error instanceof Error ? error.message : String(error),
};
}
},
},
{
name: "delete_session",
description: "Delete a session by id through chronicle command routing.",
inputSchema: {
type: "object",
properties: {
sessionId: { type: "string", description: "Session ID to delete." },
},
required: ["sessionId"],
additionalProperties: false,
},
handler: async (ctx) => {
const sessionId = typeof ctx.input?.sessionId === "string" ? ctx.input.sessionId : "";
try {
await deleteSessionViaChronicle(session, sessionId);
const result = await listChronicleSessions(session);
return { ok: true, ...result };
} catch (error) {
return {
ok: false,
error: error instanceof Error ? error.message : String(error),
};
}
},
},
],
open: async (ctx) => {
let entry = servers.get(ctx.instanceId);
if (!entry) {
entry = await startServer(ctx.instanceId, session);
servers.set(ctx.instanceId, entry);
}
return {
title: "Copilot Session Manager",
status: "Search, inspect, and delete sessions",
url: entry.url,
};
},
onClose: async (ctx) => {
const entry = servers.get(ctx.instanceId);
if (entry) {
servers.delete(ctx.instanceId);
await new Promise((resolve) => entry.server.close(() => resolve()));
}
},
}),
],
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment