@xcanwin/manyoyo 6.2.15 → 6.2.17

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.
@@ -1082,7 +1082,7 @@ body.workspace-switcher-open .workspace-switcher-panel {
1082
1082
  display: flex;
1083
1083
  align-items: center;
1084
1084
  gap: 8px;
1085
- flex-wrap: wrap;
1085
+ flex-wrap: nowrap;
1086
1086
  padding: 12px 14px;
1087
1087
  border-bottom: 1px solid var(--line);
1088
1088
  background: rgba(255, 251, 244, 0.96);
@@ -2175,6 +2175,20 @@ body.composer-options-open .composer-options-panel {
2175
2175
  border-bottom: 1px solid var(--line);
2176
2176
  }
2177
2177
 
2178
+ .files-toolbar {
2179
+ flex-wrap: wrap;
2180
+ }
2181
+
2182
+ .files-toolbar-path-group {
2183
+ flex-basis: 100%;
2184
+ }
2185
+
2186
+ .files-toolbar-meta {
2187
+ flex-basis: 100%;
2188
+ flex-wrap: wrap;
2189
+ margin-left: 0;
2190
+ }
2191
+
2178
2192
  .files-browser[data-mobile-pane="list"] .files-preview {
2179
2193
  display: none;
2180
2194
  }
@@ -99,6 +99,7 @@
99
99
  <div class="files-toolbar-meta">
100
100
  <div class="files-toolbar-status" data-role="status">未加载</div>
101
101
  <button type="button" class="secondary" data-action="mkdir">新建目录</button>
102
+ <button type="button" class="secondary" data-action="new-file">新建文件</button>
102
103
  </div>
103
104
  </header>
104
105
  <div class="files-layout">
@@ -132,6 +133,7 @@
132
133
  const filesBrowserNode = root.querySelector('.files-browser');
133
134
  const visitBtn = root.querySelector('[data-action="visit"]');
134
135
  const mkdirBtn = root.querySelector('[data-action="mkdir"]');
136
+ const newFileBtn = root.querySelector('[data-action="new-file"]');
135
137
  const saveBtn = root.querySelector('[data-action="save"]');
136
138
  const toggleMarkdownViewBtn = root.querySelector('[data-action="toggle-markdown-view"]');
137
139
  const backToListBtn = root.querySelector('[data-action="back-to-list"]');
@@ -260,6 +262,17 @@
260
262
  return host;
261
263
  }
262
264
 
265
+ function updatePreviewMeta() {
266
+ const payload = state.selectedFile;
267
+ if (!previewMetaNode || !payload) {
268
+ return;
269
+ }
270
+ const modeLabel = payload.kind === 'text'
271
+ ? (payload.editable === true ? '可编辑' : '只读预览')
272
+ : '只读预览';
273
+ previewMetaNode.textContent = `${payload.kind === 'text' ? '文本文件' : '文件'} · ${formatBytes(payload.size)} · ${modeLabel}${payload.truncated ? ' · 已截断预览' : ''}`;
274
+ }
275
+
263
276
  function renderPreviewPayload(payload) {
264
277
  state.selectedFile = payload || null;
265
278
  state.previewReadOnly = !(payload && payload.editable === true);
@@ -271,12 +284,7 @@
271
284
  if (previewTitleNode) {
272
285
  previewTitleNode.textContent = payload.path || '未命名文件';
273
286
  }
274
- if (previewMetaNode) {
275
- const modeLabel = payload.kind === 'text'
276
- ? (payload.editable === true ? '可编辑' : '只读预览')
277
- : '只读预览';
278
- previewMetaNode.textContent = `${payload.kind === 'text' ? '文本文件' : '文件'} · ${formatBytes(payload.size)} · ${modeLabel}${payload.truncated ? ' · 已截断预览' : ''}`;
279
- }
287
+ updatePreviewMeta();
280
288
  if (!previewBodyNode) {
281
289
  syncMarkdownToggleButton();
282
290
  syncSaveButton();
@@ -315,8 +323,12 @@
315
323
  doc: String(payload.content || ''),
316
324
  language,
317
325
  readOnly: state.previewReadOnly,
318
- onChange: function () {
326
+ onChange: function (nextValue) {
319
327
  state.previewDirty = true;
328
+ if (state.selectedFile) {
329
+ state.selectedFile.size = new TextEncoder().encode(nextValue).length;
330
+ }
331
+ updatePreviewMeta();
320
332
  syncSaveButton();
321
333
  }
322
334
  });
@@ -364,6 +376,9 @@
364
376
  if (mkdirBtn) {
365
377
  mkdirBtn.disabled = state.loadingList || state.loadingFile || !state.sessionName || state.historyOnly === true;
366
378
  }
379
+ if (newFileBtn) {
380
+ newFileBtn.disabled = state.loadingList || state.loadingFile || !state.sessionName || state.historyOnly === true;
381
+ }
367
382
  if (!listNode) {
368
383
  return;
369
384
  }
@@ -596,6 +611,30 @@
596
611
  }
597
612
  }
598
613
 
614
+ async function createFile() {
615
+ if (!state.sessionName || state.historyOnly === true || state.loadingList || state.loadingFile) {
616
+ return;
617
+ }
618
+ const input = await promptFn('请输入新文件名称');
619
+ const name = String(input || '').trim();
620
+ if (!name) {
621
+ return;
622
+ }
623
+ const targetPath = joinDirectoryPath(state.currentPath || state.containerPath || '/', name);
624
+ setStatus('创建文件中');
625
+ try {
626
+ await api('/api/sessions/' + encodeURIComponent(state.sessionName) + '/fs/create', {
627
+ method: 'POST',
628
+ body: JSON.stringify({ path: targetPath })
629
+ });
630
+ await loadDirectory(state.currentPath || state.containerPath || '/');
631
+ setStatus('已创建文件');
632
+ } catch (e) {
633
+ setStatus('创建文件失败');
634
+ onError(e && e.message ? e.message : '创建文件失败');
635
+ }
636
+ }
637
+
599
638
  function sync(context) {
600
639
  const session = context && context.session;
601
640
  const detail = context && context.detail;
@@ -669,6 +708,14 @@
669
708
  });
670
709
  }
671
710
 
711
+ if (newFileBtn) {
712
+ newFileBtn.addEventListener('click', function () {
713
+ createFile().catch(function (e) {
714
+ onError(e && e.message ? e.message : '创建文件失败');
715
+ });
716
+ });
717
+ }
718
+
672
719
  if (saveBtn) {
673
720
  saveBtn.addEventListener('click', function () {
674
721
  saveCurrentFile().catch(function (e) {
@@ -679,6 +726,9 @@
679
726
 
680
727
  if (toggleMarkdownViewBtn) {
681
728
  toggleMarkdownViewBtn.addEventListener('click', function () {
729
+ if (state.editor && typeof state.editor.getValue === 'function' && state.selectedFile) {
730
+ state.selectedFile.content = state.editor.getValue();
731
+ }
682
732
  state.markdownViewMode = state.markdownViewMode === 'source' ? 'rendered' : 'source';
683
733
  renderPreviewPayload(state.selectedFile);
684
734
  });
package/lib/web/server.js CHANGED
@@ -3520,6 +3520,41 @@ try {
3520
3520
  `);
3521
3521
  }
3522
3522
 
3523
+ function buildContainerFileCreateCommand(requestedPath) {
3524
+ return buildWebContainerNodeCommand(`
3525
+ // __MANYOYO_FS_CREATE__
3526
+ const fs = require('fs');
3527
+ const path = require('path');
3528
+
3529
+ const requestedPath = ${JSON.stringify(String(requestedPath || ''))};
3530
+
3531
+ try {
3532
+ const resolvedPath = path.resolve(requestedPath);
3533
+ const parentPath = path.dirname(resolvedPath);
3534
+ const realParentPath = fs.realpathSync(parentPath);
3535
+ const targetPath = path.join(realParentPath, path.basename(resolvedPath));
3536
+ if (fs.existsSync(targetPath)) {
3537
+ throw new Error('文件已存在: ' + targetPath);
3538
+ }
3539
+
3540
+ fs.writeFileSync(targetPath, '', 'utf8');
3541
+ const stat = fs.statSync(targetPath);
3542
+ process.stdout.write(JSON.stringify({
3543
+ path: targetPath,
3544
+ name: path.basename(targetPath),
3545
+ kind: 'file',
3546
+ size: stat.size,
3547
+ mtimeMs: stat.mtimeMs,
3548
+ created: true
3549
+ }));
3550
+ } catch (e) {
3551
+ process.stdout.write(JSON.stringify({
3552
+ error: e && e.message ? e.message : '创建文件失败'
3553
+ }));
3554
+ }
3555
+ `);
3556
+ }
3557
+
3523
3558
  async function execAgentInWebContainerStream(ctx, state, sessionRefOrContainerName, command, options = {}) {
3524
3559
  const opts = options && typeof options === 'object' ? options : {};
3525
3560
  const sessionRef = typeof sessionRefOrContainerName === 'string'
@@ -4800,6 +4835,34 @@ async function handleWebApi(req, res, pathname, ctx, state) {
4800
4835
  sendJson(res, 200, result);
4801
4836
  }
4802
4837
  },
4838
+ {
4839
+ method: 'POST',
4840
+ match: currentPath => currentPath.match(/^\/api\/sessions\/([^/]+)\/fs\/create$/),
4841
+ handler: async match => {
4842
+ const sessionRef = getValidSessionRef(ctx, res, match[1]);
4843
+ if (!sessionRef) {
4844
+ return;
4845
+ }
4846
+ const payload = await readJsonBody(req);
4847
+ const targetPath = String(payload && payload.path ? payload.path : '').trim();
4848
+ if (!targetPath) {
4849
+ sendJson(res, 400, { error: 'path 不能为空' });
4850
+ return;
4851
+ }
4852
+
4853
+ await ensureWebContainer(ctx, state, sessionRef.containerName, sessionRef);
4854
+ const result = await execJsonCommandInWebContainer(
4855
+ ctx,
4856
+ sessionRef.containerName,
4857
+ buildContainerFileCreateCommand(targetPath)
4858
+ );
4859
+ if (result && result.error) {
4860
+ sendJson(res, 400, { error: result.error });
4861
+ return;
4862
+ }
4863
+ sendJson(res, 200, result);
4864
+ }
4865
+ },
4803
4866
  {
4804
4867
  method: 'GET',
4805
4868
  match: currentPath => currentPath.match(/^\/api\/sessions\/([^/]+)\/detail$/),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xcanwin/manyoyo",
3
- "version": "6.2.15",
3
+ "version": "6.2.17",
4
4
  "imageVersion": "1.9.1-common",
5
5
  "playwrightCliVersion": "0.1.18",
6
6
  "description": "AI Agent CLI Security Sandbox for Docker and Podman",