Imported from nardo-kong/credit-card-manager (
AGENTS.md). Install upstream withnpx skills add nardo-kong/credit-card-manager. Copyright stays with the author.
Credit Card Manager - Agent Guidelines
Project Overview
- Path:
C:\Users\narjiang\Git\NewProcess\CreditNote\credit-card-manager - Tech Stack: Vue 3 + TypeScript + Tauri + Tailwind CSS + shadcn-vue
- Package Manager: corepack pnpm
- Dev Command:
corepack pnpm tauri dev - Frontend URL: http://localhost:1420/
Critical Bug Patterns & Solutions
Pattern 1: Dialog Button Unclickable / UI Freeze
Root Cause: z-index stacking context conflict between DialogOverlay and DialogContent
Symptoms:
- Dialog opens but buttons cannot be clicked
- Navigation stops working after opening certain pages
- UI appears frozen but no JavaScript errors
Analysis Steps:
- Check DialogOverlay.vue - it uses
fixed inset-0 isolate z-50 - Check DialogContent.vue - it also uses
fixedwithz-50 - The
isolateproperty creates a new stacking context, causing z-index conflicts - Both elements have same z-index (50), but overlay renders first in DOM
Solution:
- DialogContent must have higher z-index than DialogOverlay
- Or: Remove
isolatefrom overlay, or addpointer-events-noneto overlay andpointer-events-autoto content - Ensure DialogPortal content is properly layered above overlay
Files to Check:
src/components/ui/dialog/DialogOverlay.vuesrc/components/ui/dialog/DialogContent.vuesrc/components/ui/dialog/DialogScrollContent.vue
Pattern 2: Page Navigation Freeze
Root Cause: Dialog overlay persists and blocks all interactions
Symptoms:
- After visiting a page with Dialog, other tabs stop working
- Sidebar navigation buttons unresponsive
- Page content doesn't update on route change
Analysis Steps:
- Check if Dialog state persists across route changes
- Verify DialogPortal properly cleans up on unmount
- Check if overlay has
pointer-events: allblocking everything
Solution:
- Ensure Dialog v-model:open is properly bound and resets on route change
- Add
pointer-events-noneto overlay background - Use
v-ifinstead of just hiding Dialog when not needed
Pattern 3: Select Component Infinite Loop
Root Cause: v-model binding type mismatch with reka-ui Select
Symptoms:
- Page loads but becomes unresponsive
- High CPU usage
- Vue devtools shows infinite update cycle
Analysis Steps:
- Check Select component value binding type
- Verify SelectItem value props match the v-model type
- Check for computed properties that depend on the select value
Solution:
- Ensure v-model value matches SelectItem value type exactly
- Avoid computed properties that update the same reactive state they depend on
- Use
shallowRefinstead ofreffor complex objects if needed
Pattern 4: Select Component Not Responding to Selection
Root Cause: Multiple form fields bound to same v-model property, causing state conflicts
Symptoms:
- Select dropdown opens but selection doesn't register
- One field updates unexpectedly when another field changes
- Form data appears corrupted or inconsistent
Analysis Steps:
- Check if multiple inputs share the same v-model binding
- Verify each form field has its own unique reactive property
- Check for derived/computed values that should be separate from raw input
Solution:
- Use separate reactive properties for each form field
- Use
@update:model-valueevent to sync related fields explicitly - Example: Product select should bind to
productfield, then updatenamefield via event handler
Files to Check:
src/views/Cards.vue- Add/Edit Dialog form bindings- Any form with cascading selects (bank → product → name)
Pattern 5: Select Disabled State Not Working
Root Cause: Custom Select wrapper doesn't forward disabled prop to reka-ui SelectRoot
Symptoms:
:disabled="condition"has no effect on Select component- Select remains clickable even when it should be disabled
- No visual indication of disabled state
Analysis Steps:
- Check Select.vue wrapper component props definition
- Verify
disabledprop is included and forwarded to SelectRoot - Check if SelectTrigger receives disabled state via context
Solution:
- Add
disabled?: booleanto Select.vue props interface - Ensure prop is passed through
useForwardPropsEmits - SelectTrigger already supports disabled styling via CSS
Files to Check:
src/components/ui/select/Select.vuesrc/components/ui/select/SelectTrigger.vue
Pattern 6: Color Picker Buttons Not Working in Dialog
Root Cause: Event bubbling or Dialog event interception prevents click handlers
Symptoms:
- Color buttons appear clickable but don't update selection
- Visual state (border, scale) doesn't change on click
- Other buttons in same Dialog work fine
Analysis Steps:
- Check if click event is being captured by parent elements
- Verify
@clickhandler is properly bound - Check for event modifiers that might prevent propagation
Solution:
- Add
.stopmodifier to prevent event bubbling:@click.stop="handler" - Ensure button elements have proper
type="button"to prevent form submission - Check if Dialog or overlay has pointer-events that block clicks
Files to Check:
src/views/Cards.vue- Color picker button bindingssrc/components/ui/dialog/DialogContent.vue- Event handling
Pattern 8: SQL LIMIT with Positional ?2 Bug Silently Empties List
Root Cause: rusqlite positional placeholder mismatch — SQL references ?2 (needs 2 args) but only 1 arg passed
Symptoms:
transaction_list()(no card_id) returns empty / "暂无交易" even though DB has rows- Manual add succeeds (navigates, writes to DB) but list never shows anything
- Same query works when a filter arg is present (card_id branch), fails without it
Analysis Steps:
- The
card_id.is_some()branch usesWHERE card_id = ?1 ... LIMIT ?2withparams — works - The
elsebranch reusesLIMIT ?2but passes only[limit_val](1 arg) → rusqlite errorsWrong number of parameters passed to query. Got 1, needed 2 - Error is swallowed: frontend
fetchTransactionscatches it, setserror.value, leavestransactionsempty → "暂无交易"
Solution:
- When the SQL omits the first positional arg, renumber the remaining placeholders: use
LIMIT ?1in the no-filter branch - After editing, ALWAYS test the actual command's SQL via a standalone rusqlite example (linking the tauri lib fails with 0xC0000139 DLL error; use
dirs::data_dir()path +rusqlitedirectly)
Files to Check:
src-tauri/src/commands/transaction.rs-transaction_listSQL branches
Pattern 7: Frontend/Backend Field Contract Mismatch (camelCase vs snake_case)
Root Cause: Rust structs serialized/deserialized with snake_case while frontend TS types use camelCase
Symptoms:
transaction_listreturns records but UI shows empty list / fields areundefined- Manual add (记一笔) "does nothing" when saving (create silently fails to deserialize)
- AI-imported transactions confirmed as saved but never visible anywhere
Analysis Steps:
- Check Rust struct in
src-tauri/src/models/mod.rsfor#[serde(rename_all = "camelCase")] - Check frontend TS types in
src/types/index.tsfor field names (cardId,amount,date,currency) - Verify Tauri command parameter names match (NOT the nested struct fields): Tauri auto-converts Rust
snake_casearg names to JScamelCase(e.g. Rustcard_id→ JS{ cardId }), so top-level invoke keys must be camelCase, but nested struct fields must be handled by serderename_all - Check the store invoke call:
invoke('transaction_create', { transaction: data })— the key must match the Rust parameter name (converted to camelCase), NOT a random name likedata
Solution:
- Add
#[serde(rename_all = "camelCase")]to the Rust struct AND explicit#[serde(rename = "...")]for special renames (transaction_date→date,converted_amount→amount,original_currency→currency) - Add
#[serde(default)]on struct fields the frontend does NOT send (e.g.original_amount) - Fix store invoke key to match the Tauri command's parameter name (camelCase)
Files to Check:
src-tauri/src/models/mod.rs- struct serde attrssrc/stores/transactions.ts- invoke arg names ({ transaction: data },{ id, transaction: data })src/stores/ai.ts,src/types/index.ts- AI request/response types
Agent Work Rules (Mandatory)
Rule 1: Think Before Acting
After reading 5-10 files, you MUST pause and output a structured analysis before making any changes.
This prevents:
- Endless file reading loops without progress
- Blind command execution without understanding
- Repeated attempts that don't solve the root cause
Required Analysis Format:
## Current Understanding
[Summarize what you've learned from the files read]
## Root Cause Hypothesis
[Your best guess at what's causing the issue]
## Evidence
[Specific code snippets or patterns that support your hypothesis]
## Next Steps
[What you need to verify or fix, and why]
File Reading Thresholds:
- Minimum: After reading 5 files, you MUST output analysis
- Maximum: Do not read more than 10 files without outputting analysis
- Exception: If you've already identified the issue in <5 files, you may proceed directly to fix
Rule 2: No Blind Fixes
- Do not apply fixes based on assumptions
- Do not copy-paste solutions without understanding the context
- If unsure, ask for clarification or output your analysis for review
Rule 3: Verify Before Declaring Success
- Always run
corepack pnpm buildafter making changes - If build fails, analyze the error and fix it (don't just retry randomly)
- Test the specific scenario that was reported broken
Rule 4: Document Patterns
- When you discover a new bug pattern, add it to this AGENTS.md file
- Include: root cause, symptoms, analysis steps, solution, files to check
- This prevents future agents from repeating the same debugging process
Debugging Workflow
- STOP - Do not immediately rewrite components
- Analyze - Check browser devtools for:
- Console errors
- Vue component tree state
- Computed properties re-evaluation
- Event listeners on frozen elements
- Identify - Determine if it's:
- z-index/stacking issue (elements visually present but not clickable)
- Infinite reactivity loop (high CPU, Vue warnings)
- Event capture/bubbling issue (events not reaching targets)
- Fix - Apply targeted fix based on root cause
- Verify - Test the specific scenario that was broken
Hong Kong Localization
- Default currency: HKD (changed from CNY)
- Banks: 25 Hong Kong banks including virtual banks
- Credit card products: 50+ HK credit cards with features
- All formatCurrency calls should default to 'HKD'
Commands
- Build:
corepack pnpm build - Dev:
corepack pnpm tauri dev - Lint: Check package.json for lint script
AI Statement Parsing (PDF / Image) Notes
PDF Extraction (src-tauri/src/commands/ai.rs)
- PDFs arrive as base64 (frontend reads PDF via ArrayBuffer→btoa; readAsText would produce binary garbage).
extract_pdf_textusespdf_oxide, NOTpdf-extractand NOT a hand-rolledlopdfscanner.pdf-extract 0.7.12silently returns empty text on encrypted (V=1/R=2) bank PDFs — its decrypt errors are swallowed.- A manual lopdf content-stream scanner had to emulate
BT…ETgating +Tmcoordinates, and inline-image binary (BI…EI, 1447 blocks in the Hang Seng statement) mis-matchedBT/ETcounts (BT=15 ET=20, BT=60 ET=58) so binary payload was pulled into text items → 524K chars / 9094 lines output → 8 chunks → dttInternal Server Erroron chunk 8. Token consumption unacceptable.
- Flow: decode base64 →
PdfDocument::from_bytes→ ifis_encrypted()thenauthenticate(b"")(most bank PDFs use the standard security handler with an empty user password; this unlocks them). IfauthenticatereturnsOk(false)/Err→ return the "该 PDF 已加密且无法自动解密…请…粘贴上传" error. doc.extract_chars(page)returns character-levelTextChar(.char+.bbox{x,y,width,height}). pdf_oxide's ownextract_text_lines/extract_textDROP transaction rows on some PDFs (it lost the whole transaction table + returned 0 chars on the encrypted last page), so we rebuild layout ourselves.rebuild_chars_to_lines: sort chars y-desc (top of page first) then x-asc; group into rows withinROW_TOL=1.5baseline y; within a row split cells by x gap:< WORD_GAP(2.0)= same word,< COL_GAP(25.0)= space,>= COL_GAP= new column (tab). Emit rows as tab-separated cells. This restores the statement table (dates / description / country / amount / FX amount / FX rate all on one line) and keeps total output small.- Result on the Hang Seng test PDF: ~15K chars / 256 lines vs the old 524K — fits a single dtt chunk (well under the 60K
MAX_CONTENT_CHARSthreshold). - Regression test:
cargo run --example pdfverify -- <file.pdf>insrc-tauri(self-contained mirror of the extractor; does NOT link the tauri lib because that pulls WebView2 DLLs and the exe fails with 0xC0000135/0xC0000139).
Image Upload
- Images are gated by the
supports_visionsetting (ai_supports_visionin settings table, toggle in Import.vue AI settings). When false, uploading an image returns a clear Chinese error instead of a 500. - GLM-5.2 (dtt) does NOT support image input;
supports_visionis provided for future vision-capable models.
Known dtt API behaviors
- File-upload endpoint returns SSE (
text/event-stream), handled byextract_sse_content. - "Stream error event: ...Internal Server Error" from dtt usually means we sent content in a format the model can't parse (e.g. binary garbage for PDF, or a raw base64 image as text). Fix the input format, not the SSE parser.