dsh-better-sidebar: VSCode-like right sidebar plugin for the DSH web GUI

Host half (src/index.ts): /sidebar/api JSON API (session cwd, fs tree/read/
write, git status/diff/stage/commit/branch/log/show), /sidebar/file media
route, /sidebar/ws/terminal WebSocket over node-pty; routes share the /api
trust fence (Host-header loopback or connection.trustedHosts).

Client half (src/client): portal-mounted right panel with a per-session
split-pane workbench (recursive row/col splits, draggable dividers, tab
drag-move), tab bar with max-width 160px + plus-menu (explorer/git/terminal
multi-open rules), lazy file explorer rooted at the session cwd, editable
text editor (dirty dot, Ctrl/Cmd+S), image + Markdown preview, xterm
terminal with reconnect, and a VSCode-style git panel. Per-session state
persists to localStorage (dsh-sidebar:v1:<sessionId>). Intercepts the chat
turn-tail produced-files row (priority -1 chain entry) to open files in the
sidebar. UI styled with the dsh --dsw-* theme tokens and ui-primitives.

Build: tsdown emits the host lib plus the __ModuleLoader__ client bundle
(externals = module table words; xterm/css inlined). Tests: 19 unit + smoke
specs (pure state/tree/git parsers + real git/fs integration).
This commit is contained in:
Menghuan1918
2026-08-08 01:02:24 +08:00
parent 4f4e462f98
commit 63e2988ef1
31 changed files with 5807 additions and 0 deletions
+783
View File
@@ -0,0 +1,783 @@
/**
* Sidebar styles: everything rides the DSH theme tokens (--dsw-alias-*,
* --dsw-static-*, --ds-transition-*) so the plugin matches the host UI in
* both light and dark themes, plus a few absolute z-indexes for the
* document-level overlay (below the app's modal layer).
*/
/* ── Shell ─────────────────────────────────────────────────────────────── */
.toggleRail {
position: fixed;
right: 0;
top: 50%;
transform: translateY(-50%);
z-index: 55;
display: flex;
align-items: center;
}
.toggleButton {
display: flex;
align-items: center;
justify-content: center;
width: 22px;
height: 56px;
border: 1px solid var(--dsw-alias-border-l2-darkmode-thin);
border-right: none;
border-radius: 10px 0 0 10px;
background: var(--dsw-alias-button-floating-fill);
color: var(--dsw-alias-label-secondary);
cursor: pointer;
transition:
background var(--ds-transition-duration-slow) var(--ds-ease-in-out),
color var(--ds-transition-duration-slow) var(--ds-ease-in-out);
}
.toggleButton:hover:not(:disabled) {
background: var(--dsw-alias-button-floating-hover);
color: var(--dsw-alias-label-primary);
}
.toggleButton:disabled {
opacity: 0.4;
cursor: default;
}
.panel {
position: fixed;
top: 0;
right: 0;
bottom: 0;
z-index: 50;
display: flex;
flex-direction: column;
background: var(--dsw-alias-bg-layer-1);
border-left: 1px solid var(--dsw-alias-border-l2);
box-shadow: -8px 0 24px rgba(0, 0, 0, 0.08);
}
.panelResize {
position: absolute;
left: -4px;
top: 0;
bottom: 0;
width: 8px;
cursor: col-resize;
z-index: 2;
touch-action: none;
}
.panelResizeActive {
background: var(--dsw-alias-interactive-bg-hover-accent);
}
.panelHeader {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
padding: 6px 8px 6px 12px;
border-bottom: 1px solid var(--dsw-alias-border-l1);
flex: none;
}
.panelTitle {
font-size: 12px;
font-weight: 600;
color: var(--dsw-alias-label-secondary);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.panelBody {
flex: 1;
min-height: 0;
display: flex;
}
.iconButton {
display: inline-flex;
align-items: center;
justify-content: center;
width: 24px;
height: 24px;
padding: 0;
border: none;
border-radius: 6px;
background: transparent;
color: var(--dsw-alias-label-tertiary);
cursor: pointer;
flex: none;
}
.iconButton:hover:not(:disabled) {
background: var(--dsw-alias-interactive-bg-hover);
color: var(--dsw-alias-label-primary);
}
.iconButton:disabled {
opacity: 0.4;
cursor: default;
}
/* ── Workbench / split panes ───────────────────────────────────────────── */
.workbench {
flex: 1;
min-width: 0;
min-height: 0;
display: flex;
}
.split {
flex: 1;
min-width: 0;
min-height: 0;
display: flex;
}
.splitRow {
flex-direction: row;
}
.splitCol {
flex-direction: column;
}
.splitChild {
position: relative;
display: flex;
overflow: hidden;
}
.divider {
flex: none;
z-index: 3;
touch-action: none;
background: transparent;
transition: background var(--ds-transition-duration-slow) var(--ds-ease-in-out);
}
.dividerRow {
width: 5px;
margin: 0 -2px;
cursor: col-resize;
}
.dividerCol {
height: 5px;
margin: -2px 0;
cursor: row-resize;
}
.divider:hover,
.dividerActive {
background: var(--dsw-alias-interactive-bg-hover-accent);
}
.pane {
flex: 1;
min-width: 0;
min-height: 0;
display: flex;
flex-direction: column;
background: var(--dsw-alias-bg-base);
}
.paneDrop {
outline: 1px solid var(--dsw-alias-interactive-bg-hover-accent);
outline-offset: -1px;
}
.paneContent {
flex: 1;
min-height: 0;
display: flex;
flex-direction: column;
overflow: hidden;
}
.paneEmpty {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
padding: 16px;
text-align: center;
font-size: 12px;
color: var(--dsw-alias-label-tertiary);
}
/* ── Tab strip ─────────────────────────────────────────────────────────── */
.tabBar {
flex: none;
display: flex;
align-items: stretch;
height: 34px;
border-bottom: 1px solid var(--dsw-alias-border-l1);
background: var(--dsw-alias-bg-layer-1);
}
.tabBarDrop {
outline: 1px dashed var(--dsw-alias-interactive-bg-hover-accent);
outline-offset: -1px;
}
.tabList {
flex: 1;
min-width: 0;
display: flex;
overflow-x: auto;
scrollbar-width: none;
}
.tabList::-webkit-scrollbar {
display: none;
}
.tab {
flex: none;
max-width: 160px;
min-width: 64px;
display: flex;
align-items: center;
gap: 4px;
padding: 0 4px 0 10px;
font-size: 12px;
color: var(--dsw-alias-label-secondary);
border-right: 1px solid var(--dsw-alias-border-l1);
background: transparent;
cursor: pointer;
user-select: none;
}
.tab:hover {
background: var(--dsw-alias-interactive-bg-hover);
}
.tabActive {
color: var(--dsw-alias-label-primary);
background: var(--dsw-alias-bg-base);
box-shadow: inset 0 -2px 0 var(--dsw-alias-brand-primary);
}
.tabTitle {
flex: 1;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.tabClose {
display: inline-flex;
align-items: center;
justify-content: center;
width: 18px;
height: 18px;
padding: 0;
border: none;
border-radius: 4px;
background: transparent;
color: var(--dsw-alias-label-tertiary);
cursor: pointer;
flex: none;
}
.tabClose:hover {
background: var(--dsw-alias-interactive-bg-hover);
color: var(--dsw-alias-label-primary);
}
.tabBarActions {
flex: none;
display: flex;
align-items: center;
gap: 2px;
padding: 0 6px;
}
.tabBarButton {
display: inline-flex;
align-items: center;
justify-content: center;
width: 22px;
height: 22px;
padding: 0;
border: none;
border-radius: 5px;
background: transparent;
color: var(--dsw-alias-label-tertiary);
cursor: pointer;
}
.tabBarButton:hover {
background: var(--dsw-alias-interactive-bg-hover);
color: var(--dsw-alias-label-primary);
}
/* ── Explorer ──────────────────────────────────────────────────────────── */
.explorer {
flex: 1;
min-height: 0;
display: flex;
flex-direction: column;
}
.explorerHeader {
flex: none;
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
padding: 6px 8px 6px 12px;
}
.explorerRoot {
font-size: 12px;
font-weight: 600;
color: var(--dsw-alias-label-secondary);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.explorerBody {
flex: 1;
min-height: 0;
overflow-y: auto;
padding-bottom: 8px;
}
.explorerRow {
display: flex;
align-items: center;
gap: 6px;
width: 100%;
height: 24px;
padding: 0 8px;
border: none;
background: transparent;
font-size: 12px;
color: var(--dsw-alias-label-primary);
text-align: left;
cursor: pointer;
white-space: nowrap;
}
.explorerRow:hover {
background: var(--dsw-alias-interactive-bg-hover);
}
.explorerDir {
font-weight: 500;
}
.explorerHidden {
opacity: 0.45;
}
.explorerName {
overflow: hidden;
text-overflow: ellipsis;
}
.explorerError {
color: var(--dsw-static-red-500, #ef4444);
cursor: default;
}
.explorerEmpty {
padding: 16px;
font-size: 12px;
color: var(--dsw-alias-label-tertiary);
text-align: center;
}
/* ── Editor ────────────────────────────────────────────────────────────── */
.editor {
flex: 1;
min-height: 0;
display: flex;
flex-direction: column;
}
.editorHeader {
flex: none;
display: flex;
align-items: center;
gap: 6px;
padding: 4px 8px;
border-bottom: 1px solid var(--dsw-alias-border-l1);
}
.editorTitle {
flex: 1;
min-width: 0;
font-size: 12px;
font-weight: 600;
color: var(--dsw-alias-label-secondary);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.editorStatus {
font-size: 11px;
color: var(--dsw-alias-label-tertiary);
}
.editorStatusError {
color: var(--dsw-static-red-500, #ef4444);
}
.dirtyDot {
width: 7px;
height: 7px;
border-radius: 50%;
background: var(--dsw-static-amber-500, #f59e0b);
flex: none;
}
.editorPlaceholder {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
font-size: 12px;
color: var(--dsw-alias-label-tertiary);
padding: 16px;
text-align: center;
}
.editorError {
padding: 12px 16px;
font-size: 12px;
color: var(--dsw-static-red-500, #ef4444);
}
.editorBanner {
flex: none;
padding: 4px 12px;
font-size: 11px;
color: var(--dsw-static-amber-600, #dd8629);
background: var(--dsw-static-amber-100, #fef5e7);
}
.editorTextarea {
flex: 1;
min-height: 0;
width: 100%;
padding: 10px 12px;
border: none;
outline: none;
resize: none;
background: transparent;
color: var(--dsw-alias-label-primary);
font-family: var(--dsw-font-mono, 'SF Mono', Menlo, Consolas, monospace);
font-size: 12px;
line-height: 1.55;
tab-size: 2;
white-space: pre;
}
.editorImageWrap {
flex: 1;
min-height: 0;
display: flex;
align-items: center;
justify-content: center;
overflow: auto;
padding: 12px;
}
.editorImage {
max-width: 100%;
max-height: 100%;
object-fit: contain;
}
.editorMd {
flex: 1;
min-height: 0;
overflow-y: auto;
padding: 10px 14px;
font-size: 13px;
}
/* ── Terminal ──────────────────────────────────────────────────────────── */
.terminalWrap {
flex: 1;
min-height: 0;
position: relative;
display: flex;
flex-direction: column;
background: var(--dsw-alias-bg-base);
}
.terminal {
flex: 1;
min-height: 0;
padding: 6px 4px 6px 8px;
}
.terminal :global(.xterm) {
height: 100%;
}
.terminalBanner {
flex: none;
padding: 3px 10px;
font-size: 11px;
color: var(--dsw-static-amber-600, #dd8629);
background: var(--dsw-static-amber-100, #fef5e7);
}
/* ── Git ───────────────────────────────────────────────────────────────── */
.git {
flex: 1;
min-height: 0;
display: flex;
flex-direction: column;
overflow-y: auto;
}
.gitHeader {
flex: none;
display: flex;
align-items: center;
gap: 8px;
padding: 6px 8px 6px 12px;
}
.gitBranchSelect {
flex: 1;
min-width: 0;
height: 26px;
padding: 0 6px;
border: 1px solid var(--dsw-alias-border-l2);
border-radius: 6px;
background: var(--dsw-alias-bg-base);
color: var(--dsw-alias-label-primary);
font-size: 12px;
}
.gitSection {
border-top: 1px solid var(--dsw-alias-border-l1);
}
.gitSectionHeader {
display: flex;
align-items: center;
justify-content: space-between;
padding: 6px 12px 4px;
font-size: 11px;
font-weight: 600;
color: var(--dsw-alias-label-tertiary);
text-transform: uppercase;
}
.gitLink {
border: none;
background: transparent;
font-size: 11px;
color: var(--dsw-alias-brand-primary);
cursor: pointer;
padding: 0;
}
.gitLink:hover:not(:disabled) {
text-decoration: underline;
}
.gitLink:disabled {
opacity: 0.4;
cursor: default;
}
.gitRow {
display: flex;
align-items: center;
gap: 6px;
padding: 2px 8px 2px 12px;
}
.gitRow:hover {
background: var(--dsw-alias-interactive-bg-hover);
}
.gitRowSelected {
background: var(--dsw-alias-interactive-bg-hover);
}
.gitRowMain {
flex: 1;
min-width: 0;
display: flex;
align-items: center;
gap: 8px;
border: none;
background: transparent;
padding: 3px 0;
cursor: pointer;
text-align: left;
}
.gitBadge {
flex: none;
width: 20px;
height: 16px;
display: inline-flex;
align-items: center;
justify-content: center;
border-radius: 4px;
font-size: 10px;
font-weight: 700;
background: var(--dsw-alias-interactive-bg-hover);
color: var(--dsw-alias-label-secondary);
}
.gitName {
flex: 1;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-size: 12px;
color: var(--dsw-alias-label-primary);
}
.gitEmpty {
padding: 4px 12px 8px;
font-size: 12px;
color: var(--dsw-alias-label-tertiary);
}
.gitPlaceholder {
padding: 16px;
font-size: 12px;
color: var(--dsw-alias-label-tertiary);
text-align: center;
}
.gitError {
padding: 8px 12px;
font-size: 12px;
color: var(--dsw-static-red-500, #ef4444);
white-space: pre-wrap;
}
.gitDiff {
border-top: 1px solid var(--dsw-alias-border-l1);
padding: 8px;
}
.gitCommit {
display: flex;
gap: 6px;
padding: 8px 12px;
border-top: 1px solid var(--dsw-alias-border-l1);
align-items: center;
}
.gitCommitInput {
flex: 1;
min-width: 0;
}
.gitCommitButton {
flex: none;
height: 26px;
padding: 0 12px;
border: none;
border-radius: 6px;
background: var(--dsw-alias-button-primary-fill);
color: var(--dsw-alias-label-primary-inverted, #fff);
font-size: 12px;
cursor: pointer;
}
.gitCommitButton:hover:not(:disabled) {
background: var(--dsw-alias-button-primary-hover);
}
.gitCommitButton:disabled {
opacity: 0.45;
cursor: default;
}
.gitLogRow {
display: flex;
align-items: baseline;
gap: 8px;
padding: 3px 12px;
font-size: 12px;
}
.gitLogHash {
flex: none;
font-family: var(--dsw-font-mono, 'SF Mono', Menlo, Consolas, monospace);
color: var(--dsw-alias-label-tertiary);
}
.gitLogSubject {
flex: 1;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
color: var(--dsw-alias-label-primary);
}
/* ── Intercepted produced-files row ────────────────────────────────────── */
.producedRow {
display: flex;
align-items: center;
gap: 8px;
flex-wrap: wrap;
padding: 4px 0;
}
.producedLabel {
font-size: 12px;
color: var(--dsw-alias-label-tertiary);
}
.producedChip {
display: inline-flex;
align-items: center;
gap: 4px;
max-width: 200px;
padding: 2px 8px;
border: 1px solid var(--dsw-alias-border-l2);
border-radius: 999px;
background: var(--dsw-alias-bg-layer-2);
color: var(--dsw-alias-label-secondary);
font-size: 12px;
cursor: pointer;
overflow: hidden;
}
.producedChip:hover {
background: var(--dsw-alias-interactive-bg-hover);
color: var(--dsw-alias-label-primary);
}
.producedChip span {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.producedMore {
font-size: 12px;
color: var(--dsw-alias-label-tertiary);
}