@reticlehq/browser 2.8.0 → 2.9.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/NOTICE +7 -0
- package/dist/actions/actions-dom.js +23 -15
- package/dist/actions/actions.d.ts +17 -0
- package/dist/actions/actions.js +132 -29
- package/dist/actions/synthetic-input.d.ts +9 -0
- package/dist/actions/synthetic-input.js +40 -0
- package/dist/commands/commands.js +11 -1
- package/dist/connect-options.d.ts +9 -9
- package/dist/dom/a11y.d.ts +9 -0
- package/dist/dom/a11y.js +19 -0
- package/dist/dom/dom-ignore.d.ts +19 -6
- package/dist/dom/dom-ignore.js +20 -13
- package/dist/dom/query.d.ts +2 -2
- package/dist/dom/query.js +85 -45
- package/dist/dom/snapshot.d.ts +5 -0
- package/dist/dom/snapshot.js +34 -0
- package/dist/dom/source.d.ts +14 -0
- package/dist/dom/source.js +16 -0
- package/dist/observers/install-all.js +4 -1
- package/dist/observers/ipc.d.ts +16 -0
- package/dist/observers/ipc.js +19 -1
- package/dist/observers/network.d.ts +8 -0
- package/dist/observers/network.js +24 -3
- package/dist/observers/state.js +21 -2
- package/dist/presenter/presenter-brand.d.ts +24 -7
- package/dist/presenter/presenter-brand.js +23 -6
- package/dist/presenter/presenter-config.d.ts +102 -9
- package/dist/presenter/presenter-config.js +94 -8
- package/dist/presenter/presenter-controls.d.ts +10 -10
- package/dist/presenter/presenter-controls.js +175 -85
- package/dist/presenter/presenter-dock-layout.d.ts +6 -0
- package/dist/presenter/presenter-dock-layout.js +170 -0
- package/dist/presenter/presenter-drag.d.ts +48 -0
- package/dist/presenter/presenter-drag.js +198 -0
- package/dist/presenter/presenter-effects.d.ts +1 -1
- package/dist/presenter/presenter-effects.js +1 -1
- package/dist/presenter/presenter-glow.d.ts +5 -5
- package/dist/presenter/presenter-glow.js +7 -7
- package/dist/presenter/presenter-heroicons-data.d.ts +41 -0
- package/dist/presenter/presenter-heroicons-data.js +38 -0
- package/dist/presenter/presenter-hud-chrome.d.ts +17 -0
- package/dist/presenter/presenter-hud-chrome.js +41 -0
- package/dist/presenter/presenter-icons.d.ts +44 -0
- package/dist/presenter/presenter-icons.js +100 -0
- package/dist/presenter/presenter-log.d.ts +27 -5
- package/dist/presenter/presenter-log.js +114 -28
- package/dist/presenter/presenter-report-copy.d.ts +83 -0
- package/dist/presenter/presenter-report-copy.js +127 -0
- package/dist/presenter/presenter-report-styles.d.ts +7 -0
- package/dist/presenter/presenter-report-styles.js +102 -0
- package/dist/presenter/presenter-report.d.ts +23 -0
- package/dist/presenter/presenter-report.js +217 -0
- package/dist/presenter/presenter-run-state.d.ts +1 -1
- package/dist/presenter/presenter-settings-styles.d.ts +2 -0
- package/dist/presenter/presenter-settings-styles.js +153 -0
- package/dist/presenter/presenter-settings.d.ts +82 -0
- package/dist/presenter/presenter-settings.js +494 -0
- package/dist/presenter/presenter-shell-styles.d.ts +1 -0
- package/dist/presenter/presenter-shell-styles.js +375 -0
- package/dist/presenter/presenter-shell.d.ts +48 -0
- package/dist/presenter/presenter-shell.js +408 -0
- package/dist/presenter/presenter-styles.d.ts +9 -4
- package/dist/presenter/presenter-styles.js +61 -125
- package/dist/presenter/presenter-tally.d.ts +8 -11
- package/dist/presenter/presenter-tally.js +48 -23
- package/dist/presenter/presenter-test-helpers.d.ts +1 -1
- package/dist/presenter/presenter-test-helpers.js +1 -1
- package/dist/presenter/presenter-verbs.js +2 -2
- package/dist/presenter/presenter-workspace.d.ts +15 -0
- package/dist/presenter/presenter-workspace.js +180 -0
- package/dist/presenter/presenter.d.ts +15 -10
- package/dist/presenter/presenter.js +194 -46
- package/dist/reticle.d.ts +2 -2
- package/dist/reticle.js +23 -19
- package/dist/review/annotator-styles.d.ts +6 -0
- package/dist/review/annotator-styles.js +81 -0
- package/dist/review/annotator.d.ts +29 -17
- package/dist/review/annotator.js +477 -146
- package/package.json +7 -4
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { PresenterMode } from '@reticlehq/core';
|
|
2
|
+
import { nativeSetTimeout } from '../timers/native-timers.js';
|
|
3
|
+
import { PresenterIcon, PRESENTER_ICON_SIZE, hiIcon, } from './presenter-icons.js';
|
|
2
4
|
// Activity-log UI for the presenter HUD: a persistent, timestamped, scrollable transcript of
|
|
3
|
-
// every read/act/narration. All strings here are presenter-only UI (chips, glyphs, attrs)
|
|
5
|
+
// every read/act/narration. All strings here are presenter-only UI (chips, glyphs, attrs) - they
|
|
4
6
|
// never cross the browser↔bridge↔agent wire, so they stay as named consts (not protocol consts).
|
|
5
7
|
// All nodes carry data-reticle-* attrs so they're excluded from snapshots (see dom-ignore.ts).
|
|
6
8
|
/** Default cap on accumulated activity-log rows (bounds DOM). Presenter-local UI tunable. */
|
|
@@ -12,12 +14,14 @@ export const LOG_KIND = {
|
|
|
12
14
|
NARRATION: 'narration',
|
|
13
15
|
HUMAN: 'human',
|
|
14
16
|
};
|
|
15
|
-
/** Prefix for a human-authored activity row ("🧑 you: <text>"). Presenter-only UI. */
|
|
16
|
-
export const HUMAN_ROW_PREFIX = '🧑 you: ';
|
|
17
17
|
/** Act-row outcome glyph keys (presenter-only UI). */
|
|
18
18
|
export const LOG_RESULT = { PASS: 'pass', FAIL: 'fail' };
|
|
19
19
|
const LOG_CHIP = { read: 'READ', act: 'ACT', narration: '', human: '' };
|
|
20
|
-
|
|
20
|
+
const LOG_CHIP_ICON = {
|
|
21
|
+
read: PresenterIcon.VIEW,
|
|
22
|
+
act: PresenterIcon.POINTER,
|
|
23
|
+
};
|
|
24
|
+
/** HUD chip copy keyed by presenter mode (UI text, browser-local - not a wire string). */
|
|
21
25
|
export const CHIP_LABEL = {
|
|
22
26
|
[PresenterMode.IDLE]: '',
|
|
23
27
|
[PresenterMode.READING]: 'READING',
|
|
@@ -30,33 +34,90 @@ const LOG_CHIP_MODE = {
|
|
|
30
34
|
narration: PresenterMode.IDLE,
|
|
31
35
|
human: PresenterMode.IDLE,
|
|
32
36
|
};
|
|
33
|
-
const RESULT_GLYPH = { pass: '
|
|
37
|
+
const RESULT_GLYPH = { pass: '', fail: 'Fail' };
|
|
34
38
|
const RESULT_CLASS = { pass: 'reticle-pass', fail: 'reticle-fail' };
|
|
35
39
|
export const DATA_RETICLE_LOG = 'data-reticle-log';
|
|
36
40
|
const DATA_RETICLE_LOG_ROW = 'data-reticle-log-row';
|
|
37
|
-
|
|
41
|
+
/**
|
|
42
|
+
* The attribute on ONE log line's timestamp. Deliberately not `data-reticle-log-ts`: that name is
|
|
43
|
+
* the overlay-level SETTING (show timestamps y/n) and the overlay root carries it, so a bare
|
|
44
|
+
* `[data-reticle-log-ts]{opacity:.85}` here matched the overlay itself and made the entire HUD -
|
|
45
|
+
* chat panel included - 85% transparent, with the page legible straight through it.
|
|
46
|
+
*/
|
|
47
|
+
export const LOG_TIME_ATTR = 'data-reticle-log-time';
|
|
38
48
|
const DATA_KIND = 'data-kind';
|
|
39
49
|
const LOG_TEXT_CLASS = 'reticle-log-text';
|
|
40
50
|
const LOG_RES_CLASS = 'reticle-res';
|
|
41
51
|
const LOG_CHIP_CLASS = 'reticle-chip';
|
|
52
|
+
const LOG_EMPTY_HINT = 'Agent activity will appear here';
|
|
53
|
+
/** How long to keep re-pinning the feed after the panel opens, while rows render their real size. */
|
|
54
|
+
const LOG_SETTLE_MS = 160;
|
|
42
55
|
/** CSS for the log feed (injected with the rest of the presenter stylesheet; vars inherit from the card). */
|
|
43
56
|
export const LOG_CSS = `
|
|
44
|
-
[data-reticle-log]{flex:1;min-height:0;overflow-y:auto;overscroll-behavior:contain;
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
57
|
+
[data-reticle-log]{flex:1;min-height:0;overflow-y:auto;overscroll-behavior:contain;pointer-events:auto;touch-action:pan-y;
|
|
58
|
+
display:flex;flex-direction:column;
|
|
59
|
+
gap:4px;padding:8px 10px 10px;scrollbar-width:thin;scrollbar-color:rgba(255,255,255,.16) transparent;
|
|
60
|
+
background:transparent;}
|
|
61
|
+
[data-reticle-log]:empty{align-items:center;justify-content:center;}
|
|
62
|
+
[data-reticle-log]:empty::after{content:"${LOG_EMPTY_HINT}";display:block;padding:24px 16px;text-align:center;
|
|
63
|
+
color:var(--reticle-faint);font-size:11.5px;line-height:1.5;letter-spacing:.01em;}
|
|
64
|
+
[data-reticle-log]::-webkit-scrollbar{width:8px;}
|
|
65
|
+
[data-reticle-log]::-webkit-scrollbar-thumb{background:rgba(255,255,255,.14);border-radius:8px;border:2px solid transparent;background-clip:content-box;}
|
|
48
66
|
[data-reticle-log]::-webkit-scrollbar-thumb:hover{background:rgba(255,255,255,.26);background-clip:content-box;}
|
|
49
|
-
[data-reticle-log-row]{display:flex;align-items:
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
[data-reticle-log-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
67
|
+
[data-reticle-log-row]{display:flex;align-items:flex-start;gap:7px;font-size:11.5px;line-height:1.45;
|
|
68
|
+
padding:4px 2px;background:transparent;border:none;border-radius:0;
|
|
69
|
+
content-visibility:auto;contain-intrinsic-size:auto 28px;}
|
|
70
|
+
[data-reticle-log-row][data-kind="narration"]{padding:6px 2px;color:var(--reticle-muted);font-size:11px;font-style:italic;}
|
|
71
|
+
/**
|
|
72
|
+
* The kind marker is an ICON, not a pill.
|
|
73
|
+
*
|
|
74
|
+
* A row is "what the agent did"; wrapping READ / ACT in an uppercase capsule made the label louder
|
|
75
|
+
* than the action next to it, and forty of them down a panel read as a wall of badges. The icon
|
|
76
|
+
* carries the same distinction in the appearance colour and gets out of the way of the text.
|
|
77
|
+
*/
|
|
78
|
+
[data-reticle-log] [data-reticle-log-row] .reticle-chip{display:inline-flex;align-items:center;
|
|
79
|
+
flex:none;padding:0;border:none;background:none;box-shadow:none;border-radius:0;
|
|
80
|
+
color:var(--reticle-c-active);opacity:.85;line-height:0;padding-top:2px;}
|
|
81
|
+
[data-reticle-log] [data-reticle-log-row] .reticle-chip svg{display:block;fill:none;stroke:currentColor;
|
|
82
|
+
stroke-width:1.6;stroke-linecap:round;stroke-linejoin:round;}
|
|
83
|
+
[data-reticle-log] [data-reticle-log-row] .reticle-chip-label{
|
|
84
|
+
position:absolute;width:1px;height:1px;margin:-1px;overflow:hidden;clip-path:inset(50%);white-space:nowrap;}
|
|
85
|
+
[data-reticle-log] [data-reticle-log-row][data-kind="read"] .reticle-chip{opacity:.5;}
|
|
86
|
+
[${LOG_TIME_ATTR}]{flex:none;color:var(--reticle-faint);font-size:9px;font-variant-numeric:tabular-nums;padding-top:2px;min-width:2em;opacity:.85;}
|
|
87
|
+
[data-reticle-log] .reticle-log-text{flex:1;min-width:0;color:var(--reticle-muted);overflow-wrap:anywhere;word-break:break-word;}
|
|
88
|
+
[data-reticle-log] .reticle-res{flex:none;font-size:7.5px;font-weight:600;letter-spacing:.05em;text-transform:uppercase;color:var(--reticle-bad);opacity:.75;padding-top:2px;}
|
|
89
|
+
[data-reticle-log] .reticle-res.reticle-pass{display:none;}
|
|
90
|
+
[data-reticle-log-row][data-kind="human"]{align-self:flex-end;max-width:78%;margin:6px 0 2px;
|
|
91
|
+
padding:8px 12px;background:rgba(255,255,255,.06);border:1px solid rgba(255,255,255,.1);
|
|
92
|
+
border-radius:16px 16px 4px 16px;box-shadow:inset 0 1px 0 rgba(255,255,255,.08);}
|
|
93
|
+
[data-reticle-log-row][data-kind="human"] .reticle-log-text{color:var(--reticle-fg);font-size:12px;line-height:1.45;}
|
|
59
94
|
`;
|
|
95
|
+
/**
|
|
96
|
+
* Pin the feed to its newest row.
|
|
97
|
+
*
|
|
98
|
+
* Called on append AND when the chat is opened, because a hidden panel has no layout: rows added
|
|
99
|
+
* while it was minimised could not scroll it, so opening it showed the OLDEST row and the latest
|
|
100
|
+
* activity - the reason to open it - was somewhere below the fold.
|
|
101
|
+
*/
|
|
102
|
+
export function scrollLogToLatest(container) {
|
|
103
|
+
container.scrollTop = container.scrollHeight;
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Pin the feed to its newest row once the panel has actually laid out.
|
|
107
|
+
*
|
|
108
|
+
* A single frame is not enough on open: the rows carry `content-visibility:auto`, so their real
|
|
109
|
+
* heights arrive after the first paint and `scrollHeight` keeps growing under a scroll that has
|
|
110
|
+
* already happened - the feed landed a third of the way down instead of at the end. Two frames
|
|
111
|
+
* plus one settle pass costs nothing and lands on the last row.
|
|
112
|
+
*/
|
|
113
|
+
export function settleLogAtLatest(container) {
|
|
114
|
+
scrollLogToLatest(container);
|
|
115
|
+
requestAnimationFrame(() => {
|
|
116
|
+
scrollLogToLatest(container);
|
|
117
|
+
requestAnimationFrame(() => scrollLogToLatest(container));
|
|
118
|
+
});
|
|
119
|
+
nativeSetTimeout(() => scrollLogToLatest(container), LOG_SETTLE_MS);
|
|
120
|
+
}
|
|
60
121
|
/** Clamp a logMax option to a sane positive integer, falling back to the default. */
|
|
61
122
|
export function clampLogMax(n) {
|
|
62
123
|
if (n === undefined || !Number.isFinite(n) || n <= 0)
|
|
@@ -64,7 +125,7 @@ export function clampLogMax(n) {
|
|
|
64
125
|
return Math.floor(n);
|
|
65
126
|
}
|
|
66
127
|
/**
|
|
67
|
-
* Pure, human-readable duration ("3s", "47s", "2m", "1h 4m")
|
|
128
|
+
* Pure, human-readable duration ("3s", "47s", "2m", "1h 4m") - no clock read, so it stays
|
|
68
129
|
* deterministic in tests. Used for both the per-row timestamp (time since session start) and the
|
|
69
130
|
* live "idle · {duration} since last action" heartbeat. Sub-second reads as "0s".
|
|
70
131
|
*/
|
|
@@ -92,24 +153,49 @@ export function appendLogRow(container, kind, text, ts, logMax) {
|
|
|
92
153
|
row.setAttribute(DATA_RETICLE_LOG_ROW, '');
|
|
93
154
|
row.setAttribute(DATA_KIND, kind); // styles the human row as an accent chat bubble
|
|
94
155
|
const tsEl = document.createElement('span');
|
|
95
|
-
tsEl.setAttribute(
|
|
156
|
+
tsEl.setAttribute(LOG_TIME_ATTR, '');
|
|
96
157
|
tsEl.textContent = ts;
|
|
97
|
-
const
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
158
|
+
const rowNodes = [];
|
|
159
|
+
if (kind !== LOG_KIND.HUMAN)
|
|
160
|
+
rowNodes.push(tsEl);
|
|
161
|
+
const chipLabelText = LOG_CHIP[kind];
|
|
162
|
+
const chipIcon = LOG_CHIP_ICON[kind];
|
|
163
|
+
if (chipLabelText.length > 0 || chipIcon !== undefined) {
|
|
164
|
+
const chip = document.createElement('span');
|
|
165
|
+
chip.className = LOG_CHIP_CLASS;
|
|
166
|
+
chip.setAttribute('data-mode', LOG_CHIP_MODE[kind]);
|
|
167
|
+
if (chipIcon !== undefined)
|
|
168
|
+
chip.appendChild(hiIcon(chipIcon, PRESENTER_ICON_SIZE.LOG));
|
|
169
|
+
if (chipLabelText.length > 0) {
|
|
170
|
+
const chipLabel = document.createElement('span');
|
|
171
|
+
chipLabel.className = 'reticle-chip-label';
|
|
172
|
+
chipLabel.textContent = chipLabelText;
|
|
173
|
+
chip.appendChild(chipLabel);
|
|
174
|
+
}
|
|
175
|
+
rowNodes.push(chip);
|
|
176
|
+
}
|
|
101
177
|
const textEl = document.createElement('span');
|
|
102
178
|
textEl.className = LOG_TEXT_CLASS;
|
|
103
179
|
textEl.textContent = text;
|
|
104
180
|
const resEl = document.createElement('span');
|
|
105
181
|
resEl.className = LOG_RES_CLASS;
|
|
106
|
-
|
|
182
|
+
rowNodes.push(textEl);
|
|
183
|
+
if (kind !== LOG_KIND.HUMAN)
|
|
184
|
+
rowNodes.push(resEl);
|
|
185
|
+
row.append(...rowNodes);
|
|
107
186
|
container.appendChild(row);
|
|
108
187
|
while (container.childElementCount > logMax)
|
|
109
188
|
container.firstElementChild?.remove();
|
|
110
|
-
|
|
189
|
+
requestAnimationFrame(() => {
|
|
190
|
+
scrollLogToLatest(container);
|
|
191
|
+
});
|
|
111
192
|
return {
|
|
112
193
|
result: (r) => {
|
|
194
|
+
if (r === LOG_RESULT.PASS) {
|
|
195
|
+
resEl.textContent = '';
|
|
196
|
+
resEl.className = `${LOG_RES_CLASS} ${RESULT_CLASS.pass}`;
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
113
199
|
resEl.textContent = ` ${RESULT_GLYPH[r]}`;
|
|
114
200
|
resEl.className = `${LOG_RES_CLASS} ${RESULT_CLASS[r]}`;
|
|
115
201
|
},
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { type ImpactScope, type ImpactSnapshot } from '@reticlehq/core';
|
|
2
|
+
/**
|
|
3
|
+
* What the report SAYS, kept away from how it looks.
|
|
4
|
+
*
|
|
5
|
+
* The wording here is the product's only claim about its own worth, so it is written once, in one
|
|
6
|
+
* place, and tested. Two audiences, two registers:
|
|
7
|
+
*
|
|
8
|
+
* - The REPORT is private. It is the user's own data, so it can carry everything, estimates
|
|
9
|
+
* included, as long as each estimate shows what it is measured against.
|
|
10
|
+
* - The SHARE card is public, and the research on developer stat-cards is blunt about what happens
|
|
11
|
+
* to a public card that leads with vendor savings: estimated token/time savings are the most
|
|
12
|
+
* mocked class of number in AI tooling (JetBrains measured ~10% against a viral 65% claim), and
|
|
13
|
+
* percentiles without a denominator get called misleading. So the card leads with what the
|
|
14
|
+
* user's own setup CAUGHT, and publishes its unknowns - the credibility move, and the thing a
|
|
15
|
+
* scoreboard would never do.
|
|
16
|
+
*/
|
|
17
|
+
/** Reticle's own links, as they appear in the report footer. */
|
|
18
|
+
export declare const REPORT_LINKS: {
|
|
19
|
+
readonly DOCS: "https://docs.reticle.sh";
|
|
20
|
+
readonly SITE: "https://reticle.sh";
|
|
21
|
+
readonly GITHUB: "https://github.com/reticlehq/reticle";
|
|
22
|
+
readonly DISCORD: "https://discord.gg/BwAbzv9ZRz";
|
|
23
|
+
};
|
|
24
|
+
/**
|
|
25
|
+
* The X handle used for `via=` on a share.
|
|
26
|
+
*
|
|
27
|
+
* Empty until somebody who owns the account fills it in, and the share URL omits the parameter
|
|
28
|
+
* while it is - a guessed handle credits a stranger, and there is no way for code to know.
|
|
29
|
+
*/
|
|
30
|
+
export declare const SHARE_VIA_HANDLE = "";
|
|
31
|
+
export declare const REPORT_TEXT: {
|
|
32
|
+
readonly TITLE: "Impact";
|
|
33
|
+
readonly PROJECT: "This project";
|
|
34
|
+
readonly GLOBAL: "Everything on this machine";
|
|
35
|
+
readonly HERO_DEFECTS: "defects caught before you saw them";
|
|
36
|
+
readonly VERDICTS: "Verdicts";
|
|
37
|
+
readonly PASSED: "passed";
|
|
38
|
+
readonly FAILED: "failed";
|
|
39
|
+
readonly UNKNOWN: "unknown";
|
|
40
|
+
readonly UNKNOWN_HELP: "Reticle drove the app and could not tell what happened. Not a pass - shown, not hidden.";
|
|
41
|
+
readonly CALLS: "Tool calls";
|
|
42
|
+
readonly TOKENS: "Tokens returned";
|
|
43
|
+
readonly DRIVING: "Time driving";
|
|
44
|
+
readonly SESSIONS: "Sessions";
|
|
45
|
+
readonly STREAK: "day streak";
|
|
46
|
+
readonly LONGEST: "Longest run";
|
|
47
|
+
readonly SAVED_TOKENS: "Tokens saved";
|
|
48
|
+
readonly SAVED_MINUTES: "Time saved";
|
|
49
|
+
readonly ESTIMATE_TAG: "estimate";
|
|
50
|
+
readonly CHART: "Verdicts, last 30 days";
|
|
51
|
+
readonly EMPTY: "Nothing recorded yet. Drive the app once and this fills in.";
|
|
52
|
+
readonly SHARE: "Share";
|
|
53
|
+
readonly COPY: "Copy";
|
|
54
|
+
readonly COPIED: "Copied";
|
|
55
|
+
readonly REFER: "Send to a friend";
|
|
56
|
+
};
|
|
57
|
+
/** "4.2k", "1.3M" - a number a person reads at a glance rather than counts digits in. */
|
|
58
|
+
export declare function compactNumber(n: number): string;
|
|
59
|
+
/** "2h 14m", "47m", "38s" - the same duration vocabulary the activity strip uses. */
|
|
60
|
+
export declare function compactDuration(ms: number): string;
|
|
61
|
+
/**
|
|
62
|
+
* The post itself.
|
|
63
|
+
*
|
|
64
|
+
* Leads with defects caught, then the verdict split INCLUDING unknowns, then the superlative. No
|
|
65
|
+
* estimated savings, no percentile, no leaderboard: those are the four things the evidence says get
|
|
66
|
+
* a dev-tool card mocked rather than shared.
|
|
67
|
+
*/
|
|
68
|
+
export declare function buildShareText(scope: ImpactScope, projectName?: string): string;
|
|
69
|
+
/** X share intent. `via` is omitted while no handle is configured. */
|
|
70
|
+
export declare function buildXShareUrl(text: string): string;
|
|
71
|
+
/**
|
|
72
|
+
* LinkedIn share.
|
|
73
|
+
*
|
|
74
|
+
* `share-offsite` takes a url and NOTHING else - the title/summary parameters were deprecated in
|
|
75
|
+
* 2018 and are ignored, so the caption has to reach the composer some other way. The report copies
|
|
76
|
+
* it to the clipboard before opening this, which is the only path that works without asking the
|
|
77
|
+
* user for posting permissions.
|
|
78
|
+
*/
|
|
79
|
+
export declare function buildLinkedInShareUrl(): string;
|
|
80
|
+
/** What a referral says: the tool, not the sender's numbers. */
|
|
81
|
+
export declare function buildReferralText(): string;
|
|
82
|
+
/** Narrow an unknown push payload to a snapshot. Invalid shapes are ignored, never rendered. */
|
|
83
|
+
export declare function parseImpactSnapshot(value: unknown): ImpactSnapshot | undefined;
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { ImpactSnapshotSchema } from '@reticlehq/core';
|
|
2
|
+
/**
|
|
3
|
+
* What the report SAYS, kept away from how it looks.
|
|
4
|
+
*
|
|
5
|
+
* The wording here is the product's only claim about its own worth, so it is written once, in one
|
|
6
|
+
* place, and tested. Two audiences, two registers:
|
|
7
|
+
*
|
|
8
|
+
* - The REPORT is private. It is the user's own data, so it can carry everything, estimates
|
|
9
|
+
* included, as long as each estimate shows what it is measured against.
|
|
10
|
+
* - The SHARE card is public, and the research on developer stat-cards is blunt about what happens
|
|
11
|
+
* to a public card that leads with vendor savings: estimated token/time savings are the most
|
|
12
|
+
* mocked class of number in AI tooling (JetBrains measured ~10% against a viral 65% claim), and
|
|
13
|
+
* percentiles without a denominator get called misleading. So the card leads with what the
|
|
14
|
+
* user's own setup CAUGHT, and publishes its unknowns - the credibility move, and the thing a
|
|
15
|
+
* scoreboard would never do.
|
|
16
|
+
*/
|
|
17
|
+
/** Reticle's own links, as they appear in the report footer. */
|
|
18
|
+
export const REPORT_LINKS = {
|
|
19
|
+
DOCS: 'https://docs.reticle.sh',
|
|
20
|
+
SITE: 'https://reticle.sh',
|
|
21
|
+
GITHUB: 'https://github.com/reticlehq/reticle',
|
|
22
|
+
DISCORD: 'https://discord.gg/BwAbzv9ZRz',
|
|
23
|
+
};
|
|
24
|
+
/**
|
|
25
|
+
* The X handle used for `via=` on a share.
|
|
26
|
+
*
|
|
27
|
+
* Empty until somebody who owns the account fills it in, and the share URL omits the parameter
|
|
28
|
+
* while it is - a guessed handle credits a stranger, and there is no way for code to know.
|
|
29
|
+
*/
|
|
30
|
+
export const SHARE_VIA_HANDLE = '';
|
|
31
|
+
export const REPORT_TEXT = {
|
|
32
|
+
TITLE: 'Impact',
|
|
33
|
+
PROJECT: 'This project',
|
|
34
|
+
GLOBAL: 'Everything on this machine',
|
|
35
|
+
HERO_DEFECTS: 'defects caught before you saw them',
|
|
36
|
+
VERDICTS: 'Verdicts',
|
|
37
|
+
PASSED: 'passed',
|
|
38
|
+
FAILED: 'failed',
|
|
39
|
+
UNKNOWN: 'unknown',
|
|
40
|
+
UNKNOWN_HELP: 'Reticle drove the app and could not tell what happened. Not a pass - shown, not hidden.',
|
|
41
|
+
CALLS: 'Tool calls',
|
|
42
|
+
TOKENS: 'Tokens returned',
|
|
43
|
+
DRIVING: 'Time driving',
|
|
44
|
+
SESSIONS: 'Sessions',
|
|
45
|
+
STREAK: 'day streak',
|
|
46
|
+
LONGEST: 'Longest run',
|
|
47
|
+
SAVED_TOKENS: 'Tokens saved',
|
|
48
|
+
SAVED_MINUTES: 'Time saved',
|
|
49
|
+
ESTIMATE_TAG: 'estimate',
|
|
50
|
+
CHART: 'Verdicts, last 30 days',
|
|
51
|
+
EMPTY: 'Nothing recorded yet. Drive the app once and this fills in.',
|
|
52
|
+
SHARE: 'Share',
|
|
53
|
+
COPY: 'Copy',
|
|
54
|
+
COPIED: 'Copied',
|
|
55
|
+
REFER: 'Send to a friend',
|
|
56
|
+
};
|
|
57
|
+
/** "4.2k", "1.3M" - a number a person reads at a glance rather than counts digits in. */
|
|
58
|
+
export function compactNumber(n) {
|
|
59
|
+
if (n < 1000)
|
|
60
|
+
return String(Math.round(n));
|
|
61
|
+
if (n < 1_000_000)
|
|
62
|
+
return `${trim(n / 1000)}k`;
|
|
63
|
+
return `${trim(n / 1_000_000)}M`;
|
|
64
|
+
}
|
|
65
|
+
function trim(n) {
|
|
66
|
+
return n >= 10 ? String(Math.round(n)) : String(Math.round(n * 10) / 10);
|
|
67
|
+
}
|
|
68
|
+
/** "2h 14m", "47m", "38s" - the same duration vocabulary the activity strip uses. */
|
|
69
|
+
export function compactDuration(ms) {
|
|
70
|
+
const s = Math.floor(ms / 1000);
|
|
71
|
+
if (s < 60)
|
|
72
|
+
return `${String(s)}s`;
|
|
73
|
+
const m = Math.floor(s / 60);
|
|
74
|
+
if (m < 60)
|
|
75
|
+
return `${String(m)}m`;
|
|
76
|
+
const h = Math.floor(m / 60);
|
|
77
|
+
return 0 === m % 60 ? `${String(h)}h` : `${String(h)}h ${String(m % 60)}m`;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* The post itself.
|
|
81
|
+
*
|
|
82
|
+
* Leads with defects caught, then the verdict split INCLUDING unknowns, then the superlative. No
|
|
83
|
+
* estimated savings, no percentile, no leaderboard: those are the four things the evidence says get
|
|
84
|
+
* a dev-tool card mocked rather than shared.
|
|
85
|
+
*/
|
|
86
|
+
export function buildShareText(scope, projectName) {
|
|
87
|
+
const c = scope.counts;
|
|
88
|
+
const where = projectName !== undefined && projectName.length > 0 ? ` on ${projectName}` : '';
|
|
89
|
+
const lines = [
|
|
90
|
+
`My agent verified its own work ${String(c.verdicts)} times${where}.`,
|
|
91
|
+
`${String(c.failed)} defects caught before I looked at any of them.`,
|
|
92
|
+
];
|
|
93
|
+
if (c.unknown > 0) {
|
|
94
|
+
lines.push(`${String(c.unknown)} came back "unknown" - it drove the app and could not tell. That is the number I watch.`);
|
|
95
|
+
}
|
|
96
|
+
if (scope.records.longestRunMs > 0) {
|
|
97
|
+
lines.push(`Longest unattended run: ${compactDuration(scope.records.longestRunMs)}.`);
|
|
98
|
+
}
|
|
99
|
+
return lines.join('\n');
|
|
100
|
+
}
|
|
101
|
+
/** X share intent. `via` is omitted while no handle is configured. */
|
|
102
|
+
export function buildXShareUrl(text) {
|
|
103
|
+
const params = new URLSearchParams({ text, url: REPORT_LINKS.SITE });
|
|
104
|
+
if (SHARE_VIA_HANDLE.length > 0)
|
|
105
|
+
params.set('via', SHARE_VIA_HANDLE);
|
|
106
|
+
return `https://twitter.com/intent/tweet?${params.toString()}`;
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* LinkedIn share.
|
|
110
|
+
*
|
|
111
|
+
* `share-offsite` takes a url and NOTHING else - the title/summary parameters were deprecated in
|
|
112
|
+
* 2018 and are ignored, so the caption has to reach the composer some other way. The report copies
|
|
113
|
+
* it to the clipboard before opening this, which is the only path that works without asking the
|
|
114
|
+
* user for posting permissions.
|
|
115
|
+
*/
|
|
116
|
+
export function buildLinkedInShareUrl() {
|
|
117
|
+
return `https://www.linkedin.com/sharing/share-offsite/?url=${encodeURIComponent(REPORT_LINKS.SITE)}`;
|
|
118
|
+
}
|
|
119
|
+
/** What a referral says: the tool, not the sender's numbers. */
|
|
120
|
+
export function buildReferralText() {
|
|
121
|
+
return `I have been verifying my agent's work from inside the running app with Reticle - it returns pass/fail with the file:line to fix. ${REPORT_LINKS.SITE}`;
|
|
122
|
+
}
|
|
123
|
+
/** Narrow an unknown push payload to a snapshot. Invalid shapes are ignored, never rendered. */
|
|
124
|
+
export function parseImpactSnapshot(value) {
|
|
125
|
+
const parsed = ImpactSnapshotSchema.safeParse(value);
|
|
126
|
+
return parsed.success ? parsed.data : undefined;
|
|
127
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The impact report's surface.
|
|
3
|
+
*
|
|
4
|
+
* Same glass as the chat panel and anchored in the same slot above the toolbar, because it is the
|
|
5
|
+
* same object seen a different way: what this session is doing, and what every session has done.
|
|
6
|
+
*/
|
|
7
|
+
export declare const REPORT_CSS: string;
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { HUD_DROP_SHADOW } from './presenter-hud-chrome.js';
|
|
2
|
+
import { DOCK_ALIGN_ATTR, DOCK_ATTR, REPORT_ATTR, REPORT_PANEL_ATTR } from './presenter-config.js';
|
|
3
|
+
const OVERLAY = 'data-reticle-overlay';
|
|
4
|
+
/**
|
|
5
|
+
* The impact report's surface.
|
|
6
|
+
*
|
|
7
|
+
* Same glass as the chat panel and anchored in the same slot above the toolbar, because it is the
|
|
8
|
+
* same object seen a different way: what this session is doing, and what every session has done.
|
|
9
|
+
*/
|
|
10
|
+
export const REPORT_CSS = `
|
|
11
|
+
[${REPORT_PANEL_ATTR}]{
|
|
12
|
+
position:absolute;right:0;left:auto;bottom:calc(100% + 12px);z-index:31;
|
|
13
|
+
box-sizing:border-box;display:flex;flex-direction:column;width:var(--reticle-dock-w);
|
|
14
|
+
max-width:min(var(--reticle-dock-w),calc(100vw - 16px));max-height:calc(100vh - 140px);
|
|
15
|
+
opacity:0;visibility:hidden;transform:translate3d(0,6px,0) scale(.98);pointer-events:none;
|
|
16
|
+
border-radius:16px;border:1px solid color-mix(in srgb,var(--reticle-c-active) 26%,rgba(255,255,255,.1));
|
|
17
|
+
background:
|
|
18
|
+
radial-gradient(130% 90% at 50% 0%,color-mix(in srgb,var(--reticle-c-active) 18%,transparent),transparent 62%),
|
|
19
|
+
linear-gradient(180deg,rgba(13,15,22,.96),rgba(19,22,32,.94));
|
|
20
|
+
box-shadow:${HUD_DROP_SHADOW},0 0 54px -18px var(--reticle-c-active);
|
|
21
|
+
color:var(--reticle-fg);font-family:var(--reticle-font);
|
|
22
|
+
transition:opacity .18s ease,transform .18s ease,visibility .18s;}
|
|
23
|
+
[${DOCK_ATTR}][${DOCK_ALIGN_ATTR}="start"] [${REPORT_PANEL_ATTR}]{right:auto;left:0;}
|
|
24
|
+
[${OVERLAY}][${REPORT_ATTR}="1"] [${REPORT_PANEL_ATTR}]{
|
|
25
|
+
opacity:1;visibility:visible;transform:translate3d(0,0,0) scale(1);pointer-events:auto;}
|
|
26
|
+
[${REPORT_PANEL_ATTR}] .reticle-report-inner{
|
|
27
|
+
display:flex;flex-direction:column;min-height:0;overflow:hidden;}
|
|
28
|
+
[${REPORT_PANEL_ATTR}] .reticle-report-head{
|
|
29
|
+
flex:none;display:flex;align-items:center;gap:8px;padding:11px 12px 9px 14px;
|
|
30
|
+
border-bottom:1px solid rgba(255,255,255,.06);}
|
|
31
|
+
[${REPORT_PANEL_ATTR}] .reticle-report-title{font-size:12.5px;font-weight:600;}
|
|
32
|
+
[${REPORT_PANEL_ATTR}] .reticle-report-scope{
|
|
33
|
+
margin-left:auto;padding:3px 9px;border-radius:999px;cursor:pointer;
|
|
34
|
+
border:1px solid rgba(255,255,255,.14);background:rgba(255,255,255,.04);
|
|
35
|
+
color:var(--reticle-muted);font:inherit;font-size:10.5px;}
|
|
36
|
+
[${REPORT_PANEL_ATTR}] .reticle-report-scope:hover{background:rgba(255,255,255,.08);color:var(--reticle-fg);}
|
|
37
|
+
[${REPORT_PANEL_ATTR}] .reticle-report-close{
|
|
38
|
+
flex:none;display:inline-flex;align-items:center;justify-content:center;width:22px;height:22px;
|
|
39
|
+
border:none;background:transparent;color:var(--reticle-faint);cursor:pointer;line-height:0;}
|
|
40
|
+
[${REPORT_PANEL_ATTR}] .reticle-report-close svg{display:block;fill:none;stroke:currentColor;stroke-width:1.6;}
|
|
41
|
+
[${REPORT_PANEL_ATTR}] .reticle-report-body{
|
|
42
|
+
min-height:0;overflow-y:auto;padding:12px 14px 6px;
|
|
43
|
+
scrollbar-width:thin;scrollbar-color:rgba(255,255,255,.14) transparent;}
|
|
44
|
+
[${REPORT_PANEL_ATTR}] .reticle-report-empty{
|
|
45
|
+
margin:0;padding:22px 6px;text-align:center;color:var(--reticle-faint);font-size:11.5px;}
|
|
46
|
+
/* The streak sits above the hero, as a flame and a number: it is the one stat that is about
|
|
47
|
+
TODAY, so it reads first and never competes with the count it sits over. */
|
|
48
|
+
[${REPORT_PANEL_ATTR}] .reticle-report-streak{
|
|
49
|
+
display:inline-flex;align-items:baseline;gap:5px;margin-bottom:8px;padding:3px 9px;
|
|
50
|
+
border-radius:999px;background:rgba(255,255,255,.05);
|
|
51
|
+
border:1px solid color-mix(in srgb,var(--reticle-c-active) 22%,transparent);
|
|
52
|
+
font-size:12px;font-weight:600;font-variant-numeric:tabular-nums;}
|
|
53
|
+
[${REPORT_PANEL_ATTR}] .reticle-report-streak-label{
|
|
54
|
+
color:var(--reticle-faint);font-size:9.5px;font-weight:400;letter-spacing:.04em;text-transform:uppercase;}
|
|
55
|
+
[${REPORT_PANEL_ATTR}] .reticle-report-hero{display:flex;flex-direction:column;gap:2px;margin-bottom:10px;}
|
|
56
|
+
[${REPORT_PANEL_ATTR}] .reticle-report-hero-value{
|
|
57
|
+
font-size:34px;font-weight:700;line-height:1;letter-spacing:-.02em;color:var(--reticle-c-active);}
|
|
58
|
+
[${REPORT_PANEL_ATTR}] .reticle-report-hero-label{color:var(--reticle-muted);font-size:11.5px;}
|
|
59
|
+
[${REPORT_PANEL_ATTR}] .reticle-report-verdicts{display:flex;gap:6px;margin-bottom:12px;}
|
|
60
|
+
[${REPORT_PANEL_ATTR}] .reticle-report-verdict{
|
|
61
|
+
flex:1;padding:5px 8px;border-radius:8px;font-size:10.5px;text-align:center;
|
|
62
|
+
background:rgba(255,255,255,.05);color:var(--reticle-muted);}
|
|
63
|
+
[${REPORT_PANEL_ATTR}] .reticle-report-verdict[data-kind="fail"]{color:#fca5a5;}
|
|
64
|
+
[${REPORT_PANEL_ATTR}] .reticle-report-verdict[data-kind="unknown"]{color:#fcd34d;}
|
|
65
|
+
[${REPORT_PANEL_ATTR}] .reticle-report-grid{
|
|
66
|
+
display:grid;grid-template-columns:repeat(3,1fr);gap:6px;}
|
|
67
|
+
[${REPORT_PANEL_ATTR}] .reticle-report-card{
|
|
68
|
+
display:flex;flex-direction:column;gap:2px;padding:8px;border-radius:10px;
|
|
69
|
+
background:rgba(255,255,255,.04);}
|
|
70
|
+
[${REPORT_PANEL_ATTR}] .reticle-report-value{font-size:15px;font-weight:600;font-variant-numeric:tabular-nums;}
|
|
71
|
+
[${REPORT_PANEL_ATTR}] .reticle-report-label{color:var(--reticle-faint);font-size:9.5px;line-height:1.3;}
|
|
72
|
+
/* An estimate is labelled as one, and its label carries what it is measured against. */
|
|
73
|
+
[${REPORT_PANEL_ATTR}] .reticle-report-basis{
|
|
74
|
+
display:block;margin-top:2px;color:var(--reticle-c-active);opacity:.8;
|
|
75
|
+
font-size:8.5px;letter-spacing:.06em;text-transform:uppercase;cursor:help;}
|
|
76
|
+
[${REPORT_PANEL_ATTR}] .reticle-report-chart-wrap{margin-top:12px;}
|
|
77
|
+
[${REPORT_PANEL_ATTR}] .reticle-report-section{
|
|
78
|
+
display:block;margin-bottom:6px;color:var(--reticle-faint);font-size:9.5px;
|
|
79
|
+
letter-spacing:.06em;text-transform:uppercase;}
|
|
80
|
+
/* Thirty fixed slots, not a flex row: one recorded day is one thin bar on a month's axis, and a
|
|
81
|
+
single stretched block would read as "a month of solid work" on the day you install it. */
|
|
82
|
+
[${REPORT_PANEL_ATTR}] .reticle-report-chart{
|
|
83
|
+
display:grid;grid-template-columns:repeat(30,1fr);align-items:end;gap:2px;height:44px;padding:0 1px;}
|
|
84
|
+
[${REPORT_PANEL_ATTR}] .reticle-report-bar{
|
|
85
|
+
min-width:2px;border-radius:2px 2px 0 0;
|
|
86
|
+
background:color-mix(in srgb,var(--reticle-c-active) 55%,transparent);}
|
|
87
|
+
[${REPORT_PANEL_ATTR}] .reticle-report-bar[data-hot="1"]{background:#f87171;}
|
|
88
|
+
[${REPORT_PANEL_ATTR}] .reticle-report-foot{
|
|
89
|
+
flex:none;display:flex;flex-wrap:wrap;gap:6px;padding:10px 14px 8px;
|
|
90
|
+
border-top:1px solid rgba(255,255,255,.06);}
|
|
91
|
+
[${REPORT_PANEL_ATTR}] .reticle-report-share{
|
|
92
|
+
padding:5px 10px;border-radius:8px;cursor:pointer;font:inherit;font-size:11px;
|
|
93
|
+
border:1px solid rgba(255,255,255,.14);background:rgba(255,255,255,.04);color:var(--reticle-muted);}
|
|
94
|
+
[${REPORT_PANEL_ATTR}] .reticle-report-share:hover{background:rgba(255,255,255,.09);color:var(--reticle-fg);}
|
|
95
|
+
[${REPORT_PANEL_ATTR}] .reticle-report-refer{
|
|
96
|
+
border-color:color-mix(in srgb,var(--reticle-c-active) 45%,transparent);color:var(--reticle-fg);}
|
|
97
|
+
[${REPORT_PANEL_ATTR}] .reticle-report-links{
|
|
98
|
+
flex:none;display:flex;gap:12px;padding:0 14px 12px;}
|
|
99
|
+
[${REPORT_PANEL_ATTR}] .reticle-report-links a{
|
|
100
|
+
color:var(--reticle-faint);font-size:10.5px;text-decoration:none;}
|
|
101
|
+
[${REPORT_PANEL_ATTR}] .reticle-report-links a:hover{color:var(--reticle-fg);text-decoration:underline;}
|
|
102
|
+
`;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { ImpactScope, ImpactSnapshot } from '@reticlehq/core';
|
|
2
|
+
export declare function reportPanelHtml(): string;
|
|
3
|
+
export declare function reportBodyHtml(scope: ImpactScope): string;
|
|
4
|
+
export interface ReportHost {
|
|
5
|
+
/** Opened from the toolbar and from the chat, so the shell decides what else must close. */
|
|
6
|
+
onBeforeOpen?: () => void;
|
|
7
|
+
}
|
|
8
|
+
/** The report panel controller: scope toggle, live repaint, share + referral actions. */
|
|
9
|
+
export declare class PresenterReport {
|
|
10
|
+
#private;
|
|
11
|
+
constructor(host?: ReportHost);
|
|
12
|
+
mount(root: HTMLElement): void;
|
|
13
|
+
/** The record arrived from the daemon. Repaint only if the panel is on screen. */
|
|
14
|
+
setSnapshot(snapshot: ImpactSnapshot): void;
|
|
15
|
+
snapshot(): ImpactSnapshot | undefined;
|
|
16
|
+
isOpen(): boolean;
|
|
17
|
+
open(): void;
|
|
18
|
+
close(): void;
|
|
19
|
+
toggle(): void;
|
|
20
|
+
contains(node: Node): boolean;
|
|
21
|
+
/** The post text for whichever scope is showing. */
|
|
22
|
+
shareText(): string;
|
|
23
|
+
}
|