@theia/dev-container 1.76.0-next.7 → 1.77.0-next.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. package/lib/electron-browser/container-connection-contribution.d.ts +2 -0
  2. package/lib/electron-browser/container-connection-contribution.d.ts.map +1 -1
  3. package/lib/electron-browser/container-connection-contribution.js +12 -1
  4. package/lib/electron-browser/container-connection-contribution.js.map +1 -1
  5. package/lib/electron-browser/container-output-provider.d.ts +5 -0
  6. package/lib/electron-browser/container-output-provider.d.ts.map +1 -1
  7. package/lib/electron-browser/container-output-provider.js +11 -0
  8. package/lib/electron-browser/container-output-provider.js.map +1 -1
  9. package/lib/electron-browser/dev-container-attach-screen.d.ts +52 -0
  10. package/lib/electron-browser/dev-container-attach-screen.d.ts.map +1 -0
  11. package/lib/electron-browser/dev-container-attach-screen.js +142 -0
  12. package/lib/electron-browser/dev-container-attach-screen.js.map +1 -0
  13. package/lib/electron-browser/dev-container-attach-screen.spec.d.ts +2 -0
  14. package/lib/electron-browser/dev-container-attach-screen.spec.d.ts.map +1 -0
  15. package/lib/electron-browser/dev-container-attach-screen.spec.js +51 -0
  16. package/lib/electron-browser/dev-container-attach-screen.spec.js.map +1 -0
  17. package/lib/electron-browser/dev-container-frontend-module.d.ts +1 -0
  18. package/lib/electron-browser/dev-container-frontend-module.d.ts.map +1 -1
  19. package/lib/electron-browser/dev-container-frontend-module.js +3 -0
  20. package/lib/electron-browser/dev-container-frontend-module.js.map +1 -1
  21. package/lib/electron-browser/dev-container-startup-contribution.d.ts +31 -1
  22. package/lib/electron-browser/dev-container-startup-contribution.d.ts.map +1 -1
  23. package/lib/electron-browser/dev-container-startup-contribution.js +114 -9
  24. package/lib/electron-browser/dev-container-startup-contribution.js.map +1 -1
  25. package/lib/electron-browser/dev-container-startup-contribution.spec.d.ts +2 -0
  26. package/lib/electron-browser/dev-container-startup-contribution.spec.d.ts.map +1 -0
  27. package/lib/electron-browser/dev-container-startup-contribution.spec.js +199 -0
  28. package/lib/electron-browser/dev-container-startup-contribution.spec.js.map +1 -0
  29. package/lib/electron-common/container-output-provider.d.ts +6 -0
  30. package/lib/electron-common/container-output-provider.d.ts.map +1 -1
  31. package/lib/electron-common/remote-container-connection-provider.d.ts +10 -0
  32. package/lib/electron-common/remote-container-connection-provider.d.ts.map +1 -1
  33. package/lib/electron-main/dev-container-claim-contribution.d.ts +13 -0
  34. package/lib/electron-main/dev-container-claim-contribution.d.ts.map +1 -0
  35. package/lib/electron-main/dev-container-claim-contribution.js +38 -0
  36. package/lib/electron-main/dev-container-claim-contribution.js.map +1 -0
  37. package/lib/electron-main/dev-container-electron-main-module.d.ts +4 -0
  38. package/lib/electron-main/dev-container-electron-main-module.d.ts.map +1 -0
  39. package/lib/electron-main/dev-container-electron-main-module.js +25 -0
  40. package/lib/electron-main/dev-container-electron-main-module.js.map +1 -0
  41. package/lib/electron-node/remote-container-connection-provider.d.ts +1 -1
  42. package/lib/electron-node/remote-container-connection-provider.d.ts.map +1 -1
  43. package/lib/electron-node/remote-container-connection-provider.js +10 -5
  44. package/lib/electron-node/remote-container-connection-provider.js.map +1 -1
  45. package/package.json +9 -8
  46. package/src/electron-browser/container-connection-contribution.ts +11 -1
  47. package/src/electron-browser/container-output-provider.ts +11 -0
  48. package/src/electron-browser/dev-container-attach-screen.spec.ts +60 -0
  49. package/src/electron-browser/dev-container-attach-screen.tsx +180 -0
  50. package/src/electron-browser/dev-container-frontend-module.ts +3 -0
  51. package/src/electron-browser/dev-container-startup-contribution.spec.ts +245 -0
  52. package/src/electron-browser/dev-container-startup-contribution.ts +113 -15
  53. package/src/electron-browser/style/dev-container-attach-screen.css +75 -0
  54. package/src/electron-common/container-output-provider.ts +6 -0
  55. package/src/electron-common/remote-container-connection-provider.ts +10 -0
  56. package/src/electron-main/dev-container-claim-contribution.ts +34 -0
  57. package/src/electron-main/dev-container-electron-main-module.ts +24 -0
  58. package/src/electron-node/remote-container-connection-provider.ts +10 -5
@@ -0,0 +1,245 @@
1
+ // *****************************************************************************
2
+ // Copyright (C) 2026 EclipseSource GmbH 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 { enableJSDOM } from '@theia/core/lib/browser/test/jsdom';
18
+
19
+ let disableJSDOM = enableJSDOM();
20
+
21
+ import { expect } from 'chai';
22
+ import { LaunchArguments } from '@theia/core/lib/common/launch-arguments';
23
+
24
+ const launchArgs = (values: Record<string, string[]>): LaunchArguments => ({ values, negated: [] });
25
+ import { AttachContainerArgs, ContainerConnectionResult, RunningContainerInfo } from '../electron-common/remote-container-connection-provider';
26
+ import { DevContainerStartupContribution } from './dev-container-startup-contribution';
27
+ import type { AttachScreenErrorActions } from './dev-container-attach-screen';
28
+
29
+ disableJSDOM();
30
+
31
+ class TestDevContainerStartupContribution extends DevContainerStartupContribution {
32
+
33
+ forwardedArgs: LaunchArguments | undefined;
34
+ backendCalls = 0;
35
+ backendArgs: AttachContainerArgs | undefined;
36
+
37
+ resolve(): Promise<AttachContainerArgs | undefined> {
38
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
39
+ (this as any).launchArgs = { getLaunchArgs: () => this.forwardedArgs };
40
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
41
+ (this as any).connectionProvider = {
42
+ getAttachContainerArgs: async () => {
43
+ this.backendCalls++;
44
+ return this.backendArgs;
45
+ }
46
+ };
47
+ return this.resolveAttachArgs();
48
+ }
49
+ }
50
+
51
+ describe('DevContainerStartupContribution#resolveAttachArgs', () => {
52
+
53
+ before(() => disableJSDOM = enableJSDOM());
54
+ after(() => disableJSDOM());
55
+
56
+ let contribution: TestDevContainerStartupContribution;
57
+
58
+ beforeEach(() => {
59
+ contribution = new TestDevContainerStartupContribution();
60
+ });
61
+
62
+ it('reads the container from the backend on a cold-start window (no forwarded options)', async () => {
63
+ contribution.forwardedArgs = undefined;
64
+ contribution.backendArgs = { containerId: 'A', scanForDevJson: true };
65
+
66
+ const result = await contribution.resolve();
67
+
68
+ expect(result).to.deep.equal({ containerId: 'A', scanForDevJson: true });
69
+ expect(contribution.backendCalls).to.equal(1);
70
+ });
71
+
72
+ it('reads the container from the forwarded options on a second-instance window', async () => {
73
+ contribution.forwardedArgs = launchArgs({ 'attach-container': ['B'] });
74
+ contribution.backendArgs = { containerId: 'A', scanForDevJson: true };
75
+
76
+ const result = await contribution.resolve();
77
+
78
+ expect(result).to.deep.equal({ containerId: 'B', scanForDevJson: true });
79
+ // The stale backend value (from the first launch) must not be consulted.
80
+ expect(contribution.backendCalls).to.equal(0);
81
+ });
82
+
83
+ it('honors --no-dev-json in the forwarded options', async () => {
84
+ contribution.forwardedArgs = { values: { 'attach-container': ['B'] }, negated: ['dev-json'] };
85
+
86
+ const result = await contribution.resolve();
87
+
88
+ expect(result).to.deep.equal({ containerId: 'B', scanForDevJson: false });
89
+ });
90
+
91
+ it('does not attach for a second-instance window without --attach-container', async () => {
92
+ contribution.forwardedArgs = launchArgs({ 'session-preference': ['foo=1'] });
93
+ contribution.backendArgs = { containerId: 'A', scanForDevJson: true };
94
+
95
+ const result = await contribution.resolve();
96
+
97
+ expect(result).to.be.undefined;
98
+ expect(contribution.backendCalls).to.equal(0);
99
+ });
100
+ });
101
+
102
+ class AttachHarness extends DevContainerStartupContribution {
103
+ stages: string[] = [];
104
+ errors: Array<{ message: string, actions: AttachScreenErrorActions }> = [];
105
+ disposeCount = 0;
106
+ showCount = 0;
107
+ openRemoteCalls: string[] = [];
108
+ protected statusListener: ((message: string) => void) | undefined;
109
+
110
+ setup(opts: { containers?: RunningContainerInfo[], attachResult?: ContainerConnectionResult | Error, statusDuringAttach?: string }): void {
111
+ /* eslint-disable @typescript-eslint/no-explicit-any */
112
+ (this as any).logger = { info: () => { }, warn: () => { }, error: () => { } };
113
+ (this as any).attachScreen = {
114
+ showAttaching: () => { this.showCount++; },
115
+ reportStage: (message: string) => this.stages.push(message),
116
+ reportError: (message: string, actions: AttachScreenErrorActions) => this.errors.push({ message, actions }),
117
+ dispose: () => { this.disposeCount++; }
118
+ };
119
+ (this as any).containerOutputProvider = {
120
+ onDidReportStatus: (cb: (message: string) => void) => {
121
+ this.statusListener = cb;
122
+ return { dispose: () => { this.statusListener = undefined; } };
123
+ }
124
+ };
125
+ (this as any).remotePreferences = {};
126
+ (this as any).remoteCliArgsCollector = { collect: async () => [] };
127
+ (this as any).connectionProvider = {
128
+ listRunningContainers: async () => opts.containers ?? [],
129
+ getWorkspaceCandidates: async () => [],
130
+ scanForDevContainerConfig: async () => undefined,
131
+ attachToContainer: async () => {
132
+ if (opts.statusDuringAttach) {
133
+ this.statusListener?.(opts.statusDuringAttach);
134
+ }
135
+ if (opts.attachResult instanceof Error) {
136
+ throw opts.attachResult;
137
+ }
138
+ return opts.attachResult!;
139
+ }
140
+ };
141
+ /* eslint-enable @typescript-eslint/no-explicit-any */
142
+ }
143
+
144
+ protected override openRemote(port: string): void {
145
+ this.openRemoteCalls.push(port);
146
+ }
147
+
148
+ run(args: AttachContainerArgs): Promise<void> {
149
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
150
+ return (this as any).runStartupAttach(args);
151
+ }
152
+ }
153
+
154
+ describe('DevContainerStartupContribution#runStartupAttach', () => {
155
+
156
+ before(() => disableJSDOM = enableJSDOM());
157
+ after(() => disableJSDOM());
158
+
159
+ const runningB: RunningContainerInfo = { id: 'abc123def456', name: 'B', image: 'img', status: 'running', created: 0 };
160
+
161
+ it('reloads into the container on success without showing an error', async () => {
162
+ const harness = new AttachHarness();
163
+ harness.setup({ containers: [runningB], attachResult: { port: '9000', workspacePath: '/w', containerId: 'abc123def456' } });
164
+
165
+ await harness.run({ containerId: 'B', scanForDevJson: false });
166
+
167
+ expect(harness.openRemoteCalls).to.deep.equal(['9000']);
168
+ expect(harness.errors).to.have.lengthOf(0);
169
+ expect(harness.showCount).to.be.greaterThan(0);
170
+ });
171
+
172
+ it('forwards backend status messages to the attach screen', async () => {
173
+ const harness = new AttachHarness();
174
+ harness.setup({
175
+ containers: [runningB],
176
+ attachResult: { port: '9000', workspacePath: '/w', containerId: 'abc123def456' },
177
+ statusDuringAttach: 'Starting application on remote...'
178
+ });
179
+
180
+ await harness.run({ containerId: 'B', scanForDevJson: false });
181
+
182
+ expect(harness.stages).to.include('Starting application on remote...');
183
+ });
184
+
185
+ it('shows an error with retry/close when the container is not found, without reloading', async () => {
186
+ const harness = new AttachHarness();
187
+ harness.setup({ containers: [] });
188
+
189
+ await harness.run({ containerId: 'missing', scanForDevJson: false });
190
+
191
+ expect(harness.openRemoteCalls).to.have.lengthOf(0);
192
+ expect(harness.errors).to.have.lengthOf(1);
193
+ expect(harness.errors[0].message).to.match(/not found/i);
194
+ });
195
+
196
+ it('retry re-runs the attach; close dismisses the screen', async () => {
197
+ const harness = new AttachHarness();
198
+ harness.setup({ containers: [] });
199
+
200
+ await harness.run({ containerId: 'missing', scanForDevJson: false });
201
+ expect(harness.errors).to.have.lengthOf(1);
202
+
203
+ await harness.errors[0].actions.retry();
204
+ expect(harness.errors).to.have.lengthOf(2);
205
+
206
+ harness.errors[1].actions.close();
207
+ expect(harness.disposeCount).to.be.greaterThan(0);
208
+ });
209
+ });
210
+
211
+ class StartupHarness extends DevContainerStartupContribution {
212
+ errors: string[] = [];
213
+
214
+ setup(): void {
215
+ /* eslint-disable @typescript-eslint/no-explicit-any */
216
+ (this as any).logger = { info: () => { }, warn: () => { }, error: () => { } };
217
+ (this as any).launchArgs = { getLaunchArgs: () => undefined };
218
+ (this as any).attachScreen = { isAttached: false, dispose: () => { } };
219
+ (this as any).messageService = { error: (message: string) => this.errors.push(message) };
220
+ (this as any).connectionProvider = {
221
+ getAttachContainerArgs: async () => { throw new Error('backend unreachable'); }
222
+ };
223
+ /* eslint-enable @typescript-eslint/no-explicit-any */
224
+ }
225
+
226
+ run(): Promise<void> {
227
+ return this.handleStartupAttach();
228
+ }
229
+ }
230
+
231
+ describe('DevContainerStartupContribution#handleStartupAttach', () => {
232
+
233
+ before(() => disableJSDOM = enableJSDOM());
234
+ after(() => disableJSDOM());
235
+
236
+ it('reports a failure to resolve the attach arguments on a cold start', async () => {
237
+ const harness = new StartupHarness();
238
+ harness.setup();
239
+
240
+ await harness.run();
241
+
242
+ expect(harness.errors).to.have.lengthOf(1);
243
+ expect(harness.errors[0]).to.contain('backend unreachable');
244
+ });
245
+ });
@@ -16,10 +16,15 @@
16
16
 
17
17
  import { inject, injectable, named } from '@theia/core/shared/inversify';
18
18
  import { FrontendApplicationContribution } from '@theia/core/lib/browser';
19
- import { RemoteContainerConnectionProvider } from '../electron-common/remote-container-connection-provider';
19
+ import { AttachContainerArgs, RemoteContainerConnectionProvider } from '../electron-common/remote-container-connection-provider';
20
20
  import { AbstractRemoteRegistryContribution } from '@theia/remote/lib/electron-browser/remote-registry-contribution';
21
21
  import { ILogger, MessageService, nls } from '@theia/core';
22
+ import { LaunchArguments } from '@theia/core/lib/common/launch-arguments';
23
+ import { WindowLaunchArgs } from '@theia/core/lib/browser/window/window-launch-args';
22
24
  import { RemotePreferences } from '@theia/remote/lib/electron-common/remote-preferences';
25
+ import { ContainerOutputProvider } from './container-output-provider';
26
+ import { DevContainerAttachScreen } from './dev-container-attach-screen';
27
+ import { RemoteCliArgsCollector } from '@theia/remote/lib/electron-browser/remote-cli-args-collector';
23
28
 
24
29
  @injectable()
25
30
  export class DevContainerStartupContribution extends AbstractRemoteRegistryContribution implements FrontendApplicationContribution {
@@ -36,23 +41,71 @@ export class DevContainerStartupContribution extends AbstractRemoteRegistryContr
36
41
  @inject(RemotePreferences)
37
42
  protected readonly remotePreferences: RemotePreferences;
38
43
 
44
+ @inject(RemoteCliArgsCollector)
45
+ protected readonly remoteCliArgsCollector: RemoteCliArgsCollector;
46
+
47
+ @inject(ContainerOutputProvider)
48
+ protected readonly containerOutputProvider: ContainerOutputProvider;
49
+
50
+ @inject(DevContainerAttachScreen)
51
+ protected readonly attachScreen: DevContainerAttachScreen;
52
+
53
+ @inject(WindowLaunchArgs)
54
+ protected readonly launchArgs: WindowLaunchArgs;
55
+
39
56
  registerRemoteCommands(): void {
40
57
  // no commands to register — this contribution only handles startup
41
58
  }
42
59
 
43
60
  onStart(): void {
61
+ // For a CLI-driven attach the window is opened empty and reloads into the container once the
62
+ // attach completes. Show the "attaching" screen from the first paint so the user never
63
+ // interacts with the transient local window, before the (possibly RPC-bound) container id is
64
+ // even resolved.
65
+ if (this.isAttachPending()) {
66
+ this.attachScreen.showAttaching();
67
+ }
44
68
  this.handleStartupAttach();
45
69
  }
46
70
 
47
71
  protected async handleStartupAttach(): Promise<void> {
72
+ let args: AttachContainerArgs | undefined;
48
73
  try {
49
- const args = await this.connectionProvider.getAttachContainerArgs();
50
- if (!args) {
51
- return;
74
+ args = await this.resolveAttachArgs();
75
+ } catch (e) {
76
+ // A rejection here (e.g. failing to redeem the launch args or reach the backend) must not
77
+ // escape as an unhandled rejection and leave the attach screen stuck on "Preparing to attach".
78
+ this.logger.error('CLI: Failed to resolve attach arguments during startup:', e);
79
+ const message = e instanceof Error ? e.message : String(e);
80
+ if (this.isAttachPending()) {
81
+ this.attachScreen.reportError(message, {
82
+ retry: () => this.handleStartupAttach(),
83
+ close: () => this.disposeAttachScreen()
84
+ });
85
+ } else {
86
+ // No attach screen is up, so fall back to a notification.
87
+ this.disposeAttachScreen();
88
+ this.messageService.error(nls.localize('theia/remote/dev-container/cliAttachError', 'Failed to attach to container: {0}', message));
52
89
  }
90
+ return;
91
+ }
92
+ if (!args) {
93
+ // Not attaching after all (e.g. a second-instance window without --attach-container):
94
+ // make sure a preliminary screen is not left up.
95
+ this.disposeAttachScreen();
96
+ return;
97
+ }
98
+ await this.runStartupAttach(args);
99
+ }
53
100
 
54
- const { containerId, scanForDevJson } = args;
101
+ protected async runStartupAttach(args: AttachContainerArgs): Promise<void> {
102
+ const { containerId, scanForDevJson } = args;
103
+ this.attachScreen.showAttaching(containerId);
104
+ // Surface the backend's live status messages (RemoteStatusReport) on the attach screen.
105
+ const statusSubscription = this.containerOutputProvider.onDidReportStatus(message => this.attachScreen.reportStage(message));
106
+ try {
55
107
  this.logger.info(`CLI: --attach-container ${containerId}, initiating attach from frontend...`);
108
+ this.attachScreen.reportStage(nls.localize('theia/remote/dev-container/attachScreen/locating', 'Locating container {0}...', containerId));
56
109
 
57
110
  const containers = await this.connectionProvider.listRunningContainers();
58
111
  // Match by ID prefix (either direction — user may pass a short prefix or a full 64-char ID
@@ -64,13 +117,11 @@ export class DevContainerStartupContribution extends AbstractRemoteRegistryContr
64
117
  const target = matches[0];
65
118
 
66
119
  if (!target) {
67
- const msg = nls.localize('theia/remote/dev-container/cliContainerNotFound',
68
- 'Container "{0}" not found or not running.', containerId);
69
- this.logger.error(`CLI: ${msg}`);
70
- this.messageService.error(msg);
71
- return;
120
+ throw new Error(nls.localize('theia/remote/dev-container/cliContainerNotFound',
121
+ 'Container "{0}" not found or not running.', containerId));
72
122
  }
73
123
 
124
+ this.attachScreen.reportStage(nls.localize('theia/remote/dev-container/attachScreen/preparingWorkspace', 'Preparing workspace...'));
74
125
  const candidates = await this.connectionProvider.getWorkspaceCandidates(target.id);
75
126
  const workspacePath = candidates.length > 0 ? candidates[0].path : '/';
76
127
 
@@ -78,22 +129,69 @@ export class DevContainerStartupContribution extends AbstractRemoteRegistryContr
78
129
  ? await this.connectionProvider.scanForDevContainerConfig(target.id, workspacePath)
79
130
  : undefined;
80
131
 
132
+ this.attachScreen.reportStage(nls.localize('theia/remote/dev-container/attachScreen/starting', 'Starting the application inside the container...'));
81
133
  const result = await this.connectionProvider.attachToContainer({
82
134
  containerId: target.id,
83
135
  workspacePath,
84
136
  devcontainerFile,
85
137
  nodeDownloadTemplate: this.remotePreferences['remote.nodeDownloadTemplate'],
138
+ additionalArgs: await this.remoteCliArgsCollector.collect(),
86
139
  });
87
140
 
88
141
  this.logger.info(`CLI: startup attach ready, proxy on port ${result.port}, workspace: ${result.workspacePath}`);
142
+ // Reloads the window into the container; the attach screen is torn down by the navigation.
89
143
  this.openRemote(result.port, false, result.workspacePath);
90
144
  } catch (e) {
91
145
  this.logger.error('CLI: Failed to attach to container during startup:', e);
92
- this.messageService.error(nls.localize(
93
- 'theia/remote/dev-container/cliAttachError',
94
- 'Failed to attach to container: {0}',
95
- e instanceof Error ? e.message : String(e)
96
- ));
146
+ const message = e instanceof Error ? e.message : String(e);
147
+ this.attachScreen.reportError(message, {
148
+ retry: () => this.runStartupAttach(args),
149
+ close: () => this.attachScreen.dispose()
150
+ });
151
+ } finally {
152
+ statusSubscription.dispose();
97
153
  }
98
154
  }
155
+
156
+ /**
157
+ * Whether this window was opened for a CLI-driven attach. Decided from the window's own launch
158
+ * options, which are available synchronously from the first paint, rather than from a flag in
159
+ * the URL: only `@theia/dev-container` knows what `--attach-container` means.
160
+ */
161
+ protected isAttachPending(): boolean {
162
+ const args = this.launchArgs.getLaunchArgs();
163
+ return args !== undefined && LaunchArguments.lastValue(args, 'attach-container') !== undefined;
164
+ }
165
+
166
+ /**
167
+ * Disposes the attach screen, but only when it is actually on screen. The screen is a singleton,
168
+ * so disposing it on the many windows that never attach would leave a disposed widget behind
169
+ * that a later {@link DevContainerAttachScreen.showAttaching} could no longer attach.
170
+ */
171
+ protected disposeAttachScreen(): void {
172
+ if (this.attachScreen.isAttached) {
173
+ this.attachScreen.dispose();
174
+ }
175
+ }
176
+
177
+ /**
178
+ * Resolves the container to attach to on startup.
179
+ *
180
+ * A forwarded (second-instance) launch carries its CLI options, provided by the trusted main
181
+ * process. For such windows those options are the only source, because the shared backend still
182
+ * reflects the original cold-start launch and cannot distinguish between windows. A cold-start
183
+ * window has no forwarded options and reads them from the backend instead.
184
+ */
185
+ protected async resolveAttachArgs(): Promise<AttachContainerArgs | undefined> {
186
+ const forwarded = this.launchArgs.getLaunchArgs();
187
+ if (forwarded !== undefined) {
188
+ const containerId = LaunchArguments.lastValue(forwarded, 'attach-container');
189
+ if (!containerId) {
190
+ return undefined;
191
+ }
192
+ return { containerId, scanForDevJson: !LaunchArguments.isNegated(forwarded, 'dev-json') };
193
+ }
194
+ return this.connectionProvider.getAttachContainerArgs();
195
+ }
196
+
99
197
  }
@@ -0,0 +1,75 @@
1
+ /********************************************************************************
2
+ * Copyright (C) 2026 EclipseSource GmbH 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
+ .theia-dev-container-attach-screen {
18
+ position: fixed;
19
+ inset: 0;
20
+ z-index: 10000;
21
+ display: flex;
22
+ align-items: center;
23
+ justify-content: center;
24
+ background-color: var(--theia-editor-background);
25
+ color: var(--theia-foreground);
26
+ font-family: var(--theia-ui-font-family);
27
+ font-size: var(--theia-ui-font-size1);
28
+ }
29
+
30
+ .theia-dev-container-attach-screen-content {
31
+ display: flex;
32
+ flex-direction: column;
33
+ align-items: center;
34
+ text-align: center;
35
+ max-width: 480px;
36
+ padding: calc(var(--theia-ui-padding) * 4);
37
+ }
38
+
39
+ .theia-dev-container-attach-screen-logo {
40
+ max-width: 240px;
41
+ max-height: 96px;
42
+ margin-bottom: calc(var(--theia-ui-padding) * 4);
43
+ }
44
+
45
+ .theia-dev-container-attach-screen-spinner {
46
+ font-size: calc(var(--theia-ui-font-size1) * 2.5);
47
+ color: var(--theia-icon-foreground);
48
+ margin-bottom: calc(var(--theia-ui-padding) * 3);
49
+ }
50
+
51
+ .theia-dev-container-attach-screen-title {
52
+ font-size: var(--theia-ui-font-size3);
53
+ font-weight: 600;
54
+ margin: 0 0 var(--theia-ui-padding) 0;
55
+ }
56
+
57
+ .theia-dev-container-attach-screen-stage {
58
+ margin-bottom: calc(var(--theia-ui-padding) * 2);
59
+ }
60
+
61
+ .theia-dev-container-attach-screen-note {
62
+ color: var(--theia-descriptionForeground);
63
+ font-size: var(--theia-ui-font-size0);
64
+ }
65
+
66
+ .theia-dev-container-attach-screen-alert {
67
+ margin-bottom: calc(var(--theia-ui-padding) * 3);
68
+ text-align: left;
69
+ }
70
+
71
+ .theia-dev-container-attach-screen-actions {
72
+ display: flex;
73
+ justify-content: center;
74
+ gap: calc(var(--theia-ui-padding) * 2);
75
+ }
@@ -16,4 +16,10 @@
16
16
 
17
17
  export interface ContainerOutputProvider {
18
18
  onRemoteOutput(output: string): void;
19
+ /**
20
+ * Receives curated status messages (from {@link RemoteStatusReport}) describing the current
21
+ * attach phase, e.g. "Starting application on remote...". Used to drive the "attaching" screen.
22
+ * Optional so that existing implementers of this interface keep compiling.
23
+ */
24
+ onRemoteStatus?(message: string): void;
19
25
  }
@@ -27,6 +27,11 @@ export interface ContainerConnectionOptions {
27
27
  lastContainerInfo?: LastContainerInfo
28
28
  devcontainerFile: string;
29
29
  workspacePath?: string;
30
+ /**
31
+ * Additional CLI arguments to pass to the container's remote backend, e.g. the
32
+ * `--session-preference` values forwarded to a second-instance window.
33
+ */
34
+ additionalArgs?: string[];
30
35
  }
31
36
 
32
37
  export interface LastContainerInfo {
@@ -64,6 +69,11 @@ export interface AttachContainerOptions {
64
69
  workspacePath: string;
65
70
  nodeDownloadTemplate?: string;
66
71
  devcontainerFile?: string;
72
+ /**
73
+ * Additional CLI arguments to pass to the container's remote backend, e.g. the
74
+ * `--session-preference` values forwarded to a second-instance window.
75
+ */
76
+ additionalArgs?: string[];
67
77
  }
68
78
 
69
79
  export interface AttachContainerArgs {
@@ -0,0 +1,34 @@
1
+ // *****************************************************************************
2
+ // Copyright (C) 2026 EclipseSource GmbH 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 { injectable } from '@theia/core/shared/inversify';
18
+ import { ElectronMainApplicationContribution } from '@theia/core/lib/electron-main/electron-main-application';
19
+ import { LaunchArguments } from '@theia/core/lib/common/launch-arguments';
20
+
21
+ /**
22
+ * Claims windows opened for a CLI `--attach-container` launch. Such a window is opened empty and
23
+ * reloads into the container once the attach completes; claiming it lets core open an empty window
24
+ * (instead of restoring the last workspace) and flag it so the dev-container frontend can show its
25
+ * "attaching" screen from the first paint. Keeps the knowledge of `--attach-container` in
26
+ * `@theia/dev-container` rather than in core.
27
+ */
28
+ @injectable()
29
+ export class DevContainerClaimContribution implements ElectronMainApplicationContribution {
30
+
31
+ claimsWindow(args: LaunchArguments): boolean {
32
+ return LaunchArguments.lastValue(args, 'attach-container') !== undefined;
33
+ }
34
+ }
@@ -0,0 +1,24 @@
1
+ // *****************************************************************************
2
+ // Copyright (C) 2026 EclipseSource GmbH 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 { ContainerModule } from '@theia/core/shared/inversify';
18
+ import { ElectronMainApplicationContribution } from '@theia/core/lib/electron-main/electron-main-application';
19
+ import { DevContainerClaimContribution } from './dev-container-claim-contribution';
20
+
21
+ export default new ContainerModule(bind => {
22
+ bind(DevContainerClaimContribution).toSelf().inSingletonScope();
23
+ bind(ElectronMainApplicationContribution).toService(DevContainerClaimContribution);
24
+ });
@@ -255,7 +255,7 @@ export class DevContainerConnectionProvider implements RemoteContainerConnection
255
255
 
256
256
  report('Connecting to remote system...');
257
257
 
258
- const result = await this.setupRemoteConnection(container, docker, devContainerConfig, options.nodeDownloadTemplate, report);
258
+ const result = await this.setupRemoteConnection(container, docker, devContainerConfig, options.nodeDownloadTemplate, report, options.additionalArgs);
259
259
  remote = result.remote;
260
260
 
261
261
  await this.containerService.postConnect(options.devcontainerFile, remote, this.outputProvider, context);
@@ -296,7 +296,11 @@ export class DevContainerConnectionProvider implements RemoteContainerConnection
296
296
  async attachToContainer(options: AttachContainerOptions): Promise<ContainerConnectionResult> {
297
297
  const progress = await this.messageService.showProgress({ text: 'Attaching to container' });
298
298
  try {
299
- const report: RemoteStatusReport = message => progress.report({ message });
299
+ const report: RemoteStatusReport = message => {
300
+ progress.report({ message });
301
+ // Mirror the status to the frontend so the CLI "attaching" screen can show live progress.
302
+ this.outputProvider?.onRemoteStatus?.(message);
303
+ };
300
304
  const docker = await this.createDockerConnection();
301
305
  const container = docker.getContainer(options.containerId);
302
306
 
@@ -318,7 +322,7 @@ export class DevContainerConnectionProvider implements RemoteContainerConnection
318
322
  try {
319
323
  report('Connecting to remote system...');
320
324
 
321
- const result = await this.setupRemoteConnection(container, docker, config, options.nodeDownloadTemplate, report);
325
+ const result = await this.setupRemoteConnection(container, docker, config, options.nodeDownloadTemplate, report, options.additionalArgs);
322
326
  remote = result.remote;
323
327
 
324
328
  if (options.devcontainerFile) {
@@ -378,7 +382,7 @@ export class DevContainerConnectionProvider implements RemoteContainerConnection
378
382
  */
379
383
  protected async setupRemoteConnection(
380
384
  container: Docker.Container, docker: Docker, config: DevContainerConfiguration,
381
- nodeDownloadTemplate: string | undefined, report: RemoteStatusReport
385
+ nodeDownloadTemplate: string | undefined, report: RemoteStatusReport, additionalArgs?: string[]
382
386
  ): Promise<{ localPort: number; remote: RemoteDockerContainerConnection }> {
383
387
  const remote = new RemoteDockerContainerConnection({
384
388
  id: generateUuid(),
@@ -395,7 +399,8 @@ export class DevContainerConnectionProvider implements RemoteContainerConnection
395
399
  result = await this.remoteSetup.setup({
396
400
  connection: remote,
397
401
  report,
398
- nodeDownloadTemplate
402
+ nodeDownloadTemplate,
403
+ additionalArgs
399
404
  });
400
405
  } catch (e) {
401
406
  remote.dispose();