Skip to content

Add project automation management#155

Open
friuns2 wants to merge 14 commits into
mainfrom
codex/project-automation-sidebar
Open

Add project automation management#155
friuns2 wants to merge 14 commits into
mainfrom
codex/project-automation-sidebar

Conversation

@friuns2
Copy link
Copy Markdown
Owner

@friuns2 friuns2 commented May 10, 2026

Summary

  • add project-scoped cron automation management alongside thread heartbeat automations
  • add top-level Automations panel with combined list, active/newest sorting, counts, and edit actions
  • add sidebar automation indicators and project automation backend/API support

Verification

  • pnpm run build:frontend
  • Playwright: automations panel light/dark edit flow
  • PROFILE_BASE_URL=http://127.0.0.1:5174 PROFILE_ROUTE='#/automations?automationId=ai-blog-pulse' PROFILE_WAIT_MS=7000 pnpm run profile:browser

@qodo-free-for-open-source-projects
Copy link
Copy Markdown

qodo-free-for-open-source-projects Bot commented May 10, 2026

Review Summary by Qodo

(Agentic_describe updated until commit ddd9014)

Add project automation management with combined Automations panel

✨ Enhancement

Grey Divider

Walkthroughs

Description
• Add project-scoped cron automations with sidebar management UI
• Create top-level Automations panel combining thread and project automations
• Implement project automation API endpoints and TOML serialization
• Display automation indicators and counts in sidebar threads and projects
• Sort automations by active status first, then newest creation date
Diagram
flowchart LR
  A["Project/Thread"] -->|"Add automation"| B["Automation Dialog"]
  B -->|"Save"| C["TOML Storage"]
  C -->|"cwds array"| D["Project Cron"]
  C -->|"target_thread_id"| E["Thread Heartbeat"]
  D -->|"GET /project-automations"| F["Automations Panel"]
  E -->|"GET /thread-automations"| F
  F -->|"Display"| G["Combined List"]
  G -->|"Sort: Active→Paused→Newest"| H["Sorted Rows"]
  H -->|"Edit"| B
Loading

Grey Divider

File Changes

1. src/api/codexGateway.ts ✨ Enhancement +51/-0

Add project automation API client functions

src/api/codexGateway.ts


2. src/router/index.ts ✨ Enhancement +5/-0

Add automations route to router

src/router/index.ts


3. src/server/codexAppServerBridge.ts ✨ Enhancement +209/-3

Implement project automation backend and TOML parsing

src/server/codexAppServerBridge.ts


View more (11)
4. src/types/codex.ts ✨ Enhancement +1/-0

Add cwds array to automation type definition

src/types/codex.ts


5. src/App.vue ✨ Enhancement +93/-7

Add Automations sidebar link and panel integration

src/App.vue


6. src/components/content/AutomationsPanel.vue ✨ Enhancement +456/-0

Create new Automations panel component with sorting

src/components/content/AutomationsPanel.vue


7. src/components/sidebar/SidebarThreadTree.vue ✨ Enhancement +218/-24

Add project automation dialog and sidebar indicators

src/components/sidebar/SidebarThreadTree.vue


8. src/style.css ✨ Enhancement +52/-0

Add dark theme styles for Automations panel

src/style.css


9. AGENTS.md 📝 Documentation +3/-0

Require inline screenshot evidence in verification

AGENTS.md


10. llm-wiki/raw/features/project-cron-automations.md 📝 Documentation +14/-0

Document project cron automation implementation

llm-wiki/raw/features/project-cron-automations.md


11. llm-wiki/wiki/concepts/project-cron-automations.md 📝 Documentation +23/-0

Add project cron automations concept documentation

llm-wiki/wiki/concepts/project-cron-automations.md


12. llm-wiki/wiki/index.md 📝 Documentation +2/-0

Update wiki index with project automations links

llm-wiki/wiki/index.md


13. llm-wiki/wiki/log.md 📝 Documentation +6/-0

Add project automation implementation log entry

llm-wiki/wiki/log.md


14. tests.md 🧪 Tests +75/-2

Add Automations panel and project automation tests

tests.md


Grey Divider

Qodo Logo

@qodo-free-for-open-source-projects
Copy link
Copy Markdown

qodo-free-for-open-source-projects Bot commented May 10, 2026

Code Review by Qodo

🐞 Bugs (1) 📘 Rule violations (1) 📎 Requirement gaps (0)

Grey Divider


Action required

1. Multi-cwd selection ambiguity ✓ Resolved 🐞 Bug ≡ Correctness
Description
AutomationsPanel keys and selects rows by automation.id only, but the backend returns the same cron
automation once per cwd when it has multiple cwds. This creates duplicate Vue keys and makes
selection/edit actions ambiguous, so users can end up editing/deleting the wrong project association
for a shared automation.
Code

src/components/content/AutomationsPanel.vue[R29-38]

+        <div
+          v-for="row in automationRows"
+          :key="row.automation.id"
+          class="automation-row"
+          :class="{ 'is-selected': selectedAutomationId === row.automation.id }"
+          role="button"
+          tabindex="0"
+          @click="selectedAutomationId = row.automation.id"
+          @keydown.enter.prevent="selectedAutomationId = row.automation.id"
+          @keydown.space.prevent="selectedAutomationId = row.automation.id"
Evidence
The panel renders each row keyed by automation.id and stores selection as only
selectedAutomationId, while the server groups cron automations by iterating automation.cwds and
inserting the same automation under each cwd bucket. Therefore a multi-cwd cron automation produces
multiple rows with the same id, breaking Vue’s key uniqueness and making selectedRow/edit target
resolution ambiguous.

src/components/content/AutomationsPanel.vue[29-38]
src/components/content/AutomationsPanel.vue[159-205]
src/server/codexAppServerBridge.ts[3403-3420]
src/server/codexAppServerBridge.ts[3472-3484]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`AutomationsPanel` renders and selects rows by `automation.id` only. Multi-cwd cron automations are returned once per cwd, so the UI can render duplicate rows with identical keys and cannot disambiguate which cwd-association is selected/edited.
## Issue Context
The backend explicitly supports multi-cwd cron automations (`cwds` array) and returns them grouped per cwd. The UI needs a stable identity per (scope, target, automationId) association.
## Fix Focus Areas
- src/components/content/AutomationsPanel.vue[29-38]
- src/components/content/AutomationsPanel.vue[159-205]
- src/server/codexAppServerBridge.ts[3403-3420]
## Suggested fix approach
- Introduce a `rowKey` such as `${row.scope}:${row.targetTitle}:${row.automation.id}`.
- Use `:key="rowKey"` in the `v-for`.
- Track selection by `selectedRowKey` (or `{scope,target,id}`) instead of only `automationId`, so clicking a specific project row selects that specific association.
- Keep emitting `automationId` for the route if desired, but resolve `props.selectedAutomationId` to the first matching rowKey deterministically (and consider enhancing routing if you need exact association selection).

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. tests.md cases removed 📘 Rule violation ⚙ Maintainability
Description
This PR deletes/replaces existing manual test sections in tests.md, reducing historical regression
coverage. Compliance requires preserving existing test cases and only appending or minimally
updating what’s needed.
Code

tests.md[L274-301]

-### Pinned threads remain visible during background pagination
+### Feature: Project automations from the sidebar project menu
#### Feature/Change Name
-Pinned threads are no longer removed from the Pinned section while the sidebar is still loading older thread-list pages.
+Project rows can create and manage project-scoped cron automations using the same dialog style as thread heartbeat automations.
#### Prerequisites/Setup
1. Dev server running (`pnpm run dev`)
-2. More than 50 total unarchived threads exist
-3. At least one older thread outside the initial recent page is pinned
-4. Light theme and dark theme both available from the appearance switcher
+2. Sidebar contains at least one project folder row
+3. Light theme and dark theme both available from the appearance switcher
#### Steps
-1. In light theme, reload the app.
-2. Immediately open the sidebar Pinned section.
-3. Confirm pinned rows from older history remain in the Pinned section after the initial thread list appears.
-4. Wait for background thread pagination to finish.
-5. Confirm the same pinned rows remain visible and can still be selected.
-6. Switch to dark theme and repeat steps 1-5.
+1. In light theme, open a project row dots menu for a project without an attached project automation.
+2. Confirm the menu shows `Add automation…` between `Browse files` and Git/project actions.
+3. Click `Add automation…`.
+4. Fill a name, prompt, daily or interval schedule, and status, then save.
+5. Inspect `$CODEX_HOME/automations/<automation-id>/automation.toml` and confirm the `cwds` entry is the resolved absolute project folder path, not only the sidebar display label.
+6. Reopen the same project menu and confirm it now shows `Manage automations…`.
+7. Confirm the project row shows an automation icon.
+8. Open `Manage automations…`, confirm the saved values are prefilled, then click `Add another automation`.
+9. Save a second project automation and confirm both records appear in the dialog list independently.
+10. Confirm the project row automation chip shows the bolt icon plus the visible count `2`.
+11. Repeat the same two-automation check on a chat thread and confirm the thread row chip shows the bolt icon plus the visible count `2`.
+12. Remove one project automation and confirm the project row chip returns to icon-only for one remaining automation.
+13. Remove the final project automation and confirm the project menu returns to `Add automation…` and the project row automation icon disappears.
+14. Switch to dark theme and repeat steps 1-11.
#### Expected Results
-- Saved pinned thread IDs are preserved while only the initial thread-list page is loaded.
-- Missing pinned IDs are pruned only after the full thread list has loaded.
-- Pinned rows remain readable and selectable in both light and dark themes.
+- Project automations are stored as cron automations with `cwds = ["<absolute project path>"]`, including when the sidebar row displays only the project folder name.
+- Thread heartbeat automations remain scoped by `target_thread_id` and continue to use `Run now`.
+- Project automation dialogs use project-specific copy and do not show `Run now`.
+- The project menu, dialog list, inputs, schedule controls, status selector, automation icon, and multi-automation count badge remain readable in light and dark themes.
#### Rollback/Cleanup
-- Unpin any disposable threads created only for this test.
+- Remove test project automations from the project automation dialog or delete their folders under `$CODEX_HOME/automations/<automation-id>/`.
---
Evidence
PR Compliance ID 3 requires that existing tests.md test cases are retained and only minimally
updated. The diff shows an existing section being replaced (starting at the old header around line
274) and another full feature section being removed entirely (old lines ~1695-1727).

AGENTS.md
tests.md[274-301]
tests.md[1695-1727]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
This PR removes existing `tests.md` manual test cases (replacing one section and deleting another), which reduces historical regression coverage.
## Issue Context
Per compliance, `tests.md` updates must be additive or minimally edited for the new feature; existing cases should not be removed unless there is a very strong reason and an equivalent replacement is kept.
## Fix Focus Areas
- tests.md[274-301]
- tests.md[1695-1727]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


3. Multi-cwd cron data loss 🐞 Bug ≡ Correctness
Description
Project cron automations are indexed under every entry in cwds, but saving overwrites cwds to a
single value and deleting removes the entire automation folder; a cron automation associated with
multiple projects can be unintentionally modified/removed for other projects. This can lead to
cross-project automation loss when editing or deleting from a single project (including project
removal cleanup).
Code

src/server/codexAppServerBridge.ts[R3403-3507]

+async function listProjectCronAutomations(): Promise<Record<string, ThreadAutomationRecord[]>> {
+  const automationRoot = getCodexAutomationsDir()
+  const next: Record<string, ThreadAutomationRecord[]> = {}
+  let entries
+  try {
+    entries = await readdir(automationRoot, { withFileTypes: true })
+  } catch {
+    return next
+  }
+
+  for (const entry of entries) {
+    if (!entry.isDirectory()) continue
+    const automation = await readAutomationRecordFromFile(join(automationRoot, entry.name, 'automation.toml'))
+    if (!automation || automation.kind !== 'cron' || automation.cwds.length === 0) continue
+    for (const cwd of automation.cwds) {
+      next[cwd] = [...(next[cwd] ?? []), automation]
+    }
+  }
+
+  for (const automations of Object.values(next)) {
+    automations.sort((first, second) => {
+      const firstCreatedAt = first.createdAtMs ?? 0
+      const secondCreatedAt = second.createdAtMs ?? 0
+      if (firstCreatedAt !== secondCreatedAt) return firstCreatedAt - secondCreatedAt
+      return first.id.localeCompare(second.id)
+    })
+  }
+
+  return next
+}
+
+async function readProjectCronAutomations(projectName: string): Promise<ThreadAutomationRecord[]> {
+  const all = await listProjectCronAutomations()
+  return all[projectName] ?? []
+}
+
+async function readProjectCronAutomation(projectName: string, automationId = ''): Promise<ThreadAutomationRecord | null> {
+  const automations = await readProjectCronAutomations(projectName)
+  if (automationId) return automations.find((automation) => automation.id === automationId) ?? null
+  return automations[0] ?? null
+}
+
+async function writeProjectCronAutomation(input: {
+  projectName: string
+  id?: string
+  name: string
+  prompt: string
+  rrule: string
+  status: ThreadAutomationStatus
+}): Promise<ThreadAutomationRecord> {
+  const projectName = input.projectName.trim()
+  const name = input.name.trim()
+  const prompt = input.prompt.trim()
+  const rrule = input.rrule.trim()
+  if (!projectName || !name || !prompt || !rrule) {
+    throw new Error('projectName, name, prompt, and rrule are required')
+  }
+
+  const automationRoot = getCodexAutomationsDir()
+  await mkdir(automationRoot, { recursive: true })
+  const existing = input.id ? await readProjectCronAutomation(projectName, input.id.trim()) : null
+  const entries = await readdir(automationRoot, { withFileTypes: true }).catch(() => [])
+  const existingIds = new Set(entries.filter((entry) => entry.isDirectory()).map((entry) => entry.name))
+  const id = existing?.id ?? resolveUniqueAutomationId(existingIds, projectName, name)
+  const automationDir = join(automationRoot, id)
+  const now = Date.now()
+  const record: ThreadAutomationRecord = {
+    id,
+    kind: 'cron',
+    name,
+    prompt,
+    rrule,
+    status: input.status,
+    targetThreadId: null,
+    cwds: [projectName],
+    createdAtMs: existing?.createdAtMs ?? now,
+    updatedAtMs: now,
+    nextRunAtMs: null,
+  }
+
+  await mkdir(automationDir, { recursive: true })
+  await writeFile(join(automationDir, 'automation.toml'), serializeAutomationToml(record), 'utf8')
+  const memoryPath = join(automationDir, 'memory.md')
+  try {
+    await stat(memoryPath)
+  } catch {
+    await writeFile(memoryPath, '', 'utf8')
+  }
+  return record
+}
+
+async function deleteProjectCronAutomation(projectName: string, automationId = ''): Promise<boolean> {
+  const normalizedProjectName = projectName.trim()
+  const normalizedAutomationId = automationId.trim()
+  if (normalizedAutomationId) {
+    const automation = await readProjectCronAutomation(normalizedProjectName, normalizedAutomationId)
+    if (!automation) return false
+    await rm(join(getCodexAutomationsDir(), automation.id), { recursive: true, force: true })
+    return true
+  }
+
+  const automations = await readProjectCronAutomations(normalizedProjectName)
+  if (automations.length === 0) return false
+  await Promise.all(automations.map((automation) => rm(join(getCodexAutomationsDir(), automation.id), { recursive: true, force: true })))
+  return true
Evidence
The server groups one cron automation under multiple keys (one per cwd) but delete removes the
underlying directory by automation id, and save always writes cwds: [projectName], which will
either delete a shared automation for all cwds or drop other cwds on edit. Additionally, the sidebar
auto-deletes all project automations on project removal, which can trigger the same cross-project
deletion for shared automations.

src/server/codexAppServerBridge.ts[3403-3420]
src/server/codexAppServerBridge.ts[3469-3481]
src/server/codexAppServerBridge.ts[3494-3508]
src/components/sidebar/SidebarThreadTree.vue[2120-2126]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Project cron automations can legally have multiple `cwds`, but the current implementation (a) overwrites `cwds` to a single entry on save and (b) deletes the entire automation directory when removing an automation for a single cwd. This causes silent cross-project data loss for shared/multi-cwd cron automations.
### Issue Context
- `listProjectCronAutomations()` indexes the same automation under every cwd in `cwds`.
- Deleting by cwd currently removes the automation directory (`rm .../<automation.id>`), affecting all cwd associations.
- Saving an existing cron automation always writes `cwds: [projectName]`, dropping other cwd associations.
### Fix Focus Areas
- src/server/codexAppServerBridge.ts[3403-3432]
- src/server/codexAppServerBridge.ts[3445-3492]
- src/server/codexAppServerBridge.ts[3494-3508]
- src/components/sidebar/SidebarThreadTree.vue[2120-2126]
### Implementation direction
- Decide on supported behavior:
1) If multi-cwd cron automations are supported:
- On delete for one cwd: if `automation.cwds.length > 1`, rewrite `automation.toml` with that cwd removed (and only delete the directory if no cwds remain).
- On save/edit: preserve existing `cwds` (or at minimum, preserve all existing cwds and ensure the current cwd remains included).
2) If multi-cwd is *not* supported:
- Enforce it explicitly: reject/ignore cron automations with `cwds.length !== 1` in project-scoped APIs, and avoid deleting directories that are shared across multiple cwds.
- Ensure sidebar “remove project” cleanup does not delete shared automations (same rule as above).

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

4. Multi-cwd cache staleness ✓ Resolved 🐞 Bug ☼ Reliability
Description
SidebarThreadTree updates the project-automation cache only for the currently edited cwd key after a
save/delete. For a cron automation shared across multiple cwds, other project rows keep stale copies
(name/status/tooltips) until a full reload.
Code

src/components/sidebar/SidebarThreadTree.vue[R1917-1924]

+    const saved = automationDialogScope.value === 'project'
+      ? await upsertProjectAutomation({ ...input, projectName })
+      : await upsertThreadAutomation({ ...input, threadId })
+    if (automationDialogScope.value === 'project') {
+      automationByProjectName.value = updateAutomationForProject(automationByProjectName.value, projectName, saved)
+    } else {
+      automationByThreadId.value = updateAutomationForThread(automationByThreadId.value, threadId, saved)
+    }
Evidence
The server preserves existing cwds when saving a project automation, so one automation can
legitimately appear under multiple project cwd buckets. The sidebar cache update path only replaces
the automation under the one projectName key being edited, leaving other keys that also include
the same automation id unchanged.

src/components/sidebar/SidebarThreadTree.vue[1871-1889]
src/components/sidebar/SidebarThreadTree.vue[1900-1925]
src/server/codexAppServerBridge.ts[3472-3484]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
After saving/deleting a project cron automation, the sidebar updates `automationByProjectName` only for the single edited cwd key. If the automation has multiple `cwds`, other project entries continue to show stale automation objects.
## Issue Context
`writeProjectCronAutomation` preserves/extends the automation’s `cwds`, so the same automation id can exist under multiple cwd buckets.
## Fix Focus Areas
- src/components/sidebar/SidebarThreadTree.vue[1871-1889]
- src/components/sidebar/SidebarThreadTree.vue[1900-1960]
- src/server/codexAppServerBridge.ts[3472-3484]
## Suggested fix approach
Implement one of:
- After any project automation save/delete, reload project automations from the server: `automationByProjectName.value = await getProjectAutomationMap()` (project-scope only).
- Or, when you receive `saved`, update/replace that automation in *every* `automationByProjectName` bucket that contains the same `automation.id` (and similarly remove it from the specific bucket on delete, while updating remaining buckets if the record changed).

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


5. Non-absolute cwd stored ✓ Resolved 🐞 Bug ☼ Reliability
Description
The sidebar’s project automation key falls back to projectName when projectCwdByName has no
entry, and that value is sent to the project automation API and persisted into cwds. This can
store a non-absolute cwd (e.g., a display label) despite the repo’s own
verification/expected-results requiring an absolute project path in cwds.
Code

src/components/sidebar/SidebarThreadTree.vue[R2130-2132]

+function getProjectAutomationKey(projectName: string): string {
+  return props.projectCwdByName[projectName]?.trim() || projectName.trim()
+}
Evidence
projectCwdByName is built only from non-empty resolved cwd values, so some projects may have no
mapping; getProjectAutomationKey() then falls back to the project name, and
submitAutomationDialog() sends that value as projectName to the server. The server writes `cwds:
[projectName], and the repo’s test plan explicitly expects cwds` to be the resolved absolute path
(not the sidebar label).

src/App.vue[2396-2402]
src/components/sidebar/SidebarThreadTree.vue[2130-2132]
src/components/sidebar/SidebarThreadTree.vue[1752-1761]
src/components/sidebar/SidebarThreadTree.vue[1891-1910]
src/server/codexAppServerBridge.ts[3469-3481]
tests.md[315-333]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Project cron automations are supposed to persist an absolute project path in `cwds`, but the sidebar can fall back to using the project display/name string when it cannot resolve a cwd. That fallback value is persisted to `cwds`, violating the expected storage shape.
### Issue Context
- `projectCwdByName` omits projects with empty cwd.
- `getProjectAutomationKey()` falls back to `projectName.trim()`.
- Project automation save sends this key as `projectName`, and the server persists it into `cwds`.
### Fix Focus Areas
- src/App.vue[2396-2402]
- src/components/sidebar/SidebarThreadTree.vue[1752-1761]
- src/components/sidebar/SidebarThreadTree.vue[1891-1910]
- src/components/sidebar/SidebarThreadTree.vue[2130-2132]
- src/server/codexAppServerBridge.ts[3469-3481]
- tests.md[315-333]
### Implementation direction
- Change `getProjectAutomationKey()` to return an empty string when `projectCwdByName[projectName]` is missing, and block opening/saving the project automation dialog with a clear UI error.
- Alternatively, ensure `projectCwdByName` always has a correct absolute cwd for every project row (e.g., by resolving workspace root consistently) so the fallback is never used.
- Consider renaming variables (`projectName` -> `projectCwd`) in client/server API payloads to prevent future misuse.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Advisory comments

6. Duplicated dark-mode CSS ✓ Resolved 🐞 Bug ⚙ Maintainability
Description
Automations panel dark-theme rules are defined both in AutomationsPanel.vue and again in global
src/style.css, increasing maintenance cost and the risk of future divergence. This duplication is
unnecessary because both blocks target the same selectors.
Code

src/style.css[R1653-1699]

+:root.dark .automations-toolbar {
+  @apply border-zinc-800;
+}
+
+:root.dark .automations-summary span,
+:root.dark .automation-row-icon[data-status='PAUSED'],
+:root.dark .automation-detail-icon[data-status='PAUSED'] {
+  @apply bg-zinc-800 text-zinc-400;
+}
+
+:root.dark .automations-refresh,
+:root.dark .automations-list,
+:root.dark .automation-detail {
+  @apply border-zinc-800 bg-zinc-950 text-zinc-100;
+}
+
+:root.dark .automations-refresh:hover,
+:root.dark .automation-row:hover,
+:root.dark .automation-row.is-selected {
+  @apply bg-zinc-900;
+}
+
+:root.dark .automation-row {
+  @apply border-zinc-800;
+}
+
+:root.dark .automation-row-title,
+:root.dark .automation-detail-title-wrap h2,
+:root.dark .automation-detail-grid dd,
+:root.dark .automations-empty p {
+  @apply text-zinc-100;
+}
+
+:root.dark .automation-row-meta,
+:root.dark .automation-row-schedule,
+:root.dark .automation-detail-title-wrap span,
+:root.dark .automation-detail-grid dt,
+:root.dark .automation-detail-prompt h3,
+:root.dark .automations-summary,
+:root.dark .automations-empty {
+  @apply text-zinc-400;
+}
+
+:root.dark .automation-detail-grid div,
+:root.dark .automation-detail-prompt p {
+  @apply bg-zinc-900 text-zinc-200;
+}
Evidence
The same dark-mode selector set is present in both the component and the global stylesheet, meaning
future edits must be kept in sync across two locations.

src/components/content/AutomationsPanel.vue[456-506]
src/style.css[1653-1699]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Dark-mode styling for Automations panel is duplicated in two places, which is easy to let drift.
### Issue Context
- `AutomationsPanel.vue` includes `:global(:root.dark) ...` rules.
- `src/style.css` repeats the same selector set.
### Fix Focus Areas
- src/components/content/AutomationsPanel.vue[456-506]
- src/style.css[1653-1699]
### Implementation direction
- Keep the dark-mode rules in exactly one place:
- Either remove the global `src/style.css` block and keep styles co-located with the component, or
- Remove the component `:global(:root.dark)` block and keep all dark-mode overrides centralized.
- After removal, verify dark theme still renders correctly.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Qodo Logo

Comment on lines +3403 to +3507
async function listProjectCronAutomations(): Promise<Record<string, ThreadAutomationRecord[]>> {
const automationRoot = getCodexAutomationsDir()
const next: Record<string, ThreadAutomationRecord[]> = {}
let entries
try {
entries = await readdir(automationRoot, { withFileTypes: true })
} catch {
return next
}

for (const entry of entries) {
if (!entry.isDirectory()) continue
const automation = await readAutomationRecordFromFile(join(automationRoot, entry.name, 'automation.toml'))
if (!automation || automation.kind !== 'cron' || automation.cwds.length === 0) continue
for (const cwd of automation.cwds) {
next[cwd] = [...(next[cwd] ?? []), automation]
}
}

for (const automations of Object.values(next)) {
automations.sort((first, second) => {
const firstCreatedAt = first.createdAtMs ?? 0
const secondCreatedAt = second.createdAtMs ?? 0
if (firstCreatedAt !== secondCreatedAt) return firstCreatedAt - secondCreatedAt
return first.id.localeCompare(second.id)
})
}

return next
}

async function readProjectCronAutomations(projectName: string): Promise<ThreadAutomationRecord[]> {
const all = await listProjectCronAutomations()
return all[projectName] ?? []
}

async function readProjectCronAutomation(projectName: string, automationId = ''): Promise<ThreadAutomationRecord | null> {
const automations = await readProjectCronAutomations(projectName)
if (automationId) return automations.find((automation) => automation.id === automationId) ?? null
return automations[0] ?? null
}

async function writeProjectCronAutomation(input: {
projectName: string
id?: string
name: string
prompt: string
rrule: string
status: ThreadAutomationStatus
}): Promise<ThreadAutomationRecord> {
const projectName = input.projectName.trim()
const name = input.name.trim()
const prompt = input.prompt.trim()
const rrule = input.rrule.trim()
if (!projectName || !name || !prompt || !rrule) {
throw new Error('projectName, name, prompt, and rrule are required')
}

const automationRoot = getCodexAutomationsDir()
await mkdir(automationRoot, { recursive: true })
const existing = input.id ? await readProjectCronAutomation(projectName, input.id.trim()) : null
const entries = await readdir(automationRoot, { withFileTypes: true }).catch(() => [])
const existingIds = new Set(entries.filter((entry) => entry.isDirectory()).map((entry) => entry.name))
const id = existing?.id ?? resolveUniqueAutomationId(existingIds, projectName, name)
const automationDir = join(automationRoot, id)
const now = Date.now()
const record: ThreadAutomationRecord = {
id,
kind: 'cron',
name,
prompt,
rrule,
status: input.status,
targetThreadId: null,
cwds: [projectName],
createdAtMs: existing?.createdAtMs ?? now,
updatedAtMs: now,
nextRunAtMs: null,
}

await mkdir(automationDir, { recursive: true })
await writeFile(join(automationDir, 'automation.toml'), serializeAutomationToml(record), 'utf8')
const memoryPath = join(automationDir, 'memory.md')
try {
await stat(memoryPath)
} catch {
await writeFile(memoryPath, '', 'utf8')
}
return record
}

async function deleteProjectCronAutomation(projectName: string, automationId = ''): Promise<boolean> {
const normalizedProjectName = projectName.trim()
const normalizedAutomationId = automationId.trim()
if (normalizedAutomationId) {
const automation = await readProjectCronAutomation(normalizedProjectName, normalizedAutomationId)
if (!automation) return false
await rm(join(getCodexAutomationsDir(), automation.id), { recursive: true, force: true })
return true
}

const automations = await readProjectCronAutomations(normalizedProjectName)
if (automations.length === 0) return false
await Promise.all(automations.map((automation) => rm(join(getCodexAutomationsDir(), automation.id), { recursive: true, force: true })))
return true
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Action required

2. Multi-cwd cron data loss 🐞 Bug ≡ Correctness

Project cron automations are indexed under every entry in cwds, but saving overwrites cwds to a
single value and deleting removes the entire automation folder; a cron automation associated with
multiple projects can be unintentionally modified/removed for other projects. This can lead to
cross-project automation loss when editing or deleting from a single project (including project
removal cleanup).
Agent Prompt
### Issue description
Project cron automations can legally have multiple `cwds`, but the current implementation (a) overwrites `cwds` to a single entry on save and (b) deletes the entire automation directory when removing an automation for a single cwd. This causes silent cross-project data loss for shared/multi-cwd cron automations.

### Issue Context
- `listProjectCronAutomations()` indexes the same automation under every cwd in `cwds`.
- Deleting by cwd currently removes the automation directory (`rm .../<automation.id>`), affecting all cwd associations.
- Saving an existing cron automation always writes `cwds: [projectName]`, dropping other cwd associations.

### Fix Focus Areas
- src/server/codexAppServerBridge.ts[3403-3432]
- src/server/codexAppServerBridge.ts[3445-3492]
- src/server/codexAppServerBridge.ts[3494-3508]
- src/components/sidebar/SidebarThreadTree.vue[2120-2126]

### Implementation direction
- Decide on supported behavior:
  1) If multi-cwd cron automations are supported:
     - On delete for one cwd: if `automation.cwds.length > 1`, rewrite `automation.toml` with that cwd removed (and only delete the directory if no cwds remain).
     - On save/edit: preserve existing `cwds` (or at minimum, preserve all existing cwds and ensure the current cwd remains included).
  2) If multi-cwd is *not* supported:
     - Enforce it explicitly: reject/ignore cron automations with `cwds.length !== 1` in project-scoped APIs, and avoid deleting directories that are shared across multiple cwds.
- Ensure sidebar “remove project” cleanup does not delete shared automations (same rule as above).

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment thread tests.md

---

### Pinned threads remain visible during background pagination
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Action required

1. tests.md cases removed 📘 Rule violation ⚙ Maintainability

This PR deletes/replaces existing manual test sections in tests.md, reducing historical regression
coverage. Compliance requires preserving existing test cases and only appending or minimally
updating what’s needed.
Agent Prompt
## Issue description
This PR removes existing `tests.md` manual test cases (replacing one section and deleting another), which reduces historical regression coverage.

## Issue Context
Per compliance, `tests.md` updates must be additive or minimally edited for the new feature; existing cases should not be removed unless there is a very strong reason and an equivalent replacement is kept.

## Fix Focus Areas
- tests.md[274-301]
- tests.md[1695-1727]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

@friuns2 friuns2 marked this pull request as draft May 10, 2026 02:18
@friuns2 friuns2 marked this pull request as ready for review May 10, 2026 02:18
@qodo-free-for-open-source-projects
Copy link
Copy Markdown

qodo-free-for-open-source-projects Bot commented May 10, 2026

Persistent review updated to latest commit ddd9014

Comment thread src/components/content/AutomationsPanel.vue Outdated
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants