@loupekit/sdk 0.14.0 → 0.14.1-next.83
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/README.md +150 -257
- package/dist/index.global.js +10 -10
- package/dist/index.global.js.map +1 -1
- package/dist/index.js +10 -10
- package/dist/index.js.map +1 -1
- package/package.json +5 -2
package/README.md
CHANGED
|
@@ -1,72 +1,33 @@
|
|
|
1
|
-
|
|
1
|
+
# @loupekit/sdk
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
<img src="https://raw.githubusercontent.com/mohamed-ashraf-elsaed/loupe/main/docs/store/promo-marquee-1400x560.jpg" alt="Loupe — Pin feedback to the live UI. Hand it to Claude." width="100%" />
|
|
5
|
-
</a>
|
|
3
|
+
Add a visual feedback widget to any web page: reviewers pin comments to elements, and each comment carries the screenshot, element HTML and computed styles a developer or coding agent needs to make the fix.
|
|
6
4
|
|
|
7
|
-
|
|
5
|
+
[](https://www.npmjs.com/package/@loupekit/sdk)
|
|
6
|
+

|
|
8
7
|
|
|
9
|
-
|
|
10
|
-
Inspect any element on your live product, pin a comment to it, capture a screenshot —<br />
|
|
11
|
-
comments re-anchor across redeploys and flow to Claude Code as an actionable backlog.</p>
|
|
8
|
+

|
|
12
9
|
|
|
13
|
-
|
|
14
|
-
<a href="https://www.npmjs.com/package/@loupekit/sdk"><img src="https://img.shields.io/npm/v/@loupekit/sdk?color=4a55d6&label=npm" alt="npm version" /></a>
|
|
15
|
-
<a href="https://www.npmjs.com/package/@loupekit/sdk"><img src="https://img.shields.io/npm/dm/@loupekit/sdk?color=4a55d6" alt="npm downloads" /></a>
|
|
16
|
-
<img src="https://img.shields.io/npm/types/@loupekit/sdk?color=4a55d6" alt="TypeScript types" />
|
|
17
|
-
<img src="https://img.shields.io/npm/l/@loupekit/sdk?color=4a55d6" alt="MIT license" />
|
|
18
|
-
</p>
|
|
10
|
+
**Contents**
|
|
19
11
|
|
|
20
|
-
|
|
21
|
-
<a href="https://mohamed-ashraf-elsaed.github.io/loupe/"><b>Website</b></a> ·
|
|
22
|
-
<a href="https://mohamed-ashraf-elsaed.github.io/loupe/guide/"><b>Docs</b></a> ·
|
|
23
|
-
<a href="https://github.com/mohamed-ashraf-elsaed/loupe"><b>GitHub</b></a> ·
|
|
24
|
-
<a href="https://github.com/mohamed-ashraf-elsaed/loupe/blob/main/CHANGELOG.md"><b>Changelog</b></a> ·
|
|
25
|
-
<a href="https://www.npmjs.com/package/@loupekit/mcp"><b>MCP server</b></a>
|
|
26
|
-
</p>
|
|
27
|
-
|
|
28
|
-
</div>
|
|
29
|
-
|
|
30
|
-
---
|
|
31
|
-
|
|
32
|
-
## Overview
|
|
33
|
-
|
|
34
|
-
Traditional feedback — _"the revenue card looks off on the dashboard"_ — loses the one thing
|
|
35
|
-
an engineer needs: **which element, in what state, on which page.** Loupe captures all of it at
|
|
36
|
-
the moment of the comment, so the feedback stays actionable even after the UI changes underneath it.
|
|
37
|
-
|
|
38
|
-
<div align="center">
|
|
39
|
-
<img src="https://raw.githubusercontent.com/mohamed-ashraf-elsaed/loupe/main/docs/store/screenshot-1-inspect.jpg" alt="Pin a comment to any element" width="90%" />
|
|
40
|
-
</div>
|
|
41
|
-
|
|
42
|
-
## Table of contents
|
|
43
|
-
|
|
44
|
-
- [Features](#features)
|
|
12
|
+
- [Terms](#terms)
|
|
45
13
|
- [Install](#install)
|
|
14
|
+
- [Before you begin](#before-you-begin)
|
|
46
15
|
- [Quick start](#quick-start)
|
|
47
|
-
- [
|
|
48
|
-
- [
|
|
49
|
-
- [
|
|
50
|
-
- [
|
|
51
|
-
- [Storage
|
|
52
|
-
- [
|
|
53
|
-
- [
|
|
54
|
-
|
|
55
|
-
##
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
| ⏺ **Screen recording** | The **Record** tool drags the same box, then captures a screen video of it (via `getDisplayMedia` + canvas crop) as `.webm` — duration-capped with a Stop button. Recordings play back inline in the widget and the dashboard. |
|
|
63
|
-
| 🧲 **Dockable control** | A DevTools-style panel: dock it to the left / right / bottom edge (which pushes your page over so it's never covered) or float it as a movable, resizable window. Light/dark theme, collapses to a draggable `◎` launcher (one tap reopens the panel; a chevron expands the quick actions to pin a comment, drop a note, hide the markers or hide the launcher itself), and becomes a bottom sheet on mobile. Position, launcher position, theme and marker visibility persist. |
|
|
64
|
-
| 🕒 **Who and when** | Every thread shows its author and an absolute timestamp rendered in `init({ timeZone, locale })`, so one team reads one clock. The Home footer shows the bundle version and flags it when the host's `packageVersion` differs. |
|
|
65
|
-
| 🔁 **Redeploy-surviving re-anchoring** | A multi-signal fingerprint (stable id/testid, CSS path, XPath, text, attributes, position) re-locates the element on the current page; if it can't, the pin **detaches** instead of pointing at the wrong thing. |
|
|
66
|
-
| 📸 **Screenshot capture** | `[data-loupe-redact]` regions are painted over **before any pixels leave the browser**. |
|
|
67
|
-
| 🧩 **Shadow-DOM isolation** | The widget's CSS never leaks into your page and vice-versa. |
|
|
68
|
-
| 🔌 **Pluggable storage** | Talks to the Loupe backend, or persists to `localStorage` for offline/demo use. |
|
|
69
|
-
| 🤖 **Claude-ready** | Every comment carries the element HTML + computed styles + screenshot Claude Code needs to make the fix. |
|
|
16
|
+
- [Verify](#verify)
|
|
17
|
+
- [Troubleshooting](#troubleshooting)
|
|
18
|
+
- [Common options](#common-options)
|
|
19
|
+
- [Offline mode](#offline-mode)
|
|
20
|
+
- [Storage](#storage)
|
|
21
|
+
- [What reviewers get](#what-reviewers-get)
|
|
22
|
+
- [Links](#links)
|
|
23
|
+
|
|
24
|
+
## Terms
|
|
25
|
+
|
|
26
|
+
- **Panel:** the sidebar the widget docks to the edge of the page. It holds the Home, Comments, Activity and Chat tabs.
|
|
27
|
+
- **Launcher:** the floating button that opens the panel and the comment tools.
|
|
28
|
+
- **Pin:** a comment anchored to an element on the page.
|
|
29
|
+
- **Thread:** the replies under one comment.
|
|
30
|
+
- **Agent bridge:** a local HTTP service that `@loupekit/mcp` runs, by default on port 9800. It carries presence and chat between the panel and a coding agent.
|
|
70
31
|
|
|
71
32
|
## Install
|
|
72
33
|
|
|
@@ -74,254 +35,186 @@ the moment of the comment, so the feedback stays actionable even after the UI ch
|
|
|
74
35
|
npm i @loupekit/sdk
|
|
75
36
|
```
|
|
76
37
|
|
|
77
|
-
|
|
78
|
-
> `@mohamed-ashraf-elsaed:registry=https://npm.pkg.github.com` to your `.npmrc` to install from there.
|
|
79
|
-
|
|
80
|
-
## Quick start
|
|
38
|
+
The package is also mirrored to GitHub Packages as `@mohamed-ashraf-elsaed/sdk` (registry `https://npm.pkg.github.com`).
|
|
81
39
|
|
|
82
|
-
|
|
83
|
-
import { init } from "@loupekit/sdk";
|
|
40
|
+
The bundle has no runtime dependencies. It ships two builds:
|
|
84
41
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
// HMAC-SHA256(user.id, PROJECT_SECRET), computed on your server (see Auth model).
|
|
90
|
-
userHmac: "decb2c…",
|
|
91
|
-
});
|
|
92
|
-
```
|
|
42
|
+
| File | Format | Use it for |
|
|
43
|
+
| --- | --- | --- |
|
|
44
|
+
| `dist/index.js` | ES module | Bundlers and `import` |
|
|
45
|
+
| `dist/index.global.js` | IIFE, global `Loupe` | A plain `<script>` tag |
|
|
93
46
|
|
|
94
|
-
|
|
95
|
-
counts, the project manager, and the most recent feedback), **Comments** (with the
|
|
96
|
-
**Inspect**, **Note**, **Region**, and **Record** tools + the comment list) and
|
|
97
|
-
**Activity** (a live monitor fed by your backend's activity feed and by `trackActivity()`). The header carries a position menu
|
|
98
|
-
(left / bottom / right / float), a theme toggle, a settings dropdown — five accent colours, switches
|
|
99
|
-
for hover hints, markers and page paths, plus the running package version — and a minimize button
|
|
100
|
-
that collapses the panel to a one-line context bar. A five-step guided tour runs once on first open
|
|
101
|
-
(skippable, replayable from Settings), and each view shows a one-time hint card with a **Turn off
|
|
102
|
-
hints** link.
|
|
103
|
-
When the backend serves `GET v1/org`, the Home project chip reads "Project · Organization" and
|
|
104
|
-
the project menu lists the organization's other projects and where this project's tickets go.
|
|
105
|
-
The Chat page is off by default. Pass `chat: true` to `init()` to turn it on; it is experimental.
|
|
106
|
-
Use the header's dock controls to dock it left / right /
|
|
107
|
-
bottom (which pushes your page over) or float it, toggle light/dark, or close it to the `◎`
|
|
108
|
-
launcher. The launcher carries the comment count; one tap reopens the panel, and the chevron
|
|
109
|
-
beside it expands the quick actions (pin a comment, drop a note, hide the markers, hide the
|
|
110
|
-
launcher). Drag the launcher to move it anywhere — the spot is remembered, and **Reset launcher
|
|
111
|
-
position** in the Settings menu puts it back. Hide it from a quick action, the Settings menu,
|
|
112
|
-
`Alt+Shift+L`, or `hideLauncher()` / `showLauncher()` when it covers something on your page. On a
|
|
113
|
-
touch screen a slim tab at the right edge of the screen brings a hidden launcher back.
|
|
114
|
-
Call `destroy()` to tear it down. `init()` is idempotent — safe to call more
|
|
115
|
-
than once. Pass `label` to change the brand name shown in the header.
|
|
116
|
-
|
|
117
|
-
### Adding your own tab
|
|
118
|
-
|
|
119
|
-
The panel is extensible without forking it. Register as many sidebar pages as you like:
|
|
47
|
+
> **TypeScript:** version 0.14.1 ships no type declarations. `package.json` names `dist/index.d.ts`, but the build does not produce it, so TypeScript reports "Could not find a declaration file for module '@loupekit/sdk'". Until declarations ship, add a file such as `src/loupe.d.ts` that contains `declare module "@loupekit/sdk";`.
|
|
120
48
|
|
|
121
|
-
|
|
122
|
-
import { init, connectTab } from "@loupekit/sdk";
|
|
49
|
+
## Before you begin
|
|
123
50
|
|
|
124
|
-
|
|
125
|
-
projectKey: "pk_live_…",
|
|
126
|
-
user: { id: "u_1", name: "Ada" },
|
|
127
|
-
environments: ["https://staging.acme.test"],
|
|
128
|
-
tabs: [
|
|
129
|
-
connectTab(), // the Claude/MCP page, now opt-in
|
|
130
|
-
{
|
|
131
|
-
id: "build",
|
|
132
|
-
label: "Build",
|
|
133
|
-
hint: { title: "Build health", body: "Straight from our CI." },
|
|
134
|
-
render: (ctx) => `<p>Project <b>${ctx.projectKey}</b> — SDK v${ctx.version}</p>`,
|
|
135
|
-
},
|
|
136
|
-
],
|
|
137
|
-
});
|
|
138
|
-
```
|
|
51
|
+
The widget needs a backend to share comments between people. Pick one:
|
|
139
52
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
53
|
+
| Backend | What you need | Guide |
|
|
54
|
+
| --- | --- | --- |
|
|
55
|
+
| None (offline mode) | Nothing. Comments stay in the browser. | [Offline mode](#offline-mode) |
|
|
56
|
+
| Local server (`@loupekit/server`) | A clone of the repository and Node.js 24. Gives you `<API_BASE>`, `<PROJECT_KEY>` and `<PROJECT_SECRET>`. | [Run the local server](https://github.com/mohamed-ashraf-elsaed/loupe/blob/main/docs/how-to/run-local-server.md) |
|
|
57
|
+
| Laravel package (`loupekit/laravel`) | A Laravel app. Its `@loupeWidget` directive loads this SDK and calls `init()` for you, so you do not call it yourself. | [Install Loupe in a Laravel app](https://github.com/mohamed-ashraf-elsaed/loupe/blob/main/docs/how-to/laravel-install.md) |
|
|
58
|
+
| Loupe Hub | Hub routes tickets between Laravel apps. It is not a backend you pass to `init()`. | [Route tickets between apps](https://github.com/mohamed-ashraf-elsaed/loupe/blob/main/docs/how-to/hub-connect-apps.md) |
|
|
144
59
|
|
|
145
|
-
|
|
60
|
+
The **project secret** is the private key the backend issues with each project. The local server uses it to verify user identity and as the dashboard's admin key. Keep it on your server.
|
|
146
61
|
|
|
147
|
-
|
|
62
|
+
To start the local server with its demo project, run these commands from a clone of the repository:
|
|
148
63
|
|
|
149
|
-
```
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
trackActivity({ kind: "Bash", label: "pnpm test", detail: "exit 1", level: "error" });
|
|
64
|
+
```bash
|
|
65
|
+
npm install
|
|
66
|
+
npm run build
|
|
67
|
+
npm run seed
|
|
68
|
+
npm start
|
|
155
69
|
```
|
|
156
70
|
|
|
157
|
-
|
|
158
|
-
Laravel package does), the panel reads it on start and polls it every 15 seconds while the view is
|
|
159
|
-
open, and shows "Live" or "Offline" beside the status dot. With no feed and nothing connected the
|
|
160
|
-
view says *Monitor unavailable* and explains how to wire it up.
|
|
161
|
-
|
|
162
|
-
### Reviewing a change
|
|
71
|
+
You should see the seed print the demo project and its secret:
|
|
163
72
|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
// Via the API (or your own tooling) — the panel picks it up on the next load.
|
|
169
|
-
await fetch(`${apiBase}/v1/comments/${id}`, {
|
|
170
|
-
method: "PATCH",
|
|
171
|
-
headers: { "Content-Type": "application/json", "X-Loupe-Admin": secret },
|
|
172
|
-
body: JSON.stringify({
|
|
173
|
-
pr: { number: 412, url: "https://github.com/acme/web/pull/412", checksPassed: 3, checksTotal: 4 },
|
|
174
|
-
}),
|
|
175
|
-
});
|
|
73
|
+
```text
|
|
74
|
+
Seeded project: pk_demo_acme
|
|
75
|
+
admin key (dashboard ?key= / X-Loupe-Admin): sk_demo_acme_0f3b9c
|
|
76
|
+
demo HMAC (host-app-injected for u_92): <HMAC>
|
|
176
77
|
```
|
|
177
78
|
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
79
|
+
and the server report `[loupe] API + static on http://localhost:8787`. The demo values are:
|
|
80
|
+
|
|
81
|
+
- `<API_BASE>`: `http://localhost:8787`
|
|
82
|
+
- `<PROJECT_KEY>`: `pk_demo_acme`
|
|
83
|
+
- `<PROJECT_SECRET>`: `sk_demo_acme_0f3b9c`, or the value of `LOUPE_DEMO_SECRET` if you set it before `npm run seed`
|
|
84
|
+
- `<USER_HMAC>` for user `u_92`: the `demo HMAC` line
|
|
181
85
|
|
|
182
|
-
|
|
183
|
-
a person closes it from there.
|
|
86
|
+
## Quick start
|
|
184
87
|
|
|
185
|
-
|
|
88
|
+
Call `init()` once, after your app knows who the signed-in user is.
|
|
186
89
|
|
|
187
|
-
|
|
188
|
-
opacity comparison, undo, iteration history and a refine input. The panel owns all of
|
|
189
|
-
that; producing the markup is yours, so any model works:
|
|
90
|
+
### ES module
|
|
190
91
|
|
|
191
92
|
```ts
|
|
93
|
+
import { init } from "@loupekit/sdk";
|
|
94
|
+
|
|
192
95
|
init({
|
|
193
|
-
projectKey: "
|
|
194
|
-
user: { id: "
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
// configured in the panel (see below).
|
|
198
|
-
const { html, css, notes } = await myModel({ comment, prompt, kind, previous, localAi });
|
|
199
|
-
return { html, css, notes };
|
|
200
|
-
},
|
|
96
|
+
projectKey: "<PROJECT_KEY>",
|
|
97
|
+
user: { id: "u_92", name: "Sara", email: "sara@acme.com" },
|
|
98
|
+
apiBase: "<API_BASE>",
|
|
99
|
+
userHmac: "<USER_HMAC>",
|
|
201
100
|
});
|
|
202
101
|
```
|
|
203
102
|
|
|
204
|
-
|
|
205
|
-
page. A local-AI endpoint and model (any OpenAI-compatible server — Ollama, llama.cpp…)
|
|
206
|
-
are configurable in the project manager, with a real connection check. Without a
|
|
207
|
-
`generate` function the pane offers *Request access to generate* and hands it to
|
|
208
|
-
`init({ onRequestAccess })`.
|
|
103
|
+
### Script tag
|
|
209
104
|
|
|
210
|
-
|
|
105
|
+
1. Copy the IIFE build into your public assets folder:
|
|
211
106
|
|
|
212
|
-
|
|
213
|
-
|
|
107
|
+
```bash
|
|
108
|
+
cp node_modules/@loupekit/sdk/dist/index.global.js <PUBLIC_DIR>/loupe.js
|
|
109
|
+
```
|
|
214
110
|
|
|
215
|
-
|
|
216
|
-
import { requestNavigation } from "@loupekit/sdk";
|
|
111
|
+
You should see `loupe.js` in `<PUBLIC_DIR>`. The package `exports` map exposes only the ES module, so copy the file rather than importing it by path.
|
|
217
112
|
|
|
218
|
-
|
|
219
|
-
reason: "The fix is live on the preview URL.",
|
|
220
|
-
requester: "Claude Code",
|
|
221
|
-
});
|
|
222
|
-
```
|
|
113
|
+
2. Load it and call `Loupe.init()`:
|
|
223
114
|
|
|
224
|
-
|
|
225
|
-
|
|
115
|
+
```html
|
|
116
|
+
<script src="/<PATH_TO>/loupe.js"></script>
|
|
117
|
+
<script>
|
|
118
|
+
Loupe.init({
|
|
119
|
+
projectKey: "<PROJECT_KEY>",
|
|
120
|
+
user: { id: "u_92", name: "Sara", email: "sara@acme.com" },
|
|
121
|
+
apiBase: "<API_BASE>",
|
|
122
|
+
userHmac: "<USER_HMAC>",
|
|
123
|
+
});
|
|
124
|
+
</script>
|
|
125
|
+
```
|
|
226
126
|
|
|
227
|
-
###
|
|
127
|
+
### Placeholders
|
|
228
128
|
|
|
229
|
-
|
|
129
|
+
- `<PROJECT_KEY>`: the public project key your backend issued, for example `pk_demo_acme` from the local demo server.
|
|
130
|
+
- `<API_BASE>`: the base URL of your Loupe backend, for example `https://tracker.example.com`. Omit it to run in [offline mode](#offline-mode).
|
|
131
|
+
- `<USER_HMAC>`: `HMAC-SHA256(user.id, <PROJECT_SECRET>)` as hex, computed on your server. Never compute it in the browser, because that exposes the project secret.
|
|
132
|
+
- `<PUBLIC_DIR>`: the folder your web server serves static files from.
|
|
133
|
+
- `<PATH_TO>`: the URL path where that folder is served.
|
|
230
134
|
|
|
231
|
-
|
|
232
|
-
|
|
135
|
+
### Compute the user HMAC on your server
|
|
136
|
+
|
|
137
|
+
The local server verifies `X-Loupe-Hmac` against `HMAC-SHA256(user.id, project secret)` and rejects a mismatch. In Node.js:
|
|
138
|
+
|
|
139
|
+
```js
|
|
140
|
+
import { createHmac } from "node:crypto";
|
|
141
|
+
|
|
142
|
+
const userHmac = createHmac("sha256", process.env.LOUPE_PROJECT_SECRET)
|
|
143
|
+
.update(user.id)
|
|
144
|
+
.digest("hex");
|
|
233
145
|
```
|
|
234
146
|
|
|
235
|
-
|
|
147
|
+
Pass `userHmac` to the page that calls `init()`.
|
|
236
148
|
|
|
237
|
-
|
|
238
|
-
page load — even after the UI is rebuilt, relabeled, or reordered — Loupe re-locates the
|
|
239
|
-
element and moves the pin to it. Below, the "Revenue" card was relabeled to "Total Revenue"
|
|
240
|
-
and reordered, yet the pin follows it:
|
|
149
|
+
## Verify
|
|
241
150
|
|
|
242
|
-
|
|
243
|
-
<tr>
|
|
244
|
-
<td width="50%" align="center"><b>Before redeploy</b><br /><img src="https://raw.githubusercontent.com/mohamed-ashraf-elsaed/loupe/main/docs/before-redeploy.png" alt="Comment pinned to the revenue card" /></td>
|
|
245
|
-
<td width="50%" align="center"><b>After redeploy</b><br /><img src="https://raw.githubusercontent.com/mohamed-ashraf-elsaed/loupe/main/docs/after-redeploy.png" alt="Pin re-anchored after the layout changed" /></td>
|
|
246
|
-
</tr>
|
|
247
|
-
</table>
|
|
151
|
+
Reload the page. You should see the Loupe panel docked on the right edge of the page, open on the Home tab. On the first open on a desktop browser, a five-step tour runs once.
|
|
248
152
|
|
|
249
|
-
|
|
153
|
+
`init()` does nothing on a second call. Call `destroy()` to remove the widget and stop all its timers.
|
|
250
154
|
|
|
251
|
-
|
|
155
|
+
## Troubleshooting
|
|
252
156
|
|
|
253
|
-
|
|
|
157
|
+
| Symptom | Cause | Fix |
|
|
254
158
|
| --- | --- | --- |
|
|
255
|
-
| `projectKey` | `
|
|
256
|
-
| `user` | `
|
|
257
|
-
| `
|
|
258
|
-
| `
|
|
259
|
-
|
|
|
260
|
-
| `
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
| `timeZone` | `string` | IANA time zone every timestamp is rendered in (e.g. `"Africa/Cairo"`). Defaults to the browser's. |
|
|
266
|
-
| `locale` | `string` | BCP 47 locale for dates (e.g. `"en-GB"` for day-first). Defaults to the browser's. |
|
|
267
|
-
| `packageVersion` | `string` | Version of the host package that served this bundle (`Loupe.version` is the bundle's own). Flagged in the widget when the two differ. |
|
|
268
|
-
| `chat` | `boolean` | Turn on the experimental Chat page. Off by default: the tab shows dimmed and does not open. |
|
|
269
|
-
|
|
270
|
-
## Redaction
|
|
271
|
-
|
|
272
|
-
Any element marked `data-loupe-redact` is painted over in screenshots **before the pixels
|
|
273
|
-
ever leave the browser** — use it on PII, secrets, or anything sensitive:
|
|
274
|
-
|
|
275
|
-
```html
|
|
276
|
-
<input data-loupe-redact value="secret.person@acme.com" />
|
|
277
|
-
```
|
|
159
|
+
| Console shows `[loupe] init requires a projectKey` | `projectKey` is missing. | Pass `projectKey`. |
|
|
160
|
+
| Console shows `[loupe] init requires user.id` | `user.id` is missing. | Pass `user` with an `id`. |
|
|
161
|
+
| Requests to `/v1/comments` return `401` with `invalid or missing credentials` | `userHmac` is missing, or was signed with another secret or another user id. | Recompute it on the server from the exact `user.id` you pass to `init()` and the project's secret. |
|
|
162
|
+
| Requests return `404` with `unknown project` | No project has that key on the backend. | Check `projectKey`, or run `npm run seed` for the demo project. |
|
|
163
|
+
| Comments never leave the browser | `apiBase` is not set, so the widget is in offline mode. | Set `apiBase` to your backend URL. |
|
|
164
|
+
| A preflight (`OPTIONS`) request fails with a CORS error against the local server | You added a header with the `headers` option. The local server allows only `Content-Type`, `X-Loupe-User`, `X-Loupe-Hmac`, `X-Loupe-Admin` and `X-Loupe-Project`. | Remove the extra header, or use a backend that allows it. |
|
|
165
|
+
|
|
166
|
+
## Common options
|
|
167
|
+
|
|
168
|
+
SDK options are passed to `init()`. None of them is read from an environment variable.
|
|
278
169
|
|
|
279
|
-
|
|
170
|
+
| Option | Type | Default | Env var | Description |
|
|
171
|
+
| --- | --- | --- | --- | --- |
|
|
172
|
+
| `projectKey` | `string` | required | none | Public project key issued by the backend. |
|
|
173
|
+
| `user` | `{ id: string; name: string; email?: string }` | required | none | The signed-in user of the host app. |
|
|
174
|
+
| `userHmac` | `string` | unset | none | `HMAC-SHA256(user.id, <PROJECT_SECRET>)`, computed server-side. Sent as the `X-Loupe-Hmac` header when set. |
|
|
175
|
+
| `apiBase` | `string` | unset | none | Backend base URL. Without it, comments are stored in `localStorage`. |
|
|
176
|
+
| `bridge` | `string` | unset | none | Base URL of the agent bridge, for example `http://127.0.0.1:9800`. Without it, the peer list is hidden and live presence is off. |
|
|
177
|
+
| `credentials` | `RequestCredentials` | unset (`fetch` then uses `"same-origin"`) | none | Passed to every `fetch`. Set `"include"` for cross-origin cookie auth. |
|
|
178
|
+
| `headers` | `Record<string, string>` | unset | none | Extra headers merged into every backend request, for example `{ "X-CSRF-TOKEN": "…" }`. |
|
|
179
|
+
| `timeZone` | `string` | unset (the browser's zone) | none | IANA zone for timestamps, for example `"Europe/London"`. An unknown zone falls back to the browser's, with one console warning. |
|
|
180
|
+
| `locale` | `string` | unset (the browser's locale) | none | BCP 47 locale for dates, for example `"en-GB"`. An unknown locale falls back to the browser's, with one console warning. |
|
|
181
|
+
| `chat` | `boolean` | `false` | none | Shows the experimental Chat tab. When `false`, the tab is dimmed and cannot be opened. |
|
|
182
|
+
| `tabs` | `LoupeTab[]` | unset | none | Extra panel tabs, shown after the built-in ones. `connectTab()` returns one. |
|
|
280
183
|
|
|
281
|
-
|
|
282
|
-
(`= HMAC-SHA256(userId, PROJECT_SECRET)`), which your server computes and injects into the
|
|
283
|
-
page — users can't spoof identity. The dashboard and MCP server authenticate as admin with
|
|
284
|
-
the raw secret.
|
|
184
|
+
For every option, including `autoOpen`, `tool`, `label` and `environments`, see the [SDK reference](https://github.com/mohamed-ashraf-elsaed/loupe/blob/main/docs/reference/sdk.md).
|
|
285
185
|
|
|
286
|
-
##
|
|
186
|
+
## Offline mode
|
|
287
187
|
|
|
288
|
-
|
|
188
|
+
When you omit `apiBase`, the widget stores comments, replies and reactions in the browser's `localStorage`, under keys that start with `loupe:`. Nothing leaves the browser, so offline mode suits demos and local development. Comments are visible only in that browser, there are no notifications, and each attachment is limited to 3,000,000 bytes.
|
|
289
189
|
|
|
290
190
|
```ts
|
|
291
|
-
|
|
292
|
-
list(projectKey: string, url: string): Promise<Comment[]>;
|
|
293
|
-
save(comment: Comment): Promise<Comment>;
|
|
294
|
-
update(id: string, patch: Partial<Comment>): Promise<void>;
|
|
295
|
-
remove(id: string): Promise<void>;
|
|
296
|
-
// Optional. Return null when the backend has no such endpoint.
|
|
297
|
-
getOrg?(): Promise<OrgInfo | null>;
|
|
298
|
-
listActivity?(projectKey: string, since?: string): Promise<ActivityEvent[] | null>;
|
|
299
|
-
}
|
|
191
|
+
init({ projectKey: "pk_demo", user: { id: "u_1", name: "Sara" } });
|
|
300
192
|
```
|
|
301
193
|
|
|
302
|
-
##
|
|
194
|
+
## Storage
|
|
303
195
|
|
|
304
|
-
|
|
305
|
-
**and** **MCP server** (Claude reads it) → status flows back.
|
|
196
|
+
`apiBase` chooses the storage: with it, the widget talks to your backend over HTTP; without it, the widget uses `localStorage`. You cannot pass your own storage adapter to `init()`.
|
|
306
197
|
|
|
307
|
-
|
|
308
|
-
<tr>
|
|
309
|
-
<td width="50%" align="center"><b>Triage board (dashboard)</b><br /><img src="https://raw.githubusercontent.com/mohamed-ashraf-elsaed/loupe/main/docs/store/screenshot-2-board.jpg" alt="Kanban triage board" /></td>
|
|
310
|
-
<td width="50%" align="center"><b>Claude Code reads it via MCP</b><br /><img src="https://raw.githubusercontent.com/mohamed-ashraf-elsaed/loupe/main/docs/store/screenshot-3-claude.jpg" alt="Claude Code reading comments through MCP" /></td>
|
|
311
|
-
</tr>
|
|
312
|
-
</table>
|
|
198
|
+
## What reviewers get
|
|
313
199
|
|
|
314
|
-
|
|
200
|
+
- **Four built-in tabs:** Home, Comments, Activity and Chat. Chat is experimental and stays dimmed until you pass `chat: true`. The `tabs` option and `connectTab()` add more.
|
|
201
|
+
- **Tools:** Inspect (pin a comment to an element), Note (a page-level note), Region (drag a box) and Record (a short screen recording of a region, capped at 20 seconds). A region anchors to the smallest element that covers at least 60% of it.
|
|
202
|
+
- **Threads:** replies with file attachments, `@mentions` with autocomplete, and reactions (👍 🎉 👀 🙏 ❤️ 🚀).
|
|
203
|
+
- **Sync:** the panel refreshes comments and open threads every 10 seconds while the tab is visible and nobody is typing in the panel.
|
|
204
|
+
- **Home tiles:** Open, Needs you, Resolved and Stale (open for more than 7 days). Click a tile to filter the Comments tab.
|
|
205
|
+
- **Launcher:** drag it anywhere. Its chevron opens quick actions: Pin comment, Note, Markers and Hide launcher. Press `Alt+Shift+L` to hide or show it.
|
|
206
|
+
- **Re-anchoring:** pins follow their element across redeploys. A pin that cannot be matched detaches and shows a "moved" badge instead of pointing at the wrong element.
|
|
207
|
+
- **Redaction:** with the built-in capture, elements marked `data-loupe-redact` are left out of element screenshots and painted over in region screenshots, before upload. A `captureScreenshot` or `captureRegion` override, and screen recordings, are not redacted.
|
|
315
208
|
|
|
316
|
-
|
|
317
|
-
| --- | --- |
|
|
318
|
-
| [`@loupekit/mcp`](https://www.npmjs.com/package/@loupekit/mcp) | MCP server that hands the comments to Claude Code. |
|
|
319
|
-
| [`@loupekit/shared`](https://www.npmjs.com/package/@loupekit/shared) | The canonical TypeScript types shared across the platform. |
|
|
209
|
+
For a walkthrough of each feature, see [Use the widget](https://github.com/mohamed-ashraf-elsaed/loupe/blob/main/docs/how-to/use-the-widget.md).
|
|
320
210
|
|
|
321
|
-
##
|
|
211
|
+
## Links
|
|
322
212
|
|
|
323
|
-
|
|
324
|
-
|
|
213
|
+
- [Documentation](https://github.com/mohamed-ashraf-elsaed/loupe/tree/main/docs)
|
|
214
|
+
- [Guide](https://mohamed-ashraf-elsaed.github.io/loupe/guide/)
|
|
215
|
+
- [Changelog](https://github.com/mohamed-ashraf-elsaed/loupe/blob/main/CHANGELOG.md)
|
|
216
|
+
- [Issues](https://github.com/mohamed-ashraf-elsaed/loupe/issues)
|
|
217
|
+
- [`@loupekit/mcp`](https://www.npmjs.com/package/@loupekit/mcp): the MCP server that hands comments to a coding agent
|
|
325
218
|
|
|
326
219
|
## License
|
|
327
220
|
|
package/dist/index.global.js
CHANGED
|
@@ -4007,7 +4007,7 @@ a.fwdchip { text-decoration: none; cursor: pointer; }
|
|
|
4007
4007
|
};
|
|
4008
4008
|
|
|
4009
4009
|
// src/app.ts
|
|
4010
|
-
var SDK_VERSION = true ? "0.14.
|
|
4010
|
+
var SDK_VERSION = true ? "0.14.1" : "dev";
|
|
4011
4011
|
var FAB_SIZE = 46;
|
|
4012
4012
|
var FAB_DRAG_THRESHOLD = 6;
|
|
4013
4013
|
var LAUNCHER_SHORTCUT = "Alt+Shift+L";
|
|
@@ -4026,7 +4026,7 @@ a.fwdchip { text-decoration: none; cursor: pointer; }
|
|
|
4026
4026
|
sel: ".hstat",
|
|
4027
4027
|
tab: "home",
|
|
4028
4028
|
title: "Home shows what needs you",
|
|
4029
|
-
body: "Four tiles count
|
|
4029
|
+
body: "Four tiles count Open, Needs you, Resolved and Stale feedback. Click one to narrow the list to that bucket."
|
|
4030
4030
|
},
|
|
4031
4031
|
{
|
|
4032
4032
|
sel: ".hscope",
|
|
@@ -4038,13 +4038,13 @@ a.fwdchip { text-decoration: none; cursor: pointer; }
|
|
|
4038
4038
|
sel: ".tools",
|
|
4039
4039
|
tab: "comments",
|
|
4040
4040
|
title: "Pin feedback anywhere",
|
|
4041
|
-
body: "Inspect picks an element, Note
|
|
4041
|
+
body: "Inspect picks an element, Note comments anywhere on the page, Region screenshots a rectangle, and Record captures video of one."
|
|
4042
4042
|
},
|
|
4043
4043
|
{
|
|
4044
4044
|
sel: '.tabs [data-tab="activity"]',
|
|
4045
4045
|
tab: "activity",
|
|
4046
4046
|
title: "Watch the work happen",
|
|
4047
|
-
body: "Comments, status changes and forwarded tickets land here as they happen, with anything
|
|
4047
|
+
body: "Comments, status changes and forwarded tickets land here as they happen, with anything the local MCP bridge or your app reports. Tool chips filter it."
|
|
4048
4048
|
},
|
|
4049
4049
|
{
|
|
4050
4050
|
sel: '.dctl [data-role="settings"]',
|
|
@@ -4056,11 +4056,11 @@ a.fwdchip { text-decoration: none; cursor: pointer; }
|
|
|
4056
4056
|
var HINTS = {
|
|
4057
4057
|
chat: {
|
|
4058
4058
|
title: "Talk to the agent",
|
|
4059
|
-
body: "Gather what you are looking at and send it in one go. The agent gets it on its
|
|
4059
|
+
body: "Gather what you are looking at and send it in one go. The agent gets it on its next step, not after it finishes."
|
|
4060
4060
|
},
|
|
4061
4061
|
home: { title: "Your triage at a glance", body: "The tiles count this page by default. Switch to All for the whole project, or click a tile to jump straight to that bucket." },
|
|
4062
|
-
comments: { title: "Pin, note or record", body: "Inspect
|
|
4063
|
-
activity: { title: "Watch the work happen", body: "Every event the bridge or your app reports lands here, alongside Loupe's own operations. Click a tool chip to filter the feed." }
|
|
4062
|
+
comments: { title: "Pin, note or record", body: "Inspect picks an element, Note comments anywhere on the page, Region screenshots a rectangle, and Record captures video of one." },
|
|
4063
|
+
activity: { title: "Watch the work happen", body: "Every event the local MCP bridge or your app reports lands here, alongside Loupe's own operations. Click a tool chip to filter the feed." }
|
|
4064
4064
|
};
|
|
4065
4065
|
var BUILTIN_TABS = [
|
|
4066
4066
|
{ id: "home", label: "Home" },
|
|
@@ -4633,7 +4633,7 @@ a.fwdchip { text-decoration: none; cursor: pointer; }
|
|
|
4633
4633
|
/**
|
|
4634
4634
|
* Keep the panel current without a reload.
|
|
4635
4635
|
*
|
|
4636
|
-
* A status moved in another app (
|
|
4636
|
+
* A status moved in another app (a tracker approving a ticket), a reply relayed through Hub,
|
|
4637
4637
|
* or a teammate's new pin otherwise appears only after the page reloads. The bridge's
|
|
4638
4638
|
* SSE covers threads when one is configured; this covers everything else, for every
|
|
4639
4639
|
* host. It pauses while the tab is hidden and catches up the moment it is shown again.
|
|
@@ -7827,8 +7827,8 @@ ${c.body}` : c.body)}</div>` + (c.context?.html ? `<pre class="or-code">${escape
|
|
|
7827
7827
|
* Follow the bridge's thread channel while the panel is open.
|
|
7828
7828
|
*
|
|
7829
7829
|
* The SSE channel lives in the MCP process, so a reply posted by anyone — an agent, a
|
|
7830
|
-
* teammate's browser — arrives here rather than being discovered by the
|
|
7831
|
-
* poll. Only the threads whose messages are already loaded are refetched: pulling a
|
|
7830
|
+
* teammate's browser — arrives here rather than being discovered by the 10 s sync
|
|
7831
|
+
* poll (SYNC_POLL_MS). Only the threads whose messages are already loaded are refetched: pulling a
|
|
7832
7832
|
* conversation nobody has open would be work for nothing.
|
|
7833
7833
|
*/
|
|
7834
7834
|
startLiveThreads() {
|