mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-27 00:52:05 +00:00
Gateway: add compaction checkpoints
This commit is contained in:
@@ -79,7 +79,14 @@ import {
|
||||
import { loadLogs } from "./controllers/logs.ts";
|
||||
import { loadNodes } from "./controllers/nodes.ts";
|
||||
import { loadPresence } from "./controllers/presence.ts";
|
||||
import { deleteSessionsAndRefresh, loadSessions, patchSession } from "./controllers/sessions.ts";
|
||||
import {
|
||||
branchSessionFromCheckpoint,
|
||||
deleteSessionsAndRefresh,
|
||||
loadSessions,
|
||||
patchSession,
|
||||
restoreSessionFromCheckpoint,
|
||||
toggleSessionCompactionCheckpoints,
|
||||
} from "./controllers/sessions.ts";
|
||||
import {
|
||||
closeClawHubDetail,
|
||||
installFromClawHub,
|
||||
@@ -840,6 +847,11 @@ export function renderApp(state: AppViewState) {
|
||||
page: state.sessionsPage,
|
||||
pageSize: state.sessionsPageSize,
|
||||
selectedKeys: state.sessionsSelectedKeys,
|
||||
expandedCheckpointKey: state.sessionsExpandedCheckpointKey,
|
||||
checkpointItemsByKey: state.sessionsCheckpointItemsByKey,
|
||||
checkpointLoadingKey: state.sessionsCheckpointLoadingKey,
|
||||
checkpointBusyKey: state.sessionsCheckpointBusyKey,
|
||||
checkpointErrorByKey: state.sessionsCheckpointErrorByKey,
|
||||
onFiltersChange: (next) => {
|
||||
state.sessionsFilterActive = next.activeMinutes;
|
||||
state.sessionsFilterLimit = next.limit;
|
||||
@@ -905,6 +917,21 @@ export function renderApp(state: AppViewState) {
|
||||
switchChatSession(state, sessionKey);
|
||||
state.setTab("chat" as import("./navigation.ts").Tab);
|
||||
},
|
||||
onToggleCheckpointDetails: (sessionKey) =>
|
||||
toggleSessionCompactionCheckpoints(state, sessionKey),
|
||||
onBranchFromCheckpoint: async (sessionKey, checkpointId) => {
|
||||
const nextKey = await branchSessionFromCheckpoint(
|
||||
state,
|
||||
sessionKey,
|
||||
checkpointId,
|
||||
);
|
||||
if (nextKey) {
|
||||
switchChatSession(state, nextKey);
|
||||
state.setTab("chat" as import("./navigation.ts").Tab);
|
||||
}
|
||||
},
|
||||
onRestoreCheckpoint: (sessionKey, checkpointId) =>
|
||||
restoreSessionFromCheckpoint(state, sessionKey, checkpointId),
|
||||
}),
|
||||
)
|
||||
: nothing}
|
||||
|
||||
@@ -209,6 +209,11 @@ export type AppViewState = {
|
||||
sessionsPage: number;
|
||||
sessionsPageSize: number;
|
||||
sessionsSelectedKeys: Set<string>;
|
||||
sessionsExpandedCheckpointKey: string | null;
|
||||
sessionsCheckpointItemsByKey: Record<string, import("./types.ts").SessionCompactionCheckpoint[]>;
|
||||
sessionsCheckpointLoadingKey: string | null;
|
||||
sessionsCheckpointBusyKey: string | null;
|
||||
sessionsCheckpointErrorByKey: Record<string, string>;
|
||||
usageLoading: boolean;
|
||||
usageResult: SessionsUsageResult | null;
|
||||
usageCostSummary: CostUsageSummary | null;
|
||||
|
||||
@@ -89,6 +89,7 @@ import type {
|
||||
ModelCatalogEntry,
|
||||
PresenceEntry,
|
||||
ChannelsStatusSnapshot,
|
||||
SessionCompactionCheckpoint,
|
||||
SessionsListResult,
|
||||
SkillStatusReport,
|
||||
StatusSummary,
|
||||
@@ -312,6 +313,11 @@ export class OpenClawApp extends LitElement {
|
||||
@state() sessionsPage = 0;
|
||||
@state() sessionsPageSize = 25;
|
||||
@state() sessionsSelectedKeys: Set<string> = new Set();
|
||||
@state() sessionsExpandedCheckpointKey: string | null = null;
|
||||
@state() sessionsCheckpointItemsByKey: Record<string, SessionCompactionCheckpoint[]> = {};
|
||||
@state() sessionsCheckpointLoadingKey: string | null = null;
|
||||
@state() sessionsCheckpointBusyKey: string | null = null;
|
||||
@state() sessionsCheckpointErrorByKey: Record<string, string> = {};
|
||||
|
||||
@state() usageLoading = false;
|
||||
@state() usageResult: import("./types.js").SessionsUsageResult | null = null;
|
||||
|
||||
@@ -146,8 +146,24 @@ async function executeCompact(
|
||||
sessionKey: string,
|
||||
): Promise<SlashCommandResult> {
|
||||
try {
|
||||
await client.request("sessions.compact", { key: sessionKey });
|
||||
return { content: "Context compacted successfully.", action: "refresh" };
|
||||
const result = await client.request<{
|
||||
compacted?: boolean;
|
||||
reason?: string;
|
||||
result?: { tokensBefore?: number; tokensAfter?: number };
|
||||
}>("sessions.compact", { key: sessionKey });
|
||||
if (result?.compacted) {
|
||||
const before = result.result?.tokensBefore;
|
||||
const after = result.result?.tokensAfter;
|
||||
const tokenSummary =
|
||||
typeof before === "number" && typeof after === "number"
|
||||
? ` (${before.toLocaleString()} -> ${after.toLocaleString()} tokens)`
|
||||
: "";
|
||||
return { content: `Context compacted successfully${tokenSummary}.`, action: "refresh" };
|
||||
}
|
||||
if (typeof result?.reason === "string" && result.reason.trim()) {
|
||||
return { content: `Compaction skipped: ${result.reason}`, action: "refresh" };
|
||||
}
|
||||
return { content: "Compaction skipped.", action: "refresh" };
|
||||
} catch (err) {
|
||||
return { content: `Compaction failed: ${String(err)}` };
|
||||
}
|
||||
|
||||
@@ -1,6 +1,12 @@
|
||||
import { toNumber } from "../format.ts";
|
||||
import type { GatewayBrowserClient } from "../gateway.ts";
|
||||
import type { SessionsListResult } from "../types.ts";
|
||||
import type {
|
||||
SessionCompactionCheckpoint,
|
||||
SessionsCompactionBranchResult,
|
||||
SessionsCompactionListResult,
|
||||
SessionsCompactionRestoreResult,
|
||||
SessionsListResult,
|
||||
} from "../types.ts";
|
||||
import {
|
||||
formatMissingOperatorReadScopeMessage,
|
||||
isMissingOperatorReadScopeError,
|
||||
@@ -16,6 +22,11 @@ export type SessionsState = {
|
||||
sessionsFilterLimit: string;
|
||||
sessionsIncludeGlobal: boolean;
|
||||
sessionsIncludeUnknown: boolean;
|
||||
sessionsExpandedCheckpointKey: string | null;
|
||||
sessionsCheckpointItemsByKey: Record<string, SessionCompactionCheckpoint[]>;
|
||||
sessionsCheckpointLoadingKey: string | null;
|
||||
sessionsCheckpointBusyKey: string | null;
|
||||
sessionsCheckpointErrorByKey: Record<string, string>;
|
||||
};
|
||||
|
||||
export async function subscribeSessions(state: SessionsState) {
|
||||
@@ -156,3 +167,106 @@ export async function deleteSessionsAndRefresh(
|
||||
}
|
||||
return deleted;
|
||||
}
|
||||
|
||||
export async function toggleSessionCompactionCheckpoints(state: SessionsState, key: string) {
|
||||
const trimmedKey = key.trim();
|
||||
if (!trimmedKey) {
|
||||
return;
|
||||
}
|
||||
if (state.sessionsExpandedCheckpointKey === trimmedKey) {
|
||||
state.sessionsExpandedCheckpointKey = null;
|
||||
return;
|
||||
}
|
||||
state.sessionsExpandedCheckpointKey = trimmedKey;
|
||||
if (state.sessionsCheckpointItemsByKey[trimmedKey]) {
|
||||
return;
|
||||
}
|
||||
state.sessionsCheckpointLoadingKey = trimmedKey;
|
||||
state.sessionsCheckpointErrorByKey = {
|
||||
...state.sessionsCheckpointErrorByKey,
|
||||
[trimmedKey]: "",
|
||||
};
|
||||
try {
|
||||
const result = await state.client?.request<SessionsCompactionListResult>(
|
||||
"sessions.compaction.list",
|
||||
{ key: trimmedKey },
|
||||
);
|
||||
if (result) {
|
||||
state.sessionsCheckpointItemsByKey = {
|
||||
...state.sessionsCheckpointItemsByKey,
|
||||
[trimmedKey]: result.checkpoints ?? [],
|
||||
};
|
||||
}
|
||||
} catch (err) {
|
||||
state.sessionsCheckpointErrorByKey = {
|
||||
...state.sessionsCheckpointErrorByKey,
|
||||
[trimmedKey]: String(err),
|
||||
};
|
||||
} finally {
|
||||
if (state.sessionsCheckpointLoadingKey === trimmedKey) {
|
||||
state.sessionsCheckpointLoadingKey = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function branchSessionFromCheckpoint(
|
||||
state: SessionsState,
|
||||
key: string,
|
||||
checkpointId: string,
|
||||
): Promise<string | null> {
|
||||
if (!state.client || !state.connected) {
|
||||
return null;
|
||||
}
|
||||
const confirmed = window.confirm(
|
||||
"Create a new child session from this pre-compaction checkpoint?",
|
||||
);
|
||||
if (!confirmed) {
|
||||
return null;
|
||||
}
|
||||
state.sessionsCheckpointBusyKey = checkpointId;
|
||||
try {
|
||||
const result = await state.client.request<SessionsCompactionBranchResult>(
|
||||
"sessions.compaction.branch",
|
||||
{ key, checkpointId },
|
||||
);
|
||||
await loadSessions(state);
|
||||
return result?.key ?? null;
|
||||
} catch (err) {
|
||||
state.sessionsError = String(err);
|
||||
return null;
|
||||
} finally {
|
||||
if (state.sessionsCheckpointBusyKey === checkpointId) {
|
||||
state.sessionsCheckpointBusyKey = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function restoreSessionFromCheckpoint(
|
||||
state: SessionsState,
|
||||
key: string,
|
||||
checkpointId: string,
|
||||
) {
|
||||
if (!state.client || !state.connected) {
|
||||
return;
|
||||
}
|
||||
const confirmed = window.confirm(
|
||||
"Restore this session to the selected pre-compaction checkpoint?\n\nThis replaces the current active transcript for the session key.",
|
||||
);
|
||||
if (!confirmed) {
|
||||
return;
|
||||
}
|
||||
state.sessionsCheckpointBusyKey = checkpointId;
|
||||
try {
|
||||
await state.client.request<SessionsCompactionRestoreResult>("sessions.compaction.restore", {
|
||||
key,
|
||||
checkpointId,
|
||||
});
|
||||
await loadSessions(state);
|
||||
} catch (err) {
|
||||
state.sessionsError = String(err);
|
||||
} finally {
|
||||
if (state.sessionsCheckpointBusyKey === checkpointId) {
|
||||
state.sessionsCheckpointBusyKey = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -369,6 +369,33 @@ export type AgentsFilesSetResult = {
|
||||
|
||||
export type SessionRunStatus = "running" | "done" | "failed" | "killed" | "timeout";
|
||||
|
||||
export type SessionCompactionCheckpointReason =
|
||||
| "manual"
|
||||
| "auto-threshold"
|
||||
| "overflow-retry"
|
||||
| "timeout-retry";
|
||||
|
||||
export type SessionCompactionTranscriptReference = {
|
||||
sessionId: string;
|
||||
sessionFile?: string;
|
||||
leafId?: string;
|
||||
entryId?: string;
|
||||
};
|
||||
|
||||
export type SessionCompactionCheckpoint = {
|
||||
checkpointId: string;
|
||||
sessionKey: string;
|
||||
sessionId: string;
|
||||
createdAt: number;
|
||||
reason: SessionCompactionCheckpointReason;
|
||||
tokensBefore?: number;
|
||||
tokensAfter?: number;
|
||||
summary?: string;
|
||||
firstKeptEntryId?: string;
|
||||
preCompaction: SessionCompactionTranscriptReference;
|
||||
postCompaction: SessionCompactionTranscriptReference;
|
||||
};
|
||||
|
||||
export type GatewaySessionRow = {
|
||||
key: string;
|
||||
spawnedBy?: string;
|
||||
@@ -400,10 +427,47 @@ export type GatewaySessionRow = {
|
||||
model?: string;
|
||||
modelProvider?: string;
|
||||
contextTokens?: number;
|
||||
compactionCheckpointCount?: number;
|
||||
latestCompactionCheckpoint?: SessionCompactionCheckpoint;
|
||||
};
|
||||
|
||||
export type SessionsListResult = SessionsListResultBase<GatewaySessionsDefaults, GatewaySessionRow>;
|
||||
|
||||
export type SessionsCompactionListResult = {
|
||||
ok: true;
|
||||
key: string;
|
||||
checkpoints: SessionCompactionCheckpoint[];
|
||||
};
|
||||
|
||||
export type SessionsCompactionGetResult = {
|
||||
ok: true;
|
||||
key: string;
|
||||
checkpoint: SessionCompactionCheckpoint;
|
||||
};
|
||||
|
||||
export type SessionsCompactionBranchResult = {
|
||||
ok: true;
|
||||
sourceKey: string;
|
||||
key: string;
|
||||
sessionId: string;
|
||||
checkpoint: SessionCompactionCheckpoint;
|
||||
entry: {
|
||||
sessionId: string;
|
||||
updatedAt: number;
|
||||
} & Record<string, unknown>;
|
||||
};
|
||||
|
||||
export type SessionsCompactionRestoreResult = {
|
||||
ok: true;
|
||||
key: string;
|
||||
sessionId: string;
|
||||
checkpoint: SessionCompactionCheckpoint;
|
||||
entry: {
|
||||
sessionId: string;
|
||||
updatedAt: number;
|
||||
} & Record<string, unknown>;
|
||||
};
|
||||
|
||||
export type SessionsPatchResult = SessionsPatchResultBase<{
|
||||
sessionId: string;
|
||||
updatedAt?: number;
|
||||
|
||||
@@ -4,7 +4,11 @@ import { formatRelativeTimestamp } from "../format.ts";
|
||||
import { icons } from "../icons.ts";
|
||||
import { pathForTab } from "../navigation.ts";
|
||||
import { formatSessionTokens } from "../presenter.ts";
|
||||
import type { GatewaySessionRow, SessionsListResult } from "../types.ts";
|
||||
import type {
|
||||
GatewaySessionRow,
|
||||
SessionCompactionCheckpoint,
|
||||
SessionsListResult,
|
||||
} from "../types.ts";
|
||||
|
||||
export type SessionsProps = {
|
||||
loading: boolean;
|
||||
@@ -21,6 +25,11 @@ export type SessionsProps = {
|
||||
page: number;
|
||||
pageSize: number;
|
||||
selectedKeys: Set<string>;
|
||||
expandedCheckpointKey: string | null;
|
||||
checkpointItemsByKey: Record<string, SessionCompactionCheckpoint[]>;
|
||||
checkpointLoadingKey: string | null;
|
||||
checkpointBusyKey: string | null;
|
||||
checkpointErrorByKey: Record<string, string>;
|
||||
onFiltersChange: (next: {
|
||||
activeMinutes: string;
|
||||
limit: string;
|
||||
@@ -48,6 +57,9 @@ export type SessionsProps = {
|
||||
onDeselectAll: () => void;
|
||||
onDeleteSelected: () => void;
|
||||
onNavigateToChat?: (sessionKey: string) => void;
|
||||
onToggleCheckpointDetails: (sessionKey: string) => void;
|
||||
onBranchFromCheckpoint: (sessionKey: string, checkpointId: string) => void | Promise<void>;
|
||||
onRestoreCheckpoint: (sessionKey: string, checkpointId: string) => void | Promise<void>;
|
||||
};
|
||||
|
||||
const THINK_LEVELS = ["", "off", "minimal", "low", "medium", "high", "xhigh"] as const;
|
||||
@@ -182,6 +194,36 @@ function paginateRows<T>(rows: T[], page: number, pageSize: number): T[] {
|
||||
return rows.slice(start, start + pageSize);
|
||||
}
|
||||
|
||||
function formatCheckpointReason(reason: SessionCompactionCheckpoint["reason"]): string {
|
||||
switch (reason) {
|
||||
case "manual":
|
||||
return "manual";
|
||||
case "auto-threshold":
|
||||
return "auto-threshold";
|
||||
case "overflow-retry":
|
||||
return "overflow retry";
|
||||
case "timeout-retry":
|
||||
return "timeout retry";
|
||||
default:
|
||||
return reason;
|
||||
}
|
||||
}
|
||||
|
||||
function formatCheckpointDelta(checkpoint: SessionCompactionCheckpoint): string {
|
||||
if (
|
||||
typeof checkpoint.tokensBefore === "number" &&
|
||||
typeof checkpoint.tokensAfter === "number" &&
|
||||
Number.isFinite(checkpoint.tokensBefore) &&
|
||||
Number.isFinite(checkpoint.tokensAfter)
|
||||
) {
|
||||
return `${checkpoint.tokensBefore.toLocaleString()} → ${checkpoint.tokensAfter.toLocaleString()} tokens`;
|
||||
}
|
||||
if (typeof checkpoint.tokensBefore === "number" && Number.isFinite(checkpoint.tokensBefore)) {
|
||||
return `${checkpoint.tokensBefore.toLocaleString()} tokens before`;
|
||||
}
|
||||
return "token delta unavailable";
|
||||
}
|
||||
|
||||
export function renderSessions(props: SessionsProps) {
|
||||
const rawRows = props.result?.sessions ?? [];
|
||||
const filtered = filterRows(rawRows, props.searchQuery);
|
||||
@@ -349,6 +391,7 @@ export function renderSessions(props: SessionsProps) {
|
||||
<th>Label</th>
|
||||
${sortHeader("kind", "Kind")} ${sortHeader("updated", "Updated")}
|
||||
${sortHeader("tokens", "Tokens")}
|
||||
<th>Compaction</th>
|
||||
<th>Thinking</th>
|
||||
<th>Fast</th>
|
||||
<th>Verbose</th>
|
||||
@@ -360,24 +403,14 @@ export function renderSessions(props: SessionsProps) {
|
||||
? html`
|
||||
<tr>
|
||||
<td
|
||||
colspan="10"
|
||||
colspan="11"
|
||||
style="text-align: center; padding: 48px 16px; color: var(--muted)"
|
||||
>
|
||||
No sessions found.
|
||||
</td>
|
||||
</tr>
|
||||
`
|
||||
: paginated.map((row) =>
|
||||
renderRow(
|
||||
row,
|
||||
props.basePath,
|
||||
props.onPatch,
|
||||
props.selectedKeys.has(row.key),
|
||||
props.onToggleSelect,
|
||||
props.loading,
|
||||
props.onNavigateToChat,
|
||||
),
|
||||
)}
|
||||
: paginated.flatMap((row) => renderRows(row, props))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
@@ -416,15 +449,7 @@ export function renderSessions(props: SessionsProps) {
|
||||
`;
|
||||
}
|
||||
|
||||
function renderRow(
|
||||
row: GatewaySessionRow,
|
||||
basePath: string,
|
||||
onPatch: SessionsProps["onPatch"],
|
||||
selected: boolean,
|
||||
onToggleSelect: SessionsProps["onToggleSelect"],
|
||||
disabled: boolean,
|
||||
onNavigateToChat?: (sessionKey: string) => void,
|
||||
) {
|
||||
function renderRows(row: GatewaySessionRow, props: SessionsProps) {
|
||||
const updated = row.updatedAt ? formatRelativeTimestamp(row.updatedAt) : t("common.na");
|
||||
const rawThinking = row.thinkingLevel ?? "";
|
||||
const isBinaryThinking = isBinaryThinkingProvider(row.modelProvider);
|
||||
@@ -436,6 +461,11 @@ function renderRow(
|
||||
const verboseLevels = withCurrentLabeledOption(VERBOSE_LEVELS, verbose);
|
||||
const reasoning = row.reasoningLevel ?? "";
|
||||
const reasoningLevels = withCurrentOption(REASONING_LEVELS, reasoning);
|
||||
const latestCheckpoint = row.latestCompactionCheckpoint;
|
||||
const checkpointCount = row.compactionCheckpointCount ?? 0;
|
||||
const isExpanded = props.expandedCheckpointKey === row.key;
|
||||
const checkpointItems = props.checkpointItemsByKey[row.key] ?? [];
|
||||
const checkpointError = props.checkpointErrorByKey[row.key];
|
||||
const displayName =
|
||||
typeof row.displayName === "string" && row.displayName.trim().length > 0
|
||||
? row.displayName.trim()
|
||||
@@ -447,7 +477,7 @@ function renderRow(
|
||||
);
|
||||
const canLink = row.kind !== "global";
|
||||
const chatUrl = canLink
|
||||
? `${pathForTab("chat", basePath)}?session=${encodeURIComponent(row.key)}`
|
||||
? `${pathForTab("chat", props.basePath)}?session=${encodeURIComponent(row.key)}`
|
||||
: null;
|
||||
const badgeClass =
|
||||
row.kind === "direct"
|
||||
@@ -458,13 +488,13 @@ function renderRow(
|
||||
? "data-table-badge--global"
|
||||
: "data-table-badge--unknown";
|
||||
|
||||
return html`
|
||||
<tr>
|
||||
return [
|
||||
html`<tr>
|
||||
<td class="data-table-checkbox-col">
|
||||
<input
|
||||
type="checkbox"
|
||||
.checked=${selected}
|
||||
@change=${() => onToggleSelect(row.key)}
|
||||
.checked=${props.selectedKeys.has(row.key)}
|
||||
@change=${() => props.onToggleSelect(row.key)}
|
||||
aria-label="Select session"
|
||||
/>
|
||||
</td>
|
||||
@@ -485,9 +515,9 @@ function renderRow(
|
||||
) {
|
||||
return;
|
||||
}
|
||||
if (onNavigateToChat) {
|
||||
if (props.onNavigateToChat) {
|
||||
e.preventDefault();
|
||||
onNavigateToChat(row.key);
|
||||
props.onNavigateToChat(row.key);
|
||||
}
|
||||
}}
|
||||
>${row.key}</a
|
||||
@@ -501,12 +531,12 @@ function renderRow(
|
||||
<td>
|
||||
<input
|
||||
.value=${row.label ?? ""}
|
||||
?disabled=${disabled}
|
||||
?disabled=${props.loading}
|
||||
placeholder="(optional)"
|
||||
style="width: 100%; max-width: 140px; padding: 6px 10px; font-size: 13px; border: 1px solid var(--border); border-radius: var(--radius-sm);"
|
||||
@change=${(e: Event) => {
|
||||
const value = (e.target as HTMLInputElement).value.trim();
|
||||
onPatch(row.key, { label: value || null });
|
||||
props.onPatch(row.key, { label: value || null });
|
||||
}}
|
||||
/>
|
||||
</td>
|
||||
@@ -515,13 +545,37 @@ function renderRow(
|
||||
</td>
|
||||
<td>${updated}</td>
|
||||
<td>${formatSessionTokens(row)}</td>
|
||||
<td>
|
||||
<div style="display: grid; gap: 6px;">
|
||||
<span class="muted" style="font-size: 12px;">
|
||||
${checkpointCount > 0
|
||||
? `${checkpointCount} checkpoint${checkpointCount === 1 ? "" : "s"}`
|
||||
: "none"}
|
||||
</span>
|
||||
${latestCheckpoint
|
||||
? html`
|
||||
<span style="font-size: 12px;">
|
||||
${formatCheckpointReason(latestCheckpoint.reason)} ·
|
||||
${formatRelativeTimestamp(latestCheckpoint.createdAt)}
|
||||
</span>
|
||||
`
|
||||
: nothing}
|
||||
<button
|
||||
class="btn btn--sm"
|
||||
?disabled=${props.checkpointLoadingKey === row.key}
|
||||
@click=${() => props.onToggleCheckpointDetails(row.key)}
|
||||
>
|
||||
${isExpanded ? "Hide checkpoints" : "Show checkpoints"}
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<select
|
||||
?disabled=${disabled}
|
||||
?disabled=${props.loading}
|
||||
style="padding: 6px 10px; font-size: 13px; border: 1px solid var(--border); border-radius: var(--radius-sm); min-width: 90px;"
|
||||
@change=${(e: Event) => {
|
||||
const value = (e.target as HTMLSelectElement).value;
|
||||
onPatch(row.key, {
|
||||
props.onPatch(row.key, {
|
||||
thinkingLevel: resolveThinkLevelPatchValue(value, isBinaryThinking),
|
||||
});
|
||||
}}
|
||||
@@ -536,11 +590,11 @@ function renderRow(
|
||||
</td>
|
||||
<td>
|
||||
<select
|
||||
?disabled=${disabled}
|
||||
?disabled=${props.loading}
|
||||
style="padding: 6px 10px; font-size: 13px; border: 1px solid var(--border); border-radius: var(--radius-sm); min-width: 90px;"
|
||||
@change=${(e: Event) => {
|
||||
const value = (e.target as HTMLSelectElement).value;
|
||||
onPatch(row.key, { fastMode: value === "" ? null : value === "on" });
|
||||
props.onPatch(row.key, { fastMode: value === "" ? null : value === "on" });
|
||||
}}
|
||||
>
|
||||
${fastLevels.map(
|
||||
@@ -553,11 +607,11 @@ function renderRow(
|
||||
</td>
|
||||
<td>
|
||||
<select
|
||||
?disabled=${disabled}
|
||||
?disabled=${props.loading}
|
||||
style="padding: 6px 10px; font-size: 13px; border: 1px solid var(--border); border-radius: var(--radius-sm); min-width: 90px;"
|
||||
@change=${(e: Event) => {
|
||||
const value = (e.target as HTMLSelectElement).value;
|
||||
onPatch(row.key, { verboseLevel: value || null });
|
||||
props.onPatch(row.key, { verboseLevel: value || null });
|
||||
}}
|
||||
>
|
||||
${verboseLevels.map(
|
||||
@@ -570,11 +624,11 @@ function renderRow(
|
||||
</td>
|
||||
<td>
|
||||
<select
|
||||
?disabled=${disabled}
|
||||
?disabled=${props.loading}
|
||||
style="padding: 6px 10px; font-size: 13px; border: 1px solid var(--border); border-radius: var(--radius-sm); min-width: 90px;"
|
||||
@change=${(e: Event) => {
|
||||
const value = (e.target as HTMLSelectElement).value;
|
||||
onPatch(row.key, { reasoningLevel: value || null });
|
||||
props.onPatch(row.key, { reasoningLevel: value || null });
|
||||
}}
|
||||
>
|
||||
${reasoningLevels.map(
|
||||
@@ -585,6 +639,77 @@ function renderRow(
|
||||
)}
|
||||
</select>
|
||||
</td>
|
||||
</tr>
|
||||
`;
|
||||
</tr>`,
|
||||
...(isExpanded
|
||||
? [
|
||||
html`<tr>
|
||||
<td colspan="11" style="padding: 0;">
|
||||
<div
|
||||
style="padding: 14px 16px; border-top: 1px solid var(--border); background: var(--surface-2, rgba(127, 127, 127, 0.05));"
|
||||
>
|
||||
${props.checkpointLoadingKey === row.key
|
||||
? html`<div class="muted">Loading checkpoints…</div>`
|
||||
: checkpointError
|
||||
? html`<div class="callout danger">${checkpointError}</div>`
|
||||
: checkpointItems.length === 0
|
||||
? html`<div class="muted">
|
||||
No compaction checkpoints recorded for this session.
|
||||
</div>`
|
||||
: html`
|
||||
<div style="display: grid; gap: 10px;">
|
||||
${checkpointItems.map(
|
||||
(checkpoint) => html`
|
||||
<div
|
||||
style="border: 1px solid var(--border); border-radius: var(--radius-md); padding: 12px; display: grid; gap: 8px;"
|
||||
>
|
||||
<div
|
||||
style="display: flex; gap: 8px; justify-content: space-between; align-items: center; flex-wrap: wrap;"
|
||||
>
|
||||
<strong>
|
||||
${formatCheckpointReason(checkpoint.reason)} ·
|
||||
${formatRelativeTimestamp(checkpoint.createdAt)}
|
||||
</strong>
|
||||
<span class="muted" style="font-size: 12px;">
|
||||
${formatCheckpointDelta(checkpoint)}
|
||||
</span>
|
||||
</div>
|
||||
${checkpoint.summary
|
||||
? html`<div style="white-space: pre-wrap;">
|
||||
${checkpoint.summary}
|
||||
</div>`
|
||||
: html`<div class="muted">No summary captured.</div>`}
|
||||
<div style="display: flex; gap: 8px; flex-wrap: wrap;">
|
||||
<button
|
||||
class="btn btn--sm"
|
||||
?disabled=${props.checkpointBusyKey ===
|
||||
checkpoint.checkpointId}
|
||||
@click=${() =>
|
||||
props.onBranchFromCheckpoint(
|
||||
row.key,
|
||||
checkpoint.checkpointId,
|
||||
)}
|
||||
>
|
||||
Branch from checkpoint
|
||||
</button>
|
||||
<button
|
||||
class="btn btn--sm"
|
||||
?disabled=${props.checkpointBusyKey ===
|
||||
checkpoint.checkpointId}
|
||||
@click=${() =>
|
||||
props.onRestoreCheckpoint(row.key, checkpoint.checkpointId)}
|
||||
>
|
||||
Restore
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`,
|
||||
)}
|
||||
</div>
|
||||
`}
|
||||
</div>
|
||||
</td>
|
||||
</tr>`,
|
||||
]
|
||||
: []),
|
||||
];
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user