openclacky 1.2.5 → 1.2.7
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 +43 -0
- data/README.md +34 -0
- data/README_CN.md +34 -0
- data/lib/clacky/agent/cost_tracker.rb +24 -10
- data/lib/clacky/agent/llm_caller.rb +25 -3
- data/lib/clacky/agent/message_compressor.rb +2 -1
- data/lib/clacky/agent/message_compressor_helper.rb +6 -2
- data/lib/clacky/agent/session_serializer.rb +23 -4
- data/lib/clacky/agent/tool_executor.rb +14 -0
- data/lib/clacky/agent/tool_registry.rb +0 -7
- data/lib/clacky/agent.rb +43 -10
- data/lib/clacky/agent_config.rb +54 -6
- data/lib/clacky/billing/billing_store.rb +62 -4
- data/lib/clacky/brand_config.rb +5 -0
- data/lib/clacky/cli.rb +76 -24
- data/lib/clacky/client.rb +59 -4
- data/lib/clacky/default_parsers/wps_parser.rb +82 -0
- data/lib/clacky/default_skills/onboard/SKILL.md +2 -2
- data/lib/clacky/json_ui_controller.rb +5 -2
- data/lib/clacky/message_format/anthropic.rb +13 -3
- data/lib/clacky/message_format/bedrock.rb +2 -2
- data/lib/clacky/plain_ui_controller.rb +1 -1
- data/lib/clacky/platform_http_client.rb +28 -1
- data/lib/clacky/providers.rb +11 -29
- data/lib/clacky/server/channel/channel_manager.rb +148 -12
- data/lib/clacky/server/channel/channel_ui_controller.rb +4 -2
- data/lib/clacky/server/http_server.rb +133 -13
- data/lib/clacky/server/session_registry.rb +30 -4
- data/lib/clacky/server/web_ui_controller.rb +6 -3
- data/lib/clacky/tools/browser.rb +4 -13
- data/lib/clacky/tools/terminal.rb +23 -27
- data/lib/clacky/ui2/ui_controller.rb +1 -1
- data/lib/clacky/ui_interface.rb +1 -1
- data/lib/clacky/utils/file_processor.rb +3 -0
- data/lib/clacky/utils/parser_manager.rb +3 -0
- data/lib/clacky/version.rb +1 -1
- data/lib/clacky/web/app.css +659 -75
- data/lib/clacky/web/app.js +0 -1
- data/lib/clacky/web/billing.js +371 -99
- data/lib/clacky/web/i18n.js +48 -2
- data/lib/clacky/web/index.html +34 -1
- data/lib/clacky/web/sessions.js +213 -82
- data/lib/clacky/web/settings.js +59 -17
- data/lib/clacky/web/workspace.js +204 -0
- data/lib/clacky/web/ws-dispatcher.js +19 -3
- data/lib/clacky.rb +9 -3
- metadata +4 -5
- data/lib/clacky/tools/list_tasks.rb +0 -54
- data/lib/clacky/tools/redo_task.rb +0 -41
- data/lib/clacky/tools/undo_task.rb +0 -35
data/lib/clacky/web/i18n.js
CHANGED
|
@@ -81,6 +81,14 @@ const I18n = (() => {
|
|
|
81
81
|
"sessions.actions.downloadHint": "for debugging",
|
|
82
82
|
"sib.dir.tooltip": "Click to change directory",
|
|
83
83
|
"sib.dir.changePrompt": "Change working directory:",
|
|
84
|
+
"workspace.title": "Workspace",
|
|
85
|
+
"workspace.expand": "Open workspace",
|
|
86
|
+
"workspace.collapse": "Collapse workspace",
|
|
87
|
+
"workspace.refresh": "Refresh",
|
|
88
|
+
"workspace.empty": "This folder is empty",
|
|
89
|
+
"workspace.loading": "Loading…",
|
|
90
|
+
"workspace.error": "Failed to load files",
|
|
91
|
+
"workspace.downloadFailed": "Download failed",
|
|
84
92
|
"sib.model.tooltip": "Click to switch model",
|
|
85
93
|
"sib.model.tooltip.busy": "Model switching is disabled while the agent is responding",
|
|
86
94
|
"sib.signal.tooltip": "Recent LLM latency",
|
|
@@ -507,6 +515,7 @@ const I18n = (() => {
|
|
|
507
515
|
"settings.models.btn.saved": "Saved ✓",
|
|
508
516
|
"settings.models.btn.testing": "Testing…",
|
|
509
517
|
"settings.models.btn.test": "Test",
|
|
518
|
+
"settings.models.link.topUp": "Top up / Manage account",
|
|
510
519
|
"settings.models.btn.edit": "Edit",
|
|
511
520
|
"settings.models.btn.delete": "Delete",
|
|
512
521
|
"settings.models.btn.cancel": "Cancel",
|
|
@@ -664,12 +673,26 @@ const I18n = (() => {
|
|
|
664
673
|
"billing.byModel": "By Model",
|
|
665
674
|
"billing.model": "Model",
|
|
666
675
|
"billing.cost": "Cost",
|
|
667
|
-
"billing.dailyUsage": "
|
|
676
|
+
"billing.dailyUsage": "Usage Details",
|
|
668
677
|
"billing.period.day": "Today",
|
|
669
678
|
"billing.period.week": "This Week",
|
|
670
679
|
"billing.period.month": "This Month",
|
|
671
680
|
"billing.period.year": "This Year",
|
|
672
681
|
"billing.period.all": "All Time",
|
|
682
|
+
"billing.clearData": "Clear Data",
|
|
683
|
+
"billing.clearToday": "Clear Today",
|
|
684
|
+
"billing.clearAll": "Clear All",
|
|
685
|
+
"billing.allModels": "All Models",
|
|
686
|
+
"billing.cacheHit": "Cache Hit",
|
|
687
|
+
"billing.inputCacheHit": "Input (Cache Hit)",
|
|
688
|
+
"billing.inputCacheMiss": "Input (Cache Miss)",
|
|
689
|
+
"billing.output": "Output",
|
|
690
|
+
"billing.tokenUsage": "Token Usage",
|
|
691
|
+
"billing.costTrend": "Cost Trend",
|
|
692
|
+
"billing.noData": "No data available",
|
|
693
|
+
|
|
694
|
+
"error.insufficient_credit": "Insufficient LLM credit. Please top up your account to continue.",
|
|
695
|
+
"error.insufficient_credit.action": "Top up",
|
|
673
696
|
},
|
|
674
697
|
|
|
675
698
|
zh: {
|
|
@@ -739,6 +762,14 @@ const I18n = (() => {
|
|
|
739
762
|
"sessions.actions.downloadHint": "用于调试",
|
|
740
763
|
"sib.dir.tooltip": "点击切换工作目录",
|
|
741
764
|
"sib.dir.changePrompt": "切换工作目录:",
|
|
765
|
+
"workspace.title": "工作区",
|
|
766
|
+
"workspace.expand": "打开工作区",
|
|
767
|
+
"workspace.collapse": "收起工作区",
|
|
768
|
+
"workspace.refresh": "刷新",
|
|
769
|
+
"workspace.empty": "此文件夹为空",
|
|
770
|
+
"workspace.loading": "加载中…",
|
|
771
|
+
"workspace.error": "加载文件失败",
|
|
772
|
+
"workspace.downloadFailed": "下载失败",
|
|
742
773
|
"sib.model.tooltip": "点击切换模型",
|
|
743
774
|
"sib.model.tooltip.busy": "Agent 回复中,暂时无法切换模型",
|
|
744
775
|
"sib.signal.tooltip": "最近一次 LLM 响应延迟",
|
|
@@ -1164,6 +1195,7 @@ const I18n = (() => {
|
|
|
1164
1195
|
"settings.models.btn.saved": "已保存 ✓",
|
|
1165
1196
|
"settings.models.btn.testing": "测试中…",
|
|
1166
1197
|
"settings.models.btn.test": "测试",
|
|
1198
|
+
"settings.models.link.topUp": "充值 / 账户管理",
|
|
1167
1199
|
"settings.models.btn.edit": "编辑",
|
|
1168
1200
|
"settings.models.btn.delete": "删除",
|
|
1169
1201
|
"settings.models.btn.cancel": "取消",
|
|
@@ -1321,12 +1353,26 @@ const I18n = (() => {
|
|
|
1321
1353
|
"billing.byModel": "按模型",
|
|
1322
1354
|
"billing.model": "模型",
|
|
1323
1355
|
"billing.cost": "费用",
|
|
1324
|
-
"billing.dailyUsage": "
|
|
1356
|
+
"billing.dailyUsage": "使用详情",
|
|
1325
1357
|
"billing.period.day": "今日",
|
|
1326
1358
|
"billing.period.week": "本周",
|
|
1327
1359
|
"billing.period.month": "本月",
|
|
1328
1360
|
"billing.period.year": "今年",
|
|
1329
1361
|
"billing.period.all": "全部",
|
|
1362
|
+
"billing.clearData": "清除数据",
|
|
1363
|
+
"billing.clearToday": "清除今日",
|
|
1364
|
+
"billing.clearAll": "全部清除",
|
|
1365
|
+
"billing.allModels": "所有模型",
|
|
1366
|
+
"billing.cacheHit": "缓存命中",
|
|
1367
|
+
"billing.inputCacheHit": "输入(命中缓存)",
|
|
1368
|
+
"billing.inputCacheMiss": "输入(未命中缓存)",
|
|
1369
|
+
"billing.output": "输出",
|
|
1370
|
+
"billing.tokenUsage": "Token 用量",
|
|
1371
|
+
"billing.costTrend": "费用趋势",
|
|
1372
|
+
"billing.noData": "暂无数据",
|
|
1373
|
+
|
|
1374
|
+
"error.insufficient_credit": "LLM(大模型)余额不足,请充值后继续使用。",
|
|
1375
|
+
"error.insufficient_credit.action": "去充值",
|
|
1330
1376
|
}
|
|
1331
1377
|
};
|
|
1332
1378
|
|
data/lib/clacky/web/index.html
CHANGED
|
@@ -313,6 +313,8 @@
|
|
|
313
313
|
<!-- Chat panel -->
|
|
314
314
|
<div id="chat-panel" style="display:none">
|
|
315
315
|
|
|
316
|
+
<!-- Chat column (messages + info bar + input) -->
|
|
317
|
+
<div id="chat-main">
|
|
316
318
|
<div id="messages"></div>
|
|
317
319
|
<!-- New message notification banner -->
|
|
318
320
|
<div id="new-message-banner" class="new-message-banner" style="display:none">
|
|
@@ -334,6 +336,7 @@
|
|
|
334
336
|
<span id="sib-model-wrap">
|
|
335
337
|
<span id="sib-model" class="sib-model-clickable" title="Click to switch model"></span>
|
|
336
338
|
<div id="sib-model-dropdown" class="sib-model-dropdown" style="display:none"></div>
|
|
339
|
+
<div id="sib-submodel-panel" class="sib-submodel-panel" style="display:none"></div>
|
|
337
340
|
</span>
|
|
338
341
|
<span class="sib-sep sib-sep-after-model">│</span>
|
|
339
342
|
<span id="sib-reasoning-wrap">
|
|
@@ -381,7 +384,7 @@
|
|
|
381
384
|
<div id="image-preview-strip" style="display:none"></div>
|
|
382
385
|
<div id="input-bar">
|
|
383
386
|
<!-- Hidden file picker -->
|
|
384
|
-
<input type="file" id="image-file-input" accept="image/png,image/jpeg,image/gif,image/webp
|
|
387
|
+
<input type="file" id="image-file-input" accept="image/png,image/jpeg,image/gif,image/webp,*/*" multiple style="display:none">
|
|
385
388
|
<button id="btn-attach" title="Attach file (image, pdf, docx, md, tar.gz…) — drag & drop / Ctrl+V also work">
|
|
386
389
|
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
387
390
|
<path d="M21.44 11.05l-9.19 9.19a6 6 0 0 1-8.49-8.49l9.19-9.19a4 4 0 0 1 5.66 5.66L9.41 17.41a2 2 0 0 1-2.83-2.83l8.49-8.48"/>
|
|
@@ -397,6 +400,35 @@
|
|
|
397
400
|
<button id="btn-interrupt" style="display:none" title="Stop"></button>
|
|
398
401
|
</div>
|
|
399
402
|
</div>
|
|
403
|
+
</div><!-- /#chat-main -->
|
|
404
|
+
|
|
405
|
+
<!-- ── WORKSPACE PANEL (right) ──────────────────────────────────── -->
|
|
406
|
+
<aside id="workspace-panel" class="collapsed">
|
|
407
|
+
<div id="workspace-header">
|
|
408
|
+
<span id="workspace-title" data-i18n="workspace.title">Workspace</span>
|
|
409
|
+
<div class="workspace-header-actions">
|
|
410
|
+
<button id="btn-workspace-refresh" class="workspace-icon-btn" data-i18n-title="workspace.refresh" title="Refresh">
|
|
411
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
412
|
+
<path d="M23 4v6h-6"/><path d="M1 20v-6h6"/>
|
|
413
|
+
<path d="M3.51 9a9 9 0 0 1 14.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0 0 20.49 15"/>
|
|
414
|
+
</svg>
|
|
415
|
+
</button>
|
|
416
|
+
<button id="btn-workspace-close" class="workspace-icon-btn" data-i18n-title="workspace.collapse" title="Collapse">
|
|
417
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
418
|
+
<path d="M9 18l6-6-6-6"/>
|
|
419
|
+
</svg>
|
|
420
|
+
</button>
|
|
421
|
+
</div>
|
|
422
|
+
</div>
|
|
423
|
+
<div id="workspace-tree" role="tree"></div>
|
|
424
|
+
</aside>
|
|
425
|
+
|
|
426
|
+
<!-- Collapsed-state opener tab -->
|
|
427
|
+
<button id="btn-workspace-open" data-i18n-title="workspace.expand" title="Open workspace" style="display:none">
|
|
428
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
429
|
+
<path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"/>
|
|
430
|
+
</svg>
|
|
431
|
+
</button>
|
|
400
432
|
</div>
|
|
401
433
|
|
|
402
434
|
<!-- ── CREATOR PANEL ─────────────────────────────────────────────── -->
|
|
@@ -1250,6 +1282,7 @@
|
|
|
1250
1282
|
<script src="/ws.js"></script>
|
|
1251
1283
|
<script src="/ws-dispatcher.js"></script>
|
|
1252
1284
|
<script src="/sessions.js"></script>
|
|
1285
|
+
<script src="/workspace.js"></script>
|
|
1253
1286
|
<script src="/datepicker.js"></script>
|
|
1254
1287
|
<script src="/tasks.js"></script>
|
|
1255
1288
|
<script src="/skills.js"></script>
|
data/lib/clacky/web/sessions.js
CHANGED
|
@@ -492,57 +492,12 @@ const Sessions = (() => {
|
|
|
492
492
|
const MAX_IMAGE_LONG_EDGE = 1920; // px — scale down if larger
|
|
493
493
|
const MAX_FILE_BYTES = 32 * 1024 * 1024; // 32 MB
|
|
494
494
|
const ACCEPTED_IMAGE_TYPES = ["image/png", "image/jpeg", "image/gif", "image/webp"];
|
|
495
|
-
const ACCEPTED_DOC_TYPES = [
|
|
496
|
-
"application/pdf",
|
|
497
|
-
"application/zip",
|
|
498
|
-
"application/x-zip-compressed",
|
|
499
|
-
"application/gzip",
|
|
500
|
-
"application/x-gzip",
|
|
501
|
-
"application/x-tar",
|
|
502
|
-
"application/x-compressed-tar",
|
|
503
|
-
"application/vnd.openxmlformats-officedocument.wordprocessingml.document", // .docx
|
|
504
|
-
"application/msword", // .doc
|
|
505
|
-
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", // .xlsx
|
|
506
|
-
"application/vnd.ms-excel", // .xls
|
|
507
|
-
"application/vnd.openxmlformats-officedocument.presentationml.presentation", // .pptx
|
|
508
|
-
"application/vnd.ms-powerpoint", // .ppt
|
|
509
|
-
"text/csv", // .csv
|
|
510
|
-
"application/csv", // .csv (some browsers)
|
|
511
|
-
"text/markdown", // .md
|
|
512
|
-
"text/x-markdown", // .md (some browsers)
|
|
513
|
-
"text/plain", // .md / .txt (many browsers report this)
|
|
514
|
-
];
|
|
515
|
-
|
|
516
|
-
// Extension-based fallback for files whose MIME type is missing or unreliable.
|
|
517
|
-
// Browsers frequently report "" or "application/octet-stream" for .md / .tar.gz.
|
|
518
|
-
const ACCEPTED_DOC_EXTENSIONS = [
|
|
519
|
-
".pdf", ".zip",
|
|
520
|
-
".doc", ".docx", ".xls", ".xlsx", ".ppt", ".pptx",
|
|
521
|
-
".csv",
|
|
522
|
-
".md", ".markdown", ".txt", ".log",
|
|
523
|
-
".tar", ".gz", ".tgz", ".tar.gz", ".rar", ".7z"
|
|
524
|
-
];
|
|
525
|
-
|
|
526
|
-
function _hasAcceptedDocExt(filename) {
|
|
527
|
-
const lower = (filename || "").toLowerCase();
|
|
528
|
-
return ACCEPTED_DOC_EXTENSIONS.some(ext => lower.endsWith(ext));
|
|
529
|
-
}
|
|
530
|
-
|
|
531
|
-
function _isAcceptedDoc(file) {
|
|
532
|
-
if (!file) return false;
|
|
533
|
-
if (file.type && ACCEPTED_DOC_TYPES.includes(file.type)) return true;
|
|
534
|
-
return _hasAcceptedDocExt(file.name);
|
|
535
|
-
}
|
|
536
495
|
|
|
537
496
|
function _isAcceptedImage(file) {
|
|
538
497
|
if (!file) return false;
|
|
539
498
|
return ACCEPTED_IMAGE_TYPES.includes(file.type);
|
|
540
499
|
}
|
|
541
500
|
|
|
542
|
-
function _isAcceptedFile(file) {
|
|
543
|
-
return _isAcceptedImage(file) || _isAcceptedDoc(file);
|
|
544
|
-
}
|
|
545
|
-
|
|
546
501
|
function _docTypeIcon(mimeType, filename) {
|
|
547
502
|
const lower = (filename || "").toLowerCase();
|
|
548
503
|
if (mimeType === "application/pdf" || lower.endsWith(".pdf")) return "📄";
|
|
@@ -552,11 +507,11 @@ const Sessions = (() => {
|
|
|
552
507
|
lower.endsWith(".tar") || lower.endsWith(".gz") || lower.endsWith(".tgz") || lower.endsWith(".tar.gz") ||
|
|
553
508
|
lower.endsWith(".rar") || lower.endsWith(".7z")) return "🗜️";
|
|
554
509
|
if ((mimeType && mimeType.includes("wordprocessingml")) || mimeType === "application/msword" ||
|
|
555
|
-
lower.endsWith(".doc") || lower.endsWith(".docx")) return "📝";
|
|
510
|
+
lower.endsWith(".doc") || lower.endsWith(".docx") || lower.endsWith(".wps")) return "📝";
|
|
556
511
|
if ((mimeType && mimeType.includes("spreadsheetml")) || mimeType === "application/vnd.ms-excel" ||
|
|
557
|
-
lower.endsWith(".xls") || lower.endsWith(".xlsx")) return "📊";
|
|
512
|
+
lower.endsWith(".xls") || lower.endsWith(".xlsx") || lower.endsWith(".et")) return "📊";
|
|
558
513
|
if ((mimeType && mimeType.includes("presentationml")) || mimeType === "application/vnd.ms-powerpoint" ||
|
|
559
|
-
lower.endsWith(".ppt") || lower.endsWith(".pptx")) return "📋";
|
|
514
|
+
lower.endsWith(".ppt") || lower.endsWith(".pptx") || lower.endsWith(".dps")) return "📋";
|
|
560
515
|
if (mimeType === "text/csv" || mimeType === "application/csv" || lower.endsWith(".csv")) return "📊";
|
|
561
516
|
if (mimeType === "text/markdown" || mimeType === "text/x-markdown" ||
|
|
562
517
|
lower.endsWith(".md") || lower.endsWith(".markdown")) return "📝";
|
|
@@ -649,16 +604,10 @@ const Sessions = (() => {
|
|
|
649
604
|
}
|
|
650
605
|
|
|
651
606
|
function _addAttachmentFile(file) {
|
|
652
|
-
// Route by content category. Images must match known image MIME types
|
|
653
|
-
// (MIME is reliable for images). Documents fall back to extension-based
|
|
654
|
-
// detection because browsers frequently report "" or "application/octet-stream"
|
|
655
|
-
// for .md / .tar.gz files.
|
|
656
607
|
if (_isAcceptedImage(file)) {
|
|
657
608
|
_addImageFile(file);
|
|
658
|
-
} else if (_isAcceptedDoc(file)) {
|
|
659
|
-
_addGenericFile(file);
|
|
660
609
|
} else {
|
|
661
|
-
|
|
610
|
+
_addGenericFile(file);
|
|
662
611
|
}
|
|
663
612
|
}
|
|
664
613
|
|
|
@@ -848,27 +797,20 @@ const Sessions = (() => {
|
|
|
848
797
|
inputArea.addEventListener("drop", (e) => {
|
|
849
798
|
e.preventDefault();
|
|
850
799
|
inputArea.classList.remove("drag-over");
|
|
851
|
-
const files = Array.from(e.dataTransfer.files)
|
|
800
|
+
const files = Array.from(e.dataTransfer.files);
|
|
852
801
|
if (files.length === 0) return;
|
|
853
802
|
files.forEach(_addAttachmentFile);
|
|
854
803
|
});
|
|
855
804
|
|
|
856
|
-
// Paste handler — images and accepted docs from the clipboard.
|
|
857
805
|
document.getElementById("user-input").addEventListener("paste", (e) => {
|
|
858
806
|
const items = Array.from(e.clipboardData?.items || []);
|
|
859
|
-
|
|
860
|
-
// passes our doc filter. Must check name via getAsFile() for .md/.tar.gz
|
|
861
|
-
// (browsers often leave item.type empty for these).
|
|
862
|
-
const attachItems = items.filter(it => {
|
|
863
|
-
if (it.kind !== "file") return false;
|
|
864
|
-
if (ACCEPTED_IMAGE_TYPES.includes(it.type)) return true;
|
|
865
|
-
if (ACCEPTED_DOC_TYPES.includes(it.type)) return true;
|
|
866
|
-
const f = it.getAsFile && it.getAsFile();
|
|
867
|
-
return f ? _hasAcceptedDocExt(f.name) : false;
|
|
868
|
-
});
|
|
807
|
+
const attachItems = items.filter(it => it.kind === "file");
|
|
869
808
|
if (attachItems.length === 0) return;
|
|
870
809
|
e.preventDefault();
|
|
871
|
-
attachItems.forEach(it =>
|
|
810
|
+
attachItems.forEach(it => {
|
|
811
|
+
const f = it.getAsFile && it.getAsFile();
|
|
812
|
+
if (f) _addAttachmentFile(f);
|
|
813
|
+
});
|
|
872
814
|
});
|
|
873
815
|
}
|
|
874
816
|
|
|
@@ -1454,8 +1396,15 @@ const Sessions = (() => {
|
|
|
1454
1396
|
// showProgress() with the authoritative started_at, which is the
|
|
1455
1397
|
// single source of truth for first-visit sessions (no cached state).
|
|
1456
1398
|
} else if (session.status === "error" && session.error) {
|
|
1457
|
-
|
|
1458
|
-
|
|
1399
|
+
if (window.renderErrorEvent) {
|
|
1400
|
+
window.renderErrorEvent({
|
|
1401
|
+
code: session.error_code,
|
|
1402
|
+
message: session.error,
|
|
1403
|
+
top_up_url: session.top_up_url,
|
|
1404
|
+
});
|
|
1405
|
+
} else {
|
|
1406
|
+
Sessions.appendMsg("error", session.error);
|
|
1407
|
+
}
|
|
1459
1408
|
}
|
|
1460
1409
|
}
|
|
1461
1410
|
}
|
|
@@ -2393,6 +2342,7 @@ const Sessions = (() => {
|
|
|
2393
2342
|
/** Update the session info bar below the chat header with current session metadata. */
|
|
2394
2343
|
updateInfoBar(s) {
|
|
2395
2344
|
this._lastSession = s;
|
|
2345
|
+
if (window.Workspace) Workspace.onSession(s);
|
|
2396
2346
|
if (!s) {
|
|
2397
2347
|
// Hide all spans when no session
|
|
2398
2348
|
["sib-id", "sib-status", "sib-dir", "sib-mode", "sib-model", "sib-reasoning", "sib-tasks", "sib-cost"].forEach(id => {
|
|
@@ -2451,15 +2401,21 @@ const Sessions = (() => {
|
|
|
2451
2401
|
const sibModelWrap = $("sib-model-wrap");
|
|
2452
2402
|
const sibModel = $("sib-model");
|
|
2453
2403
|
if (sibModel) {
|
|
2454
|
-
|
|
2455
|
-
|
|
2404
|
+
const subModel = s.sub_model;
|
|
2405
|
+
const cardModel = s.card_model;
|
|
2406
|
+
const display = subModel
|
|
2407
|
+
? `${subModel}`
|
|
2408
|
+
: (s.model || "");
|
|
2409
|
+
sibModel.textContent = display;
|
|
2456
2410
|
sibModel.dataset.sessionId = s.id;
|
|
2457
2411
|
if (s.model_id) {
|
|
2458
2412
|
sibModel.dataset.modelId = s.model_id;
|
|
2459
2413
|
} else {
|
|
2460
2414
|
delete sibModel.dataset.modelId;
|
|
2461
2415
|
}
|
|
2462
|
-
|
|
2416
|
+
if (cardModel) sibModel.dataset.cardModel = cardModel; else delete sibModel.dataset.cardModel;
|
|
2417
|
+
if (subModel) sibModel.dataset.subModel = subModel; else delete sibModel.dataset.subModel;
|
|
2418
|
+
sibModel.dataset.subModelOptions = JSON.stringify(s.sub_model_options || []);
|
|
2463
2419
|
const busy = s.status === "running";
|
|
2464
2420
|
sibModel.classList.toggle("sib-model-disabled", busy);
|
|
2465
2421
|
sibModel.title = busy
|
|
@@ -3401,8 +3357,16 @@ const Sessions = (() => {
|
|
|
3401
3357
|
if (_isOpen) {
|
|
3402
3358
|
dropdown.style.display = "none";
|
|
3403
3359
|
_isOpen = false;
|
|
3360
|
+
_closeSubmodelPanel();
|
|
3404
3361
|
} else {
|
|
3405
|
-
|
|
3362
|
+
let subOptions = [];
|
|
3363
|
+
try { subOptions = JSON.parse(modelEl.dataset.subModelOptions || "[]"); } catch (_) {}
|
|
3364
|
+
const subInfo = {
|
|
3365
|
+
options: Array.isArray(subOptions) ? subOptions : [],
|
|
3366
|
+
current: modelEl.dataset.subModel || null,
|
|
3367
|
+
cardModel: modelEl.dataset.cardModel || null
|
|
3368
|
+
};
|
|
3369
|
+
await _populateModelDropdown(modelEl.dataset.sessionId, modelEl.dataset.modelId || null, subInfo);
|
|
3406
3370
|
|
|
3407
3371
|
// Calculate position relative to the model element (fixed positioning)
|
|
3408
3372
|
const rect = modelEl.getBoundingClientRect();
|
|
@@ -3417,15 +3381,17 @@ const Sessions = (() => {
|
|
|
3417
3381
|
}
|
|
3418
3382
|
|
|
3419
3383
|
// Close dropdown when clicking outside
|
|
3420
|
-
if (_isOpen && !e.target.closest(".sib-model-dropdown")) {
|
|
3384
|
+
if (_isOpen && !e.target.closest(".sib-model-dropdown") && !e.target.closest(".sib-submodel-panel")) {
|
|
3421
3385
|
const dropdown = $("sib-model-dropdown");
|
|
3422
3386
|
if (dropdown) dropdown.style.display = "none";
|
|
3423
3387
|
_isOpen = false;
|
|
3388
|
+
_closeSubmodelPanel();
|
|
3424
3389
|
}
|
|
3425
3390
|
});
|
|
3426
3391
|
|
|
3427
3392
|
// Populate dropdown with available models
|
|
3428
|
-
async function _populateModelDropdown(sessionId, currentModelId) {
|
|
3393
|
+
async function _populateModelDropdown(sessionId, currentModelId, subInfo) {
|
|
3394
|
+
subInfo = subInfo || { options: [], current: null, cardModel: null };
|
|
3429
3395
|
const dropdown = $("sib-model-dropdown");
|
|
3430
3396
|
if (!dropdown) return;
|
|
3431
3397
|
|
|
@@ -3489,6 +3455,13 @@ const Sessions = (() => {
|
|
|
3489
3455
|
nameLine.textContent = m.model;
|
|
3490
3456
|
left.appendChild(nameLine);
|
|
3491
3457
|
|
|
3458
|
+
if (m.id === currentModelId && subInfo.current && subInfo.current !== subInfo.cardModel) {
|
|
3459
|
+
const overrideLine = document.createElement("span");
|
|
3460
|
+
overrideLine.className = "sib-model-name-override";
|
|
3461
|
+
overrideLine.textContent = `→ ${subInfo.current}`;
|
|
3462
|
+
left.appendChild(overrideLine);
|
|
3463
|
+
}
|
|
3464
|
+
|
|
3492
3465
|
if (_nameCounts[m.model] > 1) {
|
|
3493
3466
|
left.classList.add("has-sub");
|
|
3494
3467
|
const host = (() => {
|
|
@@ -3516,17 +3489,36 @@ const Sessions = (() => {
|
|
|
3516
3489
|
right.appendChild(badge);
|
|
3517
3490
|
}
|
|
3518
3491
|
|
|
3519
|
-
// Latency cell — populated from _benchCache on open, updated live
|
|
3520
|
-
// when a benchmark run completes. Empty slot keeps row heights stable
|
|
3521
|
-
// so the list doesn't visually jump mid-benchmark.
|
|
3522
3492
|
const lat = document.createElement("span");
|
|
3523
3493
|
lat.className = "sib-model-latency";
|
|
3524
3494
|
_fillLatencyCell(lat, _benchCache[m.id]);
|
|
3525
3495
|
right.appendChild(lat);
|
|
3526
3496
|
|
|
3497
|
+
const hasSubModels =
|
|
3498
|
+
m.id === currentModelId &&
|
|
3499
|
+
subInfo.options &&
|
|
3500
|
+
subInfo.options.length > 1;
|
|
3501
|
+
|
|
3502
|
+
if (hasSubModels) {
|
|
3503
|
+
const toggleBtn = document.createElement("button");
|
|
3504
|
+
toggleBtn.type = "button";
|
|
3505
|
+
toggleBtn.className = "sib-submodel-toggle";
|
|
3506
|
+
toggleBtn.title = "Switch sub-model";
|
|
3507
|
+
toggleBtn.setAttribute("aria-expanded", "false");
|
|
3508
|
+
toggleBtn.innerHTML =
|
|
3509
|
+
'<svg viewBox="0 0 16 16" width="11" height="11" aria-hidden="true">' +
|
|
3510
|
+
'<path d="M6 3.5L10.5 8 6 12.5" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"/>' +
|
|
3511
|
+
'</svg>';
|
|
3512
|
+
right.appendChild(toggleBtn);
|
|
3513
|
+
|
|
3514
|
+
toggleBtn.addEventListener("click", (ev) => {
|
|
3515
|
+
ev.stopPropagation();
|
|
3516
|
+
_toggleSubmodelPanel(opt, toggleBtn, sessionId, subInfo);
|
|
3517
|
+
});
|
|
3518
|
+
}
|
|
3519
|
+
|
|
3527
3520
|
opt.appendChild(right);
|
|
3528
3521
|
|
|
3529
|
-
// Switch by id (stable across reorders/edits). Keep model name for UI update.
|
|
3530
3522
|
opt.addEventListener("click", () => _switchModel(sessionId, m.id, m.model));
|
|
3531
3523
|
dropdown.appendChild(opt);
|
|
3532
3524
|
});
|
|
@@ -3637,14 +3629,15 @@ const Sessions = (() => {
|
|
|
3637
3629
|
}
|
|
3638
3630
|
|
|
3639
3631
|
// Switch session model via API
|
|
3640
|
-
// modelId
|
|
3641
|
-
// modelName
|
|
3632
|
+
// Switch the session's current card. modelId is the stable runtime id,
|
|
3633
|
+
// modelName is for optimistic display.
|
|
3642
3634
|
async function _switchModel(sessionId, modelId, modelName) {
|
|
3643
3635
|
const dropdown = $("sib-model-dropdown");
|
|
3644
3636
|
if (dropdown) {
|
|
3645
3637
|
dropdown.style.display = "none";
|
|
3646
3638
|
_isOpen = false;
|
|
3647
3639
|
}
|
|
3640
|
+
_closeSubmodelPanel();
|
|
3648
3641
|
|
|
3649
3642
|
try {
|
|
3650
3643
|
const res = await fetch(`/api/sessions/${sessionId}/model`, {
|
|
@@ -3669,6 +3662,144 @@ const Sessions = (() => {
|
|
|
3669
3662
|
alert("Failed to switch model: " + e.message);
|
|
3670
3663
|
}
|
|
3671
3664
|
}
|
|
3665
|
+
|
|
3666
|
+
// Pin (or clear) the session's sub-model. Pass modelName=null to clear.
|
|
3667
|
+
// displayName is what we optimistically show in the status bar.
|
|
3668
|
+
async function _switchSubModel(sessionId, modelName, displayName) {
|
|
3669
|
+
const dropdown = $("sib-model-dropdown");
|
|
3670
|
+
if (dropdown) {
|
|
3671
|
+
dropdown.style.display = "none";
|
|
3672
|
+
_isOpen = false;
|
|
3673
|
+
}
|
|
3674
|
+
_closeSubmodelPanel();
|
|
3675
|
+
|
|
3676
|
+
try {
|
|
3677
|
+
const res = await fetch(`/api/sessions/${sessionId}/submodel`, {
|
|
3678
|
+
method: "PATCH",
|
|
3679
|
+
headers: { "Content-Type": "application/json" },
|
|
3680
|
+
body: JSON.stringify({ model_name: modelName })
|
|
3681
|
+
});
|
|
3682
|
+
const data = await res.json();
|
|
3683
|
+
if (!res.ok) throw new Error(data.error || "Unknown error");
|
|
3684
|
+
|
|
3685
|
+
const sibModel = $("sib-model");
|
|
3686
|
+
if (sibModel) {
|
|
3687
|
+
sibModel.textContent = displayName || "";
|
|
3688
|
+
sibModel.dataset.subModel = modelName || "";
|
|
3689
|
+
}
|
|
3690
|
+
} catch (e) {
|
|
3691
|
+
console.error("Failed to switch sub-model:", e);
|
|
3692
|
+
alert("Failed to switch sub-model: " + e.message);
|
|
3693
|
+
}
|
|
3694
|
+
}
|
|
3695
|
+
|
|
3696
|
+
let _activeSubmodelAnchor = null;
|
|
3697
|
+
|
|
3698
|
+
function _closeSubmodelPanel() {
|
|
3699
|
+
const panel = $("sib-submodel-panel");
|
|
3700
|
+
if (panel) panel.style.display = "none";
|
|
3701
|
+
if (_activeSubmodelAnchor) {
|
|
3702
|
+
const btn = _activeSubmodelAnchor.querySelector(".sib-submodel-toggle");
|
|
3703
|
+
if (btn) btn.setAttribute("aria-expanded", "false");
|
|
3704
|
+
_activeSubmodelAnchor.classList.remove("submodel-open");
|
|
3705
|
+
_activeSubmodelAnchor = null;
|
|
3706
|
+
}
|
|
3707
|
+
}
|
|
3708
|
+
|
|
3709
|
+
function _toggleSubmodelPanel(anchorRow, btn, sessionId, subInfo) {
|
|
3710
|
+
const panel = $("sib-submodel-panel");
|
|
3711
|
+
const dropdown = $("sib-model-dropdown");
|
|
3712
|
+
if (!panel || !dropdown) return;
|
|
3713
|
+
|
|
3714
|
+
if (panel.parentElement !== document.body) {
|
|
3715
|
+
document.body.appendChild(panel);
|
|
3716
|
+
}
|
|
3717
|
+
|
|
3718
|
+
const isOpen = panel.style.display !== "none" && _activeSubmodelAnchor === anchorRow;
|
|
3719
|
+
if (isOpen) {
|
|
3720
|
+
_closeSubmodelPanel();
|
|
3721
|
+
return;
|
|
3722
|
+
}
|
|
3723
|
+
|
|
3724
|
+
_renderSubmodelPanel(panel, sessionId, subInfo);
|
|
3725
|
+
|
|
3726
|
+
// Reset any prior position so measurements are accurate.
|
|
3727
|
+
panel.style.left = "0px";
|
|
3728
|
+
panel.style.top = "0px";
|
|
3729
|
+
panel.style.display = "block";
|
|
3730
|
+
panel.style.visibility = "hidden";
|
|
3731
|
+
|
|
3732
|
+
const dropRect = dropdown.getBoundingClientRect();
|
|
3733
|
+
const btnRect = btn.getBoundingClientRect();
|
|
3734
|
+
const panelRect = panel.getBoundingClientRect();
|
|
3735
|
+
const gap = 6;
|
|
3736
|
+
const margin = 8;
|
|
3737
|
+
const vw = window.innerWidth;
|
|
3738
|
+
const vh = window.innerHeight;
|
|
3739
|
+
|
|
3740
|
+
// Prefer right of dropdown; flip to left if we'd overflow viewport.
|
|
3741
|
+
let left = dropRect.right + gap;
|
|
3742
|
+
if (left + panelRect.width > vw - margin) {
|
|
3743
|
+
left = dropRect.left - panelRect.width - gap;
|
|
3744
|
+
}
|
|
3745
|
+
// If still off-screen on the left, clamp inside viewport.
|
|
3746
|
+
if (left < margin) left = margin;
|
|
3747
|
+
|
|
3748
|
+
// Vertically align to the chevron button, but clamp inside viewport.
|
|
3749
|
+
let top = btnRect.top - 6;
|
|
3750
|
+
if (top + panelRect.height > vh - margin) {
|
|
3751
|
+
top = vh - margin - panelRect.height;
|
|
3752
|
+
}
|
|
3753
|
+
if (top < margin) top = margin;
|
|
3754
|
+
|
|
3755
|
+
panel.style.left = `${left}px`;
|
|
3756
|
+
panel.style.top = `${top}px`;
|
|
3757
|
+
panel.style.visibility = "";
|
|
3758
|
+
|
|
3759
|
+
_activeSubmodelAnchor = anchorRow;
|
|
3760
|
+
anchorRow.classList.add("submodel-open");
|
|
3761
|
+
btn.setAttribute("aria-expanded", "true");
|
|
3762
|
+
}
|
|
3763
|
+
|
|
3764
|
+
function _renderSubmodelPanel(panel, sessionId, subInfo) {
|
|
3765
|
+
panel.innerHTML = "";
|
|
3766
|
+
|
|
3767
|
+
const header = document.createElement("div");
|
|
3768
|
+
header.className = "sib-submodel-panel-header";
|
|
3769
|
+
header.textContent = "Sub-model";
|
|
3770
|
+
panel.appendChild(header);
|
|
3771
|
+
|
|
3772
|
+
const cardDefault = subInfo.cardModel;
|
|
3773
|
+
subInfo.options.forEach(name => {
|
|
3774
|
+
const row = document.createElement("div");
|
|
3775
|
+
row.className = "sib-submodel-row";
|
|
3776
|
+
row.dataset.subModel = name;
|
|
3777
|
+
|
|
3778
|
+
const isActive = subInfo.current
|
|
3779
|
+
? name === subInfo.current
|
|
3780
|
+
: name === cardDefault;
|
|
3781
|
+
if (isActive) row.classList.add("current");
|
|
3782
|
+
|
|
3783
|
+
const nameEl = document.createElement("span");
|
|
3784
|
+
nameEl.className = "sib-submodel-row-name";
|
|
3785
|
+
nameEl.textContent = name;
|
|
3786
|
+
row.appendChild(nameEl);
|
|
3787
|
+
|
|
3788
|
+
if (name === cardDefault) {
|
|
3789
|
+
const tag = document.createElement("span");
|
|
3790
|
+
tag.className = "sib-submodel-default-tag";
|
|
3791
|
+
tag.textContent = "default";
|
|
3792
|
+
row.appendChild(tag);
|
|
3793
|
+
}
|
|
3794
|
+
|
|
3795
|
+
row.addEventListener("click", (ev) => {
|
|
3796
|
+
ev.stopPropagation();
|
|
3797
|
+
const passName = (name === cardDefault) ? null : name;
|
|
3798
|
+
_switchSubModel(sessionId, passName, name);
|
|
3799
|
+
});
|
|
3800
|
+
panel.appendChild(row);
|
|
3801
|
+
});
|
|
3802
|
+
}
|
|
3672
3803
|
})();
|
|
3673
3804
|
|
|
3674
3805
|
// ── Session Info Bar Working Directory Switcher ───────────────────────────
|