feat: Fly Code — Jira, SECDESK, standup, command tray & security hardening#1464
feat: Fly Code — Jira, SECDESK, standup, command tray & security hardening#1464tryan-mfly wants to merge 2 commits intopingdotgg:mainfrom
Conversation
…ow features Fork of t3code extended with a complete developer workstation platform: **Contracts & Model (Wave 1-2):** - Extended OrchestrationProject with 14 Jira metadata fields - 3 new commands (jira-metadata.update, note.update, touch) + events - Decider, projector, and schemas handle new event types - 4 new SQLite migrations (projects metadata, prompt history, Jira cache, specs) - Web types and store hydration updated **External Services (Wave 3):** - Jira service: REST API with ~/.netrc auth, ADF text extraction, ticket CRUD - Gmail service: gogcli CLI wrapper with email categorization (ACTION/REVIEW/FYI/NOISE) - Calendar service: gcalcli CLI wrapper with TSV agenda parsing - Full WS API wiring for all services (contracts → server → web) **UI Features (Wave 4):** - Sidebar: Jira ticket badges, status indicators, priority markers - Sidebar: Per-project notes (persisted via events) and scratchpad (localStorage) - Sidebar: Prompt history with click-to-resend - Command Tray: Customizable bottom bar with /update, /triage, /commit presets - Copy Terminal Output: Hover overlay button on terminal panes - PR Creation Modal: 3-step flow (review → create → Jira comment) - Spec/Planning Editor: Per-project textarea with debounced auto-save **Orchestrated Workflows (Wave 5):** - Context seeding: Auto-generates CLAUDE.md from Jira ticket data - /update skill: Aggregates Jira, Gmail, Calendar for daily status reports - /triage skill: Scans all channels, produces prioritized action list 36 files (18 modified, 18 new), 846 lines added. All 521 tests pass. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…d tray, and security hardening Features: - Rebrand from T3 Code to Fly Code - Jira ticket import with auto-generated CLAUDE.md, summary.md, and agent context - Auto-transition imported tickets to "In Progress" - Filter Jira dropdown to exclude tickets with active projects - SECDESK (JSM) ticket creation with bi-directional Jira linking - SECDESK link persistence and auto-detection from summary.md - Sidebar shows clickable SECDESK and Jira links per project - Active/Waiting project status with context menu toggle - Interactive daily standup message generator with copy-to-clipboard - Side-by-side ticket comparison modal - Command tray with configurable chat/terminal target routing - AWS SSO login button (sends directly to terminal) - Workflow rules in CLAUDE.md (IaC, PRs only) - Bitbucket API scaffolding and credentials setup - Backfill migration for pre-existing project ticket keys - Feature overview PDF with Playwright screenshots Security & Stability (12 fixes): - Auth token removed from terminal process environments - Centralized credential handling (eliminated 3 inline .netrc readers) - Thread deletion no longer crashes server (Effect.catchCause for interrupts) - SQLite busy_timeout prevents write contention under concurrent load - Jira import rolls back on partial failure with specific error messages - Forked event streams log errors instead of dying silently - Checkpoint revert failures now surface as warnings - SECDESK follow-up failures show warning toasts - WebSocket listener errors logged for diagnostics - Documented DrainableWorker serial safety for provider options maps Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
Important Review skippedAuto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Repository UI Review profile: CHILL Plan: Pro Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 5 potential issues.
Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
| title: body.title, | ||
| start: body.start, | ||
| }); | ||
| } |
There was a problem hiding this comment.
Duplicate switch cases make handlers unreachable
Medium Severity
The WS_METHODS.gmailSearch, WS_METHODS.gmailMarkRead, WS_METHODS.gmailCreateDraft, WS_METHODS.calendarAgenda, and WS_METHODS.calendarMeetingPrep cases appear twice in the same switch statement. The second set (lines 1091–1130) is unreachable dead code since the first set (lines 995–1032) always matches first. This looks like an accidental copy-paste duplication.
Additional Locations (1)
| import { GmailError } from "../Errors.ts"; | ||
| import type { GmailMessage } from "@t3tools/contracts"; | ||
|
|
||
| const ACCOUNT = "tryan@mediafly.com"; |
There was a problem hiding this comment.
Hardcoded personal email in service code
High Severity
A personal email address tryan@mediafly.com is hardcoded as the ACCOUNT constant in GmailService.ts and repeated in ServiceHealth.ts. This makes the Gmail integration only work for a single developer and exposes personal information in the codebase. It needs to be configurable (e.g., via environment variable or settings).
Additional Locations (1)
| yield* runGogcli( | ||
| `gmail drafts create --account ${ACCOUNT} --to "${to}" --subject "${subject}" --body "${body}"${replyArg}`, | ||
| ); | ||
| return { id: crypto.randomUUID() }; |
There was a problem hiding this comment.
Shell command injection via unsanitized user input
High Severity
User-provided strings (query, threadId, to, subject, body) from WebSocket requests are interpolated directly into shell commands via execSync. Double-quoting does not prevent shell injection — backticks and $() constructs within double quotes still execute. For example, a query containing $(malicious_command) would be executed by the shell. The same issue exists in CalendarService.ts where dateArg is unquoted.
| yield* sql` | ||
| UPDATE projection_projects | ||
| SET ticket_key = SUBSTR(title, 1, INSTR(title, ':') - 1), | ||
| jira_url = 'https://mediafly.atlassian.net/browse/' || SUBSTR(title, 1, INSTR(title, ':') - 1) |
There was a problem hiding this comment.
Hardcoded Jira URL in database migration
Medium Severity
The backfill migration hardcodes https://mediafly.atlassian.net/browse/ directly in a SQL UPDATE statement. Unlike JiraService.ts which reads JIRA_BASE_URL from an environment variable, this migration permanently writes organization-specific URLs into the database with no way to configure it for other environments.
| `machine\\s+${host.replace(/\./g, "\\.")}\\s+login\\s+(\\S+)\\s+password\\s+(\\S+)`, | ||
| ); | ||
| const match = content.match(machineRegex); | ||
| if (match) return { login: match[1]!, password: match[2]! }; |
There was a problem hiding this comment.
Jira netrc parser fails on multi-line format
Medium Severity
The readNetrcCredentials function in JiraService.ts uses a single regex that expects machine, login, and password on consecutive whitespace-separated tokens. A standard .netrc file typically has these on separate lines. The Bitbucket version in BitbucketApi.ts correctly parses a line-by-line format. The Jira regex will fail to match most real .netrc files where fields are on separate lines with varying indentation.
|
whoops, claude sent this to the wrong repo |
| function runGcalcli(args: string): Effect.Effect<string, CalendarError> { | ||
| return Effect.try({ | ||
| try: () => execSync(`gcalcli ${args}`, { encoding: "utf-8", timeout: 30000 }), | ||
| catch: (e) => new CalendarError({ message: `gcalcli failed: ${e}` }), | ||
| }); | ||
| } |
There was a problem hiding this comment.
🟡 Medium Layers/CalendarService.ts:7
runGcalcli interpolates args directly into a shell command string passed to execSync, so malicious input like "; rm -rf /" executes arbitrary shell commands. Pass args as an array to avoid shell interpretation.
+function runGcalcli(args: string[]): Effect.Effect<string, CalendarError> {
+ return Effect.try({
+ try: () => execSync("gcalcli", args, { encoding: "utf-8", timeout: 30000 }),
catch: (e) => new CalendarError({ message: `gcalcli failed: ${e}` }),
});
}🤖 Copy this AI Prompt to have your agent fix this:
In file apps/server/src/calendar/Layers/CalendarService.ts around lines 7-12:
`runGcalcli` interpolates `args` directly into a shell command string passed to `execSync`, so malicious input like `"; rm -rf /"` executes arbitrary shell commands. Pass `args` as an array to avoid shell interpretation.
Evidence trail:
apps/server/src/calendar/Layers/CalendarService.ts lines 7-10: `runGcalcli` function definition showing `execSync(\`gcalcli ${args}\`, ...)` pattern. Line 44 shows `runGcalcli(\`agenda ${dateArg} --tsv\`)` where `dateArg` comes from the `date` function parameter.
| createDraft: ({ to, subject, body, replyToMessageId }) => | ||
| Effect.gen(function* () { | ||
| const replyArg = replyToMessageId ? ` --reply-to-message-id "${replyToMessageId}"` : ""; | ||
| yield* runGogcli( | ||
| `gmail drafts create --account ${ACCOUNT} --to "${to}" --subject "${subject}" --body "${body}"${replyArg}`, | ||
| ); | ||
| return { id: crypto.randomUUID() }; | ||
| }), | ||
| }), |
There was a problem hiding this comment.
🟢 Low Layers/GmailService.ts:83
createDraft returns crypto.randomUUID() instead of the actual draft ID from gogcli. Callers receive a fake ID that doesn't correspond to any Gmail draft, so subsequent operations by ID will fail. Consider parsing the gogcli output to extract and return the real draft ID.
createDraft: ({ to, subject, body, replyToMessageId }) =>
Effect.gen(function* () {
const replyArg = replyToMessageId ? ` --reply-to-message-id "${replyToMessageId}"` : "";
- yield* runGogcli(
+ const output = yield* runGogcli(
`gmail drafts create --account ${ACCOUNT} --to "${to}" --subject "${subject}" --body "${body}"${replyArg}`,
);
- return { id: crypto.randomUUID() };
+ const draft = JSON.parse(output);
+ return { id: draft.id ?? draft.draftId ?? "" };
}),🤖 Copy this AI Prompt to have your agent fix this:
In file apps/server/src/gmail/Layers/GmailService.ts around lines 83-91:
`createDraft` returns `crypto.randomUUID()` instead of the actual draft ID from `gogcli`. Callers receive a fake ID that doesn't correspond to any Gmail draft, so subsequent operations by ID will fail. Consider parsing the `gogcli` output to extract and return the real draft ID.
Evidence trail:
apps/server/src/gmail/Layers/GmailService.ts lines 79-86 at REVIEWED_COMMIT - shows `createDraft` implementation that runs `gogcli gmail drafts create` but ignores its output and returns `{ id: crypto.randomUUID() }` instead of parsing the actual draft ID from the command output.
| useEffect(() => { | ||
| ensureNativeApi() | ||
| .spec.get({ projectId }) | ||
| .then((spec) => { | ||
| if (spec) setContent(spec.content); | ||
| setLoaded(true); | ||
| }) | ||
| .catch(() => setLoaded(true)); | ||
| }, [projectId]); |
There was a problem hiding this comment.
🟢 Low components/SpecEditor.tsx:17
When projectId changes, the load effect fetches new spec data but does not cancel or ignore in-flight requests from the previous projectId. If an old request completes after the new one, setContent overwrites the editor with stale data from the wrong project. Consider adding a cancellation mechanism (e.g., an aborted flag set by the cleanup function) to ignore stale responses.
// Load spec on mount
useEffect(() => {
+ let aborted = false;
ensureNativeApi()
.spec.get({ projectId })
.then((spec) => {
- if (spec) setContent(spec.content);
+ if (!aborted && spec) setContent(spec.content);
- setLoaded(true);
+ if (!aborted) setLoaded(true);
})
.catch(() => setLoaded(true));
+ return () => { aborted = true; };
}, [projectId]);Also found in 1 other location(s)
apps/web/src/components/chat/AuthStatusIndicators.tsx:25
Race condition in
loginProviderallows concurrent login attempts. The checkif (!api || loggingIn) return;on line 25 readsloggingInfrom the closure captured at render time, not the current value. SincesetLoggingIn(provider)on line 26 schedules an async state update, rapid clicks before React re-renders will each seeloggingInasnulland proceed to callapi.provider.login()multiple times concurrently.
🤖 Copy this AI Prompt to have your agent fix this:
In file apps/web/src/components/SpecEditor.tsx around lines 17-25:
When `projectId` changes, the load effect fetches new spec data but does not cancel or ignore in-flight requests from the previous `projectId`. If an old request completes after the new one, `setContent` overwrites the editor with stale data from the wrong project. Consider adding a cancellation mechanism (e.g., an `aborted` flag set by the cleanup function) to ignore stale responses.
Evidence trail:
apps/web/src/components/SpecEditor.tsx lines 17-24 at REVIEWED_COMMIT - useEffect with projectId dependency makes async call via ensureNativeApi().spec.get({ projectId }), calls setContent in .then() callback, has no cleanup function returning an abort flag or using AbortController
Also found in 1 other location(s):
- apps/web/src/components/chat/AuthStatusIndicators.tsx:25 -- Race condition in `loginProvider` allows concurrent login attempts. The check `if (!api || loggingIn) return;` on line 25 reads `loggingIn` from the closure captured at render time, not the current value. Since `setLoggingIn(provider)` on line 26 schedules an async state update, rapid clicks before React re-renders will each see `loggingIn` as `null` and proceed to call `api.provider.login()` multiple times concurrently.
| ...(row.suggestedRepo != null ? { suggestedRepo: row.suggestedRepo } : {}), | ||
| ...(row.note != null ? { note: row.note } : {}), | ||
| ...(row.lastAccessedAt != null ? { lastAccessedAt: row.lastAccessedAt } : {}), | ||
| ...(row.archivedAt !== undefined ? { archivedAt: row.archivedAt } : {}), |
There was a problem hiding this comment.
🟢 Low Layers/ProjectionSnapshotQuery.ts:572
Line 572 uses !== undefined for archivedAt, which incorrectly spreads { archivedAt: null } into the output when the database value is SQL NULL. All other optional fields like lastAccessedAt use != null to exclude both null and undefined. Consider changing to != null for consistency.
| ...(row.archivedAt !== undefined ? { archivedAt: row.archivedAt } : {}), | |
| ...(row.archivedAt != null ? { archivedAt: row.archivedAt } : {}), |
🤖 Copy this AI Prompt to have your agent fix this:
In file apps/server/src/orchestration/Layers/ProjectionSnapshotQuery.ts around line 572:
Line 572 uses `!== undefined` for `archivedAt`, which incorrectly spreads `{ archivedAt: null }` into the output when the database value is SQL NULL. All other optional fields like `lastAccessedAt` use `!= null` to exclude both `null` and `undefined`. Consider changing to `!= null` for consistency.
Evidence trail:
apps/server/src/orchestration/Layers/ProjectionSnapshotQuery.ts lines 559-572 at REVIEWED_COMMIT - Shows 13 optional fields using `!= null` pattern (lines 559-571) and `archivedAt` using `!== undefined` (line 572).
| const collectStreamAsString = <E>(stream: Stream.Stream<Uint8Array, E>): Effect.Effect<string, E> => | ||
| Stream.runFold( | ||
| stream, | ||
| () => "", | ||
| (acc, chunk) => acc + new TextDecoder().decode(chunk), | ||
| ); |
There was a problem hiding this comment.
Layers/ServiceHealth.ts:24
Creating a new TextDecoder for each chunk splits multi-byte UTF-8 characters that span chunk boundaries, causing corruption (e.g., "€" becomes replacement characters). The collectStreamAsString helper instantiates TextDecoder inside the fold step, so every chunk boundary risks data loss for non-ASCII output.
-const collectStreamAsString = <E>(stream: Stream.Stream<Uint8Array, E>): Effect.Effect<string, E> =>
- Stream.runFold(
- stream,
- () => "",
- (acc, chunk) => acc + new TextDecoder().decode(chunk),
- );🤖 Copy this AI Prompt to have your agent fix this:
In file apps/server/src/provider/Layers/ServiceHealth.ts around lines 24-29:
Creating a new `TextDecoder` for each chunk splits multi-byte UTF-8 characters that span chunk boundaries, causing corruption (e.g., "€" becomes replacement characters). The `collectStreamAsString` helper instantiates `TextDecoder` inside the fold step, so every chunk boundary risks data loss for non-ASCII output.
Evidence trail:
apps/server/src/provider/Layers/ServiceHealth.ts lines 24-28 at REVIEWED_COMMIT show `new TextDecoder().decode(chunk)` being called inside the Stream.runFold step function, creating a fresh decoder for each chunk. TextDecoder default behavior (stream: false) outputs replacement characters for incomplete multi-byte sequences at chunk boundaries. See MDN TextDecoder documentation on the `stream` option.
| sections.push("## Inbox Summary"); | ||
| sections.push(`- ${unreadEmails.length} unread emails (${actionEmails} action, ${noiseEmails} noise)`); | ||
| sections.push(`- ${tickets.length} Jira tickets`); | ||
| sections.push(`- ${realMeetings.length} meetings today, ${tomorrowMeetings.length} tomorrow`); |
There was a problem hiding this comment.
🟢 Low skills/triageSkill.ts:222
meetingsToday excludes all-day events (filtered at line 83), but meetingsTomorrow includes them. This causes inconsistent counts: all-day events appear in tomorrow's stats (line 236) and markdown summary (line 222) but not today's. Apply the same !m.isAllDay filter to tomorrowMeetings for consistency.
- sections.push(`- ${realMeetings.length} meetings today, ${tomorrowMeetings.length} tomorrow`);
+ const realMeetingsTomorrow = tomorrowMeetings.filter((m) => !m.isAllDay);
+ sections.push(`- ${realMeetings.length} meetings today, ${realMeetingsTomorrow.length} tomorrow`);🤖 Copy this AI Prompt to have your agent fix this:
In file apps/server/src/skills/triageSkill.ts around line 222:
`meetingsToday` excludes all-day events (filtered at line 83), but `meetingsTomorrow` includes them. This causes inconsistent counts: all-day events appear in tomorrow's stats (line 236) and markdown summary (line 222) but not today's. Apply the same `!m.isAllDay` filter to `tomorrowMeetings` for consistency.
Evidence trail:
apps/server/src/skills/triageSkill.ts lines 43-49 (REVIEWED_COMMIT): `tomorrowMeetings` fetched without filtering
apps/server/src/skills/triageSkill.ts line 83 (REVIEWED_COMMIT): `realMeetings = todayMeetings.filter((m) => !m.isAllDay)` filters out all-day events for today
apps/server/src/skills/triageSkill.ts line 222 (REVIEWED_COMMIT): Uses `realMeetings.length` for today but `tomorrowMeetings.length` for tomorrow
apps/server/src/skills/triageSkill.ts lines 234-235 (REVIEWED_COMMIT): Stats use `realMeetings.length` for meetingsToday and `tomorrowMeetings.length` for meetingsTomorrow
| useEffect(() => { | ||
| if (!open) return; | ||
| setStep("select"); | ||
| setCopied(false); | ||
| setYesterdayNotes(""); | ||
| setBlockerNotes(""); | ||
| setStandupText(""); | ||
| setEntries( | ||
| projects.map((p) => ({ | ||
| project: p, | ||
| included: true, | ||
| note: "", | ||
| })), | ||
| ); | ||
| }, [open, projects]); |
There was a problem hiding this comment.
🟡 Medium components/StandupModal.tsx:44
The useEffect at lines 44-58 depends on projects, so when the modal is open and projects updates (e.g., from syncServerReadModel), all user input (yesterdayNotes, blockerNotes, entries notes) is reset while the user is typing. This causes active data loss during server updates. Consider depending only on open to reset state when the modal opens, and ignore projects changes after initial load.
- // Reset state when modal opens
- useEffect(() => {
- if (!open) return;
- setStep("select");
- setCopied(false);
- setYesterdayNotes("");
- setBlockerNotes("");
- setStandupText("");
- setEntries(
- projects.map((p) => ({
- project: p,
- included: true,
- note: "",
- })),
- );
- }, [open, projects]);🤖 Copy this AI Prompt to have your agent fix this:
In file apps/web/src/components/StandupModal.tsx around lines 44-58:
The `useEffect` at lines 44-58 depends on `projects`, so when the modal is open and `projects` updates (e.g., from `syncServerReadModel`), all user input (`yesterdayNotes`, `blockerNotes`, `entries` notes) is reset while the user is typing. This causes active data loss during server updates. Consider depending only on `open` to reset state when the modal opens, and ignore `projects` changes after initial load.
Evidence trail:
apps/web/src/components/StandupModal.tsx lines 44-58 (REVIEWED_COMMIT): useEffect depends on [open, projects], and when open is true, it resets yesterdayNotes, blockerNotes, standupText to empty strings and entries to a fresh array. Line 45: `if (!open) return;` - the reset only happens when modal is open. Lines 46-56 show all state being reset including user input fields.
| function parseBitbucketRemoteInfo(url: string | null): BitbucketRemoteInfo | null { | ||
| const trimmed = url?.trim() ?? ""; | ||
| if (trimmed.length === 0) return null; | ||
|
|
||
| // SSH: git@bitbucket.org:workspace/repo.git |
There was a problem hiding this comment.
🟢 Low Layers/GitManager.ts:112
parseBitbucketRemoteInfo returns null for ssh://git@bitbucket.org/workspace/repo.git URLs because the SSH regex only matches the git@host:path format (colon separator). This causes resolveBitbucketRemote to fail silently and breaks Bitbucket PR operations when repos use the ssh:// protocol prefix. Consider adding ssh:// pattern support to match the GitHub parser at line 148.
const trimmed = url?.trim() ?? "";
if (trimmed.length === 0) return null;
+ // SSH protocol: ssh://git@bitbucket.org/workspace/repo.git
+ const sshProtocolMatch =
+ /^ssh:\/\/git@bitbucket\.org\/([^/\s]+)\/([^/\s]+?)(?:\.git)?\/?$/i.exec(trimmed);
+ if (sshProtocolMatch?.[1] && sshProtocolMatch[2]) {
+ return { workspace: sshProtocolMatch[1], repoSlug: sshProtocolMatch[2] };
+ }
+
// SSH: git@bitbucket.org:workspace/repo.git🤖 Copy this AI Prompt to have your agent fix this:
In file apps/server/src/git/Layers/GitManager.ts around lines 112-116:
`parseBitbucketRemoteInfo` returns `null` for `ssh://git@bitbucket.org/workspace/repo.git` URLs because the SSH regex only matches the `git@host:path` format (colon separator). This causes `resolveBitbucketRemote` to fail silently and breaks Bitbucket PR operations when repos use the `ssh://` protocol prefix. Consider adding `ssh://` pattern support to match the GitHub parser at line 148.
Evidence trail:
apps/server/src/git/Layers/GitManager.ts lines 113-131 (parseBitbucketRemoteInfo function) at REVIEWED_COMMIT - SSH regex at line 118-119 only matches `git@bitbucket.org:` format. Lines 148-152 (parseGitHubRepositoryNameWithOwnerFromRemoteUrl function) shows GitHub parser includes `ssh://git@github.com/` pattern support.
| variant={copied ? "default" : "outline"} | ||
| onClick={() => void handleCopy()} |
There was a problem hiding this comment.
🟢 Low components/StandupModal.tsx:262
When navigator.clipboard.writeText fails (e.g., permissions denied), the error is swallowed by the void operator in onClick={() => void handleCopy()}. The promise rejection is unhandled, so setCopied(true) never executes and the user receives no feedback that the copy failed. Consider adding .catch() to show an error state or using try/catch with an error toast.
- onClick={() => void handleCopy()}🤖 Copy this AI Prompt to have your agent fix this:
In file apps/web/src/components/StandupModal.tsx around lines 262-263:
When `navigator.clipboard.writeText` fails (e.g., permissions denied), the error is swallowed by the `void` operator in `onClick={() => void handleCopy()}`. The promise rejection is unhandled, so `setCopied(true)` never executes and the user receives no feedback that the copy failed. Consider adding `.catch()` to show an error state or using `try/catch` with an error toast.
Evidence trail:
apps/web/src/components/StandupModal.tsx lines 146-150 (handleCopy function definition with no try/catch), line 263 (onClick handler with `void handleCopy()` and no .catch())
| function buildTree(entries: ReadonlyArray<ProjectEntry>): TreeNode[] { | ||
| // First pass: determine which paths are excluded (hidden entries and all their descendants) | ||
| const excluded = new Set<string>(); | ||
| for (const entry of entries) { | ||
| const name = entry.path.split("/").pop() ?? entry.path; | ||
| if (isHiddenEntry(name)) excluded.add(entry.path); | ||
| if (entry.parentPath && excluded.has(entry.parentPath)) excluded.add(entry.path); | ||
| } |
There was a problem hiding this comment.
🟢 Low components/FileBrowser.tsx:22
The first pass only excludes children of hidden directories when entries is sorted parent-first. If a child entry (e.g., .git/config) appears before its hidden parent (.git), the check at line 28 fails because the parent hasn't been added to excluded yet. This causes hidden directory contents to leak into the tree when the backend returns entries in arbitrary order.
const excluded = new Set<string>();
- for (const entry of entries) {
- const name = entry.path.split("/").pop() ?? entry.path;
- if (isHiddenEntry(name)) excluded.add(entry.path);
- if (entry.parentPath && excluded.has(entry.parentPath)) excluded.add(entry.path);
- }🤖 Copy this AI Prompt to have your agent fix this:
In file apps/web/src/components/FileBrowser.tsx around lines 22-29:
The first pass only excludes children of hidden directories when `entries` is sorted parent-first. If a child entry (e.g., `.git/config`) appears before its hidden parent (`.git`), the check at line 28 fails because the parent hasn't been added to `excluded` yet. This causes hidden directory contents to leak into the tree when the backend returns entries in arbitrary order.
Evidence trail:
- apps/web/src/components/FileBrowser.tsx lines 22-29: buildTree function with first-pass filtering logic
- apps/server/src/workspaceEntries.ts lines 144-151: compareRankedWorkspaceEntries shows entries sorted by score then path
- apps/server/src/workspaceEntries.ts lines 542-565: searchWorkspaceEntries returns rankedEntries sorted by score
- packages/contracts/src/project.ts lines 16-21: ProjectEntry schema showing parentPath field
|
Hi — this PR was opened against the wrong repo by mistake. Could you please delete it? |


Hi — this PR was opened against the wrong repo by mistake. Could you please delete it?