tabminal 3.0.12 → 3.0.14
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/AGENTS.md +5 -0
- package/README.md +4 -0
- package/package.json +1 -1
- package/public/app.js +2305 -251
- package/public/index.html +137 -35
- package/public/styles.css +307 -0
- package/src/acp-manager.mjs +180 -18
- package/src/fs-routes.mjs +148 -39
- package/src/server.mjs +69 -25
- package/src/terminal-manager.mjs +97 -2
- package/src/terminal-session.mjs +24 -1
package/public/app.js
CHANGED
|
@@ -104,13 +104,22 @@ const editorPane = document.getElementById('editor-pane');
|
|
|
104
104
|
const HEARTBEAT_INTERVAL_MS = 1000;
|
|
105
105
|
const RECONNECT_RETRY_MS = 5000;
|
|
106
106
|
const FILE_TREE_REFRESH_INTERVAL_MS = 3000;
|
|
107
|
+
const FILE_VERSION_CHECK_INTERVAL_MS = 3000;
|
|
107
108
|
const MAIN_SERVER_ID = 'main';
|
|
108
109
|
const RUNTIME_BOOT_ID_STORAGE_KEY = 'tabminal_runtime_boot_id';
|
|
109
110
|
const WORKSPACE_DEVICE_ID_STORAGE_KEY = 'tabminal_workspace_device_id';
|
|
110
111
|
const RECENT_AGENT_USAGE_STORAGE_KEY = 'tabminal_recent_agent_usage';
|
|
111
112
|
const FILE_WORKSPACE_TAB_PREFIX = 'file:';
|
|
113
|
+
const MARKDOWN_PREVIEW_WORKSPACE_TAB_PREFIX = 'markdown-preview:';
|
|
112
114
|
const AGENT_WORKSPACE_TAB_PREFIX = 'agent:';
|
|
113
115
|
const TERMINAL_WORKSPACE_TAB_KEY = 'terminal:main';
|
|
116
|
+
const SUPPORTED_MARKDOWN_EXTENSIONS = new Set([
|
|
117
|
+
'md',
|
|
118
|
+
'markdown',
|
|
119
|
+
'mkd',
|
|
120
|
+
'mkdn',
|
|
121
|
+
'mdown'
|
|
122
|
+
]);
|
|
114
123
|
const SUPPORTED_IMAGE_EXTENSIONS = new Set([
|
|
115
124
|
'png',
|
|
116
125
|
'jpg',
|
|
@@ -119,6 +128,20 @@ const SUPPORTED_IMAGE_EXTENSIONS = new Set([
|
|
|
119
128
|
'svg',
|
|
120
129
|
'webp'
|
|
121
130
|
]);
|
|
131
|
+
const SUPPORTED_PDF_EXTENSIONS = new Set([
|
|
132
|
+
'pdf'
|
|
133
|
+
]);
|
|
134
|
+
const PDFJS_VERSION = '5.6.205';
|
|
135
|
+
const PDFJS_MODULE_URL = `https://cdn.jsdelivr.net/npm/pdfjs-dist@${PDFJS_VERSION}/build/pdf.min.mjs`;
|
|
136
|
+
const PDFJS_WORKER_URL = `https://cdn.jsdelivr.net/npm/pdfjs-dist@${PDFJS_VERSION}/build/pdf.worker.min.mjs`;
|
|
137
|
+
const MARKDOWN_IT_MODULE_URL = 'https://cdn.jsdelivr.net/npm/markdown-it@14.1.1/+esm';
|
|
138
|
+
const MARKDOWN_TASK_LISTS_MODULE_URL = 'https://cdn.jsdelivr.net/npm/markdown-it-task-lists@2.1.1/+esm';
|
|
139
|
+
const MARKDOWN_KATEX_MODULE_URL = 'https://cdn.jsdelivr.net/npm/@traptitech/markdown-it-katex@3.6.0/+esm';
|
|
140
|
+
const KATEX_MODULE_URL = 'https://cdn.jsdelivr.net/npm/katex@0.16.25/+esm';
|
|
141
|
+
const HIGHLIGHT_JS_MODULE_URL = 'https://cdn.jsdelivr.net/npm/highlight.js@11.11.1/+esm';
|
|
142
|
+
const MARKDOWN_PREVIEW_GITHUB_CSS_URL = 'https://cdn.jsdelivr.net/npm/github-markdown-css@5.8.1/github-markdown-dark.min.css';
|
|
143
|
+
const MARKDOWN_PREVIEW_HIGHLIGHT_CSS_URL = 'https://cdn.jsdelivr.net/npm/highlight.js@11.11.1/styles/github-dark.css';
|
|
144
|
+
const MARKDOWN_PREVIEW_KATEX_CSS_URL = 'https://cdn.jsdelivr.net/npm/katex@0.16.25/dist/katex.min.css';
|
|
122
145
|
const CLOSE_ICON_SVG = '<svg viewBox="0 0 24 24" width="16" height="16" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line></svg>';
|
|
123
146
|
const AGENT_ICON_SVG = '<svg viewBox="0 0 24 24" width="17" height="17" stroke="currentColor" stroke-width="1.8" fill="none" stroke-linecap="round" stroke-linejoin="round"><rect x="7" y="7" width="10" height="10" rx="2"></rect><path d="M9 7V5"></path><path d="M15 7V5"></path><path d="M12 17v2"></path><path d="M5 12H3"></path><path d="M21 12h-2"></path><path d="M9 11h.01"></path><path d="M15 11h.01"></path><path d="M9.5 14c.7.67 1.53 1 2.5 1s1.8-.33 2.5-1"></path></svg>';
|
|
124
147
|
const TERMINAL_TAB_ICON_SVG = '<svg viewBox="0 0 24 24" width="15" height="15" stroke="currentColor" stroke-width="1.8" fill="none" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="5" width="18" height="14" rx="2"></rect><path d="m8 10 3 2-3 2"></path><path d="M13 15h4"></path></svg>';
|
|
@@ -137,6 +160,9 @@ const RENAME_ICON_SVG = '<svg viewBox="0 0 24 24" width="14" height="14" stroke=
|
|
|
137
160
|
const DELETE_ICON_SVG = '<svg viewBox="0 0 24 24" width="14" height="14" stroke="currentColor" stroke-width="1.9" fill="none" stroke-linecap="round" stroke-linejoin="round"><path d="M4 7h16"></path><path d="M10 11v6"></path><path d="M14 11v6"></path><path d="M6 7l1 12h10l1-12"></path><path d="M9 7V4h6v3"></path></svg>';
|
|
138
161
|
const NEW_FOLDER_ICON_SVG = '<svg viewBox="0 0 24 24" width="14" height="14" stroke="currentColor" stroke-width="1.8" fill="none" stroke-linecap="round" stroke-linejoin="round"><path d="M3.5 7.5A2.5 2.5 0 0 1 6 5h4l2 2h6a2.5 2.5 0 0 1 2.5 2.5V17A2.5 2.5 0 0 1 18 19.5H6A2.5 2.5 0 0 1 3.5 17Z"></path><path d="M12 10.5v5"></path><path d="M9.5 13h5"></path></svg>';
|
|
139
162
|
const NEW_FILE_ICON_SVG = '<svg viewBox="0 0 24 24" width="14" height="14" stroke="currentColor" stroke-width="1.8" fill="none" stroke-linecap="round" stroke-linejoin="round"><path d="M7 3.5h7l4 4V20.5H7A2.5 2.5 0 0 1 4.5 18V6A2.5 2.5 0 0 1 7 3.5Z"></path><path d="M14 3.5V8h4"></path><path d="M12 11v6"></path><path d="M9 14h6"></path></svg>';
|
|
163
|
+
const MARKDOWN_PREVIEW_ICON_SVG = '<svg viewBox="0 0 24 24" width="15" height="15" stroke="currentColor" stroke-width="1.8" fill="none" stroke-linecap="round" stroke-linejoin="round"><path d="M3 5.5h18"></path><path d="M3 9.5h18"></path><path d="M5 5.5V18a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1V5.5"></path><path d="M9 13h6"></path><path d="M9 16h4"></path></svg>';
|
|
164
|
+
const MARKDOWN_SPLIT_ENABLE_ICON_SVG = '<svg viewBox="0 0 24 24" width="15" height="15" stroke="currentColor" stroke-width="1.8" fill="none" stroke-linecap="round" stroke-linejoin="round"><rect x="4" y="5" width="16" height="14" rx="2"></rect><path d="M12 5v14"></path></svg>';
|
|
165
|
+
const MARKDOWN_SPLIT_DISABLE_ICON_SVG = '<svg viewBox="0 0 24 24" width="15" height="15" stroke="currentColor" stroke-width="1.8" fill="none" stroke-linecap="round" stroke-linejoin="round"><rect x="4" y="5" width="16" height="14" rx="2"></rect><path d="M12 5v14"></path><path d="m9.25 8.5 5.5 7"></path></svg>';
|
|
140
166
|
const TERMINAL_FONT_FAMILY = '\'Monaspace Neon\', "SF Mono Terminal", '
|
|
141
167
|
+ '"SFMono-Regular", "SF Mono", "JetBrains Mono", Menlo, Consolas, '
|
|
142
168
|
+ 'monospace';
|
|
@@ -161,12 +187,18 @@ const agentSetupState = {
|
|
|
161
187
|
};
|
|
162
188
|
let primaryServerBootId = '';
|
|
163
189
|
let runtimeReloadScheduled = false;
|
|
190
|
+
let pdfJsLibPromise = null;
|
|
191
|
+
let markdownPreviewBundlePromise = null;
|
|
164
192
|
// #endregion
|
|
165
193
|
|
|
166
194
|
function makeFileWorkspaceTabKey(filePath) {
|
|
167
195
|
return `${FILE_WORKSPACE_TAB_PREFIX}${filePath}`;
|
|
168
196
|
}
|
|
169
197
|
|
|
198
|
+
function makeMarkdownPreviewWorkspaceTabKey(filePath) {
|
|
199
|
+
return `${MARKDOWN_PREVIEW_WORKSPACE_TAB_PREFIX}${filePath}`;
|
|
200
|
+
}
|
|
201
|
+
|
|
170
202
|
function makeAgentTabKey(serverId, tabId) {
|
|
171
203
|
return `${AGENT_WORKSPACE_TAB_PREFIX}${serverId}:${tabId}`;
|
|
172
204
|
}
|
|
@@ -182,7 +214,15 @@ function isTerminalWorkspaceTabKey(key) {
|
|
|
182
214
|
|
|
183
215
|
function isFileWorkspaceTabKey(key) {
|
|
184
216
|
return typeof key === 'string'
|
|
185
|
-
&&
|
|
217
|
+
&& (
|
|
218
|
+
key.startsWith(FILE_WORKSPACE_TAB_PREFIX)
|
|
219
|
+
|| key.startsWith(MARKDOWN_PREVIEW_WORKSPACE_TAB_PREFIX)
|
|
220
|
+
);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function isMarkdownPreviewWorkspaceTabKey(key) {
|
|
224
|
+
return typeof key === 'string'
|
|
225
|
+
&& key.startsWith(MARKDOWN_PREVIEW_WORKSPACE_TAB_PREFIX);
|
|
186
226
|
}
|
|
187
227
|
|
|
188
228
|
function isCompactWorkspaceMode() {
|
|
@@ -201,10 +241,127 @@ function isSupportedImagePath(filePath) {
|
|
|
201
241
|
return SUPPORTED_IMAGE_EXTENSIONS.has(ext);
|
|
202
242
|
}
|
|
203
243
|
|
|
244
|
+
function isSupportedPdfPath(filePath) {
|
|
245
|
+
if (typeof filePath !== 'string') {
|
|
246
|
+
return false;
|
|
247
|
+
}
|
|
248
|
+
const dotIndex = filePath.lastIndexOf('.');
|
|
249
|
+
if (dotIndex === -1) {
|
|
250
|
+
return false;
|
|
251
|
+
}
|
|
252
|
+
const ext = filePath.slice(dotIndex + 1).toLowerCase();
|
|
253
|
+
return SUPPORTED_PDF_EXTENSIONS.has(ext);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function isSupportedMarkdownPath(filePath) {
|
|
257
|
+
if (typeof filePath !== 'string') {
|
|
258
|
+
return false;
|
|
259
|
+
}
|
|
260
|
+
const dotIndex = filePath.lastIndexOf('.');
|
|
261
|
+
if (dotIndex === -1) {
|
|
262
|
+
return false;
|
|
263
|
+
}
|
|
264
|
+
const ext = filePath.slice(dotIndex + 1).toLowerCase();
|
|
265
|
+
return SUPPORTED_MARKDOWN_EXTENSIONS.has(ext);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
function ensureExternalStylesheet(id, href) {
|
|
269
|
+
if (!href || document.getElementById(id)) {
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
272
|
+
const link = document.createElement('link');
|
|
273
|
+
link.id = id;
|
|
274
|
+
link.rel = 'stylesheet';
|
|
275
|
+
link.href = href;
|
|
276
|
+
document.head.appendChild(link);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
async function loadMarkdownPreviewBundle() {
|
|
280
|
+
if (!markdownPreviewBundlePromise) {
|
|
281
|
+
markdownPreviewBundlePromise = (async () => {
|
|
282
|
+
ensureExternalStylesheet(
|
|
283
|
+
'markdown-preview-github-css',
|
|
284
|
+
MARKDOWN_PREVIEW_GITHUB_CSS_URL
|
|
285
|
+
);
|
|
286
|
+
ensureExternalStylesheet(
|
|
287
|
+
'markdown-preview-highlight-css',
|
|
288
|
+
MARKDOWN_PREVIEW_HIGHLIGHT_CSS_URL
|
|
289
|
+
);
|
|
290
|
+
ensureExternalStylesheet(
|
|
291
|
+
'markdown-preview-katex-css',
|
|
292
|
+
MARKDOWN_PREVIEW_KATEX_CSS_URL
|
|
293
|
+
);
|
|
294
|
+
const [
|
|
295
|
+
{ default: MarkdownIt },
|
|
296
|
+
{ default: markdownItTaskLists },
|
|
297
|
+
{ default: markdownItKatex },
|
|
298
|
+
{ default: katex },
|
|
299
|
+
{ default: hljs }
|
|
300
|
+
] = await Promise.all([
|
|
301
|
+
import(MARKDOWN_IT_MODULE_URL),
|
|
302
|
+
import(MARKDOWN_TASK_LISTS_MODULE_URL),
|
|
303
|
+
import(MARKDOWN_KATEX_MODULE_URL),
|
|
304
|
+
import(KATEX_MODULE_URL),
|
|
305
|
+
import(HIGHLIGHT_JS_MODULE_URL)
|
|
306
|
+
]);
|
|
307
|
+
const renderer = new MarkdownIt({
|
|
308
|
+
html: true,
|
|
309
|
+
linkify: true,
|
|
310
|
+
breaks: false,
|
|
311
|
+
highlight(source, language) {
|
|
312
|
+
const code = String(source || '');
|
|
313
|
+
const nextLanguage = String(language || '').trim();
|
|
314
|
+
let html = '';
|
|
315
|
+
if (nextLanguage && hljs.getLanguage(nextLanguage)) {
|
|
316
|
+
html = hljs.highlight(code, {
|
|
317
|
+
language: nextLanguage,
|
|
318
|
+
ignoreIllegals: true
|
|
319
|
+
}).value;
|
|
320
|
+
} else {
|
|
321
|
+
html = hljs.highlightAuto(code).value;
|
|
322
|
+
}
|
|
323
|
+
const languageClass = nextLanguage
|
|
324
|
+
? ` language-${escapeHtml(nextLanguage)}`
|
|
325
|
+
: '';
|
|
326
|
+
return `<pre class="hljs"><code class="hljs${languageClass}">${html}</code></pre>`;
|
|
327
|
+
}
|
|
328
|
+
});
|
|
329
|
+
renderer.use(markdownItTaskLists, {
|
|
330
|
+
enabled: false,
|
|
331
|
+
label: true,
|
|
332
|
+
labelAfter: true
|
|
333
|
+
});
|
|
334
|
+
renderer.use(markdownItKatex, { katex });
|
|
335
|
+
return {
|
|
336
|
+
renderer
|
|
337
|
+
};
|
|
338
|
+
})().catch((error) => {
|
|
339
|
+
markdownPreviewBundlePromise = null;
|
|
340
|
+
throw error;
|
|
341
|
+
});
|
|
342
|
+
}
|
|
343
|
+
return await markdownPreviewBundlePromise;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
async function loadPdfJs() {
|
|
347
|
+
if (!pdfJsLibPromise) {
|
|
348
|
+
pdfJsLibPromise = import(PDFJS_MODULE_URL)
|
|
349
|
+
.then((pdfjsLib) => {
|
|
350
|
+
pdfjsLib.GlobalWorkerOptions.workerSrc = PDFJS_WORKER_URL;
|
|
351
|
+
return pdfjsLib;
|
|
352
|
+
});
|
|
353
|
+
}
|
|
354
|
+
return await pdfJsLibPromise;
|
|
355
|
+
}
|
|
356
|
+
|
|
204
357
|
function isCompactTerminalTabsMode() {
|
|
205
358
|
return !!window.__tabminalCompactTerminalTabsMode;
|
|
206
359
|
}
|
|
207
360
|
|
|
361
|
+
function canUseMarkdownSplitTabsMode() {
|
|
362
|
+
return !isForcedTerminalWorkspaceMode();
|
|
363
|
+
}
|
|
364
|
+
|
|
208
365
|
function isForcedTerminalWorkspaceMode() {
|
|
209
366
|
return isCompactWorkspaceMode() || isCompactTerminalTabsMode();
|
|
210
367
|
}
|
|
@@ -220,8 +377,52 @@ function buildMainTerminalTheme() {
|
|
|
220
377
|
}
|
|
221
378
|
|
|
222
379
|
function workspaceKeyToFilePath(key) {
|
|
223
|
-
if (
|
|
224
|
-
|
|
380
|
+
if (typeof key !== 'string' || key.length === 0) return '';
|
|
381
|
+
if (key.startsWith(MARKDOWN_PREVIEW_WORKSPACE_TAB_PREFIX)) {
|
|
382
|
+
return key.slice(MARKDOWN_PREVIEW_WORKSPACE_TAB_PREFIX.length);
|
|
383
|
+
}
|
|
384
|
+
if (key.startsWith(FILE_WORKSPACE_TAB_PREFIX)) {
|
|
385
|
+
return key.slice(FILE_WORKSPACE_TAB_PREFIX.length);
|
|
386
|
+
}
|
|
387
|
+
return '';
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
function isExternalHref(href) {
|
|
391
|
+
return /^(?:[a-z][a-z0-9+.-]*:|\/\/)/i.test(String(href || '').trim());
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
function resolveMarkdownLocalTarget(baseFilePath, href) {
|
|
395
|
+
const value = String(href || '').trim();
|
|
396
|
+
const basePath = String(baseFilePath || '').trim();
|
|
397
|
+
if (!value || !basePath || value.startsWith('#') || isExternalHref(value)) {
|
|
398
|
+
return null;
|
|
399
|
+
}
|
|
400
|
+
const baseDir = basePath.includes('/')
|
|
401
|
+
? basePath.slice(0, basePath.lastIndexOf('/') + 1)
|
|
402
|
+
: '/';
|
|
403
|
+
try {
|
|
404
|
+
const resolved = new URL(
|
|
405
|
+
value,
|
|
406
|
+
`https://tabminal.local${encodeURI(baseDir)}`
|
|
407
|
+
);
|
|
408
|
+
if (resolved.origin !== 'https://tabminal.local') {
|
|
409
|
+
return null;
|
|
410
|
+
}
|
|
411
|
+
return {
|
|
412
|
+
path: decodeURIComponent(resolved.pathname),
|
|
413
|
+
hash: resolved.hash || ''
|
|
414
|
+
};
|
|
415
|
+
} catch {
|
|
416
|
+
return null;
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
function slugifyMarkdownHeading(text) {
|
|
421
|
+
return String(text || '')
|
|
422
|
+
.trim()
|
|
423
|
+
.toLowerCase()
|
|
424
|
+
.replace(/[^\p{L}\p{N}\s-]/gu, '')
|
|
425
|
+
.replace(/\s+/g, '-');
|
|
225
426
|
}
|
|
226
427
|
|
|
227
428
|
function getWorkspaceDeviceId() {
|
|
@@ -263,15 +464,44 @@ function normalizeWorkspaceSnapshot(input = {}, fallback = {}) {
|
|
|
263
464
|
? base.updatedBy
|
|
264
465
|
: ''
|
|
265
466
|
);
|
|
467
|
+
const openFiles = uniqueStringList(source.openFiles);
|
|
468
|
+
const fallbackOpenFiles = uniqueStringList(base.openFiles);
|
|
469
|
+
const markdownSplitPathSource =
|
|
470
|
+
typeof source.markdownSplitPath === 'string'
|
|
471
|
+
? source.markdownSplitPath
|
|
472
|
+
: (
|
|
473
|
+
typeof base.markdownSplitPath === 'string'
|
|
474
|
+
? base.markdownSplitPath
|
|
475
|
+
: ''
|
|
476
|
+
);
|
|
477
|
+
const markdownSplitPath = (
|
|
478
|
+
markdownSplitPathSource
|
|
479
|
+
&& isSupportedMarkdownPath(markdownSplitPathSource)
|
|
480
|
+
&& (
|
|
481
|
+
openFiles.includes(markdownSplitPathSource)
|
|
482
|
+
|| fallbackOpenFiles.includes(markdownSplitPathSource)
|
|
483
|
+
)
|
|
484
|
+
)
|
|
485
|
+
? markdownSplitPathSource
|
|
486
|
+
: '';
|
|
487
|
+
const activeWorkspaceTabKey = typeof source.activeWorkspaceTabKey === 'string'
|
|
488
|
+
? source.activeWorkspaceTabKey
|
|
489
|
+
: (
|
|
490
|
+
typeof base.activeWorkspaceTabKey === 'string'
|
|
491
|
+
? base.activeWorkspaceTabKey
|
|
492
|
+
: ''
|
|
493
|
+
);
|
|
266
494
|
return {
|
|
267
495
|
updatedAt,
|
|
268
496
|
updatedBy,
|
|
269
497
|
isVisible: !!source.isVisible,
|
|
270
|
-
openFiles
|
|
498
|
+
openFiles,
|
|
271
499
|
terminalDisplayMode: source.terminalDisplayMode === 'tab'
|
|
272
500
|
? 'tab'
|
|
273
501
|
: 'auto',
|
|
274
|
-
expandedPaths: uniqueStringList(source.expandedPaths)
|
|
502
|
+
expandedPaths: uniqueStringList(source.expandedPaths),
|
|
503
|
+
markdownSplitPath,
|
|
504
|
+
activeWorkspaceTabKey
|
|
275
505
|
};
|
|
276
506
|
}
|
|
277
507
|
|
|
@@ -299,6 +529,8 @@ function buildWorkspaceSnapshotForSession(session, overrides = {}) {
|
|
|
299
529
|
openFiles: session.editorState.openFiles,
|
|
300
530
|
terminalDisplayMode: session.sharedWorkspaceState.terminalDisplayMode,
|
|
301
531
|
expandedPaths: session.sharedWorkspaceState.expandedPaths,
|
|
532
|
+
markdownSplitPath: session.workspaceState.markdownSplitPath,
|
|
533
|
+
activeWorkspaceTabKey: session.workspaceState.activeTabKey,
|
|
302
534
|
...overrides
|
|
303
535
|
});
|
|
304
536
|
}
|
|
@@ -709,6 +941,26 @@ class EditorManager {
|
|
|
709
941
|
this.monacoContainer = document.getElementById('monaco-container');
|
|
710
942
|
this.imagePreviewContainer = document.getElementById('image-preview-container');
|
|
711
943
|
this.imagePreview = document.getElementById('image-preview');
|
|
944
|
+
this.pdfPreviewContainer = document.getElementById(
|
|
945
|
+
'pdf-preview-container'
|
|
946
|
+
);
|
|
947
|
+
this.pdfPreviewStatus = document.getElementById('pdf-preview-status');
|
|
948
|
+
this.pdfPreviewStatusPrimary = document.getElementById(
|
|
949
|
+
'pdf-preview-status-primary'
|
|
950
|
+
);
|
|
951
|
+
this.pdfPreviewStatusSecondary = document.getElementById(
|
|
952
|
+
'pdf-preview-status-secondary'
|
|
953
|
+
);
|
|
954
|
+
this.pdfPreviewPages = document.getElementById('pdf-preview-pages');
|
|
955
|
+
this.markdownPreviewContainer = document.getElementById(
|
|
956
|
+
'markdown-preview-container'
|
|
957
|
+
);
|
|
958
|
+
this.markdownPreviewScroll = document.getElementById(
|
|
959
|
+
'markdown-preview-scroll'
|
|
960
|
+
);
|
|
961
|
+
this.markdownPreviewContent = document.getElementById(
|
|
962
|
+
'markdown-preview-content'
|
|
963
|
+
);
|
|
712
964
|
this.emptyState = document.getElementById('empty-editor-state');
|
|
713
965
|
this.terminalWrapper = terminalWrapper;
|
|
714
966
|
this.terminalOriginalParent = terminalWrapper?.parentElement || null;
|
|
@@ -758,17 +1010,47 @@ class EditorManager {
|
|
|
758
1010
|
this.agentEmbeddedEditors = [];
|
|
759
1011
|
this.agentEmbeddedTerminals = new Map();
|
|
760
1012
|
this.agentTranscriptLayout = null;
|
|
1013
|
+
this.pdfPreviewState = {
|
|
1014
|
+
path: '',
|
|
1015
|
+
sessionKey: '',
|
|
1016
|
+
renderToken: 0,
|
|
1017
|
+
document: null,
|
|
1018
|
+
loadingTask: null,
|
|
1019
|
+
metadata: '',
|
|
1020
|
+
renderedWidth: 0,
|
|
1021
|
+
relayoutTimer: 0
|
|
1022
|
+
};
|
|
1023
|
+
this.markdownPreviewState = {
|
|
1024
|
+
path: '',
|
|
1025
|
+
sessionKey: '',
|
|
1026
|
+
renderToken: 0,
|
|
1027
|
+
renderTimer: 0,
|
|
1028
|
+
pendingHash: ''
|
|
1029
|
+
};
|
|
1030
|
+
this.fileVersionCheckTimer = null;
|
|
1031
|
+
this.fileVersionCheckPromise = null;
|
|
1032
|
+
this.fileConflictDialogKey = '';
|
|
1033
|
+
this.suppressFileWriteCapture = false;
|
|
761
1034
|
this.agentTranscriptResizeObserver = null;
|
|
1035
|
+
this.treeDirectoryFetches = new Map();
|
|
1036
|
+
this.treeRefreshInFlight = false;
|
|
1037
|
+
this.treeRefreshRerunRequested = false;
|
|
1038
|
+
this.treeRefreshBatchQueued = false;
|
|
1039
|
+
this.pendingForcedTreeRefreshSessions = new Set();
|
|
762
1040
|
|
|
763
1041
|
this.initTerminalControls();
|
|
764
1042
|
this.initResizer();
|
|
765
1043
|
this.initAgentPanel();
|
|
1044
|
+
this.initMarkdownPreview();
|
|
766
1045
|
this.initMonaco();
|
|
767
1046
|
this.loadIconMap();
|
|
768
1047
|
this.agentTimestampTimer = window.setInterval(() => {
|
|
769
1048
|
this.refreshAgentTimelineTimestamps();
|
|
770
1049
|
this.refreshAgentUsageHud();
|
|
771
1050
|
}, 1000);
|
|
1051
|
+
this.fileVersionCheckTimer = window.setInterval(() => {
|
|
1052
|
+
void this.checkActiveFileVersion();
|
|
1053
|
+
}, FILE_VERSION_CHECK_INTERVAL_MS);
|
|
772
1054
|
}
|
|
773
1055
|
|
|
774
1056
|
isTerminalTabPinned(session = this.currentSession) {
|
|
@@ -811,6 +1093,155 @@ class EditorManager {
|
|
|
811
1093
|
this.terminalWrapper.appendChild(this.terminalLayoutButton);
|
|
812
1094
|
}
|
|
813
1095
|
|
|
1096
|
+
initMarkdownPreview() {
|
|
1097
|
+
if (!this.markdownPreviewContainer || !this.markdownPreviewContent) {
|
|
1098
|
+
return;
|
|
1099
|
+
}
|
|
1100
|
+
this.markdownPreviewContainer.addEventListener('click', (event) => {
|
|
1101
|
+
const link = event.target.closest('a[data-markdown-local-path]');
|
|
1102
|
+
if (!link) {
|
|
1103
|
+
return;
|
|
1104
|
+
}
|
|
1105
|
+
const filePath = String(
|
|
1106
|
+
link.dataset.markdownLocalPath || ''
|
|
1107
|
+
).trim();
|
|
1108
|
+
if (!filePath) {
|
|
1109
|
+
return;
|
|
1110
|
+
}
|
|
1111
|
+
event.preventDefault();
|
|
1112
|
+
event.stopPropagation();
|
|
1113
|
+
void this.openLocalMarkdownLink(
|
|
1114
|
+
filePath,
|
|
1115
|
+
String(link.dataset.markdownLocalHash || '')
|
|
1116
|
+
);
|
|
1117
|
+
});
|
|
1118
|
+
}
|
|
1119
|
+
|
|
1120
|
+
getMarkdownSplitPath(session = this.currentSession) {
|
|
1121
|
+
if (!session?.workspaceState) {
|
|
1122
|
+
return '';
|
|
1123
|
+
}
|
|
1124
|
+
return typeof session.workspaceState.markdownSplitPath === 'string'
|
|
1125
|
+
? session.workspaceState.markdownSplitPath
|
|
1126
|
+
: '';
|
|
1127
|
+
}
|
|
1128
|
+
|
|
1129
|
+
isMarkdownSplitViewEnabled(
|
|
1130
|
+
session = this.currentSession,
|
|
1131
|
+
filePath = session?.editorState?.activeFilePath || ''
|
|
1132
|
+
) {
|
|
1133
|
+
return !!(
|
|
1134
|
+
session
|
|
1135
|
+
&& canUseMarkdownSplitTabsMode()
|
|
1136
|
+
&& filePath
|
|
1137
|
+
&& this.getMarkdownSplitPath(session) === filePath
|
|
1138
|
+
&& isSupportedMarkdownPath(filePath)
|
|
1139
|
+
);
|
|
1140
|
+
}
|
|
1141
|
+
|
|
1142
|
+
setMarkdownSplitView(
|
|
1143
|
+
filePath,
|
|
1144
|
+
enabled,
|
|
1145
|
+
session = this.currentSession
|
|
1146
|
+
) {
|
|
1147
|
+
if (!session?.workspaceState || !isSupportedMarkdownPath(filePath)) {
|
|
1148
|
+
return;
|
|
1149
|
+
}
|
|
1150
|
+
const nextMarkdownSplitPath = (
|
|
1151
|
+
enabled
|
|
1152
|
+
&& canUseMarkdownSplitTabsMode()
|
|
1153
|
+
)
|
|
1154
|
+
? filePath
|
|
1155
|
+
: '';
|
|
1156
|
+
if (session.workspaceState.markdownSplitPath === nextMarkdownSplitPath) {
|
|
1157
|
+
return;
|
|
1158
|
+
}
|
|
1159
|
+
session.workspaceState.markdownSplitPath = nextMarkdownSplitPath;
|
|
1160
|
+
session.saveState({ touchWorkspace: true });
|
|
1161
|
+
if (session.key !== this.currentSession?.key) {
|
|
1162
|
+
return;
|
|
1163
|
+
}
|
|
1164
|
+
this.renderEditorTabs();
|
|
1165
|
+
const activeKey = this.getActiveWorkspaceTabKey(session);
|
|
1166
|
+
if (
|
|
1167
|
+
activeKey === makeFileWorkspaceTabKey(filePath)
|
|
1168
|
+
|| activeKey === makeMarkdownPreviewWorkspaceTabKey(filePath)
|
|
1169
|
+
) {
|
|
1170
|
+
this.activateWorkspaceTab(activeKey, true);
|
|
1171
|
+
}
|
|
1172
|
+
this.layout();
|
|
1173
|
+
}
|
|
1174
|
+
|
|
1175
|
+
syncMarkdownSplitSupport(session = this.currentSession) {
|
|
1176
|
+
if (!session?.workspaceState) {
|
|
1177
|
+
return;
|
|
1178
|
+
}
|
|
1179
|
+
const markdownSplitPath = this.getMarkdownSplitPath(session);
|
|
1180
|
+
if (
|
|
1181
|
+
markdownSplitPath
|
|
1182
|
+
&& (
|
|
1183
|
+
!isSupportedMarkdownPath(markdownSplitPath)
|
|
1184
|
+
|| !session.editorState.openFiles.includes(markdownSplitPath)
|
|
1185
|
+
)
|
|
1186
|
+
) {
|
|
1187
|
+
session.workspaceState.markdownSplitPath = '';
|
|
1188
|
+
}
|
|
1189
|
+
}
|
|
1190
|
+
|
|
1191
|
+
showMarkdownSplitView(filePath, options = {}) {
|
|
1192
|
+
const session = options.session || this.currentSession;
|
|
1193
|
+
const focusEditor = options.focusEditor !== false;
|
|
1194
|
+
if (!session || !filePath) {
|
|
1195
|
+
return false;
|
|
1196
|
+
}
|
|
1197
|
+
const file = this.getModel(filePath, session);
|
|
1198
|
+
if (!file || file.type !== 'text') {
|
|
1199
|
+
return false;
|
|
1200
|
+
}
|
|
1201
|
+
if (!this.editor || !this.monacoContainer || !this.contentContainer) {
|
|
1202
|
+
return false;
|
|
1203
|
+
}
|
|
1204
|
+
|
|
1205
|
+
this.contentContainer.classList.add('markdown-split-active');
|
|
1206
|
+
this.agentContainer.style.display = 'none';
|
|
1207
|
+
this.imagePreviewContainer.style.display = 'none';
|
|
1208
|
+
this.hidePdfPreview();
|
|
1209
|
+
this.monacoContainer.style.display = 'block';
|
|
1210
|
+
this.markdownPreviewContainer.style.display = 'flex';
|
|
1211
|
+
this.emptyState.style.display = 'none';
|
|
1212
|
+
|
|
1213
|
+
if (!file.model && file.content !== null && this.monacoInstance) {
|
|
1214
|
+
file.model = this.monacoInstance.editor.createModel(
|
|
1215
|
+
file.content,
|
|
1216
|
+
undefined,
|
|
1217
|
+
this.monacoInstance.Uri.file(filePath)
|
|
1218
|
+
);
|
|
1219
|
+
}
|
|
1220
|
+
|
|
1221
|
+
if (file.model) {
|
|
1222
|
+
this.editor.setModel(file.model);
|
|
1223
|
+
this.editor.updateOptions({ readOnly: !!file.readonly });
|
|
1224
|
+
const savedViewState = session.editorState.viewStates.get(filePath);
|
|
1225
|
+
if (savedViewState) {
|
|
1226
|
+
this.editor.restoreViewState(savedViewState);
|
|
1227
|
+
}
|
|
1228
|
+
}
|
|
1229
|
+
|
|
1230
|
+
void this.renderMarkdownPreview(filePath, {
|
|
1231
|
+
session,
|
|
1232
|
+
show: true
|
|
1233
|
+
});
|
|
1234
|
+
|
|
1235
|
+
requestAnimationFrame(() => {
|
|
1236
|
+
this.layout();
|
|
1237
|
+
if (focusEditor && this.editor) {
|
|
1238
|
+
this.editor.focus();
|
|
1239
|
+
}
|
|
1240
|
+
});
|
|
1241
|
+
|
|
1242
|
+
return true;
|
|
1243
|
+
}
|
|
1244
|
+
|
|
814
1245
|
updateTerminalLayoutButton() {
|
|
815
1246
|
if (!this.terminalLayoutButton) return;
|
|
816
1247
|
|
|
@@ -900,7 +1331,13 @@ class EditorManager {
|
|
|
900
1331
|
}
|
|
901
1332
|
} else if (isFileWorkspaceTabKey(lastNonTerminal)) {
|
|
902
1333
|
const filePath = workspaceKeyToFilePath(lastNonTerminal);
|
|
903
|
-
if (
|
|
1334
|
+
if (
|
|
1335
|
+
session.editorState.openFiles.includes(filePath)
|
|
1336
|
+
&& (
|
|
1337
|
+
!isMarkdownPreviewWorkspaceTabKey(lastNonTerminal)
|
|
1338
|
+
|| isSupportedMarkdownPath(filePath)
|
|
1339
|
+
)
|
|
1340
|
+
) {
|
|
904
1341
|
return lastNonTerminal;
|
|
905
1342
|
}
|
|
906
1343
|
}
|
|
@@ -1445,6 +1882,12 @@ class EditorManager {
|
|
|
1445
1882
|
&& session.editorState.openFiles.includes(
|
|
1446
1883
|
workspaceKeyToFilePath(explicitKey)
|
|
1447
1884
|
)
|
|
1885
|
+
&& (
|
|
1886
|
+
!isMarkdownPreviewWorkspaceTabKey(explicitKey)
|
|
1887
|
+
|| isSupportedMarkdownPath(
|
|
1888
|
+
workspaceKeyToFilePath(explicitKey)
|
|
1889
|
+
)
|
|
1890
|
+
)
|
|
1448
1891
|
) {
|
|
1449
1892
|
return explicitKey;
|
|
1450
1893
|
}
|
|
@@ -1485,82 +1928,555 @@ class EditorManager {
|
|
|
1485
1928
|
store.set(filePath, value);
|
|
1486
1929
|
}
|
|
1487
1930
|
|
|
1488
|
-
|
|
1489
|
-
if (typeof
|
|
1490
|
-
return
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1931
|
+
normalizePendingFileWrite(write, entry = null) {
|
|
1932
|
+
if (write && typeof write === 'object' && !Array.isArray(write)) {
|
|
1933
|
+
return {
|
|
1934
|
+
content: typeof write.content === 'string' ? write.content : '',
|
|
1935
|
+
expectedVersion: typeof write.expectedVersion === 'string'
|
|
1936
|
+
? write.expectedVersion
|
|
1937
|
+
: (
|
|
1938
|
+
typeof entry?.version === 'string'
|
|
1939
|
+
? entry.version
|
|
1940
|
+
: ''
|
|
1941
|
+
),
|
|
1942
|
+
blocked: write.blocked === true,
|
|
1943
|
+
force: write.force === true
|
|
1944
|
+
};
|
|
1500
1945
|
}
|
|
1501
|
-
return
|
|
1946
|
+
return {
|
|
1947
|
+
content: typeof write === 'string' ? write : '',
|
|
1948
|
+
expectedVersion: typeof entry?.version === 'string'
|
|
1949
|
+
? entry.version
|
|
1950
|
+
: '',
|
|
1951
|
+
blocked: false,
|
|
1952
|
+
force: false
|
|
1953
|
+
};
|
|
1502
1954
|
}
|
|
1503
1955
|
|
|
1504
|
-
|
|
1505
|
-
if (!
|
|
1506
|
-
const
|
|
1507
|
-
const
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
isDirectory
|
|
1956
|
+
queuePendingFileWrite(session, filePath, content, overrides = {}) {
|
|
1957
|
+
if (!session || !filePath) return;
|
|
1958
|
+
const pending = getPendingSession(session.key);
|
|
1959
|
+
const entry = this.getModel(filePath, session);
|
|
1960
|
+
const previous = this.normalizePendingFileWrite(
|
|
1961
|
+
pending.fileWrites.get(filePath),
|
|
1962
|
+
entry
|
|
1512
1963
|
);
|
|
1513
|
-
|
|
1964
|
+
pending.fileWrites.set(filePath, {
|
|
1965
|
+
...previous,
|
|
1966
|
+
content,
|
|
1967
|
+
expectedVersion: typeof overrides.expectedVersion === 'string'
|
|
1968
|
+
? overrides.expectedVersion
|
|
1969
|
+
: previous.expectedVersion,
|
|
1970
|
+
blocked: overrides.blocked ?? false,
|
|
1971
|
+
force: overrides.force ?? false
|
|
1972
|
+
});
|
|
1514
1973
|
}
|
|
1515
1974
|
|
|
1516
|
-
|
|
1517
|
-
if (!
|
|
1518
|
-
const
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
} catch {
|
|
1528
|
-
// Ignore content extraction failure and keep cached content.
|
|
1529
|
-
}
|
|
1530
|
-
nextEntry.content = nextContent;
|
|
1975
|
+
getPendingFileWrite(session, filePath) {
|
|
1976
|
+
if (!session || !filePath) return null;
|
|
1977
|
+
const pending = getPendingSession(session.key);
|
|
1978
|
+
if (!pending?.fileWrites?.has(filePath)) {
|
|
1979
|
+
return null;
|
|
1980
|
+
}
|
|
1981
|
+
return this.normalizePendingFileWrite(
|
|
1982
|
+
pending.fileWrites.get(filePath),
|
|
1983
|
+
this.getModel(filePath, session)
|
|
1984
|
+
);
|
|
1985
|
+
}
|
|
1531
1986
|
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
const oldModel = nextEntry.model;
|
|
1537
|
-
const languageId = oldModel.getLanguageId();
|
|
1538
|
-
const uri = this.monacoInstance.Uri.file(nextPath);
|
|
1539
|
-
const existingModel = this.monacoInstance.editor.getModel(uri);
|
|
1540
|
-
if (existingModel && existingModel !== oldModel) {
|
|
1541
|
-
existingModel.setValue(nextContent ?? '');
|
|
1542
|
-
nextEntry.model = existingModel;
|
|
1543
|
-
} else {
|
|
1544
|
-
nextEntry.model = this.monacoInstance.editor.createModel(
|
|
1545
|
-
nextContent ?? '',
|
|
1546
|
-
languageId,
|
|
1547
|
-
uri
|
|
1548
|
-
);
|
|
1549
|
-
}
|
|
1550
|
-
if (nextEntry.model !== oldModel) {
|
|
1551
|
-
try {
|
|
1552
|
-
oldModel.dispose();
|
|
1553
|
-
} catch {
|
|
1554
|
-
// Ignore disposal failures for stale models.
|
|
1555
|
-
}
|
|
1556
|
-
}
|
|
1557
|
-
return nextEntry;
|
|
1558
|
-
}
|
|
1987
|
+
getTextFileEntry(filePath, session = this.currentSession) {
|
|
1988
|
+
const entry = this.getModel(filePath, session);
|
|
1989
|
+
if (!entry || entry.type !== 'text') {
|
|
1990
|
+
return null;
|
|
1559
1991
|
}
|
|
1560
|
-
|
|
1992
|
+
if (typeof entry.contentVersion !== 'string') {
|
|
1993
|
+
entry.contentVersion = typeof entry.version === 'string'
|
|
1994
|
+
? entry.version
|
|
1995
|
+
: '';
|
|
1996
|
+
}
|
|
1997
|
+
return entry;
|
|
1561
1998
|
}
|
|
1562
1999
|
|
|
1563
|
-
|
|
2000
|
+
getCurrentTextFileContent(filePath, session = this.currentSession) {
|
|
2001
|
+
const entry = this.getTextFileEntry(filePath, session);
|
|
2002
|
+
if (!entry) return '';
|
|
2003
|
+
try {
|
|
2004
|
+
if (typeof entry.model?.getValue === 'function') {
|
|
2005
|
+
return entry.model.getValue();
|
|
2006
|
+
}
|
|
2007
|
+
} catch {
|
|
2008
|
+
// Ignore model access failures and fall back to cached content.
|
|
2009
|
+
}
|
|
2010
|
+
return typeof entry.content === 'string' ? entry.content : '';
|
|
2011
|
+
}
|
|
2012
|
+
|
|
2013
|
+
isActiveTextFile(session, filePath) {
|
|
2014
|
+
if (!session || !filePath) return false;
|
|
2015
|
+
if (this.currentSession?.key !== session.key) return false;
|
|
2016
|
+
if (state.activeSessionKey !== session.key) return false;
|
|
2017
|
+
if (session.editorState.activeFilePath !== filePath) return false;
|
|
2018
|
+
return this.getActiveWorkspaceTabKey(session)
|
|
2019
|
+
=== makeFileWorkspaceTabKey(filePath);
|
|
2020
|
+
}
|
|
2021
|
+
|
|
2022
|
+
updateTextFileEntry(filePath, updates, session = this.currentSession) {
|
|
2023
|
+
const entry = this.getTextFileEntry(filePath, session);
|
|
2024
|
+
if (!entry || !updates || typeof updates !== 'object') {
|
|
2025
|
+
return null;
|
|
2026
|
+
}
|
|
2027
|
+
Object.assign(entry, updates);
|
|
2028
|
+
return entry;
|
|
2029
|
+
}
|
|
2030
|
+
|
|
2031
|
+
updateActiveEditorReadOnlyState(session, filePath, readonly) {
|
|
2032
|
+
if (!this.isActiveTextFile(session, filePath) || !this.editor) {
|
|
2033
|
+
return;
|
|
2034
|
+
}
|
|
2035
|
+
this.editor.updateOptions({ readOnly: !!readonly });
|
|
2036
|
+
this.renderEditorTabs();
|
|
2037
|
+
}
|
|
2038
|
+
|
|
2039
|
+
applyProgrammaticTextContent(entry, nextContent) {
|
|
2040
|
+
if (
|
|
2041
|
+
!entry?.model
|
|
2042
|
+
|| typeof entry.model.getValue !== 'function'
|
|
2043
|
+
|| typeof entry.model.setValue !== 'function'
|
|
2044
|
+
) {
|
|
2045
|
+
entry.content = nextContent;
|
|
2046
|
+
return;
|
|
2047
|
+
}
|
|
2048
|
+
const currentValue = entry.model.getValue();
|
|
2049
|
+
if (currentValue === nextContent) {
|
|
2050
|
+
entry.content = nextContent;
|
|
2051
|
+
return;
|
|
2052
|
+
}
|
|
2053
|
+
this.suppressFileWriteCapture = true;
|
|
2054
|
+
try {
|
|
2055
|
+
entry.model.setValue(nextContent);
|
|
2056
|
+
} finally {
|
|
2057
|
+
this.suppressFileWriteCapture = false;
|
|
2058
|
+
}
|
|
2059
|
+
entry.content = nextContent;
|
|
2060
|
+
}
|
|
2061
|
+
|
|
2062
|
+
async readTextFileSnapshot(session, filePath) {
|
|
2063
|
+
if (!session || !filePath) {
|
|
2064
|
+
throw new Error('File path required');
|
|
2065
|
+
}
|
|
2066
|
+
const response = await session.server.fetch(
|
|
2067
|
+
`/api/fs/read?path=${encodeURIComponent(filePath)}`
|
|
2068
|
+
);
|
|
2069
|
+
if (!response.ok) {
|
|
2070
|
+
await throwResponseError(response, 'Failed to read file');
|
|
2071
|
+
}
|
|
2072
|
+
return await response.json();
|
|
2073
|
+
}
|
|
2074
|
+
|
|
2075
|
+
async readTextFileInfo(session, filePath) {
|
|
2076
|
+
if (!session || !filePath) {
|
|
2077
|
+
throw new Error('File path required');
|
|
2078
|
+
}
|
|
2079
|
+
const response = await session.server.fetch(
|
|
2080
|
+
`/api/fs/info?path=${encodeURIComponent(filePath)}`
|
|
2081
|
+
);
|
|
2082
|
+
if (!response.ok) {
|
|
2083
|
+
await throwResponseError(response, 'Failed to inspect file');
|
|
2084
|
+
}
|
|
2085
|
+
return await response.json();
|
|
2086
|
+
}
|
|
2087
|
+
|
|
2088
|
+
applyTextFileSnapshot(session, filePath, snapshot, options = {}) {
|
|
2089
|
+
const entry = this.getTextFileEntry(filePath, session);
|
|
2090
|
+
if (!entry || !snapshot || typeof snapshot !== 'object') {
|
|
2091
|
+
return null;
|
|
2092
|
+
}
|
|
2093
|
+
const useLocalContent = options.useLocalContent === true;
|
|
2094
|
+
const nextReadonly = !!snapshot.readonly;
|
|
2095
|
+
const nextVersion = typeof snapshot.version === 'string'
|
|
2096
|
+
? snapshot.version
|
|
2097
|
+
: entry.version || '';
|
|
2098
|
+
const nextContent = typeof snapshot.content === 'string'
|
|
2099
|
+
? snapshot.content
|
|
2100
|
+
: entry.content || '';
|
|
2101
|
+
|
|
2102
|
+
if (!entry.model && this.monacoInstance) {
|
|
2103
|
+
const uri = this.monacoInstance.Uri.file(filePath);
|
|
2104
|
+
const existing = this.monacoInstance.editor.getModel(uri);
|
|
2105
|
+
entry.model = existing || this.monacoInstance.editor.createModel(
|
|
2106
|
+
typeof entry.content === 'string' ? entry.content : '',
|
|
2107
|
+
undefined,
|
|
2108
|
+
uri
|
|
2109
|
+
);
|
|
2110
|
+
}
|
|
2111
|
+
|
|
2112
|
+
if (!useLocalContent) {
|
|
2113
|
+
const restoreViewState = (
|
|
2114
|
+
this.isActiveTextFile(session, filePath)
|
|
2115
|
+
&& this.editor
|
|
2116
|
+
&& this.editor.getModel?.() === entry.model
|
|
2117
|
+
)
|
|
2118
|
+
? this.editor.saveViewState()
|
|
2119
|
+
: null;
|
|
2120
|
+
this.applyProgrammaticTextContent(entry, nextContent);
|
|
2121
|
+
if (restoreViewState && this.editor) {
|
|
2122
|
+
this.editor.restoreViewState(restoreViewState);
|
|
2123
|
+
}
|
|
2124
|
+
entry.contentVersion = nextVersion;
|
|
2125
|
+
} else if (typeof snapshot.content === 'string') {
|
|
2126
|
+
entry.content = snapshot.content;
|
|
2127
|
+
entry.contentVersion = nextVersion;
|
|
2128
|
+
}
|
|
2129
|
+
|
|
2130
|
+
entry.version = nextVersion;
|
|
2131
|
+
entry.readonly = nextReadonly;
|
|
2132
|
+
entry.size = Number.isFinite(snapshot.size) ? snapshot.size : entry.size;
|
|
2133
|
+
entry.mtimeMs = Number.isFinite(snapshot.mtimeMs)
|
|
2134
|
+
? snapshot.mtimeMs
|
|
2135
|
+
: entry.mtimeMs;
|
|
2136
|
+
entry.lastDismissedRemoteVersion = '';
|
|
2137
|
+
this.updateActiveEditorReadOnlyState(session, filePath, nextReadonly);
|
|
2138
|
+
if (
|
|
2139
|
+
this.currentSession?.key === session.key
|
|
2140
|
+
&& isSupportedMarkdownPath(filePath)
|
|
2141
|
+
&& this.currentSession.editorState.activeFilePath === filePath
|
|
2142
|
+
) {
|
|
2143
|
+
this.scheduleMarkdownPreviewRender(filePath, session);
|
|
2144
|
+
}
|
|
2145
|
+
return entry;
|
|
2146
|
+
}
|
|
2147
|
+
|
|
2148
|
+
getFileConflictDialogKey(session, filePath, version, source) {
|
|
2149
|
+
return [
|
|
2150
|
+
session?.key || '',
|
|
2151
|
+
filePath || '',
|
|
2152
|
+
version || '',
|
|
2153
|
+
source || ''
|
|
2154
|
+
].join(':');
|
|
2155
|
+
}
|
|
2156
|
+
|
|
2157
|
+
async promptTextFileConflict(session, filePath, snapshot, source) {
|
|
2158
|
+
if (!session || !filePath || !snapshot) {
|
|
2159
|
+
return 'dismiss';
|
|
2160
|
+
}
|
|
2161
|
+
const version = typeof snapshot.version === 'string'
|
|
2162
|
+
? snapshot.version
|
|
2163
|
+
: '';
|
|
2164
|
+
const dialogKey = this.getFileConflictDialogKey(
|
|
2165
|
+
session,
|
|
2166
|
+
filePath,
|
|
2167
|
+
version,
|
|
2168
|
+
source
|
|
2169
|
+
);
|
|
2170
|
+
if (this.fileConflictDialogKey === dialogKey) {
|
|
2171
|
+
return 'dismiss';
|
|
2172
|
+
}
|
|
2173
|
+
this.fileConflictDialogKey = dialogKey;
|
|
2174
|
+
const fileName = filePath.split('/').pop() || filePath;
|
|
2175
|
+
const keepLocal = await showConfirmModal({
|
|
2176
|
+
title: source === 'save-conflict'
|
|
2177
|
+
? 'Save Conflict'
|
|
2178
|
+
: 'File Changed on Disk',
|
|
2179
|
+
message: source === 'save-conflict'
|
|
2180
|
+
? `“${fileName}” changed on disk before Tabminal could save it.`
|
|
2181
|
+
: `“${fileName}” was modified outside Tabminal.`,
|
|
2182
|
+
note: 'Use Remote reloads the disk version. Use Local keeps your '
|
|
2183
|
+
+ 'current editor contents and overwrites the remote change '
|
|
2184
|
+
+ 'on the next save.',
|
|
2185
|
+
confirmLabel: 'Use Local',
|
|
2186
|
+
cancelLabel: 'Use Remote',
|
|
2187
|
+
preferredFocus: 'cancel',
|
|
2188
|
+
allowDismiss: false,
|
|
2189
|
+
returnFocus: this.isActiveTextFile(session, filePath)
|
|
2190
|
+
? this.monacoContainer
|
|
2191
|
+
: document.activeElement
|
|
2192
|
+
});
|
|
2193
|
+
this.fileConflictDialogKey = '';
|
|
2194
|
+
return keepLocal ? 'local' : 'remote';
|
|
2195
|
+
}
|
|
2196
|
+
|
|
2197
|
+
async resolveTextFileConflict(session, filePath, snapshot, source) {
|
|
2198
|
+
const entry = this.getTextFileEntry(filePath, session);
|
|
2199
|
+
if (!entry || !snapshot) {
|
|
2200
|
+
return;
|
|
2201
|
+
}
|
|
2202
|
+
const decision = await this.promptTextFileConflict(
|
|
2203
|
+
session,
|
|
2204
|
+
filePath,
|
|
2205
|
+
snapshot,
|
|
2206
|
+
source
|
|
2207
|
+
);
|
|
2208
|
+
if (decision === 'remote') {
|
|
2209
|
+
const remoteSnapshot = typeof snapshot.content === 'string'
|
|
2210
|
+
? snapshot
|
|
2211
|
+
: await this.readTextFileSnapshot(session, filePath);
|
|
2212
|
+
this.applyTextFileSnapshot(session, filePath, remoteSnapshot);
|
|
2213
|
+
this.clearPendingFileWrite(session.key, filePath);
|
|
2214
|
+
if (this.isActiveTextFile(session, filePath)) {
|
|
2215
|
+
this.renderEditorTabs();
|
|
2216
|
+
}
|
|
2217
|
+
return;
|
|
2218
|
+
}
|
|
2219
|
+
|
|
2220
|
+
if (decision === 'local') {
|
|
2221
|
+
const currentContent = this.getCurrentTextFileContent(
|
|
2222
|
+
filePath,
|
|
2223
|
+
session
|
|
2224
|
+
);
|
|
2225
|
+
this.applyTextFileSnapshot(session, filePath, snapshot, {
|
|
2226
|
+
useLocalContent: true
|
|
2227
|
+
});
|
|
2228
|
+
this.queuePendingFileWrite(session, filePath, currentContent, {
|
|
2229
|
+
expectedVersion: typeof snapshot.version === 'string'
|
|
2230
|
+
? snapshot.version
|
|
2231
|
+
: entry.version || '',
|
|
2232
|
+
blocked: false,
|
|
2233
|
+
force: false
|
|
2234
|
+
});
|
|
2235
|
+
requestImmediateServerSync(session.server, 0);
|
|
2236
|
+
}
|
|
2237
|
+
}
|
|
2238
|
+
|
|
2239
|
+
async applyFileWriteResults(server, sessionResults, sentFileWrites) {
|
|
2240
|
+
if (!server || !Array.isArray(sessionResults)) {
|
|
2241
|
+
return;
|
|
2242
|
+
}
|
|
2243
|
+
for (const update of sessionResults) {
|
|
2244
|
+
const session = state.sessions.get(
|
|
2245
|
+
makeSessionKey(server.id, update?.id)
|
|
2246
|
+
);
|
|
2247
|
+
if (!session || !Array.isArray(update?.fileWrites)) {
|
|
2248
|
+
continue;
|
|
2249
|
+
}
|
|
2250
|
+
for (const result of update.fileWrites) {
|
|
2251
|
+
const filePath = typeof result?.path === 'string'
|
|
2252
|
+
? result.path
|
|
2253
|
+
: '';
|
|
2254
|
+
if (!filePath) continue;
|
|
2255
|
+
const entry = this.getTextFileEntry(filePath, session);
|
|
2256
|
+
const sentWrite = sentFileWrites?.get(update.id)?.get(filePath)
|
|
2257
|
+
|| null;
|
|
2258
|
+
if (!entry) {
|
|
2259
|
+
this.clearPendingFileWrite(session.key, filePath);
|
|
2260
|
+
continue;
|
|
2261
|
+
}
|
|
2262
|
+
if (result.status === 'ok') {
|
|
2263
|
+
const currentWrite = this.getPendingFileWrite(
|
|
2264
|
+
session,
|
|
2265
|
+
filePath
|
|
2266
|
+
);
|
|
2267
|
+
const sentContent = sentWrite?.content
|
|
2268
|
+
?? this.getCurrentTextFileContent(filePath, session);
|
|
2269
|
+
entry.content = sentContent;
|
|
2270
|
+
entry.version = typeof result.version === 'string'
|
|
2271
|
+
? result.version
|
|
2272
|
+
: entry.version || '';
|
|
2273
|
+
entry.contentVersion = entry.version;
|
|
2274
|
+
entry.readonly = !!result.readonly;
|
|
2275
|
+
entry.lastDismissedRemoteVersion = '';
|
|
2276
|
+
const hasNewerPendingWrite = !!(
|
|
2277
|
+
currentWrite
|
|
2278
|
+
&& sentWrite
|
|
2279
|
+
&& (
|
|
2280
|
+
currentWrite.content !== sentWrite.content
|
|
2281
|
+
|| currentWrite.expectedVersion
|
|
2282
|
+
!== sentWrite.expectedVersion
|
|
2283
|
+
|| currentWrite.force !== sentWrite.force
|
|
2284
|
+
)
|
|
2285
|
+
);
|
|
2286
|
+
if (hasNewerPendingWrite) {
|
|
2287
|
+
this.queuePendingFileWrite(
|
|
2288
|
+
session,
|
|
2289
|
+
filePath,
|
|
2290
|
+
currentWrite.content,
|
|
2291
|
+
{
|
|
2292
|
+
expectedVersion: entry.version,
|
|
2293
|
+
blocked: false,
|
|
2294
|
+
force: currentWrite.force
|
|
2295
|
+
}
|
|
2296
|
+
);
|
|
2297
|
+
} else {
|
|
2298
|
+
this.clearPendingFileWrite(session.key, filePath);
|
|
2299
|
+
}
|
|
2300
|
+
this.updateActiveEditorReadOnlyState(
|
|
2301
|
+
session,
|
|
2302
|
+
filePath,
|
|
2303
|
+
entry.readonly
|
|
2304
|
+
);
|
|
2305
|
+
continue;
|
|
2306
|
+
}
|
|
2307
|
+
if (result.status === 'conflict') {
|
|
2308
|
+
this.queuePendingFileWrite(
|
|
2309
|
+
session,
|
|
2310
|
+
filePath,
|
|
2311
|
+
this.getCurrentTextFileContent(filePath, session),
|
|
2312
|
+
{
|
|
2313
|
+
expectedVersion: typeof result.version === 'string'
|
|
2314
|
+
? result.version
|
|
2315
|
+
: entry.version || '',
|
|
2316
|
+
blocked: true,
|
|
2317
|
+
force: false
|
|
2318
|
+
}
|
|
2319
|
+
);
|
|
2320
|
+
await this.resolveTextFileConflict(
|
|
2321
|
+
session,
|
|
2322
|
+
filePath,
|
|
2323
|
+
result,
|
|
2324
|
+
'save-conflict'
|
|
2325
|
+
);
|
|
2326
|
+
continue;
|
|
2327
|
+
}
|
|
2328
|
+
this.queuePendingFileWrite(
|
|
2329
|
+
session,
|
|
2330
|
+
filePath,
|
|
2331
|
+
this.getCurrentTextFileContent(filePath, session),
|
|
2332
|
+
{
|
|
2333
|
+
blocked: true
|
|
2334
|
+
}
|
|
2335
|
+
);
|
|
2336
|
+
alert(result?.error || 'Failed to save file.', {
|
|
2337
|
+
type: 'error',
|
|
2338
|
+
title: 'Save Error'
|
|
2339
|
+
});
|
|
2340
|
+
}
|
|
2341
|
+
}
|
|
2342
|
+
}
|
|
2343
|
+
|
|
2344
|
+
async checkActiveFileVersion() {
|
|
2345
|
+
if (
|
|
2346
|
+
this.fileVersionCheckPromise
|
|
2347
|
+
|| document.visibilityState === 'hidden'
|
|
2348
|
+
|| isConfirmModalOpen()
|
|
2349
|
+
) {
|
|
2350
|
+
return;
|
|
2351
|
+
}
|
|
2352
|
+
const session = this.currentSession;
|
|
2353
|
+
const filePath = session?.editorState?.activeFilePath || '';
|
|
2354
|
+
if (!this.isActiveTextFile(session, filePath)) {
|
|
2355
|
+
return;
|
|
2356
|
+
}
|
|
2357
|
+
const entry = this.getTextFileEntry(filePath, session);
|
|
2358
|
+
if (!entry || entry.readonly) {
|
|
2359
|
+
return;
|
|
2360
|
+
}
|
|
2361
|
+
this.fileVersionCheckPromise = (async () => {
|
|
2362
|
+
try {
|
|
2363
|
+
const info = await this.readTextFileInfo(session, filePath);
|
|
2364
|
+
if (
|
|
2365
|
+
!info
|
|
2366
|
+
|| typeof info.version !== 'string'
|
|
2367
|
+
|| !info.version
|
|
2368
|
+
|| info.version === entry.version
|
|
2369
|
+
|| info.version === entry.lastDismissedRemoteVersion
|
|
2370
|
+
) {
|
|
2371
|
+
return;
|
|
2372
|
+
}
|
|
2373
|
+
const pendingWrite = this.getPendingFileWrite(session, filePath);
|
|
2374
|
+
if (pendingWrite?.blocked) {
|
|
2375
|
+
return;
|
|
2376
|
+
}
|
|
2377
|
+
await this.resolveTextFileConflict(
|
|
2378
|
+
session,
|
|
2379
|
+
filePath,
|
|
2380
|
+
info,
|
|
2381
|
+
'remote-change'
|
|
2382
|
+
);
|
|
2383
|
+
} catch (error) {
|
|
2384
|
+
console.warn('Failed to check file version:', error);
|
|
2385
|
+
}
|
|
2386
|
+
})();
|
|
2387
|
+
try {
|
|
2388
|
+
await this.fileVersionCheckPromise;
|
|
2389
|
+
} finally {
|
|
2390
|
+
this.fileVersionCheckPromise = null;
|
|
2391
|
+
}
|
|
2392
|
+
}
|
|
2393
|
+
|
|
2394
|
+
clearPendingFileWrite(sessionKey, filePath) {
|
|
2395
|
+
const pending = pendingChanges.sessions.get(sessionKey);
|
|
2396
|
+
pending?.fileWrites?.delete(filePath);
|
|
2397
|
+
}
|
|
2398
|
+
|
|
2399
|
+
remapTreePath(pathValue, oldPath, newPath, isDirectory) {
|
|
2400
|
+
if (typeof pathValue !== 'string' || pathValue.length === 0) {
|
|
2401
|
+
return pathValue;
|
|
2402
|
+
}
|
|
2403
|
+
if (pathValue === oldPath) {
|
|
2404
|
+
return newPath;
|
|
2405
|
+
}
|
|
2406
|
+
if (
|
|
2407
|
+
isDirectory
|
|
2408
|
+
&& pathValue.startsWith(`${oldPath}/`)
|
|
2409
|
+
) {
|
|
2410
|
+
return `${newPath}${pathValue.slice(oldPath.length)}`;
|
|
2411
|
+
}
|
|
2412
|
+
return pathValue;
|
|
2413
|
+
}
|
|
2414
|
+
|
|
2415
|
+
remapWorkspaceTabKey(key, oldPath, newPath, isDirectory) {
|
|
2416
|
+
if (!isFileWorkspaceTabKey(key)) return key;
|
|
2417
|
+
const filePath = workspaceKeyToFilePath(key);
|
|
2418
|
+
const nextPath = this.remapTreePath(
|
|
2419
|
+
filePath,
|
|
2420
|
+
oldPath,
|
|
2421
|
+
newPath,
|
|
2422
|
+
isDirectory
|
|
2423
|
+
);
|
|
2424
|
+
if (!nextPath) {
|
|
2425
|
+
return key;
|
|
2426
|
+
}
|
|
2427
|
+
return isMarkdownPreviewWorkspaceTabKey(key)
|
|
2428
|
+
? makeMarkdownPreviewWorkspaceTabKey(nextPath)
|
|
2429
|
+
: makeFileWorkspaceTabKey(nextPath);
|
|
2430
|
+
}
|
|
2431
|
+
|
|
2432
|
+
cloneRenamedModelEntry(entry, nextPath) {
|
|
2433
|
+
if (!entry || typeof entry !== 'object') return entry;
|
|
2434
|
+
const nextEntry = {
|
|
2435
|
+
...entry
|
|
2436
|
+
};
|
|
2437
|
+
if (nextEntry.model) {
|
|
2438
|
+
let nextContent = nextEntry.content;
|
|
2439
|
+
try {
|
|
2440
|
+
if (typeof nextEntry.model.getValue === 'function') {
|
|
2441
|
+
nextContent = nextEntry.model.getValue();
|
|
2442
|
+
}
|
|
2443
|
+
} catch {
|
|
2444
|
+
// Ignore content extraction failure and keep cached content.
|
|
2445
|
+
}
|
|
2446
|
+
nextEntry.content = nextContent;
|
|
2447
|
+
|
|
2448
|
+
if (
|
|
2449
|
+
this.monacoInstance
|
|
2450
|
+
&& typeof nextEntry.model.getLanguageId === 'function'
|
|
2451
|
+
) {
|
|
2452
|
+
const oldModel = nextEntry.model;
|
|
2453
|
+
const languageId = oldModel.getLanguageId();
|
|
2454
|
+
const uri = this.monacoInstance.Uri.file(nextPath);
|
|
2455
|
+
const existingModel = this.monacoInstance.editor.getModel(uri);
|
|
2456
|
+
if (existingModel && existingModel !== oldModel) {
|
|
2457
|
+
existingModel.setValue(nextContent ?? '');
|
|
2458
|
+
nextEntry.model = existingModel;
|
|
2459
|
+
} else {
|
|
2460
|
+
nextEntry.model = this.monacoInstance.editor.createModel(
|
|
2461
|
+
nextContent ?? '',
|
|
2462
|
+
languageId,
|
|
2463
|
+
uri
|
|
2464
|
+
);
|
|
2465
|
+
}
|
|
2466
|
+
if (nextEntry.model !== oldModel) {
|
|
2467
|
+
try {
|
|
2468
|
+
oldModel.dispose();
|
|
2469
|
+
} catch {
|
|
2470
|
+
// Ignore disposal failures for stale models.
|
|
2471
|
+
}
|
|
2472
|
+
}
|
|
2473
|
+
return nextEntry;
|
|
2474
|
+
}
|
|
2475
|
+
}
|
|
2476
|
+
return nextEntry;
|
|
2477
|
+
}
|
|
2478
|
+
|
|
2479
|
+
remapModelStorePaths(server, oldPath, newPath, isDirectory) {
|
|
1564
2480
|
if (!server?.modelStore) return false;
|
|
1565
2481
|
const nextEntries = [];
|
|
1566
2482
|
let changed = false;
|
|
@@ -1712,6 +2628,17 @@ class EditorManager {
|
|
|
1712
2628
|
visualChanged = true;
|
|
1713
2629
|
}
|
|
1714
2630
|
|
|
2631
|
+
const nextMarkdownSplitPath = this.remapTreePath(
|
|
2632
|
+
this.getMarkdownSplitPath(session),
|
|
2633
|
+
oldPath,
|
|
2634
|
+
newPath,
|
|
2635
|
+
isDirectory
|
|
2636
|
+
);
|
|
2637
|
+
if (nextMarkdownSplitPath !== this.getMarkdownSplitPath(session)) {
|
|
2638
|
+
session.workspaceState.markdownSplitPath = nextMarkdownSplitPath;
|
|
2639
|
+
visualChanged = true;
|
|
2640
|
+
}
|
|
2641
|
+
|
|
1715
2642
|
const nextActiveTabKey = this.remapWorkspaceTabKey(
|
|
1716
2643
|
session.workspaceState.activeTabKey,
|
|
1717
2644
|
oldPath,
|
|
@@ -1840,6 +2767,17 @@ class EditorManager {
|
|
|
1840
2767
|
visualChanged = true;
|
|
1841
2768
|
}
|
|
1842
2769
|
|
|
2770
|
+
if (
|
|
2771
|
+
this.pathMatchesTarget(
|
|
2772
|
+
this.getMarkdownSplitPath(session),
|
|
2773
|
+
targetPath,
|
|
2774
|
+
isDirectory
|
|
2775
|
+
)
|
|
2776
|
+
) {
|
|
2777
|
+
session.workspaceState.markdownSplitPath = '';
|
|
2778
|
+
visualChanged = true;
|
|
2779
|
+
}
|
|
2780
|
+
|
|
1843
2781
|
if (session.editorState.viewStates.size > 0) {
|
|
1844
2782
|
const nextViewStates = new Map();
|
|
1845
2783
|
let changed = false;
|
|
@@ -2138,24 +3076,7 @@ class EditorManager {
|
|
|
2138
3076
|
}
|
|
2139
3077
|
|
|
2140
3078
|
refreshSessionTree(session) {
|
|
2141
|
-
|
|
2142
|
-
session.fileTreeRenderToken = (session.fileTreeRenderToken || 0) + 1;
|
|
2143
|
-
const renderToken = session.fileTreeRenderToken;
|
|
2144
|
-
const scrollTop = session.fileTreeElement.scrollTop;
|
|
2145
|
-
void this.renderTree(
|
|
2146
|
-
session.cwd,
|
|
2147
|
-
session.fileTreeElement,
|
|
2148
|
-
session,
|
|
2149
|
-
renderToken
|
|
2150
|
-
).finally(() => {
|
|
2151
|
-
if (
|
|
2152
|
-
session.fileTreeElement
|
|
2153
|
-
&& session.fileTreeRenderToken === renderToken
|
|
2154
|
-
) {
|
|
2155
|
-
session.fileTreeElement.scrollTop = scrollTop;
|
|
2156
|
-
}
|
|
2157
|
-
});
|
|
2158
|
-
this.updateTreeAutoRefresh();
|
|
3079
|
+
this.requestSessionTreeRefresh(session);
|
|
2159
3080
|
}
|
|
2160
3081
|
|
|
2161
3082
|
isSessionTreeVisible(session) {
|
|
@@ -2166,29 +3087,186 @@ class EditorManager {
|
|
|
2166
3087
|
return this.isSessionTreeVisible(session) && !session.treeEditingPath;
|
|
2167
3088
|
}
|
|
2168
3089
|
|
|
2169
|
-
refreshVisibleSessionTrees() {
|
|
2170
|
-
|
|
2171
|
-
|
|
2172
|
-
|
|
3090
|
+
refreshVisibleSessionTrees() {
|
|
3091
|
+
this.requestVisibleTreeRefresh();
|
|
3092
|
+
}
|
|
3093
|
+
|
|
3094
|
+
requestSessionTreeRefresh(session, { force = false } = {}) {
|
|
3095
|
+
if (!session?.fileTreeElement) {
|
|
3096
|
+
this.updateTreeAutoRefresh();
|
|
3097
|
+
return;
|
|
3098
|
+
}
|
|
3099
|
+
if (!force && !this.canRefreshSessionTree(session)) {
|
|
3100
|
+
this.updateTreeAutoRefresh();
|
|
3101
|
+
return;
|
|
3102
|
+
}
|
|
3103
|
+
if (force) {
|
|
3104
|
+
this.pendingForcedTreeRefreshSessions.add(session.key);
|
|
3105
|
+
}
|
|
3106
|
+
this.scheduleTreeRefreshBatch();
|
|
3107
|
+
}
|
|
3108
|
+
|
|
3109
|
+
requestVisibleTreeRefresh() {
|
|
3110
|
+
this.scheduleTreeRefreshBatch();
|
|
3111
|
+
}
|
|
3112
|
+
|
|
3113
|
+
scheduleTreeRefreshBatch() {
|
|
3114
|
+
if (this.treeRefreshBatchQueued) {
|
|
3115
|
+
return;
|
|
3116
|
+
}
|
|
3117
|
+
this.treeRefreshBatchQueued = true;
|
|
3118
|
+
requestAnimationFrame(() => {
|
|
3119
|
+
this.treeRefreshBatchQueued = false;
|
|
3120
|
+
void this.flushTreeRefreshBatch();
|
|
3121
|
+
});
|
|
3122
|
+
}
|
|
3123
|
+
|
|
3124
|
+
getTreeRefreshRequestKey(server, dirPath) {
|
|
3125
|
+
return `${server?.id || 'main'}:${dirPath}`;
|
|
3126
|
+
}
|
|
3127
|
+
|
|
3128
|
+
getSessionTreeRefreshPaths(session) {
|
|
3129
|
+
if (!session?.cwd) {
|
|
3130
|
+
return [];
|
|
3131
|
+
}
|
|
3132
|
+
return uniqueStringList([
|
|
3133
|
+
session.cwd,
|
|
3134
|
+
...(session.sharedWorkspaceState?.expandedPaths || [])
|
|
3135
|
+
]);
|
|
3136
|
+
}
|
|
3137
|
+
|
|
3138
|
+
collectTreeRefreshSessions() {
|
|
3139
|
+
const sessions = [];
|
|
3140
|
+
for (const session of state.sessions.values()) {
|
|
3141
|
+
if (this.canRefreshSessionTree(session)) {
|
|
3142
|
+
sessions.push(session);
|
|
3143
|
+
continue;
|
|
3144
|
+
}
|
|
3145
|
+
if (
|
|
3146
|
+
this.pendingForcedTreeRefreshSessions.has(session.key)
|
|
3147
|
+
&& this.isSessionTreeVisible(session)
|
|
3148
|
+
) {
|
|
3149
|
+
sessions.push(session);
|
|
3150
|
+
}
|
|
3151
|
+
}
|
|
3152
|
+
return sessions;
|
|
3153
|
+
}
|
|
3154
|
+
|
|
3155
|
+
async fetchTreeDirectoryListing(server, dirPath) {
|
|
3156
|
+
const key = this.getTreeRefreshRequestKey(server, dirPath);
|
|
3157
|
+
const existing = this.treeDirectoryFetches.get(key);
|
|
3158
|
+
if (existing) {
|
|
3159
|
+
return existing;
|
|
3160
|
+
}
|
|
3161
|
+
|
|
3162
|
+
const request = (async () => {
|
|
3163
|
+
const response = await server.fetch(
|
|
3164
|
+
`/api/fs/list?path=${encodeURIComponent(dirPath)}`
|
|
3165
|
+
);
|
|
3166
|
+
if (!response.ok) {
|
|
3167
|
+
throw new Error(`Failed to list path: ${dirPath}`);
|
|
3168
|
+
}
|
|
3169
|
+
const payload = await response.json();
|
|
3170
|
+
return {
|
|
3171
|
+
files: Array.isArray(payload)
|
|
3172
|
+
? payload
|
|
3173
|
+
: Array.isArray(payload?.items)
|
|
3174
|
+
? payload.items
|
|
3175
|
+
: [],
|
|
3176
|
+
creatable: Array.isArray(payload)
|
|
3177
|
+
? false
|
|
3178
|
+
: !!payload?.creatable
|
|
3179
|
+
};
|
|
3180
|
+
})().finally(() => {
|
|
3181
|
+
this.treeDirectoryFetches.delete(key);
|
|
3182
|
+
});
|
|
3183
|
+
|
|
3184
|
+
this.treeDirectoryFetches.set(key, request);
|
|
3185
|
+
return request;
|
|
3186
|
+
}
|
|
3187
|
+
|
|
3188
|
+
async flushTreeRefreshBatch() {
|
|
3189
|
+
if (this.treeRefreshInFlight) {
|
|
3190
|
+
this.treeRefreshRerunRequested = true;
|
|
3191
|
+
return;
|
|
3192
|
+
}
|
|
3193
|
+
|
|
3194
|
+
const sessions = this.collectTreeRefreshSessions();
|
|
3195
|
+
this.pendingForcedTreeRefreshSessions.clear();
|
|
3196
|
+
if (sessions.length === 0) {
|
|
3197
|
+
this.updateTreeAutoRefresh();
|
|
3198
|
+
return;
|
|
3199
|
+
}
|
|
3200
|
+
|
|
3201
|
+
this.treeRefreshInFlight = true;
|
|
3202
|
+
const renderPlans = sessions.map((session) => {
|
|
3203
|
+
session.fileTreeRenderToken = (session.fileTreeRenderToken || 0) + 1;
|
|
3204
|
+
return {
|
|
3205
|
+
session,
|
|
3206
|
+
renderToken: session.fileTreeRenderToken,
|
|
3207
|
+
scrollTop: session.fileTreeElement?.scrollTop || 0
|
|
3208
|
+
};
|
|
3209
|
+
});
|
|
3210
|
+
|
|
3211
|
+
const requestEntries = new Map();
|
|
3212
|
+
for (const { session } of renderPlans) {
|
|
3213
|
+
for (const dirPath of this.getSessionTreeRefreshPaths(session)) {
|
|
3214
|
+
requestEntries.set(
|
|
3215
|
+
this.getTreeRefreshRequestKey(session.server, dirPath),
|
|
3216
|
+
{
|
|
3217
|
+
server: session.server,
|
|
3218
|
+
dirPath
|
|
3219
|
+
}
|
|
3220
|
+
);
|
|
3221
|
+
}
|
|
3222
|
+
}
|
|
3223
|
+
|
|
3224
|
+
const directorySnapshots = new Map();
|
|
3225
|
+
await Promise.all(
|
|
3226
|
+
Array.from(requestEntries.entries()).map(
|
|
3227
|
+
async ([key, entry]) => {
|
|
3228
|
+
try {
|
|
3229
|
+
directorySnapshots.set(
|
|
3230
|
+
key,
|
|
3231
|
+
await this.fetchTreeDirectoryListing(
|
|
3232
|
+
entry.server,
|
|
3233
|
+
entry.dirPath
|
|
3234
|
+
)
|
|
3235
|
+
);
|
|
3236
|
+
} catch (error) {
|
|
3237
|
+
console.error(
|
|
3238
|
+
'Failed to fetch tree directory:',
|
|
3239
|
+
entry.dirPath,
|
|
3240
|
+
error
|
|
3241
|
+
);
|
|
3242
|
+
}
|
|
3243
|
+
}
|
|
3244
|
+
)
|
|
3245
|
+
);
|
|
3246
|
+
|
|
3247
|
+
for (const plan of renderPlans) {
|
|
3248
|
+
const { session, renderToken, scrollTop } = plan;
|
|
3249
|
+
if (!session.fileTreeElement) {
|
|
3250
|
+
continue;
|
|
3251
|
+
}
|
|
3252
|
+
this.renderTreeFromSnapshots(
|
|
3253
|
+
session.cwd,
|
|
3254
|
+
session.fileTreeElement,
|
|
3255
|
+
session,
|
|
3256
|
+
directorySnapshots,
|
|
3257
|
+
renderToken
|
|
3258
|
+
);
|
|
3259
|
+
if (session.fileTreeRenderToken === renderToken) {
|
|
3260
|
+
session.fileTreeElement.scrollTop = scrollTop;
|
|
2173
3261
|
}
|
|
2174
3262
|
}
|
|
2175
|
-
}
|
|
2176
3263
|
|
|
2177
|
-
|
|
2178
|
-
|
|
2179
|
-
|
|
2180
|
-
|
|
3264
|
+
this.treeRefreshInFlight = false;
|
|
3265
|
+
this.updateTreeAutoRefresh();
|
|
3266
|
+
if (this.treeRefreshRerunRequested) {
|
|
3267
|
+
this.treeRefreshRerunRequested = false;
|
|
3268
|
+
this.scheduleTreeRefreshBatch();
|
|
2181
3269
|
}
|
|
2182
|
-
if (session.fileTreeRefreshQueued) return;
|
|
2183
|
-
session.fileTreeRefreshQueued = true;
|
|
2184
|
-
requestAnimationFrame(() => {
|
|
2185
|
-
session.fileTreeRefreshQueued = false;
|
|
2186
|
-
if (force || this.canRefreshSessionTree(session)) {
|
|
2187
|
-
this.refreshSessionTree(session);
|
|
2188
|
-
} else {
|
|
2189
|
-
this.updateTreeAutoRefresh();
|
|
2190
|
-
}
|
|
2191
|
-
});
|
|
2192
3270
|
}
|
|
2193
3271
|
|
|
2194
3272
|
updateTreeAutoRefresh() {
|
|
@@ -2198,7 +3276,7 @@ class EditorManager {
|
|
|
2198
3276
|
(session) => this.canRefreshSessionTree(session)
|
|
2199
3277
|
)
|
|
2200
3278
|
);
|
|
2201
|
-
|
|
3279
|
+
if (shouldRun && !this.treeRefreshTimer) {
|
|
2202
3280
|
this.treeRefreshTimer = window.setInterval(() => {
|
|
2203
3281
|
if (document.visibilityState !== 'visible') {
|
|
2204
3282
|
this.updateTreeAutoRefresh();
|
|
@@ -2211,7 +3289,7 @@ class EditorManager {
|
|
|
2211
3289
|
this.updateTreeAutoRefresh();
|
|
2212
3290
|
return;
|
|
2213
3291
|
}
|
|
2214
|
-
this.
|
|
3292
|
+
this.requestVisibleTreeRefresh();
|
|
2215
3293
|
}, FILE_TREE_REFRESH_INTERVAL_MS);
|
|
2216
3294
|
return;
|
|
2217
3295
|
}
|
|
@@ -2405,7 +3483,7 @@ class EditorManager {
|
|
|
2405
3483
|
isDirectory: !!payload.isDirectory,
|
|
2406
3484
|
renameable: true
|
|
2407
3485
|
});
|
|
2408
|
-
} catch (
|
|
3486
|
+
} catch (_error) {
|
|
2409
3487
|
alert(error.message || 'Failed to create path', {
|
|
2410
3488
|
type: 'error',
|
|
2411
3489
|
title: 'Files'
|
|
@@ -2477,7 +3555,7 @@ class EditorManager {
|
|
|
2477
3555
|
);
|
|
2478
3556
|
this.requestSessionTreeRefresh(session);
|
|
2479
3557
|
session.fileTreeElement?.focus({ preventScroll: true });
|
|
2480
|
-
} catch (
|
|
3558
|
+
} catch (_error) {
|
|
2481
3559
|
alert(error.message || 'Failed to delete path', {
|
|
2482
3560
|
type: 'error',
|
|
2483
3561
|
title: 'Files'
|
|
@@ -2652,7 +3730,7 @@ class EditorManager {
|
|
|
2652
3730
|
list.appendChild(row);
|
|
2653
3731
|
}
|
|
2654
3732
|
|
|
2655
|
-
updateTreeItem(li, file, session
|
|
3733
|
+
updateTreeItem(li, file, session) {
|
|
2656
3734
|
li.dataset.path = file.path;
|
|
2657
3735
|
li.dataset.isDirectory = file.isDirectory ? '1' : '0';
|
|
2658
3736
|
li.dataset.renameable = file.renameable ? '1' : '0';
|
|
@@ -2870,7 +3948,7 @@ class EditorManager {
|
|
|
2870
3948
|
});
|
|
2871
3949
|
|
|
2872
3950
|
icon.innerHTML = this.getIcon(file.name, true, true);
|
|
2873
|
-
|
|
3951
|
+
this.requestSessionTreeRefresh(session);
|
|
2874
3952
|
this.updateTreeAutoRefresh();
|
|
2875
3953
|
session.fileTreeElement?.focus({ preventScroll: true });
|
|
2876
3954
|
return;
|
|
@@ -2914,7 +3992,7 @@ class EditorManager {
|
|
|
2914
3992
|
}
|
|
2915
3993
|
}
|
|
2916
3994
|
|
|
2917
|
-
reconcileTreeList(list, dirPath, files, creatable, session
|
|
3995
|
+
reconcileTreeList(list, dirPath, files, creatable, session) {
|
|
2918
3996
|
const existingItems = new Map();
|
|
2919
3997
|
Array.from(list.children).forEach((child) => {
|
|
2920
3998
|
if (child.tagName === 'LI' && child.dataset.path) {
|
|
@@ -2930,7 +4008,7 @@ class EditorManager {
|
|
|
2930
4008
|
} else {
|
|
2931
4009
|
existingItems.delete(file.path);
|
|
2932
4010
|
}
|
|
2933
|
-
this.updateTreeItem(li, file, session
|
|
4011
|
+
this.updateTreeItem(li, file, session);
|
|
2934
4012
|
orderedItems.push(li);
|
|
2935
4013
|
}
|
|
2936
4014
|
|
|
@@ -2962,12 +4040,32 @@ class EditorManager {
|
|
|
2962
4040
|
});
|
|
2963
4041
|
|
|
2964
4042
|
this.editor.onDidChangeModelContent(() => {
|
|
4043
|
+
if (this.suppressFileWriteCapture) return;
|
|
2965
4044
|
if (!this.currentSession) return;
|
|
2966
4045
|
const filePath = this.currentSession.editorState.activeFilePath;
|
|
2967
4046
|
if (!filePath) return;
|
|
2968
|
-
|
|
2969
|
-
|
|
2970
|
-
|
|
4047
|
+
const entry = this.getTextFileEntry(filePath, this.currentSession);
|
|
4048
|
+
if (!entry) return;
|
|
4049
|
+
const nextContent = this.editor.getValue();
|
|
4050
|
+
if (
|
|
4051
|
+
nextContent === (entry.content || '')
|
|
4052
|
+
&& (entry.contentVersion || '') === (entry.version || '')
|
|
4053
|
+
) {
|
|
4054
|
+
this.clearPendingFileWrite(this.currentSession.key, filePath);
|
|
4055
|
+
return;
|
|
4056
|
+
}
|
|
4057
|
+
entry.lastDismissedRemoteVersion = '';
|
|
4058
|
+
this.queuePendingFileWrite(
|
|
4059
|
+
this.currentSession,
|
|
4060
|
+
filePath,
|
|
4061
|
+
nextContent
|
|
4062
|
+
);
|
|
4063
|
+
if (isSupportedMarkdownPath(filePath)) {
|
|
4064
|
+
this.scheduleMarkdownPreviewRender(
|
|
4065
|
+
filePath,
|
|
4066
|
+
this.currentSession
|
|
4067
|
+
);
|
|
4068
|
+
}
|
|
2971
4069
|
});
|
|
2972
4070
|
|
|
2973
4071
|
monaco.editor.defineTheme('solarized-dark', {
|
|
@@ -2987,22 +4085,569 @@ class EditorManager {
|
|
|
2987
4085
|
'editor.lineHighlightBackground': '#073642',
|
|
2988
4086
|
'editorLineNumber.foreground': '#586e75',
|
|
2989
4087
|
}
|
|
2990
|
-
});
|
|
2991
|
-
monaco.editor.setTheme('solarized-dark');
|
|
2992
|
-
|
|
2993
|
-
// Process pending models
|
|
2994
|
-
for (const server of state.servers.values()) {
|
|
2995
|
-
for (const [path, file] of server.modelStore) {
|
|
2996
|
-
if (file.type === 'text' && !file.model && file.content !== null) {
|
|
2997
|
-
file.model = monaco.editor.createModel(file.content, undefined, monaco.Uri.file(path));
|
|
2998
|
-
}
|
|
4088
|
+
});
|
|
4089
|
+
monaco.editor.setTheme('solarized-dark');
|
|
4090
|
+
|
|
4091
|
+
// Process pending models
|
|
4092
|
+
for (const server of state.servers.values()) {
|
|
4093
|
+
for (const [path, file] of server.modelStore) {
|
|
4094
|
+
if (file.type === 'text' && !file.model && file.content !== null) {
|
|
4095
|
+
file.model = monaco.editor.createModel(file.content, undefined, monaco.Uri.file(path));
|
|
4096
|
+
}
|
|
4097
|
+
}
|
|
4098
|
+
}
|
|
4099
|
+
|
|
4100
|
+
if (this.currentSession) {
|
|
4101
|
+
this.switchTo(this.currentSession);
|
|
4102
|
+
}
|
|
4103
|
+
});
|
|
4104
|
+
}
|
|
4105
|
+
|
|
4106
|
+
clearMarkdownPreview() {
|
|
4107
|
+
const state = this.markdownPreviewState;
|
|
4108
|
+
state.renderToken += 1;
|
|
4109
|
+
clearTimeout(state.renderTimer);
|
|
4110
|
+
state.renderTimer = 0;
|
|
4111
|
+
state.path = '';
|
|
4112
|
+
state.sessionKey = '';
|
|
4113
|
+
state.pendingHash = '';
|
|
4114
|
+
if (this.markdownPreviewContent) {
|
|
4115
|
+
this.markdownPreviewContent.innerHTML = '';
|
|
4116
|
+
}
|
|
4117
|
+
if (this.markdownPreviewScroll) {
|
|
4118
|
+
this.markdownPreviewScroll.scrollTop = 0;
|
|
4119
|
+
}
|
|
4120
|
+
}
|
|
4121
|
+
|
|
4122
|
+
hideMarkdownPreview() {
|
|
4123
|
+
if (this.contentContainer) {
|
|
4124
|
+
this.contentContainer.classList.remove('markdown-split-active');
|
|
4125
|
+
}
|
|
4126
|
+
if (this.markdownPreviewContainer) {
|
|
4127
|
+
this.markdownPreviewContainer.style.display = 'none';
|
|
4128
|
+
}
|
|
4129
|
+
}
|
|
4130
|
+
|
|
4131
|
+
getMarkdownSourceContent(filePath, session = this.currentSession) {
|
|
4132
|
+
const entry = this.getModel(filePath, session);
|
|
4133
|
+
if (!entry || entry.type !== 'text') {
|
|
4134
|
+
return '';
|
|
4135
|
+
}
|
|
4136
|
+
if (entry.model && typeof entry.model.getValue === 'function') {
|
|
4137
|
+
return entry.model.getValue();
|
|
4138
|
+
}
|
|
4139
|
+
return typeof entry.content === 'string' ? entry.content : '';
|
|
4140
|
+
}
|
|
4141
|
+
|
|
4142
|
+
resolveMarkdownPreviewImageUrl(filePath, src, session) {
|
|
4143
|
+
const resolved = resolveMarkdownLocalTarget(filePath, src);
|
|
4144
|
+
if (resolved && isSupportedImagePath(resolved.path)) {
|
|
4145
|
+
return session.server.resolveUrl(
|
|
4146
|
+
`/api/fs/raw?path=${encodeURIComponent(resolved.path)}`
|
|
4147
|
+
+ `&token=${session.server.token}`
|
|
4148
|
+
);
|
|
4149
|
+
}
|
|
4150
|
+
return src;
|
|
4151
|
+
}
|
|
4152
|
+
|
|
4153
|
+
decorateMarkdownPreviewContent(root, filePath, session) {
|
|
4154
|
+
if (!(root instanceof DocumentFragment) && !(root instanceof Element)) {
|
|
4155
|
+
return;
|
|
4156
|
+
}
|
|
4157
|
+
const headingIds = new Map();
|
|
4158
|
+
for (const heading of root.querySelectorAll('h1, h2, h3, h4, h5, h6')) {
|
|
4159
|
+
const baseId = slugifyMarkdownHeading(heading.textContent || '')
|
|
4160
|
+
|| 'section';
|
|
4161
|
+
const nextCount = (headingIds.get(baseId) || 0) + 1;
|
|
4162
|
+
headingIds.set(baseId, nextCount);
|
|
4163
|
+
heading.id = nextCount === 1
|
|
4164
|
+
? baseId
|
|
4165
|
+
: `${baseId}-${nextCount}`;
|
|
4166
|
+
}
|
|
4167
|
+
|
|
4168
|
+
for (const image of root.querySelectorAll('img[src]')) {
|
|
4169
|
+
const src = String(image.getAttribute('src') || '').trim();
|
|
4170
|
+
if (!src) {
|
|
4171
|
+
continue;
|
|
4172
|
+
}
|
|
4173
|
+
image.loading = 'lazy';
|
|
4174
|
+
image.decoding = 'async';
|
|
4175
|
+
image.src = this.resolveMarkdownPreviewImageUrl(
|
|
4176
|
+
filePath,
|
|
4177
|
+
src,
|
|
4178
|
+
session
|
|
4179
|
+
);
|
|
4180
|
+
}
|
|
4181
|
+
|
|
4182
|
+
for (const link of root.querySelectorAll('a[href]')) {
|
|
4183
|
+
const href = String(link.getAttribute('href') || '').trim();
|
|
4184
|
+
if (!href) {
|
|
4185
|
+
continue;
|
|
4186
|
+
}
|
|
4187
|
+
const resolved = resolveMarkdownLocalTarget(filePath, href);
|
|
4188
|
+
if (resolved) {
|
|
4189
|
+
link.dataset.markdownLocalPath = resolved.path;
|
|
4190
|
+
link.dataset.markdownLocalHash = resolved.hash || '';
|
|
4191
|
+
continue;
|
|
4192
|
+
}
|
|
4193
|
+
if (!href.startsWith('#')) {
|
|
4194
|
+
link.target = '_blank';
|
|
4195
|
+
link.rel = 'noreferrer noopener';
|
|
4196
|
+
}
|
|
4197
|
+
}
|
|
4198
|
+
}
|
|
4199
|
+
|
|
4200
|
+
scrollMarkdownPreviewHash(hash) {
|
|
4201
|
+
const nextHash = String(hash || '').trim();
|
|
4202
|
+
if (!nextHash || !this.markdownPreviewScroll) {
|
|
4203
|
+
return;
|
|
4204
|
+
}
|
|
4205
|
+
const id = nextHash.startsWith('#') ? nextHash.slice(1) : nextHash;
|
|
4206
|
+
if (!id) {
|
|
4207
|
+
return;
|
|
4208
|
+
}
|
|
4209
|
+
const target = this.markdownPreviewContent?.querySelector(
|
|
4210
|
+
`#${CSS.escape(id)}`
|
|
4211
|
+
);
|
|
4212
|
+
if (!target) {
|
|
4213
|
+
return;
|
|
4214
|
+
}
|
|
4215
|
+
target.scrollIntoView({
|
|
4216
|
+
behavior: 'smooth',
|
|
4217
|
+
block: 'start'
|
|
4218
|
+
});
|
|
4219
|
+
}
|
|
4220
|
+
|
|
4221
|
+
scheduleMarkdownPreviewRender(filePath, session = this.currentSession) {
|
|
4222
|
+
if (
|
|
4223
|
+
!session
|
|
4224
|
+
|| !filePath
|
|
4225
|
+
|| !isSupportedMarkdownPath(filePath)
|
|
4226
|
+
) {
|
|
4227
|
+
return;
|
|
4228
|
+
}
|
|
4229
|
+
const state = this.markdownPreviewState;
|
|
4230
|
+
clearTimeout(state.renderTimer);
|
|
4231
|
+
state.renderTimer = window.setTimeout(() => {
|
|
4232
|
+
if (
|
|
4233
|
+
this.currentSession?.key !== session.key
|
|
4234
|
+
|| this.currentSession?.editorState.activeFilePath !== filePath
|
|
4235
|
+
) {
|
|
4236
|
+
return;
|
|
4237
|
+
}
|
|
4238
|
+
const activeKey = this.getActiveWorkspaceTabKey(session);
|
|
4239
|
+
const shouldShow = (
|
|
4240
|
+
isMarkdownPreviewWorkspaceTabKey(activeKey)
|
|
4241
|
+
|| this.isMarkdownSplitViewEnabled(session, filePath)
|
|
4242
|
+
);
|
|
4243
|
+
void this.renderMarkdownPreview(filePath, {
|
|
4244
|
+
session,
|
|
4245
|
+
show: shouldShow
|
|
4246
|
+
});
|
|
4247
|
+
}, 60);
|
|
4248
|
+
}
|
|
4249
|
+
|
|
4250
|
+
async renderMarkdownPreview(filePath, options = {}) {
|
|
4251
|
+
const session = options.session || this.currentSession;
|
|
4252
|
+
const show = options.show === true;
|
|
4253
|
+
if (
|
|
4254
|
+
!session
|
|
4255
|
+
|| !filePath
|
|
4256
|
+
|| !this.markdownPreviewContainer
|
|
4257
|
+
|| !this.markdownPreviewContent
|
|
4258
|
+
|| !isSupportedMarkdownPath(filePath)
|
|
4259
|
+
) {
|
|
4260
|
+
return;
|
|
4261
|
+
}
|
|
4262
|
+
const state = this.markdownPreviewState;
|
|
4263
|
+
const renderToken = state.renderToken + 1;
|
|
4264
|
+
state.renderToken = renderToken;
|
|
4265
|
+
state.path = filePath;
|
|
4266
|
+
state.sessionKey = session.key;
|
|
4267
|
+
if (show) {
|
|
4268
|
+
this.markdownPreviewContainer.style.display = 'flex';
|
|
4269
|
+
}
|
|
4270
|
+
|
|
4271
|
+
try {
|
|
4272
|
+
const { renderer } = await loadMarkdownPreviewBundle();
|
|
4273
|
+
if (
|
|
4274
|
+
state.renderToken !== renderToken
|
|
4275
|
+
|| state.path !== filePath
|
|
4276
|
+
|| state.sessionKey !== session.key
|
|
4277
|
+
) {
|
|
4278
|
+
return;
|
|
4279
|
+
}
|
|
4280
|
+
const source = this.getMarkdownSourceContent(filePath, session);
|
|
4281
|
+
const rendered = renderer.render(source || '');
|
|
4282
|
+
const sanitized = DOMPurify.sanitize(rendered, {
|
|
4283
|
+
USE_PROFILES: {
|
|
4284
|
+
html: true,
|
|
4285
|
+
mathMl: true,
|
|
4286
|
+
svg: true
|
|
4287
|
+
}
|
|
4288
|
+
});
|
|
4289
|
+
const template = document.createElement('template');
|
|
4290
|
+
template.innerHTML = sanitized;
|
|
4291
|
+
this.decorateMarkdownPreviewContent(
|
|
4292
|
+
template.content,
|
|
4293
|
+
filePath,
|
|
4294
|
+
session
|
|
4295
|
+
);
|
|
4296
|
+
this.markdownPreviewContent.replaceChildren(template.content);
|
|
4297
|
+
const pendingHash = state.pendingHash;
|
|
4298
|
+
state.pendingHash = '';
|
|
4299
|
+
if (pendingHash) {
|
|
4300
|
+
requestAnimationFrame(() => {
|
|
4301
|
+
this.scrollMarkdownPreviewHash(pendingHash);
|
|
4302
|
+
});
|
|
4303
|
+
}
|
|
4304
|
+
} catch (error) {
|
|
4305
|
+
console.error('Failed to render markdown preview:', error);
|
|
4306
|
+
this.markdownPreviewContent.innerHTML = '';
|
|
4307
|
+
const fallback = document.createElement('div');
|
|
4308
|
+
fallback.className = 'markdown-preview-error';
|
|
4309
|
+
fallback.textContent = 'Failed to render markdown preview.';
|
|
4310
|
+
this.markdownPreviewContent.appendChild(fallback);
|
|
4311
|
+
}
|
|
4312
|
+
}
|
|
4313
|
+
|
|
4314
|
+
async openLocalMarkdownLink(filePath, hash = '') {
|
|
4315
|
+
const session = this.currentSession;
|
|
4316
|
+
if (!session || !filePath) {
|
|
4317
|
+
return;
|
|
4318
|
+
}
|
|
4319
|
+
if (isSupportedMarkdownPath(filePath)) {
|
|
4320
|
+
this.markdownPreviewState.pendingHash = hash || '';
|
|
4321
|
+
await this.openFile(filePath, session, {
|
|
4322
|
+
activatePreview: true,
|
|
4323
|
+
focusEditor: false
|
|
4324
|
+
});
|
|
4325
|
+
return;
|
|
4326
|
+
}
|
|
4327
|
+
await this.openFile(filePath, session, {
|
|
4328
|
+
focusEditor: false
|
|
4329
|
+
});
|
|
4330
|
+
}
|
|
4331
|
+
|
|
4332
|
+
clearPdfPreview(preserveDocument = false) {
|
|
4333
|
+
const state = this.pdfPreviewState;
|
|
4334
|
+
state.renderToken += 1;
|
|
4335
|
+
clearTimeout(state.relayoutTimer);
|
|
4336
|
+
state.relayoutTimer = 0;
|
|
4337
|
+
if (!preserveDocument) {
|
|
4338
|
+
const documentRef = state.document;
|
|
4339
|
+
state.document = null;
|
|
4340
|
+
state.loadingTask = null;
|
|
4341
|
+
state.path = '';
|
|
4342
|
+
state.sessionKey = '';
|
|
4343
|
+
state.metadata = '';
|
|
4344
|
+
state.renderedWidth = 0;
|
|
4345
|
+
if (documentRef && typeof documentRef.destroy === 'function') {
|
|
4346
|
+
Promise.resolve(documentRef.destroy()).catch(() => {});
|
|
4347
|
+
}
|
|
4348
|
+
}
|
|
4349
|
+
if (this.pdfPreviewPages) {
|
|
4350
|
+
this.pdfPreviewPages.innerHTML = '';
|
|
4351
|
+
}
|
|
4352
|
+
this.setPdfPreviewStatus('', '');
|
|
4353
|
+
}
|
|
4354
|
+
|
|
4355
|
+
hidePdfPreview() {
|
|
4356
|
+
this.pdfPreviewContainer.style.display = 'none';
|
|
4357
|
+
}
|
|
4358
|
+
|
|
4359
|
+
getPdfPreviewUrl(filePath, session = this.currentSession) {
|
|
4360
|
+
if (!session) return '';
|
|
4361
|
+
return session.server.resolveUrl(
|
|
4362
|
+
`/api/fs/raw?path=${encodeURIComponent(filePath)}`
|
|
4363
|
+
+ `&token=${session.server.token}`
|
|
4364
|
+
);
|
|
4365
|
+
}
|
|
4366
|
+
|
|
4367
|
+
getPdfPreviewTargetWidth() {
|
|
4368
|
+
if (!this.pdfPreviewPages) {
|
|
4369
|
+
return 0;
|
|
4370
|
+
}
|
|
4371
|
+
const width = this.pdfPreviewPages.clientWidth - 36;
|
|
4372
|
+
return Math.max(240, Math.floor(Math.min(width, 960)));
|
|
4373
|
+
}
|
|
4374
|
+
|
|
4375
|
+
setPdfPreviewStatus(primary = '', secondary = '') {
|
|
4376
|
+
const nextPrimary = String(primary || '').trim();
|
|
4377
|
+
const nextSecondary = String(secondary || '').trim();
|
|
4378
|
+
if (this.pdfPreviewStatusPrimary) {
|
|
4379
|
+
this.pdfPreviewStatusPrimary.textContent = nextPrimary;
|
|
4380
|
+
}
|
|
4381
|
+
if (this.pdfPreviewStatusSecondary) {
|
|
4382
|
+
this.pdfPreviewStatusSecondary.textContent = nextSecondary;
|
|
4383
|
+
this.pdfPreviewStatusSecondary.title = nextSecondary;
|
|
4384
|
+
}
|
|
4385
|
+
if (this.pdfPreviewStatus) {
|
|
4386
|
+
this.pdfPreviewStatus.classList.toggle(
|
|
4387
|
+
'is-empty',
|
|
4388
|
+
!nextPrimary && !nextSecondary
|
|
4389
|
+
);
|
|
4390
|
+
}
|
|
4391
|
+
}
|
|
4392
|
+
|
|
4393
|
+
formatPdfByteSize(bytes) {
|
|
4394
|
+
if (!Number.isFinite(bytes) || bytes <= 0) {
|
|
4395
|
+
return '';
|
|
4396
|
+
}
|
|
4397
|
+
const units = ['B', 'KB', 'MB', 'GB'];
|
|
4398
|
+
let value = bytes;
|
|
4399
|
+
let unitIndex = 0;
|
|
4400
|
+
while (value >= 1024 && unitIndex < units.length - 1) {
|
|
4401
|
+
value /= 1024;
|
|
4402
|
+
unitIndex += 1;
|
|
4403
|
+
}
|
|
4404
|
+
const decimals = value >= 100 || unitIndex === 0 ? 0 : 1;
|
|
4405
|
+
return `${value.toFixed(decimals)} ${units[unitIndex]}`;
|
|
4406
|
+
}
|
|
4407
|
+
|
|
4408
|
+
describePdfPageSize(viewport) {
|
|
4409
|
+
if (!viewport) {
|
|
4410
|
+
return '';
|
|
4411
|
+
}
|
|
4412
|
+
const width = Math.min(viewport.width, viewport.height);
|
|
4413
|
+
const height = Math.max(viewport.width, viewport.height);
|
|
4414
|
+
const near = (targetWidth, targetHeight) => (
|
|
4415
|
+
Math.abs(width - targetWidth) < 2
|
|
4416
|
+
&& Math.abs(height - targetHeight) < 2
|
|
4417
|
+
);
|
|
4418
|
+
if (near(595.276, 841.89)) return 'A4';
|
|
4419
|
+
if (near(612, 792)) return 'Letter';
|
|
4420
|
+
return '';
|
|
4421
|
+
}
|
|
4422
|
+
|
|
4423
|
+
async loadPdfMetadata(documentRef) {
|
|
4424
|
+
const parts = [];
|
|
4425
|
+
try {
|
|
4426
|
+
const meta = await documentRef.getMetadata();
|
|
4427
|
+
const version = String(meta?.info?.PDFFormatVersion || '').trim();
|
|
4428
|
+
parts.push(version ? `PDF ${version}` : 'PDF');
|
|
4429
|
+
} catch {
|
|
4430
|
+
parts.push('PDF');
|
|
4431
|
+
}
|
|
4432
|
+
|
|
4433
|
+
try {
|
|
4434
|
+
const firstPage = await documentRef.getPage(1);
|
|
4435
|
+
const pageSize = this.describePdfPageSize(
|
|
4436
|
+
firstPage.getViewport({ scale: 1 })
|
|
4437
|
+
);
|
|
4438
|
+
if (pageSize) {
|
|
4439
|
+
parts.push(pageSize);
|
|
4440
|
+
}
|
|
4441
|
+
} catch {
|
|
4442
|
+
// Ignore optional page-size metadata failures.
|
|
4443
|
+
}
|
|
4444
|
+
|
|
4445
|
+
try {
|
|
4446
|
+
const downloadInfo = await documentRef.getDownloadInfo();
|
|
4447
|
+
const byteSize = this.formatPdfByteSize(downloadInfo?.length);
|
|
4448
|
+
if (byteSize) {
|
|
4449
|
+
parts.push(byteSize);
|
|
4450
|
+
}
|
|
4451
|
+
} catch {
|
|
4452
|
+
// Ignore optional size metadata failures.
|
|
4453
|
+
}
|
|
4454
|
+
|
|
4455
|
+
return parts.join(' · ');
|
|
4456
|
+
}
|
|
4457
|
+
|
|
4458
|
+
schedulePdfPreviewRelayout() {
|
|
4459
|
+
const state = this.pdfPreviewState;
|
|
4460
|
+
if (
|
|
4461
|
+
!this.pdfPreviewContainer
|
|
4462
|
+
|| this.pdfPreviewContainer.style.display === 'none'
|
|
4463
|
+
|| !state.document
|
|
4464
|
+
|| !state.path
|
|
4465
|
+
) {
|
|
4466
|
+
return;
|
|
4467
|
+
}
|
|
4468
|
+
clearTimeout(state.relayoutTimer);
|
|
4469
|
+
state.relayoutTimer = window.setTimeout(() => {
|
|
4470
|
+
const nextWidth = this.getPdfPreviewTargetWidth();
|
|
4471
|
+
if (
|
|
4472
|
+
nextWidth > 0
|
|
4473
|
+
&& Math.abs(nextWidth - state.renderedWidth) > 24
|
|
4474
|
+
) {
|
|
4475
|
+
void this.renderPdfPreview(state.path);
|
|
4476
|
+
}
|
|
4477
|
+
}, 120);
|
|
4478
|
+
}
|
|
4479
|
+
|
|
4480
|
+
async loadPdfDocument(filePath, session, renderToken) {
|
|
4481
|
+
const state = this.pdfPreviewState;
|
|
4482
|
+
const url = this.getPdfPreviewUrl(filePath, session);
|
|
4483
|
+
const pdfjsLib = await loadPdfJs();
|
|
4484
|
+
if (state.renderToken !== renderToken) {
|
|
4485
|
+
return null;
|
|
4486
|
+
}
|
|
4487
|
+
|
|
4488
|
+
let loadingTask = pdfjsLib.getDocument({
|
|
4489
|
+
url
|
|
4490
|
+
});
|
|
4491
|
+
state.loadingTask = loadingTask;
|
|
4492
|
+
try {
|
|
4493
|
+
return await loadingTask.promise;
|
|
4494
|
+
} catch (_error) {
|
|
4495
|
+
if (state.renderToken !== renderToken) {
|
|
4496
|
+
return null;
|
|
4497
|
+
}
|
|
4498
|
+
loadingTask = pdfjsLib.getDocument({
|
|
4499
|
+
url,
|
|
4500
|
+
disableWorker: true
|
|
4501
|
+
});
|
|
4502
|
+
state.loadingTask = loadingTask;
|
|
4503
|
+
return await loadingTask.promise;
|
|
4504
|
+
}
|
|
4505
|
+
}
|
|
4506
|
+
|
|
4507
|
+
async renderPdfPreview(filePath) {
|
|
4508
|
+
const session = this.currentSession;
|
|
4509
|
+
if (!session || !filePath) {
|
|
4510
|
+
return;
|
|
4511
|
+
}
|
|
4512
|
+
const state = this.pdfPreviewState;
|
|
4513
|
+
const renderToken = state.renderToken + 1;
|
|
4514
|
+
const targetSessionKey = session.key;
|
|
4515
|
+
const nextWidth = this.getPdfPreviewTargetWidth();
|
|
4516
|
+
if (nextWidth <= 0) {
|
|
4517
|
+
requestAnimationFrame(() => {
|
|
4518
|
+
if (
|
|
4519
|
+
this.currentSession?.key === targetSessionKey
|
|
4520
|
+
&& this.currentSession?.editorState.activeFilePath === filePath
|
|
4521
|
+
) {
|
|
4522
|
+
void this.renderPdfPreview(filePath);
|
|
4523
|
+
}
|
|
4524
|
+
});
|
|
4525
|
+
return;
|
|
4526
|
+
}
|
|
4527
|
+
|
|
4528
|
+
if (
|
|
4529
|
+
state.path !== filePath
|
|
4530
|
+
|| state.sessionKey !== targetSessionKey
|
|
4531
|
+
) {
|
|
4532
|
+
this.clearPdfPreview();
|
|
4533
|
+
} else {
|
|
4534
|
+
this.clearPdfPreview(true);
|
|
4535
|
+
}
|
|
4536
|
+
state.renderToken = renderToken;
|
|
4537
|
+
state.path = filePath;
|
|
4538
|
+
state.sessionKey = targetSessionKey;
|
|
4539
|
+
this.setPdfPreviewStatus('Loading PDF…', '');
|
|
4540
|
+
|
|
4541
|
+
try {
|
|
4542
|
+
let documentRef = state.document;
|
|
4543
|
+
if (!documentRef) {
|
|
4544
|
+
documentRef = await this.loadPdfDocument(
|
|
4545
|
+
filePath,
|
|
4546
|
+
session,
|
|
4547
|
+
renderToken
|
|
4548
|
+
);
|
|
4549
|
+
if (!documentRef || state.renderToken !== renderToken) {
|
|
4550
|
+
return;
|
|
4551
|
+
}
|
|
4552
|
+
state.document = documentRef;
|
|
4553
|
+
state.metadata = await this.loadPdfMetadata(documentRef);
|
|
4554
|
+
if (state.renderToken !== renderToken) {
|
|
4555
|
+
return;
|
|
4556
|
+
}
|
|
4557
|
+
}
|
|
4558
|
+
|
|
4559
|
+
state.renderedWidth = nextWidth;
|
|
4560
|
+
if (this.pdfPreviewPages) {
|
|
4561
|
+
this.pdfPreviewPages.innerHTML = '';
|
|
4562
|
+
}
|
|
4563
|
+
const pageCount = documentRef.numPages;
|
|
4564
|
+
this.setPdfPreviewStatus(
|
|
4565
|
+
`${pageCount} page${pageCount === 1 ? '' : 's'}`,
|
|
4566
|
+
state.metadata || 'PDF'
|
|
4567
|
+
);
|
|
4568
|
+
|
|
4569
|
+
for (let pageNumber = 1; pageNumber <= documentRef.numPages; pageNumber += 1) {
|
|
4570
|
+
if (state.renderToken !== renderToken) {
|
|
4571
|
+
return;
|
|
4572
|
+
}
|
|
4573
|
+
const page = await documentRef.getPage(pageNumber);
|
|
4574
|
+
if (state.renderToken !== renderToken) {
|
|
4575
|
+
return;
|
|
4576
|
+
}
|
|
4577
|
+
const baseViewport = page.getViewport({ scale: 1 });
|
|
4578
|
+
const scale = nextWidth / baseViewport.width;
|
|
4579
|
+
const viewport = page.getViewport({ scale });
|
|
4580
|
+
const canvas = document.createElement('canvas');
|
|
4581
|
+
const context = canvas.getContext('2d', {
|
|
4582
|
+
alpha: false
|
|
4583
|
+
});
|
|
4584
|
+
if (!context) {
|
|
4585
|
+
throw new Error('Failed to create PDF canvas context');
|
|
4586
|
+
}
|
|
4587
|
+
const outputScale = Math.max(1, window.devicePixelRatio || 1);
|
|
4588
|
+
canvas.width = Math.ceil(viewport.width * outputScale);
|
|
4589
|
+
canvas.height = Math.ceil(viewport.height * outputScale);
|
|
4590
|
+
canvas.style.width = `${Math.ceil(viewport.width)}px`;
|
|
4591
|
+
canvas.style.height = `${Math.ceil(viewport.height)}px`;
|
|
4592
|
+
context.setTransform(outputScale, 0, 0, outputScale, 0, 0);
|
|
4593
|
+
const textLayer = document.createElement('div');
|
|
4594
|
+
textLayer.className = 'textLayer';
|
|
4595
|
+
const sheet = document.createElement('div');
|
|
4596
|
+
sheet.className = 'pdf-preview-sheet';
|
|
4597
|
+
sheet.style.width = `${Math.ceil(viewport.width)}px`;
|
|
4598
|
+
sheet.style.height = `${Math.ceil(viewport.height)}px`;
|
|
4599
|
+
sheet.style.setProperty('--user-unit', '1');
|
|
4600
|
+
sheet.style.setProperty('--scale-factor', String(scale));
|
|
4601
|
+
sheet.style.setProperty(
|
|
4602
|
+
'--total-scale-factor',
|
|
4603
|
+
String(scale)
|
|
4604
|
+
);
|
|
4605
|
+
sheet.style.setProperty('--scale-round-x', '1px');
|
|
4606
|
+
sheet.style.setProperty('--scale-round-y', '1px');
|
|
4607
|
+
await page.render({
|
|
4608
|
+
canvasContext: context,
|
|
4609
|
+
viewport
|
|
4610
|
+
}).promise;
|
|
4611
|
+
if (state.renderToken !== renderToken) {
|
|
4612
|
+
return;
|
|
4613
|
+
}
|
|
4614
|
+
const textContent = await page.getTextContent();
|
|
4615
|
+
if (state.renderToken !== renderToken) {
|
|
4616
|
+
return;
|
|
4617
|
+
}
|
|
4618
|
+
const textLayerBuilder = new pdfjsLib.TextLayer({
|
|
4619
|
+
textContentSource: textContent,
|
|
4620
|
+
container: textLayer,
|
|
4621
|
+
viewport
|
|
4622
|
+
});
|
|
4623
|
+
await textLayerBuilder.render();
|
|
4624
|
+
if (state.renderToken !== renderToken) {
|
|
4625
|
+
return;
|
|
2999
4626
|
}
|
|
4627
|
+
const wrapper = document.createElement('div');
|
|
4628
|
+
wrapper.className = 'pdf-preview-page';
|
|
4629
|
+
wrapper.dataset.pageNumber = String(pageNumber);
|
|
4630
|
+
sheet.appendChild(canvas);
|
|
4631
|
+
sheet.appendChild(textLayer);
|
|
4632
|
+
wrapper.appendChild(sheet);
|
|
4633
|
+
this.pdfPreviewPages?.appendChild(wrapper);
|
|
3000
4634
|
}
|
|
3001
|
-
|
|
3002
|
-
|
|
3003
|
-
|
|
4635
|
+
} catch (error) {
|
|
4636
|
+
console.error('Failed to render PDF preview:', error);
|
|
4637
|
+
if (state.renderToken !== renderToken) {
|
|
4638
|
+
return;
|
|
3004
4639
|
}
|
|
3005
|
-
|
|
4640
|
+
this.clearPdfPreview();
|
|
4641
|
+
this.hidePdfPreview();
|
|
4642
|
+
alert(
|
|
4643
|
+
`Failed to load PDF: ${filePath.split('/').pop()}`,
|
|
4644
|
+
{
|
|
4645
|
+
type: 'error',
|
|
4646
|
+
title: 'PDF Preview Error'
|
|
4647
|
+
}
|
|
4648
|
+
);
|
|
4649
|
+
this.closeFile(filePath);
|
|
4650
|
+
}
|
|
3006
4651
|
}
|
|
3007
4652
|
|
|
3008
4653
|
updateEditorPaneVisibility() {
|
|
@@ -3089,6 +4734,7 @@ class EditorManager {
|
|
|
3089
4734
|
}
|
|
3090
4735
|
|
|
3091
4736
|
this.currentSession = session;
|
|
4737
|
+
this.syncMarkdownSplitSupport(session);
|
|
3092
4738
|
if (!session) {
|
|
3093
4739
|
this.pane.style.display = 'none';
|
|
3094
4740
|
this.resizer.style.display = 'none';
|
|
@@ -3126,10 +4772,13 @@ class EditorManager {
|
|
|
3126
4772
|
layout() {
|
|
3127
4773
|
// console.log('[Editor] layout called');
|
|
3128
4774
|
if (!this.currentSession) return;
|
|
4775
|
+
this.syncMarkdownSplitSupport(this.currentSession);
|
|
3129
4776
|
this.currentSession.fitMainTerminalIfVisible();
|
|
3130
4777
|
if (this.editor && this.pane.style.display !== 'none') {
|
|
3131
|
-
const width = this.
|
|
3132
|
-
|
|
4778
|
+
const width = this.monacoContainer?.clientWidth
|
|
4779
|
+
|| this.pane.clientWidth;
|
|
4780
|
+
const height = this.monacoContainer?.clientHeight
|
|
4781
|
+
|| (this.pane.clientHeight - 35);
|
|
3133
4782
|
|
|
3134
4783
|
if (width > 0 && height > 0) {
|
|
3135
4784
|
this.editor.layout({ width, height });
|
|
@@ -3137,56 +4786,56 @@ class EditorManager {
|
|
|
3137
4786
|
this.editor.layout();
|
|
3138
4787
|
}
|
|
3139
4788
|
}
|
|
4789
|
+
this.schedulePdfPreviewRelayout();
|
|
3140
4790
|
}
|
|
3141
4791
|
|
|
3142
|
-
|
|
4792
|
+
renderTreeFromSnapshots(
|
|
3143
4793
|
dirPath,
|
|
3144
4794
|
container,
|
|
3145
4795
|
session,
|
|
4796
|
+
directorySnapshots,
|
|
3146
4797
|
renderToken = session?.fileTreeRenderToken || 0
|
|
3147
4798
|
) {
|
|
3148
|
-
|
|
3149
|
-
|
|
3150
|
-
|
|
3151
|
-
|
|
3152
|
-
|
|
3153
|
-
|
|
3154
|
-
|
|
3155
|
-
|
|
3156
|
-
|
|
3157
|
-
? payload.items
|
|
3158
|
-
: [];
|
|
3159
|
-
const creatable = Array.isArray(payload)
|
|
3160
|
-
? false
|
|
3161
|
-
: !!payload?.creatable;
|
|
3162
|
-
if ((session.fileTreeRenderToken || 0) !== renderToken) return;
|
|
3163
|
-
|
|
3164
|
-
const list = this.ensureTreeList(container);
|
|
3165
|
-
this.reconcileTreeList(
|
|
3166
|
-
list,
|
|
3167
|
-
dirPath,
|
|
3168
|
-
files,
|
|
3169
|
-
creatable,
|
|
3170
|
-
session,
|
|
3171
|
-
renderToken
|
|
3172
|
-
);
|
|
3173
|
-
if ((session.fileTreeRenderToken || 0) !== renderToken) return;
|
|
4799
|
+
const listing = directorySnapshots.get(
|
|
4800
|
+
this.getTreeRefreshRequestKey(session.server, dirPath)
|
|
4801
|
+
);
|
|
4802
|
+
if (!listing) {
|
|
4803
|
+
return;
|
|
4804
|
+
}
|
|
4805
|
+
if ((session.fileTreeRenderToken || 0) !== renderToken) {
|
|
4806
|
+
return;
|
|
4807
|
+
}
|
|
3174
4808
|
|
|
3175
|
-
|
|
3176
|
-
|
|
3177
|
-
|
|
3178
|
-
|
|
3179
|
-
|
|
3180
|
-
|
|
3181
|
-
|
|
4809
|
+
const list = this.ensureTreeList(container);
|
|
4810
|
+
this.reconcileTreeList(
|
|
4811
|
+
list,
|
|
4812
|
+
dirPath,
|
|
4813
|
+
listing.files,
|
|
4814
|
+
listing.creatable,
|
|
4815
|
+
session
|
|
4816
|
+
);
|
|
4817
|
+
if ((session.fileTreeRenderToken || 0) !== renderToken) {
|
|
4818
|
+
return;
|
|
4819
|
+
}
|
|
4820
|
+
|
|
4821
|
+
for (const file of listing.files) {
|
|
4822
|
+
if (
|
|
4823
|
+
file.isDirectory
|
|
4824
|
+
&& this.getTreeItemExpanded(file.path, session)
|
|
4825
|
+
) {
|
|
4826
|
+
const item = Array.from(list.children).find(
|
|
4827
|
+
(child) => child.dataset.path === file.path
|
|
4828
|
+
);
|
|
4829
|
+
if (item) {
|
|
4830
|
+
this.renderTreeFromSnapshots(
|
|
4831
|
+
file.path,
|
|
4832
|
+
item,
|
|
4833
|
+
session,
|
|
4834
|
+
directorySnapshots,
|
|
4835
|
+
renderToken
|
|
3182
4836
|
);
|
|
3183
|
-
if (item) {
|
|
3184
|
-
void this.renderTree(file.path, item, session, renderToken);
|
|
3185
|
-
}
|
|
3186
4837
|
}
|
|
3187
4838
|
}
|
|
3188
|
-
} catch (err) {
|
|
3189
|
-
console.error('Failed to render tree:', err);
|
|
3190
4839
|
}
|
|
3191
4840
|
}
|
|
3192
4841
|
|
|
@@ -3209,34 +4858,31 @@ class EditorManager {
|
|
|
3209
4858
|
const state = targetSession.editorState;
|
|
3210
4859
|
const wasOpen = state.openFiles.includes(filePath);
|
|
3211
4860
|
const isImage = isSupportedImagePath(filePath);
|
|
4861
|
+
const isPdf = isSupportedPdfPath(filePath);
|
|
3212
4862
|
|
|
3213
|
-
if (!this.getModel(filePath)) {
|
|
4863
|
+
if (!this.getModel(filePath, targetSession)) {
|
|
3214
4864
|
let model = null;
|
|
3215
4865
|
let content = null;
|
|
3216
4866
|
let readonly = false;
|
|
4867
|
+
let version = '';
|
|
4868
|
+
let size = 0;
|
|
4869
|
+
let mtimeMs = 0;
|
|
3217
4870
|
|
|
3218
|
-
if (!isImage) {
|
|
4871
|
+
if (!isImage && !isPdf) {
|
|
3219
4872
|
try {
|
|
3220
|
-
const
|
|
3221
|
-
|
|
4873
|
+
const data = await this.readTextFileSnapshot(
|
|
4874
|
+
targetSession,
|
|
4875
|
+
filePath
|
|
3222
4876
|
);
|
|
3223
|
-
if (res.status === 415) {
|
|
3224
|
-
await showConfirmModal({
|
|
3225
|
-
title: 'Unsupported File Type',
|
|
3226
|
-
message: 'This file type is not supported yet.',
|
|
3227
|
-
note: 'Only text files and supported images can be opened right now.',
|
|
3228
|
-
confirmLabel: 'OK',
|
|
3229
|
-
hideCancel: true,
|
|
3230
|
-
returnFocus: document.activeElement
|
|
3231
|
-
});
|
|
3232
|
-
return;
|
|
3233
|
-
}
|
|
3234
|
-
if (!res.ok) {
|
|
3235
|
-
throw new Error('Failed to read file');
|
|
3236
|
-
}
|
|
3237
|
-
const data = await res.json();
|
|
3238
4877
|
content = data.content;
|
|
3239
4878
|
readonly = data.readonly;
|
|
4879
|
+
version = typeof data.version === 'string'
|
|
4880
|
+
? data.version
|
|
4881
|
+
: '';
|
|
4882
|
+
size = Number.isFinite(data.size) ? data.size : 0;
|
|
4883
|
+
mtimeMs = Number.isFinite(data.mtimeMs)
|
|
4884
|
+
? data.mtimeMs
|
|
4885
|
+
: 0;
|
|
3240
4886
|
|
|
3241
4887
|
if (this.monacoInstance) {
|
|
3242
4888
|
const uri = this.monacoInstance.Uri.file(filePath);
|
|
@@ -3249,6 +4895,17 @@ class EditorManager {
|
|
|
3249
4895
|
}
|
|
3250
4896
|
}
|
|
3251
4897
|
} catch (err) {
|
|
4898
|
+
if (err?.message === 'Unsupported file type') {
|
|
4899
|
+
await showConfirmModal({
|
|
4900
|
+
title: 'Unsupported File Type',
|
|
4901
|
+
message: 'This file type is not supported yet.',
|
|
4902
|
+
note: 'Only text files, supported images, and PDFs can be opened right now.',
|
|
4903
|
+
confirmLabel: 'OK',
|
|
4904
|
+
hideCancel: true,
|
|
4905
|
+
returnFocus: document.activeElement
|
|
4906
|
+
});
|
|
4907
|
+
return;
|
|
4908
|
+
}
|
|
3252
4909
|
alert(`Failed to open file: ${err.message}`, { type: 'error', title: 'Error' });
|
|
3253
4910
|
this.closeFile(filePath);
|
|
3254
4911
|
return;
|
|
@@ -3256,11 +4913,16 @@ class EditorManager {
|
|
|
3256
4913
|
}
|
|
3257
4914
|
|
|
3258
4915
|
this.setModel(filePath, {
|
|
3259
|
-
type: isImage ? 'image' : 'text',
|
|
4916
|
+
type: isImage ? 'image' : isPdf ? 'pdf' : 'text',
|
|
3260
4917
|
model: model,
|
|
3261
4918
|
content: content,
|
|
3262
|
-
readonly: readonly
|
|
3263
|
-
|
|
4919
|
+
readonly: readonly,
|
|
4920
|
+
version,
|
|
4921
|
+
contentVersion: version,
|
|
4922
|
+
size,
|
|
4923
|
+
mtimeMs,
|
|
4924
|
+
lastDismissedRemoteVersion: ''
|
|
4925
|
+
}, targetSession);
|
|
3264
4926
|
}
|
|
3265
4927
|
|
|
3266
4928
|
let touchedWorkspace = false;
|
|
@@ -3272,7 +4934,11 @@ class EditorManager {
|
|
|
3272
4934
|
|
|
3273
4935
|
this.updateEditorPaneVisibility();
|
|
3274
4936
|
|
|
3275
|
-
|
|
4937
|
+
if (options.activatePreview && isSupportedMarkdownPath(filePath)) {
|
|
4938
|
+
this.activateMarkdownPreviewTab(filePath, false);
|
|
4939
|
+
} else {
|
|
4940
|
+
this.activateFileTab(filePath, false, options);
|
|
4941
|
+
}
|
|
3276
4942
|
if (touchedWorkspace) {
|
|
3277
4943
|
targetSession.saveState({ touchWorkspace: true });
|
|
3278
4944
|
}
|
|
@@ -3281,6 +4947,9 @@ class EditorManager {
|
|
|
3281
4947
|
closeFile(filePath) {
|
|
3282
4948
|
if (!this.currentSession) return;
|
|
3283
4949
|
const state = this.currentSession.editorState;
|
|
4950
|
+
if (this.getMarkdownSplitPath(this.currentSession) === filePath) {
|
|
4951
|
+
this.currentSession.workspaceState.markdownSplitPath = '';
|
|
4952
|
+
}
|
|
3284
4953
|
|
|
3285
4954
|
const index = state.openFiles.indexOf(filePath);
|
|
3286
4955
|
let touchedWorkspace = false;
|
|
@@ -3319,8 +4988,10 @@ class EditorManager {
|
|
|
3319
4988
|
|
|
3320
4989
|
renderEditorTabs() {
|
|
3321
4990
|
if (!this.currentSession) return;
|
|
4991
|
+
this.syncMarkdownSplitSupport(this.currentSession);
|
|
3322
4992
|
const state = this.currentSession.editorState;
|
|
3323
4993
|
const activeWorkspaceTabKey = this.getActiveWorkspaceTabKey();
|
|
4994
|
+
const splitPath = this.getMarkdownSplitPath(this.currentSession);
|
|
3324
4995
|
|
|
3325
4996
|
this.tabsContainer.innerHTML = '';
|
|
3326
4997
|
if (this.hasCompactWorkspaceTabs(this.currentSession)) {
|
|
@@ -3349,11 +5020,28 @@ class EditorManager {
|
|
|
3349
5020
|
}
|
|
3350
5021
|
|
|
3351
5022
|
for (const path of state.openFiles) {
|
|
5023
|
+
const splitEnabled = this.isMarkdownSplitViewEnabled(
|
|
5024
|
+
this.currentSession,
|
|
5025
|
+
path
|
|
5026
|
+
);
|
|
3352
5027
|
const tab = document.createElement('div');
|
|
3353
5028
|
tab.className = 'editor-tab';
|
|
3354
|
-
if (
|
|
5029
|
+
if (
|
|
5030
|
+
makeFileWorkspaceTabKey(path) === activeWorkspaceTabKey
|
|
5031
|
+
|| (
|
|
5032
|
+
splitEnabled
|
|
5033
|
+
&& makeMarkdownPreviewWorkspaceTabKey(path)
|
|
5034
|
+
=== activeWorkspaceTabKey
|
|
5035
|
+
)
|
|
5036
|
+
) {
|
|
3355
5037
|
tab.classList.add('active');
|
|
3356
5038
|
}
|
|
5039
|
+
if (isSupportedMarkdownPath(path)) {
|
|
5040
|
+
tab.classList.add('bound-tab', 'bound-tab-primary');
|
|
5041
|
+
}
|
|
5042
|
+
if (splitEnabled) {
|
|
5043
|
+
tab.classList.add('is-split');
|
|
5044
|
+
}
|
|
3357
5045
|
|
|
3358
5046
|
const fileModel = this.getModel(path);
|
|
3359
5047
|
if (fileModel && fileModel.readonly) {
|
|
@@ -3375,16 +5063,96 @@ class EditorManager {
|
|
|
3375
5063
|
e.stopPropagation();
|
|
3376
5064
|
this.closeFile(path);
|
|
3377
5065
|
};
|
|
5066
|
+
let unsplitBtn = null;
|
|
5067
|
+
|
|
5068
|
+
if (splitEnabled) {
|
|
5069
|
+
unsplitBtn = document.createElement('span');
|
|
5070
|
+
unsplitBtn.className = 'tab-action-btn markdown-unsplit-btn';
|
|
5071
|
+
unsplitBtn.innerHTML = MARKDOWN_SPLIT_DISABLE_ICON_SVG;
|
|
5072
|
+
unsplitBtn.title = 'Restore tabbed markdown view';
|
|
5073
|
+
unsplitBtn.onclick = (e) => {
|
|
5074
|
+
e.stopPropagation();
|
|
5075
|
+
this.setMarkdownSplitView(
|
|
5076
|
+
path,
|
|
5077
|
+
false,
|
|
5078
|
+
this.currentSession
|
|
5079
|
+
);
|
|
5080
|
+
this.activateFileTab(path, false, {
|
|
5081
|
+
focusEditor: false
|
|
5082
|
+
});
|
|
5083
|
+
};
|
|
5084
|
+
tab.appendChild(unsplitBtn);
|
|
5085
|
+
}
|
|
3378
5086
|
|
|
3379
5087
|
tab.onclick = () => this.activateFileTab(path);
|
|
3380
5088
|
bindSingleTapActivation(tab, () => this.activateFileTab(path), {
|
|
3381
|
-
ignoreSelector: '.close-btn'
|
|
5089
|
+
ignoreSelector: '.close-btn, .tab-action-btn'
|
|
3382
5090
|
});
|
|
3383
5091
|
|
|
3384
5092
|
tab.appendChild(icon);
|
|
3385
5093
|
tab.appendChild(span);
|
|
5094
|
+
if (unsplitBtn) {
|
|
5095
|
+
tab.appendChild(unsplitBtn);
|
|
5096
|
+
}
|
|
3386
5097
|
tab.appendChild(closeBtn);
|
|
3387
5098
|
this.tabsContainer.appendChild(tab);
|
|
5099
|
+
|
|
5100
|
+
if (
|
|
5101
|
+
isSupportedMarkdownPath(path)
|
|
5102
|
+
&& !splitEnabled
|
|
5103
|
+
) {
|
|
5104
|
+
const previewTab = document.createElement('div');
|
|
5105
|
+
previewTab.className = 'editor-tab markdown-preview-tab bound-tab bound-tab-secondary';
|
|
5106
|
+
if (
|
|
5107
|
+
makeMarkdownPreviewWorkspaceTabKey(path)
|
|
5108
|
+
=== activeWorkspaceTabKey
|
|
5109
|
+
) {
|
|
5110
|
+
previewTab.classList.add('active');
|
|
5111
|
+
}
|
|
5112
|
+
|
|
5113
|
+
const previewIcon = document.createElement('span');
|
|
5114
|
+
previewIcon.className = 'file-editor-tab-icon';
|
|
5115
|
+
previewIcon.innerHTML = MARKDOWN_PREVIEW_ICON_SVG;
|
|
5116
|
+
|
|
5117
|
+
const previewLabel = document.createElement('span');
|
|
5118
|
+
previewLabel.textContent = 'Preview';
|
|
5119
|
+
let splitBtn = null;
|
|
5120
|
+
|
|
5121
|
+
if (
|
|
5122
|
+
path !== splitPath
|
|
5123
|
+
&& canUseMarkdownSplitTabsMode()
|
|
5124
|
+
) {
|
|
5125
|
+
splitBtn = document.createElement('span');
|
|
5126
|
+
splitBtn.className = 'tab-action-btn markdown-split-btn';
|
|
5127
|
+
splitBtn.innerHTML = MARKDOWN_SPLIT_ENABLE_ICON_SVG;
|
|
5128
|
+
splitBtn.title = 'Show markdown editor and preview side by side';
|
|
5129
|
+
splitBtn.onclick = (event) => {
|
|
5130
|
+
event.stopPropagation();
|
|
5131
|
+
this.setMarkdownSplitView(
|
|
5132
|
+
path,
|
|
5133
|
+
true,
|
|
5134
|
+
this.currentSession
|
|
5135
|
+
);
|
|
5136
|
+
};
|
|
5137
|
+
previewTab.appendChild(splitBtn);
|
|
5138
|
+
}
|
|
5139
|
+
|
|
5140
|
+
previewTab.onclick = () => this.activateMarkdownPreviewTab(path);
|
|
5141
|
+
bindSingleTapActivation(
|
|
5142
|
+
previewTab,
|
|
5143
|
+
() => this.activateMarkdownPreviewTab(path),
|
|
5144
|
+
{
|
|
5145
|
+
ignoreSelector: '.tab-action-btn'
|
|
5146
|
+
}
|
|
5147
|
+
);
|
|
5148
|
+
|
|
5149
|
+
previewTab.appendChild(previewIcon);
|
|
5150
|
+
previewTab.appendChild(previewLabel);
|
|
5151
|
+
if (splitBtn) {
|
|
5152
|
+
previewTab.appendChild(splitBtn);
|
|
5153
|
+
}
|
|
5154
|
+
this.tabsContainer.appendChild(previewTab);
|
|
5155
|
+
}
|
|
3388
5156
|
}
|
|
3389
5157
|
|
|
3390
5158
|
for (const agentTab of getAgentTabsForSession(this.currentSession)) {
|
|
@@ -3436,6 +5204,13 @@ class EditorManager {
|
|
|
3436
5204
|
this.activateAgentTab(workspaceTabKey, isRestore);
|
|
3437
5205
|
return;
|
|
3438
5206
|
}
|
|
5207
|
+
if (isMarkdownPreviewWorkspaceTabKey(workspaceTabKey)) {
|
|
5208
|
+
this.activateMarkdownPreviewTab(
|
|
5209
|
+
workspaceKeyToFilePath(workspaceTabKey),
|
|
5210
|
+
isRestore
|
|
5211
|
+
);
|
|
5212
|
+
return;
|
|
5213
|
+
}
|
|
3439
5214
|
this.activateFileTab(workspaceKeyToFilePath(workspaceTabKey), isRestore);
|
|
3440
5215
|
}
|
|
3441
5216
|
|
|
@@ -3461,12 +5236,14 @@ class EditorManager {
|
|
|
3461
5236
|
TERMINAL_WORKSPACE_TAB_KEY;
|
|
3462
5237
|
this.currentSession.needsAttention = false;
|
|
3463
5238
|
if (!isRestore) {
|
|
3464
|
-
this.currentSession.saveState();
|
|
5239
|
+
this.currentSession.saveState({ touchWorkspace: true });
|
|
3465
5240
|
}
|
|
3466
5241
|
this.renderEditorTabs();
|
|
3467
5242
|
this.currentSession.updateTabUI();
|
|
3468
5243
|
this.monacoContainer.style.display = 'none';
|
|
3469
5244
|
this.imagePreviewContainer.style.display = 'none';
|
|
5245
|
+
this.hidePdfPreview();
|
|
5246
|
+
this.hideMarkdownPreview();
|
|
3470
5247
|
this.agentContainer.style.display = 'none';
|
|
3471
5248
|
this.emptyState.style.display = 'none';
|
|
3472
5249
|
this.syncTerminalWorkspacePlacement(TERMINAL_WORKSPACE_TAB_KEY);
|
|
@@ -3479,6 +5256,60 @@ class EditorManager {
|
|
|
3479
5256
|
});
|
|
3480
5257
|
}
|
|
3481
5258
|
|
|
5259
|
+
activateMarkdownPreviewTab(filePath, isRestore = false) {
|
|
5260
|
+
if (!this.currentSession || !filePath) return;
|
|
5261
|
+
|
|
5262
|
+
const state = this.currentSession.editorState;
|
|
5263
|
+
if (!isRestore && state.activeFilePath && state.activeFilePath !== filePath) {
|
|
5264
|
+
const currentGlobal = this.getModel(state.activeFilePath);
|
|
5265
|
+
if (currentGlobal && currentGlobal.type === 'text' && this.editor) {
|
|
5266
|
+
state.viewStates.set(
|
|
5267
|
+
state.activeFilePath,
|
|
5268
|
+
this.editor.saveViewState()
|
|
5269
|
+
);
|
|
5270
|
+
}
|
|
5271
|
+
}
|
|
5272
|
+
|
|
5273
|
+
state.activeFilePath = filePath;
|
|
5274
|
+
this.currentSession.workspaceState.activeTabKey =
|
|
5275
|
+
makeMarkdownPreviewWorkspaceTabKey(filePath);
|
|
5276
|
+
this.currentSession.workspaceState.lastNonTerminalTabKey =
|
|
5277
|
+
makeMarkdownPreviewWorkspaceTabKey(filePath);
|
|
5278
|
+
if (!isRestore) {
|
|
5279
|
+
this.currentSession.saveState({ touchWorkspace: true });
|
|
5280
|
+
}
|
|
5281
|
+
const file = this.getModel(filePath);
|
|
5282
|
+
|
|
5283
|
+
this.renderEditorTabs();
|
|
5284
|
+
this.emptyState.style.display = 'none';
|
|
5285
|
+
this.syncTerminalWorkspacePlacement();
|
|
5286
|
+
|
|
5287
|
+
if (!file) {
|
|
5288
|
+
void this.openFile(filePath, true, {
|
|
5289
|
+
activatePreview: true,
|
|
5290
|
+
focusEditor: false
|
|
5291
|
+
});
|
|
5292
|
+
return;
|
|
5293
|
+
}
|
|
5294
|
+
|
|
5295
|
+
this.agentContainer.style.display = 'none';
|
|
5296
|
+
this.imagePreviewContainer.style.display = 'none';
|
|
5297
|
+
this.hidePdfPreview();
|
|
5298
|
+
if (this.isMarkdownSplitViewEnabled(this.currentSession, filePath)) {
|
|
5299
|
+
this.showMarkdownSplitView(filePath, {
|
|
5300
|
+
session: this.currentSession,
|
|
5301
|
+
focusEditor: false
|
|
5302
|
+
});
|
|
5303
|
+
} else {
|
|
5304
|
+
this.contentContainer.classList.remove('markdown-split-active');
|
|
5305
|
+
this.monacoContainer.style.display = 'none';
|
|
5306
|
+
this.markdownPreviewContainer.style.display = 'flex';
|
|
5307
|
+
void this.renderMarkdownPreview(filePath, {
|
|
5308
|
+
show: true
|
|
5309
|
+
});
|
|
5310
|
+
}
|
|
5311
|
+
}
|
|
5312
|
+
|
|
3482
5313
|
activateFileTab(filePath, isRestore = false, options = {}) {
|
|
3483
5314
|
if (!this.currentSession) return;
|
|
3484
5315
|
if (!filePath) return;
|
|
@@ -3496,7 +5327,9 @@ class EditorManager {
|
|
|
3496
5327
|
this.currentSession.workspaceState.activeTabKey = makeFileWorkspaceTabKey(filePath);
|
|
3497
5328
|
this.currentSession.workspaceState.lastNonTerminalTabKey =
|
|
3498
5329
|
makeFileWorkspaceTabKey(filePath);
|
|
3499
|
-
|
|
5330
|
+
if (!isRestore) {
|
|
5331
|
+
this.currentSession.saveState({ touchWorkspace: true });
|
|
5332
|
+
}
|
|
3500
5333
|
const file = this.getModel(filePath);
|
|
3501
5334
|
|
|
3502
5335
|
this.renderEditorTabs();
|
|
@@ -3511,7 +5344,9 @@ class EditorManager {
|
|
|
3511
5344
|
if (file.type === 'image') {
|
|
3512
5345
|
this.agentContainer.style.display = 'none';
|
|
3513
5346
|
this.monacoContainer.style.display = 'none';
|
|
5347
|
+
this.hideMarkdownPreview();
|
|
3514
5348
|
this.imagePreviewContainer.style.display = 'flex';
|
|
5349
|
+
this.hidePdfPreview();
|
|
3515
5350
|
|
|
3516
5351
|
this.imagePreview.onerror = () => {
|
|
3517
5352
|
alert(`Failed to load image: ${filePath.split('/').pop()}`, { type: 'error', title: 'Error' });
|
|
@@ -3522,9 +5357,55 @@ class EditorManager {
|
|
|
3522
5357
|
this.imagePreview.src = this.currentSession.server.resolveUrl(
|
|
3523
5358
|
`/api/fs/raw?path=${encodeURIComponent(filePath)}&token=${this.currentSession.server.token}`
|
|
3524
5359
|
);
|
|
5360
|
+
} else if (file.type === 'pdf') {
|
|
5361
|
+
this.agentContainer.style.display = 'none';
|
|
5362
|
+
this.monacoContainer.style.display = 'none';
|
|
5363
|
+
this.imagePreviewContainer.style.display = 'none';
|
|
5364
|
+
this.hideMarkdownPreview();
|
|
5365
|
+
this.pdfPreviewContainer.style.display = 'flex';
|
|
5366
|
+
void this.renderPdfPreview(filePath);
|
|
5367
|
+
} else if (isSupportedMarkdownPath(filePath)) {
|
|
5368
|
+
if (this.isMarkdownSplitViewEnabled(this.currentSession, filePath)) {
|
|
5369
|
+
this.showMarkdownSplitView(filePath, {
|
|
5370
|
+
session: this.currentSession,
|
|
5371
|
+
focusEditor
|
|
5372
|
+
});
|
|
5373
|
+
this.scheduleMarkdownPreviewRender(
|
|
5374
|
+
filePath,
|
|
5375
|
+
this.currentSession
|
|
5376
|
+
);
|
|
5377
|
+
return;
|
|
5378
|
+
}
|
|
5379
|
+
this.agentContainer.style.display = 'none';
|
|
5380
|
+
this.imagePreviewContainer.style.display = 'none';
|
|
5381
|
+
this.hidePdfPreview();
|
|
5382
|
+
this.hideMarkdownPreview();
|
|
5383
|
+
this.monacoContainer.style.display = 'block';
|
|
5384
|
+
if (!file.model && file.content !== null && this.monacoInstance) {
|
|
5385
|
+
file.model = this.monacoInstance.editor.createModel(
|
|
5386
|
+
file.content,
|
|
5387
|
+
undefined,
|
|
5388
|
+
this.monacoInstance.Uri.file(filePath)
|
|
5389
|
+
);
|
|
5390
|
+
}
|
|
5391
|
+
if (this.editor && file.model) {
|
|
5392
|
+
this.editor.setModel(file.model);
|
|
5393
|
+
this.editor.updateOptions({ readOnly: !!file.readonly });
|
|
5394
|
+
const savedViewState = state.viewStates.get(filePath);
|
|
5395
|
+
if (savedViewState) {
|
|
5396
|
+
this.editor.restoreViewState(savedViewState);
|
|
5397
|
+
}
|
|
5398
|
+
if (focusEditor) {
|
|
5399
|
+
this.editor.focus();
|
|
5400
|
+
}
|
|
5401
|
+
requestAnimationFrame(() => this.editor.layout());
|
|
5402
|
+
}
|
|
5403
|
+
this.scheduleMarkdownPreviewRender(filePath, this.currentSession);
|
|
3525
5404
|
} else {
|
|
3526
5405
|
this.agentContainer.style.display = 'none';
|
|
3527
5406
|
this.imagePreviewContainer.style.display = 'none';
|
|
5407
|
+
this.hidePdfPreview();
|
|
5408
|
+
this.hideMarkdownPreview();
|
|
3528
5409
|
this.monacoContainer.style.display = 'block';
|
|
3529
5410
|
|
|
3530
5411
|
if (!file.model && file.content !== null && this.monacoInstance) {
|
|
@@ -3545,6 +5426,7 @@ class EditorManager {
|
|
|
3545
5426
|
// Force layout to ensure content is visible
|
|
3546
5427
|
requestAnimationFrame(() => this.editor.layout());
|
|
3547
5428
|
}
|
|
5429
|
+
void this.checkActiveFileVersion();
|
|
3548
5430
|
}
|
|
3549
5431
|
}
|
|
3550
5432
|
|
|
@@ -3581,12 +5463,16 @@ class EditorManager {
|
|
|
3581
5463
|
this.currentSession.workspaceState.lastNonTerminalTabKey = agentTabKey;
|
|
3582
5464
|
noteRecentAgentTab(this.currentSession, agentTabKey);
|
|
3583
5465
|
agentTab.needsAttention = false;
|
|
3584
|
-
|
|
5466
|
+
if (!isRestore) {
|
|
5467
|
+
this.currentSession.saveState({ touchWorkspace: true });
|
|
5468
|
+
}
|
|
3585
5469
|
this.renderEditorTabs();
|
|
3586
5470
|
this.currentSession.updateTabUI();
|
|
3587
5471
|
this.syncTerminalWorkspacePlacement(agentTabKey);
|
|
3588
5472
|
this.monacoContainer.style.display = 'none';
|
|
3589
5473
|
this.imagePreviewContainer.style.display = 'none';
|
|
5474
|
+
this.hidePdfPreview();
|
|
5475
|
+
this.hideMarkdownPreview();
|
|
3590
5476
|
this.emptyState.style.display = 'none';
|
|
3591
5477
|
this.agentContainer.style.display = 'flex';
|
|
3592
5478
|
this.renderAgentPanel(agentTab);
|
|
@@ -5292,20 +7178,57 @@ class EditorManager {
|
|
|
5292
7178
|
const session = agentTab.getLinkedSession();
|
|
5293
7179
|
if (!session) return;
|
|
5294
7180
|
try {
|
|
7181
|
+
let targetAgentTab = null;
|
|
7182
|
+
let targetPromptDraft = '';
|
|
5295
7183
|
if (command.openTabKey) {
|
|
5296
7184
|
const existingTab = state.agentTabs.get(command.openTabKey);
|
|
5297
7185
|
const existingSession = existingTab?.getLinkedSession() || null;
|
|
5298
7186
|
if (existingTab && existingSession) {
|
|
5299
|
-
|
|
7187
|
+
targetPromptDraft = String(
|
|
7188
|
+
existingTab.promptDraft || ''
|
|
7189
|
+
);
|
|
7190
|
+
targetAgentTab = await activateAgentTab(
|
|
7191
|
+
existingSession,
|
|
7192
|
+
existingTab,
|
|
7193
|
+
{
|
|
5300
7194
|
switchSession: true
|
|
5301
|
-
|
|
7195
|
+
}
|
|
7196
|
+
);
|
|
5302
7197
|
} else {
|
|
5303
|
-
await resumeAgentTabFromHistory(
|
|
7198
|
+
targetAgentTab = await resumeAgentTabFromHistory(
|
|
7199
|
+
session,
|
|
7200
|
+
agentTab,
|
|
7201
|
+
command
|
|
7202
|
+
);
|
|
7203
|
+
targetPromptDraft = String(
|
|
7204
|
+
targetAgentTab?.promptDraft || ''
|
|
7205
|
+
);
|
|
5304
7206
|
}
|
|
5305
7207
|
} else {
|
|
5306
|
-
await resumeAgentTabFromHistory(
|
|
7208
|
+
targetAgentTab = await resumeAgentTabFromHistory(
|
|
7209
|
+
session,
|
|
7210
|
+
agentTab,
|
|
7211
|
+
command
|
|
7212
|
+
);
|
|
7213
|
+
targetPromptDraft = String(
|
|
7214
|
+
targetAgentTab?.promptDraft || ''
|
|
7215
|
+
);
|
|
5307
7216
|
}
|
|
5308
|
-
|
|
7217
|
+
const targetPromptIntent = getAgentPromptIntent(
|
|
7218
|
+
targetAgentTab,
|
|
7219
|
+
targetPromptDraft
|
|
7220
|
+
);
|
|
7221
|
+
if (targetPromptIntent.kind === 'resume') {
|
|
7222
|
+
targetPromptDraft = '';
|
|
7223
|
+
if (targetAgentTab) {
|
|
7224
|
+
targetAgentTab.promptDraft = '';
|
|
7225
|
+
}
|
|
7226
|
+
}
|
|
7227
|
+
agentTab.promptDraft = '';
|
|
7228
|
+
this.setAgentPromptValue(
|
|
7229
|
+
targetPromptDraft,
|
|
7230
|
+
targetAgentTab || getActiveAgentTab() || agentTab
|
|
7231
|
+
);
|
|
5309
7232
|
} catch (error) {
|
|
5310
7233
|
alert(error.message, {
|
|
5311
7234
|
type: 'error',
|
|
@@ -5327,6 +7250,8 @@ class EditorManager {
|
|
|
5327
7250
|
showEmptyState() {
|
|
5328
7251
|
this.monacoContainer.style.display = 'none';
|
|
5329
7252
|
this.imagePreviewContainer.style.display = 'none';
|
|
7253
|
+
this.hidePdfPreview();
|
|
7254
|
+
this.hideMarkdownPreview();
|
|
5330
7255
|
this.agentContainer.style.display = 'none';
|
|
5331
7256
|
this.emptyState.style.display = 'flex';
|
|
5332
7257
|
this.syncTerminalWorkspacePlacement('');
|
|
@@ -5994,13 +7919,31 @@ class Session {
|
|
|
5994
7919
|
: Array.from(this.server.expandedPaths)
|
|
5995
7920
|
}
|
|
5996
7921
|
);
|
|
7922
|
+
const sharedActiveWorkspaceTabKey = typeof (
|
|
7923
|
+
this.sharedWorkspaceState.activeWorkspaceTabKey
|
|
7924
|
+
) === 'string'
|
|
7925
|
+
? this.sharedWorkspaceState.activeWorkspaceTabKey
|
|
7926
|
+
: '';
|
|
7927
|
+
const initialActiveWorkspaceTabKey = (
|
|
7928
|
+
isFileWorkspaceTabKey(sharedActiveWorkspaceTabKey)
|
|
7929
|
+
&& !this.sharedWorkspaceState.openFiles.includes(
|
|
7930
|
+
workspaceKeyToFilePath(sharedActiveWorkspaceTabKey)
|
|
7931
|
+
)
|
|
7932
|
+
)
|
|
7933
|
+
? ''
|
|
7934
|
+
: sharedActiveWorkspaceTabKey;
|
|
7935
|
+
const preferredActiveFilePath = isFileWorkspaceTabKey(
|
|
7936
|
+
initialActiveWorkspaceTabKey
|
|
7937
|
+
)
|
|
7938
|
+
? workspaceKeyToFilePath(initialActiveWorkspaceTabKey)
|
|
7939
|
+
: legacyEditorState.activeFilePath;
|
|
5997
7940
|
const initialActiveFilePath = (
|
|
5998
|
-
typeof
|
|
7941
|
+
typeof preferredActiveFilePath === 'string'
|
|
5999
7942
|
&& this.sharedWorkspaceState.openFiles.includes(
|
|
6000
|
-
|
|
7943
|
+
preferredActiveFilePath
|
|
6001
7944
|
)
|
|
6002
7945
|
)
|
|
6003
|
-
?
|
|
7946
|
+
? preferredActiveFilePath
|
|
6004
7947
|
: (this.sharedWorkspaceState.openFiles[0] || null);
|
|
6005
7948
|
|
|
6006
7949
|
this.editorState = {
|
|
@@ -6011,15 +7954,15 @@ class Session {
|
|
|
6011
7954
|
viewStates: new Map() // Path -> ViewState
|
|
6012
7955
|
};
|
|
6013
7956
|
this.workspaceState = {
|
|
6014
|
-
activeTabKey:
|
|
7957
|
+
activeTabKey: initialActiveWorkspaceTabKey
|
|
6015
7958
|
|| (initialActiveFilePath
|
|
6016
7959
|
? makeFileWorkspaceTabKey(initialActiveFilePath)
|
|
6017
7960
|
: ''),
|
|
6018
|
-
lastNonTerminalTabKey:
|
|
7961
|
+
lastNonTerminalTabKey: initialActiveWorkspaceTabKey
|
|
6019
7962
|
&& !isTerminalWorkspaceTabKey(
|
|
6020
|
-
|
|
7963
|
+
initialActiveWorkspaceTabKey
|
|
6021
7964
|
)
|
|
6022
|
-
?
|
|
7965
|
+
? initialActiveWorkspaceTabKey
|
|
6023
7966
|
: (initialActiveFilePath
|
|
6024
7967
|
? makeFileWorkspaceTabKey(initialActiveFilePath)
|
|
6025
7968
|
: ''),
|
|
@@ -6029,7 +7972,8 @@ class Session {
|
|
|
6029
7972
|
? legacyEditorState.recentAgentTabKeys.filter(
|
|
6030
7973
|
(key) => typeof key === 'string' && key.length > 0
|
|
6031
7974
|
)
|
|
6032
|
-
: []
|
|
7975
|
+
: [],
|
|
7976
|
+
markdownSplitPath: this.sharedWorkspaceState.markdownSplitPath || ''
|
|
6033
7977
|
};
|
|
6034
7978
|
|
|
6035
7979
|
this.layoutState = {
|
|
@@ -6166,6 +8110,7 @@ class Session {
|
|
|
6166
8110
|
this.sharedWorkspaceState = normalized;
|
|
6167
8111
|
this.editorState.isVisible = normalized.isVisible;
|
|
6168
8112
|
this.editorState.openFiles = [...normalized.openFiles];
|
|
8113
|
+
this.workspaceState.markdownSplitPath = normalized.markdownSplitPath;
|
|
6169
8114
|
|
|
6170
8115
|
if (
|
|
6171
8116
|
this.editorState.activeFilePath
|
|
@@ -6180,7 +8125,13 @@ class Session {
|
|
|
6180
8125
|
const activeKey = this.workspaceState.activeTabKey || '';
|
|
6181
8126
|
if (isFileWorkspaceTabKey(activeKey)) {
|
|
6182
8127
|
const filePath = workspaceKeyToFilePath(activeKey);
|
|
6183
|
-
if (
|
|
8128
|
+
if (
|
|
8129
|
+
!this.editorState.openFiles.includes(filePath)
|
|
8130
|
+
|| (
|
|
8131
|
+
isMarkdownPreviewWorkspaceTabKey(activeKey)
|
|
8132
|
+
&& !isSupportedMarkdownPath(filePath)
|
|
8133
|
+
)
|
|
8134
|
+
) {
|
|
6184
8135
|
this.workspaceState.activeTabKey = resolveFallbackActiveKey();
|
|
6185
8136
|
}
|
|
6186
8137
|
} else if (
|
|
@@ -6194,7 +8145,13 @@ class Session {
|
|
|
6194
8145
|
this.workspaceState.lastNonTerminalTabKey || '';
|
|
6195
8146
|
if (isFileWorkspaceTabKey(lastNonTerminalKey)) {
|
|
6196
8147
|
const filePath = workspaceKeyToFilePath(lastNonTerminalKey);
|
|
6197
|
-
if (
|
|
8148
|
+
if (
|
|
8149
|
+
!this.editorState.openFiles.includes(filePath)
|
|
8150
|
+
|| (
|
|
8151
|
+
isMarkdownPreviewWorkspaceTabKey(lastNonTerminalKey)
|
|
8152
|
+
&& !isSupportedMarkdownPath(filePath)
|
|
8153
|
+
)
|
|
8154
|
+
) {
|
|
6198
8155
|
this.workspaceState.lastNonTerminalTabKey = '';
|
|
6199
8156
|
}
|
|
6200
8157
|
}
|
|
@@ -7818,6 +9775,25 @@ function getActiveServer() {
|
|
|
7818
9775
|
return getActiveSession()?.server || getMainServer();
|
|
7819
9776
|
}
|
|
7820
9777
|
|
|
9778
|
+
function getDocumentTitle() {
|
|
9779
|
+
const server = getActiveServer();
|
|
9780
|
+
if (!server) {
|
|
9781
|
+
return 'Tabminal';
|
|
9782
|
+
}
|
|
9783
|
+
const host = String(getDisplayHost(server) || '').trim();
|
|
9784
|
+
if (!host || host.toLowerCase() === 'unknown') {
|
|
9785
|
+
return 'Tabminal';
|
|
9786
|
+
}
|
|
9787
|
+
return `Tabminal: ${host}`;
|
|
9788
|
+
}
|
|
9789
|
+
|
|
9790
|
+
function updateDocumentTitle() {
|
|
9791
|
+
const nextTitle = getDocumentTitle();
|
|
9792
|
+
if (document.title !== nextTitle) {
|
|
9793
|
+
document.title = nextTitle;
|
|
9794
|
+
}
|
|
9795
|
+
}
|
|
9796
|
+
|
|
7821
9797
|
function getSessionsForServer(serverId) {
|
|
7822
9798
|
return Array.from(state.sessions.values()).filter(
|
|
7823
9799
|
session => session.serverId === serverId
|
|
@@ -7984,6 +9960,9 @@ function getWorkspaceTabKeysForSession(session) {
|
|
|
7984
9960
|
}
|
|
7985
9961
|
for (const path of session.editorState?.openFiles || []) {
|
|
7986
9962
|
keys.push(makeFileWorkspaceTabKey(path));
|
|
9963
|
+
if (isSupportedMarkdownPath(path)) {
|
|
9964
|
+
keys.push(makeMarkdownPreviewWorkspaceTabKey(path));
|
|
9965
|
+
}
|
|
7987
9966
|
}
|
|
7988
9967
|
for (const agentTab of getAgentTabsForSession(session)) {
|
|
7989
9968
|
keys.push(agentTab.key);
|
|
@@ -8147,6 +10126,7 @@ function normalizeAgentSessionCapabilities(sessionCapabilities) {
|
|
|
8147
10126
|
return {
|
|
8148
10127
|
load: !!source.load,
|
|
8149
10128
|
list: !!source.list,
|
|
10129
|
+
listAll: !!source.listAll,
|
|
8150
10130
|
resume: !!source.resume,
|
|
8151
10131
|
fork: !!source.fork
|
|
8152
10132
|
};
|
|
@@ -8564,6 +10544,9 @@ function getAgentResumeSuggestions(agentTab, promptValue, sessions = []) {
|
|
|
8564
10544
|
const intent = getAgentPromptIntent(agentTab, promptValue);
|
|
8565
10545
|
if (intent.kind !== 'resume') return [];
|
|
8566
10546
|
const query = String(intent.query || '').toLowerCase();
|
|
10547
|
+
const currentCwd = String(
|
|
10548
|
+
agentTab?.cwd || agentTab?.getLinkedSession?.()?.cwd || ''
|
|
10549
|
+
).trim().toLowerCase();
|
|
8567
10550
|
const openSessions = getOpenAgentSessionsForServer(
|
|
8568
10551
|
agentTab?.serverId,
|
|
8569
10552
|
agentTab?.agentId
|
|
@@ -8577,24 +10560,30 @@ function getAgentResumeSuggestions(agentTab, promptValue, sessions = []) {
|
|
|
8577
10560
|
).toLowerCase();
|
|
8578
10561
|
const cwd = String(session.cwd || '').toLowerCase();
|
|
8579
10562
|
const sessionId = String(session.sessionId || '').toLowerCase();
|
|
10563
|
+
const cwdMatch = !!currentCwd && cwd === currentCwd;
|
|
8580
10564
|
const titleMatch = !query || displayName.includes(query);
|
|
8581
|
-
const otherMatch = !query
|
|
10565
|
+
const otherMatch = !query
|
|
10566
|
+
|| cwd.includes(query)
|
|
10567
|
+
|| sessionId.includes(query);
|
|
8582
10568
|
return {
|
|
8583
10569
|
session,
|
|
8584
10570
|
index,
|
|
10571
|
+
cwdMatch,
|
|
8585
10572
|
titleMatch,
|
|
8586
10573
|
matched: titleMatch || otherMatch
|
|
8587
10574
|
};
|
|
8588
10575
|
})
|
|
8589
10576
|
.filter(({ matched }) => matched)
|
|
8590
10577
|
.sort((left, right) => {
|
|
10578
|
+
if (left.cwdMatch !== right.cwdMatch) {
|
|
10579
|
+
return left.cwdMatch ? -1 : 1;
|
|
10580
|
+
}
|
|
8591
10581
|
if (left.titleMatch !== right.titleMatch) {
|
|
8592
10582
|
return left.titleMatch ? -1 : 1;
|
|
8593
10583
|
}
|
|
8594
10584
|
return left.index - right.index;
|
|
8595
10585
|
})
|
|
8596
10586
|
.map(({ session }) => session)
|
|
8597
|
-
.slice(0, 12)
|
|
8598
10587
|
.map((session) => ({
|
|
8599
10588
|
...session,
|
|
8600
10589
|
openTabKey: openSessions.get(session.sessionId)?.key || '',
|
|
@@ -11135,7 +13124,7 @@ async function syncAgentsForServer(server, { force = false } = {}) {
|
|
|
11135
13124
|
&& state.agentTabs.has(activeKey)
|
|
11136
13125
|
) {
|
|
11137
13126
|
noteRecentAgentTab(session, activeKey);
|
|
11138
|
-
session.saveState();
|
|
13127
|
+
session.saveState({ touchWorkspace: true });
|
|
11139
13128
|
}
|
|
11140
13129
|
}
|
|
11141
13130
|
|
|
@@ -11169,7 +13158,7 @@ async function syncAgentsForServer(server, { force = false } = {}) {
|
|
|
11169
13158
|
getAgentTabsForSession(preferredSession)[0]?.key || ''
|
|
11170
13159
|
);
|
|
11171
13160
|
}
|
|
11172
|
-
preferredSession.saveState();
|
|
13161
|
+
preferredSession.saveState({ touchWorkspace: true });
|
|
11173
13162
|
if (state.activeSessionKey === preferredSession.key) {
|
|
11174
13163
|
restoreWorkspaceForSession(preferredSession);
|
|
11175
13164
|
} else {
|
|
@@ -11208,7 +13197,7 @@ async function activateAgentTab(session, agentTab, options = {}) {
|
|
|
11208
13197
|
}
|
|
11209
13198
|
session.workspaceState.activeTabKey = agentTab.key;
|
|
11210
13199
|
noteRecentAgentTab(session, agentTab.key);
|
|
11211
|
-
session.saveState();
|
|
13200
|
+
session.saveState({ touchWorkspace: true });
|
|
11212
13201
|
if (state.activeSessionKey === session.key) {
|
|
11213
13202
|
restoreWorkspaceForSession(session);
|
|
11214
13203
|
requestAnimationFrame(() => {
|
|
@@ -11481,6 +13470,7 @@ async function syncServer(server) {
|
|
|
11481
13470
|
}
|
|
11482
13471
|
|
|
11483
13472
|
const updates = { sessions: [] };
|
|
13473
|
+
const sentFileWrites = new Map();
|
|
11484
13474
|
for (const [sessionKey, pending] of pendingChanges.sessions) {
|
|
11485
13475
|
const { serverId, sessionId } = splitSessionKey(sessionKey);
|
|
11486
13476
|
if (serverId !== server.id) continue;
|
|
@@ -11497,10 +13487,31 @@ async function syncServer(server) {
|
|
|
11497
13487
|
hasUpdate = true;
|
|
11498
13488
|
}
|
|
11499
13489
|
if (pending.fileWrites && pending.fileWrites.size > 0) {
|
|
11500
|
-
|
|
13490
|
+
const fileWrites = Array.from(
|
|
11501
13491
|
pending.fileWrites.entries()
|
|
11502
|
-
)
|
|
11503
|
-
|
|
13492
|
+
)
|
|
13493
|
+
.map(([path, write]) => ({
|
|
13494
|
+
path,
|
|
13495
|
+
write: editorManager.normalizePendingFileWrite(write)
|
|
13496
|
+
}))
|
|
13497
|
+
.filter(({ write }) => !write.blocked);
|
|
13498
|
+
if (fileWrites.length > 0) {
|
|
13499
|
+
sessionUpdate.fileWrites = fileWrites.map(
|
|
13500
|
+
({ path, write }) => ({
|
|
13501
|
+
path,
|
|
13502
|
+
content: write.content,
|
|
13503
|
+
expectedVersion: write.expectedVersion,
|
|
13504
|
+
force: write.force === true
|
|
13505
|
+
})
|
|
13506
|
+
);
|
|
13507
|
+
sentFileWrites.set(
|
|
13508
|
+
sessionUpdate.id,
|
|
13509
|
+
new Map(
|
|
13510
|
+
fileWrites.map(({ path, write }) => [path, write])
|
|
13511
|
+
)
|
|
13512
|
+
);
|
|
13513
|
+
hasUpdate = true;
|
|
13514
|
+
}
|
|
11504
13515
|
}
|
|
11505
13516
|
|
|
11506
13517
|
if (hasUpdate) {
|
|
@@ -11544,11 +13555,6 @@ async function syncServer(server) {
|
|
|
11544
13555
|
|
|
11545
13556
|
if (update.resize) delete pending.resize;
|
|
11546
13557
|
if (update.workspaceState) delete pending.workspaceState;
|
|
11547
|
-
if (update.fileWrites) {
|
|
11548
|
-
for (const file of update.fileWrites) {
|
|
11549
|
-
pending.fileWrites.delete(file.path);
|
|
11550
|
-
}
|
|
11551
|
-
}
|
|
11552
13558
|
}
|
|
11553
13559
|
|
|
11554
13560
|
const data = await response.json();
|
|
@@ -11575,6 +13581,33 @@ async function syncServer(server) {
|
|
|
11575
13581
|
const sessions = Array.isArray(data) ? data : data.sessions;
|
|
11576
13582
|
reconcileSessions(server, sessions || []);
|
|
11577
13583
|
reconcileAgentInventory(server, data.agents);
|
|
13584
|
+
await editorManager.applyFileWriteResults(
|
|
13585
|
+
server,
|
|
13586
|
+
Array.isArray(data?.fileWriteResults)
|
|
13587
|
+
? data.fileWriteResults
|
|
13588
|
+
: [],
|
|
13589
|
+
sentFileWrites
|
|
13590
|
+
);
|
|
13591
|
+
|
|
13592
|
+
for (const [sessionId, writes] of sentFileWrites.entries()) {
|
|
13593
|
+
const pending = pendingChanges.sessions.get(
|
|
13594
|
+
makeSessionKey(server.id, sessionId)
|
|
13595
|
+
);
|
|
13596
|
+
if (!pending?.fileWrites) {
|
|
13597
|
+
continue;
|
|
13598
|
+
}
|
|
13599
|
+
for (const [path] of writes.entries()) {
|
|
13600
|
+
if (!pending.fileWrites.has(path)) {
|
|
13601
|
+
continue;
|
|
13602
|
+
}
|
|
13603
|
+
const current = editorManager.normalizePendingFileWrite(
|
|
13604
|
+
pending.fileWrites.get(path)
|
|
13605
|
+
);
|
|
13606
|
+
if (!current.blocked) {
|
|
13607
|
+
pending.fileWrites.delete(path);
|
|
13608
|
+
}
|
|
13609
|
+
}
|
|
13610
|
+
}
|
|
11578
13611
|
} catch (error) {
|
|
11579
13612
|
if (!wasReconnecting) {
|
|
11580
13613
|
console.warn(
|
|
@@ -12096,7 +14129,7 @@ function reconcileSessions(server, remoteSessions) {
|
|
|
12096
14129
|
}
|
|
12097
14130
|
}
|
|
12098
14131
|
|
|
12099
|
-
async function createNewSession(server = getActiveServer()) {
|
|
14132
|
+
async function createNewSession(server = getActiveServer(), options = {}) {
|
|
12100
14133
|
if (!server) return;
|
|
12101
14134
|
if (server.needsLogin || !server.isAuthenticated) {
|
|
12102
14135
|
const password = window.prompt(`Password for ${getDisplayHost(server)}`);
|
|
@@ -12104,16 +14137,15 @@ async function createNewSession(server = getActiveServer()) {
|
|
|
12104
14137
|
await server.login(password);
|
|
12105
14138
|
}
|
|
12106
14139
|
try {
|
|
12107
|
-
const
|
|
12108
|
-
|
|
12109
|
-
|
|
12110
|
-
options.cwd = activeSession.cwd;
|
|
14140
|
+
const request = {};
|
|
14141
|
+
if (typeof options.cwd === 'string' && options.cwd.trim()) {
|
|
14142
|
+
request.cwd = options.cwd.trim();
|
|
12111
14143
|
}
|
|
12112
14144
|
|
|
12113
14145
|
const response = await server.fetch('/api/sessions', {
|
|
12114
14146
|
method: 'POST',
|
|
12115
14147
|
headers: { 'Content-Type': 'application/json' },
|
|
12116
|
-
body: JSON.stringify(
|
|
14148
|
+
body: JSON.stringify(request)
|
|
12117
14149
|
});
|
|
12118
14150
|
if (!response.ok) throw new Error('Failed to create session');
|
|
12119
14151
|
const newSession = await response.json();
|
|
@@ -12141,6 +14173,7 @@ function removeSession(key) {
|
|
|
12141
14173
|
|
|
12142
14174
|
// #region UI Logic
|
|
12143
14175
|
function renderTabs() {
|
|
14176
|
+
updateDocumentTitle();
|
|
12144
14177
|
if (!tabListEl) return;
|
|
12145
14178
|
|
|
12146
14179
|
const newTabItem = document.getElementById('new-tab-item');
|
|
@@ -12561,7 +14594,8 @@ const confirmModalState = {
|
|
|
12561
14594
|
resolve: null,
|
|
12562
14595
|
returnFocus: null,
|
|
12563
14596
|
preferredFocus: 'confirm',
|
|
12564
|
-
hideCancel: false
|
|
14597
|
+
hideCancel: false,
|
|
14598
|
+
allowDismiss: true
|
|
12565
14599
|
};
|
|
12566
14600
|
|
|
12567
14601
|
function isConfirmModalOpen() {
|
|
@@ -12600,6 +14634,7 @@ function settleConfirmModal(result) {
|
|
|
12600
14634
|
confirmModalState.returnFocus = null;
|
|
12601
14635
|
confirmModalState.preferredFocus = 'confirm';
|
|
12602
14636
|
confirmModalState.hideCancel = false;
|
|
14637
|
+
confirmModalState.allowDismiss = true;
|
|
12603
14638
|
if (returnFocus instanceof HTMLElement) {
|
|
12604
14639
|
requestAnimationFrame(() => {
|
|
12605
14640
|
try {
|
|
@@ -12617,8 +14652,11 @@ function showConfirmModal({
|
|
|
12617
14652
|
message = '',
|
|
12618
14653
|
note = '',
|
|
12619
14654
|
confirmLabel = 'Confirm',
|
|
14655
|
+
cancelLabel = 'Cancel',
|
|
12620
14656
|
danger = false,
|
|
12621
14657
|
hideCancel = false,
|
|
14658
|
+
preferredFocus = 'confirm',
|
|
14659
|
+
allowDismiss = true,
|
|
12622
14660
|
returnFocus = null
|
|
12623
14661
|
} = {}) {
|
|
12624
14662
|
if (
|
|
@@ -12638,13 +14676,17 @@ function showConfirmModal({
|
|
|
12638
14676
|
confirmModalMessage.textContent = message;
|
|
12639
14677
|
confirmModalNote.textContent = note;
|
|
12640
14678
|
confirmModalNote.style.display = note ? '' : 'none';
|
|
14679
|
+
confirmModalCancel.textContent = cancelLabel;
|
|
12641
14680
|
confirmModalCancel.style.display = hideCancel ? 'none' : '';
|
|
12642
14681
|
confirmModalConfirm.textContent = confirmLabel;
|
|
12643
14682
|
confirmModalConfirm.classList.toggle('danger-button', danger);
|
|
12644
14683
|
confirmModal.style.display = 'flex';
|
|
12645
14684
|
confirmModalState.returnFocus = returnFocus;
|
|
12646
14685
|
confirmModalState.hideCancel = hideCancel;
|
|
12647
|
-
confirmModalState.preferredFocus = '
|
|
14686
|
+
confirmModalState.preferredFocus = preferredFocus === 'cancel'
|
|
14687
|
+
? 'cancel'
|
|
14688
|
+
: 'confirm';
|
|
14689
|
+
confirmModalState.allowDismiss = allowDismiss !== false;
|
|
12648
14690
|
requestAnimationFrame(() => {
|
|
12649
14691
|
getConfirmModalPreferredButton()?.focus({ preventScroll: true });
|
|
12650
14692
|
});
|
|
@@ -12679,6 +14721,7 @@ function moveConfirmModalFocus(delta) {
|
|
|
12679
14721
|
}
|
|
12680
14722
|
|
|
12681
14723
|
function renderServerControls() {
|
|
14724
|
+
updateDocumentTitle();
|
|
12682
14725
|
if (!serverControlsEl) return;
|
|
12683
14726
|
serverControlsEl.innerHTML = '';
|
|
12684
14727
|
|
|
@@ -12834,12 +14877,14 @@ window.addEventListener('focus', () => {
|
|
|
12834
14877
|
enterAppNotificationQuietPeriod();
|
|
12835
14878
|
editorManager.refreshVisibleSessionTrees();
|
|
12836
14879
|
editorManager.updateTreeAutoRefresh();
|
|
14880
|
+
void editorManager.checkActiveFileVersion();
|
|
12837
14881
|
});
|
|
12838
14882
|
window.addEventListener('pageshow', () => {
|
|
12839
14883
|
noteAppInteraction();
|
|
12840
14884
|
enterAppNotificationQuietPeriod();
|
|
12841
14885
|
editorManager.refreshVisibleSessionTrees();
|
|
12842
14886
|
editorManager.updateTreeAutoRefresh();
|
|
14887
|
+
void editorManager.checkActiveFileVersion();
|
|
12843
14888
|
});
|
|
12844
14889
|
|
|
12845
14890
|
document.addEventListener('click', () => {
|
|
@@ -12851,6 +14896,7 @@ document.addEventListener('visibilitychange', () => {
|
|
|
12851
14896
|
enterAppNotificationQuietPeriod();
|
|
12852
14897
|
clearVisibleAttentionState();
|
|
12853
14898
|
editorManager.refreshVisibleSessionTrees();
|
|
14899
|
+
void editorManager.checkActiveFileVersion();
|
|
12854
14900
|
}
|
|
12855
14901
|
editorManager.updateTreeAutoRefresh();
|
|
12856
14902
|
});
|
|
@@ -13048,19 +15094,19 @@ window.addEventListener('tabminal:layout-modechange', () => {
|
|
|
13048
15094
|
&& terminalEl.contains(activeElement)
|
|
13049
15095
|
);
|
|
13050
15096
|
|
|
13051
|
-
|
|
13052
|
-
|
|
13053
|
-
|
|
13054
|
-
|
|
15097
|
+
if (isForcedTerminalWorkspaceMode()) {
|
|
15098
|
+
if (terminalHasFocus) {
|
|
15099
|
+
session.workspaceState.activeTabKey = TERMINAL_WORKSPACE_TAB_KEY;
|
|
15100
|
+
session.saveState({ touchWorkspace: true });
|
|
15101
|
+
}
|
|
15102
|
+
} else if (
|
|
15103
|
+
!editorManager.isTerminalTabPinned(session)
|
|
15104
|
+
&& isTerminalWorkspaceTabKey(session.workspaceState?.activeTabKey || '')
|
|
15105
|
+
) {
|
|
15106
|
+
session.workspaceState.activeTabKey =
|
|
15107
|
+
editorManager.getPreferredNonTerminalWorkspaceTabKey(session);
|
|
15108
|
+
session.saveState({ touchWorkspace: true });
|
|
13055
15109
|
}
|
|
13056
|
-
} else if (
|
|
13057
|
-
!editorManager.isTerminalTabPinned(session)
|
|
13058
|
-
&& isTerminalWorkspaceTabKey(session.workspaceState?.activeTabKey || '')
|
|
13059
|
-
) {
|
|
13060
|
-
session.workspaceState.activeTabKey =
|
|
13061
|
-
editorManager.getPreferredNonTerminalWorkspaceTabKey(session);
|
|
13062
|
-
session.saveState();
|
|
13063
|
-
}
|
|
13064
15110
|
|
|
13065
15111
|
editorManager.switchTo(session);
|
|
13066
15112
|
editorManager.updateEditorPaneVisibility();
|
|
@@ -13281,7 +15327,10 @@ if (
|
|
|
13281
15327
|
});
|
|
13282
15328
|
|
|
13283
15329
|
confirmModal.addEventListener('click', (event) => {
|
|
13284
|
-
if (
|
|
15330
|
+
if (
|
|
15331
|
+
event.target === confirmModal
|
|
15332
|
+
&& confirmModalState.allowDismiss
|
|
15333
|
+
) {
|
|
13285
15334
|
settleConfirmModal(false);
|
|
13286
15335
|
}
|
|
13287
15336
|
});
|
|
@@ -13292,6 +15341,11 @@ if (
|
|
|
13292
15341
|
|
|
13293
15342
|
confirmModal.addEventListener('keydown', (event) => {
|
|
13294
15343
|
if (event.key === 'Escape') {
|
|
15344
|
+
if (!confirmModalState.allowDismiss) {
|
|
15345
|
+
event.preventDefault();
|
|
15346
|
+
event.stopPropagation();
|
|
15347
|
+
return;
|
|
15348
|
+
}
|
|
13295
15349
|
event.preventDefault();
|
|
13296
15350
|
settleConfirmModal(false);
|
|
13297
15351
|
return;
|