Skip to content

feat(sidebar): pin tasks waiting for input to the top, make task names readable - #250

Open
oluies wants to merge 3 commits into
johannesjo:mainfrom
oluies:task/projectlist-29a448
Open

feat(sidebar): pin tasks waiting for input to the top, make task names readable#250
oluies wants to merge 3 commits into
johannesjo:mainfrom
oluies:task/projectlist-29a448

Conversation

@oluies

@oluies oluies commented Aug 2, 2026

Copy link
Copy Markdown

What

Two changes to the left sidebar task list.

1. A "Waiting for you" tray, pinned under + New Task

When several tasks run in parallel, the one that just stopped to ask you something is wherever it happens to sit in the list — easy to miss, and easy to lose again once the list scrolls. This adds a tray between the + New Task button and the scrollable list holding every task blocked on an answer, newest question first. It sits outside the scroll container, so it never scrolls away.

Each row shows the status dot, the task name, the project it belongs to (the row is out of its project group, so it needs the label), and how long it has been waiting. Clicking a row opens the task and restores its last focused panel, so the question can be answered in one click.

Tasks stay in their project group as well — the tray is a shortcut, not a move. That keeps the data-task-indextaskOrder mapping drag-reorder depends on intact, and keeps list positions stable.

It's a mode: sidebarNeedsInputFirst, persisted, on by default, with an × in the tray header and a Settings → Behavior checkbox to turn it back on.

Pinning on — two tasks asked, most recent on top Pinning off — plain grouped list
Sidebar with the Waiting for you tray pinned under New Task, listing two tasks newest-first Sidebar with pinning disabled, showing the plain project-grouped task list

Ordering by recency needed a question-onset timestamp, which nothing recorded. taskStatus now stamps one per agent when a question appears, clears it when the question goes away, and exposes getTaskQuestionSince(taskId).

2. Task names you can actually read

Long names were clipped mid-word. Three separate causes, all fixed:

  • The name span sat in a flex row with no min-width: 0, so text-overflow: ellipsis never applied and the text was simply cut by the row's overflow: hidden. Names now wrap to at most two lines with the full name as a tooltip.
  • Rows are flex children of the scrolling task column, so they were shrunk below their content height — the second line got clipped. Rows now set flex-shrink: 0.
  • The task-item appear animation tweens max-height to a fixed 40px, shorter than any two-line row, and the class stays on the element. The keyframe now animates only opacity and transform.

Both screenshots above show the result — every name renders on two full lines with a proper ellipsis.

Alongside that: the direct branch badge moved after the name so the name claims width first (and the badge is capped and ellipsised), project group headers ellipsise while keeping their (n) count visible, and row spacing went 1px → 3px.

Testing

npm run typecheck, npm run lint, npm run format:check clean; full suite 1683 passed / 24 skipped.

New src/store/sidebar-attention.test.ts covers the ordering: newest question first, collapsed tasks included, unknown timestamps sink to the bottom, missing task ids skipped, no task listed twice.

Verified in the running app, not just in tests: launched Electron against a scratch profile seeded with two projects and four tasks, driven by stub agents that print a question and wait, so the real PTY question detector fires. The tray appeared with one entry, then two, with the later question sorted above the earlier one; the × persisted the mode to state.json. The two clipping bugs above were found because of that run — they were invisible to typecheck, lint, and unit tests.

@johannesjo

Copy link
Copy Markdown
Owner

Thanks — I carefully re-reviewed the complete current head (71a466f), including the surrounding task-status, multi-agent tab, shell-focus, persistence, and sidebar-ordering paths. The full suite (1,683 tests), typecheck, lint, formatting, and git diff --check all pass.

I found two cases that I think should be fixed before merge:

  1. A task with an open question can be omitted from the tray when another status has higher priority. computeNeedsInputTasks() requires getTaskAttentionState(taskId) === 'needs_input', but that function returns error or review before checking question state. In a multi-agent task, one agent can exit non-zero while another running agent is asking; the task remains blocked on human input but disappears from “Waiting for you.” The same conflict can occur with needsReview. Tray membership should come directly from open-question state rather than the single prioritized display status, with a mixed error/question regression test.

  2. Clicking a tray row does not necessarily open the terminal that asked the question. jumpToWaitingTask() restores the previously focused panel. If a non-selected AI agent asks while terminals are in tabs mode, its pane remains hidden; if a shell asks, the click can focus an AI terminal, Notes, or Prompt instead. That means another click is still required to answer. Please carry the newest asking agent/panel through the tray entry and focus its exact ai-terminal:<id> or shell:<index> target, with coverage for a non-selected tab or shell question.

A small helper returning the newest open question's { agentId, since } (or resolved panel target) could address both cases without adding another broad abstraction. Other than these two issues, I did not find additional correctness problems in the current diff.

oluies added a commit to oluies/parallel-code that referenced this pull request Aug 4, 2026
Address PR johannesjo#250 review.

Tray membership came from `getTaskAttentionState(taskId) === 'needs_input'`,
but that function returns `error` or `review` before it ever checks question
state. In a multi-agent task one agent can exit non-zero — or the task can be
flagged for review — while another agent is still blocked on an answer, and the
task silently dropped out of "Waiting for you" even though a human answer was
still owed.

`getTaskOpenQuestion(taskId)` now reports the newest open question directly as
`{ agentId, since }`, independent of the prioritized display status, and
replaces `getTaskQuestionSince`. `computeNeedsInputTasks` keys off it. The
asking set and the onset map are separate structures kept in step by their sole
writer, so the one place they meet is the one place that tolerates a skew —
`since` is a plain `number` everywhere downstream.

Clicking a tray row also restored the *previously* focused panel, so a question
from a non-selected agent tab or from a shell landed somewhere else and needed a
second click. Each entry now carries the asking panel — `ai-terminal:<agentId>`
or `shell:<index>` — and `jumpToWaitingTask` focuses that, re-validating it
against the task's current agents at click time so a stale panel degrades to the
last focused one instead of focusing nothing. Since `setTaskFocusedPanel`
selects the agent behind an ai-terminal panel, a background tab is brought to
the front; the three writes are batched so `setActiveTask`'s intermediate
selection of the *old* agent never renders. `jumpToWaitingTask` moved from
Sidebar.tsx into the store so that ordering is testable.

Two display consequences of question-driven membership, fixed here:

- The tray's status dot was hardcoded to `needs_input`. A row can now also be
  errored or awaiting review, so it takes the task's real attention state and no
  longer contradicts the same task's dot in the list below. The tray header
  carries the "waiting for you" message on its own.
- `getDotTooltip` had no `review` branch, so a review-flagged task with an
  active agent showed a purple dot reading "Busy". Added.

The `shell:<index>` convention was hand-rolled at six sites and `ai-terminal:`
at two. `shellPanelId`/`shellPanelIndex`/`isShellPanel` join `aiTerminalPanelId`
in focused-panel.ts and every site now goes through them, so the format is
built and parsed in one place. The parser takes digits only — `Number('')` is 0
and `Number('1e2')` is 100, so a loose parse would let `shell:` validate as a
real panel.

Two comments corrected while here: the collapsed sweep in
`computeNeedsInputTasks` and the matching `uncollapseTask` guard are inert —
`collapseTask` kills every agent and clears its question state — so they no
longer claim minimized tasks show up in the tray.

Known gap, deliberately not converted: `desktopNotifications.ts` and
`remoteStatusSync.ts` still classify `needs_input` from the attention state and
so inherit the same masking. Changing when a desktop notification fires is a
product decision rather than mechanical follow-through, so it is documented on
`getTaskOpenQuestion` and left to the author.

Tests: `getTaskOpenQuestion` covered against a mixed error/question task, a
review-flagged task, several agents asking at once, a shell question, and a
stopped agent; the tray covered for panel resolution and an unplaceable agent;
`jumpToWaitingTask` for the ai-terminal and shell targets, the last-focused
fallback, select-then-focus ordering, the uncollapse guard, and stale or
malformed panels; plus shell-panel-id round-trip and rejection cases, and
`getDotTooltip('busy', 'review')`.

Verified in the running app on a scratch profile with stub agents, not just in
tests: a task whose first agent exits 1 while a second asks stays listed, its
dot reads red in both the tray and the list, and clicking the row with the
failed agent's tab in front switches to the asking agent's tab.

Full suite 1710 passed / 24 skipped.
@oluies

oluies commented Aug 4, 2026

Copy link
Copy Markdown
Author

desktopNotifications.ts:97 and remoteStatusSync.ts:38 still classify needs_input from getTaskAttentionState, so they carry the same masking. In the scenario you described, the "Task Needs Input" desktop notification never fires and the mobile overview shows only the error. getTaskOpenQuestion is what both would use. I didn't change it

oluies added 3 commits August 4, 2026 18:57
The sidebar task list clipped long names mid-word — the name span had no
min-width:0 inside its flex row, so text-overflow never applied — and a task
that started asking a question stayed wherever it happened to sit in the list.

Names now wrap to two lines with the full name as a tooltip, the direct-branch
badge moved after the name so the name claims width first, and project group
headers ellipsise instead of pushing their count out of view.

A new "Waiting for you" tray sits between the New Task button and the
scrollable list, holding every task blocked on an answer, newest question
first. Ordering needed a question-onset timestamp, which nothing tracked, so
taskStatus now stamps one per agent and exposes getTaskQuestionSince(). Tasks
stay in their project group as well — the tray is a shortcut, not a move, which
keeps drag-reorder indices intact. Clicking a row opens the task and restores
its last focused panel so the question can be answered in one click.

The tray is a mode (sidebarNeedsInputFirst, persisted, on by default) with an
in-tray dismiss and a Settings → Behavior checkbox.
Running the app showed the wrapped names cut mid-glyph. Two separate caps
were doing it: rows are flex children of the scrolling task column, so they
were shrunk below their content height, and the task-item appear animation
tweens max-height to a fixed 40px — shorter than any two-line row, and the
class stays on the element.

Rows now set flex-shrink: 0 and the keyframe animates only opacity and
transform. Also moves the tray's elapsed-time label down to the project line
so the name gets the full row width.
Address PR johannesjo#250 review.

Tray membership came from `getTaskAttentionState(taskId) === 'needs_input'`,
but that function returns `error` or `review` before it ever checks question
state. In a multi-agent task one agent can exit non-zero — or the task can be
flagged for review — while another agent is still blocked on an answer, and the
task silently dropped out of "Waiting for you" even though a human answer was
still owed.

`getTaskOpenQuestion(taskId)` now reports the newest open question directly as
`{ agentId, since }`, independent of the prioritized display status, and
replaces `getTaskQuestionSince`. `computeNeedsInputTasks` keys off it. The
asking set and the onset map are separate structures kept in step by their sole
writer, so the one place they meet is the one place that tolerates a skew —
`since` is a plain `number` everywhere downstream.

Clicking a tray row also restored the *previously* focused panel, so a question
from a non-selected agent tab or from a shell landed somewhere else and needed a
second click. Each entry now carries the asking panel — `ai-terminal:<agentId>`
or `shell:<index>` — and `jumpToWaitingTask` focuses that, re-validating it
against the task's current agents at click time so a stale panel degrades to the
last focused one instead of focusing nothing. Since `setTaskFocusedPanel`
selects the agent behind an ai-terminal panel, a background tab is brought to
the front; the three writes are batched so `setActiveTask`'s intermediate
selection of the *old* agent never renders. `jumpToWaitingTask` moved from
Sidebar.tsx into the store so that ordering is testable.

Two display consequences of question-driven membership, fixed here:

- The tray's status dot was hardcoded to `needs_input`. A row can now also be
  errored or awaiting review, so it takes the task's real attention state and no
  longer contradicts the same task's dot in the list below. The tray header
  carries the "waiting for you" message on its own.
- `getDotTooltip` had no `review` branch, so a review-flagged task with an
  active agent showed a purple dot reading "Busy". Added.

The `shell:<index>` convention was hand-rolled at six sites and `ai-terminal:`
at two. `shellPanelId`/`shellPanelIndex`/`isShellPanel` join `aiTerminalPanelId`
in focused-panel.ts and every site now goes through them, so the format is
built and parsed in one place. The parser takes digits only — `Number('')` is 0
and `Number('1e2')` is 100, so a loose parse would let `shell:` validate as a
real panel.

Two comments corrected while here: the collapsed sweep in
`computeNeedsInputTasks` and the matching `uncollapseTask` guard are inert —
`collapseTask` kills every agent and clears its question state — so they no
longer claim minimized tasks show up in the tray.

Known gap, deliberately not converted: `desktopNotifications.ts` and
`remoteStatusSync.ts` still classify `needs_input` from the attention state and
so inherit the same masking. Changing when a desktop notification fires is a
product decision rather than mechanical follow-through, so it is documented on
`getTaskOpenQuestion` and left to the author.

Tests: `getTaskOpenQuestion` covered against a mixed error/question task, a
review-flagged task, several agents asking at once, a shell question, and a
stopped agent; the tray covered for panel resolution and an unplaceable agent;
`jumpToWaitingTask` for the ai-terminal and shell targets, the last-focused
fallback, select-then-focus ordering, the uncollapse guard, and stale or
malformed panels; plus shell-panel-id round-trip and rejection cases, and
`getDotTooltip('busy', 'review')`.

Verified in the running app on a scratch profile with stub agents, not just in
tests: a task whose first agent exits 1 while a second asks stays listed, its
dot reads red in both the tray and the list, and clicking the row with the
failed agent's tab in front switches to the asking agent's tab.

Full suite 1710 passed / 24 skipped.
@oluies
oluies force-pushed the task/projectlist-29a448 branch from a9033ea to 3b289e4 Compare August 4, 2026 17:10
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants