Skip to content

feat: add timer and alarm support via two-stage LLM router#3

Merged
jakkra merged 7 commits into
mainfrom
jakkra_llm_alarms
Apr 2, 2026
Merged

feat: add timer and alarm support via two-stage LLM router#3
jakkra merged 7 commits into
mainfrom
jakkra_llm_alarms

Conversation

@jakkra

@jakkra jakkra commented Mar 29, 2026

Copy link
Copy Markdown
Contributor

Implement timer/alarm creation from voice memos using a two-stage LLM routing approach: a lightweight router prompt classifies transcripts as timer_alarm/voice_memo/mixed, then routes to the appropriate extraction prompt. Timers and alarms are created via Android AlarmClock intents (with EXTRA_SKIP_UI for silent background operation) and iOS Clock app URL schemes.

Key changes:

  • Two-stage router in LlmService (routerTemplate → timerAlarmTemplate/compactTemplate)
  • Android: SET_TIMER/SET_ALARM intents, manifest permissions and queries
  • iOS: Clock app URL scheme support
  • DB schema v3→v4: durationSeconds column on extracted_actions
  • Crash recovery: reset stuck processing memos on app startup
  • UI: timer/alarm action cards with Create/Dismiss, retry on failure
  • Settings: test buttons for timer/alarm creation
  • Fix raw JSON showing as summary for timer/alarm memos
  • Skip auto-create and watch notification for plain notes
  • Benchmark results for timer/alarm test cases

Summary by CodeRabbit

  • New Features

    • Timer & alarm actions: create/start via system Clock apps, test buttons in AI Models, and a new Alarms & Timers management screen.
    • Two-stage AI routing and timer/alarm extraction with duration support; UI shows formatted Timer/Alarm titles.
  • Bug Fixes

    • Retry UI for failed AI processing and crash-recovery for stuck memo processing.
    • Swipe-to-archive gesture and persisted archived state.
  • Documentation

    • Added comprehensive alarms & timers spec and headless benchmark docs.

Copilot AI review requested due to automatic review settings March 29, 2026 20:01
@coderabbitai

coderabbitai Bot commented Mar 29, 2026

Copy link
Copy Markdown

Warning

Rate limit exceeded

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

Your organization is not enrolled in usage-based pricing. Contact your admin to enable usage-based pricing to continue reviews beyond the rate limit, or try again in 4 minutes and 5 seconds.

⌛ 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: defaults

Review profile: CHILL

Plan: Pro

Run ID: d466d794-d220-4ea9-89b8-f0138d57ff91

📥 Commits

Reviewing files that changed from the base of the PR and between 06a58ec and e0734ed.

📒 Files selected for processing (16)
  • packages/chrono_ai_flow/lib/src/correction_prompt_template.dart
  • packages/chrono_ai_flow/lib/src/parser.dart
  • packages/chrono_ai_flow/lib/src/time_expression_resolver.dart
  • zswatch_app/lib/data/database/app_database.dart
  • zswatch_app/lib/data/database/tables/voice_memos_table.dart
  • zswatch_app/lib/data/repositories/extracted_action_repository.dart
  • zswatch_app/lib/providers/ai_providers.dart
  • zswatch_app/lib/providers/voice_memo_providers.dart
  • zswatch_app/lib/services/ai/extracted_action_creation_service.dart
  • zswatch_app/lib/services/ai/llm_service.dart
  • zswatch_app/lib/services/ai/voice_note_ai_pipeline.dart
  • zswatch_app/lib/ui/screens/voice_memos/alarms_timers_screen.dart
  • zswatch_app/lib/ui/screens/voice_memos/voice_memos_screen.dart
  • zswatch_app/lib/ui/widgets/ai_debug_widgets.dart
  • zswatch_app/lib/ui/widgets/voice_memos/memo_list_item.dart
  • zswatch_app/test/no_speech_filter_test.dart
📝 Walkthrough

Walkthrough

Adds end-to-end timer/alarm support: LLM router + extraction templates, benchmark tooling, DB schema (durations, archived memos), native Android/iOS timer-alarm creation, UI screens/widgets and settings, AI pipeline routing/no-speech filtering, repository and provider updates, tests, and a long spec document.

Changes

Cohort / File(s) Summary
AI Pipeline & Prompts
packages/chrono_ai_flow/lib/src/models.dart, packages/chrono_ai_flow/lib/src/parser.dart, packages/chrono_ai_flow/lib/src/prompt_template.dart
Added durationSeconds to ChronoLlmExtraction, parse duration_seconds from LLM JSON, normalize intents (timer/countdowntimer, alarm/wakealarm), and added routerTemplate + timerAlarmTemplate.
AI Benchmarking & CLI
ai_testbench/.gitignore, ai_testbench/README.md, ai_testbench/lib/main.dart, ai_testbench/lib/router_benchmark_main.dart, ai_testbench/lib/benchmark_main.dart, ai_testbench/lib/services/model_benchmark_service.dart
Introduced router benchmark entrypoint (--headless-router) and timer headless docs/flags; benchmark service supports duration expectations, timer cases, and serializes durationMatch/durationDetail; ignored benchmark_results/.
Database Schema & Generated Models
zswatch_app/lib/data/database/app_database.dart, zswatch_app/lib/data/database/app_database.g.dart, zswatch_app/lib/data/database/tables/extracted_actions_table.dart, zswatch_app/lib/data/database/tables/voice_memos_table.dart
Bumped schema to 5; added duration_seconds to extracted_actions and archived to voice_memos; migrations, companions, mappings, and generated code updated.
Domain Models & Freezed
zswatch_app/lib/data/models/extracted_action.dart, zswatch_app/lib/data/models/extracted_action.freezed.dart, zswatch_app/lib/data/models/voice_memo.dart, zswatch_app/lib/data/models/voice_memo.freezed.dart
Added timer/alarm enum variants and durationSeconds to ExtractedAction; added archived to VoiceMemo model and updated generated helpers.
Repositories & DAOs
zswatch_app/lib/data/repositories/extracted_action_repository.dart, zswatch_app/lib/data/repositories/voice_memo_repository.dart
Persist durationSeconds on insert, added deleteAction, watchAlarmTimerActions, watchMemoActionTypesMap, resetStuckProcessingMemos, and setArchived methods; mapping updated to include new fields.
Core AI Services & Pipeline
zswatch_app/lib/services/ai/llm_service.dart, zswatch_app/lib/services/ai/voice_note_ai_pipeline.dart, zswatch_app/lib/services/ai/extracted_action_creation_service.dart, zswatch_app/lib/services/ai/ai_debug_info.dart
Two-stage router→extract flow added; extraction selects timerAlarmTemplate for timer/alarm routes; results and debug info include durationSeconds; added no-speech detection, ActionCreationDraft fields (durationSeconds, skipUi), permission tweaks, and mapping for timer/alarm.
Platform Integration (Android/iOS)
zswatch_app/android/app/src/main/AndroidManifest.xml, zswatch_app/android/app/src/main/kotlin/dev/zswatch/app/MainActivity.kt, zswatch_app/ios/Runner/AppDelegate.swift
Added SET_ALARM permission and intent queries; implemented Android AlarmClock intent helpers and iOS Clock deep-links; timer/alarm actions short-circuit to platform intent flows.
UI: Voice Memos & Settings
zswatch_app/lib/ui/screens/voice_memos/voice_memos_screen.dart, zswatch_app/lib/ui/screens/voice_memos/alarms_timers_screen.dart, zswatch_app/lib/ui/screens/settings/ai_models_settings_screen.dart, zswatch_app/lib/ui/widgets/voice_memos/memo_list_item.dart, zswatch_app/lib/ui/widgets/ai_debug_widgets.dart
New Alarms & Timers screen, timer/alarm formatting/icons/titles, archived state handling and swipe-to-archive, retry UI for failed AI processing, settings test buttons, cleaned AI-summary parsing, and debug formatting for durations.
Providers & State Management
zswatch_app/lib/providers/voice_memo_providers.dart, zswatch_app/lib/providers/ai_providers.dart
Adjusted auto-create skip logic for notes; added providers/operations for alarm/timer actions, deletion, and streams for alarm/timer actions and memo action-type maps.
Specs & Tests
specs/ai-tasks/alarms-and-timers.md, zswatch_app/test/no_speech_filter_test.dart
Added comprehensive alarms & timers specification and unit tests covering no-speech transcript filtering.
Miscellaneous Generated & Helpers
ai_testbench/lib/benchmark_main.dart, ai_testbench/lib/services/model_benchmark_service.dart
Serialization of durationMatch/durationDetail and promptTemplate made configurable for benchmarks.

Sequence Diagram(s)

sequenceDiagram
    participant User as User
    participant App as Voice Memo App
    participant RouterLLM as Router LLM
    participant ExtractorLLM as Extractor LLM
    participant Parser as Chrono Parser
    participant Platform as Platform API

    User->>App: Submit voice memo
    App->>RouterLLM: classify via routerTemplate
    RouterLLM-->>App: { "route": "timer_alarm" } or { "route": "voice_memo" }
    alt route == "timer_alarm"
        App->>ExtractorLLM: run timerAlarmTemplate
        ExtractorLLM-->>Parser: JSON extraction
        Parser-->>App: ChronoLlmExtraction(intent,title,durationSeconds,...)
        alt intent == "timer"
            App->>Platform: createTimer(durationSeconds, title, skipUi)
            Platform-->>User: Timer created/started
        else intent == "alarm"
            App->>Platform: createAlarm(scheduledAt, title, skipUi)
            Platform-->>User: Alarm scheduled
        end
    else route == "voice_memo"
        App->>ExtractorLLM: run voice-memo template
        ExtractorLLM-->>Parser: JSON extraction
        Parser-->>App: ChronoLlmExtraction(reminder/event/note,...)
        App->>App: persist actions and possibly create platform reminders/events
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

🐰 A hop, a tick, a clever cue,

Router finds the memo true,
Timers start and alarms peal bright,
Benchmarks race and parsers write,
A rabbit cheers this tidy flight.

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title clearly and concisely summarizes the primary change: adding timer and alarm support via a two-stage LLM router. The title is specific enough to convey the main feature without including noise.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

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

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

❤️ Share

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

Implement timer/alarm creation from voice memos using a two-stage LLM
routing approach: a lightweight router prompt classifies transcripts as
timer_alarm/voice_memo/mixed, then routes to the appropriate extraction
prompt. Timers and alarms are created via Android AlarmClock intents
(with EXTRA_SKIP_UI for silent background operation) and iOS Clock app
URL schemes.

Key changes:
- Two-stage router in LlmService (routerTemplate → timerAlarmTemplate/compactTemplate)
- Android: SET_TIMER/SET_ALARM intents, manifest permissions and queries
- iOS: Clock app URL scheme support
- DB schema v3→v4: durationSeconds column on extracted_actions
- Crash recovery: reset stuck processing memos on app startup
- UI: timer/alarm action cards with Create/Dismiss, retry on failure
- Settings: test buttons for timer/alarm creation
- Fix raw JSON showing as summary for timer/alarm memos
- Skip auto-create and watch notification for plain notes
- Benchmark results for timer/alarm test cases

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@jakkra jakkra force-pushed the jakkra_llm_alarms branch from 597f6fa to ce71fdc Compare March 29, 2026 20:02

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

Adds timer/alarm support to the voice memo AI pipeline using a two-stage LLM router (router → dedicated timer/alarm extraction), persists timer duration in the DB, updates UI rendering/creation flows, and extends the AI testbench/specs to cover the new intents.

Changes:

  • Introduces router + dedicated timer/alarm extraction prompt and adds durationSeconds support end-to-end (parser → models → DB → UI).
  • Implements platform bridges for creating timers/alarms (Android AlarmClock intents; iOS Clock URL schemes) and updates UI to display/trigger these actions.
  • Extends AI testbench benchmarks/spec documentation with timer/alarm cases and router benchmarking.

Reviewed changes

Copilot reviewed 29 out of 29 changed files in this pull request and generated 9 comments.

Show a summary per file
File Description
zswatch_app/lib/ui/widgets/voice_memos/memo_list_item.dart Adds cleanSummary() to avoid showing raw JSON as the memo preview/summary.
zswatch_app/lib/ui/widgets/ai_debug_widgets.dart Shows timer duration in chrono debug output.
zswatch_app/lib/ui/screens/voice_memos/voice_memos_screen.dart Adds retry UI for failed AI, cleans displayed summaries, and adds timer/alarm action card rendering.
zswatch_app/lib/ui/screens/settings/ai_models_settings_screen.dart Adds local “Timer/Alarm test” buttons that invoke platform creation without a watch.
zswatch_app/lib/services/ai/voice_note_ai_pipeline.dart Persists durationSeconds, skips watch notifications for “note-only” tasks, and resets stuck memos before processing.
zswatch_app/lib/services/ai/llm_service.dart Adds two-stage routing (router → prompt selection) and maps timer/alarm to extracted actions + friendly summaries.
zswatch_app/lib/services/ai/extracted_action_creation_service.dart Extends action drafts with duration/skipUi and adds timer/alarm creation plumbing + messaging.
zswatch_app/lib/services/ai/ai_debug_info.dart Adds durationSeconds to chrono debug model.
zswatch_app/lib/providers/voice_memo_providers.dart Skips auto-create for “notes” (tasks without time).
zswatch_app/lib/data/repositories/voice_memo_repository.dart Exposes DB method to reset stuck processing states.
zswatch_app/lib/data/repositories/extracted_action_repository.dart Writes/reads durationSeconds for extracted actions.
zswatch_app/lib/data/models/extracted_action.dart Adds timer/alarm action types and durationSeconds.
zswatch_app/lib/data/models/extracted_action.freezed.dart Regenerates Freezed model for new durationSeconds field.
zswatch_app/lib/data/database/tables/extracted_actions_table.dart Adds nullable duration_seconds column.
zswatch_app/lib/data/database/app_database.dart Bumps schema v3→v4, migrates duration_seconds, and resets stuck memos on DB open.
zswatch_app/lib/data/database/app_database.g.dart Regenerated Drift DB code for new column.
zswatch_app/ios/Runner/AppDelegate.swift Handles timer/alarm action creation by opening Clock app URL schemes.
zswatch_app/android/app/src/main/kotlin/dev/zswatch/app/MainActivity.kt Handles timer/alarm action creation via AlarmClock intents (with EXTRA_SKIP_UI).
zswatch_app/android/app/src/main/AndroidManifest.xml Adds SET_ALARM permission and package visibility queries for timer/alarm intents.
specs/ai-tasks/alarms-and-timers.md Adds a detailed draft spec for alarms/timers and routing strategy.
packages/chrono_ai_flow/lib/src/prompt_template.dart Adds routerTemplate and timerAlarmTemplate.
packages/chrono_ai_flow/lib/src/parser.dart Parses duration_seconds and normalizes timer/alarm intent variants.
packages/chrono_ai_flow/lib/src/models.dart Adds durationSeconds to extraction model.
ai_testbench/lib/services/model_benchmark_service.dart Adds timer/alarm benchmark cases and duration validation fields.
ai_testbench/lib/router_benchmark_main.dart Adds a headless router benchmark runner.
ai_testbench/lib/main.dart Adds --headless-router mode dispatch.
ai_testbench/lib/benchmark_main.dart Serializes duration match details into JSON output.
ai_testbench/benchmark_results/exp2_extended_timer_alarm.json Adds benchmark output artifact for extended timer/alarm experiment.
ai_testbench/benchmark_results/exp1_baseline_timer.json Adds benchmark output artifact for baseline timer experiment.
ai_testbench/benchmark_results/exp1_baseline_alarm.json Adds benchmark output artifact for baseline alarm experiment.
ai_testbench/README.md Documents new (timer/alarm) benchmark modes/options.
Comments suppressed due to low confidence (1)

ai_testbench/lib/main.dart:37

  • This entrypoint adds --headless-router but does not implement the --headless-timer / --prompt-timer flags that are documented in ai_testbench/README.md. If those flags are intended, add handling here (or route to benchmark_main with the right case/prompt selection); otherwise update the README to avoid suggesting unsupported CLI options.
void main(List<String> args) async {
  // Headless mode: run router pre-classifier benchmark
  if (args.contains('--headless-router')) {
    await runRouterBenchmark(args);
    exit(exitCode);
  }

  // Headless mode: run time extraction tests from CLI
  if (args.contains('--headless-time')) {
    await runHeadlessTimeExtraction(args);
    exit(exitCode);
  }

  // Headless mode: run correction benchmark from CLI
  if (args.contains('--headless-correction')) {
    WidgetsFlutterBinding.ensureInitialized();
    await runHeadlessCorrectionBenchmark(args);
    exit(exitCode);
  }

  // Headless mode: run model benchmark from CLI
  if (args.contains('--headless') ||
      Platform.environment['AI_BENCH_HEADLESS'] == '1') {
    await model_bench.main(args);
    exit(exitCode);
  }

Comment on lines +696 to +707
private fun createAlarmViaIntent(triggerAtMillis: Long?, label: String, skipUi: Boolean = true): Map<String, Any?> {
val intent = android.content.Intent(android.provider.AlarmClock.ACTION_SET_ALARM).apply {
if (triggerAtMillis != null) {
val cal = java.util.Calendar.getInstance().apply { timeInMillis = triggerAtMillis }
putExtra(android.provider.AlarmClock.EXTRA_HOUR, cal.get(java.util.Calendar.HOUR_OF_DAY))
putExtra(android.provider.AlarmClock.EXTRA_MINUTES, cal.get(java.util.Calendar.MINUTE))
}
if (label.isNotEmpty()) {
putExtra(android.provider.AlarmClock.EXTRA_MESSAGE, label)
}
putExtra(android.provider.AlarmClock.EXTRA_SKIP_UI, skipUi)
}

Copilot AI Mar 29, 2026

Copy link

Choose a reason for hiding this comment

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

createAlarmViaIntent allows triggerAtMillis to be null while also supporting EXTRA_SKIP_UI=true. Without explicitly providing EXTRA_HOUR/EXTRA_MINUTES, behavior is device/clock-app dependent and may create an incorrect alarm or fail silently. Consider requiring a non-null trigger time when skipUi is true (and returning a clear error if it's missing).

Copilot uses AI. Check for mistakes.
Comment thread ai_testbench/README.md
Comment on lines +87 to +99
**Timer/alarm benchmark** (uses extended 5-intent prompt with timer/alarm cases):
```bash
./build/linux/x64/release/bundle/ai_testbench --headless-timer --model Qwen3.5-2B-Q4_K_M.gguf
```

### CLI Options

| Flag | Description |
|------|-------------|
| `--headless` | Run structured extraction benchmark (all models) |
| `--headless-time` | Run time extraction benchmark |
| `--headless-timer` | Run timer/alarm benchmark (5-intent prompt, timer/alarm cases only) |
| `--prompt-timer` | Use the 5-intent prompt (with `--headless` to run all cases for regression testing) |

Copilot AI Mar 29, 2026

Copy link

Choose a reason for hiding this comment

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

The README documents a --headless-timer mode, but ai_testbench/lib/main.dart doesn't handle this flag (only --headless, --headless-time, --headless-correction, --headless-router). Either implement --headless-timer (and wire it to ModelBenchmarkService.timerAlarmCases + the appropriate prompt) or remove/update the documentation.

Suggested change
**Timer/alarm benchmark** (uses extended 5-intent prompt with timer/alarm cases):
```bash
./build/linux/x64/release/bundle/ai_testbench --headless-timer --model Qwen3.5-2B-Q4_K_M.gguf
```
### CLI Options
| Flag | Description |
|------|-------------|
| `--headless` | Run structured extraction benchmark (all models) |
| `--headless-time` | Run time extraction benchmark |
| `--headless-timer` | Run timer/alarm benchmark (5-intent prompt, timer/alarm cases only) |
| `--prompt-timer` | Use the 5-intent prompt (with `--headless` to run all cases for regression testing) |
### CLI Options
| Flag | Description |
|------|-------------|
| `--headless` | Run structured extraction benchmark (all models) |
| `--headless-time` | Run time extraction benchmark |

Copilot uses AI. Check for mistakes.
Comment thread ai_testbench/README.md
Comment on lines +87 to +99
**Timer/alarm benchmark** (uses extended 5-intent prompt with timer/alarm cases):
```bash
./build/linux/x64/release/bundle/ai_testbench --headless-timer --model Qwen3.5-2B-Q4_K_M.gguf
```

### CLI Options

| Flag | Description |
|------|-------------|
| `--headless` | Run structured extraction benchmark (all models) |
| `--headless-time` | Run time extraction benchmark |
| `--headless-timer` | Run timer/alarm benchmark (5-intent prompt, timer/alarm cases only) |
| `--prompt-timer` | Use the 5-intent prompt (with `--headless` to run all cases for regression testing) |

Copilot AI Mar 29, 2026

Copy link

Choose a reason for hiding this comment

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

The CLI options list --headless-timer and --prompt-timer, but there’s no implementation for these flags in the CLI entrypoints (e.g., ai_testbench/lib/main.dart / benchmark_main.dart). Please align the docs with the implemented flags, or add support for these options in the CLI parser/runner.

Suggested change
**Timer/alarm benchmark** (uses extended 5-intent prompt with timer/alarm cases):
```bash
./build/linux/x64/release/bundle/ai_testbench --headless-timer --model Qwen3.5-2B-Q4_K_M.gguf
```
### CLI Options
| Flag | Description |
|------|-------------|
| `--headless` | Run structured extraction benchmark (all models) |
| `--headless-time` | Run time extraction benchmark |
| `--headless-timer` | Run timer/alarm benchmark (5-intent prompt, timer/alarm cases only) |
| `--prompt-timer` | Use the 5-intent prompt (with `--headless` to run all cases for regression testing) |
### CLI Options
| Flag | Description |
|------|-------------|
| `--headless` | Run structured extraction benchmark (all models) |
| `--headless-time` | Run time extraction benchmark |

Copilot uses AI. Check for mistakes.
static const ChronoLlmParser _parser = ChronoLlmParser();

/// Prompt template to use for extraction.
String promptTemplate = ChronoPromptTemplate.defaultTemplate;

Copilot AI Mar 29, 2026

Copy link

Choose a reason for hiding this comment

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

benchmarkCases now includes timer/alarm expectations, but promptTemplate defaults to ChronoPromptTemplate.defaultTemplate (which only supports reminder/event/note). As a result, the default benchmark run will systematically fail the new timer/alarm cases unless the runner switches prompts (or performs routing) based on the case set. Consider adding an explicit CLI switch that sets promptTemplate + selectedCases (e.g. timer/alarm-only), or implement per-case routing between compactTemplate and timerAlarmTemplate.

Suggested change
String promptTemplate = ChronoPromptTemplate.defaultTemplate;
/// Use the compact template by default so all intents in [benchmarkCases]
/// (including timer/alarm) are supported in the default benchmark run.
String promptTemplate = ChronoPromptTemplate.compactTemplate;

Copilot uses AI. Check for mistakes.
ChronoPromptTemplate.timerAlarmTemplate,
transcript: effectiveTranscript,
);
promptStrategy = 'router→timer_alarm';

Copilot AI Mar 29, 2026

Copy link

Choose a reason for hiding this comment

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

The router template allows a mixed route, but the routing logic only checks for routeResult == 'timer_alarm' and otherwise falls back to the voice-memo prompt. That means mixed inputs won’t extract timer/alarm items. Consider handling mixed explicitly (e.g., run both extraction prompts and merge results, or introduce a combined prompt).

Suggested change
promptStrategy = 'router→timer_alarm';
promptStrategy = 'router→timer_alarm';
} else if (routeResult == 'mixed') {
// For mixed inputs, combine timer/alarm-focused and voice-memo-focused
// instructions into a single prompt so we can extract both kinds of items.
final timerPromptPart = ChronoPromptTemplate.render(
ChronoPromptTemplate.timerAlarmTemplate,
transcript: effectiveTranscript,
);
final memoPromptPart = _buildClassifyPrompt(
effectiveTranscript,
effectiveCtx: effectiveCtx,
);
prompt = '$timerPromptPart\n\n$memoPromptPart';
promptStrategy = usesFullPrompt(effectiveCtx)
? 'router→mixed/combined-full'
: usesEmergencyCompactPrompt(effectiveCtx)
? 'router→mixed/combined-emergency-compact'
: 'router→mixed/combined-compact';

Copilot uses AI. Check for mistakes.
Comment on lines 399 to 404
switch (actionType) {
case ExtractedActionType.timer:
case ExtractedActionType.alarm:
// Timer/alarm use system intents — no calendar permissions needed.
return Permission.notification;
case ExtractedActionType.calendarEvent:

Copilot AI Mar 29, 2026

Copy link

Choose a reason for hiding this comment

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

On iOS, timer/alarm creation is implemented by opening the Clock app URLs in AppDelegate and does not require notification permission. Returning Permission.notification here means users who deny notification permission will be blocked from creating timers/alarms even though creation could proceed. Consider skipping permission checks for timer/alarm on iOS (or only requesting notification permission once the implementation actually relies on notifications).

Copilot uses AI. Check for mistakes.
Comment on lines +109 to +142
if let url = URL(string: "clock-timer://") {
UIApplication.shared.open(url, options: [:]) { success in
result([
"platformId": nil,
"targetType": "timer",
"syncDisabled": false,
] as [String: Any?])
}
} else {
result([
"platformId": nil,
"targetType": "timer",
"syncDisabled": false,
] as [String: Any?])
}
}

private func createAlarmViaClockApp(scheduledAtMillis: Double?, label: String, result: @escaping FlutterResult) {
// iOS doesn't have a public API for setting alarms programmatically.
// Open the Clock app's alarm tab as the best available option.
if let url = URL(string: "clock-alarm://") {
UIApplication.shared.open(url, options: [:]) { success in
result([
"platformId": nil,
"targetType": "alarm",
"syncDisabled": false,
] as [String: Any?])
}
} else {
result([
"platformId": nil,
"targetType": "alarm",
"syncDisabled": false,
] as [String: Any?])

Copilot AI Mar 29, 2026

Copy link

Choose a reason for hiding this comment

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

createTimerViaClockApp always returns a successful "timer" result even if the URL scheme cannot be opened (the success callback is ignored), and it doesn't use durationSeconds/label at all. This can mislead the Dart side into showing "Timer started" when no timer was actually created. Consider checking success (or canOpenURL) and returning an error/explicit "opened clock app" status when the Clock app couldn't be opened or when the action can't be created programmatically.

Suggested change
if let url = URL(string: "clock-timer://") {
UIApplication.shared.open(url, options: [:]) { success in
result([
"platformId": nil,
"targetType": "timer",
"syncDisabled": false,
] as [String: Any?])
}
} else {
result([
"platformId": nil,
"targetType": "timer",
"syncDisabled": false,
] as [String: Any?])
}
}
private func createAlarmViaClockApp(scheduledAtMillis: Double?, label: String, result: @escaping FlutterResult) {
// iOS doesn't have a public API for setting alarms programmatically.
// Open the Clock app's alarm tab as the best available option.
if let url = URL(string: "clock-alarm://") {
UIApplication.shared.open(url, options: [:]) { success in
result([
"platformId": nil,
"targetType": "alarm",
"syncDisabled": false,
] as [String: Any?])
}
} else {
result([
"platformId": nil,
"targetType": "alarm",
"syncDisabled": false,
] as [String: Any?])
debugPrint("[AppDelegate] Requested timer via Clock app. durationSeconds=\(durationSeconds), label=\(label)")
guard let url = URL(string: "clock-timer://") else {
result(
FlutterError(
code: "INVALID_URL",
message: "Unable to construct Clock timer URL.",
details: nil
)
)
return
}
UIApplication.shared.open(url, options: [:]) { success in
if success {
result([
"platformId": nil,
"targetType": "timer",
"syncDisabled": false,
] as [String: Any?])
} else {
debugPrint("[AppDelegate] Failed to open Clock app for timer.")
result(
FlutterError(
code: "CLOCK_APP_UNAVAILABLE",
message: "Unable to open the Clock app timer tab.",
details: nil
)
)
}
}
}
private func createAlarmViaClockApp(scheduledAtMillis: Double?, label: String, result: @escaping FlutterResult) {
// iOS doesn't have a public API for setting alarms programmatically.
// Open the Clock app's alarm tab as the best available option.
debugPrint("[AppDelegate] Requested alarm via Clock app. scheduledAtMillis=\(String(describing: scheduledAtMillis)), label=\(label)")
guard let url = URL(string: "clock-alarm://") else {
result(
FlutterError(
code: "INVALID_URL",
message: "Unable to construct Clock alarm URL.",
details: nil
)
)
return
}
UIApplication.shared.open(url, options: [:]) { success in
if success {
result([
"platformId": nil,
"targetType": "alarm",
"syncDisabled": false,
] as [String: Any?])
} else {
debugPrint("[AppDelegate] Failed to open Clock app for alarm.")
result(
FlutterError(
code: "CLOCK_APP_UNAVAILABLE",
message: "Unable to open the Clock app alarm tab.",
details: nil
)
)
}

Copilot uses AI. Check for mistakes.
Comment on lines +126 to +136
private func createAlarmViaClockApp(scheduledAtMillis: Double?, label: String, result: @escaping FlutterResult) {
// iOS doesn't have a public API for setting alarms programmatically.
// Open the Clock app's alarm tab as the best available option.
if let url = URL(string: "clock-alarm://") {
UIApplication.shared.open(url, options: [:]) { success in
result([
"platformId": nil,
"targetType": "alarm",
"syncDisabled": false,
] as [String: Any?])
}

Copilot AI Mar 29, 2026

Copy link

Choose a reason for hiding this comment

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

createAlarmViaClockApp ignores both scheduledAtMillis and the success result from UIApplication.open, yet reports success back to Flutter. This makes the UI think an alarm was set even when only a deep link attempt occurred (or failed). Consider validating that the Clock app was opened and returning a failure when it wasn't, or adjust the response so the app doesn't treat this as an alarm having been created.

Copilot uses AI. Check for mistakes.
Comment on lines +676 to +683
private fun createTimerViaIntent(durationSeconds: Int, label: String, skipUi: Boolean = true): Map<String, Any?> {
val intent = android.content.Intent(android.provider.AlarmClock.ACTION_SET_TIMER).apply {
putExtra(android.provider.AlarmClock.EXTRA_LENGTH, durationSeconds)
if (label.isNotEmpty()) {
putExtra(android.provider.AlarmClock.EXTRA_MESSAGE, label)
}
putExtra(android.provider.AlarmClock.EXTRA_SKIP_UI, skipUi)
}

Copilot AI Mar 29, 2026

Copy link

Choose a reason for hiding this comment

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

createTimerViaIntent accepts durationSeconds values like 0 (defaulted from null) and still sets EXTRA_SKIP_UI. Many clock apps treat 0 as invalid or behave unexpectedly. Consider validating durationSeconds > 0 and returning an error when it's missing/invalid, especially when skipUi is true.

Copilot uses AI. Check for mistakes.

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
zswatch_app/lib/services/ai/extracted_action_creation_service.dart (1)

319-325: ⚠️ Potential issue | 🟠 Major

openCreatedAction() still no-ops for timers and alarms.

These new cases are mapped to 'alarm', but _openCreatedCalendarEntryIfSupported() immediately returns for anything other than 'calendar_event' or 'calendar_reminder'. If the UI exposes the Open button for a created timer/alarm, tapping it silently does nothing.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@zswatch_app/lib/services/ai/extracted_action_creation_service.dart` around
lines 319 - 325, The Open action is still a no-op for ExtractedActionType.timer
and ExtractedActionType.alarm because they’re mapped to 'alarm' but
_openCreatedCalendarEntryIfSupported() only handles 'calendar_event' and
'calendar_reminder'; modify openCreatedAction() to route timers/alarms to a
proper opener instead of the calendar opener: change the targetType mapping to
produce distinct values (e.g., 'alarm' or 'timer') or check action.actionType
directly, then call a new helper like _openCreatedAlarmIfSupported()/
_openCreatedTimerIfSupported() (or extend _openCreatedCalendarEntryIfSupported()
to accept 'alarm'/'timer' and handle platform-specific opening) so tapping Open
on created timers/alarms actually invokes the correct platform-specific open
logic.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@ai_testbench/lib/router_benchmark_main.dart`:
- Around line 130-135: The latency summaries mix different case sets
(avgRouterMs uses routerTimes, avgTotalMs uses the timer_alarm subset, and
avgSingleMs uses the first five voice_memo cases), producing invalid
comparisons; update the calculations so comparisons use the same consistent
dataset or compute and label per-subset metrics explicitly: e.g., create clear
collections like routerTimes (all router cases), timerAlarmTimes (cases where
case.type == 'timer_alarm'), voiceMemoTimes (cases where case.type ==
'voice_memo') and compute avgRouterMs, avgTimerAlarmMs, avgVoiceMemoMs from
those collections, then use matching counts (routerCorrect vs routerCases.length
or subsetCorrect vs subset.length) when printing summaries (references:
avgRouterMs, avgTotalMs, avgSingleMs, routerTimes, routerCorrect, cases,
timer_alarm, voice_memo).
- Around line 150-196: The router result is measured but not used to gate
extraction, so misrouted cases are counted as successes; also success only
checks first.intent which misses multi-item or wrong-duration failures. Change
the loop to parse routerResult.output (e.g., inspect routerResult.output JSON or
ChronoPromptTemplate router response) and only run extraction when the router
route indicates timer/alarm; if router indicates a different route mark the case
failed immediately and count router time. After running parser.parse on
extractResult.output, make intentOk require that the router-declared route
matches the parsed extractions and that at least one extraction has intent ==
'timer' or 'alarm' with a non-null duration/time (handling multi-item by
scanning all parseResult.extractions). Ensure timing (routerMs and extractMs)
reflect whether extraction actually ran and that pipeline failures include
router misclassification or missing/invalid extractions.
- Around line 20-33: Check that Directory(modelDir) exists and is a directory
before calling listSync() to avoid a FileSystemException; update the modelPaths
construction to first call Directory(modelDir).existsSync() (or wrap
Directory(modelDir).listSync() in a try/catch) and when the directory is missing
or an exception occurs, write a friendly message via
stdout.writeln('[RouterBench] No .gguf models found' or a specific error) and
set exitCode = 1 then return; adjust code around modelPaths,
Directory(modelDir).listSync(), and the early-return branch so the friendly
message runs when the directory is absent or listSync() fails.

In `@ai_testbench/lib/services/model_benchmark_service.dart`:
- Around line 910-916: The two alarm fixtures in the BenchmarkCase.single
entries (e.g., the case with name 'en_alarm_morning' and the Swedish alarm case
around lines 946-953) use expectedDateTime set to March 11, 2026 but with
referenceTime = DateTime(2026,3,11,10,15) they should resolve to the next day;
update the expectedDateTime for those BenchmarkCase.single instances to
DateTime(2026, 3, 12, 7, 30) so the expected intent/time matches the sensible
next-day resolution.
- Around line 214-215: The service uses the mutable field promptTemplate
(ChronoPromptTemplate.defaultTemplate) independently of timerAlarmCases, so runs
that select timerAlarmCases still use whatever promptTemplate is set; update
ModelBenchmarkService (the constructor or runForModels) so that when
timerAlarmCases is enabled it explicitly sets promptTemplate =
ChronoPromptTemplate.timerAlarmTemplate (or otherwise selects that template)
before rendering prompts in runForModels; ensure the same binding is applied in
any code paths that construct ModelBenchmarkService (e.g., the default runner vs
router benchmark path) so timer/alarm benchmarks always use
ChronoPromptTemplate.timerAlarmTemplate instead of the global/default
promptTemplate.

In `@ai_testbench/README.md`:
- Around line 87-90: The README documents flags `--headless-timer` and
`--prompt-timer` that are not parsed or used; update the CLI parsing in
main.dart to accept these flags (e.g., add options for "--headless-timer" and
"--prompt-timer" to the argument parser) and wire them into the benchmark
invocation (pass the parsed booleans into the benchmark runner or
TimerAlarmBenchmark class and branch the execution where benchmarks are
selected), or alternatively remove the flags from the README; specifically,
modify the argument parsing logic in main.dart (and any
BenchmarkRunner/TimerAlarmBenchmark constructors or run methods) to expose and
forward the two flags so the timer/alarm benchmark executes when set.

In `@packages/chrono_ai_flow/lib/src/parser.dart`:
- Around line 72-90: When building the ChronoLlmExtraction, add validation to
reject incomplete timer/alarm results: if intent == 'timer' ensure
durationSeconds is non-null (rawDuration parsed successfully) and if intent ==
'alarm' ensure at least one of datetimeExpressionOriginal or
datetimeExpressionEnglish is non-null/non-empty; if those checks fail, return
null (or otherwise short-circuit the parser) instead of constructing
ChronoLlmExtraction so incomplete objects don't pass
shouldRetryInvalidChronoOutput().

In `@packages/chrono_ai_flow/lib/src/prompt_template.dart`:
- Line 233: The template currently injects the raw transcript into a quoted memo
line (Memo: "$promptPlaceholderTranscript"), which breaks if the transcript
contains quotes or newlines; update the template to use an unquoted block style
(e.g., a new indented or fenced block like Memo: followed by the raw transcript
on the next line) or ensure promptPlaceholderTranscript is properly escaped
before interpolation; locate the occurrences of the quoted memo string (the
literal containing promptPlaceholderTranscript at the lines shown and the
similar instance around 281) and replace them with the unquoted-block style or
add escaping logic where promptPlaceholderTranscript is formatted.
- Around line 243-257: The prompt in prompt_template.dart (the "Extract timer or
alarm details..." template) must explicitly require splitting multiple requests
into separate array objects; update this template to add a rule like the ones in
defaultTemplate/compactTemplate ensuring "If the memo contains multiple
timers/alarms, return one JSON object per timer/alarm in the array (do not
combine multiple intents into a single object)." Mention that each item must
follow the existing schema and that output must be an array with one object per
detected timer/alarm.
- Around line 225-231: The router can return "mixed" but llm_service.dart
currently falls through to the catch-all and only calls compactTemplate,
dropping timers; add an explicit else if (routeResult == 'mixed') branch in the
function that handles routeResult which 1) calls timerAlarmTemplate to extract
timer/alarm items, 2) calls compactTemplate to extract reminder/event/note
items, and 3) merges the two result arrays (preserving both sets of items)
before returning so inputs like "Set a timer for 5 minutes and buy milk" include
both timer and voice memo parts; reference the routeResult variable,
compactTemplate, timerAlarmTemplate and the existing catch-all path to insert
this branch.

In `@specs/ai-tasks/alarms-and-timers.md`:
- Around line 100-112: The fenced code blocks describing "timer" and "alarm" are
unlabeled and trigger markdownlint MD040; update each triple-backtick fence that
wraps the blocks containing the quoted terms "timer" and "alarm" to include a
language identifier (e.g., ```text or ```md) so the linter stops flagging them,
and apply the same fix to the other unlabeled fenced blocks referenced in the
comment (the additional blocks the reviewer listed) by adding an appropriate
language tag to each fence.

In `@zswatch_app/android/app/src/main/kotlin/dev/zswatch/app/MainActivity.kt`:
- Around line 696-707: createAlarmViaIntent currently only sets hour/minute and
thus loses absolute date info from triggerAtMillis; to fix, detect when
triggerAtMillis represents a specific absolute time (not a daily recurring time)
and schedule a one‑time alarm via AlarmManager instead of the ACTION_SET_ALARM
intent. Add or call a new helper (e.g., scheduleOneTimeAlarmUsingAlarmManager or
createOneTimeAlarmWithAlarmManager) from createAlarmViaIntent when
triggerAtMillis != null and the intended behavior is a single absolute
timestamp: build a PendingIntent (to a BroadcastReceiver/Service), use
AlarmManager.setExactAndAllowWhileIdle/setExact depending on API level, and keep
the existing intent-path only for UI-driven or recurring/daily alarms; update
createAlarmViaIntent to return consistent Map<String, Any?> metadata (or an
error) so callers know which path was taken.

In `@zswatch_app/ios/Runner/AppDelegate.swift`:
- Around line 67-75: The branch that calls createTimerViaClockApp and
createAlarmViaClockApp currently returns a success to Dart without confirming
the Clock app actually opened or using the passed parameters; update
createTimerViaClockApp and createAlarmViaClockApp to build the correct clock URL
using durationSeconds, scheduledAtMillis and label, call
UIApplication.shared.open(_:options:completionHandler:), and only call
result.success(...) inside the completionHandler based on the Bool success
parameter (and call result.error(...) on failure); ensure the methods forward
the constructed URL to UIApplication.shared.open and do not unconditionally
return success as currently done in the code paths handling those actions.

In `@zswatch_app/lib/data/database/app_database.dart`:
- Around line 61-64: The v4 migration unconditionally adds
extractedActions.durationSeconds when from < 4 which duplicates the column for
pre-v2 upgrades because extracted_actions is created with that column in the v2+
schema; change the guard so the addColumn runs only for upgrades that started at
v2 or v3 (e.g., replace the condition with from >= 2 && from < 4) or otherwise
check that extractedActions.durationSeconds does not already exist before
calling m.addColumn(extractedActions, extractedActions.durationSeconds).

In `@zswatch_app/lib/services/ai/extracted_action_creation_service.dart`:
- Around line 385-403: The code incorrectly requests app notification permission
for iOS timer/alarm actions; update _permissionForActionType to return a
nullable Permission? and have it return null for ExtractedActionType.timer and
ExtractedActionType.alarm, then modify the caller (where permission is assigned
and _requestPermission is awaited) to skip calling _requestPermission when
permission == null; also ensure _failureMessageForActionType use is safe when
permission is null (only compute/pass failureMessage when requesting). This
touches the functions _permissionForActionType, the caller that awaits
_requestPermission, and _failureMessageForActionType so iOS timers/alarms bypass
notification gating.

In `@zswatch_app/lib/services/ai/llm_service.dart`:
- Around line 975-1026: The routing logic currently sends every non-timer case
to _buildClassifyPrompt which drops timer/alarm intent for router='mixed'; add
an explicit branch handling routeResult == 'mixed' in the prompt-construction
block (the same place that sets prompt and promptStrategy) so that mixed uses a
prompt that can represent both timers/alarms and voice memos (for example render
a new or existing combined template via ChronoPromptTemplate (e.g., a
mixedTemplate or by composing timerAlarmTemplate + voice memo template) or call
a new helper like _buildMixedClassifyPrompt), and set promptStrategy to
something like 'router→mixed' (and respect
usesFullPrompt/usesEmergencyCompactPrompt if applicable) so timer intents are
preserved for mixed inputs; update any callers/metrics labels
(_renderClassifyPrompt, _buildClassifyPrompt,
ChronoPromptTemplate.timerAlarmTemplate) accordingly.
- Around line 1495-1517: Change _friendlySummary and the code that materializes
native actions from ChronoLlmExtraction so we don't create timer/alarm outputs
when required fields are missing: in _friendlySummary, treat a timer as missing
if first.durationSeconds is null (do not convert null to 0 or return "Timer 0s")
and fall through to the default raw/title case; for alarms only return the
friendly "Alarm ..." when first.datetimeExpressionEnglish or
first.datetimeExpressionOriginal is non-null. Also update the action-emission
logic that constructs actions from ChronoLlmExtraction (the block that currently
emits type: 'timer' / type: 'alarm') to skip creating a timer action when
durationSeconds is null and to skip creating an alarm action when both
datetimeExpressionEnglish and datetimeExpressionOriginal are null, treating
those extractions as failures instead of materializing malformed actions.

In `@zswatch_app/lib/services/ai/voice_note_ai_pipeline.dart`:
- Around line 191-205: The current logic only checks result.actions.first and
treats the whole memo as a note if that first action is note-like, causing
action_type to be omitted even when later actions are actionable; update the
logic that computes isNote/action type/datetime so you first check all items in
result.actions (e.g., using every/any) to determine if all extracted items are
plain notes, and if not select the first non-note action as the actionable item:
leave isNote true only when every action has type 'task' with null startTime and
dueDate, otherwise set actionType and actionDatetime from the first action that
is not a plain note before calling onProcessingComplete(filename,
result.summary, actionType, actionDatetime).

In `@zswatch_app/lib/ui/widgets/ai_debug_widgets.dart`:
- Around line 254-258: The timer branch only reads a?.durationSeconds and thus
drops the scalar path; update the duration expression to fall back to the
extracted scalar (e.g. use a?.durationSeconds ?? extractedDurationSeconds) and
pass that into formatDuration so Duration shows correctly; modify the Duration
line in the intent == 'timer' block (where intent, a, extractedIntent,
extractedTitle and formatDuration are used) to use the fallback.

---

Outside diff comments:
In `@zswatch_app/lib/services/ai/extracted_action_creation_service.dart`:
- Around line 319-325: The Open action is still a no-op for
ExtractedActionType.timer and ExtractedActionType.alarm because they’re mapped
to 'alarm' but _openCreatedCalendarEntryIfSupported() only handles
'calendar_event' and 'calendar_reminder'; modify openCreatedAction() to route
timers/alarms to a proper opener instead of the calendar opener: change the
targetType mapping to produce distinct values (e.g., 'alarm' or 'timer') or
check action.actionType directly, then call a new helper like
_openCreatedAlarmIfSupported()/ _openCreatedTimerIfSupported() (or extend
_openCreatedCalendarEntryIfSupported() to accept 'alarm'/'timer' and handle
platform-specific opening) so tapping Open on created timers/alarms actually
invokes the correct platform-specific open logic.
🪄 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: defaults

Review profile: CHILL

Plan: Pro

Run ID: 913957d1-b56c-4848-9a97-e2c9407bd28d

📥 Commits

Reviewing files that changed from the base of the PR and between a6fd54b and ce71fdc.

📒 Files selected for processing (29)
  • ai_testbench/.gitignore
  • ai_testbench/README.md
  • ai_testbench/lib/benchmark_main.dart
  • ai_testbench/lib/main.dart
  • ai_testbench/lib/router_benchmark_main.dart
  • ai_testbench/lib/services/model_benchmark_service.dart
  • packages/chrono_ai_flow/lib/src/models.dart
  • packages/chrono_ai_flow/lib/src/parser.dart
  • packages/chrono_ai_flow/lib/src/prompt_template.dart
  • specs/ai-tasks/alarms-and-timers.md
  • zswatch_app/android/app/src/main/AndroidManifest.xml
  • zswatch_app/android/app/src/main/kotlin/dev/zswatch/app/MainActivity.kt
  • zswatch_app/ios/Runner/AppDelegate.swift
  • zswatch_app/lib/data/database/app_database.dart
  • zswatch_app/lib/data/database/app_database.g.dart
  • zswatch_app/lib/data/database/tables/extracted_actions_table.dart
  • zswatch_app/lib/data/models/extracted_action.dart
  • zswatch_app/lib/data/models/extracted_action.freezed.dart
  • zswatch_app/lib/data/repositories/extracted_action_repository.dart
  • zswatch_app/lib/data/repositories/voice_memo_repository.dart
  • zswatch_app/lib/providers/voice_memo_providers.dart
  • zswatch_app/lib/services/ai/ai_debug_info.dart
  • zswatch_app/lib/services/ai/extracted_action_creation_service.dart
  • zswatch_app/lib/services/ai/llm_service.dart
  • zswatch_app/lib/services/ai/voice_note_ai_pipeline.dart
  • zswatch_app/lib/ui/screens/settings/ai_models_settings_screen.dart
  • zswatch_app/lib/ui/screens/voice_memos/voice_memos_screen.dart
  • zswatch_app/lib/ui/widgets/ai_debug_widgets.dart
  • zswatch_app/lib/ui/widgets/voice_memos/memo_list_item.dart

Comment on lines +20 to +33
final modelPaths = Directory(modelDir)
.listSync()
.whereType<File>()
.map((f) => f.path)
.where((p) => p.toLowerCase().endsWith('.gguf'))
.where(
(p) => modelFilter == null || p.toLowerCase().contains(modelFilter.toLowerCase()))
.toList()
..sort();

if (modelPaths.isEmpty) {
stdout.writeln('[RouterBench] No .gguf models found');
exitCode = 1;
return;

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 | 🟡 Minor

Check that modelDir exists before calling listSync().

If the directory is missing, this throws a FileSystemException and the friendly "No .gguf models found" path never runs.

🛠️ Proposed fix
-  final modelPaths = Directory(modelDir)
+  final modelDirectory = Directory(modelDir);
+  if (!modelDirectory.existsSync()) {
+    stdout.writeln('[RouterBench] Model directory not found: $modelDir');
+    exitCode = 1;
+    return;
+  }
+
+  final modelPaths = modelDirectory
       .listSync()
       .whereType<File>()
       .map((f) => f.path)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ai_testbench/lib/router_benchmark_main.dart` around lines 20 - 33, Check that
Directory(modelDir) exists and is a directory before calling listSync() to avoid
a FileSystemException; update the modelPaths construction to first call
Directory(modelDir).existsSync() (or wrap Directory(modelDir).listSync() in a
try/catch) and when the directory is missing or an exception occurs, write a
friendly message via stdout.writeln('[RouterBench] No .gguf models found' or a
specific error) and set exitCode = 1 then return; adjust code around modelPaths,
Directory(modelDir).listSync(), and the early-return branch so the friendly
message runs when the directory is absent or listSync() fails.

Comment on lines +130 to +135
final avgRouterMs =
routerTimes.fold<int>(0, (s, d) => s + d.inMilliseconds) ~/
routerTimes.length;
stdout.writeln(
'\n[RouterBench] Router accuracy: $routerCorrect/${cases.length}');
stdout.writeln('[RouterBench] Router avg latency: ${avgRouterMs}ms');

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

The latency summaries are comparing different case sets.

avgRouterMs is computed from all router cases, avgTotalMs from the timer_alarm subset, and avgSingleMs from the first five voice_memo cases. That makes both Avg extract only and Overhead apples-to-oranges metrics.

Also applies to: 199-207, 212-242

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ai_testbench/lib/router_benchmark_main.dart` around lines 130 - 135, The
latency summaries mix different case sets (avgRouterMs uses routerTimes,
avgTotalMs uses the timer_alarm subset, and avgSingleMs uses the first five
voice_memo cases), producing invalid comparisons; update the calculations so
comparisons use the same consistent dataset or compute and label per-subset
metrics explicitly: e.g., create clear collections like routerTimes (all router
cases), timerAlarmTimes (cases where case.type == 'timer_alarm'), voiceMemoTimes
(cases where case.type == 'voice_memo') and compute avgRouterMs,
avgTimerAlarmMs, avgVoiceMemoMs from those collections, then use matching counts
(routerCorrect vs routerCases.length or subsetCorrect vs subset.length) when
printing summaries (references: avgRouterMs, avgTotalMs, avgSingleMs,
routerTimes, routerCorrect, cases, timer_alarm, voice_memo).

Comment on lines +150 to +196
for (final tc in timerCases) {
final sw = Stopwatch()..start();

// Stage 1: Router
llm.maxTokens = 32;
final routerPrompt = ChronoPromptTemplate.render(
ChronoPromptTemplate.routerTemplate,
transcript: tc.transcript,
now: referenceTime,
);
final routerResult = await llm.generate(routerPrompt).timeout(
const Duration(seconds: 30),
onTimeout: () => const InferenceResult(
output: '{"route":"timeout"}', elapsed: Duration(seconds: 30)),
);
final routerMs = routerResult.elapsed.inMilliseconds;

// Stage 2: Timer/alarm extraction
llm.maxTokens = 384;
final extractPrompt = ChronoPromptTemplate.render(
ChronoPromptTemplate.timerAlarmTemplate,
transcript: tc.transcript,
now: referenceTime,
);
final extractResult = await llm.generate(extractPrompt).timeout(
const Duration(seconds: 60),
onTimeout: () => const InferenceResult(
output: '[]', elapsed: Duration(seconds: 60)),
);
final extractMs = extractResult.elapsed.inMilliseconds;

sw.stop();
totalTimes.add(sw.elapsed);

final parseResult = parser.parse(extractResult.output);
final first = parseResult.extractions.isNotEmpty
? parseResult.extractions.first
: null;
final intentOk = first != null &&
(first.intent == 'timer' || first.intent == 'alarm');
if (intentOk) extractionCorrect++;

stdout.writeln(
' ${intentOk ? "OK" : "FAIL"} intent=${first?.intent ?? "null"} '
'dur=${first?.durationSeconds ?? "null"} '
'router=${routerMs}ms extract=${extractMs}ms '
'total=${sw.elapsedMilliseconds}ms "${tc.transcript}"');

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

Stage 2 isn't measuring the actual two-stage pipeline.

routerResult is timed but never used to decide whether extraction should run, so router misclassifications do not count as pipeline failures. Success is also only first.intent == timer || alarm, which lets the multi-item timer+alarm case pass even if the second extraction is missing or the parsed duration/time is wrong.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ai_testbench/lib/router_benchmark_main.dart` around lines 150 - 196, The
router result is measured but not used to gate extraction, so misrouted cases
are counted as successes; also success only checks first.intent which misses
multi-item or wrong-duration failures. Change the loop to parse
routerResult.output (e.g., inspect routerResult.output JSON or
ChronoPromptTemplate router response) and only run extraction when the router
route indicates timer/alarm; if router indicates a different route mark the case
failed immediately and count router time. After running parser.parse on
extractResult.output, make intentOk require that the router-declared route
matches the parsed extractions and that at least one extraction has intent ==
'timer' or 'alarm' with a non-null duration/time (handling multi-item by
scanning all parseResult.extractions). Ensure timing (routerMs and extractMs)
reflect whether extraction actually ran and that pipeline failures include
router misclassification or missing/invalid extractions.

Comment on lines +214 to +215
/// Prompt template to use for extraction.
String promptTemplate = ChronoPromptTemplate.defaultTemplate;

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

Bind timer/alarm benchmark runs to the timer/alarm prompt.

timerAlarmCases can now be selected independently, but runForModels() still renders whatever promptTemplate happens to be set on the service. The default runner in ai_testbench/lib/benchmark_main.dart:176-214 constructs ModelBenchmarkService() without overriding it, while the dedicated timer/alarm path in ai_testbench/lib/router_benchmark_main.dart:1-25 uses ChronoPromptTemplate.timerAlarmTemplate. As written, headless timer runs can silently benchmark the new cases against the wrong prompt.

Based on learnings, in ai_testbench, test shared chrono_ai_flow prompts and schemas.

Also applies to: 1040-1043, 1088-1093

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ai_testbench/lib/services/model_benchmark_service.dart` around lines 214 -
215, The service uses the mutable field promptTemplate
(ChronoPromptTemplate.defaultTemplate) independently of timerAlarmCases, so runs
that select timerAlarmCases still use whatever promptTemplate is set; update
ModelBenchmarkService (the constructor or runForModels) so that when
timerAlarmCases is enabled it explicitly sets promptTemplate =
ChronoPromptTemplate.timerAlarmTemplate (or otherwise selects that template)
before rendering prompts in runForModels; ensure the same binding is applied in
any code paths that construct ModelBenchmarkService (e.g., the default runner vs
router benchmark path) so timer/alarm benchmarks always use
ChronoPromptTemplate.timerAlarmTemplate instead of the global/default
promptTemplate.

Comment on lines +910 to +916
BenchmarkCase.single(
name: 'en_alarm_morning',
transcript: 'Set an alarm for 7:30 AM',
expectedIntent: 'alarm',
expectTime: true,
expectedDateTime: DateTime(2026, 3, 11, 7, 30),
),

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

Two alarm fixtures are pinned to a past timestamp.

With referenceTime = DateTime(2026, 3, 11, 10, 15), 'Set an alarm for 7:30 AM' and 'Alarm klockan halv 8, dags att gå' should resolve to March 12, 2026 7:30 AM, not earlier on March 11, 2026. Leaving these on March 11 will mark sensible next-day outputs as failures.

🐛 Proposed fix
-      expectedDateTime: DateTime(2026, 3, 11, 7, 30),
+      expectedDateTime: DateTime(2026, 3, 12, 7, 30),
...
-      expectedDateTime: DateTime(2026, 3, 11, 7, 30),
+      expectedDateTime: DateTime(2026, 3, 12, 7, 30),

Also applies to: 946-953

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ai_testbench/lib/services/model_benchmark_service.dart` around lines 910 -
916, The two alarm fixtures in the BenchmarkCase.single entries (e.g., the case
with name 'en_alarm_morning' and the Swedish alarm case around lines 946-953)
use expectedDateTime set to March 11, 2026 but with referenceTime =
DateTime(2026,3,11,10,15) they should resolve to the next day; update the
expectedDateTime for those BenchmarkCase.single instances to DateTime(2026, 3,
12, 7, 30) so the expected intent/time matches the sensible next-day resolution.

Comment thread zswatch_app/lib/services/ai/extracted_action_creation_service.dart Outdated
Comment thread zswatch_app/lib/services/ai/llm_service.dart
Comment on lines +1495 to +1517
static String _friendlySummary(ChronoLlmExtraction first, String raw) {
if (first.intent == 'timer') {
final d = first.durationSeconds ?? 0;
final h = d ~/ 3600;
final m = (d % 3600) ~/ 60;
final s = d % 60;
final parts = <String>[
if (h > 0) '${h}h',
if (m > 0) '${m}m',
if (s > 0 || (h == 0 && m == 0)) '${s}s',
];
final dur = parts.join(' ');
return first.title.isNotEmpty ? 'Timer $dur — ${first.title}' : 'Timer $dur';
}
if (first.intent == 'alarm') {
final expr = first.datetimeExpressionEnglish ?? first.datetimeExpressionOriginal;
if (expr != null) {
return first.title.isNotEmpty ? 'Alarm $expr — ${first.title}' : 'Alarm $expr';
}
return first.title.isNotEmpty ? 'Alarm — ${first.title}' : 'Alarm';
}
return first.title.isNotEmpty ? first.title : raw.trim();
}

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

Don't materialize timer/alarm actions when their required fields are missing.

Lines 1497-1507 turn a missing duration into Timer 0s, and Lines 1551-1598 still emit type: 'timer' / type: 'alarm' actions when durationSeconds is null or time resolution failed. That malformed model output is then treated as a valid native action instead of being rejected here as an extraction failure.

Also applies to: 1551-1598

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@zswatch_app/lib/services/ai/llm_service.dart` around lines 1495 - 1517,
Change _friendlySummary and the code that materializes native actions from
ChronoLlmExtraction so we don't create timer/alarm outputs when required fields
are missing: in _friendlySummary, treat a timer as missing if
first.durationSeconds is null (do not convert null to 0 or return "Timer 0s")
and fall through to the default raw/title case; for alarms only return the
friendly "Alarm ..." when first.datetimeExpressionEnglish or
first.datetimeExpressionOriginal is non-null. Also update the action-emission
logic that constructs actions from ChronoLlmExtraction (the block that currently
emits type: 'timer' / type: 'alarm') to skip creating a timer action when
durationSeconds is null and to skip creating an alarm action when both
datetimeExpressionEnglish and datetimeExpressionOriginal are null, treating
those extractions as failures instead of materializing malformed actions.

Comment thread zswatch_app/lib/services/ai/voice_note_ai_pipeline.dart
Comment on lines +254 to +258
final intent = a?.intent ?? extractedIntent;
if (intent == 'timer') {
return 'Intent: timer\n'
'Title: ${show(a?.title ?? extractedTitle)}\n'
'Duration: ${formatDuration(a?.durationSeconds)}';

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 | 🟡 Minor

Timer duration is still dropped on the direct-field path.

This helper still supports callers that pass the scalar chrono fields without extractedActions, but the timer branch only reads a?.durationSeconds. In that case the debug sheet always shows Duration: null.

💡 Suggested fix
 String aiFormatChronoDetails({
   String? extractedIntent,
   String? extractedTitle,
   String? datetimeExpressionOriginal,
   String? datetimeExpressionEnglish,
   String? resolvedDateTime,
   String? resolverMethod,
+  int? durationSeconds,
   List<ActionChronoDebug> extractedActions = const [],
 }) {
@@
   if (intent == 'timer') {
     return 'Intent: timer\n'
         'Title: ${show(a?.title ?? extractedTitle)}\n'
-        'Duration: ${formatDuration(a?.durationSeconds)}';
+        'Duration: ${formatDuration(a?.durationSeconds ?? durationSeconds)}';
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@zswatch_app/lib/ui/widgets/ai_debug_widgets.dart` around lines 254 - 258, The
timer branch only reads a?.durationSeconds and thus drops the scalar path;
update the duration expression to fall back to the extracted scalar (e.g. use
a?.durationSeconds ?? extractedDurationSeconds) and pass that into
formatDuration so Duration shows correctly; modify the Duration line in the
intent == 'timer' block (where intent, a, extractedIntent, extractedTitle and
formatDuration are used) to use the fallback.

jakkra and others added 3 commits March 30, 2026 22:07
Skip LLM inference for blank audio, silence markers, and pure
punctuation. The router now supports a "none" route and the pipeline
marks empty results as failed.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
New alarms & timers screen with routing, updated prompt template
for better LLM routing, benchmark service adjustments, and minor
voice memo UI refinements.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

@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.

🧹 Nitpick comments (3)
zswatch_app/test/no_speech_filter_test.dart (1)

50-52: Consider adding an empty string test case.

While processMemo checks transcript.trim().isEmpty before calling isNoSpeechTranscript, the static method itself would return true for empty strings (since stripped.isEmpty would be true). Adding an explicit test for '' documents this behavior.

💡 Suggested test addition
test('detects empty string', () {
  expect(VoiceNoteAiPipeline.isNoSpeechTranscript(''), isTrue);
});
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@zswatch_app/test/no_speech_filter_test.dart` around lines 50 - 52, Add an
explicit unit test for the empty-string case so isNoSpeechTranscript behavior is
documented and covered: add a new test that calls
VoiceNoteAiPipeline.isNoSpeechTranscript('') and expects true. This complements
the existing test 'detects whitespace-only' and aligns with the guard in
processMemo that checks transcript.trim().isEmpty.
zswatch_app/lib/ui/widgets/voice_memos/memo_list_item.dart (1)

339-380: Duration formatting logic is duplicated from LlmService._friendlySummary.

The timer duration formatting (Lines 350-364) mirrors the logic in llm_service.dart Lines 1517-1525. If the format changes, both locations must be updated.

Consider extracting a shared formatDuration(int seconds) helper into a common utility, or at minimum add a comment noting the duplication to keep them in sync.

♻️ Proposed shared helper
// In a shared utils file:
String formatTimerDuration(int seconds) {
  final h = seconds ~/ 3600;
  final m = (seconds % 3600) ~/ 60;
  final s = seconds % 60;
  final parts = <String>[
    if (h > 0) '${h}h',
    if (m > 0) '${m}m',
    if (s > 0 || (h == 0 && m == 0)) '${s}s',
  ];
  return parts.join(' ');
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@zswatch_app/lib/ui/widgets/voice_memos/memo_list_item.dart` around lines 339
- 380, The timer duration formatting in cleanSummary duplicates logic from
LlmService._friendlySummary; extract the shared logic into a single utility
(e.g., formatTimerDuration(int seconds)) and replace the duration computation in
cleanSummary with a call to that helper (or, if you can't extract now, add a
clear TODO comment in cleanSummary referencing LlmService._friendlySummary to
keep them in sync). Update references so cleanSummary uses the new utility and
ensure LlmService._friendlySummary calls the same helper to avoid future drift.
zswatch_app/lib/data/database/app_database.g.dart (1)

4163-4164: Refresh the actionType docs to match timer/alarm support.

With durationSeconds added here, the adjacent actionType comment at Line 4130 still says the type is limited to task, calendar_event, and reminder. Please fix the source comment before regenerating so the generated API docs reflect the new action surface.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@zswatch_app/lib/data/database/app_database.g.dart` around lines 4163 - 4164,
Update the source doc comment for the actionType field (adjacent to
durationSeconds and the actionType declaration) to include the new supported
action kinds "timer" and "alarm" in addition to "task", "calendar_event", and
"reminder", then regenerate the g.dart so the generated API docs reflect
timer/alarm support; target the comment above the actionType declaration (the
actionType field/property) rather than the generated file directly.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@zswatch_app/lib/data/database/app_database.g.dart`:
- Around line 4163-4164: Update the source doc comment for the actionType field
(adjacent to durationSeconds and the actionType declaration) to include the new
supported action kinds "timer" and "alarm" in addition to "task",
"calendar_event", and "reminder", then regenerate the g.dart so the generated
API docs reflect timer/alarm support; target the comment above the actionType
declaration (the actionType field/property) rather than the generated file
directly.

In `@zswatch_app/lib/ui/widgets/voice_memos/memo_list_item.dart`:
- Around line 339-380: The timer duration formatting in cleanSummary duplicates
logic from LlmService._friendlySummary; extract the shared logic into a single
utility (e.g., formatTimerDuration(int seconds)) and replace the duration
computation in cleanSummary with a call to that helper (or, if you can't extract
now, add a clear TODO comment in cleanSummary referencing
LlmService._friendlySummary to keep them in sync). Update references so
cleanSummary uses the new utility and ensure LlmService._friendlySummary calls
the same helper to avoid future drift.

In `@zswatch_app/test/no_speech_filter_test.dart`:
- Around line 50-52: Add an explicit unit test for the empty-string case so
isNoSpeechTranscript behavior is documented and covered: add a new test that
calls VoiceNoteAiPipeline.isNoSpeechTranscript('') and expects true. This
complements the existing test 'detects whitespace-only' and aligns with the
guard in processMemo that checks transcript.trim().isEmpty.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 139aa28f-aa58-493b-9187-cd88af1704f3

📥 Commits

Reviewing files that changed from the base of the PR and between ce71fdc and 0a6902c.

📒 Files selected for processing (14)
  • zswatch_app/lib/data/database/app_database.dart
  • zswatch_app/lib/data/database/app_database.g.dart
  • zswatch_app/lib/data/database/tables/voice_memos_table.dart
  • zswatch_app/lib/data/models/voice_memo.dart
  • zswatch_app/lib/data/models/voice_memo.freezed.dart
  • zswatch_app/lib/data/repositories/extracted_action_repository.dart
  • zswatch_app/lib/data/repositories/voice_memo_repository.dart
  • zswatch_app/lib/providers/ai_providers.dart
  • zswatch_app/lib/providers/voice_memo_providers.dart
  • zswatch_app/lib/services/ai/llm_service.dart
  • zswatch_app/lib/services/ai/voice_note_ai_pipeline.dart
  • zswatch_app/lib/ui/screens/voice_memos/voice_memos_screen.dart
  • zswatch_app/lib/ui/widgets/voice_memos/memo_list_item.dart
  • zswatch_app/test/no_speech_filter_test.dart
🚧 Files skipped from review as they are similar to previous changes (2)
  • zswatch_app/lib/data/database/app_database.dart
  • zswatch_app/lib/data/repositories/voice_memo_repository.dart

jakkra and others added 2 commits April 2, 2026 21:47
… mixed route

- Fix DB migration v1→v4+ crash: guard addColumn with from >= 2 so
  freshly-created tables don't get duplicate columns
- Fix "Timer 0s" summary when durationSeconds is null
- Skip permission request for timer/alarm on iOS (uses URL schemes)
- Route "mixed" router result to timerAlarmTemplate instead of dropping
  timer/alarm intents via the voice_memo-only compact template
- Scan all actions (not just first) for watch notification routing
- Reject incomplete timer/alarm in parser (timer needs duration, alarm
  needs datetime)
- Early-return in openCreatedAction for timer/alarm (no calendar entry)
- Format all Dart code for CI

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

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

🧹 Nitpick comments (1)
ai_testbench/lib/services/model_benchmark_service.dart (1)

1355-1356: Consider adding a comment explaining the redundancy.

The parser already normalizes countdowntimer (per parser.dart:180-200), so this fuzzy match is defense-in-depth. A brief comment would clarify this is intentional robustness, not oversight.

     // Allow note <-> task fuzzy match (no time = note)
     if ({g, e}.containsAll({'note', 'task'})) return true;
-    // Allow countdown <-> timer fuzzy match
+    // Allow countdown <-> timer fuzzy match (parser normalizes, but accept both for robustness)
     if ({g, e}.containsAll({'countdown', 'timer'})) return true;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ai_testbench/lib/services/model_benchmark_service.dart` around lines 1355 -
1356, Add a short clarifying comment immediately above the defensive fuzzy-match
line that uses {g, e}.containsAll({'countdown', 'timer'}) stating that this
check is redundant by design because the parser already normalizes "countdown" →
"timer" (see parser.dart normalization), and that the extra check is retained as
defense-in-depth to tolerate unexpected upstream inputs or future parser
changes; keep the comment concise and reference both the normalization and that
this is intentional robustness, not an oversight.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@zswatch_app/lib/ui/screens/voice_memos/alarms_timers_screen.dart`:
- Around line 252-275: The action row currently only disables individual
buttons; compute a single busy flag (e.g. _isBusy = _isCreating || _isDismissing
|| _isDeleting || _isOpening) and use it to disable or block the whole row when
any async operation is in flight: wrap the buttons block (the
FilledButton/OutlinedButton group that checks isPending and the always-available
Delete/Open buttons) with an IgnorePointer or AbsorbPointer keyed to _isBusy
and/or set each button's onPressed to null when _isBusy, so no other action
(_createAction, _dismissAction, _deleteAction, _openCreatedAction) can be
invoked while any of the async flags are true; apply the same pattern to the
other affected ranges (303-395) mentioned in the comment.
- Around line 103-116: The list item widgets use _AlarmTimerCard without keys,
which lets Flutter reuse internal state when actions move between pending and
completed; give each _AlarmTimerCard a stable Key (e.g., pass Key:
ValueKey(action.id) or ValueKey(action.uuid) or ValueKey(action) if the action
has a reliable unique field) when constructing the card in both the pending and
completed loops (the usages of _AlarmTimerCard in the pending/completed
builders, including the other occurrence noted), so each action keeps its own
state when the stream reorders items.

In `@zswatch_app/lib/ui/widgets/voice_memos/memo_list_item.dart`:
- Around line 364-404: The cleanSummary function should parse JSON structurally
instead of using fragile regexes: in cleanSummary(String summary) try to
jsonDecode the trimmed string (import dart:convert) inside a try/catch, handle
resulting Map or List (if List, use first element if it's a Map), then read keys
like "title", "intent", "duration_seconds" and "datetime_expression_english"
directly from the Map to build the same formatted outputs (timer: compute h/m/s
from duration_seconds; alarm: use datetime_expression_english; fallback to
title), and return trimmed if decoding fails or the decoded value doesn't match
expected types; keep existing return formats and fallback behavior.

---

Nitpick comments:
In `@ai_testbench/lib/services/model_benchmark_service.dart`:
- Around line 1355-1356: Add a short clarifying comment immediately above the
defensive fuzzy-match line that uses {g, e}.containsAll({'countdown', 'timer'})
stating that this check is redundant by design because the parser already
normalizes "countdown" → "timer" (see parser.dart normalization), and that the
extra check is retained as defense-in-depth to tolerate unexpected upstream
inputs or future parser changes; keep the comment concise and reference both the
normalization and that this is intentional robustness, not an oversight.
🪄 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: defaults

Review profile: CHILL

Plan: Pro

Run ID: db2df3a1-10f2-4bdc-91f4-21f1cab2de9b

📥 Commits

Reviewing files that changed from the base of the PR and between 0a6902c and 06a58ec.

📒 Files selected for processing (6)
  • ai_testbench/lib/services/model_benchmark_service.dart
  • packages/chrono_ai_flow/lib/src/prompt_template.dart
  • zswatch_app/lib/ui/navigation/app_router.dart
  • zswatch_app/lib/ui/screens/voice_memos/alarms_timers_screen.dart
  • zswatch_app/lib/ui/screens/voice_memos/voice_memos_screen.dart
  • zswatch_app/lib/ui/widgets/voice_memos/memo_list_item.dart
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/chrono_ai_flow/lib/src/prompt_template.dart

Comment thread zswatch_app/lib/ui/screens/voice_memos/alarms_timers_screen.dart
Comment on lines +252 to +275
if (isPending) ...[
FilledButton(
style: _compactFilledStyle(),
onPressed: _isCreating ? null : _createAction,
child: Text(_isCreating ? 'Setting...' : 'Set'),
),
OutlinedButton(
style: _compactWarnStyle(),
onPressed: _isDismissing ? null : _dismissAction,
child: Text(_isDismissing ? 'Dismissing...' : 'Dismiss'),
),
],
if (action.created && action.platformTargetId != null)
OutlinedButton(
style: _compactOutlinedStyle(),
onPressed: _isOpening ? null : _openCreatedAction,
child: const Text('Open'),
),
// Delete is always available
OutlinedButton(
style: _compactDangerStyle(),
onPressed: _isDeleting ? null : _deleteAction,
child: Text(_isDeleting ? 'Deleting...' : 'Delete'),
),

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

Disable the whole action row while one async operation is in flight.

Set, Dismiss, Delete, and Open only disable themselves. That still lets users fire Delete or Dismiss while createAction() is awaiting the OS call, which can leave a timer/alarm created but no longer tracked in the database.

🛡️ Suggested fix
   `@override`
   Widget build(BuildContext context) {
     final isPending = !action.created && !action.dismissed;
+    final isBusy =
+        _isCreating || _isDismissing || _isDeleting || _isOpening;
...
-                    onPressed: _isCreating ? null : _createAction,
+                    onPressed: isBusy ? null : _createAction,
...
-                    onPressed: _isDismissing ? null : _dismissAction,
+                    onPressed: isBusy ? null : _dismissAction,
...
-                    onPressed: _isOpening ? null : _openCreatedAction,
+                    onPressed: isBusy ? null : _openCreatedAction,
...
-                  onPressed: _isDeleting ? null : _deleteAction,
+                  onPressed: isBusy ? null : _deleteAction,

Also applies to: 303-395

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@zswatch_app/lib/ui/screens/voice_memos/alarms_timers_screen.dart` around
lines 252 - 275, The action row currently only disables individual buttons;
compute a single busy flag (e.g. _isBusy = _isCreating || _isDismissing ||
_isDeleting || _isOpening) and use it to disable or block the whole row when any
async operation is in flight: wrap the buttons block (the
FilledButton/OutlinedButton group that checks isPending and the always-available
Delete/Open buttons) with an IgnorePointer or AbsorbPointer keyed to _isBusy
and/or set each button's onPressed to null when _isBusy, so no other action
(_createAction, _dismissAction, _deleteAction, _openCreatedAction) can be
invoked while any of the async flags are true; apply the same pattern to the
other affected ranges (303-395) mentioned in the comment.

Comment on lines +364 to +404
/// Clean up AI summary text that may contain raw JSON from a previous run.
String cleanSummary(String summary) {
final trimmed = summary.trim();
if (trimmed.startsWith('[') || trimmed.startsWith('{')) {
// Attempt to extract a readable title from JSON
final titleMatch = RegExp(r'"title"\s*:\s*"([^"]*)"').firstMatch(trimmed);
final intentMatch = RegExp(r'"intent"\s*:\s*"([^"]*)"').firstMatch(trimmed);
final intent = intentMatch?.group(1);
final title = titleMatch?.group(1);

if (intent == 'timer') {
final durMatch =
RegExp(r'"duration_seconds"\s*:\s*(\d+)').firstMatch(trimmed);
final d = int.tryParse(durMatch?.group(1) ?? '') ?? 0;
final h = d ~/ 3600;
final m = (d % 3600) ~/ 60;
final s = d % 60;
final parts = <String>[
if (h > 0) '${h}h',
if (m > 0) '${m}m',
if (s > 0 || (h == 0 && m == 0)) '${s}s',
];
final dur = parts.join(' ');
return (title != null && title.isNotEmpty)
? 'Timer $dur — $title'
: 'Timer $dur';
}
if (intent == 'alarm') {
final expr = RegExp(r'"datetime_expression_english"\s*:\s*"([^"]*)"')
.firstMatch(trimmed)
?.group(1);
if (expr != null && expr.isNotEmpty) {
return (title != null && title.isNotEmpty)
? 'Alarm $expr — $title'
: 'Alarm $expr';
}
return (title != null && title.isNotEmpty) ? 'Alarm — $title' : 'Alarm';
}
if (title != null && title.isNotEmpty) return title;
}
return trimmed;

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 | 🟡 Minor

Use structured JSON parsing in cleanSummary().

The current regexes stop at the first raw ", so escaped fields like Wake up \"early\" still render truncated timer/alarm labels here. jsonDecode would preserve the content and let this function handle real JSON instead of regex-scraping it.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@zswatch_app/lib/ui/widgets/voice_memos/memo_list_item.dart` around lines 364
- 404, The cleanSummary function should parse JSON structurally instead of using
fragile regexes: in cleanSummary(String summary) try to jsonDecode the trimmed
string (import dart:convert) inside a try/catch, handle resulting Map or List
(if List, use first element if it's a Map), then read keys like "title",
"intent", "duration_seconds" and "datetime_expression_english" directly from the
Map to build the same formatted outputs (timer: compute h/m/s from
duration_seconds; alarm: use datetime_expression_english; fallback to title),
and return trimmed if decoding fails or the decoded value doesn't match expected
types; keep existing return formats and fallback behavior.

…rder

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@jakkra jakkra merged commit b2b7019 into main Apr 2, 2026
5 checks passed
jakkra added a commit that referenced this pull request Apr 2, 2026
… mixed route

- Fix DB migration v1→v4+ crash: guard addColumn with from >= 2 so
  freshly-created tables don't get duplicate columns
- Fix "Timer 0s" summary when durationSeconds is null
- Skip permission request for timer/alarm on iOS (uses URL schemes)
- Route "mixed" router result to timerAlarmTemplate instead of dropping
  timer/alarm intents via the voice_memo-only compact template
- Scan all actions (not just first) for watch notification routing
- Reject incomplete timer/alarm in parser (timer needs duration, alarm
  needs datetime)
- Early-return in openCreatedAction for timer/alarm (no calendar entry)
- Format all Dart code for CI

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants