import React from 'react';
import { Icon } from '@/components/icon/Icon';
import type { IconName } from '@/components/icon/icons';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuRadioGroup,
DropdownMenuRadioItem,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu';
import { ScrollableOverlay } from '@/components/ui/ScrollableOverlay';
import { ScrollShadow } from '@/components/ui/ScrollShadow';
import { toast } from '@/components/ui';
import { SimpleMarkdownRenderer } from '@/components/chat/MarkdownRenderer';
import { cn } from '@/lib/utils';
import { useRuntimeAPIs } from '@/hooks/useRuntimeAPIs';
import { useDebouncedValue } from '@/hooks/useDebouncedValue';
import { useLinearAuthStore } from '@/stores/useLinearAuthStore';
import { useUIStore, LINEAR_ISSUE_LIST_ALL_TEAMS } from '@/stores/useUIStore';
import { useI18n } from '@/lib/i18n';
import { formatDateTimeForPreference } from '@/lib/timeFormat';
import { openExternalUrl } from '@/lib/url';
import { startLinearIssueSession } from '@/lib/linearStartSession';
import type {
LinearIssue,
LinearIssueLabel,
LinearIssueListPriority,
LinearIssueListStatus,
LinearIssueSummary,
LinearTeamMapping,
LinearWorkflowState,
} from '@/lib/api/types';
const LINEAR_MARKDOWN_CLASS = '[&_img]:max-w-full [&_img]:h-auto';
const FILTER_TRIGGER_CLASS = 'flex h-8 min-w-0 flex-1 items-center gap-1.5 rounded-md px-2 typography-ui-label font-semibold text-foreground outline-none hover:bg-interactive-hover focus-visible:ring-2 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50';
const FILTER_COMPACT_TRIGGER_CLASS = 'flex h-8 w-8 shrink-0 items-center justify-center rounded-md text-foreground outline-none hover:bg-interactive-hover focus-visible:ring-2 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50';
// The Linear rail is 380–600px. Below this, four or five flex pickers squeeze
// labels to an ellipsis. Status keeps its label (the filter people use most);
// search and the other filters drop to icons that already identify them.
// Walkthrough uses the same icon-only idea at 680px for a wider header.
const FILTER_COMPACT_WIDTH = 520;
const workspaceLabel = (workspace: { name: string | null; urlKey: string | null; id: string }) => (
workspace.name?.trim() || workspace.urlKey?.trim() || workspace.id
);
const LINEAR_PRIORITY_KEYS = {
0: 'contextPanel.linear.priority.none',
1: 'contextPanel.linear.priority.urgent',
2: 'contextPanel.linear.priority.high',
3: 'contextPanel.linear.priority.medium',
4: 'contextPanel.linear.priority.low',
} as const;
const LINEAR_WORKFLOW_TYPE_RANK = {
triage: 0,
backlog: 1,
unstarted: 2,
started: 3,
completed: 4,
canceled: 5,
} as const;
const linearWorkflowTypeRank = (type: string | null): number => {
if (
type === 'triage'
|| type === 'backlog'
|| type === 'unstarted'
|| type === 'started'
|| type === 'completed'
|| type === 'canceled'
) {
return LINEAR_WORKFLOW_TYPE_RANK[type];
}
return 99;
};
const compareLinearWorkflowStates = (left: LinearWorkflowState, right: LinearWorkflowState): number => {
const typeDelta = linearWorkflowTypeRank(left.type) - linearWorkflowTypeRank(right.type);
if (typeDelta !== 0) return typeDelta;
if (left.position !== right.position) return left.position - right.position;
return left.name.localeCompare(right.name);
};
const linearPriorityMessageKey = (priority: number | null | undefined) => {
if (priority !== 0 && priority !== 1 && priority !== 2 && priority !== 3 && priority !== 4) {
return null;
}
return LINEAR_PRIORITY_KEYS[priority];
};
const STATUS_FILTER_ITEMS = [
{ value: 'all', labelKey: 'contextPanel.linear.filter.status.all' },
{ value: 'backlog', labelKey: 'contextPanel.linear.filter.status.backlog' },
{ value: 'todo', labelKey: 'contextPanel.linear.filter.status.todo' },
{ value: 'started', labelKey: 'contextPanel.linear.filter.status.started' },
{ value: 'inReview', labelKey: 'contextPanel.linear.filter.status.inReview' },
{ value: 'completed', labelKey: 'contextPanel.linear.filter.status.completed' },
{ value: 'canceled', labelKey: 'contextPanel.linear.filter.status.canceled' },
{ value: 'duplicate', labelKey: 'contextPanel.linear.filter.status.duplicate' },
] as const;
const isLinearIssueListStatus = (value: string): value is LinearIssueListStatus => (
STATUS_FILTER_ITEMS.some((item) => item.value === value)
);
const PRIORITY_FILTER_ITEMS = [
{ value: 'all', labelKey: 'contextPanel.linear.filter.priority.all' },
{ value: 'urgent', labelKey: 'contextPanel.linear.priority.urgent' },
{ value: 'high', labelKey: 'contextPanel.linear.priority.high' },
{ value: 'medium', labelKey: 'contextPanel.linear.priority.medium' },
{ value: 'low', labelKey: 'contextPanel.linear.priority.low' },
{ value: 'none', labelKey: 'contextPanel.linear.priority.none' },
] as const;
const isLinearIssueListPriority = (value: string): value is LinearIssueListPriority => (
PRIORITY_FILTER_ITEMS.some((item) => item.value === value)
);
const labelChipStyle = (color: string | null): React.CSSProperties | undefined => {
if (!color) {
return { backgroundColor: 'color-mix(in srgb, var(--surface-mutedForeground) 12%, transparent)' };
}
return {
color,
backgroundColor: `color-mix(in srgb, ${color} 18%, transparent)`,
};
};
const LinearIssueLabelChips: React.FC<{ labels: LinearIssueLabel[] }> = ({ labels }) => {
if (labels.length === 0) return null;
return (
{labels.map((label) => (
{label.name}
))}
);
};
const LinearFilterMenu: React.FC<{
icon: IconName;
label: string;
ariaLabel: string;
value: string;
items: Array<{ value: string; label: string }>;
disabled?: boolean;
compact?: boolean;
active?: boolean;
onValueChange: (value: string) => void;
}> = ({ icon, label, ariaLabel, value, items, disabled, compact, active, onValueChange }) => {
const [open, setOpen] = React.useState(false);
return (
{
if (!disabled) setOpen(next);
}}
>
{
onValueChange(next);
setOpen(false);
}}
>
{items.map((item) => (
{item.label}
))}
);
};
const parseLinearIssueQuery = (value: string): string | null => {
const trimmed = value.trim();
if (!trimmed) return null;
const urlMatch = trimmed.match(/linear\.app\/(?:[^/]+\/)?issue\/([A-Za-z][A-Za-z0-9]*-\d+)/i);
if (urlMatch) return urlMatch[1].toUpperCase();
if (/^[A-Za-z][A-Za-z0-9]*-\d+$/.test(trimmed)) return trimmed.toUpperCase();
return null;
};
const toIssueSummary = (issue: LinearIssue): LinearIssueSummary => ({
id: issue.id,
identifier: issue.identifier,
title: issue.title,
url: issue.url,
state: issue.state,
assignee: issue.assignee,
team: issue.team,
priority: issue.priority,
labels: issue.labels,
});
const patchIssueInList = (issues: LinearIssueSummary[], next: LinearIssue): LinearIssueSummary[] => {
const summary = toIssueSummary(next);
return issues.map((issue) => (issue.id === next.id ? summary : issue));
};
export const LinearIssuesView: React.FC = () => {
const { t } = useI18n();
const { linear } = useRuntimeAPIs();
const linearAuthStatus = useLinearAuthStore((state) => state.status);
const linearAuthChecked = useLinearAuthStore((state) => state.hasChecked);
const refreshStatus = useLinearAuthStore((state) => state.refreshStatus);
const setLinearAuthStatus = useLinearAuthStore((state) => state.setStatus);
const setSettingsDialogOpen = useUIStore((state) => state.setSettingsDialogOpen);
const setSettingsPage = useUIStore((state) => state.setSettingsPage);
const listStatus = useUIStore((state) => state.linearIssueListStatus);
const listAssignee = useUIStore((state) => state.linearIssueListAssignee);
const listTeamId = useUIStore((state) => state.linearIssueListTeamId);
const listPriority = useUIStore((state) => state.linearIssueListPriority);
const linearIssueFocus = useUIStore((state) => state.linearIssueFocus);
const timeFormatPreference = useUIStore((state) => state.timeFormatPreference);
const setListStatus = useUIStore((state) => state.setLinearIssueListStatus);
const setListAssignee = useUIStore((state) => state.setLinearIssueListAssignee);
const setListTeamId = useUIStore((state) => state.setLinearIssueListTeamId);
const setListPriority = useUIStore((state) => state.setLinearIssueListPriority);
const resetListFilters = useUIStore((state) => state.resetLinearIssueListFilters);
const setLinearIssueFocus = useUIStore((state) => state.setLinearIssueFocus);
const [query, setQuery] = React.useState('');
const [searchOpen, setSearchOpen] = React.useState(false);
const [issues, setIssues] = React.useState([]);
const [cursor, setCursor] = React.useState(null);
const [hasMore, setHasMore] = React.useState(false);
const [connected, setConnected] = React.useState(true);
const [isLoading, setIsLoading] = React.useState(false);
const [isLoadingMore, setIsLoadingMore] = React.useState(false);
const [error, setError] = React.useState(null);
const [selectedIssueId, setSelectedIssueId] = React.useState(null);
const [selectedIssue, setSelectedIssue] = React.useState(null);
const [workflowStates, setWorkflowStates] = React.useState([]);
const [isLoadingIssue, setIsLoadingIssue] = React.useState(false);
const [isUpdating, setIsUpdating] = React.useState(false);
const [isStarting, setIsStarting] = React.useState(false);
const [createInWorktree, setCreateInWorktree] = React.useState(false);
const [teams, setTeams] = React.useState([]);
const [isSwitchingWorkspace, setIsSwitchingWorkspace] = React.useState(false);
const listRequestId = React.useRef(0);
const listRootRef = React.useRef(null);
const searchInputRef = React.useRef(null);
const [panelWidth, setPanelWidth] = React.useState(0);
const directIdentifier = React.useMemo(() => parseLinearIssueQuery(query), [query]);
const debouncedQuery = useDebouncedValue(query, 350);
// Same shape the pull request panel uses, so both context surfaces read alike.
const formatCommentTimestamp = React.useCallback((value: string | null) => {
if (!value) return '';
const timestamp = Date.parse(value);
if (!Number.isFinite(timestamp)) return '';
return formatDateTimeForPreference(timestamp, timeFormatPreference, {
year: 'numeric',
month: 'short',
day: 'numeric',
hour: 'numeric',
minute: '2-digit',
});
}, [timeFormatPreference]);
const openLinearSettings = React.useCallback(() => {
setSettingsPage('integrations');
setSettingsDialogOpen(true);
}, [setSettingsDialogOpen, setSettingsPage]);
const listQuery = React.useMemo(() => ({
query: debouncedQuery.trim() || undefined,
status: listStatus,
assignee: listAssignee,
teamId: listTeamId === LINEAR_ISSUE_LIST_ALL_TEAMS ? undefined : listTeamId,
priority: listPriority === 'all' ? undefined : listPriority,
}), [debouncedQuery, listAssignee, listPriority, listStatus, listTeamId]);
const workspaces = linearAuthStatus?.workspaces ?? [];
const currentWorkspaceId = workspaces.find((workspace) => workspace.current)?.id
|| linearAuthStatus?.organization?.id
|| '';
const refresh = React.useCallback(async () => {
if (linearAuthChecked && linearAuthStatus?.connected === false) {
setConnected(false);
setIssues([]);
setHasMore(false);
setCursor(null);
setError(null);
return;
}
if (!linear?.issuesList) {
setConnected(true);
setError(t('session.linearIssuePicker.error.runtimeUnavailable'));
return;
}
const requestId = listRequestId.current + 1;
listRequestId.current = requestId;
setIsLoading(true);
setError(null);
try {
const next = await linear.issuesList(listQuery);
if (requestId !== listRequestId.current) return;
setConnected(next.connected !== false);
if (next.connected === false) {
setIssues([]);
setHasMore(false);
setCursor(null);
return;
}
setIssues(next.issues ?? []);
setCursor(next.cursor ?? null);
setHasMore(Boolean(next.hasMore));
} catch (e) {
if (requestId !== listRequestId.current) return;
setError(e instanceof Error ? e.message : String(e));
} finally {
if (requestId === listRequestId.current) {
setIsLoading(false);
}
}
}, [linear, linearAuthChecked, linearAuthStatus, listQuery, t]);
React.useEffect(() => {
if (linear && !linearAuthChecked) {
void refreshStatus(linear);
}
}, [linear, linearAuthChecked, refreshStatus]);
React.useEffect(() => {
void refresh();
}, [refresh]);
React.useEffect(() => {
if (!linear?.mappingGet || !connected) {
setTeams([]);
return;
}
let cancelled = false;
void linear.mappingGet().then((mapping) => {
if (cancelled) return;
if (mapping.connected === false) {
setTeams([]);
return;
}
setTeams(mapping.teams ?? []);
}).catch(() => {
if (!cancelled) {
setTeams([]);
}
});
return () => {
cancelled = true;
};
}, [connected, currentWorkspaceId, linear]);
React.useEffect(() => {
if (listTeamId === LINEAR_ISSUE_LIST_ALL_TEAMS || teams.length === 0) {
return;
}
if (!teams.some((team) => team.id === listTeamId)) {
setListTeamId(LINEAR_ISSUE_LIST_ALL_TEAMS);
}
}, [listTeamId, setListTeamId, teams]);
const loadMore = React.useCallback(async () => {
if (!linear?.issuesList) return;
if (isLoadingMore || isLoading) return;
if (!hasMore || !cursor) return;
const requestId = listRequestId.current + 1;
listRequestId.current = requestId;
setIsLoadingMore(true);
try {
const next = await linear.issuesList({
...listQuery,
cursor,
});
if (requestId !== listRequestId.current) return;
setConnected(next.connected !== false);
if (next.connected === false) {
return;
}
setIssues((prev) => [...prev, ...(next.issues ?? [])]);
setCursor(next.cursor ?? null);
setHasMore(Boolean(next.hasMore));
} catch (e) {
if (requestId !== listRequestId.current) return;
const message = e instanceof Error ? e.message : String(e);
toast.error(t('session.linearIssuePicker.toast.loadMoreFailed'), { description: message });
} finally {
if (requestId === listRequestId.current) {
setIsLoadingMore(false);
}
}
}, [cursor, hasMore, isLoading, isLoadingMore, linear, listQuery, t]);
React.useEffect(() => {
if (!selectedIssueId || !linear?.issueGet) {
return;
}
let cancelled = false;
setIsLoadingIssue(true);
setSelectedIssue(null);
setWorkflowStates([]);
void (async () => {
try {
const issueRes = await linear.issueGet(selectedIssueId);
if (cancelled) return;
if (issueRes.connected === false) {
setConnected(false);
setSelectedIssueId(null);
return;
}
const issue = issueRes.issue;
if (!issue) {
toast.error(t('session.linearIssuePicker.error.issueNotFound'));
setSelectedIssueId(null);
return;
}
setSelectedIssue(issue);
const teamId = issue.team?.id;
if (!teamId || !linear.issueStates) {
return;
}
try {
const statesRes = await linear.issueStates(teamId);
if (cancelled) return;
if (statesRes.connected === false) {
setConnected(false);
return;
}
setWorkflowStates(statesRes.states ?? []);
} catch (e) {
if (cancelled) return;
const message = e instanceof Error ? e.message : String(e);
toast.error(t('session.linearIssuePicker.toast.loadIssueDetailsFailed'), { description: message });
}
} catch (e) {
if (cancelled) return;
const message = e instanceof Error ? e.message : String(e);
toast.error(t('session.linearIssuePicker.toast.loadIssueDetailsFailed'), { description: message });
setSelectedIssueId(null);
} finally {
if (!cancelled) {
setIsLoadingIssue(false);
}
}
})();
return () => {
cancelled = true;
};
}, [linear, selectedIssueId, t]);
React.useEffect(() => {
if (!linearIssueFocus) return;
setSelectedIssueId(linearIssueFocus);
setLinearIssueFocus(null);
}, [linearIssueFocus, setLinearIssueFocus]);
const applyUpdatedIssue = React.useCallback((issue: LinearIssue) => {
setSelectedIssue(issue);
setIssues((prev) => patchIssueInList(prev, issue));
}, []);
const updateIssueState = React.useCallback(async (stateId: string, failedKey: 'contextPanel.linear.toast.statusUpdateFailed' | 'contextPanel.linear.toast.closeFailed') => {
if (!linear?.issueUpdate || !selectedIssue || isUpdating) {
return;
}
if (selectedIssue.state?.id === stateId) {
return;
}
setIsUpdating(true);
try {
const result = await linear.issueUpdate({ id: selectedIssue.id, stateId });
if (result.connected === false) {
setConnected(false);
toast.error(t(failedKey));
return;
}
if (!result.issue) {
toast.error(t(failedKey));
return;
}
applyUpdatedIssue(result.issue);
toast.success(t('contextPanel.linear.toast.statusUpdated'));
} catch (e) {
const message = e instanceof Error ? e.message : String(e);
toast.error(t(failedKey), { description: message });
} finally {
setIsUpdating(false);
}
}, [applyUpdatedIssue, isUpdating, linear, selectedIssue, t]);
const closeIssue = React.useCallback(() => {
const completed = workflowStates.find((state) => state.type === 'completed');
if (!completed) {
toast.error(t('contextPanel.linear.error.noCompletedState'));
return;
}
void updateIssueState(completed.id, 'contextPanel.linear.toast.closeFailed');
}, [t, updateIssueState, workflowStates]);
const startSession = React.useCallback(async () => {
if (!selectedIssue || isStarting) return;
setIsStarting(true);
try {
await startLinearIssueSession({
linear,
issueKey: selectedIssue.id,
createInWorktree,
t,
});
} finally {
setIsStarting(false);
}
}, [createInWorktree, isStarting, linear, selectedIssue, t]);
const switchWorkspace = React.useCallback(async (organizationId: string) => {
if (!linear?.authActivate || !organizationId || organizationId === currentWorkspaceId || isSwitchingWorkspace) {
return;
}
setIsSwitchingWorkspace(true);
try {
const payload = await linear.authActivate(organizationId);
setLinearAuthStatus(payload);
setSelectedIssueId(null);
setSelectedIssue(null);
setWorkflowStates([]);
setListTeamId(LINEAR_ISSUE_LIST_ALL_TEAMS);
toast.success(t('contextPanel.linear.toast.workspaceSwitched'));
} catch (e) {
const message = e instanceof Error ? e.message : String(e);
toast.error(t('contextPanel.linear.toast.workspaceSwitchFailed'), { description: message });
} finally {
setIsSwitchingWorkspace(false);
}
}, [currentWorkspaceId, isSwitchingWorkspace, linear, setLinearAuthStatus, setListTeamId, t]);
const statusOptions = React.useMemo(() => {
const byId = new Map(workflowStates.map((state) => [state.id, state]));
const currentId = selectedIssue?.state?.id;
const currentName = selectedIssue?.state?.name;
const states = currentId && currentName && !byId.has(currentId)
? [
{
id: currentId,
name: currentName,
type: selectedIssue.state?.type ?? null,
position: 0,
},
...workflowStates,
]
: workflowStates;
return [...states].sort(compareLinearWorkflowStates);
}, [selectedIssue, workflowStates]);
const completedState = workflowStates.find((state) => state.type === 'completed');
const alreadyCompleted = selectedIssue?.state?.type === 'completed';
const showDisconnected = linearAuthChecked && connected === false;
const runtimeMissing = !linear;
const showingDetail = Boolean(selectedIssueId);
const usingDefaultFilters = listStatus === 'all' && listAssignee === 'any' && listTeamId === LINEAR_ISSUE_LIST_ALL_TEAMS && listPriority === 'all';
const canUseListControls = Boolean(linear) && connected && !showDisconnected;
const filtersDisabled = !canUseListControls || isSwitchingWorkspace;
// Zero means the observer has not reported yet; assume there is room rather
// than rendering a compact filter row for one frame on every open.
const compactFilters = panelWidth > 0 && panelWidth < FILTER_COMPACT_WIDTH;
const searchActive = query.trim().length > 0;
const hasActiveFilters = !usingDefaultFilters || searchActive;
const showSearchField = !compactFilters || searchOpen || searchActive;
const closeCompactSearch = React.useCallback(() => {
setQuery('');
setSearchOpen(false);
}, []);
React.useEffect(() => {
const element = listRootRef.current;
if (!element || !globalThis.ResizeObserver) return;
const observer = new ResizeObserver((entries) => {
setPanelWidth(entries[0]?.contentRect.width ?? 0);
});
observer.observe(element);
return () => observer.disconnect();
}, [showingDetail]);
React.useEffect(() => {
if (compactFilters && searchOpen) {
searchInputRef.current?.focus();
}
}, [compactFilters, searchOpen]);
const worktreeToggle = (
setCreateInWorktree((value) => !value)}
onKeyDown={(event) => {
if (event.key === ' ' || event.key === 'Enter') {
event.preventDefault();
setCreateInWorktree((value) => !value);
}
}}
>
{t('session.linearIssuePicker.actions.createInWorktree')}
);
const renderIssueRow = (issue: LinearIssueSummary) => (
setSelectedIssueId(issue.id)}
>
{issue.identifier}
{issue.title}
);
if (showingDetail) {
const assigneeName = selectedIssue?.assignee?.displayName || selectedIssue?.assignee?.name;
const comments = selectedIssue?.comments ?? [];
const description = selectedIssue?.description?.trim() || '';
const statusValue = selectedIssue?.state?.id || '';
const priorityKey = linearPriorityMessageKey(selectedIssue?.priority);
const labels = selectedIssue?.labels ?? [];
return (
{selectedIssue ? (
) : null}
{isLoadingIssue && !selectedIssue ? (
{t('contextPanel.linear.loading.issue')}
) : null}
{selectedIssue ? (
{t('contextPanel.linear.loading.issue')}
}>
{selectedIssue.identifier}
{selectedIssue.title}
{statusOptions.length > 0 && statusValue ? (
) : selectedIssue.state?.name ? (
{selectedIssue.state.name}
) : null}
{completedState && !alreadyCompleted ? (
) : null}
{selectedIssue.team?.name ? (
<>
- {t('contextPanel.linear.label.team')}
- {selectedIssue.team.name}
>
) : null}
- {t('contextPanel.linear.label.assignee')}
-
{assigneeName || t('contextPanel.linear.label.unassigned')}
{priorityKey ? (
<>
- {t('contextPanel.linear.label.priority')}
-
{t(priorityKey)}
>
) : null}
{labels.length > 0 ? (
<>
- {t('contextPanel.linear.label.labels')}
-
>
) : null}
{description ? (
) : (
{t('contextPanel.linear.empty.noDescription')}
)}
{t('contextPanel.linear.label.comments')}
{comments.length === 0 ? (
{t('contextPanel.linear.empty.noComments')}
) : (
{comments.map((comment, index) => {
const author = comment.user?.displayName
|| comment.user?.name
|| t('contextPanel.linear.label.unassigned');
const avatarUrl = comment.user?.avatarUrl || null;
const initial = author.charAt(0).toUpperCase();
const isLast = index === comments.length - 1;
const createdLabel = formatCommentTimestamp(comment.createdAt);
return (
{!isLast ? (
) : null}
{avatarUrl ? (

) : (
{initial}
)}
{author}
{createdLabel ? {createdLabel} : null}
{comment.body.trim() ? (
) : null}
);
})}
)}
) : null}
{selectedIssue ? (
{worktreeToggle}
) : null}
);
}
return (
{showSearchField ? (
setQuery(event.target.value)}
onKeyDown={(event) => {
if (event.key === 'Escape' && compactFilters) {
event.preventDefault();
closeCompactSearch();
}
}}
className={cn('pl-9 w-full', compactFilters && 'pr-9')}
/>
{compactFilters ? (
) : null}
) : null}
{canUseListControls || (compactFilters && !showSearchField) ? (
{canUseListControls ? (
<>
item.value === listStatus) ?? STATUS_FILTER_ITEMS[0]).labelKey)}
ariaLabel={t('contextPanel.linear.filter.statusAria')}
value={listStatus}
active={listStatus !== 'all'}
disabled={filtersDisabled}
items={STATUS_FILTER_ITEMS.map((item) => ({
value: item.value,
label: t(item.labelKey),
}))}
onValueChange={(value) => {
if (isLinearIssueListStatus(value)) {
setListStatus(value);
}
}}
/>
item.value === listPriority) ?? PRIORITY_FILTER_ITEMS[0]).labelKey)}
ariaLabel={t('contextPanel.linear.filter.priorityAria')}
value={listPriority}
active={listPriority !== 'all'}
disabled={filtersDisabled}
items={PRIORITY_FILTER_ITEMS.map((item) => ({
value: item.value,
label: t(item.labelKey),
}))}
onValueChange={(value) => {
if (isLinearIssueListPriority(value)) {
setListPriority(value);
}
}}
/>
{
if (value === 'any' || value === 'me') {
setListAssignee(value);
}
}}
/>
{teams.length > 0 ? (
team.id === listTeamId)?.name ?? listTeamId)
}
ariaLabel={t('contextPanel.linear.filter.teamAria')}
value={listTeamId}
active={listTeamId !== LINEAR_ISSUE_LIST_ALL_TEAMS}
disabled={filtersDisabled}
items={[
{ value: LINEAR_ISSUE_LIST_ALL_TEAMS, label: t('contextPanel.linear.filter.team.all') },
...teams.map((team) => ({ value: team.id, label: team.name })),
]}
onValueChange={setListTeamId}
/>
) : null}
{workspaces.length > 1 && currentWorkspaceId ? (
workspace.id === currentWorkspaceId) ?? { id: currentWorkspaceId, name: null, urlKey: null })}
ariaLabel={t('contextPanel.linear.label.workspaceAria')}
value={currentWorkspaceId}
disabled={isSwitchingWorkspace}
items={workspaces.map((workspace) => ({
value: workspace.id,
label: workspaceLabel(workspace),
}))}
onValueChange={(value) => {
void switchWorkspace(value);
}}
/>
) : null}
{hasActiveFilters ? (
) : null}
>
) : null}
{compactFilters && !showSearchField ? (
) : null}
) : null}
{runtimeMissing ? (
{t('session.linearIssuePicker.empty.runtimeUnavailable')}
) : null}
{isLoading && issues.length === 0 ? (
{t('session.linearIssuePicker.loading.issues')}
) : null}
{showDisconnected ? (
{t('session.linearIssuePicker.empty.notConnected')}
) : null}
{error ? (
{error}
) : null}
{directIdentifier && linear && connected ? (
setSelectedIssueId(directIdentifier)}
>
{directIdentifier}
{t('session.linearIssuePicker.actions.useIssue', { identifier: directIdentifier })}
) : null}
{issues.length === 0 && !isLoading && connected && linear ? (
{debouncedQuery.trim()
? t('session.linearIssuePicker.empty.noIssuesFound')
: usingDefaultFilters
? t('session.linearIssuePicker.empty.noOpenIssuesFound')
: t('contextPanel.linear.empty.noMatchingIssues')}
) : null}
{issues.map(renderIssueRow)}
{hasMore && connected && linear ? (
) : null}
);
};