Skip to content

Fix sidebar collapse and Codex project labels#8

Closed
davej wants to merge 2 commits into
achimala:mainfrom
davej:pr/sidebar-collapse-and-project-labels
Closed

Fix sidebar collapse and Codex project labels#8
davej wants to merge 2 commits into
achimala:mainfrom
davej:pr/sidebar-collapse-and-project-labels

Conversation

@davej
Copy link
Copy Markdown
Contributor

@davej davej commented Feb 22, 2026

Summary

  • fix sidebar collapse so selected project groups can be collapsed
  • use Codex workspace root labels for sidebar project names
  • add tests covering both behaviors

Testing

  • apps/server: bun run typecheck
  • apps/web: bun run typecheck
  • apps/web: bun run test -- -t "allows collapsing the selected thread project group|shows Codex workspace labels for project groups when present"

Summary by CodeRabbit

  • New Features

    • Added support for custom project labels in project groups, sourced from workspace configuration, providing more meaningful names instead of default folder identifiers.
  • Tests

    • Added test cases for project group labeling and collapse behavior.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Feb 22, 2026

📝 Walkthrough

Walkthrough

The PR introduces workspace labeling support for agents by adding a projectLabels field to AgentDescriptor. The server reads workspace labels from Codex global state and passes them through descriptor construction, while the web client uses these labels to display project group names.

Changes

Cohort / File(s) Summary
Type Definition
apps/server/src/agents/types.ts
Added projectLabels: Record<string, string> field to AgentDescriptor interface.
Server Integration
apps/server/src/index.ts
Implemented readCodexWorkspaceRootLabels() to extract labels from Codex global state using Zod validation; updated buildAgentDescriptor() signature to accept and include projectLabels; wired label passing through agent listing and descriptor construction call sites.
Web Client
apps/web/src/lib/api.ts, apps/web/src/App.tsx
Added projectLabels field to AgentsResponseSchema; introduced memoized projectLabelsByPath map to aggregate labels from agent descriptors; updated group label resolution logic to prefer workspace labels over path-based fallbacks.
Tests
apps/web/test/app.test.tsx
Added fixture support for projectLabels; introduced tests for workspace label display and selected thread group collapse behavior.

Sequence Diagram

sequenceDiagram
    participant Codex as Codex Global State
    participant Server as Server (index.ts)
    participant Descriptor as Agent Descriptor
    participant API as /api/agents Response
    participant Web as Web Client (App.tsx)
    participant UI as Project Groups

    Codex->>Server: electron-workspace-root-labels
    Server->>Server: readCodexWorkspaceRootLabels()
    Server->>Descriptor: buildAgentDescriptor(..., projectLabels)
    Descriptor->>Descriptor: Include projectLabels field
    Descriptor->>API: Return AgentDescriptor
    API->>Web: Send agents with projectLabels
    Web->>Web: Aggregate projectLabelsByPath from descriptors
    Web->>Web: Resolve group labels from projectLabelsByPath
    Web->>UI: Render project groups with workspace labels
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~22 minutes

Possibly related PRs

Suggested reviewers

  • achimala

Poem

🐰 A workspace labeled bright,
No longer paths of cryptic blight,
From Codex roots to web so fair,
Each project gets its given name to wear,

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 20.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes both main changes: fixing sidebar collapse behavior and implementing Codex project labels support.

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

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

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

@davej davej closed this Feb 22, 2026
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: 2

🧹 Nitpick comments (1)
apps/server/src/index.ts (1)

117-136: Consider caching or async reads for workspace labels.

readFileSync in the /api/agents hot path can block the event loop every refresh; a small cache or async read would reduce tail latency.

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

In `@apps/server/src/index.ts` around lines 117 - 136,
readCodexWorkspaceRootLabels currently uses synchronous fs.readFileSync which
blocks the event loop; change it to a non-blocking, cached implementation:
create an in-memory cache (e.g., module-level variable like workspaceLabelsCache
with a TTL or last-loaded timestamp) and replace readCodexWorkspaceRootLabels
with an async variant (e.g., readCodexWorkspaceRootLabelsAsync) that uses
fs.promises.readFile(CODEX_GLOBAL_STATE_PATH, "utf8") and
CodexGlobalStateSchema.parse(JSON.parse(...)); on read errors return {} and log
with toErrorMessage as before; update any callers (such as the /api/agents hot
path) to await the async function or read from the cache synchronously if fresh
to avoid blocking.
🤖 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/server/src/index.ts`:
- Around line 42-46: The schema CodexGlobalStateSchema currently uses
.passthrough(), allowing unknown keys and bypassing strict validation; replace
.passthrough() with .strict() on the z.object(...) so the schema enforces exact
keys (e.g., "electron-workspace-root-labels") and rejects extra properties,
keeping the symbol CodexGlobalStateSchema and the object definition otherwise
unchanged.

In `@apps/web/src/App.tsx`:
- Around line 589-599: projectLabelsByPath currently assigns labels even if they
are empty/whitespace which leads to blank group names; update the logic that
builds the Map (inside the useMemo that calculates projectLabelsByPath) to trim
the label and only set it when label.trim() is non-empty, otherwise skip setting
so the UI falls back to the basename; apply the same guard wherever
descriptor.projectLabels values are consumed (the other occurrences handling
project labels) to ensure empty or whitespace-only labels are ignored.

---

Nitpick comments:
In `@apps/server/src/index.ts`:
- Around line 117-136: readCodexWorkspaceRootLabels currently uses synchronous
fs.readFileSync which blocks the event loop; change it to a non-blocking, cached
implementation: create an in-memory cache (e.g., module-level variable like
workspaceLabelsCache with a TTL or last-loaded timestamp) and replace
readCodexWorkspaceRootLabels with an async variant (e.g.,
readCodexWorkspaceRootLabelsAsync) that uses
fs.promises.readFile(CODEX_GLOBAL_STATE_PATH, "utf8") and
CodexGlobalStateSchema.parse(JSON.parse(...)); on read errors return {} and log
with toErrorMessage as before; update any callers (such as the /api/agents hot
path) to await the async function or read from the cache synchronously if fresh
to avoid blocking.

Comment thread apps/server/src/index.ts
Comment on lines +42 to +46
const CodexGlobalStateSchema = z
.object({
"electron-workspace-root-labels": z.record(z.string()).default({})
})
.passthrough();
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:

cd /tmp && cat -n apps/server/src/index.ts 2>/dev/null | head -60 || echo "File not found or different path"

Repository: achimala/farfield

Length of output: 43


🌐 Web query:

Zod .strict() method vs .passthrough() difference schema validation

💡 Result:

In Zod object schemas, these methods control what happens to unknown (extra) keys during parsing:

  • .strict(): rejects unknown keys → parse() throws a ZodError (issue code like unrecognized_keys). Use when you want an exact data contract. [1]
  • .passthrough(): keeps unknown keys → they are preserved in the parsed output. Use when you want to accept/forward extra data (e.g., metadata, logging). [1]
  • (For context) default is “strip”: unknown keys are removed from the output unless you change the behavior. [1]

Example:

import { z } from "zod";

const Base = z.object({ name: z.string() });

Base.parse({ name: "A", extra: 1 });              // => { name: "A" }   (default: strip)
Base.passthrough().parse({ name: "A", extra: 1 }); // => { name: "A", extra: 1 }
Base.strict().parse({ name: "A", extra: 1 });      // => throws ZodError

If you want unknown keys allowed but validated, use .catchall(schema) instead of passthrough/strict (it makes “unknown” keys no longer “unknown”—they must match the catchall schema). [1]

Sources:
[1] Zod docs on npm (“Unrecognized keys”, .passthrough, .strict, default stripping, .catchall). https://www.npmjs.com/package/zod/v/3.7.0


🏁 Script executed:

fd -e ts -e tsx | grep -E "(index\.ts|schema)" | head -20

Repository: achimala/farfield

Length of output: 1719


🏁 Script executed:

cat -n apps/server/src/index.ts | head -80

Repository: achimala/farfield

Length of output: 2962


🏁 Script executed:

cat -n apps/server/src/index.ts | sed -n '30,60p'

Repository: achimala/farfield

Length of output: 1171


🏁 Script executed:

rg -A 5 -B 2 "CodexGlobalStateSchema" apps/server/src/index.ts

Repository: achimala/farfield

Length of output: 589


🏁 Script executed:

rg -n "readCodexWorkspaceRootLabels\|CodexGlobalState" apps/server/src/index.ts | head -20

Repository: achimala/farfield

Length of output: 43


Use a strict Zod schema for Codex global state.

The schema uses .passthrough() which allows unknown keys, violating the strict type validation requirement. Replace with .strict() to enforce exact schema compliance.

Proposed fix
const CodexGlobalStateSchema = z
  .object({
    "electron-workspace-root-labels": z.record(z.string()).default({})
  })
-  .passthrough();
+  .strict();
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const CodexGlobalStateSchema = z
.object({
"electron-workspace-root-labels": z.record(z.string()).default({})
})
.passthrough();
const CodexGlobalStateSchema = z
.object({
"electron-workspace-root-labels": z.record(z.string()).default({})
})
.strict();
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/server/src/index.ts` around lines 42 - 46, The schema
CodexGlobalStateSchema currently uses .passthrough(), allowing unknown keys and
bypassing strict validation; replace .passthrough() with .strict() on the
z.object(...) so the schema enforces exact keys (e.g.,
"electron-workspace-root-labels") and rejects extra properties, keeping the
symbol CodexGlobalStateSchema and the object definition otherwise unchanged.

Comment thread apps/web/src/App.tsx
Comment on lines +589 to +599
const projectLabelsByPath = useMemo(() => {
const labels = new Map<string, string>();
for (const descriptor of agentDescriptors) {
for (const [projectPath, label] of Object.entries(descriptor.projectLabels)) {
if (!labels.has(projectPath)) {
labels.set(projectPath, label);
}
}
}
return labels;
}, [agentDescriptors]);
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

Guard against empty project labels to avoid blank group names.

If a label is an empty string or whitespace, the UI will render an empty group label instead of the basename.

🛠️ Suggested hardening
-  const projectLabelsByPath = useMemo(() => {
-    const labels = new Map<string, string>();
-    for (const descriptor of agentDescriptors) {
-      for (const [projectPath, label] of Object.entries(descriptor.projectLabels)) {
-        if (!labels.has(projectPath)) {
-          labels.set(projectPath, label);
-        }
-      }
-    }
-    return labels;
-  }, [agentDescriptors]);
+  const projectLabelsByPath = useMemo(() => {
+    const labels = new Map<string, string>();
+    for (const descriptor of agentDescriptors) {
+      for (const [projectPath, label] of Object.entries(descriptor.projectLabels)) {
+        const normalizedPath = projectPath.trim();
+        const normalizedLabel = label.trim();
+        if (!normalizedPath || !normalizedLabel) continue;
+        if (!labels.has(normalizedPath)) {
+          labels.set(normalizedPath, normalizedLabel);
+        }
+      }
+    }
+    return labels;
+  }, [agentDescriptors]);
-      const label = projectPath
-        ? (projectLabelsByPath.get(projectPath) ?? basenameFromPath(projectPath))
-        : "Unknown";
+      const label = projectPath
+        ? (projectLabelsByPath.get(projectPath) ?? basenameFromPath(projectPath))
+        : "Unknown";
-          label: projectLabelsByPath.get(normalized) ?? basenameFromPath(normalized),
+          label: projectLabelsByPath.get(normalized) ?? basenameFromPath(normalized),

Also applies to: 616-618, 655-656

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

In `@apps/web/src/App.tsx` around lines 589 - 599, projectLabelsByPath currently
assigns labels even if they are empty/whitespace which leads to blank group
names; update the logic that builds the Map (inside the useMemo that calculates
projectLabelsByPath) to trim the label and only set it when label.trim() is
non-empty, otherwise skip setting so the UI falls back to the basename; apply
the same guard wherever descriptor.projectLabels values are consumed (the other
occurrences handling project labels) to ensure empty or whitespace-only labels
are ignored.

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