| ... | @@ -298,133 +298,139 @@ export default function DiffPanel({ mode = "inline" }: DiffPanelProps) { | ... | @@ -298,133 +298,139 @@ export default function DiffPanel({ mode = "inline" }: DiffPanelProps) { |
| 298 | }, [selectedTurn?.turnId, selectedTurnId]); | 298 | }, [selectedTurn?.turnId, selectedTurnId]); |
| 299 | | 299 | |
| 300 | const shouldUseDragRegion = isElectron && mode !== "sheet"; | 300 | const shouldUseDragRegion = isElectron && mode !== "sheet"; |
| 301 | | 301 | const headerRow = ( |
| 302 | return ( | 302 | <> |
| 303 | <div | 303 | <div className="relative min-w-0 flex-1 [-webkit-app-region:no-drag]"> |
| 304 | className={cn( | 304 | {canScrollTurnStripLeft && ( |
| 305 | "flex h-full min-w-0 flex-col bg-card", | 305 | <div className="pointer-events-none absolute inset-y-0 left-8 z-10 w-7 bg-linear-to-r from-card to-transparent" /> |
| 306 | mode === "inline" | 306 | )} |
| 307 | ? "w-[42vw] min-w-[360px] max-w-[560px] shrink-0 border-l border-border" | 307 | {canScrollTurnStripRight && ( |
| 308 | : "w-full", | 308 | <div className="pointer-events-none absolute inset-y-0 right-8 z-10 w-7 bg-linear-to-l from-card to-transparent" /> |
| 309 | )} | 309 | )} |
| 310 | > | 310 | <button |
| 311 | <div className="border-b border-border"> | 311 | type="button" |
| 312 | <div | | |
| 313 | className={cn( | 312 | className={cn( |
| 314 | "flex h-12 items-center justify-between gap-2 px-4", | 313 | "absolute left-0 top-1/2 z-20 inline-flex size-6 -translate-y-1/2 items-center justify-center rounded-md border bg-background/90 text-muted-foreground transition-colors", |
| 315 | shouldUseDragRegion ? "drag-region" : undefined, | 314 | canScrollTurnStripLeft |
| | 315 | ? "border-border/70 hover:border-border hover:text-foreground" |
| | 316 | : "cursor-not-allowed border-border/40 text-muted-foreground/40", |
| 316 | )} | 317 | )} |
| | 318 | onClick={() => scrollTurnStripBy(-180)} |
| | 319 | disabled={!canScrollTurnStripLeft} |
| | 320 | aria-label="Scroll turn list left" |
| 317 | > | 321 | > |
| 318 | <div className="relative min-w-0 flex-1 [-webkit-app-region:no-drag]"> | 322 | <ChevronLeftIcon className="size-3.5" /> |
| 319 | {canScrollTurnStripLeft && ( | 323 | </button> |
| 320 | <div className="pointer-events-none absolute inset-y-0 left-8 z-10 w-7 bg-linear-to-r from-card to-transparent" /> | 324 | <button |
| 321 | )} | 325 | type="button" |
| 322 | {canScrollTurnStripRight && ( | 326 | className={cn( |
| 323 | <div className="pointer-events-none absolute inset-y-0 right-8 z-10 w-7 bg-linear-to-l from-card to-transparent" /> | 327 | "absolute right-0 top-1/2 z-20 inline-flex size-6 -translate-y-1/2 items-center justify-center rounded-md border bg-background/90 text-muted-foreground transition-colors", |
| | 328 | canScrollTurnStripRight |
| | 329 | ? "border-border/70 hover:border-border hover:text-foreground" |
| | 330 | : "cursor-not-allowed border-border/40 text-muted-foreground/40", |
| 324 | )} | 331 | )} |
| | 332 | onClick={() => scrollTurnStripBy(180)} |
| | 333 | disabled={!canScrollTurnStripRight} |
| | 334 | aria-label="Scroll turn list right" |
| | 335 | > |
| | 336 | <ChevronRightIcon className="size-3.5" /> |
| | 337 | </button> |
| | 338 | <div |
| | 339 | ref={turnStripRef} |
| | 340 | className="turn-chip-strip flex gap-1 overflow-x-auto px-8 py-0.5" |
| | 341 | > |
| 325 | <button | 342 | <button |
| 326 | type="button" | 343 | type="button" |
| 327 | className={cn( | 344 | className="shrink-0 rounded-md" |
| 328 | "absolute left-0 top-1/2 z-20 inline-flex size-6 -translate-y-1/2 items-center justify-center rounded-md border bg-background/90 text-muted-foreground transition-colors", | 345 | onClick={selectWholeConversation} |
| 329 | canScrollTurnStripLeft | 346 | data-turn-chip-selected={selectedTurnId === null} |
| 330 | ? "border-border/70 hover:border-border hover:text-foreground" | | |
| 331 | : "cursor-not-allowed border-border/40 text-muted-foreground/40", | | |
| 332 | )} | | |
| 333 | onClick={() => scrollTurnStripBy(-180)} | | |
| 334 | disabled={!canScrollTurnStripLeft} | | |
| 335 | aria-label="Scroll turn list left" | | |
| 336 | > | | |
| 337 | <ChevronLeftIcon className="size-3.5" /> | | |
| 338 | </button> | | |
| 339 | <button | | |
| 340 | type="button" | | |
| 341 | className={cn( | | |
| 342 | "absolute right-0 top-1/2 z-20 inline-flex size-6 -translate-y-1/2 items-center justify-center rounded-md border bg-background/90 text-muted-foreground transition-colors", | | |
| 343 | canScrollTurnStripRight | | |
| 344 | ? "border-border/70 hover:border-border hover:text-foreground" | | |
| 345 | : "cursor-not-allowed border-border/40 text-muted-foreground/40", | | |
| 346 | )} | | |
| 347 | onClick={() => scrollTurnStripBy(180)} | | |
| 348 | disabled={!canScrollTurnStripRight} | | |
| 349 | aria-label="Scroll turn list right" | | |
| 350 | > | 347 | > |
| 351 | <ChevronRightIcon className="size-3.5" /> | 348 | <div |
| | 349 | className={cn( |
| | 350 | "rounded-md border px-2 py-1 text-left transition-colors", |
| | 351 | selectedTurnId === null |
| | 352 | ? "border-border bg-accent text-accent-foreground" |
| | 353 | : "border-border/70 bg-background/70 text-muted-foreground/80 hover:border-border hover:text-foreground/80", |
| | 354 | )} |
| | 355 | > |
| | 356 | <div className="text-[10px] leading-tight font-medium">All turns</div> |
| | 357 | </div> |
| 352 | </button> | 358 | </button> |
| 353 | <div | 359 | {orderedTurnDiffSummaries.map((summary) => ( |
| 354 | ref={turnStripRef} | | |
| 355 | className="turn-chip-strip flex gap-1 overflow-x-auto px-8 py-0.5" | | |
| 356 | > | | |
| 357 | <button | 360 | <button |
| | 361 | key={summary.turnId} |
| 358 | type="button" | 362 | type="button" |
| 359 | className="shrink-0 rounded-md" | 363 | className="shrink-0 rounded-md" |
| 360 | onClick={selectWholeConversation} | 364 | onClick={() => selectTurn(summary.turnId)} |
| 361 | data-turn-chip-selected={selectedTurnId === null} | 365 | title={summary.turnId} |
| | 366 | data-turn-chip-selected={summary.turnId === selectedTurn?.turnId} |
| 362 | > | 367 | > |
| 363 | <div | 368 | <div |
| 364 | className={cn( | 369 | className={cn( |
| 365 | "rounded-md border px-2 py-1 text-left transition-colors", | 370 | "rounded-md border px-2 py-1 text-left transition-colors", |
| 366 | selectedTurnId === null | 371 | summary.turnId === selectedTurn?.turnId |
| 367 | ? "border-border bg-accent text-accent-foreground" | 372 | ? "border-border bg-accent text-accent-foreground" |
| 368 | : "border-border/70 bg-background/70 text-muted-foreground/80 hover:border-border hover:text-foreground/80", | 373 | : "border-border/70 bg-background/70 text-muted-foreground/80 hover:border-border hover:text-foreground/80", |
| 369 | )} | 374 | )} |
| 370 | > | 375 | > |
| 371 | <div className="text-[10px] leading-tight font-medium">All turns</div> | 376 | <div className="flex items-center gap-1"> |
| | 377 | <span className="text-[10px] leading-tight font-medium"> |
| | 378 | Turn{" "} |
| | 379 | {summary.checkpointTurnCount ?? |
| | 380 | inferredCheckpointTurnCountByTurnId[summary.turnId] ?? |
| | 381 | "?"} |
| | 382 | </span> |
| | 383 | <span className="text-[9px] leading-tight opacity-70"> |
| | 384 | {formatTurnChipTimestamp(summary.completedAt)} |
| | 385 | </span> |
| | 386 | </div> |
| 372 | </div> | 387 | </div> |
| 373 | </button> | 388 | </button> |
| 374 | {orderedTurnDiffSummaries.map((summary) => ( | 389 | ))} |
| 375 | <button | | |
| 376 | key={summary.turnId} | | |
| 377 | type="button" | | |
| 378 | className="shrink-0 rounded-md" | | |
| 379 | onClick={() => selectTurn(summary.turnId)} | | |
| 380 | title={summary.turnId} | | |
| 381 | data-turn-chip-selected={summary.turnId === selectedTurn?.turnId} | | |
| 382 | > | | |
| 383 | <div | | |
| 384 | className={cn( | | |
| 385 | "rounded-md border px-2 py-1 text-left transition-colors", | | |
| 386 | summary.turnId === selectedTurn?.turnId | | |
| 387 | ? "border-border bg-accent text-accent-foreground" | | |
| 388 | : "border-border/70 bg-background/70 text-muted-foreground/80 hover:border-border hover:text-foreground/80", | | |
| 389 | )} | | |
| 390 | > | | |
| 391 | <div className="flex items-center gap-1"> | | |
| 392 | <span className="text-[10px] leading-tight font-medium"> | | |
| 393 | Turn{" "} | | |
| 394 | {summary.checkpointTurnCount ?? | | |
| 395 | inferredCheckpointTurnCountByTurnId[summary.turnId] ?? | | |
| 396 | "?"} | | |
| 397 | </span> | | |
| 398 | <span className="text-[9px] leading-tight opacity-70"> | | |
| 399 | {formatTurnChipTimestamp(summary.completedAt)} | | |
| 400 | </span> | | |
| 401 | </div> | | |
| 402 | </div> | | |
| 403 | </button> | | |
| 404 | ))} | | |
| 405 | </div> | | |
| 406 | </div> | | |
| 407 | <ToggleGroup | | |
| 408 | className="shrink-0 [-webkit-app-region:no-drag]" | | |
| 409 | variant="outline" | | |
| 410 | size="xs" | | |
| 411 | value={[diffRenderMode]} | | |
| 412 | onValueChange={(value) => { | | |
| 413 | const next = value[0]; | | |
| 414 | if (next === "stacked" || next === "split") { | | |
| 415 | setDiffRenderMode(next); | | |
| 416 | } | | |
| 417 | }} | | |
| 418 | > | | |
| 419 | <Toggle aria-label="Stacked diff view" value="stacked"> | | |
| 420 | <Rows3Icon className="size-3" /> | | |
| 421 | </Toggle> | | |
| 422 | <Toggle aria-label="Split diff view" value="split"> | | |
| 423 | <Columns2Icon className="size-3" /> | | |
| 424 | </Toggle> | | |
| 425 | </ToggleGroup> | | |
| 426 | </div> | 390 | </div> |
| 427 | </div> | 391 | </div> |
| | 392 | <ToggleGroup |
| | 393 | className="shrink-0 [-webkit-app-region:no-drag]" |
| | 394 | variant="outline" |
| | 395 | size="xs" |
| | 396 | value={[diffRenderMode]} |
| | 397 | onValueChange={(value) => { |
| | 398 | const next = value[0]; |
| | 399 | if (next === "stacked" || next === "split") { |
| | 400 | setDiffRenderMode(next); |
| | 401 | } |
| | 402 | }} |
| | 403 | > |
| | 404 | <Toggle aria-label="Stacked diff view" value="stacked"> |
| | 405 | <Rows3Icon className="size-3" /> |
| | 406 | </Toggle> |
| | 407 | <Toggle aria-label="Split diff view" value="split"> |
| | 408 | <Columns2Icon className="size-3" /> |
| | 409 | </Toggle> |
| | 410 | </ToggleGroup> |
| | 411 | </> |
| | 412 | ); |
| | 413 | const headerRowClassName = cn( |
| | 414 | "flex items-center justify-between gap-2 px-4", |
| | 415 | shouldUseDragRegion ? "drag-region h-[52px] border-b border-border" : "h-12", |
| | 416 | ); |
| | 417 | |
| | 418 | return ( |
| | 419 | <div |
| | 420 | className={cn( |
| | 421 | "flex h-full min-w-0 flex-col bg-card", |
| | 422 | mode === "inline" |
| | 423 | ? "w-[42vw] min-w-[360px] max-w-[560px] shrink-0 border-l border-border" |
| | 424 | : "w-full", |
| | 425 | )} |
| | 426 | > |
| | 427 | {shouldUseDragRegion ? ( |
| | 428 | <div className={headerRowClassName}>{headerRow}</div> |
| | 429 | ) : ( |
| | 430 | <div className="border-b border-border"> |
| | 431 | <div className={headerRowClassName}>{headerRow}</div> |
| | 432 | </div> |
| | 433 | )} |
| 428 | | 434 | |
| 429 | {!activeThread ? ( | 435 | {!activeThread ? ( |
| 430 | <div className="flex flex-1 items-center justify-center px-5 text-center text-xs text-muted-foreground/70"> | 436 | <div className="flex flex-1 items-center justify-center px-5 text-center text-xs text-muted-foreground/70"> |