diff --git a/apps/desktop/src/renderer/components/app/AppShell.tsx b/apps/desktop/src/renderer/components/app/AppShell.tsx index ae49627c2..cec81749a 100644 --- a/apps/desktop/src/renderer/components/app/AppShell.tsx +++ b/apps/desktop/src/renderer/components/app/AppShell.tsx @@ -406,8 +406,8 @@ export function AppShell({ children }: { children: React.ReactNode }) { const laneId = aiFailure.laneId; if (!laneId) return; selectLane(laneId); - setLaneInspectorTab(laneId, "packs"); - window.location.hash = `#/lanes?laneId=${encodeURIComponent(laneId)}&focus=single&inspectorTab=packs`; + setLaneInspectorTab(laneId, "context"); + window.location.hash = `#/lanes?laneId=${encodeURIComponent(laneId)}&focus=single&inspectorTab=context`; }} title="Open lane packs" > diff --git a/apps/desktop/src/renderer/components/history/HistoryPage.tsx b/apps/desktop/src/renderer/components/history/HistoryPage.tsx index bdf6b14a4..b8d447f1f 100644 --- a/apps/desktop/src/renderer/components/history/HistoryPage.tsx +++ b/apps/desktop/src/renderer/components/history/HistoryPage.tsx @@ -334,10 +334,10 @@ export function HistoryPage() { ) : null} diff --git a/apps/desktop/src/renderer/components/lanes/LaneInspectorPane.tsx b/apps/desktop/src/renderer/components/lanes/LaneInspectorPane.tsx index a4856a727..4fbaba5f3 100644 --- a/apps/desktop/src/renderer/components/lanes/LaneInspectorPane.tsx +++ b/apps/desktop/src/renderer/components/lanes/LaneInspectorPane.tsx @@ -6,26 +6,26 @@ import { LanePrPanel } from "../prs/LanePrPanel"; import { LaneConflictsPanel } from "./LaneConflictsPanel"; const tabTrigger = - "px-2.5 py-1.5 text-xs font-semibold rounded-lg text-muted-fg transition-colors data-[state=active]:text-fg data-[state=active]:bg-accent/10"; + "px-3 py-1.5 text-xs font-semibold rounded-md cursor-pointer select-none text-muted-fg border border-transparent transition-all hover:text-fg hover:bg-white/5 data-[state=active]:text-fg data-[state=active]:bg-accent/15 data-[state=active]:border-border/30 data-[state=active]:shadow-sm"; export function LaneInspectorPane({ laneId, defaultTab }: { laneId: string | null; - defaultTab?: "packs" | "pr" | "conflicts"; + defaultTab?: "context" | "pr" | "conflicts"; }) { - const [tab, setTab] = useState<"packs" | "pr" | "conflicts">(defaultTab ?? "packs"); + const [tab, setTab] = useState<"context" | "pr" | "conflicts">(defaultTab ?? "context"); return ( setTab(v as "packs" | "pr" | "conflicts")} + onValueChange={(v) => setTab(v as "context" | "pr" | "conflicts")} className="flex h-full flex-col" > - - Packs + + Context PR @@ -35,7 +35,7 @@ export function LaneInspectorPane({
- + diff --git a/apps/desktop/src/renderer/components/lanes/LaneTerminalsPanel.tsx b/apps/desktop/src/renderer/components/lanes/LaneTerminalsPanel.tsx index 60507e742..8266a4b63 100644 --- a/apps/desktop/src/renderer/components/lanes/LaneTerminalsPanel.tsx +++ b/apps/desktop/src/renderer/components/lanes/LaneTerminalsPanel.tsx @@ -506,7 +506,7 @@ export function LaneTerminalsPanel({ overrideLaneId }: { overrideLaneId?: string - +
Terminal Settings diff --git a/apps/desktop/src/renderer/components/lanes/LanesPage.tsx b/apps/desktop/src/renderer/components/lanes/LanesPage.tsx index b5805e556..3e00f31b2 100644 --- a/apps/desktop/src/renderer/components/lanes/LanesPage.tsx +++ b/apps/desktop/src/renderer/components/lanes/LanesPage.tsx @@ -2,7 +2,7 @@ import React, { useCallback, useEffect, useMemo, useRef, useState } from "react" import { useNavigate, useSearchParams } from "react-router-dom"; import * as Dialog from "@radix-ui/react-dialog"; import { Group, Panel } from "react-resizable-panels"; -import { Check, ChevronDown, FileCode2, GitBranch, Home, Layers3, Link2, Maximize2, Pin, Play, Plus, Search, Terminal, X } from "lucide-react"; +import { Check, ChevronDown, FileCode2, GitBranch, Home, Layers3, Link2, Maximize2, Pin, Plus, Search, Terminal, X } from "lucide-react"; import { useAppStore } from "../../state/appStore"; import { EmptyState } from "../ui/EmptyState"; import { cn } from "../ui/cn"; @@ -95,16 +95,6 @@ function mergeUnique(...lists: string[][]): string[] { return out; } -function toggleFilterToken(query: string, token: string): string { - const normalizedToken = token.toLowerCase().trim(); - if (!normalizedToken.length) return query; - const tokens = query.trim().split(/\s+/).map((part) => part.toLowerCase()).filter(Boolean); - const next = new Set(tokens); - if (next.has(normalizedToken)) next.delete(normalizedToken); - else next.add(normalizedToken); - return Array.from(next).join(" "); -} - function matchesLaneFilterToken(lane: LaneSummary, isPinned: boolean, token: string): boolean { const normalized = token.trim().toLowerCase(); if (!normalized.length) return true; @@ -281,6 +271,9 @@ export function LanesPage() { const [branchCheckoutError, setBranchCheckoutError] = useState(null); const branchDropdownRef = useRef(null); + const [addLaneDropdownOpen, setAddLaneDropdownOpen] = useState(false); + const addLaneDropdownRef = useRef(null); + const [lanePaneDetails, setLanePaneDetails] = useState>({}); const [laneContextMenu, setLaneContextMenu] = useState<{ laneId: string; x: number; y: number } | null>(null); const [expandedLaneId, setExpandedLaneId] = useState(null); @@ -344,6 +337,17 @@ export function LanesPage() { return () => document.removeEventListener("mousedown", handler); }, [branchDropdownOpen]); + useEffect(() => { + if (!addLaneDropdownOpen) return; + const handler = (e: MouseEvent) => { + if (addLaneDropdownRef.current && !addLaneDropdownRef.current.contains(e.target as Node)) { + setAddLaneDropdownOpen(false); + } + }; + document.addEventListener("mousedown", handler); + return () => document.removeEventListener("mousedown", handler); + }, [addLaneDropdownOpen]); + /* ---- Conflict loading ---- */ const loadConflictStatuses = useCallback(async () => { @@ -401,7 +405,7 @@ export function LanesPage() { const inspectorTab = params.get("inspectorTab"); if (laneId) { selectLane(laneId); - if (inspectorTab === "terminals" || inspectorTab === "packs" || inspectorTab === "stack" || inspectorTab === "merge") { + if (inspectorTab === "terminals" || inspectorTab === "context" || inspectorTab === "stack" || inspectorTab === "merge") { setLaneInspectorTab(laneId, inspectorTab); } if (params.get("focus") === "single") { @@ -552,10 +556,6 @@ export function LanesPage() { /* ---- Lane management actions ---- */ - const activeFilterTokens = useMemo(() => { - return new Set(laneFilter.trim().toLowerCase().split(/\s+/).filter(Boolean)); - }, [laneFilter]); - const currentPrimaryBranch = useMemo( () => primaryBranches.find((branch) => branch.isCurrent)?.name ?? primaryLane?.branchRef ?? "", [primaryBranches, primaryLane?.branchRef] @@ -927,93 +927,57 @@ export function LanesPage() { ) : null}
- - - - - -
- - - - + {/* Add Lane dropdown */} +
+ + {addLaneDropdownOpen ? ( +
+ + +
+ ) : null} +
- {filteredLanes.length}/{sortedLanes.length} · shift-click split · j/k move · [ ] cycle · / filter + {filteredLanes.length}/{sortedLanes.length} · shift-click split
@@ -1035,7 +999,7 @@ export function LanesPage() { key={lane.id} type="button" className={cn( - "inline-flex max-w-[320px] shrink-0 items-center gap-1 rounded-xl px-2 py-1 text-xs transition-colors", + "group inline-flex max-w-[320px] shrink-0 items-center gap-1 rounded-xl px-2 py-1 text-xs transition-colors", isSelected ? "bg-accent/20 text-fg shadow-card ring-1 ring-accent/40 font-semibold" : isVisible @@ -1055,9 +1019,7 @@ export function LanesPage() { > {isPrimary ? ( - ) : ( - - )} + ) : null} {lane.name} {isPrimary ? {lane.branchRef} : null} @@ -1078,8 +1040,10 @@ export function LanesPage() { {!isPrimary ? ( { event.stopPropagation(); @@ -1208,18 +1172,20 @@ export function LanesPage() { {/* Fullscreen lane overlay */} {expandedLaneId && lanesById.has(expandedLaneId) ? ( -
-
-
- {lanesById.get(expandedLaneId)?.name} - Fullscreen -
-
Copy Path ) : null} + {ctxLane && ctxLane.laneType !== "primary" ? ( + + ) : null}
); })() : null} @@ -1255,7 +1234,7 @@ export function LanesPage() { {/* Manage Lane dialog */} - +
Manage lane @@ -1326,7 +1305,7 @@ export function LanesPage() { {/* Create Lane dialog */} - +
Create lane @@ -1437,7 +1416,7 @@ export function LanesPage() { {/* Attach Lane dialog */} - +
Attach lane diff --git a/apps/desktop/src/renderer/components/packs/PackViewer.tsx b/apps/desktop/src/renderer/components/packs/PackViewer.tsx index b41e06f26..dc3e2df82 100644 --- a/apps/desktop/src/renderer/components/packs/PackViewer.tsx +++ b/apps/desktop/src/renderer/components/packs/PackViewer.tsx @@ -1,7 +1,6 @@ import React, { useEffect, useMemo, useRef, useState } from "react"; -import * as Dialog from "@radix-ui/react-dialog"; -import { RefreshCw, Sparkles } from "lucide-react"; -import type { HostedBootstrapConfig, HostedJobStatusResult, HostedStatus, PackEvent, PackSummary, PackVersionSummary } from "../../../shared/types"; +import { RefreshCw } from "lucide-react"; +import type { HostedBootstrapConfig, HostedJobStatusResult, HostedStatus, PackSummary } from "../../../shared/types"; import { useAppStore } from "../../state/appStore"; import { Button } from "../ui/Button"; import { EmptyState } from "../ui/EmptyState"; @@ -41,52 +40,6 @@ function PackBody({ pack }: { pack: PackSummary | null }) { ); } -function formatPackEvent(ev: PackEvent): { title: string; detail: string; tone: "neutral" | "good" | "warn" | "bad" } { - const payload = ev.payload ?? {}; - const trigger = typeof payload.trigger === "string" ? payload.trigger : typeof payload.reason === "string" ? payload.reason : null; - const providerMode = typeof payload.providerMode === "string" ? payload.providerMode : null; - const error = typeof payload.error === "string" ? payload.error : null; - const jobId = typeof payload.jobId === "string" ? (payload.jobId as string) : null; - const jobStatus = typeof payload.status === "string" ? (payload.status as string) : null; - - if (ev.eventType === "refresh_triggered") { - return { title: "Pack refreshed", detail: trigger ? `trigger: ${trigger}` : "deterministic refresh", tone: "good" }; - } - if (ev.eventType === "narrative_requested") { - return { - title: "AI update requested", - detail: `${providerMode ? `provider: ${providerMode}` : "provider: ?"}${jobId ? ` · job ${shortId(jobId)}` : ""}${jobStatus ? ` · ${jobStatus}` : ""}${trigger ? ` · trigger: ${trigger}` : ""}`, - tone: "neutral" - }; - } - if (ev.eventType === "narrative_update") { - const provider = typeof payload.provider === "string" ? payload.provider : null; - const model = typeof payload.model === "string" ? payload.model : null; - const jobId = typeof payload.jobId === "string" ? payload.jobId : null; - const suffix = provider || model ? `${provider ?? "hosted"}${model ? ` · ${model}` : ""}` : jobId ? `job ${jobId}` : "updated"; - if (provider === "mock") { - return { - title: "AI details updated (mock)", - detail: "Hosted backend is in mock mode. Configure the hosted LLM secret/env to enable Gemini Flash.", - tone: "warn" - }; - } - return { title: "AI details updated", detail: suffix, tone: "good" }; - } - if (ev.eventType === "narrative_failed") { - const suffix = `${jobId ? `job ${shortId(jobId)} · ` : ""}${error ?? "unknown error"}`; - return { title: "AI update failed", detail: suffix, tone: "bad" }; - } - if (ev.eventType === "version_created") { - const vn = payload.versionNumber; - return { title: `Version saved${typeof vn === "number" ? ` (v${vn})` : ""}`, detail: "snapshot recorded", tone: "neutral" }; - } - if (ev.eventType === "checkpoint") { - return { title: "Checkpoint recorded", detail: "session boundary captured", tone: "neutral" }; - } - return { title: ev.eventType, detail: "", tone: "neutral" }; -} - function hostedReadiness( status: HostedStatus | null, error: string | null, @@ -117,6 +70,8 @@ function hostedReadiness( return null; } +const AI_COOLDOWN_MS = 30_000; + export function PackViewer({ laneId }: { laneId: string | null }) { const navigate = useNavigate(); const providerMode = useAppStore((s) => s.providerMode); @@ -126,7 +81,6 @@ export function PackViewer({ laneId }: { laneId: string | null }) { const [projectPack, setProjectPack] = useState(null); const [refreshBusy, setRefreshBusy] = useState(false); - const [aiBusy, setAiBusy] = useState(false); const [aiQueued, setAiQueued] = useState(false); const [error, setError] = useState(null); const [aiError, setAiError] = useState(null); @@ -137,25 +91,13 @@ export function PackViewer({ laneId }: { laneId: string | null }) { const [aiJob, setAiJob] = useState(null); - const [versionsDialogOpen, setVersionsDialogOpen] = useState(false); - const [versionsLoading, setVersionsLoading] = useState(false); - const [versions, setVersions] = useState([]); - const [fromVersionId, setFromVersionId] = useState(null); - const [toVersionId, setToVersionId] = useState(null); - const [diffBusy, setDiffBusy] = useState(false); - const [diffText, setDiffText] = useState(null); - - const [eventsDialogOpen, setEventsDialogOpen] = useState(false); - const [eventsLoading, setEventsLoading] = useState(false); - const [events, setEvents] = useState([]); - const activePack = scope === "project" ? projectPack : lanePack; - const activePackKey = activePack?.packKey ?? null; const activeMeta = (activePack?.metadata ?? null) as Record | null; const lanePackKey = laneId ? `lane:${laneId}` : null; const refreshTimers = useRef<{ lane?: number | null; project?: number | null }>({}); + const lastAiTriggerRef = useRef(0); const fetchLanePack = async () => { if (!laneId) return; @@ -185,7 +127,7 @@ export function PackViewer({ laneId }: { laneId: string | null }) { }, 120); }; - const refreshDeterministic = async () => { + const refreshCombined = async () => { setRefreshBusy(true); setError(null); try { @@ -196,8 +138,23 @@ export function PackViewer({ laneId }: { laneId: string | null }) { if (!laneId) return; const pack = await window.ade.packs.refreshLanePack(laneId); setLanePack(pack); - // Manual lane refresh also refreshes project pack in main. await fetchProjectPack().catch(() => {}); + + // Rate-limited AI narrative + const now = Date.now(); + if (providerMode !== "guest" && now - lastAiTriggerRef.current >= AI_COOLDOWN_MS) { + lastAiTriggerRef.current = now; + setAiQueued(true); + setAiError(null); + try { + const aiPack = await window.ade.packs.generateNarrative(laneId); + setLanePack(aiPack); + } catch (err) { + setAiError(err instanceof Error ? err.message : String(err)); + } finally { + setAiQueued(false); + } + } } } catch (err) { setError(err instanceof Error ? err.message : String(err)); @@ -206,74 +163,6 @@ export function PackViewer({ laneId }: { laneId: string | null }) { } }; - const updateWithAi = async () => { - if (!laneId) return; - setAiBusy(true); - setAiQueued(true); - setAiError(null); - setError(null); - try { - const pack = await window.ade.packs.generateNarrative(laneId); - setLanePack(pack); - } catch (err) { - const message = err instanceof Error ? err.message : String(err); - setAiError(message); - } finally { - setAiBusy(false); - setAiQueued(false); - } - }; - - const openVersions = async () => { - if (!activePackKey) return; - setVersionsDialogOpen(true); - setVersionsLoading(true); - setDiffText(null); - setError(null); - try { - const list = await window.ade.packs.listVersions({ packKey: activePackKey, limit: 60 }); - setVersions(list); - setFromVersionId(list[1]?.id ?? list[0]?.id ?? null); - setToVersionId(list[0]?.id ?? null); - } catch (err) { - setError(err instanceof Error ? err.message : String(err)); - setVersions([]); - } finally { - setVersionsLoading(false); - } - }; - - const runDiff = async () => { - if (!fromVersionId || !toVersionId) return; - if (fromVersionId === toVersionId) return; - setDiffBusy(true); - setError(null); - try { - const out = await window.ade.packs.diffVersions({ fromId: fromVersionId, toId: toVersionId }); - setDiffText(out.trim().length ? out : "(no diff)"); - } catch (err) { - setError(err instanceof Error ? err.message : String(err)); - } finally { - setDiffBusy(false); - } - }; - - const openActivity = async () => { - if (!activePackKey) return; - setEventsDialogOpen(true); - setEventsLoading(true); - setError(null); - try { - const list = await window.ade.packs.listEvents({ packKey: activePackKey, limit: 120 }); - setEvents(list); - } catch (err) { - setError(err instanceof Error ? err.message : String(err)); - setEvents([]); - } finally { - setEventsLoading(false); - } - }; - useEffect(() => { setLanePack(null); setProjectPack(null); @@ -404,9 +293,6 @@ export function PackViewer({ laneId }: { laneId: string | null }) { if (providerMode === "hosted") { const ready = hostedReadiness(hostedStatus, hostedError, aiJob); if (ready) return ready; - if (aiBusy && !aiJob) { - return { tone: "neutral" as const, message: "Submitting job…" }; - } if (aiJob?.jobId) { const elapsedSec = Math.max(0, Math.floor((Date.now() - aiJob.statusSinceMs) / 1000)); if (aiJob.status === "queued") { @@ -423,18 +309,18 @@ export function PackViewer({ laneId }: { laneId: string | null }) { } } if (aiQueued) { - return { tone: "neutral" as const, message: "AI update queued… (this will update automatically after pack refresh)." }; + return { tone: "neutral" as const, message: "AI update queued…" }; } - return { tone: "neutral" as const, message: "AI details update automatically after pack refresh. Use the button to re-run on demand." }; + return null; } if (providerMode === "byok") { if (aiQueued) { - return { tone: "neutral" as const, message: "AI update queued… (this will update automatically after pack refresh)." }; + return { tone: "neutral" as const, message: "AI update queued…" }; } - return { tone: "neutral" as const, message: "BYOK enabled. AI details update automatically after pack refresh (if configured). Use the button to re-run on demand." }; + return null; } return null; - }, [scope, providerMode, hostedStatus, hostedError, aiQueued, aiJob, aiBusy]); + }, [scope, providerMode, hostedStatus, hostedError, aiQueued, aiJob]); const aiMetaHint = useMemo(() => { if (scope !== "lane") return null; @@ -514,10 +400,10 @@ export function PackViewer({ laneId }: { laneId: string | null }) {
-
+
- - - {scope === "lane" ? ( - - ) : null} - - -
@@ -657,145 +526,6 @@ export function PackViewer({ laneId }: { laneId: string | null }) { {activePack?.path ?
{activePack.path}
: null} - - setVersionsDialogOpen(open)}> - - - -
- Pack Versions - - - -
- {versionsLoading ? ( -
Loading versions…
- ) : ( -
-
- {versions.length === 0 ? ( -
No versions recorded yet.
- ) : ( -
- {versions.map((v) => ( -
-
-
v{v.versionNumber}
-
{new Date(v.createdAt).toLocaleString()}
-
-
{v.contentHash.slice(0, 12)}
-
- - -
-
- ))} -
- )} -
-
-
-
Diff
- -
- {diffText ? ( -
-                      {diffText}
-                    
- ) : ( -
Select two versions and run diff.
- )} -
-
- )} -
-
-
- - setEventsDialogOpen(open)}> - - - -
- Activity - - - -
- {eventsLoading ? ( -
Loading activity…
- ) : events.length === 0 ? ( -
No activity recorded yet.
- ) : ( -
-
- {events.map((ev) => { - const formatted = formatPackEvent(ev); - const opId = typeof ev.payload?.operationId === "string" ? (ev.payload.operationId as string) : null; - return ( -
-
-
-
- {formatted.title} -
- {formatted.detail ?
{formatted.detail}
: null} -
-
-
{new Date(ev.createdAt).toLocaleString()}
- {opId ? ( - - ) : null} -
-
-
- ); - })} -
-
- )} -
-
-
); } diff --git a/apps/desktop/src/renderer/components/prs/LanePrPanel.tsx b/apps/desktop/src/renderer/components/prs/LanePrPanel.tsx index 8554c0d7e..e4892a669 100644 --- a/apps/desktop/src/renderer/components/prs/LanePrPanel.tsx +++ b/apps/desktop/src/renderer/components/prs/LanePrPanel.tsx @@ -417,6 +417,9 @@ export function LanePrPanel({ laneId }: { laneId: string | null }) { +
diff --git a/apps/desktop/src/renderer/state/appStore.ts b/apps/desktop/src/renderer/state/appStore.ts index 869f5a449..8ebaf5a0f 100644 --- a/apps/desktop/src/renderer/state/appStore.ts +++ b/apps/desktop/src/renderer/state/appStore.ts @@ -49,7 +49,7 @@ type AppState = { switchProjectToPath: (rootPath: string) => Promise; }; -export type LaneInspectorTab = "terminals" | "packs" | "stack" | "merge"; +export type LaneInspectorTab = "terminals" | "context" | "stack" | "merge"; export const useAppStore = create((set, get) => ({ project: null,