Skip to content
Open
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
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { subscribeOverlayInteraction, createOverlayPortal, Icon, IconButton, Men
import {
DEFAULT_WORKSPACE_SESSION_VIEW,
hasWorkspaceSessionFilters,
WORKSPACE_SESSION_ORDERING_MODES,
type WorkspaceSessionEnvironment,
type WorkspaceSessionOrdering,
type WorkspaceSessionShow,
Expand Down Expand Up @@ -116,7 +117,9 @@ const WorkspaceSessionFilterMenu: React.FC = () => {

const definitions = useMemo<Record<Submenu, MenuDefinition>>(() => ({
ordering: {
kind: 'single', value: view.ordering, options: ['updated', 'status', 'created', 'name'] as WorkspaceSessionOrdering[], choose: value => view.setOrdering(value as WorkspaceSessionOrdering),
// The full mode list, so `manual` shows up as the current value and can be
// left again once a drag or a row-menu move switched the list to it.
kind: 'single', value: view.ordering, options: WORKSPACE_SESSION_ORDERING_MODES, choose: value => view.setOrdering(value as WorkspaceSessionOrdering),
},
show: {
kind: 'single', value: view.show, options: ['all', 'unread', 'attention'] as WorkspaceSessionShow[], choose: value => view.setShow(value as WorkspaceSessionShow),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -910,3 +910,37 @@
@include nav-font.nav-panel-text-icon;
}
}

// ── Manual (drag) reordering ────────────────────────────────────────────────────
// The dragged row and the drop slot live on the row itself; the drop line is the
// same accent rail the workspace reorder uses, drawn on the slot's edge. Flat
// selectors on purpose: `&__inline-item` above is the row's one definition and a
// second nested block would fork it.
.openbitfun-nav-panel__inline-item.is-manual-dragging {
opacity: 0.42;
}

.openbitfun-nav-panel__inline-item.is-manual-drop-before,
.openbitfun-nav-panel__inline-item.is-manual-drop-after {
background: color-mix(in srgb, var(--openbitfun-color-accent-default) 8%, transparent);
}

.openbitfun-nav-panel__inline-item-manual-drop-line {
position: absolute;
left: 0;
right: 0;
z-index: 2;
height: 2px;
margin: 0;
background: var(--openbitfun-color-accent-default);
border-radius: 999px;
pointer-events: none;

.is-manual-drop-before & {
top: 0;
}

.is-manual-drop-after & {
bottom: 0;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,13 @@ import {
import { useSessionRowRemovalTransition } from './sessionRowShift';
import { SessionStatusIndicator } from './SessionStatusIndicator';
import { createSessionsNavSelector } from './sessionsNavSelector';
import {
orderSessionsForManualView,
sessionManualOrderScopeKey,
switchSessionViewToManualOrdering,
useSessionManualOrder,
useSessionManualReorder,
} from './sessionManualReorder';
import './SessionsSection.scss';

const log = createLogger('SessionsSection');
Expand Down Expand Up @@ -738,6 +745,11 @@ const SessionsSection: React.FC<SessionsSectionProps> = ({
};
}, [isExportScopeMenu, openMenuSessionId, updateContextSessionMenuPosition]);

// Manual ordering is per scope: one workspace group, or the flat "all sessions"
// projection, which renders without a workspace id.
const manualSessionOrderScopeKey = sessionManualOrderScopeKey(workspaceId);
const manualSessionOrder = useSessionManualOrder(manualSessionOrderScopeKey);

const sessions = useMemo(
() =>
Array.from(flowChatState.sessions.values())
Expand Down Expand Up @@ -788,15 +800,22 @@ const SessionsSection: React.FC<SessionsSectionProps> = ({
sessionNavStatusService.getSortTimestamp,
);

// Manual ordering is the only mode that does not compare two rows: the stored
// order of this scope decides, and a row it has never seen keeps its place.
const isManualOrdering = sessionOrdering === 'manual';
const orderForCurrentView = (list: Session[]): Session[] => isManualOrdering
? orderSessionsForManualView(list, manualSessionOrder)
: [...list].sort(compareForCurrentView);

for (const [pid, list] of childMap) {
childMap.set(pid, [...list].sort(compareForCurrentView));
childMap.set(pid, orderForCurrentView(list));
}

return {
topLevelSessions: [...parents].sort(compareForCurrentView),
topLevelSessions: orderForCurrentView(parents),
childrenByParent: childMap,
};
}, [orderingRevision, runningSessionIds, sessionOrdering, sessions]);
}, [manualSessionOrder, orderingRevision, runningSessionIds, sessionOrdering, sessions]);

const topLevelSessions = useMemo(
() => allTopLevelSessions.filter(session => matchesWorkspaceSessionView(
Expand Down Expand Up @@ -1004,6 +1023,22 @@ const SessionsSection: React.FC<SessionsSectionProps> = ({
[visibleItems],
);

const sessionReorder = useSessionManualReorder({
scopeKey: manualSessionOrderScopeKey,
// A draggable row would take the drag that selects text in the inline title
// editor, so the whole list stops being draggable while a row is edited.
enabled: useWorkspaceViewPreferences && editingSessionId === null,
// Loaded rows decide the stored order, but only the rendered ones can be
// reordered: the rows behind the render boundary must not swallow a step.
topLevelSessions: allTopLevelSessions,
childrenByParent,
renderedIds: visibleSessionIds,
listRef: sessionListRef,
// A committed reorder switches the view to the manual order it just wrote,
// without a notice: the mode control already shows the new mode.
onManualOrderCommit: switchSessionViewToManualOrdering,
});

const visibleRowSignature = useMemo(
() => visibleItems.map(item => item.session.sessionId).join('|'),
[visibleItems],
Expand Down Expand Up @@ -1733,6 +1768,8 @@ const SessionsSection: React.FC<SessionsSectionProps> = ({
: false,
});
const showAssistantIdentity = Boolean(assistantIdentity && !isChildSession);
const isDraggingRow = sessionReorder.isDragging(session.sessionId);
const dropPosition = sessionReorder.dropPositionFor(session.sessionId);
const row = (
<div
className={[
Expand All @@ -1743,6 +1780,8 @@ const SessionsSection: React.FC<SessionsSectionProps> = ({
isRowActive && 'is-active',
isEditing && 'is-editing',
openMenuSessionId === session.sessionId && 'is-menu-open',
isDraggingRow && 'is-manual-dragging',
dropPosition && `is-manual-drop-${dropPosition}`,
]
.filter(Boolean)
.join(' ')}
Expand All @@ -1760,8 +1799,13 @@ const SessionsSection: React.FC<SessionsSectionProps> = ({
data-session-kind={relationship.kind}
data-session-level={String(level)}
data-session-active={isRowActive ? 'true' : 'false'}
{...sessionReorder.rowDragProps(session.sessionId)}
onPointerDown={event => handleSessionOpenPointerDown(event, session)}
onClick={() => handleSwitch(session.sessionId)}
onClick={() => {
// A committed drop must not open the row it reordered.
if (sessionReorder.isRowClickSuppressed()) return;
handleSwitch(session.sessionId);
}}
onContextMenu={event => handleContextMenu(event, session.sessionId)}
>
{showAssistantIdentity && assistantIdentity ? (
Expand Down Expand Up @@ -2098,6 +2142,9 @@ const SessionsSection: React.FC<SessionsSectionProps> = ({
)}
</>
)}
{dropPosition ? (
<span className="openbitfun-nav-panel__inline-item-manual-drop-line" aria-hidden="true" />
) : null}
</div>
);
// Always wrapped, even while editing or with a row menu open: swapping
Expand Down
Loading
Loading