lonny-agent 0.2.4 → 0.2.6
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.
- package/README.md +326 -326
- package/dist/agent/llm.d.ts +4 -0
- package/dist/agent/llm.d.ts.map +1 -1
- package/dist/agent/prompt-builder.d.ts.map +1 -1
- package/dist/agent/prompt-builder.js +13 -9
- package/dist/agent/prompt-builder.js.map +1 -1
- package/dist/agent/providers/google.d.ts.map +1 -1
- package/dist/agent/providers/google.js.map +1 -1
- package/dist/agent/providers/openai.d.ts.map +1 -1
- package/dist/agent/providers/openai.js +43 -3
- package/dist/agent/providers/openai.js.map +1 -1
- package/dist/agent/session.d.ts +4 -0
- package/dist/agent/session.d.ts.map +1 -1
- package/dist/agent/session.js +45 -7
- package/dist/agent/session.js.map +1 -1
- package/dist/config/index.d.ts.map +1 -1
- package/dist/config/index.js +7 -1
- package/dist/config/index.js.map +1 -1
- package/dist/tools/__tests__/bash.test.js +1 -1
- package/dist/tools/__tests__/bash.test.js.map +1 -1
- package/dist/tools/__tests__/edit.test.js +875 -371
- package/dist/tools/__tests__/edit.test.js.map +1 -1
- package/dist/tools/__tests__/grep.test.js +32 -0
- package/dist/tools/__tests__/grep.test.js.map +1 -1
- package/dist/tools/__tests__/sed.test.d.ts +2 -0
- package/dist/tools/__tests__/sed.test.d.ts.map +1 -0
- package/dist/tools/__tests__/sed.test.js +228 -0
- package/dist/tools/__tests__/sed.test.js.map +1 -0
- package/dist/tools/bash.d.ts.map +1 -1
- package/dist/tools/bash.js +83 -10
- package/dist/tools/bash.js.map +1 -1
- package/dist/tools/edit.d.ts +50 -9
- package/dist/tools/edit.d.ts.map +1 -1
- package/dist/tools/edit.js +328 -158
- package/dist/tools/edit.js.map +1 -1
- package/dist/tools/grep.d.ts.map +1 -1
- package/dist/tools/grep.js +54 -30
- package/dist/tools/grep.js.map +1 -1
- package/dist/tools/registry.d.ts +0 -6
- package/dist/tools/registry.d.ts.map +1 -1
- package/dist/tools/registry.js +2 -29
- package/dist/tools/registry.js.map +1 -1
- package/dist/tools/sed.d.ts +4 -0
- package/dist/tools/sed.d.ts.map +1 -0
- package/dist/tools/sed.js +121 -0
- package/dist/tools/sed.js.map +1 -0
- package/dist/web/index.d.ts.map +1 -1
- package/dist/web/index.js +26 -3
- package/dist/web/index.js.map +1 -1
- package/dist/web/public/app.js +5 -855
- package/dist/web/public/confirm.js +70 -0
- package/dist/web/public/index.html +121 -120
- package/dist/web/public/input.js +107 -0
- package/dist/web/public/messages.js +395 -0
- package/dist/web/public/sidebar.js +82 -0
- package/dist/web/public/state.js +72 -0
- package/dist/web/public/style.css +996 -949
- package/dist/web/public/utils.js +37 -0
- package/dist/web/public/websocket.js +267 -0
- package/dist/web/public/ws.js +19 -0
- package/dist/web/session-bridge.d.ts.map +1 -1
- package/dist/web/session-bridge.js +3 -7
- package/dist/web/session-bridge.js.map +1 -1
- package/package.json +55 -54
- package/scripts/copy-native.mjs +24 -0
- package/scripts/copy-web.mjs +15 -0
- package/src/agent/index.ts +25 -25
- package/src/agent/llm.ts +4 -0
- package/src/agent/prompt-builder.ts +13 -9
- package/src/agent/providers/anthropic.ts +179 -179
- package/src/agent/providers/google.ts +210 -211
- package/src/agent/providers/ollama.ts +186 -186
- package/src/agent/providers/openai.ts +61 -5
- package/src/agent/session.ts +59 -3
- package/src/config/index.ts +342 -335
- package/src/tools/__tests__/bash.test.ts +1 -1
- package/src/tools/__tests__/edit.test.ts +981 -385
- package/src/tools/__tests__/grep.test.ts +35 -0
- package/src/tools/bash.ts +97 -11
- package/src/tools/edit.ts +362 -169
- package/src/tools/git.ts +76 -76
- package/src/tools/grep.ts +57 -30
- package/src/tools/registry.ts +2 -31
- package/src/tui/index.ts +786 -786
- package/src/web/index.ts +25 -3
- package/src/web/public/app.js +5 -855
- package/src/web/public/confirm.js +70 -0
- package/src/web/public/index.html +121 -120
- package/src/web/public/input.js +107 -0
- package/src/web/public/messages.js +395 -0
- package/src/web/public/sidebar.js +82 -0
- package/src/web/public/state.js +72 -0
- package/src/web/public/style.css +996 -949
- package/src/web/public/utils.js +37 -0
- package/src/web/public/websocket.js +267 -0
- package/src/web/public/ws.js +19 -0
- package/src/web/session-bridge.ts +193 -194
- package/.claude/settings.local.json +0 -14
- package/.kilo/plans/1780064105789-mighty-pixel.md +0 -171
- package/.kilo/plans/1780293725888-quick-wizard.md +0 -62
- package/.lonny/plan-web-cwd-status.md +0 -38
- package/src/tools/exec.ts +0 -348
package/src/web/public/app.js
CHANGED
|
@@ -1,857 +1,7 @@
|
|
|
1
|
-
/* ──
|
|
1
|
+
/* ── Entry Point ── */
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
const messagesEl = document.getElementById('messages')
|
|
6
|
-
const chatInput = document.getElementById('chat-input')
|
|
7
|
-
const sendBtn = document.getElementById('send-btn')
|
|
8
|
-
const stopBtn = document.getElementById('stop-btn')
|
|
9
|
-
const statusIndicator = document.getElementById('status-indicator')
|
|
10
|
-
const modeDisplay = document.getElementById('mode-display')
|
|
11
|
-
const modelDisplay = document.getElementById('model-display')
|
|
12
|
-
const tokenIn = document.getElementById('token-in')
|
|
13
|
-
const tokenOut = document.getElementById('token-out')
|
|
14
|
-
const tokenCalls = document.getElementById('token-calls')
|
|
15
|
-
const balanceDisplay = document.getElementById('balance-display')
|
|
16
|
-
const balanceSep = document.getElementById('balance-sep')
|
|
17
|
-
const cwdDisplay = document.getElementById('cwd-display')
|
|
18
|
-
const connectionOverlay = document.getElementById('connection-overlay')
|
|
19
|
-
const chatContainer = document.getElementById('chat-container')
|
|
3
|
+
import { initInput } from './input.js'
|
|
4
|
+
import { connect } from './websocket.js'
|
|
20
5
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
const sidebarTabs = document.querySelectorAll('.sidebar-tab')
|
|
24
|
-
const plansPane = document.getElementById('plans-pane')
|
|
25
|
-
const todosPane = document.getElementById('todos-pane')
|
|
26
|
-
const plansList = document.getElementById('plans-list')
|
|
27
|
-
const todosList = document.getElementById('todos-list')
|
|
28
|
-
const plansPlaceholder = document.getElementById('plans-placeholder')
|
|
29
|
-
const todosPlaceholder = document.getElementById('todos-placeholder')
|
|
30
|
-
|
|
31
|
-
// ── State ──
|
|
32
|
-
let ws = null
|
|
33
|
-
let isRunning = false
|
|
34
|
-
let currentMode = 'code'
|
|
35
|
-
let currentModel = ''
|
|
36
|
-
let currentProvider = ''
|
|
37
|
-
let streamingMsgEl = null
|
|
38
|
-
let streamingText = ''
|
|
39
|
-
let thinkingEl = null
|
|
40
|
-
let thinkingText = ''
|
|
41
|
-
let reconnectAttempts = 0
|
|
42
|
-
let plans = []
|
|
43
|
-
let currentPlanName = ''
|
|
44
|
-
let todos = []
|
|
45
|
-
const MAX_RECONNECT_ATTEMPTS = 10
|
|
46
|
-
const RECONNECT_DELAY = 2000
|
|
47
|
-
const pendingToolCalls = new Map() // id -> tool-call element
|
|
48
|
-
let pendingConfirmResolve = null // for confirm dialog
|
|
49
|
-
|
|
50
|
-
// ── Utility Functions ──
|
|
51
|
-
|
|
52
|
-
function getWsUrl() {
|
|
53
|
-
const proto = location.protocol === 'https:' ? 'wss:' : 'ws:'
|
|
54
|
-
return `${proto}//${location.host}`
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
function scrollToBottom() {
|
|
58
|
-
requestAnimationFrame(() => {
|
|
59
|
-
chatContainer.scrollTop = chatContainer.scrollHeight
|
|
60
|
-
})
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
function escapeHtml(text) {
|
|
64
|
-
const div = document.createElement('div')
|
|
65
|
-
div.textContent = text
|
|
66
|
-
return div.innerHTML
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
function formatTimestamp() {
|
|
70
|
-
return new Date().toLocaleTimeString()
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
function formatTokenCount(n) {
|
|
74
|
-
if (n >= 1000000) return (n / 1000000).toFixed(n % 1000000 === 0 ? 0 : 1) + 'M'
|
|
75
|
-
if (n >= 1000) return (n / 1000).toFixed(n % 1000 === 0 ? 0 : 1) + 'K'
|
|
76
|
-
return String(n)
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
function setInputEnabled(enabled) {
|
|
80
|
-
chatInput.disabled = !enabled
|
|
81
|
-
sendBtn.disabled = !enabled
|
|
82
|
-
sendBtn.classList.toggle('hidden', !enabled)
|
|
83
|
-
stopBtn.classList.toggle('hidden', enabled)
|
|
84
|
-
if (!enabled) {
|
|
85
|
-
stopBtn.disabled = false
|
|
86
|
-
}
|
|
87
|
-
if (enabled) {
|
|
88
|
-
chatInput.focus()
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
function setStatus(running) {
|
|
93
|
-
isRunning = running
|
|
94
|
-
statusIndicator.textContent = running ? '● running' : '○ idle'
|
|
95
|
-
statusIndicator.className = running ? 'status-running' : 'status-idle'
|
|
96
|
-
setInputEnabled(!running)
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
function updateStats() {
|
|
100
|
-
// Token stats are updated via messages from the server
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
// ── Sidebar Functions ──
|
|
104
|
-
|
|
105
|
-
function switchTab(tabId) {
|
|
106
|
-
sidebarTabs.forEach(tab => {
|
|
107
|
-
tab.classList.toggle('active', tab.dataset.tab === tabId)
|
|
108
|
-
})
|
|
109
|
-
plansPane.classList.toggle('active', tabId === 'plans')
|
|
110
|
-
todosPane.classList.toggle('active', tabId === 'todos')
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
function updatePlans() {
|
|
114
|
-
plansList.innerHTML = ''
|
|
115
|
-
if (plans.length === 0) {
|
|
116
|
-
plansPlaceholder.style.display = ''
|
|
117
|
-
return
|
|
118
|
-
}
|
|
119
|
-
plansPlaceholder.style.display = 'none'
|
|
120
|
-
for (const plan of plans) {
|
|
121
|
-
const div = document.createElement('div')
|
|
122
|
-
div.className = 'plan-item' + (plan.name === currentPlanName ? ' active' : '')
|
|
123
|
-
const timeStr = plan.mtime
|
|
124
|
-
? new Date(plan.mtime).toLocaleDateString() +
|
|
125
|
-
' ' +
|
|
126
|
-
new Date(plan.mtime).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })
|
|
127
|
-
: ''
|
|
128
|
-
div.dataset.planName = plan.name
|
|
129
|
-
div.innerHTML = `<div class="plan-name">${escapeHtml(plan.name)}</div><div class="plan-time">${escapeHtml(timeStr)}</div>`
|
|
130
|
-
div.addEventListener('click', () => onPlanClick(plan.name))
|
|
131
|
-
plansList.appendChild(div)
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
function updateTodos() {
|
|
136
|
-
todosList.innerHTML = ''
|
|
137
|
-
if (!currentPlanName) {
|
|
138
|
-
todosPlaceholder.textContent = '(no plan selected)'
|
|
139
|
-
todosPlaceholder.style.display = ''
|
|
140
|
-
return
|
|
141
|
-
}
|
|
142
|
-
if (todos.length === 0) {
|
|
143
|
-
todosPlaceholder.textContent = '(no todos)'
|
|
144
|
-
todosPlaceholder.style.display = ''
|
|
145
|
-
return
|
|
146
|
-
}
|
|
147
|
-
todosPlaceholder.style.display = 'none'
|
|
148
|
-
// Header
|
|
149
|
-
const header = document.createElement('div')
|
|
150
|
-
header.className = 'todo-header-text'
|
|
151
|
-
header.textContent = currentPlanName
|
|
152
|
-
todosList.appendChild(header)
|
|
153
|
-
// Items
|
|
154
|
-
for (const todo of todos) {
|
|
155
|
-
const div = document.createElement('div')
|
|
156
|
-
div.className = 'todo-item'
|
|
157
|
-
const done = todo.done
|
|
158
|
-
div.innerHTML =
|
|
159
|
-
`<span class="todo-check ${done ? 'done' : 'pending'}">${done ? '✅' : '⬜'}</span>` +
|
|
160
|
-
`<span class="todo-text ${done ? 'done' : ''}">${escapeHtml(todo.text)}</span>`
|
|
161
|
-
todosList.appendChild(div)
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
function onPlanClick(planName) {
|
|
166
|
-
// Send load_plan message to server
|
|
167
|
-
if (ws && ws.readyState === WebSocket.OPEN) {
|
|
168
|
-
ws.send(JSON.stringify({ type: 'load_plan', planName }))
|
|
169
|
-
}
|
|
170
|
-
// Switch to the Todo tab
|
|
171
|
-
switchTab('todos')
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
function updatePlansAndTodos() {
|
|
175
|
-
updatePlans()
|
|
176
|
-
updateTodos()
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
// ── Message Rendering ──
|
|
180
|
-
|
|
181
|
-
function addSystemMessage(text) {
|
|
182
|
-
const div = document.createElement('div')
|
|
183
|
-
div.className = 'system-message'
|
|
184
|
-
div.textContent = text
|
|
185
|
-
messagesEl.appendChild(div)
|
|
186
|
-
scrollToBottom()
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
function addUserMessage(text) {
|
|
190
|
-
const msgDiv = document.createElement('div')
|
|
191
|
-
msgDiv.className = 'message'
|
|
192
|
-
|
|
193
|
-
const header = document.createElement('div')
|
|
194
|
-
header.className = 'message-header'
|
|
195
|
-
header.innerHTML = `<span class="label user">You</span> <span class="timestamp">${formatTimestamp()}</span>`
|
|
196
|
-
msgDiv.appendChild(header)
|
|
197
|
-
|
|
198
|
-
const body = document.createElement('div')
|
|
199
|
-
body.className = 'message-body user'
|
|
200
|
-
body.innerHTML = renderMarkdown(text)
|
|
201
|
-
msgDiv.appendChild(body)
|
|
202
|
-
|
|
203
|
-
messagesEl.appendChild(msgDiv)
|
|
204
|
-
scrollToBottom()
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
function startAssistantMessage() {
|
|
208
|
-
streamingText = ''
|
|
209
|
-
streamingMsgEl = document.createElement('div')
|
|
210
|
-
streamingMsgEl.className = 'message'
|
|
211
|
-
|
|
212
|
-
const header = document.createElement('div')
|
|
213
|
-
header.className = 'message-header'
|
|
214
|
-
header.innerHTML = `<span class="label assistant">Lonny</span> <span class="timestamp">${formatTimestamp()}</span>`
|
|
215
|
-
streamingMsgEl.appendChild(header)
|
|
216
|
-
|
|
217
|
-
const body = document.createElement('div')
|
|
218
|
-
body.className = 'message-body assistant streaming'
|
|
219
|
-
body.textContent = ''
|
|
220
|
-
streamingMsgEl.appendChild(body)
|
|
221
|
-
|
|
222
|
-
messagesEl.appendChild(streamingMsgEl)
|
|
223
|
-
scrollToBottom()
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
function renderMarkdown(text) {
|
|
227
|
-
if (typeof marked !== 'undefined' && marked.parse) {
|
|
228
|
-
return marked.parse(text, { breaks: true, gfm: true })
|
|
229
|
-
}
|
|
230
|
-
return '<pre>' + escapeHtml(text) + '</pre>'
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
function appendChunk(text) {
|
|
234
|
-
if (!streamingMsgEl) {
|
|
235
|
-
startAssistantMessage()
|
|
236
|
-
}
|
|
237
|
-
streamingText += text
|
|
238
|
-
const body = streamingMsgEl.querySelector('.message-body')
|
|
239
|
-
body.innerHTML = renderMarkdown(streamingText)
|
|
240
|
-
// Ensure streaming cursor stays visible
|
|
241
|
-
body.classList.add('streaming')
|
|
242
|
-
scrollToBottom()
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
function finalizeAssistantMessage() {
|
|
246
|
-
if (!streamingMsgEl) return
|
|
247
|
-
const body = streamingMsgEl.querySelector('.message-body')
|
|
248
|
-
// Re-render final text as markdown
|
|
249
|
-
body.innerHTML = renderMarkdown(streamingText)
|
|
250
|
-
body.classList.remove('streaming')
|
|
251
|
-
streamingMsgEl = null
|
|
252
|
-
streamingText = ''
|
|
253
|
-
scrollToBottom()
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
function addToolCall(name, input, id) {
|
|
257
|
-
const container = streamingMsgEl || messagesEl.querySelector('.message:last-child')
|
|
258
|
-
const div = document.createElement('div')
|
|
259
|
-
div.className = id ? 'tool-call executing' : 'tool-call'
|
|
260
|
-
div.dataset.toolId = id || ''
|
|
261
|
-
|
|
262
|
-
if (name === 'edit') {
|
|
263
|
-
// Special display for edit tool: show file paths being edited
|
|
264
|
-
const edits = input && input.edits
|
|
265
|
-
const paths = Array.isArray(edits) ? edits.map(e => e.file_path).filter(Boolean) : []
|
|
266
|
-
const uniquePaths = [...new Set(paths)]
|
|
267
|
-
const pathsStr =
|
|
268
|
-
uniquePaths.length > 0 ? uniquePaths.map(p => escapeHtml(p)).join(', ') : '(no files)'
|
|
269
|
-
div.innerHTML = `<span class="tool-name tool-name-edit">✏ edit</span> <span class="tool-input tool-input-edit">${pathsStr}</span>`
|
|
270
|
-
} else {
|
|
271
|
-
const inputStr =
|
|
272
|
-
typeof input === 'object' ? JSON.stringify(input).slice(0, 120) : String(input)
|
|
273
|
-
div.innerHTML = `<span class="tool-name">${escapeHtml(name)}</span> <span class="tool-input">${escapeHtml(inputStr)}</span>`
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
if (container && container.matches('.message')) {
|
|
277
|
-
container.appendChild(div)
|
|
278
|
-
} else {
|
|
279
|
-
messagesEl.appendChild(div)
|
|
280
|
-
}
|
|
281
|
-
if (id) pendingToolCalls.set(id, div)
|
|
282
|
-
scrollToBottom()
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
function renderDiffContent(text) {
|
|
286
|
-
const lines = text.split('\n')
|
|
287
|
-
return lines
|
|
288
|
-
.map(line => {
|
|
289
|
-
const trimmed = line.trim()
|
|
290
|
-
if (trimmed.startsWith('+ ')) {
|
|
291
|
-
return `<span class="diff-added">${escapeHtml(line)}\n</span>`
|
|
292
|
-
} else if (trimmed.startsWith('- ')) {
|
|
293
|
-
return `<span class="diff-removed">${escapeHtml(line)}\n</span>`
|
|
294
|
-
}
|
|
295
|
-
return escapeHtml(line) + '\n'
|
|
296
|
-
})
|
|
297
|
-
.join('')
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
function addToolResult(name, success, outputOrError, id) {
|
|
301
|
-
// Remove from pending tracking and clear executing state
|
|
302
|
-
if (id) {
|
|
303
|
-
const pendingEl = pendingToolCalls.get(id)
|
|
304
|
-
if (pendingEl) {
|
|
305
|
-
pendingEl.classList.remove('executing')
|
|
306
|
-
pendingToolCalls.delete(id)
|
|
307
|
-
// For edit tool, update the tool-call element to show completion summary
|
|
308
|
-
if (name === 'edit' && success) {
|
|
309
|
-
pendingEl.classList.add('completed')
|
|
310
|
-
}
|
|
311
|
-
}
|
|
312
|
-
}
|
|
313
|
-
const container = streamingMsgEl || messagesEl.querySelector('.message:last-child')
|
|
314
|
-
const div = document.createElement('div')
|
|
315
|
-
div.className = 'tool-result'
|
|
316
|
-
if (success) {
|
|
317
|
-
let display = outputOrError
|
|
318
|
-
if (display === '(no output)') display = ''
|
|
319
|
-
// Special display for edit tool success — show rich diff content
|
|
320
|
-
if (name === 'edit' && display) {
|
|
321
|
-
const lines = display.split('\n')
|
|
322
|
-
const summaryLines = []
|
|
323
|
-
const diffLines = []
|
|
324
|
-
for (const line of lines) {
|
|
325
|
-
const trimmed = line.trim()
|
|
326
|
-
if (
|
|
327
|
-
trimmed.startsWith('Edited ') ||
|
|
328
|
-
trimmed.startsWith('Created ') ||
|
|
329
|
-
trimmed.startsWith('Deleted ')
|
|
330
|
-
) {
|
|
331
|
-
summaryLines.push(trimmed)
|
|
332
|
-
} else if (trimmed.startsWith('- ') || trimmed.startsWith('+ ')) {
|
|
333
|
-
diffLines.push(line)
|
|
334
|
-
}
|
|
335
|
-
}
|
|
336
|
-
let html = `<span class="tool-result-success">✔ ${escapeHtml(name)}</span>`
|
|
337
|
-
if (summaryLines.length > 0) {
|
|
338
|
-
html += `<span class="tool-result-summary">${summaryLines.map(s => escapeHtml(s)).join('<br>')}</span>`
|
|
339
|
-
}
|
|
340
|
-
if (diffLines.length > 0) {
|
|
341
|
-
html += `<span class="tool-result-diff"><pre><code>${renderDiffContent(diffLines.join('\n'))}</code></pre></span>`
|
|
342
|
-
} else if (display.includes(' FAIL ')) {
|
|
343
|
-
html += `<span class="tool-result-diff"><pre>${escapeHtml(display)}</pre></span>`
|
|
344
|
-
}
|
|
345
|
-
div.innerHTML = html
|
|
346
|
-
} else {
|
|
347
|
-
const summary = typeof display === 'string' ? display.slice(0, 80) : ''
|
|
348
|
-
div.innerHTML = `<span class="tool-result-success">✔ ${escapeHtml(name)}</span>${summary ? ' ' + escapeHtml(summary) : ''}`
|
|
349
|
-
}
|
|
350
|
-
} else {
|
|
351
|
-
div.innerHTML = `<span class="tool-result-error">✖ ${escapeHtml(name)}</span> ${escapeHtml(outputOrError)}`
|
|
352
|
-
}
|
|
353
|
-
if (container && container.matches('.message')) {
|
|
354
|
-
container.appendChild(div)
|
|
355
|
-
} else {
|
|
356
|
-
messagesEl.appendChild(div)
|
|
357
|
-
}
|
|
358
|
-
scrollToBottom()
|
|
359
|
-
}
|
|
360
|
-
|
|
361
|
-
function showConfirmDialog(toolCalls) {
|
|
362
|
-
const overlay = document.getElementById('confirm-overlay')
|
|
363
|
-
const list = document.getElementById('confirm-tool-list')
|
|
364
|
-
if (!overlay || !list) return
|
|
365
|
-
list.innerHTML = ''
|
|
366
|
-
for (const tc of toolCalls) {
|
|
367
|
-
const detail = tc.input?.file_path || tc.input?.command || tc.input?.package_name || ''
|
|
368
|
-
const item = document.createElement('div')
|
|
369
|
-
item.className = 'confirm-tool-item'
|
|
370
|
-
item.innerHTML =
|
|
371
|
-
`<span class="confirm-tool-name">${escapeHtml(tc.name)}</span>` +
|
|
372
|
-
(detail
|
|
373
|
-
? ` <span class="confirm-tool-input">${escapeHtml(detail.length > 80 ? detail.slice(0, 80) + '…' : detail)}</span>`
|
|
374
|
-
: '')
|
|
375
|
-
list.appendChild(item)
|
|
376
|
-
}
|
|
377
|
-
overlay.classList.remove('hidden')
|
|
378
|
-
|
|
379
|
-
pendingConfirmResolve = null
|
|
380
|
-
|
|
381
|
-
function cleanup(approved) {
|
|
382
|
-
overlay.classList.add('hidden')
|
|
383
|
-
allowBtn.removeEventListener('click', onAllow)
|
|
384
|
-
rejectBtn.removeEventListener('click', onReject)
|
|
385
|
-
if (pendingConfirmResolve) {
|
|
386
|
-
pendingConfirmResolve(approved)
|
|
387
|
-
pendingConfirmResolve = null
|
|
388
|
-
}
|
|
389
|
-
}
|
|
390
|
-
|
|
391
|
-
const allowBtn = document.getElementById('confirm-allow-btn')
|
|
392
|
-
const rejectBtn = document.getElementById('confirm-reject-btn')
|
|
393
|
-
const onAllow = () => {
|
|
394
|
-
if (ws && ws.readyState === WebSocket.OPEN) {
|
|
395
|
-
ws.send(JSON.stringify({ type: 'tool_confirm_response', approved: true }))
|
|
396
|
-
}
|
|
397
|
-
cleanup(true)
|
|
398
|
-
}
|
|
399
|
-
const onReject = () => {
|
|
400
|
-
if (ws && ws.readyState === WebSocket.OPEN) {
|
|
401
|
-
ws.send(JSON.stringify({ type: 'tool_confirm_response', approved: false }))
|
|
402
|
-
}
|
|
403
|
-
cleanup(false)
|
|
404
|
-
}
|
|
405
|
-
allowBtn.addEventListener('click', onAllow)
|
|
406
|
-
rejectBtn.addEventListener('click', onReject)
|
|
407
|
-
}
|
|
408
|
-
|
|
409
|
-
function closeConfirmDialog() {
|
|
410
|
-
const overlay = document.getElementById('confirm-overlay')
|
|
411
|
-
if (overlay && !overlay.classList.contains('hidden')) {
|
|
412
|
-
overlay.classList.add('hidden')
|
|
413
|
-
if (pendingConfirmResolve) {
|
|
414
|
-
pendingConfirmResolve(false)
|
|
415
|
-
pendingConfirmResolve = null
|
|
416
|
-
}
|
|
417
|
-
}
|
|
418
|
-
}
|
|
419
|
-
|
|
420
|
-
function showThinking(text) {
|
|
421
|
-
if (!thinkingEl) {
|
|
422
|
-
thinkingEl = document.createElement('div')
|
|
423
|
-
thinkingEl.className = 'thinking-block'
|
|
424
|
-
thinkingEl.innerHTML =
|
|
425
|
-
'<div class="thinking-label">🤔 Think</div><div class="thinking-content"></div>'
|
|
426
|
-
// Insert inside the streaming message (before .message-body) so it stays
|
|
427
|
-
// within the assistant message flow, not as a separate top-level element.
|
|
428
|
-
if (streamingMsgEl) {
|
|
429
|
-
const body = streamingMsgEl.querySelector('.message-body')
|
|
430
|
-
streamingMsgEl.insertBefore(thinkingEl, body)
|
|
431
|
-
} else {
|
|
432
|
-
messagesEl.appendChild(thinkingEl)
|
|
433
|
-
}
|
|
434
|
-
thinkingText = ''
|
|
435
|
-
}
|
|
436
|
-
thinkingText += text
|
|
437
|
-
const content = thinkingEl.querySelector('.thinking-content')
|
|
438
|
-
content.innerHTML = renderMarkdown(thinkingText)
|
|
439
|
-
scrollToBottom()
|
|
440
|
-
}
|
|
441
|
-
|
|
442
|
-
function hideThinking() {
|
|
443
|
-
if (thinkingEl) {
|
|
444
|
-
// Don't remove — keep the thinking content visible in the message flow.
|
|
445
|
-
// Just mark it as finalized and clear the reference so new thinking
|
|
446
|
-
// blocks can be created for subsequent turns.
|
|
447
|
-
thinkingEl.classList.add('thinking-done')
|
|
448
|
-
thinkingEl = null
|
|
449
|
-
thinkingText = ''
|
|
450
|
-
}
|
|
451
|
-
}
|
|
452
|
-
|
|
453
|
-
function renderSessionHistory(messages) {
|
|
454
|
-
if (!Array.isArray(messages)) return
|
|
455
|
-
for (const msg of messages) {
|
|
456
|
-
if (msg.role === 'user') {
|
|
457
|
-
addUserMessage(msg.content || '')
|
|
458
|
-
} else if (msg.role === 'assistant') {
|
|
459
|
-
startAssistantMessage()
|
|
460
|
-
if (msg.reasoning_content) {
|
|
461
|
-
showThinking(msg.reasoning_content)
|
|
462
|
-
hideThinking()
|
|
463
|
-
}
|
|
464
|
-
if (msg.content) {
|
|
465
|
-
streamingText = msg.content
|
|
466
|
-
const body = streamingMsgEl.querySelector('.message-body')
|
|
467
|
-
body.innerHTML = renderMarkdown(msg.content)
|
|
468
|
-
}
|
|
469
|
-
if (msg.tool_calls && msg.tool_calls.length > 0) {
|
|
470
|
-
for (const tc of msg.tool_calls) {
|
|
471
|
-
addToolCall(tc.name, tc.input)
|
|
472
|
-
}
|
|
473
|
-
}
|
|
474
|
-
finalizeAssistantMessage()
|
|
475
|
-
} else if (msg.role === 'tool') {
|
|
476
|
-
const content = msg.content || ''
|
|
477
|
-
const isError = content.startsWith('ERROR: ')
|
|
478
|
-
if (isError) {
|
|
479
|
-
addToolResult(msg.name || '', false, content.replace(/^ERROR:\s*/, ''))
|
|
480
|
-
} else {
|
|
481
|
-
addToolResult(msg.name || '', true, content)
|
|
482
|
-
}
|
|
483
|
-
}
|
|
484
|
-
}
|
|
485
|
-
}
|
|
486
|
-
|
|
487
|
-
function addTokenStats(turnIn, turnOut, totalIn, totalOut, turnApi, totalApi) {
|
|
488
|
-
const div = document.createElement('div')
|
|
489
|
-
div.className = 'token-stats-bar'
|
|
490
|
-
div.textContent = `▴${turnIn} ▾${turnOut} total ${totalIn + totalOut} calls ${turnApi}(${totalApi})`
|
|
491
|
-
messagesEl.appendChild(div)
|
|
492
|
-
scrollToBottom()
|
|
493
|
-
|
|
494
|
-
// Update status bar (use formatted values)
|
|
495
|
-
tokenIn.textContent = formatTokenCount(totalIn)
|
|
496
|
-
tokenOut.textContent = formatTokenCount(totalOut)
|
|
497
|
-
tokenCalls.textContent = `(${totalApi})`
|
|
498
|
-
}
|
|
499
|
-
|
|
500
|
-
function addErrorMessage(text) {
|
|
501
|
-
const div = document.createElement('div')
|
|
502
|
-
div.className = 'message'
|
|
503
|
-
div.innerHTML = `<div class="message-body error">${escapeHtml(text)}</div>`
|
|
504
|
-
messagesEl.appendChild(div)
|
|
505
|
-
scrollToBottom()
|
|
506
|
-
}
|
|
507
|
-
|
|
508
|
-
// ── WebSocket Connection ──
|
|
509
|
-
|
|
510
|
-
function connect() {
|
|
511
|
-
connectionOverlay.classList.remove('hidden')
|
|
512
|
-
|
|
513
|
-
try {
|
|
514
|
-
ws = new WebSocket(getWsUrl())
|
|
515
|
-
} catch (err) {
|
|
516
|
-
connectionOverlay.classList.add('hidden')
|
|
517
|
-
addErrorMessage(`Connection failed: ${err.message}`)
|
|
518
|
-
return
|
|
519
|
-
}
|
|
520
|
-
|
|
521
|
-
ws.onopen = () => {
|
|
522
|
-
connectionOverlay.classList.add('hidden')
|
|
523
|
-
reconnectAttempts = 0
|
|
524
|
-
addSystemMessage('Connected to Lonny')
|
|
525
|
-
}
|
|
526
|
-
|
|
527
|
-
ws.onmessage = event => {
|
|
528
|
-
try {
|
|
529
|
-
const msg = JSON.parse(event.data)
|
|
530
|
-
handleMessage(msg)
|
|
531
|
-
} catch (err) {
|
|
532
|
-
console.error('Failed to parse message:', err)
|
|
533
|
-
}
|
|
534
|
-
}
|
|
535
|
-
|
|
536
|
-
ws.onclose = () => {
|
|
537
|
-
setStatus(false)
|
|
538
|
-
hideThinking()
|
|
539
|
-
// Clear any pending tool call spinners on disconnect
|
|
540
|
-
for (const [, el] of pendingToolCalls) el.classList.remove('executing')
|
|
541
|
-
pendingToolCalls.clear()
|
|
542
|
-
if (streamingMsgEl) {
|
|
543
|
-
finalizeAssistantMessage()
|
|
544
|
-
}
|
|
545
|
-
|
|
546
|
-
if (reconnectAttempts < MAX_RECONNECT_ATTEMPTS) {
|
|
547
|
-
reconnectAttempts++
|
|
548
|
-
const delay = RECONNECT_DELAY * Math.min(reconnectAttempts, 5)
|
|
549
|
-
connectionOverlay.querySelector('p').textContent =
|
|
550
|
-
`Reconnecting in ${delay / 1000}s... (attempt ${reconnectAttempts}/${MAX_RECONNECT_ATTEMPTS})`
|
|
551
|
-
connectionOverlay.classList.remove('hidden')
|
|
552
|
-
setTimeout(connect, delay)
|
|
553
|
-
} else {
|
|
554
|
-
connectionOverlay.querySelector('p').textContent =
|
|
555
|
-
'Connection lost. Please refresh the page.'
|
|
556
|
-
connectionOverlay.classList.remove('hidden')
|
|
557
|
-
addErrorMessage('Connection lost. Refresh the page to reconnect.')
|
|
558
|
-
}
|
|
559
|
-
}
|
|
560
|
-
|
|
561
|
-
ws.onerror = () => {
|
|
562
|
-
// onclose will fire after this
|
|
563
|
-
}
|
|
564
|
-
}
|
|
565
|
-
|
|
566
|
-
// ── Message Handler ──
|
|
567
|
-
|
|
568
|
-
function handleMessage(msg) {
|
|
569
|
-
switch (msg.type) {
|
|
570
|
-
case 'hello':
|
|
571
|
-
currentMode = msg.mode || 'code'
|
|
572
|
-
currentModel = msg.model || ''
|
|
573
|
-
currentProvider = msg.provider || ''
|
|
574
|
-
modeDisplay.textContent = currentMode
|
|
575
|
-
modelDisplay.textContent = `${currentProvider}/${currentModel}`
|
|
576
|
-
// Sync token stats from session (formatted)
|
|
577
|
-
if (msg.totalIn !== undefined) {
|
|
578
|
-
tokenIn.textContent = formatTokenCount(msg.totalIn || 0)
|
|
579
|
-
tokenOut.textContent = formatTokenCount(msg.totalOut || 0)
|
|
580
|
-
tokenCalls.textContent = `(${msg.totalApi || 0})`
|
|
581
|
-
}
|
|
582
|
-
// Show DeepSeek balance if available
|
|
583
|
-
if (msg.webBalance) {
|
|
584
|
-
balanceDisplay.textContent = '余额:' + msg.webBalance
|
|
585
|
-
balanceDisplay.style.display = ''
|
|
586
|
-
balanceSep.style.display = ''
|
|
587
|
-
} else if (msg.balance) {
|
|
588
|
-
balanceDisplay.textContent = '余额:' + msg.balance
|
|
589
|
-
balanceDisplay.style.display = ''
|
|
590
|
-
balanceSep.style.display = ''
|
|
591
|
-
} else {
|
|
592
|
-
balanceDisplay.style.display = 'none'
|
|
593
|
-
balanceSep.style.display = 'none'
|
|
594
|
-
}
|
|
595
|
-
// Show working directory
|
|
596
|
-
if (msg.cwd) {
|
|
597
|
-
const maxLen = 35
|
|
598
|
-
cwdDisplay.textContent =
|
|
599
|
-
msg.cwd.length > maxLen ? '...' + msg.cwd.slice(-maxLen) : msg.cwd
|
|
600
|
-
}
|
|
601
|
-
break
|
|
602
|
-
|
|
603
|
-
case 'chunk':
|
|
604
|
-
appendChunk(msg.text || '')
|
|
605
|
-
break
|
|
606
|
-
|
|
607
|
-
case 'thinking':
|
|
608
|
-
showThinking(msg.text || '')
|
|
609
|
-
break
|
|
610
|
-
|
|
611
|
-
case 'thinking_end':
|
|
612
|
-
hideThinking()
|
|
613
|
-
break
|
|
614
|
-
|
|
615
|
-
case 'tool_call':
|
|
616
|
-
addToolCall(msg.name, msg.input, msg.id)
|
|
617
|
-
break
|
|
618
|
-
|
|
619
|
-
case 'tool_result':
|
|
620
|
-
if (msg.success) {
|
|
621
|
-
addToolResult(msg.name, true, msg.output || '', msg.id)
|
|
622
|
-
} else {
|
|
623
|
-
addToolResult(msg.name, false, msg.error || 'Unknown error', msg.id)
|
|
624
|
-
}
|
|
625
|
-
break
|
|
626
|
-
|
|
627
|
-
case 'turn_start':
|
|
628
|
-
setStatus(true)
|
|
629
|
-
// Start a fresh assistant message container for the upcoming response
|
|
630
|
-
startAssistantMessage()
|
|
631
|
-
break
|
|
632
|
-
|
|
633
|
-
case 'turn_end':
|
|
634
|
-
hideThinking()
|
|
635
|
-
// Clear any remaining pending tool call spinners
|
|
636
|
-
for (const [, el] of pendingToolCalls) el.classList.remove('executing')
|
|
637
|
-
pendingToolCalls.clear()
|
|
638
|
-
finalizeAssistantMessage()
|
|
639
|
-
break
|
|
640
|
-
|
|
641
|
-
case 'done':
|
|
642
|
-
setStatus(false)
|
|
643
|
-
hideThinking()
|
|
644
|
-
// Clear any remaining pending tool call spinners
|
|
645
|
-
for (const [, el] of pendingToolCalls) el.classList.remove('executing')
|
|
646
|
-
pendingToolCalls.clear()
|
|
647
|
-
finalizeAssistantMessage()
|
|
648
|
-
if (msg.reason === 'error') {
|
|
649
|
-
addErrorMessage('An error occurred during processing.')
|
|
650
|
-
}
|
|
651
|
-
break
|
|
652
|
-
|
|
653
|
-
case 'mode_changed':
|
|
654
|
-
currentMode = msg.mode
|
|
655
|
-
modeDisplay.textContent = currentMode
|
|
656
|
-
addSystemMessage(`Switched to ${currentMode} mode`)
|
|
657
|
-
break
|
|
658
|
-
|
|
659
|
-
case 'model_changed':
|
|
660
|
-
currentModel = msg.model
|
|
661
|
-
modelDisplay.textContent = `${currentProvider}/${currentModel}`
|
|
662
|
-
addSystemMessage(`Model switched to ${currentModel}`)
|
|
663
|
-
break
|
|
664
|
-
|
|
665
|
-
case 'session_cleared':
|
|
666
|
-
messagesEl.innerHTML = ''
|
|
667
|
-
addSystemMessage('Session cleared. Starting fresh.')
|
|
668
|
-
break
|
|
669
|
-
|
|
670
|
-
case 'help':
|
|
671
|
-
if (Array.isArray(msg.commands)) {
|
|
672
|
-
addSystemMessage('Available commands:')
|
|
673
|
-
msg.commands.forEach(cmd => addSystemMessage(cmd))
|
|
674
|
-
}
|
|
675
|
-
break
|
|
676
|
-
|
|
677
|
-
case 'compaction':
|
|
678
|
-
addSystemMessage(`📦 Compressed context: ${msg.before} → ${msg.after} messages`)
|
|
679
|
-
break
|
|
680
|
-
|
|
681
|
-
case 'plan_written':
|
|
682
|
-
addSystemMessage(`📝 Plan written: ${msg.display || ''}`)
|
|
683
|
-
break
|
|
684
|
-
|
|
685
|
-
case 'session_history':
|
|
686
|
-
renderSessionHistory(msg.messages)
|
|
687
|
-
// Scroll to bottom after all messages are rendered
|
|
688
|
-
setTimeout(scrollToBottom, 50)
|
|
689
|
-
break
|
|
690
|
-
|
|
691
|
-
case 'plan_data':
|
|
692
|
-
plans = msg.plans || []
|
|
693
|
-
currentPlanName = msg.currentPlanName || ''
|
|
694
|
-
todos = msg.todos || []
|
|
695
|
-
updatePlansAndTodos()
|
|
696
|
-
break
|
|
697
|
-
|
|
698
|
-
case 'balance_update':
|
|
699
|
-
if (msg.webBalance) {
|
|
700
|
-
balanceDisplay.textContent = '余额:' + msg.webBalance
|
|
701
|
-
balanceDisplay.style.display = ''
|
|
702
|
-
balanceSep.style.display = ''
|
|
703
|
-
} else if (msg.balance) {
|
|
704
|
-
balanceDisplay.textContent = '余额:' + msg.balance
|
|
705
|
-
balanceDisplay.style.display = ''
|
|
706
|
-
balanceSep.style.display = ''
|
|
707
|
-
}
|
|
708
|
-
break
|
|
709
|
-
|
|
710
|
-
case 'token_stats':
|
|
711
|
-
addTokenStats(msg.turnIn, msg.turnOut, msg.totalIn, msg.totalOut, msg.turnApi, msg.totalApi)
|
|
712
|
-
break
|
|
713
|
-
|
|
714
|
-
case 'tool_confirm_request': {
|
|
715
|
-
if (!msg.toolCalls || msg.toolCalls.length === 0) break
|
|
716
|
-
showConfirmDialog(msg.toolCalls)
|
|
717
|
-
break
|
|
718
|
-
}
|
|
719
|
-
|
|
720
|
-
case 'error':
|
|
721
|
-
closeConfirmDialog()
|
|
722
|
-
addErrorMessage(msg.message || 'Unknown error')
|
|
723
|
-
break
|
|
724
|
-
|
|
725
|
-
case 'pong':
|
|
726
|
-
break
|
|
727
|
-
|
|
728
|
-
default:
|
|
729
|
-
console.log('Unknown message type:', msg.type)
|
|
730
|
-
}
|
|
731
|
-
}
|
|
732
|
-
|
|
733
|
-
// ── Confirm dialog: click overlay background to close ──
|
|
734
|
-
document.getElementById('confirm-overlay').addEventListener('click', e => {
|
|
735
|
-
if (e.target === e.currentTarget) {
|
|
736
|
-
closeConfirmDialog()
|
|
737
|
-
}
|
|
738
|
-
})
|
|
739
|
-
|
|
740
|
-
// ── Send Message ──
|
|
741
|
-
|
|
742
|
-
function sendMessage() {
|
|
743
|
-
const text = chatInput.value.trim()
|
|
744
|
-
if (!text || isRunning) return
|
|
745
|
-
|
|
746
|
-
// Add user message to chat
|
|
747
|
-
addUserMessage(text)
|
|
748
|
-
chatInput.value = ''
|
|
749
|
-
|
|
750
|
-
// Send to server
|
|
751
|
-
if (ws && ws.readyState === WebSocket.OPEN) {
|
|
752
|
-
ws.send(JSON.stringify({ type: 'message', text }))
|
|
753
|
-
} else {
|
|
754
|
-
addErrorMessage('Not connected to server.')
|
|
755
|
-
}
|
|
756
|
-
}
|
|
757
|
-
|
|
758
|
-
// ── Event Listeners ──
|
|
759
|
-
|
|
760
|
-
sendBtn.addEventListener('click', sendMessage)
|
|
761
|
-
|
|
762
|
-
// Stop button
|
|
763
|
-
stopBtn.addEventListener('click', () => {
|
|
764
|
-
if (ws && ws.readyState === WebSocket.OPEN) {
|
|
765
|
-
ws.send(JSON.stringify({ type: 'stop' }))
|
|
766
|
-
}
|
|
767
|
-
stopBtn.disabled = true
|
|
768
|
-
})
|
|
769
|
-
|
|
770
|
-
chatInput.addEventListener('keydown', e => {
|
|
771
|
-
if (e.key === 'Enter' && !e.shiftKey) {
|
|
772
|
-
e.preventDefault()
|
|
773
|
-
sendMessage()
|
|
774
|
-
}
|
|
775
|
-
// Tab to select hint item
|
|
776
|
-
if (e.key === 'Tab' && !slashHint.classList.contains('hidden')) {
|
|
777
|
-
e.preventDefault()
|
|
778
|
-
const selected = slashHint.querySelector('.slash-hint-item.selected')
|
|
779
|
-
if (selected) {
|
|
780
|
-
const cmd = selected.dataset.cmd
|
|
781
|
-
chatInput.value = '/' + cmd
|
|
782
|
-
hideSlashHint()
|
|
783
|
-
chatInput.focus()
|
|
784
|
-
}
|
|
785
|
-
}
|
|
786
|
-
if (e.key === 'Escape') {
|
|
787
|
-
hideSlashHint()
|
|
788
|
-
}
|
|
789
|
-
})
|
|
790
|
-
|
|
791
|
-
// Slash command hint
|
|
792
|
-
const slashHint = document.getElementById('slash-hint')
|
|
793
|
-
|
|
794
|
-
function showSlashHint() {
|
|
795
|
-
slashHint.classList.remove('hidden')
|
|
796
|
-
// Select first item by default
|
|
797
|
-
const items = slashHint.querySelectorAll('.slash-hint-item')
|
|
798
|
-
items.forEach((item, i) => {
|
|
799
|
-
item.classList.toggle('selected', i === 0)
|
|
800
|
-
})
|
|
801
|
-
}
|
|
802
|
-
|
|
803
|
-
function hideSlashHint() {
|
|
804
|
-
slashHint.classList.add('hidden')
|
|
805
|
-
}
|
|
806
|
-
|
|
807
|
-
// Click to select a hint item
|
|
808
|
-
slashHint.addEventListener('click', e => {
|
|
809
|
-
const item = e.target.closest('.slash-hint-item')
|
|
810
|
-
if (item) {
|
|
811
|
-
const cmd = item.dataset.cmd
|
|
812
|
-
chatInput.value = '/' + cmd
|
|
813
|
-
hideSlashHint()
|
|
814
|
-
chatInput.focus()
|
|
815
|
-
}
|
|
816
|
-
})
|
|
817
|
-
|
|
818
|
-
// Hover to change selection
|
|
819
|
-
slashHint.addEventListener('mouseover', e => {
|
|
820
|
-
const item = e.target.closest('.slash-hint-item')
|
|
821
|
-
if (item) {
|
|
822
|
-
slashHint.querySelectorAll('.slash-hint-item').forEach(el => el.classList.remove('selected'))
|
|
823
|
-
item.classList.add('selected')
|
|
824
|
-
}
|
|
825
|
-
})
|
|
826
|
-
|
|
827
|
-
// Show/hide hint based on input content
|
|
828
|
-
chatInput.addEventListener('input', () => {
|
|
829
|
-
chatInput.style.height = 'auto'
|
|
830
|
-
chatInput.style.height = Math.min(chatInput.scrollHeight, 150) + 'px'
|
|
831
|
-
|
|
832
|
-
const text = chatInput.value
|
|
833
|
-
// Show hint only when the text starts with / and is only the / (no other chars yet)
|
|
834
|
-
if (text === '/') {
|
|
835
|
-
showSlashHint()
|
|
836
|
-
} else if (!text.startsWith('/') || text.includes(' ')) {
|
|
837
|
-
hideSlashHint()
|
|
838
|
-
}
|
|
839
|
-
})
|
|
840
|
-
|
|
841
|
-
// ── Sidebar Tab Switching ──
|
|
842
|
-
sidebarTabs.forEach(tab => {
|
|
843
|
-
tab.addEventListener('click', () => {
|
|
844
|
-
switchTab(tab.dataset.tab)
|
|
845
|
-
})
|
|
846
|
-
})
|
|
847
|
-
|
|
848
|
-
// ── Heartbeat (keep connection alive) ──
|
|
849
|
-
setInterval(() => {
|
|
850
|
-
if (ws && ws.readyState === WebSocket.OPEN) {
|
|
851
|
-
ws.send(JSON.stringify({ type: 'ping' }))
|
|
852
|
-
}
|
|
853
|
-
}, 30000)
|
|
854
|
-
|
|
855
|
-
// ── Initialize ──
|
|
856
|
-
connect()
|
|
857
|
-
})()
|
|
6
|
+
connect()
|
|
7
|
+
initInput()
|