Skip to content

Add project-work sidebar and worktree polish#269

Merged
IgorWarzocha merged 50 commits into
devfrom
automations-worktrees-sidebar
May 25, 2026
Merged

Add project-work sidebar and worktree polish#269
IgorWarzocha merged 50 commits into
devfrom
automations-worktrees-sidebar

Conversation

@IgorWarzocha
Copy link
Copy Markdown
Owner

Summary

  • replaces the old project sidebar with the project-work sidebar: visible project scope, branch-grouped sessions, Past sessions, project menus, and branch actions
  • splits the sidebar implementation into feature modules and ownership-based CSS instead of one giant TSX/CSS surface
  • adds safer archive/delete flows, restored archived code-session visibility, safe branch pruning, and fixed project-work search/scope regressions from review
  • adds worktree layout defaults plus the worktree docs/blog/poll-worker pieces included in this branch
  • polishes the new sidebar popovers, alignment, shadows, and simplified row hover states

Validation

  • bun run ai:check
  • review loop findings addressed across the sidebar, session deletion/archive, branch prune, poll worker, and project menu flows
  • non-destructive UI smoke pass covered project menus, confirmations, Automations nav state, project-work search, branch prune confirmation, and safe branch prune behavior

Changelog

  • Updated docs/changelog.md under 0.1.66.

@reactreview
Copy link
Copy Markdown

reactreview Bot commented May 25, 2026

69 score

Copy as prompt
Check if these React Review issues are valid. If so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.

Run this before and after your changes to verify the result:
npx react-doctor@latest --verbose --diff

Do not modify the react-doctor configuration unless explicitly asked.
Fix the underlying code issues instead of changing or suppressing the rules.

React Review found 0 errors and 30 warnings. This PR leaves the React health score unchanged.

<file name="src/app/components/sidebar/project-work/project-scope-selector.tsx">

<violation number="1" location="src/app/components/sidebar/project-work/project-scope-selector.tsx:140">
Severity: Warning

w-7 h-7 → use the shorthand size-7 (Tailwind v3.4+)

Collapse `w-N h-N` to `size-N` (Tailwind v3.4+) when both axes match

Rule: `design-no-redundant-size-axes`
</violation>

</file>

<file name="src/app/components/sidebar/project-work/branch-thread-groups.tsx">

<violation number="1" location="src/app/components/sidebar/project-work/branch-thread-groups.tsx:126">
Severity: Warning

Declare only one React component per file. Found extra component: ProjectExpandedBranchGroups.

Move secondary components into their own files.

Rule: `no-multi-comp`
</violation>

<violation number="2" location="src/app/components/sidebar/project-work/branch-thread-groups.tsx:198">
Severity: Warning

Declare only one React component per file. Found extra component: ProjectCompactBranchGroups.

Move secondary components into their own files.

Rule: `no-multi-comp`
</violation>

</file>

<file name="src/app/components/sidebar/project-work/project-work-actions-menu.tsx">

<violation number="1" location="src/app/components/sidebar/project-work/project-work-actions-menu.tsx:2">
Severity: Warning

forwardRef is no longer needed on React 19+ — refs are regular props on function components; remove forwardRef and pass ref directly

Pass `ref` as a regular prop on function components — `forwardRef` is no longer needed in React 19+. Replace `useContext(X)` with `use(X)` for branch-aware context reads. Only enabled on projects detected as React 19+.

Rule: `no-react19-deprecated-apis`
</violation>

</file>

<file name="src/app/components/sidebar/project-work/project-work-section.tsx">

<violation number="1" location="src/app/components/sidebar/project-work/project-work-section.tsx:107">
Severity: Warning

useEffect reacts to "storedVisibleProjectIds" which is set by another useEffect — chains of effects add an extra render per link and become rigid as code evolves. Compute what you can during render and write all related state inside the event handler that originally fires the chain

Compute as much as possible during render (e.g. `const isGameOver = round > 5`) and write all related state inside the event handler that originally fires the chain. Each effect link adds an extra render and makes the code rigid as requirements evolve

Rule: `no-effect-chain`
</violation>

<violation number="2" location="src/app/components/sidebar/project-work/project-work-section.tsx:74">
Severity: Warning

Component "ProjectWorkSection" has 7 useState calls — consider useReducer for related state

Group related state: `const [state, dispatch] = useReducer(reducer, { field1, field2, ... })`

Rule: `prefer-useReducer`
</violation>

<violation number="3" location="src/app/components/sidebar/project-work/project-work-section.tsx:104">
Severity: Warning

Avoid storing derived state. Compute "storedVisibleProjectIds" directly during render, optionally with `useMemo` if it's expensive.

Compute derived state inline during render (or with useMemo if expensive) instead of mirroring it into useState via a useEffect. See https://react.dev/learn/you-might-not-need-an-effect#updating-state-based-on-props-or-state

Rule: `no-derived-state`
</violation>

<violation number="4" location="src/app/components/sidebar/project-work/project-work-section.tsx:109">
Severity: Warning

Avoid chaining state changes. When possible, update all relevant state simultaneously.

Update all related state simultaneously inside the event handler that originally fires, instead of reacting to one state update in a useEffect that writes another state. See https://react.dev/learn/you-might-not-need-an-effect#chains-of-computations

Rule: `no-chain-state-updates`
</violation>

<violation number="5" location="src/app/components/sidebar/project-work/project-work-section.tsx:109">
Severity: Warning

Avoid storing derived state. Compute "storedVisibleProjectIds" directly during render, optionally with `useMemo` if it's expensive.

Compute derived state inline during render (or with useMemo if expensive) instead of mirroring it into useState via a useEffect. See https://react.dev/learn/you-might-not-need-an-effect#updating-state-based-on-props-or-state

Rule: `no-derived-state`
</violation>

<violation number="6" location="src/app/components/sidebar/project-work/project-work-section.tsx:163">
Severity: Warning

useEffect calls prop callback "onLoadProjectThreads" with local state in deps — this is the "lift state via callback" anti-pattern; lift state into a shared Provider so both sides read the same source

Lift the shared state into a Provider so both sides read the same source — no useEffect-driven sync needed

Rule: `no-prop-callback-in-effect`
</violation>

</file>

<file name="src/app/components/sidebar/project-work/new-thread-menu.tsx">

<violation number="1" location="src/app/components/sidebar/project-work/new-thread-menu.tsx:32">
Severity: Warning

Component "NewThreadMenu" has 5 useState calls — consider useReducer for related state

Group related state: `const [state, dispatch] = useReducer(reducer, { field1, field2, ... })`

Rule: `prefer-useReducer`
</violation>

<violation number="2" location="src/app/components/sidebar/project-work/new-thread-menu.tsx:61">
Severity: Warning

4 setState calls in a single useEffect — consider using useReducer or deriving state

Combine into useReducer: `const [state, dispatch] = useReducer(reducer, initialState)`

Rule: `no-cascading-set-state`
</violation>

<violation number="3" location="src/app/components/sidebar/project-work/new-thread-menu.tsx:116">
Severity: Warning

w-7 h-7 → use the shorthand size-7 (Tailwind v3.4+)

Collapse `w-N h-N` to `size-N` (Tailwind v3.4+) when both axes match

Rule: `design-no-redundant-size-axes`
</violation>

</file>

<file name="src/app/components/sidebar/project-work/project-work-block.tsx">

<violation number="1" location="src/app/components/sidebar/project-work/project-work-block.tsx:39">
Severity: Warning

Component "ProjectWorkBlockHeader" has 5 useState calls — consider useReducer for related state

Group related state: `const [state, dispatch] = useReducer(reducer, { field1, field2, ... })`

Rule: `prefer-useReducer`
</violation>

<violation number="2" location="src/app/components/sidebar/project-work/project-work-block.tsx:41">
Severity: Warning

useState initialized from prop "project" — if this value should stay in sync with the prop, derive it during render instead

Remove useState and compute the value inline: `const value = transform(propName)`

Rule: `no-derived-useState`
</violation>

</file>

Showing 15 of 30 issues.

Reviewed by reactreview for commit e0806aa. Configure here.

@IgorWarzocha
Copy link
Copy Markdown
Owner Author

@codex please review this PR and give me 10-20 issues if any. Categorize findings as required, recommended, or optional.

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: d8bcf1c719

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +93 to +96
if (appliedInitialEmptyScopeRef.current || initialVisibleProjectIds === undefined) return
appliedInitialEmptyScopeRef.current = true
if (initialVisibleProjectIds === null || initialVisibleProjectIds.length > 0) return
onShowView('landing')
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Skip empty-scope redirect in project target mode

This effect forces onShowView('landing') whenever the persisted scope is [], but ProjectWorkSection is also reused for extension/skill project-target selection. If a user previously saved an empty scope, opening Extensions/Skills immediately gets redirected away before they can choose a target project, which blocks those workflows until scope state is manually changed elsewhere.

Useful? React with 👍 / 👎.

Comment thread src/app/common/confirm-popover.tsx Outdated
Comment on lines +24 to +25
confirmLabel: _confirmLabel = 'Yes',
cancelLabel: _cancelLabel = 'Cancel',
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve confirm/cancel labels instead of discarding them

confirmLabel and cancelLabel are explicitly renamed to unused variables, so callers can no longer provide action-specific text (for example, “Delete”). This removes contextual confirmation wording across existing call sites and makes destructive confirmations less clear.

Useful? React with 👍 / 👎.

Comment on lines 59 to 60
<button
type="button"
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Add accessible names to icon-only confirm popover buttons

Both controls are icon-only buttons without an accessible name (aria-label/text), so assistive tech announces unlabeled buttons. This is an accessibility regression for all confirmations rendered by this component.

Useful? React with 👍 / 👎.

Comment on lines +46 to +49
onDelete={() =>
onAction('thread.archive', {
projectId: project.id,
threadId: thread.id,
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Align delete affordance with actual thread action

This row wires the ThreadRow “Delete session” affordance to thread.archive, which only archives/hides the thread instead of deleting persisted session data. Users who invoke delete from active project rows will believe data was removed when it was only archived.

Useful? React with 👍 / 👎.

Comment on lines +48 to +56
function getAssistantUsage(entry: unknown) {
const message =
typeof entry === 'object' && entry !== null && 'message' in entry ? entry.message : null
if (
typeof message !== 'object' ||
message === null ||
!('role' in message) ||
message.role !== 'assistant' ||
!('usage' in message) ||
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Gate stored usage parsing to message entries

The new deletion-time usage parser counts any object with message.role === 'assistant', but the existing usage summarizer only counts entries where type === 'message'. If session logs contain non-message records with embedded message payloads, deleted-session totals become inflated versus live/archived summaries.

Useful? React with 👍 / 👎.

Comment on lines +178 to +179
CREATE TABLE IF NOT EXISTS project_usage_totals (
cwd TEXT PRIMARY KEY,
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Add project lifecycle linkage for usage-totals rows

project_usage_totals is keyed by cwd but has no foreign key to projects and no cascade behavior. After project deletion, totals rows can remain orphaned and will be re-applied if the same path is added again, producing stale/incorrect lifetime usage for the new project entry.

Useful? React with 👍 / 👎.

@IgorWarzocha
Copy link
Copy Markdown
Owner Author

@codex please review this PR and give me 10-20 issues if any. Categorize findings as required, recommended, or optional.

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: f8b7548333

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +171 to +172
projectTargetMode={true}
projectScopeLockActive={props.projectScopeLockActive}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge [required] Gate project-target mode by scope lock

Do not force projectTargetMode on every Extensions/Skills view render; this bypasses projectScopeLockActive and keeps the sidebar in install-target selection mode even when scope locking is not active. That removes normal sidebar navigation for those views and changes behavior from conditional lock mode to always-on target mode.

Useful? React with 👍 / 👎.

Comment thread desktop/pi-threads/thread-actions.ts Outdated
Comment on lines +147 to +151
await storeUsageBeforeDelete(threadId, sessionPath)
} catch (error) {
if (
typeof error !== 'object' ||
error === null ||
!('code' in error) ||
error.code !== 'ENOENT'
) {
throw error
}
if (!isMissingFileError(error)) throw error
}
}

if (sessionPath) {
await unlinkSessionFile(sessionPath)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge [required] Make usage aggregation atomic with thread deletion

Avoid writing usage totals before the delete is guaranteed to succeed. If storeUsageBeforeDelete() succeeds but unlinkSessionFile() or deleteThreadRecord() fails, a retry will add the same session usage again and permanently overcount project usage/cost totals.

Useful? React with 👍 / 👎.

Comment on lines +34 to +36
const error = result?.result?.error
if (typeof error === 'string' && error.includes('Worktree is dirty')) onBlocked()
})
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge [recommended] Preserve branch-switch errors for non-dirty failures

This handler only reacts to the 'Worktree is dirty' substring and ignores other switch errors. Since workspace.switch-branch is excluded from global error surfacing, failures like checkout conflicts or missing refs become effectively silent to users.

Useful? React with 👍 / 👎.

Comment on lines +90 to +97
<button
type="button"
className="sidebar-project-work-project-block-heading"
onClick={() => onFocusProject(project.id)}
>
{project.repoOriginUrl ? <GitHubInvertocatMark size={13} /> : <FolderCode size={13} />}
{editingName ? (
<ProjectRenameField
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge [recommended] Remove input nesting inside clickable button

Rendering ProjectRenameField inside a <button> creates invalid interactive nesting and can cause keyboard/click side effects while renaming (for example, triggering the parent button action unexpectedly). Use a non-button container while edit mode is active.

Useful? React with 👍 / 👎.

Comment on lines +134 to +138
onToggleExpanded={() =>
onSetExpandedProjectIds((current) => ({
...current,
[`project:${project.id}`]: expanded,
}))
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge [recommended] Persist project expand/collapse state changes

Project expansion is tracked only in local React state here and never routed through persisted project collapse actions, so expand/collapse preferences are lost after shell reload. The backend still carries persistent collapsed project state, so this creates a UX regression in state durability.

Useful? React with 👍 / 👎.

const defaultCollapsed = !(group.current || group.id === selectedGroupId)
const collapsed = normalizedSearchQuery
? false
: (collapsedBranchIds[group.id] ?? defaultCollapsed)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge [recommended] Namespace single-project collapse keys by project

Using group.id directly as the collapse key leaks collapse state across projects that share branch names (e.g., main, release). Switching projects can inherit the wrong expanded/collapsed state because keys are not scoped by project id.

Useful? React with 👍 / 👎.

group={group}
project={project}
onAction={onAction}
onBlocked={() => onSetSwitchErrorBranchId(branchActionKey)}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3 Badge [optional] Reset stale dirty-branch blocked state

Once switchErrorBranchId is set for a branch, this branch remains in blocked-tooltip mode without a reset path in this flow. After users clean the worktree, the stale blocked state can keep showing misleading dirty-state affordances.

Useful? React with 👍 / 👎.

Comment on lines +24 to 25
{ value: 'project', label: `Project ${counts.project}` },
{ value: 'chat', label: `Chat ${counts.chat}` },
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3 Badge [optional] Disable project scope when no project is selected

The project scope option is always enabled even when projectScopeAvailable is false. That allows users to enter a scope that immediately errors on install (Select a project first) instead of preventing the invalid state up front.

Useful? React with 👍 / 👎.

Comment on lines +25 to 26
{ value: 'project', label: `Project ${counts.project}` },
{ value: 'chat', label: `Chat ${counts.chat}` },
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3 Badge [optional] Disable project scope when no skills target exists

As in Extensions, the Skills scope switch keeps project selectable without an active project path. This permits an invalid scope selection and pushes failure handling downstream instead of gating in the control.

Useful? React with 👍 / 👎.

Comment thread workers/polls/src/index.ts Outdated

const recentEvents = await env.DB.prepare(
"SELECT COUNT(*) AS count FROM poll_vote_events WHERE voter_hash = ? AND created_at > datetime('now', '-30 seconds')",
"SELECT COUNT(*) AS count FROM poll_vote_events WHERE voter_hash = ? AND julianday(created_at) > julianday('now', '-30 seconds')",
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3 Badge [optional] Keep rate-limit predicate index-friendly

Wrapping created_at with julianday() makes the predicate non-sargable against poll_vote_events_voter_created_idx (voter_hash, created_at), so the hot-path throttle query can degrade into per-voter scans as event history grows.

Useful? React with 👍 / 👎.

@IgorWarzocha IgorWarzocha merged commit 14cf533 into dev May 25, 2026
1 check passed
@IgorWarzocha IgorWarzocha deleted the automations-worktrees-sidebar branch May 25, 2026 02:26
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.

1 participant