cobro-mcp 0.9.3 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/overlay.js CHANGED
@@ -25,7 +25,95 @@
25
25
  // src/overlay/refs.ts
26
26
  var isChildRef = (ref, g) => !!ref && new RegExp("^" + g + "[a-l]$").test(ref);
27
27
 
28
+ // src/core/page.ts
29
+ function samePage(a, b) {
30
+ try {
31
+ const ua = new URL(a);
32
+ const ub = new URL(b);
33
+ return ua.origin + ua.pathname + ua.search === ub.origin + ub.pathname + ub.search;
34
+ } catch {
35
+ return a === b;
36
+ }
37
+ }
38
+
39
+ // src/overlay/cart.ts
40
+ function currentDraft(drafts, href) {
41
+ return drafts.find((b) => !b.page || samePage(b.page.url, href)) ?? null;
42
+ }
43
+ function roundBatches(batches) {
44
+ const active = batches.filter((b) => b.status === "sent" || b.status === "working");
45
+ if (active.length === 0) return [];
46
+ const parseSentAt = (b) => {
47
+ const t = Date.parse(b.sentAt ?? "");
48
+ return Number.isNaN(t) ? Infinity : t;
49
+ };
50
+ const minSentAt = Math.min(...active.map(parseSentAt));
51
+ const done = batches.filter((b) => b.status === "done" && b.doneAt && Date.parse(b.doneAt) >= minSentAt);
52
+ return [...active, ...done];
53
+ }
54
+ function roundOf(batches) {
55
+ const round = roundBatches(batches);
56
+ const done = round.filter((b) => b.status === "done").length;
57
+ return { queue: round.length - done, done, total: round.length };
58
+ }
59
+ function pageLoc(url, href) {
60
+ const u = new URL(url);
61
+ const cur = new URL(href);
62
+ return u.origin === cur.origin ? u.pathname + u.search : u.host + u.pathname + u.search;
63
+ }
64
+ function pendingElsewhere(session, href) {
65
+ const item = (session?.pendingDone ?? []).find((p) => !samePage(p.url, href));
66
+ if (!item) return null;
67
+ try {
68
+ return { url: item.url, path: pageLoc(item.url, href) };
69
+ } catch {
70
+ return null;
71
+ }
72
+ }
73
+ function pathnameOf(url) {
74
+ try {
75
+ return new URL(url).pathname;
76
+ } catch {
77
+ return url;
78
+ }
79
+ }
80
+ function queueCards(drafts, batches, href, busy, labels) {
81
+ const noteMeta = (note) => {
82
+ const line = note.split("\n")[0] ?? "";
83
+ return line ? truncate40(line) : labels.noNote;
84
+ };
85
+ const draftCards = drafts.filter((b) => b.elements.length || b.regions?.length || b.note.trim()).map((b) => {
86
+ const url = b.page?.url ?? href;
87
+ const count = b.elements.length + (b.regions?.length ?? 0);
88
+ return {
89
+ kind: "draft",
90
+ url,
91
+ title: b.page?.title || pathnameOf(url),
92
+ path: pageLoc(url, href),
93
+ meta: `${labels.count(count)} \xB7 ${noteMeta(b.note)}`,
94
+ isCurrent: samePage(url, href),
95
+ count
96
+ };
97
+ });
98
+ const round = roundBatches(batches);
99
+ const progressSrc = busy ? round : round.filter((b) => b.status !== "done");
100
+ const progressCards = progressSrc.slice().sort((a, b) => (a.sentAt ?? "").localeCompare(b.sentAt ?? "")).map((b) => {
101
+ const url = b.page?.url ?? href;
102
+ const count = b.elements.length + (b.regions?.length ?? 0);
103
+ const meta = b.status === "done" ? "\u2713 " + truncate40(b.summary ?? "") : `${labels.count(count)} \xB7 ${noteMeta(b.note)}`;
104
+ return { kind: b.status, url, title: b.page?.title || pathnameOf(url), path: pageLoc(url, href), meta, isCurrent: samePage(url, href), count };
105
+ });
106
+ return [...draftCards, ...progressCards];
107
+ }
108
+ function lastRoundDone(batches) {
109
+ return batches.filter((b) => b.status === "done");
110
+ }
111
+ function truncate40(s) {
112
+ return s.length > 40 ? s.slice(0, 40) + "\u2026" : s;
113
+ }
114
+
28
115
  // src/overlay/ui.ts
116
+ var QUEUE_ICON = '<svg viewBox="0 0 16 16" width="14" height="14" aria-hidden="true"><rect x="2" y="5" width="12" height="2" rx="1" fill="currentColor"/><rect x="2" y="9" width="12" height="2" rx="1" fill="currentColor"/></svg>';
29
117
  var CSS2 = `
30
118
  :host{
31
119
  --bg:#0e111a;--bg-2:#171b28;--bg-3:#232c42;--chip:#161b2a;--border:#2a3350;--border-2:#3a4a72;
@@ -62,12 +150,14 @@
62
150
  .toolbar button:hover,.panel button:hover{background:var(--hover);border-color:var(--hover-border);color:var(--fg-hover)}
63
151
  .toolbar button:focus-visible,.panel button:focus-visible{outline:2px solid var(--info);outline-offset:1px}
64
152
  .toolbar button.on{color:var(--fg-on-accent);background:var(--accent);border-color:var(--accent)}
153
+ .toolbar button:disabled,.panel button:disabled{opacity:.4;cursor:not-allowed}
154
+ .toolbar button.locked{opacity:.6}
155
+ textarea:disabled{opacity:.6;cursor:not-allowed}
65
156
  .els button{background:transparent;border-color:transparent;color:var(--fg-5);padding:0 6px}
66
157
  .els button:hover{background:var(--hover);color:var(--fg-hover)}
67
158
  .chip{display:inline-flex;align-items:center;gap:5px;padding:2px 8px;border-radius:999px;background:var(--chip);border:1px solid transparent;color:var(--fg-3);white-space:nowrap;cursor:default;user-select:none}
68
159
  .chip[hidden]{display:none}
69
160
  .chip .ico{display:inline-flex;line-height:0}
70
- .chip.strategy .ico svg{width:12px;height:12px}
71
161
  .chip.off{color:var(--warn);border-color:var(--chip-off-border)}
72
162
  .dot{font-size:9px;line-height:1;color:var(--fg-5)}
73
163
  .dot.waiting{color:var(--ok)}
@@ -79,6 +169,7 @@
79
169
  .status-in{display:inline-block;max-width:100%;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;vertical-align:bottom;transition:transform .2s ease-out}
80
170
  .status-in.scroll{max-width:none;overflow:visible;text-overflow:clip}
81
171
  .status-in.enter{animation:cobroin .15s ease-out}
172
+ .status-in a.goto{color:inherit;text-decoration:underline;cursor:pointer}
82
173
  @keyframes cobroin{from{opacity:.4;transform:translateX(-6px)}to{opacity:1;transform:translateX(0)}}
83
174
  @media (prefers-reduced-motion: reduce){.status-in{transition:none}.status-in.enter{animation:none}}
84
175
  .wbox{position:fixed;border:1.5px solid var(--info);border-radius:2px;pointer-events:none;box-sizing:border-box;animation:cobrobreathe 2.4s ease-in-out infinite}
@@ -101,6 +192,12 @@
101
192
  .panel h4 .grip svg{width:14px;height:16px;fill:currentColor;display:block}
102
193
  .panel h4 .close{margin-left:auto;background:transparent;border-color:transparent;color:var(--fg-5);padding:0 6px}
103
194
  .panel h4 .close:hover{background:var(--hover);color:var(--fg-hover)}
195
+ .panel .tabs{display:flex;flex:1;border-bottom:1px solid var(--border)}
196
+ .panel .tabs button.tab{flex:1;min-height:40px;background:none;border:0;border-bottom:2px solid transparent;color:var(--fg-4);font-weight:600;font-family:inherit;letter-spacing:0;display:inline-flex;align-items:center;justify-content:center;gap:5px;border-radius:0;padding:4px 6px}
197
+ .panel .tabs button.tab.on{color:var(--fg);border-bottom-color:var(--accent)}
198
+ .panel .tabs .badge{display:inline-flex;min-width:16px;height:16px;padding:0 4px;border-radius:999px;background:var(--chip);font-size:10px;line-height:16px;text-align:center}
199
+ .sub{display:flex;align-items:center;gap:6px;margin:12px 0;color:var(--fg);font-size:12px;font-weight:700}
200
+ .sub .mark{color:var(--accent)}
104
201
  .els{max-height:min(45vh,260px);overflow:auto;margin-bottom:8px;color:var(--fg-3);font-size:11px}
105
202
  .els div{display:flex;align-items:center;gap:8px;padding:8px 10px;margin-bottom:8px;background:var(--bg-3);border:1px solid var(--border);border-radius:8px;word-break:break-all}
106
203
  .els div:last-child{margin-bottom:0}
@@ -117,10 +214,34 @@
117
214
  .marker.child{border-style:dotted}
118
215
  .marker.child .n{background:#fff;color:var(--accent);border:1.5px solid var(--accent)}
119
216
  textarea{width:100%;min-height:80px;resize:none;font:inherit;color:var(--fg);background:var(--bg);border:1px solid var(--border-2);border-radius:8px;padding:10px;margin-bottom:0;display:block}
217
+ div.queue{display:flex;flex-direction:column;gap:8px;max-height:min(45vh,320px);overflow:auto;margin:12px 0}
218
+ .queue .card{display:flex;gap:8px;padding:8px;border:1px solid var(--border);border-radius:10px;text-decoration:none;color:inherit;align-items:center}
219
+ .card.working{border-color:var(--accent)}
220
+ .card.here{background:var(--chip)}
221
+ .queue .card .body{flex:1;min-width:0;display:flex;flex-direction:column;gap:2px}
222
+ .queue .card .title{font-weight:600;color:var(--fg-2);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
223
+ .queue .card .path{font-size:10px;color:var(--fg-5);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
224
+ .queue .card .meta{font-size:11px;color:var(--fg-4);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
225
+ .queue .card .chip{flex:none;font-size:10px;line-height:16px;padding:0 6px;border-radius:999px;background:var(--chip);border:1px solid var(--border);color:var(--fg-4);align-self:flex-start}
226
+ .queue .card.working .chip{color:var(--accent)}
227
+ .queue .card.done .chip{color:var(--ok)}
228
+ .queue .card .here{flex:none;font-size:10px;color:var(--fg-5)}
229
+ .thumb{flex:none;width:44px;height:32px;background:#fff;border-radius:6px;display:flex;align-items:center;justify-content:center;gap:3px;padding:4px}
230
+ .thumb .rect{display:block;width:10px;height:14px;border:1.5px solid var(--border-2);border-radius:2px}
231
+ .thumb .rect.sent{border-color:var(--warn)}
232
+ .thumb .rect.working{border-color:var(--accent)}
233
+ .thumb .rect.done{border-color:var(--ok)}
234
+ .done-row{display:block;width:100%;text-align:left;background:transparent;border:1px dashed var(--border);color:var(--fg-4);padding:8px 10px;border-radius:8px}
235
+ .done-item{display:flex;gap:8px;padding:6px 10px;font-size:11px;color:var(--fg-4)}
236
+ .done-item .title{flex:none;color:var(--fg-3)}
237
+ .done-item .path{flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
238
+ .done-item .summary{color:var(--ok)}
120
239
  .row{display:flex;justify-content:space-between;margin-top:8px;gap:6px;align-items:center}
121
240
  .row .sends{color:var(--fg-5);font-size:11px;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
122
241
  .row .send{color:var(--fg-on-accent);background:var(--accent);border-color:var(--accent);font-weight:700;flex:none;white-space:nowrap}
123
242
  .row .send:disabled{opacity:.4;cursor:not-allowed}
243
+ .row .foot{font-size:11px;color:var(--fg-4)}
244
+ .row .foot.ok{color:var(--ok)}
124
245
  .marker{position:fixed;border:2px solid var(--accent);border-radius:2px;pointer-events:none;box-sizing:border-box}
125
246
  .marker.region{border-style:dashed;background:color-mix(in srgb, var(--accent) 6%, transparent)}
126
247
  .marker .n{position:absolute;left:-2px;top:-2px;transform:translate(-50%,-50%);min-width:18px;height:18px;padding:0 5px;border-radius:9px;background:var(--accent);color:#fff;font:700 11px/18px ui-monospace,Menlo,Consolas,monospace;text-align:center;box-shadow:0 1px 3px rgba(0,0,0,.4)}
@@ -144,13 +265,17 @@ textarea{width:100%;min-height:80px;resize:none;font:inherit;color:var(--fg);bac
144
265
  .seg button.on{background:var(--bg-3);color:var(--fg)}
145
266
  .seg button:disabled{opacity:.5;cursor:not-allowed}
146
267
  .pop-note{margin-top:6px;color:var(--warn);font-size:11px}
268
+ .pop-row.strategy{align-items:center;flex-wrap:wrap}
269
+ .pop-row.strategy[hidden]{display:none}
270
+ .strategy-val{display:inline-flex;align-items:center;gap:4px;font-family:ui-monospace,Menlo,Consolas,monospace;padding:2px 8px;border-radius:999px;background:var(--chip);color:var(--fg-3)}
271
+ .strategy-val .ico svg{width:12px;height:12px}
272
+ .pop-sub{font-size:11px;color:var(--fg-4);margin-left:8px}
147
273
  .ib{display:inline-flex;align-items:center;padding:4px 7px}
148
274
  .ib .ico{display:inline-flex;line-height:0}
149
275
  .ib .lbl{max-width:0;opacity:0;overflow:hidden;white-space:nowrap;margin-left:0;transition:max-width .1s ease-in,opacity .1s ease-in,margin-left .1s ease-in}
150
276
  .ib:hover .lbl,.ib:focus-visible .lbl{max-width:9ch;opacity:1;margin-left:5px;transition-duration:.15s;transition-timing-function:ease-out}
151
277
  @media (prefers-reduced-motion: reduce){.ib .lbl{transition:none}}
152
278
  @media (max-width:760px){
153
- .chip.strategy{display:none}
154
279
  .status{width:180px}
155
280
  .ib .lbl,.ib:hover .lbl,.ib:focus-visible .lbl{max-width:0;opacity:0;margin-left:0}
156
281
  }
@@ -184,7 +309,7 @@ textarea{width:100%;min-height:80px;resize:none;font:inherit;color:var(--fg);bac
184
309
  hintMore: (n) => `${n}\uAC1C \uC120\uD0DD \xB7 \uB354 \uACE0\uB974\uAC70\uB098 \uBA54\uBAA8\uB97C \uC801\uC73C\uC138\uC694`,
185
310
  hintNote: "\uBA54\uBAA8\uB97C \uC801\uACE0 Send\uB97C \uB204\uB974\uC138\uC694",
186
311
  hintPick: "Ctrl+Shift+F \uB610\uB294 Select\uB85C \uC694\uC18C\uB97C \uACE0\uB974\uC138\uC694",
187
- tipStrategy: (s) => `\uAC31\uC2E0 \uC804\uB7B5: ${s} \u2014 ${{ none: "HMR\uC774 \uC788\uC5B4 done \uB4A4 \uC0C8\uB85C\uACE0\uCE68 \uC5C6\uC74C", reload: "done \uB4A4 \uD398\uC774\uC9C0 \uC0C8\uB85C\uACE0\uCE68", event: "\uC571\uC774 cobro:done \uC774\uBCA4\uD2B8\uB85C \uC9C1\uC811 \uAC31\uC2E0" }[s]}`,
312
+ strategyDesc: (s) => ({ none: "HMR\uC774 \uC788\uC5B4 done \uB4A4 \uC0C8\uB85C\uACE0\uCE68 \uC5C6\uC74C", reload: "done \uB4A4 \uD398\uC774\uC9C0 \uC0C8\uB85C\uACE0\uCE68", event: "\uC571\uC774 cobro:done \uC774\uBCA4\uD2B8\uB85C \uC9C1\uC811 \uAC31\uC2E0" })[s],
188
313
  selCount: (n) => `\uC694\uC18C ${n}\uAC1C \uC120\uD0DD\uB428`,
189
314
  selCountMixed: (n, m) => `\uC694\uC18C ${n}\uAC1C + \uC601\uC5ED ${m}\uAC1C \uC120\uD0DD\uB428`,
190
315
  regionRow: (w, h) => `\u25AD ${w}\xD7${h}`,
@@ -200,15 +325,31 @@ textarea{width:100%;min-height:80px;resize:none;font:inherit;color:var(--fg);bac
200
325
  tipDrag: "\uD234\uBC14 \uC774\uB3D9",
201
326
  sends: "\uC120\uD0DD\uC790 \xB7 \uC2A4\uD0C0\uC77C \xB7 \uC2A4\uD06C\uB9B0\uC0F7 \xB7 \uCF58\uC194",
202
327
  tipSend: "\uC120\uD0DD\uD55C \uC694\uC18C\uC640 \uBA54\uBAA8\uB97C \uC5D0\uC774\uC804\uD2B8\uC5D0 \uC804\uC1A1",
203
- tipSendLocked: "\uC5D0\uC774\uC804\uD2B8\uAC00 \uC791\uC5C5 \uC911 \u2014 done \uB4A4\uC5D0 \uBCF4\uB0BC \uC218 \uC788\uC2B5\uB2C8\uB2E4",
204
328
  tipRemove: "\uC774 \uC694\uC18C \uBE7C\uAE30",
329
+ cartNoNote: "\uBA54\uBAA8 \uC5C6\uC74C",
330
+ doneElsewhere: (path) => `\u2713 \uC644\uB8CC \xB7 ${path} \uBCF4\uAE30`,
331
+ tabHere: "\uC774 \uD398\uC774\uC9C0",
332
+ tabQueue: "\uD050",
333
+ herePage: "\uC9C0\uAE08 \uBCF4\uACE0 \uC788\uB294 \uD398\uC774\uC9C0",
334
+ queueCount: (n) => `${n}\uAC1C`,
335
+ footBusy: (d, n) => `\uCC98\uB9AC \uC911 ${d}/${n} \u2014 \uB05D\uB098\uBA74 \uB2F4\uC744 \uC218 \uC788\uC5B4\uC694`,
336
+ footPaused: (d, n) => `\uCC98\uB9AC \uC911 ${d}/${n} \u2014 \uC774 \uD398\uC774\uC9C0\uB97C \uB5A0\uB098 \uB530\uB77C\uAC00\uAE30\uB97C \uBA48\uCDC4\uC5B4\uC694`,
337
+ footAllDone: (n) => `\u2713 ${n}\uD398\uC774\uC9C0 \uC644\uB8CC \u2014 \uC694\uC18C\uB97C \uACE0\uB974\uBA74 \uC0C8 \uC694\uCCAD\uC774 \uC2DC\uC791\uB429\uB2C8\uB2E4`,
338
+ footHint: "\uBA54\uBAA8\uB97C \uC801\uC73C\uBA74 \uBCF4\uB0BC \uC218 \uC788\uC5B4\uC694",
339
+ tipSelectLocked: "\uC5D0\uC774\uC804\uD2B8\uAC00 \uC218\uC815 \uC911 \u2014 \uD050\uB97C \uBCFC \uC218 \uC788\uC5B4\uC694, \uB2F4\uAE30\uB294 \uC644\uB8CC \uD6C4",
340
+ followPaused: "\uB530\uB77C\uAC00\uAE30 \uC911\uC9C0 \u2014 \uC644\uB8CC\uB418\uBA74 \u300C\uBCF4\uAE30\u300D \uB9C1\uD06C\uB85C \uD655\uC778",
341
+ doneRow: (n) => `\u2713 \uC644\uB8CC ${n}\uD398\uC774\uC9C0 \xB7 \uC0C8\uB85C \uB2F4\uC73C\uBA74 \uC815\uB9AC\uB429\uB2C8\uB2E4`,
342
+ sendPages: (n) => `Send \xB7 ${n}\uD398\uC774\uC9C0 \u2192`,
343
+ chipDraft: "\uCD08\uC548",
344
+ progress: (d, n) => `${d}/${n}`,
205
345
  tipSettings: "\uC124\uC815",
206
346
  theme: "\uD14C\uB9C8",
207
347
  themeAuto: "\uC790\uB3D9",
208
348
  themeDark: "\uC5B4\uB461\uAC8C",
209
349
  themeLight: "\uBC1D\uAC8C",
210
350
  themeFrost: "\uC720\uB9AC",
211
- themeLocked: "COBRO_THEME \uD658\uACBD \uBCC0\uC218\uB85C \uACE0\uC815\uB428"
351
+ themeLocked: "COBRO_THEME \uD658\uACBD \uBCC0\uC218\uB85C \uACE0\uC815\uB428",
352
+ strategyLabel: "\uAC31\uC2E0 \uC804\uB7B5"
212
353
  },
213
354
  en: {
214
355
  agentIdle: "Agent not connected",
@@ -230,7 +371,7 @@ textarea{width:100%;min-height:80px;resize:none;font:inherit;color:var(--fg);bac
230
371
  hintMore: (n) => `${n} selected \xB7 pick more or write a note`,
231
372
  hintNote: "Write a note, then press Send",
232
373
  hintPick: "Press Ctrl+Shift+F or Select to pick an element",
233
- tipStrategy: (s) => `Refresh strategy: ${s} \u2014 ${{ none: "HMR present \u2014 no reload after done", reload: "page reloads after done", event: "the app refreshes itself on cobro:done" }[s]}`,
374
+ strategyDesc: (s) => ({ none: "HMR present \u2014 no reload after done", reload: "page reloads after done", event: "the app refreshes itself on cobro:done" })[s],
234
375
  selCount: (n) => `${n} element(s) selected`,
235
376
  selCountMixed: (n, m) => `${n} element(s) + ${m} region(s) selected`,
236
377
  regionRow: (w, h) => `\u25AD ${w}\xD7${h}`,
@@ -246,15 +387,31 @@ textarea{width:100%;min-height:80px;resize:none;font:inherit;color:var(--fg);bac
246
387
  tipDrag: "Move toolbar",
247
388
  sends: "Sends selector \xB7 styles \xB7 shot \xB7 console",
248
389
  tipSend: "Send the selected elements and note to the agent",
249
- tipSendLocked: "Agent is working \u2014 you can send after done",
250
390
  tipRemove: "Remove this element",
391
+ cartNoNote: "No note",
392
+ doneElsewhere: (path) => `\u2713 Done \xB7 view ${path}`,
393
+ tabHere: "This page",
394
+ tabQueue: "Queue",
395
+ herePage: "The page you're on",
396
+ queueCount: (n) => `${n} element${n === 1 ? "" : "s"}`,
397
+ footBusy: (d, n) => `Working ${d}/${n} \u2014 you can add more once it's done`,
398
+ footPaused: (d, n) => `Working ${d}/${n} \u2014 you left this page, so following paused`,
399
+ footAllDone: (n) => `\u2713 ${n} page${n === 1 ? "" : "s"} done \u2014 pick an element to start a new request`,
400
+ footHint: "Write a note to send",
401
+ tipSelectLocked: "Agent is working \u2014 view the queue; picking resumes after done",
402
+ followPaused: `Following paused \u2014 check the "view" link when it's done`,
403
+ doneRow: (n) => `\u2713 ${n} done \xB7 cleared when you pick again`,
404
+ sendPages: (n) => `Send \xB7 ${n} pages \u2192`,
405
+ chipDraft: "Draft",
406
+ progress: (d, n) => `${d}/${n}`,
251
407
  tipSettings: "Settings",
252
408
  theme: "Theme",
253
409
  themeAuto: "Auto",
254
410
  themeDark: "Dark",
255
411
  themeLight: "Light",
256
412
  themeFrost: "Frost",
257
- themeLocked: "Pinned by COBRO_THEME"
413
+ themeLocked: "Pinned by COBRO_THEME",
414
+ strategyLabel: "Refresh"
258
415
  }
259
416
  }[LANG];
260
417
  var THEME_LABEL = { auto: T.themeAuto, dark: T.themeDark, light: T.themeLight, frost: T.themeFrost };
@@ -267,7 +424,7 @@ textarea{width:100%;min-height:80px;resize:none;font:inherit;color:var(--fg);bac
267
424
  done: (t) => stripStatusLabel(t, LABELS.done)
268
425
  };
269
426
  var DOT_TITLE = { idle: T.agentIdle, waiting: T.agentWaiting, sent: T.agentSent, working: T.agentWorking, done: T.agentDone };
270
- var CHIP_LABEL = { idle: T.chipIdle, waiting: T.chipWaiting, sent: T.chipSent, working: T.chipWorking, done: T.chipDone };
427
+ var CHIP_LABEL = { idle: T.chipIdle, waiting: T.chipWaiting, sent: T.chipSent, working: T.chipWorking, done: T.chipDone, draft: T.chipDraft };
271
428
  function makeDraggable(target, handle, opts) {
272
429
  const current = target.style.translate.split(" ").map((v) => parseFloat(v) || 0);
273
430
  let off = { x: current[0] ?? 0, y: current[1] ?? 0 };
@@ -344,14 +501,6 @@ textarea{width:100%;min-height:80px;resize:none;font:inherit;color:var(--fg);bac
344
501
  const chipLabel = document.createElement("span");
345
502
  chipLabel.className = "chip-label";
346
503
  chip.append(dot, chipLabel);
347
- const chip2 = document.createElement("span");
348
- chip2.className = "chip strategy";
349
- const chip2Ico = document.createElement("span");
350
- chip2Ico.className = "ico";
351
- chip2Ico.innerHTML = svg("sync");
352
- const chip2Label = document.createElement("span");
353
- chip2Label.className = "chip-label";
354
- chip2.append(chip2Ico, chip2Label);
355
504
  const status = document.createElement("span");
356
505
  status.className = "status";
357
506
  const statusIn = document.createElement("span");
@@ -362,7 +511,7 @@ textarea{width:100%;min-height:80px;resize:none;font:inherit;color:var(--fg);bac
362
511
  const gripParts = iconBtn("drag", "Move", T.tipDrag, "grip", false);
363
512
  const grip = gripParts.btn;
364
513
  grip.setAttribute("aria-label", "Move toolbar");
365
- toolbar.append(grip, selectBtn, chip, chip2, status, gearBtn);
514
+ toolbar.append(grip, selectBtn, chip, status, gearBtn);
366
515
  const line = document.createElement("span");
367
516
  line.className = "line";
368
517
  line.hidden = true;
@@ -386,10 +535,25 @@ textarea{width:100%;min-height:80px;resize:none;font:inherit;color:var(--fg);bac
386
535
  });
387
536
  seg2.append(...segButtons);
388
537
  popRow.append(popLabel, seg2);
538
+ const stratRow = document.createElement("div");
539
+ stratRow.className = "pop-row strategy";
540
+ const stratLabel = document.createElement("span");
541
+ stratLabel.className = "pop-label";
542
+ stratLabel.textContent = T.strategyLabel;
543
+ const stratVal = document.createElement("span");
544
+ stratVal.className = "strategy-val";
545
+ const stratIco = document.createElement("span");
546
+ stratIco.className = "ico";
547
+ stratIco.innerHTML = svg("sync");
548
+ const stratValText = document.createElement("span");
549
+ stratVal.append(stratIco, stratValText);
550
+ const stratSub = document.createElement("span");
551
+ stratSub.className = "pop-sub";
552
+ stratRow.append(stratLabel, stratVal, stratSub);
389
553
  const popNote = document.createElement("div");
390
554
  popNote.className = "pop-note";
391
555
  popNote.textContent = T.themeLocked;
392
- pop.append(popRow, popNote);
556
+ pop.append(popRow, stratRow, popNote);
393
557
  let popOpen = false;
394
558
  let popCloseListeners = null;
395
559
  const closePop = () => {
@@ -487,6 +651,7 @@ textarea{width:100%;min-height:80px;resize:none;font:inherit;color:var(--fg);bac
487
651
  if (text != null) d.textContent = text;
488
652
  return d;
489
653
  };
654
+ const truncate402 = (s) => s.length > 40 ? s.slice(0, 40) + "\u2026" : s;
490
655
  function render(vm) {
491
656
  renderMarkers(vm);
492
657
  updateTbH();
@@ -494,15 +659,22 @@ textarea{width:100%;min-height:80px;resize:none;font:inherit;color:var(--fg);bac
494
659
  const wasTa = active instanceof HTMLTextAreaElement ? active : null;
495
660
  const sel = wasTa ? [wasTa.selectionStart, wasTa.selectionEnd] : null;
496
661
  selectBtn.classList.toggle("on", vm.selecting);
662
+ selectBtn.classList.toggle("locked", vm.locked);
663
+ selectBtn.title = vm.locked ? T.tipSelectLocked : T.tipSelect;
497
664
  for (const btn of segButtons) {
498
665
  btn.classList.toggle("on", btn.dataset.theme === vm.prefs.theme);
499
666
  btn.disabled = vm.prefs.themeLocked;
500
667
  }
501
668
  popNote.hidden = !vm.prefs.themeLocked;
502
- const cur = vm.drafts[vm.drafts.length - 1];
669
+ const cur = vm.current;
503
670
  const hasElements = !!cur && cur.elements.length + (cur.regions?.length ?? 0) > 0;
504
671
  let hint;
672
+ let gotoUrl = null;
505
673
  if (!vm.connected) hint = T.disconnected;
674
+ else if (!vm.busy && vm.pendingElsewhere) {
675
+ hint = T.doneElsewhere(vm.pendingElsewhere.path);
676
+ gotoUrl = vm.pendingElsewhere.url;
677
+ } else if (vm.locked && vm.followPaused) hint = T.followPaused;
506
678
  else {
507
679
  let text;
508
680
  if (vm.agent.status === "sent" || vm.agent.status === "working" || vm.agent.status === "done") text = AGENT_TEXT[vm.agent.status](vm.agent.text);
@@ -523,7 +695,16 @@ textarea{width:100%;min-height:80px;resize:none;font:inherit;color:var(--fg);bac
523
695
  statusIn.classList.add("enter");
524
696
  }
525
697
  }
526
- statusIn.textContent = displayHint;
698
+ if (gotoUrl) {
699
+ statusIn.textContent = "";
700
+ const a = document.createElement("a");
701
+ a.className = "goto";
702
+ a.href = gotoUrl;
703
+ a.textContent = displayHint;
704
+ statusIn.append(a);
705
+ } else {
706
+ statusIn.textContent = displayHint;
707
+ }
527
708
  statusIn.title = hint;
528
709
  if (hovering) {
529
710
  applyHoverScroll();
@@ -532,117 +713,181 @@ textarea{width:100%;min-height:80px;resize:none;font:inherit;color:var(--fg);bac
532
713
  status.classList.toggle("off", !vm.connected);
533
714
  chip.classList.toggle("off", !vm.connected);
534
715
  dot.className = vm.connected ? "dot " + vm.agent.status : "dot";
535
- chipLabel.textContent = vm.connected ? CHIP_LABEL[vm.agent.status] : T.chipOff;
536
- chip.title = vm.connected ? DOT_TITLE[vm.agent.status] : T.disconnected;
537
- chip2.hidden = !vm.strategy;
716
+ const round = roundOf(vm.queue);
717
+ const roundSuffix = vm.busy ? " " + T.progress(round.done, round.total) : "";
718
+ chipLabel.textContent = vm.connected ? CHIP_LABEL[vm.agent.status] + roundSuffix : T.chipOff;
719
+ chip.title = vm.connected ? DOT_TITLE[vm.agent.status] + roundSuffix : T.disconnected;
720
+ stratRow.hidden = !vm.strategy;
538
721
  if (vm.strategy) {
539
- chip2Label.textContent = vm.strategy;
540
- chip2.title = T.tipStrategy(vm.strategy);
722
+ stratValText.textContent = vm.strategy;
723
+ stratSub.textContent = T.strategyDesc(vm.strategy);
724
+ stratRow.title = T.strategyDesc(vm.strategy);
541
725
  }
542
- const show = vm.selecting;
543
- panel.classList.toggle("show", show);
544
- if (vm.drafts.length === 0) {
726
+ panel.classList.toggle("show", vm.panelOpen);
727
+ if (!vm.panelOpen) {
545
728
  panel.textContent = "";
546
729
  textareas.clear();
547
730
  return;
548
731
  }
549
- if (!show) return;
550
732
  panel.textContent = "";
551
- if (cur) {
552
- const h4 = el("h4");
553
- const g = el("span", "grip");
554
- g.innerHTML = svg("drag");
555
- g.setAttribute("aria-hidden", "true");
556
- h4.append(g);
557
- h4.append(el("span", "mark", "\u25AE"), document.createTextNode(cur.regions?.length ? T.selCountMixed(cur.elements.length, cur.regions.length) : cur.elements.length > 0 ? T.selCount(cur.elements.length) : T.selNone));
558
- const close = el("button", "close", "\u2715");
559
- close.title = T.tipClose;
560
- close.setAttribute("aria-label", "Close");
561
- close.onclick = () => h.onToggleSelect();
562
- h4.append(close);
563
- panel.append(h4);
564
- const list = el("div", "els");
565
- const looseEls = cur.elements.filter((e) => /^\d+$/.test(e.ref ?? ""));
566
- const rows = [];
567
- looseEls.forEach((e) => {
568
- const idx = cur.elements.indexOf(e);
569
- rows.push({ ref: e.ref, render: () => {
570
- const row2 = el("div", e.missing ? "missing" : "");
571
- row2.append(el("span", "num", `${e.ref}.`));
572
- const comp = (e.react ?? e.vue)?.component;
573
- row2.append(el("span", "label", `${e.selector.split(" > ").pop()}${comp ? " \xB7 " + comp : ""}${e.missing ? " \xB7 " + T.elMissing : ""}`));
574
- row2.append(el("span", "kind", e.tag));
575
- const x = el("button", "", "\u2715");
576
- x.title = T.tipRemove;
577
- x.onclick = () => h.onRemoveElement(cur.id, idx);
578
- row2.append(x);
579
- list.append(row2);
580
- } });
581
- });
582
- cur.regions?.forEach((r, i) => {
583
- rows.push({ ref: r.ref ?? "", render: () => {
584
- const kids = r.ref ? cur.elements.filter((e) => isChildRef(e.ref, r.ref)) : [];
585
- const row2 = el("div", "");
586
- if (kids.length) {
587
- row2.classList.add("group");
588
- const chev = el("span", "chev", vm.expanded === r.ref ? "\u25BE" : "\u25B8");
589
- chev.title = T.tipExpand;
590
- row2.append(chev);
591
- }
592
- row2.append(el("span", "num", `${r.ref ?? ""}.`));
593
- row2.append(el("span", "label", `${T.regionRow(r.rect.w, r.rect.h)}${r.within ? T.regionIn(r.within.split(" > ").pop()) : ""}`));
594
- if (kids.length) row2.append(el("span", "cnt", T.groupInside(kids.length)));
595
- if (!kids.length) row2.append(el("span", "kind region", "region"));
596
- const x = el("button", "", "\u2715");
597
- x.title = T.tipRemove;
598
- x.onclick = () => h.onRemoveRegion(cur.id, i);
599
- row2.append(x);
600
- if (kids.length) row2.addEventListener("click", (ev) => {
601
- const t = ev.target;
602
- if (t.closest("button") || t.closest(".num")) return;
603
- h.onToggleGroup(r.ref);
604
- });
605
- list.append(row2);
606
- if (kids.length && vm.expanded === r.ref) {
607
- kids.forEach((c) => {
608
- const cidx = cur.elements.indexOf(c);
609
- const crow = el("div", "child" + (c.missing ? " missing" : ""));
610
- crow.append(el("span", "num", `${c.ref}.`));
611
- const ccomp = (c.react ?? c.vue)?.component;
612
- crow.append(el("span", "label", `${c.selector.split(" > ").pop()}${ccomp ? " \xB7 " + ccomp : ""}${c.missing ? " \xB7 " + T.elMissing : ""}`));
613
- crow.append(el("span", "kind", c.tag));
614
- const cx = el("button", "", "\u2715");
615
- cx.title = T.tipRemove;
616
- cx.onclick = () => h.onRemoveElement(cur.id, cidx);
617
- crow.append(cx);
618
- list.append(crow);
733
+ const labels = { noNote: T.cartNoNote, count: T.queueCount };
734
+ const cards = queueCards(vm.drafts, vm.batches, vm.href, vm.busy, labels);
735
+ const doneList = lastRoundDone(vm.batches);
736
+ const queueN = cards.length > 0 ? cards.length : doneList.length;
737
+ const h4 = el("h4");
738
+ const g = el("span", "grip");
739
+ g.innerHTML = svg("drag");
740
+ g.setAttribute("aria-hidden", "true");
741
+ h4.append(g);
742
+ const tabs = el("div", "tabs");
743
+ const tabHereBtn = el("button", "tab current" + (vm.tab === "here" ? " on" : ""), T.tabHere);
744
+ tabHereBtn.onclick = () => h.onTab("here");
745
+ const tabQueueBtn = el("button", "tab queue" + (vm.tab === "queue" ? " on" : ""));
746
+ tabQueueBtn.innerHTML = QUEUE_ICON;
747
+ tabQueueBtn.append(document.createTextNode(T.tabQueue), el("span", "badge", String(queueN)));
748
+ tabQueueBtn.onclick = () => h.onTab("queue");
749
+ tabs.append(tabHereBtn, tabQueueBtn);
750
+ h4.append(tabs);
751
+ const close = el("button", "close", "\u2715");
752
+ close.title = T.tipClose;
753
+ close.setAttribute("aria-label", "Close");
754
+ close.onclick = () => h.onClose();
755
+ h4.append(close);
756
+ panel.append(h4);
757
+ if (vm.tab === "here") {
758
+ if (cur) {
759
+ const sub = el("div", "sub");
760
+ sub.append(el("span", "mark", "\u25AE"), document.createTextNode(cur.regions?.length ? T.selCountMixed(cur.elements.length, cur.regions.length) : cur.elements.length > 0 ? T.selCount(cur.elements.length) : T.selNone));
761
+ panel.append(sub);
762
+ const list = el("div", "els");
763
+ const looseEls = cur.elements.filter((e) => /^\d+$/.test(e.ref ?? ""));
764
+ const rows = [];
765
+ looseEls.forEach((e) => {
766
+ const idx = cur.elements.indexOf(e);
767
+ rows.push({ ref: e.ref, render: () => {
768
+ const row2 = el("div", e.missing ? "missing" : "");
769
+ row2.append(el("span", "num", `${e.ref}.`));
770
+ const comp = (e.react ?? e.vue)?.component;
771
+ row2.append(el("span", "label", `${e.selector.split(" > ").pop()}${comp ? " \xB7 " + comp : ""}${e.missing ? " \xB7 " + T.elMissing : ""}`));
772
+ row2.append(el("span", "kind", e.tag));
773
+ const x = el("button", "", "\u2715");
774
+ x.title = T.tipRemove;
775
+ x.onclick = () => h.onRemoveElement(cur.id, idx);
776
+ row2.append(x);
777
+ list.append(row2);
778
+ } });
779
+ });
780
+ cur.regions?.forEach((r, i) => {
781
+ rows.push({ ref: r.ref ?? "", render: () => {
782
+ const kids = r.ref ? cur.elements.filter((e) => isChildRef(e.ref, r.ref)) : [];
783
+ const row2 = el("div", "");
784
+ if (kids.length) {
785
+ row2.classList.add("group");
786
+ const chev = el("span", "chev", vm.expanded === r.ref ? "\u25BE" : "\u25B8");
787
+ chev.title = T.tipExpand;
788
+ row2.append(chev);
789
+ }
790
+ row2.append(el("span", "num", `${r.ref ?? ""}.`));
791
+ row2.append(el("span", "label", `${T.regionRow(r.rect.w, r.rect.h)}${r.within ? T.regionIn(r.within.split(" > ").pop()) : ""}`));
792
+ if (kids.length) row2.append(el("span", "cnt", T.groupInside(kids.length)));
793
+ if (!kids.length) row2.append(el("span", "kind region", "region"));
794
+ const x = el("button", "", "\u2715");
795
+ x.title = T.tipRemove;
796
+ x.onclick = () => h.onRemoveRegion(cur.id, i);
797
+ row2.append(x);
798
+ if (kids.length) row2.addEventListener("click", (ev) => {
799
+ const t = ev.target;
800
+ if (t.closest("button") || t.closest(".num")) return;
801
+ h.onToggleGroup(r.ref);
619
802
  });
803
+ list.append(row2);
804
+ if (kids.length && vm.expanded === r.ref) {
805
+ kids.forEach((c) => {
806
+ const cidx = cur.elements.indexOf(c);
807
+ const crow = el("div", "child" + (c.missing ? " missing" : ""));
808
+ crow.append(el("span", "num", `${c.ref}.`));
809
+ const ccomp = (c.react ?? c.vue)?.component;
810
+ crow.append(el("span", "label", `${c.selector.split(" > ").pop()}${ccomp ? " \xB7 " + ccomp : ""}${c.missing ? " \xB7 " + T.elMissing : ""}`));
811
+ crow.append(el("span", "kind", c.tag));
812
+ const cx = el("button", "", "\u2715");
813
+ cx.title = T.tipRemove;
814
+ cx.onclick = () => h.onRemoveElement(cur.id, cidx);
815
+ crow.append(cx);
816
+ list.append(crow);
817
+ });
818
+ }
819
+ } });
820
+ });
821
+ rows.sort((a, b) => Number(a.ref) - Number(b.ref));
822
+ rows.forEach((r) => r.render());
823
+ panel.append(list);
824
+ let ta = textareas.get(cur.id);
825
+ if (!ta) {
826
+ ta = document.createElement("textarea");
827
+ const id = cur.id;
828
+ ta.addEventListener("input", () => h.onNoteInput(id, ta.value));
829
+ textareas.set(id, ta);
830
+ }
831
+ ta.placeholder = cur.elements.length + (cur.regions?.length ?? 0) >= 2 ? T.notePlaceholderMulti : T.notePlaceholder;
832
+ if (ta.value !== cur.note) ta.value = cur.note;
833
+ ta.disabled = vm.locked;
834
+ panel.append(ta);
835
+ }
836
+ } else {
837
+ const queue = el("div", "queue");
838
+ for (const c of cards) {
839
+ const a = document.createElement("a");
840
+ a.className = `card ${c.kind}` + (c.isCurrent ? " here" : "");
841
+ a.href = c.url;
842
+ a.onclick = (ev) => {
843
+ ev.preventDefault();
844
+ h.onGoPage(c.url);
845
+ };
846
+ const thumb = el("div", "thumb");
847
+ for (let i = 0; i < Math.min(c.count, 2); i++) thumb.append(el("span", `rect ${c.kind}`));
848
+ a.append(thumb);
849
+ const body = el("div", "body");
850
+ body.append(el("div", "title", c.title), el("div", "path", c.path), el("div", "meta", c.meta));
851
+ a.append(body);
852
+ a.append(el("span", "chip", CHIP_LABEL[c.kind]));
853
+ if (c.isCurrent) a.append(el("span", "here", T.herePage));
854
+ queue.append(a);
855
+ }
856
+ if (!vm.busy && doneList.length > 0) {
857
+ const doneRow = el("button", "done-row", `${vm.doneOpen ? "\u25BE" : "\u25B8"} ${T.doneRow(doneList.length)}`);
858
+ doneRow.onclick = () => h.onToggleDone();
859
+ queue.append(doneRow);
860
+ if (vm.doneOpen) {
861
+ for (const b of doneList) {
862
+ const url = b.page?.url ?? vm.href;
863
+ const item = el("div", "done-item");
864
+ item.append(el("span", "title", b.page?.title || url), el("span", "path", pageLoc(url, vm.href)), el("span", "summary", "\u2713 " + truncate402(b.summary ?? "")));
865
+ queue.append(item);
620
866
  }
621
- } });
622
- });
623
- rows.sort((a, b) => Number(a.ref) - Number(b.ref));
624
- rows.forEach((r) => r.render());
625
- panel.append(list);
626
- let ta = textareas.get(cur.id);
627
- if (!ta) {
628
- ta = document.createElement("textarea");
629
- const id = cur.id;
630
- ta.addEventListener("input", () => h.onNoteInput(id, ta.value));
631
- textareas.set(id, ta);
867
+ }
632
868
  }
633
- ta.placeholder = cur.elements.length + (cur.regions?.length ?? 0) >= 2 ? T.notePlaceholderMulti : T.notePlaceholder;
634
- if (ta.value !== cur.note) ta.value = cur.note;
635
- panel.append(ta);
869
+ panel.append(queue);
636
870
  }
871
+ const readyDrafts = vm.drafts.filter((b) => b.note.trim());
637
872
  const row = el("div", "row");
638
- const send = el("button", "send", "Send \u2192");
639
- send.disabled = vm.locked;
640
- send.title = vm.locked ? T.tipSendLocked : T.tipSend;
641
- send.onclick = () => {
642
- closePop();
643
- h.onSend();
644
- };
645
- row.append(el("span", "sends", T.sends), send);
873
+ if (readyDrafts.length > 0) {
874
+ const send = el("button", "send", readyDrafts.length >= 2 ? T.sendPages(readyDrafts.length) : "Send \u2192");
875
+ send.title = T.tipSend;
876
+ send.onclick = () => {
877
+ closePop();
878
+ h.onSend();
879
+ };
880
+ row.append(el("span", "sends", T.sends), send);
881
+ } else {
882
+ let footText;
883
+ let footOk = false;
884
+ if (vm.busy) footText = vm.followPaused ? T.footPaused(round.done, round.total) : T.footBusy(round.done, round.total);
885
+ else if (doneList.length > 0) {
886
+ footText = T.footAllDone(doneList.length);
887
+ footOk = true;
888
+ } else footText = T.footHint;
889
+ row.append(el("span", `foot${footOk ? " ok" : ""}`, footText));
890
+ }
646
891
  panel.append(row);
647
892
  for (const id of [...textareas.keys()]) if (!vm.drafts.some((b) => b.id === id)) textareas.delete(id);
648
893
  if (wasTa && sel && wasTa.isConnected) {
@@ -663,7 +908,7 @@ textarea{width:100%;min-height:80px;resize:none;font:inherit;color:var(--fg);bac
663
908
  m.append(el("span", "n", String(n)));
664
909
  root.append(m);
665
910
  };
666
- const cur = vm.drafts.length ? vm.drafts[vm.drafts.length - 1] : null;
911
+ const cur = vm.current;
667
912
  if (cur) {
668
913
  cur.elements.forEach((e) => {
669
914
  if (e.missing) return;
@@ -683,7 +928,7 @@ textarea{width:100%;min-height:80px;resize:none;font:inherit;color:var(--fg);bac
683
928
  place(el("div", "marker region"), r.rect.x - scrollX, r.rect.y - scrollY, r.rect.w, r.rect.h, r.ref ?? "");
684
929
  });
685
930
  }
686
- const working = vm.locked && vm.connected;
931
+ const working = vm.busy && vm.connected;
687
932
  line.hidden = !working;
688
933
  if (working) {
689
934
  const wbox = (cls) => {
@@ -693,7 +938,9 @@ textarea{width:100%;min-height:80px;resize:none;font:inherit;color:var(--fg);bac
693
938
  m.append(clip);
694
939
  return m;
695
940
  };
696
- for (const b of vm.sent) {
941
+ for (const b of vm.queue) {
942
+ if (b.status === "done") continue;
943
+ if (b.page && !samePage(b.page.url, vm.href)) continue;
697
944
  b.elements.forEach((e) => {
698
945
  if (e.missing) return;
699
946
  if (/[a-z]$/.test(e.ref ?? "")) return;
@@ -976,37 +1223,48 @@ ${Math.round(r.width)}\xD7${Math.round(r.height)}`;
976
1223
  const frame = frames.find((f) => !REACT_INTERNAL_FN.test(f.fn) && !NODE_MODULES_URL.test(f.url));
977
1224
  return frame ? { url: frame.url, line: frame.line, col: frame.col } : null;
978
1225
  }
1226
+ function normalizeDrivePath(path) {
1227
+ return path.replace(/\\/g, "/").replace(/^\/?([a-zA-Z]):\//, (_, drive) => `${drive.toLowerCase()}:/`);
1228
+ }
1229
+ function isLibraryPath(path, root) {
1230
+ const norm = normalizeDrivePath(path);
1231
+ let rel = norm;
1232
+ if (root) {
1233
+ const r = normalizeDrivePath(root).replace(/\/+$/, "");
1234
+ if (norm.startsWith(`${r}/`)) rel = norm.slice(r.length + 1);
1235
+ }
1236
+ return /(^|\/)node_modules\//.test(rel);
1237
+ }
979
1238
 
980
1239
  // src/overlay/frameworks/react.ts
981
- function userLoc(f) {
1240
+ function userLoc(f, root) {
982
1241
  const src = f._debugSource;
983
- const fileName = src?.fileName?.replace(/\\/g, "/");
984
- if (fileName && !fileName.includes("node_modules/")) return `${src.fileName}${src.lineNumber ? ":" + src.lineNumber : ""}`;
1242
+ const fileName = src?.fileName;
1243
+ if (fileName && !isLibraryPath(fileName, root)) return `${fileName}${src.lineNumber ? ":" + src.lineNumber : ""}`;
985
1244
  const stack = typeof f._debugStack === "string" ? f._debugStack : f._debugStack?.stack;
986
1245
  return stack ? pickUserFrame(stack) ?? void 0 : void 0;
987
1246
  }
988
- function reactInfo(el) {
1247
+ function reactInfo(el, root) {
989
1248
  const key = Object.keys(el).find((k) => k.startsWith("__reactFiber$"));
990
1249
  if (!key) return void 0;
991
1250
  let f = el[key] ?? null;
992
1251
  let fallback;
993
1252
  let loc;
994
1253
  let component;
995
- const callers = [];
996
- const callerFrames = [];
1254
+ const callerLocs = [];
997
1255
  let pending;
998
1256
  const commit = (p) => {
999
- if (callers.length + callerFrames.length >= 2) return;
1257
+ if (callerLocs.length >= 2) return;
1000
1258
  if (typeof p === "string") {
1001
- if (p !== loc && !callers.includes(p)) callers.push(p);
1002
- } else callerFrames.push(p);
1259
+ if (p !== loc && !callerLocs.includes(p)) callerLocs.push(p);
1260
+ } else callerLocs.push(p);
1003
1261
  };
1004
1262
  let libBelow = false;
1005
1263
  for (let i = 0; f && i < 30; i++, f = f.return ?? null) {
1006
1264
  const t = f.type;
1007
1265
  const name = t && typeof t !== "string" ? t.displayName || t.name : void 0;
1008
1266
  if (loc === void 0) {
1009
- const l2 = userLoc(f);
1267
+ const l2 = userLoc(f, root);
1010
1268
  if (l2 !== void 0) {
1011
1269
  loc = l2;
1012
1270
  component = name;
@@ -1016,13 +1274,13 @@ ${Math.round(r.width)}\xD7${Math.round(r.height)}`;
1016
1274
  continue;
1017
1275
  }
1018
1276
  if (!name) {
1019
- if ((f._debugSource !== void 0 || f._debugStack !== void 0) && userLoc(f) === void 0) libBelow = true;
1277
+ if ((f._debugSource !== void 0 || f._debugStack !== void 0) && userLoc(f, root) === void 0) libBelow = true;
1020
1278
  continue;
1021
1279
  }
1022
1280
  if (component === void 0) {
1023
- if (libBelow && userLoc(f) === void 0) continue;
1281
+ if (libBelow && userLoc(f, root) === void 0) continue;
1024
1282
  component = name;
1025
- const l2 = userLoc(f);
1283
+ const l2 = userLoc(f, root);
1026
1284
  if (l2 !== void 0) pending = l2;
1027
1285
  continue;
1028
1286
  }
@@ -1030,36 +1288,35 @@ ${Math.round(r.width)}\xD7${Math.round(r.height)}`;
1030
1288
  commit(pending);
1031
1289
  pending = void 0;
1032
1290
  }
1033
- const l = userLoc(f);
1291
+ const l = userLoc(f, root);
1034
1292
  if (l !== void 0) pending = l;
1035
1293
  }
1036
1294
  if (loc === void 0) return fallback;
1037
1295
  if (!component) return fallback;
1038
1296
  const hit = typeof loc === "string" ? { component, source: loc } : { component, frame: loc };
1039
- if (callers.length) hit.callers = callers;
1040
- if (callerFrames.length) hit.callerFrames = callerFrames;
1297
+ if (callerLocs.length) hit.callerLocs = callerLocs;
1041
1298
  return hit;
1042
1299
  }
1043
1300
  var react = { key: "react", detect: reactInfo };
1044
1301
 
1045
1302
  // src/overlay/frameworks/vue.ts
1046
1303
  var ABSOLUTE_RE = /^([a-zA-Z]:\/|\/)/;
1047
- function normalizeVueSource(file) {
1304
+ function normalizeVueSource(file, root) {
1048
1305
  const norm = file.replace(/\\/g, "/");
1049
- if (norm.includes("node_modules")) return void 0;
1306
+ if (isLibraryPath(norm, root)) return void 0;
1050
1307
  if (!ABSOLUTE_RE.test(norm)) return norm;
1051
1308
  const idx = norm.lastIndexOf("/src/");
1052
1309
  if (idx !== -1) return norm.slice(idx + 1);
1053
1310
  const segs = norm.split("/").filter(Boolean);
1054
1311
  return segs.slice(-2).join("/");
1055
1312
  }
1056
- function vueInfo(el) {
1313
+ function vueInfo(el, root) {
1057
1314
  let c = el["__vueParentComponent"] ?? null;
1058
1315
  for (let i = 0; c && i < 10; i++, c = c.parent ?? null) {
1059
1316
  const name = c.type?.name || c.type?.__name;
1060
1317
  if (name) {
1061
1318
  const file = c.type?.__file;
1062
- const source = file ? normalizeVueSource(file) : void 0;
1319
+ const source = file ? normalizeVueSource(file, root) : void 0;
1063
1320
  return source ? { component: name, source } : { component: name };
1064
1321
  }
1065
1322
  }
@@ -1069,9 +1326,13 @@ ${Math.round(r.width)}\xD7${Math.round(r.height)}`;
1069
1326
 
1070
1327
  // src/overlay/frameworks/index.ts
1071
1328
  var adapters = [react, vue];
1329
+ var projectRoot;
1330
+ function setProjectRoot(root) {
1331
+ projectRoot = root;
1332
+ }
1072
1333
  function detectComponent(el) {
1073
1334
  for (const adapter of adapters) {
1074
- const info = adapter.detect(el);
1335
+ const info = adapter.detect(el, projectRoot);
1075
1336
  if (info) return { key: adapter.key, info };
1076
1337
  }
1077
1338
  return void 0;
@@ -1173,28 +1434,31 @@ ${Math.round(r.width)}\xD7${Math.round(r.height)}`;
1173
1434
  if (window.top !== window) return;
1174
1435
  const PORT = __COBRO_PORT__;
1175
1436
  const TOKEN = __COBRO_TOKEN__;
1437
+ setProjectRoot(__COBRO_ROOT__);
1176
1438
  const boot = () => {
1177
1439
  if (document.documentElement.hasAttribute("data-cobro")) return;
1178
1440
  document.documentElement.setAttribute("data-cobro", "1");
1179
1441
  let session = null;
1180
1442
  let drafts = null;
1181
- let current = null;
1182
1443
  let connected = false;
1183
1444
  let stateSeen = false;
1184
1445
  let openPending = false;
1185
1446
  let draftTimer = null;
1186
1447
  let prefs = { theme: "auto", themeLocked: false };
1187
1448
  let expandedGroup = null;
1449
+ let tab = "here";
1450
+ let doneOpen = false;
1451
+ let panelOpen = false;
1188
1452
  const mq = matchMedia("(prefers-color-scheme: dark)");
1189
1453
  const pageInfo = () => ({ url: location.href, title: document.title, viewport: { w: innerWidth, h: innerHeight } });
1190
- const newBatch = () => ({ id: crypto.randomUUID(), note: "", elements: [], status: "draft", createdAt: (/* @__PURE__ */ new Date()).toISOString() });
1454
+ const newBatch = () => ({ id: crypto.randomUUID(), note: "", elements: [], status: "draft", createdAt: (/* @__PURE__ */ new Date()).toISOString(), page: { url: location.href, title: document.title } });
1191
1455
  const ensureCurrent = () => {
1192
1456
  drafts ??= [];
1193
- let b = drafts.find((d) => d.id === current);
1457
+ let b = currentDraft(drafts, location.href);
1194
1458
  if (!b) {
1195
1459
  b = newBatch();
1196
1460
  drafts.push(b);
1197
- current = b.id;
1461
+ tab = "here";
1198
1462
  }
1199
1463
  return b;
1200
1464
  };
@@ -1203,26 +1467,48 @@ ${Math.round(r.width)}\xD7${Math.round(r.height)}`;
1203
1467
  clearTimeout(draftTimer);
1204
1468
  draftTimer = null;
1205
1469
  }
1206
- chan.send({ type: "draft", batches: drafts ?? [] });
1470
+ const href = location.href;
1471
+ const here = (drafts ?? []).filter((d) => !d.page || samePage(d.page.url, href));
1472
+ chan.send({ type: "draft", batches: here, page: { url: href, title: document.title } });
1207
1473
  };
1208
1474
  const pushDraft = () => {
1209
1475
  if (draftTimer) clearTimeout(draftTimer);
1210
1476
  draftTimer = setTimeout(flushDraft, 300);
1211
1477
  };
1212
- const vm = () => ({
1213
- selecting: picker.isActive(),
1214
- connected,
1215
- agent: session?.agent ?? { status: "idle", text: "" },
1216
- strategy: session ? session.strategy ?? session.detected : null,
1217
- drafts: drafts ?? [],
1218
- sent: session?.batches.filter((b) => b.status === "sent") ?? [],
1219
- locked: session?.agent.status === "sent" || session?.agent.status === "working",
1220
- prefs,
1221
- expanded: expandedGroup
1222
- });
1478
+ const isLocked = () => session?.agent.status === "sent" || session?.agent.status === "working";
1479
+ const vm = () => {
1480
+ const href = location.href;
1481
+ return {
1482
+ selecting: picker.isActive(),
1483
+ connected,
1484
+ agent: session?.agent ?? { status: "idle", text: "" },
1485
+ strategy: session ? session.strategy ?? session.detected : null,
1486
+ drafts: drafts ?? [],
1487
+ current: currentDraft(drafts ?? [], href),
1488
+ queue: roundBatches(session?.batches ?? []),
1489
+ // R166·R169: sent·working·라운드 done(현재+다른 페이지 전부)
1490
+ batches: session?.batches ?? [],
1491
+ // R172·R174: 큐 카드·완료 정리는 전체 묶음이 필요
1492
+ busy: (session?.batches ?? []).some((b) => b.status === "sent" || b.status === "working"),
1493
+ locked: isLocked(),
1494
+ // R175
1495
+ followPaused: !!session?.followPaused,
1496
+ // R176
1497
+ pendingElsewhere: pendingElsewhere(session, href),
1498
+ // R171: 다른 페이지에서 끝난 완료의 폴백 "보기" 링크
1499
+ prefs,
1500
+ expanded: expandedGroup,
1501
+ href,
1502
+ tab,
1503
+ doneOpen,
1504
+ panelOpen
1505
+ };
1506
+ };
1223
1507
  const render = () => ui.render(vm());
1224
1508
  const setSelecting = (on) => {
1509
+ if (on && isLocked()) return;
1225
1510
  picker.setActive(on);
1511
+ panelOpen = on;
1226
1512
  if (!on) {
1227
1513
  render();
1228
1514
  return;
@@ -1236,6 +1522,15 @@ ${Math.round(r.width)}\xD7${Math.round(r.height)}`;
1236
1522
  render();
1237
1523
  ui.focusNote();
1238
1524
  };
1525
+ const togglePanelLocked = () => {
1526
+ if (panelOpen) {
1527
+ panelOpen = false;
1528
+ } else {
1529
+ panelOpen = true;
1530
+ tab = "queue";
1531
+ }
1532
+ render();
1533
+ };
1239
1534
  const GROUP_LETTERS = "abcdefghijkl";
1240
1535
  const nextRef = (b) => {
1241
1536
  b.refSeq = (b.refSeq ?? 0) + 1;
@@ -1296,7 +1591,15 @@ ${Math.round(r.width)}\xD7${Math.round(r.height)}`;
1296
1591
  }) };
1297
1592
  };
1298
1593
  const ui = createUI({
1299
- onToggleSelect: () => setSelecting(!picker.isActive()),
1594
+ onToggleSelect: () => {
1595
+ if (isLocked()) {
1596
+ togglePanelLocked();
1597
+ return;
1598
+ }
1599
+ setSelecting(!picker.isActive());
1600
+ },
1601
+ onClose: () => setSelecting(false),
1602
+ // R175: 패널 헤더 ✕ — 잠겨서 선택이 이미 꺼져 있어도 패널을 닫는다
1300
1603
  onNoteInput: (id, note) => {
1301
1604
  const b = drafts?.find((d) => d.id === id);
1302
1605
  if (b) {
@@ -1330,15 +1633,29 @@ ${Math.round(r.width)}\xD7${Math.round(r.height)}`;
1330
1633
  flushDraft();
1331
1634
  chan.send({ type: "send", batchIds: ready.map((b) => b.id), page: pageInfo() });
1332
1635
  drafts = (drafts ?? []).filter((b) => !ready.includes(b));
1333
- current = null;
1334
1636
  picker.setActive(false);
1335
1637
  expandedGroup = null;
1638
+ tab = "queue";
1336
1639
  render();
1337
1640
  },
1338
1641
  onSettings: (patch) => chan.send({ type: "settings", patch }),
1339
1642
  onToggleGroup: (ref) => {
1340
1643
  expandedGroup = expandedGroup === ref ? null : ref;
1341
1644
  render();
1645
+ },
1646
+ onTab: (t) => {
1647
+ tab = t;
1648
+ render();
1649
+ },
1650
+ onToggleDone: () => {
1651
+ doneOpen = !doneOpen;
1652
+ render();
1653
+ },
1654
+ onGoPage: (url) => {
1655
+ if (samePage(url, location.href)) {
1656
+ tab = "here";
1657
+ render();
1658
+ } else location.assign(url);
1342
1659
  }
1343
1660
  });
1344
1661
  const applyTheme = () => ui.setTheme(resolveTheme(prefs.theme, {
@@ -1402,8 +1719,12 @@ ${Math.round(r.width)}\xD7${Math.round(r.height)}`;
1402
1719
  if (e.ctrlKey && e.shiftKey && e.code === "KeyF") {
1403
1720
  e.preventDefault();
1404
1721
  ui.closePop();
1722
+ if (isLocked()) {
1723
+ togglePanelLocked();
1724
+ return;
1725
+ }
1405
1726
  setSelecting(!picker.isActive());
1406
- } else if (e.key === "Escape" && picker.isActive()) {
1727
+ } else if (e.key === "Escape" && (picker.isActive() || panelOpen)) {
1407
1728
  setSelecting(false);
1408
1729
  }
1409
1730
  }, true);
@@ -1417,14 +1738,24 @@ ${Math.round(r.width)}\xD7${Math.round(r.height)}`;
1417
1738
  session = m.session;
1418
1739
  prefs = m.ui;
1419
1740
  applyTheme();
1741
+ if (isLocked() && picker.isActive()) picker.setActive(false);
1420
1742
  const nonDraft = new Set(m.session.batches.filter((b) => b.status !== "draft").map((b) => b.id));
1421
1743
  const serverDrafts = m.session.batches.filter((b) => b.status === "draft");
1422
- if (drafts === null) {
1423
- drafts = serverDrafts.map(resolveDraft);
1424
- current = drafts[drafts.length - 1]?.id ?? null;
1425
- } else {
1426
- drafts = drafts.filter((d) => !nonDraft.has(d.id));
1744
+ const href = location.href;
1745
+ const localById = new Map((drafts ?? []).map((d) => [d.id, d]));
1746
+ const merged = [];
1747
+ for (const raw of serverDrafts) {
1748
+ const b = raw.page ? raw : { ...raw, page: { url: href, title: document.title } };
1749
+ if (!samePage(b.page.url, href)) {
1750
+ merged.push(b);
1751
+ continue;
1752
+ }
1753
+ const local = localById.get(b.id);
1754
+ merged.push(local ?? (drafts === null ? resolveDraft(b) : b));
1427
1755
  }
1756
+ const seen = new Set(serverDrafts.map((b) => b.id));
1757
+ for (const d of drafts ?? []) if (!seen.has(d.id) && !nonDraft.has(d.id)) merged.push(d);
1758
+ drafts = merged;
1428
1759
  render();
1429
1760
  stateSeen = true;
1430
1761
  if (openPending) {