Skip to content

feat: ClarificationCard for NeedsClarification chat responses - #100

Merged
thomasluizon merged 13 commits into
mainfrom
fix/chat-clarification
May 19, 2026
Merged

feat: ClarificationCard for NeedsClarification chat responses#100
thomasluizon merged 13 commits into
mainfrom
fix/chat-clarification

Conversation

@thomasluizon

@thomasluizon thomasluizon commented May 19, 2026

Copy link
Copy Markdown
Owner

Summary

Frontend half of the two-layer #98 + #99 fix. Renders a structured <ClarificationCard> with quick-action buttons (Daily, Weekly, 3× per week, One-time) when the AI returns NeedsClarification for a habit-flavored title with no frequency. Tapping a button POSTs to the new backend resolve endpoint, which merges the chosen value into the partial args and re-invokes create_habit deterministically server-side.

  • Shared: extends actionStatusSchema with 'NeedsClarification'; adds quickActionSchema, clarificationRequestSchema, clarificationResolveResponseSchema; adds clarificationRequest field to actionResultSchema; adds API.ai.clarificationResolve(operationId); adds habits.clarification.* i18n keys in en.json and pt-BR.json
  • Web: resolveClarification server action; useResolveClarification TanStack mutation hook (invalidates habitKeys.lists/count/summaryPrefix); <ClarificationCard> component with idle/submitting/success/error states; dispatch wired in message-bubble.tsx
  • Mobile: parity-paired useResolveClarification (apiClient-based); <ClarificationCard> (React Native + StyleSheet matching breakdown-suggestion.tsx visual tokens); dispatch wired in message-bubble.tsx
  • Tests: 5 web (Vitest + RTL) + 5 mobile (Vitest + react-test-renderer) — renders question + 4 buttons, submits chosen value, shows success state, handles 404/generic errors

Linked issues

Closes #98
Closes #99

Paired PR

Parity

Item Web Mobile
Hook apps/web/hooks/use-resolve-clarification.ts apps/mobile/hooks/use-resolve-clarification.ts
Card apps/web/components/chat/clarification-card.tsx apps/mobile/components/chat/clarification-card.tsx
Dispatch apps/web/components/chat/message-bubble.tsx apps/mobile/components/message-bubble.tsx
Tests 5 cases 5 cases
i18n en + pt-BR en + pt-BR (shared)

Test plan

  • npm run type-check (turbo, all 3 packages): PASS
  • npm test: PASS — 1549 web + mobile + shared tests, 10 new clarification card tests
  • Manual smoke web: chat "Create a meditation habit" → ClarificationCard renders; tap Daily → success state; habit appears in /habits with frequency_unit: Day, frequency_quantity: 1
  • Regression web: "Create a one-time task to call the dentist Friday" → creates immediately, no card
  • Regression web: "Create a daily meditation habit" → creates immediately, no card
  • PT smoke web: "Crie o hábito de tomar café da manhã" → ClarificationCard renders
  • Mobile parity: repeat all above via npm run android
  • TTL: wait 31 minutes after card renders → tap a button → see "expired" error

Notes

  • npm run lint is broken on main: next lint was removed in Next.js 15+, ESLint 10 needs a new flat-config format. Verified to be a pre-existing issue, not caused by this PR. Tooling-modernization fix belongs in a separate PR.
  • Merge order: merge api PR first (so the resolve endpoint is live), wait for Render auto-deploy, then merge this PR.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • AI asks schedule follow-ups with quick-action buttons (daily, weekly, three-per-week, one-time).
    • Clarification cards appear inline in chat on web and mobile, showing pending, success, expired, or generic error states.
  • Documentation

    • Added an implementation plan and a detailed implementation report for the clarification flow.
  • Tests

    • Web and mobile tests covering clarification UI, resolution outcomes, and edge/error cases.
  • Localization

    • Added English and Portuguese (pt-BR) clarification strings.

Review Change Stack

Renders structured clarification card with quick-action buttons when
the AI needs to ask about an ambiguous habit schedule. Tapping a
button resolves the clarification server-side, invalidates habit
queries, and shows local success state.

- Shared: extends actionStatusSchema with NeedsClarification, adds
  clarificationRequestSchema and quickActionSchema, new endpoint
  constant, habits.clarification.* i18n keys (en + pt-BR)
- Web: ClarificationCard + useResolveClarification hook +
  resolveClarification server action + message-bubble dispatch
- Mobile: parity-paired ClarificationCard (RN) + hook (apiClient)
  + message-bubble dispatch
- Tests: 5 web + 5 mobile cases

Closes #98
Closes #99

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@vercel

vercel Bot commented May 19, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
orbit-ui-mobile-web Ignored Ignored May 19, 2026 7:50pm

@coderabbitai

coderabbitai Bot commented May 19, 2026

Copy link
Copy Markdown

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds NeedsClarification contracts and i18n; a clarification-resolve API helper and web server action; client hooks; ClarificationCard components for web and mobile with message-bubble wiring; and unit tests for resolve success, expired (404), and generic error cases.

Changes

Chat Clarification Flow

Layer / File(s) Summary
Design plan and report
.agents/plans/completed/chat-clarification.plan.md, .agents/reports/chat-clarification-report.md
Implementation plan and completed-work report documenting design decisions, task breakdown, validations, deviations, and next steps for the clarification feature.
Shared contracts, endpoint, and i18n
packages/shared/src/types/chat.ts, packages/shared/src/api/endpoints.ts, packages/shared/src/i18n/*
Adds NeedsClarification to ActionStatus; new Zod schemas/types for QuickAction and ClarificationRequest; extends ActionResult with optional clarificationRequest; adds API.ai.clarificationResolve(operationId); inserts English and Portuguese i18n keys for clarification question, quick-action labels, success, expired, and generic error messages.
Web server action, hook, UI, wiring, and tests
apps/web/app/actions/chat.ts, apps/web/hooks/use-resolve-clarification.ts, apps/web/components/chat/clarification-card.tsx, apps/web/components/chat/message-bubble.tsx, apps/web/__tests__/components/chat/clarification-card.test.tsx
Adds resolveClarification server action and useResolveClarification hook; ClarificationCard renders question + quick-action buttons, handles pending/success/error states and only invalidates habit queries when operation status is Succeeded; message-bubble renders clarification cards; tests cover rendering, interaction, success, expired (404), and generic error states.
Mobile hook, UI, wiring, and tests
apps/mobile/hooks/use-resolve-clarification.ts, apps/mobile/components/chat/clarification-card.tsx, apps/mobile/components/message-bubble.tsx, apps/mobile/__tests__/components/chat/clarification-card.test.tsx
Adds React Query useResolveClarification hook, React Native ClarificationCard with themed chips and spinner/pending behavior, message-bubble wiring to render clarification cards, and mobile unit tests verifying press handling and resolution success/expired/generic error flows.

Sequence Diagram

sequenceDiagram
  participant User
  participant ChatUI as Chat UI
  participant ClientHook as ResolveAction
  participant API as ClarifyAPI
  User->>ChatUI: Views ClarificationCard and selects an option
  ChatUI->>ClientHook: Call resolveClarification(operationId, value)
  ClientHook->>API: POST value
  API-->>ClientHook: Response with operation.status
  ClientHook-->>ChatUI: Return result
  ChatUI->>ChatUI: Render success or error and invalidate on success
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

Little rabbit hops to ask with care,
"Which schedule shall we bind?" it chews the air.
Buttons glow; a tap resumes the plan,
The tool runs on, not a vanished span.
Hooray — no silent one-time habit ran. 🐇✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 13.33% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title 'feat: ClarificationCard for NeedsClarification chat responses' accurately summarizes the main frontend change: introducing the ClarificationCard component to handle NeedsClarification status from the AI.
Linked Issues check ✅ Passed The PR implements all core requirements from #98 (prompt clarification infra foundation) and #99 (ClarificationCard UI, NeedsClarification status, clarification-resolve endpoint, and parity web/mobile components).
Out of Scope Changes check ✅ Passed All changes are directly scoped to the linked issues: shared type/schema/i18n additions, web/mobile components/hooks/tests, and API endpoint constants—no unrelated modifications detected.

✏️ 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 fix/chat-clarification

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

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 885c68cf4a

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread apps/web/components/chat/clarification-card.tsx Outdated
Comment thread apps/web/app/actions/chat.ts
Comment thread apps/mobile/hooks/use-resolve-clarification.ts Outdated
Comment thread packages/shared/src/types/chat.ts Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 4

Caution

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

⚠️ Outside diff range comments (1)
packages/shared/src/types/chat.ts (1)

116-126: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Enforce clarificationRequest when status is NeedsClarification.

actionResultSchema currently allows status: 'NeedsClarification' with no clarificationRequest, which can cause the clarification flow to vanish at runtime.

Suggested fix
 export const actionResultSchema = z.object({
   type: aiActionTypeSchema,
   status: actionStatusSchema,
   entityId: z.string().nullable(),
   entityName: z.string().nullable(),
   error: z.string().nullable(),
   field: z.string().nullable(),
   suggestedSubHabits: z.array(suggestedSubHabitSchema).nullable(),
   conflictWarning: conflictWarningSchema.nullable(),
   clarificationRequest: clarificationRequestSchema.nullable().optional(),
-})
+}).superRefine((value, ctx) => {
+  if (value.status === 'NeedsClarification' && !value.clarificationRequest) {
+    ctx.addIssue({
+      code: z.ZodIssueCode.custom,
+      path: ['clarificationRequest'],
+      message: 'clarificationRequest is required when status is NeedsClarification',
+    })
+  }
+})
🤖 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 `@packages/shared/src/types/chat.ts` around lines 116 - 126, The
actionResultSchema allows status to be NeedsClarification without a
clarificationRequest, causing lost clarification flows; update
actionResultSchema (the z.object with fields type, status, clarificationRequest,
etc.) to add a Zod refinement/superRefine that checks when status ===
'NeedsClarification' then clarificationRequest must be non-null/non-empty and
otherwise allow nullable, and return a clear path-specific error (e.g., on
'clarificationRequest') if missing; ensure the check uses the same
actionStatusSchema enum value (NeedsClarification) so validation enforces the
presence of clarificationRequest when required.
🧹 Nitpick comments (1)
.agents/plans/completed/chat-clarification.plan.md (1)

232-246: ⚡ Quick win

Mark superseded tasks in this completed plan to avoid implementation drift confusion.

This completed plan still reads as if ResolveClarificationCommand and related tests/integration tests were implemented, while the report records a controller-inline deviation. Add an explicit “superseded by implementation deviation” note beside these tasks (or link directly to the deviation section) so future readers don’t treat these as delivered artifacts.

Also applies to: 393-405, 447-464

🤖 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 @.agents/plans/completed/chat-clarification.plan.md around lines 232 - 246,
Update the completed plan to mark tasks that were not implemented as originally
specified as "superseded by implementation deviation": add a clear superseded
note next to the entries for ResolveClarificationCommand
(src/Orbit.Application/Chat/Commands/ResolveClarificationCommand.cs), the
AiController change (src/Orbit.Api/Controllers/AiController.cs), the
PendingClarification store/DB artifacts, and the associated test files
(ResolveClarificationCommandTests, ClarificationFlowTests,
CreateHabitToolClarificationTests) indicating they were replaced by a
controller-inline implementation; either add a one-line “Superseded by
implementation deviation — see deviation section” next to each affected bullet
or link directly to the deviation section so future readers don’t treat these as
delivered artifacts.
🤖 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 @.agents/reports/chat-clarification-report.md:
- Line 128: The report table row referencing the plan file uses the wrong path
for chat-clarification.plan.md; update the table entry in
.agents/reports/chat-clarification-report.md so the plan link points to the
committed location under the plans completed subdirectory (i.e., change the
target to the completed plan location for chat-clarification.plan.md) to restore
correct traceability.

In `@apps/mobile/__tests__/components/chat/clarification-card.test.tsx`:
- Around line 4-5: The test imports and uses TestRenderer (TestRenderer.create
and TestRenderer.act) which violates the repo guideline to use React Testing
Library; replace TestRenderer usage with React Testing Library utilities: import
{ render, screen, act } from '`@testing-library/react`' (or from test-utils if you
have a wrapper), change TestRenderer.create(<ClarificationCard ... />) to
render(<ClarificationCard clarificationRequest={baseClarification} />), replace
assertions that rely on the TestRenderer output with DOM queries via screen
(e.g., getByText/getByRole), and convert TestRenderer.act(...) blocks to the
testing-library act or the async utilities (await act(async () => ...) or use
waitFor) around actions that cause updates so all occurrences of TestRenderer,
TestRenderer.create, TestRenderer.act, and usage with
ClarificationCard/baseClarification are migrated accordingly.
- Around line 6-14: Replace all uses of the any type with unknown and add proper
narrowing/type guards or concrete typings: change colorProxy's declaration from
any to a typed Proxy (e.g., Proxy<Record<string, string> | unknown, ...> using
unknown where appropriate and narrowing to string when accessing values), update
mocked component props (replace any with the component prop interface or unknown
then cast after a guard), change helper function parameters from any to unknown
and add runtime type checks before use, and update TestRenderer tree variables
and node-tree callback parameters from any to unknown and narrow them with type
predicates or explicit casts where their shape is asserted; reference symbols to
adjust: colorProxy, the mocked prop variables passed to the component render
calls, the helper functions used in tests, and the TestRenderer/tree node
callback functions so that all previous any annotations are replaced with
unknown and safely narrowed before use.

In `@apps/web/components/chat/clarification-card.tsx`:
- Around line 70-77: clarificationRequest.question and action.label are being
cast to IntlKey and passed directly to t() (in the JSX around
clarificationRequest.question and inside the quickActions map) which breaks when
backend provides literal text; update the rendering to use the same defensive
fallback as mobile: call t(clarificationRequest.question as IntlKey, {
defaultValue: clarificationRequest.question }) and for each action use
t(action.label as IntlKey, { defaultValue: action.label }), keeping the existing
activeValue logic intact so literal strings render when no translation key
exists.

---

Outside diff comments:
In `@packages/shared/src/types/chat.ts`:
- Around line 116-126: The actionResultSchema allows status to be
NeedsClarification without a clarificationRequest, causing lost clarification
flows; update actionResultSchema (the z.object with fields type, status,
clarificationRequest, etc.) to add a Zod refinement/superRefine that checks when
status === 'NeedsClarification' then clarificationRequest must be
non-null/non-empty and otherwise allow nullable, and return a clear
path-specific error (e.g., on 'clarificationRequest') if missing; ensure the
check uses the same actionStatusSchema enum value (NeedsClarification) so
validation enforces the presence of clarificationRequest when required.

---

Nitpick comments:
In @.agents/plans/completed/chat-clarification.plan.md:
- Around line 232-246: Update the completed plan to mark tasks that were not
implemented as originally specified as "superseded by implementation deviation":
add a clear superseded note next to the entries for ResolveClarificationCommand
(src/Orbit.Application/Chat/Commands/ResolveClarificationCommand.cs), the
AiController change (src/Orbit.Api/Controllers/AiController.cs), the
PendingClarification store/DB artifacts, and the associated test files
(ResolveClarificationCommandTests, ClarificationFlowTests,
CreateHabitToolClarificationTests) indicating they were replaced by a
controller-inline implementation; either add a one-line “Superseded by
implementation deviation — see deviation section” next to each affected bullet
or link directly to the deviation section so future readers don’t treat these as
delivered artifacts.
🪄 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 Plus

Run ID: 4eea4d25-2468-4612-83d4-456020ac5b7d

📥 Commits

Reviewing files that changed from the base of the PR and between 32a6ab0 and 885c68c.

📒 Files selected for processing (15)
  • .agents/plans/completed/chat-clarification.plan.md
  • .agents/reports/chat-clarification-report.md
  • apps/mobile/__tests__/components/chat/clarification-card.test.tsx
  • apps/mobile/components/chat/clarification-card.tsx
  • apps/mobile/components/message-bubble.tsx
  • apps/mobile/hooks/use-resolve-clarification.ts
  • apps/web/__tests__/components/chat/clarification-card.test.tsx
  • apps/web/app/actions/chat.ts
  • apps/web/components/chat/clarification-card.tsx
  • apps/web/components/chat/message-bubble.tsx
  • apps/web/hooks/use-resolve-clarification.ts
  • packages/shared/src/api/endpoints.ts
  • packages/shared/src/i18n/en.json
  • packages/shared/src/i18n/pt-BR.json
  • packages/shared/src/types/chat.ts

Comment thread .agents/reports/chat-clarification-report.md Outdated
Comment thread apps/mobile/__tests__/components/chat/clarification-card.test.tsx Outdated
Comment thread apps/mobile/__tests__/components/chat/clarification-card.test.tsx Outdated
Comment thread apps/web/components/chat/clarification-card.tsx
Comment thread apps/web/components/chat/clarification-card.tsx Outdated
Comment thread apps/web/components/chat/clarification-card.tsx Outdated
Comment thread apps/web/hooks/use-resolve-clarification.ts Outdated
Comment thread apps/mobile/hooks/use-resolve-clarification.ts Outdated
Comment thread apps/web/components/chat/message-bubble.tsx Outdated
Comment thread apps/mobile/__tests__/components/chat/clarification-card.test.tsx Outdated
@claude

claude Bot commented May 19, 2026

Copy link
Copy Markdown

Review summary

Good cross-platform coverage and clean architecture overall. One correctness bug and a few policy violations need addressing before merge.

Must fix

# File Issue
1 apps/web/app/actions/chat.ts:147–149 Wrong response typeresolveClarification is typed as AgentExecuteOperationResponse but the endpoint returns ClarificationResolveResponse. The shared schema added in this PR is never wired in.
2 apps/mobile/hooks/use-resolve-clarification.ts:5,12 Same type mismatch on the mobile side.
3 packages/shared/src/types/chat.ts:57 clarificationResolveResponseSchema / ClarificationResolveResponse are dead exports until the callers above are fixed.
4 apps/mobile/__tests__/components/chat/clarification-card.test.tsx:4,6 ~10 uses of any (banned by AGENTS.md). CommonJS require() in an ESM test file.

Should fix

# File Issue
5 apps/web/hooks/use-resolve-clarification.ts:14 onSettled invalidates habit caches even on error; use onSuccess.
6 apps/mobile/hooks/use-resolve-clarification.ts:21 Same onSettled / onSuccess issue.
7 apps/web/components/chat/clarification-card.tsx:71,76 Server-provided strings cast to IntlKey with no fallback guard (mobile version handles this correctly with defaultValue).
8 apps/web/components/chat/message-bubble.tsx:158–159 / apps/mobile/components/message-bubble.tsx:234–235 Non-null assertions (!) can be eliminated by narrowing the type at the filter call site.

Looks good

  • Parity table in the PR description is accurate and complete.
  • i18n keys added in both en.json and pt-BR.json with no hardcoded strings in production code.
  • No console.log in production paths.
  • Query key imports correctly use @orbit/shared/query; type imports use @orbit/shared/types.
  • Idle/submitting/success/error states are consistent across both platforms.
  • 10 new tests (5 web + 5 mobile) cover the main paths.

🤖 Generated with Claude Code

Frontend hardening from review:

- Cards (web + mobile): gate success state on operation.status ===
  'Succeeded'. HTTP 200 with status Denied/Failed/PendingConfirmation
  no longer renders a misleading green "Created" message — shows the
  generic error instead.
- Hooks (web + mobile): switch invalidation from onSettled to onSuccess
  and additionally gate on operation.status === 'Succeeded'. Cache no
  longer wastefully invalidates on 4xx/5xx or denied operations.
- Shared schema: actionResultSchema gets a superRefine that enforces
  clarificationRequest is present when status is NeedsClarification.
  Removed the unused clarificationResolveResponseSchema and
  ClarificationResolveResponse type (dead code; the action and hook
  correctly use AgentExecuteOperationResponse which matches the
  backend's actual response shape).
- message-bubble (web + mobile): narrowed the filter type predicate so
  action.clarificationRequest is non-null by type, dropping the !
  non-null assertions.
- Web card: replaced unsafe `as IntlKey` casts with a translateOrLiteral
  helper that falls back to the literal when next-intl can't find the
  key (matches mobile's defaultValue pattern).
- Mobile card: added defaultValue fallback to the action.label
  translation call (it was already on the question).
- Mobile test: eliminated all 12 `any` annotations per AGENTS.md ("Zero
  any. Use unknown with narrowing."). Local TestNode/TestInstance
  interfaces describe the react-test-renderer shape.
- Tests: added "non-Succeeded operation" cases on web + mobile that
  prove the false-success bug is closed.
- Report: fixed the plan path to point to the archived location.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Comment thread apps/web/components/chat/clarification-card.tsx Outdated
Comment thread apps/web/app/actions/chat.ts
Comment thread packages/shared/src/i18n/en.json
@claude

claude Bot commented May 19, 2026

Copy link
Copy Markdown

Code Review — PR #100

Recommendation: APPROVE with minor fixes

Summary

Solid implementation. Architecture is clean, cross-platform parity is complete, i18n keys landed in both locales, and the superRefine invariant that rejects a NeedsClarification result without a clarificationRequest payload is a nice defensive touch. 10 tests cover the success, expiry, generic-error, and non-Succeeded HTTP-200 paths across both platforms.

Issues (inline comments posted)

Severity Location Issue
Medium apps/web/components/chat/clarification-card.tsx:120 translateOrLiteral: the !keyOrLiteral.includes('.') guard is dead code — both branches return the same value when translated === keyOrLiteral. Simplify or add a comment explaining why.
Medium apps/web/app/actions/chat.ts:149 resolveClarification interpolates operationId into the URL without UUID validation. A direct Server Action call with a path-traversal string could reach an unintended backend route. Add a UUID regex guard before the fetch.
Low packages/shared/src/i18n/en.json:534 habits.clarification.submitting is declared in both locale files but never referenced in either component. Wire it up as an accessible loading label or remove it.

Note: the PR description mentions clarificationResolveResponseSchema was added to shared types — it wasn't; the implementation correctly reuses the existing AgentExecuteOperationResponse. No code issue, just a misleading description.

Safe to merge after the UUID validation gap is addressed. The dead condition and unused i18n key can follow in a cleanup PR.

- resolveClarification server action now rejects non-UUID operationId
  before interpolating into the request URL. Server Actions can be
  invoked directly without going through the React component, so the
  guard is at the action level rather than only the component.
- i18n: removed the unused habits.clarification.submitting key from
  en.json and pt-BR.json — never wired up; the loading state is
  conveyed by the spinner icon alone.
- Web ClarificationCard: dropped the translateOrLiteral helper. Its
  dot-containing-key branch returned the same string as the fallback,
  so the condition was dead. next-intl returns the key verbatim on a
  miss, which naturally doubles as the literal fallback path.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Comment thread apps/web/components/chat/clarification-card.tsx
Comment thread apps/web/app/actions/chat.ts
@claude

claude Bot commented May 19, 2026

Copy link
Copy Markdown

Review summary

Overall this is solid work — parity is complete, i18n is correct in both locales, no any, no console.log, all user-facing strings go through the catalog, and the test coverage mirrors the BreakdownSuggestion precedent faithfully.

Two issues flagged inline:

  1. Web ClarificationCard — missing try/catch (comment): handleSelect doesn't wrap mutateAsync in try/catch, so an unexpected throw leaves the active button stuck in its loading state. The mobile implementation handles this correctly with try/catch/finally; the web component should match it.

  2. resolveClarification server action — value is unvalidated (comment): operationId is UUID-validated before URL interpolation (good), but value (an arbitrary JSON string) passes through with no size or format check. Since the code comment already notes Server Actions are directly invocable, a cheap length guard keeps both inputs symmetrically defended.

Minor note: The PR description lists clarificationResolveResponseSchema as added to packages/shared/src/types/chat.ts, but it's not in the diff — the mobile hook types the response as the pre-existing AgentExecuteOperationResponse. No functional impact, just an inaccuracy in the description worth correcting before merge.

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

🤖 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 `@apps/web/components/chat/clarification-card.tsx`:
- Line 6: The import of the ClarificationRequest type is using the deeper barrel
'`@orbit/shared/types/chat`' instead of the shared types barrel; change the import
for ClarificationRequest to come from '`@orbit/shared/types`' (update the import
statement that currently references ClarificationRequest) so it follows the
repository contract for shared type imports.
- Line 95: Remove the disallowed color transition from the button class on the
ClarificationCard UI: in the element whose className contains "inline-flex
items-center ... transition-colors duration-150 ..." remove "transition-colors"
(or replace it with "transition-opacity" or "transition-transform" if you want a
permitted animation) so only transform/opacity transitions are used; keep the
rest of the className unchanged.
🪄 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 Plus

Run ID: 2bee8089-63d8-42a9-964a-9091eb80b7f4

📥 Commits

Reviewing files that changed from the base of the PR and between 14870ac and 4feb5b8.

📒 Files selected for processing (4)
  • apps/web/app/actions/chat.ts
  • apps/web/components/chat/clarification-card.tsx
  • packages/shared/src/i18n/en.json
  • packages/shared/src/i18n/pt-BR.json
🚧 Files skipped from review as they are similar to previous changes (2)
  • packages/shared/src/i18n/en.json
  • apps/web/app/actions/chat.ts

Comment thread apps/web/components/chat/clarification-card.tsx Outdated
Comment thread apps/web/components/chat/clarification-card.tsx Outdated
- resolveClarification server action: also bounds-check `value`
  (string, non-empty, <= 2048 chars) — mirrors AppConstants on the
  backend. operationId was already UUID-validated; this gives both
  inputs symmetric client guards for direct-Server-Action callers.
- Web ClarificationCard: wrap handleSelect's mutateAsync in
  try/catch/finally so that unmount-mid-await, query-abort, or any
  future throw can't leave the tapped button permanently disabled.
  Matches the pattern already used in the mobile card.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Comment thread apps/web/components/chat/clarification-card.tsx Outdated
Comment thread apps/web/components/chat/clarification-card.tsx Outdated
Comment thread apps/mobile/components/chat/clarification-card.tsx Outdated
Comment thread apps/mobile/__tests__/components/chat/clarification-card.test.tsx Outdated
@claude

claude Bot commented May 19, 2026

Copy link
Copy Markdown

Review summary

Overall this is well-executed — cross-platform parity is solid, i18n is complete for both locales, no any / console.log violations, Zod schema has a useful superRefine guard, and the 10 tests cover the important paths.

Issues flagged (4 inline comments)

# Severity File Issue
1 🔴 Required apps/web/components/chat/clarification-card.tsx L102 Missing focus-visible: on quick-action buttons — CLAUDE.md requires all clickable elements to have hover + focus-visible + active states
2 🟡 Suggestion apps/web/components/chat/clarification-card.tsx L45 HTTP 410 (already-resolved) falls through to errorGeneric; worth a dedicated errorAlreadyResolved key to avoid confusing the user
3 🟡 Suggestion apps/mobile/components/chat/clarification-card.tsx L65 Same 410 gap on mobile — keep both platforms in sync with whatever is decided above
4 🟢 Nit apps/mobile/components/chat/clarification-card.tsx L11 + test L2 Deep import @orbit/shared/types/chat — AGENTS.md says use the barrel @orbit/shared/types

Minor note on PR description: the mobile test file has 7 test cases, not 5 as stated; more coverage is good, just update the count.

Item 1 is the only blocker — the rest are suggestions.

… error

- Web + mobile cards: map status 409 (Conflict — already resolved)
  to a new habits.clarification.errorAlreadyResolved key instead of
  falling through to the generic error. en + pt-BR locale entries
  added. 404 → errorExpired stays as before.
- Extracted both card status → error-key mappings into a small
  mapStatusToErrorKey helper so the table is a single line.
- Web quick-action button: added focus-visible:ring styling so
  keyboard navigation surfaces a visible focus state per CLAUDE.md
  "every clickable element needs hover, focus-visible, and active".
- apps/mobile/components/chat/clarification-card.tsx and the test
  file: ClarificationRequest now imported from @orbit/shared/types
  (barrel) instead of the deep types/chat path, matching AGENTS.md.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Comment thread apps/web/components/chat/clarification-card.tsx
Comment thread apps/mobile/components/chat/clarification-card.tsx
Comment thread apps/web/__tests__/components/chat/clarification-card.test.tsx
Comment thread apps/mobile/__tests__/components/chat/clarification-card.test.tsx
@claude

claude Bot commented May 19, 2026

Copy link
Copy Markdown

Review: feat/ClarificationCard — NeedsClarification flow

Recommendation: NEEDS WORK (minor)

What is good:

  • Full cross-platform parity: shared Zod schema, matching hooks, identical component logic, both message-bubble dispatchers updated, both i18n files updated together.
  • superRefine on actionResultSchema fails loudly when NeedsClarification arrives without a payload.
  • Input validation at the server action level (UUID regex, length cap) and in the mobile hook, with MAX_CLARIFICATION_VALUE_LENGTH centralised in @orbit/shared/api.
  • safeT / defaultValue fallback handles backend-sent literal strings vs. i18n keys correctly.
  • Error state is cleared on retry and button unblocked in finally — no stuck-spinner risk.

Issues:

[High] 410 vs 404 for TTL expiry — inline suggestions on both clarification-card files

Plan D7 states the backend returns 410 Gone for TTL expiry, but mapStatusToErrorKey maps only 404 to errorExpired; 410 falls through to errorGeneric. If the backend follows the plan, expired clarifications show "Something went wrong" instead of "This clarification expired". Please confirm which status code the API actually returns for an expired operation.

[Medium] Missing 409 test in both platforms — inline suggestions on both test files

mapStatusToErrorKey handles 409 to errorAlreadyResolved but no test exercises this path in either platform's test file.

Generated with Claude Code

- Both cards' mapStatusToErrorKey now treats 404 and 410 identically
  (errorExpired). Backend currently returns 404 for a vanished row;
  410 is the semantically-correct HTTP status for an expired resource
  and would be safe to switch to without a frontend change. Forward-
  compatible.
- Added two new web Vitest cases and two new mobile cases covering
  the 409 (already-resolved) and 410 (gone) → error-key mappings, so
  the table is exercised end-to-end.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Comment thread packages/shared/src/types/chat.ts Outdated
Comment thread apps/web/components/chat/clarification-card.tsx Outdated
Comment thread apps/web/components/chat/clarification-card.tsx Outdated
Comment thread apps/mobile/components/chat/clarification-card.tsx Outdated
@claude

claude Bot commented May 19, 2026

Copy link
Copy Markdown

Code Review — PR #100

Recommendation: APPROVE with minor fixes

Solid implementation of the NeedsClarification clarification flow. Cross-platform parity is complete (shared types, hooks, components, message-bubble dispatch, tests, i18n in both locales). No any types, no console.log, all imports from the correct @orbit/shared/* sub-paths, UUID guard in the server action, and input validation mirrored between the web server action and mobile hook.

Issues

Severity Count
Medium 2 — error text on both web and mobile lacks aria-live/accessibilityLiveRegion; screen readers won't announce errors dynamically
Low 2 — active:scale-[0.98] missing transition-transform (violates the "only animate transform with easing" design rule); quickActionSchema.value allows empty string

Notes

  • PR body says "5 web + 5 mobile tests" — actual count is 8 web + 9 mobile. Good, not bad.
  • npm run lint is broken on main (pre-existing, not caused by this PR).
  • Merge order (API PR first, then this) is correctly documented.

- Web + mobile error texts now act as ARIA live regions: role=\"alert\"
  on the web <p>, accessibilityLiveRegion=\"polite\" +
  accessibilityRole=\"alert\" on the mobile <Text>. Assistive tech
  announces resolve failures when they appear.
- Web quick-action button: added transition-transform duration-150
  so active:scale-[0.98] eases instead of snapping. Still complies
  with the transform/opacity-only animation rule.
- quickActionSchema.value: .min(1) so an empty value never round-
  trips through the client — the backend already rejects but the
  schema is now honest about it.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Comment thread apps/web/components/chat/clarification-card.tsx
Comment thread apps/mobile/components/chat/clarification-card.tsx
Comment thread packages/shared/src/types/chat.ts
@claude

claude Bot commented May 19, 2026

Copy link
Copy Markdown

Review: feat/ClarificationCard — APPROVE

Solid implementation. Full web/mobile parity maintained, all user-facing strings through i18n (both locales), zero any, no console.log, types from @orbit/shared/types, query keys from @orbit/shared/query. The two-layer architecture (prompt + structural) is well-reasoned; the superRefine guard on actionResultSchema is a good defensive addition.

Issues: 0 critical · 0 high · 0 medium · 3 low (inline comments posted)

Low File Note
PendingConfirmation edge case clarification-card.tsx (web + mobile) Status falls to errorGeneric; unlikely but misleading if it ever fires
nullable vs optional types/chat.ts:118 Confirm backend sends null (not omits) before keeping both modifiers

Validation

Check Result
No any Pass
No console.log Pass
Parity (web + mobile) Pass
i18n (en + pt-BR) Pass
Input validation Pass (UUID regex + length on server action; length guard mirrored on mobile)
Tests 8 web + 9 mobile covering render, submit, success, 404/409/410/500, non-Succeeded status

Generated with Claude Code

- Both ClarificationCards: comment noting that PendingConfirmation
  isn't expected on the resolve path (clarifications use HabitsWrite,
  no step-up). Falls through to errorGeneric and the user can re-
  initiate from chat — acceptable MVP trade-off, now documented.
- chat.ts `clarificationRequest`: kept `.nullable().optional()` but
  added a comment explaining the dual modifier — nullable covers the
  wire format (System.Text.Json writes null), optional is the cheap
  belt for existing fixtures that don't set the field.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
question: z.string(),
operationId: z.string().uuid(),
missingArgumentKey: z.string(),
quickActions: z.array(quickActionSchema),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Medium — defensive schema gap

quickActions has no minimum length constraint. If the backend ever sends an empty array (or a bug causes it), the card renders an unanswerable question — the user sees the prompt but has no buttons to respond.

Suggested change
quickActions: z.array(quickActionSchema),
quickActions: z.array(quickActionSchema).min(1),

This also makes the intent explicit: a ClarificationRequest with zero options is malformed by definition.

@sonarqubecloud

Copy link
Copy Markdown

Quality Gate Failed Quality Gate failed

Failed conditions
31.7% Coverage on New Code (required ≥ 80%)

See analysis details on SonarQube Cloud

)
}

function mapStatusToErrorKey(status: number): string {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Suggestion — duplicated helper

mapStatusToErrorKey is byte-for-byte identical to the one in apps/mobile/components/chat/clarification-card.tsx:128. Since both platforms share the same HTTP status codes and error key names, this could live in @orbit/shared/utils and be imported on both sides, keeping the mapping in one place if status codes ever change.

@claude

claude Bot commented May 19, 2026

Copy link
Copy Markdown

Plan: Chat Clarification — Stop silent one-time tasks + structural NeedsClarification fix

Combines GitHub issues #98 (prompt-level fix) and #99 (structural fix). One branch per repo: fix/chat-clarification in both orbit-ui-mobile and orbit-api.


Summary

The AI chat creates a habit as a one-time auto-completing task whenever the model forgets to pass frequency_unit — the habit vanishes after one check-off. This plan ships a two-layer fix in a single branch per repo:

  1. Prompt layer (AI silently creates one-time tasks when habit frequency isn't specified #98) — tighten CreateHabitTool.Description and add a rule to StructuringStrategySection so the model asks the user for a schedule before calling create_habit on a habit-flavored title.
  2. Structural layer (Add ActionStatus.NeedsClarification + chat UI question-card (web + mobile) #99) — new ActionStatus.NeedsClarification value, structured ClarificationRequest payload on ActionResult, server-side PendingClarificationStore keyed by OperationId, dedicated POST /api/ai/clarifications/{operationId}/resolve endpoint, and parity-paired <ClarificationCard> web + mobile components. CreateHabitTool becomes the first consumer; future tools can adopt the same pattern.

The two layers reinforce each other. If the prompt layer drifts, the tool still refuses to silently create a one-time task. If the tool's heuristic is too narrow, the prompt still catches it first.

User Story

As an in-app chat user
I want the AI to ask me what schedule I meant when I describe something as a habit without giving a frequency
So that my habit doesn't silently disappear after one check-off

Metadata

Field Value
Type BUG_FIX + NEW_CAPABILITY
Complexity HIGH
Repos both
Parity Required yes
GitHub Issues #98, #99
Web Affected yes
Mobile Affected yes
API Affected yes

Key design decisions

D1 — Resume mechanism: server-side store

Picked Option 2 from #99's tech notes. New PendingClarification entity + PendingClarificationStore + POST /api/ai/clarifications/{operationId}/resolve.

Why:

  • Survives page reload (state doesn't live in JS component state).
  • Avoids LLM re-interpretation: the resolved arg is merged deterministically by the backend, not re-parsed by the model.
  • Codebase already has a precedent (PendingAgentOperationStore at src/Orbit.Infrastructure/Services/PendingAgentOperationStore.cs). Frontend pattern already exists (pending-operation-card.tsx follows the same callback-driven shape).
  • The brittle multilingual [clarification:frequency=daily] synthetic-message approach is rejected.

Why a NEW store rather than overloading PendingAgentOperationStore:

  • Confirmation operations have a policy / fingerprint / SHA256-token / step-up lifecycle.
  • Clarifications have none of that — they're a partial-args stash with a missing field to fill in.
  • Overloading the table would force conceptual mixing.

D2 — CreateHabitTool heuristic

The tool returns NeedsClarification iff both:

  1. frequency_unit is absent (parsed as null)
  2. title contains case-insensitive substring "habit", "rotina", or "hábito" (matches both English and pt-BR)

Intentionally narrow. Verb-phrase detection is unreliable in C# without NLP. Broader cases are handled by the prompt rule from #98, which fires before the tool is called.

D3 — Status sits on ActionResult, payload on a new field

Mirror the existing Suggestion pattern: ActionResult.Status = NeedsClarification, and add ActionResult.ClarificationRequest: ClarificationRequest? next to SuggestedSubHabits.

In the handler at ProcessUserChatCommand.cs:413 (special-case for suggest_breakdown), add a parallel branch that recognizes a ClarificationRequest payload from ToolResult.Payload and packages it into ActionStatus.NeedsClarification. Before returning, the handler calls PendingClarificationStore.Create(...) to stash the partial args server-side.

D4 — Capability registration

Add AgentCapabilityIds.ClarifyHabitArguments (or reuse HabitsWrite?). Pick: reuse HabitsWrite. Clarification is a synthetic intermediate state of create_habit — same capability, same risk class. No new policy gate needed.

D5 — Resolve endpoint authorization

POST /api/ai/clarifications/{operationId}/resolve is authenticated (JWT bearer) and scoped to the owning user. Store lookup compares operationId.UserId == principal.UserId and rejects 404 otherwise. No fresh-confirmation token needed — clarifications are non-destructive intermediate steps.

D6 — Re-execution

On resolve, the endpoint:

  1. Loads the stashed PartialArgumentsJson + MissingArgumentKey.
  2. Merges the user's value into the partial args (e.g., frequency_unit = "Day", frequency_quantity = 1).
  3. Constructs an AgentExecuteOperationRequest for the original tool (create_habit).
  4. Dispatches via IAgentOperationExecutor.ExecuteAsync (same path the chat handler uses).
  5. Marks the clarification resolved (one-shot — second call returns 410 Gone).
  6. Returns a minimal ClarificationResolveResponse { status, entityId?, entityName?, error? }.

The frontend, on success, triggers queryClient.invalidateQueries({ queryKey: habitKeys.lists() }) and renders a local success state in the card — mirroring BreakdownSuggestion.onConfirmed.

D7 — Lifecycle / TTL

PendingClarification rows expire after 30 minutes (matching AgentPlatformSettings.PendingOperationTtlMinutes). After expiry the resolve endpoint returns 410 Gone; the frontend renders "this clarification expired — please ask again."


Patterns to follow

Backend — Tool returning a structured payload

Existing precedent: SuggestBreakdownTool returns ToolResult(success: true, EntityName: title) and the handler at ProcessUserChatCommand.cs:413-420 special-cases the name to surface SuggestedSubHabits.

// SOURCE: src/Orbit.Application/Chat/Commands/ProcessUserChatCommand.cs:413-420
if (call.Name == "suggest_breakdown")
{
    return new ActionResult(
        ToolNameToPascalCase(call.Name),
        ActionStatus.Suggestion,
        EntityName: result.EntityName,
        SuggestedSubHabits: ExtractSuggestedSubHabits(call.Args));
}

We will not add another name-keyed special case. Instead, the tool returns the payload on ToolResult.Payload and the handler checks if (result.Payload is ClarificationRequest cr) { ... }. This generalizes the pattern so future tools (delete-by-name disambiguation, bulk-op scope) can adopt NeedsClarification without editing the handler.

Backend — Pending store (precedent to mirror, not extend)

// SOURCE: src/Orbit.Infrastructure/Services/PendingAgentOperationStore.cs
public async Task<PendingAgentOperation> Create(/* ... */) {
    // checks for existing, builds entity, saves to DB, returns view
}

New PendingClarificationStore mirrors the shape but drops everything related to confirmation tokens, step-up, fingerprint dedup, policy state. Just: create → get → resolve (one-shot).

Backend — Endpoint precedent

// SOURCE: src/Orbit.Api/Controllers/AiController.cs:254-296
[HttpPost("pending-operations/{id:guid}/execute")]
public async Task<IActionResult> Execute(Guid id, [FromBody] ExecutePendingOperationRequest body, CancellationToken ct) { ... }

New endpoint sits as a sibling: [HttpPost("clarifications/{operationId:guid}/resolve")] on the same controller.

Backend — Prompt section

// SOURCE: src/Orbit.Infrastructure/Services/Prompts/Sections/Static/StructuringStrategySection.cs
public int Order => 250;
public bool ShouldInclude(PromptContext context) => true;
public string Build(PromptContext context) { /* return rule list */ }

Add a NEW section ClarificationGuidanceSection (e.g. Order = 260, always include) that teaches the model: "You may return NeedsClarification from any tool by setting clarification on the response. The user will see a card with your question + quick actions. Prefer this over plain text when the missing argument is a discrete enum (frequency unit, day of week, scope)."

Frontend — Status dispatch (web)

// SOURCE: apps/web/components/chat/message-bubble.tsx (lines ~53-138)
// Existing filters Suggestion and routes to <BreakdownSuggestion>
const suggestionActions = actions.filter(a => a.status === 'Suggestion' && a.suggestedSubHabits?.length)
const nonSuggestionActions = actions.filter(a => a.status !== 'Suggestion' /* not Suggestion */)

Add a parallel filter for a.status === 'NeedsClarification' && a.clarificationRequest. Mount <ClarificationCard> instead of <BreakdownSuggestion> for those.

Frontend — Mobile dispatch

// SOURCE: apps/mobile/components/message-bubble.tsx (lines ~115-234)

Same structure, React Native primitives.

Frontend — Card visual conventions

Match BreakdownSuggestion exactly:

  • Web: bg-surface-elevated/50 border border-border-muted rounded-[var(--radius-xl)] p-4 shadow-[var(--shadow-sm)]
  • Mobile: backgroundColor: colors.surfaceOverlay, borderRadius: radius.xl, padding: 16, ...shadows.sm, elevation: 2

Quick-action buttons follow the existing chip pattern at apps/mobile/components/chat/suggestion-chips.tsx:54-61 (full-pill borderRadius: 9999, bg-surface-elevated, tap → submit). Web uses Tailwind equivalents.

Frontend — Hook precedent

// SOURCE: apps/web/hooks/use-habits.ts (TanStack mutation pattern)
export function useBulkCreateHabits() {
  return useMutation({ mutationFn: ..., onSuccess: () => queryClient.invalidateQueries({ queryKey: habitKeys.lists() }) })
}

Create useResolveClarification in both apps/web/hooks/ and apps/mobile/hooks/ using the same mutation shape.

Shared — Endpoint constant precedent

// SOURCE: packages/shared/src/api/endpoints.ts
export const API = {
  // ... existing nested structure ...
}

Add ai.clarifications.resolve(operationId) next to the existing pending-operation endpoints.

Shared — Zod chat types

// SOURCE: packages/shared/src/types/chat.ts:34-36
export const actionStatusSchema = z.enum(['Success', 'Failed', 'Suggestion'])

Becomes z.enum(['Success', 'Failed', 'Suggestion', 'NeedsClarification']). Add clarificationRequestSchema (zod) with the matching shape from the backend record.

Tests

// SOURCE: tests/Orbit.Application.Tests/Chat/Tools/BulkLogHabitsToolTests.cs
public class BulkLogHabitsToolTests {
  [Fact] public async Task ExecutesSuccessfully() { ... }
}
// SOURCE: apps/web/__tests__/components/chat/breakdown-suggestion.test.tsx
test('renders + cancels + submits', () => { ... })

Files to change

orbit-api (branch fix/chat-clarification)

File Action Purpose
src/Orbit.Application/Chat/Tools/Implementations/CreateHabitTool.cs UPDATE Tighten Description (#98); add heuristic that returns ToolResult with ClarificationRequest payload (#99)
src/Orbit.Infrastructure/Services/Prompts/Sections/Static/StructuringStrategySection.cs UPDATE Add rule between line 49 and 51: "If the user calls something a habit/rotina/hábito without stating a schedule, ASK before calling create_habit." (#98)
src/Orbit.Application/Chat/Commands/ProcessUserChatCommand.cs UPDATE Add NeedsClarification to ActionStatus enum (line 47); add ClarificationRequest? ClarificationRequest to ActionResult (line 38-45); add handler branch to recognize ToolResult.Payload is ClarificationRequest and stash it via IPendingClarificationStore before returning
src/Orbit.Application/Chat/Models/ClarificationRequest.cs CREATE Record: Question, OperationId, MissingArgumentKey, QuickActions: IReadOnlyList<QuickAction>
src/Orbit.Application/Chat/Models/QuickAction.cs CREATE Record: Label, Value, Description?
src/Orbit.Application/Chat/Commands/ResolveClarificationCommand.cs CREATE MediatR command + handler. Loads pending clarification, merges value, dispatches via IAgentOperationExecutor, returns ClarificationResolveResponse
src/Orbit.Application/Common/Interfaces/IPendingClarificationStore.cs CREATE Methods: Create, GetById, Resolve
src/Orbit.Domain/Entities/PendingClarification.cs CREATE Id, UserId, ToolName, PartialArgumentsJson, MissingArgumentKey, Question, QuickActionsJson, CreatedAtUtc, ResolvedAtUtc?, ExpiresAtUtc
src/Orbit.Infrastructure/Services/PendingClarificationStore.cs CREATE DB-backed implementation
src/Orbit.Infrastructure/Persistence/OrbitDbContext.cs UPDATE Add DbSet<PendingClarification> PendingClarifications
src/Orbit.Infrastructure/Persistence/Configurations/PendingClarificationConfiguration.cs CREATE EF configuration (PK, indexes on UserId + CreatedAtUtc, ExpiresAtUtc)
src/Orbit.Infrastructure/Migrations/<ts>_AddPendingClarifications.cs CREATE EF migration
src/Orbit.Api/Controllers/AiController.cs UPDATE Add POST /api/ai/clarifications/{operationId:guid}/resolve — dispatches ResolveClarificationCommand
src/Orbit.Infrastructure/Services/Prompts/Sections/Static/ClarificationGuidanceSection.cs CREATE New prompt section explaining the NeedsClarification flow to the model
src/Orbit.Infrastructure/Services/SystemPromptBuilder.cs UPDATE Register ClarificationGuidanceSection in _sections
src/Orbit.Api/Extensions/ServiceCollectionExtensions.cs UPDATE DI: AddScoped<IPendingClarificationStore, PendingClarificationStore>()
tests/Orbit.Application.Tests/Chat/Tools/CreateHabitToolClarificationTests.cs CREATE Unit tests: triggers (habit keyword + no freq), bypasses (one-time markers, freq present, non-habit title)
tests/Orbit.Application.Tests/Chat/Commands/ResolveClarificationCommandTests.cs CREATE Unit tests: happy path, expired, already-resolved, wrong-user, missing arg key
tests/Orbit.IntegrationTests/Chat/ClarificationFlowTests.cs CREATE E2E: chat returns NeedsClarification → resolve endpoint → habit created with correct frequency

orbit-ui-mobile (branch fix/chat-clarification)

File Action Purpose
packages/shared/src/types/chat.ts UPDATE Add 'NeedsClarification' to actionStatusSchema; add clarificationRequestSchema + quickActionSchema; add clarificationRequest field to actionResultSchema
packages/shared/src/api/endpoints.ts UPDATE Add ai.clarifications.resolve(operationId)
packages/shared/src/query/keys.ts UPDATE (optional) no new keys needed — re-uses habitKeys.lists() for invalidation
packages/shared/src/i18n/en.json UPDATE Add habits.clarification.* keys (see below)
packages/shared/src/i18n/pt-BR.json UPDATE pt-BR equivalents
apps/web/components/chat/clarification-card.tsx CREATE Web <ClarificationCard> — matches BreakdownSuggestion visual language
apps/mobile/components/chat/clarification-card.tsx CREATE Mobile parallel, React Native primitives
apps/web/components/chat/message-bubble.tsx UPDATE Add filter for status === 'NeedsClarification'; mount <ClarificationCard>
apps/mobile/components/message-bubble.tsx UPDATE Mobile parallel
apps/web/app/actions/chat.ts UPDATE Add resolveClarification(operationId, value) server action that POSTs to the backend resolve endpoint
apps/web/hooks/use-resolve-clarification.ts CREATE TanStack mutation that calls the server action + invalidates habitKeys.lists()
apps/mobile/hooks/use-resolve-clarification.ts CREATE Mobile parallel using apiClient
apps/web/__tests__/components/chat/clarification-card.test.tsx CREATE Renders question + quick actions; tap submits + invalidates
apps/mobile/__tests__/components/chat/clarification-card.test.tsx CREATE Mobile parallel

i18n keys

"habits": {
  "clarification": {
    "questionFallback": "What schedule should this be on?",
    "quickAction": {
      "daily": "Daily",
      "weekly": "Weekly",
      "xPerWeek": "{n} times per week",
      "oneTime": "One-time task"
    },
    "submitting": "Setting up…",
    "successCreated": "Created '{name}'",
    "errorExpired": "This clarification expired. Please ask again.",
    "errorGeneric": "Something went wrong. Please try again."
  }
}

Tasks

Execute in this order. Tasks 1–3 are #98 (prompt-only — ship-able alone). Tasks 4–17 are #99 backend. Tasks 18–25 are #99 frontend. Tests interleave.

Phase A — Prompt-level fix (#98)

Task 1: Tighten CreateHabitTool.Description

  • Repo: api
  • File: src/Orbit.Application/Chat/Tools/Implementations/CreateHabitTool.cs
  • Action: UPDATE lines 22-23
  • Implement: Add explicit phrase: "For one-time tasks, omit frequency_unit ONLY if the user explicitly described it as a one-time task (e.g., 'just once', 'this Friday only', 'one-time', 'uma vez'). If the user called it a habit/rotina/hábito or did not mention frequency, ASK first via clarification."
  • Validate: dotnet build

Task 2: Add structuring rule for habit-flavored titles

  • Repo: api
  • File: src/Orbit.Infrastructure/Services/Prompts/Sections/Static/StructuringStrategySection.cs
  • Action: UPDATE — insert new rule between the existing "Ask ONE targeted clarifying question when" block (line 37-43) and "NEVER ask a question (act immediately) when" block (line 45-49)
  • Implement: "User calls something a 'habit' / 'rotina' / 'hábito' without stating daily / weekly / X times per week / a specific schedule → ASK before calling create_habit. Offer: daily, weekly with specific days, X times per week, or one-time task."
  • Validate: dotnet build

Task 3: Manual smoke test (post-deploy of Phase A only — optional gate)

  • Verify on local dev:
    • PT: "Crie o hábito de tomar café da manhã. Eu geralmente como pão, ovos e uma porção de frutas. Gostaria que cada item desse fosse um checklist" → AI asks before creating
    • EN: "Create a meditation habit" → AI asks
    • Regression: "Create a one-time task to call the dentist Friday" → AI creates without asking
    • Regression: "Create a daily meditation habit" → AI creates without asking
  • Skippable if Phase A and Phase B ship in the same PR.

Phase B — Backend structural fix (#99)

Task 4: Add ActionStatus.NeedsClarification + ClarificationRequest field

  • Repo: api
  • File: src/Orbit.Application/Chat/Commands/ProcessUserChatCommand.cs
  • Action: UPDATE
    • Line 47: enum becomes { Success, Failed, Suggestion, NeedsClarification }
    • Lines 38-45: add ClarificationRequest? ClarificationRequest = null to ActionResult record (after SuggestedSubHabits)
  • Validate: dotnet build

Task 5: Define ClarificationRequest + QuickAction records

  • Repo: api
  • File: src/Orbit.Application/Chat/Models/ClarificationRequest.cs (CREATE), src/Orbit.Application/Chat/Models/QuickAction.cs (CREATE)
  • Implement:
    public record ClarificationRequest(
        string Question,
        Guid OperationId,
        string MissingArgumentKey,
        IReadOnlyList<QuickAction> QuickActions);
    public record QuickAction(string Label, string Value, string? Description = null);
  • Validate: dotnet build

Task 6: Create PendingClarification entity + EF config + migration

  • Repo: api
  • Files:
    • src/Orbit.Domain/Entities/PendingClarification.cs (CREATE)
    • src/Orbit.Infrastructure/Persistence/Configurations/PendingClarificationConfiguration.cs (CREATE)
    • src/Orbit.Infrastructure/Persistence/OrbitDbContext.cs (UPDATE — add DbSet)
  • Entity fields: Id (Guid PK), UserId (Guid, indexed), ToolName (string), PartialArgumentsJson (text), MissingArgumentKey (string), Question (string), QuickActionsJson (text), CreatedAtUtc, ExpiresAtUtc (indexed), ResolvedAtUtc?
  • Mirror: src/Orbit.Domain/Entities/PendingAgentOperationState.cs for entity structure conventions
  • Validate: dotnet build, then dotnet ef migrations add AddPendingClarifications --project src/Orbit.Infrastructure --startup-project src/Orbit.Api

Task 7: Implement PendingClarificationStore

  • Repo: api
  • Files:
    • src/Orbit.Application/Common/Interfaces/IPendingClarificationStore.cs (CREATE)
    • src/Orbit.Infrastructure/Services/PendingClarificationStore.cs (CREATE)
  • Implement:
    • Task<PendingClarification> Create(Guid userId, string toolName, JsonElement partialArgs, string missingKey, string question, IReadOnlyList<QuickAction> actions, CancellationToken ct) — sets ExpiresAtUtc = DateTime.UtcNow.AddMinutes(30)
    • Task<PendingClarification?> GetById(Guid operationId, Guid userId, CancellationToken ct) — returns null if user mismatch or expired
    • Task Resolve(Guid operationId, Guid userId, CancellationToken ct) — sets ResolvedAtUtc, throws if already resolved
  • Mirror: src/Orbit.Infrastructure/Services/PendingAgentOperationStore.cs
  • Validate: dotnet build

Task 8: Refactor CreateHabitTool to emit ClarificationRequest

  • Repo: api
  • File: src/Orbit.Application/Chat/Tools/Implementations/CreateHabitTool.cs
  • Action: UPDATE
  • Implement:
    • Inject IPendingClarificationStore + IUserDateService (already injected) + AI prompt builder for question text? Actually no — generate question + quick actions inline.
    • At top of ExecuteAsync: parse title and frequency_unit.
    • If frequency_unit == null AND title (lowercased) contains any of "habit", "rotina", "hábito":
      • Build ClarificationRequest with localized question (use title in question), 4 quick actions: Daily / Weekly / 3× per week / One-time task.
      • Return ToolResult(success: true, Payload: clarificationRequest).
    • Otherwise proceed with existing creation logic unchanged.
  • Validate: dotnet build

Task 9: Wire handler to recognize ClarificationRequest payload + stash to store

  • Repo: api
  • File: src/Orbit.Application/Chat/Commands/ProcessUserChatCommand.cs
  • Action: UPDATE — in ExecuteSingleToolCallAsync (around lines 295-362), after the existing suggest_breakdown special case (line 413), add:
    if (result.Payload is ClarificationRequest cr)
    {
        // OperationId is set by the store, not the tool
        var saved = await pendingClarificationStore.Create(userId, call.Name, ParseArgs(call.Args), cr.MissingArgumentKey, cr.Question, cr.QuickActions, ct);
        return new ActionResult(
            ToolNameToPascalCase(call.Name),
            ActionStatus.NeedsClarification,
            ClarificationRequest: cr with { OperationId = saved.Id });
    }
  • Inject IPendingClarificationStore into the handler via the existing ChatExecutionDependencies record (line 73-79)
  • Validate: dotnet build

Task 10: Create ResolveClarificationCommand + handler

  • Repo: api
  • File: src/Orbit.Application/Chat/Commands/ResolveClarificationCommand.cs (CREATE)
  • Implement:
    • record ResolveClarificationCommand(Guid OperationId, Guid UserId, string Value) : IRequest<Result<ClarificationResolveResponse>>;
    • Handler:
      1. Load pending via IPendingClarificationStore.GetById. If null → Result.Failure("clarification_expired_or_missing").
      2. Deserialize PartialArgumentsJson to JsonElement, merge MissingArgumentKey: Value (use JsonObject for write).
      3. Construct AgentExecuteOperationRequest for the original ToolName.
      4. Call IAgentOperationExecutor.ExecuteAsync(...).
      5. Call store.Resolve(...).
      6. Return Result<ClarificationResolveResponse> with status + entityId + entityName.
  • Validate: dotnet build

Task 11: Add resolve endpoint to AiController

  • Repo: api
  • File: src/Orbit.Api/Controllers/AiController.cs
  • Action: UPDATE — add new method:
    [HttpPost("clarifications/{operationId:guid}/resolve")]
    public async Task<IActionResult> ResolveClarification(Guid operationId, [FromBody] ResolveClarificationRequest body, CancellationToken ct) { ... }
  • Mirror: existing Execute endpoint at line 254-296
  • Validate: dotnet build

Task 12: Add ClarificationGuidanceSection prompt section

  • Repo: api
  • File: src/Orbit.Infrastructure/Services/Prompts/Sections/Static/ClarificationGuidanceSection.cs (CREATE)
  • Implement: Order = 260, always include. Body teaches model:
    • When the user is ambiguous about a discrete enum (frequency, scope), the tool may return NeedsClarification.
    • This is preferred over plain-text questions when the answer is one of a small set of choices.
    • Do not over-ask. If the user gave a clear answer, do not invoke clarification.
  • Update: register in SystemPromptBuilder constructor
  • Validate: dotnet build

Task 13: DI registration

  • Repo: api
  • File: src/Orbit.Api/Extensions/ServiceCollectionExtensions.cs
  • Action: UPDATE AddOrbitAiServices — add builder.Services.AddScoped<IPendingClarificationStore, PendingClarificationStore>();
  • Validate: dotnet build

Task 14: Unit tests — CreateHabitTool clarification triggers

  • Repo: api
  • File: tests/Orbit.Application.Tests/Chat/Tools/CreateHabitToolClarificationTests.cs (CREATE)
  • Mirror: tests/Orbit.Application.Tests/Chat/Tools/BulkLogHabitsToolTests.cs
  • Cases:
    • Title contains "habit" + no frequency_unit → returns payload of type ClarificationRequest
    • Title contains "hábito" + no frequency_unit → same
    • Title contains "rotina" + no frequency_unit → same
    • Title contains "habit" + frequency_unit = "Day" → creates habit (no clarification)
    • Title = "call the dentist" + no frequency_unit → creates one-time task (no clarification)
    • Title case sensitivity: "MY DAILY HABIT" + no frequency_unit → returns clarification
  • Validate: dotnet test tests/Orbit.Application.Tests --filter CreateHabitToolClarificationTests

Task 15: Unit tests — ResolveClarificationCommandHandler

  • Repo: api
  • File: tests/Orbit.Application.Tests/Chat/Commands/ResolveClarificationCommandTests.cs (CREATE)
  • Cases: happy path; expired returns failure; already-resolved returns failure; wrong-user returns failure; missing arg key handled
  • Validate: dotnet test

Task 16: Integration test — full clarification flow

  • Repo: api
  • File: tests/Orbit.IntegrationTests/Chat/ClarificationFlowTests.cs (CREATE)
  • Mirror: existing integration tests under tests/Orbit.IntegrationTests/ (find Habits or Chat folder)
  • Flow:
    1. POST /api/chat with body { message: "Create a meditation habit" }
    2. Assert response contains actions[0].status === "NeedsClarification" + clarificationRequest.operationId
    3. POST /api/ai/clarifications/{operationId}/resolve with { value: "Day" }
    4. Assert habit is created with FrequencyUnit = Day, FrequencyQuantity = 1
    5. Second resolve call returns 410 Gone
  • Validate: dotnet test tests/Orbit.IntegrationTests --filter ClarificationFlowTests

Task 17: Manual smoke (backend only — optional)

  • Start API locally, hit /api/chat with bearer token, confirm NeedsClarification is returned for "Create a meditation habit" but UI not yet wired.
  • Skippable if Phase C is merged together.

Phase C — Frontend structural fix (#99) — PARITY REQUIRED

Task 18: Extend shared Zod chat types

  • Repo: ui-mobile
  • File: packages/shared/src/types/chat.ts
  • Action: UPDATE
    • actionStatusSchema = z.enum(['Success', 'Failed', 'Suggestion', 'NeedsClarification'])
    • Add quickActionSchema = z.object({ label, value, description: z.string().optional() })
    • Add clarificationRequestSchema = z.object({ question, operationId: z.string().uuid(), missingArgumentKey, quickActions: z.array(quickActionSchema) })
    • Add clarificationRequest: clarificationRequestSchema.optional() to actionResultSchema
  • Validate: npm run type-check

Task 19: Add resolve endpoint constant

  • Repo: ui-mobile
  • File: packages/shared/src/api/endpoints.ts
  • Action: UPDATE — add under ai:
    clarifications: {
      resolve: (operationId: string) => `/api/ai/clarifications/${operationId}/resolve` as const,
    }
  • Validate: npm run type-check

Task 20: Add i18n keys

  • Repo: ui-mobile
  • Files: packages/shared/src/i18n/en.json, packages/shared/src/i18n/pt-BR.json
  • Action: UPDATE — add habits.clarification.* namespace (see "i18n keys" above)
  • Validate: npm run type-check

Task 21: Web — resolveClarification server action

  • Repo: ui-mobile
  • File: apps/web/app/actions/chat.ts
  • Action: UPDATE — add async function resolveClarification(operationId: string, value: string) that POSTs to ${API_BASE}/api/ai/clarifications/${operationId}/resolve with bearer cookie. Returns parsed ClarificationResolveResponse.
  • Mirror: existing sendChatMessage at line ~37
  • Validate: npm run type-check

Task 22: Web hook — useResolveClarification

  • Repo: ui-mobile
  • File: apps/web/hooks/use-resolve-clarification.ts (CREATE)
  • Action: CREATE — useMutation calling the server action; on success: queryClient.invalidateQueries({ queryKey: habitKeys.lists() })
  • Mirror: apps/web/hooks/use-habits.ts mutation patterns
  • Validate: npm run type-check

Task 23: Web <ClarificationCard> component

  • Repo: ui-mobile
  • File: apps/web/components/chat/clarification-card.tsx (CREATE)
  • Implement:
    • Props: { clarificationRequest, onResolved, onCancelled }
    • Shows clarificationRequest.question + map quick actions to pill buttons
    • Tap → useResolveClarification.mutateAsync({ operationId, value: button.value })
    • States: idle, submitting (spinner on tapped button), success (green check + entity name), error (red text + retry)
    • Visual: bg-surface-elevated/50 border border-border-muted rounded-[var(--radius-xl)] p-4 shadow-[var(--shadow-sm)]
    • Quick action button: bg-surface-elevated border border-border-muted rounded-full px-3 py-1.5 text-xs (matching suggestion-chips.tsx styling)
  • Mirror: apps/web/components/chat/breakdown-suggestion.tsx
  • Validate: npm run type-check && npm run lint

Task 24: Web dispatch — message-bubble.tsx

  • Repo: ui-mobile
  • File: apps/web/components/chat/message-bubble.tsx
  • Action: UPDATE
    • Add filter: const clarificationActions = actions.filter(a => a.status === 'NeedsClarification' && a.clarificationRequest)
    • Update nonSuggestionActions filter to also exclude NeedsClarification
    • Mount <ClarificationCard> for each clarificationActions entry (similar to existing <BreakdownSuggestion> mount)
  • Validate: npm run type-check && npm run lint

Task 25: Mobile — hook + card + dispatch (parity with 21-24)

  • Repo: ui-mobile
  • Files:
    • apps/mobile/hooks/use-resolve-clarification.ts (CREATE) — uses apiClient instead of server action
    • apps/mobile/components/chat/clarification-card.tsx (CREATE) — React Native primitives, matches apps/mobile/components/chat/breakdown-suggestion.tsx styling tokens
    • apps/mobile/components/message-bubble.tsx (UPDATE) — parallel dispatch
  • Validate: npm run type-check && npm run lint

Task 26: Web unit tests

  • Repo: ui-mobile
  • File: apps/web/__tests__/components/chat/clarification-card.test.tsx (CREATE)
  • Mirror: apps/web/__tests__/components/chat/breakdown-suggestion.test.tsx
  • Cases: renders question + buttons; tap submits; loading state; success state; expired-error state
  • Validate: npx vitest run apps/web/__tests__/components/chat/clarification-card.test.tsx

Task 27: Mobile unit tests

  • Repo: ui-mobile
  • File: apps/mobile/__tests__/components/chat/clarification-card.test.tsx (CREATE)
  • Mirror: any existing mobile chat tests (e.g. apps/mobile/__tests__/components/chat/action-chips.test.tsx)
  • Validate: npx vitest run apps/mobile/__tests__/components/chat/clarification-card.test.tsx

Validation commands

orbit-api (from C:\Users\thoma\Documents\Programming\Projects\orbit-api)

dotnet build
dotnet test tests/Orbit.Application.Tests
dotnet test tests/Orbit.IntegrationTests --filter Clarification

orbit-ui-mobile (from C:\Users\thoma\Documents\Programming\Projects\orbit-ui-mobile)

npm run lint
npm run type-check
npx vitest run

End-to-end smoke

  • Start API: dotnet run --project src/Orbit.Api in orbit-api
  • Start web: npm run web in orbit-ui-mobile
  • Log in as test user, open /chat
  • Send: "Create a meditation habit"<ClarificationCard> renders with Daily / Weekly / 3× per week / One-time buttons
  • Tap "Daily" → card shows "Created 'meditation'" success state; habit appears in /habits with frequency_unit: Day, frequency_quantity: 1
  • Send (regression): "Create a one-time task to call the dentist Friday" → habit created immediately, no card
  • Send (regression): "Create a daily meditation habit" → habit created immediately, no card
  • Send PT: "Crie o hábito de tomar café da manhã" → ClarificationCard renders
  • Repeat all of the above on mobile via npm run android
  • Open /chat again 31 minutes after clarification rendered — tap a button → see "expired" error (validates TTL)

Risks

Risk Mitigation
Model returns NeedsClarification for cases where the user already gave clear intent (over-asking) New prompt section explicitly tells the model not to over-ask. Integration test covers the regression-safe cases.
Tool heuristic too narrow (verb-phrase habits like "Tomar café") Two-layer defense: prompt rule from #98 catches it before the tool fires. Tool is the safety net for explicit habit-keyword cases.
PendingClarification table grows unbounded TTL of 30 min via ExpiresAtUtc; add a hosted background sweep job in a follow-up (out of scope here — note in PR description).
Concurrent resolve attempts (double-tap) Resolve() is one-shot — throws on second call. Frontend disables button after first tap; error state catches race.
EF migration on Render auto-deploy could fail mid-rollout if web ships first Backend ships and migration runs before frontend can hit the new endpoint. Sequence: merge api PR first → wait for Render deploy → merge ui-mobile PR.
Shared types out of sync with backend record Zod schemas in packages/shared/src/types/chat.ts are hand-mirrored from C# — manual diligence required. Add a snapshot test that asserts the shape doesn't drift (future).

Acceptance criteria

@thomasluizon
thomasluizon merged commit 9211f01 into main May 19, 2026
8 of 9 checks passed
@thomasluizon
thomasluizon deleted the fix/chat-clarification branch May 19, 2026 20:13
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.

Add ActionStatus.NeedsClarification + chat UI question-card (web + mobile) AI silently creates one-time tasks when habit frequency isn't specified

1 participant