@reticlehq/browser 2.7.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.
Files changed (92) hide show
  1. package/NOTICE +7 -0
  2. package/README.md +2 -2
  3. package/dist/actions/actions-dom.js +23 -15
  4. package/dist/actions/actions.d.ts +17 -0
  5. package/dist/actions/actions.js +132 -29
  6. package/dist/actions/synthetic-input.d.ts +9 -0
  7. package/dist/actions/synthetic-input.js +40 -0
  8. package/dist/commands/commands.js +18 -1
  9. package/dist/connect-options.d.ts +9 -9
  10. package/dist/dom/a11y.d.ts +9 -0
  11. package/dist/dom/a11y.js +19 -0
  12. package/dist/dom/desktop-capture.js +20 -3
  13. package/dist/dom/dom-ignore.d.ts +19 -6
  14. package/dist/dom/dom-ignore.js +20 -13
  15. package/dist/dom/query.d.ts +2 -2
  16. package/dist/dom/query.js +85 -45
  17. package/dist/dom/refs.d.ts +28 -0
  18. package/dist/dom/refs.js +87 -0
  19. package/dist/dom/snapshot.d.ts +5 -0
  20. package/dist/dom/snapshot.js +34 -0
  21. package/dist/dom/source.d.ts +14 -0
  22. package/dist/dom/source.js +16 -0
  23. package/dist/dom/theme.d.ts +33 -10
  24. package/dist/dom/theme.js +109 -50
  25. package/dist/observers/console.js +5 -2
  26. package/dist/observers/install-all.js +4 -1
  27. package/dist/observers/ipc.d.ts +16 -0
  28. package/dist/observers/ipc.js +19 -1
  29. package/dist/observers/network.d.ts +8 -0
  30. package/dist/observers/network.js +29 -6
  31. package/dist/observers/state.js +21 -2
  32. package/dist/presenter/presenter-brand.d.ts +24 -7
  33. package/dist/presenter/presenter-brand.js +23 -6
  34. package/dist/presenter/presenter-config.d.ts +102 -9
  35. package/dist/presenter/presenter-config.js +94 -8
  36. package/dist/presenter/presenter-controls.d.ts +10 -10
  37. package/dist/presenter/presenter-controls.js +175 -85
  38. package/dist/presenter/presenter-dock-layout.d.ts +6 -0
  39. package/dist/presenter/presenter-dock-layout.js +170 -0
  40. package/dist/presenter/presenter-drag.d.ts +48 -0
  41. package/dist/presenter/presenter-drag.js +198 -0
  42. package/dist/presenter/presenter-effects.d.ts +1 -1
  43. package/dist/presenter/presenter-effects.js +1 -1
  44. package/dist/presenter/presenter-glow.d.ts +5 -5
  45. package/dist/presenter/presenter-glow.js +7 -7
  46. package/dist/presenter/presenter-heroicons-data.d.ts +41 -0
  47. package/dist/presenter/presenter-heroicons-data.js +38 -0
  48. package/dist/presenter/presenter-hud-chrome.d.ts +17 -0
  49. package/dist/presenter/presenter-hud-chrome.js +41 -0
  50. package/dist/presenter/presenter-icons.d.ts +44 -0
  51. package/dist/presenter/presenter-icons.js +100 -0
  52. package/dist/presenter/presenter-log.d.ts +27 -5
  53. package/dist/presenter/presenter-log.js +114 -28
  54. package/dist/presenter/presenter-report-copy.d.ts +83 -0
  55. package/dist/presenter/presenter-report-copy.js +127 -0
  56. package/dist/presenter/presenter-report-styles.d.ts +7 -0
  57. package/dist/presenter/presenter-report-styles.js +102 -0
  58. package/dist/presenter/presenter-report.d.ts +23 -0
  59. package/dist/presenter/presenter-report.js +217 -0
  60. package/dist/presenter/presenter-run-state.d.ts +1 -1
  61. package/dist/presenter/presenter-settings-styles.d.ts +2 -0
  62. package/dist/presenter/presenter-settings-styles.js +153 -0
  63. package/dist/presenter/presenter-settings.d.ts +82 -0
  64. package/dist/presenter/presenter-settings.js +494 -0
  65. package/dist/presenter/presenter-shell-styles.d.ts +1 -0
  66. package/dist/presenter/presenter-shell-styles.js +375 -0
  67. package/dist/presenter/presenter-shell.d.ts +48 -0
  68. package/dist/presenter/presenter-shell.js +408 -0
  69. package/dist/presenter/presenter-styles.d.ts +9 -4
  70. package/dist/presenter/presenter-styles.js +61 -125
  71. package/dist/presenter/presenter-tally.d.ts +8 -11
  72. package/dist/presenter/presenter-tally.js +48 -23
  73. package/dist/presenter/presenter-test-helpers.d.ts +1 -1
  74. package/dist/presenter/presenter-test-helpers.js +1 -1
  75. package/dist/presenter/presenter-verbs.js +2 -2
  76. package/dist/presenter/presenter-workspace.d.ts +15 -0
  77. package/dist/presenter/presenter-workspace.js +180 -0
  78. package/dist/presenter/presenter.d.ts +15 -10
  79. package/dist/presenter/presenter.js +194 -46
  80. package/dist/registry/emitter.d.ts +2 -2
  81. package/dist/reticle.d.ts +2 -2
  82. package/dist/reticle.js +27 -25
  83. package/dist/review/annotator-styles.d.ts +6 -0
  84. package/dist/review/annotator-styles.js +81 -0
  85. package/dist/review/annotator.d.ts +29 -17
  86. package/dist/review/annotator.js +477 -146
  87. package/dist/timers/clock.js +8 -5
  88. package/dist/transport/unreachable-message.d.ts +12 -0
  89. package/dist/transport/unreachable-message.js +20 -0
  90. package/dist/util/captured-method.d.ts +37 -0
  91. package/dist/util/captured-method.js +45 -0
  92. package/package.json +9 -6
@@ -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) — they
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
- /** HUD chip copy keyed by presenter mode (UI text, browser-local — not a wire string). */
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: '✓', fail: '✗' };
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
- const DATA_RETICLE_LOG_TS = 'data-reticle-log-ts';
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;display:flex;flex-direction:column;
45
- gap:7px;padding:12px 14px;scrollbar-width:thin;scrollbar-color:rgba(255,255,255,.16) transparent;}
46
- [data-reticle-log]::-webkit-scrollbar{width:9px;}
47
- [data-reticle-log]::-webkit-scrollbar-thumb{background:rgba(255,255,255,.14);border-radius:9px;border:2px solid transparent;background-clip:content-box;}
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:baseline;gap:8px;font-size:12px;line-height:1.45;
50
- animation:reticle-row-in .26s cubic-bezier(.16,1,.3,1);}
51
- @keyframes reticle-row-in{from{opacity:0;transform:translateY(5px);}to{opacity:1;transform:none;}}
52
- [data-reticle-log-ts]{flex:none;color:var(--reticle-faint);font-size:10px;font-variant-numeric:tabular-nums;padding-top:1px;}
53
- [data-reticle-log] .reticle-log-text{flex:1;min-width:0;color:#d6dae4;overflow-wrap:anywhere;word-break:break-word;}
54
- [data-reticle-log] .reticle-res{flex:none;font-weight:700;}
55
- [data-reticle-log-row][data-kind="human"]{align-self:flex-end;max-width:88%;
56
- background:var(--reticle-accent-soft);border:1px solid var(--reticle-accent);border-radius:13px 13px 4px 13px;padding:6px 11px;}
57
- [data-reticle-log-row][data-kind="human"] [data-reticle-log-ts]{display:none;}
58
- [data-reticle-log-row][data-kind="human"] .reticle-log-text{color:var(--reticle-fg);}
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") — no clock read, so it stays
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(DATA_RETICLE_LOG_TS, '');
156
+ tsEl.setAttribute(LOG_TIME_ATTR, '');
96
157
  tsEl.textContent = ts;
97
- const chip = document.createElement('span');
98
- chip.className = LOG_CHIP_CLASS;
99
- chip.setAttribute('data-mode', LOG_CHIP_MODE[kind]);
100
- chip.textContent = LOG_CHIP[kind];
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
- row.append(tsEl, chip, textEl, resEl);
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
- container.scrollTop = container.scrollHeight;
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
+ }