cobro-mcp 0.9.4 → 0.10.1

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);min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
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,33 @@ 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",
328
+ tipSendLocked: "\uC5D0\uC774\uC804\uD2B8\uAC00 \uC218\uC815 \uC911 \u2014 \uC644\uB8CC \uD6C4 \uBCF4\uB0BC \uC218 \uC788\uC5B4\uC694",
329
+ tipSendNote: "\uBA54\uBAA8\uB97C \uC801\uC73C\uBA74 \uBCF4\uB0BC \uC218 \uC788\uC5B4\uC694",
204
330
  tipRemove: "\uC774 \uC694\uC18C \uBE7C\uAE30",
331
+ cartNoNote: "\uBA54\uBAA8 \uC5C6\uC74C",
332
+ doneElsewhere: (path) => `\u2713 ${path} \uBCF4\uAE30`,
333
+ tabHere: "\uC774 \uD398\uC774\uC9C0",
334
+ tabQueue: "\uD050",
335
+ herePage: "\uC9C0\uAE08 \uBCF4\uACE0 \uC788\uB294 \uD398\uC774\uC9C0",
336
+ queueCount: (n) => `${n}\uAC1C`,
337
+ footBusy: (d, n) => `\uCC98\uB9AC \uC911 ${d}/${n} \u2014 \uB05D\uB098\uBA74 \uB2F4\uC744 \uC218 \uC788\uC5B4\uC694`,
338
+ 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`,
339
+ 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`,
340
+ footHint: "\uBA54\uBAA8\uB97C \uC801\uC73C\uBA74 \uBCF4\uB0BC \uC218 \uC788\uC5B4\uC694",
341
+ tipSelectLocked: "\uC5D0\uC774\uC804\uD2B8\uAC00 \uC218\uC815 \uC911 \u2014 \uD050\uB97C \uBCFC \uC218 \uC788\uC5B4\uC694, \uB2F4\uAE30\uB294 \uC644\uB8CC \uD6C4",
342
+ followPaused: "\uB530\uB77C\uAC00\uAE30 \uC911\uC9C0 \u2014 \uC644\uB8CC\uB418\uBA74 \u300C\uBCF4\uAE30\u300D \uB9C1\uD06C\uB85C \uD655\uC778",
343
+ doneRow: (n) => `\u2713 \uC644\uB8CC ${n}\uD398\uC774\uC9C0 \xB7 \uC0C8\uB85C \uB2F4\uC73C\uBA74 \uC815\uB9AC\uB429\uB2C8\uB2E4`,
344
+ sendPages: (n) => `Send \xB7 ${n}\uD398\uC774\uC9C0 \u2192`,
345
+ chipDraft: "\uCD08\uC548",
346
+ progress: (d, n) => `${d}/${n}`,
205
347
  tipSettings: "\uC124\uC815",
206
348
  theme: "\uD14C\uB9C8",
207
349
  themeAuto: "\uC790\uB3D9",
208
350
  themeDark: "\uC5B4\uB461\uAC8C",
209
351
  themeLight: "\uBC1D\uAC8C",
210
352
  themeFrost: "\uC720\uB9AC",
211
- themeLocked: "COBRO_THEME \uD658\uACBD \uBCC0\uC218\uB85C \uACE0\uC815\uB428"
353
+ themeLocked: "COBRO_THEME \uD658\uACBD \uBCC0\uC218\uB85C \uACE0\uC815\uB428",
354
+ strategyLabel: "\uAC31\uC2E0 \uC804\uB7B5"
212
355
  },
213
356
  en: {
214
357
  agentIdle: "Agent not connected",
@@ -230,7 +373,7 @@ textarea{width:100%;min-height:80px;resize:none;font:inherit;color:var(--fg);bac
230
373
  hintMore: (n) => `${n} selected \xB7 pick more or write a note`,
231
374
  hintNote: "Write a note, then press Send",
232
375
  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]}`,
376
+ 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
377
  selCount: (n) => `${n} element(s) selected`,
235
378
  selCountMixed: (n, m) => `${n} element(s) + ${m} region(s) selected`,
236
379
  regionRow: (w, h) => `\u25AD ${w}\xD7${h}`,
@@ -247,14 +390,32 @@ textarea{width:100%;min-height:80px;resize:none;font:inherit;color:var(--fg);bac
247
390
  sends: "Sends selector \xB7 styles \xB7 shot \xB7 console",
248
391
  tipSend: "Send the selected elements and note to the agent",
249
392
  tipSendLocked: "Agent is working \u2014 you can send after done",
393
+ tipSendNote: "Write a note to send",
250
394
  tipRemove: "Remove this element",
395
+ cartNoNote: "No note",
396
+ doneElsewhere: (path) => `\u2713 view ${path}`,
397
+ tabHere: "This page",
398
+ tabQueue: "Queue",
399
+ herePage: "The page you're on",
400
+ queueCount: (n) => `${n} element${n === 1 ? "" : "s"}`,
401
+ footBusy: (d, n) => `Working ${d}/${n} \u2014 you can add more once it's done`,
402
+ footPaused: (d, n) => `Working ${d}/${n} \u2014 you left this page, so following paused`,
403
+ footAllDone: (n) => `\u2713 ${n} page${n === 1 ? "" : "s"} done \u2014 pick an element to start a new request`,
404
+ footHint: "Write a note to send",
405
+ tipSelectLocked: "Agent is working \u2014 view the queue; picking resumes after done",
406
+ followPaused: `Following paused \u2014 check the "view" link when it's done`,
407
+ doneRow: (n) => `\u2713 ${n} done \xB7 cleared when you pick again`,
408
+ sendPages: (n) => `Send \xB7 ${n} pages \u2192`,
409
+ chipDraft: "Draft",
410
+ progress: (d, n) => `${d}/${n}`,
251
411
  tipSettings: "Settings",
252
412
  theme: "Theme",
253
413
  themeAuto: "Auto",
254
414
  themeDark: "Dark",
255
415
  themeLight: "Light",
256
416
  themeFrost: "Frost",
257
- themeLocked: "Pinned by COBRO_THEME"
417
+ themeLocked: "Pinned by COBRO_THEME",
418
+ strategyLabel: "Refresh"
258
419
  }
259
420
  }[LANG];
260
421
  var THEME_LABEL = { auto: T.themeAuto, dark: T.themeDark, light: T.themeLight, frost: T.themeFrost };
@@ -267,7 +428,7 @@ textarea{width:100%;min-height:80px;resize:none;font:inherit;color:var(--fg);bac
267
428
  done: (t) => stripStatusLabel(t, LABELS.done)
268
429
  };
269
430
  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 };
431
+ var CHIP_LABEL = { idle: T.chipIdle, waiting: T.chipWaiting, sent: T.chipSent, working: T.chipWorking, done: T.chipDone, draft: T.chipDraft };
271
432
  function makeDraggable(target, handle, opts) {
272
433
  const current = target.style.translate.split(" ").map((v) => parseFloat(v) || 0);
273
434
  let off = { x: current[0] ?? 0, y: current[1] ?? 0 };
@@ -344,14 +505,6 @@ textarea{width:100%;min-height:80px;resize:none;font:inherit;color:var(--fg);bac
344
505
  const chipLabel = document.createElement("span");
345
506
  chipLabel.className = "chip-label";
346
507
  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
508
  const status = document.createElement("span");
356
509
  status.className = "status";
357
510
  const statusIn = document.createElement("span");
@@ -362,7 +515,7 @@ textarea{width:100%;min-height:80px;resize:none;font:inherit;color:var(--fg);bac
362
515
  const gripParts = iconBtn("drag", "Move", T.tipDrag, "grip", false);
363
516
  const grip = gripParts.btn;
364
517
  grip.setAttribute("aria-label", "Move toolbar");
365
- toolbar.append(grip, selectBtn, chip, chip2, status, gearBtn);
518
+ toolbar.append(grip, selectBtn, chip, status, gearBtn);
366
519
  const line = document.createElement("span");
367
520
  line.className = "line";
368
521
  line.hidden = true;
@@ -386,10 +539,25 @@ textarea{width:100%;min-height:80px;resize:none;font:inherit;color:var(--fg);bac
386
539
  });
387
540
  seg2.append(...segButtons);
388
541
  popRow.append(popLabel, seg2);
542
+ const stratRow = document.createElement("div");
543
+ stratRow.className = "pop-row strategy";
544
+ const stratLabel = document.createElement("span");
545
+ stratLabel.className = "pop-label";
546
+ stratLabel.textContent = T.strategyLabel;
547
+ const stratVal = document.createElement("span");
548
+ stratVal.className = "strategy-val";
549
+ const stratIco = document.createElement("span");
550
+ stratIco.className = "ico";
551
+ stratIco.innerHTML = svg("sync");
552
+ const stratValText = document.createElement("span");
553
+ stratVal.append(stratIco, stratValText);
554
+ const stratSub = document.createElement("span");
555
+ stratSub.className = "pop-sub";
556
+ stratRow.append(stratLabel, stratVal, stratSub);
389
557
  const popNote = document.createElement("div");
390
558
  popNote.className = "pop-note";
391
559
  popNote.textContent = T.themeLocked;
392
- pop.append(popRow, popNote);
560
+ pop.append(popRow, stratRow, popNote);
393
561
  let popOpen = false;
394
562
  let popCloseListeners = null;
395
563
  const closePop = () => {
@@ -487,6 +655,7 @@ textarea{width:100%;min-height:80px;resize:none;font:inherit;color:var(--fg);bac
487
655
  if (text != null) d.textContent = text;
488
656
  return d;
489
657
  };
658
+ const truncate402 = (s) => s.length > 40 ? s.slice(0, 40) + "\u2026" : s;
490
659
  function render(vm) {
491
660
  renderMarkers(vm);
492
661
  updateTbH();
@@ -494,15 +663,22 @@ textarea{width:100%;min-height:80px;resize:none;font:inherit;color:var(--fg);bac
494
663
  const wasTa = active instanceof HTMLTextAreaElement ? active : null;
495
664
  const sel = wasTa ? [wasTa.selectionStart, wasTa.selectionEnd] : null;
496
665
  selectBtn.classList.toggle("on", vm.selecting);
666
+ selectBtn.classList.toggle("locked", vm.locked);
667
+ selectBtn.title = vm.locked ? T.tipSelectLocked : T.tipSelect;
497
668
  for (const btn of segButtons) {
498
669
  btn.classList.toggle("on", btn.dataset.theme === vm.prefs.theme);
499
670
  btn.disabled = vm.prefs.themeLocked;
500
671
  }
501
672
  popNote.hidden = !vm.prefs.themeLocked;
502
- const cur = vm.drafts[vm.drafts.length - 1];
673
+ const cur = vm.current;
503
674
  const hasElements = !!cur && cur.elements.length + (cur.regions?.length ?? 0) > 0;
504
675
  let hint;
676
+ let gotoUrl = null;
505
677
  if (!vm.connected) hint = T.disconnected;
678
+ else if (!vm.busy && vm.pendingElsewhere) {
679
+ hint = T.doneElsewhere(vm.pendingElsewhere.path);
680
+ gotoUrl = vm.pendingElsewhere.url;
681
+ } else if (vm.locked && vm.followPaused) hint = T.followPaused;
506
682
  else {
507
683
  let text;
508
684
  if (vm.agent.status === "sent" || vm.agent.status === "working" || vm.agent.status === "done") text = AGENT_TEXT[vm.agent.status](vm.agent.text);
@@ -523,7 +699,16 @@ textarea{width:100%;min-height:80px;resize:none;font:inherit;color:var(--fg);bac
523
699
  statusIn.classList.add("enter");
524
700
  }
525
701
  }
526
- statusIn.textContent = displayHint;
702
+ if (gotoUrl) {
703
+ statusIn.textContent = "";
704
+ const a = document.createElement("a");
705
+ a.className = "goto";
706
+ a.href = gotoUrl;
707
+ a.textContent = displayHint;
708
+ statusIn.append(a);
709
+ } else {
710
+ statusIn.textContent = displayHint;
711
+ }
527
712
  statusIn.title = hint;
528
713
  if (hovering) {
529
714
  applyHoverScroll();
@@ -532,117 +717,183 @@ textarea{width:100%;min-height:80px;resize:none;font:inherit;color:var(--fg);bac
532
717
  status.classList.toggle("off", !vm.connected);
533
718
  chip.classList.toggle("off", !vm.connected);
534
719
  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;
720
+ const round = roundOf(vm.queue);
721
+ const roundSuffix = vm.busy ? " " + T.progress(round.done, round.total) : "";
722
+ chipLabel.textContent = vm.connected ? CHIP_LABEL[vm.agent.status] + roundSuffix : T.chipOff;
723
+ chip.title = vm.connected ? DOT_TITLE[vm.agent.status] + roundSuffix : T.disconnected;
724
+ stratRow.hidden = !vm.strategy;
538
725
  if (vm.strategy) {
539
- chip2Label.textContent = vm.strategy;
540
- chip2.title = T.tipStrategy(vm.strategy);
726
+ stratValText.textContent = vm.strategy;
727
+ stratSub.textContent = T.strategyDesc(vm.strategy);
728
+ stratRow.title = T.strategyDesc(vm.strategy);
541
729
  }
542
- const show = vm.selecting;
543
- panel.classList.toggle("show", show);
544
- if (vm.drafts.length === 0) {
730
+ panel.classList.toggle("show", vm.panelOpen);
731
+ if (!vm.panelOpen) {
545
732
  panel.textContent = "";
546
733
  textareas.clear();
547
734
  return;
548
735
  }
549
- if (!show) return;
550
736
  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);
737
+ const labels = { noNote: T.cartNoNote, count: T.queueCount };
738
+ const cards = queueCards(vm.drafts, vm.batches, vm.href, vm.busy, labels);
739
+ const doneList = lastRoundDone(vm.batches);
740
+ const queueN = cards.length > 0 ? cards.length : doneList.length;
741
+ const h4 = el("h4");
742
+ const g = el("span", "grip");
743
+ g.innerHTML = svg("drag");
744
+ g.setAttribute("aria-hidden", "true");
745
+ h4.append(g);
746
+ const tabs = el("div", "tabs");
747
+ const tabHereBtn = el("button", "tab current" + (vm.tab === "here" ? " on" : ""), T.tabHere);
748
+ tabHereBtn.onclick = () => h.onTab("here");
749
+ const tabQueueBtn = el("button", "tab queue" + (vm.tab === "queue" ? " on" : ""));
750
+ tabQueueBtn.innerHTML = QUEUE_ICON;
751
+ tabQueueBtn.append(document.createTextNode(T.tabQueue), el("span", "badge", String(queueN)));
752
+ tabQueueBtn.onclick = () => h.onTab("queue");
753
+ tabs.append(tabHereBtn, tabQueueBtn);
754
+ h4.append(tabs);
755
+ const close = el("button", "close", "\u2715");
756
+ close.title = T.tipClose;
757
+ close.setAttribute("aria-label", "Close");
758
+ close.onclick = () => h.onClose();
759
+ h4.append(close);
760
+ panel.append(h4);
761
+ if (vm.tab === "here") {
762
+ if (cur) {
763
+ const sub = el("div", "sub");
764
+ 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));
765
+ panel.append(sub);
766
+ const list = el("div", "els");
767
+ const looseEls = cur.elements.filter((e) => /^\d+$/.test(e.ref ?? ""));
768
+ const rows = [];
769
+ looseEls.forEach((e) => {
770
+ const idx = cur.elements.indexOf(e);
771
+ rows.push({ ref: e.ref, render: () => {
772
+ const row2 = el("div", e.missing ? "missing" : "");
773
+ row2.append(el("span", "num", `${e.ref}.`));
774
+ const comp = (e.react ?? e.vue)?.component;
775
+ row2.append(el("span", "label", `${e.selector.split(" > ").pop()}${comp ? " \xB7 " + comp : ""}${e.missing ? " \xB7 " + T.elMissing : ""}`));
776
+ row2.append(el("span", "kind", e.tag));
777
+ const x = el("button", "", "\u2715");
778
+ x.title = T.tipRemove;
779
+ x.onclick = () => h.onRemoveElement(cur.id, idx);
780
+ row2.append(x);
781
+ list.append(row2);
782
+ } });
783
+ });
784
+ cur.regions?.forEach((r, i) => {
785
+ rows.push({ ref: r.ref ?? "", render: () => {
786
+ const kids = r.ref ? cur.elements.filter((e) => isChildRef(e.ref, r.ref)) : [];
787
+ const row2 = el("div", "");
788
+ if (kids.length) {
789
+ row2.classList.add("group");
790
+ const chev = el("span", "chev", vm.expanded === r.ref ? "\u25BE" : "\u25B8");
791
+ chev.title = T.tipExpand;
792
+ row2.append(chev);
793
+ }
794
+ row2.append(el("span", "num", `${r.ref ?? ""}.`));
795
+ row2.append(el("span", "label", `${T.regionRow(r.rect.w, r.rect.h)}${r.within ? T.regionIn(r.within.split(" > ").pop()) : ""}`));
796
+ if (kids.length) row2.append(el("span", "cnt", T.groupInside(kids.length)));
797
+ if (!kids.length) row2.append(el("span", "kind region", "region"));
798
+ const x = el("button", "", "\u2715");
799
+ x.title = T.tipRemove;
800
+ x.onclick = () => h.onRemoveRegion(cur.id, i);
801
+ row2.append(x);
802
+ if (kids.length) row2.addEventListener("click", (ev) => {
803
+ const t = ev.target;
804
+ if (t.closest("button") || t.closest(".num")) return;
805
+ h.onToggleGroup(r.ref);
619
806
  });
807
+ list.append(row2);
808
+ if (kids.length && vm.expanded === r.ref) {
809
+ kids.forEach((c) => {
810
+ const cidx = cur.elements.indexOf(c);
811
+ const crow = el("div", "child" + (c.missing ? " missing" : ""));
812
+ crow.append(el("span", "num", `${c.ref}.`));
813
+ const ccomp = (c.react ?? c.vue)?.component;
814
+ crow.append(el("span", "label", `${c.selector.split(" > ").pop()}${ccomp ? " \xB7 " + ccomp : ""}${c.missing ? " \xB7 " + T.elMissing : ""}`));
815
+ crow.append(el("span", "kind", c.tag));
816
+ const cx = el("button", "", "\u2715");
817
+ cx.title = T.tipRemove;
818
+ cx.onclick = () => h.onRemoveElement(cur.id, cidx);
819
+ crow.append(cx);
820
+ list.append(crow);
821
+ });
822
+ }
823
+ } });
824
+ });
825
+ rows.sort((a, b) => Number(a.ref) - Number(b.ref));
826
+ rows.forEach((r) => r.render());
827
+ panel.append(list);
828
+ let ta = textareas.get(cur.id);
829
+ if (!ta) {
830
+ ta = document.createElement("textarea");
831
+ const id = cur.id;
832
+ ta.addEventListener("input", () => h.onNoteInput(id, ta.value));
833
+ textareas.set(id, ta);
834
+ }
835
+ ta.placeholder = cur.elements.length + (cur.regions?.length ?? 0) >= 2 ? T.notePlaceholderMulti : T.notePlaceholder;
836
+ if (ta.value !== cur.note) ta.value = cur.note;
837
+ ta.disabled = vm.locked;
838
+ panel.append(ta);
839
+ }
840
+ } else {
841
+ const queue = el("div", "queue");
842
+ for (const c of cards) {
843
+ const a = document.createElement("a");
844
+ a.className = `card ${c.kind}` + (c.isCurrent ? " here" : "");
845
+ a.href = c.url;
846
+ a.onclick = (ev) => {
847
+ ev.preventDefault();
848
+ h.onGoPage(c.url);
849
+ };
850
+ const thumb = el("div", "thumb");
851
+ for (let i = 0; i < Math.min(c.count, 2); i++) thumb.append(el("span", `rect ${c.kind}`));
852
+ a.append(thumb);
853
+ const body = el("div", "body");
854
+ body.append(el("div", "title", c.title), el("div", "path", c.path), el("div", "meta", c.meta));
855
+ a.append(body);
856
+ a.append(el("span", "chip", CHIP_LABEL[c.kind]));
857
+ if (c.isCurrent) a.append(el("span", "here", T.herePage));
858
+ queue.append(a);
859
+ }
860
+ if (!vm.busy && doneList.length > 0) {
861
+ const doneRow = el("button", "done-row", `${vm.doneOpen ? "\u25BE" : "\u25B8"} ${T.doneRow(doneList.length)}`);
862
+ doneRow.onclick = () => h.onToggleDone();
863
+ queue.append(doneRow);
864
+ if (vm.doneOpen) {
865
+ for (const b of doneList) {
866
+ const url = b.page?.url ?? vm.href;
867
+ const item = el("div", "done-item");
868
+ item.append(el("span", "title", b.page?.title || url), el("span", "path", pageLoc(url, vm.href)), el("span", "summary", "\u2713 " + truncate402(b.summary ?? "")));
869
+ queue.append(item);
620
870
  }
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);
871
+ }
632
872
  }
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);
873
+ panel.append(queue);
636
874
  }
875
+ const readyDrafts = vm.drafts.filter((b) => b.note.trim());
637
876
  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);
877
+ const send = el("button", "send", readyDrafts.length >= 2 ? T.sendPages(readyDrafts.length) : "Send \u2192");
878
+ if (readyDrafts.length > 0) {
879
+ send.title = T.tipSend;
880
+ send.onclick = () => {
881
+ closePop();
882
+ h.onSend();
883
+ };
884
+ row.append(el("span", "sends", T.sends), send);
885
+ } else {
886
+ send.disabled = true;
887
+ send.title = vm.locked ? T.tipSendLocked : T.tipSendNote;
888
+ let footText;
889
+ let footOk = false;
890
+ if (vm.busy) footText = vm.followPaused ? T.footPaused(round.done, round.total) : T.footBusy(round.done, round.total);
891
+ else if (doneList.length > 0) {
892
+ footText = T.footAllDone(doneList.length);
893
+ footOk = true;
894
+ } else footText = T.footHint;
895
+ row.append(el("span", `foot${footOk ? " ok" : ""}`, footText), send);
896
+ }
646
897
  panel.append(row);
647
898
  for (const id of [...textareas.keys()]) if (!vm.drafts.some((b) => b.id === id)) textareas.delete(id);
648
899
  if (wasTa && sel && wasTa.isConnected) {
@@ -663,7 +914,7 @@ textarea{width:100%;min-height:80px;resize:none;font:inherit;color:var(--fg);bac
663
914
  m.append(el("span", "n", String(n)));
664
915
  root.append(m);
665
916
  };
666
- const cur = vm.drafts.length ? vm.drafts[vm.drafts.length - 1] : null;
917
+ const cur = vm.current;
667
918
  if (cur) {
668
919
  cur.elements.forEach((e) => {
669
920
  if (e.missing) return;
@@ -683,7 +934,7 @@ textarea{width:100%;min-height:80px;resize:none;font:inherit;color:var(--fg);bac
683
934
  place(el("div", "marker region"), r.rect.x - scrollX, r.rect.y - scrollY, r.rect.w, r.rect.h, r.ref ?? "");
684
935
  });
685
936
  }
686
- const working = vm.locked && vm.connected;
937
+ const working = vm.busy && vm.connected;
687
938
  line.hidden = !working;
688
939
  if (working) {
689
940
  const wbox = (cls) => {
@@ -693,7 +944,9 @@ textarea{width:100%;min-height:80px;resize:none;font:inherit;color:var(--fg);bac
693
944
  m.append(clip);
694
945
  return m;
695
946
  };
696
- for (const b of vm.sent) {
947
+ for (const b of vm.queue) {
948
+ if (b.status === "done") continue;
949
+ if (b.page && !samePage(b.page.url, vm.href)) continue;
697
950
  b.elements.forEach((e) => {
698
951
  if (e.missing) return;
699
952
  if (/[a-z]$/.test(e.ref ?? "")) return;
@@ -1193,23 +1446,25 @@ ${Math.round(r.width)}\xD7${Math.round(r.height)}`;
1193
1446
  document.documentElement.setAttribute("data-cobro", "1");
1194
1447
  let session = null;
1195
1448
  let drafts = null;
1196
- let current = null;
1197
1449
  let connected = false;
1198
1450
  let stateSeen = false;
1199
1451
  let openPending = false;
1200
1452
  let draftTimer = null;
1201
1453
  let prefs = { theme: "auto", themeLocked: false };
1202
1454
  let expandedGroup = null;
1455
+ let tab = "here";
1456
+ let doneOpen = false;
1457
+ let panelOpen = false;
1203
1458
  const mq = matchMedia("(prefers-color-scheme: dark)");
1204
1459
  const pageInfo = () => ({ url: location.href, title: document.title, viewport: { w: innerWidth, h: innerHeight } });
1205
- const newBatch = () => ({ id: crypto.randomUUID(), note: "", elements: [], status: "draft", createdAt: (/* @__PURE__ */ new Date()).toISOString() });
1460
+ const newBatch = () => ({ id: crypto.randomUUID(), note: "", elements: [], status: "draft", createdAt: (/* @__PURE__ */ new Date()).toISOString(), page: { url: location.href, title: document.title } });
1206
1461
  const ensureCurrent = () => {
1207
1462
  drafts ??= [];
1208
- let b = drafts.find((d) => d.id === current);
1463
+ let b = currentDraft(drafts, location.href);
1209
1464
  if (!b) {
1210
1465
  b = newBatch();
1211
1466
  drafts.push(b);
1212
- current = b.id;
1467
+ tab = "here";
1213
1468
  }
1214
1469
  return b;
1215
1470
  };
@@ -1218,26 +1473,48 @@ ${Math.round(r.width)}\xD7${Math.round(r.height)}`;
1218
1473
  clearTimeout(draftTimer);
1219
1474
  draftTimer = null;
1220
1475
  }
1221
- chan.send({ type: "draft", batches: drafts ?? [] });
1476
+ const href = location.href;
1477
+ const here = (drafts ?? []).filter((d) => !d.page || samePage(d.page.url, href));
1478
+ chan.send({ type: "draft", batches: here, page: { url: href, title: document.title } });
1222
1479
  };
1223
1480
  const pushDraft = () => {
1224
1481
  if (draftTimer) clearTimeout(draftTimer);
1225
1482
  draftTimer = setTimeout(flushDraft, 300);
1226
1483
  };
1227
- const vm = () => ({
1228
- selecting: picker.isActive(),
1229
- connected,
1230
- agent: session?.agent ?? { status: "idle", text: "" },
1231
- strategy: session ? session.strategy ?? session.detected : null,
1232
- drafts: drafts ?? [],
1233
- sent: session?.batches.filter((b) => b.status === "sent") ?? [],
1234
- locked: session?.agent.status === "sent" || session?.agent.status === "working",
1235
- prefs,
1236
- expanded: expandedGroup
1237
- });
1484
+ const isLocked = () => session?.agent.status === "sent" || session?.agent.status === "working";
1485
+ const vm = () => {
1486
+ const href = location.href;
1487
+ return {
1488
+ selecting: picker.isActive(),
1489
+ connected,
1490
+ agent: session?.agent ?? { status: "idle", text: "" },
1491
+ strategy: session ? session.strategy ?? session.detected : null,
1492
+ drafts: drafts ?? [],
1493
+ current: currentDraft(drafts ?? [], href),
1494
+ queue: roundBatches(session?.batches ?? []),
1495
+ // R166·R169: sent·working·라운드 done(현재+다른 페이지 전부)
1496
+ batches: session?.batches ?? [],
1497
+ // R172·R174: 큐 카드·완료 정리는 전체 묶음이 필요
1498
+ busy: (session?.batches ?? []).some((b) => b.status === "sent" || b.status === "working"),
1499
+ locked: isLocked(),
1500
+ // R175
1501
+ followPaused: !!session?.followPaused,
1502
+ // R176
1503
+ pendingElsewhere: pendingElsewhere(session, href),
1504
+ // R171: 다른 페이지에서 끝난 완료의 폴백 "보기" 링크
1505
+ prefs,
1506
+ expanded: expandedGroup,
1507
+ href,
1508
+ tab,
1509
+ doneOpen,
1510
+ panelOpen
1511
+ };
1512
+ };
1238
1513
  const render = () => ui.render(vm());
1239
1514
  const setSelecting = (on) => {
1515
+ if (on && isLocked()) return;
1240
1516
  picker.setActive(on);
1517
+ panelOpen = on;
1241
1518
  if (!on) {
1242
1519
  render();
1243
1520
  return;
@@ -1251,6 +1528,15 @@ ${Math.round(r.width)}\xD7${Math.round(r.height)}`;
1251
1528
  render();
1252
1529
  ui.focusNote();
1253
1530
  };
1531
+ const togglePanelLocked = () => {
1532
+ if (panelOpen) {
1533
+ panelOpen = false;
1534
+ } else {
1535
+ panelOpen = true;
1536
+ tab = "queue";
1537
+ }
1538
+ render();
1539
+ };
1254
1540
  const GROUP_LETTERS = "abcdefghijkl";
1255
1541
  const nextRef = (b) => {
1256
1542
  b.refSeq = (b.refSeq ?? 0) + 1;
@@ -1311,7 +1597,15 @@ ${Math.round(r.width)}\xD7${Math.round(r.height)}`;
1311
1597
  }) };
1312
1598
  };
1313
1599
  const ui = createUI({
1314
- onToggleSelect: () => setSelecting(!picker.isActive()),
1600
+ onToggleSelect: () => {
1601
+ if (isLocked()) {
1602
+ togglePanelLocked();
1603
+ return;
1604
+ }
1605
+ setSelecting(!picker.isActive());
1606
+ },
1607
+ onClose: () => setSelecting(false),
1608
+ // R175: 패널 헤더 ✕ — 잠겨서 선택이 이미 꺼져 있어도 패널을 닫는다
1315
1609
  onNoteInput: (id, note) => {
1316
1610
  const b = drafts?.find((d) => d.id === id);
1317
1611
  if (b) {
@@ -1345,15 +1639,29 @@ ${Math.round(r.width)}\xD7${Math.round(r.height)}`;
1345
1639
  flushDraft();
1346
1640
  chan.send({ type: "send", batchIds: ready.map((b) => b.id), page: pageInfo() });
1347
1641
  drafts = (drafts ?? []).filter((b) => !ready.includes(b));
1348
- current = null;
1349
1642
  picker.setActive(false);
1350
1643
  expandedGroup = null;
1644
+ tab = "queue";
1351
1645
  render();
1352
1646
  },
1353
1647
  onSettings: (patch) => chan.send({ type: "settings", patch }),
1354
1648
  onToggleGroup: (ref) => {
1355
1649
  expandedGroup = expandedGroup === ref ? null : ref;
1356
1650
  render();
1651
+ },
1652
+ onTab: (t) => {
1653
+ tab = t;
1654
+ render();
1655
+ },
1656
+ onToggleDone: () => {
1657
+ doneOpen = !doneOpen;
1658
+ render();
1659
+ },
1660
+ onGoPage: (url) => {
1661
+ if (samePage(url, location.href)) {
1662
+ tab = "here";
1663
+ render();
1664
+ } else location.assign(url);
1357
1665
  }
1358
1666
  });
1359
1667
  const applyTheme = () => ui.setTheme(resolveTheme(prefs.theme, {
@@ -1417,8 +1725,12 @@ ${Math.round(r.width)}\xD7${Math.round(r.height)}`;
1417
1725
  if (e.ctrlKey && e.shiftKey && e.code === "KeyF") {
1418
1726
  e.preventDefault();
1419
1727
  ui.closePop();
1728
+ if (isLocked()) {
1729
+ togglePanelLocked();
1730
+ return;
1731
+ }
1420
1732
  setSelecting(!picker.isActive());
1421
- } else if (e.key === "Escape" && picker.isActive()) {
1733
+ } else if (e.key === "Escape" && (picker.isActive() || panelOpen)) {
1422
1734
  setSelecting(false);
1423
1735
  }
1424
1736
  }, true);
@@ -1432,14 +1744,24 @@ ${Math.round(r.width)}\xD7${Math.round(r.height)}`;
1432
1744
  session = m.session;
1433
1745
  prefs = m.ui;
1434
1746
  applyTheme();
1747
+ if (isLocked() && picker.isActive()) picker.setActive(false);
1435
1748
  const nonDraft = new Set(m.session.batches.filter((b) => b.status !== "draft").map((b) => b.id));
1436
1749
  const serverDrafts = m.session.batches.filter((b) => b.status === "draft");
1437
- if (drafts === null) {
1438
- drafts = serverDrafts.map(resolveDraft);
1439
- current = drafts[drafts.length - 1]?.id ?? null;
1440
- } else {
1441
- drafts = drafts.filter((d) => !nonDraft.has(d.id));
1750
+ const href = location.href;
1751
+ const localById = new Map((drafts ?? []).map((d) => [d.id, d]));
1752
+ const merged = [];
1753
+ for (const raw of serverDrafts) {
1754
+ const b = raw.page ? raw : { ...raw, page: { url: href, title: document.title } };
1755
+ if (!samePage(b.page.url, href)) {
1756
+ merged.push(b);
1757
+ continue;
1758
+ }
1759
+ const local = localById.get(b.id);
1760
+ merged.push(local ?? (drafts === null ? resolveDraft(b) : b));
1442
1761
  }
1762
+ const seen = new Set(serverDrafts.map((b) => b.id));
1763
+ for (const d of drafts ?? []) if (!seen.has(d.id) && !nonDraft.has(d.id)) merged.push(d);
1764
+ drafts = merged;
1443
1765
  render();
1444
1766
  stateSeen = true;
1445
1767
  if (openPending) {