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.
@@ -478,8 +478,27 @@ function sendMessage() {
478
478
  }
479
479
 
480
480
  // ── DOM event listeners ───────────────────────────────────────────────────
481
- $("btn-new-session").addEventListener("click", () => Sessions.create());
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
- if (input.value === "" || input.value === "/") {
510
- input.value = "/";
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
- let _composing = false;
548
- $("user-input").addEventListener("compositionstart", () => { _composing = true; });
549
- $("user-input").addEventListener("compositionend", () => { _composing = false; });
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 && !_composing) {
742
+ if (e.key === "Enter" && !e.shiftKey && !e.isComposing) {
724
743
  e.preventDefault();
725
744
  sendMessage();
726
745
  }
@@ -9,35 +9,69 @@
9
9
  <body>
10
10
  <div id="app">
11
11
 
12
- <!-- ── SIDEBAR ──────────────────────────────────────────────────────── -->
13
- <aside id="sidebar">
14
- <div id="sidebar-header">
15
- <span class="logo" id="sidebar-logo">{{BRAND_NAME}}</span>
16
- <button id="btn-new-session" title="New Session">+</button>
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
- <!-- Combined list: sessions + tasks -->
35
+ <!-- ── SIDEBAR ──────────────────────────────────────────────────────── -->
36
+ <aside id="sidebar">
37
+ <!-- Sidebar navigation groups -->
20
38
  <div id="sidebar-list">
21
- <div id="session-list"></div>
22
- <div id="tasks-section">
23
- <div class="sidebar-divider"><span>Scheduled Tasks</span></div>
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
- <span class="task-icon">⏰</span>
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
- <span class="task-icon">🧩</span>
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 nav -->
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
- <select id="onboard-provider-select" class="onboard-select">
87
- <option value="">— Choose provider —</option>
88
- </select>
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">👁</button>
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
- <header id="task-detail-header">
160
- <span id="task-detail-title">Scheduled Tasks</span>
161
- <button id="btn-create-task" title="Create a new task">+ Create Task</button>
162
- </header>
163
- <div id="task-list-table"></div>
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">↻ Refresh</button>
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
- </div>
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>
@@ -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
- const sel = $("onboard-provider-select");
69
+
70
+ const dropdown = $("onboard-provider-dropdown");
70
71
  _providers.forEach(p => {
71
- const opt = document.createElement("option");
72
- opt.value = p.id;
73
- opt.textContent = p.name;
74
- sel.appendChild(opt);
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 _bindKeyPhase() {
80
- // Provider quick-fill
81
- $("onboard-provider-select").addEventListener("change", e => {
82
- const preset = _providers.find(p => p.id === e.target.value);
83
- if (!preset) return;
84
- $("onboard-model").value = preset.default_model || "";
85
- $("onboard-base-url").value = preset.base_url || "";
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
- // Toggle key visibility
89
- $("onboard-toggle-key").addEventListener("click", () => {
90
- const inp = $("onboard-api-key");
91
- inp.type = inp.type === "password" ? "text" : "password";
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
@@ -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 = "&lt;think&gt;";
36
+ const CLOSE = "&lt;/think&gt;";
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
- $("chat-status").className = status === "running" ? "status-running" : "status-idle";
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
- el.innerHTML = html;
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
  },