@aprovan/patchwork-editor 0.1.2-dev.03aaf5b → 0.1.2-dev.ba8f277

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.
@@ -1,66 +1,123 @@
1
- import { useState } from 'react';
2
- import { ChevronDown, Server } from 'lucide-react';
1
+ import { useState } from "react";
2
+ import { ChevronDown, Server } from "lucide-react";
3
3
 
4
4
  export interface ServiceInfo {
5
5
  name: string;
6
6
  namespace: string;
7
7
  procedure: string;
8
8
  description: string;
9
- parameters: {
10
- jsonSchema: Record<string, unknown>;
11
- };
9
+ parameters?: Record<string, unknown>;
12
10
  }
13
11
 
14
12
  interface ServicesInspectorProps {
15
13
  namespaces: string[];
16
14
  services?: ServiceInfo[];
17
15
  /** Custom badge component for rendering the service count */
18
- BadgeComponent?: React.ComponentType<{ children: React.ReactNode; variant?: string; className?: string }>;
16
+ BadgeComponent?: React.ComponentType<{
17
+ children: React.ReactNode;
18
+ variant?: string;
19
+ className?: string;
20
+ }>;
19
21
  /** Custom collapsible components */
20
- CollapsibleComponent?: React.ComponentType<{ children: React.ReactNode; defaultOpen?: boolean; className?: string }>;
21
- CollapsibleTriggerComponent?: React.ComponentType<{ children: React.ReactNode; className?: string }>;
22
- CollapsibleContentComponent?: React.ComponentType<{ children: React.ReactNode }>;
22
+ CollapsibleComponent?: React.ComponentType<{
23
+ children: React.ReactNode;
24
+ defaultOpen?: boolean;
25
+ className?: string;
26
+ }>;
27
+ CollapsibleTriggerComponent?: React.ComponentType<{
28
+ children: React.ReactNode;
29
+ className?: string;
30
+ }>;
31
+ CollapsibleContentComponent?: React.ComponentType<{
32
+ children: React.ReactNode;
33
+ }>;
23
34
  /** Custom dialog components */
24
- DialogComponent?: React.ComponentType<{ children: React.ReactNode; open?: boolean; onOpenChange?: (open: boolean) => void }>;
35
+ DialogComponent?: React.ComponentType<{
36
+ children: React.ReactNode;
37
+ open?: boolean;
38
+ onOpenChange?: (open: boolean) => void;
39
+ }>;
25
40
  DialogHeaderComponent?: React.ComponentType<{ children: React.ReactNode }>;
26
- DialogContentComponent?: React.ComponentType<{ children: React.ReactNode }>;
41
+ DialogContentComponent?: React.ComponentType<{
42
+ children: React.ReactNode;
43
+ className?: string;
44
+ }>;
27
45
  DialogCloseComponent?: React.ComponentType<{ onClose?: () => void }>;
28
46
  }
29
47
 
30
48
  // Fallback components for when custom UI is not provided
31
- function DefaultBadge({ children, className = '' }: { children: React.ReactNode; className?: string }) {
49
+ function DefaultBadge({
50
+ children,
51
+ className = "",
52
+ }: {
53
+ children: React.ReactNode;
54
+ className?: string;
55
+ }) {
32
56
  return (
33
- <span className={`inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 ${className}`}>
57
+ <span
58
+ className={`inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 ${className}`}
59
+ >
34
60
  {children}
35
61
  </span>
36
62
  );
37
63
  }
38
64
 
39
- function DefaultDialog({ children, open, onOpenChange }: { children: React.ReactNode; open?: boolean; onOpenChange?: (open: boolean) => void }) {
65
+ function DefaultDialog({
66
+ children,
67
+ open,
68
+ onOpenChange,
69
+ }: {
70
+ children: React.ReactNode;
71
+ open?: boolean;
72
+ onOpenChange?: (open: boolean) => void;
73
+ }) {
40
74
  if (!open) return null;
41
75
  return (
42
- <div className="fixed inset-0 z-50 bg-black/50" onClick={() => onOpenChange?.(false)}>
43
- <div className="fixed left-1/2 top-1/2 z-50 w-full max-w-lg -translate-x-1/2 -translate-y-1/2 bg-background p-6 shadow-lg rounded-lg" onClick={e => e.stopPropagation()}>
76
+ <div
77
+ className="fixed inset-0 z-50 bg-black/50"
78
+ onClick={() => onOpenChange?.(false)}
79
+ >
80
+ <div
81
+ className="fixed left-1/2 top-1/2 z-50 w-full max-w-lg -translate-x-1/2 -translate-y-1/2 bg-background p-6 shadow-lg rounded-lg"
82
+ onClick={(e) => e.stopPropagation()}
83
+ >
44
84
  {children}
45
85
  </div>
46
86
  </div>
47
87
  );
48
88
  }
49
89
 
50
- export function ServicesInspector({
51
- namespaces,
90
+ export function ServicesInspector({
91
+ namespaces,
52
92
  services = [],
53
93
  BadgeComponent = DefaultBadge,
54
94
  DialogComponent = DefaultDialog,
95
+ DialogHeaderComponent = ({ children }) => (
96
+ <div className="flex justify-between items-center mb-4">{children}</div>
97
+ ),
98
+ DialogContentComponent = ({ children, className = "" }) => (
99
+ <div className={className}>{children}</div>
100
+ ),
101
+ DialogCloseComponent = ({ onClose }) => (
102
+ <button
103
+ onClick={() => onClose?.()}
104
+ className="text-muted-foreground hover:text-foreground"
105
+ >
106
+ ×
107
+ </button>
108
+ ),
55
109
  }: ServicesInspectorProps) {
56
110
  const [open, setOpen] = useState(false);
57
111
 
58
112
  if (namespaces.length === 0) return null;
59
113
 
60
- const groupedServices = services.reduce<Record<string, ServiceInfo[]>>((acc, svc) => {
61
- (acc[svc.namespace] ??= []).push(svc);
62
- return acc;
63
- }, {});
114
+ const groupedServices = services.reduce<Record<string, ServiceInfo[]>>(
115
+ (acc, svc) => {
116
+ (acc[svc.namespace] ??= []).push(svc);
117
+ return acc;
118
+ },
119
+ {},
120
+ );
64
121
 
65
122
  return (
66
123
  <>
@@ -70,16 +127,16 @@ export function ServicesInspector({
70
127
  >
71
128
  <Server className="h-4 w-4 text-muted-foreground" />
72
129
  <BadgeComponent className="text-xs">
73
- {namespaces.length} service{namespaces.length !== 1 ? 's' : ''}
130
+ {namespaces.length} service{namespaces.length !== 1 ? "s" : ""}
74
131
  </BadgeComponent>
75
132
  </button>
76
133
 
77
134
  <DialogComponent open={open} onOpenChange={setOpen}>
78
- <div className="flex justify-between items-center mb-4">
135
+ <DialogHeaderComponent>
79
136
  <h2 className="text-lg font-semibold">Available Services</h2>
80
- <button onClick={() => setOpen(false)} className="text-muted-foreground hover:text-foreground">×</button>
81
- </div>
82
- <div className="space-y-3 max-h-96 overflow-auto">
137
+ <DialogCloseComponent onClose={() => setOpen(false)} />
138
+ </DialogHeaderComponent>
139
+ <DialogContentComponent className="space-y-3 max-h-96 overflow-auto">
83
140
  {namespaces.map((ns) => (
84
141
  <details key={ns} open={namespaces.length === 1}>
85
142
  <summary className="flex items-center gap-2 w-full p-2 rounded bg-muted/50 hover:bg-muted transition-colors cursor-pointer">
@@ -87,7 +144,8 @@ export function ServicesInspector({
87
144
  <span className="font-medium text-sm">{ns}</span>
88
145
  {groupedServices[ns] && (
89
146
  <BadgeComponent className="ml-auto text-xs">
90
- {groupedServices[ns].length} tool{groupedServices[ns].length !== 1 ? 's' : ''}
147
+ {groupedServices[ns].length} tool
148
+ {groupedServices[ns].length !== 1 ? "s" : ""}
91
149
  </BadgeComponent>
92
150
  )}
93
151
  </summary>
@@ -95,23 +153,29 @@ export function ServicesInspector({
95
153
  {groupedServices[ns]?.map((svc) => (
96
154
  <details key={svc.name}>
97
155
  <summary className="flex items-center gap-2 w-full text-left text-sm hover:text-foreground text-muted-foreground transition-colors cursor-pointer">
98
- <ChevronDown className="h-3 w-3" />
156
+ {svc.parameters && <ChevronDown className="h-3 w-3" />}
99
157
  <code className="font-mono text-xs">{svc.procedure}</code>
100
- <span className="truncate text-xs opacity-70">{svc.description}</span>
158
+ <span className="truncate text-xs opacity-70">
159
+ {svc.description}
160
+ </span>
101
161
  </summary>
102
- <div className="ml-5 mt-1 p-2 rounded border bg-muted/30 overflow-auto max-h-48">
103
- <pre className="text-xs font-mono whitespace-pre-wrap break-words m-0">
104
- {JSON.stringify(svc.parameters.jsonSchema, null, 2)}
105
- </pre>
106
- </div>
162
+ {svc.parameters && (
163
+ <div className="ml-5 mt-1 p-2 rounded border bg-muted/30 overflow-auto max-h-48">
164
+ <pre className="text-xs font-mono whitespace-pre-wrap break-words m-0">
165
+ {JSON.stringify(svc.parameters, null, 2)}
166
+ </pre>
167
+ </div>
168
+ )}
107
169
  </details>
108
170
  )) ?? (
109
- <p className="text-xs text-muted-foreground">No tool details available</p>
171
+ <p className="text-xs text-muted-foreground">
172
+ No tool details available
173
+ </p>
110
174
  )}
111
175
  </div>
112
176
  </details>
113
177
  ))}
114
- </div>
178
+ </DialogContentComponent>
115
179
  </DialogComponent>
116
180
  </>
117
181
  );
@@ -0,0 +1,102 @@
1
+ import { useEffect, useRef, useState } from 'react';
2
+ import { AlertCircle, Loader2 } from 'lucide-react';
3
+ import type { Compiler, Manifest, MountedWidget } from '@aprovan/patchwork-compiler';
4
+
5
+ export interface WidgetPreviewProps {
6
+ code: string;
7
+ compiler: Compiler | null;
8
+ services?: string[];
9
+ enabled?: boolean;
10
+ }
11
+
12
+ function createManifest(services?: string[]): Manifest {
13
+ return {
14
+ name: 'preview',
15
+ version: '1.0.0',
16
+ platform: 'browser',
17
+ image: '@aprovan/patchwork-image-shadcn',
18
+ services,
19
+ };
20
+ }
21
+
22
+ export function WidgetPreview({
23
+ code,
24
+ compiler,
25
+ services,
26
+ enabled = true,
27
+ }: WidgetPreviewProps) {
28
+ const [loading, setLoading] = useState(false);
29
+ const [error, setError] = useState<string | null>(null);
30
+ const containerRef = useRef<HTMLDivElement>(null);
31
+ const mountedRef = useRef<MountedWidget | null>(null);
32
+
33
+ useEffect(() => {
34
+ if (!enabled || !compiler || !containerRef.current) return;
35
+
36
+ let cancelled = false;
37
+
38
+ const compileAndMount = async () => {
39
+ setLoading(true);
40
+ setError(null);
41
+
42
+ try {
43
+ if (mountedRef.current) {
44
+ compiler.unmount(mountedRef.current);
45
+ mountedRef.current = null;
46
+ }
47
+
48
+ const widget = await compiler.compile(code, createManifest(services), {
49
+ typescript: true,
50
+ });
51
+
52
+ if (cancelled || !containerRef.current) return;
53
+
54
+ const mounted = await compiler.mount(widget, {
55
+ target: containerRef.current,
56
+ mode: 'embedded',
57
+ });
58
+
59
+ mountedRef.current = mounted;
60
+ } catch (err) {
61
+ if (!cancelled) {
62
+ setError(err instanceof Error ? err.message : 'Failed to render preview');
63
+ }
64
+ } finally {
65
+ if (!cancelled) {
66
+ setLoading(false);
67
+ }
68
+ }
69
+ };
70
+
71
+ void compileAndMount();
72
+
73
+ return () => {
74
+ cancelled = true;
75
+ if (mountedRef.current && compiler) {
76
+ compiler.unmount(mountedRef.current);
77
+ mountedRef.current = null;
78
+ }
79
+ };
80
+ }, [code, compiler, enabled, services]);
81
+
82
+ return (
83
+ <>
84
+ {error && (
85
+ <div className="text-sm text-destructive flex items-center gap-2 p-3">
86
+ <AlertCircle className="h-4 w-4 shrink-0" />
87
+ <span>{error}</span>
88
+ </div>
89
+ )}
90
+ {loading && (
91
+ <div className="p-3 flex items-center gap-2 text-muted-foreground">
92
+ <Loader2 className="h-4 w-4 animate-spin" />
93
+ <span className="text-sm">Rendering preview...</span>
94
+ </div>
95
+ )}
96
+ {!compiler && enabled && !loading && !error && (
97
+ <div className="p-3 text-sm text-muted-foreground">Compiler not initialized</div>
98
+ )}
99
+ <div ref={containerRef} className="w-full" />
100
+ </>
101
+ );
102
+ }
@@ -1,4 +1,4 @@
1
- import { useState, useCallback, useMemo, useRef, type ReactNode } from 'react';
1
+ import { useState, useCallback, useMemo, useRef, useEffect, type ReactNode } from 'react';
2
2
  import {
3
3
  Code,
4
4
  Eye,
@@ -10,9 +10,10 @@ import {
10
10
  Send,
11
11
  FolderTree,
12
12
  FileCode,
13
- Save,
14
13
  } from 'lucide-react';
15
14
  import { MarkdownEditor } from '../MarkdownEditor';
15
+ import { MarkdownPreview } from '../MarkdownPreview';
16
+ import { SaveStatusButton, type SaveStatus } from '../SaveStatusButton';
16
17
  import { EditHistory } from './EditHistory';
17
18
  import { FileTree } from './FileTree';
18
19
  import { SaveConfirmDialog } from './SaveConfirmDialog';
@@ -20,7 +21,7 @@ import { CodeBlockView } from './CodeBlockView';
20
21
  import { MediaPreview } from './MediaPreview';
21
22
  import { useEditSession, type UseEditSessionOptions } from './useEditSession';
22
23
  import { getActiveContent, getFiles } from './types';
23
- import { getFileType, isCompilable, getMimeType } from './fileTypes';
24
+ import { getFileType, isCompilable, isMarkdownFile, getMimeType } from './fileTypes';
24
25
  import { Bobbin, serializeChangesToYAML, type Change } from '@aprovan/bobbin';
25
26
  import type { VirtualProject } from '@aprovan/patchwork-compiler';
26
27
 
@@ -36,6 +37,7 @@ function hashCode(str: string): number {
36
37
 
37
38
  export interface EditModalProps extends UseEditSessionOptions {
38
39
  isOpen: boolean;
40
+ initialTreePath?: string;
39
41
  initialState?: Partial<{
40
42
  showTree: boolean;
41
43
  showPreview: boolean;
@@ -61,6 +63,7 @@ export function EditModal({
61
63
  renderError,
62
64
  previewError,
63
65
  previewLoading,
66
+ initialTreePath,
64
67
  initialState = {},
65
68
  hideFileTree = false,
66
69
  ...sessionOptions
@@ -75,19 +78,33 @@ export function EditModal({
75
78
  const [pillContainer, setPillContainer] = useState<HTMLDivElement | null>(null);
76
79
  const [showConfirm, setShowConfirm] = useState(false);
77
80
  const [isSaving, setIsSaving] = useState(false);
81
+ const [saveStatus, setSaveStatus] = useState<SaveStatus>('saved');
82
+ const [lastSavedSnapshot, setLastSavedSnapshot] = useState('');
78
83
  const [saveError, setSaveError] = useState<string | null>(null);
79
84
  const [pendingClose, setPendingClose] = useState<{ code: string; count: number } | null>(null);
85
+ const [treePath, setTreePath] = useState(initialTreePath ?? '');
86
+ const wasOpenRef = useRef(false);
80
87
  const currentCodeRef = useRef<string>('');
81
88
 
82
89
  const session = useEditSession(sessionOptions);
83
90
  const code = getActiveContent(session);
91
+ const effectiveTreePath = treePath || session.activeFile;
84
92
  currentCodeRef.current = code;
85
93
  const files = useMemo(() => getFiles(session.project), [session.project]);
94
+ const projectSnapshot = useMemo(
95
+ () =>
96
+ Array.from(session.project.files.entries())
97
+ .sort(([a], [b]) => a.localeCompare(b))
98
+ .map(([path, file]) => `${path}\u0000${file.content}`)
99
+ .join('\u0001'),
100
+ [session.project]
101
+ );
86
102
  const hasChanges = code !== (session.originalProject.files.get(session.activeFile)?.content ?? '');
87
103
 
88
104
  const fileType = useMemo(() => getFileType(session.activeFile), [session.activeFile]);
89
105
  const isCompilableFile = isCompilable(session.activeFile);
90
- const showPreviewToggle = isCompilableFile;
106
+ const isMarkdown = isMarkdownFile(session.activeFile);
107
+ const showPreviewToggle = isCompilableFile || isMarkdown;
91
108
 
92
109
  const handleBobbinChanges = useCallback((changes: Change[]) => {
93
110
  setBobbinChanges(changes);
@@ -111,6 +128,28 @@ export function EditModal({
111
128
 
112
129
  const hasSaveHandler = onSave || onSaveProject;
113
130
 
131
+ useEffect(() => {
132
+ if (isOpen && !wasOpenRef.current) {
133
+ setLastSavedSnapshot(projectSnapshot);
134
+ setSaveStatus('saved');
135
+ setSaveError(null);
136
+ }
137
+ wasOpenRef.current = isOpen;
138
+ }, [isOpen, projectSnapshot]);
139
+
140
+ useEffect(() => {
141
+ if (!hasSaveHandler) return;
142
+ if (projectSnapshot === lastSavedSnapshot) {
143
+ if (saveStatus !== 'saving' && saveStatus !== 'saved') {
144
+ setSaveStatus('saved');
145
+ }
146
+ return;
147
+ }
148
+ if (saveStatus === 'saved' || saveStatus === 'error') {
149
+ setSaveStatus('unsaved');
150
+ }
151
+ }, [projectSnapshot, lastSavedSnapshot, saveStatus, hasSaveHandler]);
152
+
114
153
  const handleClose = useCallback(() => {
115
154
  const editCount = session.history.length;
116
155
  const finalCode = code;
@@ -129,6 +168,7 @@ export function EditModal({
129
168
  const handleSaveAndClose = useCallback(async () => {
130
169
  if (!pendingClose || !hasSaveHandler) return;
131
170
  setIsSaving(true);
171
+ setSaveStatus('saving');
132
172
  setSaveError(null);
133
173
  try {
134
174
  if (onSaveProject) {
@@ -136,6 +176,8 @@ export function EditModal({
136
176
  } else if (onSave) {
137
177
  await onSave(pendingClose.code);
138
178
  }
179
+ setLastSavedSnapshot(projectSnapshot);
180
+ setSaveStatus('saved');
139
181
  setShowConfirm(false);
140
182
  setEditInput('');
141
183
  session.clearError();
@@ -143,10 +185,11 @@ export function EditModal({
143
185
  setPendingClose(null);
144
186
  } catch (e) {
145
187
  setSaveError(e instanceof Error ? e.message : 'Save failed');
188
+ setSaveStatus('error');
146
189
  } finally {
147
190
  setIsSaving(false);
148
191
  }
149
- }, [pendingClose, onSave, onSaveProject, session, onClose]);
192
+ }, [pendingClose, onSave, onSaveProject, session, onClose, projectSnapshot, hasSaveHandler]);
150
193
 
151
194
  const handleDiscard = useCallback(() => {
152
195
  if (!pendingClose) return;
@@ -166,6 +209,7 @@ export function EditModal({
166
209
  const handleDirectSave = useCallback(async () => {
167
210
  if (!hasSaveHandler) return;
168
211
  setIsSaving(true);
212
+ setSaveStatus('saving');
169
213
  setSaveError(null);
170
214
  try {
171
215
  if (onSaveProject) {
@@ -173,12 +217,15 @@ export function EditModal({
173
217
  } else if (onSave && currentCodeRef.current) {
174
218
  await onSave(currentCodeRef.current);
175
219
  }
220
+ setLastSavedSnapshot(projectSnapshot);
221
+ setSaveStatus('saved');
176
222
  } catch (e) {
177
223
  setSaveError(e instanceof Error ? e.message : 'Save failed');
224
+ setSaveStatus('error');
178
225
  } finally {
179
226
  setIsSaving(false);
180
227
  }
181
- }, [onSave, onSaveProject, session.project]);
228
+ }, [onSave, onSaveProject, session.project, hasSaveHandler, projectSnapshot]);
182
229
 
183
230
  if (!isOpen) return null;
184
231
 
@@ -213,30 +260,23 @@ export function EditModal({
213
260
  {showTree ? <FileCode className="h-3 w-3" /> : <FolderTree className="h-3 w-3" />}
214
261
  </button>
215
262
  )}
263
+ {hasSaveHandler && (
264
+ <SaveStatusButton
265
+ status={saveStatus}
266
+ onClick={handleDirectSave}
267
+ disabled={isSaving}
268
+ tone="primary"
269
+ />
270
+ )}
216
271
  {showPreviewToggle && (
217
272
  <button
218
273
  onClick={() => setShowPreview(!showPreview)}
219
- className={`px-2 py-1 text-xs rounded flex items-center gap-1 ${showPreview ? 'bg-primary text-primary-foreground' : 'hover:bg-primary/20 text-primary'}`}
274
+ className={`w-[5rem] px-2 py-1 text-xs rounded flex items-center gap-1 ${showPreview ? 'bg-primary text-primary-foreground' : 'hover:bg-primary/20 text-primary'}`}
220
275
  >
221
276
  {showPreview ? <Eye className="h-3 w-3" /> : <Code className="h-3 w-3" />}
222
277
  {showPreview ? 'Preview' : 'Code'}
223
278
  </button>
224
279
  )}
225
- {hasSaveHandler && (
226
- <button
227
- onClick={handleDirectSave}
228
- disabled={isSaving}
229
- className="px-2 py-1 text-xs rounded flex items-center gap-1 hover:bg-primary/20 text-primary disabled:opacity-50"
230
- title="Save changes"
231
- >
232
- {isSaving ? (
233
- <Loader2 className="h-3 w-3 animate-spin" />
234
- ) : (
235
- <Save className="h-3 w-3" />
236
- )}
237
- Save
238
- </button>
239
- )}
240
280
  <button
241
281
  onClick={handleClose}
242
282
  className="px-2 py-1 text-xs rounded flex items-center gap-1 bg-primary text-primary-foreground hover:bg-primary/90"
@@ -253,7 +293,12 @@ export function EditModal({
253
293
  <FileTree
254
294
  files={files}
255
295
  activeFile={session.activeFile}
256
- onSelectFile={session.setActiveFile}
296
+ activePath={effectiveTreePath}
297
+ onSelectFile={(path) => {
298
+ setTreePath(path);
299
+ session.setActiveFile(path);
300
+ }}
301
+ onSelectDirectory={(path) => setTreePath(path)}
257
302
  onReplaceFile={session.replaceFile}
258
303
  />
259
304
  )}
@@ -293,6 +338,14 @@ export function EditModal({
293
338
  editable
294
339
  onChange={session.updateActiveFile}
295
340
  />
341
+ ) : isMarkdown && showPreview ? (
342
+ <div className="p-4 prose prose-sm dark:prose-invert max-w-none h-full overflow-auto">
343
+ <MarkdownPreview
344
+ value={code}
345
+ editable
346
+ onChange={session.updateActiveFile}
347
+ />
348
+ </div>
296
349
  ) : fileType.category === 'text' ? (
297
350
  <CodeBlockView
298
351
  content={code}
@@ -306,10 +359,14 @@ export function EditModal({
306
359
  mimeType={getMimeType(session.activeFile)}
307
360
  fileName={session.activeFile.split('/').pop() ?? session.activeFile}
308
361
  />
362
+ // Default to code view for unknown types
309
363
  ) : (
310
- <div className="flex items-center justify-center h-full text-muted-foreground">
311
- <p className="text-sm">Preview not available for this file type</p>
312
- </div>
364
+ <CodeBlockView
365
+ content={code}
366
+ language={fileType.language}
367
+ editable
368
+ onChange={session.updateActiveFile}
369
+ />
313
370
  )}
314
371
  </div>
315
372
  </div>