Skip to content
Sidebar & Navigation

Sidebar & Navigation

The sidebar provides the primary navigation structure for the Loop desktop app. It lists channels and threads, supports drag-and-drop reordering, batch selection, search filtering, and houses the app’s footer actions.

Related docs: Desktop App | Layouts | Settings | Chat


Layout

The sidebar is a vertical column on the left side of the app with the following sections, top to bottom:

  1. Drag region (38px) – macOS window dragging area with a collapse button at the top-right
  2. Header bar – “CHANNELS” label with Select and “+ new” buttons
  3. Search box – filter channels and threads
  4. New channel input – inline text field (shown when creating)
  5. Tabs – Recent and Tree, with the needs-you bell and the open tab’s task-thread toggle at the right; only the two toggles when nothing is recent
  6. Recent tab – sessions from anywhere in the tree, active ones first (see Recent Sessions )
  7. Tree tab – the DM channel, pinned at top, then the project channels as a sortable list with collapsible threads
  8. Spacer – pushes footer to bottom
  9. Footer – update button, settings, README
  10. Resize handle – right edge for width adjustment

Dimensions

PropertyValue
Minimum width180px
Maximum width25% of window width
Default width280px

The width is adjusted via a 4px-wide resize handle on the right edge:

  • Cursor changes to col-resize on hover
  • Handle becomes visible (colors.textDim) on hover and during drag
  • A 1px border-right (colors.border) is always visible
  • user-select: none is applied during drag to prevent text selection

When collapsed (sidebarOpen === false), the entire sidebar is hidden (returns null). An expand button appears in the workspace layout’s top drag region to restore the sidebar.


Channel Ordering

Drag-and-Drop

Top-level channels (not threads, not DM) support drag-and-drop reordering:

  1. Each ChannelItem has draggable set on its container.
  2. On drag start, the channel ID is stored in draggedIdRef.
  3. On drag over, the target channel shows a green top border (2px solid colors.active).
  4. On drop, the channel order array is updated: the source is removed from its position and inserted at the target position.

Persistence

Channel order is stored in localStorage under the key loop-channel-order as a JSON array of channel IDs. Channels not in the stored order sort to the end.


Recent Sessions

The Recent tab lists sessions (channels and threads alike) wherever they sit in it, so a running agent or one waiting on you is visible without expanding its parents.

TabHoldsOrder
RecentSessions with activity in the last 48 hours, including the active ones: those waiting on you (an approval, a question, a plan, a ready review) or with an agent or a review running. A container that’s only idling doesn’t count as active.Newest first; an active session counts as active now, so the active ones come first
TreeThe channel tree, unchangedAs described in Channel Ordering

Activity is the time of the channel’s newest message (last_activity_at from GET /api/channels ). A session that just stopped being active keeps that moment as its activity, so it doesn’t drop down the list before the next channel refresh brings its newest message’s time.

Each row shows:

  • a warning dot when it waits on you, or a spinner while its agent runs, and under it the same kind icon as in the tree: # for a channel, a branch for a worktree thread, a clock for a task thread, a return arrow for an ephemeral one, and a speech bubble for any other thread (the tree marks those by indent instead);
  • its name (task threads lose their marker prefix), in bold when unread, and below it the names of its parents, e.g. loop-dc6a › updates;
  • its uncommitted diff (+N -N), the unread dot and its status pills;
  • how long ago it was active (now, 12m, 30h; hours up to 48, so the whole Recent window reads in hours), unless it’s active now or a pill is shown.

Clicking a row opens the session; right-clicking opens the same context menu as the tree, and hovering shows the row’s details popup.

Tabs at the top of the list switch between Recent and Tree (the channel tree); the open tab is stored in localStorage under loop-sidebar-tab, and it’s Tree until you pick one. With nothing active in the last 48 hours there’s nothing to switch to, so only the tree shows, without tabs; the task toggle stays at the right and applies to Tree. A search or the task filter that empties Recent keeps the tabs, and Recent says no sessions match. Selection mode hides the tabs and shows the tree. The search box filters Recent by name or parent names, and it’s hidden in selection mode.


Search

The search box sits below the header and provides real-time case-insensitive filtering.

Behavior

  • Filters top-level channels by name match
  • Shows a parent channel if any of its threads match (even if the parent name does not match)
  • When the parent matches, all its threads are shown
  • When only threads match, only those matching threads are shown under the parent
  • Press Escape to clear the search query
  • Or click the clear button (✕) at the input’s right end; it shows only while there’s text, and focus stays in the input

UI

  • Search icon (magnifying glass) positioned inside the input field (left-aligned)
  • Clear button (✕, “Clear search”) inside the input field (right-aligned), shown only when the query isn’t empty
  • Input: full width, colors.bg background, 12px font, 4px border-radius
  • Placeholder: “Search…”

Hiding task threads

The clock button at the right of the tabs hides task threads from the open tab; click it again to show them. Each tab has its own setting, so you can hide tasks in Recent and keep them in Tree. A task thread is one a scheduled task created for its output (the rows with the clock icon): the backend records the task’s id on the thread when it creates it, and /api/channels returns it as task_id, so a renamed task thread is still hidden, while a channel or thread that only hosts tasks is not. Threads created before the id was recorded get it from their task #N (`schedule`) <prompt> name when the database is migrated. A hidden thread’s sub-threads are hidden in the tree with it. A task thread that’s running or waiting on you stays visible. The choices are stored in localStorage under loop-sidebar-hide-tasks, as {"recent": …, "tree": …}; a single setting saved before it became per tab applies to both.

Showing only what needs you

The bell at the right of the tabs narrows both tabs to the sessions with a lit pill: gate (an approval), ask (a question), plan (a plan to approve), rev (a review session open), trust (a project config to trust), ci (a CI failure no agent is fixing), cnf (a PR/MR with a merge conflict ) and cmt (a PR/MR with unresolved review comments ). The tree keeps the channels and threads that lead to a lit row, so it still reads as a tree, and drops their other threads; a lit row’s own unlit threads go too. Recent keeps only the lit sessions. With nothing lit, the tree says Nothing needs you right now. A running agent or review (rev…), or an agent fixing CI (ci…), doesn’t count: it isn’t waiting on you. The filter combines with the search and the task toggle, and steps aside in selection mode so every row can be picked. The choice is stored in localStorage under loop-sidebar-needs-you.


Selection Mode

Clicking “Select” enters batch selection mode.

Behavior in Select Mode

  • Each channel and thread shows a checkbox instead of the collapse chevron
  • The DM channel is excluded from selection (pinned, not selectable)
  • Checked items have a green border and background with a checkmark icon
  • Header shows:
    • “Delete (N)” button – deletes all selected items (red on hover)
    • “Cancel” button – exits selection mode and clears selections

Batch Delete

Channels and threads are deleted individually in sequence. If the currently selected channel is among the deleted, the selection is cleared.


Create Channel

Clicking “+ new” opens a dropdown menu with two options:

OptionAction
New projectShows an inline text input in the sidebar. Enter submits, Escape/blur cancels.
Open directory…Opens a native directory picker dialog (via showOpenDirectoryDialog IPC), then runs onboard:local and creates/selects the channel. Only available in Electron.

The dropdown is a positioned overlay that closes on outside click (left mousedown).


DM Channel

The DM channel is always pinned at the top of the channel list:

  • Identified by name === "dm" and no parent_id
  • Created automatically on first load if it does not exist (ensureChannel)
  • Auto-selected on first load if no channel is selected and no hash is present
  • Cannot be deleted (excluded from context menu delete option)
  • Cannot be selected in batch selection mode

Channel Item

Each channel item (ChannelItem component) displays:

Visual Elements

ElementDescription
Collapse chevronShown when channel has threads. Rotates 90 degrees when collapsed. Click to toggle.
Hash symbol (#)Channel prefix, dimmed text
Channel nameTruncated with ellipsis. Shows dir_path.split("/").pop() if no name set.
Status pillsrev (colors.active) when a review session is ready, rev… (colors.active) while a review runs, ask (colors.warning) when an agent is parked on an AskUserQuestion card. Pills come from store refs (reviewChannelIdsRef, askUserChannelIdsRef) that are kept in sync via WebSocket events and rehydrated on reconnect against /api/review/sessions and /api/asks/pending. The rev… pill comes from the channel’s review_running, which /api/channels reports and each review.status event patches. Status pills have no tooltip, so they don’t cover the row info popup, which says why the PR/MR pills are lit; only a plugin badge shows its plugin’s title.
Status indicatorGreen dot (6px circle, colors.active) when container_running or agent_running is true, or a review is running on it
Config button (gear icon)Shown on hover for channels with dir_path. Opens project settings.
“+ thread” buttonShown on hover. Toggles the new thread input.

Interaction

ActionBehavior
Click channel nameSelect the channel
Click collapse chevronToggle thread visibility
Double-click channel rowCollapse/expand threads
HoverShows config and thread buttons; applies colors.hoverBg background. Shows the row info popup.
Right-clickOpens context menu
DragInitiates channel reorder

Context Menu

Right-clicking a channel opens a context menu with:

ItemAction
Copy LinkCopies loop://channel/<id> to clipboard
Copy Channel IDCopies the raw channel ID
Open TicketOpens the channel’s ticket URL in the browser (only when one is set; threads too)
Edit TicketSets, changes or clears the channel’s ticket URL (not shown for DM; threads too)
Delete ChannelDeletes the channel (not shown for DM)

The “Delete Channel” item has danger: true styling (red text) and a separator above it.

Row Info

Hovering a channel, thread or worktree thread shows a popup beside the row (RowInfoPopup) with:

LineShows
titleThe row’s full name, in bold at the top: the sidebar cuts a long one short.
descriptionWhat the thread is for, above the other lines, as written (line breaks kept). Set from the context menu or by the agent’s set_thread_description MCP tool. Left out when empty.
ticketThe URL of the row’s ticket , e.g. a Jira issue. Left out when there’s none.
pathThe row’s directory. For a worktree thread, that’s the worktree’s checkout.
branchThe checked-out branch, e.g. main, picked out in the theme’s accent color. A worktree thread adds the branch it was cut from, e.g. worktree/fix (from main). A detached checkout shows detached.
prThe pull request open on the branch, e.g. #42 Fix the login (open). Left out when there’s none.
ciWhy the ci or ci… pill is lit: the failing count and checks, then why auto-fix acted as it did, e.g. ✗ 2 failing (lint, test) The head commit isn't one this thread's agent made, so auto-fix leaves it to you., or agent fixing.
conflictWhy the cnf pill is lit: with main yours to resolve.
cmtWhy the cmt pill is lit: 2 unresolved review threads.
commitThe short commit hash and its subject line.
syncHow far the branch is from its base (worktree threads) and its upstream, e.g. ↑3 ↓1 vs main · ↑1 vs origin/main, or even with main.
modelThe model and effort picked for this channel in the chat, e.g. claude-opus-5-5 · high. Left out when it uses the config’s. Updates as soon as they’re changed.
statusagent running or container running, and locked, when any of them apply.

The git lines follow the branch poller , so they update within a few seconds of a commit, even while the popup is open. The ci, conflict and cmt lines show only while their pill is lit, and the watcher’s PR status behind them is looked up only while the popup is open; the pills themselves have no tooltip. A line with nothing to show is left out; every row has at least its title. Moving off the row, clicking, dragging or scrolling hides it.

Agents read the same lines for a whole tree of channels, threads and worktree threads with the channel_status MCP tool .


Thread Item

Hovering a channel row reveals a +wt button; clicking it opens a branch picker (portaled just below the button) to spin off a git worktree from any branch — the worktree opens as its own thread under that channel.

The +wt branch picker on a channel row, listing branches to create a git worktree from

A worktree thread (git-branch icon) nested under its parent channel in the sidebar, opened in its own isolated workspace

Threads are listed below their parent channel, indented with a tree connector line. Threads can contain sub-threads (e.g. scheduled task threads), forming a 3-level hierarchy.

Visual Elements

ElementDescription
Tree connectorSVG with vertical line and horizontal branch at 50% height. Last thread’s vertical line stops at 50%. Positioned at 16px left offset. Z-index 1 to stay above hover backgrounds.
Worktree iconGit branch SVG icon (colors.active) for worktree threads.
Task thread iconClock SVG icon (colors.textDim) for scheduled task threads (detected by task # name prefix).
Ephemeral iconUndo-arrow SVG icon (60% opacity) for ephemeral task threads (detected by [ephemeral] prefix).
Thread nameTruncated with ellipsis. Task thread names have emoji prefixes (🧵, ⏱) stripped for display. Falls back to thread ID.
Status pillsSame set as channels: rev for a ready review session, rev… while a review runs, and ask when an agent is parked on an AskUserQuestion card.
Status indicatorGreen dot when container_running or agent_running, or a review is running on it, positioned at the right edge.
CheckboxShown in selection mode instead of normal layout
Sub-threadsRendered recursively below the thread with the same connector style.

Indentation

Threads are indented with padding-left: 30px (accounting for the tree connector). The connector line uses colors.textDisabled (#555555) at 1.5px stroke width.

Interaction

ActionBehavior
ClickSelect the thread
Hovercolors.hoverBg background. Shows the row info popup.
Right-clickOpens the same context menu as channels

Create Thread

Clicking “+ thread” on a channel reveals an inline input (NewThreadInput) below the channel. Behavior:

  • Enter submits the thread name
  • Escape or blur cancels
  • The input auto-focuses on mount

Rename Thread

Right-click a thread and pick Rename Thread (Rename Worktree for a worktree thread) to open the rename dialog. It isn’t offered for the DM or a locked thread.

Renaming changes only the display name, for threads and worktree threads alike: the directory, git branch and Claude sessions stay as they are (POST /api/channels/{id}/rename).

Thread Description

Right-click a thread or worktree thread and pick Edit Description to open the description dialog (DescriptionDialog), prefilled with the current description when there is one. Enter saves, Shift+Enter starts a new line, Escape cancels; saving it empty clears it. It’s capped at 500 characters. Unlike renaming, it’s offered on locked threads too, since it changes nothing on disk; it isn’t offered for channels or the DM.

The description shows at the top of the row info popup. It’s stored on the channel row (POST /api/channels/{id}/description), and a change is broadcast as a channel.updated event carrying only the description, so every open window picks it up, including an open popup. Agents set it with the set_thread_description MCP tool , which defaults to their own thread.

Ticket URL

A channel, thread or worktree thread can be linked to its ticket: the URL of a Jira issue, a GitHub issue or PR, or any other tracker’s page. Agents set it with the set_ticket_url MCP tool , which defaults to their own channel or thread; it’s stored on the channel row (POST /api/channels/{id}/ticket, absolute http(s) URLs only, empty clears).

It shows as the first line of the row info popup and follows changes live, like the description. The popup can’t be clicked, so the row’s context menu gets an Open Ticket item while one is set, which opens it in the browser. The workspace header shows it too, after the channel or thread id, by its key (ChannelHeaderInfo, ticketKey): the issue key when the URL carries one (PROJ-123, Jira’s ?selectedIssue= included, Linear and the like), owner/repo#7 for a GitHub issue or PR, project!5 / project#3 for a GitLab merge request or issue, otherwise the URL’s last path segment. Clicking it opens the ticket; hovering shows the full URL.

Right-click any row but the DM and pick Edit Ticket to open the ticket dialog (TicketDialog), prefilled with the current URL when there is one. Enter saves, Escape cancels; saving it empty clears it. It checks the URL as you type with the same rules as the API, so a URL the API would reject (a bare PROJ-123, a non-http(s) scheme) shows why and can’t be saved. It’s offered on locked rows too, since it changes nothing on disk.

Pull Request

The PR open on a row’s branch isn’t stored: the daemon looks it up with gh, or with glab when the repo’s origin is a GitLab host (see GitLab ) (GET /api/channels/{id}/pr, cached for a minute per directory and branch, and dropped when the branch poller sees the branch or commit change). So it follows the branch: a new PR or a branch switch shows without anyone setting it. Without gh (or glab), without an origin remote, or for a branch with no PR, nothing shows.

It shows in the row info popup, looked up only while the popup is open, and in the workspace header after the ticket (ChannelHeaderInfo, useChannelPR) as PR #42 with a dot in GitHub’s state colors: green open, grey draft, purple merged, red closed. A GitLab merge request shows as MR !12. Clicking it opens the PR; hovering shows its title, state and URL. The Git panel shows the same PR as a chip.

Delete Thread

Threads can be deleted via:

  • Context menu “Delete Channel” (same operation for threads)
  • Batch selection mode

Footer

The footer sits at the bottom of the sidebar, separated by a top border.

Update Button

Only shown when updateStatus?.available is true. The button text and behavior depend on the update state:

StateTextColorAction
Available“Update available vX.Y.Z”colors.active (green)Download update
Downloading“Downloading…”colors.textDimDisabled (no action)
Downloaded“Restart to update”colors.active (green)Install and restart
Error“Update failed – click to retry”colors.activeRetry download

The button includes a download icon (arrow into tray).

Kanban Button

  • Board icon + “Kanban” text
  • Click opens the Kanban panel as an overlay
  • colors.textDim text, brightens on hover

Settings Button

  • Gear icon + “Settings” text
  • Click opens the Settings panel
  • Keyboard shortcut: Cmd+, (handled at the app level)
  • colors.textDim text, brightens on hover

README Button

  • Book icon + “README” text
  • Click opens the README markdown panel
  • colors.textDim text, brightens on hover

Window Title

The app title bar updates based on the selected channel/thread (handled in App.tsx):

SelectionTitle
NoneLoop
Channel<channel-name> - Loop
Thread<parent-name> > <thread-name> - Loop