Skip to content

refactor(chat): extract composer core to @orbit/shared (#85) - #130

Merged
thomasluizon merged 3 commits into
mainfrom
issue-85
Jun 5, 2026
Merged

refactor(chat): extract composer core to @orbit/shared (#85)#130
thomasluizon merged 3 commits into
mainfrom
issue-85

Conversation

@thomasluizon

Copy link
Copy Markdown
Owner

Closes #85

Stacked on #128 (mobile type-check fix). The base branch is chore/fix-mobile-type-check, not main — merge #128 first, then this PR's base auto-retargets to main.

What

Lifts the chat-composer logic into a framework-agnostic core in packages/shared so any chat change is written once and both web and mobile inherit it (previously a 537-line web hook and 1544-line mobile screen drifted independently — policyReason, error classification, and upgrade resolution all differed).

Deviation from the issue's literal acceptance criteria (F4 = A, pure-core)

The issue asks for the core at packages/shared/hooks/use-chat-composer-core.ts as a "platform-agnostic state machine." But packages/shared/CLAUDE.md mandates pure TypeScript — no React/React Native imports. A use* hook calling useState/useCallback would import React into the shared package and violate that rule.

Reconciliation:

  • The core lives at packages/shared/src/hooks/chat-composer-core.ts (every shared module lives under src/ and is barreled through src/index.ts; reconciles the issue's packages/shared/hooks/ path).
  • It is a reducer + pure handler functions, not a use* hook: the de-duped action-type sets, buildAgentExecutionMessage, classifySendFailure, selectActionInvalidations, findPremiumPolicyDenial, the agent-query invalidation set, and the composer reducer. Zero react/react-native imports (QueryClient is typed from @tanstack/query-core, already a shared dep).
  • Thin per-app useChatComposer hooks wire React state + inject I/O (web: chat Server Actions; mobile: apiClient).

Divergence normalization (mobile adopts web's richer behavior)

  • buildAgentExecutionMessage keeps operation.policyReason in the fallback chain on both platforms (mobile previously omitted it).
  • Send-error classification has 408 timeout / 403 limit / upgrade / generic on both; mobile normalizes its thrown apiClient error into { status, code, reason } via extractBackendStatus/extractBackendError/extractBackendErrorCode before calling the shared classifySendFailure (mobile previously only had the upgrade branch).
  • Both platforms use the shared CHAT_GOAL_ACTION_TYPES for the goal/habit drawer routing.

Platform-only adapters stay per-app: web textarea autosize / DOM paste / localStorage draft; mobile AdMob rewarded ad (use-chat-reward.ts), useOffline gating, AsyncStorage draft, Android keyboard insets.

Mobile screen slimming

apps/mobile/app/chat.tsx: 1544 → 286 lines. Extracted createStyleschat.styles.ts; the 3 animation components → chat-animations.tsx; ChatComposerInput, ChatInputBar, ChatInputArea, ChatEmptyStatecomponents/chat/*; rewarded-ad flow → hooks/use-chat-reward.ts.

Tests & validation

  • New: shared core unit test (18 cases — reducer, classifySendFailure 408/403/upgrade/generic, buildAgentExecutionMessage precedence, selectActionInvalidations) and mobile hook test (7 cases — send success appends + usage bump, 403 premium → upgrade route, 403 non-upgrade → limit error, offline guard, pending-op confirm→execute endpoints).
  • npm run type-check ✅ (3/3), npm run lint ✅ (2/2), npm test ✅ (shared 830, web 1448, mobile 368 — green per workspace; turbo serial run 3/3).

Manual E2E pending

No browser E2E here. Manual E2E pending for the chat smoke flows on both platforms: image upload (pick/paste/validate → attach), history paging, tool-call / pending-operation rendering, retry, and error states (timeout / limit / upgrade-redirect / generic).

🤖 Generated with Claude Code

@vercel

vercel Bot commented Jun 4, 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 Jun 5, 2026 12:07am

Comment thread packages/shared/src/hooks/chat-composer-core.ts Outdated

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

One hard-rule violation in the otherwise excellent extraction work.

composerReducer / initialComposerState / ComposerState / ComposerAction (lines 167–194, packages/shared/src/hooks/chat-composer-core.ts) are dead exports — no app code imports them. Both hooks still wire sendError and showLangPicker via useState, so the reducer is untouched outside its own test file. This is a direct CLAUDE.md rule 2 violation ("No 'just in case' exports"). Fix: either delete the block and its test cases, or actually migrate the app hooks to useReducer(composerReducer, initialComposerState) so the export is load-bearing. Inline comment with both options posted on the relevant lines.

Comment thread packages/shared/src/hooks/chat-composer-core.ts Outdated
Comment thread packages/shared/src/hooks/chat-composer-core.ts Outdated

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

Two dead exports remain in the new shared core after the reducer cleanup from the previous round.

CHAT_HABIT_ACTION_TYPES (line 33) and AGENT_INVALIDATION_KEY_FAMILIES (line 78) in packages/shared/src/hooks/chat-composer-core.ts are both exported but have zero callers outside the file that defines them. Both violate CLAUDE.md rule 2 (no 'just in case' exports). Fix is a one-character drop of the export keyword on each — callers go through selectActionInvalidations and invalidateAgentQueries respectively, which remain exported.

Everything else in this PR is solid: the reducer dead-code from the prior round is fully removed, the shared core is pure TS with no React/RN imports, both platforms consume the shared functions, tests cover the core logic and the mobile hook, and the chat screen slimming is clean.

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

Both flagged rounds are fully resolved. The latest commit correctly drops the remaining internal-only export keywords from chat-composer-core.ts (CHAT_HABIT_ACTION_TYPES, AGENT_INVALIDATION_KEY_FAMILIES, SendFailureKind, SendFailureInput, SendFailureClassification, ActionInvalidations), so the file now exports exactly what external callers use: buildAgentExecutionMessage, classifySendFailure, findPremiumPolicyDenial, invalidateAgentQueries, selectActionInvalidations, CHAT_GOAL_ACTION_TYPES, and CHAT_DRAFT_STORAGE_KEY. The shared core is clean pure-TS with no React/RN imports, both thin wrappers consume it correctly, the chat screen slim-down is clean, and the test suite (shared core unit + mobile hook integration) gives solid coverage of the extracted logic. Ready to merge.

Base automatically changed from chore/fix-mobile-type-check to main June 5, 2026 00:05
thomasluizon and others added 3 commits June 4, 2026 21:06
Lift the chat-composer logic into a framework-agnostic core in
packages/shared so any chat change lands once and both apps inherit it.

- Add packages/shared/src/hooks/chat-composer-core.ts: pure TS (no react /
  react-native at runtime) owning the de-duped action-type sets,
  buildAgentExecutionMessage (web-rich, keeps policyReason),
  classifySendFailure (408/limit/upgrade/generic), selectActionInvalidations,
  findPremiumPolicyDenial, the agent-query invalidation set, and the composer
  reducer. Exported via the index + new "./hooks" package subpath.
- Rewrite apps/web/hooks/use-chat-composer.ts as a thin wrapper injecting the
  chat Server Actions; web chat page now consumes pre-shaped step-up adapters.
- Add apps/mobile/hooks/use-chat-composer.ts (thin apiClient wrapper) +
  use-chat-reward.ts (mobile-only rewarded-ad flow).
- Slim apps/mobile/app/chat.tsx from 1544 to 286 lines: extract createStyles,
  the 3 animation components, ChatComposerInput, ChatInputBar, ChatInputArea,
  and ChatEmptyState to siblings.
- Normalize divergences to web's richer behavior: mobile gains policyReason and
  the timeout/limit error branches; both platforms use the shared
  CHAT_GOAL_ACTION_TYPES.

Tests: shared core unit test + mobile hook test added; all suites green.

Closes #85

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@thomasluizon
thomasluizon merged commit 2b8ca4c into main Jun 5, 2026
6 checks passed
@thomasluizon
thomasluizon deleted the issue-85 branch June 5, 2026 00:07

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

Three rounds of review all resolved. The shared core is clean: CHAT_HABIT_ACTION_TYPES, AGENT_INVALIDATION_KEY_FAMILIES, and the internal discriminated-union types (SendFailureKind, SendFailureInput, SendFailureClassification, ActionInvalidations) are all module-private — no export leakage. The five public exports (buildAgentExecutionMessage, classifySendFailure, findPremiumPolicyDenial, invalidateAgentQueries, selectActionInvalidations) plus CHAT_GOAL_ACTION_TYPES and CHAT_DRAFT_STORAGE_KEY are all load-bearing in both platform hooks. The shared core is pure TypeScript with zero React/RN imports, both thin wrappers consume it correctly with the divergence normalization (policyReason, 408/limit error branches) landing on both platforms, the chat screen slim-down from 1544 → 286 lines is clean, and the test suite gives solid coverage of the extracted logic.

@sonarqubecloud

sonarqubecloud Bot commented Jun 5, 2026

Copy link
Copy Markdown

Quality Gate Failed Quality Gate failed

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

See analysis details on SonarQube Cloud

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.

Extract chat composer to packages/shared, both apps consume

1 participant