fix(workspace): move lane list to a vertical column beside detail panel
The detail panel (LaneCard/PipelineMap/proof gallery/console) could grow tall enough to visually crowd out the lane-strip carousel above it once a run was attached in split view. Move the lane list into its own scrolling vertical column beside the detail panel instead of stacking it above, so neither can cover the other; also collapse the info block by default in 2/4-pane split view (toggle to expand) and move the pane-count control into the detail header.
This commit is contained in:
@@ -1,9 +1,9 @@
|
|||||||
/**
|
/**
|
||||||
* @file The compact lane tile used in the Workspace carousel. It carries only
|
* @file The compact lane tile used in the Workspace's vertical lane list. It
|
||||||
* what you need to pick a lane — which lane, is it alive, what stage, how far —
|
* carries only what you need to pick a lane — which lane, is it alive, what
|
||||||
* because the full card, its controls and its working-copy facts live in the
|
* stage, how far — because the full card, its controls and its working-copy
|
||||||
* detail panel below. Keeping the tile small is what lets a dozen lanes stay
|
* facts live in the detail panel beside it. Keeping the tile small and full
|
||||||
* scannable in one horizontal row.
|
* width is what lets many lanes stay scannable in one scrolling column.
|
||||||
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
|
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@@ -42,7 +42,7 @@ export default function LaneStripCard({
|
|||||||
aria-pressed={selected}
|
aria-pressed={selected}
|
||||||
onClick={onSelect}
|
onClick={onSelect}
|
||||||
title={lane.cwd}
|
title={lane.cwd}
|
||||||
className={`w-56 shrink-0 snap-start rounded-lg border p-3 text-left shadow-sm transition-colors ${
|
className={`w-full shrink-0 rounded-lg border p-3 text-left shadow-sm transition-colors ${
|
||||||
selected
|
selected
|
||||||
? "border-accent bg-accent/10"
|
? "border-accent bg-accent/10"
|
||||||
: "border-border bg-surface-2 hover:border-border-light hover:bg-surface-3"
|
: "border-border bg-surface-2 hover:border-border-light hover:bg-surface-3"
|
||||||
|
|||||||
@@ -90,6 +90,8 @@
|
|||||||
"git.uncommitted": "{{dirty}} modified · {{untracked}} untracked",
|
"git.uncommitted": "{{dirty}} modified · {{untracked}} untracked",
|
||||||
"kind.adopted": "adopted",
|
"kind.adopted": "adopted",
|
||||||
"kind.managed": "managed",
|
"kind.managed": "managed",
|
||||||
|
"laneDetail.hide": "Hide details",
|
||||||
|
"laneDetail.show": "Lane details",
|
||||||
"laneHeader": "Lane {{id}} · {{title}} · {{pipeline}}",
|
"laneHeader": "Lane {{id}} · {{title}} · {{pipeline}}",
|
||||||
"locks.held_one": "{{count}} lock held",
|
"locks.held_one": "{{count}} lock held",
|
||||||
"locks.held_other": "{{count}} locks held",
|
"locks.held_other": "{{count}} locks held",
|
||||||
|
|||||||
@@ -90,6 +90,8 @@
|
|||||||
"git.uncommitted": "{{dirty}} đã sửa · {{untracked}} chưa theo dõi",
|
"git.uncommitted": "{{dirty}} đã sửa · {{untracked}} chưa theo dõi",
|
||||||
"kind.adopted": "đã nhận",
|
"kind.adopted": "đã nhận",
|
||||||
"kind.managed": "được quản lý",
|
"kind.managed": "được quản lý",
|
||||||
|
"laneDetail.hide": "Ẩn chi tiết",
|
||||||
|
"laneDetail.show": "Chi tiết lane",
|
||||||
"laneHeader": "Làn đường {{id}} · {{title}} · {{pipeline}}",
|
"laneHeader": "Làn đường {{id}} · {{title}} · {{pipeline}}",
|
||||||
"locks.held_one": "Đang giữ {{count}} khóa",
|
"locks.held_one": "Đang giữ {{count}} khóa",
|
||||||
"locks.held_other": "Đang giữ {{count}} khóa",
|
"locks.held_other": "Đang giữ {{count}} khóa",
|
||||||
|
|||||||
+242
-192
@@ -54,6 +54,36 @@ import type { SplitViewState, SplitLayout } from "../lib/splitViewStorage";
|
|||||||
|
|
||||||
// ── Page ──────────────────────────────────────────────────────────────
|
// ── Page ──────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
function SplitLayoutToggle({
|
||||||
|
splitView,
|
||||||
|
setLayout,
|
||||||
|
}: {
|
||||||
|
splitView: SplitViewState;
|
||||||
|
setLayout: (layout: SplitLayout) => void;
|
||||||
|
}) {
|
||||||
|
const { t: tLanes } = useTranslation("lanes");
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex items-center gap-1.5">
|
||||||
|
{([1, 2, 4] as const).map((n) => (
|
||||||
|
<button
|
||||||
|
key={n}
|
||||||
|
type="button"
|
||||||
|
aria-pressed={splitView.layout === n}
|
||||||
|
onClick={() => setLayout(n)}
|
||||||
|
className={`rounded border px-2 py-1 text-xs ${
|
||||||
|
splitView.layout === n
|
||||||
|
? "border-accent bg-accent/15 text-accent"
|
||||||
|
: "border-border text-fg-secondary hover:border-border-light"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{tLanes("splitView.paneCount", { count: n })}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function ConsoleArea({
|
function ConsoleArea({
|
||||||
lanes,
|
lanes,
|
||||||
selectedLaneId,
|
selectedLaneId,
|
||||||
@@ -68,6 +98,7 @@ function ConsoleArea({
|
|||||||
onHasActiveRunChange,
|
onHasActiveRunChange,
|
||||||
onLaneCreated,
|
onLaneCreated,
|
||||||
onLaneIdChange,
|
onLaneIdChange,
|
||||||
|
showToggle = true,
|
||||||
}: {
|
}: {
|
||||||
lanes: Lane[];
|
lanes: Lane[];
|
||||||
selectedLaneId: number | null;
|
selectedLaneId: number | null;
|
||||||
@@ -82,28 +113,11 @@ function ConsoleArea({
|
|||||||
onHasActiveRunChange: (val: boolean) => void;
|
onHasActiveRunChange: (val: boolean) => void;
|
||||||
onLaneCreated: (lane: Lane) => void;
|
onLaneCreated: (lane: Lane) => void;
|
||||||
onLaneIdChange: (id: number | null) => void;
|
onLaneIdChange: (id: number | null) => void;
|
||||||
|
showToggle?: boolean;
|
||||||
}) {
|
}) {
|
||||||
const { t: tLanes } = useTranslation("lanes");
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="flex items-center gap-1.5">
|
{showToggle && <SplitLayoutToggle splitView={splitView} setLayout={setLayout} />}
|
||||||
{([1, 2, 4] as const).map((n) => (
|
|
||||||
<button
|
|
||||||
key={n}
|
|
||||||
type="button"
|
|
||||||
aria-pressed={splitView.layout === n}
|
|
||||||
onClick={() => setLayout(n)}
|
|
||||||
className={`rounded border px-2 py-1 text-xs ${
|
|
||||||
splitView.layout === n
|
|
||||||
? "border-accent bg-accent/15 text-accent"
|
|
||||||
: "border-border text-fg-secondary hover:border-border-light"
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{tLanes("splitView.paneCount", { count: n })}
|
|
||||||
</button>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
{splitView.layout === 1 ? (
|
{splitView.layout === 1 ? (
|
||||||
<LaneConsolePane
|
<LaneConsolePane
|
||||||
lanes={lanes}
|
lanes={lanes}
|
||||||
@@ -175,6 +189,11 @@ export function Workspace() {
|
|||||||
|
|
||||||
// Split view state
|
// Split view state
|
||||||
const [splitView, setSplitView] = useState<SplitViewState>(() => readSplitViewState());
|
const [splitView, setSplitView] = useState<SplitViewState>(() => readSplitViewState());
|
||||||
|
// Info block (pickers, LaneCard, PipelineMap, proof gallery) is always shown
|
||||||
|
// in single-pane view; in split view it starts collapsed so tall content
|
||||||
|
// never crowds the pane grid, and the user expands it on demand.
|
||||||
|
const [infoExpanded, setInfoExpanded] = useState(false);
|
||||||
|
const showInfo = splitView.layout === 1 || infoExpanded;
|
||||||
|
|
||||||
const setLayout = useCallback((layout: SplitLayout) => {
|
const setLayout = useCallback((layout: SplitLayout) => {
|
||||||
setSplitView((prev) => {
|
setSplitView((prev) => {
|
||||||
@@ -488,161 +507,214 @@ export function Workspace() {
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Lane carousel: pick a lane here, read it below. Horizontal scroll with
|
{/* Lane list + detail live side by side so neither can cover the other:
|
||||||
snap so a dozen lanes stay in one row instead of a wall of cards. */}
|
the list is a scrolling column (holds any number of lanes), the
|
||||||
<div
|
detail/console area to its right never has to compete with it for
|
||||||
data-testid="lane-strip"
|
vertical space. */}
|
||||||
className="flex snap-x snap-mandatory gap-2 overflow-x-auto pb-1"
|
<div className="flex min-h-0 flex-1 gap-4">
|
||||||
>
|
<div
|
||||||
{lanes.map((l) => (
|
data-testid="lane-strip"
|
||||||
<LaneStripCard
|
className="flex w-60 shrink-0 flex-col gap-2 overflow-y-auto pr-1"
|
||||||
key={l.id}
|
>
|
||||||
lane={l}
|
{lanes.map((l) => (
|
||||||
selected={selectedLaneId === l.id}
|
<LaneStripCard
|
||||||
onSelect={() => setSelectedLaneId(l.id)}
|
key={l.id}
|
||||||
/>
|
lane={l}
|
||||||
))}
|
selected={selectedLaneId === l.id}
|
||||||
{!lanes.length && (
|
onSelect={() => setSelectedLaneId(l.id)}
|
||||||
<p className="text-sm text-fg-muted">
|
/>
|
||||||
{tLanes("emptyState")} <code>ccam lanes add --cwd $(pwd)</code>
|
))}
|
||||||
</p>
|
{!lanes.length && (
|
||||||
)}
|
<p className="text-sm text-fg-muted">
|
||||||
</div>
|
{tLanes("emptyState")} <code>ccam lanes add --cwd $(pwd)</code>
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* The selected lane's pipeline, full width — the thing you actually
|
{/* The selected lane's pipeline — the thing you actually come to this
|
||||||
come to this page to read. */}
|
page to read, beside the list rather than stacked under it. */}
|
||||||
{currentLane && (
|
{currentLane && (
|
||||||
<section data-testid="lane-detail" className="card p-4">
|
<section data-testid="lane-detail" className="flex min-h-0 flex-1 flex-col card p-4">
|
||||||
<div className="mb-3 flex flex-wrap items-baseline gap-2">
|
<div className="mb-3 flex flex-wrap items-center justify-between gap-2">
|
||||||
<span className="text-[11px] font-semibold uppercase tracking-widest text-fg-muted">
|
<span className="truncate text-sm font-semibold text-fg-primary">
|
||||||
{tLanes("cardId", { id: currentLane.id })}
|
{tLanes("laneHeader", {
|
||||||
</span>
|
id: currentLane.id,
|
||||||
<span className="truncate text-sm font-semibold text-fg-primary">
|
title: currentLane.title || currentLane.cwd,
|
||||||
{currentLane.title || currentLane.cwd}
|
pipeline: currentLane.pipeline_name,
|
||||||
</span>
|
})}
|
||||||
<select
|
|
||||||
data-testid="pipeline-picker"
|
|
||||||
aria-label={tLanes("pipelinePicker.label")}
|
|
||||||
className="rounded border border-border bg-surface-1 px-2 py-0.5 text-xs text-fg-secondary disabled:opacity-60"
|
|
||||||
value={currentLane.pipeline}
|
|
||||||
disabled={!!viewedFeature}
|
|
||||||
title={
|
|
||||||
viewedFeature
|
|
||||||
? tLanes("features.viewingArchived", { slug: viewedFeature.slug })
|
|
||||||
: undefined
|
|
||||||
}
|
|
||||||
onChange={(e) => void handlePipelineChange(currentLane.id, e.target.value)}
|
|
||||||
>
|
|
||||||
{(pipelineTemplates.length
|
|
||||||
? pipelineTemplates
|
|
||||||
: [{ id: currentLane.pipeline, name: currentLane.pipeline_name, nodes: [] }]
|
|
||||||
).map((p) => (
|
|
||||||
<option key={p.id} value={p.id}>
|
|
||||||
{p.nodes.length ? `${p.name} (${p.nodes.length})` : p.name}
|
|
||||||
</option>
|
|
||||||
))}
|
|
||||||
</select>
|
|
||||||
{/* `stage` defaults to the DB sentinel "idle" until the driving
|
|
||||||
session ever calls `ccam stage` — that string collides with
|
|
||||||
`status`'s own "idle"/"running" vocabulary, so a lane that is
|
|
||||||
actively running but has never declared a stage read as if it
|
|
||||||
were sitting idle. Show a distinct label instead of the raw
|
|
||||||
sentinel whenever it doesn't match any node this pipeline
|
|
||||||
actually has. */}
|
|
||||||
<span className="rounded bg-surface-2 px-2 py-0.5 text-xs text-fg-secondary">
|
|
||||||
{currentLane.pipeline_nodes.some((n) => n.id === currentLane.stage)
|
|
||||||
? currentLane.stage
|
|
||||||
: tLanes("stageUndeclared")}
|
|
||||||
</span>
|
|
||||||
{currentLane.detected_stage && (
|
|
||||||
<span
|
|
||||||
data-testid="detail-auto-stage"
|
|
||||||
title={currentLane.detected_signal || undefined}
|
|
||||||
className="rounded border border-dashed border-status-warning px-2 py-0.5 text-xs text-status-warning"
|
|
||||||
>
|
|
||||||
{tLanes("autoStage", { stage: currentLane.detected_stage })}
|
|
||||||
</span>
|
</span>
|
||||||
)}
|
<div className="flex items-center gap-2">
|
||||||
{features.length > 0 && (
|
{splitView.layout !== 1 && (
|
||||||
<select
|
<button
|
||||||
data-testid="feature-picker"
|
type="button"
|
||||||
className="rounded border border-border bg-surface-1 px-2 py-0.5 text-xs"
|
data-testid="lane-detail-toggle"
|
||||||
value={viewedFeatureSlug ?? ""}
|
aria-expanded={infoExpanded}
|
||||||
onChange={(e) => setViewedFeatureSlug(e.target.value || null)}
|
onClick={() => setInfoExpanded((v) => !v)}
|
||||||
>
|
className="rounded border border-border-light px-2 py-1 text-xs text-fg-secondary transition-colors hover:text-fg-primary"
|
||||||
<option value="">{tLanes("features.live")}</option>
|
|
||||||
{features.map((f) => (
|
|
||||||
<option key={f.slug} value={f.slug}>
|
|
||||||
{f.slug}
|
|
||||||
{f.archived_at ? ` (${tLanes("features.archived")})` : ""}
|
|
||||||
</option>
|
|
||||||
))}
|
|
||||||
</select>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div className="mb-3">
|
|
||||||
<LaneCard
|
|
||||||
lane={currentLane}
|
|
||||||
onAction={(a, b) => handleLaneAction(currentLane.id, a, b)}
|
|
||||||
childWorktrees={lanes.filter(
|
|
||||||
(l) => l.source_repo === currentLane.cwd && l.id !== currentLane.id
|
|
||||||
)}
|
|
||||||
onSelectLane={setSelectedLaneId}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="mb-3">
|
|
||||||
<PipelineMap
|
|
||||||
nodes={viewedFeature ? viewedFeature.pipeline_nodes : currentLane.pipeline_nodes}
|
|
||||||
detectedSignal={viewedFeature ? undefined : currentLane.detected_signal}
|
|
||||||
/>
|
|
||||||
{viewedFeature && (
|
|
||||||
<p data-testid="feature-viewer-banner" className="mb-2 text-xs text-fg-muted">
|
|
||||||
{tLanes("features.viewingArchived", { slug: viewedFeature.slug })}
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
{proofFeature &&
|
|
||||||
(Object.keys(proofFeature.groups).length > 0 || proofFeature.ticket_report) && (
|
|
||||||
<div data-testid="proof-gallery" className="mt-2">
|
|
||||||
{proofFeature.ticket_report && (
|
|
||||||
<a
|
|
||||||
href={`/api/lanes/${currentLane.id}/proof/${proofFeature.ticket_report}`}
|
|
||||||
target="_blank"
|
|
||||||
rel="noreferrer"
|
|
||||||
className="text-xs text-fg-muted"
|
|
||||||
>
|
>
|
||||||
{tLanes("proof.ticketReport")}
|
{infoExpanded ? tLanes("laneDetail.hide") : tLanes("laneDetail.show")}
|
||||||
</a>
|
</button>
|
||||||
)}
|
)}
|
||||||
{Object.entries(proofFeature.groups).map(([group, images]) => (
|
<SplitLayoutToggle splitView={splitView} setLayout={setLayout} />
|
||||||
<div key={group} className="mt-1">
|
|
||||||
<span className="text-xs text-fg-muted">
|
|
||||||
{group} · {images.length}
|
|
||||||
</span>
|
|
||||||
<div className="flex flex-wrap gap-1">
|
|
||||||
{images.slice(0, 8).map((img) => (
|
|
||||||
<img
|
|
||||||
key={img}
|
|
||||||
loading="lazy"
|
|
||||||
className="h-16 w-16 rounded object-cover"
|
|
||||||
src={api.lanes.proof.imageUrl(
|
|
||||||
currentLane.id,
|
|
||||||
proofFeature.slug,
|
|
||||||
group,
|
|
||||||
img
|
|
||||||
)}
|
|
||||||
alt={img}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
{images.length > 8 && (
|
|
||||||
<span className="text-xs text-fg-muted">+{images.length - 8}</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
{showInfo && (
|
||||||
|
<>
|
||||||
|
<div className="mb-3 flex flex-wrap items-baseline gap-2">
|
||||||
|
<span className="text-[11px] font-semibold uppercase tracking-widest text-fg-muted">
|
||||||
|
{tLanes("cardId", { id: currentLane.id })}
|
||||||
|
</span>
|
||||||
|
<select
|
||||||
|
data-testid="pipeline-picker"
|
||||||
|
aria-label={tLanes("pipelinePicker.label")}
|
||||||
|
className="rounded border border-border bg-surface-1 px-2 py-0.5 text-xs text-fg-secondary disabled:opacity-60"
|
||||||
|
value={currentLane.pipeline}
|
||||||
|
disabled={!!viewedFeature}
|
||||||
|
title={
|
||||||
|
viewedFeature
|
||||||
|
? tLanes("features.viewingArchived", { slug: viewedFeature.slug })
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
onChange={(e) => void handlePipelineChange(currentLane.id, e.target.value)}
|
||||||
|
>
|
||||||
|
{(pipelineTemplates.length
|
||||||
|
? pipelineTemplates
|
||||||
|
: [{ id: currentLane.pipeline, name: currentLane.pipeline_name, nodes: [] }]
|
||||||
|
).map((p) => (
|
||||||
|
<option key={p.id} value={p.id}>
|
||||||
|
{p.nodes.length ? `${p.name} (${p.nodes.length})` : p.name}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
{/* `stage` defaults to the DB sentinel "idle" until the driving
|
||||||
|
session ever calls `ccam stage` — that string collides with
|
||||||
|
`status`'s own "idle"/"running" vocabulary, so a lane that is
|
||||||
|
actively running but has never declared a stage read as if it
|
||||||
|
were sitting idle. Show a distinct label instead of the raw
|
||||||
|
sentinel whenever it doesn't match any node this pipeline
|
||||||
|
actually has. */}
|
||||||
|
<span className="rounded bg-surface-2 px-2 py-0.5 text-xs text-fg-secondary">
|
||||||
|
{currentLane.pipeline_nodes.some((n) => n.id === currentLane.stage)
|
||||||
|
? currentLane.stage
|
||||||
|
: tLanes("stageUndeclared")}
|
||||||
|
</span>
|
||||||
|
{currentLane.detected_stage && (
|
||||||
|
<span
|
||||||
|
data-testid="detail-auto-stage"
|
||||||
|
title={currentLane.detected_signal || undefined}
|
||||||
|
className="rounded border border-dashed border-status-warning px-2 py-0.5 text-xs text-status-warning"
|
||||||
|
>
|
||||||
|
{tLanes("autoStage", { stage: currentLane.detected_stage })}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{features.length > 0 && (
|
||||||
|
<select
|
||||||
|
data-testid="feature-picker"
|
||||||
|
className="rounded border border-border bg-surface-1 px-2 py-0.5 text-xs"
|
||||||
|
value={viewedFeatureSlug ?? ""}
|
||||||
|
onChange={(e) => setViewedFeatureSlug(e.target.value || null)}
|
||||||
|
>
|
||||||
|
<option value="">{tLanes("features.live")}</option>
|
||||||
|
{features.map((f) => (
|
||||||
|
<option key={f.slug} value={f.slug}>
|
||||||
|
{f.slug}
|
||||||
|
{f.archived_at ? ` (${tLanes("features.archived")})` : ""}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="mb-3">
|
||||||
|
<LaneCard
|
||||||
|
lane={currentLane}
|
||||||
|
onAction={(a, b) => handleLaneAction(currentLane.id, a, b)}
|
||||||
|
childWorktrees={lanes.filter(
|
||||||
|
(l) => l.source_repo === currentLane.cwd && l.id !== currentLane.id
|
||||||
|
)}
|
||||||
|
onSelectLane={setSelectedLaneId}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="mb-3">
|
||||||
|
<PipelineMap
|
||||||
|
nodes={
|
||||||
|
viewedFeature ? viewedFeature.pipeline_nodes : currentLane.pipeline_nodes
|
||||||
|
}
|
||||||
|
detectedSignal={viewedFeature ? undefined : currentLane.detected_signal}
|
||||||
|
/>
|
||||||
|
{viewedFeature && (
|
||||||
|
<p data-testid="feature-viewer-banner" className="mb-2 text-xs text-fg-muted">
|
||||||
|
{tLanes("features.viewingArchived", { slug: viewedFeature.slug })}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{proofFeature &&
|
||||||
|
(Object.keys(proofFeature.groups).length > 0 || proofFeature.ticket_report) && (
|
||||||
|
<div data-testid="proof-gallery" className="mt-2">
|
||||||
|
{proofFeature.ticket_report && (
|
||||||
|
<a
|
||||||
|
href={`/api/lanes/${currentLane.id}/proof/${proofFeature.ticket_report}`}
|
||||||
|
target="_blank"
|
||||||
|
rel="noreferrer"
|
||||||
|
className="text-xs text-fg-muted"
|
||||||
|
>
|
||||||
|
{tLanes("proof.ticketReport")}
|
||||||
|
</a>
|
||||||
|
)}
|
||||||
|
{Object.entries(proofFeature.groups).map(([group, images]) => (
|
||||||
|
<div key={group} className="mt-1">
|
||||||
|
<span className="text-xs text-fg-muted">
|
||||||
|
{group} · {images.length}
|
||||||
|
</span>
|
||||||
|
<div className="flex flex-wrap gap-1">
|
||||||
|
{images.slice(0, 8).map((img) => (
|
||||||
|
<img
|
||||||
|
key={img}
|
||||||
|
loading="lazy"
|
||||||
|
className="h-16 w-16 rounded object-cover"
|
||||||
|
src={api.lanes.proof.imageUrl(
|
||||||
|
currentLane.id,
|
||||||
|
proofFeature.slug,
|
||||||
|
group,
|
||||||
|
img
|
||||||
|
)}
|
||||||
|
alt={img}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
{images.length > 8 && (
|
||||||
|
<span className="text-xs text-fg-muted">+{images.length - 8}</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
<div className="flex min-h-0 flex-col gap-2 border-t border-border pt-3">
|
<div className="flex min-h-0 flex-1 flex-col gap-2 border-t border-border pt-3">
|
||||||
|
<ConsoleArea
|
||||||
|
lanes={lanes}
|
||||||
|
selectedLaneId={selectedLaneId}
|
||||||
|
splitView={splitView}
|
||||||
|
setLayout={setLayout}
|
||||||
|
setPaneLaneId={setPaneLaneId}
|
||||||
|
binaryStatus={binaryStatus}
|
||||||
|
cwdSuggestions={cwdSuggestions}
|
||||||
|
activeRuns={activeRuns}
|
||||||
|
wsConnected={wsConnected}
|
||||||
|
defaultCwd={defaultCwd}
|
||||||
|
onHasActiveRunChange={setPaneHasActiveRun}
|
||||||
|
onLaneCreated={(lane) =>
|
||||||
|
setLanes((prev) => (prev.some((l) => l.id === lane.id) ? prev : [...prev, lane]))
|
||||||
|
}
|
||||||
|
onLaneIdChange={setSelectedLaneId}
|
||||||
|
showToggle={false}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{!currentLane && (
|
||||||
|
<div className="flex min-h-0 flex-1 flex-col gap-2">
|
||||||
<ConsoleArea
|
<ConsoleArea
|
||||||
lanes={lanes}
|
lanes={lanes}
|
||||||
selectedLaneId={selectedLaneId}
|
selectedLaneId={selectedLaneId}
|
||||||
@@ -661,30 +733,8 @@ export function Workspace() {
|
|||||||
onLaneIdChange={setSelectedLaneId}
|
onLaneIdChange={setSelectedLaneId}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
)}
|
||||||
)}
|
</div>
|
||||||
|
|
||||||
{!currentLane && (
|
|
||||||
<div className="flex min-h-0 flex-col gap-2">
|
|
||||||
<ConsoleArea
|
|
||||||
lanes={lanes}
|
|
||||||
selectedLaneId={selectedLaneId}
|
|
||||||
splitView={splitView}
|
|
||||||
setLayout={setLayout}
|
|
||||||
setPaneLaneId={setPaneLaneId}
|
|
||||||
binaryStatus={binaryStatus}
|
|
||||||
cwdSuggestions={cwdSuggestions}
|
|
||||||
activeRuns={activeRuns}
|
|
||||||
wsConnected={wsConnected}
|
|
||||||
defaultCwd={defaultCwd}
|
|
||||||
onHasActiveRunChange={setPaneHasActiveRun}
|
|
||||||
onLaneCreated={(lane) =>
|
|
||||||
setLanes((prev) => (prev.some((l) => l.id === lane.id) ? prev : [...prev, lane]))
|
|
||||||
}
|
|
||||||
onLaneIdChange={setSelectedLaneId}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<AddLaneModal
|
<AddLaneModal
|
||||||
open={addLaneOpen}
|
open={addLaneOpen}
|
||||||
|
|||||||
@@ -5762,205 +5762,295 @@ exports[`screen snapshots > Run 1`] = `
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
class="flex snap-x snap-mandatory gap-2 overflow-x-auto pb-1"
|
class="flex min-h-0 flex-1 gap-4"
|
||||||
data-testid="lane-strip"
|
|
||||||
>
|
|
||||||
<p
|
|
||||||
class="text-sm text-fg-muted"
|
|
||||||
>
|
|
||||||
No lanes yet. Create one from a working directory:
|
|
||||||
|
|
||||||
<code>
|
|
||||||
ccam lanes add --cwd $(pwd)
|
|
||||||
</code>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="flex min-h-0 flex-col gap-2"
|
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="flex items-center gap-1.5"
|
class="flex w-60 shrink-0 flex-col gap-2 overflow-y-auto pr-1"
|
||||||
|
data-testid="lane-strip"
|
||||||
>
|
>
|
||||||
<button
|
<p
|
||||||
aria-pressed="true"
|
class="text-sm text-fg-muted"
|
||||||
class="rounded border px-2 py-1 text-xs border-accent bg-accent/15 text-accent"
|
|
||||||
type="button"
|
|
||||||
>
|
>
|
||||||
1 pane
|
No lanes yet. Create one from a working directory:
|
||||||
</button>
|
|
||||||
<button
|
<code>
|
||||||
aria-pressed="false"
|
ccam lanes add --cwd $(pwd)
|
||||||
class="rounded border px-2 py-1 text-xs border-border text-fg-secondary hover:border-border-light"
|
</code>
|
||||||
type="button"
|
</p>
|
||||||
>
|
|
||||||
2 pane
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
aria-pressed="false"
|
|
||||||
class="rounded border px-2 py-1 text-xs border-border text-fg-secondary hover:border-border-light"
|
|
||||||
type="button"
|
|
||||||
>
|
|
||||||
4 pane
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
class="flex min-h-0 flex-1 flex-col gap-5"
|
class="flex min-h-0 flex-1 flex-col gap-2"
|
||||||
data-testid="console-body"
|
|
||||||
>
|
>
|
||||||
<header
|
|
||||||
class="flex items-start gap-3"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="w-9 h-9 rounded-xl bg-accent/15 flex items-center justify-center flex-shrink-0"
|
|
||||||
>
|
|
||||||
<svg
|
|
||||||
class="lucide lucide-play w-4.5 h-4.5 text-accent"
|
|
||||||
fill="none"
|
|
||||||
height="24"
|
|
||||||
stroke="currentColor"
|
|
||||||
stroke-linecap="round"
|
|
||||||
stroke-linejoin="round"
|
|
||||||
stroke-width="2"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
width="24"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
>
|
|
||||||
<polygon
|
|
||||||
points="6 3 20 12 6 21 6 3"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="min-w-0 flex-1"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="flex items-center gap-2"
|
|
||||||
>
|
|
||||||
<h1
|
|
||||||
class="text-lg font-semibold text-fg-primary"
|
|
||||||
>
|
|
||||||
Run Claude
|
|
||||||
</h1>
|
|
||||||
<span
|
|
||||||
class="flex items-center gap-1.5 text-[11px] text-status-success bg-status-success/10 border border-status-success/20 px-2 py-0.5 rounded-full"
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
class="w-1.5 h-1.5 rounded-full bg-status-success animate-pulse-dot"
|
|
||||||
/>
|
|
||||||
Live
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<p
|
|
||||||
class="text-xs text-fg-muted max-w-3xl"
|
|
||||||
>
|
|
||||||
Spin up a Claude Code session right inside the dashboard. Live streaming output, multi-turn conversation, and the same hooks-driven analytics as your terminal sessions.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<button
|
|
||||||
class="inline-flex items-center gap-2 rounded-lg border px-3 py-1.5 text-xs font-medium transition-colors disabled:opacity-50 disabled:cursor-not-allowed border-border bg-surface-2 text-fg-secondary hover:bg-surface-3"
|
|
||||||
disabled=""
|
|
||||||
>
|
|
||||||
<svg
|
|
||||||
class="lucide lucide-list-ordered w-3.5 h-3.5"
|
|
||||||
fill="none"
|
|
||||||
height="24"
|
|
||||||
stroke="currentColor"
|
|
||||||
stroke-linecap="round"
|
|
||||||
stroke-linejoin="round"
|
|
||||||
stroke-width="2"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
width="24"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
d="M10 12h11"
|
|
||||||
/>
|
|
||||||
<path
|
|
||||||
d="M10 18h11"
|
|
||||||
/>
|
|
||||||
<path
|
|
||||||
d="M10 6h11"
|
|
||||||
/>
|
|
||||||
<path
|
|
||||||
d="M4 10h2"
|
|
||||||
/>
|
|
||||||
<path
|
|
||||||
d="M4 6h1v4"
|
|
||||||
/>
|
|
||||||
<path
|
|
||||||
d="M6 18H4c0-1 2-2 2-3s-1-1.5-2-1"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
Active runs
|
|
||||||
</button>
|
|
||||||
</header>
|
|
||||||
<div
|
<div
|
||||||
class="rounded-xl border border-border bg-surface-1"
|
class="flex items-center gap-1.5"
|
||||||
>
|
>
|
||||||
<div
|
<button
|
||||||
class="flex flex-wrap items-center gap-x-3 gap-y-1.5 border-b border-border px-3 py-2 text-[11.5px]"
|
aria-pressed="true"
|
||||||
|
class="rounded border px-2 py-1 text-xs border-accent bg-accent/15 text-accent"
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
1 pane
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
aria-pressed="false"
|
||||||
|
class="rounded border px-2 py-1 text-xs border-border text-fg-secondary hover:border-border-light"
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
2 pane
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
aria-pressed="false"
|
||||||
|
class="rounded border px-2 py-1 text-xs border-border text-fg-secondary hover:border-border-light"
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
4 pane
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="flex min-h-0 flex-1 flex-col gap-5"
|
||||||
|
data-testid="console-body"
|
||||||
|
>
|
||||||
|
<header
|
||||||
|
class="flex items-start gap-3"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="flex items-center rounded-md border border-border bg-surface-2 p-0.5"
|
class="w-9 h-9 rounded-xl bg-accent/15 flex items-center justify-center flex-shrink-0"
|
||||||
>
|
>
|
||||||
<button
|
<svg
|
||||||
aria-pressed="true"
|
class="lucide lucide-play w-4.5 h-4.5 text-accent"
|
||||||
class="rounded px-2 py-0.5 font-medium transition-colors bg-accent/20 text-accent"
|
fill="none"
|
||||||
title="Start a fresh Claude Code session."
|
height="24"
|
||||||
type="button"
|
stroke="currentColor"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
stroke-width="2"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
width="24"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
>
|
>
|
||||||
New session
|
<polygon
|
||||||
</button>
|
points="6 3 20 12 6 21 6 3"
|
||||||
<button
|
/>
|
||||||
aria-pressed="false"
|
</svg>
|
||||||
class="rounded px-2 py-0.5 font-medium transition-colors text-fg-secondary hover:text-fg-primary"
|
|
||||||
title="Pick a session from your history and continue the conversation. Cwd is locked to the original."
|
|
||||||
type="button"
|
|
||||||
>
|
|
||||||
Resume existing session
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="px-4 py-3 border-b border-border"
|
|
||||||
>
|
|
||||||
<label
|
|
||||||
class="block text-[11px] font-semibold uppercase tracking-wider text-fg-muted mb-1.5"
|
|
||||||
>
|
|
||||||
Prompt
|
|
||||||
</label>
|
|
||||||
<textarea
|
|
||||||
class="w-full bg-surface-2 border border-border rounded-md px-3 py-2 text-[11px] font-mono text-fg-primary placeholder:text-fg-muted focus:outline-none focus:border-accent/50 resize-none"
|
|
||||||
placeholder="Ask Claude anything…"
|
|
||||||
rows="5"
|
|
||||||
/>
|
|
||||||
<div
|
<div
|
||||||
class="mt-1 text-[10px] text-fg-muted"
|
class="min-w-0 flex-1"
|
||||||
>
|
>
|
||||||
Cmd+Enter / Ctrl+Enter to send
|
|
||||||
· / for slash commands · @ for file references
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="grid grid-cols-1 gap-3 px-4 py-3 sm:grid-cols-2 lg:grid-cols-4"
|
|
||||||
>
|
|
||||||
<div>
|
|
||||||
<label
|
|
||||||
class="block text-[10px] font-semibold uppercase tracking-wider text-fg-muted mb-1"
|
|
||||||
>
|
|
||||||
Working directory
|
|
||||||
</label>
|
|
||||||
<div
|
<div
|
||||||
title="Absolute path. Defaults to the dashboard's own cwd."
|
class="flex items-center gap-2"
|
||||||
>
|
>
|
||||||
|
<h1
|
||||||
|
class="text-lg font-semibold text-fg-primary"
|
||||||
|
>
|
||||||
|
Run Claude
|
||||||
|
</h1>
|
||||||
|
<span
|
||||||
|
class="flex items-center gap-1.5 text-[11px] text-status-success bg-status-success/10 border border-status-success/20 px-2 py-0.5 rounded-full"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="w-1.5 h-1.5 rounded-full bg-status-success animate-pulse-dot"
|
||||||
|
/>
|
||||||
|
Live
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<p
|
||||||
|
class="text-xs text-fg-muted max-w-3xl"
|
||||||
|
>
|
||||||
|
Spin up a Claude Code session right inside the dashboard. Live streaming output, multi-turn conversation, and the same hooks-driven analytics as your terminal sessions.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
class="inline-flex items-center gap-2 rounded-lg border px-3 py-1.5 text-xs font-medium transition-colors disabled:opacity-50 disabled:cursor-not-allowed border-border bg-surface-2 text-fg-secondary hover:bg-surface-3"
|
||||||
|
disabled=""
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
class="lucide lucide-list-ordered w-3.5 h-3.5"
|
||||||
|
fill="none"
|
||||||
|
height="24"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
stroke-width="2"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
width="24"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M10 12h11"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M10 18h11"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M10 6h11"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M4 10h2"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M4 6h1v4"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M6 18H4c0-1 2-2 2-3s-1-1.5-2-1"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
Active runs
|
||||||
|
</button>
|
||||||
|
</header>
|
||||||
|
<div
|
||||||
|
class="rounded-xl border border-border bg-surface-1"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="flex flex-wrap items-center gap-x-3 gap-y-1.5 border-b border-border px-3 py-2 text-[11.5px]"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="flex items-center rounded-md border border-border bg-surface-2 p-0.5"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
aria-pressed="true"
|
||||||
|
class="rounded px-2 py-0.5 font-medium transition-colors bg-accent/20 text-accent"
|
||||||
|
title="Start a fresh Claude Code session."
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
New session
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
aria-pressed="false"
|
||||||
|
class="rounded px-2 py-0.5 font-medium transition-colors text-fg-secondary hover:text-fg-primary"
|
||||||
|
title="Pick a session from your history and continue the conversation. Cwd is locked to the original."
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
Resume existing session
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="px-4 py-3 border-b border-border"
|
||||||
|
>
|
||||||
|
<label
|
||||||
|
class="block text-[11px] font-semibold uppercase tracking-wider text-fg-muted mb-1.5"
|
||||||
|
>
|
||||||
|
Prompt
|
||||||
|
</label>
|
||||||
|
<textarea
|
||||||
|
class="w-full bg-surface-2 border border-border rounded-md px-3 py-2 text-[11px] font-mono text-fg-primary placeholder:text-fg-muted focus:outline-none focus:border-accent/50 resize-none"
|
||||||
|
placeholder="Ask Claude anything…"
|
||||||
|
rows="5"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
class="mt-1 text-[10px] text-fg-muted"
|
||||||
|
>
|
||||||
|
Cmd+Enter / Ctrl+Enter to send
|
||||||
|
· / for slash commands · @ for file references
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="grid grid-cols-1 gap-3 px-4 py-3 sm:grid-cols-2 lg:grid-cols-4"
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<label
|
||||||
|
class="block text-[10px] font-semibold uppercase tracking-wider text-fg-muted mb-1"
|
||||||
|
>
|
||||||
|
Working directory
|
||||||
|
</label>
|
||||||
<div
|
<div
|
||||||
class="relative"
|
title="Absolute path. Defaults to the dashboard's own cwd."
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="relative"
|
class="relative"
|
||||||
>
|
>
|
||||||
|
<div
|
||||||
|
class="relative"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
class="lucide lucide-folder-open absolute left-2 top-1/2 -translate-y-1/2 w-3.5 h-3.5 text-fg-muted pointer-events-none"
|
||||||
|
fill="none"
|
||||||
|
height="24"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
stroke-width="2"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
width="24"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="m6 14 1.5-2.9A2 2 0 0 1 9.24 10H20a2 2 0 0 1 1.94 2.5l-1.54 6a2 2 0 0 1-1.95 1.5H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h3.9a2 2 0 0 1 1.69.9l.81 1.2a2 2 0 0 0 1.67.9H18a2 2 0 0 1 2 2v2"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
<input
|
||||||
|
autocomplete="off"
|
||||||
|
class="w-full bg-surface-2 border border-border rounded-md pl-7 pr-3 py-1.5 text-[11px] font-mono text-fg-primary placeholder:text-fg-muted focus:outline-none focus:border-accent/50"
|
||||||
|
placeholder="Type to search or paste an absolute path…"
|
||||||
|
spellcheck="false"
|
||||||
|
type="text"
|
||||||
|
value=""
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label
|
||||||
|
class="block text-[10px] font-semibold uppercase tracking-wider text-fg-muted mb-1"
|
||||||
|
>
|
||||||
|
Model
|
||||||
|
</label>
|
||||||
|
<div
|
||||||
|
class="space-y-1.5"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="relative"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
class="w-full flex items-center justify-between gap-2 bg-surface-2 border border-border rounded-md px-3 py-1.5 text-[11px] text-fg-primary focus:outline-none focus:border-accent/50 hover:bg-surface-3 transition-colors disabled:opacity-60 disabled:cursor-not-allowed"
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="truncate"
|
||||||
|
>
|
||||||
|
Inherit from settings
|
||||||
|
</span>
|
||||||
|
<svg
|
||||||
|
class="lucide lucide-chevron-down w-3.5 h-3.5 text-fg-muted flex-shrink-0"
|
||||||
|
fill="none"
|
||||||
|
height="24"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
stroke-width="2"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
width="24"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="m6 9 6 6 6-6"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label
|
||||||
|
class="block text-[10px] font-semibold uppercase tracking-wider text-fg-muted mb-1"
|
||||||
|
>
|
||||||
|
Permission mode
|
||||||
|
</label>
|
||||||
|
<div
|
||||||
|
class="relative"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
class="w-full flex items-center justify-between gap-2 bg-surface-2 border border-border rounded-md px-3 py-1.5 text-[11px] text-fg-primary focus:outline-none focus:border-accent/50 hover:bg-surface-3 transition-colors disabled:opacity-60 disabled:cursor-not-allowed"
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="truncate"
|
||||||
|
>
|
||||||
|
acceptEdits (recommended)
|
||||||
|
</span>
|
||||||
<svg
|
<svg
|
||||||
class="lucide lucide-folder-open absolute left-2 top-1/2 -translate-y-1/2 w-3.5 h-3.5 text-fg-muted pointer-events-none"
|
class="lucide lucide-chevron-down w-3.5 h-3.5 text-fg-muted flex-shrink-0"
|
||||||
fill="none"
|
fill="none"
|
||||||
height="24"
|
height="24"
|
||||||
stroke="currentColor"
|
stroke="currentColor"
|
||||||
@@ -5972,30 +6062,18 @@ exports[`screen snapshots > Run 1`] = `
|
|||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
>
|
>
|
||||||
<path
|
<path
|
||||||
d="m6 14 1.5-2.9A2 2 0 0 1 9.24 10H20a2 2 0 0 1 1.94 2.5l-1.54 6a2 2 0 0 1-1.95 1.5H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h3.9a2 2 0 0 1 1.69.9l.81 1.2a2 2 0 0 0 1.67.9H18a2 2 0 0 1 2 2v2"
|
d="m6 9 6 6 6-6"
|
||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
<input
|
</button>
|
||||||
autocomplete="off"
|
|
||||||
class="w-full bg-surface-2 border border-border rounded-md pl-7 pr-3 py-1.5 text-[11px] font-mono text-fg-primary placeholder:text-fg-muted focus:outline-none focus:border-accent/50"
|
|
||||||
placeholder="Type to search or paste an absolute path…"
|
|
||||||
spellcheck="false"
|
|
||||||
type="text"
|
|
||||||
value=""
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div>
|
||||||
<div>
|
<label
|
||||||
<label
|
class="block text-[10px] font-semibold uppercase tracking-wider text-fg-muted mb-1"
|
||||||
class="block text-[10px] font-semibold uppercase tracking-wider text-fg-muted mb-1"
|
>
|
||||||
>
|
Thinking effort
|
||||||
Model
|
</label>
|
||||||
</label>
|
|
||||||
<div
|
|
||||||
class="space-y-1.5"
|
|
||||||
>
|
|
||||||
<div
|
<div
|
||||||
class="relative"
|
class="relative"
|
||||||
>
|
>
|
||||||
@@ -6006,7 +6084,7 @@ exports[`screen snapshots > Run 1`] = `
|
|||||||
<span
|
<span
|
||||||
class="truncate"
|
class="truncate"
|
||||||
>
|
>
|
||||||
Inherit from settings
|
Default (model decides)
|
||||||
</span>
|
</span>
|
||||||
<svg
|
<svg
|
||||||
class="lucide lucide-chevron-down w-3.5 h-3.5 text-fg-muted flex-shrink-0"
|
class="lucide lucide-chevron-down w-3.5 h-3.5 text-fg-muted flex-shrink-0"
|
||||||
@@ -6028,109 +6106,35 @@ exports[`screen snapshots > Run 1`] = `
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
|
||||||
<label
|
|
||||||
class="block text-[10px] font-semibold uppercase tracking-wider text-fg-muted mb-1"
|
|
||||||
>
|
|
||||||
Permission mode
|
|
||||||
</label>
|
|
||||||
<div
|
|
||||||
class="relative"
|
|
||||||
>
|
|
||||||
<button
|
|
||||||
class="w-full flex items-center justify-between gap-2 bg-surface-2 border border-border rounded-md px-3 py-1.5 text-[11px] text-fg-primary focus:outline-none focus:border-accent/50 hover:bg-surface-3 transition-colors disabled:opacity-60 disabled:cursor-not-allowed"
|
|
||||||
type="button"
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
class="truncate"
|
|
||||||
>
|
|
||||||
acceptEdits (recommended)
|
|
||||||
</span>
|
|
||||||
<svg
|
|
||||||
class="lucide lucide-chevron-down w-3.5 h-3.5 text-fg-muted flex-shrink-0"
|
|
||||||
fill="none"
|
|
||||||
height="24"
|
|
||||||
stroke="currentColor"
|
|
||||||
stroke-linecap="round"
|
|
||||||
stroke-linejoin="round"
|
|
||||||
stroke-width="2"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
width="24"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
d="m6 9 6 6 6-6"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label
|
|
||||||
class="block text-[10px] font-semibold uppercase tracking-wider text-fg-muted mb-1"
|
|
||||||
>
|
|
||||||
Thinking effort
|
|
||||||
</label>
|
|
||||||
<div
|
|
||||||
class="relative"
|
|
||||||
>
|
|
||||||
<button
|
|
||||||
class="w-full flex items-center justify-between gap-2 bg-surface-2 border border-border rounded-md px-3 py-1.5 text-[11px] text-fg-primary focus:outline-none focus:border-accent/50 hover:bg-surface-3 transition-colors disabled:opacity-60 disabled:cursor-not-allowed"
|
|
||||||
type="button"
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
class="truncate"
|
|
||||||
>
|
|
||||||
Default (model decides)
|
|
||||||
</span>
|
|
||||||
<svg
|
|
||||||
class="lucide lucide-chevron-down w-3.5 h-3.5 text-fg-muted flex-shrink-0"
|
|
||||||
fill="none"
|
|
||||||
height="24"
|
|
||||||
stroke="currentColor"
|
|
||||||
stroke-linecap="round"
|
|
||||||
stroke-linejoin="round"
|
|
||||||
stroke-width="2"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
width="24"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
d="m6 9 6 6 6-6"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="border-t border-border px-4 py-3 flex items-center justify-between gap-3 flex-wrap"
|
|
||||||
>
|
|
||||||
<div
|
<div
|
||||||
class="flex items-center gap-3 text-[11px] min-w-0"
|
class="border-t border-border px-4 py-3 flex items-center justify-between gap-3 flex-wrap"
|
||||||
/>
|
|
||||||
<button
|
|
||||||
class="inline-flex items-center gap-2 rounded-lg border border-accent/40 bg-accent/15 hover:bg-accent/25 text-accent px-4 py-1.5 text-sm font-medium transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
|
||||||
disabled=""
|
|
||||||
>
|
>
|
||||||
<svg
|
<div
|
||||||
class="lucide lucide-play w-3.5 h-3.5"
|
class="flex items-center gap-3 text-[11px] min-w-0"
|
||||||
fill="none"
|
/>
|
||||||
height="24"
|
<button
|
||||||
stroke="currentColor"
|
class="inline-flex items-center gap-2 rounded-lg border border-accent/40 bg-accent/15 hover:bg-accent/25 text-accent px-4 py-1.5 text-sm font-medium transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
||||||
stroke-linecap="round"
|
disabled=""
|
||||||
stroke-linejoin="round"
|
|
||||||
stroke-width="2"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
width="24"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
>
|
>
|
||||||
<polygon
|
<svg
|
||||||
points="6 3 20 12 6 21 6 3"
|
class="lucide lucide-play w-3.5 h-3.5"
|
||||||
/>
|
fill="none"
|
||||||
</svg>
|
height="24"
|
||||||
Run
|
stroke="currentColor"
|
||||||
</button>
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
stroke-width="2"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
width="24"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<polygon
|
||||||
|
points="6 3 20 12 6 21 6 3"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
Run
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user