@zzusp/ccsm 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +232 -0
- package/bin/cli.mjs +52 -0
- package/dist/assets/DiskUsage-Bq4VaoUA.js +2 -0
- package/dist/assets/DiskUsage-Bq4VaoUA.js.map +1 -0
- package/dist/assets/ImportPage-b8NORa8b.js +2 -0
- package/dist/assets/ImportPage-b8NORa8b.js.map +1 -0
- package/dist/assets/ProjectMemory-aSV8UzQ9.js +2 -0
- package/dist/assets/ProjectMemory-aSV8UzQ9.js.map +1 -0
- package/dist/assets/charts-A5eNHLjX.js +56 -0
- package/dist/assets/charts-A5eNHLjX.js.map +1 -0
- package/dist/assets/geist-mono-cyrillic-wght-normal-BZdD_g9V.woff2 +0 -0
- package/dist/assets/geist-mono-latin-ext-wght-normal-b6lpi8_2.woff2 +0 -0
- package/dist/assets/geist-mono-latin-wght-normal-Cjtb1TV-.woff2 +0 -0
- package/dist/assets/index-DLATR3tZ.js +5 -0
- package/dist/assets/index-DLATR3tZ.js.map +1 -0
- package/dist/assets/index-DLDtbkux.css +1 -0
- package/dist/assets/plus-jakarta-sans-latin-ext-wght-italic-DJWiFoht.woff2 +0 -0
- package/dist/assets/plus-jakarta-sans-latin-ext-wght-normal-DmpS2jIq.woff2 +0 -0
- package/dist/assets/plus-jakarta-sans-latin-wght-italic-DnD1KgkH.woff2 +0 -0
- package/dist/assets/plus-jakarta-sans-latin-wght-normal-eXO_dkmS.woff2 +0 -0
- package/dist/assets/plus-jakarta-sans-vietnamese-wght-italic-CPBsCcxN.woff2 +0 -0
- package/dist/assets/plus-jakarta-sans-vietnamese-wght-normal-qRpaaN48.woff2 +0 -0
- package/dist/assets/query-C1K1uQRu.js +2 -0
- package/dist/assets/query-C1K1uQRu.js.map +1 -0
- package/dist/assets/react-W0jzChlo.js +50 -0
- package/dist/assets/react-W0jzChlo.js.map +1 -0
- package/dist/assets/router-DfbutHY3.js +13 -0
- package/dist/assets/router-DfbutHY3.js.map +1 -0
- package/dist/assets/vendor-CH80ylbS.js +19 -0
- package/dist/assets/vendor-CH80ylbS.js.map +1 -0
- package/dist/favicon.svg +7 -0
- package/dist/index.html +30 -0
- package/package.json +72 -0
- package/server/index.ts +126 -0
- package/server/lib/active-sessions.ts +95 -0
- package/server/lib/bundle.ts +86 -0
- package/server/lib/claude-paths.ts +36 -0
- package/server/lib/constants.ts +7 -0
- package/server/lib/delete-project.ts +100 -0
- package/server/lib/delete.ts +203 -0
- package/server/lib/disk-usage.ts +83 -0
- package/server/lib/encode-cwd.ts +24 -0
- package/server/lib/export-bundle.ts +236 -0
- package/server/lib/fs-size.ts +38 -0
- package/server/lib/import-bundle.ts +488 -0
- package/server/lib/load-memory.ts +120 -0
- package/server/lib/load-session.ts +209 -0
- package/server/lib/open-folder.ts +40 -0
- package/server/lib/parse-jsonl.ts +107 -0
- package/server/lib/port.ts +23 -0
- package/server/lib/rename-session.ts +0 -0
- package/server/lib/safe-id.ts +6 -0
- package/server/lib/scan.ts +183 -0
- package/server/lib/search-all.ts +130 -0
- package/server/lib/search-session.ts +203 -0
- package/server/lib/system-tags.ts +20 -0
- package/server/routes/disk.ts +9 -0
- package/server/routes/import.ts +87 -0
- package/server/routes/projects.ts +104 -0
- package/server/routes/search.ts +79 -0
- package/server/routes/sessions.ts +81 -0
- package/server/types.ts +1 -0
- package/shared/constants.ts +2 -0
- package/shared/types.ts +359 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Aaron Sun
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
# Claude Code Session Manager
|
|
2
|
+
|
|
3
|
+
A local web UI to view and clean up Claude Code session history stored under `~/.claude/`.
|
|
4
|
+
|
|
5
|
+
> **Read-only on disk by default.** The server only writes when you explicitly act in the UI — *Delete* a session, *Export* a bundle (written to a folder **outside** `~/.claude/`), or *Import* one. Active sessions (with a live PID or recent activity) are never deleted or overwritten.
|
|
6
|
+
|
|
7
|
+
## Screenshots
|
|
8
|
+
|
|
9
|
+
> See [`docs/screenshots/README.md`](docs/screenshots/README.md) for the capture recipe (Windows / macOS / Linux) if you want to refresh these.
|
|
10
|
+
|
|
11
|
+
<p align="center">
|
|
12
|
+
<a href="docs/screenshots/project-detail.png"><img src="docs/screenshots/project-detail.png" alt="Project detail — session table with multi-select and live/idle badges" width="720"></a>
|
|
13
|
+
<br><sub><b>Project detail</b> — sessions in one project, multi-select, status badges</sub>
|
|
14
|
+
</p>
|
|
15
|
+
|
|
16
|
+
<p align="center">
|
|
17
|
+
<a href="docs/screenshots/session-detail.png"><img src="docs/screenshots/session-detail.png" alt="Session detail — message timeline with a highlighted search hit" width="720"></a>
|
|
18
|
+
<br><sub><b>Session detail</b> — full message timeline with search highlight</sub>
|
|
19
|
+
</p>
|
|
20
|
+
|
|
21
|
+
<p align="center">
|
|
22
|
+
<a href="docs/screenshots/disk-usage.png"><img src="docs/screenshots/disk-usage.png" alt="Disk usage — pie chart, monthly bar chart, top-20 sessions table" width="720"></a>
|
|
23
|
+
<br><sub><b>Disk usage</b> — pie + monthly bars + top-20 table</sub>
|
|
24
|
+
</p>
|
|
25
|
+
|
|
26
|
+
## Features
|
|
27
|
+
|
|
28
|
+
| Page | What you can do |
|
|
29
|
+
|---|---|
|
|
30
|
+
| **Projects** (`/`) | See every Claude Code project (one per `cwd`) with session count, total bytes on disk, last-activity time. |
|
|
31
|
+
| **Project detail** (`/projects/:id`) | Browse all sessions in one project. Multi-select + cascade-delete. Each row shows title, message count, byte breakdown, and a status badge (`live · pid N` / `recently active` / `idle`). Inline rename appends a `custom-title` record to the session's `.jsonl` (refused while a live PID owns the session). *Open folder* reveals the project's working directory in the OS file manager (Explorer / Finder / `xdg-open`). *Export* bundles the selected sessions (or all) plus the project's memory into a portable folder. |
|
|
32
|
+
| **Import** (`/import`) | Bring a project's sessions + memory from another device. The bundle is path-independent; on import you pick the local target folder and paths are remapped to this machine. A dry-run preview shows exactly what will be created / skipped / overwritten before any write. See [Cross-device sharing](#cross-device-sharing). |
|
|
33
|
+
| **Session detail** (`/projects/:id/sessions/:sid`) | Full message timeline: text, tool calls (collapsible), tool results, thinking blocks. Sticky search bar with client-side highlight. Toggle to show or hide system messages (`<command-name>` etc.). Inline *Delete* (top-right of the masthead) removes the current session and returns to the project list. |
|
|
34
|
+
| **Project memory** (`/projects/:id/memory`) | Two-pane reader for `~/.claude/projects/<encoded-cwd>/memory/`: searchable file list (sort by index / recent / name / size) on the left, rendered Markdown on the right, with `MEMORY.md` pinned as the index. |
|
|
35
|
+
| **Disk usage** (`/disk`) | Pie chart by project, monthly bar chart, top-20 largest sessions with deep links. |
|
|
36
|
+
| **Cross-session search** (⌘K / Ctrl+K) | Global modal that streams matches from every project as you type. Searches text, tool calls, and thinking blocks; each result deep-links into the session at the matched message. |
|
|
37
|
+
|
|
38
|
+
The persistent sidebar carries the search trigger plus locale (zh / en) and theme (light / dark) toggles. The HTTP listener exposes the same content as a single SPA, so deep links like `/projects/:id/sessions/:sid?q=foo` are sharable between browser tabs on the same machine.
|
|
39
|
+
|
|
40
|
+
## Quick start
|
|
41
|
+
|
|
42
|
+
Requires **Node 22+** (Node 24 recommended).
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
git clone <this-repo>
|
|
46
|
+
cd claude-code-session
|
|
47
|
+
npm install
|
|
48
|
+
npm run dev
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Open <http://localhost:5173>.
|
|
52
|
+
|
|
53
|
+
| Script | Effect |
|
|
54
|
+
|---|---|
|
|
55
|
+
| `npm run dev` | Concurrent backend (`:3131`) + Vite dev server (`:5173`) with HMR. Vite proxies `/api/*` to the backend. |
|
|
56
|
+
| `npm run build` | Builds the SPA to `dist/`. |
|
|
57
|
+
| `npm run start` | Single-process production mode: backend on `:3131` serves `dist/` + the API. |
|
|
58
|
+
| `npm run typecheck` | `tsc -b` over both server and web projects. |
|
|
59
|
+
|
|
60
|
+
The HTTP server binds to `127.0.0.1` only — never reachable from the LAN.
|
|
61
|
+
Default port is `3131`. If it's busy, the server tries `3132 … 3140` and prints the actual port to stdout.
|
|
62
|
+
|
|
63
|
+
## Install as a CLI
|
|
64
|
+
|
|
65
|
+
Published to npm, the manager installs as a single command — `ccsm` — that starts the
|
|
66
|
+
production server (the built SPA + the API in one process, no separate Vite dev server).
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
# Run once, without installing
|
|
70
|
+
npx @zzusp/ccsm
|
|
71
|
+
|
|
72
|
+
# …or install globally and run `ccsm` from anywhere
|
|
73
|
+
npm install -g @zzusp/ccsm
|
|
74
|
+
ccsm
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
It prints the URL it picked (e.g. <http://127.0.0.1:3131>); open that. Requires **Node 22+**.
|
|
78
|
+
|
|
79
|
+
| Flag | Effect |
|
|
80
|
+
|---|---|
|
|
81
|
+
| `-p, --port <number>` | Port to listen on. Default: first free port in `3131 … 3140`. If the given port is busy, `ccsm` exits — it won't silently pick another. |
|
|
82
|
+
| `--host <host>` | Host to bind. Default `127.0.0.1` (loopback only). Pass `0.0.0.0` to expose the UI on your LAN — **no authentication**, see [Security model](#security-model). |
|
|
83
|
+
| `-o, --open` | Open the UI in your default browser once the server is listening. |
|
|
84
|
+
| `-h, --help` | Show usage and exit. |
|
|
85
|
+
| `-v, --version` | Print the version and exit. |
|
|
86
|
+
|
|
87
|
+
```bash
|
|
88
|
+
ccsm --port 4000 --open # custom port, then pop open the browser
|
|
89
|
+
ccsm --host 0.0.0.0 # expose on the LAN (trusted networks only)
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
**Install from a local checkout** (without publishing to npm):
|
|
93
|
+
|
|
94
|
+
```bash
|
|
95
|
+
git clone <this-repo> && cd claude-code-session
|
|
96
|
+
npm install
|
|
97
|
+
npm run build # produce dist/ — the CLI serves it
|
|
98
|
+
npm install -g . # or: npm link — registers the `ccsm` command
|
|
99
|
+
ccsm
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
## What gets deleted
|
|
103
|
+
|
|
104
|
+
Claude Code stores session data across **five** locations under `~/.claude/`. Deleting a session here cleans up all of them in one shot:
|
|
105
|
+
|
|
106
|
+
| Path | Contents |
|
|
107
|
+
|---|---|
|
|
108
|
+
| `projects/<encoded-cwd>/<sessionId>.jsonl` | Main message stream |
|
|
109
|
+
| `projects/<encoded-cwd>/<sessionId>/` | Sub-agent threads + per-session memory |
|
|
110
|
+
| `file-history/<sessionId>/` | Snapshots of every file Claude edited in this session (often the largest contributor) |
|
|
111
|
+
| `session-env/<sessionId>/` | Environment snapshots |
|
|
112
|
+
| `history.jsonl` | Lines whose `sessionId` matches are stripped via atomic rewrite (backup → tmp → rename) |
|
|
113
|
+
| `sessions/<pid>.json` | Removed only when the PID has actually exited |
|
|
114
|
+
|
|
115
|
+
**Safety rails (a session is *skipped*, not deleted, when):**
|
|
116
|
+
- Its `sessionId` appears in a `sessions/<pid>.json` file whose PID is alive (`process.kill(pid, 0)` on Unix, `tasklist` on Windows).
|
|
117
|
+
- Its `.jsonl` was modified within the last 5 minutes (could still be in use after a `/clear` even if the PID file points elsewhere).
|
|
118
|
+
- The decoded path escapes `~/.claude/` (path-traversal guard).
|
|
119
|
+
- The session id contains `/`, `\`, `..`, or starts with `.`.
|
|
120
|
+
|
|
121
|
+
The UI's confirmation dialog shows the exact files and bytes that will be removed *and* lists which selections will be skipped and why.
|
|
122
|
+
|
|
123
|
+
## Cross-device sharing
|
|
124
|
+
|
|
125
|
+
Export/import move a project's **memory + conversation history** between machines, accounting for the fact that the same project lives at a different absolute path on each device.
|
|
126
|
+
|
|
127
|
+
**The path problem.** A project id is just the encoded `cwd`, and the real path is recorded *inside* the data — as `cwd` on every session `.jsonl` line and as `project` on every matching `history.jsonl` line. A bundle is therefore made **path-independent** at export: those two fields are replaced with a `${CLAUDE_PROJECT_ROOT}` sentinel. On import you choose the local target folder and the sentinel is substituted with that path. Message bodies, `gitBranch`, and `version` are **never** rewritten — they're an archival record.
|
|
128
|
+
|
|
129
|
+
**Bundle (a plain folder you can copy, or keep in a git repo / cloud drive):**
|
|
130
|
+
|
|
131
|
+
```
|
|
132
|
+
<bundle>/
|
|
133
|
+
manifest.json # schema, source platform + cwd, per-file sha256
|
|
134
|
+
memory/ # MEMORY.md + *.md, copied verbatim
|
|
135
|
+
sessions/<sid>/
|
|
136
|
+
conversation.jsonl # cwd -> ${CLAUDE_PROJECT_ROOT}
|
|
137
|
+
history.ndjson # project -> ${CLAUDE_PROJECT_ROOT}
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
Scope is the **core tier**: memory, conversations, and the matching `history.jsonl` lines (all path-portable). `file-history/`, `session-env/`, and the session subdir are intentionally excluded — they embed source-device absolute paths and aren't portable.
|
|
141
|
+
|
|
142
|
+
**Import is safe by construction.** It reuses the same guards as delete: paths are validated under `~/.claude/`, a session that is live or was modified in the last 5 minutes is never overwritten, files are written via tmp→rename, and the `history.jsonl` merge is an atomic backup→tmp→rename append with de-duplication (so re-importing the same bundle is a no-op). When a session id already exists you choose **skip** (default), **overwrite if newer**, or **keep both** (imports under a fresh id).
|
|
143
|
+
|
|
144
|
+
## Architecture
|
|
145
|
+
|
|
146
|
+
```
|
|
147
|
+
shared/ Wire types + constants imported by BOTH server and web.
|
|
148
|
+
server/ Hono + @hono/node-server backend, all filesystem operations.
|
|
149
|
+
lib/ claude-paths, encode-cwd, scan, parse-jsonl, load-session,
|
|
150
|
+
load-memory, rename-session, search-all, search-session,
|
|
151
|
+
active-sessions, delete, bundle, export-bundle, import-bundle,
|
|
152
|
+
disk-usage, fs-size, safe-id, system-tags, port, …
|
|
153
|
+
routes/ projects, sessions, disk, search, import
|
|
154
|
+
web/ React 19 + Vite + Tailwind v4 SPA.
|
|
155
|
+
src/routes/ ProjectsList, ProjectDetail, SessionDetail,
|
|
156
|
+
ProjectMemory, DiskUsage (lazy), ImportPage (lazy)
|
|
157
|
+
src/components Sidebar, SearchModal, PageHeader, MessageBubble,
|
|
158
|
+
ToolBlock, DeleteDialog, ExportDialog, HighlightedText, …
|
|
159
|
+
src/lib api, query-keys, i18n, theme, hotkeys, format
|
|
160
|
+
docs/spec/ Design notes (start here before refactoring).
|
|
161
|
+
docs/acceptance/ Per-feature e2e plans, round evidence, retrospectives.
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
### HTTP API
|
|
165
|
+
|
|
166
|
+
| Method | Path | Purpose |
|
|
167
|
+
|---|---|---|
|
|
168
|
+
| `GET` | `/api/health` | `claudeRoot`, platform, pid — used by the UI's empty-state banner. |
|
|
169
|
+
| `GET` | `/api/projects` | Project summaries (one per `cwd`). |
|
|
170
|
+
| `GET` | `/api/projects/:id/sessions` | Session list for a project. |
|
|
171
|
+
| `GET` | `/api/projects/:id/memory` | Memory file index + content for the two-pane reader. |
|
|
172
|
+
| `GET` | `/api/sessions/:projectId/:sessionId` | Full parsed message timeline. |
|
|
173
|
+
| `PATCH` | `/api/sessions/:projectId/:sessionId` | Rename a session by appending a `custom-title` record to the `.jsonl`. |
|
|
174
|
+
| `DELETE` | `/api/sessions` | Cascade-delete one or more sessions; CSRF-checked via `Origin`. |
|
|
175
|
+
| `POST` | `/api/projects/:id/export` | Write a path-independent bundle (memory + conversations) to a folder outside `~/.claude/`; CSRF-checked. |
|
|
176
|
+
| `POST` | `/api/import/preview` | Dry run: remap plan, per-session/memory actions, and history-line count — writes nothing; CSRF-checked. |
|
|
177
|
+
| `POST` | `/api/import` | Commit an import into `~/.claude/` (atomic, never overwrites a live/recent session); CSRF-checked. |
|
|
178
|
+
| `GET` | `/api/disk-usage` | Per-project totals + monthly buckets + top-N sessions. |
|
|
179
|
+
| `GET` | `/api/search?q=...` | NDJSON stream of matches across every project. |
|
|
180
|
+
|
|
181
|
+
| Layer | Tech |
|
|
182
|
+
|---|---|
|
|
183
|
+
| Backend runtime | Node 22+ + `tsx` (TS direct execution) |
|
|
184
|
+
| HTTP | Hono + `@hono/node-server` |
|
|
185
|
+
| Frontend | React 19 + Vite 6 + Tailwind v4 + TanStack Query + React Router 7 |
|
|
186
|
+
| Charts | Recharts (lazy-loaded only on `/disk`) |
|
|
187
|
+
|
|
188
|
+
The DiskUsage page is code-split, so the initial bundle is **~124 KB gzipped**. Recharts (~80 KB gzipped) loads on demand when you navigate to `/disk`.
|
|
189
|
+
|
|
190
|
+
## Cross-platform
|
|
191
|
+
|
|
192
|
+
| OS | `~/.claude/` resolves to | `projects/` subdir naming |
|
|
193
|
+
|---|---|---|
|
|
194
|
+
| macOS / Linux | `$HOME/.claude/` | `/foo/bar` → `-foo-bar` |
|
|
195
|
+
| Windows | `C:\Users\<you>\.claude\` | `C:\foo\bar` → `C--foo-bar` |
|
|
196
|
+
|
|
197
|
+
Decoding a project id back to a real path uses each session's own `cwd` field (recorded inside the `.jsonl`) when available; otherwise it falls back to a heuristic decode and stat-checks the result. Projects whose decoded path no longer exists on disk are flagged in the UI.
|
|
198
|
+
|
|
199
|
+
## Troubleshooting
|
|
200
|
+
|
|
201
|
+
**Port 3131 busy.** The server auto-picks the next free port up to 3140 and prints it on startup. Look at the `[server] listening on http://127.0.0.1:<port>` line.
|
|
202
|
+
|
|
203
|
+
**`Claude root .../.claude doesn't exist`.** The Projects page shows an amber banner with the resolved path. Either Claude Code hasn't been run on this machine, or `$HOME` resolves somewhere unexpected. Check `node -e 'console.log(require("os").homedir())'`.
|
|
204
|
+
|
|
205
|
+
**Delete dialog says my current session is skipped.** Expected — your live Claude Code process has its `sessionId` registered in `~/.claude/sessions/<pid>.json` *or* the `.jsonl` was just modified. Wait 5+ minutes after closing Claude, then try again.
|
|
206
|
+
|
|
207
|
+
**`history.jsonl.bak-<timestamp>` left behind.** Means the swap rename failed mid-flight. The original is intact (the backup *is* the original). Compare both files and either `mv backup history.jsonl` to restore or `rm` the backup if the new one looks fine.
|
|
208
|
+
|
|
209
|
+
**Windows: delete fails with `EBUSY`.** Some Windows AV / indexer can hold a lock on `.jsonl` files. Close any text editors that have the file open and retry.
|
|
210
|
+
|
|
211
|
+
**Browser can't reach the server.** The bind is `127.0.0.1`, not `0.0.0.0`. Make sure you're using `http://localhost:3131` from the same machine — not a LAN IP.
|
|
212
|
+
|
|
213
|
+
## Security model
|
|
214
|
+
|
|
215
|
+
This tool is intended for a single user on their own machine. It is *not* hardened for multi-user / shared environments.
|
|
216
|
+
|
|
217
|
+
- The HTTP listener binds to `127.0.0.1` only by default — never reachable from the LAN. The CLI's `--host` flag can override this (e.g. `--host 0.0.0.0`); doing so exposes the UI on your network with **no authentication**, so anyone who can reach the host:port can read and delete your Claude Code history. The server prints a warning on startup whenever it binds to a non-loopback host. Only do this on a network you trust.
|
|
218
|
+
- Mutating endpoints (`DELETE /api/sessions`, `PATCH /api/sessions/:projectId/:sessionId`, `POST /api/projects/:id/export`, `POST /api/import` and its `/preview`) require an `Origin` header matching `http(s)://(localhost|127.0.0.1):*`. This blocks other web pages your browser opens from triggering writes via CSRF, but cannot stop another local process running as the same user.
|
|
219
|
+
- *Export* refuses to write inside `~/.claude/`; *import* validates every destination under `~/.claude/`, skips any live/recently-active session, and writes atomically — the same safety rails as delete.
|
|
220
|
+
- All filesystem paths are validated with `path.resolve(...).startsWith(claudeRoot)` (Windows-aware case-folded) before any read or write.
|
|
221
|
+
- IDs from URL params are rejected if they contain `/`, `\`, `..`, or start with `.`.
|
|
222
|
+
- The cross-session search endpoint streams NDJSON and aborts when the client disconnects, so a closed browser tab stops the scan immediately.
|
|
223
|
+
|
|
224
|
+
There is no authentication. If you're paranoid, run behind a firewall rule or only invoke the tool when needed.
|
|
225
|
+
|
|
226
|
+
## Project layout
|
|
227
|
+
|
|
228
|
+
See [`docs/spec/session-manager-design.md`](docs/spec/session-manager-design.md) for the full design rationale (data model, routing decisions, cross-platform strategy, future work).
|
|
229
|
+
|
|
230
|
+
## License
|
|
231
|
+
|
|
232
|
+
MIT — see [LICENSE](LICENSE).
|
package/bin/cli.mjs
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// ccsm — Claude Code Session Manager CLI launcher.
|
|
3
|
+
//
|
|
4
|
+
// Plain JS so it runs under a bare `node` (global install / npx) with no build
|
|
5
|
+
// step. --help/--version are answered here without loading the server. For the
|
|
6
|
+
// real thing we register tsx's ESM loader (resolved from THIS package, so it
|
|
7
|
+
// works regardless of the user's cwd) and import the TypeScript server entry —
|
|
8
|
+
// the same path `npm run start` (`tsx server/index.ts`) takes.
|
|
9
|
+
import { readFileSync } from 'node:fs';
|
|
10
|
+
import { dirname, join } from 'node:path';
|
|
11
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
12
|
+
|
|
13
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
14
|
+
const pkgRoot = join(__dirname, '..');
|
|
15
|
+
const args = process.argv.slice(2);
|
|
16
|
+
|
|
17
|
+
if (args.includes('-v') || args.includes('--version')) {
|
|
18
|
+
const { version } = JSON.parse(readFileSync(join(pkgRoot, 'package.json'), 'utf8'));
|
|
19
|
+
console.log(version);
|
|
20
|
+
process.exit(0);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
if (args.includes('-h') || args.includes('--help')) {
|
|
24
|
+
printHelp();
|
|
25
|
+
process.exit(0);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const { register } = await import('tsx/esm/api');
|
|
29
|
+
register();
|
|
30
|
+
await import(pathToFileURL(join(pkgRoot, 'server', 'index.ts')).href);
|
|
31
|
+
|
|
32
|
+
function printHelp() {
|
|
33
|
+
console.log(`ccsm — Claude Code Session Manager
|
|
34
|
+
|
|
35
|
+
A local web UI to browse and clean up your Claude Code session history (~/.claude/).
|
|
36
|
+
|
|
37
|
+
Usage:
|
|
38
|
+
ccsm [options]
|
|
39
|
+
|
|
40
|
+
Options:
|
|
41
|
+
-p, --port <number> Port to listen on. Default: first free port in 3131-3140.
|
|
42
|
+
If the given port is busy, ccsm exits (it won't pick another).
|
|
43
|
+
--host <host> Host to bind. Default: 127.0.0.1 (loopback only).
|
|
44
|
+
Pass 0.0.0.0 to expose the UI on your LAN. There is NO
|
|
45
|
+
authentication, so only do this on a network you trust.
|
|
46
|
+
-o, --open Open the UI in your default browser once it's listening.
|
|
47
|
+
-h, --help Show this help and exit.
|
|
48
|
+
-v, --version Print the version and exit.
|
|
49
|
+
|
|
50
|
+
The server binds to 127.0.0.1 by default and is unreachable from the network.
|
|
51
|
+
Once it's up, open http://127.0.0.1:<port> in your browser.`);
|
|
52
|
+
}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import{j as e,r as c}from"./react-W0jzChlo.js";import{b as E}from"./query-C1K1uQRu.js";import{u as b,L as K,s as _,f as g,a as n,b as q,M as j,S as B,c as z,q as D}from"./index-DLATR3tZ.js";import{G as h}from"./vendor-CH80ylbS.js";import{R as M,P as U,a as V,C as O,T as A,A as G,X as W,Y as X,b as Y}from"./charts-A5eNHLjX.js";import{L}from"./router-DfbutHY3.js";function y({label:s,value:t,unit:o,trail:r,accent:l}){return e.jsxs("div",{className:"surface-card is-interactive group relative overflow-hidden p-5 "+(l?"border-[var(--color-accent)]/40 hover:border-[var(--color-accent)]/60":""),children:[l&&e.jsx("span",{"aria-hidden":!0,className:"pointer-events-none absolute -right-12 -top-12 h-32 w-32 rounded-full bg-[var(--color-accent-soft)] opacity-70 blur-2xl"}),e.jsx("div",{className:"eyebrow",children:s}),e.jsxs("div",{className:"mt-3 flex items-baseline gap-1.5",children:[e.jsx("span",{className:"font-mono text-4xl font-light leading-none tracking-[-0.02em] tabular-nums text-[var(--color-fg-primary)]",children:t}),o&&e.jsx("span",{className:"font-mono text-[11px] uppercase tracking-[0.16em] text-[var(--color-fg-muted)]",children:o})]}),r&&e.jsx("div",{className:"mt-3 font-mono text-[11px] text-[var(--color-fg-muted)]",children:r})]})}const P=["--color-accent","--color-moss","--color-iris","--color-fg-secondary","--color-accent-ink","--color-fg-muted"],H=["--color-accent","--color-fg-muted","--color-hairline","--color-surface","--color-fg-primary"];function R(s){const[t,o]=c.useState(()=>F(s));return c.useEffect(()=>{const r=new MutationObserver(()=>o(F(s)));return r.observe(document.documentElement,{attributes:!0,attributeFilter:["class"]}),()=>r.disconnect()},[s]),t}function F(s){const t=getComputedStyle(document.documentElement),o={};for(const r of s)o[r]=t.getPropertyValue(r).trim()||"#888";return o}function re(){var C;const s=b(),{data:t,isLoading:o,error:r}=E({queryKey:D.diskUsage(),queryFn:()=>z("/api/disk-usage")}),l=R(H),u=l["--color-accent"],N=l["--color-fg-muted"],d=l["--color-hairline"],f=l["--color-surface"],k=l["--color-fg-primary"],w=R(P),m=c.useMemo(()=>P.map(a=>w[a]),[w]),x=c.useMemo(()=>t?t.byProject.map(a=>({name:T(a.decodedCwd),value:a.totalBytes,sessions:a.sessionCount})):[],[t]),S=c.useMemo(()=>t?t.byMonth.map(a=>({month:a.month,MB:+(a.totalBytes/1048576).toFixed(2)})):[],[t]);return e.jsxs("section",{children:[e.jsx("div",{className:"surface-card p-6",children:e.jsx(Q,{title:s("disk.title"),tagline:s("disk.tagline"),stats:t?{totalBytes:t.totalBytes,projectCount:t.byProject.length,totalSessions:t.totalSessions}:null})}),o&&e.jsx(K,{label:s("common.computing"),className:"mt-10"}),r&&e.jsxs("p",{className:"mt-10 rounded-md border border-[var(--color-danger)]/40 bg-[var(--color-danger-soft)] px-4 py-3 text-sm text-[var(--color-danger)]",children:[s("common.failed"),": ",r.message]}),t&&e.jsxs(e.Fragment,{children:[e.jsxs(h.div,{initial:"hidden",animate:"show",variants:_,className:"mt-8 grid gap-3 sm:grid-cols-3",children:[e.jsx(h.div,{variants:g,children:e.jsx(y,{accent:!0,label:s("disk.stat.total"),value:n(t.totalBytes).split(" ")[0],unit:n(t.totalBytes).split(" ")[1],trail:s("disk.stat.acrossProjects",{n:t.byProject.length})})}),e.jsx(h.div,{variants:g,children:e.jsx(y,{label:s("disk.stat.sessions"),value:t.totalSessions.toLocaleString(),trail:t.topSessions[0]?s("disk.stat.largest",{size:n(t.topSessions[0].totalBytes)}):void 0})}),e.jsx(h.div,{variants:g,children:e.jsx(y,{label:s("disk.stat.months"),value:t.byMonth.length,trail:t.byMonth[0]?`${t.byMonth[0].month} → ${((C=t.byMonth.at(-1))==null?void 0:C.month)??t.byMonth[0].month}`:void 0})})]}),e.jsxs("div",{className:"mt-12 grid gap-8 lg:grid-cols-5",children:[e.jsx($,{title:s("disk.composition.title"),subtitle:s("disk.composition.subtitle"),className:"lg:col-span-3",children:x.length===0?e.jsx(v,{}):e.jsxs("div",{className:"grid gap-6 md:grid-cols-[minmax(0,1fr)_minmax(0,1.1fr)] md:items-center",children:[e.jsxs("div",{className:"relative mx-auto aspect-square w-full max-w-[260px]",children:[e.jsx(M,{width:"100%",height:"100%",children:e.jsxs(U,{margin:{top:0,right:0,bottom:0,left:0},children:[e.jsx(V,{data:x,dataKey:"value",nameKey:"name",cx:"50%",cy:"50%",innerRadius:"58%",outerRadius:"92%",paddingAngle:1.5,stroke:f,strokeWidth:2,isAnimationActive:!1,children:x.map((a,i)=>e.jsx(O,{fill:m[i%m.length]},i))}),e.jsx(A,{contentStyle:I(f,d,k),formatter:a=>n(a)})]})}),e.jsxs("div",{className:"pointer-events-none absolute inset-0 flex flex-col items-center justify-center text-center",children:[e.jsx("span",{className:"eyebrow",children:s("disk.composition.total")}),e.jsx("span",{className:"mt-1 font-mono text-2xl font-light tabular-nums text-[var(--color-fg-primary)]",children:n(t.totalBytes)})]})]}),e.jsx("ol",{className:"space-y-1.5 text-sm",children:x.slice(0,8).map((a,i)=>{const p=(a.value/t.totalBytes*100).toFixed(1);return e.jsxs("li",{className:"grid grid-cols-[14px_1fr_auto] items-baseline gap-2",children:[e.jsx("span",{"aria-hidden":!0,className:"block h-2.5 w-2.5 self-center rounded-sm",style:{background:m[i%m.length]}}),e.jsx("span",{className:"truncate font-mono text-xs text-[var(--color-fg-secondary)]",title:a.name,children:a.name}),e.jsxs("span",{className:"font-mono tabular-nums text-xs text-[var(--color-fg-primary)]",children:[p,"% ",e.jsxs("span",{className:"text-[var(--color-fg-faint)]",children:["· ",n(a.value)]})]})]},i)})})]})}),e.jsx($,{title:s("disk.cadence.title"),subtitle:s("disk.cadence.subtitle"),className:"lg:col-span-2",children:S.length===0?e.jsx(v,{}):e.jsx("div",{className:"h-72",children:e.jsx(M,{width:"100%",height:"100%",children:e.jsxs(G,{data:S,margin:{top:10,right:8,bottom:0,left:-10},children:[e.jsx("defs",{children:e.jsxs("linearGradient",{id:"cadenceFill",x1:"0",y1:"0",x2:"0",y2:"1",children:[e.jsx("stop",{offset:"0%",stopColor:u,stopOpacity:.55}),e.jsx("stop",{offset:"100%",stopColor:u,stopOpacity:0})]})}),e.jsx(W,{dataKey:"month",tick:{fontSize:11,fill:N,fontFamily:"var(--font-mono)"},tickLine:!1,axisLine:{stroke:d}}),e.jsx(X,{tick:{fontSize:11,fill:N,fontFamily:"var(--font-mono)"},tickLine:!1,axisLine:!1,width:36}),e.jsx(A,{contentStyle:I(f,d,k),formatter:a=>`${a.toFixed(2)} MB`,cursor:{stroke:d,strokeDasharray:"2 3"}}),e.jsx(Y,{type:"monotone",dataKey:"MB",stroke:u,strokeWidth:1.6,fill:"url(#cadenceFill)"})]})})})})]}),e.jsxs("div",{className:"surface-card mt-12 p-6",children:[e.jsxs("div",{className:"flex items-baseline justify-between",children:[e.jsx("h2",{className:"font-display text-xl font-light tracking-tight text-[var(--color-fg-primary)]",children:s("disk.heaviest.title")}),t.topSessions.length>0&&e.jsx("span",{className:"font-mono text-[11px] uppercase tracking-[0.16em] text-[var(--color-fg-muted)]",children:s("disk.heaviest.top",{n:t.topSessions.length})})]}),e.jsx("div",{className:"rule-dotted mt-3","aria-hidden":!0}),t.topSessions.length===0?e.jsx(v,{className:"mt-6"}):e.jsx("div",{className:"mt-4 -mx-6 overflow-x-auto px-6",children:e.jsxs("table",{className:"w-full table-fixed text-sm",children:[e.jsxs("colgroup",{children:[e.jsx("col",{className:"w-10"}),e.jsx("col",{}),e.jsx("col",{className:"w-[22rem]"}),e.jsx("col",{className:"w-24"}),e.jsx("col",{className:"w-24"})]}),e.jsx("thead",{children:e.jsxs("tr",{className:"text-left",children:[e.jsx("th",{className:"px-2 py-3 eyebrow",children:s("disk.col.num")}),e.jsx("th",{className:"px-2 py-3 eyebrow",children:s("disk.col.title")}),e.jsx("th",{className:"px-2 py-3 eyebrow",children:s("disk.col.project")}),e.jsx("th",{className:"px-2 py-3 eyebrow text-right",children:s("disk.col.last")}),e.jsx("th",{className:"px-2 py-3 eyebrow text-right",children:s("disk.col.size")})]})}),e.jsx("tbody",{className:"border-t border-[var(--color-hairline)]",children:t.topSessions.map((a,i)=>{const p=a.customTitle??a.title;return e.jsxs("tr",{className:"ribbon-row border-b border-[var(--color-hairline)] hover:bg-[var(--color-sunken)]",children:[e.jsx("td",{className:"px-2 py-3 align-top font-mono text-[11px] text-[var(--color-fg-faint)]",children:String(i+1).padStart(2,"0")}),e.jsx("td",{className:"px-2 py-3 align-top",children:e.jsx(L,{to:`/projects/${encodeURIComponent(a.projectId)}/sessions/${a.sessionId}`,className:"block truncate font-medium text-[var(--color-fg-primary)] hover:text-[var(--color-accent-ink)] dark:hover:text-[var(--color-accent)]",title:p,children:p})}),e.jsx("td",{className:"px-2 py-3 align-top font-mono text-[12px] text-[var(--color-fg-muted)]",children:e.jsx(L,{to:`/projects/${encodeURIComponent(a.projectId)}`,className:"block truncate hover:text-[var(--color-fg-primary)]",title:a.projectId,children:J(t,a.projectId)})}),e.jsx("td",{className:"px-2 py-3 text-right align-top font-mono text-[12.5px] text-[var(--color-fg-secondary)]",children:q(a.lastAt)}),e.jsx("td",{className:"px-2 py-3 text-right align-top font-mono tabular-nums text-[var(--color-fg-primary)]",children:n(a.totalBytes)})]},`${a.projectId}/${a.sessionId}`)})})]})})]})]})]})}function Q({title:s,tagline:t,stats:o}){const r=b();return e.jsxs("header",{className:"relative",children:[e.jsxs("div",{className:"flex flex-wrap items-baseline gap-x-4 gap-y-1",children:[e.jsxs("h1",{className:"font-display text-[clamp(1.75rem,3.5vw,2.25rem)] font-light leading-[1.1] tracking-[-0.02em] text-[var(--color-fg-primary)]",children:[s,e.jsx("span",{className:"text-[var(--color-accent)]",children:"."})]}),e.jsx("p",{className:"min-w-0 flex-1 font-display text-[13px] italic leading-snug text-[var(--color-fg-muted)]",children:t})]}),o&&e.jsxs("div",{className:"mt-3 flex flex-wrap items-baseline gap-x-3 gap-y-1 text-xs",children:[e.jsx(j,{label:r("disk.meta.total"),value:n(o.totalBytes)}),e.jsx(B,{}),e.jsx(j,{label:r("disk.meta.projects"),value:o.projectCount}),e.jsx(B,{}),e.jsx(j,{label:r("disk.meta.sessions"),value:o.totalSessions.toLocaleString()})]})]})}function I(s,t,o){return{background:s,border:`1px solid ${t}`,borderRadius:8,fontFamily:"var(--font-mono)",fontSize:11,color:o,boxShadow:"var(--shadow-pop)"}}function J(s,t){const o=s.byProject.find(r=>r.projectId===t);return o?T(o.decodedCwd):t}function T(s){const t=s.split(/[\\/]+/).filter(Boolean);return t.length<=2?s:"…/"+t.slice(-2).join("/")}function $({title:s,subtitle:t,children:o,className:r=""}){return e.jsxs("section",{className:`surface-card p-5 ${r}`,children:[e.jsxs("header",{className:"mb-4 flex items-baseline justify-between gap-3",children:[e.jsx("h3",{className:"font-display text-lg font-light tracking-tight text-[var(--color-fg-primary)]",children:s}),t&&e.jsx("span",{className:"font-mono text-[10px] uppercase tracking-[0.18em] text-[var(--color-fg-muted)]",children:t})]}),o]})}function v({className:s=""}){const t=b();return e.jsx("p",{className:`font-mono text-xs uppercase tracking-[0.18em] text-[var(--color-fg-muted)] ${s}`,children:t("common.noData")})}export{re as default};
|
|
2
|
+
//# sourceMappingURL=DiskUsage-Bq4VaoUA.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"DiskUsage-Bq4VaoUA.js","sources":["../../web/src/components/StatCard.tsx","../../web/src/routes/DiskUsage.tsx"],"sourcesContent":["import type { ReactNode } from 'react';\r\n\r\ninterface Props {\r\n label: string;\r\n value: ReactNode;\r\n unit?: ReactNode;\r\n trail?: ReactNode;\r\n accent?: boolean;\r\n}\r\n\r\nexport default function StatCard({ label, value, unit, trail, accent }: Props) {\r\n return (\r\n <div\r\n className={\r\n 'surface-card is-interactive group relative overflow-hidden p-5 ' +\r\n (accent ? 'border-[var(--color-accent)]/40 hover:border-[var(--color-accent)]/60' : '')\r\n }\r\n >\r\n {accent && (\r\n <span\r\n aria-hidden\r\n className=\"pointer-events-none absolute -right-12 -top-12 h-32 w-32 rounded-full bg-[var(--color-accent-soft)] opacity-70 blur-2xl\"\r\n />\r\n )}\r\n <div className=\"eyebrow\">{label}</div>\r\n <div className=\"mt-3 flex items-baseline gap-1.5\">\r\n <span className=\"font-mono text-4xl font-light leading-none tracking-[-0.02em] tabular-nums text-[var(--color-fg-primary)]\">\r\n {value}\r\n </span>\r\n {unit && (\r\n <span className=\"font-mono text-[11px] uppercase tracking-[0.16em] text-[var(--color-fg-muted)]\">\r\n {unit}\r\n </span>\r\n )}\r\n </div>\r\n {trail && (\r\n <div className=\"mt-3 font-mono text-[11px] text-[var(--color-fg-muted)]\">{trail}</div>\r\n )}\r\n </div>\r\n );\r\n}\r\n","import { useQuery } from '@tanstack/react-query';\r\nimport { motion } from 'motion/react';\r\nimport { useEffect, useMemo, useState } from 'react';\r\nimport { Link } from 'react-router-dom';\r\nimport {\r\n Area,\r\n AreaChart,\r\n Cell,\r\n Pie,\r\n PieChart,\r\n ResponsiveContainer,\r\n Tooltip,\r\n XAxis,\r\n YAxis,\r\n} from 'recharts';\r\nimport { Loading } from '../components/Loading.tsx';\r\nimport { MetaItem, Sep } from '../components/PageHeader.tsx';\r\nimport StatCard from '../components/StatCard.tsx';\r\nimport { api, type DiskUsage } from '../lib/api.ts';\r\nimport { formatBytes, formatRelativeTime } from '../lib/format.ts';\r\nimport { useT } from '../lib/i18n.ts';\r\nimport { fadeUpItem, staggerParent } from '../lib/motion.ts';\r\nimport { queryKeys } from '../lib/query-keys.ts';\r\n\r\nconst PALETTE_VARS = [\r\n '--color-accent',\r\n '--color-moss',\r\n '--color-iris',\r\n '--color-fg-secondary',\r\n '--color-accent-ink',\r\n '--color-fg-muted',\r\n];\r\n\r\nconst CHART_VARS = [\r\n '--color-accent',\r\n '--color-fg-muted',\r\n '--color-hairline',\r\n '--color-surface',\r\n '--color-fg-primary',\r\n] as const;\r\n\r\n// Resolve a list of CSS variables on <html> and re-resolve whenever the\r\n// theme class flips. Recharts needs concrete colors; CSS vars don't traverse SVG cleanly.\r\nfunction useThemeColors<T extends readonly string[]>(vars: T): Record<T[number], string> {\r\n const [snapshot, setSnapshot] = useState(() => readVars(vars));\r\n useEffect(() => {\r\n const observer = new MutationObserver(() => setSnapshot(readVars(vars)));\r\n observer.observe(document.documentElement, { attributes: true, attributeFilter: ['class'] });\r\n return () => observer.disconnect();\r\n }, [vars]);\r\n return snapshot;\r\n}\r\n\r\nfunction readVars<T extends readonly string[]>(vars: T): Record<T[number], string> {\r\n const cs = getComputedStyle(document.documentElement);\r\n const out = {} as Record<T[number], string>;\r\n for (const v of vars) (out as Record<string, string>)[v] = cs.getPropertyValue(v).trim() || '#888';\r\n return out;\r\n}\r\n\r\nexport default function DiskUsageRoute() {\r\n const t = useT();\r\n const { data, isLoading, error } = useQuery({\r\n queryKey: queryKeys.diskUsage(),\r\n queryFn: () => api<DiskUsage>('/api/disk-usage'),\r\n });\r\n\r\n const colors = useThemeColors(CHART_VARS);\r\n const accent = colors['--color-accent'];\r\n const muted = colors['--color-fg-muted'];\r\n const hairline = colors['--color-hairline'];\r\n const surface = colors['--color-surface'];\r\n const fgPrimary = colors['--color-fg-primary'];\r\n\r\n const palette = useThemeColors(PALETTE_VARS);\r\n const resolvedPalette = useMemo(() => PALETTE_VARS.map((v) => palette[v]), [palette]);\r\n\r\n const pieData = useMemo(() => {\r\n if (!data) return [];\r\n return data.byProject.map((p) => ({\r\n name: shortCwd(p.decodedCwd),\r\n value: p.totalBytes,\r\n sessions: p.sessionCount,\r\n }));\r\n }, [data]);\r\n\r\n const monthData = useMemo(() => {\r\n if (!data) return [];\r\n return data.byMonth.map((m) => ({ month: m.month, MB: +(m.totalBytes / 1_048_576).toFixed(2) }));\r\n }, [data]);\r\n\r\n return (\r\n <section>\r\n <div className=\"surface-card p-6\">\r\n <Masthead\r\n title={t('disk.title')}\r\n tagline={t('disk.tagline')}\r\n stats={\r\n data\r\n ? {\r\n totalBytes: data.totalBytes,\r\n projectCount: data.byProject.length,\r\n totalSessions: data.totalSessions,\r\n }\r\n : null\r\n }\r\n />\r\n </div>\r\n\r\n {isLoading && <Loading label={t('common.computing')} className=\"mt-10\" />}\r\n {error && (\r\n <p className=\"mt-10 rounded-md border border-[var(--color-danger)]/40 bg-[var(--color-danger-soft)] px-4 py-3 text-sm text-[var(--color-danger)]\">\r\n {t('common.failed')}: {(error as Error).message}\r\n </p>\r\n )}\r\n\r\n {data && (\r\n <>\r\n <motion.div\r\n initial=\"hidden\"\r\n animate=\"show\"\r\n variants={staggerParent}\r\n className=\"mt-8 grid gap-3 sm:grid-cols-3\"\r\n >\r\n <motion.div variants={fadeUpItem}>\r\n <StatCard\r\n accent\r\n label={t('disk.stat.total')}\r\n value={formatBytes(data.totalBytes).split(' ')[0]}\r\n unit={formatBytes(data.totalBytes).split(' ')[1]}\r\n trail={t('disk.stat.acrossProjects', { n: data.byProject.length })}\r\n />\r\n </motion.div>\r\n <motion.div variants={fadeUpItem}>\r\n <StatCard\r\n label={t('disk.stat.sessions')}\r\n value={data.totalSessions.toLocaleString()}\r\n trail={\r\n data.topSessions[0]\r\n ? t('disk.stat.largest', { size: formatBytes(data.topSessions[0].totalBytes) })\r\n : undefined\r\n }\r\n />\r\n </motion.div>\r\n <motion.div variants={fadeUpItem}>\r\n <StatCard\r\n label={t('disk.stat.months')}\r\n value={data.byMonth.length}\r\n trail={\r\n data.byMonth[0]\r\n ? `${data.byMonth[0].month} → ${data.byMonth.at(-1)?.month ?? data.byMonth[0].month}`\r\n : undefined\r\n }\r\n />\r\n </motion.div>\r\n </motion.div>\r\n\r\n <div className=\"mt-12 grid gap-8 lg:grid-cols-5\">\r\n <Card\r\n title={t('disk.composition.title')}\r\n subtitle={t('disk.composition.subtitle')}\r\n className=\"lg:col-span-3\"\r\n >\r\n {pieData.length === 0 ? (\r\n <Empty />\r\n ) : (\r\n <div className=\"grid gap-6 md:grid-cols-[minmax(0,1fr)_minmax(0,1.1fr)] md:items-center\">\r\n <div className=\"relative mx-auto aspect-square w-full max-w-[260px]\">\r\n <ResponsiveContainer width=\"100%\" height=\"100%\">\r\n <PieChart margin={{ top: 0, right: 0, bottom: 0, left: 0 }}>\r\n <Pie\r\n data={pieData}\r\n dataKey=\"value\"\r\n nameKey=\"name\"\r\n cx=\"50%\"\r\n cy=\"50%\"\r\n innerRadius=\"58%\"\r\n outerRadius=\"92%\"\r\n paddingAngle={1.5}\r\n stroke={surface}\r\n strokeWidth={2}\r\n isAnimationActive={false}\r\n >\r\n {pieData.map((_, i) => (\r\n <Cell key={i} fill={resolvedPalette[i % resolvedPalette.length]} />\r\n ))}\r\n </Pie>\r\n <Tooltip\r\n contentStyle={tooltipStyle(surface, hairline, fgPrimary)}\r\n formatter={(value: number) => formatBytes(value)}\r\n />\r\n </PieChart>\r\n </ResponsiveContainer>\r\n <div className=\"pointer-events-none absolute inset-0 flex flex-col items-center justify-center text-center\">\r\n <span className=\"eyebrow\">{t('disk.composition.total')}</span>\r\n <span className=\"mt-1 font-mono text-2xl font-light tabular-nums text-[var(--color-fg-primary)]\">\r\n {formatBytes(data.totalBytes)}\r\n </span>\r\n </div>\r\n </div>\r\n <ol className=\"space-y-1.5 text-sm\">\r\n {pieData.slice(0, 8).map((p, i) => {\r\n const pct = ((p.value / data.totalBytes) * 100).toFixed(1);\r\n return (\r\n <li key={i} className=\"grid grid-cols-[14px_1fr_auto] items-baseline gap-2\">\r\n <span\r\n aria-hidden\r\n className=\"block h-2.5 w-2.5 self-center rounded-sm\"\r\n style={{ background: resolvedPalette[i % resolvedPalette.length] }}\r\n />\r\n <span className=\"truncate font-mono text-xs text-[var(--color-fg-secondary)]\" title={p.name}>\r\n {p.name}\r\n </span>\r\n <span className=\"font-mono tabular-nums text-xs text-[var(--color-fg-primary)]\">\r\n {pct}% <span className=\"text-[var(--color-fg-faint)]\">· {formatBytes(p.value)}</span>\r\n </span>\r\n </li>\r\n );\r\n })}\r\n </ol>\r\n </div>\r\n )}\r\n </Card>\r\n\r\n <Card\r\n title={t('disk.cadence.title')}\r\n subtitle={t('disk.cadence.subtitle')}\r\n className=\"lg:col-span-2\"\r\n >\r\n {monthData.length === 0 ? (\r\n <Empty />\r\n ) : (\r\n <div className=\"h-72\">\r\n <ResponsiveContainer width=\"100%\" height=\"100%\">\r\n <AreaChart data={monthData} margin={{ top: 10, right: 8, bottom: 0, left: -10 }}>\r\n <defs>\r\n <linearGradient id=\"cadenceFill\" x1=\"0\" y1=\"0\" x2=\"0\" y2=\"1\">\r\n <stop offset=\"0%\" stopColor={accent} stopOpacity={0.55} />\r\n <stop offset=\"100%\" stopColor={accent} stopOpacity={0} />\r\n </linearGradient>\r\n </defs>\r\n <XAxis\r\n dataKey=\"month\"\r\n tick={{ fontSize: 11, fill: muted, fontFamily: 'var(--font-mono)' }}\r\n tickLine={false}\r\n axisLine={{ stroke: hairline }}\r\n />\r\n <YAxis\r\n tick={{ fontSize: 11, fill: muted, fontFamily: 'var(--font-mono)' }}\r\n tickLine={false}\r\n axisLine={false}\r\n width={36}\r\n />\r\n <Tooltip\r\n contentStyle={tooltipStyle(surface, hairline, fgPrimary)}\r\n formatter={(value: number) => `${value.toFixed(2)} MB`}\r\n cursor={{ stroke: hairline, strokeDasharray: '2 3' }}\r\n />\r\n <Area\r\n type=\"monotone\"\r\n dataKey=\"MB\"\r\n stroke={accent}\r\n strokeWidth={1.6}\r\n fill=\"url(#cadenceFill)\"\r\n />\r\n </AreaChart>\r\n </ResponsiveContainer>\r\n </div>\r\n )}\r\n </Card>\r\n </div>\r\n\r\n <div className=\"surface-card mt-12 p-6\">\r\n <div className=\"flex items-baseline justify-between\">\r\n <h2 className=\"font-display text-xl font-light tracking-tight text-[var(--color-fg-primary)]\">\r\n {t('disk.heaviest.title')}\r\n </h2>\r\n {data.topSessions.length > 0 && (\r\n <span className=\"font-mono text-[11px] uppercase tracking-[0.16em] text-[var(--color-fg-muted)]\">\r\n {t('disk.heaviest.top', { n: data.topSessions.length })}\r\n </span>\r\n )}\r\n </div>\r\n <div className=\"rule-dotted mt-3\" aria-hidden />\r\n {data.topSessions.length === 0 ? (\r\n <Empty className=\"mt-6\" />\r\n ) : (\r\n <div className=\"mt-4 -mx-6 overflow-x-auto px-6\">\r\n <table className=\"w-full table-fixed text-sm\">\r\n <colgroup>\r\n <col className=\"w-10\" />\r\n <col />\r\n <col className=\"w-[22rem]\" />\r\n <col className=\"w-24\" />\r\n <col className=\"w-24\" />\r\n </colgroup>\r\n <thead>\r\n <tr className=\"text-left\">\r\n <th className=\"px-2 py-3 eyebrow\">{t('disk.col.num')}</th>\r\n <th className=\"px-2 py-3 eyebrow\">{t('disk.col.title')}</th>\r\n <th className=\"px-2 py-3 eyebrow\">{t('disk.col.project')}</th>\r\n <th className=\"px-2 py-3 eyebrow text-right\">{t('disk.col.last')}</th>\r\n <th className=\"px-2 py-3 eyebrow text-right\">{t('disk.col.size')}</th>\r\n </tr>\r\n </thead>\r\n <tbody className=\"border-t border-[var(--color-hairline)]\">\r\n {data.topSessions.map((s, i) => {\r\n const displayTitle = s.customTitle ?? s.title;\r\n return (\r\n <tr\r\n key={`${s.projectId}/${s.sessionId}`}\r\n className=\"ribbon-row border-b border-[var(--color-hairline)] hover:bg-[var(--color-sunken)]\"\r\n >\r\n <td className=\"px-2 py-3 align-top font-mono text-[11px] text-[var(--color-fg-faint)]\">\r\n {String(i + 1).padStart(2, '0')}\r\n </td>\r\n <td className=\"px-2 py-3 align-top\">\r\n <Link\r\n to={`/projects/${encodeURIComponent(s.projectId)}/sessions/${s.sessionId}`}\r\n className=\"block truncate font-medium text-[var(--color-fg-primary)] hover:text-[var(--color-accent-ink)] dark:hover:text-[var(--color-accent)]\"\r\n title={displayTitle}\r\n >\r\n {displayTitle}\r\n </Link>\r\n </td>\r\n <td className=\"px-2 py-3 align-top font-mono text-[12px] text-[var(--color-fg-muted)]\">\r\n <Link\r\n to={`/projects/${encodeURIComponent(s.projectId)}`}\r\n className=\"block truncate hover:text-[var(--color-fg-primary)]\"\r\n title={s.projectId}\r\n >\r\n {projectCwdLabel(data, s.projectId)}\r\n </Link>\r\n </td>\r\n <td className=\"px-2 py-3 text-right align-top font-mono text-[12.5px] text-[var(--color-fg-secondary)]\">\r\n {formatRelativeTime(s.lastAt)}\r\n </td>\r\n <td className=\"px-2 py-3 text-right align-top font-mono tabular-nums text-[var(--color-fg-primary)]\">\r\n {formatBytes(s.totalBytes)}\r\n </td>\r\n </tr>\r\n );\r\n })}\r\n </tbody>\r\n </table>\r\n </div>\r\n )}\r\n </div>\r\n </>\r\n )}\r\n </section>\r\n );\r\n}\r\n\r\nfunction Masthead({\r\n title,\r\n tagline,\r\n stats,\r\n}: {\r\n title: string;\r\n tagline: string;\r\n stats: {\r\n totalBytes: number;\r\n projectCount: number;\r\n totalSessions: number;\r\n } | null;\r\n}) {\r\n const t = useT();\r\n return (\r\n <header className=\"relative\">\r\n <div className=\"flex flex-wrap items-baseline gap-x-4 gap-y-1\">\r\n <h1 className=\"font-display text-[clamp(1.75rem,3.5vw,2.25rem)] font-light leading-[1.1] tracking-[-0.02em] text-[var(--color-fg-primary)]\">\r\n {title}\r\n <span className=\"text-[var(--color-accent)]\">.</span>\r\n </h1>\r\n <p className=\"min-w-0 flex-1 font-display text-[13px] italic leading-snug text-[var(--color-fg-muted)]\">\r\n {tagline}\r\n </p>\r\n </div>\r\n {stats && (\r\n <div className=\"mt-3 flex flex-wrap items-baseline gap-x-3 gap-y-1 text-xs\">\r\n <MetaItem label={t('disk.meta.total')} value={formatBytes(stats.totalBytes)} />\r\n <Sep />\r\n <MetaItem label={t('disk.meta.projects')} value={stats.projectCount} />\r\n <Sep />\r\n <MetaItem label={t('disk.meta.sessions')} value={stats.totalSessions.toLocaleString()} />\r\n </div>\r\n )}\r\n </header>\r\n );\r\n}\r\n\r\nfunction tooltipStyle(surface: string, hairline: string, fg: string): React.CSSProperties {\r\n return {\r\n background: surface,\r\n border: `1px solid ${hairline}`,\r\n borderRadius: 8,\r\n fontFamily: 'var(--font-mono)',\r\n fontSize: 11,\r\n color: fg,\r\n boxShadow: 'var(--shadow-pop)',\r\n };\r\n}\r\n\r\nfunction projectCwdLabel(data: DiskUsage, projectId: string): string {\r\n const p = data.byProject.find((row) => row.projectId === projectId);\r\n return p ? shortCwd(p.decodedCwd) : projectId;\r\n}\r\n\r\nfunction shortCwd(cwd: string): string {\r\n const parts = cwd.split(/[\\\\/]+/).filter(Boolean);\r\n if (parts.length <= 2) return cwd;\r\n return '…/' + parts.slice(-2).join('/');\r\n}\r\n\r\nfunction Card({\r\n title,\r\n subtitle,\r\n children,\r\n className = '',\r\n}: {\r\n title: string;\r\n subtitle?: string;\r\n children: React.ReactNode;\r\n className?: string;\r\n}) {\r\n return (\r\n <section className={`surface-card p-5 ${className}`}>\r\n <header className=\"mb-4 flex items-baseline justify-between gap-3\">\r\n <h3 className=\"font-display text-lg font-light tracking-tight text-[var(--color-fg-primary)]\">\r\n {title}\r\n </h3>\r\n {subtitle && (\r\n <span className=\"font-mono text-[10px] uppercase tracking-[0.18em] text-[var(--color-fg-muted)]\">\r\n {subtitle}\r\n </span>\r\n )}\r\n </header>\r\n {children}\r\n </section>\r\n );\r\n}\r\n\r\nfunction Empty({ className = '' }: { className?: string }) {\r\n const t = useT();\r\n return (\r\n <p className={`font-mono text-xs uppercase tracking-[0.18em] text-[var(--color-fg-muted)] ${className}`}>\r\n {t('common.noData')}\r\n </p>\r\n );\r\n}\r\n"],"names":["StatCard","label","value","unit","trail","accent","jsxs","jsx","PALETTE_VARS","CHART_VARS","useThemeColors","vars","snapshot","setSnapshot","useState","readVars","useEffect","observer","cs","out","v","DiskUsageRoute","t","useT","data","isLoading","error","useQuery","queryKeys","api","colors","muted","hairline","surface","fgPrimary","palette","resolvedPalette","useMemo","pieData","p","shortCwd","monthData","m","Masthead","Loading","Fragment","motion","staggerParent","fadeUpItem","formatBytes","_a","Card","Empty","ResponsiveContainer","PieChart","Pie","_","Cell","Tooltip","tooltipStyle","pct","AreaChart","XAxis","YAxis","Area","s","displayTitle","Link","projectCwdLabel","formatRelativeTime","title","tagline","stats","MetaItem","Sep","fg","projectId","row","cwd","parts","subtitle","children","className"],"mappings":"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"}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import{r as x,j as e}from"./react-W0jzChlo.js";import{u as P,a as h}from"./query-C1K1uQRu.js";import{u as C,P as I,c as b,q as u}from"./index-DLATR3tZ.js";import{L as S}from"./router-DfbutHY3.js";import"./vendor-CH80ylbS.js";const E=["skip","overwrite-if-newer","keep-both"],K={skip:"import.policy.skip","overwrite-if-newer":"import.policy.overwrite-if-newer","keep-both":"import.policy.keep-both"},L={create:"import.action.create",overwrite:"import.action.overwrite","keep-both":"import.action.keep-both",skip:"import.action.skip"},y={"existing-project":"import.suggestion.existing-project","original-path":"import.suggestion.original-path","same-basename":"import.suggestion.same-basename"},T={create:"import.memory.create",skip:"import.memory.skip",conflict:"import.memory.conflict"};function q(o){switch(o){case"create":return"border-[var(--color-moss)]/40 bg-[var(--color-moss-soft)] text-[var(--color-fg-primary)]";case"overwrite":return"border-[var(--color-danger)]/40 bg-[var(--color-danger-soft)] text-[var(--color-danger)]";case"keep-both":return"border-[var(--color-accent)]/40 bg-[var(--color-accent-soft)] text-[var(--color-accent-ink)] dark:text-[var(--color-accent)]";default:return"border-[var(--color-hairline-strong)] bg-[var(--color-sunken)] text-[var(--color-fg-muted)]"}}function A(){const o=C(),d=P(),[l,j]=x.useState(""),[i,v]=x.useState(""),[s,w]=x.useState("skip"),[t,g]=x.useState(null),n=h({mutationFn:r=>b("/api/import/preview",{method:"POST",body:JSON.stringify({bundleDir:l.trim(),targetCwd:r.targetCwd,collisionPolicy:r.collisionPolicy})}),onSuccess:r=>{g(r),v(r.remap.targetCwd)}}),a=h({mutationFn:()=>b("/api/import",{method:"POST",body:JSON.stringify({bundleDir:l.trim(),targetCwd:i.trim(),collisionPolicy:s})}),onSuccess:r=>{d.invalidateQueries({queryKey:u.projects()}),d.invalidateQueries({queryKey:u.projectSessions(r.targetProjectId)}),d.invalidateQueries({queryKey:u.projectMemory(r.targetProjectId)}),d.invalidateQueries({queryKey:u.diskUsage()})}});function f(){l.trim()!==""&&(g(null),a.reset(),n.mutate({targetCwd:void 0,collisionPolicy:s}))}function m(r,k){a.reset(),n.mutate({targetCwd:r.trim()||void 0,collisionPolicy:k})}function N(r){w(r),t&&m(i,r)}const c=a.data,p=n.isPending;return e.jsxs("section",{children:[e.jsxs("div",{className:"surface-card p-6",children:[e.jsx(I,{eyebrow:o("nav.import"),title:o("import.title")}),e.jsx("p",{className:"mt-2 max-w-2xl text-sm text-[var(--color-fg-muted)]",children:o("import.tagline")}),e.jsxs("div",{className:"mt-5 flex flex-col gap-2 sm:flex-row",children:[e.jsx("input",{type:"text",value:l,onChange:r=>j(r.target.value),onKeyDown:r=>r.key==="Enter"&&f(),placeholder:o("import.bundlePlaceholder"),spellCheck:!1,"aria-label":o("import.bundleLabel"),className:"min-w-0 flex-1 rounded-[var(--radius-input)] border border-[var(--color-hairline-strong)] bg-[var(--color-canvas)] px-3 py-2 font-mono text-sm text-[var(--color-fg-primary)] outline-none focus:border-[var(--color-accent)]"}),e.jsx("button",{type:"button",onClick:f,disabled:l.trim()===""||p,className:"rounded-[var(--radius-control)] bg-[var(--color-fg-primary)] px-4 py-2 text-sm font-medium text-[var(--color-canvas)] shadow-[var(--shadow-rise)] hover:opacity-90 disabled:cursor-not-allowed disabled:opacity-50",children:o(p?"import.btn.loading":t?"import.btn.recheck":"import.btn.load")})]}),n.error&&e.jsx("p",{className:"mt-3 rounded-md border border-[var(--color-danger)]/40 bg-[var(--color-danger-soft)] px-3 py-2 text-sm text-[var(--color-danger)]",children:n.error.message}),!t&&!n.error&&e.jsx("p",{className:"mt-4 text-sm text-[var(--color-fg-muted)]",children:o("import.empty")})]}),t&&e.jsxs("div",{className:"surface-card mt-6 space-y-6 p-6",children:[e.jsx("p",{className:"break-all font-mono text-xs text-[var(--color-fg-muted)]",children:o("import.source",{platform:t.source.platform,cwd:t.source.cwd})}),e.jsxs("div",{children:[e.jsx("span",{className:"eyebrow",children:o("import.targetLabel")}),e.jsxs("div",{className:"mt-1.5 flex flex-col gap-2 sm:flex-row",children:[e.jsx("input",{type:"text",value:i,onChange:r=>v(r.target.value),onKeyDown:r=>r.key==="Enter"&&m(i,s),spellCheck:!1,"aria-label":o("import.targetLabel"),className:"min-w-0 flex-1 rounded-[var(--radius-input)] border border-[var(--color-hairline-strong)] bg-[var(--color-canvas)] px-3 py-2 font-mono text-sm text-[var(--color-fg-primary)] outline-none focus:border-[var(--color-accent)]"}),e.jsx("button",{type:"button",onClick:()=>m(i,s),disabled:p,className:"rounded-[var(--radius-control)] border border-[var(--color-hairline-strong)] px-4 py-2 text-sm text-[var(--color-fg-secondary)] hover:bg-[var(--color-sunken)] disabled:opacity-50",children:o("import.btn.recheck")})]}),e.jsx("p",{className:"mt-1.5 text-xs text-[var(--color-fg-muted)]",children:o("import.targetHint")}),e.jsx("p",{className:"mt-1 font-mono text-[11px] text-[var(--color-fg-faint)]",children:o("import.targetId",{id:t.remap.targetProjectId})}),t.suggestions.length>0&&e.jsxs("div",{className:"mt-2 flex flex-wrap items-center gap-1.5",children:[e.jsx("span",{className:"eyebrow mr-1",children:o("import.suggestions")}),t.suggestions.map(r=>e.jsxs("button",{type:"button",onClick:()=>{v(r.cwd),m(r.cwd,s)},className:"inline-flex items-center gap-1.5 rounded-[var(--radius-control)] border border-[var(--color-hairline-strong)] bg-[var(--color-surface)] px-2.5 py-1 font-mono text-[11px] text-[var(--color-fg-secondary)] hover:border-[var(--color-accent)] hover:text-[var(--color-accent-ink)] dark:hover:text-[var(--color-accent)]",title:o(y[r.reason]),children:[e.jsx("span",{className:"max-w-[20rem] truncate",children:r.cwd}),e.jsxs("span",{className:"text-[var(--color-fg-faint)]",children:["· ",o(y[r.reason])]})]},r.projectId))]})]}),e.jsxs("div",{children:[e.jsx("span",{className:"eyebrow",children:o("import.policyLabel")}),e.jsx("div",{className:"mt-1.5 inline-flex flex-wrap gap-1 rounded-[var(--radius-input)] border border-[var(--color-hairline)] bg-[var(--color-sunken)] p-1",children:E.map(r=>e.jsx("button",{type:"button",onClick:()=>N(r),"aria-pressed":s===r,className:"rounded-[var(--radius-control)] px-3 py-1.5 text-xs font-medium transition "+(s===r?"bg-[var(--color-surface)] text-[var(--color-fg-primary)] shadow-[var(--shadow-rise)]":"text-[var(--color-fg-muted)] hover:text-[var(--color-fg-primary)]"),children:o(K[r])},r))})]}),e.jsxs("div",{children:[e.jsxs("div",{className:"flex items-baseline justify-between",children:[e.jsx("span",{className:"eyebrow",children:o("import.sessions.heading")}),e.jsx("span",{className:"font-mono text-[11px] text-[var(--color-fg-faint)]",children:o("import.history",{n:t.historyLinesToAdd})})]}),t.sessions.length===0?e.jsx("p",{className:"mt-2 text-sm text-[var(--color-fg-muted)]",children:o("import.sessions.empty")}):e.jsx("ul",{className:"mt-2 space-y-1.5",children:t.sessions.map(r=>e.jsxs("li",{className:"flex items-center gap-3 rounded-md border border-[var(--color-hairline)] bg-[var(--color-canvas)] px-3 py-2",children:[e.jsx("span",{className:"shrink-0 rounded-[var(--radius-control)] border px-2 py-0.5 text-[10px] font-medium uppercase tracking-[0.12em] "+q(r.action),children:o(L[r.action])}),e.jsxs("span",{className:"min-w-0 flex-1",children:[e.jsx("span",{className:"block truncate text-sm text-[var(--color-fg-primary)]",children:r.title}),e.jsx("span",{className:"block truncate font-mono text-[10.5px] text-[var(--color-fg-faint)]",children:r.newSessionId??r.sessionId})]}),r.reason&&e.jsx("span",{className:"shrink-0 text-[11px] text-[var(--color-fg-muted)]",children:r.reason})]},r.sessionId))})]}),t.memory.length>0&&e.jsxs("div",{children:[e.jsx("span",{className:"eyebrow",children:o("import.memory.heading")}),e.jsx("ul",{className:"mt-2 flex flex-wrap gap-1.5",children:t.memory.map(r=>e.jsxs("li",{className:"inline-flex items-center gap-1.5 rounded-[var(--radius-control)] border border-[var(--color-hairline)] bg-[var(--color-canvas)] px-2.5 py-1 font-mono text-[11px] text-[var(--color-fg-secondary)]",children:[e.jsx("span",{className:"text-[var(--color-fg-primary)]",children:r.filename}),e.jsxs("span",{className:"text-[var(--color-fg-faint)]",children:["·"," ",r.action==="conflict"?o("import.memory.conflict",{name:r.writtenAs??""}):o(T[r.action])]})]},r.filename))})]}),a.error&&e.jsx("p",{className:"rounded-md border border-[var(--color-danger)]/40 bg-[var(--color-danger-soft)] px-3 py-2 text-sm text-[var(--color-danger)]",children:a.error.message}),c?e.jsxs("div",{className:"space-y-3 rounded-[var(--radius-card)] border border-[var(--color-moss)]/40 bg-[var(--color-moss-soft)] px-4 py-3",children:[e.jsx("p",{className:"font-display text-lg font-light text-[var(--color-fg-primary)]",children:o("import.result.title")}),e.jsx("p",{className:"text-sm text-[var(--color-fg-secondary)]",children:o("import.result.summary",{n:c.imported.length,skipped:c.skipped.length,memory:c.memoryWritten.length,lines:c.historyLinesAdded})}),e.jsx(S,{to:`/projects/${encodeURIComponent(c.targetProjectId)}`,className:"inline-flex w-fit items-center gap-2 rounded-[var(--radius-control)] bg-[var(--color-fg-primary)] px-4 py-1.5 text-sm font-medium text-[var(--color-canvas)] hover:opacity-90",children:o("import.result.viewProject")})]}):e.jsx("div",{className:"flex justify-end border-t border-[var(--color-hairline)] pt-4",children:e.jsx("button",{type:"button",onClick:()=>a.mutate(),disabled:i.trim()===""||p||a.isPending,className:"rounded-[var(--radius-control)] bg-[var(--color-fg-primary)] px-5 py-2 text-sm font-medium text-[var(--color-canvas)] shadow-[var(--shadow-rise)] hover:opacity-90 disabled:cursor-not-allowed disabled:opacity-50",children:a.isPending?o("import.btn.committing"):o("import.btn.commit")})})]})]})}export{A as default};
|
|
2
|
+
//# sourceMappingURL=ImportPage-b8NORa8b.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ImportPage-b8NORa8b.js","sources":["../../web/src/routes/ImportPage.tsx"],"sourcesContent":["import { useMutation, useQueryClient } from '@tanstack/react-query';\r\nimport { useState } from 'react';\r\nimport { Link } from 'react-router-dom';\r\nimport PageHeader from '../components/PageHeader.tsx';\r\nimport {\r\n api,\r\n type ImportCollisionPolicy,\r\n type ImportPreviewResult,\r\n type ImportResult,\r\n type ImportSessionAction,\r\n} from '../lib/api.ts';\r\nimport { useT } from '../lib/i18n.ts';\r\nimport { queryKeys } from '../lib/query-keys.ts';\r\n\r\nconst POLICIES: ImportCollisionPolicy[] = ['skip', 'overwrite-if-newer', 'keep-both'];\r\n\r\nconst POLICY_KEY = {\r\n skip: 'import.policy.skip',\r\n 'overwrite-if-newer': 'import.policy.overwrite-if-newer',\r\n 'keep-both': 'import.policy.keep-both',\r\n} as const;\r\n\r\nconst ACTION_KEY = {\r\n create: 'import.action.create',\r\n overwrite: 'import.action.overwrite',\r\n 'keep-both': 'import.action.keep-both',\r\n skip: 'import.action.skip',\r\n} as const;\r\n\r\nconst SUGGEST_KEY = {\r\n 'existing-project': 'import.suggestion.existing-project',\r\n 'original-path': 'import.suggestion.original-path',\r\n 'same-basename': 'import.suggestion.same-basename',\r\n} as const;\r\n\r\nconst MEM_KEY = {\r\n create: 'import.memory.create',\r\n skip: 'import.memory.skip',\r\n conflict: 'import.memory.conflict',\r\n} as const;\r\n\r\nfunction actionTone(action: ImportSessionAction): string {\r\n switch (action) {\r\n case 'create':\r\n return 'border-[var(--color-moss)]/40 bg-[var(--color-moss-soft)] text-[var(--color-fg-primary)]';\r\n case 'overwrite':\r\n return 'border-[var(--color-danger)]/40 bg-[var(--color-danger-soft)] text-[var(--color-danger)]';\r\n case 'keep-both':\r\n return 'border-[var(--color-accent)]/40 bg-[var(--color-accent-soft)] text-[var(--color-accent-ink)] dark:text-[var(--color-accent)]';\r\n default:\r\n return 'border-[var(--color-hairline-strong)] bg-[var(--color-sunken)] text-[var(--color-fg-muted)]';\r\n }\r\n}\r\n\r\nexport default function ImportPage() {\r\n const t = useT();\r\n const queryClient = useQueryClient();\r\n\r\n const [bundleDir, setBundleDir] = useState('');\r\n const [targetCwd, setTargetCwd] = useState('');\r\n const [policy, setPolicy] = useState<ImportCollisionPolicy>('skip');\r\n const [preview, setPreview] = useState<ImportPreviewResult | null>(null);\r\n\r\n const previewMutation = useMutation({\r\n mutationFn: (vars: { targetCwd?: string; collisionPolicy: ImportCollisionPolicy }) =>\r\n api<ImportPreviewResult>('/api/import/preview', {\r\n method: 'POST',\r\n body: JSON.stringify({\r\n bundleDir: bundleDir.trim(),\r\n targetCwd: vars.targetCwd,\r\n collisionPolicy: vars.collisionPolicy,\r\n }),\r\n }),\r\n onSuccess: (data) => {\r\n setPreview(data);\r\n setTargetCwd(data.remap.targetCwd);\r\n },\r\n });\r\n\r\n const commitMutation = useMutation({\r\n mutationFn: () =>\r\n api<ImportResult>('/api/import', {\r\n method: 'POST',\r\n body: JSON.stringify({\r\n bundleDir: bundleDir.trim(),\r\n targetCwd: targetCwd.trim(),\r\n collisionPolicy: policy,\r\n }),\r\n }),\r\n onSuccess: (data) => {\r\n queryClient.invalidateQueries({ queryKey: queryKeys.projects() });\r\n queryClient.invalidateQueries({ queryKey: queryKeys.projectSessions(data.targetProjectId) });\r\n queryClient.invalidateQueries({ queryKey: queryKeys.projectMemory(data.targetProjectId) });\r\n queryClient.invalidateQueries({ queryKey: queryKeys.diskUsage() });\r\n },\r\n });\r\n\r\n function loadBundle() {\r\n if (bundleDir.trim() === '') return;\r\n setPreview(null);\r\n commitMutation.reset();\r\n previewMutation.mutate({ targetCwd: undefined, collisionPolicy: policy });\r\n }\r\n\r\n function recheck(nextTarget: string, nextPolicy: ImportCollisionPolicy) {\r\n commitMutation.reset();\r\n previewMutation.mutate({\r\n targetCwd: nextTarget.trim() || undefined,\r\n collisionPolicy: nextPolicy,\r\n });\r\n }\r\n\r\n function changePolicy(p: ImportCollisionPolicy) {\r\n setPolicy(p);\r\n if (preview) recheck(targetCwd, p);\r\n }\r\n\r\n const result = commitMutation.data;\r\n const busy = previewMutation.isPending;\r\n\r\n return (\r\n <section>\r\n <div className=\"surface-card p-6\">\r\n <PageHeader eyebrow={t('nav.import')} title={t('import.title')} />\r\n <p className=\"mt-2 max-w-2xl text-sm text-[var(--color-fg-muted)]\">\r\n {t('import.tagline')}\r\n </p>\r\n\r\n <div className=\"mt-5 flex flex-col gap-2 sm:flex-row\">\r\n <input\r\n type=\"text\"\r\n value={bundleDir}\r\n onChange={(e) => setBundleDir(e.target.value)}\r\n onKeyDown={(e) => e.key === 'Enter' && loadBundle()}\r\n placeholder={t('import.bundlePlaceholder')}\r\n spellCheck={false}\r\n aria-label={t('import.bundleLabel')}\r\n className=\"min-w-0 flex-1 rounded-[var(--radius-input)] border border-[var(--color-hairline-strong)] bg-[var(--color-canvas)] px-3 py-2 font-mono text-sm text-[var(--color-fg-primary)] outline-none focus:border-[var(--color-accent)]\"\r\n />\r\n <button\r\n type=\"button\"\r\n onClick={loadBundle}\r\n disabled={bundleDir.trim() === '' || busy}\r\n className=\"rounded-[var(--radius-control)] bg-[var(--color-fg-primary)] px-4 py-2 text-sm font-medium text-[var(--color-canvas)] shadow-[var(--shadow-rise)] hover:opacity-90 disabled:cursor-not-allowed disabled:opacity-50\"\r\n >\r\n {busy ? t('import.btn.loading') : preview ? t('import.btn.recheck') : t('import.btn.load')}\r\n </button>\r\n </div>\r\n\r\n {previewMutation.error && (\r\n <p className=\"mt-3 rounded-md border border-[var(--color-danger)]/40 bg-[var(--color-danger-soft)] px-3 py-2 text-sm text-[var(--color-danger)]\">\r\n {(previewMutation.error as Error).message}\r\n </p>\r\n )}\r\n\r\n {!preview && !previewMutation.error && (\r\n <p className=\"mt-4 text-sm text-[var(--color-fg-muted)]\">{t('import.empty')}</p>\r\n )}\r\n </div>\r\n\r\n {preview && (\r\n <div className=\"surface-card mt-6 space-y-6 p-6\">\r\n <p className=\"break-all font-mono text-xs text-[var(--color-fg-muted)]\">\r\n {t('import.source', { platform: preview.source.platform, cwd: preview.source.cwd })}\r\n </p>\r\n\r\n {/* target */}\r\n <div>\r\n <span className=\"eyebrow\">{t('import.targetLabel')}</span>\r\n <div className=\"mt-1.5 flex flex-col gap-2 sm:flex-row\">\r\n <input\r\n type=\"text\"\r\n value={targetCwd}\r\n onChange={(e) => setTargetCwd(e.target.value)}\r\n onKeyDown={(e) => e.key === 'Enter' && recheck(targetCwd, policy)}\r\n spellCheck={false}\r\n aria-label={t('import.targetLabel')}\r\n className=\"min-w-0 flex-1 rounded-[var(--radius-input)] border border-[var(--color-hairline-strong)] bg-[var(--color-canvas)] px-3 py-2 font-mono text-sm text-[var(--color-fg-primary)] outline-none focus:border-[var(--color-accent)]\"\r\n />\r\n <button\r\n type=\"button\"\r\n onClick={() => recheck(targetCwd, policy)}\r\n disabled={busy}\r\n className=\"rounded-[var(--radius-control)] border border-[var(--color-hairline-strong)] px-4 py-2 text-sm text-[var(--color-fg-secondary)] hover:bg-[var(--color-sunken)] disabled:opacity-50\"\r\n >\r\n {t('import.btn.recheck')}\r\n </button>\r\n </div>\r\n <p className=\"mt-1.5 text-xs text-[var(--color-fg-muted)]\">{t('import.targetHint')}</p>\r\n <p className=\"mt-1 font-mono text-[11px] text-[var(--color-fg-faint)]\">\r\n {t('import.targetId', { id: preview.remap.targetProjectId })}\r\n </p>\r\n\r\n {preview.suggestions.length > 0 && (\r\n <div className=\"mt-2 flex flex-wrap items-center gap-1.5\">\r\n <span className=\"eyebrow mr-1\">{t('import.suggestions')}</span>\r\n {preview.suggestions.map((s) => (\r\n <button\r\n key={s.projectId}\r\n type=\"button\"\r\n onClick={() => {\r\n setTargetCwd(s.cwd);\r\n recheck(s.cwd, policy);\r\n }}\r\n className=\"inline-flex items-center gap-1.5 rounded-[var(--radius-control)] border border-[var(--color-hairline-strong)] bg-[var(--color-surface)] px-2.5 py-1 font-mono text-[11px] text-[var(--color-fg-secondary)] hover:border-[var(--color-accent)] hover:text-[var(--color-accent-ink)] dark:hover:text-[var(--color-accent)]\"\r\n title={t(SUGGEST_KEY[s.reason])}\r\n >\r\n <span className=\"max-w-[20rem] truncate\">{s.cwd}</span>\r\n <span className=\"text-[var(--color-fg-faint)]\">· {t(SUGGEST_KEY[s.reason])}</span>\r\n </button>\r\n ))}\r\n </div>\r\n )}\r\n </div>\r\n\r\n {/* policy */}\r\n <div>\r\n <span className=\"eyebrow\">{t('import.policyLabel')}</span>\r\n <div className=\"mt-1.5 inline-flex flex-wrap gap-1 rounded-[var(--radius-input)] border border-[var(--color-hairline)] bg-[var(--color-sunken)] p-1\">\r\n {POLICIES.map((p) => (\r\n <button\r\n key={p}\r\n type=\"button\"\r\n onClick={() => changePolicy(p)}\r\n aria-pressed={policy === p}\r\n className={\r\n 'rounded-[var(--radius-control)] px-3 py-1.5 text-xs font-medium transition ' +\r\n (policy === p\r\n ? 'bg-[var(--color-surface)] text-[var(--color-fg-primary)] shadow-[var(--shadow-rise)]'\r\n : 'text-[var(--color-fg-muted)] hover:text-[var(--color-fg-primary)]')\r\n }\r\n >\r\n {t(POLICY_KEY[p])}\r\n </button>\r\n ))}\r\n </div>\r\n </div>\r\n\r\n {/* sessions */}\r\n <div>\r\n <div className=\"flex items-baseline justify-between\">\r\n <span className=\"eyebrow\">{t('import.sessions.heading')}</span>\r\n <span className=\"font-mono text-[11px] text-[var(--color-fg-faint)]\">\r\n {t('import.history', { n: preview.historyLinesToAdd })}\r\n </span>\r\n </div>\r\n {preview.sessions.length === 0 ? (\r\n <p className=\"mt-2 text-sm text-[var(--color-fg-muted)]\">\r\n {t('import.sessions.empty')}\r\n </p>\r\n ) : (\r\n <ul className=\"mt-2 space-y-1.5\">\r\n {preview.sessions.map((s) => (\r\n <li\r\n key={s.sessionId}\r\n className=\"flex items-center gap-3 rounded-md border border-[var(--color-hairline)] bg-[var(--color-canvas)] px-3 py-2\"\r\n >\r\n <span\r\n className={\r\n 'shrink-0 rounded-[var(--radius-control)] border px-2 py-0.5 text-[10px] font-medium uppercase tracking-[0.12em] ' +\r\n actionTone(s.action)\r\n }\r\n >\r\n {t(ACTION_KEY[s.action])}\r\n </span>\r\n <span className=\"min-w-0 flex-1\">\r\n <span className=\"block truncate text-sm text-[var(--color-fg-primary)]\">\r\n {s.title}\r\n </span>\r\n <span className=\"block truncate font-mono text-[10.5px] text-[var(--color-fg-faint)]\">\r\n {s.newSessionId ?? s.sessionId}\r\n </span>\r\n </span>\r\n {s.reason && (\r\n <span className=\"shrink-0 text-[11px] text-[var(--color-fg-muted)]\">\r\n {s.reason}\r\n </span>\r\n )}\r\n </li>\r\n ))}\r\n </ul>\r\n )}\r\n </div>\r\n\r\n {/* memory */}\r\n {preview.memory.length > 0 && (\r\n <div>\r\n <span className=\"eyebrow\">{t('import.memory.heading')}</span>\r\n <ul className=\"mt-2 flex flex-wrap gap-1.5\">\r\n {preview.memory.map((m) => (\r\n <li\r\n key={m.filename}\r\n className=\"inline-flex items-center gap-1.5 rounded-[var(--radius-control)] border border-[var(--color-hairline)] bg-[var(--color-canvas)] px-2.5 py-1 font-mono text-[11px] text-[var(--color-fg-secondary)]\"\r\n >\r\n <span className=\"text-[var(--color-fg-primary)]\">{m.filename}</span>\r\n <span className=\"text-[var(--color-fg-faint)]\">\r\n ·{' '}\r\n {m.action === 'conflict'\r\n ? t('import.memory.conflict', { name: m.writtenAs ?? '' })\r\n : t(MEM_KEY[m.action])}\r\n </span>\r\n </li>\r\n ))}\r\n </ul>\r\n </div>\r\n )}\r\n\r\n {commitMutation.error && (\r\n <p className=\"rounded-md border border-[var(--color-danger)]/40 bg-[var(--color-danger-soft)] px-3 py-2 text-sm text-[var(--color-danger)]\">\r\n {(commitMutation.error as Error).message}\r\n </p>\r\n )}\r\n\r\n {result ? (\r\n <div className=\"space-y-3 rounded-[var(--radius-card)] border border-[var(--color-moss)]/40 bg-[var(--color-moss-soft)] px-4 py-3\">\r\n <p className=\"font-display text-lg font-light text-[var(--color-fg-primary)]\">\r\n {t('import.result.title')}\r\n </p>\r\n <p className=\"text-sm text-[var(--color-fg-secondary)]\">\r\n {t('import.result.summary', {\r\n n: result.imported.length,\r\n skipped: result.skipped.length,\r\n memory: result.memoryWritten.length,\r\n lines: result.historyLinesAdded,\r\n })}\r\n </p>\r\n <Link\r\n to={`/projects/${encodeURIComponent(result.targetProjectId)}`}\r\n className=\"inline-flex w-fit items-center gap-2 rounded-[var(--radius-control)] bg-[var(--color-fg-primary)] px-4 py-1.5 text-sm font-medium text-[var(--color-canvas)] hover:opacity-90\"\r\n >\r\n {t('import.result.viewProject')}\r\n </Link>\r\n </div>\r\n ) : (\r\n <div className=\"flex justify-end border-t border-[var(--color-hairline)] pt-4\">\r\n <button\r\n type=\"button\"\r\n onClick={() => commitMutation.mutate()}\r\n disabled={targetCwd.trim() === '' || busy || commitMutation.isPending}\r\n className=\"rounded-[var(--radius-control)] bg-[var(--color-fg-primary)] px-5 py-2 text-sm font-medium text-[var(--color-canvas)] shadow-[var(--shadow-rise)] hover:opacity-90 disabled:cursor-not-allowed disabled:opacity-50\"\r\n >\r\n {commitMutation.isPending ? t('import.btn.committing') : t('import.btn.commit')}\r\n </button>\r\n </div>\r\n )}\r\n </div>\r\n )}\r\n </section>\r\n );\r\n}\r\n"],"names":["POLICIES","POLICY_KEY","ACTION_KEY","SUGGEST_KEY","MEM_KEY","actionTone","action","ImportPage","t","useT","queryClient","useQueryClient","bundleDir","setBundleDir","useState","targetCwd","setTargetCwd","policy","setPolicy","preview","setPreview","previewMutation","useMutation","vars","api","data","commitMutation","queryKeys","loadBundle","recheck","nextTarget","nextPolicy","changePolicy","p","result","busy","jsxs","jsx","PageHeader","e","s","m","Link"],"mappings":"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"}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import{r as u,j as e}from"./react-W0jzChlo.js";import{b as T}from"./query-C1K1uQRu.js";import{u as y,B as Y,d as Z,P as ee,M as R,S as U,b as C,L as te,a as re,c as O,q as Q}from"./index-DLATR3tZ.js";import{b as ne,c as oe}from"./router-DfbutHY3.js";import"./vendor-CH80ylbS.js";const se=/^[\s*-]*\[(?<title>[^\]]+)\]\((?<href>[^)]+)\)\s*[—–\-:]?\s*(?<hook>.*)$/;function we(){const t=y(),{projectId:r}=ne(),i=r??"",[s,l]=oe(),c=T({queryKey:Q.projectMemory(i),queryFn:()=>O(`/api/projects/${encodeURIComponent(i)}/memory`),enabled:!!i}),a=T({queryKey:Q.projects(),queryFn:()=>O("/api/projects")}),d=u.useMemo(()=>{var n;return(n=a.data)==null?void 0:n.find(p=>p.id===i)},[a.data,i]),o=c.data,m=u.useMemo(()=>(o==null?void 0:o.entries)??[],[o==null?void 0:o.entries]),x=u.useMemo(()=>fe((o==null?void 0:o.index)??null),[o==null?void 0:o.index]),f=u.useMemo(()=>{const n=new Map;for(const p of x)p.link&&n.set(p.link.href,p.link.hook);return n},[x]),{rows:h,orphans:j}=u.useMemo(()=>he(x,m),[x,m]),v=s.get("entry"),b=s.get("view"),H=v!==null||b==="raw";function V(n){const p=new URLSearchParams(s);p.set("entry",n),p.delete("view"),l(p)}function _(){const n=new URLSearchParams(s);n.set("view","raw"),n.delete("entry"),l(n)}function F(){const n=new URLSearchParams(s);n.delete("entry"),n.delete("view"),l(n)}const g=u.useMemo(()=>b==="raw"?{kind:"raw"}:v?{kind:"entry",entry:m.find(p=>p.filename===v)??null,filename:v}:null,[v,b,m]),[L,G]=u.useState(""),k=L.trim().toLowerCase(),w=k.length>0,I=u.useMemo(()=>w?h.filter(n=>n.kind==="entry"&&z(n.entry,n.hook,k)):h,[h,w,k]),E=u.useMemo(()=>w?j.filter(n=>z(n,"",k)):j,[j,w,k]),N=m.length,B=!!(o!=null&&o.index),S=N>0||B,J=I.filter(n=>n.kind==="entry").length+E.length,P=(d==null?void 0:d.decodedCwd)??i,q=P.split(/[\\/]+/).filter(Boolean),X=q.at(-1)??P,$=q.slice(0,-1).join("/"),D=u.useMemo(()=>{let n=null;for(const p of m){if(!p.mtime)continue;const M=Date.parse(p.mtime);Number.isFinite(M)&&(n===null||M>n)&&(n=M)}return n?new Date(n).toISOString():null},[m]),K=u.useMemo(()=>{const n=new Set;for(const p of m)n.add(p.type??"other");return n.size},[m]);return e.jsxs("section",{children:[e.jsx(Y,{items:[{label:t("session.crumbProjects"),to:"/"},{label:X,to:`/projects/${encodeURIComponent(i)}`,mono:!0,icon:e.jsx(Z,{})},{label:t("memory.title")}]}),e.jsx("div",{className:"surface-card mt-6 p-6",children:e.jsx(ee,{eyebrow:e.jsx("span",{className:"inline-flex items-center gap-2",children:$?e.jsxs("span",{className:"font-mono normal-case tracking-normal",children:[$,"/"]}):t("memory.action.open")}),title:t("memory.title"),meta:o?e.jsxs(e.Fragment,{children:[e.jsx(R,{label:t("memory.meta.entries"),value:N}),K>0&&e.jsxs(e.Fragment,{children:[e.jsx(U,{}),e.jsx(R,{label:t("memory.meta.types"),value:K})]}),D&&e.jsxs(e.Fragment,{children:[e.jsx(U,{}),e.jsx(R,{label:t("memory.meta.lastUpdate"),value:C(D)})]})]}):null})}),c.isLoading&&e.jsx(te,{label:t("memory.loading"),className:"mt-6"}),c.error&&e.jsxs("p",{className:"mt-6 rounded-md border border-[var(--color-danger)]/40 bg-[var(--color-danger-soft)] px-4 py-3 text-sm text-[var(--color-danger)]",children:[t("common.failedMemory"),": ",c.error.message]}),o&&!S&&e.jsx(de,{}),o&&S&&e.jsx(ae,{rows:I,orphans:E,totalMatched:J,totalEntries:N,query:L,onQuery:G,searchActive:w,indexAvailable:B,onOpenEntry:V,onOpenRaw:_}),H&&g&&e.jsx(ie,{target:g,rawIndex:(o==null?void 0:o.index)??"",hookForEntry:g.kind==="entry"&&g.entry?f.get(g.entry.filename)??null:null,onClose:F,onJumpToCover:()=>{F(),requestAnimationFrame(()=>{const n=document.getElementById("memo-index-card");n==null||n.scrollIntoView({behavior:"smooth",block:"start"})})}})]})}function ae({rows:t,orphans:r,totalMatched:i,totalEntries:s,query:l,onQuery:c,searchActive:a,indexAvailable:d,onOpenEntry:o,onOpenRaw:m}){const x=y(),f=a&&i===0;return e.jsxs("div",{id:"memo-index-card",className:"surface-card mt-6 px-7 py-6",children:[e.jsxs("div",{className:"flex items-baseline justify-between gap-3",children:[e.jsxs("div",{children:[e.jsx("div",{className:"eyebrow",children:x("memory.cover.title")}),e.jsx("h2",{className:"mt-0.5 font-display text-[18px] font-medium tracking-tight text-[var(--color-fg-primary)]",children:x("memory.cover.subtitle")})]}),d&&e.jsxs("button",{type:"button",onClick:m,className:"font-mono text-[11px] uppercase tracking-[0.16em] text-[var(--color-fg-muted)] hover:text-[var(--color-accent-ink)] dark:hover:text-[var(--color-accent)]",children:[x("memory.cover.viewRaw")," ↗"]})]}),e.jsxs("div",{className:"mt-4 flex flex-wrap items-center gap-3",children:[e.jsxs("div",{className:"flex flex-1 min-w-[14rem] items-center gap-2 border-b border-[var(--color-hairline)] py-1.5 transition focus-within:border-[var(--color-accent)]",children:[e.jsx(xe,{className:"text-[var(--color-fg-muted)]"}),e.jsx("input",{type:"search",value:l,onChange:h=>c(h.target.value),placeholder:x("memory.search.placeholder"),className:"w-full bg-transparent text-sm text-[var(--color-fg-primary)] placeholder:text-[var(--color-fg-faint)] focus:outline-none"})]}),e.jsx("span",{className:"font-mono text-[10.5px] uppercase tracking-[0.18em] tabular-nums text-[var(--color-fg-faint)]",children:a?x("memory.list.count",{n:i,total:s}):`${s} ${x("memory.meta.entries").toLowerCase()}`})]}),e.jsx("div",{className:"rule-dotted mt-5","aria-hidden":!0}),f?e.jsx("p",{className:"mt-6 text-sm text-[var(--color-fg-muted)]",children:x("memory.list.noResults")}):e.jsxs("ul",{className:"mt-3",children:[t.map((h,j)=>e.jsx(W,{row:h,onOpenEntry:o},j)),r.length>0&&e.jsxs(e.Fragment,{children:[!a&&t.length>0&&e.jsx("li",{"aria-hidden":!0,className:"h-3"}),e.jsx("li",{className:"pt-3 pb-2",children:e.jsxs("div",{className:"flex items-baseline gap-3",children:[e.jsx("span",{className:"font-mono text-[10.5px] uppercase tracking-[0.18em] text-[var(--color-fg-faint)]",children:x("memory.cover.orphan")}),e.jsxs("span",{className:"font-mono text-[10.5px] tabular-nums text-[var(--color-fg-faint)]",children:["· ",r.length]}),e.jsx("span",{"aria-hidden":!0,className:"ml-2 h-px flex-1 bg-[var(--color-hairline)]"})]})}),r.map(h=>e.jsx(W,{row:{kind:"entry",entry:h,hook:""},onOpenEntry:o},h.filename))]})]})]})}function W({row:t,onOpenEntry:r}){const i=y();if(t.kind==="spacer")return e.jsx("li",{"aria-hidden":!0,className:"h-2"});if(t.kind==="heading")return e.jsx("li",{className:"pt-4 pb-1",children:e.jsx("div",{className:"font-mono text-[10.5px] uppercase tracking-[0.18em] text-[var(--color-fg-muted)]",children:t.text})});if(t.kind==="text")return e.jsx("li",{className:"px-1 py-0.5 text-[12.5px] text-[var(--color-fg-faint)] whitespace-pre-wrap break-words",children:t.text});if(t.kind==="missing")return e.jsxs("li",{title:i("memory.cover.missingTooltip"),className:"flex items-baseline gap-3 px-2 py-1.5 text-[var(--color-fg-faint)]",children:[e.jsx("span",{className:"text-[14px] font-medium line-through decoration-[var(--color-fg-faint)]/50",children:t.title}),t.hook&&e.jsx("span",{className:"truncate text-[12px]",children:t.hook})]});const s=t.entry,l=s.type??"other",c=s.name??s.filename,a=t.hook||s.description||"";return e.jsx("li",{children:e.jsxs("button",{type:"button",onClick:()=>r(s.filename),className:`memo-row memo-type-${l}`,children:[e.jsx("span",{"aria-hidden":!0,className:"memo-row-dot"}),e.jsxs("div",{className:"memo-row-content",children:[e.jsxs("div",{className:"memo-row-headline",children:[e.jsx("span",{className:"memo-row-title",children:c}),s.mtime&&e.jsx("span",{className:"memo-row-meta-inline",children:C(s.mtime)}),e.jsx(A,{k:l})]}),a&&e.jsx("p",{className:"memo-row-hook",children:a})]}),e.jsx("span",{"aria-hidden":!0,className:"memo-row-chevron",children:"↗"})]})})}function A({k:t}){const r=y(),i=r(t==="user"?"memory.type.user":t==="feedback"?"memory.type.feedback":t==="project"?"memory.type.project":t==="reference"?"memory.type.reference":"memory.type.other");return e.jsx("span",{className:"memo-chip",children:i})}function ie({target:t,rawIndex:r,hookForEntry:i,onClose:s,onJumpToCover:l}){var o;const c=y(),a=u.useRef(null);u.useEffect(()=>{const m=f=>{f.key==="Escape"&&(f.preventDefault(),s())};window.addEventListener("keydown",m);const x=document.body.style.overflow;return document.body.style.overflow="hidden",requestAnimationFrame(()=>{var f;return(f=a.current)==null?void 0:f.focus()}),()=>{window.removeEventListener("keydown",m),document.body.style.overflow=x}},[s]);const d=t.kind==="entry"?((o=t.entry)==null?void 0:o.type)??"other":"reference";return e.jsxs(e.Fragment,{children:[e.jsx("div",{className:"memo-drawer-backdrop",onClick:s,"aria-hidden":!0}),e.jsxs("div",{ref:a,role:"dialog","aria-modal":"true",tabIndex:-1,className:`memo-drawer-panel memo-type-${d} focus:outline-none`,children:[e.jsxs("div",{className:"flex items-start justify-between gap-3 border-b border-[var(--color-hairline)] px-7 pt-7 pb-5",children:[e.jsx("div",{className:"min-w-0 flex-1",children:t.kind==="entry"?e.jsxs(e.Fragment,{children:[e.jsx(A,{k:d}),t.entry?e.jsxs(e.Fragment,{children:[e.jsx("h2",{className:"mt-3 font-display text-[24px] font-light leading-tight tracking-tight text-[var(--color-fg-primary)]",children:t.entry.name??t.entry.filename}),e.jsxs("p",{className:"mt-1.5 font-mono text-[11px] uppercase tracking-[0.16em] text-[var(--color-fg-faint)]",children:[t.entry.filename," · ",re(t.entry.bytes),t.entry.mtime?` · ${C(t.entry.mtime)}`:""]})]}):e.jsxs(e.Fragment,{children:[e.jsx("h2",{className:"mt-3 font-display text-[22px] font-light leading-tight text-[var(--color-fg-primary)]",children:c("memory.drawer.notFound.title")}),e.jsx("p",{className:"mt-1.5 font-mono text-[11px] uppercase tracking-[0.16em] text-[var(--color-fg-faint)]",children:t.filename})]})]}):e.jsxs(e.Fragment,{children:[e.jsx("span",{className:"memo-chip memo-type-reference",children:c("memory.cover.title")}),e.jsx("h2",{className:"mt-3 font-display text-[24px] font-light leading-tight tracking-tight text-[var(--color-fg-primary)]",children:c("memory.raw.title")}),e.jsx("p",{className:"mt-1.5 font-mono text-[11px] uppercase tracking-[0.16em] text-[var(--color-fg-faint)]",children:c("memory.raw.subtitle")})]})}),e.jsx("button",{type:"button",onClick:s,"aria-label":c("memory.drawer.close"),className:"rounded-md p-2 text-[var(--color-fg-muted)] hover:bg-[var(--color-sunken)] hover:text-[var(--color-fg-primary)]",children:e.jsx(pe,{})})]}),e.jsx("div",{className:"flex-1 overflow-y-auto px-7 py-6",children:t.kind==="entry"&&t.entry?e.jsx(le,{entry:t.entry,hookForEntry:i,onJumpToCover:l}):t.kind==="entry"?e.jsx(ce,{filename:t.filename,onJumpToCover:l}):e.jsx(me,{source:r})})]})]})}function le({entry:t,hookForEntry:r,onJumpToCover:i}){const s=y(),l=t.body.trim();return e.jsxs("div",{children:[t.description&&e.jsx("p",{className:"text-[14px] text-[var(--color-fg-secondary)]",children:t.description}),e.jsx("pre",{className:"whitespace-pre-wrap break-words rounded-[var(--radius-input)] border border-[var(--color-hairline)] bg-[var(--color-sunken)] px-4 py-3 font-mono text-[12.5px] leading-[1.65] text-[var(--color-fg-primary)] "+(t.description?"mt-5":""),children:l||"—"}),r&&e.jsxs("div",{className:"mt-7",children:[e.jsx("div",{className:"rule-dotted","aria-hidden":!0}),e.jsxs("div",{className:"mt-4 flex items-baseline justify-between gap-3",children:[e.jsx("div",{className:"eyebrow",children:s("memory.drawer.indexHook")}),e.jsxs("button",{type:"button",onClick:i,className:"font-mono text-[10.5px] uppercase tracking-[0.16em] text-[var(--color-fg-muted)] hover:text-[var(--color-accent-ink)] dark:hover:text-[var(--color-accent)]",children:[s("memory.cover.title")," ↑"]})]}),e.jsx("p",{className:"mt-1.5 text-[13px] text-[var(--color-fg-secondary)]",children:r})]})]})}function ce({filename:t,onJumpToCover:r}){const i=y();return e.jsxs("div",{children:[e.jsx("p",{className:"text-sm text-[var(--color-fg-secondary)]",children:i("memory.drawer.notFound.body")}),e.jsx("p",{className:"mt-3 font-mono text-[12px] text-[var(--color-fg-faint)]",children:t}),e.jsxs("button",{type:"button",onClick:r,className:"mt-5 inline-flex items-center gap-1.5 rounded-[var(--radius-control)] border border-[var(--color-hairline)] px-3 py-1.5 text-[11px] font-medium uppercase tracking-[0.14em] text-[var(--color-fg-secondary)] hover:border-[var(--color-hairline-strong)] hover:text-[var(--color-fg-primary)]",children:[i("memory.cover.title")," ↑"]})]})}function me({source:t}){const r=t.trim();return e.jsx("pre",{className:"whitespace-pre-wrap break-words rounded-[var(--radius-input)] border border-[var(--color-hairline)] bg-[var(--color-sunken)] px-4 py-3 font-mono text-[12.5px] leading-[1.65] text-[var(--color-fg-primary)]",children:r||"—"})}function de(){const t=y();return e.jsxs("div",{className:"surface-card mt-6 px-8 py-14 text-center",children:[e.jsx("div",{className:"mx-auto flex h-12 w-12 items-center justify-center rounded-full border border-[var(--color-hairline)] bg-[var(--color-sunken)] text-[var(--color-fg-muted)]",children:e.jsx(ue,{})}),e.jsx("h2",{className:"mt-5 font-display text-[20px] font-light tracking-tight text-[var(--color-fg-primary)]",children:t("memory.empty.title")}),e.jsx("p",{className:"mx-auto mt-3 max-w-xl text-[13px] leading-relaxed text-[var(--color-fg-secondary)]",children:t("memory.empty.body")})]})}function xe({className:t=""}){return e.jsxs("svg",{width:"14",height:"14",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"1.7",strokeLinecap:"round",className:t,"aria-hidden":!0,children:[e.jsx("circle",{cx:"11",cy:"11",r:"6.2"}),e.jsx("path",{d:"M20 20l-4.3-4.3"})]})}function pe(){return e.jsx("svg",{width:"16",height:"16",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"1.8",strokeLinecap:"round","aria-hidden":!0,children:e.jsx("path",{d:"M6 6l12 12M18 6L6 18"})})}function ue(){return e.jsx("svg",{width:"20",height:"20",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"1.6",strokeLinecap:"round",strokeLinejoin:"round","aria-hidden":!0,children:e.jsx("path",{d:"M6 4h12v17l-6-4-6 4z"})})}function z(t,r,i){return[t.name??"",t.filename,t.description??"",t.body,r].some(l=>l.toLowerCase().includes(i))}function he(t,r){const i=new Map(r.map(a=>[a.filename,a])),s=new Set,l=[];for(const a of t){if(a.link){const m=i.get(a.link.href);m?(l.push({kind:"entry",entry:m,hook:a.link.hook}),s.add(m.filename)):l.push({kind:"missing",title:a.link.title,hook:a.link.hook});continue}const d=a.raw.trim();if(!d){l.push({kind:"spacer"});continue}const o=/^(#{1,6})\s+(.+?)\s*$/.exec(d);o?l.push({kind:"heading",text:o[2]}):l.push({kind:"text",text:a.raw})}const c=r.filter(a=>!s.has(a.filename));return{rows:l,orphans:c}}function fe(t){var i,s,l;if(!t)return[];const r=[];for(const c of t.split(/\r?\n/)){const a=c.trimEnd();if(a.trim()===""){const x=r[r.length-1];x&&(x.link||x.raw.trim()!=="")&&r.push({raw:"",link:null});continue}const d=se.exec(a),o=(i=d==null?void 0:d.groups)==null?void 0:i.title,m=(s=d==null?void 0:d.groups)==null?void 0:s.href;d&&o&&m?r.push({raw:a,link:{title:o.trim(),href:m.trim(),hook:(((l=d.groups)==null?void 0:l.hook)??"").trim()}}):r.push({raw:a,link:null})}for(;r.length>0;){const c=r[r.length-1];if(c&&!c.link&&c.raw.trim()==="")r.pop();else break}return r}export{we as default};
|
|
2
|
+
//# sourceMappingURL=ProjectMemory-aSV8UzQ9.js.map
|