@nimbus-sh/worker 0.1.4 → 0.1.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +37 -0
- package/dist/_shared/crypto.d.ts +15 -0
- package/dist/_shared/crypto.d.ts.map +1 -0
- package/dist/_shared/crypto.js +122 -0
- package/dist/facets/manager.d.ts +24 -0
- package/dist/facets/manager.d.ts.map +1 -1
- package/dist/facets/manager.js +279 -70
- package/dist/facets/process.d.ts +11 -2
- package/dist/facets/process.d.ts.map +1 -1
- package/dist/facets/process.js +187 -21
- package/dist/facets/vite-dev-server.d.ts +2 -0
- package/dist/facets/vite-dev-server.d.ts.map +1 -1
- package/dist/facets/vite-dev-server.js +39 -12
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/loaders/child-process/spawn-facet.d.ts +1 -3
- package/dist/loaders/child-process/spawn-facet.d.ts.map +1 -1
- package/dist/loaders/child-process/spawn-facet.js +1 -12
- package/dist/loaders/child-process/spawn-pool.d.ts +1 -1
- package/dist/loaders/child-process/spawn-pool.d.ts.map +1 -1
- package/dist/loaders/child-process/spawn-pool.js +1 -6
- package/dist/loaders/loader-pool.d.ts +7 -0
- package/dist/loaders/loader-pool.d.ts.map +1 -1
- package/dist/loaders/loader-pool.js +3 -1
- package/dist/npm/installer.d.ts.map +1 -1
- package/dist/npm/installer.js +47 -27
- package/dist/npm/pre-bundle-facet.d.ts.map +1 -1
- package/dist/npm/pre-bundle-facet.js +2 -1
- package/dist/npm/resolve-one-facet.d.ts.map +1 -1
- package/dist/npm/resolve-one-facet.js +41 -13
- package/dist/npm/resolver.d.ts.map +1 -1
- package/dist/npm/resolver.js +70 -35
- package/dist/router/index.d.ts.map +1 -1
- package/dist/router/index.js +45 -21
- package/dist/router/remote-api.d.ts.map +1 -1
- package/dist/router/remote-api.js +29 -8
- package/dist/runtime/barrel-synthesizer.d.ts +7 -0
- package/dist/runtime/barrel-synthesizer.d.ts.map +1 -1
- package/dist/runtime/barrel-synthesizer.js +11 -0
- package/dist/runtime/clang-runner.d.ts +2 -2
- package/dist/runtime/clang-runner.d.ts.map +1 -1
- package/dist/runtime/clang-runner.js +64 -47
- package/dist/runtime/esbuild-service.d.ts +12 -5
- package/dist/runtime/esbuild-service.d.ts.map +1 -1
- package/dist/runtime/esbuild-service.js +28 -14
- package/dist/runtime/node-runner.d.ts +2 -0
- package/dist/runtime/node-runner.d.ts.map +1 -1
- package/dist/runtime/node-shims.d.ts +4 -2
- package/dist/runtime/node-shims.d.ts.map +1 -1
- package/dist/runtime/node-shims.js +404 -55
- package/dist/runtime/os-contracts.d.ts +128 -0
- package/dist/runtime/os-contracts.d.ts.map +1 -0
- package/dist/runtime/os-contracts.js +1 -0
- package/dist/runtime/package-manager.d.ts +31 -0
- package/dist/runtime/package-manager.d.ts.map +1 -1
- package/dist/runtime/package-manager.js +162 -15
- package/dist/runtime/port-registry.d.ts +5 -0
- package/dist/runtime/port-registry.d.ts.map +1 -1
- package/dist/runtime/port-registry.js +22 -1
- package/dist/runtime/pyodide-runtime-assets.d.ts +19 -0
- package/dist/runtime/pyodide-runtime-assets.d.ts.map +1 -0
- package/dist/runtime/pyodide-runtime-assets.js +44 -0
- package/dist/runtime/python-repl.d.ts +3 -0
- package/dist/runtime/python-repl.d.ts.map +1 -1
- package/dist/runtime/python-repl.js +18 -32
- package/dist/runtime/python-runner.d.ts +8 -9
- package/dist/runtime/python-runner.d.ts.map +1 -1
- package/dist/runtime/python-runner.js +958 -118
- package/dist/runtime/python-socket-shim.d.ts +11 -0
- package/dist/runtime/python-socket-shim.d.ts.map +1 -0
- package/dist/runtime/python-socket-shim.js +526 -0
- package/dist/runtime/require-resolver.d.ts.map +1 -1
- package/dist/runtime/require-resolver.js +2 -11
- package/dist/runtime/ruby-gems.d.ts +25 -0
- package/dist/runtime/ruby-gems.d.ts.map +1 -0
- package/dist/runtime/ruby-gems.js +582 -0
- package/dist/runtime/ruby-runner.d.ts +2 -4
- package/dist/runtime/ruby-runner.d.ts.map +1 -1
- package/dist/runtime/ruby-runner.js +335 -34
- package/dist/runtime/runtime-catalog.d.ts +16 -4
- package/dist/runtime/runtime-catalog.d.ts.map +1 -1
- package/dist/runtime/runtime-catalog.js +21 -10
- package/dist/runtime/runtime-registry.d.ts +3 -0
- package/dist/runtime/runtime-registry.d.ts.map +1 -1
- package/dist/runtime/runtime-registry.js +4 -0
- package/dist/runtime/sqlite-runtime-fs-bridge.d.ts +43 -0
- package/dist/runtime/sqlite-runtime-fs-bridge.d.ts.map +1 -0
- package/dist/runtime/sqlite-runtime-fs-bridge.js +224 -0
- package/dist/runtime/static-server.d.ts +3 -0
- package/dist/runtime/static-server.d.ts.map +1 -0
- package/dist/runtime/static-server.js +121 -0
- package/dist/runtime/vfs-snapshot.d.ts +57 -0
- package/dist/runtime/vfs-snapshot.d.ts.map +1 -0
- package/dist/runtime/vfs-snapshot.js +141 -0
- package/dist/runtime/virtual-socket-kernel.d.ts +16 -0
- package/dist/runtime/virtual-socket-kernel.d.ts.map +1 -0
- package/dist/runtime/virtual-socket-kernel.js +360 -0
- package/dist/runtime/wasi-instance.d.ts +6 -3
- package/dist/runtime/wasi-instance.d.ts.map +1 -1
- package/dist/runtime/wasm-runner.d.ts +1 -25
- package/dist/runtime/wasm-runner.d.ts.map +1 -1
- package/dist/runtime/wasm-runner.js +1 -160
- package/dist/session/agent.d.ts +43 -0
- package/dist/session/agent.d.ts.map +1 -0
- package/dist/session/agent.js +1252 -0
- package/dist/session/init.d.ts.map +1 -1
- package/dist/session/init.js +64 -19
- package/dist/session/nimbus-session.d.ts +14 -2
- package/dist/session/nimbus-session.d.ts.map +1 -1
- package/dist/session/nimbus-session.js +104 -30
- package/dist/session/programmatic.d.ts +10 -0
- package/dist/session/programmatic.d.ts.map +1 -1
- package/dist/session/programmatic.js +121 -0
- package/dist/session/routes.d.ts.map +1 -1
- package/dist/session/routes.js +19 -1
- package/dist/session/rpc.d.ts +11 -0
- package/dist/session/rpc.d.ts.map +1 -1
- package/dist/session/rpc.js +56 -43
- package/dist/session/supervisor-rpc.d.ts +15 -0
- package/dist/session/supervisor-rpc.d.ts.map +1 -1
- package/dist/session/supervisor-rpc.js +32 -0
- package/dist/session/ws.d.ts +4 -3
- package/dist/session/ws.d.ts.map +1 -1
- package/dist/session/ws.js +11 -1
- package/dist/shell/unix-commands.d.ts.map +1 -1
- package/dist/shell/unix-commands.js +52 -51
- package/dist/vfs/path.d.ts +8 -4
- package/dist/vfs/path.d.ts.map +1 -1
- package/dist/vfs/path.js +16 -4
- package/dist/vfs/sqlite-vfs.d.ts +2 -0
- package/dist/vfs/sqlite-vfs.d.ts.map +1 -1
- package/dist/vfs/sqlite-vfs.js +11 -0
- package/dist/vfs/symlink-registry.d.ts +7 -1
- package/dist/vfs/symlink-registry.d.ts.map +1 -1
- package/dist/vfs/symlink-registry.js +16 -11
- package/package.json +4 -1
- package/public/index.html +240 -0
- package/public/s/index.html +1316 -128
- package/runtime-contracts/pyodide-workerd-adapter.json +18 -0
- package/scripts/bundle-runtime.mjs +99 -24
package/public/s/index.html
CHANGED
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
.toolbar .sep { width: 1px; height: 16px; background: #30363d; margin: 0 4px; }
|
|
30
30
|
.toolbar .vite-status { font-size: 11px; color: #8b949e; margin-left: auto; }
|
|
31
31
|
.toolbar .vite-status.running { color: #3fb950; }
|
|
32
|
-
/* Process count button. Sits next to the
|
|
32
|
+
/* Process count button. Sits next to the workspace buttons. Hidden
|
|
33
33
|
(display:none) until at least one process has been seen in this
|
|
34
34
|
session so it doesn't clutter a brand-new session's toolbar. */
|
|
35
35
|
.toolbar #btnProcs { display: none; gap: 6px; align-items: center; font-family: ui-monospace, Menlo, monospace; }
|
|
@@ -96,11 +96,81 @@
|
|
|
96
96
|
/* Preview panel */
|
|
97
97
|
.panel-preview { flex: 1; display: flex; flex-direction: column; background: #0d1117; overflow: hidden; min-width: 0; }
|
|
98
98
|
.panel-preview.hidden { display: none; }
|
|
99
|
-
.preview-bar { display: flex; align-items: center; gap: 6px; padding: 3px 8px; background: #161b22; border-bottom: 1px solid #30363d; flex-shrink: 0; }
|
|
99
|
+
.preview-bar { display: flex; align-items: center; gap: 6px; padding: 3px 8px; background: #161b22; border-bottom: 1px solid #30363d; flex-shrink: 0; min-width: 0; }
|
|
100
|
+
.preview-tabs {
|
|
101
|
+
display: flex; align-items: center; gap: 3px; min-width: 110px; max-width: 45%;
|
|
102
|
+
overflow-x: auto; scrollbar-width: none; flex: 0 1 auto;
|
|
103
|
+
}
|
|
104
|
+
.preview-tabs::-webkit-scrollbar { display: none; }
|
|
105
|
+
.preview-bar .preview-tab {
|
|
106
|
+
height: 22px; display: inline-flex; align-items: center; gap: 5px; min-width: 0;
|
|
107
|
+
max-width: 150px; padding: 0 8px; border-radius: 4px 4px 0 0;
|
|
108
|
+
border: 1px solid transparent; border-bottom-color: transparent;
|
|
109
|
+
background: transparent; color: #8b949e; font-size: 11px; line-height: 1;
|
|
110
|
+
white-space: nowrap; cursor: pointer;
|
|
111
|
+
}
|
|
112
|
+
.preview-bar .preview-tab:hover { background: #21262d; color: #c9d1d9; }
|
|
113
|
+
.preview-bar .preview-tab.active {
|
|
114
|
+
background: #0d1117; color: #f0f6fc; border-color: #30363d;
|
|
115
|
+
border-bottom-color: #0d1117;
|
|
116
|
+
}
|
|
117
|
+
.preview-bar .preview-tab.running::before {
|
|
118
|
+
content: ""; width: 6px; height: 6px; border-radius: 999px;
|
|
119
|
+
background: #3fb950; box-shadow: 0 0 8px rgba(63,185,80,0.55);
|
|
120
|
+
flex: 0 0 auto;
|
|
121
|
+
}
|
|
122
|
+
.preview-bar .preview-tab.markdown::before {
|
|
123
|
+
content: "md"; color: #79c0ff; font: 9px/1 ui-monospace, Menlo, monospace;
|
|
124
|
+
border: 1px solid rgba(121,192,255,0.35); border-radius: 3px; padding: 1px 2px;
|
|
125
|
+
flex: 0 0 auto;
|
|
126
|
+
}
|
|
127
|
+
.preview-tab-label { overflow: hidden; text-overflow: ellipsis; min-width: 0; }
|
|
100
128
|
.preview-bar .url-bar { flex: 1; background: #0d1117; border: 1px solid #30363d; color: #c9d1d9; padding: 2px 8px; font-size: 11px; border-radius: 4px; font-family: monospace; }
|
|
101
129
|
.preview-bar button { background: none; border: none; color: #8b949e; cursor: pointer; font-size: 14px; padding: 2px 4px; }
|
|
102
130
|
.preview-bar button:hover { color: #c9d1d9; }
|
|
103
131
|
#preview-frame { flex: 1; border: none; background: white; width: 100%; }
|
|
132
|
+
#markdown-preview {
|
|
133
|
+
display: none; flex: 1; min-height: 0; overflow: auto;
|
|
134
|
+
background: #0d1117; color: #c9d1d9;
|
|
135
|
+
}
|
|
136
|
+
#markdown-preview.active { display: block; }
|
|
137
|
+
.markdown-body {
|
|
138
|
+
max-width: 860px; margin: 0 auto; padding: 34px 44px 56px;
|
|
139
|
+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif;
|
|
140
|
+
font-size: 14px; line-height: 1.65;
|
|
141
|
+
}
|
|
142
|
+
.markdown-body h1, .markdown-body h2, .markdown-body h3 {
|
|
143
|
+
color: #f0f6fc; line-height: 1.25; margin: 24px 0 12px;
|
|
144
|
+
border-bottom: 1px solid #30363d; padding-bottom: 8px;
|
|
145
|
+
}
|
|
146
|
+
.markdown-body h1 { font-size: 30px; margin-top: 0; }
|
|
147
|
+
.markdown-body h2 { font-size: 22px; }
|
|
148
|
+
.markdown-body h3 { font-size: 18px; border-bottom: 0; padding-bottom: 0; }
|
|
149
|
+
.markdown-body p, .markdown-body ul, .markdown-body ol, .markdown-body table,
|
|
150
|
+
.markdown-body pre, .markdown-body blockquote { margin: 0 0 16px; }
|
|
151
|
+
.markdown-body a { color: #58a6ff; text-decoration: none; }
|
|
152
|
+
.markdown-body a:hover { text-decoration: underline; }
|
|
153
|
+
.markdown-body code {
|
|
154
|
+
background: rgba(110, 118, 129, 0.28); color: #f0f6fc;
|
|
155
|
+
border-radius: 4px; padding: 0.15em 0.35em;
|
|
156
|
+
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
|
157
|
+
font-size: 0.92em;
|
|
158
|
+
}
|
|
159
|
+
.markdown-body pre {
|
|
160
|
+
background: #161b22; border: 1px solid #30363d; border-radius: 6px;
|
|
161
|
+
padding: 14px 16px; overflow: auto;
|
|
162
|
+
}
|
|
163
|
+
.markdown-body pre code { background: transparent; padding: 0; color: #c9d1d9; }
|
|
164
|
+
.markdown-body blockquote {
|
|
165
|
+
border-left: 3px solid #3fb950; color: #8b949e; padding: 4px 0 4px 14px;
|
|
166
|
+
}
|
|
167
|
+
.markdown-body table { border-collapse: collapse; width: 100%; display: block; overflow: auto; }
|
|
168
|
+
.markdown-body th, .markdown-body td {
|
|
169
|
+
border: 1px solid #30363d; padding: 7px 10px; text-align: left;
|
|
170
|
+
}
|
|
171
|
+
.markdown-body th { background: #161b22; color: #f0f6fc; }
|
|
172
|
+
.markdown-body tr:nth-child(2n) td { background: rgba(22, 27, 34, 0.5); }
|
|
173
|
+
.markdown-body hr { border: 0; border-top: 1px solid #30363d; margin: 24px 0; }
|
|
104
174
|
|
|
105
175
|
/* Footer */
|
|
106
176
|
.footer { padding: 3px 12px; background: #161b22; border-top: 1px solid #30363d; font-size: 11px; color: #8b949e; flex-shrink: 0; display: flex; justify-content: space-between; gap: 12px; }
|
|
@@ -109,26 +179,201 @@
|
|
|
109
179
|
.footer .stat-value { color: #c9d1d9; }
|
|
110
180
|
.footer .stat-value.good { color: #3fb950; }
|
|
111
181
|
|
|
112
|
-
/*
|
|
113
|
-
.
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
.
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
182
|
+
/* Agent chat */
|
|
183
|
+
.panel-agent {
|
|
184
|
+
display: none; flex: 1; min-width: 0; min-height: 0;
|
|
185
|
+
background: #0f141b; color: #d6dee8; flex-direction: column;
|
|
186
|
+
}
|
|
187
|
+
.agent-shell {
|
|
188
|
+
flex: 1; min-height: 0; display: grid;
|
|
189
|
+
grid-template-rows: auto 1fr auto;
|
|
190
|
+
width: 100%;
|
|
191
|
+
background:
|
|
192
|
+
linear-gradient(180deg, rgba(88,166,255,0.04), rgba(63,185,80,0.015) 28%, transparent 48%),
|
|
193
|
+
#0f141b;
|
|
194
|
+
}
|
|
195
|
+
.agent-top {
|
|
196
|
+
display: flex; align-items: center; gap: 8px;
|
|
197
|
+
padding: 7px 12px; background: rgba(22,27,34,0.96);
|
|
198
|
+
border-bottom: 1px solid #30363d; min-height: 40px;
|
|
199
|
+
}
|
|
200
|
+
.agent-heading { display: flex; align-items: baseline; gap: 8px; min-width: 0; }
|
|
201
|
+
.agent-title { font-size: 12px; font-weight: 650; color: #f0f6fc; white-space: nowrap; }
|
|
202
|
+
.agent-subtitle {
|
|
203
|
+
color: #8b949e; font-size: 11px; overflow: hidden; text-overflow: ellipsis;
|
|
204
|
+
white-space: nowrap; max-width: 220px;
|
|
205
|
+
}
|
|
206
|
+
.agent-status {
|
|
207
|
+
margin-left: auto; font-size: 11px; color: #8b949e;
|
|
208
|
+
font-family: ui-monospace, Menlo, monospace; display: inline-flex;
|
|
209
|
+
align-items: center; gap: 6px; min-width: 0; white-space: nowrap;
|
|
210
|
+
}
|
|
211
|
+
.agent-status::before {
|
|
212
|
+
content: ''; width: 6px; height: 6px; border-radius: 50%;
|
|
213
|
+
background: #6e7681; box-shadow: 0 0 0 1px rgba(255,255,255,0.05);
|
|
214
|
+
}
|
|
215
|
+
.agent-status.ready { color: #3fb950; }
|
|
216
|
+
.agent-status.ready::before { background: #3fb950; box-shadow: 0 0 8px rgba(63,185,80,0.55); }
|
|
217
|
+
.agent-status.warn { color: #d29922; }
|
|
218
|
+
.agent-status.warn::before { background: #d29922; box-shadow: 0 0 8px rgba(210,153,34,0.45); }
|
|
219
|
+
.agent-status.streaming { color: #58a6ff; }
|
|
220
|
+
.agent-status.streaming::before { background: #58a6ff; animation: pulse 1.1s ease-in-out infinite; }
|
|
221
|
+
.agent-top button,
|
|
222
|
+
.agent-composer button {
|
|
223
|
+
background: #21262d; border: 1px solid #30363d; color: #c9d1d9;
|
|
224
|
+
padding: 4px 10px; border-radius: 6px; cursor: pointer; font-size: 11px;
|
|
225
|
+
}
|
|
226
|
+
.agent-top button:hover,
|
|
227
|
+
.agent-composer button:hover { background: #30363d; }
|
|
228
|
+
.agent-top button:disabled,
|
|
229
|
+
.agent-composer button:disabled { opacity: 0.55; cursor: not-allowed; }
|
|
230
|
+
.agent-top .agent-secondary { color: #8b949e; }
|
|
231
|
+
.agent-top select {
|
|
232
|
+
background: #0d1117; border: 1px solid #30363d; color: #c9d1d9;
|
|
233
|
+
padding: 4px 8px; border-radius: 6px; font-size: 11px;
|
|
234
|
+
max-width: 260px;
|
|
235
|
+
}
|
|
236
|
+
.agent-messages {
|
|
237
|
+
min-height: 0; overflow: auto; padding: 22px 18px 26px;
|
|
238
|
+
display: flex; flex-direction: column; gap: 16px;
|
|
239
|
+
scrollbar-width: thin; scrollbar-color: #30363d transparent;
|
|
240
|
+
}
|
|
241
|
+
.agent-empty {
|
|
242
|
+
margin: auto; max-width: 560px; color: #8b949e; text-align: center;
|
|
243
|
+
font-size: 13px; line-height: 1.6; padding: 28px;
|
|
244
|
+
border: 1px dashed #30363d; border-radius: 8px;
|
|
245
|
+
background: rgba(22,27,34,0.55);
|
|
246
|
+
}
|
|
247
|
+
.agent-msg {
|
|
248
|
+
width: min(900px, 100%); margin: 0 auto;
|
|
249
|
+
display: flex; gap: 10px; align-items: flex-start;
|
|
250
|
+
}
|
|
251
|
+
.agent-msg.user { justify-content: flex-end; }
|
|
252
|
+
.agent-avatar {
|
|
253
|
+
width: 24px; height: 24px; border-radius: 6px; flex: 0 0 auto;
|
|
254
|
+
display: grid; place-items: center; margin-top: 2px;
|
|
255
|
+
border: 1px solid #30363d; background: #161b22;
|
|
256
|
+
color: #8b949e; font: 10px ui-monospace, Menlo, monospace;
|
|
257
|
+
}
|
|
258
|
+
.agent-msg.assistant .agent-avatar { color: #7ee787; border-color: rgba(63,185,80,0.35); background: rgba(63,185,80,0.08); }
|
|
259
|
+
.agent-msg.user .agent-avatar { display: none; }
|
|
260
|
+
.agent-content {
|
|
261
|
+
min-width: 0; max-width: min(760px, 86%); display: flex; flex-direction: column; gap: 8px;
|
|
262
|
+
}
|
|
263
|
+
.agent-msg.user .agent-content { align-items: flex-end; }
|
|
264
|
+
.agent-meta {
|
|
265
|
+
color: #6e7681; font: 10px ui-monospace, Menlo, monospace;
|
|
266
|
+
text-transform: uppercase; letter-spacing: 0.02em;
|
|
267
|
+
}
|
|
268
|
+
.agent-bubble {
|
|
269
|
+
border-radius: 8px; padding: 10px 12px; overflow-wrap: anywhere;
|
|
270
|
+
font-size: 13px; line-height: 1.58; color: #d6dee8;
|
|
271
|
+
}
|
|
272
|
+
.agent-msg.assistant .agent-bubble {
|
|
273
|
+
background: rgba(22,27,34,0.78); border: 1px solid rgba(48,54,61,0.85);
|
|
274
|
+
}
|
|
275
|
+
.agent-msg.user .agent-bubble {
|
|
276
|
+
background: linear-gradient(180deg, rgba(31,111,235,0.24), rgba(31,111,235,0.15));
|
|
277
|
+
border: 1px solid rgba(88,166,255,0.65); color: #f0f6fc;
|
|
278
|
+
max-width: min(680px, 76%);
|
|
279
|
+
}
|
|
280
|
+
.agent-bubble p { margin: 0 0 10px; }
|
|
281
|
+
.agent-bubble p:last-child { margin-bottom: 0; }
|
|
282
|
+
.agent-bubble ul, .agent-bubble ol { margin: 6px 0 10px 18px; }
|
|
283
|
+
.agent-bubble li { margin: 3px 0; }
|
|
284
|
+
.agent-bubble code {
|
|
285
|
+
background: rgba(110,118,129,0.25); border-radius: 4px;
|
|
286
|
+
padding: 0.12em 0.35em; color: #f0f6fc;
|
|
287
|
+
font-family: ui-monospace, Menlo, Consolas, monospace; font-size: 0.92em;
|
|
288
|
+
}
|
|
289
|
+
.agent-bubble pre {
|
|
290
|
+
margin: 8px 0; padding: 10px 12px; border-radius: 8px;
|
|
291
|
+
border: 1px solid #30363d; background: #0d1117; overflow: auto;
|
|
292
|
+
white-space: pre;
|
|
293
|
+
}
|
|
294
|
+
.agent-bubble pre code { background: transparent; padding: 0; }
|
|
295
|
+
.agent-thinking {
|
|
296
|
+
display: inline-flex; align-items: center; gap: 7px;
|
|
297
|
+
color: #8b949e; font-size: 12px; padding: 4px 0;
|
|
298
|
+
}
|
|
299
|
+
.agent-thinking .dots { display: inline-flex; gap: 4px; }
|
|
300
|
+
.agent-thinking .dots span {
|
|
301
|
+
width: 5px; height: 5px; border-radius: 50%; background: #58a6ff;
|
|
302
|
+
animation: agent-dot 1s ease-in-out infinite;
|
|
303
|
+
}
|
|
304
|
+
.agent-thinking .dots span:nth-child(2) { animation-delay: 120ms; }
|
|
305
|
+
.agent-thinking .dots span:nth-child(3) { animation-delay: 240ms; }
|
|
306
|
+
@keyframes agent-dot { 0%, 70%, 100% { opacity: 0.32; transform: translateY(0); } 35% { opacity: 1; transform: translateY(-2px); } }
|
|
307
|
+
.agent-cursor {
|
|
308
|
+
display: inline-block; width: 2px; height: 1em; margin-left: 2px;
|
|
309
|
+
background: #58a6ff; vertical-align: -0.15em; animation: agent-cursor 1s steps(1) infinite;
|
|
310
|
+
}
|
|
311
|
+
@keyframes agent-cursor { 0%, 45% { opacity: 1; } 46%, 100% { opacity: 0; } }
|
|
312
|
+
.agent-reasoning {
|
|
313
|
+
border-left: 2px solid rgba(88,166,255,0.65); padding: 7px 10px;
|
|
314
|
+
background: rgba(88,166,255,0.06); color: #8b949e; border-radius: 0 8px 8px 0;
|
|
315
|
+
font-size: 12px; line-height: 1.5;
|
|
316
|
+
}
|
|
317
|
+
.agent-reasoning summary { cursor: pointer; color: #79c0ff; font-weight: 600; }
|
|
318
|
+
.agent-reasoning pre { margin-top: 7px; white-space: pre-wrap; font: 12px/1.5 ui-monospace, Menlo, monospace; color: #c9d1d9; }
|
|
319
|
+
.agent-tools { display: flex; flex-direction: column; gap: 7px; }
|
|
320
|
+
.agent-tool {
|
|
321
|
+
border: 1px solid #30363d; background: rgba(13,17,23,0.72);
|
|
322
|
+
border-radius: 8px; overflow: hidden;
|
|
323
|
+
}
|
|
324
|
+
.agent-tool summary {
|
|
325
|
+
display: flex; align-items: center; gap: 8px; cursor: pointer;
|
|
326
|
+
padding: 7px 9px; list-style: none; user-select: none;
|
|
327
|
+
}
|
|
328
|
+
.agent-tool summary::-webkit-details-marker { display: none; }
|
|
329
|
+
.agent-tool .tool-dot {
|
|
330
|
+
width: 7px; height: 7px; border-radius: 50%; background: #6e7681; flex: 0 0 auto;
|
|
331
|
+
}
|
|
332
|
+
.agent-tool.running .tool-dot { background: #58a6ff; box-shadow: 0 0 8px rgba(88,166,255,0.7); animation: pulse 1s ease-in-out infinite; }
|
|
333
|
+
.agent-tool.done .tool-dot { background: #3fb950; }
|
|
334
|
+
.agent-tool.error .tool-dot { background: #ff7b72; }
|
|
335
|
+
.agent-tool .tool-name { font: 12px ui-monospace, Menlo, monospace; color: #f0f6fc; }
|
|
336
|
+
.agent-tool .tool-state { margin-left: auto; color: #8b949e; font-size: 11px; }
|
|
337
|
+
.agent-tool.running .tool-state { color: #58a6ff; }
|
|
338
|
+
.agent-tool.done .tool-state { color: #3fb950; }
|
|
339
|
+
.agent-tool.error .tool-state { color: #ff7b72; }
|
|
340
|
+
.agent-tool-body {
|
|
341
|
+
border-top: 1px solid #30363d; padding: 8px 9px; display: grid;
|
|
342
|
+
gap: 8px; background: rgba(22,27,34,0.55);
|
|
343
|
+
}
|
|
344
|
+
.agent-tool-body label {
|
|
345
|
+
display: block; color: #6e7681; font-size: 10px; text-transform: uppercase;
|
|
346
|
+
margin-bottom: 4px; font-family: ui-monospace, Menlo, monospace;
|
|
347
|
+
}
|
|
348
|
+
.agent-tool-body pre {
|
|
349
|
+
margin: 0; max-height: 190px; overflow: auto; white-space: pre-wrap;
|
|
350
|
+
background: #0d1117; border: 1px solid #30363d; border-radius: 6px;
|
|
351
|
+
padding: 8px; color: #c9d1d9; font: 11px/1.5 ui-monospace, Menlo, monospace;
|
|
352
|
+
}
|
|
353
|
+
.agent-composer {
|
|
354
|
+
display: grid; grid-template-columns: 1fr auto; gap: 8px;
|
|
355
|
+
padding: 12px 14px; background: rgba(22,27,34,0.96);
|
|
356
|
+
border-top: 1px solid #30363d;
|
|
357
|
+
}
|
|
358
|
+
.agent-composer textarea {
|
|
359
|
+
resize: none; min-height: 44px; max-height: 170px; background: #0d1117;
|
|
360
|
+
border: 1px solid #30363d; color: #f0f6fc; border-radius: 8px;
|
|
361
|
+
padding: 10px 12px; font: 13px/1.45 -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; outline: none;
|
|
362
|
+
}
|
|
363
|
+
.agent-composer textarea:focus {
|
|
364
|
+
border-color: #58a6ff; box-shadow: 0 0 0 2px rgba(88,166,255,0.16);
|
|
365
|
+
}
|
|
366
|
+
.agent-composer button { min-width: 76px; font-weight: 650; }
|
|
367
|
+
.agent-composer button.streaming { border-color: #ff7b72; color: #ff7b72; }
|
|
368
|
+
|
|
369
|
+
/* Editor workspace:
|
|
123
370
|
FILE TREE | editor-stack (editor TOP + terminal BOTTOM) | PREVIEW
|
|
124
371
|
|
|
125
|
-
Editor
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
into .main). In editor mode the stack flips to flex column so
|
|
129
|
-
editor (top) + terminal (bottom) stack vertically. */
|
|
372
|
+
Editor, Agent, and terminal live inside .panel-left-stack. The top
|
|
373
|
+
surface switches between the editor and chat while the terminal and
|
|
374
|
+
preview stay visible. */
|
|
130
375
|
|
|
131
|
-
/* Editor panel — hidden by default; shown by
|
|
376
|
+
/* Editor panel — hidden by default; shown by workspace rules. */
|
|
132
377
|
.panel-editor {
|
|
133
378
|
flex: 1; display: none; flex-direction: column;
|
|
134
379
|
background: #1e1e1e; /* VSCode dark editor background */
|
|
@@ -136,12 +381,11 @@
|
|
|
136
381
|
}
|
|
137
382
|
.panel-editor.active { display: flex; }
|
|
138
383
|
|
|
139
|
-
/* Wrapper for the
|
|
140
|
-
modes (transparent). flex column in editor mode. */
|
|
384
|
+
/* Wrapper for the center workspace column. */
|
|
141
385
|
.panel-left-stack { display: contents; }
|
|
142
386
|
|
|
143
387
|
/* File-tree sidebar. Lives as a FIRST child of
|
|
144
|
-
.main; visible
|
|
388
|
+
.main; visible in the editor workspace. ~250px default width,
|
|
145
389
|
resizable via .tree-resize-handle. */
|
|
146
390
|
.panel-tree {
|
|
147
391
|
display: none; flex-direction: column;
|
|
@@ -241,6 +485,11 @@
|
|
|
241
485
|
.main.editor .panel-terminal {
|
|
242
486
|
display: flex; flex: 0 0 40%; min-height: 80px;
|
|
243
487
|
}
|
|
488
|
+
.main.editor .panel-agent { display: none; }
|
|
489
|
+
.main.editor .panel-left-stack.agent-surface .panel-editor { display: none; }
|
|
490
|
+
.main.editor .panel-left-stack.agent-surface .panel-agent {
|
|
491
|
+
display: flex; flex: 1 1 60%; min-height: 100px;
|
|
492
|
+
}
|
|
244
493
|
.main.editor .vresize-handle { display: block; }
|
|
245
494
|
.main.editor .resize-handle { display: block; }
|
|
246
495
|
.main.editor .panel-preview { display: flex; flex: 1; min-width: 0; }
|
|
@@ -291,13 +540,9 @@
|
|
|
291
540
|
</div>
|
|
292
541
|
</div>
|
|
293
542
|
<div class="toolbar" id="toolbar">
|
|
294
|
-
|
|
295
|
-
<button id="
|
|
296
|
-
<button id="
|
|
297
|
-
<!-- Single canonical editor mode:
|
|
298
|
-
file tree | editor (top) + terminal (bottom) | preview.
|
|
299
|
-
Lazy-loads Monaco + the tree on first click. -->
|
|
300
|
-
<button id="btnEditor" title="Editor (file tree + Monaco + terminal + preview)">Editor</button>
|
|
543
|
+
<!-- Single workspace: file tree | editor/agent + terminal | preview. -->
|
|
544
|
+
<button id="btnEditor" class="active" title="Editor">Editor</button>
|
|
545
|
+
<button id="btnAgent" title="Agent chat">Agent</button>
|
|
301
546
|
<div class="sep"></div>
|
|
302
547
|
<button id="btnProcs" title="Toggle process logs panel">
|
|
303
548
|
<span class="procs-dot"></span>
|
|
@@ -308,9 +553,8 @@
|
|
|
308
553
|
<span class="vite-status" id="viteStatus"></span>
|
|
309
554
|
</div>
|
|
310
555
|
<div class="main" id="mainPanel">
|
|
311
|
-
<!-- File-tree sidebar. Visible
|
|
312
|
-
|
|
313
|
-
The .tree-resize-handle sits between tree and editor stack;
|
|
556
|
+
<!-- File-tree sidebar. Visible in the workspace.
|
|
557
|
+
The .tree-resize-handle sits between tree and the center stack;
|
|
314
558
|
dragging it adjusts .panel-tree's width inline. -->
|
|
315
559
|
<div class="panel-tree" id="treePanel">
|
|
316
560
|
<div class="tree-toolbar">
|
|
@@ -325,11 +569,7 @@
|
|
|
325
569
|
<div class="tree-body" id="treeBody"></div>
|
|
326
570
|
</div>
|
|
327
571
|
<div class="tree-resize-handle" id="treeResizeHandle"></div>
|
|
328
|
-
<!-- The .panel-left-stack wrapper is
|
|
329
|
-
(terminal-only, split, preview-only) — its children act as
|
|
330
|
-
direct flex children of .main. In editor mode, CSS flips it to
|
|
331
|
-
display:flex so editor and terminal stack vertically in the middle
|
|
332
|
-
column. -->
|
|
572
|
+
<!-- The .panel-left-stack wrapper is the middle workspace column. -->
|
|
333
573
|
<div class="panel-left-stack" id="leftStack">
|
|
334
574
|
<div class="panel-editor" id="editorPanel">
|
|
335
575
|
<div class="editor-bar">
|
|
@@ -352,9 +592,29 @@
|
|
|
352
592
|
<span id="editorStatusEnc">UTF-8</span>
|
|
353
593
|
</div>
|
|
354
594
|
</div>
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
595
|
+
<div class="panel-agent" id="agentPanel">
|
|
596
|
+
<div class="agent-shell">
|
|
597
|
+
<div class="agent-top">
|
|
598
|
+
<div class="agent-heading">
|
|
599
|
+
<div class="agent-title">Nimbus Agent</div>
|
|
600
|
+
<div class="agent-subtitle" id="agentSubtitle">Workspace operator</div>
|
|
601
|
+
</div>
|
|
602
|
+
<select id="agentAccount" title="Cloudflare account" style="display:none"></select>
|
|
603
|
+
<button id="agentConnect" style="display:none">Connect Cloudflare</button>
|
|
604
|
+
<button id="agentDisconnect" class="agent-secondary" style="display:none">Disconnect</button>
|
|
605
|
+
<button id="agentClear" class="agent-secondary" title="Clear chat">Clear</button>
|
|
606
|
+
<div class="agent-status" id="agentStatus">Checking...</div>
|
|
607
|
+
</div>
|
|
608
|
+
<div class="agent-messages" id="agentMessages">
|
|
609
|
+
<div class="agent-empty" id="agentEmpty">Cloudflare connection required.</div>
|
|
610
|
+
</div>
|
|
611
|
+
<form class="agent-composer" id="agentForm">
|
|
612
|
+
<textarea id="agentInput" rows="2" placeholder="Message Nimbus"></textarea>
|
|
613
|
+
<button id="agentSend" type="submit">Send</button>
|
|
614
|
+
</form>
|
|
615
|
+
</div>
|
|
616
|
+
</div>
|
|
617
|
+
<!-- Vertical resize handle between the top surface and terminal. -->
|
|
358
618
|
<div class="vresize-handle" id="editorTerminalResizeHandle"></div>
|
|
359
619
|
<div class="panel-terminal">
|
|
360
620
|
<div id="terminal-container"></div>
|
|
@@ -367,11 +627,14 @@
|
|
|
367
627
|
<div class="resize-handle" id="resizeHandle"></div>
|
|
368
628
|
<div class="panel-preview" id="previewPanel">
|
|
369
629
|
<div class="preview-bar">
|
|
370
|
-
<
|
|
630
|
+
<div class="preview-tabs" id="previewTabs" aria-label="Preview tabs"></div>
|
|
371
631
|
<input class="url-bar" id="urlBar" value="" readonly>
|
|
372
632
|
<button id="btnReloadPreview" title="Reload">↻</button>
|
|
373
633
|
</div>
|
|
374
634
|
<iframe id="preview-frame" src="about:blank"></iframe>
|
|
635
|
+
<div id="markdown-preview" aria-label="Markdown preview">
|
|
636
|
+
<article class="markdown-body" id="markdown-preview-body"></article>
|
|
637
|
+
</div>
|
|
375
638
|
</div>
|
|
376
639
|
</div>
|
|
377
640
|
<!-- Ctrl+P command palette. Lazy-populated fuzzy file finder; fs-list
|
|
@@ -409,6 +672,11 @@
|
|
|
409
672
|
const STATS_URL = SESSION_PREFIX + '/api/stats';
|
|
410
673
|
const VITE_URL = SESSION_PREFIX + '/preview/';
|
|
411
674
|
const WORKER_URL = SESSION_PREFIX + '/worker/';
|
|
675
|
+
const AGENT_STATUS_URL = SESSION_PREFIX + '/api/agent/status';
|
|
676
|
+
const AGENT_MESSAGES_URL = SESSION_PREFIX + '/api/agent/messages';
|
|
677
|
+
const AGENT_OAUTH_START_URL = SESSION_PREFIX + '/api/agent/oauth/start';
|
|
678
|
+
const AGENT_OAUTH_LOGOUT_URL = SESSION_PREFIX + '/api/agent/oauth/logout';
|
|
679
|
+
const AGENT_ACCOUNT_URL = SESSION_PREFIX + '/api/agent/account';
|
|
412
680
|
// The iframe starts pointing at /preview/ which handles its own
|
|
413
681
|
// "no dev server running" placeholder. When the user spawns a
|
|
414
682
|
// wrangler dev (via `wrangler dev` in the terminal), stats polling
|
|
@@ -475,63 +743,67 @@
|
|
|
475
743
|
const stText = document.getElementById('statusText');
|
|
476
744
|
const previewFrame = document.getElementById('preview-frame');
|
|
477
745
|
const previewPanel = document.getElementById('previewPanel');
|
|
746
|
+
const previewTabsEl = document.getElementById('previewTabs');
|
|
747
|
+
const markdownPreview = document.getElementById('markdown-preview');
|
|
748
|
+
const markdownPreviewBody = document.getElementById('markdown-preview-body');
|
|
478
749
|
const mainPanel = document.getElementById('mainPanel');
|
|
479
750
|
const viteStatus = document.getElementById('viteStatus');
|
|
480
751
|
let ws = null, rd = 1000, statsTimer = null;
|
|
481
|
-
//
|
|
482
|
-
|
|
483
|
-
let
|
|
752
|
+
// The app has one workspace layout. Only the top center surface switches.
|
|
753
|
+
let layout = 'editor';
|
|
754
|
+
let workSurface = 'editor';
|
|
484
755
|
|
|
485
756
|
// ── Layout controls ──
|
|
486
757
|
function setLayout(mode) {
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
document.getElementById('
|
|
491
|
-
|
|
492
|
-
document.getElementById('btnEditor').classList.toggle('active',
|
|
493
|
-
|
|
758
|
+
workSurface = mode === 'agent' ? 'agent' : 'editor';
|
|
759
|
+
layout = 'editor';
|
|
760
|
+
mainPanel.className = 'main editor';
|
|
761
|
+
const leftStack = document.getElementById('leftStack');
|
|
762
|
+
leftStack.classList.toggle('agent-surface', workSurface === 'agent');
|
|
763
|
+
document.getElementById('btnEditor').classList.toggle('active', workSurface === 'editor');
|
|
764
|
+
document.getElementById('btnAgent').classList.toggle('active', workSurface === 'agent');
|
|
765
|
+
// Editor panel visibility — drives display:flex via .active unless
|
|
766
|
+
// the agent surface is currently selected.
|
|
494
767
|
const editorPanel = document.getElementById('editorPanel');
|
|
495
|
-
const wantEditor = (
|
|
768
|
+
const wantEditor = (workSurface === 'editor');
|
|
496
769
|
editorPanel.classList.toggle('active', wantEditor);
|
|
497
|
-
|
|
498
|
-
//
|
|
770
|
+
FileTree.ensureLoaded().catch(() => {});
|
|
771
|
+
// Lazy-init Monaco the first time the editor surface is selected.
|
|
499
772
|
if (wantEditor) {
|
|
500
|
-
Editor.ensureLoaded()
|
|
501
|
-
|
|
773
|
+
Editor.ensureLoaded()
|
|
774
|
+
.then(() => Editor.openDefaultWelcome())
|
|
775
|
+
.catch(() => {});
|
|
776
|
+
}
|
|
777
|
+
if (workSurface === 'agent') {
|
|
778
|
+
Agent.ensureLoaded().catch(() => {});
|
|
502
779
|
}
|
|
503
780
|
setTimeout(() => {
|
|
504
|
-
// Re-apply persisted pane dims for this
|
|
505
|
-
// PaneResizer uses inline flex which is mode-specific (split vs
|
|
506
|
-
// editor have different pane structures); onLayoutChange picks
|
|
507
|
-
// the right elements for the active mode.
|
|
781
|
+
// Re-apply persisted pane dims for this workspace.
|
|
508
782
|
try { if (typeof PaneResizer !== 'undefined') PaneResizer.onLayoutChange(); } catch {}
|
|
509
783
|
try { fitAddon.fit(); } catch {}
|
|
510
784
|
// Re-layout Monaco after the panel resizes. automaticLayout:true
|
|
511
|
-
// already handles this but explicit layout() on
|
|
785
|
+
// already handles this but explicit layout() on surface switch
|
|
512
786
|
// avoids a one-frame flash where the editor sees its old size.
|
|
513
787
|
if (window.__nimbusMonacoEditor) {
|
|
514
788
|
try { window.__nimbusMonacoEditor.layout(); } catch {}
|
|
515
789
|
}
|
|
516
790
|
}, 50);
|
|
517
791
|
}
|
|
518
|
-
document.getElementById('btnTerminal').onclick = () => setLayout('terminal-only');
|
|
519
|
-
document.getElementById('btnPreview').onclick = () => setLayout('preview-only');
|
|
520
|
-
document.getElementById('btnSplit').onclick = () => setLayout('split');
|
|
521
792
|
document.getElementById('btnEditor').onclick = () => setLayout('editor');
|
|
793
|
+
document.getElementById('btnAgent').onclick = () => setLayout('agent');
|
|
522
794
|
document.getElementById('btnRefresh').onclick = () => reloadPreview();
|
|
523
795
|
document.getElementById('btnReloadPreview').onclick = () => reloadPreview();
|
|
524
796
|
|
|
525
797
|
// ── Editor module ──
|
|
526
798
|
//
|
|
527
799
|
// Lazy-loads Monaco via cdnjs UMD on first call to ensureLoaded().
|
|
528
|
-
// Cold session (terminal-only flow) never pays the ~5MB download.
|
|
529
800
|
//
|
|
530
801
|
// Owns the open file, dirty state, Ctrl+P palette, Ctrl+S save.
|
|
531
802
|
// All fs operations route through the live WS via fs-read /
|
|
532
803
|
// fs-write / fs-list messages → init.ts handler → SqliteVFS.
|
|
533
804
|
//
|
|
534
|
-
// Perf-invariant: Monaco is
|
|
805
|
+
// Perf-invariant: Monaco is loaded by runtime appending, not by an eager
|
|
806
|
+
// <script src> in the served HTML.
|
|
535
807
|
const Editor = (function() {
|
|
536
808
|
// CDN choice: cdnjs hosts Monaco's UMD AMD-loader (`loader.min.js`)
|
|
537
809
|
// which fetches the remaining vs/* chunks on demand. Total cold
|
|
@@ -550,11 +822,13 @@
|
|
|
550
822
|
// race on the same path.
|
|
551
823
|
const pending = new Map();
|
|
552
824
|
let nextReqId = 1;
|
|
825
|
+
const DEFAULT_WELCOME_PATH = '/home/user/welcome.md';
|
|
553
826
|
|
|
554
827
|
// Current open file state.
|
|
555
828
|
let openPath = null; // POSIX absolute path or null
|
|
556
829
|
let originalContent = ''; // last-saved content (for dirty check)
|
|
557
830
|
let editor = null; // Monaco editor instance
|
|
831
|
+
let markdownUpdateTimer = null;
|
|
558
832
|
|
|
559
833
|
function tabEl() { return document.getElementById('editorTab'); }
|
|
560
834
|
function setTabFile(path, dirty) {
|
|
@@ -668,6 +942,20 @@
|
|
|
668
942
|
return map[ext] || 'plaintext';
|
|
669
943
|
}
|
|
670
944
|
|
|
945
|
+
function isMarkdownPath(path) {
|
|
946
|
+
return /\.(md|markdown|mdown|mkdn)$/i.test(path || '');
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
function scheduleMarkdownPreviewUpdate(content) {
|
|
950
|
+
if (!openPath || !isMarkdownPath(openPath)) return;
|
|
951
|
+
if (markdownUpdateTimer) clearTimeout(markdownUpdateTimer);
|
|
952
|
+
markdownUpdateTimer = setTimeout(() => {
|
|
953
|
+
showMarkdownPreview(openPath, content).catch((e) => {
|
|
954
|
+
setStatusFlash('markdown preview failed: ' + (e?.message || e));
|
|
955
|
+
});
|
|
956
|
+
}, 160);
|
|
957
|
+
}
|
|
958
|
+
|
|
671
959
|
function ensureLoaded() {
|
|
672
960
|
if (loaded) return Promise.resolve();
|
|
673
961
|
if (loading) return loading;
|
|
@@ -803,6 +1091,9 @@
|
|
|
803
1091
|
if (typeof FileTree !== 'undefined') {
|
|
804
1092
|
try { FileTree.markDirty(openPath, dirty); } catch {}
|
|
805
1093
|
}
|
|
1094
|
+
if (isMarkdownPath(openPath)) {
|
|
1095
|
+
scheduleMarkdownPreviewUpdate(cur);
|
|
1096
|
+
}
|
|
806
1097
|
});
|
|
807
1098
|
// Cursor position → status bar.
|
|
808
1099
|
const statusPos = document.getElementById('editorStatusPos');
|
|
@@ -823,7 +1114,7 @@
|
|
|
823
1114
|
setTimeout(() => editor.layout(), 0);
|
|
824
1115
|
}
|
|
825
1116
|
|
|
826
|
-
async function openFile(path) {
|
|
1117
|
+
async function openFile(path, options = {}) {
|
|
827
1118
|
if (!path) return;
|
|
828
1119
|
// Dirty-prompt before switching files. If the current buffer has
|
|
829
1120
|
// unsaved changes, ask the user whether to save or discard.
|
|
@@ -880,6 +1171,13 @@
|
|
|
880
1171
|
if (typeof FileTree !== 'undefined') {
|
|
881
1172
|
try { FileTree.setSelected(path); FileTree.markDirty(path, false); } catch {}
|
|
882
1173
|
}
|
|
1174
|
+
if (isMarkdownPath(path)) {
|
|
1175
|
+
showMarkdownPreview(path, content).catch((e) => {
|
|
1176
|
+
setStatusFlash('markdown preview failed: ' + (e?.message || e));
|
|
1177
|
+
});
|
|
1178
|
+
} else if (!options.keepMarkdownPreview) {
|
|
1179
|
+
showAppPreview();
|
|
1180
|
+
}
|
|
883
1181
|
editor.focus();
|
|
884
1182
|
}
|
|
885
1183
|
|
|
@@ -1027,7 +1325,21 @@
|
|
|
1027
1325
|
fileListCachedAt = 0;
|
|
1028
1326
|
}
|
|
1029
1327
|
|
|
1030
|
-
|
|
1328
|
+
function openDefaultWelcome() {
|
|
1329
|
+
if (openPath) return Promise.resolve();
|
|
1330
|
+
return openFile(DEFAULT_WELCOME_PATH);
|
|
1331
|
+
}
|
|
1332
|
+
|
|
1333
|
+
function refreshMarkdownPreview() {
|
|
1334
|
+
if (!openPath || !editor || !isMarkdownPath(openPath)) return Promise.resolve();
|
|
1335
|
+
return showMarkdownPreview(openPath, editor.getValue());
|
|
1336
|
+
}
|
|
1337
|
+
|
|
1338
|
+
return {
|
|
1339
|
+
ensureLoaded, openFile, save, openPalette, tryHandleFsResult,
|
|
1340
|
+
drainFsQueue, invalidateFileListCache, openDefaultWelcome,
|
|
1341
|
+
refreshMarkdownPreview,
|
|
1342
|
+
};
|
|
1031
1343
|
})();
|
|
1032
1344
|
|
|
1033
1345
|
// ── FileTree module ──
|
|
@@ -1593,56 +1905,947 @@
|
|
|
1593
1905
|
};
|
|
1594
1906
|
})();
|
|
1595
1907
|
|
|
1596
|
-
//
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1908
|
+
// ── Agent chat module ─────────────────────────────────────────────
|
|
1909
|
+
const Agent = (function() {
|
|
1910
|
+
const statusEl = document.getElementById('agentStatus');
|
|
1911
|
+
const subtitleEl = document.getElementById('agentSubtitle');
|
|
1912
|
+
const messagesEl = document.getElementById('agentMessages');
|
|
1913
|
+
const emptyEl = document.getElementById('agentEmpty');
|
|
1914
|
+
const form = document.getElementById('agentForm');
|
|
1915
|
+
const input = document.getElementById('agentInput');
|
|
1916
|
+
const sendBtn = document.getElementById('agentSend');
|
|
1917
|
+
const connectBtn = document.getElementById('agentConnect');
|
|
1918
|
+
const disconnectBtn = document.getElementById('agentDisconnect');
|
|
1919
|
+
const clearBtn = document.getElementById('agentClear');
|
|
1920
|
+
const accountSelect = document.getElementById('agentAccount');
|
|
1921
|
+
|
|
1922
|
+
let loaded = false;
|
|
1923
|
+
let status = null;
|
|
1924
|
+
let messages = [];
|
|
1925
|
+
let busy = false;
|
|
1926
|
+
let currentAbort = null;
|
|
1927
|
+
let oauthPoll = null;
|
|
1928
|
+
let renderSeq = 0;
|
|
1929
|
+
|
|
1930
|
+
function setAgentStatus(text, tone) {
|
|
1931
|
+
statusEl.textContent = text;
|
|
1932
|
+
statusEl.className = 'agent-status' + (tone ? ' ' + tone : '');
|
|
1933
|
+
}
|
|
1934
|
+
|
|
1935
|
+
function canStartChat() {
|
|
1936
|
+
return !!status && status.configured && status.connected;
|
|
1937
|
+
}
|
|
1938
|
+
|
|
1939
|
+
function applyControls() {
|
|
1940
|
+
const oauth = status?.oauth || {};
|
|
1941
|
+
const owner = status?.ownerToken || {};
|
|
1942
|
+
const connected = !!status?.connected;
|
|
1943
|
+
const canStart = canStartChat();
|
|
1944
|
+
connectBtn.style.display = oauth.configured && !oauth.connected ? 'inline-block' : 'none';
|
|
1945
|
+
disconnectBtn.style.display = oauth.connected ? 'inline-block' : 'none';
|
|
1946
|
+
accountSelect.style.display = oauth.connected && Array.isArray(oauth.accounts) && oauth.accounts.length > 1
|
|
1947
|
+
? 'inline-block'
|
|
1948
|
+
: 'none';
|
|
1949
|
+
input.disabled = !canStart || busy;
|
|
1950
|
+
sendBtn.disabled = !canStart || (!busy && !input.value.trim());
|
|
1951
|
+
sendBtn.textContent = busy ? 'Stop' : 'Send';
|
|
1952
|
+
sendBtn.classList.toggle('streaming', busy);
|
|
1953
|
+
if (!status) {
|
|
1954
|
+
emptyEl.textContent = 'Cloudflare connection required.';
|
|
1955
|
+
subtitleEl.textContent = 'Workspace operator';
|
|
1956
|
+
setAgentStatus('Checking...', '');
|
|
1957
|
+
} else if (!status.configured) {
|
|
1958
|
+
emptyEl.textContent = 'Configure Cloudflare OAuth or an owner API token to enable chat.';
|
|
1959
|
+
subtitleEl.textContent = 'Cloudflare Workers AI';
|
|
1960
|
+
setAgentStatus('AI not configured', 'warn');
|
|
1961
|
+
} else if (connected) {
|
|
1962
|
+
const mode = oauth.connected ? 'Cloudflare connected' : owner.configured ? 'Owner token' : 'Ready';
|
|
1963
|
+
emptyEl.textContent = 'Ask Nimbus to inspect or change this session.';
|
|
1964
|
+
subtitleEl.textContent = 'Shell · files · runtimes · previews';
|
|
1965
|
+
setAgentStatus((busy ? 'Streaming' : mode) + ' · ' + status.model, busy ? 'streaming' : 'ready');
|
|
1966
|
+
} else {
|
|
1967
|
+
emptyEl.textContent = 'Connect Cloudflare to use Workers AI.';
|
|
1968
|
+
subtitleEl.textContent = 'Use your Cloudflare quota';
|
|
1969
|
+
setAgentStatus('Connect Cloudflare', 'warn');
|
|
1970
|
+
}
|
|
1971
|
+
}
|
|
1972
|
+
|
|
1973
|
+
function renderAccounts() {
|
|
1974
|
+
const oauth = status?.oauth || {};
|
|
1975
|
+
const accounts = Array.isArray(oauth.accounts) ? oauth.accounts : [];
|
|
1976
|
+
accountSelect.replaceChildren();
|
|
1977
|
+
for (const account of accounts) {
|
|
1978
|
+
const opt = document.createElement('option');
|
|
1979
|
+
opt.value = account.id;
|
|
1980
|
+
opt.textContent = account.name || account.id;
|
|
1981
|
+
opt.selected = account.id === oauth.accountId;
|
|
1982
|
+
accountSelect.appendChild(opt);
|
|
1983
|
+
}
|
|
1984
|
+
}
|
|
1985
|
+
|
|
1986
|
+
function roleLabel(role, name) {
|
|
1987
|
+
if (role === 'tool') return name || 'tool';
|
|
1988
|
+
return role === 'assistant' ? 'Nimbus' : 'You';
|
|
1989
|
+
}
|
|
1990
|
+
|
|
1991
|
+
function formatTime(ts) {
|
|
1992
|
+
const date = new Date(ts || Date.now());
|
|
1993
|
+
if (Number.isNaN(date.getTime())) return '';
|
|
1994
|
+
return date.toLocaleTimeString([], { hour: 'numeric', minute: '2-digit' });
|
|
1995
|
+
}
|
|
1996
|
+
|
|
1997
|
+
function renderMessages(list = messages) {
|
|
1998
|
+
renderSeq++;
|
|
1999
|
+
const seq = renderSeq;
|
|
2000
|
+
const displayList = normalizeTranscriptForRender(list);
|
|
2001
|
+
messagesEl.replaceChildren();
|
|
2002
|
+
if (!displayList || displayList.length === 0) {
|
|
2003
|
+
messagesEl.appendChild(emptyEl);
|
|
2004
|
+
return;
|
|
2005
|
+
}
|
|
2006
|
+
for (const msg of displayList) {
|
|
2007
|
+
messagesEl.appendChild(renderMessage(msg, seq));
|
|
2008
|
+
}
|
|
2009
|
+
messagesEl.scrollTop = messagesEl.scrollHeight;
|
|
2010
|
+
}
|
|
2011
|
+
|
|
2012
|
+
function normalizeTranscriptForRender(list) {
|
|
2013
|
+
if (!Array.isArray(list)) return [];
|
|
2014
|
+
const out = [];
|
|
2015
|
+
let pendingTools = [];
|
|
2016
|
+
const flushPendingTools = () => {
|
|
2017
|
+
for (const tool of pendingTools) {
|
|
2018
|
+
out.push({ id: tool.toolCallId, role: 'assistant', content: '', createdAt: Date.now(), parts: [tool] });
|
|
2019
|
+
}
|
|
2020
|
+
pendingTools = [];
|
|
2021
|
+
};
|
|
2022
|
+
for (const msg of list) {
|
|
2023
|
+
if (msg?.role === 'tool') {
|
|
2024
|
+
pendingTools.push(legacyToolPart(msg));
|
|
2025
|
+
continue;
|
|
2026
|
+
}
|
|
2027
|
+
if (msg?.role === 'assistant' && pendingTools.length > 0) {
|
|
2028
|
+
const next = { ...msg, parts: [...pendingTools, ...normalizeTurnParts(msg)] };
|
|
2029
|
+
pendingTools = [];
|
|
2030
|
+
out.push(next);
|
|
2031
|
+
continue;
|
|
2032
|
+
}
|
|
2033
|
+
flushPendingTools();
|
|
2034
|
+
out.push(msg);
|
|
2035
|
+
}
|
|
2036
|
+
flushPendingTools();
|
|
2037
|
+
return out;
|
|
2038
|
+
}
|
|
2039
|
+
|
|
2040
|
+
function renderMessage(msg, seq) {
|
|
2041
|
+
if (msg.role === 'tool') return renderStoredToolMessage(msg);
|
|
2042
|
+
|
|
2043
|
+
const row = document.createElement('div');
|
|
2044
|
+
row.className = 'agent-msg ' + msg.role;
|
|
2045
|
+
const avatar = document.createElement('div');
|
|
2046
|
+
avatar.className = 'agent-avatar';
|
|
2047
|
+
avatar.textContent = msg.role === 'assistant' ? 'N' : '';
|
|
2048
|
+
const content = document.createElement('div');
|
|
2049
|
+
content.className = 'agent-content';
|
|
2050
|
+
|
|
2051
|
+
const meta = document.createElement('div');
|
|
2052
|
+
meta.className = 'agent-meta';
|
|
2053
|
+
meta.textContent = roleLabel(msg.role, msg.name) + (msg.createdAt ? ' · ' + formatTime(msg.createdAt) : '');
|
|
2054
|
+
|
|
2055
|
+
const isStreaming = !!msg.streaming;
|
|
2056
|
+
|
|
2057
|
+
if (msg.role === 'assistant') content.appendChild(meta);
|
|
2058
|
+
|
|
2059
|
+
if (msg.role === 'assistant') {
|
|
2060
|
+
const parts = normalizeTurnParts(msg);
|
|
2061
|
+
if (msg.error) {
|
|
2062
|
+
const bubble = document.createElement('div');
|
|
2063
|
+
bubble.className = 'agent-bubble';
|
|
2064
|
+
bubble.textContent = msg.error;
|
|
2065
|
+
content.appendChild(bubble);
|
|
2066
|
+
} else if (parts.length > 0) {
|
|
2067
|
+
renderTurnParts(content, parts, { streaming: isStreaming, seq });
|
|
2068
|
+
} else if (isStreaming) {
|
|
2069
|
+
content.appendChild(renderThinking('Thinking'));
|
|
2070
|
+
}
|
|
2071
|
+
} else if (msg.content || msg.error) {
|
|
2072
|
+
const bubble = document.createElement('div');
|
|
2073
|
+
bubble.className = 'agent-bubble';
|
|
2074
|
+
if (msg.error) {
|
|
2075
|
+
bubble.textContent = msg.error;
|
|
2076
|
+
} else {
|
|
2077
|
+
bubble.textContent = String(msg.content || '');
|
|
2078
|
+
}
|
|
2079
|
+
content.appendChild(bubble);
|
|
2080
|
+
}
|
|
2081
|
+
|
|
2082
|
+
if (msg.role === 'user') {
|
|
2083
|
+
content.appendChild(meta);
|
|
2084
|
+
row.append(content);
|
|
2085
|
+
} else {
|
|
2086
|
+
row.append(avatar, content);
|
|
2087
|
+
}
|
|
2088
|
+
return row;
|
|
2089
|
+
}
|
|
2090
|
+
|
|
2091
|
+
function normalizeTurnParts(msg) {
|
|
2092
|
+
if (Array.isArray(msg.parts)) return msg.parts.filter((part) => part && typeof part === 'object');
|
|
2093
|
+
const parts = [];
|
|
2094
|
+
if (msg.reasoning) parts.push({ type: 'reasoning', text: String(msg.reasoning) });
|
|
2095
|
+
if (Array.isArray(msg.tools)) {
|
|
2096
|
+
for (const tool of msg.tools) parts.push({ type: 'tool', ...tool });
|
|
2097
|
+
}
|
|
2098
|
+
if (msg.content) parts.push({ type: 'text', text: String(msg.content) });
|
|
2099
|
+
return parts;
|
|
2100
|
+
}
|
|
2101
|
+
|
|
2102
|
+
function renderTurnParts(content, parts, opts = {}) {
|
|
2103
|
+
const lastTextIndex = findLastTextPartIndex(parts);
|
|
2104
|
+
parts.forEach((part, index) => {
|
|
2105
|
+
if (part.type === 'reasoning') {
|
|
2106
|
+
if (part.text) content.appendChild(renderReasoning(part.text));
|
|
2107
|
+
} else if (part.type === 'tool') {
|
|
2108
|
+
content.appendChild(renderToolCard(part));
|
|
2109
|
+
} else if (part.type === 'text' && part.text) {
|
|
2110
|
+
const bubble = document.createElement('div');
|
|
2111
|
+
bubble.className = 'agent-bubble';
|
|
2112
|
+
renderMarkdownInto(bubble, part.text, {
|
|
2113
|
+
cursor: !!opts.streaming && index === lastTextIndex,
|
|
2114
|
+
seq: opts.seq,
|
|
2115
|
+
});
|
|
2116
|
+
content.appendChild(bubble);
|
|
2117
|
+
}
|
|
2118
|
+
});
|
|
2119
|
+
if (opts.streaming && (parts.length === 0 || shouldShowPostPartThinking(parts))) {
|
|
2120
|
+
content.appendChild(renderThinking('Thinking'));
|
|
2121
|
+
}
|
|
2122
|
+
}
|
|
2123
|
+
|
|
2124
|
+
function findLastTextPartIndex(parts) {
|
|
2125
|
+
for (let i = parts.length - 1; i >= 0; i--) {
|
|
2126
|
+
if (parts[i]?.type === 'text' && parts[i].text) return i;
|
|
2127
|
+
}
|
|
2128
|
+
return -1;
|
|
2129
|
+
}
|
|
2130
|
+
|
|
2131
|
+
function shouldShowPostPartThinking(parts) {
|
|
2132
|
+
const last = parts[parts.length - 1];
|
|
2133
|
+
return !!last && last.type === 'tool' && last.status !== 'running';
|
|
2134
|
+
}
|
|
2135
|
+
|
|
2136
|
+
function renderStoredToolMessage(msg) {
|
|
2137
|
+
const row = document.createElement('div');
|
|
2138
|
+
row.className = 'agent-msg assistant';
|
|
2139
|
+
const avatar = document.createElement('div');
|
|
2140
|
+
avatar.className = 'agent-avatar';
|
|
2141
|
+
avatar.textContent = 'T';
|
|
2142
|
+
const content = document.createElement('div');
|
|
2143
|
+
content.className = 'agent-content';
|
|
2144
|
+
content.appendChild(renderToolCard(legacyToolPart(msg)));
|
|
2145
|
+
row.append(avatar, content);
|
|
2146
|
+
return row;
|
|
2147
|
+
}
|
|
2148
|
+
|
|
2149
|
+
function legacyToolPart(msg) {
|
|
2150
|
+
const payload = parseToolPayload(msg);
|
|
2151
|
+
const output = payload.output ?? msg.content;
|
|
2152
|
+
return {
|
|
2153
|
+
type: 'tool',
|
|
2154
|
+
toolCallId: msg.id,
|
|
2155
|
+
toolName: payload.tool || msg.name || 'tool',
|
|
2156
|
+
input: payload.input,
|
|
2157
|
+
output,
|
|
2158
|
+
status: isToolOutputFailure(output) ? 'error' : 'done',
|
|
2159
|
+
durationMs: payload.durationMs,
|
|
2160
|
+
};
|
|
2161
|
+
}
|
|
2162
|
+
|
|
2163
|
+
function parseToolPayload(msg) {
|
|
2164
|
+
try {
|
|
2165
|
+
const parsed = JSON.parse(msg.content || '{}');
|
|
2166
|
+
if (parsed && typeof parsed === 'object') return parsed;
|
|
2167
|
+
} catch {}
|
|
2168
|
+
return { tool: msg.name || 'tool', output: msg.content || '' };
|
|
2169
|
+
}
|
|
2170
|
+
|
|
2171
|
+
function renderThinking(label) {
|
|
2172
|
+
const wrap = document.createElement('div');
|
|
2173
|
+
wrap.className = 'agent-thinking';
|
|
2174
|
+
const dots = document.createElement('span');
|
|
2175
|
+
dots.className = 'dots';
|
|
2176
|
+
dots.append(document.createElement('span'), document.createElement('span'), document.createElement('span'));
|
|
2177
|
+
const text = document.createElement('span');
|
|
2178
|
+
text.textContent = label;
|
|
2179
|
+
wrap.append(dots, text);
|
|
2180
|
+
return wrap;
|
|
2181
|
+
}
|
|
2182
|
+
|
|
2183
|
+
function renderReasoning(text) {
|
|
2184
|
+
const details = document.createElement('details');
|
|
2185
|
+
details.className = 'agent-reasoning';
|
|
2186
|
+
const summary = document.createElement('summary');
|
|
2187
|
+
summary.textContent = 'Thinking';
|
|
2188
|
+
const pre = document.createElement('pre');
|
|
2189
|
+
pre.textContent = text;
|
|
2190
|
+
details.append(summary, pre);
|
|
2191
|
+
return details;
|
|
2192
|
+
}
|
|
2193
|
+
|
|
2194
|
+
function renderToolCard(tool) {
|
|
2195
|
+
const status = tool.status || 'done';
|
|
2196
|
+
const details = document.createElement('details');
|
|
2197
|
+
details.className = 'agent-tool ' + status;
|
|
2198
|
+
if (status === 'running' || status === 'error') details.open = true;
|
|
2199
|
+
|
|
2200
|
+
const summary = document.createElement('summary');
|
|
2201
|
+
const dot = document.createElement('span');
|
|
2202
|
+
dot.className = 'tool-dot';
|
|
2203
|
+
const name = document.createElement('span');
|
|
2204
|
+
name.className = 'tool-name';
|
|
2205
|
+
name.textContent = tool.toolName || 'tool';
|
|
2206
|
+
const state = document.createElement('span');
|
|
2207
|
+
state.className = 'tool-state';
|
|
2208
|
+
state.textContent = toolStateLabel(tool);
|
|
2209
|
+
summary.append(dot, name, state);
|
|
2210
|
+
|
|
2211
|
+
const body = document.createElement('div');
|
|
2212
|
+
body.className = 'agent-tool-body';
|
|
2213
|
+
if (tool.input !== undefined) body.appendChild(renderToolSection('Input', tool.input));
|
|
2214
|
+
if (tool.output !== undefined) body.appendChild(renderToolSection(status === 'error' ? 'Error' : 'Output', tool.output));
|
|
2215
|
+
if (tool.error) body.appendChild(renderToolSection('Error', tool.error));
|
|
2216
|
+
details.append(summary, body);
|
|
2217
|
+
return details;
|
|
2218
|
+
}
|
|
2219
|
+
|
|
2220
|
+
function toolStateLabel(tool) {
|
|
2221
|
+
if (tool.status === 'running') return 'running';
|
|
2222
|
+
if (tool.status === 'error') return tool.durationMs ? 'failed · ' + formatDuration(tool.durationMs) : 'failed';
|
|
2223
|
+
return tool.durationMs ? 'done · ' + formatDuration(tool.durationMs) : 'done';
|
|
2224
|
+
}
|
|
2225
|
+
|
|
2226
|
+
function formatDuration(ms) {
|
|
2227
|
+
if (!Number.isFinite(ms) || ms < 100) return '<0.1s';
|
|
2228
|
+
return (ms / 1000).toFixed(1) + 's';
|
|
2229
|
+
}
|
|
2230
|
+
|
|
2231
|
+
function renderToolSection(label, value) {
|
|
2232
|
+
const section = document.createElement('div');
|
|
2233
|
+
const title = document.createElement('label');
|
|
2234
|
+
title.textContent = label;
|
|
2235
|
+
const pre = document.createElement('pre');
|
|
2236
|
+
pre.textContent = prettyValue(value);
|
|
2237
|
+
section.append(title, pre);
|
|
2238
|
+
return section;
|
|
2239
|
+
}
|
|
2240
|
+
|
|
2241
|
+
function prettyValue(value) {
|
|
2242
|
+
if (typeof value === 'string') return value;
|
|
2243
|
+
try { return JSON.stringify(value, null, 2); } catch { return String(value); }
|
|
2244
|
+
}
|
|
2245
|
+
|
|
2246
|
+
function isToolOutputFailure(output) {
|
|
2247
|
+
if (!output || typeof output !== 'object') return false;
|
|
2248
|
+
if (typeof output.error === 'string' && output.error.trim()) return true;
|
|
2249
|
+
if (output.success === false) return true;
|
|
2250
|
+
if (typeof output.exitCode === 'number' && output.exitCode !== 0) return true;
|
|
2251
|
+
if (output.exit && typeof output.exit === 'object' && typeof output.exit.code === 'number') {
|
|
2252
|
+
return output.exit.code !== 0;
|
|
2253
|
+
}
|
|
2254
|
+
return false;
|
|
2255
|
+
}
|
|
2256
|
+
|
|
2257
|
+
function renderMarkdownInto(el, text, opts = {}) {
|
|
2258
|
+
const token = String((opts.seq || renderSeq) + ':' + text.length + ':' + (opts.cursor ? '1' : '0'));
|
|
2259
|
+
el.dataset.renderToken = token;
|
|
2260
|
+
el.textContent = text;
|
|
2261
|
+
renderMarkdown(text).then((html) => {
|
|
2262
|
+
if (el.dataset.renderToken !== token) return;
|
|
2263
|
+
el.innerHTML = html;
|
|
2264
|
+
if (opts.cursor) {
|
|
2265
|
+
const cursor = document.createElement('span');
|
|
2266
|
+
cursor.className = 'agent-cursor';
|
|
2267
|
+
el.appendChild(cursor);
|
|
2268
|
+
}
|
|
2269
|
+
}).catch(() => {
|
|
2270
|
+
if (opts.cursor) {
|
|
2271
|
+
const cursor = document.createElement('span');
|
|
2272
|
+
cursor.className = 'agent-cursor';
|
|
2273
|
+
el.appendChild(cursor);
|
|
2274
|
+
}
|
|
2275
|
+
});
|
|
2276
|
+
}
|
|
2277
|
+
|
|
2278
|
+
async function refreshStatus() {
|
|
2279
|
+
const r = await fetch(AGENT_STATUS_URL, { headers: { Accept: 'application/json' } });
|
|
2280
|
+
const payload = await r.json();
|
|
2281
|
+
if (!r.ok) throw new Error(payload.error || 'Agent status failed');
|
|
2282
|
+
status = payload;
|
|
2283
|
+
renderAccounts();
|
|
2284
|
+
applyControls();
|
|
2285
|
+
return status;
|
|
2286
|
+
}
|
|
2287
|
+
|
|
2288
|
+
async function loadMessages() {
|
|
2289
|
+
const r = await fetch(AGENT_MESSAGES_URL, { headers: { Accept: 'application/json' } });
|
|
2290
|
+
const payload = await r.json();
|
|
2291
|
+
if (!r.ok) throw new Error(payload.error || 'Agent messages failed');
|
|
2292
|
+
messages = Array.isArray(payload.messages) ? payload.messages : [];
|
|
2293
|
+
renderMessages();
|
|
2294
|
+
}
|
|
2295
|
+
|
|
2296
|
+
async function connectCloudflare() {
|
|
2297
|
+
connectBtn.disabled = true;
|
|
2298
|
+
try {
|
|
2299
|
+
const r = await fetch(AGENT_OAUTH_START_URL, { method: 'POST', headers: { Accept: 'application/json' } });
|
|
2300
|
+
const payload = await r.json();
|
|
2301
|
+
if (!r.ok || !payload.authUrl) {
|
|
2302
|
+
throw new Error(payload.error || 'OAuth start failed');
|
|
2303
|
+
}
|
|
2304
|
+
const popup = window.open(payload.authUrl, 'nimbus-agent-oauth', 'width=720,height=760');
|
|
2305
|
+
if (!popup) location.href = payload.authUrl;
|
|
2306
|
+
setAgentStatus('Waiting for Cloudflare...', 'warn');
|
|
2307
|
+
if (oauthPoll) clearInterval(oauthPoll);
|
|
2308
|
+
oauthPoll = setInterval(() => {
|
|
2309
|
+
if (!popup || popup.closed) {
|
|
2310
|
+
clearInterval(oauthPoll);
|
|
2311
|
+
oauthPoll = null;
|
|
2312
|
+
refreshStatus().catch((e) => setAgentStatus(e?.message || String(e), 'warn'));
|
|
2313
|
+
}
|
|
2314
|
+
}, 1200);
|
|
2315
|
+
} catch (e) {
|
|
2316
|
+
setAgentStatus(e?.message || String(e), 'warn');
|
|
2317
|
+
} finally {
|
|
2318
|
+
connectBtn.disabled = false;
|
|
2319
|
+
}
|
|
2320
|
+
}
|
|
2321
|
+
|
|
2322
|
+
async function selectAccount() {
|
|
2323
|
+
const accountId = accountSelect.value;
|
|
2324
|
+
if (!accountId) return;
|
|
2325
|
+
await fetch(AGENT_ACCOUNT_URL, {
|
|
2326
|
+
method: 'POST',
|
|
2327
|
+
headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
|
|
2328
|
+
body: JSON.stringify({ accountId }),
|
|
2329
|
+
}).then(async (r) => {
|
|
2330
|
+
const payload = await r.json().catch(() => ({}));
|
|
2331
|
+
if (!r.ok) throw new Error(payload.error || 'Account switch failed');
|
|
2332
|
+
});
|
|
2333
|
+
await refreshStatus();
|
|
2334
|
+
}
|
|
2335
|
+
|
|
2336
|
+
async function disconnectCloudflare() {
|
|
2337
|
+
disconnectBtn.disabled = true;
|
|
2338
|
+
try {
|
|
2339
|
+
const r = await fetch(AGENT_OAUTH_LOGOUT_URL, { method: 'POST', headers: { Accept: 'application/json' } });
|
|
2340
|
+
const payload = await r.json().catch(() => ({}));
|
|
2341
|
+
if (!r.ok) throw new Error(payload.error || 'Disconnect failed');
|
|
2342
|
+
status = null;
|
|
2343
|
+
await refreshStatus();
|
|
2344
|
+
} catch (e) {
|
|
2345
|
+
setAgentStatus(e?.message || String(e), 'warn');
|
|
2346
|
+
} finally {
|
|
2347
|
+
disconnectBtn.disabled = false;
|
|
2348
|
+
}
|
|
2349
|
+
}
|
|
2350
|
+
|
|
2351
|
+
async function clearMessages() {
|
|
2352
|
+
const r = await fetch(AGENT_MESSAGES_URL, { method: 'DELETE', headers: { Accept: 'application/json' } });
|
|
2353
|
+
const payload = await r.json().catch(() => ({}));
|
|
2354
|
+
if (!r.ok) {
|
|
2355
|
+
setAgentStatus(payload.error || 'Clear failed', 'warn');
|
|
2356
|
+
return;
|
|
2357
|
+
}
|
|
2358
|
+
messages = [];
|
|
2359
|
+
renderMessages();
|
|
2360
|
+
}
|
|
2361
|
+
|
|
2362
|
+
async function sendMessage(text) {
|
|
2363
|
+
const content = String(text || '').trim();
|
|
2364
|
+
if (busy) {
|
|
2365
|
+
stopStreaming();
|
|
2366
|
+
return;
|
|
2367
|
+
}
|
|
2368
|
+
if (!content) return;
|
|
2369
|
+
busy = true;
|
|
2370
|
+
currentAbort = new AbortController();
|
|
2371
|
+
applyControls();
|
|
2372
|
+
input.value = '';
|
|
2373
|
+
const optimistic = {
|
|
2374
|
+
id: 'pending-user',
|
|
2375
|
+
role: 'user',
|
|
2376
|
+
content,
|
|
2377
|
+
createdAt: Date.now(),
|
|
2378
|
+
};
|
|
2379
|
+
const pending = {
|
|
2380
|
+
id: 'pending-assistant',
|
|
2381
|
+
role: 'assistant',
|
|
2382
|
+
content: '',
|
|
2383
|
+
createdAt: Date.now(),
|
|
2384
|
+
streaming: true,
|
|
2385
|
+
parts: [],
|
|
2386
|
+
};
|
|
2387
|
+
let liveList = [...messages, optimistic, pending];
|
|
2388
|
+
renderMessages(liveList);
|
|
2389
|
+
const repaint = () => {
|
|
2390
|
+
liveList = [...messages, pending];
|
|
2391
|
+
renderMessages(liveList);
|
|
2392
|
+
};
|
|
2393
|
+
try {
|
|
2394
|
+
const r = await fetch(AGENT_MESSAGES_URL, {
|
|
2395
|
+
method: 'POST',
|
|
2396
|
+
headers: { 'Content-Type': 'application/json', Accept: 'application/x-ndjson' },
|
|
2397
|
+
body: JSON.stringify({ message: content, stream: true }),
|
|
2398
|
+
signal: currentAbort.signal,
|
|
2399
|
+
});
|
|
2400
|
+
if (!r.ok || !r.body) {
|
|
2401
|
+
const payload = await r.json().catch(() => ({}));
|
|
2402
|
+
throw new Error(payload.error || 'Agent request failed');
|
|
2403
|
+
}
|
|
2404
|
+
await readAgentStream(r.body, pending, repaint);
|
|
2405
|
+
} catch (e) {
|
|
2406
|
+
if (e?.name !== 'AbortError') {
|
|
2407
|
+
pending.streaming = false;
|
|
2408
|
+
pending.error = e?.message || String(e);
|
|
2409
|
+
repaint();
|
|
2410
|
+
}
|
|
2411
|
+
} finally {
|
|
2412
|
+
busy = false;
|
|
2413
|
+
currentAbort = null;
|
|
2414
|
+
applyControls();
|
|
2415
|
+
input.focus();
|
|
2416
|
+
}
|
|
2417
|
+
}
|
|
2418
|
+
|
|
2419
|
+
function stopStreaming() {
|
|
2420
|
+
if (currentAbort) currentAbort.abort();
|
|
2421
|
+
}
|
|
2422
|
+
|
|
2423
|
+
async function readAgentStream(body, liveAssistant, repaint) {
|
|
2424
|
+
const reader = body.getReader();
|
|
2425
|
+
const decoder = new TextDecoder();
|
|
2426
|
+
let buffer = '';
|
|
2427
|
+
while (true) {
|
|
2428
|
+
const { value, done } = await reader.read();
|
|
2429
|
+
if (done) break;
|
|
2430
|
+
buffer += decoder.decode(value, { stream: true });
|
|
2431
|
+
let lineEnd = buffer.indexOf('\n');
|
|
2432
|
+
while (lineEnd >= 0) {
|
|
2433
|
+
const line = buffer.slice(0, lineEnd).trim();
|
|
2434
|
+
buffer = buffer.slice(lineEnd + 1);
|
|
2435
|
+
if (line) applyStreamEvent(JSON.parse(line), liveAssistant, repaint);
|
|
2436
|
+
lineEnd = buffer.indexOf('\n');
|
|
2437
|
+
}
|
|
2438
|
+
}
|
|
2439
|
+
const tail = buffer.trim();
|
|
2440
|
+
if (tail) applyStreamEvent(JSON.parse(tail), liveAssistant, repaint);
|
|
2441
|
+
}
|
|
2442
|
+
|
|
2443
|
+
function applyStreamEvent(event, liveAssistant, repaint) {
|
|
2444
|
+
if (!event || typeof event !== 'object') return;
|
|
2445
|
+
if (event.type === 'start') {
|
|
2446
|
+
if (Array.isArray(event.messages)) messages = event.messages;
|
|
2447
|
+
} else if (event.type === 'assistant-start') {
|
|
2448
|
+
liveAssistant.id = event.messageId || liveAssistant.id;
|
|
2449
|
+
liveAssistant.createdAt = event.createdAt || liveAssistant.createdAt;
|
|
2450
|
+
} else if (event.type === 'text-delta') {
|
|
2451
|
+
const delta = String(event.delta || '');
|
|
2452
|
+
liveAssistant.content += delta;
|
|
2453
|
+
appendLiveTextPart(liveAssistant, 'text', delta);
|
|
2454
|
+
} else if (event.type === 'reasoning-delta') {
|
|
2455
|
+
appendLiveTextPart(liveAssistant, 'reasoning', String(event.delta || ''));
|
|
2456
|
+
} else if (event.type === 'tool-call') {
|
|
2457
|
+
upsertLiveTool(liveAssistant, {
|
|
2458
|
+
toolCallId: event.toolCallId,
|
|
2459
|
+
toolName: event.toolName,
|
|
2460
|
+
input: event.input,
|
|
2461
|
+
status: 'running',
|
|
2462
|
+
startedAt: Date.now(),
|
|
2463
|
+
});
|
|
2464
|
+
} else if (event.type === 'tool-result' || event.type === 'tool-error') {
|
|
2465
|
+
const tool = upsertLiveTool(liveAssistant, {
|
|
2466
|
+
toolCallId: event.toolCallId,
|
|
2467
|
+
toolName: event.toolName,
|
|
2468
|
+
input: event.input,
|
|
2469
|
+
});
|
|
2470
|
+
tool.status = event.type === 'tool-error' ? 'error' : (event.status || 'done');
|
|
2471
|
+
tool.output = event.type === 'tool-error' ? { error: event.error } : event.output;
|
|
2472
|
+
tool.error = event.type === 'tool-error' ? event.error : '';
|
|
2473
|
+
if (tool.startedAt) tool.durationMs = Date.now() - tool.startedAt;
|
|
2474
|
+
} else if (event.type === 'done') {
|
|
2475
|
+
liveAssistant.streaming = false;
|
|
2476
|
+
if (Array.isArray(event.messages)) {
|
|
2477
|
+
messages = event.messages;
|
|
2478
|
+
renderMessages();
|
|
2479
|
+
return;
|
|
2480
|
+
}
|
|
2481
|
+
if (event.message) {
|
|
2482
|
+
messages = [...messages, event.message];
|
|
2483
|
+
}
|
|
2484
|
+
} else if (event.type === 'error') {
|
|
2485
|
+
liveAssistant.streaming = false;
|
|
2486
|
+
liveAssistant.error = event.error || 'Agent request failed';
|
|
2487
|
+
if (Array.isArray(event.messages)) messages = event.messages;
|
|
2488
|
+
setAgentStatus(liveAssistant.error, 'warn');
|
|
2489
|
+
}
|
|
2490
|
+
repaint();
|
|
2491
|
+
}
|
|
2492
|
+
|
|
2493
|
+
function appendLiveTextPart(liveAssistant, type, delta) {
|
|
2494
|
+
if (!delta) return;
|
|
2495
|
+
if (!Array.isArray(liveAssistant.parts)) liveAssistant.parts = [];
|
|
2496
|
+
const last = liveAssistant.parts[liveAssistant.parts.length - 1];
|
|
2497
|
+
if (last && last.type === type) {
|
|
2498
|
+
last.text = String(last.text || '') + delta;
|
|
2499
|
+
} else {
|
|
2500
|
+
liveAssistant.parts.push({ type, text: delta });
|
|
2501
|
+
}
|
|
2502
|
+
}
|
|
2503
|
+
|
|
2504
|
+
function upsertLiveTool(liveAssistant, patch) {
|
|
2505
|
+
if (!Array.isArray(liveAssistant.parts)) liveAssistant.parts = [];
|
|
2506
|
+
let tool = liveAssistant.parts.find((item) => item.type === 'tool' && item.toolCallId === patch.toolCallId);
|
|
2507
|
+
if (!tool) {
|
|
2508
|
+
tool = { type: 'tool', status: 'running' };
|
|
2509
|
+
liveAssistant.parts.push(tool);
|
|
2510
|
+
}
|
|
2511
|
+
Object.assign(tool, patch);
|
|
2512
|
+
return tool;
|
|
2513
|
+
}
|
|
2514
|
+
|
|
2515
|
+
function wire() {
|
|
2516
|
+
form.addEventListener('submit', (e) => {
|
|
2517
|
+
e.preventDefault();
|
|
2518
|
+
if (busy) stopStreaming();
|
|
2519
|
+
else sendMessage(input.value);
|
|
2520
|
+
});
|
|
2521
|
+
input.addEventListener('keydown', (e) => {
|
|
2522
|
+
if (e.key === 'Enter' && !e.shiftKey) {
|
|
2523
|
+
e.preventDefault();
|
|
2524
|
+
sendMessage(input.value);
|
|
2525
|
+
}
|
|
2526
|
+
});
|
|
2527
|
+
input.addEventListener('input', applyControls);
|
|
2528
|
+
connectBtn.addEventListener('click', connectCloudflare);
|
|
2529
|
+
disconnectBtn.addEventListener('click', disconnectCloudflare);
|
|
2530
|
+
clearBtn.addEventListener('click', clearMessages);
|
|
2531
|
+
accountSelect.addEventListener('change', selectAccount);
|
|
2532
|
+
window.addEventListener('message', (event) => {
|
|
2533
|
+
if (event.origin !== location.origin) return;
|
|
2534
|
+
if (event.data && event.data.type === 'nimbus-agent-oauth') {
|
|
2535
|
+
if (oauthPoll) {
|
|
2536
|
+
clearInterval(oauthPoll);
|
|
2537
|
+
oauthPoll = null;
|
|
2538
|
+
}
|
|
2539
|
+
refreshStatus().catch(() => {});
|
|
2540
|
+
}
|
|
2541
|
+
});
|
|
2542
|
+
}
|
|
2543
|
+
|
|
2544
|
+
async function ensureLoaded() {
|
|
2545
|
+
if (!loaded) {
|
|
2546
|
+
loaded = true;
|
|
2547
|
+
wire();
|
|
2548
|
+
}
|
|
2549
|
+
const results = await Promise.allSettled([refreshStatus(), loadMessages()]);
|
|
2550
|
+
if (results[0].status === 'rejected') {
|
|
2551
|
+
setAgentStatus(results[0].reason?.message || String(results[0].reason), 'warn');
|
|
2552
|
+
input.disabled = true;
|
|
2553
|
+
sendBtn.disabled = true;
|
|
2554
|
+
}
|
|
2555
|
+
if (results[1].status === 'rejected') renderMessages([]);
|
|
2556
|
+
}
|
|
2557
|
+
|
|
2558
|
+
const api = { ensureLoaded, refreshStatus, loadMessages };
|
|
2559
|
+
window.NimbusAgent = api;
|
|
2560
|
+
return api;
|
|
2561
|
+
})();
|
|
2562
|
+
|
|
2563
|
+
// Tabbed preview surface. Markdown preview and live app previews share
|
|
2564
|
+
// the right pane without fighting over a single iframe.
|
|
2565
|
+
let previewBackendKind = 'none'; // 'vite' | 'worker' | 'none'
|
|
2566
|
+
let activePreviewTabId = null;
|
|
2567
|
+
let markdownRenderSeq = 0;
|
|
2568
|
+
const previewTabs = new Map();
|
|
2569
|
+
let markdownRendererPromise = null;
|
|
2570
|
+
let markdownRenderer = null;
|
|
1603
2571
|
|
|
1604
2572
|
function previewUrlFor(kind) {
|
|
1605
2573
|
return kind === 'worker' ? WORKER_URL : VITE_URL;
|
|
1606
2574
|
}
|
|
1607
2575
|
|
|
1608
|
-
function
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
2576
|
+
function portPreviewUrl(port) {
|
|
2577
|
+
return SESSION_PREFIX + '/port/' + Number(port) + '/';
|
|
2578
|
+
}
|
|
2579
|
+
|
|
2580
|
+
function absolutePreviewUrl(url) {
|
|
2581
|
+
return new URL(url, location.origin).href;
|
|
2582
|
+
}
|
|
2583
|
+
|
|
2584
|
+
function shortPathLabel(path) {
|
|
2585
|
+
const parts = String(path || '').split('/').filter(Boolean);
|
|
2586
|
+
return parts[parts.length - 1] || 'Markdown';
|
|
2587
|
+
}
|
|
2588
|
+
|
|
2589
|
+
function appTabFor(kind) {
|
|
2590
|
+
const appKind = kind === 'worker' ? 'worker' : 'vite';
|
|
2591
|
+
return {
|
|
2592
|
+
id: 'app:' + appKind,
|
|
2593
|
+
type: 'app',
|
|
2594
|
+
kind: appKind,
|
|
2595
|
+
label: appKind === 'worker' ? 'Worker' : 'Preview',
|
|
2596
|
+
title: appKind === 'worker' ? 'Cloudflare Worker preview' : 'Vite preview',
|
|
2597
|
+
url: previewUrlFor(appKind),
|
|
2598
|
+
running: previewBackendKind === appKind,
|
|
2599
|
+
};
|
|
2600
|
+
}
|
|
2601
|
+
|
|
2602
|
+
function renderPreviewTabs() {
|
|
2603
|
+
if (!previewTabsEl) return;
|
|
2604
|
+
previewTabsEl.innerHTML = '';
|
|
2605
|
+
for (const tab of previewTabs.values()) {
|
|
2606
|
+
const button = document.createElement('button');
|
|
2607
|
+
button.type = 'button';
|
|
2608
|
+
button.className = [
|
|
2609
|
+
'preview-tab',
|
|
2610
|
+
tab.type === 'markdown' ? 'markdown' : '',
|
|
2611
|
+
tab.running ? 'running' : '',
|
|
2612
|
+
tab.id === activePreviewTabId ? 'active' : '',
|
|
2613
|
+
].filter(Boolean).join(' ');
|
|
2614
|
+
button.title = tab.title || tab.url || tab.path || tab.label;
|
|
2615
|
+
button.onclick = () => activatePreviewTab(tab.id);
|
|
2616
|
+
|
|
2617
|
+
const label = document.createElement('span');
|
|
2618
|
+
label.className = 'preview-tab-label';
|
|
2619
|
+
label.textContent = tab.label;
|
|
2620
|
+
button.appendChild(label);
|
|
2621
|
+
|
|
2622
|
+
previewTabsEl.appendChild(button);
|
|
2623
|
+
}
|
|
2624
|
+
}
|
|
2625
|
+
|
|
2626
|
+
function ensurePreviewTab(tab, options = {}) {
|
|
2627
|
+
const existing = previewTabs.get(tab.id);
|
|
2628
|
+
const merged = {
|
|
2629
|
+
...(existing || {}),
|
|
2630
|
+
...tab,
|
|
2631
|
+
createdAt: existing?.createdAt || Date.now(),
|
|
2632
|
+
updatedAt: Date.now(),
|
|
2633
|
+
};
|
|
2634
|
+
previewTabs.set(tab.id, merged);
|
|
2635
|
+
if (!existing && options.focusNew) {
|
|
2636
|
+
activatePreviewTab(tab.id);
|
|
2637
|
+
} else {
|
|
2638
|
+
renderPreviewTabs();
|
|
2639
|
+
}
|
|
2640
|
+
return merged;
|
|
2641
|
+
}
|
|
2642
|
+
|
|
2643
|
+
function firstAppTabId() {
|
|
2644
|
+
const port = [...previewTabs.values()].filter((tab) => tab.type === 'port').pop();
|
|
2645
|
+
if (port) return port.id;
|
|
2646
|
+
const preferred = previewBackendKind === 'worker' ? 'app:worker' : 'app:vite';
|
|
2647
|
+
if (previewTabs.has(preferred)) return preferred;
|
|
2648
|
+
if (previewTabs.has('app:vite')) return 'app:vite';
|
|
2649
|
+
if (previewTabs.has('app:worker')) return 'app:worker';
|
|
2650
|
+
return ensurePreviewTab(appTabFor('vite')).id;
|
|
2651
|
+
}
|
|
2652
|
+
|
|
2653
|
+
function activatePreviewTab(id) {
|
|
2654
|
+
const tab = previewTabs.get(id);
|
|
2655
|
+
if (!tab) return;
|
|
2656
|
+
activePreviewTabId = id;
|
|
2657
|
+
renderPreviewTabs();
|
|
2658
|
+
if (tab.type === 'markdown') {
|
|
2659
|
+
previewPanel.classList.add('markdown-mode');
|
|
2660
|
+
if (previewFrame) previewFrame.style.display = 'none';
|
|
2661
|
+
if (markdownPreview) markdownPreview.classList.add('active');
|
|
2662
|
+
setPreviewUrlBar('Markdown Preview · ' + tab.path);
|
|
2663
|
+
paintMarkdownPreview(tab.markdown || '').catch(() => {});
|
|
2664
|
+
return;
|
|
2665
|
+
}
|
|
2666
|
+
|
|
2667
|
+
previewPanel.classList.remove('markdown-mode');
|
|
2668
|
+
if (markdownPreview) markdownPreview.classList.remove('active');
|
|
2669
|
+
if (previewFrame) {
|
|
2670
|
+
previewFrame.style.display = 'block';
|
|
2671
|
+
const next = absolutePreviewUrl(tab.url);
|
|
2672
|
+
if (previewFrame.src !== next) previewFrame.src = tab.url;
|
|
2673
|
+
}
|
|
2674
|
+
setPreviewUrlBar(tab.url);
|
|
2675
|
+
}
|
|
2676
|
+
|
|
2677
|
+
function normalizePortStats(portStats) {
|
|
2678
|
+
const raw = Array.isArray(portStats?.ports)
|
|
2679
|
+
? portStats.ports
|
|
2680
|
+
: Array.isArray(portStats)
|
|
2681
|
+
? portStats
|
|
2682
|
+
: [];
|
|
2683
|
+
const seen = new Set();
|
|
2684
|
+
const ports = [];
|
|
2685
|
+
for (const entry of raw) {
|
|
2686
|
+
const port = Number(entry?.port);
|
|
2687
|
+
if (!Number.isInteger(port) || port <= 0 || seen.has(port)) continue;
|
|
2688
|
+
seen.add(port);
|
|
2689
|
+
ports.push({ port, pid: Number(entry?.pid) || 0 });
|
|
2690
|
+
}
|
|
2691
|
+
return ports;
|
|
2692
|
+
}
|
|
2693
|
+
|
|
2694
|
+
function syncPortPreviewTabs(portStats, canonicalPort, canonicalTabId) {
|
|
2695
|
+
const ports = normalizePortStats(portStats);
|
|
2696
|
+
const hiddenPort = Number.isInteger(canonicalPort) && canonicalPort > 0 ? canonicalPort : null;
|
|
2697
|
+
const visiblePorts = hiddenPort == null
|
|
2698
|
+
? ports
|
|
2699
|
+
: ports.filter((p) => p.port !== hiddenPort);
|
|
2700
|
+
const live = new Set(visiblePorts.map((p) => 'port:' + p.port));
|
|
2701
|
+
let activeWasRemoved = false;
|
|
2702
|
+
let activeWasCanonicalAlias = false;
|
|
2703
|
+
|
|
2704
|
+
for (const [id, tab] of previewTabs) {
|
|
2705
|
+
if (tab.type === 'port' && !live.has(id)) {
|
|
2706
|
+
previewTabs.delete(id);
|
|
2707
|
+
if (activePreviewTabId === id) {
|
|
2708
|
+
activeWasRemoved = true;
|
|
2709
|
+
activeWasCanonicalAlias = hiddenPort != null && tab.port === hiddenPort;
|
|
2710
|
+
}
|
|
2711
|
+
}
|
|
2712
|
+
}
|
|
2713
|
+
|
|
2714
|
+
for (const entry of visiblePorts) {
|
|
2715
|
+
const id = 'port:' + entry.port;
|
|
2716
|
+
const existed = previewTabs.has(id);
|
|
2717
|
+
ensurePreviewTab({
|
|
2718
|
+
id,
|
|
2719
|
+
type: 'port',
|
|
2720
|
+
port: entry.port,
|
|
2721
|
+
pid: entry.pid,
|
|
2722
|
+
label: ':' + entry.port,
|
|
2723
|
+
title: 'Port ' + entry.port,
|
|
2724
|
+
url: portPreviewUrl(entry.port),
|
|
2725
|
+
running: true,
|
|
2726
|
+
}, { focusNew: !existed });
|
|
2727
|
+
}
|
|
2728
|
+
|
|
2729
|
+
if (activeWasRemoved || !activePreviewTabId || !previewTabs.has(activePreviewTabId)) {
|
|
2730
|
+
activatePreviewTab(activeWasCanonicalAlias && canonicalTabId ? canonicalTabId : firstAppTabId());
|
|
2731
|
+
} else {
|
|
2732
|
+
renderPreviewTabs();
|
|
2733
|
+
}
|
|
2734
|
+
}
|
|
2735
|
+
|
|
2736
|
+
function setPreviewUrlBar(value) {
|
|
1619
2737
|
const urlBar = document.getElementById('urlBar');
|
|
1620
|
-
if (urlBar) urlBar.value =
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
2738
|
+
if (urlBar) urlBar.value = value;
|
|
2739
|
+
}
|
|
2740
|
+
|
|
2741
|
+
function showAppPreview() {
|
|
2742
|
+
ensurePreviewTab(appTabFor(previewBackendKind));
|
|
2743
|
+
activatePreviewTab(firstAppTabId());
|
|
2744
|
+
}
|
|
2745
|
+
|
|
2746
|
+
async function showMarkdownPreview(path, markdown) {
|
|
2747
|
+
const id = 'markdown:' + path;
|
|
2748
|
+
ensurePreviewTab({
|
|
2749
|
+
id,
|
|
2750
|
+
type: 'markdown',
|
|
2751
|
+
label: shortPathLabel(path),
|
|
2752
|
+
title: path,
|
|
2753
|
+
path,
|
|
2754
|
+
markdown: markdown || '',
|
|
2755
|
+
running: false,
|
|
2756
|
+
});
|
|
2757
|
+
activatePreviewTab(id);
|
|
2758
|
+
if (activePreviewTabId !== id) return;
|
|
2759
|
+
await paintMarkdownPreview(markdown || '');
|
|
2760
|
+
}
|
|
2761
|
+
|
|
2762
|
+
async function paintMarkdownPreview(markdown) {
|
|
2763
|
+
const seq = ++markdownRenderSeq;
|
|
2764
|
+
const html = await renderMarkdown(markdown || '');
|
|
2765
|
+
const active = activePreviewTabId ? previewTabs.get(activePreviewTabId) : null;
|
|
2766
|
+
if (seq === markdownRenderSeq && active?.type === 'markdown' && markdownPreviewBody) {
|
|
2767
|
+
markdownPreviewBody.innerHTML = html;
|
|
2768
|
+
}
|
|
2769
|
+
}
|
|
2770
|
+
|
|
2771
|
+
async function renderMarkdown(markdown) {
|
|
2772
|
+
try {
|
|
2773
|
+
const renderer = await ensureMarkdownRenderer();
|
|
2774
|
+
const raw = renderer.marked(markdown, { gfm: true, breaks: false });
|
|
2775
|
+
return renderer.purifier.sanitize(raw, { USE_PROFILES: { html: true } });
|
|
2776
|
+
} catch (e) {
|
|
2777
|
+
return '<pre><code>' + escapeHtml(markdown) + '</code></pre>';
|
|
2778
|
+
}
|
|
2779
|
+
}
|
|
2780
|
+
|
|
2781
|
+
function ensureMarkdownRenderer() {
|
|
2782
|
+
if (markdownRenderer) return Promise.resolve(markdownRenderer);
|
|
2783
|
+
if (markdownRendererPromise) return markdownRendererPromise;
|
|
2784
|
+
markdownRendererPromise = Promise.all([
|
|
2785
|
+
import('https://cdn.jsdelivr.net/npm/marked@12.0.2/+esm'),
|
|
2786
|
+
import('https://cdn.jsdelivr.net/npm/dompurify@3.1.7/+esm'),
|
|
2787
|
+
]).then(([markedModule, purifyModule]) => {
|
|
2788
|
+
const marked = markedModule.marked || markedModule.parse || markedModule.default;
|
|
2789
|
+
const createDOMPurify = purifyModule.default || purifyModule.DOMPurify || purifyModule;
|
|
2790
|
+
const purifier = typeof createDOMPurify === 'function'
|
|
2791
|
+
? createDOMPurify(window)
|
|
2792
|
+
: createDOMPurify;
|
|
2793
|
+
if (typeof marked !== 'function' || !purifier || typeof purifier.sanitize !== 'function') {
|
|
2794
|
+
throw new Error('markdown renderer unavailable');
|
|
2795
|
+
}
|
|
2796
|
+
markdownRenderer = { marked, purifier };
|
|
2797
|
+
return markdownRenderer;
|
|
2798
|
+
});
|
|
2799
|
+
return markdownRendererPromise;
|
|
2800
|
+
}
|
|
2801
|
+
|
|
2802
|
+
function escapeHtml(s) {
|
|
2803
|
+
const div = document.createElement('div');
|
|
2804
|
+
div.textContent = String(s);
|
|
2805
|
+
return div.innerHTML;
|
|
2806
|
+
}
|
|
2807
|
+
|
|
2808
|
+
function previewBackendPort(kind, stats) {
|
|
2809
|
+
const raw = kind === 'vite' ? stats?.vite?.port : null;
|
|
2810
|
+
const n = Number(raw);
|
|
2811
|
+
return Number.isInteger(n) && n > 0 ? n : null;
|
|
2812
|
+
}
|
|
2813
|
+
|
|
2814
|
+
function applyPreviewTarget(kind, stats) {
|
|
2815
|
+
const previousKind = previewBackendKind;
|
|
2816
|
+
previewBackendKind = kind === 'worker' || kind === 'vite' ? kind : 'none';
|
|
2817
|
+
const appKind = previewBackendKind === 'worker' ? 'worker' : 'vite';
|
|
2818
|
+
const appTab = ensurePreviewTab(appTabFor(appKind));
|
|
2819
|
+
const shouldFocusApp = previousKind !== previewBackendKind && previewBackendKind !== 'none';
|
|
2820
|
+
if (shouldFocusApp) activatePreviewTab(appTab.id);
|
|
2821
|
+
if (!activePreviewTabId) activatePreviewTab(appTab.id);
|
|
2822
|
+
syncPortPreviewTabs(stats?.ports, previewBackendPort(previewBackendKind, stats), appTab.id);
|
|
1624
2823
|
}
|
|
1625
2824
|
|
|
1626
2825
|
function reloadPreview() {
|
|
2826
|
+
const active = activePreviewTabId ? previewTabs.get(activePreviewTabId) : null;
|
|
2827
|
+
if (active?.type === 'markdown') {
|
|
2828
|
+
Editor.refreshMarkdownPreview().catch(() => {});
|
|
2829
|
+
return;
|
|
2830
|
+
}
|
|
1627
2831
|
// Refreshing without a running backend should re-poll the
|
|
1628
2832
|
// placeholder so users see dev-server status update promptly.
|
|
1629
|
-
if (previewFrame.src === 'about:blank'
|
|
1630
|
-
|
|
1631
|
-
previewFrame.src = previewUrlFor(
|
|
1632
|
-
(activePreview === 'none' || activePreview === null) ? 'vite' : activePreview
|
|
1633
|
-
);
|
|
2833
|
+
if (!active || previewFrame.src === 'about:blank') {
|
|
2834
|
+
activatePreviewTab(firstAppTabId());
|
|
1634
2835
|
} else {
|
|
1635
2836
|
try { previewFrame.contentWindow?.location.reload(); }
|
|
1636
2837
|
catch { previewFrame.src = previewFrame.src; }
|
|
1637
2838
|
}
|
|
1638
2839
|
}
|
|
1639
2840
|
|
|
2841
|
+
applyPreviewTarget('none', null);
|
|
2842
|
+
|
|
1640
2843
|
// ── Pane resize manager ──
|
|
1641
2844
|
//
|
|
1642
|
-
// Owns the
|
|
1643
|
-
// H1 tree ↔
|
|
1644
|
-
// H2
|
|
1645
|
-
// V1
|
|
2845
|
+
// Owns the three workspace resize handles:
|
|
2846
|
+
// H1 tree ↔ center stack (col) — TreeResizeHandle in FileTree IIFE
|
|
2847
|
+
// H2 center stack ↔ preview (col) — #resizeHandle
|
|
2848
|
+
// V1 top surface ↔ terminal (row) — #editorTerminalResizeHandle
|
|
1646
2849
|
//
|
|
1647
2850
|
// Each handle persists its dimension to localStorage so a reload
|
|
1648
2851
|
// restores the user's layout. Keyed per-session.
|
|
@@ -1691,41 +2894,25 @@
|
|
|
1691
2894
|
applyMonacoLayout();
|
|
1692
2895
|
}
|
|
1693
2896
|
|
|
1694
|
-
// Map middlePct
|
|
1695
|
-
// compete for horizontal space in each mode:
|
|
1696
|
-
// - split mode: .panel-terminal vs .panel-preview
|
|
1697
|
-
// - editor mode: .panel-left-stack vs .panel-preview
|
|
1698
|
-
// In editor mode .panel-left-stack is flex:1 (CSS rule); we
|
|
1699
|
-
// override with explicit basis. Same for .panel-terminal in split.
|
|
2897
|
+
// Map middlePct to the center workspace and preview columns.
|
|
1700
2898
|
function applyMiddlePreviewFlex() {
|
|
1701
2899
|
const stack = document.getElementById('leftStack');
|
|
1702
2900
|
const term = mainPanel.querySelector('.panel-terminal');
|
|
1703
2901
|
const prev = mainPanel.querySelector('.panel-preview');
|
|
1704
2902
|
const middlePct = Math.max(20, Math.min(80, dims.middlePct));
|
|
1705
|
-
if (
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
if (prev) prev.style.flex = (100 - middlePct) + ' 0 0%';
|
|
1709
|
-
if (term) term.style.flex = ''; // unset; vertical handler owns it
|
|
1710
|
-
} else if (layout === 'split') {
|
|
1711
|
-
if (term) term.style.flex = middlePct + ' 0 0%';
|
|
1712
|
-
if (prev) prev.style.flex = (100 - middlePct) + ' 0 0%';
|
|
1713
|
-
if (stack) stack.style.flex = '';
|
|
1714
|
-
} else {
|
|
1715
|
-
// Other modes (terminal-only / preview-only) don't use either
|
|
1716
|
-
// handle visibly; clear inline so CSS defaults take effect.
|
|
1717
|
-
if (term) term.style.flex = '';
|
|
1718
|
-
if (prev) prev.style.flex = '';
|
|
1719
|
-
if (stack) stack.style.flex = '';
|
|
1720
|
-
}
|
|
2903
|
+
if (stack) stack.style.flex = middlePct + ' 0 0%';
|
|
2904
|
+
if (prev) prev.style.flex = (100 - middlePct) + ' 0 0%';
|
|
2905
|
+
if (term) term.style.flex = '';
|
|
1721
2906
|
}
|
|
1722
2907
|
|
|
1723
2908
|
function applyEditorTerminalFlex() {
|
|
1724
2909
|
if (layout !== 'editor') return;
|
|
1725
|
-
const
|
|
2910
|
+
const topSurface = workSurface === 'agent'
|
|
2911
|
+
? document.getElementById('agentPanel')
|
|
2912
|
+
: document.getElementById('editorPanel');
|
|
1726
2913
|
const term = mainPanel.querySelector('.panel-terminal');
|
|
1727
2914
|
const editorPct = Math.max(10, Math.min(90, dims.editorPct));
|
|
1728
|
-
if (
|
|
2915
|
+
if (topSurface) topSurface.style.flex = editorPct + ' 0 0%';
|
|
1729
2916
|
if (term) term.style.flex = (100 - editorPct) + ' 0 0%';
|
|
1730
2917
|
}
|
|
1731
2918
|
|
|
@@ -1813,9 +3000,7 @@
|
|
|
1813
3000
|
if (tree) tree.style.width = dims.treeWidth + 'px';
|
|
1814
3001
|
}
|
|
1815
3002
|
|
|
1816
|
-
// Re-apply dims any time setLayout() switches
|
|
1817
|
-
// flex values are mode-specific (split vs editor have different
|
|
1818
|
-
// pane structures).
|
|
3003
|
+
// Re-apply dims any time setLayout() switches surfaces.
|
|
1819
3004
|
function onLayoutChange() {
|
|
1820
3005
|
applyMiddlePreviewFlex();
|
|
1821
3006
|
applyEditorTerminalFlex();
|
|
@@ -1852,7 +3037,7 @@
|
|
|
1852
3037
|
let kind = 'none';
|
|
1853
3038
|
if (s.wrangler?.running) kind = 'worker';
|
|
1854
3039
|
else if (s.vite?.running) kind = 'vite';
|
|
1855
|
-
applyPreviewTarget(kind);
|
|
3040
|
+
applyPreviewTarget(kind, s);
|
|
1856
3041
|
|
|
1857
3042
|
if (s.wrangler?.running) {
|
|
1858
3043
|
viteStatus.textContent = 'worker running (' + (s.wrangler.name || '?') + ')';
|
|
@@ -2341,7 +3526,10 @@
|
|
|
2341
3526
|
// Restore persisted pane dims before activating layout so the first paint
|
|
2342
3527
|
// shows the user's prior arrangement.
|
|
2343
3528
|
PaneResizer.restoreDims();
|
|
2344
|
-
|
|
3529
|
+
const initialMode = new URLSearchParams(location.search).get('agent') === '1'
|
|
3530
|
+
? 'agent'
|
|
3531
|
+
: 'editor';
|
|
3532
|
+
setLayout(initialMode);
|
|
2345
3533
|
connect();
|
|
2346
3534
|
</script>
|
|
2347
3535
|
</body>
|