@workbench-kit/react 0.0.1-prototype.0

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.
Files changed (51) hide show
  1. package/package.json +62 -0
  2. package/src/index.ts +43 -0
  3. package/src/layout/Panel.tsx +27 -0
  4. package/src/layout/SideBarViewFrame.tsx +328 -0
  5. package/src/modal/ConfirmDialog.tsx +53 -0
  6. package/src/modal/Modal.tsx +110 -0
  7. package/src/overlay/ContextMenu.tsx +145 -0
  8. package/src/primitives/Badge.tsx +12 -0
  9. package/src/primitives/Button.tsx +14 -0
  10. package/src/primitives/Checkbox.tsx +15 -0
  11. package/src/primitives/EmptyState.tsx +26 -0
  12. package/src/primitives/Field.tsx +38 -0
  13. package/src/primitives/IconButton.tsx +32 -0
  14. package/src/primitives/Select.tsx +12 -0
  15. package/src/primitives/TextInput.tsx +24 -0
  16. package/src/primitives/Toolbar.tsx +8 -0
  17. package/src/styles.css +1762 -0
  18. package/src/utils/cx.ts +3 -0
  19. package/src/workbench/ActivityBar.tsx +52 -0
  20. package/src/workbench/SplitView.tsx +136 -0
  21. package/src/workbench/StatusBar.tsx +123 -0
  22. package/src/workbench/WorkbenchShell.tsx +70 -0
  23. package/src/workbench/WorkbenchStandaloneShell.tsx +291 -0
  24. package/src/workbench/chat/ChatComposer.tsx +148 -0
  25. package/src/workbench/chat/ChatMessageItem.tsx +35 -0
  26. package/src/workbench/chat/ChatMessageList.tsx +49 -0
  27. package/src/workbench/chat/ChatPanel.tsx +55 -0
  28. package/src/workbench/chat/index.ts +9 -0
  29. package/src/workbench/chat/types.ts +10 -0
  30. package/src/workbench/commands.ts +490 -0
  31. package/src/workbench/index.ts +98 -0
  32. package/src/workbench/settings/WorkbenchSettingsModal.tsx +188 -0
  33. package/src/workbench/settings/WorkbenchSettingsNav.tsx +41 -0
  34. package/src/workbench/settings/WorkbenchSettingsSection.tsx +30 -0
  35. package/src/workbench/settings/index.ts +7 -0
  36. package/src/workbench/settings/types.ts +16 -0
  37. package/src/workbench/shellState.ts +203 -0
  38. package/src/workbench/standalone.ts +94 -0
  39. package/src/workbench/workspace/WorkspaceEditor.tsx +226 -0
  40. package/src/workbench/workspace/WorkspaceEditorPanel.tsx +353 -0
  41. package/src/workbench/workspace/WorkspaceExplorer.tsx +524 -0
  42. package/src/workbench/workspace/WorkspaceFileIcon.tsx +149 -0
  43. package/src/workbench/workspace/WorkspaceHighlightedText.tsx +22 -0
  44. package/src/workbench/workspace/WorkspaceSearchPanel.tsx +113 -0
  45. package/src/workbench/workspace/WorkspaceSearchResults.tsx +47 -0
  46. package/src/workbench/workspace/index.ts +98 -0
  47. package/src/workbench/workspace/path.ts +10 -0
  48. package/src/workbench/workspace/search.ts +6 -0
  49. package/src/workbench/workspace/tree.ts +1 -0
  50. package/src/workbench/workspace/types.ts +10 -0
  51. package/src/workbench/workspace/useVirtualWorkspace.ts +98 -0
@@ -0,0 +1,226 @@
1
+ import Editor, { loader, type OnMount } from '@monaco-editor/react';
2
+ import * as monaco from 'monaco-editor';
3
+ import { Panel, PanelBody, PanelHeader } from '../../layout/Panel';
4
+ import { Badge } from '../../primitives/Badge';
5
+ import { IconButton } from '../../primitives/IconButton';
6
+ import { Toolbar } from '../../primitives/Toolbar';
7
+ import { extensionOfPath } from './path';
8
+ import { WorkspaceFileIcon } from './WorkspaceFileIcon';
9
+ import type { WorkspaceFile } from './types';
10
+
11
+ loader.config({ monaco });
12
+
13
+ export type WorkspaceEditorTheme = 'dark' | 'light';
14
+
15
+ const MONACO_DARK_THEME_ID = 'newchobo-workbench-dark';
16
+ const MONACO_LIGHT_THEME_ID = 'newchobo-workbench-light';
17
+ let monacoThemeDefined = false;
18
+
19
+ function defineMonacoWorkbenchTheme(monacoInstance: typeof monaco) {
20
+ if (monacoThemeDefined) return;
21
+
22
+ monacoInstance.editor.defineTheme(MONACO_DARK_THEME_ID, {
23
+ base: 'vs-dark',
24
+ inherit: true,
25
+ rules: [],
26
+ colors: {
27
+ 'editor.background': '#0d1117',
28
+ 'editor.foreground': '#e6edf3',
29
+ 'editorGutter.background': '#0d1117',
30
+ 'editorLineNumber.foreground': '#484f58',
31
+ 'editorLineNumber.activeForeground': '#7d8590',
32
+ 'editorCursor.foreground': '#e6edf3',
33
+ 'editor.lineHighlightBackground': '#161b22',
34
+ 'editor.selectionBackground': '#264f78',
35
+ 'editor.inactiveSelectionBackground': '#1f3a53',
36
+ 'editorWidget.background': '#161b22',
37
+ 'editorWidget.border': '#30363d',
38
+ 'editorHoverWidget.background': '#161b22',
39
+ 'editorHoverWidget.border': '#30363d',
40
+ 'editorSuggestWidget.background': '#161b22',
41
+ 'editorSuggestWidget.border': '#30363d',
42
+ focusBorder: '#2f81f7',
43
+ 'input.background': '#0d1117',
44
+ 'input.border': '#30363d',
45
+ 'minimap.background': '#0d1117',
46
+ 'scrollbarSlider.background': '#30363d66',
47
+ 'scrollbarSlider.hoverBackground': '#484f5888',
48
+ 'scrollbarSlider.activeBackground': '#7d8590aa',
49
+ 'editorIndentGuide.background1': '#30363d',
50
+ 'editorIndentGuide.activeBackground1': '#7d8590',
51
+ 'editorWhitespace.foreground': '#30363d',
52
+ 'editorBracketMatch.background': '#21262d',
53
+ 'editorBracketMatch.border': '#7d8590',
54
+ 'editorOverviewRuler.border': '#0d1117',
55
+ },
56
+ });
57
+
58
+ monacoInstance.editor.defineTheme(MONACO_LIGHT_THEME_ID, {
59
+ base: 'vs',
60
+ inherit: true,
61
+ rules: [],
62
+ colors: {
63
+ 'editor.background': '#ffffff',
64
+ 'editor.foreground': '#1f2328',
65
+ 'editorGutter.background': '#ffffff',
66
+ 'editorLineNumber.foreground': '#9198a1',
67
+ 'editorLineNumber.activeForeground': '#656d76',
68
+ 'editorCursor.foreground': '#1f2328',
69
+ 'editor.lineHighlightBackground': '#f6f8fa',
70
+ 'editor.selectionBackground': '#bfdbfe',
71
+ 'editor.inactiveSelectionBackground': '#dbeafe',
72
+ 'editorWidget.background': '#f6f8fa',
73
+ 'editorWidget.border': '#d0d7de',
74
+ 'editorHoverWidget.background': '#f6f8fa',
75
+ 'editorHoverWidget.border': '#d0d7de',
76
+ 'editorSuggestWidget.background': '#f6f8fa',
77
+ 'editorSuggestWidget.border': '#d0d7de',
78
+ focusBorder: '#0969da',
79
+ 'input.background': '#ffffff',
80
+ 'input.border': '#d0d7de',
81
+ 'minimap.background': '#ffffff',
82
+ 'scrollbarSlider.background': '#d0d7de66',
83
+ 'scrollbarSlider.hoverBackground': '#9198a188',
84
+ 'scrollbarSlider.activeBackground': '#656d76aa',
85
+ 'editorIndentGuide.background1': '#d0d7de',
86
+ 'editorIndentGuide.activeBackground1': '#656d76',
87
+ 'editorWhitespace.foreground': '#d0d7de',
88
+ 'editorBracketMatch.background': '#eaeef2',
89
+ 'editorBracketMatch.border': '#656d76',
90
+ 'editorOverviewRuler.border': '#ffffff',
91
+ },
92
+ });
93
+ monacoThemeDefined = true;
94
+ }
95
+
96
+ export function monacoThemeForWorkspaceTheme(theme: WorkspaceEditorTheme) {
97
+ return theme === 'light' ? MONACO_LIGHT_THEME_ID : MONACO_DARK_THEME_ID;
98
+ }
99
+
100
+ export function languageForFile(path: string, mimeType?: string) {
101
+ const extension = extensionOfPath(path);
102
+ if (mimeType === 'application/json') return 'json';
103
+ if (mimeType === 'text/css') return 'css';
104
+ if (mimeType === 'text/html') return 'html';
105
+ if (mimeType === 'text/markdown') return 'markdown';
106
+
107
+ switch (extension) {
108
+ case 'css':
109
+ return 'css';
110
+ case 'html':
111
+ return 'html';
112
+ case 'cjs':
113
+ case 'js':
114
+ case 'jsx':
115
+ case 'mjs':
116
+ return 'javascript';
117
+ case 'json':
118
+ return 'json';
119
+ case 'md':
120
+ case 'mdx':
121
+ return 'markdown';
122
+ case 'ts':
123
+ case 'tsx':
124
+ return 'typescript';
125
+ case 'xml':
126
+ return 'xml';
127
+ case 'yaml':
128
+ case 'yml':
129
+ return 'yaml';
130
+ default:
131
+ return 'plaintext';
132
+ }
133
+ }
134
+
135
+ export interface WorkspaceEditorProps {
136
+ compact?: boolean;
137
+ file: WorkspaceFile;
138
+ onChange?: (content: string) => void;
139
+ onSave?: (content: string) => void;
140
+ readOnly?: boolean;
141
+ showHeader?: boolean;
142
+ theme?: WorkspaceEditorTheme;
143
+ value?: string;
144
+ }
145
+
146
+ export function WorkspaceEditor({
147
+ compact,
148
+ file,
149
+ onChange,
150
+ onSave,
151
+ readOnly = !onChange,
152
+ showHeader = true,
153
+ theme = 'dark',
154
+ value = file.content,
155
+ }: WorkspaceEditorProps) {
156
+ const language = languageForFile(file.path, file.mimeType);
157
+ const handleMount: OnMount = (editor, monacoInstance) => {
158
+ if (!onSave) return;
159
+
160
+ editor.addCommand(monacoInstance.KeyMod.CtrlCmd | monacoInstance.KeyCode.KeyS, () => {
161
+ onSave(editor.getValue());
162
+ });
163
+ };
164
+
165
+ return (
166
+ <Panel className="workbench-monaco-panel" style={{ minWidth: 0, minHeight: compact ? 260 : 0 }}>
167
+ {showHeader ? (
168
+ <PanelHeader
169
+ actions={
170
+ <Toolbar>
171
+ <Badge variant="muted">{language}</Badge>
172
+ {file.source ? <Badge variant="muted">{file.source}</Badge> : null}
173
+ <IconButton icon="codicon-split" label="Split editor" />
174
+ </Toolbar>
175
+ }
176
+ >
177
+ <span className="workbench-editor-title">
178
+ <WorkspaceFileIcon mimeType={file.mimeType} path={file.path} />
179
+ {file.path}
180
+ </span>
181
+ </PanelHeader>
182
+ ) : (
183
+ <div className="workspace-editor__file-bar" title={file.path}>
184
+ <WorkspaceFileIcon mimeType={file.mimeType} path={file.path} />
185
+ <span className="workspace-editor__file-path">{file.path}</span>
186
+ <span className="workspace-editor__file-meta">{file.mimeType ?? language}</span>
187
+ </div>
188
+ )}
189
+ <PanelBody className="workbench-monaco-panel__body">
190
+ <div className="workspace-editor__monaco">
191
+ <Editor
192
+ beforeMount={defineMonacoWorkbenchTheme}
193
+ height="100%"
194
+ language={language}
195
+ loading={<div className="workspace-editor__loading">Loading editor...</div>}
196
+ options={{
197
+ automaticLayout: true,
198
+ contextmenu: true,
199
+ fontFamily: 'ui-monospace, SFMono-Regular, Consolas, monospace',
200
+ fontSize: 13,
201
+ lineHeight: 20,
202
+ minimap: { enabled: false },
203
+ overviewRulerBorder: false,
204
+ padding: { bottom: 12, top: 12 },
205
+ readOnly,
206
+ renderLineHighlight: 'line',
207
+ scrollBeyondLastLine: false,
208
+ scrollbar: {
209
+ alwaysConsumeMouseWheel: false,
210
+ horizontalScrollbarSize: 10,
211
+ verticalScrollbarSize: 10,
212
+ },
213
+ tabSize: 2,
214
+ wordWrap: 'on',
215
+ }}
216
+ path={file.path}
217
+ theme={monacoThemeForWorkspaceTheme(theme)}
218
+ value={value}
219
+ onChange={(nextValue) => onChange?.(nextValue ?? '')}
220
+ onMount={handleMount}
221
+ />
222
+ </div>
223
+ </PanelBody>
224
+ </Panel>
225
+ );
226
+ }
@@ -0,0 +1,353 @@
1
+ import { useMemo, useState, type MouseEvent } from 'react';
2
+ import {
3
+ canExecuteCommand,
4
+ createCommandRegistry,
5
+ executeCommand,
6
+ resolveCommandMenuItems,
7
+ } from '@workbench-kit/core';
8
+ import { isSaveSuccess, type SaveResult } from '@workbench-kit/contracts';
9
+ import {
10
+ discardWorkspaceFileDraft,
11
+ resolveWorkspaceFileDraft,
12
+ saveWorkspaceFileDraft,
13
+ updateWorkspaceFileDraft,
14
+ type WorkspaceFileDraftMap,
15
+ } from '@workbench-kit/workspace';
16
+ import { ConfirmDialog } from '../../modal/ConfirmDialog';
17
+ import { ContextMenu, type ContextMenuItem } from '../../overlay/ContextMenu';
18
+ import { EmptyState } from '../../primitives/EmptyState';
19
+ import { IconButton } from '../../primitives/IconButton';
20
+ import { Toolbar } from '../../primitives/Toolbar';
21
+ import { Panel, PanelBody } from '../../layout/Panel';
22
+ import {
23
+ WORKBENCH_EDITOR_DISCARD_CHANGES_COMMAND_ID,
24
+ WORKBENCH_EDITOR_SAVE_COMMAND_ID,
25
+ WORKBENCH_COMMAND_SURFACE_EDITOR,
26
+ commandMenuItemsToContextMenuItems,
27
+ createWorkbenchEditorCommands,
28
+ createWorkbenchEditorTabListMenuEntries,
29
+ createWorkbenchEditorTabMenuEntries,
30
+ type WorkbenchEditorCommandContext,
31
+ } from '../commands';
32
+ import { fileNameOfPath } from './path';
33
+ import { WorkspaceEditor, type WorkspaceEditorTheme } from './WorkspaceEditor';
34
+ import { WorkspaceFileIcon } from './WorkspaceFileIcon';
35
+ import type { WorkspaceFile } from './types';
36
+
37
+ const editorCommandRegistry = createCommandRegistry(createWorkbenchEditorCommands());
38
+ const editorTabListMenuEntries = createWorkbenchEditorTabListMenuEntries();
39
+ const editorTabMenuEntries = createWorkbenchEditorTabMenuEntries();
40
+
41
+ export interface WorkspaceEditorPanelProps {
42
+ emptyLabel?: string;
43
+ files: WorkspaceFile[];
44
+ onCloseAll?: () => void;
45
+ onCloseOthers?: (path: string) => void;
46
+ onClosePath?: (path: string) => void;
47
+ onCopyPath?: (path: string) => void;
48
+ onDeletePath?: (path: string) => void;
49
+ onSaveFile?: (
50
+ path: string,
51
+ content: string,
52
+ previousUpdatedAt?: string,
53
+ ) => SaveResult | Promise<SaveResult | undefined> | undefined;
54
+ onSelectedPathChange: (path: string) => void;
55
+ openPaths: string[];
56
+ selectedPath?: string;
57
+ theme?: WorkspaceEditorTheme;
58
+ }
59
+
60
+ export function WorkspaceEditorPanel({
61
+ emptyLabel = 'Open a file from Explorer or Search.',
62
+ files,
63
+ onCloseAll,
64
+ onCloseOthers,
65
+ onClosePath,
66
+ onCopyPath,
67
+ onDeletePath,
68
+ onSaveFile,
69
+ onSelectedPathChange,
70
+ openPaths,
71
+ selectedPath,
72
+ theme,
73
+ }: WorkspaceEditorPanelProps) {
74
+ const filesByPath = useMemo(() => new Map(files.map((file) => [file.path, file])), [files]);
75
+ const openFiles = openPaths
76
+ .map((path) => filesByPath.get(path))
77
+ .filter((file): file is WorkspaceFile => Boolean(file));
78
+ const selectedFile = selectedPath ? filesByPath.get(selectedPath) : openFiles[0];
79
+ const [deletePath, setDeletePath] = useState<string | null>(null);
80
+ const [draftsByPath, setDraftsByPath] = useState<WorkspaceFileDraftMap>({});
81
+ const [tabContextMenu, setTabContextMenu] = useState<{
82
+ path: string | null;
83
+ x: number;
84
+ y: number;
85
+ } | null>(null);
86
+ const selectedDraft = selectedFile
87
+ ? resolveWorkspaceFileDraft({
88
+ draft: draftsByPath[selectedFile.path],
89
+ file: selectedFile,
90
+ })
91
+ : null;
92
+ const selectedContent = selectedDraft?.content ?? '';
93
+
94
+ const updateDraft = (path: string, content: string) => {
95
+ setDraftsByPath((currentDrafts) =>
96
+ updateWorkspaceFileDraft({
97
+ content,
98
+ drafts: currentDrafts,
99
+ fileContent: filesByPath.get(path)?.content ?? '',
100
+ path,
101
+ }),
102
+ );
103
+ };
104
+
105
+ const saveFile = (path: string, content: string) => {
106
+ Promise.resolve(onSaveFile?.(path, content, filesByPath.get(path)?.updatedAt))
107
+ .then((result) => {
108
+ if (!result || isSaveSuccess(result)) {
109
+ setDraftsByPath((currentDrafts) =>
110
+ saveWorkspaceFileDraft({
111
+ content,
112
+ drafts: currentDrafts,
113
+ path,
114
+ }),
115
+ );
116
+ }
117
+ })
118
+ .catch(() => {
119
+ // keep draft intact when persistence fails
120
+ });
121
+ };
122
+
123
+ const discardFile = (file: WorkspaceFile) => {
124
+ setDraftsByPath((currentDrafts) =>
125
+ discardWorkspaceFileDraft({
126
+ drafts: currentDrafts,
127
+ file,
128
+ }),
129
+ );
130
+ };
131
+
132
+ const handleTabContextMenu = (event: MouseEvent<HTMLElement>, path: string | null) => {
133
+ event.preventDefault();
134
+ event.stopPropagation();
135
+ setTabContextMenu({ path, x: event.clientX, y: event.clientY });
136
+ };
137
+
138
+ const createEditorCommandContext = (
139
+ file: WorkspaceFile | undefined,
140
+ ): WorkbenchEditorCommandContext => {
141
+ const filePath = file?.path;
142
+ const draft = file
143
+ ? resolveWorkspaceFileDraft({
144
+ draft: draftsByPath[file.path],
145
+ file,
146
+ })
147
+ : undefined;
148
+ const content = draft?.content ?? '';
149
+
150
+ return {
151
+ canCloseAll: Boolean(onCloseAll),
152
+ canCloseOthers: Boolean(onCloseOthers),
153
+ canClosePath: Boolean(onClosePath),
154
+ canCopyPath: Boolean(onCopyPath),
155
+ canDeletePath: Boolean(onDeletePath),
156
+ canDiscardFile: Boolean(filePath),
157
+ canSaveFile: Boolean(filePath),
158
+ closeAll: () => onCloseAll?.(),
159
+ closeOthers: () => {
160
+ if (filePath) onCloseOthers?.(filePath);
161
+ },
162
+ closePath: () => {
163
+ if (filePath) onClosePath?.(filePath);
164
+ },
165
+ copyPath: () => {
166
+ if (filePath) onCopyPath?.(filePath);
167
+ },
168
+ deletePath: () => {
169
+ if (filePath) setDeletePath(filePath);
170
+ },
171
+ discardFile: () => {
172
+ if (file) discardFile(file);
173
+ },
174
+ filePath,
175
+ hasMultipleOpenFiles: openFiles.length > 1,
176
+ hasOpenFiles: openFiles.length > 0,
177
+ hasUnsavedChanges: Boolean(file && content !== file.content),
178
+ saveFile: () => {
179
+ if (filePath) saveFile(filePath, content);
180
+ },
181
+ };
182
+ };
183
+
184
+ const createTabContextItems = (path: string | null): ContextMenuItem[] => {
185
+ const file = path ? filesByPath.get(path) : undefined;
186
+ const context = createEditorCommandContext(file);
187
+
188
+ return commandMenuItemsToContextMenuItems(
189
+ resolveCommandMenuItems({
190
+ context,
191
+ surface: WORKBENCH_COMMAND_SURFACE_EDITOR,
192
+ entries: file ? editorTabMenuEntries : editorTabListMenuEntries,
193
+ registry: editorCommandRegistry,
194
+ }),
195
+ (commandId) => executeCommand(editorCommandRegistry, commandId, context),
196
+ );
197
+ };
198
+
199
+ const handleConfirmDelete = () => {
200
+ if (deletePath) {
201
+ onDeletePath?.(deletePath);
202
+ }
203
+ setDeletePath(null);
204
+ };
205
+ const selectedCommandContext = createEditorCommandContext(selectedFile);
206
+
207
+ return (
208
+ <Panel className="workspace-panel">
209
+ <PanelBody className="workspace-panel__body">
210
+ {openFiles.length > 0 ? (
211
+ <div className="workspace-editor">
212
+ <div className="workspace-editor__tabbar">
213
+ <div
214
+ aria-label="Open workspace files"
215
+ className="workspace-editor__tabs"
216
+ role="tablist"
217
+ onContextMenu={(event) => handleTabContextMenu(event, null)}
218
+ >
219
+ {openFiles.map((file) => {
220
+ const isActive = file.path === selectedFile?.path;
221
+ const draft = resolveWorkspaceFileDraft({
222
+ draft: draftsByPath[file.path],
223
+ file,
224
+ });
225
+ const isDirty = draft.content !== file.content;
226
+
227
+ return (
228
+ <div
229
+ key={file.path}
230
+ className={`workspace-editor__tab${isActive ? ' workspace-editor__tab--active' : ''}`}
231
+ title={file.path}
232
+ onContextMenu={(event) => handleTabContextMenu(event, file.path)}
233
+ >
234
+ <button
235
+ aria-selected={isActive}
236
+ className="workspace-editor__tab-button"
237
+ role="tab"
238
+ type="button"
239
+ onClick={() => onSelectedPathChange(file.path)}
240
+ >
241
+ <WorkspaceFileIcon mimeType={file.mimeType} path={file.path} />
242
+ <span className="workspace-editor__path">{fileNameOfPath(file.path)}</span>
243
+ {isDirty ? (
244
+ <span className="workspace-editor__dirty" aria-label="Unsaved changes" />
245
+ ) : null}
246
+ </button>
247
+ <IconButton
248
+ className="workspace-editor__tab-close"
249
+ icon="codicon-close"
250
+ label={`Close ${fileNameOfPath(file.path)}`}
251
+ onClick={() => onClosePath?.(file.path)}
252
+ />
253
+ </div>
254
+ );
255
+ })}
256
+ </div>
257
+
258
+ {selectedFile ? (
259
+ <Toolbar className="workspace-editor__actions">
260
+ <IconButton
261
+ disabled={
262
+ !canExecuteCommand(
263
+ editorCommandRegistry,
264
+ WORKBENCH_EDITOR_SAVE_COMMAND_ID,
265
+ selectedCommandContext,
266
+ )
267
+ }
268
+ icon="codicon-save"
269
+ label="Save"
270
+ onClick={() =>
271
+ executeCommand(
272
+ editorCommandRegistry,
273
+ WORKBENCH_EDITOR_SAVE_COMMAND_ID,
274
+ selectedCommandContext,
275
+ )
276
+ }
277
+ />
278
+ <IconButton
279
+ disabled={
280
+ !canExecuteCommand(
281
+ editorCommandRegistry,
282
+ WORKBENCH_EDITOR_DISCARD_CHANGES_COMMAND_ID,
283
+ selectedCommandContext,
284
+ )
285
+ }
286
+ icon="codicon-discard"
287
+ label="Discard changes"
288
+ onClick={() =>
289
+ executeCommand(
290
+ editorCommandRegistry,
291
+ WORKBENCH_EDITOR_DISCARD_CHANGES_COMMAND_ID,
292
+ selectedCommandContext,
293
+ )
294
+ }
295
+ />
296
+ <IconButton
297
+ disabled={!onDeletePath}
298
+ icon="codicon-trash"
299
+ label="Delete"
300
+ variant="danger"
301
+ onClick={() => setDeletePath(selectedFile.path)}
302
+ />
303
+ </Toolbar>
304
+ ) : null}
305
+ </div>
306
+
307
+ {selectedFile ? (
308
+ <WorkspaceEditor
309
+ key={selectedFile.path}
310
+ file={selectedFile}
311
+ readOnly={false}
312
+ showHeader={false}
313
+ theme={theme}
314
+ value={selectedContent}
315
+ onChange={(content) => updateDraft(selectedFile.path, content)}
316
+ onSave={(content) => saveFile(selectedFile.path, content)}
317
+ />
318
+ ) : (
319
+ <EmptyState icon="codicon-open-preview">{emptyLabel}</EmptyState>
320
+ )}
321
+ </div>
322
+ ) : files.length === 0 ? (
323
+ <EmptyState icon="codicon-folder">No workspace files</EmptyState>
324
+ ) : (
325
+ <EmptyState icon="codicon-open-preview">{emptyLabel}</EmptyState>
326
+ )}
327
+ </PanelBody>
328
+
329
+ {deletePath ? (
330
+ <ConfirmDialog
331
+ closeLabel="Close confirmation"
332
+ confirmLabel="Delete"
333
+ detail={<code>{deletePath}</code>}
334
+ message="Delete this workspace file?"
335
+ title="Delete File"
336
+ variant="danger"
337
+ onCancel={() => setDeletePath(null)}
338
+ onConfirm={handleConfirmDelete}
339
+ />
340
+ ) : null}
341
+
342
+ {tabContextMenu ? (
343
+ <ContextMenu
344
+ ariaLabel="Editor tab menu"
345
+ items={createTabContextItems(tabContextMenu.path)}
346
+ x={tabContextMenu.x}
347
+ y={tabContextMenu.y}
348
+ onClose={() => setTabContextMenu(null)}
349
+ />
350
+ ) : null}
351
+ </Panel>
352
+ );
353
+ }