@theia/core 1.71.0-next.4 → 1.71.0-next.41

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 (140) hide show
  1. package/README.md +13 -13
  2. package/i18n/nls.cs.json +4 -4
  3. package/i18n/nls.de.json +4 -4
  4. package/i18n/nls.es.json +4 -4
  5. package/i18n/nls.fr.json +4 -4
  6. package/i18n/nls.hu.json +4 -4
  7. package/i18n/nls.it.json +4 -4
  8. package/i18n/nls.ja.json +4 -4
  9. package/i18n/nls.ko.json +4 -4
  10. package/i18n/nls.pl.json +4 -4
  11. package/i18n/nls.pt-br.json +4 -4
  12. package/i18n/nls.ru.json +4 -4
  13. package/i18n/nls.tr.json +4 -4
  14. package/i18n/nls.zh-cn.json +4 -4
  15. package/i18n/nls.zh-tw.json +4 -4
  16. package/lib/browser/catalog.json +93 -5
  17. package/lib/browser/components/card.d.ts.map +1 -1
  18. package/lib/browser/components/card.js +11 -3
  19. package/lib/browser/components/card.js.map +1 -1
  20. package/lib/browser/keyboard/index.d.ts +1 -0
  21. package/lib/browser/keyboard/index.d.ts.map +1 -1
  22. package/lib/browser/keyboard/index.js +1 -0
  23. package/lib/browser/keyboard/index.js.map +1 -1
  24. package/lib/browser/keyboard/keyboard-utils.d.ts +17 -0
  25. package/lib/browser/keyboard/keyboard-utils.d.ts.map +1 -0
  26. package/lib/browser/keyboard/keyboard-utils.js +40 -0
  27. package/lib/browser/keyboard/keyboard-utils.js.map +1 -0
  28. package/lib/browser/messaging/messaging-frontend-module.d.ts.map +1 -1
  29. package/lib/browser/messaging/messaging-frontend-module.js +3 -0
  30. package/lib/browser/messaging/messaging-frontend-module.js.map +1 -1
  31. package/lib/browser/messaging/ws-connection-source.d.ts +2 -1
  32. package/lib/browser/messaging/ws-connection-source.d.ts.map +1 -1
  33. package/lib/browser/messaging/ws-connection-source.js +5 -2
  34. package/lib/browser/messaging/ws-connection-source.js.map +1 -1
  35. package/lib/browser/shell/tab-bar-toolbar/tab-bar-toolbar.d.ts +1 -0
  36. package/lib/browser/shell/tab-bar-toolbar/tab-bar-toolbar.d.ts.map +1 -1
  37. package/lib/browser/shell/tab-bar-toolbar/tab-bar-toolbar.js +10 -1
  38. package/lib/browser/shell/tab-bar-toolbar/tab-bar-toolbar.js.map +1 -1
  39. package/lib/browser/shell/tab-bar-toolbar/tab-toolbar-item.d.ts +2 -0
  40. package/lib/browser/shell/tab-bar-toolbar/tab-toolbar-item.d.ts.map +1 -1
  41. package/lib/browser/shell/tab-bar-toolbar/tab-toolbar-item.js +11 -2
  42. package/lib/browser/shell/tab-bar-toolbar/tab-toolbar-item.js.map +1 -1
  43. package/lib/common/event.d.ts +16 -0
  44. package/lib/common/event.d.ts.map +1 -1
  45. package/lib/common/event.js +20 -2
  46. package/lib/common/event.js.map +1 -1
  47. package/lib/common/event.spec.js +63 -0
  48. package/lib/common/event.spec.js.map +1 -1
  49. package/lib/common/glob.d.ts +2 -0
  50. package/lib/common/glob.d.ts.map +1 -1
  51. package/lib/common/glob.js +8 -7
  52. package/lib/common/glob.js.map +1 -1
  53. package/lib/common/message-rpc/channel.d.ts +1 -1
  54. package/lib/common/message-rpc/channel.d.ts.map +1 -1
  55. package/lib/common/message-rpc/channel.js +20 -12
  56. package/lib/common/message-rpc/channel.js.map +1 -1
  57. package/lib/common/message-rpc/channel.spec.d.ts.map +1 -1
  58. package/lib/common/message-rpc/channel.spec.js +94 -0
  59. package/lib/common/message-rpc/channel.spec.js.map +1 -1
  60. package/lib/common/message-rpc/rpc-protocol.d.ts.map +1 -1
  61. package/lib/common/message-rpc/rpc-protocol.js +13 -3
  62. package/lib/common/message-rpc/rpc-protocol.js.map +1 -1
  63. package/lib/common/message-rpc/uint8-array-message-buffer.d.ts.map +1 -1
  64. package/lib/common/message-rpc/uint8-array-message-buffer.js +1 -1
  65. package/lib/common/message-rpc/uint8-array-message-buffer.js.map +1 -1
  66. package/lib/common/messaging/index.d.ts +1 -0
  67. package/lib/common/messaging/index.d.ts.map +1 -1
  68. package/lib/common/messaging/index.js +1 -0
  69. package/lib/common/messaging/index.js.map +1 -1
  70. package/lib/common/messaging/socket-write-buffer.d.ts +4 -3
  71. package/lib/common/messaging/socket-write-buffer.d.ts.map +1 -1
  72. package/lib/common/messaging/socket-write-buffer.js +14 -4
  73. package/lib/common/messaging/socket-write-buffer.js.map +1 -1
  74. package/lib/common/preferences/index.d.ts +1 -0
  75. package/lib/common/preferences/index.d.ts.map +1 -1
  76. package/lib/common/preferences/index.js +1 -0
  77. package/lib/common/preferences/index.js.map +1 -1
  78. package/lib/common/preferences/preference-utils.d.ts +6 -0
  79. package/lib/common/preferences/preference-utils.d.ts.map +1 -0
  80. package/lib/common/preferences/preference-utils.js +29 -0
  81. package/lib/common/preferences/preference-utils.js.map +1 -0
  82. package/lib/common/resource.d.ts +2 -0
  83. package/lib/common/resource.d.ts.map +1 -1
  84. package/lib/common/resource.js +7 -3
  85. package/lib/common/resource.js.map +1 -1
  86. package/lib/electron-browser/menu/electron-main-menu-factory.d.ts.map +1 -1
  87. package/lib/electron-browser/menu/electron-main-menu-factory.js +5 -1
  88. package/lib/electron-browser/menu/electron-main-menu-factory.js.map +1 -1
  89. package/lib/electron-browser/messaging/electron-messaging-frontend-module.d.ts.map +1 -1
  90. package/lib/electron-browser/messaging/electron-messaging-frontend-module.js +3 -0
  91. package/lib/electron-browser/messaging/electron-messaging-frontend-module.js.map +1 -1
  92. package/lib/electron-main/electron-api-main.d.ts.map +1 -1
  93. package/lib/electron-main/electron-api-main.js +4 -2
  94. package/lib/electron-main/electron-api-main.js.map +1 -1
  95. package/lib/electron-main/theia-electron-window.d.ts.map +1 -1
  96. package/lib/electron-main/theia-electron-window.js +3 -0
  97. package/lib/electron-main/theia-electron-window.js.map +1 -1
  98. package/lib/node/messaging/index.d.ts +1 -0
  99. package/lib/node/messaging/index.d.ts.map +1 -1
  100. package/lib/node/messaging/index.js +1 -0
  101. package/lib/node/messaging/index.js.map +1 -1
  102. package/lib/node/messaging/messaging-backend-module.d.ts.map +1 -1
  103. package/lib/node/messaging/messaging-backend-module.js +4 -0
  104. package/lib/node/messaging/messaging-backend-module.js.map +1 -1
  105. package/lib/node/messaging/websocket-frontend-connection-service.d.ts +9 -5
  106. package/lib/node/messaging/websocket-frontend-connection-service.d.ts.map +1 -1
  107. package/lib/node/messaging/websocket-frontend-connection-service.js +21 -5
  108. package/lib/node/messaging/websocket-frontend-connection-service.js.map +1 -1
  109. package/lib/node/process-utils.d.ts.map +1 -1
  110. package/lib/node/process-utils.js +9 -1
  111. package/lib/node/process-utils.js.map +1 -1
  112. package/package.json +32 -32
  113. package/src/browser/components/card.tsx +13 -2
  114. package/src/browser/keyboard/index.ts +1 -0
  115. package/src/browser/keyboard/keyboard-utils.ts +37 -0
  116. package/src/browser/messaging/messaging-frontend-module.ts +3 -0
  117. package/src/browser/messaging/ws-connection-source.ts +3 -2
  118. package/src/browser/shell/tab-bar-toolbar/tab-bar-toolbar.tsx +14 -1
  119. package/src/browser/shell/tab-bar-toolbar/tab-toolbar-item.tsx +13 -2
  120. package/src/browser/style/card.css +4 -2
  121. package/src/common/event.spec.ts +80 -0
  122. package/src/common/event.ts +31 -2
  123. package/src/common/glob.ts +2 -2
  124. package/src/common/message-rpc/channel.spec.ts +116 -0
  125. package/src/common/message-rpc/channel.ts +15 -11
  126. package/src/common/message-rpc/rpc-protocol.ts +12 -3
  127. package/src/common/message-rpc/uint8-array-message-buffer.ts +1 -1
  128. package/src/common/messaging/index.ts +1 -0
  129. package/src/common/messaging/socket-write-buffer.ts +10 -4
  130. package/src/common/preferences/index.ts +1 -0
  131. package/src/common/preferences/preference-utils.ts +28 -0
  132. package/src/common/resource.ts +8 -2
  133. package/src/electron-browser/menu/electron-main-menu-factory.ts +5 -1
  134. package/src/electron-browser/messaging/electron-messaging-frontend-module.ts +3 -0
  135. package/src/electron-main/electron-api-main.ts +4 -2
  136. package/src/electron-main/theia-electron-window.ts +3 -0
  137. package/src/node/messaging/index.ts +1 -0
  138. package/src/node/messaging/messaging-backend-module.ts +5 -1
  139. package/src/node/messaging/websocket-frontend-connection-service.ts +20 -7
  140. package/src/node/process-utils.ts +9 -1
@@ -14,6 +14,7 @@
14
14
  // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
15
15
  // *****************************************************************************
16
16
 
17
+ import { injectable } from 'inversify';
17
18
  import { Disposable, DisposableCollection } from '../disposable';
18
19
  import { Emitter, Event } from '../event';
19
20
  import { ReadBuffer, WriteBuffer } from './message-buffer';
@@ -71,6 +72,7 @@ export type MessageProvider = () => ReadBuffer;
71
72
  * Reusable abstract {@link Channel} implementation that sets up
72
73
  * the basic channel event listeners and offers a generic close method.
73
74
  */
75
+ @injectable()
74
76
  export abstract class AbstractChannel implements Channel {
75
77
 
76
78
  onCloseEmitter: Emitter<ChannelCloseEvent> = new Emitter();
@@ -152,7 +154,7 @@ export enum MessageTypes {
152
154
  * messages and always in one go.
153
155
  */
154
156
  export class ChannelMultiplexer implements Disposable {
155
- protected pendingOpen: Map<string, (channel: ForwardingChannel) => void> = new Map();
157
+ private pendingOpen: Map<string, { resolve: (channel: ForwardingChannel) => void, reject: (err: Error) => void }> = new Map();
156
158
  protected openChannels: Map<string, ForwardingChannel> = new Map();
157
159
 
158
160
  protected readonly onOpenChannelEmitter = new Emitter<{ id: string, channel: Channel }>();
@@ -180,9 +182,11 @@ export class ChannelMultiplexer implements Disposable {
180
182
  onUnderlyingChannelClose(event?: ChannelCloseEvent): void {
181
183
  if (!this.toDispose.disposed) {
182
184
  this.toDispose.push(Disposable.create(() => {
185
+ const reason = event?.reason ?? 'Multiplexer main channel has been closed from the remote side!';
186
+ this.pendingOpen.forEach(pending => pending.reject(new Error(reason)));
183
187
  this.pendingOpen.clear();
184
188
  this.openChannels.forEach(channel => {
185
- channel.onCloseEmitter.fire(event ?? { reason: 'Multiplexer main channel has been closed from the remote side!' });
189
+ channel.onCloseEmitter.fire(event ?? { reason });
186
190
  });
187
191
 
188
192
  this.openChannels.clear();
@@ -212,13 +216,13 @@ export class ChannelMultiplexer implements Disposable {
212
216
  }
213
217
 
214
218
  protected handleAckOpen(id: string): void {
215
- // edge case: both side try to open a channel at the same time.
216
- const resolve = this.pendingOpen.get(id);
217
- if (resolve) {
219
+ // edge case: both sides try to open a channel at the same time.
220
+ const pending = this.pendingOpen.get(id);
221
+ if (pending) {
218
222
  const channel = this.createChannel(id);
219
223
  this.pendingOpen.delete(id);
220
224
  this.openChannels.set(id, channel);
221
- resolve(channel);
225
+ pending.resolve(channel);
222
226
  this.onOpenChannelEmitter.fire({ id, channel });
223
227
  } else {
224
228
  console.error(`not expecting ack-open on for ${id}`);
@@ -229,10 +233,10 @@ export class ChannelMultiplexer implements Disposable {
229
233
  if (!this.openChannels.has(id)) {
230
234
  const channel = this.createChannel(id);
231
235
  this.openChannels.set(id, channel);
232
- const resolve = this.pendingOpen.get(id);
233
- if (resolve) {
234
- // edge case: both side try to open a channel at the same time.
235
- resolve(channel);
236
+ const pending = this.pendingOpen.get(id);
237
+ if (pending) {
238
+ // edge case: both sides try to open a channel at the same time.
239
+ pending.resolve(channel);
236
240
  }
237
241
  this.underlyingChannel.getWriteBuffer().writeUint8(MessageTypes.AckOpen).writeString(id).commit();
238
242
  this.onOpenChannelEmitter.fire({ id, channel });
@@ -283,7 +287,7 @@ export class ChannelMultiplexer implements Disposable {
283
287
  throw new Error(`Another channel with the id '${id}' is already open.`);
284
288
  }
285
289
  const result = new Promise<Channel>((resolve, reject) => {
286
- this.pendingOpen.set(id, resolve);
290
+ this.pendingOpen.set(id, { resolve, reject });
287
291
  });
288
292
  this.underlyingChannel.getWriteBuffer().writeUint8(MessageTypes.Open).writeString(id).commit();
289
293
  return result;
@@ -178,9 +178,18 @@ export class RpcProtocol {
178
178
  const disposableWrapper = new DisposableWrapper();
179
179
  this.pendingRequestCancellationEventListeners.set(id, disposableWrapper);
180
180
 
181
- const output = this.channel.getWriteBuffer();
182
- this.encoder.request(output, id, method, args);
183
- output.commit();
181
+ try {
182
+ const output = this.channel.getWriteBuffer();
183
+ this.encoder.request(output, id, method, args);
184
+ output.commit();
185
+ } catch (err) {
186
+ // The message could not be sent (e.g. write buffer overflow).
187
+ // Clean up the pending request and reject the promise.
188
+ this.pendingRequests.delete(id);
189
+ this.disposeCancellationEventListener(id);
190
+ reply.reject(err);
191
+ return reply.promise;
192
+ }
184
193
 
185
194
  if (cancellationToken?.isCancellationRequested) {
186
195
  this.sendCancel(id);
@@ -113,7 +113,7 @@ export class Uint8ArrayWriteBuffer implements WriteBuffer, Disposable {
113
113
  return this;
114
114
  }
115
115
 
116
- private onCommitEmitter = new Emitter<Uint8Array>();
116
+ private onCommitEmitter = new Emitter<Uint8Array>({ errorHandling: 'propagate' });
117
117
  get onCommit(): Event<Uint8Array> {
118
118
  return this.onCommitEmitter.event;
119
119
  }
@@ -17,3 +17,4 @@
17
17
  export * from './handler';
18
18
  export * from './proxy-factory';
19
19
  export * from './connection-error-handler';
20
+ export * from './socket-write-buffer';
@@ -14,13 +14,19 @@
14
14
  // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
15
15
  // *****************************************************************************
16
16
 
17
+ import { injectable } from 'inversify';
17
18
  import { WebSocket } from './web-socket-channel';
18
19
 
20
+ @injectable()
19
21
  export class SocketWriteBuffer {
20
- private static DISCONNECTED_BUFFER_SIZE = 100 * 1024;
22
+ private static readonly DEFAULT_DISCONNECTED_BUFFER_SIZE = 100 * 1024;
21
23
 
22
- private disconnectedBuffer: Uint8Array | undefined;
23
- private bufferWritePosition = 0;
24
+ protected disconnectedBuffer: Uint8Array | undefined;
25
+ protected bufferWritePosition = 0;
26
+
27
+ protected get maxBufferSize(): number {
28
+ return SocketWriteBuffer.DEFAULT_DISCONNECTED_BUFFER_SIZE;
29
+ }
24
30
 
25
31
  buffer(data: Uint8Array): void {
26
32
  this.ensureWriteBuffer(data.byteLength);
@@ -30,7 +36,7 @@ export class SocketWriteBuffer {
30
36
 
31
37
  protected ensureWriteBuffer(byteLength: number): void {
32
38
  if (!this.disconnectedBuffer) {
33
- this.disconnectedBuffer = new Uint8Array(SocketWriteBuffer.DISCONNECTED_BUFFER_SIZE);
39
+ this.disconnectedBuffer = new Uint8Array(this.maxBufferSize);
34
40
  this.bufferWritePosition = 0;
35
41
  }
36
42
 
@@ -25,3 +25,4 @@ export * from './preference-service';
25
25
  export * from './injectable-preference-proxy';
26
26
  export * from './preference-proxy';
27
27
  export * from './preference-configurations';
28
+ export * from './preference-utils';
@@ -0,0 +1,28 @@
1
+ // *****************************************************************************
2
+ // Copyright (C) 2026 EclipseSource and others.
3
+ //
4
+ // This program and the accompanying materials are made available under the
5
+ // terms of the Eclipse Public License v. 2.0 which is available at
6
+ // http://www.eclipse.org/legal/epl-2.0.
7
+ //
8
+ // This Source Code may also be made available under the following Secondary
9
+ // Licenses when the conditions for such availability set forth in the Eclipse
10
+ // Public License v. 2.0 are satisfied: GNU General Public License, version 2
11
+ // with the GNU Classpath Exception which is available at
12
+ // https://www.gnu.org/software/classpath/license.html.
13
+ //
14
+ // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
15
+ // *****************************************************************************
16
+
17
+ import { nls } from '../nls';
18
+ import { isOSX, isWindows } from '../os';
19
+
20
+ /**
21
+ * Hint appended to API key preference descriptions on Linux, where environment variables
22
+ * set in `~/.bashrc` are not available to desktop-launched applications.
23
+ */
24
+ export const LINUX_ENV_HINT = !isWindows && !isOSX
25
+ ? ' ' + nls.localize('theia/ai-core/preferences/linuxEnvHint',
26
+ 'On Linux, make sure the variable is defined in `~/.profile` (not just `~/.bashrc`) if you launch the application from a desktop shortcut.' +
27
+ ' See the [documentation](https://theia-ide.org/docs/user_ai/#setting-api-keys) for details.')
28
+ : '';
@@ -311,13 +311,19 @@ export class InMemoryResources implements ResourceResolver {
311
311
  }
312
312
 
313
313
  export const MEMORY_TEXT = 'mem-txt';
314
+ export const MEMORY_TEXT_READONLY = 'mem-txt-readonly';
314
315
 
315
316
  /**
316
317
  * Resource implementation for 'mem-txt' URI scheme where content is saved in URI query.
317
318
  */
318
319
  export class InMemoryTextResource implements Resource {
320
+
319
321
  constructor(readonly uri: URI) { }
320
322
 
323
+ get readOnly(): boolean {
324
+ return this.uri.scheme === MEMORY_TEXT_READONLY;
325
+ }
326
+
321
327
  async readContents(options?: { encoding?: string | undefined; } | undefined): Promise<string> {
322
328
  return this.uri.query;
323
329
  }
@@ -330,8 +336,8 @@ export class InMemoryTextResource implements Resource {
330
336
  @injectable()
331
337
  export class InMemoryTextResourceResolver implements ResourceResolver {
332
338
  resolve(uri: URI): MaybePromise<Resource> {
333
- if (uri.scheme !== MEMORY_TEXT) {
334
- throw new Error(`Expected a URI with ${MEMORY_TEXT} scheme. Was: ${uri}.`);
339
+ if (uri.scheme !== MEMORY_TEXT && uri.scheme !== MEMORY_TEXT_READONLY) {
340
+ throw new Error(`Expected a URI with ${MEMORY_TEXT} or ${MEMORY_TEXT_READONLY} scheme. Was: ${uri}.`);
335
341
  }
336
342
  return new InMemoryTextResource(uri);
337
343
  }
@@ -233,7 +233,11 @@ export class ElectronMainMenuFactory extends BrowserMainMenuFactory {
233
233
  }
234
234
  };
235
235
 
236
- const role = this.roleFor(menu.id);
236
+ // Only assign Electron roles when no custom args are present.
237
+ // Custom args indicate that command handlers have context-dependent
238
+ // behavior (e.g. chat view copying the whole message when there is
239
+ // no DOM selection) that would be bypassed by the native role.
240
+ const role = args.length === 0 ? this.roleFor(menu.id) : undefined;
237
241
  if (role) {
238
242
  menuItem.role = role;
239
243
  delete menuItem.execute;
@@ -26,11 +26,14 @@ import { LocalConnectionProvider, RemoteConnectionProvider, ServiceConnectionPro
26
26
  import { WebSocketConnectionProvider } from '../../browser/messaging/ws-connection-provider';
27
27
  import { ConnectionCloseService, connectionCloseServicePath } from '../../common/messaging/connection-management';
28
28
  import { WebSocketConnectionSource } from '../../browser/messaging/ws-connection-source';
29
+ import { SocketWriteBuffer } from '../../common/messaging/socket-write-buffer';
29
30
 
30
31
  const backendServiceProvider = Symbol('backendServiceProvider2');
31
32
  const localServiceProvider = Symbol('localServiceProvider');
32
33
 
33
34
  export const messagingFrontendModule = new ContainerModule(bind => {
35
+ // Transient: each connection source gets its own private buffer instance.
36
+ bind(SocketWriteBuffer).toSelf();
34
37
  bind(ConnectionCloseService).toDynamicValue(ctx => WebSocketConnectionProvider.createProxy(ctx.container, connectionCloseServicePath)).inSingletonScope();
35
38
  bind(ElectronWebSocketConnectionSource).toSelf().inSingletonScope();
36
39
  bind(WebSocketConnectionSource).toService(ElectronWebSocketConnectionSource);
@@ -136,6 +136,8 @@ export class TheiaMainApi implements ElectronMainApplicationContribution {
136
136
  }
137
137
  popup.popup({
138
138
  window: electronWindow,
139
+ x,
140
+ y,
139
141
  callback: () => {
140
142
  this.openPopups.delete(menuId);
141
143
  event.sender.send(CHANNEL_ON_CLOSE_POPUP, menuId);
@@ -343,13 +345,13 @@ export namespace TheiaRendererAPI {
343
345
  const disposables = new DisposableCollection();
344
346
 
345
347
  return new Promise<boolean>(resolve => {
346
- wc.send(CHANNEL_REQUEST_CLOSE, stopReason, confirmChannel, cancelChannel);
347
348
  createDisposableListener(ipcMain, confirmChannel, e => {
348
349
  resolve(true);
349
350
  }, disposables);
350
351
  createDisposableListener(ipcMain, cancelChannel, e => {
351
352
  resolve(false);
352
353
  }, disposables);
354
+ wc.send(CHANNEL_REQUEST_CLOSE, stopReason, confirmChannel, cancelChannel);
353
355
  }).finally(() => disposables.dispose());
354
356
  }
355
357
 
@@ -360,13 +362,13 @@ export namespace TheiaRendererAPI {
360
362
  const disposables = new DisposableCollection();
361
363
 
362
364
  return new Promise<boolean>(resolve => {
363
- mainWindow.send(CHANNEL_REQUEST_SECONDARY_CLOSE, secondaryWindow.mainFrame.name, confirmChannel, cancelChannel);
364
365
  createDisposableListener(ipcMain, confirmChannel, e => {
365
366
  resolve(true);
366
367
  }, disposables);
367
368
  createDisposableListener(ipcMain, cancelChannel, e => {
368
369
  resolve(false);
369
370
  }, disposables);
371
+ mainWindow.send(CHANNEL_REQUEST_SECONDARY_CLOSE, secondaryWindow.mainFrame.name, confirmChannel, cancelChannel);
370
372
  }).finally(() => disposables.dispose());
371
373
  }
372
374
 
@@ -140,6 +140,9 @@ export class TheiaElectronWindow {
140
140
 
141
141
  protected async doCloseWindow(): Promise<void> {
142
142
  this.closeIsConfirmed = true;
143
+ // Hide the window immediately so the user perceives an instant close.
144
+ // This is done after veto checks have passed to ensure save dialogs remain visible.
145
+ this._window.hide();
143
146
  await TheiaRendererAPI.sendAboutToClose(this._window.webContents);
144
147
  this._window.close();
145
148
  }
@@ -17,3 +17,4 @@
17
17
  export * from './messaging-service';
18
18
  export * from './ipc-connection-provider';
19
19
  export * from './ipc-channel';
20
+ export * from './websocket-frontend-connection-service';
@@ -24,8 +24,9 @@ import { MessagingListener, MessagingListenerContribution } from './messaging-li
24
24
  import { FrontendConnectionService } from './frontend-connection-service';
25
25
  import { BackendApplicationContribution } from '../backend-application';
26
26
  import { connectionCloseServicePath } from '../../common/messaging/connection-management';
27
- import { WebsocketFrontendConnectionService } from './websocket-frontend-connection-service';
27
+ import { ReconnectableSocketChannel, WebsocketFrontendConnectionService } from './websocket-frontend-connection-service';
28
28
  import { WebsocketEndpoint } from './websocket-endpoint';
29
+ import { SocketWriteBuffer } from '../../common/messaging/socket-write-buffer';
29
30
 
30
31
  export const messagingBackendModule = new ContainerModule(bind => {
31
32
  bindRootContributionProvider(bind, ConnectionContainerModule);
@@ -36,6 +37,9 @@ export const messagingBackendModule = new ContainerModule(bind => {
36
37
  bind(MessagingContainer).toDynamicValue(({ container }) => container).inSingletonScope();
37
38
  bind(WebsocketEndpoint).toSelf().inSingletonScope();
38
39
  bind(BackendApplicationContribution).toService(WebsocketEndpoint);
40
+ // Transient: each connection gets its own private buffer and channel instances.
41
+ bind(SocketWriteBuffer).toSelf();
42
+ bind(ReconnectableSocketChannel).toSelf();
39
43
  bind(WebsocketFrontendConnectionService).toSelf().inSingletonScope();
40
44
  bind(FrontendConnectionService).toService(WebsocketFrontendConnectionService);
41
45
  bind(MessagingListener).toSelf().inSingletonScope();
@@ -15,9 +15,9 @@
15
15
 
16
16
  import { Channel, WriteBuffer } from '../../common/message-rpc';
17
17
  import { MessagingService } from './messaging-service';
18
- import { inject, injectable } from 'inversify';
18
+ import { inject, injectable, interfaces } from 'inversify';
19
19
  import { Socket } from 'socket.io';
20
- import { ConnectionHandlers } from './default-messaging-service';
20
+ import { ConnectionHandlers, MessagingContainer } from './default-messaging-service';
21
21
  import { SocketWriteBuffer } from '../../common/messaging/socket-write-buffer';
22
22
  import { FrontendConnectionService } from './frontend-connection-service';
23
23
  import { AbstractChannel } from '../../common/message-rpc/channel';
@@ -33,6 +33,9 @@ export class WebsocketFrontendConnectionService implements FrontendConnectionSer
33
33
  @inject(WebsocketEndpoint)
34
34
  protected readonly websocketServer: WebsocketEndpoint;
35
35
 
36
+ @inject(MessagingContainer)
37
+ protected readonly container: interfaces.Container;
38
+
36
39
  protected readonly wsHandlers = new ConnectionHandlers();
37
40
  protected readonly connectionsByFrontend = new Map<string, ReconnectableSocketChannel>();
38
41
  protected readonly closeTimeouts = new Map<string, NodeJS.Timeout>();
@@ -89,12 +92,13 @@ export class WebsocketFrontendConnectionService implements FrontendConnectionSer
89
92
  this.closeTimeouts.delete(frontEndId);
90
93
 
91
94
  connection.onCloseEmitter.fire({ reason });
95
+ connection.drainBuffer();
92
96
  connection.close();
93
97
  }
94
98
 
95
99
  protected createConnection(socket: Socket, frontEndId: string): ReconnectableSocketChannel {
96
100
  console.info(`creating connection for ${frontEndId}`);
97
- const channel = new ReconnectableSocketChannel();
101
+ const channel = this.container.get(ReconnectableSocketChannel);
98
102
  channel.connect(socket);
99
103
 
100
104
  this.connectionsByFrontend.set(frontEndId, channel);
@@ -136,12 +140,17 @@ export class WebsocketFrontendConnectionService implements FrontendConnectionSer
136
140
  }
137
141
  }
138
142
 
139
- class ReconnectableSocketChannel extends AbstractChannel {
140
- private socket: Socket | undefined;
141
- private socketBuffer = new SocketWriteBuffer();
142
- private disposables = new DisposableCollection();
143
+ @injectable()
144
+ export class ReconnectableSocketChannel extends AbstractChannel {
145
+ protected socket: Socket | undefined;
146
+
147
+ @inject(SocketWriteBuffer)
148
+ protected socketBuffer: SocketWriteBuffer;
149
+
150
+ protected disposables = new DisposableCollection();
143
151
 
144
152
  connect(socket: Socket): void {
153
+ this.disposables.dispose();
145
154
  this.disposables = new DisposableCollection();
146
155
  this.socket = socket;
147
156
  const errorHandler = (err: Error) => {
@@ -171,6 +180,10 @@ class ReconnectableSocketChannel extends AbstractChannel {
171
180
  this.socket = undefined;
172
181
  }
173
182
 
183
+ drainBuffer(): void {
184
+ this.socketBuffer.drain();
185
+ }
186
+
174
187
  override getWriteBuffer(): WriteBuffer {
175
188
  const writeBuffer = new Uint8ArrayWriteBuffer();
176
189
  writeBuffer.onCommit(data => {
@@ -32,7 +32,15 @@ export class ProcessUtils {
32
32
  }
33
33
 
34
34
  protected winTerminateProcessTree(ppid: number): void {
35
- this.spawnSync('taskkill.exe', ['/f', '/t', '/pid', ppid.toString(10)]);
35
+ const result = cp.spawnSync('taskkill.exe', ['/f', '/t', '/pid', ppid.toString(10)], { encoding: 'utf8' });
36
+ if (result.error) {
37
+ throw result.error;
38
+ }
39
+ // taskkill may exit with a non-zero code when some child processes have already exited.
40
+ // This is expected during shutdown — log but don't throw.
41
+ if (result.status !== 0) {
42
+ console.warn(`taskkill.exe exited with ${result.status} for PID ${ppid}. Output:\n${JSON.stringify(result.output)}`);
43
+ }
36
44
  }
37
45
 
38
46
  protected unixTerminateProcessTree(ppid: number): void {