feat: import habits from another app via Astra (#10) - #331
Conversation
Let new (or existing) users bring habits over from another app, with Astra doing the parsing. No custom parser, no backend change. Onboarding entry: an "Import from another app" affordance on the Meet Astra step (both platforms) writes a prefilled prompt to the chat draft, completes onboarding, and routes into the Astra chat. Chat composer text-file attachment (.csv/.json/.txt/.md): the file is read client-side and folded into the outgoing message as plain chat text so the existing AI pipeline parses it. Mirrors the existing image-attachment UX (attach button + removable chip) but rides as text rather than an upload. - Web: hidden file input + File.text(). - Mobile: expo-document-picker + expo-file-system File.text(). Shared @orbit/shared/chat gains getChatTextFileValidationError (extension + 1 MB size gate) and buildChatMessageWithFileContent (identical framing across platforms), mirroring the image-attachment seam. Size/format failures surface i18n errors. New keys added to en.json and pt-BR.json. Tests: shared validation/compose + i18n parity, web composer/hook/onboarding, mobile composer hook/input-bar. Adds expo-document-picker (~55.0.12, Expo SDK 55 pinned) to apps/mobile. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
There was a problem hiding this comment.
Code Review: PR #331
Scope: PR #331 — feat: import habits from another app via Astra (#10)
Recommendation: APPROVE
Summary
PR #331 adds two entry points for importing habits: (1) an "Import from another app" pill on the onboarding Meet-Astra step that pre-fills a chat draft and routes into Astra, and (2) a text-file attachment button (Paperclip) in the Astra chat composer that reads .csv/.json/.txt/.md files client-side and folds their content into the outgoing message. No backend changes. Both entry points are implemented symmetrically across web and mobile. Validation, i18n, and tests are complete. No Critical or High findings survive adversarial review.
Findings
Critical
None.
High
None.
Medium
None.
Low / Info
[Info] JSDoc WHY note without a URL link
- dimension: Comment policy (#4)
- location:
packages/shared/src/chat/index.ts—getChatTextFileValidationErrorJSDoc - issue: The JSDoc body explains "The gate is extension-based because picker/browser MIME types are unreliable for .csv/.md" — reads as a WHY note but has no http(s):// URL to an upstream source.
- risk: None — zero user impact. Linter may or may not flag it depending on whether it scans JSDoc bodies.
- fix: Remove the explanatory sentence, or add a reference URL (MDN / caniuse). If CI lint passes, no action needed.
- reference: CLAUDE.md rule 5
[Info] Harmlessly redundant globalThis.localStorage guard
- dimension: No-workaround (#5)
- location:
apps/web/components/onboarding/onboarding-flow.tsx:124 - issue:
OnboardingFlowis'use client'andhandleImportonly fires from a button press (never SSR), solocalStorageis always defined. The guard is over-defensive. - risk: None.
- fix: Could use
localStorage.setItem(...)directly, matchingtrial-expired-modal.tsx. Or leave it — matchesgoal-detail-drawer.tsxandhabit-detail-drawer.tsx. - reference: CLAUDE.md rule 1
Subagents
| Agent | Verdict |
|---|---|
| parity-checker | PAIRED |
| i18n-syncer | IN SYNC |
| contract-aligner | MATCH |
| security-reviewer | N/A — no orbit-api changes |
Validation
| Check | Result |
|---|---|
| Lint | N/A — sandbox-blocked (PR body: 0 errors) |
| Type check | N/A — sandbox-blocked (PR body: 3/3 clean) |
| Tests | N/A — sandbox-blocked (PR body: shared 1037, web 1663, mobile 639 passed) |
| Build (api) | N/A — no backend changes |
Deferred
- Dimension 12 Security (API side): no orbit-api changes; text-file content sent over the same authenticated channel as any user message; prompt injection is user-self-scoped only.
- Dimension 13 Backend hard rules: gated out — no orbit-api changes.
- Dimension 11 Backward-compat guard: no Zod schema fields added, removed, or renamed — only new utility exports from
packages/shared/src/chat/index.ts. package-lock.json: onlyexpo-document-picker@55.0.14added (SDK 55, MIT).
What's good
- Shared
getChatTextFileValidationError+buildChatMessageWithFileContentextract topackages/sharedat the right DRY level — both platforms validate identically and frame the Astra message identically. - Size-guard design (
typeof candidate.fileSize === 'number'guard) is correct and intentional — consistent with the existing image-validation pattern;copyToCacheDirectory: trueensuresasset.sizeis always populated by expo-document-picker. - Onboarding import is resilient: best-effort
completeOnboarding()with empty catch mirrors the pre-existinghandleFinish— user is never blocked by a transient API failure. - Test coverage is thorough: happy path + wrong-type error + onboarding import path with router assertion on both platforms.
- All new UI uses semantic tokens only (
var(--bg-elev),var(--hairline),var(--primary), etc.) — no raw slate values, no hardcoded violet rgba. transition-[background-color,transform]on the new remove button is correct per DESIGN.md (transition-allis banned; named transitions are fine).
Recommendation
Clean implementation. All candidate Critical/High findings were refuted in the adversarial pass. Two Info observations are non-blocking. Ready to merge.
# Conflicts: # apps/mobile/components/onboarding/onboarding-meet-astra.tsx # apps/mobile/test-mocks/lucide-react-native.ts # apps/web/__tests__/components/onboarding/onboarding-flow.test.tsx # apps/web/components/onboarding/onboarding-meet-astra.tsx
|
There was a problem hiding this comment.
No Critical or High findings. APPROVE. Severity: Critical=0, High=0, Medium=0, Low/Info=1 (non-blocking). Adds file-attachment button in Astra chat composer and Import-from-another-app pill on Meet Astra onboarding. Both platforms covered, i18n synced, shared validation in packages/shared, no API contract modified. One info finding: mobile inlines text-file attachment logic that web extracts into a dedicated hook — correct and tested, non-blocking follow-up. Parity: PAIRED. i18n: IN SYNC (8 new keys in both locales). Contract: MATCH. Backward compat: PASS. Security: PASS. orbit-api: N/A. Validation skipped (separate CI). PR body reports type-check clean, lint 0 errors, all tests passing.
) The canvas run is complete. The screens project holds 20 documents and 21 pages, all new canon, and no document survives from the first run. Screens built: Hoje, habit create, habit detail, Calendario, Progresso, the Astra conversation, onboarding, auth, Perfil, notifications, the Pro pitch, the subscription manage view, the surviving celebration, the error and static surfaces, offline, search, step up, the overlay primitives, Wrapped, the four static screens, and the Android widget. Three design system rounds ran alongside the screens, each from gaps a build reported rather than from a guess: - D the four defects found on the first Hoje: icon centring, emoji used as iconography, frozen and skip as habit statuses, and a hand drawn mark - D2 DayStrip, Checkbox, a shell header slot, TimeField, StatTile states, Menu - D3 BlockFrame's control, proposed and irreversible rows plus a risk slot, Composer's busy state and its discriminated atLimit, and an authorable conversation slot Two of those are enforced by contract rather than by prose, because the same bug came back twice under different disguises. DayStrip discriminates on scope, so passing frozen to a habit strip is a type error. Composer discriminates on state, so atLimit cannot render without its message and no other state can pass one. Six open questions were put to Thomas and answered: a rejected preview collapses to one line, step up is a hand off, a partially failed bulk create keeps what it created, the day arc is the exact fraction, Calendario pages on an empty account, and the month rate has a definition. DESIGN.md gains that definition, because Calendario computed it and correctly flagged that no endpoint states the window, which means the next surface would compute a different number from the same data. Four API tickets were filed from what the screens reported they needed: #331 the streak repair endpoint, #332 the achievements payload, #333 the Astra metrics schema, #334 the notification urls. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>



Import habits from another app (via Astra)
Closes #10. A new (or existing) user can bring habits over from another app, with Astra doing the parsing — no custom import parser, no backend change.
Two entry points
.csv/.json/.txt/.md, ≤1 MB): the file is read client-side and its contents fold into the outgoing message, so the existing Astra pipeline parses it and creates the habits. Mirrors the existing image-attachment UX, but routes through the message text (no upload).Per platform
<input type="file">→File.text().expo-document-picker→expo-file-systemFile(uri).text(). New dep:expo-document-picker@~55.0.12(SDK 55 bundled; autolinks on next build).packages/shared/src/chat):getChatTextFileValidationError(extension + size) +buildChatMessageWithFileContent(identical framing on both platforms).i18n
New keys in both
en.json+pt-BR.json(parity test green).Validation
type-check 3/3 clean; lint 0 errors; tests — shared 1037, web 1663, mobile 639 passed.
🤖 Generated with Claude Code