@lvce-editor/main-area-worker 1.13.0 → 4.0.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.
@@ -6,42 +6,68 @@ const create$7 = () => {
6
6
  const states = Object.create(null);
7
7
  const commandMapRef = {};
8
8
  return {
9
- get(uid) {
10
- return states[uid];
9
+ clear() {
10
+ for (const key of Object.keys(states)) {
11
+ delete states[key];
12
+ }
11
13
  },
12
- set(uid, oldState, newState) {
13
- states[uid] = {
14
- oldState,
15
- newState
16
- };
14
+ diff(uid, modules, numbers) {
15
+ const {
16
+ newState,
17
+ oldState
18
+ } = states[uid];
19
+ const diffResult = [];
20
+ for (let i = 0; i < modules.length; i++) {
21
+ const fn = modules[i];
22
+ if (!fn(oldState, newState)) {
23
+ diffResult.push(numbers[i]);
24
+ }
25
+ }
26
+ return diffResult;
17
27
  },
18
28
  dispose(uid) {
19
29
  delete states[uid];
20
30
  },
31
+ get(uid) {
32
+ return states[uid];
33
+ },
34
+ getCommandIds() {
35
+ const keys = Object.keys(commandMapRef);
36
+ const ids = keys.map(toCommandId);
37
+ return ids;
38
+ },
21
39
  getKeys() {
22
40
  return Object.keys(states).map(key => {
23
41
  return Number.parseInt(key);
24
42
  });
25
43
  },
26
- clear() {
27
- for (const key of Object.keys(states)) {
28
- delete states[key];
29
- }
44
+ registerCommands(commandMap) {
45
+ Object.assign(commandMapRef, commandMap);
46
+ },
47
+ set(uid, oldState, newState) {
48
+ states[uid] = {
49
+ newState,
50
+ oldState
51
+ };
30
52
  },
31
53
  wrapCommand(fn) {
32
54
  const wrapped = async (uid, ...args) => {
33
55
  const {
34
- oldState,
35
- newState
56
+ newState,
57
+ oldState
36
58
  } = states[uid];
37
59
  const newerState = await fn(newState, ...args);
38
60
  if (oldState === newerState || newState === newerState) {
39
61
  return;
40
62
  }
41
- const latest = states[uid];
63
+ const latestOld = states[uid];
64
+ const latestNew = {
65
+ ...latestOld.newState,
66
+ ...newerState
67
+ };
42
68
  states[uid] = {
43
- oldState: latest.oldState,
44
- newState: newerState
69
+ newState: latestNew,
70
+ oldState: latestOld.oldState
45
71
  };
46
72
  };
47
73
  return wrapped;
@@ -54,28 +80,6 @@ const create$7 = () => {
54
80
  return fn(newState, ...args);
55
81
  };
56
82
  return wrapped;
57
- },
58
- diff(uid, modules, numbers) {
59
- const {
60
- oldState,
61
- newState
62
- } = states[uid];
63
- const diffResult = [];
64
- for (let i = 0; i < modules.length; i++) {
65
- const fn = modules[i];
66
- if (!fn(oldState, newState)) {
67
- diffResult.push(numbers[i]);
68
- }
69
- }
70
- return diffResult;
71
- },
72
- getCommandIds() {
73
- const keys = Object.keys(commandMapRef);
74
- const ids = keys.map(toCommandId);
75
- return ids;
76
- },
77
- registerCommands(commandMap) {
78
- Object.assign(commandMapRef, commandMap);
79
83
  }
80
84
  };
81
85
  };
@@ -106,13 +110,17 @@ const {
106
110
  const create$6 = (uid, uri, x, y, width, height, platform, assetDir) => {
107
111
  const state = {
108
112
  assetDir,
113
+ height,
109
114
  layout: {
110
115
  activeGroupId: undefined,
111
116
  direction: 'horizontal',
112
117
  groups: []
113
118
  },
114
119
  platform,
115
- uid
120
+ uid,
121
+ width,
122
+ x,
123
+ y
116
124
  };
117
125
  set$3(uid, state, state);
118
126
  };
@@ -179,95 +187,6 @@ const handleClick = async (state, name) => {
179
187
  return state;
180
188
  };
181
189
 
182
- const closeTab = (state, groupId, tabId) => {
183
- const {
184
- layout
185
- } = state;
186
- const {
187
- activeGroupId,
188
- groups
189
- } = layout;
190
- const newGroups = groups.map(group => {
191
- if (group.id === groupId) {
192
- const newTabs = group.tabs.filter(tab => tab.id !== tabId);
193
- let newActiveTabId = group.activeTabId;
194
- if (group.activeTabId === tabId) {
195
- const tabIndex = group.tabs.findIndex(tab => tab.id === tabId);
196
- if (newTabs.length > 0) {
197
- newActiveTabId = newTabs[Math.min(tabIndex, newTabs.length - 1)].id;
198
- } else {
199
- newActiveTabId = undefined;
200
- }
201
- }
202
- return {
203
- ...group,
204
- activeTabId: newActiveTabId,
205
- tabs: newTabs
206
- };
207
- }
208
- return group;
209
- });
210
-
211
- // If the group has no tabs left and there are other groups, remove the group
212
- const groupWithNoTabs = newGroups.find(group => group.id === groupId && group.tabs.length === 0);
213
- if (groupWithNoTabs && newGroups.length > 1) {
214
- const remainingGroups = newGroups.filter(group => group.id !== groupId);
215
- const redistributedGroups = remainingGroups.map(group => ({
216
- ...group,
217
- size: Math.round(100 / remainingGroups.length)
218
- }));
219
- const newActiveGroupId = activeGroupId === groupId ? remainingGroups[0]?.id : activeGroupId;
220
- return {
221
- ...state,
222
- layout: {
223
- ...layout,
224
- activeGroupId: newActiveGroupId,
225
- groups: redistributedGroups
226
- }
227
- };
228
- }
229
- return {
230
- ...state,
231
- layout: {
232
- ...layout,
233
- groups: newGroups
234
- }
235
- };
236
- };
237
-
238
- const handleClickCloseTab = (state, rawGroupIndex, rawIndex) => {
239
- if (!rawGroupIndex || !rawIndex) {
240
- return state;
241
- }
242
- const groupIndex = Number.parseInt(rawGroupIndex);
243
- const index = Number.parseInt(rawIndex);
244
- const {
245
- layout
246
- } = state;
247
- const {
248
- groups
249
- } = layout;
250
-
251
- // Validate indexes
252
- if (groupIndex < 0 || groupIndex >= groups.length) {
253
- return state;
254
- }
255
- const group = groups[groupIndex];
256
- if (index < 0 || index >= group.tabs.length) {
257
- return state;
258
- }
259
- const tab = group.tabs[index];
260
- const groupId = group.id;
261
- const tabId = tab.id;
262
- return closeTab(state, groupId, tabId);
263
- };
264
-
265
- // Counter for request IDs to handle race conditions
266
- let requestIdCounter = 0;
267
- const getNextRequestId = () => {
268
- return ++requestIdCounter;
269
- };
270
-
271
190
  const normalizeLine = line => {
272
191
  if (line.startsWith('Error: ')) {
273
192
  return line.slice('Error: '.length);
@@ -726,7 +645,7 @@ const getFirstEvent = (eventEmitter, eventMap) => {
726
645
  return promise;
727
646
  };
728
647
  const Message$1 = 3;
729
- const create$5 = async ({
648
+ const create$5$1 = async ({
730
649
  isMessagePortOpen,
731
650
  messagePort
732
651
  }) => {
@@ -777,7 +696,7 @@ const wrap$5 = messagePort => {
777
696
  };
778
697
  const IpcParentWithMessagePort$1 = {
779
698
  __proto__: null,
780
- create: create$5,
699
+ create: create$5$1,
781
700
  signal: signal$1,
782
701
  wrap: wrap$5
783
702
  };
@@ -983,7 +902,7 @@ const getErrorProperty = (error, prettyError) => {
983
902
  }
984
903
  };
985
904
  };
986
- const create$1$2 = (id, error) => {
905
+ const create$1$1 = (id, error) => {
987
906
  return {
988
907
  jsonrpc: Two$1,
989
908
  id,
@@ -994,9 +913,9 @@ const getErrorResponse = (id, error, preparePrettyError, logError) => {
994
913
  const prettyError = preparePrettyError(error);
995
914
  logError(error, prettyError);
996
915
  const errorProperty = getErrorProperty(error, prettyError);
997
- return create$1$2(id, errorProperty);
916
+ return create$1$1(id, errorProperty);
998
917
  };
999
- const create$3 = (message, result) => {
918
+ const create$4 = (message, result) => {
1000
919
  return {
1001
920
  jsonrpc: Two$1,
1002
921
  id: message.id,
@@ -1005,7 +924,7 @@ const create$3 = (message, result) => {
1005
924
  };
1006
925
  const getSuccessResponse = (message, result) => {
1007
926
  const resultProperty = result ?? null;
1008
- return create$3(message, resultProperty);
927
+ return create$4(message, resultProperty);
1009
928
  };
1010
929
  const getErrorResponseSimple = (id, error) => {
1011
930
  return {
@@ -1119,14 +1038,14 @@ const execute = (command, ...args) => {
1119
1038
  };
1120
1039
 
1121
1040
  const Two = '2.0';
1122
- const create$s = (method, params) => {
1041
+ const create$t = (method, params) => {
1123
1042
  return {
1124
1043
  jsonrpc: Two,
1125
1044
  method,
1126
1045
  params
1127
1046
  };
1128
1047
  };
1129
- const create$r = (id, method, params) => {
1048
+ const create$s = (id, method, params) => {
1130
1049
  const message = {
1131
1050
  id,
1132
1051
  jsonrpc: Two,
@@ -1136,14 +1055,14 @@ const create$r = (id, method, params) => {
1136
1055
  return message;
1137
1056
  };
1138
1057
  let id$1 = 0;
1139
- const create$q = () => {
1058
+ const create$r = () => {
1140
1059
  return ++id$1;
1141
1060
  };
1142
1061
 
1143
1062
  /* eslint-disable n/no-unsupported-features/es-syntax */
1144
1063
 
1145
1064
  const registerPromise = map => {
1146
- const id = create$q();
1065
+ const id = create$r();
1147
1066
  const {
1148
1067
  promise,
1149
1068
  resolve
@@ -1161,7 +1080,7 @@ const invokeHelper = async (callbacks, ipc, method, params, useSendAndTransfer)
1161
1080
  id,
1162
1081
  promise
1163
1082
  } = registerPromise(callbacks);
1164
- const message = create$r(id, method, params);
1083
+ const message = create$s(id, method, params);
1165
1084
  if (useSendAndTransfer && ipc.sendAndTransfer) {
1166
1085
  ipc.sendAndTransfer(message);
1167
1086
  } else {
@@ -1197,7 +1116,7 @@ const createRpc = ipc => {
1197
1116
  * @deprecated
1198
1117
  */
1199
1118
  send(method, ...params) {
1200
- const message = create$s(method, params);
1119
+ const message = create$t(method, params);
1201
1120
  ipc.send(message);
1202
1121
  }
1203
1122
  };
@@ -1233,7 +1152,7 @@ const listen$1 = async (module, options) => {
1233
1152
  const ipc = module.wrap(rawIpc);
1234
1153
  return ipc;
1235
1154
  };
1236
- const create$4 = async ({
1155
+ const create$5 = async ({
1237
1156
  commandMap,
1238
1157
  isMessagePortOpen = true,
1239
1158
  messagePort
@@ -1250,7 +1169,7 @@ const create$4 = async ({
1250
1169
  messagePort.start();
1251
1170
  return rpc;
1252
1171
  };
1253
- const create$2 = async ({
1172
+ const create$3 = async ({
1254
1173
  commandMap,
1255
1174
  isMessagePortOpen,
1256
1175
  send
@@ -1260,7 +1179,7 @@ const create$2 = async ({
1260
1179
  port2
1261
1180
  } = new MessageChannel();
1262
1181
  await send(port1);
1263
- return create$4({
1182
+ return create$5({
1264
1183
  commandMap,
1265
1184
  isMessagePortOpen,
1266
1185
  messagePort: port2
@@ -1268,9 +1187,9 @@ const create$2 = async ({
1268
1187
  };
1269
1188
  const TransferMessagePortRpcParent = {
1270
1189
  __proto__: null,
1271
- create: create$2
1190
+ create: create$3
1272
1191
  };
1273
- const create$1$1 = async ({
1192
+ const create$2 = async ({
1274
1193
  commandMap
1275
1194
  }) => {
1276
1195
  // TODO create a commandMap per rpc instance
@@ -1282,7 +1201,7 @@ const create$1$1 = async ({
1282
1201
  };
1283
1202
  const WebWorkerRpcClient = {
1284
1203
  __proto__: null,
1285
- create: create$1$1
1204
+ create: create$2
1286
1205
  };
1287
1206
  const createMockRpc = ({
1288
1207
  commandMap
@@ -1374,6 +1293,23 @@ const sendMessagePortToExtensionHostWorker$1 = async (port, rpcId = 0) => {
1374
1293
  await invokeAndTransfer('SendMessagePortToExtensionHostWorker.sendMessagePortToExtensionHostWorker', port, command, rpcId);
1375
1294
  };
1376
1295
 
1296
+ const handleAttach = async command => {
1297
+ // TODO find a better way to append editors
1298
+ const parentNodeSelector = '.editor-groups-container';
1299
+ // @ts-ignore
1300
+ await invoke('Layout.attachViewlet', parentNodeSelector, command.instanceId);
1301
+ };
1302
+
1303
+ let counter = 0;
1304
+ const create = () => {
1305
+ return ++counter;
1306
+ };
1307
+ const setMinId = minId => {
1308
+ if (minId > counter) {
1309
+ counter = minId;
1310
+ }
1311
+ };
1312
+
1377
1313
  const updateTab = (state, tabId, updates) => {
1378
1314
  const {
1379
1315
  layout
@@ -1408,6 +1344,7 @@ const updateTab = (state, tabId, updates) => {
1408
1344
  }
1409
1345
  };
1410
1346
  };
1347
+
1411
1348
  const findTab = (state, tabId) => {
1412
1349
  const {
1413
1350
  layout
@@ -1440,8 +1377,12 @@ const loadTabContentAsync = async (tabId, path, requestId, getLatestState) => {
1440
1377
  if (!latestTab || latestTab.loadRequestId !== requestId) {
1441
1378
  return latestState;
1442
1379
  }
1380
+
1381
+ // Assign editorUid if tab doesn't have one yet
1382
+ const editorUid = latestTab.editorUid === -1 ? create() : latestTab.editorUid;
1443
1383
  return updateTab(latestState, tabId, {
1444
1384
  content,
1385
+ editorUid,
1445
1386
  errorMessage: undefined,
1446
1387
  loadingState: 'loaded'
1447
1388
  });
@@ -1461,86 +1402,327 @@ const loadTabContentAsync = async (tabId, path, requestId, getLatestState) => {
1461
1402
  }
1462
1403
  };
1463
1404
 
1464
- const startContentLoading = async (oldState, state, tabId, path, requestId) => {
1465
- try {
1466
- const getLatestState = () => {
1467
- return get$2(state.uid).newState;
1468
- };
1469
- set$3(state.uid, oldState, state);
1470
- const newState = await loadTabContentAsync(tabId, path, requestId, getLatestState);
1471
- return newState;
1472
- } catch {
1473
- // Silently ignore errors - the tab may have been closed or the component unmounted
1405
+ /**
1406
+ * SAFE: Create viewlet for a tab (no visible side effects)
1407
+ * Can be called eagerly when tab is opened - the viewlet loads in the background.
1408
+ */
1409
+ const createViewletForTab = (state, tabId, viewletModuleId, bounds) => {
1410
+ const tab = findTab(state, tabId);
1411
+ if (!tab) {
1412
+ return state;
1474
1413
  }
1475
- return state;
1476
- };
1477
1414
 
1478
- const shouldLoadContent = tab => {
1479
- // Load if:
1480
- // - Has a path (file-based tab)
1481
- // - Not already loaded or currently loading
1482
- if (!tab.path) {
1483
- return false;
1484
- }
1485
- if (tab.loadingState === 'loading') {
1486
- return false;
1487
- }
1488
- if (tab.loadingState === 'loaded' && tab.content) {
1489
- return false;
1415
+ // If tab already has an editorUid or is loading/loaded, don't recreate
1416
+ if (tab.editorUid !== -1 || tab.loadingState === 'loading' || tab.loadingState === 'loaded') {
1417
+ return state;
1490
1418
  }
1491
- return true;
1419
+ const editorUid = create();
1420
+ const newState = updateTab(state, tabId, {
1421
+ editorUid
1422
+ });
1423
+ return newState;
1492
1424
  };
1493
- const selectTab = async (state, groupIndex, index) => {
1425
+
1426
+ /**
1427
+ * Called when switching tabs.
1428
+ * With reference nodes, attachment/detachment is handled automatically by virtual DOM rendering.
1429
+ * No attach/detach commands needed - virtual DOM will render the correct reference at each position.
1430
+ */
1431
+ const switchViewlet = (state, fromTabId, toTabId) => {
1432
+ // No commands needed - virtual DOM reference nodes handle attachment automatically
1433
+ // Virtual DOM will:
1434
+ // 1. Remove the old reference node from the previous active tab
1435
+ // 2. Add the new reference node for the now-active tab
1436
+ // This achieves the same effect as detach/attach without explicit commands
1437
+
1438
+ return {
1439
+ commands: [],
1440
+ newState: state
1441
+ };
1442
+ };
1443
+
1444
+ const findTabByEditorUid = (state, editorUid) => {
1494
1445
  const {
1495
- layout,
1496
- uid
1446
+ layout
1497
1447
  } = state;
1498
1448
  const {
1499
1449
  groups
1500
1450
  } = layout;
1451
+ for (const group of groups) {
1452
+ const tab = group.tabs.find(t => t.editorUid === editorUid);
1453
+ if (tab) {
1454
+ return tab;
1455
+ }
1456
+ }
1457
+ return undefined;
1458
+ };
1501
1459
 
1502
- // Validate indexes
1503
- if (groupIndex < 0 || groupIndex >= groups.length) {
1504
- return state;
1460
+ /**
1461
+ * Called when renderer reports viewlet finished creating.
1462
+ * With reference nodes, attachment is handled automatically by virtual DOM rendering.
1463
+ * No commands needed - state update is sufficient.
1464
+ */
1465
+ const handleViewletReady = (state, editorUid) => {
1466
+ if (editorUid === -1) {
1467
+ throw new Error('Invalid editorUid -1');
1505
1468
  }
1506
- const group = groups[groupIndex];
1507
- if (index < 0 || index >= group.tabs.length) {
1469
+ // Find the tab by viewletRequestId
1470
+ const tab = findTabByEditorUid(state, editorUid);
1471
+ if (!tab) {
1472
+ // Tab was closed, dispose viewlet
1508
1473
  return state;
1509
1474
  }
1510
- const tab = group.tabs[index];
1511
- const groupId = group.id;
1512
- const tabId = tab.id;
1513
1475
 
1514
- // Return same state if this group and tab are already active
1515
- if (layout.activeGroupId === groupId && group.activeTabId === tabId) {
1516
- return state;
1517
- }
1476
+ // Mark viewlet as ready and set instance id
1477
+ // Reference nodes will handle rendering at the correct position automatically
1478
+ const newState = updateTab(state, tab.id, {
1479
+ editorUid,
1480
+ loadingState: 'loaded'
1481
+ });
1518
1482
 
1519
- // Check if we need to load content for the newly selected tab
1520
- const needsLoading = shouldLoadContent(tab);
1521
- const requestId = needsLoading ? getNextRequestId() : 0;
1483
+ // No attach commands needed - virtual DOM reference nodes handle positioning
1484
+ return newState;
1485
+ };
1522
1486
 
1523
- // Update the groups array with the new active tab and active group
1524
- // Also set loading state if needed
1525
- const updatedGroups = groups.map((g, i) => {
1526
- if (i !== groupIndex) {
1527
- return {
1528
- ...g,
1529
- focused: false
1530
- };
1531
- }
1487
+ const handleCreate = async command => {
1488
+ // Safe to call - no visible side effects
1489
+ // @ts-ignore
1490
+ const instanceId = Math.random(); // TODO try to find a better way to get consistent integer ids (thread safe)
1532
1491
 
1533
- // This is the group being selected
1534
- const updatedTabs = needsLoading ? g.tabs.map(t => {
1535
- if (t.id === tabId) {
1536
- return {
1537
- ...t,
1538
- errorMessage: undefined,
1539
- loadingState: 'loading',
1540
- loadRequestId: requestId
1541
- };
1542
- }
1543
- return t;
1492
+ await invoke('Layout.createViewlet', command.viewletModuleId, command.tabId, command.bounds, command.uri, instanceId);
1493
+
1494
+ // After viewlet is created, mark it as ready
1495
+ // Attachment is handled automatically by virtual DOM reference nodes
1496
+ const {
1497
+ newState: state,
1498
+ oldState
1499
+ } = get$2(command.uid);
1500
+ const readyState = handleViewletReady(state, command.editorUid);
1501
+ set$3(command.uid, oldState, readyState);
1502
+ return readyState;
1503
+ };
1504
+
1505
+ const handleDetach = async command => {
1506
+ // Hides viewlet but keeps it alive
1507
+ // @ts-ignore
1508
+ await invoke('Viewlet.detach', command.instanceId);
1509
+ };
1510
+
1511
+ const handleDispose = async command => {
1512
+ // Fully destroys viewlet
1513
+ // @ts-ignore
1514
+ await invoke('Viewlet.dispose', command.instanceId);
1515
+ };
1516
+
1517
+ const handleSetBounds = async command => {
1518
+ // @ts-ignore
1519
+ await invoke('Viewlet.setBounds', command.instanceId, command.bounds);
1520
+ };
1521
+
1522
+ const executeViewletCommands = async commands => {
1523
+ for (const command of commands) {
1524
+ switch (command.type) {
1525
+ case 'attach':
1526
+ await handleAttach(command);
1527
+ break;
1528
+ case 'create':
1529
+ await handleCreate(command);
1530
+ break;
1531
+ case 'detach':
1532
+ await handleDetach(command);
1533
+ break;
1534
+ case 'dispose':
1535
+ await handleDispose(command);
1536
+ break;
1537
+ case 'setBounds':
1538
+ await handleSetBounds(command);
1539
+ break;
1540
+ }
1541
+ }
1542
+ };
1543
+
1544
+ const closeTab = (state, groupId, tabId) => {
1545
+ const {
1546
+ layout
1547
+ } = state;
1548
+ const {
1549
+ activeGroupId,
1550
+ groups
1551
+ } = layout;
1552
+ const newGroups = groups.map(group => {
1553
+ if (group.id === groupId) {
1554
+ const newTabs = group.tabs.filter(tab => tab.id !== tabId);
1555
+ let newActiveTabId = group.activeTabId;
1556
+ if (group.activeTabId === tabId) {
1557
+ const tabIndex = group.tabs.findIndex(tab => tab.id === tabId);
1558
+ if (newTabs.length > 0) {
1559
+ newActiveTabId = newTabs[Math.min(tabIndex, newTabs.length - 1)].id;
1560
+ } else {
1561
+ newActiveTabId = undefined;
1562
+ }
1563
+ }
1564
+ return {
1565
+ ...group,
1566
+ activeTabId: newActiveTabId,
1567
+ tabs: newTabs
1568
+ };
1569
+ }
1570
+ return group;
1571
+ });
1572
+
1573
+ // If the group has no tabs left and there are other groups, remove the group
1574
+ const groupWithNoTabs = newGroups.find(group => group.id === groupId && group.tabs.length === 0);
1575
+ if (groupWithNoTabs && newGroups.length > 1) {
1576
+ const remainingGroups = newGroups.filter(group => group.id !== groupId);
1577
+ const redistributedGroups = remainingGroups.map(group => ({
1578
+ ...group,
1579
+ size: Math.round(100 / remainingGroups.length)
1580
+ }));
1581
+ const newActiveGroupId = activeGroupId === groupId ? remainingGroups[0]?.id : activeGroupId;
1582
+ return {
1583
+ ...state,
1584
+ layout: {
1585
+ ...layout,
1586
+ activeGroupId: newActiveGroupId,
1587
+ groups: redistributedGroups
1588
+ }
1589
+ };
1590
+ }
1591
+ return {
1592
+ ...state,
1593
+ layout: {
1594
+ ...layout,
1595
+ groups: newGroups
1596
+ }
1597
+ };
1598
+ };
1599
+
1600
+ const handleClickCloseTab = (state, rawGroupIndex, rawIndex) => {
1601
+ if (!rawGroupIndex || !rawIndex) {
1602
+ return state;
1603
+ }
1604
+ const groupIndex = Number.parseInt(rawGroupIndex);
1605
+ const index = Number.parseInt(rawIndex);
1606
+ const {
1607
+ layout
1608
+ } = state;
1609
+ const {
1610
+ groups
1611
+ } = layout;
1612
+
1613
+ // Validate indexes
1614
+ if (groupIndex < 0 || groupIndex >= groups.length) {
1615
+ return state;
1616
+ }
1617
+ const group = groups[groupIndex];
1618
+ if (index < 0 || index >= group.tabs.length) {
1619
+ return state;
1620
+ }
1621
+ const tab = group.tabs[index];
1622
+ const groupId = group.id;
1623
+ const tabId = tab.id;
1624
+ return closeTab(state, groupId, tabId);
1625
+ };
1626
+
1627
+ // Counter for request IDs to handle race conditions
1628
+ let requestIdCounter = 0;
1629
+ const getNextRequestId = () => {
1630
+ return ++requestIdCounter;
1631
+ };
1632
+
1633
+ const startContentLoading = async (oldState, state, tabId, path, requestId) => {
1634
+ try {
1635
+ const getLatestState = () => {
1636
+ return get$2(state.uid).newState;
1637
+ };
1638
+ set$3(state.uid, oldState, state);
1639
+ const newState = await loadTabContentAsync(tabId, path, requestId, getLatestState);
1640
+ return newState;
1641
+ } catch {
1642
+ // Silently ignore errors - the tab may have been closed or the component unmounted
1643
+ }
1644
+ return state;
1645
+ };
1646
+
1647
+ const shouldLoadContent = tab => {
1648
+ // Load if:
1649
+ // - Has a path (file-based tab)
1650
+ // - Not already loaded or currently loading
1651
+ if (!tab.uri) {
1652
+ return false;
1653
+ }
1654
+ if (tab.loadingState === 'loading') {
1655
+ return false;
1656
+ }
1657
+ if (tab.loadingState === 'loaded' && tab.content) {
1658
+ return false;
1659
+ }
1660
+ return true;
1661
+ };
1662
+ const getActiveTabId$1 = state => {
1663
+ const {
1664
+ layout
1665
+ } = state;
1666
+ const {
1667
+ activeGroupId,
1668
+ groups
1669
+ } = layout;
1670
+ const activeGroup = groups.find(g => g.id === activeGroupId);
1671
+ return activeGroup?.activeTabId;
1672
+ };
1673
+ const selectTab = async (state, groupIndex, index) => {
1674
+ const {
1675
+ layout,
1676
+ uid
1677
+ } = state;
1678
+ const {
1679
+ groups
1680
+ } = layout;
1681
+
1682
+ // Validate indexes
1683
+ if (groupIndex < 0 || groupIndex >= groups.length) {
1684
+ return state;
1685
+ }
1686
+ const group = groups[groupIndex];
1687
+ if (index < 0 || index >= group.tabs.length) {
1688
+ return state;
1689
+ }
1690
+ const tab = group.tabs[index];
1691
+ const groupId = group.id;
1692
+ const tabId = tab.id;
1693
+
1694
+ // Return same state if this group and tab are already active
1695
+ if (layout.activeGroupId === groupId && group.activeTabId === tabId) {
1696
+ return state;
1697
+ }
1698
+
1699
+ // Get previous active tab ID for viewlet switching
1700
+ getActiveTabId$1(state);
1701
+
1702
+ // Check if we need to load content for the newly selected tab
1703
+ const needsLoading = shouldLoadContent(tab);
1704
+ const requestId = needsLoading ? getNextRequestId() : 0;
1705
+
1706
+ // Update the groups array with the new active tab and active group
1707
+ // Also set loading state if needed
1708
+ const updatedGroups = groups.map((g, i) => {
1709
+ if (i !== groupIndex) {
1710
+ return {
1711
+ ...g,
1712
+ focused: false
1713
+ };
1714
+ }
1715
+
1716
+ // This is the group being selected
1717
+ const updatedTabs = needsLoading ? g.tabs.map(t => {
1718
+ if (t.id === tabId) {
1719
+ return {
1720
+ ...t,
1721
+ errorMessage: '',
1722
+ loadingState: 'loading'
1723
+ };
1724
+ }
1725
+ return t;
1544
1726
  }) : g.tabs;
1545
1727
  return {
1546
1728
  ...g,
@@ -1549,7 +1731,7 @@ const selectTab = async (state, groupIndex, index) => {
1549
1731
  tabs: updatedTabs
1550
1732
  };
1551
1733
  });
1552
- const newState = {
1734
+ let newState = {
1553
1735
  ...state,
1554
1736
  layout: {
1555
1737
  ...layout,
@@ -1557,11 +1739,47 @@ const selectTab = async (state, groupIndex, index) => {
1557
1739
  groups: updatedGroups
1558
1740
  }
1559
1741
  };
1742
+
1743
+ // Switch viewlet: detach old, attach new (if ready)
1744
+ const {
1745
+ commands: switchCommands,
1746
+ newState: stateWithViewlet
1747
+ } = switchViewlet(newState);
1748
+ newState = stateWithViewlet;
1749
+
1750
+ // If new tab's viewlet isn't ready yet, trigger creation (idempotent)
1751
+ const newTab = newState.layout.groups[groupIndex].tabs[index];
1752
+ if (!newTab.loadingState || newTab.loadingState === 'loading') {
1753
+ try {
1754
+ // Query RendererWorker for viewlet module ID
1755
+ // @ts-ignore
1756
+ const viewletModuleId = await invoke('Layout.getModuleId', newTab.uri);
1757
+ if (viewletModuleId) {
1758
+ // Calculate bounds: use main area bounds minus 35px for tab height
1759
+ const TAB_HEIGHT = 35;
1760
+ const bounds = {
1761
+ height: newState.height - TAB_HEIGHT,
1762
+ width: newState.width,
1763
+ x: newState.x,
1764
+ y: newState.y + TAB_HEIGHT
1765
+ };
1766
+ const createdState = createViewletForTab(newState, tabId, viewletModuleId, bounds);
1767
+ newState = createdState;
1768
+ }
1769
+ } catch {
1770
+ // Viewlet creation is optional - silently ignore if RendererWorker isn't available
1771
+ }
1772
+ }
1560
1773
  set$3(uid, state, newState);
1561
1774
 
1775
+ // Execute viewlet commands if any
1776
+ if (switchCommands.length > 0) {
1777
+ await executeViewletCommands(switchCommands);
1778
+ }
1779
+
1562
1780
  // Start loading content in the background if needed
1563
- if (needsLoading && tab.path) {
1564
- const latestState = await startContentLoading(state, newState, tabId, tab.path, requestId);
1781
+ if (needsLoading && tab.uri) {
1782
+ const latestState = await startContentLoading(state, newState, tabId, tab.uri, requestId);
1565
1783
  return latestState;
1566
1784
  }
1567
1785
  return newState;
@@ -1614,6 +1832,11 @@ const initialize = async () => {
1614
1832
  set$1(rpc);
1615
1833
  };
1616
1834
 
1835
+ const createViewlet = async (viewletModuleId, editorUid, tabId, bounds, uri) => {
1836
+ // @ts-ignore
1837
+ await invoke('Layout.createViewlet', viewletModuleId, editorUid, tabId, bounds, uri);
1838
+ };
1839
+
1617
1840
  const getMaxIdFromLayout = layout => {
1618
1841
  let maxId = 0;
1619
1842
  for (const group of layout.groups) {
@@ -1629,15 +1852,16 @@ const getMaxIdFromLayout = layout => {
1629
1852
  return maxId;
1630
1853
  };
1631
1854
 
1632
- let idCounter = 0;
1633
- const create = () => {
1634
- idCounter++;
1635
- return idCounter;
1636
- };
1637
- const setMinId = minId => {
1638
- if (minId > idCounter) {
1639
- idCounter = minId;
1855
+ const getViewletModuleId = async uri => {
1856
+ // Query RendererWorker for viewlet module ID (optional, may fail in tests)
1857
+ let viewletModuleId;
1858
+ try {
1859
+ // @ts-ignore
1860
+ viewletModuleId = await invoke('Layout.getModuleId', uri);
1861
+ } catch {
1862
+ // Viewlet creation is optional - silently ignore if RendererWorker isn't available
1640
1863
  }
1864
+ return viewletModuleId;
1641
1865
  };
1642
1866
 
1643
1867
  const isValidTab = tab => {
@@ -1689,10 +1913,66 @@ const loadContent = async (state, savedState) => {
1689
1913
  if (restoredLayout) {
1690
1914
  const maxId = getMaxIdFromLayout(restoredLayout);
1691
1915
  setMinId(maxId);
1692
- return {
1916
+ let newState = {
1693
1917
  ...state,
1694
1918
  layout: restoredLayout
1695
1919
  };
1920
+
1921
+ // Create viewlets only for active tabs in each group
1922
+ const TAB_HEIGHT = 35;
1923
+ const bounds = {
1924
+ height: newState.height - TAB_HEIGHT,
1925
+ width: newState.width,
1926
+ x: newState.x,
1927
+ y: newState.y + TAB_HEIGHT
1928
+ };
1929
+ for (const group of restoredLayout.groups) {
1930
+ // Find the active tab in this group
1931
+ const activeTab = group.tabs.find(tab => tab.id === group.activeTabId);
1932
+ if (activeTab && activeTab.uri) {
1933
+ const viewletModuleId = await getViewletModuleId(activeTab.uri);
1934
+ if (viewletModuleId) {
1935
+ // Ensure the tab has an editorUid
1936
+ const editorUid = activeTab.editorUid === -1 ? create() : activeTab.editorUid;
1937
+
1938
+ // Create viewlet for the tab
1939
+ newState = createViewletForTab(newState, activeTab.id);
1940
+
1941
+ // Update the tab with the editorUid in the state
1942
+ const updatedGroups = newState.layout.groups.map(g => {
1943
+ if (g.id !== group.id) {
1944
+ return g;
1945
+ }
1946
+ return {
1947
+ ...g,
1948
+ tabs: g.tabs.map(t => {
1949
+ if (t.id !== activeTab.id) {
1950
+ return t;
1951
+ }
1952
+ return {
1953
+ ...t,
1954
+ editorUid
1955
+ };
1956
+ })
1957
+ };
1958
+ });
1959
+ newState = {
1960
+ ...newState,
1961
+ layout: {
1962
+ ...newState.layout,
1963
+ groups: updatedGroups
1964
+ }
1965
+ };
1966
+
1967
+ // Create the actual viewlet instance
1968
+ await createViewlet(viewletModuleId, editorUid, activeTab.id, bounds, activeTab.uri);
1969
+
1970
+ // Mark the viewlet as ready
1971
+ newState = handleViewletReady(newState, editorUid);
1972
+ }
1973
+ }
1974
+ }
1975
+ return newState;
1696
1976
  }
1697
1977
  return {
1698
1978
  ...state,
@@ -1716,33 +1996,30 @@ const i18nString = (key, placeholders = emptyObject) => {
1716
1996
  return key.replaceAll(RE_PLACEHOLDER, replacer);
1717
1997
  };
1718
1998
 
1719
- /**
1720
- * @enum {string}
1721
- */
1722
- const UiStrings = {
1723
- Close: 'Close',
1724
- CloseAll: 'Close All',
1725
- CloseOthers: 'Close Others',
1726
- CloseToTheRight: 'Close To The Right',
1727
- FindFileReferences: 'Find File References',
1728
- RevealInExplorer: 'Reveal in Explorer'};
1999
+ const Close = 'Close';
2000
+ const CloseAll = 'Close All';
2001
+ const CloseOthers = 'Close Others';
2002
+ const CloseToTheRight = 'Close To The Right';
2003
+ const FindFileReferences = 'Find File References';
2004
+ const RevealInExplorer = 'Reveal in Explorer';
2005
+
1729
2006
  const close = () => {
1730
- return i18nString(UiStrings.Close);
2007
+ return i18nString(Close);
1731
2008
  };
1732
2009
  const closeOthers = () => {
1733
- return i18nString(UiStrings.CloseOthers);
2010
+ return i18nString(CloseOthers);
1734
2011
  };
1735
2012
  const closeAll = () => {
1736
- return i18nString(UiStrings.CloseAll);
2013
+ return i18nString(CloseAll);
1737
2014
  };
1738
2015
  const revealInExplorer = () => {
1739
- return i18nString(UiStrings.RevealInExplorer);
2016
+ return i18nString(RevealInExplorer);
1740
2017
  };
1741
2018
  const closeToTheRight = () => {
1742
- return i18nString(UiStrings.CloseToTheRight);
2019
+ return i18nString(CloseToTheRight);
1743
2020
  };
1744
2021
  const findFileReferences = () => {
1745
- return i18nString(UiStrings.FindFileReferences);
2022
+ return i18nString(FindFileReferences);
1746
2023
  };
1747
2024
 
1748
2025
  const menuEntrySeparator = {
@@ -1769,7 +2046,7 @@ const getMenuEntries$1 = state => {
1769
2046
  const tab = tabs[activeTabId || 0];
1770
2047
  object(tab);
1771
2048
  const {
1772
- path
2049
+ uri: path
1773
2050
  } = tab;
1774
2051
  return [{
1775
2052
  command: 'Main.closeFocusedTab',
@@ -1815,42 +2092,59 @@ const getMenuEntries = async (state, props) => {
1815
2092
  }
1816
2093
  };
1817
2094
 
1818
- const findTabByUri = (state, uri) => {
1819
- const {
1820
- layout
1821
- } = state;
1822
- const {
1823
- groups
1824
- } = layout;
1825
- for (const group of groups) {
1826
- const tab = group.tabs.find(t => t.path === uri);
1827
- if (tab) {
1828
- return {
1829
- groupId: group.id,
1830
- tab
1831
- };
1832
- }
2095
+ const getBasename = uri => {
2096
+ const lastSlashIndex = uri.lastIndexOf('/');
2097
+ if (lastSlashIndex === -1) {
2098
+ return uri;
1833
2099
  }
1834
- return undefined;
2100
+ return uri.slice(lastSlashIndex + 1);
2101
+ };
2102
+ const getLabel = uri => {
2103
+ if (uri.startsWith('settings://')) {
2104
+ return 'Settings';
2105
+ }
2106
+ if (uri.startsWith('simple-browser://')) {
2107
+ return 'Simple Browser';
2108
+ }
2109
+ return getBasename(uri);
1835
2110
  };
1836
2111
 
1837
- const focusEditorGroup = (state, groupId) => {
2112
+ const createEmptyGroup = (state, uri, requestId) => {
1838
2113
  const {
1839
2114
  layout
1840
2115
  } = state;
1841
2116
  const {
1842
2117
  groups
1843
2118
  } = layout;
1844
- const updatedGroups = groups.map(group => ({
1845
- ...group,
1846
- focused: group.id === groupId
1847
- }));
2119
+ const groupId = create();
2120
+ const title = getLabel(uri);
2121
+ const tabId = create();
2122
+ const editorUid = create();
2123
+ const newTab = {
2124
+ content: '',
2125
+ editorType: 'text',
2126
+ editorUid,
2127
+ errorMessage: '',
2128
+ id: tabId,
2129
+ isDirty: false,
2130
+ language: '',
2131
+ loadingState: 'loading',
2132
+ title,
2133
+ uri
2134
+ };
2135
+ const newGroup = {
2136
+ activeTabId: newTab.id,
2137
+ focused: true,
2138
+ id: groupId,
2139
+ size: 100,
2140
+ tabs: [newTab]
2141
+ };
1848
2142
  return {
1849
2143
  ...state,
1850
2144
  layout: {
1851
2145
  ...layout,
1852
2146
  activeGroupId: groupId,
1853
- groups: updatedGroups
2147
+ groups: [...groups, newGroup]
1854
2148
  }
1855
2149
  };
1856
2150
  };
@@ -1885,21 +2179,128 @@ const openTab = (state, groupId, tab) => {
1885
2179
  };
1886
2180
  };
1887
2181
 
1888
- const getBasename = uri => {
1889
- const lastSlashIndex = uri.lastIndexOf('/');
1890
- if (lastSlashIndex === -1) {
1891
- return uri;
2182
+ const ensureActiveGroup = (state, uri) => {
2183
+ // Find the active group (by activeGroupId or focused flag)
2184
+ const {
2185
+ layout
2186
+ } = state;
2187
+ const {
2188
+ activeGroupId,
2189
+ groups
2190
+ } = layout;
2191
+ const activeGroup = activeGroupId === undefined ? groups.find(group => group.focused) : groups.find(group => group.id === activeGroupId);
2192
+
2193
+ // Generate a request ID for content loading
2194
+ getNextRequestId();
2195
+
2196
+ // If no active group exists, create one
2197
+ let newState;
2198
+ if (activeGroup) {
2199
+ // Create a new tab with the URI in the active group
2200
+ const title = getLabel(uri);
2201
+ const tabId = create();
2202
+ const editorUid = create();
2203
+ const newTab = {
2204
+ content: '',
2205
+ editorType: 'text',
2206
+ editorUid,
2207
+ errorMessage: '',
2208
+ id: tabId,
2209
+ isDirty: false,
2210
+ language: '',
2211
+ loadingState: 'loading',
2212
+ title,
2213
+ uri: uri
2214
+ };
2215
+ newState = openTab(state, activeGroup.id, newTab);
2216
+ } else {
2217
+ newState = createEmptyGroup(state, uri);
1892
2218
  }
1893
- return uri.slice(lastSlashIndex + 1);
2219
+ return newState;
1894
2220
  };
1895
- const getLabel = uri => {
1896
- if (uri.startsWith('settings://')) {
1897
- return 'Settings';
2221
+
2222
+ const findTabById = (state, tabId) => {
2223
+ const {
2224
+ layout
2225
+ } = state;
2226
+ const {
2227
+ groups
2228
+ } = layout;
2229
+ for (const group of groups) {
2230
+ const tab = group.tabs.find(t => t.id === tabId);
2231
+ if (tab) {
2232
+ return {
2233
+ groupId: group.id,
2234
+ tab
2235
+ };
2236
+ }
1898
2237
  }
1899
- if (uri.startsWith('simple-browser://')) {
1900
- return 'Simple Browser';
2238
+ return undefined;
2239
+ };
2240
+
2241
+ const findTabByUri = (state, uri) => {
2242
+ const {
2243
+ layout
2244
+ } = state;
2245
+ const {
2246
+ groups
2247
+ } = layout;
2248
+ for (const group of groups) {
2249
+ const tab = group.tabs.find(t => t.uri === uri);
2250
+ if (tab) {
2251
+ return {
2252
+ groupId: group.id,
2253
+ tab
2254
+ };
2255
+ }
1901
2256
  }
1902
- return getBasename(uri);
2257
+ return undefined;
2258
+ };
2259
+
2260
+ const focusEditorGroup = (state, groupId) => {
2261
+ const {
2262
+ layout
2263
+ } = state;
2264
+ const {
2265
+ groups
2266
+ } = layout;
2267
+ const updatedGroups = groups.map(group => ({
2268
+ ...group,
2269
+ focused: group.id === groupId
2270
+ }));
2271
+ return {
2272
+ ...state,
2273
+ layout: {
2274
+ ...layout,
2275
+ activeGroupId: groupId,
2276
+ groups: updatedGroups
2277
+ }
2278
+ };
2279
+ };
2280
+
2281
+ const getActiveTabId = state => {
2282
+ const {
2283
+ layout
2284
+ } = state;
2285
+ const {
2286
+ activeGroupId,
2287
+ groups
2288
+ } = layout;
2289
+ const activeGroup = groups.find(g => g.id === activeGroupId);
2290
+ return activeGroup?.activeTabId;
2291
+ };
2292
+
2293
+ const getOptionUriOptions = options => {
2294
+ let uri = '';
2295
+ if (typeof options === 'string') {
2296
+ uri = options;
2297
+ } else {
2298
+ const {
2299
+ uri: optionsUri
2300
+ } = options;
2301
+ uri = optionsUri;
2302
+ }
2303
+ return uri;
1903
2304
  };
1904
2305
 
1905
2306
  const switchTab = (state, groupId, tabId) => {
@@ -1930,16 +2331,12 @@ const switchTab = (state, groupId, tabId) => {
1930
2331
  };
1931
2332
  };
1932
2333
 
1933
- /* eslint-disable prefer-destructuring */
1934
-
1935
2334
  const openUri = async (state, options) => {
1936
2335
  object(state);
1937
- let uri = '';
1938
- if (typeof options === 'string') {
1939
- uri = options;
1940
- } else {
1941
- uri = options.uri;
1942
- }
2336
+ const {
2337
+ uid
2338
+ } = state;
2339
+ const uri = getOptionUriOptions(options);
1943
2340
 
1944
2341
  // Check if a tab with this URI already exists
1945
2342
  const existingTab = findTabByUri(state, uri);
@@ -1949,69 +2346,58 @@ const openUri = async (state, options) => {
1949
2346
  return switchTab(focusedState, existingTab.groupId, existingTab.tab.id);
1950
2347
  }
1951
2348
 
1952
- // Find the active group (by activeGroupId or focused flag)
1953
- const {
1954
- layout
1955
- } = state;
1956
- const {
1957
- activeGroupId,
1958
- groups
1959
- } = layout;
1960
- const activeGroup = activeGroupId === undefined ? groups.find(group => group.focused) : groups.find(group => group.id === activeGroupId);
2349
+ // Get previous active tab ID for viewlet switching
2350
+ getActiveTabId(state);
2351
+ const newState = ensureActiveGroup(state, uri);
2352
+ const tabId = getActiveTabId(newState);
2353
+ const viewletModuleId = await getViewletModuleId(uri);
2354
+ if (!viewletModuleId) {
2355
+ // TODO display some kind of errro that editor couldn't be opened
2356
+ return newState;
2357
+ }
1961
2358
 
1962
- // Generate a request ID for content loading
1963
- const requestId = getNextRequestId();
2359
+ // Calculate bounds: use main area bounds minus 35px for tab height
2360
+ const TAB_HEIGHT = 35;
2361
+ const bounds = {
2362
+ height: newState.height - TAB_HEIGHT,
2363
+ width: newState.width,
2364
+ x: newState.x,
2365
+ y: newState.y + TAB_HEIGHT
2366
+ };
2367
+ const stateWithViewlet = createViewletForTab(newState, tabId);
2368
+ let intermediateState1 = stateWithViewlet;
1964
2369
 
1965
- // If no active group exists, create one
1966
- if (!activeGroup) {
1967
- const groupId = create();
1968
- const title = getLabel(uri);
1969
- const tabId = create();
1970
- const newTab = {
1971
- content: '',
1972
- editorType: 'text',
1973
- id: tabId,
1974
- isDirty: false,
1975
- loadingState: 'loading',
1976
- loadRequestId: requestId,
1977
- path: uri,
1978
- title
1979
- };
1980
- const newGroup = {
1981
- activeTabId: newTab.id,
1982
- focused: true,
1983
- id: groupId,
1984
- size: 100,
1985
- tabs: [newTab]
1986
- };
1987
- const newState = {
1988
- ...state,
1989
- layout: {
1990
- ...layout,
1991
- activeGroupId: groupId,
1992
- groups: [...groups, newGroup]
1993
- }
1994
- };
2370
+ // Switch viewlet (detach old, attach new if ready)
2371
+ const {
2372
+ newState: switchedState
2373
+ } = switchViewlet(intermediateState1);
2374
+ intermediateState1 = switchedState;
2375
+ set$3(uid, state, intermediateState1);
1995
2376
 
1996
- // Start loading content
1997
- return startContentLoading(state, newState, tabId, uri, requestId);
2377
+ // @ts-ignore
2378
+
2379
+ // Get the tab to extract editorUid
2380
+ const tabWithViewlet = findTabById(intermediateState1, tabId);
2381
+ if (!tabWithViewlet) {
2382
+ return intermediateState1;
2383
+ }
2384
+ const {
2385
+ editorUid
2386
+ } = tabWithViewlet.tab;
2387
+ if (editorUid === -1) {
2388
+ throw new Error(`invalid editorUid`);
1998
2389
  }
2390
+ await createViewlet(viewletModuleId, editorUid, tabId, bounds, uri);
1999
2391
 
2000
- // Create a new tab with the URI in the active group
2001
- const title = getLabel(uri);
2002
- const tabId = create();
2003
- const newTab = {
2004
- content: '',
2005
- editorType: 'text',
2006
- id: tabId,
2007
- isDirty: false,
2008
- loadingState: 'loading',
2009
- loadRequestId: requestId,
2010
- path: uri,
2011
- title
2012
- };
2013
- const newState = openTab(state, activeGroup.id, newTab);
2014
- return startContentLoading(state, newState, tabId, uri, requestId);
2392
+ // After viewlet is created, get the latest state and mark it as ready
2393
+ // This ensures we have any state updates that occurred during viewlet creation
2394
+ const {
2395
+ newState: latestState
2396
+ } = get$2(uid);
2397
+
2398
+ // Attachment is handled automatically by virtual DOM reference nodes
2399
+ const readyState = handleViewletReady(latestState, editorUid);
2400
+ return readyState;
2015
2401
  };
2016
2402
 
2017
2403
  const refresh = state => {
@@ -2064,29 +2450,38 @@ const renderContent = content => {
2064
2450
  type: Pre
2065
2451
  }, text(content)];
2066
2452
  };
2453
+ const renderViewletReference = tab => {
2454
+ return [{
2455
+ childCount: 0,
2456
+ type: 100,
2457
+ // Reference node type - frontend will append the component at this position
2458
+ uid: tab.editorUid
2459
+ }];
2460
+ };
2067
2461
  const renderEditor = tab => {
2068
2462
  if (!tab) {
2463
+ // Keep backward compatible behavior: render empty content
2069
2464
  return renderContent('');
2070
2465
  }
2071
- if (tab.editorType === 'custom') {
2072
- return [{
2073
- childCount: 1,
2074
- className: 'CustomEditor',
2075
- type: Div
2076
- }, text(`Custom Editor: ${tab.customEditorId}`)];
2077
- }
2078
2466
 
2079
- // Handle loading state
2467
+ // Viewlet is being created in background - show loading
2080
2468
  if (tab.loadingState === 'loading') {
2081
2469
  return renderLoading();
2082
2470
  }
2083
2471
 
2084
- // Handle error state
2472
+ // Viewlet is ready - render a reference node
2473
+ // Frontend will append the pre-created component at this position using the uid
2474
+ // Check for viewletInstanceId to distinguish between viewlet and plain text tabs
2475
+ if (tab.loadingState === 'loaded' && tab.editorUid !== -1) {
2476
+ return renderViewletReference(tab);
2477
+ }
2478
+
2479
+ // Viewlet error state
2085
2480
  if (tab.loadingState === 'error' && tab.errorMessage) {
2086
2481
  return renderError(tab.errorMessage);
2087
2482
  }
2088
2483
 
2089
- // Default: render content
2484
+ // Default: render content (fallback for simple text without viewlet)
2090
2485
  return renderContent(tab.content || '');
2091
2486
  };
2092
2487
 
@@ -2104,7 +2499,7 @@ const renderTab = (tab, isActive, tabIndex, groupIndex) => {
2104
2499
  onClick: HandleClickTab,
2105
2500
  onContextMenu: HandleTabContextMenu,
2106
2501
  role: 'tab',
2107
- title: tab.path || tab.title,
2502
+ title: tab.uri || tab.title,
2108
2503
  type: Div
2109
2504
  }, {
2110
2505
  childCount: 0,
@@ -2249,7 +2644,7 @@ const commandMap = {
2249
2644
  'MainArea.render2': render2,
2250
2645
  'MainArea.renderEventListeners': renderEventListeners,
2251
2646
  'MainArea.resize': wrapCommand(resize),
2252
- 'MainArea.saveState': saveState,
2647
+ 'MainArea.saveState': wrapGetter(saveState),
2253
2648
  'MainArea.selectTab': wrapCommand(selectTab),
2254
2649
  'MainArea.terminate': terminate
2255
2650
  };