@xxmachina/components 19.21.8 → 19.25.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 (79) hide show
  1. package/extras/flow/index.d.ts +13 -3
  2. package/extras/flow/index.d.ts.map +1 -1
  3. package/features/query/index.d.ts.map +1 -1
  4. package/fesm2022/xxmachina-components-extras-flow.mjs +110 -11
  5. package/fesm2022/xxmachina-components-extras-flow.mjs.map +1 -1
  6. package/fesm2022/xxmachina-components-groups-query-form.mjs.map +1 -1
  7. package/fesm2022/xxmachina-components-molecules-inline-edit-field.mjs +117 -0
  8. package/fesm2022/xxmachina-components-molecules-inline-edit-field.mjs.map +1 -0
  9. package/fesm2022/xxmachina-components-molecules-weekly-header.mjs +2 -2
  10. package/fesm2022/xxmachina-components-molecules-weekly-header.mjs.map +1 -1
  11. package/fesm2022/xxmachina-components-organisms-calendar-section.mjs +2 -2
  12. package/fesm2022/xxmachina-components-organisms-calendar-section.mjs.map +1 -1
  13. package/fesm2022/xxmachina-components-organisms-terminal-input-section.mjs +19 -4
  14. package/fesm2022/xxmachina-components-organisms-terminal-input-section.mjs.map +1 -1
  15. package/fesm2022/xxmachina-components-organisms-xterm.mjs +849 -49
  16. package/fesm2022/xxmachina-components-organisms-xterm.mjs.map +1 -1
  17. package/fesm2022/xxmachina-components-pages-command-harness.mjs +28 -0
  18. package/fesm2022/xxmachina-components-pages-command-harness.mjs.map +1 -0
  19. package/fesm2022/xxmachina-components-pages-command.mjs +10 -6
  20. package/fesm2022/xxmachina-components-pages-command.mjs.map +1 -1
  21. package/fesm2022/xxmachina-components-pages-query.mjs +2 -2
  22. package/fesm2022/xxmachina-components-pages-query.mjs.map +1 -1
  23. package/fesm2022/xxmachina-components-pages-thread.mjs +2 -2
  24. package/fesm2022/xxmachina-components-pages-thread.mjs.map +1 -1
  25. package/fesm2022/xxmachina-components-services-message.mjs.map +1 -1
  26. package/fesm2022/xxmachina-components-templates-agent.mjs +151 -123
  27. package/fesm2022/xxmachina-components-templates-agent.mjs.map +1 -1
  28. package/fesm2022/xxmachina-components-templates-background.mjs +376 -242
  29. package/fesm2022/xxmachina-components-templates-background.mjs.map +1 -1
  30. package/fesm2022/xxmachina-components-templates-flow-nodes-group.mjs +164 -0
  31. package/fesm2022/xxmachina-components-templates-flow-nodes-group.mjs.map +1 -0
  32. package/fesm2022/xxmachina-components-templates-flow-nodes-issue.mjs +157 -0
  33. package/fesm2022/xxmachina-components-templates-flow-nodes-issue.mjs.map +1 -0
  34. package/fesm2022/xxmachina-components-templates-flow-nodes-task.mjs +154 -0
  35. package/fesm2022/xxmachina-components-templates-flow-nodes-task.mjs.map +1 -0
  36. package/fesm2022/xxmachina-components-templates-flow.mjs +337 -0
  37. package/fesm2022/xxmachina-components-templates-flow.mjs.map +1 -0
  38. package/fesm2022/xxmachina-components.mjs +2 -2
  39. package/fesm2022/xxmachina-components.mjs.map +1 -1
  40. package/groups/query-form/index.d.ts +3 -4
  41. package/groups/query-form/index.d.ts.map +1 -1
  42. package/index.d.ts.map +1 -1
  43. package/molecules/inline-edit-field/index.d.ts +32 -0
  44. package/molecules/inline-edit-field/index.d.ts.map +1 -0
  45. package/organisms/terminal-input-section/index.d.ts +2 -1
  46. package/organisms/terminal-input-section/index.d.ts.map +1 -1
  47. package/organisms/xterm/index.d.ts +176 -4
  48. package/organisms/xterm/index.d.ts.map +1 -1
  49. package/package.json +25 -9
  50. package/pages/command/harness/index.d.ts +14 -0
  51. package/pages/command/harness/index.d.ts.map +1 -0
  52. package/pages/command/index.d.ts +12 -4
  53. package/pages/command/index.d.ts.map +1 -1
  54. package/pages/query/index.d.ts +2 -2
  55. package/pages/query/index.d.ts.map +1 -1
  56. package/pages/query-v2/index.d.ts.map +1 -1
  57. package/services/command/index.d.ts.map +1 -1
  58. package/services/message/index.d.ts +3 -3
  59. package/services/message/index.d.ts.map +1 -1
  60. package/templates/agent/index.d.ts +11 -2
  61. package/templates/agent/index.d.ts.map +1 -1
  62. package/templates/background/index.d.ts +14 -20
  63. package/templates/background/index.d.ts.map +1 -1
  64. package/templates/flow/index.d.ts +61 -0
  65. package/templates/flow/index.d.ts.map +1 -0
  66. package/templates/flow/nodes/group/index.d.ts +44 -0
  67. package/templates/flow/nodes/group/index.d.ts.map +1 -0
  68. package/templates/flow/nodes/issue/index.d.ts +46 -0
  69. package/templates/flow/nodes/issue/index.d.ts.map +1 -0
  70. package/templates/flow/nodes/task/index.d.ts +37 -0
  71. package/templates/flow/nodes/task/index.d.ts.map +1 -0
  72. package/fesm2022/xxmachina-components-services-calendar.mjs +0 -25
  73. package/fesm2022/xxmachina-components-services-calendar.mjs.map +0 -1
  74. package/fesm2022/xxmachina-components-services-schedule.mjs +0 -51
  75. package/fesm2022/xxmachina-components-services-schedule.mjs.map +0 -1
  76. package/services/calendar/index.d.ts +0 -14
  77. package/services/calendar/index.d.ts.map +0 -1
  78. package/services/schedule/index.d.ts +0 -27
  79. package/services/schedule/index.d.ts.map +0 -1
@@ -1,8 +1,128 @@
1
1
  import * as i0 from '@angular/core';
2
- import { viewChild, output, ChangeDetectionStrategy, Component } from '@angular/core';
2
+ import { input, Directive, viewChild, inject, ElementRef, output, signal, effect, ChangeDetectionStrategy, Component } from '@angular/core';
3
3
  import { Terminal } from '@xterm/xterm';
4
4
  import { FitAddon } from '@xterm/addon-fit';
5
- import { WebLinksAddon } from 'xterm-addon-web-links';
5
+ import { InjectableComponent, NgAtomicComponent } from '@ng-atomic/core';
6
+ import { makeDI } from '@ng-atomic/common/services/ui';
7
+
8
+ const URL_REGEX = /https?:\/\/[^\s<>'")\]]+/g;
9
+ const MAX_SCAN_LINES = 30;
10
+ /**
11
+ * Link provider that detects URLs spanning multiple terminal lines.
12
+ *
13
+ * Handles two wrapping scenarios:
14
+ * 1. Terminal wrapping (isWrapped=true) - xterm splits long output
15
+ * 2. Application wrapping (isWrapped=false) - e.g. Claude Code/Ink explicitly
16
+ * breaks lines at some width. Detected by heuristic: previous line ends
17
+ * without whitespace and current line starts without whitespace.
18
+ */
19
+ class WebLinkProvider {
20
+ _terminal;
21
+ _handler;
22
+ constructor(_terminal, _handler = (_, uri) => {
23
+ window.open(uri, '_blank', 'noopener');
24
+ }) {
25
+ this._terminal = _terminal;
26
+ this._handler = _handler;
27
+ }
28
+ provideLinks(bufferLineNumber, callback) {
29
+ const buffer = this._terminal.buffer.active;
30
+ // Find the start of this line group (0-indexed)
31
+ let startY = bufferLineNumber - 1;
32
+ while (startY > 0
33
+ && (bufferLineNumber - 1 - startY) < MAX_SCAN_LINES
34
+ && this._isLineContinuation(startY)) {
35
+ startY--;
36
+ }
37
+ // Collect text from all lines in the group
38
+ const lineTexts = [];
39
+ let y = startY;
40
+ do {
41
+ const line = buffer.getLine(y);
42
+ if (!line)
43
+ break;
44
+ lineTexts.push(line.translateToString(true));
45
+ y++;
46
+ } while (y < buffer.length
47
+ && (y - startY) < MAX_SCAN_LINES
48
+ && this._isLineContinuation(y));
49
+ const fullText = lineTexts.join('');
50
+ URL_REGEX.lastIndex = 0;
51
+ let match;
52
+ const links = [];
53
+ while ((match = URL_REGEX.exec(fullText)) !== null) {
54
+ const startPos = this._offsetToPos(match.index, startY, lineTexts);
55
+ const endPos = this._offsetToPos(match.index + match[0].length - 1, startY, lineTexts);
56
+ if (!startPos || !endPos)
57
+ continue;
58
+ links.push({
59
+ text: match[0],
60
+ range: {
61
+ start: { x: startPos.x + 1, y: startPos.y + 1 },
62
+ end: { x: endPos.x + 1, y: endPos.y + 1 },
63
+ },
64
+ activate: (event, text) => this._handler(event, text),
65
+ });
66
+ }
67
+ callback(links.length > 0 ? links : undefined);
68
+ }
69
+ /**
70
+ * Determine if a line is a continuation of the previous line.
71
+ * True when either:
72
+ * - xterm marks it as wrapped (isWrapped=true), OR
73
+ * - no whitespace at line boundary (previous line ends without space,
74
+ * current line starts without space) suggesting mid-token line break
75
+ */
76
+ _isLineContinuation(lineIdx) {
77
+ const buffer = this._terminal.buffer.active;
78
+ const line = buffer.getLine(lineIdx);
79
+ if (!line)
80
+ return false;
81
+ if (line.isWrapped)
82
+ return true;
83
+ if (lineIdx === 0)
84
+ return false;
85
+ const prevLine = buffer.getLine(lineIdx - 1);
86
+ if (!prevLine)
87
+ return false;
88
+ const prevText = prevLine.translateToString(true);
89
+ const currentText = line.translateToString(true);
90
+ return (prevText.length > 0
91
+ && !/\s$/.test(prevText)
92
+ && currentText.length > 0
93
+ && !/^\s/.test(currentText));
94
+ }
95
+ /** Convert a character offset in the concatenated text to a buffer position. */
96
+ _offsetToPos(offset, startY, lineTexts) {
97
+ let remaining = offset;
98
+ for (let i = 0; i < lineTexts.length; i++) {
99
+ if (remaining < lineTexts[i].length) {
100
+ return { x: remaining, y: startY + i };
101
+ }
102
+ remaining -= lineTexts[i].length;
103
+ }
104
+ const lastIdx = lineTexts.length - 1;
105
+ return { x: lineTexts[lastIdx].length, y: startY + lastIdx };
106
+ }
107
+ }
108
+
109
+ /**
110
+ * Drop-in replacement for @xterm/addon-web-links that supports
111
+ * URLs spanning wrapped terminal lines.
112
+ */
113
+ class WebLinksAddon {
114
+ _handler;
115
+ _linkProvider;
116
+ constructor(_handler) {
117
+ this._handler = _handler;
118
+ }
119
+ activate(terminal) {
120
+ this._linkProvider = terminal.registerLinkProvider(new WebLinkProvider(terminal, this._handler));
121
+ }
122
+ dispose() {
123
+ this._linkProvider?.dispose();
124
+ }
125
+ }
6
126
 
7
127
  class GitHubLinkProvider {
8
128
  _terminal;
@@ -23,13 +143,8 @@ class GitHubLinkProvider {
23
143
  const text = line.translateToString(true);
24
144
  const links = [];
25
145
  this.GITHUB_ISSUE_REGEX.lastIndex = 0;
26
- // Debug: check if text contains potential issue references
27
- if (text.includes('#')) {
28
- console.log('[GitHubLinkProvider] Line contains #:', text);
29
- }
30
146
  let match;
31
147
  while ((match = this.GITHUB_ISSUE_REGEX.exec(text)) !== null) {
32
- console.log('[GitHubLinkProvider] Found match:', match[0], 'issue:', match[1]);
33
148
  const issueNumber = parseInt(match[1], 10);
34
149
  const fullMatch = match[0];
35
150
  const issueRef = `#${match[1]}`;
@@ -84,50 +199,385 @@ class GitHubLinksAddon {
84
199
  _linkProvider;
85
200
  constructor(_handler) {
86
201
  this._handler = _handler;
87
- console.log('[GitHubLinksAddon] constructor called');
88
202
  }
89
203
  activate(terminal) {
90
- console.log('[GitHubLinksAddon] activate called');
91
204
  this._terminal = terminal;
92
205
  this._linkProvider = terminal.registerLinkProvider(new GitHubLinkProvider(terminal, this._handler));
93
- console.log('[GitHubLinksAddon] linkProvider registered');
94
206
  }
95
207
  dispose() {
96
208
  this._linkProvider?.dispose();
97
209
  }
98
210
  }
99
211
 
100
- class XtermOrganism {
212
+ class FileLinkProvider {
213
+ _terminal;
214
+ _handler;
215
+ // ファイルパスを検出する正規表現
216
+ // - 絶対パス: /path/to/file.ts, /path/to/file.ts:10:5
217
+ // - 相対パス: ./src/file.ts, ../lib/utils.ts:20
218
+ // - 拡張子を持つファイル名
219
+ FILE_PATH_REGEX = /(?:^|[\s'"`(])((\.\.?\/|\/)?[\w.@-]+(?:\/[\w.@-]+)*\.[a-zA-Z0-9]+(?::\d+(?::\d+)?)?)/g;
220
+ constructor(_terminal, _handler) {
221
+ this._terminal = _terminal;
222
+ this._handler = _handler;
223
+ }
224
+ provideLinks(bufferLineNumber, callback) {
225
+ // bufferLineNumber は 1-indexed、buffer.getLine() は 0-indexed を期待
226
+ const lineIndex = bufferLineNumber - 1;
227
+ const line = this._terminal.buffer.active.getLine(lineIndex);
228
+ if (!line) {
229
+ callback(undefined);
230
+ return;
231
+ }
232
+ const text = line.translateToString(true);
233
+ const links = [];
234
+ // Reset regex lastIndex
235
+ this.FILE_PATH_REGEX.lastIndex = 0;
236
+ let match;
237
+ while ((match = this.FILE_PATH_REGEX.exec(text)) !== null) {
238
+ const filePath = match[1];
239
+ if (this._isLikelyFilePath(filePath) && !this._isUrl(filePath)) {
240
+ // 文字列インデックスから表示位置(セル位置)を計算
241
+ const stringStartIndex = match.index + (match[0].length - match[1].length);
242
+ // WebLinksAddon と同じ方式で位置をマッピング(0-indexed lineIndexを渡す)
243
+ const [, startX] = this._mapStrIdx(lineIndex, 0, stringStartIndex);
244
+ const [, endX] = this._mapStrIdx(lineIndex, startX, filePath.length);
245
+ if (startX === -1 || endX === -1) {
246
+ continue;
247
+ }
248
+ links.push({
249
+ text: filePath,
250
+ range: {
251
+ // IBufferCellPosition は 1-indexed
252
+ start: { x: startX + 1, y: bufferLineNumber },
253
+ end: { x: endX, y: bufferLineNumber }
254
+ },
255
+ activate: (event, linkText) => {
256
+ const parsed = this._parseFilePath(linkText);
257
+ this._handler(event, parsed.path, parsed.line, parsed.column);
258
+ }
259
+ });
260
+ }
261
+ }
262
+ callback(links.length > 0 ? links : undefined);
263
+ }
264
+ /**
265
+ * Map a string index back to buffer positions.
266
+ * Returns buffer position as [lineIndex, columnIndex] 0-based,
267
+ * or [-1, -1] in case the lookup ran into a non-existing line.
268
+ * (Based on xterm.js WebLinkProvider implementation)
269
+ */
270
+ _mapStrIdx(lineIndex, rowIndex, stringIndex) {
271
+ const buf = this._terminal.buffer.active;
272
+ const cell = buf.getNullCell();
273
+ let start = rowIndex;
274
+ while (stringIndex) {
275
+ const line = buf.getLine(lineIndex);
276
+ if (!line) {
277
+ return [-1, -1];
278
+ }
279
+ for (let i = start; i < line.length; ++i) {
280
+ line.getCell(i, cell);
281
+ const chars = cell.getChars();
282
+ const width = cell.getWidth();
283
+ if (width) {
284
+ stringIndex -= chars.length || 1;
285
+ }
286
+ if (stringIndex < 0) {
287
+ return [lineIndex, i];
288
+ }
289
+ }
290
+ lineIndex++;
291
+ start = 0;
292
+ }
293
+ return [lineIndex, start];
294
+ }
295
+ _isLikelyFilePath(text) {
296
+ // 拡張子を持つか確認
297
+ return /\.[a-zA-Z0-9]+(?::\d+(?::\d+)?)?$/.test(text);
298
+ }
299
+ _isUrl(text) {
300
+ // URL形式を除外
301
+ return /^https?:\/\//.test(text) || /^file:\/\//.test(text);
302
+ }
303
+ _parseFilePath(text) {
304
+ const match = text.match(/^(.+?)(?::(\d+)(?::(\d+))?)?$/);
305
+ if (match) {
306
+ return {
307
+ path: match[1],
308
+ line: match[2] ? parseInt(match[2], 10) : undefined,
309
+ column: match[3] ? parseInt(match[3], 10) : undefined
310
+ };
311
+ }
312
+ return { path: text };
313
+ }
314
+ }
315
+ // Export helper functions for testing
316
+ const isLikelyFilePath = (text) => {
317
+ return /\.[a-zA-Z0-9]+(?::\d+(?::\d+)?)?$/.test(text) && !/^https?:\/\//.test(text);
318
+ };
319
+ const parseFilePath = (text) => {
320
+ const match = text.match(/^(.+?)(?::(\d+)(?::(\d+))?)?$/);
321
+ if (match) {
322
+ return {
323
+ path: match[1],
324
+ line: match[2] ? parseInt(match[2], 10) : undefined,
325
+ column: match[3] ? parseInt(match[3], 10) : undefined
326
+ };
327
+ }
328
+ return { path: text };
329
+ };
330
+
331
+ /**
332
+ * xterm.js addon for detecting and handling file path links in terminal output.
333
+ * Similar to WebLinksAddon but for local file paths.
334
+ *
335
+ * Usage:
336
+ * ```typescript
337
+ * const fileLinksAddon = new FileLinksAddon((event, filePath, line, column) => {
338
+ * if (event.metaKey || event.ctrlKey) {
339
+ * // Open file in editor
340
+ * }
341
+ * });
342
+ * terminal.loadAddon(fileLinksAddon);
343
+ * ```
344
+ */
345
+ class FileLinksAddon {
346
+ _handler;
347
+ _terminal;
348
+ _linkProvider;
349
+ constructor(_handler) {
350
+ this._handler = _handler;
351
+ }
352
+ activate(terminal) {
353
+ this._terminal = terminal;
354
+ this._linkProvider = terminal.registerLinkProvider(new FileLinkProvider(terminal, this._handler));
355
+ }
356
+ dispose() {
357
+ this._linkProvider?.dispose();
358
+ }
359
+ }
360
+
361
+ var XtermActionId;
362
+ (function (XtermActionId) {
363
+ XtermActionId["DATA_INPUT"] = "xterm:data-input";
364
+ XtermActionId["RESIZED"] = "xterm:resized";
365
+ XtermActionId["GITHUB_LINK_CLICK"] = "xterm:github-link-click";
366
+ XtermActionId["FILE_LINK_CLICK"] = "xterm:file-link-click";
367
+ })(XtermActionId || (XtermActionId = {}));
368
+ class XtermOrganismStore extends InjectableComponent {
369
+ static DI = makeDI(XtermOrganismStore, () => () => ({
370
+ data: '',
371
+ interactive: false,
372
+ queryResult: '',
373
+ useInteractiveTheme: false,
374
+ }), ['components', 'organisms', 'xterm']);
375
+ config = XtermOrganismStore.DI.injectConfig();
376
+ // Note: Using simple default values instead of _computed() to ensure
377
+ // input bindings from templates work correctly with Angular signal effects
378
+ data = input(undefined);
379
+ interactive = input(false);
380
+ queryResult = input('');
381
+ /** Use interactive theme even when not interactive (for display consistency) */
382
+ useInteractiveTheme = input(false);
383
+ /** Total bytes written to the buffer (for accurate diff detection when buffer overflows) */
384
+ totalWritten = input(0);
385
+ constructor() {
386
+ super();
387
+ XtermOrganismStore.DI.initialize(this);
388
+ }
389
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.0.5", ngImport: i0, type: XtermOrganismStore, deps: [], target: i0.ɵɵFactoryTarget.Directive });
390
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "20.0.5", type: XtermOrganismStore, isStandalone: true, selector: "organisms-xterm", inputs: { data: { classPropertyName: "data", publicName: "data", isSignal: true, isRequired: false, transformFunction: null }, interactive: { classPropertyName: "interactive", publicName: "interactive", isSignal: true, isRequired: false, transformFunction: null }, queryResult: { classPropertyName: "queryResult", publicName: "queryResult", isSignal: true, isRequired: false, transformFunction: null }, useInteractiveTheme: { classPropertyName: "useInteractiveTheme", publicName: "useInteractiveTheme", isSignal: true, isRequired: false, transformFunction: null }, totalWritten: { classPropertyName: "totalWritten", publicName: "totalWritten", isSignal: true, isRequired: false, transformFunction: null } }, usesInheritance: true, ngImport: i0 });
391
+ }
392
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.0.5", ngImport: i0, type: XtermOrganismStore, decorators: [{
393
+ type: Directive,
394
+ args: [{ standalone: true, selector: 'organisms-xterm' }]
395
+ }], ctorParameters: () => [] });
396
+
397
+ /**
398
+ * Minimum container size required for terminal initialization.
399
+ * Prevents issues with SR-only hidden containers (1x1 pixel) causing FitAddon problems.
400
+ */
401
+ const MIN_CONTAINER_SIZE = 10;
402
+ /** Default display-only theme (for catalog/storybook) */
403
+ const DISPLAY_THEME = {
404
+ background: '#0a0a0a',
405
+ foreground: '#66d9ef',
406
+ cursor: '#ff79c6',
407
+ cyan: '#8be9fd',
408
+ brightCyan: '#50fa7b',
409
+ green: '#50fa7b',
410
+ brightGreen: '#50fa7b',
411
+ magenta: '#ff79c6',
412
+ brightMagenta: '#ff79c6',
413
+ yellow: '#f1fa8c',
414
+ brightYellow: '#ffb86c',
415
+ red: '#ff5555',
416
+ brightRed: '#ff5555'
417
+ };
418
+ /** Interactive session theme (matches original TerminalManager) */
419
+ const INTERACTIVE_THEME = {
420
+ background: '#0a0a0a',
421
+ foreground: '#e0e0e0',
422
+ cursor: '#e0e0e0',
423
+ black: '#000000',
424
+ red: '#cd3131',
425
+ green: '#0dbc79',
426
+ yellow: '#e5e510',
427
+ blue: '#2472c8',
428
+ magenta: '#bc3fbc',
429
+ cyan: '#11a8cd',
430
+ white: '#e5e5e5',
431
+ brightBlack: '#666666',
432
+ brightRed: '#f14c4c',
433
+ brightGreen: '#23d18b',
434
+ brightYellow: '#f5f543',
435
+ brightBlue: '#3b8eea',
436
+ brightMagenta: '#d670d6',
437
+ brightCyan: '#29b8db',
438
+ brightWhite: '#e5e5e5',
439
+ };
440
+ class XtermOrganism extends NgAtomicComponent {
101
441
  container = viewChild.required('container');
442
+ store = inject(XtermOrganismStore);
443
+ hostElement = inject((ElementRef));
102
444
  terminalReady = output();
103
445
  githubLinkClick = output();
104
446
  terminal;
105
447
  fitAddon;
448
+ imageAddon;
106
449
  githubLinksAddon;
450
+ fileLinksAddon;
107
451
  resizeObserver;
452
+ lastDataLength = 0;
453
+ /** Tracks how many bytes have been processed (for totalWritten-based diff detection) */
454
+ lastProcessedLength = 0;
455
+ terminalInitialized = signal(false);
456
+ /** Tracks if initialization was skipped due to container being too small */
457
+ initSkippedDueToSize = false;
458
+ constructor() {
459
+ super();
460
+ // Watch for data input changes (with totalWritten support for buffer overflow handling)
461
+ effect(() => {
462
+ const isInitialized = this.terminalInitialized();
463
+ const rawData = this.store.data();
464
+ const totalWritten = this.store.totalWritten();
465
+ const dataIsFunction = typeof rawData === 'function';
466
+ const data = dataIsFunction ? rawData() : rawData;
467
+ if (!isInitialized)
468
+ return;
469
+ // Use totalWritten-based logic when available (handles buffer overflow correctly)
470
+ if (totalWritten > 0) {
471
+ this.writeDataWithTotalWritten(data ?? '', totalWritten);
472
+ }
473
+ else {
474
+ // Legacy fallback for backward compatibility
475
+ this.writeDataToTerminal(data);
476
+ }
477
+ });
478
+ // Watch for interactive mode changes and update host class
479
+ // Note: Using effect with classList because @HostBinding doesn't work with OnPush + signals
480
+ effect(() => {
481
+ const isInteractive = this.store.interactive();
482
+ if (isInteractive) {
483
+ this.hostElement.nativeElement.classList.add('interactive');
484
+ }
485
+ else {
486
+ this.hostElement.nativeElement.classList.remove('interactive');
487
+ }
488
+ });
489
+ // Watch for interactive mode changes and update cursor blink
490
+ effect(() => {
491
+ const isInitialized = this.terminalInitialized();
492
+ const isInteractive = this.store.interactive();
493
+ if (!isInitialized || !this.terminal)
494
+ return;
495
+ // Update cursor blink setting
496
+ this.terminal.options.cursorBlink = isInteractive;
497
+ });
498
+ }
499
+ writeDataToTerminal(data) {
500
+ // Guard against undefined/null/empty data
501
+ // Empty string is treated as "no data yet" (from startWith('') in streams)
502
+ // Explicit reset should use the public clear() or reset() methods
503
+ if (data === undefined || data === null || data === '') {
504
+ return;
505
+ }
506
+ // O(1) length comparison instead of O(n) startsWith
507
+ if (data.length >= this.lastDataLength) {
508
+ const newPart = data.slice(this.lastDataLength);
509
+ if (newPart) {
510
+ this.terminal.write(newPart);
511
+ }
512
+ this.lastDataLength = data.length;
513
+ }
514
+ else {
515
+ // Data got shorter = reset occurred
516
+ this.terminal.reset();
517
+ this.terminal.write(data);
518
+ this.lastDataLength = data.length;
519
+ }
520
+ }
521
+ /**
522
+ * Write data using totalWritten for accurate diff detection.
523
+ * This method handles buffer overflow scenarios correctly.
524
+ * @param data The current buffer content
525
+ * @param totalWritten Total bytes written to the buffer (cumulative)
526
+ */
527
+ writeDataWithTotalWritten(data, totalWritten) {
528
+ if (!data)
529
+ return;
530
+ // Calculate how many new bytes arrived
531
+ const newBytesCount = totalWritten - this.lastProcessedLength;
532
+ if (newBytesCount <= 0) {
533
+ // No new data or reset occurred
534
+ if (totalWritten < this.lastProcessedLength) {
535
+ // Reset detected
536
+ this.terminal.reset();
537
+ this.terminal.write(data);
538
+ this.lastProcessedLength = totalWritten;
539
+ }
540
+ return;
541
+ }
542
+ // Extract new content from the end of buffer
543
+ const newContent = data.slice(-newBytesCount);
544
+ if (newContent) {
545
+ this.terminal.write(newContent);
546
+ }
547
+ this.lastProcessedLength = totalWritten;
548
+ }
108
549
  ngAfterViewInit() {
550
+ const containerElement = this.container().nativeElement;
551
+ // Guard against initialization in very small containers (e.g., SR-only 1x1 pixel hidden elements)
552
+ // This prevents FitAddon issues and excessive processing in hidden query sections
553
+ if (containerElement.offsetWidth < MIN_CONTAINER_SIZE || containerElement.offsetHeight < MIN_CONTAINER_SIZE) {
554
+ this.initSkippedDueToSize = true;
555
+ // Retry initialization when container reaches minimum size (e.g., after CSS/layout settles)
556
+ this.resizeObserver = new ResizeObserver(() => {
557
+ if (containerElement.offsetWidth >= MIN_CONTAINER_SIZE && containerElement.offsetHeight >= MIN_CONTAINER_SIZE) {
558
+ this.resizeObserver?.disconnect();
559
+ this.resizeObserver = undefined;
560
+ this.initSkippedDueToSize = false;
561
+ this.initializeTerminal(containerElement);
562
+ }
563
+ });
564
+ this.resizeObserver.observe(containerElement);
565
+ return;
566
+ }
567
+ this.initializeTerminal(containerElement);
568
+ }
569
+ initializeTerminal(containerElement) {
570
+ const isInteractive = this.store.interactive();
571
+ const useInteractiveTheme = this.store.useInteractiveTheme() || isInteractive;
109
572
  this.terminal = new Terminal({
110
- theme: {
111
- background: '#0a0a0a',
112
- foreground: '#66d9ef',
113
- cursor: '#ff79c6',
114
- cyan: '#8be9fd',
115
- brightCyan: '#50fa7b',
116
- green: '#50fa7b',
117
- brightGreen: '#50fa7b',
118
- magenta: '#ff79c6',
119
- brightMagenta: '#ff79c6',
120
- yellow: '#f1fa8c',
121
- brightYellow: '#ffb86c',
122
- red: '#ff5555',
123
- brightRed: '#ff5555'
124
- },
125
- scrollback: 1000,
126
- fontSize: 12,
573
+ theme: useInteractiveTheme ? INTERACTIVE_THEME : DISPLAY_THEME,
574
+ scrollback: isInteractive ? 10000 : 1000,
575
+ fontSize: useInteractiveTheme ? 14 : 12,
576
+ fontFamily: '"FiraCode Nerd Font", "Fira Code", "SF Mono", "Cascadia Code", "Consolas", "Courier New", monospace',
127
577
  lineHeight: 1.2,
128
578
  allowTransparency: false,
129
- disableStdin: true,
130
- cursorBlink: false,
579
+ disableStdin: !isInteractive,
580
+ cursorBlink: isInteractive,
131
581
  cursorStyle: 'block',
132
582
  convertEol: true,
133
583
  rows: 24
@@ -135,34 +585,110 @@ class XtermOrganism {
135
585
  this.fitAddon = new FitAddon();
136
586
  this.terminal.loadAddon(this.fitAddon);
137
587
  this.terminal.loadAddon(new WebLinksAddon());
588
+ // ImageAddon for displaying inline images (SIXEL and iTerm2 IIP support)
589
+ // Loaded dynamically to avoid browser compatibility issues
590
+ this.loadImageAddon();
138
591
  this.githubLinksAddon = new GitHubLinksAddon((event, issueNumber) => {
592
+ this.dispatch({ id: XtermActionId.GITHUB_LINK_CLICK, payload: issueNumber });
593
+ });
594
+ this.terminal.loadAddon(this.githubLinksAddon);
595
+ this.fileLinksAddon = new FileLinksAddon((event, filePath, line, column) => {
139
596
  if (event.metaKey || event.ctrlKey) {
140
- this.githubLinkClick.emit(issueNumber);
597
+ this.dispatch({ id: XtermActionId.FILE_LINK_CLICK, payload: { filePath, line, column } });
141
598
  }
142
599
  });
143
- this.terminal.loadAddon(this.githubLinksAddon);
144
- const containerElement = this.container().nativeElement;
600
+ this.terminal.loadAddon(this.fileLinksAddon);
145
601
  this.terminal.open(containerElement);
146
- // 初期化後の処理
147
- setTimeout(() => {
148
- this.fitAddon.fit();
149
- this.terminalReady.emit(this.terminal);
150
- // ResizeObserverでサイズ変更を監視
151
- this.resizeObserver = new ResizeObserver(() => {
152
- try {
602
+ // Setup event handlers for interactive mode
603
+ if (isInteractive) {
604
+ this.terminal.onData((data) => {
605
+ this.dispatch({ id: XtermActionId.DATA_INPUT, payload: data });
606
+ });
607
+ this.terminal.onResize(({ cols, rows }) => this.dispatch({ id: XtermActionId.RESIZED, payload: { cols, rows } }));
608
+ }
609
+ // Use ResizeObserver with debounce to wait for container size to stabilize
610
+ // (CSS transitions take 300-500ms, so fixed setTimeout(100ms) is too early)
611
+ let stableTimer = null;
612
+ let resizeTimer = null;
613
+ this.resizeObserver = new ResizeObserver(() => {
614
+ try {
615
+ if (!this.terminalInitialized()) {
153
616
  this.fitAddon.fit();
617
+ // Debounce: wait for container to stop changing before initializing
618
+ if (stableTimer)
619
+ clearTimeout(stableTimer);
620
+ stableTimer = setTimeout(() => {
621
+ this.fitAddon.fit();
622
+ this.markInitialized(isInteractive);
623
+ }, 150);
154
624
  }
155
- catch (e) {
156
- console.warn('Failed to fit terminal:', e);
625
+ else if (isInteractive) {
626
+ // Debounce resize to wait for CSS transitions to complete (300ms)
627
+ if (resizeTimer)
628
+ clearTimeout(resizeTimer);
629
+ resizeTimer = setTimeout(() => {
630
+ this.fitAddon.fit();
631
+ this.dispatch({ id: XtermActionId.RESIZED, payload: { cols: this.cols, rows: this.rows } });
632
+ }, 300);
157
633
  }
158
- });
159
- this.resizeObserver.observe(containerElement);
160
- }, 100);
634
+ }
635
+ catch (e) {
636
+ console.warn('Failed to fit terminal:', e);
637
+ }
638
+ });
639
+ this.resizeObserver.observe(containerElement);
640
+ // Fallback: initialize after 500ms if ResizeObserver hasn't triggered stable state
641
+ setTimeout(() => {
642
+ if (!this.terminalInitialized()) {
643
+ this.fitAddon.fit();
644
+ this.markInitialized(isInteractive);
645
+ }
646
+ }, 500);
161
647
  }
162
648
  ngOnDestroy() {
163
649
  this.resizeObserver?.disconnect();
650
+ // Remove terminal instance from window for E2E testing cleanup
651
+ if (typeof window !== 'undefined' && window.__XTERM_INSTANCES__) {
652
+ const instances = window.__XTERM_INSTANCES__;
653
+ const index = instances.indexOf(this.terminal);
654
+ if (index !== -1) {
655
+ instances.splice(index, 1);
656
+ }
657
+ }
164
658
  this.terminal?.dispose();
165
659
  }
660
+ markInitialized(isInteractive) {
661
+ if (this.terminalInitialized())
662
+ return;
663
+ this.terminalInitialized.set(true);
664
+ this.terminalReady.emit(this.terminal);
665
+ // Expose terminal instance for E2E testing (only in development/test environments)
666
+ if (typeof window !== 'undefined') {
667
+ window.__XTERM_INSTANCES__ = window.__XTERM_INSTANCES__ || [];
668
+ window.__XTERM_INSTANCES__.push(this.terminal);
669
+ }
670
+ // Dispatch initial RESIZED for interactive mode
671
+ if (isInteractive) {
672
+ this.dispatch({ id: XtermActionId.RESIZED, payload: { cols: this.cols, rows: this.rows } });
673
+ }
674
+ }
675
+ async loadImageAddon() {
676
+ try {
677
+ const { ImageAddon } = await import('@xterm/addon-image');
678
+ const imageAddonOptions = {
679
+ enableSizeReports: true,
680
+ sixelSupport: true,
681
+ sixelScrolling: true,
682
+ sixelPaletteLimit: 256,
683
+ };
684
+ this.imageAddon = new ImageAddon(imageAddonOptions);
685
+ this.terminal.loadAddon(this.imageAddon);
686
+ }
687
+ catch (e) {
688
+ // ImageAddon not available in browser environment - this is expected
689
+ console.debug('ImageAddon not available:', e);
690
+ }
691
+ }
166
692
  // Public API
167
693
  write(text) {
168
694
  this.terminal?.write(text);
@@ -182,17 +708,291 @@ class XtermOrganism {
182
708
  scrollLines(lines) {
183
709
  this.terminal?.scrollLines(lines);
184
710
  }
711
+ fit() {
712
+ this.fitAddon?.fit();
713
+ // Only dispatch RESIZED after initialization to prevent sending wrong cols/rows
714
+ // during CSS transitions (SessionTemplateV2.ngAfterViewInit calls fit() before layout stabilizes)
715
+ if (this.terminalInitialized() && this.terminal) {
716
+ this.dispatch({ id: XtermActionId.RESIZED, payload: { cols: this.cols, rows: this.rows } });
717
+ }
718
+ }
719
+ get cols() {
720
+ return this.terminal?.cols ?? 80;
721
+ }
722
+ get rows() {
723
+ return this.terminal?.rows ?? 24;
724
+ }
725
+ getTerminal() {
726
+ // Only expose terminal after fitAddon.fit() has run (terminalInitialized === true)
727
+ return this.terminalInitialized() ? this.terminal : undefined;
728
+ }
729
+ /**
730
+ * Display an inline image using iTerm2's Inline Image Protocol
731
+ * @param base64Data Base64 encoded image data (PNG, JPEG, etc.)
732
+ * @param options Image display options
733
+ */
734
+ displayImage(base64Data, options) {
735
+ if (!this.terminal)
736
+ return;
737
+ const args = ['inline=1'];
738
+ if (options?.width) {
739
+ args.push(`width=${options.width}`);
740
+ }
741
+ if (options?.height) {
742
+ args.push(`height=${options.height}`);
743
+ }
744
+ if (options?.preserveAspectRatio !== undefined) {
745
+ args.push(`preserveAspectRatio=${options.preserveAspectRatio ? 1 : 0}`);
746
+ }
747
+ if (options?.name) {
748
+ args.push(`name=${btoa(options.name)}`);
749
+ }
750
+ // iTerm2 Inline Image Protocol escape sequence
751
+ const escapeSequence = `\x1b]1337;File=${args.join(';')}:${base64Data}\x07`;
752
+ this.terminal.write(escapeSequence);
753
+ }
754
+ /**
755
+ * Get the ImageAddon instance for advanced image operations
756
+ */
757
+ getImageAddon() {
758
+ return this.imageAddon;
759
+ }
185
760
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.0.5", ngImport: i0, type: XtermOrganism, deps: [], target: i0.ɵɵFactoryTarget.Component });
186
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.2.0", version: "20.0.5", type: XtermOrganism, isStandalone: true, selector: "organisms-xterm", outputs: { terminalReady: "terminalReady", githubLinkClick: "githubLinkClick" }, viewQueries: [{ propertyName: "container", first: true, predicate: ["container"], descendants: true, isSignal: true }], 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%}.xterm-wrapper{width:100%;height:100%;background:#0a0a0a;position:relative;overflow:hidden}.xterm-wrapper ::ng-deep .xterm-cursor-layer{display:none!important}.xterm-wrapper ::ng-deep .xterm-cursor,.xterm-wrapper ::ng-deep .xterm-cursor-outline{opacity:0!important}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush });
761
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.2.0", version: "20.0.5", 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 });
187
762
  }
188
763
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.0.5", ngImport: i0, type: XtermOrganism, decorators: [{
189
764
  type: Component,
190
- args: [{ selector: 'organisms-xterm', standalone: true, imports: [], template: `<div #container class="xterm-wrapper"></div>`, changeDetection: ChangeDetectionStrategy.OnPush, styles: ["@import\"https://unpkg.com/@xterm/xterm@5.5.0/css/xterm.css\";:host{display:block;width:100%;height:100%}.xterm-wrapper{width:100%;height:100%;background:#0a0a0a;position:relative;overflow:hidden}.xterm-wrapper ::ng-deep .xterm-cursor-layer{display:none!important}.xterm-wrapper ::ng-deep .xterm-cursor,.xterm-wrapper ::ng-deep .xterm-cursor-outline{opacity:0!important}\n"] }]
191
- }] });
765
+ args: [{ selector: 'organisms-xterm', standalone: true, imports: [], template: `<div #container class="xterm-wrapper"></div>`, changeDetection: ChangeDetectionStrategy.OnPush, hostDirectives: [{
766
+ directive: XtermOrganismStore,
767
+ inputs: ['data', 'interactive', 'useInteractiveTheme', 'totalWritten'],
768
+ }], 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"] }]
769
+ }], ctorParameters: () => [] });
770
+
771
+ /**
772
+ * Utility functions for displaying images in xterm using SIXEL format
773
+ */
774
+ /**
775
+ * Generate ASCII art text using figlet
776
+ * Note: figlet is loaded dynamically as it's not available in browser environments
777
+ */
778
+ async function generateAsciiArt(text, font = 'ANSI Shadow') {
779
+ try {
780
+ const figlet = await import('figlet');
781
+ return new Promise((resolve, reject) => {
782
+ figlet.default.text(text, { font: font }, (err, result) => {
783
+ if (err) {
784
+ reject(err);
785
+ return;
786
+ }
787
+ resolve(result || '');
788
+ });
789
+ });
790
+ }
791
+ catch (e) {
792
+ // figlet not available in browser - return simple text
793
+ console.debug('figlet not available:', e);
794
+ return text;
795
+ }
796
+ }
797
+ /**
798
+ * Convert ASCII art text to SVG with transparent background
799
+ */
800
+ function asciiArtToSvg(asciiArt, options = {}) {
801
+ const { textColor = '#00FFFF', fontSize = 14 } = options;
802
+ const lines = asciiArt.split('\n');
803
+ const maxLineLength = Math.max(...lines.map(l => l.length));
804
+ // Calculate dimensions based on monospace font metrics (block chars need more width)
805
+ const charWidth = fontSize * 0.65;
806
+ const lineHeight = fontSize * 1.4;
807
+ const padding = 40;
808
+ const width = Math.ceil(maxLineLength * charWidth) + padding;
809
+ const height = Math.ceil(lines.length * lineHeight) + padding;
810
+ // Build SVG with text elements for each line
811
+ const textElements = lines.map((line, i) => {
812
+ // Escape special XML characters
813
+ const escapedLine = line
814
+ .replace(/&/g, '&amp;')
815
+ .replace(/</g, '&lt;')
816
+ .replace(/>/g, '&gt;')
817
+ .replace(/"/g, '&quot;');
818
+ return `<text x="20" y="${20 + (i + 1) * lineHeight}" fill="${textColor}" xml:space="preserve">${escapedLine}</text>`;
819
+ }).join('\n ');
820
+ return `<svg width="${width}" height="${height}" xmlns="http://www.w3.org/2000/svg">
821
+ <style>text { font-family: 'Courier New', Consolas, monospace; font-size: ${fontSize}px; white-space: pre; }</style>
822
+ ${textElements}
823
+ </svg>`;
824
+ }
825
+ /**
826
+ * Convert SVG to SIXEL format for xterm display
827
+ */
828
+ async function svgToSixel(svgString, options = {}) {
829
+ const { width = 400, height = 200, backgroundColor } = options;
830
+ return new Promise((resolve, reject) => {
831
+ const canvas = document.createElement('canvas');
832
+ canvas.width = width;
833
+ canvas.height = height;
834
+ const ctx = canvas.getContext('2d');
835
+ if (!ctx) {
836
+ reject(new Error('Failed to get canvas context'));
837
+ return;
838
+ }
839
+ // Fill background (transparent if not specified)
840
+ if (backgroundColor) {
841
+ ctx.fillStyle = backgroundColor;
842
+ ctx.fillRect(0, 0, width, height);
843
+ }
844
+ const img = new Image();
845
+ const blob = new Blob([svgString], { type: 'image/svg+xml;charset=utf-8' });
846
+ const url = URL.createObjectURL(blob);
847
+ img.onload = () => {
848
+ ctx.drawImage(img, 0, 0, width, height);
849
+ URL.revokeObjectURL(url);
850
+ const imageData = ctx.getImageData(0, 0, width, height);
851
+ const sixelData = imageDataToSixel(imageData);
852
+ resolve(sixelData);
853
+ };
854
+ img.onerror = (error) => {
855
+ URL.revokeObjectURL(url);
856
+ reject(error);
857
+ };
858
+ img.src = url;
859
+ });
860
+ }
861
+ /**
862
+ * Convert ImageData to SIXEL escape sequence
863
+ */
864
+ function imageDataToSixel(imageData) {
865
+ const { width, height, data } = imageData;
866
+ // Build color palette (quantize to 16 colors)
867
+ const palette = buildColorPalette(data, 16);
868
+ const colorMap = new Map();
869
+ palette.forEach((color, index) => {
870
+ colorMap.set(color, index);
871
+ });
872
+ // SIXEL header
873
+ let sixel = '\x1bPq';
874
+ // Define colors in palette
875
+ palette.forEach((color, index) => {
876
+ const [r, g, b] = hexToRgb(color);
877
+ const rPct = Math.round((r / 255) * 100);
878
+ const gPct = Math.round((g / 255) * 100);
879
+ const bPct = Math.round((b / 255) * 100);
880
+ sixel += `#${index};2;${rPct};${gPct};${bPct}`;
881
+ });
882
+ // Process image in bands of 6 rows
883
+ for (let bandY = 0; bandY < height; bandY += 6) {
884
+ for (let colorIdx = 0; colorIdx < palette.length; colorIdx++) {
885
+ const color = palette[colorIdx];
886
+ let rowData = '';
887
+ let hasPixels = false;
888
+ rowData += `#${colorIdx}`;
889
+ for (let x = 0; x < width; x++) {
890
+ let sixelValue = 0;
891
+ for (let dy = 0; dy < 6; dy++) {
892
+ const y = bandY + dy;
893
+ if (y >= height)
894
+ continue;
895
+ const pixelIndex = (y * width + x) * 4;
896
+ const r = data[pixelIndex];
897
+ const g = data[pixelIndex + 1];
898
+ const b = data[pixelIndex + 2];
899
+ const a = data[pixelIndex + 3];
900
+ if (a < 128)
901
+ continue;
902
+ const pixelColor = rgbToHex(r, g, b);
903
+ const nearestColor = findNearestColor(pixelColor, palette);
904
+ if (nearestColor === color) {
905
+ sixelValue |= (1 << dy);
906
+ hasPixels = true;
907
+ }
908
+ }
909
+ rowData += String.fromCharCode(63 + sixelValue);
910
+ }
911
+ if (hasPixels) {
912
+ sixel += rowData;
913
+ sixel += '$';
914
+ }
915
+ }
916
+ if (bandY + 6 < height) {
917
+ sixel += '-';
918
+ }
919
+ }
920
+ sixel += '\x1b\\';
921
+ return sixel;
922
+ }
923
+ function buildColorPalette(data, maxColors) {
924
+ const colorCounts = new Map();
925
+ for (let i = 0; i < data.length; i += 4) {
926
+ const r = data[i];
927
+ const g = data[i + 1];
928
+ const b = data[i + 2];
929
+ const a = data[i + 3];
930
+ if (a < 128)
931
+ continue;
932
+ const qr = Math.floor(r / 16) * 16;
933
+ const qg = Math.floor(g / 16) * 16;
934
+ const qb = Math.floor(b / 16) * 16;
935
+ const color = rgbToHex(qr, qg, qb);
936
+ colorCounts.set(color, (colorCounts.get(color) || 0) + 1);
937
+ }
938
+ return Array.from(colorCounts.entries())
939
+ .sort((a, b) => b[1] - a[1])
940
+ .slice(0, maxColors)
941
+ .map(([color]) => color);
942
+ }
943
+ function rgbToHex(r, g, b) {
944
+ return `#${r.toString(16).padStart(2, '0')}${g.toString(16).padStart(2, '0')}${b.toString(16).padStart(2, '0')}`;
945
+ }
946
+ function hexToRgb(hex) {
947
+ const match = hex.match(/^#([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})$/i);
948
+ if (!match)
949
+ return [0, 0, 0];
950
+ return [parseInt(match[1], 16), parseInt(match[2], 16), parseInt(match[3], 16)];
951
+ }
952
+ function findNearestColor(targetHex, palette) {
953
+ const [tr, tg, tb] = hexToRgb(targetHex);
954
+ let nearest = palette[0];
955
+ let minDist = Infinity;
956
+ for (const color of palette) {
957
+ const [r, g, b] = hexToRgb(color);
958
+ const dist = (tr - r) ** 2 + (tg - g) ** 2 + (tb - b) ** 2;
959
+ if (dist < minDist) {
960
+ minDist = dist;
961
+ nearest = color;
962
+ }
963
+ }
964
+ return nearest;
965
+ }
966
+ /**
967
+ * Generate MACHINA ASCII art banner as SIXEL
968
+ */
969
+ async function getMachinaAsciiArtSixel(options = {}) {
970
+ // Generate ASCII art for "MACHINA"
971
+ const machinaArt = await generateAsciiArt('MACHINA', 'ANSI Shadow');
972
+ // Convert to SVG with cyan color (terminal style)
973
+ const fontSize = 14;
974
+ const svg = asciiArtToSvg(machinaArt, {
975
+ textColor: options.textColor || '#00FFFF',
976
+ fontSize
977
+ });
978
+ // Calculate appropriate dimensions matching asciiArtToSvg calculation
979
+ const lines = machinaArt.split('\n');
980
+ const maxLineLength = Math.max(...lines.map(l => l.length));
981
+ const charWidth = fontSize * 0.65;
982
+ const lineHeight = fontSize * 1.4;
983
+ const padding = 40;
984
+ const defaultWidth = Math.ceil(maxLineLength * charWidth) + padding;
985
+ const defaultHeight = Math.ceil(lines.length * lineHeight) + padding;
986
+ return svgToSixel(svg, {
987
+ width: options.width || defaultWidth,
988
+ height: options.height || defaultHeight,
989
+ backgroundColor: options.backgroundColor || '#0a0a0a',
990
+ });
991
+ }
192
992
 
193
993
  /**
194
994
  * Generated bundle index. Do not edit.
195
995
  */
196
996
 
197
- export { GitHubLinkProvider, GitHubLinksAddon, XtermOrganism };
997
+ export { GitHubLinkProvider, GitHubLinksAddon, WebLinksAddon, XtermActionId, XtermOrganism, XtermOrganismStore, asciiArtToSvg, generateAsciiArt, getMachinaAsciiArtSixel, svgToSixel };
198
998
  //# sourceMappingURL=xxmachina-components-organisms-xterm.mjs.map