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: 0 additions & 2 deletions packages/web-shell/client/components/ChatEditor.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -404,7 +404,6 @@ interface ChatEditorRenderProps {
builtinAtProviders?: WebShellCustomization['builtinAtProviders'];
atProviders?: WebShellCustomization['atProviders'];
skills?: Array<{ name: string; description: string }>;
language?: WebShellLanguage;
reasoning?: DaemonReasoningControls;
onSelectReasoningEffort?: (value: ReasoningSelection) => Promise<void> | void;
}
Expand All @@ -422,7 +421,6 @@ function renderChatEditorInto(
language = 'en',
renderComposerTagTooltip,
onComposerTagClick,
language = 'en',
...chatEditorProps
} = props;
if (composerTags) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1165,7 +1165,7 @@ describe('SessionOverviewPanel', () => {
expect(rows()).toHaveLength(50);
act(() => click(selectAllCheckbox()));
expect(container!.textContent).toContain('60 of 60 row(s) selected.');
});
}, 15000);

it('lists an other-workspace session as a row with its folder', async () => {
connectionState.capabilities = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,35 @@ import { SessionGroupSection } from './SessionGroupSection';
globalThis.IS_REACT_ACT_ENVIRONMENT = true;

describe('SessionGroupSection', () => {
it('renders a supplied source icon instead of a color dot', () => {
const container = document.createElement('div');
document.body.appendChild(container);
const root = createRoot(container);
act(() => {
root.render(
<I18nProvider language="en">
<SessionGroupSection
id="scheduled-task:task-1"
label="Review PRs"
count={1}
expanded
icon={<svg data-testid="scheduled-task-icon" />}
onToggle={() => {}}
>
<div>Run 1</div>
</SessionGroupSection>
</I18nProvider>,
);
});

expect(
container.querySelector('[data-testid="scheduled-task-icon"]'),
).not.toBeNull();
expect(container.querySelector('[class*="sessionGroupDot"]')).toBeNull();
act(() => root.unmount());
container.remove();
});

it('shows five sessions and resets Show all after collapsing', () => {
const container = document.createElement('div');
document.body.appendChild(container);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ export interface SessionGroupSectionProps {
count: number;
expanded: boolean;
color?: DaemonSessionGroupColor;
icon?: ReactNode;
children: ReactNode;
onToggle: () => void;
onRename?: () => void;
Expand All @@ -37,6 +38,7 @@ export function SessionGroupSection({
count,
expanded,
color,
icon,
children,
onToggle,
onRename,
Expand Down Expand Up @@ -71,11 +73,17 @@ export function SessionGroupSection({
aria-expanded={expanded}
onClick={onToggle}
>
<span
className={`${styles.sessionGroupDot} ${colorClass}`}
style={dotStyle}
aria-hidden="true"
/>
{icon ? (
<span className={styles.sessionGroupIcon} aria-hidden="true">
{icon}
</span>
) : (
<span
className={`${styles.sessionGroupDot} ${colorClass}`}
style={dotStyle}
aria-hidden="true"
/>
)}
<span className={styles.sessionGroupTitle}>{label}</span>
<span className={styles.sessionGroupCount}>· {count}</span>
<span className={styles.sessionGroupChevron} aria-hidden="true">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -604,6 +604,26 @@
background: var(--muted-foreground);
}

.sessionGroupIcon {
width: 14px;
height: 14px;
flex: 0 0 14px;
display: inline-flex;
align-items: center;
justify-content: center;
}

.sessionGroupIcon svg {
width: 14px;
height: 14px;
display: block;
fill: none;
stroke: currentColor;
stroke-width: 1.6;
stroke-linecap: round;
stroke-linejoin: round;
}

.sessionGroupTitle {
min-width: 0;
overflow: hidden;
Expand Down
78 changes: 58 additions & 20 deletions packages/web-shell/client/components/sidebar/WebShellSidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,11 @@ import {
replaceOwnedCollapsedSessionSectionIds,
} from './collapsedSessionSections';
import { measureSessionTitleScroll } from './sessionTitleScroll';
import {
collectScheduledTaskSession,
getScheduledTaskSessionGroup,
type ScheduledTaskSessionSection,
} from './scheduled-task-session-groups';
import {
SESSION_LIST_PAGE_SIZE,
SESSION_ORGANIZATION_FEATURE,
Expand Down Expand Up @@ -155,19 +160,12 @@ const SESSION_MENU_PORTAL_STYLE: CSSProperties = {
const GROUP_MENU_MARGIN = 8;
const CUSTOM_GROUP_COLOR_OPTION = '__custom__';
const DEFAULT_CUSTOM_GROUP_COLOR: DaemonSessionGroupHexColor = '#416ef5';
// Mirrors `SCHEDULED_TASK_RUN_SOURCE_ID_PREFIX` in acp-bridge/session-source.ts
// (the client cannot import that package). Per-run scheduled task children keep
// the `default` source type so they list with ordinary conversations.
const SCHEDULED_TASK_RUN_SOURCE_ID_PREFIX = 'scheduled_task_run:';

type SidebarSessionSource = 'default' | 'channel';

function isScheduledTaskSession(session: DaemonSessionSummary): boolean {
return (
session.sourceType === 'scheduled_task' ||
(session.sourceType === 'default' &&
session.sourceId?.startsWith(SCHEDULED_TASK_RUN_SOURCE_ID_PREFIX) ===
true)
getScheduledTaskSessionGroup(session) !== undefined
);
}

Expand Down Expand Up @@ -324,7 +322,7 @@ const SESSION_GROUP_COLORS: DaemonSessionGroupPresetColor[] = [

type GroupEditorMode = 'create' | 'edit';

type SessionSectionKind = 'color' | 'group' | 'recent';
type SessionSectionKind = 'color' | 'group' | 'scheduled-task' | 'recent';

interface SessionSection {
id: string;
Expand Down Expand Up @@ -968,11 +966,13 @@ export function WebShellSidebar({
? sessionSource
: 'default'
: undefined;
const channelGroupingEnabled = Boolean(
projectFeaturesEnabled &&
selectedSessionSource === 'channel' &&
workspace.capabilities?.features.includes('channel_management'),
const channelManagementEnabled = Boolean(
workspace.capabilities?.features.includes('channel_management'),
);
const channelGroupingEnabled =
projectFeaturesEnabled &&
selectedSessionSource === 'channel' &&
channelManagementEnabled;
const {
data: channelCatalogData,
catalog: channelTypeCatalog,
Expand Down Expand Up @@ -1284,6 +1284,29 @@ export function WebShellSidebar({
if (prevOrganizationEnabled !== organizationEnabled) {
setPrevOrganizationEnabled(organizationEnabled);
setGroupsCatalogReady(!organizationEnabled);
if (organizationEnabled) {
// An org-disabled settle may already have consumed the Tasks latch
// against scheduled-task sections alone — possibly while the Channels
// tab was selected. Re-arm the source whose catalog gains manual
// groups so the first organized settle registers them as initial
// instead of collapsing them.
awaitingInitialSessionCatalogBySourceRef.current.default = true;
}
}
// The channel-grouping branch clears `groups` while the Channels tab is
// selected; when switching back to Tasks, close the gate during that same
// render so the settle cannot consume the first-sync latch against the
// empty catalog before the organized groups reload lands.
const [prevSessionSource, setPrevSessionSource] = useState(sessionSource);
if (prevSessionSource !== sessionSource) {
setPrevSessionSource(sessionSource);
if (
organizationEnabled &&
sessionSource === 'default' &&
channelManagementEnabled
) {
setGroupsCatalogReady(false);
}
}
const [sidebarWidth, setSidebarWidth] = useState(readSidebarWidth);
const [projectExpanded, setProjectExpanded] = useState(() =>
Expand Down Expand Up @@ -3855,14 +3878,17 @@ export function WebShellSidebar({
);

const sessionSections = useMemo<SessionSection[]>(() => {
if (!organizationEnabled) return [];
const searching = searchQuery.trim().length > 0;
const validGroupIds = new Set(groups.map((group) => group.id));
const sessionsByColor = new Map<
DaemonSessionGroupPresetColor,
DaemonSessionSummary[]
>();
const sessionsByGroupId = new Map<string, DaemonSessionSummary[]>();
const scheduledTaskSections = new Map<
string,
ScheduledTaskSessionSection
>();
for (const group of groups) {
sessionsByGroupId.set(group.id, []);
}
Expand All @@ -3878,7 +3904,11 @@ export function WebShellSidebar({
for (const session of searchedSessions) {
// Color takes precedence: the picker keeps color and group mutually
// exclusive, but stay defensive if a store somehow carries both.
if (session.color && SESSION_GROUP_COLORS.includes(session.color)) {
if (
organizationEnabled &&
session.color &&
SESSION_GROUP_COLORS.includes(session.color)
) {
const bucket = sessionsByColor.get(session.color) ?? [];
bucket.push(session);
sessionsByColor.set(session.color, bucket);
Expand All @@ -3892,6 +3922,9 @@ export function WebShellSidebar({
groupSessions.push(session);
continue;
}
if (collectScheduledTaskSession(scheduledTaskSections, session)) {
Comment thread
qwen-code-dev-bot marked this conversation as resolved.
continue;
}
// On sources with a Pinned section, a pinned session without a
// (renderable) group stays Pinned-section-only; it never spills into
// Ungrouped. The channel source has no Pinned section, so its pinned
Expand Down Expand Up @@ -3936,6 +3969,9 @@ export function WebShellSidebar({
sessions: groupSessions,
});
}
for (const section of scheduledTaskSections.values()) {
sections.push({ ...section, kind: 'scheduled-task' });
}
if (recentSessions.length > 0 && sections.length > 0) {
sections.push({
id: RECENT_SESSION_SECTION_ID,
Expand Down Expand Up @@ -3964,8 +4000,8 @@ export function WebShellSidebar({
// until it settles; wait for a page fetched for the channel source.
if (settledSessionsSourceRef.current !== 'channel') return;
} else {
if (!organizationEnabled) return;
if (!groupsCatalogReady || !sessionsCatalogReady) return;
if (!sessionsCatalogReady) return;
if (organizationEnabled && !groupsCatalogReady) return;
Comment thread
qwen-code-dev-bot marked this conversation as resolved.
}
const unseenIds = activeSections
.map((section) => section.id)
Expand Down Expand Up @@ -4809,9 +4845,6 @@ export function WebShellSidebar({
if (selectedSessionSource === 'channel') {
return renderFlatSessions();
}
if (!organizationEnabled) {
return renderFlatSessions();
}
if (sessionSections.length === 0) {
return renderFlatSessions();
}
Expand All @@ -4826,6 +4859,11 @@ export function WebShellSidebar({
label={section.label}
count={section.sessions.length}
color={section.color}
icon={
section.kind === 'scheduled-task' ? (
<CalendarClockIcon data-web-shell-scheduled-task-group />
) : undefined
}
limitSessions={editingSessionIdentity === null && !searchQuery.trim()}
expanded={expanded}
onToggle={() => toggleSessionSection(section.id)}
Expand Down
Loading
Loading