@loupekit/sdk 0.10.23-next.69 → 0.10.24-next.70
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/dist/index.global.js +869 -602
- package/dist/index.global.js.map +1 -1
- package/dist/index.js +750 -483
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/index.global.js
CHANGED
|
@@ -794,6 +794,43 @@ var Loupe = (() => {
|
|
|
794
794
|
.voice:disabled { opacity: .45; cursor: not-allowed; }
|
|
795
795
|
.voice.on { border-color: var(--pin); color: var(--pin); background: var(--bg-3); }
|
|
796
796
|
|
|
797
|
+
/* reactions */
|
|
798
|
+
.rxns { display: flex; flex-wrap: wrap; align-items: center; gap: 4px; margin-top: 6px; position: relative; }
|
|
799
|
+
.rxn {
|
|
800
|
+
display: inline-flex; align-items: center; gap: 3px; padding: 1px 7px;
|
|
801
|
+
border: 1px solid var(--line); border-radius: 999px; background: var(--bg);
|
|
802
|
+
color: var(--ink); font-size: 11px; cursor: pointer; line-height: 1.7;
|
|
803
|
+
}
|
|
804
|
+
.rxn:hover { border-color: var(--accent); }
|
|
805
|
+
.rxn.mine { border-color: var(--accent); background: var(--accent-soft); }
|
|
806
|
+
.rxn-n { color: var(--muted); font-size: 10px; font-variant-numeric: tabular-nums; }
|
|
807
|
+
.rxn.mine .rxn-n { color: var(--accent); }
|
|
808
|
+
.rxn-add {
|
|
809
|
+
width: 20px; height: 20px; padding: 0; border: 1px dashed var(--line); border-radius: 50%;
|
|
810
|
+
background: transparent; color: var(--muted); font-size: 11px; cursor: pointer; line-height: 1;
|
|
811
|
+
}
|
|
812
|
+
.rxn-add:hover { border-color: var(--accent); color: var(--accent); }
|
|
813
|
+
.rxn-pick {
|
|
814
|
+
display: flex; gap: 2px; padding: 3px; border: 1px solid var(--line); border-radius: 999px;
|
|
815
|
+
background: var(--bg); box-shadow: 0 4px 14px rgb(0 0 0 / 18%); z-index: 3;
|
|
816
|
+
}
|
|
817
|
+
.rxn-opt {
|
|
818
|
+
width: 24px; height: 24px; padding: 0; border: 0; border-radius: 50%;
|
|
819
|
+
background: transparent; font-size: 14px; cursor: pointer; line-height: 1;
|
|
820
|
+
}
|
|
821
|
+
.rxn-opt:hover { background: var(--accent-soft); }
|
|
822
|
+
|
|
823
|
+
/* who else is here */
|
|
824
|
+
.peers { display: flex; align-items: center; gap: -2px; margin-right: 8px; }
|
|
825
|
+
.peer-av {
|
|
826
|
+
display: inline-flex; align-items: center; justify-content: center;
|
|
827
|
+
width: 20px; height: 20px; margin-left: -5px; border-radius: 50%;
|
|
828
|
+
border: 2px solid var(--bg); background: var(--accent); color: #fff;
|
|
829
|
+
font-size: 9px; font-weight: 700; letter-spacing: .02em;
|
|
830
|
+
}
|
|
831
|
+
.peer-av:first-child { margin-left: 0; }
|
|
832
|
+
.peer-more { margin-left: 3px; font-size: 10px; color: var(--muted); }
|
|
833
|
+
|
|
797
834
|
/* in-app mentions */
|
|
798
835
|
.hnotif { margin-top: 10px; }
|
|
799
836
|
.nf-head { display: flex; align-items: center; gap: 6px; margin-bottom: 5px; font-size: 11.5px; color: var(--ink); }
|
|
@@ -2927,171 +2964,649 @@ var Loupe = (() => {
|
|
|
2927
2964
|
});
|
|
2928
2965
|
}
|
|
2929
2966
|
|
|
2930
|
-
//
|
|
2931
|
-
var
|
|
2932
|
-
|
|
2933
|
-
|
|
2934
|
-
|
|
2935
|
-
|
|
2936
|
-
|
|
2937
|
-
|
|
2938
|
-
|
|
2939
|
-
|
|
2940
|
-
|
|
2941
|
-
}
|
|
2942
|
-
|
|
2943
|
-
|
|
2944
|
-
|
|
2945
|
-
|
|
2946
|
-
|
|
2947
|
-
|
|
2967
|
+
// ../shared/dist/activity.js
|
|
2968
|
+
var ACTIVITY_STATUS_LABELS = {
|
|
2969
|
+
idle: "Idle",
|
|
2970
|
+
working: "Working",
|
|
2971
|
+
error: "Error"
|
|
2972
|
+
};
|
|
2973
|
+
function formatDuration(ms) {
|
|
2974
|
+
if (!ms || ms < 0)
|
|
2975
|
+
return "\u2014";
|
|
2976
|
+
const s = Math.round(ms / 1e3);
|
|
2977
|
+
if (s < 60)
|
|
2978
|
+
return `${s}s`;
|
|
2979
|
+
const m = Math.floor(s / 60);
|
|
2980
|
+
const rest = s % 60;
|
|
2981
|
+
if (m < 60)
|
|
2982
|
+
return rest ? `${m}m ${rest}s` : `${m}m`;
|
|
2983
|
+
const h = Math.floor(m / 60);
|
|
2984
|
+
return `${h}h ${m % 60}m`;
|
|
2985
|
+
}
|
|
2986
|
+
function summarizeActivity(events, status = "idle", now = Date.now()) {
|
|
2987
|
+
const files = /* @__PURE__ */ new Set();
|
|
2988
|
+
const kinds = /* @__PURE__ */ new Map();
|
|
2989
|
+
let errors = 0;
|
|
2990
|
+
let first = Infinity;
|
|
2991
|
+
for (const e of events) {
|
|
2992
|
+
for (const f of e.files ?? [])
|
|
2993
|
+
files.add(f);
|
|
2994
|
+
kinds.set(e.kind, (kinds.get(e.kind) ?? 0) + 1);
|
|
2995
|
+
if (e.level === "error")
|
|
2996
|
+
errors++;
|
|
2997
|
+
const t = Date.parse(e.at);
|
|
2998
|
+
if (!Number.isNaN(t))
|
|
2999
|
+
first = Math.min(first, t);
|
|
2948
3000
|
}
|
|
2949
|
-
|
|
2950
|
-
|
|
2951
|
-
|
|
2952
|
-
|
|
2953
|
-
|
|
2954
|
-
|
|
2955
|
-
|
|
2956
|
-
|
|
2957
|
-
|
|
2958
|
-
|
|
3001
|
+
return {
|
|
3002
|
+
status,
|
|
3003
|
+
events: events.length,
|
|
3004
|
+
errors,
|
|
3005
|
+
files: files.size,
|
|
3006
|
+
durationMs: events.length && first !== Infinity ? Math.max(0, now - first) : 0,
|
|
3007
|
+
byKind: [...kinds.entries()].map(([kind, count]) => ({ kind, count })).sort((a, b) => b.count - a.count || a.kind.localeCompare(b.kind))
|
|
3008
|
+
};
|
|
3009
|
+
}
|
|
3010
|
+
|
|
3011
|
+
// ../shared/dist/lifecycle.js
|
|
3012
|
+
var LIFECYCLE_LABELS = {
|
|
3013
|
+
sent: "Sent to agent",
|
|
3014
|
+
in_pr: "In PR",
|
|
3015
|
+
preview: "Review preview",
|
|
3016
|
+
reviewed: "Reviewed"
|
|
3017
|
+
};
|
|
3018
|
+
function lifecycle(c) {
|
|
3019
|
+
const hasProposal = !!c.proposal;
|
|
3020
|
+
const pr = c.pr ?? void 0;
|
|
3021
|
+
const withPr = (stage) => {
|
|
3022
|
+
const out = { stage, label: LIFECYCLE_LABELS[stage] };
|
|
3023
|
+
if (pr) {
|
|
3024
|
+
out.pr = pr;
|
|
3025
|
+
if (typeof pr.checksTotal === "number" && pr.checksTotal > 0) {
|
|
3026
|
+
const passed = Math.max(0, Math.min(pr.checksTotal, pr.checksPassed ?? 0));
|
|
3027
|
+
out.checks = {
|
|
3028
|
+
text: `${passed}/${pr.checksTotal}`,
|
|
3029
|
+
ratio: passed / pr.checksTotal
|
|
3030
|
+
};
|
|
2959
3031
|
}
|
|
2960
3032
|
}
|
|
2961
|
-
return out
|
|
3033
|
+
return out;
|
|
3034
|
+
};
|
|
3035
|
+
if (c.status === "resolved")
|
|
3036
|
+
return hasProposal || pr ? withPr("reviewed") : null;
|
|
3037
|
+
if (pr)
|
|
3038
|
+
return withPr("in_pr");
|
|
3039
|
+
if (c.status === "in_review")
|
|
3040
|
+
return { stage: "preview", label: LIFECYCLE_LABELS.preview };
|
|
3041
|
+
if (hasProposal)
|
|
3042
|
+
return { stage: "sent", label: LIFECYCLE_LABELS.sent };
|
|
3043
|
+
return null;
|
|
3044
|
+
}
|
|
3045
|
+
function awaitingReview(comments) {
|
|
3046
|
+
return comments.filter((c) => c.status === "in_review");
|
|
3047
|
+
}
|
|
3048
|
+
|
|
3049
|
+
// ../shared/dist/iteration.js
|
|
3050
|
+
function emptyIterations() {
|
|
3051
|
+
return { items: [], index: -1 };
|
|
3052
|
+
}
|
|
3053
|
+
function current(state) {
|
|
3054
|
+
return state.index >= 0 ? state.items[state.index] ?? null : null;
|
|
3055
|
+
}
|
|
3056
|
+
function addIteration(state, iteration) {
|
|
3057
|
+
const head = state.items.slice(0, state.index + 1);
|
|
3058
|
+
const items = [...head, iteration].slice(-MAX_ITERATIONS);
|
|
3059
|
+
return { items, index: items.length - 1 };
|
|
3060
|
+
}
|
|
3061
|
+
var MAX_ITERATIONS = 20;
|
|
3062
|
+
function canUndo(state) {
|
|
3063
|
+
return state.items.length > 0;
|
|
3064
|
+
}
|
|
3065
|
+
function undo(state) {
|
|
3066
|
+
if (!state.items.length)
|
|
3067
|
+
return state;
|
|
3068
|
+
const items = state.items.slice(0, -1);
|
|
3069
|
+
return { items, index: items.length - 1 };
|
|
3070
|
+
}
|
|
3071
|
+
function canMove(state, delta) {
|
|
3072
|
+
const next = state.index + delta;
|
|
3073
|
+
return next >= 0 && next < state.items.length;
|
|
3074
|
+
}
|
|
3075
|
+
function move(state, delta) {
|
|
3076
|
+
return canMove(state, delta) ? { ...state, index: state.index + delta } : state;
|
|
3077
|
+
}
|
|
3078
|
+
function stackLabel(state) {
|
|
3079
|
+
return state.items.length ? `${state.index + 1} / ${state.items.length}` : "";
|
|
3080
|
+
}
|
|
3081
|
+
|
|
3082
|
+
// ../shared/dist/consent.js
|
|
3083
|
+
function emptyConsent() {
|
|
3084
|
+
return { state: "idle", request: null, history: [] };
|
|
3085
|
+
}
|
|
3086
|
+
function isNavigableUrl(raw) {
|
|
3087
|
+
try {
|
|
3088
|
+
const u = new URL(raw);
|
|
3089
|
+
return u.protocol === "http:" || u.protocol === "https:";
|
|
3090
|
+
} catch {
|
|
3091
|
+
return false;
|
|
2962
3092
|
}
|
|
2963
|
-
|
|
2964
|
-
|
|
2965
|
-
|
|
2966
|
-
|
|
2967
|
-
|
|
3093
|
+
}
|
|
3094
|
+
function requestNavigation(record, request) {
|
|
3095
|
+
if (!isNavigableUrl(request.url))
|
|
3096
|
+
return record;
|
|
3097
|
+
return {
|
|
3098
|
+
state: "requested",
|
|
3099
|
+
request: {
|
|
3100
|
+
id: request.id ?? `nav${Date.now().toString(36)}`,
|
|
3101
|
+
at: request.at ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
3102
|
+
url: request.url,
|
|
3103
|
+
reason: request.reason,
|
|
3104
|
+
requester: request.requester
|
|
3105
|
+
},
|
|
3106
|
+
history: record.history
|
|
3107
|
+
};
|
|
3108
|
+
}
|
|
3109
|
+
function isPending(record) {
|
|
3110
|
+
return record.state === "requested" && record.request !== null;
|
|
3111
|
+
}
|
|
3112
|
+
function decide(record, granted, now = (/* @__PURE__ */ new Date()).toISOString()) {
|
|
3113
|
+
if (!isPending(record))
|
|
3114
|
+
return { record, navigateTo: null };
|
|
3115
|
+
const url = record.request.url;
|
|
3116
|
+
return {
|
|
3117
|
+
record: {
|
|
3118
|
+
state: granted ? "granted" : "denied",
|
|
3119
|
+
request: null,
|
|
3120
|
+
history: [...record.history, { url, decision: granted ? "granted" : "denied", at: now }]
|
|
3121
|
+
},
|
|
3122
|
+
navigateTo: granted ? url : null
|
|
3123
|
+
};
|
|
3124
|
+
}
|
|
3125
|
+
function withdraw(record) {
|
|
3126
|
+
return isPending(record) ? { ...record, state: "idle", request: null } : record;
|
|
3127
|
+
}
|
|
3128
|
+
|
|
3129
|
+
// ../shared/dist/thread.js
|
|
3130
|
+
function firstMessageFromComment(comment) {
|
|
3131
|
+
return {
|
|
3132
|
+
id: `${comment.id}:0`,
|
|
3133
|
+
threadId: comment.id,
|
|
3134
|
+
author: { ...comment.author, type: "user" },
|
|
3135
|
+
body: comment.body,
|
|
3136
|
+
attachments: comment.attachments?.length ? comment.attachments : void 0,
|
|
3137
|
+
createdAt: comment.createdAt
|
|
3138
|
+
};
|
|
3139
|
+
}
|
|
3140
|
+
function threadConversation(comment, replies) {
|
|
3141
|
+
return [firstMessageFromComment(comment), ...replies].sort((a, b) => a.createdAt.localeCompare(b.createdAt));
|
|
3142
|
+
}
|
|
3143
|
+
function iterationLabel(link) {
|
|
3144
|
+
if (!link.parentThreadId || link.iterationType !== "revision")
|
|
3145
|
+
return null;
|
|
3146
|
+
return `Iteration ${link.iterationNumber ?? 2}`;
|
|
3147
|
+
}
|
|
3148
|
+
|
|
3149
|
+
// ../shared/dist/timeline.js
|
|
3150
|
+
function describeTarget(input) {
|
|
3151
|
+
const parts = [];
|
|
3152
|
+
if (input.anchor?.tag)
|
|
3153
|
+
parts.push(`a <${input.anchor.tag}>`);
|
|
3154
|
+
if (input.anchor?.selector)
|
|
3155
|
+
parts.push(`\`${input.anchor.selector}\``);
|
|
3156
|
+
return parts.length ? parts.join(" ") : input.kind === "free" ? "a page note" : "an element";
|
|
3157
|
+
}
|
|
3158
|
+
function threadTimeline(input, messages = []) {
|
|
3159
|
+
const out = [];
|
|
3160
|
+
const latest = messages.length ? messages[messages.length - 1].at : input.createdAt;
|
|
3161
|
+
out.push({
|
|
3162
|
+
at: input.createdAt,
|
|
3163
|
+
kind: "captured",
|
|
3164
|
+
label: "Feedback captured",
|
|
3165
|
+
detail: [input.title, describeTarget(input)].filter(Boolean).join(" \xB7 ")
|
|
3166
|
+
});
|
|
3167
|
+
for (const m of messages) {
|
|
3168
|
+
out.push({
|
|
3169
|
+
at: m.at,
|
|
3170
|
+
kind: "message",
|
|
3171
|
+
label: `${m.authorName} replied`,
|
|
3172
|
+
actor: m.fromAgent ? m.authorName : void 0
|
|
3173
|
+
});
|
|
2968
3174
|
}
|
|
2969
|
-
|
|
2970
|
-
|
|
2971
|
-
|
|
2972
|
-
|
|
2973
|
-
|
|
2974
|
-
|
|
2975
|
-
|
|
2976
|
-
|
|
2977
|
-
|
|
2978
|
-
|
|
2979
|
-
|
|
2980
|
-
size: file.size
|
|
2981
|
-
};
|
|
2982
|
-
}
|
|
2983
|
-
/**
|
|
2984
|
-
* Keys that hold a comment list. `loupe:dock` (panel state) and `loupe:msgs:*`
|
|
2985
|
-
* (replies) share the prefix but are not comment lists — treating them as one made
|
|
2986
|
-
* update()/remove() throw, and would have it hunt a comment id among messages.
|
|
2987
|
-
*/
|
|
2988
|
-
commentKeys() {
|
|
2989
|
-
const keys = [];
|
|
2990
|
-
for (let i = 0; i < localStorage.length; i++) {
|
|
2991
|
-
const k = localStorage.key(i);
|
|
2992
|
-
if (k && k.startsWith("loupe:") && k !== "loupe:dock" && !k.startsWith("loupe:msgs:")) keys.push(k);
|
|
2993
|
-
}
|
|
2994
|
-
return keys;
|
|
3175
|
+
const stage = normalizeStage(input.status);
|
|
3176
|
+
const reached = (s) => STAGE_ORDER.indexOf(stage) >= STAGE_ORDER.indexOf(s);
|
|
3177
|
+
if (input.proposal || reached("in_review")) {
|
|
3178
|
+
const agent = input.proposal?.author ?? "Claude Code";
|
|
3179
|
+
out.push({
|
|
3180
|
+
at: input.proposal?.createdAt ?? latest,
|
|
3181
|
+
kind: "sent",
|
|
3182
|
+
label: `Change ready from ${agent}`,
|
|
3183
|
+
detail: "The rewritten markup is on the thread.",
|
|
3184
|
+
actor: agent
|
|
3185
|
+
});
|
|
2995
3186
|
}
|
|
2996
|
-
|
|
2997
|
-
|
|
2998
|
-
|
|
2999
|
-
|
|
3000
|
-
|
|
3001
|
-
|
|
3187
|
+
if (input.pr) {
|
|
3188
|
+
out.push({
|
|
3189
|
+
at: latest,
|
|
3190
|
+
kind: "pr",
|
|
3191
|
+
label: input.pr.number ? `Pull request #${input.pr.number} opened` : "Pull request opened",
|
|
3192
|
+
detail: input.pr.state === "merged" ? "Merged." : input.pr.state === "closed" ? "Closed without merging." : "Waiting on review."
|
|
3193
|
+
});
|
|
3194
|
+
if (input.pr.previewUrl) {
|
|
3195
|
+
out.push({ at: latest, kind: "preview", label: "Preview live", detail: input.pr.previewUrl });
|
|
3002
3196
|
}
|
|
3003
3197
|
}
|
|
3004
|
-
|
|
3005
|
-
|
|
3006
|
-
return `loupe:msgs:${threadId}`;
|
|
3198
|
+
if (reached("in_review")) {
|
|
3199
|
+
out.push({ at: latest, kind: "review", label: "Waiting on a human review", detail: "Only a person can close this." });
|
|
3007
3200
|
}
|
|
3008
|
-
|
|
3009
|
-
|
|
3010
|
-
const raw = localStorage.getItem(this.msgKey(threadId));
|
|
3011
|
-
const parsed = raw ? JSON.parse(raw) : [];
|
|
3012
|
-
return Array.isArray(parsed) ? parsed : [];
|
|
3013
|
-
} catch {
|
|
3014
|
-
return [];
|
|
3015
|
-
}
|
|
3201
|
+
if (stage === "resolved") {
|
|
3202
|
+
out.push({ at: latest, kind: "resolved", label: "Resolved", detail: "A person closed this." });
|
|
3016
3203
|
}
|
|
3017
|
-
|
|
3018
|
-
|
|
3019
|
-
|
|
3020
|
-
|
|
3021
|
-
|
|
3022
|
-
|
|
3023
|
-
|
|
3024
|
-
|
|
3025
|
-
|
|
3026
|
-
|
|
3027
|
-
|
|
3028
|
-
|
|
3204
|
+
return out;
|
|
3205
|
+
}
|
|
3206
|
+
var STAGE_ORDER = ["queue", "todo", "in_progress", "in_review", "resolved"];
|
|
3207
|
+
function normalizeStage(status) {
|
|
3208
|
+
if (status === "open")
|
|
3209
|
+
return "queue";
|
|
3210
|
+
if (status === "done")
|
|
3211
|
+
return "resolved";
|
|
3212
|
+
return status;
|
|
3213
|
+
}
|
|
3214
|
+
function threadAsText(input, messages = []) {
|
|
3215
|
+
const lines = [
|
|
3216
|
+
`# ${input.title ?? "Feedback"} (#${input.id})`,
|
|
3217
|
+
"",
|
|
3218
|
+
`- Stage: ${normalizeStage(input.status)}`,
|
|
3219
|
+
`- Page: ${input.url ?? "\u2014"}`
|
|
3220
|
+
];
|
|
3221
|
+
if (input.anchor?.selector)
|
|
3222
|
+
lines.push(`- Target: \`${input.anchor.selector}\``);
|
|
3223
|
+
if (input.pr?.number)
|
|
3224
|
+
lines.push(`- PR: #${input.pr.number}${input.pr.previewUrl ? ` \xB7 preview ${input.pr.previewUrl}` : ""}`);
|
|
3225
|
+
lines.push("", "## Request", "", input.body);
|
|
3226
|
+
if (messages.length) {
|
|
3227
|
+
lines.push("", "## Conversation", "");
|
|
3228
|
+
for (const m of messages)
|
|
3229
|
+
lines.push(`**${m.authorName}**${m.fromAgent ? " (agent)" : ""} \u2014 ${m.at}`, "", m.body, "");
|
|
3029
3230
|
}
|
|
3030
|
-
|
|
3031
|
-
|
|
3032
|
-
|
|
3033
|
-
|
|
3034
|
-
|
|
3035
|
-
|
|
3036
|
-
|
|
3037
|
-
|
|
3038
|
-
|
|
3039
|
-
|
|
3040
|
-
|
|
3041
|
-
|
|
3231
|
+
return lines.join("\n");
|
|
3232
|
+
}
|
|
3233
|
+
|
|
3234
|
+
// ../shared/dist/mentions.js
|
|
3235
|
+
var HANDLE = /^[A-Za-z0-9][A-Za-z0-9._-]*/;
|
|
3236
|
+
function excludedRanges(body) {
|
|
3237
|
+
const ranges = [];
|
|
3238
|
+
for (const m of body.matchAll(/```[\s\S]*?```/g))
|
|
3239
|
+
ranges.push([m.index, m.index + m[0].length]);
|
|
3240
|
+
const fenced = (i) => ranges.some(([a, b]) => i >= a && i < b);
|
|
3241
|
+
for (const m of body.matchAll(/`[^`\n]*`/g)) {
|
|
3242
|
+
if (!fenced(m.index))
|
|
3243
|
+
ranges.push([m.index, m.index + m[0].length]);
|
|
3042
3244
|
}
|
|
3043
|
-
|
|
3044
|
-
|
|
3245
|
+
return ranges;
|
|
3246
|
+
}
|
|
3247
|
+
function parseMentions(body) {
|
|
3248
|
+
if (!body)
|
|
3045
3249
|
return [];
|
|
3250
|
+
const excluded = excludedRanges(body);
|
|
3251
|
+
const inExcluded = (i) => excluded.some(([a, b]) => i >= a && i < b);
|
|
3252
|
+
const out = [];
|
|
3253
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3254
|
+
for (let i = 0; i < body.length; i++) {
|
|
3255
|
+
if (body[i] !== "@")
|
|
3256
|
+
continue;
|
|
3257
|
+
if (inExcluded(i))
|
|
3258
|
+
continue;
|
|
3259
|
+
const prev = i > 0 ? body[i - 1] : "";
|
|
3260
|
+
if (prev && !/[\s([{<"'*_~]/.test(prev))
|
|
3261
|
+
continue;
|
|
3262
|
+
const match = HANDLE.exec(body.slice(i + 1));
|
|
3263
|
+
if (!match)
|
|
3264
|
+
continue;
|
|
3265
|
+
const raw = match[0];
|
|
3266
|
+
const handle = raw.replace(/[._-]+$/, "");
|
|
3267
|
+
if (!handle)
|
|
3268
|
+
continue;
|
|
3269
|
+
const key = handle.toLowerCase();
|
|
3270
|
+
if (seen.has(key))
|
|
3271
|
+
continue;
|
|
3272
|
+
seen.add(key);
|
|
3273
|
+
out.push({ handle, start: i, length: handle.length + 1 });
|
|
3046
3274
|
}
|
|
3047
|
-
|
|
3048
|
-
|
|
3049
|
-
|
|
3050
|
-
|
|
3051
|
-
|
|
3052
|
-
|
|
3053
|
-
|
|
3054
|
-
|
|
3055
|
-
|
|
3056
|
-
|
|
3057
|
-
}
|
|
3058
|
-
}
|
|
3059
|
-
|
|
3060
|
-
async remove(id) {
|
|
3061
|
-
for (const k of this.commentKeys()) {
|
|
3062
|
-
const list = this.parseList(k);
|
|
3063
|
-
const next = list.filter((c) => c.id !== id);
|
|
3064
|
-
if (next.length !== list.length) {
|
|
3065
|
-
localStorage.setItem(k, JSON.stringify(next));
|
|
3066
|
-
return;
|
|
3067
|
-
}
|
|
3068
|
-
}
|
|
3275
|
+
return out;
|
|
3276
|
+
}
|
|
3277
|
+
function mentionSegments(body, mentions) {
|
|
3278
|
+
if (!mentions.length)
|
|
3279
|
+
return [{ text: body, mention: false }];
|
|
3280
|
+
const sorted = [...mentions].sort((a, b) => a.start - b.start);
|
|
3281
|
+
const out = [];
|
|
3282
|
+
let at = 0;
|
|
3283
|
+
for (const m of sorted) {
|
|
3284
|
+
if (m.start > at)
|
|
3285
|
+
out.push({ text: body.slice(at, m.start), mention: false });
|
|
3286
|
+
out.push({ text: body.slice(m.start, m.start + m.length), mention: true });
|
|
3287
|
+
at = m.start + m.length;
|
|
3069
3288
|
}
|
|
3070
|
-
|
|
3289
|
+
if (at < body.length)
|
|
3290
|
+
out.push({ text: body.slice(at), mention: false });
|
|
3291
|
+
return out;
|
|
3292
|
+
}
|
|
3293
|
+
function mentionSuggestions(body, caret, candidates) {
|
|
3294
|
+
const before = body.slice(0, caret);
|
|
3295
|
+
const at = before.lastIndexOf("@");
|
|
3296
|
+
if (at < 0)
|
|
3297
|
+
return [];
|
|
3298
|
+
const typed = before.slice(at + 1);
|
|
3299
|
+
if (/\s/.test(typed))
|
|
3300
|
+
return [];
|
|
3301
|
+
const q = typed.toLowerCase();
|
|
3302
|
+
return candidates.filter((c) => !q || c.name.toLowerCase().includes(q) || c.id.toLowerCase().includes(q)).slice(0, 6);
|
|
3303
|
+
}
|
|
3071
3304
|
|
|
3072
|
-
//
|
|
3073
|
-
var
|
|
3074
|
-
|
|
3075
|
-
|
|
3076
|
-
|
|
3077
|
-
|
|
3078
|
-
|
|
3079
|
-
|
|
3080
|
-
|
|
3081
|
-
|
|
3082
|
-
|
|
3083
|
-
|
|
3084
|
-
|
|
3085
|
-
|
|
3086
|
-
|
|
3305
|
+
// ../shared/dist/needs-you.js
|
|
3306
|
+
var NEEDS_YOU_LABELS = {
|
|
3307
|
+
review: "Waiting on your review",
|
|
3308
|
+
question: "The agent asked you something",
|
|
3309
|
+
failed: "The agent's run failed"
|
|
3310
|
+
};
|
|
3311
|
+
function looksLikeQuestion(body) {
|
|
3312
|
+
const lines = body.split("\n").map((l) => l.trim()).filter(Boolean);
|
|
3313
|
+
const last = lines[lines.length - 1];
|
|
3314
|
+
if (!last)
|
|
3315
|
+
return false;
|
|
3316
|
+
return /\?["')\]]*$/.test(last);
|
|
3317
|
+
}
|
|
3318
|
+
function needsYou(input) {
|
|
3319
|
+
const stage = normalizeStage2(input.status);
|
|
3320
|
+
if (stage === "resolved")
|
|
3321
|
+
return { needs: false };
|
|
3322
|
+
if (input.agentFailed)
|
|
3323
|
+
return { needs: true, reason: "failed", label: NEEDS_YOU_LABELS.failed };
|
|
3324
|
+
if (input.last?.fromAgent && looksLikeQuestion(input.last.body)) {
|
|
3325
|
+
return { needs: true, reason: "question", label: NEEDS_YOU_LABELS.question };
|
|
3087
3326
|
}
|
|
3088
|
-
|
|
3089
|
-
|
|
3090
|
-
|
|
3327
|
+
if (stage === "in_review")
|
|
3328
|
+
return { needs: true, reason: "review", label: NEEDS_YOU_LABELS.review };
|
|
3329
|
+
return { needs: false };
|
|
3330
|
+
}
|
|
3331
|
+
function normalizeStage2(status) {
|
|
3332
|
+
if (status === "open")
|
|
3333
|
+
return "queue";
|
|
3334
|
+
if (status === "done")
|
|
3335
|
+
return "resolved";
|
|
3336
|
+
return status;
|
|
3337
|
+
}
|
|
3338
|
+
|
|
3339
|
+
// ../shared/dist/reactions.js
|
|
3340
|
+
var REACTION_CHOICES = ["\u{1F44D}", "\u{1F389}", "\u{1F440}", "\u{1F64F}", "\u2764\uFE0F", "\u{1F680}"];
|
|
3341
|
+
function summarizeReactions(reactions, viewerId) {
|
|
3342
|
+
const byEmoji = /* @__PURE__ */ new Map();
|
|
3343
|
+
for (const r of reactions) {
|
|
3344
|
+
const list = byEmoji.get(r.emoji) ?? [];
|
|
3345
|
+
list.push(r);
|
|
3346
|
+
byEmoji.set(r.emoji, list);
|
|
3347
|
+
}
|
|
3348
|
+
const rank = (emoji) => {
|
|
3349
|
+
const at = REACTION_CHOICES.indexOf(emoji);
|
|
3350
|
+
return at < 0 ? REACTION_CHOICES.length : at;
|
|
3351
|
+
};
|
|
3352
|
+
return [...byEmoji.entries()].map(([emoji, list]) => ({
|
|
3353
|
+
emoji,
|
|
3354
|
+
count: list.length,
|
|
3355
|
+
mine: viewerId ? list.some((r) => r.userId === viewerId) : false,
|
|
3356
|
+
users: list.map((r) => r.userName ?? r.userId)
|
|
3357
|
+
})).sort((a, b) => b.count - a.count || rank(a.emoji) - rank(b.emoji) || a.emoji.localeCompare(b.emoji));
|
|
3358
|
+
}
|
|
3359
|
+
function toggleReaction(reactions, next) {
|
|
3360
|
+
const has = reactions.some((r) => r.messageId === next.messageId && r.emoji === next.emoji && r.userId === next.userId);
|
|
3361
|
+
if (has) {
|
|
3362
|
+
return reactions.filter((r) => !(r.messageId === next.messageId && r.emoji === next.emoji && r.userId === next.userId));
|
|
3091
3363
|
}
|
|
3092
|
-
|
|
3093
|
-
|
|
3094
|
-
|
|
3364
|
+
return [...reactions, next];
|
|
3365
|
+
}
|
|
3366
|
+
|
|
3367
|
+
// ../shared/dist/presence.js
|
|
3368
|
+
var PEER_HEARTBEAT_MS = 6e3;
|
|
3369
|
+
function initialsOf(name) {
|
|
3370
|
+
const words = name.trim().split(/\s+/).filter(Boolean);
|
|
3371
|
+
if (!words.length)
|
|
3372
|
+
return "?";
|
|
3373
|
+
if (words.length === 1)
|
|
3374
|
+
return words[0].slice(0, 1).toUpperCase();
|
|
3375
|
+
return (words[0].slice(0, 1) + words[words.length - 1].slice(0, 1)).toUpperCase();
|
|
3376
|
+
}
|
|
3377
|
+
|
|
3378
|
+
// ../shared/dist/index.js
|
|
3379
|
+
var COMMENT_STAGES = ["queue", "todo", "in_progress", "in_review", "resolved"];
|
|
3380
|
+
var STAGE_LABELS = {
|
|
3381
|
+
queue: "Queue",
|
|
3382
|
+
todo: "To Do",
|
|
3383
|
+
in_progress: "In Progress",
|
|
3384
|
+
in_review: "In Review",
|
|
3385
|
+
resolved: "Resolved"
|
|
3386
|
+
};
|
|
3387
|
+
var LEGACY_STATUS = {
|
|
3388
|
+
open: "queue",
|
|
3389
|
+
in_progress: "in_progress",
|
|
3390
|
+
done: "resolved"
|
|
3391
|
+
};
|
|
3392
|
+
function normalizeStatus(value) {
|
|
3393
|
+
if (typeof value === "string") {
|
|
3394
|
+
if (COMMENT_STAGES.includes(value))
|
|
3395
|
+
return value;
|
|
3396
|
+
if (value in LEGACY_STATUS)
|
|
3397
|
+
return LEGACY_STATUS[value];
|
|
3398
|
+
}
|
|
3399
|
+
return "queue";
|
|
3400
|
+
}
|
|
3401
|
+
var COMMENT_PRIORITIES = ["critical", "high", "medium", "low"];
|
|
3402
|
+
var PRIORITY_LABELS = {
|
|
3403
|
+
critical: "Critical",
|
|
3404
|
+
high: "High",
|
|
3405
|
+
medium: "Medium",
|
|
3406
|
+
low: "Low"
|
|
3407
|
+
};
|
|
3408
|
+
var CHANGE_TYPES = ["frontend", "backend", "api", "other"];
|
|
3409
|
+
var CHANGE_TYPE_LABELS = {
|
|
3410
|
+
frontend: "Frontend",
|
|
3411
|
+
backend: "Backend",
|
|
3412
|
+
api: "API",
|
|
3413
|
+
other: "Other"
|
|
3414
|
+
};
|
|
3415
|
+
var DEFAULT_PRIORITY = "medium";
|
|
3416
|
+
var DEFAULT_CHANGE_TYPE = "other";
|
|
3417
|
+
function normalizePriority(value) {
|
|
3418
|
+
if (typeof value === "string" && COMMENT_PRIORITIES.includes(value)) {
|
|
3419
|
+
return value;
|
|
3420
|
+
}
|
|
3421
|
+
return DEFAULT_PRIORITY;
|
|
3422
|
+
}
|
|
3423
|
+
|
|
3424
|
+
// src/store.ts
|
|
3425
|
+
var LocalStorageAdapter = class {
|
|
3426
|
+
key(projectKey, url) {
|
|
3427
|
+
return `loupe:${projectKey}:${url}`;
|
|
3428
|
+
}
|
|
3429
|
+
readAll(projectKey, url) {
|
|
3430
|
+
try {
|
|
3431
|
+
const raw = localStorage.getItem(this.key(projectKey, url));
|
|
3432
|
+
return raw ? JSON.parse(raw) : [];
|
|
3433
|
+
} catch {
|
|
3434
|
+
return [];
|
|
3435
|
+
}
|
|
3436
|
+
}
|
|
3437
|
+
writeAll(projectKey, url, comments) {
|
|
3438
|
+
localStorage.setItem(this.key(projectKey, url), JSON.stringify(comments));
|
|
3439
|
+
}
|
|
3440
|
+
async list(projectKey, url) {
|
|
3441
|
+
return this.readAll(projectKey, url);
|
|
3442
|
+
}
|
|
3443
|
+
/** Every page's comments for this project, newest first — the "All" scope. */
|
|
3444
|
+
async listAll(projectKey) {
|
|
3445
|
+
const prefix = `loupe:${projectKey}:`;
|
|
3446
|
+
const out = [];
|
|
3447
|
+
for (let i = 0; i < localStorage.length; i++) {
|
|
3448
|
+
const k = localStorage.key(i);
|
|
3449
|
+
if (!k || !k.startsWith(prefix)) continue;
|
|
3450
|
+
try {
|
|
3451
|
+
out.push(...JSON.parse(localStorage.getItem(k) || "[]"));
|
|
3452
|
+
} catch {
|
|
3453
|
+
}
|
|
3454
|
+
}
|
|
3455
|
+
return out.sort((a, b) => b.createdAt.localeCompare(a.createdAt));
|
|
3456
|
+
}
|
|
3457
|
+
async save(comment) {
|
|
3458
|
+
const all = this.readAll(comment.projectKey, comment.url);
|
|
3459
|
+
all.push(comment);
|
|
3460
|
+
this.writeAll(comment.projectKey, comment.url, all);
|
|
3461
|
+
return comment;
|
|
3462
|
+
}
|
|
3463
|
+
/**
|
|
3464
|
+
* Offline mode: keep the file inline. localStorage is only a few MB, so refuse
|
|
3465
|
+
* anything that would blow the quota rather than silently dropping it.
|
|
3466
|
+
*/
|
|
3467
|
+
async upload(_projectKey, file) {
|
|
3468
|
+
if (file.size > 3e6) throw new Error("attachment too large for offline mode");
|
|
3469
|
+
return {
|
|
3470
|
+
url: await fileToDataUrl(file),
|
|
3471
|
+
name: file.name,
|
|
3472
|
+
mime: file.type || void 0,
|
|
3473
|
+
kind: attachmentKind(file.type),
|
|
3474
|
+
size: file.size
|
|
3475
|
+
};
|
|
3476
|
+
}
|
|
3477
|
+
/**
|
|
3478
|
+
* Keys that hold a comment list. `loupe:dock` (panel state) and `loupe:msgs:*`
|
|
3479
|
+
* (replies) share the prefix but are not comment lists — treating them as one made
|
|
3480
|
+
* update()/remove() throw, and would have it hunt a comment id among messages.
|
|
3481
|
+
*/
|
|
3482
|
+
commentKeys() {
|
|
3483
|
+
const keys = [];
|
|
3484
|
+
for (let i = 0; i < localStorage.length; i++) {
|
|
3485
|
+
const k = localStorage.key(i);
|
|
3486
|
+
if (k && k.startsWith("loupe:") && k !== "loupe:dock" && !k.startsWith("loupe:msgs:")) keys.push(k);
|
|
3487
|
+
}
|
|
3488
|
+
return keys;
|
|
3489
|
+
}
|
|
3490
|
+
parseList(key) {
|
|
3491
|
+
try {
|
|
3492
|
+
const value = JSON.parse(localStorage.getItem(key) || "[]");
|
|
3493
|
+
return Array.isArray(value) ? value : [];
|
|
3494
|
+
} catch {
|
|
3495
|
+
return [];
|
|
3496
|
+
}
|
|
3497
|
+
}
|
|
3498
|
+
/** Replies, kept under their own key so they survive a comment being re-saved. */
|
|
3499
|
+
msgKey(threadId) {
|
|
3500
|
+
return `loupe:msgs:${threadId}`;
|
|
3501
|
+
}
|
|
3502
|
+
async listMessages(threadId) {
|
|
3503
|
+
try {
|
|
3504
|
+
const raw = localStorage.getItem(this.msgKey(threadId));
|
|
3505
|
+
const parsed = raw ? JSON.parse(raw) : [];
|
|
3506
|
+
return Array.isArray(parsed) ? parsed : [];
|
|
3507
|
+
} catch {
|
|
3508
|
+
return [];
|
|
3509
|
+
}
|
|
3510
|
+
}
|
|
3511
|
+
async addMessage(threadId, message) {
|
|
3512
|
+
const stored = {
|
|
3513
|
+
id: `m${Date.now().toString(36)}${Math.random().toString(36).slice(2, 6)}`,
|
|
3514
|
+
threadId,
|
|
3515
|
+
author: message.author,
|
|
3516
|
+
body: message.body,
|
|
3517
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
3518
|
+
};
|
|
3519
|
+
const all = await this.listMessages(threadId);
|
|
3520
|
+
all.push(stored);
|
|
3521
|
+
localStorage.setItem(this.msgKey(threadId), JSON.stringify(all));
|
|
3522
|
+
return stored;
|
|
3523
|
+
}
|
|
3524
|
+
/** Offline reactions, keyed per thread. */
|
|
3525
|
+
async listReactions(threadId) {
|
|
3526
|
+
try {
|
|
3527
|
+
const raw = localStorage.getItem(`loupe:rxn:${threadId}`);
|
|
3528
|
+
const parsed = raw ? JSON.parse(raw) : [];
|
|
3529
|
+
return Array.isArray(parsed) ? parsed : [];
|
|
3530
|
+
} catch {
|
|
3531
|
+
return [];
|
|
3532
|
+
}
|
|
3533
|
+
}
|
|
3534
|
+
async toggleReaction(input) {
|
|
3535
|
+
const current2 = await this.listReactions(input.threadId);
|
|
3536
|
+
const next = toggleReaction(current2, {
|
|
3537
|
+
messageId: input.messageId,
|
|
3538
|
+
emoji: input.emoji,
|
|
3539
|
+
userId: input.userId,
|
|
3540
|
+
userName: input.userName
|
|
3541
|
+
});
|
|
3542
|
+
localStorage.setItem(`loupe:rxn:${input.threadId}`, JSON.stringify(next));
|
|
3543
|
+
return next;
|
|
3544
|
+
}
|
|
3545
|
+
/** Offline: the people are whoever has already commented locally. */
|
|
3546
|
+
async listPeople(projectKey) {
|
|
3547
|
+
const prefix = `loupe:${projectKey}:`;
|
|
3548
|
+
const seen = /* @__PURE__ */ new Map();
|
|
3549
|
+
for (let i = 0; i < localStorage.length; i++) {
|
|
3550
|
+
const k = localStorage.key(i);
|
|
3551
|
+
if (!k || !k.startsWith(prefix)) continue;
|
|
3552
|
+
for (const c of this.parseList(k)) {
|
|
3553
|
+
if (c.author?.id && !seen.has(c.author.id)) seen.set(c.author.id, c.author);
|
|
3554
|
+
}
|
|
3555
|
+
}
|
|
3556
|
+
return [...seen.values()];
|
|
3557
|
+
}
|
|
3558
|
+
/** Offline mode has no server to notify anyone from. */
|
|
3559
|
+
async listNotifications() {
|
|
3560
|
+
return [];
|
|
3561
|
+
}
|
|
3562
|
+
async markNotificationsRead() {
|
|
3563
|
+
}
|
|
3564
|
+
async update(id, patch) {
|
|
3565
|
+
for (const k of this.commentKeys()) {
|
|
3566
|
+
const list = this.parseList(k);
|
|
3567
|
+
const idx = list.findIndex((c) => c.id === id);
|
|
3568
|
+
if (idx >= 0) {
|
|
3569
|
+
list[idx] = { ...list[idx], ...patch };
|
|
3570
|
+
localStorage.setItem(k, JSON.stringify(list));
|
|
3571
|
+
return;
|
|
3572
|
+
}
|
|
3573
|
+
}
|
|
3574
|
+
}
|
|
3575
|
+
async remove(id) {
|
|
3576
|
+
for (const k of this.commentKeys()) {
|
|
3577
|
+
const list = this.parseList(k);
|
|
3578
|
+
const next = list.filter((c) => c.id !== id);
|
|
3579
|
+
if (next.length !== list.length) {
|
|
3580
|
+
localStorage.setItem(k, JSON.stringify(next));
|
|
3581
|
+
return;
|
|
3582
|
+
}
|
|
3583
|
+
}
|
|
3584
|
+
}
|
|
3585
|
+
};
|
|
3586
|
+
|
|
3587
|
+
// src/http-adapter.ts
|
|
3588
|
+
var HttpAdapter = class {
|
|
3589
|
+
constructor(base, user, userHmac, extraHeaders, credentials) {
|
|
3590
|
+
this.base = base;
|
|
3591
|
+
this.user = user;
|
|
3592
|
+
this.userHmac = userHmac;
|
|
3593
|
+
this.extraHeaders = extraHeaders;
|
|
3594
|
+
this.credentials = credentials;
|
|
3595
|
+
this.base = base.replace(/\/$/, "");
|
|
3596
|
+
}
|
|
3597
|
+
headers() {
|
|
3598
|
+
const h = { "Content-Type": "application/json", "X-Loupe-User": this.user.id };
|
|
3599
|
+
if (this.userHmac) h["X-Loupe-Hmac"] = this.userHmac;
|
|
3600
|
+
if (this.extraHeaders) Object.assign(h, this.extraHeaders);
|
|
3601
|
+
return h;
|
|
3602
|
+
}
|
|
3603
|
+
/** Base fetch options shared by every request (credentials mode, if set). */
|
|
3604
|
+
opts(init2) {
|
|
3605
|
+
return this.credentials ? { credentials: this.credentials, ...init2 } : { ...init2 };
|
|
3606
|
+
}
|
|
3607
|
+
async list(projectKey, url) {
|
|
3608
|
+
const q = new URLSearchParams({ projectKey, url });
|
|
3609
|
+
const res = await fetch(`${this.base}/v1/comments?${q}`, this.opts({ headers: this.headers() }));
|
|
3095
3610
|
if (!res.ok) throw new Error(`list failed: ${res.status}`);
|
|
3096
3611
|
return await res.json();
|
|
3097
3612
|
}
|
|
@@ -3148,471 +3663,71 @@ var Loupe = (() => {
|
|
|
3148
3663
|
return await res.json();
|
|
3149
3664
|
}
|
|
3150
3665
|
async addMessage(threadId, message) {
|
|
3151
|
-
const res = await fetch(`${this.base}/v1/comments/${encodeURIComponent(threadId)}/messages`, this.opts({
|
|
3152
|
-
method: "POST",
|
|
3153
|
-
headers: this.headers(),
|
|
3154
|
-
body: JSON.stringify(message)
|
|
3155
|
-
}));
|
|
3156
|
-
if (!res.ok) throw new Error(`addMessage failed: ${res.status}`);
|
|
3157
|
-
return await res.json();
|
|
3158
|
-
}
|
|
3159
|
-
/** Everyone who has taken part in this project, for mention resolution. */
|
|
3160
|
-
async listPeople(projectKey) {
|
|
3161
|
-
const q = new URLSearchParams({ projectKey });
|
|
3162
|
-
const res = await fetch(`${this.base}/v1/people?${q}`, this.opts({ headers: this.headers() }));
|
|
3163
|
-
if (!res.ok) throw new Error(`listPeople failed: ${res.status}`);
|
|
3164
|
-
return await res.json();
|
|
3165
|
-
}
|
|
3166
|
-
async listNotifications(projectKey, recipient) {
|
|
3167
|
-
const q = new URLSearchParams({ projectKey, recipient });
|
|
3168
|
-
const res = await fetch(`${this.base}/v1/notifications?${q}`, this.opts({ headers: this.headers() }));
|
|
3169
|
-
if (!res.ok) throw new Error(`listNotifications failed: ${res.status}`);
|
|
3170
|
-
return (await res.json()).notifications;
|
|
3171
|
-
}
|
|
3172
|
-
async markNotificationsRead(projectKey, recipient, id) {
|
|
3173
|
-
await fetch(`${this.base}/v1/notifications/read`, this.opts({
|
|
3174
|
-
method: "POST",
|
|
3175
|
-
headers: this.headers(),
|
|
3176
|
-
body: JSON.stringify({ projectKey, recipient, id })
|
|
3177
|
-
}));
|
|
3178
|
-
}
|
|
3179
|
-
async update(id, patch) {
|
|
3180
|
-
const res = await fetch(`${this.base}/v1/comments/${encodeURIComponent(id)}`, this.opts({
|
|
3181
|
-
method: "PATCH",
|
|
3182
|
-
headers: this.headers(),
|
|
3183
|
-
body: JSON.stringify(patch)
|
|
3184
|
-
}));
|
|
3185
|
-
if (!res.ok) throw new Error(`update failed: ${res.status}`);
|
|
3186
|
-
}
|
|
3187
|
-
async remove(id) {
|
|
3188
|
-
const res = await fetch(`${this.base}/v1/comments/${encodeURIComponent(id)}`, this.opts({
|
|
3189
|
-
method: "DELETE",
|
|
3190
|
-
headers: this.headers()
|
|
3191
|
-
}));
|
|
3192
|
-
if (!res.ok && res.status !== 404) throw new Error(`remove failed: ${res.status}`);
|
|
3193
|
-
}
|
|
3194
|
-
};
|
|
3195
|
-
|
|
3196
|
-
// ../shared/dist/activity.js
|
|
3197
|
-
var ACTIVITY_STATUS_LABELS = {
|
|
3198
|
-
idle: "Idle",
|
|
3199
|
-
working: "Working",
|
|
3200
|
-
error: "Error"
|
|
3201
|
-
};
|
|
3202
|
-
function formatDuration(ms) {
|
|
3203
|
-
if (!ms || ms < 0)
|
|
3204
|
-
return "\u2014";
|
|
3205
|
-
const s = Math.round(ms / 1e3);
|
|
3206
|
-
if (s < 60)
|
|
3207
|
-
return `${s}s`;
|
|
3208
|
-
const m = Math.floor(s / 60);
|
|
3209
|
-
const rest = s % 60;
|
|
3210
|
-
if (m < 60)
|
|
3211
|
-
return rest ? `${m}m ${rest}s` : `${m}m`;
|
|
3212
|
-
const h = Math.floor(m / 60);
|
|
3213
|
-
return `${h}h ${m % 60}m`;
|
|
3214
|
-
}
|
|
3215
|
-
function summarizeActivity(events, status = "idle", now = Date.now()) {
|
|
3216
|
-
const files = /* @__PURE__ */ new Set();
|
|
3217
|
-
const kinds = /* @__PURE__ */ new Map();
|
|
3218
|
-
let errors = 0;
|
|
3219
|
-
let first = Infinity;
|
|
3220
|
-
for (const e of events) {
|
|
3221
|
-
for (const f of e.files ?? [])
|
|
3222
|
-
files.add(f);
|
|
3223
|
-
kinds.set(e.kind, (kinds.get(e.kind) ?? 0) + 1);
|
|
3224
|
-
if (e.level === "error")
|
|
3225
|
-
errors++;
|
|
3226
|
-
const t = Date.parse(e.at);
|
|
3227
|
-
if (!Number.isNaN(t))
|
|
3228
|
-
first = Math.min(first, t);
|
|
3229
|
-
}
|
|
3230
|
-
return {
|
|
3231
|
-
status,
|
|
3232
|
-
events: events.length,
|
|
3233
|
-
errors,
|
|
3234
|
-
files: files.size,
|
|
3235
|
-
durationMs: events.length && first !== Infinity ? Math.max(0, now - first) : 0,
|
|
3236
|
-
byKind: [...kinds.entries()].map(([kind, count]) => ({ kind, count })).sort((a, b) => b.count - a.count || a.kind.localeCompare(b.kind))
|
|
3237
|
-
};
|
|
3238
|
-
}
|
|
3239
|
-
|
|
3240
|
-
// ../shared/dist/lifecycle.js
|
|
3241
|
-
var LIFECYCLE_LABELS = {
|
|
3242
|
-
sent: "Sent to agent",
|
|
3243
|
-
in_pr: "In PR",
|
|
3244
|
-
preview: "Review preview",
|
|
3245
|
-
reviewed: "Reviewed"
|
|
3246
|
-
};
|
|
3247
|
-
function lifecycle(c) {
|
|
3248
|
-
const hasProposal = !!c.proposal;
|
|
3249
|
-
const pr = c.pr ?? void 0;
|
|
3250
|
-
const withPr = (stage) => {
|
|
3251
|
-
const out = { stage, label: LIFECYCLE_LABELS[stage] };
|
|
3252
|
-
if (pr) {
|
|
3253
|
-
out.pr = pr;
|
|
3254
|
-
if (typeof pr.checksTotal === "number" && pr.checksTotal > 0) {
|
|
3255
|
-
const passed = Math.max(0, Math.min(pr.checksTotal, pr.checksPassed ?? 0));
|
|
3256
|
-
out.checks = {
|
|
3257
|
-
text: `${passed}/${pr.checksTotal}`,
|
|
3258
|
-
ratio: passed / pr.checksTotal
|
|
3259
|
-
};
|
|
3260
|
-
}
|
|
3261
|
-
}
|
|
3262
|
-
return out;
|
|
3263
|
-
};
|
|
3264
|
-
if (c.status === "resolved")
|
|
3265
|
-
return hasProposal || pr ? withPr("reviewed") : null;
|
|
3266
|
-
if (pr)
|
|
3267
|
-
return withPr("in_pr");
|
|
3268
|
-
if (c.status === "in_review")
|
|
3269
|
-
return { stage: "preview", label: LIFECYCLE_LABELS.preview };
|
|
3270
|
-
if (hasProposal)
|
|
3271
|
-
return { stage: "sent", label: LIFECYCLE_LABELS.sent };
|
|
3272
|
-
return null;
|
|
3273
|
-
}
|
|
3274
|
-
function awaitingReview(comments) {
|
|
3275
|
-
return comments.filter((c) => c.status === "in_review");
|
|
3276
|
-
}
|
|
3277
|
-
|
|
3278
|
-
// ../shared/dist/iteration.js
|
|
3279
|
-
function emptyIterations() {
|
|
3280
|
-
return { items: [], index: -1 };
|
|
3281
|
-
}
|
|
3282
|
-
function current(state) {
|
|
3283
|
-
return state.index >= 0 ? state.items[state.index] ?? null : null;
|
|
3284
|
-
}
|
|
3285
|
-
function addIteration(state, iteration) {
|
|
3286
|
-
const head = state.items.slice(0, state.index + 1);
|
|
3287
|
-
const items = [...head, iteration].slice(-MAX_ITERATIONS);
|
|
3288
|
-
return { items, index: items.length - 1 };
|
|
3289
|
-
}
|
|
3290
|
-
var MAX_ITERATIONS = 20;
|
|
3291
|
-
function canUndo(state) {
|
|
3292
|
-
return state.items.length > 0;
|
|
3293
|
-
}
|
|
3294
|
-
function undo(state) {
|
|
3295
|
-
if (!state.items.length)
|
|
3296
|
-
return state;
|
|
3297
|
-
const items = state.items.slice(0, -1);
|
|
3298
|
-
return { items, index: items.length - 1 };
|
|
3299
|
-
}
|
|
3300
|
-
function canMove(state, delta) {
|
|
3301
|
-
const next = state.index + delta;
|
|
3302
|
-
return next >= 0 && next < state.items.length;
|
|
3303
|
-
}
|
|
3304
|
-
function move(state, delta) {
|
|
3305
|
-
return canMove(state, delta) ? { ...state, index: state.index + delta } : state;
|
|
3306
|
-
}
|
|
3307
|
-
function stackLabel(state) {
|
|
3308
|
-
return state.items.length ? `${state.index + 1} / ${state.items.length}` : "";
|
|
3309
|
-
}
|
|
3310
|
-
|
|
3311
|
-
// ../shared/dist/consent.js
|
|
3312
|
-
function emptyConsent() {
|
|
3313
|
-
return { state: "idle", request: null, history: [] };
|
|
3314
|
-
}
|
|
3315
|
-
function isNavigableUrl(raw) {
|
|
3316
|
-
try {
|
|
3317
|
-
const u = new URL(raw);
|
|
3318
|
-
return u.protocol === "http:" || u.protocol === "https:";
|
|
3319
|
-
} catch {
|
|
3320
|
-
return false;
|
|
3321
|
-
}
|
|
3322
|
-
}
|
|
3323
|
-
function requestNavigation(record, request) {
|
|
3324
|
-
if (!isNavigableUrl(request.url))
|
|
3325
|
-
return record;
|
|
3326
|
-
return {
|
|
3327
|
-
state: "requested",
|
|
3328
|
-
request: {
|
|
3329
|
-
id: request.id ?? `nav${Date.now().toString(36)}`,
|
|
3330
|
-
at: request.at ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
3331
|
-
url: request.url,
|
|
3332
|
-
reason: request.reason,
|
|
3333
|
-
requester: request.requester
|
|
3334
|
-
},
|
|
3335
|
-
history: record.history
|
|
3336
|
-
};
|
|
3337
|
-
}
|
|
3338
|
-
function isPending(record) {
|
|
3339
|
-
return record.state === "requested" && record.request !== null;
|
|
3340
|
-
}
|
|
3341
|
-
function decide(record, granted, now = (/* @__PURE__ */ new Date()).toISOString()) {
|
|
3342
|
-
if (!isPending(record))
|
|
3343
|
-
return { record, navigateTo: null };
|
|
3344
|
-
const url = record.request.url;
|
|
3345
|
-
return {
|
|
3346
|
-
record: {
|
|
3347
|
-
state: granted ? "granted" : "denied",
|
|
3348
|
-
request: null,
|
|
3349
|
-
history: [...record.history, { url, decision: granted ? "granted" : "denied", at: now }]
|
|
3350
|
-
},
|
|
3351
|
-
navigateTo: granted ? url : null
|
|
3352
|
-
};
|
|
3353
|
-
}
|
|
3354
|
-
function withdraw(record) {
|
|
3355
|
-
return isPending(record) ? { ...record, state: "idle", request: null } : record;
|
|
3356
|
-
}
|
|
3357
|
-
|
|
3358
|
-
// ../shared/dist/thread.js
|
|
3359
|
-
function firstMessageFromComment(comment) {
|
|
3360
|
-
return {
|
|
3361
|
-
id: `${comment.id}:0`,
|
|
3362
|
-
threadId: comment.id,
|
|
3363
|
-
author: { ...comment.author, type: "user" },
|
|
3364
|
-
body: comment.body,
|
|
3365
|
-
attachments: comment.attachments?.length ? comment.attachments : void 0,
|
|
3366
|
-
createdAt: comment.createdAt
|
|
3367
|
-
};
|
|
3368
|
-
}
|
|
3369
|
-
function threadConversation(comment, replies) {
|
|
3370
|
-
return [firstMessageFromComment(comment), ...replies].sort((a, b) => a.createdAt.localeCompare(b.createdAt));
|
|
3371
|
-
}
|
|
3372
|
-
function iterationLabel(link) {
|
|
3373
|
-
if (!link.parentThreadId || link.iterationType !== "revision")
|
|
3374
|
-
return null;
|
|
3375
|
-
return `Iteration ${link.iterationNumber ?? 2}`;
|
|
3376
|
-
}
|
|
3377
|
-
|
|
3378
|
-
// ../shared/dist/timeline.js
|
|
3379
|
-
function describeTarget(input) {
|
|
3380
|
-
const parts = [];
|
|
3381
|
-
if (input.anchor?.tag)
|
|
3382
|
-
parts.push(`a <${input.anchor.tag}>`);
|
|
3383
|
-
if (input.anchor?.selector)
|
|
3384
|
-
parts.push(`\`${input.anchor.selector}\``);
|
|
3385
|
-
return parts.length ? parts.join(" ") : input.kind === "free" ? "a page note" : "an element";
|
|
3386
|
-
}
|
|
3387
|
-
function threadTimeline(input, messages = []) {
|
|
3388
|
-
const out = [];
|
|
3389
|
-
const latest = messages.length ? messages[messages.length - 1].at : input.createdAt;
|
|
3390
|
-
out.push({
|
|
3391
|
-
at: input.createdAt,
|
|
3392
|
-
kind: "captured",
|
|
3393
|
-
label: "Feedback captured",
|
|
3394
|
-
detail: [input.title, describeTarget(input)].filter(Boolean).join(" \xB7 ")
|
|
3395
|
-
});
|
|
3396
|
-
for (const m of messages) {
|
|
3397
|
-
out.push({
|
|
3398
|
-
at: m.at,
|
|
3399
|
-
kind: "message",
|
|
3400
|
-
label: `${m.authorName} replied`,
|
|
3401
|
-
actor: m.fromAgent ? m.authorName : void 0
|
|
3402
|
-
});
|
|
3403
|
-
}
|
|
3404
|
-
const stage = normalizeStage(input.status);
|
|
3405
|
-
const reached = (s) => STAGE_ORDER.indexOf(stage) >= STAGE_ORDER.indexOf(s);
|
|
3406
|
-
if (input.proposal || reached("in_review")) {
|
|
3407
|
-
const agent = input.proposal?.author ?? "Claude Code";
|
|
3408
|
-
out.push({
|
|
3409
|
-
at: input.proposal?.createdAt ?? latest,
|
|
3410
|
-
kind: "sent",
|
|
3411
|
-
label: `Change ready from ${agent}`,
|
|
3412
|
-
detail: "The rewritten markup is on the thread.",
|
|
3413
|
-
actor: agent
|
|
3414
|
-
});
|
|
3415
|
-
}
|
|
3416
|
-
if (input.pr) {
|
|
3417
|
-
out.push({
|
|
3418
|
-
at: latest,
|
|
3419
|
-
kind: "pr",
|
|
3420
|
-
label: input.pr.number ? `Pull request #${input.pr.number} opened` : "Pull request opened",
|
|
3421
|
-
detail: input.pr.state === "merged" ? "Merged." : input.pr.state === "closed" ? "Closed without merging." : "Waiting on review."
|
|
3422
|
-
});
|
|
3423
|
-
if (input.pr.previewUrl) {
|
|
3424
|
-
out.push({ at: latest, kind: "preview", label: "Preview live", detail: input.pr.previewUrl });
|
|
3425
|
-
}
|
|
3426
|
-
}
|
|
3427
|
-
if (reached("in_review")) {
|
|
3428
|
-
out.push({ at: latest, kind: "review", label: "Waiting on a human review", detail: "Only a person can close this." });
|
|
3666
|
+
const res = await fetch(`${this.base}/v1/comments/${encodeURIComponent(threadId)}/messages`, this.opts({
|
|
3667
|
+
method: "POST",
|
|
3668
|
+
headers: this.headers(),
|
|
3669
|
+
body: JSON.stringify(message)
|
|
3670
|
+
}));
|
|
3671
|
+
if (!res.ok) throw new Error(`addMessage failed: ${res.status}`);
|
|
3672
|
+
return await res.json();
|
|
3429
3673
|
}
|
|
3430
|
-
|
|
3431
|
-
|
|
3674
|
+
async listReactions(threadId) {
|
|
3675
|
+
const res = await fetch(`${this.base}/v1/comments/${encodeURIComponent(threadId)}/messages/all/reactions`, this.opts({ headers: this.headers() }));
|
|
3676
|
+
if (res.status === 404) return [];
|
|
3677
|
+
if (!res.ok) throw new Error(`listReactions failed: ${res.status}`);
|
|
3678
|
+
return (await res.json()).reactions;
|
|
3432
3679
|
}
|
|
3433
|
-
|
|
3434
|
-
|
|
3435
|
-
|
|
3436
|
-
|
|
3437
|
-
|
|
3438
|
-
|
|
3439
|
-
|
|
3440
|
-
|
|
3441
|
-
|
|
3442
|
-
|
|
3443
|
-
|
|
3444
|
-
const lines = [
|
|
3445
|
-
`# ${input.title ?? "Feedback"} (#${input.id})`,
|
|
3446
|
-
"",
|
|
3447
|
-
`- Stage: ${normalizeStage(input.status)}`,
|
|
3448
|
-
`- Page: ${input.url ?? "\u2014"}`
|
|
3449
|
-
];
|
|
3450
|
-
if (input.anchor?.selector)
|
|
3451
|
-
lines.push(`- Target: \`${input.anchor.selector}\``);
|
|
3452
|
-
if (input.pr?.number)
|
|
3453
|
-
lines.push(`- PR: #${input.pr.number}${input.pr.previewUrl ? ` \xB7 preview ${input.pr.previewUrl}` : ""}`);
|
|
3454
|
-
lines.push("", "## Request", "", input.body);
|
|
3455
|
-
if (messages.length) {
|
|
3456
|
-
lines.push("", "## Conversation", "");
|
|
3457
|
-
for (const m of messages)
|
|
3458
|
-
lines.push(`**${m.authorName}**${m.fromAgent ? " (agent)" : ""} \u2014 ${m.at}`, "", m.body, "");
|
|
3680
|
+
async toggleReaction(input) {
|
|
3681
|
+
const res = await fetch(
|
|
3682
|
+
`${this.base}/v1/comments/${encodeURIComponent(input.threadId)}/messages/${encodeURIComponent(input.messageId)}/reactions`,
|
|
3683
|
+
this.opts({
|
|
3684
|
+
method: "POST",
|
|
3685
|
+
headers: this.headers(),
|
|
3686
|
+
body: JSON.stringify({ emoji: input.emoji, userId: input.userId, userName: input.userName })
|
|
3687
|
+
})
|
|
3688
|
+
);
|
|
3689
|
+
if (!res.ok) throw new Error(`toggleReaction failed: ${res.status}`);
|
|
3690
|
+
return (await res.json()).reactions;
|
|
3459
3691
|
}
|
|
3460
|
-
|
|
3461
|
-
|
|
3462
|
-
|
|
3463
|
-
|
|
3464
|
-
|
|
3465
|
-
|
|
3466
|
-
const ranges = [];
|
|
3467
|
-
for (const m of body.matchAll(/```[\s\S]*?```/g))
|
|
3468
|
-
ranges.push([m.index, m.index + m[0].length]);
|
|
3469
|
-
const fenced = (i) => ranges.some(([a, b]) => i >= a && i < b);
|
|
3470
|
-
for (const m of body.matchAll(/`[^`\n]*`/g)) {
|
|
3471
|
-
if (!fenced(m.index))
|
|
3472
|
-
ranges.push([m.index, m.index + m[0].length]);
|
|
3692
|
+
/** Everyone who has taken part in this project, for mention resolution. */
|
|
3693
|
+
async listPeople(projectKey) {
|
|
3694
|
+
const q = new URLSearchParams({ projectKey });
|
|
3695
|
+
const res = await fetch(`${this.base}/v1/people?${q}`, this.opts({ headers: this.headers() }));
|
|
3696
|
+
if (!res.ok) throw new Error(`listPeople failed: ${res.status}`);
|
|
3697
|
+
return await res.json();
|
|
3473
3698
|
}
|
|
3474
|
-
|
|
3475
|
-
|
|
3476
|
-
|
|
3477
|
-
|
|
3478
|
-
return
|
|
3479
|
-
const excluded = excludedRanges(body);
|
|
3480
|
-
const inExcluded = (i) => excluded.some(([a, b]) => i >= a && i < b);
|
|
3481
|
-
const out = [];
|
|
3482
|
-
const seen = /* @__PURE__ */ new Set();
|
|
3483
|
-
for (let i = 0; i < body.length; i++) {
|
|
3484
|
-
if (body[i] !== "@")
|
|
3485
|
-
continue;
|
|
3486
|
-
if (inExcluded(i))
|
|
3487
|
-
continue;
|
|
3488
|
-
const prev = i > 0 ? body[i - 1] : "";
|
|
3489
|
-
if (prev && !/[\s([{<"'*_~]/.test(prev))
|
|
3490
|
-
continue;
|
|
3491
|
-
const match = HANDLE.exec(body.slice(i + 1));
|
|
3492
|
-
if (!match)
|
|
3493
|
-
continue;
|
|
3494
|
-
const raw = match[0];
|
|
3495
|
-
const handle = raw.replace(/[._-]+$/, "");
|
|
3496
|
-
if (!handle)
|
|
3497
|
-
continue;
|
|
3498
|
-
const key = handle.toLowerCase();
|
|
3499
|
-
if (seen.has(key))
|
|
3500
|
-
continue;
|
|
3501
|
-
seen.add(key);
|
|
3502
|
-
out.push({ handle, start: i, length: handle.length + 1 });
|
|
3699
|
+
async listNotifications(projectKey, recipient) {
|
|
3700
|
+
const q = new URLSearchParams({ projectKey, recipient });
|
|
3701
|
+
const res = await fetch(`${this.base}/v1/notifications?${q}`, this.opts({ headers: this.headers() }));
|
|
3702
|
+
if (!res.ok) throw new Error(`listNotifications failed: ${res.status}`);
|
|
3703
|
+
return (await res.json()).notifications;
|
|
3503
3704
|
}
|
|
3504
|
-
|
|
3505
|
-
|
|
3506
|
-
|
|
3507
|
-
|
|
3508
|
-
|
|
3509
|
-
|
|
3510
|
-
const out = [];
|
|
3511
|
-
let at = 0;
|
|
3512
|
-
for (const m of sorted) {
|
|
3513
|
-
if (m.start > at)
|
|
3514
|
-
out.push({ text: body.slice(at, m.start), mention: false });
|
|
3515
|
-
out.push({ text: body.slice(m.start, m.start + m.length), mention: true });
|
|
3516
|
-
at = m.start + m.length;
|
|
3705
|
+
async markNotificationsRead(projectKey, recipient, id) {
|
|
3706
|
+
await fetch(`${this.base}/v1/notifications/read`, this.opts({
|
|
3707
|
+
method: "POST",
|
|
3708
|
+
headers: this.headers(),
|
|
3709
|
+
body: JSON.stringify({ projectKey, recipient, id })
|
|
3710
|
+
}));
|
|
3517
3711
|
}
|
|
3518
|
-
|
|
3519
|
-
|
|
3520
|
-
|
|
3521
|
-
|
|
3522
|
-
|
|
3523
|
-
|
|
3524
|
-
|
|
3525
|
-
if (at < 0)
|
|
3526
|
-
return [];
|
|
3527
|
-
const typed = before.slice(at + 1);
|
|
3528
|
-
if (/\s/.test(typed))
|
|
3529
|
-
return [];
|
|
3530
|
-
const q = typed.toLowerCase();
|
|
3531
|
-
return candidates.filter((c) => !q || c.name.toLowerCase().includes(q) || c.id.toLowerCase().includes(q)).slice(0, 6);
|
|
3532
|
-
}
|
|
3533
|
-
|
|
3534
|
-
// ../shared/dist/needs-you.js
|
|
3535
|
-
var NEEDS_YOU_LABELS = {
|
|
3536
|
-
review: "Waiting on your review",
|
|
3537
|
-
question: "The agent asked you something",
|
|
3538
|
-
failed: "The agent's run failed"
|
|
3539
|
-
};
|
|
3540
|
-
function looksLikeQuestion(body) {
|
|
3541
|
-
const lines = body.split("\n").map((l) => l.trim()).filter(Boolean);
|
|
3542
|
-
const last = lines[lines.length - 1];
|
|
3543
|
-
if (!last)
|
|
3544
|
-
return false;
|
|
3545
|
-
return /\?["')\]]*$/.test(last);
|
|
3546
|
-
}
|
|
3547
|
-
function needsYou(input) {
|
|
3548
|
-
const stage = normalizeStage2(input.status);
|
|
3549
|
-
if (stage === "resolved")
|
|
3550
|
-
return { needs: false };
|
|
3551
|
-
if (input.agentFailed)
|
|
3552
|
-
return { needs: true, reason: "failed", label: NEEDS_YOU_LABELS.failed };
|
|
3553
|
-
if (input.last?.fromAgent && looksLikeQuestion(input.last.body)) {
|
|
3554
|
-
return { needs: true, reason: "question", label: NEEDS_YOU_LABELS.question };
|
|
3712
|
+
async update(id, patch) {
|
|
3713
|
+
const res = await fetch(`${this.base}/v1/comments/${encodeURIComponent(id)}`, this.opts({
|
|
3714
|
+
method: "PATCH",
|
|
3715
|
+
headers: this.headers(),
|
|
3716
|
+
body: JSON.stringify(patch)
|
|
3717
|
+
}));
|
|
3718
|
+
if (!res.ok) throw new Error(`update failed: ${res.status}`);
|
|
3555
3719
|
}
|
|
3556
|
-
|
|
3557
|
-
|
|
3558
|
-
|
|
3559
|
-
|
|
3560
|
-
|
|
3561
|
-
|
|
3562
|
-
return "queue";
|
|
3563
|
-
if (status === "done")
|
|
3564
|
-
return "resolved";
|
|
3565
|
-
return status;
|
|
3566
|
-
}
|
|
3567
|
-
|
|
3568
|
-
// ../shared/dist/index.js
|
|
3569
|
-
var COMMENT_STAGES = ["queue", "todo", "in_progress", "in_review", "resolved"];
|
|
3570
|
-
var STAGE_LABELS = {
|
|
3571
|
-
queue: "Queue",
|
|
3572
|
-
todo: "To Do",
|
|
3573
|
-
in_progress: "In Progress",
|
|
3574
|
-
in_review: "In Review",
|
|
3575
|
-
resolved: "Resolved"
|
|
3576
|
-
};
|
|
3577
|
-
var LEGACY_STATUS = {
|
|
3578
|
-
open: "queue",
|
|
3579
|
-
in_progress: "in_progress",
|
|
3580
|
-
done: "resolved"
|
|
3581
|
-
};
|
|
3582
|
-
function normalizeStatus(value) {
|
|
3583
|
-
if (typeof value === "string") {
|
|
3584
|
-
if (COMMENT_STAGES.includes(value))
|
|
3585
|
-
return value;
|
|
3586
|
-
if (value in LEGACY_STATUS)
|
|
3587
|
-
return LEGACY_STATUS[value];
|
|
3720
|
+
async remove(id) {
|
|
3721
|
+
const res = await fetch(`${this.base}/v1/comments/${encodeURIComponent(id)}`, this.opts({
|
|
3722
|
+
method: "DELETE",
|
|
3723
|
+
headers: this.headers()
|
|
3724
|
+
}));
|
|
3725
|
+
if (!res.ok && res.status !== 404) throw new Error(`remove failed: ${res.status}`);
|
|
3588
3726
|
}
|
|
3589
|
-
return "queue";
|
|
3590
|
-
}
|
|
3591
|
-
var COMMENT_PRIORITIES = ["critical", "high", "medium", "low"];
|
|
3592
|
-
var PRIORITY_LABELS = {
|
|
3593
|
-
critical: "Critical",
|
|
3594
|
-
high: "High",
|
|
3595
|
-
medium: "Medium",
|
|
3596
|
-
low: "Low"
|
|
3597
|
-
};
|
|
3598
|
-
var CHANGE_TYPES = ["frontend", "backend", "api", "other"];
|
|
3599
|
-
var CHANGE_TYPE_LABELS = {
|
|
3600
|
-
frontend: "Frontend",
|
|
3601
|
-
backend: "Backend",
|
|
3602
|
-
api: "API",
|
|
3603
|
-
other: "Other"
|
|
3604
3727
|
};
|
|
3605
|
-
var DEFAULT_PRIORITY = "medium";
|
|
3606
|
-
var DEFAULT_CHANGE_TYPE = "other";
|
|
3607
|
-
function normalizePriority(value) {
|
|
3608
|
-
if (typeof value === "string" && COMMENT_PRIORITIES.includes(value)) {
|
|
3609
|
-
return value;
|
|
3610
|
-
}
|
|
3611
|
-
return DEFAULT_PRIORITY;
|
|
3612
|
-
}
|
|
3613
3728
|
|
|
3614
3729
|
// src/app.ts
|
|
3615
|
-
var SDK_VERSION = true ? "0.10.
|
|
3730
|
+
var SDK_VERSION = true ? "0.10.24" : "dev";
|
|
3616
3731
|
var ACCENTS = [
|
|
3617
3732
|
{ id: "indigo", dark: "#6b73e6", light: "#4a55d6", soft: "rgba(107,115,230,0.12)" },
|
|
3618
3733
|
{ id: "violet", dark: "#a06be6", light: "#7c3fd4", soft: "rgba(160,107,230,0.14)" },
|
|
@@ -4030,6 +4145,10 @@ var Loupe = (() => {
|
|
|
4030
4145
|
/** Optimistic replies the store rejected — offered for retry rather than lost. */
|
|
4031
4146
|
this.msgFailed = /* @__PURE__ */ new Set();
|
|
4032
4147
|
this.msgErr = /* @__PURE__ */ new Map();
|
|
4148
|
+
/** Other people on this page. Empty unless a bridge is configured. */
|
|
4149
|
+
this.peers = [];
|
|
4150
|
+
/** Reactions per thread, keyed `${threadId}:${messageId}`, so a re-render is free. */
|
|
4151
|
+
this.reactions = /* @__PURE__ */ new Map();
|
|
4033
4152
|
/** Everyone who has taken part, for mention autocomplete. Fetched once. */
|
|
4034
4153
|
this.people = /* @__PURE__ */ new Map();
|
|
4035
4154
|
this.cfg = cfg;
|
|
@@ -4049,6 +4168,7 @@ var Loupe = (() => {
|
|
|
4049
4168
|
this.renderPins();
|
|
4050
4169
|
this.renderList();
|
|
4051
4170
|
this.renderHome();
|
|
4171
|
+
this.startPresence();
|
|
4052
4172
|
void this.loadNotifications();
|
|
4053
4173
|
if (this.scope === "all") void this.loadAllComments();
|
|
4054
4174
|
this.observe();
|
|
@@ -4198,7 +4318,9 @@ var Loupe = (() => {
|
|
|
4198
4318
|
closeBtn.innerHTML = I_CLOSE;
|
|
4199
4319
|
closeBtn.onclick = () => this.closeDock();
|
|
4200
4320
|
ctl.append(posWrap, this.themeBtn, setWrap, minBtn, closeBtn);
|
|
4201
|
-
|
|
4321
|
+
this.peerEl = el("div", "peers");
|
|
4322
|
+
if (!this.cfg.bridge) this.peerEl.style.display = "none";
|
|
4323
|
+
head.append(brand, this.peerEl, ctl);
|
|
4202
4324
|
this.minBar = el("div", "minbar");
|
|
4203
4325
|
this.minBar.onclick = () => this.setMinimized(false);
|
|
4204
4326
|
this.tabList = [...BUILTIN_TABS, ...(this.cfg.tabs ?? []).map((t) => ({ id: t.id, label: t.label }))];
|
|
@@ -6358,6 +6480,37 @@ ${c.body}` : c.body)}</div>` + (c.context?.html ? `<pre class="or-code">${escape
|
|
|
6358
6480
|
else body.appendChild(document.createTextNode(seg.text));
|
|
6359
6481
|
}
|
|
6360
6482
|
row.append(head, body);
|
|
6483
|
+
const pills = el("div", "rxns");
|
|
6484
|
+
for (const r of summarizeReactions(this.reactionsOf(c.id, m.id), this.cfg.user.id)) {
|
|
6485
|
+
const pill = el("button", "rxn" + (r.mine ? " mine" : ""));
|
|
6486
|
+
pill.append(document.createTextNode(r.emoji), el("span", "rxn-n", String(r.count)));
|
|
6487
|
+
pill.title = r.users.join(", ");
|
|
6488
|
+
pill.onclick = (e) => {
|
|
6489
|
+
e.stopPropagation();
|
|
6490
|
+
void this.react(c, m.id, r.emoji);
|
|
6491
|
+
};
|
|
6492
|
+
pills.appendChild(pill);
|
|
6493
|
+
}
|
|
6494
|
+
const add = el("button", "rxn-add", "\uFF0B");
|
|
6495
|
+
add.title = "Add a reaction";
|
|
6496
|
+
add.onclick = (e) => {
|
|
6497
|
+
e.stopPropagation();
|
|
6498
|
+
const open = pills.querySelector(".rxn-pick");
|
|
6499
|
+
pills.querySelectorAll(".rxn-pick").forEach((n) => n.remove());
|
|
6500
|
+
if (open) return;
|
|
6501
|
+
const pick = el("div", "rxn-pick");
|
|
6502
|
+
for (const emoji of REACTION_CHOICES) {
|
|
6503
|
+
const b = el("button", "rxn-opt", emoji);
|
|
6504
|
+
b.onclick = (ev) => {
|
|
6505
|
+
ev.stopPropagation();
|
|
6506
|
+
void this.react(c, m.id, emoji);
|
|
6507
|
+
};
|
|
6508
|
+
pick.appendChild(b);
|
|
6509
|
+
}
|
|
6510
|
+
pills.appendChild(pick);
|
|
6511
|
+
};
|
|
6512
|
+
pills.appendChild(add);
|
|
6513
|
+
row.appendChild(pills);
|
|
6361
6514
|
if (this.msgPending.has(m.id)) row.appendChild(el("div", "msg-state", "Sending\u2026"));
|
|
6362
6515
|
if (this.msgFailed.has(m.id)) {
|
|
6363
6516
|
const failed = el("div", "msg-state failed");
|
|
@@ -6469,6 +6622,108 @@ ${c.body}` : c.body)}</div>` + (c.context?.html ? `<pre class="or-code">${escape
|
|
|
6469
6622
|
wrap.appendChild(copyRow);
|
|
6470
6623
|
return wrap;
|
|
6471
6624
|
}
|
|
6625
|
+
/**
|
|
6626
|
+
* Join, then keep talking.
|
|
6627
|
+
*
|
|
6628
|
+
* Presence is a heartbeat, not a flag, because a browser that is closed or crashes
|
|
6629
|
+
* sends no goodbye. The bridge expires anything silent past the TTL, and the panel
|
|
6630
|
+
* re-joins on a 404 rather than assuming it is still known.
|
|
6631
|
+
*/
|
|
6632
|
+
startPresence() {
|
|
6633
|
+
const bridge = this.cfg.bridge;
|
|
6634
|
+
if (!bridge) return;
|
|
6635
|
+
const base = bridge.replace(/\/$/, "");
|
|
6636
|
+
const pageUrl = location.pathname + location.search;
|
|
6637
|
+
const tick = async () => {
|
|
6638
|
+
try {
|
|
6639
|
+
if (!this.peerId) {
|
|
6640
|
+
const res = await fetch(`${base}/presence`, {
|
|
6641
|
+
method: "POST",
|
|
6642
|
+
headers: { "content-type": "application/json" },
|
|
6643
|
+
body: JSON.stringify({ url: pageUrl, userId: this.cfg.user.id, name: this.cfg.user.name })
|
|
6644
|
+
});
|
|
6645
|
+
if (!res.ok) return;
|
|
6646
|
+
const data = await res.json();
|
|
6647
|
+
this.peerId = data.peer?.id;
|
|
6648
|
+
} else {
|
|
6649
|
+
const res = await fetch(`${base}/presence/${encodeURIComponent(this.peerId)}/heartbeat`, { method: "POST" });
|
|
6650
|
+
if (res.status === 404) this.peerId = void 0;
|
|
6651
|
+
}
|
|
6652
|
+
const listed = await fetch(
|
|
6653
|
+
`${base}/presence?url=${encodeURIComponent(pageUrl)}&viewer=${encodeURIComponent(this.peerId ?? "")}`
|
|
6654
|
+
);
|
|
6655
|
+
if (listed.ok) {
|
|
6656
|
+
const data = await listed.json();
|
|
6657
|
+
this.peers = Array.isArray(data.peers) ? data.peers : [];
|
|
6658
|
+
this.renderPeers();
|
|
6659
|
+
}
|
|
6660
|
+
} catch {
|
|
6661
|
+
this.peers = [];
|
|
6662
|
+
this.renderPeers();
|
|
6663
|
+
}
|
|
6664
|
+
};
|
|
6665
|
+
void tick();
|
|
6666
|
+
this.peerTimer = setInterval(() => void tick(), PEER_HEARTBEAT_MS);
|
|
6667
|
+
}
|
|
6668
|
+
renderPeers() {
|
|
6669
|
+
if (!this.peerEl) return;
|
|
6670
|
+
this.peerEl.textContent = "";
|
|
6671
|
+
if (!this.peers.length) {
|
|
6672
|
+
this.peerEl.style.display = "none";
|
|
6673
|
+
return;
|
|
6674
|
+
}
|
|
6675
|
+
this.peerEl.style.display = "";
|
|
6676
|
+
for (const p of this.peers.slice(0, 4)) {
|
|
6677
|
+
const av = el("span", "peer-av", initialsOf(p.name));
|
|
6678
|
+
av.title = `${p.name} is on this page`;
|
|
6679
|
+
this.peerEl.appendChild(av);
|
|
6680
|
+
}
|
|
6681
|
+
if (this.peers.length > 4) this.peerEl.appendChild(el("span", "peer-more", `+${this.peers.length - 4}`));
|
|
6682
|
+
}
|
|
6683
|
+
stopPresence() {
|
|
6684
|
+
if (this.peerTimer) clearInterval(this.peerTimer);
|
|
6685
|
+
this.peerTimer = void 0;
|
|
6686
|
+
if (this.cfg.bridge && this.peerId) {
|
|
6687
|
+
const base = this.cfg.bridge.replace(/\/$/, "");
|
|
6688
|
+
void fetch(`${base}/presence/${encodeURIComponent(this.peerId)}`, { method: "DELETE", keepalive: true }).catch(() => {
|
|
6689
|
+
});
|
|
6690
|
+
}
|
|
6691
|
+
this.peerId = void 0;
|
|
6692
|
+
this.peers = [];
|
|
6693
|
+
}
|
|
6694
|
+
reactionsOf(threadId, messageId) {
|
|
6695
|
+
return this.reactions.get(`${threadId}:${messageId}`) ?? [];
|
|
6696
|
+
}
|
|
6697
|
+
/**
|
|
6698
|
+
* Toggle a reaction.
|
|
6699
|
+
*
|
|
6700
|
+
* Optimistic, then replaced by the server's own set: the server returns the whole
|
|
6701
|
+
* list precisely so the count can never drift from what was stored.
|
|
6702
|
+
*/
|
|
6703
|
+
async react(c, messageId, emoji) {
|
|
6704
|
+
const key = `${c.id}:${messageId}`;
|
|
6705
|
+
const before = this.reactionsOf(c.id, messageId);
|
|
6706
|
+
this.reactions.set(key, toggleReaction(before, {
|
|
6707
|
+
messageId,
|
|
6708
|
+
emoji,
|
|
6709
|
+
userId: this.cfg.user.id,
|
|
6710
|
+
userName: this.cfg.user.name
|
|
6711
|
+
}));
|
|
6712
|
+
this.renderList();
|
|
6713
|
+
try {
|
|
6714
|
+
const next = await this.store.toggleReaction({
|
|
6715
|
+
threadId: c.id,
|
|
6716
|
+
messageId,
|
|
6717
|
+
emoji,
|
|
6718
|
+
userId: this.cfg.user.id,
|
|
6719
|
+
userName: this.cfg.user.name
|
|
6720
|
+
});
|
|
6721
|
+
this.reactions.set(key, Array.isArray(next) ? next : this.reactionsOf(c.id, messageId));
|
|
6722
|
+
} catch {
|
|
6723
|
+
this.reactions.set(key, before);
|
|
6724
|
+
}
|
|
6725
|
+
this.renderList();
|
|
6726
|
+
}
|
|
6472
6727
|
async loadPeople() {
|
|
6473
6728
|
if (this.people.has(this.cfg.projectKey)) return;
|
|
6474
6729
|
try {
|
|
@@ -6481,6 +6736,17 @@ ${c.body}` : c.body)}</div>` + (c.context?.html ? `<pre class="or-code">${escape
|
|
|
6481
6736
|
async loadMessages(c) {
|
|
6482
6737
|
if (this.messages.has(c.id)) return;
|
|
6483
6738
|
void this.loadPeople();
|
|
6739
|
+
void this.store.listReactions(c.id).then((all) => {
|
|
6740
|
+
if (!Array.isArray(all)) return;
|
|
6741
|
+
for (const r of all) {
|
|
6742
|
+
const key = `${c.id}:${r.messageId}`;
|
|
6743
|
+
const list = this.reactions.get(key) ?? [];
|
|
6744
|
+
if (!list.some((x) => x.userId === r.userId && x.emoji === r.emoji)) list.push(r);
|
|
6745
|
+
this.reactions.set(key, list);
|
|
6746
|
+
}
|
|
6747
|
+
this.renderList();
|
|
6748
|
+
}).catch(() => {
|
|
6749
|
+
});
|
|
6484
6750
|
try {
|
|
6485
6751
|
this.messages.set(c.id, await this.store.listMessages(c.id));
|
|
6486
6752
|
} catch {
|
|
@@ -6627,6 +6893,7 @@ ${c.body}` : c.body)}</div>` + (c.context?.html ? `<pre class="or-code">${escape
|
|
|
6627
6893
|
if (elx) elx.scrollIntoView({ behavior: "smooth", block: "center" });
|
|
6628
6894
|
}
|
|
6629
6895
|
destroy() {
|
|
6896
|
+
this.stopPresence();
|
|
6630
6897
|
this.stopRecording?.();
|
|
6631
6898
|
this.setMode("off");
|
|
6632
6899
|
this.mo?.disconnect();
|