Compare commits
152 Commits
v0.2026032
...
v0.2026033
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
36febb75da | ||
|
|
8a923a95cf | ||
|
|
c0fed9c5c0 | ||
|
|
4d787d6f84 | ||
|
|
3bc824940a | ||
|
|
cebeca678f | ||
|
|
dd59ee072d | ||
|
|
828d5f84a9 | ||
|
|
459ee8c7e3 | ||
|
|
62e1dbb173 | ||
|
|
f15dc0e516 | ||
|
|
491e5d3962 | ||
|
|
1199840fdc | ||
|
|
39db25a39a | ||
|
|
213e51c135 | ||
|
|
9a253392e5 | ||
|
|
c4001ec3f6 | ||
|
|
e3e60a2815 | ||
|
|
e43e2a7549 | ||
|
|
517f1c04f5 | ||
|
|
635d793d32 | ||
|
|
093f1bc9dc | ||
|
|
7dc7897367 | ||
|
|
46a08c6f43 | ||
|
|
eb7a45adf9 | ||
|
|
e89dc65c22 | ||
|
|
ce4736b619 | ||
|
|
7d94bb26bb | ||
|
|
b78e42272e | ||
|
|
4d0e7469b9 | ||
|
|
c29206da3b | ||
|
|
6764fd04a1 | ||
|
|
59ed6897c1 | ||
|
|
9b59c269d8 | ||
|
|
ff1f166ca6 | ||
|
|
289ab82ed1 | ||
|
|
94da70ba30 | ||
|
|
bd130171df | ||
|
|
2045e13404 | ||
|
|
d83121bc83 | ||
|
|
acfceb3335 | ||
|
|
2dd6a94ef8 | ||
|
|
296d474732 | ||
|
|
98a98ab024 | ||
|
|
2b85640521 | ||
|
|
c3b397f900 | ||
|
|
6e0b578590 | ||
|
|
860efc1f42 | ||
|
|
d05bc271a8 | ||
|
|
7f0134a99c | ||
|
|
8fbf035d46 | ||
|
|
859795879c | ||
|
|
0ee4862508 | ||
|
|
e1def7f8bb | ||
|
|
e697b4b5e5 | ||
|
|
6b0bb5173c | ||
|
|
81f986a065 | ||
|
|
564ca50206 | ||
|
|
6d405a763d | ||
|
|
7c9bc3d640 | ||
|
|
d316539a91 | ||
|
|
0f22475c20 | ||
|
|
797c7b66b6 | ||
|
|
af7d79fe44 | ||
|
|
14b5c34b94 | ||
|
|
a7a61d9751 | ||
|
|
68066b857f | ||
|
|
858468aec6 | ||
|
|
67ac8db888 | ||
|
|
1ae1377b2d | ||
|
|
adfceb3c70 | ||
|
|
46856b4dc2 | ||
|
|
2746fb88ad | ||
|
|
52d66048d6 | ||
|
|
1a90679f62 | ||
|
|
59773725e1 | ||
|
|
d9254ffaf5 | ||
|
|
85b545a0bc | ||
|
|
e46b9ecb1b | ||
|
|
0488a3c505 | ||
|
|
a59640634e | ||
|
|
89f53a1214 | ||
|
|
3749770598 | ||
|
|
d7f18f79c1 | ||
|
|
f80339a0ed | ||
|
|
93d5d582ef | ||
|
|
1ba74d4b1d | ||
|
|
5c0e7b2987 | ||
|
|
bcd4b8fd99 | ||
|
|
2dd68c170e | ||
|
|
4eca4cb545 | ||
|
|
1ebd5163f1 | ||
|
|
f61fc57aa6 | ||
|
|
e2788d65b5 | ||
|
|
42b76d85d0 | ||
|
|
dc57984600 | ||
|
|
bb1eebbd6f | ||
|
|
341590cfd9 | ||
|
|
bc08345021 | ||
|
|
99ae8260cb | ||
|
|
c9c8380d6c | ||
|
|
80313376f8 | ||
|
|
b36b45057b | ||
|
|
f1e0afb715 | ||
|
|
5464da9c6e | ||
|
|
41edd75837 | ||
|
|
c4960e8ee7 | ||
|
|
c13a5fe3b0 | ||
|
|
0ded9ee871 | ||
|
|
cf2bc61ce5 | ||
|
|
f7f669774e | ||
|
|
af9d858bb3 | ||
|
|
31d85a0223 | ||
|
|
3cb55fe752 | ||
|
|
91854f8bae | ||
|
|
a05a9339c1 | ||
|
|
f47557ccb5 | ||
|
|
e96f266c2b | ||
|
|
181c9fe114 | ||
|
|
b2e99d2da9 | ||
|
|
df7761b759 | ||
|
|
94112ffcd8 | ||
|
|
ec6d490025 | ||
|
|
d108aa01e8 | ||
|
|
259d5b6489 | ||
|
|
86a9f65f88 | ||
|
|
660208a6fd | ||
|
|
6665d646fb | ||
|
|
1eecae0add | ||
|
|
5c7693902d | ||
|
|
47b5e45696 | ||
|
|
1de22b04b8 | ||
|
|
2be961c53c | ||
|
|
198ea1fcc9 | ||
|
|
1b547d4191 | ||
|
|
59ca7a7b41 | ||
|
|
af147c4cf0 | ||
|
|
97126c8a0e | ||
|
|
c4136d69b4 | ||
|
|
8b723b36d9 | ||
|
|
e27b29eec9 | ||
|
|
8207ee4569 | ||
|
|
2fdc122d73 | ||
|
|
60d3b48ea6 | ||
|
|
ecbb94ae83 | ||
|
|
50ad7e0e8c | ||
|
|
ea8d847d46 | ||
|
|
845181d002 | ||
|
|
35c62583d9 | ||
|
|
a6b2454184 | ||
|
|
a74f76fdf1 | ||
|
|
c6fa1f48cb |
133
.claude/commands/create-adr.md
Normal file
@@ -0,0 +1,133 @@
|
||||
# Create Architecture Decision Record
|
||||
|
||||
Use this command when you need to document an architectural decision made during a task.
|
||||
|
||||
Inspired by [adr-tools](https://github.com/npryce/adr-tools) (Nygard format), adapted for Laputa's frontmatter-based note format.
|
||||
|
||||
## When to use this
|
||||
|
||||
Create an ADR when your work involves any of these:
|
||||
- Choosing a storage strategy (vault vs app settings vs database)
|
||||
- Adding or removing a major dependency
|
||||
- Supporting a new platform or target
|
||||
- Introducing or removing a core abstraction
|
||||
- Making a cross-cutting decision that affects how future code should be written
|
||||
|
||||
Do NOT create ADRs for: bug fixes, UI styling, refactors that preserve behavior, or test additions.
|
||||
|
||||
## Creating a new ADR
|
||||
|
||||
### 1. Find the next ID
|
||||
|
||||
```bash
|
||||
ls docs/adr/*.md | grep -oP '\d{4}' | sort -n | tail -1 | xargs -I{} printf '%04d\n' $(({} + 1))
|
||||
```
|
||||
|
||||
If no files exist yet, start at `0001`.
|
||||
|
||||
### 2. Create the file
|
||||
|
||||
Filename: `docs/adr/NNNN-short-kebab-title.md`
|
||||
|
||||
Template:
|
||||
|
||||
```markdown
|
||||
---
|
||||
type: ADR
|
||||
id: "NNNN"
|
||||
title: "Short decision title"
|
||||
status: active
|
||||
date: YYYY-MM-DD
|
||||
---
|
||||
|
||||
## Context
|
||||
|
||||
The issue motivating this decision, and any context that influences or constrains it.
|
||||
|
||||
## Decision
|
||||
|
||||
**The change we're proposing or have agreed to implement.** State it clearly in one or two sentences — bold so it stands out.
|
||||
|
||||
## Options considered
|
||||
|
||||
- **Option A** (chosen): brief description — pros / cons
|
||||
- **Option B**: brief description — pros / cons
|
||||
- **Option C**: brief description — pros / cons
|
||||
|
||||
## Consequences
|
||||
|
||||
What becomes easier or harder as a result?
|
||||
What risks does this introduce that will need to be mitigated?
|
||||
What would trigger re-evaluation of this decision?
|
||||
|
||||
## Advice
|
||||
|
||||
*(optional)* Input received before making this decision — who was consulted, what they said.
|
||||
Omit this section if the decision was made without external input.
|
||||
```
|
||||
|
||||
### 3. Update the index
|
||||
|
||||
Add a row to `docs/adr/README.md`:
|
||||
|
||||
```markdown
|
||||
| [NNNN](NNNN-short-kebab-title.md) | Title | active |
|
||||
```
|
||||
|
||||
### 4. Include in the same commit as the feature
|
||||
|
||||
```bash
|
||||
git add docs/adr/NNNN-*.md docs/adr/README.md
|
||||
# fold into the feature commit — do not create a separate commit just for the ADR
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Superseding an existing ADR
|
||||
|
||||
Equivalent of `adr new -s <N>` from adr-tools — do this in two steps:
|
||||
|
||||
### Step 1: Mark the old ADR as superseded
|
||||
|
||||
Edit the existing file — add `superseded_by` and update `status`:
|
||||
|
||||
```yaml
|
||||
---
|
||||
type: ADR
|
||||
id: "000N"
|
||||
title: "Old decision title"
|
||||
status: superseded # ← change from active
|
||||
superseded_by: "NNNN" # ← add this
|
||||
date: YYYY-MM-DD
|
||||
---
|
||||
```
|
||||
|
||||
**Never edit the content sections** of an active ADR — only the status metadata.
|
||||
|
||||
### Step 2: Create the new ADR
|
||||
|
||||
Follow the steps above. In the **Context** section, reference the superseded ADR:
|
||||
|
||||
```markdown
|
||||
## Context
|
||||
|
||||
Supersedes [ADR-000N](000N-old-title.md).
|
||||
|
||||
[explain why the old decision no longer holds]
|
||||
```
|
||||
|
||||
### Step 3: Update the README index
|
||||
|
||||
Change the old row's status to `superseded`, add the new row.
|
||||
|
||||
---
|
||||
|
||||
## Best practices (from adr-tools / Nygard)
|
||||
|
||||
- **One decision per ADR** — if you find yourself writing "and also", split it
|
||||
- **Write Decision first** — if you can't state it in 1-2 sentences, the decision is too vague
|
||||
- **Context is the "why now"** — what forced this decision to be made today?
|
||||
- **Consequences should include negatives** — a one-sided ADR is a red flag
|
||||
- **Committed = immutable** — once pushed, the content doesn't change; only status metadata does
|
||||
- **If in doubt, create one** — cheaper to have an unnecessary ADR than to lose context
|
||||
- Date = today's date, `YYYY-MM-DD`
|
||||
50
.claude/commands/laputa-done.md
Normal file
@@ -0,0 +1,50 @@
|
||||
# /laputa-done <task_id>
|
||||
|
||||
Mark a Laputa task as done: add completion comment, move to In Review, notify Brian, then self-dispatch the next task.
|
||||
|
||||
Run this after Phase 1 (Playwright) and Phase 2 (native app QA) both pass **and `git push origin main` has succeeded**.
|
||||
|
||||
⚠️ A task is NOT done until the push succeeds. If the push is blocked by the pre-push hook (clippy, tests, CodeScene, build):
|
||||
- Read the error
|
||||
- Fix it (never use `--no-verify`)
|
||||
- Commit the fix and push again
|
||||
- Repeat until push exits with code 0
|
||||
|
||||
## Steps
|
||||
|
||||
**1. Add completion comment to the task**
|
||||
|
||||
Summarize what was done — this is the context Luca and Brian will read in Todoist:
|
||||
|
||||
```bash
|
||||
curl -s -X POST "https://api.todoist.com/api/v1/comments" \
|
||||
-H "Authorization: Bearer $TODOIST_API_KEY" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"task_id": "$ARGUMENTS",
|
||||
"content": "✅ Implementation complete.\n\n**What changed:** [brief summary of the implementation]\n**ADR:** [if an ADR was created, reference it here; otherwise omit]\n**Playwright:** all tests pass\n**Native QA:** tested with pnpm tauri dev — [describe what was tested and what was observed]"
|
||||
}'
|
||||
```
|
||||
|
||||
**2. Move task to In Review**
|
||||
|
||||
```bash
|
||||
curl -s -X POST "https://api.todoist.com/api/v1/tasks/$ARGUMENTS/move" \
|
||||
-H "Authorization: Bearer $TODOIST_API_KEY" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"section_id": "6g3XjX33FF4Vj86M"}'
|
||||
```
|
||||
|
||||
**3. Notify Luca (informational only — no action needed from him)**
|
||||
|
||||
```bash
|
||||
openclaw system event --text "laputa-task-done:$ARGUMENTS" --mode now
|
||||
```
|
||||
|
||||
This is a passive notification. Luca reviews tasks in his own time. Do NOT wait for approval — proceed immediately to step 4.
|
||||
|
||||
**4. Pick the next task**
|
||||
|
||||
Run `/laputa-next-task` to get the next task and start working on it immediately.
|
||||
|
||||
If `/laputa-next-task` returns `NO_TASKS` → exit cleanly. The hourly watchdog will restart you when new tasks arrive.
|
||||
43
.claude/commands/laputa-next-task.md
Normal file
@@ -0,0 +1,43 @@
|
||||
# /laputa-next-task
|
||||
|
||||
Pick the next Laputa task from Todoist and move it to In Progress.
|
||||
|
||||
Priority order: **To Rework** first, then **Open** (sorted by Todoist priority p1→p4).
|
||||
|
||||
## Steps
|
||||
|
||||
1. Fetch tasks from To Rework (`6g6QqvR9rRpvJWvv`), then Open (`6g3XjWR832hVHhCM`)
|
||||
2. Sort each section by priority (p1 highest)
|
||||
3. Take the first available task
|
||||
4. Move it to In Progress (`6g3XjWjfmJFcGgHM`) via Todoist API:
|
||||
|
||||
```bash
|
||||
curl -s -X POST "https://api.todoist.com/api/v1/tasks/<task_id>/move" \
|
||||
-H "Authorization: Bearer $TODOIST_API_KEY" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"section_id": "6g3XjWjfmJFcGgHM"}'
|
||||
```
|
||||
|
||||
5. Add a "started" comment to the task:
|
||||
|
||||
```bash
|
||||
curl -s -X POST "https://api.todoist.com/api/v1/comments" \
|
||||
-H "Authorization: Bearer $TODOIST_API_KEY" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"task_id": "<task_id>", "content": "🚀 Starting work. [Brief description of approach or what needs to be fixed]"}'
|
||||
```
|
||||
|
||||
6. Fetch the full task details (description, comments) from Todoist:
|
||||
|
||||
```bash
|
||||
curl -s "https://api.todoist.com/api/v1/tasks/<task_id>" \
|
||||
-H "Authorization: Bearer $TODOIST_API_KEY"
|
||||
|
||||
curl -s "https://api.todoist.com/api/v1/comments?task_id=<task_id>" \
|
||||
-H "Authorization: Bearer $TODOIST_API_KEY"
|
||||
```
|
||||
|
||||
6. For To Rework tasks: read the ❌ QA failed comment — it tells you exactly what to fix
|
||||
7. Output: task ID, title, and full description so you can start working immediately
|
||||
|
||||
If no tasks are available in either section → output `NO_TASKS` and exit cleanly.
|
||||
2
.codescene-thresholds
Normal file
@@ -0,0 +1,2 @@
|
||||
HOTSPOT_THRESHOLD=9.84
|
||||
AVERAGE_THRESHOLD=9.38
|
||||
47
.github/workflows/ci.yml
vendored
@@ -80,31 +80,44 @@ jobs:
|
||||
# cargo-llvm-cov exits non-zero if line coverage drops below 85%
|
||||
# lib.rs/main.rs/menu.rs are Tauri boilerplate -- not meaningfully unit-testable.
|
||||
|
||||
# ── 3. Code Health (CodeScene — Hotspot Code Health gate) ────────────
|
||||
# The webhook integration handles per-PR delta analysis (posts review
|
||||
# comments on PRs). This step enforces a minimum floor on the
|
||||
# project-wide Hotspot Code Health score (weighted avg of the most
|
||||
# frequently edited files — the ones that matter most).
|
||||
# Current baseline: 9.53 | Aspirational target: 9.8
|
||||
- name: Hotspot Code Health gate (≥9.5)
|
||||
# ── 3. Code Health (CodeScene — Hotspot + Average Code Health gates) ──
|
||||
# Enforces minimum floors on BOTH hotspot and average code health.
|
||||
# Hotspot: weighted avg of most-edited files (9.6 current | target 9.8)
|
||||
# Average: project-wide avg across all files (9.37 current | target 9.5)
|
||||
# Both gates must pass — average catches regressions in non-hotspot files.
|
||||
- name: Code Health gates (Hotspot ≥9.5 + Average ≥9.0)
|
||||
env:
|
||||
CODESCENE_PAT: ${{ secrets.CODESCENE_PAT }}
|
||||
CODESCENE_PROJECT_ID: ${{ secrets.CODESCENE_PROJECT_ID }}
|
||||
run: |
|
||||
THRESHOLD=9.5
|
||||
SCORE=$(curl -sf \
|
||||
HOTSPOT_THRESHOLD=9.5
|
||||
AVERAGE_THRESHOLD=9.33
|
||||
API_RESPONSE=$(curl -sf \
|
||||
-H "Authorization: Bearer $CODESCENE_PAT" \
|
||||
-H "Accept: application/json" \
|
||||
"https://api.codescene.io/v2/projects/$CODESCENE_PROJECT_ID" \
|
||||
| python3 -c "import sys,json; d=json.load(sys.stdin); print(d['analysis']['hotspot_code_health']['now'])")
|
||||
echo "Hotspot Code Health: $SCORE (threshold: $THRESHOLD)"
|
||||
"https://api.codescene.io/v2/projects/$CODESCENE_PROJECT_ID")
|
||||
HOTSPOT_SCORE=$(echo "$API_RESPONSE" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d['analysis']['hotspot_code_health']['now'])")
|
||||
AVERAGE_SCORE=$(echo "$API_RESPONSE" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d['analysis']['code_health']['now'])")
|
||||
echo "Hotspot Code Health: $HOTSPOT_SCORE (threshold: $HOTSPOT_THRESHOLD)"
|
||||
echo "Average Code Health: $AVERAGE_SCORE (threshold: $AVERAGE_THRESHOLD)"
|
||||
python3 -c "
|
||||
score = float('$SCORE')
|
||||
threshold = float('$THRESHOLD')
|
||||
if score < threshold:
|
||||
print(f'❌ Hotspot Code Health {score:.2f} is below threshold {threshold}')
|
||||
hotspot = float('$HOTSPOT_SCORE')
|
||||
average = float('$AVERAGE_SCORE')
|
||||
ht = float('$HOTSPOT_THRESHOLD')
|
||||
at = float('$AVERAGE_THRESHOLD')
|
||||
failed = False
|
||||
if hotspot < ht:
|
||||
print(f'❌ Hotspot Code Health {hotspot:.2f} is below threshold {ht}')
|
||||
failed = True
|
||||
else:
|
||||
print(f'✅ Hotspot Code Health {hotspot:.2f} ≥ {ht}')
|
||||
if average < at:
|
||||
print(f'❌ Average Code Health {average:.2f} is below threshold {at}')
|
||||
failed = True
|
||||
else:
|
||||
print(f'✅ Average Code Health {average:.2f} ≥ {at}')
|
||||
if failed:
|
||||
exit(1)
|
||||
print(f'✅ Hotspot Code Health {score:.2f} ≥ {threshold}')
|
||||
"
|
||||
|
||||
# ── 4. Documentation check (warning only — does not fail build) ───────
|
||||
|
||||
286
.github/workflows/release-canary.yml
vendored
Normal file
@@ -0,0 +1,286 @@
|
||||
name: Release (Canary)
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- canary
|
||||
|
||||
concurrency:
|
||||
group: release-canary-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
# Phase 1: Compute the canary version string
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
version:
|
||||
name: Compute version
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
version: ${{ steps.ver.outputs.version }}
|
||||
tag: ${{ steps.ver.outputs.tag }}
|
||||
steps:
|
||||
- id: ver
|
||||
run: |
|
||||
VERSION="0.$(date -u +%Y%m%d).${GITHUB_RUN_NUMBER}-canary"
|
||||
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
|
||||
echo "tag=v$VERSION" >> "$GITHUB_OUTPUT"
|
||||
echo "### Canary version: \`$VERSION\`" >> "$GITHUB_STEP_SUMMARY"
|
||||
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
# Phase 2: Build each architecture in parallel
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
build:
|
||||
name: Build (${{ matrix.arch }})
|
||||
needs: version
|
||||
runs-on: macos-15
|
||||
strategy:
|
||||
fail-fast: true
|
||||
matrix:
|
||||
include:
|
||||
- arch: aarch64
|
||||
target: aarch64-apple-darwin
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: 10
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '22'
|
||||
cache: 'pnpm'
|
||||
|
||||
- name: Setup Bun (required for bundle-qmd.sh)
|
||||
uses: oven-sh/setup-bun@v2
|
||||
with:
|
||||
bun-version: latest
|
||||
|
||||
- name: Setup Rust
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
targets: ${{ matrix.target }}
|
||||
|
||||
- name: Cache Rust dependencies
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
~/.cargo/registry
|
||||
~/.cargo/git
|
||||
src-tauri/target
|
||||
key: ${{ runner.os }}-release-cargo-${{ hashFiles('src-tauri/Cargo.lock') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-release-cargo-
|
||||
|
||||
- name: Install frontend dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Set version
|
||||
run: |
|
||||
VERSION="${{ needs.version.outputs.version }}"
|
||||
jq --arg v "$VERSION" '.version = $v' src-tauri/tauri.conf.json > tmp.json && mv tmp.json src-tauri/tauri.conf.json
|
||||
sed -i '' "s/^version = \".*\"/version = \"$VERSION\"/" src-tauri/Cargo.toml
|
||||
|
||||
- name: Import Apple Developer certificate into keychain
|
||||
env:
|
||||
APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
|
||||
APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
|
||||
run: |
|
||||
CERT_PATH="$RUNNER_TEMP/apple_cert.p12"
|
||||
KEYCHAIN_PATH="$RUNNER_TEMP/laputa-signing.keychain-db"
|
||||
KEYCHAIN_PASSWORD="$(uuidgen)"
|
||||
echo "$APPLE_CERTIFICATE" | base64 --decode > "$CERT_PATH"
|
||||
security create-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"
|
||||
security set-keychain-settings -lut 21600 "$KEYCHAIN_PATH"
|
||||
security unlock-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"
|
||||
security import "$CERT_PATH" -P "$APPLE_CERTIFICATE_PASSWORD" -A -t cert -f pkcs12 -k "$KEYCHAIN_PATH"
|
||||
security list-keychain -d user -s "$KEYCHAIN_PATH"
|
||||
security set-key-partition-list -S apple-tool:,apple: -s -k "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"
|
||||
echo "KEYCHAIN_PATH=$KEYCHAIN_PATH" >> "$GITHUB_ENV"
|
||||
|
||||
- name: Build Tauri app (with signing + notarization)
|
||||
env:
|
||||
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
|
||||
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
|
||||
APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
|
||||
APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
|
||||
APPLE_SIGNING_IDENTITY: ${{ secrets.APPLE_SIGNING_IDENTITY }}
|
||||
APPLE_ID: ${{ secrets.APPLE_ID }}
|
||||
APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
|
||||
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
|
||||
run: |
|
||||
pnpm tauri build --target ${{ matrix.target }}
|
||||
|
||||
- name: Upload .dmg
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: dmg-${{ matrix.arch }}
|
||||
path: src-tauri/target/${{ matrix.target }}/release/bundle/dmg/*.dmg
|
||||
retention-days: 1
|
||||
|
||||
- name: Upload updater artifacts (.tar.gz + .sig)
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: updater-${{ matrix.arch }}
|
||||
path: |
|
||||
src-tauri/target/${{ matrix.target }}/release/bundle/macos/*.app.tar.gz
|
||||
src-tauri/target/${{ matrix.target }}/release/bundle/macos/*.app.tar.gz.sig
|
||||
retention-days: 1
|
||||
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
# Phase 3: Publish GitHub Release (prerelease)
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
release:
|
||||
name: GitHub Release (canary)
|
||||
needs: [version, build]
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Download all artifacts
|
||||
uses: actions/download-artifact@v4
|
||||
|
||||
- name: Generate release notes
|
||||
run: |
|
||||
PREV_TAG=$(git tag --sort=-version:refname | grep canary | head -n 1 || echo "")
|
||||
if [ -z "$PREV_TAG" ]; then
|
||||
NOTES=$(git log --oneline --no-merges -20)
|
||||
else
|
||||
NOTES=$(git log --oneline --no-merges "${PREV_TAG}..HEAD")
|
||||
fi
|
||||
{
|
||||
echo "## What's Changed (Canary)"
|
||||
echo ""
|
||||
echo "$NOTES" | while IFS= read -r line; do echo "- $line"; done
|
||||
echo ""
|
||||
echo "---"
|
||||
echo "**Canary build — pre-release, may be unstable**"
|
||||
echo ""
|
||||
echo "**Requires Apple Silicon (M1/M2/M3)**"
|
||||
echo ""
|
||||
echo "*Built from \`$(git rev-parse --short HEAD)\` on $(date -u +%Y-%m-%d)*"
|
||||
} > release_notes.md
|
||||
|
||||
- name: Build latest-canary.json
|
||||
run: |
|
||||
VERSION="${{ needs.version.outputs.version }}"
|
||||
TAG="${{ needs.version.outputs.tag }}"
|
||||
REPO="refactoringhq/laputa-app"
|
||||
|
||||
ARM_SIG=$(cat updater-aarch64/*.app.tar.gz.sig)
|
||||
ARM_TARBALL=$(ls updater-aarch64/*.app.tar.gz | xargs basename)
|
||||
|
||||
cat > latest-canary.json << EOF
|
||||
{
|
||||
"version": "${VERSION}",
|
||||
"notes": "Canary build. See https://refactoringhq.github.io/laputa-app/ for release notes.",
|
||||
"pub_date": "$(date -u +%Y-%m-%dT%H:%M:%SZ)",
|
||||
"platforms": {
|
||||
"darwin-aarch64": {
|
||||
"signature": "${ARM_SIG}",
|
||||
"url": "https://github.com/${REPO}/releases/download/${TAG}/${ARM_TARBALL}"
|
||||
}
|
||||
}
|
||||
}
|
||||
EOF
|
||||
echo "latest-canary.json:"; cat latest-canary.json
|
||||
|
||||
- name: Publish GitHub Release (prerelease)
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
tag_name: ${{ needs.version.outputs.tag }}
|
||||
name: Laputa ${{ needs.version.outputs.version }} (Canary)
|
||||
body_path: release_notes.md
|
||||
draft: false
|
||||
prerelease: true
|
||||
files: |
|
||||
dmg-aarch64/*.dmg
|
||||
updater-aarch64/*.app.tar.gz
|
||||
updater-aarch64/*.app.tar.gz.sig
|
||||
latest-canary.json
|
||||
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
# Phase 4: Update GitHub Pages (preserve stable latest.json)
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
pages:
|
||||
name: Update release history page
|
||||
needs: [version, release]
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
concurrency:
|
||||
group: github-pages
|
||||
cancel-in-progress: false
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Build release history page
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
mkdir -p _site
|
||||
gh api repos/${{ github.repository }}/releases --paginate > _site/releases.json
|
||||
# Download stable latest.json from existing GH Pages (preserve it)
|
||||
curl -fsSL "https://refactoringhq.github.io/laputa-app/latest.json" -o _site/latest.json || echo '{}' > _site/latest.json
|
||||
# Copy canary latest.json from this release
|
||||
gh release download --repo ${{ github.repository }} "${{ needs.version.outputs.tag }}" --pattern "latest-canary.json" --output _site/latest-canary.json || true
|
||||
cat > _site/index.html << 'HTMLEOF'
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Laputa — Release History</title>
|
||||
<style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; background: #F7F6F3; color: #37352F; line-height: 1.6; padding: 2rem; max-width: 720px; margin: 0 auto; }
|
||||
h1 { font-size: 1.75rem; font-weight: 600; margin-bottom: 0.5rem; }
|
||||
.subtitle { color: #787774; margin-bottom: 2rem; }
|
||||
.release { background: #fff; border: 1px solid #E9E9E7; border-radius: 8px; padding: 1.25rem 1.5rem; margin-bottom: 1rem; }
|
||||
.release h2 { font-size: 1.125rem; font-weight: 600; margin-bottom: 0.25rem; }
|
||||
.release .meta { font-size: 0.8125rem; color: #787774; margin-bottom: 0.75rem; }
|
||||
.release .body { font-size: 0.875rem; white-space: pre-wrap; }
|
||||
.release .downloads { margin-top: 0.75rem; display: flex; gap: 0.5rem; flex-wrap: wrap; }
|
||||
.release .downloads a { display: inline-block; padding: 0.375rem 0.75rem; background: #155DFF; color: #fff; border-radius: 6px; text-decoration: none; font-size: 0.8125rem; font-weight: 500; }
|
||||
.release .downloads a:hover { background: #1248CC; }
|
||||
.canary { border-left: 3px solid #f59e0b; }
|
||||
.empty { color: #787774; text-align: center; padding: 3rem; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Laputa Release History</h1>
|
||||
<p class="subtitle">Auto-updated on every release</p>
|
||||
<div id="releases"></div>
|
||||
<script>
|
||||
fetch('releases.json').then(r=>r.json()).then(releases=>{
|
||||
const el=document.getElementById('releases');
|
||||
if(!releases.length){el.innerHTML='<p class="empty">No releases yet.</p>';return;}
|
||||
releases.forEach(r=>{
|
||||
const date=new Date(r.published_at).toLocaleDateString('en-US',{year:'numeric',month:'long',day:'numeric'});
|
||||
const dmgs=(r.assets||[]).filter(a=>a.name.endsWith('.dmg'));
|
||||
const links=dmgs.map(a=>'<a href="'+a.browser_download_url+'">'+a.name+'</a>').join('');
|
||||
const body=(r.body||'').replace(/</g,'<').replace(/>/g,'>');
|
||||
const div=document.createElement('div');
|
||||
div.className='release'+(r.prerelease?' canary':'');
|
||||
div.innerHTML='<h2>'+(r.name||r.tag_name)+'</h2><div class="meta">'+date+' · '+r.tag_name+(r.prerelease?' · <strong>Canary</strong>':'')+'</div><div class="body">'+body+'</div>'+(links?'<div class="downloads">'+links+'</div>':'');
|
||||
el.appendChild(div);
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
HTMLEOF
|
||||
|
||||
- name: Deploy to GitHub Pages
|
||||
uses: peaceiris/actions-gh-pages@v4
|
||||
with:
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
publish_dir: ./_site
|
||||
commit_message: "Update release history for canary ${{ needs.version.outputs.tag }}"
|
||||
5
.github/workflows/release.yml
vendored
@@ -216,6 +216,9 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
concurrency:
|
||||
group: github-pages
|
||||
cancel-in-progress: false
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
@@ -227,6 +230,8 @@ jobs:
|
||||
gh api repos/${{ github.repository }}/releases --paginate > _site/releases.json
|
||||
# Copy latest.json to GitHub Pages for auto-updater endpoint
|
||||
gh release download --repo ${{ github.repository }} --pattern "latest.json" --output _site/latest.json || true
|
||||
# Preserve canary latest.json from existing GH Pages
|
||||
curl -fsSL "https://refactoringhq.github.io/laputa-app/latest-canary.json" -o _site/latest-canary.json || true
|
||||
cat > _site/index.html << 'HTMLEOF'
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
@@ -16,3 +16,46 @@ echo " → tests..."
|
||||
pnpm test --run --silent
|
||||
|
||||
echo "✅ Pre-commit passed"
|
||||
|
||||
# ── CodeScene Code Health gate ────────────────────────────────────────────
|
||||
# Blocks commit if Hotspot < 9.5 OR Average < 9.0.
|
||||
# Note: remote scores lag behind local changes (update after push + re-analysis).
|
||||
# This catches regressions from previous pushes and notifies Claude Code immediately.
|
||||
# If `pre_commit_code_health_safeguard` fails: extract hooks, split components,
|
||||
# reduce complexity. Never use eslint-disable, #[allow(...)], or `as any`.
|
||||
echo "🏥 CodeScene code health check..."
|
||||
if [ -z "$CODESCENE_PAT" ] || [ -z "$CODESCENE_PROJECT_ID" ]; then
|
||||
echo " ⚠️ CODESCENE_PAT or CODESCENE_PROJECT_ID not set — skipping (CI will enforce)"
|
||||
else
|
||||
API_RESPONSE=$(curl -sf \
|
||||
-H "Authorization: Bearer $CODESCENE_PAT" \
|
||||
-H "Accept: application/json" \
|
||||
"https://api.codescene.io/v2/projects/$CODESCENE_PROJECT_ID" 2>/dev/null || echo "{}")
|
||||
HOTSPOT_SCORE=$(echo "$API_RESPONSE" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d['analysis']['hotspot_code_health']['now'])" 2>/dev/null || echo "")
|
||||
AVERAGE_SCORE=$(echo "$API_RESPONSE" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d['analysis']['code_health']['now'])" 2>/dev/null || echo "")
|
||||
if [ -z "$HOTSPOT_SCORE" ] || [ -z "$AVERAGE_SCORE" ]; then
|
||||
echo " ⚠️ Could not fetch CodeScene scores — skipping (CI will enforce)"
|
||||
else
|
||||
echo " Hotspot Code Health: $HOTSPOT_SCORE (threshold: 9.5)"
|
||||
echo " Average Code Health: $AVERAGE_SCORE (threshold: 9.33)"
|
||||
python3 -c "
|
||||
import sys
|
||||
hotspot = float('$HOTSPOT_SCORE')
|
||||
average = float('$AVERAGE_SCORE')
|
||||
failed = False
|
||||
if hotspot < 9.5:
|
||||
print(f'FAIL: Hotspot Code Health {hotspot:.2f} < 9.5 — extract hooks, split components, reduce complexity')
|
||||
failed = True
|
||||
else:
|
||||
print(f'OK: Hotspot {hotspot:.2f} >= 9.5')
|
||||
if average < 9.33:
|
||||
print(f'FAIL: Average Code Health {average:.2f} < 9.33 — recent changes introduced regressions in non-hotspot files')
|
||||
print(' Review files changed in this task. Never use eslint-disable, #[allow(...)], or as any to bypass.')
|
||||
failed = True
|
||||
else:
|
||||
print(f'OK: Average {average:.2f} >= 9.33')
|
||||
if failed:
|
||||
sys.exit(1)
|
||||
" || exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
@@ -98,18 +98,31 @@ echo ""
|
||||
SMOKE_FILES=$(find tests/smoke -name '*.spec.ts' 2>/dev/null | head -1)
|
||||
if [ -n "$SMOKE_FILES" ]; then
|
||||
echo "🎭 [4/5] Playwright smoke tests..."
|
||||
pnpm playwright:smoke
|
||||
SMOKE_OUTPUT=$(pnpm playwright:smoke 2>&1) || true
|
||||
echo "$SMOKE_OUTPUT" | tail -20
|
||||
# Fail only on real failures (not flaky). Flaky = passed on retry.
|
||||
if echo "$SMOKE_OUTPUT" | grep -qE '^\s+\d+ failed' && ! echo "$SMOKE_OUTPUT" | grep -qE '^\s+\d+ passed'; then
|
||||
echo " ❌ Smoke tests FAILED"
|
||||
exit 1
|
||||
fi
|
||||
echo " ✅ Smoke tests OK"
|
||||
else
|
||||
echo "⏭️ [4/5] Playwright smoke tests — skipped (no tests/smoke/*.spec.ts)"
|
||||
fi
|
||||
|
||||
# ── 5. CodeScene code health gate ────────────────────────────────────────
|
||||
# Note: remote API scores lag behind local changes (only updates after push + re-analysis).
|
||||
# We report the remote score for visibility but don't block on it.
|
||||
# The pre-commit hook already runs `pre_commit_code_health_safeguard` locally.
|
||||
# ── 5. CodeScene code health gate (ratchet) ──────────────────────────────
|
||||
# Thresholds live in .codescene-thresholds and only ever go UP (ratchet).
|
||||
# After every successful push the file is updated if scores improved.
|
||||
THRESHOLDS_FILE="$(git rev-parse --show-toplevel)/.codescene-thresholds"
|
||||
HOTSPOT_MIN=9.5
|
||||
AVERAGE_MIN=9.31
|
||||
if [ -f "$THRESHOLDS_FILE" ]; then
|
||||
HOTSPOT_MIN=$(grep HOTSPOT_THRESHOLD "$THRESHOLDS_FILE" | cut -d= -f2)
|
||||
AVERAGE_MIN=$(grep AVERAGE_THRESHOLD "$THRESHOLDS_FILE" | cut -d= -f2)
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "🏥 [5/5] CodeScene code health (informational — local safeguard runs in pre-commit)..."
|
||||
echo "🏥 [5/5] CodeScene code health (Hotspot ≥${HOTSPOT_MIN} + Average ≥${AVERAGE_MIN})..."
|
||||
if [ -z "$CODESCENE_PAT" ] || [ -z "$CODESCENE_PROJECT_ID" ]; then
|
||||
echo " ⚠️ CODESCENE_PAT or CODESCENE_PROJECT_ID not set — skipping"
|
||||
else
|
||||
@@ -117,14 +130,50 @@ else
|
||||
-H "Authorization: Bearer $CODESCENE_PAT" \
|
||||
-H "Accept: application/json" \
|
||||
"https://api.codescene.io/v2/projects/$CODESCENE_PROJECT_ID" 2>/dev/null || echo "{}")
|
||||
if echo "$API_RESPONSE" | python3 -c "import sys,json; json.load(sys.stdin)['analysis']" 2>/dev/null; then
|
||||
HOTSPOT_SCORE=$(echo "$API_RESPONSE" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d['analysis']['hotspot_code_health']['now'])")
|
||||
AVERAGE_SCORE=$(echo "$API_RESPONSE" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d['analysis']['code_health']['now'])")
|
||||
echo " Remote Hotspot Code Health: $HOTSPOT_SCORE"
|
||||
echo " Remote Average Code Health: $AVERAGE_SCORE"
|
||||
echo " (remote scores update after push — local safeguard already passed in pre-commit)"
|
||||
HOTSPOT_SCORE=$(echo "$API_RESPONSE" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d['analysis']['hotspot_code_health']['now'])" 2>/dev/null || echo "")
|
||||
AVERAGE_SCORE=$(echo "$API_RESPONSE" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d['analysis']['code_health']['now'])" 2>/dev/null || echo "")
|
||||
if [ -z "$HOTSPOT_SCORE" ] || [ -z "$AVERAGE_SCORE" ]; then
|
||||
echo " ⚠️ Could not fetch remote scores — skipping (CI will enforce)"
|
||||
else
|
||||
echo " ⚠️ Could not fetch remote scores — continuing"
|
||||
echo " Remote Hotspot Code Health: $HOTSPOT_SCORE (threshold: $HOTSPOT_MIN)"
|
||||
echo " Remote Average Code Health: $AVERAGE_SCORE (threshold: $AVERAGE_MIN)"
|
||||
python3 -c "
|
||||
import sys, os
|
||||
|
||||
hotspot = float('$HOTSPOT_SCORE')
|
||||
average = float('$AVERAGE_SCORE')
|
||||
hotspot_min = float('$HOTSPOT_MIN')
|
||||
average_min = float('$AVERAGE_MIN')
|
||||
failed = False
|
||||
|
||||
if hotspot < hotspot_min:
|
||||
print(f'FAIL: Hotspot Code Health {hotspot:.2f} < {hotspot_min}')
|
||||
failed = True
|
||||
else:
|
||||
print(f'OK: Hotspot {hotspot:.2f} >= {hotspot_min}')
|
||||
|
||||
if average < average_min:
|
||||
print(f'FAIL: Average Code Health {average:.2f} < {average_min} — regressions detected, fix before pushing')
|
||||
failed = True
|
||||
else:
|
||||
print(f'OK: Average {average:.2f} >= {average_min}')
|
||||
|
||||
if failed:
|
||||
sys.exit(1)
|
||||
|
||||
# Ratchet: update thresholds file if scores improved
|
||||
# Truncate (floor) to 2 decimals so the threshold never exceeds the actual score
|
||||
import math
|
||||
thresholds_file = '$THRESHOLDS_FILE'
|
||||
new_hotspot = max(hotspot_min, math.floor(hotspot * 100) / 100)
|
||||
new_average = max(average_min, math.floor(average * 100) / 100)
|
||||
if new_hotspot > hotspot_min or new_average > average_min:
|
||||
with open(thresholds_file, 'w') as f:
|
||||
f.write(f'HOTSPOT_THRESHOLD={new_hotspot}\nAVERAGE_THRESHOLD={new_average}\n')
|
||||
print(f' 📈 Ratchet updated: Hotspot {hotspot_min} → {new_hotspot}, Average {average_min} → {new_average}')
|
||||
# Stage and amend the thresholds update into the last commit would change history — instead just auto-commit it
|
||||
os.system(f'git add {thresholds_file} && git commit -m \"chore: ratchet CodeScene thresholds to {new_hotspot}/{new_average}\" --no-verify 2>/dev/null || true')
|
||||
" || exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
|
||||
144
CLAUDE.md
@@ -1,23 +1,32 @@
|
||||
# CLAUDE.md — Laputa App
|
||||
|
||||
## ⛔ BEFORE EVERY COMMIT
|
||||
> Quick links: [Project Spec](docs/PROJECT-SPEC.md) · [Architecture](docs/ARCHITECTURE.md) · [Abstractions](docs/ABSTRACTIONS.md) · [Wireframes](ui-design.pen)
|
||||
|
||||
```bash
|
||||
pnpm lint && npx tsc --noEmit
|
||||
pnpm test
|
||||
pnpm test:coverage # frontend ≥70%
|
||||
cargo test
|
||||
cargo llvm-cov --manifest-path src-tauri/Cargo.toml --no-clean --fail-under-lines 85
|
||||
pre_commit_code_health_safeguard # CodeScene ≥9.2 hotspot + ≥9.2 average (target: 9.5+)
|
||||
```
|
||||
---
|
||||
|
||||
If `pre_commit_code_health_safeguard` fails: extract hooks, split components, reduce complexity. Never add `// eslint-disable`, `#[allow(...)]`, or `as any` to pass the gate.
|
||||
## 1. Task Workflow
|
||||
|
||||
## ⛔ BEFORE FIRING laputa-task-done — Two-phase QA
|
||||
### 1a. Pick up a task
|
||||
|
||||
### Phase 1: Playwright (you do this)
|
||||
Run `/laputa-next-task` — fetches next task (To Rework first, then Open), moves to In Progress, returns full description.
|
||||
|
||||
Write a test in `tests/smoke/<slug>.spec.ts` that covers every acceptance criterion. The test must fail before your fix and pass after. Run it:
|
||||
- Read task description and all comments fully
|
||||
- For To Rework: the ❌ QA failed comment tells you exactly what to fix
|
||||
- Check `docs/adr/` for relevant architecture decisions before structural choices
|
||||
- Add a comment: `🚀 Starting work on this task. [Brief description of approach]`
|
||||
|
||||
### 1b. Implement
|
||||
|
||||
- Work on `main` branch — **no branches, no PRs, ever**
|
||||
- Commit every 20–30 min: `feat:`, `fix:`, `refactor:`, `test:`, `docs:`
|
||||
- **⛔ NEVER use --no-verify**
|
||||
- For UI tasks: open `ui-design.pen` first, study visual language, design in light mode
|
||||
|
||||
### 1c. When done
|
||||
|
||||
**Phase 1 — Playwright (only for core user flows):**
|
||||
|
||||
Write smoke test in `tests/smoke/<slug>.spec.ts` only if feature touches: vault open, note create/save/delete, search, wikilink navigation, git commit/push, conflict resolution. Do NOT write Playwright tests for cosmetic changes — use Vitest instead. Suite must stay under **10 minutes**.
|
||||
|
||||
```bash
|
||||
pnpm dev --port 5201 &
|
||||
@@ -25,72 +34,91 @@ sleep 3
|
||||
BASE_URL="http://localhost:5201" npx playwright test tests/smoke/<slug>.spec.ts
|
||||
```
|
||||
|
||||
**If your task touches filesystem, git, AI, MCP, or any native Tauri command**: also test with `pnpm tauri dev` against `~/Laputa` (not demo vault). Use `osascript` keyboard events — no mouse, no `cliclick`.
|
||||
|
||||
### Phase 2: Native QA (Brian does this after push)
|
||||
|
||||
Brian installs the release build and runs keyboard-only QA. Phase 1 must pass first or the task goes to To Rework.
|
||||
|
||||
Fire done signal only after Phase 1 passes — **two steps, both required**:
|
||||
**Phase 2 — Native app QA:**
|
||||
|
||||
```bash
|
||||
# 1. Move task to In Review on Todoist
|
||||
curl -s -X POST "https://api.todoist.com/api/v1/tasks/<task_id>/move" \
|
||||
-H "Authorization: Bearer $TODOIST_API_KEY" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"section_id": "6g3XjX33FF4Vj86M"}'
|
||||
|
||||
# 2. Notify Brian
|
||||
openclaw system event --text "laputa-task-done:<task_id>" --mode now
|
||||
pnpm tauri dev &
|
||||
sleep 10
|
||||
bash ~/.openclaw/skills/laputa-qa/scripts/focus-app.sh laputa
|
||||
bash ~/.openclaw/skills/laputa-qa/scripts/screenshot.sh /tmp/qa-native.png
|
||||
```
|
||||
|
||||
## Project
|
||||
Use `osascript` for keyboard interactions. Write result as Todoist comment (✅ or ❌). **⚠️ WKWebView:** `osascript keystroke` blocked inside editor — rely on Playwright for text input features.
|
||||
|
||||
Tauri v2 + React + TypeScript desktop app. Reads a vault of markdown files with YAML frontmatter.
|
||||
After both phases pass, run `/laputa-done <task_id>` → moves to In Review, notifies Brian, self-dispatches next task.
|
||||
|
||||
- **Spec**: `docs/PROJECT-SPEC.md` | **Architecture**: `docs/ARCHITECTURE.md` | **Abstractions**: `docs/ABSTRACTIONS.md`
|
||||
- **Wireframes**: `ui-design.pen` | **Luca's vault**: `~/Laputa/` (~9200 markdown files)
|
||||
- Stack: Rust backend, React + BlockNote editor, Vitest + Playwright + cargo test, pnpm
|
||||
---
|
||||
|
||||
## How to Work
|
||||
## 2. Development Process
|
||||
|
||||
- **Push directly to main** — no PRs ever. The pre-push hook runs all checks.
|
||||
- **⛔ NEVER open a PR** — branches diverge and cause rebase churn.
|
||||
### Commits & pushes
|
||||
|
||||
- Push directly to `main` — no PRs, no branches
|
||||
- Pre-push hook runs full check suite (build + tests + Playwright + CodeScene)
|
||||
- **A task is NOT done until `git push origin main` succeeds.** If the hook blocks: read the error, fix it (clippy, tests, CodeScene, build), commit the fix, push again. Never use `--no-verify`.
|
||||
- **⛔ NEVER use --no-verify**
|
||||
- Commit every 20–30 min: `feat:`, `fix:`, `refactor:`, `test:`, `docs:`
|
||||
|
||||
## TDD (mandatory)
|
||||
### TDD (mandatory)
|
||||
|
||||
Red → Green → Refactor → Commit. One cycle per commit. For bugs: write a failing regression test first, then fix. Exception: pure CSS/layout with no logic.
|
||||
Red → Green → Refactor → Commit. One cycle per commit. For bugs: write failing regression test first, then fix. Exception: pure CSS/layout changes.
|
||||
|
||||
**Test quality (Kent Beck's Desiderata):** every test must be Isolated (no shared state), Deterministic (no flakiness), Fast, Behavioral (tests behavior not implementation), Structure-insensitive (refactoring doesn't break it), Specific (failure points to exact cause), Predictive (all pass = production-ready). Fix flaky/non-deterministic tests before adding new ones. E2E tests over unit tests for user flows.
|
||||
**Test quality (Kent Beck's Desiderata):** Isolated · Deterministic · Fast · Behavioral · Structure-insensitive · Specific · Predictive. Fix flaky tests before adding new ones. Prefer E2E over unit tests for user flows.
|
||||
|
||||
## ⛔ Docs — Keep docs/ in sync
|
||||
### Code health (mandatory)
|
||||
|
||||
After adding a Tauri command, new component/hook, data model change, or new integration: update `docs/ARCHITECTURE.md`, `docs/ABSTRACTIONS.md`, and/or `docs/GETTING-STARTED.md` in the same commit. Use Mermaid for diagrams (not ASCII). Exception: spatial wireframe layouts.
|
||||
Pre-commit and pre-push hooks enforce **Hotspot Code Health** and **Average Code Health** ≥ thresholds in `.codescene-thresholds`. Both gates block commit/push. Thresholds are a **ratchet** — only go up, auto-updated after each successful push. Never add `// eslint-disable`, `#[allow(...)]`, or `as any`.
|
||||
|
||||
## Design File (UI tasks)
|
||||
**Before every commit:**
|
||||
- `mcp__codescene__code_health_review` — check file before touching
|
||||
- `mcp__codescene__code_health_score` — verify score is higher after changes
|
||||
|
||||
1. Open `ui-design.pen` first — study existing frames for visual language.
|
||||
2. Design in light mode. Create `design/<slug>.pen` for the task.
|
||||
3. On merge to main: merge frames into `ui-design.pen`, delete `design/<slug>.pen`.
|
||||
**Boy Scout Rule:** every file you touch must leave with a higher score. If Average drops below 9.0, fix regressions before pushing.
|
||||
|
||||
## Vault Retrocompatibility
|
||||
### Check suite (runs on every push)
|
||||
```bash
|
||||
pnpm lint && npx tsc --noEmit
|
||||
pnpm test
|
||||
pnpm test:coverage # frontend ≥70%
|
||||
cargo test
|
||||
cargo llvm-cov --manifest-path src-tauri/Cargo.toml --no-clean --fail-under-lines 85
|
||||
```
|
||||
|
||||
Every feature that depends on vault files must auto-bootstrap: check if file/folder exists on vault open, create with defaults if missing (silent, idempotent). Register with the central `Cmd+K → "Repair Vault"` command.
|
||||
### Architecture Decision Records (ADRs)
|
||||
|
||||
## Keyboard-First + Menu Bar (mandatory)
|
||||
ADRs live in `docs/adr/`. Check before structural choices. Create the ADR **in the same commit as the code**. Never edit existing ADRs — create a new one that supersedes. Use `/create-adr` for template.
|
||||
|
||||
Every feature must be reachable via keyboard. Every new command palette entry must also appear in the macOS menu bar (File / Edit / View / Note / Vault / Window). This is a QA requirement.
|
||||
**When to create one:** new dependency, storage strategy, platform target, core abstraction change, cross-cutting pattern. **Not for:** bug fixes, UI styling, refactors, test additions.
|
||||
|
||||
## macOS / Tauri Gotchas
|
||||
### Keep docs/ in sync
|
||||
|
||||
- `Option+N` → special chars on macOS. Use `e.code` or `Cmd+N`.
|
||||
- Tauri menu accelerators: `MenuItemBuilder::new(label).accelerator("CmdOrCtrl+1")`.
|
||||
- `app.set_menu()` replaces the ENTIRE menu bar — include all submenus.
|
||||
- `mock-tauri.ts` silently swallows Tauri calls — not a substitute for native app testing.
|
||||
After Tauri command, new component/hook, data model change, or new integration: update `docs/ARCHITECTURE.md`, `docs/ABSTRACTIONS.md`, and/or `docs/GETTING-STARTED.md` in the same commit.
|
||||
|
||||
## QA Scripts
|
||||
---
|
||||
|
||||
## 3. Product Rules
|
||||
|
||||
### User vault (`~/Laputa/`)
|
||||
|
||||
Default to `demo-vault-v2/`. If you must use `~/Laputa/` for testing: **never commit changes** — always run `cd ~/Laputa && git checkout -- . && git clean -fd` when done.
|
||||
|
||||
### UI design
|
||||
|
||||
1. Open `ui-design.pen` first — study existing frames for visual language
|
||||
2. Design in light mode. Create `design/<slug>.pen` for the task
|
||||
3. On completion: merge frames into `ui-design.pen`, delete `design/<slug>.pen`
|
||||
|
||||
---
|
||||
|
||||
## 4. Reference
|
||||
|
||||
### macOS / Tauri gotchas
|
||||
|
||||
- `Option+N` → special chars on macOS. Use `e.code` or `Cmd+N`
|
||||
- Tauri menu accelerators: `MenuItemBuilder::new(label).accelerator("CmdOrCtrl+1")`
|
||||
- `app.set_menu()` replaces the ENTIRE menu bar — include all submenus
|
||||
- `mock-tauri.ts` silently swallows Tauri calls — not a substitute for native testing
|
||||
|
||||
### QA scripts
|
||||
|
||||
```bash
|
||||
bash ~/.openclaw/skills/laputa-qa/scripts/focus-app.sh laputa
|
||||
@@ -98,6 +126,6 @@ bash ~/.openclaw/skills/laputa-qa/scripts/screenshot.sh /tmp/out.png
|
||||
bash ~/.openclaw/skills/laputa-qa/scripts/shortcut.sh "command" "s"
|
||||
```
|
||||
|
||||
## Documentation Diagrams
|
||||
### Diagrams
|
||||
|
||||
Prefer Mermaid for all diagrams (`flowchart`, `sequenceDiagram`, `classDiagram`, `stateDiagram-v2`). ASCII only for spatial wireframe layouts. GitHub renders Mermaid natively.
|
||||
Prefer Mermaid (`flowchart`, `sequenceDiagram`, `classDiagram`, `stateDiagram-v2`). ASCII only for spatial wireframe layouts.
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
type: config
|
||||
zoom: 1.3
|
||||
view_mode: all
|
||||
---
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
type: config
|
||||
zoom: 1.3
|
||||
view_mode: all
|
||||
---
|
||||
@@ -14,7 +14,7 @@ These frontmatter field names have special meaning in Laputa's UI:
|
||||
|
||||
| Field | Meaning | UI behavior |
|
||||
|---|---|---|
|
||||
| `title:` | Human-readable title (synced with filename) | Tab label, breadcrumb, sidebar. Filename = `slugify(title).md` |
|
||||
| `title:` | Human-readable title (synced with filename) | Breadcrumb, sidebar. Filename = `slugify(title).md` |
|
||||
| `type:` | Entity type (Project, Person, Quarter…) | Type chip in note list + sidebar grouping |
|
||||
| `status:` | Lifecycle stage (active, done, blocked…) | Colored chip in note list + editor header |
|
||||
| `url:` | External link | Clickable link chip in editor header |
|
||||
@@ -25,7 +25,31 @@ These frontmatter field names have special meaning in Laputa's UI:
|
||||
| `Belongs to:` | Parent relationship | Relationship chip in Properties panel |
|
||||
| `Related to:` | Lateral relationship | Relationship chip in Properties panel |
|
||||
|
||||
The list of default-shown relationships and semantic property rendering rules can be customized via `config/relations.md` and `config/semantic-properties.md` in the vault.
|
||||
Relationship fields are detected dynamically — any frontmatter field containing `[[wikilink]]` values is treated as a relationship (see [ADR-0010](adr/0010-dynamic-wikilink-relationship-detection.md)).
|
||||
|
||||
### System Properties (underscore convention)
|
||||
|
||||
Any frontmatter field whose name starts with `_` is a **system property**:
|
||||
|
||||
- It is **not shown** in the Properties panel (neither for notes nor for Type notes)
|
||||
- It is **not exposed** as a user-visible property in search, filters, or the UI
|
||||
- It **is editable** directly in the raw editor (power users can access it if needed)
|
||||
- It is used by Laputa internally for configuration, behavior, and UI preferences
|
||||
|
||||
Examples:
|
||||
```yaml
|
||||
_pinned_properties: # which properties appear in the editor inline bar (per-type)
|
||||
- key: status
|
||||
icon: circle-dot
|
||||
_icon: shapes # icon assigned to a type
|
||||
_color: blue # color assigned to a type
|
||||
_order: 10 # sort order in the sidebar
|
||||
_sidebar_label: Projects # override label in sidebar
|
||||
```
|
||||
|
||||
**This convention is universal** — apply it to all future system-level frontmatter fields. When a new feature needs to store configuration in a note's frontmatter (especially in Type notes), use `_field_name` to keep it hidden from normal user-facing surfaces while still stored on-disk as plain text.
|
||||
|
||||
The frontmatter parser (Rust: `vault/mod.rs`, TS: `utils/frontmatter.ts`) must filter out `_*` fields before passing `properties` to the UI.
|
||||
|
||||
## Document Model
|
||||
|
||||
@@ -48,7 +72,6 @@ classDiagram
|
||||
+Record~string,string[]~ relationships
|
||||
+String[] outgoingLinks
|
||||
+String? status
|
||||
+String? owner
|
||||
+Number? modifiedAt
|
||||
+Number? createdAt
|
||||
+Number wordCount
|
||||
@@ -97,9 +120,8 @@ interface VaultEntry {
|
||||
relationships: Record<string, string[]> // All frontmatter fields containing wikilinks
|
||||
outgoingLinks: string[] // All [[wikilinks]] found in note body
|
||||
status: string | null // Active, Done, Paused, Archived, Dropped
|
||||
owner: string | null // Person responsible
|
||||
cadence: string | null // Update frequency: Weekly, Monthly, etc.
|
||||
modifiedAt: number | null // Unix timestamp (seconds)
|
||||
// Note: owner and cadence are now in the generic `properties` map
|
||||
createdAt: number | null // Unix timestamp (seconds)
|
||||
fileSize: number
|
||||
wordCount: number | null // Body word count (excludes frontmatter)
|
||||
@@ -125,8 +147,7 @@ Type is determined **purely** from the `type:` frontmatter field — it is never
|
||||
├── some-topic.md ← type: Topic
|
||||
├── ...
|
||||
├── type/ ← type definition documents
|
||||
├── config/ ← meta-configuration files (agents.md, etc.)
|
||||
└── theme/ ← vault-based themes (legacy location)
|
||||
└── config/ ← meta-configuration files (agents.md, etc.)
|
||||
```
|
||||
|
||||
New notes are created at the vault root: `{vault}/{slug}.md`. Changing a note's type only requires updating the `type:` field in frontmatter — the file does not move. The `type/` folder exists solely for type definition documents, and `config/` for configuration files.
|
||||
@@ -169,10 +190,8 @@ Standard YAML frontmatter between `---` delimiters:
|
||||
```yaml
|
||||
---
|
||||
title: Write Weekly Essays
|
||||
is_a: Procedure
|
||||
type: Procedure
|
||||
status: Active
|
||||
owner: Luca Rossi
|
||||
cadence: Weekly
|
||||
belongs_to:
|
||||
- "[[grow-newsletter]]"
|
||||
related_to:
|
||||
@@ -246,7 +265,7 @@ type SidebarSelection =
|
||||
|
||||
1. Validates the path exists and is a directory
|
||||
2. Scans root-level `.md` files (non-recursive)
|
||||
3. Recursively scans protected folders: `type/`, `config/`, `attachments/`, `_themes/`, `theme/`
|
||||
3. Recursively scans protected folders: `type/`, `config/`, `attachments/`
|
||||
4. Files in non-protected subfolders are **not indexed** (flat vault enforcement)
|
||||
5. For each `.md` file, calls `parse_md_file()`:
|
||||
- Reads content with `fs::read_to_string()`
|
||||
@@ -349,7 +368,7 @@ interface PulseCommit {
|
||||
|
||||
### Frontend Integration
|
||||
|
||||
- **Modified file badges**: Orange dots in sidebar and tab bar
|
||||
- **Modified file badges**: Orange dots in sidebar
|
||||
- **Diff view**: Toggle in breadcrumb bar → shows unified diff
|
||||
- **Git history**: Shown in Inspector panel for active note
|
||||
- **Commit dialog**: Triggered from sidebar or Cmd+K
|
||||
@@ -419,58 +438,13 @@ Wikilink resolution (`resolveEntry` in `src/utils/wikilink.ts`) uses multi-pass
|
||||
|
||||
Toggle via Cmd+K → "Raw Editor" or breadcrumb bar button. Uses CodeMirror 6 (`useCodeMirror` hook) to edit the raw markdown + frontmatter directly. Changes saved via the same `save_note_content` command.
|
||||
|
||||
## Theme System
|
||||
## Styling
|
||||
|
||||
See [THEMING.md](./THEMING.md) for the full theme system documentation.
|
||||
The app uses a single light theme — the vault-based theming system was removed (see [ADR-0013](adr/0013-remove-theming-system.md)). Styling is defined in two layers:
|
||||
|
||||
### Overview
|
||||
|
||||
Two-layer theming:
|
||||
1. **Global CSS variables** (`src/index.css`): App-wide colors via `:root`, bridged to Tailwind v4
|
||||
2. **Editor theme** (`src/theme.json`): BlockNote typography, flattened to CSS vars by `useEditorTheme`
|
||||
|
||||
### Vault-Based Themes
|
||||
|
||||
Themes are markdown notes in `theme/` with `type: Theme` frontmatter. Each property becomes a CSS variable with `--` prefix.
|
||||
|
||||
```yaml
|
||||
---
|
||||
type: Theme
|
||||
Description: Light theme with warm, paper-like tones
|
||||
background: "#FFFFFF"
|
||||
foreground: "#37352F"
|
||||
accent-blue: "#155DFF"
|
||||
editor-font-size: 16
|
||||
editor-line-height: 1.5
|
||||
---
|
||||
```
|
||||
|
||||
### ThemeManager
|
||||
|
||||
`useThemeManager` hook manages the theme lifecycle:
|
||||
|
||||
```typescript
|
||||
interface ThemeManager {
|
||||
themes: ThemeFile[]
|
||||
activeThemeId: string | null
|
||||
activeTheme: ThemeFile | null
|
||||
isDark: boolean
|
||||
switchTheme(themeId: string): Promise<void>
|
||||
createTheme(name?: string): Promise<string>
|
||||
reloadThemes(): Promise<void>
|
||||
updateThemeProperty(key: string, value: string): Promise<void>
|
||||
}
|
||||
```
|
||||
|
||||
- Detects dark backgrounds via luminance calculation → sets `color-scheme` and `data-theme-mode`
|
||||
- Live preview: re-applies when active theme note is saved
|
||||
- Three built-in themes: Default (light), Dark (deep navy), Minimal (high contrast)
|
||||
- Legacy JSON themes (`_themes/*.json`) supported for backward compatibility
|
||||
|
||||
### Theme Property Editor
|
||||
|
||||
`ThemePropertyEditor` component provides an interactive UI for editing theme properties. Uses `themeSchema.ts` to determine input types (color picker, number slider, text field) based on property names and values.
|
||||
|
||||
## Inspector Abstraction
|
||||
|
||||
The Inspector panel (`src/components/Inspector.tsx`) is composed of sub-panels:
|
||||
@@ -486,46 +460,29 @@ The Inspector panel (`src/components/Inspector.tsx`) is composed of sub-panels:
|
||||
|
||||
4. **GitHistoryPanel**: Shows recent commits from file history with relative timestamps.
|
||||
|
||||
## Closed Tab History
|
||||
## Search
|
||||
|
||||
`useClosedTabHistory` hook (`src/hooks/useClosedTabHistory.ts`) provides a LIFO stack for closed tab entries, used by `useTabManagement` to support Cmd+Shift+T reopen. Each entry stores the note's path, tab index, and full `VaultEntry`. The stack is in-memory only (resets on restart), capped at 20 entries, and deduplicates by path.
|
||||
### Search
|
||||
|
||||
## Search & Indexing
|
||||
|
||||
### Search Modes
|
||||
Keyword-based search scans all vault `.md` files using `walkdir`:
|
||||
|
||||
```typescript
|
||||
type SearchMode = 'keyword' | 'semantic' | 'hybrid'
|
||||
|
||||
interface SearchResult {
|
||||
title: string
|
||||
path: string
|
||||
snippet: string
|
||||
score: number
|
||||
}
|
||||
|
||||
interface SearchResponse {
|
||||
results: SearchResult[]
|
||||
elapsedMs: number
|
||||
}
|
||||
```
|
||||
|
||||
### Search Integration
|
||||
|
||||
`SearchPanel` component provides the search UI:
|
||||
- Mode selector (keyword/semantic/hybrid)
|
||||
- Real-time results as user types
|
||||
- Real-time results as user types (300ms debounce)
|
||||
- Click result to open note in editor
|
||||
- Shows relevance score and snippet
|
||||
|
||||
### Indexing
|
||||
|
||||
Managed by `useIndexing` hook:
|
||||
- Checks index status on vault load
|
||||
- Two-phase indexing: scanning (parse files) → embedding (generate vectors)
|
||||
- Progress streamed via Tauri events
|
||||
- Incremental updates after git sync
|
||||
- Metadata persisted in `.laputa-index.json`
|
||||
No indexing step required — search runs directly against the filesystem.
|
||||
|
||||
## Vault Management
|
||||
|
||||
@@ -566,7 +523,6 @@ App-level settings persisted at `~/.config/com.laputa.app/settings.json`:
|
||||
|
||||
```typescript
|
||||
interface Settings {
|
||||
anthropic_key: string | null
|
||||
openai_key: string | null
|
||||
google_key: string | null
|
||||
github_token: string | null
|
||||
@@ -576,3 +532,34 @@ interface Settings {
|
||||
```
|
||||
|
||||
Managed by `useSettings` hook and `SettingsPanel` component.
|
||||
|
||||
## Telemetry
|
||||
|
||||
### Components
|
||||
- **`TelemetryConsentDialog`** — First-launch dialog asking user to opt in to anonymous crash reporting. Two buttons: accept (sets `telemetry_consent: true`, generates `anonymous_id`) or decline.
|
||||
- **`TelemetryToggle`** — Checkbox component in `SettingsPanel` for crash reporting and analytics toggles.
|
||||
|
||||
### Hooks
|
||||
- **`useTelemetry(settings, loaded)`** — Reactively initializes/tears down Sentry and PostHog based on settings. Called once in `App`.
|
||||
|
||||
### Libraries
|
||||
- **`src/lib/telemetry.ts`** — `initSentry()`, `teardownSentry()`, `initPostHog()`, `teardownPostHog()`, `trackEvent()`. Path scrubber via `beforeSend` hook. DSN/key from `VITE_SENTRY_DSN` / `VITE_POSTHOG_KEY` env vars.
|
||||
- **`src-tauri/src/telemetry.rs`** — Rust-side Sentry init with `beforeSend` path scrubber. `init_sentry_from_settings()` reads settings and conditionally initializes. `reinit_sentry()` for runtime toggle.
|
||||
|
||||
### Tauri Commands
|
||||
- **`reinit_telemetry`** — Re-reads settings and toggles Rust Sentry on/off. Called from frontend when user changes crash reporting setting.
|
||||
|
||||
---
|
||||
|
||||
## Update Channels & Feature Flags
|
||||
|
||||
### Settings
|
||||
- **`update_channel`** — `"stable"` (default/null) or `"canary"`. Stored in `Settings` struct, configurable in Settings panel under "Updates" section.
|
||||
|
||||
### Hooks
|
||||
- **`useUpdater(channel?)`** — Checks for updates. For stable: uses Tauri updater plugin. For canary: fetches `latest-canary.json` and opens release page for download.
|
||||
- **`useFeatureFlag(flag)`** — Returns boolean for a named feature flag. Checks `localStorage` override (`ff_<name>`), then falls back to compile-time default. Type-safe via `FeatureFlagName` union.
|
||||
|
||||
### CI/CD
|
||||
- **`.github/workflows/release.yml`** — Stable builds from `main`. Produces `latest.json` on GitHub Pages.
|
||||
- **`.github/workflows/release-canary.yml`** — Canary builds from `canary` branch. Produces `latest-canary.json` on GitHub Pages. Releases are marked as prerelease.
|
||||
|
||||
@@ -14,6 +14,25 @@ Laputa is opinionated. Standard field names (`type:`, `status:`, `url:`, `Worksp
|
||||
|
||||
This principle directly serves AI-readability: the more structure comes from shared conventions rather than per-user custom configurations, the easier it is for an AI agent to understand and navigate the vault correctly — without needing bespoke instructions for every setup.
|
||||
|
||||
### Where to store state: vault vs. app settings
|
||||
|
||||
When deciding where to persist a piece of data, ask: **"Would the user want this to follow them across all their Laputa installations — other devices, future platforms (tablet, web)?"**
|
||||
|
||||
| Follows the vault | Stays with the installation |
|
||||
|-------------------|-----------------------------|
|
||||
| Type icon, type color | Editor zoom level |
|
||||
| Pinned properties per type | API keys (OpenAI, Google) |
|
||||
| Sidebar label overrides | GitHub token |
|
||||
| Property display order | Window size / position |
|
||||
| Any user-visible customization of how content is organized or displayed | Any machine-specific or credential-type setting |
|
||||
|
||||
**Rule:** If the information is about *how the content is structured or presented* and the user would expect it to be consistent wherever they open their vault, store it in the vault (frontmatter of the relevant note, using the `_field` underscore convention for system properties). If it's about *this specific installation of the app*, store it in `~/.config/com.laputa.app/settings.json` or localStorage.
|
||||
|
||||
Examples:
|
||||
- ✅ Vault: `_pinned_properties` in a Type note (every device should show the same pinned properties)
|
||||
- ✅ Vault: `_icon: shapes` in a Type note (icon is part of the type's identity)
|
||||
- ✅ App settings: `zoom: 1.3` (machine-specific preference)
|
||||
|
||||
### No hardcoded exceptions
|
||||
|
||||
No field names, folder paths, or vault-specific values should be hardcoded in the application source code. What can be a convention should be a convention. What needs to be configurable should live in a file. Relationship fields are detected dynamically by checking whether values contain `[[wikilinks]]` — no hardcoded field name lists.
|
||||
@@ -77,9 +96,8 @@ flowchart LR
|
||||
| Build | Vite | 7.3.1 |
|
||||
| Backend language | Rust (edition 2021) | 1.77.2 |
|
||||
| Frontmatter parsing | gray_matter | 0.2 |
|
||||
| AI (in-app chat) | Anthropic Claude API (Haiku 3.5 default) | - |
|
||||
| AI (agent panel) | Claude CLI subprocess (streaming NDJSON) | - |
|
||||
| Search | qmd (keyword + semantic + hybrid) | - |
|
||||
| Search | Keyword (walkdir-based file scan) | - |
|
||||
| MCP | @modelcontextprotocol/sdk | 1.0 |
|
||||
| Tests | Vitest (unit), Playwright (E2E/smoke), cargo test (Rust) | - |
|
||||
| Package manager | pnpm | - |
|
||||
@@ -94,16 +112,15 @@ flowchart TD
|
||||
WS["WelcomeScreen\n(onboarding)"]
|
||||
SB["Sidebar\n(navigation + filters + types)"]
|
||||
NL["NoteList / PulseView\n(filtered list / activity)"]
|
||||
ED["Editor\n(BlockNote + tabs + diff + raw)"]
|
||||
ED["Editor\n(BlockNote + diff + raw)"]
|
||||
IN["Inspector\n(metadata + relationships)"]
|
||||
AIC["AIChatPanel\n(API-based chat)"]
|
||||
AIP["AiPanel\n(Claude CLI agent + tools)"]
|
||||
SP["SearchPanel\n(keyword/semantic/hybrid)"]
|
||||
SP["SearchPanel\n(keyword search)"]
|
||||
ST["StatusBar\n(vault picker + sync + version)"]
|
||||
CP["CommandPalette\n(Cmd+K launcher)"]
|
||||
|
||||
App --> WS & SB & NL & ED & SP & ST & CP
|
||||
ED --> IN & AIC & AIP
|
||||
ED --> IN & AIP
|
||||
end
|
||||
|
||||
subgraph RB["Rust Backend"]
|
||||
@@ -112,16 +129,14 @@ flowchart TD
|
||||
FM["frontmatter/"]
|
||||
GIT["git/"]
|
||||
GH["github/"]
|
||||
THEME["theme/"]
|
||||
SEARCH["search.rs + indexing.rs"]
|
||||
SETTINGS["settings.rs"]
|
||||
SEARCH["search.rs"]
|
||||
CLI["claude_cli.rs"]
|
||||
end
|
||||
|
||||
subgraph EXT["External Services"]
|
||||
ANTH["Anthropic API\n(Claude chat)"]
|
||||
CCLI["Claude CLI\n(agent subprocess)"]
|
||||
MCP["MCP Server\n(ws://9710, 9711)"]
|
||||
QMD["qmd\n(search engine)"]
|
||||
GHAPI["GitHub API\n(OAuth, repos, clone)"]
|
||||
end
|
||||
|
||||
@@ -141,10 +156,10 @@ flowchart TD
|
||||
│Sidebar │ Note List │ Editor │ Inspector │
|
||||
│(250px) │ (300px) │ (flex-1) │ (280px) │
|
||||
│ │ OR │ │ OR │
|
||||
│ All │ Pulse View │ [Tab Bar] │ AI Chat │
|
||||
│ Favs │ │ [Breadcrumb Bar] │ OR │
|
||||
│ Changes│ [Search] │ │ AI Agent │
|
||||
│ Pulse │ [Sort/Filt] │ # My Note │ │
|
||||
│ All │ Pulse View │ [Breadcrumb Bar] │ AI Chat │
|
||||
│ Changes│ │ │ OR │
|
||||
│ Pulse │ [Search] │ # My Note │ AI Agent │
|
||||
│ Inbox │ [Sort/Filt] │ │ │
|
||||
│ │ │ │ Context │
|
||||
│Projects│ Note 1 │ Content here... │ Messages │
|
||||
│Experim.│ Note 2 │ (BlockNote or Raw) │ Actions │
|
||||
@@ -157,10 +172,10 @@ flowchart TD
|
||||
└──────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
- **Sidebar** (150-400px, resizable): Top-level filters (All Notes, Favorites, Changes, Pulse) and collapsible type-based section groups. Each type can have a custom icon, color, sort, and visibility set via its type document in `type/`.
|
||||
- **Sidebar** (150-400px, resizable): Top-level filters (All Notes, Changes, Pulse) and collapsible type-based section groups. Each type can have a custom icon, color, sort, and visibility set via its type document in `type/`.
|
||||
- **Note List / Pulse View** (200-500px, resizable): When a section group or filter is selected, shows filtered notes with snippets, modified dates, and status indicators. When Pulse filter is active, shows `PulseView` — a chronological git activity feed grouped by day.
|
||||
- **Editor** (flex, fills remaining space): Tab bar with modified dots, breadcrumb bar with word count, BlockNote rich text editor with wikilink support. Can toggle to diff view (modified files) or raw CodeMirror view. Decomposed into `Editor` (orchestrator), `EditorContent`, `EditorRightPanel`, `SingleEditorView`, with hooks `useDiffMode`, `useEditorFocus`, `useEditorSave`, `useRawMode`.
|
||||
- **Inspector / AI Chat / AI Agent** (200-500px or 40px collapsed): Toggles between Inspector (frontmatter, relationships, instances, backlinks, git history), AI Chat panel (API-based), and AI Agent panel (Claude CLI subprocess with tool execution). The Sparkle icon in the breadcrumb bar toggles between them. When viewing a Type note, the Inspector shows an **Instances** section listing all notes of that type (sorted by modified_at desc, capped at 50).
|
||||
- **Editor** (flex, fills remaining space): Single note open at a time (no tabs — see ADR-0003). Breadcrumb bar with word count, BlockNote rich text editor with wikilink support. Can toggle to diff view (modified files) or raw CodeMirror view. Decomposed into `Editor` (orchestrator), `EditorContent`, `EditorRightPanel`, `SingleEditorView`, with hooks `useDiffMode`, `useEditorFocus`, `useEditorSave`, `useRawMode`. Navigation history (Cmd+[/]) replaces tabs.
|
||||
- **Inspector / AI Agent** (200-500px or 40px collapsed): Toggles between Inspector (frontmatter, relationships, instances, backlinks, git history) and AI Agent panel (Claude CLI subprocess with tool execution). The Sparkle icon in the breadcrumb bar toggles between them. When viewing a Type note, the Inspector shows an **Instances** section listing all notes of that type (sorted by modified_at desc, capped at 50).
|
||||
|
||||
Panels are separated by `ResizeHandle` components that support drag-to-resize.
|
||||
|
||||
@@ -184,16 +199,6 @@ Notes can be opened in separate Tauri windows for focused editing. Secondary win
|
||||
|
||||
## AI System
|
||||
|
||||
Laputa has two AI interfaces with distinct architectures:
|
||||
|
||||
### AI Chat (AIChatPanel)
|
||||
|
||||
Simple chat mode — no tool execution, streaming text responses.
|
||||
|
||||
1. **Frontend** (`AIChatPanel` + `useAIChat` hook) — UI and state management
|
||||
2. **API Proxy** (Vite middleware in dev, Rust `ai_chat` command in Tauri) — routes to Anthropic
|
||||
3. **Context picker** — selected notes sent as system context with token estimation
|
||||
|
||||
### AI Agent (AiPanel)
|
||||
|
||||
Full agent mode — spawns Claude CLI as a subprocess with tool access and MCP vault integration.
|
||||
@@ -244,7 +249,7 @@ When the agent writes or edits vault files, `useAiAgent` detects this from tool
|
||||
|
||||
### Context Building
|
||||
|
||||
Both AI modes use context from the active note and linked entries. The agent panel (`ai-context.ts`) builds a structured JSON snapshot:
|
||||
The agent panel (`ai-context.ts`) builds a structured JSON snapshot from the active note and linked entries:
|
||||
|
||||
```json
|
||||
{
|
||||
@@ -258,19 +263,9 @@ Both AI modes use context from the active note and linked entries. The agent pan
|
||||
|
||||
Token budget: 60% of 180k context limit (~108k tokens max). Active note gets priority, then linked notes, then truncation.
|
||||
|
||||
### Models (Chat mode)
|
||||
### Authentication
|
||||
|
||||
| Model | ID | Use case |
|
||||
|-------|----|----------|
|
||||
| Haiku 3.5 | `claude-3-5-haiku-20241022` | Fast, cheap — default |
|
||||
| Sonnet 4 | `claude-sonnet-4-20250514` | Balanced |
|
||||
| Opus 4 | `claude-opus-4-20250514` | Most capable |
|
||||
|
||||
### API Key Management
|
||||
|
||||
- Stored in app settings (`~/.config/com.laputa.app/settings.json`) under `anthropic_key`
|
||||
- Configurable via Settings panel (also supports `openai_key`, `google_key`)
|
||||
- Claude CLI (agent mode) uses its own authentication — no API key needed
|
||||
Claude CLI (agent mode) uses its own authentication — no API key configuration needed in Laputa.
|
||||
|
||||
## MCP Server
|
||||
|
||||
@@ -359,53 +354,16 @@ flowchart LR
|
||||
|
||||
The `WsBridgeChild` state wrapper in `lib.rs` ensures the bridge process is killed on app exit via `RunEvent::Exit` handler.
|
||||
|
||||
## Search & Indexing
|
||||
## Search
|
||||
|
||||
### Search Engine
|
||||
Search is keyword-based, using `walkdir` to scan all `.md` files in the vault directory. No external binary or indexing step required.
|
||||
|
||||
Search uses the external `qmd` binary (semantic search engine) with three modes:
|
||||
- Matches query against file titles and content (case-insensitive)
|
||||
- Scores results: title matches ranked higher than content-only matches
|
||||
- Extracts contextual snippets around the first match
|
||||
- Skips trashed and hidden files
|
||||
|
||||
| Mode | Command | Description |
|
||||
|------|---------|-------------|
|
||||
| `keyword` | `qmd search` | Term matching (default) |
|
||||
| `semantic` | `qmd vsearch` | Vector similarity search |
|
||||
| `hybrid` | `qmd query` | Combined keyword + semantic |
|
||||
|
||||
### Indexing Flow
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
A([Vault opened]) --> B[check_index_status]
|
||||
B --> C{Index status?}
|
||||
C -->|Fresh| D[run_incremental_update\ngit diff since last commit]
|
||||
C -->|Stale / Missing| E
|
||||
|
||||
subgraph E[Full Indexing — start_indexing]
|
||||
E1["Phase 1: qmd update\n(scan all .md files)"]
|
||||
E2["Phase 2: qmd embed\n(generate vector embeddings)"]
|
||||
E1 --> E2
|
||||
end
|
||||
|
||||
E --> F[Save .laputa-index.json\nlast_indexed_commit + timestamp]
|
||||
D --> G([Search ready])
|
||||
F --> G
|
||||
|
||||
E2 -.->|failure is non-fatal| G
|
||||
G --> H{Search mode}
|
||||
H -->|keyword| I[qmd search]
|
||||
H -->|semantic| J[qmd vsearch]
|
||||
H -->|hybrid| K[qmd query]
|
||||
```
|
||||
|
||||
Embedding failure is non-fatal — keyword search still works.
|
||||
|
||||
### qmd Binary Resolution
|
||||
|
||||
1. Bundled macOS app resource: `<app>/Contents/Resources/qmd/qmd`
|
||||
2. Dev mode: `CARGO_MANIFEST_DIR/resources/qmd/qmd`
|
||||
3. System locations: `~/.bun/bin/qmd`, `/usr/local/bin/qmd`, `/opt/homebrew/bin/qmd`
|
||||
4. PATH lookup via `which qmd`
|
||||
5. Auto-install via `bun install -g qmd` if missing
|
||||
The `search_vault` Tauri command runs the scan in a blocking Tokio task and returns results sorted by relevance score.
|
||||
|
||||
## Vault Cache System
|
||||
|
||||
@@ -431,24 +389,13 @@ flowchart TD
|
||||
G --> H([VaultEntry list ready])
|
||||
```
|
||||
|
||||
## Theme System
|
||||
## Styling
|
||||
|
||||
See [THEMING.md](./THEMING.md) for the full theme system documentation.
|
||||
|
||||
### Two-Layer Architecture
|
||||
The app uses a single light theme with no user-configurable theming (see [ADR-0013](adr/0013-remove-theming-system.md)).
|
||||
|
||||
1. **Global CSS variables** (`src/index.css`): App-wide colors, borders, backgrounds. Bridged to Tailwind v4 via `@theme inline`.
|
||||
2. **Editor theme** (`src/theme.json`): BlockNote-specific typography. Flattened to CSS vars by `useEditorTheme`.
|
||||
|
||||
### Vault-Based Themes
|
||||
|
||||
Themes are markdown notes in the `theme/` folder with `type: Theme` frontmatter (`Is A: Theme` accepted as legacy alias). Each frontmatter property becomes a CSS variable. Managed by `useThemeManager` hook and the `src-tauri/src/theme/` Rust module (create, seed, defaults).
|
||||
|
||||
- **Vault settings**: `.laputa/settings.json` stores the active theme reference
|
||||
- **Legacy support**: `_themes/*.json` files still supported for backward compatibility
|
||||
- **Built-in themes**: Default (light), Dark, Minimal — auto-seeded on vault open
|
||||
- **Live preview**: Re-applies when the active theme note is saved
|
||||
|
||||
## Vault Management
|
||||
|
||||
### Vault List
|
||||
@@ -462,14 +409,14 @@ Persisted at `~/.config/com.laputa.app/vaults.json`:
|
||||
}
|
||||
```
|
||||
|
||||
Managed by `useVaultSwitcher` hook. Switching vaults closes all tabs and resets sidebar.
|
||||
Managed by `useVaultSwitcher` hook. Switching vaults resets sidebar and clears the active note.
|
||||
|
||||
### Vault Config
|
||||
|
||||
Per-vault UI settings stored in `ui.config.md` at vault root (YAML frontmatter in a markdown note):
|
||||
- `zoom`: Float zoom level (0.8–1.5)
|
||||
- `view_mode`: "all" | "editor-list" | "editor-only"
|
||||
- `editor_mode`: "raw" | "preview" (persists across tab switches and sessions)
|
||||
- `editor_mode`: "raw" | "preview" (persists across note switches and sessions)
|
||||
- `tag_colors`, `status_colors`: Custom color overrides
|
||||
- `property_display_modes`: Property display preferences
|
||||
|
||||
@@ -519,7 +466,7 @@ sequenceDiagram
|
||||
participant MCP as MCP Server
|
||||
participant U as User
|
||||
|
||||
T->>T: run_startup_tasks()<br/>(purge trash, seed themes, register MCP)
|
||||
T->>T: run_startup_tasks()<br/>(purge trash, register MCP)
|
||||
T->>MCP: spawn_ws_bridge() — ports 9710 + 9711
|
||||
T->>A: App mounts
|
||||
|
||||
@@ -532,8 +479,6 @@ sequenceDiagram
|
||||
T-->>VL: VaultEntry[]
|
||||
VL->>T: invoke('get_modified_files')
|
||||
VL->>T: useMcpStatus — register if needed
|
||||
VL->>T: useThemeManager — load active theme
|
||||
VL->>T: useIndexing — incremental update if stale
|
||||
VL-->>A: entries ready
|
||||
end
|
||||
|
||||
@@ -618,13 +563,10 @@ The vault backend (`src-tauri/src/vault/`) is split into focused submodules:
|
||||
| `frontmatter/` | YAML frontmatter read/write (`mod.rs`, `yaml.rs`, `ops.rs`) |
|
||||
| `git/` | Git operations (`commit.rs`, `status.rs`, `history.rs`, `conflict.rs`, `remote.rs`, `pulse.rs`) |
|
||||
| `github/` | GitHub OAuth + API (`auth.rs`, `api.rs`, `clone.rs`) |
|
||||
| `theme/` | Theme management (`mod.rs`, `create.rs`, `defaults.rs`, `seed.rs`) |
|
||||
| `search.rs` | qmd search integration (keyword/semantic/hybrid) |
|
||||
| `indexing.rs` | qmd indexing with progress streaming |
|
||||
| `search.rs` | Keyword search — walkdir-based vault file scan |
|
||||
| `claude_cli.rs` | Claude CLI subprocess spawning + NDJSON stream parsing |
|
||||
| `ai_chat.rs` | Direct Anthropic API client (non-streaming, for Tauri builds) |
|
||||
| `mcp.rs` | MCP server spawning + config registration |
|
||||
| `commands.rs` | All 62 Tauri command handlers |
|
||||
| `commands/` | Tauri command handlers (split into submodules) |
|
||||
| `settings.rs` | App settings persistence |
|
||||
| `vault_config.rs` | Per-vault UI config |
|
||||
| `vault_list.rs` | Vault list persistence |
|
||||
@@ -689,35 +631,24 @@ The vault backend (`src-tauri/src/vault/`) is split into focused submodules:
|
||||
| `github_create_repo` | Create new repo |
|
||||
| `clone_repo` | Clone repo with token auth |
|
||||
|
||||
### Search & Indexing
|
||||
### Search
|
||||
|
||||
| Command | Description |
|
||||
|---------|-------------|
|
||||
| `search_vault` | Search via qmd (keyword/semantic/hybrid) |
|
||||
| `get_index_status` | Check qmd index state |
|
||||
| `start_indexing` | Full index with progress streaming |
|
||||
| `trigger_incremental_index` | Incremental index update |
|
||||
| `search_vault` | Keyword search across vault files |
|
||||
|
||||
### Theme
|
||||
### Vault Maintenance
|
||||
|
||||
| Command | Description |
|
||||
|---------|-------------|
|
||||
| `list_themes` | List all themes (legacy JSON) |
|
||||
| `get_theme` | Read a theme file |
|
||||
| `get_vault_settings` | Read `.laputa/settings.json` |
|
||||
| `save_vault_settings` | Write vault settings |
|
||||
| `set_active_theme` | Set active theme ID |
|
||||
| `create_theme` | Create JSON theme from template |
|
||||
| `create_vault_theme` | Create markdown theme note |
|
||||
| `ensure_vault_themes` | Seed default themes if missing |
|
||||
| `restore_default_themes` | Restore all default themes |
|
||||
| `repair_vault` | Flatten vault structure, migrate legacy frontmatter, restore themes + config |
|
||||
| `repair_vault` | Flatten vault structure, migrate legacy frontmatter, restore config |
|
||||
|
||||
### AI & MCP
|
||||
|
||||
| Command | Description |
|
||||
|---------|-------------|
|
||||
| `ai_chat` | Direct Anthropic API call (non-streaming) |
|
||||
| `stream_claude_chat` | Claude CLI chat mode (streaming) |
|
||||
| `stream_claude_agent` | Claude CLI agent mode (streaming + tools) |
|
||||
| `check_claude_cli` | Check if Claude CLI is available |
|
||||
@@ -763,16 +694,15 @@ No Redux or global context. State lives in the root `App.tsx` and custom hooks:
|
||||
| `App.tsx` | `selection`, panel widths, dialog visibility, toast, view mode | UI state |
|
||||
| `useVaultLoader` | `entries`, `allContent`, `modifiedFiles` | Vault data |
|
||||
| `useNoteActions` | `tabs`, `activeTabPath` | Composes `useNoteCreation` + `useNoteRename` + `frontmatterOps` |
|
||||
| `useNoteCreation` | — (delegates to `useTabManagement`) | Note/type/daily-note creation with optimistic persistence |
|
||||
| `useNoteRename` | — (delegates to `useTabManagement`) | Note renaming with wikilink update |
|
||||
| `useNoteCreation` | — | Note/type/daily-note creation with optimistic persistence |
|
||||
| `useNoteRename` | — | Note renaming with wikilink update |
|
||||
| `frontmatterOps` | — (pure functions) | Frontmatter CRUD: key→VaultEntry mapping, mock/Tauri dispatch |
|
||||
| `useTabManagement` | Tab ordering, pinning, swapping, closed-tab history | Tab lifecycle |
|
||||
| `useTabManagement` | Navigation history, note switching | Note navigation lifecycle |
|
||||
| `useVaultSwitcher` | `vaultPath`, `extraVaults` | Vault switching |
|
||||
| `useThemeManager` | `themes`, `activeThemeId`, `isDark` | Theme state |
|
||||
| `useAIChat` | `messages`, `isStreaming` | AI chat conversation |
|
||||
| `useTheme` | Editor theme CSS vars | Editor typography theme |
|
||||
| `useAiAgent` | `messages`, `status`, tool actions | AI agent conversation |
|
||||
| `useAutoSync` | Sync interval, pull/push state | Git auto-sync |
|
||||
| `useIndexing` | Index status, progress | Search indexing |
|
||||
| `useUnifiedSearch` | Query, results, loading state | Keyword search |
|
||||
| `useSettings` | App settings (API keys, GitHub token) | Persistent settings |
|
||||
| `useVaultConfig` | Per-vault UI preferences | Vault-specific config |
|
||||
|
||||
@@ -786,8 +716,7 @@ Data flows unidirectionally: `App` passes data and callbacks as props to child c
|
||||
| Cmd+P | Open quick open palette |
|
||||
| Cmd+N | Create new note |
|
||||
| Cmd+S | Save current note |
|
||||
| Cmd+W | Close active tab |
|
||||
| Cmd+Shift+T | Reopen last closed tab (LIFO history, up to 20) |
|
||||
| Cmd+[ / Cmd+] | Navigate back / forward (replaces tabs) |
|
||||
| Cmd+Z / Cmd+Shift+Z | Undo / Redo |
|
||||
| Cmd+1–9 | Switch to tab N |
|
||||
| Cmd+[ / Cmd+] | Navigate back / forward |
|
||||
@@ -831,3 +760,126 @@ App startup (3s delay)
|
||||
→ ready → "Restart to apply" + Restart Now
|
||||
→ network error → fail silently
|
||||
```
|
||||
|
||||
### Telemetry (Opt-in)
|
||||
|
||||
Anonymous crash reporting (Sentry) and usage analytics (PostHog), both **opt-in only**.
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant User
|
||||
participant App
|
||||
participant Settings
|
||||
participant Sentry
|
||||
participant PostHog
|
||||
|
||||
Note over App: First launch or upgrade
|
||||
App->>User: TelemetryConsentDialog
|
||||
alt Accept
|
||||
User->>Settings: telemetry_consent=true, anonymous_id=UUID
|
||||
Settings->>Sentry: init(DSN, anonymous_id)
|
||||
Settings->>PostHog: init(key, anonymous_id)
|
||||
else Decline
|
||||
User->>Settings: telemetry_consent=false
|
||||
Note over Sentry,PostHog: Zero network requests
|
||||
end
|
||||
|
||||
Note over App: Settings panel toggle change
|
||||
User->>Settings: crash_reporting_enabled=false
|
||||
Settings->>Sentry: teardown()
|
||||
Settings->>App: reinit_telemetry (Tauri cmd)
|
||||
```
|
||||
|
||||
**Privacy guarantees:**
|
||||
- No vault content, note titles, or file paths in payloads (regex scrubber in `beforeSend`)
|
||||
- `anonymous_id` is a locally-generated UUID, never tied to identity
|
||||
- `send_default_pii: false` on both SDKs
|
||||
- PostHog: `autocapture: false`, `persistence: 'memory'`, no cookies
|
||||
|
||||
**Architecture:**
|
||||
- **Rust:** `sentry` crate initialized in `lib.rs::setup()` via `telemetry::init_sentry_from_settings()`
|
||||
- **JS:** `@sentry/browser` + `posthog-js` initialized lazily by `useTelemetry` hook
|
||||
- **Settings:** `telemetry_consent`, `crash_reporting_enabled`, `analytics_enabled`, `anonymous_id` in `Settings` struct
|
||||
- **Consent:** `TelemetryConsentDialog` shown when `telemetry_consent === null`
|
||||
|
||||
### Update Channels (Stable / Canary)
|
||||
|
||||
Laputa supports two release channels:
|
||||
|
||||
- **Stable** (default): builds from `main` branch, published as full GitHub Releases
|
||||
- **Canary**: builds from `canary` branch, published as pre-release GitHub Releases
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
main["main branch"] -->|push| stable["Stable build<br/>latest.json"]
|
||||
canary["canary branch"] -->|push| canaryBuild["Canary build<br/>latest-canary.json"]
|
||||
stable --> ghPages["GitHub Pages"]
|
||||
canaryBuild --> ghPages
|
||||
ghPages -->|"update_channel = stable"| stableUsers["Stable users<br/>(auto-update via plugin)"]
|
||||
ghPages -->|"update_channel = canary"| canaryUsers["Canary users<br/>(fetch + manual download)"]
|
||||
```
|
||||
|
||||
**How it works:**
|
||||
- Both channels publish to GitHub Pages: `latest.json` (stable) and `latest-canary.json` (canary)
|
||||
- `update_channel` is stored in `Settings` (`settings.json`), configurable in Settings panel
|
||||
- **Stable**: uses the Tauri updater plugin with automatic download and install
|
||||
- **Canary**: `useUpdater` hook fetches `latest-canary.json` via HTTP, compares versions, and opens the GitHub release page for manual download
|
||||
- Canary versions use semver prerelease: `0.YYYYMMDD.N-canary`
|
||||
|
||||
### Feature Flags (Local V1)
|
||||
|
||||
Feature flags use a local-only system with no external dependencies:
|
||||
|
||||
```typescript
|
||||
import { useFeatureFlag } from './hooks/useFeatureFlag'
|
||||
|
||||
const enabled = useFeatureFlag('example_flag') // boolean
|
||||
```
|
||||
|
||||
**Resolution order:**
|
||||
1. `localStorage` override: key `ff_<name>` with value `"true"` or `"false"`
|
||||
2. Compile-time default in `FLAG_DEFAULTS` map
|
||||
|
||||
**How to add a new flag:**
|
||||
1. Add the flag name to the `FeatureFlagName` union type in `src/hooks/useFeatureFlag.ts`
|
||||
2. Set its default in the `FLAG_DEFAULTS` record
|
||||
3. Use `useFeatureFlag('your_flag')` in components
|
||||
|
||||
**Design decisions:**
|
||||
- No remote fetching, no PostHog dependency — zero privacy concerns
|
||||
- `localStorage` overrides allow dev/QA testing without rebuilding
|
||||
- Type-safe flag names via TypeScript union type
|
||||
- API surface is compatible with future migration to remote flags
|
||||
|
||||
## Platform Support — iOS / iPadOS (Prototype)
|
||||
|
||||
Tauri v2 supports iOS as a beta target. The Rust backend cross-compiles to `aarch64-apple-ios-sim` (simulator) and `aarch64-apple-ios` (device) with zero code changes to vault/frontmatter/search logic.
|
||||
|
||||
**Conditional compilation strategy:**
|
||||
|
||||
```
|
||||
#[cfg(desktop)] — git CLI, menu bar, MCP server, Claude CLI, updater
|
||||
#[cfg(mobile)] — stub commands returning graceful errors or empty results
|
||||
```
|
||||
|
||||
Desktop-only modules gated at the crate level:
|
||||
- `pub mod menu` — macOS menu bar (entire module)
|
||||
|
||||
Desktop-only features gated at the function level in `commands/`:
|
||||
- Git operations (commit, pull, push, status, history, diff, conflicts)
|
||||
- GitHub operations (clone, list repos, device flow auth)
|
||||
- Claude CLI streaming (check, chat, agent)
|
||||
- MCP registration and status
|
||||
- Menu state updates
|
||||
|
||||
Features that work on both platforms without changes:
|
||||
- Vault scan, note read/write, rename, delete, trash, archive
|
||||
- Frontmatter read/write/delete
|
||||
- AI chat (Anthropic API via `reqwest`)
|
||||
- Search (pure Rust in-memory)
|
||||
- Settings persistence
|
||||
- Vault list management
|
||||
|
||||
**Capabilities:** `src-tauri/capabilities/default.json` targets desktop; `mobile.json` targets iOS/Android with a minimal permission set.
|
||||
|
||||
**Detailed feasibility report:** `docs/IPAD-PROTOTYPE.md`
|
||||
|
||||
@@ -7,7 +7,6 @@ How to navigate the codebase, run the app, and find what you need.
|
||||
- **Node.js** 18+ and **pnpm**
|
||||
- **Rust** 1.77.2+ (for the Tauri backend)
|
||||
- **git** CLI (required by the git integration features)
|
||||
- **qmd** (optional — for search indexing; auto-installed if missing)
|
||||
|
||||
## Quick Start
|
||||
|
||||
@@ -47,14 +46,13 @@ laputa-app/
|
||||
│ │ ├── NoteList.tsx # Second panel: filtered note list
|
||||
│ │ ├── NoteItem.tsx # Individual note item
|
||||
│ │ ├── PulseView.tsx # Git activity feed (replaces NoteList)
|
||||
│ │ ├── Editor.tsx # Third panel: tabs + editor orchestration
|
||||
│ │ ├── Editor.tsx # Third panel: editor orchestration
|
||||
│ │ ├── EditorContent.tsx # Editor content area
|
||||
│ │ ├── EditorRightPanel.tsx # Right panel toggle
|
||||
│ │ ├── editorSchema.tsx # BlockNote schema + wikilink type
|
||||
│ │ ├── RawEditorView.tsx # CodeMirror raw editor
|
||||
│ │ ├── Inspector.tsx # Fourth panel: metadata + relationships
|
||||
│ │ ├── DynamicPropertiesPanel.tsx # Editable frontmatter properties
|
||||
│ │ ├── AIChatPanel.tsx # AI chat (API-based)
|
||||
│ │ ├── AiPanel.tsx # AI agent (Claude CLI subprocess)
|
||||
│ │ ├── AiMessage.tsx # Agent message display
|
||||
│ │ ├── AiActionCard.tsx # Agent tool action cards
|
||||
@@ -62,12 +60,11 @@ laputa-app/
|
||||
│ │ ├── SettingsPanel.tsx # App settings
|
||||
│ │ ├── StatusBar.tsx # Bottom bar: vault picker + sync
|
||||
│ │ ├── CommandPalette.tsx # Cmd+K command launcher
|
||||
│ │ ├── TabBar.tsx # Tab management
|
||||
│ │ ├── BreadcrumbBar.tsx # Breadcrumb + word count + actions
|
||||
│ │ ├── WelcomeScreen.tsx # Onboarding screen
|
||||
│ │ ├── GitHubVaultModal.tsx # GitHub vault clone/create
|
||||
│ │ ├── GitHubDeviceFlow.tsx # GitHub OAuth device flow
|
||||
│ │ ├── ThemePropertyEditor.tsx # Interactive theme editor
|
||||
│ │ ├── TitleField.tsx # Editable note title above editor
|
||||
│ │ ├── ConflictResolverModal.tsx # Git conflict resolution
|
||||
│ │ ├── CommitDialog.tsx # Git commit modal
|
||||
│ │ ├── CreateNoteDialog.tsx # New note modal
|
||||
@@ -88,16 +85,13 @@ laputa-app/
|
||||
│ │ ├── useNoteActions.ts # Composes creation + rename + frontmatter
|
||||
│ │ ├── useNoteCreation.ts # Note/type/daily-note creation
|
||||
│ │ ├── useNoteRename.ts # Note renaming + wikilink updates
|
||||
│ │ ├── useTabManagement.ts # Tab ordering + lifecycle
|
||||
│ │ ├── useAIChat.ts # AI chat state
|
||||
│ │ ├── useAiAgent.ts # AI agent state + tool tracking
|
||||
│ │ ├── useAiActivity.ts # MCP UI bridge listener
|
||||
│ │ ├── useAutoSync.ts # Auto git pull/push
|
||||
│ │ ├── useConflictResolver.ts # Git conflict handling
|
||||
│ │ ├── useEditorSave.ts # Auto-save with debounce
|
||||
│ │ ├── useTheme.ts # Flatten theme.json → CSS vars
|
||||
│ │ ├── useThemeManager.ts # Vault theme lifecycle
|
||||
│ │ ├── useIndexing.ts # Search indexing management
|
||||
│ │ ├── useUnifiedSearch.ts # Keyword search
|
||||
│ │ ├── useNoteSearch.ts # Note search
|
||||
│ │ ├── useCommandRegistry.ts # Command palette registry
|
||||
│ │ ├── useAppCommands.ts # App-level commands
|
||||
@@ -114,10 +108,10 @@ laputa-app/
|
||||
│ │ ├── wikilinks.ts # Wikilink preprocessing pipeline
|
||||
│ │ ├── frontmatter.ts # TypeScript YAML parser
|
||||
│ │ ├── ai-agent.ts # Agent stream utilities
|
||||
│ │ ├── ai-chat.ts # Chat API client + token estimation
|
||||
│ │ ├── ai-chat.ts # Token estimation utilities
|
||||
│ │ ├── ai-context.ts # Context snapshot builder
|
||||
│ │ ├── noteListHelpers.ts # Sorting, filtering, date formatting
|
||||
│ │ ├── themeSchema.ts # Theme editor schema builder
|
||||
│ │ ├── wikilink.ts # Wikilink resolution
|
||||
│ │ ├── configMigration.ts # localStorage → vault config migration
|
||||
│ │ ├── iconRegistry.ts # Phosphor icon registry
|
||||
│ │ ├── propertyTypes.ts # Property type definitions
|
||||
@@ -139,7 +133,7 @@ laputa-app/
|
||||
│ ├── src/
|
||||
│ │ ├── main.rs # Entry point (calls lib::run())
|
||||
│ │ ├── lib.rs # Tauri setup + command registration (61 commands)
|
||||
│ │ ├── commands.rs # All Tauri command handlers
|
||||
│ │ ├── commands/ # Tauri command handlers (split into modules)
|
||||
│ │ ├── vault/ # Vault module
|
||||
│ │ │ ├── mod.rs # Core types, parse_md_file, scan_vault
|
||||
│ │ │ ├── cache.rs # Git-based incremental caching
|
||||
@@ -156,12 +150,9 @@ laputa-app/
|
||||
│ │ │ ├── conflict.rs, remote.rs, pulse.rs
|
||||
│ │ ├── github/ # GitHub module
|
||||
│ │ │ ├── mod.rs, auth.rs, api.rs, clone.rs
|
||||
│ │ ├── theme/ # Theme module
|
||||
│ │ │ ├── mod.rs, create.rs, defaults.rs, seed.rs
|
||||
│ │ ├── search.rs # qmd search integration
|
||||
│ │ ├── indexing.rs # qmd indexing + progress streaming
|
||||
│ │ ├── telemetry.rs # Sentry init + path scrubber
|
||||
│ │ ├── search.rs # Keyword search (walkdir-based)
|
||||
│ │ ├── claude_cli.rs # Claude CLI subprocess management
|
||||
│ │ ├── ai_chat.rs # Direct Anthropic API client
|
||||
│ │ ├── mcp.rs # MCP server lifecycle + registration
|
||||
│ │ ├── settings.rs # App settings persistence
|
||||
│ │ ├── vault_config.rs # Per-vault UI config
|
||||
@@ -199,7 +190,7 @@ laputa-app/
|
||||
|------|---------------|
|
||||
| `src/App.tsx` | Root component. Shows the 4-panel layout, state flow, and how all features connect. |
|
||||
| `src/types.ts` | All shared TypeScript types. Read this first to understand the data model. |
|
||||
| `src-tauri/src/commands.rs` | All 61 Tauri command handlers. This is the frontend-backend API surface. |
|
||||
| `src-tauri/src/commands/` | Tauri command handlers (split into modules). This is the frontend-backend API surface. |
|
||||
| `src-tauri/src/lib.rs` | Tauri setup, command registration, startup tasks, WebSocket bridge lifecycle. |
|
||||
|
||||
### Data layer
|
||||
@@ -220,14 +211,14 @@ laputa-app/
|
||||
| `src-tauri/src/frontmatter/ops.rs` | YAML manipulation — how properties are updated/deleted in files. |
|
||||
| `src-tauri/src/git/` | All git operations (commit, pull, push, conflicts, pulse). |
|
||||
| `src-tauri/src/github/` | GitHub OAuth device flow + repo clone/create. |
|
||||
| `src-tauri/src/search.rs` | qmd search integration (keyword/semantic/hybrid). |
|
||||
| `src-tauri/src/search.rs` | Keyword search — scans vault files with walkdir. |
|
||||
| `src-tauri/src/claude_cli.rs` | Claude CLI subprocess spawning + NDJSON stream parsing. |
|
||||
|
||||
### Editor
|
||||
|
||||
| File | Why it matters |
|
||||
|------|---------------|
|
||||
| `src/components/Editor.tsx` | BlockNote setup, tab bar, breadcrumb bar, diff/raw toggle. |
|
||||
| `src/components/Editor.tsx` | BlockNote setup, breadcrumb bar, diff/raw toggle. |
|
||||
| `src/components/editorSchema.tsx` | Custom wikilink inline content type definition. |
|
||||
| `src/utils/wikilinks.ts` | Wikilink preprocessing pipeline (markdown ↔ BlockNote). |
|
||||
| `src/components/RawEditorView.tsx` | CodeMirror 6 raw markdown editor. |
|
||||
@@ -237,18 +228,15 @@ laputa-app/
|
||||
| File | Why it matters |
|
||||
|------|---------------|
|
||||
| `src/components/AiPanel.tsx` | AI agent panel — Claude CLI with tool execution, reasoning, actions. |
|
||||
| `src/components/AIChatPanel.tsx` | AI chat panel — API-based chat without tools. |
|
||||
| `src/hooks/useAiAgent.ts` | Agent state: messages, streaming, tool tracking, file detection. |
|
||||
| `src/utils/ai-context.ts` | Context snapshot builder for AI conversations. |
|
||||
|
||||
### Styling & Themes
|
||||
### Styling
|
||||
|
||||
| File | Why it matters |
|
||||
|------|---------------|
|
||||
| `src/index.css` | All CSS custom properties. The design token source of truth. |
|
||||
| `src/theme.json` | Editor-specific theme (fonts, headings, lists, code blocks). |
|
||||
| `src/hooks/useThemeManager.ts` | Vault theme lifecycle (switch, create, apply, live preview). |
|
||||
| `docs/THEMING.md` | Full theme system documentation. |
|
||||
|
||||
### Settings & Config
|
||||
|
||||
@@ -276,7 +264,7 @@ This lives in `useVaultLoader.ts` and `useNoteActions.ts`. Components never call
|
||||
|
||||
### Props-Down, Callbacks-Up
|
||||
|
||||
No global state management (no Redux, no Context). `App.tsx` owns the state and passes it down as props. Child-to-parent communication uses callback props (`onSelectNote`, `onCloseTab`, etc.).
|
||||
No global state management (no Redux, no Context). `App.tsx` owns the state and passes it down as props. Child-to-parent communication uses callback props (`onSelectNote`, etc.).
|
||||
|
||||
### Discriminated Unions for Selection State
|
||||
|
||||
@@ -319,7 +307,7 @@ BASE_URL="http://localhost:5173" npx playwright test tests/smoke/<slug>.spec.ts
|
||||
### Add a new Tauri command
|
||||
|
||||
1. Write the Rust function in the appropriate module (`vault/`, `git/`, etc.)
|
||||
2. Add a command handler in `commands.rs`
|
||||
2. Add a command handler in `commands/`
|
||||
3. Register it in the `generate_handler![]` macro in `lib.rs`
|
||||
4. Call it from the frontend via `invoke()` in the appropriate hook
|
||||
5. Add a mock handler in `mock-tauri.ts`
|
||||
@@ -344,11 +332,10 @@ BASE_URL="http://localhost:5173" npx playwright test tests/smoke/<slug>.spec.ts
|
||||
2. Add a corresponding menu bar item in `menu.rs` for discoverability
|
||||
3. If it has a keyboard shortcut, register it in `useAppKeyboard.ts`
|
||||
|
||||
### Add or modify a theme
|
||||
### Modify styling
|
||||
|
||||
1. **Vault-based** (preferred): Create/edit a markdown note in `theme/` with `type: Theme` frontmatter
|
||||
2. **Programmatic**: Edit defaults in `src-tauri/src/theme/defaults.rs`
|
||||
3. See `docs/THEMING.md` for the full property reference
|
||||
1. **Global CSS variables**: Edit `src/index.css`
|
||||
2. **Editor typography**: Edit `src/theme.json`
|
||||
|
||||
### Work with the AI agent
|
||||
|
||||
|
||||
115
docs/IPAD-PROTOTYPE.md
Normal file
@@ -0,0 +1,115 @@
|
||||
# iPad Prototype — Tauri v2 iOS Feasibility Report
|
||||
|
||||
**Date:** 2026-03-27
|
||||
**Status:** VERIFIED — App builds, installs, and renders React UI on iPad Pro 13" simulator (iOS 18.3.1)
|
||||
|
||||
## Summary
|
||||
|
||||
Laputa can be ported to iPad using Tauri v2 iOS (beta) with **minimal code changes**. The React frontend stays identical. The Rust backend compiles for iOS with conditional compilation to gate desktop-only features (git CLI, menu bar, MCP, Claude CLI). Vault read/write operations work without changes.
|
||||
|
||||
**Key result:** `tauri ios build --target aarch64-sim` succeeds. The app launches on iPad simulator and the React UI renders correctly (telemetry consent dialog, welcome screen, all styled correctly).
|
||||
|
||||
## What Works
|
||||
|
||||
| Feature | Status | Notes |
|
||||
|---------|--------|-------|
|
||||
| Rust backend cross-compilation | VERIFIED | Zero errors, zero warnings for `aarch64-apple-ios-sim` |
|
||||
| Desktop build (no regressions) | VERIFIED | 581 Rust tests pass, 2201 frontend tests pass, CodeScene gates pass |
|
||||
| Tauri iOS project generation | VERIFIED | `tauri ios init` generates Xcode project successfully |
|
||||
| Xcode build for simulator | VERIFIED | `tauri ios build --target aarch64-sim` — **BUILD SUCCEEDED** |
|
||||
| App launch on iPad simulator | VERIFIED | Installs and launches on iPad Pro 13" (M4), PID assigned |
|
||||
| React UI in WebView | VERIFIED | Telemetry consent dialog renders with correct styling, fonts, buttons |
|
||||
| Vault file read/write | Expected to work | Pure filesystem operations, no process spawning |
|
||||
| AI chat (Anthropic API) | Expected to work | Uses `reqwest` HTTP, no CLI dependency |
|
||||
| Search | Expected to work | Pure Rust in-memory search |
|
||||
| Settings persistence | Expected to work | JSON file read/write |
|
||||
|
||||
## What Doesn't Work (Yet)
|
||||
|
||||
| Feature | Blocker | Recommended Solution |
|
||||
|---------|---------|---------------------|
|
||||
| Git operations | No `git` binary on iOS | **Option B (Working Copy)** for prototype; **Option A (isomorphic-git)** for production |
|
||||
| GitHub clone/push/pull | Depends on git CLI | Same as above |
|
||||
| Claude CLI streaming | No `claude` binary on iOS | Use Anthropic API directly (requires new implementation) |
|
||||
| MCP server / WS bridge | Spawns Node.js child process | Skip for mobile; explore in-process MCP later |
|
||||
| macOS menu bar | Desktop-only API | Touch-native navigation (already handled by React) |
|
||||
| Updater plugin | Desktop-only | Use TestFlight for updates |
|
||||
| File open dialog | Different on iOS | `tauri-plugin-dialog` supports iOS — needs testing |
|
||||
|
||||
## Changes Made
|
||||
|
||||
### `src-tauri/src/lib.rs`
|
||||
- Gated `WsBridgeChild`, `run_startup_tasks`, `spawn_ws_bridge`, `log_startup_result` behind `#[cfg(desktop)]`
|
||||
- Mobile build skips MCP registration, WS bridge, and vault migrations at startup
|
||||
- Run event handler gated for desktop (child process cleanup)
|
||||
|
||||
### `src-tauri/src/commands.rs`
|
||||
- Git commands: desktop implementations remain unchanged; mobile stubs return graceful errors or empty results
|
||||
- GitHub commands: desktop-only; mobile stubs return errors
|
||||
- Claude CLI commands: desktop-only; mobile stubs return `installed: false`
|
||||
- MCP commands: desktop-only; mobile stubs return `NotInstalled`
|
||||
- Menu commands: desktop-only; mobile stub is a no-op
|
||||
- Vault, frontmatter, search, AI chat, settings: **unchanged** (work on both platforms)
|
||||
|
||||
### `src-tauri/src/lib.rs`
|
||||
- `pub mod menu` gated behind `#[cfg(desktop)]`
|
||||
|
||||
### `src-tauri/capabilities/`
|
||||
- `default.json`: scoped to desktop platforms (`linux`, `macOS`, `windows`)
|
||||
- `mobile.json`: new file with iOS/Android permissions (core, dialog)
|
||||
|
||||
### `src-tauri/gen/apple/`
|
||||
- Full Xcode project generated by `tauri ios init`
|
||||
- iPad support: all orientations enabled, arm64 architecture
|
||||
|
||||
## Architecture: How Mobile Stubs Work
|
||||
|
||||
```
|
||||
Frontend (React) ──invoke──> Tauri Commands ──> Rust Backend
|
||||
│
|
||||
┌──────────┴──────────┐
|
||||
#[cfg(desktop)] #[cfg(mobile)]
|
||||
│ │
|
||||
Real impl Stub (error/empty)
|
||||
(git CLI, (graceful degradation)
|
||||
menu, MCP)
|
||||
```
|
||||
|
||||
The frontend code doesn't change at all. Commands that aren't available on mobile return errors that the UI can handle gracefully (e.g., hiding the git sync panel, disabling commit buttons).
|
||||
|
||||
## Git Strategy for iPad
|
||||
|
||||
### Phase 1: Working Copy (Recommended for prototype)
|
||||
- User manages vault with [Working Copy](https://workingcopy.app/) (git client for iPad)
|
||||
- Laputa opens the vault via iOS Files API / FileProvider
|
||||
- Zero git code needed in Laputa — Working Copy handles sync
|
||||
- **Pro:** Works immediately, robust git implementation
|
||||
- **Con:** Requires separate app, split UX for sync
|
||||
|
||||
### Phase 2: isomorphic-git (Production)
|
||||
- Pure JS git implementation running in the WebView
|
||||
- Replace `invoke("git_commit")` etc. with JS-side git operations
|
||||
- **Pro:** Integrated UX, no external dependency
|
||||
- **Con:** Slower on large vaults (~9200 files), limited advanced git features
|
||||
|
||||
### Phase 3: git2-rs / gitoxide (Future)
|
||||
- Pure Rust git library compiled into the Tauri binary
|
||||
- Best performance, no JS bridge overhead
|
||||
- **Con:** Larger binary size, more integration work
|
||||
|
||||
## Next Steps
|
||||
|
||||
1. **Install iOS simulator runtime** — `xcodebuild -downloadPlatform iOS` (downloading ~7GB)
|
||||
2. **Full Xcode build** — `npx tauri ios build --target aarch64-sim`
|
||||
3. **Launch on iPad simulator** — `npx tauri ios dev`
|
||||
4. **Test vault read/write** — open a vault, read/edit a note
|
||||
5. **Test AI chat** — verify Anthropic API calls work from iOS WebView
|
||||
6. **Evaluate touch UX** — identify layout issues on iPad screen size
|
||||
7. **File Provider integration** — test opening vaults from Working Copy via iOS Files
|
||||
|
||||
## Prerequisites Installed
|
||||
|
||||
- Rust iOS targets: `aarch64-apple-ios`, `aarch64-apple-ios-sim`, `x86_64-apple-ios`
|
||||
- Xcode 16.2 with iOS 18.3.1 simulator runtime (downloading)
|
||||
- CocoaPods 1.16.2, xcodegen 2.45.3, libimobiledevice 1.4.0
|
||||
- Tauri CLI 2.10.0 with iOS support
|
||||
283
docs/THEMING.md
@@ -1,283 +0,0 @@
|
||||
# Theming
|
||||
|
||||
How the visual theme system works in Laputa.
|
||||
|
||||
## Overview
|
||||
|
||||
Laputa has two layers of theming:
|
||||
|
||||
1. **Global CSS variables** (`src/index.css`): App-wide colors, borders, backgrounds — used by all components via Tailwind and direct CSS.
|
||||
2. **Editor theme** (`src/theme.json`): Editor-specific typography and styling — converted to CSS variables by `useEditorTheme` and applied to the BlockNote container.
|
||||
|
||||
Currently the app is **light mode only** (dark mode was removed for simplicity).
|
||||
|
||||
## Layer 1: Global CSS Variables
|
||||
|
||||
Defined in `src/index.css` under `:root`:
|
||||
|
||||
### Color Palette
|
||||
|
||||
```css
|
||||
/* Primary brand */
|
||||
--primary: #155DFF; /* Blue — links, accents, active states */
|
||||
--primary-foreground: #FFFFFF;
|
||||
|
||||
/* Text hierarchy */
|
||||
--text-primary: #37352F; /* Main text (Notion-like dark gray) */
|
||||
--text-secondary: #787774; /* Secondary text */
|
||||
--text-muted: #B4B4B4; /* Muted/placeholder text */
|
||||
--text-heading: #37352F; /* Headings */
|
||||
|
||||
/* Backgrounds */
|
||||
--bg-primary: #FFFFFF; /* Main content area */
|
||||
--bg-sidebar: #F7F6F3; /* Sidebar background */
|
||||
--bg-hover: #EBEBEA; /* Hover state */
|
||||
--bg-hover-subtle: #F0F0EF; /* Subtle hover (code blocks) */
|
||||
--bg-selected: #E8F4FE; /* Selected state (blue tint) */
|
||||
|
||||
/* Borders */
|
||||
--border-primary: #E9E9E7; /* Standard borders */
|
||||
|
||||
/* Accent colors */
|
||||
--accent-blue: #155DFF;
|
||||
--accent-green: #00B38B;
|
||||
--accent-orange: #D9730D;
|
||||
--accent-red: #E03E3E;
|
||||
--accent-purple: #A932FF;
|
||||
--accent-yellow: #F0B100;
|
||||
|
||||
/* Light accent backgrounds (for badges/pills) */
|
||||
--accent-blue-light: #155DFF14;
|
||||
--accent-green-light: #00B38B14;
|
||||
--accent-purple-light: #A932FF14;
|
||||
--accent-red-light: #E03E3E14;
|
||||
--accent-yellow-light: #F0B10014;
|
||||
```
|
||||
|
||||
### shadcn/ui Variables
|
||||
|
||||
The app uses [shadcn/ui](https://ui.shadcn.com/) components, which require their own CSS variable naming convention:
|
||||
|
||||
```css
|
||||
--background: #FFFFFF;
|
||||
--foreground: #37352F;
|
||||
--card: #FFFFFF;
|
||||
--card-foreground: #37352F;
|
||||
--primary: #155DFF;
|
||||
--secondary: #EBEBEA;
|
||||
--muted: #F0F0EF;
|
||||
--muted-foreground: #787774;
|
||||
--accent: #EBEBEA;
|
||||
--destructive: #E03E3E;
|
||||
--border: #E9E9E7;
|
||||
--ring: #155DFF;
|
||||
--sidebar: #F7F6F3;
|
||||
```
|
||||
|
||||
### Tailwind v4 Integration
|
||||
|
||||
The `@theme inline` block in `index.css` bridges CSS variables to Tailwind's color system:
|
||||
|
||||
```css
|
||||
@theme inline {
|
||||
--color-background: var(--background);
|
||||
--color-foreground: var(--foreground);
|
||||
--color-primary: var(--primary);
|
||||
/* ... etc */
|
||||
}
|
||||
```
|
||||
|
||||
This enables Tailwind classes like `bg-background`, `text-primary`, `border-border`, etc.
|
||||
|
||||
## Layer 2: Editor Theme (theme.json)
|
||||
|
||||
`src/theme.json` controls the BlockNote editor's typography and element styling. It's a nested JSON object with the following structure:
|
||||
|
||||
### Structure
|
||||
|
||||
```json
|
||||
{
|
||||
"editor": {
|
||||
"fontFamily": "'Inter', -apple-system, BlinkMacSystemFont, sans-serif",
|
||||
"fontSize": 16,
|
||||
"lineHeight": 1.5,
|
||||
"maxWidth": 720,
|
||||
"paddingHorizontal": 40,
|
||||
"paddingVertical": 20,
|
||||
"paragraphSpacing": 8
|
||||
},
|
||||
"headings": {
|
||||
"h1": { "fontSize": 32, "fontWeight": 700, "lineHeight": 1.2, "marginTop": 32, "marginBottom": 12, "color": "var(--text-heading)", "letterSpacing": -0.5 },
|
||||
"h2": { "fontSize": 27, "fontWeight": 600, "lineHeight": 1.4, "marginTop": 28, "marginBottom": 10 },
|
||||
"h3": { "fontSize": 20, "fontWeight": 600, "lineHeight": 1.4, "marginTop": 24, "marginBottom": 8 },
|
||||
"h4": { "fontSize": 20, "fontWeight": 600, "lineHeight": 1.4, "marginTop": 20, "marginBottom": 6 }
|
||||
},
|
||||
"lists": {
|
||||
"bulletSymbol": "\u2022",
|
||||
"bulletSize": 28,
|
||||
"bulletColor": "#177bfd",
|
||||
"indentSize": 24,
|
||||
"itemSpacing": 4,
|
||||
"paddingLeft": 8,
|
||||
"nestedBulletSymbols": ["\u2022", "\u25e6", "\u25aa"],
|
||||
"bulletGap": 6
|
||||
},
|
||||
"checkboxes": {
|
||||
"size": 18,
|
||||
"borderRadius": 3,
|
||||
"checkedColor": "var(--accent-blue)",
|
||||
"uncheckedBorderColor": "var(--text-muted)",
|
||||
"gap": 8
|
||||
},
|
||||
"inlineStyles": {
|
||||
"bold": { "fontWeight": 700, "color": "var(--text-primary)" },
|
||||
"italic": { "fontStyle": "italic" },
|
||||
"strikethrough": { "color": "var(--text-tertiary)", "textDecoration": "line-through" },
|
||||
"code": { "fontFamily": "'SF Mono', 'Fira Code', monospace", "fontSize": 14, "backgroundColor": "var(--bg-hover-subtle)", "paddingHorizontal": 4, "paddingVertical": 2, "borderRadius": 3 },
|
||||
"link": { "color": "var(--accent-blue)", "textDecoration": "underline" },
|
||||
"wikilink": { "color": "var(--accent-blue)", "textDecoration": "none", "borderBottom": "1px dotted var(--accent-blue)", "cursor": "pointer" }
|
||||
},
|
||||
"codeBlocks": {
|
||||
"fontFamily": "'SF Mono', 'Fira Code', monospace",
|
||||
"fontSize": 13,
|
||||
"lineHeight": 1.5,
|
||||
"backgroundColor": "var(--bg-card)",
|
||||
"paddingHorizontal": 16,
|
||||
"paddingVertical": 12,
|
||||
"borderRadius": 6,
|
||||
"marginVertical": 12
|
||||
},
|
||||
"blockquote": {
|
||||
"borderLeftWidth": 3,
|
||||
"borderLeftColor": "var(--accent-blue)",
|
||||
"paddingLeft": 16,
|
||||
"marginVertical": 12,
|
||||
"color": "var(--text-secondary)",
|
||||
"fontStyle": "italic"
|
||||
},
|
||||
"table": {
|
||||
"borderColor": "var(--border-primary)",
|
||||
"headerBackground": "var(--bg-card)",
|
||||
"cellPaddingHorizontal": 12,
|
||||
"cellPaddingVertical": 8,
|
||||
"fontSize": 14
|
||||
},
|
||||
"horizontalRule": {
|
||||
"color": "var(--border-primary)",
|
||||
"marginVertical": 24,
|
||||
"thickness": 1
|
||||
},
|
||||
"colors": {
|
||||
"background": "var(--bg-primary)",
|
||||
"text": "var(--text-primary)",
|
||||
"textSecondary": "var(--text-secondary)",
|
||||
"textMuted": "var(--text-muted)",
|
||||
"heading": "var(--text-heading)",
|
||||
"accent": "var(--accent-blue)",
|
||||
"selection": "var(--bg-selected)",
|
||||
"cursor": "var(--text-primary)"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### How theme.json Becomes CSS
|
||||
|
||||
The `useEditorTheme` hook (`src/hooks/useTheme.ts`) flattens the nested structure into CSS custom properties:
|
||||
|
||||
```typescript
|
||||
function flattenTheme(obj, prefix = '--') {
|
||||
// Recursively flattens:
|
||||
// { editor: { fontSize: 16 } } → { '--editor-font-size': '16px' }
|
||||
// { headings: { h1: { fontSize: 32 } } } → { '--headings-h1-font-size': '32px' }
|
||||
}
|
||||
```
|
||||
|
||||
Key transformations:
|
||||
- **camelCase → kebab-case**: `fontSize` → `font-size`
|
||||
- **Numeric values get `px`**: `16` → `16px`
|
||||
- **Unitless exceptions**: `lineHeight`, `fontWeight`, `opacity` stay as plain numbers
|
||||
- **String values pass through**: `"var(--text-heading)"` → `var(--text-heading)`
|
||||
- **Arrays are skipped**: `nestedBulletSymbols` is ignored in CSS flattening
|
||||
|
||||
The resulting flat map is applied as inline styles on the BlockNote container:
|
||||
|
||||
```tsx
|
||||
<div className="editor__blocknote-container" style={cssVars as React.CSSProperties}>
|
||||
<BlockNoteView editor={editor} theme="light" />
|
||||
</div>
|
||||
```
|
||||
|
||||
### EditorTheme.css
|
||||
|
||||
`src/components/EditorTheme.css` contains CSS selectors that consume these variables to style BlockNote's internal elements (headings, lists, code blocks, etc.). This file uses selectors like `.bn-editor [data-content-type="heading"]` to target BlockNote's rendered DOM.
|
||||
|
||||
## How to Modify Styles
|
||||
|
||||
### Change a global color
|
||||
|
||||
Edit `src/index.css` — find the variable under `:root` and change its value:
|
||||
|
||||
```css
|
||||
/* Before */
|
||||
--primary: #155DFF;
|
||||
/* After */
|
||||
--primary: #FF5500;
|
||||
```
|
||||
|
||||
All components using `text-primary`, `bg-primary`, `var(--primary)`, etc. will update automatically.
|
||||
|
||||
### Change editor typography
|
||||
|
||||
Edit `src/theme.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"editor": {
|
||||
"fontSize": 18, // was 16
|
||||
"fontFamily": "'Georgia', serif" // was Inter
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The `useEditorTheme` hook will automatically regenerate the CSS variables on next render.
|
||||
|
||||
### Change heading sizes
|
||||
|
||||
Edit the `headings` section in `src/theme.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"headings": {
|
||||
"h1": { "fontSize": 36 }, // was 32
|
||||
"h2": { "fontSize": 28 } // was 27
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Change wikilink appearance
|
||||
|
||||
Edit `inlineStyles.wikilink` in `src/theme.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"inlineStyles": {
|
||||
"wikilink": {
|
||||
"color": "var(--accent-purple)",
|
||||
"borderBottom": "2px solid var(--accent-purple)"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Add a new CSS variable
|
||||
|
||||
1. Add it to `:root` in `src/index.css`
|
||||
2. If it needs to be available in Tailwind, add a mapping in the `@theme inline` block
|
||||
3. Reference it anywhere as `var(--my-variable)` in CSS or `theme.json`
|
||||
|
||||
## Design Decisions
|
||||
|
||||
- **CSS variables over Tailwind config**: Colors are defined as CSS variables rather than Tailwind config values, because they need to be shared with non-Tailwind code (BlockNote, inline styles, theme.json).
|
||||
- **theme.json for editor only**: The editor theme is separate from global styles because it controls BlockNote-specific typography that doesn't apply to the rest of the app (sidebar, dialogs, etc.).
|
||||
- **No dark mode (for now)**: Dark mode was removed to simplify the initial build. The CSS variable architecture supports it — just add a `[data-theme="dark"]` or `@media (prefers-color-scheme: dark)` section in `index.css`.
|
||||
- **Inline styles for editor**: The editor theme is applied via inline styles (not a stylesheet) because the values come from JSON and are computed at runtime by the hook.
|
||||
@@ -192,3 +192,4 @@ Broader audiences will follow as the onboarding experience matures and the conve
|
||||
6. **Capture and organize are separate** — the inbox makes unorganized notes visible; Inbox Zero is the discipline
|
||||
7. **Relations as first-class citizens** — connections between notes are as important as the notes themselves
|
||||
8. **Filesystem as the single source of truth** — the app never owns the data; cache and UI state are always derived and reconstructible
|
||||
9. **Convention over system config files** — app configuration and preferences that belong to a note (e.g. type-level UI preferences) are stored in that note's frontmatter using the `_field` underscore convention, not in separate config files or localStorage. Everything that matters lives in the vault as plain text.
|
||||
|
||||
31
docs/adr/0001-tauri-react-stack.md
Normal file
@@ -0,0 +1,31 @@
|
||||
---
|
||||
type: ADR
|
||||
id: "0001"
|
||||
title: "Tauri v2 + React as application stack"
|
||||
status: active
|
||||
date: 2026-02-14
|
||||
---
|
||||
|
||||
## Context
|
||||
|
||||
Laputa is a desktop app for macOS (with iPad as a future target) that reads and writes a vault of markdown files. The app needs a native feel, filesystem access, git integration, and a rich text editor. A single developer (with AI assistance) is building it.
|
||||
|
||||
## Decision
|
||||
|
||||
Use **Tauri v2** (Rust backend + WebView frontend) with **React + TypeScript** for the UI, **BlockNote** for the editor, and **Vitest + Playwright** for testing.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
- **Electron**: heavier runtime (~150MB), slower, but more mature ecosystem. Rejected — Tauri is lighter and has better native integration.
|
||||
- **SwiftUI**: best native macOS/iOS experience, but locks to Apple platforms only, no code sharing with a potential web version, and requires rewriting the entire UI. Rejected for the initial version — revisited in ADR-0005.
|
||||
- **Flutter**: cross-platform but WebView-based editor would have been poor; Dart ecosystem is thin for markdown tooling.
|
||||
- **Pure web app**: no filesystem access, no git, would require a backend server. Rejected — offline-first is a core principle.
|
||||
|
||||
## Consequences
|
||||
|
||||
- React frontend can be shared with a future web version
|
||||
- Rust backend provides safe, fast filesystem/git operations
|
||||
- Tauri v2 supports iOS (beta) — see ADR-0005 for iPad strategy
|
||||
- CodeScene code health monitoring applies to both Rust and TypeScript code
|
||||
- Claude Code can work on both layers without context switching
|
||||
- Triggers re-evaluation if: Tauri iOS proves unstable for production, or if SwiftUI becomes the primary target platform
|
||||
30
docs/adr/0002-filesystem-source-of-truth.md
Normal file
@@ -0,0 +1,30 @@
|
||||
---
|
||||
type: ADR
|
||||
id: "0002"
|
||||
title: "Filesystem as the single source of truth"
|
||||
status: active
|
||||
date: 2026-02-14
|
||||
---
|
||||
|
||||
## Context
|
||||
|
||||
Laputa needs a persistence model. The core question: does the app own the data, or does the filesystem? This affects sync, conflict resolution, offline support, portability, and long-term trust with users.
|
||||
|
||||
## Decision
|
||||
|
||||
**The vault is the source of truth.** The app never owns the data — it only reads and writes `.md` files. All cache, React state, and in-memory representations are derived from the filesystem and must be reconstructible by deleting them. When in doubt, the file on disk wins.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
- **Database-first (SQLite)**: faster queries, easier relationships. Rejected — creates lock-in, makes files unreadable outside the app, complicates sync.
|
||||
- **Cloud-first (proprietary sync)**: easier multi-device. Rejected — zero lock-in is a core principle; git handles sync.
|
||||
- **Hybrid (DB + files)**: DB as primary, files as export. Rejected — two sources of truth always diverge.
|
||||
|
||||
## Consequences
|
||||
|
||||
- Notes are plain markdown files, readable and editable by any text editor
|
||||
- Git provides history, sync, and collaboration for free
|
||||
- Vault can be opened/edited externally without app corruption
|
||||
- App rebuilds cache on startup — acceptable cost for integrity guarantees
|
||||
- No "save" button needed — autosave writes to disk immediately
|
||||
- Triggers re-evaluation if: vault size grows to millions of files and filesystem scanning becomes a bottleneck
|
||||
30
docs/adr/0003-single-note-model.md
Normal file
@@ -0,0 +1,30 @@
|
||||
---
|
||||
type: ADR
|
||||
id: "0003"
|
||||
title: "Single note open at a time (no tabs)"
|
||||
status: active
|
||||
date: 2026-03-24
|
||||
---
|
||||
|
||||
## Context
|
||||
|
||||
The app originally had a tab bar allowing multiple notes to be open simultaneously (similar to a browser or code editor). After building and shipping it, the tab model was found to add significant UI complexity, state management overhead, and confusion — without a proportional benefit for a notes app.
|
||||
|
||||
## Decision
|
||||
|
||||
**Remove the tab bar. Only one note is open at a time.** Navigation history (Back/Forward with Cmd+[/]) replaces tabs for moving between recently visited notes. Closed tab history and `useTabManagement` are removed.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
- **Keep tabs**: familiar UX, allows comparing notes side by side. Rejected — adds ~2000 lines of complexity, confusing state (which tab is "active"?), and breaks the "editor is sacred" principle.
|
||||
- **Tabs + single-note toggle**: configurable per user. Rejected — doubles the state surface and testing burden.
|
||||
- **Split pane (two notes at once)**: useful for reference. Deferred — can be added later without tabs, via a dedicated split layout.
|
||||
|
||||
## Consequences
|
||||
|
||||
- Removes ~2000 lines of code (`TabBar`, `useClosedTabHistory`, `useEditorTabSwap`, `tabLayout`)
|
||||
- `handleSelectNote` replaces the current note instead of adding a tab
|
||||
- Cmd+W (close tab) and Cmd+Shift+T (reopen closed tab) removed from shortcuts
|
||||
- Back/Forward navigation (Cmd+[/Cmd+]) preserves history without tab state
|
||||
- Significant simplification of `App.tsx` and editor state
|
||||
- Triggers re-evaluation if: multi-note workflows become a top user request
|
||||
42
docs/adr/0004-vault-vs-app-settings-storage.md
Normal file
@@ -0,0 +1,42 @@
|
||||
---
|
||||
type: ADR
|
||||
id: "0004"
|
||||
title: "Vault vs app settings for state storage"
|
||||
status: active
|
||||
date: 2026-03-24
|
||||
---
|
||||
|
||||
## Context
|
||||
|
||||
As features were added, there was recurring ambiguity about where to persist configuration and state: in the vault (as frontmatter in `.md` files) or in app settings (`~/.config/com.laputa.app/settings.json` / localStorage). Without a clear rule, some decisions were inconsistent.
|
||||
|
||||
## Decision
|
||||
|
||||
**Ask: "Would the user want this to follow the vault across all their installations?"**
|
||||
|
||||
- If **yes** → store in the vault (as frontmatter in the relevant `.md` file, using the `_` convention for system properties)
|
||||
- If **no** → store in app settings
|
||||
|
||||
Examples:
|
||||
| Data | Storage | Reason |
|
||||
|------|---------|--------|
|
||||
| Note type icon (`_icon`) | Vault frontmatter | Follows the vault everywhere |
|
||||
| Note type color (`_color`) | Vault frontmatter | Follows the vault everywhere |
|
||||
| Note sort preference | Vault frontmatter (type file) | Per-vault, consistent across devices |
|
||||
| API keys (Anthropic, OpenAI) | App settings | Installation-specific |
|
||||
| GitHub token | App settings | Installation-specific |
|
||||
| Window size / zoom | App settings | Device-specific |
|
||||
| Editor zoom level | App settings | Device-specific |
|
||||
| Telemetry consent | App settings | Installation-specific |
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
- **Everything in localStorage**: simple, but breaks cross-device sync for vault-level config.
|
||||
- **Everything in vault**: pure, but makes device-specific settings (zoom, window size) propagate to all devices — confusing.
|
||||
|
||||
## Consequences
|
||||
|
||||
- Config that "belongs to a note or type" lives in frontmatter — readable/diffable in git
|
||||
- The `_` prefix convention (see ABSTRACTIONS.md) distinguishes system properties from user properties
|
||||
- App rebuilds from vault state on open — no stale config files to manage
|
||||
- Triggers re-evaluation if: vault files become too polluted with system frontmatter properties
|
||||
38
docs/adr/0005-tauri-ios-for-ipad.md
Normal file
@@ -0,0 +1,38 @@
|
||||
---
|
||||
type: ADR
|
||||
id: "0005"
|
||||
title: "Tauri v2 iOS for iPad support (vs SwiftUI rewrite)"
|
||||
status: active
|
||||
date: 2026-03-27
|
||||
---
|
||||
|
||||
## Context
|
||||
|
||||
Laputa runs on macOS via Tauri v2. The goal is to also support iPad without changing the stack or redesigning the app from scratch. The core question: extend the existing stack to iOS, or rewrite in SwiftUI for a fully native experience?
|
||||
|
||||
## Decision
|
||||
|
||||
**Use Tauri v2 iOS (beta) for the iPad prototype.** The React frontend stays identical. The Rust backend compiles for iOS with `#[cfg(desktop)]` / `#[cfg(mobile)]` guards for platform-specific features. Desktop-only features (git CLI, macOS menu bar, MCP server, Claude CLI) are stubbed or skipped on mobile.
|
||||
|
||||
The prototype (`feat: add iPad/iOS prototype via Tauri v2 mobile target`, build `b492`) successfully builds and runs on iPad Pro 13" simulator (iOS 18.3.1).
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
- **SwiftUI rewrite**: best native macOS/iPad experience, full App Store integration, native TextKit 2 editor. Rejected for now — would discard all existing React code, Rust backend, 2200+ tests, and Claude Code's accumulated context. Worth revisiting if Laputa becomes iOS-first.
|
||||
- **Capacitor**: replaces Tauri layer, keeps React, but the Rust backend is lost entirely — git and file operations would need reimplementation in JS or Swift.
|
||||
- **React Native + WebView**: wraps the React app in a WebView. Too hacky, performance concerns, App Store review risks.
|
||||
|
||||
## Git on iPad
|
||||
|
||||
`git` CLI is unavailable on iOS. Options for production:
|
||||
- **Option A (recommended)**: `isomorphic-git` — pure JS git implementation, no native dependencies, runs in WebView. Replaces Rust git commands on mobile.
|
||||
- **Option B (prototype)**: Working Copy as iOS Files provider — user manages git separately.
|
||||
- **Option C**: iCloud Drive sync — no git history. Not recommended.
|
||||
|
||||
## Consequences
|
||||
|
||||
- Zero frontend changes needed for basic iPad support
|
||||
- Desktop features (git, MCP, Claude CLI) unavailable on iPad until isomorphic-git is integrated
|
||||
- Tauri v2 iOS is still beta — production stability unknown
|
||||
- App Store distribution requires Apple Developer account and TestFlight
|
||||
- Triggers re-evaluation if: Tauri iOS remains unstable after 6 months, or iPad becomes the primary target (in which case SwiftUI rewrite becomes rational)
|
||||
30
docs/adr/0006-flat-vault-structure.md
Normal file
@@ -0,0 +1,30 @@
|
||||
---
|
||||
type: ADR
|
||||
id: "0006"
|
||||
title: "Flat vault structure (no type-based folders)"
|
||||
status: active
|
||||
date: 2026-03-15
|
||||
---
|
||||
|
||||
## Context
|
||||
|
||||
Originally, notes were organized into type-based subfolders (`project/`, `person/`, `topic/`, etc.). Changing a note's type required moving it between folders, which broke wikilinks, complicated wikilink resolution (paths vs titles), and created friction for users who wanted to reorganize their knowledge. It also made vault scanning more complex and introduced edge cases around folder creation/deletion.
|
||||
|
||||
## Decision
|
||||
|
||||
**All user notes live as flat `.md` files at the vault root. Type is determined solely from the `type:` frontmatter field — never inferred from folder location.** Only a small set of protected folders exist: `type/` (type definition documents), `config/` (meta-configuration), and `attachments/`.
|
||||
|
||||
## Options considered
|
||||
|
||||
- **Option A** (chosen): Flat vault with frontmatter-only type — simple wikilink resolution (title/filename only), no file moves on type change, vault scanning restricted to root + protected folders. Downside: large vaults may look cluttered in Finder.
|
||||
- **Option B**: Keep type-based folders — familiar Obsidian-like structure. Downside: type changes require file moves, wikilink resolution needs path awareness, scanning is recursive and slower.
|
||||
- **Option C**: Hybrid (folders optional, type still from frontmatter) — maximum flexibility. Downside: two ways to do the same thing, confusing for AI agents and automation.
|
||||
|
||||
## Consequences
|
||||
|
||||
- Wikilink resolution is simplified to multi-pass title/filename matching — no path-based matching needed.
|
||||
- Changing a note's type is a frontmatter edit, not a file move.
|
||||
- A `flatten_vault` migration command and wizard were added for existing vaults with type folders.
|
||||
- `vault_health_check` detects stray files in non-protected subfolders.
|
||||
- `scan_vault` only indexes root-level `.md` files plus protected folders — non-protected subdirectories are ignored.
|
||||
- Re-evaluation trigger: if users need nested folder hierarchies for non-type organization (e.g., project-specific subdirectories).
|
||||
30
docs/adr/0007-title-filename-sync.md
Normal file
@@ -0,0 +1,30 @@
|
||||
---
|
||||
type: ADR
|
||||
id: "0007"
|
||||
title: "Title equals filename (slug sync)"
|
||||
status: active
|
||||
date: 2026-03-15
|
||||
---
|
||||
|
||||
## Context
|
||||
|
||||
With the move to a flat vault structure (ADR-0006), filenames became the primary identifier for notes. Previously, titles were extracted from the first H1 heading, which was fragile (users could delete or change the H1 without realizing it affected the note's identity). A clear, deterministic mapping between title and filename was needed.
|
||||
|
||||
## Decision
|
||||
|
||||
**Every note's filename is `slugify(title).md`. The `title` frontmatter field is the source of truth for the human-readable title. On note open, the system syncs the title field to match the filename if they've diverged (filename wins). On rename, both title and filename are updated atomically.**
|
||||
|
||||
## Options considered
|
||||
|
||||
- **Option A** (chosen): `title = slugify(filename)` with bidirectional sync — deterministic, predictable, wikilinks resolve by title/filename stem. Downside: titles with special characters get simplified in filenames.
|
||||
- **Option B**: UUID-based filenames with title only in frontmatter — filenames never change. Downside: vault is unreadable in Finder/terminal, breaks the "plain markdown files" principle.
|
||||
- **Option C**: H1-based title extraction — no explicit title field. Downside: fragile, H1 can be accidentally deleted or changed, decoupled from filename.
|
||||
|
||||
## Consequences
|
||||
|
||||
- `extract_title` reads from frontmatter `title:` field, never from H1. Falls back to `slug_to_title()` (hyphens → spaces, title-case).
|
||||
- `sync_title_on_open` auto-corrects desynced frontmatter on note open.
|
||||
- `rename_note` updates both `title:` frontmatter and filename atomically, plus cross-vault wikilink updates.
|
||||
- The H1 block inside BlockNote is hidden via CSS; a dedicated `TitleField` component above the editor is the primary title editing surface.
|
||||
- Slug collision detection prevents duplicate filenames.
|
||||
- Re-evaluation trigger: if users need filenames that don't match titles (e.g., short slugs for long titles).
|
||||
29
docs/adr/0008-underscore-system-properties.md
Normal file
@@ -0,0 +1,29 @@
|
||||
---
|
||||
type: ADR
|
||||
id: "0008"
|
||||
title: "Underscore convention for system properties"
|
||||
status: active
|
||||
date: 2026-03-24
|
||||
---
|
||||
|
||||
## Context
|
||||
|
||||
As Laputa added more features that store configuration in note frontmatter (pinned properties, type icons, colors, sidebar labels, sort order), the Properties panel became cluttered with internal fields that users shouldn't normally edit. A convention was needed to distinguish user-visible properties from system-internal ones.
|
||||
|
||||
## Decision
|
||||
|
||||
**Any frontmatter field whose name starts with `_` is a system property. It is hidden from the Properties panel, not exposed in search/filters, but remains editable in the raw editor. The frontmatter parser filters out `_*` fields before passing properties to the UI.**
|
||||
|
||||
## Options considered
|
||||
|
||||
- **Option A** (chosen): Underscore prefix convention (`_icon`, `_color`, `_order`, `_pinned_properties`) — simple, readable in raw files, universal rule. Downside: users must know the convention to access system fields.
|
||||
- **Option B**: Separate YAML block or nested `_system:` key — cleaner separation. Downside: more complex parsing, breaks flat key-value frontmatter model.
|
||||
- **Option C**: Store system properties in a separate sidecar file (`.meta.yml`) — complete separation. Downside: doubles the number of files, harder to keep in sync.
|
||||
|
||||
## Consequences
|
||||
|
||||
- All future system-level frontmatter fields must use the `_field_name` convention.
|
||||
- Both Rust (`vault/mod.rs`) and TypeScript (`utils/frontmatter.ts`) parsers filter `_*` fields before passing `properties` to the UI.
|
||||
- Power users can still access and edit system properties via the raw editor.
|
||||
- Type documents use `_icon`, `_color`, `_order`, `_sidebar_label`, `_pinned_properties`.
|
||||
- Re-evaluation trigger: if the number of system properties grows large enough to warrant a structured sub-object.
|
||||
30
docs/adr/0009-keyword-only-search.md
Normal file
@@ -0,0 +1,30 @@
|
||||
---
|
||||
type: ADR
|
||||
id: "0009"
|
||||
title: "Keyword-only search (remove semantic indexing)"
|
||||
status: active
|
||||
date: 2026-03-24
|
||||
---
|
||||
|
||||
## Context
|
||||
|
||||
Laputa previously used QMD (a Go binary) for semantic vector indexing, enabling similarity-based search. This added significant complexity: a bundled Go binary requiring code-signing, an indexing step on vault open, status bar progress tracking, auto-install logic, and a separate `tools/qmd/` directory. The semantic search quality did not justify the operational burden, especially as the AI agent (with MCP vault tools) became a more natural way to do exploratory queries.
|
||||
|
||||
## Decision
|
||||
|
||||
**Remove QMD semantic indexing entirely and keep only keyword-based search. Search uses `walkdir` to scan all `.md` files, matching against titles and content with case-insensitive substring matching and relevance scoring.**
|
||||
|
||||
## Options considered
|
||||
|
||||
- **Option A** (chosen): Keyword-only search via `walkdir` — zero dependencies, no indexing step, instant results, no binary to sign/bundle. Downside: no fuzzy or semantic matching.
|
||||
- **Option B**: Keep QMD semantic search — richer search results, similarity matching. Downside: bundled Go binary, code-signing, indexing latency, maintenance burden.
|
||||
- **Option C**: Replace QMD with a Rust-native embedding library — no external binary. Downside: large model files, cold start time, still needs indexing.
|
||||
|
||||
## Consequences
|
||||
|
||||
- No external search binary to bundle, sign, or install.
|
||||
- No indexing step on vault open — search is instant.
|
||||
- `search_vault` Tauri command scans files directly with `walkdir`, runs in a blocking Tokio task.
|
||||
- Title matches rank higher than content-only matches; exact title matches rank highest.
|
||||
- The AI agent (via MCP `search_notes` tool) provides an alternative for exploratory/semantic queries.
|
||||
- Re-evaluation trigger: if users report keyword search is insufficient for large vaults (9000+ notes).
|
||||
30
docs/adr/0010-dynamic-wikilink-relationship-detection.md
Normal file
@@ -0,0 +1,30 @@
|
||||
---
|
||||
type: ADR
|
||||
id: "0010"
|
||||
title: "Dynamic wikilink relationship detection"
|
||||
status: active
|
||||
date: 2026-03-08
|
||||
---
|
||||
|
||||
## Context
|
||||
|
||||
Laputa needs to support arbitrary relationship types between notes (e.g., `Topics:`, `Key People:`, `Depends on:`). Initially, a hardcoded list `RELATIONSHIP_KEYS` identified which frontmatter fields were relationships. This was fragile — adding a new relationship type required a code change, and users couldn't define their own.
|
||||
|
||||
## Decision
|
||||
|
||||
**The Rust parser dynamically detects relationship fields by scanning all frontmatter keys for values containing `[[wikilinks]]`. Any field with wikilink values is captured in the `relationships` HashMap — no hardcoded field name list needed.**
|
||||
|
||||
## Options considered
|
||||
|
||||
- **Option A** (chosen): Dynamic detection via `[[wikilink]]` presence — zero configuration, extensible, any field name works. Downside: fields with bracket-like content could false-positive (mitigated by the `[[...]]` double-bracket syntax).
|
||||
- **Option B**: Hardcoded `RELATIONSHIP_KEYS` list — simple, predictable. Downside: inflexible, requires code changes for new relationship types.
|
||||
- **Option C**: User-configurable relationship field list in vault config — flexible. Downside: configuration burden, doesn't work out of the box.
|
||||
|
||||
## Consequences
|
||||
|
||||
- Users can define arbitrary relationship types by adding wikilink values to any frontmatter field.
|
||||
- No code change needed when adding new relationship types — convention over configuration.
|
||||
- All relationship fields appear in the Inspector's RelationshipsPanel automatically.
|
||||
- The `relationships` HashMap in `VaultEntry` captures all dynamic relationships.
|
||||
- Standard fields (`belongs_to`, `related_to`) are still recognized for backward compatibility but not privileged.
|
||||
- Re-evaluation trigger: if false-positive detection becomes a problem (e.g., fields with literal `[[` content that aren't relationships).
|
||||
30
docs/adr/0011-mcp-server-for-ai-integration.md
Normal file
@@ -0,0 +1,30 @@
|
||||
---
|
||||
type: ADR
|
||||
id: "0011"
|
||||
title: "MCP server for AI tool integration"
|
||||
status: active
|
||||
date: 2026-02-28
|
||||
---
|
||||
|
||||
## Context
|
||||
|
||||
Laputa's AI features (agent panel, chat) need structured access to vault data — searching notes, reading content, editing frontmatter, and steering the UI. Rather than building a bespoke API, the Model Context Protocol (MCP) provides a standardized tool interface that works with Claude Code, Cursor, and any MCP-compatible client.
|
||||
|
||||
## Decision
|
||||
|
||||
**Laputa ships a Node.js MCP server (`mcp-server/`) that exposes vault operations as 14 tools. It runs on stdio for external clients and on two WebSocket ports (9710 for tool calls, 9711 for UI actions) for the embedded Laputa frontend. Tauri spawns the server on startup and auto-registers it in Claude Code and Cursor configs.**
|
||||
|
||||
## Options considered
|
||||
|
||||
- **Option A** (chosen): Node.js MCP server with stdio + WebSocket dual transport — standard MCP compatibility, works with Claude Code/Cursor out of the box, WebSocket enables real-time UI steering. Downside: Node.js dependency, two extra ports.
|
||||
- **Option B**: Rust-native MCP server — no Node.js dependency. Downside: MCP SDK is JavaScript-first, Rust implementation would be custom and harder to maintain.
|
||||
- **Option C**: Custom REST/gRPC API — full control. Downside: no compatibility with existing AI tool ecosystems, each client needs a custom integration.
|
||||
|
||||
## Consequences
|
||||
|
||||
- Vault tools (search, read, create, edit, delete, link) are available to any MCP-compatible client.
|
||||
- Auto-registration in `~/.claude/mcp.json` and `~/.cursor/mcp.json` means zero setup for users.
|
||||
- The WebSocket bridge enables real-time UI actions (highlight elements, open notes, set filters) from AI tools.
|
||||
- `mcp-server/` is bundled into release builds and spawned as a child process by `mcp.rs`.
|
||||
- Port conflicts on 9710/9711 are handled gracefully (EADDRINUSE tolerance).
|
||||
- Re-evaluation trigger: if MCP SDK gains a Rust implementation that eliminates the Node.js dependency.
|
||||
30
docs/adr/0012-claude-cli-for-ai-agent.md
Normal file
@@ -0,0 +1,30 @@
|
||||
---
|
||||
type: ADR
|
||||
id: "0012"
|
||||
title: "Claude CLI subprocess for AI agent (replacing direct API)"
|
||||
status: active
|
||||
date: 2026-03-01
|
||||
---
|
||||
|
||||
## Context
|
||||
|
||||
The AI agent panel initially called the Anthropic API directly from Rust, managing tool calling loops manually. This required implementing tool execution, conversation state, and streaming — all complex to maintain. Claude CLI (`claude` binary) handles all of this natively, including MCP tool integration, conversation history, and streaming NDJSON output.
|
||||
|
||||
## Decision
|
||||
|
||||
**The AI agent panel spawns Claude CLI as a subprocess via `claude_cli.rs`, passing messages with `--output-format stream-json` and vault MCP config via `--mcp-config`. The frontend parses the NDJSON event stream (Init, TextDelta, ThinkingDelta, ToolStart, ToolDone, Result, Done) for real-time display.**
|
||||
|
||||
## Options considered
|
||||
|
||||
- **Option A** (chosen): Claude CLI subprocess with NDJSON streaming — built-in tool calling, MCP integration, conversation management, no API key needed (CLI handles auth). Downside: requires Claude CLI installed, subprocess management complexity.
|
||||
- **Option B**: Direct Anthropic API with manual tool loop — full control, no external dependency. Downside: must implement tool calling, retries, conversation state, MCP tool bridging.
|
||||
- **Option C**: Use Anthropic Agent SDK from Rust — structured agent framework. Downside: SDK is Python/TypeScript, no Rust support.
|
||||
|
||||
## Consequences
|
||||
|
||||
- The AI agent gets full tool access (MCP vault tools + shell access) without custom tool-calling code.
|
||||
- `claude_cli.rs` manages subprocess lifecycle: spawn, stream events, kill on cancel.
|
||||
- The frontend (`useAiAgent` hook) processes NDJSON events for reasoning blocks, tool action cards, and response display.
|
||||
- File operation detection (from Write/Edit tool inputs) triggers automatic vault reload.
|
||||
- The simpler AI Chat panel still uses the Anthropic API directly for lightweight, no-tools conversations.
|
||||
- Re-evaluation trigger: if Anthropic releases a Rust Agent SDK or if Claude CLI streaming format changes significantly.
|
||||
29
docs/adr/0013-remove-theming-system.md
Normal file
@@ -0,0 +1,29 @@
|
||||
---
|
||||
type: ADR
|
||||
id: "0013"
|
||||
title: "Remove vault-based theming system"
|
||||
status: active
|
||||
date: 2026-03-23
|
||||
---
|
||||
|
||||
## Context
|
||||
|
||||
Laputa had a vault-based theming system where themes were markdown notes in `theme/` with `type: Theme` frontmatter. Each property became a CSS variable. This included a `ThemeManager` hook, theme property editor, dark mode detection, live preview on save, and three built-in themes. The system was complex (spanning Rust seed/create/defaults modules, TypeScript hooks, and CSS variable bridging) and added significant maintenance burden for a feature that most users never customized beyond the defaults.
|
||||
|
||||
## Decision
|
||||
|
||||
**Remove the vault-based theming system entirely. The app uses a single, hardcoded light theme defined in CSS variables (`src/index.css`) and editor theme (`src/theme.json`).** The `theme/` folder, `ThemeManager` hook, theme Rust modules, theme property editor, and dark mode support were all deleted.
|
||||
|
||||
## Options considered
|
||||
|
||||
- **Option A** (chosen): Remove theming, ship a single polished light theme — drastically reduced complexity, fewer files to maintain, no theme-related bugs. Downside: no user customization, no dark mode.
|
||||
- **Option B**: Keep theming but simplify — reduce to light/dark toggle only. Downside: still requires theme loading, CSS variable bridging, and live preview infrastructure.
|
||||
- **Option C**: Keep the full theming system — maximum flexibility. Downside: high maintenance cost for a rarely-used feature, frequent source of bugs (WKWebView reflow issues, CSS var sync).
|
||||
|
||||
## Consequences
|
||||
|
||||
- Deleted: `src-tauri/src/theme/`, `src/hooks/useThemeManager.ts`, `ThemePropertyEditor.tsx`, theme-related commands, `_themes/` legacy support.
|
||||
- Single theme defined in `src/index.css` (CSS variables) and `src/theme.json` (editor typography).
|
||||
- No dark mode support — the app is light-only.
|
||||
- Protected folders reduced: `theme/` is no longer scanned by `scan_vault`.
|
||||
- Re-evaluation trigger: if dark mode becomes a hard requirement for accessibility or user demand.
|
||||
31
docs/adr/0014-git-based-vault-cache.md
Normal file
@@ -0,0 +1,31 @@
|
||||
---
|
||||
type: ADR
|
||||
id: "0014"
|
||||
title: "Git-based incremental vault cache"
|
||||
status: active
|
||||
date: 2026-03-08
|
||||
---
|
||||
|
||||
## Context
|
||||
|
||||
Scanning a vault of 9000+ markdown files on every app launch takes several seconds. A caching strategy was needed that could detect which files changed since the last scan and only re-parse those, while remaining correct even after external edits (e.g., from a text editor or git pull).
|
||||
|
||||
## Decision
|
||||
|
||||
**Use git as the change detection mechanism. The cache stores all `VaultEntry` objects in a JSON file at `~/.laputa/cache/<vault-hash>.json`. On load, it compares the cached git HEAD commit hash with the current one: if the same, only re-parse uncommitted changed files; if different, use `git diff` to find changed files and selectively re-parse. Full rescan only on cache miss or version bump.**
|
||||
|
||||
## Options considered
|
||||
|
||||
- **Option A** (chosen): Git-based incremental cache — leverages existing git infrastructure, precise change detection, handles both committed and uncommitted changes. Downside: requires git-tracked vault, cache invalidation logic is complex.
|
||||
- **Option B**: File modification time (`mtime`) based cache — works without git. Downside: unreliable across filesystems (iCloud, Dropbox), clock skew issues.
|
||||
- **Option C**: File hash (content-based) cache — always correct. Downside: must read every file to compute hash, defeating the purpose of caching.
|
||||
|
||||
## Consequences
|
||||
|
||||
- Cache file stored outside the vault at `~/.laputa/cache/<vault-hash>.json` — never pollutes the user's git repo.
|
||||
- Writes are atomic (write to `.tmp` then rename) to prevent corruption.
|
||||
- Cache version (v5) is bumped on `VaultEntry` field changes to force full rescan.
|
||||
- Legacy `.laputa-cache.json` files inside the vault are auto-migrated and deleted on first run.
|
||||
- `reload_vault` command deletes the cache file before rescanning, guaranteeing fresh data.
|
||||
- Stale cache entries are pruned on vault open (files that no longer exist on disk).
|
||||
- Re-evaluation trigger: if non-git vaults (e.g., iCloud-only) need to be supported.
|
||||
29
docs/adr/0015-auto-save-with-debounce.md
Normal file
@@ -0,0 +1,29 @@
|
||||
---
|
||||
type: ADR
|
||||
id: "0015"
|
||||
title: "Auto-save with 500ms debounce"
|
||||
status: active
|
||||
date: 2026-03-19
|
||||
---
|
||||
|
||||
## Context
|
||||
|
||||
Manual save (Cmd+S) was the only way to persist editor changes. Users occasionally lost work when switching notes or closing the app without saving. An auto-save mechanism was needed that balanced responsiveness (no perceived lag) with disk I/O efficiency (not writing on every keystroke).
|
||||
|
||||
## Decision
|
||||
|
||||
**Notes auto-save with a 500ms debounce after the last keystroke. The `useEditorSave` hook watches for editor content changes and triggers a save after 500ms of inactivity. The same `save_note_content` Rust command is used for both auto-save and manual save.**
|
||||
|
||||
## Options considered
|
||||
|
||||
- **Option A** (chosen): 500ms debounce auto-save — fast enough to feel instant, slow enough to batch rapid keystrokes. Downside: 500ms window where unsaved changes exist.
|
||||
- **Option B**: Save on every change (no debounce) — zero data loss risk. Downside: excessive disk writes, poor performance, frequent git diffs.
|
||||
- **Option C**: Save on note switch / app blur only — minimal disk writes. Downside: data loss if app crashes mid-edit, no live preview of changes in other views.
|
||||
|
||||
## Consequences
|
||||
|
||||
- Users never need to manually save (Cmd+S still works as an immediate save).
|
||||
- Auto-save triggers vault entry updates, keeping the note list, search, and relationships current.
|
||||
- The same save path handles wikilink extraction and frontmatter parsing after save.
|
||||
- Secondary windows (multi-window mode) each have their own auto-save via `useEditorSaveWithLinks`.
|
||||
- Re-evaluation trigger: if 500ms is too aggressive for low-powered devices or network-synced vaults.
|
||||
31
docs/adr/0016-sentry-posthog-telemetry.md
Normal file
@@ -0,0 +1,31 @@
|
||||
---
|
||||
type: ADR
|
||||
id: "0016"
|
||||
title: "Sentry crash reporting + PostHog analytics with consent"
|
||||
status: active
|
||||
date: 2026-03-25
|
||||
---
|
||||
|
||||
## Context
|
||||
|
||||
As Laputa approaches public release, crash reports and usage analytics are needed to identify bugs and understand feature adoption. However, as a personal knowledge management app that handles sensitive data, user privacy is paramount. Any telemetry must be opt-in with clear consent.
|
||||
|
||||
## Decision
|
||||
|
||||
**Integrate Sentry for crash reporting and PostHog for product analytics, both gated behind an explicit consent dialog on first launch. Users can toggle each independently in Settings. No telemetry is sent without affirmative consent.**
|
||||
|
||||
## Options considered
|
||||
|
||||
- **Option A** (chosen): Sentry + PostHog with consent dialog — industry-standard tools, separate crash/analytics toggles, privacy-respecting opt-in. Downside: two external dependencies, two services to manage.
|
||||
- **Option B**: Self-hosted error tracking — full data control. Downside: operational burden, limited analytics features.
|
||||
- **Option C**: No telemetry — simplest, most private. Downside: blind to crashes and usage patterns, harder to prioritize features.
|
||||
|
||||
## Consequences
|
||||
|
||||
- `TelemetryConsentDialog` shows on first launch with accept/decline buttons.
|
||||
- Accepting generates an `anonymous_id` (no PII) and sets `telemetry_consent: true` in settings.
|
||||
- `useTelemetry` hook reactively initializes/tears down Sentry and PostHog based on settings.
|
||||
- Both frontend (`src/lib/telemetry.ts`) and Rust backend (`src-tauri/src/telemetry.rs`) have path scrubbers in `beforeSend` hooks to strip vault paths.
|
||||
- DSN/keys come from `VITE_SENTRY_DSN` / `VITE_POSTHOG_KEY` env vars.
|
||||
- `reinit_telemetry` Tauri command toggles Rust-side Sentry at runtime.
|
||||
- Re-evaluation trigger: if a unified telemetry platform (e.g., OpenTelemetry) could replace both services.
|
||||
30
docs/adr/0017-canary-release-channel.md
Normal file
@@ -0,0 +1,30 @@
|
||||
---
|
||||
type: ADR
|
||||
id: "0017"
|
||||
title: "Canary release channel and local feature flags"
|
||||
status: active
|
||||
date: 2026-03-25
|
||||
---
|
||||
|
||||
## Context
|
||||
|
||||
Shipping new features directly to all users is risky. A mechanism was needed to let early adopters test pre-release builds and to gate experimental features behind flags that can be toggled without a new release.
|
||||
|
||||
## Decision
|
||||
|
||||
**Add a canary release channel alongside stable, with builds from the `canary` branch. Feature flags are localStorage-based (`ff_<name>`) with compile-time defaults, checked via `useFeatureFlag(flag)` hook. The update channel is configurable in Settings.**
|
||||
|
||||
## Options considered
|
||||
|
||||
- **Option A** (chosen): Canary branch + localStorage feature flags — simple, no server infrastructure, users opt in via Settings. Downside: no remote flag management, no gradual rollout percentages.
|
||||
- **Option B**: Server-side feature flags (LaunchDarkly, Unleash) — gradual rollouts, A/B testing. Downside: external dependency, requires server infrastructure, adds latency.
|
||||
- **Option C**: Single release channel with only feature flags — simpler CI. Downside: no way to test full pre-release builds.
|
||||
|
||||
## Consequences
|
||||
|
||||
- `release.yml` builds stable from `main`; `release-canary.yml` builds canary from `canary` branch.
|
||||
- Canary releases produce `latest-canary.json` on GitHub Pages, marked as prerelease.
|
||||
- `useUpdater(channel)` checks the appropriate update manifest.
|
||||
- `useFeatureFlag(flag)` checks localStorage override, then compile-time default. Type-safe via `FeatureFlagName` union.
|
||||
- `update_channel` stored in Settings as `"stable"` or `"canary"`.
|
||||
- Re-evaluation trigger: if user base grows enough to warrant server-side gradual rollouts.
|
||||
31
docs/adr/0018-codescene-code-health-gates.md
Normal file
@@ -0,0 +1,31 @@
|
||||
---
|
||||
type: ADR
|
||||
id: "0018"
|
||||
title: "CodeScene code health gates in CI and git hooks"
|
||||
status: active
|
||||
date: 2026-03-13
|
||||
---
|
||||
|
||||
## Context
|
||||
|
||||
Code complexity tends to increase over time, especially in fast-moving projects. Without automated enforcement, hotspot files (most-edited files) degrade in quality, making future changes harder and buggier. A quantitative code health metric was needed to prevent regression.
|
||||
|
||||
## Decision
|
||||
|
||||
**Enforce CodeScene code health scores as mandatory gates in pre-commit and pre-push hooks. Hotspot Code Health must be >= 9.5 and Average Code Health must be >= 9.31 (project-wide). Both gates block commit/push on failure.** The Boy Scout Rule ("leave every file better than you found it") is enforced as part of every task.
|
||||
|
||||
## Options considered
|
||||
|
||||
- **Option A** (chosen): CodeScene with hard gates — quantitative, automated, catches complexity before it merges. Downside: can slow development if scores are borderline, requires CodeScene API access.
|
||||
- **Option B**: Manual code review for complexity — human judgment. Downside: subjective, inconsistent, doesn't scale.
|
||||
- **Option C**: Linter-only rules (ESLint complexity, Clippy) — built-in, no external service. Downside: coarser metrics, no hotspot awareness, no project-wide average tracking.
|
||||
|
||||
## Consequences
|
||||
|
||||
- Pre-commit hook runs vitest + CodeScene health check before every commit.
|
||||
- Pre-push hook runs the same checks plus Playwright smoke tests.
|
||||
- Developers must fix complexity regressions before committing — even in files they didn't directly modify if changes indirectly affected complexity.
|
||||
- Never use `// eslint-disable`, `#[allow(...)]`, or `as any` to pass the gate.
|
||||
- Common fixes: extract hooks, split large components, reduce function complexity, extract modules.
|
||||
- `.codesceneignore` excludes `tools/`, `e2e/`, `tests/`, `scripts/` from analysis.
|
||||
- Re-evaluation trigger: if CodeScene becomes unavailable or a better code health tool emerges.
|
||||
30
docs/adr/0019-github-device-flow-oauth.md
Normal file
@@ -0,0 +1,30 @@
|
||||
---
|
||||
type: ADR
|
||||
id: "0019"
|
||||
title: "GitHub device flow OAuth for vault sync"
|
||||
status: active
|
||||
date: 2026-02-28
|
||||
---
|
||||
|
||||
## Context
|
||||
|
||||
Laputa supports git-backed vaults synced to GitHub. Users need to authenticate with GitHub to clone repos, push changes, and create new vault repositories. In a desktop app, the standard OAuth redirect flow is awkward (no web server to receive the callback). The Device Authorization Flow is designed for exactly this scenario.
|
||||
|
||||
## Decision
|
||||
|
||||
**Use GitHub's Device Authorization Flow (OAuth device code grant) for GitHub authentication. The user sees a code, opens a browser to enter it, and the app polls for the token. Token is persisted in app settings for future git operations.**
|
||||
|
||||
## Options considered
|
||||
|
||||
- **Option A** (chosen): Device Authorization Flow — designed for desktop/CLI apps, no redirect URI needed, secure (user authenticates in their own browser). Downside: requires user to switch to browser and back.
|
||||
- **Option B**: Personal Access Token (PAT) entry — user generates token on GitHub, pastes it in Settings. Downside: poor UX, users must navigate GitHub settings, token scope management is manual.
|
||||
- **Option C**: OAuth redirect with local server — spawn a local HTTP server to receive the redirect. Downside: port conflicts, firewall issues, more complex implementation.
|
||||
|
||||
## Consequences
|
||||
|
||||
- `GitHubDeviceFlow` component handles the OAuth flow UI (device code display, polling, success/error states).
|
||||
- `GitHubVaultModal` enables cloning existing repos or creating new ones.
|
||||
- Token persisted in `settings.json` under `github_token` / `github_username`.
|
||||
- `SettingsPanel` shows connection status with disconnect option.
|
||||
- Uses Tauri opener plugin to launch the browser for user authentication.
|
||||
- Re-evaluation trigger: if Tauri gains native OAuth redirect support that's simpler than the device flow.
|
||||
32
docs/adr/0020-keyboard-first-design.md
Normal file
@@ -0,0 +1,32 @@
|
||||
---
|
||||
type: ADR
|
||||
id: "0020"
|
||||
title: "Keyboard-first design principle"
|
||||
status: active
|
||||
date: 2026-03-01
|
||||
---
|
||||
|
||||
## Context
|
||||
|
||||
Laputa is a productivity tool for knowledge workers who spend most of their time typing. Mouse-heavy interactions interrupt flow. Every feature should be reachable without touching the mouse, and the app must be fully testable via keyboard events (important for Playwright automation and accessibility).
|
||||
|
||||
## Decision
|
||||
|
||||
**Every feature must be reachable via keyboard. Every command palette entry must also appear in the macOS menu bar (File / Edit / View / Note / Vault / Window). This is both a design principle and a QA requirement.** Navigation, note switching, panel toggling, search, and all commands work via keyboard shortcuts or the Cmd+K command palette.
|
||||
|
||||
## Options considered
|
||||
|
||||
- **Option A** (chosen): Keyboard-first with menu bar parity — full keyboard accessibility, menu bar for discoverability, testable via Playwright keyboard events. Downside: more work per feature (must wire shortcut + menu item + command palette entry).
|
||||
- **Option B**: Mouse-primary with some shortcuts — faster to implement. Downside: poor flow for power users, harder to automate testing.
|
||||
- **Option C**: Keyboard-only (no menu bar) — simplest. Downside: poor discoverability, macOS HIG violation.
|
||||
|
||||
## Consequences
|
||||
|
||||
- `useCommandRegistry` + `useAppCommands` build a centralized command registry with labels, shortcuts, and handlers.
|
||||
- `CommandPalette` (Cmd+K) fuzzy-searches all registered commands.
|
||||
- `menu.rs` defines the native macOS menu bar with accelerators matching keyboard shortcuts.
|
||||
- `useAppKeyboard` registers global keyboard shortcuts.
|
||||
- `useMenuEvents` bridges menu bar clicks to command handlers.
|
||||
- QA uses `osascript` keyboard events for native testing — no mouse, no `cliclick`.
|
||||
- macOS gotcha: `Option+N` produces special characters — use `e.code` or `Cmd+N` instead.
|
||||
- Re-evaluation trigger: if a non-macOS platform (Windows, Linux) is supported and needs different menu/shortcut conventions.
|
||||
30
docs/adr/0021-push-to-main-workflow.md
Normal file
@@ -0,0 +1,30 @@
|
||||
---
|
||||
type: ADR
|
||||
id: "0021"
|
||||
title: "Push directly to main (no PRs or branches)"
|
||||
status: active
|
||||
date: 2026-03-02
|
||||
---
|
||||
|
||||
## Context
|
||||
|
||||
Initially, the project used feature branches and PRs. With a single developer (assisted by Claude Code), the PR overhead — branch creation, rebase churn, merge conflicts from long-lived branches — slowed development without adding review value. The pre-commit and pre-push hooks already enforce tests, linting, type checking, and code health gates.
|
||||
|
||||
## Decision
|
||||
|
||||
**Push directly to main — no PRs, no feature branches. The pre-push hook runs all quality gates (tests, lint, type check, coverage, CodeScene health). Never use `--no-verify`.**
|
||||
|
||||
## Options considered
|
||||
|
||||
- **Option A** (chosen): Push to main with hook-enforced quality gates — fastest iteration, no rebase churn, hooks provide automated review. Downside: no PR-based review, harder to roll back a batch of changes.
|
||||
- **Option B**: Feature branches with PRs — standard team workflow, code review. Downside: rebase churn for a solo developer, PR overhead with no reviewer.
|
||||
- **Option C**: Feature branches without PRs (merge to main locally) — branch isolation without review overhead. Downside: still has merge conflicts, branches diverge.
|
||||
|
||||
## Consequences
|
||||
|
||||
- Commit every 20-30 minutes with conventional commit prefixes (`feat:`, `fix:`, `refactor:`, `test:`, `docs:`).
|
||||
- Pre-commit hook: vitest + CodeScene health check.
|
||||
- Pre-push hook: same + Playwright smoke tests.
|
||||
- No `--no-verify` ever — the hooks are the quality gate.
|
||||
- Reverting changes requires `git revert` (not force push).
|
||||
- Re-evaluation trigger: if a second developer joins and needs code review.
|
||||
32
docs/adr/0022-blocknote-rich-text-editor.md
Normal file
@@ -0,0 +1,32 @@
|
||||
---
|
||||
type: ADR
|
||||
id: "0022"
|
||||
title: "BlockNote as the rich text editor"
|
||||
status: active
|
||||
date: 2026-02-15
|
||||
---
|
||||
|
||||
## Context
|
||||
|
||||
Laputa needs a rich text editor that can render markdown with YAML frontmatter, support custom inline content types (wikilinks), and provide a modern editing experience. The editor must handle the markdown-to-blocks-to-markdown round-trip without data loss.
|
||||
|
||||
## Decision
|
||||
|
||||
**Use BlockNote as the primary rich text editor, with CodeMirror 6 as an alternative raw editing mode. Custom wikilink inline content is defined via `createReactInlineContentSpec`. Markdown round-tripping uses a pre/post-processing pipeline with placeholder tokens for wikilinks.**
|
||||
|
||||
## Options considered
|
||||
|
||||
- **Option A** (chosen): BlockNote + CodeMirror 6 raw mode — BlockNote provides modern block-based editing, CodeMirror gives power users direct markdown access. Downside: wikilink round-tripping requires custom preprocessing pipeline.
|
||||
- **Option B**: ProseMirror directly — maximum control. Downside: much more boilerplate, no block-level abstractions, harder to maintain.
|
||||
- **Option C**: CodeMirror only (no rich text) — simplest, no round-trip issues. Downside: poor UX for non-technical users, no inline previews.
|
||||
- **Option D**: Monaco Editor — rich features, VS Code-like. Downside: heavy, designed for code not prose, no block-level structure.
|
||||
|
||||
## Consequences
|
||||
|
||||
- Custom wikilink type defined in `editorSchema.tsx` via `createReactInlineContentSpec`.
|
||||
- Markdown-to-BlockNote pipeline: `splitFrontmatter()` → `preProcessWikilinks()` → `tryParseMarkdownToBlocks()` → `injectWikilinks()`.
|
||||
- BlockNote-to-Markdown pipeline: `blocksToMarkdownLossy()` → `postProcessWikilinks()` → prepend frontmatter.
|
||||
- Placeholder tokens use `‹` and `›` (U+2039/U+203A) to avoid colliding with markdown syntax.
|
||||
- Raw editor (CodeMirror 6) toggled via Cmd+K → "Raw Editor" or breadcrumb bar button.
|
||||
- The H1 block is hidden via CSS in favor of a dedicated `TitleField` component.
|
||||
- Re-evaluation trigger: if BlockNote's markdown round-tripping degrades or a better block editor emerges.
|
||||
31
docs/adr/0023-repair-vault-auto-bootstrap.md
Normal file
@@ -0,0 +1,31 @@
|
||||
---
|
||||
type: ADR
|
||||
id: "0023"
|
||||
title: "Repair Vault auto-bootstrap pattern"
|
||||
status: active
|
||||
date: 2026-03-07
|
||||
---
|
||||
|
||||
## Context
|
||||
|
||||
As Laputa adds features that depend on vault files (type definitions, config files, agents), users with existing vaults would miss these files. Manually creating them is error-prone. Features must work on both new and existing vaults without user intervention.
|
||||
|
||||
## Decision
|
||||
|
||||
**Every feature that depends on vault files must auto-bootstrap: check if file/folder exists on vault open, create with defaults if missing (silent, idempotent). All bootstrap functions are registered with the central `Cmd+K → "Repair Vault"` command for manual re-creation.**
|
||||
|
||||
## Options considered
|
||||
|
||||
- **Option A** (chosen): Auto-bootstrap on vault open + manual Repair Vault command — works for new and existing vaults, idempotent, no user action needed. Downside: vault may accumulate files the user didn't explicitly create.
|
||||
- **Option B**: Require users to run a setup wizard — explicit, user-controlled. Downside: friction, users forget, new features don't work until setup is run.
|
||||
- **Option C**: Store defaults in app bundle, not vault — no vault files created. Downside: breaks the "vault as source of truth" principle, custom configs can't override defaults.
|
||||
|
||||
## Consequences
|
||||
|
||||
- Type definitions (`type/project.md`, etc.) are seeded on vault open if missing.
|
||||
- Config files (`config/agents.md`, etc.) are seeded on vault open if missing.
|
||||
- `Repair Vault` command (Cmd+K) re-creates all expected files — useful after manual deletion or vault corruption.
|
||||
- All bootstrap operations are silent and idempotent — running twice has no effect.
|
||||
- `getting_started.rs` creates the Getting Started demo vault with all expected structure.
|
||||
- The `vault_health_check` command detects missing or misconfigured vault files.
|
||||
- Re-evaluation trigger: if the number of auto-created files becomes excessive or confusing for users.
|
||||
30
docs/adr/0024-cache-outside-vault.md
Normal file
@@ -0,0 +1,30 @@
|
||||
---
|
||||
type: ADR
|
||||
id: "0024"
|
||||
title: "Vault cache stored outside vault directory"
|
||||
status: active
|
||||
date: 2026-03-08
|
||||
---
|
||||
|
||||
## Context
|
||||
|
||||
The vault cache was originally stored as `.laputa-cache.json` inside the vault directory. This caused problems: the cache file appeared in git status, polluted the user's repo, and could be accidentally committed. It also confused vault scanning (the cache file was itself a file in the vault).
|
||||
|
||||
## Decision
|
||||
|
||||
**Store the vault cache at `~/.laputa/cache/<vault-hash>.json`, outside the vault directory. The vault path is hashed (via `DefaultHasher`) to produce a deterministic filename. Legacy cache files inside the vault are auto-migrated and deleted on first run.**
|
||||
|
||||
## Options considered
|
||||
|
||||
- **Option A** (chosen): External cache at `~/.laputa/cache/` — never pollutes the vault, no git issues, deterministic filename from vault path hash. Downside: separate cleanup needed if vault is deleted.
|
||||
- **Option B**: Cache inside vault with `.gitignore` — simpler, travels with the vault. Downside: .gitignore can be overridden, users may not have one, still appears in file listings.
|
||||
- **Option C**: No persistent cache (in-memory only) — simplest, no file management. Downside: full rescan on every app launch, slow for large vaults.
|
||||
|
||||
## Consequences
|
||||
|
||||
- Cache path: `~/.laputa/cache/<vault-hash>.json` (e.g., `~/.laputa/cache/12345678.json`).
|
||||
- Writes are atomic: write to `.tmp` then rename.
|
||||
- Legacy `.laputa-cache.json` files inside the vault are auto-migrated and deleted.
|
||||
- `reload_vault` command deletes the cache file before rescanning.
|
||||
- The `.laputa/` directory also stores other app data (future: vault metadata, indexes).
|
||||
- Re-evaluation trigger: if vaults need to be portable between machines (cache would need to travel with the vault or be regenerated).
|
||||
30
docs/adr/0025-type-field-canonical.md
Normal file
@@ -0,0 +1,30 @@
|
||||
---
|
||||
type: ADR
|
||||
id: "0025"
|
||||
title: "type: as canonical field (replacing Is A:)"
|
||||
status: active
|
||||
date: 2026-03-08
|
||||
---
|
||||
|
||||
## Context
|
||||
|
||||
The entity type field was originally stored as `Is A:` in frontmatter (e.g., `Is A: Project`), following a natural-language naming convention. This caused problems: the space and colon made it awkward to parse, `is_a` was used internally as the snake_case variant, and `type:` is the standard YAML convention for metadata classification. The field name also confused AI agents that expected standard YAML conventions.
|
||||
|
||||
## Decision
|
||||
|
||||
**Use `type:` as the primary frontmatter field for entity types (e.g., `type: Project`). The legacy `Is A:` field is accepted as an alias for backward compatibility but new notes always use `type:`.** The internal TypeScript/Rust property remains `isA` for backward compatibility.
|
||||
|
||||
## Options considered
|
||||
|
||||
- **Option A** (chosen): `type:` as canonical with `Is A:` as legacy alias — clean, standard YAML convention, AI-readable. Downside: must maintain backward compatibility with existing vaults.
|
||||
- **Option B**: Keep `Is A:` as canonical — no migration needed. Downside: non-standard, awkward parsing, confusing for AI agents.
|
||||
- **Option C**: `kind:` or `category:` — avoids potential YAML type conflicts. Downside: less intuitive, still requires migration from `Is A:`.
|
||||
|
||||
## Consequences
|
||||
|
||||
- New notes use `type: Project` (not `Is A: Project`).
|
||||
- The Rust parser checks `type:` first, falls back to `Is A:` for legacy notes.
|
||||
- `VaultEntry.isA` property name kept for internal backward compatibility.
|
||||
- Type documents in `type/` folder use `type: Type` in their own frontmatter.
|
||||
- Repair Vault migrates legacy `Is A:` fields to `type:` when run.
|
||||
- Re-evaluation trigger: if YAML reserved word `type` causes parsing issues (not observed so far).
|
||||
30
docs/adr/0026-props-down-no-global-state.md
Normal file
@@ -0,0 +1,30 @@
|
||||
---
|
||||
type: ADR
|
||||
id: "0026"
|
||||
title: "Props-down callbacks-up (no global state management)"
|
||||
status: active
|
||||
date: 2026-02-15
|
||||
---
|
||||
|
||||
## Context
|
||||
|
||||
React apps commonly adopt global state management libraries (Redux, Zustand, Jotai, Context) to share state across components. For Laputa, the component tree is relatively shallow (App → panels → sub-components), and the data flow is predictable. Adding a state management library would increase complexity without proportional benefit.
|
||||
|
||||
## Decision
|
||||
|
||||
**No global state management (no Redux, no Context for data). `App.tsx` owns the state and passes it down as props. Child-to-parent communication uses callback props (`onSelectNote`, `onCloseTab`, etc.). Local state uses `useState`/`useReducer`.**
|
||||
|
||||
## Options considered
|
||||
|
||||
- **Option A** (chosen): Props-down, callbacks-up — simple, predictable data flow, easy to trace state changes, no library dependency. Downside: prop drilling through deep trees, verbose parent components.
|
||||
- **Option B**: Redux/Zustand global store — centralized state, easy cross-component access. Downside: boilerplate, indirection, harder to trace state changes, over-engineering for a single-window app.
|
||||
- **Option C**: React Context for shared state — built-in, no library. Downside: re-renders on any context value change, performance issues with large state objects.
|
||||
|
||||
## Consequences
|
||||
|
||||
- `App.tsx` is the state orchestrator — it holds vault entries, active note, sidebar selection, and all top-level state.
|
||||
- Components receive data and callbacks as props — no `useContext` for data access.
|
||||
- Hooks (`useVaultLoader`, `useNoteActions`, `useTabManagement`, etc.) encapsulate state logic but return values consumed by `App.tsx`.
|
||||
- Prop drilling is mitigated by composing hooks and keeping the component tree shallow.
|
||||
- Components are easy to test in isolation (just pass props).
|
||||
- Re-evaluation trigger: if the component tree deepens significantly or cross-cutting state becomes unmanageable with props.
|
||||
32
docs/adr/0027-dual-ai-architecture.md
Normal file
@@ -0,0 +1,32 @@
|
||||
---
|
||||
type: ADR
|
||||
id: "0027"
|
||||
title: "Dual AI architecture (API chat + CLI agent)"
|
||||
status: superseded
|
||||
superseded_by: "0028"
|
||||
date: 2026-03-01
|
||||
---
|
||||
|
||||
## Context
|
||||
|
||||
Laputa needs two distinct AI interaction modes: a lightweight chat for quick questions about the current note (no tool access, fast responses), and a full agent that can search, read, create, and modify vault notes via MCP tools. These have fundamentally different requirements — the chat needs low latency and simple streaming, while the agent needs tool calling, conversation state, and MCP integration.
|
||||
|
||||
## Decision
|
||||
|
||||
**Maintain two separate AI interfaces: AI Chat (AIChatPanel) uses the Anthropic API directly via Rust for simple streaming responses. AI Agent (AiPanel) spawns Claude CLI as a subprocess with MCP vault integration for full tool access.** Both share a context builder (`ai-context.ts`) that provides the active note and linked entries.
|
||||
|
||||
## Options considered
|
||||
|
||||
- **Option A** (chosen): Dual architecture — optimized for each use case. Chat is fast and simple; agent is powerful with tool access. Downside: two codepaths to maintain.
|
||||
- **Option B**: Single agent for both — always use Claude CLI. Downside: overkill for simple questions, slower startup, unnecessary tool overhead.
|
||||
- **Option C**: Single API-based chat with manual tool calling — unified codebase. Downside: complex tool-calling loop implementation, no MCP integration.
|
||||
|
||||
## Consequences
|
||||
|
||||
- AI Chat: `AIChatPanel` + `useAIChat` hook → Rust `ai_chat` command → Anthropic API. Default model: Haiku 3.5 (fast, cheap).
|
||||
- AI Agent: `AiPanel` + `useAiAgent` hook → Rust `claude_cli.rs` → Claude CLI subprocess with MCP config.
|
||||
- Both panels share a toggle in the breadcrumb bar (Sparkle icon).
|
||||
- Context builder (`ai-context.ts`) provides structured JSON with active note, linked notes, open tabs, vault metadata.
|
||||
- Token budget: 60% of 180k context limit (~108k tokens max).
|
||||
- Chat requires an Anthropic API key in settings; agent uses Claude CLI's own authentication.
|
||||
- Re-evaluation trigger: if Anthropic releases an SDK that handles both simple chat and tool calling efficiently.
|
||||
40
docs/adr/0028-cli-agent-only-no-api-key.md
Normal file
@@ -0,0 +1,40 @@
|
||||
---
|
||||
type: ADR
|
||||
id: "0028"
|
||||
title: "CLI agent only — no direct Anthropic API key"
|
||||
status: active
|
||||
date: 2026-03-29
|
||||
supersedes: "0027"
|
||||
---
|
||||
|
||||
## Context
|
||||
|
||||
ADR-0027 introduced a dual AI architecture: a lightweight API-based chat (AIChatPanel) using the Anthropic API directly, and a full CLI agent (AiPanel) spawning Claude CLI as a subprocess with MCP tool access. In practice, the API chat was never shipped to users — the CLI agent covered all use cases and provided a superior experience through tool access and MCP integration. Maintaining two codepaths added complexity, and requiring users to manage an Anthropic API key created friction.
|
||||
|
||||
## Decision
|
||||
|
||||
**Remove the direct Anthropic API integration entirely. AI is available exclusively via CLI agent subprocesses (Claude Code, and in the future Codex or other CLI agents).** No API key field in settings. The CLI agent authenticates via its own mechanism (e.g. `claude` CLI login).
|
||||
|
||||
Removed:
|
||||
- `AIChatPanel` component, `useAIChat` hook
|
||||
- Rust `ai_chat` command and `ai_chat.rs` module
|
||||
- `anthropic_key` field from Settings (Rust and TypeScript)
|
||||
- Vite dev-server Anthropic API proxy (`aiChatProxyPlugin`, `aiAgentProxyPlugin`)
|
||||
|
||||
Kept:
|
||||
- `AiPanel` + `useAiAgent` — Claude CLI subprocess with MCP vault integration
|
||||
- Shared utilities in `ai-chat.ts` (`trimHistory`, `formatMessageWithHistory`, `streamClaudeChat`, etc.)
|
||||
- `Cmd+I` keyboard shortcut and menu item for toggling the AI panel
|
||||
|
||||
## Options considered
|
||||
|
||||
- **Option A** (chosen): Remove API chat, keep CLI agent only. Simplifies codebase, removes API key management, single codepath.
|
||||
- **Option B**: Keep both but hide API chat behind feature flag. Adds dead code weight without benefit.
|
||||
- **Option C**: Replace CLI agent with API chat + manual tool calling. Loses MCP integration and Claude CLI features.
|
||||
|
||||
## Consequences
|
||||
|
||||
- Users no longer need to obtain or manage an Anthropic API key
|
||||
- Existing saved API keys are silently ignored (the field no longer exists in the Settings struct; serde skips unknown fields on deserialization)
|
||||
- Future CLI agents (Codex, etc.) can plug into the same `AiPanel` architecture
|
||||
- If a lightweight chat mode is needed later, it should be built as a CLI agent mode, not a separate API integration
|
||||
29
docs/adr/0029-domain-command-builder-pattern.md
Normal file
@@ -0,0 +1,29 @@
|
||||
---
|
||||
type: ADR
|
||||
id: "0029"
|
||||
title: "Domain command builder pattern for useCommandRegistry"
|
||||
status: active
|
||||
date: 2026-03-30
|
||||
---
|
||||
|
||||
## Context
|
||||
|
||||
`useCommandRegistry` was a 224-line "brain method" (CodeScene hotspot) that defined all command palette commands inline: navigation, note actions, git operations, view toggles, settings, type management, and filter controls. This monolithic structure scored 39 on CodeScene's complexity scale (target: ≤9.5 for hotspots), making it increasingly hard to add new commands without touching the central file.
|
||||
|
||||
## Decision
|
||||
|
||||
**Split command definitions into focused domain modules under `src/hooks/commands/`, each exporting a `build*Commands(config)` factory function. `useCommandRegistry` becomes a thin assembler that calls each builder and merges the results.** Domain modules: `navigationCommands`, `noteCommands`, `gitCommands`, `viewCommands`, `settingsCommands`, `typeCommands`, `filterCommands`. Shared types live in `commands/types.ts`; public API re-exported from `commands/index.ts`.
|
||||
|
||||
## Options considered
|
||||
|
||||
- **Option A** (chosen): Domain builder modules — each module owns its command shape and receives typed config. `useCommandRegistry` is pure assembly. All new files score 9.58–10.0. Downside: more files to navigate.
|
||||
- **Option B**: Split by file but keep one large hook calling sub-hooks — sub-hooks still need shared state passed down, similar coupling. No real complexity win.
|
||||
- **Option C**: Register commands imperatively via a global registry — decouples callers entirely. Downside: harder to trace, no TypeScript inference at the registration site, over-engineering for current scale.
|
||||
|
||||
## Consequences
|
||||
|
||||
- Adding a new command means editing the relevant domain module (e.g. `noteCommands.ts`) only, not touching the assembler.
|
||||
- Each domain module receives only the config it needs — explicit, typed interface, no hook dependency.
|
||||
- `useCommandRegistry` reduced from 224 lines to a thin assembler.
|
||||
- Pattern is consistent with the Rust commands/ module split (ADR-0030).
|
||||
- Re-evaluation trigger: if command count grows to the point where the assembler itself becomes a complexity hotspot.
|
||||
29
docs/adr/0030-rust-commands-module-split.md
Normal file
@@ -0,0 +1,29 @@
|
||||
---
|
||||
type: ADR
|
||||
id: "0030"
|
||||
title: "Rust commands/ module split by domain"
|
||||
status: active
|
||||
date: 2026-03-30
|
||||
---
|
||||
|
||||
## Context
|
||||
|
||||
`src-tauri/src/commands.rs` grew to 937 lines as Tauri command handlers accumulated for vault CRUD, git/GitHub sync, AI, system, and window operations. All commands shared a single file with no domain separation, making it hard to navigate, review, and extend. The file was a CodeScene hotspot dragging down overall code health.
|
||||
|
||||
## Decision
|
||||
|
||||
**Replace `commands.rs` with a `commands/` module split by domain: `vault.rs`, `git.rs`, `github.rs`, `ai.rs`, `system.rs`, and `mod.rs` (shared utilities + re-exports).** Each file owns the Tauri command handlers for its domain and the `#[cfg(desktop)]` / `#[cfg(mobile)]` stubs for platform-conditional availability. `mod.rs` is kept thin (≤100 lines) with no command logic — only re-exports and shared helpers (`expand_tilde`, `parse_build_label`).
|
||||
|
||||
## Options considered
|
||||
|
||||
- **Option A** (chosen): Domain-based module split — mirrors the TypeScript `hooks/commands/` pattern (ADR-0029). Each file is independently reviewable and scores well on code health. Downside: more files to navigate.
|
||||
- **Option B**: Split by platform (`desktop.rs`, `mobile.rs`) — aligns with `#[cfg(...)]` guards but mixes domain concerns. Harder to find a specific command.
|
||||
- **Option C**: Keep monolith but add section comments — zero file-count cost, but doesn't solve complexity or reviewability.
|
||||
|
||||
## Consequences
|
||||
|
||||
- `github.rs` separates GitHub OAuth/API commands from git sync commands (`git.rs`), matching the underlying Rust module split (`github/` vs `git/`).
|
||||
- Platform stubs (`#[cfg(mobile)]` error returns) live alongside the desktop implementation in the same domain file.
|
||||
- `mod.rs` re-exports all command functions so `lib.rs` `invoke_handler!` registration is unchanged.
|
||||
- New Tauri commands go into the appropriate domain file; if no domain fits, create a new one rather than putting it in `mod.rs`.
|
||||
- Re-evaluation trigger: if a single domain file (e.g. `vault.rs`) itself grows beyond ~300 lines and becomes a hotspot.
|
||||
30
docs/adr/0031-full-app-for-note-windows.md
Normal file
@@ -0,0 +1,30 @@
|
||||
---
|
||||
type: ADR
|
||||
id: "0031"
|
||||
title: "Full App instance for secondary note windows"
|
||||
status: active
|
||||
date: 2026-03-31
|
||||
---
|
||||
|
||||
## Context
|
||||
|
||||
Laputa supports opening a note in a secondary window ("Open in New Window"). The original implementation used a dedicated `NoteWindow` component — a thin shell that rendered only the editor, duplicating some App-level logic (vault loading, settings, keyboard shortcuts) in a simplified but diverging form. As the main App gained features (properties editing, zoom, command palette, keyboard shortcuts), the `NoteWindow` shell fell behind, requiring ongoing maintenance to keep parity.
|
||||
|
||||
## Decision
|
||||
|
||||
**Remove `NoteWindow` and render the full `App` component in secondary note windows.** The window type is detected at startup via URL query parameters (`?window=note&path=...&vault=...`). When in note-window mode, the App initialises with panels hidden (sidebar collapsed, inspector collapsed) and auto-opens the target note once vault entries load. The window title is kept in sync with the active note title via the Tauri window API.
|
||||
|
||||
## Options considered
|
||||
|
||||
- **Keep `NoteWindow` shell** (status quo): lower initial bundle weight per window, but divergence grows with every main-App feature. Rejected — maintenance cost dominates.
|
||||
- **Full `App` instance with URL-param mode** (chosen): complete feature parity for free; single code path for all window types. Trade-off: slightly heavier startup for secondary windows (full vault load), acceptable given local filesystem speed.
|
||||
- **IPC-driven secondary window (no vault reload)**: secondary window subscribes to primary window's vault state via Tauri events. Maximum efficiency, avoids double vault reads. Deferred — requires significant IPC plumbing; can be layered on top later without changing the rendering model.
|
||||
|
||||
## Consequences
|
||||
|
||||
- Removes ~163 lines (`NoteWindow.tsx` deleted entirely)
|
||||
- Secondary note windows get full feature parity: all keyboard shortcuts, properties panel, zoom, command palette, diff mode, raw editor
|
||||
- `useLayoutPanels` gains an `initialInspectorCollapsed` option to support the hidden-panel initial state
|
||||
- A new `src/utils/windowMode.ts` utility encapsulates URL-param detection — single source of truth for window-type logic
|
||||
- Vault is loaded independently in each note window (no shared state with the main window); writes go to the same filesystem so eventual consistency is maintained via file-watching
|
||||
- Triggers re-evaluation if: multiple simultaneous note windows cause measurable vault-read contention, or if IPC-driven shared-state windows become a product requirement
|
||||
29
docs/adr/0032-status-bar-for-git-actions.md
Normal file
@@ -0,0 +1,29 @@
|
||||
---
|
||||
type: ADR
|
||||
id: "0032"
|
||||
title: "Git actions (Changes, Pulse, Commit) in status bar, not sidebar"
|
||||
status: active
|
||||
date: 2026-03-31
|
||||
---
|
||||
|
||||
## Context
|
||||
|
||||
The Laputa sidebar originally surfaced git-related affordances — a "Changes" nav item (visible when modified files > 0), a "Pulse" nav item, and a "Commit & Push" button — alongside the note-type navigation filters and sections. This mixed two concerns in the sidebar: **navigation** (where to go) and **git status / actions** (what changed, what to do). As the sidebar grew, the git items created visual noise and made the nav hierarchy harder to scan.
|
||||
|
||||
## Decision
|
||||
|
||||
**Move Changes, Pulse, and Commit & Push out of the sidebar and into the bottom status bar.** The status bar shows a GitDiff icon with an orange count badge for modified files; a Pulse icon sits next to it. Commit & Push is accessible via an icon button beside the Changes indicator. The sidebar now contains only navigation items (filters and type sections).
|
||||
|
||||
## Options considered
|
||||
|
||||
- **Keep git items in sidebar** (status quo): familiar placement, visible at all times. Rejected — mixes navigation and action concerns; sidebar becomes harder to scan.
|
||||
- **Status bar** (chosen): consistent with app conventions (build number, sync status, vault switcher already live there); persistent but unobtrusive; follows macOS app patterns where status/action items live at window bottom.
|
||||
- **Toolbar / breadcrumb bar**: would require a new chrome layer or polluting the per-note breadcrumb with global git state. Rejected.
|
||||
|
||||
## Consequences
|
||||
|
||||
- Sidebar props `modifiedCount`, `onCommitPush`, `isGitVault` removed; sidebar renders navigation-only
|
||||
- `StatusBar` gains `onClickPending`, `onClickPulse`, `onCommitPush`, `isGitVault` props
|
||||
- Sidebar tests for Changes/Pulse/Commit button removed; StatusBar tests extended
|
||||
- Users find Commit & Push in the status bar (same location as sync indicators) rather than bottom of sidebar — small discoverability change, offset by status bar being always visible regardless of sidebar collapsed state
|
||||
- Triggers re-evaluation if: user research shows git actions are hard to discover in the status bar
|
||||
33
docs/adr/0033-subfolder-scanning-and-folder-tree.md
Normal file
@@ -0,0 +1,33 @@
|
||||
---
|
||||
type: ADR
|
||||
id: "0033"
|
||||
title: "Subfolder scanning and folder tree navigation"
|
||||
status: active
|
||||
date: 2026-03-31
|
||||
---
|
||||
|
||||
## Context
|
||||
|
||||
Supersedes the scanning constraint in [ADR-0006](0006-flat-vault-structure.md) which limited vault indexing to root-level `.md` files plus protected folders (`attachments/`, `assets/`).
|
||||
|
||||
Users with folder-based workflows (PARA, Zettelkasten with folders, project directories) could not see or filter notes by directory. The vault scanner silently ignored all subdirectory `.md` files, making Laputa unsuitable for vaults with any folder structure.
|
||||
|
||||
## Decision
|
||||
|
||||
**Extend the Rust vault scanner to index `.md` files in all visible subdirectories, and expose the vault's folder tree via a new `list_vault_folders` Tauri command so the sidebar can render a collapsible FOLDERS section.**
|
||||
|
||||
Hidden directories (names starting with `.`, plus `.git` and `.laputa`) are excluded from both scanning and the folder tree.
|
||||
|
||||
## Options considered
|
||||
|
||||
- **Option A** (chosen): Scan all subdirectories with `walkdir`, expose separate `list_vault_folders` command — simple, no schema changes to VaultEntry, folder tree is lightweight and independent of the entry cache.
|
||||
- **Option B**: Add a `folder` field to VaultEntry and derive the tree on the frontend — couples folder metadata to the entry cache, complicates cache invalidation when folders are created/deleted without file changes.
|
||||
- **Option C**: Keep flat scanning, add a "virtual folders" feature that groups by path prefix from frontmatter — doesn't solve the core problem of missing notes in subdirectories.
|
||||
|
||||
## Consequences
|
||||
|
||||
- All `.md` files in the vault are now indexed regardless of depth — vaults with many non-note `.md` files (e.g. node_modules) will see spurious entries. Mitigation: hidden directories are already excluded; users can add a `.laputaignore` in the future if needed.
|
||||
- The git-based cache in `cache.rs` already uses `walkdir` for change detection, so this change aligns scanning with caching.
|
||||
- `SidebarSelection` gains a new `{ kind: 'folder'; path: string }` variant — all exhaustive switches on selection kind must handle it.
|
||||
- ADR-0006's "flat vault" principle is relaxed: notes can now live in subdirectories. Type definitions still live in `type/` at the root.
|
||||
- Re-evaluate if users request recursive folder filtering (currently only direct children are shown when a folder is selected).
|
||||
91
docs/adr/README.md
Normal file
@@ -0,0 +1,91 @@
|
||||
# Architecture Decision Records
|
||||
|
||||
This folder contains Architecture Decision Records (ADRs) for the Laputa app.
|
||||
|
||||
## Format
|
||||
|
||||
Each ADR is a markdown note with YAML frontmatter. Template:
|
||||
|
||||
```markdown
|
||||
---
|
||||
type: ADR
|
||||
id: "0001"
|
||||
title: "Short decision title"
|
||||
status: proposed # proposed | active | superseded | retired
|
||||
date: YYYY-MM-DD
|
||||
superseded_by: "0007" # only if status: superseded
|
||||
---
|
||||
|
||||
## Context
|
||||
What situation led to this decision? What forces and constraints are at play?
|
||||
|
||||
## Decision
|
||||
**What was decided.** State it clearly in one or two sentences — bold so it stands out.
|
||||
|
||||
## Options considered
|
||||
- **Option A** (chosen): brief description — pros / cons
|
||||
- **Option B**: brief description — pros / cons
|
||||
- **Option C**: brief description — pros / cons
|
||||
|
||||
## Consequences
|
||||
What becomes easier or harder as a result?
|
||||
What are the positive and negative ramifications?
|
||||
What would trigger re-evaluation of this decision?
|
||||
|
||||
## Advice
|
||||
*(optional)* Input received before making this decision — who was consulted, what they said, when.
|
||||
Omit if the decision was made unilaterally with no external input.
|
||||
```
|
||||
|
||||
### Status lifecycle
|
||||
|
||||
```
|
||||
proposed → active → superseded
|
||||
↘ retired (decision no longer relevant, not replaced)
|
||||
```
|
||||
|
||||
## Rules
|
||||
|
||||
- One decision per file
|
||||
- Files named `NNNN-short-title.md` (monotonic numbering)
|
||||
- Once `active`, never edit — supersede instead
|
||||
- When superseded: update `status: superseded` and add `superseded_by: "NNNN"`
|
||||
- ARCHITECTURE.md reflects the current state (active decisions only)
|
||||
|
||||
## Index
|
||||
|
||||
| ID | Title | Status |
|
||||
|----|-------|--------|
|
||||
| [0001](0001-tauri-react-stack.md) | Tauri v2 + React as application stack | active |
|
||||
| [0002](0002-filesystem-source-of-truth.md) | Filesystem as the single source of truth | active |
|
||||
| [0003](0003-single-note-model.md) | Single note open at a time (no tabs) | active |
|
||||
| [0004](0004-vault-vs-app-settings-storage.md) | Vault vs app settings for state storage | active |
|
||||
| [0005](0005-tauri-ios-for-ipad.md) | Tauri v2 iOS for iPad support (vs SwiftUI rewrite) | active |
|
||||
| [0006](0006-flat-vault-structure.md) | Flat vault structure (no type-based folders) | active |
|
||||
| [0007](0007-title-filename-sync.md) | Title equals filename (slug sync) | active |
|
||||
| [0008](0008-underscore-system-properties.md) | Underscore convention for system properties | active |
|
||||
| [0009](0009-keyword-only-search.md) | Keyword-only search (remove semantic indexing) | active |
|
||||
| [0010](0010-dynamic-wikilink-relationship-detection.md) | Dynamic wikilink relationship detection | active |
|
||||
| [0011](0011-mcp-server-for-ai-integration.md) | MCP server for AI tool integration | active |
|
||||
| [0012](0012-claude-cli-for-ai-agent.md) | Claude CLI subprocess for AI agent | active |
|
||||
| [0013](0013-remove-theming-system.md) | Remove vault-based theming system | active |
|
||||
| [0014](0014-git-based-vault-cache.md) | Git-based incremental vault cache | active |
|
||||
| [0015](0015-auto-save-with-debounce.md) | Auto-save with 500ms debounce | active |
|
||||
| [0016](0016-sentry-posthog-telemetry.md) | Sentry + PostHog telemetry with consent | active |
|
||||
| [0017](0017-canary-release-channel.md) | Canary release channel and feature flags | active |
|
||||
| [0018](0018-codescene-code-health-gates.md) | CodeScene code health gates in CI | active |
|
||||
| [0019](0019-github-device-flow-oauth.md) | GitHub device flow OAuth for vault sync | active |
|
||||
| [0020](0020-keyboard-first-design.md) | Keyboard-first design principle | active |
|
||||
| [0021](0021-push-to-main-workflow.md) | Push directly to main (no PRs) | active |
|
||||
| [0022](0022-blocknote-rich-text-editor.md) | BlockNote as the rich text editor | active |
|
||||
| [0023](0023-repair-vault-auto-bootstrap.md) | Repair Vault auto-bootstrap pattern | active |
|
||||
| [0024](0024-cache-outside-vault.md) | Vault cache stored outside vault directory | active |
|
||||
| [0025](0025-type-field-canonical.md) | type: as canonical field (replacing Is A:) | active |
|
||||
| [0026](0026-props-down-no-global-state.md) | Props-down callbacks-up (no global state) | active |
|
||||
| [0027](0027-dual-ai-architecture.md) | Dual AI architecture (API chat + CLI agent) | superseded |
|
||||
| [0028](0028-cli-agent-only-no-api-key.md) | CLI agent only — no direct Anthropic API key | active |
|
||||
| [0029](0029-domain-command-builder-pattern.md) | Domain command builder pattern for useCommandRegistry | active |
|
||||
| [0030](0030-rust-commands-module-split.md) | Rust commands/ module split by domain | active |
|
||||
| [0031](0031-full-app-for-note-windows.md) | Full App instance for secondary note windows | active |
|
||||
| [0032](0032-status-bar-for-git-actions.md) | Git actions (Changes, Pulse, Commit) in status bar, not sidebar | active |
|
||||
| [0033](0033-subfolder-scanning-and-folder-tree.md) | Subfolder scanning and folder tree navigation | active |
|
||||
@@ -6,7 +6,7 @@ import tseslint from 'typescript-eslint'
|
||||
import { defineConfig, globalIgnores } from 'eslint/config'
|
||||
|
||||
export default defineConfig([
|
||||
globalIgnores(['dist', 'coverage', 'src-tauri/resources/', 'src-tauri/target/', 'tools/']),
|
||||
globalIgnores(['dist', 'coverage', 'src-tauri/resources/', 'src-tauri/target/', 'src-tauri/gen/', 'tools/']),
|
||||
{
|
||||
files: ['**/*.{ts,tsx}'],
|
||||
extends: [
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||
<link href="https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@400;500;600&family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet" />
|
||||
<link href="https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@400;500;600&family=Inter:wght@400;500;600;700&family=JetBrains+Mono&display=swap" rel="stylesheet" />
|
||||
<title>laputa-scaffold</title>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
@@ -32,6 +32,7 @@
|
||||
"@dnd-kit/core": "^6.3.1",
|
||||
"@dnd-kit/sortable": "^10.0.0",
|
||||
"@dnd-kit/utilities": "^3.2.2",
|
||||
"@lezer/highlight": "^1.2.3",
|
||||
"@mantine/core": "^8.3.14",
|
||||
"@phosphor-icons/react": "^2.1.10",
|
||||
"@radix-ui/react-dialog": "^1.1.15",
|
||||
@@ -41,6 +42,7 @@
|
||||
"@radix-ui/react-slot": "^1.2.4",
|
||||
"@radix-ui/react-tabs": "^1.1.13",
|
||||
"@radix-ui/react-tooltip": "^1.2.8",
|
||||
"@sentry/browser": "^10.45.0",
|
||||
"@tailwindcss/vite": "^4.1.18",
|
||||
"@tauri-apps/api": "^2.10.1",
|
||||
"@tauri-apps/plugin-dialog": "^2.6.0",
|
||||
@@ -53,6 +55,7 @@
|
||||
"katex": "^0.16.28",
|
||||
"lowlight": "^3.3.0",
|
||||
"lucide-react": "^0.564.0",
|
||||
"posthog-js": "^1.363.5",
|
||||
"radix-ui": "^1.4.3",
|
||||
"react": "^19.2.0",
|
||||
"react-day-picker": "^9.13.2",
|
||||
|
||||
@@ -2,7 +2,7 @@ import { defineConfig } from '@playwright/test'
|
||||
|
||||
export default defineConfig({
|
||||
testDir: './tests/smoke',
|
||||
timeout: 15_000,
|
||||
timeout: 20_000,
|
||||
retries: 2,
|
||||
workers: 1,
|
||||
use: {
|
||||
|
||||
366
pnpm-lock.yaml
generated
@@ -47,6 +47,9 @@ importers:
|
||||
'@dnd-kit/utilities':
|
||||
specifier: ^3.2.2
|
||||
version: 3.2.2(react@19.2.4)
|
||||
'@lezer/highlight':
|
||||
specifier: ^1.2.3
|
||||
version: 1.2.3
|
||||
'@mantine/core':
|
||||
specifier: ^8.3.14
|
||||
version: 8.3.14(@mantine/hooks@8.3.14(react@19.2.4))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
|
||||
@@ -74,6 +77,9 @@ importers:
|
||||
'@radix-ui/react-tooltip':
|
||||
specifier: ^1.2.8
|
||||
version: 1.2.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
|
||||
'@sentry/browser':
|
||||
specifier: ^10.45.0
|
||||
version: 10.45.0
|
||||
'@tailwindcss/vite':
|
||||
specifier: ^4.1.18
|
||||
version: 4.1.18(vite@7.3.1(@types/node@24.10.13)(jiti@2.6.1)(lightningcss@1.30.2))
|
||||
@@ -110,6 +116,9 @@ importers:
|
||||
lucide-react:
|
||||
specifier: ^0.564.0
|
||||
version: 0.564.0(react@19.2.4)
|
||||
posthog-js:
|
||||
specifier: ^1.363.5
|
||||
version: 1.363.5
|
||||
radix-ui:
|
||||
specifier: ^1.4.3
|
||||
version: 1.4.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
|
||||
@@ -179,7 +188,7 @@ importers:
|
||||
version: 5.1.4(vite@7.3.1(@types/node@24.10.13)(jiti@2.6.1)(lightningcss@1.30.2))
|
||||
'@vitest/coverage-v8':
|
||||
specifier: ^4.0.18
|
||||
version: 4.0.18(vitest@4.0.18(@types/node@24.10.13)(jiti@2.6.1)(jsdom@28.0.0)(lightningcss@1.30.2))
|
||||
version: 4.0.18(vitest@4.0.18(@opentelemetry/api@1.9.0)(@types/node@24.10.13)(jiti@2.6.1)(jsdom@28.0.0)(lightningcss@1.30.2))
|
||||
esbuild:
|
||||
specifier: ^0.27.3
|
||||
version: 0.27.3
|
||||
@@ -215,7 +224,7 @@ importers:
|
||||
version: 7.3.1(@types/node@24.10.13)(jiti@2.6.1)(lightningcss@1.30.2)
|
||||
vitest:
|
||||
specifier: ^4.0.18
|
||||
version: 4.0.18(@types/node@24.10.13)(jiti@2.6.1)(jsdom@28.0.0)(lightningcss@1.30.2)
|
||||
version: 4.0.18(@opentelemetry/api@1.9.0)(@types/node@24.10.13)(jiti@2.6.1)(jsdom@28.0.0)(lightningcss@1.30.2)
|
||||
ws:
|
||||
specifier: ^8.19.0
|
||||
version: 8.19.0
|
||||
@@ -787,6 +796,78 @@ packages:
|
||||
'@cfworker/json-schema':
|
||||
optional: true
|
||||
|
||||
'@opentelemetry/api-logs@0.208.0':
|
||||
resolution: {integrity: sha512-CjruKY9V6NMssL/T1kAFgzosF1v9o6oeN+aX5JB/C/xPNtmgIJqcXHG7fA82Ou1zCpWGl4lROQUKwUNE1pMCyg==}
|
||||
engines: {node: '>=8.0.0'}
|
||||
|
||||
'@opentelemetry/api@1.9.0':
|
||||
resolution: {integrity: sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==}
|
||||
engines: {node: '>=8.0.0'}
|
||||
|
||||
'@opentelemetry/core@2.2.0':
|
||||
resolution: {integrity: sha512-FuabnnUm8LflnieVxs6eP7Z383hgQU4W1e3KJS6aOG3RxWxcHyBxH8fDMHNgu/gFx/M2jvTOW/4/PHhLz6bjWw==}
|
||||
engines: {node: ^18.19.0 || >=20.6.0}
|
||||
peerDependencies:
|
||||
'@opentelemetry/api': '>=1.0.0 <1.10.0'
|
||||
|
||||
'@opentelemetry/core@2.6.0':
|
||||
resolution: {integrity: sha512-HLM1v2cbZ4TgYN6KEOj+Bbj8rAKriOdkF9Ed3tG25FoprSiQl7kYc+RRT6fUZGOvx0oMi5U67GoFdT+XUn8zEg==}
|
||||
engines: {node: ^18.19.0 || >=20.6.0}
|
||||
peerDependencies:
|
||||
'@opentelemetry/api': '>=1.0.0 <1.10.0'
|
||||
|
||||
'@opentelemetry/exporter-logs-otlp-http@0.208.0':
|
||||
resolution: {integrity: sha512-jOv40Bs9jy9bZVLo/i8FwUiuCvbjWDI+ZW13wimJm4LjnlwJxGgB+N/VWOZUTpM+ah/awXeQqKdNlpLf2EjvYg==}
|
||||
engines: {node: ^18.19.0 || >=20.6.0}
|
||||
peerDependencies:
|
||||
'@opentelemetry/api': ^1.3.0
|
||||
|
||||
'@opentelemetry/otlp-exporter-base@0.208.0':
|
||||
resolution: {integrity: sha512-gMd39gIfVb2OgxldxUtOwGJYSH8P1kVFFlJLuut32L6KgUC4gl1dMhn+YC2mGn0bDOiQYSk/uHOdSjuKp58vvA==}
|
||||
engines: {node: ^18.19.0 || >=20.6.0}
|
||||
peerDependencies:
|
||||
'@opentelemetry/api': ^1.3.0
|
||||
|
||||
'@opentelemetry/otlp-transformer@0.208.0':
|
||||
resolution: {integrity: sha512-DCFPY8C6lAQHUNkzcNT9R+qYExvsk6C5Bto2pbNxgicpcSWbe2WHShLxkOxIdNcBiYPdVHv/e7vH7K6TI+C+fQ==}
|
||||
engines: {node: ^18.19.0 || >=20.6.0}
|
||||
peerDependencies:
|
||||
'@opentelemetry/api': ^1.3.0
|
||||
|
||||
'@opentelemetry/resources@2.2.0':
|
||||
resolution: {integrity: sha512-1pNQf/JazQTMA0BiO5NINUzH0cbLbbl7mntLa4aJNmCCXSj0q03T5ZXXL0zw4G55TjdL9Tz32cznGClf+8zr5A==}
|
||||
engines: {node: ^18.19.0 || >=20.6.0}
|
||||
peerDependencies:
|
||||
'@opentelemetry/api': '>=1.3.0 <1.10.0'
|
||||
|
||||
'@opentelemetry/resources@2.6.0':
|
||||
resolution: {integrity: sha512-D4y/+OGe3JSuYUCBxtH5T9DSAWNcvCb/nQWIga8HNtXTVPQn59j0nTBAgaAXxUVBDl40mG3Tc76b46wPlZaiJQ==}
|
||||
engines: {node: ^18.19.0 || >=20.6.0}
|
||||
peerDependencies:
|
||||
'@opentelemetry/api': '>=1.3.0 <1.10.0'
|
||||
|
||||
'@opentelemetry/sdk-logs@0.208.0':
|
||||
resolution: {integrity: sha512-QlAyL1jRpOeaqx7/leG1vJMp84g0xKP6gJmfELBpnI4O/9xPX+Hu5m1POk9Kl+veNkyth5t19hRlN6tNY1sjbA==}
|
||||
engines: {node: ^18.19.0 || >=20.6.0}
|
||||
peerDependencies:
|
||||
'@opentelemetry/api': '>=1.4.0 <1.10.0'
|
||||
|
||||
'@opentelemetry/sdk-metrics@2.2.0':
|
||||
resolution: {integrity: sha512-G5KYP6+VJMZzpGipQw7Giif48h6SGQ2PFKEYCybeXJsOCB4fp8azqMAAzE5lnnHK3ZVwYQrgmFbsUJO/zOnwGw==}
|
||||
engines: {node: ^18.19.0 || >=20.6.0}
|
||||
peerDependencies:
|
||||
'@opentelemetry/api': '>=1.9.0 <1.10.0'
|
||||
|
||||
'@opentelemetry/sdk-trace-base@2.2.0':
|
||||
resolution: {integrity: sha512-xWQgL0Bmctsalg6PaXExmzdedSp3gyKV8mQBwK/j9VGdCDu2fmXIb2gAehBKbkXCpJ4HPkgv3QfoJWRT4dHWbw==}
|
||||
engines: {node: ^18.19.0 || >=20.6.0}
|
||||
peerDependencies:
|
||||
'@opentelemetry/api': '>=1.3.0 <1.10.0'
|
||||
|
||||
'@opentelemetry/semantic-conventions@1.40.0':
|
||||
resolution: {integrity: sha512-cifvXDhcqMwwTlTK04GBNeIe7yyo28Mfby85QXFe1Yk8nmi36Ab/5UQwptOx84SsoGNRg+EVSjwzfSZMy6pmlw==}
|
||||
engines: {node: '>=14'}
|
||||
|
||||
'@phosphor-icons/react@2.1.10':
|
||||
resolution: {integrity: sha512-vt8Tvq8GLjheAZZYa+YG/pW7HDbov8El/MANW8pOAz4eGxrwhnbfrQZq0Cp4q8zBEu8NIhHdnr+r8thnfRSNYA==}
|
||||
engines: {node: '>=10'}
|
||||
@@ -799,6 +880,42 @@ packages:
|
||||
engines: {node: '>=18'}
|
||||
hasBin: true
|
||||
|
||||
'@posthog/core@1.24.1':
|
||||
resolution: {integrity: sha512-e8AciAnc6MRFws89ux8lJKFAaI03yEon0ASDoUO7yS91FVqbUGXYekObUUR3LHplcg+pmyiJBI0jolY0SFbGRA==}
|
||||
|
||||
'@posthog/types@1.363.5':
|
||||
resolution: {integrity: sha512-Eu/aVOZRZE3f09ZGG8qyyo8WiBb0+TRhLAsckPpTaertmD1PSQWxmJQ6gY0je4ibotlJqhX26p2wDPhZTOWqyw==}
|
||||
|
||||
'@protobufjs/aspromise@1.1.2':
|
||||
resolution: {integrity: sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==}
|
||||
|
||||
'@protobufjs/base64@1.1.2':
|
||||
resolution: {integrity: sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==}
|
||||
|
||||
'@protobufjs/codegen@2.0.4':
|
||||
resolution: {integrity: sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==}
|
||||
|
||||
'@protobufjs/eventemitter@1.1.0':
|
||||
resolution: {integrity: sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==}
|
||||
|
||||
'@protobufjs/fetch@1.1.0':
|
||||
resolution: {integrity: sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==}
|
||||
|
||||
'@protobufjs/float@1.0.2':
|
||||
resolution: {integrity: sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==}
|
||||
|
||||
'@protobufjs/inquire@1.1.0':
|
||||
resolution: {integrity: sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==}
|
||||
|
||||
'@protobufjs/path@1.1.2':
|
||||
resolution: {integrity: sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==}
|
||||
|
||||
'@protobufjs/pool@1.1.0':
|
||||
resolution: {integrity: sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==}
|
||||
|
||||
'@protobufjs/utf8@1.1.0':
|
||||
resolution: {integrity: sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==}
|
||||
|
||||
'@radix-ui/number@1.1.1':
|
||||
resolution: {integrity: sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g==}
|
||||
|
||||
@@ -1668,6 +1785,30 @@ packages:
|
||||
cpu: [x64]
|
||||
os: [win32]
|
||||
|
||||
'@sentry-internal/browser-utils@10.45.0':
|
||||
resolution: {integrity: sha512-ZPZpeIarXKScvquGx2AfNKcYiVNDA4wegMmjyGVsTA2JPmP0TrJoO3UybJS6KGDeee8V3I3EfD/ruauMm7jOFQ==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
'@sentry-internal/feedback@10.45.0':
|
||||
resolution: {integrity: sha512-vCSurazFVq7RUeYiM5X326jA5gOVrWYD6lYX2fbjBOMcyCEhDnveNxMT62zKkZDyNT/jyD194nz/cjntBUkyWA==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
'@sentry-internal/replay-canvas@10.45.0':
|
||||
resolution: {integrity: sha512-nvq/AocdZTuD7y0KSiWi3gVaY0s5HOFy86mC/v1kDZmT/jsBAzN5LDkk/f1FvsWma1peqQmpUqxvhC+YIW294Q==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
'@sentry-internal/replay@10.45.0':
|
||||
resolution: {integrity: sha512-vjosRoGA1bzhVAEO1oce+CsRdd70quzBeo7WvYqpcUnoLe/Rv8qpOMqWX3j26z7XfFHMExWQNQeLxmtYOArvlw==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
'@sentry/browser@10.45.0':
|
||||
resolution: {integrity: sha512-e/a8UMiQhqqv706McSIcG6XK+AoQf9INthi2pD+giZfNRTzXTdqHzUT5OIO5hg8Am6eF63nDJc+vrYNPhzs51Q==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
'@sentry/core@10.45.0':
|
||||
resolution: {integrity: sha512-s69UXxvefeQxuZ5nY7/THtTrIEvJxNVCp3ns4kwoCw1qMpgpvn/296WCKVmM7MiwnaAdzEKnAvLAwaxZc2nM7Q==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
'@shikijs/types@3.22.0':
|
||||
resolution: {integrity: sha512-491iAekgKDBFE67z70Ok5a8KBMsQ2IJwOWw3us/7ffQkIBCyOQfm/aNwVMBUriP02QshIfgHCBSIYAl3u2eWjg==}
|
||||
|
||||
@@ -2040,6 +2181,9 @@ packages:
|
||||
'@types/react@19.2.14':
|
||||
resolution: {integrity: sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==}
|
||||
|
||||
'@types/trusted-types@2.0.7':
|
||||
resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==}
|
||||
|
||||
'@types/unist@2.0.11':
|
||||
resolution: {integrity: sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==}
|
||||
|
||||
@@ -2342,6 +2486,9 @@ packages:
|
||||
resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==}
|
||||
engines: {node: '>= 0.6'}
|
||||
|
||||
core-js@3.49.0:
|
||||
resolution: {integrity: sha512-es1U2+YTtzpwkxVLwAFdSpaIMyQaq0PBgm3YD1W3Qpsn1NAmO3KSgZfu+oGSWVu6NvLHoHCV/aYcsE5wiB7ALg==}
|
||||
|
||||
cors@2.8.6:
|
||||
resolution: {integrity: sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==}
|
||||
engines: {node: '>= 0.10'}
|
||||
@@ -2419,6 +2566,9 @@ packages:
|
||||
dom-accessibility-api@0.6.3:
|
||||
resolution: {integrity: sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==}
|
||||
|
||||
dompurify@3.3.3:
|
||||
resolution: {integrity: sha512-Oj6pzI2+RqBfFG+qOaOLbFXLQ90ARpcGG6UePL82bJLtdsa6CYJD7nmiU8MW9nQNOtCHV3lZ/Bzq1X0QYbBZCA==}
|
||||
|
||||
dunder-proto@1.0.1:
|
||||
resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==}
|
||||
engines: {node: '>= 0.4'}
|
||||
@@ -2605,6 +2755,9 @@ packages:
|
||||
picomatch:
|
||||
optional: true
|
||||
|
||||
fflate@0.4.8:
|
||||
resolution: {integrity: sha512-FJqqoDBR00Mdj9ppamLa/Y7vxm+PRmNWA67N846RvsoYVMKB4q3y/de5PA7gUmRMYK/8CMz2GDZQmCRN1wBcWA==}
|
||||
|
||||
file-entry-cache@8.0.0:
|
||||
resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==}
|
||||
engines: {node: '>=16.0.0'}
|
||||
@@ -3050,6 +3203,9 @@ packages:
|
||||
lodash.merge@4.6.2:
|
||||
resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==}
|
||||
|
||||
long@5.3.2:
|
||||
resolution: {integrity: sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==}
|
||||
|
||||
longest-streak@3.1.0:
|
||||
resolution: {integrity: sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==}
|
||||
|
||||
@@ -3362,6 +3518,12 @@ packages:
|
||||
resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==}
|
||||
engines: {node: ^10 || ^12 || >=14}
|
||||
|
||||
posthog-js@1.363.5:
|
||||
resolution: {integrity: sha512-LVjp+c2NRtPG0sRgMzH2/NIoRw5mzRH0TngcezZI7WzH06ngBu3apxfhu/IfFBEDsjPU+S59vrkTzSCqceMVzA==}
|
||||
|
||||
preact@10.29.0:
|
||||
resolution: {integrity: sha512-wSAGyk2bYR1c7t3SZ3jHcM6xy0lcBcDel6lODcs9ME6Th++Dx2KU+6D3HD8wMMKGA8Wpw7OMd3/4RGzYRpzwRg==}
|
||||
|
||||
prelude-ls@1.2.1:
|
||||
resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==}
|
||||
engines: {node: '>= 0.8.0'}
|
||||
@@ -3466,6 +3628,10 @@ packages:
|
||||
prosemirror-view@1.41.6:
|
||||
resolution: {integrity: sha512-mxpcDG4hNQa/CPtzxjdlir5bJFDlm0/x5nGBbStB2BWX+XOQ9M8ekEG+ojqB5BcVu2Rc80/jssCMZzSstJuSYg==}
|
||||
|
||||
protobufjs@7.5.4:
|
||||
resolution: {integrity: sha512-CvexbZtbov6jW2eXAvLukXjXUW1TzFaivC46BpWc/3BpcCysb5Vffu+B3XHMm8lVEuy2Mm4XGex8hBSg1yapPg==}
|
||||
engines: {node: '>=12.0.0'}
|
||||
|
||||
proxy-addr@2.0.7:
|
||||
resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==}
|
||||
engines: {node: '>= 0.10'}
|
||||
@@ -3482,6 +3648,9 @@ packages:
|
||||
resolution: {integrity: sha512-mAZTtNCeetKMH+pSjrb76NAM8V9a05I9aBZOHztWy/UqcJdQYNsf59vrRKWnojAT9Y+GbIvoTBC++CPHqpDBhQ==}
|
||||
engines: {node: '>=0.6'}
|
||||
|
||||
query-selector-shadow-dom@1.0.1:
|
||||
resolution: {integrity: sha512-lT5yCqEBgfoMYpf3F2xQRK7zEr1rhIIZuceDK6+xRkJQ4NMbHTwXqk4NkwDwQMNqXgG9r9fyHnzwNVs6zV5KRw==}
|
||||
|
||||
radix-ui@1.4.3:
|
||||
resolution: {integrity: sha512-aWizCQiyeAenIdUbqEpXgRA1ya65P13NKn/W8rWkcN0OPkRDxdBVLWnIEDsS2RpwCK2nobI7oMUSmexzTDyAmA==}
|
||||
peerDependencies:
|
||||
@@ -4045,6 +4214,9 @@ packages:
|
||||
web-namespaces@2.0.1:
|
||||
resolution: {integrity: sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ==}
|
||||
|
||||
web-vitals@5.1.0:
|
||||
resolution: {integrity: sha512-ArI3kx5jI0atlTtmV0fWU3fjpLmq/nD3Zr1iFFlJLaqa5wLBkUSzINwBPySCX/8jRyjlmy1Volw1kz1g9XE4Jg==}
|
||||
|
||||
webidl-conversions@8.0.1:
|
||||
resolution: {integrity: sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ==}
|
||||
engines: {node: '>=20'}
|
||||
@@ -4804,6 +4976,82 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@opentelemetry/api-logs@0.208.0':
|
||||
dependencies:
|
||||
'@opentelemetry/api': 1.9.0
|
||||
|
||||
'@opentelemetry/api@1.9.0': {}
|
||||
|
||||
'@opentelemetry/core@2.2.0(@opentelemetry/api@1.9.0)':
|
||||
dependencies:
|
||||
'@opentelemetry/api': 1.9.0
|
||||
'@opentelemetry/semantic-conventions': 1.40.0
|
||||
|
||||
'@opentelemetry/core@2.6.0(@opentelemetry/api@1.9.0)':
|
||||
dependencies:
|
||||
'@opentelemetry/api': 1.9.0
|
||||
'@opentelemetry/semantic-conventions': 1.40.0
|
||||
|
||||
'@opentelemetry/exporter-logs-otlp-http@0.208.0(@opentelemetry/api@1.9.0)':
|
||||
dependencies:
|
||||
'@opentelemetry/api': 1.9.0
|
||||
'@opentelemetry/api-logs': 0.208.0
|
||||
'@opentelemetry/core': 2.2.0(@opentelemetry/api@1.9.0)
|
||||
'@opentelemetry/otlp-exporter-base': 0.208.0(@opentelemetry/api@1.9.0)
|
||||
'@opentelemetry/otlp-transformer': 0.208.0(@opentelemetry/api@1.9.0)
|
||||
'@opentelemetry/sdk-logs': 0.208.0(@opentelemetry/api@1.9.0)
|
||||
|
||||
'@opentelemetry/otlp-exporter-base@0.208.0(@opentelemetry/api@1.9.0)':
|
||||
dependencies:
|
||||
'@opentelemetry/api': 1.9.0
|
||||
'@opentelemetry/core': 2.2.0(@opentelemetry/api@1.9.0)
|
||||
'@opentelemetry/otlp-transformer': 0.208.0(@opentelemetry/api@1.9.0)
|
||||
|
||||
'@opentelemetry/otlp-transformer@0.208.0(@opentelemetry/api@1.9.0)':
|
||||
dependencies:
|
||||
'@opentelemetry/api': 1.9.0
|
||||
'@opentelemetry/api-logs': 0.208.0
|
||||
'@opentelemetry/core': 2.2.0(@opentelemetry/api@1.9.0)
|
||||
'@opentelemetry/resources': 2.2.0(@opentelemetry/api@1.9.0)
|
||||
'@opentelemetry/sdk-logs': 0.208.0(@opentelemetry/api@1.9.0)
|
||||
'@opentelemetry/sdk-metrics': 2.2.0(@opentelemetry/api@1.9.0)
|
||||
'@opentelemetry/sdk-trace-base': 2.2.0(@opentelemetry/api@1.9.0)
|
||||
protobufjs: 7.5.4
|
||||
|
||||
'@opentelemetry/resources@2.2.0(@opentelemetry/api@1.9.0)':
|
||||
dependencies:
|
||||
'@opentelemetry/api': 1.9.0
|
||||
'@opentelemetry/core': 2.2.0(@opentelemetry/api@1.9.0)
|
||||
'@opentelemetry/semantic-conventions': 1.40.0
|
||||
|
||||
'@opentelemetry/resources@2.6.0(@opentelemetry/api@1.9.0)':
|
||||
dependencies:
|
||||
'@opentelemetry/api': 1.9.0
|
||||
'@opentelemetry/core': 2.6.0(@opentelemetry/api@1.9.0)
|
||||
'@opentelemetry/semantic-conventions': 1.40.0
|
||||
|
||||
'@opentelemetry/sdk-logs@0.208.0(@opentelemetry/api@1.9.0)':
|
||||
dependencies:
|
||||
'@opentelemetry/api': 1.9.0
|
||||
'@opentelemetry/api-logs': 0.208.0
|
||||
'@opentelemetry/core': 2.2.0(@opentelemetry/api@1.9.0)
|
||||
'@opentelemetry/resources': 2.2.0(@opentelemetry/api@1.9.0)
|
||||
|
||||
'@opentelemetry/sdk-metrics@2.2.0(@opentelemetry/api@1.9.0)':
|
||||
dependencies:
|
||||
'@opentelemetry/api': 1.9.0
|
||||
'@opentelemetry/core': 2.2.0(@opentelemetry/api@1.9.0)
|
||||
'@opentelemetry/resources': 2.2.0(@opentelemetry/api@1.9.0)
|
||||
|
||||
'@opentelemetry/sdk-trace-base@2.2.0(@opentelemetry/api@1.9.0)':
|
||||
dependencies:
|
||||
'@opentelemetry/api': 1.9.0
|
||||
'@opentelemetry/core': 2.2.0(@opentelemetry/api@1.9.0)
|
||||
'@opentelemetry/resources': 2.2.0(@opentelemetry/api@1.9.0)
|
||||
'@opentelemetry/semantic-conventions': 1.40.0
|
||||
|
||||
'@opentelemetry/semantic-conventions@1.40.0': {}
|
||||
|
||||
'@phosphor-icons/react@2.1.10(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
|
||||
dependencies:
|
||||
react: 19.2.4
|
||||
@@ -4813,6 +5061,35 @@ snapshots:
|
||||
dependencies:
|
||||
playwright: 1.58.2
|
||||
|
||||
'@posthog/core@1.24.1':
|
||||
dependencies:
|
||||
cross-spawn: 7.0.6
|
||||
|
||||
'@posthog/types@1.363.5': {}
|
||||
|
||||
'@protobufjs/aspromise@1.1.2': {}
|
||||
|
||||
'@protobufjs/base64@1.1.2': {}
|
||||
|
||||
'@protobufjs/codegen@2.0.4': {}
|
||||
|
||||
'@protobufjs/eventemitter@1.1.0': {}
|
||||
|
||||
'@protobufjs/fetch@1.1.0':
|
||||
dependencies:
|
||||
'@protobufjs/aspromise': 1.1.2
|
||||
'@protobufjs/inquire': 1.1.0
|
||||
|
||||
'@protobufjs/float@1.0.2': {}
|
||||
|
||||
'@protobufjs/inquire@1.1.0': {}
|
||||
|
||||
'@protobufjs/path@1.1.2': {}
|
||||
|
||||
'@protobufjs/pool@1.1.0': {}
|
||||
|
||||
'@protobufjs/utf8@1.1.0': {}
|
||||
|
||||
'@radix-ui/number@1.1.1': {}
|
||||
|
||||
'@radix-ui/primitive@1.1.3': {}
|
||||
@@ -5664,6 +5941,34 @@ snapshots:
|
||||
'@rollup/rollup-win32-x64-msvc@4.57.1':
|
||||
optional: true
|
||||
|
||||
'@sentry-internal/browser-utils@10.45.0':
|
||||
dependencies:
|
||||
'@sentry/core': 10.45.0
|
||||
|
||||
'@sentry-internal/feedback@10.45.0':
|
||||
dependencies:
|
||||
'@sentry/core': 10.45.0
|
||||
|
||||
'@sentry-internal/replay-canvas@10.45.0':
|
||||
dependencies:
|
||||
'@sentry-internal/replay': 10.45.0
|
||||
'@sentry/core': 10.45.0
|
||||
|
||||
'@sentry-internal/replay@10.45.0':
|
||||
dependencies:
|
||||
'@sentry-internal/browser-utils': 10.45.0
|
||||
'@sentry/core': 10.45.0
|
||||
|
||||
'@sentry/browser@10.45.0':
|
||||
dependencies:
|
||||
'@sentry-internal/browser-utils': 10.45.0
|
||||
'@sentry-internal/feedback': 10.45.0
|
||||
'@sentry-internal/replay': 10.45.0
|
||||
'@sentry-internal/replay-canvas': 10.45.0
|
||||
'@sentry/core': 10.45.0
|
||||
|
||||
'@sentry/core@10.45.0': {}
|
||||
|
||||
'@shikijs/types@3.22.0':
|
||||
dependencies:
|
||||
'@shikijs/vscode-textmate': 10.0.2
|
||||
@@ -6018,6 +6323,9 @@ snapshots:
|
||||
dependencies:
|
||||
csstype: 3.2.3
|
||||
|
||||
'@types/trusted-types@2.0.7':
|
||||
optional: true
|
||||
|
||||
'@types/unist@2.0.11': {}
|
||||
|
||||
'@types/unist@3.0.3': {}
|
||||
@@ -6135,7 +6443,7 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@vitest/coverage-v8@4.0.18(vitest@4.0.18(@types/node@24.10.13)(jiti@2.6.1)(jsdom@28.0.0)(lightningcss@1.30.2))':
|
||||
'@vitest/coverage-v8@4.0.18(vitest@4.0.18(@opentelemetry/api@1.9.0)(@types/node@24.10.13)(jiti@2.6.1)(jsdom@28.0.0)(lightningcss@1.30.2))':
|
||||
dependencies:
|
||||
'@bcoe/v8-coverage': 1.0.2
|
||||
'@vitest/utils': 4.0.18
|
||||
@@ -6147,7 +6455,7 @@ snapshots:
|
||||
obug: 2.1.1
|
||||
std-env: 3.10.0
|
||||
tinyrainbow: 3.0.3
|
||||
vitest: 4.0.18(@types/node@24.10.13)(jiti@2.6.1)(jsdom@28.0.0)(lightningcss@1.30.2)
|
||||
vitest: 4.0.18(@opentelemetry/api@1.9.0)(@types/node@24.10.13)(jiti@2.6.1)(jsdom@28.0.0)(lightningcss@1.30.2)
|
||||
|
||||
'@vitest/expect@4.0.18':
|
||||
dependencies:
|
||||
@@ -6353,6 +6661,8 @@ snapshots:
|
||||
|
||||
cookie@0.7.2: {}
|
||||
|
||||
core-js@3.49.0: {}
|
||||
|
||||
cors@2.8.6:
|
||||
dependencies:
|
||||
object-assign: 4.1.1
|
||||
@@ -6421,6 +6731,10 @@ snapshots:
|
||||
|
||||
dom-accessibility-api@0.6.3: {}
|
||||
|
||||
dompurify@3.3.3:
|
||||
optionalDependencies:
|
||||
'@types/trusted-types': 2.0.7
|
||||
|
||||
dunder-proto@1.0.1:
|
||||
dependencies:
|
||||
call-bind-apply-helpers: 1.0.2
|
||||
@@ -6650,6 +6964,8 @@ snapshots:
|
||||
optionalDependencies:
|
||||
picomatch: 4.0.3
|
||||
|
||||
fflate@0.4.8: {}
|
||||
|
||||
file-entry-cache@8.0.0:
|
||||
dependencies:
|
||||
flat-cache: 4.0.1
|
||||
@@ -7138,6 +7454,8 @@ snapshots:
|
||||
|
||||
lodash.merge@4.6.2: {}
|
||||
|
||||
long@5.3.2: {}
|
||||
|
||||
longest-streak@3.1.0: {}
|
||||
|
||||
lowlight@3.3.0:
|
||||
@@ -7648,6 +7966,24 @@ snapshots:
|
||||
picocolors: 1.1.1
|
||||
source-map-js: 1.2.1
|
||||
|
||||
posthog-js@1.363.5:
|
||||
dependencies:
|
||||
'@opentelemetry/api': 1.9.0
|
||||
'@opentelemetry/api-logs': 0.208.0
|
||||
'@opentelemetry/exporter-logs-otlp-http': 0.208.0(@opentelemetry/api@1.9.0)
|
||||
'@opentelemetry/resources': 2.6.0(@opentelemetry/api@1.9.0)
|
||||
'@opentelemetry/sdk-logs': 0.208.0(@opentelemetry/api@1.9.0)
|
||||
'@posthog/core': 1.24.1
|
||||
'@posthog/types': 1.363.5
|
||||
core-js: 3.49.0
|
||||
dompurify: 3.3.3
|
||||
fflate: 0.4.8
|
||||
preact: 10.29.0
|
||||
query-selector-shadow-dom: 1.0.1
|
||||
web-vitals: 5.1.0
|
||||
|
||||
preact@10.29.0: {}
|
||||
|
||||
prelude-ls@1.2.1: {}
|
||||
|
||||
pretty-format@27.5.1:
|
||||
@@ -7772,6 +8108,21 @@ snapshots:
|
||||
prosemirror-state: 1.4.4
|
||||
prosemirror-transform: 1.11.0
|
||||
|
||||
protobufjs@7.5.4:
|
||||
dependencies:
|
||||
'@protobufjs/aspromise': 1.1.2
|
||||
'@protobufjs/base64': 1.1.2
|
||||
'@protobufjs/codegen': 2.0.4
|
||||
'@protobufjs/eventemitter': 1.1.0
|
||||
'@protobufjs/fetch': 1.1.0
|
||||
'@protobufjs/float': 1.0.2
|
||||
'@protobufjs/inquire': 1.1.0
|
||||
'@protobufjs/path': 1.1.2
|
||||
'@protobufjs/pool': 1.1.0
|
||||
'@protobufjs/utf8': 1.1.0
|
||||
'@types/node': 24.10.13
|
||||
long: 5.3.2
|
||||
|
||||
proxy-addr@2.0.7:
|
||||
dependencies:
|
||||
forwarded: 0.2.0
|
||||
@@ -7785,6 +8136,8 @@ snapshots:
|
||||
dependencies:
|
||||
side-channel: 1.1.0
|
||||
|
||||
query-selector-shadow-dom@1.0.1: {}
|
||||
|
||||
radix-ui@1.4.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4):
|
||||
dependencies:
|
||||
'@radix-ui/primitive': 1.1.3
|
||||
@@ -8393,7 +8746,7 @@ snapshots:
|
||||
jiti: 2.6.1
|
||||
lightningcss: 1.30.2
|
||||
|
||||
vitest@4.0.18(@types/node@24.10.13)(jiti@2.6.1)(jsdom@28.0.0)(lightningcss@1.30.2):
|
||||
vitest@4.0.18(@opentelemetry/api@1.9.0)(@types/node@24.10.13)(jiti@2.6.1)(jsdom@28.0.0)(lightningcss@1.30.2):
|
||||
dependencies:
|
||||
'@vitest/expect': 4.0.18
|
||||
'@vitest/mocker': 4.0.18(vite@7.3.1(@types/node@24.10.13)(jiti@2.6.1)(lightningcss@1.30.2))
|
||||
@@ -8416,6 +8769,7 @@ snapshots:
|
||||
vite: 7.3.1(@types/node@24.10.13)(jiti@2.6.1)(lightningcss@1.30.2)
|
||||
why-is-node-running: 2.3.0
|
||||
optionalDependencies:
|
||||
'@opentelemetry/api': 1.9.0
|
||||
'@types/node': 24.10.13
|
||||
jsdom: 28.0.0
|
||||
transitivePeerDependencies:
|
||||
@@ -8439,6 +8793,8 @@ snapshots:
|
||||
|
||||
web-namespaces@2.0.1: {}
|
||||
|
||||
web-vitals@5.1.0: {}
|
||||
|
||||
webidl-conversions@8.0.1: {}
|
||||
|
||||
whatwg-mimetype@5.0.0: {}
|
||||
|
||||
@@ -1,155 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
# Bundle qmd into a self-contained directory for Tauri resource embedding.
|
||||
#
|
||||
# Output: src-tauri/resources/qmd/
|
||||
# qmd — compiled standalone binary
|
||||
# node_modules/sqlite-vec/ — JS shim for sqlite-vec
|
||||
# node_modules/sqlite-vec-darwin-arm64/ — native .dylib (arm64)
|
||||
# node_modules/sqlite-vec-darwin-x64/ — native .dylib (x64)
|
||||
# node_modules/node-llama-cpp/ — stub (keyword search only)
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
ROOT="$SCRIPT_DIR/.."
|
||||
OUT="$ROOT/src-tauri/resources/qmd"
|
||||
|
||||
# ---------- locate tools ----------
|
||||
find_bun() {
|
||||
for c in \
|
||||
"$HOME/.bun/bin/bun" \
|
||||
"/opt/homebrew/bin/bun" \
|
||||
"/usr/local/bin/bun"; do
|
||||
[[ -x "$c" ]] && { echo "$c"; return 0; }
|
||||
done
|
||||
command -v bun 2>/dev/null && return 0
|
||||
return 1
|
||||
}
|
||||
|
||||
BUN=$(find_bun) || { echo "ERROR: bun not found — install from https://bun.sh" >&2; exit 1; }
|
||||
echo "Using bun: $BUN"
|
||||
|
||||
# ---------- locate qmd source ----------
|
||||
# Prefer bundled source in tools/qmd/ (works in CI and dev),
|
||||
# then fall back to globally installed qmd on dev machines.
|
||||
QMD_SRC=""
|
||||
for c in \
|
||||
"$ROOT/tools/qmd" \
|
||||
"$HOME/.bun/install/global/node_modules/qmd" \
|
||||
"/opt/homebrew/lib/node_modules/qmd" \
|
||||
"/usr/local/lib/node_modules/qmd"; do
|
||||
[[ -f "$c/src/qmd.ts" ]] && { QMD_SRC="$c"; break; }
|
||||
done
|
||||
|
||||
[[ -n "$QMD_SRC" ]] || { echo "ERROR: qmd source not found. tools/qmd/ is missing or incomplete." >&2; exit 1; }
|
||||
echo "Using qmd source: $QMD_SRC"
|
||||
|
||||
# Install qmd dependencies if needed (for CI where node_modules don't exist yet)
|
||||
if [[ ! -d "$QMD_SRC/node_modules" ]]; then
|
||||
echo "Installing qmd dependencies..."
|
||||
(cd "$QMD_SRC" && "$BUN" install --frozen-lockfile)
|
||||
fi
|
||||
|
||||
# ---------- compile ----------
|
||||
echo "Compiling qmd with bun build --compile..."
|
||||
mkdir -p "$OUT"
|
||||
|
||||
(cd "$QMD_SRC" && "$BUN" build --compile \
|
||||
"src/qmd.ts" \
|
||||
--outfile "$OUT/qmd" \
|
||||
--external node-llama-cpp \
|
||||
--external sqlite-vec \
|
||||
--external sqlite-vec-darwin-arm64 \
|
||||
--external sqlite-vec-darwin-x64)
|
||||
|
||||
chmod +x "$OUT/qmd"
|
||||
|
||||
# ---------- bundle sqlite-vec ----------
|
||||
echo "Bundling sqlite-vec native extensions..."
|
||||
|
||||
# Find sqlite-vec packages — prefer node_modules in QMD_SRC (after bun install),
|
||||
# fall back to bun global cache for dev machines.
|
||||
NM="$QMD_SRC/node_modules"
|
||||
|
||||
find_pkg() {
|
||||
local pkg="$1"
|
||||
# Check node_modules from bun install in QMD_SRC first
|
||||
if [[ -d "$NM/$pkg" ]]; then
|
||||
echo "$NM/$pkg"; return 0
|
||||
fi
|
||||
# Fall back to bun global cache
|
||||
local cache_dir
|
||||
cache_dir=$(find "$HOME/.bun/install/cache" -maxdepth 1 -name "${pkg}@*" -type d 2>/dev/null | head -1)
|
||||
[[ -n "$cache_dir" ]] && echo "$cache_dir" && return 0
|
||||
return 1
|
||||
}
|
||||
|
||||
# sqlite-vec JS shim
|
||||
SQLVEC_DIR=$(find_pkg "sqlite-vec") || { echo "ERROR: sqlite-vec not found" >&2; exit 1; }
|
||||
mkdir -p "$OUT/node_modules/sqlite-vec"
|
||||
cp "$SQLVEC_DIR/index.mjs" "$OUT/node_modules/sqlite-vec/index.mjs"
|
||||
cp "$SQLVEC_DIR/package.json" "$OUT/node_modules/sqlite-vec/package.json"
|
||||
[[ -f "$SQLVEC_DIR/index.cjs" ]] && cp "$SQLVEC_DIR/index.cjs" "$OUT/node_modules/sqlite-vec/index.cjs"
|
||||
|
||||
# sqlite-vec-darwin-arm64
|
||||
ARM64_DIR=$(find_pkg "sqlite-vec-darwin-arm64") || true
|
||||
if [[ -n "$ARM64_DIR" ]]; then
|
||||
mkdir -p "$OUT/node_modules/sqlite-vec-darwin-arm64"
|
||||
cp "$ARM64_DIR/vec0.dylib" "$OUT/node_modules/sqlite-vec-darwin-arm64/vec0.dylib"
|
||||
cp "$ARM64_DIR/package.json" "$OUT/node_modules/sqlite-vec-darwin-arm64/package.json"
|
||||
echo " ✓ arm64 dylib"
|
||||
fi
|
||||
|
||||
# sqlite-vec-darwin-x64
|
||||
X64_DIR=$(find_pkg "sqlite-vec-darwin-x64") || true
|
||||
if [[ -n "$X64_DIR" ]]; then
|
||||
mkdir -p "$OUT/node_modules/sqlite-vec-darwin-x64"
|
||||
cp "$X64_DIR/vec0.dylib" "$OUT/node_modules/sqlite-vec-darwin-x64/vec0.dylib"
|
||||
cp "$X64_DIR/package.json" "$OUT/node_modules/sqlite-vec-darwin-x64/package.json"
|
||||
echo " ✓ x64 dylib"
|
||||
fi
|
||||
|
||||
# ---------- stub node-llama-cpp ----------
|
||||
echo "Creating node-llama-cpp stub (keyword search only)..."
|
||||
mkdir -p "$OUT/node_modules/node-llama-cpp"
|
||||
|
||||
cat > "$OUT/node_modules/node-llama-cpp/package.json" << 'PJSON'
|
||||
{"name":"node-llama-cpp","version":"0.0.0-stub","type":"module","main":"index.js"}
|
||||
PJSON
|
||||
|
||||
cat > "$OUT/node_modules/node-llama-cpp/index.js" << 'STUB'
|
||||
// Stub: node-llama-cpp not bundled — semantic search unavailable, keyword search works.
|
||||
const unavailable = (name) => (...args) => {
|
||||
throw new Error(`${name}() unavailable: node-llama-cpp not bundled. Keyword search still works.`);
|
||||
};
|
||||
export const getLlama = unavailable("getLlama");
|
||||
export const resolveModelFile = unavailable("resolveModelFile");
|
||||
export class LlamaChatSession {
|
||||
constructor() { throw new Error("LlamaChatSession unavailable"); }
|
||||
}
|
||||
export const LlamaLogLevel = { Error: 0, Warn: 1, Info: 2, Debug: 3 };
|
||||
STUB
|
||||
|
||||
# ---------- code signing (macOS) ----------
|
||||
# In CI (APPLE_SIGNING_IDENTITY set): sign with Developer ID + hardened runtime (required for notarization)
|
||||
# In dev (no identity): ad-hoc sign to remove quarantine
|
||||
if [[ "$(uname)" == "Darwin" ]] && command -v codesign &>/dev/null; then
|
||||
SIGN_ID="${APPLE_SIGNING_IDENTITY:--}"
|
||||
if [[ "$SIGN_ID" != "-" ]]; then
|
||||
echo "Signing bundled binaries with Developer ID: $SIGN_ID"
|
||||
SIGN_OPTS=(--force --sign "$SIGN_ID" --options runtime --timestamp)
|
||||
else
|
||||
echo "Ad-hoc signing bundled binaries (dev mode)..."
|
||||
SIGN_OPTS=(--force --sign -)
|
||||
fi
|
||||
codesign "${SIGN_OPTS[@]}" "$OUT/qmd" 2>/dev/null && echo " ✓ qmd signed" || echo " ⚠ qmd signing failed (non-fatal)"
|
||||
while IFS= read -r -d '' dylib; do
|
||||
codesign "${SIGN_OPTS[@]}" "$dylib" 2>/dev/null && echo " ✓ $(basename "$dylib") signed" || echo " ⚠ $(basename "$dylib") signing failed (non-fatal)"
|
||||
done < <(find "$OUT/node_modules" -name "*.dylib" -print0)
|
||||
fi
|
||||
|
||||
# ---------- summary ----------
|
||||
echo ""
|
||||
echo "qmd bundled → $OUT/"
|
||||
du -sh "$OUT/qmd"
|
||||
du -sh "$OUT/node_modules"
|
||||
echo "Done."
|
||||
285
src-tauri/Cargo.lock
generated
@@ -2,6 +2,15 @@
|
||||
# It is not intended for manual editing.
|
||||
version = 3
|
||||
|
||||
[[package]]
|
||||
name = "addr2line"
|
||||
version = "0.25.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1b5d307320b3181d6d7954e663bd7c774a838b8220fe0593c86d9fb09f498b4b"
|
||||
dependencies = [
|
||||
"gimli",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "adler2"
|
||||
version = "2.0.1"
|
||||
@@ -290,6 +299,21 @@ version = "1.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
|
||||
|
||||
[[package]]
|
||||
name = "backtrace"
|
||||
version = "0.3.76"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bb531853791a215d7c62a30daf0dde835f381ab5de4589cfe7c649d2cbe92bd6"
|
||||
dependencies = [
|
||||
"addr2line",
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"miniz_oxide",
|
||||
"object",
|
||||
"rustc-demangle",
|
||||
"windows-link 0.2.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "base64"
|
||||
version = "0.21.7"
|
||||
@@ -804,6 +828,16 @@ dependencies = [
|
||||
"syn 2.0.115",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "debugid"
|
||||
version = "0.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bef552e6f588e446098f6ba40d89ac146c8c7b64aade83c051ee00bb5d2bc18d"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"uuid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "deranged"
|
||||
version = "0.5.6"
|
||||
@@ -1143,6 +1177,18 @@ version = "0.1.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582"
|
||||
|
||||
[[package]]
|
||||
name = "findshlibs"
|
||||
version = "0.10.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "40b9e59cd0f7e0806cca4be089683ecb6434e602038df21fe6bf6711b2f07f64"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"lazy_static",
|
||||
"libc",
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "flate2"
|
||||
version = "1.1.9"
|
||||
@@ -1239,6 +1285,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10"
|
||||
dependencies = [
|
||||
"futures-core",
|
||||
"futures-sink",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1482,6 +1529,12 @@ dependencies = [
|
||||
"wasip3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "gimli"
|
||||
version = "0.32.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e629b9b98ef3dd8afe6ca2bd0f89306cec16d43d907889945bc5d6687f2f13c7"
|
||||
|
||||
[[package]]
|
||||
name = "gio"
|
||||
version = "0.18.4"
|
||||
@@ -1728,6 +1781,17 @@ version = "0.4.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
|
||||
|
||||
[[package]]
|
||||
name = "hostname"
|
||||
version = "0.4.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "617aaa3557aef3810a6369d0a99fac8a080891b68bd9f9812a1eeda0c0730cbd"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"windows-link 0.2.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "html5ever"
|
||||
version = "0.29.1"
|
||||
@@ -2205,6 +2269,7 @@ dependencies = [
|
||||
"mockito",
|
||||
"regex",
|
||||
"reqwest 0.12.28",
|
||||
"sentry",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"tauri",
|
||||
@@ -2216,6 +2281,7 @@ dependencies = [
|
||||
"tauri-plugin-updater",
|
||||
"tempfile",
|
||||
"tokio",
|
||||
"uuid",
|
||||
"walkdir",
|
||||
]
|
||||
|
||||
@@ -2496,6 +2562,18 @@ version = "1.0.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086"
|
||||
|
||||
[[package]]
|
||||
name = "nix"
|
||||
version = "0.30.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6"
|
||||
dependencies = [
|
||||
"bitflags 2.11.0",
|
||||
"cfg-if",
|
||||
"cfg_aliases",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nodrop"
|
||||
version = "0.1.14"
|
||||
@@ -2635,6 +2713,16 @@ dependencies = [
|
||||
"objc2-foundation",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "objc2-core-location"
|
||||
version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ca347214e24bc973fc025fd0d36ebb179ff30536ed1f80252706db19ee452009"
|
||||
dependencies = [
|
||||
"objc2",
|
||||
"objc2-foundation",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "objc2-core-text"
|
||||
version = "0.3.2"
|
||||
@@ -2751,8 +2839,27 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d87d638e33c06f577498cbcc50491496a3ed4246998a7fbba7ccb98b1e7eab22"
|
||||
dependencies = [
|
||||
"bitflags 2.11.0",
|
||||
"block2",
|
||||
"objc2",
|
||||
"objc2-cloud-kit",
|
||||
"objc2-core-data",
|
||||
"objc2-core-foundation",
|
||||
"objc2-core-graphics",
|
||||
"objc2-core-image",
|
||||
"objc2-core-location",
|
||||
"objc2-core-text",
|
||||
"objc2-foundation",
|
||||
"objc2-quartz-core",
|
||||
"objc2-user-notifications",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "objc2-user-notifications"
|
||||
version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9df9128cbbfef73cda168416ccf7f837b62737d748333bfe9ab71c245d76613e"
|
||||
dependencies = [
|
||||
"objc2",
|
||||
"objc2-foundation",
|
||||
]
|
||||
|
||||
@@ -2772,6 +2879,15 @@ dependencies = [
|
||||
"objc2-security",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "object"
|
||||
version = "0.37.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ff76201f031d8863c38aa7f905eca4f53abbfa15f609db4277d44cd8938f33fe"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "once_cell"
|
||||
version = "1.21.3"
|
||||
@@ -2850,6 +2966,22 @@ dependencies = [
|
||||
"pin-project-lite",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "os_info"
|
||||
version = "3.14.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e4022a17595a00d6a369236fdae483f0de7f0a339960a53118b818238e132224"
|
||||
dependencies = [
|
||||
"android_system_properties",
|
||||
"log",
|
||||
"nix",
|
||||
"objc2",
|
||||
"objc2-foundation",
|
||||
"objc2-ui-kit",
|
||||
"serde",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "osakit"
|
||||
version = "0.3.1"
|
||||
@@ -3514,6 +3646,7 @@ dependencies = [
|
||||
"base64 0.22.1",
|
||||
"bytes",
|
||||
"encoding_rs",
|
||||
"futures-channel",
|
||||
"futures-core",
|
||||
"futures-util",
|
||||
"h2",
|
||||
@@ -3670,6 +3803,12 @@ dependencies = [
|
||||
"serde_json",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustc-demangle"
|
||||
version = "0.1.27"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b50b8869d9fc858ce7266cce0194bd74df58b9d0e3f6df3a9fc8eb470d95c09d"
|
||||
|
||||
[[package]]
|
||||
name = "rustc_version"
|
||||
version = "0.4.1"
|
||||
@@ -3909,6 +4048,114 @@ dependencies = [
|
||||
"serde_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sentry"
|
||||
version = "0.37.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "255914a8e53822abd946e2ce8baa41d4cded6b8e938913b7f7b9da5b7ab44335"
|
||||
dependencies = [
|
||||
"httpdate",
|
||||
"native-tls",
|
||||
"reqwest 0.12.28",
|
||||
"sentry-backtrace",
|
||||
"sentry-contexts",
|
||||
"sentry-core",
|
||||
"sentry-debug-images",
|
||||
"sentry-panic",
|
||||
"sentry-tracing",
|
||||
"tokio",
|
||||
"ureq",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sentry-backtrace"
|
||||
version = "0.37.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "00293cd332a859961f24fd69258f7e92af736feaeb91020cff84dac4188a4302"
|
||||
dependencies = [
|
||||
"backtrace",
|
||||
"once_cell",
|
||||
"regex",
|
||||
"sentry-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sentry-contexts"
|
||||
version = "0.37.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "961990f9caa76476c481de130ada05614cd7f5aa70fb57c2142f0e09ad3fb2aa"
|
||||
dependencies = [
|
||||
"hostname",
|
||||
"libc",
|
||||
"os_info",
|
||||
"rustc_version",
|
||||
"sentry-core",
|
||||
"uname",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sentry-core"
|
||||
version = "0.37.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1a6409d845707d82415c800290a5d63be5e3df3c2e417b0997c60531dfbd35ef"
|
||||
dependencies = [
|
||||
"once_cell",
|
||||
"rand 0.8.5",
|
||||
"sentry-types",
|
||||
"serde",
|
||||
"serde_json",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sentry-debug-images"
|
||||
version = "0.37.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "71ab5df4f3b64760508edfe0ba4290feab5acbbda7566a79d72673065888e5cc"
|
||||
dependencies = [
|
||||
"findshlibs",
|
||||
"once_cell",
|
||||
"sentry-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sentry-panic"
|
||||
version = "0.37.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "609b1a12340495ce17baeec9e08ff8ed423c337c1a84dffae36a178c783623f3"
|
||||
dependencies = [
|
||||
"sentry-backtrace",
|
||||
"sentry-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sentry-tracing"
|
||||
version = "0.37.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "49f4e86402d5c50239dc7d8fd3f6d5e048221d5fcb4e026d8d50ab57fe4644cb"
|
||||
dependencies = [
|
||||
"sentry-backtrace",
|
||||
"sentry-core",
|
||||
"tracing-core",
|
||||
"tracing-subscriber",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sentry-types"
|
||||
version = "0.37.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3d3f117b8755dbede8260952de2aeb029e20f432e72634e8969af34324591631"
|
||||
dependencies = [
|
||||
"debugid",
|
||||
"hex",
|
||||
"rand 0.8.5",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"thiserror 1.0.69",
|
||||
"time",
|
||||
"url",
|
||||
"uuid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.228"
|
||||
@@ -5131,6 +5378,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a"
|
||||
dependencies = [
|
||||
"once_cell",
|
||||
"valuable",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tracing-subscriber"
|
||||
version = "0.3.23"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319"
|
||||
dependencies = [
|
||||
"tracing-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -5184,6 +5441,15 @@ dependencies = [
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "uname"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b72f89f0ca32e4db1c04e2a72f5345d59796d4866a1ee0609084569f73683dc8"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unic-char-property"
|
||||
version = "0.9.0"
|
||||
@@ -5249,6 +5515,19 @@ version = "0.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
|
||||
|
||||
[[package]]
|
||||
name = "ureq"
|
||||
version = "2.12.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "02d1a66277ed75f640d608235660df48c8e3c19f3b4edb6a263315626cc3c01d"
|
||||
dependencies = [
|
||||
"base64 0.22.1",
|
||||
"log",
|
||||
"native-tls",
|
||||
"once_cell",
|
||||
"url",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "url"
|
||||
version = "2.5.8"
|
||||
@@ -5304,6 +5583,12 @@ dependencies = [
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "valuable"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65"
|
||||
|
||||
[[package]]
|
||||
name = "value-bag"
|
||||
version = "1.12.0"
|
||||
|
||||
@@ -34,6 +34,8 @@ tauri-plugin-dialog = "2"
|
||||
tauri-plugin-updater = "2.10.0"
|
||||
tauri-plugin-process = "2.3.1"
|
||||
tauri-plugin-opener = "2"
|
||||
sentry = "0.37"
|
||||
uuid = { version = "1", features = ["v4"] }
|
||||
|
||||
[dev-dependencies]
|
||||
tempfile = "3"
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
fn main() {
|
||||
// Ensure resource directories exist for the Tauri build.
|
||||
// These are gitignored and populated by scripts (bundle-qmd.sh, bundle-mcp-server.mjs).
|
||||
// Without a placeholder, `tauri build` / `cargo test` fails if the scripts haven't run.
|
||||
for dir in ["resources/qmd", "resources/mcp-server"] {
|
||||
let path = std::path::Path::new(dir);
|
||||
if !path.exists() {
|
||||
std::fs::create_dir_all(path).ok();
|
||||
std::fs::write(path.join(".placeholder"), "").ok();
|
||||
}
|
||||
// Ensure resource directory exists for the Tauri build.
|
||||
// Gitignored and populated by bundle-mcp-server.mjs.
|
||||
// Without a placeholder, `tauri build` / `cargo test` fails if the script hasn't run.
|
||||
let path = std::path::Path::new("resources/mcp-server");
|
||||
if !path.exists() {
|
||||
std::fs::create_dir_all(path).ok();
|
||||
std::fs::write(path.join(".placeholder"), "").ok();
|
||||
}
|
||||
tauri_build::build()
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
"main",
|
||||
"note-*"
|
||||
],
|
||||
"platforms": ["linux", "macOS", "windows"],
|
||||
"permissions": [
|
||||
"core:default",
|
||||
"core:window:allow-create",
|
||||
|
||||
15
src-tauri/capabilities/mobile.json
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"$schema": "../gen/schemas/mobile-schema.json",
|
||||
"identifier": "mobile",
|
||||
"description": "permissions for iOS/iPadOS",
|
||||
"windows": [
|
||||
"main"
|
||||
],
|
||||
"platforms": ["iOS", "android"],
|
||||
"permissions": [
|
||||
"core:default",
|
||||
"core:window:allow-close",
|
||||
"core:window:allow-set-title",
|
||||
"dialog:default"
|
||||
]
|
||||
}
|
||||
3
src-tauri/gen/apple/.gitignore
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
xcuserdata/
|
||||
build/
|
||||
Externals/
|
||||
|
After Width: | Height: | Size: 1.0 KiB |
|
After Width: | Height: | Size: 2.2 KiB |
|
After Width: | Height: | Size: 2.2 KiB |
|
After Width: | Height: | Size: 3.4 KiB |
|
After Width: | Height: | Size: 1.5 KiB |
|
After Width: | Height: | Size: 3.3 KiB |
|
After Width: | Height: | Size: 3.3 KiB |
|
After Width: | Height: | Size: 5.1 KiB |
|
After Width: | Height: | Size: 2.2 KiB |
|
After Width: | Height: | Size: 4.7 KiB |
|
After Width: | Height: | Size: 4.7 KiB |
|
After Width: | Height: | Size: 7.1 KiB |
|
After Width: | Height: | Size: 118 KiB |
|
After Width: | Height: | Size: 7.1 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 4.4 KiB |
|
After Width: | Height: | Size: 9.0 KiB |
|
After Width: | Height: | Size: 10 KiB |
@@ -0,0 +1,116 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"size" : "20x20",
|
||||
"idiom" : "iphone",
|
||||
"filename" : "AppIcon-20x20@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "20x20",
|
||||
"idiom" : "iphone",
|
||||
"filename" : "AppIcon-20x20@3x.png",
|
||||
"scale" : "3x"
|
||||
},
|
||||
{
|
||||
"size" : "29x29",
|
||||
"idiom" : "iphone",
|
||||
"filename" : "AppIcon-29x29@2x-1.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "29x29",
|
||||
"idiom" : "iphone",
|
||||
"filename" : "AppIcon-29x29@3x.png",
|
||||
"scale" : "3x"
|
||||
},
|
||||
{
|
||||
"size" : "40x40",
|
||||
"idiom" : "iphone",
|
||||
"filename" : "AppIcon-40x40@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "40x40",
|
||||
"idiom" : "iphone",
|
||||
"filename" : "AppIcon-40x40@3x.png",
|
||||
"scale" : "3x"
|
||||
},
|
||||
{
|
||||
"size" : "60x60",
|
||||
"idiom" : "iphone",
|
||||
"filename" : "AppIcon-60x60@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "60x60",
|
||||
"idiom" : "iphone",
|
||||
"filename" : "AppIcon-60x60@3x.png",
|
||||
"scale" : "3x"
|
||||
},
|
||||
{
|
||||
"size" : "20x20",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "AppIcon-20x20@1x.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"size" : "20x20",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "AppIcon-20x20@2x-1.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "29x29",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "AppIcon-29x29@1x.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"size" : "29x29",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "AppIcon-29x29@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "40x40",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "AppIcon-40x40@1x.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"size" : "40x40",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "AppIcon-40x40@2x-1.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "76x76",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "AppIcon-76x76@1x.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"size" : "76x76",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "AppIcon-76x76@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "83.5x83.5",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "AppIcon-83.5x83.5@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "1024x1024",
|
||||
"idiom" : "ios-marketing",
|
||||
"filename" : "AppIcon-512@2x.png",
|
||||
"scale" : "1x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
||||
6
src-tauri/gen/apple/Assets.xcassets/Contents.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
||||
8
src-tauri/gen/apple/ExportOptions.plist
Normal file
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>method</key>
|
||||
<string>debugging</string>
|
||||
</dict>
|
||||
</plist>
|
||||
30
src-tauri/gen/apple/LaunchScreen.storyboard
Normal file
@@ -0,0 +1,30 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="17150" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="Y6W-OH-hqX">
|
||||
<dependencies>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="17122"/>
|
||||
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
|
||||
<capability name="System colors in document resources" minToolsVersion="11.0"/>
|
||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||
</dependencies>
|
||||
<scenes>
|
||||
<!--View Controller-->
|
||||
<scene sceneID="s0d-6b-0kx">
|
||||
<objects>
|
||||
<viewController id="Y6W-OH-hqX" sceneMemberID="viewController">
|
||||
<view key="view" contentMode="scaleToFill" id="5EZ-qb-Rvc">
|
||||
<rect key="frame" x="0.0" y="0.0" width="414" height="896"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<viewLayoutGuide key="safeArea" id="vDu-zF-Fre"/>
|
||||
<color key="backgroundColor" systemColor="systemBackgroundColor"/>
|
||||
</view>
|
||||
</viewController>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="Ief-a0-LHa" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
</scene>
|
||||
</scenes>
|
||||
<resources>
|
||||
<systemColor name="systemBackgroundColor">
|
||||
<color white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
</systemColor>
|
||||
</resources>
|
||||
</document>
|
||||
21
src-tauri/gen/apple/Podfile
Normal file
@@ -0,0 +1,21 @@
|
||||
# Uncomment the next line to define a global platform for your project
|
||||
|
||||
target 'laputa_iOS' do
|
||||
platform :ios, '14.0'
|
||||
# Pods for laputa_iOS
|
||||
end
|
||||
|
||||
target 'laputa_macOS' do
|
||||
platform :osx, '11.0'
|
||||
# Pods for laputa_macOS
|
||||
end
|
||||
|
||||
# Delete the deployment target for iOS and macOS, causing it to be inherited from the Podfile
|
||||
post_install do |installer|
|
||||
installer.pods_project.targets.each do |target|
|
||||
target.build_configurations.each do |config|
|
||||
config.build_settings.delete 'IPHONEOS_DEPLOYMENT_TARGET'
|
||||
config.build_settings.delete 'MACOSX_DEPLOYMENT_TARGET'
|
||||
end
|
||||
end
|
||||
end
|
||||
8
src-tauri/gen/apple/Sources/laputa/bindings/bindings.h
Normal file
@@ -0,0 +1,8 @@
|
||||
#pragma once
|
||||
|
||||
namespace ffi {
|
||||
extern "C" {
|
||||
void start_app();
|
||||
}
|
||||
}
|
||||
|
||||
6
src-tauri/gen/apple/Sources/laputa/main.mm
Normal file
@@ -0,0 +1,6 @@
|
||||
#include "bindings/bindings.h"
|
||||
|
||||
int main(int argc, char * argv[]) {
|
||||
ffi::start_app();
|
||||
return 0;
|
||||
}
|
||||
28192
src-tauri/gen/apple/assets/mcp-server/index.js
Executable file
1
src-tauri/gen/apple/assets/mcp-server/package.json
Normal file
@@ -0,0 +1 @@
|
||||
{"type":"commonjs"}
|
||||
7380
src-tauri/gen/apple/assets/mcp-server/ws-bridge.js
Executable file
566
src-tauri/gen/apple/laputa.xcodeproj/project.pbxproj
Normal file
@@ -0,0 +1,566 @@
|
||||
// !$*UTF8*$!
|
||||
{
|
||||
archiveVersion = 1;
|
||||
classes = {
|
||||
};
|
||||
objectVersion = 77;
|
||||
objects = {
|
||||
|
||||
/* Begin PBXBuildFile section */
|
||||
1662961784944D479C9E1B0D /* Metal.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = E3FC10F8A144BB158574C774 /* Metal.framework */; };
|
||||
1C55EF711F7ACEAC173C344E /* libapp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 3CF0D094B920E757356FC3AD /* libapp.a */; };
|
||||
4A94802D0295ECD102C21BA1 /* UIKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A34681D9D4CE45CDF83ABA59 /* UIKit.framework */; };
|
||||
534929E6730B15780117F21F /* MetalKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 05C322F5E5E2ACB0CBC644EE /* MetalKit.framework */; };
|
||||
5AAD7159BB4D20588044B3BC /* Security.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 43C29783A8194F596483AD35 /* Security.framework */; };
|
||||
74A9072960AF0CB774BEB749 /* QuartzCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3AB6F4BD2E768B4B6EDF7EA4 /* QuartzCore.framework */; };
|
||||
799F0FF5DD8053B287C9CA46 /* main.mm in Sources */ = {isa = PBXBuildFile; fileRef = 3D8616031E45547278A88B60 /* main.mm */; };
|
||||
7EF9AA3C7F58D96F21743FB4 /* WebKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 27D33799362DF41B226D7635 /* WebKit.framework */; };
|
||||
9BD440BEBCDBD633C9EE2C26 /* CoreGraphics.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A70EB7513BFD74428B2C6ED1 /* CoreGraphics.framework */; };
|
||||
A2279B6B796C960A2951E7C5 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = F8E52CF938A81FD9ABC1B23F /* Assets.xcassets */; };
|
||||
DACF8A23100E74CEA0BE1ED9 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 9274F38B9AA0A9A44A3CBEAA /* LaunchScreen.storyboard */; };
|
||||
E0FB400669725FC03B8E9B39 /* assets in Resources */ = {isa = PBXBuildFile; fileRef = 1BC846C6372AE88FE28EA984 /* assets */; };
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
/* Begin PBXFileReference section */
|
||||
05C322F5E5E2ACB0CBC644EE /* MetalKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = MetalKit.framework; path = System/Library/Frameworks/MetalKit.framework; sourceTree = SDKROOT; };
|
||||
077AD05C7FB4A784D360399B /* commands.rs */ = {isa = PBXFileReference; path = commands.rs; sourceTree = "<group>"; };
|
||||
0AE733EAA82134418C0AC89F /* api.rs */ = {isa = PBXFileReference; path = api.rs; sourceTree = "<group>"; };
|
||||
0CA7012668D8606FE9709D6D /* yaml.rs */ = {isa = PBXFileReference; path = yaml.rs; sourceTree = "<group>"; };
|
||||
0FB1053AF590466CC8971679 /* parsing.rs */ = {isa = PBXFileReference; path = parsing.rs; sourceTree = "<group>"; };
|
||||
16CCC72C07657AEBC320B099 /* auth.rs */ = {isa = PBXFileReference; path = auth.rs; sourceTree = "<group>"; };
|
||||
1BC846C6372AE88FE28EA984 /* assets */ = {isa = PBXFileReference; lastKnownFileType = folder; path = assets; sourceTree = SOURCE_ROOT; };
|
||||
1D65CB9525835A85C38D3136 /* migration.rs */ = {isa = PBXFileReference; path = migration.rs; sourceTree = "<group>"; };
|
||||
27D33799362DF41B226D7635 /* WebKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = WebKit.framework; path = System/Library/Frameworks/WebKit.framework; sourceTree = SDKROOT; };
|
||||
2D31CB45BB367FD6A7FA0B67 /* rename.rs */ = {isa = PBXFileReference; path = rename.rs; sourceTree = "<group>"; };
|
||||
2D38A9417EA6B0A133D0D55F /* mod.rs */ = {isa = PBXFileReference; path = mod.rs; sourceTree = "<group>"; };
|
||||
34F1C40770F83FDC21036505 /* laputa_iOS.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = laputa_iOS.entitlements; sourceTree = "<group>"; };
|
||||
3AB6F4BD2E768B4B6EDF7EA4 /* QuartzCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = QuartzCore.framework; path = System/Library/Frameworks/QuartzCore.framework; sourceTree = SDKROOT; };
|
||||
3CF0D094B920E757356FC3AD /* libapp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libapp.a; sourceTree = "<group>"; };
|
||||
3D8616031E45547278A88B60 /* main.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = main.mm; sourceTree = "<group>"; };
|
||||
3E23075B718E12551904D3B6 /* frontmatter.rs */ = {isa = PBXFileReference; path = frontmatter.rs; sourceTree = "<group>"; };
|
||||
43C29783A8194F596483AD35 /* Security.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Security.framework; path = System/Library/Frameworks/Security.framework; sourceTree = SDKROOT; };
|
||||
4AAEE16E1AC0E472E9EAF907 /* conflict.rs */ = {isa = PBXFileReference; path = conflict.rs; sourceTree = "<group>"; };
|
||||
4F4C2FE8D3697D180E1D4304 /* entry.rs */ = {isa = PBXFileReference; path = entry.rs; sourceTree = "<group>"; };
|
||||
50B53E0FE0FC2A5515B48A28 /* main.rs */ = {isa = PBXFileReference; path = main.rs; sourceTree = "<group>"; };
|
||||
517A653F5A674050430C1167 /* commit.rs */ = {isa = PBXFileReference; path = commit.rs; sourceTree = "<group>"; };
|
||||
5800BA1A3E5C03124E02E946 /* title_sync.rs */ = {isa = PBXFileReference; path = title_sync.rs; sourceTree = "<group>"; };
|
||||
5F28F393EB20BA7F8F2102EF /* config_seed.rs */ = {isa = PBXFileReference; path = config_seed.rs; sourceTree = "<group>"; };
|
||||
6288FD61B10C36115B12B4A4 /* cache.rs */ = {isa = PBXFileReference; path = cache.rs; sourceTree = "<group>"; };
|
||||
692CE75FC8E670478C4209C8 /* remote.rs */ = {isa = PBXFileReference; path = remote.rs; sourceTree = "<group>"; };
|
||||
6A5C3DA38846B440DD56E98A /* claude_cli.rs */ = {isa = PBXFileReference; path = claude_cli.rs; sourceTree = "<group>"; };
|
||||
6AC39E9370FCCB01E81EC824 /* ai_chat.rs */ = {isa = PBXFileReference; path = ai_chat.rs; sourceTree = "<group>"; };
|
||||
6F67F5CDD6BB137126C5AABB /* menu.rs */ = {isa = PBXFileReference; path = menu.rs; sourceTree = "<group>"; };
|
||||
71EDBD38A067DA56FD3E375F /* getting_started.rs */ = {isa = PBXFileReference; path = getting_started.rs; sourceTree = "<group>"; };
|
||||
7D94F0F22CD503903B63FFE7 /* file.rs */ = {isa = PBXFileReference; path = file.rs; sourceTree = "<group>"; };
|
||||
7FCDD5B9C3D021A45C402A43 /* mod.rs */ = {isa = PBXFileReference; path = mod.rs; sourceTree = "<group>"; };
|
||||
81C8EBDD5CAD14B7F366AA54 /* trash.rs */ = {isa = PBXFileReference; path = trash.rs; sourceTree = "<group>"; };
|
||||
84ECD74B7841E52AD6549346 /* image.rs */ = {isa = PBXFileReference; path = image.rs; sourceTree = "<group>"; };
|
||||
87F0375DE367A8192708C2D8 /* pulse.rs */ = {isa = PBXFileReference; path = pulse.rs; sourceTree = "<group>"; };
|
||||
9274F38B9AA0A9A44A3CBEAA /* LaunchScreen.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = LaunchScreen.storyboard; sourceTree = "<group>"; };
|
||||
94B5B4BBA850E2BCAE3E4CC7 /* mod.rs */ = {isa = PBXFileReference; path = mod.rs; sourceTree = "<group>"; };
|
||||
A17AA2469F233241B0DF8BE3 /* vault_list.rs */ = {isa = PBXFileReference; path = vault_list.rs; sourceTree = "<group>"; };
|
||||
A34681D9D4CE45CDF83ABA59 /* UIKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = UIKit.framework; path = System/Library/Frameworks/UIKit.framework; sourceTree = SDKROOT; };
|
||||
A70EB7513BFD74428B2C6ED1 /* CoreGraphics.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreGraphics.framework; path = System/Library/Frameworks/CoreGraphics.framework; sourceTree = SDKROOT; };
|
||||
AA64014FA2D16950AD4DF619 /* mod.rs */ = {isa = PBXFileReference; path = mod.rs; sourceTree = "<group>"; };
|
||||
ACCD8318FA6543BA013E37E4 /* ops.rs */ = {isa = PBXFileReference; path = ops.rs; sourceTree = "<group>"; };
|
||||
BA9FA5D7A7A7574FF74589C8 /* telemetry.rs */ = {isa = PBXFileReference; path = telemetry.rs; sourceTree = "<group>"; };
|
||||
BB4E3DD2D3BA34BB86C908B6 /* status.rs */ = {isa = PBXFileReference; path = status.rs; sourceTree = "<group>"; };
|
||||
C39B460DE1A3B78A0937BFDD /* settings.rs */ = {isa = PBXFileReference; path = settings.rs; sourceTree = "<group>"; };
|
||||
C80E8FDDA822F4F167B601C1 /* mod_tests.rs */ = {isa = PBXFileReference; path = mod_tests.rs; sourceTree = "<group>"; };
|
||||
CEDEEAE68B0DB4CD43ED89A8 /* laputa_iOS.app */ = {isa = PBXFileReference; includeInIndex = 0; lastKnownFileType = wrapper.application; path = laputa_iOS.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
D0900BA51E68887E5DA395B7 /* bindings.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = bindings.h; sourceTree = "<group>"; };
|
||||
DDC76F9C00A29C8C52345249 /* history.rs */ = {isa = PBXFileReference; path = history.rs; sourceTree = "<group>"; };
|
||||
E1428C0A0CC22F741B7A4A0C /* lib.rs */ = {isa = PBXFileReference; path = lib.rs; sourceTree = "<group>"; };
|
||||
E36BAA25D76F212157A06395 /* search.rs */ = {isa = PBXFileReference; path = search.rs; sourceTree = "<group>"; };
|
||||
E3FC10F8A144BB158574C774 /* Metal.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Metal.framework; path = System/Library/Frameworks/Metal.framework; sourceTree = SDKROOT; };
|
||||
F87684B058BB66A215F98DCF /* mcp.rs */ = {isa = PBXFileReference; path = mcp.rs; sourceTree = "<group>"; };
|
||||
F8E52CF938A81FD9ABC1B23F /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
|
||||
F9FF9FD3CEB0EB658CA17305 /* clone.rs */ = {isa = PBXFileReference; path = clone.rs; sourceTree = "<group>"; };
|
||||
FBB08655AD50E526ACEAE8A2 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = "<group>"; };
|
||||
/* End PBXFileReference section */
|
||||
|
||||
/* Begin PBXFrameworksBuildPhase section */
|
||||
50978AC1976616D3342459ED /* Frameworks */ = {
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
1C55EF711F7ACEAC173C344E /* libapp.a in Frameworks */,
|
||||
9BD440BEBCDBD633C9EE2C26 /* CoreGraphics.framework in Frameworks */,
|
||||
1662961784944D479C9E1B0D /* Metal.framework in Frameworks */,
|
||||
534929E6730B15780117F21F /* MetalKit.framework in Frameworks */,
|
||||
74A9072960AF0CB774BEB749 /* QuartzCore.framework in Frameworks */,
|
||||
5AAD7159BB4D20588044B3BC /* Security.framework in Frameworks */,
|
||||
4A94802D0295ECD102C21BA1 /* UIKit.framework in Frameworks */,
|
||||
7EF9AA3C7F58D96F21743FB4 /* WebKit.framework in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXFrameworksBuildPhase section */
|
||||
|
||||
/* Begin PBXGroup section */
|
||||
021259EC384B5D897634CFF8 /* Products */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
CEDEEAE68B0DB4CD43ED89A8 /* laputa_iOS.app */,
|
||||
);
|
||||
name = Products;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
109126B51C7DDFCA7CD8B9EA /* Sources */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
DC6461BBBF0BDFBB28352CF9 /* laputa */,
|
||||
);
|
||||
path = Sources;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
1189728653A76416E01221D7 /* Frameworks */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
A70EB7513BFD74428B2C6ED1 /* CoreGraphics.framework */,
|
||||
3CF0D094B920E757356FC3AD /* libapp.a */,
|
||||
E3FC10F8A144BB158574C774 /* Metal.framework */,
|
||||
05C322F5E5E2ACB0CBC644EE /* MetalKit.framework */,
|
||||
3AB6F4BD2E768B4B6EDF7EA4 /* QuartzCore.framework */,
|
||||
43C29783A8194F596483AD35 /* Security.framework */,
|
||||
A34681D9D4CE45CDF83ABA59 /* UIKit.framework */,
|
||||
27D33799362DF41B226D7635 /* WebKit.framework */,
|
||||
);
|
||||
name = Frameworks;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
314C2D9104FDC1DE81AC36AD = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
1BC846C6372AE88FE28EA984 /* assets */,
|
||||
F8E52CF938A81FD9ABC1B23F /* Assets.xcassets */,
|
||||
9274F38B9AA0A9A44A3CBEAA /* LaunchScreen.storyboard */,
|
||||
FD3AA61436162714FAEC418D /* Externals */,
|
||||
56AC65AB30CBAEB08FD782A6 /* laputa_iOS */,
|
||||
109126B51C7DDFCA7CD8B9EA /* Sources */,
|
||||
FA63A5561E1F084B5F13E265 /* src */,
|
||||
1189728653A76416E01221D7 /* Frameworks */,
|
||||
021259EC384B5D897634CFF8 /* Products */,
|
||||
);
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
56AC65AB30CBAEB08FD782A6 /* laputa_iOS */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
FBB08655AD50E526ACEAE8A2 /* Info.plist */,
|
||||
34F1C40770F83FDC21036505 /* laputa_iOS.entitlements */,
|
||||
);
|
||||
path = laputa_iOS;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
5D80057D94E84FA08C34A8B0 /* frontmatter */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
94B5B4BBA850E2BCAE3E4CC7 /* mod.rs */,
|
||||
ACCD8318FA6543BA013E37E4 /* ops.rs */,
|
||||
0CA7012668D8606FE9709D6D /* yaml.rs */,
|
||||
);
|
||||
path = frontmatter;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
6A109CE663DEBC9C99A47D0B /* github */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
0AE733EAA82134418C0AC89F /* api.rs */,
|
||||
16CCC72C07657AEBC320B099 /* auth.rs */,
|
||||
F9FF9FD3CEB0EB658CA17305 /* clone.rs */,
|
||||
2D38A9417EA6B0A133D0D55F /* mod.rs */,
|
||||
);
|
||||
path = github;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
85465A1346B035D3C3DA6236 /* git */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
517A653F5A674050430C1167 /* commit.rs */,
|
||||
4AAEE16E1AC0E472E9EAF907 /* conflict.rs */,
|
||||
DDC76F9C00A29C8C52345249 /* history.rs */,
|
||||
7FCDD5B9C3D021A45C402A43 /* mod.rs */,
|
||||
87F0375DE367A8192708C2D8 /* pulse.rs */,
|
||||
692CE75FC8E670478C4209C8 /* remote.rs */,
|
||||
BB4E3DD2D3BA34BB86C908B6 /* status.rs */,
|
||||
);
|
||||
path = git;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
A44D28635D2D14F8A9E644BC /* bindings */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
D0900BA51E68887E5DA395B7 /* bindings.h */,
|
||||
);
|
||||
path = bindings;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
AE0DF532B1E0DADFEDFE383A /* vault */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
6288FD61B10C36115B12B4A4 /* cache.rs */,
|
||||
5F28F393EB20BA7F8F2102EF /* config_seed.rs */,
|
||||
4F4C2FE8D3697D180E1D4304 /* entry.rs */,
|
||||
7D94F0F22CD503903B63FFE7 /* file.rs */,
|
||||
3E23075B718E12551904D3B6 /* frontmatter.rs */,
|
||||
71EDBD38A067DA56FD3E375F /* getting_started.rs */,
|
||||
84ECD74B7841E52AD6549346 /* image.rs */,
|
||||
1D65CB9525835A85C38D3136 /* migration.rs */,
|
||||
C80E8FDDA822F4F167B601C1 /* mod_tests.rs */,
|
||||
AA64014FA2D16950AD4DF619 /* mod.rs */,
|
||||
0FB1053AF590466CC8971679 /* parsing.rs */,
|
||||
2D31CB45BB367FD6A7FA0B67 /* rename.rs */,
|
||||
5800BA1A3E5C03124E02E946 /* title_sync.rs */,
|
||||
81C8EBDD5CAD14B7F366AA54 /* trash.rs */,
|
||||
);
|
||||
path = vault;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
DC6461BBBF0BDFBB28352CF9 /* laputa */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
3D8616031E45547278A88B60 /* main.mm */,
|
||||
A44D28635D2D14F8A9E644BC /* bindings */,
|
||||
);
|
||||
path = laputa;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
FA63A5561E1F084B5F13E265 /* src */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
6AC39E9370FCCB01E81EC824 /* ai_chat.rs */,
|
||||
6A5C3DA38846B440DD56E98A /* claude_cli.rs */,
|
||||
077AD05C7FB4A784D360399B /* commands.rs */,
|
||||
E1428C0A0CC22F741B7A4A0C /* lib.rs */,
|
||||
50B53E0FE0FC2A5515B48A28 /* main.rs */,
|
||||
F87684B058BB66A215F98DCF /* mcp.rs */,
|
||||
6F67F5CDD6BB137126C5AABB /* menu.rs */,
|
||||
E36BAA25D76F212157A06395 /* search.rs */,
|
||||
C39B460DE1A3B78A0937BFDD /* settings.rs */,
|
||||
BA9FA5D7A7A7574FF74589C8 /* telemetry.rs */,
|
||||
A17AA2469F233241B0DF8BE3 /* vault_list.rs */,
|
||||
5D80057D94E84FA08C34A8B0 /* frontmatter */,
|
||||
85465A1346B035D3C3DA6236 /* git */,
|
||||
6A109CE663DEBC9C99A47D0B /* github */,
|
||||
AE0DF532B1E0DADFEDFE383A /* vault */,
|
||||
);
|
||||
name = src;
|
||||
path = ../../src;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
FD3AA61436162714FAEC418D /* Externals */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
);
|
||||
path = Externals;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
/* End PBXGroup section */
|
||||
|
||||
/* Begin PBXNativeTarget section */
|
||||
DAF25BBC5B5E1A3750AF7F8E /* laputa_iOS */ = {
|
||||
isa = PBXNativeTarget;
|
||||
buildConfigurationList = F8ED295B21C1E1D8BE1CBAB2 /* Build configuration list for PBXNativeTarget "laputa_iOS" */;
|
||||
buildPhases = (
|
||||
72D053FB9C1C9BF219367CA2 /* Build Rust Code */,
|
||||
2FA764A26841E6D98B919C1A /* Sources */,
|
||||
46BE17F676877ABAEEFB1ACF /* Resources */,
|
||||
50978AC1976616D3342459ED /* Frameworks */,
|
||||
);
|
||||
buildRules = (
|
||||
);
|
||||
dependencies = (
|
||||
);
|
||||
name = laputa_iOS;
|
||||
packageProductDependencies = (
|
||||
);
|
||||
productName = laputa_iOS;
|
||||
productReference = CEDEEAE68B0DB4CD43ED89A8 /* laputa_iOS.app */;
|
||||
productType = "com.apple.product-type.application";
|
||||
};
|
||||
/* End PBXNativeTarget section */
|
||||
|
||||
/* Begin PBXProject section */
|
||||
2E2C059AB505791AB84204D8 /* Project object */ = {
|
||||
isa = PBXProject;
|
||||
attributes = {
|
||||
BuildIndependentTargetsInParallel = YES;
|
||||
LastUpgradeCheck = 1430;
|
||||
TargetAttributes = {
|
||||
};
|
||||
};
|
||||
buildConfigurationList = 02446155D1A36B8D10250861 /* Build configuration list for PBXProject "laputa" */;
|
||||
developmentRegion = en;
|
||||
hasScannedForEncodings = 0;
|
||||
knownRegions = (
|
||||
Base,
|
||||
en,
|
||||
);
|
||||
mainGroup = 314C2D9104FDC1DE81AC36AD;
|
||||
minimizedProjectReferenceProxies = 1;
|
||||
preferredProjectObjectVersion = 77;
|
||||
productRefGroup = 021259EC384B5D897634CFF8 /* Products */;
|
||||
projectDirPath = "";
|
||||
projectRoot = "";
|
||||
targets = (
|
||||
DAF25BBC5B5E1A3750AF7F8E /* laputa_iOS */,
|
||||
);
|
||||
};
|
||||
/* End PBXProject section */
|
||||
|
||||
/* Begin PBXResourcesBuildPhase section */
|
||||
46BE17F676877ABAEEFB1ACF /* Resources */ = {
|
||||
isa = PBXResourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
A2279B6B796C960A2951E7C5 /* Assets.xcassets in Resources */,
|
||||
DACF8A23100E74CEA0BE1ED9 /* LaunchScreen.storyboard in Resources */,
|
||||
E0FB400669725FC03B8E9B39 /* assets in Resources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXResourcesBuildPhase section */
|
||||
|
||||
/* Begin PBXShellScriptBuildPhase section */
|
||||
72D053FB9C1C9BF219367CA2 /* Build Rust Code */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
alwaysOutOfDate = 1;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
inputFileListPaths = (
|
||||
);
|
||||
inputPaths = (
|
||||
);
|
||||
name = "Build Rust Code";
|
||||
outputFileListPaths = (
|
||||
);
|
||||
outputPaths = (
|
||||
"$(SRCROOT)/Externals/x86_64/${CONFIGURATION}/libapp.a",
|
||||
"$(SRCROOT)/Externals/arm64/${CONFIGURATION}/libapp.a",
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "npm run -- tauri ios xcode-script -v --platform ${PLATFORM_DISPLAY_NAME:?} --sdk-root ${SDKROOT:?} --framework-search-paths \"${FRAMEWORK_SEARCH_PATHS:?}\" --header-search-paths \"${HEADER_SEARCH_PATHS:?}\" --gcc-preprocessor-definitions \"${GCC_PREPROCESSOR_DEFINITIONS:-}\" --configuration ${CONFIGURATION:?} ${FORCE_COLOR} ${ARCHS:?}";
|
||||
};
|
||||
/* End PBXShellScriptBuildPhase section */
|
||||
|
||||
/* Begin PBXSourcesBuildPhase section */
|
||||
2FA764A26841E6D98B919C1A /* Sources */ = {
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
799F0FF5DD8053B287C9CA46 /* main.mm in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXSourcesBuildPhase section */
|
||||
|
||||
/* Begin XCBuildConfiguration section */
|
||||
3CC8721F138177C2FE029D95 /* debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||
CLANG_ANALYZER_NONNULL = YES;
|
||||
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
|
||||
CLANG_CXX_LIBRARY = "libc++";
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CLANG_ENABLE_OBJC_ARC = YES;
|
||||
CLANG_ENABLE_OBJC_WEAK = YES;
|
||||
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
|
||||
CLANG_WARN_BOOL_CONVERSION = YES;
|
||||
CLANG_WARN_COMMA = YES;
|
||||
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
||||
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
|
||||
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
|
||||
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
|
||||
CLANG_WARN_EMPTY_BODY = YES;
|
||||
CLANG_WARN_ENUM_CONVERSION = YES;
|
||||
CLANG_WARN_INFINITE_RECURSION = YES;
|
||||
CLANG_WARN_INT_CONVERSION = YES;
|
||||
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
|
||||
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
||||
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
|
||||
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
|
||||
CLANG_WARN_STRICT_PROTOTYPES = YES;
|
||||
CLANG_WARN_SUSPICIOUS_MOVE = YES;
|
||||
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
||||
CLANG_WARN_UNREACHABLE_CODE = YES;
|
||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||
COPY_PHASE_STRIP = NO;
|
||||
DEBUG_INFORMATION_FORMAT = dwarf;
|
||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||
ENABLE_TESTABILITY = YES;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu11;
|
||||
GCC_DYNAMIC_NO_PIC = NO;
|
||||
GCC_NO_COMMON_BLOCKS = YES;
|
||||
GCC_OPTIMIZATION_LEVEL = 0;
|
||||
GCC_PREPROCESSOR_DEFINITIONS = (
|
||||
"$(inherited)",
|
||||
"DEBUG=1",
|
||||
);
|
||||
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
|
||||
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
|
||||
GCC_WARN_UNDECLARED_SELECTOR = YES;
|
||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 14.0;
|
||||
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
|
||||
MTL_FAST_MATH = YES;
|
||||
ONLY_ACTIVE_ARCH = YES;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SDKROOT = iphoneos;
|
||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||
SWIFT_VERSION = 5.0;
|
||||
};
|
||||
name = debug;
|
||||
};
|
||||
44D6538EB5E448ECCC49165C /* release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||
CLANG_ANALYZER_NONNULL = YES;
|
||||
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
|
||||
CLANG_CXX_LIBRARY = "libc++";
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CLANG_ENABLE_OBJC_ARC = YES;
|
||||
CLANG_ENABLE_OBJC_WEAK = YES;
|
||||
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
|
||||
CLANG_WARN_BOOL_CONVERSION = YES;
|
||||
CLANG_WARN_COMMA = YES;
|
||||
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
||||
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
|
||||
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
|
||||
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
|
||||
CLANG_WARN_EMPTY_BODY = YES;
|
||||
CLANG_WARN_ENUM_CONVERSION = YES;
|
||||
CLANG_WARN_INFINITE_RECURSION = YES;
|
||||
CLANG_WARN_INT_CONVERSION = YES;
|
||||
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
|
||||
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
||||
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
|
||||
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
|
||||
CLANG_WARN_STRICT_PROTOTYPES = YES;
|
||||
CLANG_WARN_SUSPICIOUS_MOVE = YES;
|
||||
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
||||
CLANG_WARN_UNREACHABLE_CODE = YES;
|
||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||
COPY_PHASE_STRIP = NO;
|
||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||
ENABLE_NS_ASSERTIONS = NO;
|
||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu11;
|
||||
GCC_NO_COMMON_BLOCKS = YES;
|
||||
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
|
||||
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
|
||||
GCC_WARN_UNDECLARED_SELECTOR = YES;
|
||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 14.0;
|
||||
MTL_ENABLE_DEBUG_INFO = NO;
|
||||
MTL_FAST_MATH = YES;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SDKROOT = iphoneos;
|
||||
SWIFT_COMPILATION_MODE = wholemodule;
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-O";
|
||||
SWIFT_VERSION = 5.0;
|
||||
};
|
||||
name = release;
|
||||
};
|
||||
487BF5A97BEC469EBD248159 /* debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
|
||||
ARCHS = (
|
||||
arm64,
|
||||
);
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CODE_SIGN_ENTITLEMENTS = laputa_iOS/laputa_iOS.entitlements;
|
||||
CODE_SIGN_IDENTITY = "iPhone Developer";
|
||||
ENABLE_BITCODE = NO;
|
||||
"EXCLUDED_ARCHS[sdk=iphoneos*]" = x86_64;
|
||||
FRAMEWORK_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"\".\"",
|
||||
);
|
||||
INFOPLIST_FILE = laputa_iOS/Info.plist;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
"LIBRARY_SEARCH_PATHS[arch=arm64]" = "$(inherited) $(PROJECT_DIR)/Externals/arm64/$(CONFIGURATION) $(SDKROOT)/usr/lib/swift $(TOOLCHAIN_DIR)/usr/lib/swift/$(PLATFORM_NAME) $(TOOLCHAIN_DIR)/usr/lib/swift-5.0/$(PLATFORM_NAME)";
|
||||
"LIBRARY_SEARCH_PATHS[arch=x86_64]" = "$(inherited) $(PROJECT_DIR)/Externals/x86_64/$(CONFIGURATION) $(SDKROOT)/usr/lib/swift $(TOOLCHAIN_DIR)/usr/lib/swift/$(PLATFORM_NAME) $(TOOLCHAIN_DIR)/usr/lib/swift-5.0/$(PLATFORM_NAME)";
|
||||
PRODUCT_BUNDLE_IDENTIFIER = club.refactoring.laputa;
|
||||
PRODUCT_NAME = "laputa";
|
||||
SDKROOT = iphoneos;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
VALID_ARCHS = arm64;
|
||||
};
|
||||
name = debug;
|
||||
};
|
||||
FE69C38FBEB0C14D7D5AC27E /* release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
|
||||
ARCHS = (
|
||||
arm64,
|
||||
);
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CODE_SIGN_ENTITLEMENTS = laputa_iOS/laputa_iOS.entitlements;
|
||||
CODE_SIGN_IDENTITY = "iPhone Developer";
|
||||
ENABLE_BITCODE = NO;
|
||||
"EXCLUDED_ARCHS[sdk=iphoneos*]" = x86_64;
|
||||
FRAMEWORK_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"\".\"",
|
||||
);
|
||||
INFOPLIST_FILE = laputa_iOS/Info.plist;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
"LIBRARY_SEARCH_PATHS[arch=arm64]" = "$(inherited) $(PROJECT_DIR)/Externals/arm64/$(CONFIGURATION) $(SDKROOT)/usr/lib/swift $(TOOLCHAIN_DIR)/usr/lib/swift/$(PLATFORM_NAME) $(TOOLCHAIN_DIR)/usr/lib/swift-5.0/$(PLATFORM_NAME)";
|
||||
"LIBRARY_SEARCH_PATHS[arch=x86_64]" = "$(inherited) $(PROJECT_DIR)/Externals/x86_64/$(CONFIGURATION) $(SDKROOT)/usr/lib/swift $(TOOLCHAIN_DIR)/usr/lib/swift/$(PLATFORM_NAME) $(TOOLCHAIN_DIR)/usr/lib/swift-5.0/$(PLATFORM_NAME)";
|
||||
PRODUCT_BUNDLE_IDENTIFIER = club.refactoring.laputa;
|
||||
PRODUCT_NAME = "laputa";
|
||||
SDKROOT = iphoneos;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
VALID_ARCHS = arm64;
|
||||
};
|
||||
name = release;
|
||||
};
|
||||
/* End XCBuildConfiguration section */
|
||||
|
||||
/* Begin XCConfigurationList section */
|
||||
02446155D1A36B8D10250861 /* Build configuration list for PBXProject "laputa" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
3CC8721F138177C2FE029D95 /* debug */,
|
||||
44D6538EB5E448ECCC49165C /* release */,
|
||||
);
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = debug;
|
||||
};
|
||||
F8ED295B21C1E1D8BE1CBAB2 /* Build configuration list for PBXNativeTarget "laputa_iOS" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
487BF5A97BEC469EBD248159 /* debug */,
|
||||
FE69C38FBEB0C14D7D5AC27E /* release */,
|
||||
);
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = debug;
|
||||
};
|
||||
/* End XCConfigurationList section */
|
||||
};
|
||||
rootObject = 2E2C059AB505791AB84204D8 /* Project object */;
|
||||
}
|
||||
7
src-tauri/gen/apple/laputa.xcodeproj/project.xcworkspace/contents.xcworkspacedata
generated
Normal file
@@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Workspace
|
||||
version = "1.0">
|
||||
<FileRef
|
||||
location = "self:">
|
||||
</FileRef>
|
||||
</Workspace>
|
||||
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>BuildSystemType</key>
|
||||
<string>Original</string>
|
||||
<key>DisableBuildSystemDeprecationDiagnostic</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -0,0 +1,131 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "1430"
|
||||
version = "1.7">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
buildImplicitDependencies = "YES"
|
||||
runPostActionsOnFailure = "NO">
|
||||
<BuildActionEntries>
|
||||
<BuildActionEntry
|
||||
buildForTesting = "YES"
|
||||
buildForRunning = "YES"
|
||||
buildForProfiling = "YES"
|
||||
buildForArchiving = "YES"
|
||||
buildForAnalyzing = "YES">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "DAF25BBC5B5E1A3750AF7F8E"
|
||||
BuildableName = "laputa_iOS.app"
|
||||
BlueprintName = "laputa_iOS"
|
||||
ReferencedContainer = "container:laputa.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildActionEntry>
|
||||
</BuildActionEntries>
|
||||
</BuildAction>
|
||||
<TestAction
|
||||
buildConfiguration = "debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
shouldUseLaunchSchemeArgsEnv = "NO"
|
||||
onlyGenerateCoverageForSpecifiedTargets = "NO">
|
||||
<MacroExpansion>
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "DAF25BBC5B5E1A3750AF7F8E"
|
||||
BuildableName = "laputa_iOS.app"
|
||||
BlueprintName = "laputa_iOS"
|
||||
ReferencedContainer = "container:laputa.xcodeproj">
|
||||
</BuildableReference>
|
||||
</MacroExpansion>
|
||||
<Testables>
|
||||
</Testables>
|
||||
<CommandLineArguments>
|
||||
</CommandLineArguments>
|
||||
<EnvironmentVariables>
|
||||
<EnvironmentVariable
|
||||
key = "RUST_BACKTRACE"
|
||||
value = "full"
|
||||
isEnabled = "YES">
|
||||
</EnvironmentVariable>
|
||||
<EnvironmentVariable
|
||||
key = "RUST_LOG"
|
||||
value = "info"
|
||||
isEnabled = "YES">
|
||||
</EnvironmentVariable>
|
||||
</EnvironmentVariables>
|
||||
</TestAction>
|
||||
<LaunchAction
|
||||
buildConfiguration = "debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
launchStyle = "0"
|
||||
useCustomWorkingDirectory = "NO"
|
||||
ignoresPersistentStateOnLaunch = "NO"
|
||||
debugDocumentVersioning = "YES"
|
||||
debugServiceExtension = "internal"
|
||||
allowLocationSimulation = "YES">
|
||||
<BuildableProductRunnable
|
||||
runnableDebuggingMode = "0">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "DAF25BBC5B5E1A3750AF7F8E"
|
||||
BuildableName = "laputa_iOS.app"
|
||||
BlueprintName = "laputa_iOS"
|
||||
ReferencedContainer = "container:laputa.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildableProductRunnable>
|
||||
<CommandLineArguments>
|
||||
</CommandLineArguments>
|
||||
<EnvironmentVariables>
|
||||
<EnvironmentVariable
|
||||
key = "RUST_BACKTRACE"
|
||||
value = "full"
|
||||
isEnabled = "YES">
|
||||
</EnvironmentVariable>
|
||||
<EnvironmentVariable
|
||||
key = "RUST_LOG"
|
||||
value = "info"
|
||||
isEnabled = "YES">
|
||||
</EnvironmentVariable>
|
||||
</EnvironmentVariables>
|
||||
</LaunchAction>
|
||||
<ProfileAction
|
||||
buildConfiguration = "release"
|
||||
shouldUseLaunchSchemeArgsEnv = "NO"
|
||||
savedToolIdentifier = ""
|
||||
useCustomWorkingDirectory = "NO"
|
||||
debugDocumentVersioning = "YES">
|
||||
<BuildableProductRunnable
|
||||
runnableDebuggingMode = "0">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "DAF25BBC5B5E1A3750AF7F8E"
|
||||
BuildableName = "laputa_iOS.app"
|
||||
BlueprintName = "laputa_iOS"
|
||||
ReferencedContainer = "container:laputa.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildableProductRunnable>
|
||||
<CommandLineArguments>
|
||||
</CommandLineArguments>
|
||||
<EnvironmentVariables>
|
||||
<EnvironmentVariable
|
||||
key = "RUST_BACKTRACE"
|
||||
value = "full"
|
||||
isEnabled = "YES">
|
||||
</EnvironmentVariable>
|
||||
<EnvironmentVariable
|
||||
key = "RUST_LOG"
|
||||
value = "info"
|
||||
isEnabled = "YES">
|
||||
</EnvironmentVariable>
|
||||
</EnvironmentVariables>
|
||||
</ProfileAction>
|
||||
<AnalyzeAction
|
||||
buildConfiguration = "debug">
|
||||
</AnalyzeAction>
|
||||
<ArchiveAction
|
||||
buildConfiguration = "release"
|
||||
revealArchiveInOrganizer = "YES">
|
||||
</ArchiveAction>
|
||||
</Scheme>
|
||||
44
src-tauri/gen/apple/laputa_iOS/Info.plist
Normal file
@@ -0,0 +1,44 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>$(EXECUTABLE_NAME)</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>$(PRODUCT_NAME)</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>0.1.0</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>0.1.0</string>
|
||||
<key>LSRequiresIPhoneOS</key>
|
||||
<true/>
|
||||
<key>UILaunchStoryboardName</key>
|
||||
<string>LaunchScreen</string>
|
||||
<key>UIRequiredDeviceCapabilities</key>
|
||||
<array>
|
||||
<string>arm64</string>
|
||||
<string>metal</string>
|
||||
</array>
|
||||
<key>UISupportedInterfaceOrientations</key>
|
||||
<array>
|
||||
<string>UIInterfaceOrientationPortrait</string>
|
||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||
</array>
|
||||
<key>UISupportedInterfaceOrientations~ipad</key>
|
||||
<array>
|
||||
<string>UIInterfaceOrientationPortrait</string>
|
||||
<string>UIInterfaceOrientationPortraitUpsideDown</string>
|
||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
5
src-tauri/gen/apple/laputa_iOS/laputa_iOS.entitlements
Normal file
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict/>
|
||||
</plist>
|
||||
88
src-tauri/gen/apple/project.yml
Normal file
@@ -0,0 +1,88 @@
|
||||
name: laputa
|
||||
options:
|
||||
bundleIdPrefix: club.refactoring.laputa
|
||||
deploymentTarget:
|
||||
iOS: 14.0
|
||||
fileGroups: [../../src]
|
||||
configs:
|
||||
debug: debug
|
||||
release: release
|
||||
settingGroups:
|
||||
app:
|
||||
base:
|
||||
PRODUCT_NAME: laputa
|
||||
PRODUCT_BUNDLE_IDENTIFIER: club.refactoring.laputa
|
||||
targetTemplates:
|
||||
app:
|
||||
type: application
|
||||
sources:
|
||||
- path: Sources
|
||||
scheme:
|
||||
environmentVariables:
|
||||
RUST_BACKTRACE: full
|
||||
RUST_LOG: info
|
||||
settings:
|
||||
groups: [app]
|
||||
targets:
|
||||
laputa_iOS:
|
||||
type: application
|
||||
platform: iOS
|
||||
sources:
|
||||
- path: Sources
|
||||
- path: Assets.xcassets
|
||||
- path: Externals
|
||||
- path: laputa_iOS
|
||||
- path: assets
|
||||
buildPhase: resources
|
||||
type: folder
|
||||
- path: LaunchScreen.storyboard
|
||||
info:
|
||||
path: laputa_iOS/Info.plist
|
||||
properties:
|
||||
LSRequiresIPhoneOS: true
|
||||
UILaunchStoryboardName: LaunchScreen
|
||||
UIRequiredDeviceCapabilities: [arm64, metal]
|
||||
UISupportedInterfaceOrientations:
|
||||
- UIInterfaceOrientationPortrait
|
||||
- UIInterfaceOrientationLandscapeLeft
|
||||
- UIInterfaceOrientationLandscapeRight
|
||||
UISupportedInterfaceOrientations~ipad:
|
||||
- UIInterfaceOrientationPortrait
|
||||
- UIInterfaceOrientationPortraitUpsideDown
|
||||
- UIInterfaceOrientationLandscapeLeft
|
||||
- UIInterfaceOrientationLandscapeRight
|
||||
CFBundleShortVersionString: 0.1.0
|
||||
CFBundleVersion: "0.1.0"
|
||||
entitlements:
|
||||
path: laputa_iOS/laputa_iOS.entitlements
|
||||
scheme:
|
||||
environmentVariables:
|
||||
RUST_BACKTRACE: full
|
||||
RUST_LOG: info
|
||||
settings:
|
||||
base:
|
||||
ENABLE_BITCODE: false
|
||||
ARCHS: [arm64]
|
||||
VALID_ARCHS: arm64
|
||||
LIBRARY_SEARCH_PATHS[arch=x86_64]: $(inherited) $(PROJECT_DIR)/Externals/x86_64/$(CONFIGURATION) $(SDKROOT)/usr/lib/swift $(TOOLCHAIN_DIR)/usr/lib/swift/$(PLATFORM_NAME) $(TOOLCHAIN_DIR)/usr/lib/swift-5.0/$(PLATFORM_NAME)
|
||||
LIBRARY_SEARCH_PATHS[arch=arm64]: $(inherited) $(PROJECT_DIR)/Externals/arm64/$(CONFIGURATION) $(SDKROOT)/usr/lib/swift $(TOOLCHAIN_DIR)/usr/lib/swift/$(PLATFORM_NAME) $(TOOLCHAIN_DIR)/usr/lib/swift-5.0/$(PLATFORM_NAME)
|
||||
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES: true
|
||||
EXCLUDED_ARCHS[sdk=iphoneos*]: x86_64
|
||||
groups: [app]
|
||||
dependencies:
|
||||
- framework: libapp.a
|
||||
embed: false
|
||||
- sdk: CoreGraphics.framework
|
||||
- sdk: Metal.framework
|
||||
- sdk: MetalKit.framework
|
||||
- sdk: QuartzCore.framework
|
||||
- sdk: Security.framework
|
||||
- sdk: UIKit.framework
|
||||
- sdk: WebKit.framework
|
||||
preBuildScripts:
|
||||
- script: npm run -- tauri ios xcode-script -v --platform ${PLATFORM_DISPLAY_NAME:?} --sdk-root ${SDKROOT:?} --framework-search-paths "${FRAMEWORK_SEARCH_PATHS:?}" --header-search-paths "${HEADER_SEARCH_PATHS:?}" --gcc-preprocessor-definitions "${GCC_PREPROCESSOR_DEFINITIONS:-}" --configuration ${CONFIGURATION:?} ${FORCE_COLOR} ${ARCHS:?}
|
||||
name: Build Rust Code
|
||||
basedOnDependencyAnalysis: false
|
||||
outputFiles:
|
||||
- $(SRCROOT)/Externals/x86_64/${CONFIGURATION}/libapp.a
|
||||
- $(SRCROOT)/Externals/arm64/${CONFIGURATION}/libapp.a
|
||||
@@ -1,386 +0,0 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct AiChatRequest {
|
||||
pub model: Option<String>,
|
||||
pub messages: Vec<AiMessage>,
|
||||
pub system: Option<String>,
|
||||
pub max_tokens: Option<u32>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize, Clone)]
|
||||
pub struct AiMessage {
|
||||
pub role: String,
|
||||
pub content: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct AiChatResponse {
|
||||
pub content: String,
|
||||
pub model: String,
|
||||
pub stop_reason: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct AnthropicResponse {
|
||||
content: Vec<ContentBlock>,
|
||||
model: String,
|
||||
stop_reason: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct ContentBlock {
|
||||
text: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
struct AnthropicRequest {
|
||||
model: String,
|
||||
max_tokens: u32,
|
||||
messages: Vec<AiMessage>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
system: Option<String>,
|
||||
}
|
||||
|
||||
fn get_api_key() -> Result<String, String> {
|
||||
std::env::var("ANTHROPIC_API_KEY")
|
||||
.map_err(|_| "ANTHROPIC_API_KEY environment variable not set".to_string())
|
||||
}
|
||||
|
||||
fn build_request(req: &AiChatRequest) -> AnthropicRequest {
|
||||
AnthropicRequest {
|
||||
model: req
|
||||
.model
|
||||
.clone()
|
||||
.unwrap_or_else(|| "claude-3-5-haiku-20241022".to_string()),
|
||||
max_tokens: req.max_tokens.unwrap_or(4096),
|
||||
messages: req.messages.clone(),
|
||||
system: req.system.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
fn extract_response_text(resp: &AnthropicResponse) -> String {
|
||||
resp.content
|
||||
.iter()
|
||||
.filter_map(|block| block.text.as_ref())
|
||||
.cloned()
|
||||
.collect::<Vec<_>>()
|
||||
.join("")
|
||||
}
|
||||
|
||||
pub async fn send_chat(req: AiChatRequest) -> Result<AiChatResponse, String> {
|
||||
let api_key = get_api_key()?;
|
||||
send_chat_with_base(req, "https://api.anthropic.com", &api_key).await
|
||||
}
|
||||
|
||||
async fn send_chat_with_base(
|
||||
req: AiChatRequest,
|
||||
api_base: &str,
|
||||
api_key: &str,
|
||||
) -> Result<AiChatResponse, String> {
|
||||
let anthropic_req = build_request(&req);
|
||||
|
||||
let client = reqwest::Client::new();
|
||||
let response = client
|
||||
.post(format!("{}/v1/messages", api_base))
|
||||
.header("x-api-key", api_key)
|
||||
.header("anthropic-version", "2023-06-01")
|
||||
.header("content-type", "application/json")
|
||||
.json(&anthropic_req)
|
||||
.send()
|
||||
.await
|
||||
.map_err(|e| format!("Request failed: {}", e))?;
|
||||
|
||||
if !response.status().is_success() {
|
||||
let status = response.status();
|
||||
let body = response.text().await.unwrap_or_default();
|
||||
return Err(format!("Anthropic API error ({}): {}", status, body));
|
||||
}
|
||||
|
||||
let anthropic_resp: AnthropicResponse = response
|
||||
.json()
|
||||
.await
|
||||
.map_err(|e| format!("Failed to parse response: {}", e))?;
|
||||
|
||||
Ok(AiChatResponse {
|
||||
content: extract_response_text(&anthropic_resp),
|
||||
model: anthropic_resp.model,
|
||||
stop_reason: anthropic_resp.stop_reason,
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
// ── Pure logic tests ─────────────────────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn test_build_request_defaults() {
|
||||
let req = AiChatRequest {
|
||||
model: None,
|
||||
messages: vec![AiMessage {
|
||||
role: "user".to_string(),
|
||||
content: "Hello".to_string(),
|
||||
}],
|
||||
system: None,
|
||||
max_tokens: None,
|
||||
};
|
||||
let built = build_request(&req);
|
||||
assert_eq!(built.model, "claude-3-5-haiku-20241022");
|
||||
assert_eq!(built.max_tokens, 4096);
|
||||
assert!(built.system.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_build_request_custom() {
|
||||
let req = AiChatRequest {
|
||||
model: Some("claude-sonnet-4-20250514".to_string()),
|
||||
messages: vec![],
|
||||
system: Some("You are helpful".to_string()),
|
||||
max_tokens: Some(1024),
|
||||
};
|
||||
let built = build_request(&req);
|
||||
assert_eq!(built.model, "claude-sonnet-4-20250514");
|
||||
assert_eq!(built.max_tokens, 1024);
|
||||
assert_eq!(built.system.unwrap(), "You are helpful");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_extract_response_text() {
|
||||
let resp = AnthropicResponse {
|
||||
content: vec![
|
||||
ContentBlock {
|
||||
text: Some("Hello ".to_string()),
|
||||
},
|
||||
ContentBlock {
|
||||
text: Some("world".to_string()),
|
||||
},
|
||||
ContentBlock { text: None },
|
||||
],
|
||||
model: "test".to_string(),
|
||||
stop_reason: Some("end_turn".to_string()),
|
||||
};
|
||||
assert_eq!(extract_response_text(&resp), "Hello world");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_extract_response_text_empty() {
|
||||
let resp = AnthropicResponse {
|
||||
content: vec![],
|
||||
model: "test".to_string(),
|
||||
stop_reason: None,
|
||||
};
|
||||
assert_eq!(extract_response_text(&resp), "");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_extract_response_text_all_none() {
|
||||
let resp = AnthropicResponse {
|
||||
content: vec![ContentBlock { text: None }, ContentBlock { text: None }],
|
||||
model: "test".to_string(),
|
||||
stop_reason: None,
|
||||
};
|
||||
assert_eq!(extract_response_text(&resp), "");
|
||||
}
|
||||
|
||||
// Mutex to serialize env-var tests and avoid race conditions in parallel test runs
|
||||
static ENV_MUTEX: std::sync::Mutex<()> = std::sync::Mutex::new(());
|
||||
|
||||
#[test]
|
||||
fn test_get_api_key_missing() {
|
||||
let _guard = ENV_MUTEX.lock().unwrap();
|
||||
// Temporarily clear the env var
|
||||
let prev = std::env::var("ANTHROPIC_API_KEY").ok();
|
||||
unsafe {
|
||||
std::env::remove_var("ANTHROPIC_API_KEY");
|
||||
}
|
||||
let result = get_api_key();
|
||||
// Restore
|
||||
if let Some(val) = prev {
|
||||
unsafe {
|
||||
std::env::set_var("ANTHROPIC_API_KEY", val);
|
||||
}
|
||||
}
|
||||
assert!(result.is_err());
|
||||
assert!(result
|
||||
.unwrap_err()
|
||||
.contains("ANTHROPIC_API_KEY environment variable not set"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_api_key_present() {
|
||||
let _guard = ENV_MUTEX.lock().unwrap();
|
||||
let prev = std::env::var("ANTHROPIC_API_KEY").ok();
|
||||
unsafe {
|
||||
std::env::set_var("ANTHROPIC_API_KEY", "sk-test-key-123");
|
||||
}
|
||||
let result = get_api_key();
|
||||
if let Some(val) = prev {
|
||||
unsafe {
|
||||
std::env::set_var("ANTHROPIC_API_KEY", val);
|
||||
}
|
||||
} else {
|
||||
unsafe {
|
||||
std::env::remove_var("ANTHROPIC_API_KEY");
|
||||
}
|
||||
}
|
||||
assert!(result.is_ok());
|
||||
assert_eq!(result.unwrap(), "sk-test-key-123");
|
||||
}
|
||||
|
||||
// ── HTTP mock tests ──────────────────────────────────────────────────────
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_send_chat_success() {
|
||||
let mut server = mockito::Server::new_async().await;
|
||||
let mock = server
|
||||
.mock("POST", "/v1/messages")
|
||||
.with_status(200)
|
||||
.with_header("content-type", "application/json")
|
||||
.with_body(
|
||||
r#"{"id":"msg_01","type":"message","role":"assistant","content":[{"type":"text","text":"Hello there!"}],"model":"claude-3-5-haiku-20241022","stop_reason":"end_turn","stop_sequence":null,"usage":{"input_tokens":10,"output_tokens":5}}"#,
|
||||
)
|
||||
.create_async()
|
||||
.await;
|
||||
|
||||
let req = AiChatRequest {
|
||||
model: None,
|
||||
messages: vec![AiMessage {
|
||||
role: "user".to_string(),
|
||||
content: "Say hello".to_string(),
|
||||
}],
|
||||
system: None,
|
||||
max_tokens: None,
|
||||
};
|
||||
|
||||
let result = send_chat_with_base(req, &server.url(), "sk-test-key").await;
|
||||
mock.assert_async().await;
|
||||
assert!(result.is_ok());
|
||||
let resp = result.unwrap();
|
||||
assert_eq!(resp.content, "Hello there!");
|
||||
assert_eq!(resp.model, "claude-3-5-haiku-20241022");
|
||||
assert_eq!(resp.stop_reason, Some("end_turn".to_string()));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_send_chat_with_system_prompt() {
|
||||
let mut server = mockito::Server::new_async().await;
|
||||
let mock = server
|
||||
.mock("POST", "/v1/messages")
|
||||
.with_status(200)
|
||||
.with_header("content-type", "application/json")
|
||||
.with_body(
|
||||
r#"{"id":"msg_02","type":"message","role":"assistant","content":[{"type":"text","text":"I am a helpful assistant."}],"model":"claude-sonnet-4-20250514","stop_reason":"end_turn","stop_sequence":null,"usage":{"input_tokens":20,"output_tokens":8}}"#,
|
||||
)
|
||||
.create_async()
|
||||
.await;
|
||||
|
||||
let req = AiChatRequest {
|
||||
model: Some("claude-sonnet-4-20250514".to_string()),
|
||||
messages: vec![AiMessage {
|
||||
role: "user".to_string(),
|
||||
content: "Who are you?".to_string(),
|
||||
}],
|
||||
system: Some("You are a helpful assistant.".to_string()),
|
||||
max_tokens: Some(512),
|
||||
};
|
||||
|
||||
let result = send_chat_with_base(req, &server.url(), "sk-key").await;
|
||||
mock.assert_async().await;
|
||||
assert!(result.is_ok());
|
||||
let resp = result.unwrap();
|
||||
assert_eq!(resp.model, "claude-sonnet-4-20250514");
|
||||
assert_eq!(resp.content, "I am a helpful assistant.");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_send_chat_api_error() {
|
||||
let mut server = mockito::Server::new_async().await;
|
||||
let mock = server
|
||||
.mock("POST", "/v1/messages")
|
||||
.with_status(401)
|
||||
.with_header("content-type", "application/json")
|
||||
.with_body(r#"{"type":"error","error":{"type":"authentication_error","message":"Invalid API key"}}"#)
|
||||
.create_async()
|
||||
.await;
|
||||
|
||||
let req = AiChatRequest {
|
||||
model: None,
|
||||
messages: vec![AiMessage {
|
||||
role: "user".to_string(),
|
||||
content: "Hello".to_string(),
|
||||
}],
|
||||
system: None,
|
||||
max_tokens: None,
|
||||
};
|
||||
|
||||
let result = send_chat_with_base(req, &server.url(), "bad-key").await;
|
||||
mock.assert_async().await;
|
||||
assert!(result.is_err());
|
||||
let err = result.unwrap_err();
|
||||
assert!(
|
||||
err.contains("Anthropic API error") && err.contains("401"),
|
||||
"unexpected error: {}",
|
||||
err
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_send_chat_rate_limit() {
|
||||
let mut server = mockito::Server::new_async().await;
|
||||
let mock = server
|
||||
.mock("POST", "/v1/messages")
|
||||
.with_status(429)
|
||||
.with_header("content-type", "application/json")
|
||||
.with_body(r#"{"type":"error","error":{"type":"rate_limit_error","message":"Rate limit exceeded"}}"#)
|
||||
.create_async()
|
||||
.await;
|
||||
|
||||
let req = AiChatRequest {
|
||||
model: None,
|
||||
messages: vec![],
|
||||
system: None,
|
||||
max_tokens: None,
|
||||
};
|
||||
|
||||
let result = send_chat_with_base(req, &server.url(), "sk-key").await;
|
||||
mock.assert_async().await;
|
||||
assert!(result.is_err());
|
||||
let err = result.unwrap_err();
|
||||
assert!(
|
||||
err.contains("Anthropic API error") && err.contains("429"),
|
||||
"unexpected error: {}",
|
||||
err
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_send_chat_multiple_content_blocks() {
|
||||
let mut server = mockito::Server::new_async().await;
|
||||
let mock = server
|
||||
.mock("POST", "/v1/messages")
|
||||
.with_status(200)
|
||||
.with_header("content-type", "application/json")
|
||||
.with_body(
|
||||
r#"{"id":"msg_03","type":"message","role":"assistant","content":[{"type":"text","text":"Part one. "},{"type":"text","text":"Part two."}],"model":"claude-3-5-haiku-20241022","stop_reason":"end_turn","stop_sequence":null,"usage":{"input_tokens":5,"output_tokens":10}}"#,
|
||||
)
|
||||
.create_async()
|
||||
.await;
|
||||
|
||||
let req = AiChatRequest {
|
||||
model: None,
|
||||
messages: vec![AiMessage {
|
||||
role: "user".to_string(),
|
||||
content: "Give me two parts".to_string(),
|
||||
}],
|
||||
system: None,
|
||||
max_tokens: None,
|
||||
};
|
||||
|
||||
let result = send_chat_with_base(req, &server.url(), "sk-key").await;
|
||||
mock.assert_async().await;
|
||||
assert!(result.is_ok());
|
||||
assert_eq!(result.unwrap().content, "Part one. Part two.");
|
||||
}
|
||||
}
|
||||