lonny-agent 0.2.3 → 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.
Files changed (129) hide show
  1. package/README.md +326 -326
  2. package/dist/agent/index.js +1 -1
  3. package/dist/agent/index.js.map +1 -1
  4. package/dist/agent/llm.d.ts +4 -0
  5. package/dist/agent/llm.d.ts.map +1 -1
  6. package/dist/agent/project.d.ts +26 -0
  7. package/dist/agent/project.d.ts.map +1 -0
  8. package/dist/agent/project.js +303 -0
  9. package/dist/agent/project.js.map +1 -0
  10. package/dist/agent/prompt-builder.d.ts +1 -1
  11. package/dist/agent/prompt-builder.d.ts.map +1 -1
  12. package/dist/agent/prompt-builder.js +22 -13
  13. package/dist/agent/prompt-builder.js.map +1 -1
  14. package/dist/agent/providers/anthropic.d.ts.map +1 -1
  15. package/dist/agent/providers/anthropic.js +20 -2
  16. package/dist/agent/providers/anthropic.js.map +1 -1
  17. package/dist/agent/providers/google.d.ts.map +1 -1
  18. package/dist/agent/providers/google.js +1 -1
  19. package/dist/agent/providers/google.js.map +1 -1
  20. package/dist/agent/providers/ollama.d.ts.map +1 -1
  21. package/dist/agent/providers/ollama.js +2 -1
  22. package/dist/agent/providers/ollama.js.map +1 -1
  23. package/dist/agent/providers/openai.d.ts.map +1 -1
  24. package/dist/agent/providers/openai.js +63 -3
  25. package/dist/agent/providers/openai.js.map +1 -1
  26. package/dist/agent/session.d.ts +8 -2
  27. package/dist/agent/session.d.ts.map +1 -1
  28. package/dist/agent/session.js +250 -153
  29. package/dist/agent/session.js.map +1 -1
  30. package/dist/config/index.d.ts.map +1 -1
  31. package/dist/config/index.js +7 -1
  32. package/dist/config/index.js.map +1 -1
  33. package/dist/tools/__tests__/bash.test.js +1 -1
  34. package/dist/tools/__tests__/bash.test.js.map +1 -1
  35. package/dist/tools/__tests__/edit.test.js +875 -371
  36. package/dist/tools/__tests__/edit.test.js.map +1 -1
  37. package/dist/tools/__tests__/grep.test.js +32 -0
  38. package/dist/tools/__tests__/grep.test.js.map +1 -1
  39. package/dist/tools/__tests__/sed.test.d.ts +2 -0
  40. package/dist/tools/__tests__/sed.test.d.ts.map +1 -0
  41. package/dist/tools/__tests__/sed.test.js +228 -0
  42. package/dist/tools/__tests__/sed.test.js.map +1 -0
  43. package/dist/tools/bash.d.ts.map +1 -1
  44. package/dist/tools/bash.js +83 -10
  45. package/dist/tools/bash.js.map +1 -1
  46. package/dist/tools/edit.d.ts +58 -0
  47. package/dist/tools/edit.d.ts.map +1 -1
  48. package/dist/tools/edit.js +366 -149
  49. package/dist/tools/edit.js.map +1 -1
  50. package/dist/tools/grep.d.ts.map +1 -1
  51. package/dist/tools/grep.js +54 -30
  52. package/dist/tools/grep.js.map +1 -1
  53. package/dist/tools/read.d.ts +6 -0
  54. package/dist/tools/read.d.ts.map +1 -1
  55. package/dist/tools/read.js +42 -15
  56. package/dist/tools/read.js.map +1 -1
  57. package/dist/tools/registry.d.ts +0 -6
  58. package/dist/tools/registry.d.ts.map +1 -1
  59. package/dist/tools/registry.js +2 -33
  60. package/dist/tools/registry.js.map +1 -1
  61. package/dist/tools/sed.d.ts +4 -0
  62. package/dist/tools/sed.d.ts.map +1 -0
  63. package/dist/tools/sed.js +121 -0
  64. package/dist/tools/sed.js.map +1 -0
  65. package/dist/tools/write_plan.d.ts +1 -1
  66. package/dist/tools/write_plan.d.ts.map +1 -1
  67. package/dist/tools/write_plan.js +14 -4
  68. package/dist/tools/write_plan.js.map +1 -1
  69. package/dist/tui/index.d.ts.map +1 -1
  70. package/dist/tui/index.js +5 -4
  71. package/dist/tui/index.js.map +1 -1
  72. package/dist/web/index.d.ts.map +1 -1
  73. package/dist/web/index.js +30 -7
  74. package/dist/web/index.js.map +1 -1
  75. package/dist/web/public/app.js +5 -855
  76. package/dist/web/public/confirm.js +70 -0
  77. package/dist/web/public/index.html +121 -120
  78. package/dist/web/public/input.js +107 -0
  79. package/dist/web/public/messages.js +395 -0
  80. package/dist/web/public/sidebar.js +82 -0
  81. package/dist/web/public/state.js +72 -0
  82. package/dist/web/public/style.css +996 -949
  83. package/dist/web/public/utils.js +37 -0
  84. package/dist/web/public/websocket.js +267 -0
  85. package/dist/web/public/ws.js +19 -0
  86. package/dist/web/session-bridge.d.ts.map +1 -1
  87. package/dist/web/session-bridge.js +3 -7
  88. package/dist/web/session-bridge.js.map +1 -1
  89. package/package.json +55 -54
  90. package/scripts/copy-native.mjs +24 -0
  91. package/scripts/copy-web.mjs +15 -0
  92. package/src/agent/index.ts +25 -25
  93. package/src/agent/llm.ts +4 -0
  94. package/src/agent/project.ts +366 -0
  95. package/src/agent/prompt-builder.ts +23 -13
  96. package/src/agent/providers/anthropic.ts +179 -172
  97. package/src/agent/providers/google.ts +210 -211
  98. package/src/agent/providers/ollama.ts +186 -182
  99. package/src/agent/providers/openai.ts +73 -5
  100. package/src/agent/session.ts +275 -153
  101. package/src/config/index.ts +342 -335
  102. package/src/tools/__tests__/bash.test.ts +1 -1
  103. package/src/tools/__tests__/edit.test.ts +981 -385
  104. package/src/tools/__tests__/grep.test.ts +35 -0
  105. package/src/tools/bash.ts +97 -11
  106. package/src/tools/edit.ts +414 -154
  107. package/src/tools/git.ts +76 -76
  108. package/src/tools/grep.ts +57 -30
  109. package/src/tools/read.ts +61 -25
  110. package/src/tools/registry.ts +231 -265
  111. package/src/tools/write_plan.ts +21 -5
  112. package/src/tui/index.ts +786 -785
  113. package/src/web/index.ts +29 -7
  114. package/src/web/public/app.js +5 -855
  115. package/src/web/public/confirm.js +70 -0
  116. package/src/web/public/index.html +121 -120
  117. package/src/web/public/input.js +107 -0
  118. package/src/web/public/messages.js +395 -0
  119. package/src/web/public/sidebar.js +82 -0
  120. package/src/web/public/state.js +72 -0
  121. package/src/web/public/style.css +996 -949
  122. package/src/web/public/utils.js +37 -0
  123. package/src/web/public/websocket.js +267 -0
  124. package/src/web/public/ws.js +19 -0
  125. package/src/web/session-bridge.ts +193 -194
  126. package/.kilo/plans/1780064105789-mighty-pixel.md +0 -171
  127. package/.kilo/plans/1780293725888-quick-wizard.md +0 -62
  128. package/.lonny/plan-web-cwd-status.md +0 -38
  129. package/src/tools/exec.ts +0 -348
@@ -0,0 +1,70 @@
1
+ /* ── Tool Confirm Dialog ── */
2
+
3
+ import { state } from './state.js'
4
+ import { escapeHtml } from './utils.js'
5
+ import { sendWsMsg } from './ws.js'
6
+
7
+ export function showConfirmDialog(toolCalls) {
8
+ const overlay = document.getElementById('confirm-overlay')
9
+ const list = document.getElementById('confirm-tool-list')
10
+ if (!overlay || !list) return
11
+ list.innerHTML = ''
12
+ for (const tc of toolCalls) {
13
+ const detail = tc.input?.file_path || tc.input?.command || tc.input?.package_name || ''
14
+ const item = document.createElement('div')
15
+ item.className = 'confirm-tool-item'
16
+ item.innerHTML =
17
+ `<span class="confirm-tool-name">${escapeHtml(tc.name)}</span>` +
18
+ (detail
19
+ ? ` <span class="confirm-tool-input">${escapeHtml(detail.length > 80 ? `${detail.slice(0, 80)}…` : detail)}</span>`
20
+ : '')
21
+ list.appendChild(item)
22
+ }
23
+ overlay.classList.remove('hidden')
24
+
25
+ state.pendingConfirmResolve = null
26
+
27
+ function cleanup(approved) {
28
+ overlay.classList.add('hidden')
29
+ allowBtn.removeEventListener('click', onAllow)
30
+ rejectBtn.removeEventListener('click', onReject)
31
+ if (state.pendingConfirmResolve) {
32
+ state.pendingConfirmResolve(approved)
33
+ state.pendingConfirmResolve = null
34
+ }
35
+ }
36
+
37
+ const allowBtn = document.getElementById('confirm-allow-btn')
38
+ const rejectBtn = document.getElementById('confirm-reject-btn')
39
+ const onAllow = () => {
40
+ sendWsMsg({ type: 'tool_confirm_response', approved: true })
41
+ cleanup(true)
42
+ }
43
+ const onReject = () => {
44
+ sendWsMsg({ type: 'tool_confirm_response', approved: false })
45
+ cleanup(false)
46
+ }
47
+ allowBtn.addEventListener('click', onAllow)
48
+ rejectBtn.addEventListener('click', onReject)
49
+ }
50
+
51
+ export function closeConfirmDialog() {
52
+ const overlay = document.getElementById('confirm-overlay')
53
+ if (overlay && !overlay.classList.contains('hidden')) {
54
+ overlay.classList.add('hidden')
55
+ if (state.pendingConfirmResolve) {
56
+ state.pendingConfirmResolve(false)
57
+ state.pendingConfirmResolve = null
58
+ }
59
+ }
60
+ }
61
+
62
+ // Click overlay background to close (added once)
63
+ const confirmOverlay = document.getElementById('confirm-overlay')
64
+ if (confirmOverlay) {
65
+ confirmOverlay.addEventListener('click', e => {
66
+ if (e.target === e.currentTarget) {
67
+ closeConfirmDialog()
68
+ }
69
+ })
70
+ }
@@ -1,120 +1,121 @@
1
- <!DOCTYPE html>
2
- <html lang="zh-CN">
3
- <head>
4
- <meta charset="UTF-8">
5
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
- <title>Lonny - AI 编码代理</title>
7
- <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/normalize.css@8.0.1/normalize.css">
8
- <link rel="stylesheet" href="/style.css">
9
- </head>
10
- <body>
11
- <!-- Status Bar -->
12
- <header id="status-bar">
13
- <div class="status-left">
14
- <span class="logo">lonny</span>
15
- <span class="separator">|</span>
16
- <span id="status-indicator" class="status-idle">○ idle</span>
17
- <span class="separator">|</span>
18
- <span id="mode-display" class="mode-badge">code</span>
19
- <span class="separator">|</span>
20
- <span id="cwd-display" class="cwd-text"></span>
21
- </div>
22
- <div class="status-right">
23
- <span id="model-display" class="model-name">loading...</span>
24
- <span class="separator">|</span>
25
- <span class="token-stats">
26
- <span class="token-in" id="token-in">0</span>
27
- <span class="token-sep">/</span>
28
- <span class="token-out" id="token-out">0</span>
29
- <span class="token-calls" id="token-calls">(0)</span>
30
- </span>
31
- <span class="separator" id="balance-sep" style="display:none">|</span>
32
- <span id="balance-display" class="balance-display" style="display:none"></span>
33
- </div>
34
- </header>
35
-
36
- <!-- App Content (chat + sidebar) -->
37
- <div id="app-content">
38
- <!-- Chat Panel -->
39
- <div id="chat-panel">
40
- <!-- Chat Container -->
41
- <main id="chat-container">
42
- <div id="messages">
43
- <!-- Messages will be rendered here -->
44
- </div>
45
- </main>
46
-
47
- <!-- Input Area -->
48
- <footer id="input-area">
49
- <div class="input-wrapper">
50
- <div class="input-container">
51
- <div id="slash-hint" class="slash-hint hidden">
52
- <div class="slash-hint-header">Commands</div>
53
- <div class="slash-hint-item" data-cmd="mode code|plan|ask">/mode <span class="hint-arg">code|plan|ask</span> <span class="hint-desc">Switch mode</span></div>
54
- <div class="slash-hint-item" data-cmd="model ">/model <span class="hint-arg">&lt;name&gt;</span> <span class="hint-desc">Switch model</span></div>
55
- <div class="slash-hint-item" data-cmd="new">/new <span class="hint-desc">Start a new session</span></div>
56
- <div class="slash-hint-item" data-cmd="stop">/stop <span class="hint-desc">Stop the running agent</span></div>
57
- <div class="slash-hint-item" data-cmd="skills">/skills <span class="hint-desc">List active skills</span></div>
58
- <div class="slash-hint-item" data-cmd="prompts">/prompts <span class="hint-desc">List prompt templates</span></div>
59
- <div class="slash-hint-item" data-cmd="init">/init <span class="hint-desc">Create .lonny/skills/ &amp; prompts/</span></div>
60
- <div class="slash-hint-item" data-cmd="help">/help <span class="hint-desc">Show this help</span></div>
61
- </div>
62
- <textarea id="chat-input" rows="3" placeholder="Type a message... (type / for commands)"></textarea>
63
- </div>
64
- <button id="send-btn" title="Send">
65
- <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
66
- <path d="M22 2L11 13"/><path d="M22 2L15 22L11 13L2 9L22 2Z"/>
67
- </svg>
68
- </button>
69
- <button id="stop-btn" class="stop-btn hidden" title="Stop">
70
- <svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor">
71
- <rect x="6" y="6" width="12" height="12" rx="2"/>
72
- </svg>
73
- </button>
74
- </div>
75
- </footer>
76
- </div>
77
-
78
- <!-- Right Sidebar -->
79
- <aside id="sidebar">
80
- <div class="sidebar-tabs">
81
- <button class="sidebar-tab active" data-tab="plans">░ Plans</button>
82
- <button class="sidebar-tab" data-tab="todos">☑ Todo</button>
83
- </div>
84
- <div class="sidebar-content">
85
- <div class="sidebar-pane active" id="plans-pane">
86
- <div id="plans-placeholder" class="sidebar-placeholder">Loading plans...</div>
87
- <div id="plans-list"></div>
88
- </div>
89
- <div class="sidebar-pane" id="todos-pane">
90
- <div id="todos-placeholder" class="sidebar-placeholder">Loading todos...</div>
91
- <div id="todos-list"></div>
92
- </div>
93
- </div>
94
- </aside>
95
- </div>
96
-
97
- <!-- Connection status overlay -->
98
- <div id="connection-overlay" class="overlay hidden">
99
- <div class="overlay-content">
100
- <div class="spinner"></div>
101
- <p>Connecting to Lonny...</p>
102
- </div>
103
- </div>
104
-
105
- <!-- Tool confirm dialog -->
106
- <div id="confirm-overlay" class="overlay hidden">
107
- <div class="confirm-dialog">
108
- <div class="confirm-header">Allow these tool calls?</div>
109
- <div id="confirm-tool-list" class="confirm-tool-list"></div>
110
- <div class="confirm-actions">
111
- <button id="confirm-reject-btn" class="confirm-btn confirm-btn-reject">Reject</button>
112
- <button id="confirm-allow-btn" class="confirm-btn confirm-btn-allow">Allow</button>
113
- </div>
114
- </div>
115
- </div>
116
-
117
- <script src="https://cdn.jsdelivr.net/npm/marked@15.0.4/marked.min.js"></script>
118
- <script src="/app.js"></script>
119
- </body>
120
- </html>
1
+ <!DOCTYPE html>
2
+ <html lang="zh-CN">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Lonny - AI 编码代理</title>
7
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/normalize.css@8.0.1/normalize.css">
8
+ <link rel="stylesheet" href="/style.css">
9
+ </head>
10
+ <body>
11
+ <!-- Status Bar -->
12
+ <header id="status-bar">
13
+ <div class="status-left">
14
+ <span class="logo">lonny</span>
15
+ <span class="separator">|</span>
16
+ <span id="status-indicator" class="status-idle">○ idle</span>
17
+ <span class="separator">|</span>
18
+ <span id="mode-display" class="mode-badge">code</span>
19
+ <span class="separator">|</span>
20
+ <span id="cwd-display" class="cwd-text"></span>
21
+ </div>
22
+ <div class="status-right">
23
+ <span id="model-display" class="model-name">loading...</span>
24
+ <span class="separator">|</span>
25
+ <span class="token-stats">
26
+ <span class="token-in" id="token-in">0</span>
27
+ <span class="token-sep">/</span>
28
+ <span class="token-out" id="token-out">0</span>
29
+ <span class="token-calls" id="token-calls">(0)</span>
30
+ <span class="token-cache hidden" id="token-cache"></span>
31
+ </span>
32
+ <span class="separator" id="balance-sep" style="display:none">|</span>
33
+ <span id="balance-display" class="balance-display" style="display:none"></span>
34
+ </div>
35
+ </header>
36
+
37
+ <!-- App Content (chat + sidebar) -->
38
+ <div id="app-content">
39
+ <!-- Chat Panel -->
40
+ <div id="chat-panel">
41
+ <!-- Chat Container -->
42
+ <main id="chat-container">
43
+ <div id="messages">
44
+ <!-- Messages will be rendered here -->
45
+ </div>
46
+ </main>
47
+
48
+ <!-- Input Area -->
49
+ <footer id="input-area">
50
+ <div class="input-wrapper">
51
+ <div class="input-container">
52
+ <div id="slash-hint" class="slash-hint hidden">
53
+ <div class="slash-hint-header">Commands</div>
54
+ <div class="slash-hint-item" data-cmd="mode code|plan|ask">/mode <span class="hint-arg">code|plan|ask</span> <span class="hint-desc">Switch mode</span></div>
55
+ <div class="slash-hint-item" data-cmd="model ">/model <span class="hint-arg">&lt;name&gt;</span> <span class="hint-desc">Switch model</span></div>
56
+ <div class="slash-hint-item" data-cmd="new">/new <span class="hint-desc">Start a new session</span></div>
57
+ <div class="slash-hint-item" data-cmd="stop">/stop <span class="hint-desc">Stop the running agent</span></div>
58
+ <div class="slash-hint-item" data-cmd="skills">/skills <span class="hint-desc">List active skills</span></div>
59
+ <div class="slash-hint-item" data-cmd="prompts">/prompts <span class="hint-desc">List prompt templates</span></div>
60
+ <div class="slash-hint-item" data-cmd="init">/init <span class="hint-desc">Create .lonny/skills/ &amp; prompts/</span></div>
61
+ <div class="slash-hint-item" data-cmd="help">/help <span class="hint-desc">Show this help</span></div>
62
+ </div>
63
+ <textarea id="chat-input" rows="3" placeholder="Type a message... (type / for commands)"></textarea>
64
+ </div>
65
+ <button id="send-btn" title="Send">
66
+ <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
67
+ <path d="M22 2L11 13"/><path d="M22 2L15 22L11 13L2 9L22 2Z"/>
68
+ </svg>
69
+ </button>
70
+ <button id="stop-btn" class="stop-btn hidden" title="Stop">
71
+ <svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor">
72
+ <rect x="6" y="6" width="12" height="12" rx="2"/>
73
+ </svg>
74
+ </button>
75
+ </div>
76
+ </footer>
77
+ </div>
78
+
79
+ <!-- Right Sidebar -->
80
+ <aside id="sidebar">
81
+ <div class="sidebar-tabs">
82
+ <button class="sidebar-tab active" data-tab="plans">░ Plans</button>
83
+ <button class="sidebar-tab" data-tab="todos">☑ Todo</button>
84
+ </div>
85
+ <div class="sidebar-content">
86
+ <div class="sidebar-pane active" id="plans-pane">
87
+ <div id="plans-placeholder" class="sidebar-placeholder">Loading plans...</div>
88
+ <div id="plans-list"></div>
89
+ </div>
90
+ <div class="sidebar-pane" id="todos-pane">
91
+ <div id="todos-placeholder" class="sidebar-placeholder">Loading todos...</div>
92
+ <div id="todos-list"></div>
93
+ </div>
94
+ </div>
95
+ </aside>
96
+ </div>
97
+
98
+ <!-- Connection status overlay -->
99
+ <div id="connection-overlay" class="overlay hidden">
100
+ <div class="overlay-content">
101
+ <div class="spinner"></div>
102
+ <p>Connecting to Lonny...</p>
103
+ </div>
104
+ </div>
105
+
106
+ <!-- Tool confirm dialog -->
107
+ <div id="confirm-overlay" class="overlay hidden">
108
+ <div class="confirm-dialog">
109
+ <div class="confirm-header">Allow these tool calls?</div>
110
+ <div id="confirm-tool-list" class="confirm-tool-list"></div>
111
+ <div class="confirm-actions">
112
+ <button id="confirm-reject-btn" class="confirm-btn confirm-btn-reject">Reject</button>
113
+ <button id="confirm-allow-btn" class="confirm-btn confirm-btn-allow">Allow</button>
114
+ </div>
115
+ </div>
116
+ </div>
117
+
118
+ <script src="https://cdn.jsdelivr.net/npm/marked@15.0.4/marked.min.js"></script>
119
+ <script type="module" src="/app.js"></script>
120
+ </body>
121
+ </html>
@@ -0,0 +1,107 @@
1
+ /* ── Input Handling, Event Listeners, Slash Hints ── */
2
+
3
+ import { addErrorMessage, addUserMessage } from './messages.js'
4
+ import { switchTab } from './sidebar.js'
5
+ import { chatInput, sendBtn, sidebarTabs, slashHint, state, stopBtn } from './state.js'
6
+ import { sendWsMsg } from './ws.js'
7
+
8
+ function sendMessage() {
9
+ const text = chatInput.value.trim()
10
+ if (!text || state.isRunning) return
11
+
12
+ addUserMessage(text)
13
+ chatInput.value = ''
14
+
15
+ if (!sendWsMsg({ type: 'message', text })) {
16
+ addErrorMessage('Not connected to server.')
17
+ }
18
+ }
19
+
20
+ function showSlashHint() {
21
+ slashHint.classList.remove('hidden')
22
+ const items = slashHint.querySelectorAll('.slash-hint-item')
23
+ items.forEach((item, i) => {
24
+ item.classList.toggle('selected', i === 0)
25
+ })
26
+ }
27
+
28
+ function hideSlashHint() {
29
+ slashHint.classList.add('hidden')
30
+ }
31
+
32
+ export function initInput() {
33
+ // Send button
34
+ sendBtn.addEventListener('click', sendMessage)
35
+
36
+ // Stop button
37
+ stopBtn.addEventListener('click', () => {
38
+ sendWsMsg({ type: 'stop' })
39
+ stopBtn.disabled = true
40
+ })
41
+
42
+ // Chat input keyboard
43
+ chatInput.addEventListener('keydown', e => {
44
+ if (e.key === 'Enter' && !e.shiftKey) {
45
+ e.preventDefault()
46
+ sendMessage()
47
+ }
48
+ if (e.key === 'Tab' && !slashHint.classList.contains('hidden')) {
49
+ e.preventDefault()
50
+ const selected = slashHint.querySelector('.slash-hint-item.selected')
51
+ if (selected) {
52
+ const cmd = selected.dataset.cmd
53
+ chatInput.value = `/${cmd}`
54
+ hideSlashHint()
55
+ chatInput.focus()
56
+ }
57
+ }
58
+ if (e.key === 'Escape') {
59
+ hideSlashHint()
60
+ }
61
+ })
62
+
63
+ // Slash hint: click to select
64
+ slashHint.addEventListener('click', e => {
65
+ const item = e.target.closest('.slash-hint-item')
66
+ if (item) {
67
+ const cmd = item.dataset.cmd
68
+ chatInput.value = `/${cmd}`
69
+ hideSlashHint()
70
+ chatInput.focus()
71
+ }
72
+ })
73
+
74
+ // Slash hint: hover to change selection
75
+ slashHint.addEventListener('mouseover', e => {
76
+ const item = e.target.closest('.slash-hint-item')
77
+ if (item) {
78
+ slashHint.querySelectorAll('.slash-hint-item').forEach(el => el.classList.remove('selected'))
79
+ item.classList.add('selected')
80
+ }
81
+ })
82
+
83
+ // Show/hide hint based on input content
84
+ chatInput.addEventListener('input', () => {
85
+ chatInput.style.height = 'auto'
86
+ chatInput.style.height = `${Math.min(chatInput.scrollHeight, 150)}px`
87
+
88
+ const text = chatInput.value
89
+ if (text === '/') {
90
+ showSlashHint()
91
+ } else if (!text.startsWith('/') || text.includes(' ')) {
92
+ hideSlashHint()
93
+ }
94
+ })
95
+
96
+ // Sidebar tab switching
97
+ sidebarTabs.forEach(tab => {
98
+ tab.addEventListener('click', () => {
99
+ switchTab(tab.dataset.tab)
100
+ })
101
+ })
102
+
103
+ // Heartbeat
104
+ setInterval(() => {
105
+ sendWsMsg({ type: 'ping' })
106
+ }, 30000)
107
+ }