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
2 changes: 2 additions & 0 deletions .changelog/NEXT.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@

- **Branch reconciler prioritizes recognized work branches.** The in-flight set handed to the coordinator agent is now ordered by branch prefix — `claim/`, `cos/`, `next/`, `feature/`, `fix/`, and other conventional prefixes are reconciled ahead of unrecognized/ad-hoc branches — so a bounded run spends its budget on real deliverables first. Both `/` and `-` separators match.

- **The Catalog page is usable on a phone.** The toolbar used to run off the side of the screen and the filter dropdowns each claimed a fixed width they didn't have room for. Now the view toggle spans the width as a segmented Grid/Albums control with the three actions split evenly beneath it, search sits directly under the header so it's reachable without scrolling, the type chips stay on one swipeable row instead of stacking three rows deep, and the Universe/Series/Tag dropdowns lay out two-up. The selection bar stacks its count above three equal-width buttons, and its "Remix into…" / "Add to universe/series…" menus now span the bar instead of opening off-screen. Delete and select targets on each card are bigger, and long album subtitles drop below the title rather than squeezing it. Confirming a card delete now shows a band across the top of the card naming the ingredient — `Delete "Echo Saint"?` — instead of an unlabeled prompt tucked in the corner, which on a phone left barely enough room for the name beside it. That one is visible at every width; the rest of the layout is unchanged on a desktop-width window.

## AI providers

- **[issue-3194] Ask and image captioning now work on a local OpenCode provider.** If you pointed Ask Yourself or vision captioning at an OpenCode provider backed by Ollama, the run failed with the model reported as "not valid" — OpenCode has to be told which local models exist before it will accept one, and only the agent and Run Prompt paths were doing that. Every path that starts a coding CLI now declares them the same way, so the model you picked is accepted wherever you use it.
Expand Down
18 changes: 12 additions & 6 deletions client/src/components/catalog/CatalogAlbum.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ export default function CatalogAlbum({
}, [expanded, ensureLoaded]);

const toggle = () => setExpanded((e) => !e);
const Chevron = expanded ? ChevronDown : ChevronRight;

const loadMore = async () => {
setLoadingMore(true);
Expand Down Expand Up @@ -102,21 +103,26 @@ export default function CatalogAlbum({

return (
<section className="border border-port-border rounded-lg overflow-hidden bg-port-card/40">
{/* On a phone the subtitle drops to its own line under the title rather
than competing with it for a ~200px slot; the chevron and count stay
pinned so the row reads the same at every width. */}
<button
type="button"
onClick={toggle}
aria-expanded={expanded}
className="w-full flex items-center gap-2 px-4 py-3 text-left hover:bg-port-bg/40"
className="w-full flex items-start sm:items-center gap-2 px-3 sm:px-4 py-3 text-left hover:bg-port-bg/40"
>
{expanded ? <ChevronDown size={16} className="text-gray-400" /> : <ChevronRight size={16} className="text-gray-400" />}
<span className="text-white font-medium">{title}</span>
{subtitle ? <span className="text-xs text-gray-500">{subtitle}</span> : null}
<span className="ml-auto text-xs text-gray-400 px-2 py-0.5 rounded-full border border-port-border">
<Chevron size={16} className="shrink-0 mt-0.5 sm:mt-0 text-gray-400" />
<span className="min-w-0 flex-1 flex flex-col sm:flex-row sm:items-center sm:gap-2">
<span className="text-white font-medium truncate">{title}</span>
{subtitle ? <span className="text-xs text-gray-500 sm:truncate">{subtitle}</span> : null}
</span>
<span className="shrink-0 text-xs text-gray-400 px-2 py-0.5 rounded-full border border-port-border">
{count}
</span>
</button>
{expanded && (
<div className="px-4 pb-4">
<div className="px-3 sm:px-4 pb-3 sm:pb-4">
{loading ? (
<div className="flex items-center gap-2 text-sm text-gray-500 py-3">
<Loader2 size={14} className="animate-spin" /> Loading…
Expand Down
72 changes: 36 additions & 36 deletions client/src/components/catalog/CatalogCard.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
import { Link } from 'react-router-dom';
import { Sparkles, Trash2 } from 'lucide-react';
import MediaImage from '../MediaImage';
import InlineConfirmRow from '../ui/InlineConfirmRow';
import { payloadSnippet } from '../../lib/catalogTypes';

function TypeBadge({ type, getType }) {
Expand Down Expand Up @@ -60,7 +61,7 @@ export default function CatalogCard({
const snippet = payloadSnippet(it.payload, it.type, 120, getType);
return (
<li
className={`relative bg-port-card border rounded-lg transition-colors ${
className={`relative bg-port-card border rounded-lg overflow-hidden transition-colors ${
selected
? 'border-port-accent ring-1 ring-port-accent'
: 'border-port-border hover:border-port-accent/60'
Expand All @@ -73,12 +74,30 @@ export default function CatalogCard({
onChange={() => onToggleSelect(it)}
onClick={(e) => e.stopPropagation()}
aria-label={`Select ${name}`}
className="w-4 h-4 m-1 accent-port-accent cursor-pointer"
className="w-4 h-4 m-1.5 accent-port-accent cursor-pointer"
/>
</span>
{/* Armed delete: a full-width band above the card body (the house
inline-confirm pattern) rather than a corner overlay. A corner overlay
has to be paid for with reserved padding on the link, and on a
single-column phone card there isn't enough width to reserve without
crushing the name — the version that stretched the overlay instead
covered the name, hiding the very thing being deleted. The band names
it. `pl-11` clears the absolutely-positioned checkbox. */}
{armed && (
<InlineConfirmRow
variant="separator"
className="pl-11"
question={`Delete "${name}"?`}
confirmText="Yes"
cancelText="No"
onConfirm={() => onConfirmDelete(it)}
onCancel={onCancelArm}
/>
)}
<Link
to={`/catalog/${encodeURIComponent(it.type)}/${encodeURIComponent(it.id)}`}
className={`flex gap-3 p-3 pl-10 min-h-[88px] ${armed ? 'pr-32' : 'pr-10'}`}
className="flex gap-3 p-3 pl-11 pr-11 min-h-[88px]"
>
<CardThumb mediaKey={it.thumbnailKey} alt={name} />
<span className="flex flex-col gap-2 min-w-0 flex-1">
Expand All @@ -96,39 +115,20 @@ export default function CatalogCard({
) : null}
</span>
</Link>
<div className="absolute top-2 right-2">
{/* The delete control is a sibling of the <Link>, not a descendant, so a
click on these buttons never traverses the anchor. */}
{armed ? (
<span className="inline-flex items-center gap-1 text-xs bg-port-card border border-port-border rounded px-1 py-0.5 shadow-sm">
<span className="text-gray-400 pl-1">Delete?</span>
<button
type="button"
onClick={() => onConfirmDelete(it)}
className="px-2 py-0.5 rounded bg-port-error/20 text-port-error hover:bg-port-error/30 font-medium"
>
Yes
</button>
<button
type="button"
onClick={onCancelArm}
className="px-2 py-0.5 rounded text-gray-400 hover:text-white"
>
No
</button>
</span>
) : (
<button
type="button"
onClick={() => onArm(it.id)}
className="p-1.5 rounded text-gray-500 hover:text-port-error bg-port-card"
aria-label={`Delete ${name}`}
title="Delete ingredient"
>
<Trash2 size={14} />
</button>
)}
</div>
{/* The arm button is a sibling of the <Link>, not a descendant, so its
click never traverses the anchor. It gives way to the confirm band
above once armed, so the card never shows two delete affordances. */}
{!armed && (
<button
type="button"
onClick={() => onArm(it.id)}
className="absolute top-2 right-2 p-2 rounded text-gray-500 hover:text-port-error bg-port-card"
aria-label={`Delete ${name}`}
title="Delete ingredient"
>
<Trash2 size={16} />
</button>
)}
</li>
);
}
Loading