@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.js
CHANGED
|
@@ -760,6 +760,43 @@ var STYLES = (
|
|
|
760
760
|
.voice:disabled { opacity: .45; cursor: not-allowed; }
|
|
761
761
|
.voice.on { border-color: var(--pin); color: var(--pin); background: var(--bg-3); }
|
|
762
762
|
|
|
763
|
+
/* reactions */
|
|
764
|
+
.rxns { display: flex; flex-wrap: wrap; align-items: center; gap: 4px; margin-top: 6px; position: relative; }
|
|
765
|
+
.rxn {
|
|
766
|
+
display: inline-flex; align-items: center; gap: 3px; padding: 1px 7px;
|
|
767
|
+
border: 1px solid var(--line); border-radius: 999px; background: var(--bg);
|
|
768
|
+
color: var(--ink); font-size: 11px; cursor: pointer; line-height: 1.7;
|
|
769
|
+
}
|
|
770
|
+
.rxn:hover { border-color: var(--accent); }
|
|
771
|
+
.rxn.mine { border-color: var(--accent); background: var(--accent-soft); }
|
|
772
|
+
.rxn-n { color: var(--muted); font-size: 10px; font-variant-numeric: tabular-nums; }
|
|
773
|
+
.rxn.mine .rxn-n { color: var(--accent); }
|
|
774
|
+
.rxn-add {
|
|
775
|
+
width: 20px; height: 20px; padding: 0; border: 1px dashed var(--line); border-radius: 50%;
|
|
776
|
+
background: transparent; color: var(--muted); font-size: 11px; cursor: pointer; line-height: 1;
|
|
777
|
+
}
|
|
778
|
+
.rxn-add:hover { border-color: var(--accent); color: var(--accent); }
|
|
779
|
+
.rxn-pick {
|
|
780
|
+
display: flex; gap: 2px; padding: 3px; border: 1px solid var(--line); border-radius: 999px;
|
|
781
|
+
background: var(--bg); box-shadow: 0 4px 14px rgb(0 0 0 / 18%); z-index: 3;
|
|
782
|
+
}
|
|
783
|
+
.rxn-opt {
|
|
784
|
+
width: 24px; height: 24px; padding: 0; border: 0; border-radius: 50%;
|
|
785
|
+
background: transparent; font-size: 14px; cursor: pointer; line-height: 1;
|
|
786
|
+
}
|
|
787
|
+
.rxn-opt:hover { background: var(--accent-soft); }
|
|
788
|
+
|
|
789
|
+
/* who else is here */
|
|
790
|
+
.peers { display: flex; align-items: center; gap: -2px; margin-right: 8px; }
|
|
791
|
+
.peer-av {
|
|
792
|
+
display: inline-flex; align-items: center; justify-content: center;
|
|
793
|
+
width: 20px; height: 20px; margin-left: -5px; border-radius: 50%;
|
|
794
|
+
border: 2px solid var(--bg); background: var(--accent); color: #fff;
|
|
795
|
+
font-size: 9px; font-weight: 700; letter-spacing: .02em;
|
|
796
|
+
}
|
|
797
|
+
.peer-av:first-child { margin-left: 0; }
|
|
798
|
+
.peer-more { margin-left: 3px; font-size: 10px; color: var(--muted); }
|
|
799
|
+
|
|
763
800
|
/* in-app mentions */
|
|
764
801
|
.hnotif { margin-top: 10px; }
|
|
765
802
|
.nf-head { display: flex; align-items: center; gap: 6px; margin-bottom: 5px; font-size: 11.5px; color: var(--ink); }
|
|
@@ -2893,41 +2930,498 @@ function cropRegion(dataUrl, rect, ox, oy, scale, redact) {
|
|
|
2893
2930
|
});
|
|
2894
2931
|
}
|
|
2895
2932
|
|
|
2896
|
-
//
|
|
2897
|
-
var
|
|
2898
|
-
|
|
2899
|
-
|
|
2933
|
+
// ../shared/dist/activity.js
|
|
2934
|
+
var ACTIVITY_STATUS_LABELS = {
|
|
2935
|
+
idle: "Idle",
|
|
2936
|
+
working: "Working",
|
|
2937
|
+
error: "Error"
|
|
2938
|
+
};
|
|
2939
|
+
function formatDuration(ms) {
|
|
2940
|
+
if (!ms || ms < 0)
|
|
2941
|
+
return "\u2014";
|
|
2942
|
+
const s = Math.round(ms / 1e3);
|
|
2943
|
+
if (s < 60)
|
|
2944
|
+
return `${s}s`;
|
|
2945
|
+
const m = Math.floor(s / 60);
|
|
2946
|
+
const rest = s % 60;
|
|
2947
|
+
if (m < 60)
|
|
2948
|
+
return rest ? `${m}m ${rest}s` : `${m}m`;
|
|
2949
|
+
const h = Math.floor(m / 60);
|
|
2950
|
+
return `${h}h ${m % 60}m`;
|
|
2951
|
+
}
|
|
2952
|
+
function summarizeActivity(events, status = "idle", now = Date.now()) {
|
|
2953
|
+
const files = /* @__PURE__ */ new Set();
|
|
2954
|
+
const kinds = /* @__PURE__ */ new Map();
|
|
2955
|
+
let errors = 0;
|
|
2956
|
+
let first = Infinity;
|
|
2957
|
+
for (const e of events) {
|
|
2958
|
+
for (const f of e.files ?? [])
|
|
2959
|
+
files.add(f);
|
|
2960
|
+
kinds.set(e.kind, (kinds.get(e.kind) ?? 0) + 1);
|
|
2961
|
+
if (e.level === "error")
|
|
2962
|
+
errors++;
|
|
2963
|
+
const t = Date.parse(e.at);
|
|
2964
|
+
if (!Number.isNaN(t))
|
|
2965
|
+
first = Math.min(first, t);
|
|
2900
2966
|
}
|
|
2901
|
-
|
|
2902
|
-
|
|
2903
|
-
|
|
2904
|
-
|
|
2905
|
-
|
|
2906
|
-
|
|
2967
|
+
return {
|
|
2968
|
+
status,
|
|
2969
|
+
events: events.length,
|
|
2970
|
+
errors,
|
|
2971
|
+
files: files.size,
|
|
2972
|
+
durationMs: events.length && first !== Infinity ? Math.max(0, now - first) : 0,
|
|
2973
|
+
byKind: [...kinds.entries()].map(([kind, count]) => ({ kind, count })).sort((a, b) => b.count - a.count || a.kind.localeCompare(b.kind))
|
|
2974
|
+
};
|
|
2975
|
+
}
|
|
2976
|
+
|
|
2977
|
+
// ../shared/dist/lifecycle.js
|
|
2978
|
+
var LIFECYCLE_LABELS = {
|
|
2979
|
+
sent: "Sent to agent",
|
|
2980
|
+
in_pr: "In PR",
|
|
2981
|
+
preview: "Review preview",
|
|
2982
|
+
reviewed: "Reviewed"
|
|
2983
|
+
};
|
|
2984
|
+
function lifecycle(c) {
|
|
2985
|
+
const hasProposal = !!c.proposal;
|
|
2986
|
+
const pr = c.pr ?? void 0;
|
|
2987
|
+
const withPr = (stage) => {
|
|
2988
|
+
const out = { stage, label: LIFECYCLE_LABELS[stage] };
|
|
2989
|
+
if (pr) {
|
|
2990
|
+
out.pr = pr;
|
|
2991
|
+
if (typeof pr.checksTotal === "number" && pr.checksTotal > 0) {
|
|
2992
|
+
const passed = Math.max(0, Math.min(pr.checksTotal, pr.checksPassed ?? 0));
|
|
2993
|
+
out.checks = {
|
|
2994
|
+
text: `${passed}/${pr.checksTotal}`,
|
|
2995
|
+
ratio: passed / pr.checksTotal
|
|
2996
|
+
};
|
|
2997
|
+
}
|
|
2907
2998
|
}
|
|
2999
|
+
return out;
|
|
3000
|
+
};
|
|
3001
|
+
if (c.status === "resolved")
|
|
3002
|
+
return hasProposal || pr ? withPr("reviewed") : null;
|
|
3003
|
+
if (pr)
|
|
3004
|
+
return withPr("in_pr");
|
|
3005
|
+
if (c.status === "in_review")
|
|
3006
|
+
return { stage: "preview", label: LIFECYCLE_LABELS.preview };
|
|
3007
|
+
if (hasProposal)
|
|
3008
|
+
return { stage: "sent", label: LIFECYCLE_LABELS.sent };
|
|
3009
|
+
return null;
|
|
3010
|
+
}
|
|
3011
|
+
function awaitingReview(comments) {
|
|
3012
|
+
return comments.filter((c) => c.status === "in_review");
|
|
3013
|
+
}
|
|
3014
|
+
|
|
3015
|
+
// ../shared/dist/iteration.js
|
|
3016
|
+
function emptyIterations() {
|
|
3017
|
+
return { items: [], index: -1 };
|
|
3018
|
+
}
|
|
3019
|
+
function current(state) {
|
|
3020
|
+
return state.index >= 0 ? state.items[state.index] ?? null : null;
|
|
3021
|
+
}
|
|
3022
|
+
function addIteration(state, iteration) {
|
|
3023
|
+
const head = state.items.slice(0, state.index + 1);
|
|
3024
|
+
const items = [...head, iteration].slice(-MAX_ITERATIONS);
|
|
3025
|
+
return { items, index: items.length - 1 };
|
|
3026
|
+
}
|
|
3027
|
+
var MAX_ITERATIONS = 20;
|
|
3028
|
+
function canUndo(state) {
|
|
3029
|
+
return state.items.length > 0;
|
|
3030
|
+
}
|
|
3031
|
+
function undo(state) {
|
|
3032
|
+
if (!state.items.length)
|
|
3033
|
+
return state;
|
|
3034
|
+
const items = state.items.slice(0, -1);
|
|
3035
|
+
return { items, index: items.length - 1 };
|
|
3036
|
+
}
|
|
3037
|
+
function canMove(state, delta) {
|
|
3038
|
+
const next = state.index + delta;
|
|
3039
|
+
return next >= 0 && next < state.items.length;
|
|
3040
|
+
}
|
|
3041
|
+
function move(state, delta) {
|
|
3042
|
+
return canMove(state, delta) ? { ...state, index: state.index + delta } : state;
|
|
3043
|
+
}
|
|
3044
|
+
function stackLabel(state) {
|
|
3045
|
+
return state.items.length ? `${state.index + 1} / ${state.items.length}` : "";
|
|
3046
|
+
}
|
|
3047
|
+
|
|
3048
|
+
// ../shared/dist/consent.js
|
|
3049
|
+
function emptyConsent() {
|
|
3050
|
+
return { state: "idle", request: null, history: [] };
|
|
3051
|
+
}
|
|
3052
|
+
function isNavigableUrl(raw) {
|
|
3053
|
+
try {
|
|
3054
|
+
const u = new URL(raw);
|
|
3055
|
+
return u.protocol === "http:" || u.protocol === "https:";
|
|
3056
|
+
} catch {
|
|
3057
|
+
return false;
|
|
2908
3058
|
}
|
|
2909
|
-
|
|
2910
|
-
|
|
3059
|
+
}
|
|
3060
|
+
function requestNavigation(record, request) {
|
|
3061
|
+
if (!isNavigableUrl(request.url))
|
|
3062
|
+
return record;
|
|
3063
|
+
return {
|
|
3064
|
+
state: "requested",
|
|
3065
|
+
request: {
|
|
3066
|
+
id: request.id ?? `nav${Date.now().toString(36)}`,
|
|
3067
|
+
at: request.at ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
3068
|
+
url: request.url,
|
|
3069
|
+
reason: request.reason,
|
|
3070
|
+
requester: request.requester
|
|
3071
|
+
},
|
|
3072
|
+
history: record.history
|
|
3073
|
+
};
|
|
3074
|
+
}
|
|
3075
|
+
function isPending(record) {
|
|
3076
|
+
return record.state === "requested" && record.request !== null;
|
|
3077
|
+
}
|
|
3078
|
+
function decide(record, granted, now = (/* @__PURE__ */ new Date()).toISOString()) {
|
|
3079
|
+
if (!isPending(record))
|
|
3080
|
+
return { record, navigateTo: null };
|
|
3081
|
+
const url = record.request.url;
|
|
3082
|
+
return {
|
|
3083
|
+
record: {
|
|
3084
|
+
state: granted ? "granted" : "denied",
|
|
3085
|
+
request: null,
|
|
3086
|
+
history: [...record.history, { url, decision: granted ? "granted" : "denied", at: now }]
|
|
3087
|
+
},
|
|
3088
|
+
navigateTo: granted ? url : null
|
|
3089
|
+
};
|
|
3090
|
+
}
|
|
3091
|
+
function withdraw(record) {
|
|
3092
|
+
return isPending(record) ? { ...record, state: "idle", request: null } : record;
|
|
3093
|
+
}
|
|
3094
|
+
|
|
3095
|
+
// ../shared/dist/thread.js
|
|
3096
|
+
function firstMessageFromComment(comment) {
|
|
3097
|
+
return {
|
|
3098
|
+
id: `${comment.id}:0`,
|
|
3099
|
+
threadId: comment.id,
|
|
3100
|
+
author: { ...comment.author, type: "user" },
|
|
3101
|
+
body: comment.body,
|
|
3102
|
+
attachments: comment.attachments?.length ? comment.attachments : void 0,
|
|
3103
|
+
createdAt: comment.createdAt
|
|
3104
|
+
};
|
|
3105
|
+
}
|
|
3106
|
+
function threadConversation(comment, replies) {
|
|
3107
|
+
return [firstMessageFromComment(comment), ...replies].sort((a, b) => a.createdAt.localeCompare(b.createdAt));
|
|
3108
|
+
}
|
|
3109
|
+
function iterationLabel(link) {
|
|
3110
|
+
if (!link.parentThreadId || link.iterationType !== "revision")
|
|
3111
|
+
return null;
|
|
3112
|
+
return `Iteration ${link.iterationNumber ?? 2}`;
|
|
3113
|
+
}
|
|
3114
|
+
|
|
3115
|
+
// ../shared/dist/timeline.js
|
|
3116
|
+
function describeTarget(input) {
|
|
3117
|
+
const parts = [];
|
|
3118
|
+
if (input.anchor?.tag)
|
|
3119
|
+
parts.push(`a <${input.anchor.tag}>`);
|
|
3120
|
+
if (input.anchor?.selector)
|
|
3121
|
+
parts.push(`\`${input.anchor.selector}\``);
|
|
3122
|
+
return parts.length ? parts.join(" ") : input.kind === "free" ? "a page note" : "an element";
|
|
3123
|
+
}
|
|
3124
|
+
function threadTimeline(input, messages = []) {
|
|
3125
|
+
const out = [];
|
|
3126
|
+
const latest = messages.length ? messages[messages.length - 1].at : input.createdAt;
|
|
3127
|
+
out.push({
|
|
3128
|
+
at: input.createdAt,
|
|
3129
|
+
kind: "captured",
|
|
3130
|
+
label: "Feedback captured",
|
|
3131
|
+
detail: [input.title, describeTarget(input)].filter(Boolean).join(" \xB7 ")
|
|
3132
|
+
});
|
|
3133
|
+
for (const m of messages) {
|
|
3134
|
+
out.push({
|
|
3135
|
+
at: m.at,
|
|
3136
|
+
kind: "message",
|
|
3137
|
+
label: `${m.authorName} replied`,
|
|
3138
|
+
actor: m.fromAgent ? m.authorName : void 0
|
|
3139
|
+
});
|
|
2911
3140
|
}
|
|
2912
|
-
|
|
2913
|
-
|
|
3141
|
+
const stage = normalizeStage(input.status);
|
|
3142
|
+
const reached = (s) => STAGE_ORDER.indexOf(stage) >= STAGE_ORDER.indexOf(s);
|
|
3143
|
+
if (input.proposal || reached("in_review")) {
|
|
3144
|
+
const agent = input.proposal?.author ?? "Claude Code";
|
|
3145
|
+
out.push({
|
|
3146
|
+
at: input.proposal?.createdAt ?? latest,
|
|
3147
|
+
kind: "sent",
|
|
3148
|
+
label: `Change ready from ${agent}`,
|
|
3149
|
+
detail: "The rewritten markup is on the thread.",
|
|
3150
|
+
actor: agent
|
|
3151
|
+
});
|
|
2914
3152
|
}
|
|
2915
|
-
|
|
2916
|
-
|
|
2917
|
-
|
|
2918
|
-
|
|
2919
|
-
|
|
2920
|
-
|
|
2921
|
-
|
|
2922
|
-
|
|
2923
|
-
|
|
2924
|
-
} catch {
|
|
2925
|
-
}
|
|
3153
|
+
if (input.pr) {
|
|
3154
|
+
out.push({
|
|
3155
|
+
at: latest,
|
|
3156
|
+
kind: "pr",
|
|
3157
|
+
label: input.pr.number ? `Pull request #${input.pr.number} opened` : "Pull request opened",
|
|
3158
|
+
detail: input.pr.state === "merged" ? "Merged." : input.pr.state === "closed" ? "Closed without merging." : "Waiting on review."
|
|
3159
|
+
});
|
|
3160
|
+
if (input.pr.previewUrl) {
|
|
3161
|
+
out.push({ at: latest, kind: "preview", label: "Preview live", detail: input.pr.previewUrl });
|
|
2926
3162
|
}
|
|
2927
|
-
return out.sort((a, b) => b.createdAt.localeCompare(a.createdAt));
|
|
2928
3163
|
}
|
|
2929
|
-
|
|
2930
|
-
|
|
3164
|
+
if (reached("in_review")) {
|
|
3165
|
+
out.push({ at: latest, kind: "review", label: "Waiting on a human review", detail: "Only a person can close this." });
|
|
3166
|
+
}
|
|
3167
|
+
if (stage === "resolved") {
|
|
3168
|
+
out.push({ at: latest, kind: "resolved", label: "Resolved", detail: "A person closed this." });
|
|
3169
|
+
}
|
|
3170
|
+
return out;
|
|
3171
|
+
}
|
|
3172
|
+
var STAGE_ORDER = ["queue", "todo", "in_progress", "in_review", "resolved"];
|
|
3173
|
+
function normalizeStage(status) {
|
|
3174
|
+
if (status === "open")
|
|
3175
|
+
return "queue";
|
|
3176
|
+
if (status === "done")
|
|
3177
|
+
return "resolved";
|
|
3178
|
+
return status;
|
|
3179
|
+
}
|
|
3180
|
+
function threadAsText(input, messages = []) {
|
|
3181
|
+
const lines = [
|
|
3182
|
+
`# ${input.title ?? "Feedback"} (#${input.id})`,
|
|
3183
|
+
"",
|
|
3184
|
+
`- Stage: ${normalizeStage(input.status)}`,
|
|
3185
|
+
`- Page: ${input.url ?? "\u2014"}`
|
|
3186
|
+
];
|
|
3187
|
+
if (input.anchor?.selector)
|
|
3188
|
+
lines.push(`- Target: \`${input.anchor.selector}\``);
|
|
3189
|
+
if (input.pr?.number)
|
|
3190
|
+
lines.push(`- PR: #${input.pr.number}${input.pr.previewUrl ? ` \xB7 preview ${input.pr.previewUrl}` : ""}`);
|
|
3191
|
+
lines.push("", "## Request", "", input.body);
|
|
3192
|
+
if (messages.length) {
|
|
3193
|
+
lines.push("", "## Conversation", "");
|
|
3194
|
+
for (const m of messages)
|
|
3195
|
+
lines.push(`**${m.authorName}**${m.fromAgent ? " (agent)" : ""} \u2014 ${m.at}`, "", m.body, "");
|
|
3196
|
+
}
|
|
3197
|
+
return lines.join("\n");
|
|
3198
|
+
}
|
|
3199
|
+
|
|
3200
|
+
// ../shared/dist/mentions.js
|
|
3201
|
+
var HANDLE = /^[A-Za-z0-9][A-Za-z0-9._-]*/;
|
|
3202
|
+
function excludedRanges(body) {
|
|
3203
|
+
const ranges = [];
|
|
3204
|
+
for (const m of body.matchAll(/```[\s\S]*?```/g))
|
|
3205
|
+
ranges.push([m.index, m.index + m[0].length]);
|
|
3206
|
+
const fenced = (i) => ranges.some(([a, b]) => i >= a && i < b);
|
|
3207
|
+
for (const m of body.matchAll(/`[^`\n]*`/g)) {
|
|
3208
|
+
if (!fenced(m.index))
|
|
3209
|
+
ranges.push([m.index, m.index + m[0].length]);
|
|
3210
|
+
}
|
|
3211
|
+
return ranges;
|
|
3212
|
+
}
|
|
3213
|
+
function parseMentions(body) {
|
|
3214
|
+
if (!body)
|
|
3215
|
+
return [];
|
|
3216
|
+
const excluded = excludedRanges(body);
|
|
3217
|
+
const inExcluded = (i) => excluded.some(([a, b]) => i >= a && i < b);
|
|
3218
|
+
const out = [];
|
|
3219
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3220
|
+
for (let i = 0; i < body.length; i++) {
|
|
3221
|
+
if (body[i] !== "@")
|
|
3222
|
+
continue;
|
|
3223
|
+
if (inExcluded(i))
|
|
3224
|
+
continue;
|
|
3225
|
+
const prev = i > 0 ? body[i - 1] : "";
|
|
3226
|
+
if (prev && !/[\s([{<"'*_~]/.test(prev))
|
|
3227
|
+
continue;
|
|
3228
|
+
const match = HANDLE.exec(body.slice(i + 1));
|
|
3229
|
+
if (!match)
|
|
3230
|
+
continue;
|
|
3231
|
+
const raw = match[0];
|
|
3232
|
+
const handle = raw.replace(/[._-]+$/, "");
|
|
3233
|
+
if (!handle)
|
|
3234
|
+
continue;
|
|
3235
|
+
const key = handle.toLowerCase();
|
|
3236
|
+
if (seen.has(key))
|
|
3237
|
+
continue;
|
|
3238
|
+
seen.add(key);
|
|
3239
|
+
out.push({ handle, start: i, length: handle.length + 1 });
|
|
3240
|
+
}
|
|
3241
|
+
return out;
|
|
3242
|
+
}
|
|
3243
|
+
function mentionSegments(body, mentions) {
|
|
3244
|
+
if (!mentions.length)
|
|
3245
|
+
return [{ text: body, mention: false }];
|
|
3246
|
+
const sorted = [...mentions].sort((a, b) => a.start - b.start);
|
|
3247
|
+
const out = [];
|
|
3248
|
+
let at = 0;
|
|
3249
|
+
for (const m of sorted) {
|
|
3250
|
+
if (m.start > at)
|
|
3251
|
+
out.push({ text: body.slice(at, m.start), mention: false });
|
|
3252
|
+
out.push({ text: body.slice(m.start, m.start + m.length), mention: true });
|
|
3253
|
+
at = m.start + m.length;
|
|
3254
|
+
}
|
|
3255
|
+
if (at < body.length)
|
|
3256
|
+
out.push({ text: body.slice(at), mention: false });
|
|
3257
|
+
return out;
|
|
3258
|
+
}
|
|
3259
|
+
function mentionSuggestions(body, caret, candidates) {
|
|
3260
|
+
const before = body.slice(0, caret);
|
|
3261
|
+
const at = before.lastIndexOf("@");
|
|
3262
|
+
if (at < 0)
|
|
3263
|
+
return [];
|
|
3264
|
+
const typed = before.slice(at + 1);
|
|
3265
|
+
if (/\s/.test(typed))
|
|
3266
|
+
return [];
|
|
3267
|
+
const q = typed.toLowerCase();
|
|
3268
|
+
return candidates.filter((c) => !q || c.name.toLowerCase().includes(q) || c.id.toLowerCase().includes(q)).slice(0, 6);
|
|
3269
|
+
}
|
|
3270
|
+
|
|
3271
|
+
// ../shared/dist/needs-you.js
|
|
3272
|
+
var NEEDS_YOU_LABELS = {
|
|
3273
|
+
review: "Waiting on your review",
|
|
3274
|
+
question: "The agent asked you something",
|
|
3275
|
+
failed: "The agent's run failed"
|
|
3276
|
+
};
|
|
3277
|
+
function looksLikeQuestion(body) {
|
|
3278
|
+
const lines = body.split("\n").map((l) => l.trim()).filter(Boolean);
|
|
3279
|
+
const last = lines[lines.length - 1];
|
|
3280
|
+
if (!last)
|
|
3281
|
+
return false;
|
|
3282
|
+
return /\?["')\]]*$/.test(last);
|
|
3283
|
+
}
|
|
3284
|
+
function needsYou(input) {
|
|
3285
|
+
const stage = normalizeStage2(input.status);
|
|
3286
|
+
if (stage === "resolved")
|
|
3287
|
+
return { needs: false };
|
|
3288
|
+
if (input.agentFailed)
|
|
3289
|
+
return { needs: true, reason: "failed", label: NEEDS_YOU_LABELS.failed };
|
|
3290
|
+
if (input.last?.fromAgent && looksLikeQuestion(input.last.body)) {
|
|
3291
|
+
return { needs: true, reason: "question", label: NEEDS_YOU_LABELS.question };
|
|
3292
|
+
}
|
|
3293
|
+
if (stage === "in_review")
|
|
3294
|
+
return { needs: true, reason: "review", label: NEEDS_YOU_LABELS.review };
|
|
3295
|
+
return { needs: false };
|
|
3296
|
+
}
|
|
3297
|
+
function normalizeStage2(status) {
|
|
3298
|
+
if (status === "open")
|
|
3299
|
+
return "queue";
|
|
3300
|
+
if (status === "done")
|
|
3301
|
+
return "resolved";
|
|
3302
|
+
return status;
|
|
3303
|
+
}
|
|
3304
|
+
|
|
3305
|
+
// ../shared/dist/reactions.js
|
|
3306
|
+
var REACTION_CHOICES = ["\u{1F44D}", "\u{1F389}", "\u{1F440}", "\u{1F64F}", "\u2764\uFE0F", "\u{1F680}"];
|
|
3307
|
+
function summarizeReactions(reactions, viewerId) {
|
|
3308
|
+
const byEmoji = /* @__PURE__ */ new Map();
|
|
3309
|
+
for (const r of reactions) {
|
|
3310
|
+
const list = byEmoji.get(r.emoji) ?? [];
|
|
3311
|
+
list.push(r);
|
|
3312
|
+
byEmoji.set(r.emoji, list);
|
|
3313
|
+
}
|
|
3314
|
+
const rank = (emoji) => {
|
|
3315
|
+
const at = REACTION_CHOICES.indexOf(emoji);
|
|
3316
|
+
return at < 0 ? REACTION_CHOICES.length : at;
|
|
3317
|
+
};
|
|
3318
|
+
return [...byEmoji.entries()].map(([emoji, list]) => ({
|
|
3319
|
+
emoji,
|
|
3320
|
+
count: list.length,
|
|
3321
|
+
mine: viewerId ? list.some((r) => r.userId === viewerId) : false,
|
|
3322
|
+
users: list.map((r) => r.userName ?? r.userId)
|
|
3323
|
+
})).sort((a, b) => b.count - a.count || rank(a.emoji) - rank(b.emoji) || a.emoji.localeCompare(b.emoji));
|
|
3324
|
+
}
|
|
3325
|
+
function toggleReaction(reactions, next) {
|
|
3326
|
+
const has = reactions.some((r) => r.messageId === next.messageId && r.emoji === next.emoji && r.userId === next.userId);
|
|
3327
|
+
if (has) {
|
|
3328
|
+
return reactions.filter((r) => !(r.messageId === next.messageId && r.emoji === next.emoji && r.userId === next.userId));
|
|
3329
|
+
}
|
|
3330
|
+
return [...reactions, next];
|
|
3331
|
+
}
|
|
3332
|
+
|
|
3333
|
+
// ../shared/dist/presence.js
|
|
3334
|
+
var PEER_HEARTBEAT_MS = 6e3;
|
|
3335
|
+
function initialsOf(name) {
|
|
3336
|
+
const words = name.trim().split(/\s+/).filter(Boolean);
|
|
3337
|
+
if (!words.length)
|
|
3338
|
+
return "?";
|
|
3339
|
+
if (words.length === 1)
|
|
3340
|
+
return words[0].slice(0, 1).toUpperCase();
|
|
3341
|
+
return (words[0].slice(0, 1) + words[words.length - 1].slice(0, 1)).toUpperCase();
|
|
3342
|
+
}
|
|
3343
|
+
|
|
3344
|
+
// ../shared/dist/index.js
|
|
3345
|
+
var COMMENT_STAGES = ["queue", "todo", "in_progress", "in_review", "resolved"];
|
|
3346
|
+
var STAGE_LABELS = {
|
|
3347
|
+
queue: "Queue",
|
|
3348
|
+
todo: "To Do",
|
|
3349
|
+
in_progress: "In Progress",
|
|
3350
|
+
in_review: "In Review",
|
|
3351
|
+
resolved: "Resolved"
|
|
3352
|
+
};
|
|
3353
|
+
var LEGACY_STATUS = {
|
|
3354
|
+
open: "queue",
|
|
3355
|
+
in_progress: "in_progress",
|
|
3356
|
+
done: "resolved"
|
|
3357
|
+
};
|
|
3358
|
+
function normalizeStatus(value) {
|
|
3359
|
+
if (typeof value === "string") {
|
|
3360
|
+
if (COMMENT_STAGES.includes(value))
|
|
3361
|
+
return value;
|
|
3362
|
+
if (value in LEGACY_STATUS)
|
|
3363
|
+
return LEGACY_STATUS[value];
|
|
3364
|
+
}
|
|
3365
|
+
return "queue";
|
|
3366
|
+
}
|
|
3367
|
+
var COMMENT_PRIORITIES = ["critical", "high", "medium", "low"];
|
|
3368
|
+
var PRIORITY_LABELS = {
|
|
3369
|
+
critical: "Critical",
|
|
3370
|
+
high: "High",
|
|
3371
|
+
medium: "Medium",
|
|
3372
|
+
low: "Low"
|
|
3373
|
+
};
|
|
3374
|
+
var CHANGE_TYPES = ["frontend", "backend", "api", "other"];
|
|
3375
|
+
var CHANGE_TYPE_LABELS = {
|
|
3376
|
+
frontend: "Frontend",
|
|
3377
|
+
backend: "Backend",
|
|
3378
|
+
api: "API",
|
|
3379
|
+
other: "Other"
|
|
3380
|
+
};
|
|
3381
|
+
var DEFAULT_PRIORITY = "medium";
|
|
3382
|
+
var DEFAULT_CHANGE_TYPE = "other";
|
|
3383
|
+
function normalizePriority(value) {
|
|
3384
|
+
if (typeof value === "string" && COMMENT_PRIORITIES.includes(value)) {
|
|
3385
|
+
return value;
|
|
3386
|
+
}
|
|
3387
|
+
return DEFAULT_PRIORITY;
|
|
3388
|
+
}
|
|
3389
|
+
|
|
3390
|
+
// src/store.ts
|
|
3391
|
+
var LocalStorageAdapter = class {
|
|
3392
|
+
key(projectKey, url) {
|
|
3393
|
+
return `loupe:${projectKey}:${url}`;
|
|
3394
|
+
}
|
|
3395
|
+
readAll(projectKey, url) {
|
|
3396
|
+
try {
|
|
3397
|
+
const raw = localStorage.getItem(this.key(projectKey, url));
|
|
3398
|
+
return raw ? JSON.parse(raw) : [];
|
|
3399
|
+
} catch {
|
|
3400
|
+
return [];
|
|
3401
|
+
}
|
|
3402
|
+
}
|
|
3403
|
+
writeAll(projectKey, url, comments) {
|
|
3404
|
+
localStorage.setItem(this.key(projectKey, url), JSON.stringify(comments));
|
|
3405
|
+
}
|
|
3406
|
+
async list(projectKey, url) {
|
|
3407
|
+
return this.readAll(projectKey, url);
|
|
3408
|
+
}
|
|
3409
|
+
/** Every page's comments for this project, newest first — the "All" scope. */
|
|
3410
|
+
async listAll(projectKey) {
|
|
3411
|
+
const prefix = `loupe:${projectKey}:`;
|
|
3412
|
+
const out = [];
|
|
3413
|
+
for (let i = 0; i < localStorage.length; i++) {
|
|
3414
|
+
const k = localStorage.key(i);
|
|
3415
|
+
if (!k || !k.startsWith(prefix)) continue;
|
|
3416
|
+
try {
|
|
3417
|
+
out.push(...JSON.parse(localStorage.getItem(k) || "[]"));
|
|
3418
|
+
} catch {
|
|
3419
|
+
}
|
|
3420
|
+
}
|
|
3421
|
+
return out.sort((a, b) => b.createdAt.localeCompare(a.createdAt));
|
|
3422
|
+
}
|
|
3423
|
+
async save(comment) {
|
|
3424
|
+
const all = this.readAll(comment.projectKey, comment.url);
|
|
2931
3425
|
all.push(comment);
|
|
2932
3426
|
this.writeAll(comment.projectKey, comment.url, all);
|
|
2933
3427
|
return comment;
|
|
@@ -2993,6 +3487,27 @@ var LocalStorageAdapter = class {
|
|
|
2993
3487
|
localStorage.setItem(this.msgKey(threadId), JSON.stringify(all));
|
|
2994
3488
|
return stored;
|
|
2995
3489
|
}
|
|
3490
|
+
/** Offline reactions, keyed per thread. */
|
|
3491
|
+
async listReactions(threadId) {
|
|
3492
|
+
try {
|
|
3493
|
+
const raw = localStorage.getItem(`loupe:rxn:${threadId}`);
|
|
3494
|
+
const parsed = raw ? JSON.parse(raw) : [];
|
|
3495
|
+
return Array.isArray(parsed) ? parsed : [];
|
|
3496
|
+
} catch {
|
|
3497
|
+
return [];
|
|
3498
|
+
}
|
|
3499
|
+
}
|
|
3500
|
+
async toggleReaction(input) {
|
|
3501
|
+
const current2 = await this.listReactions(input.threadId);
|
|
3502
|
+
const next = toggleReaction(current2, {
|
|
3503
|
+
messageId: input.messageId,
|
|
3504
|
+
emoji: input.emoji,
|
|
3505
|
+
userId: input.userId,
|
|
3506
|
+
userName: input.userName
|
|
3507
|
+
});
|
|
3508
|
+
localStorage.setItem(`loupe:rxn:${input.threadId}`, JSON.stringify(next));
|
|
3509
|
+
return next;
|
|
3510
|
+
}
|
|
2996
3511
|
/** Offline: the people are whoever has already commented locally. */
|
|
2997
3512
|
async listPeople(projectKey) {
|
|
2998
3513
|
const prefix = `loupe:${projectKey}:`;
|
|
@@ -3114,471 +3629,71 @@ var HttpAdapter = class {
|
|
|
3114
3629
|
return await res.json();
|
|
3115
3630
|
}
|
|
3116
3631
|
async addMessage(threadId, message) {
|
|
3117
|
-
const res = await fetch(`${this.base}/v1/comments/${encodeURIComponent(threadId)}/messages`, this.opts({
|
|
3118
|
-
method: "POST",
|
|
3119
|
-
headers: this.headers(),
|
|
3120
|
-
body: JSON.stringify(message)
|
|
3121
|
-
}));
|
|
3122
|
-
if (!res.ok) throw new Error(`addMessage failed: ${res.status}`);
|
|
3123
|
-
return await res.json();
|
|
3124
|
-
}
|
|
3125
|
-
/** Everyone who has taken part in this project, for mention resolution. */
|
|
3126
|
-
async listPeople(projectKey) {
|
|
3127
|
-
const q = new URLSearchParams({ projectKey });
|
|
3128
|
-
const res = await fetch(`${this.base}/v1/people?${q}`, this.opts({ headers: this.headers() }));
|
|
3129
|
-
if (!res.ok) throw new Error(`listPeople failed: ${res.status}`);
|
|
3130
|
-
return await res.json();
|
|
3131
|
-
}
|
|
3132
|
-
async listNotifications(projectKey, recipient) {
|
|
3133
|
-
const q = new URLSearchParams({ projectKey, recipient });
|
|
3134
|
-
const res = await fetch(`${this.base}/v1/notifications?${q}`, this.opts({ headers: this.headers() }));
|
|
3135
|
-
if (!res.ok) throw new Error(`listNotifications failed: ${res.status}`);
|
|
3136
|
-
return (await res.json()).notifications;
|
|
3137
|
-
}
|
|
3138
|
-
async markNotificationsRead(projectKey, recipient, id) {
|
|
3139
|
-
await fetch(`${this.base}/v1/notifications/read`, this.opts({
|
|
3140
|
-
method: "POST",
|
|
3141
|
-
headers: this.headers(),
|
|
3142
|
-
body: JSON.stringify({ projectKey, recipient, id })
|
|
3143
|
-
}));
|
|
3144
|
-
}
|
|
3145
|
-
async update(id, patch) {
|
|
3146
|
-
const res = await fetch(`${this.base}/v1/comments/${encodeURIComponent(id)}`, this.opts({
|
|
3147
|
-
method: "PATCH",
|
|
3148
|
-
headers: this.headers(),
|
|
3149
|
-
body: JSON.stringify(patch)
|
|
3150
|
-
}));
|
|
3151
|
-
if (!res.ok) throw new Error(`update failed: ${res.status}`);
|
|
3152
|
-
}
|
|
3153
|
-
async remove(id) {
|
|
3154
|
-
const res = await fetch(`${this.base}/v1/comments/${encodeURIComponent(id)}`, this.opts({
|
|
3155
|
-
method: "DELETE",
|
|
3156
|
-
headers: this.headers()
|
|
3157
|
-
}));
|
|
3158
|
-
if (!res.ok && res.status !== 404) throw new Error(`remove failed: ${res.status}`);
|
|
3159
|
-
}
|
|
3160
|
-
};
|
|
3161
|
-
|
|
3162
|
-
// ../shared/dist/activity.js
|
|
3163
|
-
var ACTIVITY_STATUS_LABELS = {
|
|
3164
|
-
idle: "Idle",
|
|
3165
|
-
working: "Working",
|
|
3166
|
-
error: "Error"
|
|
3167
|
-
};
|
|
3168
|
-
function formatDuration(ms) {
|
|
3169
|
-
if (!ms || ms < 0)
|
|
3170
|
-
return "\u2014";
|
|
3171
|
-
const s = Math.round(ms / 1e3);
|
|
3172
|
-
if (s < 60)
|
|
3173
|
-
return `${s}s`;
|
|
3174
|
-
const m = Math.floor(s / 60);
|
|
3175
|
-
const rest = s % 60;
|
|
3176
|
-
if (m < 60)
|
|
3177
|
-
return rest ? `${m}m ${rest}s` : `${m}m`;
|
|
3178
|
-
const h = Math.floor(m / 60);
|
|
3179
|
-
return `${h}h ${m % 60}m`;
|
|
3180
|
-
}
|
|
3181
|
-
function summarizeActivity(events, status = "idle", now = Date.now()) {
|
|
3182
|
-
const files = /* @__PURE__ */ new Set();
|
|
3183
|
-
const kinds = /* @__PURE__ */ new Map();
|
|
3184
|
-
let errors = 0;
|
|
3185
|
-
let first = Infinity;
|
|
3186
|
-
for (const e of events) {
|
|
3187
|
-
for (const f of e.files ?? [])
|
|
3188
|
-
files.add(f);
|
|
3189
|
-
kinds.set(e.kind, (kinds.get(e.kind) ?? 0) + 1);
|
|
3190
|
-
if (e.level === "error")
|
|
3191
|
-
errors++;
|
|
3192
|
-
const t = Date.parse(e.at);
|
|
3193
|
-
if (!Number.isNaN(t))
|
|
3194
|
-
first = Math.min(first, t);
|
|
3195
|
-
}
|
|
3196
|
-
return {
|
|
3197
|
-
status,
|
|
3198
|
-
events: events.length,
|
|
3199
|
-
errors,
|
|
3200
|
-
files: files.size,
|
|
3201
|
-
durationMs: events.length && first !== Infinity ? Math.max(0, now - first) : 0,
|
|
3202
|
-
byKind: [...kinds.entries()].map(([kind, count]) => ({ kind, count })).sort((a, b) => b.count - a.count || a.kind.localeCompare(b.kind))
|
|
3203
|
-
};
|
|
3204
|
-
}
|
|
3205
|
-
|
|
3206
|
-
// ../shared/dist/lifecycle.js
|
|
3207
|
-
var LIFECYCLE_LABELS = {
|
|
3208
|
-
sent: "Sent to agent",
|
|
3209
|
-
in_pr: "In PR",
|
|
3210
|
-
preview: "Review preview",
|
|
3211
|
-
reviewed: "Reviewed"
|
|
3212
|
-
};
|
|
3213
|
-
function lifecycle(c) {
|
|
3214
|
-
const hasProposal = !!c.proposal;
|
|
3215
|
-
const pr = c.pr ?? void 0;
|
|
3216
|
-
const withPr = (stage) => {
|
|
3217
|
-
const out = { stage, label: LIFECYCLE_LABELS[stage] };
|
|
3218
|
-
if (pr) {
|
|
3219
|
-
out.pr = pr;
|
|
3220
|
-
if (typeof pr.checksTotal === "number" && pr.checksTotal > 0) {
|
|
3221
|
-
const passed = Math.max(0, Math.min(pr.checksTotal, pr.checksPassed ?? 0));
|
|
3222
|
-
out.checks = {
|
|
3223
|
-
text: `${passed}/${pr.checksTotal}`,
|
|
3224
|
-
ratio: passed / pr.checksTotal
|
|
3225
|
-
};
|
|
3226
|
-
}
|
|
3227
|
-
}
|
|
3228
|
-
return out;
|
|
3229
|
-
};
|
|
3230
|
-
if (c.status === "resolved")
|
|
3231
|
-
return hasProposal || pr ? withPr("reviewed") : null;
|
|
3232
|
-
if (pr)
|
|
3233
|
-
return withPr("in_pr");
|
|
3234
|
-
if (c.status === "in_review")
|
|
3235
|
-
return { stage: "preview", label: LIFECYCLE_LABELS.preview };
|
|
3236
|
-
if (hasProposal)
|
|
3237
|
-
return { stage: "sent", label: LIFECYCLE_LABELS.sent };
|
|
3238
|
-
return null;
|
|
3239
|
-
}
|
|
3240
|
-
function awaitingReview(comments) {
|
|
3241
|
-
return comments.filter((c) => c.status === "in_review");
|
|
3242
|
-
}
|
|
3243
|
-
|
|
3244
|
-
// ../shared/dist/iteration.js
|
|
3245
|
-
function emptyIterations() {
|
|
3246
|
-
return { items: [], index: -1 };
|
|
3247
|
-
}
|
|
3248
|
-
function current(state) {
|
|
3249
|
-
return state.index >= 0 ? state.items[state.index] ?? null : null;
|
|
3250
|
-
}
|
|
3251
|
-
function addIteration(state, iteration) {
|
|
3252
|
-
const head = state.items.slice(0, state.index + 1);
|
|
3253
|
-
const items = [...head, iteration].slice(-MAX_ITERATIONS);
|
|
3254
|
-
return { items, index: items.length - 1 };
|
|
3255
|
-
}
|
|
3256
|
-
var MAX_ITERATIONS = 20;
|
|
3257
|
-
function canUndo(state) {
|
|
3258
|
-
return state.items.length > 0;
|
|
3259
|
-
}
|
|
3260
|
-
function undo(state) {
|
|
3261
|
-
if (!state.items.length)
|
|
3262
|
-
return state;
|
|
3263
|
-
const items = state.items.slice(0, -1);
|
|
3264
|
-
return { items, index: items.length - 1 };
|
|
3265
|
-
}
|
|
3266
|
-
function canMove(state, delta) {
|
|
3267
|
-
const next = state.index + delta;
|
|
3268
|
-
return next >= 0 && next < state.items.length;
|
|
3269
|
-
}
|
|
3270
|
-
function move(state, delta) {
|
|
3271
|
-
return canMove(state, delta) ? { ...state, index: state.index + delta } : state;
|
|
3272
|
-
}
|
|
3273
|
-
function stackLabel(state) {
|
|
3274
|
-
return state.items.length ? `${state.index + 1} / ${state.items.length}` : "";
|
|
3275
|
-
}
|
|
3276
|
-
|
|
3277
|
-
// ../shared/dist/consent.js
|
|
3278
|
-
function emptyConsent() {
|
|
3279
|
-
return { state: "idle", request: null, history: [] };
|
|
3280
|
-
}
|
|
3281
|
-
function isNavigableUrl(raw) {
|
|
3282
|
-
try {
|
|
3283
|
-
const u = new URL(raw);
|
|
3284
|
-
return u.protocol === "http:" || u.protocol === "https:";
|
|
3285
|
-
} catch {
|
|
3286
|
-
return false;
|
|
3287
|
-
}
|
|
3288
|
-
}
|
|
3289
|
-
function requestNavigation(record, request) {
|
|
3290
|
-
if (!isNavigableUrl(request.url))
|
|
3291
|
-
return record;
|
|
3292
|
-
return {
|
|
3293
|
-
state: "requested",
|
|
3294
|
-
request: {
|
|
3295
|
-
id: request.id ?? `nav${Date.now().toString(36)}`,
|
|
3296
|
-
at: request.at ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
3297
|
-
url: request.url,
|
|
3298
|
-
reason: request.reason,
|
|
3299
|
-
requester: request.requester
|
|
3300
|
-
},
|
|
3301
|
-
history: record.history
|
|
3302
|
-
};
|
|
3303
|
-
}
|
|
3304
|
-
function isPending(record) {
|
|
3305
|
-
return record.state === "requested" && record.request !== null;
|
|
3306
|
-
}
|
|
3307
|
-
function decide(record, granted, now = (/* @__PURE__ */ new Date()).toISOString()) {
|
|
3308
|
-
if (!isPending(record))
|
|
3309
|
-
return { record, navigateTo: null };
|
|
3310
|
-
const url = record.request.url;
|
|
3311
|
-
return {
|
|
3312
|
-
record: {
|
|
3313
|
-
state: granted ? "granted" : "denied",
|
|
3314
|
-
request: null,
|
|
3315
|
-
history: [...record.history, { url, decision: granted ? "granted" : "denied", at: now }]
|
|
3316
|
-
},
|
|
3317
|
-
navigateTo: granted ? url : null
|
|
3318
|
-
};
|
|
3319
|
-
}
|
|
3320
|
-
function withdraw(record) {
|
|
3321
|
-
return isPending(record) ? { ...record, state: "idle", request: null } : record;
|
|
3322
|
-
}
|
|
3323
|
-
|
|
3324
|
-
// ../shared/dist/thread.js
|
|
3325
|
-
function firstMessageFromComment(comment) {
|
|
3326
|
-
return {
|
|
3327
|
-
id: `${comment.id}:0`,
|
|
3328
|
-
threadId: comment.id,
|
|
3329
|
-
author: { ...comment.author, type: "user" },
|
|
3330
|
-
body: comment.body,
|
|
3331
|
-
attachments: comment.attachments?.length ? comment.attachments : void 0,
|
|
3332
|
-
createdAt: comment.createdAt
|
|
3333
|
-
};
|
|
3334
|
-
}
|
|
3335
|
-
function threadConversation(comment, replies) {
|
|
3336
|
-
return [firstMessageFromComment(comment), ...replies].sort((a, b) => a.createdAt.localeCompare(b.createdAt));
|
|
3337
|
-
}
|
|
3338
|
-
function iterationLabel(link) {
|
|
3339
|
-
if (!link.parentThreadId || link.iterationType !== "revision")
|
|
3340
|
-
return null;
|
|
3341
|
-
return `Iteration ${link.iterationNumber ?? 2}`;
|
|
3342
|
-
}
|
|
3343
|
-
|
|
3344
|
-
// ../shared/dist/timeline.js
|
|
3345
|
-
function describeTarget(input) {
|
|
3346
|
-
const parts = [];
|
|
3347
|
-
if (input.anchor?.tag)
|
|
3348
|
-
parts.push(`a <${input.anchor.tag}>`);
|
|
3349
|
-
if (input.anchor?.selector)
|
|
3350
|
-
parts.push(`\`${input.anchor.selector}\``);
|
|
3351
|
-
return parts.length ? parts.join(" ") : input.kind === "free" ? "a page note" : "an element";
|
|
3352
|
-
}
|
|
3353
|
-
function threadTimeline(input, messages = []) {
|
|
3354
|
-
const out = [];
|
|
3355
|
-
const latest = messages.length ? messages[messages.length - 1].at : input.createdAt;
|
|
3356
|
-
out.push({
|
|
3357
|
-
at: input.createdAt,
|
|
3358
|
-
kind: "captured",
|
|
3359
|
-
label: "Feedback captured",
|
|
3360
|
-
detail: [input.title, describeTarget(input)].filter(Boolean).join(" \xB7 ")
|
|
3361
|
-
});
|
|
3362
|
-
for (const m of messages) {
|
|
3363
|
-
out.push({
|
|
3364
|
-
at: m.at,
|
|
3365
|
-
kind: "message",
|
|
3366
|
-
label: `${m.authorName} replied`,
|
|
3367
|
-
actor: m.fromAgent ? m.authorName : void 0
|
|
3368
|
-
});
|
|
3369
|
-
}
|
|
3370
|
-
const stage = normalizeStage(input.status);
|
|
3371
|
-
const reached = (s) => STAGE_ORDER.indexOf(stage) >= STAGE_ORDER.indexOf(s);
|
|
3372
|
-
if (input.proposal || reached("in_review")) {
|
|
3373
|
-
const agent = input.proposal?.author ?? "Claude Code";
|
|
3374
|
-
out.push({
|
|
3375
|
-
at: input.proposal?.createdAt ?? latest,
|
|
3376
|
-
kind: "sent",
|
|
3377
|
-
label: `Change ready from ${agent}`,
|
|
3378
|
-
detail: "The rewritten markup is on the thread.",
|
|
3379
|
-
actor: agent
|
|
3380
|
-
});
|
|
3381
|
-
}
|
|
3382
|
-
if (input.pr) {
|
|
3383
|
-
out.push({
|
|
3384
|
-
at: latest,
|
|
3385
|
-
kind: "pr",
|
|
3386
|
-
label: input.pr.number ? `Pull request #${input.pr.number} opened` : "Pull request opened",
|
|
3387
|
-
detail: input.pr.state === "merged" ? "Merged." : input.pr.state === "closed" ? "Closed without merging." : "Waiting on review."
|
|
3388
|
-
});
|
|
3389
|
-
if (input.pr.previewUrl) {
|
|
3390
|
-
out.push({ at: latest, kind: "preview", label: "Preview live", detail: input.pr.previewUrl });
|
|
3391
|
-
}
|
|
3392
|
-
}
|
|
3393
|
-
if (reached("in_review")) {
|
|
3394
|
-
out.push({ at: latest, kind: "review", label: "Waiting on a human review", detail: "Only a person can close this." });
|
|
3632
|
+
const res = await fetch(`${this.base}/v1/comments/${encodeURIComponent(threadId)}/messages`, this.opts({
|
|
3633
|
+
method: "POST",
|
|
3634
|
+
headers: this.headers(),
|
|
3635
|
+
body: JSON.stringify(message)
|
|
3636
|
+
}));
|
|
3637
|
+
if (!res.ok) throw new Error(`addMessage failed: ${res.status}`);
|
|
3638
|
+
return await res.json();
|
|
3395
3639
|
}
|
|
3396
|
-
|
|
3397
|
-
|
|
3640
|
+
async listReactions(threadId) {
|
|
3641
|
+
const res = await fetch(`${this.base}/v1/comments/${encodeURIComponent(threadId)}/messages/all/reactions`, this.opts({ headers: this.headers() }));
|
|
3642
|
+
if (res.status === 404) return [];
|
|
3643
|
+
if (!res.ok) throw new Error(`listReactions failed: ${res.status}`);
|
|
3644
|
+
return (await res.json()).reactions;
|
|
3398
3645
|
}
|
|
3399
|
-
|
|
3400
|
-
|
|
3401
|
-
|
|
3402
|
-
|
|
3403
|
-
|
|
3404
|
-
|
|
3405
|
-
|
|
3406
|
-
|
|
3407
|
-
|
|
3408
|
-
}
|
|
3409
|
-
|
|
3410
|
-
const lines = [
|
|
3411
|
-
`# ${input.title ?? "Feedback"} (#${input.id})`,
|
|
3412
|
-
"",
|
|
3413
|
-
`- Stage: ${normalizeStage(input.status)}`,
|
|
3414
|
-
`- Page: ${input.url ?? "\u2014"}`
|
|
3415
|
-
];
|
|
3416
|
-
if (input.anchor?.selector)
|
|
3417
|
-
lines.push(`- Target: \`${input.anchor.selector}\``);
|
|
3418
|
-
if (input.pr?.number)
|
|
3419
|
-
lines.push(`- PR: #${input.pr.number}${input.pr.previewUrl ? ` \xB7 preview ${input.pr.previewUrl}` : ""}`);
|
|
3420
|
-
lines.push("", "## Request", "", input.body);
|
|
3421
|
-
if (messages.length) {
|
|
3422
|
-
lines.push("", "## Conversation", "");
|
|
3423
|
-
for (const m of messages)
|
|
3424
|
-
lines.push(`**${m.authorName}**${m.fromAgent ? " (agent)" : ""} \u2014 ${m.at}`, "", m.body, "");
|
|
3646
|
+
async toggleReaction(input) {
|
|
3647
|
+
const res = await fetch(
|
|
3648
|
+
`${this.base}/v1/comments/${encodeURIComponent(input.threadId)}/messages/${encodeURIComponent(input.messageId)}/reactions`,
|
|
3649
|
+
this.opts({
|
|
3650
|
+
method: "POST",
|
|
3651
|
+
headers: this.headers(),
|
|
3652
|
+
body: JSON.stringify({ emoji: input.emoji, userId: input.userId, userName: input.userName })
|
|
3653
|
+
})
|
|
3654
|
+
);
|
|
3655
|
+
if (!res.ok) throw new Error(`toggleReaction failed: ${res.status}`);
|
|
3656
|
+
return (await res.json()).reactions;
|
|
3425
3657
|
}
|
|
3426
|
-
|
|
3427
|
-
|
|
3428
|
-
|
|
3429
|
-
|
|
3430
|
-
|
|
3431
|
-
|
|
3432
|
-
const ranges = [];
|
|
3433
|
-
for (const m of body.matchAll(/```[\s\S]*?```/g))
|
|
3434
|
-
ranges.push([m.index, m.index + m[0].length]);
|
|
3435
|
-
const fenced = (i) => ranges.some(([a, b]) => i >= a && i < b);
|
|
3436
|
-
for (const m of body.matchAll(/`[^`\n]*`/g)) {
|
|
3437
|
-
if (!fenced(m.index))
|
|
3438
|
-
ranges.push([m.index, m.index + m[0].length]);
|
|
3658
|
+
/** Everyone who has taken part in this project, for mention resolution. */
|
|
3659
|
+
async listPeople(projectKey) {
|
|
3660
|
+
const q = new URLSearchParams({ projectKey });
|
|
3661
|
+
const res = await fetch(`${this.base}/v1/people?${q}`, this.opts({ headers: this.headers() }));
|
|
3662
|
+
if (!res.ok) throw new Error(`listPeople failed: ${res.status}`);
|
|
3663
|
+
return await res.json();
|
|
3439
3664
|
}
|
|
3440
|
-
|
|
3441
|
-
}
|
|
3442
|
-
|
|
3443
|
-
|
|
3444
|
-
return
|
|
3445
|
-
const excluded = excludedRanges(body);
|
|
3446
|
-
const inExcluded = (i) => excluded.some(([a, b]) => i >= a && i < b);
|
|
3447
|
-
const out = [];
|
|
3448
|
-
const seen = /* @__PURE__ */ new Set();
|
|
3449
|
-
for (let i = 0; i < body.length; i++) {
|
|
3450
|
-
if (body[i] !== "@")
|
|
3451
|
-
continue;
|
|
3452
|
-
if (inExcluded(i))
|
|
3453
|
-
continue;
|
|
3454
|
-
const prev = i > 0 ? body[i - 1] : "";
|
|
3455
|
-
if (prev && !/[\s([{<"'*_~]/.test(prev))
|
|
3456
|
-
continue;
|
|
3457
|
-
const match = HANDLE.exec(body.slice(i + 1));
|
|
3458
|
-
if (!match)
|
|
3459
|
-
continue;
|
|
3460
|
-
const raw = match[0];
|
|
3461
|
-
const handle = raw.replace(/[._-]+$/, "");
|
|
3462
|
-
if (!handle)
|
|
3463
|
-
continue;
|
|
3464
|
-
const key = handle.toLowerCase();
|
|
3465
|
-
if (seen.has(key))
|
|
3466
|
-
continue;
|
|
3467
|
-
seen.add(key);
|
|
3468
|
-
out.push({ handle, start: i, length: handle.length + 1 });
|
|
3665
|
+
async listNotifications(projectKey, recipient) {
|
|
3666
|
+
const q = new URLSearchParams({ projectKey, recipient });
|
|
3667
|
+
const res = await fetch(`${this.base}/v1/notifications?${q}`, this.opts({ headers: this.headers() }));
|
|
3668
|
+
if (!res.ok) throw new Error(`listNotifications failed: ${res.status}`);
|
|
3669
|
+
return (await res.json()).notifications;
|
|
3469
3670
|
}
|
|
3470
|
-
|
|
3471
|
-
}
|
|
3472
|
-
|
|
3473
|
-
|
|
3474
|
-
|
|
3475
|
-
|
|
3476
|
-
const out = [];
|
|
3477
|
-
let at = 0;
|
|
3478
|
-
for (const m of sorted) {
|
|
3479
|
-
if (m.start > at)
|
|
3480
|
-
out.push({ text: body.slice(at, m.start), mention: false });
|
|
3481
|
-
out.push({ text: body.slice(m.start, m.start + m.length), mention: true });
|
|
3482
|
-
at = m.start + m.length;
|
|
3671
|
+
async markNotificationsRead(projectKey, recipient, id) {
|
|
3672
|
+
await fetch(`${this.base}/v1/notifications/read`, this.opts({
|
|
3673
|
+
method: "POST",
|
|
3674
|
+
headers: this.headers(),
|
|
3675
|
+
body: JSON.stringify({ projectKey, recipient, id })
|
|
3676
|
+
}));
|
|
3483
3677
|
}
|
|
3484
|
-
|
|
3485
|
-
|
|
3486
|
-
|
|
3487
|
-
|
|
3488
|
-
|
|
3489
|
-
|
|
3490
|
-
|
|
3491
|
-
if (at < 0)
|
|
3492
|
-
return [];
|
|
3493
|
-
const typed = before.slice(at + 1);
|
|
3494
|
-
if (/\s/.test(typed))
|
|
3495
|
-
return [];
|
|
3496
|
-
const q = typed.toLowerCase();
|
|
3497
|
-
return candidates.filter((c) => !q || c.name.toLowerCase().includes(q) || c.id.toLowerCase().includes(q)).slice(0, 6);
|
|
3498
|
-
}
|
|
3499
|
-
|
|
3500
|
-
// ../shared/dist/needs-you.js
|
|
3501
|
-
var NEEDS_YOU_LABELS = {
|
|
3502
|
-
review: "Waiting on your review",
|
|
3503
|
-
question: "The agent asked you something",
|
|
3504
|
-
failed: "The agent's run failed"
|
|
3505
|
-
};
|
|
3506
|
-
function looksLikeQuestion(body) {
|
|
3507
|
-
const lines = body.split("\n").map((l) => l.trim()).filter(Boolean);
|
|
3508
|
-
const last = lines[lines.length - 1];
|
|
3509
|
-
if (!last)
|
|
3510
|
-
return false;
|
|
3511
|
-
return /\?["')\]]*$/.test(last);
|
|
3512
|
-
}
|
|
3513
|
-
function needsYou(input) {
|
|
3514
|
-
const stage = normalizeStage2(input.status);
|
|
3515
|
-
if (stage === "resolved")
|
|
3516
|
-
return { needs: false };
|
|
3517
|
-
if (input.agentFailed)
|
|
3518
|
-
return { needs: true, reason: "failed", label: NEEDS_YOU_LABELS.failed };
|
|
3519
|
-
if (input.last?.fromAgent && looksLikeQuestion(input.last.body)) {
|
|
3520
|
-
return { needs: true, reason: "question", label: NEEDS_YOU_LABELS.question };
|
|
3678
|
+
async update(id, patch) {
|
|
3679
|
+
const res = await fetch(`${this.base}/v1/comments/${encodeURIComponent(id)}`, this.opts({
|
|
3680
|
+
method: "PATCH",
|
|
3681
|
+
headers: this.headers(),
|
|
3682
|
+
body: JSON.stringify(patch)
|
|
3683
|
+
}));
|
|
3684
|
+
if (!res.ok) throw new Error(`update failed: ${res.status}`);
|
|
3521
3685
|
}
|
|
3522
|
-
|
|
3523
|
-
|
|
3524
|
-
|
|
3525
|
-
|
|
3526
|
-
|
|
3527
|
-
|
|
3528
|
-
return "queue";
|
|
3529
|
-
if (status === "done")
|
|
3530
|
-
return "resolved";
|
|
3531
|
-
return status;
|
|
3532
|
-
}
|
|
3533
|
-
|
|
3534
|
-
// ../shared/dist/index.js
|
|
3535
|
-
var COMMENT_STAGES = ["queue", "todo", "in_progress", "in_review", "resolved"];
|
|
3536
|
-
var STAGE_LABELS = {
|
|
3537
|
-
queue: "Queue",
|
|
3538
|
-
todo: "To Do",
|
|
3539
|
-
in_progress: "In Progress",
|
|
3540
|
-
in_review: "In Review",
|
|
3541
|
-
resolved: "Resolved"
|
|
3542
|
-
};
|
|
3543
|
-
var LEGACY_STATUS = {
|
|
3544
|
-
open: "queue",
|
|
3545
|
-
in_progress: "in_progress",
|
|
3546
|
-
done: "resolved"
|
|
3547
|
-
};
|
|
3548
|
-
function normalizeStatus(value) {
|
|
3549
|
-
if (typeof value === "string") {
|
|
3550
|
-
if (COMMENT_STAGES.includes(value))
|
|
3551
|
-
return value;
|
|
3552
|
-
if (value in LEGACY_STATUS)
|
|
3553
|
-
return LEGACY_STATUS[value];
|
|
3686
|
+
async remove(id) {
|
|
3687
|
+
const res = await fetch(`${this.base}/v1/comments/${encodeURIComponent(id)}`, this.opts({
|
|
3688
|
+
method: "DELETE",
|
|
3689
|
+
headers: this.headers()
|
|
3690
|
+
}));
|
|
3691
|
+
if (!res.ok && res.status !== 404) throw new Error(`remove failed: ${res.status}`);
|
|
3554
3692
|
}
|
|
3555
|
-
return "queue";
|
|
3556
|
-
}
|
|
3557
|
-
var COMMENT_PRIORITIES = ["critical", "high", "medium", "low"];
|
|
3558
|
-
var PRIORITY_LABELS = {
|
|
3559
|
-
critical: "Critical",
|
|
3560
|
-
high: "High",
|
|
3561
|
-
medium: "Medium",
|
|
3562
|
-
low: "Low"
|
|
3563
|
-
};
|
|
3564
|
-
var CHANGE_TYPES = ["frontend", "backend", "api", "other"];
|
|
3565
|
-
var CHANGE_TYPE_LABELS = {
|
|
3566
|
-
frontend: "Frontend",
|
|
3567
|
-
backend: "Backend",
|
|
3568
|
-
api: "API",
|
|
3569
|
-
other: "Other"
|
|
3570
3693
|
};
|
|
3571
|
-
var DEFAULT_PRIORITY = "medium";
|
|
3572
|
-
var DEFAULT_CHANGE_TYPE = "other";
|
|
3573
|
-
function normalizePriority(value) {
|
|
3574
|
-
if (typeof value === "string" && COMMENT_PRIORITIES.includes(value)) {
|
|
3575
|
-
return value;
|
|
3576
|
-
}
|
|
3577
|
-
return DEFAULT_PRIORITY;
|
|
3578
|
-
}
|
|
3579
3694
|
|
|
3580
3695
|
// src/app.ts
|
|
3581
|
-
var SDK_VERSION = true ? "0.10.
|
|
3696
|
+
var SDK_VERSION = true ? "0.10.24" : "dev";
|
|
3582
3697
|
var ACCENTS = [
|
|
3583
3698
|
{ id: "indigo", dark: "#6b73e6", light: "#4a55d6", soft: "rgba(107,115,230,0.12)" },
|
|
3584
3699
|
{ id: "violet", dark: "#a06be6", light: "#7c3fd4", soft: "rgba(160,107,230,0.14)" },
|
|
@@ -3996,6 +4111,10 @@ var LoupeApp = class {
|
|
|
3996
4111
|
/** Optimistic replies the store rejected — offered for retry rather than lost. */
|
|
3997
4112
|
this.msgFailed = /* @__PURE__ */ new Set();
|
|
3998
4113
|
this.msgErr = /* @__PURE__ */ new Map();
|
|
4114
|
+
/** Other people on this page. Empty unless a bridge is configured. */
|
|
4115
|
+
this.peers = [];
|
|
4116
|
+
/** Reactions per thread, keyed `${threadId}:${messageId}`, so a re-render is free. */
|
|
4117
|
+
this.reactions = /* @__PURE__ */ new Map();
|
|
3999
4118
|
/** Everyone who has taken part, for mention autocomplete. Fetched once. */
|
|
4000
4119
|
this.people = /* @__PURE__ */ new Map();
|
|
4001
4120
|
this.cfg = cfg;
|
|
@@ -4015,6 +4134,7 @@ var LoupeApp = class {
|
|
|
4015
4134
|
this.renderPins();
|
|
4016
4135
|
this.renderList();
|
|
4017
4136
|
this.renderHome();
|
|
4137
|
+
this.startPresence();
|
|
4018
4138
|
void this.loadNotifications();
|
|
4019
4139
|
if (this.scope === "all") void this.loadAllComments();
|
|
4020
4140
|
this.observe();
|
|
@@ -4164,7 +4284,9 @@ var LoupeApp = class {
|
|
|
4164
4284
|
closeBtn.innerHTML = I_CLOSE;
|
|
4165
4285
|
closeBtn.onclick = () => this.closeDock();
|
|
4166
4286
|
ctl.append(posWrap, this.themeBtn, setWrap, minBtn, closeBtn);
|
|
4167
|
-
|
|
4287
|
+
this.peerEl = el("div", "peers");
|
|
4288
|
+
if (!this.cfg.bridge) this.peerEl.style.display = "none";
|
|
4289
|
+
head.append(brand, this.peerEl, ctl);
|
|
4168
4290
|
this.minBar = el("div", "minbar");
|
|
4169
4291
|
this.minBar.onclick = () => this.setMinimized(false);
|
|
4170
4292
|
this.tabList = [...BUILTIN_TABS, ...(this.cfg.tabs ?? []).map((t) => ({ id: t.id, label: t.label }))];
|
|
@@ -6324,6 +6446,37 @@ ${c.body}` : c.body)}</div>` + (c.context?.html ? `<pre class="or-code">${escape
|
|
|
6324
6446
|
else body.appendChild(document.createTextNode(seg.text));
|
|
6325
6447
|
}
|
|
6326
6448
|
row.append(head, body);
|
|
6449
|
+
const pills = el("div", "rxns");
|
|
6450
|
+
for (const r of summarizeReactions(this.reactionsOf(c.id, m.id), this.cfg.user.id)) {
|
|
6451
|
+
const pill = el("button", "rxn" + (r.mine ? " mine" : ""));
|
|
6452
|
+
pill.append(document.createTextNode(r.emoji), el("span", "rxn-n", String(r.count)));
|
|
6453
|
+
pill.title = r.users.join(", ");
|
|
6454
|
+
pill.onclick = (e) => {
|
|
6455
|
+
e.stopPropagation();
|
|
6456
|
+
void this.react(c, m.id, r.emoji);
|
|
6457
|
+
};
|
|
6458
|
+
pills.appendChild(pill);
|
|
6459
|
+
}
|
|
6460
|
+
const add = el("button", "rxn-add", "\uFF0B");
|
|
6461
|
+
add.title = "Add a reaction";
|
|
6462
|
+
add.onclick = (e) => {
|
|
6463
|
+
e.stopPropagation();
|
|
6464
|
+
const open = pills.querySelector(".rxn-pick");
|
|
6465
|
+
pills.querySelectorAll(".rxn-pick").forEach((n) => n.remove());
|
|
6466
|
+
if (open) return;
|
|
6467
|
+
const pick = el("div", "rxn-pick");
|
|
6468
|
+
for (const emoji of REACTION_CHOICES) {
|
|
6469
|
+
const b = el("button", "rxn-opt", emoji);
|
|
6470
|
+
b.onclick = (ev) => {
|
|
6471
|
+
ev.stopPropagation();
|
|
6472
|
+
void this.react(c, m.id, emoji);
|
|
6473
|
+
};
|
|
6474
|
+
pick.appendChild(b);
|
|
6475
|
+
}
|
|
6476
|
+
pills.appendChild(pick);
|
|
6477
|
+
};
|
|
6478
|
+
pills.appendChild(add);
|
|
6479
|
+
row.appendChild(pills);
|
|
6327
6480
|
if (this.msgPending.has(m.id)) row.appendChild(el("div", "msg-state", "Sending\u2026"));
|
|
6328
6481
|
if (this.msgFailed.has(m.id)) {
|
|
6329
6482
|
const failed = el("div", "msg-state failed");
|
|
@@ -6435,6 +6588,108 @@ ${c.body}` : c.body)}</div>` + (c.context?.html ? `<pre class="or-code">${escape
|
|
|
6435
6588
|
wrap.appendChild(copyRow);
|
|
6436
6589
|
return wrap;
|
|
6437
6590
|
}
|
|
6591
|
+
/**
|
|
6592
|
+
* Join, then keep talking.
|
|
6593
|
+
*
|
|
6594
|
+
* Presence is a heartbeat, not a flag, because a browser that is closed or crashes
|
|
6595
|
+
* sends no goodbye. The bridge expires anything silent past the TTL, and the panel
|
|
6596
|
+
* re-joins on a 404 rather than assuming it is still known.
|
|
6597
|
+
*/
|
|
6598
|
+
startPresence() {
|
|
6599
|
+
const bridge = this.cfg.bridge;
|
|
6600
|
+
if (!bridge) return;
|
|
6601
|
+
const base = bridge.replace(/\/$/, "");
|
|
6602
|
+
const pageUrl = location.pathname + location.search;
|
|
6603
|
+
const tick = async () => {
|
|
6604
|
+
try {
|
|
6605
|
+
if (!this.peerId) {
|
|
6606
|
+
const res = await fetch(`${base}/presence`, {
|
|
6607
|
+
method: "POST",
|
|
6608
|
+
headers: { "content-type": "application/json" },
|
|
6609
|
+
body: JSON.stringify({ url: pageUrl, userId: this.cfg.user.id, name: this.cfg.user.name })
|
|
6610
|
+
});
|
|
6611
|
+
if (!res.ok) return;
|
|
6612
|
+
const data = await res.json();
|
|
6613
|
+
this.peerId = data.peer?.id;
|
|
6614
|
+
} else {
|
|
6615
|
+
const res = await fetch(`${base}/presence/${encodeURIComponent(this.peerId)}/heartbeat`, { method: "POST" });
|
|
6616
|
+
if (res.status === 404) this.peerId = void 0;
|
|
6617
|
+
}
|
|
6618
|
+
const listed = await fetch(
|
|
6619
|
+
`${base}/presence?url=${encodeURIComponent(pageUrl)}&viewer=${encodeURIComponent(this.peerId ?? "")}`
|
|
6620
|
+
);
|
|
6621
|
+
if (listed.ok) {
|
|
6622
|
+
const data = await listed.json();
|
|
6623
|
+
this.peers = Array.isArray(data.peers) ? data.peers : [];
|
|
6624
|
+
this.renderPeers();
|
|
6625
|
+
}
|
|
6626
|
+
} catch {
|
|
6627
|
+
this.peers = [];
|
|
6628
|
+
this.renderPeers();
|
|
6629
|
+
}
|
|
6630
|
+
};
|
|
6631
|
+
void tick();
|
|
6632
|
+
this.peerTimer = setInterval(() => void tick(), PEER_HEARTBEAT_MS);
|
|
6633
|
+
}
|
|
6634
|
+
renderPeers() {
|
|
6635
|
+
if (!this.peerEl) return;
|
|
6636
|
+
this.peerEl.textContent = "";
|
|
6637
|
+
if (!this.peers.length) {
|
|
6638
|
+
this.peerEl.style.display = "none";
|
|
6639
|
+
return;
|
|
6640
|
+
}
|
|
6641
|
+
this.peerEl.style.display = "";
|
|
6642
|
+
for (const p of this.peers.slice(0, 4)) {
|
|
6643
|
+
const av = el("span", "peer-av", initialsOf(p.name));
|
|
6644
|
+
av.title = `${p.name} is on this page`;
|
|
6645
|
+
this.peerEl.appendChild(av);
|
|
6646
|
+
}
|
|
6647
|
+
if (this.peers.length > 4) this.peerEl.appendChild(el("span", "peer-more", `+${this.peers.length - 4}`));
|
|
6648
|
+
}
|
|
6649
|
+
stopPresence() {
|
|
6650
|
+
if (this.peerTimer) clearInterval(this.peerTimer);
|
|
6651
|
+
this.peerTimer = void 0;
|
|
6652
|
+
if (this.cfg.bridge && this.peerId) {
|
|
6653
|
+
const base = this.cfg.bridge.replace(/\/$/, "");
|
|
6654
|
+
void fetch(`${base}/presence/${encodeURIComponent(this.peerId)}`, { method: "DELETE", keepalive: true }).catch(() => {
|
|
6655
|
+
});
|
|
6656
|
+
}
|
|
6657
|
+
this.peerId = void 0;
|
|
6658
|
+
this.peers = [];
|
|
6659
|
+
}
|
|
6660
|
+
reactionsOf(threadId, messageId) {
|
|
6661
|
+
return this.reactions.get(`${threadId}:${messageId}`) ?? [];
|
|
6662
|
+
}
|
|
6663
|
+
/**
|
|
6664
|
+
* Toggle a reaction.
|
|
6665
|
+
*
|
|
6666
|
+
* Optimistic, then replaced by the server's own set: the server returns the whole
|
|
6667
|
+
* list precisely so the count can never drift from what was stored.
|
|
6668
|
+
*/
|
|
6669
|
+
async react(c, messageId, emoji) {
|
|
6670
|
+
const key = `${c.id}:${messageId}`;
|
|
6671
|
+
const before = this.reactionsOf(c.id, messageId);
|
|
6672
|
+
this.reactions.set(key, toggleReaction(before, {
|
|
6673
|
+
messageId,
|
|
6674
|
+
emoji,
|
|
6675
|
+
userId: this.cfg.user.id,
|
|
6676
|
+
userName: this.cfg.user.name
|
|
6677
|
+
}));
|
|
6678
|
+
this.renderList();
|
|
6679
|
+
try {
|
|
6680
|
+
const next = await this.store.toggleReaction({
|
|
6681
|
+
threadId: c.id,
|
|
6682
|
+
messageId,
|
|
6683
|
+
emoji,
|
|
6684
|
+
userId: this.cfg.user.id,
|
|
6685
|
+
userName: this.cfg.user.name
|
|
6686
|
+
});
|
|
6687
|
+
this.reactions.set(key, Array.isArray(next) ? next : this.reactionsOf(c.id, messageId));
|
|
6688
|
+
} catch {
|
|
6689
|
+
this.reactions.set(key, before);
|
|
6690
|
+
}
|
|
6691
|
+
this.renderList();
|
|
6692
|
+
}
|
|
6438
6693
|
async loadPeople() {
|
|
6439
6694
|
if (this.people.has(this.cfg.projectKey)) return;
|
|
6440
6695
|
try {
|
|
@@ -6447,6 +6702,17 @@ ${c.body}` : c.body)}</div>` + (c.context?.html ? `<pre class="or-code">${escape
|
|
|
6447
6702
|
async loadMessages(c) {
|
|
6448
6703
|
if (this.messages.has(c.id)) return;
|
|
6449
6704
|
void this.loadPeople();
|
|
6705
|
+
void this.store.listReactions(c.id).then((all) => {
|
|
6706
|
+
if (!Array.isArray(all)) return;
|
|
6707
|
+
for (const r of all) {
|
|
6708
|
+
const key = `${c.id}:${r.messageId}`;
|
|
6709
|
+
const list = this.reactions.get(key) ?? [];
|
|
6710
|
+
if (!list.some((x) => x.userId === r.userId && x.emoji === r.emoji)) list.push(r);
|
|
6711
|
+
this.reactions.set(key, list);
|
|
6712
|
+
}
|
|
6713
|
+
this.renderList();
|
|
6714
|
+
}).catch(() => {
|
|
6715
|
+
});
|
|
6450
6716
|
try {
|
|
6451
6717
|
this.messages.set(c.id, await this.store.listMessages(c.id));
|
|
6452
6718
|
} catch {
|
|
@@ -6593,6 +6859,7 @@ ${c.body}` : c.body)}</div>` + (c.context?.html ? `<pre class="or-code">${escape
|
|
|
6593
6859
|
if (elx) elx.scrollIntoView({ behavior: "smooth", block: "center" });
|
|
6594
6860
|
}
|
|
6595
6861
|
destroy() {
|
|
6862
|
+
this.stopPresence();
|
|
6596
6863
|
this.stopRecording?.();
|
|
6597
6864
|
this.setMode("off");
|
|
6598
6865
|
this.mo?.disconnect();
|