xibecode 1.0.2 → 1.0.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 (288) hide show
  1. package/dist/commands/chat.d.ts +0 -1
  2. package/dist/commands/chat.d.ts.map +1 -1
  3. package/dist/commands/chat.js +10 -7
  4. package/dist/commands/chat.js.map +1 -1
  5. package/dist/commands/config.d.ts.map +1 -1
  6. package/dist/commands/config.js +5 -3
  7. package/dist/commands/config.js.map +1 -1
  8. package/dist/commands/diagnostics.js +1 -1
  9. package/dist/commands/diagnostics.js.map +1 -1
  10. package/dist/commands/mcp.js +1 -1
  11. package/dist/commands/mcp.js.map +1 -1
  12. package/dist/commands/resume.js +1 -1
  13. package/dist/commands/resume.js.map +1 -1
  14. package/dist/commands/run-pr.d.ts.map +1 -1
  15. package/dist/commands/run-pr.js +13 -10
  16. package/dist/commands/run-pr.js.map +1 -1
  17. package/dist/commands/run.d.ts.map +1 -1
  18. package/dist/commands/run.js +17 -14
  19. package/dist/commands/run.js.map +1 -1
  20. package/dist/commands/skills.d.ts.map +1 -1
  21. package/dist/commands/skills.js +3 -2
  22. package/dist/commands/skills.js.map +1 -1
  23. package/dist/components/AssistantMarkdown.js +1 -1
  24. package/dist/components/AssistantMarkdown.js.map +1 -1
  25. package/dist/index.js +2 -39
  26. package/dist/index.js.map +1 -1
  27. package/dist/ui/claude-style-chat.d.ts.map +1 -1
  28. package/dist/ui/claude-style-chat.js +15 -11
  29. package/dist/ui/claude-style-chat.js.map +1 -1
  30. package/dist/utils/built-in-skills-dir.d.ts +7 -0
  31. package/dist/utils/built-in-skills-dir.d.ts.map +1 -0
  32. package/dist/utils/built-in-skills-dir.js +11 -0
  33. package/dist/utils/built-in-skills-dir.js.map +1 -0
  34. package/dist/utils/config.d.ts +2 -119
  35. package/dist/utils/config.d.ts.map +1 -1
  36. package/dist/utils/config.js +3 -88
  37. package/dist/utils/config.js.map +1 -1
  38. package/package.json +11 -26
  39. package/dist/commands/punycode.d.ts +0 -5
  40. package/dist/commands/punycode.d.ts.map +0 -1
  41. package/dist/commands/punycode.js +0 -48
  42. package/dist/commands/punycode.js.map +0 -1
  43. package/dist/commands/tui.d.ts +0 -9
  44. package/dist/commands/tui.d.ts.map +0 -1
  45. package/dist/commands/tui.js +0 -83
  46. package/dist/commands/tui.js.map +0 -1
  47. package/dist/core/agent-tool-policies.d.ts +0 -5
  48. package/dist/core/agent-tool-policies.d.ts.map +0 -1
  49. package/dist/core/agent-tool-policies.js +0 -18
  50. package/dist/core/agent-tool-policies.js.map +0 -1
  51. package/dist/core/agent.d.ts +0 -181
  52. package/dist/core/agent.d.ts.map +0 -1
  53. package/dist/core/agent.js +0 -1777
  54. package/dist/core/agent.js.map +0 -1
  55. package/dist/core/background-agent.d.ts +0 -23
  56. package/dist/core/background-agent.d.ts.map +0 -1
  57. package/dist/core/background-agent.js +0 -175
  58. package/dist/core/background-agent.js.map +0 -1
  59. package/dist/core/code-graph.d.ts +0 -18
  60. package/dist/core/code-graph.d.ts.map +0 -1
  61. package/dist/core/code-graph.js +0 -105
  62. package/dist/core/code-graph.js.map +0 -1
  63. package/dist/core/conflict-solver.d.ts +0 -26
  64. package/dist/core/conflict-solver.d.ts.map +0 -1
  65. package/dist/core/conflict-solver.js +0 -108
  66. package/dist/core/conflict-solver.js.map +0 -1
  67. package/dist/core/context-compactor.d.ts +0 -10
  68. package/dist/core/context-compactor.d.ts.map +0 -1
  69. package/dist/core/context-compactor.js +0 -158
  70. package/dist/core/context-compactor.js.map +0 -1
  71. package/dist/core/context-pruner.d.ts +0 -19
  72. package/dist/core/context-pruner.d.ts.map +0 -1
  73. package/dist/core/context-pruner.js +0 -103
  74. package/dist/core/context-pruner.js.map +0 -1
  75. package/dist/core/context.d.ts +0 -82
  76. package/dist/core/context.d.ts.map +0 -1
  77. package/dist/core/context.js +0 -273
  78. package/dist/core/context.js.map +0 -1
  79. package/dist/core/conversation-recovery.d.ts +0 -9
  80. package/dist/core/conversation-recovery.d.ts.map +0 -1
  81. package/dist/core/conversation-recovery.js +0 -15
  82. package/dist/core/conversation-recovery.js.map +0 -1
  83. package/dist/core/debug-workflow.d.ts +0 -9
  84. package/dist/core/debug-workflow.d.ts.map +0 -1
  85. package/dist/core/debug-workflow.js +0 -19
  86. package/dist/core/debug-workflow.js.map +0 -1
  87. package/dist/core/docs-scraper.d.ts +0 -40
  88. package/dist/core/docs-scraper.d.ts.map +0 -1
  89. package/dist/core/docs-scraper.js +0 -386
  90. package/dist/core/docs-scraper.js.map +0 -1
  91. package/dist/core/editor.d.ts +0 -87
  92. package/dist/core/editor.d.ts.map +0 -1
  93. package/dist/core/editor.js +0 -377
  94. package/dist/core/editor.js.map +0 -1
  95. package/dist/core/export.d.ts +0 -11
  96. package/dist/core/export.d.ts.map +0 -1
  97. package/dist/core/export.js +0 -54
  98. package/dist/core/export.js.map +0 -1
  99. package/dist/core/history-manager.d.ts +0 -75
  100. package/dist/core/history-manager.d.ts.map +0 -1
  101. package/dist/core/history-manager.js +0 -146
  102. package/dist/core/history-manager.js.map +0 -1
  103. package/dist/core/marketplace-client.d.ts +0 -52
  104. package/dist/core/marketplace-client.d.ts.map +0 -1
  105. package/dist/core/marketplace-client.js +0 -71
  106. package/dist/core/marketplace-client.js.map +0 -1
  107. package/dist/core/mcp/mcp-config.d.ts +0 -10
  108. package/dist/core/mcp/mcp-config.d.ts.map +0 -1
  109. package/dist/core/mcp/mcp-config.js +0 -70
  110. package/dist/core/mcp/mcp-config.js.map +0 -1
  111. package/dist/core/mcp/mcp-policy.d.ts +0 -17
  112. package/dist/core/mcp/mcp-policy.d.ts.map +0 -1
  113. package/dist/core/mcp/mcp-policy.js +0 -56
  114. package/dist/core/mcp/mcp-policy.js.map +0 -1
  115. package/dist/core/mcp/oauth-flow.d.ts +0 -30
  116. package/dist/core/mcp/oauth-flow.d.ts.map +0 -1
  117. package/dist/core/mcp/oauth-flow.js +0 -230
  118. package/dist/core/mcp/oauth-flow.js.map +0 -1
  119. package/dist/core/mcp/oauth-store.d.ts +0 -13
  120. package/dist/core/mcp/oauth-store.d.ts.map +0 -1
  121. package/dist/core/mcp/oauth-store.js +0 -68
  122. package/dist/core/mcp/oauth-store.js.map +0 -1
  123. package/dist/core/mcp/resolve-mcp-servers.d.ts +0 -16
  124. package/dist/core/mcp/resolve-mcp-servers.d.ts.map +0 -1
  125. package/dist/core/mcp/resolve-mcp-servers.js +0 -83
  126. package/dist/core/mcp/resolve-mcp-servers.js.map +0 -1
  127. package/dist/core/mcp-client.d.ts +0 -99
  128. package/dist/core/mcp-client.d.ts.map +0 -1
  129. package/dist/core/mcp-client.js +0 -315
  130. package/dist/core/mcp-client.js.map +0 -1
  131. package/dist/core/memory-promotions.d.ts +0 -15
  132. package/dist/core/memory-promotions.d.ts.map +0 -1
  133. package/dist/core/memory-promotions.js +0 -38
  134. package/dist/core/memory-promotions.js.map +0 -1
  135. package/dist/core/memory.d.ts +0 -32
  136. package/dist/core/memory.d.ts.map +0 -1
  137. package/dist/core/memory.js +0 -121
  138. package/dist/core/memory.js.map +0 -1
  139. package/dist/core/modes.d.ts +0 -432
  140. package/dist/core/modes.d.ts.map +0 -1
  141. package/dist/core/modes.js +0 -1088
  142. package/dist/core/modes.js.map +0 -1
  143. package/dist/core/pattern-miner.d.ts +0 -43
  144. package/dist/core/pattern-miner.d.ts.map +0 -1
  145. package/dist/core/pattern-miner.js +0 -123
  146. package/dist/core/pattern-miner.js.map +0 -1
  147. package/dist/core/permission-store.d.ts +0 -15
  148. package/dist/core/permission-store.d.ts.map +0 -1
  149. package/dist/core/permission-store.js +0 -30
  150. package/dist/core/permission-store.js.map +0 -1
  151. package/dist/core/permissions.d.ts +0 -33
  152. package/dist/core/permissions.d.ts.map +0 -1
  153. package/dist/core/permissions.js +0 -141
  154. package/dist/core/permissions.js.map +0 -1
  155. package/dist/core/plan-artifacts.d.ts +0 -10
  156. package/dist/core/plan-artifacts.d.ts.map +0 -1
  157. package/dist/core/plan-artifacts.js +0 -60
  158. package/dist/core/plan-artifacts.js.map +0 -1
  159. package/dist/core/plan-session.d.ts +0 -25
  160. package/dist/core/plan-session.d.ts.map +0 -1
  161. package/dist/core/plan-session.js +0 -99
  162. package/dist/core/plan-session.js.map +0 -1
  163. package/dist/core/planMode.d.ts +0 -51
  164. package/dist/core/planMode.d.ts.map +0 -1
  165. package/dist/core/planMode.js +0 -245
  166. package/dist/core/planMode.js.map +0 -1
  167. package/dist/core/plugins.d.ts +0 -96
  168. package/dist/core/plugins.d.ts.map +0 -1
  169. package/dist/core/plugins.js +0 -202
  170. package/dist/core/plugins.js.map +0 -1
  171. package/dist/core/session-bridge.d.ts +0 -128
  172. package/dist/core/session-bridge.d.ts.map +0 -1
  173. package/dist/core/session-bridge.js +0 -328
  174. package/dist/core/session-bridge.js.map +0 -1
  175. package/dist/core/session-manager.d.ts +0 -80
  176. package/dist/core/session-manager.d.ts.map +0 -1
  177. package/dist/core/session-manager.js +0 -166
  178. package/dist/core/session-manager.js.map +0 -1
  179. package/dist/core/session-memory.d.ts +0 -45
  180. package/dist/core/session-memory.d.ts.map +0 -1
  181. package/dist/core/session-memory.js +0 -103
  182. package/dist/core/session-memory.js.map +0 -1
  183. package/dist/core/skill-selection.d.ts +0 -36
  184. package/dist/core/skill-selection.d.ts.map +0 -1
  185. package/dist/core/skill-selection.js +0 -172
  186. package/dist/core/skill-selection.js.map +0 -1
  187. package/dist/core/skills-sh-client.d.ts +0 -19
  188. package/dist/core/skills-sh-client.d.ts.map +0 -1
  189. package/dist/core/skills-sh-client.js +0 -75
  190. package/dist/core/skills-sh-client.js.map +0 -1
  191. package/dist/core/skills.d.ts +0 -97
  192. package/dist/core/skills.d.ts.map +0 -1
  193. package/dist/core/skills.js +0 -339
  194. package/dist/core/skills.js.map +0 -1
  195. package/dist/core/swarm.d.ts +0 -34
  196. package/dist/core/swarm.d.ts.map +0 -1
  197. package/dist/core/swarm.js +0 -111
  198. package/dist/core/swarm.js.map +0 -1
  199. package/dist/core/task-status.d.ts +0 -13
  200. package/dist/core/task-status.d.ts.map +0 -1
  201. package/dist/core/task-status.js +0 -17
  202. package/dist/core/task-status.js.map +0 -1
  203. package/dist/core/tool-orchestrator.d.ts +0 -30
  204. package/dist/core/tool-orchestrator.d.ts.map +0 -1
  205. package/dist/core/tool-orchestrator.js +0 -89
  206. package/dist/core/tool-orchestrator.js.map +0 -1
  207. package/dist/core/tools.d.ts +0 -462
  208. package/dist/core/tools.d.ts.map +0 -1
  209. package/dist/core/tools.js +0 -2916
  210. package/dist/core/tools.js.map +0 -1
  211. package/dist/core/transcript-cleanup.d.ts +0 -8
  212. package/dist/core/transcript-cleanup.d.ts.map +0 -1
  213. package/dist/core/transcript-cleanup.js +0 -52
  214. package/dist/core/transcript-cleanup.js.map +0 -1
  215. package/dist/core/visual-feedback.d.ts +0 -20
  216. package/dist/core/visual-feedback.d.ts.map +0 -1
  217. package/dist/core/visual-feedback.js +0 -117
  218. package/dist/core/visual-feedback.js.map +0 -1
  219. package/dist/tools/browser.d.ts +0 -120
  220. package/dist/tools/browser.d.ts.map +0 -1
  221. package/dist/tools/browser.js +0 -439
  222. package/dist/tools/browser.js.map +0 -1
  223. package/dist/tools/test-generator.d.ts +0 -157
  224. package/dist/tools/test-generator.d.ts.map +0 -1
  225. package/dist/tools/test-generator.js +0 -893
  226. package/dist/tools/test-generator.js.map +0 -1
  227. package/dist/tui/InkApp.d.ts +0 -21
  228. package/dist/tui/InkApp.d.ts.map +0 -1
  229. package/dist/tui/InkApp.js +0 -146
  230. package/dist/tui/InkApp.js.map +0 -1
  231. package/dist/tui/MarkdownMessage.d.ts +0 -16
  232. package/dist/tui/MarkdownMessage.d.ts.map +0 -1
  233. package/dist/tui/MarkdownMessage.js +0 -63
  234. package/dist/tui/MarkdownMessage.js.map +0 -1
  235. package/dist/tui/blessed-chat.d.ts +0 -9
  236. package/dist/tui/blessed-chat.d.ts.map +0 -1
  237. package/dist/tui/blessed-chat.js +0 -887
  238. package/dist/tui/blessed-chat.js.map +0 -1
  239. package/dist/tui/markdown-to-blessed.d.ts +0 -6
  240. package/dist/tui/markdown-to-blessed.d.ts.map +0 -1
  241. package/dist/tui/markdown-to-blessed.js +0 -26
  242. package/dist/tui/markdown-to-blessed.js.map +0 -1
  243. package/dist/ui/ink/App.d.ts +0 -25
  244. package/dist/ui/ink/App.d.ts.map +0 -1
  245. package/dist/ui/ink/App.js +0 -372
  246. package/dist/ui/ink/App.js.map +0 -1
  247. package/dist/utils/at-references.d.ts +0 -14
  248. package/dist/utils/at-references.d.ts.map +0 -1
  249. package/dist/utils/at-references.js +0 -47
  250. package/dist/utils/at-references.js.map +0 -1
  251. package/dist/utils/auto-memory.d.ts +0 -24
  252. package/dist/utils/auto-memory.d.ts.map +0 -1
  253. package/dist/utils/auto-memory.js +0 -153
  254. package/dist/utils/auto-memory.js.map +0 -1
  255. package/dist/utils/git.d.ts +0 -89
  256. package/dist/utils/git.d.ts.map +0 -1
  257. package/dist/utils/git.js +0 -444
  258. package/dist/utils/git.js.map +0 -1
  259. package/dist/utils/mcp-servers-file.d.ts +0 -46
  260. package/dist/utils/mcp-servers-file.d.ts.map +0 -1
  261. package/dist/utils/mcp-servers-file.js +0 -212
  262. package/dist/utils/mcp-servers-file.js.map +0 -1
  263. package/dist/utils/safety.d.ts +0 -60
  264. package/dist/utils/safety.d.ts.map +0 -1
  265. package/dist/utils/safety.js +0 -254
  266. package/dist/utils/safety.js.map +0 -1
  267. package/dist/utils/smithery.d.ts +0 -25
  268. package/dist/utils/smithery.d.ts.map +0 -1
  269. package/dist/utils/smithery.js +0 -50
  270. package/dist/utils/smithery.js.map +0 -1
  271. package/dist/utils/testRunner.d.ts +0 -44
  272. package/dist/utils/testRunner.d.ts.map +0 -1
  273. package/dist/utils/testRunner.js +0 -270
  274. package/dist/utils/testRunner.js.map +0 -1
  275. package/dist/webui/server.d.ts +0 -99
  276. package/dist/webui/server.d.ts.map +0 -1
  277. package/dist/webui/server.js +0 -2619
  278. package/dist/webui/server.js.map +0 -1
  279. package/webui-dist/assets/index-CSla6Lzy.css +0 -32
  280. package/webui-dist/assets/index-G_Z4gzPy.js +0 -457
  281. package/webui-dist/assets/index-G_Z4gzPy.js.map +0 -1
  282. package/webui-dist/assets/xterm-Da5jL1MD.js +0 -10
  283. package/webui-dist/assets/xterm-Da5jL1MD.js.map +0 -1
  284. package/webui-dist/assets/xterm-addon-fit-CMeqLIvm.js +0 -2
  285. package/webui-dist/assets/xterm-addon-fit-CMeqLIvm.js.map +0 -1
  286. package/webui-dist/assets/xterm-addon-web-links-D6m8jNVE.js +0 -2
  287. package/webui-dist/assets/xterm-addon-web-links-D6m8jNVE.js.map +0 -1
  288. package/webui-dist/index.html +0 -15
@@ -1,2619 +0,0 @@
1
- /**
2
- * XibeCode WebUI Server
3
- *
4
- * A lightweight HTTP server that provides:
5
- * - REST API for interacting with XibeCode
6
- * - WebSocket for real-time agent communication
7
- * - Static file serving for the WebUI frontend
8
- *
9
- * @module webui/server
10
- * @since 0.4.0
11
- */
12
- import { createServer } from 'http';
13
- import { WebSocketServer, WebSocket } from 'ws';
14
- import * as fs from 'fs/promises';
15
- import * as fsSync from 'fs';
16
- import * as path from 'path';
17
- import { fileURLToPath } from 'url';
18
- import { spawn } from 'child_process';
19
- import { ConfigManager, PROVIDER_CONFIGS } from '../utils/config.js';
20
- import { SkillManager } from '../core/skills.js';
21
- import { EnhancedAgent } from '../core/agent.js';
22
- import { CodingToolExecutor } from '../core/tools.js';
23
- import { GitUtils } from '../utils/git.js';
24
- import { TestRunnerDetector } from '../utils/testRunner.js';
25
- import { TestGenerator, writeTestFile } from '../tools/test-generator.js';
26
- import { SessionBridge } from '../core/session-bridge.js';
27
- import { HistoryManager } from '../core/history-manager.js';
28
- import { extractAtReferences, splitAtReferences } from '../utils/at-references.js';
29
- import { loadImageAttachment, mimeFromExtension } from '../utils/image-attachments.js';
30
- const __filename = fileURLToPath(import.meta.url);
31
- const __dirname = path.dirname(__filename);
32
- /**
33
- * Available AI models configuration
34
- */
35
- export const AVAILABLE_MODELS = [
36
- // OpenAI Models
37
- // GPT-5 Series
38
- { id: 'gpt-5.2', name: 'GPT-5.2', provider: 'openai', tier: 'premium' },
39
- { id: 'gpt-5.2-pro', name: 'GPT-5.2 Pro', provider: 'openai', tier: 'premium' },
40
- { id: 'gpt-5.2-codex', name: 'GPT-5.2 Codex', provider: 'openai', tier: 'premium' },
41
- { id: 'gpt-5.1', name: 'GPT-5.1', provider: 'openai', tier: 'standard' },
42
- { id: 'gpt-5.1-codex', name: 'GPT-5.1 Codex', provider: 'openai', tier: 'standard' },
43
- { id: 'gpt-5.1-chat', name: 'GPT-5.1 Chat', provider: 'openai', tier: 'standard' },
44
- { id: 'gpt-5', name: 'GPT-5', provider: 'openai', tier: 'standard' },
45
- { id: 'gpt-5-mini', name: 'GPT-5 Mini', provider: 'openai', tier: 'fast' },
46
- { id: 'gpt-5-nano', name: 'GPT-5 Nano', provider: 'openai', tier: 'fast' },
47
- { id: 'gpt-5-chat', name: 'GPT-5 Chat', provider: 'openai', tier: 'fast' },
48
- // GPT-4 & Reasoning
49
- { id: 'gpt-4o', name: 'GPT-4o', provider: 'openai', tier: 'standard' },
50
- { id: 'o3-deep-research', name: 'O3 Deep Research', provider: 'openai', tier: 'reasoning' },
51
- { id: 'o3-pro', name: 'O3 Pro', provider: 'openai', tier: 'reasoning' },
52
- { id: 'o3', name: 'O3', provider: 'openai', tier: 'reasoning' },
53
- { id: 'o4-mini', name: 'O4 Mini', provider: 'openai', tier: 'reasoning' },
54
- { id: 'o4-mini-deep-research', name: 'O4 Mini Deep Research', provider: 'openai', tier: 'reasoning' },
55
- // Anthropic Models
56
- // Claude 4 Series
57
- { id: 'claude-opus-4.6', name: 'Claude Opus 4.6', provider: 'anthropic', tier: 'premium' },
58
- { id: 'claude-opus-4.5', name: 'Claude Opus 4.5', provider: 'anthropic', tier: 'premium' },
59
- { id: 'claude-haiku-4.5', name: 'Claude Haiku 4.5', provider: 'anthropic', tier: 'fast' },
60
- { id: 'claude-sonnet-4.5', name: 'Claude Sonnet 4.5', provider: 'anthropic', tier: 'standard' },
61
- { id: 'claude-sonnet-4', name: 'Claude Sonnet 4', provider: 'anthropic', tier: 'standard' },
62
- // Claude 3 Series
63
- { id: 'claude-3.7-sonnet', name: 'Claude 3.7 Sonnet', provider: 'anthropic', tier: 'standard' },
64
- { id: 'claude-3-5-sonnet-20241022', name: 'Claude 3.5 Sonnet', provider: 'anthropic', tier: 'standard' },
65
- // Google (Native)
66
- { id: 'gemini-3-deep-think', name: 'Gemini 3 Deep Think', provider: 'google', tier: 'reasoning' },
67
- { id: 'gemini-3-flash-preview', name: 'Gemini 3 Flash Preview', provider: 'google', tier: 'fast' },
68
- { id: 'gemini-3-pro-preview', name: 'Gemini 3 Pro Preview', provider: 'google', tier: 'premium' },
69
- { id: 'gemini-2.5-pro', name: 'Gemini 2.5 Pro', provider: 'google', tier: 'premium' },
70
- { id: 'gemini-2.5-flash', name: 'Gemini 2.5 Flash', provider: 'google', tier: 'fast' },
71
- // OpenRouter Models
72
- { id: 'anthropic/claude-3.5-sonnet', name: 'Claude 3.5 Sonnet (OpenRouter)', provider: 'openrouter', tier: 'standard' },
73
- // Zhipu AI (GLM)
74
- { id: 'glm-5', name: 'GLM-5', provider: 'zai', tier: 'premium' },
75
- { id: 'glm-4.7', name: 'GLM-4.7', provider: 'zai', tier: 'standard' },
76
- { id: 'glm-4.6', name: 'GLM-4.6', provider: 'zai', tier: 'standard' },
77
- { id: 'glm-4.5-air', name: 'GLM-4.5 Air', provider: 'zai', tier: 'fast' },
78
- { id: 'glm-4-plus', name: 'GLM-4 Plus', provider: 'zai', tier: 'standard' },
79
- // Alibaba (Qwen)
80
- // Qwen 3 Series
81
- { id: 'qwen3.5-coder-plus', name: 'Qwen 3.5 Coder Plus', provider: 'alibaba', tier: 'premium' },
82
- { id: 'qwen3.5-max', name: 'Qwen 3.5 Max', provider: 'alibaba', tier: 'premium' },
83
- { id: 'qwen3-max-thinking', name: 'Qwen 3 Max Thinking', provider: 'alibaba', tier: 'reasoning' },
84
- { id: 'qwen3-coder-plus', name: 'Qwen 3 Coder Plus', provider: 'alibaba', tier: 'standard' },
85
- { id: 'qwen3-235b', name: 'Qwen 3 235B', provider: 'alibaba', tier: 'standard' },
86
- // Qwen 2 Series
87
- { id: 'qwen2.5-coder', name: 'Qwen 2.5 Coder', provider: 'alibaba', tier: 'standard' },
88
- { id: 'qwen2.5-math', name: 'Qwen 2.5 Math', provider: 'alibaba', tier: 'reasoning' },
89
- { id: 'qwen2.5-72b', name: 'Qwen 2.5 72B', provider: 'alibaba', tier: 'standard' },
90
- // Moonshot (Kimi)
91
- // Kimi K2 Series
92
- { id: 'kimi-k2.5', name: 'Kimi k2.5', provider: 'kimi', tier: 'standard' },
93
- { id: 'kimi-k2-thinking', name: 'Kimi k2 Thinking', provider: 'kimi', tier: 'reasoning' },
94
- { id: 'kimi-k2-turbo-preview', name: 'Kimi k2 Turbo Preview', provider: 'kimi', tier: 'fast' },
95
- { id: 'kimi-k2-0905', name: 'Kimi k2 (0905)', provider: 'kimi', tier: 'standard' },
96
- { id: 'kimi-k2-0711', name: 'Kimi k2 (0711)', provider: 'kimi', tier: 'standard' },
97
- // xAI (Grok)
98
- // Grok-4 Series
99
- { id: 'grok-4.1-fast-reasoning', name: 'Grok 4.1 Fast Reasoning', provider: 'grok', tier: 'reasoning' },
100
- { id: 'grok-4.1-fast', name: 'Grok 4.1 Fast', provider: 'grok', tier: 'fast' },
101
- { id: 'grok-4', name: 'Grok 4', provider: 'grok', tier: 'premium' },
102
- { id: 'grok-4-code', name: 'Grok 4 Code', provider: 'grok', tier: 'standard' },
103
- { id: 'grok-4-0709', name: 'Grok 4 (0709)', provider: 'grok', tier: 'standard' },
104
- // Grok-3 Series
105
- { id: 'grok-3', name: 'Grok 3', provider: 'grok', tier: 'standard' },
106
- { id: 'grok-3-mini', name: 'Grok 3 Mini', provider: 'grok', tier: 'fast' },
107
- ];
108
- /**
109
- * WebUI Server for XibeCode
110
- */
111
- export class WebUIServer {
112
- server = null;
113
- wss = null;
114
- config;
115
- configManager;
116
- sessions = new Map();
117
- wsClients = new Map();
118
- workingDir;
119
- historyManager;
120
- constructor(config = {}) {
121
- this.config = {
122
- port: config.port || 3847,
123
- host: config.host || 'localhost',
124
- staticDir: config.staticDir || path.join(__dirname, '../../webui-dist'),
125
- workingDir: config.workingDir || process.cwd(),
126
- };
127
- this.workingDir = this.config.workingDir;
128
- this.configManager = new ConfigManager();
129
- this.historyManager = new HistoryManager(this.workingDir);
130
- }
131
- /**
132
- * Start the WebUI server
133
- */
134
- async start() {
135
- return new Promise((resolve, reject) => {
136
- this.server = createServer((req, res) => this.handleRequest(req, res));
137
- // WebSocket server for real-time communication
138
- this.wss = new WebSocketServer({ server: this.server });
139
- this.wss.on('connection', (ws, req) => this.handleWebSocket(ws, req));
140
- this.server.listen(this.config.port, this.config.host, () => {
141
- console.log(`XibeCode WebUI running at http://${this.config.host}:${this.config.port}`);
142
- resolve();
143
- });
144
- this.server.on('error', reject);
145
- });
146
- }
147
- /**
148
- * Stop the WebUI server
149
- */
150
- async stop() {
151
- return new Promise((resolve) => {
152
- // Close all WebSocket connections
153
- this.wsClients.forEach((ws) => ws.close());
154
- this.wsClients.clear();
155
- if (this.wss) {
156
- this.wss.close();
157
- }
158
- if (this.server) {
159
- this.server.close(() => resolve());
160
- }
161
- else {
162
- resolve();
163
- }
164
- });
165
- }
166
- /**
167
- * Handle HTTP requests
168
- */
169
- async handleRequest(req, res) {
170
- const url = new URL(req.url || '/', `http://${req.headers.host}`);
171
- const pathname = url.pathname;
172
- // CORS headers
173
- res.setHeader('Access-Control-Allow-Origin', '*');
174
- res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
175
- res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
176
- if (req.method === 'OPTIONS') {
177
- res.writeHead(204);
178
- res.end();
179
- return;
180
- }
181
- // API routes
182
- if (pathname.startsWith('/api/')) {
183
- await this.handleAPI(req, res, pathname);
184
- return;
185
- }
186
- // Static file serving
187
- await this.serveStatic(req, res, pathname);
188
- }
189
- /**
190
- * Handle API requests
191
- */
192
- async handleAPI(req, res, pathname) {
193
- const sendJSON = (data, status = 200) => {
194
- res.writeHead(status, { 'Content-Type': 'application/json' });
195
- res.end(JSON.stringify(data));
196
- };
197
- const parseBody = async () => {
198
- return new Promise((resolve, reject) => {
199
- let body = '';
200
- req.on('data', (chunk) => (body += chunk));
201
- req.on('end', () => {
202
- try {
203
- resolve(body ? JSON.parse(body) : {});
204
- }
205
- catch {
206
- resolve({});
207
- }
208
- });
209
- req.on('error', reject);
210
- });
211
- };
212
- try {
213
- // Health check
214
- if (pathname === '/api/health') {
215
- sendJSON({ status: 'ok', version: '1.0.2' });
216
- return;
217
- }
218
- // Configuration
219
- if (pathname === '/api/config') {
220
- if (req.method === 'GET') {
221
- const display = this.configManager.getDisplayConfig();
222
- const currentModel = this.configManager.getModel();
223
- const apiKeySet = !!this.configManager.getApiKey();
224
- const allConfig = this.configManager.getAll();
225
- sendJSON({
226
- ...display,
227
- apiKeySet,
228
- currentModel,
229
- availableModels: AVAILABLE_MODELS,
230
- // Raw config values for the settings panel
231
- raw: {
232
- provider: allConfig.provider || '',
233
- model: allConfig.model || 'claude-sonnet-4-5-20250929',
234
- apiKey: apiKeySet ? '••••••••' : '',
235
- baseUrl: allConfig.baseUrl || '',
236
- maxIterations: allConfig.maxIterations ?? 50,
237
- theme: allConfig.theme || 'default',
238
- showDetails: this.configManager.getShowDetails(),
239
- showThinking: this.configManager.getShowThinking(),
240
- compactThreshold: allConfig.compactThreshold ?? 50000,
241
- preferredPackageManager: allConfig.preferredPackageManager || 'pnpm',
242
- enableDryRunByDefault: allConfig.enableDryRunByDefault ?? false,
243
- gitCheckpointStrategy: allConfig.gitCheckpointStrategy || 'stash',
244
- testCommandOverride: allConfig.testCommandOverride || '',
245
- defaultEditor: allConfig.defaultEditor || '',
246
- statusBarEnabled: allConfig.statusBarEnabled ?? true,
247
- headerMinimal: allConfig.headerMinimal ?? false,
248
- sessionDirectory: allConfig.sessionDirectory || '',
249
- plugins: allConfig.plugins || [],
250
- },
251
- providerConfigs: PROVIDER_CONFIGS,
252
- });
253
- return;
254
- }
255
- if (req.method === 'PUT') {
256
- const body = await parseBody();
257
- // Core AI settings
258
- if (body.apiKey && !body.apiKey.includes('••••') && !body.apiKey.includes('****')) {
259
- this.configManager.set('apiKey', body.apiKey);
260
- }
261
- if (body.model)
262
- this.configManager.set('model', body.model);
263
- if (body.provider !== undefined)
264
- this.configManager.set('provider', body.provider);
265
- if (body.baseUrl !== undefined)
266
- this.configManager.set('baseUrl', body.baseUrl);
267
- if (body.maxIterations !== undefined)
268
- this.configManager.set('maxIterations', body.maxIterations);
269
- // Display settings
270
- if (body.theme !== undefined)
271
- this.configManager.set('theme', body.theme);
272
- if (body.showDetails !== undefined)
273
- this.configManager.set('showDetails', body.showDetails);
274
- if (body.showThinking !== undefined)
275
- this.configManager.set('showThinking', body.showThinking);
276
- if (body.compactThreshold !== undefined)
277
- this.configManager.set('compactThreshold', body.compactThreshold);
278
- // Dev settings
279
- if (body.preferredPackageManager !== undefined)
280
- this.configManager.set('preferredPackageManager', body.preferredPackageManager);
281
- if (body.enableDryRunByDefault !== undefined)
282
- this.configManager.set('enableDryRunByDefault', body.enableDryRunByDefault);
283
- if (body.gitCheckpointStrategy !== undefined)
284
- this.configManager.set('gitCheckpointStrategy', body.gitCheckpointStrategy);
285
- if (body.testCommandOverride !== undefined)
286
- this.configManager.set('testCommandOverride', body.testCommandOverride);
287
- if (body.defaultEditor !== undefined)
288
- this.configManager.set('defaultEditor', body.defaultEditor);
289
- if (body.statusBarEnabled !== undefined)
290
- this.configManager.set('statusBarEnabled', body.statusBarEnabled);
291
- if (body.headerMinimal !== undefined)
292
- this.configManager.set('headerMinimal', body.headerMinimal);
293
- if (body.sessionDirectory !== undefined)
294
- this.configManager.set('sessionDirectory', body.sessionDirectory);
295
- sendJSON({ success: true });
296
- return;
297
- }
298
- }
299
- // Models
300
- if (pathname === '/api/models') {
301
- sendJSON({
302
- models: AVAILABLE_MODELS,
303
- current: this.configManager.getModel(),
304
- });
305
- return;
306
- }
307
- // MCP servers JSON file (read/write for the Monaco editor)
308
- if (pathname === '/api/mcp/file') {
309
- const mcpFilePath = path.join(process.env.HOME || process.env.USERPROFILE || '.', '.xibecode', 'mcp-servers.json');
310
- if (req.method === 'GET') {
311
- try {
312
- const content = await fs.readFile(mcpFilePath, 'utf-8');
313
- sendJSON({ success: true, content, path: mcpFilePath });
314
- }
315
- catch {
316
- // File doesn't exist yet, return default template
317
- const defaultContent = JSON.stringify({
318
- mcpServers: {}
319
- }, null, 2);
320
- sendJSON({ success: true, content: defaultContent, path: mcpFilePath });
321
- }
322
- return;
323
- }
324
- if (req.method === 'PUT') {
325
- const body = await parseBody();
326
- if (!body.content) {
327
- sendJSON({ success: false, error: 'Missing content' }, 400);
328
- return;
329
- }
330
- try {
331
- // Validate it's valid JSON
332
- JSON.parse(body.content);
333
- // Ensure directory exists
334
- const dir = path.dirname(mcpFilePath);
335
- await fs.mkdir(dir, { recursive: true });
336
- await fs.writeFile(mcpFilePath, body.content, 'utf-8');
337
- sendJSON({ success: true });
338
- }
339
- catch (error) {
340
- sendJSON({ success: false, error: error.message || 'Invalid JSON' }, 400);
341
- }
342
- return;
343
- }
344
- }
345
- // Project info
346
- if (pathname === '/api/project') {
347
- const projectInfo = await this.getProjectInfo();
348
- sendJSON(projectInfo);
349
- return;
350
- }
351
- // Git status
352
- if (pathname === '/api/git/status') {
353
- const gitUtils = new GitUtils(this.workingDir);
354
- try {
355
- const status = await gitUtils.getStatus();
356
- sendJSON({ success: true, ...status });
357
- }
358
- catch (error) {
359
- sendJSON({ success: false, error: error.message }, 500);
360
- }
361
- return;
362
- }
363
- // Git diff
364
- if (pathname === '/api/git/diff') {
365
- const gitUtils = new GitUtils(this.workingDir);
366
- try {
367
- const diff = await gitUtils.getUnifiedDiff();
368
- const summary = await gitUtils.getDiffSummary();
369
- sendJSON({ success: true, diff, summary });
370
- }
371
- catch (error) {
372
- sendJSON({ success: false, error: error.message }, 500);
373
- }
374
- return;
375
- }
376
- // Git log (commit history)
377
- if (pathname === '/api/git/log') {
378
- try {
379
- const { execSync } = await import('child_process');
380
- const count = 30;
381
- const logOutput = execSync(`git log --pretty=format:'{"hash":"%H","shortHash":"%h","author":"%an","email":"%ae","date":"%ai","message":"%s","refs":"%D"}' -${count}`, { cwd: this.workingDir, encoding: 'utf-8', timeout: 5000 });
382
- const commits = logOutput.trim().split('\n').filter(Boolean).map(line => {
383
- try {
384
- // Handle special chars in commit messages
385
- const sanitized = line.replace(/\\/g, '\\\\').replace(/(?<!\\)"/g, (match, offset) => {
386
- // Only escape quotes inside the message field
387
- return match;
388
- });
389
- return JSON.parse(sanitized);
390
- }
391
- catch {
392
- // Fallback: parse manually
393
- const hashMatch = line.match(/"hash":"([^"]+)"/);
394
- const shortMatch = line.match(/"shortHash":"([^"]+)"/);
395
- const authorMatch = line.match(/"author":"([^"]+)"/);
396
- const dateMatch = line.match(/"date":"([^"]+)"/);
397
- const messageMatch = line.match(/"message":"(.+?)","refs"/);
398
- const refsMatch = line.match(/"refs":"([^"]*)"/);
399
- return {
400
- hash: hashMatch?.[1] || '',
401
- shortHash: shortMatch?.[1] || '',
402
- author: authorMatch?.[1] || '',
403
- email: '',
404
- date: dateMatch?.[1] || '',
405
- message: messageMatch?.[1] || 'commit',
406
- refs: refsMatch?.[1] || '',
407
- };
408
- }
409
- });
410
- // Also get graph lines
411
- let graph = [];
412
- try {
413
- const graphOutput = execSync(`git log --graph --oneline --decorate -${count}`, { cwd: this.workingDir, encoding: 'utf-8', timeout: 5000 });
414
- graph = graphOutput.trim().split('\n');
415
- }
416
- catch { }
417
- sendJSON({ success: true, commits, graph });
418
- }
419
- catch (error) {
420
- sendJSON({ success: false, error: error.message, commits: [], graph: [] }, 500);
421
- }
422
- return;
423
- }
424
- // File operations
425
- if (pathname === '/api/files/list') {
426
- const body = await parseBody();
427
- const dirPath = body.path || '.';
428
- const fullPath = path.resolve(this.workingDir, dirPath);
429
- try {
430
- const entries = await fs.readdir(fullPath, { withFileTypes: true });
431
- const files = entries.map((entry) => ({
432
- name: entry.name,
433
- isDirectory: entry.isDirectory(),
434
- path: path.join(dirPath, entry.name),
435
- }));
436
- sendJSON({ success: true, files });
437
- }
438
- catch (error) {
439
- sendJSON({ success: false, error: error.message }, 500);
440
- }
441
- return;
442
- }
443
- // Recursive file tree for the WebUI file explorer
444
- if (pathname === '/api/files/tree') {
445
- const body = await parseBody();
446
- const dirPath = body.path || '.';
447
- const maxDepth = body.depth || 10;
448
- const SKIP_DIRS = new Set([
449
- 'node_modules', '.git', 'dist', 'build', '.next', '.cache',
450
- '__pycache__', '.venv', 'venv', '.tox', 'coverage', '.nyc_output',
451
- '.svn', '.hg', 'bower_components', '.parcel-cache', '.turbo',
452
- ]);
453
- const buildTree = async (currentPath, relativePath, depth) => {
454
- if (depth <= 0)
455
- return [];
456
- const fullPath = path.resolve(this.workingDir, currentPath);
457
- // Path traversal protection
458
- if (!fullPath.startsWith(path.resolve(this.workingDir)))
459
- return [];
460
- try {
461
- const entries = await fs.readdir(fullPath, { withFileTypes: true });
462
- const nodes = [];
463
- // Sort: directories first, then alphabetically
464
- const sorted = entries.sort((a, b) => {
465
- if (a.isDirectory() && !b.isDirectory())
466
- return -1;
467
- if (!a.isDirectory() && b.isDirectory())
468
- return 1;
469
- return a.name.localeCompare(b.name);
470
- });
471
- for (const entry of sorted) {
472
- if (entry.name.startsWith('.') && entry.name !== '.env' && entry.name !== '.env.local') {
473
- // Skip most hidden files/dirs but allow .env
474
- if (entry.isDirectory())
475
- continue;
476
- }
477
- if (entry.isDirectory() && SKIP_DIRS.has(entry.name))
478
- continue;
479
- const entryRelPath = relativePath ? `${relativePath}/${entry.name}` : entry.name;
480
- const node = {
481
- name: entry.name,
482
- path: entryRelPath,
483
- isDirectory: entry.isDirectory(),
484
- };
485
- if (entry.isDirectory()) {
486
- node.children = await buildTree(path.join(currentPath, entry.name), entryRelPath, depth - 1);
487
- }
488
- nodes.push(node);
489
- }
490
- return nodes;
491
- }
492
- catch {
493
- return [];
494
- }
495
- };
496
- try {
497
- const tree = await buildTree(dirPath, '', maxDepth);
498
- sendJSON({ success: true, tree });
499
- }
500
- catch (error) {
501
- sendJSON({ success: false, error: error.message }, 500);
502
- }
503
- return;
504
- }
505
- if (pathname === '/api/files/read') {
506
- const body = await parseBody();
507
- if (!body.path) {
508
- sendJSON({ success: false, error: 'Missing path parameter' }, 400);
509
- return;
510
- }
511
- const fullPath = path.resolve(this.workingDir, body.path);
512
- try {
513
- const content = await fs.readFile(fullPath, 'utf-8');
514
- sendJSON({ success: true, content, path: body.path });
515
- }
516
- catch (error) {
517
- sendJSON({ success: false, error: error.message }, 500);
518
- }
519
- return;
520
- }
521
- // Serve raw binary files (images, videos, audio) for media preview
522
- if (pathname === '/api/files/raw') {
523
- const url = new URL(req.url || '/', `http://${req.headers.host}`);
524
- const filePath = url.searchParams.get('path');
525
- if (!filePath) {
526
- sendJSON({ success: false, error: 'Missing path parameter' }, 400);
527
- return;
528
- }
529
- const fullPath = path.resolve(this.workingDir, filePath);
530
- // Path traversal protection
531
- if (!fullPath.startsWith(path.resolve(this.workingDir))) {
532
- sendJSON({ success: false, error: 'Invalid path' }, 403);
533
- return;
534
- }
535
- try {
536
- const content = await fs.readFile(fullPath);
537
- const ext = path.extname(fullPath).toLowerCase();
538
- const mimeTypes = {
539
- '.png': 'image/png', '.jpg': 'image/jpeg', '.jpeg': 'image/jpeg', '.gif': 'image/gif',
540
- '.webp': 'image/webp', '.svg': 'image/svg+xml', '.ico': 'image/x-icon', '.bmp': 'image/bmp',
541
- '.avif': 'image/avif', '.tiff': 'image/tiff', '.tif': 'image/tiff',
542
- '.mp4': 'video/mp4', '.webm': 'video/webm', '.ogg': 'video/ogg', '.mov': 'video/quicktime',
543
- '.avi': 'video/x-msvideo', '.mkv': 'video/x-matroska',
544
- '.mp3': 'audio/mpeg', '.wav': 'audio/wav', '.flac': 'audio/flac', '.aac': 'audio/aac',
545
- '.m4a': 'audio/mp4', '.opus': 'audio/opus',
546
- '.pdf': 'application/pdf',
547
- '.woff': 'font/woff', '.woff2': 'font/woff2', '.ttf': 'font/ttf', '.otf': 'font/otf',
548
- };
549
- const contentType = mimeTypes[ext] || 'application/octet-stream';
550
- res.writeHead(200, {
551
- 'Content-Type': contentType,
552
- 'Content-Length': content.length.toString(),
553
- 'Cache-Control': 'public, max-age=3600',
554
- });
555
- res.end(content);
556
- }
557
- catch (error) {
558
- sendJSON({ success: false, error: error.message }, 404);
559
- }
560
- return;
561
- }
562
- // Session management
563
- if (pathname === '/api/session/create') {
564
- const sessionId = this.createSession();
565
- sendJSON({ success: true, sessionId });
566
- return;
567
- }
568
- if (pathname.startsWith('/api/session/') && pathname.endsWith('/message')) {
569
- const sessionId = pathname.split('/')[3];
570
- const body = await parseBody();
571
- if (!body.message) {
572
- sendJSON({ success: false, error: 'Missing message' }, 400);
573
- return;
574
- }
575
- const session = this.sessions.get(sessionId);
576
- if (!session) {
577
- sendJSON({ success: false, error: 'Session not found' }, 404);
578
- return;
579
- }
580
- // Start agent run - responses will be sent via WebSocket
581
- this.runAgentMessage(sessionId, body.message);
582
- sendJSON({ success: true, status: 'processing' });
583
- return;
584
- }
585
- // Test generation - use TestGenerator directly to avoid permission checks
586
- if (pathname === '/api/tests/generate') {
587
- const body = await parseBody();
588
- if (!body.filePath) {
589
- sendJSON({ success: false, error: 'Missing filePath parameter' }, 400);
590
- return;
591
- }
592
- try {
593
- const generator = new TestGenerator(this.workingDir);
594
- // First analyze the file
595
- const analysis = await generator.analyzeFile(body.filePath);
596
- // Then generate tests from the analysis
597
- const result = await generator.generateTests(analysis, {
598
- framework: body.framework,
599
- outputDir: body.outputDir,
600
- includeEdgeCases: body.includeEdgeCases !== false,
601
- includeMocks: body.includeMocks !== false,
602
- maxTestsPerFunction: body.maxTestsPerFunction || 5,
603
- });
604
- // Write file if requested
605
- if (body.writeFile && result.content) {
606
- await writeTestFile(result);
607
- sendJSON({ ...result, outputPath: result.testFilePath, success: true });
608
- }
609
- else {
610
- sendJSON({ ...result, success: true });
611
- }
612
- }
613
- catch (error) {
614
- sendJSON({ success: false, error: error.message }, 500);
615
- }
616
- return;
617
- }
618
- if (pathname === '/api/tests/analyze') {
619
- const body = await parseBody();
620
- if (!body.filePath) {
621
- sendJSON({ success: false, error: 'Missing filePath parameter' }, 400);
622
- return;
623
- }
624
- try {
625
- const generator = new TestGenerator(this.workingDir);
626
- const analysis = await generator.analyzeFile(body.filePath);
627
- sendJSON({ success: true, analysis });
628
- }
629
- catch (error) {
630
- sendJSON({ success: false, error: error.message }, 500);
631
- }
632
- return;
633
- }
634
- // Run tests
635
- if (pathname === '/api/tests/run') {
636
- const body = await parseBody();
637
- const toolExecutor = new CodingToolExecutor(this.workingDir);
638
- const result = await toolExecutor.execute('run_tests', {
639
- command: body.command,
640
- cwd: body.cwd,
641
- });
642
- sendJSON(result);
643
- return;
644
- }
645
- // Environment variables (.env file management)
646
- if (pathname === '/api/env') {
647
- if (req.method === 'GET') {
648
- // Auto-detect .env file in the working directory
649
- const envFiles = ['.env', '.env.local', '.env.development', '.env.production'];
650
- let envFilePath = null;
651
- let envContent = '';
652
- for (const envFile of envFiles) {
653
- const fullEnvPath = path.join(this.workingDir, envFile);
654
- try {
655
- envContent = await fs.readFile(fullEnvPath, 'utf-8');
656
- envFilePath = envFile;
657
- break;
658
- }
659
- catch {
660
- // File doesn't exist, try next
661
- }
662
- }
663
- if (!envFilePath) {
664
- // No .env file found, return empty state with suggested path
665
- sendJSON({
666
- success: true,
667
- exists: false,
668
- path: '.env',
669
- fullPath: path.join(this.workingDir, '.env'),
670
- variables: [],
671
- raw: '',
672
- });
673
- return;
674
- }
675
- // Parse the .env file content into structured variables
676
- const variables = [];
677
- const lines = envContent.split('\n');
678
- for (const line of lines) {
679
- const trimmed = line.trim();
680
- if (trimmed === '') {
681
- variables.push({ key: '', value: '', isComment: false, raw: line });
682
- }
683
- else if (trimmed.startsWith('#')) {
684
- variables.push({ key: '', value: '', comment: trimmed.slice(1).trim(), isComment: true, raw: line });
685
- }
686
- else {
687
- const eqIndex = trimmed.indexOf('=');
688
- if (eqIndex !== -1) {
689
- const key = trimmed.substring(0, eqIndex).trim();
690
- let value = trimmed.substring(eqIndex + 1).trim();
691
- // Remove surrounding quotes if present
692
- if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) {
693
- value = value.slice(1, -1);
694
- }
695
- variables.push({ key, value, isComment: false, raw: line });
696
- }
697
- else {
698
- variables.push({ key: trimmed, value: '', isComment: false, raw: line });
699
- }
700
- }
701
- }
702
- sendJSON({
703
- success: true,
704
- exists: true,
705
- path: envFilePath,
706
- fullPath: path.join(this.workingDir, envFilePath),
707
- variables,
708
- raw: envContent,
709
- });
710
- return;
711
- }
712
- if (req.method === 'PUT') {
713
- const body = await parseBody();
714
- const envFileName = body.path || '.env';
715
- const fullEnvPath = path.join(this.workingDir, envFileName);
716
- // Path traversal protection
717
- if (!fullEnvPath.startsWith(path.resolve(this.workingDir))) {
718
- sendJSON({ success: false, error: 'Invalid path' }, 400);
719
- return;
720
- }
721
- try {
722
- if (body.raw !== undefined) {
723
- // Write raw content directly
724
- await fs.writeFile(fullEnvPath, body.raw, 'utf-8');
725
- }
726
- else if (body.variables) {
727
- // Build .env content from structured variables
728
- const lines = [];
729
- for (const v of body.variables) {
730
- if (v.isComment) {
731
- lines.push(`# ${v.comment || ''}`);
732
- }
733
- else if (v.key === '' && v.value === '') {
734
- lines.push('');
735
- }
736
- else {
737
- const needsQuotes = v.value && (v.value.includes(' ') || v.value.includes('#') || v.value.includes('"'));
738
- const quotedValue = needsQuotes ? `"${v.value}"` : (v.value || '');
739
- lines.push(`${v.key}=${quotedValue}`);
740
- }
741
- }
742
- await fs.writeFile(fullEnvPath, lines.join('\n') + '\n', 'utf-8');
743
- }
744
- else {
745
- sendJSON({ success: false, error: 'Missing raw or variables field' }, 400);
746
- return;
747
- }
748
- sendJSON({ success: true, path: envFileName, fullPath: fullEnvPath });
749
- }
750
- catch (error) {
751
- sendJSON({ success: false, error: error.message }, 500);
752
- }
753
- return;
754
- }
755
- }
756
- // File list for @ command (simple GET)
757
- if (pathname === '/api/files') {
758
- try {
759
- const files = [];
760
- const walkDir = async (dir, prefix = '') => {
761
- const entries = await fs.readdir(dir, { withFileTypes: true });
762
- for (const entry of entries) {
763
- // Skip hidden files, node_modules, dist, etc.
764
- if (entry.name.startsWith('.') ||
765
- entry.name === 'node_modules' ||
766
- entry.name === 'dist' ||
767
- entry.name === 'build' ||
768
- entry.name === 'coverage' ||
769
- entry.name === '__pycache__') {
770
- continue;
771
- }
772
- const relativePath = prefix ? `${prefix}/${entry.name}` : entry.name;
773
- if (entry.isDirectory()) {
774
- files.push(relativePath + '/');
775
- // Limit depth to avoid huge lists
776
- if (relativePath.split('/').length < 4) {
777
- await walkDir(path.join(dir, entry.name), relativePath);
778
- }
779
- }
780
- else {
781
- files.push(relativePath);
782
- }
783
- }
784
- };
785
- await walkDir(this.workingDir);
786
- // Sort: directories first, then files
787
- files.sort((a, b) => {
788
- const aIsDir = a.endsWith('/');
789
- const bIsDir = b.endsWith('/');
790
- if (aIsDir && !bIsDir)
791
- return -1;
792
- if (!aIsDir && bIsDir)
793
- return 1;
794
- return a.localeCompare(b);
795
- });
796
- sendJSON({ success: true, files: files.slice(0, 500) }); // Limit to 500 files
797
- }
798
- catch (error) {
799
- sendJSON({ success: false, error: error.message }, 500);
800
- }
801
- return;
802
- }
803
- // Chat history API
804
- if (pathname === '/api/history') {
805
- if (req.method === 'GET') {
806
- try {
807
- const conversations = await this.historyManager.list();
808
- sendJSON({ success: true, conversations });
809
- }
810
- catch (error) {
811
- sendJSON({ success: false, error: error.message }, 500);
812
- }
813
- return;
814
- }
815
- if (req.method === 'POST') {
816
- const body = await parseBody();
817
- try {
818
- if (body.conversation) {
819
- await this.historyManager.save(body.conversation);
820
- sendJSON({ success: true, id: body.conversation.id });
821
- }
822
- else {
823
- sendJSON({ success: false, error: 'Missing conversation data' }, 400);
824
- }
825
- }
826
- catch (error) {
827
- sendJSON({ success: false, error: error.message }, 500);
828
- }
829
- return;
830
- }
831
- }
832
- // Load specific conversation
833
- if (pathname.startsWith('/api/history/') && req.method === 'GET') {
834
- const id = pathname.split('/')[3];
835
- if (id) {
836
- try {
837
- const conversation = await this.historyManager.load(id);
838
- if (conversation) {
839
- sendJSON({ success: true, conversation });
840
- }
841
- else {
842
- sendJSON({ success: false, error: 'Conversation not found' }, 404);
843
- }
844
- }
845
- catch (error) {
846
- sendJSON({ success: false, error: error.message }, 500);
847
- }
848
- return;
849
- }
850
- }
851
- // Delete specific conversation
852
- if (pathname.startsWith('/api/history/') && req.method === 'DELETE') {
853
- const id = pathname.split('/')[3];
854
- if (id) {
855
- try {
856
- const deleted = await this.historyManager.delete(id);
857
- sendJSON({ success: deleted, error: deleted ? undefined : 'Not found' });
858
- }
859
- catch (error) {
860
- sendJSON({ success: false, error: error.message }, 500);
861
- }
862
- return;
863
- }
864
- }
865
- // 404 for unknown API routes
866
- sendJSON({ error: 'Not found' }, 404);
867
- }
868
- catch (error) {
869
- sendJSON({ error: error.message }, 500);
870
- }
871
- }
872
- /**
873
- * Serve static files
874
- */
875
- async serveStatic(req, res, pathname) {
876
- const staticDir = this.config.staticDir;
877
- // Default to index.html for SPA
878
- let filePath = pathname === '/' ? '/index.html' : pathname;
879
- let fullPath = path.join(staticDir, filePath);
880
- // Check if file exists, otherwise serve index.html (SPA fallback)
881
- if (!fsSync.existsSync(fullPath)) {
882
- fullPath = path.join(staticDir, 'index.html');
883
- }
884
- try {
885
- const content = await fs.readFile(fullPath);
886
- const ext = path.extname(fullPath).toLowerCase();
887
- const mimeTypes = {
888
- '.html': 'text/html',
889
- '.js': 'application/javascript',
890
- '.css': 'text/css',
891
- '.json': 'application/json',
892
- '.png': 'image/png',
893
- '.jpg': 'image/jpeg',
894
- '.svg': 'image/svg+xml',
895
- '.ico': 'image/x-icon',
896
- '.woff': 'font/woff',
897
- '.woff2': 'font/woff2',
898
- };
899
- res.writeHead(200, { 'Content-Type': mimeTypes[ext] || 'application/octet-stream' });
900
- res.end(content);
901
- }
902
- catch {
903
- // Serve inline fallback HTML if no frontend build exists
904
- res.writeHead(200, { 'Content-Type': 'text/html' });
905
- res.end(this.getFallbackHTML());
906
- }
907
- }
908
- /**
909
- * Handle WebSocket connections
910
- */
911
- handleWebSocket(ws, req) {
912
- const url = new URL(req.url || '/', `http://${req.headers.host}`);
913
- const sessionId = url.searchParams.get('session');
914
- const mode = url.searchParams.get('mode'); // 'bridge' for TUI sync, null for standalone
915
- // Register with SessionBridge for TUI-WebUI sync
916
- if (mode === 'bridge') {
917
- SessionBridge.registerWebSocket(ws);
918
- ws.on('message', async (data) => {
919
- try {
920
- const message = JSON.parse(data.toString());
921
- if (message.type === 'ping') {
922
- ws.send(JSON.stringify({ type: 'pong' }));
923
- return;
924
- }
925
- // Forward user messages to TUI via SessionBridge
926
- if (message.type === 'message' && message.content) {
927
- SessionBridge.onWebUIUserMessage(message.content);
928
- }
929
- }
930
- catch (error) {
931
- console.error('WebSocket message error:', error);
932
- }
933
- });
934
- ws.on('close', () => {
935
- SessionBridge.unregisterWebSocket(ws);
936
- });
937
- return;
938
- }
939
- // Terminal mode - spawn a real shell with PTY via Python bridge
940
- if (mode === 'terminal') {
941
- let ptyProcess = null;
942
- ws.on('message', (data) => {
943
- try {
944
- const message = JSON.parse(data.toString());
945
- if (message.type === 'terminal:create') {
946
- const cwd = message.cwd ? path.resolve(this.workingDir, message.cwd) : this.workingDir;
947
- const shell = process.env.SHELL || '/bin/bash';
948
- const cols = message.cols || 120;
949
- const rows = message.rows || 30;
950
- // Python PTY bridge script - creates a real pseudo-terminal
951
- const ptyBridge = `
952
- import pty, os, sys, select, signal, struct, fcntl, termios
953
-
954
- def set_winsize(fd, rows, cols):
955
- s = struct.pack('HHHH', rows, cols, 0, 0)
956
- fcntl.ioctl(fd, termios.TIOCSWINSZ, s)
957
-
958
- master, slave = pty.openpty()
959
- set_winsize(master, ${rows}, ${cols})
960
-
961
- pid = os.fork()
962
- if pid == 0:
963
- os.close(master)
964
- os.setsid()
965
- os.dup2(slave, 0)
966
- os.dup2(slave, 1)
967
- os.dup2(slave, 2)
968
- os.close(slave)
969
- os.environ['TERM'] = 'xterm-256color'
970
- os.environ['COLORTERM'] = 'truecolor'
971
- os.chdir('${cwd.replace(/'/g, "\\'")}')
972
- os.execvp('${shell}', ['${shell}', '-i'])
973
- else:
974
- os.close(slave)
975
- stdin_fd = sys.stdin.fileno()
976
- stdout_fd = sys.stdout.fileno()
977
- try:
978
- while True:
979
- r, _, _ = select.select([stdin_fd, master], [], [], 0.02)
980
- if stdin_fd in r:
981
- d = os.read(stdin_fd, 4096)
982
- if not d: break
983
- os.write(master, d)
984
- if master in r:
985
- try:
986
- d = os.read(master, 4096)
987
- except OSError: break
988
- if not d: break
989
- os.write(stdout_fd, d)
990
- except (IOError, OSError): pass
991
- finally:
992
- try: os.kill(pid, signal.SIGTERM)
993
- except: pass
994
- `;
995
- ptyProcess = spawn('python3', ['-u', '-c', ptyBridge], {
996
- cwd,
997
- env: { ...process.env, PYTHONUNBUFFERED: '1' },
998
- stdio: ['pipe', 'pipe', 'pipe'],
999
- });
1000
- if (ptyProcess.stdout) {
1001
- ptyProcess.stdout.on('data', (chunk) => {
1002
- if (ws.readyState === WebSocket.OPEN) {
1003
- ws.send(JSON.stringify({ type: 'terminal:output', data: chunk.toString('utf-8') }));
1004
- }
1005
- });
1006
- }
1007
- if (ptyProcess.stderr) {
1008
- ptyProcess.stderr.on('data', (chunk) => {
1009
- // stderr from the PTY bridge (mostly shell startup messages)
1010
- if (ws.readyState === WebSocket.OPEN) {
1011
- ws.send(JSON.stringify({ type: 'terminal:output', data: chunk.toString('utf-8') }));
1012
- }
1013
- });
1014
- }
1015
- ptyProcess.on('exit', (code) => {
1016
- if (ws.readyState === WebSocket.OPEN) {
1017
- ws.send(JSON.stringify({ type: 'terminal:output', data: `\r\n\x1b[90mShell exited (code ${code})\x1b[0m\r\n` }));
1018
- }
1019
- });
1020
- ptyProcess.on('error', (err) => {
1021
- if (ws.readyState === WebSocket.OPEN) {
1022
- ws.send(JSON.stringify({ type: 'terminal:output', data: `\r\n\x1b[31mError: ${err.message}\x1b[0m\r\n` }));
1023
- }
1024
- });
1025
- ws.send(JSON.stringify({ type: 'terminal:created', pid: ptyProcess.pid }));
1026
- }
1027
- if (message.type === 'terminal:input' && ptyProcess?.stdin) {
1028
- ptyProcess.stdin.write(message.data);
1029
- }
1030
- if (message.type === 'terminal:resize' && ptyProcess?.pid) {
1031
- // For resize, we'd need to signal the Python bridge
1032
- // The Python bridge will handle SIGWINCH
1033
- try {
1034
- process.kill(ptyProcess.pid, 'SIGWINCH');
1035
- }
1036
- catch {
1037
- // ignore
1038
- }
1039
- }
1040
- }
1041
- catch (error) {
1042
- console.error('Terminal WebSocket error:', error);
1043
- }
1044
- });
1045
- ws.on('close', () => {
1046
- if (ptyProcess) {
1047
- try {
1048
- ptyProcess.kill('SIGTERM');
1049
- setTimeout(() => {
1050
- try {
1051
- ptyProcess?.kill('SIGKILL');
1052
- }
1053
- catch { }
1054
- }, 1000);
1055
- }
1056
- catch { }
1057
- ptyProcess = null;
1058
- }
1059
- });
1060
- return;
1061
- }
1062
- // Legacy standalone mode for backward compatibility
1063
- if (sessionId) {
1064
- this.wsClients.set(sessionId, ws);
1065
- }
1066
- ws.on('message', async (data) => {
1067
- try {
1068
- const message = JSON.parse(data.toString());
1069
- if (message.type === 'ping') {
1070
- ws.send(JSON.stringify({ type: 'pong' }));
1071
- return;
1072
- }
1073
- if (message.type === 'message' && message.sessionId && message.content) {
1074
- this.runAgentMessage(message.sessionId, message.content);
1075
- }
1076
- }
1077
- catch (error) {
1078
- console.error('WebSocket message error:', error);
1079
- }
1080
- });
1081
- ws.on('close', () => {
1082
- if (sessionId) {
1083
- this.wsClients.delete(sessionId);
1084
- }
1085
- });
1086
- }
1087
- /**
1088
- * Create a new agent session
1089
- */
1090
- createSession() {
1091
- const sessionId = `session_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
1092
- const toolExecutor = new CodingToolExecutor(this.workingDir);
1093
- this.sessions.set(sessionId, {
1094
- id: sessionId,
1095
- agent: null,
1096
- toolExecutor,
1097
- messages: [],
1098
- status: 'idle',
1099
- createdAt: new Date(),
1100
- lastActivity: new Date(),
1101
- });
1102
- return sessionId;
1103
- }
1104
- /**
1105
- * Run agent with a message
1106
- */
1107
- async runAgentMessage(sessionId, message) {
1108
- const session = this.sessions.get(sessionId);
1109
- if (!session)
1110
- return;
1111
- const ws = this.wsClients.get(sessionId);
1112
- const send = (data) => {
1113
- if (ws && ws.readyState === WebSocket.OPEN) {
1114
- ws.send(JSON.stringify(data));
1115
- }
1116
- };
1117
- try {
1118
- session.status = 'running';
1119
- session.lastActivity = new Date();
1120
- const apiKey = this.configManager.getApiKey();
1121
- if (!apiKey) {
1122
- send({ type: 'error', error: 'API key not configured' });
1123
- session.status = 'error';
1124
- return;
1125
- }
1126
- // Create agent if not exists
1127
- if (!session.agent) {
1128
- if (!session.builtInSkillsPrompt) {
1129
- const sm = new SkillManager(this.workingDir, apiKey, this.configManager.getBaseUrl(), this.configManager.getModel(), this.configManager.get('provider'));
1130
- await sm.loadSkills();
1131
- const autoSkillsShEnabled = process.env.XIBECODE_AUTO_SKILLS_SH === '1' || process.env.XIBECODE_AUTO_SKILLS_SH === 'true';
1132
- let autoInstalledSkillNames = [];
1133
- if (autoSkillsShEnabled) {
1134
- const auto = await sm.autoInstallFromSkillsShForTask(message, { enabled: true, maxInstalls: 1 });
1135
- autoInstalledSkillNames = auto.installedSkillNames || [];
1136
- }
1137
- session.builtInSkillsPrompt = await sm.buildDefaultSkillsPromptForTask(message, this.workingDir);
1138
- for (const name of autoInstalledSkillNames) {
1139
- const s = sm.getSkill(name);
1140
- if (!s?.instructions)
1141
- continue;
1142
- session.builtInSkillsPrompt += `\n\n---\n\n## Auto-installed skills.sh skill\n\n### ${s.name}\n*${s.description}*\n\n${s.instructions}`;
1143
- break;
1144
- }
1145
- }
1146
- session.agent = new EnhancedAgent({
1147
- apiKey,
1148
- baseUrl: this.configManager.getBaseUrl(),
1149
- model: this.configManager.getModel(),
1150
- maxIterations: this.configManager.get('maxIterations') || 50,
1151
- provider: this.configManager.get('provider'),
1152
- customProviderFormat: this.configManager.get('customProviderFormat'),
1153
- requestFormat: this.configManager.get('requestFormat') ?? 'auto',
1154
- defaultSkillsPrompt: session.builtInSkillsPrompt,
1155
- }, this.configManager.get('provider'));
1156
- }
1157
- const agent = session.agent;
1158
- // Subscribe to agent events
1159
- agent.on('event', (event) => {
1160
- switch (event.type) {
1161
- case 'thinking':
1162
- send({ type: 'thinking', message: event.data.message });
1163
- break;
1164
- case 'stream_start':
1165
- send({ type: 'stream_start', persona: event.data.persona });
1166
- break;
1167
- case 'stream_text':
1168
- send({ type: 'stream_text', text: event.data.text });
1169
- break;
1170
- case 'stream_end':
1171
- send({ type: 'stream_end' });
1172
- break;
1173
- case 'tool_call':
1174
- send({
1175
- type: 'tool_call',
1176
- name: event.data.name,
1177
- input: event.data.input,
1178
- });
1179
- break;
1180
- case 'tool_result':
1181
- send({
1182
- type: 'tool_result',
1183
- name: event.data.name,
1184
- success: event.data.success,
1185
- });
1186
- break;
1187
- case 'response':
1188
- send({ type: 'response', text: event.data.text });
1189
- break;
1190
- case 'complete':
1191
- send({ type: 'complete', stats: event.data });
1192
- session.status = 'idle';
1193
- break;
1194
- case 'error':
1195
- send({ type: 'error', error: event.data.message || event.data.error });
1196
- break;
1197
- }
1198
- });
1199
- // Run agent
1200
- const tools = session.toolExecutor.getTools();
1201
- const refs = extractAtReferences(message, this.workingDir);
1202
- const { image: imageRefs } = splitAtReferences(refs);
1203
- const images = [];
1204
- for (const ref of imageRefs) {
1205
- try {
1206
- const mime = mimeFromExtension(ref.extension);
1207
- if (!mime)
1208
- continue;
1209
- const attachment = await loadImageAttachment(ref.resolvedPath, { mime });
1210
- images.push(attachment);
1211
- }
1212
- catch {
1213
- // Ignore image load failures so text chat still works.
1214
- }
1215
- }
1216
- await agent.run(message, tools, session.toolExecutor, images.length ? { images } : undefined);
1217
- session.messages.push({ role: 'user', content: message });
1218
- session.status = 'idle';
1219
- }
1220
- catch (error) {
1221
- send({ type: 'error', error: error.message });
1222
- session.status = 'error';
1223
- }
1224
- }
1225
- /**
1226
- * Get project information
1227
- */
1228
- async getProjectInfo() {
1229
- const gitUtils = new GitUtils(this.workingDir);
1230
- const testRunner = new TestRunnerDetector(this.workingDir);
1231
- let gitInfo = null;
1232
- try {
1233
- gitInfo = await gitUtils.getStatus();
1234
- }
1235
- catch {
1236
- // Not a git repo
1237
- }
1238
- let testInfo = null;
1239
- try {
1240
- testInfo = await testRunner.detectTestRunner();
1241
- }
1242
- catch {
1243
- // No test runner
1244
- }
1245
- // Read package.json if exists
1246
- let packageJson = null;
1247
- try {
1248
- const pkgPath = path.join(this.workingDir, 'package.json');
1249
- packageJson = JSON.parse(await fs.readFile(pkgPath, 'utf-8'));
1250
- }
1251
- catch {
1252
- // No package.json
1253
- }
1254
- return {
1255
- name: packageJson?.name || path.basename(this.workingDir),
1256
- version: packageJson?.version,
1257
- description: packageJson?.description,
1258
- workingDir: this.workingDir,
1259
- isGitRepo: !!gitInfo,
1260
- gitBranch: gitInfo?.branch,
1261
- gitStatus: gitInfo?.clean ? 'clean' : 'dirty',
1262
- testRunner: testInfo?.runner,
1263
- testCommand: testInfo?.command,
1264
- packageManager: testInfo?.packageManager,
1265
- dependencies: packageJson?.dependencies ? Object.keys(packageJson.dependencies).length : 0,
1266
- devDependencies: packageJson?.devDependencies ? Object.keys(packageJson.devDependencies).length : 0,
1267
- };
1268
- }
1269
- /**
1270
- * Fallback HTML when no frontend build exists
1271
- * Minimalistic terminal-style WebUI
1272
- */
1273
- getFallbackHTML() {
1274
- return `<!DOCTYPE html>
1275
- <html lang="en">
1276
- <head>
1277
- <meta charset="UTF-8">
1278
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
1279
- <title>XibeCode</title>
1280
- <style>
1281
- :root {
1282
- --bg-primary: #0d1117;
1283
- --bg-secondary: #161b22;
1284
- --bg-tertiary: #21262d;
1285
- --border-color: #30363d;
1286
- --text-primary: #e6edf3;
1287
- --text-secondary: #8b949e;
1288
- --text-muted: #6e7681;
1289
- --accent-blue: #58a6ff;
1290
- --accent-green: #3fb950;
1291
- --accent-yellow: #d29922;
1292
- --accent-red: #f85149;
1293
- --accent-purple: #a371f7;
1294
- --accent-cyan: #39c5cf;
1295
- }
1296
- * { box-sizing: border-box; margin: 0; padding: 0; }
1297
- body {
1298
- font-family: 'SF Mono', 'Fira Code', 'Monaco', 'Consolas', monospace;
1299
- background: var(--bg-primary);
1300
- color: var(--text-primary);
1301
- min-height: 100vh;
1302
- display: flex;
1303
- flex-direction: column;
1304
- font-size: 14px;
1305
- line-height: 1.5;
1306
- }
1307
-
1308
- /* Header */
1309
- .header {
1310
- background: var(--bg-secondary);
1311
- border-bottom: 1px solid var(--border-color);
1312
- padding: 12px 20px;
1313
- display: flex;
1314
- align-items: center;
1315
- justify-content: space-between;
1316
- flex-wrap: wrap;
1317
- gap: 12px;
1318
- }
1319
- .logo {
1320
- display: flex;
1321
- align-items: center;
1322
- gap: 10px;
1323
- }
1324
- .logo-icon {
1325
- width: 28px;
1326
- height: 28px;
1327
- background: linear-gradient(135deg, var(--accent-cyan), var(--accent-blue));
1328
- border-radius: 6px;
1329
- display: flex;
1330
- align-items: center;
1331
- justify-content: center;
1332
- font-weight: bold;
1333
- font-size: 16px;
1334
- }
1335
- .logo-text {
1336
- font-size: 18px;
1337
- font-weight: 600;
1338
- color: var(--text-primary);
1339
- }
1340
- .logo-text span { color: var(--accent-cyan); }
1341
- .header-info {
1342
- display: flex;
1343
- align-items: center;
1344
- gap: 16px;
1345
- flex-wrap: wrap;
1346
- }
1347
- .info-item {
1348
- display: flex;
1349
- align-items: center;
1350
- gap: 6px;
1351
- font-size: 12px;
1352
- color: var(--text-secondary);
1353
- }
1354
- .info-item .label { color: var(--text-muted); }
1355
- .info-item .value { color: var(--accent-cyan); }
1356
- .status-dot {
1357
- width: 8px;
1358
- height: 8px;
1359
- border-radius: 50%;
1360
- background: var(--accent-red);
1361
- }
1362
- .status-dot.connected { background: var(--accent-green); }
1363
-
1364
- /* Main layout */
1365
- .main {
1366
- flex: 1;
1367
- display: flex;
1368
- flex-direction: column;
1369
- max-width: 1200px;
1370
- width: 100%;
1371
- margin: 0 auto;
1372
- padding: 16px;
1373
- }
1374
-
1375
- /* Messages area */
1376
- .messages {
1377
- flex: 1;
1378
- overflow-y: auto;
1379
- padding: 16px 0;
1380
- display: flex;
1381
- flex-direction: column;
1382
- gap: 16px;
1383
- min-height: 400px;
1384
- }
1385
-
1386
- /* Message styles */
1387
- .message {
1388
- padding: 12px 16px;
1389
- border-radius: 8px;
1390
- max-width: 85%;
1391
- word-wrap: break-word;
1392
- }
1393
- .message.user {
1394
- background: var(--bg-tertiary);
1395
- border: 1px solid var(--border-color);
1396
- align-self: flex-end;
1397
- margin-left: auto;
1398
- }
1399
- .message.user::before {
1400
- content: '> ';
1401
- color: var(--accent-green);
1402
- }
1403
- .message.assistant {
1404
- background: var(--bg-secondary);
1405
- border-left: 3px solid var(--accent-cyan);
1406
- align-self: flex-start;
1407
- }
1408
- .message.system {
1409
- background: transparent;
1410
- color: var(--text-muted);
1411
- font-size: 12px;
1412
- text-align: center;
1413
- align-self: center;
1414
- max-width: 100%;
1415
- }
1416
- .message.tool {
1417
- background: var(--bg-tertiary);
1418
- border-left: 3px solid var(--accent-yellow);
1419
- font-size: 13px;
1420
- padding: 10px 14px;
1421
- }
1422
- .message.tool .tool-name {
1423
- color: var(--accent-yellow);
1424
- font-weight: 600;
1425
- }
1426
- .message.tool .tool-status {
1427
- color: var(--text-muted);
1428
- margin-left: 8px;
1429
- }
1430
- .message.tool .tool-status.success { color: var(--accent-green); }
1431
- .message.tool .tool-status.error { color: var(--accent-red); }
1432
-
1433
- /* Thinking indicator */
1434
- .thinking {
1435
- display: none;
1436
- align-items: center;
1437
- gap: 10px;
1438
- padding: 12px 16px;
1439
- color: var(--text-secondary);
1440
- font-size: 13px;
1441
- }
1442
- .thinking.visible { display: flex; }
1443
- .spinner {
1444
- width: 16px;
1445
- height: 16px;
1446
- border: 2px solid var(--border-color);
1447
- border-top-color: var(--accent-cyan);
1448
- border-radius: 50%;
1449
- animation: spin 0.8s linear infinite;
1450
- }
1451
- @keyframes spin {
1452
- to { transform: rotate(360deg); }
1453
- }
1454
-
1455
- /* Input area */
1456
- .input-area {
1457
- background: var(--bg-secondary);
1458
- border: 1px solid var(--border-color);
1459
- border-radius: 8px;
1460
- padding: 12px;
1461
- margin-top: 16px;
1462
- position: relative;
1463
- }
1464
- .input-wrapper {
1465
- display: flex;
1466
- gap: 12px;
1467
- align-items: flex-end;
1468
- }
1469
- .input-field {
1470
- flex: 1;
1471
- background: transparent;
1472
- border: none;
1473
- color: var(--text-primary);
1474
- font-family: inherit;
1475
- font-size: 14px;
1476
- resize: none;
1477
- min-height: 24px;
1478
- max-height: 200px;
1479
- outline: none;
1480
- }
1481
- .input-field::placeholder { color: var(--text-muted); }
1482
- .send-btn {
1483
- background: var(--accent-cyan);
1484
- color: var(--bg-primary);
1485
- border: none;
1486
- padding: 8px 16px;
1487
- border-radius: 6px;
1488
- font-family: inherit;
1489
- font-weight: 600;
1490
- cursor: pointer;
1491
- transition: opacity 0.2s;
1492
- }
1493
- .send-btn:hover { opacity: 0.9; }
1494
- .send-btn:disabled {
1495
- opacity: 0.4;
1496
- cursor: not-allowed;
1497
- }
1498
- .input-hints {
1499
- display: flex;
1500
- gap: 16px;
1501
- margin-top: 8px;
1502
- font-size: 12px;
1503
- color: var(--text-muted);
1504
- }
1505
- .hint-key {
1506
- background: var(--bg-tertiary);
1507
- padding: 2px 6px;
1508
- border-radius: 3px;
1509
- font-size: 11px;
1510
- }
1511
-
1512
- /* Command popup */
1513
- .cmd-popup {
1514
- display: none;
1515
- position: absolute;
1516
- bottom: 100%;
1517
- left: 0;
1518
- right: 0;
1519
- background: var(--bg-secondary);
1520
- border: 1px solid var(--border-color);
1521
- border-radius: 8px;
1522
- margin-bottom: 8px;
1523
- max-height: 300px;
1524
- overflow-y: auto;
1525
- z-index: 100;
1526
- }
1527
- .cmd-popup.visible { display: block; }
1528
- .cmd-popup-header {
1529
- padding: 10px 14px;
1530
- border-bottom: 1px solid var(--border-color);
1531
- font-size: 12px;
1532
- color: var(--text-muted);
1533
- font-weight: 600;
1534
- }
1535
- .cmd-item {
1536
- padding: 10px 14px;
1537
- cursor: pointer;
1538
- display: flex;
1539
- align-items: center;
1540
- gap: 12px;
1541
- border-bottom: 1px solid var(--border-color);
1542
- }
1543
- .cmd-item:last-child { border-bottom: none; }
1544
- .cmd-item:hover { background: var(--bg-tertiary); }
1545
- .cmd-item.selected { background: var(--bg-tertiary); }
1546
- .cmd-item-icon {
1547
- width: 24px;
1548
- text-align: center;
1549
- }
1550
- .cmd-item-info { flex: 1; }
1551
- .cmd-item-name {
1552
- font-weight: 600;
1553
- color: var(--text-primary);
1554
- }
1555
- .cmd-item-desc {
1556
- font-size: 12px;
1557
- color: var(--text-muted);
1558
- }
1559
- .cmd-item-color {
1560
- width: 12px;
1561
- height: 12px;
1562
- border-radius: 3px;
1563
- }
1564
- .cmd-section-header {
1565
- padding: 8px 14px;
1566
- font-size: 11px;
1567
- text-transform: uppercase;
1568
- letter-spacing: 0.5px;
1569
- color: var(--text-muted);
1570
- background: var(--bg-tertiary);
1571
- font-weight: 600;
1572
- }
1573
-
1574
- /* Settings panel */
1575
- .settings-overlay {
1576
- display: none;
1577
- position: fixed;
1578
- top: 0;
1579
- left: 0;
1580
- right: 0;
1581
- bottom: 0;
1582
- background: rgba(0,0,0,0.7);
1583
- z-index: 200;
1584
- align-items: center;
1585
- justify-content: center;
1586
- }
1587
- .settings-overlay.visible {
1588
- display: flex;
1589
- }
1590
- .settings-panel {
1591
- background: var(--bg-secondary);
1592
- border: 1px solid var(--border-color);
1593
- border-radius: 12px;
1594
- width: 90%;
1595
- max-width: 500px;
1596
- max-height: 80vh;
1597
- overflow-y: auto;
1598
- }
1599
- .settings-header {
1600
- padding: 16px 20px;
1601
- border-bottom: 1px solid var(--border-color);
1602
- display: flex;
1603
- justify-content: space-between;
1604
- align-items: center;
1605
- }
1606
- .settings-header h2 {
1607
- font-size: 16px;
1608
- font-weight: 600;
1609
- }
1610
- .settings-close {
1611
- background: transparent;
1612
- border: none;
1613
- color: var(--text-secondary);
1614
- cursor: pointer;
1615
- font-size: 18px;
1616
- padding: 4px;
1617
- }
1618
- .settings-close:hover { color: var(--text-primary); }
1619
- .settings-content {
1620
- padding: 20px;
1621
- }
1622
- .settings-section {
1623
- margin-bottom: 24px;
1624
- }
1625
- .settings-section:last-child { margin-bottom: 0; }
1626
- .settings-section h3 {
1627
- font-size: 12px;
1628
- text-transform: uppercase;
1629
- color: var(--text-muted);
1630
- margin-bottom: 12px;
1631
- letter-spacing: 0.5px;
1632
- }
1633
- .settings-field {
1634
- margin-bottom: 16px;
1635
- }
1636
- .settings-field:last-child { margin-bottom: 0; }
1637
- .settings-field label {
1638
- display: block;
1639
- font-size: 13px;
1640
- color: var(--text-secondary);
1641
- margin-bottom: 6px;
1642
- }
1643
- .settings-field input,
1644
- .settings-field select {
1645
- width: 100%;
1646
- background: var(--bg-tertiary);
1647
- border: 1px solid var(--border-color);
1648
- border-radius: 6px;
1649
- padding: 10px 12px;
1650
- color: var(--text-primary);
1651
- font-family: inherit;
1652
- font-size: 13px;
1653
- }
1654
- .settings-field input:focus,
1655
- .settings-field select:focus {
1656
- outline: none;
1657
- border-color: var(--accent-cyan);
1658
- }
1659
- .settings-field input::placeholder { color: var(--text-muted); }
1660
- .settings-field small {
1661
- display: block;
1662
- margin-top: 4px;
1663
- font-size: 11px;
1664
- color: var(--text-muted);
1665
- }
1666
- .settings-btn {
1667
- width: 100%;
1668
- background: var(--accent-cyan);
1669
- color: var(--bg-primary);
1670
- border: none;
1671
- padding: 12px;
1672
- border-radius: 6px;
1673
- font-family: inherit;
1674
- font-weight: 600;
1675
- cursor: pointer;
1676
- margin-top: 16px;
1677
- }
1678
- .settings-btn:hover { opacity: 0.9; }
1679
-
1680
- /* Markdown rendering */
1681
- .message.assistant code {
1682
- background: var(--bg-tertiary);
1683
- padding: 2px 6px;
1684
- border-radius: 4px;
1685
- font-size: 13px;
1686
- }
1687
- .message.assistant pre {
1688
- background: var(--bg-primary);
1689
- border: 1px solid var(--border-color);
1690
- border-radius: 6px;
1691
- padding: 12px;
1692
- margin: 8px 0;
1693
- overflow-x: auto;
1694
- }
1695
- .message.assistant pre code {
1696
- background: none;
1697
- padding: 0;
1698
- }
1699
- .message.assistant strong { color: var(--accent-cyan); }
1700
- .message.assistant em { color: var(--text-secondary); }
1701
- .message.assistant a {
1702
- color: var(--accent-blue);
1703
- text-decoration: none;
1704
- }
1705
- .message.assistant a:hover { text-decoration: underline; }
1706
- .message.assistant ul, .message.assistant ol {
1707
- margin: 8px 0;
1708
- padding-left: 20px;
1709
- }
1710
- .message.assistant li { margin: 4px 0; }
1711
- .message.assistant blockquote {
1712
- border-left: 3px solid var(--border-color);
1713
- padding-left: 12px;
1714
- color: var(--text-secondary);
1715
- margin: 8px 0;
1716
- }
1717
- .message.assistant h1, .message.assistant h2, .message.assistant h3 {
1718
- margin: 16px 0 8px;
1719
- color: var(--text-primary);
1720
- }
1721
- .message.assistant h1 { font-size: 18px; }
1722
- .message.assistant h2 { font-size: 16px; }
1723
- .message.assistant h3 { font-size: 14px; }
1724
-
1725
- /* Settings button in header */
1726
- .settings-trigger {
1727
- background: transparent;
1728
- border: 1px solid var(--border-color);
1729
- color: var(--text-secondary);
1730
- padding: 6px 12px;
1731
- border-radius: 6px;
1732
- cursor: pointer;
1733
- font-family: inherit;
1734
- font-size: 12px;
1735
- display: flex;
1736
- align-items: center;
1737
- gap: 6px;
1738
- }
1739
- .settings-trigger:hover {
1740
- background: var(--bg-tertiary);
1741
- color: var(--text-primary);
1742
- }
1743
-
1744
- /* Responsive */
1745
- @media (max-width: 768px) {
1746
- .header { padding: 10px 14px; }
1747
- .header-info { display: none; }
1748
- .main { padding: 12px; }
1749
- .message { max-width: 95%; }
1750
- .settings-panel { width: 95%; }
1751
- }
1752
- </style>
1753
- </head>
1754
- <body>
1755
- <header class="header">
1756
- <div class="logo">
1757
- <div class="logo-icon">X</div>
1758
- <div class="logo-text">Xibe<span>Code</span></div>
1759
- </div>
1760
- <div class="header-info">
1761
- <div class="info-item">
1762
- <span class="label">Path:</span>
1763
- <span class="value" id="current-path">~</span>
1764
- </div>
1765
- <div class="info-item">
1766
- <span class="label">Model:</span>
1767
- <span class="value" id="current-model">-</span>
1768
- </div>
1769
- <div class="info-item">
1770
- <span class="label">Mode:</span>
1771
- <span class="value" id="current-mode">Agent</span>
1772
- </div>
1773
- <div class="info-item">
1774
- <span class="status-dot" id="status-dot"></span>
1775
- <span id="status-text">Connecting...</span>
1776
- </div>
1777
- </div>
1778
- <button class="settings-trigger" onclick="openSettings()">
1779
- <span>&#9881;</span> Settings
1780
- </button>
1781
- </header>
1782
-
1783
- <main class="main">
1784
- <div class="messages" id="messages">
1785
- <div class="message system">Welcome to XibeCode. Type <span class="hint-key">/</span> for modes or <span class="hint-key">@</span> to reference files.</div>
1786
- </div>
1787
-
1788
- <div class="thinking" id="thinking">
1789
- <div class="spinner"></div>
1790
- <span id="thinking-text">AI is thinking...</span>
1791
- </div>
1792
-
1793
- <div class="input-area">
1794
- <div class="cmd-popup" id="cmd-popup">
1795
- <div class="cmd-popup-header">Select Mode</div>
1796
- <div id="cmd-list"></div>
1797
- </div>
1798
- <div class="cmd-popup" id="file-popup">
1799
- <div class="cmd-popup-header">Select File</div>
1800
- <div id="file-list"></div>
1801
- </div>
1802
- <div class="input-wrapper">
1803
- <textarea class="input-field" id="user-input" placeholder="Type a message... (/ for modes, @ for files)" rows="1"></textarea>
1804
- <button class="send-btn" id="send-btn" onclick="sendMessage()">Send</button>
1805
- </div>
1806
- <div class="input-hints">
1807
- <span><span class="hint-key">/</span> modes</span>
1808
- <span><span class="hint-key">@</span> files</span>
1809
- <span><span class="hint-key">Enter</span> send</span>
1810
- <span><span class="hint-key">Shift+Enter</span> new line</span>
1811
- </div>
1812
- </div>
1813
- </main>
1814
-
1815
- <!-- Settings Panel -->
1816
- <div class="settings-overlay" id="settings-overlay" onclick="closeSettings(event)">
1817
- <div class="settings-panel" onclick="event.stopPropagation()">
1818
- <div class="settings-header">
1819
- <h2>Settings</h2>
1820
- <button class="settings-close" onclick="closeSettings()">&times;</button>
1821
- </div>
1822
- <div class="settings-content">
1823
- <div class="settings-section">
1824
- <h3>AI Provider</h3>
1825
- <div class="settings-field">
1826
- <label>Provider</label>
1827
- <select id="settings-provider" onchange="onProviderChange()">
1828
- <option value="anthropic">Anthropic (Claude)</option>
1829
- <option value="openai">OpenAI</option>
1830
- <option value="routingrun">Routing.run (recommended) (cheapest opensource model provider)</option>
1831
- <option value="zenllm">zenllm.org (recommended) (best ai provider with 200+ models)</option>
1832
- <option value="custom">Custom / OpenAI-compatible</option>
1833
- </select>
1834
- </div>
1835
- <div class="settings-field">
1836
- <label>Model</label>
1837
- <select id="settings-model">
1838
- <option value="claude-sonnet-4-5-20250929">Claude Sonnet 4.5</option>
1839
- <option value="claude-opus-4-5-20251101">Claude Opus 4.5</option>
1840
- <option value="claude-haiku-4-5-20251015">Claude Haiku 4.5</option>
1841
- </select>
1842
- </div>
1843
- <div class="settings-field" id="custom-model-field" style="display:none;">
1844
- <label>Custom Model ID</label>
1845
- <input type="text" id="settings-custom-model" placeholder="e.g., gpt-4-turbo, llama-3-70b">
1846
- <small>Enter the model identifier for your provider</small>
1847
- </div>
1848
- <div class="settings-field">
1849
- <label>API Key</label>
1850
- <input type="password" id="settings-api-key" placeholder="sk-...">
1851
- </div>
1852
- <div class="settings-field" id="base-url-field" style="display:none;">
1853
- <label>Base URL</label>
1854
- <input type="text" id="settings-base-url" placeholder="https://api.openai.com/v1">
1855
- <small>For custom OpenAI-compatible endpoints</small>
1856
- </div>
1857
- </div>
1858
-
1859
- <div class="settings-section">
1860
- <h3>Session Info</h3>
1861
- <div class="settings-field">
1862
- <label>Working Directory</label>
1863
- <input type="text" id="settings-workdir" readonly>
1864
- </div>
1865
- <div class="settings-field">
1866
- <label>Git Branch</label>
1867
- <input type="text" id="settings-branch" readonly>
1868
- </div>
1869
- <div class="settings-field">
1870
- <label>Session ID</label>
1871
- <input type="text" id="settings-session" readonly>
1872
- </div>
1873
- </div>
1874
-
1875
- <button class="settings-btn" onclick="saveSettings()">Save Settings</button>
1876
- </div>
1877
- </div>
1878
- </div>
1879
-
1880
- <script>
1881
- // Commands configuration
1882
- const COMMANDS = [
1883
- { id: 'clear', name: '/clear', icon: '&#x1F9F9;', desc: 'Clear chat messages', color: '#8B949E', type: 'action' },
1884
- { id: 'help', name: '/help', icon: '&#x2753;', desc: 'Show available commands', color: '#58A6FF', type: 'action' },
1885
- { id: 'diff', name: '/diff', icon: '&#x1F4DD;', desc: 'Show git diff', color: '#3FB950', type: 'action' },
1886
- { id: 'status', name: '/status', icon: '&#x1F4CA;', desc: 'Show git status', color: '#A371F7', type: 'action' },
1887
- { id: 'test', name: '/test', icon: '&#x1F9EA;', desc: 'Run project tests', color: '#FF4081', type: 'action' },
1888
- { id: 'format', name: '/format', icon: '&#x2728;', desc: 'Format code in project', color: '#FFD740', type: 'action' },
1889
- { id: 'reset', name: '/reset', icon: '&#x1F504;', desc: 'Reset chat session', color: '#F85149', type: 'action' },
1890
- { id: 'files', name: '/files', icon: '&#x1F4C1;', desc: 'List project files', color: '#39C5CF', type: 'action' },
1891
- ];
1892
-
1893
- // Modes configuration
1894
- const MODES = [
1895
- { id: 'agent', name: 'Agent', icon: '&#x1F916;', desc: 'Autonomous coding', color: '#00E676' },
1896
- { id: 'plan', name: 'Plan', icon: '&#x1F4CB;', desc: 'Analyze and plan without modifying', color: '#40C4FF' },
1897
- { id: 'review', name: 'Review', icon: '&#x1F440;', desc: 'Code review', color: '#BB86FC' },
1898
- ];
1899
-
1900
- // Combined list for slash popup
1901
- const ALL_SLASH_ITEMS = [
1902
- ...COMMANDS.map(c => ({ ...c, category: 'command' })),
1903
- ...MODES.map(m => ({ ...m, name: '/mode ' + m.id, category: 'mode' })),
1904
- ];
1905
-
1906
- let ws = null;
1907
- let files = [];
1908
- let selectedCmdIndex = 0;
1909
- let selectedFileIndex = 0;
1910
- let currentPopup = null; // 'modes' | 'files' | null
1911
- let streamingMessageEl = null;
1912
- let streamingText = ''; // Track raw text for streaming
1913
-
1914
- // Initialize
1915
- document.addEventListener('DOMContentLoaded', async () => {
1916
- await loadProjectInfo();
1917
- await loadConfig();
1918
- connectWebSocket();
1919
- setupInput();
1920
- });
1921
-
1922
- // Setup input handling
1923
- function setupInput() {
1924
- const input = document.getElementById('user-input');
1925
-
1926
- input.addEventListener('input', (e) => {
1927
- autoResize(input);
1928
- handleInputChange(e.target.value);
1929
- });
1930
-
1931
- input.addEventListener('keydown', (e) => {
1932
- if (currentPopup) {
1933
- if (e.key === 'ArrowDown') {
1934
- e.preventDefault();
1935
- navigatePopup(1);
1936
- } else if (e.key === 'ArrowUp') {
1937
- e.preventDefault();
1938
- navigatePopup(-1);
1939
- } else if (e.key === 'Enter' && !e.shiftKey) {
1940
- e.preventDefault();
1941
- selectPopupItem();
1942
- } else if (e.key === 'Escape') {
1943
- closePopups();
1944
- }
1945
- } else if (e.key === 'Enter' && !e.shiftKey) {
1946
- e.preventDefault();
1947
- sendMessage();
1948
- }
1949
- });
1950
- }
1951
-
1952
- function autoResize(textarea) {
1953
- textarea.style.height = 'auto';
1954
- textarea.style.height = Math.min(textarea.scrollHeight, 200) + 'px';
1955
- }
1956
-
1957
- function handleInputChange(value) {
1958
- const lastChar = value.slice(-1);
1959
- const beforeLast = value.slice(0, -1);
1960
-
1961
- // Check for / command at start or after space
1962
- if (lastChar === '/' && (beforeLast === '' || beforeLast.endsWith(' '))) {
1963
- openModePopup();
1964
- return;
1965
- }
1966
-
1967
- // Check for @ command
1968
- if (lastChar === '@' && (beforeLast === '' || beforeLast.endsWith(' '))) {
1969
- openFilePopup();
1970
- return;
1971
- }
1972
-
1973
- // Filter popups based on input after trigger
1974
- if (currentPopup === 'modes') {
1975
- const match = value.match(/\\/([\\w]*)$/);
1976
- if (match) {
1977
- filterSlashList(match[1]);
1978
- } else {
1979
- closePopups();
1980
- }
1981
- } else if (currentPopup === 'files') {
1982
- const match = value.match(/@([\\w\\.\\-\\/]*)$/);
1983
- if (match) {
1984
- filterFileList(match[1]);
1985
- } else {
1986
- closePopups();
1987
- }
1988
- }
1989
- }
1990
-
1991
- function openModePopup() {
1992
- currentPopup = 'modes';
1993
- selectedCmdIndex = 0;
1994
- document.getElementById('cmd-popup').classList.add('visible');
1995
- document.getElementById('file-popup').classList.remove('visible');
1996
- renderSlashList();
1997
- }
1998
-
1999
- function openFilePopup() {
2000
- currentPopup = 'files';
2001
- selectedFileIndex = 0;
2002
- document.getElementById('file-popup').classList.add('visible');
2003
- document.getElementById('cmd-popup').classList.remove('visible');
2004
- loadFiles();
2005
- }
2006
-
2007
- function closePopups() {
2008
- currentPopup = null;
2009
- document.getElementById('cmd-popup').classList.remove('visible');
2010
- document.getElementById('file-popup').classList.remove('visible');
2011
- }
2012
-
2013
- function renderSlashList(filter = '') {
2014
- const list = document.getElementById('cmd-list');
2015
- const header = document.querySelector('#cmd-popup .cmd-popup-header');
2016
- header.textContent = 'Commands & Modes';
2017
-
2018
- const filtered = ALL_SLASH_ITEMS.filter(item =>
2019
- item.name.toLowerCase().includes(filter.toLowerCase()) ||
2020
- item.id.toLowerCase().includes(filter.toLowerCase()) ||
2021
- item.desc.toLowerCase().includes(filter.toLowerCase())
2022
- );
2023
-
2024
- if (filtered.length === 0) {
2025
- list.innerHTML = '<div class="cmd-item"><div class="cmd-item-info"><div class="cmd-item-desc">No commands found</div></div></div>';
2026
- return;
2027
- }
2028
-
2029
- // Group by category
2030
- const commands = filtered.filter(i => i.category === 'command');
2031
- const modes = filtered.filter(i => i.category === 'mode');
2032
-
2033
- let html = '';
2034
-
2035
- if (commands.length > 0) {
2036
- html += '<div class="cmd-section-header">Commands</div>';
2037
- commands.forEach((item, i) => {
2038
- const globalIdx = filtered.indexOf(item);
2039
- html += \`
2040
- <div class="cmd-item \${globalIdx === selectedCmdIndex ? 'selected' : ''}"
2041
- onclick="executeSlashItem('\${item.id}', '\${item.category}')"
2042
- onmouseenter="selectedCmdIndex = \${globalIdx}; renderSlashList('\${filter}')">
2043
- <div class="cmd-item-icon">\${item.icon}</div>
2044
- <div class="cmd-item-info">
2045
- <div class="cmd-item-name">\${item.name}</div>
2046
- <div class="cmd-item-desc">\${item.desc}</div>
2047
- </div>
2048
- <div class="cmd-item-color" style="background: \${item.color}"></div>
2049
- </div>
2050
- \`;
2051
- });
2052
- }
2053
-
2054
- if (modes.length > 0) {
2055
- html += '<div class="cmd-section-header">Modes</div>';
2056
- modes.forEach((item, i) => {
2057
- const globalIdx = filtered.indexOf(item);
2058
- html += \`
2059
- <div class="cmd-item \${globalIdx === selectedCmdIndex ? 'selected' : ''}"
2060
- onclick="executeSlashItem('\${item.id}', '\${item.category}')"
2061
- onmouseenter="selectedCmdIndex = \${globalIdx}; renderSlashList('\${filter}')">
2062
- <div class="cmd-item-icon">\${item.icon}</div>
2063
- <div class="cmd-item-info">
2064
- <div class="cmd-item-name">\${item.name}</div>
2065
- <div class="cmd-item-desc">\${item.desc}</div>
2066
- </div>
2067
- <div class="cmd-item-color" style="background: \${item.color}"></div>
2068
- </div>
2069
- \`;
2070
- });
2071
- }
2072
-
2073
- list.innerHTML = html;
2074
- }
2075
-
2076
- function filterSlashList(filter) {
2077
- selectedCmdIndex = 0;
2078
- renderSlashList(filter);
2079
- }
2080
-
2081
- async function loadFiles() {
2082
- try {
2083
- const res = await fetch('/api/files');
2084
- const data = await res.json();
2085
- files = data.files || [];
2086
- renderFileList();
2087
- } catch (e) {
2088
- files = [];
2089
- renderFileList();
2090
- }
2091
- }
2092
-
2093
- function renderFileList(filter = '') {
2094
- const list = document.getElementById('file-list');
2095
- const filtered = files.filter(f =>
2096
- f.toLowerCase().includes(filter.toLowerCase())
2097
- ).slice(0, 20);
2098
-
2099
- if (filtered.length === 0) {
2100
- list.innerHTML = '<div class="cmd-item"><div class="cmd-item-info"><div class="cmd-item-desc">No files found</div></div></div>';
2101
- return;
2102
- }
2103
-
2104
- list.innerHTML = filtered.map((file, i) => \`
2105
- <div class="cmd-item \${i === selectedFileIndex ? 'selected' : ''}"
2106
- onclick="selectFile('\${file}')"
2107
- onmouseenter="selectedFileIndex = \${i}; renderFileList('\${filter}')">
2108
- <div class="cmd-item-icon">\${file.includes('.') ? '&#x1F4C4;' : '&#x1F4C1;'}</div>
2109
- <div class="cmd-item-info">
2110
- <div class="cmd-item-name">\${file.split('/').pop()}</div>
2111
- <div class="cmd-item-desc">\${file}</div>
2112
- </div>
2113
- </div>
2114
- \`).join('');
2115
- }
2116
-
2117
- function filterFileList(filter) {
2118
- selectedFileIndex = 0;
2119
- renderFileList(filter);
2120
- }
2121
-
2122
- function navigatePopup(direction) {
2123
- if (currentPopup === 'modes') {
2124
- const input = document.getElementById('user-input').value;
2125
- const match = input.match(/\\/([\\w]*)$/);
2126
- const filter = match ? match[1] : '';
2127
- const filtered = ALL_SLASH_ITEMS.filter(item =>
2128
- item.name.toLowerCase().includes(filter.toLowerCase()) ||
2129
- item.id.toLowerCase().includes(filter.toLowerCase())
2130
- );
2131
- selectedCmdIndex = Math.max(0, Math.min(filtered.length - 1, selectedCmdIndex + direction));
2132
- renderSlashList(filter);
2133
- } else if (currentPopup === 'files') {
2134
- const input = document.getElementById('user-input').value;
2135
- const match = input.match(/@([\\w\\.\\-\\/]*)$/);
2136
- const filter = match ? match[1] : '';
2137
- const filteredFiles = files.filter(f =>
2138
- f.toLowerCase().includes(filter.toLowerCase())
2139
- ).slice(0, 20);
2140
- selectedFileIndex = Math.max(0, Math.min(filteredFiles.length - 1, selectedFileIndex + direction));
2141
- renderFileList(filter);
2142
- }
2143
- }
2144
-
2145
- function selectPopupItem() {
2146
- if (currentPopup === 'modes') {
2147
- const input = document.getElementById('user-input').value;
2148
- const match = input.match(/\\/([\\w]*)$/);
2149
- const filter = match ? match[1] : '';
2150
- const filtered = ALL_SLASH_ITEMS.filter(item =>
2151
- item.name.toLowerCase().includes(filter.toLowerCase()) ||
2152
- item.id.toLowerCase().includes(filter.toLowerCase())
2153
- );
2154
- if (filtered[selectedCmdIndex]) {
2155
- executeSlashItem(filtered[selectedCmdIndex].id, filtered[selectedCmdIndex].category);
2156
- }
2157
- } else if (currentPopup === 'files') {
2158
- const input = document.getElementById('user-input').value;
2159
- const match = input.match(/@([\\w\\.\\-\\/]*)$/);
2160
- const filter = match ? match[1] : '';
2161
- const filteredFiles = files.filter(f =>
2162
- f.toLowerCase().includes(filter.toLowerCase())
2163
- ).slice(0, 20);
2164
- if (filteredFiles[selectedFileIndex]) {
2165
- selectFile(filteredFiles[selectedFileIndex]);
2166
- }
2167
- }
2168
- }
2169
-
2170
- let currentMode = 'agent';
2171
-
2172
- async function executeSlashItem(itemId, category) {
2173
- const input = document.getElementById('user-input');
2174
- input.value = ''; // Clear input
2175
- closePopups();
2176
- input.focus();
2177
-
2178
- if (category === 'mode') {
2179
- // Switch mode
2180
- currentMode = itemId;
2181
- const mode = MODES.find(m => m.id === itemId);
2182
- document.getElementById('current-mode').textContent = mode?.name || itemId;
2183
- addMessage('system', \`Switched to \${mode?.icon || ''} \${mode?.name || itemId} mode\`);
2184
-
2185
- // Send mode switch command to TUI
2186
- if (ws && ws.readyState === WebSocket.OPEN) {
2187
- ws.send(JSON.stringify({ type: 'message', content: '/mode ' + itemId }));
2188
- }
2189
- return;
2190
- }
2191
-
2192
- // Execute command
2193
- switch (itemId) {
2194
- case 'clear':
2195
- document.getElementById('messages').innerHTML = '<div class="message system">Chat cleared. Type <span class="hint-key">/</span> for commands.</div>';
2196
- break;
2197
-
2198
- case 'help':
2199
- const helpText = \`**Available Commands:**
2200
- - **/clear** - Clear chat messages
2201
- - **/help** - Show this help
2202
- - **/diff** - Show git diff
2203
- - **/status** - Show git status
2204
- - **/test** - Run project tests
2205
- - **/format** - Format code
2206
- - **/reset** - Reset session
2207
- - **/files** - List project files
2208
- - **/mode [name]** - Switch agent mode
2209
-
2210
- **Available Modes:** agent, plan, review
2211
-
2212
- **Tips:**
2213
- - Type **@** to reference files
2214
- - Press **Enter** to send, **Shift+Enter** for new line\`;
2215
- addMessage('assistant', helpText);
2216
- break;
2217
-
2218
- case 'diff':
2219
- showThinking(true, 'Getting git diff...');
2220
- try {
2221
- const diffRes = await fetch('/api/git/diff');
2222
- const diffData = await diffRes.json();
2223
- showThinking(false);
2224
- if (diffData.success && diffData.diff) {
2225
- addMessage('assistant', '**Git Diff:**\\n\`\`\`diff\\n' + diffData.diff + '\\n\`\`\`');
2226
- } else {
2227
- addMessage('system', 'No changes to show or not a git repository.');
2228
- }
2229
- } catch (e) {
2230
- showThinking(false);
2231
- addMessage('system', 'Failed to get git diff');
2232
- }
2233
- break;
2234
-
2235
- case 'status':
2236
- showThinking(true, 'Getting git status...');
2237
- try {
2238
- const statusRes = await fetch('/api/git/status');
2239
- const statusData = await statusRes.json();
2240
- showThinking(false);
2241
- if (statusData.success) {
2242
- const statusMsg = \`**Git Status:**
2243
- - Branch: **\${statusData.branch || 'unknown'}**
2244
- - Status: \${statusData.clean ? '✅ Clean' : '⚠️ Uncommitted changes'}
2245
- - Modified: \${statusData.modifiedCount || 0} files
2246
- - Staged: \${statusData.stagedCount || 0} files
2247
- - Untracked: \${statusData.untrackedCount || 0} files\`;
2248
- addMessage('assistant', statusMsg);
2249
- } else {
2250
- addMessage('system', 'Not a git repository or git not available.');
2251
- }
2252
- } catch (e) {
2253
- showThinking(false);
2254
- addMessage('system', 'Failed to get git status');
2255
- }
2256
- break;
2257
-
2258
- case 'test':
2259
- addMessage('system', 'Running tests...');
2260
- // Send to TUI to run tests
2261
- if (ws && ws.readyState === WebSocket.OPEN) {
2262
- ws.send(JSON.stringify({ type: 'message', content: 'Run the project tests' }));
2263
- }
2264
- break;
2265
-
2266
- case 'format':
2267
- addMessage('system', 'Formatting code...');
2268
- // Send to TUI to format
2269
- if (ws && ws.readyState === WebSocket.OPEN) {
2270
- ws.send(JSON.stringify({ type: 'message', content: 'Format the code in this project using the appropriate formatter' }));
2271
- }
2272
- break;
2273
-
2274
- case 'reset':
2275
- document.getElementById('messages').innerHTML = '<div class="message system">Session reset. Welcome to XibeCode.</div>';
2276
- currentMode = 'agent';
2277
- document.getElementById('current-mode').textContent = 'Agent';
2278
- break;
2279
-
2280
- case 'files':
2281
- showThinking(true, 'Listing files...');
2282
- try {
2283
- const filesRes = await fetch('/api/files');
2284
- const filesData = await filesRes.json();
2285
- showThinking(false);
2286
- if (filesData.success && filesData.files) {
2287
- const filesList = filesData.files.slice(0, 50).join('\\n');
2288
- addMessage('assistant', \`**Project Files (\${filesData.files.length} total):**\\n\\\`\\\`\\\`\\n\${filesList}\${filesData.files.length > 50 ? '\\n... and more' : ''}\\n\\\`\\\`\\\`\`);
2289
- } else {
2290
- addMessage('system', 'Failed to list files.');
2291
- }
2292
- } catch (e) {
2293
- showThinking(false);
2294
- addMessage('system', 'Failed to list files');
2295
- }
2296
- break;
2297
- }
2298
- }
2299
-
2300
- function selectFile(file) {
2301
- const input = document.getElementById('user-input');
2302
- // Replace @xxx with the file path
2303
- input.value = input.value.replace(/@[\\w\\.\\-\\/]*$/, '@' + file + ' ');
2304
- closePopups();
2305
- input.focus();
2306
- }
2307
-
2308
- // WebSocket connection
2309
- function connectWebSocket() {
2310
- const protocol = location.protocol === 'https:' ? 'wss:' : 'ws:';
2311
- ws = new WebSocket(\`\${protocol}//\${location.host}?mode=bridge\`);
2312
-
2313
- ws.onopen = () => {
2314
- document.getElementById('status-dot').classList.add('connected');
2315
- document.getElementById('status-text').textContent = 'Connected';
2316
- };
2317
-
2318
- ws.onclose = () => {
2319
- document.getElementById('status-dot').classList.remove('connected');
2320
- document.getElementById('status-text').textContent = 'Disconnected';
2321
- setTimeout(connectWebSocket, 3000);
2322
- };
2323
-
2324
- ws.onmessage = (event) => {
2325
- const data = JSON.parse(event.data);
2326
- handleWSMessage(data);
2327
- };
2328
- }
2329
-
2330
- function handleWSMessage(data) {
2331
- switch (data.type) {
2332
- case 'user_message':
2333
- // Only show TUI messages - WebUI messages are already shown locally
2334
- if (data.source === 'tui') {
2335
- addMessage('user', data.data.content + ' (TUI)');
2336
- }
2337
- document.getElementById('send-btn').disabled = true;
2338
- showThinking(true);
2339
- break;
2340
- case 'assistant_message':
2341
- addMessage('assistant', data.data.content);
2342
- document.getElementById('send-btn').disabled = false;
2343
- showThinking(false);
2344
- break;
2345
- case 'thinking':
2346
- showThinking(true, data.data?.text || 'Processing...');
2347
- break;
2348
- case 'stream_start':
2349
- startStreamMessage();
2350
- showThinking(false);
2351
- break;
2352
- case 'stream_text':
2353
- appendStreamText(data.data?.text || data.text || '');
2354
- break;
2355
- case 'stream_end':
2356
- endStreamMessage();
2357
- document.getElementById('send-btn').disabled = false;
2358
- break;
2359
- case 'tool_call':
2360
- addToolMessage(data.data?.name || data.name, 'running');
2361
- break;
2362
- case 'tool_result':
2363
- updateLastToolMessage(data.data?.name || data.name, data.data?.success ? 'success' : 'error');
2364
- break;
2365
- case 'complete':
2366
- document.getElementById('send-btn').disabled = false;
2367
- showThinking(false);
2368
- break;
2369
- case 'error':
2370
- addMessage('system', 'Error: ' + (data.data?.error || data.error));
2371
- document.getElementById('send-btn').disabled = false;
2372
- showThinking(false);
2373
- break;
2374
- case 'session_sync':
2375
- if (data.data?.sessionId) {
2376
- document.getElementById('settings-session').value = data.data.sessionId;
2377
- }
2378
- break;
2379
- }
2380
- }
2381
-
2382
- function showThinking(show, text = 'AI is thinking...') {
2383
- const el = document.getElementById('thinking');
2384
- if (show) {
2385
- el.classList.add('visible');
2386
- document.getElementById('thinking-text').textContent = text;
2387
- } else {
2388
- el.classList.remove('visible');
2389
- }
2390
- }
2391
-
2392
- function startStreamMessage() {
2393
- const messages = document.getElementById('messages');
2394
- streamingMessageEl = document.createElement('div');
2395
- streamingMessageEl.className = 'message assistant';
2396
- streamingText = ''; // Reset streaming text
2397
- messages.appendChild(streamingMessageEl);
2398
- messages.scrollTop = messages.scrollHeight;
2399
- }
2400
-
2401
- function appendStreamText(text) {
2402
- if (streamingMessageEl && text) {
2403
- streamingText += text;
2404
- // For performance, only render markdown every few updates or just show plain text while streaming
2405
- streamingMessageEl.textContent = streamingText;
2406
- document.getElementById('messages').scrollTop = document.getElementById('messages').scrollHeight;
2407
- }
2408
- }
2409
-
2410
- function endStreamMessage() {
2411
- if (streamingMessageEl) {
2412
- // Final markdown render when streaming is complete
2413
- streamingMessageEl.innerHTML = renderMarkdown(streamingText);
2414
- }
2415
- streamingMessageEl = null;
2416
- streamingText = '';
2417
- }
2418
-
2419
- function addMessage(role, content) {
2420
- const messages = document.getElementById('messages');
2421
- const msg = document.createElement('div');
2422
- msg.className = 'message ' + role;
2423
- if (role === 'assistant') {
2424
- msg.innerHTML = renderMarkdown(content);
2425
- } else {
2426
- msg.textContent = content;
2427
- }
2428
- messages.appendChild(msg);
2429
- messages.scrollTop = messages.scrollHeight;
2430
- }
2431
-
2432
- function addToolMessage(name, status) {
2433
- const messages = document.getElementById('messages');
2434
- const msg = document.createElement('div');
2435
- msg.className = 'message tool';
2436
- msg.innerHTML = \`<span class="tool-name">\${escapeHtml(name)}</span><span class="tool-status \${status}">\${status === 'running' ? '&#x23F3; running' : status}</span>\`;
2437
- msg.dataset.toolName = name;
2438
- messages.appendChild(msg);
2439
- messages.scrollTop = messages.scrollHeight;
2440
- }
2441
-
2442
- function updateLastToolMessage(name, status) {
2443
- const messages = document.getElementById('messages');
2444
- const toolMsgs = messages.querySelectorAll('.message.tool');
2445
- for (let i = toolMsgs.length - 1; i >= 0; i--) {
2446
- if (toolMsgs[i].dataset.toolName === name) {
2447
- toolMsgs[i].innerHTML = \`<span class="tool-name">\${escapeHtml(name)}</span><span class="tool-status \${status}">\${status === 'success' ? '&#x2713; done' : '&#x2717; failed'}</span>\`;
2448
- break;
2449
- }
2450
- }
2451
- }
2452
-
2453
- function sendMessage() {
2454
- const input = document.getElementById('user-input');
2455
- const message = input.value.trim();
2456
- if (!message) return;
2457
-
2458
- addMessage('user', message);
2459
- input.value = '';
2460
- autoResize(input);
2461
- document.getElementById('send-btn').disabled = true;
2462
- showThinking(true);
2463
-
2464
- if (ws && ws.readyState === WebSocket.OPEN) {
2465
- ws.send(JSON.stringify({ type: 'message', content: message }));
2466
- }
2467
- }
2468
-
2469
- // Simple markdown renderer
2470
- function renderMarkdown(text) {
2471
- if (!text) return '';
2472
- let html = escapeHtml(text);
2473
-
2474
- // Code blocks
2475
- html = html.replace(/\`\`\`([\\s\\S]*?)\`\`\`/g, '<pre><code>$1</code></pre>');
2476
- // Inline code
2477
- html = html.replace(/\`([^\`]+)\`/g, '<code>$1</code>');
2478
- // Bold
2479
- html = html.replace(/\\*\\*([^*]+)\\*\\*/g, '<strong>$1</strong>');
2480
- // Italic
2481
- html = html.replace(/\\*([^*]+)\\*/g, '<em>$1</em>');
2482
- // Headers
2483
- html = html.replace(/^### (.+)$/gm, '<h3>$1</h3>');
2484
- html = html.replace(/^## (.+)$/gm, '<h2>$1</h2>');
2485
- html = html.replace(/^# (.+)$/gm, '<h1>$1</h1>');
2486
- // Lists
2487
- html = html.replace(/^- (.+)$/gm, '<li>$1</li>');
2488
- html = html.replace(/(<li>.*<\\/li>\\n?)+/g, '<ul>$&</ul>');
2489
- // Links
2490
- html = html.replace(/\\[([^\\]]+)\\]\\(([^)]+)\\)/g, '<a href="$2" target="_blank">$1</a>');
2491
- // Line breaks
2492
- html = html.replace(/\\n/g, '<br>');
2493
-
2494
- return html;
2495
- }
2496
-
2497
- function escapeHtml(text) {
2498
- const div = document.createElement('div');
2499
- div.textContent = text;
2500
- return div.innerHTML;
2501
- }
2502
-
2503
- // Settings
2504
- function openSettings() {
2505
- document.getElementById('settings-overlay').classList.add('visible');
2506
- }
2507
-
2508
- function closeSettings(event) {
2509
- if (!event || event.target === event.currentTarget) {
2510
- document.getElementById('settings-overlay').classList.remove('visible');
2511
- }
2512
- }
2513
-
2514
- function onProviderChange() {
2515
- const provider = document.getElementById('settings-provider').value;
2516
- const modelSelect = document.getElementById('settings-model');
2517
- const customField = document.getElementById('custom-model-field');
2518
- const baseUrlField = document.getElementById('base-url-field');
2519
-
2520
- if (provider === 'anthropic') {
2521
- modelSelect.innerHTML = \`
2522
- <option value="claude-sonnet-4-5-20250929">Claude Sonnet 4.5</option>
2523
- <option value="claude-opus-4-5-20251101">Claude Opus 4.5</option>
2524
- <option value="claude-haiku-4-5-20251015">Claude Haiku 4.5</option>
2525
- \`;
2526
- customField.style.display = 'none';
2527
- baseUrlField.style.display = 'none';
2528
- } else if (provider === 'openai') {
2529
- modelSelect.innerHTML = \`
2530
- <option value="gpt-4o">GPT-4o</option>
2531
- <option value="gpt-4o-mini">GPT-4o Mini</option>
2532
- <option value="gpt-4-turbo">GPT-4 Turbo</option>
2533
- <option value="gpt-3.5-turbo">GPT-3.5 Turbo</option>
2534
- \`;
2535
- customField.style.display = 'none';
2536
- baseUrlField.style.display = 'none';
2537
- } else if (provider === 'routingrun' || provider === 'zenllm') {
2538
- modelSelect.innerHTML = \`
2539
- <option value="gpt-4o">GPT-4o</option>
2540
- <option value="gpt-4o-mini">GPT-4o Mini</option>
2541
- <option value="gpt-4-turbo">GPT-4 Turbo</option>
2542
- <option value="gpt-3.5-turbo">GPT-3.5 Turbo</option>
2543
- \`;
2544
- customField.style.display = 'none';
2545
- baseUrlField.style.display = 'none';
2546
- } else {
2547
- modelSelect.innerHTML = '<option value="custom">Custom Model</option>';
2548
- customField.style.display = 'block';
2549
- baseUrlField.style.display = 'block';
2550
- }
2551
- }
2552
-
2553
- async function loadProjectInfo() {
2554
- try {
2555
- const res = await fetch('/api/project');
2556
- const info = await res.json();
2557
- document.getElementById('current-path').textContent = info.workingDir || info.name || '~';
2558
- document.getElementById('settings-workdir').value = info.workingDir || process.cwd();
2559
- document.getElementById('settings-branch').value = info.gitBranch || 'N/A';
2560
- } catch (e) {
2561
- console.error('Failed to load project info:', e);
2562
- }
2563
- }
2564
-
2565
- async function loadConfig() {
2566
- try {
2567
- const res = await fetch('/api/config');
2568
- const config = await res.json();
2569
- document.getElementById('current-model').textContent = config.currentModel?.split('-').slice(0, 2).join('-') || 'Claude';
2570
- document.getElementById('settings-model').value = config.currentModel || 'claude-sonnet-4-5-20250929';
2571
- if (config.apiKeySet) {
2572
- document.getElementById('settings-api-key').placeholder = '••••••••';
2573
- }
2574
- } catch (e) {
2575
- console.error('Failed to load config:', e);
2576
- }
2577
- }
2578
-
2579
- async function saveSettings() {
2580
- const provider = document.getElementById('settings-provider').value;
2581
- const model = provider === 'custom'
2582
- ? document.getElementById('settings-custom-model').value
2583
- : document.getElementById('settings-model').value;
2584
- const apiKey = document.getElementById('settings-api-key').value;
2585
- const baseUrl = document.getElementById('settings-base-url').value;
2586
-
2587
- try {
2588
- await fetch('/api/config', {
2589
- method: 'PUT',
2590
- headers: { 'Content-Type': 'application/json' },
2591
- body: JSON.stringify({
2592
- model,
2593
- apiKey: apiKey || undefined,
2594
- baseUrl: baseUrl || undefined,
2595
- provider
2596
- }),
2597
- });
2598
- addMessage('system', 'Settings saved successfully');
2599
- closeSettings();
2600
- // Update header
2601
- document.getElementById('current-model').textContent = model.split('-').slice(0, 2).join('-');
2602
- } catch (e) {
2603
- addMessage('system', 'Failed to save settings');
2604
- }
2605
- }
2606
- </script>
2607
- </body>
2608
- </html>`;
2609
- }
2610
- }
2611
- /**
2612
- * Start WebUI server from CLI
2613
- */
2614
- export async function startWebUI(options = {}) {
2615
- const server = new WebUIServer(options);
2616
- await server.start();
2617
- return server;
2618
- }
2619
- //# sourceMappingURL=server.js.map