Skip to content
This repository was archived by the owner on Sep 4, 2026. It is now read-only.
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
6 changes: 6 additions & 0 deletions docs/releases/UNRELEASED.md
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,12 @@ reset this file.
permissions are still bypassed only when the user explicitly selects bypass
mode.
- Removed the `piAgents` flag after Pi native chat shipped default-on in v0.2.0.
- A `pickforge-lanes` MCP call in chat now expands to the run's lane cards —
the same cards the Settings panel shows, not a second copy (#362 PR 3, behind
the default-off `mcpToolDetail` flag). Lanes are live while the call is in
flight, so watching a `lanes_wait` is worth something, and frozen once it
finishes so a replayed message does not rewrite itself from a run that has
moved on. Abandon stays in Settings.
- The working row now names what is running and how long it has been running,
behind the new default-off `turnActivity` flag (#365). During a long MCP call
the chat used to show a bare "Working" dot with no way to tell thinking from
Expand Down
27 changes: 22 additions & 5 deletions src/components/chat/McpCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import { compactInline, hasHiddenDetail } from "../../lib/chatDisplay";
import { IconChevronRight } from "../icons";
import type { ToolCallStatus } from "../../stores/agentChat";
import { Disclosure, StatusPill, type StatusIntent } from "../ui";
import { pikitRowIsLive, pikitRunRef } from "../../lib/pikitRunRef";
import { PiKitRunLanes } from "../pikit/PiKitRunLanes";
import "./chat.css";

const STATUS_INTENT: Record<ToolCallStatus, StatusIntent> = {
Expand All @@ -23,7 +25,15 @@ export function McpCard(props: {
const open = () => props.open ?? localOpen();
const toggle = () => (props.onToggle ? props.onToggle() : setLocalOpen((v) => !v));
const detail = () => props.detail ?? "";
const canExpand = () => hasHiddenDetail(detail(), 120);
const row = () => ({
server: props.server,
tool: props.tool,
detail: props.detail,
status: props.status,
});
const runRef = () => pikitRunRef(row());
const live = () => pikitRowIsLive(row());
const canExpand = () => hasHiddenDetail(detail(), 120) || runRef() !== null;

return (
<div class="pf-chat-line" classList={{ "pf-chat-line--open": open() }}>
Expand Down Expand Up @@ -64,11 +74,18 @@ export function McpCard(props: {
</Show>
</button>
<Disclosure open={open()}>
<Show when={detail()}>
<div class="pf-chat-line-body">
<div class="pf-chat-line-body">
{/* A pickforge-lanes call renders the run's lanes the way Settings
does — same component, so the two cannot drift (#362). Live while
the call is in flight, frozen afterwards, so a replayed row does
not quietly rewrite itself from a run that has moved on. */}
<Show when={runRef()}>
{(run) => <PiKitRunLanes run={run()} live={live()} />}
</Show>
<Show when={detail()}>
<pre class="pf-chat-tail">{detail()}</pre>
</div>
</Show>
</Show>
</div>
</Disclosure>
</div>
);
Expand Down
137 changes: 3 additions & 134 deletions src/components/pikit/PiKitLanesPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,9 @@
// token usage.
import { For, Show, type JSX, createSignal, onCleanup, onMount } from "solid-js";
import { ConfirmDialog } from "../ConfirmDialog";
import { Disclosure, ForgeEmptyState, MonoEyebrow } from "../ui";
import { IconChevronRight, IconGrid, IconRefresh } from "../icons";
import { RunCard, type AbandonTarget } from "./PiKitRunCard";
import { ForgeEmptyState, MonoEyebrow } from "../ui";
import { IconGrid, IconRefresh } from "../icons";
import type { PiKitRunEntry } from "../../lib/process";
import {
loadAllPiKitRuns,
Expand All @@ -20,145 +21,13 @@ import {
startPiKitLanesPolling,
stopPiKitLanesPolling,
} from "../../stores/pikitLanes";
import {
abandonDisabledReason,
abandonHint,
formatCost,
formatDuration,
formatTokens,
laneDetail,
laneStatusTone,
orphanNote,
runLabel,
runStatusTone,
} from "./pikitLaneDisplay";
import "./pikitLanes.css";

interface AbandonTarget {
run: string;
lane: string | null;
label: string;
}

interface Notice {
text: string;
error: boolean;
}

// eslint-disable-next-line max-lines-per-function -- single cohesive panel; see codebase-design note in PR.
function RunCard(props: {
entry: PiKitRunEntry;
onAbandon: (target: AbandonTarget) => void;
}): JSX.Element {
const [open, setOpen] = createSignal(false);
const entry = () => props.entry;
const status = () => entry().status;
const runAbandonReason = () => abandonDisabledReason(entry());
const runAbandonHint = () => abandonHint(entry());

return (
<section
class="pf-pikit-card"
classList={{ "pf-pikit-card--open": open() }}
aria-label="pi-kit run"
>
<button
type="button"
class="pf-pikit-summary"
aria-expanded={open()}
onClick={() => setOpen((v) => !v)}
>
<span class="pf-pikit-summary-chevron" aria-hidden="true">
<IconChevronRight size={12} />
</span>
<span class="pf-pikit-summary-title">
<IconGrid size={13} />
<span>{entry().run}</span>
</span>
<span class="pf-pikit-summary-meta">
<Show when={status()}>
{(s) => (
<>
{s().lanes.length} lane{s().lanes.length === 1 ? "" : "s"} · {formatCost(s().totals.cost)} ·{" "}
{formatDuration(s().durationMs)}
</>
)}
</Show>
</span>
<span class="pf-pikit-status" style={{ "--pf-pikit-status": runStatusTone(entry()) }}>
<span class="pf-pikit-dot" />
{runLabel(entry())}
</span>
</button>
<Disclosure open={open()}>
<div class="pf-pikit-body">
<Show when={status()} fallback={<div class="pf-pikit-empty">No status details available.</div>}>
{(s) => (
<div class="pf-pikit-lanes">
<For each={s().lanes}>
{(lane) => {
const reason = () => abandonDisabledReason(entry(), lane);
const hint = () => abandonHint(entry());
return (
<div class="pf-pikit-lane">
<span
class="pf-pikit-lane-status"
style={{ "--pf-pikit-status": laneStatusTone(lane.state) }}
>
<span class="pf-pikit-dot" />
{lane.state}
</span>
<span class="pf-pikit-lane-main">
<span class="pf-pikit-lane-title">{lane.lane}</span>
<span class="pf-pikit-lane-meta">
{lane.model} · {lane.effort} · {formatTokens(lane.tokensIn)}/
{formatTokens(lane.tokensOut)} tok · {formatCost(lane.cost)} ·{" "}
{formatDuration(lane.durationMs)}
</span>
<span class="pf-pikit-lane-detail">{laneDetail(lane)}</span>
</span>
<button
type="button"
class="pf-text-btn pf-pikit-abandon"
disabled={reason() !== null}
title={reason() ?? hint() ?? "Request that pi-kit abandon this lane"}
onClick={() =>
props.onAbandon({ run: entry().run, lane: lane.lane, label: lane.lane })
}
>
Abandon
</button>
</div>
);
}}
</For>
</div>
)}
</Show>
<Show when={orphanNote(entry())}>
{(note) => <div class="pf-pikit-orphan-note">{note()}</div>}
</Show>
<Show when={status() && status()!.lanes.length > 0}>
<button
type="button"
class="pf-text-btn"
disabled={runAbandonReason() !== null}
title={
runAbandonReason() ??
runAbandonHint() ??
"Request that pi-kit abandon every active lane in this run"
}
onClick={() => props.onAbandon({ run: entry().run, lane: null, label: "all lanes" })}
>
Abandon all lanes
</button>
</Show>
</div>
</Disclosure>
</section>
);
}

/** The complete run list, behind the panel's "view all" affordance. Reuses
* `ConfirmDialog`'s portal/backdrop shape rather than introducing a second
* modal idiom (#363). */
Expand Down
147 changes: 147 additions & 0 deletions src/components/pikit/PiKitRunCard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
// One pi-kit run, rendered identically in Settings and in an agent chat's MCP
// row (#362). Extracted out of PiKitLanesPanel rather than written twice —
// a second lane card would drift from this one the first time either changed.
import { For, Show, type JSX, createSignal } from "solid-js";
import { Disclosure } from "../ui";
import { IconChevronRight, IconGrid } from "../icons";
import type { PiKitRunEntry } from "../../lib/process";
import {
abandonDisabledReason,
abandonHint,
formatCost,
formatDuration,
formatTokens,
laneDetail,
laneStatusTone,
orphanNote,
runLabel,
runStatusTone,
} from "./pikitLaneDisplay";
import "./pikitLanes.css";

export interface AbandonTarget {
run: string;
lane: string | null;
label: string;
}

// eslint-disable-next-line max-lines-per-function -- one cohesive card; splitting it would spread the lane row across files.
export function RunCard(props: {
entry: PiKitRunEntry;
onAbandon: (target: AbandonTarget) => void;
/** Hides the Abandon controls. Acting on a run belongs in the Settings panel
* that owns it — offering a destructive action two clicks from a replayed
* chat message would be a trap (#362). */
readOnly?: boolean;
}): JSX.Element {
const [open, setOpen] = createSignal(false);
const entry = () => props.entry;
const status = () => entry().status;
const runAbandonReason = () => abandonDisabledReason(entry());
const runAbandonHint = () => abandonHint(entry());

return (
<section
class="pf-pikit-card"
classList={{ "pf-pikit-card--open": open() }}
aria-label="pi-kit run"
>
<button
type="button"
class="pf-pikit-summary"
aria-expanded={open()}
onClick={() => setOpen((v) => !v)}
>
<span class="pf-pikit-summary-chevron" aria-hidden="true">
<IconChevronRight size={12} />
</span>
<span class="pf-pikit-summary-title">
<IconGrid size={13} />
<span>{entry().run}</span>
</span>
<span class="pf-pikit-summary-meta">
<Show when={status()}>
{(s) => (
<>
{s().lanes.length} lane{s().lanes.length === 1 ? "" : "s"} · {formatCost(s().totals.cost)} ·{" "}
{formatDuration(s().durationMs)}
</>
)}
</Show>
</span>
<span class="pf-pikit-status" style={{ "--pf-pikit-status": runStatusTone(entry()) }}>
<span class="pf-pikit-dot" />
{runLabel(entry())}
</span>
</button>
<Disclosure open={open()}>
<div class="pf-pikit-body">
<Show when={status()} fallback={<div class="pf-pikit-empty">No status details available.</div>}>
{(s) => (
<div class="pf-pikit-lanes">
<For each={s().lanes}>
{(lane) => {
const reason = () => abandonDisabledReason(entry(), lane);
const hint = () => abandonHint(entry());
return (
<div class="pf-pikit-lane">
<span
class="pf-pikit-lane-status"
style={{ "--pf-pikit-status": laneStatusTone(lane.state) }}
>
<span class="pf-pikit-dot" />
{lane.state}
</span>
<span class="pf-pikit-lane-main">
<span class="pf-pikit-lane-title">{lane.lane}</span>
<span class="pf-pikit-lane-meta">
{lane.model} · {lane.effort} · {formatTokens(lane.tokensIn)}/
{formatTokens(lane.tokensOut)} tok · {formatCost(lane.cost)} ·{" "}
{formatDuration(lane.durationMs)}
</span>
<span class="pf-pikit-lane-detail">{laneDetail(lane)}</span>
</span>
<Show when={!props.readOnly}>
<button
type="button"
class="pf-text-btn pf-pikit-abandon"
disabled={reason() !== null}
title={reason() ?? hint() ?? "Request that pi-kit abandon this lane"}
onClick={() =>
props.onAbandon({ run: entry().run, lane: lane.lane, label: lane.lane })
}
>
Abandon
</button>
</Show>
</div>
);
}}
</For>
</div>
)}
</Show>
<Show when={orphanNote(entry())}>
{(note) => <div class="pf-pikit-orphan-note">{note()}</div>}
</Show>
<Show when={!props.readOnly && status() && status()!.lanes.length > 0}>
<button
type="button"
class="pf-text-btn"
disabled={runAbandonReason() !== null}
title={
runAbandonReason() ??
runAbandonHint() ??
"Request that pi-kit abandon every active lane in this run"
}
onClick={() => props.onAbandon({ run: entry().run, lane: null, label: "all lanes" })}
>
Abandon all lanes
</button>
</Show>
</div>
</Disclosure>
</section>
);
}

Loading
Loading