@xxmachina/components 19.51.0 → 19.81.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 (118) hide show
  1. package/fesm2022/xxmachina-components-extras-flow.mjs +73 -140
  2. package/fesm2022/xxmachina-components-extras-flow.mjs.map +1 -1
  3. package/fesm2022/xxmachina-components-features-query.mjs +3 -29
  4. package/fesm2022/xxmachina-components-features-query.mjs.map +1 -1
  5. package/fesm2022/xxmachina-components-groups-query-form.mjs +0 -54
  6. package/fesm2022/xxmachina-components-groups-query-form.mjs.map +1 -1
  7. package/fesm2022/xxmachina-components-molecules-inline-edit-field.mjs +0 -1
  8. package/fesm2022/xxmachina-components-molecules-inline-edit-field.mjs.map +1 -1
  9. package/fesm2022/xxmachina-components-molecules-user-avatar-menu.mjs +96 -0
  10. package/fesm2022/xxmachina-components-molecules-user-avatar-menu.mjs.map +1 -0
  11. package/fesm2022/xxmachina-components-molecules-weekly-header.mjs +2 -2
  12. package/fesm2022/xxmachina-components-molecules-workspace-selector.mjs +123 -0
  13. package/fesm2022/xxmachina-components-molecules-workspace-selector.mjs.map +1 -0
  14. package/fesm2022/xxmachina-components-organisms-calendar-section.mjs +2 -2
  15. package/fesm2022/xxmachina-components-organisms-calendar-section.mjs.map +1 -1
  16. package/fesm2022/xxmachina-components-organisms-event-input-section.mjs +0 -1
  17. package/fesm2022/xxmachina-components-organisms-event-input-section.mjs.map +1 -1
  18. package/fesm2022/xxmachina-components-organisms-terminal-input-section.mjs +2 -18
  19. package/fesm2022/xxmachina-components-organisms-terminal-input-section.mjs.map +1 -1
  20. package/fesm2022/xxmachina-components-organisms-video-trimmer-section.mjs +0 -3
  21. package/fesm2022/xxmachina-components-organisms-video-trimmer-section.mjs.map +1 -1
  22. package/fesm2022/xxmachina-components-organisms-xterm.mjs +283 -261
  23. package/fesm2022/xxmachina-components-organisms-xterm.mjs.map +1 -1
  24. package/fesm2022/xxmachina-components-pages-command.mjs +3 -9
  25. package/fesm2022/xxmachina-components-pages-command.mjs.map +1 -1
  26. package/fesm2022/xxmachina-components-pages-query-v2.mjs +0 -13
  27. package/fesm2022/xxmachina-components-pages-query-v2.mjs.map +1 -1
  28. package/fesm2022/xxmachina-components-pages-query.mjs +3 -67
  29. package/fesm2022/xxmachina-components-pages-query.mjs.map +1 -1
  30. package/fesm2022/xxmachina-components-pages-thread.mjs +2 -18
  31. package/fesm2022/xxmachina-components-pages-thread.mjs.map +1 -1
  32. package/fesm2022/{xxmachina-components-routes-commands-command.routes-CSwIdXZg.mjs → xxmachina-components-routes-commands-command.routes-Ca2yVrwL.mjs} +1 -2
  33. package/fesm2022/xxmachina-components-routes-commands-command.routes-Ca2yVrwL.mjs.map +1 -0
  34. package/fesm2022/{xxmachina-components-routes-commands-pages.routes-j2xVop4W.mjs → xxmachina-components-routes-commands-pages.routes-COpFeEsz.mjs} +2 -2
  35. package/fesm2022/{xxmachina-components-routes-commands-pages.routes-j2xVop4W.mjs.map → xxmachina-components-routes-commands-pages.routes-COpFeEsz.mjs.map} +1 -1
  36. package/fesm2022/xxmachina-components-routes-commands.mjs +1 -2
  37. package/fesm2022/xxmachina-components-routes-commands.mjs.map +1 -1
  38. package/fesm2022/xxmachina-components-routes-pages.mjs +0 -1
  39. package/fesm2022/xxmachina-components-routes-pages.mjs.map +1 -1
  40. package/fesm2022/xxmachina-components-routes-queries.mjs +0 -2
  41. package/fesm2022/xxmachina-components-routes-queries.mjs.map +1 -1
  42. package/fesm2022/{xxmachina-components-routes-query-pages.routes-CxTbOJDQ.mjs → xxmachina-components-routes-query-pages.routes-BNuONo3g.mjs} +1 -5
  43. package/fesm2022/xxmachina-components-routes-query-pages.routes-BNuONo3g.mjs.map +1 -0
  44. package/fesm2022/xxmachina-components-routes-query.mjs +1 -1
  45. package/fesm2022/{xxmachina-components-routes-resources-pages.routes-CRXxavHb.mjs → xxmachina-components-routes-resources-pages.routes-DEu2qpeu.mjs} +2 -2
  46. package/fesm2022/{xxmachina-components-routes-resources-pages.routes-CRXxavHb.mjs.map → xxmachina-components-routes-resources-pages.routes-DEu2qpeu.mjs.map} +1 -1
  47. package/fesm2022/{xxmachina-components-routes-resources-resource.routes-CkkmqB2_.mjs → xxmachina-components-routes-resources-resource.routes-CWxi2agj.mjs} +1 -2
  48. package/fesm2022/xxmachina-components-routes-resources-resource.routes-CWxi2agj.mjs.map +1 -0
  49. package/fesm2022/xxmachina-components-routes-resources.mjs +1 -1
  50. package/fesm2022/xxmachina-components-routes-transformer.mjs +0 -1
  51. package/fesm2022/xxmachina-components-routes-transformer.mjs.map +1 -1
  52. package/fesm2022/xxmachina-components-services-forms-query.mjs +1 -1
  53. package/fesm2022/xxmachina-components-services-forms-query.mjs.map +1 -1
  54. package/fesm2022/xxmachina-components-services-graphql.mjs +0 -4
  55. package/fesm2022/xxmachina-components-services-graphql.mjs.map +1 -1
  56. package/fesm2022/xxmachina-components-services-json-schema.mjs +6 -12
  57. package/fesm2022/xxmachina-components-services-json-schema.mjs.map +1 -1
  58. package/fesm2022/xxmachina-components-services-query.mjs +0 -7
  59. package/fesm2022/xxmachina-components-services-query.mjs.map +1 -1
  60. package/fesm2022/xxmachina-components-services-recording.mjs +0 -13
  61. package/fesm2022/xxmachina-components-services-recording.mjs.map +1 -1
  62. package/fesm2022/xxmachina-components-services-thread.mjs +0 -2
  63. package/fesm2022/xxmachina-components-services-thread.mjs.map +1 -1
  64. package/fesm2022/xxmachina-components-services-version.mjs +171 -0
  65. package/fesm2022/xxmachina-components-services-version.mjs.map +1 -0
  66. package/fesm2022/xxmachina-components-services-workspace-switcher.mjs +9 -0
  67. package/fesm2022/xxmachina-components-services-workspace-switcher.mjs.map +1 -0
  68. package/fesm2022/xxmachina-components-templates-agent.mjs +98 -146
  69. package/fesm2022/xxmachina-components-templates-agent.mjs.map +1 -1
  70. package/fesm2022/xxmachina-components-templates-background.mjs +10 -30
  71. package/fesm2022/xxmachina-components-templates-background.mjs.map +1 -1
  72. package/fesm2022/xxmachina-components-templates-env.mjs +295 -0
  73. package/fesm2022/xxmachina-components-templates-env.mjs.map +1 -0
  74. package/fesm2022/xxmachina-components-templates-flow-nodes-issue.mjs.map +1 -1
  75. package/fesm2022/xxmachina-components-templates-flow.mjs +1 -1
  76. package/fesm2022/xxmachina-components-templates-flow.mjs.map +1 -1
  77. package/fesm2022/xxmachina-components-templates-machina-icon-button-menu.mjs +78 -0
  78. package/fesm2022/xxmachina-components-templates-machina-icon-button-menu.mjs.map +1 -0
  79. package/fesm2022/xxmachina-components.mjs +9 -10
  80. package/fesm2022/xxmachina-components.mjs.map +1 -1
  81. package/package.json +25 -1
  82. package/types/xxmachina-components-extras-flow.d.ts +8 -43
  83. package/types/xxmachina-components-extras-flow.d.ts.map +1 -1
  84. package/types/xxmachina-components-features-query.d.ts +3 -24
  85. package/types/xxmachina-components-features-query.d.ts.map +1 -1
  86. package/types/xxmachina-components-groups-query-form.d.ts +2 -2
  87. package/types/xxmachina-components-molecules-user-avatar-menu.d.ts +19 -0
  88. package/types/xxmachina-components-molecules-user-avatar-menu.d.ts.map +1 -0
  89. package/types/xxmachina-components-molecules-workspace-selector.d.ts +25 -0
  90. package/types/xxmachina-components-molecules-workspace-selector.d.ts.map +1 -0
  91. package/types/xxmachina-components-organisms-xterm.d.ts +130 -111
  92. package/types/xxmachina-components-organisms-xterm.d.ts.map +1 -1
  93. package/types/xxmachina-components-pages-query-v2.d.ts +0 -3
  94. package/types/xxmachina-components-pages-query-v2.d.ts.map +1 -1
  95. package/types/xxmachina-components-pages-query.d.ts +0 -19
  96. package/types/xxmachina-components-pages-query.d.ts.map +1 -1
  97. package/types/xxmachina-components-services-forms-query.d.ts +1 -1
  98. package/types/xxmachina-components-services-graphql.d.ts +0 -3
  99. package/types/xxmachina-components-services-graphql.d.ts.map +1 -1
  100. package/types/xxmachina-components-services-query.d.ts +0 -7
  101. package/types/xxmachina-components-services-query.d.ts.map +1 -1
  102. package/types/xxmachina-components-services-version.d.ts +32 -0
  103. package/types/xxmachina-components-services-version.d.ts.map +1 -0
  104. package/types/xxmachina-components-services-workspace-switcher.d.ts +17 -0
  105. package/types/xxmachina-components-services-workspace-switcher.d.ts.map +1 -0
  106. package/types/xxmachina-components-templates-agent.d.ts +2 -1
  107. package/types/xxmachina-components-templates-agent.d.ts.map +1 -1
  108. package/types/xxmachina-components-templates-env.d.ts +43 -0
  109. package/types/xxmachina-components-templates-env.d.ts.map +1 -0
  110. package/types/xxmachina-components-templates-flow-nodes-issue.d.ts +0 -2
  111. package/types/xxmachina-components-templates-flow-nodes-issue.d.ts.map +1 -1
  112. package/types/xxmachina-components-templates-machina-icon-button-menu.d.ts +18 -0
  113. package/types/xxmachina-components-templates-machina-icon-button-menu.d.ts.map +1 -0
  114. package/types/xxmachina-components.d.ts +2 -0
  115. package/types/xxmachina-components.d.ts.map +1 -1
  116. package/fesm2022/xxmachina-components-routes-commands-command.routes-CSwIdXZg.mjs.map +0 -1
  117. package/fesm2022/xxmachina-components-routes-query-pages.routes-CxTbOJDQ.mjs.map +0 -1
  118. package/fesm2022/xxmachina-components-routes-resources-resource.routes-CkkmqB2_.mjs.map +0 -1
@@ -8,10 +8,6 @@ import { makeDI } from '@ng-atomic/common/services/ui';
8
8
  const URL_REGEX = /https?:\/\/[^\s<>'")\[\]]+/g;
9
9
  const LOCALHOST_REGEX = /^https?:\/\/(?:localhost|127\.0\.0\.1)(?::\d+)?(?:\/|$)/;
10
10
  const MAX_SCAN_LINES = 30;
11
- /**
12
- * Link provider that detects URLs spanning multiple terminal lines.
13
- * localhost/127.0.0.1 URLs are skipped (handled by ProxyLinksAddon).
14
- */
15
11
  class WebLinkProvider {
16
12
  _terminal;
17
13
  _handler;
@@ -94,11 +90,6 @@ class WebLinkProvider {
94
90
  }
95
91
  }
96
92
 
97
- /**
98
- * Drop-in replacement for @xterm/addon-web-links that supports
99
- * URLs spanning wrapped terminal lines.
100
- * localhost/127.0.0.1 URLs are skipped (handled by ProxyLinksAddon).
101
- */
102
93
  class WebLinksAddon {
103
94
  _handler;
104
95
  _linkProvider;
@@ -116,7 +107,6 @@ class WebLinksAddon {
116
107
  class GitHubLinkProvider {
117
108
  _terminal;
118
109
  _handler;
119
- // #番号 形式を検出(負の先読みで数字以外が続く場合にマッチ)
120
110
  GITHUB_ISSUE_REGEX = /(?:^|[\s(])#(\d+)(?!\d)/g;
121
111
  constructor(_terminal, _handler) {
122
112
  this._terminal = _terminal;
@@ -137,7 +127,6 @@ class GitHubLinkProvider {
137
127
  const issueNumber = parseInt(match[1], 10);
138
128
  const fullMatch = match[0];
139
129
  const issueRef = `#${match[1]}`;
140
- // マッチ開始位置を計算(プレフィックス文字を除く)
141
130
  const stringStartIndex = match.index + (fullMatch.length - issueRef.length);
142
131
  const [, startX] = this._mapStrIdx(lineIndex, 0, stringStartIndex);
143
132
  const [, endX] = this._mapStrIdx(lineIndex, startX, issueRef.length);
@@ -201,17 +190,12 @@ class GitHubLinksAddon {
201
190
  class FileLinkProvider {
202
191
  _terminal;
203
192
  _handler;
204
- // ファイルパスを検出する正規表現
205
- // - 絶対パス: /path/to/file.ts, /path/to/file.ts:10:5
206
- // - 相対パス: ./src/file.ts, ../lib/utils.ts:20
207
- // - 拡張子を持つファイル名
208
193
  FILE_PATH_REGEX = /(?:^|[\s'"`(])((\.\.?\/|\/)?[\w.@-]+(?:\/[\w.@-]+)*\.[a-zA-Z0-9]+(?::\d+(?::\d+)?)?)/g;
209
194
  constructor(_terminal, _handler) {
210
195
  this._terminal = _terminal;
211
196
  this._handler = _handler;
212
197
  }
213
198
  provideLinks(bufferLineNumber, callback) {
214
- // bufferLineNumber は 1-indexed、buffer.getLine() は 0-indexed を期待
215
199
  const lineIndex = bufferLineNumber - 1;
216
200
  const line = this._terminal.buffer.active.getLine(lineIndex);
217
201
  if (!line) {
@@ -220,15 +204,12 @@ class FileLinkProvider {
220
204
  }
221
205
  const text = line.translateToString(true);
222
206
  const links = [];
223
- // Reset regex lastIndex
224
207
  this.FILE_PATH_REGEX.lastIndex = 0;
225
208
  let match;
226
209
  while ((match = this.FILE_PATH_REGEX.exec(text)) !== null) {
227
210
  const filePath = match[1];
228
211
  if (this._isLikelyFilePath(filePath) && !this._isUrl(filePath)) {
229
- // 文字列インデックスから表示位置(セル位置)を計算
230
212
  const stringStartIndex = match.index + (match[0].length - match[1].length);
231
- // WebLinksAddon と同じ方式で位置をマッピング(0-indexed lineIndexを渡す)
232
213
  const [, startX] = this._mapStrIdx(lineIndex, 0, stringStartIndex);
233
214
  const [, endX] = this._mapStrIdx(lineIndex, startX, filePath.length);
234
215
  if (startX === -1 || endX === -1) {
@@ -237,7 +218,6 @@ class FileLinkProvider {
237
218
  links.push({
238
219
  text: filePath,
239
220
  range: {
240
- // IBufferCellPosition は 1-indexed
241
221
  start: { x: startX + 1, y: bufferLineNumber },
242
222
  end: { x: endX, y: bufferLineNumber }
243
223
  },
@@ -250,12 +230,6 @@ class FileLinkProvider {
250
230
  }
251
231
  callback(links.length > 0 ? links : undefined);
252
232
  }
253
- /**
254
- * Map a string index back to buffer positions.
255
- * Returns buffer position as [lineIndex, columnIndex] 0-based,
256
- * or [-1, -1] in case the lookup ran into a non-existing line.
257
- * (Based on xterm.js WebLinkProvider implementation)
258
- */
259
233
  _mapStrIdx(lineIndex, rowIndex, stringIndex) {
260
234
  const buf = this._terminal.buffer.active;
261
235
  const cell = buf.getNullCell();
@@ -282,11 +256,9 @@ class FileLinkProvider {
282
256
  return [lineIndex, start];
283
257
  }
284
258
  _isLikelyFilePath(text) {
285
- // 拡張子を持つか確認
286
259
  return /\.[a-zA-Z0-9]+(?::\d+(?::\d+)?)?$/.test(text);
287
260
  }
288
261
  _isUrl(text) {
289
- // URL形式を除外
290
262
  return /^https?:\/\//.test(text) || /^file:\/\//.test(text);
291
263
  }
292
264
  _parseFilePath(text) {
@@ -301,7 +273,6 @@ class FileLinkProvider {
301
273
  return { path: text };
302
274
  }
303
275
  }
304
- // Export helper functions for testing
305
276
  const isLikelyFilePath = (text) => {
306
277
  return /\.[a-zA-Z0-9]+(?::\d+(?::\d+)?)?$/.test(text) && !/^https?:\/\//.test(text);
307
278
  };
@@ -317,20 +288,6 @@ const parseFilePath = (text) => {
317
288
  return { path: text };
318
289
  };
319
290
 
320
- /**
321
- * xterm.js addon for detecting and handling file path links in terminal output.
322
- * Similar to WebLinksAddon but for local file paths.
323
- *
324
- * Usage:
325
- * ```typescript
326
- * const fileLinksAddon = new FileLinksAddon((event, filePath, line, column) => {
327
- * if (event.metaKey || event.ctrlKey) {
328
- * // Open file in editor
329
- * }
330
- * });
331
- * terminal.loadAddon(fileLinksAddon);
332
- * ```
333
- */
334
291
  class FileLinksAddon {
335
292
  _handler;
336
293
  _terminal;
@@ -347,34 +304,6 @@ class FileLinksAddon {
347
304
  }
348
305
  }
349
306
 
350
- /**
351
- * localhost/127.0.0.1 URLをmachina web proxy URLに変換するリンクプロバイダ。
352
- *
353
- * 変換ルール:
354
- * http://localhost:4200/path → https://{serverUUID}--4200.proxy.machina.at/path
355
- * http://127.0.0.1:3000/api → https://{serverUUID}--3000.proxy.machina.at/api
356
- *
357
- * deployVersion に応じて suffix を付与:
358
- * - 'pr-2464' → https://{uuid}--{port}--pr-2464.proxy.machina.at/...
359
- * - 'v19.10.0-dev.5' 等 → https://{uuid}--{port}--dev.proxy.machina.at/...
360
- * - null / 'stable' → suffix なし
361
- *
362
- * proxy iframe の SW intercept は referrer-based に変更されたため、 top window URL に
363
- * 余分な marker prefix を埋め込む必要はない。 iframe.src は location.pathname をそのまま継承する。
364
- * @see projects/@machina.at/web-proxy/src/app/services/proxy-iframe-url.ts
365
- */
366
- const LOCALHOST_URL_REGEX = /https?:\/\/(?:localhost|127\.0\.0\.1)(?::(\d+))?(\/[^\s<>'")\]]*)?/g;
367
- /**
368
- * deployVersion を proxy.machina.at のサブドメイン suffix に変換。
369
- *
370
- * 変換ルール:
371
- * - null / 'stable' → '' (suffix なし)
372
- * - 'pr-{N}' → '--pr-{N}'
373
- * - 'dev' / 'develop' → '--dev'
374
- * - 'v{X.Y.Z}' / 'v{X.Y.Z-pre.N}' → '--v{X-Y-Z}[--pre-N]' (dot → dash)
375
- * - 'X.Y.Z' (v prefix なし) → 同上
376
- * - その他 → '' (fallback)
377
- */
378
307
  function deployVersionToHostSuffix(version) {
379
308
  if (!version || version === 'stable')
380
309
  return '';
@@ -382,7 +311,6 @@ function deployVersionToHostSuffix(version) {
382
311
  return `--${version}`;
383
312
  if (version === 'dev' || version === 'develop')
384
313
  return '--dev';
385
- // semver format: (v)?{MAJOR}.{MINOR}.{PATCH}[-{pre}.{num}]
386
314
  const match = version.match(/^v?(\d+)\.(\d+)\.(\d+)(?:-([a-z]+)\.(\d+))?$/);
387
315
  if (match) {
388
316
  const [, major, minor, patch, pre, num] = match;
@@ -393,6 +321,43 @@ function deployVersionToHostSuffix(version) {
393
321
  }
394
322
  return '';
395
323
  }
324
+
325
+ function encodeUrlHandoff(token, endpoints, env) {
326
+ if (env) {
327
+ return btoa(JSON.stringify({ t: token, env }));
328
+ }
329
+ return btoa(JSON.stringify({ t: token, e: endpoints }));
330
+ }
331
+ function appendUrlHandoff(proxyUrl, token, endpoints, env) {
332
+ const u = new URL(proxyUrl);
333
+ u.searchParams.set('lt', encodeUrlHandoff(token, endpoints, env));
334
+ return u.toString();
335
+ }
336
+
337
+ function openWebProxyForPort(opts) {
338
+ const { serverUUID, port, path = '/', token, endpoints, env, deployVersion } = opts;
339
+ const handler = opts.handler ?? ((_, url) => { window.open(url, '_blank', 'noopener'); });
340
+ const hostSuffix = deployVersionToHostSuffix(deployVersion);
341
+ const baseProxyUrl = `https://${serverUUID}--${port}${hostSuffix}.proxy.machina.at${path}`;
342
+ if (!token || !endpoints) {
343
+ handler(undefined, baseProxyUrl);
344
+ return;
345
+ }
346
+ const proxyUrlWithHandoff = appendUrlHandoff(baseProxyUrl, token, endpoints, env);
347
+ window.open(proxyUrlWithHandoff, '_blank', 'noopener');
348
+ }
349
+
350
+ function toProxyUrl(serverUUID, localhostUrl, deployVersion) {
351
+ const match = localhostUrl.match(/^https?:\/\/(?:localhost|127\.0\.0\.1)(?::(\d+))?(\/.*)?$/);
352
+ if (!match)
353
+ return null;
354
+ const port = match[1] || '80';
355
+ const path = match[2] || '/';
356
+ const hostSuffix = deployVersionToHostSuffix(deployVersion);
357
+ return `https://${serverUUID}--${port}${hostSuffix}.proxy.machina.at${path}`;
358
+ }
359
+
360
+ const LOCALHOST_URL_REGEX = /https?:\/\/(?:localhost|127\.0\.0\.1)(?::(\d+))?(\/[^\s<>'")\]]*)?/g;
396
361
  class ProxyLinkProvider {
397
362
  _terminal;
398
363
  _getServerUUID;
@@ -400,15 +365,19 @@ class ProxyLinkProvider {
400
365
  _handler;
401
366
  _getDeployVersion;
402
367
  _getEndpoints;
368
+ _onMissingToken;
369
+ _getEnv;
403
370
  constructor(_terminal, _getServerUUID, _getToken, _handler = (_, url) => {
404
371
  window.open(url, '_blank', 'noopener');
405
- }, _getDeployVersion, _getEndpoints) {
372
+ }, _getDeployVersion, _getEndpoints, _onMissingToken, _getEnv) {
406
373
  this._terminal = _terminal;
407
374
  this._getServerUUID = _getServerUUID;
408
375
  this._getToken = _getToken;
409
376
  this._handler = _handler;
410
377
  this._getDeployVersion = _getDeployVersion;
411
378
  this._getEndpoints = _getEndpoints;
379
+ this._onMissingToken = _onMissingToken;
380
+ this._getEnv = _getEnv;
412
381
  }
413
382
  provideLinks(bufferLineNumber, callback) {
414
383
  const lineIndex = bufferLineNumber - 1;
@@ -434,82 +403,60 @@ class ProxyLinkProvider {
434
403
  start: { x: startX, y: bufferLineNumber },
435
404
  end: { x: endX, y: bufferLineNumber },
436
405
  },
437
- // serverUUIDはクリック時にlazy評価(セッション接続後に値が入る)
438
406
  activate: (event) => {
439
407
  const uuid = this._getServerUUID();
440
- const token = this._getToken?.();
441
- const hostSuffix = deployVersionToHostSuffix(this._getDeployVersion?.());
442
- // endpoints は postMessage payload (`proxy-auth-token` の `endpoints` フィールド) で送る。
443
- // web-proxy 側は main.ts で bootstrapApplication 前に await し、
444
- // window.__machinaProxyAuthResult に格納 → Apollo init で sync 読み出しできる。
445
- const proxyUrl = uuid
446
- ? `https://${uuid}--${port}${hostSuffix}.proxy.machina.at${path}`
447
- : null;
448
- const targetUrl = proxyUrl || matchedUrl;
449
- console.log('[proxy-link] activate:', { matchedUrl, uuid, proxyUrl, targetUrl, hasToken: !!token });
450
- if (token && proxyUrl) {
451
- // postMessage方式: openerとしてwindow参照を保持
452
- const proxyOrigin = new URL(proxyUrl).origin;
453
- const win = window.open(targetUrl, '_blank');
454
- if (win) {
455
- const listener = (e) => {
456
- // クロスオリジン WindowProxy の === 比較は信頼できないため origin で判定
457
- if (e.origin === proxyOrigin && e.data?.type === 'proxy-auth-ready') {
458
- console.log('[proxy-link] auth-ready received, sending token');
459
- const endpoints = this._getEndpoints?.();
460
- win.postMessage({ type: 'proxy-auth-token', token, endpoints }, proxyOrigin);
461
- window.removeEventListener('message', listener);
462
- }
463
- };
464
- window.addEventListener('message', listener);
465
- // 10秒後にリスナーをクリーンアップ
466
- setTimeout(() => window.removeEventListener('message', listener), 10000);
467
- }
408
+ if (!uuid) {
409
+ console.warn('[ProxyLinkProvider] serverUUID undefined, opening localhost URL as-is (no proxy):', {
410
+ matchedUrl,
411
+ hasGetToken: !!this._getToken,
412
+ hasGetEndpoints: !!this._getEndpoints,
413
+ });
414
+ this._handler(event, matchedUrl);
415
+ return;
468
416
  }
469
- else {
470
- this._handler(event, targetUrl);
417
+ const token = this._getToken?.();
418
+ const endpoints = this._getEndpoints?.();
419
+ if ((!token || !endpoints) && this._onMissingToken) {
420
+ this._onMissingToken();
421
+ return;
471
422
  }
423
+ openWebProxyForPort({
424
+ serverUUID: uuid,
425
+ port,
426
+ path,
427
+ token,
428
+ endpoints,
429
+ env: this._getEnv?.(),
430
+ deployVersion: this._getDeployVersion?.(),
431
+ handler: (_e, url) => this._handler(event, url),
432
+ });
472
433
  },
473
434
  });
474
435
  }
475
436
  callback(links.length > 0 ? links : undefined);
476
437
  }
477
438
  }
478
- function toProxyUrl(serverUUID, localhostUrl, deployVersion) {
479
- const match = localhostUrl.match(/^https?:\/\/(?:localhost|127\.0\.0\.1)(?::(\d+))?(\/.*)?$/);
480
- if (!match)
481
- return null;
482
- const port = match[1] || '80';
483
- const path = match[2] || '/';
484
- const hostSuffix = deployVersionToHostSuffix(deployVersion);
485
- return `https://${serverUUID}--${port}${hostSuffix}.proxy.machina.at${path}`;
486
- }
487
439
 
488
- /**
489
- * xterm.js addon: localhost URLをmachina web proxyにリダイレクトするリンクを表示。
490
- *
491
- * Usage:
492
- * ```typescript
493
- * const addon = new ProxyLinksAddon(() => currentServerUUID);
494
- * terminal.loadAddon(addon);
495
- * ```
496
- */
497
440
  class ProxyLinksAddon {
498
441
  _getServerUUID;
499
442
  _getToken;
500
443
  _handler;
501
444
  _getDeployVersion;
502
445
  _getEndpoints;
446
+ _onMissingToken;
447
+ _getEnv;
503
448
  _linkProvider;
504
- constructor(_getServerUUID, _getToken, _handler, _getDeployVersion, _getEndpoints) {
449
+ constructor(_getServerUUID, _getToken, _handler, _getDeployVersion, _getEndpoints, _onMissingToken, _getEnv) {
505
450
  this._getServerUUID = _getServerUUID;
506
451
  this._getToken = _getToken;
507
452
  this._handler = _handler;
508
453
  this._getDeployVersion = _getDeployVersion;
509
454
  this._getEndpoints = _getEndpoints;
455
+ this._onMissingToken = _onMissingToken;
456
+ this._getEnv = _getEnv;
510
457
  }
511
458
  activate(terminal) {
512
- this._linkProvider = terminal.registerLinkProvider(new ProxyLinkProvider(terminal, this._getServerUUID, this._getToken, this._handler, this._getDeployVersion, this._getEndpoints));
459
+ this._linkProvider = terminal.registerLinkProvider(new ProxyLinkProvider(terminal, this._getServerUUID, this._getToken, this._handler, this._getDeployVersion, this._getEndpoints, this._onMissingToken, this._getEnv));
513
460
  }
514
461
  dispose() {
515
462
  this._linkProvider?.dispose();
@@ -524,17 +471,7 @@ var XtermActionId;
524
471
  XtermActionId["FILE_LINK_CLICK"] = "xterm:file-link-click";
525
472
  XtermActionId["FOCUS_INPUT_REQUEST"] = "xterm:focus-input-request";
526
473
  })(XtermActionId || (XtermActionId = {}));
527
- /** DI token for providing serverUUID getter to ProxyLinksAddon */
528
- const XTERM_SERVER_UUID = new InjectionToken('XTERM_SERVER_UUID');
529
- /** DI token for providing CLI token getter to ProxyLinksAddon */
530
- const XTERM_PROXY_TOKEN = new InjectionToken('XTERM_PROXY_TOKEN');
531
- /** DI token for providing deploy version getter to ProxyLinksAddon */
532
- const XTERM_DEPLOY_VERSION = new InjectionToken('XTERM_DEPLOY_VERSION');
533
- /**
534
- * DI token for providing API endpoints (apiNext / hasura) to ProxyLinksAddon.
535
- * web-proxy が opener postMessage 経由でこの値を受信して使用する (環境追従)。
536
- */
537
- const XTERM_PROXY_ENDPOINTS = new InjectionToken('XTERM_PROXY_ENDPOINTS');
474
+ const XTERM_SESSION_CONTEXT = new InjectionToken('XTERM_SESSION_CONTEXT');
538
475
  class XtermOrganismStore extends InjectableComponent {
539
476
  static DI = makeDI(XtermOrganismStore, () => () => ({
540
477
  data: '',
@@ -548,14 +485,7 @@ class XtermOrganismStore extends InjectableComponent {
548
485
  queryResult = input('', ...(ngDevMode ? [{ debugName: "queryResult" }] : []));
549
486
  useInteractiveTheme = input(false, ...(ngDevMode ? [{ debugName: "useInteractiveTheme" }] : []));
550
487
  totalWritten = input(0, ...(ngDevMode ? [{ debugName: "totalWritten" }] : []));
551
- /** Injected serverUUID getter for proxy links (optional) */
552
- getServerUUID = inject(XTERM_SERVER_UUID, { optional: true });
553
- /** Injected CLI token getter for proxy link auto-auth (optional) */
554
- getProxyToken = inject(XTERM_PROXY_TOKEN, { optional: true });
555
- /** Injected deploy version getter for proxy URL host suffix (optional) */
556
- getDeployVersion = inject(XTERM_DEPLOY_VERSION, { optional: true });
557
- /** Injected endpoints getter for opener-driven environment switching (optional) */
558
- getProxyEndpoints = inject(XTERM_PROXY_ENDPOINTS, { optional: true });
488
+ sessionContext = inject(XTERM_SESSION_CONTEXT, { optional: true });
559
489
  constructor() {
560
490
  super();
561
491
  XtermOrganismStore.DI.initialize(this);
@@ -568,20 +498,143 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.6", ngImpor
568
498
  args: [{ standalone: true, selector: 'organisms-xterm' }]
569
499
  }], ctorParameters: () => [], propDecorators: { data: [{ type: i0.Input, args: [{ isSignal: true, alias: "data", required: false }] }], interactive: [{ type: i0.Input, args: [{ isSignal: true, alias: "interactive", required: false }] }], queryResult: [{ type: i0.Input, args: [{ isSignal: true, alias: "queryResult", required: false }] }], useInteractiveTheme: [{ type: i0.Input, args: [{ isSignal: true, alias: "useInteractiveTheme", required: false }] }], totalWritten: [{ type: i0.Input, args: [{ isSignal: true, alias: "totalWritten", required: false }] }] } });
570
500
 
571
- /**
572
- * Minimum container size required for terminal initialization.
573
- * Prevents issues with SR-only hidden containers (1x1 pixel) causing FitAddon problems.
574
- */
501
+ const MIN_VIEWPORT_COLS = 2;
502
+ const MIN_VIEWPORT_ROWS = 1;
503
+ const MAX_VIEWPORT_COLS = 500;
504
+ const MAX_VIEWPORT_ROWS = 200;
505
+ const CANONICAL_FONT_FAMILY = '"FiraCode Nerd Font", "Fira Code", "SF Mono", "Cascadia Code", "Consolas", "Courier New", monospace';
506
+ const CANONICAL_FONT_SIZE_PX = 14;
507
+ const CANONICAL_LINE_HEIGHT = 1.2;
508
+ const DEFAULT_CELL_WIDTH = 8.4;
509
+ const DEFAULT_CELL_HEIGHT = CANONICAL_FONT_SIZE_PX * CANONICAL_LINE_HEIGHT;
510
+ function measureCellMetric() {
511
+ try {
512
+ const doc = document;
513
+ if (!doc?.body)
514
+ return { cellWidth: DEFAULT_CELL_WIDTH, cellHeight: DEFAULT_CELL_HEIGHT };
515
+ const probe = doc.createElement('span');
516
+ probe.textContent = 'M';
517
+ probe.style.position = 'absolute';
518
+ probe.style.visibility = 'hidden';
519
+ probe.style.whiteSpace = 'pre';
520
+ probe.style.left = '-9999px';
521
+ probe.style.top = '-9999px';
522
+ probe.style.fontFamily = CANONICAL_FONT_FAMILY;
523
+ probe.style.fontSize = `${CANONICAL_FONT_SIZE_PX}px`;
524
+ probe.style.lineHeight = String(CANONICAL_LINE_HEIGHT);
525
+ doc.body.appendChild(probe);
526
+ const rect = probe.getBoundingClientRect();
527
+ doc.body.removeChild(probe);
528
+ const cellWidth = rect.width >= 3 ? rect.width : DEFAULT_CELL_WIDTH;
529
+ const cellHeight = rect.height >= 8 ? rect.height : DEFAULT_CELL_HEIGHT;
530
+ return { cellWidth, cellHeight };
531
+ }
532
+ catch {
533
+ return { cellWidth: DEFAULT_CELL_WIDTH, cellHeight: DEFAULT_CELL_HEIGHT };
534
+ }
535
+ }
536
+ function computeFitToViewport(input) {
537
+ const { viewportElement, cellWidth, cellHeight } = input;
538
+ if (!cellWidth || !cellHeight || cellWidth <= 0 || cellHeight <= 0)
539
+ return null;
540
+ const w = viewportElement.offsetWidth;
541
+ const h = viewportElement.offsetHeight;
542
+ if (w <= 0 || h <= 0)
543
+ return null;
544
+ return {
545
+ cols: Math.min(MAX_VIEWPORT_COLS, Math.max(MIN_VIEWPORT_COLS, Math.floor(w / cellWidth))),
546
+ rows: Math.min(MAX_VIEWPORT_ROWS, Math.max(MIN_VIEWPORT_ROWS, Math.floor(h / cellHeight))),
547
+ };
548
+ }
549
+
550
+ let cached = null;
551
+ function clearCanvasCellMetricCache() {
552
+ cached = null;
553
+ }
554
+ function recordCanvasCellMetric(metric) {
555
+ if (cached)
556
+ return;
557
+ if (metric.cellWidth > 0 && metric.cellHeight > 0) {
558
+ cached = { cellWidth: metric.cellWidth, cellHeight: metric.cellHeight };
559
+ }
560
+ }
561
+ const CELL_WIDTH_RATIO = 0.615;
562
+ const CELL_HEIGHT_RATIO = 1.43;
563
+ function getCanvasCellMetric(fontSize = CANONICAL_FONT_SIZE_PX) {
564
+ if (cached)
565
+ return cached;
566
+ cached = {
567
+ cellWidth: fontSize * CELL_WIDTH_RATIO,
568
+ cellHeight: Math.round(fontSize * CELL_HEIGHT_RATIO),
569
+ };
570
+ return cached;
571
+ }
572
+ function _legacyMeasureViaTempTerminal(fontSize = CANONICAL_FONT_SIZE_PX) {
573
+ const fallback = {
574
+ cellWidth: fontSize * 0.6,
575
+ cellHeight: fontSize * CANONICAL_LINE_HEIGHT,
576
+ };
577
+ if (typeof document === 'undefined')
578
+ return fallback;
579
+ let container = null;
580
+ let terminal = null;
581
+ try {
582
+ container = document.createElement('div');
583
+ container.style.position = 'absolute';
584
+ container.style.visibility = 'hidden';
585
+ container.style.pointerEvents = 'none';
586
+ container.style.width = '500px';
587
+ container.style.height = '500px';
588
+ container.style.left = '-9999px';
589
+ container.style.top = '-9999px';
590
+ document.body.appendChild(container);
591
+ terminal = new Terminal({
592
+ fontSize,
593
+ fontFamily: CANONICAL_FONT_FAMILY,
594
+ lineHeight: CANONICAL_LINE_HEIGHT,
595
+ rows: 24,
596
+ cols: 80,
597
+ });
598
+ terminal.open(container);
599
+ const internal = terminal;
600
+ const cell = internal._core?._renderService?.dimensions?.css?.cell;
601
+ const cellWidth = cell?.width;
602
+ const cellHeight = cell?.height;
603
+ if (typeof cellWidth === 'number' && typeof cellHeight === 'number' && cellWidth > 0 && cellHeight > 0) {
604
+ cached = { cellWidth: Math.ceil(cellWidth), cellHeight: Math.ceil(cellHeight) };
605
+ return cached;
606
+ }
607
+ return fallback;
608
+ }
609
+ catch {
610
+ return fallback;
611
+ }
612
+ finally {
613
+ try {
614
+ terminal?.dispose();
615
+ }
616
+ catch {
617
+ void 0;
618
+ }
619
+ try {
620
+ if (container?.parentElement)
621
+ container.parentElement.removeChild(container);
622
+ }
623
+ catch {
624
+ void 0;
625
+ }
626
+ }
627
+ }
628
+
575
629
  const MIN_CONTAINER_SIZE = 10;
576
- /** Default display-only theme (for catalog/storybook) */
577
630
  const DISPLAY_THEME = {
578
631
  background: '#0a0a0a',
579
632
  foreground: '#66d9ef',
580
633
  cursor: '#ff79c6',
581
- cyan: '#8be9fd',
582
- brightCyan: '#50fa7b',
583
- green: '#50fa7b',
584
- brightGreen: '#50fa7b',
634
+ cyan: '#5bc5ff',
635
+ brightCyan: '#8ad7ff',
636
+ green: '#33ff33',
637
+ brightGreen: '#66ff66',
585
638
  magenta: '#ff79c6',
586
639
  brightMagenta: '#ff79c6',
587
640
  yellow: '#f1fa8c',
@@ -589,26 +642,25 @@ const DISPLAY_THEME = {
589
642
  red: '#ff5555',
590
643
  brightRed: '#ff5555'
591
644
  };
592
- /** Interactive session theme (matches original TerminalManager) */
593
645
  const INTERACTIVE_THEME = {
594
646
  background: '#0a0a0a',
595
647
  foreground: '#e0e0e0',
596
648
  cursor: '#e0e0e0',
597
649
  black: '#000000',
598
650
  red: '#cd3131',
599
- green: '#0dbc79',
651
+ green: '#33ff33',
600
652
  yellow: '#e5e510',
601
653
  blue: '#2472c8',
602
654
  magenta: '#bc3fbc',
603
- cyan: '#11a8cd',
655
+ cyan: '#5bc5ff',
604
656
  white: '#e5e5e5',
605
657
  brightBlack: '#666666',
606
658
  brightRed: '#f14c4c',
607
- brightGreen: '#23d18b',
659
+ brightGreen: '#66ff66',
608
660
  brightYellow: '#f5f543',
609
661
  brightBlue: '#3b8eea',
610
662
  brightMagenta: '#d670d6',
611
- brightCyan: '#29b8db',
663
+ brightCyan: '#8ad7ff',
612
664
  brightWhite: '#e5e5e5',
613
665
  };
614
666
  class XtermOrganism extends NgAtomicComponent {
@@ -624,20 +676,15 @@ class XtermOrganism extends NgAtomicComponent {
624
676
  fileLinksAddon;
625
677
  resizeObserver;
626
678
  lastDataLength = 0;
627
- /** Tracks how many bytes have been processed (for totalWritten-based diff detection) */
628
679
  lastProcessedLength = 0;
629
680
  terminalInitialized = signal(false, ...(ngDevMode ? [{ debugName: "terminalInitialized" }] : []));
630
- /** Tracks if initialization was skipped due to container being too small */
631
681
  initSkippedDueToSize = false;
632
- /** IDisposable refs for explicit cleanup (prevents memory leaks on mobile) */
633
682
  disposables = [];
634
- /** Timer refs for cleanup in ngOnDestroy */
635
683
  stableTimer = null;
636
684
  resizeTimer = null;
637
685
  fallbackTimer = null;
638
686
  constructor() {
639
687
  super();
640
- // Watch for data input changes (with totalWritten support for buffer overflow handling)
641
688
  effect(() => {
642
689
  const isInitialized = this.terminalInitialized();
643
690
  const rawData = this.store.data();
@@ -646,17 +693,13 @@ class XtermOrganism extends NgAtomicComponent {
646
693
  const data = dataIsFunction ? rawData() : rawData;
647
694
  if (!isInitialized)
648
695
  return;
649
- // Use totalWritten-based logic when available (handles buffer overflow correctly)
650
696
  if (totalWritten > 0) {
651
697
  this.writeDataWithTotalWritten(data ?? '', totalWritten);
652
698
  }
653
699
  else {
654
- // Legacy fallback for backward compatibility
655
700
  this.writeDataToTerminal(data);
656
701
  }
657
702
  });
658
- // Watch for interactive mode changes and update host class
659
- // Note: Using effect with classList because @HostBinding doesn't work with OnPush + signals
660
703
  effect(() => {
661
704
  const isInteractive = this.store.interactive();
662
705
  if (isInteractive) {
@@ -666,24 +709,18 @@ class XtermOrganism extends NgAtomicComponent {
666
709
  this.hostElement.nativeElement.classList.remove('interactive');
667
710
  }
668
711
  });
669
- // Watch for interactive mode changes and update cursor blink
670
712
  effect(() => {
671
713
  const isInitialized = this.terminalInitialized();
672
714
  const isInteractive = this.store.interactive();
673
715
  if (!isInitialized || !this.terminal)
674
716
  return;
675
- // Update cursor blink setting
676
717
  this.terminal.options.cursorBlink = isInteractive;
677
718
  });
678
719
  }
679
720
  writeDataToTerminal(data) {
680
- // Guard against undefined/null/empty data
681
- // Empty string is treated as "no data yet" (from startWith('') in streams)
682
- // Explicit reset should use the public clear() or reset() methods
683
721
  if (data === undefined || data === null || data === '') {
684
722
  return;
685
723
  }
686
- // O(1) length comparison instead of O(n) startsWith
687
724
  if (data.length >= this.lastDataLength) {
688
725
  const newPart = data.slice(this.lastDataLength);
689
726
  if (newPart) {
@@ -692,55 +729,48 @@ class XtermOrganism extends NgAtomicComponent {
692
729
  this.lastDataLength = data.length;
693
730
  }
694
731
  else {
695
- // Data got shorter = reset occurred
696
732
  this.terminal.reset();
697
733
  this.terminal.write(data);
698
734
  this.lastDataLength = data.length;
699
735
  }
700
736
  }
701
- /**
702
- * Write data using totalWritten for accurate diff detection.
703
- * This method handles buffer overflow scenarios correctly.
704
- * When newBytesCount >= data.length, performs a full redraw with terminal.reset()
705
- * to prevent stale output from previous sessions remaining on screen.
706
- * @param data The current buffer content
707
- * @param totalWritten Total bytes written to the buffer (cumulative)
708
- */
709
737
  writeDataWithTotalWritten(data, totalWritten) {
710
738
  if (!data)
711
739
  return;
712
740
  const newBytesCount = totalWritten - this.lastProcessedLength;
713
741
  if (newBytesCount <= 0) {
714
742
  if (totalWritten < this.lastProcessedLength) {
715
- // Reset detected (totalWritten decreased)
716
743
  this.terminal.reset();
717
744
  this.terminal.write(data);
718
745
  this.lastProcessedLength = totalWritten;
719
746
  }
720
747
  return;
721
748
  }
722
- // Full redraw needed: first render, session switch, or buffer overflow
723
- // (newBytesCount >= data.length means the diff covers the entire buffer or more)
749
+ let written = '';
724
750
  if (newBytesCount >= data.length) {
725
- this.terminal.reset();
751
+ if (hasVisibleContent(data)) {
752
+ this.terminal.reset();
753
+ }
726
754
  this.terminal.write(data);
755
+ written = data;
727
756
  }
728
757
  else {
729
- // Normal incremental write - extract only new content
730
- const newContent = data.slice(-newBytesCount);
758
+ const sliceN = Math.min(newBytesCount, data.length);
759
+ const newContent = data.slice(-sliceN);
731
760
  if (newContent) {
732
761
  this.terminal.write(newContent);
762
+ written = newContent;
733
763
  }
734
764
  }
735
765
  this.lastProcessedLength = totalWritten;
766
+ if (hasDisplayClearWithoutScrollbackClear(written)) {
767
+ this.terminal.clear();
768
+ }
736
769
  }
737
770
  ngAfterViewInit() {
738
771
  const containerElement = this.container().nativeElement;
739
- // Guard against initialization in very small containers (e.g., SR-only 1x1 pixel hidden elements)
740
- // This prevents FitAddon issues and excessive processing in hidden query sections
741
772
  if (containerElement.offsetWidth < MIN_CONTAINER_SIZE || containerElement.offsetHeight < MIN_CONTAINER_SIZE) {
742
773
  this.initSkippedDueToSize = true;
743
- // Retry initialization when container reaches minimum size (e.g., after CSS/layout settles)
744
774
  this.resizeObserver = new ResizeObserver(() => {
745
775
  if (containerElement.offsetWidth >= MIN_CONTAINER_SIZE && containerElement.offsetHeight >= MIN_CONTAINER_SIZE) {
746
776
  this.resizeObserver?.disconnect();
@@ -773,8 +803,6 @@ class XtermOrganism extends NgAtomicComponent {
773
803
  this.fitAddon = new FitAddon();
774
804
  this.terminal.loadAddon(this.fitAddon);
775
805
  this.terminal.loadAddon(new WebLinksAddon());
776
- // ImageAddon for displaying inline images (SIXEL and iTerm2 IIP support)
777
- // Loaded dynamically to avoid browser compatibility issues
778
806
  this.loadImageAddon();
779
807
  this.githubLinksAddon = new GitHubLinksAddon((event, issueNumber) => {
780
808
  this.dispatch({ id: XtermActionId.GITHUB_LINK_CLICK, payload: issueNumber });
@@ -786,38 +814,50 @@ class XtermOrganism extends NgAtomicComponent {
786
814
  }
787
815
  });
788
816
  this.terminal.loadAddon(this.fileLinksAddon);
789
- // ProxyLinksAddon: localhost → proxy.machina.at redirect (always loaded)
790
- this.terminal.loadAddon(new ProxyLinksAddon(this.store.getServerUUID ?? (() => undefined), this.store.getProxyToken ?? undefined, undefined, this.store.getDeployVersion ?? undefined, this.store.getProxyEndpoints ?? undefined));
817
+ const ctx = this.store.sessionContext;
818
+ this.terminal.loadAddon(new ProxyLinksAddon(ctx?.getServerUUID ?? (() => undefined), ctx?.getProxyToken, undefined, ctx?.getDeployVersion, ctx?.getProxyEndpoints, ctx?.onMissingToken, ctx?.getProxyEnv));
791
819
  this.terminal.open(containerElement);
792
- // Setup event handlers for interactive mode
820
+ const core = this.terminal._core;
821
+ const charSizeChange = core?._charSizeService?.onCharSizeChange;
822
+ if (typeof charSizeChange === 'function') {
823
+ const disp = charSizeChange.call(core._charSizeService, () => {
824
+ if (!this.terminal)
825
+ return;
826
+ this.fitToViewport();
827
+ if (isInteractive) {
828
+ this.dispatch({ id: XtermActionId.RESIZED, payload: { cols: this.cols, rows: this.rows } });
829
+ }
830
+ });
831
+ if (disp && typeof disp.dispose === 'function') {
832
+ this.disposables.push(disp);
833
+ }
834
+ }
793
835
  if (isInteractive) {
794
836
  this.disposables.push(this.terminal.onData((data) => {
795
837
  this.dispatch({ id: XtermActionId.DATA_INPUT, payload: data });
796
838
  }), this.terminal.onResize(({ cols, rows }) => this.dispatch({ id: XtermActionId.RESIZED, payload: { cols, rows } })));
797
839
  }
798
- // Use ResizeObserver with debounce to wait for container size to stabilize
799
- // (CSS transitions take 300-500ms, so fixed setTimeout(100ms) is too early)
800
840
  this.resizeObserver = new ResizeObserver(() => {
801
841
  try {
802
842
  if (!this.terminalInitialized()) {
803
- this.fitAddon.fit();
804
- // Debounce: wait for container to stop changing before initializing
843
+ this.fitToViewport();
805
844
  if (this.stableTimer)
806
845
  clearTimeout(this.stableTimer);
807
846
  this.stableTimer = setTimeout(() => {
808
847
  this.stableTimer = null;
809
- this.fitAddon.fit();
848
+ this.fitToViewport();
810
849
  this.markInitialized(isInteractive);
811
850
  }, 150);
812
851
  }
813
- else if (isInteractive) {
814
- // Debounce resize to wait for CSS transitions to complete (300ms)
852
+ else {
815
853
  if (this.resizeTimer)
816
854
  clearTimeout(this.resizeTimer);
817
855
  this.resizeTimer = setTimeout(() => {
818
856
  this.resizeTimer = null;
819
- this.fitAddon.fit();
820
- this.dispatch({ id: XtermActionId.RESIZED, payload: { cols: this.cols, rows: this.rows } });
857
+ this.fitToViewport();
858
+ if (isInteractive) {
859
+ this.dispatch({ id: XtermActionId.RESIZED, payload: { cols: this.cols, rows: this.rows } });
860
+ }
821
861
  }, 300);
822
862
  }
823
863
  }
@@ -826,17 +866,32 @@ class XtermOrganism extends NgAtomicComponent {
826
866
  }
827
867
  });
828
868
  this.resizeObserver.observe(containerElement);
829
- // Fallback: initialize after 500ms if ResizeObserver hasn't triggered stable state
830
869
  this.fallbackTimer = setTimeout(() => {
831
870
  this.fallbackTimer = null;
832
871
  if (!this.terminalInitialized()) {
833
- this.fitAddon.fit();
872
+ this.fitToViewport();
834
873
  this.markInitialized(isInteractive);
835
874
  }
836
875
  }, 500);
837
876
  }
877
+ fitToViewport() {
878
+ if (!this.terminal)
879
+ return;
880
+ const viewportElement = this.container().nativeElement;
881
+ const sharedCanvas = getCanvasCellMetric();
882
+ const probe = measureCellMetric();
883
+ const cellWidth = sharedCanvas.cellWidth > 0 ? sharedCanvas.cellWidth : probe.cellWidth;
884
+ const cellHeight = sharedCanvas.cellHeight > 0 ? sharedCanvas.cellHeight : probe.cellHeight;
885
+ const result = computeFitToViewport({ viewportElement, cellWidth, cellHeight });
886
+ if (!result) {
887
+ this.fitAddon.fit();
888
+ return;
889
+ }
890
+ if (result.cols !== this.terminal.cols || result.rows !== this.terminal.rows) {
891
+ this.terminal.resize(result.cols, result.rows);
892
+ }
893
+ }
838
894
  ngOnDestroy() {
839
- // Clear pending timers to prevent post-destroy callbacks
840
895
  if (this.stableTimer)
841
896
  clearTimeout(this.stableTimer);
842
897
  if (this.resizeTimer)
@@ -844,11 +899,9 @@ class XtermOrganism extends NgAtomicComponent {
844
899
  if (this.fallbackTimer)
845
900
  clearTimeout(this.fallbackTimer);
846
901
  this.resizeObserver?.disconnect();
847
- // Dispose xterm event handlers (onData, onResize) to prevent memory leaks
848
902
  for (const d of this.disposables)
849
903
  d.dispose();
850
904
  this.disposables.length = 0;
851
- // Remove terminal instance from window for E2E testing cleanup
852
905
  if (typeof window !== 'undefined' && window.__XTERM_INSTANCES__) {
853
906
  const instances = window.__XTERM_INSTANCES__;
854
907
  const index = instances.indexOf(this.terminal);
@@ -863,12 +916,10 @@ class XtermOrganism extends NgAtomicComponent {
863
916
  return;
864
917
  this.terminalInitialized.set(true);
865
918
  this.terminalReady.emit(this.terminal);
866
- // Expose terminal instance for E2E testing (only in development/test environments)
867
919
  if (typeof window !== 'undefined') {
868
920
  window.__XTERM_INSTANCES__ = window.__XTERM_INSTANCES__ || [];
869
921
  window.__XTERM_INSTANCES__.push(this.terminal);
870
922
  }
871
- // Dispatch initial RESIZED for interactive mode
872
923
  if (isInteractive) {
873
924
  this.dispatch({ id: XtermActionId.RESIZED, payload: { cols: this.cols, rows: this.rows } });
874
925
  }
@@ -886,11 +937,9 @@ class XtermOrganism extends NgAtomicComponent {
886
937
  this.terminal.loadAddon(this.imageAddon);
887
938
  }
888
939
  catch (e) {
889
- // ImageAddon not available in browser environment - this is expected
890
940
  console.debug('ImageAddon not available:', e);
891
941
  }
892
942
  }
893
- // Public API
894
943
  write(text) {
895
944
  this.terminal?.write(text);
896
945
  }
@@ -909,10 +958,13 @@ class XtermOrganism extends NgAtomicComponent {
909
958
  scrollLines(lines) {
910
959
  this.terminal?.scrollLines(lines);
911
960
  }
961
+ focus() {
962
+ this.terminal?.focus();
963
+ const helperTextarea = this.container().nativeElement.querySelector('.xterm-helper-textarea');
964
+ helperTextarea?.focus();
965
+ }
912
966
  fit() {
913
- this.fitAddon?.fit();
914
- // Only dispatch RESIZED after initialization to prevent sending wrong cols/rows
915
- // during CSS transitions (SessionTemplateV2.ngAfterViewInit calls fit() before layout stabilizes)
967
+ this.fitToViewport();
916
968
  if (this.terminalInitialized() && this.terminal) {
917
969
  this.dispatch({ id: XtermActionId.RESIZED, payload: { cols: this.cols, rows: this.rows } });
918
970
  }
@@ -924,14 +976,8 @@ class XtermOrganism extends NgAtomicComponent {
924
976
  return this.terminal?.rows ?? 24;
925
977
  }
926
978
  getTerminal() {
927
- // Only expose terminal after fitAddon.fit() has run (terminalInitialized === true)
928
979
  return this.terminalInitialized() ? this.terminal : undefined;
929
980
  }
930
- /**
931
- * Display an inline image using iTerm2's Inline Image Protocol
932
- * @param base64Data Base64 encoded image data (PNG, JPEG, etc.)
933
- * @param options Image display options
934
- */
935
981
  displayImage(base64Data, options) {
936
982
  if (!this.terminal)
937
983
  return;
@@ -948,34 +994,34 @@ class XtermOrganism extends NgAtomicComponent {
948
994
  if (options?.name) {
949
995
  args.push(`name=${btoa(options.name)}`);
950
996
  }
951
- // iTerm2 Inline Image Protocol escape sequence
952
997
  const escapeSequence = `\x1b]1337;File=${args.join(';')}:${base64Data}\x07`;
953
998
  this.terminal.write(escapeSequence);
954
999
  }
955
- /**
956
- * Get the ImageAddon instance for advanced image operations
957
- */
958
1000
  getImageAddon() {
959
1001
  return this.imageAddon;
960
1002
  }
961
1003
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.6", ngImport: i0, type: XtermOrganism, deps: [], target: i0.ɵɵFactoryTarget.Component });
962
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.2.0", version: "21.1.6", type: XtermOrganism, isStandalone: true, selector: "organisms-xterm", outputs: { terminalReady: "terminalReady", githubLinkClick: "githubLinkClick" }, viewQueries: [{ propertyName: "container", first: true, predicate: ["container"], descendants: true, isSignal: true }], usesInheritance: true, hostDirectives: [{ directive: XtermOrganismStore, inputs: ["data", "data", "interactive", "interactive", "useInteractiveTheme", "useInteractiveTheme", "totalWritten", "totalWritten"] }], ngImport: i0, template: `<div #container class="xterm-wrapper"></div>`, isInline: true, styles: ["", "@import\"https://unpkg.com/@xterm/xterm@5.5.0/css/xterm.css\";:host{display:block;width:100%;height:100%}:host .xterm-wrapper{width:100%;height:100%;background:#0a0a0a;position:relative;overflow:hidden}:host:not(.interactive) .xterm-wrapper ::ng-deep .xterm-cursor-layer{display:none!important}:host:not(.interactive) .xterm-wrapper ::ng-deep .xterm-cursor,:host:not(.interactive) .xterm-wrapper ::ng-deep .xterm-cursor-outline{opacity:0!important}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush });
1004
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.2.0", version: "21.1.6", type: XtermOrganism, isStandalone: true, selector: "organisms-xterm", outputs: { terminalReady: "terminalReady", githubLinkClick: "githubLinkClick" }, viewQueries: [{ propertyName: "container", first: true, predicate: ["container"], descendants: true, isSignal: true }], usesInheritance: true, hostDirectives: [{ directive: XtermOrganismStore, inputs: ["data", "data", "interactive", "interactive", "useInteractiveTheme", "useInteractiveTheme", "totalWritten", "totalWritten"] }], ngImport: i0, template: `<div #container class="terminal-viewport xterm-wrapper"></div>`, isInline: true, styles: ["", "@import\"https://unpkg.com/@xterm/xterm@5.5.0/css/xterm.css\";:host{display:block;width:100%;height:100%}:host .terminal-viewport,:host .xterm-wrapper{width:100%;height:100%;background:#0a0a0a;position:relative;overflow:hidden}:host:not(.interactive) .xterm-wrapper ::ng-deep .xterm-cursor-layer{display:none!important}:host:not(.interactive) .xterm-wrapper ::ng-deep .xterm-cursor,:host:not(.interactive) .xterm-wrapper ::ng-deep .xterm-cursor-outline{opacity:0!important}:host ::ng-deep .xterm-screen canvas{pointer-events:none}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush });
963
1005
  }
964
1006
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.6", ngImport: i0, type: XtermOrganism, decorators: [{
965
1007
  type: Component,
966
- args: [{ selector: 'organisms-xterm', standalone: true, imports: [], template: `<div #container class="xterm-wrapper"></div>`, changeDetection: ChangeDetectionStrategy.OnPush, hostDirectives: [{
1008
+ args: [{ selector: 'organisms-xterm', standalone: true, imports: [], template: `<div #container class="terminal-viewport xterm-wrapper"></div>`, changeDetection: ChangeDetectionStrategy.OnPush, hostDirectives: [{
967
1009
  directive: XtermOrganismStore,
968
1010
  inputs: ['data', 'interactive', 'useInteractiveTheme', 'totalWritten'],
969
- }], styles: ["@import\"https://unpkg.com/@xterm/xterm@5.5.0/css/xterm.css\";:host{display:block;width:100%;height:100%}:host .xterm-wrapper{width:100%;height:100%;background:#0a0a0a;position:relative;overflow:hidden}:host:not(.interactive) .xterm-wrapper ::ng-deep .xterm-cursor-layer{display:none!important}:host:not(.interactive) .xterm-wrapper ::ng-deep .xterm-cursor,:host:not(.interactive) .xterm-wrapper ::ng-deep .xterm-cursor-outline{opacity:0!important}\n"] }]
1011
+ }], styles: ["@import\"https://unpkg.com/@xterm/xterm@5.5.0/css/xterm.css\";:host{display:block;width:100%;height:100%}:host .terminal-viewport,:host .xterm-wrapper{width:100%;height:100%;background:#0a0a0a;position:relative;overflow:hidden}:host:not(.interactive) .xterm-wrapper ::ng-deep .xterm-cursor-layer{display:none!important}:host:not(.interactive) .xterm-wrapper ::ng-deep .xterm-cursor,:host:not(.interactive) .xterm-wrapper ::ng-deep .xterm-cursor-outline{opacity:0!important}:host ::ng-deep .xterm-screen canvas{pointer-events:none}\n"] }]
970
1012
  }], ctorParameters: () => [], propDecorators: { container: [{ type: i0.ViewChild, args: ['container', { isSignal: true }] }], terminalReady: [{ type: i0.Output, args: ["terminalReady"] }], githubLinkClick: [{ type: i0.Output, args: ["githubLinkClick"] }] } });
1013
+ function hasVisibleContent(s) {
1014
+ const stripped = s
1015
+ // eslint-disable-next-line no-control-regex -- ANSI CSI / OSC を strip するため意図的に \x1b / \x07 を match
1016
+ .replace(/\x1b\[[\d;?]*[a-zA-Z]/g, '')
1017
+ // eslint-disable-next-line no-control-regex -- ANSI OSC を strip するため意図的に \x1b / \x07 を match
1018
+ .replace(/\x1b\][^\x07\x1b]*[\x07\x1b]/g, '');
1019
+ return /\S/.test(stripped);
1020
+ }
1021
+ function hasDisplayClearWithoutScrollbackClear(s) {
1022
+ return s.includes('\x1b[2J') && !s.includes('\x1b[3J');
1023
+ }
971
1024
 
972
- /**
973
- * Utility functions for displaying images in xterm using SIXEL format
974
- */
975
- /**
976
- * Generate ASCII art text using figlet
977
- * Note: figlet is loaded dynamically as it's not available in browser environments
978
- */
979
1025
  async function generateAsciiArt(text, font = 'ANSI Shadow') {
980
1026
  try {
981
1027
  const figlet = await import('figlet');
@@ -990,27 +1036,20 @@ async function generateAsciiArt(text, font = 'ANSI Shadow') {
990
1036
  });
991
1037
  }
992
1038
  catch (e) {
993
- // figlet not available in browser - return simple text
994
1039
  console.debug('figlet not available:', e);
995
1040
  return text;
996
1041
  }
997
1042
  }
998
- /**
999
- * Convert ASCII art text to SVG with transparent background
1000
- */
1001
1043
  function asciiArtToSvg(asciiArt, options = {}) {
1002
1044
  const { textColor = '#00FFFF', fontSize = 14 } = options;
1003
1045
  const lines = asciiArt.split('\n');
1004
1046
  const maxLineLength = Math.max(...lines.map(l => l.length));
1005
- // Calculate dimensions based on monospace font metrics (block chars need more width)
1006
1047
  const charWidth = fontSize * 0.65;
1007
1048
  const lineHeight = fontSize * 1.4;
1008
1049
  const padding = 40;
1009
1050
  const width = Math.ceil(maxLineLength * charWidth) + padding;
1010
1051
  const height = Math.ceil(lines.length * lineHeight) + padding;
1011
- // Build SVG with text elements for each line
1012
1052
  const textElements = lines.map((line, i) => {
1013
- // Escape special XML characters
1014
1053
  const escapedLine = line
1015
1054
  .replace(/&/g, '&amp;')
1016
1055
  .replace(/</g, '&lt;')
@@ -1023,9 +1062,6 @@ function asciiArtToSvg(asciiArt, options = {}) {
1023
1062
  ${textElements}
1024
1063
  </svg>`;
1025
1064
  }
1026
- /**
1027
- * Convert SVG to SIXEL format for xterm display
1028
- */
1029
1065
  async function svgToSixel(svgString, options = {}) {
1030
1066
  const { width = 400, height = 200, backgroundColor } = options;
1031
1067
  return new Promise((resolve, reject) => {
@@ -1037,7 +1073,6 @@ async function svgToSixel(svgString, options = {}) {
1037
1073
  reject(new Error('Failed to get canvas context'));
1038
1074
  return;
1039
1075
  }
1040
- // Fill background (transparent if not specified)
1041
1076
  if (backgroundColor) {
1042
1077
  ctx.fillStyle = backgroundColor;
1043
1078
  ctx.fillRect(0, 0, width, height);
@@ -1059,20 +1094,14 @@ async function svgToSixel(svgString, options = {}) {
1059
1094
  img.src = url;
1060
1095
  });
1061
1096
  }
1062
- /**
1063
- * Convert ImageData to SIXEL escape sequence
1064
- */
1065
1097
  function imageDataToSixel(imageData) {
1066
1098
  const { width, height, data } = imageData;
1067
- // Build color palette (quantize to 16 colors)
1068
1099
  const palette = buildColorPalette(data, 16);
1069
1100
  const colorMap = new Map();
1070
1101
  palette.forEach((color, index) => {
1071
1102
  colorMap.set(color, index);
1072
1103
  });
1073
- // SIXEL header
1074
1104
  let sixel = '\x1bPq';
1075
- // Define colors in palette
1076
1105
  palette.forEach((color, index) => {
1077
1106
  const [r, g, b] = hexToRgb(color);
1078
1107
  const rPct = Math.round((r / 255) * 100);
@@ -1080,7 +1109,6 @@ function imageDataToSixel(imageData) {
1080
1109
  const bPct = Math.round((b / 255) * 100);
1081
1110
  sixel += `#${index};2;${rPct};${gPct};${bPct}`;
1082
1111
  });
1083
- // Process image in bands of 6 rows
1084
1112
  for (let bandY = 0; bandY < height; bandY += 6) {
1085
1113
  for (let colorIdx = 0; colorIdx < palette.length; colorIdx++) {
1086
1114
  const color = palette[colorIdx];
@@ -1164,19 +1192,13 @@ function findNearestColor(targetHex, palette) {
1164
1192
  }
1165
1193
  return nearest;
1166
1194
  }
1167
- /**
1168
- * Generate MACHINA ASCII art banner as SIXEL
1169
- */
1170
1195
  async function getMachinaAsciiArtSixel(options = {}) {
1171
- // Generate ASCII art for "MACHINA"
1172
1196
  const machinaArt = await generateAsciiArt('MACHINA', 'ANSI Shadow');
1173
- // Convert to SVG with cyan color (terminal style)
1174
1197
  const fontSize = 14;
1175
1198
  const svg = asciiArtToSvg(machinaArt, {
1176
1199
  textColor: options.textColor || '#00FFFF',
1177
1200
  fontSize
1178
1201
  });
1179
- // Calculate appropriate dimensions matching asciiArtToSvg calculation
1180
1202
  const lines = machinaArt.split('\n');
1181
1203
  const maxLineLength = Math.max(...lines.map(l => l.length));
1182
1204
  const charWidth = fontSize * 0.65;
@@ -1195,5 +1217,5 @@ async function getMachinaAsciiArtSixel(options = {}) {
1195
1217
  * Generated bundle index. Do not edit.
1196
1218
  */
1197
1219
 
1198
- export { GitHubLinkProvider, GitHubLinksAddon, ProxyLinkProvider, ProxyLinksAddon, WebLinksAddon, XTERM_DEPLOY_VERSION, XTERM_PROXY_ENDPOINTS, XTERM_PROXY_TOKEN, XTERM_SERVER_UUID, XtermActionId, XtermOrganism, XtermOrganismStore, asciiArtToSvg, deployVersionToHostSuffix, generateAsciiArt, getMachinaAsciiArtSixel, svgToSixel, toProxyUrl };
1220
+ export { CANONICAL_FONT_FAMILY, CANONICAL_FONT_SIZE_PX, CANONICAL_LINE_HEIGHT, DISPLAY_THEME, GitHubLinkProvider, GitHubLinksAddon, INTERACTIVE_THEME, MAX_VIEWPORT_COLS, MAX_VIEWPORT_ROWS, MIN_VIEWPORT_COLS, MIN_VIEWPORT_ROWS, ProxyLinkProvider, ProxyLinksAddon, WebLinksAddon, XTERM_SESSION_CONTEXT, XtermActionId, XtermOrganism, XtermOrganismStore, appendUrlHandoff, asciiArtToSvg, clearCanvasCellMetricCache, computeFitToViewport, deployVersionToHostSuffix, encodeUrlHandoff, generateAsciiArt, getCanvasCellMetric, getMachinaAsciiArtSixel, measureCellMetric, openWebProxyForPort, recordCanvasCellMetric, svgToSixel, toProxyUrl };
1199
1221
  //# sourceMappingURL=xxmachina-components-organisms-xterm.mjs.map