Compare commits

...

1 Commits

Author SHA1 Message Date
abeatrix 85976d10cf feat(cli): filter history by file-based cron sessions
Add a --file-based history option and shared helpers to show only sessions started from file-based cron specs. Prefix matching history entries in CLI/TUI views so automated cron runs are easier to identify.
2026-05-20 19:11:43 -07:00
9 changed files with 75 additions and 12 deletions
+25 -9
View File
@@ -1,5 +1,6 @@
import { mkdir, writeFile } from "node:fs/promises";
import { dirname, resolve } from "node:path";
import type { SessionHistoryRecord } from "@cline/core";
import { generateConversationHTML } from "../session/export";
import {
deleteSession,
@@ -8,12 +9,15 @@ import {
updateSession,
} from "../session/session";
import { disableOpenTuiGraphicsProbe } from "../tui/opentui-env";
import { isFileBasedCronSession } from "../utils/history-format";
import { writeln } from "../utils/output";
import type { CliOutputMode } from "../utils/types";
export {
fileBasedCronPrefix,
formatCheckpointDetail,
formatHistoryListLine,
isFileBasedCronSession,
mergeHistoryStatusRows,
} from "../utils/history-format";
@@ -160,6 +164,7 @@ export async function runHistoryList(input: {
limit: number;
outputMode: CliOutputMode;
workspaceRoot?: string;
fileBasedOnly?: boolean;
io?: HistoryIo;
}): Promise<number | string> {
const io = input.io ?? {
@@ -168,15 +173,24 @@ export async function runHistoryList(input: {
};
const limit = Number.isFinite(input.limit) ? input.limit : 50;
const rows = await listSessions(limit, {
workspaceRoot: input.workspaceRoot,
hydrate: input.outputMode !== "json",
});
const applyFilter = (rows: SessionHistoryRecord[]) =>
input.fileBasedOnly ? rows.filter(isFileBasedCronSession) : rows;
const rows = applyFilter(
await listSessions(limit, {
workspaceRoot: input.workspaceRoot,
hydrate: input.outputMode !== "json",
}),
);
if (rows.length === 0) {
if (input.outputMode === "json") {
process.stdout.write(JSON.stringify([]));
} else {
io.writeln("No history found.");
io.writeln(
input.fileBasedOnly
? "No file-based cron sessions found."
: "No history found.",
);
}
return 0;
}
@@ -191,10 +205,12 @@ export async function runHistoryList(input: {
return await renderHistoryStandalone({
rows,
refreshRows: async () =>
await listSessions(limit, {
workspaceRoot: input.workspaceRoot,
hydrate: false,
}),
applyFilter(
await listSessions(limit, {
workspaceRoot: input.workspaceRoot,
hydrate: false,
}),
),
onExport: async (sessionId: string) =>
await exportHistorySession(sessionId, undefined),
});
+5
View File
@@ -362,6 +362,10 @@ export async function runCli(): Promise<void> {
.option("--json", "Output as JSON")
.option("--limit <count>", "Maximum number of sessions to show", "50")
.option("--page <number>", "Page number for paginated results")
.option(
"--file-based",
"Only show sessions started by file-based cron specs",
)
.option("--config <dir>", "configuration directory")
.action(async () => {
const opts = historyCmd.opts();
@@ -374,6 +378,7 @@ export async function runCli(): Promise<void> {
const result = await runHistoryList({
limit,
outputMode,
fileBasedOnly: opts.fileBased === true,
io,
});
if (typeof result === "string") {
+3 -1
View File
@@ -12,6 +12,7 @@ import { useDialogKeyboard } from "@opentui-ui/dialog/react";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { listSessions } from "../../session/session";
import {
fileBasedCronPrefix,
formatSessionStatusLabel,
mergeHistoryStatusRows,
} from "../../utils/history-format";
@@ -28,7 +29,8 @@ function formatTitle(row: SessionHistoryRecord, maxLen: number): string {
const raw = row.metadata?.title?.trim() || row.prompt?.trim() || "Untitled";
const forkTitle =
hasForkMetadata(row) && !raw.endsWith(" (fork)") ? `${raw} (fork)` : raw;
const normalized = formatDisplayUserInput(forkTitle);
const prefix = fileBasedCronPrefix(row) ?? "";
const normalized = formatDisplayUserInput(`${prefix}${forkTitle}`);
return truncateStr(normalized.replace(/\s+/g, " "), maxLen);
}
+18 -1
View File
@@ -42,6 +42,22 @@ function formatHistoryTitle(
return truncateStr(normalized.replace(/\s+/g, " "), 40);
}
export function isFileBasedCronSession(row: SessionHistoryRecord): boolean {
const cron = row.metadata?.cron;
if (!cron || typeof cron !== "object") return false;
const specPath = (cron as { specPath?: unknown }).specPath;
return typeof specPath === "string" && specPath.length > 0;
}
export function fileBasedCronPrefix(
row: SessionHistoryRecord,
): string | undefined {
if (!isFileBasedCronSession(row)) return undefined;
const specPath = (row.metadata?.cron as { specPath?: string }).specPath;
const base = specPath ? specPath.split("/").pop() : undefined;
return base ? `[file-based: ${base}] ` : "[file-based] ";
}
export function formatCheckpointBadge(
row: SessionHistoryRecord,
): string | undefined {
@@ -104,5 +120,6 @@ export function formatHistoryListLine(row: SessionHistoryRecord): string {
: new Date(row.startedAt).getTime();
const date = formatUtcDate(new Date(timestamp));
const costSegment = cost ? ` | ${cost}` : "";
return `${date} ${provider}:${model} [${formatSessionStatusLabel(row.status)}]${costSegment} | ${title}`;
const prefix = fileBasedCronPrefix(row) ?? "";
return `${date} ${provider}:${model} [${formatSessionStatusLabel(row.status)}]${costSegment} | ${prefix}${title}`;
}
@@ -104,6 +104,7 @@ export function createClineCoreAutomationRuntimeHandlers(
const started = await host.startSession({
source: request.source?.trim() || SessionSource.CLI,
interactive: false,
sessionMetadata: request.sessionMetadata,
config: {
providerId: normalizeProviderId(request.provider),
modelId: request.model,
@@ -470,13 +470,22 @@ export class CronRunner {
}
const mode =
spec.mode === "plan" ? "plan" : spec.mode === "act" ? "act" : "yolo";
const fileBasedMetadata = isFileBasedSpec(spec)
? {
cron: {
specId: spec.specId,
specPath: spec.sourcePath,
triggerKind: spec.triggerKind,
},
}
: undefined;
return {
workspaceRoot,
cwd,
provider,
model,
mode,
source: spec.source?.trim() || "user",
source: fileBasedMetadata ? "cron" : spec.source?.trim() || "user",
systemPrompt: await this.buildSystemPrompt(
spec,
workspaceRoot,
@@ -492,6 +501,12 @@ export class CronRunner {
configExtensions: DEFAULT_CRON_EXTENSIONS.filter((extension) =>
cronExtensionEnabled(spec, extension),
),
sessionMetadata: fileBasedMetadata,
};
}
}
function isFileBasedSpec(spec: CronSpecRecord): boolean {
const path = spec.sourcePath ?? "";
return path.length > 0 && !path.startsWith("hub/schedules/");
}
+1
View File
@@ -17,6 +17,7 @@ export const SessionSource = {
IDE: "ide",
JETBRAINS: "jetbrains",
NEOVIM: "neovim",
CRON: "cron",
UNKNOWN: "unknown",
} as const;
+5
View File
@@ -38,6 +38,11 @@ export interface SessionHistoryMetadata extends Record<string, unknown> {
runCount?: number;
}>;
};
cron?: {
specId?: string;
specPath?: string;
triggerKind?: "one_off" | "schedule" | "event";
};
}
export interface SessionHistoryRecord extends Omit<SessionRecord, "metadata"> {
+1
View File
@@ -38,6 +38,7 @@ export interface ChatStartSessionRequest extends ChatRuntimeConfig {
model: string;
source?: string;
interactive?: boolean;
sessionMetadata?: Record<string, unknown>;
}
export interface ChatStartSessionArtifacts {