openclacky 0.8.2 → 0.8.4
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 +34 -0
- data/lib/clacky/agent/session_serializer.rb +31 -0
- data/lib/clacky/agent/skill_manager.rb +59 -0
- data/lib/clacky/agent.rb +7 -2
- data/lib/clacky/agent_config.rb +10 -0
- data/lib/clacky/brand_config.rb +111 -24
- data/lib/clacky/cli.rb +1 -1
- data/lib/clacky/default_skills/onboard/SKILL.md +2 -2
- data/lib/clacky/server/http_server.rb +7 -4
- data/lib/clacky/skill_loader.rb +22 -18
- data/lib/clacky/ui2/layout_manager.rb +5 -0
- data/lib/clacky/ui2/screen_buffer.rb +24 -7
- data/lib/clacky/ui2/ui_controller.rb +56 -19
- data/lib/clacky/version.rb +1 -1
- data/lib/clacky/web/app.css +947 -337
- data/lib/clacky/web/app.js +30 -11
- data/lib/clacky/web/index.html +108 -30
- data/lib/clacky/web/onboard.js +92 -16
- data/lib/clacky/web/sessions.js +78 -3
- data/lib/clacky/web/settings.js +179 -26
- data/lib/clacky/web/skills.js +7 -3
- data/lib/clacky/web/tasks.js +34 -8
- data/lib/clacky/web/theme.js +67 -0
- metadata +16 -1
data/lib/clacky/web/app.js
CHANGED
|
@@ -478,8 +478,27 @@ function sendMessage() {
|
|
|
478
478
|
}
|
|
479
479
|
|
|
480
480
|
// ── DOM event listeners ───────────────────────────────────────────────────
|
|
481
|
-
|
|
481
|
+
// Sidebar toggle
|
|
482
|
+
if ($("btn-toggle-sidebar")) {
|
|
483
|
+
$("btn-toggle-sidebar").addEventListener("click", () => {
|
|
484
|
+
const sidebar = $("sidebar");
|
|
485
|
+
sidebar.classList.toggle("hidden");
|
|
486
|
+
});
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
// New session buttons (both old and new inline button)
|
|
490
|
+
if ($("btn-new-session")) {
|
|
491
|
+
$("btn-new-session").addEventListener("click", () => Sessions.create());
|
|
492
|
+
}
|
|
493
|
+
if ($("btn-new-session-inline")) {
|
|
494
|
+
$("btn-new-session-inline").addEventListener("click", () => Sessions.create());
|
|
495
|
+
}
|
|
482
496
|
$("btn-welcome-new").addEventListener("click", () => Sessions.create());
|
|
497
|
+
|
|
498
|
+
// Theme toggle in header
|
|
499
|
+
if ($("theme-toggle-header")) {
|
|
500
|
+
$("theme-toggle-header").addEventListener("click", () => Theme.toggle());
|
|
501
|
+
}
|
|
483
502
|
$("btn-delete-session").addEventListener("click", () => {
|
|
484
503
|
if (Sessions.activeId) Sessions.deleteSession(Sessions.activeId);
|
|
485
504
|
});
|
|
@@ -506,11 +525,8 @@ $("btn-slash").addEventListener("mousedown", e => {
|
|
|
506
525
|
});
|
|
507
526
|
$("btn-slash").addEventListener("click", () => {
|
|
508
527
|
const input = $("user-input");
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
input.style.height = "auto";
|
|
512
|
-
input.style.height = Math.min(input.scrollHeight, 200) + "px";
|
|
513
|
-
}
|
|
528
|
+
// Do nothing when the input is disabled (e.g. during subscribe handshake)
|
|
529
|
+
if (input.disabled) return;
|
|
514
530
|
SkillAC.openAll();
|
|
515
531
|
$("btn-slash").classList.add("active");
|
|
516
532
|
input.focus();
|
|
@@ -544,9 +560,12 @@ $("user-input").addEventListener("paste", e => {
|
|
|
544
560
|
imageItems.forEach(it => _addImageFile(it.getAsFile()));
|
|
545
561
|
});
|
|
546
562
|
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
563
|
+
// Note: do NOT use a manual _composing flag + compositionstart/compositionend.
|
|
564
|
+
// IMEs like Sogou fire keydown(Enter) in the same tick as compositionend, so
|
|
565
|
+
// the flag would already be false when the Enter keydown arrives — causing an
|
|
566
|
+
// accidental send. e.isComposing is set by the browser on the event object
|
|
567
|
+
// itself and remains true for the keydown that terminates a composition,
|
|
568
|
+
// which is exactly what we need.
|
|
550
569
|
|
|
551
570
|
// Hide skill autocomplete when input loses focus (unless clicking a dropdown item)
|
|
552
571
|
$("user-input").addEventListener("blur", () => {
|
|
@@ -699,7 +718,7 @@ const SkillAC = (() => {
|
|
|
699
718
|
_select(_activeIndex >= 0 ? _activeIndex : 0);
|
|
700
719
|
return true;
|
|
701
720
|
}
|
|
702
|
-
if (e.key === "Enter") {
|
|
721
|
+
if (e.key === "Enter" && !e.isComposing) {
|
|
703
722
|
if (_activeIndex >= 0) {
|
|
704
723
|
e.preventDefault();
|
|
705
724
|
_select(_activeIndex);
|
|
@@ -720,7 +739,7 @@ $("user-input").addEventListener("keydown", e => {
|
|
|
720
739
|
// Let skill autocomplete consume arrow/enter/escape first
|
|
721
740
|
if (SkillAC.handleKey(e)) return;
|
|
722
741
|
|
|
723
|
-
if (e.key === "Enter" && !e.shiftKey && !
|
|
742
|
+
if (e.key === "Enter" && !e.shiftKey && !e.isComposing) {
|
|
724
743
|
e.preventDefault();
|
|
725
744
|
sendMessage();
|
|
726
745
|
}
|
data/lib/clacky/web/index.html
CHANGED
|
@@ -9,35 +9,69 @@
|
|
|
9
9
|
<body>
|
|
10
10
|
<div id="app">
|
|
11
11
|
|
|
12
|
-
<!-- ──
|
|
13
|
-
<
|
|
14
|
-
<div id="
|
|
15
|
-
<
|
|
16
|
-
|
|
12
|
+
<!-- ── TOP HEADER ──────────────────────────────────────────────────────── -->
|
|
13
|
+
<header id="top-header">
|
|
14
|
+
<div id="header-left">
|
|
15
|
+
<button id="btn-toggle-sidebar" class="sidebar-toggle-btn" title="Toggle sidebar">
|
|
16
|
+
<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" class="icon-sm">
|
|
17
|
+
<rect width="18" height="18" x="3" y="3" rx="2"/>
|
|
18
|
+
<path d="M9 3v18"/>
|
|
19
|
+
</svg>
|
|
20
|
+
</button>
|
|
21
|
+
<span class="header-logo" id="header-logo">{{BRAND_NAME}}</span>
|
|
17
22
|
</div>
|
|
23
|
+
<div id="header-right">
|
|
24
|
+
<button id="theme-toggle-header" class="theme-toggle-btn" title="Toggle theme">
|
|
25
|
+
<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" class="icon-sm">
|
|
26
|
+
<path d="M12 3a6 6 0 0 0 9 9 9 9 0 1 1-9-9Z"/>
|
|
27
|
+
</svg>
|
|
28
|
+
</button>
|
|
29
|
+
</div>
|
|
30
|
+
</header>
|
|
31
|
+
|
|
32
|
+
<!-- ── CONTENT ROW (Sidebar + Main) ────────────────────────────────────── -->
|
|
33
|
+
<div id="content-row">
|
|
18
34
|
|
|
19
|
-
|
|
35
|
+
<!-- ── SIDEBAR ──────────────────────────────────────────────────────── -->
|
|
36
|
+
<aside id="sidebar">
|
|
37
|
+
<!-- Sidebar navigation groups -->
|
|
20
38
|
<div id="sidebar-list">
|
|
21
|
-
|
|
22
|
-
<div id="
|
|
23
|
-
<div class="sidebar-divider"
|
|
39
|
+
<!-- Chat Group -->
|
|
40
|
+
<div id="chat-section">
|
|
41
|
+
<div class="sidebar-divider">
|
|
42
|
+
<span>Chat</span>
|
|
43
|
+
<button id="btn-new-session-inline" class="btn-new-inline" title="New Session">
|
|
44
|
+
<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" class="icon-sm">
|
|
45
|
+
<path d="M5 12h14"/>
|
|
46
|
+
<path d="M12 5v14"/>
|
|
47
|
+
</svg>
|
|
48
|
+
</button>
|
|
49
|
+
</div>
|
|
50
|
+
<div id="session-list"></div>
|
|
51
|
+
</div>
|
|
52
|
+
|
|
53
|
+
<!-- Config Group -->
|
|
54
|
+
<div id="config-section">
|
|
55
|
+
<div class="sidebar-divider"><span>Config</span></div>
|
|
24
56
|
<div id="task-list-items">
|
|
25
57
|
<div id="tasks-sidebar-item" class="task-item task-item-summary">
|
|
26
58
|
<div class="task-row">
|
|
27
|
-
<
|
|
59
|
+
<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" class="task-icon">
|
|
60
|
+
<circle cx="12" cy="12" r="10"/>
|
|
61
|
+
<polyline points="12 6 12 12 16 14"/>
|
|
62
|
+
</svg>
|
|
28
63
|
<div class="task-info">
|
|
29
64
|
<span class="task-name" id="tasks-sidebar-label">Tasks</span>
|
|
30
65
|
</div>
|
|
31
66
|
</div>
|
|
32
67
|
</div>
|
|
33
68
|
</div>
|
|
34
|
-
</div>
|
|
35
|
-
<div id="skills-section">
|
|
36
|
-
<div class="sidebar-divider"><span>Skills</span></div>
|
|
37
69
|
<div id="skill-list-items">
|
|
38
70
|
<div id="skills-sidebar-item" class="task-item task-item-summary">
|
|
39
71
|
<div class="task-row">
|
|
40
|
-
<
|
|
72
|
+
<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" class="task-icon">
|
|
73
|
+
<path d="M19.439 7.85c-.049.322.059.648.289.878l1.568 1.568c.47.47.706 1.087.706 1.704s-.235 1.233-.706 1.704l-1.611 1.611a.98.98 0 0 1-.837.276c-.47-.07-.802-.48-.968-.925a2.501 2.501 0 1 0-3.214 3.214c.446.166.855.497.925.968a.979.979 0 0 1-.276.837l-1.61 1.61a2.404 2.404 0 0 1-1.705.707 2.402 2.402 0 0 1-1.704-.706l-1.568-1.568a1.026 1.026 0 0 0-.877-.29c-.493.074-.84.504-1.02.968a2.5 2.5 0 1 1-3.237-3.237c.464-.18.894-.527.967-1.02a1.026 1.026 0 0 0-.289-.877l-1.568-1.568A2.402 2.402 0 0 1 1.998 12c0-.617.236-1.234.706-1.704L4.23 8.77c.24-.24.581-.353.917-.303.515.077.877.528 1.073 1.01a2.5 2.5 0 1 0 3.259-3.259c-.482-.196-.933-.558-1.01-1.073-.05-.336.062-.676.303-.917l1.525-1.525A2.402 2.402 0 0 1 12 1.998c.617 0 1.234.236 1.704.706l1.568 1.568c.23.23.556.338.877.29.493-.074.84-.504 1.02-.968a2.5 2.5 0 1 1 3.237 3.237c-.464.18-.894.527-.967 1.02Z"/>
|
|
74
|
+
</svg>
|
|
41
75
|
<div class="task-info">
|
|
42
76
|
<span class="task-name" id="skills-sidebar-label">Skills</span>
|
|
43
77
|
</div>
|
|
@@ -47,7 +81,7 @@
|
|
|
47
81
|
</div>
|
|
48
82
|
</div>
|
|
49
83
|
|
|
50
|
-
<!-- Bottom
|
|
84
|
+
<!-- Bottom Settings -->
|
|
51
85
|
<div id="sidebar-footer">
|
|
52
86
|
<button id="btn-settings" class="sidebar-nav-btn" title="Settings">
|
|
53
87
|
<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">
|
|
@@ -83,9 +117,17 @@
|
|
|
83
117
|
<!-- Provider quick-select -->
|
|
84
118
|
<div class="onboard-field">
|
|
85
119
|
<label class="onboard-label">Provider</label>
|
|
86
|
-
<
|
|
87
|
-
<
|
|
88
|
-
|
|
120
|
+
<div class="custom-select-wrapper" id="onboard-provider-wrapper">
|
|
121
|
+
<div class="custom-select-trigger">
|
|
122
|
+
<span class="custom-select-value placeholder">— Choose provider —</span>
|
|
123
|
+
<svg class="custom-select-arrow" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
124
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"></path>
|
|
125
|
+
</svg>
|
|
126
|
+
</div>
|
|
127
|
+
<div class="custom-select-dropdown" id="onboard-provider-dropdown">
|
|
128
|
+
<div class="custom-select-option" data-value="">— Choose provider —</div>
|
|
129
|
+
</div>
|
|
130
|
+
</div>
|
|
89
131
|
</div>
|
|
90
132
|
<div class="onboard-field">
|
|
91
133
|
<label class="onboard-label">Model</label>
|
|
@@ -99,7 +141,12 @@
|
|
|
99
141
|
<label class="onboard-label">API Key</label>
|
|
100
142
|
<div class="onboard-input-row">
|
|
101
143
|
<input id="onboard-api-key" type="password" class="onboard-input" placeholder="sk-…">
|
|
102
|
-
<button id="onboard-toggle-key" class="btn-toggle-key" title="Show/hide"
|
|
144
|
+
<button id="onboard-toggle-key" class="btn-toggle-key" title="Show/hide">
|
|
145
|
+
<svg width="16" height="16" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
146
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"></path>
|
|
147
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"></path>
|
|
148
|
+
</svg>
|
|
149
|
+
</button>
|
|
103
150
|
</div>
|
|
104
151
|
</div>
|
|
105
152
|
|
|
@@ -156,11 +203,22 @@
|
|
|
156
203
|
|
|
157
204
|
<!-- Scheduled Tasks list panel (shown when user clicks "Scheduled Tasks") -->
|
|
158
205
|
<div id="task-detail-panel" style="display:none">
|
|
159
|
-
<
|
|
160
|
-
|
|
161
|
-
<
|
|
162
|
-
|
|
163
|
-
|
|
206
|
+
<div id="task-detail-body">
|
|
207
|
+
<!-- Title and Description -->
|
|
208
|
+
<div class="task-page-header">
|
|
209
|
+
<h2 class="task-page-title">Scheduled Tasks</h2>
|
|
210
|
+
<p class="task-page-subtitle">Manage and schedule automated tasks for your assistant</p>
|
|
211
|
+
<button id="btn-create-task" class="btn-create-task" title="Create a new task">
|
|
212
|
+
<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" class="icon-sm">
|
|
213
|
+
<path d="M5 12h14"/>
|
|
214
|
+
<path d="M12 5v14"/>
|
|
215
|
+
</svg>
|
|
216
|
+
<span>Create Task</span>
|
|
217
|
+
</button>
|
|
218
|
+
</div>
|
|
219
|
+
|
|
220
|
+
<div id="task-list-table"></div>
|
|
221
|
+
</div>
|
|
164
222
|
</div>
|
|
165
223
|
|
|
166
224
|
<!-- Chat panel -->
|
|
@@ -203,11 +261,20 @@
|
|
|
203
261
|
|
|
204
262
|
<!-- ── SKILLS PANEL ───────────────────────────────────────────────── -->
|
|
205
263
|
<div id="skills-panel" style="display:none">
|
|
206
|
-
<header id="skills-header">
|
|
207
|
-
<span>Skills</span>
|
|
208
|
-
<button id="btn-create-skill" title="Create a new skill">+ New Skill</button>
|
|
209
|
-
</header>
|
|
210
264
|
<div id="skills-body">
|
|
265
|
+
<!-- Title and Description -->
|
|
266
|
+
<div class="skills-page-header">
|
|
267
|
+
<h2 class="skills-page-title">Skills</h2>
|
|
268
|
+
<p class="skills-page-subtitle">Extend your assistant's capabilities with custom skills</p>
|
|
269
|
+
<button id="btn-create-skill" class="btn-create-skill" title="Create a new skill">
|
|
270
|
+
<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" class="icon-sm">
|
|
271
|
+
<path d="M5 12h14"/>
|
|
272
|
+
<path d="M12 5v14"/>
|
|
273
|
+
</svg>
|
|
274
|
+
<span>New Skill</span>
|
|
275
|
+
</button>
|
|
276
|
+
</div>
|
|
277
|
+
|
|
211
278
|
<!-- Tab bar -->
|
|
212
279
|
<div id="skills-tabs">
|
|
213
280
|
<button class="skills-tab active" data-tab="my-skills">My Skills</button>
|
|
@@ -223,7 +290,14 @@
|
|
|
223
290
|
<!-- Tab: Brand Skills (only visible when license is activated) -->
|
|
224
291
|
<div id="skills-tab-brand" class="skills-tab-content" style="display:none">
|
|
225
292
|
<div id="brand-skills-header">
|
|
226
|
-
<button id="btn-refresh-brand-skills" class="btn-brand-skills-refresh" title="Refresh from cloud"
|
|
293
|
+
<button id="btn-refresh-brand-skills" class="btn-brand-skills-refresh" title="Refresh from cloud">
|
|
294
|
+
<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" class="icon-sm">
|
|
295
|
+
<path d="M21 2v6h-6"/>
|
|
296
|
+
<path d="M3 12a9 9 0 0 1 15-6.7L21 8"/>
|
|
297
|
+
<path d="M3 22v-6h6"/>
|
|
298
|
+
<path d="M21 12a9 9 0 0 1-15 6.7L3 16"/>
|
|
299
|
+
</svg> Refresh
|
|
300
|
+
</button>
|
|
227
301
|
</div>
|
|
228
302
|
<div id="brand-skills-list"></div>
|
|
229
303
|
</div>
|
|
@@ -307,7 +381,10 @@
|
|
|
307
381
|
</div>
|
|
308
382
|
|
|
309
383
|
</main>
|
|
310
|
-
|
|
384
|
+
|
|
385
|
+
</div><!-- end #content-row -->
|
|
386
|
+
|
|
387
|
+
</div><!-- end #app -->
|
|
311
388
|
|
|
312
389
|
<!-- ── CONFIRMATION MODAL ────────────────────────────────────────────────── -->
|
|
313
390
|
<div id="modal-overlay" class="modal-overlay" style="display:none">
|
|
@@ -320,6 +397,7 @@
|
|
|
320
397
|
</div>
|
|
321
398
|
</div>
|
|
322
399
|
|
|
400
|
+
<script src="/theme.js"></script>
|
|
323
401
|
<script src="/ws.js"></script>
|
|
324
402
|
<script src="/sessions.js"></script>
|
|
325
403
|
<script src="/tasks.js"></script>
|
data/lib/clacky/web/onboard.js
CHANGED
|
@@ -66,29 +66,105 @@ const Onboard = (() => {
|
|
|
66
66
|
const res = await fetch("/api/providers");
|
|
67
67
|
const data = await res.json();
|
|
68
68
|
_providers = data.providers || [];
|
|
69
|
-
|
|
69
|
+
|
|
70
|
+
const dropdown = $("onboard-provider-dropdown");
|
|
70
71
|
_providers.forEach(p => {
|
|
71
|
-
const
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
72
|
+
const option = document.createElement("div");
|
|
73
|
+
option.className = "custom-select-option";
|
|
74
|
+
option.dataset.value = p.id;
|
|
75
|
+
option.textContent = p.name;
|
|
76
|
+
dropdown.appendChild(option);
|
|
75
77
|
});
|
|
78
|
+
|
|
79
|
+
// Bind custom dropdown events
|
|
80
|
+
_bindCustomDropdown();
|
|
76
81
|
} catch (_) { /* ignore */ }
|
|
77
82
|
}
|
|
78
83
|
|
|
79
|
-
function
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
84
|
+
function _bindCustomDropdown() {
|
|
85
|
+
const wrapper = $("onboard-provider-wrapper");
|
|
86
|
+
const trigger = wrapper.querySelector(".custom-select-trigger");
|
|
87
|
+
const dropdown = wrapper.querySelector(".custom-select-dropdown");
|
|
88
|
+
const valueSpan = trigger.querySelector(".custom-select-value");
|
|
89
|
+
const options = dropdown.querySelectorAll(".custom-select-option");
|
|
90
|
+
|
|
91
|
+
// Toggle dropdown
|
|
92
|
+
trigger.addEventListener("click", (e) => {
|
|
93
|
+
e.stopPropagation();
|
|
94
|
+
const isOpen = dropdown.classList.contains("open");
|
|
95
|
+
if (isOpen) {
|
|
96
|
+
dropdown.classList.remove("open");
|
|
97
|
+
trigger.classList.remove("open");
|
|
98
|
+
} else {
|
|
99
|
+
dropdown.classList.add("open");
|
|
100
|
+
trigger.classList.add("open");
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
// Select option
|
|
105
|
+
options.forEach(option => {
|
|
106
|
+
option.addEventListener("click", (e) => {
|
|
107
|
+
e.stopPropagation();
|
|
108
|
+
const value = option.dataset.value;
|
|
109
|
+
const text = option.textContent;
|
|
110
|
+
|
|
111
|
+
// Update UI
|
|
112
|
+
valueSpan.textContent = text;
|
|
113
|
+
if (value) {
|
|
114
|
+
valueSpan.classList.remove("placeholder");
|
|
115
|
+
} else {
|
|
116
|
+
valueSpan.classList.add("placeholder");
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Update selected state
|
|
120
|
+
options.forEach(opt => opt.classList.remove("selected"));
|
|
121
|
+
option.classList.add("selected");
|
|
122
|
+
|
|
123
|
+
// Close dropdown
|
|
124
|
+
dropdown.classList.remove("open");
|
|
125
|
+
trigger.classList.remove("open");
|
|
126
|
+
|
|
127
|
+
// Auto-fill model & base_url if a provider preset was selected
|
|
128
|
+
if (value) {
|
|
129
|
+
const preset = _providers.find(p => p.id === value);
|
|
130
|
+
if (preset) {
|
|
131
|
+
$("onboard-model").value = preset.default_model || "";
|
|
132
|
+
$("onboard-base-url").value = preset.base_url || "";
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
});
|
|
86
136
|
});
|
|
87
137
|
|
|
88
|
-
//
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
138
|
+
// Close dropdown when clicking outside
|
|
139
|
+
document.addEventListener("click", () => {
|
|
140
|
+
dropdown.classList.remove("open");
|
|
141
|
+
trigger.classList.remove("open");
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function _bindKeyPhase() {
|
|
146
|
+
// Toggle key visibility with icon change
|
|
147
|
+
const toggleKeyBtn = $("onboard-toggle-key");
|
|
148
|
+
const apiKeyInput = $("onboard-api-key");
|
|
149
|
+
const eyeIcon = toggleKeyBtn.querySelector("svg");
|
|
150
|
+
|
|
151
|
+
toggleKeyBtn.addEventListener("click", () => {
|
|
152
|
+
const isPassword = apiKeyInput.type === "password";
|
|
153
|
+
apiKeyInput.type = isPassword ? "text" : "password";
|
|
154
|
+
|
|
155
|
+
// Update icon
|
|
156
|
+
if (isPassword) {
|
|
157
|
+
// Show eye-off icon
|
|
158
|
+
eyeIcon.innerHTML = `
|
|
159
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13.875 18.825A10.05 10.05 0 0112 19c-4.478 0-8.268-2.943-9.543-7a9.97 9.97 0 011.563-3.029m5.858.908a3 3 0 114.243 4.243M9.878 9.878l4.242 4.242M9.88 9.88l-3.29-3.29m7.532 7.532l3.29 3.29M3 3l3.59 3.59m0 0A9.953 9.953 0 0112 5c4.478 0 8.268 2.943 9.543 7a10.025 10.025 0 01-4.132 5.411m0 0L21 21"></path>
|
|
160
|
+
`;
|
|
161
|
+
} else {
|
|
162
|
+
// Show eye icon
|
|
163
|
+
eyeIcon.innerHTML = `
|
|
164
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"></path>
|
|
165
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"></path>
|
|
166
|
+
`;
|
|
167
|
+
}
|
|
92
168
|
});
|
|
93
169
|
|
|
94
170
|
// Test & Continue
|
data/lib/clacky/web/sessions.js
CHANGED
|
@@ -23,6 +23,59 @@ const Sessions = (() => {
|
|
|
23
23
|
let _pendingRunTaskId = null; // session_id waiting to send "run_task" after subscribe
|
|
24
24
|
let _pendingMessage = null; // { session_id, content } — slash command to send after subscribe
|
|
25
25
|
|
|
26
|
+
// ── Thinking block parser ──────────────────────────────────────────────
|
|
27
|
+
//
|
|
28
|
+
// Converts <think>...</think> blocks in assistant messages into collapsible
|
|
29
|
+
// "Thinking" sections. The block is collapsed by default and can be
|
|
30
|
+
// expanded by clicking the header.
|
|
31
|
+
|
|
32
|
+
function _parseThinkingBlocks(rawHtml) {
|
|
33
|
+
// rawHtml is already HTML-escaped text (via escapeHtml). We need to detect
|
|
34
|
+
// the escaped versions of <think> and </think>.
|
|
35
|
+
const OPEN = "<think>";
|
|
36
|
+
const CLOSE = "</think>";
|
|
37
|
+
|
|
38
|
+
// Fast path: no thinking block present
|
|
39
|
+
if (!rawHtml.includes(OPEN)) return rawHtml;
|
|
40
|
+
|
|
41
|
+
let result = "";
|
|
42
|
+
let rest = rawHtml;
|
|
43
|
+
|
|
44
|
+
while (rest.includes(OPEN)) {
|
|
45
|
+
const openIdx = rest.indexOf(OPEN);
|
|
46
|
+
const closeIdx = rest.indexOf(CLOSE, openIdx + OPEN.length);
|
|
47
|
+
|
|
48
|
+
// Prepend any text before the <think> block
|
|
49
|
+
result += rest.slice(0, openIdx);
|
|
50
|
+
|
|
51
|
+
if (closeIdx === -1) {
|
|
52
|
+
// Unclosed <think> — treat remainder as plain text
|
|
53
|
+
result += rest.slice(openIdx);
|
|
54
|
+
rest = "";
|
|
55
|
+
break;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const thinkContent = rest.slice(openIdx + OPEN.length, closeIdx);
|
|
59
|
+
result += _buildThinkingBlock(thinkContent);
|
|
60
|
+
// Strip leading newlines after </think> to avoid blank space from pre-wrap
|
|
61
|
+
rest = rest.slice(closeIdx + CLOSE.length).replace(/^\n+/, "");
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
result += rest;
|
|
65
|
+
return result;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Build the collapsible thinking block HTML for a given (already-escaped) content string.
|
|
69
|
+
function _buildThinkingBlock(escapedContent) {
|
|
70
|
+
return `<details class="thinking-block">` +
|
|
71
|
+
`<summary class="thinking-summary">` +
|
|
72
|
+
`<span class="thinking-chevron">›</span>` +
|
|
73
|
+
`<span class="thinking-label">Thought for a moment</span>` +
|
|
74
|
+
`</summary>` +
|
|
75
|
+
`<div class="thinking-body">${escapedContent}</div>` +
|
|
76
|
+
`</details>`;
|
|
77
|
+
}
|
|
78
|
+
|
|
26
79
|
// ── Private helpers ────────────────────────────────────────────────────
|
|
27
80
|
|
|
28
81
|
function _cacheActiveMessages() {
|
|
@@ -151,7 +204,7 @@ const Sessions = (() => {
|
|
|
151
204
|
if (historyCtx.group) { _collapseToolGroup(historyCtx.group); historyCtx.group = null; }
|
|
152
205
|
const el = document.createElement("div");
|
|
153
206
|
el.className = "msg msg-assistant";
|
|
154
|
-
el.innerHTML = escapeHtml(ev.content || "");
|
|
207
|
+
el.innerHTML = _parseThinkingBlocks(escapeHtml(ev.content || ""));
|
|
155
208
|
container.appendChild(el);
|
|
156
209
|
break;
|
|
157
210
|
}
|
|
@@ -241,6 +294,21 @@ const Sessions = (() => {
|
|
|
241
294
|
messages.appendChild(frag);
|
|
242
295
|
messages.scrollTop = messages.scrollHeight;
|
|
243
296
|
}
|
|
297
|
+
|
|
298
|
+
// Restore transient UI state based on session status after initial load
|
|
299
|
+
// (not prepend, which is scroll-up pagination — no need to re-restore then)
|
|
300
|
+
if (!prepend) {
|
|
301
|
+
const session = _sessions.find(s => s.id === id);
|
|
302
|
+
if (session) {
|
|
303
|
+
if (session.status === "running") {
|
|
304
|
+
// Agent is still running (e.g. page was refreshed mid-task)
|
|
305
|
+
Sessions.showProgress("Thinking…");
|
|
306
|
+
} else if (session.status === "error" && session.error) {
|
|
307
|
+
// Show the stored error message at the end of history
|
|
308
|
+
Sessions.appendMsg("error", session.error);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
}
|
|
244
312
|
}
|
|
245
313
|
} finally {
|
|
246
314
|
state.loading = false;
|
|
@@ -401,7 +469,13 @@ const Sessions = (() => {
|
|
|
401
469
|
|
|
402
470
|
updateStatusBar(status) {
|
|
403
471
|
$("chat-status").textContent = status || "idle";
|
|
404
|
-
|
|
472
|
+
if (status === "running") {
|
|
473
|
+
$("chat-status").className = "status-running";
|
|
474
|
+
} else if (status === "error") {
|
|
475
|
+
$("chat-status").className = "status-error";
|
|
476
|
+
} else {
|
|
477
|
+
$("chat-status").className = "status-idle";
|
|
478
|
+
}
|
|
405
479
|
$("btn-interrupt").style.display = status === "running" ? "" : "none";
|
|
406
480
|
},
|
|
407
481
|
|
|
@@ -447,7 +521,8 @@ const Sessions = (() => {
|
|
|
447
521
|
const messages = $("messages");
|
|
448
522
|
const el = document.createElement("div");
|
|
449
523
|
el.className = `msg msg-${type}`;
|
|
450
|
-
|
|
524
|
+
// Parse thinking blocks out of assistant messages
|
|
525
|
+
el.innerHTML = type === "assistant" ? _parseThinkingBlocks(html) : html;
|
|
451
526
|
messages.appendChild(el);
|
|
452
527
|
messages.scrollTop = messages.scrollHeight;
|
|
453
528
|
},
|