openclacky 1.5.19 → 1.5.21
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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +95 -0
- data/README.md +19 -1
- data/README_CN.md +1 -1
- data/README_JA.md +1 -1
- data/lib/clacky/agent/chunk_index.rb +8 -1
- data/lib/clacky/agent/feedback.rb +30 -0
- data/lib/clacky/agent/history_navigation.rb +17 -3
- data/lib/clacky/agent/hook_manager.rb +6 -1
- data/lib/clacky/agent/llm_caller.rb +17 -3
- data/lib/clacky/agent/message_compressor_helper.rb +36 -2
- data/lib/clacky/agent/session_serializer.rb +45 -4
- data/lib/clacky/agent.rb +135 -55
- data/lib/clacky/agent_config.rb +82 -4
- data/lib/clacky/billing/platform_billing.rb +10 -5
- data/lib/clacky/brand_config.rb +1 -1
- data/lib/clacky/cli.rb +5 -4
- data/lib/clacky/client.rb +12 -5
- data/lib/clacky/deepseek_web_tools.rb +156 -0
- data/lib/clacky/default_extensions/ext-studio/api/handler.rb +8 -0
- data/lib/clacky/default_extensions/ext-studio/panels/studio/view.js +97 -20
- data/lib/clacky/default_extensions/time_machine/panels/time_machine/view.js +1 -0
- data/lib/clacky/default_skills/onboard/SKILL.md +1 -1
- data/lib/clacky/extension/loader.rb +5 -3
- data/lib/clacky/locales/en.rb +5 -5
- data/lib/clacky/locales/zh.rb +5 -5
- data/lib/clacky/message_format/anthropic.rb +20 -0
- data/lib/clacky/message_format/open_ai.rb +31 -14
- data/lib/clacky/message_history.rb +45 -4
- data/lib/clacky/platform_http_client.rb +6 -2
- data/lib/clacky/providers.rb +38 -138
- data/lib/clacky/search_config.rb +47 -13
- data/lib/clacky/server/browser_manager.rb +35 -5
- data/lib/clacky/server/channel/channel_manager.rb +6 -6
- data/lib/clacky/server/discover.rb +5 -4
- data/lib/clacky/server/http_server.rb +285 -53
- data/lib/clacky/server/model_prices.rb +10 -5
- data/lib/clacky/server/pid_files.rb +25 -0
- data/lib/clacky/server/server_master.rb +9 -3
- data/lib/clacky/server/session_registry.rb +37 -7
- data/lib/clacky/server/web_ui_controller.rb +72 -0
- data/lib/clacky/session_manager.rb +28 -6
- data/lib/clacky/skill.rb +1 -1
- data/lib/clacky/tools/ask_user.rb +4 -1
- data/lib/clacky/tools/base.rb +3 -2
- data/lib/clacky/tools/browser.rb +18 -2
- data/lib/clacky/tools/file_reader.rb +1 -1
- data/lib/clacky/tools/terminal/session_manager.rb +19 -1
- data/lib/clacky/tools/terminal.rb +65 -23
- data/lib/clacky/tools/trash_manager.rb +5 -3
- data/lib/clacky/tools/web_fetch.rb +81 -22
- data/lib/clacky/tools/web_search.rb +45 -4
- data/lib/clacky/ui2/ui_controller.rb +1 -1
- data/lib/clacky/ui_interface.rb +4 -3
- data/lib/clacky/utils/file_processor.rb +51 -0
- data/lib/clacky/utils/model_pricing.rb +242 -39
- data/lib/clacky/version.rb +1 -1
- data/lib/clacky/web/app.css +1158 -249
- data/lib/clacky/web/app.js +82 -13
- data/lib/clacky/web/components/chat-navigator.js +8 -1
- data/lib/clacky/web/components/code-editor.js +6 -2
- data/lib/clacky/web/components/mentions.js +2 -13
- data/lib/clacky/web/components/model-picker.js +92 -97
- data/lib/clacky/web/components/onboard.js +13 -10
- data/lib/clacky/web/components/sidebar.js +6 -2
- data/lib/clacky/web/features/archive/store.js +196 -0
- data/lib/clacky/web/features/archive/view.js +409 -0
- data/lib/clacky/web/features/backup/view.js +17 -7
- data/lib/clacky/web/features/billing/view.js +84 -10
- data/lib/clacky/web/features/channels/view.js +75 -29
- data/lib/clacky/web/features/new-session/store.js +3 -3
- data/lib/clacky/web/features/new-session/view.js +1 -0
- data/lib/clacky/web/features/trash/store.js +14 -11
- data/lib/clacky/web/features/trash/view.js +66 -13
- data/lib/clacky/web/features/workspace/view.js +421 -16
- data/lib/clacky/web/i18n.js +242 -70
- data/lib/clacky/web/index.html +270 -117
- data/lib/clacky/web/projects.js +56 -1
- data/lib/clacky/web/sessions.js +609 -267
- data/lib/clacky/web/settings.js +186 -32
- data/lib/clacky/web/ws-dispatcher.js +33 -21
- data/lib/clacky.rb +4 -2
- metadata +6 -1
|
@@ -238,6 +238,7 @@ const BillingView = (() => {
|
|
|
238
238
|
_bindChartTooltip();
|
|
239
239
|
_bindHeatmapTooltip();
|
|
240
240
|
_bindTrendTooltip();
|
|
241
|
+
_scrollHeatmapToLatest();
|
|
241
242
|
}
|
|
242
243
|
|
|
243
244
|
// ── Scorecard ───────────────────────────────────────────────────────────────
|
|
@@ -350,6 +351,14 @@ const BillingView = (() => {
|
|
|
350
351
|
// so following the pointer covers the data no matter which side it flips to.
|
|
351
352
|
// There it docks just outside `chart` instead, keeping the plot fully visible.
|
|
352
353
|
const MOBILE_TOOLTIP_MAX_WIDTH = 768;
|
|
354
|
+
const MOBILE_CHART_MEDIA_QUERY = `(max-width: ${MOBILE_TOOLTIP_MAX_WIDTH}px)`;
|
|
355
|
+
|
|
356
|
+
function _isMobileChartLayout() {
|
|
357
|
+
if (typeof window.matchMedia === "function") {
|
|
358
|
+
return window.matchMedia(MOBILE_CHART_MEDIA_QUERY).matches;
|
|
359
|
+
}
|
|
360
|
+
return window.innerWidth <= MOBILE_TOOLTIP_MAX_WIDTH;
|
|
361
|
+
}
|
|
353
362
|
|
|
354
363
|
function _placeTooltip(tooltip, e, chart) {
|
|
355
364
|
tooltip.style.display = "block";
|
|
@@ -431,8 +440,7 @@ const BillingView = (() => {
|
|
|
431
440
|
const tooltip = document.getElementById("billing-tooltip");
|
|
432
441
|
if (!grid || !tooltip) return;
|
|
433
442
|
|
|
434
|
-
|
|
435
|
-
const cell = e.target.closest(".billing-heat-cell");
|
|
443
|
+
const showTooltip = (cell, e) => {
|
|
436
444
|
if (!cell || cell.classList.contains("is-empty") || !cell.dataset.date) {
|
|
437
445
|
tooltip.style.display = "none";
|
|
438
446
|
return;
|
|
@@ -453,6 +461,21 @@ const BillingView = (() => {
|
|
|
453
461
|
</div>
|
|
454
462
|
`;
|
|
455
463
|
_placeTooltip(tooltip, e, grid);
|
|
464
|
+
};
|
|
465
|
+
|
|
466
|
+
grid.addEventListener("mousemove", (e) => {
|
|
467
|
+
showTooltip(e.target.closest(".billing-heat-cell"), e);
|
|
468
|
+
});
|
|
469
|
+
|
|
470
|
+
grid.addEventListener("click", (e) => {
|
|
471
|
+
if (!_isMobileChartLayout()) return;
|
|
472
|
+
|
|
473
|
+
grid.querySelectorAll(".billing-heat-cell.is-active").forEach(cell => cell.classList.remove("is-active"));
|
|
474
|
+
const cell = _nearestHeatmapCell(grid, e.clientX, e.clientY);
|
|
475
|
+
if (cell && !cell.classList.contains("is-empty") && cell.dataset.date) {
|
|
476
|
+
cell.classList.add("is-active");
|
|
477
|
+
}
|
|
478
|
+
showTooltip(cell, e);
|
|
456
479
|
});
|
|
457
480
|
|
|
458
481
|
grid.addEventListener("mouseleave", () => {
|
|
@@ -461,9 +484,49 @@ const BillingView = (() => {
|
|
|
461
484
|
});
|
|
462
485
|
}
|
|
463
486
|
|
|
464
|
-
//
|
|
465
|
-
//
|
|
466
|
-
|
|
487
|
+
// Mobile heatmap cells are intentionally compact, so include the surrounding
|
|
488
|
+
// gap in their logical hit area by snapping taps to the nearest week and day.
|
|
489
|
+
function _nearestHeatmapCell(grid, clientX, clientY) {
|
|
490
|
+
const nearestByRect = (elements, coordinate, startKey, endKey) => {
|
|
491
|
+
let nearest = null;
|
|
492
|
+
let nearestDistance = Infinity;
|
|
493
|
+
elements.forEach(element => {
|
|
494
|
+
const rect = element.getBoundingClientRect();
|
|
495
|
+
const distance = coordinate < rect[startKey]
|
|
496
|
+
? rect[startKey] - coordinate
|
|
497
|
+
: coordinate > rect[endKey] ? coordinate - rect[endKey] : 0;
|
|
498
|
+
if (distance < nearestDistance) {
|
|
499
|
+
nearest = element;
|
|
500
|
+
nearestDistance = distance;
|
|
501
|
+
}
|
|
502
|
+
});
|
|
503
|
+
return nearest;
|
|
504
|
+
};
|
|
505
|
+
|
|
506
|
+
const week = nearestByRect(
|
|
507
|
+
Array.from(grid.querySelectorAll(".billing-heat-week")),
|
|
508
|
+
clientX,
|
|
509
|
+
"left",
|
|
510
|
+
"right"
|
|
511
|
+
);
|
|
512
|
+
if (!week) return null;
|
|
513
|
+
|
|
514
|
+
return nearestByRect(
|
|
515
|
+
Array.from(week.querySelectorAll(".billing-heat-cell")),
|
|
516
|
+
clientY,
|
|
517
|
+
"top",
|
|
518
|
+
"bottom"
|
|
519
|
+
);
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
function _scrollHeatmapToLatest() {
|
|
523
|
+
if (!_isMobileChartLayout()) return;
|
|
524
|
+
const scroller = document.querySelector(".billing-heatmap-year .billing-heat-year-wrap");
|
|
525
|
+
if (scroller) scroller.scrollLeft = scroller.scrollWidth;
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
// Even with larger mobile dots, a single point is too small to target reliably.
|
|
529
|
+
// Snap to the nearest point on the x axis so the whole column is clickable.
|
|
467
530
|
function _nearestTrendDot(svg, clientX, clientY) {
|
|
468
531
|
const count = parseInt(svg.dataset.count, 10);
|
|
469
532
|
const xStep = parseFloat(svg.dataset.xStep);
|
|
@@ -487,7 +550,7 @@ const BillingView = (() => {
|
|
|
487
550
|
const crosshairX = svg.querySelector(".billing-trend-crosshair-x");
|
|
488
551
|
const crosshairY = svg.querySelector(".billing-trend-crosshair-y");
|
|
489
552
|
|
|
490
|
-
|
|
553
|
+
const showTooltip = (e) => {
|
|
491
554
|
const dot = _nearestTrendDot(svg, e.clientX, e.clientY);
|
|
492
555
|
if (!dot) {
|
|
493
556
|
tooltip.style.display = "none";
|
|
@@ -515,6 +578,11 @@ const BillingView = (() => {
|
|
|
515
578
|
</div>
|
|
516
579
|
`;
|
|
517
580
|
_placeTooltip(tooltip, e, svg);
|
|
581
|
+
};
|
|
582
|
+
|
|
583
|
+
svg.addEventListener("mousemove", showTooltip);
|
|
584
|
+
svg.addEventListener("pointerdown", (e) => {
|
|
585
|
+
if (e.pointerType !== "mouse") showTooltip(e);
|
|
518
586
|
});
|
|
519
587
|
|
|
520
588
|
svg.addEventListener("mouseleave", () => {
|
|
@@ -788,9 +856,14 @@ const BillingView = (() => {
|
|
|
788
856
|
const maxCost = Math.max(...costs, 0.0001);
|
|
789
857
|
const minCost = Math.min(...costs);
|
|
790
858
|
|
|
791
|
-
const
|
|
792
|
-
const
|
|
793
|
-
const
|
|
859
|
+
const mobileLayout = _isMobileChartLayout();
|
|
860
|
+
const contentWidth = document.getElementById("billing-content")?.clientWidth || window.innerWidth;
|
|
861
|
+
const pad = mobileLayout
|
|
862
|
+
? { top: 14, right: 12, bottom: 26, left: 58 }
|
|
863
|
+
: { top: 12, right: 16, bottom: 20, left: 48 };
|
|
864
|
+
const w = mobileLayout ? Math.max(240, contentWidth - 30) : 400;
|
|
865
|
+
const h = mobileLayout ? 160 : 100;
|
|
866
|
+
const dotRadius = mobileLayout ? 4 : 3;
|
|
794
867
|
const plotW = w - pad.left - pad.right;
|
|
795
868
|
const plotH = h - pad.top - pad.bottom;
|
|
796
869
|
|
|
@@ -851,7 +924,7 @@ const BillingView = (() => {
|
|
|
851
924
|
<line class="billing-trend-crosshair billing-trend-crosshair-y" x1="${pad.left}" y1="0" x2="${w - pad.right}" y2="0" />
|
|
852
925
|
<path d="${linePath}" fill="none" class="billing-trend-line" />
|
|
853
926
|
${costs.map((c, i) => `
|
|
854
|
-
<circle cx="${pts[i].x.toFixed(2)}" cy="${pts[i].y.toFixed(2)}" r="
|
|
927
|
+
<circle cx="${pts[i].x.toFixed(2)}" cy="${pts[i].y.toFixed(2)}" r="${dotRadius}" class="billing-trend-dot" data-index="${i}" data-date="${days[i].date}" data-cost="${currencySymbol}${_formatCost(c)}" />
|
|
855
928
|
`).join("")}
|
|
856
929
|
</svg>
|
|
857
930
|
</div>
|
|
@@ -1041,6 +1114,7 @@ const BillingView = (() => {
|
|
|
1041
1114
|
Billing.on("billing:actionError", _onActionError);
|
|
1042
1115
|
document.addEventListener("currencychange", () => Billing.refreshView());
|
|
1043
1116
|
document.getElementById("header-print-receipt")?.addEventListener("click", _openScorecardShare);
|
|
1117
|
+
window.matchMedia?.(MOBILE_CHART_MEDIA_QUERY).addEventListener?.("change", _render);
|
|
1044
1118
|
}
|
|
1045
1119
|
|
|
1046
1120
|
return { init: _subscribe };
|
|
@@ -328,6 +328,8 @@ const ChannelsView = (() => {
|
|
|
328
328
|
|
|
329
329
|
// ── QQ one-click bind (official QR page) ─────────────────────────────────
|
|
330
330
|
|
|
331
|
+
const REFRESH_SVG = `<svg viewBox="0 0 24 24" width="22" height="22" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M21 12a9 9 0 1 1-2.64-6.36"/><polyline points="21 3 21 9 15 9"/></svg>`;
|
|
332
|
+
|
|
331
333
|
let _qrPollTimer = null;
|
|
332
334
|
let _qrAbort = false;
|
|
333
335
|
|
|
@@ -336,13 +338,17 @@ const ChannelsView = (() => {
|
|
|
336
338
|
_qrAbort = true;
|
|
337
339
|
}
|
|
338
340
|
|
|
339
|
-
async function
|
|
341
|
+
async function _startQrTask() {
|
|
340
342
|
_stopQrPolling();
|
|
343
|
+
const task = await Channels.qrStart();
|
|
341
344
|
_qrAbort = false;
|
|
345
|
+
return task;
|
|
346
|
+
}
|
|
342
347
|
|
|
348
|
+
async function _openQrBind() {
|
|
343
349
|
let task;
|
|
344
350
|
try {
|
|
345
|
-
task = await
|
|
351
|
+
task = await _startQrTask();
|
|
346
352
|
} catch (e) {
|
|
347
353
|
Modal.toast("Error: " + e.message, "error");
|
|
348
354
|
return;
|
|
@@ -356,26 +362,40 @@ const ChannelsView = (() => {
|
|
|
356
362
|
overlay.className = "modal-overlay";
|
|
357
363
|
overlay.innerHTML = `
|
|
358
364
|
<div class="modal-box qq-qr-modal" role="dialog" aria-modal="true" aria-labelledby="qq-qr-title">
|
|
359
|
-
<button type="button" class="modal-close-btn" id="qq-qr-close" aria-label="
|
|
365
|
+
<button type="button" class="modal-close-btn" id="qq-qr-close" aria-label="${_esc(I18n.t("modal.close"))}"></button>
|
|
360
366
|
<h3 id="qq-qr-title">${_esc(I18n.t("channels.qq.qr.title"))}</h3>
|
|
361
|
-
<
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
367
|
+
<div class="qq-qr-pending" id="qq-qr-pending">
|
|
368
|
+
<p class="qq-qr-hint">${_esc(I18n.t("channels.qq.qr.hint"))}</p>
|
|
369
|
+
<div class="qq-qr-frame-wrap" id="qq-qr-frame-wrap">
|
|
370
|
+
<iframe class="qq-qr-frame" id="qq-qr-frame" src="${_esc(task.connectUrl)}" title="${_esc(I18n.t("channels.qq.qr.title"))}" referrerpolicy="no-referrer" scrolling="no"></iframe>
|
|
371
|
+
<button type="button" class="qq-qr-refresh" id="qq-qr-refresh" hidden>
|
|
372
|
+
<span class="qq-qr-refresh-icon">${REFRESH_SVG}</span>
|
|
373
|
+
<span id="qq-qr-refresh-text"></span>
|
|
374
|
+
</button>
|
|
375
|
+
</div>
|
|
376
|
+
<div class="qq-qr-status" id="qq-qr-status">${_esc(I18n.t("channels.qq.qr.waiting"))}</div>
|
|
377
|
+
<div class="qq-qr-actions">
|
|
378
|
+
<a class="qq-qr-open-btn" id="qq-qr-open" href="${_esc(task.connectUrl)}" target="_blank" rel="noopener">
|
|
379
|
+
${_esc(I18n.t("channels.qq.qr.open"))}
|
|
380
|
+
</a>
|
|
381
|
+
<button type="button" class="qq-qr-copy-btn" id="qq-qr-copy">${_esc(I18n.t("channels.qq.qr.copy"))}</button>
|
|
382
|
+
</div>
|
|
383
|
+
</div>
|
|
384
|
+
<div class="qq-qr-success" id="qq-qr-success" hidden>
|
|
385
|
+
<div class="qq-qr-success-title">${_esc(I18n.t("channels.qq.qr.success"))}</div>
|
|
386
|
+
<p class="qq-qr-hint">${_esc(I18n.t("channels.qq.qr.successDesc"))}</p>
|
|
387
|
+
<button type="button" class="qq-qr-open-btn" id="qq-qr-done">${_esc(I18n.t("channels.qq.qr.done"))}</button>
|
|
368
388
|
</div>
|
|
369
389
|
</div>
|
|
370
390
|
`;
|
|
371
391
|
document.body.appendChild(overlay);
|
|
372
392
|
|
|
393
|
+
let bound = false;
|
|
373
394
|
const closeModal = () => { _stopQrPolling(); overlay.remove(); };
|
|
374
|
-
|
|
395
|
+
// While waiting, a stray backdrop click would drop the bind before the scan lands.
|
|
396
|
+
overlay.addEventListener("click", (e) => { if (e.target === overlay && bound) closeModal(); });
|
|
375
397
|
$("qq-qr-close").addEventListener("click", closeModal);
|
|
376
|
-
$("qq-qr-
|
|
377
|
-
$("qq-qr-status").textContent = I18n.t("channels.qq.qr.scanPrompt");
|
|
378
|
-
});
|
|
398
|
+
$("qq-qr-done").addEventListener("click", closeModal);
|
|
379
399
|
$("qq-qr-copy").addEventListener("click", async () => {
|
|
380
400
|
try {
|
|
381
401
|
await navigator.clipboard.writeText(task.connectUrl);
|
|
@@ -386,13 +406,44 @@ const ChannelsView = (() => {
|
|
|
386
406
|
} catch (_) { /* ignore */ }
|
|
387
407
|
});
|
|
388
408
|
|
|
389
|
-
|
|
390
|
-
|
|
409
|
+
const showExpired = (key) => {
|
|
410
|
+
_stopQrPolling();
|
|
411
|
+
$("qq-qr-frame-wrap").classList.add("expired");
|
|
412
|
+
$("qq-qr-refresh-text").textContent = I18n.t(key);
|
|
413
|
+
$("qq-qr-refresh").hidden = false;
|
|
414
|
+
$("qq-qr-status").textContent = "";
|
|
415
|
+
};
|
|
391
416
|
|
|
392
|
-
|
|
417
|
+
$("qq-qr-refresh").addEventListener("click", async () => {
|
|
418
|
+
const btn = $("qq-qr-refresh");
|
|
419
|
+
btn.disabled = true;
|
|
420
|
+
try {
|
|
421
|
+
task = await _startQrTask();
|
|
422
|
+
} catch (e) {
|
|
423
|
+
Modal.toast("Error: " + e.message, "error");
|
|
424
|
+
btn.disabled = false;
|
|
425
|
+
return;
|
|
426
|
+
}
|
|
427
|
+
$("qq-qr-frame").src = task.connectUrl;
|
|
428
|
+
$("qq-qr-open").href = task.connectUrl;
|
|
429
|
+
$("qq-qr-frame-wrap").classList.remove("expired");
|
|
430
|
+
btn.hidden = true;
|
|
431
|
+
btn.disabled = false;
|
|
432
|
+
$("qq-qr-status").textContent = I18n.t("channels.qq.qr.waiting");
|
|
433
|
+
_pollQr(task.taskId, { onSuccess, onExpired: showExpired });
|
|
434
|
+
});
|
|
435
|
+
|
|
436
|
+
const onSuccess = () => {
|
|
437
|
+
bound = true;
|
|
438
|
+
$("qq-qr-pending").hidden = true;
|
|
439
|
+
$("qq-qr-success").hidden = false;
|
|
440
|
+
$("qq-qr-done").focus();
|
|
441
|
+
};
|
|
442
|
+
|
|
443
|
+
_pollQr(task.taskId, { onSuccess, onExpired: showExpired });
|
|
393
444
|
}
|
|
394
445
|
|
|
395
|
-
|
|
446
|
+
function _pollQr(taskId, { onSuccess, onExpired }) {
|
|
396
447
|
const POLL_MS = 2000;
|
|
397
448
|
const tick = async () => {
|
|
398
449
|
if (_qrAbort) return;
|
|
@@ -400,27 +451,22 @@ const ChannelsView = (() => {
|
|
|
400
451
|
try {
|
|
401
452
|
const status = await Channels.qrPoll(taskId);
|
|
402
453
|
if (_qrAbort) return;
|
|
403
|
-
const statusEl = $("qq-qr-status");
|
|
404
454
|
if (status === "completed") {
|
|
405
|
-
|
|
455
|
+
_stopQrPolling();
|
|
456
|
+
onSuccess();
|
|
406
457
|
await Channels.load({ silent: true });
|
|
407
|
-
setTimeout(closeModal, 1200);
|
|
408
458
|
return;
|
|
409
459
|
}
|
|
410
460
|
if (status === "expired") {
|
|
411
|
-
|
|
412
|
-
_stopQrPolling();
|
|
461
|
+
onExpired("channels.qq.qr.expired");
|
|
413
462
|
return;
|
|
414
463
|
}
|
|
415
464
|
} catch (e) {
|
|
416
465
|
if (_qrAbort) return;
|
|
417
|
-
// 404/422 means the session was pruned (~600 s TTL):
|
|
418
|
-
//
|
|
466
|
+
// 404/422 means the session was pruned (~600 s TTL): offer a refresh
|
|
467
|
+
// instead of spinning on the stale code forever.
|
|
419
468
|
if (e.status === 404 || e.status === 422) {
|
|
420
|
-
|
|
421
|
-
if (statusEl) statusEl.textContent = I18n.t("channels.qq.qr.sessionExpired");
|
|
422
|
-
Modal.toast(I18n.t("channels.qq.qr.sessionExpired"), "warning");
|
|
423
|
-
_stopQrPolling();
|
|
469
|
+
onExpired("channels.qq.qr.sessionExpired");
|
|
424
470
|
keepGoing = false;
|
|
425
471
|
}
|
|
426
472
|
// Other errors (network blips) keep polling.
|
|
@@ -152,11 +152,11 @@ const NewSessionStore = (() => {
|
|
|
152
152
|
_emit("newSession:creating", { creating: true });
|
|
153
153
|
|
|
154
154
|
try {
|
|
155
|
-
// useDefaults=true: quick-create from sidebar —
|
|
156
|
-
//
|
|
155
|
+
// useDefaults=true: quick-create from sidebar — ignore the /#new draft
|
|
156
|
+
// (agent / name / model / dir / project) so the session always starts clean.
|
|
157
157
|
const agentId = useDefaults ? "general" : (_state.selectedAgentId || "general");
|
|
158
158
|
const adv = _state.advanced;
|
|
159
|
-
const name = adv.name.trim() || _autoName(existingSessions);
|
|
159
|
+
const name = (!useDefaults && adv.name.trim()) || _autoName(existingSessions);
|
|
160
160
|
|
|
161
161
|
const payload = { name, agent_profile: agentId, source: "manual" };
|
|
162
162
|
if (!useDefaults) {
|
|
@@ -471,6 +471,7 @@ const NewSessionView = (() => {
|
|
|
471
471
|
item.className = "img-preview-item";
|
|
472
472
|
item.title = img.name;
|
|
473
473
|
const thumbnail = document.createElement("img");
|
|
474
|
+
thumbnail.className = "img-preview-thumb";
|
|
474
475
|
thumbnail.src = img.dataUrl;
|
|
475
476
|
thumbnail.alt = img.name;
|
|
476
477
|
const removeBtn = document.createElement("button");
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
// ───────────────────────────────────────────────────────────────────────────
|
|
14
14
|
|
|
15
15
|
const TrashStore = (() => {
|
|
16
|
-
const SOURCES = Object.freeze({ ALL: "all", CRON: "cron", MANUAL: "manual" });
|
|
16
|
+
const SOURCES = Object.freeze({ ALL: "all", CRON: "cron", MANUAL: "manual", CHANNEL: "channel" });
|
|
17
17
|
|
|
18
18
|
let _files = [];
|
|
19
19
|
let _totals = { count: 0, size: 0 };
|
|
@@ -51,9 +51,9 @@ const TrashStore = (() => {
|
|
|
51
51
|
get sessions() {
|
|
52
52
|
let list = _sessions;
|
|
53
53
|
if (_filterSource !== SOURCES.ALL) {
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
54
|
+
// Same predicate the archive type filter uses: sessions with no source
|
|
55
|
+
// count as regular ones.
|
|
56
|
+
list = list.filter(s => (s.source || SOURCES.MANUAL) === _filterSource);
|
|
57
57
|
}
|
|
58
58
|
if (_filterProjectId !== null) {
|
|
59
59
|
list = list.filter(s => String(s.project_id ?? "") === _filterProjectId);
|
|
@@ -200,14 +200,14 @@ const TrashStore = (() => {
|
|
|
200
200
|
}
|
|
201
201
|
},
|
|
202
202
|
|
|
203
|
-
|
|
204
|
-
if (!Array.isArray(items)) return
|
|
205
|
-
if (!daysOld || daysOld <= 0) return items
|
|
203
|
+
matching(items, daysOld) {
|
|
204
|
+
if (!Array.isArray(items)) return [];
|
|
205
|
+
if (!daysOld || daysOld <= 0) return items;
|
|
206
206
|
const cutoff = Date.now() - daysOld * 86400000;
|
|
207
207
|
return items.filter(it => {
|
|
208
208
|
const t = Date.parse(it.deleted_at || "");
|
|
209
209
|
return !isNaN(t) && t < cutoff;
|
|
210
|
-
})
|
|
210
|
+
});
|
|
211
211
|
},
|
|
212
212
|
|
|
213
213
|
async emptyFilesBulk(daysOld) {
|
|
@@ -223,10 +223,13 @@ const TrashStore = (() => {
|
|
|
223
223
|
}
|
|
224
224
|
},
|
|
225
225
|
|
|
226
|
-
async
|
|
227
|
-
const url = "/api/trash/sessions?" + new URLSearchParams({ days_old: String(daysOld) }).toString();
|
|
226
|
+
async deleteSessions(sessions) {
|
|
228
227
|
try {
|
|
229
|
-
const res = await fetch(
|
|
228
|
+
const res = await fetch("/api/trash/sessions/delete", {
|
|
229
|
+
method: "POST",
|
|
230
|
+
headers: { "Content-Type": "application/json" },
|
|
231
|
+
body: JSON.stringify({ session_ids: sessions.map(s => s.session_id) })
|
|
232
|
+
});
|
|
230
233
|
const data = await res.json();
|
|
231
234
|
if (!res.ok || !data.ok) return { ok: false, error: data.error || res.statusText };
|
|
232
235
|
await Trash.loadSessions();
|
|
@@ -49,6 +49,46 @@ const TrashView = (() => {
|
|
|
49
49
|
return d.toLocaleDateString();
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
+
// ── Source filter select helpers ─────────────────────────────────────
|
|
53
|
+
|
|
54
|
+
// "All types / Regular / Scheduled / Channel" — the archive page filters
|
|
55
|
+
// archived sessions by the same four sources, so this page mirrors it.
|
|
56
|
+
function _buildSourceOptions() {
|
|
57
|
+
const wrap = $("trash-filter-source");
|
|
58
|
+
if (!wrap) return;
|
|
59
|
+
const dropdown = wrap.querySelector(".custom-select-dropdown");
|
|
60
|
+
if (!dropdown || dropdown.dataset.built === "1") return;
|
|
61
|
+
|
|
62
|
+
const opts = [
|
|
63
|
+
{ value: "", label: _t("trash.filterAllTypes") },
|
|
64
|
+
{ value: Trash.SOURCES.MANUAL, label: _t("trash.filterManual") },
|
|
65
|
+
{ value: Trash.SOURCES.CRON, label: _t("trash.filterCron") },
|
|
66
|
+
{ value: Trash.SOURCES.CHANNEL, label: _t("trash.filterChannel") },
|
|
67
|
+
];
|
|
68
|
+
dropdown.innerHTML = "";
|
|
69
|
+
opts.forEach(o => {
|
|
70
|
+
const el = document.createElement("div");
|
|
71
|
+
el.className = "custom-select-option";
|
|
72
|
+
el.dataset.value = o.value;
|
|
73
|
+
el.dataset.label = o.label;
|
|
74
|
+
el.textContent = o.label;
|
|
75
|
+
dropdown.appendChild(el);
|
|
76
|
+
});
|
|
77
|
+
dropdown.dataset.built = "1";
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// The empty option is "every source", so the trigger keeps the placeholder
|
|
81
|
+
// styling the archive filters show until a real filter is picked.
|
|
82
|
+
function _syncSourceSelect() {
|
|
83
|
+
const wrap = $("trash-filter-source");
|
|
84
|
+
if (!wrap) return;
|
|
85
|
+
const dropdown = wrap.querySelector(".custom-select-dropdown");
|
|
86
|
+
const ctl = dropdown && dropdown.__customSelect;
|
|
87
|
+
if (!ctl) return;
|
|
88
|
+
const current = Trash.state.filterSource;
|
|
89
|
+
ctl.setValue(current === Trash.SOURCES.ALL ? "" : current);
|
|
90
|
+
}
|
|
91
|
+
|
|
52
92
|
// ── Project filter select helpers ────────────────────────────────────
|
|
53
93
|
|
|
54
94
|
function _refreshProjectOptions() {
|
|
@@ -386,18 +426,18 @@ const TrashView = (() => {
|
|
|
386
426
|
async function _emptyBulk(daysOld, confirmKey) {
|
|
387
427
|
const isSession = _activeTab === "session-trash";
|
|
388
428
|
const items = isSession ? Trash.state.sessions : Trash.state.files;
|
|
389
|
-
const
|
|
429
|
+
const matches = Trash.matching(items, daysOld);
|
|
390
430
|
|
|
391
|
-
if (
|
|
431
|
+
if (matches.length === 0) {
|
|
392
432
|
Modal.toast(_t(daysOld > 0 ? "trash.nothingOld" : "trash.empty"), "info");
|
|
393
433
|
return;
|
|
394
434
|
}
|
|
395
435
|
|
|
396
|
-
const confirmed = await Modal.confirm(I18n.t(confirmKey, { count:
|
|
436
|
+
const confirmed = await Modal.confirm(I18n.t(confirmKey, { count: matches.length }));
|
|
397
437
|
if (!confirmed) return;
|
|
398
438
|
|
|
399
439
|
if (isSession) {
|
|
400
|
-
const res = await Trash.
|
|
440
|
+
const res = await Trash.deleteSessions(matches);
|
|
401
441
|
if (!res.ok) { Modal.toast(I18n.t("trash.cleanFail", { msg: res.error }), "error"); return; }
|
|
402
442
|
Modal.toast(I18n.t("trash.sessionsCleaned", { count: res.deleted_count }), "success");
|
|
403
443
|
} else {
|
|
@@ -459,14 +499,21 @@ const TrashView = (() => {
|
|
|
459
499
|
() => _emptyBulk(0, _activeTab === "session-trash"
|
|
460
500
|
? "trash.confirmEmptySessionAll" : "trash.confirmEmptyAll"));
|
|
461
501
|
|
|
462
|
-
// Filter
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
502
|
+
// Filter selects — source (all / regular / scheduled) and project
|
|
503
|
+
_buildSourceOptions();
|
|
504
|
+
|
|
505
|
+
const sourceWrap = $("trash-filter-source");
|
|
506
|
+
if (sourceWrap && window.CustomSelect) {
|
|
507
|
+
const dropdown = sourceWrap.querySelector(".custom-select-dropdown");
|
|
508
|
+
if (!dropdown.__customSelect) {
|
|
509
|
+
window.CustomSelect.init({
|
|
510
|
+
trigger: sourceWrap.querySelector(".custom-select-trigger"),
|
|
511
|
+
dropdown: dropdown,
|
|
512
|
+
onSelect: (value) => Trash.setFilterSource(value || Trash.SOURCES.ALL)
|
|
513
|
+
});
|
|
514
|
+
}
|
|
515
|
+
_syncSourceSelect();
|
|
516
|
+
}
|
|
470
517
|
|
|
471
518
|
// Project filter select
|
|
472
519
|
const projWrap = $("trash-filter-project");
|
|
@@ -501,7 +548,13 @@ const TrashView = (() => {
|
|
|
501
548
|
onPanelShow() {
|
|
502
549
|
_wire();
|
|
503
550
|
_activeTab = null;
|
|
504
|
-
|
|
551
|
+
// Sessions are what people come here for; the file trash is the side trip.
|
|
552
|
+
_switchTab("session-trash");
|
|
553
|
+
},
|
|
554
|
+
|
|
555
|
+
// Sidebar deletes land in the session trash while this page may be open behind them.
|
|
556
|
+
refreshIfShown() {
|
|
557
|
+
if (Router.current === "trash" && _activeTab === "session-trash") Trash.loadSessions();
|
|
505
558
|
}
|
|
506
559
|
};
|
|
507
560
|
|