Skip to content

fix(schedules): submit scheduled runs via prompt_async with SSE monitoring - #335

Open
chriswritescode-dev wants to merge 1 commit into
mainfrom
fix/schedule-prompt-async
Open

fix(schedules): submit scheduled runs via prompt_async with SSE monitoring#335
chriswritescode-dev wants to merge 1 commit into
mainfrom
fix/schedule-prompt-async

Conversation

@chriswritescode-dev

@chriswritescode-dev chriswritescode-dev commented Aug 10, 2026

Copy link
Copy Markdown
Owner

Scheduled runs submitted synchronously via POST /session/:id/message, so a connection interruption or OpenCode restart mid-run aborted the job with Proxy request failed. Submission now uses POST /session/:id/prompt_async and completion is tracked through the existing SSE aggregator (session idle/error signals plus a settled assistant-message read), with directory-scoped session status replay covering scheduled sessions across upstream reconnects. Adds an allowQuestions permission toggle (default off) so unattended runs deny agent questions instead of stalling. Fixes #334.

Summary

Type of Change

  • Bug fix
  • New feature
  • Refactor
  • Documentation

Checklist

  • Code follows project style (no comments, named imports)
  • TypeScript types are properly defined
  • Tests added/updated (80% coverage target)
  • pnpm lint passes locally
  • pnpm typecheck passes locally

Summary by CodeRabbit

  • New Features

    • Added a schedule permission setting to allow or deny agents’ use of the question tool.
    • Questions are denied by default unless explicitly enabled.
    • Schedule status updates now provide clearer active, completed, stopped, and error states.
  • Bug Fixes

    • Improved monitoring reliability for asynchronous scheduled tasks.
    • Improved status restoration after reconnecting or returning to a schedule view.
    • Preserved accurate status updates when tasks finish while disconnected.
  • Tests

    • Expanded coverage for question permissions, asynchronous task completion, error handling, and reconnect scenarios.

@coderabbitai

coderabbitai Bot commented Aug 10, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Scheduled runs now submit prompts through /prompt_async and finish through event-driven session monitoring. SSE replay includes scheduled sessions by directory. Schedule permissions now include configurable question-tool access, with persistence and UI coverage.

Changes

Scheduled execution monitoring

Layer / File(s) Summary
Event-driven session monitoring
backend/src/services/schedules.ts
Session monitoring queues status signals, classifies assistant outcomes, and returns active session references with directories.
Asynchronous prompt completion flow
backend/src/services/schedules.ts, backend/test/services/schedules.test.ts
Scheduled prompts use /prompt_async. Completion waits for idle status and finalized assistant messages.
Directory-aware session status replay
backend/src/services/sse-aggregator.ts, backend/src/index.ts, backend/test/services/sse-aggregator.test.ts
SSE replay includes scheduled sessions grouped by directory and emits active or idle statuses during reconnects.

Scheduled question permissions

Layer / File(s) Summary
Question permission contract and rules
shared/src/schemas/schedule.ts, backend/test/services/schedule-permissions.test.ts
allowQuestions defaults to false and controls the generated question deny rule.
Permission persistence and schedule integration
backend/test/db/schedules.permission.test.ts, backend/test/services/schedules.permission.test.ts
Persistence tests include allowQuestions. Permission request fixtures use /prompt_async.
Schedule dialog question toggle
frontend/src/components/schedules/GeneralTab.tsx, frontend/src/components/schedules/ScheduleJobDialog.tsx, frontend/src/components/schedules/ScheduleJobDialog.permissions.test.tsx
The schedule dialog stores, initializes, submits, and renders the question permission setting.

Repository ignore configuration

Layer / File(s) Summary
pnpm store ignore rule
.gitignore
Adds .pnpm-store/ to ignored paths.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant ScheduleService
  participant OpenCodeAPI
  participant SessionMonitor
  participant SSEAggregator
  ScheduleService->>OpenCodeAPI: POST /session/{id}/prompt_async
  ScheduleService->>SessionMonitor: markSubmitted
  OpenCodeAPI-->>SessionMonitor: status and session.idle events
  SessionMonitor-->>ScheduleService: completion signal
  ScheduleService->>SSEAggregator: provide active session reference
  SSEAggregator-->>SSEAggregator: replay status by directory
Loading
🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Out of Scope Changes check ⚠️ Warning The .gitignore update is unrelated to issue #334, and the allowQuestions feature is not required by the linked issue. Move the .gitignore and allowQuestions changes to a separate pull request, or link issues that define those requirements.
Docstring Coverage ⚠️ Warning Docstring coverage is 30.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the primary change: scheduled runs use prompt_async with SSE monitoring.
Description check ✅ Passed The description covers the change, classifies it as a bug fix, and completes the checklist; the Summary heading is empty but the preceding text provides the summary.
Linked Issues check ✅ Passed The changes implement issue #334 by using prompt_async and monitoring scheduled-run completion through SSE signals.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/schedule-prompt-async

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

❤️ Share

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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 4

🧹 Nitpick comments (3)
backend/test/services/schedules.test.ts (1)

384-446: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add coverage for the stopped outcome.

The new tests cover busy and settled. They do not cover the case where the session reports idle and no assistant message is completed and no error is set. That path returns SESSION_STOPPED_ERROR and marks the run failed. It is the branch that the linked issue reports in production.

Add a test that emits session.idle while /session/:id/message returns no completed assistant message, then assert status: 'failed' with the stopped error text.

As per coding guidelines: "Maintain at least 80% test coverage for the backend."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/test/services/schedules.test.ts` around lines 384 - 446, Add a
ScheduleService test covering the stopped outcome: configure the session to
report idle while /session/:id/message returns no completed assistant message
and no error, emit the matching session.idle event, then assert
updateScheduleRun marks the run failed with the SESSION_STOPPED_ERROR text. Keep
the existing busy and settled scenarios unchanged.

Source: Coding guidelines

backend/src/services/schedules.ts (1)

311-365: 🩺 Stability & Availability | 🔵 Trivial | 💤 Low value

Guard nextSignal against concurrent consumers.

waiting holds a single resolver. If two callers await nextSignal() at the same time, the first resolver is overwritten and its promise never settles. Only one consumer exists today, so this is a defensive hardening item, not a current defect.

♻️ Suggested change to queue waiters
-  const queued: SessionSignal[] = []
-  let waiting: ((signal: SessionSignal) => void) | null = null
+  const queued: SessionSignal[] = []
+  const waiters: ((signal: SessionSignal) => void)[] = []
   let disposed = false

   const push = (signal: SessionSignal): void => {
-    if (waiting) {
-      const resolve = waiting
-      waiting = null
-      resolve(signal)
-      return
-    }
+    const resolve = waiters.shift()
+    if (resolve) {
+      resolve(signal)
+      return
+    }
     queued.push(signal)
   }
-      return new Promise<SessionSignal>((resolve) => { waiting = resolve })
+      return new Promise<SessionSignal>((resolve) => { waiters.push(resolve) })
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/src/services/schedules.ts` around lines 311 - 365, Update the session
signal consumer returned by the surrounding factory so nextSignal prevents
concurrent pending consumers instead of overwriting the single waiting resolver.
Preserve queued-signal delivery, disposed handling, and existing behavior for
the supported single-consumer flow, while ensuring any second simultaneous call
is handled explicitly and no promise remains unsettled.
shared/src/schemas/schedule.ts (1)

62-64: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Remove the added TypeScript comments in both files.

The repository requires TypeScript and TSX code to remain self-documenting.

  • shared/src/schemas/schedule.ts#L62-L64: remove the added JSDoc text or move it to external documentation.
  • frontend/src/components/schedules/ScheduleJobDialog.permissions.test.tsx#L102-L105: remove the added block comment.

As per coding guidelines: **/*.{ts,tsx,js,jsx}: Do not add code comments; code must be self-documenting.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@shared/src/schemas/schedule.ts` around lines 62 - 64, Remove the added
TypeScript comments from shared/src/schemas/schedule.ts lines 62-64 and the
block comment from
frontend/src/components/schedules/ScheduleJobDialog.permissions.test.tsx lines
102-105, leaving the surrounding code unchanged.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@backend/src/services/schedules.ts`:
- Around line 1124-1128: Remove the added block comment near the assistant
outcome handling. Preserve its intent through a self-documenting method name
such as readOutcomeOnlyWhenSessionIdle, or retain readAssistantOutcome while
relying on the explicit AssistantOutcome union.
- Around line 1153-1181: Bound waitForAssistantMessage so it cannot remain
blocked indefinitely when sessionMonitor.nextSignal() never emits another event.
Add a periodic timeout or re-check that invokes readAssistantOutcome and
preserves the existing busy, settled, and stopped result handling, ensuring the
wait eventually returns an error or terminal outcome and allows cleanup to
remove the run from activeRuns.

In `@backend/src/services/sse-aggregator.ts`:
- Around line 539-545: Update getScheduledSessions to wrap the
scheduledSessionsResolver call in try/catch, log the resolver failure using the
service’s existing logging mechanism, and return an empty ScheduledSessionRef
array on error so replaySessionStatusesForTrackedDirectories continues
processing all directories.

In `@frontend/src/components/schedules/GeneralTab.tsx`:
- Around line 185-193: Give the Radix Switch in GeneralTab the accessible name
“Allow questions” by associating it with the visible label via a stable id and
aria-labelledby, or an equivalent aria-label. Update the related permissions
test to locate it with getByRole('switch', { name: 'Allow questions' }) instead
of relying on switch indexes.

---

Nitpick comments:
In `@backend/src/services/schedules.ts`:
- Around line 311-365: Update the session signal consumer returned by the
surrounding factory so nextSignal prevents concurrent pending consumers instead
of overwriting the single waiting resolver. Preserve queued-signal delivery,
disposed handling, and existing behavior for the supported single-consumer flow,
while ensuring any second simultaneous call is handled explicitly and no promise
remains unsettled.

In `@backend/test/services/schedules.test.ts`:
- Around line 384-446: Add a ScheduleService test covering the stopped outcome:
configure the session to report idle while /session/:id/message returns no
completed assistant message and no error, emit the matching session.idle event,
then assert updateScheduleRun marks the run failed with the
SESSION_STOPPED_ERROR text. Keep the existing busy and settled scenarios
unchanged.

In `@shared/src/schemas/schedule.ts`:
- Around line 62-64: Remove the added TypeScript comments from
shared/src/schemas/schedule.ts lines 62-64 and the block comment from
frontend/src/components/schedules/ScheduleJobDialog.permissions.test.tsx lines
102-105, leaving the surrounding code unchanged.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

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

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: cff9beaf-9692-4122-8c97-d1731b00dcfd

📥 Commits

Reviewing files that changed from the base of the PR and between 1666d0d and ed4d4b0.

📒 Files selected for processing (13)
  • .gitignore
  • backend/src/index.ts
  • backend/src/services/schedules.ts
  • backend/src/services/sse-aggregator.ts
  • backend/test/db/schedules.permission.test.ts
  • backend/test/services/schedule-permissions.test.ts
  • backend/test/services/schedules.permission.test.ts
  • backend/test/services/schedules.test.ts
  • backend/test/services/sse-aggregator.test.ts
  • frontend/src/components/schedules/GeneralTab.tsx
  • frontend/src/components/schedules/ScheduleJobDialog.permissions.test.tsx
  • frontend/src/components/schedules/ScheduleJobDialog.tsx
  • shared/src/schemas/schedule.ts

Comment on lines +1124 to +1128
/**
* An assistant message completing is not the end of a run: a multi-step agent
* settles one message per tool call. Only a session that has gone idle has
* finished, and only then is the final message guaranteed to carry its parts.
*/

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Remove the added block comment.

The coding guidelines forbid comments in TypeScript files. Encode the intent in the method name instead, for example readOutcomeOnlyWhenSessionIdle, or keep readAssistantOutcome and rely on the explicit AssistantOutcome union.

As per coding guidelines: "Do not add comments; code should be self-documenting."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/src/services/schedules.ts` around lines 1124 - 1128, Remove the added
block comment near the assistant outcome handling. Preserve its intent through a
self-documenting method name such as readOutcomeOnlyWhenSessionIdle, or retain
readAssistantOutcome while relying on the explicit AssistantOutcome union.

Source: Coding guidelines

Comment on lines 1153 to 1181
private async waitForAssistantMessage(
job: ScheduleJob,
sessionId: string,
sessionMonitor: SessionMonitor,
directory: string,
): Promise<{ responseText: string | null; errorText: string | null }> {
const startedAt = Date.now()

while (Date.now() - startedAt < RUN_POLL_TIMEOUT_MS) {
const messages = await this.listSessionMessages(directory, sessionId)
const assistantState = getAssistantMessageState(messages)
for (;;) {
const signal = await sessionMonitor.nextSignal()

if (assistantState && (assistantState.completed || assistantState.errorText)) {
if (signal.errorText || signal.disposed) {
const messages = await this.listSessionMessages(directory, sessionId)
return {
responseText: assistantState.responseText,
errorText: assistantState.errorText,
responseText: getAssistantMessageState(messages)?.responseText ?? null,
errorText: signal.errorText ?? SESSION_STOPPED_ERROR,
}
}

const sessionErrorText = sessionMonitor.getErrorText()
if (sessionErrorText) {
return {
responseText: null,
errorText: sessionErrorText,
}
}
const outcome = await this.readAssistantOutcome(directory, sessionId)

if (sessionMonitor.isIdle()) {
return {
responseText: null,
errorText: 'The session became idle without producing an assistant response. Open the linked session to inspect any pending questions, permissions, or provider issues.',
}
if (outcome.kind === 'busy') {
continue
}

await Bun.sleep(RUN_POLL_INTERVAL_MS)
}
if (outcome.kind === 'settled') {
return { responseText: outcome.responseText, errorText: outcome.errorText }
}

return {
responseText: null,
errorText: 'Timed out waiting for the assistant response. Open the linked session to inspect any pending questions, permissions, or provider issues.',
return { responseText: outcome.responseText, errorText: SESSION_STOPPED_ERROR }
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

waitForAssistantMessage can wait forever.

The previous implementation used fixed-interval polling with a timeout. The new loop blocks on sessionMonitor.nextSignal() and only resumes when a session.idle, session.status idle, or session.error event arrives for the directory, or when dispose() runs. dispose() only runs in the finally of monitorRunCompletion, which is reached after this loop returns, so it cannot unblock the loop.

If the upstream event for the session is never delivered, for example when the OpenCode process for that directory dies and the aggregator never emits a further status for the session, the run stays running forever. ScheduleService.activeRuns keeps the job id, so runJob then rejects every later trigger with "Schedule is already running".

Add a bounded fallback. One option is a maximum wait that re-reads the session state, another is a periodic re-check that calls readAssistantOutcome even without a signal.

🛡️ Sketch of a bounded wait
-    for (;;) {
-      const signal = await sessionMonitor.nextSignal()
+    for (;;) {
+      const signal = await Promise.race([
+        sessionMonitor.nextSignal(),
+        Bun.sleep(SESSION_SIGNAL_RECHECK_MS).then((): SessionSignal | null => null),
+      ])
+
+      if (!signal) {
+        const polled = await this.readAssistantOutcome(directory, sessionId)
+        if (polled.kind === 'busy') continue
+        if (polled.kind === 'settled') {
+          return { responseText: polled.responseText, errorText: polled.errorText }
+        }
+        return { responseText: polled.responseText, errorText: SESSION_STOPPED_ERROR }
+      }

Confirm whether another component already bounds scheduled run duration.

#!/bin/bash
# Description: Look for any timeout, watchdog, or stale-run reaper covering schedule runs.
set -euo pipefail

rg -nP --type=ts -C4 '\b(setTimeout|AbortSignal\.timeout|TIMEOUT|_MS)\b' backend/src/services/schedules.ts backend/src/services/schedule-worktree.ts 2>/dev/null || true

fd -e ts . backend/src --exec rg -nP -C4 'stale|watchdog|reap|maxRunDuration|runTimeout' {} \;

rg -nP --type=ts -C4 '\bactiveRuns\b' backend/src
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/src/services/schedules.ts` around lines 1153 - 1181, Bound
waitForAssistantMessage so it cannot remain blocked indefinitely when
sessionMonitor.nextSignal() never emits another event. Add a periodic timeout or
re-check that invokes readAssistantOutcome and preserves the existing busy,
settled, and stopped result handling, ensuring the wait eventually returns an
error or terminal outcome and allows cleanup to remove the run from activeRuns.

Comment on lines 539 to +545
getScheduledSessionIds(): Set<string> {
return this.scheduledSessionsResolver?.() ?? new Set()
return new Set(this.getScheduledSessions().map(ref => ref.sessionID))
}

private getScheduledSessions(): ScheduledSessionRef[] {
return this.scheduledSessionsResolver?.() ?? []
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Guard the resolver call against throwing.

replaySessionStatusesForTrackedDirectories runs from void this.replaySessionStatusesForTrackedDirectories() in es.onopen. It calls getScheduledSessionsByDirectory first, which calls the resolver. The resolver is scheduleService.getActiveRunSessions(), which reads SQLite. If that read throws, the rejection is unhandled and the whole status replay is skipped for every directory, including client directories.

Wrap the resolver call in try/catch and log the failure.

🛡️ Proposed fix
   private getScheduledSessions(): ScheduledSessionRef[] {
-    return this.scheduledSessionsResolver?.() ?? []
+    try {
+      return this.scheduledSessionsResolver?.() ?? []
+    } catch (error) {
+      logger.warn(`replay: failed to resolve scheduled sessions: ${String(error)}`)
+      return []
+    }
   }

As per coding guidelines: "Handle errors appropriately with try/catch where required."

📝 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
getScheduledSessionIds(): Set<string> {
return this.scheduledSessionsResolver?.() ?? new Set()
return new Set(this.getScheduledSessions().map(ref => ref.sessionID))
}
private getScheduledSessions(): ScheduledSessionRef[] {
return this.scheduledSessionsResolver?.() ?? []
}
getScheduledSessionIds(): Set<string> {
return new Set(this.getScheduledSessions().map(ref => ref.sessionID))
}
private getScheduledSessions(): ScheduledSessionRef[] {
try {
return this.scheduledSessionsResolver?.() ?? []
} catch (error) {
logger.warn(`replay: failed to resolve scheduled sessions: ${String(error)}`)
return []
}
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/src/services/sse-aggregator.ts` around lines 539 - 545, Update
getScheduledSessions to wrap the scheduledSessionsResolver call in try/catch,
log the resolver failure using the service’s existing logging mechanism, and
return an empty ScheduledSessionRef array on error so
replaySessionStatusesForTrackedDirectories continues processing all directories.

Source: Coding guidelines

Comment on lines +185 to +193
<div className="rounded-lg border border-border bg-card p-4">
<div className="flex items-center justify-between gap-4">
<div className="flex items-center gap-2">
<p className="text-sm font-medium">Allow questions</p>
<InfoHint text="Scheduled runs are unattended. When disabled, the agent's question tool is denied so a run can never stall waiting for an answer that nobody is there to give." />
</div>
<Switch checked={allowQuestions} onCheckedChange={onAllowQuestionsChange} />
</div>
</div>

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== locate files =="
fd -a 'GeneralTab\.tsx|ScheduleJobDialog.permissions.test.tsx' . || true

echo "== GeneralTab outline =="
if [ -f frontend/src/components/schedules/GeneralTab.tsx ]; then
  wc -l frontend/src/components/schedules/GeneralTab.tsx
  ast-grep outline frontend/src/components/schedules/GeneralTab.tsx || true
  sed -n '150,210p' frontend/src/components/schedules/GeneralTab.tsx
fi

echo "== search Switch imports and occurrences in GeneralTab =="
rg -n "import .*Switch|Switch|Allow questions|allowQuestions|onAllowQuestionsChange" frontend/src/components/schedules/GeneralTab.tsx || true

echo "== test relevant content =="
if [ -f frontend/src/components/schedules/ScheduleJobDialog.permissions.test.tsx ]; then
  wc -l frontend/src/components/schedules/ScheduleJobDialog.permissions.test.tsx
  rg -n "Allow questions|getByRole|getByLabelText|queryByRole|allow.questions|allow questions|question" frontend/src/components/schedules/ScheduleJobDialog.permissions.test.tsx || true
fi

Repository: chriswritescode-dev/opencode-manager

Length of output: 5352


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== components/ui/switch =="
fd -a 'switch\.(tsx|ts|jsx|js)$' frontend/src/components/ui || true
if [ -f frontend/src/components/ui/switch.tsx ]; then
  wc -l frontend/src/components/ui/switch.tsx
  sed -n '1,220p' frontend/src/components/ui/switch.tsx
fi

echo "== relevant tests =="
sed -n '180,245p' frontend/src/components/schedules/ScheduleJobDialog.permissions.test.tsx

echo "== all allowQuestions switch usages =="
rg -n "getByRole\('switch'|getByRole\(\"switch\"|getByLabel|Allow questions|allowQuestions" frontend/src/components/schedules/ScheduleJobDialog.permissions.test.tsx frontend/src/components/schedules/GeneralTab.tsx || true

echo "== React switch accessibility pattern probe from source text =="
python3 - <<'PY'
from pathlib import Path
p = Path('frontend/src/components/ui/switch.tsx')
def load():
    try:
        return p.read_text()
    except Exception as e:
        print(f"missing or unreadable {p}: {e}")
        return ""
s=load()
print("contains aria-labelledby:", "aria-labelledby" in s)
print("uses SwitchPrimitive.Root:", "SwitchPrimitive.Root" in s or "Switch.Root" in s)
print("role=switch:", "role=\"switch\"" in s or "role='switch'" in s)
print("tabIndex set:", "tabIndex" in s)
PY

Repository: chriswritescode-dev/opencode-manager

Length of output: 4817


Give the question switch an accessible name.

GeneralTab.tsx uses a Radix Switch, but the Allow questions label is only in a nearby <p>, so the switch can be exposed without a name. Add a stable id on the <p> and associate it with Switch via aria-labelledby, or add aria-label="Allow questions". Update frontend/src/components/schedules/ScheduleJobDialog.permissions.test.tsx to find the switch with getByRole('switch', { name: 'Allow questions' }) instead of index-based switch selection.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/src/components/schedules/GeneralTab.tsx` around lines 185 - 193,
Give the Radix Switch in GeneralTab the accessible name “Allow questions” by
associating it with the visible label via a stable id and aria-labelledby, or an
equivalent aria-label. Update the related permissions test to locate it with
getByRole('switch', { name: 'Allow questions' }) instead of relying on switch
indexes.

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.

Scheduled jobs fail after long runs with Proxy request failed

1 participant