leedab 0.2.0 → 0.2.2

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.
@@ -65,7 +65,8 @@ export function createRoutes(config) {
65
65
  result.text ??
66
66
  result.content ??
67
67
  stdout.trim();
68
- json(res, { reply, session: session ?? "console" });
68
+ const thoughts = await readLatestThoughts(stateDir, session ?? "console");
69
+ json(res, { reply, thoughts, session: session ?? "console" });
69
70
  }
70
71
  catch {
71
72
  json(res, {
@@ -88,6 +89,7 @@ export function createRoutes(config) {
88
89
  const jsonlPath = resolve(sessionsDir, `${session}.jsonl`);
89
90
  const raw = await readFile(jsonlPath, "utf-8");
90
91
  const messages = [];
92
+ let pendingThoughts = [];
91
93
  for (const line of raw.split("\n")) {
92
94
  if (!line.trim())
93
95
  continue;
@@ -99,11 +101,16 @@ export function createRoutes(config) {
99
101
  if (!msg || (msg.role !== "user" && msg.role !== "assistant"))
100
102
  continue;
101
103
  let text = "";
104
+ const theseThoughts = [];
102
105
  if (Array.isArray(msg.content)) {
103
- text = msg.content
104
- .filter((b) => b.type === "text")
105
- .map((b) => b.text)
106
- .join("\n");
106
+ for (const b of msg.content) {
107
+ if (b?.type === "text" && typeof b.text === "string") {
108
+ text += (text ? "\n" : "") + b.text;
109
+ }
110
+ else if (b?.type === "thinking" && typeof b.thinking === "string") {
111
+ theseThoughts.push(b.thinking);
112
+ }
113
+ }
107
114
  }
108
115
  else if (typeof msg.content === "string") {
109
116
  text = msg.content;
@@ -111,12 +118,22 @@ export function createRoutes(config) {
111
118
  // Strip all OpenClaw "(untrusted metadata)" blocks and timestamp prefix
112
119
  text = text.replace(/^(\w[\w\s]*\(untrusted metadata\):\n```json\n[\s\S]*?\n```\n\n)+/, "");
113
120
  text = text.replace(/^\[[\w]{3} \d{4}-\d{2}-\d{2} \d{2}:\d{2} \w+\] /, "");
114
- if (text) {
115
- messages.push({
116
- role: msg.role,
117
- text,
118
- timestamp: entry.timestamp,
119
- });
121
+ if (msg.role === "assistant") {
122
+ // Accumulate thoughts across tool-use turns until we see text
123
+ pendingThoughts.push(...theseThoughts);
124
+ if (text) {
125
+ messages.push({
126
+ role: msg.role,
127
+ text,
128
+ thoughts: pendingThoughts.length ? pendingThoughts : undefined,
129
+ timestamp: entry.timestamp,
130
+ });
131
+ pendingThoughts = [];
132
+ }
133
+ }
134
+ else if (text) {
135
+ messages.push({ role: msg.role, text, timestamp: entry.timestamp });
136
+ pendingThoughts = [];
120
137
  }
121
138
  }
122
139
  catch { }
@@ -703,6 +720,49 @@ function parseSessionFirstMessage(text) {
703
720
  userText = userText.replace(/\nUntrusted context \(metadata[\s\S]*$/, "");
704
721
  return { senderName, cleanText: userText.trim() };
705
722
  }
723
+ /**
724
+ * Walk the session JSONL backwards and collect `thinking` block content from
725
+ * the most recent assistant turn(s) since the last user message. Returns an
726
+ * ordered list of thought strings (oldest → newest).
727
+ */
728
+ async function readLatestThoughts(stateDir, session) {
729
+ try {
730
+ const jsonlPath = resolve(stateDir, "agents", "main", "sessions", `${session}.jsonl`);
731
+ const raw = await readFile(jsonlPath, "utf-8");
732
+ const lines = raw.split("\n").filter((l) => l.trim());
733
+ const thoughts = [];
734
+ for (let i = lines.length - 1; i >= 0; i--) {
735
+ let entry;
736
+ try {
737
+ entry = JSON.parse(lines[i]);
738
+ }
739
+ catch {
740
+ continue;
741
+ }
742
+ if (entry.type !== "message")
743
+ continue;
744
+ const msg = entry.message;
745
+ if (!msg)
746
+ continue;
747
+ // Stop once we hit the user message that triggered this turn.
748
+ if (msg.role === "user")
749
+ break;
750
+ if (msg.role !== "assistant")
751
+ continue;
752
+ if (!Array.isArray(msg.content))
753
+ continue;
754
+ for (const block of msg.content) {
755
+ if (block?.type === "thinking" && typeof block.thinking === "string") {
756
+ thoughts.unshift(block.thinking);
757
+ }
758
+ }
759
+ }
760
+ return thoughts;
761
+ }
762
+ catch {
763
+ return [];
764
+ }
765
+ }
706
766
  function json(res, data, status = 200) {
707
767
  res.writeHead(status, { "Content-Type": "application/json" });
708
768
  res.end(JSON.stringify(data));
@@ -11,7 +11,7 @@
11
11
  <style>
12
12
  .page-header { display:flex; align-items:center; justify-content:space-between; padding:0 20px; height:52px; border-bottom:1px solid var(--border); background:var(--bg); }
13
13
  .page-header-left { display:flex; align-items:center; gap:10px; }
14
- .page-header-title { font-size:14px; font-weight:600; letter-spacing:-0.01em; }
14
+ .page-header-title { font-size:13px; font-weight:700; letter-spacing:-0.01em; }
15
15
  .page-nav { display:flex; align-items:center; gap:2px; }
16
16
  .page-nav a, .page-nav button { color:var(--text-dim); text-decoration:none; display:flex; align-items:center; gap:5px; padding:6px 12px; border-radius:8px; font-size:13px; font-weight:450; transition:all 0.15s; background:none; border:none; cursor:pointer; font-family:inherit; }
17
17
  .page-nav a:hover, .page-nav button:hover { color:var(--text-secondary); background:var(--surface-raised); }
@@ -75,8 +75,8 @@
75
75
  }
76
76
 
77
77
  .header-title {
78
- font-size: 14px;
79
- font-weight: 600;
78
+ font-size: 13px;
79
+ font-weight: 700;
80
80
  letter-spacing: -0.01em;
81
81
  }
82
82
 
@@ -302,6 +302,39 @@
302
302
  font-size: 13px;
303
303
  }
304
304
 
305
+ /* Thoughts (collapsible, shown before final answer) */
306
+ .thoughts {
307
+ margin-bottom: 8px;
308
+ border: 1px solid var(--border);
309
+ border-radius: var(--radius);
310
+ background: var(--surface);
311
+ font-size: 13px;
312
+ }
313
+ .thoughts > summary {
314
+ cursor: pointer;
315
+ list-style: none;
316
+ padding: 8px 12px;
317
+ color: var(--text-dim);
318
+ display: flex;
319
+ align-items: center;
320
+ gap: 6px;
321
+ user-select: none;
322
+ }
323
+ .thoughts > summary::-webkit-details-marker { display: none; }
324
+ .thoughts > summary::before {
325
+ content: "▸";
326
+ font-size: 10px;
327
+ transition: transform 0.15s;
328
+ }
329
+ .thoughts[open] > summary::before { transform: rotate(90deg); }
330
+ .thoughts .thought {
331
+ padding: 8px 12px;
332
+ border-top: 1px solid var(--border);
333
+ color: var(--text-secondary);
334
+ white-space: pre-wrap;
335
+ line-height: 1.5;
336
+ }
337
+
305
338
  .thinking-spinner {
306
339
  width: 14px;
307
340
  height: 14px;
@@ -676,7 +709,7 @@
676
709
  if (data.error) {
677
710
  appendMessage("Something went wrong. Is the gateway running?", "agent");
678
711
  } else {
679
- appendMessage(data.reply, "agent");
712
+ appendMessage(data.reply, "agent", data.thoughts);
680
713
  }
681
714
  } catch (err) {
682
715
  thinkingEl.remove();
@@ -695,7 +728,7 @@
695
728
  if (emptyState) emptyState.remove();
696
729
  for (const m of messages) {
697
730
  const type = m.role === "user" ? "user" : "agent";
698
- appendMessage(m.text || m.content || "", type);
731
+ appendMessage(m.text || m.content || "", type, m.thoughts);
699
732
  }
700
733
  }
701
734
  } catch {}
@@ -705,7 +738,7 @@
705
738
  return new Date().toLocaleTimeString("en-US", { hour: "numeric", minute: "2-digit" });
706
739
  }
707
740
 
708
- function appendMessage(text, type) {
741
+ function appendMessage(text, type, thoughts) {
709
742
  const row = document.createElement("div");
710
743
  row.className = `msg-row ${type}`;
711
744
 
@@ -728,6 +761,23 @@
728
761
  meta.className = "msg-meta";
729
762
  meta.innerHTML = `<span class="msg-name">${type === "agent" ? "LeedAB" : userName}</span><span class="msg-time">${now()}</span>`;
730
763
 
764
+ content.appendChild(meta);
765
+
766
+ if (type === "agent" && Array.isArray(thoughts) && thoughts.length) {
767
+ const details = document.createElement("details");
768
+ details.className = "thoughts";
769
+ const summary = document.createElement("summary");
770
+ summary.textContent = `Thought for a moment · ${thoughts.length} step${thoughts.length === 1 ? "" : "s"}`;
771
+ details.appendChild(summary);
772
+ for (const t of thoughts) {
773
+ const div = document.createElement("div");
774
+ div.className = "thought";
775
+ div.textContent = t;
776
+ details.appendChild(div);
777
+ }
778
+ content.appendChild(details);
779
+ }
780
+
731
781
  const bubble = document.createElement("div");
732
782
  bubble.className = `msg-bubble ${type}`;
733
783
  if (type === "agent") {
@@ -736,7 +786,6 @@
736
786
  bubble.textContent = text;
737
787
  }
738
788
 
739
- content.appendChild(meta);
740
789
  content.appendChild(bubble);
741
790
  row.appendChild(avatar);
742
791
  row.appendChild(content);
@@ -11,7 +11,7 @@
11
11
  <style>
12
12
  .page-header { display:flex; align-items:center; justify-content:space-between; padding:0 20px; height:52px; border-bottom:1px solid var(--border); background:var(--bg); }
13
13
  .page-header-left { display:flex; align-items:center; gap:10px; }
14
- .page-header-title { font-size:14px; font-weight:600; letter-spacing:-0.01em; }
14
+ .page-header-title { font-size:13px; font-weight:700; letter-spacing:-0.01em; }
15
15
  .page-nav { display:flex; align-items:center; gap:2px; }
16
16
  .page-nav a, .page-nav button { color:var(--text-dim); text-decoration:none; display:flex; align-items:center; gap:5px; padding:6px 12px; border-radius:8px; font-size:13px; font-weight:450; transition:all 0.15s; background:none; border:none; cursor:pointer; font-family:inherit; }
17
17
  .page-nav a:hover, .page-nav button:hover { color:var(--text-secondary); background:var(--surface-raised); }
@@ -23,10 +23,6 @@
23
23
  <a href="/" class="page-header-title" style="text-decoration:none;color:inherit">LeedAB</a>
24
24
  </div>
25
25
  <div class="page-nav">
26
- <button class="theme-btn" onclick="toggleTheme()" title="Toggle theme">
27
- <svg id="theme-icon-sun" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="5"/><line x1="12" y1="1" x2="12" y2="3"/><line x1="12" y1="21" x2="12" y2="23"/><line x1="4.22" y1="4.22" x2="5.64" y2="5.64"/><line x1="18.36" y1="18.36" x2="19.78" y2="19.78"/><line x1="1" y1="12" x2="3" y2="12"/><line x1="21" y1="12" x2="23" y2="12"/><line x1="4.22" y1="19.78" x2="5.64" y2="18.36"/><line x1="18.36" y1="5.64" x2="19.78" y2="4.22"/></svg>
28
- <svg id="theme-icon-moon" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="display:none"><path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/></svg>
29
- </button>
30
26
  </div>
31
27
  </div>
32
28
 
@@ -106,22 +102,6 @@
106
102
  function initTheme() {
107
103
  const saved = localStorage.getItem("leedab-theme") || "dark";
108
104
  document.documentElement.setAttribute("data-theme", saved);
109
- updateThemeIcon(saved);
110
- }
111
- function toggleTheme() {
112
- const current = document.documentElement.getAttribute("data-theme") || "dark";
113
- const next = current === "dark" ? "light" : "dark";
114
- document.documentElement.setAttribute("data-theme", next);
115
- localStorage.setItem("leedab-theme", next);
116
- updateThemeIcon(next);
117
- }
118
- function updateThemeIcon(theme) {
119
- const sun = document.getElementById("theme-icon-sun");
120
- const moon = document.getElementById("theme-icon-moon");
121
- if (sun && moon) {
122
- sun.style.display = theme === "dark" ? "block" : "none";
123
- moon.style.display = theme === "light" ? "block" : "none";
124
- }
125
105
  }
126
106
  initTheme();
127
107
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "leedab",
3
- "version": "0.2.0",
3
+ "version": "0.2.2",
4
4
  "description": "LeedAB — Your enterprise AI agent. Local-first, private by default.",
5
5
  "license": "SEE LICENSE IN LICENSE",
6
6
  "author": "LeedAB <hello@leedab.com>",