Skip to content

Integrate Ralph Loop and enhance Git control UI/UX features - #8

Merged
Rishabh-Bajpai merged 52 commits into
mainfrom
development
May 21, 2026
Merged

Integrate Ralph Loop and enhance Git control UI/UX features#8
Rishabh-Bajpai merged 52 commits into
mainfrom
development

Conversation

@Rishabh-Bajpai

@Rishabh-Bajpai Rishabh-Bajpai commented May 21, 2026

Copy link
Copy Markdown
Collaborator

Summary by CodeRabbit

  • New Features
    • Git version control integration with branch management, commits, history viewing, and diff preview
    • Notification system with ntfy topic URL support for task updates
    • PRD-driven task automation loop for autonomous feature development
    • Project file browser with tree view, search, and content preview
    • Voice transcription and speech synthesis capabilities
    • Question and approval workflow handling
    • Enhanced message rendering with markdown support

Review Change Stack

google-labs-jules Bot and others added 30 commits May 7, 2026 23:09
- Adopted Ralph Loop pattern from ChristianKuri/ralph-and-opencode
- Added `ralph.sh` script to orchestrate the loop execution
- Added `.opencode/skills/` globally available OpenCode agent skills
- Added `prompt.md` instructions and `prd.json.example` format definition
- Updated `AGENTS.md` with instructions on how to use Ralph Loop

Co-authored-by: Rishabh-Bajpai <28703138+Rishabh-Bajpai@users.noreply.github.com>
…, files, and tasks views. Uses GitPython in the backend to provide basic repository management, allowing users to initialize repos, view status/changes, stage and commit files, and push/pull to/from a remote origin.

Co-authored-by: Rishabh-Bajpai <28703138+Rishabh-Bajpai@users.noreply.github.com>
- Adopted Ralph Loop pattern from ChristianKuri/ralph-and-opencode
- Added `ralph.sh` script to orchestrate the loop execution
- Added `.opencode/skills/` globally available OpenCode agent skills
- Added `prompt.md` instructions and `prd.json.example` format definition
- Updated `AGENTS.md` with instructions on how to use Ralph Loop
- Added UI checkboxes to "Create Chat Project" modals to scaffold Ralph Loop
- Backend copies Ralph Loop files securely without using shell subprocesses

Co-authored-by: Rishabh-Bajpai <28703138+Rishabh-Bajpai@users.noreply.github.com>
…git push by auto-setting upstream when pushing from a new branch.\n- Enhances Git UI styling so background cards look consistent with the rest of the application.\n- Updates GitView to correctly use React FormEvent typings.

Co-authored-by: Rishabh-Bajpai <28703138+Rishabh-Bajpai@users.noreply.github.com>
Agent-Logs-Url: https://github.com/Rishabh-Bajpai/mobile-opencode-control/sessions/ae4cf8a5-8b07-4e1d-98a3-f3be8c995b13

Co-authored-by: Rishabh-Bajpai <28703138+Rishabh-Bajpai@users.noreply.github.com>
Agent-Logs-Url: https://github.com/Rishabh-Bajpai/mobile-opencode-control/sessions/ae4cf8a5-8b07-4e1d-98a3-f3be8c995b13

Co-authored-by: Rishabh-Bajpai <28703138+Rishabh-Bajpai@users.noreply.github.com>
Agent-Logs-Url: https://github.com/Rishabh-Bajpai/mobile-opencode-control/sessions/ae4cf8a5-8b07-4e1d-98a3-f3be8c995b13

Co-authored-by: Rishabh-Bajpai <28703138+Rishabh-Bajpai@users.noreply.github.com>
Agent-Logs-Url: https://github.com/Rishabh-Bajpai/mobile-opencode-control/sessions/ae4cf8a5-8b07-4e1d-98a3-f3be8c995b13

Co-authored-by: Rishabh-Bajpai <28703138+Rishabh-Bajpai@users.noreply.github.com>
…116035049

feat: add Git control to project view
… inline-styled checkbox

Agent-Logs-Url: https://github.com/Rishabh-Bajpai/mobile-opencode-control/sessions/e8245915-d516-4bab-b1a0-3909966004a2

Co-authored-by: Rishabh-Bajpai <28703138+Rishabh-Bajpai@users.noreply.github.com>
…prd routes

Agent-Logs-Url: https://github.com/Rishabh-Bajpai/mobile-opencode-control/sessions/e8245915-d516-4bab-b1a0-3909966004a2

Co-authored-by: Rishabh-Bajpai <28703138+Rishabh-Bajpai@users.noreply.github.com>
…trace exposure

Agent-Logs-Url: https://github.com/Rishabh-Bajpai/mobile-opencode-control/sessions/e8245915-d516-4bab-b1a0-3909966004a2

Co-authored-by: Rishabh-Bajpai <28703138+Rishabh-Bajpai@users.noreply.github.com>
…p-integration-4511484040334154221

# Conflicts:
#	frontend/src/api.ts

Co-authored-by: Rishabh-Bajpai <28703138+Rishabh-Bajpai@users.noreply.github.com>
…-4511484040334154221

feat: Integrate Ralph Loop mechanism for automated app development
Rishabh-Bajpai and others added 15 commits May 20, 2026 15:34
… messages by adding horizontal scrolling wrappers
…rontend support for tracking context usage and compaction updates.
…d approvals, and add debouncing for assistant message notifications
Agent-Logs-Url: https://github.com/Rishabh-Bajpai/mobile-opencode-control/sessions/21222144-c657-46cf-957d-18faf75e6449

Co-authored-by: Rishabh-Bajpai <28703138+Rishabh-Bajpai@users.noreply.github.com>
Agent-Logs-Url: https://github.com/Rishabh-Bajpai/mobile-opencode-control/sessions/21222144-c657-46cf-957d-18faf75e6449

Co-authored-by: Rishabh-Bajpai <28703138+Rishabh-Bajpai@users.noreply.github.com>
Agent-Logs-Url: https://github.com/Rishabh-Bajpai/mobile-opencode-control/sessions/21222144-c657-46cf-957d-18faf75e6449

Co-authored-by: Rishabh-Bajpai <28703138+Rishabh-Bajpai@users.noreply.github.com>
Enhance project features, UI improvements, and documentation updates
@coderabbitai

coderabbitai Bot commented May 21, 2026

Copy link
Copy Markdown

Warning

Rate limit exceeded

@Copilot has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 54 minutes and 42 seconds before requesting another review.

You’ve run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: dd3b9836-5751-435c-9747-6fce14d9bfae

📥 Commits

Reviewing files that changed from the base of the PR and between 2f019cb and 8accccd.

📒 Files selected for processing (14)
  • backend/app/__init__.py
  • backend/app/git_routes.py
  • backend/app/routes/helpers.py
  • backend/app/routes/messages.py
  • backend/app/routes/notifications.py
  • backend/app/routes/projects.py
  • backend/app/routes/sessions.py
  • backend/tests/test_review_fixes.py
  • frontend/src/api.ts
  • frontend/src/components/chat/DiffPanel.tsx
  • frontend/src/components/projects/VirtualizedProjectList.tsx
  • frontend/src/styles.css
  • frontend/src/utils/streamUtils.ts
  • scripts/ralph/ralph.sh
📝 Walkthrough

Walkthrough

Backend routes were modularized with helpers and new endpoints across sessions, messages/streaming, files, runtime, tasks, STT/TTS, notifications, and opencode. A full Git API was added and registered. Frontend gained Git UI, file browser, chat rendering, runtime/notification controls, PRD/tasks panels, extensive utils/types, API wrappers, and styles. Docs/skills/scripts updated.

Changes

Unified Backend and Frontend Expansion

Layer / File(s) Summary
Backend routing refactor and core API modules
backend/app/routes.py, backend/app/routes/*, backend/app/routes/helpers.py, backend/app/opencode.py
Centralizes route registration; adds helpers and modules for files, messages/streaming, notifications, opencode, projects/state, runtime, scheduler status, sessions, STT/TTS, tasks; extends OpenCode client for questions/session ops.
Git backend blueprint, registration, dependency, and tests
backend/app/git_routes.py, backend/app/__init__.py, backend/requirements.txt, backend/tests/test_git_diff.py
Implements Git REST endpoints with Repo orchestration, registers blueprint, adds GitPython, and tests untracked diff preview/limits.
Frontend API client, types, and utilities
frontend/src/api.ts, frontend/src/types*.ts, frontend/src/utils/*.ts
Adds wrappers for settings, questions, notifications, PRD, sessions, and git; introduces types and utilities for files, formatting, messages, misc/project, stream parsing, and task timezones.
Frontend Git workspace UI
frontend/src/GitView.tsx
New Git UI: overview/changes/branches/history, with init/commit/push/pull/remote/branch actions.
Files browser, project list, and tasks panels
frontend/src/components/projects/*, frontend/src/components/tasks/*
Project file browser with preview/download; virtualized project list; Ralph PRD panel and scheduled tasks editor/browser.
Chat message rendering and activity components
frontend/src/components/chat/*
Rich markdown rendering, message bubbles/parts, activity summaries, diff panel, transitions, empty states, and task run timeline items.
Toolbar controls, auth, modal, question card, and styles
frontend/src/components/{auth,toolbar,ui}/*, frontend/src/styles.css
Adds login, runtime/notification/install controls, command picker, fixture banner, question card; large style additions for settings/git/tasks/chat.
Skills, docs, README, scripts, PRD example, and env/config
.opencode/skills/*, README.md, AGENTS.md, scripts/ralph/*, prd.json.example, .env.example, backend/app/config.py
Adds skills (compound-engineering, dev-browser, frontend-design, prd, ralph), updates docs; introduces Ralph loop scripts and PRD example; adds ntfy env/config.
Frontend dependency for markdown rendering
frontend/package.json
Adds react-markdown and remark-gfm.

Sequence Diagram(s)

sequenceDiagram
  participant Frontend
  participant API as Backend /api
  participant OpenCode as OpenCodeClient
  participant Git as GitPython Repo

  Frontend->>API: GET /projects/:id/messages
  API->>OpenCode: list_messages(session)
  OpenCode-->>API: messages
  API-->>Frontend: messages + timeline

  Frontend->>API: GET /projects/:id/git/status
  API->>Git: diff/status/heads
  Git-->>API: repo state
  API-->>Frontend: status/branches/history
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Possibly related PRs

Poem

Ear to the wire, I twitch with glee,
New routes bloom wide like a code oak tree.
Git purrs softly, diffs unfurl bright,
Tasks and PRDs march into the night.
With ntfy whispers and markdown hue—
Thump-thump! says the rabbit: shipped anew. 🐇✨

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch development

@Rishabh-Bajpai
Rishabh-Bajpai requested a review from Copilot May 21, 2026 00:01
@Rishabh-Bajpai

Copy link
Copy Markdown
Collaborator Author

PR #8 Review: "Integrate Ralph Loop and enhance Git control UI/UX features"

49 commits | 81 files changed | +18,003 / -7,483 lines | developmentmain


🔴 Critical Bug

B1. remote.pull() is not a valid GitPython API

File: backend/app/git_routes.py:556

remote.pull(branch_spec)  # Will throw at runtime

Should be repo.git.pull(remote_name, branch_spec) or remote.fetch() followed by a merge. This will crash at runtime on push/pull.


🟡 Security Issues

# File Issue Severity
S1 backend/app/__init__.py:30-36 SQL built with f-strings in _copy_legacy_rows — fragile pattern, should use SQLAlchemy text() with bound parameters Low
S2 backend/app/routes/sessions.py:108, projects.py:91, messages.py:57 str(exc) in error responses leaks internal paths, SQL details, library internals. Fixed in PRD routes only, but all other routes still expose raw exception strings Medium
S3 backend/app/routes/helpers.py ntfy topic_url not validated — a malicious user could set it to an internal endpoint (SSRF) Medium
S4 backend/app/git_routes.py:591 Git remote URL accepts any scheme including file:// — no validation Low
S5 backend/app/routes/helpers.py:130-143 Symlink traversal possible in _resolve_project_relative_path.resolve() follows symlinks, check could be bypassed Low

🟠 Other Bugs

# Issue File
B2 apiGitDiff has no explicit return type, .then(r => r.diff) loses wrapper type frontend/src/api.ts:637
B3 Race condition in _dispatch_queued_runs_active_runs counter can desync under concurrent threads; max(0, ...) is a band-aid backend/app/scheduler.py
B4 handleRejectQuestion referenced in QuestionCard callback — verify this function is defined frontend/src/App.tsx:4700
B5 apiGitDiff exported from api.ts but no loading mechanism visible in App.tsxgitDiffEntries state may never be populated frontend/src/api.ts / App.tsx
B6 handleSaveGlobalDefaultModel referenced in Settings card — verify it exists frontend/src/App.tsx:4330

📝 Code Quality Concerns

# Issue File(s)
Q1 App.tsx remains enormous despite refactoring — should split into ChatView, TasksView, FilesView frontend/src/App.tsx
Q2 All route modules use from .helpers import * with # ruff: noqa: F405 — impossible to trace symbol origins, should use explicit imports backend/app/routes/*.py (all 10)
Q3 Duplicate global app, settings, ... boilerplate repeated across 10 route files — should extract to shared base or Flask extension pattern backend/app/routes/*.py
Q4 ralph.sh hardcodes --model=claude-3-5-sonnet-20241022 — should be configurable via env var scripts/ralph/ralph.sh:107
Q5 No timeout on requests.post in _send_ntfy_notification — could block notification thread indefinitely backend/app/routes/helpers.py
Q6 Cron parser brute-forces up to 527,040 iterations (minute-by-minute scan for a year) — should use smarter algorithm or add timeout backend/app/scheduler.py:100
Q7 git diff endpoint loads full diff for every changed file into memory — no pagination or file-specific requests backend/app/git_routes.py:565-610
Q8 _build_untracked_patch reads entire file into memory — even with 1MB limit, slow for large repos backend/app/git_routes.py:52-76
Q9 No rate limiting on git operations — rapid successive push/pull/commit could corrupt git index backend/app/git_routes.py
Q10 Branch names only .strip() checked for emptiness — no validation against reserved names (HEAD, .) or length limits backend/app/git_routes.py:371,400

📋 Commit History Issues


✅ Positives

  • Path traversal protection in _resolve_project_relative_path is well-implemented
  • GitPython used instead of shell subprocesses — eliminates shell injection risk
  • @auth_required decorators consistently applied on all new routes
  • Input validation with _get_request_data() helper in git routes
  • Schema migration logic is careful with SQLite-specific handling
  • TypeScript types are comprehensive for new types (GitDiffEntry, QuestionRequest, etc.)
  • PWA support (beforeinstallprompt, service worker) is well-structured
  • Notification debouncing (2.5s) prevents notification spam
  • Ralph Loop integration with PRD tracking is clean and well-scoped
  • No print() debug statements found in backend code
  • Stack trace exposure was fixed in PRD routes (commit 33ebe71)

Recommendation

Do not merge as-is. Fix the following before merging:

  1. B1 (Critical): Fix remote.pull() — this is a runtime crash
  2. S2 (Medium): Replace str(exc) with generic error messages in all route error responses, log details server-side
  3. S3 (Medium): Validate ntfy topic_url — ensure it's https or an allowed domain
  4. B4/B6: Verify handleRejectQuestion and handleSaveGlobalDefaultModel are defined in App.tsx
  5. Commit history: Squash 49 commits down to ~8-10 logical feature commits, remove merge commits

Everything else can be addressed in follow-up PRs.

Copilot AI 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.

Pull request overview

This PR expands the mobile OpenCode controller into a more full-featured control surface by adding Ralph (PRD-driven loop) tooling, Git controls, notifications, and richer streaming/timeline UI support across the backend + frontend.

Changes:

  • Add Ralph loop support (CLI script + PRD scaffold + Tasks UI panel) and new OpenCode “skills” docs.
  • Introduce Git integration (backend routes + frontend API/types + diff/timeline UI).
  • Enhance runtime/session controls, notifications, question/approval handling, and project list virtualization.

Reviewed changes

Copilot reviewed 69 out of 71 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
ui-spec.md Removed old UI spec document
scripts/ralph/ralph.sh Adds Ralph CLI iteration loop
scripts/ralph/prompt.md Ralph agent prompt template
README.md Refresh docs for new features/endpoints
prd.json.example Example PRD JSON template
PDR.md Removed old PRD document
frontend/src/utils/taskUtils.ts Task status/date-time helpers
frontend/src/utils/streamUtils.ts Stream parsing + classification helpers
frontend/src/utils/projectUtils.ts Project path + session sorting helpers
frontend/src/utils/miscUtils.ts Misc UI helpers (telemetry, install, etc.)
frontend/src/utils/messageUtils.ts Timeline/activity summarization utilities
frontend/src/utils/formatting.ts Formatting helpers for UI labels/times
frontend/src/utils/fileUtils.ts File tree + language/path helpers
frontend/src/types/internal.ts Internal UI/telemetry timeline types
frontend/src/types.ts Expand shared API/types (git, PRD, notifications, etc.)
frontend/src/components/ui/QuestionCard.tsx UI for multi-choice agent questions
frontend/src/components/ui/FixtureBanner.tsx Dev fixture mode banner
frontend/src/components/ui/CommandPickerModal.tsx Modal to pick server commands
frontend/src/components/toolbar/RuntimeControls.tsx Model/agent/session runtime toolbar
frontend/src/components/toolbar/NotificationControls.tsx Notification settings UI
frontend/src/components/toolbar/InstallControls.tsx PWA install UI
frontend/src/components/tasks/RalphLoopPanel.tsx Ralph PRD tracker/task creator UI
frontend/src/components/projects/VirtualizedProjectList.tsx Virtualized project list rendering
frontend/src/components/projects/ProjectItem.tsx Project list row UI
frontend/src/components/chat/TaskRunTimelineRow.tsx Timeline rendering for task runs
frontend/src/components/chat/RichMessageText.tsx Markdown rendering for messages
frontend/src/components/chat/MessageParts.tsx Render message parts + collapse group
frontend/src/components/chat/MessagePartCard.tsx UI for tool/step/file/patch parts
frontend/src/components/chat/MessageBubble.tsx Message bubble + parts + TTS button
frontend/src/components/chat/EmptyState.tsx Chat empty state component
frontend/src/components/chat/DiffPanel.tsx Session/git diff panel UI
frontend/src/components/chat/ChatTransitionStrip.tsx Status strip for transitions
frontend/src/components/chat/ChatStateCard.tsx Generic chat state card
frontend/src/components/chat/AgentActivityCard.tsx Collapsed activity grouping UI
frontend/src/components/auth/LoginView.tsx Single-password login view
frontend/src/api.ts Expand frontend API client (git, notifications, PRD, questions, summarize)
frontend/package.json Add markdown dependencies
docs/task-feature-test-plan.md Removed old manual test plan doc
backend/tests/test_git_diff.py Tests for git diff endpoint (untracked/limits)
backend/requirements.txt Adds GitPython dependency
backend/app/routes/tasks.py Task + PRD routes
backend/app/routes/stt.py STT/TTS routes
backend/app/routes/sessions.py Session list/create/switch/delete/abort/summarize routes
backend/app/routes/scheduler_routes.py Scheduler status endpoint
backend/app/routes/runtime.py Runtime model/agent options endpoints
backend/app/routes/projects.py Project list/create/sync + app settings/state
backend/app/routes/opencode.py OpenCode health/commands endpoints
backend/app/routes/notifications.py Notification settings + ntfy endpoints
backend/app/routes/files.py File tree/list/read/download/archive endpoints
backend/app/routes/init.py API blueprint + route registration
backend/app/opencode.py Adds question + compact/summarize client methods
backend/app/config.py Adds NOTIFICATION_NTFY_TOPIC_URL setting
backend/app/init.py Registers git routes in app factory
AGENTS.md Documents Ralph loop usage/files
.opencode/skills/ralph/SKILL.md Ralph PRD converter skill docs
.opencode/skills/prd/SKILL.md PRD generator skill docs
.opencode/skills/frontend-design/SKILL.md Frontend design skill docs update
.opencode/skills/dev-browser/SKILL.md Dev-browser (Chrome MCP) skill docs
.opencode/skills/compound-engineering/SKILL.md Compound engineering workflow docs
.env.example Adds NOTIFICATION_NTFY_TOPIC_URL env var
Files not reviewed (1)
  • frontend/package-lock.json: Language not supported

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +26 to +33
export function findPermissionId(value: unknown): string | null {
if (typeof value === "string") {
return null;
}

if (Array.isArray(value)) {
for (const item of value) {
const found = findPermissionId(item);
Comment on lines +8 to +15
function DiffLine({ line }: { line: string }) {
const cls = line.startsWith("+")
? "diff-line-add"
: line.startsWith("-")
? "diff-line-del"
: line.startsWith("@@")
? "diff-line-hunk"
: "";
Comment on lines +9 to +13
onSelect,
emptyLabel,
searchQuery,
totalLabel,
hasMore,
Comment thread scripts/ralph/ralph.sh
Comment on lines +7 to +9
MAX_ITERATIONS=${1:-10}
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PRD_FILE="$SCRIPT_DIR/prd.json"
@Rishabh-Bajpai

Copy link
Copy Markdown
Collaborator Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented May 21, 2026

Copy link
Copy Markdown
✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@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: 19

Note

Due to the large number of review comments, Critical, Major severity comments were prioritized as inline comments.

🟡 Minor comments (4)
frontend/src/components/chat/MessagePartCard.tsx-143-169 (1)

143-169: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Fix zero-cost handling in step-finish placeholder logic.

Line 167 uses !cost, so cost = 0 is treated as absent and can render a contradictory fallback message.

Proposed patch
 function StepCard({ part }: { part: Record<string, unknown> }) {
@@
   const tokens = part.tokens as Record<string, unknown> | undefined;
   const cost = typeof part.cost === "number" ? part.cost : null;
+  const hasTokens = !!tokens && Object.keys(tokens).length > 0;
@@
-          {tokens && Object.keys(tokens).length > 0 && (
+          {hasTokens && (
             <div className="step-info">
@@
-      {partType === "step-finish" && !reason && !cost && !tokens && (
+      {partType === "step-finish" && !reason && cost === null && !hasTokens && (
         <p className="step-placeholder">Step finished</p>
       )}
🤖 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/chat/MessagePartCard.tsx` around lines 143 - 169, The
placeholder for the "step-finish" branch treats cost=0 as falsy because it uses
"!cost"; change the absence checks to explicitly test for null/emptiness:
replace the condition `partType === "step-finish" && !reason && !cost &&
!tokens` with checks that reason is falsy, cost is null (e.g., cost === null),
and tokens is null/empty (e.g., !tokens || Object.keys(tokens).length === 0) so
a zero cost still displays correctly; update the conditional around the
step-placeholder rendering to use these explicit checks (refer to variables
partType, reason, cost, tokens and the "step-finish" placeholder).
frontend/src/components/auth/LoginView.tsx-24-30 (1)

24-30: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Add an explicit label for the password field.

Line 24 currently relies on placeholder text only; add a real label for accessibility and better form usability.

Suggested fix
       <form className="auth-card" onSubmit={handleSubmit}>
         <h1>OpenCode Controller</h1>
         <p>Single-password access</p>
+        <label htmlFor="login-password">Password</label>
         <input
+          id="login-password"
           type="password"
           value={password}
           onChange={(event) => setPassword(event.target.value)}
           placeholder="Enter password"
           autoComplete="current-password"
         />
🤖 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/auth/LoginView.tsx` around lines 24 - 30, The
password input in LoginView.tsx lacks an explicit label; add a real <label>
associated with the input (e.g., label text "Password" and htmlFor="password")
and give the input a matching id (e.g., id="password") so setPassword and the
existing onChange remain unchanged; if the design requires hiding the visual
label, use a visually-hidden CSS class but keep the semantic label for
accessibility.
frontend/src/utils/messageUtils.ts-368-390 (1)

368-390: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Parse boolean-like payload values explicitly.

Line 368 currently uses Boolean(value), so "false"/"0" are interpreted as true, which can flip task run flags in the UI.

Suggested fix
-  const asBoolean = (value: unknown) => Boolean(value);
+  const asBoolean = (value: unknown) => {
+    if (typeof value === "boolean") {
+      return value;
+    }
+    if (typeof value === "number") {
+      return value !== 0;
+    }
+    if (typeof value === "string") {
+      const normalized = value.trim().toLowerCase();
+      if (normalized === "true" || normalized === "1") {
+        return true;
+      }
+      if (normalized === "false" || normalized === "0" || normalized === "") {
+        return false;
+      }
+    }
+    return false;
+  };
🤖 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/utils/messageUtils.ts` around lines 368 - 390, Replace the
current asBoolean implementation (used by heartbeatLoaded and goalAttempted) so
it returns true/false/null instead of using Boolean(value); specifically, have
asBoolean(value: unknown) return true for boolean true, number 1, or strings
"true"/"1" (case-insensitive), return false for boolean false, number 0, or
strings "false"/"0", and return null for any other values; keep the rest of the
mapping (taskRunId, runNumber, goalMet) unchanged and ensure callers like
heartbeatLoaded and goalAttempted accept the new boolean | null result.
frontend/src/utils/projectUtils.ts-7-8 (1)

7-8: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Avoid returning // when the source path is filesystem root.

At Line 8, sourcePath === "/" becomes "//". That creates malformed root suggestions and can cascade into broken joined paths.

Suggested fix
   const slashIndex = sourcePath.lastIndexOf("/");
   if (slashIndex <= 0) {
-    return sourcePath ? `${sourcePath}/` : "";
+    if (!sourcePath) return "";
+    return sourcePath === "/" ? "/" : `${sourcePath}/`;
   }
🤖 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/utils/projectUtils.ts` around lines 7 - 8, The current
conditional returns `${sourcePath}/` when slashIndex <= 0, which yields "//" for
sourcePath === "/"; update the branch that checks slashIndex and sourcePath so
that when sourcePath === "/" you return "/" (not "//"), otherwise return
`${sourcePath}/`; locate the conditional that uses slashIndex and sourcePath in
frontend/src/utils/projectUtils.ts and replace the single return with a check
like if (sourcePath === "/") return "/"; else return `${sourcePath}/`.
🧹 Nitpick comments (5)
scripts/ralph/ralph.sh (3)

118-118: ⚡ Quick win

Quote the variable to prevent word splitting.

The unquoted $MAX_ITERATIONS in the seq command could cause unexpected behavior if the variable contains whitespace or special characters.

🛡️ Proposed fix
-for i in $(seq 1 $MAX_ITERATIONS); do
+for i in $(seq 1 "$MAX_ITERATIONS"); do
🤖 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 `@scripts/ralph/ralph.sh` at line 118, The loop uses an unquoted variable in
the seq call which can cause word-splitting; update the for-loop to quote
MAX_ITERATIONS in the command substitution so seq receives it as a single
argument (look for the for loop that uses $(seq 1 $MAX_ITERATIONS) and change
the invocation to use a quoted variable).

55-64: ⚡ Quick win

Prevent archive folder name collisions.

If the same branch is archived multiple times on the same date, the second archive will overwrite the first. Adding a timestamp would prevent data loss.

📁 Proposed fix
-    DATE=$(date +%Y-%m-%d)
+    TIMESTAMP=$(date +%Y-%m-%d-%H%M%S)
     # Strip "ralph/" prefix from branch name for folder
     FOLDER_NAME=$(echo "$LAST_BRANCH" | sed 's|^ralph/||')
-    ARCHIVE_FOLDER="$ARCHIVE_DIR/$DATE-$FOLDER_NAME"
+    ARCHIVE_FOLDER="$ARCHIVE_DIR/$TIMESTAMP-$FOLDER_NAME"
 
     echo "Archiving previous run: $LAST_BRANCH"
🤖 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 `@scripts/ralph/ralph.sh` around lines 55 - 64, The archive path can collide
when the same branch is archived on the same DATE; modify the ARCHIVE_FOLDER
computation to include a time component (e.g., append a timestamp or seconds
since epoch) so each archive is unique. Locate the variables DATE, FOLDER_NAME
and ARCHIVE_FOLDER and change DATE or ARCHIVE_FOLDER to include a timestamp
(from date +%s or date +%Y-%m-%d_%H%M%S) before mkdir -p and the cp commands so
the new folder name is unique while keeping the existing echo messages and
file-copy logic intact.

92-92: ⚡ Quick win

Make model name configurable via environment variable.

The model is currently hardcoded to claude-3-5-sonnet-20241022, limiting flexibility for users who want to use different models or test with newer versions.

♻️ Proposed fix
-MODEL_FLAGS=(--model=claude-3-5-sonnet-20241022)
+RALPH_MODEL="${RALPH_MODEL:-claude-3-5-sonnet-20241022}"
+MODEL_FLAGS=(--model="$RALPH_MODEL")

This allows users to override with RALPH_MODEL=other-model ./ralph.sh while preserving the current default.

🤖 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 `@scripts/ralph/ralph.sh` at line 92, Replace the hardcoded model string in
MODEL_FLAGS with a shell parameter expansion that reads RALPH_MODEL and falls
back to the current default; update the assignment to set MODEL_FLAGS using the
value of RALPH_MODEL if present (e.g., use
${RALPH_MODEL:-claude-3-5-sonnet-20241022}) so callers can override by exporting
RALPH_MODEL, while preserving the existing default for MODEL_FLAGS.
scripts/ralph/prompt.md (1)

27-36: 💤 Low value

Add language identifiers to fenced code blocks.

Markdown linters prefer explicit language tags on code blocks for proper syntax highlighting and rendering.

📝 Proposed fix

At line 27, specify the language:

-```
+```markdown
 ## [Date/Time] - [Story ID]

At line 44, specify the language:

-```
+```markdown
 ## Codebase Patterns

Also applies to: 44-49

🤖 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 `@scripts/ralph/prompt.md` around lines 27 - 36, The Markdown file uses fenced
code blocks without language identifiers which fails linters; update the two
fenced blocks shown (the one containing the "## [Date/Time] - [Story ID]" header
and the one containing "## Codebase Patterns") to include explicit language tags
(e.g., add ```markdown after the opening backticks) so linters and renderers get
proper syntax highlighting; locate those blocks in scripts/ralph/prompt.md and
add the language identifier to each opening fence.
frontend/src/components/chat/DiffPanel.tsx (1)

62-65: ⚡ Quick win

Default to the first non-empty diff source.

Starting on "session" can show an empty state even when git diffs exist. Prefer selecting the first non-empty tab and syncing when counts change.

Proposed patch
-import React, { useState } from "react";
+import React, { useEffect, useState } from "react";
@@
-  const [tab, setTab] = useState<"session" | "git">("session");
+  const [tab, setTab] = useState<"session" | "git">(
+    sessionDiff.length > 0 ? "session" : "git"
+  );
   const sessionCount = sessionDiff.length;
   const gitCount = gitDiff.length;
   if (sessionCount === 0 && gitCount === 0) return null;
+
+  useEffect(() => {
+    if (tab === "session" && sessionCount === 0 && gitCount > 0) setTab("git");
+    if (tab === "git" && gitCount === 0 && sessionCount > 0) setTab("session");
+  }, [tab, sessionCount, gitCount]);
🤖 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/chat/DiffPanel.tsx` around lines 62 - 65, The
component currently initializes tab state to "session" and can render an empty
panel when sessionDiff is empty even though gitDiff has items; modify the
initialization and add an effect that watches sessionDiff and gitDiff (or
sessionCount/gitCount) to setTab to the first non-empty source ("session" if
sessionCount>0 else "git" if gitCount>0) and keep it in sync when counts change;
update the useState default to choose based on initial sessionDiff/gitDiff and
add a useEffect that calls setTab appropriately when sessionCount or gitCount
change so the UI always shows the first available non-empty tab.
🤖 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/app/git_routes.py`:
- Around line 279-283: The endpoints in git_routes.py currently return raw
exceptions (str(e)) to clients (e.g., the Repo.init(project.path) block and the
other listed try/excepts); replace these by logging the exception server-side
and returning a generic error response. Concretely, inside each except block
(for example in the handler that calls Repo.init, and the other handlers
referenced at lines ~314, 332, 357, etc.), call the Flask logger or module
logger with full exception info (e.g., app.logger.exception(...) or
logger.exception(...)) and then return jsonify({"error": "Internal server
error"}), 500 instead of jsonify({"error": str(e)}), 500; do this consistently
across all handlers in git_routes.py that currently expose str(e).
- Around line 636-647: Validate the incoming url string before calling
_get_named_remote / existing_remote.set_url / repo.create_remote: parse the url
(e.g., with urllib.parse), reject unsafe or local schemes like file: and any
missing/invalid components, and only accept a whitelisted set of schemes (e.g.,
http, https, ssh/git formats) and ensure netloc or valid host/path is present;
if validation fails return a 400 JSON error and do not call
existing_remote.set_url or repo.create_remote.

In `@backend/app/routes/files.py`:
- Around line 187-194: The temp ZIP is being unlinked too early via
`@after_this_request`; instead register deletion on response close using
response.call_on_close so the file is removed only after send_file(archive_path)
finishes; locate the cleanup handler named cleanup_archive (and the similar
cleanup block around the other occurrence using archive_path) and replace the
after_this_request decorator logic with a response.call_on_close(lambda:
archive_path.unlink(missing_ok=True) wrapped with try/except OSError) to ensure
safe deletion after the response is fully sent.

In `@backend/app/routes/helpers.py`:
- Around line 483-502: Validate ntfyTopicUrl before storing or posting: update
_set_notification_settings to parse and validate the provided ntfy_topic_url and
update _send_ntfy_notification to re-validate topic_url before making the
request. Ensure validation enforces scheme is http or https, hostname is not
localhost/127.0.0.1 and does not resolve to loopback, link-local, or
private-network IP ranges (e.g., 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, ::1,
fe80::/10, fc00::/7), and reject or raise/abort on invalid URLs; use
urllib.parse to parse the URL and socket.getaddrinfo (or equivalent) to resolve
and check IPs, and only call requests.post from _send_ntfy_notification after
the validation passes so untrusted hosts cannot be contacted.

In `@backend/app/routes/messages.py`:
- Around line 39-61: The handler in messages.py (the get_project_messages route)
is currently catching all exceptions and echoing raw exception text to clients;
instead, validate inputs first (parse limit with int(...) inside a try/except
ValueError and return a 400 with a stable message like "invalid 'limit'
parameter"), keep the session validation branch that returns 400 for mismatched
sessions (_session_matches_project), and for upstream calls
(opencode_client.get_session, _ensure_project_session,
opencode_client.list_messages, and TimelineEvent.query) catch only
upstream/network/database errors, log the full exception server-side (use
logger.error(..., exc_info=True)) and return a generic 502 message like "failed
to load messages" without including exc text or internal details; make analogous
changes for the other handlers referenced so input parsing errors return 400 and
upstream failures return generic 502 with server-side logging.

In `@backend/app/routes/opencode.py`:
- Around line 33-40: The except blocks that currently return jsonify({"healthy":
False, "error": str(exc), "upstreamBaseUrl": settings.opencode_base_url}) should
be changed to log the full exception server-side (e.g., using
current_app.logger.exception or app.logger.error with the exc) and instead
return a generic JSON payload such as {"healthy": False, "error": "upstream
service error"} without including str(exc) or settings.opencode_base_url; update
both the except handling around the health check and the similar handler at the
other occurrence (the blocks that capture Exception as exc and call jsonify) to
perform logging then return the sanitized response.

In `@backend/app/routes/projects.py`:
- Around line 72-73: Replace responses that embed raw exception text (the except
Exception as exc blocks that call jsonify with f"Failed ...: {exc}" at the shown
locations) so they log the full exception locally and return a stable, generic
error message and status to the client (e.g., "Failed to sync projects" with 502
or "Failed to create project" with 4xx/5xx as appropriate). Use the existing
application logger (e.g., current_app.logger or app.logger) to log the exception
and stack trace, and remove the exception string interpolation from the JSON
response; keep the same HTTP status codes currently returned.

In `@backend/app/routes/runtime.py`:
- Around line 68-72: The endpoint currently returns raw exception text from
calls to opencode_client.get_config() and
opencode_client.list_config_providers() (and similarly in the other 502 path
around lines 155-159); change these handlers to log the exception server-side
(use the existing logger) and return a generic error JSON like {"error": "Failed
to load runtime options"} with the 502 status, removing f"{exc}" from the
response body; keep the try/except blocks around opencode_client.get_config() /
list_config_providers() and the other failing call, but replace the
client-facing message with a generic one and call logger.exception or
logger.error to record exc.
- Around line 31-39: The /health route (api_bp.get("/health") -> app_health)
currently always returns healthy: true; update app_health to perform a real
liveness check against the OpenCode proxy (use the configured OpenCode URL from
settings, e.g. settings.opencode_proxy_url or equivalent) by issuing a
short-timeout HTTP request and treating non-2xx/non-reachable responses as
unhealthy, catching exceptions and returning healthy: False (and appropriate
non-200 status) when the proxy is down; ensure you log the failure and keep
returning service and env fields (settings.app_env) as before.

In `@backend/app/routes/sessions.py`:
- Around line 37-41: The except blocks in sessions route handlers (e.g., around
calls to _clear_project_session_cache and _list_project_sessions) currently
interpolate the exception object into the JSON error response; replace those
responses so they return a generic error message (e.g., {"error": "Failed to
load sessions"}) without including exc, and keep the same 502 status; apply the
same change to all similar handlers in this file (the other except blocks that
wrap calls like _get_project_session, _create_session, _delete_session, etc.) so
no upstream/internal exception details are echoed to clients while preserving
logging of exc internally if needed.
- Around line 343-352: The fallback incorrectly parses the default mapping by
taking default_map's first value and treating it as "provider/model"; instead,
use the default_map's first key and value as provider_id and model_id
respectively. In the block around provider_data =
opencode_client.list_config_providers() where default_map is derived, when
provider_id or model_id are missing, set provider_id to the first key from
default_map and model_id to default_map[first_key] (ensuring the value is a
string) rather than partitioning a single string; update handling of
default_model and the variables provider_id and model_id accordingly.
- Around line 272-292: The handler currently treats any exception from
_resolve_project_session and friends as a backend error; catch ValueError raised
by _resolve_project_session (and any input-validation related errors)
separately, return a 400 JSON response with the validation message and do not
set project.session_status to "error" (leave status/last_activity_at unchanged
or set to the appropriate client-error state), while keeping the existing broad
except for unexpected exceptions that still set project.session_status="error"
and return 502; reference _resolve_project_session, abort_session,
project.session_status, and the existing except block when implementing this
change.

In `@backend/app/routes/stt.py`:
- Around line 64-72: The code currently calls upload.read() (the local variable
upload from request.files.get("audio")) which can load an unbounded file into
memory; before calling upload.read() in the STT route, check
request.content_length (or a configured max like
current_app.config['MAX_UPLOAD_SIZE'] / MAX_CONTENT_LENGTH) and return 413 if it
exceeds the limit, or alternatively stream the file to disk using
upload.save(temp_path) or read in bounded chunks to enforce the size cap; update
the conditional around upload.read() (and the error responses for
empty/oversized payloads) to use this size check so large uploads are rejected
or streamed before they are fully read into memory.
- Around line 107-128: Replace the handlers that return the raw exception text
(currently returning jsonify({"error": f"STT transcription failed: {exc}"}) in
the try/except blocks around the requests.post/response handling and the similar
block at lines 165-176) so they return a generic error payload (e.g.,
jsonify({"error": "STT transcription failed"}), 502) while logging the full
exception server-side using the module/app logger (call logger.exception(...) or
app.logger.exception(...) with context like "STT transcription failed") so
provider/backend details are recorded in logs but not leaked to clients.

In `@backend/app/routes/tasks.py`:
- Around line 110-120: The handler currently treats a provided but-not-found id
as "no id" and proceeds to create a new ScheduledTask; change the flow so that
after parsing task_id and resolving task via ScheduledTask.query.filter_by(...),
if a non-empty task_id was provided and task is None you return an HTTP 404 (or
400) JSON error instead of falling through to the creation branch. Locate the
task_id variable and the lookup using ScheduledTask.query.filter_by(...) and
prevent calling db.session.add(task) when the client explicitly supplied an id
that wasn't found; keep the existing TypeError/ValueError handling for malformed
ids.

In `@frontend/src/components/toolbar/RuntimeControls.tsx`:
- Around line 120-135: The dropdown currently omits the "+ New session" option
when sortedSessions.length === 0, preventing onSessionCreate from being
triggered; update the JSX in RuntimeControls (the component rendering
sortedSessions.map and the option with value="__new__") to always include an
<option value="__new__">+ New session</option> regardless of whether
sortedSessions is empty—e.g., keep the placeholder ("Loading..." or "No
session") when there are no sessions but render the "__new__" option outside or
in both branches so the create action remains reachable.

In `@frontend/src/GitView.tsx`:
- Around line 265-335: Add a request-id guard so async responses from
loadGitData and loadHistory don't overwrite state for a newer projectId: create
a mutable ref (e.g., requestsRef) that you increment/set to a unique token
inside useEffect before calling loadGitData, pass or capture the token inside
loadGitData and loadHistory, and before every setState call (setStatus,
setBranches, setRemoteUrl, setHistory, setHistoryHasMore, setError, setLoading,
setRefreshing, etc.) check that the current token still matches the ref; if it
does not, return early and skip state updates; also update the token on
projectId change and optionally clear/abort any outstanding work on unmount.

In `@frontend/src/utils/streamUtils.ts`:
- Around line 83-86: The current check in streamUtils.ts that treats any string
response as resolved (const response = record.response ?? record.decision ??
record.action; if (typeof response === "string") return true;) is too broad;
change it to only treat explicit resolved decision values as resolved (e.g.,
check response === "allow" || response === "deny" or map to a small whitelist of
resolved string constants), or implement a helper like
isResolvedDecision(response) that returns true only for those known resolved
values; update the conditional that references response (and any callers that
rely on it) so pending/unknown string decisions are not treated as resolved.

---

Minor comments:
In `@frontend/src/components/auth/LoginView.tsx`:
- Around line 24-30: The password input in LoginView.tsx lacks an explicit
label; add a real <label> associated with the input (e.g., label text "Password"
and htmlFor="password") and give the input a matching id (e.g., id="password")
so setPassword and the existing onChange remain unchanged; if the design
requires hiding the visual label, use a visually-hidden CSS class but keep the
semantic label for accessibility.

In `@frontend/src/components/chat/MessagePartCard.tsx`:
- Around line 143-169: The placeholder for the "step-finish" branch treats
cost=0 as falsy because it uses "!cost"; change the absence checks to explicitly
test for null/emptiness: replace the condition `partType === "step-finish" &&
!reason && !cost && !tokens` with checks that reason is falsy, cost is null
(e.g., cost === null), and tokens is null/empty (e.g., !tokens ||
Object.keys(tokens).length === 0) so a zero cost still displays correctly;
update the conditional around the step-placeholder rendering to use these
explicit checks (refer to variables partType, reason, cost, tokens and the
"step-finish" placeholder).

In `@frontend/src/utils/messageUtils.ts`:
- Around line 368-390: Replace the current asBoolean implementation (used by
heartbeatLoaded and goalAttempted) so it returns true/false/null instead of
using Boolean(value); specifically, have asBoolean(value: unknown) return true
for boolean true, number 1, or strings "true"/"1" (case-insensitive), return
false for boolean false, number 0, or strings "false"/"0", and return null for
any other values; keep the rest of the mapping (taskRunId, runNumber, goalMet)
unchanged and ensure callers like heartbeatLoaded and goalAttempted accept the
new boolean | null result.

In `@frontend/src/utils/projectUtils.ts`:
- Around line 7-8: The current conditional returns `${sourcePath}/` when
slashIndex <= 0, which yields "//" for sourcePath === "/"; update the branch
that checks slashIndex and sourcePath so that when sourcePath === "/" you return
"/" (not "//"), otherwise return `${sourcePath}/`; locate the conditional that
uses slashIndex and sourcePath in frontend/src/utils/projectUtils.ts and replace
the single return with a check like if (sourcePath === "/") return "/"; else
return `${sourcePath}/`.

---

Nitpick comments:
In `@frontend/src/components/chat/DiffPanel.tsx`:
- Around line 62-65: The component currently initializes tab state to "session"
and can render an empty panel when sessionDiff is empty even though gitDiff has
items; modify the initialization and add an effect that watches sessionDiff and
gitDiff (or sessionCount/gitCount) to setTab to the first non-empty source
("session" if sessionCount>0 else "git" if gitCount>0) and keep it in sync when
counts change; update the useState default to choose based on initial
sessionDiff/gitDiff and add a useEffect that calls setTab appropriately when
sessionCount or gitCount change so the UI always shows the first available
non-empty tab.

In `@scripts/ralph/prompt.md`:
- Around line 27-36: The Markdown file uses fenced code blocks without language
identifiers which fails linters; update the two fenced blocks shown (the one
containing the "## [Date/Time] - [Story ID]" header and the one containing "##
Codebase Patterns") to include explicit language tags (e.g., add ```markdown
after the opening backticks) so linters and renderers get proper syntax
highlighting; locate those blocks in scripts/ralph/prompt.md and add the
language identifier to each opening fence.

In `@scripts/ralph/ralph.sh`:
- Line 118: The loop uses an unquoted variable in the seq call which can cause
word-splitting; update the for-loop to quote MAX_ITERATIONS in the command
substitution so seq receives it as a single argument (look for the for loop that
uses $(seq 1 $MAX_ITERATIONS) and change the invocation to use a quoted
variable).
- Around line 55-64: The archive path can collide when the same branch is
archived on the same DATE; modify the ARCHIVE_FOLDER computation to include a
time component (e.g., append a timestamp or seconds since epoch) so each archive
is unique. Locate the variables DATE, FOLDER_NAME and ARCHIVE_FOLDER and change
DATE or ARCHIVE_FOLDER to include a timestamp (from date +%s or date
+%Y-%m-%d_%H%M%S) before mkdir -p and the cp commands so the new folder name is
unique while keeping the existing echo messages and file-copy logic intact.
- Line 92: Replace the hardcoded model string in MODEL_FLAGS with a shell
parameter expansion that reads RALPH_MODEL and falls back to the current
default; update the assignment to set MODEL_FLAGS using the value of RALPH_MODEL
if present (e.g., use ${RALPH_MODEL:-claude-3-5-sonnet-20241022}) so callers can
override by exporting RALPH_MODEL, while preserving the existing default for
MODEL_FLAGS.
🪄 Autofix (Beta)

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: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 108fdb16-ac6a-414e-8611-9787d9a7811b

📥 Commits

Reviewing files that changed from the base of the PR and between a6cc16e and 2f019cb.

⛔ Files ignored due to path filters (1)
  • frontend/package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (70)
  • .env.example
  • .opencode/skills/compound-engineering/SKILL.md
  • .opencode/skills/dev-browser/SKILL.md
  • .opencode/skills/frontend-design/SKILL.md
  • .opencode/skills/prd/SKILL.md
  • .opencode/skills/ralph/SKILL.md
  • AGENTS.md
  • PDR.md
  • README.md
  • backend/app/__init__.py
  • backend/app/config.py
  • backend/app/git_routes.py
  • backend/app/opencode.py
  • backend/app/routes.py
  • backend/app/routes/__init__.py
  • backend/app/routes/files.py
  • backend/app/routes/helpers.py
  • backend/app/routes/messages.py
  • backend/app/routes/notifications.py
  • backend/app/routes/opencode.py
  • backend/app/routes/projects.py
  • backend/app/routes/runtime.py
  • backend/app/routes/scheduler_routes.py
  • backend/app/routes/sessions.py
  • backend/app/routes/stt.py
  • backend/app/routes/tasks.py
  • backend/requirements.txt
  • backend/tests/test_git_diff.py
  • docs/task-feature-test-plan.md
  • frontend/package.json
  • frontend/src/App.tsx
  • frontend/src/GitView.tsx
  • frontend/src/api.ts
  • frontend/src/components/auth/LoginView.tsx
  • frontend/src/components/chat/AgentActivityCard.tsx
  • frontend/src/components/chat/ChatStateCard.tsx
  • frontend/src/components/chat/ChatTransitionStrip.tsx
  • frontend/src/components/chat/DiffPanel.tsx
  • frontend/src/components/chat/EmptyState.tsx
  • frontend/src/components/chat/MessageBubble.tsx
  • frontend/src/components/chat/MessagePartCard.tsx
  • frontend/src/components/chat/MessageParts.tsx
  • frontend/src/components/chat/RichMessageText.tsx
  • frontend/src/components/chat/TaskRunTimelineRow.tsx
  • frontend/src/components/projects/ProjectFilesPanel.tsx
  • frontend/src/components/projects/ProjectItem.tsx
  • frontend/src/components/projects/VirtualizedProjectList.tsx
  • frontend/src/components/tasks/RalphLoopPanel.tsx
  • frontend/src/components/tasks/ScheduledTaskPanel.tsx
  • frontend/src/components/toolbar/InstallControls.tsx
  • frontend/src/components/toolbar/NotificationControls.tsx
  • frontend/src/components/toolbar/RuntimeControls.tsx
  • frontend/src/components/ui/CommandPickerModal.tsx
  • frontend/src/components/ui/FixtureBanner.tsx
  • frontend/src/components/ui/QuestionCard.tsx
  • frontend/src/styles.css
  • frontend/src/types.ts
  • frontend/src/types/internal.ts
  • frontend/src/utils/fileUtils.ts
  • frontend/src/utils/formatting.ts
  • frontend/src/utils/messageUtils.ts
  • frontend/src/utils/miscUtils.ts
  • frontend/src/utils/projectUtils.ts
  • frontend/src/utils/streamUtils.ts
  • frontend/src/utils/taskUtils.ts
  • prd.json.example
  • scripts/ralph/prompt.md
  • scripts/ralph/ralph.sh
  • tasks.md
  • ui-spec.md
💤 Files with no reviewable changes (4)
  • PDR.md
  • tasks.md
  • docs/task-feature-test-plan.md
  • ui-spec.md
📜 Review details
🧰 Additional context used
📓 Path-based instructions (3)
**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

Frontend uses Vite + React 18 + TypeScript for type-safe component development

Files:

  • frontend/src/components/ui/FixtureBanner.tsx
  • frontend/src/components/chat/ChatStateCard.tsx
  • frontend/src/components/auth/LoginView.tsx
  • frontend/src/components/chat/TaskRunTimelineRow.tsx
  • frontend/src/components/toolbar/InstallControls.tsx
  • frontend/src/components/chat/DiffPanel.tsx
  • frontend/src/components/chat/ChatTransitionStrip.tsx
  • frontend/src/components/ui/CommandPickerModal.tsx
  • frontend/src/components/chat/EmptyState.tsx
  • frontend/src/components/chat/MessagePartCard.tsx
  • frontend/src/components/projects/VirtualizedProjectList.tsx
  • frontend/src/components/tasks/RalphLoopPanel.tsx
  • frontend/src/components/toolbar/NotificationControls.tsx
  • frontend/src/components/chat/AgentActivityCard.tsx
  • frontend/src/components/projects/ProjectItem.tsx
  • frontend/src/components/chat/RichMessageText.tsx
  • frontend/src/components/chat/MessageParts.tsx
  • frontend/src/components/toolbar/RuntimeControls.tsx
  • frontend/src/utils/projectUtils.ts
  • frontend/src/utils/fileUtils.ts
  • frontend/src/types/internal.ts
  • frontend/src/components/ui/QuestionCard.tsx
  • frontend/src/components/chat/MessageBubble.tsx
  • frontend/src/components/projects/ProjectFilesPanel.tsx
  • frontend/src/GitView.tsx
  • frontend/src/utils/taskUtils.ts
  • frontend/src/components/tasks/ScheduledTaskPanel.tsx
  • frontend/src/types.ts
  • frontend/src/utils/streamUtils.ts
  • frontend/src/utils/formatting.ts
  • frontend/src/utils/miscUtils.ts
  • frontend/src/utils/messageUtils.ts
  • frontend/src/api.ts
.env*

📄 CodeRabbit inference engine (AGENTS.md)

Set up environment configuration by copying .env.example to .env

Files:

  • .env.example
**/*.py

📄 CodeRabbit inference engine (AGENTS.md)

Backend uses Flask + Flask-SQLAlchemy with SQLite as default database (can be overridden with PostgreSQL via DATABASE_URL)

Files:

  • backend/app/__init__.py
  • backend/tests/test_git_diff.py
  • backend/app/opencode.py
  • backend/app/config.py
  • backend/app/routes/opencode.py
  • backend/app/routes/projects.py
  • backend/app/routes/stt.py
  • backend/app/routes.py
  • backend/app/routes/notifications.py
  • backend/app/routes/runtime.py
  • backend/app/routes/scheduler_routes.py
  • backend/app/routes/__init__.py
  • backend/app/routes/files.py
  • backend/app/routes/sessions.py
  • backend/app/routes/messages.py
  • backend/app/routes/helpers.py
  • backend/app/routes/tasks.py
  • backend/app/git_routes.py
🧠 Learnings (1)
📓 Common learnings
Learnt from: CR
Repo: Rishabh-Bajpai/mobile-opencode-control

Timestamp: 2026-05-21T00:33:27.030Z
Learning: Start services in required order: OpenCode server first (needs available port), then Backend (Flask on localhost:8080), then Frontend (Vite on localhost:5173)
Learnt from: CR
Repo: Rishabh-Bajpai/mobile-opencode-control

Timestamp: 2026-05-21T00:33:27.030Z
Learning: Backend `/api/health` endpoint checks OpenCode proxy availability - if OpenCode isn't running, the health check will fail
🪛 dotenv-linter (4.0.0)
.env.example

[warning] 14-14: [UnorderedKey] The NOTIFICATION_NTFY_TOPIC_URL key should go before the OPENCODE_CORS_ORIGINS key

(UnorderedKey)

🪛 LanguageTool
.opencode/skills/frontend-design/SKILL.md

[style] ~39-~39: “thinking outside the box” can be a clichéd phrase in professional communication. Consider an alternative expression to make your writing more engaging and original.
Context: ...ck, show what can truly be created when thinking outside the box and committing fully to a distinctive v...

(THINK_OUTSIDE_THE_BOX)

.opencode/skills/compound-engineering/SKILL.md

[style] ~22-~22: The wording of this phrase can be improved.
Context: ...*: Feed results back into the system to make the next loop better 80% of compound engineering is in plan...

(MAKE_STYLE_BETTER)


[style] ~189-~189: Three successive sentences begin with the same word. Consider rewording the sentence or use a thesaurus to find a synonym.
Context: ...hat pattern did I discover or create? - What decision was made and why? Document th...

(ENGLISH_WORD_REPEAT_BEGINNING_RULE)

🪛 markdownlint-cli2 (0.22.1)
scripts/ralph/prompt.md

[warning] 27-27: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


[warning] 44-44: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

.opencode/skills/compound-engineering/SKILL.md

[warning] 15-15: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


[warning] 196-196: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


[warning] 205-205: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


[warning] 215-215: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


[warning] 225-225: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

.opencode/skills/prd/SKILL.md

[warning] 36-36: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

.opencode/skills/ralph/SKILL.md

[warning] 102-102: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


[warning] 107-107: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


[warning] 112-112: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

🪛 OpenGrep (1.21.0)
frontend/src/utils/messageUtils.ts

[ERROR] 284-284: Dynamic command passed to child_process.exec/execSync. Use child_process.execFile or spawn with an argument array instead.

(coderabbit.command-injection.exec-js)

🪛 Ruff (0.15.13)
backend/app/routes/opencode.py

[warning] 33-33: Do not catch blind exception: Exception

(BLE001)


[warning] 65-65: Do not catch blind exception: Exception

(BLE001)

backend/app/routes/projects.py

[warning] 72-72: Do not catch blind exception: Exception

(BLE001)


[warning] 204-204: Do not catch blind exception: Exception

(BLE001)

backend/app/routes/stt.py

[warning] 127-127: Do not catch blind exception: Exception

(BLE001)


[warning] 175-175: Do not catch blind exception: Exception

(BLE001)

backend/app/routes.py

[warning] 25-25: __all__ is not sorted

Apply an isort-style sorting to __all__

(RUF022)

backend/app/routes/notifications.py

[warning] 69-69: Do not catch blind exception: Exception

(BLE001)


[warning] 92-92: Do not catch blind exception: Exception

(BLE001)

backend/app/routes/runtime.py

[warning] 71-71: Do not catch blind exception: Exception

(BLE001)


[warning] 158-158: Do not catch blind exception: Exception

(BLE001)

backend/app/routes/__init__.py

[warning] 41-41: __all__ is not sorted

Apply an isort-style sorting to __all__

(RUF022)

backend/app/routes/files.py

[warning] 47-47: Do not catch blind exception: Exception

(BLE001)


[warning] 78-78: Do not catch blind exception: Exception

(BLE001)

backend/app/routes/sessions.py

[warning] 40-40: Do not catch blind exception: Exception

(BLE001)


[warning] 89-89: Do not catch blind exception: Exception

(BLE001)


[warning] 131-131: Do not catch blind exception: Exception

(BLE001)


[warning] 222-222: Do not catch blind exception: Exception

(BLE001)


[warning] 259-259: Do not catch blind exception: Exception

(BLE001)


[warning] 289-289: Do not catch blind exception: Exception

(BLE001)


[warning] 319-319: Do not catch blind exception: Exception

(BLE001)


[warning] 348-348: Prefer next(iter(default_map.keys())) over single element slice

Replace with next(iter(default_map.keys()))

(RUF015)


[warning] 365-365: Do not catch blind exception: Exception

(BLE001)

backend/app/routes/messages.py

[warning] 60-60: Do not catch blind exception: Exception

(BLE001)


[warning] 114-114: Do not catch blind exception: Exception

(BLE001)


[warning] 161-161: Prefer next(iter(default_map.keys())) over single element slice

Replace with next(iter(default_map.keys()))

(RUF015)


[warning] 223-223: Do not catch blind exception: Exception

(BLE001)


[warning] 244-244: Do not catch blind exception: Exception

(BLE001)


[warning] 264-264: Do not catch blind exception: Exception

(BLE001)


[warning] 288-288: Do not catch blind exception: Exception

(BLE001)


[warning] 298-298: Do not catch blind exception: Exception

(BLE001)


[warning] 313-313: Do not catch blind exception: Exception

(BLE001)


[warning] 359-359: Do not catch blind exception: Exception

(BLE001)


[warning] 405-405: Do not catch blind exception: Exception

(BLE001)


[warning] 476-476: Do not catch blind exception: Exception

(BLE001)


[warning] 517-517: Do not catch blind exception: Exception

(BLE001)


[warning] 544-544: Do not catch blind exception: Exception

(BLE001)

backend/app/routes/helpers.py

[warning] 12-66: __all__ is not sorted

Apply an isort-style sorting to __all__

(RUF022)


[warning] 198-198: Do not catch blind exception: Exception

(BLE001)


[warning] 396-396: Do not catch blind exception: Exception

(BLE001)


[warning] 440-440: Do not catch blind exception: Exception

(BLE001)


[warning] 943-943: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling

(B904)

backend/app/routes/tasks.py

[warning] 189-189: Do not catch blind exception: Exception

(BLE001)


[warning] 278-278: Do not catch blind exception: Exception

(BLE001)

backend/app/git_routes.py

[warning] 29-29: Do not catch blind exception: Exception

(BLE001)


[warning] 92-92: Do not catch blind exception: Exception

(BLE001)


[warning] 100-100: Do not catch blind exception: Exception

(BLE001)


[warning] 190-190: Do not catch blind exception: Exception

(BLE001)


[warning] 200-200: Do not catch blind exception: Exception

(BLE001)


[warning] 205-205: Do not catch blind exception: Exception

(BLE001)


[warning] 224-224: Do not catch blind exception: Exception

(BLE001)


[error] 253-254: try-except-continue detected, consider logging the exception

(S112)


[warning] 253-253: Do not catch blind exception: Exception

(BLE001)


[warning] 282-282: Do not catch blind exception: Exception

(BLE001)


[warning] 314-314: Do not catch blind exception: Exception

(BLE001)


[warning] 332-332: Do not catch blind exception: Exception

(BLE001)


[warning] 357-357: Do not catch blind exception: Exception

(BLE001)


[warning] 391-391: Do not catch blind exception: Exception

(BLE001)


[warning] 429-429: Do not catch blind exception: Exception

(BLE001)


[warning] 460-460: Do not catch blind exception: Exception

(BLE001)


[warning] 477-477: Do not catch blind exception: Exception

(BLE001)


[warning] 505-505: Do not catch blind exception: Exception

(BLE001)


[warning] 536-536: Do not catch blind exception: Exception

(BLE001)


[warning] 567-567: Do not catch blind exception: Exception

(BLE001)


[warning] 622-622: Do not catch blind exception: Exception

(BLE001)


[warning] 648-648: Do not catch blind exception: Exception

(BLE001)

🪛 Shellcheck (0.11.0)
scripts/ralph/ralph.sh

[style] 57-57: See if you can use ${variable//search/replace} instead.

(SC2001)


[info] 118-118: Double quote to prevent globbing and word splitting.

(SC2086)

🪛 Stylelint (17.11.1)
frontend/src/styles.css

[error] 1426-1426: Deprecated keyword "break-word" for property "word-break" (declaration-property-value-keyword-no-deprecated)

(declaration-property-value-keyword-no-deprecated)


[error] 1800-1800: Expected keyframe name "transitionStripIn" to be kebab-case (keyframes-name-pattern)

(keyframes-name-pattern)

🔇 Additional comments (43)
frontend/src/GitView.tsx (2)

39-99: LGTM!

Also applies to: 101-245


337-488: LGTM!

Also applies to: 526-875

.env.example (1)

14-14: LGTM!

backend/app/config.py (1)

34-34: LGTM!

Also applies to: 70-70, 123-123

.opencode/skills/compound-engineering/SKILL.md (1)

1-250: LGTM!

.opencode/skills/dev-browser/SKILL.md (1)

1-53: LGTM!

.opencode/skills/frontend-design/SKILL.md (1)

1-39: LGTM!

.opencode/skills/prd/SKILL.md (1)

1-243: LGTM!

.opencode/skills/ralph/SKILL.md (1)

1-260: LGTM!

AGENTS.md (1)

41-82: LGTM!

README.md (1)

5-506: LGTM!

frontend/src/components/chat/AgentActivityCard.tsx (1)

4-74: LGTM!

frontend/src/components/chat/ChatStateCard.tsx (1)

3-18: LGTM!

frontend/src/components/chat/ChatTransitionStrip.tsx (1)

3-18: LGTM!

frontend/src/components/chat/EmptyState.tsx (1)

4-11: LGTM!

frontend/src/components/chat/MessageBubble.tsx (1)

7-77: LGTM!

frontend/src/components/chat/MessageParts.tsx (1)

5-78: LGTM!

frontend/src/components/chat/RichMessageText.tsx (1)

1-32: LGTM!

frontend/src/components/chat/TaskRunTimelineRow.tsx (1)

6-36: LGTM!

frontend/src/components/toolbar/InstallControls.tsx (1)

3-37: LGTM!

frontend/src/components/toolbar/NotificationControls.tsx (1)

4-83: LGTM!

frontend/src/components/ui/CommandPickerModal.tsx (1)

4-57: LGTM!

frontend/src/components/ui/FixtureBanner.tsx (1)

4-6: LGTM!

frontend/src/components/ui/QuestionCard.tsx (1)

5-86: LGTM!

frontend/src/styles.css (1)

1426-1426: ⚡ Quick win

Provide the original <review_comment> (and the related diff/code snippet) so I can rewrite it in the required format.

frontend/package.json (1)

14-15: Confirm react-markdown@10.1.0 and remark-gfm@4.0.1 are safe and React 18 compatible.

  • No known security vulnerabilities/advisories are reported for react-markdown@10.1.0 or remark-gfm@4.0.1 (direct package security listings show none).
  • These are the latest stable releases for the React 18+ line and are compatible with React 18.3.1.
  • Still run npm audit/Snyk for transitive dependencies, and ensure you’re using the official remark-gfm package (not similarly named remark-gfm-v4).
frontend/src/components/projects/ProjectFilesPanel.tsx (1)

1-501: LGTM!

frontend/src/components/projects/ProjectItem.tsx (1)

1-47: LGTM!

frontend/src/components/projects/VirtualizedProjectList.tsx (2)

1-91: LGTM!

Also applies to: 112-145


97-109: ⚡ Quick win

Missing review context: No <review_comment> content (and no diff/code locations) was provided, so I can’t rewrite or verify the comment. Paste the original review comment and the relevant diff/snippet.

frontend/src/components/tasks/RalphLoopPanel.tsx (1)

1-101: LGTM!

frontend/src/components/tasks/ScheduledTaskPanel.tsx (1)

1-454: LGTM!

backend/app/__init__.py (1)

10-10: LGTM!

Also applies to: 266-266

backend/tests/test_git_diff.py (1)

1-106: LGTM!

frontend/src/api.ts (1)

74-79: LGTM!

Also applies to: 142-172, 204-261, 329-339, 520-690

frontend/src/types.ts (1)

3-4: LGTM!

Also applies to: 38-39, 47-56, 78-79, 114-191, 146-155, 296-315

frontend/src/types/internal.ts (1)

1-110: LGTM!

frontend/src/utils/fileUtils.ts (1)

1-92: LGTM!

frontend/src/utils/formatting.ts (1)

1-175: LGTM!

frontend/src/utils/messageUtils.ts (1)

1-367: LGTM!

Also applies to: 391-397

frontend/src/utils/miscUtils.ts (1)

1-186: LGTM!

frontend/src/utils/taskUtils.ts (1)

1-157: LGTM!

prd.json.example (1)

1-65: LGTM!

Comment thread backend/app/git_routes.py Outdated
Comment thread backend/app/git_routes.py Outdated
Comment on lines +187 to +194
@after_this_request
def cleanup_archive(response):
if archive_path is not None:
try:
archive_path.unlink(missing_ok=True)
except OSError:
pass
return response

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

In Flask/Werkzeug, does after_this_request run before a send_file(path) response is fully sent, and is response.call_on_close the recommended hook for deleting a temporary file after the download completes?

💡 Result:

Yes for the first part: Flask’s after_this_request runs before the response is actually sent to the client. Flask’s after_request/after_this_request callbacks are meant to “modify or replace the response before it is sent” [1]. So any cleanup you do in an after_this_request handler happens during request processing while Flask/Werkzeug are still preparing to transmit the response, not after the socket write/send is complete. This is consistent with reports where deleting the file in after_this_request happens “before send_file has completed” [2]. For the second part: response.call_on_close is the recommended hook for deferred cleanup when you need it to run after the response is finished/closed. Werkzeug’s Response.call_on_close registers functions to be called when the response is closed (Response.close runs the functions in the internal _on_close list) [3]. Flask community guidance for “after returning the response” uses after_this_request only to get access to the response object, then defers cleanup with response.call_on_close so it runs at response close time [4]. Recommendation: - If your goal is “delete the temporary file after the download completes”, don’t delete it directly in after_this_request; use response.call_on_close for cleanup instead [1][3][4]. - If you delete by pathname, be aware this still relies on OS/filesystem semantics; a safer approach is to pass a file object/stream (or a NamedTemporaryFile-style object) that remains readable until the response is done [5].

Citations:


Move temporary ZIP cleanup to response.call_on_close (not after_this_request)

after_this_request runs before send_file(archive_path) has finished sending the response, so unlinking the temp file there can break the download or leave files behind. Use response.call_on_close to delete the ZIP after the response is closed.

Also applies to backend/app/routes/files.py (lines 223-228).

♻️ Safer cleanup pattern
-        `@after_this_request`
-        def cleanup_archive(response):
-            if archive_path is not None:
-                try:
-                    archive_path.unlink(missing_ok=True)
-                except OSError:
-                    pass
-            return response
-
         with zipfile.ZipFile(
             archive_path, mode="w", compression=zipfile.ZIP_DEFLATED
         ) as archive:
             ...
@@
-    return send_file(
+    response = send_file(
         archive_path,
         mimetype="application/zip",
         as_attachment=True,
         download_name=archive_name,
     )
+    `@response.call_on_close`
+    def cleanup_archive():
+        if archive_path is not None:
+            try:
+                archive_path.unlink(missing_ok=True)
+            except OSError:
+                pass
+    return response
🤖 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/app/routes/files.py` around lines 187 - 194, The temp ZIP is being
unlinked too early via `@after_this_request`; instead register deletion on
response close using response.call_on_close so the file is removed only after
send_file(archive_path) finishes; locate the cleanup handler named
cleanup_archive (and the similar cleanup block around the other occurrence using
archive_path) and replace the after_this_request decorator logic with a
response.call_on_close(lambda: archive_path.unlink(missing_ok=True) wrapped with
try/except OSError) to ensure safe deletion after the response is fully sent.

Comment thread backend/app/routes/helpers.py
Comment thread backend/app/routes/messages.py Outdated
Comment on lines +110 to +120
try:
body = request.get_json(silent=True) or {}
task_id = str(body.get("id") or body.get("taskId") or "").strip()
task = ScheduledTask.query.filter_by(id=int(task_id), project_id=project_id).first() if task_id else None
except (TypeError, ValueError):
return jsonify({"error": "Invalid task id"}), 400

now = _current_utc_now()
if task is None:
task = ScheduledTask(project_id=project_id, instruction="", interval_minutes=15)
db.session.add(task)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

A missing task lookup silently turns into create.

When id/taskId is provided but doesn't match this project, task stays None and the code falls into the create path. That turns stale UI IDs into duplicate tasks instead of returning a 404/400.

Suggested fix
     try:
         body = request.get_json(silent=True) or {}
         task_id = str(body.get("id") or body.get("taskId") or "").strip()
         task = ScheduledTask.query.filter_by(id=int(task_id), project_id=project_id).first() if task_id else None
     except (TypeError, ValueError):
         return jsonify({"error": "Invalid task id"}), 400
+
+    if task_id and task is None:
+        return jsonify({"error": "Scheduled task not found"}), 404
 
     now = _current_utc_now()
     if task is None:
         task = ScheduledTask(project_id=project_id, instruction="", interval_minutes=15)
         db.session.add(task)
🤖 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/app/routes/tasks.py` around lines 110 - 120, The handler currently
treats a provided but-not-found id as "no id" and proceeds to create a new
ScheduledTask; change the flow so that after parsing task_id and resolving task
via ScheduledTask.query.filter_by(...), if a non-empty task_id was provided and
task is None you return an HTTP 404 (or 400) JSON error instead of falling
through to the creation branch. Locate the task_id variable and the lookup using
ScheduledTask.query.filter_by(...) and prevent calling db.session.add(task) when
the client explicitly supplied an id that wasn't found; keep the existing
TypeError/ValueError handling for malformed ids.

Comment on lines +120 to +135
{sortedSessions.length === 0 ? (
<option value="" disabled>{sessionLoading ? "Loading..." : "No session"}</option>
) : (
<>
{sortedSessions.map((session) => {
const label = session.title || "Untitled session";
const ts = formatSessionTimestamp(session.updatedAt ?? session.createdAt);
return (
<option key={session.id} value={session.id}>
{label} — {ts}
</option>
);
})}
<option value="__new__">+ New session</option>
</>
)}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Keep “+ New session” available when the list is empty.

Line 120 currently removes the "__new__" option when there are no sessions, so onSessionCreate() can’t be reached from this control.

Suggested fix
         <select
           value={activeSessionId ?? ""}
           onChange={(event) => handleSessionChange(event.currentTarget.value)}
           disabled={sessionLoading || sessionSwitching}
         >
           {sortedSessions.length === 0 ? (
             <option value="" disabled>{sessionLoading ? "Loading..." : "No session"}</option>
           ) : (
-            <>
-              {sortedSessions.map((session) => {
-                const label = session.title || "Untitled session";
-                const ts = formatSessionTimestamp(session.updatedAt ?? session.createdAt);
-                return (
-                  <option key={session.id} value={session.id}>
-                    {label} — {ts}
-                  </option>
-                );
-              })}
-              <option value="__new__">+ New session</option>
-            </>
+            sortedSessions.map((session) => {
+              const label = session.title || "Untitled session";
+              const ts = formatSessionTimestamp(session.updatedAt ?? session.createdAt);
+              return (
+                <option key={session.id} value={session.id}>
+                  {label} — {ts}
+                </option>
+              );
+            })
           )}
+          <option value="__new__">+ New session</option>
         </select>
📝 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
{sortedSessions.length === 0 ? (
<option value="" disabled>{sessionLoading ? "Loading..." : "No session"}</option>
) : (
<>
{sortedSessions.map((session) => {
const label = session.title || "Untitled session";
const ts = formatSessionTimestamp(session.updatedAt ?? session.createdAt);
return (
<option key={session.id} value={session.id}>
{label} {ts}
</option>
);
})}
<option value="__new__">+ New session</option>
</>
)}
{sortedSessions.length === 0 ? (
<option value="" disabled>{sessionLoading ? "Loading..." : "No session"}</option>
) : (
sortedSessions.map((session) => {
const label = session.title || "Untitled session";
const ts = formatSessionTimestamp(session.updatedAt ?? session.createdAt);
return (
<option key={session.id} value={session.id}>
{label} {ts}
</option>
);
})
)}
<option value="__new__">+ New session</option>
🤖 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/toolbar/RuntimeControls.tsx` around lines 120 - 135,
The dropdown currently omits the "+ New session" option when
sortedSessions.length === 0, preventing onSessionCreate from being triggered;
update the JSX in RuntimeControls (the component rendering sortedSessions.map
and the option with value="__new__") to always include an <option
value="__new__">+ New session</option> regardless of whether sortedSessions is
empty—e.g., keep the placeholder ("Loading..." or "No session") when there are
no sessions but render the "__new__" option outside or in both branches so the
create action remains reachable.

Comment thread frontend/src/GitView.tsx
Comment on lines +265 to +335
const loadHistory = async (skip = 0, append = false) => {
setHistoryLoading(true);
try {
const data: GitHistoryResponse = await apiGitHistory(projectId, {
limit: HISTORY_PAGE_SIZE,
skip,
});
setHistory((current) => (append ? [...current, ...data.commits] : data.commits));
setHistoryHasMore(data.hasMore);
} catch (err) {
const message = err instanceof Error ? err.message : "Failed to load commit history";
setError(message);
} finally {
setHistoryLoading(false);
}
};

const loadGitData = async (background = false) => {
if (background) {
setRefreshing(true);
} else {
setLoading(true);
}
setError(null);

try {
const [statusData, branchData] = await Promise.all([apiGitStatus(projectId), apiGitBranches(projectId)]);
setStatus(statusData);
setBranches(branchData);

const primaryRemote = statusData.remoteDetails.find((remote) => remote.name === "origin") ?? statusData.remoteDetails[0];
setRemoteUrl((currentRemoteUrl) => {
if (!primaryRemote) {
return "";
}
return primaryRemote.url ?? currentRemoteUrl;
});

if (statusData.hasCommits) {
await loadHistory(0, false);
} else {
setHistory([]);
setHistoryHasMore(false);
}
} catch (err) {
const message = err instanceof Error ? err.message : "Failed to load git workspace";
setError(message);
setStatus(null);
setBranches(null);
setHistory([]);
setHistoryHasMore(false);
} finally {
if (background) {
setRefreshing(false);
} else {
setLoading(false);
}
}
};

useEffect(() => {
if (!projectId) {
return;
}
void loadGitData();
setActiveSection("overview");
setCommitMessage("");
setNewBranchName("");
setTrackLocalName("");
setNotice(null);
}, [projectId]);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Guard async state updates to avoid stale project data overwriting current view.

loadGitData/loadHistory can resolve out of order when projectId changes quickly, and stale responses still call setState.

Suggested fix pattern (request id guard)
-import { FormEvent, useEffect, useMemo, useState } from "react";
+import { FormEvent, useEffect, useMemo, useRef, useState } from "react";
...
 export default function GitView({ projectId, mobile = false }: GitViewProps) {
+  const requestIdRef = useRef(0);
...
-  const loadHistory = async (skip = 0, append = false) => {
+  const loadHistory = async (skip = 0, append = false, requestId = requestIdRef.current) => {
     setHistoryLoading(true);
     try {
       const data: GitHistoryResponse = await apiGitHistory(projectId, {
         limit: HISTORY_PAGE_SIZE,
         skip,
       });
+      if (requestId !== requestIdRef.current) return;
       setHistory((current) => (append ? [...current, ...data.commits] : data.commits));
       setHistoryHasMore(data.hasMore);
     } catch (err) {
+      if (requestId !== requestIdRef.current) return;
       const message = err instanceof Error ? err.message : "Failed to load commit history";
       setError(message);
     } finally {
-      setHistoryLoading(false);
+      if (requestId === requestIdRef.current) setHistoryLoading(false);
     }
   };

   const loadGitData = async (background = false) => {
+    const requestId = ++requestIdRef.current;
...
       const [statusData, branchData] = await Promise.all([apiGitStatus(projectId), apiGitBranches(projectId)]);
+      if (requestId !== requestIdRef.current) return;
       setStatus(statusData);
       setBranches(branchData);
...
-        await loadHistory(0, false);
+        await loadHistory(0, false, requestId);
...
     } catch (err) {
+      if (requestId !== requestIdRef.current) return;
...
     } finally {
+      if (requestId !== requestIdRef.current) return;
       if (background) {
         setRefreshing(false);
       } else {
         setLoading(false);
       }
     }
   };

   useEffect(() => {
     if (!projectId) {
+      setLoading(false);
       return;
     }
     void loadGitData();
...
+    return () => {
+      requestIdRef.current += 1;
+    };
   }, [projectId]);
🤖 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/GitView.tsx` around lines 265 - 335, Add a request-id guard so
async responses from loadGitData and loadHistory don't overwrite state for a
newer projectId: create a mutable ref (e.g., requestsRef) that you increment/set
to a unique token inside useEffect before calling loadGitData, pass or capture
the token inside loadGitData and loadHistory, and before every setState call
(setStatus, setBranches, setRemoteUrl, setHistory, setHistoryHasMore, setError,
setLoading, setRefreshing, etc.) check that the current token still matches the
ref; if it does not, return early and skip state updates; also update the token
on projectId change and optionally clear/abort any outstanding work on unmount.

Comment thread frontend/src/GitView.tsx
Comment on lines +751 to +760
remoteBranches.map((branch) => (
<GitBranchRow
key={branch.name}
branch={branch}
action={() => {
void handleTrackRemoteBranch(branch);
}}
actionLabel={branch.trackedBy ? `Open ${branch.trackedBy}` : "Track branch"}
actionBusy={operation === "track"}
actionDisabled={operation !== null}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Open <local-branch> action is wired to the wrong handler.

On Line 758 the label implies checkout, but the click handler always calls tracking logic. For already tracked remotes, this can fail or perform the wrong action.

Suggested fix
                     remoteBranches.map((branch) => (
                       <GitBranchRow
                         key={branch.name}
                         branch={branch}
                         action={() => {
-                          void handleTrackRemoteBranch(branch);
+                          if (branch.trackedBy) {
+                            void handleCheckoutLocalBranch(branch.trackedBy);
+                            return;
+                          }
+                          void handleTrackRemoteBranch(branch);
                         }}
                         actionLabel={branch.trackedBy ? `Open ${branch.trackedBy}` : "Track branch"}
-                        actionBusy={operation === "track"}
+                        actionBusy={operation === "track" || operation === "checkout"}
                         actionDisabled={operation !== null}
                       />
                     ))

Comment on lines +83 to +86
const response = record.response ?? record.decision ?? record.action;
if (typeof response === "string") {
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.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Do not treat every string decision as “resolved.”

Line 85 currently marks permission state as resolved for any string value. That can incorrectly resolve pending/unknown decisions and drop active approval prompts.

Suggested fix
     const response = record.response ?? record.decision ?? record.action;
     if (typeof response === "string") {
-      return true;
+      return isPermissionResolved(response);
     }
📝 Committable suggestion

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

Suggested change
const response = record.response ?? record.decision ?? record.action;
if (typeof response === "string") {
return true;
}
const response = record.response ?? record.decision ?? record.action;
if (typeof response === "string") {
return ["approved", "denied", "resolved"].includes(response);
}
🤖 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/utils/streamUtils.ts` around lines 83 - 86, The current check in
streamUtils.ts that treats any string response as resolved (const response =
record.response ?? record.decision ?? record.action; if (typeof response ===
"string") return true;) is too broad; change it to only treat explicit resolved
decision values as resolved (e.g., check response === "allow" || response ===
"deny" or map to a small whitelist of resolved string constants), or implement a
helper like isResolvedDecision(response) that returns true only for those known
resolved values; update the conditional that references response (and any
callers that rely on it) so pending/unknown string decisions are not treated as
resolved.

@Rishabh-Bajpai

Copy link
Copy Markdown
Collaborator Author

@copilot Please address all the comments on this PR and prepare it for merge.

Copilot AI commented May 21, 2026

Copy link
Copy Markdown
Contributor

@copilot Please address all the comments on this PR and prepare it for merge.

Addressed the blocking review items in 3ed7bbd: fixed git pull to use repo.git.pull(...), sanitized backend error responses, validated ntfy topic URLs and git remote URLs, hardened project-relative path resolution against symlink traversal, added the requested frontend/script fixes, and added backend coverage for the new guardrails. I also verified that handleRejectQuestion, handleSaveGlobalDefaultModel, and git diff loading were already present. UI preview screenshot: /tmp/pr8-ui-review-preview.png. The only remaining review item I could not resolve in a follow-up commit is squashing the existing PR history, since that requires rewriting prior commits rather than adding a new one.

PR #8 Review: "Integrate Ralph Loop and enhance Git control UI/UX features"

49 commits | 81 files changed | +18,003 / -7,483 lines | developmentmain
...

Addressed the requested blockers in 8accccd: fixed the pull implementation, sanitized backend error surfaces, locked ntfy delivery to a trusted configured host with URL validation, rejected unsafe git remote URLs, blocked symlink traversal, tightened the frontend/script follow-ups, and added regression coverage. I also verified the referenced App.tsx handlers and git diff loading path were already present. UI preview screenshot: /tmp/pr8-ui-review-preview.png. The one remaining item I could not resolve in a follow-up commit is squashing the existing PR history, because that requires rewriting prior commits rather than adding a new one.

Copilot AI and others added 2 commits May 21, 2026 01:01
Agent-Logs-Url: https://github.com/Rishabh-Bajpai/mobile-opencode-control/sessions/7ec5ccc8-ef02-4677-9d62-bbad579b7bbd

Co-authored-by: Rishabh-Bajpai <28703138+Rishabh-Bajpai@users.noreply.github.com>
Agent-Logs-Url: https://github.com/Rishabh-Bajpai/mobile-opencode-control/sessions/7ec5ccc8-ef02-4677-9d62-bbad579b7bbd

Co-authored-by: Rishabh-Bajpai <28703138+Rishabh-Bajpai@users.noreply.github.com>
@Rishabh-Bajpai
Rishabh-Bajpai merged commit 46db8bb into main May 21, 2026
1 check failed
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.

3 participants