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
43 changes: 24 additions & 19 deletions client/src/components/cos/ReviewerPicker.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,21 @@ const normalizeReviewerValue = (value) => normalizeReviewerSlug(value);
// older build either.
const CUSTOM_MODEL_OPTION = '[custom]';

// Shared row grid, one template for the header and every row so their columns
// cannot drift apart. The wide form keeps minimum tracks for order, provider,
// model, effort, optional, max, and remove — together ~32rem, so the collapse is
// keyed to a CONTAINER query (`@xl`), not a viewport one: this picker also
// renders inside a narrow dashboard tile on a wide screen, where a viewport
// `sm:` was unconditionally true and forced the wide grid into a ~250px column.
// Below `@xl` a row collapses to a stacked 2-column label/value block, so a
// narrow container never scrolls horizontally. The header is wide-only — in the
// stacked form each cell carries its own inline label, since a header far above
// a stacked row doesn't associate.
const WIDE_TRACKS = '@xl:grid-cols-[2.5rem_minmax(5rem,1fr)_minmax(8rem,2fr)_minmax(7rem,1fr)_auto_3.25rem_auto]';
const ROW_CLASS = `grid grid-cols-[auto_1fr] ${WIDE_TRACKS} items-center gap-x-2 gap-y-1 px-1.5 py-1.5 rounded border border-port-border bg-port-bg @xl:border-transparent @xl:bg-transparent @xl:py-0.5 @xl:rounded-none`;
const CELL_LABEL_CLASS = '@xl:hidden text-[10px] uppercase tracking-wide text-gray-600';
const HEADER_CLASS = `hidden @xl:grid ${WIDE_TRACKS} items-center gap-x-2 px-1.5 text-[10px] uppercase tracking-wide text-gray-600`;

/**
* Ordered multi-reviewer picker, rendered as one row per reviewer with the five
* per-reviewer controls as columns: **Provider | Model | Effort | Optional | Max
Expand Down Expand Up @@ -647,18 +662,8 @@ export default function ReviewerPicker({
emit({ reviewers: next });
};

// Shared row grid. Desktop keeps minimum tracks for order, provider, model,
// effort, optional, max, and remove; under `sm` it collapses to a stacked
// 2-column label/value block so a narrow screen never needs horizontal
// scrolling. The header row is desktop-only — on mobile each cell carries
// its own inline label, since a header far above a stacked row doesn't
// associate.
const ROW_CLASS = 'grid grid-cols-[auto_1fr] sm:grid-cols-[2.5rem_minmax(5rem,1fr)_minmax(8rem,2fr)_minmax(7rem,1fr)_auto_3.25rem_auto] items-center gap-x-2 gap-y-1 px-1.5 py-1.5 rounded border border-port-border bg-port-bg sm:border-transparent sm:bg-transparent sm:py-0.5 sm:rounded-none';
const CELL_LABEL_CLASS = 'sm:hidden text-[10px] uppercase tracking-wide text-gray-600';
const HEADER_CLASS = 'hidden sm:grid sm:grid-cols-[2.5rem_minmax(5rem,1fr)_minmax(8rem,2fr)_minmax(7rem,1fr)_auto_3.25rem_auto] items-center gap-x-2 px-1.5 text-[10px] uppercase tracking-wide text-gray-600';

return (
<div className="flex flex-col gap-2 w-full">
<div className="@container flex flex-col gap-2 w-full">
<div className="flex flex-col gap-1">
<span className="text-xs text-gray-500">Reviewers (in order):</span>
{selected.length > 0 && (
Expand All @@ -672,14 +677,14 @@ export default function ReviewerPicker({
<span className="text-center">Max</span>
<span className="sr-only">Remove</span>
</div>
<div className="flex flex-col gap-1.5 sm:gap-0.5">
<div className="flex flex-col gap-1.5 @xl:gap-0.5">
{selected.map((value, index) => (
<div
key={value}
className={ROW_CLASS}
title={REVIEWER_OPTIONS.find(o => o.value === value)?.description}
>
<div className="flex items-center gap-0.5 col-span-2 sm:col-span-1">
<div className="flex items-center gap-0.5 col-span-2 @xl:col-span-1">
<span className="text-port-accent font-mono text-xs">{index + 1}.</span>
<button
type="button"
Expand All @@ -700,7 +705,7 @@ export default function ReviewerPicker({
<ChevronDown size={12} />
</button>
</div>
<span className="flex items-center gap-1 min-w-0 col-span-2 sm:col-span-1">
<span className="flex items-center gap-1 min-w-0 col-span-2 @xl:col-span-1">
<span className="text-xs text-gray-300 truncate">{reviewerLabel(value)}</span>
{renderUnavailableBadge(value)}
</span>
Expand All @@ -716,7 +721,7 @@ export default function ReviewerPicker({
</div>
<span className={CELL_LABEL_CLASS}>Max iterations</span>
<div>{renderMaxRounds(value, reviewerLabel(value))}</div>
<div className="col-span-2 sm:col-span-1 justify-self-end">
<div className="col-span-2 @xl:col-span-1 justify-self-end">
<button
type="button"
disabled={disabled}
Expand Down Expand Up @@ -787,15 +792,15 @@ export default function ReviewerPicker({
<div className="flex flex-col gap-1.5 pt-1 border-t border-port-border/50">
<span className="text-xs text-gray-500">GitHub reviewers (gate merge):</span>
{selectedUsernames.length > 0 ? (
<div className="flex flex-col gap-1.5 sm:gap-0.5">
<div className="flex flex-col gap-1.5 @xl:gap-0.5">
{selectedUsernames.map((value) => (
<div
key={value}
className={ROW_CLASS}
title="GitHub username requested as a PR reviewer to gate the merge"
>
<span className="text-port-accent font-mono text-xs col-span-2 sm:col-span-1">@</span>
<span className="text-xs text-gray-300 col-span-2 sm:col-span-1 truncate">{value}</span>
<span className="text-port-accent font-mono text-xs col-span-2 @xl:col-span-1">@</span>
<span className="text-xs text-gray-300 col-span-2 @xl:col-span-1 truncate">{value}</span>
<span className={CELL_LABEL_CLASS}>Model</span>
<div className="min-w-0">{renderModelCell(`@${value}`)}</div>
<span className={CELL_LABEL_CLASS}>Effort</span>
Expand All @@ -808,7 +813,7 @@ export default function ReviewerPicker({
</div>
<span className={CELL_LABEL_CLASS}>Max iterations</span>
<div>{renderMaxRounds(`@${value}`, `@${value}`)}</div>
<div className="col-span-2 sm:col-span-1 justify-self-end">
<div className="col-span-2 @xl:col-span-1 justify-self-end">
<button
type="button"
disabled={disabled}
Expand Down
59 changes: 33 additions & 26 deletions client/src/components/cos/TaskAddForm.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -724,11 +724,18 @@ export default function TaskAddForm({ providers, providersLoaded = true, apps, o
onTaskAdded?.(result, { position: addToTop ? 'top' : 'bottom' });
};

// Compact mode: single row with description + app + add, expandable
// Compact mode: single row with description + app + add, expandable.
// Every breakpoint below is a CONTAINER query, not a viewport one: compact
// mode renders inside a dashboard tile that can be ~250px wide on a 2560px
// screen, where a viewport `sm:` kept the wide row and squeezed the textarea
// down to one character per line. `@xl` (576px) is where the textarea, the
// 10rem app picker and the button each still get usable width — roughly the
// old 640px viewport threshold once page padding is subtracted, so a phone
// keeps the stacked form it had before.
if (compact) {
return (
<div className="space-y-3">
<div className="flex flex-col sm:flex-row gap-2">
<div className="@container space-y-3">
<div className="flex flex-col @xl:flex-row gap-2">
<label htmlFor="compact-task-desc" className="sr-only">Task description (required)</label>
<AutoSizeTextarea
id="compact-task-desc"
Expand All @@ -741,11 +748,11 @@ export default function TaskAddForm({ providers, providersLoaded = true, apps, o
handleAddTask();
}
}}
className="w-full sm:flex-1 px-3 py-2 bg-port-bg border border-port-border rounded-lg text-white text-sm min-h-[44px]"
className="w-full @xl:flex-1 px-3 py-2 bg-port-bg border border-port-border rounded-lg text-white text-sm min-h-[44px]"
aria-required="true"
/>
<div className="flex gap-2">
<div className="flex-1 sm:w-40 sm:flex-none">
<div className="flex-1 min-w-0 @xl:w-40 @xl:flex-none">
<AppContextPicker
apps={apps}
value={newTask.app}
Expand All @@ -760,7 +767,7 @@ export default function TaskAddForm({ providers, providersLoaded = true, apps, o
<button
onClick={handleAddTask}
disabled={isSubmitting || isEnhancing}
className="flex items-center gap-1 px-3 py-2 bg-port-accent/20 hover:bg-port-accent/30 text-port-accent rounded-lg text-sm transition-colors disabled:opacity-50 min-h-[44px]"
className="flex shrink-0 items-center gap-1 whitespace-nowrap px-3 py-2 bg-port-accent/20 hover:bg-port-accent/30 text-port-accent rounded-lg text-sm transition-colors disabled:opacity-50 min-h-[44px]"
>
{(isSubmitting || isEnhancing) ? <Loader2 size={14} className="animate-spin" /> : <Plus size={14} />}
{isSubmitting ? (planOnly ? 'Planning...' : 'Adding...') : planOnly ? 'Plan & File' : 'Add'}
Expand All @@ -786,7 +793,7 @@ export default function TaskAddForm({ providers, providersLoaded = true, apps, o

// Full mode: identical to original TasksTab form
return (
<div className="bg-port-card border border-port-accent/50 rounded-lg p-4 mb-4" role="form" aria-label="Add new task">
<div className="@container bg-port-card border border-port-accent/50 rounded-lg p-4 mb-4" role="form" aria-label="Add new task">
{/* Quick Templates */}
{templates.length > 0 && (
<div className="mb-4">
Expand Down Expand Up @@ -815,7 +822,7 @@ export default function TaskAddForm({ providers, providersLoaded = true, apps, o
{/* The Claude-Code form of the command, as a recognizable label.
The actual invocation is resolved server-side per provider. */}
{template.slashdoCommand && (
<span className="hidden sm:inline text-xs text-port-accent/80 font-mono">{slashdoLabel(template.slashdoCommand)}</span>
<span className="hidden @sm:inline text-xs text-port-accent/80 font-mono">{slashdoLabel(template.slashdoCommand)}</span>
)}
{template.useCount > 0 && (
<span className="text-xs text-gray-600">({template.useCount})</span>
Expand Down Expand Up @@ -878,7 +885,7 @@ export default function TaskAddForm({ providers, providersLoaded = true, apps, o
instances={assignableInstances}
/>
)}
<div className="grid grid-cols-1 sm:flex sm:items-center gap-x-4 gap-y-1 sm:flex-wrap">
<div className="grid grid-cols-1 @sm:flex @sm:items-center gap-x-4 gap-y-1 @sm:flex-wrap">
<label className="flex items-center gap-2 cursor-pointer select-none py-1">
<input
type="checkbox"
Expand Down Expand Up @@ -961,9 +968,9 @@ export default function TaskAddForm({ providers, providersLoaded = true, apps, o
</span>
</label>
{!useWorktree && (
<label htmlFor="task-when-done" className="flex items-center gap-2 py-1 basis-full sm:basis-auto">
<label htmlFor="task-when-done" className="flex flex-wrap items-center gap-2 py-1 basis-full @sm:basis-auto">
<span className="text-sm text-gray-400">When done</span>
<select id="task-when-done" value={whenDone} onChange={(e) => setWhenDone(e.target.value)} className="min-w-52 rounded border border-port-border bg-port-bg px-2 py-1 text-sm text-white focus:border-port-accent focus:outline-hidden">
<select id="task-when-done" value={whenDone} onChange={(e) => setWhenDone(e.target.value)} className="w-full @sm:w-auto @sm:min-w-52 rounded border border-port-border bg-port-bg px-2 py-1 text-sm text-white focus:border-port-accent focus:outline-hidden">
<option value="leave-uncommitted">Leave code uncommitted</option>
<option value="commit-push">Commit and push to default branch</option>
</select>
Expand Down Expand Up @@ -995,14 +1002,14 @@ export default function TaskAddForm({ providers, providersLoaded = true, apps, o
</span>
</label>
{openPR && (
<label htmlFor="task-pr-completion" className="flex items-center gap-2 py-1 basis-full sm:basis-auto">
<label htmlFor="task-pr-completion" className="flex flex-wrap items-center gap-2 py-1 basis-full @sm:basis-auto">
<span className="text-sm text-gray-400">After opening PR</span>
<select
id="task-pr-completion"
value={prCompletion}
title={prCompletionOption(prCompletion)?.description}
onChange={(e) => setPrCompletion(e.target.value)}
className="min-w-44 rounded border border-port-border bg-port-bg px-2 py-1 text-sm text-white focus:border-port-accent focus:outline-hidden"
className="w-full @sm:w-auto @sm:min-w-44 rounded border border-port-border bg-port-bg px-2 py-1 text-sm text-white focus:border-port-accent focus:outline-hidden"
>
{PR_COMPLETION_OPTIONS.map(option => (
<option key={option.value} value={option.value}>{option.label}</option>
Expand Down Expand Up @@ -1055,8 +1062,8 @@ export default function TaskAddForm({ providers, providersLoaded = true, apps, o
</>
)}
</div>
<div className="flex items-center justify-between gap-2 pt-1 border-t border-port-border/40">
<div className="flex items-center gap-1.5 text-xs text-gray-400">
<div className="flex flex-wrap items-center justify-between gap-2 pt-1 border-t border-port-border/40">
<div className="flex flex-wrap items-center gap-1.5 text-xs text-gray-400">
<span>Execution:</span>
<button
type="button"
Expand Down Expand Up @@ -1110,13 +1117,13 @@ export default function TaskAddForm({ providers, providersLoaded = true, apps, o
const models = selectedProv ? effortAwareModelOptions(selectedProv, roleData.model) : [];

return (
<div key={key} className="flex flex-col sm:flex-row sm:items-center gap-2 text-xs">
<div className="sm:w-28 flex-shrink-0">
<div key={key} className="flex flex-col @lg:flex-row @lg:items-center gap-2 text-xs">
<div className="@lg:w-28 flex-shrink-0">
<span className="font-medium text-white">{label}</span>
<span className="block text-[10px] text-gray-400 truncate">{hint}</span>
</div>

<div className="flex-1 grid grid-cols-1 sm:grid-cols-3 gap-2">
<div className="flex-1 min-w-0 grid grid-cols-1 @lg:grid-cols-3 gap-2">
<select
aria-label={`${label} provider`}
value={roleData.provider || ''}
Expand Down Expand Up @@ -1160,8 +1167,8 @@ export default function TaskAddForm({ providers, providersLoaded = true, apps, o
</div>
</div>
) : (
<div className="flex flex-col sm:flex-row gap-3">
<div className="sm:w-40">
<div className="flex flex-col @lg:flex-row gap-3">
<div className="@lg:w-40">
<label htmlFor="task-provider" className="sr-only">AI provider</label>
<select
id="task-provider"
Expand All @@ -1179,7 +1186,7 @@ export default function TaskAddForm({ providers, providersLoaded = true, apps, o
</select>
</div>
{availableModels.length > 0 ? (
<div className="flex-1">
<div className="flex-1 min-w-0">
<label htmlFor="task-model" className="sr-only">AI model</label>
<select
id="task-model"
Expand Down Expand Up @@ -1207,7 +1214,7 @@ export default function TaskAddForm({ providers, providersLoaded = true, apps, o
)}
</div>
) : selectedProvider ? (
<div className="flex-1 px-3 py-2 min-h-[44px] bg-port-bg border border-port-border rounded-lg text-xs text-gray-400 flex items-center">
<div className="flex-1 min-w-0 px-3 py-2 min-h-[44px] bg-port-bg border border-port-border rounded-lg text-xs text-gray-400 flex items-center">
{providerModelNote}
</div>
) : null}
Expand All @@ -1216,12 +1223,12 @@ export default function TaskAddForm({ providers, providersLoaded = true, apps, o
model={effectiveModelFor(selectedProvider, newTask.model)}
value={newTask.effort}
onChange={effort => setNewTask(t => ({ ...t, effort }))}
className="sm:w-40 w-full px-3 py-2 bg-port-bg border border-port-border rounded-lg text-white text-sm min-h-[44px]"
className="@lg:w-40 w-full px-3 py-2 bg-port-bg border border-port-border rounded-lg text-white text-sm min-h-[44px]"
/>
</div>
)}
{isOpencodeLocalProvider(selectedProvider) && (
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
<div className="grid grid-cols-1 @md:grid-cols-2 gap-3">
{/* OrcaRouter fronts cloud models that own their own reasoning
switch, so it is the one local-namespace wrapper with no
thinking toggle to override. */}
Expand Down Expand Up @@ -1346,7 +1353,7 @@ export default function TaskAddForm({ providers, providersLoaded = true, apps, o
)}
{/* Template Save Inline Input */}
{showTemplateSave && (
<div className="flex gap-2 items-center">
<div className="flex flex-wrap gap-2 items-center">
<input
type="text"
value={templateNameInput}
Expand Down Expand Up @@ -1396,7 +1403,7 @@ export default function TaskAddForm({ providers, providersLoaded = true, apps, o
title="Save current form as a reusable template"
>
<Bookmark size={14} aria-hidden="true" />
<span className="hidden sm:inline">Save Template</span>
<span className="hidden @sm:inline">Save Template</span>
</button>
<button
onClick={handleAddTask}
Expand Down
Loading