devglide 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (252) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +338 -0
  3. package/bin/claude-md-template.js +94 -0
  4. package/bin/devglide.js +387 -0
  5. package/package.json +85 -0
  6. package/pnpm-workspace.yaml +3 -0
  7. package/src/apps/coder/.turbo/turbo-lint.log +5 -0
  8. package/src/apps/coder/package.json +16 -0
  9. package/src/apps/coder/public/favicon.svg +7 -0
  10. package/src/apps/coder/public/page.css +275 -0
  11. package/src/apps/coder/public/page.js +528 -0
  12. package/src/apps/coder/server.js +3 -0
  13. package/src/apps/documentation/public/page.css +597 -0
  14. package/src/apps/documentation/public/page.js +609 -0
  15. package/src/apps/kanban/.turbo/turbo-lint.log +97 -0
  16. package/src/apps/kanban/.turbo/turbo-typecheck.log +5 -0
  17. package/src/apps/kanban/package.json +32 -0
  18. package/src/apps/kanban/public/favicon.svg +7 -0
  19. package/src/apps/kanban/public/page.css +1010 -0
  20. package/src/apps/kanban/public/page.js +1730 -0
  21. package/src/apps/kanban/public/vendor/marked.min.js +6 -0
  22. package/src/apps/kanban/public/vendor/sortable.min.js +2 -0
  23. package/src/apps/kanban/src/db.ts +319 -0
  24. package/src/apps/kanban/src/index.ts +14 -0
  25. package/src/apps/kanban/src/mcp-helpers.test.ts +88 -0
  26. package/src/apps/kanban/src/mcp-helpers.ts +60 -0
  27. package/src/apps/kanban/src/mcp.ts +59 -0
  28. package/src/apps/kanban/src/routes/attachments.ts +161 -0
  29. package/src/apps/kanban/src/routes/features.ts +233 -0
  30. package/src/apps/kanban/src/routes/issues.ts +373 -0
  31. package/src/apps/kanban/src/tools/feature-tools.ts +164 -0
  32. package/src/apps/kanban/src/tools/item-tools.ts +307 -0
  33. package/src/apps/kanban/src/tools/versioned-entry-tools.ts +72 -0
  34. package/src/apps/kanban/tsconfig.check.json +9 -0
  35. package/src/apps/kanban/tsconfig.json +9 -0
  36. package/src/apps/keymap/.turbo/turbo-lint.log +5 -0
  37. package/src/apps/keymap/package.json +16 -0
  38. package/src/apps/keymap/public/page.css +275 -0
  39. package/src/apps/keymap/public/page.js +294 -0
  40. package/src/apps/keymap/server.js +25 -0
  41. package/src/apps/log/.turbo/turbo-build.log +5 -0
  42. package/src/apps/log/.turbo/turbo-lint.log +45 -0
  43. package/src/apps/log/.turbo/turbo-typecheck.log +5 -0
  44. package/src/apps/log/node_modules/.bin/tsc +21 -0
  45. package/src/apps/log/node_modules/.bin/tsserver +21 -0
  46. package/src/apps/log/node_modules/.bin/tsx +21 -0
  47. package/src/apps/log/package.json +36 -0
  48. package/src/apps/log/public/console-sniffer.js +221 -0
  49. package/src/apps/log/public/favicon.svg +7 -0
  50. package/src/apps/log/public/page.css +322 -0
  51. package/src/apps/log/public/page.js +463 -0
  52. package/src/apps/log/src/index.ts +9 -0
  53. package/src/apps/log/src/mcp.ts +122 -0
  54. package/src/apps/log/src/routes/log.ts +333 -0
  55. package/src/apps/log/src/routes/status.ts +25 -0
  56. package/src/apps/log/src/server-sniffer.ts +118 -0
  57. package/src/apps/log/src/services/file-patterns.ts +39 -0
  58. package/src/apps/log/src/services/file-tailer.ts +228 -0
  59. package/src/apps/log/src/services/line-parser.ts +94 -0
  60. package/src/apps/log/src/services/log-writer.ts +39 -0
  61. package/src/apps/log/tsconfig.json +8 -0
  62. package/src/apps/prompts/.turbo/turbo-build.log +5 -0
  63. package/src/apps/prompts/.turbo/turbo-lint.log +24 -0
  64. package/src/apps/prompts/.turbo/turbo-typecheck.log +5 -0
  65. package/src/apps/prompts/mcp.ts +175 -0
  66. package/src/apps/prompts/node_modules/.bin/tsc +21 -0
  67. package/src/apps/prompts/node_modules/.bin/tsserver +21 -0
  68. package/src/apps/prompts/node_modules/.bin/tsx +21 -0
  69. package/src/apps/prompts/package.json +25 -0
  70. package/src/apps/prompts/public/page.css +315 -0
  71. package/src/apps/prompts/public/page.js +541 -0
  72. package/src/apps/prompts/services/prompt-store.ts +212 -0
  73. package/src/apps/prompts/src/index.ts +9 -0
  74. package/src/apps/prompts/tsconfig.json +8 -0
  75. package/src/apps/prompts/types.ts +27 -0
  76. package/src/apps/shell/.turbo/turbo-build.log +5 -0
  77. package/src/apps/shell/.turbo/turbo-lint.log +34 -0
  78. package/src/apps/shell/.turbo/turbo-typecheck.log +5 -0
  79. package/src/apps/shell/package.json +35 -0
  80. package/src/apps/shell/public/favicon.svg +7 -0
  81. package/src/apps/shell/public/page.css +407 -0
  82. package/src/apps/shell/public/page.js +1577 -0
  83. package/src/apps/shell/src/index.ts +150 -0
  84. package/src/apps/shell/src/mcp.ts +398 -0
  85. package/src/apps/shell/src/shell-types.ts +41 -0
  86. package/src/apps/shell/tsconfig.json +8 -0
  87. package/src/apps/test/.turbo/turbo-build.log +5 -0
  88. package/src/apps/test/.turbo/turbo-lint.log +27 -0
  89. package/src/apps/test/.turbo/turbo-typecheck.log +5 -0
  90. package/src/apps/test/node_modules/.bin/tsc +21 -0
  91. package/src/apps/test/node_modules/.bin/tsserver +21 -0
  92. package/src/apps/test/node_modules/.bin/tsx +21 -0
  93. package/src/apps/test/node_modules/.bin/uuid +21 -0
  94. package/src/apps/test/package.json +35 -0
  95. package/src/apps/test/public/favicon.svg +7 -0
  96. package/src/apps/test/public/page.css +499 -0
  97. package/src/apps/test/public/page.js +417 -0
  98. package/src/apps/test/public/scenario-runner.js +450 -0
  99. package/src/apps/test/src/index.ts +9 -0
  100. package/src/apps/test/src/mcp.ts +192 -0
  101. package/src/apps/test/src/routes/trigger.ts +285 -0
  102. package/src/apps/test/src/services/scenario-broadcaster.ts +60 -0
  103. package/src/apps/test/src/services/scenario-manager.ts +361 -0
  104. package/src/apps/test/src/services/scenario-store.ts +145 -0
  105. package/src/apps/test/tsconfig.json +8 -0
  106. package/src/apps/vocabulary/.turbo/turbo-build.log +5 -0
  107. package/src/apps/vocabulary/.turbo/turbo-lint.log +25 -0
  108. package/src/apps/vocabulary/.turbo/turbo-typecheck.log +5 -0
  109. package/src/apps/vocabulary/mcp.ts +173 -0
  110. package/src/apps/vocabulary/node_modules/.bin/tsc +21 -0
  111. package/src/apps/vocabulary/node_modules/.bin/tsserver +21 -0
  112. package/src/apps/vocabulary/node_modules/.bin/tsx +21 -0
  113. package/src/apps/vocabulary/package.json +25 -0
  114. package/src/apps/vocabulary/public/page.css +247 -0
  115. package/src/apps/vocabulary/public/page.js +444 -0
  116. package/src/apps/vocabulary/services/vocabulary-store.ts +179 -0
  117. package/src/apps/vocabulary/src/index.ts +10 -0
  118. package/src/apps/vocabulary/tsconfig.json +8 -0
  119. package/src/apps/vocabulary/types.ts +22 -0
  120. package/src/apps/voice/.turbo/turbo-build.log +5 -0
  121. package/src/apps/voice/.turbo/turbo-lint.log +43 -0
  122. package/src/apps/voice/.turbo/turbo-typecheck.log +5 -0
  123. package/src/apps/voice/node_modules/.bin/openai +21 -0
  124. package/src/apps/voice/node_modules/.bin/tsc +21 -0
  125. package/src/apps/voice/node_modules/.bin/tsserver +21 -0
  126. package/src/apps/voice/node_modules/.bin/tsx +21 -0
  127. package/src/apps/voice/package.json +35 -0
  128. package/src/apps/voice/public/favicon.svg +7 -0
  129. package/src/apps/voice/public/page.css +388 -0
  130. package/src/apps/voice/public/page.js +718 -0
  131. package/src/apps/voice/src/index.ts +10 -0
  132. package/src/apps/voice/src/mcp.ts +70 -0
  133. package/src/apps/voice/src/providers/index.ts +85 -0
  134. package/src/apps/voice/src/providers/openai-compatible.ts +94 -0
  135. package/src/apps/voice/src/providers/types.ts +27 -0
  136. package/src/apps/voice/src/routes/config.ts +118 -0
  137. package/src/apps/voice/src/routes/transcribe.ts +90 -0
  138. package/src/apps/voice/src/services/config-store.ts +129 -0
  139. package/src/apps/voice/src/services/stats.ts +108 -0
  140. package/src/apps/voice/src/transcribe.ts +11 -0
  141. package/src/apps/voice/src/utils/mime.ts +16 -0
  142. package/src/apps/voice/tsconfig.json +8 -0
  143. package/src/apps/workflow/.turbo/turbo-build.log +5 -0
  144. package/src/apps/workflow/.turbo/turbo-lint.log +96 -0
  145. package/src/apps/workflow/.turbo/turbo-typecheck.log +5 -0
  146. package/src/apps/workflow/engine/executors/decision-executor.ts +87 -0
  147. package/src/apps/workflow/engine/executors/file-executor.ts +90 -0
  148. package/src/apps/workflow/engine/executors/git-executor.ts +137 -0
  149. package/src/apps/workflow/engine/executors/http-executor.ts +65 -0
  150. package/src/apps/workflow/engine/executors/index.ts +28 -0
  151. package/src/apps/workflow/engine/executors/kanban-executor.ts +154 -0
  152. package/src/apps/workflow/engine/executors/llm-executor.ts +46 -0
  153. package/src/apps/workflow/engine/executors/log-executor.ts +62 -0
  154. package/src/apps/workflow/engine/executors/loop-executor.ts +14 -0
  155. package/src/apps/workflow/engine/executors/shell-executor.ts +107 -0
  156. package/src/apps/workflow/engine/executors/sub-workflow-executor.ts +61 -0
  157. package/src/apps/workflow/engine/executors/test-executor.ts +73 -0
  158. package/src/apps/workflow/engine/executors/trigger-executor.ts +39 -0
  159. package/src/apps/workflow/engine/expression-evaluator.ts +117 -0
  160. package/src/apps/workflow/engine/graph-runner.ts +438 -0
  161. package/src/apps/workflow/engine/node-executor.ts +104 -0
  162. package/src/apps/workflow/engine/node-registry.ts +15 -0
  163. package/src/apps/workflow/engine/variable-resolver.ts +109 -0
  164. package/src/apps/workflow/mcp.ts +223 -0
  165. package/src/apps/workflow/node_modules/.bin/tsc +21 -0
  166. package/src/apps/workflow/node_modules/.bin/tsserver +21 -0
  167. package/src/apps/workflow/node_modules/.bin/tsx +21 -0
  168. package/src/apps/workflow/package.json +25 -0
  169. package/src/apps/workflow/public/editor/canvas.js +366 -0
  170. package/src/apps/workflow/public/editor/drag-manager.js +326 -0
  171. package/src/apps/workflow/public/editor/edge-renderer.js +235 -0
  172. package/src/apps/workflow/public/editor/history-manager.js +147 -0
  173. package/src/apps/workflow/public/editor/layout-engine.js +159 -0
  174. package/src/apps/workflow/public/editor/node-renderer.js +199 -0
  175. package/src/apps/workflow/public/editor/selection-manager.js +193 -0
  176. package/src/apps/workflow/public/favicon.svg +7 -0
  177. package/src/apps/workflow/public/models/node-types.js +300 -0
  178. package/src/apps/workflow/public/models/workflow-model.js +257 -0
  179. package/src/apps/workflow/public/page.css +406 -0
  180. package/src/apps/workflow/public/page.js +658 -0
  181. package/src/apps/workflow/public/panels/inspector.js +360 -0
  182. package/src/apps/workflow/public/panels/palette.js +106 -0
  183. package/src/apps/workflow/public/panels/run-view.js +275 -0
  184. package/src/apps/workflow/public/panels/toolbar.js +232 -0
  185. package/src/apps/workflow/public/panels/workflow-list.js +237 -0
  186. package/src/apps/workflow/public/state/store.js +47 -0
  187. package/src/apps/workflow/services/custom-node-loader.ts +48 -0
  188. package/src/apps/workflow/services/legacy-converter.ts +72 -0
  189. package/src/apps/workflow/services/run-manager.ts +190 -0
  190. package/src/apps/workflow/services/workflow-store.ts +424 -0
  191. package/src/apps/workflow/services/workflow-validator.test.ts +103 -0
  192. package/src/apps/workflow/services/workflow-validator.ts +98 -0
  193. package/src/apps/workflow/src/index.ts +10 -0
  194. package/src/apps/workflow/templates/ci-pipeline.json +18 -0
  195. package/src/apps/workflow/templates/code-review.json +22 -0
  196. package/src/apps/workflow/templates/kanban-testing.json +24 -0
  197. package/src/apps/workflow/tsconfig.json +8 -0
  198. package/src/apps/workflow/types.ts +268 -0
  199. package/src/packages/auth-middleware.ts +14 -0
  200. package/src/packages/design-tokens/.turbo/turbo-build.log +10 -0
  201. package/src/packages/design-tokens/STYLEGUIDE.md +414 -0
  202. package/src/packages/design-tokens/build.js +413 -0
  203. package/src/packages/design-tokens/demo/index.html +1367 -0
  204. package/src/packages/design-tokens/demo/proposition-a.html +717 -0
  205. package/src/packages/design-tokens/demo/proposition-b.html +1239 -0
  206. package/src/packages/design-tokens/demo/proposition-c.html +1049 -0
  207. package/src/packages/design-tokens/dist/tailwind-preset.js +115 -0
  208. package/src/packages/design-tokens/dist/tokens.css +345 -0
  209. package/src/packages/design-tokens/dist/tokens.d.ts +229 -0
  210. package/src/packages/design-tokens/dist/tokens.js +386 -0
  211. package/src/packages/design-tokens/package.json +25 -0
  212. package/src/packages/design-tokens/tokens.json +228 -0
  213. package/src/packages/devtools-middleware.ts +22 -0
  214. package/src/packages/eslint-config/index.js +63 -0
  215. package/src/packages/eslint-config/node_modules/.bin/eslint +21 -0
  216. package/src/packages/eslint-config/package.json +18 -0
  217. package/src/packages/json-file-store.ts +232 -0
  218. package/src/packages/mcp-utils/.turbo/turbo-build.log +5 -0
  219. package/src/packages/mcp-utils/dist/index.d.ts +33 -0
  220. package/src/packages/mcp-utils/dist/index.d.ts.map +1 -0
  221. package/src/packages/mcp-utils/dist/index.js +126 -0
  222. package/src/packages/mcp-utils/dist/index.js.map +1 -0
  223. package/src/packages/mcp-utils/node_modules/.bin/tsc +21 -0
  224. package/src/packages/mcp-utils/node_modules/.bin/tsserver +21 -0
  225. package/src/packages/mcp-utils/package.json +32 -0
  226. package/src/packages/mcp-utils/src/index.ts +171 -0
  227. package/src/packages/mcp-utils/tsconfig.json +9 -0
  228. package/src/packages/paths.ts +18 -0
  229. package/src/packages/project-context/index.js +55 -0
  230. package/src/packages/project-context/package.json +13 -0
  231. package/src/packages/project-store.ts +127 -0
  232. package/src/packages/server-sniffer.ts +132 -0
  233. package/src/packages/shared-assets/favicon.svg +7 -0
  234. package/src/packages/shared-assets/keymap-registry.js +512 -0
  235. package/src/packages/shared-assets/logo.svg +6 -0
  236. package/src/packages/shared-assets/package.json +11 -0
  237. package/src/packages/shared-assets/ui-utils.js +48 -0
  238. package/src/packages/shared-assets/voice-widget.d.ts +37 -0
  239. package/src/packages/shared-assets/voice-widget.js +695 -0
  240. package/src/packages/shared-types/.turbo/turbo-build.log +5 -0
  241. package/src/packages/shared-types/dist/index.d.ts +39 -0
  242. package/src/packages/shared-types/dist/index.d.ts.map +1 -0
  243. package/src/packages/shared-types/node_modules/.bin/tsc +21 -0
  244. package/src/packages/shared-types/node_modules/.bin/tsserver +21 -0
  245. package/src/packages/shared-types/package.json +25 -0
  246. package/src/packages/shared-types/src/index.ts +41 -0
  247. package/src/packages/shared-types/tsconfig.json +11 -0
  248. package/src/packages/tsconfig/base.json +15 -0
  249. package/src/packages/tsconfig/next.json +14 -0
  250. package/src/packages/tsconfig/node.json +11 -0
  251. package/src/packages/tsconfig/package.json +10 -0
  252. package/turbo.json +25 -0
@@ -0,0 +1,528 @@
1
+ // ── Coder App — Page Module ───────────────────────────────────────────
2
+ // ES module that exports mount(container, ctx), unmount(container),
3
+ // and onProjectChange(project).
4
+ // Renders a Monaco-based code editor with file tree sidebar natively
5
+ // in the SPA shell (no iframe).
6
+
7
+ import { escapeHtml } from '/shared-assets/ui-utils.js';
8
+
9
+ const MONACO_CDN = 'https://cdn.jsdelivr.net/npm/monaco-editor@0.52.2';
10
+
11
+ // Proxy worker through a blob URL so cross-origin importScripts succeeds
12
+ window.MonacoEnvironment = {
13
+ getWorkerUrl(_moduleId, label) {
14
+ const workerPath = MONACO_CDN + '/min/vs/base/worker/workerMain.js';
15
+ const blob = new Blob(
16
+ [`importScripts("${workerPath}");`],
17
+ { type: 'application/javascript' }
18
+ );
19
+ return URL.createObjectURL(blob);
20
+ },
21
+ };
22
+
23
+ let _container = null;
24
+ let _editor = null;
25
+ let _voiceHandler = null;
26
+ let _currentRoot = null;
27
+ let _tabs = new Map(); // path -> { model, dirty }
28
+ let _activeFile = null;
29
+ let _treeGen = 0;
30
+ let _statusTimer = null;
31
+ let _monacoReady = false;
32
+
33
+ // ── HTML ─────────────────────────────────────────────────────────────
34
+
35
+ const BODY_HTML = `
36
+ <div class="coder-toolbar">
37
+ <span class="app-name">Coder</span>
38
+ <span class="save-status"></span>
39
+ </div>
40
+
41
+ <div class="coder-layout">
42
+ <div class="coder-sidebar" aria-label="File explorer">
43
+ <div class="tree-header">Explorer</div>
44
+ <div class="file-tree" role="tree" aria-label="File tree"></div>
45
+ </div>
46
+
47
+ <div class="coder-editor-area">
48
+ <div class="coder-tab-bar"></div>
49
+ <div class="coder-editor-wrap">
50
+ <div class="coder-editor-container"></div>
51
+ </div>
52
+ <div class="coder-no-file">
53
+ <div class="hint">&lt;/&gt;</div>
54
+ <div class="sub">Open a file from the explorer</div>
55
+ </div>
56
+ <div class="coder-status-bar"></div>
57
+ </div>
58
+ </div>
59
+
60
+ <div class="modal-overlay hidden" id="coder-confirm-dialog" role="dialog" aria-modal="true">
61
+ <div class="modal">
62
+ <div class="modal-header">
63
+ <h2 id="coder-confirm-title"></h2>
64
+ <p class="modal-desc" id="coder-confirm-msg"></p>
65
+ </div>
66
+ <div class="modal-actions">
67
+ <button class="btn btn-secondary" data-action="confirm-cancel">Cancel</button>
68
+ <button class="btn btn-danger" data-action="confirm-ok">Close Anyway</button>
69
+ </div>
70
+ </div>
71
+ </div>
72
+ `;
73
+
74
+ // ── Monaco loader ───────────────────────────────────────────────────
75
+
76
+ let _monacoLoadPromise = null;
77
+
78
+ function loadMonaco() {
79
+ if (_monacoLoadPromise) return _monacoLoadPromise;
80
+ _monacoLoadPromise = new Promise((resolve, reject) => {
81
+ // If Monaco is already available globally (loaded by another module)
82
+ if (typeof monaco !== 'undefined' && monaco.editor) {
83
+ _monacoReady = true;
84
+ resolve();
85
+ return;
86
+ }
87
+
88
+ // Check if the AMD loader script is already present
89
+ if (typeof require !== 'undefined' && typeof require.config === 'function') {
90
+ require.config({
91
+ paths: { vs: MONACO_CDN + '/min/vs' },
92
+ });
93
+ require(['vs/editor/editor.main'], () => {
94
+ _monacoReady = true;
95
+ resolve();
96
+ });
97
+ return;
98
+ }
99
+
100
+ // Load the AMD loader script
101
+ const script = document.createElement('script');
102
+ script.src = MONACO_CDN + '/min/vs/loader.js';
103
+ script.onload = () => {
104
+ require.config({
105
+ paths: { vs: MONACO_CDN + '/min/vs' },
106
+ });
107
+ require(['vs/editor/editor.main'], () => {
108
+ _monacoReady = true;
109
+ resolve();
110
+ });
111
+ };
112
+ script.onerror = () => reject(new Error('Failed to load Monaco editor'));
113
+ document.head.appendChild(script);
114
+ });
115
+ return _monacoLoadPromise;
116
+ }
117
+
118
+ // ── Monaco theme ────────────────────────────────────────────────────
119
+
120
+ function applyMonacoTheme() {
121
+ const s = getComputedStyle(document.documentElement);
122
+ const v = (name) => s.getPropertyValue(name).trim();
123
+ monaco.editor.defineTheme('devglide', {
124
+ base: 'vs-dark',
125
+ inherit: true,
126
+ rules: [
127
+ { token: 'comment', foreground: v('--df-color-text-muted').replace('#','') },
128
+ { token: 'keyword', foreground: v('--df-color-accent-default').replace('#','') },
129
+ { token: 'string', foreground: v('--df-color-state-success').replace('#','') },
130
+ { token: 'number', foreground: v('--df-color-accent-bright').replace('#','') },
131
+ ],
132
+ colors: {
133
+ 'editor.background': v('--df-color-bg-base'),
134
+ 'editor.foreground': v('--df-color-text-primary'),
135
+ 'editor.lineHighlightBackground': v('--df-color-bg-raised'),
136
+ 'editor.selectionBackground': v('--df-color-accent-dim'),
137
+ 'editorCursor.foreground': v('--df-color-accent-default'),
138
+ 'editorLineNumber.foreground': v('--df-color-text-muted'),
139
+ 'editorGutter.background': v('--df-color-bg-base'),
140
+ 'editorWidget.background': v('--df-color-bg-surface'),
141
+ 'editorWidget.border': v('--df-color-border-default'),
142
+ 'minimap.background': v('--df-color-bg-base'),
143
+ },
144
+ });
145
+ monaco.editor.setTheme('devglide');
146
+ }
147
+
148
+ // ── Helpers ──────────────────────────────────────────────────────────
149
+
150
+ function langFromPath(path) {
151
+ const ext = path.split('.').pop()?.toLowerCase() ?? '';
152
+ return {
153
+ ts: 'typescript', tsx: 'typescript',
154
+ js: 'javascript', jsx: 'javascript',
155
+ json: 'json', css: 'css', html: 'html',
156
+ md: 'markdown', sh: 'shell', bash: 'shell',
157
+ yaml: 'yaml', yml: 'yaml', py: 'python',
158
+ prisma: 'prisma', toml: 'toml', sql: 'sql',
159
+ }[ext] ?? 'plaintext';
160
+ }
161
+
162
+ function extBadge(name) {
163
+ const ext = name.split('.').pop()?.toLowerCase() ?? '';
164
+ const map = {
165
+ ts: 'TS', tsx: 'TS', js: 'JS', jsx: 'JS',
166
+ json: '{}', css: '~~', html: '<>', md: 'MD',
167
+ svg: 'SG', sh: '$_', py: 'PY', prisma: 'DB',
168
+ yaml: 'YL', yml: 'YL', toml: 'TM', sql: 'SQ',
169
+ };
170
+ return map[ext] ?? '\u00b7\u00b7';
171
+ }
172
+
173
+ function setStatus(msg, cls) {
174
+ if (!_container) return;
175
+ const el = _container.querySelector('.save-status');
176
+ if (!el) return;
177
+ el.textContent = msg;
178
+ el.className = 'save-status' + (cls ? ' ' + cls : '');
179
+ }
180
+
181
+ function showEditor(visible) {
182
+ if (!_container) return;
183
+ const wrap = _container.querySelector('.coder-editor-wrap');
184
+ const noFile = _container.querySelector('.coder-no-file');
185
+ if (wrap) wrap.style.display = visible ? 'block' : 'none';
186
+ if (noFile) noFile.style.display = visible ? 'none' : 'flex';
187
+ }
188
+
189
+ function updateTreeHeader() {
190
+ if (!_container) return;
191
+ const header = _container.querySelector('.tree-header');
192
+ if (header) header.textContent = _currentRoot ? _currentRoot.split('/').pop() : 'Explorer';
193
+ }
194
+
195
+ function coderConfirm(title, message) {
196
+ return new Promise(resolve => {
197
+ const overlay = _container?.querySelector('.modal-overlay');
198
+ if (!overlay) { resolve(false); return; }
199
+
200
+ const titleEl = overlay.querySelector('#coder-confirm-title');
201
+ const msgEl = overlay.querySelector('#coder-confirm-msg');
202
+ if (titleEl) titleEl.textContent = title;
203
+ if (msgEl) msgEl.textContent = message;
204
+
205
+ overlay.classList.remove('hidden');
206
+
207
+ const ac = new AbortController();
208
+ const close = (value) => {
209
+ overlay.classList.add('hidden');
210
+ ac.abort();
211
+ resolve(value);
212
+ };
213
+
214
+ overlay.addEventListener('click', (e) => {
215
+ if (e.target === overlay) close(false);
216
+ }, { signal: ac.signal });
217
+
218
+ overlay.querySelector('[data-action="confirm-cancel"]')?.addEventListener('click', () => close(false), { signal: ac.signal });
219
+ overlay.querySelector('[data-action="confirm-ok"]')?.addEventListener('click', () => close(true), { signal: ac.signal });
220
+
221
+ document.addEventListener('keydown', (e) => {
222
+ if (e.key === 'Escape') close(false);
223
+ }, { signal: ac.signal });
224
+ });
225
+ }
226
+
227
+ // ── File tree ───────────────────────────────────────────────────────
228
+
229
+ async function fetchTree() {
230
+ if (!_container) return;
231
+ const gen = ++_treeGen;
232
+ const tree = _container.querySelector('.file-tree');
233
+ if (!tree) return;
234
+ try {
235
+ const url = _currentRoot
236
+ ? `/api/coder/tree?root=${encodeURIComponent(_currentRoot)}`
237
+ : '/api/coder/tree';
238
+ const res = await fetch(url);
239
+ const nodes = await res.json();
240
+ if (gen !== _treeGen) return;
241
+ tree.innerHTML = '';
242
+ renderTree(nodes, tree, 0);
243
+ } catch (e) {
244
+ if (gen !== _treeGen) return;
245
+ tree.innerHTML = '';
246
+ tree.textContent = 'Failed to load tree.';
247
+ }
248
+ }
249
+
250
+ function renderTree(nodes, container, depth) {
251
+ for (const node of nodes) {
252
+ if (node.type === 'dir') {
253
+ const wrap = document.createElement('div');
254
+
255
+ const label = document.createElement('div');
256
+ label.className = 'tree-item tree-dir';
257
+ label.setAttribute('role', 'treeitem');
258
+ label.setAttribute('aria-expanded', 'false');
259
+ label.style.paddingLeft = `${depth * 14 + 8}px`;
260
+ label.innerHTML =
261
+ `<span class="dir-icon">&#9658;</span><span class="item-name">${escapeHtml(node.name)}</span>`;
262
+
263
+ const children = document.createElement('div');
264
+ children.className = 'tree-children collapsed';
265
+ children.setAttribute('role', 'group');
266
+ if (node.children?.length) renderTree(node.children, children, depth + 1);
267
+
268
+ label.addEventListener('click', () => {
269
+ const collapsed = children.classList.toggle('collapsed');
270
+ label.querySelector('.dir-icon').innerHTML = collapsed ? '&#9658;' : '&#9660;';
271
+ label.setAttribute('aria-expanded', String(!collapsed));
272
+ });
273
+
274
+ wrap.appendChild(label);
275
+ wrap.appendChild(children);
276
+ container.appendChild(wrap);
277
+ } else {
278
+ const item = document.createElement('div');
279
+ item.className = 'tree-item tree-file';
280
+ item.setAttribute('role', 'treeitem');
281
+ item.style.paddingLeft = `${depth * 14 + 8}px`;
282
+ item.dataset.path = node.path;
283
+ item.innerHTML =
284
+ `<span class="ext-badge">${extBadge(node.name)}</span><span class="item-name">${escapeHtml(node.name)}</span>`;
285
+ item.addEventListener('click', () => openFile(node.path));
286
+ container.appendChild(item);
287
+ }
288
+ }
289
+ }
290
+
291
+ // ── File open / tabs ────────────────────────────────────────────────
292
+
293
+ async function openFile(path) {
294
+ if (_tabs.has(path)) { activateTab(path); return; }
295
+ if (!_monacoReady || !_editor) return;
296
+ try {
297
+ const rootParam = _currentRoot ? `&root=${encodeURIComponent(_currentRoot)}` : '';
298
+ const res = await fetch(`/api/coder/file?path=${encodeURIComponent(path)}${rootParam}`);
299
+ if (!res.ok) { setStatus((await res.json()).error, 'err'); return; }
300
+ const { content } = await res.json();
301
+ const model = monaco.editor.createModel(content, langFromPath(path));
302
+ model.onDidChangeContent(() => markDirty(path));
303
+ _tabs.set(path, { model, dirty: false });
304
+ addTab(path);
305
+ activateTab(path);
306
+ } catch (e) {
307
+ setStatus(e.message, 'err');
308
+ }
309
+ }
310
+
311
+ function addTab(path) {
312
+ if (!_container) return;
313
+ const name = path.split('/').pop();
314
+ const tab = document.createElement('button');
315
+ tab.className = 'tab';
316
+ tab.dataset.path = path;
317
+ tab.title = path;
318
+ const escaped = escapeHtml(name);
319
+ tab.innerHTML = `<span class="tab-name">${escaped}</span><span class="tab-close" title="Close" aria-label="Close ${escaped}">&#215;</span>`;
320
+ tab.addEventListener('click', e => {
321
+ if (e.target.classList.contains('tab-close')) closeTab(path);
322
+ else activateTab(path);
323
+ });
324
+ _container.querySelector('.coder-tab-bar').appendChild(tab);
325
+ }
326
+
327
+ function activateTab(path) {
328
+ if (!_container || !_editor) return;
329
+ _activeFile = path;
330
+ _container.querySelectorAll('.tab').forEach(t =>
331
+ t.classList.toggle('active', t.dataset.path === path));
332
+ _container.querySelectorAll('.tree-file').forEach(el =>
333
+ el.classList.toggle('active', el.dataset.path === path));
334
+ _editor.setModel(_tabs.get(path).model);
335
+ showEditor(true);
336
+ const statusBar = _container.querySelector('.coder-status-bar');
337
+ if (statusBar) statusBar.textContent = `${path} \u00b7 ${langFromPath(path)}`;
338
+ }
339
+
340
+ async function closeTab(path) {
341
+ if (!_container) return;
342
+ const tab = _tabs.get(path);
343
+ if (tab?.dirty) {
344
+ const ok = await coderConfirm('Unsaved Changes', `Unsaved changes in ${path.split('/').pop()}. Close anyway?`);
345
+ if (!ok) return;
346
+ }
347
+ tab?.model?.dispose();
348
+ _tabs.delete(path);
349
+ _container.querySelector(`.tab[data-path="${CSS.escape(path)}"]`)?.remove();
350
+ if (_activeFile === path) {
351
+ const remaining = [..._tabs.keys()];
352
+ if (remaining.length) {
353
+ activateTab(remaining[remaining.length - 1]);
354
+ } else {
355
+ _activeFile = null;
356
+ if (_editor) _editor.setModel(null);
357
+ showEditor(false);
358
+ const statusBar = _container.querySelector('.coder-status-bar');
359
+ if (statusBar) statusBar.textContent = '';
360
+ }
361
+ }
362
+ }
363
+
364
+ function markDirty(path) {
365
+ const tab = _tabs.get(path);
366
+ if (!tab || tab.dirty) return;
367
+ tab.dirty = true;
368
+ if (!_container) return;
369
+ const nameEl = _container.querySelector(`.tab[data-path="${CSS.escape(path)}"] .tab-name`);
370
+ if (nameEl) nameEl.textContent = path.split('/').pop() + ' \u25cf';
371
+ }
372
+
373
+ // ── Save ────────────────────────────────────────────────────────────
374
+
375
+ async function saveActive() {
376
+ if (!_activeFile) return;
377
+ const tab = _tabs.get(_activeFile);
378
+ if (!tab) return;
379
+ const content = tab.model.getValue();
380
+ setStatus('Saving\u2026', '');
381
+ try {
382
+ const res = await fetch('/api/coder/file', {
383
+ method: 'PUT',
384
+ headers: { 'Content-Type': 'application/json' },
385
+ body: JSON.stringify({ path: _activeFile, content, root: _currentRoot }),
386
+ });
387
+ if (!res.ok) throw new Error('Server error');
388
+ tab.dirty = false;
389
+ if (_container) {
390
+ const nameEl = _container.querySelector(`.tab[data-path="${CSS.escape(_activeFile)}"] .tab-name`);
391
+ if (nameEl) nameEl.textContent = _activeFile.split('/').pop();
392
+ }
393
+ setStatus('Saved \u2713', 'ok');
394
+ if (_statusTimer) clearTimeout(_statusTimer);
395
+ _statusTimer = setTimeout(() => setStatus('', ''), 2000);
396
+ } catch (e) {
397
+ setStatus('Save failed!', 'err');
398
+ }
399
+ }
400
+
401
+ // ── Refresh tree (on project change) ────────────────────────────────
402
+
403
+ function refreshTree() {
404
+ // Close all tabs and clear editor
405
+ for (const [p] of _tabs) closeTab(p);
406
+ if (_container) {
407
+ const tree = _container.querySelector('.file-tree');
408
+ if (tree) tree.innerHTML = '';
409
+ }
410
+ fetchTree();
411
+ }
412
+
413
+ // ── Exports ─────────────────────────────────────────────────────────
414
+
415
+ export async function mount(container, ctx) {
416
+ _container = container;
417
+ _tabs = new Map();
418
+ _activeFile = null;
419
+ _treeGen = 0;
420
+
421
+ // 1. Scope the container
422
+ container.classList.add('page-coder');
423
+
424
+ // 2. Build HTML
425
+ container.innerHTML = BODY_HTML;
426
+
427
+ // 3. Show empty state initially
428
+ showEditor(false);
429
+
430
+ // 4. Set initial project from context
431
+ _currentRoot = ctx?.project?.path || null;
432
+ updateTreeHeader();
433
+
434
+ // 5. Load Monaco and initialize editor
435
+ try {
436
+ await loadMonaco();
437
+ // Guard against unmount during async load
438
+ if (!_container) return;
439
+
440
+ applyMonacoTheme();
441
+
442
+ const editorContainer = container.querySelector('.coder-editor-container');
443
+ _editor = monaco.editor.create(editorContainer, {
444
+ theme: 'devglide',
445
+ automaticLayout: true,
446
+ fontSize: 14,
447
+ fontFamily: "'JetBrains Mono', 'Fira Code', 'Cascadia Code', 'Consolas', monospace",
448
+ minimap: { enabled: true },
449
+ scrollBeyondLastLine: false,
450
+ tabSize: 2,
451
+ renderWhitespace: 'selection',
452
+ });
453
+
454
+ // Ctrl/Cmd+S to save
455
+ _editor.addCommand(monaco.KeyMod.CtrlCmd | monaco.KeyCode.KeyS, saveActive);
456
+
457
+ // 6. Load file tree
458
+ fetchTree();
459
+ } catch (e) {
460
+ if (_container) {
461
+ const tree = container.querySelector('.file-tree');
462
+ if (tree) tree.textContent = 'Failed to load editor: ' + e.message;
463
+ }
464
+ return;
465
+ }
466
+
467
+ // 7. Voice input: insert dictated text at cursor when editor is focused
468
+ _voiceHandler = (e) => {
469
+ if (!_editor || !_container) return;
470
+ if (!_editor.hasTextFocus()) return;
471
+ const text = e.detail?.text;
472
+ if (!text) return;
473
+ const selection = _editor.getSelection();
474
+ if (selection) {
475
+ _editor.executeEdits('voice', [{
476
+ range: selection,
477
+ text: text,
478
+ forceMoveMarkers: true,
479
+ }]);
480
+ }
481
+ };
482
+ document.addEventListener('voice:result', _voiceHandler);
483
+ }
484
+
485
+ export function unmount(container) {
486
+ // 1. Clear status timer
487
+ if (_statusTimer) { clearTimeout(_statusTimer); _statusTimer = null; }
488
+
489
+ // 2. Remove voice handler
490
+ if (_voiceHandler) {
491
+ document.removeEventListener('voice:result', _voiceHandler);
492
+ _voiceHandler = null;
493
+ }
494
+
495
+ // 3. Dispose all Monaco models
496
+ for (const [, tab] of _tabs) {
497
+ tab.model?.dispose();
498
+ }
499
+ _tabs = new Map();
500
+ _activeFile = null;
501
+
502
+ // 4. Dispose Monaco editor instance
503
+ if (_editor) {
504
+ _editor.dispose();
505
+ _editor = null;
506
+ }
507
+
508
+ // 5. Remove scope class & clear HTML
509
+ container.classList.remove('page-coder');
510
+ container.innerHTML = '';
511
+
512
+ // 6. Clear module references
513
+ _container = null;
514
+ _currentRoot = null;
515
+ }
516
+
517
+ export function onProjectChange(project) {
518
+ const newRoot = project?.path || null;
519
+ if (newRoot && newRoot !== _currentRoot) {
520
+ _currentRoot = newRoot;
521
+ updateTreeHeader();
522
+ refreshTree();
523
+ } else if (!newRoot) {
524
+ _currentRoot = null;
525
+ updateTreeHeader();
526
+ refreshTree();
527
+ }
528
+ }
@@ -0,0 +1,3 @@
1
+ // ── Coder — standalone server (superseded by unified server) ─────────────────
2
+ // This file is retained for reference. All coder routes are now served by
3
+ // src/server.ts via src/routers/coder.ts. Use `devglide dev` to run.