Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions packages/plugins/execution-history/src/api/group.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@ const RunActorCount = Schema.Struct({
const RunInteractionCounts = Schema.Struct({
withInteraction: Schema.Number,
withoutInteraction: Schema.Number,
formApproval: Schema.Number,
urlApproval: Schema.Number,
});
const RunChartBucket = Schema.Struct({
bucketStart: Schema.Number,
Expand Down Expand Up @@ -118,6 +120,7 @@ const ListRunsQuery = Schema.Struct({
from: Schema.optional(Schema.NumberFromString),
to: Schema.optional(Schema.NumberFromString),
interaction: Schema.optional(Schema.String),
approvalType: Schema.optional(Schema.Literals(["form", "url"])),
// Live-tail floor: only runs newer than this `startedAt`.
after: Schema.optional(Schema.NumberFromString),
sort: Schema.optional(Schema.Literals(["startedAt", "durationMs"])),
Expand Down
6 changes: 5 additions & 1 deletion packages/plugins/execution-history/src/api/handlers.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,8 @@ const runRow = (overrides: Partial<RunRow>): RunRow => ({
actorId: null,
actorLabel: null,
actorKind: null,
hadFormApproval: false,
hadUrlApproval: false,
startedAt: 1000,
completedAt: 2000,
durationMs: 1000,
Expand Down Expand Up @@ -129,7 +131,7 @@ describe("ExecutionHistoryHandlers", () => {

const res = yield* get(
web,
"http://localhost/execution-history/runs?status=completed,failed&trigger=manual&from=100&to=900&interaction=true&after=50&limit=10&sort=startedAt&dir=asc",
"http://localhost/execution-history/runs?status=completed,failed&trigger=manual&from=100&to=900&interaction=true&approvalType=form&after=50&limit=10&sort=startedAt&dir=asc",
);
expect(res.status).toBe(200);
const body = (yield* Effect.promise(() => res.json())) as {
Expand All @@ -147,6 +149,7 @@ describe("ExecutionHistoryHandlers", () => {
expect(options?.triggerFilter).toEqual(["manual"]);
expect(options?.timeRange).toEqual({ from: 100, to: 900 });
expect(options?.hadInteraction).toBe(true);
expect(options?.approvalType).toBe("form");
expect(options?.after).toBe(50);
expect(options?.limit).toBe(10);
expect(options?.sortField).toBe("startedAt");
Expand All @@ -168,6 +171,7 @@ describe("ExecutionHistoryHandlers", () => {
expect(options?.triggerFilter).toBeUndefined();
expect(options?.timeRange).toBeUndefined();
expect(options?.hadInteraction).toBeUndefined();
expect(options?.approvalType).toBeUndefined();
}),
);

Expand Down
1 change: 1 addition & 0 deletions packages/plugins/execution-history/src/api/handlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@ export const ExecutionHistoryHandlers = HttpApiBuilder.group(
actorFilter: actorFilter.length > 0 ? actorFilter : undefined,
timeRange,
hadInteraction: parseBooleanFlag(query.interaction),
approvalType: query.approvalType,
after: query.after,
sortField: query.sort,
sortDirection: query.dir,
Expand Down
1 change: 1 addition & 0 deletions packages/plugins/execution-history/src/react/atoms.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ export interface RunsQuery {
readonly from?: number;
readonly to?: number;
readonly interaction?: string;
readonly approvalType?: "form" | "url";
readonly after?: number;
readonly sort?: "startedAt" | "durationMs";
readonly dir?: "asc" | "desc";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -114,10 +114,10 @@ export function RunsColumnHeader({
</th>
) : null}

{/* Interaction (centered) */}
{/* Approval (centered) */}
{columns.interaction ? (
<th scope="col" className={cn(HEAD, "text-center")}>
interaction
approval
</th>
) : null}

Expand Down
14 changes: 7 additions & 7 deletions packages/plugins/execution-history/src/react/detail-drawer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -216,7 +216,7 @@ function ToolCallsTab(props: { readonly run: RunRow; readonly toolCalls: readonl
}

// ---------------------------------------------------------------------------
// Interactions (elicitations) — rendered inline on the Properties tab
// Approvals (elicitations), rendered inline on the Properties tab
// ---------------------------------------------------------------------------

const INTERACTION_BADGE: Record<InteractionStatus, string> = {
Expand All @@ -228,7 +228,7 @@ const INTERACTION_BADGE: Record<InteractionStatus, string> = {
};

const interactionKindLabel = (kind: string): string =>
kind === "UrlElicitation" ? "url" : kind === "FormElicitation" ? "form" : kind;
kind === "UrlElicitation" ? "URL approval" : kind === "FormElicitation" ? "Form approval" : kind;

function InteractionBlock(props: { readonly interaction: InteractionRow }) {
const { interaction } = props;
Expand Down Expand Up @@ -282,14 +282,14 @@ function DetailContent(props: {
const pendingCount = props.interactions.filter(
(interaction) => interaction.status === "pending",
).length;
// Singular/plural only when every interaction is pending; a mix gets the
// neutral "Interactions" so the heading never misrepresents the block.
// Singular/plural only when every approval is pending; a mix gets the neutral
// "Approvals" so the heading never misrepresents the block.
const interactionsHeading =
pendingCount > 0 && pendingCount === props.interactions.length
? pendingCount === 1
? "Pending interaction"
: "Pending interactions"
: "Interactions";
? "Pending approval"
: "Pending approvals"
: "Approvals";
return (
<div className="flex min-h-0 flex-1 flex-col">
<SheetHeader className="border-b border-border px-5 py-3">
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
// ---------------------------------------------------------------------------
// Pure parser for the runs filter command palette. Splits a free-text query
// into `key:value` tokens and produces a partial filter set. Adapted to the
// plugin's contract: only the 4 statuses, no tool/code/duration facets — the
// keys are status, trigger, interaction, after, and before.
// plugin's contract: only the 4 statuses, no tool/code/duration facets. The
// keys are status, trigger, interaction, approval, after, and before.
//
// Parsing is forgiving by design: unknown keys, empty values, and unparsable
// dates are silently dropped (never throws). Relative durations like "1h" /
Expand All @@ -14,6 +14,7 @@ export interface RunsFilterTokens {
readonly status: string[];
readonly trigger: string[];
readonly interaction: "true" | "false" | null;
readonly approvalType: "form" | "url" | null;
readonly from: number | null;
readonly to: number | null;
}
Expand All @@ -27,6 +28,7 @@ export const FILTER_KEYS: readonly FilterKey[] = [
{ key: "status", hint: "completed,failed,running,waiting" },
{ key: "trigger", hint: "mcp, http, cli, manual" },
{ key: "interaction", hint: "true | false" },
{ key: "approval", hint: "form | url" },
{ key: "after", hint: "1h · 30m · 7d · ISO date" },
{ key: "before", hint: "1h · 7d · ISO date" },
];
Expand Down Expand Up @@ -64,6 +66,7 @@ export const parseRunsFilter = (text: string): RunsFilterTokens => {
const status: string[] = [];
const trigger: string[] = [];
let interaction: "true" | "false" | null = null;
let approvalType: "form" | "url" | null = null;
let from: number | null = null;
let to: number | null = null;

Expand All @@ -82,6 +85,8 @@ export const parseRunsFilter = (text: string): RunsFilterTokens => {
trigger.push(...value.split(",").filter(Boolean));
} else if (key === "interaction") {
if (value === "true" || value === "false") interaction = value;
} else if (key === "approval" || key === "approvalType") {
if (value === "form" || value === "url") approvalType = value;
} else if (key === "after") {
const epoch = tokenToEpochMs(value);
if (epoch !== null) from = epoch;
Expand All @@ -92,5 +97,5 @@ export const parseRunsFilter = (text: string): RunsFilterTokens => {
// Unknown keys are silently dropped so the input stays forgiving.
}

return { status, trigger, interaction, from, to };
return { status, trigger, interaction, approvalType, from, to };
};
11 changes: 6 additions & 5 deletions packages/plugins/execution-history/src/react/filter-command.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -53,9 +53,9 @@ const toggleFacet = (current: readonly string[], value: string): string[] =>
current.includes(value) ? current.filter((entry) => entry !== value) : [...current, value];

/** Merge parsed tokens onto the current filters: status/trigger REPLACE (only
* when present and valid), interaction/from/to are set only when the token was
* actually typed — so applying a command never wipes a time range or
* interaction filter set elsewhere (the rail, the timeline drag). The palette
* when present and valid), interaction/approval/from/to are set only when the
* token was actually typed, so applying a command never wipes a time range or
* approval filter set elsewhere (the rail, the timeline drag). The palette
* sets filters; clearing is done from the rail. Status tokens are normalized to
* canonical RunStatus values; an all-invalid status token leaves status
* untouched. */
Expand All @@ -68,6 +68,7 @@ const mergeTokens = (filters: RunsFilters, tokens: RunsFilterTokens): RunsFilter
status: statuses.length > 0 ? statuses : filters.status,
trigger: tokens.trigger.length > 0 ? tokens.trigger : filters.trigger,
interaction: tokens.interaction !== null ? tokens.interaction : filters.interaction,
approvalType: tokens.approvalType !== null ? tokens.approvalType : filters.approvalType,
Comment on lines 70 to +71

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 When mergeTokens applies a parsed token for interaction, it should clear approvalType (and vice versa), mirroring the mutual-exclusivity logic in the filter rail toggles. Without this, typing interaction:false approval:form in the command palette sets both simultaneously, producing hadInteraction=false AND hadFormApproval=true — a contradiction that always returns 0 results with no error.

Suggested change
interaction: tokens.interaction !== null ? tokens.interaction : filters.interaction,
approvalType: tokens.approvalType !== null ? tokens.approvalType : filters.approvalType,
interaction: tokens.interaction !== null ? tokens.interaction : tokens.approvalType !== null ? null : filters.interaction,
approvalType: tokens.approvalType !== null ? tokens.approvalType : tokens.interaction !== null ? null : filters.approvalType,

Fix in Codex Fix in Cursor Cloud Agents Fix in Claude Code Fix in Cursor

from: tokens.from !== null ? tokens.from : filters.from,
to: tokens.to !== null ? tokens.to : filters.to,
};
Expand Down Expand Up @@ -163,7 +164,7 @@ export const RunsFilterCommand = forwardRef<HTMLInputElement, RunsFilterCommandP
value={value}
onValueChange={setValue}
onFocus={() => onOpenChange(true)}
placeholder="Filter runs — status:… trigger:… interaction:… after:…"
placeholder="Filter runs — status:… trigger:… approval:… after:…"
className="font-mono text-xs"
/>
</Command>
Expand Down Expand Up @@ -192,7 +193,7 @@ export const RunsFilterCommand = forwardRef<HTMLInputElement, RunsFilterCommandP
value={value}
onValueChange={setValue}
onFocus={() => onOpenChange(true)}
placeholder="Filter runs — status:… trigger:… interaction:… after:…"
placeholder="Filter runs — status:… trigger:… approval:… after:…"
className="font-mono text-xs"
/>

Expand Down
41 changes: 37 additions & 4 deletions packages/plugins/execution-history/src/react/filter-rail.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ const isFiltersActive = (filters: RunsFilters): boolean =>
filters.trigger.length > 0 ||
filters.actor.length > 0 ||
filters.interaction !== null ||
filters.approvalType !== null ||
filters.from !== null ||
filters.to !== null;

Expand Down Expand Up @@ -329,8 +330,8 @@ export function RunsFilterRail({ filters, meta, onChange, onReset }: RunsFilterR
</FacetSection>
) : null}

{/* Interaction facet */}
<FacetSection value="interaction" label="Interactions">
{/* Approval facet */}
<FacetSection value="interaction" label="Approvals">
<ul className="space-y-0">
<li>
<FacetRow
Expand All @@ -339,10 +340,11 @@ export function RunsFilterRail({ filters, meta, onChange, onReset }: RunsFilterR
onChange({
...filters,
interaction: filters.interaction === "true" ? null : "true",
approvalType: null,
})
}
dotClass="bg-amber-500"
label="With interaction"
label="With approval"
count={meta?.interactionCounts.withInteraction}
/>
</li>
Expand All @@ -353,13 +355,44 @@ export function RunsFilterRail({ filters, meta, onChange, onReset }: RunsFilterR
onChange({
...filters,
interaction: filters.interaction === "false" ? null : "false",
approvalType: null,
})
}
dotClass="bg-muted-foreground/40"
label="Without interaction"
label="No approval"
count={meta?.interactionCounts.withoutInteraction}
/>
</li>
<li>
<FacetRow
checked={filters.approvalType === "form"}
onToggle={() =>
onChange({
...filters,
interaction: null,
approvalType: filters.approvalType === "form" ? null : "form",
})
}
dotClass="bg-violet-500"
label="Form approval"
count={meta?.interactionCounts.formApproval}
/>
</li>
<li>
<FacetRow
checked={filters.approvalType === "url"}
onToggle={() =>
onChange({
...filters,
interaction: null,
approvalType: filters.approvalType === "url" ? null : "url",
})
}
dotClass="bg-sky-500"
label="URL approval"
count={meta?.interactionCounts.urlApproval}
/>
</li>
</ul>
</FacetSection>

Expand Down
12 changes: 9 additions & 3 deletions packages/plugins/execution-history/src/react/run-row.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,12 @@ export function RunListRow({ run, selected, isPast, columns, onSelect }: RunList
const isLive = run.status === "running" || run.status === "waiting_for_interaction";
const logErrors = run.logErrorCount;
const logWarns = run.logWarnCount;
const approvalLabels = [
run.hadFormApproval ? "form" : null,
run.hadUrlApproval ? "url" : null,
].filter((label): label is string => label !== null);
const approvalLabel =
approvalLabels.length > 0 ? approvalLabels.join(",") : run.hadInteraction ? "yes" : null;

return (
<tr
Expand Down Expand Up @@ -133,15 +139,15 @@ export function RunListRow({ run, selected, isPast, columns, onSelect }: RunList
</td>
) : null}

{/* Interaction (centered, optional) */}
{/* Approval (centered, optional) */}
{columns.interaction ? (
<td className={cn(CELL, "text-center")}>
{run.hadInteraction ? (
{approvalLabel ? (
<Badge
variant="outline"
className="border-amber-500/30 bg-amber-500/10 text-amber-700 dark:text-amber-300"
>
yes
{approvalLabel}
</Badge>
) : (
<span className="text-muted-foreground/50">—</span>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ const row = (id: string, startedAt: number): RunRow => ({
durationMs: 100,
toolCallCount: 0,
hadInteraction: false,
hadFormApproval: false,
hadUrlApproval: false,
});

const run = (actions: readonly RunsListAction[], from: RunsListState = initialRunsListState) =>
Expand Down
4 changes: 4 additions & 0 deletions packages/plugins/execution-history/src/react/use-runs-list.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ export interface RunsFilters {
readonly trigger: readonly string[];
readonly actor: readonly string[];
readonly interaction: "true" | "false" | null;
readonly approvalType: "form" | "url" | null;
readonly from: number | null;
readonly to: number | null;
readonly sortField: RunsSortField;
Expand All @@ -37,6 +38,7 @@ export const emptyRunsFilters: RunsFilters = {
trigger: [],
actor: [],
interaction: null,
approvalType: null,
from: null,
to: null,
sortField: "startedAt",
Expand All @@ -51,6 +53,7 @@ export const buildRunsQuery = (filters: RunsFilters, cursor: string | undefined)
...(filters.trigger.length > 0 ? { trigger: filters.trigger.join(",") } : {}),
...(filters.actor.length > 0 ? { actor: filters.actor.join(",") } : {}),
...(filters.interaction != null ? { interaction: filters.interaction } : {}),
...(filters.approvalType != null ? { approvalType: filters.approvalType } : {}),
...(filters.from != null ? { from: filters.from } : {}),
...(filters.to != null ? { to: filters.to } : {}),
...(cursor !== undefined ? { cursor } : {}),
Expand All @@ -62,6 +65,7 @@ const filtersKey = (filters: RunsFilters): string =>
filters.trigger,
filters.actor,
filters.interaction,
filters.approvalType,
filters.from,
filters.to,
filters.sortField,
Expand Down
2 changes: 1 addition & 1 deletion packages/plugins/execution-history/src/react/view.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,6 @@ export const RUN_COLUMN_LABELS: Record<RunColumnKey, string> = {
actor: "Actor",
duration: "Duration",
tools: "Tools",
interaction: "Interaction",
interaction: "Approval",
log: "Log",
};
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@ describe("RunRow legacy-document tolerance", () => {
expect(decoded.actorId).toBeNull();
expect(decoded.actorLabel).toBeNull();
expect(decoded.actorKind).toBeNull();
expect(decoded.hadFormApproval).toBe(false);
expect(decoded.hadUrlApproval).toBe(false);
// The decoded type is `string | null` (always present) — readers treat it as
// required; it just defaults to null for pre-actor runs.
expect(decoded.executionId).toBe("exec_legacy");
Expand All @@ -55,6 +57,8 @@ describe("RunRow legacy-document tolerance", () => {
expect(encoded.actorId ?? null).toBeNull();
expect(encoded.actorLabel ?? null).toBeNull();
expect(encoded.actorKind ?? null).toBeNull();
expect(encoded.hadFormApproval ?? false).toBe(false);
expect(encoded.hadUrlApproval ?? false).toBe(false);
});

it("round-trips a run that DOES carry an actor through decode AND encode", () => {
Expand Down
Loading
Loading