@cremini/skillpack 1.0.8 → 1.0.9-im.1

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.
Files changed (33) hide show
  1. package/README.md +3 -1
  2. package/dist/cli.js +45 -11
  3. package/package.json +3 -2
  4. package/runtime/server/dist/adapters/markdown.js +74 -0
  5. package/runtime/server/dist/adapters/markdown.js.map +1 -0
  6. package/runtime/server/dist/adapters/slack.js +369 -0
  7. package/runtime/server/dist/adapters/slack.js.map +1 -0
  8. package/runtime/server/dist/adapters/telegram.js +199 -0
  9. package/runtime/server/dist/adapters/telegram.js.map +1 -0
  10. package/runtime/server/dist/adapters/types.js +2 -0
  11. package/runtime/server/dist/adapters/types.js.map +1 -0
  12. package/runtime/server/dist/adapters/web.js +168 -0
  13. package/runtime/server/dist/adapters/web.js.map +1 -0
  14. package/runtime/server/dist/agent.js +219 -0
  15. package/runtime/server/dist/agent.js.map +1 -0
  16. package/runtime/server/dist/config.js +73 -0
  17. package/runtime/server/dist/config.js.map +1 -0
  18. package/runtime/server/dist/index.js +122 -0
  19. package/runtime/server/dist/index.js.map +1 -0
  20. package/runtime/server/package-lock.json +2768 -121
  21. package/runtime/server/package.json +13 -3
  22. package/runtime/start.bat +2 -2
  23. package/runtime/start.sh +1 -1
  24. package/runtime/web/index.html +58 -15
  25. package/runtime/web/js/api.js +13 -0
  26. package/runtime/web/{app.js → js/chat.js} +126 -193
  27. package/runtime/web/js/config.js +15 -0
  28. package/runtime/web/js/main.js +47 -0
  29. package/runtime/web/js/settings.js +132 -0
  30. package/runtime/web/styles.css +171 -0
  31. package/runtime/server/chat-proxy.js +0 -161
  32. package/runtime/server/index.js +0 -63
  33. package/runtime/server/routes.js +0 -104
@@ -3,11 +3,21 @@
3
3
  "version": "1.0.0",
4
4
  "type": "module",
5
5
  "scripts": {
6
- "start": "node index.js"
6
+ "build": "tsc",
7
+ "start": "node dist/index.js"
7
8
  },
8
9
  "dependencies": {
9
- "express": "^5.1.0",
10
10
  "@mariozechner/pi-coding-agent": "^0.57.1",
11
+ "@slack/bolt": "^4.6.0",
12
+ "express": "^5.1.0",
13
+ "node-telegram-bot-api": "^0.66.0",
11
14
  "ws": "^8.19.0"
15
+ },
16
+ "devDependencies": {
17
+ "@types/express": "^5.0.0",
18
+ "@types/node": "^22.0.0",
19
+ "@types/node-telegram-bot-api": "^0.64.0",
20
+ "@types/ws": "^8.18.0",
21
+ "typescript": "^5.9.3"
12
22
  }
13
- }
23
+ }
package/runtime/start.bat CHANGED
@@ -11,5 +11,5 @@ if not exist "server\node_modules" (
11
11
  echo.
12
12
  )
13
13
 
14
- rem Start the server (port detection and browser launch are handled by server\index.js)
15
- cd server && node index.js
14
+ rem Start the server (port detection and browser launch are handled by server\dist\index.js)
15
+ cd server && node dist/index.js
package/runtime/start.sh CHANGED
@@ -19,4 +19,4 @@ if [ ! -d "server/node_modules" ]; then
19
19
  fi
20
20
 
21
21
  # Start the server
22
- cd server && node index.js
22
+ cd server && node dist/index.js
@@ -25,20 +25,10 @@
25
25
  </div>
26
26
 
27
27
  <div class="sidebar-section">
28
- <h3>API KEYS</h3>
29
- <div class="provider-select-wrapper">
30
- <select id="provider-select">
31
- <option value="openai">OpenAI</option>
32
- <option value="anthropic">Anthropic</option>
33
- </select>
34
- </div>
35
- <div class="api-key-form">
36
- <input type="password" id="api-key-input" placeholder="sk-..." />
37
- </div>
38
- <div class="api-key-footer">
39
- <p id="key-status" class="status-text"></p>
40
- <button id="save-key-btn">Save</button>
41
- </div>
28
+ <button id="open-settings-btn" class="settings-trigger-btn">
29
+ <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="btn-icon"><circle cx="12" cy="12" r="3"></circle><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"></path></svg>
30
+ Settings
31
+ </button>
42
32
  </div>
43
33
  </aside>
44
34
 
@@ -65,8 +55,61 @@
65
55
  </div>
66
56
  </main>
67
57
  </div>
58
+
59
+ <!-- Settings Dialog -->
60
+ <dialog id="settings-dialog" class="settings-modal">
61
+ <div class="settings-modal-header">
62
+ <h2>Settings</h2>
63
+ <button id="close-settings-btn" class="close-btn" aria-label="Close">
64
+ <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line></svg>
65
+ </button>
66
+ </div>
67
+ <div class="settings-modal-body">
68
+ <div class="settings-sections">
69
+ <!-- General Section -->
70
+ <div class="settings-section">
71
+ <h3 class="section-title">General</h3>
72
+ <div class="form-group">
73
+ <label>Provider</label>
74
+ <div class="provider-select-wrapper">
75
+ <select id="provider-select">
76
+ <option value="openai">OpenAI</option>
77
+ <option value="anthropic">Anthropic</option>
78
+ </select>
79
+ </div>
80
+ </div>
81
+ <div class="form-group">
82
+ <label>API Key</label>
83
+ <input type="password" id="api-key-input" placeholder="sk-..." class="form-input" />
84
+ <p id="key-status" class="status-text" style="margin-top: 8px; min-height: 18px;"></p>
85
+ </div>
86
+ </div>
87
+
88
+ <!-- IM Bots Section -->
89
+ <div class="settings-section">
90
+ <h3 class="section-title">IM Bots</h3>
91
+ <div class="form-group">
92
+ <label>Telegram Bot Token</label>
93
+ <input type="password" id="telegram-token-input" placeholder="123456:ABC-DEF..." class="form-input" />
94
+ </div>
95
+ <div class="form-group">
96
+ <label>Slack Bot Token</label>
97
+ <input type="password" id="slack-bot-token-input" placeholder="xoxb-..." class="form-input" />
98
+ </div>
99
+ <div class="form-group">
100
+ <label>Slack App Token</label>
101
+ <input type="password" id="slack-app-token-input" placeholder="xapp-..." class="form-input" />
102
+ </div>
103
+ </div>
104
+ </div>
105
+ </div>
106
+ <div class="settings-modal-footer">
107
+ <button id="save-settings-btn" class="primary-btn">Save Settings</button>
108
+ </div>
109
+ </dialog>
110
+
68
111
  <script src="marked.min.js"></script>
69
- <script src="app.js"></script>
112
+ <script type="module" src="js/main.js"></script>
70
113
  </body>
71
114
 
72
115
  </html>
@@ -0,0 +1,13 @@
1
+ import { state } from "./config.js";
2
+
3
+ export async function saveConfigData(updates) {
4
+ const res = await fetch(state.API_BASE + "/api/config/update", {
5
+ method: "POST",
6
+ headers: { "Content-Type": "application/json" },
7
+ body: JSON.stringify(updates),
8
+ });
9
+ if (!res.ok) {
10
+ throw new Error("Save Config Failed");
11
+ }
12
+ return await res.json();
13
+ }
@@ -1,69 +1,47 @@
1
- const API_BASE = "";
2
- let chatHistory = [];
1
+ import { state } from "./config.js";
3
2
 
4
- // Initialize
5
- async function init() {
6
- await loadConfig();
7
- setupEventListeners();
8
- }
9
-
10
- async function loadConfig() {
11
- try {
12
- const res = await fetch(API_BASE + "/api/config");
13
- const config = await res.json();
14
-
15
- document.getElementById("pack-name").textContent = config.name;
16
- document.getElementById("pack-desc").textContent = config.description;
17
- document.title = config.name;
18
-
19
- // Skills
20
- const skillsList = document.getElementById("skills-list");
21
- skillsList.innerHTML = config.skills
22
- .map(
23
- (s) =>
24
- `<li><div class="skill-name">${s.name}</div><div class="skill-desc">${s.description}</div></li>`,
25
- )
26
- .join("");
27
-
28
- // Pre-fill when there is exactly one prompt
29
- if (config.prompts && config.prompts.length === 1) {
30
- const input = document.getElementById("user-input");
31
- input.value = config.prompts[0];
32
- input.style.height = "auto";
33
- input.style.height = Math.min(input.scrollHeight, 120) + "px";
34
- }
3
+ export const chatHistory = [];
4
+ let ws = null;
5
+ let currentAssistantMsg = null;
35
6
 
36
- // API key status and provider
37
- const keyStatus = document.getElementById("key-status");
38
- if (config.hasApiKey) {
39
- keyStatus.textContent = "API key configured";
40
- keyStatus.className = "status-text success";
41
- }
7
+ export function initChat() {
8
+ // Send button
9
+ document.getElementById("send-btn").addEventListener("click", sendMessage);
42
10
 
43
- const providerSelect = document.getElementById("provider-select");
44
- if (providerSelect && config.provider) {
45
- providerSelect.value = config.provider;
11
+ // Send on Enter
12
+ document.getElementById("user-input").addEventListener("keydown", (e) => {
13
+ if (e.key === "Enter" && !e.shiftKey) {
14
+ e.preventDefault();
15
+ sendMessage();
46
16
  }
17
+ });
47
18
 
48
- const updatePlaceholder = () => {
49
- const p = providerSelect.value;
50
- const input = document.getElementById("api-key-input");
51
- if (p === "openai") input.placeholder = "sk-proj-...";
52
- else if (p === "anthropic") input.placeholder = "sk-ant-api03-...";
53
- else input.placeholder = "sk-...";
54
- };
19
+ // Auto-resize the input box
20
+ document.getElementById("user-input").addEventListener("input", (e) => {
21
+ e.target.style.height = "auto";
22
+ e.target.style.height = Math.min(e.target.scrollHeight, 120) + "px";
23
+ });
55
24
 
56
- providerSelect.addEventListener("change", updatePlaceholder);
57
- updatePlaceholder();
25
+ // Prompt click (Welcome message prompts)
26
+ const welcomeContent = document.getElementById("welcome-content");
27
+ if (welcomeContent) {
28
+ welcomeContent.addEventListener("click", (e) => {
29
+ const item = e.target.closest(".prompt-card");
30
+ if (!item) return;
31
+ const index = parseInt(item.dataset.index);
58
32
 
59
- // Show welcome view
60
- showWelcome(config);
61
- } catch (err) {
62
- console.error("Failed to load config:", err);
33
+ if (state.config && state.config.prompts[index]) {
34
+ const input = document.getElementById("user-input");
35
+ input.value = state.config.prompts[index];
36
+ input.focus();
37
+ input.style.height = "auto";
38
+ input.style.height = Math.min(input.scrollHeight, 120) + "px";
39
+ }
40
+ });
63
41
  }
64
42
  }
65
43
 
66
- function showWelcome(config) {
44
+ export function showWelcome(config) {
67
45
  const welcomeContent = document.getElementById("welcome-content");
68
46
 
69
47
  let promptsHtml = "";
@@ -94,94 +72,69 @@ function showWelcome(config) {
94
72
  }
95
73
  }
96
74
 
97
- function setupEventListeners() {
98
- // Send button
99
- document.getElementById("send-btn").addEventListener("click", sendMessage);
75
+ export async function sendMessage() {
76
+ const input = document.getElementById("user-input");
77
+ const text = input.value.trim();
78
+ if (!text) return;
100
79
 
101
- // Send on Enter
102
- document.getElementById("user-input").addEventListener("keydown", (e) => {
103
- if (e.key === "Enter" && !e.shiftKey) {
104
- e.preventDefault();
105
- sendMessage();
106
- }
107
- });
80
+ const chatArea = document.getElementById("chat-area");
81
+ if (chatArea.classList.contains("mode-welcome")) {
82
+ chatArea.classList.remove("mode-welcome");
83
+ chatArea.classList.add("mode-chat");
84
+ }
108
85
 
109
- // Auto-resize the input box
110
- document.getElementById("user-input").addEventListener("input", (e) => {
111
- e.target.style.height = "auto";
112
- e.target.style.height = Math.min(e.target.scrollHeight, 120) + "px";
113
- });
86
+ input.value = "";
87
+ input.style.height = "auto";
114
88
 
115
- // Save API key
116
- document.getElementById("save-key-btn").addEventListener("click", saveApiKey);
89
+ appendMessage("user", text);
90
+ chatHistory.push({ role: "user", content: text });
117
91
 
118
- // Prompt click
119
- const welcomeContent = document.getElementById("welcome-content");
120
- if (welcomeContent) {
121
- welcomeContent.addEventListener("click", (e) => {
122
- const item = e.target.closest(".prompt-card");
123
- if (!item) return;
124
- const index = parseInt(item.dataset.index);
92
+ const sendBtn = document.getElementById("send-btn");
93
+ sendBtn.disabled = true;
125
94
 
126
- // Get the full prompt text
127
- fetch(API_BASE + "/api/config")
128
- .then((r) => r.json())
129
- .then((config) => {
130
- if (config.prompts[index]) {
131
- const input = document.getElementById("user-input");
132
- input.value = config.prompts[index];
133
- input.focus();
134
- input.style.height = "auto";
135
- input.style.height = Math.min(input.scrollHeight, 120) + "px";
136
- }
137
- });
138
- });
95
+ currentAssistantMsg = appendMessage("assistant", "");
96
+ showLoadingIndicator();
97
+
98
+ try {
99
+ const socket = await getOrCreateWs();
100
+ socket.send(JSON.stringify({ text }));
101
+ } catch (err) {
102
+ handleError(err.message);
139
103
  }
140
104
  }
141
105
 
142
- async function saveApiKey() {
143
- const input = document.getElementById("api-key-input");
144
- const providerSelect = document.getElementById("provider-select");
145
- const status = document.getElementById("key-status");
146
- const key = input.value.trim();
147
- const provider = providerSelect ? providerSelect.value : "openai";
148
-
149
- if (!key) {
150
- status.textContent = "Enter an API key";
151
- status.className = "status-text error";
152
- return;
106
+ export async function sendBotCommand(cmdText) {
107
+ const chatArea = document.getElementById("chat-area");
108
+ if (chatArea.classList.contains("mode-welcome")) {
109
+ chatArea.classList.remove("mode-welcome");
110
+ chatArea.classList.add("mode-chat");
153
111
  }
154
112
 
155
- try {
156
- const res = await fetch(API_BASE + "/api/config/key", {
157
- method: "POST",
158
- headers: { "Content-Type": "application/json" },
159
- body: JSON.stringify({ key, provider }),
160
- });
113
+ appendMessage("user", cmdText);
114
+ chatHistory.push({ role: "user", content: cmdText });
161
115
 
162
- if (res.ok) {
163
- status.textContent = "API key saved";
164
- status.className = "status-text success";
165
- input.value = "";
166
- } else {
167
- status.textContent = "Save failed";
168
- status.className = "status-text error";
169
- }
116
+ const sendBtn = document.getElementById("send-btn");
117
+ sendBtn.disabled = true;
118
+
119
+ currentAssistantMsg = appendMessage("assistant", "");
120
+ showLoadingIndicator();
121
+
122
+ try {
123
+ const socket = await getOrCreateWs();
124
+ socket.send(JSON.stringify({ text: cmdText }));
170
125
  } catch (err) {
171
- status.textContent = "Save failed: " + err.message;
172
- status.className = "status-text error";
126
+ handleError(err.message);
173
127
  }
174
128
  }
175
129
 
176
- let ws = null;
177
- let currentAssistantMsg = null;
130
+ // ========== Internal Rendering & WS logic ==========
178
131
 
179
132
  function renderMarkdown(mdText, { renderEmbeddedMarkdown = true } = {}) {
180
- if (typeof marked === "undefined") {
133
+ if (typeof window.marked === "undefined") {
181
134
  return escapeHtml(mdText);
182
135
  }
183
136
 
184
- const html = marked.parse(mdText);
137
+ const html = ensureLinksOpenInNewTab(window.marked.parse(mdText));
185
138
  if (!renderEmbeddedMarkdown) {
186
139
  return html;
187
140
  }
@@ -189,6 +142,18 @@ function renderMarkdown(mdText, { renderEmbeddedMarkdown = true } = {}) {
189
142
  return renderEmbeddedMarkdownBlocks(html);
190
143
  }
191
144
 
145
+ function ensureLinksOpenInNewTab(html) {
146
+ const template = document.createElement("template");
147
+ template.innerHTML = html;
148
+
149
+ template.content.querySelectorAll("a[href]").forEach((linkEl) => {
150
+ linkEl.setAttribute("target", "_blank");
151
+ linkEl.setAttribute("rel", "noopener noreferrer");
152
+ });
153
+
154
+ return template.innerHTML;
155
+ }
156
+
192
157
  function renderEmbeddedMarkdownBlocks(html) {
193
158
  const template = document.createElement("template");
194
159
  template.innerHTML = html;
@@ -196,11 +161,9 @@ function renderEmbeddedMarkdownBlocks(html) {
196
161
  const codeBlocks = template.content.querySelectorAll("pre > code");
197
162
  codeBlocks.forEach((codeEl) => {
198
163
  const languageClass = Array.from(codeEl.classList).find((className) =>
199
- className.startsWith("language-"),
164
+ className.startsWith("language-")
200
165
  );
201
- const language = languageClass
202
- ? languageClass.slice("language-".length)
203
- : "";
166
+ const language = languageClass ? languageClass.slice("language-".length) : "";
204
167
 
205
168
  if (!/^(markdown|md)$/i.test(language)) {
206
169
  return;
@@ -228,11 +191,9 @@ async function getOrCreateWs() {
228
191
 
229
192
  return new Promise((resolve, reject) => {
230
193
  const protocol = window.location.protocol === "https:" ? "wss:" : "ws:";
231
- const providerSelect = document.getElementById("provider-select");
232
- const provider = providerSelect ? providerSelect.value : "openai";
194
+ const provider = state.config && state.config.provider ? state.config.provider : "openai";
233
195
 
234
- // URLSearchParams would be cleaner if more query params are added later
235
- const wsUrl = `${protocol}//${window.location.host}${API_BASE}/api/chat?provider=${provider}`;
196
+ const wsUrl = `${protocol}//${window.location.host}${state.API_BASE}/api/chat?provider=${provider}`;
236
197
 
237
198
  ws = new WebSocket(wsUrl);
238
199
 
@@ -315,12 +276,27 @@ function handleAgentEvent(event) {
315
276
 
316
277
  if (
317
278
  ["text_delta", "thinking_delta", "tool_start", "tool_end"].includes(
318
- event.type,
279
+ event.type
319
280
  )
320
281
  ) {
321
282
  hideLoadingIndicator();
322
283
  }
323
284
 
285
+ // Handle bot command results injected by the backend WebSocket response
286
+ if (event.type === "command_result") {
287
+ const textBlock = getOrCreateTextBlock();
288
+ let resText = `Command \`${event.command}\` succeeded.`;
289
+ if (event.errorMessage) {
290
+ resText = `Command \`${event.command}\` failed: ${event.errorMessage}`;
291
+ } else if (event.result) {
292
+ resText = `Command \`${event.command}\` result:\n\n${event.result}`;
293
+ }
294
+ textBlock.dataset.mdContent += resText;
295
+ textBlock.innerHTML = renderMarkdown(textBlock.dataset.mdContent);
296
+ scrollToBottom();
297
+ return;
298
+ }
299
+
324
300
  switch (event.type) {
325
301
  case "agent_start":
326
302
  case "message_start":
@@ -336,7 +312,7 @@ function handleAgentEvent(event) {
336
312
  const thinkingBlock = getOrCreateThinkingBlock();
337
313
  thinkingBlock.dataset.mdContent += event.delta;
338
314
  const contentEl = thinkingBlock.querySelector(".thinking-content");
339
- if (typeof marked !== "undefined") {
315
+ if (typeof window.marked !== "undefined") {
340
316
  contentEl.innerHTML = renderMarkdown(thinkingBlock.dataset.mdContent);
341
317
  } else {
342
318
  contentEl.textContent = thinkingBlock.dataset.mdContent;
@@ -347,7 +323,7 @@ function handleAgentEvent(event) {
347
323
  case "text_delta":
348
324
  const textBlock = getOrCreateTextBlock();
349
325
  textBlock.dataset.mdContent += event.delta;
350
- if (typeof marked !== "undefined") {
326
+ if (typeof window.marked !== "undefined") {
351
327
  textBlock.innerHTML = renderMarkdown(textBlock.dataset.mdContent);
352
328
  } else {
353
329
  textBlock.textContent = textBlock.dataset.mdContent;
@@ -364,8 +340,10 @@ function handleAgentEvent(event) {
364
340
  : JSON.stringify(event.toolInput, null, 2);
365
341
 
366
342
  let inputHtml = "";
367
- if (typeof marked !== "undefined") {
368
- inputHtml = marked.parse("```json\n" + safeInput + "\n```");
343
+ if (typeof window.marked !== "undefined") {
344
+ inputHtml = ensureLinksOpenInNewTab(
345
+ window.marked.parse("\`\`\`json\n" + safeInput + "\n\`\`\`")
346
+ );
369
347
  } else {
370
348
  inputHtml = escapeHtml(safeInput);
371
349
  }
@@ -373,7 +351,7 @@ function handleAgentEvent(event) {
373
351
  toolCard.innerHTML = `
374
352
  <div class="tool-header">
375
353
  <span class="tool-chevron">
376
- <svg viewBox="0 0 24 24" width="16" height="16" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round" stroke-linejoin="round"><polyline points="9 18 15 12 9 6"></polyline></svg>
354
+ <svg viewBox="0 0 24 24" width="16" height="16" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round" stroke-linejoin="round"><polyline points="9 18 15 12 9 6"></polyline></svg>
377
355
  </span>
378
356
  <span class="tool-icon">🛠️</span>
379
357
  <span class="tool-name">${escapeHtml(event.toolName)}</span>
@@ -389,9 +367,7 @@ function handleAgentEvent(event) {
389
367
  toolCard.classList.toggle("collapsed");
390
368
  });
391
369
 
392
- // Insert before loading indicator if exists
393
- const toolIndicator =
394
- currentAssistantMsg.querySelector(".loading-indicator");
370
+ const toolIndicator = currentAssistantMsg.querySelector(".loading-indicator");
395
371
  if (toolIndicator) {
396
372
  currentAssistantMsg.insertBefore(toolCard, toolIndicator);
397
373
  } else {
@@ -400,17 +376,12 @@ function handleAgentEvent(event) {
400
376
 
401
377
  toolCard.dataset.toolName = event.toolName;
402
378
  scrollToBottom();
403
-
404
379
  showLoadingIndicator();
405
380
  break;
406
381
 
407
382
  case "tool_end":
408
- const cards = Array.from(
409
- currentAssistantMsg.querySelectorAll(".tool-card.running"),
410
- );
411
- const card = cards
412
- .reverse()
413
- .find((c) => c.dataset.toolName === event.toolName);
383
+ const cards = Array.from(currentAssistantMsg.querySelectorAll(".tool-card.running"));
384
+ const card = cards.reverse().find((c) => c.dataset.toolName === event.toolName);
414
385
  if (card) {
415
386
  card.classList.remove("running");
416
387
  card.classList.add(event.isError ? "error" : "success");
@@ -434,17 +405,16 @@ function handleAgentEvent(event) {
434
405
  event.result &&
435
406
  typeof event.result === "string" &&
436
407
  (event.result.includes("\n") || event.result.length > 50)
437
- ? "```bash\n" + safeResult + "\n```"
438
- : "```json\n" + safeResult + "\n```";
408
+ ? "\`\`\`bash\n" + safeResult + "\n\`\`\`"
409
+ : "\`\`\`json\n" + safeResult + "\n\`\`\`";
439
410
 
440
- if (typeof marked !== "undefined") {
441
- resultEl.innerHTML = marked.parse(mdText);
411
+ if (typeof window.marked !== "undefined") {
412
+ resultEl.innerHTML = ensureLinksOpenInNewTab(window.marked.parse(mdText));
442
413
  } else {
443
414
  resultEl.textContent = safeResult;
444
415
  }
445
416
  }
446
417
  scrollToBottom();
447
-
448
418
  showLoadingIndicator();
449
419
  break;
450
420
  }
@@ -452,7 +422,7 @@ function handleAgentEvent(event) {
452
422
 
453
423
  function getOrCreateThinkingBlock() {
454
424
  const children = Array.from(currentAssistantMsg.children).filter(
455
- (c) => !c.classList.contains("loading-indicator"),
425
+ (c) => !c.classList.contains("loading-indicator")
456
426
  );
457
427
  let lastChild = children[children.length - 1];
458
428
 
@@ -464,7 +434,7 @@ function getOrCreateThinkingBlock() {
464
434
  lastChild.innerHTML = `
465
435
  <div class="tool-header thinking-header">
466
436
  <span class="tool-chevron">
467
- <svg viewBox="0 0 24 24" width="16" height="16" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round" stroke-linejoin="round"><polyline points="9 18 15 12 9 6"></polyline></svg>
437
+ <svg viewBox="0 0 24 24" width="16" height="16" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round" stroke-linejoin="round"><polyline points="9 18 15 12 9 6"></polyline></svg>
468
438
  </span>
469
439
  <span class="tool-icon">🧠</span>
470
440
  <span class="tool-name" style="color: var(--text-secondary);">Thinking Process</span>
@@ -488,7 +458,7 @@ function getOrCreateThinkingBlock() {
488
458
 
489
459
  function getOrCreateTextBlock() {
490
460
  const children = Array.from(currentAssistantMsg.children).filter(
491
- (c) => !c.classList.contains("loading-indicator"),
461
+ (c) => !c.classList.contains("loading-indicator")
492
462
  );
493
463
  let lastChild = children[children.length - 1];
494
464
 
@@ -513,40 +483,6 @@ function enableInput() {
513
483
  currentAssistantMsg = null;
514
484
  }
515
485
 
516
- async function sendMessage() {
517
- const input = document.getElementById("user-input");
518
- const text = input.value.trim();
519
- if (!text) return;
520
-
521
- const chatArea = document.getElementById("chat-area");
522
- if (chatArea.classList.contains("mode-welcome")) {
523
- chatArea.classList.remove("mode-welcome");
524
- chatArea.classList.add("mode-chat");
525
- }
526
-
527
- input.value = "";
528
- input.style.height = "auto";
529
-
530
- // Add the user message
531
- appendMessage("user", text);
532
- chatHistory.push({ role: "user", content: text });
533
-
534
- // Disable input while the agent is responding
535
- const sendBtn = document.getElementById("send-btn");
536
- sendBtn.disabled = true;
537
-
538
- // Create an assistant message placeholder
539
- currentAssistantMsg = appendMessage("assistant", "");
540
- showLoadingIndicator();
541
-
542
- try {
543
- const socket = await getOrCreateWs();
544
- socket.send(JSON.stringify({ text }));
545
- } catch (err) {
546
- handleError(err.message);
547
- }
548
- }
549
-
550
486
  function appendMessage(role, text) {
551
487
  const messages = document.getElementById("messages");
552
488
  const div = document.createElement("div");
@@ -577,6 +513,3 @@ function scrollToBottom() {
577
513
  const messages = document.getElementById("messages");
578
514
  messages.scrollTop = messages.scrollHeight;
579
515
  }
580
-
581
- // Start the app
582
- init();
@@ -0,0 +1,15 @@
1
+ export const state = {
2
+ config: null,
3
+ API_BASE: ""
4
+ };
5
+
6
+ export async function loadConfig() {
7
+ try {
8
+ const res = await fetch(state.API_BASE + "/api/config");
9
+ state.config = await res.json();
10
+ return state.config;
11
+ } catch (err) {
12
+ console.error("Failed to load config:", err);
13
+ throw err;
14
+ }
15
+ }