Skip to content

Commit c8bc6b2

Browse files
committed
Merge branch 'codex/project-automation-sidebar-fixes' into main
2 parents a4fa8ae + 6dec792 commit c8bc6b2

14 files changed

Lines changed: 1419 additions & 39 deletions

File tree

AGENTS.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,6 @@
6262
- Reproduce the issue with a focused test when feasible; if direct reproduction is impractical, document the exact reasoning and code evidence used to accept or reject the finding.
6363
- Prefer adding or updating a regression test for every accepted review-bot bug before or alongside the fix.
6464
- Do not patch purely to satisfy a bot comment if the behavior is correct, stale, already fixed, or the proposed change would make the implementation worse.
65-
- After pushing any commit to an open PR, wait and poll for Qodo/review-bot comments and PR review status for about 30 seconds before reporting the push workflow as complete.
6665
- After fixing an accepted review-bot finding, run the narrow regression test plus the relevant build/typecheck command, push the commit, and re-check the PR comments/status.
6766
- In the completion report, distinguish confirmed fixes from stale or rejected bot comments.
6867

@@ -142,6 +141,7 @@
142141
- viewport(s)
143142
- assertion/result summary
144143
- screenshot absolute path(s)
144+
- inline screenshot image(s) rendered in chat with Markdown image syntax using absolute local paths
145145
- CJS command/result (when module-loading behavior was changed)
146146

147147
## Worktree Dev Server Rule
@@ -181,6 +181,7 @@
181181

182182
- When the user asks to test with Playwright, run the verification on the explicitly requested project/thread context (for example `TestChat`).
183183
- Screenshot artifacts must show complete passing evidence for the tested feature, not only the base page load.
184+
- Always show captured screenshots inline in the chat, not only as links or filesystem paths. Use Markdown image tags with absolute local paths, for example `![light verification](/absolute/path/output/playwright/example.png)`.
184185
- For UI work, include dark-theme evidence in addition to the default/light-theme evidence unless the task is explicitly light-only.
185186
- For refresh-persistence fixes, include a post-refresh screenshot that still shows the expected UI state.
186187

@@ -209,6 +210,7 @@
209210
- exact CJS command/script path
210211
- assertion summary (`hrefOk`, `titleOk`, `textOk`)
211212
- screenshot absolute path
213+
- inline screenshot image rendered in chat with Markdown image syntax using the absolute screenshot path
212214

213215
## LLM Wiki Schema
214216

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
# Project cron automations source
2+
3+
Date: 2026-05-10
4+
5+
Project-scoped automations are represented as Codex cron automations with a `cwds` array containing absolute project folder paths. The sidebar resolves display labels such as `TestChat` to the real cwd before saving so the Codex scheduler can run in the intended project. The project automation UI blocks unresolved/non-absolute project cwd values. Editing or deleting a project association on a multi-cwd cron automation preserves the other cwd associations and only deletes the automation folder when the final cwd is removed. Thread automations remain heartbeat automations keyed by `target_thread_id`.
6+
7+
Implementation facts:
8+
- `src/server/codexAppServerBridge.ts` parses and serializes `cwds` in automation TOML records.
9+
- `GET /codex-api/project-automations` returns project cron automations grouped by project cwd.
10+
- `GET`, `PUT`, and `DELETE /codex-api/project-automation` read, save, and remove automations for one project cwd.
11+
- `src/api/codexGateway.ts` exposes project automation helpers mirroring the thread automation helpers.
12+
- `src/components/sidebar/SidebarThreadTree.vue` adds project menu `Add automation…` / `Manage automations…`, project row automation icons, and reuses the existing automation dialog with project-specific copy.
13+
- `src/components/content/AutomationsPanel.vue` lists both thread heartbeat automations and project cron automations in one top-level panel. It sorts active automations before paused automations, newest first within each status group, and exposes row/detail edit buttons that open the shared automation editor.
14+
- Project automations intentionally do not expose `Run now`; the existing manual run behavior remains thread-heartbeat-only.
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
# Project Cron Automations
2+
3+
Project automations extend the existing sidebar automation UI from thread-scoped heartbeat records to project-scoped cron records.
4+
5+
## Storage
6+
7+
Project automations use Codex cron automation TOML records with `cwds = ["<absolute project path>"]`. Sidebar display labels must be resolved to the real project folder before saving, so folder-name rows such as `TestChat` still write the scheduler-visible cwd. The UI/server reject unresolved non-absolute cwd values for project automation saves. Multi-cwd cron records keep their other cwd associations when edited or removed from one project, and the automation folder is deleted only when the final cwd association is removed. This matches Codex's project/folder automation shape while preserving thread heartbeat records that use `target_thread_id`.
8+
9+
Source: [project-cron-automations.md](../../raw/features/project-cron-automations.md)
10+
11+
## UI
12+
13+
The sidebar project row dots menu exposes `Add automation…` or `Manage automations…`. The dialog reuses the thread automation manager style, including multiple automation selection, schedule presets, status, and remove/save behavior.
14+
15+
Project rows show the same compact automation icon when at least one project automation is attached. The top-level Automations panel lists both project cron automations and thread heartbeat automations together, sorts active automations before paused automations with newest records first inside each status group, and exposes edit buttons that open the shared automation editor.
16+
17+
Source: [project-cron-automations.md](../../raw/features/project-cron-automations.md)
18+
19+
## Boundaries
20+
21+
`Run now` remains available only for thread heartbeat automations because it queues a heartbeat message into a concrete thread. Project cron automations are scheduled against one or more working directories instead.
22+
23+
Source: [project-cron-automations.md](../../raw/features/project-cron-automations.md)

llm-wiki/wiki/index.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,15 @@
1414
- [concepts/realtime-chat-rendering.md](./concepts/realtime-chat-rendering.md): realtime chat rendering, sync-churn reduction, and inline media sanitization.
1515
- [concepts/skills-route-ui.md](./concepts/skills-route-ui.md): Skills route naming, first-launch Plugins card persistence, dark-theme fixes, and verification lessons.
1616
- [concepts/thread-heartbeat-automations.md](./concepts/thread-heartbeat-automations.md): thread-scoped heartbeat automation storage, multi-automation management, and manual run behavior.
17+
- [concepts/project-cron-automations.md](./concepts/project-cron-automations.md): project-scoped cron automation storage and sidebar management UI.
1718

1819
## Sources
1920
- [../raw/features/integrated-terminal.md](../raw/features/integrated-terminal.md): source facts for the integrated terminal implementation and follow-up tests.
2021
- [../raw/features/directory-hub-composio-skills-search.md](../raw/features/directory-hub-composio-skills-search.md): source facts for Directory Hub, Composio connectors, Skills search/install, and edge-case tests.
2122
- [../raw/features/realtime-chat-rendering-inline-media.md](../raw/features/realtime-chat-rendering-inline-media.md): source facts for realtime chat rendering and inline media sanitization.
2223
- [../raw/features/skills-route-ui-and-first-launch-card.md](../raw/features/skills-route-ui-and-first-launch-card.md): source facts for the Skills route rename, first-launch Plugins card, dark-theme fix, and dev-server workflow adjustment.
2324
- [../raw/features/thread-heartbeat-automations.md](../raw/features/thread-heartbeat-automations.md): source facts for thread heartbeat automations, multiple automations per thread, and Run now queue behavior.
25+
- [../raw/features/project-cron-automations.md](../raw/features/project-cron-automations.md): source facts for project cron automations in the sidebar.
2426
- [../raw/projects/codex-web-local.md](../raw/projects/codex-web-local.md): immutable source snapshot for project facts.
2527
- [../raw/fixes/opencode-zen-big-pickle-codex-cli.md](../raw/fixes/opencode-zen-big-pickle-codex-cli.md): Big Pickle + Codex CLI fix details.
2628
- [../raw/fixes/opencode-zen-reasoning-content-proxy.md](../raw/fixes/opencode-zen-reasoning-content-proxy.md): Codex Web Local Zen proxy reasoning_content round-trip fix and Docker verification.

llm-wiki/wiki/log.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,3 +46,9 @@
4646
- Updated wiki page: `concepts/opencode-zen-big-pickle.md`.
4747
- Documents: DeepSeek thinking-mode `reasoning_content` round-trip requirement, Chat-shaped Zen proxy endpoint selection, streaming reasoning preservation, Docker validation, and the `/tmp/app.tar` restart gotcha.
4848
- Updated `index.md`.
49+
## 2026-05-10
50+
51+
- Added project cron automation notes for sidebar project-level automation management.
52+
- Updated project cron automation notes for the combined Automations panel.
53+
- Updated Automations panel notes for active/newest sorting and direct edit buttons.
54+
- Updated project cron automation notes for absolute cwd validation and multi-cwd preservation.

src/App.vue

Lines changed: 104 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -60,8 +60,25 @@
6060
</span>
6161
</button>
6262

63-
<SidebarThreadTree :groups="projectGroups" :project-display-name-by-id="projectDisplayNameById"
63+
<button
64+
v-if="!isSidebarCollapsed"
65+
class="sidebar-skills-link"
66+
:class="{ 'is-active': isAutomationsRoute }"
67+
type="button"
68+
@click="router.push({ name: 'automations' }); isMobile && setSidebarCollapsed(true)"
69+
>
70+
<span class="sidebar-skills-link-icon sidebar-automations-link-icon" aria-hidden="true">
71+
<IconTablerBolt />
72+
</span>
73+
<span class="sidebar-skills-link-copy">
74+
<span class="sidebar-skills-link-title">{{ t('Automations') }}</span>
75+
<span class="sidebar-skills-link-subtitle">{{ t('Scheduled work') }}</span>
76+
</span>
77+
</button>
78+
79+
<SidebarThreadTree ref="sidebarThreadTreeRef" :groups="projectGroups" :project-display-name-by-id="projectDisplayNameById"
6480
:project-git-repo-by-name="projectGitRepoByName"
81+
:project-cwd-by-name="projectCwdByName"
6582
v-if="!isSidebarCollapsed"
6683
:selected-thread-id="selectedThreadId" :is-loading="isLoadingThreads"
6784
:is-thread-list-fully-loaded="isThreadListFullyLoaded"
@@ -77,6 +94,7 @@
7794
@fork-thread="onForkThread"
7895
@remove-project="onRemoveProject" @reorder-project="onReorderProject"
7996
@export-thread="onExportThread"
97+
@automations-changed="onAutomationsChanged"
8098
@start-new-chat="onStartNewThreadFromToolbar" />
8199
</div>
82100

@@ -484,7 +502,7 @@
484502
:style="contentStyle"
485503
>
486504
<span v-if="isVirtualKeyboardOpen" class="content-keyboard-spacer" aria-hidden="true" />
487-
<ContentHeader :title="contentTitle" :accent="isSkillsRoute">
505+
<ContentHeader :title="contentTitle" :accent="isSkillsRoute || isAutomationsRoute">
488506
<template #leading>
489507
<SidebarThreadControls
490508
v-if="isSidebarCollapsed || isMobile"
@@ -497,6 +515,9 @@
497515
<span v-if="isSkillsRoute" class="skills-route-header-icon" aria-hidden="true">
498516
<IconTablerBolt />
499517
</span>
518+
<span v-else-if="isAutomationsRoute" class="skills-route-header-icon automations-route-header-icon" aria-hidden="true">
519+
<IconTablerBolt />
520+
</span>
500521
</template>
501522
<template #actions>
502523
<ComposerDropdown
@@ -547,6 +568,18 @@
547568
@try-item="onTryDirectoryItem"
548569
/>
549570
</template>
571+
<template v-else-if="isAutomationsRoute">
572+
<AutomationsPanel
573+
ref="automationsPanelRef"
574+
:groups="projectGroups"
575+
:project-cwd-by-name="projectCwdByName"
576+
:project-display-name-by-id="projectDisplayNameById"
577+
:selected-automation-id="routeAutomationId"
578+
@select-automation="onSelectAutomationInPanel"
579+
@edit-automation="onEditAutomationFromPanel"
580+
@create-automation="onCreateAutomationFromPanel"
581+
/>
582+
</template>
550583
<template v-else-if="isHomeRoute">
551584
<div class="content-grid content-grid-home">
552585
<div class="new-thread-empty">
@@ -1087,7 +1120,7 @@ import {
10871120
searchThreads,
10881121
switchAccount,
10891122
} from './api/codexGateway'
1090-
import type { ReasoningEffort, SpeedMode, UiAccountEntry, UiRateLimitWindow, UiServerRequest, UiServerRequestReply, UiThreadTokenUsage } from './types/codex'
1123+
import type { ReasoningEffort, SpeedMode, UiAccountEntry, UiRateLimitWindow, UiServerRequest, UiServerRequestReply, UiThreadAutomation, UiThreadTokenUsage } from './types/codex'
10911124
import type { ComposerDraftPayload, ThreadComposerExposed } from './components/content/ThreadComposer.vue'
10921125
import type { GitCommitOption, LocalDirectoryEntry, TelegramStatus, ThreadTerminalQuickCommand, WorktreeBranchOption } from './api/codexGateway'
10931126
import { getFreeModeStatus, setFreeMode, setFreeModeCustomKey, setCustomProvider } from './api/codexGateway'
@@ -1097,6 +1130,7 @@ const ThreadConversation = defineAsyncComponent(() => import('./components/conte
10971130
const ThreadTerminalPanel = defineAsyncComponent(() => import('./components/content/ThreadTerminalPanel.vue'))
10981131
const ReviewPane = defineAsyncComponent(() => import('./components/content/ReviewPane.vue'))
10991132
const DirectoryHub = defineAsyncComponent(() => import('./components/content/DirectoryHub.vue'))
1133+
const AutomationsPanel = defineAsyncComponent(() => import('./components/content/AutomationsPanel.vue'))
11001134
const { t, uiLanguage, uiLanguageOptions, setUiLanguage } = useUiLanguage()
11011135
11021136
const SIDEBAR_COLLAPSED_STORAGE_KEY = 'codex-web-local.sidebar-collapsed.v1'
@@ -1332,6 +1366,20 @@ const {
13321366
const route = useRoute()
13331367
const router = useRouter()
13341368
const { isMobile } = useMobile()
1369+
type SidebarThreadTreeExposed = {
1370+
openAutomationEditorFromPanel: (payload: AutomationEditRequest) => void
1371+
openAutomationCreatorFromPanel: () => void
1372+
}
1373+
type AutomationsPanelExposed = {
1374+
loadAutomations: () => Promise<void>
1375+
}
1376+
type AutomationEditRequest = {
1377+
scope: 'thread' | 'project'
1378+
target: string
1379+
automation: UiThreadAutomation
1380+
}
1381+
const sidebarThreadTreeRef = ref<SidebarThreadTreeExposed | null>(null)
1382+
const automationsPanelRef = ref<AutomationsPanelExposed | null>(null)
13351383
const homeThreadComposerRef = ref<ThreadComposerExposed | null>(null)
13361384
const threadComposerRef = ref<ThreadComposerExposed | null>(null)
13371385
const threadConversationRef = ref<{ jumpToLatest: () => void } | null>(null)
@@ -1496,7 +1544,13 @@ const routeThreadId = computed(() => {
14961544
14971545
const isHomeRoute = computed(() => route.name === 'home')
14981546
const isSkillsRoute = computed(() => route.name === 'skills')
1547+
const isAutomationsRoute = computed(() => route.name === 'automations')
1548+
const routeAutomationId = computed(() => {
1549+
const raw = route.query.automationId
1550+
return typeof raw === 'string' ? raw : ''
1551+
})
14991552
const contentTitle = computed(() => {
1553+
if (isAutomationsRoute.value) return t('Automations')
15001554
if (isSkillsRoute.value) return t('Skills')
15011555
if (isHomeRoute.value) return t('Start new thread')
15021556
return selectedThread.value?.title ?? t('Choose a thread')
@@ -1561,7 +1615,7 @@ const isTerminalKeyboardLayoutActive = computed(() => (
15611615
))
15621616
const directoryCwd = computed(() => selectedThread.value?.cwd?.trim() ?? newThreadCwd.value.trim())
15631617
const isSelectedThreadInProgress = computed(() => !isHomeRoute.value && selectedThread.value?.inProgress === true)
1564-
const showThreadContextBadge = computed(() => !isHomeRoute.value && !isSkillsRoute.value && selectedThreadId.value.trim().length > 0)
1618+
const showThreadContextBadge = computed(() => !isHomeRoute.value && !isSkillsRoute.value && !isAutomationsRoute.value && selectedThreadId.value.trim().length > 0)
15651619
const isAccountSwitchBlocked = computed(() =>
15661620
isSendingMessage.value ||
15671621
isInterruptingTurn.value ||
@@ -2089,6 +2143,33 @@ function onSelectThread(threadId: string): void {
20892143
if (isMobile.value) setSidebarCollapsed(true)
20902144
}
20912145
2146+
function onSelectAutomationInPanel(automationId: string): void {
2147+
if (route.name !== 'automations') return
2148+
if (routeAutomationId.value === automationId) return
2149+
void router.replace({ name: 'automations', query: automationId ? { automationId } : {} })
2150+
}
2151+
2152+
async function onEditAutomationFromPanel(payload: AutomationEditRequest): Promise<void> {
2153+
if (isSidebarCollapsed.value) {
2154+
setSidebarCollapsed(false)
2155+
await nextTick()
2156+
}
2157+
sidebarThreadTreeRef.value?.openAutomationEditorFromPanel(payload)
2158+
}
2159+
2160+
async function onCreateAutomationFromPanel(): Promise<void> {
2161+
if (isSidebarCollapsed.value) {
2162+
setSidebarCollapsed(false)
2163+
await nextTick()
2164+
}
2165+
sidebarThreadTreeRef.value?.openAutomationCreatorFromPanel()
2166+
}
2167+
2168+
function onAutomationsChanged(): void {
2169+
if (route.name !== 'automations') return
2170+
void automationsPanelRef.value?.loadAutomations()
2171+
}
2172+
20922173
async function onExportThread(threadId: string): Promise<void> {
20932174
if (!threadId) return
20942175
if (selectedThreadId.value !== threadId) {
@@ -2440,6 +2521,14 @@ function getProjectCwd(projectName: string): string {
24402521
return resolvePreferredLocalCwd(projectName, projectGroup?.threads[0]?.cwd?.trim() ?? '')
24412522
}
24422523
2524+
const projectCwdByName = computed<Record<string, string>>(() =>
2525+
Object.fromEntries(
2526+
projectGroups.value
2527+
.map((group) => [group.projectName, getProjectCwd(group.projectName).trim()] as const)
2528+
.filter(([, cwd]) => cwd.length > 0),
2529+
),
2530+
)
2531+
24432532
function getProjectDisplayNameForWorktree(projectName: string): string {
24442533
return (projectDisplayNameById.value[projectName] ?? projectName).trim() || projectName
24452534
}
@@ -3578,7 +3667,7 @@ function onImplementPlan(payload: { turnId: string }): void {
35783667
35793668
35803669
function onExportChat(): void {
3581-
if (isHomeRoute.value || isSkillsRoute.value || typeof document === 'undefined') return
3670+
if (isHomeRoute.value || isSkillsRoute.value || isAutomationsRoute.value || typeof document === 'undefined') return
35823671
if (!selectedThread.value || filteredMessages.value.length === 0) return
35833672
const markdown = buildThreadMarkdown()
35843673
const fileName = buildExportFileName()
@@ -4034,7 +4123,7 @@ async function syncThreadSelectionWithRoute(): Promise<void> {
40344123
do {
40354124
hasPendingRouteSync = false
40364125
4037-
if (route.name === 'home' || route.name === 'skills') {
4126+
if (route.name === 'home' || route.name === 'skills' || route.name === 'automations') {
40384127
if (selectedThreadId.value !== '') {
40394128
await selectThread('')
40404129
}
@@ -4101,7 +4190,7 @@ watch(
41014190
async (threadId) => {
41024191
if (!hasInitialized.value) return
41034192
if (isRouteSyncInProgress.value) return
4104-
if (isHomeRoute.value || isSkillsRoute.value) return
4193+
if (isHomeRoute.value || isSkillsRoute.value || isAutomationsRoute.value) return
41054194
41064195
if (!threadId) {
41074196
if (route.name !== 'home') {
@@ -4426,6 +4515,10 @@ async function loadWorktreeBranches(sourceCwd: string): Promise<void> {
44264515
@apply flex h-10 w-10 shrink-0 items-center justify-center rounded-2xl bg-emerald-600 text-white;
44274516
}
44284517
4518+
.sidebar-automations-link-icon {
4519+
@apply bg-amber-500;
4520+
}
4521+
44294522
.sidebar-skills-link-icon :deep(svg) {
44304523
@apply h-5 w-5;
44314524
}
@@ -4450,6 +4543,10 @@ async function loadWorktreeBranches(sourceCwd: string): Promise<void> {
44504543
@apply flex h-9 w-9 shrink-0 items-center justify-center rounded-2xl bg-emerald-600 text-white shadow-[0_16px_32px_-20px_rgba(5,150,105,0.9)];
44514544
}
44524545
4546+
.automations-route-header-icon {
4547+
@apply bg-amber-500 shadow-[0_16px_32px_-20px_rgba(245,158,11,0.9)];
4548+
}
4549+
44534550
.skills-route-header-icon :deep(svg) {
44544551
@apply h-4.5 w-4.5;
44554552
}

0 commit comments

Comments
 (0)