Skip to content

Fixes To Buncha Things#211

Merged
arul28 merged 1 commit intomainfrom
ade/fixes-to-buncha-things-17615678
Apr 27, 2026
Merged

Fixes To Buncha Things#211
arul28 merged 1 commit intomainfrom
ade/fixes-to-buncha-things-17615678

Conversation

@arul28
Copy link
Copy Markdown
Owner

@arul28 arul28 commented Apr 27, 2026

Summary

Describe the change.

What Changed

Key files and behaviors.

Validation

How you tested.

Risks

Anything to watch.

Summary by CodeRabbit

  • New Features

    • Added lane color customization with visual accent indicators throughout the app
    • GitHub fine-grained token integration now verifies repository access
    • Work session tiling presets for flexible layout control
    • Enhanced automations editor with schema-driven action configuration
  • Bug Fixes

    • Fine-grained GitHub tokens now properly detect repository access failures
    • Lane color uniqueness constraint prevents conflicts
    • GitHub token status now refreshes on demand
  • Improvements

    • Richer contextual tooltips across the app
    • Enhanced GitHub connection status messaging

Greptile Summary

This PR delivers four main features: fine-grained GitHub token support with a live repo-access probe (GET /repos/{owner}/{name}), lane color customization with a uniqueness constraint enforced in SQLite, tiling layout presets ("rows"/"columns"/"auto") for multi-session work areas, and a schema-driven automation action editor. The GitHub banner now surfaces the real failure reason rather than showing a generic "not connected" state, and a new IPC.githubStatusChanged push event keeps all windows in sync immediately after a token change.

Confidence Score: 5/5

PR is safe to merge; all findings are P2 style suggestions only.

No P0 or P1 issues found. Both flagged items are P2: a slightly misleading banner fallback message for auth-failure vs. scope-failure cases, and a case-sensitive idempotency guard in the lane color update path that could silently normalise hex casing. Well-tested with good coverage across backend, service, and UI layers.

apps/desktop/src/renderer/components/app/AppShell.tsx (banner message accuracy), apps/desktop/src/main/services/lanes/laneService.ts (color guard casing)

Important Files Changed

Filename Overview
apps/desktop/src/main/services/github/githubService.ts Adds probeRepoAccess (GET /repos/{owner}/{name}) for fine-grained token validation, computeConnected logic, forceRefresh support, and new connected/repoAccessOk/repoAccessError fields on GitHubStatus.
apps/desktop/src/main/services/lanes/laneService.ts Adds color uniqueness constraint via SQL probe before updateAppearance. The idempotency guard uses a case-sensitive JS comparison which could pass through a case-normalisation update silently (P2 only).
apps/desktop/src/main/services/ipc/registerIpc.ts Adds broadcastGithubStatus helper that fans out IPC.githubStatusChanged to all windows on set/clear token; wires forceRefresh through to getStatus.
apps/desktop/src/renderer/components/app/AppShell.tsx Banner condition changed from !tokenStored to !connected; GithubBanner component extracts message logic; onStatusChanged subscription keeps banner in sync with Settings. Minor: fallback banner message is misleading for auth-failure case (P2).
apps/desktop/src/renderer/components/settings/GitHubSection.tsx Reworked connection status logic: separates tokenAuthenticated from isConnected, shows red error on save when token fails probe, adds repoProbeFailed diagnostic block, and uses forceRefresh on manual refresh.
apps/desktop/src/renderer/components/lanes/laneColorPalette.ts New palette module: 12-color LANE_COLOR_PALETTE, getLaneAccent fallback, colorsInUse, nextAvailableColor, laneColorName helpers. Clean, well-typed, and tested.
apps/desktop/src/renderer/components/terminals/WorkViewArea.tsx Adds tilingPreset state, ArrangeMenu component, and key-based remount of PaneTilingLayout on preset change. SmartTooltip wrapping throughout session tabs and groups.

Sequence Diagram

sequenceDiagram
    participant UI as Settings / AppShell
    participant Preload as Preload (IPC)
    participant Main as Main Process
    participant GitHub as GitHub API

    UI->>Preload: getStatus({ forceRefresh: true })
    Preload->>Main: ipcMain githubGetStatus
    Main->>GitHub: GET /user (validateToken)
    GitHub-->>Main: userLogin, scopes, tokenType
    alt fine-grained token + repo present
        Main->>GitHub: GET /repos/{owner}/{name} (probeRepoAccess)
        GitHub-->>Main: 200 OK or 403/404
    end
    Main->>Main: computeConnected(tokenType, scopes, repoAccessOk)
    Main-->>Preload: GitHubStatus { connected, repoAccessOk, ... }
    Preload-->>UI: status

    Note over UI,Main: On setToken / clearToken
    Main->>Main: broadcastGithubStatus(status)
    Main-->>Preload: IPC.githubStatusChanged event
    Preload-->>UI: onStatusChanged callback → setGithubStatus
Loading

Fix All in Claude Code

Prompt To Fix All With AI
This is a comment left during a code review.
Path: apps/desktop/src/renderer/components/app/AppShell.tsx
Line: 144-163

Comment:
**Misleading banner message when authentication itself fails**

`describeGithubBanner` has three cases: `!tokenStored`, fine-grained probe failure, and a fallback. The fallback fires whenever `connected=false` with a stored token — including when `validateToken` threw (invalid token, network error, decryption failure), in which case `userLogin` is `null`. A user who entered an outright-wrong token will see "GitHub token saved, but it does not have the required permissions" rather than something like "token could not be authenticated". Consider adding a `!status.userLogin` guard before the scope-failure message:

```ts
if (!status.userLogin) {
  return { message: "GitHub token saved, but authentication failed. ", linkLabel: "Fix GitHub token" };
}
```

How can I resolve this? If you propose a fix, please make it concise.

---

This is a comment left during a code review.
Path: apps/desktop/src/main/services/lanes/laneService.ts
Line: 2882

Comment:
**Case-sensitive idempotency guard may normalise stored color casing**

`normalizedColor !== lane.color` is a strict JS string comparison. If the stored `lane.color` is `"#A78BFA"` (uppercase, e.g. from a legacy client) and the user re-selects the palette entry whose `hex` is `"#a78bfa"`, the guard evaluates to `true` and the SQL conflict probe runs. Because the current lane is excluded by `id != ?`, no conflict is found, the `UPDATE` proceeds and persists `"#a78bfa"` — silently normalising the casing. A `toLowerCase()` comparison would be more precise:

```ts
if (normalizedColor && normalizedColor.toLowerCase() !== (lane.color ?? "").toLowerCase()) {
```

How can I resolve this? If you propose a fix, please make it concise.

Reviews (2): Last reviewed commit: "fixes to a bunch of things across lanes,..." | Re-trigger Greptile

@vercel
Copy link
Copy Markdown

vercel Bot commented Apr 27, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
ade Ignored Ignored Preview Apr 27, 2026 4:44am

@arul28
Copy link
Copy Markdown
Owner Author

arul28 commented Apr 27, 2026

@copilot review but do not make fixes

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 27, 2026

📝 Walkthrough

Walkthrough

The PR adds GitHub repository access verification with a new connected status field, implements a lane color management system with uniqueness constraints across platforms, and enhances multiple UI components (chat, automations, terminals, PRs) to render and select lane colors. It includes repo-level connectivity checks for fine-grained tokens, IPC broadcast updates for GitHub status changes, work session tiling presets, and expanded tooltip/accessibility improvements throughout the desktop app.

Changes

Cohort / File(s) Summary
GitHub Service & Status
apps/desktop/src/main/services/github/githubService.ts, githubService.test.ts
Adds repo-level GET /repos/{owner}/{repo} probing for token connectivity verification; exposes repoAccessOk, repoAccessError, and connected fields; supports optional forceRefresh parameter; classic tokens use header scope evaluation, fine-grained tokens require successful repo probe.
GitHub IPC & Type Updates
apps/desktop/src/main/services/ipc/registerIpc.ts, apps/desktop/src/shared/ipc.ts, apps/desktop/src/shared/types/git.ts
Adds githubStatusChanged broadcast channel; extends githubGetStatus to accept forceRefresh flag; emits github.repo_probe_failed warnings; augments GitHubStatus type with connected, repoAccessOk, repoAccessError fields.
GitHub Preload & Browser API
apps/desktop/src/preload/global.d.ts, apps/desktop/src/preload/preload.ts, apps/desktop/src/renderer/browserMock.ts
Updates getStatus signature to accept optional { forceRefresh?: boolean }; adds onStatusChanged subscription handler; expands mock GitHubStatus shape with full token/repo state; updates Settings UI to distinguish tokenAuthenticated from isConnected and report repo probe failures.
Lane Color System (Core)
apps/desktop/src/renderer/components/lanes/laneColorPalette.ts, laneUtils.test.ts
Defines 12-color palette with getLaneAccent, colorsInUse, nextAvailableColor, laneColorName utilities; computes case-insensitive color uniqueness; provides fallback index-based colors for legacy lanes.
Lane Color UI Components
apps/desktop/src/renderer/components/lanes/LaneColorPicker.tsx, LaneAccentDot.tsx, CreateLaneDialog.tsx, ManageLaneDialog.tsx, LaneContextMenu.tsx
Implements color swatch picker with conflict detection; adds inline lane accent dot component; extends create/manage dialogs with color selection; adds context menu color controls with update/error handling.
Lane Visual Integration (Desktop)
apps/desktop/src/renderer/components/lanes/LanesPage.tsx, LaneStackPane.tsx, apps/desktop/src/renderer/components/missions/MissionHeader.tsx, ManageMissionDialog.tsx, apps/desktop/src/renderer/components/prs/tabs/GitHubTab.tsx, QueueTab.tsx
Renders lane accent dots conditionally across lane stack, mission headers, PR tabs, queue views; derives color from store for linked/active lanes; updates badge/label styling to reflect lane color.
Lane Service Constraint
apps/desktop/src/main/services/lanes/laneService.ts, laneService.test.ts
Enforces color uniqueness across active lanes; ignores archived lanes and idempotent updates; throws descriptive errors when conflicts detected.
Chat Composer & Pane
apps/desktop/src/renderer/components/chat/AgentChatComposer.tsx, AgentChatComposer.test.tsx
Replaces title attributes with SmartTooltip for buttons (queued message actions, model selector, attachment, send controls); enhances paste-to-attachments to include clipboard items; moves parallel-mode toggle to right toolbar.
Chat Pane & Toolbar
apps/desktop/src/renderer/components/chat/AgentChatPane.tsx, AgentChatPane.submit.test.tsx, ChatGitToolbar.tsx, apps/desktop/src/renderer/components/app/AppShell.tsx
Updates lane selector pill to use LaneAccentDot with lane color; derives laneColor from store; changes GitHub banner visibility from tokenStored to connected; adds onStatusChanged listener for real-time status updates; refactors PR toast meta rendering with lane-specific chip colors.
Automation Action Editor & Schemas
apps/desktop/src/renderer/components/automations/AdeActionEditor.tsx, adeActionSchemas.ts
Replaces JSON/dropdown UI with schema-driven ActionPicker combobox and ActionParamsEditor for structured input; adds PlaceholderRow and JsonArgsEditor with parse-error display; exports ADE_ACTION_SCHEMA_COUNT; defines 2100+ lines of curated action metadata.
Automation UI Refactoring
apps/desktop/src/renderer/components/automations/ActionList.tsx, ActionRow.tsx, shared.ts
Splits action UI into AddStepBar dropdown and EmptyPicker; refactors ActionRow with inset box-shadow accent, badge-based step indicator, and updated control styling; removes CARD_SHADOW_STYLE and getAutomationsBridge exports.
Automation Rule Editor
apps/desktop/src/renderer/components/automations/components/RuleEditorPanel.tsx
Converts single-column card layout to responsive two-column grid; removes "Advanced" collapsible section; reorganizes controls into Identity/Trigger/Execution/Brains/Limits (left) and Workflow steps (right); adds TRIGGER_FAMILIES metadata and icon/accent button grid; introduces Section, Toggle, SmallLabel, AccentBadge helpers.
Terminal & Work Session UI
apps/desktop/src/renderer/components/terminals/WorkViewArea.tsx, workSessionTiling.ts, workSessionTiling.test.ts
Adds TilingPreset ("auto"/"rows"/"columns") support to buildWorkSessionTilingTree; renders ArrangeMenu for preset selection; persists layout choices; wraps mode/session buttons with SmartTooltip descriptions.
Terminal Session Cards
apps/desktop/src/renderer/components/terminals/SessionCard.tsx, SessionListPane.tsx
Derives laneAccent from lane and applies it to card border/text colors; StickyGroupHeader accepts optional accentColor for lane-based group theming.
Shared & UI Utilities
apps/desktop/src/renderer/components/ui/SmartTooltip.tsx, apps/desktop/src/renderer/components/shared/ProviderModelSelector.tsx
Extends SmartTooltip with wrapperClassName/wrapperStyle props; updates model selector and reasoning effort controls to use tooltips instead of title.
iOS Lane Color System
apps/ios/ADE/Views/Lanes/LaneColorPalette.swift, LaneColorSwatchPicker.swift, LaneCreateSheet.swift, LaneManageSheet.swift, LaneListViewParts.swift, LaneTreeView.swift, LaneComponents.swift
Introduces iOS LaneColorPalette with hex parsing, color mapping, and availability checking; adds LaneColorSwatchPicker swatch grid UI; extends lane create/manage sheets with color selection; updates lane list/stack/tree views to render colored accents; integrates updateLaneAppearance async calls.
iOS Work Session UI
apps/ios/ADE/Views/Work/WorkRootComponents.swift
Updates WorkSessionRow to render lane accent colors and provider label; replaces icon with colored circle when lane color exists.
Finalize Test Instructions
.claude/commands/finalize.md
Tightens Phase 3e failure procedure to run only specific failing test file (forbids shard re-runs); narrows Phase 3 drift-check remediation to re-run single file instead of full shard.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested labels

desktop, ios

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 11.36% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive The title "Fixes To Buncha Things" is vague and generic, using non-descriptive phrasing that doesn't convey meaningful information about the specific changes. Replace with a more specific title that captures the primary change or main theme, such as "Add lane color customization and GitHub token connectivity improvements" or "Support lane accent colors and enhance GitHub status detection."
✅ Passed checks (3 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch ade/fixes-to-buncha-things-17615678

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Comment thread apps/desktop/src/renderer/components/terminals/WorkViewArea.tsx
Comment thread apps/desktop/src/renderer/components/lanes/LanesPage.tsx
Comment thread apps/desktop/src/main/services/github/githubService.ts
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 20

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
apps/desktop/src/renderer/components/automations/components/RuleEditorPanel.tsx (2)

459-486: ⚠️ Potential issue | 🟠 Major

missionsEnabled is declared as a required prop but never destructured/used.

The destructuring at lines 459–471 drops missionsEnabled, but line 477 still requires it in the prop type. The parent (RulesTab.tsx:645) keeps passing missionsEnabled={missionsEnabled}, so today this just silently ignores the value, but the type still forces every caller to thread it through. This contradicts the AI summary's claim that the parameter was removed and leaves a dead, mandatory contract on the component.

Either fully remove the prop (and stop passing it from RulesTab.tsx) or actually consume it. Recommended cleanup:

♻️ Drop the unused prop from the type
 }: {
   draft: AutomationRuleDraft;
   setDraft: (draft: AutomationRuleDraft) => void;
   lanes: Array<{ id: string; name: string }>;
   suites: TestSuiteDefinition[];
-  missionsEnabled: boolean;
   issues: AutomationDraftIssue[];
   requiredConfirmations: AutomationDraftConfirmationRequirement[];

And in apps/desktop/src/renderer/components/automations/RulesTab.tsx around line 645, drop the now-unused prop:

 <RuleEditorPanel
   draft={draft}
   setDraft={setDraft}
   lanes={lanes.map((lane) => ({ id: lane.id, name: lane.name }))}
   suites={suites}
-  missionsEnabled={missionsEnabled}
   issues={issues}

(Verify missionsEnabled is still needed elsewhere in RulesTab.tsx; if not, remove its computation too.)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@apps/desktop/src/renderer/components/automations/components/RuleEditorPanel.tsx`
around lines 459 - 486, RuleEditorPanel declares missionsEnabled in its prop
type but does not destructure or use it; remove missionsEnabled from the
RuleEditorPanel prop type and its parameter list (the function signature) and
then remove the missionsEnabled={missionsEnabled} prop passed from RulesTab (or
stop computing it there if unused) so the component signature and callers are
consistent; search for RuleEditorPanel and RulesTab to update the component
props and the caller(s) that pass missionsEnabled accordingly.

214-226: ⚠️ Potential issue | 🟡 Minor

Normalize legacy git.pr_* trigger types before saving to prevent UI select mismatches.

When a persisted rule carries a git.pr_* type (valid per AUTOMATION_TRIGGER_TYPES), the renderer maps it to the github family in triggerFamilyForType() (line 217). However, TRIGGER_OPTIONS.github only lists the canonical github.pr_* variants, leaving the select element with no matching option. This causes the browser to silently rewrite the type on the next user interaction.

The issue is that normalizeDraft() in the main process validates but does not normalize legacy trigger types before persisting. A git.pr_* type passes validation and is saved as-is, even though normalizeTriggerType() exists to convert it to github.pr_*. Consider applying the same normalization during save that already happens at runtime in automationService.ts (lines 312–316), so persisted rules always use canonical trigger names and render options match their values.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@apps/desktop/src/renderer/components/automations/components/RuleEditorPanel.tsx`
around lines 214 - 226, Persisted rules may contain legacy git.pr_* trigger
types which map to the github family in triggerFamilyForType but don't match
TRIGGER_OPTIONS.github, causing the select to lose its option; fix this by
applying normalizeTriggerType when saving/normalizing drafts (the same
normalization used in automationService.normalizeDraft flow) so that
normalizeDraft (or the save path) converts git.pr_* to github.pr_* before
validation/persisting; update the save/normalize path that calls normalizeDraft
to call normalizeTriggerType on rule.trigger.type (or run the existing
normalizeTriggerType utility) so persisted rules always use canonical
github.pr_* names and the UI select matches.
apps/desktop/src/renderer/components/terminals/SessionListPane.tsx (1)

49-76: ⚠️ Potential issue | 🟡 Minor

Lane accent applied as raw color may hurt label readability and breaks header hover.

Three concerns with the new accentColor styling (all stem from the same wiring):

  1. Label contrast (line 73): style={{ color: accentColor }} paints the label in the lane's raw hex. With dark or low-luminance lane colors on the dark sidebar surface, the label can become hard to read. The icon path already keeps a sane fallback (var(--color-muted-fg)), but the label gets the raw color with no luminance handling. Consider mixing toward a foreground-readable tint, e.g. color-mix(in srgb, ${accentColor} 70%, var(--color-fg)), or only tinting the icon/border and leaving the label at text-fg/90.
  2. Hover background suppressed (lines 56–58): style.background = accentBg is always set (even in the no-accent fallback), so the hover:bg-white/[0.04] class on the same element has no effect — inline styles win over the :hover class. If hover feedback is desired, drive the background from a CSS variable + class, or move the hover state into JS (onMouseEnter/Leave) to override the inline value.
  3. 2px alignment shift (line 60): The left border is only added when accentColor is truthy, so in the by-lane view, lanes with lane.color set are indented 2px further than lanes without a color and than the orphan-lane fallback at lines 354–371. Either always reserve 2px (transparent border when no accent) or pass through an accent for the orphan rows so groups line up.
♻️ Suggested adjustment
-  const accentBg = accentColor
-    ? `color-mix(in srgb, ${accentColor} 8%, transparent)`
-    : "rgba(255, 255, 255, 0.02)";
+  const accentBg = accentColor
+    ? `color-mix(in srgb, ${accentColor} 8%, transparent)`
+    : "transparent";
   return (
     <div className="mt-0.5 first:mt-0">
       <button
         type="button"
         className="sticky top-0 z-10 flex w-full items-center gap-1.5 rounded-md px-2 py-1.5 text-left transition-colors backdrop-blur-xl cursor-pointer select-none hover:bg-white/[0.04]"
         style={{
           background: accentBg,
           borderBottom: "1px solid rgba(255, 255, 255, 0.04)",
-          borderLeft: accentColor ? `2px solid ${accentColor}` : undefined,
+          borderLeft: `2px solid ${accentColor ?? "transparent"}`,
         }}
         ...
       >
         ...
-        <span
-          className="min-w-0 flex-1 truncate text-[11px] font-semibold text-fg/90"
-          style={accentColor ? { color: accentColor } : undefined}
-        >
+        <span
+          className="min-w-0 flex-1 truncate text-[11px] font-semibold text-fg/90"
+          style={
+            accentColor
+              ? { color: `color-mix(in srgb, ${accentColor} 70%, var(--color-fg))` }
+              : undefined
+          }
+        >
           {label}
         </span>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/desktop/src/renderer/components/terminals/SessionListPane.tsx` around
lines 49 - 76, The current accent wiring in SessionListPane causes readability
and hover issues: change the label span so it doesn't use the raw accentColor
(replace style={{ color: accentColor }} with a readable mix, e.g. color-mix(in
srgb, ${accentColor} 70%, var(--color-fg)) or keep text-fg/90 and only tint
icon/border), stop overriding the :hover class by removing the always-set inline
background when no accent (drive background via a CSS variable/class or only set
style.background when accentColor is present), and ensure the left border
reservation is consistent by always setting borderLeft (use a transparent 2px
border when accentColor is falsy) so rows remain aligned; look for accentBg,
accentColor, the style prop on the container button, the borderLeft assignment,
and the label span to apply these fixes.
🧹 Nitpick comments (6)
apps/desktop/src/renderer/components/automations/ActionList.tsx (1)

190-282: Optional: add aria-haspopup/aria-expanded to the menu trigger.

The trigger button opens a role="menu" popup but doesn't advertise that to assistive tech. Adding aria-haspopup="menu" and aria-expanded={open} would let screen readers announce the toggle state. Same applies to the ActionPicker combobox in AdeActionEditor.tsx.

♻️ Suggested change
       <button
         type="button"
         onClick={() => setOpen((value) => !value)}
+        aria-haspopup="menu"
+        aria-expanded={open}
         className={cn(
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/desktop/src/renderer/components/automations/ActionList.tsx` around lines
190 - 282, The AddStepBar trigger button does not announce the menu state to
assistive tech; update the button in AddStepBar to include aria-haspopup="menu"
and aria-expanded={open} so screen readers know it opens a menu and its current
state, and apply the same pattern to the ActionPicker trigger in
AdeActionEditor.tsx (add aria-haspopup="listbox" or "menu" and aria-expanded
bound to its open state) to ensure proper accessibility semantics.
apps/desktop/src/renderer/components/automations/adeActionSchemas.ts (1)

2100-2102: Optional: index schemas by domain.action for O(1) lookups.

findAdeActionSchema is called per render in AdeActionEditor (and once per registry item inside ActionPicker.matches), each time doing a linear scan over ~200+ entries. Building a Map<string, AdeActionSchema> keyed by ${domain}.${action} once at module load would remove the quadratic factor in the picker's match loop.

♻️ Suggested change
-export function findAdeActionSchema(domain: string, action: string): AdeActionSchema | undefined {
-  return ADE_ACTION_SCHEMAS.find((s) => s.domain === domain && s.action === action);
-}
+const SCHEMA_INDEX: ReadonlyMap<string, AdeActionSchema> = new Map(
+  ADE_ACTION_SCHEMAS.map((s) => [`${s.domain}.${s.action}`, s]),
+);
+
+export function findAdeActionSchema(domain: string, action: string): AdeActionSchema | undefined {
+  return SCHEMA_INDEX.get(`${domain}.${action}`);
+}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/desktop/src/renderer/components/automations/adeActionSchemas.ts` around
lines 2100 - 2102, Replace the O(n) scan in findAdeActionSchema with an indexed
Map built once at module load: create a module-level Map<string,
AdeActionSchema> (key = `${domain}.${action}`) populated from ADE_ACTION_SCHEMAS
when the file initializes, then change findAdeActionSchema(domain, action) to
compute the same key and return map.get(key). Ensure the Map is exported or
referenced in this module only and remove the .find usage so callers (e.g.,
AdeActionEditor and ActionPicker.matches) get O(1) lookups.
apps/desktop/src/main/services/lanes/laneService.test.ts (1)

2307-2326: Add one more idempotency regression test for case-only color differences.

Current idempotent coverage is same-string only. Add a case-variant scenario (same logical hex color, different case) to lock behavior.

Suggested test addition
+  it("treats same-color different-case updates as idempotent for conflict probing", async () => {
+    const { db, service } = await setup();
+    db.run("update lanes set color = ? where id = ?", ["#A78BFA", "lane-parent"]);
+    db.run("update lanes set color = ? where id = ?", ["#a78bfa", "lane-child"]);
+
+    expect(() =>
+      service.updateAppearance({ laneId: "lane-child", color: "#A78BFA", icon: "star" }),
+    ).not.toThrow();
+  });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/desktop/src/main/services/lanes/laneService.test.ts` around lines 2307 -
2326, Add a new test (or extend the existing idempotency test) that covers
case-only hex differences: seed two lanes ("lane-parent" and "lane-child") with
the same logical color but different casing (e.g., db.run set parent to
"#A78BFA" and child to "#a78bfa"), then call service.updateAppearance({ laneId:
"lane-child", color: "#A78BFA", icon: "star" }) and assert it does not throw and
that the stored color/icon for "lane-child" are updated as expected; reference
the existing test harness, the service.updateAppearance call, and the DB
queries/selects used to verify color and icon.
apps/ios/ADE/Views/Lanes/LaneComponents.swift (1)

353-360: Consider computing laneAccent once in LaneStackCard for clarity.

You currently resolve the same palette color twice in the same block; caching it locally would simplify the view and avoid duplicate work.

♻️ Optional cleanup
       HStack(alignment: .center, spacing: 10) {
         LaneStatusIndicator(bucket: snapshot.runtime.bucket, size: 10)
           .adeMatchedGeometry(id: isSelectedTransitionSource ? "lane-icon-\(snapshot.lane.id)" : nil, in: transitionNamespace)

-        if let laneAccent = LaneColorPalette.color(forHex: snapshot.lane.color) {
+        let laneAccent = LaneColorPalette.color(forHex: snapshot.lane.color)
+        if let laneAccent {
           Circle()
             .fill(laneAccent)
             .frame(width: 7, height: 7)
         }
         Text(snapshot.lane.name)
           .font(.subheadline.weight(.semibold))
-          .foregroundStyle(LaneColorPalette.color(forHex: snapshot.lane.color) ?? ADEColor.textPrimary)
+          .foregroundStyle(laneAccent ?? ADEColor.textPrimary)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/ios/ADE/Views/Lanes/LaneComponents.swift` around lines 353 - 360,
Compute the lane accent once in the LaneStackCard view by assigning
LaneColorPalette.color(forHex: snapshot.lane.color) to a local let (e.g.,
laneAccent) and reuse that variable for both the Circle().fill(...) and the
Text.foregroundStyle(...) fallback, replacing the second call to
LaneColorPalette.color(forHex: ...) so you don't resolve the palette twice and
the view is clearer.
apps/desktop/src/main/services/github/githubService.test.ts (1)

564-572: Optional: tighten the missing-token assertion.

This test relies on getStatus() not issuing any HTTP probes when the token is absent, but it doesn't verify that. Adding expect(mockFetch).not.toHaveBeenCalled() would lock in the "no network when unauthenticated" contract and make a regression here visible as a failing test rather than a vague mock state.

♻️ Suggested addition
   it("missing token returns not-connected status with all the new fields populated", async () => {
     stubOriginRemote();
     const status = await makeService().getStatus();

     expect(status.tokenStored).toBe(false);
     expect(status.connected).toBe(false);
     expect(status.repoAccessOk).toBeNull();
     expect(status.repoAccessError).toBeNull();
+    expect(mockFetch).not.toHaveBeenCalled();
   });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/desktop/src/main/services/github/githubService.test.ts` around lines 564
- 572, Tighten the "missing token" test by asserting that no HTTP probes were
made: after calling makeService().getStatus() in the test, add an assertion that
mockFetch (the fetch/mock used by the githubService tests) was not called to
ensure getStatus() does not perform network requests when no token is stored;
locate this in the "missing token returns not-connected status..." test that
uses stubOriginRemote(), makeService(), and getStatus().
apps/desktop/src/renderer/components/prs/tabs/GitHubTab.tsx (1)

174-178: Consider precomputing a laneId→color map once.

This hook performs a linear find on every call. For long PR lists, a memoized map in the parent would reduce repeated lookups.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/desktop/src/renderer/components/prs/tabs/GitHubTab.tsx` around lines 174
- 178, The hook useLaneColorById does a linear find each call; instead compute a
laneId→color map once (e.g., in the parent component GitHubTab using useMemo
over s.lanes from useAppStore) and then either (a) replace useLaneColorById with
a hook that accepts the precomputed map or (b) add a new selector/useMemo named
laneColorMap and change call sites to index into laneColorMap[laneId] — update
all usages of useLaneColorById to use the map to avoid repeated s.lanes.find
scans.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@apps/desktop/src/main/services/github/githubService.ts`:
- Around line 388-390: The classic-PAT branch currently returns
access.hasRequiredAccess unconditionally; change it so that when args.tokenType
=== "classic" you also require repoAccessOk when a repo is known (i.e., return
access.hasRequiredAccess && repoAccessOk if repo is present), using the same
repoAccessOk value used elsewhere in this function, so classic tokens are only
considered connected when they both have required scopes
(getGitHubTokenAccessState(...) -> access.hasRequiredAccess) and actual access
to the active repo.
- Around line 215-238: The probeRepoAccess function currently calls fetch
without an AbortController; wrap the fetch in an AbortController with a timeout
(reuse the same pattern as detectRepo) by creating a controller, passing
controller.signal to fetch, and scheduling a timer (e.g., setTimeout) to call
controller.abort() after the desired timeout; clear the timer on success or
error and ensure the catch branch treats an aborted request (AbortError) the
same way as other errors (returning { ok: false, error: ... }). Update
probeRepoAccess to add the controller, signal, timeout, and proper cleanup
around the existing fetch logic.

In `@apps/desktop/src/main/services/lanes/laneService.ts`:
- Around line 2883-2896: The current check in laneService.ts runs the conflict
query even when the only change is case (e.g. "#a78bfa" -> "#A78BFA"); update
the conditional that guards the DB conflict probe to perform a case-insensitive
idempotence check first (compare normalizedColor.toLowerCase() with
lane.color?.toLowerCase()) and only run the db.get conflict query when those
lowercase values differ; keep the existing conflict query and error throw in
place (look for the normalizedColor check and the db.get block).

In `@apps/desktop/src/renderer/browserMock.ts`:
- Around line 3059-3101: The mock returns static snapshots and leaves
onStatusChanged as noop, so AppShell doesn't receive immediate banner updates;
update the browser mock to support subscribing and emitting status changes: add
an internal listener list and implement onStatusChanged to register callbacks
(and return an unsubscribe), then modify setToken (resolvedArg) and clearToken
(resolved) handlers to update the internal status object (same shape as
getStatus) and invoke all registered listeners with the new status; ensure
detectRepo/listRepo* remain unchanged and use the same status shape fields
(tokenStored, tokenType, userLogin, scopes, checkedAt, repoAccessOk, connected,
etc.) so AppShell's reactions work in browser-preview.

In `@apps/desktop/src/renderer/components/automations/AdeActionEditor.tsx`:
- Around line 495-521: The json branch in AdeActionEditor.tsx currently calls
onChange(raw) when JSON.parse fails, which stores a malformed string into a
json-typed param; update the textarea onChange handler (the block handling
param.type === "json") to stop writing the raw string back via onChange on parse
error and instead retain the previous parsed value (value) and set a local parse
error state (similar to JsonArgsEditor's parseError pattern) to surface an
inline error message; only call onChange with the parsed object when JSON.parse
succeeds and ensure the placeholder/textarea still displays the user's raw text
without persisting it into args until it parses.
- Around line 309-324: The component currently coerces array-shaped
AdeActionValue.args into an empty object and then setArg (in ActionParamsEditor)
overwrites the original array, silently destroying data; update the logic to
detect array-shaped args (e.g., const argsAreArray = Array.isArray(value.args))
and do NOT coerce arrays to {} — if argsAreArray is true, render or switch to
the raw JSON editor (or show a confirmation/warning) and disable the
keyed-schema form and setArg path so edits cannot overwrite the array; keep
setArg and onChange behavior only for object-shaped args (i.e., guard setArg
with !argsAreArray) so legacy/externally authored array payloads are preserved.
- Around line 539-549: In PlaceholderRow the window.setTimeout callback can run
after the component unmounts causing a React state-update warning; fix by
storing the timeout id in a ref (e.g., timeoutRef) whenever you call
window.setTimeout in the copy function, clear any existing timeout before
setting a new one, and add a useEffect cleanup that calls
clearTimeout(timeoutRef.current) on unmount (and also clear the timeout on
subsequent clicks in copy) so setCopied is never called after unmount.

In `@apps/desktop/src/renderer/components/automations/adeActionSchemas.ts`:
- Around line 1-4: Update the top-of-file comment in adeActionSchemas.ts to
reference the full repo-relative path for the ADE actions registry: replace
`src/main/services/adeActions/registry.ts` with
`apps/desktop/src/main/services/adeActions/registry.ts` so the comment in this
file (adeActionSchemas.ts) accurately points to the registry implementation used
by the automation rule editor.
- Line 559: The schema for the "draft" boolean in adeActionSchemas.ts is marked
required: true while also supplying defaultValue: false; update the "draft"
parameter to be optional to match other boolean params (i.e., remove or set
required to false) while keeping defaultValue: false so the UI doesn't force an
explicit toggle; locate the "draft" entry in the schema used by
pr.createFromLane and adjust the required flag accordingly.

In `@apps/desktop/src/renderer/components/lanes/CreateLaneDialog.tsx`:
- Around line 128-133: The effect in CreateLaneDialog.tsx currently sets a
default color whenever selectedColor is null while open, which causes a cleared
selection to be immediately repopulated; change the effect so it only sets a
default color when the dialog is opened (i.e., when open transitions from false
-> true). Implement a prevOpen ref (e.g., prevOpenRef) and in the useEffect
check if open === true && prevOpenRef.current === false && selectedColor ===
null before calling nextAvailableColor(lanes) and setSelectedColor(next); then
update prevOpenRef.current = open—this preserves the nullable selectedColor
while the dialog is open but still assigns a default on initial open.

In `@apps/desktop/src/renderer/components/lanes/laneColorPalette.ts`:
- Around line 29-32: getLaneAccent may index LANE_FALLBACK_COLORS with a
negative fallbackIndex (because JS % preserves sign), returning undefined;
update getLaneAccent to normalize fallbackIndex into the valid 0..length-1 range
before indexing (e.g. compute const idx = ((fallbackIndex % len) + len) % len
where len = LANE_FALLBACK_COLORS.length) and use that idx to select the fallback
color so the function always returns a string.

In `@apps/desktop/src/renderer/components/lanes/LanesPage.tsx`:
- Around line 1658-1663: The catch block swallowing errors when applying a lane
color (inside the if (createSelectedColor) branch around
window.ade.lanes.updateAppearance({ laneId: lane.id, color: createSelectedColor
})) should surface the failure; update the catch to log the caught error with
context (include lane.id and createSelectedColor) and/or trigger a user-visible
notification so color-apply failures are discoverable for debugging and UX.

In `@apps/desktop/src/renderer/components/lanes/ManageLaneDialog.tsx`:
- Around line 298-323: The apply function and the LaneColorPicker onChange
currently allow calls even when an update is already in progress; guard against
concurrent updates by checking the busy state (or a disabled flag) before
initiating updateAppearance. Modify apply to return immediately if busy is true,
and update the onChange handler to avoid invoking apply when busy (e.g.,
onChange={(next) => { if (!busy) void apply(next); }}). Also pass the
busy/disabled state into the LaneColorPicker (if it supports a disabled prop) or
disable its UI while busy so users cannot trigger overlapping updates; keep
existing setBusy/setError behavior around the call to
window.ade.lanes.updateAppearance in apply.

In `@apps/desktop/src/renderer/components/terminals/WorkViewArea.tsx`:
- Around line 510-564: The tab close control is nested inside the outer tab
<button> (in the WorkViewArea tab rendering) which creates invalid interactive
nesting; refactor by moving the close affordance out of the inner content so it
is a sibling element to the tab button (keep selection onClick/onContextMenu on
the main <button> via onSelectItem(session.id) and handleContextMenu(session,
e), and wire the sibling close element to call onCloseItem(session.id>),
preserving e.stopPropagation(), keyboard support (Enter/Space) and isBusy
guarding; remove role="button" and tabIndex from the inner markup and replicate
the visual styles (width/height/color/opacity/group-hover behavior and X icon)
on the new sibling element; apply the same change for the second equivalent tab
rendering (the other tab list) so both places use a sibling close control
instead of nested interactive elements.

In `@apps/ios/ADE/Views/Lanes/LaneColorPalette.swift`:
- Around line 36-38: The Scanner usage in the color parsing block (variables
cleaned and value) can return true after only consuming a prefix; update the
logic to create a Scanner instance (e.g., let scanner = Scanner(string:
cleaned)), call scanner.scanHexInt64(&value) and then validate the scanner
consumed the entire string by checking scanner.scanLocation == cleaned.count (or
perform explicit char validation) before proceeding; ensure you still respect
the existing length guards (cleaned.count == 6 || cleaned.count == 8) and return
nil if the scanner did not consume the full input.

In `@apps/ios/ADE/Views/Lanes/LaneColorSwatchPicker.swift`:
- Around line 39-61: The "No color" Button does not expose selection state to
VoiceOver; update the Button (the Button that calls onSelect(nil) and visually
checks selectedHex == nil) to add accessibilityAddTraits so it includes
.isSelected when selectedHex == nil and always includes .isButton, matching the
color swatch pattern (use accessibilityAddTraits(selectedHex == nil ?
[.isSelected, .isButton] : [.isButton])).

In `@apps/ios/ADE/Views/Lanes/LaneCreateSheet.swift`:
- Around line 339-345: The catch block that swallows errors when calling
syncService.updateLaneAppearance(created.id, color: hex) should present
non‑fatal user feedback and log the error rather than silently ignoring it;
update the catch to (1) log the caught error (e.g., via a logger or print) and
(2) surface a UI notification by setting/using a local `@State` or viewModel
property (e.g., showColorApplyError / colorApplyErrorMessage) so the sheet can
display a non‑blocking alert/toast indicating the color couldn't be applied
while leaving the lane creation successful. Ensure this change is made inside
the existing if let selectedColorHex ... try await ... catch block so behavior
remains non‑fatal.

In `@apps/ios/ADE/Views/Lanes/LaneListViewParts.swift`:
- Around line 513-522: The applyLaneColor(_ hex: String?, to laneId: String)
async method calls syncService.updateLaneAppearance(...) but then reloads the UI
with reload(refreshRemote: false), causing stale cached data; change the reload
call to reload(refreshRemote: true) so the UI performs a server refresh after
updateLaneAppearance and immediately reflects the color change. Ensure you only
modify the reload(...) invocation in applyLaneColor to pass true.

In `@apps/ios/ADE/Views/Lanes/LaneTreeView.swift`:
- Around line 166-175: The headline Text should not reuse the resolved
laneAccent tint because some palette colors fail WCAG contrast; update the views
that currently color the headline (the Text(snapshot.lane.name) usage in
LaneTreeView / LaneComponents.swift and the similar usage in
WorkRootComponents.swift) to always use ADEColor.textPrimary for the headline
while leaving the Circle().fill(laneAccent) (and LaneStatusIndicator) to convey
the color; alternatively, if you prefer dynamic coloring, run laneAccent through
a luminance-contrast helper (create/apply a helper named something like
accessibleAccent(for:)) and only apply the adjusted color to Text when it meets
a 3:1 contrast ratio against ADEColor.surfaceBackground, falling back to
ADEColor.textPrimary otherwise.

In `@apps/ios/ADE/Views/Work/WorkRootComponents.swift`:
- Around line 700-711: The lane name text is using the raw
LaneColorPalette.color(forHex:) which can yield poor contrast; add a new
LaneColorPalette.textColor(forHex:) that adjusts luminance or enforces a minimum
contrast (e.g., lighten/darken or pick white/black fallback) and update the
Text(session.laneName).foregroundStyle(...) to use
LaneColorPalette.textColor(forHex: lane?.color) while keeping
Circle().fill(LaneColorPalette.color(forHex: lane?.color)) for the 6×6 accent
dot so the accent remains the raw user color and the label uses the
contrast-safe color.

---

Outside diff comments:
In
`@apps/desktop/src/renderer/components/automations/components/RuleEditorPanel.tsx`:
- Around line 459-486: RuleEditorPanel declares missionsEnabled in its prop type
but does not destructure or use it; remove missionsEnabled from the
RuleEditorPanel prop type and its parameter list (the function signature) and
then remove the missionsEnabled={missionsEnabled} prop passed from RulesTab (or
stop computing it there if unused) so the component signature and callers are
consistent; search for RuleEditorPanel and RulesTab to update the component
props and the caller(s) that pass missionsEnabled accordingly.
- Around line 214-226: Persisted rules may contain legacy git.pr_* trigger types
which map to the github family in triggerFamilyForType but don't match
TRIGGER_OPTIONS.github, causing the select to lose its option; fix this by
applying normalizeTriggerType when saving/normalizing drafts (the same
normalization used in automationService.normalizeDraft flow) so that
normalizeDraft (or the save path) converts git.pr_* to github.pr_* before
validation/persisting; update the save/normalize path that calls normalizeDraft
to call normalizeTriggerType on rule.trigger.type (or run the existing
normalizeTriggerType utility) so persisted rules always use canonical
github.pr_* names and the UI select matches.

In `@apps/desktop/src/renderer/components/terminals/SessionListPane.tsx`:
- Around line 49-76: The current accent wiring in SessionListPane causes
readability and hover issues: change the label span so it doesn't use the raw
accentColor (replace style={{ color: accentColor }} with a readable mix, e.g.
color-mix(in srgb, ${accentColor} 70%, var(--color-fg)) or keep text-fg/90 and
only tint icon/border), stop overriding the :hover class by removing the
always-set inline background when no accent (drive background via a CSS
variable/class or only set style.background when accentColor is present), and
ensure the left border reservation is consistent by always setting borderLeft
(use a transparent 2px border when accentColor is falsy) so rows remain aligned;
look for accentBg, accentColor, the style prop on the container button, the
borderLeft assignment, and the label span to apply these fixes.

---

Nitpick comments:
In `@apps/desktop/src/main/services/github/githubService.test.ts`:
- Around line 564-572: Tighten the "missing token" test by asserting that no
HTTP probes were made: after calling makeService().getStatus() in the test, add
an assertion that mockFetch (the fetch/mock used by the githubService tests) was
not called to ensure getStatus() does not perform network requests when no token
is stored; locate this in the "missing token returns not-connected status..."
test that uses stubOriginRemote(), makeService(), and getStatus().

In `@apps/desktop/src/main/services/lanes/laneService.test.ts`:
- Around line 2307-2326: Add a new test (or extend the existing idempotency
test) that covers case-only hex differences: seed two lanes ("lane-parent" and
"lane-child") with the same logical color but different casing (e.g., db.run set
parent to "#A78BFA" and child to "#a78bfa"), then call
service.updateAppearance({ laneId: "lane-child", color: "#A78BFA", icon: "star"
}) and assert it does not throw and that the stored color/icon for "lane-child"
are updated as expected; reference the existing test harness, the
service.updateAppearance call, and the DB queries/selects used to verify color
and icon.

In `@apps/desktop/src/renderer/components/automations/ActionList.tsx`:
- Around line 190-282: The AddStepBar trigger button does not announce the menu
state to assistive tech; update the button in AddStepBar to include
aria-haspopup="menu" and aria-expanded={open} so screen readers know it opens a
menu and its current state, and apply the same pattern to the ActionPicker
trigger in AdeActionEditor.tsx (add aria-haspopup="listbox" or "menu" and
aria-expanded bound to its open state) to ensure proper accessibility semantics.

In `@apps/desktop/src/renderer/components/automations/adeActionSchemas.ts`:
- Around line 2100-2102: Replace the O(n) scan in findAdeActionSchema with an
indexed Map built once at module load: create a module-level Map<string,
AdeActionSchema> (key = `${domain}.${action}`) populated from ADE_ACTION_SCHEMAS
when the file initializes, then change findAdeActionSchema(domain, action) to
compute the same key and return map.get(key). Ensure the Map is exported or
referenced in this module only and remove the .find usage so callers (e.g.,
AdeActionEditor and ActionPicker.matches) get O(1) lookups.

In `@apps/desktop/src/renderer/components/prs/tabs/GitHubTab.tsx`:
- Around line 174-178: The hook useLaneColorById does a linear find each call;
instead compute a laneId→color map once (e.g., in the parent component GitHubTab
using useMemo over s.lanes from useAppStore) and then either (a) replace
useLaneColorById with a hook that accepts the precomputed map or (b) add a new
selector/useMemo named laneColorMap and change call sites to index into
laneColorMap[laneId] — update all usages of useLaneColorById to use the map to
avoid repeated s.lanes.find scans.

In `@apps/ios/ADE/Views/Lanes/LaneComponents.swift`:
- Around line 353-360: Compute the lane accent once in the LaneStackCard view by
assigning LaneColorPalette.color(forHex: snapshot.lane.color) to a local let
(e.g., laneAccent) and reuse that variable for both the Circle().fill(...) and
the Text.foregroundStyle(...) fallback, replacing the second call to
LaneColorPalette.color(forHex: ...) so you don't resolve the palette twice and
the view is clearer.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 60de06ca-c44c-4517-a98b-3dbd877e71dc

📥 Commits

Reviewing files that changed from the base of the PR and between d47a0ff and b6980f5.

⛔ Files ignored due to path filters (7)
  • docs/features/automations/README.md is excluded by !docs/**
  • docs/features/automations/triggers-and-actions.md is excluded by !docs/**
  • docs/features/lanes/README.md is excluded by !docs/**
  • docs/features/onboarding-and-settings/README.md is excluded by !docs/**
  • docs/features/pull-requests/README.md is excluded by !docs/**
  • docs/features/terminals-and-sessions/README.md is excluded by !docs/**
  • docs/features/terminals-and-sessions/ui-surfaces.md is excluded by !docs/**
📒 Files selected for processing (52)
  • .claude/commands/finalize.md
  • apps/desktop/src/main/services/github/githubService.test.ts
  • apps/desktop/src/main/services/github/githubService.ts
  • apps/desktop/src/main/services/ipc/registerIpc.ts
  • apps/desktop/src/main/services/lanes/laneService.test.ts
  • apps/desktop/src/main/services/lanes/laneService.ts
  • apps/desktop/src/preload/global.d.ts
  • apps/desktop/src/preload/preload.ts
  • apps/desktop/src/renderer/browserMock.ts
  • apps/desktop/src/renderer/components/app/AppShell.tsx
  • apps/desktop/src/renderer/components/automations/ActionList.tsx
  • apps/desktop/src/renderer/components/automations/ActionRow.tsx
  • apps/desktop/src/renderer/components/automations/AdeActionEditor.tsx
  • apps/desktop/src/renderer/components/automations/adeActionSchemas.ts
  • apps/desktop/src/renderer/components/automations/components/RuleEditorPanel.tsx
  • apps/desktop/src/renderer/components/automations/shared.ts
  • apps/desktop/src/renderer/components/chat/AgentChatComposer.test.tsx
  • apps/desktop/src/renderer/components/chat/AgentChatComposer.tsx
  • apps/desktop/src/renderer/components/chat/AgentChatPane.submit.test.tsx
  • apps/desktop/src/renderer/components/chat/AgentChatPane.tsx
  • apps/desktop/src/renderer/components/chat/ChatGitToolbar.tsx
  • apps/desktop/src/renderer/components/lanes/CreateLaneDialog.tsx
  • apps/desktop/src/renderer/components/lanes/LaneAccentDot.tsx
  • apps/desktop/src/renderer/components/lanes/LaneColorPicker.tsx
  • apps/desktop/src/renderer/components/lanes/LaneContextMenu.tsx
  • apps/desktop/src/renderer/components/lanes/LaneStackPane.tsx
  • apps/desktop/src/renderer/components/lanes/LanesPage.tsx
  • apps/desktop/src/renderer/components/lanes/ManageLaneDialog.tsx
  • apps/desktop/src/renderer/components/lanes/laneColorPalette.ts
  • apps/desktop/src/renderer/components/lanes/laneUtils.test.ts
  • apps/desktop/src/renderer/components/missions/ManageMissionDialog.tsx
  • apps/desktop/src/renderer/components/missions/MissionHeader.tsx
  • apps/desktop/src/renderer/components/prs/tabs/GitHubTab.tsx
  • apps/desktop/src/renderer/components/prs/tabs/QueueTab.tsx
  • apps/desktop/src/renderer/components/settings/GitHubSection.tsx
  • apps/desktop/src/renderer/components/shared/ProviderModelSelector.tsx
  • apps/desktop/src/renderer/components/terminals/SessionCard.tsx
  • apps/desktop/src/renderer/components/terminals/SessionListPane.tsx
  • apps/desktop/src/renderer/components/terminals/WorkViewArea.tsx
  • apps/desktop/src/renderer/components/terminals/workSessionTiling.test.ts
  • apps/desktop/src/renderer/components/terminals/workSessionTiling.ts
  • apps/desktop/src/renderer/components/ui/SmartTooltip.tsx
  • apps/desktop/src/shared/ipc.ts
  • apps/desktop/src/shared/types/git.ts
  • apps/ios/ADE/Views/Lanes/LaneColorPalette.swift
  • apps/ios/ADE/Views/Lanes/LaneColorSwatchPicker.swift
  • apps/ios/ADE/Views/Lanes/LaneComponents.swift
  • apps/ios/ADE/Views/Lanes/LaneCreateSheet.swift
  • apps/ios/ADE/Views/Lanes/LaneListViewParts.swift
  • apps/ios/ADE/Views/Lanes/LaneManageSheet.swift
  • apps/ios/ADE/Views/Lanes/LaneTreeView.swift
  • apps/ios/ADE/Views/Work/WorkRootComponents.swift
💤 Files with no reviewable changes (1)
  • apps/desktop/src/renderer/components/automations/shared.ts

Comment on lines +215 to +238
const probeRepoAccess = async (
token: string,
repo: GitHubRepoRef,
): Promise<{ ok: boolean; error: string | null }> => {
try {
const response = await fetch(
`https://api.github.com/repos/${encodeURIComponent(repo.owner)}/${encodeURIComponent(repo.name)}`,
{
method: "GET",
headers: {
accept: "application/vnd.github+json",
authorization: `Bearer ${token}`,
"user-agent": "ade-desktop",
},
},
);
if (response.ok) return { ok: true, error: null };
const payload = (await response.json().catch(() => ({}))) as Record<string, unknown>;
const message = asString(payload.message) || `HTTP ${response.status}`;
return { ok: false, error: `${response.status}: ${message}` };
} catch (error) {
return { ok: false, error: error instanceof Error ? error.message : String(error) };
}
};
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

cat -n apps/desktop/src/main/services/github/githubService.ts | head -300

Repository: arul28/ADE

Length of output: 12832


🏁 Script executed:

cat -n apps/desktop/src/main/services/github/githubService.ts | sed -n '238,400p'

Repository: arul28/ADE

Length of output: 7282


🏁 Script executed:

cat -n apps/desktop/src/main/services/github/githubService.ts | sed -n '396,450p'

Repository: arul28/ADE

Length of output: 2412


🏁 Script executed:

cat -n apps/desktop/src/main/services/github/githubService.ts | sed -n '450,480p'

Repository: arul28/ADE

Length of output: 1129


🏁 Script executed:

rg -n "AbortController|timeoutMs" apps/desktop/src/main/services/github/githubService.ts

Repository: arul28/ADE

Length of output: 160


🏁 Script executed:

rg -n "AbortController" apps/desktop/src/main --type ts --type tsx -B 2 -A 2

Repository: arul28/ADE

Length of output: 82


🏁 Script executed:

rg -n "AbortController" apps/desktop/src/main -B 2 -A 2

Repository: arul28/ADE

Length of output: 24595


🏁 Script executed:

rg -n "getStatus" apps/desktop/src/main/services/github/githubService.ts | head -20

Repository: arul28/ADE

Length of output: 171


🏁 Script executed:

rg -n "github.*getStatus\|getStatus.*github" apps/desktop/src/main --type ts -B 2 -A 2 | head -50

Repository: arul28/ADE

Length of output: 36


🏁 Script executed:

grep -r "getStatus" apps/desktop/src/main --include="*.ts" | grep -i "github\|service" | head -20

Repository: arul28/ADE

Length of output: 2298


🏁 Script executed:

rg -n "github.*\.getStatus\|githubService.*getStatus" apps/desktop/src --type ts -B 2 -A 2 | head -60

Repository: arul28/ADE

Length of output: 36


🏁 Script executed:

sed -n '640,660p' apps/desktop/src/main/services/github/githubService.ts

Repository: arul28/ADE

Length of output: 532


Add a timeout to the repo probe.

The probeRepoAccess fetch at lines 220–229 runs without an AbortController or timeout signal. If GitHub stalls after /user succeeds, getStatus() never settles and the Settings UI can hang indefinitely on the main process. Wrap the probe in an AbortController with a timeout, following the pattern already established in detectRepo() (line 180) and throughout the codebase (e.g., usageTrackingService, modelsDevService).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/desktop/src/main/services/github/githubService.ts` around lines 215 -
238, The probeRepoAccess function currently calls fetch without an
AbortController; wrap the fetch in an AbortController with a timeout (reuse the
same pattern as detectRepo) by creating a controller, passing controller.signal
to fetch, and scheduling a timer (e.g., setTimeout) to call controller.abort()
after the desired timeout; clear the timer on success or error and ensure the
catch branch treats an aborted request (AbortError) the same way as other errors
(returning { ok: false, error: ... }). Update probeRepoAccess to add the
controller, signal, timeout, and proper cleanup around the existing fetch logic.

Comment on lines +388 to +390
if (args.tokenType === "classic") {
const access = getGitHubTokenAccessState(args.scopes);
return access.hasRequiredAccess;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Don’t report classic PATs as connected when repo access failed.

connected is now the renderer’s “GitHub is usable for this project” flag, but this branch ignores repoAccessOk for classic tokens. A classic PAT with the right scopes but no access to the active repo will still come back as connected, which leads to a green connected state alongside a repo-access error. When repo is known, classic tokens should also require repoAccessOk === true.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/desktop/src/main/services/github/githubService.ts` around lines 388 -
390, The classic-PAT branch currently returns access.hasRequiredAccess
unconditionally; change it so that when args.tokenType === "classic" you also
require repoAccessOk when a repo is known (i.e., return access.hasRequiredAccess
&& repoAccessOk if repo is present), using the same repoAccessOk value used
elsewhere in this function, so classic tokens are only considered connected when
they both have required scopes (getGitHubTokenAccessState(...) ->
access.hasRequiredAccess) and actual access to the active repo.

Comment on lines +2883 to +2896
if (normalizedColor && normalizedColor !== lane.color) {
const conflict = db.get<{ name: string }>(
`select name from lanes
where project_id = ?
and id != ?
and archived_at is null
and lower(color) = lower(?)
limit 1`,
[projectId, laneId, normalizedColor]
);
if (conflict) {
throw new Error(`Color already in use by lane "${conflict.name}"`);
}
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Make idempotent color comparison case-insensitive before conflict probing.

Right now, case-only changes (e.g. #a78bfa#A78BFA) still trigger the conflict query. On legacy duplicated colors, that can block non-color updates unexpectedly.

Proposed fix
-      if (normalizedColor && normalizedColor !== lane.color) {
+      const nextColorKey = normalizedColor?.trim().toLowerCase() ?? null;
+      const currentColorKey = lane.color?.trim().toLowerCase() ?? null;
+      if (nextColorKey && nextColorKey !== currentColorKey) {
         const conflict = db.get<{ name: string }>(
           `select name from lanes
            where project_id = ?
              and id != ?
              and archived_at is null
-             and lower(color) = lower(?)
+             and lower(trim(color)) = ?
            limit 1`,
-          [projectId, laneId, normalizedColor]
+          [projectId, laneId, nextColorKey]
         );
         if (conflict) {
           throw new Error(`Color already in use by lane "${conflict.name}"`);
         }
       }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/desktop/src/main/services/lanes/laneService.ts` around lines 2883 -
2896, The current check in laneService.ts runs the conflict query even when the
only change is case (e.g. "#a78bfa" -> "#A78BFA"); update the conditional that
guards the DB conflict probe to perform a case-insensitive idempotence check
first (compare normalizedColor.toLowerCase() with lane.color?.toLowerCase()) and
only run the db.get conflict query when those lowercase values differ; keep the
existing conflict query and error throw in place (look for the normalizedColor
check and the db.get block).

Comment on lines +3059 to +3101
getStatus: resolved({
tokenStored: true,
tokenDecryptionFailed: false,
storageScope: "app",
tokenType: "classic",
repo: { owner: "arul28", name: "ADE" },
userLogin: "arul",
scopes: ["repo", "workflow"],
checkedAt: new Date().toISOString(),
repoAccessOk: true,
repoAccessError: null,
connected: true,
}),
setToken: resolvedArg({
tokenStored: true,
tokenDecryptionFailed: false,
storageScope: "app",
tokenType: "classic",
repo: { owner: "arul28", name: "ADE" },
userLogin: "arul",
scopes: ["repo", "workflow"],
checkedAt: new Date().toISOString(),
repoAccessOk: true,
repoAccessError: null,
connected: true,
}),
clearToken: resolved({
tokenStored: false,
tokenDecryptionFailed: false,
storageScope: "app",
tokenType: "unknown",
repo: { owner: "arul28", name: "ADE" },
userLogin: null,
scopes: [],
checkedAt: null,
repoAccessOk: null,
repoAccessError: null,
connected: false,
}),
detectRepo: resolved({ owner: "arul28", name: "ADE" }),
listRepoLabels: resolved([]),
listRepoCollaborators: resolved([]),
onStatusChanged: noop,
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Make the GitHub browser mock emit status changes.

AppShell now depends on onStatusChanged for immediate banner updates, but this mock still returns fixed snapshots and onStatusChanged: noop. In browser-preview mode, token save/clear flows will stay stale and won't validate the new behavior.

Suggested direction
+let mockGithubStatus = {
+  tokenStored: true,
+  tokenDecryptionFailed: false,
+  storageScope: "app",
+  tokenType: "classic",
+  repo: { owner: "arul28", name: "ADE" },
+  userLogin: "arul",
+  scopes: ["repo", "workflow"],
+  checkedAt: new Date().toISOString(),
+  repoAccessOk: true,
+  repoAccessError: null,
+  connected: true,
+};
+
+const githubStatusListeners = new Set<(status: any) => void>();
+const emitGithubStatus = () => {
+  for (const listener of githubStatusListeners) listener({ ...mockGithubStatus });
+};
+
 github: {
-  getStatus: resolved({
-    ...
-  }),
-  setToken: resolvedArg({
-    ...
-  }),
-  clearToken: resolved({
-    ...
-  }),
+  getStatus: async () => ({ ...mockGithubStatus }),
+  setToken: async () => {
+    mockGithubStatus = { ...mockGithubStatus, tokenStored: true, connected: true, checkedAt: new Date().toISOString() };
+    emitGithubStatus();
+    return { ...mockGithubStatus };
+  },
+  clearToken: async () => {
+    mockGithubStatus = { ...mockGithubStatus, tokenStored: false, userLogin: null, scopes: [], checkedAt: null, repoAccessOk: null, repoAccessError: null, connected: false };
+    emitGithubStatus();
+    return { ...mockGithubStatus };
+  },
   detectRepo: resolved({ owner: "arul28", name: "ADE" }),
   listRepoLabels: resolved([]),
   listRepoCollaborators: resolved([]),
-  onStatusChanged: noop,
+  onStatusChanged: (listener: (status: any) => void) => {
+    githubStatusListeners.add(listener);
+    return () => githubStatusListeners.delete(listener);
+  },
 },
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/desktop/src/renderer/browserMock.ts` around lines 3059 - 3101, The mock
returns static snapshots and leaves onStatusChanged as noop, so AppShell doesn't
receive immediate banner updates; update the browser mock to support subscribing
and emitting status changes: add an internal listener list and implement
onStatusChanged to register callbacks (and return an unsubscribe), then modify
setToken (resolvedArg) and clearToken (resolved) handlers to update the internal
status object (same shape as getStatus) and invoke all registered listeners with
the new status; ensure detectRepo/listRepo* remain unchanged and use the same
status shape fields (tokenStored, tokenType, userLogin, scopes, checkedAt,
repoAccessOk, connected, etc.) so AppShell's reactions work in browser-preview.

Comment on lines +309 to +324
const args = (value.args && typeof value.args === "object" && !Array.isArray(value.args))
? (value.args as Record<string, unknown>)
: {};

const setArg = (name: string, next: unknown) => {
const nextArgs = { ...args };
if (next === undefined || next === "" || next === null) {
delete nextArgs[name];
} else {
nextArgs[name] = next;
}
onChange({
...value,
args: Object.keys(nextArgs).length === 0 ? undefined : nextArgs,
});
};
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Array-typed args are silently dropped when editing a schema action.

AdeActionValue.args is typed as Record<string, unknown> | unknown[], but ActionParamsEditor coerces array args to {} and then setArg writes { [name]: value } over value.args. If the user lands on an action whose stored args is an array (legacy rules, externally authored payloads, etc.), the first parameter edit silently destroys the original array contents.

At minimum, fall back to the raw JSON editor when the args shape doesn't match the schema's keyed-form expectation, or warn the user before overwriting.

♻️ Suggested guard
-  const args = (value.args && typeof value.args === "object" && !Array.isArray(value.args))
-    ? (value.args as Record<string, unknown>)
-    : {};
+  const argsAreArray = Array.isArray(value.args);
+  const args = (value.args && typeof value.args === "object" && !argsAreArray)
+    ? (value.args as Record<string, unknown>)
+    : {};
+  // If the persisted args are an array, force the raw JSON view so the user
+  // can see/edit them instead of silently coercing to `{}` on first keystroke.

…and gate the schema form on !argsAreArray so array-shaped payloads stay in the JSON editor.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/desktop/src/renderer/components/automations/AdeActionEditor.tsx` around
lines 309 - 324, The component currently coerces array-shaped
AdeActionValue.args into an empty object and then setArg (in ActionParamsEditor)
overwrites the original array, silently destroying data; update the logic to
detect array-shaped args (e.g., const argsAreArray = Array.isArray(value.args))
and do NOT coerce arrays to {} — if argsAreArray is true, render or switch to
the raw JSON editor (or show a confirmation/warning) and disable the
keyed-schema form and setArg path so edits cannot overwrite the array; keep
setArg and onChange behavior only for object-shaped args (i.e., guard setArg
with !argsAreArray) so legacy/externally authored array payloads are preserved.

Comment on lines +39 to +61
Button {
onSelect(nil)
} label: {
ZStack {
Circle()
.fill(Color.clear)
.frame(width: 30, height: 30)
.overlay(
Circle()
.strokeBorder(Color.white.opacity(0.35), style: StrokeStyle(lineWidth: 1, dash: [3]))
)
Image(systemName: "xmark")
.font(.system(size: 11, weight: .semibold))
.foregroundStyle(ADEColor.textSecondary)
if selectedHex == nil {
Circle()
.stroke(ADEColor.textPrimary, lineWidth: 2)
.frame(width: 36, height: 36)
}
}
}
.buttonStyle(.plain)
.accessibilityLabel("No color")
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

# First, let's locate and view the full file
find . -name "LaneColorSwatchPicker.swift" -type f

Repository: arul28/ADE

Length of output: 109


🏁 Script executed:

# View the complete file to understand the swatch implementations
cat -n apps/ios/ADE/Views/Lanes/LaneColorSwatchPicker.swift

Repository: arul28/ADE

Length of output: 2553


🏁 Script executed:

# Search for accessibility modifiers in the file
rg "accessibility|isSelected" apps/ios/ADE/Views/Lanes/LaneColorSwatchPicker.swift -A 2 -B 2

Repository: arul28/ADE

Length of output: 899


🏁 Script executed:

# Check if other swatch buttons in the file use isSelected
rg "\.isSelected|\.accessibilityAddTraits|\.accessibilityRemoveTraits" apps/ios/ADE/Views/Lanes/ -A 1 -B 1

Repository: arul28/ADE

Length of output: 903


Add .isSelected accessibility trait to the "No color" button.

This button visually indicates selection when selectedHex == nil (the ring overlay), but it never adds .accessibilityAddTraits(), so VoiceOver cannot tell that "No color" is the current choice. Add the trait to match the pattern used for the color swatches:

.accessibilityLabel("No color")
.accessibilityAddTraits(selectedHex == nil ? [.isSelected, .isButton] : [.isButton])
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/ios/ADE/Views/Lanes/LaneColorSwatchPicker.swift` around lines 39 - 61,
The "No color" Button does not expose selection state to VoiceOver; update the
Button (the Button that calls onSelect(nil) and visually checks selectedHex ==
nil) to add accessibilityAddTraits so it includes .isSelected when selectedHex
== nil and always includes .isButton, matching the color swatch pattern (use
accessibilityAddTraits(selectedHex == nil ? [.isSelected, .isButton] :
[.isButton])).

Comment on lines +339 to +345
if let hex = selectedColorHex, !hex.isEmpty {
do {
try await syncService.updateLaneAppearance(created.id, color: hex)
} catch {
// Non-fatal: lane was created, color failed (likely uniqueness collision).
}
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Don’t silently swallow lane-color update failures after create.

If color assignment fails, the sheet currently succeeds with no user feedback. That can leave users thinking the chosen color was applied when it wasn’t.

💡 Suggested non-fatal feedback
       if let hex = selectedColorHex, !hex.isEmpty {
         do {
           try await syncService.updateLaneAppearance(created.id, color: hex)
         } catch {
-          // Non-fatal: lane was created, color failed (likely uniqueness collision).
+          // Non-fatal: lane was created, but color update failed.
+          queuedNotice = "Lane was created, but its color could not be applied. You can update it from lane settings."
         }
       }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/ios/ADE/Views/Lanes/LaneCreateSheet.swift` around lines 339 - 345, The
catch block that swallows errors when calling
syncService.updateLaneAppearance(created.id, color: hex) should present
non‑fatal user feedback and log the error rather than silently ignoring it;
update the catch to (1) log the caught error (e.g., via a logger or print) and
(2) surface a UI notification by setting/using a local `@State` or viewModel
property (e.g., showColorApplyError / colorApplyErrorMessage) so the sheet can
display a non‑blocking alert/toast indicating the color couldn't be applied
while leaving the lane creation successful. Ensure this change is made inside
the existing if let selectedColorHex ... try await ... catch block so behavior
remains non‑fatal.

Comment on lines +513 to +522
@MainActor
func applyLaneColor(_ hex: String?, to laneId: String) async {
do {
try await syncService.updateLaneAppearance(laneId, color: hex ?? "")
await reload(refreshRemote: false)
} catch {
ADEHaptics.error()
errorMessage = error.localizedDescription
}
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE="apps/ios/ADE/Services/SyncService.swift"
rg -n -C4 'func (updateLaneAppearance|refreshLaneSnapshots|fetchLaneListSnapshots)' "$FILE"

Repository: arul28/ADE

Length of output: 1305


🏁 Script executed:

FILE="apps/ios/ADE/Services/SyncService.swift"
# Get full updateLaneAppearance implementation
rg -n -A10 'func updateLaneAppearance\(' "$FILE" | head -20

Repository: arul28/ADE

Length of output: 438


🏁 Script executed:

FILE="apps/ios/ADE/Views/Lanes/LaneListViewParts.swift"
# Find and examine the reload() function
rg -n -B2 -A10 'func reload\(' "$FILE"

Repository: arul28/ADE

Length of output: 588


🏁 Script executed:

FILE="apps/ios/ADE/Views/Lanes/LaneListViewParts.swift"
# Check how archive/unarchive mutations are handled
rg -n -B2 -A8 'func (archiveLane|restoreLane)' "$FILE"

Repository: arul28/ADE

Length of output: 36


🏁 Script executed:

FILE="apps/ios/ADE/Services/SyncService.swift"
rg -n -A20 'func updateLaneAppearance\(' "$FILE"

Repository: arul28/ADE

Length of output: 848


🏁 Script executed:

FILE="apps/ios/ADE/Views/Lanes/LaneListViewParts.swift"
# Search more broadly for mutation functions
rg -n 'func (archive|restore|update|delete)' "$FILE" | head -20

Repository: arul28/ADE

Length of output: 36


🏁 Script executed:

FILE="apps/ios/ADE/Views/Lanes/LaneListViewParts.swift"
# Look at the full applyLaneColor function context and surrounding mutations
rg -n -B15 'func applyLaneColor' "$FILE"

Repository: arul28/ADE

Length of output: 600


🏁 Script executed:

FILE="apps/ios/ADE/Views/Lanes/LaneListViewParts.swift"
# Find where archiveLane/unarchiveLane are invoked
rg -n -B3 -A5 'syncService\.(archiveLane|unarchiveLane)' "$FILE"

Repository: arul28/ADE

Length of output: 695


🏁 Script executed:

FILE="apps/ios/ADE/Views/Lanes/LaneListViewParts.swift"
# Get the full applyLaneColor function with more context
sed -n '513,522p' "$FILE"

Repository: arul28/ADE

Length of output: 354


Change reload(refreshRemote: false) to reload(refreshRemote: true) for consistency.

The archiveLane() and unarchiveLane() calls in this file both use reload(refreshRemote: true) after their respective mutations. Since updateLaneAppearance() has the same backend pattern—it sends a command without updating the local cache—the color change needs a server refresh to appear in the UI. Using reload(refreshRemote: false) will display stale cached data until a later refresh occurs.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/ios/ADE/Views/Lanes/LaneListViewParts.swift` around lines 513 - 522, The
applyLaneColor(_ hex: String?, to laneId: String) async method calls
syncService.updateLaneAppearance(...) but then reloads the UI with
reload(refreshRemote: false), causing stale cached data; change the reload call
to reload(refreshRemote: true) so the UI performs a server refresh after
updateLaneAppearance and immediately reflects the color change. Ensure you only
modify the reload(...) invocation in applyLaneColor to pass true.

Comment on lines +166 to +175
let laneAccent = LaneColorPalette.color(forHex: snapshot.lane.color)
VStack(alignment: .leading, spacing: 10) {
HStack(alignment: .firstTextBaseline, spacing: 8) {
LaneStatusIndicator(bucket: snapshot.runtime.bucket, size: 9)
if let laneAccent {
Circle().fill(laneAccent).frame(width: 7, height: 7)
}
Text(snapshot.lane.name)
.font(.headline)
.foregroundStyle(ADEColor.textPrimary)
.foregroundStyle(laneAccent ?? ADEColor.textPrimary)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🌐 Web query:

WCAG contrast guidance for tinting headline text with palette colors on a dark surface

💡 Result:

WCAG contrast guidance for tinting headline text with palette colors on a dark surface requires calculating the contrast ratio between the tinted text color and the dark background color using the WCAG formula, ensuring it meets the minimum thresholds based on text size and boldness. For WCAG 2.2 AA (standard compliance level): - Normal text (<18pt or <14pt bold): 4.5:1 minimum. - Large headlines (≥18pt or ≥14pt bold): 3:1 minimum. For AAA (enhanced): 7:1 normal, 4.5:1 large. Tinted palette colors (e.g., brand hues) must have sufficient luminance difference from the dark surface. Hue/saturation aids branding but does not substitute for luminance contrast; low-luminance tints on dark backgrounds often fail. Test every tinted color explicitly against the background. On dark surfaces, WCAG 2 contrast formula may overestimate readability for light text; aim higher (e.g., 7:1+ for small text) or use WCAG 3's APCA, which is stricter for light-on-dark and font-aware (higher contrast needed for thin/small fonts, lower for bold/large). Practical steps: 1. Use tools like WebAIM Contrast Checker to compute ratios. 2. Adjust tint lightness/chroma in OKLCH/HSLuv (darken for light-on-dark). 3. Create separate "on-dark" palette variants (e.g., lighter tints). 4. Verify large headlines qualify as "large text" (24px+ or 18.5px bold). 5. Account for anti-aliasing/font rendering: thin fonts need ~30% higher contrast. Examples: Pure blue tints struggle on dark (add green); aim for near-white (#E9ECF1) or high-luminance colors for body, adjusted tints for headlines. WCAG 2.2 remains the enforced standard (as of 2026-04-27); WCAG 3/APCA is emerging best practice.

Citations:


🏁 Script executed:

# Check if the file exists and read the specific lines
if [ -f "apps/ios/ADE/Views/Lanes/LaneTreeView.swift" ]; then
  echo "=== File exists. Reading lines 160-180 for context ==="
  sed -n '160,180p' "apps/ios/ADE/Views/Lanes/LaneTreeView.swift" | cat -n
else
  echo "File not found at specified path. Searching for LaneTreeView.swift..."
  find . -name "LaneTreeView.swift" -type f
fi

Repository: arul28/ADE

Length of output: 996


🏁 Script executed:

# Search for LaneColorPalette definition
echo "=== Searching for LaneColorPalette ==="
rg "LaneColorPalette" --type swift -A 10 -B 2

Repository: arul28/ADE

Length of output: 15022


🏁 Script executed:

# Search for ADEColor.surfaceBackground definition
echo "=== Searching for surfaceBackground definition ==="
rg "surfaceBackground" --type swift -B 2 -A 2

Repository: arul28/ADE

Length of output: 40713


🏁 Script executed:

# Search for any contrast helpers or accessibility utilities
echo "=== Searching for contrast-related helpers ==="
rg "contrast|accessibility|luminance" --type swift -i

Repository: arul28/ADE

Length of output: 41532


Review accessibility concern: palette color contrast on lane headlines.

Once laneAccent is non-nil, both the leading Circle and headline Text use the same tint. The dot suffices to signal lane color identity; tinting the headline as well risks contrast failures against ADEColor.surfaceBackground for certain palette entries—particularly saturated yellows (amber), pinks, and pale hues on both light (#faf8f5) and dark (#16141e) surfaces. Per WCAG 2.2, large headlines require 3:1 minimum contrast (AA); many palette colors lack sufficient luminance separation.

This pattern appears in LaneComponents.swift (2 locations) and WorkRootComponents.swift as well, so a repo-wide fix is warranted. Consider either:

  1. Keep headline at ADEColor.textPrimary and let the dot carry color signaling.
  2. Run resolved accents through a luminance-aware helper before applying to text.
♻️ Suggested adjustment (dot-only color signal)
         if let laneAccent {
           Circle().fill(laneAccent).frame(width: 7, height: 7)
         }
         Text(snapshot.lane.name)
           .font(.headline)
-          .foregroundStyle(laneAccent ?? ADEColor.textPrimary)
+          .foregroundStyle(ADEColor.textPrimary)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/ios/ADE/Views/Lanes/LaneTreeView.swift` around lines 166 - 175, The
headline Text should not reuse the resolved laneAccent tint because some palette
colors fail WCAG contrast; update the views that currently color the headline
(the Text(snapshot.lane.name) usage in LaneTreeView / LaneComponents.swift and
the similar usage in WorkRootComponents.swift) to always use
ADEColor.textPrimary for the headline while leaving the
Circle().fill(laneAccent) (and LaneStatusIndicator) to convey the color;
alternatively, if you prefer dynamic coloring, run laneAccent through a
luminance-contrast helper (create/apply a helper named something like
accessibleAccent(for:)) and only apply the adjusted color to Text when it meets
a 3:1 contrast ratio against ADEColor.surfaceBackground, falling back to
ADEColor.textPrimary otherwise.

Comment on lines +700 to +711
if let laneAccent = LaneColorPalette.color(forHex: lane?.color) {
Circle()
.fill(laneAccent)
.frame(width: 6, height: 6)
} else {
Image(systemName: "arrow.triangle.branch")
.font(.system(size: 10, weight: .semibold))
.foregroundStyle(ADEColor.textMuted)
}
Text(session.laneName)
.font(.caption2)
.foregroundStyle(ADEColor.textMuted)
.foregroundStyle(LaneColorPalette.color(forHex: lane?.color) ?? ADEColor.textMuted)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Inspect LaneColorPalette to confirm whether color(forHex:) is intended for text or only for accents/dots.
fd -t f 'LaneColorPalette.swift' apps/ios | xargs -I {} sh -c 'echo "===== {} ====="; cat "{}"'

Repository: arul28/ADE

Length of output: 3092


Add contrast-safe text color mapping for lane names.

LaneColorPalette.color(forHex:) returns the raw user-selected hex color without luminance adjustment. Using it directly for Text(session.laneName).foregroundStyle(...) against the providerTintColor.opacity(0.10) row background creates a risk of low contrast and poor readability, especially if users pick dark or saturated colors.

Add a separate LaneColorPalette.textColor(forHex:) method that applies luminance adjustment (e.g., ensure minimum contrast ratio or lighten dark colors) and use it for the lane name text at line 711. Keep the raw color(forHex:) for the 6×6 accent dot, which is less sensitive to contrast issues.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/ios/ADE/Views/Work/WorkRootComponents.swift` around lines 700 - 711, The
lane name text is using the raw LaneColorPalette.color(forHex:) which can yield
poor contrast; add a new LaneColorPalette.textColor(forHex:) that adjusts
luminance or enforces a minimum contrast (e.g., lighten/darken or pick
white/black fallback) and update the Text(session.laneName).foregroundStyle(...)
to use LaneColorPalette.textColor(forHex: lane?.color) while keeping
Circle().fill(LaneColorPalette.color(forHex: lane?.color)) for the 6×6 accent
dot so the accent remains the raw user color and the label uses the
contrast-safe color.

…tions, and iOS

Includes paste-image fallback in AgentChatComposer (read clipboardData.items
when files is empty so screenshot/image paste works on macOS).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@arul28 arul28 force-pushed the ade/fixes-to-buncha-things-17615678 branch from b6980f5 to 3109b61 Compare April 27, 2026 04:44
@arul28
Copy link
Copy Markdown
Owner Author

arul28 commented Apr 27, 2026

@copilot review but do not make fixes

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