dsh-vscode-workbench 0.1.6 → 0.1.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.en.md +16 -2
- package/README.md +18 -2
- package/lib/client.js +45249 -3409
- package/lib/client.js.map +1 -1
- package/lib/index.js +252 -10
- package/lib/index.js.map +1 -1
- package/lib/types/client/Icon.d.ts +1 -1
- package/lib/types/client/TerminalPanel.d.ts +3 -0
- package/lib/types/client/Workbench.d.ts +6 -0
- package/lib/types/client/documentPreview.d.ts +11 -0
- package/lib/types/client/shortcuts.d.ts +4 -0
- package/lib/types/client/terminal.d.ts +43 -0
- package/lib/types/host.d.ts +10 -0
- package/lib/types/terminal.d.ts +41 -0
- package/package.json +6 -1
- package/src/client/Icon.tsx +1 -1
- package/src/client/TerminalPanel.tsx +90 -0
- package/src/client/Workbench.tsx +64 -8
- package/src/client/documentPreview.tsx +122 -0
- package/src/client/index.tsx +3 -0
- package/src/client/shortcuts.ts +17 -1
- package/src/client/styles.ts +22 -4
- package/src/client/terminal.ts +83 -0
- package/src/host.ts +8 -0
- package/src/index.ts +16 -3
- package/src/terminal.ts +145 -0
package/src/client/styles.ts
CHANGED
|
@@ -2,19 +2,20 @@ const CSS = String.raw`
|
|
|
2
2
|
@font-face{font-family:codicon;font-display:block;src:url('/dsh-vscode/codicons/codicon.ttf') format('truetype')}
|
|
3
3
|
.codicon{display:inline-block;font:normal normal normal 16px/1 codicon;text-align:center;text-decoration:none;text-rendering:auto;-webkit-font-smoothing:antialiased;user-select:none}
|
|
4
4
|
.codicon-add:before{content:"\ea60"}.codicon-git-branch:before,.codicon-source-control:before{content:"\ea68"}.codicon-search:before{content:"\ea6d"}.codicon-edit:before{content:"\ea73"}.codicon-close:before{content:"\ea76"}.codicon-close-all:before{content:"\eac1"}.codicon-cloud-upload:before{content:"\eac3"}.codicon-file:before{content:"\ea7b"}.codicon-new-file:before{content:"\ea7f"}.codicon-new-folder:before{content:"\ea80"}.codicon-folder:before{content:"\ea83"}.codicon-trash:before{content:"\ea81"}.codicon-code:before{content:"\eac4"}.codicon-check:before{content:"\eab2"}.codicon-chevron-down:before{content:"\eab4"}.codicon-chevron-right:before{content:"\eab6"}.codicon-color-mode:before{content:"\eac6"}.codicon-diff:before,.codicon-diff-sidebyside:before{content:"\eae1"}.codicon-diff-single:before{content:"\ec22"}.codicon-discard:before{content:"\eae2"}.codicon-file-code:before{content:"\eae9"}.codicon-file-media:before{content:"\eaea"}.codicon-file-pdf:before{content:"\eaeb"}.codicon-file-zip:before{content:"\eaef"}.codicon-files:before{content:"\eaf0"}.codicon-folder-opened:before{content:"\eaf7"}.codicon-git-commit:before{content:"\eafc"}.codicon-graph:before{content:"\eb03"}.codicon-home:before{content:"\eb06"}.codicon-list-tree:before{content:"\eb86"}.codicon-preview:before{content:"\eb2f"}.codicon-refresh:before{content:"\eb37"}.codicon-remove:before{content:"\eb3b"}.codicon-check-all:before{content:"\ebb1"}
|
|
5
|
-
.codicon-layout-sidebar-right:before{content:"\ebf4"}.codicon-layout-sidebar-right-off:before{content:"\ec00"}.codicon-settings:before{content:"\eb51"}.codicon-sparkle:before{content:"\ec10"}
|
|
5
|
+
.codicon-layout-sidebar-right:before{content:"\ebf4"}.codicon-layout-sidebar-right-off:before{content:"\ec00"}.codicon-settings:before{content:"\eb51"}.codicon-sparkle:before{content:"\ec10"}.codicon-split-horizontal:before{content:"\eb56"}.codicon-terminal:before{content:"\ea85"}
|
|
6
6
|
.dvw-launch{display:flex;align-items:center;gap:10px;width:100%;height:36px;padding:0 9px;border:0;border-radius:8px;background:transparent;color:var(--dsw-alias-label-secondary,#aaa);cursor:pointer;font:inherit}.dvw-launch:hover{background:var(--dsw-alias-interactive-bg-hover,#292929);color:var(--dsw-alias-label-primary,#fff)}.dvw-launcher-mount{position:fixed;z-index:20;right:16px;bottom:16px}.dvw-launcher-mount .dvw-launch{width:auto;height:32px;padding:0 12px;justify-content:center;border:1px solid var(--dsw-alias-border-l3,#e5e7eb);border-radius:6px;background:var(--dsw-alias-bg-layer-1,#fff);box-shadow:0 1px 2px rgba(0,0,0,.06);color:var(--dsw-alias-label-primary,#333)}body[data-ds-dark-theme] .dvw-launcher-mount .dvw-launch{color:var(--dsw-alias-label-primary,#ddd)}@media(max-width:640px){.dvw-launcher-mount{right:8px;bottom:8px}.dvw-launcher-mount .dvw-launch{height:30px;padding:0 10px;font-size:12px}}
|
|
7
7
|
.dvw-overlay{--dvw-side-width:210px;--dvw-base:var(--dsw-alias-bg-base,#181818);--dvw-surface-1:var(--dsw-alias-bg-layer-1,#1e1e1e);--dvw-surface-2:var(--dsw-alias-bg-layer-2,#252526);--dvw-surface-3:var(--dsw-alias-bg-layer-3,#2d2d30);--dvw-text:var(--dsw-alias-label-primary,#ccc);--dvw-text-2:var(--dsw-alias-label-secondary,#aaa);--dvw-text-3:var(--dsw-alias-label-tertiary,#858585);--dvw-border:var(--dsw-alias-border-l2,#333);--dvw-border-soft:var(--dsw-alias-border-l3,#292929);--dvw-hover:var(--dsw-alias-interactive-bg-hover,rgba(255,255,255,.08));--dvw-active:var(--dsw-alias-interactive-bg-active,rgba(255,255,255,.14));--dvw-accent:var(--dsw-alias-state-business-primary,#3b82f6);--dvw-error:var(--dsw-alias-state-error-primary,#f48771);--dvw-success:var(--dsw-alias-state-success-primary,#73c991);--dvw-warn:var(--dsw-alias-state-warn-primary,#d7ba7d);position:absolute;inset:0;z-index:100;display:grid;grid-template-columns:48px var(--dvw-side-width) minmax(0,1fr);grid-template-rows:minmax(0,1fr) auto 24px;background:var(--dvw-base);color:var(--dvw-text);pointer-events:auto;font:13px/1.4 var(--dsw-font-family,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif);overflow:hidden;color-scheme:light}
|
|
8
8
|
body[data-ds-dark-theme] .dvw-overlay,.dvw-overlay[data-dvw-theme=dark]{color-scheme:dark}
|
|
9
9
|
.dvw-overlay[data-dvw-theme=dark]{--dvw-base:#151517;--dvw-surface-1:#232324;--dvw-surface-2:#2c2c2e;--dvw-surface-3:#353638;--dvw-text:#f9fafb;--dvw-text-2:#cfd3d6;--dvw-text-3:#adb2b8;--dvw-border:#ffffff1f;--dvw-border-soft:#ffffff29;--dvw-hover:#ffffff14;--dvw-active:#ffffff24;--dvw-accent:#679efe;--dvw-error:#f25a5a;--dvw-success:#22c55e;--dvw-warn:#f59e0b}
|
|
10
10
|
.dvw-overlay[data-dvw-theme=light]{--dvw-base:#fff;--dvw-surface-1:#fff;--dvw-surface-2:#fff;--dvw-surface-3:#fff;--dvw-text:#0f1115;--dvw-text-2:#61666b;--dvw-text-3:#81858c;--dvw-border:#0000001a;--dvw-border-soft:#0000001f;--dvw-hover:#2631480f;--dvw-active:#2631481a;--dvw-accent:#4176e6;--dvw-error:#ec1313;--dvw-success:#22c55e;--dvw-warn:#f59e0b;color-scheme:light}
|
|
11
|
-
.dvw-overlay *{box-sizing:border-box}.dvw-activity{grid-row:1/3;background:var(--dvw-base);border-right:1px solid var(--dvw-border-soft);display:flex;flex-direction:column;align-items:center}.dvw-act{width:48px;height:48px;border:0;border-left:2px solid transparent;background:transparent;color:var(--dvw-text-3);font-size:22px;cursor:pointer}.dvw-act:hover,.dvw-act[data-active=true]{color:var(--dvw-text)}.dvw-act[data-active=true]{border-left-color:var(--dvw-accent)}.dvw-act.dsh{margin-top:auto}.dvw-side{grid-column:2;grid-row:1/3;min-width:0;width:100%;background:var(--dvw-surface-1);border-right:1px solid var(--dvw-border);display:flex;flex-direction:column}.dvw-side-resizer{grid-column:2;grid-row:1/3;justify-self:end;width:
|
|
11
|
+
.dvw-overlay *{box-sizing:border-box}.dvw-activity{grid-row:1/3;background:var(--dvw-base);border-right:1px solid var(--dvw-border-soft);display:flex;flex-direction:column;align-items:center}.dvw-act{width:48px;height:48px;border:0;border-left:2px solid transparent;background:transparent;color:var(--dvw-text-3);font-size:22px;cursor:pointer}.dvw-act:hover,.dvw-act[data-active=true]{color:var(--dvw-text)}.dvw-act[data-active=true]{border-left-color:var(--dvw-accent)}.dvw-act.dsh{margin-top:auto}.dvw-side{grid-column:2;grid-row:1/3;min-width:0;width:100%;background:var(--dvw-surface-1);border-right:1px solid var(--dvw-border);display:flex;flex-direction:column}.dvw-side-resizer{grid-column:2;grid-row:1/3;justify-self:end;width:4px;height:100%;z-index:4;cursor:ew-resize;transform:translateX(2px);touch-action:none}.dvw-side-resizer:hover,.dvw-side-resizer:focus-visible,.dvw-side-resizer[data-dragging=true]{background:var(--dvw-accent)}.dvw-side-head{height:36px;display:flex;align-items:center;padding:0 12px;text-transform:uppercase;font-size:11px;letter-spacing:.4px}.dvw-toolbar{margin-left:auto;display:flex;gap:2px}.dvw-icon{border:0;background:transparent;color:var(--dvw-text-2);border-radius:4px;min-width:24px;height:24px;display:inline-grid;place-items:center;cursor:pointer}.dvw-icon:hover{background:var(--dvw-hover);color:var(--dvw-text)}.dvw-icon:disabled{opacity:.45;cursor:default}.dvw-file-picker{display:none}.dvw-tree,.dvw-results,.dvw-git{overflow:auto;flex:1}.dvw-tree[data-drop-target=true]{outline:1px dashed var(--dvw-accent);outline-offset:-3px}.dvw-tree-row{display:flex;align-items:center;height:24px;padding-right:6px;cursor:pointer;white-space:nowrap}.dvw-tree-row:hover{background:var(--dvw-hover)}.dvw-tree-row[data-selected=true],.dvw-tree-row[data-drop-target=true]{background:var(--dvw-active)}.dvw-chevron{width:16px;flex:none;text-align:center;color:var(--dvw-text-3)}.dvw-file-icon{width:18px;flex:none;color:var(--dvw-text-2)}.dvw-file-icon.folder{color:var(--dvw-accent)}.dvw-tree-name{overflow:hidden;text-overflow:ellipsis}.dvw-tree-rename,.dvw-tree-create-input{min-width:0;flex:1;height:20px;border:1px solid var(--dvw-accent);border-radius:0;background:var(--dvw-surface-1);color:var(--dvw-text);padding:0 3px;font:inherit;letter-spacing:0;outline:0}.dvw-tree-create-row{cursor:text}.dvw-tree-create-row:hover{background:transparent}.dvw-tree-create-input::placeholder{color:var(--dvw-text-3)}.dvw-tree-create-input[aria-invalid=true]{border-color:var(--dvw-error)}.dvw-tree-create-error{min-height:20px;padding-top:2px;padding-right:6px;padding-bottom:3px;color:var(--dvw-error);font-size:11px;line-height:15px;white-space:normal;overflow-wrap:anywhere}.dvw-tree-status{margin-left:auto;padding-left:6px;font-size:11px;font-weight:600;color:var(--dvw-warn)}.dvw-tree-status[data-status=M]{color:var(--dvw-warn)}.dvw-tree-status[data-status=A]{color:var(--dvw-success)}.dvw-tree-status[data-status=D]{color:var(--dvw-error)}.dvw-context-menu{position:fixed;z-index:40;min-width:148px;padding:4px;border:1px solid var(--dvw-border);border-radius:5px;background:var(--dvw-surface-3);box-shadow:0 6px 20px rgba(0,0,0,.3)}.dvw-context-menu button{width:100%;height:28px;border:0;border-radius:3px;background:transparent;color:var(--dvw-text);display:flex;align-items:center;gap:8px;padding:0 8px;text-align:left;font:inherit;cursor:pointer}.dvw-context-menu button:hover{background:var(--dvw-hover)}.dvw-context-menu .dvw-context-delete{color:var(--dvw-error)}
|
|
12
12
|
.dvw-main{grid-column:3;grid-row:1;display:flex;min-width:0;min-height:0;flex-direction:column;background:var(--dvw-surface-1)}.dvw-tabs{height:36px;background:var(--dvw-base);display:flex;overflow-x:auto}.dvw-tab{height:36px;min-width:110px;max-width:260px;border:0;border-right:1px solid var(--dvw-border-soft);background:var(--dvw-base);color:var(--dvw-text-2);padding:0 8px 0 12px;display:flex;align-items:center;gap:8px;cursor:pointer}.dvw-tab[data-active=true]{background:var(--dvw-surface-1);color:var(--dvw-text);border-top:1px solid var(--dvw-accent)}.dvw-tab-name{overflow:hidden;text-overflow:ellipsis;flex:1;text-align:left}.dvw-editor{position:relative;flex:1;min-height:0}.dvw-editor-toolbar{position:absolute;z-index:3;top:0;right:0;left:0;height:36px;display:flex;align-items:center;justify-content:flex-end;gap:4px;padding:0 10px;border-bottom:1px solid var(--dvw-border-soft);background:var(--dvw-surface-1)}.dvw-editor-body{position:absolute;inset:0;min-height:0}.dvw-editor-body[data-toolbar=true]{top:36px}.dvw-mode-button{height:27px;padding:0 10px;display:flex;align-items:center;gap:6px;border:1px solid transparent;border-radius:5px;background:transparent;color:var(--dvw-text-2);cursor:pointer}.dvw-mode-button:hover{background:var(--dvw-hover)}.dvw-mode-button[data-active=true]{border-color:var(--dvw-border);background:var(--dvw-active);color:var(--dvw-text)}.dvw-empty{height:100%;display:grid;place-items:center;color:var(--dvw-text-3);text-align:center}.dvw-empty strong{font-size:24px;color:var(--dvw-text-2)}.dvw-message{padding:12px;color:var(--dvw-warn)}
|
|
13
13
|
.dvw-bottom{grid-column:3;grid-row:2;height:260px;min-height:120px;border-top:1px solid var(--dvw-border);background:var(--dvw-base);display:flex;flex-direction:column}.dvw-bottom-head{height:35px;display:flex;align-items:center;gap:14px;padding:0 12px}.dvw-bottom-title{border:0;border-bottom:1px solid var(--dvw-text);background:transparent;color:var(--dvw-text);height:35px}.dvw-bottom-body{flex:1;min-height:0}.dvw-status{grid-column:1/4;grid-row:3;background:var(--dvw-accent);color:#fff;display:flex;align-items:center;padding:0 8px;gap:18px;font-size:12px}.dvw-theme-control{position:relative;margin-left:auto}.dvw-theme-trigger{height:22px;min-width:112px;display:flex;align-items:center;gap:7px;padding:0 8px;border:0;border-radius:5px;background:color-mix(in srgb,#000 17%,transparent);color:#fff;cursor:pointer}.dvw-theme-trigger:hover{background:color-mix(in srgb,#000 27%,transparent)}.dvw-theme-orb{width:13px;height:13px;border:2px solid #fff;border-radius:50%;background:linear-gradient(90deg,#17181c 50%,#fff 50%)}.dvw-theme-orb.dark{background:#17181c}.dvw-theme-orb.light{background:#fff}.dvw-theme-menu{position:absolute;right:0;bottom:29px;width:224px;padding:6px;border:1px solid var(--dvw-border);border-radius:10px;background:var(--dvw-surface-3);box-shadow:0 10px 35px rgba(0,0,0,.28);color:var(--dvw-text)}.dvw-theme-menu button{width:100%;min-height:48px;border:0;border-radius:7px;background:transparent;color:inherit;display:grid;grid-template-columns:30px 1fr 20px;align-items:center;text-align:left;padding:6px;cursor:pointer}.dvw-theme-menu button:hover,.dvw-theme-menu button[aria-checked=true]{background:var(--dvw-hover)}.dvw-theme-menu strong,.dvw-theme-menu small{display:block}.dvw-theme-menu small{color:var(--dvw-text-3);margin-top:2px}.dvw-theme-swatch{width:22px;height:22px;border-radius:6px;border:1px solid var(--dvw-border);background:linear-gradient(135deg,#17181c 0 50%,#fff 50%)}.dvw-theme-swatch.dark{background:#17181c}.dvw-theme-swatch.light{background:#fff}
|
|
14
14
|
.dvw-search-form{padding:8px;display:grid;gap:6px}.dvw-input,.dvw-textarea,.dvw-select{width:100%;border:1px solid var(--dvw-border);background:var(--dvw-surface-2);color:var(--dvw-text);padding:5px 7px;outline:none}.dvw-input:focus,.dvw-textarea:focus,.dvw-select:focus{border-color:var(--dvw-accent)}.dvw-options{display:flex;align-items:center;gap:8px;font-size:11px}.dvw-result-file{padding:5px 8px;font-weight:600}.dvw-result{padding:4px 8px 4px 20px;cursor:pointer;display:flex;gap:8px}.dvw-result:hover{background:var(--dvw-hover)}.dvw-line{color:var(--dvw-text-3);min-width:30px;text-align:right}.dvw-preview{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
15
15
|
.dvw-git-head{padding:7px 10px;border-bottom:1px solid var(--dvw-border);display:flex;align-items:center;gap:8px}.dvw-branch{min-width:0;border:0;background:transparent;color:var(--dvw-text);font:inherit;font-weight:600;display:flex;align-items:center;gap:5px;cursor:pointer}.dvw-commit{display:grid;grid-template-columns:1fr auto;gap:6px;padding:9px}.dvw-commit .dvw-input{grid-column:1/3}.dvw-commit-button{grid-column:1/3;height:31px;border:0;border-radius:6px;background:color-mix(in srgb,var(--dvw-accent) 24%,var(--dvw-surface-2));color:var(--dvw-text);display:flex;align-items:center;justify-content:center;gap:7px;cursor:pointer}.dvw-commit-button:hover{background:color-mix(in srgb,var(--dvw-accent) 34%,var(--dvw-surface-2))}.dvw-commit-button:disabled{opacity:.45;cursor:default}.dvw-group-head{height:30px;display:flex;align-items:center;gap:5px;padding:0 6px 0 9px;border-radius:5px;font-weight:600;position:sticky;top:0;background:var(--dvw-surface-1);z-index:1}.dvw-group-head:hover{background:var(--dvw-hover)}.dvw-group-head .dvw-count{margin-left:auto;min-width:20px;height:20px;border-radius:10px;display:grid;place-items:center;background:var(--dvw-surface-3);color:var(--dvw-text-2);font-size:11px}.dvw-change{display:flex;align-items:center;min-height:28px;padding:3px 5px 3px 19px;gap:5px;cursor:pointer}.dvw-change:hover{background:var(--dvw-hover)}.dvw-change .dvw-icon{opacity:0}.dvw-change:hover .dvw-icon,.dvw-change:focus-within .dvw-icon{opacity:1}.dvw-change-path{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;flex:1}.dvw-badge{font-size:11px;color:var(--dvw-warn)}.dvw-graph{border-top:1px solid var(--dvw-border-soft);margin-top:8px;padding-top:3px}.dvw-graph-list{padding-bottom:8px}.dvw-graph-row{width:100%;height:38px;border:0;background:transparent;color:var(--dvw-text);display:flex;align-items:center;padding:0 7px;text-align:left;cursor:pointer;overflow:hidden}.dvw-graph-row:hover{background:var(--dvw-hover)}.dvw-graph-svg{flex:none;align-self:stretch}.dvw-graph-copy{display:flex;min-width:0;flex:1;flex-direction:column;justify-content:center}.dvw-graph-subject{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.dvw-graph-meta{font-size:11px;color:var(--dvw-text-3);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.dvw-ref{max-width:82px;margin-left:4px;padding:1px 5px;border:1px solid color-mix(in srgb,var(--dvw-accent) 50%,transparent);border-radius:9px;color:var(--dvw-accent);font-size:10px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.dvw-error{padding:8px;color:var(--dvw-error);white-space:pre-wrap}.dvw-diff{margin:0;height:100%;overflow:auto;padding:8px 0;background:var(--dvw-surface-1);font:12px/1.5 ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;white-space:pre}.dvw-diff-line{display:block;min-height:18px;padding:0 14px}.dvw-diff-line.add{background:color-mix(in srgb,var(--dvw-success) 15%,transparent);color:var(--dvw-success)}.dvw-diff-line.remove{background:color-mix(in srgb,var(--dvw-error) 13%,transparent);color:var(--dvw-error)}.dvw-diff-line.hunk{background:color-mix(in srgb,var(--dvw-accent) 12%,transparent);color:var(--dvw-accent)}.dvw-diff-line.meta{color:var(--dvw-text-3)}.dvw-diff-empty{padding:24px;color:var(--dvw-text-3)}
|
|
16
16
|
.dvw-tabs{overflow:hidden}.dvw-tab-list{display:flex;min-width:0;flex:1;overflow-x:auto}.dvw-tab-actions{flex:none;width:42px;display:grid;place-items:center;padding:0 6px;background:var(--dvw-base)}.dvw-close-all{border:1px solid var(--dvw-border);background:var(--dvw-base)}.dvw-monaco-diff{position:absolute;inset:0}.dvw-diff-toolbar{justify-content:flex-end}.dvw-group-head{gap:2px;padding-left:2px}.dvw-group-toggle{min-width:0;height:30px;flex:1;border:0;border-radius:5px;background:transparent;color:var(--dvw-text);font:inherit;font-weight:600;display:flex;align-items:center;gap:5px;padding:0 7px;cursor:pointer;text-align:left}.dvw-group-toggle:hover{background:var(--dvw-hover)}.dvw-graph-entry[data-expanded=true]{background:color-mix(in srgb,var(--dvw-accent) 5%,transparent)}.dvw-commit-files{padding:1px 0 5px 26px}.dvw-commit-file{width:100%;height:27px;border:0;background:transparent;color:var(--dvw-text);display:flex;align-items:center;gap:6px;padding:0 7px;text-align:left;cursor:pointer}.dvw-commit-file:hover{background:var(--dvw-hover)}.dvw-commit-file-name{white-space:nowrap}.dvw-commit-file-dir{min-width:0;flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--dvw-text-3);font-size:11px}
|
|
17
17
|
.dvw-outline{flex:none;max-height:38%;display:flex;flex-direction:column;border-bottom:1px solid var(--dvw-border)}.dvw-outline-head{height:30px;display:flex;align-items:center;justify-content:space-between;padding:0 5px 0 10px;font-weight:600}.dvw-outline-head>span{display:flex;align-items:center;gap:6px}.dvw-outline-list{overflow:auto;padding-bottom:5px}.dvw-outline-item{display:block;width:100%;height:25px;border:0;background:transparent;color:var(--dvw-text-2);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;text-align:left;cursor:pointer}.dvw-outline-item:hover{background:var(--dvw-hover);color:var(--dvw-text)}.dvw-image-preview{position:absolute;inset:0;overflow:auto;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:14px;padding:24px;background:repeating-conic-gradient(var(--dvw-surface-2) 0 25%,var(--dvw-surface-1) 0 50%) 0/20px 20px}.dvw-image-preview img{max-width:100%;max-height:calc(100% - 32px);object-fit:contain;box-shadow:0 4px 24px rgba(0,0,0,.22)}.dvw-image-preview span{max-width:100%;padding:3px 8px;border-radius:5px;background:var(--dvw-surface-1);color:var(--dvw-text-2);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
18
|
+
.dvw-pdf-preview{position:absolute;inset:0;display:flex;min-height:0;flex-direction:column;background:var(--dvw-base);color:var(--dvw-text)}.dvw-pdf-toolbar{height:30px;flex:none;display:flex;align-items:center;justify-content:center;gap:2px;padding:0 8px;border-bottom:1px solid var(--dvw-border);background:var(--dvw-surface-1);font-variant-numeric:tabular-nums}.dvw-pdf-toolbar .dvw-icon{font-size:18px}.dvw-pdf-zoom{min-width:52px;height:24px;border:0;border-radius:4px;background:var(--dvw-active);color:var(--dvw-text);font:inherit;cursor:pointer}.dvw-pdf-zoom:hover{background:var(--dvw-hover)}.dvw-pdf-separator{width:1px;height:18px;margin:0 4px;background:var(--dvw-border)}.dvw-pdf-page{min-height:0;flex:1;overflow:auto;display:flex;justify-content:center;padding:8px;background:var(--dvw-base)}.dvw-pdf-page canvas{flex:none;background:#fff;box-shadow:0 2px 12px rgba(0,0,0,.28)}.dvw-docx-preview{position:absolute;inset:0;overflow:auto;padding:28px;background:var(--dvw-base);color:#222}.dvw-docx-preview .docx-wrapper{margin:auto;background:#fff;box-shadow:0 2px 12px rgba(0,0,0,.2)}.dvw-document-error,.dvw-document-download{height:100%;display:grid;place-content:center;justify-items:center;gap:12px;padding:24px;color:var(--dvw-text-2);text-align:center}.dvw-document-download a{color:var(--dvw-accent);text-decoration:none}.dvw-document-download a:hover{text-decoration:underline}
|
|
18
19
|
.dvw-markdown{height:100%;overflow:auto;padding:28px clamp(24px,6vw,80px) 60px;color:var(--dvw-text);font-size:15px;line-height:1.65}.dvw-markdown>*:first-child{margin-top:0}.dvw-markdown h1,.dvw-markdown h2{padding-bottom:.3em;border-bottom:1px solid var(--dvw-border)}.dvw-markdown h1{font-size:2em}.dvw-markdown h2{font-size:1.5em}.dvw-markdown h3{font-size:1.25em}.dvw-markdown a{color:var(--dvw-accent)}.dvw-markdown code{font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;background:var(--dvw-surface-2);border-radius:4px;padding:.15em .35em}.dvw-markdown pre{overflow:auto;padding:14px;border:1px solid var(--dvw-border-soft);border-radius:7px;background:var(--dvw-base)}.dvw-markdown pre code{padding:0;background:transparent}.dvw-markdown blockquote{margin-left:0;padding-left:14px;border-left:3px solid var(--dvw-border);color:var(--dvw-text-2)}.dvw-markdown table{border-collapse:collapse;display:block;overflow:auto}.dvw-markdown th,.dvw-markdown td{padding:6px 12px;border:1px solid var(--dvw-border)}.dvw-markdown tr:nth-child(2n){background:var(--dvw-hover)}.dvw-markdown img{max-width:100%}.dvw-markdown hr{border:0;border-top:1px solid var(--dvw-border)}
|
|
19
20
|
.dvw-sidebar-launcher-mount{border-bottom:1px solid var(--dsw-alias-border-l3,#e5e7eb)}.dvw-outline-toggle{margin-right:auto}.dvw-outline-toggle[data-active=true]{background:var(--dvw-active);color:var(--dvw-text)}.dvw-editor-outline{position:absolute;z-index:2;top:36px;bottom:0;left:0;width:240px;max-height:none;background:var(--dvw-surface-1);border-right:1px solid var(--dvw-border);border-bottom:0}.dvw-editor[data-outline=true] .dvw-editor-body{left:240px}.dvw-diff-preview-switch{margin-right:auto;display:flex;gap:4px}.dvw-commit-files{position:relative;padding-top:1px;padding-right:0;padding-bottom:5px}.dvw-graph-continuation{position:absolute;top:0;bottom:0;left:7px;height:100%;pointer-events:none}.dvw-commit-file-list{position:relative;min-width:0}.dvw-overlay button:focus-visible,.dvw-overlay input:focus-visible,.dvw-overlay select:focus-visible{outline:1px solid var(--dvw-accent);outline-offset:-1px}@media(max-width:800px){.dvw-overlay{grid-template-columns:44px var(--dvw-side-width) minmax(0,1fr)}.dvw-editor-outline{width:210px}.dvw-editor[data-outline=true] .dvw-editor-body{left:210px}}
|
|
20
21
|
.dvw-overlay[data-dvw-chat=true]{grid-template-columns:48px var(--dvw-side-width) minmax(0,1fr) var(--dvw-chat-width)}
|
|
@@ -27,13 +28,30 @@ body[data-ds-dark-theme] .dvw-overlay,.dvw-overlay[data-dvw-theme=dark]{color-sc
|
|
|
27
28
|
.dvw-chat-head{height:36px;display:flex;align-items:center;justify-content:space-between;padding:0 6px 0 12px;border-bottom:1px solid var(--dvw-border);background:var(--dvw-base);font-weight:600}
|
|
28
29
|
.dvw-chat-head>span{display:flex;align-items:center;gap:7px}.dvw-chat-body{height:calc(100% - 36px);background:var(--dvw-base)}
|
|
29
30
|
.dvw-dsh-mark{display:block}
|
|
30
|
-
.dvw-chat-resizer{position:fixed;top:
|
|
31
|
-
.dvw-chat-resizer:hover,.dvw-chat-resizer:
|
|
31
|
+
.dvw-chat-resizer{position:fixed;top:36px;right:calc(var(--dvw-chat-width) - 2px);bottom:0;width:4px;z-index:4;cursor:ew-resize;touch-action:none}
|
|
32
|
+
.dvw-chat-resizer:hover,.dvw-chat-resizer:focus-visible,.dvw-chat-resizer[data-dragging=true]{background:var(--dvw-accent)}
|
|
33
|
+
.dvw-side-resizer,.dvw-chat-resizer{width:10px;cursor:ew-resize;touch-action:none;background:transparent}
|
|
34
|
+
.dvw-side-resizer{position:relative;transform:translateX(5px);z-index:22}
|
|
35
|
+
.dvw-chat-resizer{position:fixed;top:36px;right:calc(var(--dvw-chat-width) - 5px);z-index:22}
|
|
36
|
+
.dvw-side-resizer::after,.dvw-chat-resizer::after{content:"";position:absolute;top:0;bottom:0;left:4px;width:2px;background:var(--dvw-border)}
|
|
37
|
+
.dvw-chat-resizer::after{left:1px;width:6px}
|
|
38
|
+
.dvw-side-resizer:hover,.dvw-side-resizer:focus-visible,.dvw-side-resizer[data-dragging=true],.dvw-chat-resizer:hover,.dvw-chat-resizer:focus-visible,.dvw-chat-resizer[data-dragging=true]{background:transparent}
|
|
39
|
+
.dvw-side-resizer:hover::after,.dvw-side-resizer:focus-visible::after,.dvw-side-resizer[data-dragging=true]::after,.dvw-chat-resizer:hover::after,.dvw-chat-resizer:focus-visible::after,.dvw-chat-resizer[data-dragging=true]::after{background:var(--dvw-accent)}
|
|
40
|
+
.dvw-side{border-right-color:transparent}.dvw-chat,[data-dvw-conversation-panel]{border-left-color:transparent!important}
|
|
41
|
+
.dvw-side-resizer::after,.dvw-chat-resizer::after{background:transparent}.dvw-side-resizer::after{left:3px;width:3px}
|
|
42
|
+
.dvw-side-resizer:hover::after,.dvw-side-resizer:focus-visible::after,.dvw-side-resizer[data-dragging=true]::after,.dvw-chat-resizer:hover::after,.dvw-chat-resizer:focus-visible::after,.dvw-chat-resizer[data-dragging=true]::after{background:var(--dvw-accent)}
|
|
32
43
|
.dvw-chat-toggle{position:absolute;z-index:5;top:6px;right:6px;border:1px solid var(--dvw-border);background:var(--dvw-base)}
|
|
33
44
|
.dvw-overlay:not([data-dvw-chat=true]) .dvw-tabs{padding-right:36px}
|
|
34
45
|
[data-dvw-conversation-panel]{position:fixed!important;top:36px!important;right:0!important;bottom:var(--dvw-status-height,24px)!important;left:auto!important;width:var(--dvw-chat-width)!important;height:auto!important;min-width:0!important;z-index:21!important;border-left:1px solid var(--dsw-alias-border-l2)!important;box-shadow:none!important}
|
|
35
46
|
.dvw-confirm-backdrop{position:fixed;z-index:50;inset:0;display:grid;place-items:center;padding:24px;background:rgba(0,0,0,.38)}.dvw-confirm{width:min(380px,calc(100vw - 48px));border:1px solid var(--dvw-border);border-radius:7px;background:var(--dvw-surface-1);box-shadow:0 12px 36px rgba(0,0,0,.34);padding:18px}.dvw-confirm h2{margin:0 0 12px;font-size:15px}.dvw-confirm p{margin:0;color:var(--dvw-text)}.dvw-confirm-note{margin-top:8px!important;color:var(--dvw-text-3)!important;font-size:12px}.dvw-confirm footer{display:flex;justify-content:flex-end;gap:8px;margin-top:20px}.dvw-confirm footer button{height:28px;border:1px solid var(--dvw-border);border-radius:4px;background:var(--dvw-surface-2);color:var(--dvw-text);padding:0 12px;font:inherit;cursor:pointer}.dvw-confirm footer button:hover{background:var(--dvw-hover)}.dvw-confirm footer .dvw-danger-button{border-color:color-mix(in srgb,var(--dvw-error) 65%,var(--dvw-border));background:var(--dvw-error);color:#fff}.dvw-confirm footer .dvw-danger-button:hover{background:color-mix(in srgb,var(--dvw-error) 82%,#000)}
|
|
36
47
|
.dvw-tree[data-dvw-native-drop-target=true]{outline:1px dashed var(--dvw-accent);outline-offset:-3px}.dvw-tree-row[data-dvw-native-drop-target=true]{background:var(--dvw-active)}
|
|
48
|
+
.dvw-act:disabled{opacity:.4;cursor:not-allowed}
|
|
49
|
+
.dvw-terminal-root{height:100%;display:flex;flex-direction:column;background:var(--dvw-base);color:var(--dvw-text)}.dvw-terminal-tabs{height:28px;display:flex;align-items:center;padding:0 8px;gap:3px;border-bottom:1px solid var(--dvw-border-soft)}.dvw-terminal-tab{height:24px;max-width:180px;border:0;background:transparent;color:var(--dvw-text-2);padding:0 9px;cursor:pointer;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.dvw-terminal-tab[data-active=true]{color:var(--dvw-text);background:var(--dvw-active)}.dvw-terminal-actions{margin-left:auto;display:flex}.dvw-terminal-button{width:28px;height:24px;border:0;background:transparent;color:var(--dvw-text-2);cursor:pointer;font:18px/1 inherit}.dvw-terminal-button:hover{background:var(--dvw-hover);color:var(--dvw-text)}.dvw-terminal-button:disabled{opacity:.4;cursor:default}.dvw-terminal-host{position:relative;flex:1;min-height:0;padding:5px 8px;background:var(--dvw-base)}.dvw-terminal-host .xterm{height:100%;position:relative;user-select:none}.dvw-terminal-host .xterm.focus,.dvw-terminal-host .xterm:focus{outline:none}.dvw-terminal-host .xterm-viewport{position:absolute;inset:0;overflow-y:scroll;background-color:transparent}.dvw-terminal-host .xterm-screen{position:relative}.dvw-terminal-host .xterm-helpers{position:absolute;top:0;z-index:5}.dvw-terminal-host .xterm-helper-textarea{position:absolute;opacity:0;left:-9999em;top:0;width:0;height:0;z-index:-5;white-space:nowrap;overflow:hidden;resize:none}.dvw-terminal-host .composition-view{background:var(--dvw-base);color:var(--dvw-text);display:none;position:absolute;white-space:nowrap;z-index:1}.dvw-terminal-host .composition-view.active{display:block}.dvw-terminal-host .xterm-char-measure-element{display:inline-block;visibility:hidden;position:absolute;left:-9999em;line-height:normal}.dvw-terminal-host .xterm-cursor-pointer,.dvw-terminal-host .xterm .xterm-cursor-pointer{cursor:pointer}.dvw-terminal-host .xterm-accessibility,.dvw-terminal-host .xterm-message{position:absolute;inset:0;z-index:10;color:transparent;pointer-events:none}.dvw-terminal-host .xterm-accessibility-tree{position:absolute;left:0;top:0;width:100%}.dvw-terminal-host .xterm-accessibility-tree>div{position:absolute;transform-origin:top left}.dvw-terminal-host canvas{position:absolute;left:0;top:0}.dvw-terminal-error{padding:8px;color:var(--dvw-error)}
|
|
50
|
+
.dvw-terminal-tabs{height:34px;padding:0 10px;gap:2px;background:var(--dvw-surface-1)}.dvw-terminal-tab{height:28px;display:flex;align-items:center;gap:6px;border-radius:3px;padding:0 10px;font:inherit}.dvw-terminal-tab .codicon{font-size:14px}.dvw-terminal-tab[data-active=true]{box-shadow:inset 0 1px 0 var(--dvw-accent)}.dvw-terminal-button{height:28px;font:inherit;display:grid;place-items:center;border-radius:3px}.dvw-terminal-host{padding:8px 10px}
|
|
51
|
+
.dvw-terminal-root{flex-direction:row}.dvw-terminal-main{flex:1;min-width:0;min-height:0;display:flex;flex-direction:column}.dvw-terminal-main>.dvw-terminal-host{flex:1;min-height:0}.dvw-terminal-tabs{width:220px;height:auto;flex:none;display:flex;flex-direction:column;align-items:stretch;padding:0;border-left:1px solid var(--dvw-border);border-bottom:0;background:var(--dvw-surface-1)}.dvw-terminal-actions{height:35px;flex:none;margin-left:0;justify-content:flex-end;align-items:center;padding:0 6px;border-bottom:1px solid var(--dvw-border-soft)}.dvw-terminal-list{min-height:0;overflow:auto;padding:4px}.dvw-terminal-tab{width:100%;max-width:none;justify-content:flex-start;height:30px;border-radius:3px;color:var(--dvw-text-2)}.dvw-terminal-tab[data-active=true]{background:var(--dvw-active);color:var(--dvw-text);box-shadow:inset 2px 0 0 var(--dvw-accent)}
|
|
52
|
+
.dvw-terminal-rename{width:100%;min-width:0;height:22px;border:1px solid var(--dvw-accent);border-radius:2px;background:var(--dvw-base);color:var(--dvw-text);padding:0 4px;font:inherit;outline:0}.dvw-terminal-menu{position:fixed;z-index:60;min-width:140px;padding:4px;border:1px solid var(--dvw-border);border-radius:5px;background:var(--dvw-surface-3);box-shadow:0 6px 20px rgba(0,0,0,.3)}.dvw-terminal-menu button{display:block;width:100%;height:28px;border:0;border-radius:3px;background:transparent;color:var(--dvw-text);padding:0 10px;text-align:left;font:inherit;cursor:pointer}.dvw-terminal-menu button:hover{background:var(--dvw-hover)}.dvw-terminal-menu .dvw-terminal-menu-danger{color:var(--dvw-error)}
|
|
53
|
+
.dvw-terminal-panes{flex:1;min-height:0;display:flex}.dvw-terminal-panes>.dvw-terminal-host{flex:1;min-width:0}.dvw-terminal-panes[data-split=true]>.dvw-terminal-host+.dvw-terminal-host{border-left:1px solid var(--dvw-border)}.dvw-terminal-resizer{width:4px;flex:none;cursor:ew-resize;position:relative;z-index:1}.dvw-terminal-resizer:hover{background:var(--dvw-accent)}.dvw-terminal-entry{position:relative;display:flex;align-items:center;min-width:0}.dvw-terminal-entry .dvw-terminal-tab{padding-right:58px}.dvw-terminal-entry-actions{position:absolute;right:4px;top:1px;height:28px;display:none;align-items:center;background:var(--dvw-active)}.dvw-terminal-entry:hover .dvw-terminal-entry-actions,.dvw-terminal-entry:focus-within .dvw-terminal-entry-actions{display:flex}.dvw-terminal-entry-actions button{width:25px;height:28px;display:grid;place-items:center;border:0;border-radius:3px;background:transparent;color:var(--dvw-text-2);cursor:pointer}.dvw-terminal-entry-actions button:hover{background:var(--dvw-hover);color:var(--dvw-text)}
|
|
54
|
+
.dvw-terminal-list,.dvw-terminal-host .xterm-viewport{scrollbar-width:none}.dvw-terminal-list::-webkit-scrollbar,.dvw-terminal-host .xterm-viewport::-webkit-scrollbar{display:none}
|
|
37
55
|
.dvw-settings-backdrop{position:absolute;inset:0;z-index:30;display:grid;place-items:center;padding:24px;background:rgba(0,0,0,.34);backdrop-filter:blur(2px)}
|
|
38
56
|
.dvw-settings{width:min(720px,calc(100vw - 48px));max-height:min(720px,calc(100vh - 48px));display:flex;flex-direction:column;border:1px solid var(--dvw-border);border-radius:10px;background:var(--dvw-surface-1);box-shadow:0 16px 50px rgba(0,0,0,.32);overflow:hidden}
|
|
39
57
|
.dvw-settings>header{height:44px;flex:none;display:flex;align-items:center;justify-content:space-between;padding:0 10px 0 14px;border-bottom:1px solid var(--dvw-border);font-size:14px;font-weight:600}.dvw-settings>header>span{display:flex;align-items:center;gap:8px}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import type { TerminalInfo } from '../terminal.ts'
|
|
2
|
+
|
|
3
|
+
export interface TerminalConnection {
|
|
4
|
+
write(data: string): void
|
|
5
|
+
resize(cols: number, rows: number): void
|
|
6
|
+
close(): void
|
|
7
|
+
subscribe(listener: (output: { sequence: number; data: string; replay?: boolean }) => void): () => void
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface TerminalWorkspaceSnapshot { terminals: readonly TerminalInfo[]; loading: boolean; error?: string }
|
|
11
|
+
|
|
12
|
+
/** Browser client for the workbench's local terminal transport. */
|
|
13
|
+
export interface TerminalWorkspace {
|
|
14
|
+
refresh(): Promise<void>
|
|
15
|
+
create(title?: string): Promise<TerminalInfo>
|
|
16
|
+
close(id: string): Promise<void>
|
|
17
|
+
rename(id: string, title: string): Promise<void>
|
|
18
|
+
connect(id: string): TerminalConnection
|
|
19
|
+
getSnapshot(): TerminalWorkspaceSnapshot
|
|
20
|
+
subscribe(listener: () => void): () => void
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
async function request<T>(method: string, body: Record<string, unknown>): Promise<T> {
|
|
24
|
+
const response = await fetch(`/dsh-vscode-terminal/api/${method}`, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(body) })
|
|
25
|
+
const text = await response.text()
|
|
26
|
+
let answer: { ok?: boolean; value?: T; error?: { message?: string } }
|
|
27
|
+
try { answer = JSON.parse(text) as { ok?: boolean; value?: T; error?: { message?: string } } } catch { throw new Error(`terminal request returned invalid JSON (${response.status})`) }
|
|
28
|
+
if (!response.ok || answer.ok !== true) throw new Error(answer.error?.message ?? 'terminal request failed')
|
|
29
|
+
return answer.value as T
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
class SocketConnection implements TerminalConnection {
|
|
33
|
+
private readonly socket: WebSocket
|
|
34
|
+
private readonly listeners = new Set<(output: { sequence: number; data: string; replay?: boolean }) => void>()
|
|
35
|
+
private queue: string[] = []
|
|
36
|
+
|
|
37
|
+
constructor(sessionId: string, id: string) {
|
|
38
|
+
const protocol = location.protocol === 'https:' ? 'wss' : 'ws'
|
|
39
|
+
this.socket = new WebSocket(`${protocol}://${location.host}/dsh-vscode-terminal/ws?sessionId=${encodeURIComponent(sessionId)}&id=${encodeURIComponent(id)}`)
|
|
40
|
+
this.socket.onopen = () => { for (const value of this.queue) this.socket.send(value); this.queue = [] }
|
|
41
|
+
this.socket.onmessage = event => {
|
|
42
|
+
try {
|
|
43
|
+
const value = JSON.parse(String(event.data)) as { type?: unknown; sequence?: unknown; data?: unknown }
|
|
44
|
+
if (typeof value.sequence !== 'number' || typeof value.data !== 'string') return
|
|
45
|
+
for (const listener of this.listeners) listener({ sequence: value.sequence, data: value.data, replay: value.type === 'replay' })
|
|
46
|
+
} catch { /* Ignore malformed WebSocket messages from a disconnected server. */ }
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
private send(value: unknown): void { const text = JSON.stringify(value); if (this.socket.readyState === WebSocket.OPEN) this.socket.send(text); else this.queue.push(text) }
|
|
51
|
+
write(data: string): void { this.send({ type: 'input', data }) }
|
|
52
|
+
resize(cols: number, rows: number): void { this.send({ type: 'resize', cols, rows }) }
|
|
53
|
+
close(): void { this.socket.close() }
|
|
54
|
+
subscribe(listener: (output: { sequence: number; data: string; replay?: boolean }) => void): () => void { this.listeners.add(listener); return () => this.listeners.delete(listener) }
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Identify automatic terminal-emulator replies that must not be echoed during replay. */
|
|
58
|
+
export function isTerminalProtocolReply(data: string): boolean {
|
|
59
|
+
return /^\x1b\](?:10|11|12);(?:rgb:[\da-f]{4}\/[\da-f]{4}\/[\da-f]{4}|#[\da-f]{6})(?:\x07|\x1b\\)$/i.test(data)
|
|
60
|
+
|| /^\x1b\[\??[\d;]*[cR]$/.test(data)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Maintain one terminal collection for a DSH session. */
|
|
64
|
+
export class TerminalWorkspaceController implements TerminalWorkspace {
|
|
65
|
+
private snapshot: TerminalWorkspaceSnapshot = { terminals: [], loading: false }
|
|
66
|
+
private readonly listeners = new Set<() => void>()
|
|
67
|
+
|
|
68
|
+
constructor(private readonly sessionId: string) {}
|
|
69
|
+
|
|
70
|
+
async refresh(): Promise<void> {
|
|
71
|
+
this.set({ ...this.snapshot, loading: true, error: undefined })
|
|
72
|
+
try { this.set({ terminals: await request<TerminalInfo[]>('list', { sessionId: this.sessionId }), loading: false }) }
|
|
73
|
+
catch (error) { this.set({ ...this.snapshot, loading: false, error: error instanceof Error ? error.message : String(error) }) }
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
async create(title?: string): Promise<TerminalInfo> { const terminal = await request<TerminalInfo>('create', { sessionId: this.sessionId, ...(title === undefined ? {} : { title }) }); await this.refresh(); return terminal }
|
|
77
|
+
async close(id: string): Promise<void> { await request('close', { sessionId: this.sessionId, id }); await this.refresh() }
|
|
78
|
+
async rename(id: string, title: string): Promise<void> { await request('rename', { sessionId: this.sessionId, id, title }); await this.refresh() }
|
|
79
|
+
connect(id: string): TerminalConnection { return new SocketConnection(this.sessionId, id) }
|
|
80
|
+
getSnapshot(): TerminalWorkspaceSnapshot { return this.snapshot }
|
|
81
|
+
subscribe(listener: () => void): () => void { this.listeners.add(listener); return () => this.listeners.delete(listener) }
|
|
82
|
+
private set(snapshot: TerminalWorkspaceSnapshot): void { this.snapshot = snapshot; for (const listener of this.listeners) listener() }
|
|
83
|
+
}
|
package/src/host.ts
CHANGED
|
@@ -125,8 +125,16 @@ export async function deleteEntry(cwd: string, path: string): Promise<void> {
|
|
|
125
125
|
}
|
|
126
126
|
|
|
127
127
|
const IMAGE_TYPES:Record<string,string>={'.avif':'image/avif','.bmp':'image/bmp','.gif':'image/gif','.ico':'image/x-icon','.jpeg':'image/jpeg','.jpg':'image/jpeg','.png':'image/png','.svg':'image/svg+xml','.webp':'image/webp'}
|
|
128
|
+
const PREVIEW_TYPES:Record<string,string>={...IMAGE_TYPES,'.pdf':'application/pdf','.doc':'application/msword','.docx':'application/vnd.openxmlformats-officedocument.wordprocessingml.document','.odt':'application/vnd.oasis.opendocument.text'}
|
|
128
129
|
/** Resolve an image inside the workspace for protected browser preview. */
|
|
129
130
|
export async function workspaceImage(cwd:string,path:string):Promise<{path:string;type:string}>{const resolved=await workspacePath(cwd,path);const type=IMAGE_TYPES[extname(resolved.absolute).toLowerCase()];if(type===undefined)throw new WorkbenchError('unsupported-media','file is not a supported image',415);return {path:resolved.absolute,type}}
|
|
131
|
+
/** Resolve a supported preview file inside the workspace. */
|
|
132
|
+
export async function workspacePreviewFile(cwd:string,path:string):Promise<{path:string;type:string}>{const resolved=await workspacePath(cwd,path);const type=PREVIEW_TYPES[extname(resolved.absolute).toLowerCase()];if(type===undefined)throw new WorkbenchError('unsupported-media','file is not a supported preview format',415);return {path:resolved.absolute,type}}
|
|
133
|
+
|
|
134
|
+
/** Resolve the PDF.js worker shipped with this plugin's runtime dependency. */
|
|
135
|
+
export function pdfWorkerAsset(): { path: string; type: string } {
|
|
136
|
+
return { path: require.resolve('pdfjs-dist/legacy/build/pdf.worker.min.mjs'), type: 'text/javascript; charset=utf-8' }
|
|
137
|
+
}
|
|
130
138
|
|
|
131
139
|
interface ProcessResult { stdout: string; stderr: string; code: number }
|
|
132
140
|
export function run(command: string, args: string[], cwd: string, timeout = PROCESS_TIMEOUT, signal?: AbortSignal): Promise<ProcessResult> {
|
package/src/index.ts
CHANGED
|
@@ -6,7 +6,8 @@ import type { Duplex } from 'node:stream'
|
|
|
6
6
|
import chokidar from 'chokidar'
|
|
7
7
|
import { WebSocket, WebSocketServer } from 'ws'
|
|
8
8
|
import type { HostContext } from './context.ts'
|
|
9
|
-
import { codiconAsset, createEntry, cwdFor, deleteEntry, gitAction, gitStatus, listFiles, monacoAsset, readDocument, renameEntry, saveDocument, searchWorkspace, uploadFile,
|
|
9
|
+
import { codiconAsset, createEntry, cwdFor, deleteEntry, gitAction, gitStatus, listFiles, monacoAsset, pdfWorkerAsset, readDocument, renameEntry, saveDocument, searchWorkspace, uploadFile, workspacePreviewFile } from './host.ts'
|
|
10
|
+
import { installTerminal } from './terminal.ts'
|
|
10
11
|
import type { SearchRequest } from './types.ts'
|
|
11
12
|
import { readBinaryBody, readBody, send, sendError, stringField, trusted, WorkbenchError } from './wire.ts'
|
|
12
13
|
|
|
@@ -33,6 +34,7 @@ async function dispatch(ctx: HostContext, method: string, body: Record<string, u
|
|
|
33
34
|
}
|
|
34
35
|
|
|
35
36
|
export function apply(ctx: HostContext): void {
|
|
37
|
+
installTerminal(ctx)
|
|
36
38
|
ctx.effect(() => ctx.webServer.register({
|
|
37
39
|
kind: 'exact', path: '/dsh-vscode/codicons/codicon.ttf', handler: async (req, res) => {
|
|
38
40
|
if (!trusted(req, ctx.webRuntime.trustedHosts)) { res.writeHead(403); res.end(); return }
|
|
@@ -44,6 +46,17 @@ export function apply(ctx: HostContext): void {
|
|
|
44
46
|
},
|
|
45
47
|
}), 'dsh-vscode-workbench: Codicon font')
|
|
46
48
|
|
|
49
|
+
ctx.effect(() => ctx.webServer.register({
|
|
50
|
+
kind: 'exact', path: '/dsh-vscode/pdfjs/worker.mjs', handler: async (req, res) => {
|
|
51
|
+
if (!trusted(req, ctx.webRuntime.trustedHosts)) { res.writeHead(403); res.end(); return }
|
|
52
|
+
try {
|
|
53
|
+
const asset = pdfWorkerAsset()
|
|
54
|
+
res.writeHead(200, { 'content-type': asset.type, 'cache-control': 'public, max-age=31536000, immutable' })
|
|
55
|
+
res.end(await readFile(asset.path))
|
|
56
|
+
} catch (error) { sendError(res, error) }
|
|
57
|
+
},
|
|
58
|
+
}), 'dsh-vscode-workbench: PDF.js worker')
|
|
59
|
+
|
|
47
60
|
ctx.effect(() => ctx.webServer.register({
|
|
48
61
|
kind: 'prefix', path: '/dsh-vscode/api', handler: async (req, res) => {
|
|
49
62
|
if (!trusted(req, ctx.webRuntime.trustedHosts)) return send(res, 403, { ok: false, error: { code: 'forbidden', message: 'forbidden' } })
|
|
@@ -72,8 +85,8 @@ export function apply(ctx: HostContext): void {
|
|
|
72
85
|
}), 'dsh-vscode-workbench: file upload')
|
|
73
86
|
|
|
74
87
|
ctx.effect(() => ctx.webServer.register({
|
|
75
|
-
kind:'prefix',path:'/dsh-vscode/file',handler:async(req,res)=>{if(!trusted(req,ctx.webRuntime.trustedHosts)){res.writeHead(403);res.end();return}try{const url=new URL(req.url??'/','http://dsh.local');const sessionId=url.searchParams.get('sessionId');const path=url.searchParams.get('path');if(sessionId===null||path===null)throw new WorkbenchError('bad-request','sessionId and path are required');const asset=await
|
|
76
|
-
}), 'dsh-vscode-workbench:
|
|
88
|
+
kind:'prefix',path:'/dsh-vscode/file',handler:async(req,res)=>{if(!trusted(req,ctx.webRuntime.trustedHosts)){res.writeHead(403);res.end();return}try{const url=new URL(req.url??'/','http://dsh.local');const sessionId=url.searchParams.get('sessionId');const path=url.searchParams.get('path');if(sessionId===null||path===null)throw new WorkbenchError('bad-request','sessionId and path are required');const asset=await workspacePreviewFile(cwdFor(ctx,sessionId),path);res.writeHead(200,{'content-type':asset.type,'x-content-type-options':'nosniff','cache-control':'no-store'});res.end(await readFile(asset.path))}catch(error){sendError(res,error)}}
|
|
89
|
+
}), 'dsh-vscode-workbench: file preview')
|
|
77
90
|
|
|
78
91
|
ctx.effect(() => ctx.webServer.register({
|
|
79
92
|
kind: 'prefix', path: '/dsh-vscode/monaco', handler: async (req, res) => {
|
package/src/terminal.ts
ADDED
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto'
|
|
2
|
+
import { realpathSync } from 'node:fs'
|
|
3
|
+
import { basename } from 'node:path'
|
|
4
|
+
import type { IncomingMessage } from 'node:http'
|
|
5
|
+
import type { Duplex } from 'node:stream'
|
|
6
|
+
import * as pty from 'node-pty'
|
|
7
|
+
import type { IPty } from 'node-pty'
|
|
8
|
+
import { WebSocket, WebSocketServer } from 'ws'
|
|
9
|
+
import type { HostContext } from './context.ts'
|
|
10
|
+
import { cwdFor } from './host.ts'
|
|
11
|
+
import { readBody, send, sendError } from './wire.ts'
|
|
12
|
+
|
|
13
|
+
export interface TerminalInfo { id: string; title: string; cwd: string; running: boolean; exitCode?: number; sequence: number }
|
|
14
|
+
export interface TerminalOutput { sequence: number; data: string }
|
|
15
|
+
|
|
16
|
+
interface TerminalHandle { info: TerminalInfo; process: IPty; replay: string; listeners: Set<(output: TerminalOutput) => void> }
|
|
17
|
+
const SENSITIVE = /(api[_-]?key|token|credential|secret|password|passwd|private[_-]?key|^dsh_.*(?:key|token|secret))/i
|
|
18
|
+
const MAX_TERMINALS = 8
|
|
19
|
+
const REPLAY_BYTES = 2 * 1024 * 1024
|
|
20
|
+
|
|
21
|
+
/** Remove credentials while retaining ordinary interactive-shell variables. */
|
|
22
|
+
export function sanitizedTerminalEnv(env: NodeJS.ProcessEnv): Record<string, string> {
|
|
23
|
+
return Object.fromEntries(Object.entries(env).filter(([key, value]) => value !== undefined && !SENSITIVE.test(key)) as Array<[string, string]>)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Select the user's interactive shell with platform-specific fallbacks. */
|
|
27
|
+
export function defaultTerminalShell(platform = process.platform, env = process.env): { shell: string; args: string[] } {
|
|
28
|
+
if (platform === 'win32') return { shell: env.COMSPEC?.toLowerCase().includes('powershell') ? env.COMSPEC : 'powershell.exe', args: ['-NoLogo'] }
|
|
29
|
+
return { shell: env.SHELL || (platform === 'darwin' ? '/bin/zsh' : '/bin/bash'), args: ['-l'] }
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Own interactive terminals by their canonical workspace directory. */
|
|
33
|
+
export class TerminalManager {
|
|
34
|
+
private readonly workspaces = new Map<string, Map<string, TerminalHandle>>()
|
|
35
|
+
|
|
36
|
+
private workspace(cwd: string): string { return realpathSync(cwd) }
|
|
37
|
+
|
|
38
|
+
list(cwd: string): TerminalInfo[] { return [...(this.workspaces.get(this.workspace(cwd))?.values() ?? [])].map(handle => ({ ...handle.info })) }
|
|
39
|
+
|
|
40
|
+
create(cwd: string, title?: string): TerminalInfo {
|
|
41
|
+
const key = this.workspace(cwd)
|
|
42
|
+
const terminals = this.workspaces.get(key) ?? new Map<string, TerminalHandle>()
|
|
43
|
+
this.workspaces.set(key, terminals)
|
|
44
|
+
if (terminals.size >= MAX_TERMINALS) throw new Error(`terminal limit reached (${MAX_TERMINALS})`)
|
|
45
|
+
const { shell, args } = defaultTerminalShell()
|
|
46
|
+
const child = pty.spawn(shell, args, { name: 'xterm-256color', cols: 80, rows: 24, cwd: key, env: sanitizedTerminalEnv(process.env) })
|
|
47
|
+
const info: TerminalInfo = { id: randomUUID(), title: title || basename(shell), cwd: key, running: true, sequence: 0 }
|
|
48
|
+
const handle: TerminalHandle = { info, process: child, replay: '', listeners: new Set() }
|
|
49
|
+
terminals.set(info.id, handle)
|
|
50
|
+
child.onData(data => {
|
|
51
|
+
info.sequence++
|
|
52
|
+
handle.replay += data
|
|
53
|
+
if (Buffer.byteLength(handle.replay) > REPLAY_BYTES) handle.replay = handle.replay.slice(-REPLAY_BYTES)
|
|
54
|
+
for (const listener of handle.listeners) listener({ sequence: info.sequence, data })
|
|
55
|
+
})
|
|
56
|
+
child.onExit(({ exitCode }) => {
|
|
57
|
+
info.running = false
|
|
58
|
+
info.exitCode = exitCode
|
|
59
|
+
info.sequence++
|
|
60
|
+
const data = `\r\n[process exited with code ${exitCode}]\r\n`
|
|
61
|
+
for (const listener of handle.listeners) listener({ sequence: info.sequence, data })
|
|
62
|
+
})
|
|
63
|
+
return { ...info }
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
get(cwd: string, id: string): TerminalHandle | undefined { return this.workspaces.get(this.workspace(cwd))?.get(id) }
|
|
67
|
+
|
|
68
|
+
close(cwd: string, id: string): void {
|
|
69
|
+
const key = this.workspace(cwd)
|
|
70
|
+
const terminals = this.workspaces.get(key)
|
|
71
|
+
const handle = terminals?.get(id)
|
|
72
|
+
if (handle === undefined || terminals === undefined) return
|
|
73
|
+
terminals.delete(id)
|
|
74
|
+
try { handle.process.kill() } catch { /* The shell may already have exited. */ }
|
|
75
|
+
if (terminals.size === 0) this.workspaces.delete(key)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
rename(cwd: string, id: string, title: string): TerminalInfo {
|
|
79
|
+
const handle = this.get(cwd, id)
|
|
80
|
+
if (handle === undefined) throw new Error('terminal not found')
|
|
81
|
+
handle.info.title = title
|
|
82
|
+
return { ...handle.info }
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
dispose(): void { for (const [cwd, terminals] of this.workspaces) for (const id of terminals.keys()) this.close(cwd, id) }
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function terminalRequestAllowed(req: IncomingMessage): boolean {
|
|
89
|
+
const host = req.headers.host
|
|
90
|
+
if (host === undefined || req.headers['sec-fetch-site'] === 'cross-site') return false
|
|
91
|
+
let hostname: string
|
|
92
|
+
try { hostname = new URL(`http://${host}`).hostname } catch { return false }
|
|
93
|
+
if (hostname !== 'localhost' && hostname !== '[::1]' && !hostname.startsWith('127.')) return false
|
|
94
|
+
const origin = req.headers.origin
|
|
95
|
+
try { return origin === undefined || new URL(origin).host === host } catch { return false }
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** Register the private local terminal API and WebSocket transport. */
|
|
99
|
+
export function installTerminal(ctx: HostContext): void {
|
|
100
|
+
const manager = new TerminalManager()
|
|
101
|
+
ctx.effect(() => ctx.webServer.register({ kind: 'prefix', path: '/dsh-vscode-terminal/api', handler: async (req, res) => {
|
|
102
|
+
if (!terminalRequestAllowed(req)) return send(res, 403, { ok: false, error: { message: 'forbidden' } })
|
|
103
|
+
if (req.method !== 'POST') return send(res, 405, { ok: false, error: { message: 'POST required' } })
|
|
104
|
+
try {
|
|
105
|
+
const body = await readBody(req)
|
|
106
|
+
const sessionId = typeof body.sessionId === 'string' ? body.sessionId : ''
|
|
107
|
+
if (sessionId === '') throw new Error('sessionId required')
|
|
108
|
+
const cwd = cwdFor(ctx, sessionId)
|
|
109
|
+
const method = new URL(req.url ?? '/', 'http://dsh.local').pathname.slice('/dsh-vscode-terminal/api/'.length)
|
|
110
|
+
let value: unknown
|
|
111
|
+
if (method === 'list') value = manager.list(cwd)
|
|
112
|
+
else if (method === 'create') value = manager.create(cwd, typeof body.title === 'string' ? body.title : undefined)
|
|
113
|
+
else if (method === 'close') { manager.close(cwd, typeof body.id === 'string' ? body.id : ''); value = manager.list(cwd) }
|
|
114
|
+
else if (method === 'rename') value = manager.rename(cwd, typeof body.id === 'string' ? body.id : '', typeof body.title === 'string' ? body.title : '')
|
|
115
|
+
else throw new Error('unknown terminal method')
|
|
116
|
+
send(res, 200, { ok: true, value })
|
|
117
|
+
} catch (error) { sendError(res, error) }
|
|
118
|
+
} }), 'dsh-vscode-workbench: terminal API')
|
|
119
|
+
|
|
120
|
+
const webSocket = new WebSocketServer({ noServer: true })
|
|
121
|
+
ctx.effect(() => ctx.webServer.registerUpgrade({ path: '/dsh-vscode-terminal/ws', handler(req, socket, head) {
|
|
122
|
+
if (!terminalRequestAllowed(req as IncomingMessage)) { socket.destroy(); return }
|
|
123
|
+
webSocket.handleUpgrade(req as IncomingMessage, socket as Duplex, head, connection => {
|
|
124
|
+
const url = new URL(req.url ?? '/', 'http://dsh.local')
|
|
125
|
+
const sessionId = url.searchParams.get('sessionId')
|
|
126
|
+
const id = url.searchParams.get('id')
|
|
127
|
+
if (sessionId === null || id === null) { connection.close(1008, 'sessionId and id required'); return }
|
|
128
|
+
const handle = manager.get(cwdFor(ctx, sessionId), id)
|
|
129
|
+
if (handle === undefined) { connection.close(1008, 'terminal not found'); return }
|
|
130
|
+
if (handle.replay !== '') connection.send(JSON.stringify({ type: 'replay', sequence: handle.info.sequence, data: handle.replay }))
|
|
131
|
+
const listener = (output: TerminalOutput) => { if (connection.readyState === WebSocket.OPEN) connection.send(JSON.stringify({ type: 'output', ...output })) }
|
|
132
|
+
handle.listeners.add(listener)
|
|
133
|
+
connection.on('message', raw => {
|
|
134
|
+
const text = raw.toString()
|
|
135
|
+
try {
|
|
136
|
+
const message: unknown = JSON.parse(text)
|
|
137
|
+
if (message !== null && typeof message === 'object' && 'type' in message && message.type === 'input' && 'data' in message && typeof message.data === 'string') handle.process.write(message.data)
|
|
138
|
+
else if (message !== null && typeof message === 'object' && 'type' in message && message.type === 'resize' && 'cols' in message && 'rows' in message && typeof message.cols === 'number' && typeof message.rows === 'number') handle.process.resize(Math.max(2, Math.floor(message.cols)), Math.max(2, Math.floor(message.rows)))
|
|
139
|
+
} catch { handle.process.write(text) }
|
|
140
|
+
})
|
|
141
|
+
connection.on('close', () => handle.listeners.delete(listener))
|
|
142
|
+
})
|
|
143
|
+
} }), 'dsh-vscode-workbench: terminal WebSocket')
|
|
144
|
+
ctx.effect(() => () => { webSocket.close(); manager.dispose() }, 'dsh-vscode-workbench: terminal teardown')
|
|
145
|
+
}
|