openclacky 0.9.7 → 0.9.9
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/.clacky/skills/gem-release/SKILL.md +16 -1
- data/CHANGELOG.md +51 -0
- data/docs/browser-cdp-native-design.md +195 -0
- data/docs/session-management-redesign.md +202 -0
- data/docs/system-skill-authoring-guide.md +47 -0
- data/lib/clacky/agent/cost_tracker.rb +2 -1
- data/lib/clacky/agent/message_compressor_helper.rb +2 -2
- data/lib/clacky/agent/session_serializer.rb +36 -3
- data/lib/clacky/agent/skill_manager.rb +68 -5
- data/lib/clacky/agent/system_prompt_builder.rb +30 -13
- data/lib/clacky/agent/tool_executor.rb +8 -12
- data/lib/clacky/agent.rb +172 -30
- data/lib/clacky/brand_config.rb +90 -0
- data/lib/clacky/cli.rb +37 -8
- data/lib/clacky/default_parsers/doc_parser.rb +69 -0
- data/lib/clacky/default_parsers/docx_parser.rb +172 -0
- data/lib/clacky/default_parsers/pdf_parser.rb +79 -0
- data/lib/clacky/default_parsers/pptx_parser.rb +140 -0
- data/lib/clacky/default_parsers/xlsx_parser.rb +121 -0
- data/lib/clacky/default_skills/browser-setup/SKILL.md +158 -0
- data/lib/clacky/default_skills/channel-setup/SKILL.md +139 -42
- data/lib/clacky/default_skills/channel-setup/feishu_setup.rb +582 -0
- data/lib/clacky/default_skills/channel-setup/weixin_setup.rb +274 -0
- data/lib/clacky/default_skills/onboard/SKILL.md +132 -6
- data/lib/clacky/default_skills/personal-website/SKILL.md +113 -0
- data/lib/clacky/default_skills/personal-website/publish.rb +214 -0
- data/lib/clacky/default_skills/skill-add/SKILL.md +6 -7
- data/lib/clacky/default_skills/skill-add/scripts/install_from_zip.rb +1 -1
- data/lib/clacky/default_skills/skill-creator/SKILL.md +3 -3
- data/lib/clacky/json_ui_controller.rb +5 -0
- data/lib/clacky/message_format/open_ai.rb +7 -1
- data/lib/clacky/plain_ui_controller.rb +5 -0
- data/lib/clacky/server/browser_manager.rb +308 -0
- data/lib/clacky/server/channel/adapters/feishu/adapter.rb +80 -15
- data/lib/clacky/server/channel/adapters/feishu/bot.rb +216 -7
- data/lib/clacky/server/channel/adapters/feishu/message_parser.rb +12 -0
- data/lib/clacky/server/channel/adapters/wecom/adapter.rb +2 -8
- data/lib/clacky/server/channel/adapters/weixin/adapter.rb +391 -0
- data/lib/clacky/server/channel/adapters/weixin/api_client.rb +374 -0
- data/lib/clacky/server/channel/channel_config.rb +6 -0
- data/lib/clacky/server/channel/channel_manager.rb +50 -15
- data/lib/clacky/server/channel/channel_ui_controller.rb +18 -0
- data/lib/clacky/server/channel.rb +1 -0
- data/lib/clacky/server/http_server.rb +210 -45
- data/lib/clacky/server/scheduler.rb +1 -1
- data/lib/clacky/server/session_registry.rb +131 -40
- data/lib/clacky/server/web_ui_controller.rb +21 -2
- data/lib/clacky/session_manager.rb +43 -73
- data/lib/clacky/skill.rb +64 -42
- data/lib/clacky/tools/browser.rb +728 -183
- data/lib/clacky/tools/invoke_skill.rb +18 -11
- data/lib/clacky/tools/shell.rb +16 -7
- data/lib/clacky/tools/web_fetch.rb +3 -1
- data/lib/clacky/ui2/README.md +1 -1
- data/lib/clacky/ui2/components/input_area.rb +5 -2
- data/lib/clacky/ui2/components/welcome_banner.rb +23 -0
- data/lib/clacky/ui2/ui_controller.rb +16 -2
- data/lib/clacky/ui_interface.rb +7 -0
- data/lib/clacky/utils/file_processor.rb +116 -139
- data/lib/clacky/utils/model_pricing.rb +5 -4
- data/lib/clacky/utils/parser_manager.rb +93 -0
- data/lib/clacky/utils/workspace_rules.rb +46 -0
- data/lib/clacky/version.rb +1 -1
- data/lib/clacky/web/app.css +696 -62
- data/lib/clacky/web/app.js +181 -51
- data/lib/clacky/web/brand.js +14 -0
- data/lib/clacky/web/channels.js +10 -1
- data/lib/clacky/web/i18n.js +49 -7
- data/lib/clacky/web/index.html +67 -10
- data/lib/clacky/web/onboard.js +2 -1
- data/lib/clacky/web/sessions.js +255 -69
- data/lib/clacky/web/settings.js +113 -1
- data/lib/clacky/web/skills.js +16 -3
- data/lib/clacky/web/tasks.js +11 -6
- data/lib/clacky/web/weixin-qr.html +104 -0
- data/lib/clacky.rb +2 -0
- data/scripts/install.sh +41 -11
- metadata +48 -7
- data/lib/clacky/default_skills/pdf-reader/SKILL.md +0 -90
- data/lib/clacky/utils/file_parser/docx_parser.rb +0 -156
- data/lib/clacky/utils/file_parser/pptx_parser.rb +0 -116
- data/lib/clacky/utils/file_parser/xlsx_parser.rb +0 -95
- data/lib/clacky/utils/file_parser/zip_parser.rb +0 -60
- data/scripts/install_agent_browser.sh +0 -67
data/lib/clacky/web/index.html
CHANGED
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
<path d="M9 3v18"/>
|
|
28
28
|
</svg>
|
|
29
29
|
</button>
|
|
30
|
-
<div id="header-brand">
|
|
30
|
+
<div id="header-brand" style="cursor:pointer" onclick="Router.navigate('chat')">
|
|
31
31
|
<img class="header-logo-img" id="header-logo-img" src="" alt="" style="display:none">
|
|
32
32
|
<span class="header-logo-divider"></span>
|
|
33
33
|
<span class="header-logo" id="header-logo">{{BRAND_NAME}}</span>
|
|
@@ -42,6 +42,9 @@
|
|
|
42
42
|
</div>
|
|
43
43
|
</header>
|
|
44
44
|
|
|
45
|
+
<!-- ── Sidebar overlay (mobile only) ───────────────────────────────────── -->
|
|
46
|
+
<div id="sidebar-overlay"></div>
|
|
47
|
+
|
|
45
48
|
<!-- ── CONTENT ROW (Sidebar + Main) ────────────────────────────────────── -->
|
|
46
49
|
<div id="content-row">
|
|
47
50
|
|
|
@@ -51,16 +54,37 @@
|
|
|
51
54
|
<div id="sidebar-list">
|
|
52
55
|
<!-- Chat Group -->
|
|
53
56
|
<div id="chat-section">
|
|
57
|
+
<!-- Header: "Sessions" label + [+ ▾] split button -->
|
|
54
58
|
<div class="sidebar-divider">
|
|
55
|
-
<span data-i18n="sidebar.chat">
|
|
56
|
-
<
|
|
57
|
-
<
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
59
|
+
<span data-i18n="sidebar.chat">Sessions</span>
|
|
60
|
+
<div class="btn-split-wrap">
|
|
61
|
+
<button id="btn-new-session-inline" class="btn-split-main" title="New Session" data-i18n="sessions.newSession">+ New Session</button>
|
|
62
|
+
<button id="btn-new-session-arrow" class="btn-split-arrow" title="Choose agent">▾</button>
|
|
63
|
+
<!-- Dropdown -->
|
|
64
|
+
<div id="new-session-dropdown" class="new-session-dropdown" hidden>
|
|
65
|
+
<div class="dropdown-item" data-profile="general">✦ General</div>
|
|
66
|
+
<div class="dropdown-item" data-profile="coding">👨💻 Coding</div>
|
|
67
|
+
</div>
|
|
68
|
+
</div>
|
|
69
|
+
</div>
|
|
70
|
+
|
|
71
|
+
<!-- Source tabs: Manual / Scheduled / Channel -->
|
|
72
|
+
<div id="session-tabs" class="session-tabs">
|
|
73
|
+
<button class="session-tab active" data-tab="manual" data-i18n="sessions.tab.manual">Manual</button>
|
|
74
|
+
<button class="session-tab" data-tab="cron" data-i18n="sessions.tab.scheduled">Scheduled</button>
|
|
75
|
+
<button class="session-tab" data-tab="channel" data-i18n="sessions.tab.channel">Channel</button>
|
|
76
|
+
<button class="session-tab" data-tab="setup" data-i18n="sessions.tab.setup" id="tab-setup" style="display:none">Setup</button>
|
|
62
77
|
</div>
|
|
78
|
+
|
|
79
|
+
<!-- General sessions list (filtered by active tab) -->
|
|
63
80
|
<div id="session-list"></div>
|
|
81
|
+
|
|
82
|
+
<!-- Coding agent fixed section -->
|
|
83
|
+
<div id="coding-section" style="display:none">
|
|
84
|
+
<div class="coding-section-divider">👨💻 Coding</div>
|
|
85
|
+
<div id="coding-session-list"></div>
|
|
86
|
+
</div>
|
|
87
|
+
<!-- Load more buttons are rendered dynamically inside each section by Sessions.renderList() -->
|
|
64
88
|
</div>
|
|
65
89
|
|
|
66
90
|
<!-- Config Group -->
|
|
@@ -161,6 +185,12 @@
|
|
|
161
185
|
<!-- Chat panel -->
|
|
162
186
|
<div id="chat-panel" style="display:none">
|
|
163
187
|
<header id="chat-header">
|
|
188
|
+
<!-- Mobile-only back button: shown inside a session -->
|
|
189
|
+
<button id="btn-back-mobile" class="chat-back-btn" title="Back">
|
|
190
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
|
191
|
+
<path d="M15 18l-6-6 6-6"/>
|
|
192
|
+
</svg>
|
|
193
|
+
</button>
|
|
164
194
|
<span id="chat-title">Session</span>
|
|
165
195
|
<span id="chat-status" class="status-idle">idle</span>
|
|
166
196
|
<button id="btn-delete-session" class="btn-delete-session" title="Delete session">
|
|
@@ -174,7 +204,7 @@
|
|
|
174
204
|
<div id="session-info-bar" style="display:none">
|
|
175
205
|
<!-- Always visible: status | id | model | cost -->
|
|
176
206
|
<span id="sib-status"></span>
|
|
177
|
-
<span class="sib-sep">│</span>
|
|
207
|
+
<span class="sib-sep sib-sep-before-id">│</span>
|
|
178
208
|
<span id="sib-id"></span>
|
|
179
209
|
<span id="sib-model-wrap"><span class="sib-sep">│</span><span id="sib-model"></span></span>
|
|
180
210
|
<span class="sib-sep">│</span>
|
|
@@ -184,7 +214,7 @@
|
|
|
184
214
|
<span class="sib-sep">│</span>
|
|
185
215
|
<span id="sib-dir"></span>
|
|
186
216
|
<span id="sib-mode-wrap"><span class="sib-sep">│</span><span id="sib-mode"></span></span>
|
|
187
|
-
<span class="sib-sep">│</span>
|
|
217
|
+
<span class="sib-sep sib-sep-before-tasks">│</span>
|
|
188
218
|
<span id="sib-tasks"></span>
|
|
189
219
|
</span>
|
|
190
220
|
</div>
|
|
@@ -342,6 +372,23 @@
|
|
|
342
372
|
</div>
|
|
343
373
|
</section>
|
|
344
374
|
|
|
375
|
+
<!-- Browser section -->
|
|
376
|
+
<section class="settings-section">
|
|
377
|
+
<div class="settings-section-title">
|
|
378
|
+
<span data-i18n="settings.browser.title">Browser</span>
|
|
379
|
+
</div>
|
|
380
|
+
<div class="settings-personalize">
|
|
381
|
+
<p class="settings-personalize-desc" id="browser-status-desc" data-i18n="settings.browser.desc">Connect your browser to enable browser automation.</p>
|
|
382
|
+
<div class="settings-browser-actions">
|
|
383
|
+
<label class="toggle-switch" id="browser-toggle-wrap" style="display:none">
|
|
384
|
+
<input type="checkbox" id="browser-toggle-input">
|
|
385
|
+
<span class="toggle-slider"></span>
|
|
386
|
+
</label>
|
|
387
|
+
<button id="btn-browser-setup" class="btn-settings-action" data-i18n="settings.browser.btn">🌐 Configure Browser</button>
|
|
388
|
+
</div>
|
|
389
|
+
</div>
|
|
390
|
+
</section>
|
|
391
|
+
|
|
345
392
|
<!-- Language section -->
|
|
346
393
|
<section class="settings-section" id="language-section">
|
|
347
394
|
<div class="settings-section-title">
|
|
@@ -375,10 +422,20 @@
|
|
|
375
422
|
<span class="brand-status-label" data-i18n="settings.brand.label.expires">Expires</span>
|
|
376
423
|
<span class="brand-status-value" id="brand-status-expires">—</span>
|
|
377
424
|
</div>
|
|
425
|
+
<div class="brand-status-row" id="brand-status-homepage-row" style="display:none">
|
|
426
|
+
<span class="brand-status-label" data-i18n="settings.brand.label.homepage">Homepage</span>
|
|
427
|
+
<a id="brand-status-homepage" class="brand-status-value brand-status-link" href="#" target="_blank" rel="noopener noreferrer">—</a>
|
|
428
|
+
</div>
|
|
378
429
|
<div class="brand-status-actions">
|
|
379
430
|
<button id="btn-rebind-license" class="btn-settings-action" data-i18n="settings.brand.btn.change">Change License Key</button>
|
|
380
431
|
</div>
|
|
381
432
|
</div>
|
|
433
|
+
<!-- Support contact (shown when support_contact is available) -->
|
|
434
|
+
<div id="brand-support-contact" class="brand-support-contact" style="display:none">
|
|
435
|
+
<span class="brand-support-contact-label" data-i18n="settings.brand.label.supportContact">Support</span>
|
|
436
|
+
<a id="brand-support-contact-link" class="brand-support-contact-value" href="#" target="_blank" rel="noopener noreferrer"></a>
|
|
437
|
+
</div>
|
|
438
|
+
|
|
382
439
|
<!-- Support QR code (shown when support_qr_url is available) -->
|
|
383
440
|
<div id="brand-support-qr" class="brand-support-qr" style="display:none">
|
|
384
441
|
<button class="brand-support-qr-btn" id="brand-support-qr-btn" title="Click to enlarge" aria-label="View QR code larger">
|
data/lib/clacky/web/onboard.js
CHANGED
|
@@ -312,13 +312,14 @@ const Onboard = (() => {
|
|
|
312
312
|
const res = await fetch("/api/sessions", {
|
|
313
313
|
method: "POST",
|
|
314
314
|
headers: { "Content-Type": "application/json" },
|
|
315
|
-
body: JSON.stringify({ name: "✨ Onboard" })
|
|
315
|
+
body: JSON.stringify({ name: "✨ Onboard", source: "setup" })
|
|
316
316
|
});
|
|
317
317
|
const data = await res.json();
|
|
318
318
|
const session = data.session;
|
|
319
319
|
if (!session) throw new Error("No session returned");
|
|
320
320
|
|
|
321
321
|
Sessions.add(session);
|
|
322
|
+
Sessions.setTab("setup");
|
|
322
323
|
Sessions.renderList();
|
|
323
324
|
Sessions.setPendingMessage(session.id, `/onboard lang:${_selectedLang}`);
|
|
324
325
|
Sessions.select(session.id);
|
data/lib/clacky/web/sessions.js
CHANGED
|
@@ -20,6 +20,10 @@ const Sessions = (() => {
|
|
|
20
20
|
const _historyState = {}; // { [session_id]: { hasMore, oldestCreatedAt, loading, loaded } }
|
|
21
21
|
const _renderedCreatedAt = {}; // { [session_id]: Set<number> } — dedup by created_at
|
|
22
22
|
let _activeId = null;
|
|
23
|
+
let _activeTab = "manual"; // "manual" | "cron" | "channel" | "setup"
|
|
24
|
+
// Per-source pagination state (manual/cron/channel/setup for general tabs; coding for coding section)
|
|
25
|
+
const _hasMoreBySource = { manual: false, cron: false, channel: false, setup: false, coding: false };
|
|
26
|
+
const _loadingMoreSource = { manual: false, cron: false, channel: false, setup: false, coding: false };
|
|
23
27
|
let _pendingRunTaskId = null; // session_id waiting to send "run_task" after subscribe
|
|
24
28
|
let _pendingMessage = null; // { session_id, content } — slash command to send after subscribe
|
|
25
29
|
|
|
@@ -246,8 +250,20 @@ const Sessions = (() => {
|
|
|
246
250
|
if (Array.isArray(ev.images) && ev.images.length > 0) {
|
|
247
251
|
bubbleHtml += ev.images.map(src => {
|
|
248
252
|
if (src && src.startsWith("pdf:")) {
|
|
249
|
-
//
|
|
250
|
-
|
|
253
|
+
// File badge — extract filename and extension from sentinel "pdf:<name>"
|
|
254
|
+
const fname = src.slice(4);
|
|
255
|
+
const ext = (fname.split(".").pop() || "file").toUpperCase();
|
|
256
|
+
const icon = ext === "PDF" ? "📄" : ext === "ZIP" ? "🗜️" :
|
|
257
|
+
(ext === "DOC" || ext === "DOCX") ? "📝" :
|
|
258
|
+
(ext === "XLS" || ext === "XLSX") ? "📊" :
|
|
259
|
+
(ext === "PPT" || ext === "PPTX") ? "📋" : "📎";
|
|
260
|
+
return `<span class="msg-pdf-badge">` +
|
|
261
|
+
`<span class="msg-pdf-badge-icon">${icon}</span>` +
|
|
262
|
+
`<span class="msg-pdf-badge-info">` +
|
|
263
|
+
`<span class="msg-pdf-badge-name">${escapeHtml(fname)}</span>` +
|
|
264
|
+
`<span class="msg-pdf-badge-type">${escapeHtml(ext)}</span>` +
|
|
265
|
+
`</span>` +
|
|
266
|
+
`</span>`;
|
|
251
267
|
}
|
|
252
268
|
return `<img src="${escapeHtml(src)}" alt="image" class="msg-image-thumb">`;
|
|
253
269
|
}).join("");
|
|
@@ -390,6 +406,83 @@ const Sessions = (() => {
|
|
|
390
406
|
}
|
|
391
407
|
}
|
|
392
408
|
|
|
409
|
+
// ── Private helpers ───────────────────────────────────────────────────
|
|
410
|
+
|
|
411
|
+
// Return a human-readable relative label for a session with no name.
|
|
412
|
+
// e.g. "Today 14:14" / "Yesterday" / "Mar 21"
|
|
413
|
+
function _relativeTime(createdAt) {
|
|
414
|
+
if (!createdAt) return I18n.t("sessions.untitled") || "Untitled";
|
|
415
|
+
const d = new Date(createdAt);
|
|
416
|
+
const now = new Date();
|
|
417
|
+
const diffDays = Math.floor((now - d) / 86400000);
|
|
418
|
+
const pad = n => String(n).padStart(2, "0");
|
|
419
|
+
const hhmm = `${pad(d.getHours())}:${pad(d.getMinutes())}`;
|
|
420
|
+
if (diffDays === 0) return `Today ${hhmm}`;
|
|
421
|
+
if (diffDays === 1) return `Yesterday ${hhmm}`;
|
|
422
|
+
return `${d.getMonth() + 1}/${d.getDate()} ${hhmm}`;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
// Build a load-more button for a given source tab.
|
|
426
|
+
function _makeLoadMoreBtn(source) {
|
|
427
|
+
const btn = document.createElement("button");
|
|
428
|
+
btn.className = "btn-load-more-sessions";
|
|
429
|
+
btn.disabled = _loadingMoreSource[source];
|
|
430
|
+
btn.textContent = _loadingMoreSource[source]
|
|
431
|
+
? I18n.t("sessions.loadingMore")
|
|
432
|
+
: I18n.t("sessions.loadMore");
|
|
433
|
+
btn.onclick = () => Sessions.loadMoreSessions(source);
|
|
434
|
+
return btn;
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
// ── Private render helper ─────────────────────────────────────────────
|
|
438
|
+
//
|
|
439
|
+
// Build and append a single session-item <div> into `container`.
|
|
440
|
+
// Used by both the general list and the coding section.
|
|
441
|
+
function _renderSessionItem(container, s) {
|
|
442
|
+
const el = document.createElement("div");
|
|
443
|
+
el.className = "session-item" + (s.id === _activeId ? " active" : "");
|
|
444
|
+
const displayName = s.name || _relativeTime(s.created_at);
|
|
445
|
+
const metaText = I18n.t("sessions.meta", { tasks: s.total_tasks || 0, cost: (s.total_cost || 0).toFixed(4) });
|
|
446
|
+
el.innerHTML = `
|
|
447
|
+
<span class="session-dot dot-${s.status || "idle"}"></span>
|
|
448
|
+
<div class="session-body">
|
|
449
|
+
<div class="session-name">${escapeHtml(displayName)}</div>
|
|
450
|
+
<div class="session-meta">${metaText}</div>
|
|
451
|
+
</div>
|
|
452
|
+
<button class="session-delete-btn" title="${I18n.t("sessions.deleteTitle")}">×</button>`;
|
|
453
|
+
|
|
454
|
+
// Use a click timer to distinguish single-click (select) from double-click (rename).
|
|
455
|
+
let clickTimer = null;
|
|
456
|
+
el.onclick = () => {
|
|
457
|
+
if (clickTimer) {
|
|
458
|
+
clearTimeout(clickTimer);
|
|
459
|
+
clickTimer = null;
|
|
460
|
+
return;
|
|
461
|
+
}
|
|
462
|
+
clickTimer = setTimeout(() => {
|
|
463
|
+
clickTimer = null;
|
|
464
|
+
Sessions.select(s.id);
|
|
465
|
+
}, 200);
|
|
466
|
+
};
|
|
467
|
+
|
|
468
|
+
// Delete button
|
|
469
|
+
const deleteBtn = el.querySelector(".session-delete-btn");
|
|
470
|
+
deleteBtn.onclick = (e) => {
|
|
471
|
+
e.stopPropagation();
|
|
472
|
+
Sessions.deleteSession(s.id);
|
|
473
|
+
};
|
|
474
|
+
|
|
475
|
+
// Double-click to rename
|
|
476
|
+
const nameDiv = el.querySelector(".session-name");
|
|
477
|
+
nameDiv.ondblclick = (e) => {
|
|
478
|
+
e.stopPropagation();
|
|
479
|
+
if (clickTimer) { clearTimeout(clickTimer); clickTimer = null; }
|
|
480
|
+
Sessions._startRename(s.id, nameDiv, s.name);
|
|
481
|
+
};
|
|
482
|
+
|
|
483
|
+
container.appendChild(el);
|
|
484
|
+
}
|
|
485
|
+
|
|
393
486
|
// ── Public API ─────────────────────────────────────────────────────────
|
|
394
487
|
return {
|
|
395
488
|
get all() { return _sessions; },
|
|
@@ -399,9 +492,13 @@ const Sessions = (() => {
|
|
|
399
492
|
// ── List management ───────────────────────────────────────────────────
|
|
400
493
|
|
|
401
494
|
/** Populate list from initial session_list WS event (connect only). */
|
|
402
|
-
setAll(list) {
|
|
495
|
+
setAll(list, hasMoreBySource = {}) {
|
|
403
496
|
_sessions.length = 0;
|
|
404
497
|
_sessions.push(...list);
|
|
498
|
+
// Accept either new per-source map or legacy scalar boolean
|
|
499
|
+
if (typeof hasMoreBySource === "object" && hasMoreBySource !== null) {
|
|
500
|
+
Object.assign(_hasMoreBySource, hasMoreBySource);
|
|
501
|
+
}
|
|
405
502
|
},
|
|
406
503
|
|
|
407
504
|
/** Insert a newly created session into the local list. */
|
|
@@ -423,6 +520,53 @@ const Sessions = (() => {
|
|
|
423
520
|
if (idx !== -1) _sessions.splice(idx, 1);
|
|
424
521
|
},
|
|
425
522
|
|
|
523
|
+
/** Load older sessions for a specific bucket.
|
|
524
|
+
* bucket: "manual" | "cron" | "channel" | "setup" → uses ?source=<bucket>
|
|
525
|
+
* "coding" → uses ?profile=coding
|
|
526
|
+
*/
|
|
527
|
+
async loadMoreSessions(source) {
|
|
528
|
+
if (_loadingMoreSource[source] || !_hasMoreBySource[source]) return;
|
|
529
|
+
_loadingMoreSource[source] = true;
|
|
530
|
+
Sessions.renderList(); // re-render to show loading state on button
|
|
531
|
+
|
|
532
|
+
try {
|
|
533
|
+
// Cursor: oldest created_at among sessions in this bucket
|
|
534
|
+
const bucketSessions = source === "coding"
|
|
535
|
+
? _sessions.filter(s => s.agent_profile === "coding")
|
|
536
|
+
: _sessions.filter(s => s.source === source);
|
|
537
|
+
|
|
538
|
+
const oldest = bucketSessions.reduce((min, s) => {
|
|
539
|
+
if (!s.created_at) return min;
|
|
540
|
+
return (!min || s.created_at < min) ? s.created_at : min;
|
|
541
|
+
}, null);
|
|
542
|
+
|
|
543
|
+
const param = source === "coding" ? "profile=coding" : `source=${encodeURIComponent(source)}&profile=general`;
|
|
544
|
+
let url = `/api/sessions?limit=10&${param}`;
|
|
545
|
+
if (oldest) url += `&before=${encodeURIComponent(oldest)}`;
|
|
546
|
+
|
|
547
|
+
const res = await fetch(url);
|
|
548
|
+
if (!res.ok) return;
|
|
549
|
+
const data = await res.json();
|
|
550
|
+
|
|
551
|
+
let added = 0;
|
|
552
|
+
(data.sessions || []).forEach(s => {
|
|
553
|
+
if (!_sessions.find(x => x.id === s.id)) {
|
|
554
|
+
_sessions.push(s);
|
|
555
|
+
added++;
|
|
556
|
+
}
|
|
557
|
+
});
|
|
558
|
+
_hasMoreBySource[source] = !!data.has_more;
|
|
559
|
+
|
|
560
|
+
if (added > 0) Sessions.renderList();
|
|
561
|
+
else Sessions.renderList(); // update button state even with no new items
|
|
562
|
+
} catch (e) {
|
|
563
|
+
console.error("loadMoreSessions error:", e);
|
|
564
|
+
} finally {
|
|
565
|
+
_loadingMoreSource[source] = false;
|
|
566
|
+
Sessions.renderList();
|
|
567
|
+
}
|
|
568
|
+
},
|
|
569
|
+
|
|
426
570
|
/** Delete a session via API (called from UI delete button). */
|
|
427
571
|
async deleteSession(id) {
|
|
428
572
|
const s = _sessions.find(s => s.id === id);
|
|
@@ -500,70 +644,75 @@ const Sessions = (() => {
|
|
|
500
644
|
|
|
501
645
|
// ── Rendering ─────────────────────────────────────────────────────────
|
|
502
646
|
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
}
|
|
647
|
+
setTab(tab) {
|
|
648
|
+
_activeTab = tab;
|
|
649
|
+
// Sync tab button active state
|
|
650
|
+
document.querySelectorAll(".session-tab").forEach(btn => {
|
|
651
|
+
btn.classList.toggle("active", btn.dataset.tab === tab);
|
|
652
|
+
});
|
|
653
|
+
Sessions.renderList();
|
|
654
|
+
},
|
|
512
655
|
|
|
513
|
-
|
|
514
|
-
// newest-first by created_at
|
|
656
|
+
renderList() {
|
|
657
|
+
// Sort helper: newest-first by created_at
|
|
515
658
|
const byTime = (a, b) => {
|
|
516
659
|
const ta = a.created_at ? new Date(a.created_at) : 0;
|
|
517
660
|
const tb = b.created_at ? new Date(b.created_at) : 0;
|
|
518
661
|
return tb - ta;
|
|
519
662
|
};
|
|
520
|
-
const manual = _sessions.filter(s => !s.name.startsWith("⏰")).sort(byTime);
|
|
521
|
-
const scheduled = _sessions.filter(s => s.name.startsWith("⏰")).sort(byTime);
|
|
522
663
|
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
}
|
|
664
|
+
// ── Classify ──────────────────────────────────────────────────────────
|
|
665
|
+
// Two orthogonal dimensions: agent_profile (general vs coding) and source (tab).
|
|
666
|
+
// Coding section: profile === "coding", shown separately at bottom — no source filter.
|
|
667
|
+
// General area tabs: all non-coding sessions, filtered by source per tab.
|
|
668
|
+
const codingSessions = _sessions.filter(s => s.agent_profile === "coding").sort(byTime);
|
|
669
|
+
const generalSessions = _sessions.filter(s => s.agent_profile !== "coding").sort(byTime);
|
|
670
|
+
|
|
671
|
+
// ── Tab visibility ────────────────────────────────────────────────────
|
|
672
|
+
const hasCron = generalSessions.some(s => s.source === "cron") || _hasMoreBySource["cron"];
|
|
673
|
+
const hasChannel = generalSessions.some(s => s.source === "channel") || _hasMoreBySource["channel"];
|
|
674
|
+
const hasSetup = generalSessions.some(s => s.source === "setup") || _hasMoreBySource["setup"];
|
|
675
|
+
const hasNonManual = hasCron || hasChannel || hasSetup;
|
|
676
|
+
|
|
677
|
+
const tabBar = $("session-tabs");
|
|
678
|
+
if (tabBar) tabBar.style.display = hasNonManual ? "" : "none";
|
|
679
|
+
|
|
680
|
+
const setupTabBtn = $("tab-setup");
|
|
681
|
+
if (setupTabBtn) setupTabBtn.style.display = hasSetup ? "" : "none";
|
|
682
|
+
|
|
683
|
+
// Use active tab directly — no fallback to manual when content is empty.
|
|
684
|
+
// Tabs may show an empty state (e.g. cron tab before any cron sessions exist).
|
|
685
|
+
const effectiveTab = _activeTab;
|
|
686
|
+
|
|
687
|
+
const tabSessions = generalSessions.filter(s => (s.source || "manual") === effectiveTab);
|
|
688
|
+
|
|
689
|
+
// ── Render general (tab-filtered) sessions ──────────────────────────
|
|
690
|
+
const list = $("session-list");
|
|
691
|
+
list.innerHTML = "";
|
|
692
|
+
if (tabSessions.length === 0) {
|
|
693
|
+
list.innerHTML = `<div class="session-empty">${I18n.t("sessions.empty")}</div>`;
|
|
694
|
+
} else {
|
|
695
|
+
tabSessions.forEach(s => _renderSessionItem(list, s));
|
|
696
|
+
}
|
|
697
|
+
// Per-tab load-more button (inside the tab's section)
|
|
698
|
+
if (_hasMoreBySource[effectiveTab]) {
|
|
699
|
+
list.appendChild(_makeLoadMoreBtn(effectiveTab));
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
// ── Render coding sessions (fixed section) ───────────────────────────
|
|
703
|
+
const codingSection = $("coding-section");
|
|
704
|
+
const codingList = $("coding-session-list");
|
|
705
|
+
if (codingSessions.length === 0) {
|
|
706
|
+
codingSection.style.display = "none";
|
|
707
|
+
} else {
|
|
708
|
+
codingSection.style.display = "";
|
|
709
|
+
codingList.innerHTML = "";
|
|
710
|
+
codingSessions.forEach(s => _renderSessionItem(codingList, s));
|
|
711
|
+
// Per-section load-more button
|
|
712
|
+
if (_hasMoreBySource["coding"]) {
|
|
713
|
+
codingList.appendChild(_makeLoadMoreBtn("coding"));
|
|
714
|
+
}
|
|
715
|
+
}
|
|
567
716
|
},
|
|
568
717
|
|
|
569
718
|
/** Begin inline rename: replace session-name content with an <input>. */
|
|
@@ -571,10 +720,7 @@ const Sessions = (() => {
|
|
|
571
720
|
// Prevent starting a second rename while one is already active
|
|
572
721
|
if (nameDiv.querySelector("input")) return;
|
|
573
722
|
|
|
574
|
-
|
|
575
|
-
const dotHtml = dot ? dot.outerHTML : "";
|
|
576
|
-
|
|
577
|
-
// Replace entire content with just the input (no dot — keeps layout clean)
|
|
723
|
+
// Replace name span content with input (dot lives in session-row, not here)
|
|
578
724
|
nameDiv.innerHTML = "";
|
|
579
725
|
nameDiv.classList.add("renaming"); // disable overflow:hidden while editing
|
|
580
726
|
const input = document.createElement("input");
|
|
@@ -597,7 +743,7 @@ const Sessions = (() => {
|
|
|
597
743
|
|
|
598
744
|
// Restore original display immediately
|
|
599
745
|
nameDiv.classList.remove("renaming");
|
|
600
|
-
nameDiv.
|
|
746
|
+
nameDiv.textContent = currentName;
|
|
601
747
|
|
|
602
748
|
if (!newName || newName === currentName) return;
|
|
603
749
|
|
|
@@ -748,8 +894,18 @@ const Sessions = (() => {
|
|
|
748
894
|
}
|
|
749
895
|
|
|
750
896
|
// Cost: 5 decimal places (matches CLI precision)
|
|
751
|
-
|
|
752
|
-
|
|
897
|
+
// :api => "$0.00123" (exact)
|
|
898
|
+
// :price => "~$0.00123" (estimated from pricing table)
|
|
899
|
+
// :default => "N/A" (model unknown)
|
|
900
|
+
const cost = ev.cost || 0;
|
|
901
|
+
let costStr;
|
|
902
|
+
if (ev.cost_source === "default") {
|
|
903
|
+
costStr = "N/A";
|
|
904
|
+
} else if (ev.cost_source === "price") {
|
|
905
|
+
costStr = `~$${cost.toFixed(5)}`;
|
|
906
|
+
} else {
|
|
907
|
+
costStr = `$${cost.toFixed(5)}`;
|
|
908
|
+
}
|
|
753
909
|
|
|
754
910
|
// Always-visible: label, delta, cache indicator, cost
|
|
755
911
|
// Detail fields (Input/Output/Total) are hidden until hover
|
|
@@ -806,6 +962,32 @@ const Sessions = (() => {
|
|
|
806
962
|
messages.scrollTop = messages.scrollHeight;
|
|
807
963
|
},
|
|
808
964
|
|
|
965
|
+
// Two-phase idle compression status — renders as a single line that updates in-place.
|
|
966
|
+
// phase "start": create element with spinner; phase "end": update same element with result.
|
|
967
|
+
updateIdleStatus(phase, text) {
|
|
968
|
+
const messages = $("messages");
|
|
969
|
+
if (phase === "start") {
|
|
970
|
+
Sessions.collapseToolGroup();
|
|
971
|
+
const el = document.createElement("div");
|
|
972
|
+
el.className = "msg msg-info msg-idle-status";
|
|
973
|
+
el.textContent = "⟳ " + text;
|
|
974
|
+
messages.appendChild(el);
|
|
975
|
+
Sessions._idleStatusEl = el;
|
|
976
|
+
} else {
|
|
977
|
+
// phase "end" — update existing element or create a new one if start was missed
|
|
978
|
+
if (Sessions._idleStatusEl) {
|
|
979
|
+
Sessions._idleStatusEl.textContent = "· " + text;
|
|
980
|
+
Sessions._idleStatusEl.classList.add("msg-idle-done");
|
|
981
|
+
Sessions._idleStatusEl = null;
|
|
982
|
+
} else {
|
|
983
|
+
Sessions.appendInfo("· " + text);
|
|
984
|
+
}
|
|
985
|
+
}
|
|
986
|
+
messages.scrollTop = messages.scrollHeight;
|
|
987
|
+
},
|
|
988
|
+
|
|
989
|
+
_idleStatusEl: null,
|
|
990
|
+
|
|
809
991
|
showProgress(text) {
|
|
810
992
|
Sessions.clearProgress();
|
|
811
993
|
const messages = $("messages");
|
|
@@ -829,7 +1011,7 @@ const Sessions = (() => {
|
|
|
829
1011
|
// ── Create ─────────────────────────────────────────────────────────────
|
|
830
1012
|
|
|
831
1013
|
/** Create a new session and navigate to it. */
|
|
832
|
-
async create() {
|
|
1014
|
+
async create(agentProfile = "general") {
|
|
833
1015
|
const maxN = _sessions.reduce((max, s) => {
|
|
834
1016
|
const m = s.name.match(/^Session (\d+)$/);
|
|
835
1017
|
return m ? Math.max(max, parseInt(m[1], 10)) : max;
|
|
@@ -839,7 +1021,7 @@ const Sessions = (() => {
|
|
|
839
1021
|
const res = await fetch("/api/sessions", {
|
|
840
1022
|
method: "POST",
|
|
841
1023
|
headers: { "Content-Type": "application/json" },
|
|
842
|
-
body: JSON.stringify({ name })
|
|
1024
|
+
body: JSON.stringify({ name, agent_profile: agentProfile, source: "manual" })
|
|
843
1025
|
});
|
|
844
1026
|
const data = await res.json();
|
|
845
1027
|
if (!res.ok) { alert(I18n.t("sessions.createError") + (data.error || "unknown")); return; }
|
|
@@ -848,6 +1030,10 @@ const Sessions = (() => {
|
|
|
848
1030
|
if (!session) return;
|
|
849
1031
|
|
|
850
1032
|
Sessions.add(session);
|
|
1033
|
+
|
|
1034
|
+
// If a coding session was created, make sure the coding section is visible.
|
|
1035
|
+
// If a general session was created, switch to the manual tab to show it.
|
|
1036
|
+
if (agentProfile === "general") Sessions.setTab("manual");
|
|
851
1037
|
Sessions.renderList();
|
|
852
1038
|
Sessions.select(session.id);
|
|
853
1039
|
},
|